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 +6 -6
- package/CHANGELOG.md +21 -0
- package/README.md +8 -5
- package/bin/cia.cjs +10 -1
- package/bin/theme-from-tokens.cjs +69 -1
- package/dist/tokens.d.ts +1 -1
- package/llm.txt +6 -5
- package/mcp/server.cjs +39 -5
- package/package.json +5 -1
- package/scripts/tokens-to-theme.cjs +143 -6
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.**
|
|
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 **
|
|
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
|
[](https://www.npmjs.com/package/css-is-awesome) [](https://github.com/Jerry2d3d/css-is-awesome/actions/workflows/ci.yml) [](./package.json) [](./LICENSE) [](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
|
|
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 —
|
|
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 **
|
|
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** |
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
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.**
|
|
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`. **
|
|
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),
|
|
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
|
-
*
|
|
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": ["
|
|
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
|
|
42
|
-
*
|
|
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
|
|
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.
|
|
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
|
|
52
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
499
|
-
const
|
|
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,
|