diff --git a/AGENTS.md b/AGENTS.md index b681fa8..71a9668 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), in serro.ai's language: heavy titles over thin faint text, hairlines, one accent, full-screen sections, isometric line miniatures on a canvas. Pages name a story by its title's first part (`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. The Docs tab is Markdown in `frontend/src/docs/`, read in its `ORDER` and rendered by `marked` 18 (MIT) inside `vite.config.ts`'s `docs` plugin, so a bad guide fails `pnpm build` and the browser gets HTML alone; a fenced block tagged `example=NAME` shows `examples/docs/NAME.rs`, an example `scripts/check.sh` compiles as `docs_NAME`, so every full Rust sample on the site builds. The Showcase tab lists apps built with Ely from the public repo `ZacharyZhang-NY/Ely-GPUI-Showcases`: manifests, PNG previews and Markdown READMEs, checked by its `scripts/catalog.mjs` (`marked`, `pngjs`, `twitter-text`), its stars refreshed by its CI. Below 414px the header drops the sound switch. 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), in serro.ai's language: heavy titles over thin faint text, hairlines, one accent, full-screen sections, isometric line miniatures on a canvas. Pages name a story by its title's first part (`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. The Docs tab is Markdown in `frontend/src/docs/`, read in its `ORDER` and rendered by `marked` 18 (MIT) inside `vite.config.ts`'s `docs` plugin, so a bad guide fails `pnpm build` and the browser gets HTML alone; a fenced block tagged `example=NAME` shows `examples/docs/NAME.rs`, an example `scripts/check.sh` compiles as `docs_NAME`, so every full Rust sample on the site builds. The Showcase tab lists apps built with Ely from the public repo `ZacharyZhang-NY/Ely-GPUI-Showcases`: manifests, PNG previews and Markdown READMEs, checked by its `scripts/catalog.mjs` (`marked`, `pngjs`, `twitter-text`), its stars refreshed by its CI. The mark is the letter E in iso blocks, its middle arm in the accent: one geometry in `src/iso/mark.ts`, drawn by the engine the miniatures use, worn by the header in the page's iso colors and by the favicon. Below 768px the header shows the mark without its word, which stays the home link's name; below 414px it drops the sound switch. 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). ## Commands @@ -26,7 +26,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - `cargo run --example gallery -- --narrow 280 --capture ` lays each page, header and body, at that width in a padded card and shoots it without its script, whose aims are measured at full width. Demos that set their own width run past the card. - `RUSTC_BOOTSTRAP=1 cargo check --lib --target wasm32-unknown-unknown` checks the web build; gpui_web's `wasm_thread` needs the bootstrap, as Zed's web examples do. - `ELY_GALLERY_ASSETS=
scripts/web.sh ` builds the gallery for the browser, profile `web`, and fails past Cloudflare's 25 MiB. It opens at `?page=&story=&theme=light|dark`; the host page calls `gallery.setTheme(mode)` or posts `{ ely: "theme", theme }`, before the wasm runs or after, and the page posts `{ ely: "ready" }` to its parent once it runs. -- In `frontend/`: `ELY_GALLERY_ASSETS=<site>/gallery/assets/ pnpm gallery` builds the gallery into `public/gallery`, `pnpm dev` serves the site, `pnpm build` checks types and writes `dist`, `pnpm deploy` runs `wrangler deploy`, `pnpm showcase` writes the showcase catalog into `src/showcase.json` and `public/showcase` (both ignored): it downloads the commit `showcase.lock` pins, installs that commit's packages and runs its own validator, so a bad catalog fails the build; `dev` and `build` run it first, and `ELY_SHOWCASE=<checkout>` reads a local catalog instead. `pnpm test` runs the iso engine's and the showcase list's tests, and `pnpm local` builds the gallery for localhost, builds the site and serves both on http://localhost:4173/ (wasm-bindgen and wasm-opt on PATH, as `scripts/web.sh` says). `node scripts/og.mjs`, with `pnpm preview` up, renders `public/og.png` from the hero. The site reads `examples/gallery/stories.json` for its chapters, stories and search. +- In `frontend/`: `ELY_GALLERY_ASSETS=<site>/gallery/assets/ pnpm gallery` builds the gallery into `public/gallery`, `pnpm dev` serves the site, `pnpm build` checks types and writes `dist`, `pnpm deploy` runs `wrangler deploy`, `pnpm showcase` writes the showcase catalog into `src/showcase.json` and `public/showcase` (both ignored): it downloads the commit `showcase.lock` pins, installs that commit's packages and runs its own validator, so a bad catalog fails the build; `dev` and `build` run it first, and `ELY_SHOWCASE=<checkout>` reads a local catalog instead. `pnpm test` runs the iso engine's, the mark's and the showcase list's tests, and `pnpm local` builds the gallery for localhost, builds the site and serves both on http://localhost:4173/ (wasm-bindgen and wasm-opt on PATH, as `scripts/web.sh` says). `node scripts/og.mjs`, with `pnpm preview` up, renders `public/og.png` from the hero and the header's mark. `node scripts/favicon.mjs` writes `public/favicon.svg`, which follows the system's mode, and `public/apple-touch-icon.png` from the mark, in the colors `styles.css` sets. The site reads `examples/gallery/stories.json` for its chapters, stories and search. - `python3 scripts/stories.py` writes `examples/gallery/stories.json`, every page's sections in drawing order with their components, for the site: the names in a title, and in its `SHOWN` table what a story draws beyond them. It fails on a section opened inside another and on a ticked entry with a home of its own that no story names. `scripts/check.sh` refuses a stale one. - `cargo test --lib --features test-support`, `cargo clippy --all-targets --features test-support -- -D warnings`, `cargo fmt --check`. `scripts/check.sh` runs them with the house rules and lints the gallery for wasm32. - `rm -rf target/debug/incremental` and `cargo clean -p ely-gpui-component` after each task item keep the disk lean. Cargo keeps every old build of the crate and its tests under a new hash; by T25 they had filled the disk. diff --git a/README.md b/README.md index a99da56..ae93a56 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ ELY_GALLERY_ASSETS=https://<site>/gallery/assets/ pnpm gallery # the wasm gall pnpm dev # or: pnpm build && pnpm preview; both fetch the showcase at showcase.lock's commit ELY_SHOWCASE=../../Ely-GPUI-Showcases pnpm dev # the showcase from a local checkout node scripts/og.mjs # with pnpm preview up: public/og.png from the hero +node scripts/favicon.mjs # public/favicon.svg and apple-touch-icon.png from the mark pnpm deploy # Cloudflare, through wrangler ``` diff --git a/TASKS.md b/TASKS.md index 87953e5..c01c520 100644 --- a/TASKS.md +++ b/TASKS.md @@ -186,6 +186,7 @@ Per-component lines live in `tasks/`. Tags there: - [x] T50f Rendering · Docs: a guide with an example `scripts/check.sh` compiles; AGENTS.md - [x] T50g Rendering · Acceptance: the checklist, item by item - [x] T51 Showcase · an apps page fed by a public repo — `tasks/showcase.md` +- [x] T52 Logo · an isometric mark for the header, favicon and OG image — `tasks/logo.md` ## T00 detail diff --git a/frontend/e2e/site.spec.ts b/frontend/e2e/site.spec.ts index 9350206..d3d0c26 100644 --- a/frontend/e2e/site.spec.ts +++ b/frontend/e2e/site.spec.ts @@ -117,3 +117,22 @@ test("a live story that opens a window shows that window natively", async ({ pag await shot.scrollIntoViewIfNeeded(); await expect.poll(() => shot.evaluate((img: HTMLImageElement) => img.naturalWidth)).toBeGreaterThan(0); }); + +test("the header leads with the mark, and the home link keeps its name", async ({ page }) => { + await page.goto("/components/"); + const home = page.getByRole("link", { name: "Ely", exact: true }); + await expect(home.locator("svg.mark polygon")).toHaveCount(12); + await expect(home.locator("svg.mark .accent")).toHaveCount(3); + await home.click(); + await expect(page).toHaveURL("/"); +}); + +test("the favicon and the touch icon are served", async ({ page, request }) => { + await page.goto("/components/"); + for (const [rel, type] of [["icon", "image/svg+xml"], ["apple-touch-icon", "image/png"]]) { + const href = await page.locator(`link[rel="${rel}"]`).getAttribute("href"); + const res = await request.get(href!); + expect(res.status()).toBe(200); + expect(res.headers()["content-type"]).toContain(type); + } +}); diff --git a/frontend/index.html b/frontend/index.html index 7b7722d..d992d17 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,8 @@ <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Ely · GPUI components + + diff --git a/frontend/package.json b/frontend/package.json index 2e33fd1..3354c52 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,7 @@ "local": "ELY_GALLERY_ASSETS=http://localhost:4173/gallery/assets/ pnpm gallery && pnpm build && vite preview --port 4173 --strictPort", "deploy": "wrangler deploy", "e2e": "playwright test", - "test": "node --test src/iso/engine.test.ts src/apps.test.ts" + "test": "node --test src/iso/engine.test.ts src/iso/mark.test.ts src/apps.test.ts" }, "devDependencies": { "@playwright/test": "1.63.0", diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..31f25bc Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..2866cbd --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/og.png b/frontend/public/og.png index 1e1185b..9f2f19b 100644 Binary files a/frontend/public/og.png and b/frontend/public/og.png differ diff --git a/frontend/scripts/favicon.mjs b/frontend/scripts/favicon.mjs new file mode 100644 index 0000000..7b01a47 --- /dev/null +++ b/frontend/scripts/favicon.mjs @@ -0,0 +1,41 @@ +// Writes public/favicon.svg and public/apple-touch-icon.png from the header's mark. +import { chromium } from "@playwright/test"; +import { readFileSync, writeFileSync } from "node:fs"; +import { mark, points } from "../src/iso/mark.ts"; + +const css = readFileSync("src/styles.css", "utf8"); + +/** The iso colors a `:root` block of styles.css sets, as the header's mark wears them. */ +function colors(selector) { + const start = `\n${css}`.indexOf(`\n${selector} {`); + const block = start < 0 ? null : css.slice(start, css.indexOf("}", start)); + if (!block) throw new Error(`favicon: styles.css has no ${selector} block`); + const v = (name) => { + const value = block.match(new RegExp(`--${name}: (#[0-9a-f]{6});`))?.[1]; + if (!value) throw new Error(`favicon: ${selector} sets no --${name}`); + return value; + }; + return { top: v("bg"), front: v("iso-side"), right: v("iso-shade"), line: v("iso-line"), accent: v("accent") }; +} +const [LIGHT, DARK] = [colors(":root"), colors(":root.dark")]; + +const rules = (c) => `.top{fill:${c.top}}.front{fill:${c.front}}.right{fill:${c.right}}polygon{stroke:${c.line}}.accent{stroke:${c.accent}}`; + +/** The mark centered in a square, `inset` of the side left round it. */ +function svg(inset, stroke, style, back = "") { + const { width, height, polygons } = mark(0); + const side = Math.max(width, height) / (1 - inset * 2); + const [ox, oy] = [(side - width) / 2, (side - height) / 2]; + const body = polygons.map((p) => ``).join(""); + const fill = back && ``; + return `${fill}${body}\n`; +} + +writeFileSync("public/favicon.svg", svg(0.04, 0.28, `${rules(LIGHT)}@media (prefers-color-scheme:dark){${rules(DARK)}}`)); + +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 180, height: 180 } }); +await page.setContent(`${svg(0.18, 0.16, rules(LIGHT), LIGHT.top)}`); +await page.screenshot({ path: "public/apple-touch-icon.png" }); +await browser.close(); +console.log("favicon: wrote public/favicon.svg and public/apple-touch-icon.png"); diff --git a/frontend/scripts/og.mjs b/frontend/scripts/og.mjs index a21cce3..e3155da 100644 --- a/frontend/scripts/og.mjs +++ b/frontend/scripts/og.mjs @@ -4,7 +4,7 @@ import { chromium } from "@playwright/test"; const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 2, colorScheme: "light", reducedMotion: "reduce" }); await page.goto(process.env.SITE ?? "http://localhost:4173/"); -await page.addStyleTag({ content: ".bar, .skip, .hero .button { display: none } .hero { min-height: 630px; height: 630px; padding: 56px } .hero-iso { top: 0; height: 100% }" }); +await page.addStyleTag({ content: ".bar nav, .bar .end, .skip, .hero .button { display: none } .bar { background: none; backdrop-filter: none; padding: 0 56px } .hero { min-height: 630px; height: 630px; padding: 56px } .hero-iso { top: 0; height: 100% }" }); await page.waitForSelector(".hero-iso"); await page.waitForTimeout(800); await page.screenshot({ path: "public/og.png", clip: { x: 0, y: 0, width: 1200, height: 630 } }); diff --git a/frontend/src/Mark.tsx b/frontend/src/Mark.tsx new file mode 100644 index 0000000..8e07555 --- /dev/null +++ b/frontend/src/Mark.tsx @@ -0,0 +1,14 @@ +import { mark, points } from "./iso/mark"; + +const { width, height, polygons } = mark(0.12); + +/** The E in blocks, in the page's iso colors. */ +export function Mark() { + return ( + + ); +} diff --git a/frontend/src/Shell.tsx b/frontend/src/Shell.tsx index 0f9e82b..2db1aa2 100644 --- a/frontend/src/Shell.tsx +++ b/frontend/src/Shell.tsx @@ -2,6 +2,7 @@ import { Link, Outlet, useRouter } from "@tanstack/react-router"; import { useEffect } from "react"; import { REPO } from "./data"; import { Icon } from "./icons"; +import { Mark } from "./Mark"; import { useStill } from "./iso/Iso"; import { smooth } from "./scroll"; import { setSound, useSound } from "./iso/marimba"; @@ -14,7 +15,8 @@ function Header() { return (
- Ely + + Ely