detached e4d5fcf

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 {
1919 __dumber_dom_zoom_level?: number;
2020 __dumber_initial_theme?: string;
2121 __dumber_setTheme?: (theme: "light" | "dark") => void;
22+ __dumber_applyPalette?: (theme: "light" | "dark") => void;
2223 __dumber_applyDomZoom?: (level: number) => void;
2324 __dumber_showToast?: (
2425 message: string,
@@ -127,6 +128,16 @@ function detectWindowType(features?: string | null): string {
127128 } else {
128129 document.documentElement.classList.remove("dark");
129130 }
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+ }
130141 };
131142
132143 // 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 object143 // Initialize unified API object
M gui/src/injected/modules/color-scheme.ts +7 -0
@@ -187,6 +187,13 @@ class ColorSchemeManager {
187187 } else {
188188 document.documentElement.classList.remove("dark");
189189 }
190+ try {
191+ window.__dumber_applyPalette?.(
192+ this.config.isDark ? "dark" : "light",
193+ );
194+ } catch {
195+ /* noop */
196+ }
190197 }
191198 }, 100);
192199 }
@@ -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";
88 import { Omnibox } from "../../components/omnibox";
99 import { ensureShadowMount } from "./shadowHost";
1010 import type { OmniboxConfig } from "../../components/omnibox/types";
11+import { omniboxBridge } from "../../components/omnibox/messaging";
1112
1213 let omniboxComponent: ReturnType<typeof mount> | null = null;
1314 let isInitialized = false;
@@ -60,6 +61,12 @@ export async function initializeOmnibox(
6061 Object.keys(window.__dumber_omnibox),
6162 );
6263 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+
6370 // Notify isolated bundle listeners that omnibox is ready
6471 try {
6572 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 ready70 // 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 {
6969 }
7070
7171 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+}
72107
73108 /**
74109 * Ensure and return the global ShadowRoot used by injected UI.
@@ -87,6 +122,7 @@ export function getGlobalShadowRoot(): ShadowRoot {
87122
88123 cachedHost = host;
89124 ensureHostPresence(host);
125+ syncHostTokens(host);
90126
91127 const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: "open" });
92128
@@ -100,6 +136,7 @@ export function getGlobalShadowRoot(): ShadowRoot {
100136 "dark",
101137 document.documentElement.classList.contains("dark"),
102138 );
139+ syncHostTokens(host);
103140 });
104141 observer.observe(document.documentElement, {
105142 attributes: true,
@@ -109,6 +146,16 @@ export function getGlobalShadowRoot(): ShadowRoot {
109146 /* no-op */
110147 }
111148
149+ try {
150+ document.addEventListener("dumber:palette-updated", () => {
151+ if (cachedHost) {
152+ syncHostTokens(cachedHost);
153+ }
154+ });
155+ } catch {
156+ /* no-op */
157+ }
158+
112159 // Inject a minimal reset and base styles into the shadow root once
113160 if (!shadowResetApplied.has(shadowRoot)) {
114161 const resetStyle = document.createElement("style");
@@ -125,20 +172,13 @@ export function getGlobalShadowRoot(): ShadowRoot {
125172 tokensStyle.textContent = `
126173 :host {
127174 --dynamic-bg: var(--color-browser-bg);
175+ --dynamic-surface-variant: var(--color-browser-surface-variant);
128176 --dynamic-surface: var(--color-browser-surface);
129177 --dynamic-text: var(--color-browser-text);
130178 --dynamic-muted: var(--color-browser-muted);
131179 --dynamic-accent: var(--color-browser-accent);
132180 --dynamic-border: var(--color-browser-border);
133181 }
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- }
142182 `;
143183 shadowRoot.appendChild(tokensStyle);
144184
@@ -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 once159 // 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

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