feat(app): bottom timeline; drop the Odysseus thread from the album data

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-30 14:57:55 +01:00
1 parent ea728176ae
commit b3385185b5
8 files changed
+728 -6

No files matched your search

+6
View File
@@ -84,3 +84,9 @@ work; committing them would push them to Gitea and bake 1.5 GB into the image. T
**Decision:** the Odyssey and Iliad are served from Butcher & Lang (Gutenberg #1728) and Lang, Leaf &
Myers (#3059) when present, falling back to Butler (#1727, #2199). **Why:** the user disliked Butler's
Roman names (Ulysses, Minerva, Jove); EPIC uses the Greek ones. Both are public domain.
## 2026-10-30 — No thread for Odysseus himself
**Decision:** the album data has no Odysseus character theme. **Why:** character themes are derived from
singers, and Odysseus sings in 35 of 40 songs, so his thread marked almost every stop and crowded the
timeline ("+28 more"). His story is carried by the musical motifs (Warrior of the Mind, Just a Man, …).
+3 -3
View File
@@ -112,7 +112,7 @@ export function loadLyrics(ref: { url: string; timed: boolean }): Promise<Lyrics
export function loadBook(book: number): Promise<HomerBook | null>;
export function passageStart(book: HomerBook, keywords: string[]): { index: number; word: string | null };
```
- [ ] Steps: failing tests with a mocked `fetch` (library 404 → `false`; match by normalised title incl. curly apostrophe "Wouldn’t You Like"; `.txt` lyrics → `{timed:false}`; `passageStart` finds first keyword paragraph, else 0) → implement → pass → commit `feat(app): content client`.
- [x] Steps: failing tests with a mocked `fetch` (library 404 → `false`; match by normalised title incl. curly apostrophe "Wouldn’t You Like"; `.txt` lyrics → `{timed:false}`; `passageStart` finds first keyword paragraph, else 0) → implement → pass → commit `feat(app): content client`.
### Task 4: Side panel
@@ -124,7 +124,7 @@ export function passageStart(book: HomerBook, keywords: string[]): { index: numb
Behaviour pinned by the user: **Listen & lyrics** contains the player, the lyrics and **Watch** (video cards via `createVideoCards`); About contains no Watch and no listen button. **Paths** shows the motif strip at its top **only when `state.timeline === "closed"`**, then per-motif "heard before / here / returns" rows, then the nine-place chart. **Fan works**: empty state "No fan works collected yet. Real ones will be credited and linked to their creators." when `fanWorks` has none for the song. Header play button: starts audio and switches to `listen`. Reading view (`state.read`) shows the Odyssey book (Butcher & Lang, Greek names — no Roman-names note) from the passage start, "Show the N paragraphs before", Back.
- [ ] Steps: failing tests — About has no `.watch`; Listen has `.player`, `.lyrics`, `.watch`; Paths has `.motif-strip` iff timeline closed; fan empty state text; without content (`loadLibrary` false) Listen shows "Content folder not mounted" and no `<audio>`; long title wraps (no `white-space: nowrap` on `.panel__title`) → implement → pass → commit `feat(app): side panel`.
- [x] Steps: failing tests — About has no `.watch`; Listen has `.player`, `.lyrics`, `.watch`; Paths has `.motif-strip` iff timeline closed; fan empty state text; without content (`loadLibrary` false) Listen shows "Content folder not mounted" and no `<audio>`; long title wraps (no `white-space: nowrap` on `.panel__title`) → implement → pass → commit `feat(app): side panel`.
### Task 5: Timeline
@@ -139,7 +139,7 @@ export function mountTimeline(root: HTMLElement, store: Store): { refresh(): voi
```
Layout rule: the current saga is expanded (labels, spacing by measured label width + 16px gap, min 64px); other sagas are compact (no labels, 18px per stop, saga name above); remaining width distributed to gaps between sagas; if it still doesn't fit, the expanded saga scrolls into view (the strip scrolls horizontally). Threads: one row per motif of the current song, knots at songs that use it, across all 40. Hide/show control writes `state.timeline`. "Threads on the sea" switch writes `state.ribbons`.
- [ ] Steps: failing tests — 40 stops; stops strictly increasing x; labels only in the current saga; in every saga expanded (try each saga's first song, width 980, measure = 7.2px/char) no two labelled stops overlap; collapse sets `timeline=closed` → implement → pass → commit `feat(app): bottom timeline`.
- [x] Steps: failing tests — 40 stops; stops strictly increasing x; labels only in the current saga; in every saga expanded (try each saga's first song, width 980, measure = 7.2px/char) no two labelled stops overlap; collapse sets `timeline=closed` → implement → pass → commit `feat(app): bottom timeline`.
### Task 6: The 3D world, landings and friezes