/** * Theme Store - Manages app theme (dark/light mode and accent colors) */ import { writable, derived } from 'svelte/store'; import { browser } from '$app/environment'; export type ThemeMode = 'dark' | 'light'; export interface ThemeColors { primary: string; name: string; } export const PRESET_COLORS: ThemeColors[] = [ { name: 'Cyan', primary: '#00A3E0' }, { name: 'Purple', primary: '#8B5CF6' }, { name: 'Pink', primary: '#EC4899' }, { name: 'Green', primary: '#10B981' }, { name: 'Orange', primary: '#F97316' }, { name: 'Red', primary: '#EF4444' }, ]; const THEME_STORAGE_KEY = 'app_theme'; interface ThemeState { mode: ThemeMode; primaryColor: string; } const defaultTheme: ThemeState = { mode: 'dark', primaryColor: '#00A3E0', }; // Convert hex to HSL function hexToHSL(hex: string): { h: number; s: number; l: number } { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (!result) return { h: 0, s: 0, l: 0 }; let r = parseInt(result[1], 16) / 255; let g = parseInt(result[2], 16) / 255; let b = parseInt(result[3], 16) / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h = 0; let s = 0; const l = (max + min) / 2; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; case g: h = ((b - r) / d + 2) / 6; break; case b: h = ((r - g) / d + 4) / 6; break; } } return { h: h * 360, s: s * 100, l: l * 100 }; } // Convert HSL to hex function hslToHex(h: number, s: number, l: number): string { s /= 100; l /= 100; const a = s * Math.min(l, 1 - l); const f = (n: number) => { const k = (n + h / 30) % 12; const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); return Math.round(255 * color).toString(16).padStart(2, '0'); }; return `#${f(0)}${f(8)}${f(4)}`; } // Generate derived colors from primary function generateDerivedColors(primary: string, mode: ThemeMode) { const { h, s } = hexToHSL(primary); if (mode === 'dark') { return { night: hslToHex(h, Math.min(s, 40), 6), // 6% lightness - panels dark: hslToHex(h, Math.min(s, 35), 10), // 10% lightness - elevated panels background: hslToHex(h, Math.min(s, 30), 3), // 3% lightness - page background light: '#e5e6f0', // Light color for text/icons text: '#ffffff', // White text textMuted: 'rgba(229, 230, 240, 0.5)', }; } else { // Light mode: use lower saturation to avoid too colorful backgrounds const lightSat = Math.min(s, 30); return { night: hslToHex(h, lightSat, 92), dark: hslToHex(h, lightSat, 85), background: hslToHex(h, lightSat, 98), light: '#1a1a2e', text: '#0a121f', textMuted: 'rgba(10, 18, 31, 0.6)', }; } } function loadTheme(): ThemeState { if (!browser) return defaultTheme; try { const stored = localStorage.getItem(THEME_STORAGE_KEY); if (stored) { return { ...defaultTheme, ...JSON.parse(stored) }; } } catch (e) { // Theme load failure is non-critical, fall through to default } return defaultTheme; } function saveTheme(theme: ThemeState): void { if (!browser) return; localStorage.setItem(THEME_STORAGE_KEY, JSON.stringify(theme)); } function createThemeStore() { const { subscribe, set, update } = writable(loadTheme()); return { subscribe, setMode: (mode: ThemeMode) => { update(state => { const newState = { ...state, mode }; saveTheme(newState); applyTheme(newState); return newState; }); }, setPrimaryColor: (color: string) => { update(state => { const newState = { ...state, primaryColor: color }; saveTheme(newState); applyTheme(newState); return newState; }); }, toggleMode: () => { update(state => { const newMode: ThemeMode = state.mode === 'dark' ? 'light' : 'dark'; const newState: ThemeState = { ...state, mode: newMode }; saveTheme(newState); applyTheme(newState); return newState; }); }, reset: () => { set(defaultTheme); saveTheme(defaultTheme); applyTheme(defaultTheme); }, }; } export const theme = createThemeStore(); // Derived stores for convenience export const isDarkMode = derived(theme, $t => $t.mode === 'dark'); export const primaryColor = derived(theme, $t => $t.primaryColor); // Apply theme to document export function applyTheme(state: ThemeState): void { if (!browser) return; const root = document.documentElement; // Set mode class root.classList.remove('dark', 'light'); root.classList.add(state.mode); // Set CSS custom property for primary color root.style.setProperty('--color-primary', state.primaryColor); // Calculate hover variant const { h, s, l } = hexToHSL(state.primaryColor); root.style.setProperty('--color-primary-hover', hslToHex(h, s, Math.min(100, l + 10))); // Generate and apply derived colors const derived = generateDerivedColors(state.primaryColor, state.mode); root.style.setProperty('--color-night', derived.night); root.style.setProperty('--color-dark', derived.dark); root.style.setProperty('--color-background', derived.background); root.style.setProperty('--color-light', derived.light); root.style.setProperty('--color-text', derived.text); root.style.setProperty('--color-text-muted', derived.textMuted); } // Initialize theme on load if (browser) { applyTheme(loadTheme()); }