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:
1 parent
f281d01f35
commit
20a8e2b0ee
18 files changed
+158
-6
No files matched your search
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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");
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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" }}>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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(" ");
|
||||
@@ -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; }
|
||||
|
||||
Reference in new issue
Block a user