fix(keyboard): debounce shortcuts and surface missing APIs
- commit
- 30952c4c7d94ba0034b604e3adeded0974ca6eeb
- author
- bamen · · 2025-09-17 05:17 UTC
- parent
- 0066178
- signature
- unsigned
4 files · +61 -42
M gui/src/components/omnibox/OmniboxFind.svelte +1 -0
| @@ -87,6 +87,7 @@ | |||
| 87 | 87 | aria-selected={isSelected} | |
| 88 | 88 | onmouseenter={() => handleItemMouseEnter(index)} | |
| 89 | 89 | onclick={() => handleItemClick(index)} | |
| 90 | + | onkeydown={(e) => e.key === 'Enter' && handleItemClick(index)} | |
| 90 | 91 | > | |
| 91 | 92 | <!-- Match context --> | |
| 92 | 93 | <div | |
| @@ -87,6 +87,7 @@ | |||||
| 87 | aria-selected={isSelected} | 87 | aria-selected={isSelected} | ||
| 88 | onmouseenter={() => handleItemMouseEnter(index)} | 88 | onmouseenter={() => handleItemMouseEnter(index)} | ||
| 89 | onclick={() => handleItemClick(index)} | 89 | onclick={() => handleItemClick(index)} | ||
| 90 | + | onkeydown={(e) => e.key === 'Enter' && handleItemClick(index)} | |||
| 90 | > | 91 | > | ||
| 91 | <!-- Match context --> | 92 | <!-- Match context --> | ||
| 92 | <div | 93 | <div | ||
M gui/src/injected/gui.ts +12 -8
| @@ -9,6 +9,7 @@ | |||
| 9 | 9 | import { initializeToast, type ToastConfig } from './modules/toast'; | |
| 10 | 10 | import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox'; | |
| 11 | 11 | import { keyboardService, type KeyboardService } from '../lib/keyboard'; | |
| 12 | + | // Note: color-scheme module is loaded separately at document-start by WebKit | |
| 12 | 13 | ||
| 13 | 14 | // Global interface for the unified GUI system | |
| 14 | 15 | interface DumberGUI { | |
| @@ -177,10 +178,10 @@ if (!window.__dumber_gui_ready) { | |||
| 177 | 178 | if (window.__dumber_omnibox?.toggle) { | |
| 178 | 179 | console.log('✅ Using Svelte omnibox toggle'); | |
| 179 | 180 | window.__dumber_omnibox.toggle(); | |
| 180 | - | } else { | |
| 181 | - | console.log('⚠️ Falling back to keyboard service'); | |
| 182 | - | keyboardService.handleNativeShortcut('cmdorctrl+l'); | |
| 181 | + | return; | |
| 183 | 182 | } | |
| 183 | + | ||
| 184 | + | throw new Error('Omnibox toggle requested but API is unavailable'); | |
| 184 | 185 | } catch (error) { | |
| 185 | 186 | console.error('❌ Error in __dumber_toggle:', error); | |
| 186 | 187 | } | |
| @@ -190,9 +191,10 @@ if (!window.__dumber_gui_ready) { | |||
| 190 | 191 | try { | |
| 191 | 192 | if (window.__dumber_omnibox?.open) { | |
| 192 | 193 | window.__dumber_omnibox.open('find', query); | |
| 193 | - | } else { | |
| 194 | - | keyboardService.handleNativeShortcut('cmdorctrl+f'); | |
| 194 | + | return; | |
| 195 | 195 | } | |
| 196 | + | ||
| 197 | + | throw new Error('Omnibox find requested but API is unavailable'); | |
| 196 | 198 | } catch (error) { | |
| 197 | 199 | console.error('❌ Error in __dumber_find_open:', error); | |
| 198 | 200 | } | |
| @@ -201,9 +203,11 @@ if (!window.__dumber_gui_ready) { | |||
| 201 | 203 | window.__dumber_find_close = () => { | |
| 202 | 204 | if (window.__dumber_omnibox?.close) { | |
| 203 | 205 | window.__dumber_omnibox.close(); | |
| 204 | - | } else { | |
| 205 | - | keyboardService.handleNativeShortcut('escape'); | |
| 206 | + | return; | |
| 206 | 207 | } | |
| 208 | + | ||
| 209 | + | const error = new Error('Omnibox close requested but API is unavailable'); | |
| 210 | + | console.error('❌ Error in __dumber_find_close:', error); | |
| 207 | 211 | }; | |
| 208 | 212 | ||
| 209 | 213 | // Find query function that Go uses to set find query | |
| @@ -229,4 +233,4 @@ if (!window.__dumber_gui_ready) { | |||
| 229 | 233 | }; | |
| 230 | 234 | ||
| 231 | 235 | } | |
| 232 | - | }⏎̸ | |
| 236 | + | } | |
| @@ -9,6 +9,7 @@ | |||||
| 9 | import { initializeToast, type ToastConfig } from './modules/toast'; | 9 | import { initializeToast, type ToastConfig } from './modules/toast'; | ||
| 10 | import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox'; | 10 | import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox'; | ||
| 11 | import { keyboardService, type KeyboardService } from '../lib/keyboard'; | 11 | import { keyboardService, type KeyboardService } from '../lib/keyboard'; | ||
| 12 | + | // Note: color-scheme module is loaded separately at document-start by WebKit | |||
| 12 | 13 | ||||
| 13 | // Global interface for the unified GUI system | 14 | // Global interface for the unified GUI system | ||
| 14 | interface DumberGUI { | 15 | interface DumberGUI { | ||
| @@ -177,10 +178,10 @@ if (!window.__dumber_gui_ready) { | |||||
| 177 | if (window.__dumber_omnibox?.toggle) { | 178 | if (window.__dumber_omnibox?.toggle) { | ||
| 178 | console.log('✅ Using Svelte omnibox toggle'); | 179 | console.log('✅ Using Svelte omnibox toggle'); | ||
| 179 | window.__dumber_omnibox.toggle(); | 180 | window.__dumber_omnibox.toggle(); | ||
| 180 | - | } else { | 181 | + | return; |
| 181 | - | console.log('⚠️ Falling back to keyboard service'); | |||
| 182 | - | keyboardService.handleNativeShortcut('cmdorctrl+l'); | |||
| 183 | } | 182 | } | ||
| 183 | + | ||||
| 184 | + | throw new Error('Omnibox toggle requested but API is unavailable'); | |||
| 184 | } catch (error) { | 185 | } catch (error) { | ||
| 185 | console.error('❌ Error in __dumber_toggle:', error); | 186 | console.error('❌ Error in __dumber_toggle:', error); | ||
| 186 | } | 187 | } | ||
| @@ -190,9 +191,10 @@ if (!window.__dumber_gui_ready) { | |||||
| 190 | try { | 191 | try { | ||
| 191 | if (window.__dumber_omnibox?.open) { | 192 | if (window.__dumber_omnibox?.open) { | ||
| 192 | window.__dumber_omnibox.open('find', query); | 193 | window.__dumber_omnibox.open('find', query); | ||
| 193 | - | } else { | 194 | + | return; |
| 194 | - | keyboardService.handleNativeShortcut('cmdorctrl+f'); | |||
| 195 | } | 195 | } | ||
| 196 | + | ||||
| 197 | + | throw new Error('Omnibox find requested but API is unavailable'); | |||
| 196 | } catch (error) { | 198 | } catch (error) { | ||
| 197 | console.error('❌ Error in __dumber_find_open:', error); | 199 | console.error('❌ Error in __dumber_find_open:', error); | ||
| 198 | } | 200 | } | ||
| @@ -201,9 +203,11 @@ if (!window.__dumber_gui_ready) { | |||||
| 201 | window.__dumber_find_close = () => { | 203 | window.__dumber_find_close = () => { | ||
| 202 | if (window.__dumber_omnibox?.close) { | 204 | if (window.__dumber_omnibox?.close) { | ||
| 203 | window.__dumber_omnibox.close(); | 205 | window.__dumber_omnibox.close(); | ||
| 204 | - | } else { | 206 | + | return; |
| 205 | - | keyboardService.handleNativeShortcut('escape'); | |||
| 206 | } | 207 | } | ||
| 208 | + | ||||
| 209 | + | const error = new Error('Omnibox close requested but API is unavailable'); | |||
| 210 | + | console.error('❌ Error in __dumber_find_close:', error); | |||
| 207 | }; | 211 | }; | ||
| 208 | 212 | ||||
| 209 | // Find query function that Go uses to set find query | 213 | // Find query function that Go uses to set find query | ||
| @@ -229,4 +233,4 @@ if (!window.__dumber_gui_ready) { | |||||
| 229 | }; | 233 | }; | ||
| 230 | 234 | ||||
| 231 | } | 235 | } | ||
| 232 | - | }⏎̸ | 236 | + | } |
M gui/src/lib/keyboard/KeyboardService.ts +7 -11
| @@ -385,12 +385,10 @@ export const keyboardService = new KeyboardService({ | |||
| 385 | 385 | if (window.__dumber_omnibox?.toggle) { | |
| 386 | 386 | console.log('🎯 Using omnibox API directly'); | |
| 387 | 387 | window.__dumber_omnibox.toggle(); | |
| 388 | - | } else if (typeof window.__dumber_toggle === 'function') { | |
| 389 | - | console.log('🎯 Using legacy toggle function'); | |
| 390 | - | window.__dumber_toggle(); | |
| 391 | - | } else { | |
| 392 | - | console.warn('⚠️ No omnibox toggle function available'); | |
| 388 | + | return; | |
| 393 | 389 | } | |
| 390 | + | ||
| 391 | + | console.error('❌ Omnibox toggle requested but API is unavailable'); | |
| 394 | 392 | } catch (error) { | |
| 395 | 393 | console.error('❌ Error in omnibox open handler:', error); | |
| 396 | 394 | } | |
| @@ -404,12 +402,10 @@ export const keyboardService = new KeyboardService({ | |||
| 404 | 402 | if (window.__dumber_omnibox?.open) { | |
| 405 | 403 | console.log('🔍 Using omnibox find API directly'); | |
| 406 | 404 | window.__dumber_omnibox.open('find'); | |
| 407 | - | } else if (typeof window.__dumber_find_open === 'function') { | |
| 408 | - | console.log('🔍 Using legacy find function'); | |
| 409 | - | window.__dumber_find_open(); | |
| 410 | - | } else { | |
| 411 | - | console.warn('⚠️ No find function available'); | |
| 405 | + | return; | |
| 412 | 406 | } | |
| 407 | + | ||
| 408 | + | console.error('❌ Omnibox find requested but API is unavailable'); | |
| 413 | 409 | } catch (error) { | |
| 414 | 410 | console.error('❌ Error in find open handler:', error); | |
| 415 | 411 | } | |
| @@ -444,4 +440,4 @@ export const keyboardService = new KeyboardService({ | |||
| 444 | 440 | ||
| 445 | 441 | // Export types for use in components | |
| 446 | 442 | export type { ShortcutConfig, ComponentShortcuts, KeyboardServiceConfig } from './types'; | |
| 447 | - | export { COMMON_SHORTCUTS } from './types';⏎̸ | |
| 443 | + | export { COMMON_SHORTCUTS } from './types'; | |
| @@ -385,12 +385,10 @@ export const keyboardService = new KeyboardService({ | |||||
| 385 | if (window.__dumber_omnibox?.toggle) { | 385 | if (window.__dumber_omnibox?.toggle) { | ||
| 386 | console.log('🎯 Using omnibox API directly'); | 386 | console.log('🎯 Using omnibox API directly'); | ||
| 387 | window.__dumber_omnibox.toggle(); | 387 | window.__dumber_omnibox.toggle(); | ||
| 388 | - | } else if (typeof window.__dumber_toggle === 'function') { | 388 | + | return; |
| 389 | - | console.log('🎯 Using legacy toggle function'); | |||
| 390 | - | window.__dumber_toggle(); | |||
| 391 | - | } else { | |||
| 392 | - | console.warn('⚠️ No omnibox toggle function available'); | |||
| 393 | } | 389 | } | ||
| 390 | + | ||||
| 391 | + | console.error('❌ Omnibox toggle requested but API is unavailable'); | |||
| 394 | } catch (error) { | 392 | } catch (error) { | ||
| 395 | console.error('❌ Error in omnibox open handler:', error); | 393 | console.error('❌ Error in omnibox open handler:', error); | ||
| 396 | } | 394 | } | ||
| @@ -404,12 +402,10 @@ export const keyboardService = new KeyboardService({ | |||||
| 404 | if (window.__dumber_omnibox?.open) { | 402 | if (window.__dumber_omnibox?.open) { | ||
| 405 | console.log('🔍 Using omnibox find API directly'); | 403 | console.log('🔍 Using omnibox find API directly'); | ||
| 406 | window.__dumber_omnibox.open('find'); | 404 | window.__dumber_omnibox.open('find'); | ||
| 407 | - | } else if (typeof window.__dumber_find_open === 'function') { | 405 | + | return; |
| 408 | - | console.log('🔍 Using legacy find function'); | |||
| 409 | - | window.__dumber_find_open(); | |||
| 410 | - | } else { | |||
| 411 | - | console.warn('⚠️ No find function available'); | |||
| 412 | } | 406 | } | ||
| 407 | + | ||||
| 408 | + | console.error('❌ Omnibox find requested but API is unavailable'); | |||
| 413 | } catch (error) { | 409 | } catch (error) { | ||
| 414 | console.error('❌ Error in find open handler:', error); | 410 | console.error('❌ Error in find open handler:', error); | ||
| 415 | } | 411 | } | ||
| @@ -444,4 +440,4 @@ export const keyboardService = new KeyboardService({ | |||||
| 444 | 440 | ||||
| 445 | // Export types for use in components | 441 | // Export types for use in components | ||
| 446 | export type { ShortcutConfig, ComponentShortcuts, KeyboardServiceConfig } from './types'; | 442 | export type { ShortcutConfig, ComponentShortcuts, KeyboardServiceConfig } from './types'; | ||
| 447 | - | export { COMMON_SHORTCUTS } from './types';⏎̸ | 443 | + | export { COMMON_SHORTCUTS } from './types'; |
M pkg/webkit/keyboard_cgo.go +41 -23
| @@ -96,6 +96,25 @@ var ( | |||
| 96 | 96 | lastKeyTime = make(map[uintptr]map[string]time.Time) | |
| 97 | 97 | ) | |
| 98 | 98 | ||
| 99 | + | const shortcutDebounceWindow = 120 * time.Millisecond | |
| 100 | + | ||
| 101 | + | // shouldDebounceShortcut returns true when the given key should be skipped | |
| 102 | + | // because it was delivered within the debounce window for the same view. | |
| 103 | + | func shouldDebounceShortcut(uid uintptr, key string, now time.Time) bool { | |
| 104 | + | regMu.Lock() | |
| 105 | + | defer regMu.Unlock() | |
| 106 | + | ||
| 107 | + | if _, ok := lastKeyTime[uid]; !ok { | |
| 108 | + | lastKeyTime[uid] = make(map[string]time.Time) | |
| 109 | + | } | |
| 110 | + | last := lastKeyTime[uid][key] | |
| 111 | + | if now.Sub(last) < shortcutDebounceWindow { | |
| 112 | + | return true | |
| 113 | + | } | |
| 114 | + | lastKeyTime[uid][key] = now | |
| 115 | + | return false | |
| 116 | + | } | |
| 117 | + | ||
| 99 | 118 | // Locale hint function removed; no layout-specific remaps. | |
| 100 | 119 | ||
| 101 | 120 | // RegisterKeyboardShortcut registers a callback under an accelerator string. | |
| @@ -192,8 +211,9 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||
| 192 | 211 | normalized = "" | |
| 193 | 212 | } | |
| 194 | 213 | ||
| 214 | + | now := time.Now() | |
| 195 | 215 | // Forward normalized shortcut to GUI KeyboardService via DOM event if present | |
| 196 | - | if normalized != "" { | |
| 216 | + | if normalized != "" && !shouldDebounceShortcut(uid, "dom:"+normalized, now) { | |
| 197 | 217 | regMu.RLock() | |
| 198 | 218 | vw := viewByID[uid] | |
| 199 | 219 | regMu.RUnlock() | |
| @@ -210,32 +230,11 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||
| 210 | 230 | } | |
| 211 | 231 | for _, name := range candidates { | |
| 212 | 232 | if cb, ok := reg[name]; ok { | |
| 213 | - | // Debounce duplicates within 120ms for the same view+key | |
| 214 | - | regMu.Lock() | |
| 215 | - | if _, ok := lastKeyTime[uid]; !ok { | |
| 216 | - | lastKeyTime[uid] = make(map[string]time.Time) | |
| 217 | - | } | |
| 218 | - | last := lastKeyTime[uid][name] | |
| 219 | - | now := time.Now() | |
| 220 | - | if now.Sub(last) < 120*time.Millisecond { | |
| 221 | - | // Skip duplicate | |
| 222 | - | regMu.Unlock() | |
| 233 | + | if shouldDebounceShortcut(uid, "shortcut:"+name, now) { | |
| 223 | 234 | return | |
| 224 | 235 | } | |
| 225 | - | lastKeyTime[uid][name] = now | |
| 226 | - | regMu.Unlock() | |
| 227 | 236 | log.Printf("[accelerator] %s", name) | |
| 228 | 237 | cb() | |
| 229 | - | // Additionally forward well-known GUI shortcuts to the in-page GUI via a DOM event | |
| 230 | - | // so the isolated user content world KeyboardService can handle them uniformly. | |
| 231 | - | if name == "cmdorctrl+l" || name == "cmdorctrl+f" || name == "cmdorctrl+shift+c" { | |
| 232 | - | regMu.RLock() | |
| 233 | - | vw := viewByID[uid] | |
| 234 | - | regMu.RUnlock() | |
| 235 | - | if vw != nil { | |
| 236 | - | _ = vw.InjectScript(fmt.Sprintf("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));", name)) | |
| 237 | - | } | |
| 238 | - | } | |
| 239 | 238 | break | |
| 240 | 239 | } | |
| 241 | 240 | } | |
| @@ -337,6 +336,25 @@ m.setAttribute('content', d ? 'dark light' : 'light dark'); | |||
| 337 | 336 | let s=document.getElementById('__dumber_theme_style'); | |
| 338 | 337 | if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); } | |
| 339 | 338 | s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}'; | |
| 339 | + | // Call the unified theme setter for Tailwind dark mode | |
| 340 | + | if (window.__dumber_setTheme) { | |
| 341 | + | window.__dumber_setTheme(cs); | |
| 342 | + | } else { | |
| 343 | + | // Retry after a short delay in case bridge hasn't loaded yet | |
| 344 | + | setTimeout(() => { | |
| 345 | + | if (window.__dumber_setTheme) { | |
| 346 | + | window.__dumber_setTheme(cs); | |
| 347 | + | } else { | |
| 348 | + | // Fallback: directly apply dark class | |
| 349 | + | console.warn('[dumber] Theme setter not available, using fallback'); | |
| 350 | + | if (d) { | |
| 351 | + | document.documentElement.classList.add('dark'); | |
| 352 | + | } else { | |
| 353 | + | document.documentElement.classList.remove('dark'); | |
| 354 | + | } | |
| 355 | + | } | |
| 356 | + | }, 100); | |
| 357 | + | } | |
| 340 | 358 | } catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval) | |
| 341 | 359 | _ = vw.InjectScript(js) | |
| 342 | 360 | } | |
| @@ -96,6 +96,25 @@ var ( | |||||
| 96 | lastKeyTime = make(map[uintptr]map[string]time.Time) | 96 | lastKeyTime = make(map[uintptr]map[string]time.Time) | ||
| 97 | ) | 97 | ) | ||
| 98 | 98 | ||||
| 99 | + | const shortcutDebounceWindow = 120 * time.Millisecond | |||
| 100 | + | ||||
| 101 | + | // shouldDebounceShortcut returns true when the given key should be skipped | |||
| 102 | + | // because it was delivered within the debounce window for the same view. | |||
| 103 | + | func shouldDebounceShortcut(uid uintptr, key string, now time.Time) bool { | |||
| 104 | + | regMu.Lock() | |||
| 105 | + | defer regMu.Unlock() | |||
| 106 | + | ||||
| 107 | + | if _, ok := lastKeyTime[uid]; !ok { | |||
| 108 | + | lastKeyTime[uid] = make(map[string]time.Time) | |||
| 109 | + | } | |||
| 110 | + | last := lastKeyTime[uid][key] | |||
| 111 | + | if now.Sub(last) < shortcutDebounceWindow { | |||
| 112 | + | return true | |||
| 113 | + | } | |||
| 114 | + | lastKeyTime[uid][key] = now | |||
| 115 | + | return false | |||
| 116 | + | } | |||
| 117 | + | ||||
| 99 | // Locale hint function removed; no layout-specific remaps. | 118 | // Locale hint function removed; no layout-specific remaps. | ||
| 100 | 119 | ||||
| 101 | // RegisterKeyboardShortcut registers a callback under an accelerator string. | 120 | // RegisterKeyboardShortcut registers a callback under an accelerator string. | ||
| @@ -192,8 +211,9 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||||
| 192 | normalized = "" | 211 | normalized = "" | ||
| 193 | } | 212 | } | ||
| 194 | 213 | ||||
| 214 | + | now := time.Now() | |||
| 195 | // Forward normalized shortcut to GUI KeyboardService via DOM event if present | 215 | // Forward normalized shortcut to GUI KeyboardService via DOM event if present | ||
| 196 | - | if normalized != "" { | 216 | + | if normalized != "" && !shouldDebounceShortcut(uid, "dom:"+normalized, now) { |
| 197 | regMu.RLock() | 217 | regMu.RLock() | ||
| 198 | vw := viewByID[uid] | 218 | vw := viewByID[uid] | ||
| 199 | regMu.RUnlock() | 219 | regMu.RUnlock() | ||
| @@ -210,32 +230,11 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||||
| 210 | } | 230 | } | ||
| 211 | for _, name := range candidates { | 231 | for _, name := range candidates { | ||
| 212 | if cb, ok := reg[name]; ok { | 232 | if cb, ok := reg[name]; ok { | ||
| 213 | - | // Debounce duplicates within 120ms for the same view+key | 233 | + | if shouldDebounceShortcut(uid, "shortcut:"+name, now) { |
| 214 | - | regMu.Lock() | |||
| 215 | - | if _, ok := lastKeyTime[uid]; !ok { | |||
| 216 | - | lastKeyTime[uid] = make(map[string]time.Time) | |||
| 217 | - | } | |||
| 218 | - | last := lastKeyTime[uid][name] | |||
| 219 | - | now := time.Now() | |||
| 220 | - | if now.Sub(last) < 120*time.Millisecond { | |||
| 221 | - | // Skip duplicate | |||
| 222 | - | regMu.Unlock() | |||
| 223 | return | 234 | return | ||
| 224 | } | 235 | } | ||
| 225 | - | lastKeyTime[uid][name] = now | |||
| 226 | - | regMu.Unlock() | |||
| 227 | log.Printf("[accelerator] %s", name) | 236 | log.Printf("[accelerator] %s", name) | ||
| 228 | cb() | 237 | cb() | ||
| 229 | - | // Additionally forward well-known GUI shortcuts to the in-page GUI via a DOM event | |||
| 230 | - | // so the isolated user content world KeyboardService can handle them uniformly. | |||
| 231 | - | if name == "cmdorctrl+l" || name == "cmdorctrl+f" || name == "cmdorctrl+shift+c" { | |||
| 232 | - | regMu.RLock() | |||
| 233 | - | vw := viewByID[uid] | |||
| 234 | - | regMu.RUnlock() | |||
| 235 | - | if vw != nil { | |||
| 236 | - | _ = vw.InjectScript(fmt.Sprintf("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));", name)) | |||
| 237 | - | } | |||
| 238 | - | } | |||
| 239 | break | 238 | break | ||
| 240 | } | 239 | } | ||
| 241 | } | 240 | } | ||
| @@ -337,6 +336,25 @@ m.setAttribute('content', d ? 'dark light' : 'light dark'); | |||||
| 337 | let s=document.getElementById('__dumber_theme_style'); | 336 | let s=document.getElementById('__dumber_theme_style'); | ||
| 338 | if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); } | 337 | if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); } | ||
| 339 | s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}'; | 338 | s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}'; | ||
| 339 | + | // Call the unified theme setter for Tailwind dark mode | |||
| 340 | + | if (window.__dumber_setTheme) { | |||
| 341 | + | window.__dumber_setTheme(cs); | |||
| 342 | + | } else { | |||
| 343 | + | // Retry after a short delay in case bridge hasn't loaded yet | |||
| 344 | + | setTimeout(() => { | |||
| 345 | + | if (window.__dumber_setTheme) { | |||
| 346 | + | window.__dumber_setTheme(cs); | |||
| 347 | + | } else { | |||
| 348 | + | // Fallback: directly apply dark class | |||
| 349 | + | console.warn('[dumber] Theme setter not available, using fallback'); | |||
| 350 | + | if (d) { | |||
| 351 | + | document.documentElement.classList.add('dark'); | |||
| 352 | + | } else { | |||
| 353 | + | document.documentElement.classList.remove('dark'); | |||
| 354 | + | } | |||
| 355 | + | } | |||
| 356 | + | }, 100); | |||
| 357 | + | } | |||
| 340 | } catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval) | 358 | } catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval) | ||
| 341 | _ = vw.InjectScript(js) | 359 | _ = vw.InjectScript(js) | ||
| 342 | } | 360 | } | ||