/** * Theme store — manages dynamic CSS variables for theme customization. * Applies primary_color, accent_color, font_family, border_radius * from system settings to CSS custom properties on :root. */ import { create } from 'zustand'; export interface ThemeConfig { primaryColor: string; accentColor: string; fontFamily: string; borderRadius: string; darkMode: boolean; } interface ThemeState extends ThemeConfig { setTheme: (config: Partial) => void; toggleDarkMode: () => void; applyTheme: () => void; loadFromStorage: () => void; saveToStorage: () => void; } const DEFAULT_THEME: ThemeConfig = { primaryColor: '#2563eb', accentColor: '#d946ef', fontFamily: 'Inter', borderRadius: '0.5rem', darkMode: false, }; /** Convert hex color to RGB values for Tailwind CSS variable injection */ function hexToRgb(hex: string): { r: number; g: number; b: number } | null { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16), } : null; } /** Generate a Tailwind-style color scale (50-900) from a base hex color */ function generateColorScale(hex: string): Record { const rgb = hexToRgb(hex); if (!rgb) return {}; // Mix with white for lighter shades, with black for darker const mix = (base: number, amount: number, target: number) => Math.round(base + (target - base) * amount); const scales: Record = { 50: [0.95, 0], // 95% white 100: [0.90, 0], 200: [0.80, 0], 300: [0.60, 0], 400: [0.30, 0], 500: [0, 0], // base color 600: [0, 0.10], // 10% black 700: [0, 0.20], 800: [0, 0.30], 900: [0, 0.40], }; const result: Record = {}; for (const [scale, [whiteAmt, blackAmt]] of Object.entries(scales)) { const r = whiteAmt > 0 ? mix(rgb.r, whiteAmt, 255) : mix(rgb.r, blackAmt, 0); const g = whiteAmt > 0 ? mix(rgb.g, whiteAmt, 255) : mix(rgb.g, blackAmt, 0); const b = whiteAmt > 0 ? mix(rgb.b, whiteAmt, 255) : mix(rgb.b, blackAmt, 0); result[scale] = `rgb(${r} ${g} ${b})`; } result.DEFAULT = hex; return result; } function applyCSSVariables(config: ThemeConfig) { const root = document.documentElement; // Apply primary color scale const primaryScale = generateColorScale(config.primaryColor); for (const [key, val] of Object.entries(primaryScale)) { root.style.setProperty(`--color-primary-${key.toLowerCase()}`, val); } // Apply accent color scale const accentScale = generateColorScale(config.accentColor); for (const [key, val] of Object.entries(accentScale)) { root.style.setProperty(`--color-accent-${key.toLowerCase()}`, val); } // Apply font family root.style.setProperty('--font-sans', `'${config.fontFamily}', system-ui, sans-serif`); // Apply border radius root.style.setProperty('--radius-md', config.borderRadius); // Apply dark mode if (config.darkMode) { root.classList.add('dark'); } else { root.classList.remove('dark'); } } const STORAGE_KEY = 'leocrm-theme'; export const useThemeStore = create((set, get) => ({ ...DEFAULT_THEME, setTheme: (config) => { set((state) => ({ ...state, ...config })); get().applyTheme(); get().saveToStorage(); }, toggleDarkMode: () => { set((state) => ({ ...state, darkMode: !state.darkMode })); get().applyTheme(); get().saveToStorage(); }, applyTheme: () => { const state = get(); applyCSSVariables({ primaryColor: state.primaryColor, accentColor: state.accentColor, fontFamily: state.fontFamily, borderRadius: state.borderRadius, darkMode: state.darkMode, }); }, loadFromStorage: () => { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { const config = JSON.parse(stored) as Partial; set((state) => ({ ...state, ...config })); } } catch { // ignore parse errors } get().applyTheme(); }, saveToStorage: () => { const state = get(); const { setTheme, toggleDarkMode, applyTheme, loadFromStorage, saveToStorage, ...config } = state; localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); }, }));