css-is-awesome 1.17.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -5
- package/CHANGELOG.md +21 -0
- package/CONTRACT.md +19 -0
- package/README.md +8 -5
- package/VERSIONING.md +1 -0
- package/bin/cia.cjs +10 -1
- package/bin/theme-from-tokens.cjs +69 -1
- package/llm.txt +6 -5
- package/mcp/server.cjs +44 -3
- package/package.json +1 -1
- package/scripts/theme-contract.json +96 -3
- package/scripts/theme-validator.js +29 -7
- package/scripts/tokens-to-theme.cjs +84 -0
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]`
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [1.19.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.18.0...v1.19.0) (2026-09-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **cli:** cia theme map — the design-token → cia-token mapping as data ([283db9b](https://github.com/Jerry2d3d/css-is-awesome/commit/283db9b47b2195b8d8cb3afe9bad254c88e91cab))
|
|
9
|
+
* **cli:** map boilerplate's canonical DTCG layout — font.family roles, component.* overrides ([8ae71fd](https://github.com/Jerry2d3d/css-is-awesome/commit/8ae71fd55b07bbf7da6d562f5388dcc489bcffb9))
|
|
10
|
+
* **mcp:** get_token_map tool + in-process handler (33 tools) ([ff5610f](https://github.com/Jerry2d3d/css-is-awesome/commit/ff5610f5409bead49fa794a8c27967f602376c77))
|
|
11
|
+
|
|
12
|
+
# [1.18.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.17.0...v1.18.0) (2026-09-19)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **site:** playground editor no longer boots from stale text when a share link decodes early ([3f0748f](https://github.com/Jerry2d3d/css-is-awesome/commit/3f0748f28b8a70471f1f61485733872c6ccdd6a9))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **contract:** feature groups for optional tokens — contract 1.2 ([cc98e1f](https://github.com/Jerry2d3d/css-is-awesome/commit/cc98e1f9397ae89ef65b70557a942b5b561a0f85))
|
|
23
|
+
|
|
3
24
|
# [1.17.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.16.1...v1.17.0) (2026-09-19)
|
|
4
25
|
|
|
5
26
|
|
package/CONTRACT.md
CHANGED
|
@@ -306,6 +306,25 @@ Paper themes declare these as `none` / `transparent` so a swap to a glass or pho
|
|
|
306
306
|
|
|
307
307
|
---
|
|
308
308
|
|
|
309
|
+
## Optional tokens by feature (contract 1.2)
|
|
310
|
+
|
|
311
|
+
Every optional token belongs to exactly one **feature** in `scripts/theme-contract.json` (`features`), so a validator, installer or agent can say *"this theme is missing the tokens for print"* instead of listing all 41 optional names. The validator's info line reports counts per feature; `--show-optional` lists them grouped. `npm run check:contract` fails the build if an optional token is in no feature, in two, or if a feature names a required token.
|
|
312
|
+
|
|
313
|
+
| Feature | Tokens | Enables |
|
|
314
|
+
| --- | --- | --- |
|
|
315
|
+
| `density` | `--space-unit` | The density knob: shipped themes derive every --space-N from this one unit via calc(); set it to tighten or open up the whole UI (theme editor slider). |
|
|
316
|
+
| `spacing-aliases` | `--space-2xs`, `--space-xs`, `--space-sm`, `--space-md`, `--space-lg`, `--space-xl` | T-shirt spacing names (2xs–xl) for consumer CSS that prefers them; the library reads the numbered scale, so these are pure aliases. |
|
|
317
|
+
| `print` | `--print-ink`, `--print-paper`, `--print-line`, `--print-muted` | A themed printed page: cia.print-base rebinds ink/surface/border/code tokens onto this palette inside @media print. Without them the ink-on-white default applies. |
|
|
318
|
+
| `component-radius` | `--btn-radius`, `--card-radius`, `--input-radius`, `--modal-radius`, `--badge-radius`, `--tag-radius` | Per-component corner radius overrides (buttons, cards, inputs, modals, badges, tags) without rebuilding SCSS; each falls back to the generic --radius-* scale. |
|
|
319
|
+
| `component-shadows` | `--shadow-button`, `--shadow-card`, `--shadow-dropdown`, `--shadow-input-focus`, `--shadow-modal`, `--shadow-popover`, `--shadow-tooltip`, `--shadow-text` | Per-component elevation overrides; each falls back to the generic --shadow-* scale. |
|
|
320
|
+
| `component-motion` | `--duration-button-hover`, `--duration-modal-open`, `--duration-toast-slide` | Per-component durations for button hover, modal open and toast slide; each falls back to the generic --duration-* scale. |
|
|
321
|
+
| `surfaces-extended` | `--background-elevated`, `--background-hero`, `--background-overlay`, `--background-scrim` | Extra background layers (elevated, hero, overlay, scrim) for themes that want more than the required surface set; each falls back to a required surface. |
|
|
322
|
+
| `borders-extended` | `--border-card`, `--border-divider`, `--border-focus-ring`, `--border-input` | Per-context border colours (card, divider, focus ring, input); each falls back to --border-default / --border-focus. |
|
|
323
|
+
| `logo` | `--logo-default`, `--logo-mark`, `--logo-monochrome`, `--logo-wordmark` | Theme-aware logo assets (default, mark, monochrome, wordmark) as url() or SVG data values for the icon/brand mixins. |
|
|
324
|
+
| `touch-target` | `--touch-target-min` | Minimum interactive target size read by form and button mixins (WCAG 2.2 SC 2.5.8); the library default is 24px. |
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
309
328
|
## Print (optional)
|
|
310
329
|
|
|
311
330
|
Four tokens describing the printed page. They're **optional** — `cia.print-base` (included once, at the stylesheet root) emits every one of them on `:root` inside `@media print` with a clean ink-on-white default, and every print rule reads them via `var(--print-*)`. A theme MAY override them in its own `@media print` block for a paper identity (Press does, for a newsprint look); a theme that sets none of them prints the plain default.
|
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, 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/VERSIONING.md
CHANGED
|
@@ -43,6 +43,7 @@ Additive, non-breaking changes.
|
|
|
43
43
|
| New public CSS class | `.cia-grid-auto-fit` added |
|
|
44
44
|
| New public SCSS mixin | `m.cluster($gap)` added |
|
|
45
45
|
| New optional token added to contract (`"1"` → `"1.1"`) | `--dropdown-offset-y` added to component section |
|
|
46
|
+
| Contract metadata added (e.g. the `features` map, `"1.1"` → `"1.2"`, 2026-09-18) | Additive keys — old validators ignore them |
|
|
46
47
|
| Required token relaxed to optional (contract minor bump) | `--space-unit` required → optional, contract `"1"` → `"1.1"` (2026-09-18) |
|
|
47
48
|
| New theme or recipe shipped | `prism` family added; `mobile-nav` recipe added |
|
|
48
49
|
| New utility class (`.cia-*`) | `.cia-text-balance` added |
|
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/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.
|
|
@@ -104,7 +105,7 @@ Newspaper by day, hacker terminal by night. Most design systems give you dark mo
|
|
|
104
105
|
| Mobile playbook (layouts, dropdown doctrine, lessons) | `src/app/docs/mobile/page.tsx` |
|
|
105
106
|
| Browser support matrix (Baseline floor + progressive tiers) | `src/app/docs/browser-support/page.tsx` |
|
|
106
107
|
| Theme authoring (full walkthrough) | `src/app/docs/authoring/themes/page.tsx` |
|
|
107
|
-
| Theme contract (127 required + 41 optional, machine-readable) | `scripts/theme-contract.json` |
|
|
108
|
+
| Theme contract (127 required + 41 optional grouped into 10 features, machine-readable) | `scripts/theme-contract.json` |
|
|
108
109
|
| Theme pairing (`<link media>` recipe) | `src/app/docs/themes/pairing/page.tsx` |
|
|
109
110
|
| CopyButton JS recipe | `src/app/docs/recipes/copy-button/page.tsx` |
|
|
110
111
|
| Anchor positioning recipe | `src/app/docs/recipes/anchor-positioning/page.tsx` |
|
|
@@ -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:
|
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
|
|
@@ -426,6 +427,12 @@ function loadTokenContract() {
|
|
|
426
427
|
return { required: [], optional: [], byName: {}, byCategory: {} };
|
|
427
428
|
}
|
|
428
429
|
const optional = Array.isArray(contract.optional) ? contract.optional : [];
|
|
430
|
+
// Contract 1.2: optional tokens are grouped by the feature they enable.
|
|
431
|
+
const features = contract.features && typeof contract.features === 'object' ? contract.features : {};
|
|
432
|
+
const featureOf = {};
|
|
433
|
+
for (const [feature, def] of Object.entries(features)) {
|
|
434
|
+
for (const t of (def && Array.isArray(def.tokens)) ? def.tokens : []) featureOf[t] = feature;
|
|
435
|
+
}
|
|
429
436
|
|
|
430
437
|
const byName = {};
|
|
431
438
|
const byCategory = {};
|
|
@@ -460,11 +467,11 @@ function loadTokenContract() {
|
|
|
460
467
|
}
|
|
461
468
|
for (const t of optional) {
|
|
462
469
|
const category = categorize(t);
|
|
463
|
-
byName[t] = { name: t, category, required: false };
|
|
470
|
+
byName[t] = { name: t, category, required: false, feature: featureOf[t] || null };
|
|
464
471
|
(byCategory[category] = byCategory[category] || []).push(t);
|
|
465
472
|
}
|
|
466
473
|
|
|
467
|
-
return { required: contract.required, optional, byName, byCategory };
|
|
474
|
+
return { required: contract.required, optional, features, byName, byCategory };
|
|
468
475
|
}
|
|
469
476
|
|
|
470
477
|
/**
|
|
@@ -711,6 +718,25 @@ const handlers = {
|
|
|
711
718
|
return themeFromTokens({ tokens, name, format, base, dark, mode, validate });
|
|
712
719
|
},
|
|
713
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
|
+
|
|
714
740
|
// ─── Mixins ────────────────────────────────────────────────────────────
|
|
715
741
|
|
|
716
742
|
list_mixins({ category, component, limit = 500, offset = 0 } = {}) {
|
|
@@ -854,6 +880,8 @@ const handlers = {
|
|
|
854
880
|
name: entry.name,
|
|
855
881
|
category: entry.category,
|
|
856
882
|
required: entry.required,
|
|
883
|
+
// Contract 1.2: the feature an OPTIONAL token enables (null for required).
|
|
884
|
+
feature: entry.required ? null : (entry.feature || null),
|
|
857
885
|
themeValues,
|
|
858
886
|
referencedBy: referencedBy.slice(0, 20),
|
|
859
887
|
};
|
|
@@ -1388,6 +1416,19 @@ async function startServer() {
|
|
|
1388
1416
|
},
|
|
1389
1417
|
}, async (a) => ok(handlers.theme_from_tokens(a || {})));
|
|
1390
1418
|
|
|
1419
|
+
server.registerTool('get_token_map', {
|
|
1420
|
+
description:
|
|
1421
|
+
'The design-token → cia-token mapping that theme_from_tokens applies, as data. Without `path`: ' +
|
|
1422
|
+
'{ generatorVersion, contractVersion, explicit: { "<path>": "--token" }, aliases: [{ pattern, ' +
|
|
1423
|
+
'replaceWith }], genericRule, targets: { required, optional } }. With `path` (e.g. ' +
|
|
1424
|
+
'"color.text.primary"): how that one path resolves — { token, mapped, via, status, required, ' +
|
|
1425
|
+
'feature, category }. Use it to map a Figma / DTCG / Tokens Studio token name to the cia custom ' +
|
|
1426
|
+
'property the same way the converter does, or to check a name before building a theme.',
|
|
1427
|
+
inputSchema: {
|
|
1428
|
+
path: z.string().optional().describe('One token path to resolve (dot-separated, e.g. spacing.4). Omit for the whole map.'),
|
|
1429
|
+
},
|
|
1430
|
+
}, async (a) => ok(handlers.get_token_map(a || {})));
|
|
1431
|
+
|
|
1391
1432
|
// Mixins
|
|
1392
1433
|
server.registerTool('list_mixins', {
|
|
1393
1434
|
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.0",
|
|
4
4
|
"description": "A token-driven SCSS design system with light/dark theming, semantic color tokens, and a 800+ LOC mixin API.",
|
|
5
5
|
"homepage": "https://github.com/Jerry2d3d/css-is-awesome#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"description": "Authoritative token contract for css-is-awesome themes. Every theme.css MUST declare every token in `required`. Tokens in `optional` are reported by the validator as info, never as failures \u2014 the library or the theme generator supplies a default. Source of truth is CONTRACT.md (human-readable) and public/theme.css (Sketchbook reference). Contract 1.1 (2026-09-18): --space-unit relaxed from required to optional; it had been added as required in library 1.12.0, which VERSIONING.md forbids in a MINOR.",
|
|
2
|
+
"version": "1.2",
|
|
3
|
+
"description": "Authoritative token contract for css-is-awesome themes. Every theme.css MUST declare every token in `required`. Tokens in `optional` are reported by the validator as info, never as failures \u2014 the library or the theme generator supplies a default. Source of truth is CONTRACT.md (human-readable) and public/theme.css (Sketchbook reference). Contract 1.1 (2026-09-18): --space-unit relaxed from required to optional; it had been added as required in library 1.12.0, which VERSIONING.md forbids in a MINOR. Contract 1.2 (2026-09-18): `features` groups every optional token by the capability it enables, so a validator or installer can say \"missing the tokens for print\" instead of listing all optional tokens.",
|
|
4
4
|
"required": [
|
|
5
5
|
"--action-primary-active",
|
|
6
6
|
"--action-primary-default",
|
|
@@ -172,5 +172,98 @@
|
|
|
172
172
|
"--space-xs",
|
|
173
173
|
"--tag-radius",
|
|
174
174
|
"--touch-target-min"
|
|
175
|
-
]
|
|
175
|
+
],
|
|
176
|
+
"features": {
|
|
177
|
+
"density": {
|
|
178
|
+
"enables": "The density knob: shipped themes derive every --space-N from this one unit via calc(); set it to tighten or open up the whole UI (theme editor slider).",
|
|
179
|
+
"tokens": [
|
|
180
|
+
"--space-unit"
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
"spacing-aliases": {
|
|
184
|
+
"enables": "T-shirt spacing names (2xs\u2013xl) for consumer CSS that prefers them; the library reads the numbered scale, so these are pure aliases.",
|
|
185
|
+
"tokens": [
|
|
186
|
+
"--space-2xs",
|
|
187
|
+
"--space-xs",
|
|
188
|
+
"--space-sm",
|
|
189
|
+
"--space-md",
|
|
190
|
+
"--space-lg",
|
|
191
|
+
"--space-xl"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
"print": {
|
|
195
|
+
"enables": "A themed printed page: cia.print-base rebinds ink/surface/border/code tokens onto this palette inside @media print. Without them the ink-on-white default applies.",
|
|
196
|
+
"tokens": [
|
|
197
|
+
"--print-ink",
|
|
198
|
+
"--print-paper",
|
|
199
|
+
"--print-line",
|
|
200
|
+
"--print-muted"
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
"component-radius": {
|
|
204
|
+
"enables": "Per-component corner radius overrides (buttons, cards, inputs, modals, badges, tags) without rebuilding SCSS; each falls back to the generic --radius-* scale.",
|
|
205
|
+
"tokens": [
|
|
206
|
+
"--btn-radius",
|
|
207
|
+
"--card-radius",
|
|
208
|
+
"--input-radius",
|
|
209
|
+
"--modal-radius",
|
|
210
|
+
"--badge-radius",
|
|
211
|
+
"--tag-radius"
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
"component-shadows": {
|
|
215
|
+
"enables": "Per-component elevation overrides; each falls back to the generic --shadow-* scale.",
|
|
216
|
+
"tokens": [
|
|
217
|
+
"--shadow-button",
|
|
218
|
+
"--shadow-card",
|
|
219
|
+
"--shadow-dropdown",
|
|
220
|
+
"--shadow-input-focus",
|
|
221
|
+
"--shadow-modal",
|
|
222
|
+
"--shadow-popover",
|
|
223
|
+
"--shadow-tooltip",
|
|
224
|
+
"--shadow-text"
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
"component-motion": {
|
|
228
|
+
"enables": "Per-component durations for button hover, modal open and toast slide; each falls back to the generic --duration-* scale.",
|
|
229
|
+
"tokens": [
|
|
230
|
+
"--duration-button-hover",
|
|
231
|
+
"--duration-modal-open",
|
|
232
|
+
"--duration-toast-slide"
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
"surfaces-extended": {
|
|
236
|
+
"enables": "Extra background layers (elevated, hero, overlay, scrim) for themes that want more than the required surface set; each falls back to a required surface.",
|
|
237
|
+
"tokens": [
|
|
238
|
+
"--background-elevated",
|
|
239
|
+
"--background-hero",
|
|
240
|
+
"--background-overlay",
|
|
241
|
+
"--background-scrim"
|
|
242
|
+
]
|
|
243
|
+
},
|
|
244
|
+
"borders-extended": {
|
|
245
|
+
"enables": "Per-context border colours (card, divider, focus ring, input); each falls back to --border-default / --border-focus.",
|
|
246
|
+
"tokens": [
|
|
247
|
+
"--border-card",
|
|
248
|
+
"--border-divider",
|
|
249
|
+
"--border-focus-ring",
|
|
250
|
+
"--border-input"
|
|
251
|
+
]
|
|
252
|
+
},
|
|
253
|
+
"logo": {
|
|
254
|
+
"enables": "Theme-aware logo assets (default, mark, monochrome, wordmark) as url() or SVG data values for the icon/brand mixins.",
|
|
255
|
+
"tokens": [
|
|
256
|
+
"--logo-default",
|
|
257
|
+
"--logo-mark",
|
|
258
|
+
"--logo-monochrome",
|
|
259
|
+
"--logo-wordmark"
|
|
260
|
+
]
|
|
261
|
+
},
|
|
262
|
+
"touch-target": {
|
|
263
|
+
"enables": "Minimum interactive target size read by form and button mixins (WCAG 2.2 SC 2.5.8); the library default is 24px.",
|
|
264
|
+
"tokens": [
|
|
265
|
+
"--touch-target-min"
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
}
|
|
176
269
|
}
|
|
@@ -342,7 +342,19 @@ function validateTokenSet(declared, contract) {
|
|
|
342
342
|
for (const optional of Array.isArray(contract.optional) ? contract.optional : []) {
|
|
343
343
|
if (!declared.has(optional)) optionalMissing.push(optional);
|
|
344
344
|
}
|
|
345
|
-
|
|
345
|
+
// Contract 1.2: group by the feature each optional token enables, so the
|
|
346
|
+
// report can say "missing the tokens for print" instead of listing 41 names.
|
|
347
|
+
const featureOf = {};
|
|
348
|
+
const features = contract.features && typeof contract.features === 'object' ? contract.features : {};
|
|
349
|
+
for (const [feature, def] of Object.entries(features)) {
|
|
350
|
+
for (const t of (def && Array.isArray(def.tokens)) ? def.tokens : []) featureOf[t] = feature;
|
|
351
|
+
}
|
|
352
|
+
const optionalMissingByFeature = {};
|
|
353
|
+
for (const t of optionalMissing) {
|
|
354
|
+
const f = featureOf[t] || 'other';
|
|
355
|
+
(optionalMissingByFeature[f] = optionalMissingByFeature[f] || []).push(t);
|
|
356
|
+
}
|
|
357
|
+
return { ok: missing.length === 0, missing, optionalMissing, optionalMissingByFeature, declaredCount: declared.size };
|
|
346
358
|
}
|
|
347
359
|
|
|
348
360
|
// -----------------------------------------------------------
|
|
@@ -367,6 +379,7 @@ function validateText(text, contract, options) {
|
|
|
367
379
|
declaredCount: 0,
|
|
368
380
|
missing: [],
|
|
369
381
|
optionalMissing: [],
|
|
382
|
+
optionalMissingByFeature: {},
|
|
370
383
|
themes: null,
|
|
371
384
|
a11y: null,
|
|
372
385
|
error: null,
|
|
@@ -389,6 +402,7 @@ function validateText(text, contract, options) {
|
|
|
389
402
|
declaredCount: v.declaredCount,
|
|
390
403
|
missing: v.missing,
|
|
391
404
|
optionalMissing: v.optionalMissing,
|
|
405
|
+
optionalMissingByFeature: v.optionalMissingByFeature,
|
|
392
406
|
a11y: null,
|
|
393
407
|
};
|
|
394
408
|
if (wantA11y) theme.a11y = a11y.auditThemeTokens({ name: b.name, values: b.values });
|
|
@@ -417,6 +431,7 @@ function validateText(text, contract, options) {
|
|
|
417
431
|
result.declaredCount = v.declaredCount;
|
|
418
432
|
result.missing = v.missing;
|
|
419
433
|
result.optionalMissing = v.optionalMissing;
|
|
434
|
+
result.optionalMissingByFeature = v.optionalMissingByFeature;
|
|
420
435
|
result.ok = v.ok;
|
|
421
436
|
if (wantA11y) {
|
|
422
437
|
const inferredName = label !== '(pasted CSS)' ? (path.basename(path.dirname(label)) || path.basename(label, '.css')) : 'theme';
|
|
@@ -456,11 +471,18 @@ function relForDisplay(p) {
|
|
|
456
471
|
|
|
457
472
|
// Optional tokens a theme leaves to the library default. Info only — shown as
|
|
458
473
|
// a count, or listed with --show-optional. Never affects the exit code.
|
|
459
|
-
function optionalInfo(optionalMissing, indent) {
|
|
474
|
+
function optionalInfo(optionalMissing, byFeature, indent) {
|
|
460
475
|
const list = Array.isArray(optionalMissing) ? optionalMissing : [];
|
|
461
476
|
if (!list.length) return;
|
|
462
|
-
|
|
463
|
-
|
|
477
|
+
const groups = byFeature && typeof byFeature === 'object' ? byFeature : {};
|
|
478
|
+
const summary = Object.entries(groups).map(([f, ts]) => `${f} ${ts.length}`).join(' · ');
|
|
479
|
+
console.log(`${indent}${dim(`i ${list.length} optional token(s) not declared — library default applies${summary ? ` (${summary})` : ''}`)}`);
|
|
480
|
+
if (SHOW_OPTIONAL) {
|
|
481
|
+
for (const [f, ts] of Object.entries(groups)) {
|
|
482
|
+
console.log(`${indent} ${dim(f + ':')}`);
|
|
483
|
+
for (const token of ts) console.log(`${indent} ${dim(token)}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
464
486
|
}
|
|
465
487
|
|
|
466
488
|
function reportResult(result) {
|
|
@@ -483,7 +505,7 @@ function reportResult(result) {
|
|
|
483
505
|
console.log(
|
|
484
506
|
` ${green('✓')} [data-theme="${t.name}"] ${dim(`(${t.declaredCount} tokens)`)}`
|
|
485
507
|
);
|
|
486
|
-
optionalInfo(t.optionalMissing, ' ');
|
|
508
|
+
optionalInfo(t.optionalMissing, t.optionalMissingByFeature, ' ');
|
|
487
509
|
} else {
|
|
488
510
|
const n = t.missing.length;
|
|
489
511
|
console.log(
|
|
@@ -502,7 +524,7 @@ function reportResult(result) {
|
|
|
502
524
|
console.log(
|
|
503
525
|
`${green('✓')} ${bold(rel)} ${dim(`passes (${result.declaredCount} tokens declared)`)}`
|
|
504
526
|
);
|
|
505
|
-
optionalInfo(result.optionalMissing, ' ');
|
|
527
|
+
optionalInfo(result.optionalMissing, result.optionalMissingByFeature, ' ');
|
|
506
528
|
return;
|
|
507
529
|
}
|
|
508
530
|
|
|
@@ -583,7 +605,7 @@ function printUsage() {
|
|
|
583
605
|
' --all validate every theme.css under public/ (CI mode)',
|
|
584
606
|
' --no-a11y skip the WCAG 2.2 AA contrast audit',
|
|
585
607
|
' --allow-a11y-fail do NOT exit non-zero on a11y FAILs (report only)',
|
|
586
|
-
' --show-optional list optional contract tokens a theme leaves to the library default (info only)',
|
|
608
|
+
' --show-optional list optional contract tokens a theme leaves to the library default, grouped by feature (info only)',
|
|
587
609
|
' --strict accepted for backwards compatibility (no-op; FAIL is now the default)',
|
|
588
610
|
'',
|
|
589
611
|
'Exit codes:',
|
|
@@ -201,6 +201,46 @@ const TOKEN_MAP = {
|
|
|
201
201
|
'layer.modal': '--z-modal',
|
|
202
202
|
'layer.popover': '--z-popover',
|
|
203
203
|
'layer.tooltip': '--z-tooltip',
|
|
204
|
+
// ── Role-named font families (boilerplate / ui-ux-builder layout) ──────
|
|
205
|
+
// "primary" is the body face, "secondary" the display/heading face.
|
|
206
|
+
'font.primary': '--font-primary',
|
|
207
|
+
'font.secondary': '--font-display',
|
|
208
|
+
'font.heading': '--font-display',
|
|
209
|
+
'font.body': '--font-primary',
|
|
210
|
+
'typography.font.secondary': '--font-display',
|
|
211
|
+
'typography.font.heading': '--font-display',
|
|
212
|
+
'typography.family.primary': '--font-primary',
|
|
213
|
+
'typography.family.secondary': '--font-display',
|
|
214
|
+
'typography.family.heading': '--font-display',
|
|
215
|
+
|
|
216
|
+
// ── component.<name>.<knob> → the contract's per-component overrides ──
|
|
217
|
+
// (optional tokens, contract 1.2 features component-radius / -shadows /
|
|
218
|
+
// -motion / borders-extended). `components.` is aliased to `component.`.
|
|
219
|
+
'component.button.radius': '--btn-radius',
|
|
220
|
+
'component.button.shadow': '--shadow-button',
|
|
221
|
+
'component.button.duration': '--duration-button-hover',
|
|
222
|
+
'component.card.radius': '--card-radius',
|
|
223
|
+
'component.card.shadow': '--shadow-card',
|
|
224
|
+
'component.card.border': '--border-card',
|
|
225
|
+
'component.input.radius': '--input-radius',
|
|
226
|
+
'component.input.border': '--border-input',
|
|
227
|
+
'component.input.shadow': '--shadow-input-focus',
|
|
228
|
+
'component.input.focus-shadow': '--shadow-input-focus',
|
|
229
|
+
'component.modal.radius': '--modal-radius',
|
|
230
|
+
'component.modal.shadow': '--shadow-modal',
|
|
231
|
+
'component.modal.duration': '--duration-modal-open',
|
|
232
|
+
'component.badge.radius': '--badge-radius',
|
|
233
|
+
'component.tag.radius': '--tag-radius',
|
|
234
|
+
'component.chip.radius': '--tag-radius',
|
|
235
|
+
'component.dropdown.shadow': '--shadow-dropdown',
|
|
236
|
+
'component.popover.shadow': '--shadow-popover',
|
|
237
|
+
'component.tooltip.shadow': '--shadow-tooltip',
|
|
238
|
+
'component.toast.duration': '--duration-toast-slide',
|
|
239
|
+
'component.divider.border': '--border-divider',
|
|
240
|
+
'component.divider.color': '--border-divider',
|
|
241
|
+
'component.focus.ring': '--border-focus-ring',
|
|
242
|
+
'component.text.shadow': '--shadow-text',
|
|
243
|
+
'component.touch-target.min': '--touch-target-min',
|
|
204
244
|
};
|
|
205
245
|
|
|
206
246
|
// Group-name rewrites applied before the generic rule (rule 2). Case-insensitive
|
|
@@ -226,6 +266,9 @@ const PATH_ALIASES = [
|
|
|
226
266
|
[/^lineHeights?\./i, 'line-height.'],
|
|
227
267
|
[/^durations?\./i, 'duration.'],
|
|
228
268
|
[/^motion\.duration\./i, 'duration.'],
|
|
269
|
+
[/^font\.family\./, 'font.'], // font.family.mono → font.mono → --font-mono
|
|
270
|
+
[/^fontFamily\./, 'font.'],
|
|
271
|
+
[/^components\./, 'component.'], // components.button.radius → component.button.radius
|
|
229
272
|
];
|
|
230
273
|
|
|
231
274
|
// Token families that are unitless by contract — a bare number stays bare.
|
|
@@ -545,6 +588,45 @@ function collect(tokens, format, contract) {
|
|
|
545
588
|
// ---------------------------------------------------------------------------
|
|
546
589
|
const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
547
590
|
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
// The mapping as DATA — so a second implementation (a boilerplate registry,
|
|
593
|
+
// an inventory builder, another agent) can map the same source token the
|
|
594
|
+
// same way this command does, without re-deriving the rules.
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
const GENERIC_RULE =
|
|
597
|
+
'If a path is not in `explicit` (checked before and after the alias rewrites), strip/rewrite the ' +
|
|
598
|
+
'prefix per `aliases` (first matching pattern wins), join the remaining segments with "-", prefix ' +
|
|
599
|
+
'"--", and lower-case it. If that name is a contract token (required or optional) it is the target. ' +
|
|
600
|
+
'One retry converts camelCase segments to kebab-case (color.text.linkHover → --text-link-hover). ' +
|
|
601
|
+
'Otherwise the path is emitted verbatim as --<joined-path> and reported as unmapped — never dropped.';
|
|
602
|
+
|
|
603
|
+
/** The full path → token mapping as JSON-serialisable data. */
|
|
604
|
+
function tokenMap({ ciaRoot = path.join(__dirname, '..') } = {}) {
|
|
605
|
+
const contract = loadContract(ciaRoot);
|
|
606
|
+
return {
|
|
607
|
+
generatorVersion: GENERATOR_VERSION,
|
|
608
|
+
contractVersion: contract.version,
|
|
609
|
+
explicit: { ...TOKEN_MAP },
|
|
610
|
+
aliases: PATH_ALIASES.map(([re, to]) => ({ pattern: re.source, replaceWith: to })),
|
|
611
|
+
genericRule: GENERIC_RULE,
|
|
612
|
+
targets: { required: [...contract.required], optional: [...contract.optional] },
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** How ONE path resolves, with the contract loaded for the caller. */
|
|
617
|
+
function resolvePath(tokenPath, { ciaRoot = path.join(__dirname, '..') } = {}) {
|
|
618
|
+
if (typeof tokenPath !== 'string' || !tokenPath.trim()) throw new Error('resolvePath: path is required');
|
|
619
|
+
const contract = loadContract(ciaRoot);
|
|
620
|
+
const p = tokenPath.trim();
|
|
621
|
+
const result = mapPath(p, contract);
|
|
622
|
+
const via = TOKEN_MAP[p] ? 'explicit'
|
|
623
|
+
: TOKEN_MAP[applyPathAliases(p)] ? 'explicit-after-alias'
|
|
624
|
+
: result.mapped ? 'generic' : 'passthrough';
|
|
625
|
+
const status = contract.required.includes(result.token) ? 'required'
|
|
626
|
+
: contract.optional.includes(result.token) ? 'optional' : null;
|
|
627
|
+
return { path: p, token: result.token, mapped: result.mapped, via, status };
|
|
628
|
+
}
|
|
629
|
+
|
|
548
630
|
function themeFromTokens(opts = {}) {
|
|
549
631
|
const ciaRoot = opts.ciaRoot || CIA_ROOT_DEFAULT;
|
|
550
632
|
const name = String(opts.name || '').trim();
|
|
@@ -661,6 +743,8 @@ module.exports = {
|
|
|
661
743
|
resolveAliases,
|
|
662
744
|
normalizeValue,
|
|
663
745
|
mapPath,
|
|
746
|
+
tokenMap,
|
|
747
|
+
resolvePath,
|
|
664
748
|
listBases,
|
|
665
749
|
TOKEN_MAP,
|
|
666
750
|
PATH_ALIASES,
|