feat(mvp): phase 7 - UI polish & ambient backgrounds
Add layout system (sidebar, header, main layout), dark/light/system theme with HSL customization, 3 ambient backgrounds (mesh gradient, particle field, aurora), Cmd/Ctrl+K search dialog, page transitions, card hover effects, status pulse animations, skeleton loaders, and responsive design. Polish all existing pages with consistent theming.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
const THEME_STORAGE_KEY = 'wal-theme-mode';
|
||||
const PRIMARY_HUE_KEY = 'wal-primary-hue';
|
||||
const PRIMARY_SAT_KEY = 'wal-primary-sat';
|
||||
const BG_TYPE_KEY = 'wal-bg-type';
|
||||
|
||||
export type ThemeMode = 'dark' | 'light' | 'system';
|
||||
export type BackgroundType = 'mesh' | 'particles' | 'aurora' | 'none';
|
||||
|
||||
function getStoredValue<T>(key: string, fallback: T): T {
|
||||
if (typeof window === 'undefined') return fallback;
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored === null) return fallback;
|
||||
return stored as unknown as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function getStoredNumber(key: string, fallback: number): number {
|
||||
if (typeof window === 'undefined') return fallback;
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored === null) return fallback;
|
||||
const parsed = Number(stored);
|
||||
return Number.isNaN(parsed) ? fallback : parsed;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
class ThemeStore {
|
||||
mode = $state<ThemeMode>('system');
|
||||
primaryHue = $state(220);
|
||||
primarySaturation = $state(70);
|
||||
backgroundType = $state<BackgroundType>('mesh');
|
||||
|
||||
resolvedMode = $derived<'dark' | 'light'>(
|
||||
this.mode === 'system' ? this.#systemPreference : this.mode
|
||||
);
|
||||
|
||||
isDark = $derived(this.resolvedMode === 'dark');
|
||||
|
||||
#systemPreference: 'dark' | 'light' = 'dark';
|
||||
|
||||
constructor() {
|
||||
if (typeof window !== 'undefined') {
|
||||
this.mode = getStoredValue<ThemeMode>(THEME_STORAGE_KEY, 'system');
|
||||
this.primaryHue = getStoredNumber(PRIMARY_HUE_KEY, 220);
|
||||
this.primarySaturation = getStoredNumber(PRIMARY_SAT_KEY, 70);
|
||||
this.backgroundType = getStoredValue<BackgroundType>(BG_TYPE_KEY, 'mesh');
|
||||
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
this.#systemPreference = mql.matches ? 'dark' : 'light';
|
||||
mql.addEventListener('change', (e) => {
|
||||
this.#systemPreference = e.matches ? 'dark' : 'light';
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(THEME_STORAGE_KEY, this.mode);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(PRIMARY_HUE_KEY, String(this.primaryHue));
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(PRIMARY_SAT_KEY, String(this.primarySaturation));
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(BG_TYPE_KEY, this.backgroundType);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
const html = document.documentElement;
|
||||
if (this.resolvedMode === 'dark') {
|
||||
html.classList.add('dark');
|
||||
html.classList.remove('light');
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
html.classList.add('light');
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
const html = document.documentElement;
|
||||
html.style.setProperty('--primary-h', String(this.primaryHue));
|
||||
html.style.setProperty('--primary-s', `${this.primarySaturation}%`);
|
||||
});
|
||||
}
|
||||
|
||||
cycleMode() {
|
||||
const modes: ThemeMode[] = ['light', 'dark', 'system'];
|
||||
const idx = modes.indexOf(this.mode);
|
||||
this.mode = modes[(idx + 1) % modes.length];
|
||||
}
|
||||
|
||||
setMode(mode: ThemeMode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
setBackground(bg: BackgroundType) {
|
||||
this.backgroundType = bg;
|
||||
}
|
||||
|
||||
setPrimaryColor(hue: number, saturation: number) {
|
||||
this.primaryHue = Math.max(0, Math.min(360, hue));
|
||||
this.primarySaturation = Math.max(0, Math.min(100, saturation));
|
||||
}
|
||||
}
|
||||
|
||||
export const theme = new ThemeStore();
|
||||
Reference in New Issue
Block a user