"use client"; import { createContext, useContext, useState, useEffect, startTransition, type ReactNode } from "react"; import { parseThemeQuery, resolveIncomingThemeMode, resolveStoredTheme, type ResolvedTheme, } from "./theme-sync"; export type Theme = "light" | "dark" | "system"; interface ThemeContextType { theme: Theme; setTheme: (t: Theme) => void; resolved: ResolvedTheme; isEmbedded: boolean; } function getPrefersDark() { if (typeof window === "undefined") { return true; } return window.matchMedia("(prefers-color-scheme: dark)").matches; } function getIsEmbedded() { if (typeof window === "undefined") { return false; } try { return window.self !== window.top; } catch { return true; } } function getParentResolvedTheme(): ResolvedTheme | null { if (!getIsEmbedded()) { return null; } try { const parentRoot = window.parent.document.documentElement; const parentBody = window.parent.document.body; if (parentBody?.getAttribute("theme-mode") === "dark" || parentRoot.classList.contains("dark")) { return "dark"; } return "light"; } catch { return null; } } function getThemeQuery(): Theme | null { if (typeof window === "undefined") { return null; } return parseThemeQuery(new URLSearchParams(window.location.search).get("theme")); } const ThemeContext = createContext({ theme: "system", setTheme: () => {}, resolved: "dark", isEmbedded: false, }); export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setThemeState] = useState("system"); const [isEmbedded, setIsEmbedded] = useState(getIsEmbedded); const [queryTheme, setQueryTheme] = useState(getThemeQuery); const [parentResolved, setParentResolved] = useState(getParentResolvedTheme); const [resolved, setResolved] = useState( () => { const nextQueryTheme = getThemeQuery(); return ( getParentResolvedTheme() ?? (nextQueryTheme ? resolveStoredTheme(nextQueryTheme, getPrefersDark()) : null) ?? resolveStoredTheme("system", getPrefersDark()) ); }, ); useEffect(() => { const saved = localStorage.getItem("theme") as Theme | null; if (saved && ["light", "dark", "system"].includes(saved)) { startTransition(() => { setThemeState(saved); }); } }, []); useEffect(() => { const nextIsEmbedded = getIsEmbedded(); const nextQueryTheme = getThemeQuery(); const nextParentResolved = getParentResolvedTheme(); startTransition(() => { setIsEmbedded(nextIsEmbedded); setQueryTheme(nextQueryTheme); setParentResolved((current) => current ?? nextParentResolved); }); }, []); useEffect(() => { if (!isEmbedded) { return; } const handleMessage = (event: MessageEvent) => { if (event.source !== window.parent) { return; } const nextResolved = resolveIncomingThemeMode( event.data?.themeMode, window.matchMedia("(prefers-color-scheme: dark)").matches, ); if (nextResolved) { setParentResolved(nextResolved); } }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, [isEmbedded]); useEffect(() => { const mq = window.matchMedia("(prefers-color-scheme: dark)"); const root = document.documentElement; const activeTheme = queryTheme ?? theme; const queryResolved = queryTheme ? resolveStoredTheme(queryTheme, mq.matches) : null; const applyResolved = (nextResolved: ResolvedTheme) => { setResolved(nextResolved); root.classList.remove("light", "dark"); root.classList.add(nextResolved); root.setAttribute("data-theme", nextResolved); }; applyResolved( parentResolved ?? queryResolved ?? resolveStoredTheme(theme, mq.matches), ); if (parentResolved !== null || activeTheme !== "system") { return; } const handleSystemThemeChange = (event: MediaQueryListEvent) => { applyResolved(resolveStoredTheme(activeTheme, event.matches)); }; mq.addEventListener("change", handleSystemThemeChange); return () => mq.removeEventListener("change", handleSystemThemeChange); }, [parentResolved, queryTheme, theme]); const setTheme = (t: Theme) => { setThemeState(t); localStorage.setItem("theme", t); }; return ( {children} ); } export function useTheme() { return useContext(ThemeContext); }