css-is-awesome 1.8.2 → 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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
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
+
1
9
  ## [1.8.2](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.8.1...v1.8.2) (2026-09-05)
2
10
 
3
11
 
package/README.md CHANGED
@@ -221,6 +221,17 @@ npx cia migrate bootstrap ./scss/_variables.scss
221
221
 
222
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/).
223
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
+
224
235
  ## Print / PDF (zero JS)
225
236
 
226
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "css-is-awesome",
3
- "version": "1.8.2",
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": {