feat(app): app page at /, mockup gallery moved to /mockups/

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 04:50:56 +01:00
co-authored by Claude Opus 5.5
parent c11c2ef079
commit 3235df8bea
10 changed files with 53 additions and 12 deletions
+2 -2
View File
@@ -27,7 +27,7 @@ bun dist/server.js # wrong: crashes with 'Bundled file "./index-….js" no
and the `../../shared/credits.css` link in the page's HTML, and call `mountCredits()` from
`src/shared/credits.ts`. Never write your own notice text.
- Mockup agents edit only their own `src/mockups/NN-slug/` folder. `src/data/`, `src/shared/`,
`src/server.ts`, `src/index.html` and `src/mockups/registry.ts` are the shared contract; change them
`src/server.ts`, `src/mockups/index.html` and `src/mockups/registry.ts` are the shared contract; change them
only when the plan says so.
- No two mockups may have byte-identical `style.css` — the build fails with
"Multiple files share the same output path". Give each one its own styles from the start.
@@ -38,7 +38,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 every registry slug is served and that the index shows each entry's name, pitch and tryThis.
`src/mockups/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
+2 -2
View File
@@ -12,7 +12,7 @@ shared credits snippet.
| Path | Responsibility |
|---|---|
| `src/server.ts` | `startServer(port, { contentDir? })`: routes `/`, `/mockups/NN-slug/` (+ 302 from the no-slash form), `/content/*` via `src/content/routes.ts`, 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/index.html`, `index.ts`, `index.css` | Mockup gallery at `/mockups/`: 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`, plus optional helper modules (e.g. `scene.ts`, `layout.ts`). |
| `src/data/types.ts` | `Saga`, `Song`, `Part`, `Motif`, `StoryTime`, `OdysseyRef`, `Video`. |
@@ -43,7 +43,7 @@ the site works and `/content/*` answers 404. Otherwise none. Fonts (`@fontsource
## Invariants
- Every page renders `FAN_NOTICE` via `mountCredits()`.
- `src/server.ts` routes, `src/mockups/registry.ts` and the cards in `src/index.html` list the same slugs
- `src/server.ts` routes, `src/mockups/registry.ts` and the cards in `src/mockups/index.html` list the same slugs
(`tests/server.test.ts`).
- Every motif id used by a part exists; every motif appears in at least one song (`tests/data.test.ts`).
- No two mockups ship byte-identical `style.css` (the build fails otherwise).
+19
View File
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EPIC: The Musical Timeline — an unofficial fan voyage</title>
<link rel="stylesheet" href="../shared/credits.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main id="voyage-app">
<section id="stage" aria-label="The voyage"></section>
<aside id="panel" aria-label="Song details"></aside>
<nav id="timeline" aria-label="Songs and motif threads"></nav>
</main>
<footer data-fan-credits></footer>
<script type="module" src="./main.ts"></script>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
import { mountCredits } from "../shared/credits";
mountCredits();
+2
View File
@@ -0,0 +1,2 @@
/* The voyage app (spec 002). Tokens and layout are filled in by the plan's tasks. */
body { margin: 0; background: #1d1712; }
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EPIC: The Musical Timeline — design mockups</title>
<link rel="stylesheet" href="./index.css" />
<link rel="stylesheet" href="./shared/credits.css" />
<link rel="stylesheet" href="../shared/credits.css" />
</head>
<body>
<main>
+1 -1
View File
@@ -1,4 +1,4 @@
import { mountCredits } from "./shared/credits";
import { mountCredits } from "../shared/credits";
// Cards are static in index.html (tests/server.test.ts checks each registry entry's name, pitch and tryThis appear here).
mountCredits();
+5 -2
View File
@@ -1,6 +1,7 @@
import type { Server } from "bun";
import { contentResponse } from "./content/routes";
import home from "./index.html";
import app from "./app/index.html";
import gallery from "./mockups/index.html";
import m0 from "./mockups/01-editorial/index.html";
import m1 from "./mockups/02-vertical-story/index.html";
import m2 from "./mockups/03-transit-map/index.html";
@@ -29,7 +30,9 @@ export function startServer(port: number, opts: ServerOptions = {}): Server<unde
return Bun.serve({
port,
routes: {
"/": home,
"/": app,
"/mockups/": gallery,
"/mockups": Response.redirect("/mockups/", 302),
"/mockups/01-editorial/": m0,
"/mockups/01-editorial": Response.redirect("/mockups/01-editorial/", 302),
"/mockups/02-vertical-story/": m1,
+18 -4
View File
@@ -10,15 +10,15 @@ afterAll(() => server.stop(true));
test("registry has 17 mockups", () => expect(mockups).toHaveLength(17));
test("index links every mockup", async () => {
const res = await fetch(base + "/");
test("gallery links every mockup", async () => {
const res = await fetch(base + "/mockups/");
expect(res.status).toBe(200);
const html = await res.text();
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("&amp;", "&");
test("gallery shows every registry name, pitch and tryThis", async () => {
const html = (await (await fetch(base + "/mockups/")).text()).replaceAll("&amp;", "&");
for (const m of mockups) for (const text of [m.name, m.pitch, m.tryThis]) expect(html).toContain(text);
});
@@ -34,3 +34,17 @@ for (const m of mockups) {
else expect(res.status).toBe(200);
});
}
test("the app is served at / with the credits hook", async () => {
const res = await fetch(base + "/");
expect(res.status).toBe(200);
const html = await res.text();
expect(html).toContain('id="voyage-app"');
expect(html).toContain("data-fan-credits");
});
test("/mockups redirects to the gallery", async () => {
const res = await fetch(base + "/mockups", { redirect: "manual" });
expect(res.status).toBe(302);
expect(res.headers.get("location")).toContain("/mockups/");
});