Files
stack/docs/design/tools/build-tokens.mjs
T
jason.woltjeandClaude Opus 5.5 811e7ba562 docs(design): handoff package for implementing the design
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]>
2026-10-10 11:22:06 -05:00

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)`);
}