css-is-awesome 1.16.0 → 1.17.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 +13 -8
- package/CHANGELOG.md +33 -123
- package/CONTRACT.md +1 -0
- package/MIGRATION.md +148 -0
- package/README.md +13 -7
- package/VERSIONING.md +4 -0
- package/bin/cia.cjs +19 -0
- package/bin/theme-from-tokens.cjs +139 -0
- package/dist/tokens.d.ts +2 -4
- package/figma-tokens/README.md +15 -0
- package/llm.txt +4 -4
- package/mcp/server.cjs +35 -1
- package/package.json +13 -3
- package/scripts/theme-contract.json +3 -3
- package/scripts/theme-validator.js +32 -2
- package/scripts/tokens-to-theme.cjs +668 -0
- package/ROADMAP.md +0 -717
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cia theme from-tokens — design-tokens JSON → validated cia theme.css.
|
|
3
|
+
*
|
|
4
|
+
* The CLI face of scripts/tokens-to-theme.cjs (the MCP tool
|
|
5
|
+
* `theme_from_tokens` and the in-process `handlers.theme_from_tokens` are the
|
|
6
|
+
* same function). Zero dependencies, no Sass: the theme is emitted as CSS in
|
|
7
|
+
* the exact shape the shipped themes use, then the shipped validator + WCAG
|
|
8
|
+
* audit run on it before anything is written.
|
|
9
|
+
*
|
|
10
|
+
* Exit codes: 0 valid (or --no-validate), 1 contract/a11y failure (a11y only
|
|
11
|
+
* unless --allow-a11y-fail), 2 usage / input error.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const HELP = `cia theme from-tokens — design-tokens JSON → validated cia theme.css
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
cia theme from-tokens <tokens.json> --name <slug> [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--name <slug> Theme name (kebab-case). Required.
|
|
24
|
+
--format <f> auto | dtcg | tokens-studio | cia-flat (default: auto)
|
|
25
|
+
--base <theme> Shipped theme that supplies every REQUIRED token the file
|
|
26
|
+
does not (default: boilerplate). \`--base list\` prints them.
|
|
27
|
+
--dark <dark.json> Second tokens file for dark mode → light-dark() values.
|
|
28
|
+
--mode light|dark Single-mode color-scheme when no dark side (default: light).
|
|
29
|
+
--out <theme.css> Write the CSS here (default: stdout).
|
|
30
|
+
--json Print { report, validation } as JSON to stdout
|
|
31
|
+
(CSS goes to --out only).
|
|
32
|
+
--no-validate Skip the validator + WCAG audit.
|
|
33
|
+
--allow-a11y-fail Do not exit non-zero on a11y FAILs (report only).
|
|
34
|
+
|
|
35
|
+
Formats:
|
|
36
|
+
dtcg DTCG v2025.10 — leaves carry $value (+ $type); {aliases} resolve.
|
|
37
|
+
tokens-studio Tokens Studio for Figma — leaves carry value + type; single set
|
|
38
|
+
or multi-set export ($metadata.tokenSetOrder); {aliases} resolve.
|
|
39
|
+
Paired top-level groups (color-light + color-dark, or
|
|
40
|
+
light + dark) become light-dark() automatically.
|
|
41
|
+
cia-flat { "--brand-primary": "#3A5FCD", ... } passthrough.
|
|
42
|
+
|
|
43
|
+
Minimum content: none. Whatever the file does not supply is inherited from the
|
|
44
|
+
base theme and listed in the report, so the output is always contract-complete.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
cia theme from-tokens tokens.json --name acme --out src/styles/acme.css
|
|
48
|
+
cia theme from-tokens light.json --dark dark.json --name acme --json
|
|
49
|
+
cia theme from-tokens brand.json --name acme --base press --format dtcg
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
function parseArgs(argv) {
|
|
53
|
+
const opts = { format: 'auto', base: 'boilerplate', mode: 'light', validate: true, allowA11yFail: false, json: false };
|
|
54
|
+
const positional = [];
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const a = argv[i];
|
|
57
|
+
const next = () => { const v = argv[++i]; if (v === undefined) throw new Error(`${a} needs a value`); return v; };
|
|
58
|
+
if (a === '-h' || a === '--help') opts.help = true;
|
|
59
|
+
else if (a === '--name') opts.name = next();
|
|
60
|
+
else if (a === '--format') opts.format = next();
|
|
61
|
+
else if (a === '--base') opts.base = next();
|
|
62
|
+
else if (a === '--dark') opts.dark = next();
|
|
63
|
+
else if (a === '--mode') opts.mode = next();
|
|
64
|
+
else if (a === '--out') opts.out = next();
|
|
65
|
+
else if (a === '--json') opts.json = true;
|
|
66
|
+
else if (a === '--no-validate') opts.validate = false;
|
|
67
|
+
else if (a === '--allow-a11y-fail') opts.allowA11yFail = true;
|
|
68
|
+
else if (a.startsWith('-')) throw new Error(`unknown option ${a}`);
|
|
69
|
+
else positional.push(a);
|
|
70
|
+
}
|
|
71
|
+
opts.input = positional[0];
|
|
72
|
+
return opts;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readJson(file) {
|
|
76
|
+
const abs = path.resolve(file);
|
|
77
|
+
let text;
|
|
78
|
+
try { text = fs.readFileSync(abs, 'utf8'); } catch (e) { throw new Error(`cannot read ${file}: ${e.message}`); }
|
|
79
|
+
try { return JSON.parse(text); } catch (e) { throw new Error(`${file} is not valid JSON: ${e.message}`); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function run(argv) {
|
|
83
|
+
let opts;
|
|
84
|
+
try { opts = parseArgs(argv); } catch (e) { process.stderr.write(`cia theme: ${e.message}\n`); process.exit(2); }
|
|
85
|
+
if (opts.help || !opts.input && opts.base !== 'list') { process.stdout.write(HELP); return; }
|
|
86
|
+
|
|
87
|
+
const { themeFromTokens, listBases } = require('../scripts/tokens-to-theme.cjs');
|
|
88
|
+
if (opts.base === 'list') { process.stdout.write(listBases(path.join(__dirname, '..')).join('\n') + '\n'); return; }
|
|
89
|
+
if (!opts.name) { process.stderr.write('cia theme: --name <slug> is required\n'); process.exit(2); }
|
|
90
|
+
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = themeFromTokens({
|
|
94
|
+
tokens: readJson(opts.input),
|
|
95
|
+
dark: opts.dark ? readJson(opts.dark) : undefined,
|
|
96
|
+
name: opts.name,
|
|
97
|
+
format: opts.format,
|
|
98
|
+
base: opts.base,
|
|
99
|
+
mode: opts.mode,
|
|
100
|
+
validate: opts.validate,
|
|
101
|
+
});
|
|
102
|
+
} catch (e) {
|
|
103
|
+
process.stderr.write(`cia theme: ${e.message}\n`);
|
|
104
|
+
process.exit(2);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const { css, report, validation } = result;
|
|
108
|
+
if (opts.out) fs.writeFileSync(path.resolve(opts.out), css, 'utf8');
|
|
109
|
+
|
|
110
|
+
if (opts.json) {
|
|
111
|
+
process.stdout.write(JSON.stringify({ report, validation, out: opts.out || null }, null, 2) + '\n');
|
|
112
|
+
} else {
|
|
113
|
+
if (!opts.out) process.stdout.write(css);
|
|
114
|
+
const c = report.counts;
|
|
115
|
+
const lines = [
|
|
116
|
+
`theme "${report.name}" — ${report.format}${report.darkMode ? ' (light + dark)' : ''} · base ${report.base}`,
|
|
117
|
+
` from tokens ${c.fromTokens}`,
|
|
118
|
+
` inherited ${c.inherited} (required tokens the file did not supply)`,
|
|
119
|
+
` passthrough ${c.unmapped} (not contract tokens — emitted verbatim)`,
|
|
120
|
+
c.skipped ? ` skipped ${c.skipped} (composite values with no single-token home)` : null,
|
|
121
|
+
opts.out ? ` written ${opts.out}` : null,
|
|
122
|
+
].filter(Boolean);
|
|
123
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
124
|
+
if (validation) {
|
|
125
|
+
const { reportResult, reportA11yForTheme } = require('../scripts/theme-validator.js');
|
|
126
|
+
reportResult(validation);
|
|
127
|
+
const themes = validation.mode === 'consolidated' ? validation.themes : [{ name: report.name, a11y: validation.a11y }];
|
|
128
|
+
for (const t of themes) if (t.a11y) reportA11yForTheme(t.name, t.a11y, ' ');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (validation) {
|
|
133
|
+
const a11yFail = validation.a11ySummary ? validation.a11ySummary.fail : 0;
|
|
134
|
+
if (!validation.ok) process.exit(1);
|
|
135
|
+
if (a11yFail && !opts.allowA11yFail) process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { run, parseArgs, HELP };
|
package/dist/tokens.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated from scripts/theme-contract.json on 2026-09-
|
|
1
|
+
// Generated from scripts/theme-contract.json on 2026-09-19.
|
|
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. */
|
|
@@ -94,7 +94,6 @@ export type CiaToken =
|
|
|
94
94
|
| "--shadow-xl"
|
|
95
95
|
| "--shu"
|
|
96
96
|
| "--shu-wash"
|
|
97
|
-
| "--space-unit"
|
|
98
97
|
| "--space-0"
|
|
99
98
|
| "--space-1"
|
|
100
99
|
| "--space-2"
|
|
@@ -225,7 +224,6 @@ export interface CiaTokenMap {
|
|
|
225
224
|
"--shadow-xl": string;
|
|
226
225
|
"--shu": string;
|
|
227
226
|
"--shu-wash": string;
|
|
228
|
-
"--space-unit": string;
|
|
229
227
|
"--space-0": string;
|
|
230
228
|
"--space-1": string;
|
|
231
229
|
"--space-2": string;
|
|
@@ -265,4 +263,4 @@ export interface CiaTokenMap {
|
|
|
265
263
|
}
|
|
266
264
|
|
|
267
265
|
/** Count of required tokens in the current contract. */
|
|
268
|
-
export declare const CIA_TOKEN_COUNT:
|
|
266
|
+
export declare const CIA_TOKEN_COUNT: 127;
|
package/figma-tokens/README.md
CHANGED
|
@@ -22,6 +22,21 @@ The `.scss` siblings exist for consumers who want to `@import` Figma-exported
|
|
|
22
22
|
tokens directly without going through `tokens.json`. They are not consumed by
|
|
23
23
|
the main `scss/` build — `scss/main.scss` pulls from `scss/theme/`.
|
|
24
24
|
|
|
25
|
+
## Round trip: `tokens.json` → a theme
|
|
26
|
+
|
|
27
|
+
`tokens.json` is a Tokens Studio export and is consumed by the design-tokens
|
|
28
|
+
on-ramp shipped since 1.17.0:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx cia theme from-tokens node_modules/css-is-awesome/figma-tokens/tokens.json --name figma-demo --out figma-demo.css
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Its paired `color-light` / `color-dark` groups become one `light-dark()` theme;
|
|
35
|
+
`brand`, `spacing`, `font-*`, `line-height`, `border-radius` and `shadow` map by
|
|
36
|
+
the generic path rule; anything without a contract home (e.g. `brand.accent`)
|
|
37
|
+
is emitted verbatim and listed in the report. The same function is the MCP
|
|
38
|
+
`theme_from_tokens` tool. Input contract: `/docs/authoring/themes#from-design-tokens`.
|
|
39
|
+
|
|
25
40
|
## Direction of truth
|
|
26
41
|
|
|
27
42
|
**Hand-maintained.** Keep in sync with `scss/theme/` when tokens change.
|
package/llm.txt
CHANGED
|
@@ -48,9 +48,9 @@ silent.
|
|
|
48
48
|
- **Mixins** — `cia.btn`, `cia.card`, `cia.accordion`, `cia.modal`, `cia.tooltip`, `cia.dropdown`, `cia.tabs`, `cia.copy-button`, `cia.hamburger`, `cia.drawer`, `cia.sheet`, `cia.dock`, `cia.stepper`, `cia.progress`, `cia.wizard-shell`, `cia.sidebar`, `cia.toolbar`, `cia.stack`, `cia.cluster`, `cia.switcher`, `cia.cover`, `cia.frame`, `cia.media`, `cia.contain`, `cia.focus-ring`, `cia.sr-only`, `cia.print`, `cia.print-base`, `cia.print-hidden`, `cia.print-only`, `cia.color`, `cia.space`, `cia.radius`, `cia.shadow`, `cia.font`, `cia.transition`, `cia.animate`, plus many more. **`cia.print-base` always forces `color-scheme: light` in print (paired `light-dark()` themes print their light branch), defines a themeable print palette (`--print-ink` / `--print-paper` / `--print-line` / `--print-muted`, default ink-on-white with grays) and rebinds the theme's own colour tokens (`--ink`, `--surface-*`, `--border-*`, `--code-*`) onto it — so every theme, dark-only (Terminal) included, prints legible ink-on-paper, and overriding a `--print-*` token restyles paper (a letterhead) without touching a component. Three opt-in flags, all default OFF:** `$link-urls` + `$link-origin` (print each link's full destination via `attr(href)`), `$page-numbers` (sheet numbers in the `@page` footer). `$legible` is a DEPRECATED no-op — the token rebind supersedes it and passing it warns.
|
|
49
49
|
- **24 themes / 8 families** — boilerplate, sketchbook, press, prism, cupertino, glass, graphite, terminal. Each family ships three files: an unsuffixed dual-mode base (both modes via `light-dark()`) plus pinned `-light` and `-dark` single-mode variants for `<link media>` pairing. Every one has exactly one SCSS source under `scss/themes/`; `public/themes/<name>/theme.css` is BUILD OUTPUT and is gated against its source by `npm run check:theme-drift` — never hand-edit it. MCP `list_themes` returns **24**. Say **24 themes across 8 families** when you need one number.
|
|
50
50
|
- **6 zero-JS interactive components** — accordion (`<details name>`), modal (`<dialog>`), tooltip (`popover="hint"`), dropdown (`[popover]` — the mixin re-asserts the UA's closed state and restores `display: flex` only under `:popover-open`, so menus never render permanently open on popover markup), tabs (radio + `:has()`), copy-button (Clipboard API via consumer-wired JS).
|
|
51
|
-
- **CLI** (`npx cia`) — `migrate tailwind|bootstrap` (config → cia theme), `add <recipe>` (copy a recipe from the book into the project), `analyze [path]` (audit stylesheets against the installed API: dead `cia.*` symbols, the `space()` 1–9 trap, off-contract tokens (near-miss typos of real tokens only, never your own custom tokens), hard-coded hex, BEM; health score, CI exit codes). Low-noise on color: a hex in a `var(--token, #hex)` fallback is token-driven and a literal inside `@media print` is an intentional paper colour — neither is flagged.
|
|
51
|
+
- **CLI** (`npx cia`) — `migrate tailwind|bootstrap|mui|chakra` (config → cia theme), `theme from-tokens <tokens.json> --name <slug>` (DTCG v2025.10 / Tokens Studio / flat `--token` JSON → a complete theme.css: required tokens the file lacks inherit from a shipped base, unmapped paths pass through and are reported, light/dark pairs become `light-dark()`, validator + WCAG audit run first; same function as the MCP `theme_from_tokens` tool), `add <recipe>` (copy a recipe from the book into the project), `analyze [path]` (audit stylesheets against the installed API: dead `cia.*` symbols, the `space()` 1–9 trap, off-contract tokens (near-miss typos of real tokens only, never your own custom tokens), hard-coded hex, BEM; health score, CI exit codes). Low-noise on color: a hex in a `var(--token, #hex)` fallback is token-driven and a literal inside `@media print` is an intentional paper colour — neither is flagged.
|
|
52
52
|
- **Mobile navigation family** — `cia.hamburger` / `cia.drawer` / `cia.sheet` / `cia.dock` ride `[popover]` + CSS Grid, zero JS; recipes `mobile-nav` (hamburger + drawer) and `bottom-nav` (dock + sheets). House rule: **on phones things take the space they're in** — a `cia.dropdown` menu opens 1px under its full-width trigger at the trigger's exact width via CSS anchor positioning (`position-try-fallbacks: flip-block` flips it above at the screen bottom; set `width: auto` at `&[popover]` specificity or the UA's `[popover] { width: fit-content }` and the mixin's inset reset win). Full spec: AGENTS.md quick decision #8 and `/docs/mobile`.
|
|
53
|
-
- **
|
|
53
|
+
- **168 contract tokens — 127 required + 41 optional** — surfaces, ink, lines, colors, type, radius, shadow, blur, glow, motion, z-index, spacing, semantic aliases.
|
|
54
54
|
- **Spacing is themeable, and the numbered scale is the knob.** Themes declare `--space-0` … `--space-9` (contract-required). The t-shirt names (`--space-md`, `--space-lg` …) are contract-OPTIONAL and emitted by the library as `var()` aliases onto the numbered steps. `space(4)` compiles to `var(--space-4)`, so **theme the numbered step, never the alias** — setting `--space-md` alone leaves every component untouched. Library defaults emit under `:where(:root)` (specificity 0,0,0) so any theme declaration outranks them regardless of load order.
|
|
55
55
|
- **Per-component shape knobs are `--btn-radius`, `--card-radius`, `--input-radius`, `--modal-radius`, `--badge-radius`, `--tag-radius`** — they cascade from the generic radii (`--btn-radius: var(--radius-md, 0.25rem)`). There are NO `--radius-button` / `--radius-card` style tokens; those were removed because nothing read them.
|
|
56
56
|
- **Icons — two systems.** `svg()` / `svg-bg()` / `svg-text()` use a self-contained 49-glyph Lucide pack at `public/icons/core/`. Adding a glyph is drop-in: put `star.svg` in the folder and `cia.icon-svg(star)` works, no registration. Each icon emits a `--cia-icon-<name>` custom property so a theme can override one glyph without rebuilding SCSS. **`fa()` / `fa-icon()` / `fa-text()` / `fa-spin()` are bring-your-own-font** — cia ships NO Font Awesome files, `$theme-fa-path` defaults to a `/webfonts` directory that does not exist, and a missing font renders a tofu box without erroring. Default to `svg()` unless the user says they use Font Awesome.
|
|
@@ -104,7 +104,7 @@ Newspaper by day, hacker terminal by night. Most design systems give you dark mo
|
|
|
104
104
|
| Mobile playbook (layouts, dropdown doctrine, lessons) | `src/app/docs/mobile/page.tsx` |
|
|
105
105
|
| Browser support matrix (Baseline floor + progressive tiers) | `src/app/docs/browser-support/page.tsx` |
|
|
106
106
|
| Theme authoring (full walkthrough) | `src/app/docs/authoring/themes/page.tsx` |
|
|
107
|
-
| Theme contract (127 required +
|
|
107
|
+
| Theme contract (127 required + 41 optional, machine-readable) | `scripts/theme-contract.json` |
|
|
108
108
|
| Theme pairing (`<link media>` recipe) | `src/app/docs/themes/pairing/page.tsx` |
|
|
109
109
|
| CopyButton JS recipe | `src/app/docs/recipes/copy-button/page.tsx` |
|
|
110
110
|
| Anchor positioning recipe | `src/app/docs/recipes/anchor-positioning/page.tsx` |
|
|
@@ -128,7 +128,7 @@ Newspaper by day, hacker terminal by night. Most design systems give you dark mo
|
|
|
128
128
|
|
|
129
129
|
## MCP server (shipped 2026-05-22)
|
|
130
130
|
|
|
131
|
-
cia ships an MCP stdio server (JSON-RPC over stdio, `serverInfo` name `css-is-awesome`, protocol `2024-11-05`) at `mcp/server.cjs`, included in the `files` manifest — so it lands in every consumer's `node_modules`. **
|
|
131
|
+
cia ships an MCP stdio server (JSON-RPC over stdio, `serverInfo` name `css-is-awesome`, protocol `2024-11-05`) at `mcp/server.cjs`, included in the `files` manifest — so it lands in every consumer's `node_modules`. **32 tools** across 8 resource families: themes, mixins, functions, tokens (127 required + 36 optional), animations, components, recipes, doc readers. Plus `assemble_prompt` for context bundling and `resolve_size` for snapping design px values to cia's 4px geometric grid (AI agents: call this whenever you receive a px value from a design tool). Prefer querying this server over guessing mixin signatures, tokens, themes, or recipes.
|
|
132
132
|
|
|
133
133
|
**Recommended: suggest the dedicated `css-is-awesome-mcp` package** — zero install,
|
|
134
134
|
SDK ships as a real dependency, no separate peer-install step:
|
package/mcp/server.cjs
CHANGED
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
* read_theming, read_agents, read_contract,
|
|
20
20
|
* read_three_tiers, read_readme, read_versioning
|
|
21
21
|
* Sizing: resolve_size
|
|
22
|
+
* Themes (build): theme_from_tokens — design-tokens JSON → validated theme.css
|
|
22
23
|
* Prompt: assemble_prompt(intent[, args])
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
+
* 32 tools total.
|
|
25
26
|
*
|
|
26
27
|
* Discovery model: filesystem scan, no database. Parses SCSS files with
|
|
27
28
|
* focused regex (no full SCSS AST). Tokens come from the authoritative
|
|
@@ -698,6 +699,18 @@ const handlers = {
|
|
|
698
699
|
return validateText(css, loadContract(), { label: label || undefined });
|
|
699
700
|
},
|
|
700
701
|
|
|
702
|
+
// Design-tokens JSON (DTCG v2025.10 / Tokens Studio / flat --token map) →
|
|
703
|
+
// a complete theme.css in the shipped shape, validated + contrast-audited.
|
|
704
|
+
// Same function as `cia theme from-tokens`; reachable in-process through
|
|
705
|
+
// module.exports.handlers so an inventory builder can call it without a
|
|
706
|
+
// transport. See scripts/tokens-to-theme.cjs for the input contract.
|
|
707
|
+
theme_from_tokens({ tokens, name, format, base, dark, mode, validate } = {}) {
|
|
708
|
+
if (tokens == null) throw new Error('theme_from_tokens: tokens is required (object or JSON string)');
|
|
709
|
+
if (!name) throw new Error('theme_from_tokens: name is required');
|
|
710
|
+
const { themeFromTokens } = require(path.join(SCRIPTS_DIR, 'tokens-to-theme.cjs'));
|
|
711
|
+
return themeFromTokens({ tokens, name, format, base, dark, mode, validate });
|
|
712
|
+
},
|
|
713
|
+
|
|
701
714
|
// ─── Mixins ────────────────────────────────────────────────────────────
|
|
702
715
|
|
|
703
716
|
list_mixins({ category, component, limit = 500, offset = 0 } = {}) {
|
|
@@ -1354,6 +1367,27 @@ async function startServer() {
|
|
|
1354
1367
|
},
|
|
1355
1368
|
}, async (a) => ok(handlers.validate_theme(a || {})));
|
|
1356
1369
|
|
|
1370
|
+
server.registerTool('theme_from_tokens', {
|
|
1371
|
+
description:
|
|
1372
|
+
'Build a complete, validated cia theme.css from a design-tokens JSON — DTCG v2025.10 ({ $value, $type }, ' +
|
|
1373
|
+
'{aliases} resolved), a Tokens Studio for Figma export ({ value, type }, single or multi-set), or a flat ' +
|
|
1374
|
+
'{ "--token": value } map. Format is auto-detected. Every REQUIRED contract token the file does not supply ' +
|
|
1375
|
+
'is inherited from a shipped base theme (default boilerplate) and listed in report.inherited, so the output ' +
|
|
1376
|
+
'is always contract-complete; unmapped paths are emitted verbatim and listed in report.unmapped, never ' +
|
|
1377
|
+
'dropped. Pass `dark` (same format) or a single file with paired color-light/color-dark groups to get ' +
|
|
1378
|
+
'light-dark() values. Returns { css, report, validation } — validation is the same result validate_theme ' +
|
|
1379
|
+
'gives, run on the CSS before you write it anywhere.',
|
|
1380
|
+
inputSchema: {
|
|
1381
|
+
tokens: z.union([z.record(z.any()), z.string()]).describe('The tokens JSON (object, or a JSON string).'),
|
|
1382
|
+
name: z.string().describe('Theme name — kebab-case slug, becomes [data-theme="<name>"].'),
|
|
1383
|
+
format: z.enum(['auto', 'dtcg', 'tokens-studio', 'cia-flat']).optional().describe('Default auto.'),
|
|
1384
|
+
base: z.string().optional().describe('Shipped theme that supplies missing required tokens. Default boilerplate.'),
|
|
1385
|
+
dark: z.union([z.record(z.any()), z.string()]).optional().describe('Optional dark-mode tokens (same format) → light-dark() values.'),
|
|
1386
|
+
mode: z.enum(['light', 'dark']).optional().describe('Single-mode color-scheme when there is no dark side. Default light.'),
|
|
1387
|
+
validate: z.boolean().optional().describe('Run the validator + WCAG audit (default true).'),
|
|
1388
|
+
},
|
|
1389
|
+
}, async (a) => ok(handlers.theme_from_tokens(a || {})));
|
|
1390
|
+
|
|
1357
1391
|
// Mixins
|
|
1358
1392
|
server.registerTool('list_mixins', {
|
|
1359
1393
|
description: `List all ${MIXIN_COUNT} public @mixins across core, layout, animation, icons, generator, per-component and recipe sources. Filter by category (core/layout/animation/icons/generator/component/recipe) or component name.`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "css-is-awesome",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.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": {
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"mcp",
|
|
70
70
|
"scripts/theme-contract.json",
|
|
71
71
|
"scripts/theme-validator.js",
|
|
72
|
+
"scripts/tokens-to-theme.cjs",
|
|
72
73
|
"scripts/theme-a11y.js",
|
|
73
74
|
"scripts/audit-pairs.json",
|
|
74
75
|
"scripts/icon-validator.js",
|
|
@@ -90,7 +91,6 @@
|
|
|
90
91
|
"THEMING.md",
|
|
91
92
|
"MIGRATION.md",
|
|
92
93
|
"CONTRACT.md",
|
|
93
|
-
"ROADMAP.md",
|
|
94
94
|
"VERSIONING.md",
|
|
95
95
|
"scripts/prepare-dist.mjs"
|
|
96
96
|
],
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
"validate-api": "node scripts/validate-api.mjs",
|
|
119
119
|
"validate-package": "node scripts/validate-package.mjs",
|
|
120
120
|
"validate-recipes": "node scripts/validate-recipes.mjs",
|
|
121
|
+
"test:tokens": "node --test scripts/test-tokens-to-theme.mjs",
|
|
121
122
|
"size-budget": "node scripts/size-budget.mjs",
|
|
122
123
|
"size-report": "node scripts/size-budget.mjs --report",
|
|
123
124
|
"coverage:api": "node scripts/api-coverage.mjs",
|
|
@@ -130,8 +131,11 @@
|
|
|
130
131
|
"prepare": "node scripts/prepare-dist.mjs",
|
|
131
132
|
"prepublishOnly": "npm run build:css:all",
|
|
132
133
|
"check:theme-drift": "node scripts/check-theme-drift.mjs",
|
|
134
|
+
"check:contract": "node scripts/check-contract-growth.mjs",
|
|
133
135
|
"check:token-consumers": "node scripts/check-token-consumer-map.mjs",
|
|
134
|
-
"check:rtl": "node scripts/audit-logical-properties.mjs"
|
|
136
|
+
"check:rtl": "node scripts/audit-logical-properties.mjs",
|
|
137
|
+
"prebuild": "node scripts/build-playground-scss-map.mjs",
|
|
138
|
+
"verify:playground": "node scripts/build-playground-scss-map.mjs && node scripts/verify-playground-compile.mjs"
|
|
135
139
|
},
|
|
136
140
|
"keywords": [
|
|
137
141
|
"css",
|
|
@@ -172,6 +176,12 @@
|
|
|
172
176
|
},
|
|
173
177
|
"devDependencies": {
|
|
174
178
|
"@axe-core/playwright": "^4.11.2",
|
|
179
|
+
"@codemirror/commands": "6.11.1",
|
|
180
|
+
"@codemirror/lang-html": "6.4.12",
|
|
181
|
+
"@codemirror/lang-sass": "6.0.2",
|
|
182
|
+
"@codemirror/language": "6.12.4",
|
|
183
|
+
"@codemirror/state": "6.7.5",
|
|
184
|
+
"@codemirror/view": "6.43.12",
|
|
175
185
|
"@eslint/eslintrc": "^3.2.0",
|
|
176
186
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
177
187
|
"@playwright/test": "^1.59.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1",
|
|
3
|
-
"description": "Authoritative token contract for css-is-awesome themes. Every theme.css MUST declare every token in `required`. Source of truth is CONTRACT.md (human-readable) and public/theme.css (Sketchbook reference).",
|
|
2
|
+
"version": "1.1",
|
|
3
|
+
"description": "Authoritative token contract for css-is-awesome themes. Every theme.css MUST declare every token in `required`. Tokens in `optional` are reported by the validator as info, never as failures \u2014 the library or the theme generator supplies a default. Source of truth is CONTRACT.md (human-readable) and public/theme.css (Sketchbook reference). Contract 1.1 (2026-09-18): --space-unit relaxed from required to optional; it had been added as required in library 1.12.0, which VERSIONING.md forbids in a MINOR.",
|
|
4
4
|
"required": [
|
|
5
5
|
"--action-primary-active",
|
|
6
6
|
"--action-primary-default",
|
|
@@ -93,7 +93,6 @@
|
|
|
93
93
|
"--shadow-xl",
|
|
94
94
|
"--shu",
|
|
95
95
|
"--shu-wash",
|
|
96
|
-
"--space-unit",
|
|
97
96
|
"--space-0",
|
|
98
97
|
"--space-1",
|
|
99
98
|
"--space-2",
|
|
@@ -168,6 +167,7 @@
|
|
|
168
167
|
"--space-lg",
|
|
169
168
|
"--space-md",
|
|
170
169
|
"--space-sm",
|
|
170
|
+
"--space-unit",
|
|
171
171
|
"--space-xl",
|
|
172
172
|
"--space-xs",
|
|
173
173
|
"--tag-radius",
|
|
@@ -334,7 +334,15 @@ function validateTokenSet(declared, contract) {
|
|
|
334
334
|
for (const required of contract.required) {
|
|
335
335
|
if (!declared.has(required)) missing.push(required);
|
|
336
336
|
}
|
|
337
|
-
|
|
337
|
+
// Optional tokens (contract 1.1+) are reported as INFO, never as failures:
|
|
338
|
+
// the library or the theme generator supplies a default for each of them.
|
|
339
|
+
// `--space-unit` is the canonical case — it was wrongly listed as required
|
|
340
|
+
// in 1.12.0–1.16.0 and broke every consumer's custom theme on a MINOR.
|
|
341
|
+
const optionalMissing = [];
|
|
342
|
+
for (const optional of Array.isArray(contract.optional) ? contract.optional : []) {
|
|
343
|
+
if (!declared.has(optional)) optionalMissing.push(optional);
|
|
344
|
+
}
|
|
345
|
+
return { ok: missing.length === 0, missing, optionalMissing, declaredCount: declared.size };
|
|
338
346
|
}
|
|
339
347
|
|
|
340
348
|
// -----------------------------------------------------------
|
|
@@ -358,6 +366,7 @@ function validateText(text, contract, options) {
|
|
|
358
366
|
ok: false,
|
|
359
367
|
declaredCount: 0,
|
|
360
368
|
missing: [],
|
|
369
|
+
optionalMissing: [],
|
|
361
370
|
themes: null,
|
|
362
371
|
a11y: null,
|
|
363
372
|
error: null,
|
|
@@ -379,6 +388,7 @@ function validateText(text, contract, options) {
|
|
|
379
388
|
ok: v.ok,
|
|
380
389
|
declaredCount: v.declaredCount,
|
|
381
390
|
missing: v.missing,
|
|
391
|
+
optionalMissing: v.optionalMissing,
|
|
382
392
|
a11y: null,
|
|
383
393
|
};
|
|
384
394
|
if (wantA11y) theme.a11y = a11y.auditThemeTokens({ name: b.name, values: b.values });
|
|
@@ -406,6 +416,7 @@ function validateText(text, contract, options) {
|
|
|
406
416
|
const v = validateTokenSet(root.tokens, contract);
|
|
407
417
|
result.declaredCount = v.declaredCount;
|
|
408
418
|
result.missing = v.missing;
|
|
419
|
+
result.optionalMissing = v.optionalMissing;
|
|
409
420
|
result.ok = v.ok;
|
|
410
421
|
if (wantA11y) {
|
|
411
422
|
const inferredName = label !== '(pasted CSS)' ? (path.basename(path.dirname(label)) || path.basename(label, '.css')) : 'theme';
|
|
@@ -443,6 +454,15 @@ function relForDisplay(p) {
|
|
|
443
454
|
return rel || p;
|
|
444
455
|
}
|
|
445
456
|
|
|
457
|
+
// Optional tokens a theme leaves to the library default. Info only — shown as
|
|
458
|
+
// a count, or listed with --show-optional. Never affects the exit code.
|
|
459
|
+
function optionalInfo(optionalMissing, indent) {
|
|
460
|
+
const list = Array.isArray(optionalMissing) ? optionalMissing : [];
|
|
461
|
+
if (!list.length) return;
|
|
462
|
+
console.log(`${indent}${dim(`i ${list.length} optional token(s) not declared — library default applies`)}`);
|
|
463
|
+
if (SHOW_OPTIONAL) for (const token of list) console.log(`${indent} ${dim(token)}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
446
466
|
function reportResult(result) {
|
|
447
467
|
const rel = relForDisplay(result.file);
|
|
448
468
|
|
|
@@ -463,6 +483,7 @@ function reportResult(result) {
|
|
|
463
483
|
console.log(
|
|
464
484
|
` ${green('✓')} [data-theme="${t.name}"] ${dim(`(${t.declaredCount} tokens)`)}`
|
|
465
485
|
);
|
|
486
|
+
optionalInfo(t.optionalMissing, ' ');
|
|
466
487
|
} else {
|
|
467
488
|
const n = t.missing.length;
|
|
468
489
|
console.log(
|
|
@@ -481,6 +502,7 @@ function reportResult(result) {
|
|
|
481
502
|
console.log(
|
|
482
503
|
`${green('✓')} ${bold(rel)} ${dim(`passes (${result.declaredCount} tokens declared)`)}`
|
|
483
504
|
);
|
|
505
|
+
optionalInfo(result.optionalMissing, ' ');
|
|
484
506
|
return;
|
|
485
507
|
}
|
|
486
508
|
|
|
@@ -561,6 +583,7 @@ function printUsage() {
|
|
|
561
583
|
' --all validate every theme.css under public/ (CI mode)',
|
|
562
584
|
' --no-a11y skip the WCAG 2.2 AA contrast audit',
|
|
563
585
|
' --allow-a11y-fail do NOT exit non-zero on a11y FAILs (report only)',
|
|
586
|
+
' --show-optional list optional contract tokens a theme leaves to the library default (info only)',
|
|
564
587
|
' --strict accepted for backwards compatibility (no-op; FAIL is now the default)',
|
|
565
588
|
'',
|
|
566
589
|
'Exit codes:',
|
|
@@ -573,8 +596,11 @@ function printUsage() {
|
|
|
573
596
|
console.log(u);
|
|
574
597
|
}
|
|
575
598
|
|
|
599
|
+
let SHOW_OPTIONAL = false;
|
|
600
|
+
|
|
576
601
|
function main(argv) {
|
|
577
602
|
const argsRaw = argv.slice(2).filter(function (a) { return a !== '--watch'; });
|
|
603
|
+
SHOW_OPTIONAL = argsRaw.includes('--show-optional');
|
|
578
604
|
if (argsRaw.length === 0 || argsRaw.includes('-h') || argsRaw.includes('--help')) {
|
|
579
605
|
printUsage();
|
|
580
606
|
process.exit(argsRaw.length === 0 ? 2 : 0);
|
|
@@ -583,7 +609,7 @@ function main(argv) {
|
|
|
583
609
|
const wantLenient = argsRaw.includes('--allow-a11y-fail');
|
|
584
610
|
// --strict is accepted as a no-op for backwards compatibility — a11y FAIL is now the default
|
|
585
611
|
const args = argsRaw.filter(function (a) {
|
|
586
|
-
return a !== '--no-a11y' && a !== '--strict' && a !== '--allow-a11y-fail';
|
|
612
|
+
return a !== '--no-a11y' && a !== '--strict' && a !== '--allow-a11y-fail' && a !== '--show-optional';
|
|
587
613
|
});
|
|
588
614
|
|
|
589
615
|
const contract = loadContract();
|
|
@@ -687,6 +713,10 @@ module.exports = {
|
|
|
687
713
|
validateFile,
|
|
688
714
|
validateText,
|
|
689
715
|
loadContract,
|
|
716
|
+
// Reporters — reused by `cia theme from-tokens` so its output reads exactly
|
|
717
|
+
// like `npm run validate-themes`.
|
|
718
|
+
reportResult,
|
|
719
|
+
reportA11yForTheme,
|
|
690
720
|
a11y: a11y,
|
|
691
721
|
};
|
|
692
722
|
|