docs: final review fixes — deferred backlog, doc accuracy, sample-data note
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dd106c5d0e
commit
2ff58ace46
@@ -36,7 +36,7 @@ bun dist/server.js # wrong: crashes with 'Bundled file "./index-….js" no
|
||||
- Assets are bundled, never loaded from a CDN: fonts from `@fontsource/*`, three.js from npm.
|
||||
YouTube videos are link cards (`createVideoCards`), not embeds.
|
||||
- Adding a mockup route means editing `src/server.ts`, `src/mockups/registry.ts` and the cards in
|
||||
`src/index.html` together; `tests/server.test.ts` checks they agree.
|
||||
`src/index.html` together; `tests/server.test.ts` checks every registry slug is served and that the index shows each entry's name, pitch and tryThis.
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -69,7 +69,7 @@ Mockup rounds — any UI change beyond a trivial tweak, while brainstorming and
|
||||
`src/server.ts` -> `Bun.serve` with HTML-import routes: `/` and `/mockups/NN-slug/`
|
||||
`src/data/` -> `types.ts` (shapes), `sample.ts` (shared sample data), `query.ts` (helpers)
|
||||
`src/shared/` -> credits, audio slot (direct MP3, empty state), YouTube link cards
|
||||
`src/mockups/NN-slug/` -> one design direction each: `index.html`, `main.ts`, `style.css`
|
||||
`src/mockups/NN-slug/` -> one design direction each: `index.html`, `main.ts`, `style.css`, plus optional helper modules (e.g. `scene.ts`, `layout.ts`)
|
||||
`tests/` -> data invariants, shared DOM modules (happy-dom), route smoke tests
|
||||
|
||||
Full picture: `docs/ARCHITECTURE.md` — read it before your first change in a session.
|
||||
|
||||
@@ -14,7 +14,7 @@ shared credits snippet.
|
||||
| `src/server.ts` | `startServer(port)`: routes `/`, `/mockups/NN-slug/` (+ 302 from the no-slash form), 404 otherwise. Runs when `import.meta.main`; `PORT` env, default 3000. |
|
||||
| `src/index.html`, `src/index.ts`, `src/index.css` | Index page: static cards linking each mockup, then `mountCredits()`. |
|
||||
| `src/mockups/registry.ts` | `mockups: MockupInfo[]` — slug, number, name, pitch, what to try. |
|
||||
| `src/mockups/NN-slug/` | One design direction: `index.html` (links `../../shared/credits.css`, has `<footer data-fan-credits>`), `main.ts`, `style.css`. |
|
||||
| `src/mockups/NN-slug/` | One design direction: `index.html` (links `../../shared/credits.css`, has `<footer data-fan-credits>`), `main.ts`, `style.css`, plus optional helper modules (e.g. `scene.ts`, `layout.ts`). |
|
||||
| `src/data/types.ts` | `Saga`, `Song`, `Part`, `Motif`, `StoryTime`, `OdysseyRef`, `Video`. |
|
||||
| `src/data/sample.ts` | `sagas`, `motifs`, `songs`. Placeholder lyrics; unverified fields `// TODO(verify)`. |
|
||||
| `src/data/query.ts` | `songsWithMotif`, `motifsInSong`, `gapYears`, `sagaOf`. |
|
||||
|
||||
+9
-1
@@ -11,8 +11,16 @@ or `AGENTS.md` Commands.
|
||||
|
||||
| Command | Result |
|
||||
|---|---|
|
||||
| `git status --short` | worked — exit 0 |
|
||||
| `git diff --stat` | worked — exit 0 |
|
||||
| `git log --oneline -1` | worked — exit 0 |
|
||||
| `git show --stat --oneline HEAD` | worked — exit 0 |
|
||||
| `git branch --list` | worked — lists `main` |
|
||||
| `git rev-parse --short HEAD` | worked — exit 0 |
|
||||
| `git add <paths>` | worked — evidence: the final-review fixes commit that added these rows |
|
||||
| `git commit -m <msg>` | worked — evidence: the final-review fixes commit that added these rows |
|
||||
| `bun install --frozen-lockfile` | worked (no changes) |
|
||||
| `bun test` | worked — 32 pass, 0 fail, 3 files |
|
||||
| `bun test` | worked — 34 pass, 0 fail, 3 files |
|
||||
| `bun test tests/data.test.ts` | worked — 10 pass |
|
||||
| `bun test -t "registry has 7"` | worked — 1 pass |
|
||||
| `bun run typecheck` | worked (`tsc --noEmit`, exit 0) |
|
||||
|
||||
+44
-3
@@ -4,7 +4,7 @@ Durable backlog. Agents append here instead of leaving `TODO` comments in source
|
||||
Small, specific, independently actionable. Remove only when actually done.
|
||||
|
||||
## Now
|
||||
- [ ] Verify every `// TODO(verify)` field in `src/data/sample.ts` (48 at bootstrap: saga names, singers,
|
||||
- [ ] Verify every `// TODO(verify)` field in `src/data/sample.ts` (56 after final review: saga names, singers,
|
||||
story times, Odyssey book/lines, motif descriptions, facts) before any content leaves mockup stage.
|
||||
|
||||
## Next
|
||||
@@ -39,5 +39,46 @@ Small, specific, independently actionable. Remove only when actually done.
|
||||
- [ ] Two mockups with byte-identical `style.css` fail `bun run build` with "Multiple files share the same
|
||||
output path" (reproduced by copying `01-editorial/style.css` over `02-vertical-story/style.css`).
|
||||
Identical `main.ts` files do not trigger it.
|
||||
- [ ] `docs/plans/001-timeline-mockups.md` tells mockup agents to start `bun dist/server.js`, which hits
|
||||
the issue above; use `bun run start` instead.
|
||||
|
||||
## Deferred from final review (2026-10-23)
|
||||
|
||||
### Shared / foundation
|
||||
- [ ] `src/server.ts`: routes are hand-listed; generate them from `src/mockups/registry.ts` (see Someday).
|
||||
|
||||
### Docs / permissions
|
||||
- [ ] `docs/DECISIONS.md`: entries are oldest-first; the template is newest-first. Pick one.
|
||||
- [ ] `AGENTS.md`: the architecture map duplicates `docs/ARCHITECTURE.md`; trim one.
|
||||
- [ ] `docs/plans/README.md`: says "feature branch" while branching is still `TODO(maintainer)`.
|
||||
|
||||
### Sample data
|
||||
- [ ] `src/data/sample.ts`: `odyssey.url` points at Butler's prose translation, so "lines X-Y" will not match the linked text; switch to book/chapter references or a lineated edition when verifying.
|
||||
|
||||
### Mockup 01 editorial
|
||||
- [ ] `src/mockups/01-editorial/style.css`: credit footer clips at about 761-1068px width with the folio open (parked; spec targets desktop 1440x900).
|
||||
|
||||
### Mockup 02 vertical story
|
||||
- [ ] `src/mockups/02-vertical-story/main.ts`: `frame()` interleaves layout reads and writes.
|
||||
- [ ] `src/mockups/02-vertical-story/style.css`: saga marker padding inflates boundary gaps; `.sea` background snaps between sagas.
|
||||
|
||||
### Mockup 03 transit map
|
||||
- [ ] `src/mockups/03-transit-map/main.ts`: ruler repeat label reads "Year 0" for fall-of-Troy songs.
|
||||
- [ ] `src/mockups/03-transit-map/main.ts`: `--footer-h` is not re-measured after fonts load.
|
||||
|
||||
### Mockup 04 greek frieze
|
||||
- [ ] `src/mockups/04-greek-frieze/main.ts`: post-drag click-swallow listener can stay armed if no click fires.
|
||||
|
||||
### Mockup 05 score
|
||||
- [ ] `src/mockups/05-score/main.ts`: `.part.is-cued` is not cleared on song change or close.
|
||||
- [ ] `src/mockups/05-score/main.ts`: chip click re-renders the panel, which would restart the MP3.
|
||||
- [ ] `src/mockups/05-score/main.ts`: `has-song` class is unused.
|
||||
|
||||
### Mockup 06 constellation
|
||||
- [ ] `src/mockups/06-constellation/`: per-frame `Vector3` allocations; constructor failure leaks the canvas; WebGL probe context is not released.
|
||||
- [ ] `src/mockups/06-constellation/`: label box cache goes stale on resize; Enter on the credits link opens a song.
|
||||
- [ ] `src/mockups/06-constellation/`: fallback ruler is inert; legend/panel overflow; Troy saga caption is clipped.
|
||||
- [ ] `src/mockups/06-constellation/`: check intro flight and easing in a real browser (unverifiable headless).
|
||||
|
||||
### Mockup 07 sea voyage
|
||||
- [ ] `src/mockups/07-sea-voyage/`: per-frame `Color`/`Vector3` allocations; WebGL probe context is not released.
|
||||
- [ ] `src/mockups/07-sea-voyage/`: no `webglcontextlost` handler; DPR is read once.
|
||||
- [ ] `src/mockups/07-sea-voyage/`: waypoint tag `innerHTML` is unescaped; drawer clips the "The open sea" label.
|
||||
|
||||
+12
-12
@@ -44,7 +44,7 @@ export const songs: Song[] = [
|
||||
odyssey: null, // TODO(verify)
|
||||
parts: [
|
||||
{ name: "Verse 1", singers: ["Odysseus"], lines: lyric("Odysseus", 1), motifIds: ["odysseus", "just-a-man"] }, // TODO(verify)
|
||||
{ name: "Chorus", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["just-a-man"], startSec: 60 },
|
||||
{ name: "Chorus", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["just-a-man"], startSec: 60 }, // TODO(verify) singers
|
||||
],
|
||||
facts: ["Shows Odysseus as a husband and father before the voyage."], // TODO(verify)
|
||||
videos: [{ title: "Just a Man — search", url: yt("EPIC The Musical Just a Man") }],
|
||||
@@ -55,7 +55,7 @@ export const songs: Song[] = [
|
||||
odyssey: { book: 9, lines: "39-61", url: BUTLER }, // TODO(verify) raid on the Cicones
|
||||
parts: [
|
||||
{ name: "Verse 1", singers: ["Eurylochus", "Odysseus"], lines: lyric("crew", 1), motifIds: ["odysseus"] }, // TODO(verify)
|
||||
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["warrior"] },
|
||||
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["warrior"] }, // TODO(verify) singers
|
||||
],
|
||||
facts: ["The crew's high spirits are about to run into trouble."], // TODO(verify)
|
||||
videos: [{ title: "Full Speed Ahead — search", url: yt("EPIC The Musical Full Speed Ahead") }],
|
||||
@@ -65,8 +65,8 @@ export const songs: Song[] = [
|
||||
storyTime: { yearsAfterTroy: 0, label: "Shortly after leaving Troy" }, // TODO(verify)
|
||||
odyssey: null, // TODO(verify)
|
||||
parts: [
|
||||
{ name: "Verse 1", singers: ["Odysseus"], lines: lyric("Odysseus", 1), motifIds: ["open-arms"] }, // TODO(verify)
|
||||
{ name: "Verse 2", singers: ["Odysseus", "Crew"], lines: lyric("crew", 2), motifIds: ["open-arms", "odysseus"] },
|
||||
{ name: "Verse 1", singers: ["Polites", "Odysseus"], lines: lyric("Odysseus", 1), motifIds: ["open-arms"] }, // TODO(verify) Polites leads, with Odysseus
|
||||
{ name: "Verse 2", singers: ["Polites", "Odysseus", "Crew"], lines: lyric("crew", 2), motifIds: ["open-arms", "odysseus"] }, // TODO(verify) singers
|
||||
],
|
||||
facts: ["Introduces a welcoming melody that returns later."], // TODO(verify)
|
||||
videos: [{ title: "Open Arms — search", url: yt("EPIC The Musical Open Arms") }],
|
||||
@@ -77,7 +77,7 @@ export const songs: Song[] = [
|
||||
odyssey: { book: 13, lines: "287-310", url: BUTLER }, // TODO(verify) Athena and Odysseus
|
||||
parts: [
|
||||
{ name: "Verse 1", singers: ["Athena"], lines: lyric("Athena", 1), motifIds: ["athena", "warrior"] }, // TODO(verify)
|
||||
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "warrior"], startSec: 70 },
|
||||
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "warrior"], startSec: 70 }, // TODO(verify) singers
|
||||
],
|
||||
facts: ["Athena teaches Odysseus to fight with wit."], // TODO(verify)
|
||||
videos: [{ title: "Warrior of the Mind — search", url: yt("EPIC The Musical Warrior of the Mind") }],
|
||||
@@ -87,8 +87,8 @@ export const songs: Song[] = [
|
||||
storyTime: { yearsAfterTroy: 1, label: "The island of the Cyclopes" }, // TODO(verify)
|
||||
odyssey: { book: 9, lines: "105-460", url: BUTLER }, // TODO(verify)
|
||||
parts: [
|
||||
{ name: "Verse 1", singers: ["Polyphemus"], lines: lyric("Polyphemus", 1), motifIds: ["polyphemus"] },
|
||||
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "warrior"], startSec: 50 },
|
||||
{ name: "Verse 1", singers: ["Polyphemus"], lines: lyric("Polyphemus", 1), motifIds: ["polyphemus"] }, // TODO(verify) singers
|
||||
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "warrior"], startSec: 50 }, // TODO(verify) singers
|
||||
],
|
||||
facts: ["Odysseus meets Polyphemus in his cave."], // TODO(verify)
|
||||
videos: [{ title: "Polyphemus — search", url: yt("EPIC The Musical Polyphemus") }],
|
||||
@@ -99,7 +99,7 @@ export const songs: Song[] = [
|
||||
odyssey: { book: 9, lines: "287-412", url: BUTLER }, // TODO(verify)
|
||||
parts: [
|
||||
{ name: "Verse 1", singers: ["Polyphemus", "Crew"], lines: lyric("Polyphemus", 1), motifIds: ["polyphemus"] }, // TODO(verify)
|
||||
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "just-a-man"] },
|
||||
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "just-a-man"] }, // TODO(verify) singers
|
||||
],
|
||||
facts: ["The crew is trapped and Odysseus must choose a plan."], // TODO(verify)
|
||||
videos: [{ title: "Survive — search", url: yt("EPIC The Musical Survive") }],
|
||||
@@ -109,8 +109,8 @@ export const songs: Song[] = [
|
||||
storyTime: { yearsAfterTroy: 1, label: "Escaping the cave" }, // TODO(verify)
|
||||
odyssey: { book: 9, lines: "431-460", url: BUTLER }, // TODO(verify)
|
||||
parts: [
|
||||
{ name: "Verse 1", singers: ["Telemachus"], lines: lyric("Telemachus", 1), motifIds: ["remember"] }, // TODO(verify)
|
||||
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["remember", "open-arms"], startSec: 40 },
|
||||
{ name: "Verse 1", singers: ["Telemachus"], lines: lyric("Telemachus", 1), motifIds: ["remember"] }, // TODO(verify) Telemachus as singer
|
||||
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["remember", "open-arms"], startSec: 40 }, // TODO(verify) singers
|
||||
],
|
||||
facts: ["A memorial theme introduced here returns later."], // TODO(verify)
|
||||
videos: [{ title: "Remember Them — search", url: yt("EPIC The Musical Remember Them") }],
|
||||
@@ -120,9 +120,9 @@ export const songs: Song[] = [
|
||||
storyTime: { yearsAfterTroy: 1, label: "Leaving the island" }, // TODO(verify)
|
||||
odyssey: { book: 9, lines: "473-542", url: BUTLER }, // TODO(verify)
|
||||
parts: [
|
||||
{ name: "Verse 1", singers: ["Odysseus", "Polyphemus"], lines: lyric("Odysseus", 1), motifIds: ["polyphemus", "odysseus"] }, // TODO(verify)
|
||||
{ name: "Verse 1", singers: ["Odysseus", "Athena"], lines: lyric("Odysseus", 1), motifIds: ["odysseus", "athena"] }, // TODO(verify) singers
|
||||
],
|
||||
facts: ["The Cyclops saga ends with Odysseus revealing his name."], // TODO(verify)
|
||||
facts: ["Closes the Cyclops saga as Odysseus leaves the island."], // TODO(verify)
|
||||
videos: [{ title: "My Goodbye — search", url: yt("EPIC The Musical My Goodbye") }],
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<body>
|
||||
<main>
|
||||
<h1>EPIC: The Musical Timeline</h1>
|
||||
<p class="lede">Seven design directions for an interactive timeline. Pick one, or mix several.</p>
|
||||
<p class="lede">Seven design directions for an interactive timeline. Pick one, or mix several. Sample content: placeholder lyrics and unverified details, for comparing designs only.</p>
|
||||
<ul class="cards">
|
||||
<li class="card"><a href="/mockups/01-editorial/"><span class="card__num">01</span><h2>Editorial scroll</h2><p>A clean horizontal line in a parchment-and-serif editorial style, with motifs as thin coloured threads underneath.</p><p class="card__try">Try this: Pan sideways, notice gap length tracking in-story time, and open a song.</p></a></li>
|
||||
<li class="card"><a href="/mockups/02-vertical-story/"><span class="card__num">02</span><h2>Vertical story</h2><p>The voyage scrolls downward; songs reveal one at a time beside a sticky detail panel.</p><p class="card__try">Try this: Scroll and watch the sticky detail panel update.</p></a></li>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { mountCredits } from "./shared/credits";
|
||||
|
||||
// Cards are static in index.html (kept in sync with src/mockups/registry.ts by tests/server.test.ts).
|
||||
// Cards are static in index.html (tests/server.test.ts checks each registry entry's name, pitch and tryThis appear here).
|
||||
mountCredits();
|
||||
|
||||
+15
-15
@@ -12,21 +12,21 @@ export function startServer(port: number): Server<undefined> {
|
||||
return Bun.serve({
|
||||
port,
|
||||
routes: {
|
||||
"/": home,
|
||||
"/mockups/01-editorial/": m0,
|
||||
"/mockups/01-editorial": Response.redirect("/mockups/01-editorial/", 302),
|
||||
"/mockups/02-vertical-story/": m1,
|
||||
"/mockups/02-vertical-story": Response.redirect("/mockups/02-vertical-story/", 302),
|
||||
"/mockups/03-transit-map/": m2,
|
||||
"/mockups/03-transit-map": Response.redirect("/mockups/03-transit-map/", 302),
|
||||
"/mockups/04-greek-frieze/": m3,
|
||||
"/mockups/04-greek-frieze": Response.redirect("/mockups/04-greek-frieze/", 302),
|
||||
"/mockups/05-score/": m4,
|
||||
"/mockups/05-score": Response.redirect("/mockups/05-score/", 302),
|
||||
"/mockups/06-constellation/": m5,
|
||||
"/mockups/06-constellation": Response.redirect("/mockups/06-constellation/", 302),
|
||||
"/mockups/07-sea-voyage/": m6,
|
||||
"/mockups/07-sea-voyage": Response.redirect("/mockups/07-sea-voyage/", 302),
|
||||
"/": home,
|
||||
"/mockups/01-editorial/": m0,
|
||||
"/mockups/01-editorial": Response.redirect("/mockups/01-editorial/", 302),
|
||||
"/mockups/02-vertical-story/": m1,
|
||||
"/mockups/02-vertical-story": Response.redirect("/mockups/02-vertical-story/", 302),
|
||||
"/mockups/03-transit-map/": m2,
|
||||
"/mockups/03-transit-map": Response.redirect("/mockups/03-transit-map/", 302),
|
||||
"/mockups/04-greek-frieze/": m3,
|
||||
"/mockups/04-greek-frieze": Response.redirect("/mockups/04-greek-frieze/", 302),
|
||||
"/mockups/05-score/": m4,
|
||||
"/mockups/05-score": Response.redirect("/mockups/05-score/", 302),
|
||||
"/mockups/06-constellation/": m5,
|
||||
"/mockups/06-constellation": Response.redirect("/mockups/06-constellation/", 302),
|
||||
"/mockups/07-sea-voyage/": m6,
|
||||
"/mockups/07-sea-voyage": Response.redirect("/mockups/07-sea-voyage/", 302),
|
||||
},
|
||||
development: process.env.NODE_ENV !== "production",
|
||||
fetch() {
|
||||
|
||||
@@ -21,7 +21,7 @@ export function createAudioSlot(song: Song): HTMLElement {
|
||||
btn.textContent = part.name;
|
||||
btn.addEventListener("click", () => {
|
||||
audio.currentTime = part.startSec!;
|
||||
void audio.play?.();
|
||||
audio.play?.()?.catch(() => {});
|
||||
});
|
||||
root.append(btn);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ test("index links every mockup", async () => {
|
||||
for (const m of mockups) expect(html).toContain(`/mockups/${m.slug}/`);
|
||||
});
|
||||
|
||||
test("index shows every registry name, pitch and tryThis", async () => {
|
||||
const html = (await (await fetch(base + "/")).text()).replaceAll("&", "&");
|
||||
for (const m of mockups) for (const text of [m.name, m.pitch, m.tryThis]) expect(html).toContain(text);
|
||||
});
|
||||
|
||||
for (const m of mockups) {
|
||||
test(`mockup ${m.slug} serves with credits hook`, async () => {
|
||||
const res = await fetch(`${base}/mockups/${m.slug}/`);
|
||||
|
||||
@@ -48,6 +48,18 @@ describe("createAudioSlot", () => {
|
||||
expect(el.querySelectorAll("audio[src]")).toHaveLength(1);
|
||||
expect(el.querySelectorAll('button[data-start="30"]')).toHaveLength(1);
|
||||
});
|
||||
test("seek button sets currentTime and swallows a rejected play()", async () => {
|
||||
const { createAudioSlot } = await import("../src/shared/audio-slot");
|
||||
const song: Song = { ...base, audio: "/a.mp3", parts: [{ ...base.parts[0]!, startSec: 30 }] };
|
||||
const el = createAudioSlot(song);
|
||||
const audio = el.querySelector("audio") as HTMLAudioElement;
|
||||
let played = 0;
|
||||
audio.play = () => { played++; return Promise.reject(new Error("blocked")); };
|
||||
(el.querySelector("button.audio-slot__seek") as HTMLButtonElement).click();
|
||||
await Promise.resolve();
|
||||
expect(audio.currentTime).toBe(30);
|
||||
expect(played).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createVideoCards", () => {
|
||||
|
||||
Reference in New Issue
Block a user