import { readdir, readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; /** * Every colour and corner in the Dashboard's own product UI comes from a design token (ADR 0039, * ADR 0060), so Light, Dark, and System — and later a church's theme — reach all of it. * * The scan covers the shell's Dashboard source and `@ekklesia/ui`. It looks for what a design token * would replace: a hex, `rgb()`, or `hsl()` colour, and a literal corner radius. Comments are ignored, * and so is `@ekklesia/design-tokens` itself, which is where those values are meant to live. */ const repositoryRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const PRODUCT_SOURCE = [ path.join("surfaces", "shell", "src", "browser"), path.join("packages", "ui", "src"), ]; /** * Deliberate, named exceptions. The display host (`surfaces/shell/src/display`) is not scanned at * all: it is pinned to the light scheme and its pairing and offline screens keep their Stage 0 look * until displays move to Settings → Displays. */ const RAW_COLOUR = /#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\(/u; const RAW_RADIUS = /\bborder-?[Rr]adius["']?\s*:\s*["'`]?\s*(?!var\(|\$\{)\d/u; function withoutComments(source: string): string { return source.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/(^|[^:])\/\/.*$/gmu, "$1"); } async function files(directory: string): Promise { const found: string[] = []; for (const entry of await readdir(directory, { withFileTypes: true })) { const full = path.join(directory, entry.name); if (entry.isDirectory()) { found.push(...(await files(full))); } else if (/\.tsx?$/u.test(entry.name)) { found.push(full); } } return found; } async function offenders(pattern: RegExp): Promise { const found: string[] = []; for (const root of PRODUCT_SOURCE) { for (const file of await files(path.join(repositoryRoot, root))) { withoutComments(await readFile(file, "utf8")) .split("\n") .forEach((line, index) => { if (pattern.test(line)) { found.push(`${path.relative(repositoryRoot, file)}:${index + 1}: ${line.trim()}`); } }); } } return found; } describe("the Dashboard's product UI is built from tokens", () => { it("has no raw colour outside the token package", async () => { expect(await offenders(RAW_COLOUR)).toEqual([]); }); it("has no literal corner radius", async () => { expect(await offenders(RAW_RADIUS)).toEqual([]); }); it("would notice one", () => { for (const line of [ 'color: "#9f1239",', "background: rgb(28 25 23 / 0.08);", 'borderRadius: "0.375rem",', "border-radius: 4px;", ]) { expect(RAW_COLOUR.test(line) || RAW_RADIUS.test(line), line).toBe(true); } for (const line of [ 'borderRadius: designToken("radius.md"),', `border-radius: \${designToken("radius.md")};`, 'querySelector("#ekklesia-host")', 'id="ekklesia-sidebar-navigation"', ]) { expect(RAW_COLOUR.test(line) || RAW_RADIUS.test(line), line).toBe(false); } }); });