| import { |
| createContext, |
| useCallback, |
| useContext, |
| useEffect, |
| useMemo, |
| useState, |
| type ReactNode, |
| } from "react"; |
|
|
| export type ThemeMode = "light" | "dark"; |
| const STORAGE_KEY = "signalmod.theme"; |
|
|
| type ThemeValue = { |
| theme: ThemeMode; |
| setTheme: (t: ThemeMode) => void; |
| toggleTheme: () => void; |
| }; |
|
|
| const ThemeContext = createContext<ThemeValue | null>(null); |
|
|
| function detectInitialTheme(): ThemeMode { |
| if (typeof window === "undefined") return "light"; |
| const stored = window.localStorage.getItem(STORAGE_KEY); |
| if (stored === "light" || stored === "dark") return stored; |
| const prefersDark = |
| window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; |
| return prefersDark ? "dark" : "light"; |
| } |
|
|
| export function ThemeProvider({ children }: { children: ReactNode }) { |
| const [theme, setThemeState] = useState<ThemeMode>(() => detectInitialTheme()); |
|
|
| useEffect(() => { |
| if (typeof document !== "undefined") { |
| document.documentElement.dataset.theme = theme; |
| } |
| if (typeof window !== "undefined") { |
| window.localStorage.setItem(STORAGE_KEY, theme); |
| } |
| }, [theme]); |
|
|
| const setTheme = useCallback((t: ThemeMode) => setThemeState(t), []); |
| const toggleTheme = useCallback( |
| () => setThemeState((prev) => (prev === "light" ? "dark" : "light")), |
| [] |
| ); |
|
|
| const value = useMemo<ThemeValue>( |
| () => ({ theme, setTheme, toggleTheme }), |
| [theme, setTheme, toggleTheme] |
| ); |
|
|
| 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; |
| } |
|
|