@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.
- package/AGENTS.md +30 -0
- package/README.md +51 -0
- package/bunfig.toml +2 -0
- package/e2e/context.spec.ts +45 -0
- package/e2e/file-open.spec.ts +23 -0
- package/e2e/file-viewer.spec.ts +35 -0
- package/e2e/fixtures.ts +40 -0
- package/e2e/home.spec.ts +21 -0
- package/e2e/model-picker.spec.ts +43 -0
- package/e2e/navigation.spec.ts +9 -0
- package/e2e/palette.spec.ts +15 -0
- package/e2e/prompt-mention.spec.ts +26 -0
- package/e2e/prompt-slash-open.spec.ts +22 -0
- package/e2e/prompt.spec.ts +62 -0
- package/e2e/session.spec.ts +21 -0
- package/e2e/settings.spec.ts +44 -0
- package/e2e/sidebar.spec.ts +21 -0
- package/e2e/terminal-init.spec.ts +25 -0
- package/e2e/terminal.spec.ts +16 -0
- package/e2e/tsconfig.json +8 -0
- package/e2e/utils.ts +38 -0
- package/happydom.ts +75 -0
- package/index.html +23 -0
- package/package.json +72 -0
- package/playwright.config.ts +43 -0
- package/public/_headers +17 -0
- package/public/apple-touch-icon-v3.png +1 -0
- package/public/apple-touch-icon.png +1 -0
- package/public/favicon-96x96-v3.png +1 -0
- package/public/favicon-96x96.png +1 -0
- package/public/favicon-v3.ico +1 -0
- package/public/favicon-v3.svg +1 -0
- package/public/favicon.ico +1 -0
- package/public/favicon.svg +1 -0
- package/public/oc-theme-preload.js +28 -0
- package/public/site.webmanifest +1 -0
- package/public/social-share-zen.png +1 -0
- package/public/social-share.png +1 -0
- package/public/web-app-manifest-192x192.png +1 -0
- package/public/web-app-manifest-512x512.png +1 -0
- package/script/e2e-local.ts +143 -0
- package/src/addons/serialize.test.ts +319 -0
- package/src/addons/serialize.ts +591 -0
- package/src/app.tsx +150 -0
- package/src/components/dialog-connect-provider.tsx +428 -0
- package/src/components/dialog-edit-project.tsx +259 -0
- package/src/components/dialog-fork.tsx +104 -0
- package/src/components/dialog-manage-models.tsx +59 -0
- package/src/components/dialog-select-directory.tsx +208 -0
- package/src/components/dialog-select-file.tsx +196 -0
- package/src/components/dialog-select-mcp.tsx +96 -0
- package/src/components/dialog-select-model-unpaid.tsx +130 -0
- package/src/components/dialog-select-model.tsx +162 -0
- package/src/components/dialog-select-provider.tsx +70 -0
- package/src/components/dialog-select-server.tsx +249 -0
- package/src/components/dialog-settings.tsx +112 -0
- package/src/components/file-tree.tsx +112 -0
- package/src/components/link.tsx +17 -0
- package/src/components/model-tooltip.tsx +91 -0
- package/src/components/prompt-input.tsx +2076 -0
- package/src/components/session/index.ts +5 -0
- package/src/components/session/session-context-tab.tsx +428 -0
- package/src/components/session/session-header.tsx +343 -0
- package/src/components/session/session-new-view.tsx +93 -0
- package/src/components/session/session-sortable-tab.tsx +56 -0
- package/src/components/session/session-sortable-terminal-tab.tsx +187 -0
- package/src/components/session-context-usage.tsx +113 -0
- package/src/components/session-lsp-indicator.tsx +42 -0
- package/src/components/session-mcp-indicator.tsx +34 -0
- package/src/components/settings-agents.tsx +15 -0
- package/src/components/settings-commands.tsx +15 -0
- package/src/components/settings-general.tsx +306 -0
- package/src/components/settings-keybinds.tsx +437 -0
- package/src/components/settings-mcp.tsx +15 -0
- package/src/components/settings-models.tsx +15 -0
- package/src/components/settings-permissions.tsx +234 -0
- package/src/components/settings-providers.tsx +15 -0
- package/src/components/terminal.tsx +315 -0
- package/src/components/titlebar.tsx +156 -0
- package/src/context/command.tsx +308 -0
- package/src/context/comments.tsx +140 -0
- package/src/context/file.tsx +409 -0
- package/src/context/global-sdk.tsx +106 -0
- package/src/context/global-sync.tsx +898 -0
- package/src/context/language.tsx +161 -0
- package/src/context/layout-scroll.test.ts +73 -0
- package/src/context/layout-scroll.ts +118 -0
- package/src/context/layout.tsx +648 -0
- package/src/context/local.tsx +578 -0
- package/src/context/notification.tsx +173 -0
- package/src/context/permission.tsx +167 -0
- package/src/context/platform.tsx +59 -0
- package/src/context/prompt.tsx +245 -0
- package/src/context/sdk.tsx +48 -0
- package/src/context/server.tsx +214 -0
- package/src/context/settings.tsx +166 -0
- package/src/context/sync.tsx +320 -0
- package/src/context/terminal.tsx +267 -0
- package/src/custom-elements.d.ts +17 -0
- package/src/entry.tsx +76 -0
- package/src/env.d.ts +8 -0
- package/src/hooks/use-providers.ts +31 -0
- package/src/i18n/ar.ts +656 -0
- package/src/i18n/br.ts +667 -0
- package/src/i18n/da.ts +582 -0
- package/src/i18n/de.ts +591 -0
- package/src/i18n/en.ts +665 -0
- package/src/i18n/es.ts +585 -0
- package/src/i18n/fr.ts +592 -0
- package/src/i18n/ja.ts +579 -0
- package/src/i18n/ko.ts +580 -0
- package/src/i18n/no.ts +602 -0
- package/src/i18n/pl.ts +661 -0
- package/src/i18n/ru.ts +664 -0
- package/src/i18n/zh.ts +574 -0
- package/src/i18n/zht.ts +570 -0
- package/src/index.css +57 -0
- package/src/index.ts +2 -0
- package/src/pages/directory-layout.tsx +57 -0
- package/src/pages/error.tsx +290 -0
- package/src/pages/home.tsx +125 -0
- package/src/pages/layout.tsx +2599 -0
- package/src/pages/session.tsx +2505 -0
- package/src/sst-env.d.ts +10 -0
- package/src/utils/dom.ts +51 -0
- package/src/utils/id.ts +99 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/perf.ts +135 -0
- package/src/utils/persist.ts +377 -0
- package/src/utils/prompt.ts +203 -0
- package/src/utils/same.ts +6 -0
- package/src/utils/solid-dnd.tsx +55 -0
- package/src/utils/sound.ts +110 -0
- package/src/utils/speech.ts +302 -0
- package/src/utils/worktree.ts +58 -0
- package/sst-env.d.ts +9 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +15 -0
- 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
|
+
}
|