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 +8 -0
- package/README.md +11 -0
- package/bin/add-recipe.cjs +94 -0
- package/bin/analyze.cjs +191 -0
- package/bin/cia.cjs +20 -2
- package/package.json +1 -1
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 };
|
package/bin/analyze.cjs
ADDED
|
@@ -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
|
|
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
|
|
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.
|
|
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": {
|