learning_ai_invt_trdg/web/src/components/theme/ThemeProvider.tsx

87 lines
2.4 KiB
TypeScript

import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import type { ReactNode } from 'react';
type Theme = 'light' | 'dark';
type ThemeContextValue = {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
};
const STORAGE_KEY = 'trading-dashboard-theme-v2';
const ThemeContext = createContext<ThemeContextValue | null>(null);
function applyTheme(theme: Theme) {
const root = document.documentElement;
root.classList.toggle('dark', theme === 'dark');
root.dataset.theme = theme;
}
function readStoredTheme(): Theme | null {
if (typeof window === 'undefined') return null;
try {
const storage = window.localStorage;
if (typeof storage?.getItem !== 'function') return null;
const stored = storage.getItem(STORAGE_KEY);
return stored === 'light' || stored === 'dark' ? stored : null;
} catch {
return null;
}
}
function writeStoredTheme(theme: Theme) {
try {
const storage = window.localStorage;
if (typeof storage?.setItem === 'function') {
storage.setItem(STORAGE_KEY, theme);
}
} catch {
// Theme persistence is best-effort; restricted storage should not break UI.
}
}
function resolveInitialTheme(): Theme {
if (typeof window === 'undefined') return 'light';
const stored = readStoredTheme();
if (stored) return stored;
return 'light';
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => resolveInitialTheme());
useEffect(() => {
applyTheme(theme);
writeStoredTheme(theme);
}, [theme]);
useEffect(() => {
const media = window.matchMedia?.('(prefers-color-scheme: dark)');
if (!media) return;
const handleChange = () => {
if (readStoredTheme()) return;
setThemeState(media.matches ? 'dark' : 'light');
};
media.addEventListener?.('change', handleChange);
return () => media.removeEventListener?.('change', handleChange);
}, []);
const value = useMemo<ThemeContextValue>(() => ({
theme,
setTheme: setThemeState,
toggleTheme: () => setThemeState(prev => (prev === 'dark' ? 'light' : 'dark')),
}), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme must be used within ThemeProvider');
}
return ctx;
}