titlebar.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { createEffect, createMemo, Show } from "solid-js"
  2. import { IconButton } from "@opencode-ai/ui/icon-button"
  3. import { Icon } from "@opencode-ai/ui/icon"
  4. import { Button } from "@opencode-ai/ui/button"
  5. import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
  6. import { useTheme } from "@opencode-ai/ui/theme"
  7. import { useLayout } from "@/context/layout"
  8. import { usePlatform } from "@/context/platform"
  9. import { useCommand } from "@/context/command"
  10. import { useLanguage } from "@/context/language"
  11. export function Titlebar() {
  12. const layout = useLayout()
  13. const platform = usePlatform()
  14. const command = useCommand()
  15. const language = useLanguage()
  16. const theme = useTheme()
  17. const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
  18. const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
  19. const reserve = createMemo(
  20. () => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"),
  21. )
  22. const web = createMemo(() => platform.platform === "web")
  23. const getWin = () => {
  24. if (platform.platform !== "desktop") return
  25. const tauri = (
  26. window as unknown as {
  27. __TAURI__?: { window?: { getCurrentWindow?: () => { startDragging?: () => Promise<void> } } }
  28. }
  29. ).__TAURI__
  30. if (!tauri?.window?.getCurrentWindow) return
  31. return tauri.window.getCurrentWindow()
  32. }
  33. createEffect(() => {
  34. if (platform.platform !== "desktop") return
  35. const scheme = theme.colorScheme()
  36. const value = scheme === "system" ? null : scheme
  37. const tauri = (window as unknown as { __TAURI__?: { webviewWindow?: { getCurrentWebviewWindow?: () => unknown } } })
  38. .__TAURI__
  39. const get = tauri?.webviewWindow?.getCurrentWebviewWindow
  40. if (!get) return
  41. const win = get() as { setTheme?: (theme?: "light" | "dark" | null) => Promise<void> }
  42. if (!win.setTheme) return
  43. void win.setTheme(value).catch(() => undefined)
  44. })
  45. const interactive = (target: EventTarget | null) => {
  46. if (!(target instanceof Element)) return false
  47. const selector =
  48. "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']"
  49. return !!target.closest(selector)
  50. }
  51. const drag = (e: MouseEvent) => {
  52. if (platform.platform !== "desktop") return
  53. if (e.buttons !== 1) return
  54. if (interactive(e.target)) return
  55. const win = getWin()
  56. if (!win?.startDragging) return
  57. e.preventDefault()
  58. void win.startDragging().catch(() => undefined)
  59. }
  60. return (
  61. <header class="h-10 shrink-0 bg-background-base flex items-center relative" data-tauri-drag-region>
  62. <div
  63. classList={{
  64. "flex items-center w-full min-w-0": true,
  65. "pl-2": !mac(),
  66. "pr-2": !windows(),
  67. }}
  68. onMouseDown={drag}
  69. data-tauri-drag-region
  70. >
  71. <Show when={mac()}>
  72. <div class="w-[72px] h-full shrink-0" data-tauri-drag-region />
  73. <div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
  74. <IconButton
  75. icon="menu"
  76. variant="ghost"
  77. class="size-8 rounded-md"
  78. onClick={layout.mobileSidebar.toggle}
  79. aria-label={language.t("sidebar.menu.toggle")}
  80. />
  81. </div>
  82. </Show>
  83. <Show when={!mac()}>
  84. <div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
  85. <IconButton
  86. icon="menu"
  87. variant="ghost"
  88. class="size-8 rounded-md"
  89. onClick={layout.mobileSidebar.toggle}
  90. aria-label={language.t("sidebar.menu.toggle")}
  91. />
  92. </div>
  93. </Show>
  94. <TooltipKeybind
  95. class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
  96. placement="bottom"
  97. title={language.t("command.sidebar.toggle")}
  98. keybind={command.keybind("sidebar.toggle")}
  99. >
  100. <Button
  101. variant="ghost"
  102. class="group/sidebar-toggle size-6 p-0"
  103. onClick={layout.sidebar.toggle}
  104. aria-label={language.t("command.sidebar.toggle")}
  105. aria-expanded={layout.sidebar.opened()}
  106. >
  107. <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
  108. <Icon
  109. size="small"
  110. name={layout.sidebar.opened() ? "layout-left-full" : "layout-left"}
  111. class="group-hover/sidebar-toggle:hidden"
  112. />
  113. <Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
  114. <Icon
  115. size="small"
  116. name={layout.sidebar.opened() ? "layout-left" : "layout-left-full"}
  117. class="hidden group-active/sidebar-toggle:inline-block"
  118. />
  119. </div>
  120. </Button>
  121. </TooltipKeybind>
  122. <div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
  123. <div class="flex-1 h-full" data-tauri-drag-region />
  124. <div
  125. id="opencode-titlebar-right"
  126. class="flex items-center gap-3 shrink-0 flex-1 justify-end"
  127. data-tauri-drag-region
  128. />
  129. <Show when={windows()}>
  130. <div data-tauri-decorum-tb class="flex flex-row" />
  131. </Show>
  132. </div>
  133. <div class="absolute inset-0 flex items-center justify-center pointer-events-none">
  134. <div id="opencode-titlebar-center" class="pointer-events-auto" />
  135. </div>
  136. </header>
  137. )
  138. }