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) => (
  • - {s.title} + {s.name}
  • ))} @@ -65,24 +65,24 @@ export function Story() { {chapter.title} -

    {found.title}

    +

    {found.name}

    {isCaptured(chapter.slug, found.slug) ? ( - + ) : ( <> - - + + )} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d428b83..e0148a2 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -28,7 +28,8 @@ color-scheme: dark; } -* { box-sizing: border-box; } +* { box-sizing: border-box; scrollbar-width: none; } +*::-webkit-scrollbar { display: none; } html, body { margin: 0; background: var(--bg); color: var(--fg); } @@ -97,7 +98,7 @@ main { min-height: 100vh; } .tally p > span:first-child { font-size: clamp(64px, 11vw, 176px); line-height: 0.9; font-weight: 800; letter-spacing: -0.05em; color: var(--fg); font-variant-numeric: tabular-nums; } .city { height: 100vh; min-height: 720px; display: grid; grid-template-columns: 7fr 5fr; grid-template-rows: auto 1fr; gap: 48px 64px; padding: 160px 64px 96px; } -.city h2 { grid-column: 1 / 3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.city h2 { grid-column: 1 / 3; } .city > * { min-width: 0; } .city-iso { width: 100%; height: 100%; min-height: 420px; } .city-names { list-style: none; margin: 0; padding: 0; columns: 3; column-gap: 24px; font-size: 13px; line-height: 24px; align-self: end; } @@ -130,14 +131,14 @@ main > :first-child { animation: enter 480ms ease-out both; } .titles a:hover, .side a:hover, .side a[aria-current="page"], .side-chapter[data-on] { color: var(--fg); } .reader { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 64px; padding: 0 64px; } -.side { position: sticky; top: 64px; height: calc(100vh - 64px); overflow-y: auto; padding: 64px 8px 64px 0; overscroll-behavior: contain; scroll-padding-block: 64px; } +.side { position: sticky; top: 64px; height: calc(100vh - 64px); overflow-y: auto; padding: 32px 8px 64px 0; overscroll-behavior: contain; scroll-padding-block: 64px; } .side nav > div { margin-bottom: 4px; } .side-chapter { display: block; padding: 4px 0; font-size: 15px; font-weight: 500; } .side ol { list-style: none; margin: 4px 0 12px; padding: 0 0 0 14px; border-left: 1px solid var(--line); font-size: 13px; line-height: 20px; } .side li { padding: 4px 0; } -.story { padding: 128px 0 96px; min-width: 0; } +.story { padding: 96px 0 96px; min-width: 0; } .crumb { font-size: 14px; color: var(--muted); } -.story h1 { font-size: clamp(28px, 3vw, 44px); line-height: 1.05; letter-spacing: -0.035em; font-weight: 800; margin: 12px 0 64px; overflow-wrap: anywhere; } +.story h1 { font-size: clamp(28px, 3vw, 44px); line-height: 1.05; letter-spacing: -0.035em; font-weight: 800; margin: 8px 0 32px; overflow-wrap: anywhere; } .live { position: relative; height: 72vh; min-height: 520px; background: var(--bg); } .live iframe { display: block; width: 100%; height: 100%; border: 0; background: var(--bg); } @@ -167,25 +168,31 @@ img.shot { display: block; max-width: 100%; height: auto; } .foot { display: flex; flex-wrap: wrap; gap: 8px 28px; padding: 48px 64px; font-size: 13px; color: var(--faint); } .foot a { text-decoration: underline; text-underline-offset: 3px; } +@media (max-width: 1023px) { + .reader { grid-template-columns: minmax(0, 1fr); } + .side { display: none; } + .feature, .feature:nth-of-type(odd), .city { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; gap: 32px; } + .feature-words, .feature-iso, .feature-live, .feature:nth-of-type(odd) .feature-words, .feature:nth-of-type(odd) .feature-iso, .feature:nth-of-type(odd) .feature-live, .city h2 { grid-area: auto; } + .feature-iso { min-height: 280px; } + .feature-live { height: 70vh; min-height: 420px; } + .city { height: auto; } +} + @media (max-width: 767px) { .bar { padding: 0 16px; gap: 12px; } .bar .end { gap: 0; } .hero, .chapters, .missing { padding-left: 20px; padding-right: 20px; } - .reader { grid-template-columns: minmax(0, 1fr); padding: 0 20px; } - .side { display: none; } + .reader { padding: 0 20px; } .chapters li { grid-template-columns: 40px minmax(0, 1fr); } .chapters .titles { grid-column: 2; margin-top: 8px; } .foot { padding: 32px 20px; } .chapters .name { font-size: 22px; } .hero-iso { width: 100%; height: 55%; } .hero > :not(.iso) { max-width: none; } - .city { height: auto; } .hero h1, .chapters h1 { font-size: 13vw; } .feature h2, .city h2, .close h2 { font-size: 10vw; } - .feature, .feature:nth-of-type(odd), .city { grid-template-columns: 1fr; grid-template-rows: none; gap: 32px; padding: 112px 20px 48px; } - .feature-words, .feature-iso, .feature-live, .feature:nth-of-type(odd) .feature-words, .feature:nth-of-type(odd) .feature-iso, .feature:nth-of-type(odd) .feature-live, .city h2 { grid-area: auto; } + .feature, .city { padding: 112px 20px 48px; } .feature-iso { min-height: 220px; } - .feature-live { height: 70vh; min-height: 420px; } .tally { grid-template-columns: 1fr; padding: 112px 20px; } .city-iso { min-height: 260px; } .city-names { columns: 2; }