Site · Logo: the letter E in iso blocks, in the header, the favicon, the touch icon and the OG image

One geometry in src/iso/mark.ts, drawn by the engine the miniatures use. The favicon follows the system's mode; scripts/favicon.mjs takes its colors from styles.css. Below 768px the header shows the mark alone, its word kept as the link's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-04 22:19:04 -04:00
1 parent f281d01f35
commit 20a8e2b0ee
18 files changed
+158 -6

No files matched your search

+2 -2
View File
@@ -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 <dir>` 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=<address of out/assets/> scripts/web.sh <out>` builds the gallery for the browser, profile `web`, and fails past Cloudflare's 25 MiB. It opens at `?page=<slug>&story=<title or slug>&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.
+1
View File
@@ -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
```
+1
View File
@@ -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
+19
View File
@@ -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);
}
});
+2
View File
@@ -4,6 +4,8 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ely · GPUI components</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="description" content="A component library for GPUI, in light and dark. Its stories run live in the browser, or as native captures where a browser cannot run them." />
<meta property="og:type" content="website" />
<meta property="og:title" content="Ely · GPUI components" />
+1 -1
View File
@@ -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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7.826 7.826" stroke-width="0.28" stroke-linejoin="round"><style>.top{fill:#fcfaf7}.front{fill:#f3f0ec}.right{fill:#e9e5df}polygon{stroke:#1a1918}.accent{stroke:#3772bb}@media (prefers-color-scheme:dark){.top{fill:#131110}.front{fill:#1a1816}.right{fill:#201e1c}polygon{stroke:#d9d5d0}.accent{stroke:#69a1e8}}</style><polygon class="right" points="3.74,5.813 2.874,6.313 2.874,1.313 3.74,0.813"/><polygon class="front" points="2.008,5.813 2.874,6.313 2.874,1.313 2.008,0.813"/><polygon class="top" points="2.874,0.313 3.74,0.813 2.874,1.313 2.008,0.813"/><polygon class="right" points="5.818,7.013 4.952,7.513 4.952,6.513 5.818,6.013"/><polygon class="front" points="2.874,6.313 4.952,7.513 4.952,6.513 2.874,5.313"/><polygon class="top" points="3.74,4.813 5.818,6.013 4.952,6.513 2.874,5.313"/><polygon class="right accent" points="5.299,4.713 4.433,5.213 4.433,4.213 5.299,3.713"/><polygon class="front accent" points="2.874,4.313 4.433,5.213 4.433,4.213 2.874,3.313"/><polygon class="top accent" points="3.74,2.813 5.299,3.713 4.433,4.213 2.874,3.313"/><polygon class="right" points="5.818,3.013 4.952,3.513 4.952,2.513 5.818,2.013"/><polygon class="front" points="2.874,2.313 4.952,3.513 4.952,2.513 2.874,1.313"/><polygon class="top" points="3.74,0.813 5.818,2.013 4.952,2.513 2.874,1.313"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 240 KiB

+41
View File
@@ -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) => `<polygon class="${p.accent ? `${p.face} accent` : p.face}" points="${points(p.points.map(([x, y]) => [x + ox, y + oy]))}"/>`).join("");
const fill = back && `<rect width="${side}" height="${side}" fill="${back}"/>`;
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${+side.toFixed(3)} ${+side.toFixed(3)}" stroke-width="${stroke}" stroke-linejoin="round"><style>${style}</style>${fill}${body}</svg>\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(`<style>body{margin:0}svg{display:block;width:180px;height:180px}</style>${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");
+1 -1
View File
@@ -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 } });
+14
View File
@@ -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 (
<svg className="mark" viewBox={`0 0 ${width} ${height}`} aria-hidden="true">
{polygons.map((p, i) => (
<polygon key={i} className={p.accent ? `${p.face} accent` : p.face} points={points(p.points)} />
))}
</svg>
);
}
+3 -1
View File
@@ -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 (
<header className="bar">
<Link to="/" className="brand">
Ely
<Mark />
<span>Ely</span>
</Link>
<nav aria-label="Site">
<Link to="/components/" className="link" activeProps={{ "aria-current": "page" }}>
+24
View File
@@ -0,0 +1,24 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { mark, points } from "./mark.ts";
test("the mark draws three faces of each of its four blocks, the middle arm's in the accent", () => {
const { polygons } = mark(0);
assert.equal(polygons.length, 12);
assert.equal(polygons.filter((p) => p.accent).length, 3);
assert.deepEqual(polygons.slice(0, 3).map((p) => p.face), ["right", "front", "top"]);
});
test("the mark fills its box less the padding", () => {
const pad = 0.5;
const { width, height, polygons } = mark(pad);
const all = polygons.flatMap((p) => p.points);
const [xs, ys] = [all.map(([x]) => x), all.map(([, y]) => y)];
assert.ok(Math.abs(Math.min(...xs) - pad) < 1e-9 && Math.abs(Math.max(...xs) - (width - pad)) < 1e-9);
assert.ok(Math.abs(Math.min(...ys) - pad) < 1e-9 && Math.abs(Math.max(...ys) - (height - pad)) < 1e-9);
assert.ok(height > width, "the E stands taller than wide");
});
test("points print as an SVG list", () => {
assert.equal(points([[1, 2.5], [0.12345, 3]]), "1,2.5 0.123,3");
});
+27
View File
@@ -0,0 +1,27 @@
import { bounds, faces, painted, type Block, type Point } from "./engine.ts";
const b = (x: number, z: number, w: number, d: number, h: number, y = 0, accent = false): Block => ({ x, z, w, d, h, y, accent, beat: 0 });
/** The letter E in blocks, its middle arm in the accent. */
const BLOCKS = [b(0, 0, 1, 1, 5), b(1, 0, 2.4, 1, 1), b(1, 0, 1.8, 1, 1, 2, true), b(1, 0, 2.4, 1, 1, 4)];
export type Face = "top" | "front" | "right";
export interface Polygon {
points: Point[];
face: Face;
accent: boolean;
}
/** The mark's faces back to front, in a box `pad` larger than the mark each side. */
export function mark(pad: number): { width: number; height: number; polygons: Polygon[] } {
const [x0, y0, x1, y1] = bounds(BLOCKS);
const [ox, oy] = [pad - x0, pad - y0];
const polygons = painted(BLOCKS).flatMap((k) => {
const f = faces(k, k.h);
return (["right", "front", "top"] as const).map((face) => ({ points: f[face].map(([x, y]): Point => [x + ox, y + oy]), face, accent: !!k.accent }));
});
return { width: x1 - x0 + pad * 2, height: y1 - y0 + pad * 2, polygons };
}
export const points = (p: Point[]) => p.map(([x, y]) => `${+x.toFixed(3)},${+y.toFixed(3)}`).join(" ");
+7 -1
View File
@@ -63,7 +63,12 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
/* Header */
.bar { position: fixed; inset: 0 0 auto; z-index: 40; height: 64px; display: flex; align-items: center; gap: 32px; padding: 0 32px; background: color-mix(in srgb, var(--bg) 82%, transparent); backdrop-filter: blur(18px); }
.brand { font-weight: 800; font-size: 20px; letter-spacing: -0.03em; }
.brand { display: flex; align-items: center; gap: 8px; font-weight: 800; font-size: 20px; letter-spacing: -0.03em; }
.mark { display: block; height: 30px; width: auto; flex: none; stroke: var(--iso-line); stroke-width: 0.17; stroke-linejoin: round; }
.mark .top { fill: var(--bg); }
.mark .front { fill: var(--iso-side); }
.mark .right { fill: var(--iso-shade); }
.mark .accent { stroke: var(--accent); }
.bar nav { display: flex; gap: 24px; }
.link { font-size: 14px; font-weight: 400; color: var(--muted); transition: color 160ms ease-out; }
.link:hover, .link[aria-current="page"] { color: var(--fg); }
@@ -260,6 +265,7 @@ img.shot { display: block; max-width: 100%; height: auto; }
@media (max-width: 767px) {
.bar { padding: 0 16px; gap: 12px; }
.bar nav { gap: 16px; }
.brand span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.bar .end { gap: 0; }
.hero, .chapters, .missing, .showcase, .app-page { padding-left: 20px; padding-right: 20px; }
.showcase, .app-page { padding-top: 112px; }
+9
View File
@@ -0,0 +1,9 @@
# Logo round, 2026-10-04
The site's mark: the letter E in blocks, drawn by the site's own iso engine, its middle arm in the accent. The user chose it from four, and asked for it in the header, the favicon and touch icon, and the OG image.
- [x] L1 Plan: these lines, the T52 line in `TASKS.md`
- [x] L2 Mark: one geometry in `src/iso/mark.ts`; a `Mark` in the header before "Ely", in the page's colors, light and dark
- [x] L3 Favicon: `scripts/favicon.mjs` writes `public/favicon.svg`, which follows the system's mode, and a 180px `apple-touch-icon.png`; `index.html` links both
- [x] L4 OG: `scripts/og.mjs` keeps the header's mark and name; `public/og.png` rendered again
- [x] L5 Tests and docs: unit and Playwright tests; AGENTS.md; acceptance, item by item
+5
View File
@@ -377,3 +377,8 @@
| S8 | 1 | FAIL | the card click matched names by substring; the unknown id could be a real one |
| S8 | 2 | PASS | |
| S9 | 1 | PASS | |
| L1 | 1 | PASS | |
| L2 | 1 | PASS | |
| L3 | 1 | PASS | |
| L4 | 1 | PASS | |
| L5 | 1 | PASS | |