v0.8.0 0fa6785

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
17export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root';
18
19/**
20 * Ensure and return the global ShadowRoot used by injected UI.
21 */
22export function getGlobalShadowRoot(): ShadowRoot {
23  // Create or find the host container
24  let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null;
25  if (!host) {
26    host = document.createElement('div');
27    host.id = GLOBAL_SHADOW_HOST_ID;
28    host.style.cssText = `
29      position: fixed !important;
30      top: 0 !important;
31      left: 0 !important;
32      right: 0 !important;
33      bottom: 0 !important;
34      z-index: 2147483647 !important;
35      pointer-events: none !important;
36      margin: 0 !important;
37      padding: 0 !important;
38      border: none !important;
39      background: none !important;
40      isolation: isolate !important;
41      contain: layout style !important;
42    `;
43    document.documentElement.appendChild(host);
44  }
45
46  const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' });
47
48  // Reflect current theme from document root to the shadow host so :host(.dark) works
49  try {
50    const isDark = document.documentElement.classList.contains('dark');
51    host.classList.toggle('dark', isDark);
52    // Keep it in sync if the theme toggles later
53    const observer = new MutationObserver(() => {
54      host.classList.toggle('dark', document.documentElement.classList.contains('dark'));
55    });
56    observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
57  } catch { /* no-op */ }
58
59  // Inject a minimal reset and base styles into the shadow root once
60  if (!shadowResetApplied.has(shadowRoot)) {
61    const resetStyle = document.createElement('style');
62    resetStyle.textContent = `
63      :host { all: initial; }
64      *, *::before, *::after { box-sizing: border-box; }
65      :host { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif; }
66    `;
67    shadowRoot.appendChild(resetStyle);
68
69    // Ensure dynamic design tokens exist within the shadow tree. Tailwind v4
70    // utilities rely on these CSS variables. We set light defaults and a dark variant on :host.
71    const tokensStyle = document.createElement('style');
72    tokensStyle.textContent = `
73      :host {
74        --dynamic-bg: var(--color-browser-bg);
75        --dynamic-surface: var(--color-browser-surface);
76        --dynamic-text: var(--color-browser-text);
77        --dynamic-muted: var(--color-browser-muted);
78        --dynamic-accent: var(--color-browser-accent);
79        --dynamic-border: var(--color-browser-border);
80      }
81      :host(.dark) {
82        --dynamic-bg: rgb(17 24 39);
83        --dynamic-surface: rgb(31 41 55);
84        --dynamic-text: rgb(243 244 246);
85        --dynamic-muted: rgb(156 163 175);
86        --dynamic-accent: rgb(96 165 250);
87        --dynamic-border: rgb(55 65 81);
88      }
89    `;
90    shadowRoot.appendChild(tokensStyle);
91
92    // Inject the global GUI stylesheet so components rendered inside the shadow
93    // root (e.g., toasts, omnibox) receive their styles
94    try {
95      // Prefer constructable stylesheets when available
96      const hasConstructable = typeof CSSStyleSheet !== 'undefined';
97      const supportsAdopted = 'adoptedStyleSheets' in shadowRoot;
98      if (supportsAdopted && hasConstructable) {
99        const sheet = new CSSStyleSheet();
100        sheet.replaceSync(globalStyles);
101        const rootWithSheets = shadowRoot as ShadowRoot & { adoptedStyleSheets: CSSStyleSheet[] };
102        rootWithSheets.adoptedStyleSheets = [...(rootWithSheets.adoptedStyleSheets ?? []), sheet];
103      } else {
104        // Fallback: append a <style> element with the global CSS
105        const styleTag = document.createElement('style');
106        styleTag.textContent = globalStyles;
107        shadowRoot.appendChild(styleTag);
108      }
109    } catch {
110      // Final fallback if anything above fails
111      const styleTag = document.createElement('style');
112      styleTag.textContent = globalStyles;
113      shadowRoot.appendChild(styleTag);
114    }
115    shadowResetApplied.add(shadowRoot);
116  }
117
118  return shadowRoot;
119}
120
121/**
122 * Ensure and return a dedicated mount element inside the global ShadowRoot.
123 * Use a unique id per feature (e.g., "dumber-omnibox", "dumber-toast").
124 */
125export function ensureShadowMount(mountId: string): HTMLElement {
126  const root = getGlobalShadowRoot();
127  let mount = root.getElementById?.(mountId) as HTMLElement | null;
128  if (!mount) {
129    mount = document.createElement('div');
130    mount.id = mountId;
131    mount.style.cssText = `
132      position: relative;
133      pointer-events: none; /* components inside control their own pointer events */
134    `;
135    root.appendChild(mount);
136  }
137  return mount;
138}

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