refactor(gui): integrate shadow dom host and webkit message bridge in injected modules
- commit
- dbf9169b1b0a1469465a663295e3f8d51ab0e9cd
- author
- bamen · · 2025-09-15 17:20 UTC
- committer
- bamen · · 2025-09-15 16:29 UTC
- parent
- 0675302
- signature
- unsigned
3 files · +84 -52
M gui/src/injected/gui.ts +17 -8
| @@ -18,6 +18,8 @@ interface DumberGUI { | |||
| 18 | 18 | isReady: boolean; | |
| 19 | 19 | } | |
| 20 | 20 | ||
| 21 | + | // (legacy suggestions normalization removed; omnibox now fetches suggestions directly via API) | |
| 22 | + | ||
| 21 | 23 | // DOM readiness utility - handles all document ready states | |
| 22 | 24 | function whenDOMReady(callback: () => void) { | |
| 23 | 25 | if (document.readyState === 'loading') { | |
| @@ -31,6 +33,9 @@ function whenDOMReady(callback: () => void) { | |||
| 31 | 33 | } | |
| 32 | 34 | } | |
| 33 | 35 | ||
| 36 | + | // Install a page-world bridge so Go can call window.__dumber_omnibox_suggestions() | |
| 37 | + | // Page-world bridge is injected by WebKit at document-start; no-op here to avoid duplicates. | |
| 38 | + | ||
| 34 | 39 | // Create global namespace | |
| 35 | 40 | declare global { | |
| 36 | 41 | interface Window { | |
| @@ -43,7 +48,6 @@ declare global { | |||
| 43 | 48 | __dumber_toggle?: () => void; | |
| 44 | 49 | __dumber_find_open?: (query?: string) => void; | |
| 45 | 50 | __dumber_find_close?: () => void; | |
| 46 | - | __dumber_setSuggestions?: (suggestions: any[]) => void; | |
| 47 | 51 | __dumber_find_query?: (query: string) => void; | |
| 48 | 52 | } | |
| 49 | 53 | } | |
| @@ -59,6 +63,8 @@ if (!window.__dumber_gui_ready) { | |||
| 59 | 63 | // Initialize Svelte GUI systems immediately | |
| 60 | 64 | whenDOMReady(async () => { | |
| 61 | 65 | try { | |
| 66 | + | // Page-world bridge is injected by WebKit; avoid duplicate injection here | |
| 67 | + | ||
| 62 | 68 | // Initialize toast system first | |
| 63 | 69 | await initializeToast(); | |
| 64 | 70 | ||
| @@ -151,6 +157,16 @@ if (!window.__dumber_gui_ready) { | |||
| 151 | 157 | }, true); | |
| 152 | 158 | ||
| 153 | 159 | console.log('✅ KeyboardService initialized with global listeners'); | |
| 160 | + | ||
| 161 | + | // Listen to bridge keyboard events from page-world and forward to KeyboardService | |
| 162 | + | document.addEventListener('dumber:key', (e: Event) => { | |
| 163 | + | const detail = (e as CustomEvent).detail || {}; | |
| 164 | + | if (detail && typeof detail.shortcut === 'string') { | |
| 165 | + | keyboardService.handleNativeShortcut(detail.shortcut); | |
| 166 | + | } | |
| 167 | + | }); | |
| 168 | + | ||
| 169 | + | // Suggestions are now fetched via API directly by the omnibox component; no page-bridge handlers needed | |
| 154 | 170 | }); | |
| 155 | 171 | ||
| 156 | 172 | // Legacy compatibility functions for existing Go code | |
| @@ -190,13 +206,6 @@ if (!window.__dumber_gui_ready) { | |||
| 190 | 206 | } | |
| 191 | 207 | }; | |
| 192 | 208 | ||
| 193 | - | // Key function that Go uses to send search suggestions | |
| 194 | - | window.__dumber_setSuggestions = (suggestions: any[]) => { | |
| 195 | - | if (window.__dumber_omnibox?.setSuggestions) { | |
| 196 | - | window.__dumber_omnibox.setSuggestions(suggestions); | |
| 197 | - | } | |
| 198 | - | }; | |
| 199 | - | ||
| 200 | 209 | // Find query function that Go uses to set find query | |
| 201 | 210 | window.__dumber_find_query = (query: string) => { | |
| 202 | 211 | if (window.__dumber_omnibox?.findQuery) { | |
| @@ -18,6 +18,8 @@ interface DumberGUI { | |||||
| 18 | isReady: boolean; | 18 | isReady: boolean; | ||
| 19 | } | 19 | } | ||
| 20 | 20 | ||||
| 21 | + | // (legacy suggestions normalization removed; omnibox now fetches suggestions directly via API) | |||
| 22 | + | ||||
| 21 | // DOM readiness utility - handles all document ready states | 23 | // DOM readiness utility - handles all document ready states | ||
| 22 | function whenDOMReady(callback: () => void) { | 24 | function whenDOMReady(callback: () => void) { | ||
| 23 | if (document.readyState === 'loading') { | 25 | if (document.readyState === 'loading') { | ||
| @@ -31,6 +33,9 @@ function whenDOMReady(callback: () => void) { | |||||
| 31 | } | 33 | } | ||
| 32 | } | 34 | } | ||
| 33 | 35 | ||||
| 36 | + | // Install a page-world bridge so Go can call window.__dumber_omnibox_suggestions() | |||
| 37 | + | // Page-world bridge is injected by WebKit at document-start; no-op here to avoid duplicates. | |||
| 38 | + | ||||
| 34 | // Create global namespace | 39 | // Create global namespace | ||
| 35 | declare global { | 40 | declare global { | ||
| 36 | interface Window { | 41 | interface Window { | ||
| @@ -43,7 +48,6 @@ declare global { | |||||
| 43 | __dumber_toggle?: () => void; | 48 | __dumber_toggle?: () => void; | ||
| 44 | __dumber_find_open?: (query?: string) => void; | 49 | __dumber_find_open?: (query?: string) => void; | ||
| 45 | __dumber_find_close?: () => void; | 50 | __dumber_find_close?: () => void; | ||
| 46 | - | __dumber_setSuggestions?: (suggestions: any[]) => void; | |||
| 47 | __dumber_find_query?: (query: string) => void; | 51 | __dumber_find_query?: (query: string) => void; | ||
| 48 | } | 52 | } | ||
| 49 | } | 53 | } | ||
| @@ -59,6 +63,8 @@ if (!window.__dumber_gui_ready) { | |||||
| 59 | // Initialize Svelte GUI systems immediately | 63 | // Initialize Svelte GUI systems immediately | ||
| 60 | whenDOMReady(async () => { | 64 | whenDOMReady(async () => { | ||
| 61 | try { | 65 | try { | ||
| 66 | + | // Page-world bridge is injected by WebKit; avoid duplicate injection here | |||
| 67 | + | ||||
| 62 | // Initialize toast system first | 68 | // Initialize toast system first | ||
| 63 | await initializeToast(); | 69 | await initializeToast(); | ||
| 64 | 70 | ||||
| @@ -151,6 +157,16 @@ if (!window.__dumber_gui_ready) { | |||||
| 151 | }, true); | 157 | }, true); | ||
| 152 | 158 | ||||
| 153 | console.log('✅ KeyboardService initialized with global listeners'); | 159 | console.log('✅ KeyboardService initialized with global listeners'); | ||
| 160 | + | ||||
| 161 | + | // Listen to bridge keyboard events from page-world and forward to KeyboardService | |||
| 162 | + | document.addEventListener('dumber:key', (e: Event) => { | |||
| 163 | + | const detail = (e as CustomEvent).detail || {}; | |||
| 164 | + | if (detail && typeof detail.shortcut === 'string') { | |||
| 165 | + | keyboardService.handleNativeShortcut(detail.shortcut); | |||
| 166 | + | } | |||
| 167 | + | }); | |||
| 168 | + | ||||
| 169 | + | // Suggestions are now fetched via API directly by the omnibox component; no page-bridge handlers needed | |||
| 154 | }); | 170 | }); | ||
| 155 | 171 | ||||
| 156 | // Legacy compatibility functions for existing Go code | 172 | // Legacy compatibility functions for existing Go code | ||
| @@ -190,13 +206,6 @@ if (!window.__dumber_gui_ready) { | |||||
| 190 | } | 206 | } | ||
| 191 | }; | 207 | }; | ||
| 192 | 208 | ||||
| 193 | - | // Key function that Go uses to send search suggestions | |||
| 194 | - | window.__dumber_setSuggestions = (suggestions: any[]) => { | |||
| 195 | - | if (window.__dumber_omnibox?.setSuggestions) { | |||
| 196 | - | window.__dumber_omnibox.setSuggestions(suggestions); | |||
| 197 | - | } | |||
| 198 | - | }; | |||
| 199 | - | ||||
| 200 | // Find query function that Go uses to set find query | 209 | // Find query function that Go uses to set find query | ||
| 201 | window.__dumber_find_query = (query: string) => { | 210 | window.__dumber_find_query = (query: string) => { | ||
| 202 | if (window.__dumber_omnibox?.findQuery) { | 211 | if (window.__dumber_omnibox?.findQuery) { | ||
M gui/src/injected/modules/omnibox.ts +36 -32
| @@ -6,6 +6,7 @@ | |||
| 6 | 6 | ||
| 7 | 7 | import { mount } from 'svelte'; | |
| 8 | 8 | import { Omnibox } from '../../components/omnibox'; | |
| 9 | + | import { ensureShadowMount } from './shadowHost'; | |
| 9 | 10 | import type { OmniboxConfig } from '../../components/omnibox/types'; | |
| 10 | 11 | ||
| 11 | 12 | let omniboxComponent: ReturnType<typeof mount> | null = null; | |
| @@ -19,7 +20,7 @@ export interface OmniboxInitConfig extends OmniboxConfig { | |||
| 19 | 20 | /** | |
| 20 | 21 | * Initialize the omnibox component system | |
| 21 | 22 | */ | |
| 22 | - | export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise<void> { | |
| 23 | + | export async function initializeOmnibox(_config: OmniboxInitConfig = {}): Promise<void> { | |
| 23 | 24 | if (isInitialized) { | |
| 24 | 25 | console.warn('Omnibox already initialized'); | |
| 25 | 26 | return; | |
| @@ -38,34 +39,10 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise | |||
| 38 | 39 | return; | |
| 39 | 40 | } | |
| 40 | 41 | ||
| 41 | - | // Create container element | |
| 42 | - | const containerId = config.containerId || 'dumber-omnibox-root'; | |
| 43 | - | let container = document.getElementById(containerId); | |
| 44 | - | ||
| 45 | - | if (!container) { | |
| 46 | - | container = document.createElement('div'); | |
| 47 | - | container.id = containerId; | |
| 48 | - | container.style.cssText = ` | |
| 49 | - | position: fixed !important; | |
| 50 | - | top: 0 !important; | |
| 51 | - | left: 0 !important; | |
| 52 | - | right: 0 !important; | |
| 53 | - | bottom: 0 !important; | |
| 54 | - | z-index: 2147483647 !important; | |
| 55 | - | pointer-events: none !important; | |
| 56 | - | margin: 0 !important; | |
| 57 | - | padding: 0 !important; | |
| 58 | - | border: none !important; | |
| 59 | - | background: none !important; | |
| 60 | - | isolation: isolate !important; | |
| 61 | - | contain: layout style !important; | |
| 62 | - | `; | |
| 63 | - | document.documentElement.appendChild(container); | |
| 64 | - | } | |
| 65 | - | ||
| 66 | - | // Mount the Svelte component | |
| 42 | + | // Mount omnibox into the shared global Shadow DOM host | |
| 43 | + | const mountEl = ensureShadowMount('dumber-omnibox'); | |
| 67 | 44 | omniboxComponent = mount(Omnibox, { | |
| 68 | - | target: container | |
| 45 | + | target: mountEl as unknown as Element | |
| 69 | 46 | }); | |
| 70 | 47 | ||
| 71 | 48 | // Wait for the component to mount and set up the global API | |
| @@ -75,6 +52,12 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise | |||
| 75 | 52 | console.log('✅ Omnibox component system initialized'); | |
| 76 | 53 | console.log('🔧 Global API is available:', Object.keys(window.__dumber_omnibox)); | |
| 77 | 54 | isInitialized = true; | |
| 55 | + | // Notify isolated bundle listeners that omnibox is ready | |
| 56 | + | try { | |
| 57 | + | document.dispatchEvent(new CustomEvent('dumber:omnibox-ready')); | |
| 58 | + | } catch (e) { | |
| 59 | + | console.warn('Failed to dispatch omnibox-ready event', e); | |
| 60 | + | } | |
| 78 | 61 | resolve(); | |
| 79 | 62 | } else { | |
| 80 | 63 | setTimeout(checkAPI, 10); // Check again in 10ms | |
| @@ -83,6 +66,30 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise | |||
| 83 | 66 | checkAPI(); | |
| 84 | 67 | }); | |
| 85 | 68 | ||
| 69 | + | // Bridge: listen for page-world events carrying suggestions and forward to API | |
| 70 | + | const handleSuggestionsEvent = (e: Event) => { | |
| 71 | + | try { | |
| 72 | + | const detail = (e as CustomEvent).detail; | |
| 73 | + | const suggestions = detail?.suggestions ?? detail; | |
| 74 | + | if (Array.isArray(suggestions) && window.__dumber_omnibox?.setSuggestions) { | |
| 75 | + | console.log('🔗 [Bridge] Received page-world suggestions event:', suggestions.length); | |
| 76 | + | window.__dumber_omnibox.setSuggestions(suggestions); | |
| 77 | + | } else { | |
| 78 | + | console.warn('🔗 [Bridge] Suggestions event received but API not ready or invalid payload'); | |
| 79 | + | } | |
| 80 | + | } catch (err) { | |
| 81 | + | console.warn('🔗 [Bridge] Failed handling suggestions event:', err); | |
| 82 | + | } | |
| 83 | + | }; | |
| 84 | + | ||
| 85 | + | document.addEventListener( | |
| 86 | + | 'dumber:omnibox-suggestions', | |
| 87 | + | handleSuggestionsEvent, | |
| 88 | + | false | |
| 89 | + | ); | |
| 90 | + | // Unified event name | |
| 91 | + | document.addEventListener('dumber:omnibox:suggestions', handleSuggestionsEvent, false); | |
| 92 | + | ||
| 86 | 93 | } catch (error) { | |
| 87 | 94 | console.error('❌ Failed to initialize omnibox component:', error); | |
| 88 | 95 | throw error; | |
| @@ -109,10 +116,7 @@ export function cleanupOmnibox(): void { | |||
| 109 | 116 | omniboxComponent = null; | |
| 110 | 117 | } | |
| 111 | 118 | ||
| 112 | - | const container = document.getElementById('dumber-omnibox-root'); | |
| 113 | - | if (container) { | |
| 114 | - | container.remove(); | |
| 115 | - | } | |
| 119 | + | // The shared shadow host remains persistent; just log cleanup | |
| 116 | 120 | ||
| 117 | 121 | isInitialized = false; | |
| 118 | 122 | console.log('🧹 Omnibox component cleaned up'); | |
| @@ -6,6 +6,7 @@ | |||||
| 6 | 6 | ||||
| 7 | import { mount } from 'svelte'; | 7 | import { mount } from 'svelte'; | ||
| 8 | import { Omnibox } from '../../components/omnibox'; | 8 | import { Omnibox } from '../../components/omnibox'; | ||
| 9 | + | import { ensureShadowMount } from './shadowHost'; | |||
| 9 | import type { OmniboxConfig } from '../../components/omnibox/types'; | 10 | import type { OmniboxConfig } from '../../components/omnibox/types'; | ||
| 10 | 11 | ||||
| 11 | let omniboxComponent: ReturnType<typeof mount> | null = null; | 12 | let omniboxComponent: ReturnType<typeof mount> | null = null; | ||
| @@ -19,7 +20,7 @@ export interface OmniboxInitConfig extends OmniboxConfig { | |||||
| 19 | /** | 20 | /** | ||
| 20 | * Initialize the omnibox component system | 21 | * Initialize the omnibox component system | ||
| 21 | */ | 22 | */ | ||
| 22 | - | export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise<void> { | 23 | + | export async function initializeOmnibox(_config: OmniboxInitConfig = {}): Promise<void> { |
| 23 | if (isInitialized) { | 24 | if (isInitialized) { | ||
| 24 | console.warn('Omnibox already initialized'); | 25 | console.warn('Omnibox already initialized'); | ||
| 25 | return; | 26 | return; | ||
| @@ -38,34 +39,10 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise | |||||
| 38 | return; | 39 | return; | ||
| 39 | } | 40 | } | ||
| 40 | 41 | ||||
| 41 | - | // Create container element | 42 | + | // Mount omnibox into the shared global Shadow DOM host |
| 42 | - | const containerId = config.containerId || 'dumber-omnibox-root'; | 43 | + | const mountEl = ensureShadowMount('dumber-omnibox'); |
| 43 | - | let container = document.getElementById(containerId); | |||
| 44 | - | ||||
| 45 | - | if (!container) { | |||
| 46 | - | container = document.createElement('div'); | |||
| 47 | - | container.id = containerId; | |||
| 48 | - | container.style.cssText = ` | |||
| 49 | - | position: fixed !important; | |||
| 50 | - | top: 0 !important; | |||
| 51 | - | left: 0 !important; | |||
| 52 | - | right: 0 !important; | |||
| 53 | - | bottom: 0 !important; | |||
| 54 | - | z-index: 2147483647 !important; | |||
| 55 | - | pointer-events: none !important; | |||
| 56 | - | margin: 0 !important; | |||
| 57 | - | padding: 0 !important; | |||
| 58 | - | border: none !important; | |||
| 59 | - | background: none !important; | |||
| 60 | - | isolation: isolate !important; | |||
| 61 | - | contain: layout style !important; | |||
| 62 | - | `; | |||
| 63 | - | document.documentElement.appendChild(container); | |||
| 64 | - | } | |||
| 65 | - | ||||
| 66 | - | // Mount the Svelte component | |||
| 67 | omniboxComponent = mount(Omnibox, { | 44 | omniboxComponent = mount(Omnibox, { | ||
| 68 | - | target: container | 45 | + | target: mountEl as unknown as Element |
| 69 | }); | 46 | }); | ||
| 70 | 47 | ||||
| 71 | // Wait for the component to mount and set up the global API | 48 | // Wait for the component to mount and set up the global API | ||
| @@ -75,6 +52,12 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise | |||||
| 75 | console.log('✅ Omnibox component system initialized'); | 52 | console.log('✅ Omnibox component system initialized'); | ||
| 76 | console.log('🔧 Global API is available:', Object.keys(window.__dumber_omnibox)); | 53 | console.log('🔧 Global API is available:', Object.keys(window.__dumber_omnibox)); | ||
| 77 | isInitialized = true; | 54 | isInitialized = true; | ||
| 55 | + | // Notify isolated bundle listeners that omnibox is ready | |||
| 56 | + | try { | |||
| 57 | + | document.dispatchEvent(new CustomEvent('dumber:omnibox-ready')); | |||
| 58 | + | } catch (e) { | |||
| 59 | + | console.warn('Failed to dispatch omnibox-ready event', e); | |||
| 60 | + | } | |||
| 78 | resolve(); | 61 | resolve(); | ||
| 79 | } else { | 62 | } else { | ||
| 80 | setTimeout(checkAPI, 10); // Check again in 10ms | 63 | setTimeout(checkAPI, 10); // Check again in 10ms | ||
| @@ -83,6 +66,30 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise | |||||
| 83 | checkAPI(); | 66 | checkAPI(); | ||
| 84 | }); | 67 | }); | ||
| 85 | 68 | ||||
| 69 | + | // Bridge: listen for page-world events carrying suggestions and forward to API | |||
| 70 | + | const handleSuggestionsEvent = (e: Event) => { | |||
| 71 | + | try { | |||
| 72 | + | const detail = (e as CustomEvent).detail; | |||
| 73 | + | const suggestions = detail?.suggestions ?? detail; | |||
| 74 | + | if (Array.isArray(suggestions) && window.__dumber_omnibox?.setSuggestions) { | |||
| 75 | + | console.log('🔗 [Bridge] Received page-world suggestions event:', suggestions.length); | |||
| 76 | + | window.__dumber_omnibox.setSuggestions(suggestions); | |||
| 77 | + | } else { | |||
| 78 | + | console.warn('🔗 [Bridge] Suggestions event received but API not ready or invalid payload'); | |||
| 79 | + | } | |||
| 80 | + | } catch (err) { | |||
| 81 | + | console.warn('🔗 [Bridge] Failed handling suggestions event:', err); | |||
| 82 | + | } | |||
| 83 | + | }; | |||
| 84 | + | ||||
| 85 | + | document.addEventListener( | |||
| 86 | + | 'dumber:omnibox-suggestions', | |||
| 87 | + | handleSuggestionsEvent, | |||
| 88 | + | false | |||
| 89 | + | ); | |||
| 90 | + | // Unified event name | |||
| 91 | + | document.addEventListener('dumber:omnibox:suggestions', handleSuggestionsEvent, false); | |||
| 92 | + | ||||
| 86 | } catch (error) { | 93 | } catch (error) { | ||
| 87 | console.error('❌ Failed to initialize omnibox component:', error); | 94 | console.error('❌ Failed to initialize omnibox component:', error); | ||
| 88 | throw error; | 95 | throw error; | ||
| @@ -109,10 +116,7 @@ export function cleanupOmnibox(): void { | |||||
| 109 | omniboxComponent = null; | 116 | omniboxComponent = null; | ||
| 110 | } | 117 | } | ||
| 111 | 118 | ||||
| 112 | - | const container = document.getElementById('dumber-omnibox-root'); | 119 | + | // The shared shadow host remains persistent; just log cleanup |
| 113 | - | if (container) { | |||
| 114 | - | container.remove(); | |||
| 115 | - | } | |||
| 116 | 120 | ||||
| 117 | isInitialized = false; | 121 | isInitialized = false; | ||
| 118 | console.log('🧹 Omnibox component cleaned up'); | 122 | console.log('🧹 Omnibox component cleaned up'); | ||
M gui/src/injected/modules/toast.ts +31 -12
| @@ -7,6 +7,7 @@ | |||
| 7 | 7 | ||
| 8 | 8 | import { mount, flushSync } from 'svelte'; | |
| 9 | 9 | import ToastContainer from '$components/toast/ToastContainer.svelte'; | |
| 10 | + | import { ensureShadowMount } from './shadowHost'; | |
| 10 | 11 | import '$lib/styles.css'; | |
| 11 | 12 | ||
| 12 | 13 | export interface ToastConfig { | |
| @@ -78,17 +79,8 @@ export async function initializeToast(config?: ToastConfig): Promise<void> { | |||
| 78 | 79 | } | |
| 79 | 80 | ||
| 80 | 81 | try { | |
| 81 | - | // Find or create browser component root | |
| 82 | - | let rootElement = document.querySelector('.browser-component-root') as HTMLDivElement; | |
| 83 | - | if (!rootElement) { | |
| 84 | - | console.log('🔧 Creating browser-component-root'); | |
| 85 | - | rootElement = document.createElement('div'); | |
| 86 | - | rootElement.className = 'browser-component-root'; | |
| 87 | - | rootElement.style.cssText = 'position: fixed; top: 0; left: 0; pointer-events: none; z-index: 2147483647;'; | |
| 88 | - | document.documentElement.appendChild(rootElement); | |
| 89 | - | } else { | |
| 90 | - | console.log('✅ Found existing browser-component-root'); | |
| 91 | - | } | |
| 82 | + | // Use the global Shadow DOM host for isolation | |
| 83 | + | const rootElement = ensureShadowMount('dumber-toast'); | |
| 92 | 84 | ||
| 93 | 85 | // Initialize theme | |
| 94 | 86 | const initialTheme = config?.theme || window.__dumber_initial_theme; | |
| @@ -98,9 +90,36 @@ export async function initializeToast(config?: ToastConfig): Promise<void> { | |||
| 98 | 90 | ||
| 99 | 91 | // Mount the Svelte toast container (Svelte 5 syntax) | |
| 100 | 92 | mount(ToastContainer, { | |
| 101 | - | target: rootElement | |
| 93 | + | target: rootElement as unknown as Element | |
| 102 | 94 | }); | |
| 103 | 95 | ||
| 96 | + | // Signal isolated GUI readiness for page-world bridge | |
| 97 | + | try { | |
| 98 | + | document.documentElement.setAttribute('data-dumber-gui', 'ready'); | |
| 99 | + | document.dispatchEvent(new CustomEvent('dumber:gui-ready')); | |
| 100 | + | } catch { | |
| 101 | + | // Ignore errors in GUI readiness signaling | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // Listen for page-world bridge events and forward to internal toast functions | |
| 105 | + | const handleShowToast = (e: Event) => { | |
| 106 | + | const detail = (e as CustomEvent).detail || {}; | |
| 107 | + | if (typeof window.__dumber_showToast === 'function') { | |
| 108 | + | window.__dumber_showToast(detail.message, detail.duration, detail.type); | |
| 109 | + | } | |
| 110 | + | }; | |
| 111 | + | document.addEventListener('dumber:showToast', handleShowToast); | |
| 112 | + | document.addEventListener('dumber:toast:show', handleShowToast); | |
| 113 | + | ||
| 114 | + | const handleZoomToast = (e: Event) => { | |
| 115 | + | const detail = (e as CustomEvent).detail || {}; | |
| 116 | + | if (typeof window.__dumber_showZoomToast === 'function') { | |
| 117 | + | window.__dumber_showZoomToast(detail.level); | |
| 118 | + | } | |
| 119 | + | }; | |
| 120 | + | document.addEventListener('dumber:showZoomToast', handleZoomToast); | |
| 121 | + | document.addEventListener('dumber:toast:zoom', handleZoomToast); | |
| 122 | + | ||
| 104 | 123 | // Force immediate effect execution to ensure onMount callbacks run | |
| 105 | 124 | flushSync(); | |
| 106 | 125 | ||
| @@ -7,6 +7,7 @@ | |||||
| 7 | 7 | ||||
| 8 | import { mount, flushSync } from 'svelte'; | 8 | import { mount, flushSync } from 'svelte'; | ||
| 9 | import ToastContainer from '$components/toast/ToastContainer.svelte'; | 9 | import ToastContainer from '$components/toast/ToastContainer.svelte'; | ||
| 10 | + | import { ensureShadowMount } from './shadowHost'; | |||
| 10 | import '$lib/styles.css'; | 11 | import '$lib/styles.css'; | ||
| 11 | 12 | ||||
| 12 | export interface ToastConfig { | 13 | export interface ToastConfig { | ||
| @@ -78,17 +79,8 @@ export async function initializeToast(config?: ToastConfig): Promise<void> { | |||||
| 78 | } | 79 | } | ||
| 79 | 80 | ||||
| 80 | try { | 81 | try { | ||
| 81 | - | // Find or create browser component root | 82 | + | // Use the global Shadow DOM host for isolation |
| 82 | - | let rootElement = document.querySelector('.browser-component-root') as HTMLDivElement; | 83 | + | const rootElement = ensureShadowMount('dumber-toast'); |
| 83 | - | if (!rootElement) { | |||
| 84 | - | console.log('🔧 Creating browser-component-root'); | |||
| 85 | - | rootElement = document.createElement('div'); | |||
| 86 | - | rootElement.className = 'browser-component-root'; | |||
| 87 | - | rootElement.style.cssText = 'position: fixed; top: 0; left: 0; pointer-events: none; z-index: 2147483647;'; | |||
| 88 | - | document.documentElement.appendChild(rootElement); | |||
| 89 | - | } else { | |||
| 90 | - | console.log('✅ Found existing browser-component-root'); | |||
| 91 | - | } | |||
| 92 | 84 | ||||
| 93 | // Initialize theme | 85 | // Initialize theme | ||
| 94 | const initialTheme = config?.theme || window.__dumber_initial_theme; | 86 | const initialTheme = config?.theme || window.__dumber_initial_theme; | ||
| @@ -98,9 +90,36 @@ export async function initializeToast(config?: ToastConfig): Promise<void> { | |||||
| 98 | 90 | ||||
| 99 | // Mount the Svelte toast container (Svelte 5 syntax) | 91 | // Mount the Svelte toast container (Svelte 5 syntax) | ||
| 100 | mount(ToastContainer, { | 92 | mount(ToastContainer, { | ||
| 101 | - | target: rootElement | 93 | + | target: rootElement as unknown as Element |
| 102 | }); | 94 | }); | ||
| 103 | 95 | ||||
| 96 | + | // Signal isolated GUI readiness for page-world bridge | |||
| 97 | + | try { | |||
| 98 | + | document.documentElement.setAttribute('data-dumber-gui', 'ready'); | |||
| 99 | + | document.dispatchEvent(new CustomEvent('dumber:gui-ready')); | |||
| 100 | + | } catch { | |||
| 101 | + | // Ignore errors in GUI readiness signaling | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // Listen for page-world bridge events and forward to internal toast functions | |||
| 105 | + | const handleShowToast = (e: Event) => { | |||
| 106 | + | const detail = (e as CustomEvent).detail || {}; | |||
| 107 | + | if (typeof window.__dumber_showToast === 'function') { | |||
| 108 | + | window.__dumber_showToast(detail.message, detail.duration, detail.type); | |||
| 109 | + | } | |||
| 110 | + | }; | |||
| 111 | + | document.addEventListener('dumber:showToast', handleShowToast); | |||
| 112 | + | document.addEventListener('dumber:toast:show', handleShowToast); | |||
| 113 | + | ||||
| 114 | + | const handleZoomToast = (e: Event) => { | |||
| 115 | + | const detail = (e as CustomEvent).detail || {}; | |||
| 116 | + | if (typeof window.__dumber_showZoomToast === 'function') { | |||
| 117 | + | window.__dumber_showZoomToast(detail.level); | |||
| 118 | + | } | |||
| 119 | + | }; | |||
| 120 | + | document.addEventListener('dumber:showZoomToast', handleZoomToast); | |||
| 121 | + | document.addEventListener('dumber:toast:zoom', handleZoomToast); | |||
| 122 | + | ||||
| 104 | // Force immediate effect execution to ensure onMount callbacks run | 123 | // Force immediate effect execution to ensure onMount callbacks run | ||
| 105 | flushSync(); | 124 | flushSync(); | ||
| 106 | 125 | ||||