diff --git a/AGENTS.md b/AGENTS.md index fd92b2d..9e16996 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - Pictures: `image` 0.25 (MIT OR Apache-2.0), the crate gpui decodes with, default features off; it turns decoded frames and rims map tiles. - Maps: Natural Earth's 110m countries (public domain). `assets/maps/countries.geojson`, trimmed by `scripts/countries.py` to an ISO code, a name and positions to 0.01°, backs `maps::WorldMap`; the gallery's tiles are cut from the same file, z0 to z3, by `scripts/tiles.py` with Pillow. - Web: on wasm32, `web-time` 1.1 (MIT OR Apache-2.0) gives the `Instant` gpui's executor returns, std's own natively; jiff takes `js`, rust-embed `debug-embed`, similar `wasm32_web_time`. The gallery's web entry takes `gpui_web`, `wasm-bindgen`, `js-sys` and `web-sys` as wasm-only dev-dependencies, and a Hebrew cut of Noto Sans Hebrew 2.003 (OFL, `examples/gallery/fonts`): gpui_web asks the browser for emoji and CJK alone. The `wasm-bindgen` CLI at Cargo.lock's version and binaryen's `wasm-opt` finish the bundle. -- Gallery: `examples/gallery`. Website: `frontend/` (Vite 8.3, pnpm 11, TypeScript, React 19.3, TanStack Router 1.170 in code-based routes, Lenis 1.3), after `frontend-reference/`: heavy titles over thin faint text, hairlines, one accent, full-screen sections, isometric line miniatures on a canvas. Each story runs live from the gallery built for wasm32 through gpui_web, in a same-origin iframe, or, where a browser cannot run it, as a native capture. Geist and Geist Mono (OFL, `@fontsource-variable`), Reicon icons (MIT, based on Solar Icons, CC BY 4.0, credited in the footer), simple-icons' GitHub mark (CC0). It deploys to Cloudflare Workers static assets through `npx wrangler` (4.143). +- Gallery: `examples/gallery`. Website: `frontend/` (Vite 8.3, pnpm 11, TypeScript, React 19.3, TanStack Router 1.170 in code-based routes, Lenis 1.3), after `frontend-reference/`: heavy titles over thin faint text, hairlines, one accent, full-screen sections, isometric line miniatures on a canvas. Pages name a story by its first component alone (`src/data.ts`'s `name`); scroll bars stay hidden; layouts step at 1024 and 768. Each story runs live from the gallery built for wasm32 through gpui_web, in a same-origin iframe, or, where a browser cannot run it, as a native capture. Geist and Geist Mono (OFL, `@fontsource-variable`), Reicon icons (MIT, based on Solar Icons, CC BY 4.0, credited in the footer), simple-icons' GitHub mark (CC0). It deploys to Cloudflare Workers static assets through `npx wrangler` (4.143). - Reference site: `frontend-reference/` (Vite 8.3, React 19, TanStack Router 1.170 in code-based routes, `lottie-web` 5.13, pnpm 11). A 1:1 study copy of serro.ai: the SSR DOM and the site's own CSS ported verbatim, Framer custom components replayed from runtime DOM captured at five widths (`RuntimeVariant`), 17 Lottie instances on the original JSONs, and the mock's feed sequence, ROI count-up and hover pills reimplemented from measured timings. Real fonts and assets served locally. Not deployed. `NOTES.md` there documents the pipeline under `RECON/tools/`. ## Commands diff --git a/TASKS.md b/TASKS.md index 4c8001d..a678dd7 100644 --- a/TASKS.md +++ b/TASKS.md @@ -161,6 +161,7 @@ Per-component lines live in `tasks/`. Tags there: - [x] T45d Site · Components: the chapter index and the story page redesigned; live stories without frames, native captures for what a browser cannot run, search, keys - [x] T45e Site · Local: the gallery bundle built for localhost and the site served for the owner to see; not published - [x] T46 E2E: Playwright against the built site +- [ ] T46a Site · Owner's notes: the city title whole, header icons on one line, less space above a story, no visible scroll bars, each story named by its first component, every page laid out at phone, tablet and desktop widths - [ ] T47 Ship: GitHub repo (public), CI, Cloudflare deploy through `npx wrangler` - [ ] T48 Acceptance: live URL and MVP checklist diff --git a/frontend/e2e/site.spec.ts b/frontend/e2e/site.spec.ts index a7e2a1c..9350206 100644 --- a/frontend/e2e/site.spec.ts +++ b/frontend/e2e/site.spec.ts @@ -1,6 +1,9 @@ import { expect, test, type Page } from "@playwright/test"; import manifest from "../../examples/gallery/stories.json" with { type: "json" }; +/** A story's short name, as `src/data.ts` derives it. */ +const name = (title: string) => title.split(/ \/ | · | → | \(/)[0].trim(); + /** Collects console errors and uncaught exceptions. */ function watch(page: Page): string[] { const errors: string[] = []; @@ -61,12 +64,12 @@ test("a chapter opens its first story, and the pager and arrows walk on", async const [chapter] = manifest.pages; await page.goto(`/components/${chapter.slug}/`); await expect(page).toHaveURL(`/components/${chapter.slug}/${chapter.stories[0].slug}/`); - await expect(page.getByRole("heading", { level: 1 })).toHaveText(chapter.stories[0].title); + await expect(page.getByRole("heading", { level: 1 })).toHaveText(name(chapter.stories[0].title)); await page.locator(".pager a.next").click(); - await expect(page.getByRole("heading", { level: 1 })).toHaveText(chapter.stories[1].title); + await expect(page.getByRole("heading", { level: 1 })).toHaveText(name(chapter.stories[1].title)); if (isMobile) return; await page.keyboard.press("ArrowLeft"); - await expect(page.getByRole("heading", { level: 1 })).toHaveText(chapter.stories[0].title); + await expect(page.getByRole("heading", { level: 1 })).toHaveText(name(chapter.stories[0].title)); }); test("the rail marks the current story", async ({ page, isMobile }) => { @@ -74,7 +77,7 @@ test("the rail marks the current story", async ({ page, isMobile }) => { const chapter = manifest.pages[manifest.pages.length - 1]; const story = chapter.stories[chapter.stories.length - 1]; await page.goto(`/components/${chapter.slug}/${story.slug}/`); - await expect(page.locator('.side li [aria-current="page"]')).toHaveText(story.title); + await expect(page.locator('.side li [aria-current="page"]')).toHaveText(name(story.title)); await expect(page.locator('.side li [aria-current="page"]')).toBeInViewport(); }); diff --git a/frontend/src/data.ts b/frontend/src/data.ts index ef41b8e..45c33e1 100644 --- a/frontend/src/data.ts +++ b/frontend/src/data.ts @@ -2,6 +2,8 @@ import manifest from "../../examples/gallery/stories.json"; export interface Story { title: string; + /** The first component alone, as pages show it. */ + name: string; slug: string; components: string[]; } @@ -14,7 +16,10 @@ export interface Chapter { stories: Story[]; } -export const chapters: Chapter[] = manifest.pages; +export const chapters: Chapter[] = manifest.pages.map((c) => ({ + ...c, + stories: c.stories.map((s) => ({ ...s, name: s.title.split(/ \/ | · | → | \(/)[0].trim() })), +})); export const componentCount = new Set(chapters.flatMap((c) => c.stories.flatMap((s) => s.components))).size; diff --git a/frontend/src/icons.tsx b/frontend/src/icons.tsx index bdd2447..f674465 100644 --- a/frontend/src/icons.tsx +++ b/frontend/src/icons.tsx @@ -9,9 +9,9 @@ import VolumeCross from "reicon/icons/VolumeCross"; const set = { ArrowRightUp, Maximize, Minimize, Moon, Refresh, Sound, Sun, VolumeCross }; -// simple-icons' GitHub mark (CC0); Reicon has none. +// simple-icons' GitHub mark (CC0), padded to 18px in 24. const GITHUB = - ''; + ''; export type IconName = keyof typeof set | "GitHub"; diff --git a/frontend/src/pages/Chapters.tsx b/frontend/src/pages/Chapters.tsx index 44b176d..5455dd0 100644 --- a/frontend/src/pages/Chapters.tsx +++ b/frontend/src/pages/Chapters.tsx @@ -26,7 +26,7 @@ export function Chapters() { {c.stories.map((s) => ( - {s.title} + {s.name} ))} diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 210770a..4916b56 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -18,7 +18,7 @@ const FEATURES = [ ].map((f) => { const story = chapters.find((c) => c.slug === f.page)?.stories.find((s) => s.slug === f.story); if (!story) throw new Error(`home: no story ${f.page}/${f.story}`); - return { ...f, name: story.title }; + return { ...f, name: story.name }; }); /** Mounts its children while near the view or holding focus. */ diff --git a/frontend/src/pages/Story.tsx b/frontend/src/pages/Story.tsx index 91368e2..d3efd5c 100644 --- a/frontend/src/pages/Story.tsx +++ b/frontend/src/pages/Story.tsx @@ -16,7 +16,7 @@ export function Story() { const [prev, next] = at < 0 ? [] : [all[at - 1], all[at + 1]]; useEffect(() => { - if (chapter && found) document.title = `${found.title} · ${chapter.title} · Ely`; + if (chapter && found) document.title = `${found.name} · ${chapter.title} · Ely`; }, [chapter, found]); useEffect(() => { @@ -51,7 +51,7 @@ export function Story() { {c.stories.map((s) => (