v0.9.0 636c44c

shadowHost.ts RawHistoryFolder

1/**
2 * Global Shadow DOM Host Utility
3 *
4 * Ensures a single, shared ShadowRoot is created and reused for all injected UI components
5 * (omnibox, toast, etc.), with a minimal CSS reset applied once.
6 */
7
8// Import the compiled Tailwind bundle as a string so we can inject it
9// directly into the shadow root. The ?inline suffix makes Vite embed the
10// processed CSS, ensuring all utility classes are available without relying
11// on runtime @import resolution (which fails inside the injected shadow DOM).
12import globalStyles from "../../styles/tailwind.css?inline";
13
14// Track shadow-root initialization to avoid duplicate reset injection
15const shadowResetApplied = new WeakSet<ShadowRoot>();
16
17const HOST_STYLES = `
18      position: fixed !important;
19      top: 0 !important;
20      left: 0 !important;
21      right: 0 !important;
22      bottom: 0 !important;
23      z-index: 2147483647 !important;
24      pointer-events: none !important;
25      margin: 0 !important;
26      padding: 0 !important;
27      border: none !important;
28      background: none !important;
29      isolation: isolate !important;
30      contain: layout style !important;
31    `;
32
33let cachedHost: HTMLElement | null = null;
34let hostObserver: MutationObserver | null = null;
35let hostKeepaliveTimer: number | null = null;
36
37function ensureHostPresence(host: HTMLElement): void {
38  const docEl = document.documentElement;
39  if (docEl && !host.isConnected) {
40    docEl.appendChild(host);
41  }
42
43  if (typeof MutationObserver !== "undefined") {
44    if (!hostObserver) {
45      hostObserver = new MutationObserver(() => {
46        if (cachedHost && !cachedHost.isConnected && document.documentElement) {
47          document.documentElement.appendChild(cachedHost);
48        }
49      });
50    }
51
52    try {
53      hostObserver.disconnect();
54      if (docEl) {
55        hostObserver.observe(docEl, { childList: true });
56      }
57    } catch {
58      // MutationObserver might not be allowed by the page; fall back to polling in that case.
59    }
60  }
61
62  if (hostKeepaliveTimer === null && typeof window !== "undefined") {
63    hostKeepaliveTimer = window.setInterval(() => {
64      if (cachedHost && !cachedHost.isConnected && document.documentElement) {
65        document.documentElement.appendChild(cachedHost);
66      }
67    }, 2000);
68  }
69}
70
71export const GLOBAL_SHADOW_HOST_ID = "dumber-ui-root";
72
73/**
74 * Ensure and return the global ShadowRoot used by injected UI.
75 */
76export function getGlobalShadowRoot(): ShadowRoot {
77  // Create or find the host container
78  let host = document.getElementById(
79    GLOBAL_SHADOW_HOST_ID,
80  ) as HTMLElement | null;
81  if (!host) {
82    host = document.createElement("div");
83    host.id = GLOBAL_SHADOW_HOST_ID;
84  }
85
86  host.style.cssText = HOST_STYLES;
87
88  cachedHost = host;
89  ensureHostPresence(host);
90
91  const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: "open" });
92
93  // Reflect current theme from document root to the shadow host so :host(.dark) works
94  try {
95    const isDark = document.documentElement.classList.contains("dark");
96    host.classList.toggle("dark", isDark);
97    // Keep it in sync if the theme toggles later
98    const observer = new MutationObserver(() => {
99      host.classList.toggle(
100        "dark",
101        document.documentElement.classList.contains("dark"),
102      );
103    });
104    observer.observe(document.documentElement, {
105      attributes: true,
106      attributeFilter: ["class"],
107    });
108  } catch {
109    /* no-op */
110  }
111
112  // Inject a minimal reset and base styles into the shadow root once
113  if (!shadowResetApplied.has(shadowRoot)) {
114    const resetStyle = document.createElement("style");
115    resetStyle.textContent = `
116      :host { all: initial; }
117      *, *::before, *::after { box-sizing: border-box; }
118      :host { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif; }
119    `;
120    shadowRoot.appendChild(resetStyle);
121
122    // Ensure dynamic design tokens exist within the shadow tree. Tailwind v4
123    // utilities rely on these CSS variables. We set light defaults and a dark variant on :host.
124    const tokensStyle = document.createElement("style");
125    tokensStyle.textContent = `
126      :host {
127        --dynamic-bg: var(--color-browser-bg);
128        --dynamic-surface: var(--color-browser-surface);
129        --dynamic-text: var(--color-browser-text);
130        --dynamic-muted: var(--color-browser-muted);
131        --dynamic-accent: var(--color-browser-accent);
132        --dynamic-border: var(--color-browser-border);
133      }
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    `;
143    shadowRoot.appendChild(tokensStyle);
144
145    // Inject the global GUI stylesheet so components rendered inside the shadow
146    // root (e.g., toasts, omnibox) receive their styles
147    try {
148      // Prefer constructable stylesheets when available
149      const hasConstructable = typeof CSSStyleSheet !== "undefined";
150      const supportsAdopted = "adoptedStyleSheets" in shadowRoot;
151      if (supportsAdopted && hasConstructable) {
152        const sheet = new CSSStyleSheet();
153        sheet.replaceSync(globalStyles);
154        const rootWithSheets = shadowRoot as ShadowRoot & {
155          adoptedStyleSheets: CSSStyleSheet[];
156        };
157        rootWithSheets.adoptedStyleSheets = [
158          ...(rootWithSheets.adoptedStyleSheets ?? []),
159          sheet,
160        ];
161      } else {
162        // Fallback: append a <style> element with the global CSS
163        const styleTag = document.createElement("style");
164        styleTag.textContent = globalStyles;
165        shadowRoot.appendChild(styleTag);
166      }
167    } catch {
168      // Final fallback if anything above fails
169      const styleTag = document.createElement("style");
170      styleTag.textContent = globalStyles;
171      shadowRoot.appendChild(styleTag);
172    }
173    shadowResetApplied.add(shadowRoot);
174  }
175
176  return shadowRoot;
177}
178
179/**
180 * Ensure and return a dedicated mount element inside the global ShadowRoot.
181 * Use a unique id per feature (e.g., "dumber-omnibox", "dumber-toast").
182 */
183export function ensureShadowMount(mountId: string): HTMLElement {
184  const root = getGlobalShadowRoot();
185  let mount = root.getElementById?.(mountId) as HTMLElement | null;
186  if (!mount) {
187    mount = document.createElement("div");
188    mount.id = mountId;
189    mount.style.cssText = `
190      position: relative;
191      pointer-events: none; /* components inside control their own pointer events */
192    `;
193    root.appendChild(mount);
194  }
195  return mount;
196}

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