@jonsoc/app 1.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/AGENTS.md +30 -0
  2. package/README.md +51 -0
  3. package/bunfig.toml +2 -0
  4. package/e2e/context.spec.ts +45 -0
  5. package/e2e/file-open.spec.ts +23 -0
  6. package/e2e/file-viewer.spec.ts +35 -0
  7. package/e2e/fixtures.ts +40 -0
  8. package/e2e/home.spec.ts +21 -0
  9. package/e2e/model-picker.spec.ts +43 -0
  10. package/e2e/navigation.spec.ts +9 -0
  11. package/e2e/palette.spec.ts +15 -0
  12. package/e2e/prompt-mention.spec.ts +26 -0
  13. package/e2e/prompt-slash-open.spec.ts +22 -0
  14. package/e2e/prompt.spec.ts +62 -0
  15. package/e2e/session.spec.ts +21 -0
  16. package/e2e/settings.spec.ts +44 -0
  17. package/e2e/sidebar.spec.ts +21 -0
  18. package/e2e/terminal-init.spec.ts +25 -0
  19. package/e2e/terminal.spec.ts +16 -0
  20. package/e2e/tsconfig.json +8 -0
  21. package/e2e/utils.ts +38 -0
  22. package/happydom.ts +75 -0
  23. package/index.html +23 -0
  24. package/package.json +72 -0
  25. package/playwright.config.ts +43 -0
  26. package/public/_headers +17 -0
  27. package/public/apple-touch-icon-v3.png +1 -0
  28. package/public/apple-touch-icon.png +1 -0
  29. package/public/favicon-96x96-v3.png +1 -0
  30. package/public/favicon-96x96.png +1 -0
  31. package/public/favicon-v3.ico +1 -0
  32. package/public/favicon-v3.svg +1 -0
  33. package/public/favicon.ico +1 -0
  34. package/public/favicon.svg +1 -0
  35. package/public/oc-theme-preload.js +28 -0
  36. package/public/site.webmanifest +1 -0
  37. package/public/social-share-zen.png +1 -0
  38. package/public/social-share.png +1 -0
  39. package/public/web-app-manifest-192x192.png +1 -0
  40. package/public/web-app-manifest-512x512.png +1 -0
  41. package/script/e2e-local.ts +143 -0
  42. package/src/addons/serialize.test.ts +319 -0
  43. package/src/addons/serialize.ts +591 -0
  44. package/src/app.tsx +150 -0
  45. package/src/components/dialog-connect-provider.tsx +428 -0
  46. package/src/components/dialog-edit-project.tsx +259 -0
  47. package/src/components/dialog-fork.tsx +104 -0
  48. package/src/components/dialog-manage-models.tsx +59 -0
  49. package/src/components/dialog-select-directory.tsx +208 -0
  50. package/src/components/dialog-select-file.tsx +196 -0
  51. package/src/components/dialog-select-mcp.tsx +96 -0
  52. package/src/components/dialog-select-model-unpaid.tsx +130 -0
  53. package/src/components/dialog-select-model.tsx +162 -0
  54. package/src/components/dialog-select-provider.tsx +70 -0
  55. package/src/components/dialog-select-server.tsx +249 -0
  56. package/src/components/dialog-settings.tsx +112 -0
  57. package/src/components/file-tree.tsx +112 -0
  58. package/src/components/link.tsx +17 -0
  59. package/src/components/model-tooltip.tsx +91 -0
  60. package/src/components/prompt-input.tsx +2076 -0
  61. package/src/components/session/index.ts +5 -0
  62. package/src/components/session/session-context-tab.tsx +428 -0
  63. package/src/components/session/session-header.tsx +343 -0
  64. package/src/components/session/session-new-view.tsx +93 -0
  65. package/src/components/session/session-sortable-tab.tsx +56 -0
  66. package/src/components/session/session-sortable-terminal-tab.tsx +187 -0
  67. package/src/components/session-context-usage.tsx +113 -0
  68. package/src/components/session-lsp-indicator.tsx +42 -0
  69. package/src/components/session-mcp-indicator.tsx +34 -0
  70. package/src/components/settings-agents.tsx +15 -0
  71. package/src/components/settings-commands.tsx +15 -0
  72. package/src/components/settings-general.tsx +306 -0
  73. package/src/components/settings-keybinds.tsx +437 -0
  74. package/src/components/settings-mcp.tsx +15 -0
  75. package/src/components/settings-models.tsx +15 -0
  76. package/src/components/settings-permissions.tsx +234 -0
  77. package/src/components/settings-providers.tsx +15 -0
  78. package/src/components/terminal.tsx +315 -0
  79. package/src/components/titlebar.tsx +156 -0
  80. package/src/context/command.tsx +308 -0
  81. package/src/context/comments.tsx +140 -0
  82. package/src/context/file.tsx +409 -0
  83. package/src/context/global-sdk.tsx +106 -0
  84. package/src/context/global-sync.tsx +898 -0
  85. package/src/context/language.tsx +161 -0
  86. package/src/context/layout-scroll.test.ts +73 -0
  87. package/src/context/layout-scroll.ts +118 -0
  88. package/src/context/layout.tsx +648 -0
  89. package/src/context/local.tsx +578 -0
  90. package/src/context/notification.tsx +173 -0
  91. package/src/context/permission.tsx +167 -0
  92. package/src/context/platform.tsx +59 -0
  93. package/src/context/prompt.tsx +245 -0
  94. package/src/context/sdk.tsx +48 -0
  95. package/src/context/server.tsx +214 -0
  96. package/src/context/settings.tsx +166 -0
  97. package/src/context/sync.tsx +320 -0
  98. package/src/context/terminal.tsx +267 -0
  99. package/src/custom-elements.d.ts +17 -0
  100. package/src/entry.tsx +76 -0
  101. package/src/env.d.ts +8 -0
  102. package/src/hooks/use-providers.ts +31 -0
  103. package/src/i18n/ar.ts +656 -0
  104. package/src/i18n/br.ts +667 -0
  105. package/src/i18n/da.ts +582 -0
  106. package/src/i18n/de.ts +591 -0
  107. package/src/i18n/en.ts +665 -0
  108. package/src/i18n/es.ts +585 -0
  109. package/src/i18n/fr.ts +592 -0
  110. package/src/i18n/ja.ts +579 -0
  111. package/src/i18n/ko.ts +580 -0
  112. package/src/i18n/no.ts +602 -0
  113. package/src/i18n/pl.ts +661 -0
  114. package/src/i18n/ru.ts +664 -0
  115. package/src/i18n/zh.ts +574 -0
  116. package/src/i18n/zht.ts +570 -0
  117. package/src/index.css +57 -0
  118. package/src/index.ts +2 -0
  119. package/src/pages/directory-layout.tsx +57 -0
  120. package/src/pages/error.tsx +290 -0
  121. package/src/pages/home.tsx +125 -0
  122. package/src/pages/layout.tsx +2599 -0
  123. package/src/pages/session.tsx +2505 -0
  124. package/src/sst-env.d.ts +10 -0
  125. package/src/utils/dom.ts +51 -0
  126. package/src/utils/id.ts +99 -0
  127. package/src/utils/index.ts +1 -0
  128. package/src/utils/perf.ts +135 -0
  129. package/src/utils/persist.ts +377 -0
  130. package/src/utils/prompt.ts +203 -0
  131. package/src/utils/same.ts +6 -0
  132. package/src/utils/solid-dnd.tsx +55 -0
  133. package/src/utils/sound.ts +110 -0
  134. package/src/utils/speech.ts +302 -0
  135. package/src/utils/worktree.ts +58 -0
  136. package/sst-env.d.ts +9 -0
  137. package/tsconfig.json +26 -0
  138. package/vite.config.ts +15 -0
  139. package/vite.js +26 -0
@@ -0,0 +1,15 @@
1
+ import { Component } from "solid-js"
2
+ import { useLanguage } from "@/context/language"
3
+
4
+ export const SettingsProviders: Component = () => {
5
+ const language = useLanguage()
6
+
7
+ return (
8
+ <div class="flex flex-col h-full overflow-y-auto">
9
+ <div class="flex flex-col gap-6 p-6 max-w-[600px]">
10
+ <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
11
+ <p class="text-14-regular text-text-weak">{language.t("settings.providers.description")}</p>
12
+ </div>
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,315 @@
1
+ import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
2
+ import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
3
+ import { useSDK } from "@/context/sdk"
4
+ import { monoFontFamily, useSettings } from "@/context/settings"
5
+ import { SerializeAddon } from "@/addons/serialize"
6
+ import { LocalPTY } from "@/context/terminal"
7
+ import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@jonsoc/ui/theme"
8
+
9
+ export interface TerminalProps extends ComponentProps<"div"> {
10
+ pty: LocalPTY
11
+ onSubmit?: () => void
12
+ onCleanup?: (pty: LocalPTY) => void
13
+ onConnect?: () => void
14
+ onConnectError?: (error: unknown) => void
15
+ }
16
+
17
+ type TerminalColors = {
18
+ background: string
19
+ foreground: string
20
+ cursor: string
21
+ selectionBackground: string
22
+ }
23
+
24
+ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
25
+ light: {
26
+ background: "#fcfcfc",
27
+ foreground: "#211e1e",
28
+ cursor: "#211e1e",
29
+ selectionBackground: withAlpha("#211e1e", 0.2),
30
+ },
31
+ dark: {
32
+ background: "#191515",
33
+ foreground: "#d4d4d4",
34
+ cursor: "#d4d4d4",
35
+ selectionBackground: withAlpha("#d4d4d4", 0.25),
36
+ },
37
+ }
38
+
39
+ export const Terminal = (props: TerminalProps) => {
40
+ const sdk = useSDK()
41
+ const settings = useSettings()
42
+ const theme = useTheme()
43
+ let container!: HTMLDivElement
44
+ const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
45
+ let ws: WebSocket | undefined
46
+ let term: Term | undefined
47
+ let ghostty: Ghostty
48
+ let serializeAddon: SerializeAddon
49
+ let fitAddon: FitAddon
50
+ let handleResize: () => void
51
+ let handleTextareaFocus: () => void
52
+ let handleTextareaBlur: () => void
53
+ let reconnect: number | undefined
54
+ let disposed = false
55
+
56
+ const getTerminalColors = (): TerminalColors => {
57
+ const mode = theme.mode()
58
+ const fallback = DEFAULT_TERMINAL_COLORS[mode]
59
+ const currentTheme = theme.themes()[theme.themeId()]
60
+ if (!currentTheme) return fallback
61
+ const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
62
+ if (!variant?.seeds) return fallback
63
+ const resolved = resolveThemeVariant(variant, mode === "dark")
64
+ const text = resolved["text-stronger"] ?? fallback.foreground
65
+ const background = resolved["background-stronger"] ?? fallback.background
66
+ const alpha = mode === "dark" ? 0.25 : 0.2
67
+ const base = text.startsWith("#") ? (text as HexColor) : (fallback.foreground as HexColor)
68
+ const selectionBackground = withAlpha(base, alpha)
69
+ return {
70
+ background,
71
+ foreground: text,
72
+ cursor: text,
73
+ selectionBackground,
74
+ }
75
+ }
76
+
77
+ const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors())
78
+
79
+ createEffect(() => {
80
+ const colors = getTerminalColors()
81
+ setTerminalColors(colors)
82
+ if (!term) return
83
+ const setOption = (term as unknown as { setOption?: (key: string, value: TerminalColors) => void }).setOption
84
+ if (!setOption) return
85
+ setOption("theme", colors)
86
+ })
87
+
88
+ createEffect(() => {
89
+ const font = monoFontFamily(settings.appearance.font())
90
+ if (!term) return
91
+ const setOption = (term as unknown as { setOption?: (key: string, value: string) => void }).setOption
92
+ if (!setOption) return
93
+ setOption("fontFamily", font)
94
+ })
95
+
96
+ const focusTerminal = () => {
97
+ const t = term
98
+ if (!t) return
99
+ t.focus()
100
+ setTimeout(() => t.textarea?.focus(), 0)
101
+ }
102
+ const handlePointerDown = () => {
103
+ const activeElement = document.activeElement
104
+ if (activeElement instanceof HTMLElement && activeElement !== container) {
105
+ activeElement.blur()
106
+ }
107
+ focusTerminal()
108
+ }
109
+
110
+ onMount(async () => {
111
+ const mod = await import("ghostty-web")
112
+ ghostty = await mod.Ghostty.load()
113
+
114
+ const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
115
+ if (window.__OPENCODE__?.serverPassword) {
116
+ url.username = "jonsoc"
117
+ url.password = window.__OPENCODE__?.serverPassword
118
+ }
119
+ const socket = new WebSocket(url)
120
+ ws = socket
121
+
122
+ const t = new mod.Terminal({
123
+ cursorBlink: true,
124
+ cursorStyle: "bar",
125
+ fontSize: 14,
126
+ fontFamily: monoFontFamily(settings.appearance.font()),
127
+ allowTransparency: true,
128
+ theme: terminalColors(),
129
+ scrollback: 10_000,
130
+ ghostty,
131
+ })
132
+ term = t
133
+
134
+ const copy = () => {
135
+ const selection = t.getSelection()
136
+ if (!selection) return false
137
+
138
+ const body = document.body
139
+ if (body) {
140
+ const textarea = document.createElement("textarea")
141
+ textarea.value = selection
142
+ textarea.setAttribute("readonly", "")
143
+ textarea.style.position = "fixed"
144
+ textarea.style.opacity = "0"
145
+ body.appendChild(textarea)
146
+ textarea.select()
147
+ const copied = document.execCommand("copy")
148
+ body.removeChild(textarea)
149
+ if (copied) return true
150
+ }
151
+
152
+ const clipboard = navigator.clipboard
153
+ if (clipboard?.writeText) {
154
+ clipboard.writeText(selection).catch(() => {})
155
+ return true
156
+ }
157
+
158
+ return false
159
+ }
160
+
161
+ t.attachCustomKeyEventHandler((event) => {
162
+ const key = event.key.toLowerCase()
163
+
164
+ if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") {
165
+ copy()
166
+ return true
167
+ }
168
+
169
+ if (event.metaKey && !event.ctrlKey && !event.altKey && key === "c") {
170
+ if (!t.hasSelection()) return true
171
+ copy()
172
+ return true
173
+ }
174
+
175
+ // allow for ctrl-` to toggle terminal in parent
176
+ if (event.ctrlKey && key === "`") {
177
+ return true
178
+ }
179
+
180
+ return false
181
+ })
182
+
183
+ fitAddon = new mod.FitAddon()
184
+ serializeAddon = new SerializeAddon()
185
+ t.loadAddon(serializeAddon)
186
+ t.loadAddon(fitAddon)
187
+
188
+ t.open(container)
189
+ container.addEventListener("pointerdown", handlePointerDown)
190
+
191
+ handleTextareaFocus = () => {
192
+ t.options.cursorBlink = true
193
+ }
194
+ handleTextareaBlur = () => {
195
+ t.options.cursorBlink = false
196
+ }
197
+
198
+ t.textarea?.addEventListener("focus", handleTextareaFocus)
199
+ t.textarea?.addEventListener("blur", handleTextareaBlur)
200
+
201
+ focusTerminal()
202
+
203
+ if (local.pty.buffer) {
204
+ if (local.pty.rows && local.pty.cols) {
205
+ t.resize(local.pty.cols, local.pty.rows)
206
+ }
207
+ t.write(local.pty.buffer, () => {
208
+ if (local.pty.scrollY) {
209
+ t.scrollToLine(local.pty.scrollY)
210
+ }
211
+ fitAddon.fit()
212
+ })
213
+ }
214
+
215
+ fitAddon.observeResize()
216
+ handleResize = () => fitAddon.fit()
217
+ window.addEventListener("resize", handleResize)
218
+ t.onResize(async (size) => {
219
+ if (socket.readyState === WebSocket.OPEN) {
220
+ await sdk.client.pty
221
+ .update({
222
+ ptyID: local.pty.id,
223
+ size: {
224
+ cols: size.cols,
225
+ rows: size.rows,
226
+ },
227
+ })
228
+ .catch(() => {})
229
+ }
230
+ })
231
+ t.onData((data) => {
232
+ if (socket.readyState === WebSocket.OPEN) {
233
+ socket.send(data)
234
+ }
235
+ })
236
+ t.onKey((key) => {
237
+ if (key.key == "Enter") {
238
+ props.onSubmit?.()
239
+ }
240
+ })
241
+ // t.onScroll((ydisp) => {
242
+ // console.log("Scroll position:", ydisp)
243
+ // })
244
+ socket.addEventListener("open", () => {
245
+ local.onConnect?.()
246
+ sdk.client.pty
247
+ .update({
248
+ ptyID: local.pty.id,
249
+ size: {
250
+ cols: t.cols,
251
+ rows: t.rows,
252
+ },
253
+ })
254
+ .catch(() => {})
255
+ })
256
+ socket.addEventListener("message", (event) => {
257
+ t.write(event.data)
258
+ })
259
+ socket.addEventListener("error", (error) => {
260
+ if (disposed) return
261
+ console.error("WebSocket error:", error)
262
+ local.onConnectError?.(error)
263
+ })
264
+ socket.addEventListener("close", (event) => {
265
+ if (disposed) return
266
+ // Normal closure (code 1000) means PTY process exited - server event handles cleanup
267
+ // For other codes (network issues, server restart), trigger error handler
268
+ if (event.code !== 1000) {
269
+ local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
270
+ }
271
+ })
272
+ })
273
+
274
+ onCleanup(() => {
275
+ disposed = true
276
+ if (handleResize) {
277
+ window.removeEventListener("resize", handleResize)
278
+ }
279
+ container.removeEventListener("pointerdown", handlePointerDown)
280
+ term?.textarea?.removeEventListener("focus", handleTextareaFocus)
281
+ term?.textarea?.removeEventListener("blur", handleTextareaBlur)
282
+
283
+ const t = term
284
+ if (serializeAddon && props.onCleanup && t) {
285
+ const buffer = serializeAddon.serialize()
286
+ props.onCleanup({
287
+ ...local.pty,
288
+ buffer,
289
+ rows: t.rows,
290
+ cols: t.cols,
291
+ scrollY: t.getViewportY(),
292
+ })
293
+ }
294
+
295
+ ws?.close()
296
+ t?.dispose()
297
+ })
298
+
299
+ return (
300
+ <div
301
+ ref={container}
302
+ data-component="terminal"
303
+ data-prevent-autofocus
304
+ tabIndex={-1}
305
+ style={{ "background-color": terminalColors().background }}
306
+ classList={{
307
+ ...(local.classList ?? {}),
308
+ "select-text": true,
309
+ "size-full px-6 py-3 font-mono": true,
310
+ [local.class ?? ""]: !!local.class,
311
+ }}
312
+ {...others}
313
+ />
314
+ )
315
+ }
@@ -0,0 +1,156 @@
1
+ import { createEffect, createMemo, Show } from "solid-js"
2
+ import { IconButton } from "@jonsoc/ui/icon-button"
3
+ import { Icon } from "@jonsoc/ui/icon"
4
+ import { Button } from "@jonsoc/ui/button"
5
+ import { TooltipKeybind } from "@jonsoc/ui/tooltip"
6
+ import { useTheme } from "@jonsoc/ui/theme"
7
+
8
+ import { useLayout } from "@/context/layout"
9
+ import { usePlatform } from "@/context/platform"
10
+ import { useCommand } from "@/context/command"
11
+ import { useLanguage } from "@/context/language"
12
+
13
+ export function Titlebar() {
14
+ const layout = useLayout()
15
+ const platform = usePlatform()
16
+ const command = useCommand()
17
+ const language = useLanguage()
18
+ const theme = useTheme()
19
+
20
+ const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
21
+ const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
22
+ const reserve = createMemo(
23
+ () => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"),
24
+ )
25
+ const web = createMemo(() => platform.platform === "web")
26
+
27
+ const getWin = () => {
28
+ if (platform.platform !== "desktop") return
29
+
30
+ const tauri = (
31
+ window as unknown as {
32
+ __TAURI__?: { window?: { getCurrentWindow?: () => { startDragging?: () => Promise<void> } } }
33
+ }
34
+ ).__TAURI__
35
+ if (!tauri?.window?.getCurrentWindow) return
36
+
37
+ return tauri.window.getCurrentWindow()
38
+ }
39
+
40
+ createEffect(() => {
41
+ if (platform.platform !== "desktop") return
42
+
43
+ const scheme = theme.colorScheme()
44
+ const value = scheme === "system" ? null : scheme
45
+
46
+ const tauri = (window as unknown as { __TAURI__?: { webviewWindow?: { getCurrentWebviewWindow?: () => unknown } } })
47
+ .__TAURI__
48
+ const get = tauri?.webviewWindow?.getCurrentWebviewWindow
49
+ if (!get) return
50
+
51
+ const win = get() as { setTheme?: (theme?: "light" | "dark" | null) => Promise<void> }
52
+ if (!win.setTheme) return
53
+
54
+ void win.setTheme(value).catch(() => undefined)
55
+ })
56
+
57
+ const interactive = (target: EventTarget | null) => {
58
+ if (!(target instanceof Element)) return false
59
+
60
+ const selector =
61
+ "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']"
62
+
63
+ return !!target.closest(selector)
64
+ }
65
+
66
+ const drag = (e: MouseEvent) => {
67
+ if (platform.platform !== "desktop") return
68
+ if (e.buttons !== 1) return
69
+ if (interactive(e.target)) return
70
+
71
+ const win = getWin()
72
+ if (!win?.startDragging) return
73
+
74
+ e.preventDefault()
75
+ void win.startDragging().catch(() => undefined)
76
+ }
77
+
78
+ return (
79
+ <header class="h-10 shrink-0 bg-background-base flex items-center relative" data-tauri-drag-region>
80
+ <div
81
+ classList={{
82
+ "flex items-center w-full min-w-0": true,
83
+ "pl-2": !mac(),
84
+ "pr-2": !windows(),
85
+ }}
86
+ onMouseDown={drag}
87
+ data-tauri-drag-region
88
+ >
89
+ <Show when={mac()}>
90
+ <div class="w-[72px] h-full shrink-0" data-tauri-drag-region />
91
+ <div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
92
+ <IconButton
93
+ icon="menu"
94
+ variant="ghost"
95
+ class="size-8 rounded-md"
96
+ onClick={layout.mobileSidebar.toggle}
97
+ aria-label={language.t("sidebar.menu.toggle")}
98
+ />
99
+ </div>
100
+ </Show>
101
+ <Show when={!mac()}>
102
+ <div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
103
+ <IconButton
104
+ icon="menu"
105
+ variant="ghost"
106
+ class="size-8 rounded-md"
107
+ onClick={layout.mobileSidebar.toggle}
108
+ aria-label={language.t("sidebar.menu.toggle")}
109
+ />
110
+ </div>
111
+ </Show>
112
+ <TooltipKeybind
113
+ class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
114
+ placement="bottom"
115
+ title={language.t("command.sidebar.toggle")}
116
+ keybind={command.keybind("sidebar.toggle")}
117
+ >
118
+ <Button
119
+ variant="ghost"
120
+ class="group/sidebar-toggle size-6 p-0"
121
+ onClick={layout.sidebar.toggle}
122
+ aria-label={language.t("command.sidebar.toggle")}
123
+ aria-expanded={layout.sidebar.opened()}
124
+ >
125
+ <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
126
+ <Icon
127
+ size="small"
128
+ name={layout.sidebar.opened() ? "layout-left-full" : "layout-left"}
129
+ class="group-hover/sidebar-toggle:hidden"
130
+ />
131
+ <Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
132
+ <Icon
133
+ size="small"
134
+ name={layout.sidebar.opened() ? "layout-left" : "layout-left-full"}
135
+ class="hidden group-active/sidebar-toggle:inline-block"
136
+ />
137
+ </div>
138
+ </Button>
139
+ </TooltipKeybind>
140
+ <div id="jonsoc-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
141
+ <div class="flex-1 h-full" data-tauri-drag-region />
142
+ <div
143
+ id="jonsoc-titlebar-right"
144
+ class="flex items-center gap-3 shrink-0 flex-1 justify-end"
145
+ data-tauri-drag-region
146
+ />
147
+ <Show when={windows()}>
148
+ <div data-tauri-decorum-tb class="flex flex-row" />
149
+ </Show>
150
+ </div>
151
+ <div class="absolute inset-0 flex items-center justify-center pointer-events-none">
152
+ <div id="jonsoc-titlebar-center" class="pointer-events-auto" />
153
+ </div>
154
+ </header>
155
+ )
156
+ }