Mega push vol 3
This commit is contained in:
222
src/lib/stores/theme.ts
Normal file
222
src/lib/stores/theme.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Theme Store - Manages app theme (dark/light mode and accent colors)
|
||||
* Inspired by root-v2
|
||||
*/
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export type ThemeMode = 'dark' | 'light' | 'system';
|
||||
|
||||
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' },
|
||||
{ name: 'Blue', primary: '#3B82F6' },
|
||||
{ name: 'Indigo', primary: '#6366F1' },
|
||||
];
|
||||
|
||||
const THEME_STORAGE_KEY = 'root_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 };
|
||||
|
||||
const r = parseInt(result[1], 16) / 255;
|
||||
const g = parseInt(result[2], 16) / 255;
|
||||
const 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, isDark: boolean) {
|
||||
const { h, s } = hexToHSL(primary);
|
||||
|
||||
if (isDark) {
|
||||
return {
|
||||
night: hslToHex(h, Math.min(s, 40), 6),
|
||||
dark: hslToHex(h, Math.min(s, 35), 10),
|
||||
surface: hslToHex(h, Math.min(s, 30), 12),
|
||||
background: hslToHex(h, Math.min(s, 30), 3),
|
||||
light: '#e5e6f0',
|
||||
text: '#ffffff',
|
||||
};
|
||||
} else {
|
||||
const lightSat = Math.min(s, 30);
|
||||
return {
|
||||
night: hslToHex(h, lightSat, 95),
|
||||
dark: hslToHex(h, lightSat, 90),
|
||||
surface: hslToHex(h, lightSat, 98),
|
||||
background: hslToHex(h, lightSat, 100),
|
||||
light: '#1a1a2e',
|
||||
text: '#0a121f',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getEffectiveMode(mode: ThemeMode): 'dark' | 'light' {
|
||||
if (mode === 'system') {
|
||||
if (!browser) return 'dark';
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
function loadTheme(): ThemeState {
|
||||
if (!browser) return defaultTheme;
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (stored) {
|
||||
return { ...defaultTheme, ...JSON.parse(stored) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load theme:', e);
|
||||
}
|
||||
return defaultTheme;
|
||||
}
|
||||
|
||||
function saveTheme(theme: ThemeState): void {
|
||||
if (!browser) return;
|
||||
localStorage.setItem(THEME_STORAGE_KEY, JSON.stringify(theme));
|
||||
}
|
||||
|
||||
export function applyTheme(state: ThemeState): void {
|
||||
if (!browser) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
const effectiveMode = getEffectiveMode(state.mode);
|
||||
|
||||
// Set mode class
|
||||
root.classList.remove('dark', 'light');
|
||||
root.classList.add(effectiveMode);
|
||||
|
||||
// Set CSS custom properties
|
||||
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, effectiveMode === 'dark');
|
||||
root.style.setProperty('--color-night', derived.night);
|
||||
root.style.setProperty('--color-dark', derived.dark);
|
||||
root.style.setProperty('--color-surface', derived.surface);
|
||||
root.style.setProperty('--color-background', derived.background);
|
||||
root.style.setProperty('--color-light', derived.light);
|
||||
root.style.setProperty('--color-text', derived.text);
|
||||
}
|
||||
|
||||
function createThemeStore() {
|
||||
const { subscribe, set, update } = writable<ThemeState>(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 modes: ThemeMode[] = ['dark', 'light', 'system'];
|
||||
const currentIndex = modes.indexOf(state.mode);
|
||||
const newMode = modes[(currentIndex + 1) % modes.length];
|
||||
const newState: ThemeState = { ...state, mode: newMode };
|
||||
saveTheme(newState);
|
||||
applyTheme(newState);
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
reset: () => {
|
||||
set(defaultTheme);
|
||||
saveTheme(defaultTheme);
|
||||
applyTheme(defaultTheme);
|
||||
},
|
||||
init: () => {
|
||||
const state = loadTheme();
|
||||
applyTheme(state);
|
||||
set(state);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const theme = createThemeStore();
|
||||
|
||||
// Derived stores for convenience
|
||||
export const isDarkMode = derived(theme, $t => getEffectiveMode($t.mode) === 'dark');
|
||||
export const primaryColor = derived(theme, $t => $t.primaryColor);
|
||||
export const themeMode = derived(theme, $t => $t.mode);
|
||||
|
||||
// Initialize theme on load
|
||||
if (browser) {
|
||||
applyTheme(loadTheme());
|
||||
|
||||
// Listen for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
const state = loadTheme();
|
||||
if (state.mode === 'system') {
|
||||
applyTheme(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user