PlanTempusApp/PlanTempus.Application/wwwroot/ts/modules/theme.ts
Janus C. H. Knudsen 545d6606a6 Refactors employee details and UI controls
Enhances employee hours view with dynamic weekly schedule rendering
Updates toggle slider and theme switch components with improved interactions
Adds more flexible notification and settings configurations for employees

Improves user experience by streamlining UI controls and schedule display
2026-01-15 16:59:56 +01:00

131 lines
3.6 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>;
private themeCheckbox: HTMLInputElement | null;
constructor() {
this.root = document.documentElement;
this.themeOptions = document.querySelectorAll<HTMLElement>('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<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();
}
}
}