import { useMemo } from "react"; import { useFetcher, useRouteLoaderData } from "react-router"; import { createTranslator } from "~/i18n/t"; import type { AppTheme } from "~/lib/theme"; import type { Locale } from "../LanguageSwitcher"; import { cx, FOCUS_RING, HOVER_TRANSITION } from "./styles"; /** The document surface the root loader currently paints on . */ interface RootLoaderData { theme?: AppTheme; } /** A minimal sun, for "switch to light". Decorative - the label calls it. */ function SunIcon() { return ( ); } /** A minimal crescent moon, for "switch to dark". Decorative. */ function MoonIcon() { return ( ); } export interface ThemeToggleProps { /** Reading locale, for the accessible label. */ locale: Locale; className?: string; } /** * The site's one global theme control: sun in dark mode, moon in light. * * It renders the target of the next click - the label and the icon describe * what a press DOES ("Switch to light theme"), never the current state - and * posts that target to the root action at `/`. The action persists the * `sharper-theme` cookie, the loader re-runs and the shell repaints * `data-surface` on , so the whole tier-2 palette re-resolves. Because * the current theme is read off the root loader (which the server fed from * the cookie), the SSR markup and the first client render always agree. */ export function ThemeToggle({ locale, className }: ThemeToggleProps) { const fetcher = useFetcher(); const rootData = useRouteLoaderData("root"); const t = useMemo(() => createTranslator(locale), [locale]); const theme: AppTheme = rootData?.theme ?? "dark"; const targetTheme: AppTheme = theme === "light" ? "dark" : "light"; const label = targetTheme === "light" ? t("nav.themeToLight") : t("nav.themeToDark"); return ( ); } export default ThemeToggle; /* --------------------------------------------------------------------------- USAGE --------------------------------------------------------------------------- // In the nav actions cluster, between the language control and the // hamburger. Reads the theme from the root loader on its own. ------------------------------------------------------------------------ */