Skip to content

Commit 749ab50

Browse files
authored
Merge pull request #17 from goude/claude/backlog-work-DApso
Refactor Header scripts and extract magic numbers to CSS variables
2 parents 74b5a6e + e97047f commit 749ab50

4 files changed

Lines changed: 119 additions & 154 deletions

File tree

docs/backlog.md

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,17 @@ Last reviewed 2026-04-02.
66

77
### Split large components
88

9-
Header.astro is 690 lines. Extract SVG logo, theme toggle, and nav into sub-components.
9+
`opening-the-hood/index.astro` is 1,551 lines. Extract sections into components or partial Astro files.
1010

11-
- File: `src/components/Header.astro`
12-
- File: `src/pages/ai-generated/opening-the-hood/index.astro` (1,551 lines)
11+
- File: `src/pages/ai-generated/opening-the-hood/index.astro`
1312

1413
### Extract large inline scripts
1514

1615
Several pages have 100-260 line inline `<script>` blocks that could live in separate files.
1716

18-
- File: `src/components/Header.astro` (lines 548-690 — theme/mode toggle, menu, icon rotation)
1917
- File: `src/pages/cop.astro` (lines 53-313 — QR generation, clipboard, encryption)
2018
- File: `src/pages/egghunt.astro` (lines 580-700+ — decryption, puzzle UI)
2119

22-
### Replace magic numbers with named constants
23-
24-
Animation durations (30s) and max-heights (420px) in content.css should be CSS custom properties.
25-
26-
- File: `src/styles/content.css`
27-
2820
## Low Priority
2921

3022
### Resolve TODO comments
@@ -53,35 +45,6 @@ No performance budget exists. Lighthouse CI in the workflow would catch regressi
5345

5446
- File: `.github/workflows/astro.yml`
5547

56-
## Refactoring
57-
58-
Prioritized structural improvements. Address top-down.
59-
60-
### Extract shared markdown rendering utility
61-
62-
`Md.astro` and `docs/[...slug].astro` duplicate the marked + Shiki dual-theme rendering pipeline. Extract to a shared `renderMarkdown()` function in `src/utils/`.
63-
64-
- File: `src/components/Md.astro`
65-
- File: `src/pages/docs/[...slug].astro`
66-
67-
### Consolidate layout usage
68-
69-
`src/layouts/Layout.astro` and `src/components/Layout.astro` — unclear which is canonical. Audit and collapse to one.
70-
71-
- File: `src/layouts/Layout.astro`
72-
73-
### Extract Header.astro sub-components
74-
75-
Header.astro at 690 lines mixes SVG logo, theme toggle, and navigation. Extract each into its own component.
76-
77-
- File: `src/components/Header.astro`
78-
79-
### Break up opening-the-hood page
80-
81-
At 1,551 lines this is the largest file. Extract sections into components or partial Astro files.
82-
83-
- File: `src/pages/ai-generated/opening-the-hood/index.astro`
84-
8548
## Simplifications
8649

8750
Opportunities to reduce complexity without changing behavior.

src/components/Header.astro

Lines changed: 115 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

src/styles/content.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ button:focus-visible kbd {
293293
left: 50%;
294294
transform: translateX(-50%);
295295

296-
width: min(90vw, 420px);
296+
width: min(90vw, var(--tooltip-max-width));
297297
max-width: none;
298298

299299
white-space: normal;

src/styles/vars.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
/* ---------- Layout ---------- */
4949
--page-max: 900px;
5050
--page-pad: 24px;
51+
--tooltip-max-width: 420px;
5152

5253
/* ---------- Hero image ---------- */
5354
--hero-max-height: 420px;

0 commit comments

Comments
 (0)