useTheme.ts
1 /** 2 * ACDC Theme Hook 3 * Context-aware theme management with user preferences 4 */ 5 6 import { useState, useEffect, useCallback } from 'react' 7 import { themeDefaults, type ThemeMode, type Chain } from '../tokens' 8 9 // Context defaults 10 const CONTEXT_DEFAULTS: Record<string, 'light' | 'dark'> = { 11 // Alpha-centric pages → Light 12 governance: 'light', 13 staking: 'light', 14 proposals: 'light', 15 validators: 'light', 16 17 // Delta-centric pages → Dark 18 trading: 'dark', 19 dex: 'dark', 20 orderbook: 'dark', 21 charts: 'dark', 22 pools: 'dark', 23 24 // Blended → Dark 25 dashboard: 'dark', 26 bridge: 'dark', 27 wallet: 'dark', 28 settings: 'dark', 29 } 30 31 interface ThemeSettings { 32 alphaDefault: ThemeMode 33 deltaDefault: ThemeMode 34 globalOverride: 'light' | 'dark' | 'none' 35 } 36 37 const DEFAULT_SETTINGS: ThemeSettings = { 38 alphaDefault: 'light', 39 deltaDefault: 'dark', 40 globalOverride: 'none', 41 } 42 43 export function useTheme(context?: string) { 44 const [theme, setThemeState] = useState<'light' | 'dark'>('dark') 45 const [settings, setSettings] = useState<ThemeSettings>(DEFAULT_SETTINGS) 46 47 // Load settings from localStorage 48 useEffect(() => { 49 const stored = localStorage.getItem('acdc-theme-settings') 50 if (stored) { 51 try { 52 setSettings(JSON.parse(stored)) 53 } catch { 54 // Ignore parse errors 55 } 56 } 57 }, []) 58 59 // Determine effective theme 60 useEffect(() => { 61 const getEffectiveTheme = (): 'light' | 'dark' => { 62 // Check global override first 63 if (settings.globalOverride !== 'none') { 64 return settings.globalOverride 65 } 66 67 // Check context-specific preference 68 if (context) { 69 const contextPref = localStorage.getItem(`acdc-theme-${context}`) 70 if (contextPref === 'light' || contextPref === 'dark') { 71 return contextPref 72 } 73 } 74 75 // Check chain-specific defaults 76 if (context) { 77 if ((themeDefaults.alpha as readonly string[]).includes(context)) { 78 return resolveThemeMode(settings.alphaDefault) 79 } 80 if ((themeDefaults.delta as readonly string[]).includes(context)) { 81 return resolveThemeMode(settings.deltaDefault) 82 } 83 } 84 85 // Fall back to context default or dark 86 return context ? (CONTEXT_DEFAULTS[context] || 'dark') : 'dark' 87 } 88 89 setThemeState(getEffectiveTheme()) 90 }, [context, settings]) 91 92 // Apply theme to document (both data-theme and class for CSS compatibility) 93 useEffect(() => { 94 document.documentElement.setAttribute('data-theme', theme) 95 // Also toggle .dark class for Tailwind dark: prefix to work 96 if (theme === 'dark') { 97 document.documentElement.classList.add('dark') 98 } else { 99 document.documentElement.classList.remove('dark') 100 } 101 }, [theme]) 102 103 const setTheme = useCallback((newTheme: 'light' | 'dark') => { 104 setThemeState(newTheme) 105 if (context) { 106 localStorage.setItem(`acdc-theme-${context}`, newTheme) 107 } 108 }, [context]) 109 110 const toggleTheme = useCallback(() => { 111 setTheme(theme === 'dark' ? 'light' : 'dark') 112 }, [theme, setTheme]) 113 114 const updateSettings = useCallback((newSettings: Partial<ThemeSettings>) => { 115 const updated = { ...settings, ...newSettings } 116 setSettings(updated) 117 localStorage.setItem('acdc-theme-settings', JSON.stringify(updated)) 118 }, [settings]) 119 120 return { 121 theme, 122 setTheme, 123 toggleTheme, 124 settings, 125 updateSettings, 126 isDark: theme === 'dark', 127 isLight: theme === 'light', 128 } 129 } 130 131 function resolveThemeMode(mode: ThemeMode): 'light' | 'dark' { 132 if (mode === 'system') { 133 return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' 134 } 135 return mode 136 } 137 138 export function useChain(chain: Chain = 'alpha') { 139 useEffect(() => { 140 document.documentElement.setAttribute('data-chain', chain) 141 }, [chain]) 142 143 return { chain } 144 }