From a196c09b821eef529478e148238c6433f4b54eeb Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 30 Oct 2026 15:12:53 +0100 Subject: [PATCH] docs: the voyage app in DESIGN, PRODUCT, AGENTS, ARCHITECTURE, DECISIONS, TODO Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 16 ++++++---- DESIGN.md | 71 ++++++++++++++++++++++++++++++-------------- PRODUCT.md | 16 +++++----- docs/ARCHITECTURE.md | 34 +++++++++++++++------ docs/DECISIONS.md | 15 ++++++++++ docs/TODO.md | 10 +++++++ 6 files changed, 117 insertions(+), 45 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f339cd7..594b591 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,8 @@ # EPIC: The Musical Timeline -Unofficial fan site: an interactive timeline of *EPIC: The Musical* songs. Currently 7 design-direction -mockups. Bun 1.3.14 (server, bundler, test runner), TypeScript, three.js (mockups 06/07/13–17 only), Docker. +Unofficial fan site: an interactive timeline of *EPIC: The Musical* songs as a 3D sea voyage (the app at +`/`, `src/app/`), plus the design-round mockups at `/mockups/`. Bun 1.3.14 (server, bundler, test runner), +TypeScript, three.js (the app; mockups 06/07/13–17), Docker. `AGENTS.md` is the single source of truth for agents here. `CLAUDE.md` and `GEMINI.md` only point here. Deeper context on demand: `docs/ARCHITECTURE.md`, `docs/DECISIONS.md`, `docs/TODO.md`. @@ -33,7 +34,7 @@ bun dist/server.js # wrong: crashes with 'Bundled file "./index-….js" no "Multiple files share the same output path". Give each one its own styles from the start. - Real audio, lyrics and cover art are Jorge Rivera-Herrans' copyrighted work: they live only in the mounted content folder (`CONTENT_DIR`, served under `/content/`), never in git or the Docker image. -- Sample data (`src/data/sample.ts`): lyrics are placeholders, never real lyrics. Any fact, story time +- Album data (`src/data/album/`) and sample data (`src/data/sample.ts`): never real lyrics. Any fact, story time or Odyssey reference you can't confirm gets `// TODO(verify)`. - Assets are bundled, never loaded from a CDN: fonts from `@fontsource/*`, three.js from npm. YouTube videos are link cards (`createVideoCards`), not embeds. @@ -52,7 +53,7 @@ Tick plan checkboxes as you go so an interrupted session is resumable. Trivial changes (typo, version bump, one-line config): say "trivial, skipping spec" and proceed. Use impeccable for user-facing UI — `craft` for new, `audit`/`polish` on existing, `harden` before -shipping. Design context: `PRODUCT.md`, `DESIGN.md`. No visual direction is chosen yet (see `DESIGN.md`). +shipping. Design context: `PRODUCT.md`, `DESIGN.md` (the chosen direction: tokens, type, layout, 3D style). Mockup rounds — any UI change beyond a trivial tweak, while brainstorming and before the spec: - Round 1: about three genuinely different alternatives, each a clickable mockup with realistic @@ -68,8 +69,11 @@ Mockup rounds — any UI change beyond a trivial tweak, while brainstorming and ## Architecture -`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/server.ts` -> `Bun.serve` with HTML-import routes: `/` (the app), `/mockups/` (gallery), `/mockups/NN-slug/` +`src/app/` -> the voyage app: `state.ts` (URL state), `controller.ts` + `voyage.ts` (scene changes), + `world/` (three.js world, landings, ribbons), `frieze/` (friezes, curtain), `panel/`, `timeline/`, `content.ts` +`src/data/album/` -> the app's data: 9 sagas, 40 songs, motifs (`// TODO(verify)` where unconfirmed) +`src/data/` (other files) -> the mockups' shapes and sample data; frozen `src/shared/` -> credits, audio slot (direct MP3, empty state), YouTube link cards, LRC parser `src/content/` -> `/content/*` routes over the mounted content folder: library.json, files (ranges), Homer books `src/mockups/NN-slug/` -> one design direction each: `index.html`, `main.ts`, `style.css`, plus optional helper modules (e.g. `scene.ts`, `layout.ts`) diff --git a/DESIGN.md b/DESIGN.md index 585b7f1..4238876 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,29 +1,54 @@ # Design -## Status: visual direction open -No design system, palette or type scale is chosen yet. Seven mockups under `src/mockups/` each commit fully -to a different direction; the maintainer will pick one, or combine several, and this file then records the -chosen direction as tokens and rules. Until then, do not treat any single mockup's palette or fonts as the -house style. +## Status: direction chosen (2026-10-30) +The app at `/` (`src/app/`) is the house style. Its record, with every chosen and deselected idea and the +user's words: `docs/design/002-voyage-app.md`. The mockups under `src/mockups/` are history; don't take +styling from the dropped ones. New options vary structure, flow and behaviour within this direction — +never a new palette or font family. -## The candidate directions -| # | Folder | Direction | +## The direction: "two worlds" (look 2) +A stylised 3D sea voyage, bright and naturalistic in colour, framed by a Greek black-figure frieze that +drops like a curtain between scenes. The interface sits on parchment and borrows the frieze's terracotta, +black and meander band so that frieze, world and panels read as one piece. + +## Tokens (`src/app/style.css` `:root`) +| Token | Value | Use | |---|---|---| -| 1 | `01-editorial` | Editorial scroll: horizontal line, parchment and serif, motifs as thin threads | -| 2 | `02-vertical-story` | Vertical story: downward scroll with a sticky detail panel | -| 3 | `03-transit-map` | Transit map: motifs as metro lines, songs as stations | -| 4 | `04-greek-frieze` | Greek frieze: an unrolled black-figure vase frieze | -| 5 | `05-score` | Score / waveform: staff or waveform, parts as segments | -| 6 | `06-constellation` | Constellation (3D, three.js): songs as stars | -| 7 | `07-sea-voyage` | Sea voyage (3D, three.js): ship, sea, saga islands | +| `--clay` | `#c46a33` | frieze ground; accents, active tab, play button | +| `--clay-deep` | `#9a4520` | eyebrows, hover, small accent text | +| `--glaze` | `#1d140e` | frieze figures; rules, borders, dark buttons, page frame | +| `--added-white` | `#efe3c8` | text on glaze | +| `--vellum` / `--vellum-hi` / `--vellum-lo` | `#e3d6b4` / `#ece2c6` / `#e0d1ab` | panel, cards, timeline | +| `--iron` / `--iron-soft` | `#2b2118` / `#5a4836` | body text / secondary text | +| `--line` | `rgb(43 33 24 / 0.2)` | hairlines | +| `--meander` | SVG data URI | the key band: 14px strip between two 2px glaze rules (`.meander-edge`) on top of every panel, card and bar | -Details per direction: `docs/specs/001-timeline-mockups.md`. +Motif colours come from the data (`src/data/album/motifs.ts`) and are the same everywhere: timeline, +panel, ribbons and chips. On parchment, text in a motif colour is darkened to 4.5:1 +(`src/app/timeline/ink.ts`); dots and knots keep the bright colour. -## Constraints that hold for every direction -- Fonts are self-hosted from `@fontsource/*` (installed: Cormorant Garamond, Inter); nothing from a CDN. -- three.js only where a direction is 3D. -- Motif colours come from the data (`Motif.color` in `src/data/sample.ts`), so threads match across views. -- The fan-credits footer (`src/shared/credits.css`) is present on every page; a direction may restyle it - but not remove or hide it. -- Avoid generic, templated "AI-looking" UI; a direction that is just another layout in different colours - has failed. +## Type +- **Inter** for all UI, labels and controls: 13px minimum (`--fs-label`), 14px small, 15px panel body, + weights 400–600, tracking ≤0.06em. Never small wide-tracked capitals (they were "very hard to read"). +- **Cormorant Garamond** only for large display titles (`--fs-title` 2rem), lyric lines, Odyssey reading + text, and frieze inscriptions (≥19px on screen). + +## Layout +- Desktop reference 1440×900: stage (3D canvas + frieze layer + caption card + bottom timeline) | right + panel 420px (collapsible to a 56px tab) | credits footer across the bottom. +- Timeline ≤190px tall by default; the current saga expanded with titles, the others compact. +- Below 900px: the panel becomes a bottom sheet; the timeline stays over the stage. + +## 3D +Stylised, not realistic: toon/flat shading, ink outlines, flat colour-band water with drawn wave strokes, +a small flat sun, graphic silhouettes. Black-figure cut-outs (the giant, Athena) inside landings. +Pixel ratio ≤2; distant places are not drawn. + +## Motion +Curtain drop ≈1.2s, parting lift ≈1.5s, doorway zoom ≈1.6s, fade ≈0.45/0.65s; ease-out curves. +`prefers-reduced-motion`: every scene change is a 200ms crossfade. + +## Constraints +- Fonts self-hosted from `@fontsource/*`; three.js from npm; nothing from a CDN. +- The fan-credits footer is on every page and never hidden. +- Avoid generic, templated "AI-looking" UI. diff --git a/PRODUCT.md b/PRODUCT.md index 94e0b69..37e7b29 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -19,16 +19,18 @@ recur across the songs and how the story maps onto the *Odyssey*. Desktop first (`src/shared/credits.ts`), never retyped. - **Accuracy is visible:** content that isn't verified is marked as such (`// TODO(verify)` in data) and never presented as fact-checked. -- **Lyrics:** mockups use placeholder lyric lines only. Whether and how real lyrics are shown is an open - copyright question, deferred by the maintainer. -- **Media:** videos are YouTube link cards (no embedded player); audio supports direct MP3 URLs. The app - works offline apart from following those links. +- **Lyrics and audio:** real recordings, lyrics and cover art come only from a content folder mounted at + runtime (spec 003); they are never committed or baked into the image. The broader copyright question is + deferred by the maintainer. +- **Media:** videos are YouTube link cards (no embedded player). The app works without the content folder + and offline apart from following those links. ## Tone Polished and smooth; may use 3D (three.js). It should feel like a crafted piece made by a fan, not a generic template — see `DESIGN.md`. ## Current stage -Spec `docs/specs/001-timeline-mockups.md`: 7 design-direction mockups over shared sample data (Troy and -Cyclops sagas plus later callback songs). The maintainer picks or combines a direction; the real app gets -its own spec after that. +v1 of the app (spec `docs/specs/002-voyage-app.md`): the whole album, 9 sagas and 40 songs, as a stylised +3D sea voyage with the frieze as a curtain between scenes, three landings, a side panel and a bottom +timeline of motif threads. The design rounds that led here (mockups 01–17) are recorded in +`docs/design/002-direction-round-notes.md`. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 127f156..e3c16e0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -3,10 +3,12 @@ How `epic-the-musical-timeline` is built today. Update in the same commit as any structural change. ## Overview -A Bun server (`Bun.serve`) with HTML-import routes serves an index page and seven mockup pages. Bun's bundler -turns each HTML page and its imported TS/CSS into hashed assets. `bun run build` bundles the server and every -page into `dist/`; the Docker image runs that bundle. All pages read one typed sample dataset and mount one -shared credits snippet. +A Bun server (`Bun.serve`) with HTML-import routes serves the voyage app at `/`, the mockup gallery at +`/mockups/` and the design-round mockups at `/mockups/NN-slug/`, plus `/content/*` from an optional mounted +content folder. Bun's bundler turns each HTML page and its imported TS/CSS into hashed assets. `bun run +build` bundles the server and every page into `dist/`; the Docker image runs that bundle. The app reads the +album data (`src/data/album/`); the mockups read the frozen sample dataset. Every page mounts one shared +credits snippet. ## Map | Path | Responsibility | @@ -26,18 +28,30 @@ shared credits snippet. | `src/content/library.ts` | `scanLibrary(dir)`: sagas (story order) and songs from `Songs and Lyrics/EPIC - ()/`; `.lrc` preferred over `.txt`. | | `src/content/homer.ts` | `splitBooks(text)`: Gutenberg Butler text → books (heading, summary, paragraphs). | | `src/shared/lrc.ts` | `parseLrc(text)`: timed lyric lines, sorted. | -| `tests/` | `data.test.ts` (dataset invariants), `shared.test.ts` (DOM modules under happy-dom), `server.test.ts` (every route 200s, index links every registry slug, each mockup has the credits hook), `content.test.ts` (LRC, library scan, Homer split, `/content/*` routes on a temp fixture). | +| `src/app/index.html`, `main.ts`, `style.css` | The app page: stage (canvas, frieze layer, caption, timeline) and panel. `main.ts` wires store → controller → world/curtain, panel, timeline, play/prev/next, keyboard (←/→, Esc), the no-WebGL frieze stage. `style.css` holds the design tokens (see `DESIGN.md`). | +| `src/app/state.ts` | `AppState` (`songId`, `tab`, `motif`, `timeline`, `ribbons`, `read`), `parseState`/`stateToSearch` (URL), `createStore` (push on song change, replace otherwise, `popstate` restores). | +| `src/app/voyage.ts` | `transitionFor(from, to)`: same saga → sail; adjacent saga → that saga's curtain; further → fade; plus the target's landing. | +| `src/app/controller.ts` | Runs steps one at a time, last request wins, a cancelled step's curtain is lifted by the next. Order under a curtain: cover → sail → landing → horizon on the frieze ground line → lift. Play voyage (3.4s per song, silent), Back to sea. | +| `src/app/world/` | `createWorld(canvas)` (null without WebGL 2): `route.ts` (9 places, 40 stops, pure), stylised islands, ship, ribbons ("threads on the sea"), `landings/` cave, arena, hall. | +| `src/app/frieze/` | `friezeSvg(sagaId)` for the 9 sagas from a figure kit; `createCurtain(layer)`: drop, lift (part along the ground line, or zoom through a doorway), fade, `groundY()`. | +| `src/app/panel/` | `mountPanel`: About (facts, Odyssey reading view), Listen & lyrics (real player, line-synced lyrics, Watch), Paths (strip when the timeline is hidden, motif rows, nine-place chart), Fan works. | +| `src/app/timeline/` | `layoutTimeline` (pure: current saga expanded, others compact) and `mountTimeline`: stops, you-are-here, threads, hide/show, "Threads on the sea". | +| `src/app/content.ts` | Client for `/content/*`: library match by normalised title, lyrics (`.lrc` timed / `.txt`), Homer books, passage start by keyword. | +| `src/data/album/` | `sagas`, `songs` (40, album order, `landing`), `motifs` (character themes derived from singers, except Odysseus), `fanWorks` (empty in v1), query helpers. | +| `tests/` | `data.test.ts` (dataset invariants), `shared.test.ts` (DOM modules under happy-dom), `server.test.ts` (every route 200s, index links every registry slug, each mockup has the credits hook), `content.test.ts` (LRC, library scan, Homer split, `/content/*` routes on a temp fixture), `album.test.ts`, `state.test.ts`, `voyage.test.ts` (transitions, controller with fakes), `route.test.ts`, `frieze.test.ts`, `timeline.test.ts`, `panel.test.ts`, `app-content.test.ts`. | | `Dockerfile` | Two-stage `oven/bun:1.3`: install + build, then copy `dist/` and run from `WORKDIR /app/dist`. | ## Data flow 1. Request hits `Bun.serve`; an HTML-import route returns the bundled page (dev: bundled on demand with HMR via `bun --hot`; production: prebuilt assets from `dist/`). -2. The page's `main.ts` imports `src/data/*` and `src/shared/*` (bundled in; no runtime API). -3. `main.ts` renders the direction and calls `mountCredits()` into `