33 lines
1.5 KiB
TypeScript
33 lines
1.5 KiB
TypeScript
/* ============================================================================
|
|||
|
|
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`;
|
||
|
|
}
|