client-theme.ts
Raw
1import { type Theme, themeIcon } from "./shared";
2
3// Progressive enhancement for the theme toggle. The server already rendered the
4// correct theme (data-theme + the dark sheet's media), so there's nothing to do
5// on load — we only intercept the toggle form to switch instantly without the
6// full-page reload the no-JS POST would cause, and keep the cookie in sync.
7
8const order: Theme[] = ["auto", "light", "dark"];
9
10function current(): Theme {
11 const value = document.documentElement.dataset.theme;
12 return value === "light" || value === "dark" ? value : "auto";
13}
14
15function apply(theme: Theme) {
16 document.documentElement.dataset.theme = theme;
17 const darkMedia =
18 theme === "auto"
19 ? "(prefers-color-scheme: dark)"
20 : theme === "dark"
21 ? "all"
22 : "not all";
23 // Swap both dark sheets in lockstep: sakura (page chrome) and highlight.js
24 // (code syntax). Each is null on pages that don't include it.
25 for (const id of ["sakura-dark", "highlight-dark"]) {
26 const sheet = document.getElementById(id) as HTMLLinkElement | null;
27 if (sheet) sheet.media = darkMedia;
28 }
29 const button = document.getElementById("theme-toggle");
30 if (button) button.innerHTML = themeIcon(theme);
31 // biome-ignore lint/suspicious/noDocumentCookie: a single non-HttpOnly cookie write; the async Cookie Store API isn't worth the complexity (or its weaker browser support) here
32 document.cookie = `zbin-theme=${theme}; path=/; max-age=31536000; samesite=lax`;
33}
34
35document.getElementById("theme-form")?.addEventListener("submit", (e) => {
36 e.preventDefault();
37 apply(order[(order.indexOf(current()) + 1) % order.length]);
38});
39