css-is-awesome 1.18.0 → 1.19.1

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.
package/AGENTS.md CHANGED
@@ -4,7 +4,7 @@ This file is the entry point for AI coding agents (Aider, Codex, Cursor, Claude
4
4
 
5
5
  ## What this library is
6
6
 
7
- A token-driven SCSS design system with a **single mixin-router per component**. **Mixin-first since v0.8** — the mixin is the API; the class/tag/selector is the consumer's choice. The npm package ships **zero JavaScript** by hard rule.
7
+ A token-driven SCSS design system with a **single mixin-router per component**. **Mixin-first since v0.8** — the mixin is the API; the class/tag/selector is the consumer's choice. The npm package ships **zero runtime JavaScript** by hard rule — nothing in it is loaded by a page; the Node tooling (`cia` CLI, MCP server, validators) never reaches the browser.
8
8
 
9
9
  **Every mixin is a knob-board.** Each look/feel dimension is an *input*, so a consumer can restyle any mixin at any time by changing an argument — row→column is just `@include cia.flex($direction: column)`, never a hand-written `flex-direction`. Customization lives in the mixin's arguments; the consumer stays one line. **If a visual dimension can only be reached by overriding in CSS, that's a missing input — add it to the mixin.** Fewer SCSS lines always wins.
10
10
 
@@ -25,7 +25,7 @@ When asked to add a UI element, follow this order:
25
25
  3. **Never invent `cia-*` class names.** That prefix is library-owned. Consumer code uses its own naming.
26
26
  4. **All values come from tokens.** Never hardcode `#3A5FCD`, `1rem`, `8px`. Use `cia.color(primary)`, `cia.space(4)`, `cia.radius(md)`.
27
27
  5. **No BEM.** No `__element` / `--modifier` chains. `cia-` is a single-class namespace prefix, not BEM.
28
- 6. **No JavaScript.** Cia ships zero JS in the npm package. The 6 interactive components (accordion, modal, tooltip, dropdown, tabs, copy-button) use native HTML primitives — `<details name>`, `<dialog>`, `[popover]`, radio + `:has()`. Mobile navigation follows the same doctrine: the `hamburger` / `drawer` / `sheet` / `dock` mixin family rides `[popover]` + CSS Grid — see the `mobile-nav` recipe (hamburger + drawer) and the `bottom-nav` recipe (dock + sheets). **This rule binds cia, not you.** If you're *consuming* cia (building an app/component library on top of it), write JavaScript/framework components freely — React, SVG charts, interactivity, all of it — and use cia purely for styling (mixins + tokens). Compose the mixins to build any visual you want; you are not limited to cia's pre-made component mixins.
28
+ 6. **No runtime JavaScript.** Nothing in the npm package is loaded by a page — the Node tooling (`cia` CLI, MCP server, validators) never reaches the browser. The 6 interactive components (accordion, modal, tooltip, dropdown, tabs, copy-button) use native HTML primitives — `<details name>`, `<dialog>`, `[popover]`, radio + `:has()`. Mobile navigation follows the same doctrine: the `hamburger` / `drawer` / `sheet` / `dock` mixin family rides `[popover]` + CSS Grid — see the `mobile-nav` recipe (hamburger + drawer) and the `bottom-nav` recipe (dock + sheets). **This rule binds cia, not you.** If you're *consuming* cia (building an app/component library on top of it), write JavaScript/framework components freely — React, SVG charts, interactivity, all of it — and use cia purely for styling (mixins + tokens). Compose the mixins to build any visual you want; you are not limited to cia's pre-made component mixins.
29
29
  7. **Grid is the skeleton; Flex is the quick moves.** Three levels, strictly:
30
30
  - **The page shell is CSS Grid with landmark-named areas.** The body's areas ARE the document's landmarks — `nav`, `main`, `footer` — so the area map reads like the page and screen readers get the structure for free. Declared once via `cia.page-layout(default | sidebar-left | sidebar-right | holy-grail)` (100dvh, sticky footer, auto mobile collapse) or `cia.layout((sidebar content toc), $tracks: …)`; children claim slots with `cia.page-header` / `cia.page-main` / `cia.page-footer` / `cia.area(name)`. Baseline since 2020.
31
31
  - **The doctrine scales inward: any control-dense region gets its own named-area grid.** A docs article (`header / demo / usage / tabs / footer`), a selections rail (`filter / list`), a dashboard — when a region has many controls, name its rows with `cia.layout(...)` too. Nested grids all the way down where density warrants; the grid's `gap` is the region's entire vertical rhythm (children carry no rhythm margins).
@@ -338,7 +338,7 @@ Or run this in-repo copy directly — needs its SDK peer deps installed manually
338
338
  }
339
339
  ```
340
340
 
341
- Either way it exposes **32 tools** across 8 families:
341
+ Either way it exposes **33 tools** across 8 families:
342
342
 
343
343
  - **Themes** — `list_themes`, `get_theme`, `search_themes`
344
344
  - **Mixins** — `list_mixins`, `get_mixin`, `search_mixins` (real signatures — don't guess)
@@ -348,11 +348,11 @@ Either way it exposes **32 tools** across 8 families:
348
348
  - **Components** — `list_components`, `get_component`, `search_components`
349
349
  - **Recipes** — `list_recipes`, `get_recipe`
350
350
  - **Doc readers** — `read_llm_txt`, `read_changelog`, `read_migration`, `read_theming`, `read_agents`, `read_contract`, `read_three_tiers`, `read_readme`, `read_versioning`
351
- - **Helpers** — `assemble_prompt` (bundle context), `resolve_size` (snap a design px value to cia's 4px grid — call this whenever a design tool hands you a raw px value), `validate_theme` (run the real theme validator — contract + contrast — on a CSS string before you ship it), `theme_from_tokens` (DTCG / Tokens Studio / flat token JSON → a complete theme.css, base-inherited and validated; same function as `cia theme from-tokens`)
351
+ - **Helpers** — `assemble_prompt` (bundle context), `resolve_size` (snap a design px value to cia's 4px grid — call this whenever a design tool hands you a raw px value), `validate_theme` (run the real theme validator — contract + contrast — on a CSS string before you ship it), `theme_from_tokens` (DTCG / Tokens Studio / flat token JSON → a complete theme.css, base-inherited and validated; same function as `cia theme from-tokens`), `get_token_map` (that path → token mapping as data, or how one path resolves — read it instead of re-deriving the rules)
352
352
 
353
353
  ## Other tooling (shipped)
354
354
 
355
- - **`cia` CLI** — ships as `bin/cia.cjs`, exposed as the `cia` bin. Four verbs:
355
+ - **`cia` CLI** — ships as `bin/cia.cjs`, exposed as the `cia` bin. Four verbs (`theme` has two subcommands: `from-tokens` and `map`, the latter printing the path → token mapping as a table, `--json`, or `--path <p>` for one name):
356
356
  `npx cia migrate tailwind|bootstrap [path]` parses another system's config and
357
357
  dumps a cia theme; `npx cia add <recipe>` (`--list` to browse) copies a recipe
358
358
  from the book into the project — own the pattern; `npx cia analyze [path]`
@@ -365,7 +365,7 @@ Either way it exposes **32 tools** across 8 families:
365
365
  into a complete theme.css — every required token the file lacks inherits from a shipped base theme (`--base`,
366
366
  default boilerplate), unmapped paths pass through verbatim and are reported, a `--dark` file or paired
367
367
  `color-light`/`color-dark` groups become `light-dark()`, and the validator + WCAG audit run before anything is
368
- written. Same function as the MCP `theme_from_tokens` tool. Run any verb with `--help`. (`cia init` remains
368
+ written. Same function as the MCP `theme_from_tokens` tool; `npx cia theme map [--json] [--path <token.path>]` prints the design-token → cia-token mapping that verb applies (same data as the MCP `get_token_map` tool), for anyone who needs to map one token name the way the converter would. Run any verb with `--help`. (`cia init` remains
369
369
  planned.)
370
370
  - **JSON token export** — Tokens Studio-format sample in `figma-tokens/tokens.json`; it round-trips through `cia theme from-tokens` (paired light/dark groups → one `light-dark()` theme).
371
371
  - **`llm.txt`** — at the repo root and served from the docs site; single-fetch
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.19.1](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.19.0...v1.19.1) (2026-09-23)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **cli:** paired light/dark modes carried only colours; export the in-process entry point ([f1fe71f](https://github.com/Jerry2d3d/css-is-awesome/commit/f1fe71f8a91a8e7bf59e0b9e830585701beb9160))
9
+
10
+
11
+ ### Features
12
+
13
+ * **site:** cross-link the posts that explain a browser-support row (B6.4) ([44f40d8](https://github.com/Jerry2d3d/css-is-awesome/commit/44f40d8bf278113a81f7f2b643ad52390fb1af25))
14
+
15
+ # [1.19.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.18.0...v1.19.0) (2026-09-19)
16
+
17
+
18
+ ### Features
19
+
20
+ * **cli:** cia theme map — the design-token → cia-token mapping as data ([283db9b](https://github.com/Jerry2d3d/css-is-awesome/commit/283db9b47b2195b8d8cb3afe9bad254c88e91cab))
21
+ * **cli:** map boilerplate's canonical DTCG layout — font.family roles, component.* overrides ([8ae71fd](https://github.com/Jerry2d3d/css-is-awesome/commit/8ae71fd55b07bbf7da6d562f5388dcc489bcffb9))
22
+ * **mcp:** get_token_map tool + in-process handler (33 tools) ([ff5610f](https://github.com/Jerry2d3d/css-is-awesome/commit/ff5610f5409bead49fa794a8c27967f602376c77))
23
+
3
24
  # [1.18.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.17.0...v1.18.0) (2026-09-19)
4
25
 
5
26
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/css-is-awesome?logo=npm&color=cb3837)](https://www.npmjs.com/package/css-is-awesome) [![CI](https://github.com/Jerry2d3d/css-is-awesome/actions/workflows/ci.yml/badge.svg)](https://github.com/Jerry2d3d/css-is-awesome/actions/workflows/ci.yml) [![Node](https://img.shields.io/badge/node-%E2%89%A520-43853d?logo=node.js&logoColor=white)](./package.json) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![semantic-release](https://img.shields.io/badge/semantic--release-enabled-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
6
6
 
7
- **Bring your own components. Bring your own selectors. We bring the design system.** No component library to fight, in React, Vue, Angular, Svelte, Web Components, Razor, SharePoint, or plain HTML — cia styles the markup you already own. One CSS file per theme — drop it in and the page restyles, no markup change. 24 themes. Zero JavaScript in the npm package. Six browser-native interactive components. Small enough to read in an afternoon.
7
+ **Bring your own components. Bring your own selectors. We bring the design system.** No component library to fight, in React, Vue, Angular, Svelte, Web Components, Razor, SharePoint, or plain HTML — cia styles the markup you already own. One CSS file per theme — drop it in and the page restyles, no markup change. 24 themes. Zero runtime JavaScript — nothing in the package is loaded by a page. Six browser-native interactive components. Small enough to read in an afternoon.
8
8
 
9
9
  **Docs:** [cssisawesome.com](https://cssisawesome.com/) · **Install:** `npm install css-is-awesome`
10
10
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  **Read [`llm.txt`](./llm.txt) first.** One file, the whole system: install path, hard rules, the mixin vocabulary, and the traps that make agents write wrong cia code. It ships in the npm package, so it's at `node_modules/css-is-awesome/llm.txt` in any project that has cia.
16
16
 
17
- **Then connect the MCP server** and stop guessing at signatures. It answers from the real source — 32 tools covering themes, mixins, functions, tokens, recipes, components, theme validation and theme generation from design tokens.
17
+ **Then connect the MCP server** and stop guessing at signatures. It answers from the real source — 33 tools covering themes, mixins, functions, tokens, recipes, components, theme validation, theme generation from design tokens and the token mapping itself.
18
18
 
19
19
  ```json
20
20
  {
@@ -230,6 +230,9 @@ npx cia theme from-tokens tokens.json --name acme --out src/styles/acme.css
230
230
  # design tokens (DTCG v2025.10, Tokens Studio, or a
231
231
  # flat --token map) → a complete, validated theme.css;
232
232
  # missing required tokens inherit from a shipped base
233
+ npx cia theme map --path color.text.primary
234
+ # the path → token mapping from-tokens applies, as
235
+ # data: one path, or the whole table with --json
233
236
  ```
234
237
 
235
238
  `cia analyze` reads the real API surface from the installed package and exits non-zero on errors, so it slots straight into CI. It's deliberately low-noise about color: a hex used as a `var(--token, #hex)` fallback is token-driven (not flagged), and a literal inside `@media print` is an intentional paper colour (print escapes theme colours by design). Off-contract-token findings only fire on a **near-miss** of a real token — a typo like `--inkk` — never on your own custom tokens. Off-scale-length findings suggest the nearest named step (e.g. `--radius-md`) for a literal `border-radius`/`padding`/`margin`/`gap` value, as a hint toward using a token — never a claim about your active theme's exact pixel value, since themes are free to set their own numbers (Terminal sets every `--radius-*` to `0`, deliberately). The default output is a **graded report** — a health score, a section per concern (Contract / Spacing / Color / Naming / Layout / API / Accessibility) with a `✓` when clean, and a suggested fix on each finding; add `--verbose` for the flat per-file list or `--json` for the machine shape. Full rule reference: [`/docs/analyzer`](https://cssisawesome.com/docs/analyzer/).
@@ -298,7 +301,7 @@ The scope is kept narrow: 8 `!important` declarations, all inside `@media print`
298
301
 
299
302
  ## MCP server (for AI agents)
300
303
 
301
- cia ships a Model Context Protocol stdio server (JSON-RPC over stdio, protocol `2024-11-05`) exposing **32 tools** across 8 families (themes, mixins, functions, tokens · 127 required of them, animations, components, recipes, doc readers) plus `assemble_prompt` (context bundles) , `resolve_size` (snap design px values to cia's 4px grid) `validate_theme` (run the real theme validator on CSS you just wrote) and `theme_from_tokens` (design-tokens JSON → a complete, validated theme.css). Any MCP-aware client (Claude Code, Cursor, Aider, Gemini, Copilot) can then query cia's real design system — mixin signatures, tokens, themes, recipes — instead of guessing, without grep-walking the repo. Full reference: [`/docs/mcp`](https://cssisawesome.com/docs/mcp/).
304
+ cia ships a Model Context Protocol stdio server (JSON-RPC over stdio, protocol `2024-11-05`) exposing **33 tools** across 8 families (themes, mixins, functions, tokens · 127 required of them, animations, components, recipes, doc readers) plus `assemble_prompt` (context bundles) , `resolve_size` (snap design px values to cia's 4px grid) `validate_theme` (run the real theme validator on CSS you just wrote) `theme_from_tokens` (design-tokens JSON → a complete, validated theme.css) and `get_token_map` (that mapping as data, or how one path resolves). Any MCP-aware client (Claude Code, Cursor, Aider, Gemini, Copilot) can then query cia's real design system — mixin signatures, tokens, themes, recipes — instead of guessing, without grep-walking the repo. Full reference: [`/docs/mcp`](https://cssisawesome.com/docs/mcp/).
302
305
 
303
306
  **Recommended — zero install:** use the dedicated [`css-is-awesome-mcp`](https://www.npmjs.com/package/css-is-awesome-mcp) package. It depends on `css-is-awesome` and resolves your installed version's real source, so it's never out of sync — and the MCP SDK ships as a real dependency, not an optional peer you have to remember to add.
304
307
 
@@ -408,13 +411,13 @@ Full detail: [`/docs/testing`](https://cssisawesome.com/docs/testing/).
408
411
  | `dist/css-is-awesome.utilities.min.css` | 4.75 KB | Every `cia-*` utility class, nothing else |
409
412
  | `dist/css-is-awesome.min.css` | 7.96 KB | Full bundle (everything) |
410
413
  | Per-theme `themes/<name>/theme.css` | 1.9–3.7 KB | One file per theme, both modes via `light-dark()`, drop-in with no markup change |
411
- | **JavaScript shipped in package** | **0 KB** | Zero. Period. JS-driven features ship as separate add-on packages. |
414
+ | **Runtime JavaScript shipped in package** | **0 KB** | Nothing in the package is loaded by a page. The Node tooling (`cia` CLI, MCP server, validators) never reaches the browser; JS-driven UI features ship as separate add-on packages. |
412
415
 
413
416
  ## Status
414
417
 
415
418
  **Stable, [published on npm](https://www.npmjs.com/package/css-is-awesome)** (first published 2026-09-01). The mixin API, functions, token contract, and theme architecture are stable and under strict SemVer — breaking changes require a major bump. See [`VERSIONING.md`](./VERSIONING.md) for the policy.
416
419
 
417
- The 1.0 surface is the v0.8 mixin-first reframe — twelve mixin renames, theme system collapsed to 8 single-file theme families, six zero-JS components, intrinsic-layout vocabulary, opt-in utilities — plus the recipes book, the Tailwind/Bootstrap migration on-ramp, print/PDF support, and the 32-tool MCP server (now also available zero-install via the companion [`css-is-awesome-mcp`](https://www.npmjs.com/package/css-is-awesome-mcp) package). The npm package ships ZERO JavaScript by hard rule.
420
+ The 1.0 surface is the v0.8 mixin-first reframe — twelve mixin renames, theme system collapsed to 8 single-file theme families, six zero-JS components, intrinsic-layout vocabulary, opt-in utilities — plus the recipes book, the Tailwind/Bootstrap migration on-ramp, print/PDF support, the in-browser [Playground](https://cssisawesome.com/playground/), the design-tokens on-ramp (`npx cia theme from-tokens` / `theme map`), and the 33-tool MCP server (now also available zero-install via the companion [`css-is-awesome-mcp`](https://www.npmjs.com/package/css-is-awesome-mcp) package). The npm package ships ZERO runtime JavaScript by hard rule — nothing in it is loaded by a page; its Node tooling (CLI, MCP server, validators) never reaches the browser.
418
421
 
419
422
  See [CHANGELOG.md](./CHANGELOG.md) for the full history and [MIGRATION.md](./MIGRATION.md) for the v0.7 → v0.8 and v0.8 → v1.0 upgrade paths.
420
423
 
package/bin/cia.cjs CHANGED
@@ -12,6 +12,7 @@
12
12
  * migrate mui — parse a MUI createTheme() result + dump theme JSON
13
13
  * migrate chakra — parse a Chakra extendTheme() result + dump theme JSON
14
14
  * theme from-tokens — design-tokens JSON (DTCG / Tokens Studio) → validated theme.css
15
+ * theme map — the design-token → cia-token mapping, as data (table / --json / --path)
15
16
  *
16
17
  * cia core ships ZERO JavaScript in the `files` manifest. The CLI lives in
17
18
  * `bin/` which is explicitly allowed per the architecture lock — same path
@@ -40,12 +41,15 @@ Commands:
40
41
  theme from-tokens <f> Design-tokens JSON (DTCG v2025.10, Tokens Studio,
41
42
  or flat --token map) → a complete, validated
42
43
  theme.css. \`cia theme from-tokens --help\`.
44
+ theme map The path → token mapping from-tokens applies, as
45
+ data: a table, \`--json\`, or \`--path <p>\` for one.
43
46
 
44
47
  Examples:
45
48
  cia migrate tailwind ./tailwind.config.js
46
49
  cia add bottom-nav
47
50
  cia analyze src/styles
48
51
  cia theme from-tokens tokens.json --name acme --out src/styles/acme.css
52
+ cia theme map --path color.text.primary
49
53
 
50
54
  Run \`cia <command> --help\` for command-specific help.
51
55
  `;
@@ -165,7 +169,12 @@ async function main() {
165
169
  await run(themeArgs);
166
170
  return;
167
171
  }
168
- fail(`unknown theme subcommand '${sub}'. Available: from-tokens.`);
172
+ if (sub === 'map') {
173
+ const { runMap } = require('./theme-from-tokens.cjs');
174
+ await runMap(themeArgs);
175
+ return;
176
+ }
177
+ fail(`unknown theme subcommand '${sub}'. Available: from-tokens, map.`);
169
178
  }
170
179
 
171
180
  fail(`unknown command '${command}'. Run \`cia --help\` for usage.`);
@@ -136,4 +136,72 @@ async function run(argv) {
136
136
  }
137
137
  }
138
138
 
139
- module.exports = { run, parseArgs, HELP };
139
+ const MAP_HELP = `cia theme map — the design-token → cia-token mapping, as data
140
+
141
+ Usage:
142
+ cia theme map Human-readable table: explicit entries by family,
143
+ the prefix rewrites, then the generic rule
144
+ cia theme map --json The same mapping as JSON (stable shape — read it
145
+ from a build script or another tool)
146
+ cia theme map --path <p> How ONE path resolves, e.g. --path color.text.primary
147
+
148
+ The JSON shape: { generatorVersion, contractVersion, explicit: { "<path>": "--token" },
149
+ aliases: [{ pattern, replaceWith }], genericRule, targets: { required, optional } }.
150
+ Same data the MCP tool get_token_map returns and \`cia theme from-tokens\` applies.
151
+ `;
152
+
153
+ function parseMapArgs(argv) {
154
+ const opts = { json: false };
155
+ for (let i = 0; i < argv.length; i++) {
156
+ const a = argv[i];
157
+ if (a === '-h' || a === '--help') opts.help = true;
158
+ else if (a === '--json') opts.json = true;
159
+ else if (a === '--path') { opts.path = argv[++i]; if (opts.path === undefined) throw new Error('--path needs a value'); }
160
+ else throw new Error(`unknown option ${a}`);
161
+ }
162
+ return opts;
163
+ }
164
+
165
+ function familyOf(tokenPath) { return tokenPath.split('.')[0]; }
166
+
167
+ async function runMap(argv) {
168
+ let opts;
169
+ try { opts = parseMapArgs(argv); } catch (e) { process.stderr.write(`cia theme map: ${e.message}\n`); process.exit(2); }
170
+ if (opts.help) { process.stdout.write(MAP_HELP); return; }
171
+ const { tokenMap, resolvePath } = require('../scripts/tokens-to-theme.cjs');
172
+
173
+ if (opts.path) {
174
+ let r;
175
+ try { r = resolvePath(opts.path); } catch (e) { process.stderr.write(`cia theme map: ${e.message}\n`); process.exit(2); }
176
+ if (opts.json) { process.stdout.write(JSON.stringify(r, null, 2) + '\n'); return; }
177
+ const how = {
178
+ 'explicit': 'explicit table entry',
179
+ 'explicit-after-alias': 'explicit table entry, after a prefix rewrite',
180
+ 'generic': 'generic rule (join with "-", found in the contract)',
181
+ 'passthrough': 'NOT a contract token — emitted verbatim, reported as unmapped',
182
+ }[r.via];
183
+ process.stdout.write(`${r.path} → ${r.token}\n ${r.mapped ? 'mapped' : 'unmapped'} · ${how}${r.status ? ` · contract: ${r.status}` : ''}\n`);
184
+ return;
185
+ }
186
+
187
+ const m = tokenMap();
188
+ if (opts.json) { process.stdout.write(JSON.stringify(m, null, 2) + '\n'); return; }
189
+ const out = [`cia token map — generator ${m.generatorVersion}, contract ${m.contractVersion}`, ''];
190
+ const byFamily = {};
191
+ for (const [p, t] of Object.entries(m.explicit)) (byFamily[familyOf(p)] = byFamily[familyOf(p)] || []).push([p, t]);
192
+ out.push(`Explicit entries (${Object.keys(m.explicit).length})`);
193
+ for (const fam of Object.keys(byFamily).sort()) {
194
+ out.push(` ${fam}`);
195
+ const w = Math.max(...byFamily[fam].map(([p]) => p.length));
196
+ for (const [p, t] of byFamily[fam]) out.push(` ${p.padEnd(w)} → ${t}`);
197
+ }
198
+ out.push('', `Prefix rewrites (${m.aliases.length}, first match wins, applied before the generic rule)`);
199
+ const aw = Math.max(...m.aliases.map((a) => a.pattern.length));
200
+ for (const a of m.aliases) out.push(` ${a.pattern.padEnd(aw)} → ${JSON.stringify(a.replaceWith)}`);
201
+ out.push('', 'Generic rule', ` ${m.genericRule}`, '',
202
+ `Targets: ${m.targets.required.length} required + ${m.targets.optional.length} optional contract tokens`,
203
+ '', 'Try one: cia theme map --path color.text.primary');
204
+ process.stdout.write(out.join('\n') + '\n');
205
+ }
206
+
207
+ module.exports = { run, runMap, parseArgs, parseMapArgs, HELP, MAP_HELP };
package/dist/tokens.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Generated from scripts/theme-contract.json on 2026-09-19.
1
+ // Generated from scripts/theme-contract.json on 2026-09-23.
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. */
package/llm.txt CHANGED
@@ -8,7 +8,8 @@
8
8
 
9
9
  A token-driven SCSS design system. **Mixin-first** (since v0.8) — the mixin
10
10
  is the API, the class/tag/selector is the consumer's choice. The npm package
11
- ships **zero JavaScript** by hard rule.
11
+ ships **zero runtime JavaScript** by hard rule — nothing in it is loaded by a
12
+ page; its Node tooling (`cia` CLI, MCP server, validators) never reaches the browser.
12
13
 
13
14
  ## How consumers install it (the primary path)
14
15
 
@@ -48,7 +49,7 @@ silent.
48
49
  - **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
50
  - **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
51
  - **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|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
+ - **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), `theme map [--json | --path <p>]` (the path → token mapping from-tokens applies, as data; same as MCP `get_token_map`), `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
53
  - **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
54
  - **168 contract tokens — 127 required + 41 optional** — surfaces, ink, lines, colors, type, radius, shadow, blur, glow, motion, z-index, spacing, semantic aliases.
54
55
  - **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.
@@ -57,7 +58,7 @@ silent.
57
58
 
58
59
  ## Hard rules (do not violate)
59
60
 
60
- 1. **No JavaScript in the cia npm package.** Zero `.js`/`.mjs` files ship. JS-augmented features (CopyButton handler, future force-mode) ship as separate add-on packages. **This binds the package, not the consumer:** apps built ON cia should write JS/framework components freely (React, SVG charts, interactivity) and use cia only for styling — mixins + tokens. Don't over-apply "zero-JS" to your own app code.
61
+ 1. **No runtime JavaScript in the cia npm package.** Nothing in it is loaded by a page — the Node tooling (`cia` CLI, MCP server, validators) never reaches the browser. JS-augmented UI features (CopyButton handler, future force-mode) ship as separate add-on packages. **This binds the package, not the consumer:** apps built ON cia should write JS/framework components freely (React, SVG charts, interactivity) and use cia only for styling — mixins + tokens. Don't over-apply "zero-JS" to your own app code.
61
62
  2. **No `@layer`.** Tier 3 bare-tags use `:where()` (specificity 0,0,0). Cascade pollution kills the consumer experience. Corollary: **the `!important` in the print mixins is load-bearing — do not remove it.** `@media` contributes no specificity, so `print-hidden` only carries the specificity of the selector it is included in, and a later equal-specificity `display` wins in print. `@layer` would make this worse, not better: layered CSS always loses to unlayered CSS, so a layered print rule loses to any unlayered consumer stylesheet. Scope is 8 declarations, all inside `@media print`, all variable-driven (`--print-hide` / `--print-show`).
62
63
  3. **No BEM.** No `__element` / `--modifier` chains in cia source. Consumers pick their own selector names — cia has no opinion.
63
64
  4. **No Storybook.** Docs site + the `css-is-awesome-mcp` server cover both human and AI audiences.
@@ -128,7 +129,7 @@ Newspaper by day, hacker terminal by night. Most design systems give you dark mo
128
129
 
129
130
  ## MCP server (shipped 2026-05-22)
130
131
 
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
+ 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`. **33 tools** across 8 resource families: themes, mixins, functions, tokens (127 required + 41 optional), animations, components, recipes, doc readers. Plus `assemble_prompt` for context bundling, `validate_theme`, `theme_from_tokens`, `get_token_map` (the design-token → cia-token mapping as data) 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
133
 
133
134
  **Recommended: suggest the dedicated `css-is-awesome-mcp` package** — zero install,
134
135
  SDK ships as a real dependency, no separate peer-install step:
@@ -197,4 +198,4 @@ Humans first. AI's role is to compose, not to lead the pitch.
197
198
 
198
199
  ---
199
200
 
200
- *Generated 2026-05-21 for cia v0.8. Updated 2026-05-23 for the v1.0 architecture lock, 2026-08-30 for the theme single-source rework (24 themes, 127+36 contract, themeable spacing, `:root, :root[data-theme]` selector), and 2026-09-04 for the shipped recipes count (5), the mobile-nav mixin family, and the dropdown popover guard. Update when the API surface changes.*
201
+ *Generated 2026-05-21 for cia v0.8. Updated 2026-05-23 for the v1.0 architecture lock, 2026-08-30 for the theme single-source rework (24 themes, 127+36 contract, themeable spacing, `:root, :root[data-theme]` selector), 2026-09-04 for the shipped recipes count (5), the mobile-nav mixin family, and the dropdown popover guard, and 2026-09-19 for the 32-recipe book, 33-tool MCP, contract 1.2 (127 + 41 in 10 features), the playground and the design-tokens on-ramp. Update when the API surface changes.*
package/mcp/server.cjs CHANGED
@@ -20,9 +20,10 @@
20
20
  * read_three_tiers, read_readme, read_versioning
21
21
  * Sizing: resolve_size
22
22
  * Themes (build): theme_from_tokens — design-tokens JSON → validated theme.css
23
+ * get_token_map — the path → token mapping as data (or one path)
23
24
  * Prompt: assemble_prompt(intent[, args])
24
25
  *
25
- * 32 tools total.
26
+ * 33 tools total.
26
27
  *
27
28
  * Discovery model: filesystem scan, no database. Parses SCSS files with
28
29
  * focused regex (no full SCSS AST). Tokens come from the authoritative
@@ -33,13 +34,13 @@
33
34
  * "mcpServers": {
34
35
  * "css-is-awesome": {
35
36
  * "command": "node",
36
- * "args": ["K:/Repo/css-is-awesome/mcp/server.cjs"]
37
+ * "args": ["node_modules/css-is-awesome/mcp/server.cjs"]
37
38
  * }
38
39
  * }
39
40
  * }
40
41
  *
41
- * Aligned with the canonical sibling MCP shape: ui-ux-builder, ideas-master,
42
- * video-maker. Response envelope is `{ total, items }` for list/search;
42
+ * Aligned with the canonical sibling MCP shape used across our other servers.
43
+ * Response envelope is `{ total, items }` for list/search;
43
44
  * get_* tools return the full record.
44
45
  */
45
46
 
@@ -717,6 +718,25 @@ const handlers = {
717
718
  return themeFromTokens({ tokens, name, format, base, dark, mode, validate });
718
719
  },
719
720
 
721
+ // The design-token → cia-token mapping theme_from_tokens applies, as DATA:
722
+ // the explicit table, the prefix rewrites, the generic rule, and the target
723
+ // token lists — or, with `path`, how one path resolves plus the contract's
724
+ // view of the target (required/optional + contract-1.2 feature). Exists so
725
+ // two consumers (an inventory builder, a boilerplate registry) map the same
726
+ // source token the same way without re-deriving the rules.
727
+ get_token_map({ path: tokenPath } = {}) {
728
+ const { tokenMap, resolvePath } = require(path.join(SCRIPTS_DIR, 'tokens-to-theme.cjs'));
729
+ if (tokenPath == null || tokenPath === '') return tokenMap({ ciaRoot: PROJECT_ROOT });
730
+ const r = resolvePath(String(tokenPath), { ciaRoot: PROJECT_ROOT });
731
+ const entry = getTokens().byName[r.token] || null;
732
+ return {
733
+ ...r,
734
+ required: entry ? entry.required : null,
735
+ feature: entry && !entry.required ? (entry.feature || null) : null,
736
+ category: entry ? entry.category : null,
737
+ };
738
+ },
739
+
720
740
  // ─── Mixins ────────────────────────────────────────────────────────────
721
741
 
722
742
  list_mixins({ category, component, limit = 500, offset = 0 } = {}) {
@@ -1382,7 +1402,8 @@ async function startServer() {
1382
1402
  '{ "--token": value } map. Format is auto-detected. Every REQUIRED contract token the file does not supply ' +
1383
1403
  'is inherited from a shipped base theme (default boilerplate) and listed in report.inherited, so the output ' +
1384
1404
  'is always contract-complete; unmapped paths are emitted verbatim and listed in report.unmapped, never ' +
1385
- 'dropped. Pass `dark` (same format) or a single file with paired color-light/color-dark groups to get ' +
1405
+ 'dropped. Pass `dark` (same format), or one file with paired top-level groups — `light`/`dark` (each a full '
1406
+ + 'token set) or `color-light`/`color-dark` (each a colour set) — to get ' +
1386
1407
  'light-dark() values. Returns { css, report, validation } — validation is the same result validate_theme ' +
1387
1408
  'gives, run on the CSS before you write it anywhere.',
1388
1409
  inputSchema: {
@@ -1396,6 +1417,19 @@ async function startServer() {
1396
1417
  },
1397
1418
  }, async (a) => ok(handlers.theme_from_tokens(a || {})));
1398
1419
 
1420
+ server.registerTool('get_token_map', {
1421
+ description:
1422
+ 'The design-token → cia-token mapping that theme_from_tokens applies, as data. Without `path`: ' +
1423
+ '{ generatorVersion, contractVersion, explicit: { "<path>": "--token" }, aliases: [{ pattern, ' +
1424
+ 'replaceWith }], genericRule, targets: { required, optional } }. With `path` (e.g. ' +
1425
+ '"color.text.primary"): how that one path resolves — { token, mapped, via, status, required, ' +
1426
+ 'feature, category }. Use it to map a Figma / DTCG / Tokens Studio token name to the cia custom ' +
1427
+ 'property the same way the converter does, or to check a name before building a theme.',
1428
+ inputSchema: {
1429
+ path: z.string().optional().describe('One token path to resolve (dot-separated, e.g. spacing.4). Omit for the whole map.'),
1430
+ },
1431
+ }, async (a) => ok(handlers.get_token_map(a || {})));
1432
+
1399
1433
  // Mixins
1400
1434
  server.registerTool('list_mixins', {
1401
1435
  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.18.0",
3
+ "version": "1.19.1",
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": {
@@ -41,6 +41,10 @@
41
41
  "./scss/main": "./scss/main.scss",
42
42
  "./scss/tokens": "./scss/tokens.scss",
43
43
  "./scss/mixins": "./scss/_mixins.scss",
44
+ "./scripts/tokens-to-theme.cjs": "./scripts/tokens-to-theme.cjs",
45
+ "./scripts/tokens-to-theme": "./scripts/tokens-to-theme.cjs",
46
+ "./scripts/theme-validator.js": "./scripts/theme-validator.js",
47
+ "./mcp/server.cjs": "./mcp/server.cjs",
44
48
  "./scss/generator": "./scss/_generator.scss",
45
49
  "./scss/utilities": "./scss/_utilities.scss",
46
50
  "./scss/animations": "./scss/_animations.scss",
@@ -48,8 +48,11 @@
48
48
  // LIGHT + DARK
49
49
  // Pass `dark` (a second tokens object, same format) and every colour token
50
50
  // that differs becomes `light-dark(light, dark)` with `color-scheme: light dark`.
51
- // A single Tokens Studio file with paired top-level groups (`color-light` +
52
- // `color-dark`, or `light` + `dark`) is split the same way automatically.
51
+ // A single file with paired top-level groups is split automatically, and the
52
+ // two conventions mean different things: `color-light`/`color-dark` name a
53
+ // COLOUR SET (children are colours), while `light`/`dark` name a MODE whose
54
+ // group holds a FULL token set — colours, spacing, fonts, component
55
+ // overrides — which merges beside any groups shared outside the pair.
53
56
  // Without a dark side the block is single-mode: `color-scheme: <mode>`
54
57
  // (`mode`, default light).
55
58
  // ============================================================================
@@ -201,6 +204,46 @@ const TOKEN_MAP = {
201
204
  'layer.modal': '--z-modal',
202
205
  'layer.popover': '--z-popover',
203
206
  'layer.tooltip': '--z-tooltip',
207
+ // ── Role-named font families (boilerplate / ui-ux-builder layout) ──────
208
+ // "primary" is the body face, "secondary" the display/heading face.
209
+ 'font.primary': '--font-primary',
210
+ 'font.secondary': '--font-display',
211
+ 'font.heading': '--font-display',
212
+ 'font.body': '--font-primary',
213
+ 'typography.font.secondary': '--font-display',
214
+ 'typography.font.heading': '--font-display',
215
+ 'typography.family.primary': '--font-primary',
216
+ 'typography.family.secondary': '--font-display',
217
+ 'typography.family.heading': '--font-display',
218
+
219
+ // ── component.<name>.<knob> → the contract's per-component overrides ──
220
+ // (optional tokens, contract 1.2 features component-radius / -shadows /
221
+ // -motion / borders-extended). `components.` is aliased to `component.`.
222
+ 'component.button.radius': '--btn-radius',
223
+ 'component.button.shadow': '--shadow-button',
224
+ 'component.button.duration': '--duration-button-hover',
225
+ 'component.card.radius': '--card-radius',
226
+ 'component.card.shadow': '--shadow-card',
227
+ 'component.card.border': '--border-card',
228
+ 'component.input.radius': '--input-radius',
229
+ 'component.input.border': '--border-input',
230
+ 'component.input.shadow': '--shadow-input-focus',
231
+ 'component.input.focus-shadow': '--shadow-input-focus',
232
+ 'component.modal.radius': '--modal-radius',
233
+ 'component.modal.shadow': '--shadow-modal',
234
+ 'component.modal.duration': '--duration-modal-open',
235
+ 'component.badge.radius': '--badge-radius',
236
+ 'component.tag.radius': '--tag-radius',
237
+ 'component.chip.radius': '--tag-radius',
238
+ 'component.dropdown.shadow': '--shadow-dropdown',
239
+ 'component.popover.shadow': '--shadow-popover',
240
+ 'component.tooltip.shadow': '--shadow-tooltip',
241
+ 'component.toast.duration': '--duration-toast-slide',
242
+ 'component.divider.border': '--border-divider',
243
+ 'component.divider.color': '--border-divider',
244
+ 'component.focus.ring': '--border-focus-ring',
245
+ 'component.text.shadow': '--shadow-text',
246
+ 'component.touch-target.min': '--touch-target-min',
204
247
  };
205
248
 
206
249
  // Group-name rewrites applied before the generic rule (rule 2). Case-insensitive
@@ -226,6 +269,9 @@ const PATH_ALIASES = [
226
269
  [/^lineHeights?\./i, 'line-height.'],
227
270
  [/^durations?\./i, 'duration.'],
228
271
  [/^motion\.duration\./i, 'duration.'],
272
+ [/^font\.family\./, 'font.'], // font.family.mono → font.mono → --font-mono
273
+ [/^fontFamily\./, 'font.'],
274
+ [/^components\./, 'component.'], // components.button.radius → component.button.radius
229
275
  ];
230
276
 
231
277
  // Token families that are unitless by contract — a bare number stays bare.
@@ -447,6 +493,17 @@ function applyPathAliases(p) {
447
493
 
448
494
  /** Returns { token, mapped: true } or { token, mapped: false } (verbatim fallback). */
449
495
  function mapPath(tokenPath, contract) {
496
+ if (typeof tokenPath !== 'string' || !tokenPath.trim()) {
497
+ throw new Error('mapPath: first argument must be a non-empty token path, e.g. "color.text.primary"');
498
+ }
499
+ // The contract is optional: omit it and the installed one is loaded. It used
500
+ // to be required, but only the generic-rule branch touched it — so a path in
501
+ // the explicit table appeared to work and the mistake surfaced later as a
502
+ // TypeError on some other path. Fail clearly or not at all.
503
+ if (contract == null) contract = loadContract(path.join(__dirname, '..'));
504
+ else if (!contract.all || typeof contract.all.has !== 'function') {
505
+ throw new Error('mapPath: second argument must be a contract from loadContract(); omit it to load the installed contract automatically');
506
+ }
450
507
  if (TOKEN_MAP[tokenPath]) return { token: TOKEN_MAP[tokenPath], mapped: true };
451
508
  const aliased = applyPathAliases(tokenPath);
452
509
  if (TOKEN_MAP[aliased]) return { token: TOKEN_MAP[aliased], mapped: true };
@@ -455,6 +512,26 @@ function mapPath(tokenPath, contract) {
455
512
  // camelCase segments → kebab (linkHover → link-hover), one more try.
456
513
  const kebab = `--${aliased.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/\./g, '-').toLowerCase()}`;
457
514
  if (contract.all.has(kebab)) return { token: kebab, mapped: true };
515
+ // Flat per-component form: `component.btn-radius` → `--btn-radius`. The
516
+ // explicit table carries the nested shape (`component.button.radius`), and
517
+ // it is tried first; this catches exporters that flatten the group instead.
518
+ // Generic-rule first means this can never shadow a real `--component-*`.
519
+ const flat = aliased.replace(/^components?\./, '');
520
+ if (flat !== aliased) {
521
+ const body = flat.replace(/\./g, '-').toLowerCase();
522
+ // `component.btn-radius` → `--btn-radius`.
523
+ if (contract.all.has(`--${body}`)) return { token: `--${body}`, mapped: true };
524
+ // The contract names per-component overrides two ways: radius trails the
525
+ // component (`--card-radius`) but shadow, border and duration lead it
526
+ // (`--shadow-card`). A flat exporter cannot know which, so try the swap:
527
+ // `card-shadow` → `--shadow-card`, `button-hover-duration` →
528
+ // `--duration-button-hover`. Only accepted if it is a real token.
529
+ const cut = body.lastIndexOf('-');
530
+ if (cut > 0) {
531
+ const swapped = `--${body.slice(cut + 1)}-${body.slice(0, cut)}`;
532
+ if (contract.all.has(swapped)) return { token: swapped, mapped: true };
533
+ }
534
+ }
458
535
  return { token: generic, mapped: false };
459
536
  }
460
537
 
@@ -487,16 +564,35 @@ function loadBase(ciaRoot, base) {
487
564
  // ---------------------------------------------------------------------------
488
565
  // Paired-mode detection for a single Tokens Studio / DTCG file
489
566
  // ---------------------------------------------------------------------------
490
- const PAIRS = [['color-light', 'color-dark'], ['light', 'dark'], ['colors-light', 'colors-dark']];
567
+ // Two different conventions look alike and must NOT be treated alike:
568
+ //
569
+ // `color-light` / `color-dark` name a COLOUR SET. The group's children are
570
+ // colours (`color-light.brand.primary`), so they nest under `color.`.
571
+ // `light` / `dark` name a MODE. The group holds a FULL token
572
+ // set — `color.*`, `spacing.*`, `font.*`, `component.*` — so its
573
+ // children merge at the TOP level, beside any shared groups.
574
+ //
575
+ // Wrapping a mode group in `color.` (which this did for every pair until
576
+ // 2026-09-19) silently mis-routed every non-colour group: `spacing.unit`
577
+ // became `color.spacing.unit` → `--spacing-unit`, so the theme lost its
578
+ // density knob while `validation.ok` stayed true, because the required token
579
+ // was quietly inherited from the base theme instead. Reported by a consumer
580
+ // against boilerplate's canonical DTCG layout.
581
+ const PAIRS = [
582
+ { light: 'color-light', dark: 'color-dark', wrap: 'color' },
583
+ { light: 'colors-light', dark: 'colors-dark', wrap: 'color' },
584
+ { light: 'light', dark: 'dark', wrap: null },
585
+ ];
491
586
 
492
587
  function splitPairedModes(tokens) {
493
588
  if (!isPlainObject(tokens)) return null;
494
- for (const [l, d] of PAIRS) {
589
+ for (const { light: l, dark: d, wrap } of PAIRS) {
495
590
  if (isPlainObject(tokens[l]) && isPlainObject(tokens[d])) {
496
591
  const rest = {};
497
592
  for (const [k, v] of Object.entries(tokens)) if (k !== l && k !== d) rest[k] = v;
498
- const light = { ...rest, color: tokens[l] };
499
- const dark = { ...rest, color: tokens[d] };
593
+ // Mode-specific groups win over shared ones on a key collision.
594
+ const light = wrap ? { ...rest, [wrap]: tokens[l] } : { ...rest, ...tokens[l] };
595
+ const dark = wrap ? { ...rest, [wrap]: tokens[d] } : { ...rest, ...tokens[d] };
500
596
  return { light, dark, groups: [l, d] };
501
597
  }
502
598
  }
@@ -545,6 +641,45 @@ function collect(tokens, format, contract) {
545
641
  // ---------------------------------------------------------------------------
546
642
  const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
547
643
 
644
+ // ---------------------------------------------------------------------------
645
+ // The mapping as DATA — so a second implementation (a boilerplate registry,
646
+ // an inventory builder, another agent) can map the same source token the
647
+ // same way this command does, without re-deriving the rules.
648
+ // ---------------------------------------------------------------------------
649
+ const GENERIC_RULE =
650
+ 'If a path is not in `explicit` (checked before and after the alias rewrites), strip/rewrite the ' +
651
+ 'prefix per `aliases` (first matching pattern wins), join the remaining segments with "-", prefix ' +
652
+ '"--", and lower-case it. If that name is a contract token (required or optional) it is the target. ' +
653
+ 'One retry converts camelCase segments to kebab-case (color.text.linkHover → --text-link-hover). ' +
654
+ 'Otherwise the path is emitted verbatim as --<joined-path> and reported as unmapped — never dropped.';
655
+
656
+ /** The full path → token mapping as JSON-serialisable data. */
657
+ function tokenMap({ ciaRoot = path.join(__dirname, '..') } = {}) {
658
+ const contract = loadContract(ciaRoot);
659
+ return {
660
+ generatorVersion: GENERATOR_VERSION,
661
+ contractVersion: contract.version,
662
+ explicit: { ...TOKEN_MAP },
663
+ aliases: PATH_ALIASES.map(([re, to]) => ({ pattern: re.source, replaceWith: to })),
664
+ genericRule: GENERIC_RULE,
665
+ targets: { required: [...contract.required], optional: [...contract.optional] },
666
+ };
667
+ }
668
+
669
+ /** How ONE path resolves, with the contract loaded for the caller. */
670
+ function resolvePath(tokenPath, { ciaRoot = path.join(__dirname, '..') } = {}) {
671
+ if (typeof tokenPath !== 'string' || !tokenPath.trim()) throw new Error('resolvePath: path is required');
672
+ const contract = loadContract(ciaRoot);
673
+ const p = tokenPath.trim();
674
+ const result = mapPath(p, contract);
675
+ const via = TOKEN_MAP[p] ? 'explicit'
676
+ : TOKEN_MAP[applyPathAliases(p)] ? 'explicit-after-alias'
677
+ : result.mapped ? 'generic' : 'passthrough';
678
+ const status = contract.required.includes(result.token) ? 'required'
679
+ : contract.optional.includes(result.token) ? 'optional' : null;
680
+ return { path: p, token: result.token, mapped: result.mapped, via, status };
681
+ }
682
+
548
683
  function themeFromTokens(opts = {}) {
549
684
  const ciaRoot = opts.ciaRoot || CIA_ROOT_DEFAULT;
550
685
  const name = String(opts.name || '').trim();
@@ -661,6 +796,8 @@ module.exports = {
661
796
  resolveAliases,
662
797
  normalizeValue,
663
798
  mapPath,
799
+ tokenMap,
800
+ resolvePath,
664
801
  listBases,
665
802
  TOKEN_MAP,
666
803
  PATH_ALIASES,