feat: theme toggle
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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 <html>. */
|
||||
interface RootLoaderData {
|
||||
theme?: AppTheme;
|
||||
}
|
||||
|
||||
/** A minimal sun, for "switch to light". Decorative - the label calls it. */
|
||||
function SunIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** A minimal crescent moon, for "switch to dark". Decorative. */
|
||||
function MoonIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
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 <html>, 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<RootLoaderData>("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 (
|
||||
<fetcher.Form action="/" method="post">
|
||||
<input type="hidden" name="theme" value={targetTheme} />
|
||||
<button
|
||||
type="submit"
|
||||
aria-label={label}
|
||||
title={label}
|
||||
className={cx(
|
||||
"inline-flex size-9 shrink-0 cursor-pointer items-center justify-center",
|
||||
"rounded-control text-[var(--text-secondary)]",
|
||||
"hover:text-[var(--text-primary)]",
|
||||
HOVER_TRANSITION,
|
||||
FOCUS_RING,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{targetTheme === "light" ? <SunIcon /> : <MoonIcon />}
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
|
||||
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.
|
||||
<ThemeToggle locale={locale} />
|
||||
------------------------------------------------------------------------ */
|
||||
Reference in New Issue
Block a user