refactor(web): extract design tokens to tokens.json source of truth

The Flutter client (#356) needs to consume the same FabledSword tokens
the web SPA uses. Move the data into a structured tokens.json file and
generate tokens.generated.css from it on every Vite build. The Flutter
side gets its own generator pointed at the same JSON.

No visual change — generated CSS matches the previous hand-written file.

Also installs @types/node devDependency: tsconfig already declared
"types": ["node"] but no Node builtins were referenced in TS until now,
so the missing types were latent. The new vite.config.ts plugin imports
node:child_process, surfacing the gap.
This commit is contained in:
2026-05-02 14:55:46 -04:00
parent 261c2c4301
commit ea22807a3f
8 changed files with 94 additions and 55 deletions
+1
View File
@@ -2,3 +2,4 @@ node_modules/
.svelte-kit/ .svelte-kit/
build/* build/*
!build/index.html !build/index.html
src/lib/styles/tokens.generated.css
+18
View File
@@ -17,6 +17,7 @@
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1", "@testing-library/svelte": "^5.3.1",
"@types/node": "^25.6.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"jsdom": "^25.0.1", "jsdom": "^25.0.1",
"postcss": "^8.4.49", "postcss": "^8.4.49",
@@ -1334,6 +1335,16 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
}
},
"node_modules/@types/trusted-types": { "node_modules/@types/trusted-types": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -3764,6 +3775,13 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+3 -1
View File
@@ -8,7 +8,8 @@
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"test": "svelte-kit sync && vitest run" "test": "svelte-kit sync && vitest run",
"tokens": "node scripts/tokens-to-css.js"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.6",
@@ -16,6 +17,7 @@
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1", "@testing-library/svelte": "^5.3.1",
"@types/node": "^25.6.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"jsdom": "^25.0.1", "jsdom": "^25.0.1",
"postcss": "^8.4.49", "postcss": "^8.4.49",
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env node
// 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.
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
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};`, '}', '');
writeFileSync(outPath, lines.join('\n'));
console.log(`wrote ${outPath}`);
+1 -1
View File
@@ -1,4 +1,4 @@
@import './lib/styles/fabledsword-tokens.css'; @import './lib/styles/tokens.generated.css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
-52
View File
@@ -1,52 +0,0 @@
/*
* FabledSword design system tokens
*
* Shared palette across FabledSword apps (Minstrel, Scribe, Forge, ...).
* Per-app accent colour is overridable via the `[data-fs-app="<name>"]`
* attribute hook. The `:root` default is forest-teal for Minstrel, the
* only consumer of this file today.
*/
:root {
/* Surfaces */
--fs-obsidian: #14171A;
--fs-iron: #1E2228;
--fs-slate: #2C313A;
--fs-pewter: #3F4651;
/* Text */
--fs-parchment: #E8E4D8;
--fs-vellum: #C2BFB4;
--fs-ash: #9C9A92;
/* Action */
--fs-moss: #4A5D3F;
--fs-bronze: #8B7355;
--fs-oxblood: #6B2118;
/* Semantic */
--fs-warning: #8B6F1E;
--fs-error: #C04A1F;
--fs-info: #3D5A6E;
/* Accent (per-app; default = Minstrel forest-teal) */
--fs-accent: #4A6B5C;
/* Radii */
--fs-radius-sm: 4px;
--fs-radius-md: 8px;
--fs-radius-lg: 12px;
--fs-radius-xl: 16px;
/* Fonts */
--fs-font-display: 'Fraunces', Georgia, serif;
--fs-font-body: 'Inter', system-ui, sans-serif;
--fs-font-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Per-app accent overrides. Set `data-fs-app="minstrel"` on <html> or any
* ancestor to scope; the :root default already matches Minstrel, so this
* block is documentation / future override-point for other FS apps. */
[data-fs-app="minstrel"] {
--fs-accent: #4A6B5C;
}
+34
View File
@@ -0,0 +1,34 @@
{
"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"
},
"fontStacks": {
"display": "'Fraunces', Georgia, serif",
"body": "'Inter', system-ui, sans-serif",
"mono": "'JetBrains Mono', ui-monospace, monospace"
}
}
+9 -1
View File
@@ -1,8 +1,16 @@
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { execSync } from 'node:child_process';
const tokensPlugin = {
name: 'minstrel-tokens',
buildStart() {
execSync('node scripts/tokens-to-css.js', { stdio: 'inherit' });
}
};
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()], plugins: [tokensPlugin, sveltekit()],
server: { server: {
port: 5173, port: 5173,
proxy: { proxy: {