feat: theme toggle
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
/* ============================================================================
|
||||
SITE THEME
|
||||
----------------------------------------------------------------------------
|
||||
The one global theme decision the site makes. The whole tier-2 palette is
|
||||
still per-surface (see app/styles/semantic.css); this simply lets a reader
|
||||
flip WHICH surface the document stands on, `:root` dark by default. The
|
||||
preference travels as the `sharper-theme` cookie so the server renders the
|
||||
same surface the next page will paint - no flash-of-wrong-theme, no
|
||||
hydration mismatch.
|
||||
========================================================================= */
|
||||
|
||||
export type AppTheme = "dark" | "light";
|
||||
|
||||
export const THEME_COOKIE = "sharper-theme";
|
||||
|
||||
/** Default when no cookie is present. `:root` is the dark surface. */
|
||||
export const DEFAULT_THEME: AppTheme = "dark";
|
||||
|
||||
/** Parse the cookie header into a theme. Anything unknown resolves to dark. */
|
||||
export function readTheme(cookieHeader: string | null): AppTheme {
|
||||
if (!cookieHeader) return DEFAULT_THEME;
|
||||
const pair = cookieHeader
|
||||
.split(";")
|
||||
.map((part) => part.trim())
|
||||
.find((part) => part.startsWith(`${THEME_COOKIE}=`));
|
||||
const value = pair?.split("=")[1];
|
||||
return value === "light" ? "light" : value === "dark" ? "dark" : DEFAULT_THEME;
|
||||
}
|
||||
|
||||
/** The Set-Cookie value that persists a theme choice for a year. */
|
||||
export function themeCookie(theme: AppTheme): string {
|
||||
return `${THEME_COOKIE}=${theme}; Path=/; Max-Age=31536000; SameSite=Lax`;
|
||||
}
|
||||
Reference in New Issue
Block a user