2026-01-10 20:39:17 +01:00
|
|
|
/**
|
|
|
|
|
* 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<HTMLElement>;
|
2026-01-15 16:59:56 +01:00
|
|
|
private themeCheckbox: HTMLInputElement | null;
|
2026-01-10 20:39:17 +01:00
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this.root = document.documentElement;
|
|
|
|
|
this.themeOptions = document.querySelectorAll<HTMLElement>('swp-theme-option');
|
2026-01-15 16:59:56 +01:00
|
|
|
this.themeCheckbox = document.getElementById('themeCheckbox') as HTMLInputElement | null;
|
2026-01-10 20:39:17 +01:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-01-15 16:59:56 +01:00
|
|
|
// Update theme options
|
|
|
|
|
this.themeOptions?.forEach(option => {
|
2026-01-10 20:39:17 +01:00
|
|
|
const theme = option.dataset.theme as Theme;
|
|
|
|
|
const isActive = (theme === 'dark' && darkActive) || (theme === 'light' && !darkActive);
|
|
|
|
|
option.classList.toggle('active', isActive);
|
|
|
|
|
});
|
2026-01-15 16:59:56 +01:00
|
|
|
|
|
|
|
|
// Update checkbox (checked = dark mode)
|
|
|
|
|
if (this.themeCheckbox) {
|
|
|
|
|
this.themeCheckbox.checked = darkActive;
|
|
|
|
|
}
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupListeners(): void {
|
|
|
|
|
// Theme option clicks
|
|
|
|
|
this.themeOptions.forEach(option => {
|
|
|
|
|
option.addEventListener('click', (e) => this.handleOptionClick(e));
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-15 16:59:56 +01:00
|
|
|
// Theme checkbox toggle
|
|
|
|
|
this.themeCheckbox?.addEventListener('change', () => {
|
|
|
|
|
this.set(this.themeCheckbox!.checked ? 'dark' : 'light');
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-10 20:39:17 +01:00
|
|
|
// 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<HTMLElement>('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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|