css-is-awesome 1.8.1 → 1.9.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 CHANGED
@@ -26,7 +26,12 @@ When asked to add a UI element, follow this order:
26
26
  4. **All values come from tokens.** Never hardcode `#3A5FCD`, `1rem`, `8px`. Use `cia.color(primary)`, `cia.space(4)`, `cia.radius(md)`.
27
27
  5. **No BEM.** No `__element` / `--modifier` chains. `cia-` is a single-class namespace prefix, not BEM.
28
28
  6. **No JavaScript.** Cia ships zero JS in the npm package. The 6 interactive components (accordion, modal, tooltip, dropdown, tabs, copy-button) use native HTML primitives — `<details name>`, `<dialog>`, `[popover]`, radio + `:has()`. Mobile navigation follows the same doctrine: the `hamburger` / `drawer` / `sheet` / `dock` mixin family rides `[popover]` + CSS Grid — see the `mobile-nav` recipe (hamburger + drawer) and the `bottom-nav` recipe (dock + sheets). **This rule binds cia, not you.** If you're *consuming* cia (building an app/component library on top of it), write JavaScript/framework components freely — React, SVG charts, interactivity, all of it — and use cia purely for styling (mixins + tokens). Compose the mixins to build any visual you want; you are not limited to cia's pre-made component mixins.
29
- 7. **Pages stack by named grid areas the layout mixins own the shell.** Every page's structure is declared once as `grid-template-areas` (`"navbar" "hero" … "footer"`), and each section claims its slot with `grid-area`. Never hand-write that: use `cia.page-layout(default | sidebar-left | sidebar-right | holy-grail)` for a full viewport shell (100dvh, sticky footer, auto mobile collapse), `cia.layout((sidebar content toc), $tracks: …)` for custom rows of regions, and `cia.page-header` / `cia.page-main` / `cia.page-footer` / `cia.area(name)` for the children. Named areas are the single source of truth for stacking — mobile is just a different `grid-template-areas`, not a pile of margin overrides. Baseline since 2020.
29
+ 7. **Grid is the skeleton; Flex is the quick moves.** Three levels, strictly:
30
+ - **The page shell is CSS Grid with landmark-named areas.** The body's areas ARE the document's landmarks — `nav`, `main`, `footer` — so the area map reads like the page and screen readers get the structure for free. Declared once via `cia.page-layout(default | sidebar-left | sidebar-right | holy-grail)` (100dvh, sticky footer, auto mobile collapse) or `cia.layout((sidebar content toc), $tracks: …)`; children claim slots with `cia.page-header` / `cia.page-main` / `cia.page-footer` / `cia.area(name)`. Baseline since 2020.
31
+ - **The doctrine scales inward: any control-dense region gets its own named-area grid.** A docs article (`header / demo / usage / tabs / footer`), a selections rail (`filter / list`), a dashboard — when a region has many controls, name its rows with `cia.layout(...)` too. Nested grids all the way down where density warrants; the grid's `gap` is the region's entire vertical rhythm (children carry no rhythm margins).
32
+ - **Flex lives at the leaves** — simple rows and columns you flip with one command: `@include cia.flex($direction: column)` turns a row into a stack. Flex arranges the contents of a slot the grid gave it; it never builds the page.
33
+
34
+ Mobile is just a different `grid-template-areas` map — restack, hide (with `display: none`; omitting an area does not hide its element), reorder — never a pile of margin overrides. Never hand-write `grid-template-areas`; the layout mixins own it.
30
35
  8. **On phones, things take the space they're in.** Below the mobile breakpoint an interactive surface fills its *container* — 100% of the column it lives in, inside the page's existing padding and formatting (never edge-to-edge past the page's gutters, never a floating mid-width popup). Triggers stretch to 100% with `justify-content: space-between` (label left, affordance right). A top-layer popover menu can't size to its container directly, so match its trigger via CSS anchor positioning (`anchor-name` on the trigger; `inset-inline: anchor(start) anchor(end)` under `@supports (anchor-name: --a)`) — opening directly under the trigger, with `position-try-fallbacks: flip-block` so it flips above when it would run off the bottom of the screen — with viewport-minus-gutters as the no-anchor fallback — and set it at `&[popover]` specificity, since the dropdown mixin's `inset: unset` reset otherwise wins. Also set `width: auto`: the UA's `[popover] { width: fit-content }` otherwise beats both inset edges and the menu hugs its content. `cia.sheet`, `cia.drawer($side: top|bottom)` and `cia.dock` already obey the rule. **Deliberate exception — code blocks:** on phones, code is allowed to run off into a horizontal scroll *inside its own box* (`white-space: pre; overflow-x: auto`) — never wrap, shrink, or reflow code to fit, and never let it widen the page; the copy button carries usability for long lines.
31
36
  9. **Style semantic state off ARIA, not a parallel `data-*`.** When a state has an ARIA source of truth, hook your styles to *that* attribute: `[aria-selected="true"]`, `[aria-expanded="true"]`, `[aria-invalid="true"]`, `:disabled, [aria-disabled="true"]`, `[aria-pressed="true"]`, `[aria-checked="true"]`, `[aria-current]`, `[role="tab"]`. Then the state can't be styled without setting the ARIA a screen reader needs — **accessible-by-construction**, one source of truth for looks + a11y. Reserve `data-*` for **cosmetic-only** variants (`data-size`, `data-variant`, `data-color`) that carry no ARIA meaning. cia's own components already do this where native HTML doesn't cover it (`[aria-current="page"]`, `[aria-selected="true"]`, `[role="option"]`).
32
37
 
package/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ # [1.9.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.8.2...v1.9.0) (2026-09-05)
2
+
3
+
4
+ ### Features
5
+
6
+ * **cli:** cia add (recipe registry) + cia analyze (design-system health) ([194c253](https://github.com/Jerry2d3d/css-is-awesome/commit/194c253a35eec68c68e2f2ca7402fd4e72a4d6ad))
7
+ * **site:** /docs/browser-support - the dated two-tier support matrix ([d2e4604](https://github.com/Jerry2d3d/css-is-awesome/commit/d2e4604c6a331b467bdcd71b30ba2b7f7fe90f68))
8
+
9
+ ## [1.8.2](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.8.1...v1.8.2) (2026-09-05)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **site:** ComponentDoc must be a client component ([fd6ab31](https://github.com/Jerry2d3d/css-is-awesome/commit/fd6ab3117fb06d37c9ee1da4d1071324cae87338))
15
+ * **tabs:** tab list is a <nav> - the div list shifted every panel index ([5099145](https://github.com/Jerry2d3d/css-is-awesome/commit/50991455c0646ffa7ec468166d29d239cbc4c235)), closes [#1](https://github.com/Jerry2d3d/css-is-awesome/issues/1)
16
+
17
+
18
+ ### Features
19
+
20
+ * **site:** component docs are data - DocEntry registry + article grid (phase 3) ([98d2419](https://github.com/Jerry2d3d/css-is-awesome/commit/98d2419af9b543a6d09888d54a4f1d4b1ca757f6))
21
+ * **site:** docs rail anatomy (Boiler handoff, phases 1+2) + the full layout doctrine written down ([fa1fa8b](https://github.com/Jerry2d3d/css-is-awesome/commit/fa1fa8b4e182793f1806d4e13af051394ad7cf8b)), closes [#7](https://github.com/Jerry2d3d/css-is-awesome/issues/7)
22
+
1
23
  ## [1.8.1](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.8.0...v1.8.1) (2026-09-04)
2
24
 
3
25
 
package/README.md CHANGED
@@ -204,6 +204,8 @@ cia ships **no component library** — deliberately. Interactive patterns arrive
204
204
 
205
205
  **Shipped:** `dialog`, `combobox`, `print-to-pdf`, `mobile-nav`, `bottom-nav`. Queued next: `datepicker`, `data-table`, `command-palette`.
206
206
 
207
+ **Layout doctrine: Grid is the skeleton, Flex is the quick moves.** The page shell is CSS Grid with landmark-named areas (`nav / main / footer` — the map reads like the page); any control-dense region inside gets its own named-area grid whose `gap` carries all vertical rhythm; flex lives at the leaves for one-command flips (`cia.flex($direction: column)`). `cia.page-layout()` and `cia.layout()` own the maps — mobile is a different area map, never margin overrides.
208
+
207
209
  **Mobile navigation is system API, not an exercise for the reader.** Four mixins — `cia.hamburger`, `cia.drawer`, `cia.sheet`, `cia.dock` — cover both phone layouts: the **flex layout** (one fluid nav/main/footer shell that reshapes with the screen, hamburger opening a slide-in drawer — recipe [`mobile-nav`](./scss/recipes/mobile-nav.md)) and the **app layout** (a fixed bottom dock whose slots open slide-up sheets — recipe [`bottom-nav`](./scss/recipes/bottom-nav.md)). Zero JavaScript: everything rides CSS Grid and the native Popover API — the browser owns open state, `aria-expanded`, Esc and light dismiss. The docs site dogfoods the app layout on phones. The same doctrine extends to dropdowns: on phones an interactive surface takes the space it's in, so a `cia.dropdown` menu opens 1px under its full-width trigger at the trigger's exact width — CSS anchor positioning, flipping above when the screen bottom would clip it. The full spec is the [mobile playbook](https://cssisawesome.com/docs/mobile/).
208
210
 
209
211
  Humans read them at [`/docs/recipes`](https://cssisawesome.com/docs/recipes/); AI agents pull them over MCP with `list_recipes` / `get_recipe`.
@@ -219,6 +221,17 @@ npx cia migrate bootstrap ./scss/_variables.scss
219
221
 
220
222
  Both accept `--help` for the full option list. Prose walkthroughs live at [`/docs/migration-tailwind`](https://cssisawesome.com/docs/migration-tailwind/) and [`/docs/migration-bootstrap`](https://cssisawesome.com/docs/migration-bootstrap/).
221
223
 
224
+ The CLI also carries the registry and the health check:
225
+
226
+ ```bash
227
+ npx cia add --list # browse the recipe book
228
+ npx cia add bottom-nav # copy a recipe into your project — you own the pattern
229
+ npx cia analyze src/styles # design-system health: dead cia.* symbols, the
230
+ # space() scale trap, hard-coded colors, BEM creep
231
+ ```
232
+
233
+ `cia analyze` reads the real API surface from the installed package and exits non-zero on errors, so it slots straight into CI.
234
+
222
235
  ## Print / PDF (zero JS)
223
236
 
224
237
  Print support is a pure-CSS layer — the browser's native **Print → Save as PDF** is the generator, and cia ships no JavaScript for it:
@@ -0,0 +1,94 @@
1
+ /**
2
+ * cia add — copy a recipe from the installed package into the project.
3
+ *
4
+ * The recipes book ships inside the npm package (scss/recipes/*.md). This
5
+ * command copies one into the consumer's tree so they OWN the pattern —
6
+ * the registry model: own the generated code, don't import an opaque
7
+ * component. Markdown pattern recipes only; the opt-in SCSS recipes
8
+ * (e.g. bare-tags) are `@use`d, not copied.
9
+ */
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const RECIPES_DIR = path.join(__dirname, '..', 'scss', 'recipes');
16
+
17
+ const HELP = `cia add — copy a recipe into your project
18
+
19
+ Usage:
20
+ cia add <recipe> [options]
21
+ cia add --list
22
+
23
+ Options:
24
+ --list List available recipes.
25
+ --out <path> Output path. Default: ./cia-recipes/<recipe>.md
26
+ --force Overwrite if the target file exists.
27
+
28
+ Examples:
29
+ cia add --list
30
+ cia add bottom-nav
31
+ cia add mobile-nav --out docs/patterns/mobile-nav.md
32
+ `;
33
+
34
+ function fail(message) {
35
+ process.stderr.write(`cia add: ${message}\n`);
36
+ process.exit(1);
37
+ }
38
+
39
+ function listRecipes() {
40
+ return fs
41
+ .readdirSync(RECIPES_DIR)
42
+ .filter((f) => f.endsWith('.md') && !f.startsWith('_') && f !== 'README.md')
43
+ .map((f) => f.replace(/\.md$/, ''))
44
+ .sort();
45
+ }
46
+
47
+ function frontmatterDescription(raw) {
48
+ const m = raw.match(/^---[\s\S]*?\ndescription:\s*(.+?)\r?\n[\s\S]*?---/);
49
+ return m ? m[1].trim() : '';
50
+ }
51
+
52
+ async function run(args) {
53
+ if (!args.length || args[0] === '-h' || args[0] === '--help' || args[0] === 'help') {
54
+ process.stdout.write(HELP);
55
+ return;
56
+ }
57
+
58
+ if (args.includes('--list')) {
59
+ for (const slug of listRecipes()) {
60
+ const raw = fs.readFileSync(path.join(RECIPES_DIR, `${slug}.md`), 'utf8');
61
+ process.stdout.write(`${slug.padEnd(16)} ${frontmatterDescription(raw)}\n`);
62
+ }
63
+ return;
64
+ }
65
+
66
+ const slug = args[0];
67
+ const recipes = listRecipes();
68
+ if (!recipes.includes(slug)) {
69
+ fail(`unknown recipe '${slug}'. Available: ${recipes.join(', ')}.`);
70
+ }
71
+
72
+ const outFlag = args.indexOf('--out');
73
+ const outPath =
74
+ outFlag !== -1 && args[outFlag + 1]
75
+ ? path.resolve(args[outFlag + 1])
76
+ : path.resolve('cia-recipes', `${slug}.md`);
77
+
78
+ if (fs.existsSync(outPath) && !args.includes('--force')) {
79
+ fail(`${path.relative(process.cwd(), outPath)} already exists. Pass --force to overwrite.`);
80
+ }
81
+
82
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
83
+ fs.copyFileSync(path.join(RECIPES_DIR, `${slug}.md`), outPath);
84
+
85
+ const rel = path.relative(process.cwd(), outPath);
86
+ process.stdout.write(
87
+ `✓ ${slug} → ${rel}\n\n` +
88
+ `The recipe is yours now — correct HTML, the cia mixin calls, and the\n` +
89
+ `a11y checklist. Read it, copy the pattern into your stack, keep the\n` +
90
+ `checklist. Docs: https://cssisawesome.com/docs/recipes/${slug}/\n`,
91
+ );
92
+ }
93
+
94
+ module.exports = { run };
@@ -0,0 +1,191 @@
1
+ /**
2
+ * cia analyze — design-system health check for a consumer project.
3
+ *
4
+ * Reads the REAL API surface from the installed package's SCSS sources
5
+ * (the same files the MCP server serves), then audits the project's
6
+ * stylesheets for:
7
+ * - cia.* calls that don't resolve (dead symbols — typos, removals)
8
+ * - the space() scale trap (numbered scale is 1–9; unknown keys pass
9
+ * through raw and silently invalidate the declaration)
10
+ * - hard-coded hex colors (values should come from tokens)
11
+ * - BEM-style class names (__ / -- chains are forbidden in cia projects)
12
+ * - hand-written grid-template-areas (the layout mixins own the maps)
13
+ *
14
+ * Zero dependencies, filesystem only — same philosophy as the MCP server.
15
+ */
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const PKG_SCSS = path.join(__dirname, '..', 'scss');
22
+
23
+ const HELP = `cia analyze — design-system health check
24
+
25
+ Usage:
26
+ cia analyze [path] [options]
27
+
28
+ Scans [path] (default: current directory) for *.scss files and audits
29
+ them against the installed css-is-awesome API.
30
+
31
+ Options:
32
+ --namespace <ns> Extra namespace(s) to treat as cia (comma-separated).
33
+ Auto-detected per file from @use lines; use this when
34
+ imports are aliased through an intermediate file.
35
+ --json Machine-readable report on stdout.
36
+ --strict Exit 1 on warnings too (default: errors only).
37
+
38
+ Examples:
39
+ cia analyze
40
+ cia analyze src/styles --json
41
+ cia analyze src --namespace m,l
42
+ `;
43
+
44
+ // ── API surface discovery ───────────────────────────────────────────────────
45
+
46
+ function collectSymbols() {
47
+ const symbols = new Set();
48
+ const files = [];
49
+ const top = ['_mixins.scss', '_layout.scss', '_animations.scss', '_generator.scss'];
50
+ for (const f of top) files.push(path.join(PKG_SCSS, f));
51
+ const compDir = path.join(PKG_SCSS, 'components');
52
+ for (const f of fs.readdirSync(compDir)) {
53
+ if (f.endsWith('.scss')) files.push(path.join(compDir, f));
54
+ }
55
+ for (const file of files) {
56
+ let src = '';
57
+ try {
58
+ src = fs.readFileSync(file, 'utf8');
59
+ } catch {
60
+ continue;
61
+ }
62
+ for (const m of src.matchAll(/@(?:mixin|function)\s+([a-zA-Z][\w-]*)/g)) {
63
+ if (!m[1].startsWith('_')) symbols.add(m[1]);
64
+ }
65
+ }
66
+ // Icon mixins are forwarded with an icon- prefix by the /api barrel.
67
+ const icons = path.join(PKG_SCSS, '_icons.scss');
68
+ try {
69
+ const src = fs.readFileSync(icons, 'utf8');
70
+ for (const m of src.matchAll(/@(?:mixin|function)\s+([a-zA-Z][\w-]*)/g)) {
71
+ if (!m[1].startsWith('_')) symbols.add(`icon-${m[1]}`);
72
+ }
73
+ } catch {
74
+ /* icons module optional */
75
+ }
76
+ return symbols;
77
+ }
78
+
79
+ // ── project scan ────────────────────────────────────────────────────────────
80
+
81
+ function walkScss(dir, out = []) {
82
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
83
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
84
+ const full = path.join(dir, entry.name);
85
+ if (entry.isDirectory()) walkScss(full, out);
86
+ else if (entry.name.endsWith('.scss')) out.push(full);
87
+ }
88
+ return out;
89
+ }
90
+
91
+ const CIA_SOURCE = /^(?:css-is-awesome(?:\/|$)|pkg:css-is-awesome|api$|mixins$|layout$|animations$|generator$|icons$|components(?:\/|$))/;
92
+
93
+ function detectNamespaces(src, extra) {
94
+ const ns = new Set(extra);
95
+ for (const m of src.matchAll(/@use\s+['"]([^'"]+)['"]\s+as\s+([a-zA-Z][\w-]*)/g)) {
96
+ if (CIA_SOURCE.test(m[1])) ns.add(m[2]);
97
+ }
98
+ // `@use 'css-is-awesome/api';` without `as` → namespace is the last segment.
99
+ for (const m of src.matchAll(/@use\s+['"]([^'"]+)['"]\s*;/g)) {
100
+ if (CIA_SOURCE.test(m[1])) ns.add(m[1].split('/').pop().replace(/\.scss$/, ''));
101
+ }
102
+ return ns;
103
+ }
104
+
105
+ function stripComments(src) {
106
+ return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
107
+ }
108
+
109
+ function analyzeFile(file, symbols, extraNs) {
110
+ const raw = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
111
+ const src = stripComments(raw);
112
+ const ns = detectNamespaces(src, extraNs);
113
+ const findings = [];
114
+ if (ns.size) {
115
+ const nsPattern = [...ns].map((n) => n.replace(/[-]/g, '\\-')).join('|');
116
+ const callRe = new RegExp(`\\b(?:${nsPattern})\\.([a-zA-Z][\\w-]*)`, 'g');
117
+ for (const m of src.matchAll(callRe)) {
118
+ const sym = m[1];
119
+ if (!symbols.has(sym)) {
120
+ findings.push({ level: 'error', rule: 'unknown-symbol', detail: `cia.${sym} does not exist in the installed API` });
121
+ }
122
+ }
123
+ // space() scale trap: numbered keys are 1–9; higher/fractional numbers
124
+ // pass through raw and silently invalidate the declaration.
125
+ const spaceRe = new RegExp(`\\b(?:${nsPattern})\\.space\\(\\s*(\\d+(?:\\.\\d+)?)\\s*\\)`, 'g');
126
+ for (const m of src.matchAll(spaceRe)) {
127
+ const n = Number(m[1]);
128
+ if (!Number.isInteger(n) || n > 9) {
129
+ findings.push({ level: 'error', rule: 'space-scale', detail: `space(${m[1]}) — the numbered scale is 1–9; this emits a unitless number and the browser drops the declaration. Use grid(n) for 4px multiples or pass an explicit unit.` });
130
+ }
131
+ }
132
+ }
133
+ for (const m of src.matchAll(/#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g)) {
134
+ findings.push({ level: 'warn', rule: 'hard-coded-color', detail: `${m[0]} — values should come from tokens (cia.color(...) / var(--...))` });
135
+ }
136
+ for (const m of src.matchAll(/\.[a-zA-Z][\w]*(?:__|--)[\w-]+/g)) {
137
+ findings.push({ level: 'warn', rule: 'bem', detail: `${m[0]} — BEM chains are forbidden; use semantic single-class names` });
138
+ }
139
+ if (/(?:^|[{;\s])grid-template-areas\s*:/m.test(src)) {
140
+ findings.push({ level: 'info', rule: 'hand-written-areas', detail: 'grid-template-areas written by hand — the layout mixins (page-layout / layout / area) own the maps' });
141
+ }
142
+ return { file, namespaces: [...ns], findings };
143
+ }
144
+
145
+ // ── report ──────────────────────────────────────────────────────────────────
146
+
147
+ async function run(args) {
148
+ if (args[0] === '-h' || args[0] === '--help' || args[0] === 'help') {
149
+ process.stdout.write(HELP);
150
+ return;
151
+ }
152
+ const json = args.includes('--json');
153
+ const strict = args.includes('--strict');
154
+ const nsFlag = args.indexOf('--namespace');
155
+ const extraNs = nsFlag !== -1 && args[nsFlag + 1] ? args[nsFlag + 1].split(',') : [];
156
+ const target = path.resolve(args.find((a) => !a.startsWith('--') && a !== extraNs.join(',')) || '.');
157
+
158
+ if (!fs.existsSync(target)) {
159
+ process.stderr.write(`cia analyze: path not found: ${target}\n`);
160
+ process.exit(1);
161
+ }
162
+
163
+ const symbols = collectSymbols();
164
+ const files = walkScss(target);
165
+ const results = files.map((f) => analyzeFile(f, symbols, extraNs)).filter((r) => r.findings.length || r.namespaces.length);
166
+
167
+ const counts = { error: 0, warn: 0, info: 0 };
168
+ for (const r of results) for (const f of r.findings) counts[f.level]++;
169
+ const ciaFiles = results.filter((r) => r.namespaces.length).length;
170
+ const health = Math.max(0, 100 - counts.error * 10 - counts.warn * 2 - counts.info);
171
+
172
+ if (json) {
173
+ process.stdout.write(JSON.stringify({ target, files: files.length, ciaFiles, apiSymbols: symbols.size, counts, health, results }, null, 2) + '\n');
174
+ } else {
175
+ process.stdout.write(`\ncia analyze — ${path.relative(process.cwd(), target) || '.'}\n`);
176
+ process.stdout.write(`${files.length} scss file(s), ${ciaFiles} using cia, ${symbols.size} API symbols known\n\n`);
177
+ for (const r of results) {
178
+ if (!r.findings.length) continue;
179
+ process.stdout.write(`${path.relative(process.cwd(), r.file)}\n`);
180
+ for (const f of r.findings) {
181
+ const mark = f.level === 'error' ? '✗' : f.level === 'warn' ? '⚠' : 'ℹ';
182
+ process.stdout.write(` ${mark} [${f.rule}] ${f.detail}\n`);
183
+ }
184
+ }
185
+ process.stdout.write(`\nDesign-system health: ${health}% (${counts.error} error, ${counts.warn} warn, ${counts.info} info)\n`);
186
+ }
187
+
188
+ if (counts.error > 0 || (strict && counts.warn > 0)) process.exit(1);
189
+ }
190
+
191
+ module.exports = { run };
package/bin/cia.cjs CHANGED
@@ -27,11 +27,17 @@ Usage:
27
27
 
28
28
  Commands:
29
29
  migrate <tool> [path] Convert another design system's config to a cia
30
- theme. Tools: tailwind | bootstrap (planned).
30
+ theme. Tools: tailwind | bootstrap.
31
+ add <recipe> Copy a recipe from the book into your project
32
+ (own the pattern). \`cia add --list\` to browse.
33
+ analyze [path] Design-system health check: dead cia.* symbols,
34
+ the space() scale trap, hard-coded colors, BEM,
35
+ hand-written area maps.
31
36
 
32
37
  Examples:
33
38
  cia migrate tailwind ./tailwind.config.js
34
- cia migrate tailwind # auto-detect tailwind.config.*
39
+ cia add bottom-nav
40
+ cia analyze src/styles
35
41
 
36
42
  Run \`cia <command> --help\` for command-specific help.
37
43
  `;
@@ -106,6 +112,18 @@ async function main() {
106
112
  fail(`unknown migrate tool '${tool}'. Available: tailwind, bootstrap.`);
107
113
  }
108
114
 
115
+ if (command === 'add') {
116
+ const { run } = require('./add-recipe.cjs');
117
+ await run(rest);
118
+ return;
119
+ }
120
+
121
+ if (command === 'analyze') {
122
+ const { run } = require('./analyze.cjs');
123
+ await run(rest);
124
+ return;
125
+ }
126
+
109
127
  fail(`unknown command '${command}'. Run \`cia --help\` for usage.`);
110
128
  }
111
129
 
package/dist/tokens.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Generated from scripts/theme-contract.json on 2026-09-04.
1
+ // Generated from scripts/theme-contract.json on 2026-09-05.
2
2
  // Do not edit by hand. Run `npm run build:token-types` to regenerate.
3
3
 
4
4
  /** Every CSS custom property cia themes are required to declare. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "css-is-awesome",
3
- "version": "1.8.1",
3
+ "version": "1.9.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": {
@@ -8,14 +8,22 @@
8
8
  // <div class="cia-tabs">
9
9
  // <input type="radio" name="t" id="t1" checked>
10
10
  // <input type="radio" name="t" id="t2">
11
- // <div class="cia-tab-list" role="tablist">
11
+ // <nav class="cia-tab-list" role="tablist">
12
12
  // <label for="t1" role="tab">Tab 1</label>
13
13
  // <label for="t2" role="tab">Tab 2</label>
14
- // </div>
14
+ // </nav>
15
15
  // <div class="cia-tab-panel" role="tabpanel">Content 1</div>
16
16
  // <div class="cia-tab-panel" role="tabpanel">Content 2</div>
17
17
  // </div>
18
18
  //
19
+ // ⚠️ CONTRACT: the tab list must NOT share the panels' element type.
20
+ // Panel switching indexes panels with :nth-of-type(), which counts by
21
+ // ELEMENT TYPE among siblings — a <div> tab list would be div #1, every
22
+ // panel's index would shift by one, and tab 1 would show no panel while
23
+ // the labels still highlighted correctly (we shipped exactly that; fixed
24
+ // 2026-09-04 by making the documented list a <nav>). CSS has no
25
+ // nth-of-class, so the markup carries this rule instead of the selector.
26
+ //
19
27
  // A11y baseline: keyboard nav (Tab + arrows + Space) and screen-reader text
20
28
  // are native. For full aria-selected sync ship the tiny opt-in JS shim
21
29
  // documented at /docs/components/tabs#aria.