Public: anyone can read this; the crew can change it.
One design language: every page, card and word at the home page's standard #9535
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9433-one-design-language-every-page-card-and → main 2 commits fork
4 files
changed +11
−29
src/web/og/card.ts +4 −25
| @@ -73,17 +73,6 @@ const TITLE_LINES = 2; | ||
| 73 | 73 | const EM_BOLD = 0.68; |
| 74 | 74 | const EM_MONO = 0.62; |
| 75 | 75 | |
| 76 | const KIND_COLOR: Record<CardKind, string> = { | |
| 77 | repo: T.accent, | |
| 78 | issue: T.success, | |
| 79 | pull: T.merged, | |
| 80 | run: T.primary, | |
| 81 | app: T.attn, | |
| 82 | map: T.accent, | |
| 83 | audit: T.success, | |
| 84 | page: T.primary, | |
| 85 | }; | |
| 86 | ||
| 87 | 76 | /** Break a title into at most `n` lines of `max` chars; the last is clamped. */ |
| 88 | 77 | function wrap(text: string, size: number, n: number, em: number, width = INNER): string[] { |
| 89 | 78 | const max = Math.max(1, Math.floor(width / (size * em))); |
| @@ -173,8 +162,6 @@ function drawImages(images: unknown): string { | ||
| 173 | 162 | } |
| 174 | 163 | |
| 175 | 164 | export function renderCard(input: CardInput): string { |
| 176 | const kind: CardKind = CARD_KINDS.includes(input?.kind) ? input.kind : "page"; | |
| 177 | const color = KIND_COLOR[kind]; | |
| 178 | 165 | const out: string[] = []; |
| 179 | 166 | |
| 180 | 167 | out.push( |
| @@ -185,16 +172,8 @@ export function renderCard(input: CardInput): string { | ||
| 185 | 172 | text(PAD + 60, PAD + 26, 30, T.fg, "forge", ` font-weight="700"`), |
| 186 | 173 | ); |
| 187 | 174 | |
| 188 | // A card about one thing is marked with its kind: a strip down the left | |
| 189 | // edge and a badge in the header. The forge's own card is plain, as the | |
| 190 | // home page is. | |
| 191 | const plain = kind === "page"; | |
| 192 | if (!plain) { | |
| 193 | out.push( | |
| 194 | `<rect x="0" y="0" width="12" height="${CARD_HEIGHT}" fill="${color}"/>`, | |
| 195 | text(CARD_WIDTH - PAD, PAD + 24, 24, color, kind.toUpperCase(), ` font-weight="700" text-anchor="end" letter-spacing="2"`), | |
| 196 | ); | |
| 197 | } | |
| 175 | // Every card is as plain as the home page: no kind badge, no edge strip, | |
| 176 | // one background, one text colour. The kind picks the card, not its look. | |
| 198 | 177 | |
| 199 | 178 | const figure = drawFigure(input?.figure) || drawImages(input?.images); |
| 200 | 179 | const column = figure ? COLUMN : INNER; |
| @@ -204,7 +183,7 @@ export function renderCard(input: CardInput): string { | ||
| 204 | 183 | const tsize = figure ? 56 : 64; |
| 205 | 184 | const title = wrap(input?.title ?? "", tsize, TITLE_LINES, EM_BOLD, column); |
| 206 | 185 | const tagline = clean(input?.tagline); |
| 207 | const tight = plain ? ` letter-spacing="-1.5"` : ""; | |
| 186 | const tight = ` letter-spacing="-1.5"`; | |
| 208 | 187 | let y = tagline ? 230 : 270; |
| 209 | 188 | for (const row of title) { |
| 210 | 189 | out.push(text(PAD, y, tsize, T.fg, row, ` font-weight="700"${tight}`)); |
| @@ -234,7 +213,7 @@ export function renderCard(input: CardInput): string { | ||
| 234 | 213 | const stat = clean(input?.stat); |
| 235 | 214 | if (stat) { |
| 236 | 215 | out.push( |
| 237 | text(PAD, CARD_HEIGHT - 52, 32, plain ? T["muted-fg"] : color, fit(stat, 32, INNER, EM_MONO), ` font-family="${MONO}" font-weight="600"`), | |
| 216 | text(PAD, CARD_HEIGHT - 52, 32, T["muted-fg"], fit(stat, 32, INNER, EM_MONO), ` font-family="${MONO}" font-weight="600"`), | |
| 238 | 217 | ); |
| 239 | 218 | } |
| 240 | 219 | out.push(`</svg>`); |
test/og-card.test.ts +3
| @@ -25,6 +25,9 @@ describe("og card", () => { | ||
| 25 | 25 | expect(svg).toContain('width="1200" height="630"'); |
| 26 | 26 | expect(svg).toContain("Fast forge"); |
| 27 | 27 | expect(svg).toContain("12 open"); |
| 28 | // As plain as the home page: no kind label, no edge strip. | |
| 29 | expect(svg).not.toContain(`>${kind.toUpperCase()}<`); | |
| 30 | expect(svg).not.toContain(`width="12"`); | |
| 28 | 31 | }); |
| 29 | 32 | } |
| 30 | 33 |
test/og-designs.test.ts +2 −2
| @@ -106,7 +106,7 @@ describe("the map's thumbnail", () => { | ||
| 106 | 106 | const figure = svg.slice(svg.indexOf("<g>"), svg.indexOf("</g>")); |
| 107 | 107 | expect(figure.match(/<rect /g)?.length).toBe(4); |
| 108 | 108 | expect(figure.match(new RegExp(`<path [^>]*stroke="${T.accent}"`, "g"))?.length).toBe(2); |
| 109 | expect(svg).toContain(">MAP<"); | |
| 109 | expect(svg).not.toContain(">MAP<"); | |
| 110 | 110 | }); |
| 111 | 111 | |
| 112 | 112 | test("with nothing linked, the loose issues are drawn, four rows at most", () => { |
| @@ -237,7 +237,7 @@ describe("the audit's findings", () => { | ||
| 237 | 237 | const svg = renderCard(card); |
| 238 | 238 | wellFormed(svg); |
| 239 | 239 | expect(svg).toContain(">50%<"); |
| 240 | expect(svg).toContain(">AUDIT<"); | |
| 240 | expect(svg).not.toContain(">AUDIT<"); | |
| 241 | 241 | }); |
| 242 | 242 | |
| 243 | 243 | test("every app holding says so; no apps draws no ring", () => { |
test/og-route.test.ts +2 −2
| @@ -197,8 +197,8 @@ describe("og cards", () => { | ||
| 197 | 197 | }); |
| 198 | 198 | |
| 199 | 199 | test("the card says what the thing is", async () => { |
| 200 | expect(await (await get("/og/issue/alice~shown~1.svg")).text()).toContain("ISSUE"); | |
| 201 | expect(await (await get("/og/pull/alice~shown~2.svg")).text()).toContain("PULL"); | |
| 200 | expect(await (await get("/og/issue/alice~shown~1.svg")).text()).not.toContain("ISSUE"); | |
| 201 | expect(await (await get("/og/pull/alice~shown~2.svg")).text()).not.toContain("PULL"); | |
| 202 | 202 | expect(await (await get(`/og/run/alice~shown~${runId}.svg`)).text()).toContain("did a thing"); |
| 203 | 203 | }); |
| 204 | 204 |
new above
offline