PlanTempusApp/PlanTempus.Application/wwwroot/ts/modules/theme.ts
Janus C. H. Knudsen 7fc1ae0650 WIP
2026-01-10 20:39:17 +01:00

120 lines
3.2 KiB
TypeScript

/**
* 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>;
constructor() {
this.root = document.documentElement;
this.themeOptions = document.querySelectorAll<HTMLElement>('swp-theme-option');
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 {
if (!this.themeOptions) return;
const darkActive = this.isDark;
this.themeOptions.forEach(option => {
const theme = option.dataset.theme as Theme;
const isActive = (theme === 'dark' && darkActive) || (theme === 'light' && !darkActive);
option.classList.toggle('active', isActive);
});
}
private setupListeners(): void {
// Theme option clicks
this.themeOptions.forEach(option => {
option.addEventListener('click', (e) => this.handleOptionClick(e));
});
// 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();
}
}
}