Refactors project structure to support modular, feature-driven development Introduces comprehensive language localization support Adds menu management with role-based access control Implements dynamic sidebar and theme switching capabilities Enhances project scalability and maintainability
120 lines
3.2 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
}
|