windows-app-menu.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { Show, type JSX } from "solid-js"
  2. import { DropdownMenu } from "@kirincode-ai/ui/dropdown-menu"
  3. import { Icon } from "@kirincode-ai/ui/icon"
  4. import { IconButton } from "@kirincode-ai/ui/icon-button"
  5. import { IconButtonV2 } from "@kirincode-ai/ui/v2/icon-button-v2"
  6. import { Icon as IconV2 } from "@kirincode-ai/ui/v2/icon"
  7. import { useCommand } from "@/context/command"
  8. import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu"
  9. import { usePlatform } from "@/context/platform"
  10. export function WindowsAppMenu(props: {
  11. command: ReturnType<typeof useCommand>
  12. platform: ReturnType<typeof usePlatform>
  13. variant?: "legacy" | "v2"
  14. }) {
  15. let lastFocused: HTMLElement | undefined
  16. const rememberFocus = () => {
  17. const active = document.activeElement
  18. lastFocused = active instanceof HTMLElement ? active : undefined
  19. }
  20. const commandDisabled = (id: string) => {
  21. const option = props.command.options.find((option) => option.id === id)
  22. if (!option) return true
  23. return option.disabled ?? false
  24. }
  25. const runCommand = (id: string) => {
  26. if (commandDisabled(id)) return
  27. props.command.trigger(id)
  28. }
  29. const runAction = (action: DesktopMenuAction) => {
  30. if (action.startsWith("edit.") && lastFocused?.isConnected) lastFocused.focus({ preventScroll: true })
  31. void props.platform.runDesktopMenuAction?.(action)
  32. }
  33. const runEntry = (entry: DesktopMenuEntry) => {
  34. if (entry.type === "separator") return
  35. if (entry.command) {
  36. runCommand(entry.command)
  37. return
  38. }
  39. if (entry.action) {
  40. runAction(entry.action)
  41. return
  42. }
  43. if (entry.href) props.platform.openLink(entry.href)
  44. }
  45. return (
  46. <DropdownMenu gutter={4} modal={false} placement="bottom-start">
  47. {props.variant === "v2" ? (
  48. <div
  49. data-component="desktop-icon-button"
  50. class="flex h-7 w-9 shrink-0 items-center justify-center rounded-[6px] px-1"
  51. >
  52. <DropdownMenu.Trigger
  53. as={IconButtonV2}
  54. variant="ghost-muted"
  55. size="large"
  56. icon={<IconV2 name="menu" />}
  57. aria-label="KirinCode menu"
  58. onPointerDown={rememberFocus}
  59. onKeyDown={rememberFocus}
  60. />
  61. </div>
  62. ) : (
  63. <DropdownMenu.Trigger
  64. as={IconButton}
  65. icon="menu"
  66. variant="ghost"
  67. class="titlebar-icon rounded-md shrink-0"
  68. aria-label="KirinCode menu"
  69. onPointerDown={rememberFocus}
  70. onKeyDown={rememberFocus}
  71. />
  72. )}
  73. <DropdownMenu.Portal>
  74. <DropdownMenu.Content class="desktop-app-menu">
  75. <DropdownMenu.Group>
  76. <DropdownMenu.GroupLabel class="desktop-app-menu-heading">KirinCode</DropdownMenu.GroupLabel>
  77. {DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows")).map((menu) => (
  78. <DesktopMenuSubmenu label={menu.label}>
  79. {menu.items
  80. ?.filter((entry) => desktopMenuVisible(entry, "windows"))
  81. .map((entry) =>
  82. entry.type === "separator" ? (
  83. <DropdownMenu.Separator />
  84. ) : (
  85. <DesktopMenuItem
  86. label={entry.label ?? ""}
  87. keybind={entry.command ? props.command.keybind(entry.command) : entry.accelerator?.windows}
  88. disabled={entry.command ? commandDisabled(entry.command) : false}
  89. onSelect={() => runEntry(entry)}
  90. />
  91. ),
  92. )}
  93. </DesktopMenuSubmenu>
  94. ))}
  95. </DropdownMenu.Group>
  96. </DropdownMenu.Content>
  97. </DropdownMenu.Portal>
  98. </DropdownMenu>
  99. )
  100. }
  101. function DesktopMenuSubmenu(props: { label: string; children: JSX.Element }) {
  102. return (
  103. <DropdownMenu.Sub>
  104. <DropdownMenu.SubTrigger>
  105. <span data-slot="dropdown-menu-item-label">{props.label}</span>
  106. <span data-slot="desktop-app-menu-chevron">
  107. <Icon name="chevron-right" size="small" />
  108. </span>
  109. </DropdownMenu.SubTrigger>
  110. <DropdownMenu.Portal>
  111. <DropdownMenu.SubContent class="desktop-app-menu">{props.children}</DropdownMenu.SubContent>
  112. </DropdownMenu.Portal>
  113. </DropdownMenu.Sub>
  114. )
  115. }
  116. function DesktopMenuItem(props: { label: string; keybind?: string; disabled?: boolean; onSelect: () => void }) {
  117. return (
  118. <DropdownMenu.Item disabled={props.disabled} onSelect={props.onSelect}>
  119. <DropdownMenu.ItemLabel>{props.label}</DropdownMenu.ItemLabel>
  120. <Show when={props.keybind}>
  121. <span data-slot="desktop-app-menu-keybind">{props.keybind}</span>
  122. </Show>
  123. </DropdownMenu.Item>
  124. )
  125. }