mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(dashboard): add ThemeContext with localStorage persistence
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import type { PresetId, ThemeConfig, ThemePreset } from "./types.ts";
|
||||
import { DEFAULT_THEME_CONFIG } from "./types.ts";
|
||||
import { getPreset } from "./presets.ts";
|
||||
import { applyTheme } from "./theme-engine.ts";
|
||||
|
||||
const STORAGE_KEY = "ua-theme";
|
||||
|
||||
interface ThemeContextValue {
|
||||
config: ThemeConfig;
|
||||
preset: ThemePreset;
|
||||
setPreset: (presetId: PresetId) => void;
|
||||
setAccent: (accentId: string) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
function loadFromLocalStorage(): ThemeConfig | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed.presetId === "string" && typeof parsed.accentId === "string") {
|
||||
return parsed as ThemeConfig;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveToLocalStorage(config: ThemeConfig): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
||||
} catch {
|
||||
// Storage full or unavailable — ignore
|
||||
}
|
||||
}
|
||||
|
||||
function resolveInitialTheme(metaTheme?: ThemeConfig | null): ThemeConfig {
|
||||
return loadFromLocalStorage() ?? metaTheme ?? DEFAULT_THEME_CONFIG;
|
||||
}
|
||||
|
||||
interface ThemeProviderProps {
|
||||
metaTheme?: ThemeConfig | null;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ThemeProvider({ metaTheme, children }: ThemeProviderProps) {
|
||||
const [config, setConfig] = useState<ThemeConfig>(() => resolveInitialTheme(metaTheme));
|
||||
const initialized = useRef(false);
|
||||
|
||||
// Apply theme on mount and config changes
|
||||
useEffect(() => {
|
||||
applyTheme(config);
|
||||
if (initialized.current) {
|
||||
saveToLocalStorage(config);
|
||||
}
|
||||
initialized.current = true;
|
||||
}, [config]);
|
||||
|
||||
// Update if metaTheme arrives later (async fetch) and no localStorage preference exists
|
||||
useEffect(() => {
|
||||
if (metaTheme && !loadFromLocalStorage()) {
|
||||
setConfig(metaTheme);
|
||||
}
|
||||
}, [metaTheme]);
|
||||
|
||||
const setPreset = useCallback((presetId: PresetId) => {
|
||||
setConfig((prev) => {
|
||||
const newPreset = getPreset(presetId);
|
||||
return { presetId, accentId: newPreset.defaultAccentId };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setAccent = useCallback((accentId: string) => {
|
||||
setConfig((prev) => ({ ...prev, accentId }));
|
||||
}, []);
|
||||
|
||||
const preset = getPreset(config.presetId);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ config, preset, setPreset, setAccent }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme(): ThemeContextValue {
|
||||
const ctx = useContext(ThemeContext);
|
||||
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { ThemeProvider, useTheme } from "./ThemeContext.tsx";
|
||||
export { PRESETS, getPreset, getAccent } from "./presets.ts";
|
||||
export { applyTheme } from "./theme-engine.ts";
|
||||
export type { PresetId, ThemeConfig, ThemePreset, AccentSwatch } from "./types.ts";
|
||||
export { DEFAULT_THEME_CONFIG } from "./types.ts";
|
||||
Reference in New Issue
Block a user