Jason accepted the draft 1 direction on 2026-10-10; the refined Relay mark stays as a placeholder. Adds IMPLEMENTING.md (gap map against row 40 and the bus/CLI modules, strings, boundaries, acceptance, proposed rows for Sage), tokens.json and generated tokens.css using the webui's existing names, icons.svg, and tools/build-tokens.mjs and tools/check-design.mjs. Coverage statuses corrected against the code (digest implemented; mosaic stop is row 41; priority and agent state not in the bus reads). No packages/ change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
73 lines
3.3 KiB
JavaScript
73 lines
3.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Generates docs/design/tokens.css from docs/design/tokens.json.
|
|
// Usage: node docs/design/tools/build-tokens.mjs [--check]
|
|
// --check exits 1 when tokens.css is out of date instead of writing it.
|
|
//
|
|
// Selector contract for consumers:
|
|
// no attributes default palette, light or dark from prefers-color-scheme
|
|
// data-mode="light|dim|dark" explicit mode (packages/webui already sets data-mode)
|
|
// data-palette="<id>" any non-default palette
|
|
// Property names match packages/webui/src/public/shared/app.css and brand.js
|
|
// (--onAction stays camelCase there), so this file can replace the inline
|
|
// setProperty theming without renaming any var() use.
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const src = join(here, '..', 'tokens.json');
|
|
const out = join(here, '..', 'tokens.css');
|
|
const t = JSON.parse(readFileSync(src, 'utf8'));
|
|
|
|
const strong = (mode) => mode === 'light'
|
|
? 'color-mix(in srgb, var(--text) 72%, #000)'
|
|
: 'color-mix(in srgb, var(--text) 70%, #fff)';
|
|
const colors = (modes, mode) => Object.entries(modes[mode]).map(([k, v]) => ` --${k}: ${v};`)
|
|
.concat(` --strong: ${strong(mode)};`, ` color-scheme: ${mode === 'light' ? 'light' : 'dark'};`).join('\n');
|
|
const block = (sel, body) => `${sel} {\n${body}\n}`;
|
|
const media = (sel, body) => `@media (prefers-color-scheme: dark) {\n ${sel} {\n${body.replace(/^/gm, ' ')}\n }\n}`;
|
|
|
|
const def = t.palettes.find((p) => p.id === t.default.palette);
|
|
if (!def) { console.error(`default palette ${t.default.palette} is not in tokens.json`); process.exit(2); }
|
|
|
|
const shared = [
|
|
` --font: ${t.font.sans.stack};`,
|
|
` --mono: ${t.font.mono.stack};`,
|
|
` --r: ${t.shape.radius.control}px;`,
|
|
` --r-lg: ${t.shape.radius.panel}px;`,
|
|
` --r-table: ${t.shape.radius.table}px;`,
|
|
` --pad: ${t.space.gutter}px;`,
|
|
` --gap: ${t.space.gap}px;`,
|
|
].join('\n');
|
|
|
|
const parts = [
|
|
`/* Generated from tokens.json by tools/build-tokens.mjs. Do not edit by hand. */`,
|
|
`/* Fonts: ${t.font.sans.family} and ${t.font.mono.family}. ${t.font.source} */`,
|
|
block(':root', shared),
|
|
`/* ${def.name}: the default palette */`,
|
|
block(':root', colors(def.modes, 'light')),
|
|
media(':root:not([data-mode="light"]):not([data-mode="dim"])', colors(def.modes, 'dark')),
|
|
block(':root[data-mode="dark"]', colors(def.modes, 'dark')),
|
|
block(':root[data-mode="dim"]', colors(def.modes, 'dim')),
|
|
];
|
|
for (const p of t.palettes) {
|
|
if (p.id === def.id) continue;
|
|
const s = `:root[data-palette="${p.id}"]`;
|
|
parts.push(`/* ${p.name} */`,
|
|
block(s, colors(p.modes, 'light')),
|
|
media(`${s}:not([data-mode="light"]):not([data-mode="dim"])`, colors(p.modes, 'dark')),
|
|
block(`${s}[data-mode="dark"]`, colors(p.modes, 'dark')),
|
|
block(`${s}[data-mode="dim"]`, colors(p.modes, 'dim')));
|
|
}
|
|
const css = parts.join('\n\n') + '\n';
|
|
|
|
if (process.argv.includes('--check')) {
|
|
let cur = '';
|
|
try { cur = readFileSync(out, 'utf8'); } catch {}
|
|
if (cur !== css) { console.error('tokens.css is out of date; run node docs/design/tools/build-tokens.mjs'); process.exit(1); }
|
|
console.log('tokens.css is current');
|
|
} else {
|
|
writeFileSync(out, css);
|
|
console.log(`wrote ${out} (${t.palettes.length} palettes x ${t.modes.length} modes)`);
|
|
}
|