T46a Site · Owner's notes: short story names, aligned icons, a whole city title, tighter story tops, hidden scroll bars, a tablet step at 1024
This commit is contained in:
1 parent
9079b21bdb
commit
ac1a869fb5
9 files changed
+45
-29
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 =
|
||||
'<svg viewBox="0 0 24 24" width="18" height="18" style="margin:3px" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>';
|
||||
'<svg viewBox="-3 -3 30 30" width="24" height="24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>';
|
||||
|
||||
export type IconName = keyof typeof set | "GitHub";
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export function Chapters() {
|
||||
<span className="titles">
|
||||
{c.stories.map((s) => (
|
||||
<Link key={s.slug} to="/components/$page/$story/" params={{ page: c.slug, story: s.slug }}>
|
||||
{s.title}
|
||||
{s.name}
|
||||
</Link>
|
||||
))}
|
||||
</span>
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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) => (
|
||||
<li key={s.slug}>
|
||||
<Link to="/components/$page/$story/" params={{ page: c.slug, story: s.slug }} activeProps={{ "aria-current": "page" }}>
|
||||
{s.title}
|
||||
{s.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
@@ -65,24 +65,24 @@ export function Story() {
|
||||
<Link to="/components/" className="crumb">
|
||||
{chapter.title}
|
||||
</Link>
|
||||
<h1>{found.title}</h1>
|
||||
<h1>{found.name}</h1>
|
||||
{isCaptured(chapter.slug, found.slug) ? (
|
||||
<Capture page={chapter.slug} story={found.slug} title={found.title} />
|
||||
<Capture page={chapter.slug} story={found.slug} title={found.name} />
|
||||
) : (
|
||||
<>
|
||||
<Live key={`${chapter.slug}/${found.slug}`} page={chapter.slug} story={found.slug} title={found.title} />
|
||||
<Windows page={chapter.slug} story={found.slug} title={found.title} />
|
||||
<Live key={`${chapter.slug}/${found.slug}`} page={chapter.slug} story={found.slug} title={found.name} />
|
||||
<Windows page={chapter.slug} story={found.slug} title={found.name} />
|
||||
</>
|
||||
)}
|
||||
<nav className="pager" aria-label="More stories">
|
||||
{prev && (
|
||||
<Link to="/components/$page/$story/" params={{ page: prev.chapter.slug, story: prev.story.slug }}>
|
||||
← {prev.story.title}
|
||||
← {prev.story.name}
|
||||
</Link>
|
||||
)}
|
||||
{next && (
|
||||
<Link className="next" to="/components/$page/$story/" params={{ page: next.chapter.slug, story: next.story.slug }}>
|
||||
{next.story.title} →
|
||||
{next.story.name} →
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
+18
-11
@@ -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; }
|
||||
|
||||
Reference in new issue
Block a user