detached 933a721

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.
1313 - Native accelerator bridge debounces duplicate GTK key events earlier, so Ctrl±/Ctrl+0 only fires once per physical key press.
1414 - Omnibox-triggered navigations now reuse the navigation controller, so saved zoom levels are applied before first paint and redundant lookups are avoided.
1515 - 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.
1617
1718 ## [0.8.0] - 2025-09-17
1819
@@ -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-1718 ## [0.8.0] - 2025-09-17
18 19
M gui/src/components/omnibox/Omnibox.svelte +136 -25
@@ -29,6 +29,52 @@
2929 inputPadding: '10px 12px'
3030 });
3131
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+
3278 // Handle click outside to close
3379 function handleOverlayClick(event: MouseEvent) {
3480 const target = event.target as HTMLElement;
@@ -90,40 +136,71 @@
90136 // Ensure subtle transitions are present on the container so
91137 // background-color (and other cheap properties) animate smoothly.
92138 try {
93- // container transitions (background & color)
94139 if (!boxElement.style.transition) {
95- boxElement.style.transition = 'background-color 120ms ease';
140+ boxElement.style.transition = 'background-color 120ms ease, border-color 120ms ease';
96141 }
97142 } catch (e) {
98143 // defensive: ignore if setting styles fails for some reason
99144 }
100145
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+
101194 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)';
112198 inputElement.style.textShadow = textShadow;
113199 boxElement.style.textShadow = textShadow;
114-
115- // Show the blur layer (opacity animation defined in CSS)
116200 blurLayerElement.style.opacity = '1';
117201 } 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
124202 inputElement.style.textShadow = '';
125203 boxElement.style.textShadow = '';
126-
127204 blurLayerElement.style.opacity = '0';
128205 }
129206 });
@@ -142,6 +219,9 @@
142219 }
143220
144221 onMount(() => {
222+ syncTheme();
223+ observeThemeChanges();
224+
145225 console.log('🔧 Omnibox component mounted');
146226
147227 // Initialize responsive styles
@@ -228,6 +308,11 @@
228308 // Cleanup global click listener
229309 document.removeEventListener('click', handleGlobalClick, true);
230310
311+ if (themeObserver) {
312+ themeObserver.disconnect();
313+ themeObserver = null;
314+ }
315+
231316 // Reset omnibox state completely
232317 omniboxStore.reset();
233318
@@ -258,7 +343,7 @@
258343 <!-- Main omnibox container -->
259344 <div
260345 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)]"
262347 style="position: relative !important;
263348 left: 50% !important;
264349 transform: translateX(-50%) !important;
@@ -269,7 +354,11 @@
269354 padding: {responsiveStyles.padding};
270355 font-family: 'Fira sans', system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif;
271356 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);"
273362 onmousedown={handleBoxClick}
274363 onmouseenter={handleMouseEnter}
275364 role="dialog"
@@ -301,11 +390,33 @@
301390 </div>
302391
303392 <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+
304415 /* Blur layer styles: use `backdrop-filter` but animate opacity only. */
305416 .dumber-omnibox-blur-layer {
306417 backdrop-filter: blur(2px) saturate(110%);
307418 -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));
309420 transition: opacity 160ms ease;
310421 will-change: opacity;
311422 z-index: 0;
@@ -321,4 +432,4 @@
321432 backface-visibility: hidden;
322433 }
323434 /* 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 close78 // 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 so136 // 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 reason143 // 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 itself195+ 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 styles227 // Initialize responsive styles
@@ -228,6 +308,11 @@
228 // Cleanup global click listener308 // 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 completely316 // 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 <div344 <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 @@
2626
2727 let { inputElement = $bindable(), responsiveStyles }: Props = $props();
2828
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+
2946 // Computed placeholder
3047 let placeholder = $derived(
3148 mode === 'find' ? 'Find in page…' : 'Type URL or search…'
@@ -170,6 +187,17 @@
170187
171188 function handleFocus() {
172189 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);
173201 }
174202
175203 function handleMouseEnter() {
@@ -189,15 +217,16 @@
189217 type="text"
190218 {placeholder}
191219 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"
193221 style="padding: {responsiveStyles?.inputPadding || '10px 12px'};
194222 font-size: {responsiveStyles?.fontSize || '16px'};
195- box-sizing: border-box"
223+ box-sizing: border-box;"
196224 oninput={handleInput}
197225 onkeydown={handleKeyDown}
198226 onmousedown={handleMouseDown}
199227 onclick={handleClick}
200228 onfocus={handleFocus}
229+ onblur={handleBlur}
201230 onmouseenter={handleMouseEnter}
202231 autocomplete="off"
203232 spellcheck="false"
@@ -207,4 +236,21 @@
207236 aria-haspopup="listbox"
208237 aria-owns="omnibox-list"
209238 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 placeholder46 // 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>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close