improve(omnibox): adopt theme tokens for light/dark parity
- commit
- 933a721bc6bef62857e270cdd58658b4307ddd94
- author
- bamen · · 2025-09-16 17:52 UTC
- committer
- bamen · · 2025-09-16 21:07 UTC
- parent
- 7604f1e
- signature
- unsigned
3 files · +186 -28
M CHANGELOG.md +1 -0
| @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. | |||
| 13 | 13 | - Native accelerator bridge debounces duplicate GTK key events earlier, so Ctrl±/Ctrl+0 only fires once per physical key press. | |
| 14 | 14 | - Omnibox-triggered navigations now reuse the navigation controller, so saved zoom levels are applied before first paint and redundant lookups are avoided. | |
| 15 | 15 | - Configuration loading writes the missing `use_dom_zoom` flag into existing config files so DOM zoom defaults persist without manual edits. | |
| 16 | + | - Omnibox UI now relies on shared Tailwind theme tokens and the injected color-scheme service for light/dark parity. | |
| 16 | 17 | ||
| 17 | 18 | ## [0.8.0] - 2025-09-17 | |
| 18 | 19 | ||
| @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. | |||||
| 13 | - Native accelerator bridge debounces duplicate GTK key events earlier, so Ctrl±/Ctrl+0 only fires once per physical key press. | 13 | - Native accelerator bridge debounces duplicate GTK key events earlier, so Ctrl±/Ctrl+0 only fires once per physical key press. | ||
| 14 | - Omnibox-triggered navigations now reuse the navigation controller, so saved zoom levels are applied before first paint and redundant lookups are avoided. | 14 | - Omnibox-triggered navigations now reuse the navigation controller, so saved zoom levels are applied before first paint and redundant lookups are avoided. | ||
| 15 | - Configuration loading writes the missing `use_dom_zoom` flag into existing config files so DOM zoom defaults persist without manual edits. | 15 | - Configuration loading writes the missing `use_dom_zoom` flag into existing config files so DOM zoom defaults persist without manual edits. | ||
| 16 | + | - Omnibox UI now relies on shared Tailwind theme tokens and the injected color-scheme service for light/dark parity. | |||
| 16 | 17 | ||||
| 17 | ## [0.8.0] - 2025-09-17 | 18 | ## [0.8.0] - 2025-09-17 | ||
| 18 | 19 | ||||
M gui/src/components/omnibox/Omnibox.svelte +136 -25
| @@ -29,6 +29,52 @@ | |||
| 29 | 29 | inputPadding: '10px 12px' | |
| 30 | 30 | }); | |
| 31 | 31 | ||
| 32 | + | // Track theme mode derived from injected color-scheme module | |
| 33 | + | let isDarkMode = $state(false); | |
| 34 | + | let themeObserver: MutationObserver | null = null; | |
| 35 | + | ||
| 36 | + | function syncTheme() { | |
| 37 | + | if (typeof document === 'undefined') return; | |
| 38 | + | ||
| 39 | + | const globalWindow = typeof window === 'undefined' ? undefined : window; | |
| 40 | + | ||
| 41 | + | if (document.documentElement.classList.contains('dark')) { | |
| 42 | + | isDarkMode = true; | |
| 43 | + | return; | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | if (globalWindow?.matchMedia) { | |
| 47 | + | try { | |
| 48 | + | const prefersDark = globalWindow.matchMedia('(prefers-color-scheme: dark)').matches; | |
| 49 | + | if (typeof prefersDark === 'boolean') { | |
| 50 | + | isDarkMode = prefersDark; | |
| 51 | + | return; | |
| 52 | + | } | |
| 53 | + | } catch { | |
| 54 | + | // matchMedia might be unavailable in some environments | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Fallback to injected GTK preference flag if available | |
| 59 | + | isDarkMode = Boolean((globalWindow as any)?.__dumber_gtk_prefers_dark); | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | function observeThemeChanges() { | |
| 63 | + | if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') { | |
| 64 | + | return; | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | if (themeObserver) { | |
| 68 | + | themeObserver.disconnect(); | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | themeObserver = new MutationObserver(syncTheme); | |
| 72 | + | themeObserver.observe(document.documentElement, { | |
| 73 | + | attributes: true, | |
| 74 | + | attributeFilter: ['class'] | |
| 75 | + | }); | |
| 76 | + | } | |
| 77 | + | ||
| 32 | 78 | // Handle click outside to close | |
| 33 | 79 | function handleOverlayClick(event: MouseEvent) { | |
| 34 | 80 | const target = event.target as HTMLElement; | |
| @@ -90,40 +136,71 @@ | |||
| 90 | 136 | // Ensure subtle transitions are present on the container so | |
| 91 | 137 | // background-color (and other cheap properties) animate smoothly. | |
| 92 | 138 | try { | |
| 93 | - | // container transitions (background & color) | |
| 94 | 139 | if (!boxElement.style.transition) { | |
| 95 | - | boxElement.style.transition = 'background-color 120ms ease'; | |
| 140 | + | boxElement.style.transition = 'background-color 120ms ease, border-color 120ms ease'; | |
| 96 | 141 | } | |
| 97 | 142 | } catch (e) { | |
| 98 | 143 | // defensive: ignore if setting styles fails for some reason | |
| 99 | 144 | } | |
| 100 | 145 | ||
| 146 | + | const surfaceMix = isDarkMode ? '65%' : '92%'; | |
| 147 | + | const inputMix = isDarkMode ? '60%' : '94%'; | |
| 148 | + | const borderMix = isDarkMode ? '70%' : '90%'; | |
| 149 | + | const blurMix = isDarkMode ? '28%' : '16%'; | |
| 150 | + | ||
| 151 | + | const containerBackground = faded | |
| 152 | + | ? `color-mix(in srgb, var(--dynamic-surface) ${surfaceMix}, transparent)` | |
| 153 | + | : 'var(--dynamic-surface)'; | |
| 154 | + | const inputBackground = faded | |
| 155 | + | ? `color-mix(in srgb, var(--dynamic-bg) ${inputMix}, transparent)` | |
| 156 | + | : 'var(--dynamic-bg)'; | |
| 157 | + | const borderColor = faded | |
| 158 | + | ? `color-mix(in srgb, var(--dynamic-border) ${borderMix}, transparent)` | |
| 159 | + | : 'var(--dynamic-border)'; | |
| 160 | + | const blurTint = `color-mix(in srgb, var(--dynamic-bg) ${blurMix}, transparent)`; | |
| 161 | + | ||
| 162 | + | boxElement.style.background = containerBackground; | |
| 163 | + | boxElement.style.color = 'var(--dynamic-text)'; | |
| 164 | + | boxElement.style.setProperty('--dumber-border-color', borderColor); | |
| 165 | + | boxElement.style.setProperty('--dumber-omnibox-surface', containerBackground); | |
| 166 | + | ||
| 167 | + | inputElement.style.background = inputBackground; | |
| 168 | + | inputElement.style.color = 'var(--dynamic-text)'; | |
| 169 | + | inputElement.style.setProperty('--dumber-input-bg', inputBackground); | |
| 170 | + | inputElement.style.setProperty('--dumber-input-border-base', borderColor); | |
| 171 | + | inputElement.style.setProperty('--dumber-placeholder-color', 'var(--dynamic-muted)'); | |
| 172 | + | ||
| 173 | + | blurLayerElement.style.setProperty('--dumber-blur-color', blurTint); | |
| 174 | + | blurLayerElement.style.background = 'var(--dumber-blur-color)'; | |
| 175 | + | ||
| 176 | + | let accentColor = 'var(--dynamic-accent)'; | |
| 177 | + | try { | |
| 178 | + | const computedAccent = getComputedStyle(boxElement).getPropertyValue('--dynamic-accent'); | |
| 179 | + | if (computedAccent && computedAccent.trim()) { | |
| 180 | + | accentColor = computedAccent.trim(); | |
| 181 | + | } | |
| 182 | + | } catch { | |
| 183 | + | // If computed styles fail, keep fallback accent color | |
| 184 | + | } | |
| 185 | + | inputElement.style.setProperty('--dumber-input-accent', accentColor); | |
| 186 | + | boxElement.style.setProperty('--dumber-input-accent', accentColor); | |
| 187 | + | ||
| 188 | + | const isFocused = document.activeElement === inputElement; | |
| 189 | + | ||
| 190 | + | if (!isFocused) { | |
| 191 | + | inputElement.style.setProperty('--dumber-input-border-color', borderColor); | |
| 192 | + | } | |
| 193 | + | ||
| 101 | 194 | if (faded) { | |
| 102 | - | // Faded state - semi-transparent backgrounds. The blur itself | |
| 103 | - | // is provided by the blur layer whose opacity we animate. | |
| 104 | - | boxElement.style.background = 'rgba(27,27,27,0.35)'; | |
| 105 | - | inputElement.style.background = 'rgba(18,18,18,0.35)'; | |
| 106 | - | inputElement.style.color = '#eee'; | |
| 107 | - | ||
| 108 | - | // Small black text border/shadow (2px, 75% opacity) when faded. | |
| 109 | - | // Keep this immediate to avoid animating text-shadow (which can | |
| 110 | - | // trigger repaints that blur text). | |
| 111 | - | const textShadow = '1px 1px 1px rgba(0,0,0,0.75)'; | |
| 195 | + | const textShadow = isDarkMode | |
| 196 | + | ? '1px 1px 1px rgba(0,0,0,0.65)' | |
| 197 | + | : '1px 1px 1px rgba(255,255,255,0.65)'; | |
| 112 | 198 | inputElement.style.textShadow = textShadow; | |
| 113 | 199 | boxElement.style.textShadow = textShadow; | |
| 114 | - | ||
| 115 | - | // Show the blur layer (opacity animation defined in CSS) | |
| 116 | 200 | blurLayerElement.style.opacity = '1'; | |
| 117 | 201 | } else { | |
| 118 | - | // Normal state - solid background, hide blur layer. | |
| 119 | - | boxElement.style.background = '#1b1b1b'; | |
| 120 | - | inputElement.style.background = '#121212'; | |
| 121 | - | inputElement.style.color = '#eee'; | |
| 122 | - | ||
| 123 | - | // Remove text shadow in normal state | |
| 124 | 202 | inputElement.style.textShadow = ''; | |
| 125 | 203 | boxElement.style.textShadow = ''; | |
| 126 | - | ||
| 127 | 204 | blurLayerElement.style.opacity = '0'; | |
| 128 | 205 | } | |
| 129 | 206 | }); | |
| @@ -142,6 +219,9 @@ | |||
| 142 | 219 | } | |
| 143 | 220 | ||
| 144 | 221 | onMount(() => { | |
| 222 | + | syncTheme(); | |
| 223 | + | observeThemeChanges(); | |
| 224 | + | ||
| 145 | 225 | console.log('🔧 Omnibox component mounted'); | |
| 146 | 226 | ||
| 147 | 227 | // Initialize responsive styles | |
| @@ -228,6 +308,11 @@ | |||
| 228 | 308 | // Cleanup global click listener | |
| 229 | 309 | document.removeEventListener('click', handleGlobalClick, true); | |
| 230 | 310 | ||
| 311 | + | if (themeObserver) { | |
| 312 | + | themeObserver.disconnect(); | |
| 313 | + | themeObserver = null; | |
| 314 | + | } | |
| 315 | + | ||
| 231 | 316 | // Reset omnibox state completely | |
| 232 | 317 | omniboxStore.reset(); | |
| 233 | 318 | ||
| @@ -258,7 +343,7 @@ | |||
| 258 | 343 | <!-- Main omnibox container --> | |
| 259 | 344 | <div | |
| 260 | 345 | bind:this={boxElement} | |
| 261 | - | class="bg-[#1b1b1b] border border-[#121212] rounded shadow-[0_10px_30px_rgba(0,0,0,0.6)] text-[#eee]" | |
| 346 | + | class="dumber-omnibox-container omnibox-base rounded shadow-[0_10px_30px_rgba(0,0,0,0.6)]" | |
| 262 | 347 | style="position: relative !important; | |
| 263 | 348 | left: 50% !important; | |
| 264 | 349 | transform: translateX(-50%) !important; | |
| @@ -269,7 +354,11 @@ | |||
| 269 | 354 | padding: {responsiveStyles.padding}; | |
| 270 | 355 | font-family: 'Fira sans', system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif; | |
| 271 | 356 | pointer-events: auto !important; | |
| 272 | - | box-sizing: border-box !important;" | |
| 357 | + | box-sizing: border-box !important; | |
| 358 | + | background: var(--dumber-omnibox-surface, var(--dynamic-surface)); | |
| 359 | + | color: var(--dynamic-text); | |
| 360 | + | border: 2px solid var(--dumber-border-color, var(--dynamic-border)); | |
| 361 | + | --dumber-border-color: var(--dynamic-border);" | |
| 273 | 362 | onmousedown={handleBoxClick} | |
| 274 | 363 | onmouseenter={handleMouseEnter} | |
| 275 | 364 | role="dialog" | |
| @@ -301,11 +390,33 @@ | |||
| 301 | 390 | </div> | |
| 302 | 391 | ||
| 303 | 392 | <style> | |
| 393 | + | .dumber-omnibox-container { | |
| 394 | + | background: var(--dumber-omnibox-surface, var(--dynamic-surface)); | |
| 395 | + | color: var(--dynamic-text); | |
| 396 | + | border: 2px solid var(--dumber-border-color, var(--dynamic-border)); | |
| 397 | + | transition: background-color 160ms ease, border-color 160ms ease; | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | .dumber-omnibox-container:focus-within { | |
| 401 | + | border-color: var(--dumber-input-accent, var(--dynamic-accent)); | |
| 402 | + | } | |
| 403 | + | ||
| 404 | + | :global(.dumber-omnibox-container input) { | |
| 405 | + | background: var(--dumber-input-bg, var(--dynamic-bg)); | |
| 406 | + | color: var(--dynamic-text); | |
| 407 | + | border-color: var(--dumber-input-border-color, var(--dynamic-border)); | |
| 408 | + | transition: border-color 120ms ease, background-color 120ms ease; | |
| 409 | + | } | |
| 410 | + | ||
| 411 | + | :global(.dumber-omnibox-container input::placeholder) { | |
| 412 | + | color: var(--dynamic-muted); | |
| 413 | + | } | |
| 414 | + | ||
| 304 | 415 | /* Blur layer styles: use `backdrop-filter` but animate opacity only. */ | |
| 305 | 416 | .dumber-omnibox-blur-layer { | |
| 306 | 417 | backdrop-filter: blur(2px) saturate(110%); | |
| 307 | 418 | -webkit-backdrop-filter: blur(2px) saturate(110%); | |
| 308 | - | background: rgba(0,0,0,0.12); /* slight tint so blur looks natural */ | |
| 419 | + | background: var(--dumber-blur-color, color-mix(in srgb, var(--dynamic-bg) 16%, transparent)); | |
| 309 | 420 | transition: opacity 160ms ease; | |
| 310 | 421 | will-change: opacity; | |
| 311 | 422 | z-index: 0; | |
| @@ -321,4 +432,4 @@ | |||
| 321 | 432 | backface-visibility: hidden; | |
| 322 | 433 | } | |
| 323 | 434 | /* Additional custom styles if needed */ | |
| 324 | - | </style>⏎̸ | |
| 435 | + | </style> | |
| @@ -29,6 +29,52 @@ | |||||
| 29 | inputPadding: '10px 12px' | 29 | inputPadding: '10px 12px' | ||
| 30 | }); | 30 | }); | ||
| 31 | 31 | ||||
| 32 | + | // Track theme mode derived from injected color-scheme module | |||
| 33 | + | let isDarkMode = $state(false); | |||
| 34 | + | let themeObserver: MutationObserver | null = null; | |||
| 35 | + | ||||
| 36 | + | function syncTheme() { | |||
| 37 | + | if (typeof document === 'undefined') return; | |||
| 38 | + | ||||
| 39 | + | const globalWindow = typeof window === 'undefined' ? undefined : window; | |||
| 40 | + | ||||
| 41 | + | if (document.documentElement.classList.contains('dark')) { | |||
| 42 | + | isDarkMode = true; | |||
| 43 | + | return; | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | if (globalWindow?.matchMedia) { | |||
| 47 | + | try { | |||
| 48 | + | const prefersDark = globalWindow.matchMedia('(prefers-color-scheme: dark)').matches; | |||
| 49 | + | if (typeof prefersDark === 'boolean') { | |||
| 50 | + | isDarkMode = prefersDark; | |||
| 51 | + | return; | |||
| 52 | + | } | |||
| 53 | + | } catch { | |||
| 54 | + | // matchMedia might be unavailable in some environments | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // Fallback to injected GTK preference flag if available | |||
| 59 | + | isDarkMode = Boolean((globalWindow as any)?.__dumber_gtk_prefers_dark); | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | function observeThemeChanges() { | |||
| 63 | + | if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') { | |||
| 64 | + | return; | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | if (themeObserver) { | |||
| 68 | + | themeObserver.disconnect(); | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | themeObserver = new MutationObserver(syncTheme); | |||
| 72 | + | themeObserver.observe(document.documentElement, { | |||
| 73 | + | attributes: true, | |||
| 74 | + | attributeFilter: ['class'] | |||
| 75 | + | }); | |||
| 76 | + | } | |||
| 77 | + | ||||
| 32 | // Handle click outside to close | 78 | // Handle click outside to close | ||
| 33 | function handleOverlayClick(event: MouseEvent) { | 79 | function handleOverlayClick(event: MouseEvent) { | ||
| 34 | const target = event.target as HTMLElement; | 80 | const target = event.target as HTMLElement; | ||
| @@ -90,40 +136,71 @@ | |||||
| 90 | // Ensure subtle transitions are present on the container so | 136 | // Ensure subtle transitions are present on the container so | ||
| 91 | // background-color (and other cheap properties) animate smoothly. | 137 | // background-color (and other cheap properties) animate smoothly. | ||
| 92 | try { | 138 | try { | ||
| 93 | - | // container transitions (background & color) | |||
| 94 | if (!boxElement.style.transition) { | 139 | if (!boxElement.style.transition) { | ||
| 95 | - | boxElement.style.transition = 'background-color 120ms ease'; | 140 | + | boxElement.style.transition = 'background-color 120ms ease, border-color 120ms ease'; |
| 96 | } | 141 | } | ||
| 97 | } catch (e) { | 142 | } catch (e) { | ||
| 98 | // defensive: ignore if setting styles fails for some reason | 143 | // defensive: ignore if setting styles fails for some reason | ||
| 99 | } | 144 | } | ||
| 100 | 145 | ||||
| 146 | + | const surfaceMix = isDarkMode ? '65%' : '92%'; | |||
| 147 | + | const inputMix = isDarkMode ? '60%' : '94%'; | |||
| 148 | + | const borderMix = isDarkMode ? '70%' : '90%'; | |||
| 149 | + | const blurMix = isDarkMode ? '28%' : '16%'; | |||
| 150 | + | ||||
| 151 | + | const containerBackground = faded | |||
| 152 | + | ? `color-mix(in srgb, var(--dynamic-surface) ${surfaceMix}, transparent)` | |||
| 153 | + | : 'var(--dynamic-surface)'; | |||
| 154 | + | const inputBackground = faded | |||
| 155 | + | ? `color-mix(in srgb, var(--dynamic-bg) ${inputMix}, transparent)` | |||
| 156 | + | : 'var(--dynamic-bg)'; | |||
| 157 | + | const borderColor = faded | |||
| 158 | + | ? `color-mix(in srgb, var(--dynamic-border) ${borderMix}, transparent)` | |||
| 159 | + | : 'var(--dynamic-border)'; | |||
| 160 | + | const blurTint = `color-mix(in srgb, var(--dynamic-bg) ${blurMix}, transparent)`; | |||
| 161 | + | ||||
| 162 | + | boxElement.style.background = containerBackground; | |||
| 163 | + | boxElement.style.color = 'var(--dynamic-text)'; | |||
| 164 | + | boxElement.style.setProperty('--dumber-border-color', borderColor); | |||
| 165 | + | boxElement.style.setProperty('--dumber-omnibox-surface', containerBackground); | |||
| 166 | + | ||||
| 167 | + | inputElement.style.background = inputBackground; | |||
| 168 | + | inputElement.style.color = 'var(--dynamic-text)'; | |||
| 169 | + | inputElement.style.setProperty('--dumber-input-bg', inputBackground); | |||
| 170 | + | inputElement.style.setProperty('--dumber-input-border-base', borderColor); | |||
| 171 | + | inputElement.style.setProperty('--dumber-placeholder-color', 'var(--dynamic-muted)'); | |||
| 172 | + | ||||
| 173 | + | blurLayerElement.style.setProperty('--dumber-blur-color', blurTint); | |||
| 174 | + | blurLayerElement.style.background = 'var(--dumber-blur-color)'; | |||
| 175 | + | ||||
| 176 | + | let accentColor = 'var(--dynamic-accent)'; | |||
| 177 | + | try { | |||
| 178 | + | const computedAccent = getComputedStyle(boxElement).getPropertyValue('--dynamic-accent'); | |||
| 179 | + | if (computedAccent && computedAccent.trim()) { | |||
| 180 | + | accentColor = computedAccent.trim(); | |||
| 181 | + | } | |||
| 182 | + | } catch { | |||
| 183 | + | // If computed styles fail, keep fallback accent color | |||
| 184 | + | } | |||
| 185 | + | inputElement.style.setProperty('--dumber-input-accent', accentColor); | |||
| 186 | + | boxElement.style.setProperty('--dumber-input-accent', accentColor); | |||
| 187 | + | ||||
| 188 | + | const isFocused = document.activeElement === inputElement; | |||
| 189 | + | ||||
| 190 | + | if (!isFocused) { | |||
| 191 | + | inputElement.style.setProperty('--dumber-input-border-color', borderColor); | |||
| 192 | + | } | |||
| 193 | + | ||||
| 101 | if (faded) { | 194 | if (faded) { | ||
| 102 | - | // Faded state - semi-transparent backgrounds. The blur itself | 195 | + | const textShadow = isDarkMode |
| 103 | - | // is provided by the blur layer whose opacity we animate. | 196 | + | ? '1px 1px 1px rgba(0,0,0,0.65)' |
| 104 | - | boxElement.style.background = 'rgba(27,27,27,0.35)'; | 197 | + | : '1px 1px 1px rgba(255,255,255,0.65)'; |
| 105 | - | inputElement.style.background = 'rgba(18,18,18,0.35)'; | |||
| 106 | - | inputElement.style.color = '#eee'; | |||
| 107 | - | ||||
| 108 | - | // Small black text border/shadow (2px, 75% opacity) when faded. | |||
| 109 | - | // Keep this immediate to avoid animating text-shadow (which can | |||
| 110 | - | // trigger repaints that blur text). | |||
| 111 | - | const textShadow = '1px 1px 1px rgba(0,0,0,0.75)'; | |||
| 112 | inputElement.style.textShadow = textShadow; | 198 | inputElement.style.textShadow = textShadow; | ||
| 113 | boxElement.style.textShadow = textShadow; | 199 | boxElement.style.textShadow = textShadow; | ||
| 114 | - | ||||
| 115 | - | // Show the blur layer (opacity animation defined in CSS) | |||
| 116 | blurLayerElement.style.opacity = '1'; | 200 | blurLayerElement.style.opacity = '1'; | ||
| 117 | } else { | 201 | } else { | ||
| 118 | - | // Normal state - solid background, hide blur layer. | |||
| 119 | - | boxElement.style.background = '#1b1b1b'; | |||
| 120 | - | inputElement.style.background = '#121212'; | |||
| 121 | - | inputElement.style.color = '#eee'; | |||
| 122 | - | ||||
| 123 | - | // Remove text shadow in normal state | |||
| 124 | inputElement.style.textShadow = ''; | 202 | inputElement.style.textShadow = ''; | ||
| 125 | boxElement.style.textShadow = ''; | 203 | boxElement.style.textShadow = ''; | ||
| 126 | - | ||||
| 127 | blurLayerElement.style.opacity = '0'; | 204 | blurLayerElement.style.opacity = '0'; | ||
| 128 | } | 205 | } | ||
| 129 | }); | 206 | }); | ||
| @@ -142,6 +219,9 @@ | |||||
| 142 | } | 219 | } | ||
| 143 | 220 | ||||
| 144 | onMount(() => { | 221 | onMount(() => { | ||
| 222 | + | syncTheme(); | |||
| 223 | + | observeThemeChanges(); | |||
| 224 | + | ||||
| 145 | console.log('🔧 Omnibox component mounted'); | 225 | console.log('🔧 Omnibox component mounted'); | ||
| 146 | 226 | ||||
| 147 | // Initialize responsive styles | 227 | // Initialize responsive styles | ||
| @@ -228,6 +308,11 @@ | |||||
| 228 | // Cleanup global click listener | 308 | // Cleanup global click listener | ||
| 229 | document.removeEventListener('click', handleGlobalClick, true); | 309 | document.removeEventListener('click', handleGlobalClick, true); | ||
| 230 | 310 | ||||
| 311 | + | if (themeObserver) { | |||
| 312 | + | themeObserver.disconnect(); | |||
| 313 | + | themeObserver = null; | |||
| 314 | + | } | |||
| 315 | + | ||||
| 231 | // Reset omnibox state completely | 316 | // Reset omnibox state completely | ||
| 232 | omniboxStore.reset(); | 317 | omniboxStore.reset(); | ||
| 233 | 318 | ||||
| @@ -258,7 +343,7 @@ | |||||
| 258 | <!-- Main omnibox container --> | 343 | <!-- Main omnibox container --> | ||
| 259 | <div | 344 | <div | ||
| 260 | bind:this={boxElement} | 345 | bind:this={boxElement} | ||
| 261 | - | class="bg-[#1b1b1b] border border-[#121212] rounded shadow-[0_10px_30px_rgba(0,0,0,0.6)] text-[#eee]" | 346 | + | class="dumber-omnibox-container omnibox-base rounded shadow-[0_10px_30px_rgba(0,0,0,0.6)]" |
| 262 | style="position: relative !important; | 347 | style="position: relative !important; | ||
| 263 | left: 50% !important; | 348 | left: 50% !important; | ||
| 264 | transform: translateX(-50%) !important; | 349 | transform: translateX(-50%) !important; | ||
| @@ -269,7 +354,11 @@ | |||||
| 269 | padding: {responsiveStyles.padding}; | 354 | padding: {responsiveStyles.padding}; | ||
| 270 | font-family: 'Fira sans', system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif; | 355 | font-family: 'Fira sans', system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif; | ||
| 271 | pointer-events: auto !important; | 356 | pointer-events: auto !important; | ||
| 272 | - | box-sizing: border-box !important;" | 357 | + | box-sizing: border-box !important; |
| 358 | + | background: var(--dumber-omnibox-surface, var(--dynamic-surface)); | |||
| 359 | + | color: var(--dynamic-text); | |||
| 360 | + | border: 2px solid var(--dumber-border-color, var(--dynamic-border)); | |||
| 361 | + | --dumber-border-color: var(--dynamic-border);" | |||
| 273 | onmousedown={handleBoxClick} | 362 | onmousedown={handleBoxClick} | ||
| 274 | onmouseenter={handleMouseEnter} | 363 | onmouseenter={handleMouseEnter} | ||
| 275 | role="dialog" | 364 | role="dialog" | ||
| @@ -301,11 +390,33 @@ | |||||
| 301 | </div> | 390 | </div> | ||
| 302 | 391 | ||||
| 303 | <style> | 392 | <style> | ||
| 393 | + | .dumber-omnibox-container { | |||
| 394 | + | background: var(--dumber-omnibox-surface, var(--dynamic-surface)); | |||
| 395 | + | color: var(--dynamic-text); | |||
| 396 | + | border: 2px solid var(--dumber-border-color, var(--dynamic-border)); | |||
| 397 | + | transition: background-color 160ms ease, border-color 160ms ease; | |||
| 398 | + | } | |||
| 399 | + | ||||
| 400 | + | .dumber-omnibox-container:focus-within { | |||
| 401 | + | border-color: var(--dumber-input-accent, var(--dynamic-accent)); | |||
| 402 | + | } | |||
| 403 | + | ||||
| 404 | + | :global(.dumber-omnibox-container input) { | |||
| 405 | + | background: var(--dumber-input-bg, var(--dynamic-bg)); | |||
| 406 | + | color: var(--dynamic-text); | |||
| 407 | + | border-color: var(--dumber-input-border-color, var(--dynamic-border)); | |||
| 408 | + | transition: border-color 120ms ease, background-color 120ms ease; | |||
| 409 | + | } | |||
| 410 | + | ||||
| 411 | + | :global(.dumber-omnibox-container input::placeholder) { | |||
| 412 | + | color: var(--dynamic-muted); | |||
| 413 | + | } | |||
| 414 | + | ||||
| 304 | /* Blur layer styles: use `backdrop-filter` but animate opacity only. */ | 415 | /* Blur layer styles: use `backdrop-filter` but animate opacity only. */ | ||
| 305 | .dumber-omnibox-blur-layer { | 416 | .dumber-omnibox-blur-layer { | ||
| 306 | backdrop-filter: blur(2px) saturate(110%); | 417 | backdrop-filter: blur(2px) saturate(110%); | ||
| 307 | -webkit-backdrop-filter: blur(2px) saturate(110%); | 418 | -webkit-backdrop-filter: blur(2px) saturate(110%); | ||
| 308 | - | background: rgba(0,0,0,0.12); /* slight tint so blur looks natural */ | 419 | + | background: var(--dumber-blur-color, color-mix(in srgb, var(--dynamic-bg) 16%, transparent)); |
| 309 | transition: opacity 160ms ease; | 420 | transition: opacity 160ms ease; | ||
| 310 | will-change: opacity; | 421 | will-change: opacity; | ||
| 311 | z-index: 0; | 422 | z-index: 0; | ||
| @@ -321,4 +432,4 @@ | |||||
| 321 | backface-visibility: hidden; | 432 | backface-visibility: hidden; | ||
| 322 | } | 433 | } | ||
| 323 | /* Additional custom styles if needed */ | 434 | /* Additional custom styles if needed */ | ||
| 324 | - | </style>⏎̸ | 435 | + | </style> |
M gui/src/components/omnibox/OmniboxInput.svelte +49 -3
| @@ -26,6 +26,23 @@ | |||
| 26 | 26 | ||
| 27 | 27 | let { inputElement = $bindable(), responsiveStyles }: Props = $props(); | |
| 28 | 28 | ||
| 29 | + | const ACCENT_FALLBACK = 'var(--dynamic-accent)'; | |
| 30 | + | ||
| 31 | + | function resolveAccentColor(): string { | |
| 32 | + | if (!inputElement) return ACCENT_FALLBACK; | |
| 33 | + | const inlineAccent = inputElement.style.getPropertyValue('--dumber-input-accent'); | |
| 34 | + | if (inlineAccent && inlineAccent.trim()) { | |
| 35 | + | return inlineAccent.trim(); | |
| 36 | + | } | |
| 37 | + | try { | |
| 38 | + | const computedAccent = getComputedStyle(inputElement).getPropertyValue('--dynamic-accent'); | |
| 39 | + | if (computedAccent && computedAccent.trim()) { | |
| 40 | + | return computedAccent.trim(); | |
| 41 | + | } | |
| 42 | + | } catch { /* no-op */ } | |
| 43 | + | return ACCENT_FALLBACK; | |
| 44 | + | } | |
| 45 | + | ||
| 29 | 46 | // Computed placeholder | |
| 30 | 47 | let placeholder = $derived( | |
| 31 | 48 | mode === 'find' ? 'Find in page…' : 'Type URL or search…' | |
| @@ -170,6 +187,17 @@ | |||
| 170 | 187 | ||
| 171 | 188 | function handleFocus() { | |
| 172 | 189 | omniboxStore.setFaded(false); | |
| 190 | + | if (inputElement) { | |
| 191 | + | const accent = resolveAccentColor(); | |
| 192 | + | inputElement.style.setProperty('--dumber-input-border-color', accent); | |
| 193 | + | } | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | function handleBlur() { | |
| 197 | + | if (!inputElement) return; | |
| 198 | + | const base = inputElement.style.getPropertyValue('--dumber-input-border-base'); | |
| 199 | + | const fallback = (base && base.trim()) || 'var(--dynamic-border)'; | |
| 200 | + | inputElement.style.setProperty('--dumber-input-border-color', fallback); | |
| 173 | 201 | } | |
| 174 | 202 | ||
| 175 | 203 | function handleMouseEnter() { | |
| @@ -189,15 +217,16 @@ | |||
| 189 | 217 | type="text" | |
| 190 | 218 | {placeholder} | |
| 191 | 219 | value={inputValue} | |
| 192 | - | class="w-full box-border bg-[#121212] text-[#eee] border border-[#333] rounded focus:outline-none" | |
| 220 | + | class="w-full box-border omnibox-input omnibox-input-field rounded focus:outline-none" | |
| 193 | 221 | style="padding: {responsiveStyles?.inputPadding || '10px 12px'}; | |
| 194 | 222 | font-size: {responsiveStyles?.fontSize || '16px'}; | |
| 195 | - | box-sizing: border-box" | |
| 223 | + | box-sizing: border-box;" | |
| 196 | 224 | oninput={handleInput} | |
| 197 | 225 | onkeydown={handleKeyDown} | |
| 198 | 226 | onmousedown={handleMouseDown} | |
| 199 | 227 | onclick={handleClick} | |
| 200 | 228 | onfocus={handleFocus} | |
| 229 | + | onblur={handleBlur} | |
| 201 | 230 | onmouseenter={handleMouseEnter} | |
| 202 | 231 | autocomplete="off" | |
| 203 | 232 | spellcheck="false" | |
| @@ -207,4 +236,21 @@ | |||
| 207 | 236 | aria-haspopup="listbox" | |
| 208 | 237 | aria-owns="omnibox-list" | |
| 209 | 238 | aria-activedescendant={selectedIndex >= 0 ? `omnibox-item-${selectedIndex}` : undefined} | |
| 210 | - | />⏎̸ | |
| 239 | + | /> | |
| 240 | + | ||
| 241 | + | <style> | |
| 242 | + | .omnibox-input-field { | |
| 243 | + | background: var(--dumber-input-bg, var(--dynamic-bg)); | |
| 244 | + | color: var(--dynamic-text); | |
| 245 | + | border: 2px solid var(--dumber-input-border-color, var(--dynamic-border)); | |
| 246 | + | transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease; | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | .omnibox-input-field::placeholder { | |
| 250 | + | color: var(--dynamic-muted); | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | .omnibox-input-field:focus { | |
| 254 | + | color: var(--dynamic-text); | |
| 255 | + | } | |
| 256 | + | </style> | |
| @@ -26,6 +26,23 @@ | |||||
| 26 | 26 | ||||
| 27 | let { inputElement = $bindable(), responsiveStyles }: Props = $props(); | 27 | let { inputElement = $bindable(), responsiveStyles }: Props = $props(); | ||
| 28 | 28 | ||||
| 29 | + | const ACCENT_FALLBACK = 'var(--dynamic-accent)'; | |||
| 30 | + | ||||
| 31 | + | function resolveAccentColor(): string { | |||
| 32 | + | if (!inputElement) return ACCENT_FALLBACK; | |||
| 33 | + | const inlineAccent = inputElement.style.getPropertyValue('--dumber-input-accent'); | |||
| 34 | + | if (inlineAccent && inlineAccent.trim()) { | |||
| 35 | + | return inlineAccent.trim(); | |||
| 36 | + | } | |||
| 37 | + | try { | |||
| 38 | + | const computedAccent = getComputedStyle(inputElement).getPropertyValue('--dynamic-accent'); | |||
| 39 | + | if (computedAccent && computedAccent.trim()) { | |||
| 40 | + | return computedAccent.trim(); | |||
| 41 | + | } | |||
| 42 | + | } catch { /* no-op */ } | |||
| 43 | + | return ACCENT_FALLBACK; | |||
| 44 | + | } | |||
| 45 | + | ||||
| 29 | // Computed placeholder | 46 | // Computed placeholder | ||
| 30 | let placeholder = $derived( | 47 | let placeholder = $derived( | ||
| 31 | mode === 'find' ? 'Find in page…' : 'Type URL or search…' | 48 | mode === 'find' ? 'Find in page…' : 'Type URL or search…' | ||
| @@ -170,6 +187,17 @@ | |||||
| 170 | 187 | ||||
| 171 | function handleFocus() { | 188 | function handleFocus() { | ||
| 172 | omniboxStore.setFaded(false); | 189 | omniboxStore.setFaded(false); | ||
| 190 | + | if (inputElement) { | |||
| 191 | + | const accent = resolveAccentColor(); | |||
| 192 | + | inputElement.style.setProperty('--dumber-input-border-color', accent); | |||
| 193 | + | } | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | function handleBlur() { | |||
| 197 | + | if (!inputElement) return; | |||
| 198 | + | const base = inputElement.style.getPropertyValue('--dumber-input-border-base'); | |||
| 199 | + | const fallback = (base && base.trim()) || 'var(--dynamic-border)'; | |||
| 200 | + | inputElement.style.setProperty('--dumber-input-border-color', fallback); | |||
| 173 | } | 201 | } | ||
| 174 | 202 | ||||
| 175 | function handleMouseEnter() { | 203 | function handleMouseEnter() { | ||
| @@ -189,15 +217,16 @@ | |||||
| 189 | type="text" | 217 | type="text" | ||
| 190 | {placeholder} | 218 | {placeholder} | ||
| 191 | value={inputValue} | 219 | value={inputValue} | ||
| 192 | - | class="w-full box-border bg-[#121212] text-[#eee] border border-[#333] rounded focus:outline-none" | 220 | + | class="w-full box-border omnibox-input omnibox-input-field rounded focus:outline-none" |
| 193 | style="padding: {responsiveStyles?.inputPadding || '10px 12px'}; | 221 | style="padding: {responsiveStyles?.inputPadding || '10px 12px'}; | ||
| 194 | font-size: {responsiveStyles?.fontSize || '16px'}; | 222 | font-size: {responsiveStyles?.fontSize || '16px'}; | ||
| 195 | - | box-sizing: border-box" | 223 | + | box-sizing: border-box;" |
| 196 | oninput={handleInput} | 224 | oninput={handleInput} | ||
| 197 | onkeydown={handleKeyDown} | 225 | onkeydown={handleKeyDown} | ||
| 198 | onmousedown={handleMouseDown} | 226 | onmousedown={handleMouseDown} | ||
| 199 | onclick={handleClick} | 227 | onclick={handleClick} | ||
| 200 | onfocus={handleFocus} | 228 | onfocus={handleFocus} | ||
| 229 | + | onblur={handleBlur} | |||
| 201 | onmouseenter={handleMouseEnter} | 230 | onmouseenter={handleMouseEnter} | ||
| 202 | autocomplete="off" | 231 | autocomplete="off" | ||
| 203 | spellcheck="false" | 232 | spellcheck="false" | ||
| @@ -207,4 +236,21 @@ | |||||
| 207 | aria-haspopup="listbox" | 236 | aria-haspopup="listbox" | ||
| 208 | aria-owns="omnibox-list" | 237 | aria-owns="omnibox-list" | ||
| 209 | aria-activedescendant={selectedIndex >= 0 ? `omnibox-item-${selectedIndex}` : undefined} | 238 | aria-activedescendant={selectedIndex >= 0 ? `omnibox-item-${selectedIndex}` : undefined} | ||
| 210 | - | />⏎̸ | 239 | + | /> |
| 240 | + | ||||
| 241 | + | <style> | |||
| 242 | + | .omnibox-input-field { | |||
| 243 | + | background: var(--dumber-input-bg, var(--dynamic-bg)); | |||
| 244 | + | color: var(--dynamic-text); | |||
| 245 | + | border: 2px solid var(--dumber-input-border-color, var(--dynamic-border)); | |||
| 246 | + | transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease; | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | .omnibox-input-field::placeholder { | |||
| 250 | + | color: var(--dynamic-muted); | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | .omnibox-input-field:focus { | |||
| 254 | + | color: var(--dynamic-text); | |||
| 255 | + | } | |||
| 256 | + | </style> | |||