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}