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

+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; }