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.
- package/CHANGELOG.md +34 -0
- package/CONTRACT.md +15 -0
- package/README.md +9 -8
- package/THEMING.md +1 -0
- package/bin/analyze.cjs +88 -1
- package/bin/cia.cjs +30 -5
- package/bin/migrate-chakra.cjs +249 -0
- package/bin/migrate-mui.cjs +363 -0
- package/bin/migrate-tailwind.cjs +3 -1
- package/dist/css-is-awesome.css +516 -0
- package/dist/css-is-awesome.min.css +1 -1
- package/dist/css-is-awesome.utilities.css +516 -0
- package/dist/css-is-awesome.utilities.min.css +1 -1
- package/dist/tokens.d.ts +4 -2
- package/package.json +7 -3
- package/public/theme.css +293 -237
- package/public/themes/boilerplate/theme.css +11 -10
- package/public/themes/boilerplate-dark/theme.css +10 -9
- package/public/themes/boilerplate-light/theme.css +10 -9
- package/public/themes/cupertino/theme.css +11 -10
- package/public/themes/cupertino-dark/theme.css +11 -10
- package/public/themes/cupertino-light/theme.css +11 -10
- package/public/themes/glass/theme.css +11 -10
- package/public/themes/glass-dark/theme.css +11 -10
- package/public/themes/glass-light/theme.css +11 -10
- package/public/themes/graphite/theme.css +11 -10
- package/public/themes/graphite-dark/theme.css +11 -10
- package/public/themes/graphite-light/theme.css +11 -10
- package/public/themes/press/theme.css +11 -10
- package/public/themes/press-dark/theme.css +11 -10
- package/public/themes/press-light/theme.css +11 -10
- package/public/themes/prism/theme.css +11 -10
- package/public/themes/prism-dark/theme.css +11 -10
- package/public/themes/prism-light/theme.css +11 -10
- package/public/themes/sketchbook/theme.css +11 -10
- package/public/themes/sketchbook-dark/theme.css +11 -10
- package/public/themes/sketchbook-light/theme.css +10 -9
- package/public/themes/terminal/theme.css +11 -10
- package/public/themes/terminal-dark/theme.css +11 -10
- package/public/themes/terminal-light/theme.css +11 -10
- package/scripts/theme-a11y.js +11 -31
- package/scripts/theme-contract.json +1 -0
- package/scss/_spacing-scale.scss +25 -0
- package/scss/_utilities.scss +44 -9
- package/scss/components/_accordion.scss +1 -1
- package/scss/components/_data.scss +2 -2
- package/scss/components/_feedback.scss +1 -1
- package/scss/components/_forms.scss +16 -2
- package/scss/components/_navigation.scss +1 -1
- package/scss/examples/_usage.scss +3 -3
- package/scss/recipes/admin-dashboard-layout.md +277 -0
- package/scss/recipes/app-shell.md +339 -0
- package/scss/recipes/auth-flow.md +345 -0
- package/scss/recipes/confirm-dialog.md +248 -0
- package/scss/recipes/data-table.md +382 -0
- package/scss/recipes/datepicker.md +428 -0
- package/scss/recipes/form-validation-async.md +340 -0
- package/scss/recipes/form-validation-html5.md +306 -0
- package/scss/recipes/form-validation-react-hook-form.md +224 -0
- package/scss/recipes/form-validation-success-states.md +290 -0
- package/scss/recipes/form-validation-zod.md +270 -0
- package/scss/recipes/i18n-date-formatting.md +172 -0
- package/scss/recipes/i18n-number-currency.md +145 -0
- package/scss/recipes/i18n-pluralization.md +163 -0
- package/scss/recipes/multi-step-wizard.md +365 -0
- package/scss/recipes/otp-input.md +310 -0
- package/scss/recipes/rtl-layout.md +273 -0
- package/scss/themes/boilerplate-dark.scss +2 -10
- package/scss/themes/boilerplate-light.scss +2 -10
- package/scss/themes/boilerplate.scss +2 -10
- package/scss/themes/cupertino-dark.scss +2 -10
- package/scss/themes/cupertino-light.scss +2 -10
- package/scss/themes/cupertino.scss +2 -10
- package/scss/themes/glass-dark.scss +2 -10
- package/scss/themes/glass-light.scss +2 -10
- package/scss/themes/glass.scss +2 -10
- package/scss/themes/graphite-dark.scss +2 -10
- package/scss/themes/graphite-light.scss +2 -10
- package/scss/themes/graphite.scss +2 -10
- package/scss/themes/press-dark.scss +2 -10
- package/scss/themes/press-light.scss +2 -10
- package/scss/themes/press.scss +2 -10
- package/scss/themes/prism-dark.scss +2 -10
- package/scss/themes/prism-light.scss +2 -10
- package/scss/themes/prism.scss +2 -10
- package/scss/themes/sketchbook-dark.scss +2 -10
- package/scss/themes/sketchbook-light.scss +2 -10
- package/scss/themes/sketchbook.scss +2 -10
- package/scss/themes/terminal-dark.scss +2 -10
- package/scss/themes/terminal-light.scss +2 -10
- 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 —
|
|
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.
|
|
391
|
-
| `dist/css-is-awesome.core.min.css` | 2.
|
|
392
|
-
| `dist/css-is-awesome.utilities.min.css` | 4.
|
|
393
|
-
| `dist/css-is-awesome.min.css` | 7.
|
|
394
|
-
| Per-theme `themes/<name>/theme.css` | 1.
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|