3p1idvga1: Add dark and light design tokens with a persisted pre-paint theme
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
// Theme helper (ADR 0001, section 5). The theme is `data-theme` on <html>,
|
||||
// stored in localStorage under THEME_KEY. Dark is the default.
|
||||
|
||||
export type Theme = "dark" | "light";
|
||||
|
||||
export const THEME_KEY = "theme";
|
||||
export const DEFAULT_THEME: Theme = "dark";
|
||||
|
||||
type ReadableStorage = Pick<Storage, "getItem">;
|
||||
type WritableStorage = Pick<Storage, "setItem">;
|
||||
type ThemeRoot = { dataset: DOMStringMap; style: { colorScheme: string } };
|
||||
|
||||
export function isTheme(value: unknown): value is Theme {
|
||||
return value === "dark" || value === "light";
|
||||
}
|
||||
|
||||
/** The stored theme, or dark when nothing valid is stored or storage throws. */
|
||||
export function readStoredTheme(storage: ReadableStorage | null | undefined): Theme {
|
||||
try {
|
||||
const value = storage?.getItem(THEME_KEY);
|
||||
return isTheme(value) ? value : DEFAULT_THEME;
|
||||
} catch {
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
}
|
||||
|
||||
/** The theme currently on the root element, or dark when it is missing. */
|
||||
export function currentTheme(root: ThemeRoot | null | undefined): Theme {
|
||||
const value = root?.dataset.theme;
|
||||
return isTheme(value) ? value : DEFAULT_THEME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the theme on the root element and store it. A failed write still
|
||||
* changes the theme for this session.
|
||||
*/
|
||||
export function applyTheme(theme: Theme, root: ThemeRoot, storage: WritableStorage | null | undefined): void {
|
||||
root.dataset.theme = theme;
|
||||
root.style.colorScheme = theme;
|
||||
try {
|
||||
storage?.setItem(THEME_KEY, theme);
|
||||
} catch {
|
||||
// Storage blocked or full: keep the theme for this session only.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline script for the first child of <head>. It runs before first paint, so
|
||||
* a stored light theme never flashes dark. Fixed string, no user input.
|
||||
*/
|
||||
export const THEME_SCRIPT = `(function(){var t="${DEFAULT_THEME}";try{var s=localStorage.getItem("${THEME_KEY}");if(s==="light"||s==="dark")t=s;}catch(e){}var r=document.documentElement;r.dataset.theme=t;r.style.colorScheme=t;})();`;
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useSyncExternalStore } from "react";
|
||||
import { applyTheme, currentTheme, DEFAULT_THEME, THEME_KEY, type Theme } from "./theme";
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
// Another tab changed the theme: follow it.
|
||||
if (event.key !== THEME_KEY) return;
|
||||
const next = event.newValue === "light" || event.newValue === "dark" ? event.newValue : DEFAULT_THEME;
|
||||
applyTheme(next, document.documentElement, null);
|
||||
listeners.forEach((l) => l());
|
||||
};
|
||||
window.addEventListener("storage", onStorage);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
window.removeEventListener("storage", onStorage);
|
||||
};
|
||||
}
|
||||
|
||||
const getSnapshot = (): Theme => currentTheme(document.documentElement);
|
||||
const getServerSnapshot = (): Theme => DEFAULT_THEME;
|
||||
|
||||
function safeStorage(): Storage | null {
|
||||
try {
|
||||
return window.localStorage;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Shared theme state for every toggle: the current theme and a setter. */
|
||||
export function useTheme(): { theme: Theme; setTheme: (theme: Theme) => void; toggleTheme: () => void } {
|
||||
const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
const setTheme = useCallback((next: Theme) => {
|
||||
applyTheme(next, document.documentElement, safeStorage());
|
||||
listeners.forEach((l) => l());
|
||||
}, []);
|
||||
const toggleTheme = useCallback(() => setTheme(getSnapshot() === "dark" ? "light" : "dark"), [setTheme]);
|
||||
return { theme, setTheme, toggleTheme };
|
||||
}
|
||||
Reference in New Issue
Block a user