import { test } from "node:test"; import assert from "node:assert/strict"; import { ACCENTS, ACCENT_IDS, ACCENT_INK, BASE_GROUNDS, DEFAULT_ACCENT, ICON_PALETTES, MARK, MIN_ACCENT_CONTRAST, MIN_MARK_DIM_CONTRAST, MIN_MARK_LIT_OVER_DIM, childIconPalette, contrastRatio, isAccentId, markSvg, splitWordmark, wordmarkLeadFor, type BaseGround, } from "./brand"; import { PROJECT_NAME, PROJECT_WORDMARK_LEAD } from "./project"; const ON: Record = { light: "onLight", dark: "onDark", }; test("accents: the table is the plan's, keyed and ordered by ACCENT_IDS", () => { assert.deepEqual(Object.keys(ACCENTS), [...ACCENT_IDS]); for (const id of ACCENT_IDS) { assert.equal(ACCENTS[id].id, id); assert.deepEqual(Object.keys(ACCENTS[id]).sort(), ["id", "name", "onDark", "onLight"]); for (const k of ["onDark", "onLight"] as const) { assert.match(ACCENTS[id][k], /^#[0-9a-f]{6}$/, `${id}.${k}`); } } assert.equal(DEFAULT_ACCENT, "signal"); // The whole table, literally, against plans/brand-and-themes.md "Accents": // a typo that still clears 4.5:1 would pass the contrast test below. assert.deepEqual(ACCENTS, { signal: { id: "signal", name: "Signal", onDark: "#5fa8a0", onLight: "#2e7b73" }, brass: { id: "brass", name: "Brass", onDark: "#e3b15c", onLight: "#95661a" }, vermilion: { id: "vermilion", name: "Vermilion", onDark: "#ec7a52", onLight: "#b3431f" }, violet: { id: "violet", name: "Violet", onDark: "#b49cf2", onLight: "#6a4bc4" }, sakura: { id: "sakura", name: "Sakura", onDark: "#ee8fb5", onLight: "#a83a6a" }, blue: { id: "blue", name: "Blue", onDark: "#74a9f2", onLight: "#2d5fb8" }, green: { id: "green", name: "Green", onDark: "#7cc46a", onLight: "#3f7a2c" }, }); assert.equal(isAccentId("brass"), true); assert.equal(isAccentId("Brass"), false); assert.equal(isAccentId("#e3b15c"), false); }); // THE CONTRAST RULE: every accent's value on every base reaches 4.5:1 against // that base's ground AND against the ink set on it (white on light, the dark // ground on dark). 7 accents × 2 bases × 2 = 28 checks. test("accents: every value reaches 4.5:1 on its ground and with its ink", () => { // The bar itself is pinned as a literal: comparing only against the // imported constant would let an edit to brand.ts lower rule and test // together. assert.equal(MIN_ACCENT_CONTRAST, 4.5); let checks = 0; for (const id of ACCENT_IDS) { for (const base of Object.keys(BASE_GROUNDS) as BaseGround[]) { const hex = ACCENTS[id][ON[base]]; const ground = contrastRatio(hex, BASE_GROUNDS[base]); const ink = contrastRatio(hex, ACCENT_INK[base]); assert.ok(ground >= 4.5, `${id} on ${base}: ${ground.toFixed(2)} vs ground`); assert.ok(ink >= 4.5, `${id} on ${base}: ${ink.toFixed(2)} vs ink`); checks += 2; } } assert.equal(checks, 28); assert.equal(ACCENT_INK.light, "#ffffff"); assert.equal(ACCENT_INK.dark, BASE_GROUNDS.dark); }); test("contrastRatio: the WCAG endpoints", () => { assert.equal(contrastRatio("#000000", "#ffffff").toFixed(2), "21.00"); assert.equal(contrastRatio("#777777", "#777777"), 1); assert.equal(contrastRatio("#ffffff", "#000000"), contrastRatio("#000000", "#ffffff")); }); // The endpoints hold for any monotone curve, a naive gamma 2.2 included. The // sRGB boundary pair pins the real one: #767676 is the lightest grey that // passes 4.5:1 on white, and #777777 the first that does not. test("contrastRatio: the sRGB curve, at the 4.5:1 boundary on white", () => { assert.equal(contrastRatio("#767676", "#ffffff").toFixed(2), "4.54"); assert.equal(contrastRatio("#777777", "#ffffff").toFixed(2), "4.48"); assert.ok(contrastRatio("#767676", "#ffffff") >= 4.5); assert.ok(contrastRatio("#777777", "#ffffff") < 4.5); }); test("bases and icon palettes are the plan's", () => { assert.deepEqual(BASE_GROUNDS, { light: "#f3f6f7", dark: "#0c0a08" }); assert.deepEqual(ICON_PALETTES.archilyzer, { ground: "#151b20", dim: "#586977", lit: "#e7edf1" }); assert.deepEqual(childIconPalette(ACCENTS.brass.onDark), { ground: "#0c0a08", dim: "#6b5d47", lit: "#e3b15c", }); }); // The mark's two rules (brand.ts MIN_MARK_DIM_CONTRAST, MIN_MARK_LIT_OVER_DIM). // A child site can be lit by any named accent — its own, or Signal when it // has none — so the child dim is held against all seven. The parent mark is lit in bone; the // Media mark's Signal on the same slate is held in brandMedia.test.ts. const LIT_OF: ReadonlyArray<[string, string, string]> = [ ...ACCENT_IDS.map((id): [string, string, string] => [`child · ${id}`, ICON_PALETTES.child.dim, ACCENTS[id].onDark]), ["archilyzer · bone", ICON_PALETTES.archilyzer.dim, ICON_PALETTES.archilyzer.lit], ]; test("icon palettes: every dim line reaches 3:1 on its ground (WCAG 1.4.11)", () => { assert.equal(MIN_MARK_DIM_CONTRAST, 3); for (const [name, p] of Object.entries(ICON_PALETTES)) { const r = contrastRatio(p.dim, p.ground); assert.ok(r >= MIN_MARK_DIM_CONTRAST, `${name}: dim ${p.dim} on ${p.ground} is ${r.toFixed(2)}:1`); } // Pinned, so a retune shows its numbers: was 1.59 and 1.97 (2026-09-27). assert.equal(contrastRatio(ICON_PALETTES.child.dim, ICON_PALETTES.child.ground).toFixed(2), "3.09"); assert.equal(contrastRatio(ICON_PALETTES.archilyzer.dim, ICON_PALETTES.archilyzer.ground).toFixed(2), "3.06"); }); test("icon palettes: every lit value stands 2:1 above its dim, so the found line stays found", () => { assert.equal(MIN_MARK_LIT_OVER_DIM, 2); for (const [name, dim, lit] of LIT_OF) { const r = contrastRatio(lit, dim); assert.ok(r >= MIN_MARK_LIT_OVER_DIM, `${name}: lit ${lit} over dim ${dim} is ${r.toFixed(2)}:1`); } // Pinned. The tightest are Vermilion and Signal, the darkest lit values. const ratios = Object.fromEntries(LIT_OF.map(([name, dim, lit]) => [name, contrastRatio(lit, dim).toFixed(2)])); assert.deepEqual(ratios, { "child · signal": "2.31", "child · brass": "3.26", "child · vermilion": "2.28", "child · violet": "2.74", "child · sakura": "2.81", "child · blue": "2.65", "child · green": "3.04", "archilyzer · bone": "4.80", }); }); test("MARK: the Found-line geometry, back to front", () => { assert.deepEqual( MARK.map((s) => [s.part, s.tone]), [ ["ground", "ground"], ["line 1", "dim"], ["play head", "lit"], ["line 2", "lit"], ["line 3", "dim"], ["line 4", "dim"], ], ); const rect = (i: number) => { const s = MARK[i]; assert.equal(s.kind, "rect"); return s.kind === "rect" ? [s.x, s.y, s.width, s.height, s.rx] : []; }; assert.deepEqual(rect(0), [0, 0, 512, 512, 112]); assert.deepEqual(rect(1), [112, 128, 288, 44, 22]); assert.deepEqual(rect(3), [188, 212, 212, 44, 22]); assert.deepEqual(rect(4), [112, 296, 232, 44, 22]); assert.deepEqual(rect(5), [112, 380, 152, 44, 22]); const head = MARK[2]; assert.equal(head.kind, "polygon"); if (head.kind === "polygon") { assert.deepEqual(head.points, [[112, 210], [172, 234], [112, 258]]); } }); const PARENT = ICON_PALETTES.archilyzer; const FG = '' + '' + '' + '' + ''; const OPEN = ''; test("markSvg any: the rounded square", () => { assert.equal( markSvg(PARENT), `${OPEN}${FG}`, ); assert.equal(markSvg(PARENT, { variant: "any" }), markSvg(PARENT)); }); test("markSvg maskable: full-bleed ground, the lines scaled 0.8 about the centre", () => { assert.equal( markSvg(PARENT, { variant: "maskable" }), `${OPEN}` + `${FG}`, ); // The transform fixes (256,256): 51.2 + 0.8 × 256 = 256. assert.equal(51.2 + 0.8 * 256, 256); }); test("markSvg apple: full-bleed ground at scale 1", () => { assert.equal( markSvg(PARENT, { variant: "apple" }), `${OPEN}${FG}`, ); }); test("markSvg: a child site's lit line is its accent; colours are lowercased", () => { const svg = markSvg(childIconPalette("#E3B15C")); assert.match(svg, //); assert.match(svg, //); assert.match(svg, /rx="112" fill="#0c0a08"/); assert.equal((svg.match(/fill="#6b5d47"/g) ?? []).length, 3); }); test("markSvg refuses a colour that is not #rrggbb", () => { assert.throws(() => markSvg({ ...PARENT, lit: 'red"/>