@@ -258,146 +258,147 @@ const navRight = NAV_ITEMS.filter((item) => item.external);
258258 }
259259</style >
260260
261+ <!-- Critical: apply persisted theme/mode before first paint to prevent FOUC -->
261262<script is:inline >
262263 (function () {
263- "use strict";
264-
265- const STORAGE = { theme: "theme", mode: "mode" };
266264 const THEMES = ["light", "dark"];
267265 const MODES = ["low", "medium", "high"];
268266 const root = document.documentElement;
269-
270267 const prefersDark = () =>
271268 window.matchMedia?.("(prefers-color-scheme: dark)").matches;
272-
273269 const readStored = (key, allowed) => {
274270 const v = localStorage.getItem(key);
275271 return allowed.includes(v) ? v : null;
276272 };
277-
278- const cycle = (cur, list) => list[(list.indexOf(cur) + 1) % list.length];
279-
280273 const applyTheme = (t) => {
281274 root.toggleAttribute("data-theme-dark", t === "dark");
282275 root.toggleAttribute("data-theme-light", t === "light");
283276 };
284-
285- const applyMode = (m) => {
286- root.setAttribute("data-mode", m);
287- };
288-
289- const toggleTheme = () => {
290- const cur = root.hasAttribute("data-theme-dark") ? "dark" : "light";
291- const next = cycle(cur, THEMES);
292- localStorage.setItem(STORAGE.theme, next);
293- applyTheme(next);
294- };
295-
296- const toggleMode = () => {
297- const cur = root.getAttribute("data-mode") ?? "medium";
298- const next = cycle(cur, MODES);
299- localStorage.setItem(STORAGE.mode, next);
300- applyMode(next);
301- };
277+ const applyMode = (m) => root.setAttribute("data-mode", m);
302278
303279 applyTheme(
304- readStored(STORAGE. theme, THEMES) ?? (prefersDark() ? "dark" : "light")
280+ readStored(" theme" , THEMES) ?? (prefersDark() ? "dark" : "light")
305281 );
306- applyMode(readStored(STORAGE. mode, MODES) ?? "medium");
282+ applyMode(readStored(" mode" , MODES) ?? "medium");
307283
308284 window
309285 .matchMedia?.("(prefers-color-scheme: dark)")
310286 .addEventListener("change", () => {
311- if (!readStored(STORAGE. theme, THEMES)) {
287+ if (!readStored(" theme" , THEMES)) {
312288 applyTheme(prefersDark() ? "dark" : "light");
313289 }
314290 });
291+ })();
292+ </script >
315293
316- document
317- .getElementById("theme-toggle")
318- ?.addEventListener("click", toggleTheme);
319- document
320- .getElementById("theme-toggle-menu")
321- ?.addEventListener("click", toggleTheme);
322-
323- document
324- .getElementById("mode-toggle")
325- ?.addEventListener("click", toggleMode);
326- document
327- .getElementById("mode-toggle-menu")
328- ?.addEventListener("click", toggleMode);
329-
330- const menuBtn = document.getElementById("menu-toggle");
331- const panel = document.getElementById("menu-panel");
332-
333- const setOpen = (open) => {
334- root.setAttribute("data-nav-open", open ? "true" : "false");
335- menuBtn?.setAttribute("aria-expanded", open ? "true" : "false");
336- };
337-
338- const isOpen = () => root.getAttribute("data-nav-open") === "true";
339-
340- setOpen(false);
341-
342- menuBtn?.addEventListener("click", () => setOpen(!isOpen()));
343-
344- panel?.addEventListener("click", (e) => {
345- if (e.target?.closest?.("a")) setOpen(false);
346- });
347-
348- document.addEventListener("keydown", (e) => {
349- if (e.key === "Escape") setOpen(false);
350- });
351-
352- window.addEventListener("resize", () => {
353- if (window.innerWidth > 480) setOpen(false);
354- });
355-
356- const pickNextDifferent = (arr, cur) => {
357- if (arr.length <= 1) return cur;
358- let next = cur,
359- tries = 0;
360- while (next === cur && tries < 5) {
361- next = arr[Math.floor(Math.random() * arr.length)];
362- tries++;
363- }
364- return next;
365- };
366-
367- const INTERVAL = 65536; // ms
368- const rotIcons = Array.from(
369- document.querySelectorAll("i[data-icon-rotate]")
370- );
371- const perItem = rotIcons.length > 0 ? INTERVAL / rotIcons.length : INTERVAL;
372-
373- rotIcons.forEach((el, i) => {
374- const scheduleNext = (delay) => {
294+ <!-- Interactive: toggle handlers and icon rotation (deferred, bundled by Astro) -->
295+ <script >
296+ const THEMES = ["light", "dark"];
297+ const MODES = ["low", "medium", "high"];
298+ const root = document.documentElement;
299+
300+ const cycle = (cur: string, list: string[]): string =>
301+ list[(list.indexOf(cur) + 1) % list.length]!;
302+
303+ const applyTheme = (t: string) => {
304+ root.toggleAttribute("data-theme-dark", t === "dark");
305+ root.toggleAttribute("data-theme-light", t === "light");
306+ };
307+
308+ const applyMode = (m: string) => root.setAttribute("data-mode", m);
309+
310+ const toggleTheme = () => {
311+ const cur = root.hasAttribute("data-theme-dark") ? "dark" : "light";
312+ const next = cycle(cur, THEMES);
313+ localStorage.setItem("theme", next);
314+ applyTheme(next);
315+ };
316+
317+ const toggleMode = () => {
318+ const cur = root.getAttribute("data-mode") ?? "medium";
319+ const next = cycle(cur, MODES);
320+ localStorage.setItem("mode", next);
321+ applyMode(next);
322+ };
323+
324+ document
325+ .getElementById("theme-toggle")
326+ ?.addEventListener("click", toggleTheme);
327+ document
328+ .getElementById("theme-toggle-menu")
329+ ?.addEventListener("click", toggleTheme);
330+ document.getElementById("mode-toggle")?.addEventListener("click", toggleMode);
331+ document
332+ .getElementById("mode-toggle-menu")
333+ ?.addEventListener("click", toggleMode);
334+
335+ const menuBtn = document.getElementById("menu-toggle");
336+ const panel = document.getElementById("menu-panel");
337+
338+ const setOpen = (open: boolean) => {
339+ root.setAttribute("data-nav-open", open ? "true" : "false");
340+ menuBtn?.setAttribute("aria-expanded", open ? "true" : "false");
341+ };
342+
343+ const isOpen = () => root.getAttribute("data-nav-open") === "true";
344+
345+ setOpen(false);
346+
347+ menuBtn?.addEventListener("click", () => setOpen(!isOpen()));
348+ panel?.addEventListener("click", (e) => {
349+ if ((e.target as Element)?.closest?.("a")) setOpen(false);
350+ });
351+ document.addEventListener("keydown", (e) => {
352+ if (e.key === "Escape") setOpen(false);
353+ });
354+ window.addEventListener("resize", () => {
355+ if (window.innerWidth > 480) setOpen(false);
356+ });
357+
358+ const pickNextDifferent = (arr: string[], cur: string): string => {
359+ if (arr.length <= 1) return cur;
360+ let next = cur,
361+ tries = 0;
362+ while (next === cur && tries < 5) {
363+ next = arr[Math.floor(Math.random() * arr.length)]!;
364+ tries++;
365+ }
366+ return next;
367+ };
368+
369+ const INTERVAL = 65536; // ms
370+ const rotIcons = Array.from(
371+ document.querySelectorAll<HTMLElement>("i[data-icon-rotate]")
372+ );
373+ const perItem = rotIcons.length > 0 ? INTERVAL / rotIcons.length : INTERVAL;
374+
375+ rotIcons.forEach((el, i) => {
376+ const scheduleNext = (delay: number) => {
377+ window.setTimeout(() => {
378+ const raw = el.getAttribute("data-icons") ?? "";
379+ const list = raw.split("|").filter(Boolean);
380+ if (list.length <= 1) {
381+ scheduleNext(INTERVAL);
382+ return;
383+ }
384+ const cur = Array.from(el.classList)
385+ .filter((c) => c !== "rotating")
386+ .join(" ");
387+ el.classList.add("rotating");
375388 window.setTimeout(() => {
376- const raw = el.getAttribute("data-icons") || "";
377- const list = raw.split("|").filter(Boolean);
378- if (list.length <= 1) {
389+ const next = pickNextDifferent(list, cur);
390+ el.className = next + " rotating";
391+ }, 140);
392+ el.addEventListener(
393+ "animationend",
394+ () => {
395+ el.classList.remove("rotating");
379396 scheduleNext(INTERVAL);
380- return;
381- }
382- const cur = Array.from(el.classList)
383- .filter((c) => c !== "rotating")
384- .join(" ");
385- el.classList.add("rotating");
386- window.setTimeout(() => {
387- const next = pickNextDifferent(list, cur);
388- el.className = next + " rotating";
389- }, 140);
390- el.addEventListener(
391- "animationend",
392- () => {
393- el.classList.remove("rotating");
394- scheduleNext(INTERVAL);
395- },
396- { once: true }
397- );
398- }, delay);
399- };
400- scheduleNext(i * perItem);
401- });
402- })();
397+ },
398+ { once: true }
399+ );
400+ }, delay);
401+ };
402+ scheduleNext(i * perItem);
403+ });
403404</script >
0 commit comments