client-theme.ts
| 1 | import { 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 | |
| 8 | const order: Theme[] = ["auto", "light", "dark"]; |
| 9 | |
| 10 | function current(): Theme { |
| 11 | const value = document.documentElement.dataset.theme; |
| 12 | return value === "light" || value === "dark" ? value : "auto"; |
| 13 | } |
| 14 | |
| 15 | function 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 | |
| 35 | document.getElementById("theme-form")?.addEventListener("submit", (e) => { |
| 36 | e.preventDefault(); |
| 37 | apply(order[(order.indexOf(current()) + 1) % order.length]); |
| 38 | }); |
| 39 |