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}