31560a712f
- Fix ClientWrappers missing 'about' namespace (MISSING_MESSAGE error) - Add system/light/dark theme toggle with prefers-color-scheme detection - Rewrite 404 page with i18n, accessibility, and proper navigation - Rewrite books page with Header/Footer, i18n, and semantic HTML - Add i18n keys to About, Footer, and both locale files - Fix dark mode contrast: text-stone-300/600 -> text-stone-400 - Replace raw hex bg-[#fdfcf8] with bg-stone-50 across all components - Guard console.error in ChatWidget and manage/page behind NODE_ENV - Add aria-label to admin login form - Remove emoji from manage page password toggle - Update stale dates in privacy-policy and legal-notice - Fix ScrollFadeIn index->delay prop type error in books page - Fix privacy-policy and legal-notice landmark structure - Add pre-push-check.test.ts: 13-category static analysis (i18n parity, namespace coverage, key resolution, accessibility, email validation, hex colors, emojis, console guards, env docs, types) - Add explicit i18n check step to CI workflow
66 lines
1.9 KiB
TypeScript
66 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
|
|
type Theme = "system" | "light" | "dark";
|
|
|
|
const ThemeCtx = createContext<{ theme: Theme; resolvedTheme: "light" | "dark"; setTheme: (t: Theme) => void }>({
|
|
theme: "system",
|
|
resolvedTheme: "light",
|
|
setTheme: () => {},
|
|
});
|
|
|
|
function getSystemTheme(): "light" | "dark" {
|
|
if (typeof window === "undefined") return "light";
|
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
}
|
|
|
|
function applyTheme(theme: Theme) {
|
|
const resolved = theme === "system" ? getSystemTheme() : theme;
|
|
document.documentElement.classList.toggle("dark", resolved === "dark");
|
|
return resolved;
|
|
}
|
|
|
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
const [theme, setThemeState] = useState<Theme>("system");
|
|
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("light");
|
|
|
|
useEffect(() => {
|
|
try {
|
|
const stored = localStorage.getItem("theme") as Theme | null;
|
|
if (stored === "dark" || stored === "light" || stored === "system") {
|
|
setThemeState(stored);
|
|
}
|
|
} catch {}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const resolved = applyTheme(theme);
|
|
setResolvedTheme(resolved);
|
|
}, [theme]);
|
|
|
|
useEffect(() => {
|
|
if (theme !== "system") return;
|
|
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
|
const handler = () => {
|
|
const resolved = applyTheme(theme);
|
|
setResolvedTheme(resolved);
|
|
};
|
|
mql.addEventListener("change", handler);
|
|
return () => mql.removeEventListener("change", handler);
|
|
}, [theme]);
|
|
|
|
const setTheme = useCallback((t: Theme) => {
|
|
setThemeState(t);
|
|
try {
|
|
localStorage.setItem("theme", t);
|
|
} catch {}
|
|
}, []);
|
|
|
|
return <ThemeCtx.Provider value={{ theme, resolvedTheme, setTheme }}>{children}</ThemeCtx.Provider>;
|
|
}
|
|
|
|
export function useTheme() {
|
|
return useContext(ThemeCtx);
|
|
}
|