feat(injected): enhance color scheme and shadow host integration
Main World: - Add __dumber_applyPalette global function declaration - Call applyPalette when theme changes via setTheme - Dispatch 'dumber:theme-change' custom event on theme change - Add error handling for palette application Color Scheme: - Apply palette on theme toggle via __dumber_applyPalette - Add try-catch for palette application safety Omnibox: - Fetch search shortcuts on initialization - Add error handling for failed shortcut fetch Shadow Host: - Define TOKEN_KEYS array for CSS custom properties to sync - Add syncHostTokens() to copy color tokens from document to shadow host - Sync tokens on shadow root creation and theme changes - Listen for 'dumber:palette-updated' event to re-sync tokens - Remove hardcoded dark theme color values - Add --dynamic-surface-variant to token mappings
- commit
- e4d5fcf4c3d62818f95ed61303b7fe7c45fd5ddc
- author
- bamen · · 2025-10-06 17:52 UTC
- committer
- bamen · · 2025-10-06 18:07 UTC
- parent
- c19dc5b
- signature
- unsigned
4 files · +73 -8
M gui/src/injected/main-world.ts +11 -0
| @@ -19,6 +19,7 @@ declare global { | |||
| 19 | 19 | __dumber_dom_zoom_level?: number; | |
| 20 | 20 | __dumber_initial_theme?: string; | |
| 21 | 21 | __dumber_setTheme?: (theme: "light" | "dark") => void; | |
| 22 | + | __dumber_applyPalette?: (theme: "light" | "dark") => void; | |
| 22 | 23 | __dumber_applyDomZoom?: (level: number) => void; | |
| 23 | 24 | __dumber_showToast?: ( | |
| 24 | 25 | message: string, | |
| @@ -127,6 +128,16 @@ function detectWindowType(features?: string | null): string { | |||
| 127 | 128 | } else { | |
| 128 | 129 | document.documentElement.classList.remove("dark"); | |
| 129 | 130 | } | |
| 131 | + | try { | |
| 132 | + | if (typeof window.__dumber_applyPalette === "function") { | |
| 133 | + | window.__dumber_applyPalette(theme); | |
| 134 | + | } | |
| 135 | + | document.dispatchEvent( | |
| 136 | + | new CustomEvent("dumber:theme-change", { detail: { theme } }), | |
| 137 | + | ); | |
| 138 | + | } catch (error) { | |
| 139 | + | console.warn("[dumber] Failed to apply runtime palette", error); | |
| 140 | + | } | |
| 130 | 141 | }; | |
| 131 | 142 | ||
| 132 | 143 | // Initialize unified API object | |
| @@ -19,6 +19,7 @@ declare global { | |||||
| 19 | __dumber_dom_zoom_level?: number; | 19 | __dumber_dom_zoom_level?: number; | ||
| 20 | __dumber_initial_theme?: string; | 20 | __dumber_initial_theme?: string; | ||
| 21 | __dumber_setTheme?: (theme: "light" | "dark") => void; | 21 | __dumber_setTheme?: (theme: "light" | "dark") => void; | ||
| 22 | + | __dumber_applyPalette?: (theme: "light" | "dark") => void; | |||
| 22 | __dumber_applyDomZoom?: (level: number) => void; | 23 | __dumber_applyDomZoom?: (level: number) => void; | ||
| 23 | __dumber_showToast?: ( | 24 | __dumber_showToast?: ( | ||
| 24 | message: string, | 25 | message: string, | ||
| @@ -127,6 +128,16 @@ function detectWindowType(features?: string | null): string { | |||||
| 127 | } else { | 128 | } else { | ||
| 128 | document.documentElement.classList.remove("dark"); | 129 | document.documentElement.classList.remove("dark"); | ||
| 129 | } | 130 | } | ||
| 131 | + | try { | |||
| 132 | + | if (typeof window.__dumber_applyPalette === "function") { | |||
| 133 | + | window.__dumber_applyPalette(theme); | |||
| 134 | + | } | |||
| 135 | + | document.dispatchEvent( | |||
| 136 | + | new CustomEvent("dumber:theme-change", { detail: { theme } }), | |||
| 137 | + | ); | |||
| 138 | + | } catch (error) { | |||
| 139 | + | console.warn("[dumber] Failed to apply runtime palette", error); | |||
| 140 | + | } | |||
| 130 | }; | 141 | }; | ||
| 131 | 142 | ||||
| 132 | // Initialize unified API object | 143 | // Initialize unified API object | ||
M gui/src/injected/modules/color-scheme.ts +7 -0
| @@ -187,6 +187,13 @@ class ColorSchemeManager { | |||
| 187 | 187 | } else { | |
| 188 | 188 | document.documentElement.classList.remove("dark"); | |
| 189 | 189 | } | |
| 190 | + | try { | |
| 191 | + | window.__dumber_applyPalette?.( | |
| 192 | + | this.config.isDark ? "dark" : "light", | |
| 193 | + | ); | |
| 194 | + | } catch { | |
| 195 | + | /* noop */ | |
| 196 | + | } | |
| 190 | 197 | } | |
| 191 | 198 | }, 100); | |
| 192 | 199 | } | |
| @@ -187,6 +187,13 @@ class ColorSchemeManager { | |||||
| 187 | } else { | 187 | } else { | ||
| 188 | document.documentElement.classList.remove("dark"); | 188 | document.documentElement.classList.remove("dark"); | ||
| 189 | } | 189 | } | ||
| 190 | + | try { | |||
| 191 | + | window.__dumber_applyPalette?.( | |||
| 192 | + | this.config.isDark ? "dark" : "light", | |||
| 193 | + | ); | |||
| 194 | + | } catch { | |||
| 195 | + | /* noop */ | |||
| 196 | + | } | |||
| 190 | } | 197 | } | ||
| 191 | }, 100); | 198 | }, 100); | ||
| 192 | } | 199 | } | ||
M gui/src/injected/modules/omnibox.ts +7 -0
| @@ -8,6 +8,7 @@ import { mount } from "svelte"; | |||
| 8 | 8 | import { Omnibox } from "../../components/omnibox"; | |
| 9 | 9 | import { ensureShadowMount } from "./shadowHost"; | |
| 10 | 10 | import type { OmniboxConfig } from "../../components/omnibox/types"; | |
| 11 | + | import { omniboxBridge } from "../../components/omnibox/messaging"; | |
| 11 | 12 | ||
| 12 | 13 | let omniboxComponent: ReturnType<typeof mount> | null = null; | |
| 13 | 14 | let isInitialized = false; | |
| @@ -60,6 +61,12 @@ export async function initializeOmnibox( | |||
| 60 | 61 | Object.keys(window.__dumber_omnibox), | |
| 61 | 62 | ); | |
| 62 | 63 | isInitialized = true; | |
| 64 | + | ||
| 65 | + | // Fetch search shortcuts from backend | |
| 66 | + | omniboxBridge.fetchSearchShortcuts().catch((err) => { | |
| 67 | + | console.warn("Failed to fetch search shortcuts:", err); | |
| 68 | + | }); | |
| 69 | + | ||
| 63 | 70 | // Notify isolated bundle listeners that omnibox is ready | |
| 64 | 71 | try { | |
| 65 | 72 | document.dispatchEvent(new CustomEvent("dumber:omnibox-ready")); | |
| @@ -8,6 +8,7 @@ import { mount } from "svelte"; | |||||
| 8 | import { Omnibox } from "../../components/omnibox"; | 8 | import { Omnibox } from "../../components/omnibox"; | ||
| 9 | import { ensureShadowMount } from "./shadowHost"; | 9 | import { ensureShadowMount } from "./shadowHost"; | ||
| 10 | import type { OmniboxConfig } from "../../components/omnibox/types"; | 10 | import type { OmniboxConfig } from "../../components/omnibox/types"; | ||
| 11 | + | import { omniboxBridge } from "../../components/omnibox/messaging"; | |||
| 11 | 12 | ||||
| 12 | let omniboxComponent: ReturnType<typeof mount> | null = null; | 13 | let omniboxComponent: ReturnType<typeof mount> | null = null; | ||
| 13 | let isInitialized = false; | 14 | let isInitialized = false; | ||
| @@ -60,6 +61,12 @@ export async function initializeOmnibox( | |||||
| 60 | Object.keys(window.__dumber_omnibox), | 61 | Object.keys(window.__dumber_omnibox), | ||
| 61 | ); | 62 | ); | ||
| 62 | isInitialized = true; | 63 | isInitialized = true; | ||
| 64 | + | ||||
| 65 | + | // Fetch search shortcuts from backend | |||
| 66 | + | omniboxBridge.fetchSearchShortcuts().catch((err) => { | |||
| 67 | + | console.warn("Failed to fetch search shortcuts:", err); | |||
| 68 | + | }); | |||
| 69 | + | ||||
| 63 | // Notify isolated bundle listeners that omnibox is ready | 70 | // Notify isolated bundle listeners that omnibox is ready | ||
| 64 | try { | 71 | try { | ||
| 65 | document.dispatchEvent(new CustomEvent("dumber:omnibox-ready")); | 72 | document.dispatchEvent(new CustomEvent("dumber:omnibox-ready")); | ||
M gui/src/injected/modules/shadowHost.ts +48 -8
| @@ -69,6 +69,41 @@ function ensureHostPresence(host: HTMLElement): void { | |||
| 69 | 69 | } | |
| 70 | 70 | ||
| 71 | 71 | export const GLOBAL_SHADOW_HOST_ID = "dumber-ui-root"; | |
| 72 | + | const TOKEN_KEYS = [ | |
| 73 | + | "--color-browser-bg", | |
| 74 | + | "--color-browser-surface", | |
| 75 | + | "--color-browser-surface-variant", | |
| 76 | + | "--color-browser-text", | |
| 77 | + | "--color-browser-muted", | |
| 78 | + | "--color-browser-accent", | |
| 79 | + | "--color-browser-border", | |
| 80 | + | "--dynamic-bg", | |
| 81 | + | "--dynamic-surface", | |
| 82 | + | "--dynamic-surface-variant", | |
| 83 | + | "--dynamic-text", | |
| 84 | + | "--dynamic-muted", | |
| 85 | + | "--dynamic-accent", | |
| 86 | + | "--dynamic-border", | |
| 87 | + | ] as const; | |
| 88 | + | ||
| 89 | + | function syncHostTokens(host: HTMLElement) { | |
| 90 | + | if (typeof window === "undefined" || typeof document === "undefined") { | |
| 91 | + | return; | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | try { | |
| 95 | + | const root = document.documentElement; | |
| 96 | + | const computed = getComputedStyle(root); | |
| 97 | + | for (const token of TOKEN_KEYS) { | |
| 98 | + | const value = computed.getPropertyValue(token); | |
| 99 | + | if (value && value.trim()) { | |
| 100 | + | host.style.setProperty(token, value.trim()); | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | } catch (error) { | |
| 104 | + | console.warn("[shadowHost] Failed to sync color tokens", error); | |
| 105 | + | } | |
| 106 | + | } | |
| 72 | 107 | ||
| 73 | 108 | /** | |
| 74 | 109 | * Ensure and return the global ShadowRoot used by injected UI. | |
| @@ -87,6 +122,7 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||
| 87 | 122 | ||
| 88 | 123 | cachedHost = host; | |
| 89 | 124 | ensureHostPresence(host); | |
| 125 | + | syncHostTokens(host); | |
| 90 | 126 | ||
| 91 | 127 | const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: "open" }); | |
| 92 | 128 | ||
| @@ -100,6 +136,7 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||
| 100 | 136 | "dark", | |
| 101 | 137 | document.documentElement.classList.contains("dark"), | |
| 102 | 138 | ); | |
| 139 | + | syncHostTokens(host); | |
| 103 | 140 | }); | |
| 104 | 141 | observer.observe(document.documentElement, { | |
| 105 | 142 | attributes: true, | |
| @@ -109,6 +146,16 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||
| 109 | 146 | /* no-op */ | |
| 110 | 147 | } | |
| 111 | 148 | ||
| 149 | + | try { | |
| 150 | + | document.addEventListener("dumber:palette-updated", () => { | |
| 151 | + | if (cachedHost) { | |
| 152 | + | syncHostTokens(cachedHost); | |
| 153 | + | } | |
| 154 | + | }); | |
| 155 | + | } catch { | |
| 156 | + | /* no-op */ | |
| 157 | + | } | |
| 158 | + | ||
| 112 | 159 | // Inject a minimal reset and base styles into the shadow root once | |
| 113 | 160 | if (!shadowResetApplied.has(shadowRoot)) { | |
| 114 | 161 | const resetStyle = document.createElement("style"); | |
| @@ -125,20 +172,13 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||
| 125 | 172 | tokensStyle.textContent = ` | |
| 126 | 173 | :host { | |
| 127 | 174 | --dynamic-bg: var(--color-browser-bg); | |
| 175 | + | --dynamic-surface-variant: var(--color-browser-surface-variant); | |
| 128 | 176 | --dynamic-surface: var(--color-browser-surface); | |
| 129 | 177 | --dynamic-text: var(--color-browser-text); | |
| 130 | 178 | --dynamic-muted: var(--color-browser-muted); | |
| 131 | 179 | --dynamic-accent: var(--color-browser-accent); | |
| 132 | 180 | --dynamic-border: var(--color-browser-border); | |
| 133 | 181 | } | |
| 134 | - | :host(.dark) { | |
| 135 | - | --dynamic-bg: rgb(17 24 39); | |
| 136 | - | --dynamic-surface: rgb(31 41 55); | |
| 137 | - | --dynamic-text: rgb(243 244 246); | |
| 138 | - | --dynamic-muted: rgb(156 163 175); | |
| 139 | - | --dynamic-accent: rgb(96 165 250); | |
| 140 | - | --dynamic-border: rgb(55 65 81); | |
| 141 | - | } | |
| 142 | 182 | `; | |
| 143 | 183 | shadowRoot.appendChild(tokensStyle); | |
| 144 | 184 | ||
| @@ -69,6 +69,41 @@ function ensureHostPresence(host: HTMLElement): void { | |||||
| 69 | } | 69 | } | ||
| 70 | 70 | ||||
| 71 | export const GLOBAL_SHADOW_HOST_ID = "dumber-ui-root"; | 71 | export const GLOBAL_SHADOW_HOST_ID = "dumber-ui-root"; | ||
| 72 | + | const TOKEN_KEYS = [ | |||
| 73 | + | "--color-browser-bg", | |||
| 74 | + | "--color-browser-surface", | |||
| 75 | + | "--color-browser-surface-variant", | |||
| 76 | + | "--color-browser-text", | |||
| 77 | + | "--color-browser-muted", | |||
| 78 | + | "--color-browser-accent", | |||
| 79 | + | "--color-browser-border", | |||
| 80 | + | "--dynamic-bg", | |||
| 81 | + | "--dynamic-surface", | |||
| 82 | + | "--dynamic-surface-variant", | |||
| 83 | + | "--dynamic-text", | |||
| 84 | + | "--dynamic-muted", | |||
| 85 | + | "--dynamic-accent", | |||
| 86 | + | "--dynamic-border", | |||
| 87 | + | ] as const; | |||
| 88 | + | ||||
| 89 | + | function syncHostTokens(host: HTMLElement) { | |||
| 90 | + | if (typeof window === "undefined" || typeof document === "undefined") { | |||
| 91 | + | return; | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | try { | |||
| 95 | + | const root = document.documentElement; | |||
| 96 | + | const computed = getComputedStyle(root); | |||
| 97 | + | for (const token of TOKEN_KEYS) { | |||
| 98 | + | const value = computed.getPropertyValue(token); | |||
| 99 | + | if (value && value.trim()) { | |||
| 100 | + | host.style.setProperty(token, value.trim()); | |||
| 101 | + | } | |||
| 102 | + | } | |||
| 103 | + | } catch (error) { | |||
| 104 | + | console.warn("[shadowHost] Failed to sync color tokens", error); | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 72 | 107 | ||||
| 73 | /** | 108 | /** | ||
| 74 | * Ensure and return the global ShadowRoot used by injected UI. | 109 | * Ensure and return the global ShadowRoot used by injected UI. | ||
| @@ -87,6 +122,7 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||||
| 87 | 122 | ||||
| 88 | cachedHost = host; | 123 | cachedHost = host; | ||
| 89 | ensureHostPresence(host); | 124 | ensureHostPresence(host); | ||
| 125 | + | syncHostTokens(host); | |||
| 90 | 126 | ||||
| 91 | const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: "open" }); | 127 | const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: "open" }); | ||
| 92 | 128 | ||||
| @@ -100,6 +136,7 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||||
| 100 | "dark", | 136 | "dark", | ||
| 101 | document.documentElement.classList.contains("dark"), | 137 | document.documentElement.classList.contains("dark"), | ||
| 102 | ); | 138 | ); | ||
| 139 | + | syncHostTokens(host); | |||
| 103 | }); | 140 | }); | ||
| 104 | observer.observe(document.documentElement, { | 141 | observer.observe(document.documentElement, { | ||
| 105 | attributes: true, | 142 | attributes: true, | ||
| @@ -109,6 +146,16 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||||
| 109 | /* no-op */ | 146 | /* no-op */ | ||
| 110 | } | 147 | } | ||
| 111 | 148 | ||||
| 149 | + | try { | |||
| 150 | + | document.addEventListener("dumber:palette-updated", () => { | |||
| 151 | + | if (cachedHost) { | |||
| 152 | + | syncHostTokens(cachedHost); | |||
| 153 | + | } | |||
| 154 | + | }); | |||
| 155 | + | } catch { | |||
| 156 | + | /* no-op */ | |||
| 157 | + | } | |||
| 158 | + | ||||
| 112 | // Inject a minimal reset and base styles into the shadow root once | 159 | // Inject a minimal reset and base styles into the shadow root once | ||
| 113 | if (!shadowResetApplied.has(shadowRoot)) { | 160 | if (!shadowResetApplied.has(shadowRoot)) { | ||
| 114 | const resetStyle = document.createElement("style"); | 161 | const resetStyle = document.createElement("style"); | ||
| @@ -125,20 +172,13 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||||
| 125 | tokensStyle.textContent = ` | 172 | tokensStyle.textContent = ` | ||
| 126 | :host { | 173 | :host { | ||
| 127 | --dynamic-bg: var(--color-browser-bg); | 174 | --dynamic-bg: var(--color-browser-bg); | ||
| 175 | + | --dynamic-surface-variant: var(--color-browser-surface-variant); | |||
| 128 | --dynamic-surface: var(--color-browser-surface); | 176 | --dynamic-surface: var(--color-browser-surface); | ||
| 129 | --dynamic-text: var(--color-browser-text); | 177 | --dynamic-text: var(--color-browser-text); | ||
| 130 | --dynamic-muted: var(--color-browser-muted); | 178 | --dynamic-muted: var(--color-browser-muted); | ||
| 131 | --dynamic-accent: var(--color-browser-accent); | 179 | --dynamic-accent: var(--color-browser-accent); | ||
| 132 | --dynamic-border: var(--color-browser-border); | 180 | --dynamic-border: var(--color-browser-border); | ||
| 133 | } | 181 | } | ||
| 134 | - | :host(.dark) { | |||
| 135 | - | --dynamic-bg: rgb(17 24 39); | |||
| 136 | - | --dynamic-surface: rgb(31 41 55); | |||
| 137 | - | --dynamic-text: rgb(243 244 246); | |||
| 138 | - | --dynamic-muted: rgb(156 163 175); | |||
| 139 | - | --dynamic-accent: rgb(96 165 250); | |||
| 140 | - | --dynamic-border: rgb(55 65 81); | |||
| 141 | - | } | |||
| 142 | `; | 182 | `; | ||
| 143 | shadowRoot.appendChild(tokensStyle); | 183 | shadowRoot.appendChild(tokensStyle); | ||
| 144 | 184 | ||||