113 lines
2.7 KiB
TypeScript
113 lines
2.7 KiB
TypeScript
|
|
/**
|
||
|
|
* Theme types
|
||
|
|
*/
|
||
|
|
export type Theme = 'light' | 'dark' | 'system';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Theme service for managing light/dark mode
|
||
|
|
* Singleton pattern with subscription support
|
||
|
|
*/
|
||
|
|
class ThemeService {
|
||
|
|
private static instance: ThemeService;
|
||
|
|
private currentTheme: Theme = 'system';
|
||
|
|
private listeners: Set<(theme: Theme, isDark: boolean) => void> = new Set();
|
||
|
|
private mediaQuery: MediaQueryList;
|
||
|
|
private storageKey = 'dees-theme';
|
||
|
|
|
||
|
|
private constructor() {
|
||
|
|
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||
|
|
this.loadTheme();
|
||
|
|
this.mediaQuery.addEventListener('change', () => this.applyTheme());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the singleton instance
|
||
|
|
*/
|
||
|
|
static getInstance(): ThemeService {
|
||
|
|
if (!ThemeService.instance) {
|
||
|
|
ThemeService.instance = new ThemeService();
|
||
|
|
}
|
||
|
|
return ThemeService.instance;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load theme from localStorage
|
||
|
|
*/
|
||
|
|
private loadTheme() {
|
||
|
|
const saved = localStorage.getItem(this.storageKey) as Theme;
|
||
|
|
this.currentTheme = saved || 'system';
|
||
|
|
this.applyTheme();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Apply the current theme to the document
|
||
|
|
*/
|
||
|
|
private applyTheme() {
|
||
|
|
const isDark = this.isDark();
|
||
|
|
if (isDark) {
|
||
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||
|
|
} else {
|
||
|
|
document.documentElement.removeAttribute('data-theme');
|
||
|
|
}
|
||
|
|
this.notifyListeners();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if dark mode is currently active
|
||
|
|
*/
|
||
|
|
isDark(): boolean {
|
||
|
|
if (this.currentTheme === 'dark') return true;
|
||
|
|
if (this.currentTheme === 'light') return false;
|
||
|
|
return this.mediaQuery.matches;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the current theme setting
|
||
|
|
*/
|
||
|
|
getTheme(): Theme {
|
||
|
|
return this.currentTheme;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set the theme
|
||
|
|
*/
|
||
|
|
setTheme(theme: Theme) {
|
||
|
|
this.currentTheme = theme;
|
||
|
|
localStorage.setItem(this.storageKey, theme);
|
||
|
|
this.applyTheme();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Toggle through themes: light -> dark -> system -> light
|
||
|
|
*/
|
||
|
|
toggleTheme() {
|
||
|
|
const themes: Theme[] = ['light', 'dark', 'system'];
|
||
|
|
const currentIndex = themes.indexOf(this.currentTheme);
|
||
|
|
const nextIndex = (currentIndex + 1) % themes.length;
|
||
|
|
this.setTheme(themes[nextIndex]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Subscribe to theme changes
|
||
|
|
* Returns unsubscribe function
|
||
|
|
*/
|
||
|
|
subscribe(callback: (theme: Theme, isDark: boolean) => void): () => void {
|
||
|
|
this.listeners.add(callback);
|
||
|
|
// Call immediately with current state
|
||
|
|
callback(this.currentTheme, this.isDark());
|
||
|
|
return () => this.listeners.delete(callback);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Notify all listeners of theme change
|
||
|
|
*/
|
||
|
|
private notifyListeners() {
|
||
|
|
this.listeners.forEach(callback => callback(this.currentTheme, this.isDark()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Singleton theme service instance
|
||
|
|
*/
|
||
|
|
export const themeService = ThemeService.getInstance();
|