@jay-framework/design-system-validator 0.21.0 → 0.22.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.
@@ -0,0 +1,222 @@
1
+ # Design System (DESIGN.md)
2
+
3
+ The project uses a `DESIGN.md` file to define design tokens — colors, typography, spacing, rounded corners, and component specs. The validator enforces these tokens across all `.jay-html` pages.
4
+
5
+ ## How It Works
6
+
7
+ Place a `DESIGN.md` alongside your pages. It applies to that route and all child routes unless a child has its own `DESIGN.md`.
8
+
9
+ ```
10
+ src/pages/
11
+ DESIGN.md ← applies to all routes
12
+ products/
13
+ DESIGN.md ← overrides for /products and children
14
+ admin/
15
+ DESIGN.md ← separate system for /admin
16
+ ```
17
+
18
+ ## Token Format
19
+
20
+ DESIGN.md uses YAML frontmatter following the [DESIGN.md spec](https://github.com/google-labs-code/design.md):
21
+
22
+ ```yaml
23
+ ---
24
+ colors:
25
+ primary: '#2563eb'
26
+ text: '#0f172a'
27
+ background: '#ffffff'
28
+ surface: '#f8fafc'
29
+
30
+ typography:
31
+ headline-lg:
32
+ fontFamily: Inter
33
+ fontSize: 2.5rem
34
+ fontWeight: 700
35
+ lineHeight: 1.2
36
+ body-md:
37
+ fontFamily: Inter
38
+ fontSize: 1rem
39
+ fontWeight: 400
40
+ lineHeight: 1.6
41
+
42
+ spacing:
43
+ sm: 0.5rem
44
+ md: 1rem
45
+ lg: 1.5rem
46
+
47
+ rounded:
48
+ sm: 0.25rem
49
+ md: 0.5rem
50
+ full: 9999px
51
+
52
+ breakpoints:
53
+ mobile: 600px
54
+ tablet: 768px
55
+ desktop: 1024px
56
+
57
+ components:
58
+ button-primary:
59
+ backgroundColor: '{colors.primary}'
60
+ textColor: '{colors.background}'
61
+ rounded: '{rounded.md}'
62
+ jay:product-card:
63
+ backgroundColor: '{colors.surface}'
64
+ rounded: '{rounded.md}'
65
+
66
+ animations:
67
+ fade-in:
68
+ duration: 300ms
69
+ easing: cubic-bezier(0, 0, 0.2, 1)
70
+ slide-up:
71
+ duration: 500ms
72
+ easing: cubic-bezier(0.4, 0, 0.2, 1)
73
+ micro:
74
+ duration: 150ms
75
+ easing: ease-in-out
76
+
77
+ rules:
78
+ max-font-weights: 3
79
+ max-primary-buttons: 1
80
+ require-contrast-aa: true
81
+ ---
82
+ ```
83
+
84
+ Values use `{path.to.token}` references — e.g., `{colors.primary}` resolves to `"#2563eb"`.
85
+
86
+ ## Using Tokens in CSS
87
+
88
+ Use the token values directly in your `<style>` blocks:
89
+
90
+ ```css
91
+ .card {
92
+ background-color: #f8fafc; /* matches {colors.surface} ✓ */
93
+ border-radius: 0.5rem; /* matches {rounded.md} ✓ */
94
+ padding: 1rem; /* matches {spacing.md} ✓ */
95
+ color: #0f172a; /* matches {colors.text} ✓ */
96
+ transition: opacity 300ms cubic-bezier(0, 0, 0.2, 1); /* matches fade-in preset ✓ */
97
+ }
98
+ ```
99
+
100
+ CSS custom properties (`var(--name)`) are resolved statically when defined in `:root`. Unresolvable vars are accepted without validation.
101
+
102
+ ## Breakpoints
103
+
104
+ The `breakpoints` section defines approved responsive breakpoints. Media queries using `max-width` values not in this list are flagged. Breakpoint names are used in validation output for readability.
105
+
106
+ ```yaml
107
+ breakpoints:
108
+ mobile: 600px
109
+ tablet: 768px
110
+ desktop: 1024px
111
+ ```
112
+
113
+ Use only these values in `@media (max-width: ...)` queries:
114
+
115
+ ```css
116
+ @media (max-width: 768px) {
117
+ /* tablet ✓ */
118
+ }
119
+ @media (max-width: 750px) {
120
+ /* not in breakpoints ✗ */
121
+ }
122
+ ```
123
+
124
+ ## Component Specs
125
+
126
+ The `components` section defines expected styles for HTML elements (matched by class) and jay headless components (matched by `jay:` prefix):
127
+
128
+ - `button-primary` → validates elements with class `.button-primary`
129
+ - `jay:product-card` → validates the inline template inside `<jay:product-card>`
130
+
131
+ ## Exceptions
132
+
133
+ When a value intentionally breaks the design system, exempt it:
134
+
135
+ **In CSS** — add a comment after the declaration:
136
+
137
+ ```css
138
+ .hero {
139
+ padding: 7.5rem 0; /* design-system: allow */
140
+ }
141
+ ```
142
+
143
+ **On elements** — add the `jay-design="allow"` attribute:
144
+
145
+ ```html
146
+ <div style="margin-top: 7.5rem" jay-design="allow"></div>
147
+ ```
148
+
149
+ ## Validation Errors
150
+
151
+ These are the errors the validator produces. Use them to align your CSS with the design system.
152
+
153
+ **Hardcoded color:**
154
+
155
+ ```
156
+ ⚠ Hardcoded color "#ff0000" for color not in design system
157
+ Suggestion: Use token {colors.error} ("#dc2626") or add to DESIGN.md
158
+ ```
159
+
160
+ **Off-scale spacing:**
161
+
162
+ ```
163
+ ⚠ padding value "13px" not in spacing scale
164
+ Suggestion: Use a spacing token: {spacing.md} ("1rem"), {spacing.lg} ("1.5rem")
165
+ ```
166
+
167
+ **Off-scale border-radius:**
168
+
169
+ ```
170
+ ⚠ border-radius "10px" not in rounded scale
171
+ Suggestion: Use a rounded token: {rounded.md} ("0.5rem"), {rounded.full} ("9999px")
172
+ ```
173
+
174
+ **Component mismatch:**
175
+
176
+ ```
177
+ ⚠ <jay:product-card> inline template: background-color should be "#f8fafc"
178
+ per jay:product-card component spec, found "#ffffff"
179
+ ```
180
+
181
+ **Too many font weights:**
182
+
183
+ ```
184
+ ⚠ 4 unique font-weight values found (max: 3)
185
+ Suggestion: Reduce to 3 font-weight values from the typography tokens
186
+ ```
187
+
188
+ **Animation duration not in presets:**
189
+
190
+ ```
191
+ ⚠ transition-duration "200ms" not in animation presets
192
+ Suggestion: Use an animation preset duration: fade-in (300ms), slide-up (500ms), micro (150ms)
193
+ ```
194
+
195
+ **Animation easing not in presets:**
196
+
197
+ ```
198
+ ⚠ transition-timing-function "ease" not in animation presets
199
+ Suggestion: Use an animation preset easing: fade-in (cubic-bezier(0, 0, 0.2, 1)), micro (ease-in-out)
200
+ ```
201
+
202
+ **Missing reduced-motion media query:**
203
+
204
+ ```
205
+ ⚠ Page uses transitions/animations but has no @media (prefers-reduced-motion) override
206
+ Suggestion: Add @media (prefers-reduced-motion: reduce) { * { transition-duration: 0s !important; animation-duration: 0s !important; } }
207
+ ```
208
+
209
+ **Non-standard breakpoint:**
210
+
211
+ ```
212
+ ⚠ Media query @media (max-width: 750px) not in DESIGN.md breakpoints
213
+ Suggestion: Use a DESIGN.md breakpoint: mobile (600px), tablet (768px), desktop (1024px)
214
+ ```
215
+
216
+ **Low contrast:**
217
+
218
+ ```
219
+ ⚠ Contrast ratio 2.8:1 below WCAG AA (4.5:1) for color "#94a3b8"
220
+ on background "#f8fafc"
221
+ Suggestion: Darken text color or lighten background
222
+ ```
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { JayHtmlValidatorFn } from '@jay-framework/compiler-shared';
2
- import { PluginReferencesContext, PluginReferencesResult } from '@jay-framework/stack-server-runtime';
2
+ import { PluginAgentKitContext, PluginAgentKitResult } from '@jay-framework/stack-server-runtime';
3
3
 
4
4
  declare const validateTokens: JayHtmlValidatorFn;
5
5
 
@@ -13,9 +13,9 @@ declare const validateContrast: JayHtmlValidatorFn;
13
13
  * References handler for design-system-validator plugin.
14
14
  *
15
15
  * Runs during `jay-stack agent-kit` to generate AIditor add-menu entries
16
- * from the project's DESIGN.md tokens.
16
+ * from the project's DESIGN.md tokens — one item per design token, with HTML previews.
17
17
  */
18
18
 
19
- declare function generateDesignSystemReferences(ctx: PluginReferencesContext): Promise<PluginReferencesResult>;
19
+ declare function generateDesignSystemAgentKit(ctx: PluginAgentKitContext): Promise<PluginAgentKitResult>;
20
20
 
21
- export { generateDesignSystemReferences, validateComponents, validateContrast, validateStructure, validateTokens };
21
+ export { generateDesignSystemAgentKit, validateComponents, validateContrast, validateStructure, validateTokens };
package/dist/index.js CHANGED
@@ -54,6 +54,12 @@ function parseDesignMd(content) {
54
54
  const raw = yaml.load(frontmatter);
55
55
  if (!raw || typeof raw !== "object")
56
56
  return null;
57
+ const rawComponents = {};
58
+ if (raw.components) {
59
+ for (const [name, spec] of Object.entries(raw.components)) {
60
+ rawComponents[name] = { ...spec };
61
+ }
62
+ }
57
63
  resolveReferences(raw);
58
64
  return {
59
65
  name: raw.name,
@@ -61,8 +67,10 @@ function parseDesignMd(content) {
61
67
  typography: raw.typography || {},
62
68
  spacing: normalizeStringValues(raw.spacing),
63
69
  rounded: normalizeStringValues(raw.rounded),
70
+ breakpoints: normalizeStringValues(raw.breakpoints),
64
71
  animations: raw.animations || {},
65
72
  components: raw.components || {},
73
+ rawComponents,
66
74
  rules: raw.rules || {}
67
75
  };
68
76
  }
@@ -122,11 +130,21 @@ function getMediaQuery(rule) {
122
130
  }
123
131
  return void 0;
124
132
  }
125
- function parseRules(css, sourceIndexOffset = 0) {
133
+ function parseRules(css, sourceIndexOffset = 0, customProperties) {
126
134
  const rules = [];
127
135
  let ruleIndex = sourceIndexOffset;
128
136
  const root = postcss.parse(css);
129
137
  root.walkRules((rule) => {
138
+ if (customProperties && !getMediaQuery(rule)) {
139
+ const isRoot = rule.selectors.some((s) => s.trim() === ":root" || s.trim() === "html");
140
+ if (isRoot) {
141
+ rule.walkDecls((decl) => {
142
+ if (decl.prop.startsWith("--")) {
143
+ customProperties.set(decl.prop, decl.value);
144
+ }
145
+ });
146
+ }
147
+ }
130
148
  const declarations = [];
131
149
  rule.walkDecls((decl) => {
132
150
  declarations.push({
@@ -153,6 +171,19 @@ function parseRules(css, sourceIndexOffset = 0) {
153
171
  });
154
172
  return rules;
155
173
  }
174
+ function resolveVarReferences(value, customProperties) {
175
+ return value.replace(
176
+ /var\(\s*(--[a-zA-Z0-9-]+)(?:\s*,\s*([^)]+))?\s*\)/g,
177
+ (_, name, fallback) => {
178
+ const resolved = customProperties.get(name);
179
+ if (resolved !== void 0)
180
+ return resolveVarReferences(resolved, customProperties);
181
+ if (fallback !== void 0)
182
+ return fallback.trim();
183
+ return _;
184
+ }
185
+ );
186
+ }
156
187
  function parseInlineStyles(style) {
157
188
  const declarations = [];
158
189
  for (const part of style.split(";")) {
@@ -167,13 +198,19 @@ function parseInlineStyles(style) {
167
198
  }
168
199
  return declarations;
169
200
  }
170
- function matchesSelector(element, selector, root) {
171
- try {
172
- const matches = root.querySelectorAll(selector);
173
- return matches.some((el) => el === element);
174
- } catch {
175
- return false;
201
+ function buildSelectorCache(rules, root) {
202
+ const cache = /* @__PURE__ */ new Map();
203
+ for (const rule of rules) {
204
+ if (cache.has(rule.selector))
205
+ continue;
206
+ try {
207
+ const matches = root.querySelectorAll(rule.selector);
208
+ cache.set(rule.selector, new Set(matches));
209
+ } catch {
210
+ cache.set(rule.selector, /* @__PURE__ */ new Set());
211
+ }
176
212
  }
213
+ return cache;
177
214
  }
178
215
  function compareSpecificity(a, b) {
179
216
  if (a[0] !== b[0])
@@ -182,9 +219,11 @@ function compareSpecificity(a, b) {
182
219
  return a[1] - b[1];
183
220
  return a[2] - b[2];
184
221
  }
185
- function resolveForElement(element, rules, root) {
222
+ function resolveForElement(element, rules, selectorCache) {
186
223
  const resolved = {};
187
- const matchingRules = rules.filter((rule) => matchesSelector(element, rule.selector, root));
224
+ const matchingRules = rules.filter(
225
+ (rule) => selectorCache.get(rule.selector)?.has(element) ?? false
226
+ );
188
227
  matchingRules.sort((a, b) => {
189
228
  const specCmp = compareSpecificity(a.specificity, b.specificity);
190
229
  if (specCmp !== 0)
@@ -231,15 +270,24 @@ function resolveForElement(element, rules, root) {
231
270
  function resolveCascade(cssSources, root) {
232
271
  let offset = 0;
233
272
  const allRules = [];
273
+ const customProperties = /* @__PURE__ */ new Map();
234
274
  for (const css of cssSources) {
235
- const rules = parseRules(css, offset);
275
+ const rules = parseRules(css, offset, customProperties);
236
276
  allRules.push(...rules);
237
277
  offset += rules.length;
238
278
  }
279
+ const selectorCache = buildSelectorCache(allRules, root);
239
280
  const result = /* @__PURE__ */ new Map();
240
281
  function walk(el) {
241
- const resolved = resolveForElement(el, allRules, root);
282
+ const resolved = resolveForElement(el, allRules, selectorCache);
242
283
  if (Object.keys(resolved).length > 0) {
284
+ if (customProperties.size > 0) {
285
+ for (const style of Object.values(resolved)) {
286
+ if (style.value.includes("var(")) {
287
+ style.value = resolveVarReferences(style.value, customProperties);
288
+ }
289
+ }
290
+ }
243
291
  result.set(el, resolved);
244
292
  }
245
293
  for (const child of el.childNodes) {
@@ -256,8 +304,9 @@ function resolveCascade(cssSources, root) {
256
304
  function resolveCascadeByBreakpoint(cssSources, root) {
257
305
  let offset = 0;
258
306
  const allRules = [];
307
+ const customProperties = /* @__PURE__ */ new Map();
259
308
  for (const css of cssSources) {
260
- const rules = parseRules(css, offset);
309
+ const rules = parseRules(css, offset, customProperties);
261
310
  allRules.push(...rules);
262
311
  offset += rules.length;
263
312
  }
@@ -268,8 +317,15 @@ function resolveCascadeByBreakpoint(cssSources, root) {
268
317
  const result = /* @__PURE__ */ new Map();
269
318
  for (const breakpoint of breakpoints) {
270
319
  let walk = function(el) {
271
- const resolved = resolveForElement(el, rulesForBreakpoint, root);
320
+ const resolved = resolveForElement(el, rulesForBreakpoint, selectorCache);
272
321
  if (Object.keys(resolved).length > 0) {
322
+ if (customProperties.size > 0) {
323
+ for (const style of Object.values(resolved)) {
324
+ if (style.value.includes("var(")) {
325
+ style.value = resolveVarReferences(style.value, customProperties);
326
+ }
327
+ }
328
+ }
273
329
  breakpointResult.set(el, resolved);
274
330
  }
275
331
  for (const child of el.childNodes) {
@@ -280,6 +336,7 @@ function resolveCascadeByBreakpoint(cssSources, root) {
280
336
  const rulesForBreakpoint = allRules.filter(
281
337
  (r) => r.mediaQuery === void 0 || r.mediaQuery === breakpoint
282
338
  );
339
+ const selectorCache = buildSelectorCache(rulesForBreakpoint, root);
283
340
  const breakpointResult = /* @__PURE__ */ new Map();
284
341
  for (const child of root.childNodes) {
285
342
  if (child.nodeType === 1)
@@ -289,29 +346,6 @@ function resolveCascadeByBreakpoint(cssSources, root) {
289
346
  }
290
347
  return result;
291
348
  }
292
- function extractCssSources(root, filePath) {
293
- const sources = [];
294
- const linkedFiles = root.querySelectorAll('link[rel="stylesheet"]');
295
- for (const link of linkedFiles) {
296
- const href = link.getAttribute("href");
297
- if (href && !href.startsWith("http")) {
298
- try {
299
- const dir = require("node:path").dirname(filePath);
300
- const cssPath = require("node:path").resolve(dir, href);
301
- const css = require("node:fs").readFileSync(cssPath, "utf-8");
302
- sources.push(css);
303
- } catch {
304
- }
305
- }
306
- }
307
- const styleBlocks = root.querySelectorAll("style");
308
- for (const style of styleBlocks) {
309
- const text = style.textContent;
310
- if (text)
311
- sources.push(text);
312
- }
313
- return sources;
314
- }
315
349
  function normalizeHex(color) {
316
350
  const hex = color.trim().toLowerCase();
317
351
  if (!/^#[0-9a-f]{3,8}$/.test(hex))
@@ -445,10 +479,9 @@ function matchSpacing(value, spacingTokens) {
445
479
  if (part === "0" || part === "auto" || part.startsWith("var("))
446
480
  continue;
447
481
  if (!tokenValues.has(part)) {
448
- const suggestions = Object.entries(spacingTokens).map(([name, val]) => `{spacing.${name}} ("${val}")`).join(", ");
449
482
  return {
450
483
  matches: false,
451
- suggestion: `Use a spacing token: ${suggestions}`
484
+ suggestion: `Use a DESIGN.md spacing token`
452
485
  };
453
486
  }
454
487
  }
@@ -461,10 +494,9 @@ function matchRounded(value, roundedTokens) {
461
494
  const tokenValues = new Set(Object.values(roundedTokens));
462
495
  tokenValues.add("0");
463
496
  if (!tokenValues.has(value)) {
464
- const suggestions = Object.entries(roundedTokens).map(([name, val]) => `{rounded.${name}} ("${val}")`).join(", ");
465
497
  return {
466
498
  matches: false,
467
- suggestion: `Use a rounded token: ${suggestions}`
499
+ suggestion: `Use a DESIGN.md rounded token`
468
500
  };
469
501
  }
470
502
  return { matches: true };
@@ -500,7 +532,7 @@ function matchTypographyProperty(property, value, typographyTokens) {
500
532
  }
501
533
  return {
502
534
  matches: false,
503
- suggestion: `Value "${value}" for ${property} not in typography tokens`
535
+ suggestion: `Use a DESIGN.md typography token`
504
536
  };
505
537
  }
506
538
  function matchAnimationDuration(value, animationTokens) {
@@ -516,10 +548,9 @@ function matchAnimationDuration(value, animationTokens) {
516
548
  return { matches: true };
517
549
  if (durations.has(value))
518
550
  return { matches: true };
519
- const suggestions = Object.entries(animationTokens).filter(([, p]) => p.duration).map(([name, p]) => `${name} (${p.duration})`).join(", ");
520
551
  return {
521
552
  matches: false,
522
- suggestion: `Use an animation preset duration: ${suggestions}`
553
+ suggestion: `Use a DESIGN.md animation preset duration`
523
554
  };
524
555
  }
525
556
  function matchAnimationEasing(value, animationTokens) {
@@ -540,14 +571,13 @@ function matchAnimationEasing(value, animationTokens) {
540
571
  if (e.replace(/\s/g, "") === normalizedValue)
541
572
  return { matches: true };
542
573
  }
543
- const suggestions = Object.entries(animationTokens).filter(([, p]) => p.easing).map(([name, p]) => `${name} (${p.easing})`).join(", ");
544
574
  return {
545
575
  matches: false,
546
- suggestion: `Use an animation preset easing: ${suggestions}`
576
+ suggestion: `Use a DESIGN.md animation preset easing`
547
577
  };
548
578
  }
549
- function matchComponent(elementStyles, componentSpec, componentName) {
550
- const findings = [];
579
+ function matchComponent(elementStyles, componentSpec, rawSpec) {
580
+ const mismatches = [];
551
581
  const propertyMapping = {
552
582
  backgroundColor: "background-color",
553
583
  textColor: "color",
@@ -562,13 +592,72 @@ function matchComponent(elementStyles, componentSpec, componentName) {
562
592
  const cssProp = propertyMapping[specProp] || specProp;
563
593
  const actualValue = elementStyles[cssProp];
564
594
  if (actualValue && actualValue !== expectedValue) {
565
- findings.push({
566
- matches: false,
567
- suggestion: `${cssProp} should be "${expectedValue}" per ${componentName} component spec, found "${actualValue}"`
595
+ const rawValue = rawSpec?.[specProp];
596
+ mismatches.push({
597
+ cssProp,
598
+ expected: expectedValue,
599
+ expectedRaw: rawValue !== expectedValue ? rawValue : void 0,
600
+ actual: actualValue
568
601
  });
569
602
  }
570
603
  }
571
- return findings;
604
+ return mismatches;
605
+ }
606
+ function formatComponentMismatches(componentName, mismatches, elementDescription) {
607
+ const specPath = `DESIGN.md components.${componentName}`;
608
+ const header = elementDescription ? `${specPath} on ${elementDescription}:` : `${specPath}:`;
609
+ const lines = [header];
610
+ for (let i = 0; i < mismatches.length; i++) {
611
+ const m = mismatches[i];
612
+ const expectedDisplay = m.expectedRaw ? `${m.expectedRaw} (${m.expected})` : `"${m.expected}"`;
613
+ lines.push(` ${i + 1}. ${m.cssProp} should be ${expectedDisplay}, found "${m.actual}"`);
614
+ }
615
+ return lines.join("\n");
616
+ }
617
+ function isBackgroundShorthand(property) {
618
+ return property === "background";
619
+ }
620
+ function splitTopLevelCommas(value) {
621
+ const parts = [];
622
+ let depth = 0;
623
+ let current = "";
624
+ for (const ch of value) {
625
+ if (ch === "(" || ch === "[")
626
+ depth++;
627
+ else if (ch === ")" || ch === "]")
628
+ depth--;
629
+ if (ch === "," && depth === 0) {
630
+ parts.push(current.trim());
631
+ current = "";
632
+ } else {
633
+ current += ch;
634
+ }
635
+ }
636
+ if (current.trim())
637
+ parts.push(current.trim());
638
+ return parts;
639
+ }
640
+ function extractBackgroundColors(value) {
641
+ if (value.startsWith("var(") || value === "transparent" || value === "none" || value === "inherit" || value === "initial" || value === "unset") {
642
+ return [];
643
+ }
644
+ const layers = splitTopLevelCommas(value);
645
+ const colors = [];
646
+ for (const layer of layers) {
647
+ if (/^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(layer)) {
648
+ continue;
649
+ }
650
+ const hexMatch = layer.match(/(^|\s)(#[0-9a-fA-F]{3,8})(\s|$)/);
651
+ if (hexMatch) {
652
+ colors.push(hexMatch[2]);
653
+ continue;
654
+ }
655
+ const rgbMatch = layer.match(/(^|\s)(rgba?\s*\([^)]+\))/);
656
+ if (rgbMatch) {
657
+ colors.push(rgbMatch[2]);
658
+ }
659
+ }
660
+ return colors;
572
661
  }
573
662
  function hexToRgbValues(hex) {
574
663
  const normalized = normalizeHex(hex);
@@ -591,15 +680,25 @@ function contrastRatio(l1, l2) {
591
680
  const darker = Math.min(l1, l2);
592
681
  return (lighter + 0.05) / (darker + 0.05);
593
682
  }
594
- const DESIGNER_GUIDE$3 = "agent-kit/designer/design-system.md";
595
- function ref(designMdPath) {
596
- return `
597
- See ${designMdPath} for tokens, ${DESIGNER_GUIDE$3} for usage guide.`;
683
+ const GUIDE_SUGGESTION$1 = "See design-system-validator agent-kit/designer/design-system.md for usage guide";
684
+ function elementHint(el) {
685
+ const tag = el.rawTagName?.toLowerCase() || "element";
686
+ const cls = el.getAttribute?.("class");
687
+ const text = el.textContent?.trim();
688
+ const words = text ? text.split(/\s+/).slice(0, 3).join(" ") : "";
689
+ const truncated = words && text.split(/\s+/).length > 3 ? words + "..." : words;
690
+ const parts = [`<${tag}`];
691
+ if (cls)
692
+ parts.push(`class="${cls}"`);
693
+ parts.push(">");
694
+ if (truncated)
695
+ parts.push(`"${truncated}"`);
696
+ return parts.join(" ");
598
697
  }
599
698
  function validateElementStyles(el, styles, tokens, breakpointLabel, designMdPath, findings) {
600
- const tag = el.rawTagName?.toLowerCase() || "element";
699
+ const hint = elementHint(el);
601
700
  const prefix = breakpointLabel ? `[${breakpointLabel}] ` : "";
602
- const refs = ref(designMdPath);
701
+ const sug = (s) => s?.replace("DESIGN.md", designMdPath);
603
702
  for (const [property, resolved] of Object.entries(styles)) {
604
703
  if (resolved.allowed)
605
704
  continue;
@@ -608,9 +707,9 @@ function validateElementStyles(el, styles, tokens, breakpointLabel, designMdPath
608
707
  if (!result.matches) {
609
708
  findings.push({
610
709
  severity: "warning",
611
- message: `${prefix}Hardcoded color "${resolved.value}" for ${property} not in design system`,
612
- suggestion: result.suggestion + refs,
613
- element: `<${tag}>`
710
+ message: `${prefix}${hint} — Hardcoded color "${resolved.value}" for ${property} not in design system`,
711
+ suggestion: sug(result.suggestion),
712
+ element: hint
614
713
  });
615
714
  }
616
715
  }
@@ -619,9 +718,9 @@ function validateElementStyles(el, styles, tokens, breakpointLabel, designMdPath
619
718
  if (!result.matches) {
620
719
  findings.push({
621
720
  severity: "warning",
622
- message: `${prefix}${property} value "${resolved.value}" not in spacing scale`,
623
- suggestion: result.suggestion + refs,
624
- element: `<${tag}>`
721
+ message: `${prefix}${hint} — ${property} value "${resolved.value}" not in spacing scale`,
722
+ suggestion: sug(result.suggestion),
723
+ element: hint
625
724
  });
626
725
  }
627
726
  }
@@ -630,9 +729,9 @@ function validateElementStyles(el, styles, tokens, breakpointLabel, designMdPath
630
729
  if (!result.matches) {
631
730
  findings.push({
632
731
  severity: "warning",
633
- message: `${prefix}border-radius "${resolved.value}" not in rounded scale`,
634
- suggestion: result.suggestion + refs,
635
- element: `<${tag}>`
732
+ message: `${prefix}${hint} — border-radius "${resolved.value}" not in rounded scale`,
733
+ suggestion: sug(result.suggestion),
734
+ element: hint
636
735
  });
637
736
  }
638
737
  }
@@ -641,9 +740,9 @@ function validateElementStyles(el, styles, tokens, breakpointLabel, designMdPath
641
740
  if (!result.matches) {
642
741
  findings.push({
643
742
  severity: "warning",
644
- message: `${prefix}${property} value "${resolved.value}" not in typography tokens`,
645
- suggestion: result.suggestion + refs,
646
- element: `<${tag}>`
743
+ message: `${prefix}${hint} — ${property} value "${resolved.value}" not in typography tokens`,
744
+ suggestion: sug(result.suggestion),
745
+ element: hint
647
746
  });
648
747
  }
649
748
  }
@@ -652,9 +751,9 @@ function validateElementStyles(el, styles, tokens, breakpointLabel, designMdPath
652
751
  if (!result.matches) {
653
752
  findings.push({
654
753
  severity: "warning",
655
- message: `${prefix}${property} "${resolved.value}" not in animation presets`,
656
- suggestion: result.suggestion + refs,
657
- element: `<${tag}>`
754
+ message: `${prefix}${hint} — ${property} "${resolved.value}" not in animation presets`,
755
+ suggestion: sug(result.suggestion),
756
+ element: hint
658
757
  });
659
758
  }
660
759
  }
@@ -663,12 +762,26 @@ function validateElementStyles(el, styles, tokens, breakpointLabel, designMdPath
663
762
  if (!result.matches) {
664
763
  findings.push({
665
764
  severity: "warning",
666
- message: `${prefix}${property} "${resolved.value}" not in animation presets`,
667
- suggestion: result.suggestion + refs,
668
- element: `<${tag}>`
765
+ message: `${prefix}${hint} — ${property} "${resolved.value}" not in animation presets`,
766
+ suggestion: sug(result.suggestion),
767
+ element: hint
669
768
  });
670
769
  }
671
770
  }
771
+ if (isBackgroundShorthand(property) && Object.keys(tokens.colors).length > 0) {
772
+ const bgColors = extractBackgroundColors(resolved.value);
773
+ for (const bgColor of bgColors) {
774
+ const result = matchColor(bgColor, tokens.colors);
775
+ if (!result.matches) {
776
+ findings.push({
777
+ severity: "warning",
778
+ message: `${prefix}${hint} — Hardcoded color "${bgColor}" in background not in design system`,
779
+ suggestion: sug(result.suggestion),
780
+ element: hint
781
+ });
782
+ }
783
+ }
784
+ }
672
785
  }
673
786
  }
674
787
  const validateTokens = (ctx) => {
@@ -677,18 +790,39 @@ const validateTokens = (ctx) => {
677
790
  return [];
678
791
  const { tokens, designMdPath } = found;
679
792
  const findings = [];
680
- const cssSources = extractCssSources(ctx.body, ctx.filePath);
681
- if (cssSources.length === 0)
793
+ if (!ctx.css)
682
794
  return [];
795
+ const cssSources = [ctx.css];
796
+ const breakpointNames = /* @__PURE__ */ new Map();
797
+ for (const [name, value] of Object.entries(tokens.breakpoints)) {
798
+ breakpointNames.set(`(max-width: ${value})`, name);
799
+ }
800
+ const hasBreakpoints = Object.keys(tokens.breakpoints).length > 0;
801
+ const flaggedBreakpoints = /* @__PURE__ */ new Set();
683
802
  const byBreakpoint = resolveCascadeByBreakpoint(cssSources, ctx.body);
684
803
  for (const [breakpoint, cascade] of byBreakpoint) {
685
- const label = breakpoint || "";
804
+ if (breakpoint && hasBreakpoints && breakpoint.includes("max-width") && !breakpointNames.has(breakpoint)) {
805
+ if (!flaggedBreakpoints.has(breakpoint)) {
806
+ flaggedBreakpoints.add(breakpoint);
807
+ const defined = Object.entries(tokens.breakpoints).map(([n, v]) => `${n} (${v})`).join(", ");
808
+ findings.push({
809
+ severity: "warning",
810
+ message: `Media query @media ${breakpoint} not in DESIGN.md breakpoints`,
811
+ suggestion: `Use a ${designMdPath} breakpoint: ${defined}`
812
+ });
813
+ }
814
+ }
815
+ const name = breakpoint ? breakpointNames.get(breakpoint) : void 0;
816
+ const label = name ? `${name}` : breakpoint || "";
686
817
  for (const [el, styles] of cascade) {
687
818
  validateElementStyles(el, styles, tokens, label, designMdPath, findings);
688
819
  }
689
820
  }
690
821
  if (Object.keys(tokens.animations).length > 0) {
691
- checkReducedMotion(cssSources, designMdPath, findings);
822
+ checkReducedMotion(cssSources, findings);
823
+ }
824
+ if (findings.length > 0) {
825
+ findings.push({ severity: "warning", message: "", suggestion: GUIDE_SUGGESTION$1 });
692
826
  }
693
827
  return findings;
694
828
  };
@@ -700,7 +834,7 @@ const ANIMATION_PROPERTIES = /* @__PURE__ */ new Set([
700
834
  "animation-duration",
701
835
  "animation-name"
702
836
  ]);
703
- function checkReducedMotion(cssSources, designMdPath, findings) {
837
+ function checkReducedMotion(cssSources, findings) {
704
838
  let hasAnimations = false;
705
839
  let hasReducedMotion = false;
706
840
  for (const css of cssSources) {
@@ -720,21 +854,35 @@ function checkReducedMotion(cssSources, designMdPath, findings) {
720
854
  findings.push({
721
855
  severity: "warning",
722
856
  message: "Page uses transitions/animations but has no @media (prefers-reduced-motion) override",
723
- suggestion: "Add @media (prefers-reduced-motion: reduce) { * { transition-duration: 0s !important; animation-duration: 0s !important; } }" + ref(designMdPath)
857
+ suggestion: "Add @media (prefers-reduced-motion: reduce) { * { transition-duration: 0s !important; animation-duration: 0s !important; } }"
724
858
  });
725
859
  }
726
860
  }
727
- const DESIGNER_GUIDE$2 = "agent-kit/designer/design-system.md";
861
+ const GUIDE_SUGGESTION = "See design-system-validator agent-kit/designer/design-system.md for usage guide";
862
+ function describeElement(el) {
863
+ const tag = el.rawTagName?.toLowerCase() || "element";
864
+ const cls = el.getAttribute?.("class");
865
+ const ref = el.getAttribute?.("ref");
866
+ const text = el.textContent?.trim().slice(0, 30);
867
+ const parts = [`<${tag}`];
868
+ if (cls)
869
+ parts.push(`class="${cls}"`);
870
+ if (ref)
871
+ parts.push(`ref="${ref}"`);
872
+ parts.push(">");
873
+ if (text)
874
+ parts.push(`"${text}${el.textContent.trim().length > 30 ? "…" : ""}"`);
875
+ return parts.join(" ");
876
+ }
728
877
  const validateComponents = (ctx) => {
729
878
  const found = findDesignMd(ctx.filePath, ctx.projectRoot);
730
879
  if (!found || Object.keys(found.tokens.components).length === 0)
731
880
  return [];
732
881
  const { tokens, designMdPath } = found;
733
- const refs = `
734
- See ${designMdPath} for component specs, ${DESIGNER_GUIDE$2} for usage guide.`;
735
882
  const findings = [];
736
- const cssSources = extractCssSources(ctx.body, ctx.filePath);
737
- const cascade = resolveCascade(cssSources, ctx.body);
883
+ if (!ctx.css)
884
+ return [];
885
+ const cascade = resolveCascade([ctx.css], ctx.body);
738
886
  const jayComponents = /* @__PURE__ */ new Map();
739
887
  const htmlComponents = /* @__PURE__ */ new Map();
740
888
  for (const componentName of Object.keys(tokens.components)) {
@@ -764,6 +912,7 @@ See ${designMdPath} for component specs, ${DESIGNER_GUIDE$2} for usage guide.`;
764
912
  for (const [contractName, elements] of jayComponents) {
765
913
  const componentName = `jay:${contractName}`;
766
914
  const spec = tokens.components[componentName];
915
+ const rawSpec = tokens.rawComponents[componentName];
767
916
  if (!spec)
768
917
  continue;
769
918
  for (const el of elements) {
@@ -775,21 +924,21 @@ See ${designMdPath} for component specs, ${DESIGNER_GUIDE$2} for usage guide.`;
775
924
  if (!resolved.allowed)
776
925
  styleValues[prop] = resolved.value;
777
926
  }
778
- const results = matchComponent(styleValues, spec, componentName);
779
- for (const result of results) {
780
- if (!result.matches) {
781
- findings.push({
782
- severity: "warning",
783
- message: `<${componentName}> inline template: ${result.suggestion}`,
784
- suggestion: refs.trim(),
785
- element: `<${el.rawTagName || "element"}>`
786
- });
787
- }
927
+ const mismatches = matchComponent(styleValues, spec, rawSpec);
928
+ if (mismatches.length > 0) {
929
+ const desc = describeElement(el);
930
+ findings.push({
931
+ severity: "warning",
932
+ message: formatComponentMismatches(componentName, mismatches, desc),
933
+ suggestion: `See ${designMdPath} components section`,
934
+ element: desc
935
+ });
788
936
  }
789
937
  }
790
938
  }
791
939
  for (const [componentName, selector] of htmlComponents) {
792
940
  const spec = tokens.components[componentName];
941
+ const rawSpec = tokens.rawComponents[componentName];
793
942
  if (!spec)
794
943
  continue;
795
944
  for (const [el, styles] of cascade) {
@@ -805,33 +954,31 @@ See ${designMdPath} for component specs, ${DESIGNER_GUIDE$2} for usage guide.`;
805
954
  if (!resolved.allowed)
806
955
  styleValues[prop] = resolved.value;
807
956
  }
808
- const results = matchComponent(styleValues, spec, componentName);
809
- for (const result of results) {
810
- if (!result.matches) {
811
- findings.push({
812
- severity: "warning",
813
- message: `Component "${componentName}": ${result.suggestion}`,
814
- suggestion: refs.trim(),
815
- element: `<${el.rawTagName || "element"}>`
816
- });
817
- }
957
+ const mismatches = matchComponent(styleValues, spec, rawSpec);
958
+ if (mismatches.length > 0) {
959
+ const desc = describeElement(el);
960
+ findings.push({
961
+ severity: "warning",
962
+ message: formatComponentMismatches(componentName, mismatches, desc),
963
+ suggestion: `See ${designMdPath} components section`,
964
+ element: desc
965
+ });
818
966
  }
819
967
  }
820
968
  }
969
+ if (findings.length > 0) {
970
+ findings.push({ severity: "warning", message: "", suggestion: GUIDE_SUGGESTION });
971
+ }
821
972
  return findings;
822
973
  };
823
- const DESIGNER_GUIDE$1 = "agent-kit/designer/design-system.md";
824
974
  const validateStructure = (ctx) => {
825
975
  const found = findDesignMd(ctx.filePath, ctx.projectRoot);
826
976
  if (!found)
827
977
  return [];
828
- const { tokens, designMdPath } = found;
829
- const refs = `
830
- See ${designMdPath} for rules, ${DESIGNER_GUIDE$1} for usage guide.`;
978
+ const { tokens } = found;
831
979
  const findings = [];
832
- if (tokens.rules["max-font-weights"]) {
833
- const cssSources = extractCssSources(ctx.body, ctx.filePath);
834
- const cascade = resolveCascade(cssSources, ctx.body);
980
+ if (tokens.rules["max-font-weights"] && ctx.css) {
981
+ const cascade = resolveCascade([ctx.css], ctx.body);
835
982
  const fontWeights = /* @__PURE__ */ new Set();
836
983
  for (const [, styles] of cascade) {
837
984
  const fw = styles["font-weight"];
@@ -843,7 +990,7 @@ See ${designMdPath} for rules, ${DESIGNER_GUIDE$1} for usage guide.`;
843
990
  findings.push({
844
991
  severity: "warning",
845
992
  message: `${fontWeights.size} unique font-weight values found (max: ${max}): ${[...fontWeights].join(", ")}`,
846
- suggestion: `Reduce to ${max} font-weight values from the typography tokens.${refs}`
993
+ suggestion: `Reduce to ${max} font-weight values from the DESIGN.md typography tokens`
847
994
  });
848
995
  }
849
996
  }
@@ -856,9 +1003,9 @@ See ${designMdPath} for rules, ${DESIGNER_GUIDE$1} for usage guide.`;
856
1003
  const classes = el.getAttribute?.("class") || "";
857
1004
  if (!classes.includes("primary") && !classes.includes("btn-primary"))
858
1005
  return;
859
- const ref2 = el.getAttribute?.("ref") || "";
1006
+ const ref = el.getAttribute?.("ref") || "";
860
1007
  const text = el.textContent?.trim() || "";
861
- const identity = `${ref2}|${text}`;
1008
+ const identity = `${ref}|${text}`;
862
1009
  primaryButtons.set(identity, true);
863
1010
  });
864
1011
  const max = tokens.rules["max-primary-buttons"];
@@ -866,13 +1013,12 @@ See ${designMdPath} for rules, ${DESIGNER_GUIDE$1} for usage guide.`;
866
1013
  findings.push({
867
1014
  severity: "warning",
868
1015
  message: `${primaryButtons.size} distinct primary buttons found (max: ${max})`,
869
- suggestion: `Reduce to ${max} primary action button per page. Same button (same ref and text) appearing multiple times counts as one.${refs}`
1016
+ suggestion: `Reduce to ${max} primary action button per page`
870
1017
  });
871
1018
  }
872
1019
  }
873
1020
  return findings;
874
1021
  };
875
- const DESIGNER_GUIDE = "agent-kit/designer/design-system.md";
876
1022
  function resolveColorValue(value) {
877
1023
  if (value.startsWith("#"))
878
1024
  return value;
@@ -904,14 +1050,10 @@ const validateContrast = (ctx) => {
904
1050
  const found = findDesignMd(ctx.filePath, ctx.projectRoot);
905
1051
  if (!found || !found.tokens.rules["require-contrast-aa"])
906
1052
  return [];
907
- const { tokens, designMdPath } = found;
908
- const refs = `
909
- See ${designMdPath} for color tokens, ${DESIGNER_GUIDE} for usage guide.`;
910
1053
  const findings = [];
911
- const cssSources = extractCssSources(ctx.body, ctx.filePath);
912
- if (cssSources.length === 0)
1054
+ if (!ctx.css)
913
1055
  return [];
914
- const cascade = resolveCascade(cssSources, ctx.body);
1056
+ const cascade = resolveCascade([ctx.css], ctx.body);
915
1057
  for (const [el, styles] of cascade) {
916
1058
  const colorStyle = styles["color"];
917
1059
  const bgStyle = styles["background-color"] || styles["background"];
@@ -937,7 +1079,7 @@ See ${designMdPath} for color tokens, ${DESIGNER_GUIDE} for usage guide.`;
937
1079
  findings.push({
938
1080
  severity: "warning",
939
1081
  message: `Contrast ratio ${ratio.toFixed(1)}:1 below WCAG AA (${threshold}:1) for color "${colorStyle.value}" on background "${bgStyle.value}"`,
940
- suggestion: `Darken text color or lighten background to meet minimum contrast.${refs}`,
1082
+ suggestion: "Darken text color or lighten background to meet minimum contrast",
941
1083
  element: `<${tag}>`
942
1084
  });
943
1085
  }
@@ -945,99 +1087,187 @@ See ${designMdPath} for color tokens, ${DESIGNER_GUIDE} for usage guide.`;
945
1087
  return findings;
946
1088
  };
947
1089
  const ADD_MENU_OUTPUT_REL = "agent-kit/aiditor/add-menu/design-system.yaml";
948
- function buildColorItems(tokens) {
949
- const entries = Object.entries(tokens.colors);
950
- if (entries.length === 0)
951
- return [];
952
- const palette = entries.map(([name, value]) => `${name}: ${value}`).join(", ");
953
- return [
954
- {
955
- id: "design-system:color-palette",
956
- title: "Color palette",
957
- category: "Design System",
958
- subCategory: "Colors",
959
- prompt: `Apply colors from the project design system.
960
- Available color tokens: ${palette}.
961
- Use the exact token values from DESIGN.md. Do not use hardcoded colors outside this palette.`
962
- }
963
- ];
1090
+ function categoryName(designMdPath, projectRoot, tokens) {
1091
+ if (tokens.name)
1092
+ return tokens.name;
1093
+ const rel = path.relative(projectRoot, designMdPath);
1094
+ if (rel === "DESIGN.md")
1095
+ return "Design System";
1096
+ const dir = path.dirname(rel).replace(/^src\/pages\/?/, "");
1097
+ return dir ? `Design System (${dir})` : "Design System";
964
1098
  }
965
- function buildTypographyItems(tokens) {
966
- const entries = Object.entries(tokens.typography);
967
- if (entries.length === 0)
968
- return [];
969
- const presets = entries.map(([name, t]) => {
970
- const parts = [`${name}:`];
1099
+ function esc(s) {
1100
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1101
+ }
1102
+ function buildColorItems(tokens, category) {
1103
+ return Object.entries(tokens.colors).map(([name, value]) => ({
1104
+ id: `design-system:color-${name}`,
1105
+ title: `${name} (${value})`,
1106
+ category,
1107
+ subCategory: "Colors",
1108
+ prompt: `Use color token "${name}" with value ${value} from DESIGN.md.`,
1109
+ html: `<div style="display:flex;align-items:center;gap:10px;font-family:sans-serif;">
1110
+ <div style="width:40px;height:40px;border-radius:6px;background:${esc(value)};border:1px solid rgba(0,0,0,0.1);flex-shrink:0;"></div>
1111
+ <div>
1112
+ <div style="font-size:13px;font-weight:600;">${esc(name)}</div>
1113
+ <div style="font-size:11px;color:#888;font-family:monospace;">${esc(value)}</div>
1114
+ </div>
1115
+ </div>`
1116
+ }));
1117
+ }
1118
+ function buildTypographyItems(tokens, category) {
1119
+ return Object.entries(tokens.typography).map(([name, t]) => {
1120
+ const parts = [];
971
1121
  if (t.fontFamily)
972
1122
  parts.push(t.fontFamily);
973
1123
  if (t.fontSize)
974
1124
  parts.push(t.fontSize);
975
1125
  if (t.fontWeight)
976
1126
  parts.push(`weight ${t.fontWeight}`);
977
- return parts.join(" ");
978
- }).join("; ");
979
- return [
980
- {
981
- id: "design-system:typography",
982
- title: "Typography presets",
983
- category: "Design System",
1127
+ if (t.lineHeight)
1128
+ parts.push(`line-height ${t.lineHeight}`);
1129
+ if (t.letterSpacing)
1130
+ parts.push(`letter-spacing ${t.letterSpacing}`);
1131
+ const desc = parts.join(", ");
1132
+ const styles = [];
1133
+ if (t.fontFamily)
1134
+ styles.push(`font-family:${t.fontFamily},sans-serif`);
1135
+ if (t.fontSize)
1136
+ styles.push(`font-size:${t.fontSize}`);
1137
+ if (t.fontWeight)
1138
+ styles.push(`font-weight:${t.fontWeight}`);
1139
+ if (t.lineHeight)
1140
+ styles.push(`line-height:${t.lineHeight}`);
1141
+ if (t.letterSpacing)
1142
+ styles.push(`letter-spacing:${t.letterSpacing}`);
1143
+ return {
1144
+ id: `design-system:typography-${name}`,
1145
+ title: name,
1146
+ category,
984
1147
  subCategory: "Typography",
985
- prompt: `Apply typography from the project design system.
986
- Available presets: ${presets}.
987
- Use font-size, font-weight, line-height, and font-family values from these presets. Do not introduce typography values outside the design system.`
988
- }
989
- ];
1148
+ prompt: `Apply typography preset "${name}" from DESIGN.md: ${desc}.`,
1149
+ html: `<div style="font-family:sans-serif;">
1150
+ <div style="${styles.join(";")};margin:0;">The quick brown fox</div>
1151
+ <div style="font-size:10px;color:#888;margin-top:6px;font-family:monospace;">${esc(name)}: ${esc(desc)}</div>
1152
+ </div>`
1153
+ };
1154
+ });
990
1155
  }
991
- function buildSpacingItems(tokens) {
992
- const entries = Object.entries(tokens.spacing);
993
- if (entries.length === 0)
994
- return [];
995
- const scale = entries.map(([name, value]) => `${name}: ${value}`).join(", ");
996
- return [
997
- {
998
- id: "design-system:spacing",
999
- title: "Spacing scale",
1000
- category: "Design System",
1001
- subCategory: "Layout",
1002
- prompt: `Apply spacing from the project design system.
1003
- Spacing scale: ${scale}.
1004
- Use these values for padding, margin, and gap. Add /* design-system: allow */ comment to exempt intentional one-off values.`
1005
- }
1006
- ];
1156
+ function buildSpacingItems(tokens, category) {
1157
+ return Object.entries(tokens.spacing).map(([name, value]) => ({
1158
+ id: `design-system:spacing-${name}`,
1159
+ title: `${name} (${value})`,
1160
+ category,
1161
+ subCategory: "Spacing",
1162
+ prompt: `Use spacing token "${name}" with value ${value} from DESIGN.md for padding, margin, or gap.`,
1163
+ html: `<div style="display:flex;align-items:center;gap:8px;font-family:sans-serif;">
1164
+ <div style="display:flex;align-items:center;gap:0;">
1165
+ <div style="width:16px;height:24px;background:#cbd5e1;border-radius:2px 0 0 2px;"></div>
1166
+ <div style="width:${esc(value)};height:24px;background:repeating-linear-gradient(45deg,#e0f2fe,#e0f2fe 2px,#bae6fd 2px,#bae6fd 4px);"></div>
1167
+ <div style="width:16px;height:24px;background:#cbd5e1;border-radius:0 2px 2px 0;"></div>
1168
+ </div>
1169
+ <div>
1170
+ <span style="font-size:12px;font-weight:600;">${esc(name)}</span>
1171
+ <span style="font-size:11px;color:#888;font-family:monospace;margin-left:4px;">${esc(value)}</span>
1172
+ </div>
1173
+ </div>`
1174
+ }));
1007
1175
  }
1008
- function buildRoundedItems(tokens) {
1009
- const entries = Object.entries(tokens.rounded);
1010
- if (entries.length === 0)
1011
- return [];
1012
- const scale = entries.map(([name, value]) => `${name}: ${value}`).join(", ");
1013
- return [
1014
- {
1015
- id: "design-system:rounded",
1016
- title: "Border radius",
1017
- category: "Design System",
1018
- subCategory: "Shapes",
1019
- prompt: `Apply border-radius from the project design system.
1020
- Rounded scale: ${scale}.
1021
- Use these values for border-radius. Do not use arbitrary radius values.`
1176
+ function buildRoundedItems(tokens, category) {
1177
+ return Object.entries(tokens.rounded).map(([name, value]) => ({
1178
+ id: `design-system:rounded-${name}`,
1179
+ title: `${name} (${value})`,
1180
+ category,
1181
+ subCategory: "Rounded",
1182
+ prompt: `Use border-radius token "${name}" with value ${value} from DESIGN.md.`,
1183
+ html: `<div style="display:flex;align-items:center;gap:10px;font-family:sans-serif;">
1184
+ <div style="width:40px;height:40px;border-radius:${esc(value)};background:#e2e8f0;border:1.5px solid #94a3b8;flex-shrink:0;"></div>
1185
+ <div>
1186
+ <div style="font-size:12px;font-weight:600;">${esc(name)}</div>
1187
+ <div style="font-size:11px;color:#888;font-family:monospace;">${esc(value)}</div>
1188
+ </div>
1189
+ </div>`
1190
+ }));
1191
+ }
1192
+ function buildBreakpointItems(tokens, category) {
1193
+ return Object.entries(tokens.breakpoints).map(([name, value]) => ({
1194
+ id: `design-system:breakpoint-${name}`,
1195
+ title: `${name} (${value})`,
1196
+ category,
1197
+ subCategory: "Breakpoints",
1198
+ prompt: `Use breakpoint "${name}" at max-width ${value} from DESIGN.md: @media (max-width: ${value}) { ... }`
1199
+ }));
1200
+ }
1201
+ function buildAnimationItems(tokens, category) {
1202
+ return Object.entries(tokens.animations).map(([name, preset]) => {
1203
+ const parts = [];
1204
+ if (preset.duration)
1205
+ parts.push(preset.duration);
1206
+ if (preset.easing)
1207
+ parts.push(preset.easing);
1208
+ const desc = parts.join(", ");
1209
+ const dur = preset.duration || "300ms";
1210
+ const ease = preset.easing || "ease";
1211
+ return {
1212
+ id: `design-system:animation-${name}`,
1213
+ title: name,
1214
+ category,
1215
+ subCategory: "Animations",
1216
+ prompt: `Use animation preset "${name}" from DESIGN.md: ${desc}. Apply to transition-duration and transition-timing-function.`,
1217
+ html: `<div style="font-family:sans-serif;">
1218
+ <style>
1219
+ @scope {
1220
+ .anim-preview { width:40px;height:40px;border-radius:6px;background:#8b5cf6;transition:transform ${esc(dur)} ${esc(ease)};cursor:pointer; }
1221
+ .anim-preview:hover { transform:scale(1.3); }
1022
1222
  }
1023
- ];
1223
+ </style>
1224
+ <div style="display:flex;align-items:center;gap:10px;">
1225
+ <div class="anim-preview"></div>
1226
+ <div>
1227
+ <div style="font-size:12px;font-weight:600;">${esc(name)}</div>
1228
+ <div style="font-size:11px;color:#888;font-family:monospace;">${esc(desc)}</div>
1229
+ <div style="font-size:10px;color:#aaa;">hover to preview</div>
1230
+ </div>
1231
+ </div>
1232
+ </div>`
1233
+ };
1234
+ });
1024
1235
  }
1025
- function buildComponentItems(tokens) {
1026
- const entries = Object.entries(tokens.components);
1027
- if (entries.length === 0)
1028
- return [];
1029
- return entries.map(([name, spec]) => {
1030
- const props = Object.entries(spec).map(([prop, value]) => `${prop}: ${value}`).join(", ");
1031
- const isJayComponent = name.startsWith("jay:");
1032
- const title = isJayComponent ? name : `Component: ${name}`;
1236
+ function buildComponentItems(tokens, category) {
1237
+ return Object.entries(tokens.components).map(([name, spec]) => {
1238
+ const rawSpec = tokens.rawComponents[name];
1239
+ const props = Object.entries(spec).map(([prop, value]) => {
1240
+ const raw = rawSpec?.[prop];
1241
+ return raw && raw !== value ? `${prop}: ${raw} (${value})` : `${prop}: ${value}`;
1242
+ }).join(", ");
1243
+ const previewStyles = [];
1244
+ const propertyMapping = {
1245
+ backgroundColor: "background-color",
1246
+ textColor: "color",
1247
+ rounded: "border-radius",
1248
+ padding: "padding",
1249
+ borderColor: "border-color"
1250
+ };
1251
+ for (const [prop, value] of Object.entries(spec)) {
1252
+ if (prop === "typography")
1253
+ continue;
1254
+ const cssProp = propertyMapping[prop] || prop;
1255
+ previewStyles.push(`${cssProp}:${value}`);
1256
+ }
1257
+ if (!spec.padding)
1258
+ previewStyles.push("padding:8px 16px");
1259
+ if (spec.borderColor)
1260
+ previewStyles.push("border:1.5px solid " + spec.borderColor);
1033
1261
  return {
1034
1262
  id: `design-system:component-${name}`,
1035
- title,
1036
- category: "Design System",
1263
+ title: name,
1264
+ category,
1037
1265
  subCategory: "Components",
1038
- prompt: `Apply the "${name}" component spec from the design system.
1039
- Required styles: ${props}.
1040
- Ensure the element matches these values exactly. The design-system validator will flag mismatches.`
1266
+ prompt: `Apply the "${name}" component spec from DESIGN.md. Required styles: ${props}. The design-system validator will flag mismatches.`,
1267
+ html: `<div style="font-family:sans-serif;">
1268
+ <div style="${previewStyles.join(";")};display:inline-block;font-size:13px;min-width:80px;text-align:center;">${esc(name)}</div>
1269
+ <div style="font-size:10px;color:#888;margin-top:6px;font-family:monospace;">${esc(props)}</div>
1270
+ </div>`
1041
1271
  };
1042
1272
  });
1043
1273
  }
@@ -1061,10 +1291,10 @@ function findAllDesignMdFiles(projectRoot) {
1061
1291
  }
1062
1292
  return files;
1063
1293
  }
1064
- async function generateDesignSystemReferences(ctx) {
1294
+ async function generateDesignSystemAgentKit(ctx) {
1065
1295
  const designMdFiles = findAllDesignMdFiles(ctx.projectRoot);
1066
1296
  if (designMdFiles.length === 0) {
1067
- return { referencesCreated: [], message: "No DESIGN.md found in project" };
1297
+ return { agentKitCreated: [], message: "No DESIGN.md found in project" };
1068
1298
  }
1069
1299
  const allItems = [];
1070
1300
  const seen = /* @__PURE__ */ new Set();
@@ -1073,12 +1303,15 @@ async function generateDesignSystemReferences(ctx) {
1073
1303
  const tokens = parseDesignMd(content);
1074
1304
  if (!tokens)
1075
1305
  continue;
1306
+ const category = categoryName(filePath, ctx.projectRoot, tokens);
1076
1307
  const items = [
1077
- ...buildColorItems(tokens),
1078
- ...buildTypographyItems(tokens),
1079
- ...buildSpacingItems(tokens),
1080
- ...buildRoundedItems(tokens),
1081
- ...buildComponentItems(tokens)
1308
+ ...buildColorItems(tokens, category),
1309
+ ...buildTypographyItems(tokens, category),
1310
+ ...buildSpacingItems(tokens, category),
1311
+ ...buildRoundedItems(tokens, category),
1312
+ ...buildBreakpointItems(tokens, category),
1313
+ ...buildAnimationItems(tokens, category),
1314
+ ...buildComponentItems(tokens, category)
1082
1315
  ];
1083
1316
  for (const item of items) {
1084
1317
  if (!seen.has(item.id)) {
@@ -1088,18 +1321,18 @@ async function generateDesignSystemReferences(ctx) {
1088
1321
  }
1089
1322
  }
1090
1323
  if (allItems.length === 0) {
1091
- return { referencesCreated: [], message: "DESIGN.md found but no tokens defined" };
1324
+ return { agentKitCreated: [], message: "DESIGN.md found but no tokens defined" };
1092
1325
  }
1093
1326
  const outputPath = path.join(ctx.projectRoot, ADD_MENU_OUTPUT_REL);
1094
1327
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1095
1328
  fs.writeFileSync(outputPath, yaml.dump({ items: allItems }, { lineWidth: 120 }), "utf-8");
1096
1329
  return {
1097
- referencesCreated: [ADD_MENU_OUTPUT_REL],
1098
- message: `Generated ${allItems.length} add-menu items from DESIGN.md tokens`
1330
+ agentKitCreated: [ADD_MENU_OUTPUT_REL],
1331
+ message: `Generated ${allItems.length} add-menu items from ${designMdFiles.length} DESIGN.md file(s)`
1099
1332
  };
1100
1333
  }
1101
1334
  export {
1102
- generateDesignSystemReferences,
1335
+ generateDesignSystemAgentKit,
1103
1336
  validateComponents,
1104
1337
  validateContrast,
1105
1338
  validateStructure,
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@jay-framework/design-system-validator",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "description": "Design system validation plugin for Jay Framework — validates jay-html CSS against DESIGN.md tokens",
6
6
  "license": "Apache-2.0",
7
7
  "main": "dist/index.js",
8
8
  "files": [
9
9
  "dist",
10
- "plugin.yaml"
10
+ "plugin.yaml",
11
+ "agent-kit"
11
12
  ],
12
13
  "exports": {
13
14
  ".": "./dist/index.js",
@@ -25,15 +26,15 @@
25
26
  },
26
27
  "dependencies": {
27
28
  "@csstools/selector-specificity": "^5.0.0",
28
- "@jay-framework/compiler-shared": "^0.21.0",
29
- "@jay-framework/stack-server-runtime": "^0.21.0",
29
+ "@jay-framework/compiler-shared": "^0.22.0",
30
+ "@jay-framework/stack-server-runtime": "^0.22.0",
30
31
  "js-yaml": "^4.1.0",
31
32
  "postcss": "^8.5.6",
32
33
  "postcss-selector-parser": "^7.1.0"
33
34
  },
34
35
  "devDependencies": {
35
- "@jay-framework/dev-environment": "^0.21.0",
36
- "@jay-framework/jay-stack-cli": "^0.21.0",
36
+ "@jay-framework/dev-environment": "^0.22.0",
37
+ "@jay-framework/jay-stack-cli": "^0.22.0",
37
38
  "@types/js-yaml": "^4.0.9",
38
39
  "@types/node": "^22.15.21",
39
40
  "node-html-parser": "^6.1.0",
package/plugin.yaml CHANGED
@@ -1,8 +1,7 @@
1
1
  name: design-system-validator
2
2
 
3
- setup:
4
- references: generateDesignSystemReferences
5
- description: Generate AIditor add-menu entries from DESIGN.md tokens
3
+ agentkit: generateDesignSystemAgentKit
4
+ description: Generate AIditor add-menu entries from DESIGN.md tokens
6
5
 
7
6
  validators:
8
7
  - name: design-tokens