feat(web/m7-362): tokens.json dark/light/flat split + dual-block CSS generator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 13:39:54 -04:00
parent a9fc0c6fe8
commit 9b7dd8272e
3 changed files with 98 additions and 40 deletions
+31 -15
View File
@@ -2,7 +2,7 @@
// Reads ../src/lib/styles/tokens.json and emits tokens.generated.css.
// Single source of truth for FabledSword tokens shared with the Flutter
// client. Keep output deterministic — Tailwind reads tokens.json directly,
// the .css emission is for raw CSS consumers.
// the .css emission is for raw CSS consumers and theme switching.
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
@@ -10,19 +10,35 @@ import { dirname, resolve } from 'node:path';
const here = dirname(fileURLToPath(import.meta.url));
const tokensPath = resolve(here, '../src/lib/styles/tokens.json');
const outPath = resolve(here, '../src/lib/styles/tokens.generated.css');
const tokens = JSON.parse(readFileSync(tokensPath, 'utf8'));
const lines = ['/* GENERATED — do not edit. Source: tokens.json */', ':root {'];
for (const [name, value] of Object.entries(tokens.colors)) {
lines.push(` --fs-${name}: ${value};`);
}
for (const [name, value] of Object.entries(tokens.radii)) {
lines.push(` --fs-radius-${name}: ${value};`);
}
lines.push(` --fs-font-display: ${tokens.fontStacks.display};`);
lines.push(` --fs-font-body: ${tokens.fontStacks.body};`);
lines.push(` --fs-font-mono: ${tokens.fontStacks.mono};`);
lines.push('}', '[data-fs-app="minstrel"] {', ` --fs-accent: ${tokens.colors.accent};`, '}', '');
export function emit(tokens) {
const lines = ['/* GENERATED — do not edit. Source: tokens.json */', ':root {'];
for (const [name, value] of Object.entries(tokens.colors.dark)) {
lines.push(` --fs-${name}: ${value};`);
}
for (const [name, value] of Object.entries(tokens.colors.flat)) {
lines.push(` --fs-${name}: ${value};`);
}
for (const [name, value] of Object.entries(tokens.radii)) {
lines.push(` --fs-radius-${name}: ${value};`);
}
lines.push(` --fs-font-display: ${tokens.fontStacks.display};`);
lines.push(` --fs-font-body: ${tokens.fontStacks.body};`);
lines.push(` --fs-font-mono: ${tokens.fontStacks.mono};`);
lines.push('}');
writeFileSync(outPath, lines.join('\n'));
console.log(`wrote ${outPath}`);
lines.push('[data-theme="light"] {');
for (const [name, value] of Object.entries(tokens.colors.light)) {
lines.push(` --fs-${name}: ${value};`);
}
lines.push('}');
lines.push('[data-fs-app="minstrel"] {', ` --fs-accent: ${tokens.colors.flat.accent};`, '}', '');
return lines.join('\n');
}
if (import.meta.url === `file://${process.argv[1]}`) {
const tokens = JSON.parse(readFileSync(tokensPath, 'utf8'));
writeFileSync(outPath, emit(tokens));
console.log(`wrote ${outPath}`);
}
+37
View File
@@ -0,0 +1,37 @@
/* GENERATED — do not edit. Source: tokens.json */
:root {
--fs-obsidian: #14171A;
--fs-iron: #1E2228;
--fs-slate: #2C313A;
--fs-pewter: #3F4651;
--fs-parchment: #E8E4D8;
--fs-vellum: #C2BFB4;
--fs-ash: #9C9A92;
--fs-moss: #4A5D3F;
--fs-bronze: #8B7355;
--fs-oxblood: #6B2118;
--fs-warning: #8B6F1E;
--fs-error: #C04A1F;
--fs-info: #3D5A6E;
--fs-accent: #4A6B5C;
--fs-on-action: #E8E4D8;
--fs-radius-sm: 4px;
--fs-radius-md: 8px;
--fs-radius-lg: 12px;
--fs-radius-xl: 16px;
--fs-font-display: 'Fraunces', Georgia, serif;
--fs-font-body: 'Inter', system-ui, sans-serif;
--fs-font-mono: 'JetBrains Mono', ui-monospace, monospace;
}
[data-theme="light"] {
--fs-obsidian: #F8F5EE;
--fs-iron: #ECE6D5;
--fs-slate: #DCD3BD;
--fs-pewter: #B8AE94;
--fs-parchment: #14171A;
--fs-vellum: #2C313A;
--fs-ash: #5C6068;
}
[data-fs-app="minstrel"] {
--fs-accent: #4A6B5C;
}
+30 -25
View File
@@ -1,31 +1,36 @@
{
"colors": {
"obsidian": "#14171A",
"iron": "#1E2228",
"slate": "#2C313A",
"pewter": "#3F4651",
"parchment": "#E8E4D8",
"vellum": "#C2BFB4",
"ash": "#9C9A92",
"moss": "#4A5D3F",
"bronze": "#8B7355",
"oxblood": "#6B2118",
"warning": "#8B6F1E",
"error": "#C04A1F",
"info": "#3D5A6E",
"accent": "#4A6B5C"
},
"radii": {
"sm": "4px",
"md": "8px",
"lg": "12px",
"xl": "16px"
},
"fonts": {
"display": "Fraunces",
"body": "Inter",
"mono": "JetBrains Mono"
"dark": {
"obsidian": "#14171A",
"iron": "#1E2228",
"slate": "#2C313A",
"pewter": "#3F4651",
"parchment": "#E8E4D8",
"vellum": "#C2BFB4",
"ash": "#9C9A92"
},
"light": {
"obsidian": "#F8F5EE",
"iron": "#ECE6D5",
"slate": "#DCD3BD",
"pewter": "#B8AE94",
"parchment": "#14171A",
"vellum": "#2C313A",
"ash": "#5C6068"
},
"flat": {
"moss": "#4A5D3F",
"bronze": "#8B7355",
"oxblood": "#6B2118",
"warning": "#8B6F1E",
"error": "#C04A1F",
"info": "#3D5A6E",
"accent": "#4A6B5C",
"on-action": "#E8E4D8"
}
},
"radii": { "sm": "4px", "md": "8px", "lg": "12px", "xl": "16px" },
"fonts": { "display": "Fraunces", "body": "Inter", "mono": "JetBrains Mono" },
"fontStacks": {
"display": "'Fraunces', Georgia, serif",
"body": "'Inter', system-ui, sans-serif",