diff --git a/web/.gitignore b/web/.gitignore index 9b2968c3..2168b4f6 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -2,3 +2,4 @@ node_modules/ .svelte-kit/ build/* !build/index.html +src/lib/styles/tokens.generated.css diff --git a/web/package-lock.json b/web/package-lock.json index 04e47889..1e842bb5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,6 +17,7 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", + "@types/node": "^25.6.0", "autoprefixer": "^10.4.20", "jsdom": "^25.0.1", "postcss": "^8.4.49", @@ -1334,6 +1335,16 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "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": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -3764,6 +3775,13 @@ "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": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", diff --git a/web/package.json b/web/package.json index 450868d4..c530c520 100644 --- a/web/package.json +++ b/web/package.json @@ -8,7 +8,8 @@ "build": "vite build", "preview": "vite preview", "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": { "@sveltejs/adapter-static": "^3.0.6", @@ -16,6 +17,7 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", + "@types/node": "^25.6.0", "autoprefixer": "^10.4.20", "jsdom": "^25.0.1", "postcss": "^8.4.49", diff --git a/web/scripts/tokens-to-css.js b/web/scripts/tokens-to-css.js new file mode 100644 index 00000000..34cd46b8 --- /dev/null +++ b/web/scripts/tokens-to-css.js @@ -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}`); diff --git a/web/src/app.css b/web/src/app.css index 7f0649f2..93afd080 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,4 +1,4 @@ -@import './lib/styles/fabledsword-tokens.css'; +@import './lib/styles/tokens.generated.css'; @tailwind base; @tailwind components; diff --git a/web/src/lib/styles/fabledsword-tokens.css b/web/src/lib/styles/fabledsword-tokens.css deleted file mode 100644 index ab166374..00000000 --- a/web/src/lib/styles/fabledsword-tokens.css +++ /dev/null @@ -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=""]` - * 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 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; -} diff --git a/web/src/lib/styles/tokens.json b/web/src/lib/styles/tokens.json new file mode 100644 index 00000000..e8fa04ec --- /dev/null +++ b/web/src/lib/styles/tokens.json @@ -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" + } +} diff --git a/web/vite.config.ts b/web/vite.config.ts index 2e290e15..068a5570 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,8 +1,16 @@ import { sveltekit } from '@sveltejs/kit/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({ - plugins: [sveltekit()], + plugins: [tokensPlugin, sveltekit()], server: { port: 5173, proxy: {