"use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createContext, useCallback, useContext, useEffect, useState, type ReactNode, } from "react"; import { I18nProvider } from "@/lib/i18n/context"; import type { Locale } from "@/lib/i18n/config"; import { Toaster } from "@/components/ui/sonner"; import { AppSettingsProvider } from "@/components/app-settings-provider"; import { VersionPoller } from "@/components/version-poller"; // ── Theme Context ──────────────────────────────────── type Theme = "dark" | "light" | "system"; interface ThemeContextValue { theme: Theme; resolvedTheme: "dark" | "light"; setTheme: (theme: Theme) => void; } const ThemeContext = createContext({ theme: "system", resolvedTheme: "dark", setTheme: () => {}, }); export function useTheme() { return useContext(ThemeContext); } function getSystemTheme(): "dark" | "light" { if (typeof window === "undefined") return "dark"; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } function applyTheme(resolved: "dark" | "light") { document.documentElement.classList.remove("dark", "light"); document.documentElement.classList.add(resolved); } function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setThemeState] = useState(() => { if (typeof window === "undefined") return "dark"; const saved = localStorage.getItem("healthlog-theme") as Theme | null; // A stored "light"/"dark"/"system" choice wins. With no stored // preference the app defaults to dark rather than tracking the OS. if (saved === "light" || saved === "dark") return saved; if (saved === "system") return "system"; return "dark"; }); const [resolvedTheme, setResolvedTheme] = useState<"dark" | "light">(() => { if (typeof window === "undefined") return "dark"; const saved = localStorage.getItem("healthlog-theme") as Theme | null; if (saved === "light" || saved === "dark") return saved; if (saved === "system") return getSystemTheme(); return "dark"; }); // Apply theme on mount (inline script handles FOUC, this ensures classes are in sync) useEffect(() => { applyTheme(resolvedTheme); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Listen for OS preference changes when in system mode useEffect(() => { if (theme !== "system") return; const mq = window.matchMedia("(prefers-color-scheme: dark)"); const handler = (e: MediaQueryListEvent) => { const resolved = e.matches ? "dark" : "light"; setResolvedTheme(resolved); applyTheme(resolved); }; mq.addEventListener("change", handler); return () => mq.removeEventListener("change", handler); }, [theme]); const setTheme = useCallback((next: Theme) => { setThemeState(next); // Persist every explicit choice, including "system" — the absence of a // stored value is reserved for a fresh visitor and now defaults to dark. // Storing "system" verbatim keeps an explicit OS-tracking choice // distinguishable from "never chose", so a reload honours it. localStorage.setItem("healthlog-theme", next); if (next === "system") { const resolved = getSystemTheme(); setResolvedTheme(resolved); applyTheme(resolved); } else { setResolvedTheme(next); applyTheme(next); } }, []); return ( {children} ); } // ── Root Providers ─────────────────────────────────── export function Providers({ children, initialLocale, }: { children: ReactNode; initialLocale?: Locale; }) { const [queryClient] = useState( () => new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, gcTime: 10 * 60 * 1000, refetchOnWindowFocus: false, retry: 1, }, }, }), ); return ( {children} ); }