color-scheme.ts RawHistoryFolder
1/**
2 * Color Scheme Detection Module
3 *
4 * Handles GTK theme detection and applies appropriate color scheme preferences
5 * to web pages. This module is injected at document-start to ensure proper
6 * theme initialization before page content loads.
7 */
8
9declare global {
10 interface Window {
11 __dumber_gtk_prefers_dark?: boolean;
12 __dumber_setTheme?: (theme: string) => void;
13 }
14}
15
16interface ColorSchemeConfig {
17 isDark: boolean;
18 colorScheme: string;
19 metaContent: string;
20 rootStyle: string;
21}
22
23class ColorSchemeManager {
24 private config: ColorSchemeConfig;
25
26 constructor() {
27 // Use GTK preference if available, otherwise detect from system
28 const prefersDark = window.__dumber_gtk_prefers_dark ?? false;
29
30 this.config = {
31 isDark: prefersDark,
32 colorScheme: prefersDark ? 'dark' : 'light',
33 metaContent: prefersDark ? 'dark light' : 'light dark',
34 rootStyle: prefersDark ? ':root{color-scheme:dark;}' : ':root{color-scheme:light;}'
35 };
36
37 this.initialize();
38 }
39
40 private initialize(): void {
41 try {
42 console.log(`[dumber] color-scheme set: ${this.config.colorScheme}`);
43 console.log(`[dumber] GTK detected color mode: ${this.config.colorScheme}`);
44
45 // Notify native layer of theme preference
46 this.notifyNativeLayer();
47
48 // Inject color-scheme meta tag
49 this.injectColorSchemeMeta();
50
51 // Apply root styles
52 this.applyRootStyles();
53
54 // Override matchMedia for proper prefers-color-scheme support
55 this.overrideMatchMedia();
56
57 // Set up theme integration with existing UI components
58 this.setupThemeIntegration();
59
60 } catch (error) {
61 console.warn('[dumber] color-scheme injection failed', error);
62 }
63 }
64
65 private notifyNativeLayer(): void {
66 try {
67 (window as any).webkit?.messageHandlers?.dumber?.postMessage(
68 JSON.stringify({
69 type: 'theme',
70 value: this.config.colorScheme
71 })
72 );
73 } catch (error) {
74 // Ignore errors - native layer may not be available
75 }
76 }
77
78 private injectColorSchemeMeta(): void {
79 const meta = document.createElement('meta');
80 meta.name = 'color-scheme';
81 meta.content = this.config.metaContent;
82 document.documentElement.appendChild(meta);
83 }
84
85 private applyRootStyles(): void {
86 const style = document.createElement('style');
87 style.textContent = this.config.rootStyle;
88 document.documentElement.appendChild(style);
89 }
90
91 private overrideMatchMedia(): void {
92 const darkQuery = '(prefers-color-scheme: dark)';
93 const lightQuery = '(prefers-color-scheme: light)';
94 const originalMatchMedia = window.matchMedia;
95
96 window.matchMedia = (query: string): MediaQueryList => {
97 if (typeof query === 'string' && (query.includes(darkQuery) || query.includes(lightQuery))) {
98 // Create a mock MediaQueryList that reflects our GTK preference
99 const matches = query.includes('dark') ? this.config.isDark : !this.config.isDark;
100
101 return {
102 matches,
103 media: query,
104 onchange: null,
105 addListener: () => {},
106 removeListener: () => {},
107 addEventListener: () => {},
108 removeEventListener: () => {},
109 dispatchEvent: () => false
110 } as MediaQueryList;
111 }
112
113 // For non-color-scheme queries, use the original implementation
114 return originalMatchMedia.call(window, query);
115 };
116 }
117
118 private setupThemeIntegration(): void {
119 // Integrate with unified theme setter if available
120 if (window.__dumber_setTheme) {
121 window.__dumber_setTheme(this.config.colorScheme);
122 } else {
123 // Retry after a short delay in case the bridge hasn't loaded yet
124 setTimeout(() => {
125 if (window.__dumber_setTheme) {
126 window.__dumber_setTheme(this.config.colorScheme);
127 } else {
128 // Fallback: directly apply dark class for Tailwind compatibility
129 console.warn('[dumber] Theme setter not available, using fallback');
130 if (this.config.isDark) {
131 document.documentElement.classList.add('dark');
132 } else {
133 document.documentElement.classList.remove('dark');
134 }
135 }
136 }, 100);
137 }
138 }
139
140 /**
141 * Updates the color scheme preference at runtime
142 * @param isDark Whether to use dark mode
143 */
144 public updateColorScheme(isDark: boolean): void {
145 this.config.isDark = isDark;
146 this.config.colorScheme = isDark ? 'dark' : 'light';
147 this.config.metaContent = isDark ? 'dark light' : 'light dark';
148 this.config.rootStyle = isDark ? ':root{color-scheme:dark;}' : ':root{color-scheme:light;}';
149
150 // Re-apply all configurations
151 this.initialize();
152 }
153}
154
155// Initialize color scheme management
156const colorSchemeManager = new ColorSchemeManager();
157
158// Export for potential runtime updates
159(window as any).__dumber_color_scheme_manager = colorSchemeManager;
160
161export default colorSchemeManager;