css-is-awesome 1.11.1 → 1.12.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CONTRACT.md +15 -0
  3. package/README.md +9 -8
  4. package/THEMING.md +1 -0
  5. package/bin/analyze.cjs +88 -1
  6. package/bin/cia.cjs +30 -5
  7. package/bin/migrate-chakra.cjs +249 -0
  8. package/bin/migrate-mui.cjs +363 -0
  9. package/bin/migrate-tailwind.cjs +3 -1
  10. package/dist/css-is-awesome.css +516 -0
  11. package/dist/css-is-awesome.min.css +1 -1
  12. package/dist/css-is-awesome.utilities.css +516 -0
  13. package/dist/css-is-awesome.utilities.min.css +1 -1
  14. package/dist/tokens.d.ts +4 -2
  15. package/package.json +7 -3
  16. package/public/theme.css +293 -237
  17. package/public/themes/boilerplate/theme.css +11 -10
  18. package/public/themes/boilerplate-dark/theme.css +10 -9
  19. package/public/themes/boilerplate-light/theme.css +10 -9
  20. package/public/themes/cupertino/theme.css +11 -10
  21. package/public/themes/cupertino-dark/theme.css +11 -10
  22. package/public/themes/cupertino-light/theme.css +11 -10
  23. package/public/themes/glass/theme.css +11 -10
  24. package/public/themes/glass-dark/theme.css +11 -10
  25. package/public/themes/glass-light/theme.css +11 -10
  26. package/public/themes/graphite/theme.css +11 -10
  27. package/public/themes/graphite-dark/theme.css +11 -10
  28. package/public/themes/graphite-light/theme.css +11 -10
  29. package/public/themes/press/theme.css +11 -10
  30. package/public/themes/press-dark/theme.css +11 -10
  31. package/public/themes/press-light/theme.css +11 -10
  32. package/public/themes/prism/theme.css +11 -10
  33. package/public/themes/prism-dark/theme.css +11 -10
  34. package/public/themes/prism-light/theme.css +11 -10
  35. package/public/themes/sketchbook/theme.css +11 -10
  36. package/public/themes/sketchbook-dark/theme.css +11 -10
  37. package/public/themes/sketchbook-light/theme.css +10 -9
  38. package/public/themes/terminal/theme.css +11 -10
  39. package/public/themes/terminal-dark/theme.css +11 -10
  40. package/public/themes/terminal-light/theme.css +11 -10
  41. package/scripts/theme-a11y.js +11 -31
  42. package/scripts/theme-contract.json +1 -0
  43. package/scss/_spacing-scale.scss +25 -0
  44. package/scss/_utilities.scss +44 -9
  45. package/scss/components/_accordion.scss +1 -1
  46. package/scss/components/_data.scss +2 -2
  47. package/scss/components/_feedback.scss +1 -1
  48. package/scss/components/_forms.scss +16 -2
  49. package/scss/components/_navigation.scss +1 -1
  50. package/scss/examples/_usage.scss +3 -3
  51. package/scss/recipes/admin-dashboard-layout.md +277 -0
  52. package/scss/recipes/app-shell.md +339 -0
  53. package/scss/recipes/auth-flow.md +345 -0
  54. package/scss/recipes/confirm-dialog.md +248 -0
  55. package/scss/recipes/data-table.md +382 -0
  56. package/scss/recipes/datepicker.md +428 -0
  57. package/scss/recipes/form-validation-async.md +340 -0
  58. package/scss/recipes/form-validation-html5.md +306 -0
  59. package/scss/recipes/form-validation-react-hook-form.md +224 -0
  60. package/scss/recipes/form-validation-success-states.md +290 -0
  61. package/scss/recipes/form-validation-zod.md +270 -0
  62. package/scss/recipes/i18n-date-formatting.md +172 -0
  63. package/scss/recipes/i18n-number-currency.md +145 -0
  64. package/scss/recipes/i18n-pluralization.md +163 -0
  65. package/scss/recipes/multi-step-wizard.md +365 -0
  66. package/scss/recipes/otp-input.md +310 -0
  67. package/scss/recipes/rtl-layout.md +273 -0
  68. package/scss/themes/boilerplate-dark.scss +2 -10
  69. package/scss/themes/boilerplate-light.scss +2 -10
  70. package/scss/themes/boilerplate.scss +2 -10
  71. package/scss/themes/cupertino-dark.scss +2 -10
  72. package/scss/themes/cupertino-light.scss +2 -10
  73. package/scss/themes/cupertino.scss +2 -10
  74. package/scss/themes/glass-dark.scss +2 -10
  75. package/scss/themes/glass-light.scss +2 -10
  76. package/scss/themes/glass.scss +2 -10
  77. package/scss/themes/graphite-dark.scss +2 -10
  78. package/scss/themes/graphite-light.scss +2 -10
  79. package/scss/themes/graphite.scss +2 -10
  80. package/scss/themes/press-dark.scss +2 -10
  81. package/scss/themes/press-light.scss +2 -10
  82. package/scss/themes/press.scss +2 -10
  83. package/scss/themes/prism-dark.scss +2 -10
  84. package/scss/themes/prism-light.scss +2 -10
  85. package/scss/themes/prism.scss +2 -10
  86. package/scss/themes/sketchbook-dark.scss +2 -10
  87. package/scss/themes/sketchbook-light.scss +2 -10
  88. package/scss/themes/sketchbook.scss +2 -10
  89. package/scss/themes/terminal-dark.scss +2 -10
  90. package/scss/themes/terminal-light.scss +2 -10
  91. package/scss/themes/terminal.scss +2 -10
package/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ # [1.12.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.11.1...v1.12.0) (2026-09-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **a11y:** unlabeled checklist checkboxes and non-focusable code blocks ([d98a92c](https://github.com/Jerry2d3d/css-is-awesome/commit/d98a92c549a2b74c3cff976d0fdfd61031162585))
7
+ * **ci:** raise utilities size budget to 5.0 KB, correct stale bundle-size docs ([0e447f1](https://github.com/Jerry2d3d/css-is-awesome/commit/0e447f16e33d5a39cbcedb3b44caab79e4920f23))
8
+ * **theme-editor:** density slider + fix override specificity bug affecting every row ([3977043](https://github.com/Jerry2d3d/css-is-awesome/commit/39770434e5f1819b57c16e502de15e252c5cf0d5))
9
+ * **theme:** recurse into at-rules when bundling themes ([ca6397b](https://github.com/Jerry2d3d/css-is-awesome/commit/ca6397bcdcbcfc6bcb784253855b0af74d94ae74))
10
+
11
+
12
+ ### Features
13
+
14
+ * **cli:** npx cia migrate chakra — Chakra UI theme object -> cia theme.scss ([2bcc95c](https://github.com/Jerry2d3d/css-is-awesome/commit/2bcc95c32058b2b1f60378e6718b772363fd77a2))
15
+ * **cli:** npx cia migrate mui — MUI theme object -> cia theme.scss ([4f1d682](https://github.com/Jerry2d3d/css-is-awesome/commit/4f1d6828fc13fa1929f78127ab7c94503f55afb0))
16
+ * **cli:** off-scale-length and missing-focus-visible analyzer rules ([d7bb8d9](https://github.com/Jerry2d3d/css-is-awesome/commit/d7bb8d96598b9897f130a152ad024ccc13a0cbbb))
17
+ * **editor:** print preview persists per theme and seeds from overrides ([404e494](https://github.com/Jerry2d3d/css-is-awesome/commit/404e494051b9d3893c8ab18762da8044a389f5fd))
18
+ * **editor:** token-consumer map, used-by list, live contrast readout ([a52f74e](https://github.com/Jerry2d3d/css-is-awesome/commit/a52f74e968f340d1c9f6b2ed8a02b8f16457432b))
19
+ * **recipes:** admin-dashboard-layout recipe + live demo ([6e79812](https://github.com/Jerry2d3d/css-is-awesome/commit/6e79812980abbd90391507bd06db06b65820c1e2))
20
+ * **recipes:** app-shell recipe - navbar + control panel + footer + modal ([a5abd6e](https://github.com/Jerry2d3d/css-is-awesome/commit/a5abd6e68287a1d99978e8956d601c170cbff657))
21
+ * **recipes:** auth-flow recipe + live demo ([7e566e2](https://github.com/Jerry2d3d/css-is-awesome/commit/7e566e274fd84be8eab0ae877e3c222a1adc8c1e))
22
+ * **recipes:** confirm-dialog recipe + live demo ([24977c2](https://github.com/Jerry2d3d/css-is-awesome/commit/24977c2214efc5588d90c601da70c2db1ed3b1f5))
23
+ * **recipes:** data-table recipe + live demo ([cbcdbb3](https://github.com/Jerry2d3d/css-is-awesome/commit/cbcdbb384f2d44f25071d593391d0cb4a6bd5d0d))
24
+ * **recipes:** datepicker recipe + live demo ([90ea515](https://github.com/Jerry2d3d/css-is-awesome/commit/90ea515f8c2ab044a4957be642295d188cf60cb3))
25
+ * **recipes:** form validation recipes + live demos ([78ae02d](https://github.com/Jerry2d3d/css-is-awesome/commit/78ae02d4c29458d60feb90f0135faaedd7684b26))
26
+ * **recipes:** i18n-date-formatting recipe + live demo ([689152a](https://github.com/Jerry2d3d/css-is-awesome/commit/689152a4ebb55b5c19285bfcd6f4c81f1a090b1d))
27
+ * **recipes:** i18n-number-currency recipe + live demo ([a4ec9a3](https://github.com/Jerry2d3d/css-is-awesome/commit/a4ec9a35b80d9a4d6fb8f5333107694ef9e5ccf3))
28
+ * **recipes:** i18n-pluralization recipe + live demo ([fe6a3f0](https://github.com/Jerry2d3d/css-is-awesome/commit/fe6a3f0272de3d848b5306800e3ecbca84d09e1c))
29
+ * **recipes:** multi-step-wizard recipe + live demo ([932358e](https://github.com/Jerry2d3d/css-is-awesome/commit/932358e54d0550ecf7017d48266f833646634d58))
30
+ * **recipes:** otp-input recipe + live demo ([af99b86](https://github.com/Jerry2d3d/css-is-awesome/commit/af99b86ab08f8357e908bd19533ea3bd55eab548))
31
+ * **rtl:** audit + fix non-logical CSS, add a logical utility set ([35704fd](https://github.com/Jerry2d3d/css-is-awesome/commit/35704fda8941c56eea16070813845d15295f897f))
32
+ * **rtl:** recipe + /docs/rtl live demo ([b18bc04](https://github.com/Jerry2d3d/css-is-awesome/commit/b18bc04910b9db02c0091ccaa4e93ab551d13326))
33
+ * **themes:** --space-unit density knob — one var drives all 9 spacing steps ([a1638c7](https://github.com/Jerry2d3d/css-is-awesome/commit/a1638c72338309055f752e6940220a9a8cd6be99))
34
+
1
35
  ## [1.11.1](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.11.0...v1.11.1) (2026-09-09)
2
36
 
3
37
 
package/CONTRACT.md CHANGED
@@ -305,6 +305,21 @@ Paper themes declare these as `none` / `transparent` so a swap to a glass or pho
305
305
 
306
306
  ---
307
307
 
308
+ ## Print (optional)
309
+
310
+ 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.
311
+
312
+ | Token | Type | Default | Purpose |
313
+ | --------------- | ------ | --------- | ------------------------------------- |
314
+ | `--print-ink` | color | `#000` | Body text, links, code text on paper |
315
+ | `--print-paper` | color | `#fff` | Backgrounds on paper |
316
+ | `--print-line` | color | `#999` | Borders, rules, hairlines on paper |
317
+ | `--print-muted` | color | `#666` | Printed URLs, captions, secondary text |
318
+
319
+ `print-base` also rebinds the theme's own colour tokens (`--ink`, `--surface-*`, `--border-*`, `--code-*`) onto this palette inside `@media print`, so every theme — dark-only ones included — prints legible ink-on-paper with no per-theme work required. See [`/docs/print`](https://cssisawesome.com/docs/print) and the [`letterhead` recipe](./scss/recipes/letterhead.md).
320
+
321
+ ---
322
+
308
323
  ## Component overrides (optional)
309
324
 
310
325
  These are per-component tokens a theme MAY override to change how a single family of components renders (buttons, cards, inputs, etc.) without touching the library or rebuilding SCSS. They are **optional** — the library emits every one of them on `:root` with a sensible default, and every component mixin reads them via `var(--<key>, <library-default>)`. A theme that sets none of them renders exactly the same as today.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  **Docs:** [cssisawesome.com](https://cssisawesome.com/) · **Install:** `npm install css-is-awesome`
10
10
 
11
- > **The recipes book:** build any component in any framework using cia mixins — seven recipes today (`dialog`, `combobox`, `print-to-pdf`, `print-spec`, `letterhead`, `mobile-nav`, `bottom-nav`), with `datepicker`, `data-table` and `command-palette` queued. AI agents read recipes via MCP and generate components in your stack; humans read them at [`/docs/recipes`](https://cssisawesome.com/docs/recipes/).
11
+ > **The recipes book:** build any component in any framework using cia mixins — 16 recipes today, including `dialog`, `combobox`, `datepicker`, `data-table`, `app-shell`, five form-validation patterns (HTML5, react-hook-form, Zod, async, success-states), `rtl-layout`, `print-to-pdf`, `print-spec`, `letterhead`, `mobile-nav` and `bottom-nav` — with `command-palette` and five more (`confirm-dialog`, `auth-flow`, `multi-step-wizard`, `otp-input`, `admin-dashboard-layout`) queued. AI agents read recipes via MCP and generate components in your stack; humans read them at [`/docs/recipes`](https://cssisawesome.com/docs/recipes/).
12
12
 
13
13
  ## For AI agents — start here
14
14
 
@@ -228,10 +228,11 @@ npx cia add --list # browse the recipe book
228
228
  npx cia add bottom-nav # copy a recipe into your project — you own the pattern
229
229
  npx cia analyze src/styles # design-system health: dead cia.* symbols, the
230
230
  # space() scale trap, off-contract tokens (typos),
231
- # hard-coded colors, BEM creep
231
+ # off-scale lengths, hard-coded colors, BEM creep,
232
+ # missing focus-visible styling
232
233
  ```
233
234
 
234
- `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. The default output is a **graded report** — a health score, a section per concern (Contract / Spacing / Color / Naming / Layout / API) 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.
235
+ `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/).
235
236
 
236
237
  ## Print / PDF (zero JS)
237
238
 
@@ -387,11 +388,11 @@ Full detail: [`/docs/testing`](https://cssisawesome.com/docs/testing/).
387
388
 
388
389
  | Bundle | Size | Use case |
389
390
  |---|---|---|
390
- | `dist/tokens.css` | 2.2 KB | Tokens only (`:where(:root)` CSS variables, no rules) — the purest mixin-first emit |
391
- | `dist/css-is-awesome.core.min.css` | 2.4 KB | Tokens + resets, no utilities or components |
392
- | `dist/css-is-awesome.utilities.min.css` | 4.1 KB | Every `cia-*` utility class, nothing else |
393
- | `dist/css-is-awesome.min.css` | 7.3 KB | Full bundle (everything) |
394
- | Per-theme `themes/<name>/theme.css` | 1.5–3.4 KB | One file per theme, both modes via `light-dark()`, drop-in with no markup change |
391
+ | `dist/tokens.css` | 2.25 KB | Tokens only (`:where(:root)` CSS variables, no rules) — the purest mixin-first emit |
392
+ | `dist/css-is-awesome.core.min.css` | 2.38 KB | Tokens + resets, no utilities or components |
393
+ | `dist/css-is-awesome.utilities.min.css` | 4.75 KB | Every `cia-*` utility class, nothing else |
394
+ | `dist/css-is-awesome.min.css` | 7.96 KB | Full bundle (everything) |
395
+ | 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 |
395
396
  | **JavaScript shipped in package** | **0 KB** | Zero. Period. JS-driven features ship as separate add-on packages. |
396
397
 
397
398
  ## Status
package/THEMING.md CHANGED
@@ -163,6 +163,7 @@ those names were removed because nothing read them.)
163
163
  | `--z-*` | `hide`, `base`, `dropdown`, `sticky`, `fixed`, `backdrop`, `modal`, `popover`, `tooltip`, `toast` |
164
164
  | `--duration-*` | `instant`, `fast`, `normal`, `slow`, `slower` |
165
165
  | `--opacity-*` | `0` through `100` |
166
+ | `--print-*` | `ink`, `paper`, `line`, `muted` — optional, override in your own `@media print` block to restyle paper (defaults to ink-on-white; see [`CONTRACT.md`](./CONTRACT.md#print-optional)) |
166
167
 
167
168
  ---
168
169
 
package/bin/analyze.cjs CHANGED
@@ -54,6 +54,10 @@ Usage:
54
54
  Scans [path] (default: current directory) for *.scss files and audits
55
55
  them against the installed css-is-awesome API.
56
56
 
57
+ Rules: unknown-symbol, space-scale, off-contract-token, off-scale-length,
58
+ hard-coded-color, bem, hand-written-areas, missing-focus-visible.
59
+ Full reference: https://cssisawesome.com/docs/analyzer
60
+
57
61
  Options:
58
62
  --namespace <ns> Extra namespace(s) to treat as cia (comma-separated).
59
63
  Auto-detected per file from @use lines; use this when
@@ -173,6 +177,85 @@ function stripPrintBlocks(src) {
173
177
  return out;
174
178
  }
175
179
 
180
+ // Reference-scale hints for the off-scale-length rule. Sourced from Sketchbook
181
+ // (public/themes/sketchbook/theme.css) — CONTRACT.md's own designated
182
+ // "reference implementation." These are HINTS, not a value guarantee: real
183
+ // themes intentionally diverge (Terminal flattens every --radius-* to 0), so
184
+ // a suggestion names the nearest named STEP, never asserts the consumer's
185
+ // active theme actually holds this exact pixel value.
186
+ const SCALE_HINTS = [
187
+ { token: '--radius-sm', px: 2 },
188
+ { token: '--radius-md', px: 3 },
189
+ { token: '--radius-lg', px: 6 },
190
+ { token: '--radius-xl', px: 12 },
191
+ { token: '--radius-full', px: 9999 },
192
+ { token: '--space-1', px: 8 },
193
+ { token: '--space-2', px: 12 },
194
+ { token: '--space-3', px: 14 },
195
+ { token: '--space-4', px: 16 },
196
+ { token: '--space-5', px: 24 },
197
+ { token: '--space-6', px: 32 },
198
+ { token: '--space-7', px: 48 },
199
+ { token: '--space-8', px: 64 },
200
+ { token: '--space-9', px: 96 },
201
+ ];
202
+ const LENGTH_PROP_RE = /(?:^|[{;\s])(border-radius|padding|margin|gap)\s*:\s*([^;{}]+);/g;
203
+
204
+ // "7px" / "0.75rem" -> px, or null if not a plain single-value length
205
+ // (percentages, calc(), keywords like "auto" are left alone — out of scope).
206
+ function parseLengthPx(raw) {
207
+ const m = /^(-?\d*\.?\d+)(px|rem)$/.exec(raw.trim());
208
+ if (!m) return null;
209
+ return m[2] === 'rem' ? parseFloat(m[1]) * 16 : parseFloat(m[1]);
210
+ }
211
+
212
+ // off-scale-length: a literal border-radius/padding/margin/gap value that's
213
+ // close to a scale step — almost certainly meant to be that token. Values
214
+ // already routed through var() (with or without a literal fallback) were
215
+ // stripped by the caller, so only genuine literals reach here. `0` is never
216
+ // flagged: it's unambiguous and a legitimate theme choice in its own right
217
+ // (Terminal sets every --radius-* to 0) — flagging it would be a guaranteed
218
+ // false positive for exactly that theme's consumers.
219
+ function offScaleLength(varStrippedSrc) {
220
+ const findings = [];
221
+ for (const m of varStrippedSrc.matchAll(LENGTH_PROP_RE)) {
222
+ const prop = m[1];
223
+ for (const tok of m[2].trim().split(/\s+/)) {
224
+ if (tok === '0' || tok === '0px' || tok === '0rem') continue;
225
+ const px = parseLengthPx(tok);
226
+ if (px == null || px === 0) continue;
227
+ let best = null;
228
+ let bestDiff = Infinity;
229
+ for (const hint of SCALE_HINTS) {
230
+ const diff = Math.abs(hint.px - px);
231
+ if (diff < bestDiff) { bestDiff = diff; best = hint; }
232
+ }
233
+ if (best && bestDiff > 0 && bestDiff <= 2) {
234
+ findings.push({ level: 'warn', rule: 'off-scale-length', detail: `${prop}: ${tok} — close to the reference scale's ${best.token}; consider the token instead of a literal (exact px varies by theme)` });
235
+ }
236
+ }
237
+ }
238
+ return findings;
239
+ }
240
+
241
+ // missing-focus-visible: a file styles :hover/:active on something
242
+ // button/link-shaped but never mentions :focus-visible or focus-ring
243
+ // anywhere in the same file — keyboard users likely get no visible
244
+ // feedback. File-level co-occurrence, not selector-pairing: SCSS commonly
245
+ // nests hover/active under `&` while focus-ring is set once for the whole
246
+ // component elsewhere in the file (see scss/components/_buttons.scss),
247
+ // so per-selector adjacency would false-positive on that exact shape.
248
+ const INTERACTIVE_SHAPE_RE = /(?:^|[^\w-])(?:button|a)(?:[.:#[\s{,]|$)|\[role\s*=\s*["']?button["']?\]|\.[\w-]*btn[\w-]*/i;
249
+ const HOVER_ACTIVE_RE = /:(?:hover|active)\b/;
250
+ const FOCUS_VISIBLE_RE = /:focus-visible\b|focus-ring/i;
251
+
252
+ function missingFocusVisible(strippedSrc) {
253
+ if (!HOVER_ACTIVE_RE.test(strippedSrc)) return [];
254
+ if (!INTERACTIVE_SHAPE_RE.test(strippedSrc)) return [];
255
+ if (FOCUS_VISIBLE_RE.test(strippedSrc)) return [];
256
+ 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
+ }
258
+
176
259
  function analyzeFile(file, symbols, contractTokens, extraNs) {
177
260
  const raw = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
178
261
  const src = stripComments(raw);
@@ -222,6 +305,8 @@ function analyzeFile(file, symbols, contractTokens, extraNs) {
222
305
  for (const m of stripVarExpr(stripPrintBlocks(src)).matchAll(/#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g)) {
223
306
  findings.push({ level: 'warn', rule: 'hard-coded-color', detail: `${m[0]} — values should come from tokens (cia.color(...) / var(--...))` });
224
307
  }
308
+ findings.push(...offScaleLength(stripVarExpr(src)));
309
+ findings.push(...missingFocusVisible(src));
225
310
  for (const m of src.matchAll(/\.[a-zA-Z][\w]*(?:__|--)[\w-]+/g)) {
226
311
  findings.push({ level: 'warn', rule: 'bem', detail: `${m[0]} — BEM chains are forbidden; use semantic single-class names` });
227
312
  }
@@ -240,13 +325,15 @@ const RULE_CATEGORY = {
240
325
  'unknown-symbol': 'API',
241
326
  'space-scale': 'Spacing',
242
327
  'off-contract-token': 'Contract',
328
+ 'off-scale-length': 'Spacing',
243
329
  'hard-coded-color': 'Color',
244
330
  bem: 'Naming',
245
331
  'hand-written-areas': 'Layout',
332
+ 'missing-focus-visible': 'Accessibility',
246
333
  };
247
334
  // The categories with rules implemented today — shown ✓ when clean so a passing
248
335
  // audit reads as coverage, not silence.
249
- const IMPLEMENTED_CATEGORIES = ['API', 'Contract', 'Spacing', 'Color', 'Naming', 'Layout'];
336
+ const IMPLEMENTED_CATEGORIES = ['API', 'Contract', 'Spacing', 'Color', 'Naming', 'Layout', 'Accessibility'];
250
337
 
251
338
  // Details are authored as "claim — suggested fix"; split so the graded report
252
339
  // can put the fix on its own `→` line.
package/bin/cia.cjs CHANGED
@@ -5,9 +5,12 @@
5
5
  * Subcommand router. Each subcommand lives in its own file under bin/ and
6
6
  * exposes a `run(args)` async function.
7
7
  *
8
- * Status: both converters shipped (EPIC-03 migration on-ramp, 6/6).
8
+ * Status: 4 converters shipped (v1.0 EPIC-03 tailwind+bootstrap; v1.2
9
+ * EPIC-05 mui+chakra).
9
10
  * migrate tailwind — parse tailwind.config.* + dump theme JSON
10
11
  * migrate bootstrap — parse Bootstrap SCSS/CSS vars + dump theme JSON
12
+ * migrate mui — parse a MUI createTheme() result + dump theme JSON
13
+ * migrate chakra — parse a Chakra extendTheme() result + dump theme JSON
11
14
  *
12
15
  * cia core ships ZERO JavaScript in the `files` manifest. The CLI lives in
13
16
  * `bin/` which is explicitly allowed per the architecture lock — same path
@@ -27,7 +30,7 @@ Usage:
27
30
 
28
31
  Commands:
29
32
  migrate <tool> [path] Convert another design system's config to a cia
30
- theme. Tools: tailwind | bootstrap.
33
+ theme. Tools: tailwind | bootstrap | mui | chakra.
31
34
  add <recipe> Copy a recipe from the book into your project
32
35
  (own the pattern). \`cia add --list\` to browse.
33
36
  analyze [path] Design-system health check: dead cia.* symbols,
@@ -58,7 +61,18 @@ Tools:
58
61
  via Bootstrap convention.
59
62
  Run \`cia migrate bootstrap --help\` for full options.
60
63
 
61
- Common options (both tools):
64
+ mui Read a MUI (Material UI v5+) theme module (createTheme({...})
65
+ result) and write a cia theme.scss. Maps palette.primary/
66
+ secondary/error/warning/info/success, text, background,
67
+ spacing, shape.borderRadius, typography.fontFamily.
68
+ Run \`cia migrate mui --help\` for full options.
69
+
70
+ chakra Read a Chakra UI (v2) theme module (extendTheme({...})
71
+ result) and write a cia theme.scss. Maps colors, space,
72
+ radii, fonts, fontSizes.
73
+ Run \`cia migrate chakra --help\` for full options.
74
+
75
+ Common options (all tools):
62
76
  --name <name> Theme name. Default: migrated
63
77
  --out <path> Custom output path. Default: ./cia-themes/<name>.scss
64
78
  --json Skip the file write and dump JSON to stdout (pipe-safe).
@@ -66,7 +80,8 @@ Common options (both tools):
66
80
  Examples:
67
81
  cia migrate tailwind ./tailwind.config.js
68
82
  cia migrate bootstrap ./scss/_variables.scss --name acme
69
- cia migrate bootstrap ./scss/_variables.scss --json | jq '.cia.report'
83
+ cia migrate mui ./src/theme.ts --name acme
84
+ cia migrate chakra ./src/theme.ts --json | jq '.cia.report'
70
85
  `;
71
86
 
72
87
  function fail(message, exit = 1) {
@@ -109,7 +124,17 @@ async function main() {
109
124
  await run(migrateArgs);
110
125
  return;
111
126
  }
112
- fail(`unknown migrate tool '${tool}'. Available: tailwind, bootstrap.`);
127
+ if (tool === 'mui') {
128
+ const { run } = require('./migrate-mui.cjs');
129
+ await run(migrateArgs);
130
+ return;
131
+ }
132
+ if (tool === 'chakra') {
133
+ const { run } = require('./migrate-chakra.cjs');
134
+ await run(migrateArgs);
135
+ return;
136
+ }
137
+ fail(`unknown migrate tool '${tool}'. Available: tailwind, bootstrap, mui, chakra.`);
113
138
  }
114
139
 
115
140
  if (command === 'add') {
@@ -0,0 +1,249 @@
1
+ /**
2
+ * cia migrate chakra — parse a Chakra UI (v2) theme object + map to cia.
3
+ *
4
+ * v1.2 EPIC-05. Same shape as migrate-tailwind / migrate-bootstrap / migrate-mui.
5
+ * Chakra's theme object shape (colors as {shade: hex} palettes, a flat
6
+ * rem-keyed space/fontSizes scale, a named radii object) lines up closely
7
+ * enough with Tailwind's that most of this is direct reuse of the shared
8
+ * mapping engine, not new logic.
9
+ *
10
+ * Token-only: reads Chakra's theme VALUES. Never imports @chakra-ui/react,
11
+ * never translates Chakra React components — same scope boundary as the
12
+ * MUI migrator, for the same reason (cia has no component library to
13
+ * translate them to).
14
+ *
15
+ * Scoped to Chakra v2 theme shape (still dominant as of this writing); v3
16
+ * changed the theme shape significantly and isn't handled here.
17
+ */
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ // Shared engine from the Tailwind module — no need to fork it. mapColors and
24
+ // mapBorderRadius are reused UNMODIFIED: Chakra's {shade: hex} color-palette
25
+ // shape and {name: length} radii shape are structurally identical to what
26
+ // those functions already walk for Tailwind.
27
+ const {
28
+ loadConfig,
29
+ writeThemeScss,
30
+ parseFlags,
31
+ mapColors,
32
+ mapFlatScale,
33
+ mapBorderRadius,
34
+ CIA_SPACING,
35
+ CIA_FONT_SIZES,
36
+ } = require('./migrate-tailwind.cjs');
37
+
38
+ // ─── theme object discovery ─────────────────────────────────────────────────
39
+
40
+ // extendTheme({...}) already returns a fully-resolved plain object — no
41
+ // Chakra import needed to read one.
42
+ function findChakraTheme(mod) {
43
+ if (mod && typeof mod === 'object') {
44
+ if (mod.theme && (mod.theme.colors || mod.theme.space || mod.theme.fonts)) return mod.theme;
45
+ if (mod.default && (mod.default.colors || mod.default.space || mod.default.fonts)) return mod.default;
46
+ if (mod.colors || mod.space || mod.fonts) return mod;
47
+ }
48
+ return null;
49
+ }
50
+
51
+ // ─── font mapping (Chakra's heading/body key names don't match Tailwind's) ──
52
+
53
+ function mapChakraFonts(fonts) {
54
+ const mappings = {};
55
+ if (!fonts || typeof fonts !== 'object') return mappings;
56
+ if (typeof fonts.heading === 'string') {
57
+ mappings['font-display'] = {
58
+ value: fonts.heading,
59
+ source: 'fonts.heading',
60
+ confidence: 'HIGH',
61
+ rationale: `Chakra 'fonts.heading' → cia 'font-display' (direct stack copy).`,
62
+ };
63
+ }
64
+ if (typeof fonts.body === 'string') {
65
+ mappings['font-sans'] = {
66
+ value: fonts.body,
67
+ source: 'fonts.body',
68
+ confidence: 'HIGH',
69
+ rationale: `Chakra 'fonts.body' → cia 'font-sans' (direct stack copy).`,
70
+ };
71
+ }
72
+ return mappings;
73
+ }
74
+
75
+ // ─── master mapping ─────────────────────────────────────────────────────────
76
+
77
+ function mapChakraToCia(theme) {
78
+ const allMappings = {};
79
+ const allUnmapped = [];
80
+
81
+ const colors = mapColors(theme.colors);
82
+ Object.assign(allMappings, colors.mappings);
83
+ allUnmapped.push(...colors.unmapped);
84
+
85
+ const space = mapFlatScale(theme.space, CIA_SPACING, 'space', 'space');
86
+ Object.assign(allMappings, space.mappings);
87
+ allUnmapped.push(...space.unmapped);
88
+
89
+ const fontSizes = mapFlatScale(theme.fontSizes, CIA_FONT_SIZES, 'font-size', 'fontSizes');
90
+ Object.assign(allMappings, fontSizes.mappings);
91
+ allUnmapped.push(...fontSizes.unmapped);
92
+
93
+ const radii = mapBorderRadius(theme.radii);
94
+ Object.assign(allMappings, radii.mappings);
95
+ allUnmapped.push(...radii.unmapped);
96
+
97
+ Object.assign(allMappings, mapChakraFonts(theme.fonts));
98
+
99
+ // Deliberately not mapped — same disclosed scope cut as MUI's shadows:
100
+ // Chakra's shadow/breakpoint/zIndex scales don't have a clean 1:1 cia
101
+ // analog (cia's breakpoints and z-layers are a fixed system scale, not
102
+ // themed per-project — see scss/_system.scss).
103
+ if (theme.shadows && Object.keys(theme.shadows).length) {
104
+ allUnmapped.push({
105
+ source: 'shadows.*',
106
+ value: `${Object.keys(theme.shadows).length} shadow token(s)`,
107
+ reason: `No 1:1 cia analog (cia has 5 named steps: shadow-sm/md/lg/xl/2xl). Review and pick manually.`,
108
+ });
109
+ }
110
+ if (theme.breakpoints) {
111
+ allUnmapped.push({
112
+ source: 'breakpoints.*',
113
+ value: JSON.stringify(theme.breakpoints),
114
+ reason: `cia's breakpoints are a fixed system scale, not themed per-project — not migrated. Note for reference only.`,
115
+ });
116
+ }
117
+ if (theme.zIndices) {
118
+ allUnmapped.push({
119
+ source: 'zIndices.*',
120
+ value: JSON.stringify(theme.zIndices),
121
+ reason: `cia's z-layers are a fixed system scale (--z-dropdown/-modal/etc.), not themed per-project — not migrated. Note for reference only.`,
122
+ });
123
+ }
124
+
125
+ const counts = { HIGH: 0, MEDIUM: 0, LOW: 0, UNMAPPED: 0 };
126
+ for (const m of Object.values(allMappings)) {
127
+ if (counts[m.confidence] != null) counts[m.confidence] += 1;
128
+ }
129
+ counts.UNMAPPED = allUnmapped.length;
130
+ counts.total = counts.HIGH + counts.MEDIUM + counts.LOW + counts.UNMAPPED;
131
+
132
+ return { mappings: allMappings, unmapped: allUnmapped, report: counts };
133
+ }
134
+
135
+ // ─── CLI entry ────────────────────────────────────────────────────────────
136
+
137
+ const HELP = `cia migrate chakra — convert a Chakra UI (v2) theme object to a cia theme.scss
138
+
139
+ Usage:
140
+ cia migrate chakra <path> [options]
141
+
142
+ Default behavior:
143
+ Loads a module exporting a Chakra theme (the result of extendTheme({...}),
144
+ as a default export, a named 'theme' export, or a plain theme-shaped
145
+ object), maps its colors/space/radii/fonts/fontSizes to cia contract
146
+ tokens with confidence scoring, and writes a cia theme.scss to
147
+ ./cia-themes/<name>.scss. Diagnostics print to stderr.
148
+
149
+ Token-only: never imports @chakra-ui/react, never translates Chakra React
150
+ components — only reads plain values off the theme object. Scoped to
151
+ Chakra v2 theme shape.
152
+
153
+ Arguments:
154
+ path Path to a .js/.ts/.mjs/.cjs module exporting a Chakra theme.
155
+
156
+ Options:
157
+ --name <name> Theme name. Default: migrated
158
+ --out <path> Custom output path. Default: ./cia-themes/<name>.scss
159
+ --json Skip the file write and dump JSON to stdout (pipe-safe).
160
+ -h, --help Show this help.
161
+
162
+ Examples:
163
+ cia migrate chakra ./src/theme.ts
164
+ cia migrate chakra ./src/theme.ts --name acme
165
+ cia migrate chakra ./src/theme.ts --json | jq '.cia.report'
166
+
167
+ Notes:
168
+ - Chakra's 'colors' object uses the same {shade: hex} palette shape as
169
+ Tailwind's, so the color-mapping heuristics (a 'brand'/'primary'-named
170
+ scale, 'red'/'green'/'orange'/'blue' for status) apply unmodified.
171
+ - shadows / breakpoints / zIndices are not mapped automatically — see
172
+ the UNMAPPED block for why.
173
+ `;
174
+
175
+ async function run(args) {
176
+ const { flags, positional } = parseFlags(args);
177
+ if (flags.help) {
178
+ process.stdout.write(HELP);
179
+ return;
180
+ }
181
+
182
+ let themePath = positional[0];
183
+ if (!themePath) {
184
+ throw new Error(`cia migrate chakra requires a path to a module exporting a Chakra theme. Example: cia migrate chakra ./src/theme.ts`);
185
+ }
186
+ if (!path.isAbsolute(themePath)) themePath = path.resolve(process.cwd(), themePath);
187
+ if (!fs.existsSync(themePath)) throw new Error(`theme file not found: ${themePath}`);
188
+
189
+ process.stderr.write(`cia migrate chakra\n`);
190
+ process.stderr.write(` theme: ${themePath}\n`);
191
+ process.stderr.write(` loading...\n`);
192
+
193
+ const mod = loadConfig(themePath);
194
+ const theme = findChakraTheme(mod);
195
+ if (!theme) {
196
+ throw new Error(
197
+ `Could not find a Chakra theme in ${themePath}. Expected a default export, a named ` +
198
+ `'theme' export, or the module itself to be a theme-shaped object with a 'colors', ` +
199
+ `'space', or 'fonts' key (the result of extendTheme({...})).`,
200
+ );
201
+ }
202
+
203
+ process.stderr.write(` found theme\n`);
204
+ process.stderr.write(`\n mapping to cia contract tokens...\n`);
205
+ const cia = mapChakraToCia(theme);
206
+
207
+ process.stderr.write(`\n ─── confidence report ─────────────────────────\n`);
208
+ process.stderr.write(` HIGH (exact match): ${cia.report.HIGH}\n`);
209
+ process.stderr.write(` MEDIUM (close, ≤0.125rem): ${cia.report.MEDIUM}\n`);
210
+ process.stderr.write(` LOW (best guess): ${cia.report.LOW}\n`);
211
+ process.stderr.write(` UNMAPPED (no cia analog): ${cia.report.UNMAPPED}\n`);
212
+ process.stderr.write(` ─────────────────────────────────────────────\n`);
213
+ process.stderr.write(` total ${cia.report.total}\n`);
214
+
215
+ if (flags.json) {
216
+ process.stderr.write(`\n --- JSON dump on stdout below ---\n`);
217
+ process.stdout.write(JSON.stringify({ source: themePath, cia }, null, 2));
218
+ process.stdout.write('\n');
219
+ return;
220
+ }
221
+
222
+ const scss = writeThemeScss(cia, { name: flags.name, source: themePath, tool: 'chakra' });
223
+ let outPath = flags.out;
224
+ if (outPath) {
225
+ if (!path.isAbsolute(outPath)) outPath = path.resolve(process.cwd(), outPath);
226
+ } else {
227
+ outPath = path.join(process.cwd(), 'cia-themes', `${flags.name}.scss`);
228
+ }
229
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
230
+ fs.writeFileSync(outPath, scss, 'utf8');
231
+
232
+ process.stderr.write(`\n wrote ${outPath}\n`);
233
+ process.stderr.write(` ${scss.split('\n').length - 1} lines, ${Object.keys(cia.mappings).length} mapped tokens\n`);
234
+ if (cia.report.UNMAPPED > 0) {
235
+ process.stderr.write(` ${cia.report.UNMAPPED} unmapped/informational entries appended as a comment block — review\n`);
236
+ }
237
+ process.stderr.write(`\n Next steps:\n`);
238
+ process.stderr.write(` 1. Review the file (MEDIUM/LOW/UNMAPPED entries are flagged)\n`);
239
+ process.stderr.write(` 2. @use this file from your app's SCSS entry\n`);
240
+ process.stderr.write(` 3. Set <html data-theme="${flags.name}"> and run npm run build:css\n`);
241
+ process.stderr.write(` 4. Run npm run validate-themes to confirm WCAG 2.2 AA contrast\n`);
242
+ }
243
+
244
+ module.exports = {
245
+ run,
246
+ findChakraTheme,
247
+ mapChakraFonts,
248
+ mapChakraToCia,
249
+ };