feat(gui): add global shadow dom host utility for component isolation
- commit
- 382faa5b845d3447ef530528b5d5fa0511a1ad5d
- author
- bamen · · 2025-09-16 08:13 UTC
- parent
- 3ef9418
- signature
- unsigned
1 file · +74 -0
A gui/src/injected/modules/shadowHost.ts +74 -0
| @@ -0,0 +1,74 @@ | |||
| 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 | + | // Track shadow-root initialization to avoid duplicate reset injection | |
| 9 | + | const shadowResetApplied = new WeakSet<ShadowRoot>(); | |
| 10 | + | ||
| 11 | + | export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root'; | |
| 12 | + | ||
| 13 | + | /** | |
| 14 | + | * Ensure and return the global ShadowRoot used by injected UI. | |
| 15 | + | */ | |
| 16 | + | export function getGlobalShadowRoot(): ShadowRoot { | |
| 17 | + | // Create or find the host container | |
| 18 | + | let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null; | |
| 19 | + | if (!host) { | |
| 20 | + | host = document.createElement('div'); | |
| 21 | + | host.id = GLOBAL_SHADOW_HOST_ID; | |
| 22 | + | host.style.cssText = ` | |
| 23 | + | position: fixed !important; | |
| 24 | + | top: 0 !important; | |
| 25 | + | left: 0 !important; | |
| 26 | + | right: 0 !important; | |
| 27 | + | bottom: 0 !important; | |
| 28 | + | z-index: 2147483647 !important; | |
| 29 | + | pointer-events: none !important; | |
| 30 | + | margin: 0 !important; | |
| 31 | + | padding: 0 !important; | |
| 32 | + | border: none !important; | |
| 33 | + | background: none !important; | |
| 34 | + | isolation: isolate !important; | |
| 35 | + | contain: layout style !important; | |
| 36 | + | `; | |
| 37 | + | document.documentElement.appendChild(host); | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' }); | |
| 41 | + | ||
| 42 | + | // Inject a minimal reset and base styles into the shadow root once | |
| 43 | + | if (!shadowResetApplied.has(shadowRoot)) { | |
| 44 | + | const resetStyle = document.createElement('style'); | |
| 45 | + | resetStyle.textContent = ` | |
| 46 | + | :host { all: initial; } | |
| 47 | + | *, *::before, *::after { box-sizing: border-box; } | |
| 48 | + | :host { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif; } | |
| 49 | + | `; | |
| 50 | + | shadowRoot.appendChild(resetStyle); | |
| 51 | + | shadowResetApplied.add(shadowRoot); | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | return shadowRoot; | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | /** | |
| 58 | + | * Ensure and return a dedicated mount element inside the global ShadowRoot. | |
| 59 | + | * Use a unique id per feature (e.g., "dumber-omnibox", "dumber-toast"). | |
| 60 | + | */ | |
| 61 | + | export function ensureShadowMount(mountId: string): HTMLElement { | |
| 62 | + | const root = getGlobalShadowRoot(); | |
| 63 | + | let mount = root.getElementById?.(mountId) as HTMLElement | null; | |
| 64 | + | if (!mount) { | |
| 65 | + | mount = document.createElement('div'); | |
| 66 | + | mount.id = mountId; | |
| 67 | + | mount.style.cssText = ` | |
| 68 | + | position: relative; | |
| 69 | + | pointer-events: none; /* components inside control their own pointer events */ | |
| 70 | + | `; | |
| 71 | + | root.appendChild(mount); | |
| 72 | + | } | |
| 73 | + | return mount; | |
| 74 | + | } | |
| @@ -0,0 +1,74 @@ | |||||
| 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 | + | // Track shadow-root initialization to avoid duplicate reset injection | |||
| 9 | + | const shadowResetApplied = new WeakSet<ShadowRoot>(); | |||
| 10 | + | ||||
| 11 | + | export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root'; | |||
| 12 | + | ||||
| 13 | + | /** | |||
| 14 | + | * Ensure and return the global ShadowRoot used by injected UI. | |||
| 15 | + | */ | |||
| 16 | + | export function getGlobalShadowRoot(): ShadowRoot { | |||
| 17 | + | // Create or find the host container | |||
| 18 | + | let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null; | |||
| 19 | + | if (!host) { | |||
| 20 | + | host = document.createElement('div'); | |||
| 21 | + | host.id = GLOBAL_SHADOW_HOST_ID; | |||
| 22 | + | host.style.cssText = ` | |||
| 23 | + | position: fixed !important; | |||
| 24 | + | top: 0 !important; | |||
| 25 | + | left: 0 !important; | |||
| 26 | + | right: 0 !important; | |||
| 27 | + | bottom: 0 !important; | |||
| 28 | + | z-index: 2147483647 !important; | |||
| 29 | + | pointer-events: none !important; | |||
| 30 | + | margin: 0 !important; | |||
| 31 | + | padding: 0 !important; | |||
| 32 | + | border: none !important; | |||
| 33 | + | background: none !important; | |||
| 34 | + | isolation: isolate !important; | |||
| 35 | + | contain: layout style !important; | |||
| 36 | + | `; | |||
| 37 | + | document.documentElement.appendChild(host); | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' }); | |||
| 41 | + | ||||
| 42 | + | // Inject a minimal reset and base styles into the shadow root once | |||
| 43 | + | if (!shadowResetApplied.has(shadowRoot)) { | |||
| 44 | + | const resetStyle = document.createElement('style'); | |||
| 45 | + | resetStyle.textContent = ` | |||
| 46 | + | :host { all: initial; } | |||
| 47 | + | *, *::before, *::after { box-sizing: border-box; } | |||
| 48 | + | :host { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif; } | |||
| 49 | + | `; | |||
| 50 | + | shadowRoot.appendChild(resetStyle); | |||
| 51 | + | shadowResetApplied.add(shadowRoot); | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | return shadowRoot; | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | /** | |||
| 58 | + | * Ensure and return a dedicated mount element inside the global ShadowRoot. | |||
| 59 | + | * Use a unique id per feature (e.g., "dumber-omnibox", "dumber-toast"). | |||
| 60 | + | */ | |||
| 61 | + | export function ensureShadowMount(mountId: string): HTMLElement { | |||
| 62 | + | const root = getGlobalShadowRoot(); | |||
| 63 | + | let mount = root.getElementById?.(mountId) as HTMLElement | null; | |||
| 64 | + | if (!mount) { | |||
| 65 | + | mount = document.createElement('div'); | |||
| 66 | + | mount.id = mountId; | |||
| 67 | + | mount.style.cssText = ` | |||
| 68 | + | position: relative; | |||
| 69 | + | pointer-events: none; /* components inside control their own pointer events */ | |||
| 70 | + | `; | |||
| 71 | + | root.appendChild(mount); | |||
| 72 | + | } | |||
| 73 | + | return mount; | |||
| 74 | + | } | |||