css-is-awesome 1.13.1 → 1.14.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,17 @@
1
+ # [1.14.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.13.2...v1.14.0) (2026-09-11)
2
+
3
+
4
+ ### Features
5
+
6
+ * **mcp:** add validate_theme tool — the real validator, exposed as a tool call ([7a8b497](https://github.com/Jerry2d3d/css-is-awesome/commit/7a8b4971640f6fd6b13148f05288e9e3e76ade10))
7
+
8
+ ## [1.13.2](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.13.1...v1.13.2) (2026-09-11)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **themes:** fix theme-editor import losing the other mode's data (take 2) ([2d90aa9](https://github.com/Jerry2d3d/css-is-awesome/commit/2d90aa9cadfaae9ca285bcfbc8e668d81744b573))
14
+
1
15
  ## [1.13.1](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.13.0...v1.13.1) (2026-09-11)
2
16
 
3
17
 
package/mcp/server.cjs CHANGED
@@ -685,6 +685,19 @@ const handlers = {
685
685
  return { total: matches.length, items: matches };
686
686
  },
687
687
 
688
+ // Validates ANY theme CSS against the real token contract + a11y audit —
689
+ // not scoped to cia's own shipped themes. Works on cia's themes, a fully
690
+ // custom one built by an agent (e.g. via the derive-theme assemble_prompt
691
+ // intent), anything — whatever CSS text is passed in. Reuses the same
692
+ // validateText() the CLI (`npm run validate-themes`) calls, so a pass/fail
693
+ // here is exactly what `node scripts/theme-validator.js` would report,
694
+ // not a reimplementation that could quietly drift from it.
695
+ validate_theme({ css, label } = {}) {
696
+ if (!css || typeof css !== 'string') throw new Error('validate_theme: css is required');
697
+ const { validateText, loadContract } = require(path.join(SCRIPTS_DIR, 'theme-validator.js'));
698
+ return validateText(css, loadContract(), { label: label || undefined });
699
+ },
700
+
688
701
  // ─── Mixins ────────────────────────────────────────────────────────────
689
702
 
690
703
  list_mixins({ category, component, limit = 500, offset = 0 } = {}) {
@@ -1327,6 +1340,20 @@ async function startServer() {
1327
1340
  },
1328
1341
  }, async (a) => ok(handlers.search_themes(a || {})));
1329
1342
 
1343
+ server.registerTool('validate_theme', {
1344
+ description:
1345
+ 'Validate ANY theme CSS against cia\'s real token contract and WCAG contrast audit — the same check ' +
1346
+ '`npm run validate-themes` runs, exposed as a tool call. Not scoped to cia\'s own themes: works on a ' +
1347
+ 'fully custom theme you (or another agent) just built, e.g. via the derive-theme assemble_prompt intent. ' +
1348
+ 'Pass compiled CSS (a :root or [data-theme="..."] block) — this does not compile Sass, so give it the ' +
1349
+ 'output, not .scss source. Returns missing required tokens (if any, mode "per-file" or "consolidated" ' +
1350
+ 'depending on shape) and a11y warnings per contrast pair.',
1351
+ inputSchema: {
1352
+ css: z.string().describe('Compiled theme CSS to validate — the :root/[data-theme] block(s), not .scss source.'),
1353
+ label: z.string().optional().describe('Optional name for the result (e.g. the intended theme name); purely cosmetic.'),
1354
+ },
1355
+ }, async (a) => ok(handlers.validate_theme(a || {})));
1356
+
1330
1357
  // Mixins
1331
1358
  server.registerTool('list_mixins', {
1332
1359
  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.13.1",
3
+ "version": "1.14.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": {
@@ -338,15 +338,22 @@ function validateTokenSet(declared, contract) {
338
338
  }
339
339
 
340
340
  // -----------------------------------------------------------
341
- // Validate a single file. Auto-detects per-file vs consolidated.
341
+ // Validate raw CSS text. Auto-detects per-file vs consolidated.
342
342
  // Returns a unified result shape with either a flat pass/fail
343
343
  // (per-file) or a list of per-theme pass/fails (consolidated).
344
+ //
345
+ // `options.label` names the result (shown as `result.file`) and, in the
346
+ // legacy per-file branch, seeds the a11y report's inferred theme name —
347
+ // there's no real file path when validating text handed over directly
348
+ // (e.g. from an MCP tool call), so callers that have one should pass it
349
+ // as label; callers that don't get '(pasted CSS)'.
344
350
  // -----------------------------------------------------------
345
- function validateFile(filePath, contract, options) {
351
+ function validateText(text, contract, options) {
346
352
  const opts = options || {};
347
353
  const wantA11y = opts.a11y !== false;
354
+ const label = opts.label || '(pasted CSS)';
348
355
  const result = {
349
- file: filePath,
356
+ file: label,
350
357
  mode: 'per-file',
351
358
  ok: false,
352
359
  declaredCount: 0,
@@ -356,14 +363,6 @@ function validateFile(filePath, contract, options) {
356
363
  error: null,
357
364
  };
358
365
 
359
- let text;
360
- try {
361
- text = fs.readFileSync(filePath, 'utf8');
362
- } catch (err) {
363
- result.error = err.message;
364
- return result;
365
- }
366
-
367
366
  if (isConsolidated(text)) {
368
367
  result.mode = 'consolidated';
369
368
  const blocks = extractDataThemeBlocks(text);
@@ -409,12 +408,33 @@ function validateFile(filePath, contract, options) {
409
408
  result.missing = v.missing;
410
409
  result.ok = v.ok;
411
410
  if (wantA11y) {
412
- const inferredName = path.basename(path.dirname(filePath)) || path.basename(filePath, '.css');
411
+ const inferredName = label !== '(pasted CSS)' ? (path.basename(path.dirname(label)) || path.basename(label, '.css')) : 'theme';
413
412
  result.a11y = a11y.auditThemeTokens({ name: inferredName, values: root.values });
414
413
  }
415
414
  return result;
416
415
  }
417
416
 
417
+ // Reads a file and delegates to validateText, with the file path as the
418
+ // result's label (and a11y's inferred-name source in the legacy branch).
419
+ function validateFile(filePath, contract, options) {
420
+ let text;
421
+ try {
422
+ text = fs.readFileSync(filePath, 'utf8');
423
+ } catch (err) {
424
+ return {
425
+ file: filePath,
426
+ mode: 'per-file',
427
+ ok: false,
428
+ declaredCount: 0,
429
+ missing: [],
430
+ themes: null,
431
+ a11y: null,
432
+ error: err.message,
433
+ };
434
+ }
435
+ return validateText(text, contract, Object.assign({}, options, { label: filePath }));
436
+ }
437
+
418
438
  // -----------------------------------------------------------
419
439
  // Reporter
420
440
  // -----------------------------------------------------------
@@ -665,6 +685,7 @@ module.exports = {
665
685
  extractDataThemeBlocks,
666
686
  isConsolidated,
667
687
  validateFile,
688
+ validateText,
668
689
  loadContract,
669
690
  a11y: a11y,
670
691
  };