blob: c4bccfe08d3a3f51d5025c5f1cf6fbc7d302cccb [file] [log] [blame]
bankseanae3724e2025-07-18 16:52:37 +00001export type ThemeMode = "light" | "dark" | "system";
2
3export class ThemeService {
4 private static instance: ThemeService;
5 private systemPrefersDark = false;
6 private systemMediaQuery: MediaQueryList;
7
8 private constructor() {
9 this.systemMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
10 this.systemPrefersDark = this.systemMediaQuery.matches;
11
12 // Listen for system theme changes
13 this.systemMediaQuery.addEventListener("change", (e) => {
14 this.systemPrefersDark = e.matches;
15 // If current theme is 'system', update the applied theme
16 if (this.getTheme() === "system") {
17 this.applyTheme();
18 }
19 });
20 }
21
22 static getInstance(): ThemeService {
23 if (!this.instance) {
24 this.instance = new ThemeService();
25 }
26 return this.instance;
27 }
28
29 /**
30 * Cycle through theme modes: light -> dark -> system -> light
31 */
32 toggleTheme(): void {
33 const currentTheme = this.getTheme();
34 let nextTheme: ThemeMode;
35
36 switch (currentTheme) {
37 case "light":
38 nextTheme = "dark";
39 break;
40 case "dark":
41 nextTheme = "system";
42 break;
43 case "system":
44 nextTheme = "light";
45 break;
46 default:
47 nextTheme = "light";
48 }
49
50 this.setTheme(nextTheme);
51 }
52
53 /**
54 * Set the theme mode
55 */
56 setTheme(theme: ThemeMode): void {
57 // Store the theme preference
58 if (theme === "system") {
59 localStorage.removeItem("theme");
60 } else {
61 localStorage.setItem("theme", theme);
62 }
63
64 // Apply the theme
65 this.applyTheme();
66
67 // Dispatch event for components that need to react
68 document.dispatchEvent(
69 new CustomEvent("theme-changed", {
70 detail: {
71 theme,
72 effectiveTheme: this.getEffectiveTheme(),
73 systemPrefersDark: this.systemPrefersDark,
74 },
75 }),
76 );
77 }
78
79 /**
80 * Get the current theme preference (light, dark, or system)
81 */
82 getTheme(): ThemeMode {
83 const saved = localStorage.getItem("theme");
84 if (saved === "light" || saved === "dark") {
85 return saved;
86 }
banksean9ee30422025-07-19 12:57:45 -070087 return "light"; // TODO: default to "system", once dark mode is ready.
bankseanae3724e2025-07-18 16:52:37 +000088 }
89
90 /**
91 * Get the effective theme (what is actually applied: light or dark)
92 */
93 getEffectiveTheme(): "light" | "dark" {
94 const theme = this.getTheme();
95 if (theme === "system") {
96 return this.systemPrefersDark ? "dark" : "light";
97 }
98 return theme;
99 }
100
101 /**
102 * Check if dark mode is currently active
103 */
104 isDarkMode(): boolean {
105 return this.getEffectiveTheme() === "dark";
106 }
107
108 /**
109 * Apply the current theme to the DOM
110 */
111 private applyTheme(): void {
112 const effectiveTheme = this.getEffectiveTheme();
113 document.documentElement.classList.toggle(
114 "dark",
115 effectiveTheme === "dark",
116 );
117 }
118
119 /**
120 * Initialize the theme system
121 */
122 initializeTheme(): void {
123 // Apply the initial theme
124 this.applyTheme();
125 }
126}