fix(webui): resolve ESLint warnings across Svelte components
- commit
- bf4fa07bfbb79911c32a7689bf6213477705ab66
- author
- brice · · 2026-01-24 14:19 UTC
- parent
- f328a4f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
15 files · +235 -156
M webui/src/lib/components/ui/color-picker/color-picker.svelte +2 -8
| @@ -21,17 +21,11 @@ | |||
| 21 | 21 | ||
| 22 | 22 | let open = $state(false); | |
| 23 | 23 | ||
| 24 | - | // svelte-awesome-color-picker uses hex binding | |
| 25 | - | let hex = $state(value); | |
| 26 | - | ||
| 27 | - | // Sync hex when value changes externally | |
| 28 | - | $effect(() => { | |
| 29 | - | hex = value; | |
| 30 | - | }); | |
| 24 | + | // svelte-awesome-color-picker uses hex binding - use writable derived to sync with value | |
| 25 | + | let hex = $derived.by(() => value); | |
| 31 | 26 | ||
| 32 | 27 | function handleInput(event: { hex: string | null }) { | |
| 33 | 28 | if (event.hex) { | |
| 34 | - | hex = event.hex; | |
| 35 | 29 | value = event.hex; | |
| 36 | 30 | onValueChange?.(event.hex); | |
| 37 | 31 | } | |
| @@ -21,17 +21,11 @@ | |||||
| 21 | 21 | ||||
| 22 | let open = $state(false); | 22 | let open = $state(false); | ||
| 23 | 23 | ||||
| 24 | - | // svelte-awesome-color-picker uses hex binding | 24 | + | // svelte-awesome-color-picker uses hex binding - use writable derived to sync with value |
| 25 | - | let hex = $state(value); | 25 | + | let hex = $derived.by(() => value); |
| 26 | - | ||||
| 27 | - | // Sync hex when value changes externally | |||
| 28 | - | $effect(() => { | |||
| 29 | - | hex = value; | |||
| 30 | - | }); | |||
| 31 | 26 | ||||
| 32 | function handleInput(event: { hex: string | null }) { | 27 | function handleInput(event: { hex: string | null }) { | ||
| 33 | if (event.hex) { | 28 | if (event.hex) { | ||
| 34 | - | hex = event.hex; | |||
| 35 | value = event.hex; | 29 | value = event.hex; | ||
| 36 | onValueChange?.(event.hex); | 30 | onValueChange?.(event.hex); | ||
| 37 | } | 31 | } | ||
M webui/src/pages/ConfigPage.svelte +54 -42
| @@ -14,6 +14,12 @@ | |||
| 14 | 14 | import * as Tabs from "$lib/components/ui/tabs"; | |
| 15 | 15 | import { FlaskConical, RefreshCw } from "@lucide/svelte"; | |
| 16 | 16 | ||
| 17 | + | function getErrorMessage(e: unknown): string { | |
| 18 | + | if (e instanceof Error) return e.message; | |
| 19 | + | if (typeof e === "string") return e; | |
| 20 | + | return "Unknown error"; | |
| 21 | + | } | |
| 22 | + | ||
| 17 | 23 | type ColorPalette = { | |
| 18 | 24 | background: string; | |
| 19 | 25 | surface: string; | |
| @@ -119,8 +125,8 @@ | |||
| 119 | 125 | const response = await fetch("/api/config"); | |
| 120 | 126 | if (!response.ok) throw new Error("Failed to fetch config"); | |
| 121 | 127 | config = (await response.json()) as ConfigDTO; | |
| 122 | - | } catch (e: any) { | |
| 123 | - | loadError = e.message; | |
| 128 | + | } catch (e: unknown) { | |
| 129 | + | loadError = getErrorMessage(e); | |
| 124 | 130 | console.error("[config] load failed", e); | |
| 125 | 131 | } finally { | |
| 126 | 132 | loading = false; | |
| @@ -140,14 +146,14 @@ | |||
| 140 | 146 | const response = await fetch("/api/config/default"); | |
| 141 | 147 | if (!response.ok) throw new Error("Failed to fetch default config"); | |
| 142 | 148 | config = (await response.json()) as ConfigDTO; | |
| 143 | - | } catch (e: any) { | |
| 149 | + | } catch (e: unknown) { | |
| 144 | 150 | console.error("[config] reset defaults failed", e); | |
| 145 | 151 | } | |
| 146 | 152 | resetDialogOpen = false; | |
| 147 | 153 | } | |
| 148 | 154 | ||
| 149 | 155 | function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null { | |
| 150 | - | const bridge = (window as any).webkit?.messageHandlers?.dumber; | |
| 156 | + | const bridge = window.webkit?.messageHandlers?.dumber; | |
| 151 | 157 | if (bridge && typeof bridge.postMessage === "function") { | |
| 152 | 158 | return bridge; | |
| 153 | 159 | } | |
| @@ -155,7 +161,7 @@ | |||
| 155 | 161 | } | |
| 156 | 162 | ||
| 157 | 163 | function getWebViewId(): number { | |
| 158 | - | return (window as any).__dumber_webview_id || 0; | |
| 164 | + | return window.__dumber_webview_id || 0; | |
| 159 | 165 | } | |
| 160 | 166 | ||
| 161 | 167 | async function saveConfig() { | |
| @@ -195,7 +201,7 @@ | |||
| 195 | 201 | } | |
| 196 | 202 | }; | |
| 197 | 203 | ||
| 198 | - | (window as any).__dumber_config_saved = (resp?: unknown) => { | |
| 204 | + | window.__dumber_config_saved = (resp?: unknown) => { | |
| 199 | 205 | clearSaveTimeout(); | |
| 200 | 206 | console.debug("[config] save success", resp); | |
| 201 | 207 | saveSuccess = true; | |
| @@ -213,7 +219,7 @@ | |||
| 213 | 219 | saveSuccess = false; | |
| 214 | 220 | }, 3000); | |
| 215 | 221 | }; | |
| 216 | - | (window as any).__dumber_config_error = (msg: unknown) => { | |
| 222 | + | window.__dumber_config_error = (msg: unknown) => { | |
| 217 | 223 | clearSaveTimeout(); | |
| 218 | 224 | console.error("[config] save error", msg); | |
| 219 | 225 | saveError = typeof msg === "string" ? msg : "Failed to save config"; | |
| @@ -228,14 +234,13 @@ | |||
| 228 | 234 | webview_id: webviewId, | |
| 229 | 235 | payload, | |
| 230 | 236 | }); | |
| 231 | - | console.debug("[config] postMessage sent", { payloadBytes: JSON.stringify(payload).length }); | |
| 232 | 237 | } catch (postErr) { | |
| 233 | 238 | console.error("[config] postMessage threw", postErr); | |
| 234 | 239 | throw postErr; | |
| 235 | 240 | } | |
| 236 | - | } catch (e: any) { | |
| 241 | + | } catch (e: unknown) { | |
| 237 | 242 | console.error("[config] save exception", e); | |
| 238 | - | saveError = e.message; | |
| 243 | + | saveError = getErrorMessage(e); | |
| 239 | 244 | saving = false; | |
| 240 | 245 | } | |
| 241 | 246 | } | |
| @@ -247,8 +252,8 @@ | |||
| 247 | 252 | ||
| 248 | 253 | onMount(() => { | |
| 249 | 254 | console.debug("[config] mount", { | |
| 250 | - | webviewId: (window as any).__dumber_webview_id, | |
| 251 | - | hasBridge: Boolean((window as any).webkit?.messageHandlers?.dumber), | |
| 255 | + | webviewId: window.__dumber_webview_id, | |
| 256 | + | hasBridge: Boolean(window.webkit?.messageHandlers?.dumber), | |
| 252 | 257 | }); | |
| 253 | 258 | ||
| 254 | 259 | const onThemeChanged = (e: Event) => { | |
| @@ -342,7 +347,12 @@ | |||
| 342 | 347 | </div> | |
| 343 | 348 | <div class="space-y-2"> | |
| 344 | 349 | <Label for="ui_scale">UI Scale</Label> | |
| 345 | - | <Input id="ui_scale" type="number" step="0.1" bind:value={config.default_ui_scale} /> | |
| 350 | + | <Input | |
| 351 | + | id="ui_scale" | |
| 352 | + | type="number" | |
| 353 | + | step="0.1" | |
| 354 | + | bind:value={config.default_ui_scale} | |
| 355 | + | /> | |
| 346 | 356 | </div> | |
| 347 | 357 | </div> | |
| 348 | 358 | ||
| @@ -620,35 +630,37 @@ | |||
| 620 | 630 | <span>Theme x{themeEvents}</span> | |
| 621 | 631 | {/if} | |
| 622 | 632 | </div> | |
| 623 | - | <div class="flex items-center gap-3"> | |
| 624 | - | <AlertDialog.Root bind:open={resetDialogOpen}> | |
| 625 | - | <AlertDialog.Trigger disabled={saving}> | |
| 626 | - | {#snippet child({ props })} | |
| 627 | - | <Button variant="outline" {...props} type="button"> | |
| 628 | - | Reset Defaults | |
| 629 | - | </Button> | |
| 630 | - | {/snippet} | |
| 631 | - | </AlertDialog.Trigger> | |
| 632 | - | <AlertDialog.Content> | |
| 633 | - | <AlertDialog.Header> | |
| 634 | - | <AlertDialog.Title>Reset to defaults?</AlertDialog.Title> | |
| 635 | - | <AlertDialog.Description> | |
| 636 | - | This will reset all settings on this page to their default values. | |
| 637 | - | You will still need to click Save to persist the changes. | |
| 638 | - | </AlertDialog.Description> | |
| 639 | - | </AlertDialog.Header> | |
| 640 | - | <AlertDialog.Footer> | |
| 641 | - | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |
| 642 | - | <AlertDialog.Action onclick={doResetToDefaults}> | |
| 643 | - | Reset Defaults | |
| 644 | - | </AlertDialog.Action> | |
| 645 | - | </AlertDialog.Footer> | |
| 646 | - | </AlertDialog.Content> | |
| 647 | - | </AlertDialog.Root> | |
| 648 | - | <Button onclick={saveConfig} disabled={saving} type="button"> | |
| 649 | - | {saving ? "Saving…" : "Save"} | |
| 650 | - | </Button> | |
| 651 | - | </div> | |
| 633 | + | {#if activeTab !== "keybindings"} | |
| 634 | + | <div class="flex items-center gap-3"> | |
| 635 | + | <AlertDialog.Root bind:open={resetDialogOpen}> | |
| 636 | + | <AlertDialog.Trigger disabled={saving}> | |
| 637 | + | {#snippet child({ props })} | |
| 638 | + | <Button variant="outline" {...props} type="button"> | |
| 639 | + | Reset Defaults | |
| 640 | + | </Button> | |
| 641 | + | {/snippet} | |
| 642 | + | </AlertDialog.Trigger> | |
| 643 | + | <AlertDialog.Content> | |
| 644 | + | <AlertDialog.Header> | |
| 645 | + | <AlertDialog.Title>Reset to defaults?</AlertDialog.Title> | |
| 646 | + | <AlertDialog.Description> | |
| 647 | + | This will reset all settings on this page to their default values. | |
| 648 | + | You will still need to click Save to persist the changes. | |
| 649 | + | </AlertDialog.Description> | |
| 650 | + | </AlertDialog.Header> | |
| 651 | + | <AlertDialog.Footer> | |
| 652 | + | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |
| 653 | + | <AlertDialog.Action onclick={doResetToDefaults}> | |
| 654 | + | Reset Defaults | |
| 655 | + | </AlertDialog.Action> | |
| 656 | + | </AlertDialog.Footer> | |
| 657 | + | </AlertDialog.Content> | |
| 658 | + | </AlertDialog.Root> | |
| 659 | + | <Button onclick={saveConfig} disabled={saving} type="button"> | |
| 660 | + | {saving ? "Saving…" : "Save"} | |
| 661 | + | </Button> | |
| 662 | + | </div> | |
| 663 | + | {/if} | |
| 652 | 664 | </div> | |
| 653 | 665 | ||
| 654 | 666 | <!-- Save error display --> | |
| @@ -14,6 +14,12 @@ | |||||
| 14 | import * as Tabs from "$lib/components/ui/tabs"; | 14 | import * as Tabs from "$lib/components/ui/tabs"; | ||
| 15 | import { FlaskConical, RefreshCw } from "@lucide/svelte"; | 15 | import { FlaskConical, RefreshCw } from "@lucide/svelte"; | ||
| 16 | 16 | ||||
| 17 | + | function getErrorMessage(e: unknown): string { | |||
| 18 | + | if (e instanceof Error) return e.message; | |||
| 19 | + | if (typeof e === "string") return e; | |||
| 20 | + | return "Unknown error"; | |||
| 21 | + | } | |||
| 22 | + | ||||
| 17 | type ColorPalette = { | 23 | type ColorPalette = { | ||
| 18 | background: string; | 24 | background: string; | ||
| 19 | surface: string; | 25 | surface: string; | ||
| @@ -119,8 +125,8 @@ | |||||
| 119 | const response = await fetch("/api/config"); | 125 | const response = await fetch("/api/config"); | ||
| 120 | if (!response.ok) throw new Error("Failed to fetch config"); | 126 | if (!response.ok) throw new Error("Failed to fetch config"); | ||
| 121 | config = (await response.json()) as ConfigDTO; | 127 | config = (await response.json()) as ConfigDTO; | ||
| 122 | - | } catch (e: any) { | 128 | + | } catch (e: unknown) { |
| 123 | - | loadError = e.message; | 129 | + | loadError = getErrorMessage(e); |
| 124 | console.error("[config] load failed", e); | 130 | console.error("[config] load failed", e); | ||
| 125 | } finally { | 131 | } finally { | ||
| 126 | loading = false; | 132 | loading = false; | ||
| @@ -140,14 +146,14 @@ | |||||
| 140 | const response = await fetch("/api/config/default"); | 146 | const response = await fetch("/api/config/default"); | ||
| 141 | if (!response.ok) throw new Error("Failed to fetch default config"); | 147 | if (!response.ok) throw new Error("Failed to fetch default config"); | ||
| 142 | config = (await response.json()) as ConfigDTO; | 148 | config = (await response.json()) as ConfigDTO; | ||
| 143 | - | } catch (e: any) { | 149 | + | } catch (e: unknown) { |
| 144 | console.error("[config] reset defaults failed", e); | 150 | console.error("[config] reset defaults failed", e); | ||
| 145 | } | 151 | } | ||
| 146 | resetDialogOpen = false; | 152 | resetDialogOpen = false; | ||
| 147 | } | 153 | } | ||
| 148 | 154 | ||||
| 149 | function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null { | 155 | function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null { | ||
| 150 | - | const bridge = (window as any).webkit?.messageHandlers?.dumber; | 156 | + | const bridge = window.webkit?.messageHandlers?.dumber; |
| 151 | if (bridge && typeof bridge.postMessage === "function") { | 157 | if (bridge && typeof bridge.postMessage === "function") { | ||
| 152 | return bridge; | 158 | return bridge; | ||
| 153 | } | 159 | } | ||
| @@ -155,7 +161,7 @@ | |||||
| 155 | } | 161 | } | ||
| 156 | 162 | ||||
| 157 | function getWebViewId(): number { | 163 | function getWebViewId(): number { | ||
| 158 | - | return (window as any).__dumber_webview_id || 0; | 164 | + | return window.__dumber_webview_id || 0; |
| 159 | } | 165 | } | ||
| 160 | 166 | ||||
| 161 | async function saveConfig() { | 167 | async function saveConfig() { | ||
| @@ -195,7 +201,7 @@ | |||||
| 195 | } | 201 | } | ||
| 196 | }; | 202 | }; | ||
| 197 | 203 | ||||
| 198 | - | (window as any).__dumber_config_saved = (resp?: unknown) => { | 204 | + | window.__dumber_config_saved = (resp?: unknown) => { |
| 199 | clearSaveTimeout(); | 205 | clearSaveTimeout(); | ||
| 200 | console.debug("[config] save success", resp); | 206 | console.debug("[config] save success", resp); | ||
| 201 | saveSuccess = true; | 207 | saveSuccess = true; | ||
| @@ -213,7 +219,7 @@ | |||||
| 213 | saveSuccess = false; | 219 | saveSuccess = false; | ||
| 214 | }, 3000); | 220 | }, 3000); | ||
| 215 | }; | 221 | }; | ||
| 216 | - | (window as any).__dumber_config_error = (msg: unknown) => { | 222 | + | window.__dumber_config_error = (msg: unknown) => { |
| 217 | clearSaveTimeout(); | 223 | clearSaveTimeout(); | ||
| 218 | console.error("[config] save error", msg); | 224 | console.error("[config] save error", msg); | ||
| 219 | saveError = typeof msg === "string" ? msg : "Failed to save config"; | 225 | saveError = typeof msg === "string" ? msg : "Failed to save config"; | ||
| @@ -228,14 +234,13 @@ | |||||
| 228 | webview_id: webviewId, | 234 | webview_id: webviewId, | ||
| 229 | payload, | 235 | payload, | ||
| 230 | }); | 236 | }); | ||
| 231 | - | console.debug("[config] postMessage sent", { payloadBytes: JSON.stringify(payload).length }); | |||
| 232 | } catch (postErr) { | 237 | } catch (postErr) { | ||
| 233 | console.error("[config] postMessage threw", postErr); | 238 | console.error("[config] postMessage threw", postErr); | ||
| 234 | throw postErr; | 239 | throw postErr; | ||
| 235 | } | 240 | } | ||
| 236 | - | } catch (e: any) { | 241 | + | } catch (e: unknown) { |
| 237 | console.error("[config] save exception", e); | 242 | console.error("[config] save exception", e); | ||
| 238 | - | saveError = e.message; | 243 | + | saveError = getErrorMessage(e); |
| 239 | saving = false; | 244 | saving = false; | ||
| 240 | } | 245 | } | ||
| 241 | } | 246 | } | ||
| @@ -247,8 +252,8 @@ | |||||
| 247 | 252 | ||||
| 248 | onMount(() => { | 253 | onMount(() => { | ||
| 249 | console.debug("[config] mount", { | 254 | console.debug("[config] mount", { | ||
| 250 | - | webviewId: (window as any).__dumber_webview_id, | 255 | + | webviewId: window.__dumber_webview_id, |
| 251 | - | hasBridge: Boolean((window as any).webkit?.messageHandlers?.dumber), | 256 | + | hasBridge: Boolean(window.webkit?.messageHandlers?.dumber), |
| 252 | }); | 257 | }); | ||
| 253 | 258 | ||||
| 254 | const onThemeChanged = (e: Event) => { | 259 | const onThemeChanged = (e: Event) => { | ||
| @@ -342,7 +347,12 @@ | |||||
| 342 | </div> | 347 | </div> | ||
| 343 | <div class="space-y-2"> | 348 | <div class="space-y-2"> | ||
| 344 | <Label for="ui_scale">UI Scale</Label> | 349 | <Label for="ui_scale">UI Scale</Label> | ||
| 345 | - | <Input id="ui_scale" type="number" step="0.1" bind:value={config.default_ui_scale} /> | 350 | + | <Input |
| 351 | + | id="ui_scale" | |||
| 352 | + | type="number" | |||
| 353 | + | step="0.1" | |||
| 354 | + | bind:value={config.default_ui_scale} | |||
| 355 | + | /> | |||
| 346 | </div> | 356 | </div> | ||
| 347 | </div> | 357 | </div> | ||
| 348 | 358 | ||||
| @@ -620,35 +630,37 @@ | |||||
| 620 | <span>Theme x{themeEvents}</span> | 630 | <span>Theme x{themeEvents}</span> | ||
| 621 | {/if} | 631 | {/if} | ||
| 622 | </div> | 632 | </div> | ||
| 623 | - | <div class="flex items-center gap-3"> | 633 | + | {#if activeTab !== "keybindings"} |
| 624 | - | <AlertDialog.Root bind:open={resetDialogOpen}> | 634 | + | <div class="flex items-center gap-3"> |
| 625 | - | <AlertDialog.Trigger disabled={saving}> | 635 | + | <AlertDialog.Root bind:open={resetDialogOpen}> |
| 626 | - | {#snippet child({ props })} | 636 | + | <AlertDialog.Trigger disabled={saving}> |
| 627 | - | <Button variant="outline" {...props} type="button"> | 637 | + | {#snippet child({ props })} |
| 628 | - | Reset Defaults | 638 | + | <Button variant="outline" {...props} type="button"> |
| 629 | - | </Button> | 639 | + | Reset Defaults |
| 630 | - | {/snippet} | 640 | + | </Button> |
| 631 | - | </AlertDialog.Trigger> | 641 | + | {/snippet} |
| 632 | - | <AlertDialog.Content> | 642 | + | </AlertDialog.Trigger> |
| 633 | - | <AlertDialog.Header> | 643 | + | <AlertDialog.Content> |
| 634 | - | <AlertDialog.Title>Reset to defaults?</AlertDialog.Title> | 644 | + | <AlertDialog.Header> |
| 635 | - | <AlertDialog.Description> | 645 | + | <AlertDialog.Title>Reset to defaults?</AlertDialog.Title> |
| 636 | - | This will reset all settings on this page to their default values. | 646 | + | <AlertDialog.Description> |
| 637 | - | You will still need to click Save to persist the changes. | 647 | + | This will reset all settings on this page to their default values. |
| 638 | - | </AlertDialog.Description> | 648 | + | You will still need to click Save to persist the changes. |
| 639 | - | </AlertDialog.Header> | 649 | + | </AlertDialog.Description> |
| 640 | - | <AlertDialog.Footer> | 650 | + | </AlertDialog.Header> |
| 641 | - | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | 651 | + | <AlertDialog.Footer> |
| 642 | - | <AlertDialog.Action onclick={doResetToDefaults}> | 652 | + | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> |
| 643 | - | Reset Defaults | 653 | + | <AlertDialog.Action onclick={doResetToDefaults}> |
| 644 | - | </AlertDialog.Action> | 654 | + | Reset Defaults |
| 645 | - | </AlertDialog.Footer> | 655 | + | </AlertDialog.Action> |
| 646 | - | </AlertDialog.Content> | 656 | + | </AlertDialog.Footer> |
| 647 | - | </AlertDialog.Root> | 657 | + | </AlertDialog.Content> |
| 648 | - | <Button onclick={saveConfig} disabled={saving} type="button"> | 658 | + | </AlertDialog.Root> |
| 649 | - | {saving ? "Saving…" : "Save"} | 659 | + | <Button onclick={saveConfig} disabled={saving} type="button"> |
| 650 | - | </Button> | 660 | + | {saving ? "Saving…" : "Save"} |
| 651 | - | </div> | 661 | + | </Button> |
| 662 | + | </div> | |||
| 663 | + | {/if} | |||
| 652 | </div> | 664 | </div> | ||
| 653 | 665 | ||||
| 654 | <!-- Save error display --> | 666 | <!-- Save error display --> | ||
M webui/src/pages/ErrorPage.svelte +11 -2
| @@ -70,7 +70,11 @@ | |||
| 70 | 70 | const handler = (event: MessageEvent) => { | |
| 71 | 71 | if (event.data?.requestId === requestId) { | |
| 72 | 72 | window.removeEventListener('message', handler); | |
| 73 | - | event.data.success ? resolve(event.data.data) : reject(new Error(event.data.error || 'Unknown error')); | |
| 73 | + | if (event.data.success) { | |
| 74 | + | resolve(event.data.data); | |
| 75 | + | } else { | |
| 76 | + | reject(new Error(event.data.error || 'Unknown error')); | |
| 77 | + | } | |
| 74 | 78 | } | |
| 75 | 79 | }; | |
| 76 | 80 | window.addEventListener('message', handler); | |
| @@ -110,7 +114,11 @@ | |||
| 110 | 114 | } | |
| 111 | 115 | ||
| 112 | 116 | function retry() { | |
| 113 | - | targetUrl ? window.location.href = targetUrl : window.location.reload(); | |
| 117 | + | if (targetUrl) { | |
| 118 | + | window.location.href = targetUrl; | |
| 119 | + | } else { | |
| 120 | + | window.location.reload(); | |
| 121 | + | } | |
| 114 | 122 | } | |
| 115 | 123 | ||
| 116 | 124 | function goBack() { | |
| @@ -144,6 +152,7 @@ | |||
| 144 | 152 | <Card.Header class="text-center"> | |
| 145 | 153 | <div class="flex justify-center mb-4"> | |
| 146 | 154 | <svg class="w-16 h-16 {iconColors[config.icon] || 'text-muted-foreground'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | |
| 155 | + | <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static SVG paths --> | |
| 147 | 156 | {@html icons[config.icon] || icons.error} | |
| 148 | 157 | </svg> | |
| 149 | 158 | </div> | |
| @@ -70,7 +70,11 @@ | |||||
| 70 | const handler = (event: MessageEvent) => { | 70 | const handler = (event: MessageEvent) => { | ||
| 71 | if (event.data?.requestId === requestId) { | 71 | if (event.data?.requestId === requestId) { | ||
| 72 | window.removeEventListener('message', handler); | 72 | window.removeEventListener('message', handler); | ||
| 73 | - | event.data.success ? resolve(event.data.data) : reject(new Error(event.data.error || 'Unknown error')); | 73 | + | if (event.data.success) { |
| 74 | + | resolve(event.data.data); | |||
| 75 | + | } else { | |||
| 76 | + | reject(new Error(event.data.error || 'Unknown error')); | |||
| 77 | + | } | |||
| 74 | } | 78 | } | ||
| 75 | }; | 79 | }; | ||
| 76 | window.addEventListener('message', handler); | 80 | window.addEventListener('message', handler); | ||
| @@ -110,7 +114,11 @@ | |||||
| 110 | } | 114 | } | ||
| 111 | 115 | ||||
| 112 | function retry() { | 116 | function retry() { | ||
| 113 | - | targetUrl ? window.location.href = targetUrl : window.location.reload(); | 117 | + | if (targetUrl) { |
| 118 | + | window.location.href = targetUrl; | |||
| 119 | + | } else { | |||
| 120 | + | window.location.reload(); | |||
| 121 | + | } | |||
| 114 | } | 122 | } | ||
| 115 | 123 | ||||
| 116 | function goBack() { | 124 | function goBack() { | ||
| @@ -144,6 +152,7 @@ | |||||
| 144 | <Card.Header class="text-center"> | 152 | <Card.Header class="text-center"> | ||
| 145 | <div class="flex justify-center mb-4"> | 153 | <div class="flex justify-center mb-4"> | ||
| 146 | <svg class="w-16 h-16 {iconColors[config.icon] || 'text-muted-foreground'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | 154 | <svg class="w-16 h-16 {iconColors[config.icon] || 'text-muted-foreground'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | ||
| 155 | + | <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static SVG paths --> | |||
| 147 | {@html icons[config.icon] || icons.error} | 156 | {@html icons[config.icon] || icons.error} | ||
| 148 | </svg> | 157 | </svg> | ||
| 149 | </div> | 158 | </div> | ||
M webui/src/pages/config/ConfigShell.svelte +3 -4
| @@ -9,14 +9,12 @@ | |||
| 9 | 9 | let { children }: Props = $props(); | |
| 10 | 10 | ||
| 11 | 11 | function navigateWithViewTransition(url: string) { | |
| 12 | - | const doc = document as any; | |
| 13 | - | ||
| 14 | - | if (typeof doc?.startViewTransition !== 'function') { | |
| 12 | + | if (typeof document.startViewTransition !== 'function') { | |
| 15 | 13 | window.location.href = url; | |
| 16 | 14 | return; | |
| 17 | 15 | } | |
| 18 | 16 | ||
| 19 | - | const transition = doc.startViewTransition(() => { | |
| 17 | + | const transition = document.startViewTransition(() => { | |
| 20 | 18 | document.documentElement.dataset.vt = 'leaving'; | |
| 21 | 19 | }); | |
| 22 | 20 | ||
| @@ -34,6 +32,7 @@ | |||
| 34 | 32 | <svelte:head> | |
| 35 | 33 | <title>dumb://config</title> | |
| 36 | 34 | <meta name="description" content="Dumber Browser - Settings" /> | |
| 35 | + | <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static styles for shell initialization --> | |
| 37 | 36 | {@html `<style> | |
| 38 | 37 | html, body { margin: 0; padding: 0; } | |
| 39 | 38 | html { background: var(--background, #0a0a0a); } | |
| @@ -9,14 +9,12 @@ | |||||
| 9 | let { children }: Props = $props(); | 9 | let { children }: Props = $props(); | ||
| 10 | 10 | ||||
| 11 | function navigateWithViewTransition(url: string) { | 11 | function navigateWithViewTransition(url: string) { | ||
| 12 | - | const doc = document as any; | 12 | + | if (typeof document.startViewTransition !== 'function') { |
| 13 | - | ||||
| 14 | - | if (typeof doc?.startViewTransition !== 'function') { | |||
| 15 | window.location.href = url; | 13 | window.location.href = url; | ||
| 16 | return; | 14 | return; | ||
| 17 | } | 15 | } | ||
| 18 | 16 | ||||
| 19 | - | const transition = doc.startViewTransition(() => { | 17 | + | const transition = document.startViewTransition(() => { |
| 20 | document.documentElement.dataset.vt = 'leaving'; | 18 | document.documentElement.dataset.vt = 'leaving'; | ||
| 21 | }); | 19 | }); | ||
| 22 | 20 | ||||
| @@ -34,6 +32,7 @@ | |||||
| 34 | <svelte:head> | 32 | <svelte:head> | ||
| 35 | <title>dumb://config</title> | 33 | <title>dumb://config</title> | ||
| 36 | <meta name="description" content="Dumber Browser - Settings" /> | 34 | <meta name="description" content="Dumber Browser - Settings" /> | ||
| 35 | + | <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static styles for shell initialization --> | |||
| 37 | {@html `<style> | 36 | {@html `<style> | ||
| 38 | html, body { margin: 0; padding: 0; } | 37 | html, body { margin: 0; padding: 0; } | ||
| 39 | html { background: var(--background, #0a0a0a); } | 38 | html { background: var(--background, #0a0a0a); } | ||
M webui/src/pages/config/KeyCaptureModal.svelte +16 -13
| @@ -21,10 +21,10 @@ | |||
| 21 | 21 | ||
| 22 | 22 | let { binding, onSave, onCancel }: Props = $props(); | |
| 23 | 23 | ||
| 24 | - | // Initialize with a plain copy of the binding keys (not a Proxy reference) | |
| 25 | - | // untrack() tells Svelte we intentionally want the initial value only | |
| 24 | + | // Initialize with a plain copy of the binding keys using $state.snapshot() | |
| 25 | + | // to strip Svelte 5 Proxy wrappers. untrack() ensures we only read the initial value. | |
| 26 | 26 | let newKeys = $state<string[]>( | |
| 27 | - | untrack(() => JSON.parse(JSON.stringify(binding.keys)) as string[]) | |
| 27 | + | untrack(() => $state.snapshot(binding.keys) as string[]) | |
| 28 | 28 | ); | |
| 29 | 29 | let isCapturing = $state(false); | |
| 30 | 30 | let capturedKey = $state<string | null>(null); | |
| @@ -93,6 +93,17 @@ | |||
| 93 | 93 | newKeys = newKeys.filter((_, i) => i !== index); | |
| 94 | 94 | } | |
| 95 | 95 | ||
| 96 | + | function handleSave() { | |
| 97 | + | // Use $state.snapshot() to get plain array without Svelte 5 Proxy wrappers | |
| 98 | + | const keysToSave = $state.snapshot(newKeys) as string[]; | |
| 99 | + | // Auto-add any pending captured key before saving | |
| 100 | + | if (capturedKey && !keysToSave.includes(capturedKey)) { | |
| 101 | + | keysToSave.push(capturedKey); | |
| 102 | + | } | |
| 103 | + | isSaving = true; | |
| 104 | + | onSave(keysToSave); | |
| 105 | + | } | |
| 106 | + | ||
| 96 | 107 | onMount(() => { | |
| 97 | 108 | const handler = (e: KeyboardEvent) => handleKeyDown(e); | |
| 98 | 109 | window.addEventListener("keydown", handler, true); | |
| @@ -116,7 +127,7 @@ | |||
| 116 | 127 | Current Bindings | |
| 117 | 128 | </div> | |
| 118 | 129 | <div class="flex flex-wrap gap-2"> | |
| 119 | - | {#each newKeys as key, i} | |
| 130 | + | {#each newKeys as key, i (key)} | |
| 120 | 131 | <div class="flex items-center gap-1 rounded bg-muted px-2 py-1"> | |
| 121 | 132 | <kbd class="text-xs font-mono">{formatKey(key)}</kbd> | |
| 122 | 133 | <button | |
| @@ -173,15 +184,7 @@ | |||
| 173 | 184 | ||
| 174 | 185 | <AlertDialog.Footer> | |
| 175 | 186 | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |
| 176 | - | <AlertDialog.Action onclick={() => { | |
| 177 | - | // Auto-add any pending captured key before saving | |
| 178 | - | let keysToSave = [...newKeys]; | |
| 179 | - | if (capturedKey && !keysToSave.includes(capturedKey)) { | |
| 180 | - | keysToSave.push(capturedKey); | |
| 181 | - | } | |
| 182 | - | isSaving = true; | |
| 183 | - | onSave(keysToSave); | |
| 184 | - | }}>Save</AlertDialog.Action> | |
| 187 | + | <AlertDialog.Action onclick={handleSave}>Save</AlertDialog.Action> | |
| 185 | 188 | </AlertDialog.Footer> | |
| 186 | 189 | </AlertDialog.Content> | |
| 187 | 190 | </AlertDialog.Root> | |
| @@ -21,10 +21,10 @@ | |||||
| 21 | 21 | ||||
| 22 | let { binding, onSave, onCancel }: Props = $props(); | 22 | let { binding, onSave, onCancel }: Props = $props(); | ||
| 23 | 23 | ||||
| 24 | - | // Initialize with a plain copy of the binding keys (not a Proxy reference) | 24 | + | // Initialize with a plain copy of the binding keys using $state.snapshot() |
| 25 | - | // untrack() tells Svelte we intentionally want the initial value only | 25 | + | // to strip Svelte 5 Proxy wrappers. untrack() ensures we only read the initial value. |
| 26 | let newKeys = $state<string[]>( | 26 | let newKeys = $state<string[]>( | ||
| 27 | - | untrack(() => JSON.parse(JSON.stringify(binding.keys)) as string[]) | 27 | + | untrack(() => $state.snapshot(binding.keys) as string[]) |
| 28 | ); | 28 | ); | ||
| 29 | let isCapturing = $state(false); | 29 | let isCapturing = $state(false); | ||
| 30 | let capturedKey = $state<string | null>(null); | 30 | let capturedKey = $state<string | null>(null); | ||
| @@ -93,6 +93,17 @@ | |||||
| 93 | newKeys = newKeys.filter((_, i) => i !== index); | 93 | newKeys = newKeys.filter((_, i) => i !== index); | ||
| 94 | } | 94 | } | ||
| 95 | 95 | ||||
| 96 | + | function handleSave() { | |||
| 97 | + | // Use $state.snapshot() to get plain array without Svelte 5 Proxy wrappers | |||
| 98 | + | const keysToSave = $state.snapshot(newKeys) as string[]; | |||
| 99 | + | // Auto-add any pending captured key before saving | |||
| 100 | + | if (capturedKey && !keysToSave.includes(capturedKey)) { | |||
| 101 | + | keysToSave.push(capturedKey); | |||
| 102 | + | } | |||
| 103 | + | isSaving = true; | |||
| 104 | + | onSave(keysToSave); | |||
| 105 | + | } | |||
| 106 | + | ||||
| 96 | onMount(() => { | 107 | onMount(() => { | ||
| 97 | const handler = (e: KeyboardEvent) => handleKeyDown(e); | 108 | const handler = (e: KeyboardEvent) => handleKeyDown(e); | ||
| 98 | window.addEventListener("keydown", handler, true); | 109 | window.addEventListener("keydown", handler, true); | ||
| @@ -116,7 +127,7 @@ | |||||
| 116 | Current Bindings | 127 | Current Bindings | ||
| 117 | </div> | 128 | </div> | ||
| 118 | <div class="flex flex-wrap gap-2"> | 129 | <div class="flex flex-wrap gap-2"> | ||
| 119 | - | {#each newKeys as key, i} | 130 | + | {#each newKeys as key, i (key)} |
| 120 | <div class="flex items-center gap-1 rounded bg-muted px-2 py-1"> | 131 | <div class="flex items-center gap-1 rounded bg-muted px-2 py-1"> | ||
| 121 | <kbd class="text-xs font-mono">{formatKey(key)}</kbd> | 132 | <kbd class="text-xs font-mono">{formatKey(key)}</kbd> | ||
| 122 | <button | 133 | <button | ||
| @@ -173,15 +184,7 @@ | |||||
| 173 | 184 | ||||
| 174 | <AlertDialog.Footer> | 185 | <AlertDialog.Footer> | ||
| 175 | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | 186 | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | ||
| 176 | - | <AlertDialog.Action onclick={() => { | 187 | + | <AlertDialog.Action onclick={handleSave}>Save</AlertDialog.Action> |
| 177 | - | // Auto-add any pending captured key before saving | |||
| 178 | - | let keysToSave = [...newKeys]; | |||
| 179 | - | if (capturedKey && !keysToSave.includes(capturedKey)) { | |||
| 180 | - | keysToSave.push(capturedKey); | |||
| 181 | - | } | |||
| 182 | - | isSaving = true; | |||
| 183 | - | onSave(keysToSave); | |||
| 184 | - | }}>Save</AlertDialog.Action> | |||
| 185 | </AlertDialog.Footer> | 188 | </AlertDialog.Footer> | ||
| 186 | </AlertDialog.Content> | 189 | </AlertDialog.Content> | ||
| 187 | </AlertDialog.Root> | 190 | </AlertDialog.Root> | ||
M webui/src/pages/config/KeybindingGroup.svelte +1 -1
| @@ -56,7 +56,7 @@ | |||
| 56 | 56 | <Table.Cell class="text-muted-foreground">{binding.description}</Table.Cell> | |
| 57 | 57 | <Table.Cell> | |
| 58 | 58 | <div class="flex flex-wrap gap-1"> | |
| 59 | - | {#each binding.keys as key} | |
| 59 | + | {#each binding.keys as key (key)} | |
| 60 | 60 | <kbd class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono"> | |
| 61 | 61 | {formatKey(key)} | |
| 62 | 62 | </kbd> | |
| @@ -56,7 +56,7 @@ | |||||
| 56 | <Table.Cell class="text-muted-foreground">{binding.description}</Table.Cell> | 56 | <Table.Cell class="text-muted-foreground">{binding.description}</Table.Cell> | ||
| 57 | <Table.Cell> | 57 | <Table.Cell> | ||
| 58 | <div class="flex flex-wrap gap-1"> | 58 | <div class="flex flex-wrap gap-1"> | ||
| 59 | - | {#each binding.keys as key} | 59 | + | {#each binding.keys as key (key)} |
| 60 | <kbd class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono"> | 60 | <kbd class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono"> | ||
| 61 | {formatKey(key)} | 61 | {formatKey(key)} | ||
| 62 | </kbd> | 62 | </kbd> | ||
M webui/src/pages/config/KeybindingsTab.svelte +129 -70
| @@ -41,6 +41,13 @@ | |||
| 41 | 41 | let resetAllDialogOpen = $state(false); | |
| 42 | 42 | let successMessage = $state<string | null>(null); | |
| 43 | 43 | let conflicts = $state<KeybindingConflict[]>([]); | |
| 44 | + | let hasPendingReset = $state(false); | |
| 45 | + | let savingReset = $state(false); | |
| 46 | + | ||
| 47 | + | // Type-safe callback setup helper | |
| 48 | + | function setupCallback<T>(name: string, handler: (data: T) => void): void { | |
| 49 | + | (window as unknown as Record<string, (data: T) => void>)[name] = handler; | |
| 50 | + | } | |
| 44 | 51 | ||
| 45 | 52 | async function loadKeybindings() { | |
| 46 | 53 | loading = true; | |
| @@ -52,14 +59,14 @@ | |||
| 52 | 59 | return; | |
| 53 | 60 | } | |
| 54 | 61 | ||
| 55 | - | (window as any).__dumber_keybindings_loaded = (data: KeybindingsConfig) => { | |
| 62 | + | setupCallback<KeybindingsConfig>("__dumber_keybindings_loaded", (data) => { | |
| 56 | 63 | keybindings = data; | |
| 57 | 64 | loading = false; | |
| 58 | - | }; | |
| 59 | - | (window as any).__dumber_keybindings_error = (msg: string) => { | |
| 65 | + | }); | |
| 66 | + | setupCallback<string>("__dumber_keybindings_error", (msg) => { | |
| 60 | 67 | error = typeof msg === "string" ? msg : "Failed to load keybindings"; | |
| 61 | 68 | loading = false; | |
| 62 | - | }; | |
| 69 | + | }); | |
| 63 | 70 | ||
| 64 | 71 | try { | |
| 65 | 72 | postMessage({ | |
| @@ -83,45 +90,50 @@ | |||
| 83 | 90 | } | |
| 84 | 91 | ||
| 85 | 92 | async function saveBinding(keys: string[]) { | |
| 86 | - | if (!editingBinding) { | |
| 87 | - | return; | |
| 88 | - | } | |
| 93 | + | if (!editingBinding) return; | |
| 89 | 94 | ||
| 90 | 95 | if (!getWebKitBridge()) { | |
| 91 | 96 | error = "WebKit bridge not available"; | |
| 92 | 97 | return; | |
| 93 | 98 | } | |
| 94 | 99 | ||
| 95 | - | (window as any).__dumber_keybinding_set = (response: { status: string; conflicts?: KeybindingConflict[] }) => { | |
| 96 | - | // Verify we got a valid success response from backend | |
| 97 | - | if (!response || response.status !== "success") { | |
| 98 | - | error = "Failed to save keybinding: invalid response"; | |
| 99 | - | return; | |
| 100 | - | } | |
| 101 | - | if (response.conflicts && response.conflicts.length > 0) { | |
| 102 | - | conflicts = response.conflicts; | |
| 103 | - | } else { | |
| 104 | - | conflicts = []; | |
| 100 | + | // Timeout to detect if backend never responds (message silently dropped) | |
| 101 | + | const timeoutId = setTimeout(() => { | |
| 102 | + | error = "Save timed out - backend did not respond"; | |
| 103 | + | }, 5000); | |
| 104 | + | ||
| 105 | + | setupCallback<{ status: string; conflicts?: KeybindingConflict[] }>( | |
| 106 | + | "__dumber_keybinding_set", | |
| 107 | + | (response) => { | |
| 108 | + | clearTimeout(timeoutId); | |
| 109 | + | if (!response || response.status !== "success") { | |
| 110 | + | error = "Failed to save keybinding: invalid response"; | |
| 111 | + | return; | |
| 112 | + | } | |
| 113 | + | conflicts = response.conflicts?.length ? response.conflicts : []; | |
| 114 | + | loadKeybindings(); | |
| 115 | + | editingBinding = null; | |
| 116 | + | showSuccess("Keybinding saved successfully"); | |
| 105 | 117 | } | |
| 106 | - | loadKeybindings(); | |
| 107 | - | editingBinding = null; | |
| 108 | - | showSuccess("Keybinding saved successfully"); | |
| 109 | - | }; | |
| 110 | - | (window as any).__dumber_keybinding_set_error = (msg: string) => { | |
| 118 | + | ); | |
| 119 | + | setupCallback<string>("__dumber_keybinding_set_error", (msg) => { | |
| 120 | + | clearTimeout(timeoutId); | |
| 111 | 121 | error = typeof msg === "string" ? msg : "Failed to save keybinding"; | |
| 112 | - | }; | |
| 122 | + | }); | |
| 123 | + | ||
| 124 | + | // Create plain payload - extract primitive values explicitly to avoid any proxy issues | |
| 125 | + | const mode = String(editingBinding.mode); | |
| 126 | + | const action = String(editingBinding.action); | |
| 127 | + | const payload = { mode, action, keys: [...keys] }; | |
| 113 | 128 | ||
| 114 | 129 | try { | |
| 115 | 130 | postMessage({ | |
| 116 | 131 | type: "set_keybinding", | |
| 117 | 132 | webviewId: getWebViewId(), | |
| 118 | - | payload: { | |
| 119 | - | mode: editingBinding.mode, | |
| 120 | - | action: editingBinding.action, | |
| 121 | - | keys: keys, | |
| 122 | - | }, | |
| 133 | + | payload, | |
| 123 | 134 | }); | |
| 124 | 135 | } catch (err) { | |
| 136 | + | clearTimeout(timeoutId); | |
| 125 | 137 | error = err instanceof Error ? err.message : "Failed to save keybinding"; | |
| 126 | 138 | } | |
| 127 | 139 | } | |
| @@ -129,14 +141,14 @@ | |||
| 129 | 141 | async function resetBinding(mode: string, action: string) { | |
| 130 | 142 | if (!getWebKitBridge()) return; | |
| 131 | 143 | ||
| 132 | - | (window as any).__dumber_keybinding_reset = () => { | |
| 133 | - | conflicts = []; // Clear conflicts after reset | |
| 144 | + | setupCallback<void>("__dumber_keybinding_reset", () => { | |
| 145 | + | conflicts = []; | |
| 134 | 146 | loadKeybindings(); | |
| 135 | 147 | showSuccess("Keybinding reset to default"); | |
| 136 | - | }; | |
| 137 | - | (window as any).__dumber_keybinding_reset_error = (msg: string) => { | |
| 148 | + | }); | |
| 149 | + | setupCallback<string>("__dumber_keybinding_reset_error", (msg) => { | |
| 138 | 150 | error = typeof msg === "string" ? msg : "Failed to reset keybinding"; | |
| 139 | - | }; | |
| 151 | + | }); | |
| 140 | 152 | ||
| 141 | 153 | try { | |
| 142 | 154 | postMessage({ | |
| @@ -149,18 +161,43 @@ | |||
| 149 | 161 | } | |
| 150 | 162 | } | |
| 151 | 163 | ||
| 152 | - | async function resetAllBindings() { | |
| 164 | + | // Reset All: update local state to defaults WITHOUT saving | |
| 165 | + | function resetAllBindings() { | |
| 166 | + | if (!keybindings) return; | |
| 167 | + | ||
| 168 | + | // Reset each binding's keys to default_keys in local state | |
| 169 | + | const resetGroups = keybindings.groups.map(group => ({ | |
| 170 | + | ...group, | |
| 171 | + | bindings: group.bindings.map(binding => ({ | |
| 172 | + | ...binding, | |
| 173 | + | keys: [...binding.default_keys], | |
| 174 | + | is_custom: false, | |
| 175 | + | })), | |
| 176 | + | })); | |
| 177 | + | ||
| 178 | + | keybindings = { groups: resetGroups }; | |
| 179 | + | hasPendingReset = true; | |
| 180 | + | conflicts = []; | |
| 181 | + | resetAllDialogOpen = false; | |
| 182 | + | showSuccess("Preview: all keybindings reset to defaults. Click Save to apply."); | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // Actually save the reset to backend | |
| 186 | + | async function saveResetAll() { | |
| 153 | 187 | if (!getWebKitBridge()) return; | |
| 154 | 188 | ||
| 155 | - | (window as any).__dumber_keybindings_reset_all = () => { | |
| 156 | - | conflicts = []; // Clear conflicts after reset all | |
| 189 | + | savingReset = true; | |
| 190 | + | ||
| 191 | + | setupCallback<void>("__dumber_keybindings_reset_all", () => { | |
| 192 | + | hasPendingReset = false; | |
| 193 | + | savingReset = false; | |
| 157 | 194 | loadKeybindings(); | |
| 158 | - | resetAllDialogOpen = false; | |
| 159 | - | showSuccess("All keybindings reset to defaults"); | |
| 160 | - | }; | |
| 161 | - | (window as any).__dumber_keybindings_reset_all_error = (msg: string) => { | |
| 162 | - | error = typeof msg === "string" ? msg : "Failed to reset keybindings"; | |
| 163 | - | }; | |
| 195 | + | showSuccess("All keybindings saved successfully"); | |
| 196 | + | }); | |
| 197 | + | setupCallback<string>("__dumber_keybindings_reset_all_error", (msg) => { | |
| 198 | + | error = typeof msg === "string" ? msg : "Failed to save keybindings"; | |
| 199 | + | savingReset = false; | |
| 200 | + | }); | |
| 164 | 201 | ||
| 165 | 202 | try { | |
| 166 | 203 | postMessage({ | |
| @@ -169,10 +206,17 @@ | |||
| 169 | 206 | payload: {}, | |
| 170 | 207 | }); | |
| 171 | 208 | } catch (err) { | |
| 172 | - | error = err instanceof Error ? err.message : "Failed to reset keybindings"; | |
| 209 | + | error = err instanceof Error ? err.message : "Failed to save keybindings"; | |
| 210 | + | savingReset = false; | |
| 173 | 211 | } | |
| 174 | 212 | } | |
| 175 | 213 | ||
| 214 | + | // Cancel pending reset and reload from backend | |
| 215 | + | function cancelReset() { | |
| 216 | + | hasPendingReset = false; | |
| 217 | + | loadKeybindings(); | |
| 218 | + | } | |
| 219 | + | ||
| 176 | 220 | onMount(() => { | |
| 177 | 221 | loadKeybindings(); | |
| 178 | 222 | }); | |
| @@ -188,36 +232,51 @@ | |||
| 188 | 232 | </Card.Description> | |
| 189 | 233 | </div> | |
| 190 | 234 | <div class="flex gap-2"> | |
| 191 | - | <Button variant="outline" size="sm" onclick={loadKeybindings}> | |
| 192 | - | <RefreshCw class="mr-1 size-4" /> | |
| 193 | - | Refresh | |
| 194 | - | </Button> | |
| 195 | - | <AlertDialog.Root bind:open={resetAllDialogOpen}> | |
| 196 | - | <AlertDialog.Trigger> | |
| 197 | - | {#snippet child({ props })} | |
| 198 | - | <Button variant="outline" size="sm" {...props}> | |
| 199 | - | <RotateCcw class="mr-1 size-4" /> | |
| 200 | - | Reset All | |
| 201 | - | </Button> | |
| 202 | - | {/snippet} | |
| 203 | - | </AlertDialog.Trigger> | |
| 204 | - | <AlertDialog.Content> | |
| 205 | - | <AlertDialog.Header> | |
| 206 | - | <AlertDialog.Title>Reset all keybindings?</AlertDialog.Title> | |
| 207 | - | <AlertDialog.Description> | |
| 208 | - | This will restore all keybindings to their default values. This action cannot be undone. | |
| 209 | - | </AlertDialog.Description> | |
| 210 | - | </AlertDialog.Header> | |
| 211 | - | <AlertDialog.Footer> | |
| 212 | - | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |
| 213 | - | <AlertDialog.Action onclick={resetAllBindings}>Reset All</AlertDialog.Action> | |
| 214 | - | </AlertDialog.Footer> | |
| 215 | - | </AlertDialog.Content> | |
| 216 | - | </AlertDialog.Root> | |
| 235 | + | {#if hasPendingReset} | |
| 236 | + | <Button variant="outline" size="sm" onclick={cancelReset} disabled={savingReset}> | |
| 237 | + | Cancel | |
| 238 | + | </Button> | |
| 239 | + | <Button size="sm" onclick={saveResetAll} disabled={savingReset}> | |
| 240 | + | {savingReset ? "Saving…" : "Save"} | |
| 241 | + | </Button> | |
| 242 | + | {:else} | |
| 243 | + | <Button variant="outline" size="sm" onclick={loadKeybindings}> | |
| 244 | + | <RefreshCw class="mr-1 size-4" /> | |
| 245 | + | Refresh | |
| 246 | + | </Button> | |
| 247 | + | <AlertDialog.Root bind:open={resetAllDialogOpen}> | |
| 248 | + | <AlertDialog.Trigger> | |
| 249 | + | {#snippet child({ props })} | |
| 250 | + | <Button variant="outline" size="sm" {...props}> | |
| 251 | + | <RotateCcw class="mr-1 size-4" /> | |
| 252 | + | Reset All | |
| 253 | + | </Button> | |
| 254 | + | {/snippet} | |
| 255 | + | </AlertDialog.Trigger> | |
| 256 | + | <AlertDialog.Content> | |
| 257 | + | <AlertDialog.Header> | |
| 258 | + | <AlertDialog.Title>Reset all keybindings?</AlertDialog.Title> | |
| 259 | + | <AlertDialog.Description> | |
| 260 | + | This will preview all keybindings with their default values. You'll need to click Save to apply. | |
| 261 | + | </AlertDialog.Description> | |
| 262 | + | </AlertDialog.Header> | |
| 263 | + | <AlertDialog.Footer> | |
| 264 | + | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |
| 265 | + | <AlertDialog.Action onclick={resetAllBindings}>Reset All</AlertDialog.Action> | |
| 266 | + | </AlertDialog.Footer> | |
| 267 | + | </AlertDialog.Content> | |
| 268 | + | </AlertDialog.Root> | |
| 269 | + | {/if} | |
| 217 | 270 | </div> | |
| 218 | 271 | </div> | |
| 219 | 272 | </Card.Header> | |
| 220 | 273 | <Card.Content class="space-y-6"> | |
| 274 | + | {#if hasPendingReset} | |
| 275 | + | <div class="flex items-center gap-2 rounded-md border border-yellow-500/50 bg-yellow-500/10 px-4 py-3 text-sm text-yellow-600 dark:text-yellow-400"> | |
| 276 | + | <AlertTriangle class="size-4" /> | |
| 277 | + | <span>Unsaved changes: all keybindings previewed with defaults. Click <strong>Save</strong> to apply or <strong>Cancel</strong> to discard.</span> | |
| 278 | + | </div> | |
| 279 | + | {/if} | |
| 221 | 280 | {#if loading} | |
| 222 | 281 | <div class="flex items-center justify-center py-8"> | |
| 223 | 282 | <Spinner class="size-6" /> | |
| @@ -241,7 +300,7 @@ | |||
| 241 | 300 | <span class="font-medium">Keybinding conflicts detected</span> | |
| 242 | 301 | </div> | |
| 243 | 302 | <ul class="mt-2 list-disc pl-6 text-muted-foreground"> | |
| 244 | - | {#each conflicts as conflict} | |
| 303 | + | {#each conflicts as conflict (conflict.key + conflict.conflicting_action)} | |
| 245 | 304 | <li> | |
| 246 | 305 | <kbd class="font-mono text-xs">{conflict.key}</kbd> is also bound to | |
| 247 | 306 | <strong>{conflict.conflicting_action}</strong> in <em>{conflict.conflicting_mode}</em> mode | |
| @@ -41,6 +41,13 @@ | |||||
| 41 | let resetAllDialogOpen = $state(false); | 41 | let resetAllDialogOpen = $state(false); | ||
| 42 | let successMessage = $state<string | null>(null); | 42 | let successMessage = $state<string | null>(null); | ||
| 43 | let conflicts = $state<KeybindingConflict[]>([]); | 43 | let conflicts = $state<KeybindingConflict[]>([]); | ||
| 44 | + | let hasPendingReset = $state(false); | |||
| 45 | + | let savingReset = $state(false); | |||
| 46 | + | ||||
| 47 | + | // Type-safe callback setup helper | |||
| 48 | + | function setupCallback<T>(name: string, handler: (data: T) => void): void { | |||
| 49 | + | (window as unknown as Record<string, (data: T) => void>)[name] = handler; | |||
| 50 | + | } | |||
| 44 | 51 | ||||
| 45 | async function loadKeybindings() { | 52 | async function loadKeybindings() { | ||
| 46 | loading = true; | 53 | loading = true; | ||
| @@ -52,14 +59,14 @@ | |||||
| 52 | return; | 59 | return; | ||
| 53 | } | 60 | } | ||
| 54 | 61 | ||||
| 55 | - | (window as any).__dumber_keybindings_loaded = (data: KeybindingsConfig) => { | 62 | + | setupCallback<KeybindingsConfig>("__dumber_keybindings_loaded", (data) => { |
| 56 | keybindings = data; | 63 | keybindings = data; | ||
| 57 | loading = false; | 64 | loading = false; | ||
| 58 | - | }; | 65 | + | }); |
| 59 | - | (window as any).__dumber_keybindings_error = (msg: string) => { | 66 | + | setupCallback<string>("__dumber_keybindings_error", (msg) => { |
| 60 | error = typeof msg === "string" ? msg : "Failed to load keybindings"; | 67 | error = typeof msg === "string" ? msg : "Failed to load keybindings"; | ||
| 61 | loading = false; | 68 | loading = false; | ||
| 62 | - | }; | 69 | + | }); |
| 63 | 70 | ||||
| 64 | try { | 71 | try { | ||
| 65 | postMessage({ | 72 | postMessage({ | ||
| @@ -83,45 +90,50 @@ | |||||
| 83 | } | 90 | } | ||
| 84 | 91 | ||||
| 85 | async function saveBinding(keys: string[]) { | 92 | async function saveBinding(keys: string[]) { | ||
| 86 | - | if (!editingBinding) { | 93 | + | if (!editingBinding) return; |
| 87 | - | return; | |||
| 88 | - | } | |||
| 89 | 94 | ||||
| 90 | if (!getWebKitBridge()) { | 95 | if (!getWebKitBridge()) { | ||
| 91 | error = "WebKit bridge not available"; | 96 | error = "WebKit bridge not available"; | ||
| 92 | return; | 97 | return; | ||
| 93 | } | 98 | } | ||
| 94 | 99 | ||||
| 95 | - | (window as any).__dumber_keybinding_set = (response: { status: string; conflicts?: KeybindingConflict[] }) => { | 100 | + | // Timeout to detect if backend never responds (message silently dropped) |
| 96 | - | // Verify we got a valid success response from backend | 101 | + | const timeoutId = setTimeout(() => { |
| 97 | - | if (!response || response.status !== "success") { | 102 | + | error = "Save timed out - backend did not respond"; |
| 98 | - | error = "Failed to save keybinding: invalid response"; | 103 | + | }, 5000); |
| 99 | - | return; | 104 | + | |
| 100 | - | } | 105 | + | setupCallback<{ status: string; conflicts?: KeybindingConflict[] }>( |
| 101 | - | if (response.conflicts && response.conflicts.length > 0) { | 106 | + | "__dumber_keybinding_set", |
| 102 | - | conflicts = response.conflicts; | 107 | + | (response) => { |
| 103 | - | } else { | 108 | + | clearTimeout(timeoutId); |
| 104 | - | conflicts = []; | 109 | + | if (!response || response.status !== "success") { |
| 110 | + | error = "Failed to save keybinding: invalid response"; | |||
| 111 | + | return; | |||
| 112 | + | } | |||
| 113 | + | conflicts = response.conflicts?.length ? response.conflicts : []; | |||
| 114 | + | loadKeybindings(); | |||
| 115 | + | editingBinding = null; | |||
| 116 | + | showSuccess("Keybinding saved successfully"); | |||
| 105 | } | 117 | } | ||
| 106 | - | loadKeybindings(); | 118 | + | ); |
| 107 | - | editingBinding = null; | 119 | + | setupCallback<string>("__dumber_keybinding_set_error", (msg) => { |
| 108 | - | showSuccess("Keybinding saved successfully"); | 120 | + | clearTimeout(timeoutId); |
| 109 | - | }; | |||
| 110 | - | (window as any).__dumber_keybinding_set_error = (msg: string) => { | |||
| 111 | error = typeof msg === "string" ? msg : "Failed to save keybinding"; | 121 | error = typeof msg === "string" ? msg : "Failed to save keybinding"; | ||
| 112 | - | }; | 122 | + | }); |
| 123 | + | ||||
| 124 | + | // Create plain payload - extract primitive values explicitly to avoid any proxy issues | |||
| 125 | + | const mode = String(editingBinding.mode); | |||
| 126 | + | const action = String(editingBinding.action); | |||
| 127 | + | const payload = { mode, action, keys: [...keys] }; | |||
| 113 | 128 | ||||
| 114 | try { | 129 | try { | ||
| 115 | postMessage({ | 130 | postMessage({ | ||
| 116 | type: "set_keybinding", | 131 | type: "set_keybinding", | ||
| 117 | webviewId: getWebViewId(), | 132 | webviewId: getWebViewId(), | ||
| 118 | - | payload: { | 133 | + | payload, |
| 119 | - | mode: editingBinding.mode, | |||
| 120 | - | action: editingBinding.action, | |||
| 121 | - | keys: keys, | |||
| 122 | - | }, | |||
| 123 | }); | 134 | }); | ||
| 124 | } catch (err) { | 135 | } catch (err) { | ||
| 136 | + | clearTimeout(timeoutId); | |||
| 125 | error = err instanceof Error ? err.message : "Failed to save keybinding"; | 137 | error = err instanceof Error ? err.message : "Failed to save keybinding"; | ||
| 126 | } | 138 | } | ||
| 127 | } | 139 | } | ||
| @@ -129,14 +141,14 @@ | |||||
| 129 | async function resetBinding(mode: string, action: string) { | 141 | async function resetBinding(mode: string, action: string) { | ||
| 130 | if (!getWebKitBridge()) return; | 142 | if (!getWebKitBridge()) return; | ||
| 131 | 143 | ||||
| 132 | - | (window as any).__dumber_keybinding_reset = () => { | 144 | + | setupCallback<void>("__dumber_keybinding_reset", () => { |
| 133 | - | conflicts = []; // Clear conflicts after reset | 145 | + | conflicts = []; |
| 134 | loadKeybindings(); | 146 | loadKeybindings(); | ||
| 135 | showSuccess("Keybinding reset to default"); | 147 | showSuccess("Keybinding reset to default"); | ||
| 136 | - | }; | 148 | + | }); |
| 137 | - | (window as any).__dumber_keybinding_reset_error = (msg: string) => { | 149 | + | setupCallback<string>("__dumber_keybinding_reset_error", (msg) => { |
| 138 | error = typeof msg === "string" ? msg : "Failed to reset keybinding"; | 150 | error = typeof msg === "string" ? msg : "Failed to reset keybinding"; | ||
| 139 | - | }; | 151 | + | }); |
| 140 | 152 | ||||
| 141 | try { | 153 | try { | ||
| 142 | postMessage({ | 154 | postMessage({ | ||
| @@ -149,18 +161,43 @@ | |||||
| 149 | } | 161 | } | ||
| 150 | } | 162 | } | ||
| 151 | 163 | ||||
| 152 | - | async function resetAllBindings() { | 164 | + | // Reset All: update local state to defaults WITHOUT saving |
| 165 | + | function resetAllBindings() { | |||
| 166 | + | if (!keybindings) return; | |||
| 167 | + | ||||
| 168 | + | // Reset each binding's keys to default_keys in local state | |||
| 169 | + | const resetGroups = keybindings.groups.map(group => ({ | |||
| 170 | + | ...group, | |||
| 171 | + | bindings: group.bindings.map(binding => ({ | |||
| 172 | + | ...binding, | |||
| 173 | + | keys: [...binding.default_keys], | |||
| 174 | + | is_custom: false, | |||
| 175 | + | })), | |||
| 176 | + | })); | |||
| 177 | + | ||||
| 178 | + | keybindings = { groups: resetGroups }; | |||
| 179 | + | hasPendingReset = true; | |||
| 180 | + | conflicts = []; | |||
| 181 | + | resetAllDialogOpen = false; | |||
| 182 | + | showSuccess("Preview: all keybindings reset to defaults. Click Save to apply."); | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | // Actually save the reset to backend | |||
| 186 | + | async function saveResetAll() { | |||
| 153 | if (!getWebKitBridge()) return; | 187 | if (!getWebKitBridge()) return; | ||
| 154 | 188 | ||||
| 155 | - | (window as any).__dumber_keybindings_reset_all = () => { | 189 | + | savingReset = true; |
| 156 | - | conflicts = []; // Clear conflicts after reset all | 190 | + | |
| 191 | + | setupCallback<void>("__dumber_keybindings_reset_all", () => { | |||
| 192 | + | hasPendingReset = false; | |||
| 193 | + | savingReset = false; | |||
| 157 | loadKeybindings(); | 194 | loadKeybindings(); | ||
| 158 | - | resetAllDialogOpen = false; | 195 | + | showSuccess("All keybindings saved successfully"); |
| 159 | - | showSuccess("All keybindings reset to defaults"); | 196 | + | }); |
| 160 | - | }; | 197 | + | setupCallback<string>("__dumber_keybindings_reset_all_error", (msg) => { |
| 161 | - | (window as any).__dumber_keybindings_reset_all_error = (msg: string) => { | 198 | + | error = typeof msg === "string" ? msg : "Failed to save keybindings"; |
| 162 | - | error = typeof msg === "string" ? msg : "Failed to reset keybindings"; | 199 | + | savingReset = false; |
| 163 | - | }; | 200 | + | }); |
| 164 | 201 | ||||
| 165 | try { | 202 | try { | ||
| 166 | postMessage({ | 203 | postMessage({ | ||
| @@ -169,10 +206,17 @@ | |||||
| 169 | payload: {}, | 206 | payload: {}, | ||
| 170 | }); | 207 | }); | ||
| 171 | } catch (err) { | 208 | } catch (err) { | ||
| 172 | - | error = err instanceof Error ? err.message : "Failed to reset keybindings"; | 209 | + | error = err instanceof Error ? err.message : "Failed to save keybindings"; |
| 210 | + | savingReset = false; | |||
| 173 | } | 211 | } | ||
| 174 | } | 212 | } | ||
| 175 | 213 | ||||
| 214 | + | // Cancel pending reset and reload from backend | |||
| 215 | + | function cancelReset() { | |||
| 216 | + | hasPendingReset = false; | |||
| 217 | + | loadKeybindings(); | |||
| 218 | + | } | |||
| 219 | + | ||||
| 176 | onMount(() => { | 220 | onMount(() => { | ||
| 177 | loadKeybindings(); | 221 | loadKeybindings(); | ||
| 178 | }); | 222 | }); | ||
| @@ -188,36 +232,51 @@ | |||||
| 188 | </Card.Description> | 232 | </Card.Description> | ||
| 189 | </div> | 233 | </div> | ||
| 190 | <div class="flex gap-2"> | 234 | <div class="flex gap-2"> | ||
| 191 | - | <Button variant="outline" size="sm" onclick={loadKeybindings}> | 235 | + | {#if hasPendingReset} |
| 192 | - | <RefreshCw class="mr-1 size-4" /> | 236 | + | <Button variant="outline" size="sm" onclick={cancelReset} disabled={savingReset}> |
| 193 | - | Refresh | 237 | + | Cancel |
| 194 | - | </Button> | 238 | + | </Button> |
| 195 | - | <AlertDialog.Root bind:open={resetAllDialogOpen}> | 239 | + | <Button size="sm" onclick={saveResetAll} disabled={savingReset}> |
| 196 | - | <AlertDialog.Trigger> | 240 | + | {savingReset ? "Saving…" : "Save"} |
| 197 | - | {#snippet child({ props })} | 241 | + | </Button> |
| 198 | - | <Button variant="outline" size="sm" {...props}> | 242 | + | {:else} |
| 199 | - | <RotateCcw class="mr-1 size-4" /> | 243 | + | <Button variant="outline" size="sm" onclick={loadKeybindings}> |
| 200 | - | Reset All | 244 | + | <RefreshCw class="mr-1 size-4" /> |
| 201 | - | </Button> | 245 | + | Refresh |
| 202 | - | {/snippet} | 246 | + | </Button> |
| 203 | - | </AlertDialog.Trigger> | 247 | + | <AlertDialog.Root bind:open={resetAllDialogOpen}> |
| 204 | - | <AlertDialog.Content> | 248 | + | <AlertDialog.Trigger> |
| 205 | - | <AlertDialog.Header> | 249 | + | {#snippet child({ props })} |
| 206 | - | <AlertDialog.Title>Reset all keybindings?</AlertDialog.Title> | 250 | + | <Button variant="outline" size="sm" {...props}> |
| 207 | - | <AlertDialog.Description> | 251 | + | <RotateCcw class="mr-1 size-4" /> |
| 208 | - | This will restore all keybindings to their default values. This action cannot be undone. | 252 | + | Reset All |
| 209 | - | </AlertDialog.Description> | 253 | + | </Button> |
| 210 | - | </AlertDialog.Header> | 254 | + | {/snippet} |
| 211 | - | <AlertDialog.Footer> | 255 | + | </AlertDialog.Trigger> |
| 212 | - | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | 256 | + | <AlertDialog.Content> |
| 213 | - | <AlertDialog.Action onclick={resetAllBindings}>Reset All</AlertDialog.Action> | 257 | + | <AlertDialog.Header> |
| 214 | - | </AlertDialog.Footer> | 258 | + | <AlertDialog.Title>Reset all keybindings?</AlertDialog.Title> |
| 215 | - | </AlertDialog.Content> | 259 | + | <AlertDialog.Description> |
| 216 | - | </AlertDialog.Root> | 260 | + | This will preview all keybindings with their default values. You'll need to click Save to apply. |
| 261 | + | </AlertDialog.Description> | |||
| 262 | + | </AlertDialog.Header> | |||
| 263 | + | <AlertDialog.Footer> | |||
| 264 | + | <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> | |||
| 265 | + | <AlertDialog.Action onclick={resetAllBindings}>Reset All</AlertDialog.Action> | |||
| 266 | + | </AlertDialog.Footer> | |||
| 267 | + | </AlertDialog.Content> | |||
| 268 | + | </AlertDialog.Root> | |||
| 269 | + | {/if} | |||
| 217 | </div> | 270 | </div> | ||
| 218 | </div> | 271 | </div> | ||
| 219 | </Card.Header> | 272 | </Card.Header> | ||
| 220 | <Card.Content class="space-y-6"> | 273 | <Card.Content class="space-y-6"> | ||
| 274 | + | {#if hasPendingReset} | |||
| 275 | + | <div class="flex items-center gap-2 rounded-md border border-yellow-500/50 bg-yellow-500/10 px-4 py-3 text-sm text-yellow-600 dark:text-yellow-400"> | |||
| 276 | + | <AlertTriangle class="size-4" /> | |||
| 277 | + | <span>Unsaved changes: all keybindings previewed with defaults. Click <strong>Save</strong> to apply or <strong>Cancel</strong> to discard.</span> | |||
| 278 | + | </div> | |||
| 279 | + | {/if} | |||
| 221 | {#if loading} | 280 | {#if loading} | ||
| 222 | <div class="flex items-center justify-center py-8"> | 281 | <div class="flex items-center justify-center py-8"> | ||
| 223 | <Spinner class="size-6" /> | 282 | <Spinner class="size-6" /> | ||
| @@ -241,7 +300,7 @@ | |||||
| 241 | <span class="font-medium">Keybinding conflicts detected</span> | 300 | <span class="font-medium">Keybinding conflicts detected</span> | ||
| 242 | </div> | 301 | </div> | ||
| 243 | <ul class="mt-2 list-disc pl-6 text-muted-foreground"> | 302 | <ul class="mt-2 list-disc pl-6 text-muted-foreground"> | ||
| 244 | - | {#each conflicts as conflict} | 303 | + | {#each conflicts as conflict (conflict.key + conflict.conflicting_action)} |
| 245 | <li> | 304 | <li> | ||
| 246 | <kbd class="font-mono text-xs">{conflict.key}</kbd> is also bound to | 305 | <kbd class="font-mono text-xs">{conflict.key}</kbd> is also bound to | ||
| 247 | <strong>{conflict.conflicting_action}</strong> in <em>{conflict.conflicting_mode}</em> mode | 306 | <strong>{conflict.conflicting_action}</strong> in <em>{conflict.conflicting_mode}</em> mode | ||
M webui/src/pages/homepage/analytics/HourlyActivityChart.svelte +1 -0
| @@ -19,6 +19,7 @@ | |||
| 19 | 19 | ||
| 20 | 20 | // Convert data to Frappe Charts format (ensure all 24 hours) | |
| 21 | 21 | const chartData = $derived.by(() => { | |
| 22 | + | // eslint-disable-next-line svelte/prefer-svelte-reactivity -- temporary Map for data transformation | |
| 22 | 23 | const visitsByHour = new Map<number, number>(); | |
| 23 | 24 | hourlyDistribution.forEach(h => visitsByHour.set(h.hour, h.visit_count)); | |
| 24 | 25 | ||
| @@ -19,6 +19,7 @@ | |||||
| 19 | 19 | ||||
| 20 | // Convert data to Frappe Charts format (ensure all 24 hours) | 20 | // Convert data to Frappe Charts format (ensure all 24 hours) | ||
| 21 | const chartData = $derived.by(() => { | 21 | const chartData = $derived.by(() => { | ||
| 22 | + | // eslint-disable-next-line svelte/prefer-svelte-reactivity -- temporary Map for data transformation | |||
| 22 | const visitsByHour = new Map<number, number>(); | 23 | const visitsByHour = new Map<number, number>(); | ||
| 23 | hourlyDistribution.forEach(h => visitsByHour.set(h.hour, h.visit_count)); | 24 | hourlyDistribution.forEach(h => visitsByHour.set(h.hour, h.visit_count)); | ||
| 24 | 25 | ||||
M webui/src/pages/homepage/favorites/FavoriteEditor.svelte +1 -1
| @@ -139,7 +139,7 @@ | |||
| 139 | 139 | <label class="form-label" for="fav-shortcut">QUICK ACCESS KEY</label> | |
| 140 | 140 | <select id="fav-shortcut" class="form-select" bind:value={shortcutKey}> | |
| 141 | 141 | <option value={null}>— None —</option> | |
| 142 | - | {#each [1, 2, 3, 4, 5, 6, 7, 8, 9] as key} | |
| 142 | + | {#each [1, 2, 3, 4, 5, 6, 7, 8, 9] as key (key)} | |
| 143 | 143 | <option value={key}>{key}</option> | |
| 144 | 144 | {/each} | |
| 145 | 145 | </select> | |
| @@ -139,7 +139,7 @@ | |||||
| 139 | <label class="form-label" for="fav-shortcut">QUICK ACCESS KEY</label> | 139 | <label class="form-label" for="fav-shortcut">QUICK ACCESS KEY</label> | ||
| 140 | <select id="fav-shortcut" class="form-select" bind:value={shortcutKey}> | 140 | <select id="fav-shortcut" class="form-select" bind:value={shortcutKey}> | ||
| 141 | <option value={null}>— None —</option> | 141 | <option value={null}>— None —</option> | ||
| 142 | - | {#each [1, 2, 3, 4, 5, 6, 7, 8, 9] as key} | 142 | + | {#each [1, 2, 3, 4, 5, 6, 7, 8, 9] as key (key)} |
| 143 | <option value={key}>{key}</option> | 143 | <option value={key}>{key}</option> | ||
| 144 | {/each} | 144 | {/each} | ||
| 145 | </select> | 145 | </select> | ||
M webui/src/pages/homepage/panels/HistoryPanel.svelte +2 -0
| @@ -37,6 +37,7 @@ | |||
| 37 | 37 | }); | |
| 38 | 38 | ||
| 39 | 39 | // Recompute timeline from filtered entries | |
| 40 | + | /* eslint-disable svelte/prefer-svelte-reactivity -- temporary Map/Date for data grouping, not reactive state */ | |
| 40 | 41 | const displayedTimeline = $derived(() => { | |
| 41 | 42 | const entries = displayedEntries(); | |
| 42 | 43 | if (entries.length === 0) return []; | |
| @@ -78,6 +79,7 @@ | |||
| 78 | 79 | ||
| 79 | 80 | return result.sort((a, b) => b.date.localeCompare(a.date)); | |
| 80 | 81 | }); | |
| 82 | + | /* eslint-enable svelte/prefer-svelte-reactivity */ | |
| 81 | 83 | ||
| 82 | 84 | const handleSelectEntry = (entry: HistoryEntry) => { | |
| 83 | 85 | navigateTo(entry.url); | |
| @@ -37,6 +37,7 @@ | |||||
| 37 | }); | 37 | }); | ||
| 38 | 38 | ||||
| 39 | // Recompute timeline from filtered entries | 39 | // Recompute timeline from filtered entries | ||
| 40 | + | /* eslint-disable svelte/prefer-svelte-reactivity -- temporary Map/Date for data grouping, not reactive state */ | |||
| 40 | const displayedTimeline = $derived(() => { | 41 | const displayedTimeline = $derived(() => { | ||
| 41 | const entries = displayedEntries(); | 42 | const entries = displayedEntries(); | ||
| 42 | if (entries.length === 0) return []; | 43 | if (entries.length === 0) return []; | ||
| @@ -78,6 +79,7 @@ | |||||
| 78 | 79 | ||||
| 79 | return result.sort((a, b) => b.date.localeCompare(a.date)); | 80 | return result.sort((a, b) => b.date.localeCompare(a.date)); | ||
| 80 | }); | 81 | }); | ||
| 82 | + | /* eslint-enable svelte/prefer-svelte-reactivity */ | |||
| 81 | 83 | ||||
| 82 | const handleSelectEntry = (entry: HistoryEntry) => { | 84 | const handleSelectEntry = (entry: HistoryEntry) => { | ||
| 83 | navigateTo(entry.url); | 85 | navigateTo(entry.url); | ||
M webui/src/pages/homepage/shell/CommandPalette.svelte +1 -1
| @@ -165,7 +165,7 @@ | |||
| 165 | 165 | ||
| 166 | 166 | // Reset selection when query changes | |
| 167 | 167 | $effect(() => { | |
| 168 | - | query; // Track query | |
| 168 | + | void query; // Track query changes | |
| 169 | 169 | selectedIndex = 0; | |
| 170 | 170 | }); | |
| 171 | 171 | ||
| @@ -165,7 +165,7 @@ | |||||
| 165 | 165 | ||||
| 166 | // Reset selection when query changes | 166 | // Reset selection when query changes | ||
| 167 | $effect(() => { | 167 | $effect(() => { | ||
| 168 | - | query; // Track query | 168 | + | void query; // Track query changes |
| 169 | selectedIndex = 0; | 169 | selectedIndex = 0; | ||
| 170 | }); | 170 | }); | ||
| 171 | 171 | ||||
M webui/src/pages/homepage/shell/HomepageShell.svelte +6 -9
| @@ -35,14 +35,12 @@ | |||
| 35 | 35 | }; | |
| 36 | 36 | ||
| 37 | 37 | function navigateWithViewTransition(url: string) { | |
| 38 | - | const doc = document as any; | |
| 39 | - | ||
| 40 | - | if (typeof doc?.startViewTransition !== 'function') { | |
| 38 | + | if (typeof document.startViewTransition !== 'function') { | |
| 41 | 39 | window.location.href = url; | |
| 42 | 40 | return; | |
| 43 | 41 | } | |
| 44 | 42 | ||
| 45 | - | const transition = doc.startViewTransition(() => { | |
| 43 | + | const transition = document.startViewTransition(() => { | |
| 46 | 44 | document.documentElement.dataset.vt = 'leaving'; | |
| 47 | 45 | }); | |
| 48 | 46 | ||
| @@ -53,17 +51,15 @@ | |||
| 53 | 51 | ||
| 54 | 52 | const toggleTheme = () => { | |
| 55 | 53 | const nextMode = themeMode === 'dark' ? 'light' : 'dark'; | |
| 56 | - | const manager = (window as any).__dumber_color_scheme_manager as | |
| 57 | - | | { setUserPreference?: (theme: ThemeMode) => void } | |
| 58 | - | | undefined; | |
| 54 | + | const manager = window.__dumber_color_scheme_manager; | |
| 59 | 55 | ||
| 60 | 56 | if (manager?.setUserPreference) { | |
| 61 | 57 | manager.setUserPreference(nextMode); | |
| 62 | 58 | return; | |
| 63 | 59 | } | |
| 64 | 60 | ||
| 65 | - | if ((window as any).__dumber_setTheme) { | |
| 66 | - | (window as any).__dumber_setTheme(nextMode); | |
| 61 | + | if (window.__dumber_setTheme) { | |
| 62 | + | window.__dumber_setTheme(nextMode); | |
| 67 | 63 | localStorage.setItem('dumber.theme', nextMode); | |
| 68 | 64 | return; | |
| 69 | 65 | } | |
| @@ -109,6 +105,7 @@ | |||
| 109 | 105 | <svelte:head> | |
| 110 | 106 | <title>dumb://home</title> | |
| 111 | 107 | <meta name="description" content="Dumber Browser - Homepage" /> | |
| 108 | + | <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static styles for shell initialization --> | |
| 112 | 109 | {@html `<style> | |
| 113 | 110 | html, body { margin: 0; padding: 0; } | |
| 114 | 111 | html { background: var(--background, #0a0a0a); } | |
| @@ -35,14 +35,12 @@ | |||||
| 35 | }; | 35 | }; | ||
| 36 | 36 | ||||
| 37 | function navigateWithViewTransition(url: string) { | 37 | function navigateWithViewTransition(url: string) { | ||
| 38 | - | const doc = document as any; | 38 | + | if (typeof document.startViewTransition !== 'function') { |
| 39 | - | ||||
| 40 | - | if (typeof doc?.startViewTransition !== 'function') { | |||
| 41 | window.location.href = url; | 39 | window.location.href = url; | ||
| 42 | return; | 40 | return; | ||
| 43 | } | 41 | } | ||
| 44 | 42 | ||||
| 45 | - | const transition = doc.startViewTransition(() => { | 43 | + | const transition = document.startViewTransition(() => { |
| 46 | document.documentElement.dataset.vt = 'leaving'; | 44 | document.documentElement.dataset.vt = 'leaving'; | ||
| 47 | }); | 45 | }); | ||
| 48 | 46 | ||||
| @@ -53,17 +51,15 @@ | |||||
| 53 | 51 | ||||
| 54 | const toggleTheme = () => { | 52 | const toggleTheme = () => { | ||
| 55 | const nextMode = themeMode === 'dark' ? 'light' : 'dark'; | 53 | const nextMode = themeMode === 'dark' ? 'light' : 'dark'; | ||
| 56 | - | const manager = (window as any).__dumber_color_scheme_manager as | 54 | + | const manager = window.__dumber_color_scheme_manager; |
| 57 | - | | { setUserPreference?: (theme: ThemeMode) => void } | |||
| 58 | - | | undefined; | |||
| 59 | 55 | ||||
| 60 | if (manager?.setUserPreference) { | 56 | if (manager?.setUserPreference) { | ||
| 61 | manager.setUserPreference(nextMode); | 57 | manager.setUserPreference(nextMode); | ||
| 62 | return; | 58 | return; | ||
| 63 | } | 59 | } | ||
| 64 | 60 | ||||
| 65 | - | if ((window as any).__dumber_setTheme) { | 61 | + | if (window.__dumber_setTheme) { |
| 66 | - | (window as any).__dumber_setTheme(nextMode); | 62 | + | window.__dumber_setTheme(nextMode); |
| 67 | localStorage.setItem('dumber.theme', nextMode); | 63 | localStorage.setItem('dumber.theme', nextMode); | ||
| 68 | return; | 64 | return; | ||
| 69 | } | 65 | } | ||
| @@ -109,6 +105,7 @@ | |||||
| 109 | <svelte:head> | 105 | <svelte:head> | ||
| 110 | <title>dumb://home</title> | 106 | <title>dumb://home</title> | ||
| 111 | <meta name="description" content="Dumber Browser - Homepage" /> | 107 | <meta name="description" content="Dumber Browser - Homepage" /> | ||
| 108 | + | <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static styles for shell initialization --> | |||
| 112 | {@html `<style> | 109 | {@html `<style> | ||
| 113 | html, body { margin: 0; padding: 0; } | 110 | html, body { margin: 0; padding: 0; } | ||
| 114 | html { background: var(--background, #0a0a0a); } | 111 | html { background: var(--background, #0a0a0a); } | ||
M webui/src/pages/homepage/shell/KeyboardHints.svelte +4 -4
| @@ -136,7 +136,7 @@ | |||
| 136 | 136 | {#each navHints as hint (hint.description)} | |
| 137 | 137 | <div class="hint-row"> | |
| 138 | 138 | <div class="hint-keys"> | |
| 139 | - | {#each hint.keys as key, i} | |
| 139 | + | {#each hint.keys as key, i (key)} | |
| 140 | 140 | {#if i > 0}<span class="key-sep">/</span>{/if} | |
| 141 | 141 | <kbd>{key}</kbd> | |
| 142 | 142 | {/each} | |
| @@ -157,7 +157,7 @@ | |||
| 157 | 157 | {#each panelSwitchHints as hint (hint.description)} | |
| 158 | 158 | <div class="hint-row"> | |
| 159 | 159 | <div class="hint-keys"> | |
| 160 | - | {#each hint.keys as key, i} | |
| 160 | + | {#each hint.keys as key, i (key)} | |
| 161 | 161 | {#if i > 0}<span class="key-sep">/</span>{/if} | |
| 162 | 162 | <kbd>{key}</kbd> | |
| 163 | 163 | {/each} | |
| @@ -179,7 +179,7 @@ | |||
| 179 | 179 | {#each panelHints as hint (hint.description)} | |
| 180 | 180 | <div class="hint-row"> | |
| 181 | 181 | <div class="hint-keys"> | |
| 182 | - | {#each hint.keys as key, i} | |
| 182 | + | {#each hint.keys as key, i (key)} | |
| 183 | 183 | {#if i > 0}<span class="key-sep">/</span>{/if} | |
| 184 | 184 | <kbd>{key}</kbd> | |
| 185 | 185 | {/each} | |
| @@ -201,7 +201,7 @@ | |||
| 201 | 201 | {#each quickAccessHints as hint (hint.description)} | |
| 202 | 202 | <div class="hint-row"> | |
| 203 | 203 | <div class="hint-keys"> | |
| 204 | - | {#each hint.keys as key, i} | |
| 204 | + | {#each hint.keys as key, i (key)} | |
| 205 | 205 | {#if i > 0}<span class="key-sep">/</span>{/if} | |
| 206 | 206 | <kbd>{key}</kbd> | |
| 207 | 207 | {/each} | |
| @@ -136,7 +136,7 @@ | |||||
| 136 | {#each navHints as hint (hint.description)} | 136 | {#each navHints as hint (hint.description)} | ||
| 137 | <div class="hint-row"> | 137 | <div class="hint-row"> | ||
| 138 | <div class="hint-keys"> | 138 | <div class="hint-keys"> | ||
| 139 | - | {#each hint.keys as key, i} | 139 | + | {#each hint.keys as key, i (key)} |
| 140 | {#if i > 0}<span class="key-sep">/</span>{/if} | 140 | {#if i > 0}<span class="key-sep">/</span>{/if} | ||
| 141 | <kbd>{key}</kbd> | 141 | <kbd>{key}</kbd> | ||
| 142 | {/each} | 142 | {/each} | ||
| @@ -157,7 +157,7 @@ | |||||
| 157 | {#each panelSwitchHints as hint (hint.description)} | 157 | {#each panelSwitchHints as hint (hint.description)} | ||
| 158 | <div class="hint-row"> | 158 | <div class="hint-row"> | ||
| 159 | <div class="hint-keys"> | 159 | <div class="hint-keys"> | ||
| 160 | - | {#each hint.keys as key, i} | 160 | + | {#each hint.keys as key, i (key)} |
| 161 | {#if i > 0}<span class="key-sep">/</span>{/if} | 161 | {#if i > 0}<span class="key-sep">/</span>{/if} | ||
| 162 | <kbd>{key}</kbd> | 162 | <kbd>{key}</kbd> | ||
| 163 | {/each} | 163 | {/each} | ||
| @@ -179,7 +179,7 @@ | |||||
| 179 | {#each panelHints as hint (hint.description)} | 179 | {#each panelHints as hint (hint.description)} | ||
| 180 | <div class="hint-row"> | 180 | <div class="hint-row"> | ||
| 181 | <div class="hint-keys"> | 181 | <div class="hint-keys"> | ||
| 182 | - | {#each hint.keys as key, i} | 182 | + | {#each hint.keys as key, i (key)} |
| 183 | {#if i > 0}<span class="key-sep">/</span>{/if} | 183 | {#if i > 0}<span class="key-sep">/</span>{/if} | ||
| 184 | <kbd>{key}</kbd> | 184 | <kbd>{key}</kbd> | ||
| 185 | {/each} | 185 | {/each} | ||
| @@ -201,7 +201,7 @@ | |||||
| 201 | {#each quickAccessHints as hint (hint.description)} | 201 | {#each quickAccessHints as hint (hint.description)} | ||
| 202 | <div class="hint-row"> | 202 | <div class="hint-row"> | ||
| 203 | <div class="hint-keys"> | 203 | <div class="hint-keys"> | ||
| 204 | - | {#each hint.keys as key, i} | 204 | + | {#each hint.keys as key, i (key)} |
| 205 | {#if i > 0}<span class="key-sep">/</span>{/if} | 205 | {#if i > 0}<span class="key-sep">/</span>{/if} | ||
| 206 | <kbd>{key}</kbd> | 206 | <kbd>{key}</kbd> | ||
| 207 | {/each} | 207 | {/each} | ||
M webui/src/pages/homepage/shell/StatusBar.svelte +2 -1
| @@ -38,7 +38,7 @@ | |||
| 38 | 38 | label: 'entries', | |
| 39 | 39 | filtered: false, | |
| 40 | 40 | }; | |
| 41 | - | case 'favorites': | |
| 41 | + | case 'favorites': { | |
| 42 | 42 | const filtered = homepageState.filteredFavorites; | |
| 43 | 43 | const total = homepageState.favorites.length; | |
| 44 | 44 | return { | |
| @@ -46,6 +46,7 @@ | |||
| 46 | 46 | label: filtered.length === total ? 'items' : `of ${total}`, | |
| 47 | 47 | filtered: filtered.length !== total, | |
| 48 | 48 | }; | |
| 49 | + | } | |
| 49 | 50 | case 'analytics': | |
| 50 | 51 | return { | |
| 51 | 52 | count: homepageState.domainStats.length, | |
| @@ -38,7 +38,7 @@ | |||||
| 38 | label: 'entries', | 38 | label: 'entries', | ||
| 39 | filtered: false, | 39 | filtered: false, | ||
| 40 | }; | 40 | }; | ||
| 41 | - | case 'favorites': | 41 | + | case 'favorites': { |
| 42 | const filtered = homepageState.filteredFavorites; | 42 | const filtered = homepageState.filteredFavorites; | ||
| 43 | const total = homepageState.favorites.length; | 43 | const total = homepageState.favorites.length; | ||
| 44 | return { | 44 | return { | ||
| @@ -46,6 +46,7 @@ | |||||
| 46 | label: filtered.length === total ? 'items' : `of ${total}`, | 46 | label: filtered.length === total ? 'items' : `of ${total}`, | ||
| 47 | filtered: filtered.length !== total, | 47 | filtered: filtered.length !== total, | ||
| 48 | }; | 48 | }; | ||
| 49 | + | } | |||
| 49 | case 'analytics': | 50 | case 'analytics': | ||
| 50 | return { | 51 | return { | ||
| 51 | count: homepageState.domainStats.length, | 52 | count: homepageState.domainStats.length, | ||
M webui/src/pages/homepage/state.svelte.ts +2 -0
| @@ -71,6 +71,7 @@ let pendingKeyPrefix = $state<string | null>(null); | |||
| 71 | 71 | // HELPER FUNCTIONS | |
| 72 | 72 | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 73 | 73 | ||
| 74 | + | /* eslint-disable svelte/prefer-svelte-reactivity -- helper function uses temporary Map/Date, not reactive state */ | |
| 74 | 75 | function groupHistoryByDate(entries: HistoryEntry[]): TimelineGroup[] { | |
| 75 | 76 | const groups = new Map<string, HistoryEntry[]>(); | |
| 76 | 77 | const today = new Date(); | |
| @@ -109,6 +110,7 @@ function groupHistoryByDate(entries: HistoryEntry[]): TimelineGroup[] { | |||
| 109 | 110 | ||
| 110 | 111 | return result.sort((a, b) => b.date.localeCompare(a.date)); | |
| 111 | 112 | } | |
| 113 | + | /* eslint-enable svelte/prefer-svelte-reactivity */ | |
| 112 | 114 | ||
| 113 | 115 | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 114 | 116 | // EXPORTED STORE | |
| @@ -71,6 +71,7 @@ let pendingKeyPrefix = $state<string | null>(null); | |||||
| 71 | // HELPER FUNCTIONS | 71 | // HELPER FUNCTIONS | ||
| 72 | // ═══════════════════════════════════════════════════════════════════════════════ | 72 | // ═══════════════════════════════════════════════════════════════════════════════ | ||
| 73 | 73 | ||||
| 74 | + | /* eslint-disable svelte/prefer-svelte-reactivity -- helper function uses temporary Map/Date, not reactive state */ | |||
| 74 | function groupHistoryByDate(entries: HistoryEntry[]): TimelineGroup[] { | 75 | function groupHistoryByDate(entries: HistoryEntry[]): TimelineGroup[] { | ||
| 75 | const groups = new Map<string, HistoryEntry[]>(); | 76 | const groups = new Map<string, HistoryEntry[]>(); | ||
| 76 | const today = new Date(); | 77 | const today = new Date(); | ||
| @@ -109,6 +110,7 @@ function groupHistoryByDate(entries: HistoryEntry[]): TimelineGroup[] { | |||||
| 109 | 110 | ||||
| 110 | return result.sort((a, b) => b.date.localeCompare(a.date)); | 111 | return result.sort((a, b) => b.date.localeCompare(a.date)); | ||
| 111 | } | 112 | } | ||
| 113 | + | /* eslint-enable svelte/prefer-svelte-reactivity */ | |||
| 112 | 114 | ||||
| 113 | // ═══════════════════════════════════════════════════════════════════════════════ | 115 | // ═══════════════════════════════════════════════════════════════════════════════ | ||
| 114 | // EXPORTED STORE | 116 | // EXPORTED STORE | ||