css-is-awesome 1.20.0 → 1.21.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 +9 -3
- package/CHANGELOG.md +7 -0
- package/CONTRACT.md +7 -0
- package/MIGRATION.md +6 -0
- package/README.md +16 -10
- package/bin/analyze.cjs +47 -2
- package/bin/cia.cjs +10 -0
- package/bin/fix-theme.cjs +151 -0
- package/llm.txt +3 -3
- package/mcp/server.cjs +64 -3
- package/package.json +3 -1
- package/scripts/fix-theme.cjs +184 -0
- package/scripts/theme-validator.js +40 -1
package/AGENTS.md
CHANGED
|
@@ -168,7 +168,7 @@ Why it matters: components call `cia.space(4)`, which resolves to `var(--space-4
|
|
|
168
168
|
|
|
169
169
|
Two `<link media>` themes still work under the new selector model: a stylesheet whose `media` doesn't match is loaded but never applied, so only the matching file's `:root` block lands.
|
|
170
170
|
|
|
171
|
-
Validator: `node scripts/theme-validator.js path/to/theme.css` (or `--all` for every shipped theme). Every theme must declare every required contract token (127 required in contract 1.
|
|
171
|
+
Validator: `node scripts/theme-validator.js path/to/theme.css` (or `--all` for every shipped theme). Every theme must declare every required contract token (127 required in contract 1.3; missing required tokens always fail, missing optional ones are reported as info). The audit also runs a WCAG 2.2 AA contrast check over 22 pairs; **a11y FAILs are fatal by default** as of v0.7. Pass `--allow-a11y-fail` to downgrade contrast failures to a report-only warning (the older `--strict` flag is accepted as a no-op alias). `--border-default` is treated as decorative per WCAG 2.2 SC 1.4.11 and reports as info, not FAIL.
|
|
172
172
|
|
|
173
173
|
### Theme init (Next.js / SSR consumers)
|
|
174
174
|
|
|
@@ -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 **34 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)
|
|
@@ -347,6 +347,7 @@ Either way it exposes **33 tools** across 8 families:
|
|
|
347
347
|
- **Animations** — `list_animations`, `get_animation`
|
|
348
348
|
- **Components** — `list_components`, `get_component`, `search_components`
|
|
349
349
|
- **Recipes** — `list_recipes`, `get_recipe`
|
|
350
|
+
- **Theme upgrades** — `fix_theme` (rewrite deprecated tokens to their replacements; returns text, never writes)
|
|
350
351
|
- **Doc readers** — `read_llm_txt`, `read_changelog`, `read_migration`, `read_theming`, `read_agents`, `read_contract`, `read_three_tiers`, `read_readme`, `read_versioning`
|
|
351
352
|
- **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
353
|
|
|
@@ -366,7 +367,12 @@ Either way it exposes **33 tools** across 8 families:
|
|
|
366
367
|
default boilerplate), unmapped paths pass through verbatim and are reported, a `--dark` file or paired
|
|
367
368
|
`color-light`/`color-dark` groups become `light-dark()`, and the validator + WCAG audit run before anything is
|
|
368
369
|
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
|
-
planned.)
|
|
370
|
+
planned.) `npx cia fix-theme <theme.css> [--write]` moves a theme onto
|
|
371
|
+
current token names: it renames any DEPRECATED token to its replacement,
|
|
372
|
+
changing the property only — values, comments and ordering survive, so the
|
|
373
|
+
rendered theme is identical. Prints by default; writes only with `--write`;
|
|
374
|
+
a block already declaring the replacement is reported, never merged. Same
|
|
375
|
+
function as the MCP `fix_theme` tool.
|
|
370
376
|
- **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
377
|
- **`llm.txt`** — at the repo root and served from the docs site; single-fetch
|
|
372
378
|
summary for any AI agent. Also readable over MCP via `read_llm_txt`.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [1.21.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.20.0...v1.21.0) (2026-09-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **cli:** cia fix-theme — rename deprecated tokens to their replacements ([665d871](https://github.com/Jerry2d3d/css-is-awesome/commit/665d87129f578d1ac38f410ce7461f37bbe24357))
|
|
9
|
+
|
|
3
10
|
# [1.20.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.19.2...v1.20.0) (2026-09-23)
|
|
4
11
|
|
|
5
12
|
|
package/CONTRACT.md
CHANGED
|
@@ -635,3 +635,10 @@ A PR that adds a new contract glyph must:
|
|
|
635
635
|
Per-theme override glyphs are NEVER required by the contract — themes
|
|
636
636
|
opt in glyph-by-glyph by declaring `--cia-icon-<name>` and shipping the
|
|
637
637
|
replacement file alongside.
|
|
638
|
+
>
|
|
639
|
+
> **Deprecations are machine-readable.** Every one lives in the `deprecated` map in
|
|
640
|
+
> [`scripts/theme-contract.json`](./scripts/theme-contract.json) with the token it replaces,
|
|
641
|
+
> when it was deprecated and when it goes. The validator prints it, `get_token` returns it,
|
|
642
|
+
> `npx cia analyze` flags it in your stylesheets, and `npx cia fix-theme <file> --write`
|
|
643
|
+
> renames it for you — property only, so the theme renders identically. `check:contract`
|
|
644
|
+
> fails the build if a deprecation points at a token that does not exist.
|
package/MIGRATION.md
CHANGED
|
@@ -30,6 +30,12 @@ so the guarantee is enforced rather than promised.
|
|
|
30
30
|
| `--background-hero` | optional, read by nothing | **deprecated**; `--page-hero-bg` falls back through it, so an existing declaration keeps working until contract 2 |
|
|
31
31
|
| Your custom theme | validates | validates — the new tokens are optional, and missing optional tokens report as info |
|
|
32
32
|
|
|
33
|
+
Nothing here needs doing. If you would rather move onto the current name now,
|
|
34
|
+
`npx cia fix-theme <your-theme.css>` shows what it would change and
|
|
35
|
+
`--write` applies it. It renames the property and nothing else, so the theme
|
|
36
|
+
renders identically; a block already declaring `--page-hero-bg` is reported
|
|
37
|
+
rather than merged.
|
|
38
|
+
|
|
33
39
|
### If you want a hero
|
|
34
40
|
|
|
35
41
|
Declare the tokens your theme needs and apply the surface where you want it:
|
package/README.md
CHANGED
|
@@ -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 — 34 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
|
{
|
|
@@ -79,7 +79,7 @@ Author your own class names; the mixin handles the styling. Mixins for buttons,
|
|
|
79
79
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/css-is-awesome@1/dist/css-is-awesome.min.css">
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
Theme first (sets the tokens), library second. **No `data-theme` attribute needed** — a single theme file styles the page on its own. Swap the URL to swap the theme; the HTML never changes. Bundle tiers — `dist/tokens.css` (2.2 KB gz, `:where(:root)` vars only), `dist/css-is-awesome.core.min.css` (2.4 KB gz, tokens + resets), `dist/css-is-awesome.min.css` (
|
|
82
|
+
Theme first (sets the tokens), library second. **No `data-theme` attribute needed** — a single theme file styles the page on its own. Swap the URL to swap the theme; the HTML never changes. Bundle tiers — `dist/tokens.css` (2.2 KB gz, `:where(:root)` vars only), `dist/css-is-awesome.core.min.css` (2.4 KB gz, tokens + resets), `dist/css-is-awesome.min.css` (8.0 KB gz, full).
|
|
83
83
|
|
|
84
84
|
### 3. Bare tags (opt-in Pico-mode)
|
|
85
85
|
|
|
@@ -151,7 +151,7 @@ Full authoring walkthrough: [`/docs/authoring/themes`](https://cssisawesome.com/
|
|
|
151
151
|
|
|
152
152
|
## Token contract
|
|
153
153
|
|
|
154
|
-
Every theme declares the same slots: **surfaces · ink · lines · primary · seal · accent · code · type · space · radius · shadow · blur · glow · motion**. Components read tokens, themes set tokens, nothing else. 127 required,
|
|
154
|
+
Every theme declares the same slots: **surfaces · ink · lines · primary · seal · accent · code · type · space · radius · shadow · blur · glow · motion**. Components read tokens, themes set tokens, nothing else. 127 required, 49 optional.
|
|
155
155
|
|
|
156
156
|
**Themes own the spacing scale.** A theme declares the numbered scale `--space-0` … `--space-9` (contract-required), which is exactly what `cia.space(4)` compiles to — so a theme can ship tighter or airier rhythm without touching a component. The six t-shirt names (`--space-2xs/xs/sm/md/lg/xl`) are optional; the library emits them as references (`--space-md: var(--space-4)`), so they track the numbered scale automatically.
|
|
157
157
|
|
|
@@ -223,6 +223,7 @@ The CLI also carries the registry and the health check:
|
|
|
223
223
|
|
|
224
224
|
```bash
|
|
225
225
|
npx cia add --list # browse the recipe book
|
|
226
|
+
npx cia fix-theme t.css # move a theme onto current token names (--write to apply)
|
|
226
227
|
npx cia add bottom-nav # copy a recipe into your project — you own the pattern
|
|
227
228
|
npx cia analyze src/styles # design-system health: dead cia.* symbols, the
|
|
228
229
|
# space() scale trap, off-contract tokens (typos),
|
|
@@ -303,7 +304,7 @@ The scope is kept narrow: 8 `!important` declarations, all inside `@media print`
|
|
|
303
304
|
|
|
304
305
|
## MCP server (for AI agents)
|
|
305
306
|
|
|
306
|
-
cia ships a Model Context Protocol stdio server (JSON-RPC over stdio, protocol `2024-11-05`) exposing **
|
|
307
|
+
cia ships a Model Context Protocol stdio server (JSON-RPC over stdio, protocol `2024-11-05`) exposing **34 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/).
|
|
307
308
|
|
|
308
309
|
**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.
|
|
309
310
|
|
|
@@ -366,6 +367,8 @@ It also hosts the **[playground](https://cssisawesome.com/playground/)**: write
|
|
|
366
367
|
| `npm run build:css:all` | Compile all bundles (full + core + utilities + minified) + themes + token types |
|
|
367
368
|
| `npm run build:css:themes` | Rebuild the 24 per-theme CSS files in `public/themes/` **and** regenerate the all-in-one `public/theme.css` bundle |
|
|
368
369
|
| `npm run check:theme-drift` | Rebuild the themes into a scratch copy and fail if the committed artifacts don't match their SCSS sources |
|
|
370
|
+
| `npm run check:contract` | Diff the token contract against the last release tag and fail if the required/optional lists changed without the version bump [`VERSIONING.md`](./VERSIONING.md) requires |
|
|
371
|
+
| `npm run check:rtl` | Audit the SCSS for physical properties that should be logical (`margin-left` → `margin-inline-start`) |
|
|
369
372
|
| `npm run build:token-types` | Generate `dist/tokens.d.ts` from the contract |
|
|
370
373
|
| `npm run dtcg-to-scss` | Convert DTCG-format design tokens into cia SCSS |
|
|
371
374
|
| `npm run lint` | ESLint on the Next.js app |
|
|
@@ -374,6 +377,9 @@ It also hosts the **[playground](https://cssisawesome.com/playground/)**: write
|
|
|
374
377
|
| `npm run validate-icons` | Validate the `core` icon pack against the 49-glyph contract |
|
|
375
378
|
| `npm run validate-api` | Assert the `css-is-awesome/api` barrel stays zero-emit |
|
|
376
379
|
| `npm run validate-package` | Pack + install into a temp project and compile every documented `@use` form — catches breakage that in-repo checks can't see |
|
|
380
|
+
| `npm run test:surfaces` | Assert cia never applies a page surface automatically — the guarantee that adding hero/band tokens changes nobody's existing page |
|
|
381
|
+
| `npm run test:tokens` | Exercise the design-tokens converter (DTCG / Tokens Studio / flat) against fixtures |
|
|
382
|
+
| `npm run verify:playground` | Compile real samples through the playground's in-browser Sass path in Node |
|
|
377
383
|
| `npm run pack:consumer` | Pack and install this build into a local consumer (defaults to `../boiler-project-ai`); `--dry-run` supported |
|
|
378
384
|
| `npm test` | Playwright suite — axe a11y checks + per-theme visual snapshots |
|
|
379
385
|
|
|
@@ -408,18 +414,18 @@ Full detail: [`/docs/testing`](https://cssisawesome.com/docs/testing/).
|
|
|
408
414
|
|
|
409
415
|
| Bundle | Size | Use case |
|
|
410
416
|
|---|---|---|
|
|
411
|
-
| `dist/tokens.css` | 2.
|
|
412
|
-
| `dist/css-is-awesome.core.min.css` | 2.
|
|
413
|
-
| `dist/css-is-awesome.utilities.min.css` | 4.
|
|
414
|
-
| `dist/css-is-awesome.min.css` | 7.
|
|
415
|
-
| Per-theme `themes/<name>/theme.css` |
|
|
417
|
+
| `dist/tokens.css` | 2.26 KB | Tokens only (`:where(:root)` CSS variables, no rules) — the purest mixin-first emit |
|
|
418
|
+
| `dist/css-is-awesome.core.min.css` | 2.42 KB | Tokens + resets, no utilities or components |
|
|
419
|
+
| `dist/css-is-awesome.utilities.min.css` | 4.79 KB | Every `cia-*` utility class, nothing else |
|
|
420
|
+
| `dist/css-is-awesome.min.css` | 7.99 KB | Full bundle (everything) |
|
|
421
|
+
| Per-theme `themes/<name>/theme.css` | 2.0–3.8 KB | One file per theme, both modes via `light-dark()`, drop-in with no markup change |
|
|
416
422
|
| **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. |
|
|
417
423
|
|
|
418
424
|
## Status
|
|
419
425
|
|
|
420
426
|
**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.
|
|
421
427
|
|
|
422
|
-
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
|
|
428
|
+
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 34-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.
|
|
423
429
|
|
|
424
430
|
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.
|
|
425
431
|
|
package/bin/analyze.cjs
CHANGED
|
@@ -33,6 +33,18 @@ function loadContractTokens() {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Tokens the contract has superseded. They still resolve — cia deprecates
|
|
37
|
+
// rather than deletes — so this is a warning with a one-command fix, not an
|
|
38
|
+
// error. Read from the same map `cia fix-theme` and the validator use.
|
|
39
|
+
function loadDeprecated() {
|
|
40
|
+
try {
|
|
41
|
+
const c = JSON.parse(fs.readFileSync(CONTRACT_PATH, 'utf8'));
|
|
42
|
+
return c.deprecated && typeof c.deprecated === 'object' ? c.deprecated : {};
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
// Tiny Levenshtein (zero-dep). Only called on var(--x) misses, so cost is trivial.
|
|
37
49
|
function editDistance(a, b) {
|
|
38
50
|
const m = a.length, n = b.length;
|
|
@@ -256,7 +268,7 @@ function missingFocusVisible(strippedSrc) {
|
|
|
256
268
|
return [{ level: 'info', rule: 'missing-focus-visible', detail: 'interactive :hover/:active styling with no :focus-visible or focus-ring anywhere in this file — keyboard users may get no visible feedback' }];
|
|
257
269
|
}
|
|
258
270
|
|
|
259
|
-
function analyzeFile(file, symbols, contractTokens, extraNs) {
|
|
271
|
+
function analyzeFile(file, symbols, contractTokens, extraNs, deprecated) {
|
|
260
272
|
const raw = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
|
|
261
273
|
const src = stripComments(raw);
|
|
262
274
|
const ns = detectNamespaces(src, extraNs);
|
|
@@ -280,6 +292,37 @@ function analyzeFile(file, symbols, contractTokens, extraNs) {
|
|
|
280
292
|
}
|
|
281
293
|
}
|
|
282
294
|
}
|
|
295
|
+
// deprecated-token: still resolves, but the contract names a successor.
|
|
296
|
+
// Catches both a declaration (--old: value) and a reference (var(--old)).
|
|
297
|
+
// A plain scan, not a built regex: custom-property names are distinctive
|
|
298
|
+
// enough that a substring plus a name-boundary check is exact, and it does
|
|
299
|
+
// not depend on escaping a token name into a pattern.
|
|
300
|
+
if (deprecated && Object.keys(deprecated).length) {
|
|
301
|
+
const isNameChar = (ch) => /[A-Za-z0-9_-]/.test(ch);
|
|
302
|
+
for (const [tok, def] of Object.entries(deprecated)) {
|
|
303
|
+
let at = src.indexOf(tok);
|
|
304
|
+
let hit = false;
|
|
305
|
+
while (at !== -1) {
|
|
306
|
+
const after = src[at + tok.length];
|
|
307
|
+
// Reject a longer name that merely starts with this one.
|
|
308
|
+
if (after === undefined || !isNameChar(after)) { hit = true; break; }
|
|
309
|
+
at = src.indexOf(tok, at + 1);
|
|
310
|
+
}
|
|
311
|
+
if (!hit) continue;
|
|
312
|
+
const to = (def && def.replacedBy) || null;
|
|
313
|
+
let since = '';
|
|
314
|
+
if (def && def.since) {
|
|
315
|
+
since = ' (contract ' + def.since + (def.removeIn ? ', removed in ' + def.removeIn : '') + ')';
|
|
316
|
+
}
|
|
317
|
+
findings.push({
|
|
318
|
+
level: 'warn',
|
|
319
|
+
rule: 'deprecated-token',
|
|
320
|
+
detail: tok + ' is deprecated' + since + (to ? ' — use ' + to : '') +
|
|
321
|
+
'; your value still works. Run `npx cia fix-theme <file>` to rename it.',
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
283
326
|
// off-contract-token: a var(--x) that's a near-miss of a real contract token
|
|
284
327
|
// (a typo → the declaration silently fails). Pure custom tokens — not close to
|
|
285
328
|
// any contract token — are the consumer's own and are left alone.
|
|
@@ -325,6 +368,7 @@ const RULE_CATEGORY = {
|
|
|
325
368
|
'unknown-symbol': 'API',
|
|
326
369
|
'space-scale': 'Spacing',
|
|
327
370
|
'off-contract-token': 'Contract',
|
|
371
|
+
'deprecated-token': 'Contract',
|
|
328
372
|
'off-scale-length': 'Spacing',
|
|
329
373
|
'hard-coded-color': 'Color',
|
|
330
374
|
bem: 'Naming',
|
|
@@ -364,8 +408,9 @@ async function run(args) {
|
|
|
364
408
|
|
|
365
409
|
const symbols = collectSymbols();
|
|
366
410
|
const contractTokens = loadContractTokens();
|
|
411
|
+
const deprecated = loadDeprecated();
|
|
367
412
|
const files = walkScss(target);
|
|
368
|
-
const results = files.map((f) => analyzeFile(f, symbols, contractTokens, extraNs)).filter((r) => r.findings.length || r.namespaces.length);
|
|
413
|
+
const results = files.map((f) => analyzeFile(f, symbols, contractTokens, extraNs, deprecated)).filter((r) => r.findings.length || r.namespaces.length);
|
|
369
414
|
|
|
370
415
|
const counts = { error: 0, warn: 0, info: 0 };
|
|
371
416
|
for (const r of results) for (const f of r.findings) counts[f.level]++;
|
package/bin/cia.cjs
CHANGED
|
@@ -43,6 +43,9 @@ Commands:
|
|
|
43
43
|
theme.css. \`cia theme from-tokens --help\`.
|
|
44
44
|
theme map The path → token mapping from-tokens applies, as
|
|
45
45
|
data: a table, \`--json\`, or \`--path <p>\` for one.
|
|
46
|
+
fix-theme <file> Move a theme onto current token names: rewrites
|
|
47
|
+
deprecated tokens to their replacements. Prints by
|
|
48
|
+
default, \`--write\` applies.
|
|
46
49
|
|
|
47
50
|
Examples:
|
|
48
51
|
cia migrate tailwind ./tailwind.config.js
|
|
@@ -50,6 +53,7 @@ Examples:
|
|
|
50
53
|
cia analyze src/styles
|
|
51
54
|
cia theme from-tokens tokens.json --name acme --out src/styles/acme.css
|
|
52
55
|
cia theme map --path color.text.primary
|
|
56
|
+
cia fix-theme src/styles/acme.css --write
|
|
53
57
|
|
|
54
58
|
Run \`cia <command> --help\` for command-specific help.
|
|
55
59
|
`;
|
|
@@ -158,6 +162,12 @@ async function main() {
|
|
|
158
162
|
return;
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
if (command === 'fix-theme') {
|
|
166
|
+
const { run } = require('./fix-theme.cjs');
|
|
167
|
+
await run(rest);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
161
171
|
if (command === 'theme') {
|
|
162
172
|
const [sub, ...themeArgs] = rest;
|
|
163
173
|
if (!sub || sub === '-h' || sub === '--help' || sub === 'help') {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cia fix-theme — rewrite a theme's deprecated tokens to their replacements.
|
|
3
|
+
*
|
|
4
|
+
* The CLI face of scripts/fix-theme.cjs (the MCP tool `fix_theme` and the
|
|
5
|
+
* in-process `handlers.fix_theme` are the same function). cia deprecates a
|
|
6
|
+
* token rather than deleting it, so the old declaration keeps working — this
|
|
7
|
+
* is the one command that moves you onto the current name.
|
|
8
|
+
*
|
|
9
|
+
* Only the property name changes. Values, comments, ordering and whitespace
|
|
10
|
+
* survive byte-for-byte, so applying this cannot alter how the theme looks.
|
|
11
|
+
*
|
|
12
|
+
* Reads and prints by default; writes only with --write.
|
|
13
|
+
*
|
|
14
|
+
* Exit codes: 0 nothing to do or fixed, 1 collisions need a human, 2 usage /
|
|
15
|
+
* input error.
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const HELP = `cia fix-theme — move a theme onto current token names
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
cia fix-theme <theme.css> [options]
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--write Apply the changes to the file (default: print, change nothing)
|
|
29
|
+
--json Machine-readable { css, changes, unchanged, summary }
|
|
30
|
+
-h, --help This text
|
|
31
|
+
|
|
32
|
+
What it does:
|
|
33
|
+
cia deprecates a token instead of deleting it, so your existing declaration
|
|
34
|
+
keeps working. This renames it to the replacement the contract names. Only
|
|
35
|
+
the property name moves — the value, your comments, the ordering and the
|
|
36
|
+
whitespace are untouched, so the rendered theme is identical.
|
|
37
|
+
|
|
38
|
+
If a block already declares the replacement, that line is left alone and
|
|
39
|
+
reported: merging two values is your decision, not the tool's.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
cia fix-theme src/styles/acme.css # show me what would change
|
|
43
|
+
cia fix-theme src/styles/acme.css --write # do it
|
|
44
|
+
cia fix-theme src/styles/acme.css --json # for a script
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const isTTY = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
48
|
+
const c = (code, s) => (isTTY ? `[${code}m${s}[0m` : s);
|
|
49
|
+
const bold = (s) => c('1', s);
|
|
50
|
+
const dim = (s) => c('2', s);
|
|
51
|
+
const green = (s) => c('32', s);
|
|
52
|
+
const yellow = (s) => c('33', s);
|
|
53
|
+
const red = (s) => c('31', s);
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const out = { file: null, write: false, json: false, help: false };
|
|
57
|
+
for (const a of argv) {
|
|
58
|
+
if (a === '--write') out.write = true;
|
|
59
|
+
else if (a === '--json') out.json = true;
|
|
60
|
+
else if (a === '-h' || a === '--help' || a === 'help') out.help = true;
|
|
61
|
+
else if (a.startsWith('-')) throw new Error(`unknown option '${a}'`);
|
|
62
|
+
else if (out.file == null) out.file = a;
|
|
63
|
+
else throw new Error(`unexpected extra argument '${a}'`);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function run(argv) {
|
|
69
|
+
let args;
|
|
70
|
+
try {
|
|
71
|
+
args = parseArgs(argv || []);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
process.stderr.write(`${red('error:')} ${err.message}\n\n${HELP}`);
|
|
74
|
+
process.exitCode = 2;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (args.help || !args.file) {
|
|
78
|
+
process.stdout.write(HELP);
|
|
79
|
+
if (!args.help) process.exitCode = 2;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const abs = path.resolve(args.file);
|
|
84
|
+
let css;
|
|
85
|
+
try {
|
|
86
|
+
css = fs.readFileSync(abs, 'utf8');
|
|
87
|
+
} catch (err) {
|
|
88
|
+
process.stderr.write(`${red('error:')} could not read ${args.file} — ${err.message}\n`);
|
|
89
|
+
process.exitCode = 2;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { fixTheme } = require(path.join(__dirname, '..', 'scripts', 'fix-theme.cjs'));
|
|
94
|
+
let result;
|
|
95
|
+
try {
|
|
96
|
+
result = fixTheme({ css });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
process.stderr.write(`${red('error:')} ${err.message}\n`);
|
|
99
|
+
process.exitCode = 2;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rewrites = result.changes.filter((x) => x.kind === 'rewrite');
|
|
104
|
+
const conflicts = result.changes.filter((x) => x.kind === 'conflict');
|
|
105
|
+
|
|
106
|
+
if (args.json) {
|
|
107
|
+
process.stdout.write(
|
|
108
|
+
JSON.stringify(
|
|
109
|
+
{ file: args.file, written: args.write && rewrites.length > 0, ...result },
|
|
110
|
+
null,
|
|
111
|
+
2
|
|
112
|
+
) + '\n'
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
const rel = path.relative(process.cwd(), abs) || args.file;
|
|
116
|
+
if (!result.changes.length) {
|
|
117
|
+
process.stdout.write(`${green('✓')} ${bold(rel)} ${dim('— no deprecated tokens; already current')}\n`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
process.stdout.write(`${bold(rel)}\n`);
|
|
121
|
+
for (const ch of rewrites) {
|
|
122
|
+
process.stdout.write(` ${green('→')} line ${ch.line}: ${ch.from} ${dim('→')} ${bold(ch.to)}\n`);
|
|
123
|
+
process.stdout.write(` ${dim(ch.reason)}\n`);
|
|
124
|
+
}
|
|
125
|
+
for (const ch of conflicts) {
|
|
126
|
+
process.stdout.write(` ${yellow('!')} line ${ch.line}: ${ch.from} ${dim('left as-is')}\n`);
|
|
127
|
+
process.stdout.write(` ${dim(ch.reason)}\n`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (args.write && rewrites.length) {
|
|
132
|
+
try {
|
|
133
|
+
fs.writeFileSync(abs, result.css);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
process.stderr.write(`${red('error:')} could not write ${args.file} — ${err.message}\n`);
|
|
136
|
+
process.exitCode = 2;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!args.json) {
|
|
140
|
+
process.stdout.write(`\n${green('✓')} wrote ${rewrites.length} change(s) to ${bold(path.relative(process.cwd(), abs) || args.file)}\n`);
|
|
141
|
+
}
|
|
142
|
+
} else if (rewrites.length && !args.json) {
|
|
143
|
+
process.stdout.write(`\n${dim('nothing written — re-run with --write to apply')}\n`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Collisions are the one case a human has to settle, so say so in the exit
|
|
147
|
+
// code too: a pipeline should stop rather than assume the theme is current.
|
|
148
|
+
if (conflicts.length) process.exitCode = 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { run, parseArgs, HELP };
|
package/llm.txt
CHANGED
|
@@ -49,7 +49,7 @@ silent.
|
|
|
49
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.
|
|
50
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.
|
|
51
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).
|
|
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
|
+
- **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. `fix-theme <theme.css> [--write]` renames DEPRECATED tokens to their replacements (property only — values, comments and ordering survive, so the theme renders identically; prints by default, writes with `--write`; a block already declaring the replacement is reported, never merged).
|
|
53
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`.
|
|
54
54
|
- **176 contract tokens — 127 required + 49 optional** — surfaces, ink, lines, colors, type, radius, shadow, blur, glow, motion, z-index, spacing, semantic aliases.
|
|
55
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.
|
|
@@ -129,7 +129,7 @@ Newspaper by day, hacker terminal by night. Most design systems give you dark mo
|
|
|
129
129
|
|
|
130
130
|
## MCP server (shipped 2026-05-22)
|
|
131
131
|
|
|
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`. **
|
|
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`. **34 tools** across 8 resource families: themes, mixins, functions, tokens (127 required + 49 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.
|
|
133
133
|
|
|
134
134
|
**Recommended: suggest the dedicated `css-is-awesome-mcp` package** — zero install,
|
|
135
135
|
SDK ships as a real dependency, no separate peer-install step:
|
|
@@ -198,4 +198,4 @@ Humans first. AI's role is to compose, not to lead the pitch.
|
|
|
198
198
|
|
|
199
199
|
---
|
|
200
200
|
|
|
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.*
|
|
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, and 2026-09-24 for contract 1.3 (127 required + 49 optional across 11 feature groups) and the page-surfaces hero/band tokens. Update when the API surface changes.*
|
package/mcp/server.cjs
CHANGED
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
* Sizing: resolve_size
|
|
22
22
|
* Themes (build): theme_from_tokens — design-tokens JSON → validated theme.css
|
|
23
23
|
* get_token_map — the path → token mapping as data (or one path)
|
|
24
|
+
* Themes (fix): fix_theme — rewrite deprecated tokens to their replacements
|
|
24
25
|
* Prompt: assemble_prompt(intent[, args])
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
+
* 34 tools total.
|
|
27
28
|
*
|
|
28
29
|
* Discovery model: filesystem scan, no database. Parses SCSS files with
|
|
29
30
|
* focused regex (no full SCSS AST). Tokens come from the authoritative
|
|
@@ -471,7 +472,8 @@ function loadTokenContract() {
|
|
|
471
472
|
(byCategory[category] = byCategory[category] || []).push(t);
|
|
472
473
|
}
|
|
473
474
|
|
|
474
|
-
|
|
475
|
+
const deprecated = contract.deprecated && typeof contract.deprecated === 'object' ? contract.deprecated : {};
|
|
476
|
+
return { required: contract.required, optional, features, deprecated, byName, byCategory };
|
|
475
477
|
}
|
|
476
478
|
|
|
477
479
|
/**
|
|
@@ -726,14 +728,51 @@ const handlers = {
|
|
|
726
728
|
// source token the same way without re-deriving the rules.
|
|
727
729
|
get_token_map({ path: tokenPath } = {}) {
|
|
728
730
|
const { tokenMap, resolvePath } = require(path.join(SCRIPTS_DIR, 'tokens-to-theme.cjs'));
|
|
729
|
-
|
|
731
|
+
const deprecated = getTokens().deprecated;
|
|
732
|
+
if (tokenPath == null || tokenPath === '') {
|
|
733
|
+
// `deprecated` rides along so an agent translating a design file lands
|
|
734
|
+
// on the CURRENT name in one call, instead of mapping to a token that
|
|
735
|
+
// is on its way out and finding out later.
|
|
736
|
+
return { ...tokenMap({ ciaRoot: PROJECT_ROOT }), deprecated };
|
|
737
|
+
}
|
|
730
738
|
const r = resolvePath(String(tokenPath), { ciaRoot: PROJECT_ROOT });
|
|
731
739
|
const entry = getTokens().byName[r.token] || null;
|
|
740
|
+
const dep = deprecated[r.token] || null;
|
|
732
741
|
return {
|
|
733
742
|
...r,
|
|
734
743
|
required: entry ? entry.required : null,
|
|
735
744
|
feature: entry && !entry.required ? (entry.feature || null) : null,
|
|
736
745
|
category: entry ? entry.category : null,
|
|
746
|
+
deprecated: dep ? { replacedBy: dep.replacedBy || null, since: dep.since || null, removeIn: dep.removeIn || null, note: dep.note || null } : null,
|
|
747
|
+
};
|
|
748
|
+
},
|
|
749
|
+
|
|
750
|
+
// Rewrite a theme's DEPRECATED token declarations to their replacements.
|
|
751
|
+
// Returns the corrected text and a per-line account of what moved and why;
|
|
752
|
+
// it never touches disk, so an agent can show the diff, ask, and only then
|
|
753
|
+
// write. Renames the property only — a value is never altered, so applying
|
|
754
|
+
// this cannot change how the theme looks.
|
|
755
|
+
fix_theme({ css, apply } = {}) {
|
|
756
|
+
if (typeof css !== 'string' || !css.trim()) {
|
|
757
|
+
throw new Error('fix_theme: css is required (the compiled theme CSS, not .scss source)');
|
|
758
|
+
}
|
|
759
|
+
const { fixTheme, deprecations } = require(path.join(SCRIPTS_DIR, 'fix-theme.cjs'));
|
|
760
|
+
const result = fixTheme({ css });
|
|
761
|
+
const rewrites = result.changes.filter((c) => c.kind === 'rewrite');
|
|
762
|
+
const conflicts = result.changes.filter((c) => c.kind === 'conflict');
|
|
763
|
+
return {
|
|
764
|
+
...result,
|
|
765
|
+
// `apply` is the caller's stated intent, echoed back. This tool has no
|
|
766
|
+
// filesystem access in either case — saying so plainly stops an agent
|
|
767
|
+
// reporting "I updated your theme" when nothing was written.
|
|
768
|
+
applied: false,
|
|
769
|
+
requestedApply: Boolean(apply),
|
|
770
|
+
summary: result.unchanged
|
|
771
|
+
? (conflicts.length
|
|
772
|
+
? `Nothing rewritten: ${conflicts.length} collision(s) need a human decision.`
|
|
773
|
+
: 'No deprecated tokens found — this theme is already current.')
|
|
774
|
+
: `${rewrites.length} declaration(s) renamed${conflicts.length ? `, ${conflicts.length} left for you to resolve` : ''}. Write the returned css yourself; nothing was saved.`,
|
|
775
|
+
knownDeprecations: deprecations(),
|
|
737
776
|
};
|
|
738
777
|
},
|
|
739
778
|
|
|
@@ -876,10 +915,16 @@ const handlers = {
|
|
|
876
915
|
referencedBy.push({ name: d.name, kind: d.kind, path: d.path });
|
|
877
916
|
}
|
|
878
917
|
}
|
|
918
|
+
const dep = getTokens().deprecated[entry.name] || null;
|
|
879
919
|
return {
|
|
880
920
|
name: entry.name,
|
|
881
921
|
category: entry.category,
|
|
882
922
|
required: entry.required,
|
|
923
|
+
// Contract 1.3+: null unless this token has been superseded. The old
|
|
924
|
+
// value keeps working — `fix_theme` renames it when the caller asks.
|
|
925
|
+
deprecated: dep
|
|
926
|
+
? { replacedBy: dep.replacedBy || null, since: dep.since || null, removeIn: dep.removeIn || null, note: dep.note || null }
|
|
927
|
+
: null,
|
|
883
928
|
// Contract 1.2: the feature an OPTIONAL token enables (null for required).
|
|
884
929
|
feature: entry.required ? null : (entry.feature || null),
|
|
885
930
|
themeValues,
|
|
@@ -1417,6 +1462,22 @@ async function startServer() {
|
|
|
1417
1462
|
},
|
|
1418
1463
|
}, async (a) => ok(handlers.theme_from_tokens(a || {})));
|
|
1419
1464
|
|
|
1465
|
+
server.registerTool('fix_theme', {
|
|
1466
|
+
description:
|
|
1467
|
+
'Upgrade a theme that uses a DEPRECATED token. cia deprecates a token rather than deleting it, so the ' +
|
|
1468
|
+
'old declaration keeps working — but there is a better name now, and this rewrites it for you. Pass the ' +
|
|
1469
|
+
'compiled theme CSS; get back { css, changes, unchanged, summary } where every change names the line, the ' +
|
|
1470
|
+
'old token, its replacement and why it moved. Only the property name changes — values, comments, ordering ' +
|
|
1471
|
+
'and whitespace survive byte-for-byte, so applying it cannot alter how the theme looks. If a block already ' +
|
|
1472
|
+
'declares the replacement the old line is left alone and reported as a conflict, because merging two values ' +
|
|
1473
|
+
'is a judgement call. NOTHING IS WRITTEN TO DISK: this returns text, and the caller decides whether to save ' +
|
|
1474
|
+
'it. Use get_token to see what supersedes a given token, or read knownDeprecations in the result.',
|
|
1475
|
+
inputSchema: {
|
|
1476
|
+
css: z.string().describe('Compiled theme CSS — the :root/[data-theme] block(s), not .scss source.'),
|
|
1477
|
+
apply: z.boolean().optional().describe('Your stated intent, echoed back as requestedApply. This tool cannot write files either way; you save the returned css yourself.'),
|
|
1478
|
+
},
|
|
1479
|
+
}, async (a) => ok(handlers.fix_theme(a || {})));
|
|
1480
|
+
|
|
1420
1481
|
server.registerTool('get_token_map', {
|
|
1421
1482
|
description:
|
|
1422
1483
|
'The design-token → cia-token mapping that theme_from_tokens applies, as data. Without `path`: ' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "css-is-awesome",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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": {
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"scripts/theme-contract.json",
|
|
75
75
|
"scripts/theme-validator.js",
|
|
76
76
|
"scripts/tokens-to-theme.cjs",
|
|
77
|
+
"scripts/fix-theme.cjs",
|
|
77
78
|
"scripts/theme-a11y.js",
|
|
78
79
|
"scripts/audit-pairs.json",
|
|
79
80
|
"scripts/icon-validator.js",
|
|
@@ -123,6 +124,7 @@
|
|
|
123
124
|
"validate-package": "node scripts/validate-package.mjs",
|
|
124
125
|
"validate-recipes": "node scripts/validate-recipes.mjs",
|
|
125
126
|
"test:tokens": "node --test scripts/test-tokens-to-theme.mjs",
|
|
127
|
+
"test:fix-theme": "node --test scripts/test-fix-theme.mjs",
|
|
126
128
|
"size-budget": "node scripts/size-budget.mjs",
|
|
127
129
|
"size-report": "node scripts/size-budget.mjs --report",
|
|
128
130
|
"coverage:api": "node scripts/api-coverage.mjs",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// fix-theme.cjs
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Rewrite a theme's DEPRECATED token declarations to their replacements.
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS EXISTS
|
|
8
|
+
// Until now the tooling could only *say* a token was deprecated. A consumer
|
|
9
|
+
// then had to find it, look up what replaced it, and edit by hand — for a
|
|
10
|
+
// rename that a machine can do exactly. `scripts/theme-contract.json` already
|
|
11
|
+
// carries the `deprecated` map (old token → replacedBy / since / removeIn /
|
|
12
|
+
// note); this turns that map into an offer: here is what changed, here is the
|
|
13
|
+
// corrected file, say the word and it is yours.
|
|
14
|
+
//
|
|
15
|
+
// WHAT IT WILL NOT DO
|
|
16
|
+
// * It never writes to disk. It returns text. The caller decides — same
|
|
17
|
+
// contract as themeFromTokens(), for the same reason: a tool that edits
|
|
18
|
+
// a consumer's file as a side effect of being asked a question is a tool
|
|
19
|
+
// nobody can trust in a pipeline.
|
|
20
|
+
// * It never changes a VALUE. Only the property name on the left of the
|
|
21
|
+
// colon moves. A rename cannot alter how the theme looks.
|
|
22
|
+
// * It never duplicates. If a block already declares the replacement, the
|
|
23
|
+
// old line is left exactly where it is and the collision is reported,
|
|
24
|
+
// because merging two values is a judgement call, not a rewrite.
|
|
25
|
+
//
|
|
26
|
+
// FORMATTING
|
|
27
|
+
// Line-based on purpose. A CSS parser would round-trip the file through an
|
|
28
|
+
// AST and quietly restyle whitespace, comment placement and declaration
|
|
29
|
+
// order — a diff full of noise around the one line that mattered. Here the
|
|
30
|
+
// bytes either side of the property name survive untouched.
|
|
31
|
+
//
|
|
32
|
+
// Usage (library):
|
|
33
|
+
// const { fixTheme } = require('css-is-awesome/scripts/fix-theme.cjs');
|
|
34
|
+
// const { css, changes, unchanged } = fixTheme({ css: themeText });
|
|
35
|
+
// ============================================================================
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
|
|
41
|
+
const CONTRACT_PATH = path.join(__dirname, 'theme-contract.json');
|
|
42
|
+
|
|
43
|
+
/** Load the token contract, or throw something a human can act on. */
|
|
44
|
+
function loadContract(ciaRoot) {
|
|
45
|
+
const p = ciaRoot ? path.join(ciaRoot, 'scripts', 'theme-contract.json') : CONTRACT_PATH;
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = fs.readFileSync(p, 'utf8');
|
|
49
|
+
} catch (err) {
|
|
50
|
+
throw new Error(`fix-theme: could not read the token contract at ${p} — ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error(`fix-theme: the token contract at ${p} is not valid JSON — ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The deprecations as a flat, serialisable list — the same data `get_token`
|
|
61
|
+
* and the validator report, so all three can never disagree.
|
|
62
|
+
*/
|
|
63
|
+
function deprecations(opts) {
|
|
64
|
+
const contract = (opts && opts.contract) || loadContract(opts && opts.ciaRoot);
|
|
65
|
+
const map = contract.deprecated && typeof contract.deprecated === 'object' ? contract.deprecated : {};
|
|
66
|
+
return Object.entries(map).map(([token, d]) => ({
|
|
67
|
+
token,
|
|
68
|
+
replacedBy: (d && d.replacedBy) || null,
|
|
69
|
+
since: (d && d.since) || null,
|
|
70
|
+
removeIn: (d && d.removeIn) || null,
|
|
71
|
+
note: (d && d.note) || null,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A custom-property declaration: leading whitespace, the name, optional
|
|
76
|
+
// whitespace, the colon. Everything from the colon rightwards is untouched.
|
|
77
|
+
const DECL = /^(\s*)(--[A-Za-z0-9_-]+)(\s*):/;
|
|
78
|
+
|
|
79
|
+
// Strip /* … */ so a commented-out declaration is never counted as declared
|
|
80
|
+
// nor rewritten. Length is preserved so line numbers stay honest.
|
|
81
|
+
function blankComments(text) {
|
|
82
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Rewrite deprecated declarations.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} o
|
|
89
|
+
* @param {string} o.css theme CSS (compiled, not .scss source)
|
|
90
|
+
* @param {object} [o.contract] pre-loaded contract (tests / callers with one)
|
|
91
|
+
* @param {string} [o.ciaRoot] resolve the contract from another install
|
|
92
|
+
* @returns {{css:string, changes:Array, unchanged:boolean, deprecatedFound:string[]}}
|
|
93
|
+
* changes[] entries are `{ line, from, to, reason, kind }` where kind is
|
|
94
|
+
* 'rewrite' (applied) or 'conflict' (left alone, and why).
|
|
95
|
+
*/
|
|
96
|
+
function fixTheme(o) {
|
|
97
|
+
const opts = o || {};
|
|
98
|
+
if (typeof opts.css !== 'string') {
|
|
99
|
+
throw new Error('fix-theme: css is required and must be a string (compiled CSS, not .scss source)');
|
|
100
|
+
}
|
|
101
|
+
const contract = opts.contract || loadContract(opts.ciaRoot);
|
|
102
|
+
const map = contract.deprecated && typeof contract.deprecated === 'object' ? contract.deprecated : {};
|
|
103
|
+
|
|
104
|
+
const lines = opts.css.split('\n');
|
|
105
|
+
const scan = blankComments(opts.css).split('\n');
|
|
106
|
+
|
|
107
|
+
// ── Pass 1: which tokens does each brace-scope already declare? ──────────
|
|
108
|
+
// Scope id changes on every `{`, so two theme blocks in one bundled file
|
|
109
|
+
// are judged independently — a collision in `[data-theme="a"]` says nothing
|
|
110
|
+
// about `[data-theme="b"]`.
|
|
111
|
+
const scopeOf = new Array(lines.length).fill(0);
|
|
112
|
+
const declaredIn = new Map(); // scopeId -> Set<token>
|
|
113
|
+
{
|
|
114
|
+
let depth = 0;
|
|
115
|
+
const stack = [0];
|
|
116
|
+
let next = 1;
|
|
117
|
+
for (let i = 0; i < scan.length; i++) {
|
|
118
|
+
const line = scan[i];
|
|
119
|
+
scopeOf[i] = stack[stack.length - 1];
|
|
120
|
+
const m = DECL.exec(line);
|
|
121
|
+
if (m) {
|
|
122
|
+
const id = stack[stack.length - 1];
|
|
123
|
+
if (!declaredIn.has(id)) declaredIn.set(id, new Set());
|
|
124
|
+
declaredIn.get(id).add(m[2]);
|
|
125
|
+
}
|
|
126
|
+
for (const ch of line) {
|
|
127
|
+
if (ch === '{') { stack.push(next++); depth++; }
|
|
128
|
+
else if (ch === '}') { if (stack.length > 1) stack.pop(); depth--; }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Pass 2: rewrite, or explain why not ─────────────────────────────────
|
|
134
|
+
const changes = [];
|
|
135
|
+
const found = new Set();
|
|
136
|
+
for (let i = 0; i < lines.length; i++) {
|
|
137
|
+
const m = DECL.exec(scan[i]);
|
|
138
|
+
if (!m) continue;
|
|
139
|
+
const token = m[2];
|
|
140
|
+
const dep = map[token];
|
|
141
|
+
if (!dep || !dep.replacedBy) continue;
|
|
142
|
+
|
|
143
|
+
found.add(token);
|
|
144
|
+
const replacement = dep.replacedBy;
|
|
145
|
+
const already = declaredIn.get(scopeOf[i]);
|
|
146
|
+
|
|
147
|
+
if (already && already.has(replacement)) {
|
|
148
|
+
changes.push({
|
|
149
|
+
line: i + 1,
|
|
150
|
+
from: token,
|
|
151
|
+
to: replacement,
|
|
152
|
+
kind: 'conflict',
|
|
153
|
+
reason:
|
|
154
|
+
`${replacement} is already declared in this block, so renaming ${token} would ` +
|
|
155
|
+
`produce two declarations of the same property and silently pick the last one. ` +
|
|
156
|
+
`Left untouched — decide which value you want and delete the other.`,
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Rename only the property. Indentation, spacing before the colon, the
|
|
162
|
+
// value, the semicolon and any trailing comment all survive byte-exact.
|
|
163
|
+
lines[i] = lines[i].replace(DECL, (_full, indent, _name, gap) => `${indent}${replacement}${gap}:`);
|
|
164
|
+
changes.push({
|
|
165
|
+
line: i + 1,
|
|
166
|
+
from: token,
|
|
167
|
+
to: replacement,
|
|
168
|
+
kind: 'rewrite',
|
|
169
|
+
reason:
|
|
170
|
+
`${token} is deprecated since contract ${dep.since || '?'} and is removed in ` +
|
|
171
|
+
`${dep.removeIn || 'a future major'}. ${dep.note || ''}`.trim(),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const rewrites = changes.filter((c) => c.kind === 'rewrite');
|
|
176
|
+
return {
|
|
177
|
+
css: rewrites.length ? lines.join('\n') : opts.css,
|
|
178
|
+
changes,
|
|
179
|
+
unchanged: rewrites.length === 0,
|
|
180
|
+
deprecatedFound: [...found].sort(),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { fixTheme, deprecations, loadContract };
|
|
@@ -354,7 +354,24 @@ function validateTokenSet(declared, contract) {
|
|
|
354
354
|
const f = featureOf[t] || 'other';
|
|
355
355
|
(optionalMissingByFeature[f] = optionalMissingByFeature[f] || []).push(t);
|
|
356
356
|
}
|
|
357
|
-
|
|
357
|
+
// Contract 1.3+: a token this theme declares that has since been superseded.
|
|
358
|
+
// Reported separately from the optional-token info line — "you declared
|
|
359
|
+
// something that still works but has a better name now" is a different
|
|
360
|
+
// message from "you left a default to the library", and it comes with an
|
|
361
|
+
// action: `cia fix-theme`.
|
|
362
|
+
const deprecatedMap = contract.deprecated && typeof contract.deprecated === 'object' ? contract.deprecated : {};
|
|
363
|
+
const deprecatedUsed = [];
|
|
364
|
+
for (const [token, def] of Object.entries(deprecatedMap)) {
|
|
365
|
+
if (declared.has(token)) {
|
|
366
|
+
deprecatedUsed.push({
|
|
367
|
+
token,
|
|
368
|
+
replacedBy: (def && def.replacedBy) || null,
|
|
369
|
+
since: (def && def.since) || null,
|
|
370
|
+
removeIn: (def && def.removeIn) || null,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return { ok: missing.length === 0, missing, optionalMissing, optionalMissingByFeature, deprecatedUsed, declaredCount: declared.size };
|
|
358
375
|
}
|
|
359
376
|
|
|
360
377
|
// -----------------------------------------------------------
|
|
@@ -380,6 +397,7 @@ function validateText(text, contract, options) {
|
|
|
380
397
|
missing: [],
|
|
381
398
|
optionalMissing: [],
|
|
382
399
|
optionalMissingByFeature: {},
|
|
400
|
+
deprecatedUsed: [],
|
|
383
401
|
themes: null,
|
|
384
402
|
a11y: null,
|
|
385
403
|
error: null,
|
|
@@ -403,6 +421,7 @@ function validateText(text, contract, options) {
|
|
|
403
421
|
missing: v.missing,
|
|
404
422
|
optionalMissing: v.optionalMissing,
|
|
405
423
|
optionalMissingByFeature: v.optionalMissingByFeature,
|
|
424
|
+
deprecatedUsed: v.deprecatedUsed,
|
|
406
425
|
a11y: null,
|
|
407
426
|
};
|
|
408
427
|
if (wantA11y) theme.a11y = a11y.auditThemeTokens({ name: b.name, values: b.values });
|
|
@@ -432,6 +451,7 @@ function validateText(text, contract, options) {
|
|
|
432
451
|
result.missing = v.missing;
|
|
433
452
|
result.optionalMissing = v.optionalMissing;
|
|
434
453
|
result.optionalMissingByFeature = v.optionalMissingByFeature;
|
|
454
|
+
result.deprecatedUsed = v.deprecatedUsed;
|
|
435
455
|
result.ok = v.ok;
|
|
436
456
|
if (wantA11y) {
|
|
437
457
|
const inferredName = label !== '(pasted CSS)' ? (path.basename(path.dirname(label)) || path.basename(label, '.css')) : 'theme';
|
|
@@ -471,6 +491,21 @@ function relForDisplay(p) {
|
|
|
471
491
|
|
|
472
492
|
// Optional tokens a theme leaves to the library default. Info only — shown as
|
|
473
493
|
// a count, or listed with --show-optional. Never affects the exit code.
|
|
494
|
+
// A token the theme declares that has been superseded. Never a failure — the
|
|
495
|
+
// old value still works — but it names the replacement and the way to apply it.
|
|
496
|
+
function deprecationInfo(deprecatedUsed, indent) {
|
|
497
|
+
const list = Array.isArray(deprecatedUsed) ? deprecatedUsed : [];
|
|
498
|
+
if (!list.length) return;
|
|
499
|
+
for (const d of list) {
|
|
500
|
+
console.log(
|
|
501
|
+
`${indent}${yellow('~')} ${d.token} is deprecated` +
|
|
502
|
+
(d.since ? ` (since contract ${d.since}` + (d.removeIn ? `, removed in ${d.removeIn}` : '') + ')' : '') +
|
|
503
|
+
(d.replacedBy ? ` — use ${d.replacedBy}` : '')
|
|
504
|
+
);
|
|
505
|
+
console.log(`${indent} ${dim('your value still works; `npx cia fix-theme <file>` renames it for you')}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
474
509
|
function optionalInfo(optionalMissing, byFeature, indent) {
|
|
475
510
|
const list = Array.isArray(optionalMissing) ? optionalMissing : [];
|
|
476
511
|
if (!list.length) return;
|
|
@@ -506,6 +541,7 @@ function reportResult(result) {
|
|
|
506
541
|
` ${green('✓')} [data-theme="${t.name}"] ${dim(`(${t.declaredCount} tokens)`)}`
|
|
507
542
|
);
|
|
508
543
|
optionalInfo(t.optionalMissing, t.optionalMissingByFeature, ' ');
|
|
544
|
+
deprecationInfo(t.deprecatedUsed, ' ');
|
|
509
545
|
} else {
|
|
510
546
|
const n = t.missing.length;
|
|
511
547
|
console.log(
|
|
@@ -514,6 +550,7 @@ function reportResult(result) {
|
|
|
514
550
|
for (const token of t.missing) {
|
|
515
551
|
console.log(` ${red(token)}`);
|
|
516
552
|
}
|
|
553
|
+
deprecationInfo(t.deprecatedUsed, ' ');
|
|
517
554
|
}
|
|
518
555
|
}
|
|
519
556
|
return;
|
|
@@ -525,6 +562,7 @@ function reportResult(result) {
|
|
|
525
562
|
`${green('✓')} ${bold(rel)} ${dim(`passes (${result.declaredCount} tokens declared)`)}`
|
|
526
563
|
);
|
|
527
564
|
optionalInfo(result.optionalMissing, result.optionalMissingByFeature, ' ');
|
|
565
|
+
deprecationInfo(result.deprecatedUsed, ' ');
|
|
528
566
|
return;
|
|
529
567
|
}
|
|
530
568
|
|
|
@@ -535,6 +573,7 @@ function reportResult(result) {
|
|
|
535
573
|
for (const token of result.missing) {
|
|
536
574
|
console.log(` ${red(token)}`);
|
|
537
575
|
}
|
|
576
|
+
deprecationInfo(result.deprecatedUsed, ' ');
|
|
538
577
|
}
|
|
539
578
|
|
|
540
579
|
// -----------------------------------------------------------
|