"use client";
import {
createContext,
useCallback,
useContext,
useLayoutEffect,
useMemo,
useState,
} from "react";
import { translate, type Lang } from "@/lib/i18n";
import type { ThemeName } from "@/lib/theme";
import {
LANG_STORAGE_KEY,
THEME_STORAGE_KEY,
parseSavedLang,
parseSavedTheme,
} from "@/lib/ui-prefs";
interface ThemeContextValue {
theme: ThemeName;
lang: Lang;
toggleTheme: () => void;
toggleLang: () => void;
t: (text: string) => string;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
// ★既定(dark / ja)で始める★ — サーバーも既定で描くので、最初の描画を一致させる(ハイドレーションの不一致を避ける)。
// ★保存値は下の useLayoutEffect で読む(描く前)★。`useState(() => localStorage…)` にしないこと(指示書BC 3-0・D-183)
const [theme, setTheme] = useState<ThemeName>("dark");
const [lang, setLang] = useState<Lang>("ja");
/** 保存値を読み終えたか。★読み終える前は <html> にも localStorage にも書かない★(既定値で上書きしない) */
const [prefsLoaded, setPrefsLoaded] = useState(false);
// ★保存した表示設定を、ハイドレーションの直後・描く前に読む★(ユーザー決定 2026-10-19 U1)。
// <html> の data-theme / lang は <head> のスクリプト(app/layout.tsx の PREFS_INLINE_SCRIPT)が先に入れている。
// ここでは JS で色・文字を決めている部品(THEME_TOKENS[theme]・t())のために state を合わせる。
// 開発時の Strict Mode の再マウントで React が <html> の属性を消すので、下の effect が入れ直す(ガイドの「Re-applying attributes」)
useLayoutEffect(() => {
let savedTheme: ThemeName | null = null;
let savedLang: Lang | null = null;
try {
savedTheme = parseSavedTheme(window.localStorage.getItem(THEME_STORAGE_KEY));
savedLang = parseSavedLang(window.localStorage.getItem(LANG_STORAGE_KEY));
} catch {
// ★使えない環境(プライベートウィンドウ等)は既定のまま★
}
// eslint-disable-next-line react-hooks/set-state-in-effect -- 保存した表示設定を描く前に反映する(D-183)
if (savedTheme) setTheme(savedTheme);
if (savedLang) setLang(savedLang);
setPrefsLoaded(true);
}, []);
useLayoutEffect(() => {
if (!prefsLoaded) return;
document.documentElement.dataset.theme = theme;
document.documentElement.lang = lang === "ja" ? "ja" : "en";
try {
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
window.localStorage.setItem(LANG_STORAGE_KEY, lang);
} catch {
// ★保存できなくても画面は切り替わる★(その閲覧中だけ有効)
}
}, [theme, lang, prefsLoaded]);
const toggleTheme = useCallback(() => {
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
}, []);
const toggleLang = useCallback(() => {
setLang((prev) => (prev === "ja" ? "en" : "ja"));
}, []);
const t = useCallback((text: string) => translate(lang, text), [lang]);
const value = useMemo(
() => ({ theme, lang, toggleTheme, toggleLang, t }),
[theme, lang, toggleTheme, toggleLang, t]
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error("useTheme must be used within ThemeProvider");
}
return ctx;
}