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