/** * Theme Controller * * Handles dark/light mode switching and system preference detection */ export type Theme = 'light' | 'dark' | 'system'; export class ThemeController { private static readonly STORAGE_KEY = 'theme-preference'; private static readonly DARK_CLASS = 'dark-mode'; private static readonly LIGHT_CLASS = 'light-mode'; private root: HTMLElement; private themeOptions: NodeListOf; private themeCheckbox: HTMLInputElement | null; constructor() { this.root = document.documentElement; this.themeOptions = document.querySelectorAll('swp-theme-option'); this.themeCheckbox = document.getElementById('themeCheckbox') as HTMLInputElement | null; this.applyTheme(this.current); this.updateUI(); this.setupListeners(); } /** * Get the current theme setting */ get current(): Theme { const stored = localStorage.getItem(ThemeController.STORAGE_KEY) as Theme | null; if (stored === 'dark' || stored === 'light' || stored === 'system') { return stored; } return 'system'; } /** * Check if dark mode is currently active */ get isDark(): boolean { return this.root.classList.contains(ThemeController.DARK_CLASS) || (this.systemPrefersDark && !this.root.classList.contains(ThemeController.LIGHT_CLASS)); } /** * Check if system prefers dark mode */ get systemPrefersDark(): boolean { return window.matchMedia('(prefers-color-scheme: dark)').matches; } /** * Set theme and persist preference */ set(theme: Theme): void { localStorage.setItem(ThemeController.STORAGE_KEY, theme); this.applyTheme(theme); this.updateUI(); } /** * Toggle between light and dark themes */ toggle(): void { this.set(this.isDark ? 'light' : 'dark'); } private applyTheme(theme: Theme): void { this.root.classList.remove(ThemeController.DARK_CLASS, ThemeController.LIGHT_CLASS); if (theme === 'dark') { this.root.classList.add(ThemeController.DARK_CLASS); } else if (theme === 'light') { this.root.classList.add(ThemeController.LIGHT_CLASS); } // 'system' leaves both classes off, letting CSS media query handle it } private updateUI(): void { const darkActive = this.isDark; // Update theme options this.themeOptions?.forEach(option => { const theme = option.dataset.theme as Theme; const isActive = (theme === 'dark' && darkActive) || (theme === 'light' && !darkActive); option.classList.toggle('active', isActive); }); // Update checkbox (checked = dark mode) if (this.themeCheckbox) { this.themeCheckbox.checked = darkActive; } } private setupListeners(): void { // Theme option clicks this.themeOptions.forEach(option => { option.addEventListener('click', (e) => this.handleOptionClick(e)); }); // Theme checkbox toggle this.themeCheckbox?.addEventListener('change', () => { this.set(this.themeCheckbox!.checked ? 'dark' : 'light'); }); // System theme changes window.matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', () => this.handleSystemChange()); } private handleOptionClick(e: Event): void { const target = e.target as HTMLElement; const option = target.closest('swp-theme-option'); if (option) { const theme = option.dataset.theme as Theme; if (theme) { this.set(theme); } } } private handleSystemChange(): void { // Only react to system changes if we're using system preference if (this.current === 'system') { this.updateUI(); } } }