WIP
This commit is contained in:
parent
54b057886c
commit
7fc1ae0650
204 changed files with 4345 additions and 134 deletions
120
PlanTempus.Application/wwwroot/ts/modules/theme.ts
Normal file
120
PlanTempus.Application/wwwroot/ts/modules/theme.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue