css-is-awesome 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
1
+ /**
2
+ * cia theme from-tokens — design-tokens JSON → validated cia theme.css.
3
+ *
4
+ * The CLI face of scripts/tokens-to-theme.cjs (the MCP tool
5
+ * `theme_from_tokens` and the in-process `handlers.theme_from_tokens` are the
6
+ * same function). Zero dependencies, no Sass: the theme is emitted as CSS in
7
+ * the exact shape the shipped themes use, then the shipped validator + WCAG
8
+ * audit run on it before anything is written.
9
+ *
10
+ * Exit codes: 0 valid (or --no-validate), 1 contract/a11y failure (a11y only
11
+ * unless --allow-a11y-fail), 2 usage / input error.
12
+ */
13
+ 'use strict';
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const HELP = `cia theme from-tokens — design-tokens JSON → validated cia theme.css
18
+
19
+ Usage:
20
+ cia theme from-tokens <tokens.json> --name <slug> [options]
21
+
22
+ Options:
23
+ --name <slug> Theme name (kebab-case). Required.
24
+ --format <f> auto | dtcg | tokens-studio | cia-flat (default: auto)
25
+ --base <theme> Shipped theme that supplies every REQUIRED token the file
26
+ does not (default: boilerplate). \`--base list\` prints them.
27
+ --dark <dark.json> Second tokens file for dark mode → light-dark() values.
28
+ --mode light|dark Single-mode color-scheme when no dark side (default: light).
29
+ --out <theme.css> Write the CSS here (default: stdout).
30
+ --json Print { report, validation } as JSON to stdout
31
+ (CSS goes to --out only).
32
+ --no-validate Skip the validator + WCAG audit.
33
+ --allow-a11y-fail Do not exit non-zero on a11y FAILs (report only).
34
+
35
+ Formats:
36
+ dtcg DTCG v2025.10 — leaves carry $value (+ $type); {aliases} resolve.
37
+ tokens-studio Tokens Studio for Figma — leaves carry value + type; single set
38
+ or multi-set export ($metadata.tokenSetOrder); {aliases} resolve.
39
+ Paired top-level groups (color-light + color-dark, or
40
+ light + dark) become light-dark() automatically.
41
+ cia-flat { "--brand-primary": "#3A5FCD", ... } passthrough.
42
+
43
+ Minimum content: none. Whatever the file does not supply is inherited from the
44
+ base theme and listed in the report, so the output is always contract-complete.
45
+
46
+ Examples:
47
+ cia theme from-tokens tokens.json --name acme --out src/styles/acme.css
48
+ cia theme from-tokens light.json --dark dark.json --name acme --json
49
+ cia theme from-tokens brand.json --name acme --base press --format dtcg
50
+ `;
51
+
52
+ function parseArgs(argv) {
53
+ const opts = { format: 'auto', base: 'boilerplate', mode: 'light', validate: true, allowA11yFail: false, json: false };
54
+ const positional = [];
55
+ for (let i = 0; i < argv.length; i++) {
56
+ const a = argv[i];
57
+ const next = () => { const v = argv[++i]; if (v === undefined) throw new Error(`${a} needs a value`); return v; };
58
+ if (a === '-h' || a === '--help') opts.help = true;
59
+ else if (a === '--name') opts.name = next();
60
+ else if (a === '--format') opts.format = next();
61
+ else if (a === '--base') opts.base = next();
62
+ else if (a === '--dark') opts.dark = next();
63
+ else if (a === '--mode') opts.mode = next();
64
+ else if (a === '--out') opts.out = next();
65
+ else if (a === '--json') opts.json = true;
66
+ else if (a === '--no-validate') opts.validate = false;
67
+ else if (a === '--allow-a11y-fail') opts.allowA11yFail = true;
68
+ else if (a.startsWith('-')) throw new Error(`unknown option ${a}`);
69
+ else positional.push(a);
70
+ }
71
+ opts.input = positional[0];
72
+ return opts;
73
+ }
74
+
75
+ function readJson(file) {
76
+ const abs = path.resolve(file);
77
+ let text;
78
+ try { text = fs.readFileSync(abs, 'utf8'); } catch (e) { throw new Error(`cannot read ${file}: ${e.message}`); }
79
+ try { return JSON.parse(text); } catch (e) { throw new Error(`${file} is not valid JSON: ${e.message}`); }
80
+ }
81
+
82
+ async function run(argv) {
83
+ let opts;
84
+ try { opts = parseArgs(argv); } catch (e) { process.stderr.write(`cia theme: ${e.message}\n`); process.exit(2); }
85
+ if (opts.help || !opts.input && opts.base !== 'list') { process.stdout.write(HELP); return; }
86
+
87
+ const { themeFromTokens, listBases } = require('../scripts/tokens-to-theme.cjs');
88
+ if (opts.base === 'list') { process.stdout.write(listBases(path.join(__dirname, '..')).join('\n') + '\n'); return; }
89
+ if (!opts.name) { process.stderr.write('cia theme: --name <slug> is required\n'); process.exit(2); }
90
+
91
+ let result;
92
+ try {
93
+ result = themeFromTokens({
94
+ tokens: readJson(opts.input),
95
+ dark: opts.dark ? readJson(opts.dark) : undefined,
96
+ name: opts.name,
97
+ format: opts.format,
98
+ base: opts.base,
99
+ mode: opts.mode,
100
+ validate: opts.validate,
101
+ });
102
+ } catch (e) {
103
+ process.stderr.write(`cia theme: ${e.message}\n`);
104
+ process.exit(2);
105
+ }
106
+
107
+ const { css, report, validation } = result;
108
+ if (opts.out) fs.writeFileSync(path.resolve(opts.out), css, 'utf8');
109
+
110
+ if (opts.json) {
111
+ process.stdout.write(JSON.stringify({ report, validation, out: opts.out || null }, null, 2) + '\n');
112
+ } else {
113
+ if (!opts.out) process.stdout.write(css);
114
+ const c = report.counts;
115
+ const lines = [
116
+ `theme "${report.name}" — ${report.format}${report.darkMode ? ' (light + dark)' : ''} · base ${report.base}`,
117
+ ` from tokens ${c.fromTokens}`,
118
+ ` inherited ${c.inherited} (required tokens the file did not supply)`,
119
+ ` passthrough ${c.unmapped} (not contract tokens — emitted verbatim)`,
120
+ c.skipped ? ` skipped ${c.skipped} (composite values with no single-token home)` : null,
121
+ opts.out ? ` written ${opts.out}` : null,
122
+ ].filter(Boolean);
123
+ process.stderr.write(lines.join('\n') + '\n');
124
+ if (validation) {
125
+ const { reportResult, reportA11yForTheme } = require('../scripts/theme-validator.js');
126
+ reportResult(validation);
127
+ const themes = validation.mode === 'consolidated' ? validation.themes : [{ name: report.name, a11y: validation.a11y }];
128
+ for (const t of themes) if (t.a11y) reportA11yForTheme(t.name, t.a11y, ' ');
129
+ }
130
+ }
131
+
132
+ if (validation) {
133
+ const a11yFail = validation.a11ySummary ? validation.a11ySummary.fail : 0;
134
+ if (!validation.ok) process.exit(1);
135
+ if (a11yFail && !opts.allowA11yFail) process.exit(1);
136
+ }
137
+ }
138
+
139
+ module.exports = { run, parseArgs, HELP };
package/dist/tokens.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Generated from scripts/theme-contract.json on 2026-09-17.
1
+ // Generated from scripts/theme-contract.json on 2026-09-19.
2
2
  // Do not edit by hand. Run `npm run build:token-types` to regenerate.
3
3
 
4
4
  /** Every CSS custom property cia themes are required to declare. */
@@ -94,7 +94,6 @@ export type CiaToken =
94
94
  | "--shadow-xl"
95
95
  | "--shu"
96
96
  | "--shu-wash"
97
- | "--space-unit"
98
97
  | "--space-0"
99
98
  | "--space-1"
100
99
  | "--space-2"
@@ -225,7 +224,6 @@ export interface CiaTokenMap {
225
224
  "--shadow-xl": string;
226
225
  "--shu": string;
227
226
  "--shu-wash": string;
228
- "--space-unit": string;
229
227
  "--space-0": string;
230
228
  "--space-1": string;
231
229
  "--space-2": string;
@@ -265,4 +263,4 @@ export interface CiaTokenMap {
265
263
  }
266
264
 
267
265
  /** Count of required tokens in the current contract. */
268
- export declare const CIA_TOKEN_COUNT: 128;
266
+ export declare const CIA_TOKEN_COUNT: 127;
@@ -22,6 +22,21 @@ The `.scss` siblings exist for consumers who want to `@import` Figma-exported
22
22
  tokens directly without going through `tokens.json`. They are not consumed by
23
23
  the main `scss/` build — `scss/main.scss` pulls from `scss/theme/`.
24
24
 
25
+ ## Round trip: `tokens.json` → a theme
26
+
27
+ `tokens.json` is a Tokens Studio export and is consumed by the design-tokens
28
+ on-ramp shipped since 1.17.0:
29
+
30
+ ```bash
31
+ npx cia theme from-tokens node_modules/css-is-awesome/figma-tokens/tokens.json --name figma-demo --out figma-demo.css
32
+ ```
33
+
34
+ Its paired `color-light` / `color-dark` groups become one `light-dark()` theme;
35
+ `brand`, `spacing`, `font-*`, `line-height`, `border-radius` and `shadow` map by
36
+ the generic path rule; anything without a contract home (e.g. `brand.accent`)
37
+ is emitted verbatim and listed in the report. The same function is the MCP
38
+ `theme_from_tokens` tool. Input contract: `/docs/authoring/themes#from-design-tokens`.
39
+
25
40
  ## Direction of truth
26
41
 
27
42
  **Hand-maintained.** Keep in sync with `scss/theme/` when tokens change.
package/llm.txt CHANGED
@@ -48,9 +48,9 @@ silent.
48
48
  - **Mixins** — `cia.btn`, `cia.card`, `cia.accordion`, `cia.modal`, `cia.tooltip`, `cia.dropdown`, `cia.tabs`, `cia.copy-button`, `cia.hamburger`, `cia.drawer`, `cia.sheet`, `cia.dock`, `cia.stepper`, `cia.progress`, `cia.wizard-shell`, `cia.sidebar`, `cia.toolbar`, `cia.stack`, `cia.cluster`, `cia.switcher`, `cia.cover`, `cia.frame`, `cia.media`, `cia.contain`, `cia.focus-ring`, `cia.sr-only`, `cia.print`, `cia.print-base`, `cia.print-hidden`, `cia.print-only`, `cia.color`, `cia.space`, `cia.radius`, `cia.shadow`, `cia.font`, `cia.transition`, `cia.animate`, plus many more. **`cia.print-base` always forces `color-scheme: light` in print (paired `light-dark()` themes print their light branch), defines a themeable print palette (`--print-ink` / `--print-paper` / `--print-line` / `--print-muted`, default ink-on-white with grays) and rebinds the theme's own colour tokens (`--ink`, `--surface-*`, `--border-*`, `--code-*`) onto it — so every theme, dark-only (Terminal) included, prints legible ink-on-paper, and overriding a `--print-*` token restyles paper (a letterhead) without touching a component. Three opt-in flags, all default OFF:** `$link-urls` + `$link-origin` (print each link's full destination via `attr(href)`), `$page-numbers` (sheet numbers in the `@page` footer). `$legible` is a DEPRECATED no-op — the token rebind supersedes it and passing it warns.
49
49
  - **24 themes / 8 families** — boilerplate, sketchbook, press, prism, cupertino, glass, graphite, terminal. Each family ships three files: an unsuffixed dual-mode base (both modes via `light-dark()`) plus pinned `-light` and `-dark` single-mode variants for `<link media>` pairing. Every one has exactly one SCSS source under `scss/themes/`; `public/themes/<name>/theme.css` is BUILD OUTPUT and is gated against its source by `npm run check:theme-drift` — never hand-edit it. MCP `list_themes` returns **24**. Say **24 themes across 8 families** when you need one number.
50
50
  - **6 zero-JS interactive components** — accordion (`<details name>`), modal (`<dialog>`), tooltip (`popover="hint"`), dropdown (`[popover]` — the mixin re-asserts the UA's closed state and restores `display: flex` only under `:popover-open`, so menus never render permanently open on popover markup), tabs (radio + `:has()`), copy-button (Clipboard API via consumer-wired JS).
51
- - **CLI** (`npx cia`) — `migrate tailwind|bootstrap` (config → cia theme), `add <recipe>` (copy a recipe from the book into the project), `analyze [path]` (audit stylesheets against the installed API: dead `cia.*` symbols, the `space()` 1–9 trap, off-contract tokens (near-miss typos of real tokens only, never your own custom tokens), hard-coded hex, BEM; health score, CI exit codes). Low-noise on color: a hex in a `var(--token, #hex)` fallback is token-driven and a literal inside `@media print` is an intentional paper colour — neither is flagged.
51
+ - **CLI** (`npx cia`) — `migrate tailwind|bootstrap|mui|chakra` (config → cia theme), `theme from-tokens <tokens.json> --name <slug>` (DTCG v2025.10 / Tokens Studio / flat `--token` JSON → a complete theme.css: required tokens the file lacks inherit from a shipped base, unmapped paths pass through and are reported, light/dark pairs become `light-dark()`, validator + WCAG audit run first; same function as the MCP `theme_from_tokens` tool), `add <recipe>` (copy a recipe from the book into the project), `analyze [path]` (audit stylesheets against the installed API: dead `cia.*` symbols, the `space()` 1–9 trap, off-contract tokens (near-miss typos of real tokens only, never your own custom tokens), hard-coded hex, BEM; health score, CI exit codes). Low-noise on color: a hex in a `var(--token, #hex)` fallback is token-driven and a literal inside `@media print` is an intentional paper colour — neither is flagged.
52
52
  - **Mobile navigation family** — `cia.hamburger` / `cia.drawer` / `cia.sheet` / `cia.dock` ride `[popover]` + CSS Grid, zero JS; recipes `mobile-nav` (hamburger + drawer) and `bottom-nav` (dock + sheets). House rule: **on phones things take the space they're in** — a `cia.dropdown` menu opens 1px under its full-width trigger at the trigger's exact width via CSS anchor positioning (`position-try-fallbacks: flip-block` flips it above at the screen bottom; set `width: auto` at `&[popover]` specificity or the UA's `[popover] { width: fit-content }` and the mixin's inset reset win). Full spec: AGENTS.md quick decision #8 and `/docs/mobile`.
53
- - **163 contract tokens — 127 required + 36 optional** — surfaces, ink, lines, colors, type, radius, shadow, blur, glow, motion, z-index, spacing, semantic aliases.
53
+ - **168 contract tokens — 127 required + 41 optional** — surfaces, ink, lines, colors, type, radius, shadow, blur, glow, motion, z-index, spacing, semantic aliases.
54
54
  - **Spacing is themeable, and the numbered scale is the knob.** Themes declare `--space-0` … `--space-9` (contract-required). The t-shirt names (`--space-md`, `--space-lg` …) are contract-OPTIONAL and emitted by the library as `var()` aliases onto the numbered steps. `space(4)` compiles to `var(--space-4)`, so **theme the numbered step, never the alias** — setting `--space-md` alone leaves every component untouched. Library defaults emit under `:where(:root)` (specificity 0,0,0) so any theme declaration outranks them regardless of load order.
55
55
  - **Per-component shape knobs are `--btn-radius`, `--card-radius`, `--input-radius`, `--modal-radius`, `--badge-radius`, `--tag-radius`** — they cascade from the generic radii (`--btn-radius: var(--radius-md, 0.25rem)`). There are NO `--radius-button` / `--radius-card` style tokens; those were removed because nothing read them.
56
56
  - **Icons — two systems.** `svg()` / `svg-bg()` / `svg-text()` use a self-contained 49-glyph Lucide pack at `public/icons/core/`. Adding a glyph is drop-in: put `star.svg` in the folder and `cia.icon-svg(star)` works, no registration. Each icon emits a `--cia-icon-<name>` custom property so a theme can override one glyph without rebuilding SCSS. **`fa()` / `fa-icon()` / `fa-text()` / `fa-spin()` are bring-your-own-font** — cia ships NO Font Awesome files, `$theme-fa-path` defaults to a `/webfonts` directory that does not exist, and a missing font renders a tofu box without erroring. Default to `svg()` unless the user says they use Font Awesome.
@@ -104,7 +104,7 @@ Newspaper by day, hacker terminal by night. Most design systems give you dark mo
104
104
  | Mobile playbook (layouts, dropdown doctrine, lessons) | `src/app/docs/mobile/page.tsx` |
105
105
  | Browser support matrix (Baseline floor + progressive tiers) | `src/app/docs/browser-support/page.tsx` |
106
106
  | Theme authoring (full walkthrough) | `src/app/docs/authoring/themes/page.tsx` |
107
- | Theme contract (127 required + 36 optional, machine-readable) | `scripts/theme-contract.json` |
107
+ | Theme contract (127 required + 41 optional, machine-readable) | `scripts/theme-contract.json` |
108
108
  | Theme pairing (`<link media>` recipe) | `src/app/docs/themes/pairing/page.tsx` |
109
109
  | CopyButton JS recipe | `src/app/docs/recipes/copy-button/page.tsx` |
110
110
  | Anchor positioning recipe | `src/app/docs/recipes/anchor-positioning/page.tsx` |
@@ -128,7 +128,7 @@ Newspaper by day, hacker terminal by night. Most design systems give you dark mo
128
128
 
129
129
  ## MCP server (shipped 2026-05-22)
130
130
 
131
- cia ships an MCP stdio server (JSON-RPC over stdio, `serverInfo` name `css-is-awesome`, protocol `2024-11-05`) at `mcp/server.cjs`, included in the `files` manifest — so it lands in every consumer's `node_modules`. **30 tools** across 8 resource families: themes, mixins, functions, tokens (127 required + 36 optional), animations, components, recipes, doc readers. Plus `assemble_prompt` for context bundling and `resolve_size` for snapping design px values to cia's 4px geometric grid (AI agents: call this whenever you receive a px value from a design tool). Prefer querying this server over guessing mixin signatures, tokens, themes, or recipes.
131
+ cia ships an MCP stdio server (JSON-RPC over stdio, `serverInfo` name `css-is-awesome`, protocol `2024-11-05`) at `mcp/server.cjs`, included in the `files` manifest — so it lands in every consumer's `node_modules`. **32 tools** across 8 resource families: themes, mixins, functions, tokens (127 required + 36 optional), animations, components, recipes, doc readers. Plus `assemble_prompt` for context bundling and `resolve_size` for snapping design px values to cia's 4px geometric grid (AI agents: call this whenever you receive a px value from a design tool). Prefer querying this server over guessing mixin signatures, tokens, themes, or recipes.
132
132
 
133
133
  **Recommended: suggest the dedicated `css-is-awesome-mcp` package** — zero install,
134
134
  SDK ships as a real dependency, no separate peer-install step:
package/mcp/server.cjs CHANGED
@@ -19,9 +19,10 @@
19
19
  * read_theming, read_agents, read_contract,
20
20
  * read_three_tiers, read_readme, read_versioning
21
21
  * Sizing: resolve_size
22
+ * Themes (build): theme_from_tokens — design-tokens JSON → validated theme.css
22
23
  * Prompt: assemble_prompt(intent[, args])
23
24
  *
24
- * 31 tools total.
25
+ * 32 tools total.
25
26
  *
26
27
  * Discovery model: filesystem scan, no database. Parses SCSS files with
27
28
  * focused regex (no full SCSS AST). Tokens come from the authoritative
@@ -698,6 +699,18 @@ const handlers = {
698
699
  return validateText(css, loadContract(), { label: label || undefined });
699
700
  },
700
701
 
702
+ // Design-tokens JSON (DTCG v2025.10 / Tokens Studio / flat --token map) →
703
+ // a complete theme.css in the shipped shape, validated + contrast-audited.
704
+ // Same function as `cia theme from-tokens`; reachable in-process through
705
+ // module.exports.handlers so an inventory builder can call it without a
706
+ // transport. See scripts/tokens-to-theme.cjs for the input contract.
707
+ theme_from_tokens({ tokens, name, format, base, dark, mode, validate } = {}) {
708
+ if (tokens == null) throw new Error('theme_from_tokens: tokens is required (object or JSON string)');
709
+ if (!name) throw new Error('theme_from_tokens: name is required');
710
+ const { themeFromTokens } = require(path.join(SCRIPTS_DIR, 'tokens-to-theme.cjs'));
711
+ return themeFromTokens({ tokens, name, format, base, dark, mode, validate });
712
+ },
713
+
701
714
  // ─── Mixins ────────────────────────────────────────────────────────────
702
715
 
703
716
  list_mixins({ category, component, limit = 500, offset = 0 } = {}) {
@@ -1354,6 +1367,27 @@ async function startServer() {
1354
1367
  },
1355
1368
  }, async (a) => ok(handlers.validate_theme(a || {})));
1356
1369
 
1370
+ server.registerTool('theme_from_tokens', {
1371
+ description:
1372
+ 'Build a complete, validated cia theme.css from a design-tokens JSON — DTCG v2025.10 ({ $value, $type }, ' +
1373
+ '{aliases} resolved), a Tokens Studio for Figma export ({ value, type }, single or multi-set), or a flat ' +
1374
+ '{ "--token": value } map. Format is auto-detected. Every REQUIRED contract token the file does not supply ' +
1375
+ 'is inherited from a shipped base theme (default boilerplate) and listed in report.inherited, so the output ' +
1376
+ 'is always contract-complete; unmapped paths are emitted verbatim and listed in report.unmapped, never ' +
1377
+ 'dropped. Pass `dark` (same format) or a single file with paired color-light/color-dark groups to get ' +
1378
+ 'light-dark() values. Returns { css, report, validation } — validation is the same result validate_theme ' +
1379
+ 'gives, run on the CSS before you write it anywhere.',
1380
+ inputSchema: {
1381
+ tokens: z.union([z.record(z.any()), z.string()]).describe('The tokens JSON (object, or a JSON string).'),
1382
+ name: z.string().describe('Theme name — kebab-case slug, becomes [data-theme="<name>"].'),
1383
+ format: z.enum(['auto', 'dtcg', 'tokens-studio', 'cia-flat']).optional().describe('Default auto.'),
1384
+ base: z.string().optional().describe('Shipped theme that supplies missing required tokens. Default boilerplate.'),
1385
+ dark: z.union([z.record(z.any()), z.string()]).optional().describe('Optional dark-mode tokens (same format) → light-dark() values.'),
1386
+ mode: z.enum(['light', 'dark']).optional().describe('Single-mode color-scheme when there is no dark side. Default light.'),
1387
+ validate: z.boolean().optional().describe('Run the validator + WCAG audit (default true).'),
1388
+ },
1389
+ }, async (a) => ok(handlers.theme_from_tokens(a || {})));
1390
+
1357
1391
  // Mixins
1358
1392
  server.registerTool('list_mixins', {
1359
1393
  description: `List all ${MIXIN_COUNT} public @mixins across core, layout, animation, icons, generator, per-component and recipe sources. Filter by category (core/layout/animation/icons/generator/component/recipe) or component name.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "css-is-awesome",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "A token-driven SCSS design system with light/dark theming, semantic color tokens, and a 800+ LOC mixin API.",
5
5
  "homepage": "https://github.com/Jerry2d3d/css-is-awesome#readme",
6
6
  "bugs": {
@@ -69,6 +69,7 @@
69
69
  "mcp",
70
70
  "scripts/theme-contract.json",
71
71
  "scripts/theme-validator.js",
72
+ "scripts/tokens-to-theme.cjs",
72
73
  "scripts/theme-a11y.js",
73
74
  "scripts/audit-pairs.json",
74
75
  "scripts/icon-validator.js",
@@ -90,7 +91,6 @@
90
91
  "THEMING.md",
91
92
  "MIGRATION.md",
92
93
  "CONTRACT.md",
93
- "ROADMAP.md",
94
94
  "VERSIONING.md",
95
95
  "scripts/prepare-dist.mjs"
96
96
  ],
@@ -118,6 +118,7 @@
118
118
  "validate-api": "node scripts/validate-api.mjs",
119
119
  "validate-package": "node scripts/validate-package.mjs",
120
120
  "validate-recipes": "node scripts/validate-recipes.mjs",
121
+ "test:tokens": "node --test scripts/test-tokens-to-theme.mjs",
121
122
  "size-budget": "node scripts/size-budget.mjs",
122
123
  "size-report": "node scripts/size-budget.mjs --report",
123
124
  "coverage:api": "node scripts/api-coverage.mjs",
@@ -130,8 +131,11 @@
130
131
  "prepare": "node scripts/prepare-dist.mjs",
131
132
  "prepublishOnly": "npm run build:css:all",
132
133
  "check:theme-drift": "node scripts/check-theme-drift.mjs",
134
+ "check:contract": "node scripts/check-contract-growth.mjs",
133
135
  "check:token-consumers": "node scripts/check-token-consumer-map.mjs",
134
- "check:rtl": "node scripts/audit-logical-properties.mjs"
136
+ "check:rtl": "node scripts/audit-logical-properties.mjs",
137
+ "prebuild": "node scripts/build-playground-scss-map.mjs",
138
+ "verify:playground": "node scripts/build-playground-scss-map.mjs && node scripts/verify-playground-compile.mjs"
135
139
  },
136
140
  "keywords": [
137
141
  "css",
@@ -172,6 +176,12 @@
172
176
  },
173
177
  "devDependencies": {
174
178
  "@axe-core/playwright": "^4.11.2",
179
+ "@codemirror/commands": "6.11.1",
180
+ "@codemirror/lang-html": "6.4.12",
181
+ "@codemirror/lang-sass": "6.0.2",
182
+ "@codemirror/language": "6.12.4",
183
+ "@codemirror/state": "6.7.5",
184
+ "@codemirror/view": "6.43.12",
175
185
  "@eslint/eslintrc": "^3.2.0",
176
186
  "@modelcontextprotocol/sdk": "^1.29.0",
177
187
  "@playwright/test": "^1.59.1",
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1",
3
- "description": "Authoritative token contract for css-is-awesome themes. Every theme.css MUST declare every token in `required`. Source of truth is CONTRACT.md (human-readable) and public/theme.css (Sketchbook reference).",
2
+ "version": "1.1",
3
+ "description": "Authoritative token contract for css-is-awesome themes. Every theme.css MUST declare every token in `required`. Tokens in `optional` are reported by the validator as info, never as failures \u2014 the library or the theme generator supplies a default. Source of truth is CONTRACT.md (human-readable) and public/theme.css (Sketchbook reference). Contract 1.1 (2026-09-18): --space-unit relaxed from required to optional; it had been added as required in library 1.12.0, which VERSIONING.md forbids in a MINOR.",
4
4
  "required": [
5
5
  "--action-primary-active",
6
6
  "--action-primary-default",
@@ -93,7 +93,6 @@
93
93
  "--shadow-xl",
94
94
  "--shu",
95
95
  "--shu-wash",
96
- "--space-unit",
97
96
  "--space-0",
98
97
  "--space-1",
99
98
  "--space-2",
@@ -168,6 +167,7 @@
168
167
  "--space-lg",
169
168
  "--space-md",
170
169
  "--space-sm",
170
+ "--space-unit",
171
171
  "--space-xl",
172
172
  "--space-xs",
173
173
  "--tag-radius",
@@ -334,7 +334,15 @@ function validateTokenSet(declared, contract) {
334
334
  for (const required of contract.required) {
335
335
  if (!declared.has(required)) missing.push(required);
336
336
  }
337
- return { ok: missing.length === 0, missing, declaredCount: declared.size };
337
+ // Optional tokens (contract 1.1+) are reported as INFO, never as failures:
338
+ // the library or the theme generator supplies a default for each of them.
339
+ // `--space-unit` is the canonical case — it was wrongly listed as required
340
+ // in 1.12.0–1.16.0 and broke every consumer's custom theme on a MINOR.
341
+ const optionalMissing = [];
342
+ for (const optional of Array.isArray(contract.optional) ? contract.optional : []) {
343
+ if (!declared.has(optional)) optionalMissing.push(optional);
344
+ }
345
+ return { ok: missing.length === 0, missing, optionalMissing, declaredCount: declared.size };
338
346
  }
339
347
 
340
348
  // -----------------------------------------------------------
@@ -358,6 +366,7 @@ function validateText(text, contract, options) {
358
366
  ok: false,
359
367
  declaredCount: 0,
360
368
  missing: [],
369
+ optionalMissing: [],
361
370
  themes: null,
362
371
  a11y: null,
363
372
  error: null,
@@ -379,6 +388,7 @@ function validateText(text, contract, options) {
379
388
  ok: v.ok,
380
389
  declaredCount: v.declaredCount,
381
390
  missing: v.missing,
391
+ optionalMissing: v.optionalMissing,
382
392
  a11y: null,
383
393
  };
384
394
  if (wantA11y) theme.a11y = a11y.auditThemeTokens({ name: b.name, values: b.values });
@@ -406,6 +416,7 @@ function validateText(text, contract, options) {
406
416
  const v = validateTokenSet(root.tokens, contract);
407
417
  result.declaredCount = v.declaredCount;
408
418
  result.missing = v.missing;
419
+ result.optionalMissing = v.optionalMissing;
409
420
  result.ok = v.ok;
410
421
  if (wantA11y) {
411
422
  const inferredName = label !== '(pasted CSS)' ? (path.basename(path.dirname(label)) || path.basename(label, '.css')) : 'theme';
@@ -443,6 +454,15 @@ function relForDisplay(p) {
443
454
  return rel || p;
444
455
  }
445
456
 
457
+ // Optional tokens a theme leaves to the library default. Info only — shown as
458
+ // a count, or listed with --show-optional. Never affects the exit code.
459
+ function optionalInfo(optionalMissing, indent) {
460
+ const list = Array.isArray(optionalMissing) ? optionalMissing : [];
461
+ if (!list.length) return;
462
+ console.log(`${indent}${dim(`i ${list.length} optional token(s) not declared — library default applies`)}`);
463
+ if (SHOW_OPTIONAL) for (const token of list) console.log(`${indent} ${dim(token)}`);
464
+ }
465
+
446
466
  function reportResult(result) {
447
467
  const rel = relForDisplay(result.file);
448
468
 
@@ -463,6 +483,7 @@ function reportResult(result) {
463
483
  console.log(
464
484
  ` ${green('✓')} [data-theme="${t.name}"] ${dim(`(${t.declaredCount} tokens)`)}`
465
485
  );
486
+ optionalInfo(t.optionalMissing, ' ');
466
487
  } else {
467
488
  const n = t.missing.length;
468
489
  console.log(
@@ -481,6 +502,7 @@ function reportResult(result) {
481
502
  console.log(
482
503
  `${green('✓')} ${bold(rel)} ${dim(`passes (${result.declaredCount} tokens declared)`)}`
483
504
  );
505
+ optionalInfo(result.optionalMissing, ' ');
484
506
  return;
485
507
  }
486
508
 
@@ -561,6 +583,7 @@ function printUsage() {
561
583
  ' --all validate every theme.css under public/ (CI mode)',
562
584
  ' --no-a11y skip the WCAG 2.2 AA contrast audit',
563
585
  ' --allow-a11y-fail do NOT exit non-zero on a11y FAILs (report only)',
586
+ ' --show-optional list optional contract tokens a theme leaves to the library default (info only)',
564
587
  ' --strict accepted for backwards compatibility (no-op; FAIL is now the default)',
565
588
  '',
566
589
  'Exit codes:',
@@ -573,8 +596,11 @@ function printUsage() {
573
596
  console.log(u);
574
597
  }
575
598
 
599
+ let SHOW_OPTIONAL = false;
600
+
576
601
  function main(argv) {
577
602
  const argsRaw = argv.slice(2).filter(function (a) { return a !== '--watch'; });
603
+ SHOW_OPTIONAL = argsRaw.includes('--show-optional');
578
604
  if (argsRaw.length === 0 || argsRaw.includes('-h') || argsRaw.includes('--help')) {
579
605
  printUsage();
580
606
  process.exit(argsRaw.length === 0 ? 2 : 0);
@@ -583,7 +609,7 @@ function main(argv) {
583
609
  const wantLenient = argsRaw.includes('--allow-a11y-fail');
584
610
  // --strict is accepted as a no-op for backwards compatibility — a11y FAIL is now the default
585
611
  const args = argsRaw.filter(function (a) {
586
- return a !== '--no-a11y' && a !== '--strict' && a !== '--allow-a11y-fail';
612
+ return a !== '--no-a11y' && a !== '--strict' && a !== '--allow-a11y-fail' && a !== '--show-optional';
587
613
  });
588
614
 
589
615
  const contract = loadContract();
@@ -687,6 +713,10 @@ module.exports = {
687
713
  validateFile,
688
714
  validateText,
689
715
  loadContract,
716
+ // Reporters — reused by `cia theme from-tokens` so its output reads exactly
717
+ // like `npm run validate-themes`.
718
+ reportResult,
719
+ reportA11yForTheme,
690
720
  a11y: a11y,
691
721
  };
692
722