@jay-framework/design-system-validator 0.22.2 → 0.23.1

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/dist/index.js CHANGED
@@ -1,12 +1,17 @@
1
1
  import postcss from "postcss";
2
- import yaml from "js-yaml";
2
+ import yaml, { load, dump } from "js-yaml";
3
3
  import * as fs from "node:fs";
4
- import fs__default from "node:fs";
4
+ import fs__default, { promises } from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import path__default from "node:path";
7
7
  import selectorParser from "postcss-selector-parser";
8
8
  import { selectorSpecificity } from "@csstools/selector-specificity";
9
- import { walkElements } from "@jay-framework/compiler-shared";
9
+ import { walkElements, JAY_EXTENSION } from "@jay-framework/compiler-shared";
10
+ import { makeJayAction, makeJayQuery, makeJayStackComponent, phaseOutput } from "@jay-framework/fullstack-component";
11
+ import { fontFamilyToCamelCase } from "@capsizecss/metrics";
12
+ import { createFontStack } from "@capsizecss/core";
13
+ import { fileURLToPath } from "node:url";
14
+ import { parseJayFile, JAY_IMPORT_RESOLVER } from "@jay-framework/compiler-jay-html";
10
15
  function extractFrontmatter(content) {
11
16
  const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
12
17
  return match ? match[1] : null;
@@ -17,14 +22,11 @@ function resolveReferences(tokens) {
17
22
  const parts = refPath.split(".");
18
23
  let current = tokens;
19
24
  for (const part of parts) {
20
- if (current == null || typeof current !== "object")
21
- return `{${refPath}}`;
25
+ if (current == null || typeof current !== "object") return `{${refPath}}`;
22
26
  current = current[part];
23
27
  }
24
- if (typeof current === "string")
25
- return resolve(current);
26
- if (typeof current === "number")
27
- return String(current);
28
+ if (typeof current === "string") return resolve(current);
29
+ if (typeof current === "number") return String(current);
28
30
  return `{${refPath}}`;
29
31
  });
30
32
  };
@@ -39,8 +41,7 @@ function resolveReferences(tokens) {
39
41
  }
40
42
  }
41
43
  function normalizeStringValues(map) {
42
- if (!map)
43
- return {};
44
+ if (!map) return {};
44
45
  const result = {};
45
46
  for (const [key, value] of Object.entries(map)) {
46
47
  result[key] = String(value);
@@ -49,11 +50,9 @@ function normalizeStringValues(map) {
49
50
  }
50
51
  function parseDesignMd(content) {
51
52
  const frontmatter = extractFrontmatter(content);
52
- if (!frontmatter)
53
- return null;
53
+ if (!frontmatter) return null;
54
54
  const raw = yaml.load(frontmatter);
55
- if (!raw || typeof raw !== "object")
56
- return null;
55
+ if (!raw || typeof raw !== "object") return null;
57
56
  const rawComponents = {};
58
57
  if (raw.components) {
59
58
  for (const [name, spec] of Object.entries(raw.components)) {
@@ -82,14 +81,12 @@ function findDesignMd(filePath, projectRoot) {
82
81
  if (fs__default.existsSync(candidate)) {
83
82
  const content = fs__default.readFileSync(candidate, "utf-8");
84
83
  const tokens = parseDesignMd(content);
85
- if (!tokens)
86
- return null;
84
+ if (!tokens) return null;
87
85
  const designMdPath = path__default.relative(projectRoot, candidate);
88
86
  return { tokens, designMdPath };
89
87
  }
90
88
  const parent = path__default.dirname(dir);
91
- if (parent === dir)
92
- break;
89
+ if (parent === dir) break;
93
90
  dir = parent;
94
91
  }
95
92
  return null;
@@ -157,8 +154,7 @@ function parseRules(css, sourceIndexOffset = 0, customProperties) {
157
154
  const mediaQuery = getMediaQuery(rule);
158
155
  for (const selector of rule.selectors) {
159
156
  const trimmed = selector.trim();
160
- if (!trimmed)
161
- continue;
157
+ if (!trimmed) continue;
162
158
  rules.push({
163
159
  selector: trimmed,
164
160
  specificity: computeSpecificity(trimmed),
@@ -176,10 +172,8 @@ function resolveVarReferences(value, customProperties) {
176
172
  /var\(\s*(--[a-zA-Z0-9-]+)(?:\s*,\s*([^)]+))?\s*\)/g,
177
173
  (_, name, fallback) => {
178
174
  const resolved = customProperties.get(name);
179
- if (resolved !== void 0)
180
- return resolveVarReferences(resolved, customProperties);
181
- if (fallback !== void 0)
182
- return fallback.trim();
175
+ if (resolved !== void 0) return resolveVarReferences(resolved, customProperties);
176
+ if (fallback !== void 0) return fallback.trim();
183
177
  return _;
184
178
  }
185
179
  );
@@ -188,8 +182,7 @@ function parseInlineStyles(style) {
188
182
  const declarations = [];
189
183
  for (const part of style.split(";")) {
190
184
  const colonIdx = part.indexOf(":");
191
- if (colonIdx === -1)
192
- continue;
185
+ if (colonIdx === -1) continue;
193
186
  const prop = part.substring(0, colonIdx).trim();
194
187
  const val = part.substring(colonIdx + 1).trim();
195
188
  if (prop && val) {
@@ -201,8 +194,7 @@ function parseInlineStyles(style) {
201
194
  function buildSelectorCache(rules, root) {
202
195
  const cache = /* @__PURE__ */ new Map();
203
196
  for (const rule of rules) {
204
- if (cache.has(rule.selector))
205
- continue;
197
+ if (cache.has(rule.selector)) continue;
206
198
  try {
207
199
  const matches = root.querySelectorAll(rule.selector);
208
200
  cache.set(rule.selector, new Set(matches));
@@ -213,10 +205,8 @@ function buildSelectorCache(rules, root) {
213
205
  return cache;
214
206
  }
215
207
  function compareSpecificity(a, b) {
216
- if (a[0] !== b[0])
217
- return a[0] - b[0];
218
- if (a[1] !== b[1])
219
- return a[1] - b[1];
208
+ if (a[0] !== b[0]) return a[0] - b[0];
209
+ if (a[1] !== b[1]) return a[1] - b[1];
220
210
  return a[2] - b[2];
221
211
  }
222
212
  function resolveForElement(element, rules, selectorCache) {
@@ -226,15 +216,13 @@ function resolveForElement(element, rules, selectorCache) {
226
216
  );
227
217
  matchingRules.sort((a, b) => {
228
218
  const specCmp = compareSpecificity(a.specificity, b.specificity);
229
- if (specCmp !== 0)
230
- return specCmp;
219
+ if (specCmp !== 0) return specCmp;
231
220
  return a.sourceIndex - b.sourceIndex;
232
221
  });
233
222
  for (const rule of matchingRules) {
234
223
  for (const decl of rule.declarations) {
235
224
  const existing = resolved[decl.property];
236
- if (existing?.important && !decl.important)
237
- continue;
225
+ if (existing?.important && !decl.important) continue;
238
226
  resolved[decl.property] = {
239
227
  property: decl.property,
240
228
  value: decl.value,
@@ -252,8 +240,7 @@ function resolveForElement(element, rules, selectorCache) {
252
240
  const inlineDecls = parseInlineStyles(inlineStyle);
253
241
  for (const decl of inlineDecls) {
254
242
  const existing = resolved[decl.property];
255
- if (existing?.important)
256
- continue;
243
+ if (existing?.important) continue;
257
244
  resolved[decl.property] = {
258
245
  property: decl.property,
259
246
  value: decl.value,
@@ -291,13 +278,11 @@ function resolveCascade(cssSources, root) {
291
278
  result.set(el, resolved);
292
279
  }
293
280
  for (const child of el.childNodes) {
294
- if (child.nodeType === 1)
295
- walk(child);
281
+ if (child.nodeType === 1) walk(child);
296
282
  }
297
283
  }
298
284
  for (const child of root.childNodes) {
299
- if (child.nodeType === 1)
300
- walk(child);
285
+ if (child.nodeType === 1) walk(child);
301
286
  }
302
287
  return result;
303
288
  }
@@ -329,8 +314,7 @@ function resolveCascadeByBreakpoint(cssSources, root) {
329
314
  breakpointResult.set(el, resolved);
330
315
  }
331
316
  for (const child of el.childNodes) {
332
- if (child.nodeType === 1)
333
- walk(child);
317
+ if (child.nodeType === 1) walk(child);
334
318
  }
335
319
  };
336
320
  const rulesForBreakpoint = allRules.filter(
@@ -339,8 +323,7 @@ function resolveCascadeByBreakpoint(cssSources, root) {
339
323
  const selectorCache = buildSelectorCache(rulesForBreakpoint, root);
340
324
  const breakpointResult = /* @__PURE__ */ new Map();
341
325
  for (const child of root.childNodes) {
342
- if (child.nodeType === 1)
343
- walk(child);
326
+ if (child.nodeType === 1) walk(child);
344
327
  }
345
328
  result.set(breakpoint, breakpointResult);
346
329
  }
@@ -348,8 +331,7 @@ function resolveCascadeByBreakpoint(cssSources, root) {
348
331
  }
349
332
  function normalizeHex(color) {
350
333
  const hex = color.trim().toLowerCase();
351
- if (!/^#[0-9a-f]{3,8}$/.test(hex))
352
- return null;
334
+ if (!/^#[0-9a-f]{3,8}$/.test(hex)) return null;
353
335
  if (hex.length === 4) {
354
336
  return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
355
337
  }
@@ -357,8 +339,7 @@ function normalizeHex(color) {
357
339
  }
358
340
  function parseRgb(value) {
359
341
  const match = value.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
360
- if (!match)
361
- return null;
342
+ if (!match) return null;
362
343
  const r = parseInt(match[1], 10);
363
344
  const g = parseInt(match[2], 10);
364
345
  const b = parseInt(match[3], 10);
@@ -366,10 +347,8 @@ function parseRgb(value) {
366
347
  }
367
348
  function normalizeColor(value) {
368
349
  const trimmed = value.trim().toLowerCase();
369
- if (trimmed.startsWith("#"))
370
- return normalizeHex(trimmed);
371
- if (trimmed.startsWith("rgb"))
372
- return parseRgb(trimmed);
350
+ if (trimmed.startsWith("#")) return normalizeHex(trimmed);
351
+ if (trimmed.startsWith("rgb")) return parseRgb(trimmed);
373
352
  return null;
374
353
  }
375
354
  const COLOR_PROPERTIES = /* @__PURE__ */ new Set([
@@ -430,26 +409,22 @@ function isAnimationEasingProperty(property) {
430
409
  return property === "transition-timing-function" || property === "animation-timing-function";
431
410
  }
432
411
  function matchColor(value, colorTokens) {
433
- if (value.startsWith("var("))
434
- return { matches: true };
412
+ if (value.startsWith("var(")) return { matches: true };
435
413
  if (value === "transparent" || value === "inherit" || value === "currentColor" || value === "initial" || value === "unset") {
436
414
  return { matches: true };
437
415
  }
438
416
  const normalized = normalizeColor(value);
439
- if (!normalized)
440
- return { matches: true };
417
+ if (!normalized) return { matches: true };
441
418
  const tokenEntries = Object.entries(colorTokens);
442
419
  for (const [name, tokenValue] of tokenEntries) {
443
420
  const normalizedToken = normalizeColor(tokenValue);
444
- if (normalizedToken === normalized)
445
- return { matches: true };
421
+ if (normalizedToken === normalized) return { matches: true };
446
422
  }
447
423
  let closestName = "";
448
424
  let closestDist = Infinity;
449
425
  for (const [name, tokenValue] of tokenEntries) {
450
426
  const nt = normalizeColor(tokenValue);
451
- if (!nt)
452
- continue;
427
+ if (!nt) continue;
453
428
  const dist = colorDistance(normalized, nt);
454
429
  if (dist < closestDist) {
455
430
  closestDist = dist;
@@ -476,8 +451,7 @@ function matchSpacing(value, spacingTokens) {
476
451
  const tokenValues = new Set(Object.values(spacingTokens));
477
452
  tokenValues.add("0");
478
453
  for (const part of parts) {
479
- if (part === "0" || part === "auto" || part.startsWith("var("))
480
- continue;
454
+ if (part === "0" || part === "auto" || part.startsWith("var(")) continue;
481
455
  if (!tokenValues.has(part)) {
482
456
  return {
483
457
  matches: false,
@@ -507,27 +481,22 @@ function matchTypographyProperty(property, value, typographyTokens) {
507
481
  }
508
482
  const tokenValues = /* @__PURE__ */ new Set();
509
483
  for (const token of Object.values(typographyTokens)) {
510
- if (property === "font-size" && token.fontSize)
511
- tokenValues.add(token.fontSize);
484
+ if (property === "font-size" && token.fontSize) tokenValues.add(token.fontSize);
512
485
  if (property === "font-weight" && token.fontWeight != null)
513
486
  tokenValues.add(String(token.fontWeight));
514
487
  if (property === "line-height" && token.lineHeight != null)
515
488
  tokenValues.add(String(token.lineHeight));
516
489
  if (property === "letter-spacing" && token.letterSpacing)
517
490
  tokenValues.add(token.letterSpacing);
518
- if (property === "font-family" && token.fontFamily)
519
- tokenValues.add(token.fontFamily);
491
+ if (property === "font-family" && token.fontFamily) tokenValues.add(token.fontFamily);
520
492
  }
521
- if (tokenValues.size === 0)
522
- return { matches: true };
523
- if (tokenValues.has(value))
524
- return { matches: true };
493
+ if (tokenValues.size === 0) return { matches: true };
494
+ if (tokenValues.has(value)) return { matches: true };
525
495
  if (property === "font-family") {
526
496
  const normalizedValue = value.replace(/['"]/g, "").trim();
527
497
  const primaryFont = normalizedValue.split(",")[0].trim();
528
498
  for (const tv of tokenValues) {
529
- if (tv.replace(/['"]/g, "").trim() === primaryFont)
530
- return { matches: true };
499
+ if (tv.replace(/['"]/g, "").trim() === primaryFont) return { matches: true };
531
500
  }
532
501
  }
533
502
  return {
@@ -541,13 +510,10 @@ function matchAnimationDuration(value, animationTokens) {
541
510
  }
542
511
  const durations = /* @__PURE__ */ new Set();
543
512
  for (const preset of Object.values(animationTokens)) {
544
- if (preset.duration)
545
- durations.add(preset.duration);
513
+ if (preset.duration) durations.add(preset.duration);
546
514
  }
547
- if (durations.size === 0)
548
- return { matches: true };
549
- if (durations.has(value))
550
- return { matches: true };
515
+ if (durations.size === 0) return { matches: true };
516
+ if (durations.has(value)) return { matches: true };
551
517
  return {
552
518
  matches: false,
553
519
  suggestion: `Use a DESIGN.md animation preset duration`
@@ -559,17 +525,13 @@ function matchAnimationEasing(value, animationTokens) {
559
525
  }
560
526
  const easings = /* @__PURE__ */ new Set();
561
527
  for (const preset of Object.values(animationTokens)) {
562
- if (preset.easing)
563
- easings.add(preset.easing);
528
+ if (preset.easing) easings.add(preset.easing);
564
529
  }
565
- if (easings.size === 0)
566
- return { matches: true };
567
- if (easings.has(value))
568
- return { matches: true };
530
+ if (easings.size === 0) return { matches: true };
531
+ if (easings.has(value)) return { matches: true };
569
532
  const normalizedValue = value.replace(/\s/g, "");
570
533
  for (const e of easings) {
571
- if (e.replace(/\s/g, "") === normalizedValue)
572
- return { matches: true };
534
+ if (e.replace(/\s/g, "") === normalizedValue) return { matches: true };
573
535
  }
574
536
  return {
575
537
  matches: false,
@@ -587,8 +549,7 @@ function matchComponent(elementStyles, componentSpec, rawSpec) {
587
549
  width: "width"
588
550
  };
589
551
  for (const [specProp, expectedValue] of Object.entries(componentSpec)) {
590
- if (specProp === "typography")
591
- continue;
552
+ if (specProp === "typography") continue;
592
553
  const cssProp = propertyMapping[specProp] || specProp;
593
554
  const actualValue = elementStyles[cssProp];
594
555
  if (actualValue && actualValue !== expectedValue) {
@@ -622,10 +583,8 @@ function splitTopLevelCommas(value) {
622
583
  let depth = 0;
623
584
  let current = "";
624
585
  for (const ch of value) {
625
- if (ch === "(" || ch === "[")
626
- depth++;
627
- else if (ch === ")" || ch === "]")
628
- depth--;
586
+ if (ch === "(" || ch === "[") depth++;
587
+ else if (ch === ")" || ch === "]") depth--;
629
588
  if (ch === "," && depth === 0) {
630
589
  parts.push(current.trim());
631
590
  current = "";
@@ -633,8 +592,7 @@ function splitTopLevelCommas(value) {
633
592
  current += ch;
634
593
  }
635
594
  }
636
- if (current.trim())
637
- parts.push(current.trim());
595
+ if (current.trim()) parts.push(current.trim());
638
596
  return parts;
639
597
  }
640
598
  function extractBackgroundColors(value) {
@@ -661,8 +619,7 @@ function extractBackgroundColors(value) {
661
619
  }
662
620
  function hexToRgbValues(hex) {
663
621
  const normalized = normalizeHex(hex);
664
- if (!normalized)
665
- return null;
622
+ if (!normalized) return null;
666
623
  return [
667
624
  parseInt(normalized.slice(1, 3), 16),
668
625
  parseInt(normalized.slice(3, 5), 16),
@@ -680,7 +637,7 @@ function contrastRatio(l1, l2) {
680
637
  const darker = Math.min(l1, l2);
681
638
  return (lighter + 0.05) / (darker + 0.05);
682
639
  }
683
- const GUIDE_SUGGESTION$1 = "See design-system-validator agent-kit/designer/design-system.md for usage guide";
640
+ const GUIDE_SUGGESTION$2 = "See design-system-validator agent-kit/designer/design-system.md for usage guide";
684
641
  function elementHint(el) {
685
642
  const tag = el.rawTagName?.toLowerCase() || "element";
686
643
  const cls = el.getAttribute?.("class");
@@ -688,20 +645,17 @@ function elementHint(el) {
688
645
  const words = text ? text.split(/\s+/).slice(0, 3).join(" ") : "";
689
646
  const truncated = words && text.split(/\s+/).length > 3 ? words + "..." : words;
690
647
  const parts = [`<${tag}`];
691
- if (cls)
692
- parts.push(`class="${cls}"`);
648
+ if (cls) parts.push(`class="${cls}"`);
693
649
  parts.push(">");
694
- if (truncated)
695
- parts.push(`"${truncated}"`);
650
+ if (truncated) parts.push(`"${truncated}"`);
696
651
  return parts.join(" ");
697
652
  }
698
653
  function validateElementStyles(el, styles, tokens, breakpointLabel, designMdPath, findings) {
699
654
  const hint = elementHint(el);
700
655
  const prefix = breakpointLabel ? `[${breakpointLabel}] ` : "";
701
- const sug = (s) => s?.replace("DESIGN.md", designMdPath);
656
+ const sug = (s) => s ? s.replace("DESIGN.md", designMdPath) + "\nTo suppress intentional exceptions, see: agent-kit/designer/design-system.md" : void 0;
702
657
  for (const [property, resolved] of Object.entries(styles)) {
703
- if (resolved.allowed)
704
- continue;
658
+ if (resolved.allowed) continue;
705
659
  if (isColorProperty(property) && Object.keys(tokens.colors).length > 0) {
706
660
  const result = matchColor(resolved.value, tokens.colors);
707
661
  if (!result.matches) {
@@ -786,12 +740,10 @@ function validateElementStyles(el, styles, tokens, breakpointLabel, designMdPath
786
740
  }
787
741
  const validateTokens = (ctx) => {
788
742
  const found = findDesignMd(ctx.filePath, ctx.projectRoot);
789
- if (!found)
790
- return [];
743
+ if (!found) return [];
791
744
  const { tokens, designMdPath } = found;
792
745
  const findings = [];
793
- if (!ctx.css)
794
- return [];
746
+ if (!ctx.css) return [];
795
747
  const cssSources = [ctx.css];
796
748
  const breakpointNames = /* @__PURE__ */ new Map();
797
749
  for (const [name, value] of Object.entries(tokens.breakpoints)) {
@@ -822,7 +774,7 @@ const validateTokens = (ctx) => {
822
774
  checkReducedMotion(cssSources, findings);
823
775
  }
824
776
  if (findings.length > 0) {
825
- findings.push({ severity: "warning", message: "", suggestion: GUIDE_SUGGESTION$1 });
777
+ findings.push({ severity: "warning", message: "", suggestion: GUIDE_SUGGESTION$2 });
826
778
  }
827
779
  return findings;
828
780
  };
@@ -858,30 +810,25 @@ function checkReducedMotion(cssSources, findings) {
858
810
  });
859
811
  }
860
812
  }
861
- const GUIDE_SUGGESTION = "See design-system-validator agent-kit/designer/design-system.md for usage guide";
813
+ const GUIDE_SUGGESTION$1 = "See design-system-validator agent-kit/designer/design-system.md for usage guide";
862
814
  function describeElement(el) {
863
815
  const tag = el.rawTagName?.toLowerCase() || "element";
864
816
  const cls = el.getAttribute?.("class");
865
817
  const ref = el.getAttribute?.("ref");
866
818
  const text = el.textContent?.trim().slice(0, 30);
867
819
  const parts = [`<${tag}`];
868
- if (cls)
869
- parts.push(`class="${cls}"`);
870
- if (ref)
871
- parts.push(`ref="${ref}"`);
820
+ if (cls) parts.push(`class="${cls}"`);
821
+ if (ref) parts.push(`ref="${ref}"`);
872
822
  parts.push(">");
873
- if (text)
874
- parts.push(`"${text}${el.textContent.trim().length > 30 ? "…" : ""}"`);
823
+ if (text) parts.push(`"${text}${el.textContent.trim().length > 30 ? "…" : ""}"`);
875
824
  return parts.join(" ");
876
825
  }
877
826
  const validateComponents = (ctx) => {
878
827
  const found = findDesignMd(ctx.filePath, ctx.projectRoot);
879
- if (!found || Object.keys(found.tokens.components).length === 0)
880
- return [];
828
+ if (!found || Object.keys(found.tokens.components).length === 0) return [];
881
829
  const { tokens, designMdPath } = found;
882
830
  const findings = [];
883
- if (!ctx.css)
884
- return [];
831
+ if (!ctx.css) return [];
885
832
  const cascade = resolveCascade([ctx.css], ctx.body);
886
833
  const jayComponents = /* @__PURE__ */ new Map();
887
834
  const htmlComponents = /* @__PURE__ */ new Map();
@@ -904,8 +851,7 @@ const validateComponents = (ctx) => {
904
851
  }
905
852
  }
906
853
  for (const child of el.childNodes) {
907
- if (child.nodeType === 1)
908
- walk(child);
854
+ if (child.nodeType === 1) walk(child);
909
855
  }
910
856
  }
911
857
  walk(ctx.body);
@@ -913,16 +859,13 @@ const validateComponents = (ctx) => {
913
859
  const componentName = `jay:${contractName}`;
914
860
  const spec = tokens.components[componentName];
915
861
  const rawSpec = tokens.rawComponents[componentName];
916
- if (!spec)
917
- continue;
862
+ if (!spec) continue;
918
863
  for (const el of elements) {
919
864
  const styles = cascade.get(el);
920
- if (!styles)
921
- continue;
865
+ if (!styles) continue;
922
866
  const styleValues = {};
923
867
  for (const [prop, resolved] of Object.entries(styles)) {
924
- if (!resolved.allowed)
925
- styleValues[prop] = resolved.value;
868
+ if (!resolved.allowed) styleValues[prop] = resolved.value;
926
869
  }
927
870
  const mismatches = matchComponent(styleValues, spec, rawSpec);
928
871
  if (mismatches.length > 0) {
@@ -939,20 +882,17 @@ const validateComponents = (ctx) => {
939
882
  for (const [componentName, selector] of htmlComponents) {
940
883
  const spec = tokens.components[componentName];
941
884
  const rawSpec = tokens.rawComponents[componentName];
942
- if (!spec)
943
- continue;
885
+ if (!spec) continue;
944
886
  for (const [el, styles] of cascade) {
945
887
  try {
946
888
  const matchedEls = ctx.body.querySelectorAll(selector);
947
- if (!matchedEls.some((m) => m === el))
948
- continue;
889
+ if (!matchedEls.some((m) => m === el)) continue;
949
890
  } catch {
950
891
  continue;
951
892
  }
952
893
  const styleValues = {};
953
894
  for (const [prop, resolved] of Object.entries(styles)) {
954
- if (!resolved.allowed)
955
- styleValues[prop] = resolved.value;
895
+ if (!resolved.allowed) styleValues[prop] = resolved.value;
956
896
  }
957
897
  const mismatches = matchComponent(styleValues, spec, rawSpec);
958
898
  if (mismatches.length > 0) {
@@ -967,14 +907,13 @@ const validateComponents = (ctx) => {
967
907
  }
968
908
  }
969
909
  if (findings.length > 0) {
970
- findings.push({ severity: "warning", message: "", suggestion: GUIDE_SUGGESTION });
910
+ findings.push({ severity: "warning", message: "", suggestion: GUIDE_SUGGESTION$1 });
971
911
  }
972
912
  return findings;
973
913
  };
974
914
  const validateStructure = (ctx) => {
975
915
  const found = findDesignMd(ctx.filePath, ctx.projectRoot);
976
- if (!found)
977
- return [];
916
+ if (!found) return [];
978
917
  const { tokens } = found;
979
918
  const findings = [];
980
919
  if (tokens.rules["max-font-weights"] && ctx.css) {
@@ -982,8 +921,7 @@ const validateStructure = (ctx) => {
982
921
  const fontWeights = /* @__PURE__ */ new Set();
983
922
  for (const [, styles] of cascade) {
984
923
  const fw = styles["font-weight"];
985
- if (fw && !fw.allowed)
986
- fontWeights.add(fw.value);
924
+ if (fw && !fw.allowed) fontWeights.add(fw.value);
987
925
  }
988
926
  const max = tokens.rules["max-font-weights"];
989
927
  if (fontWeights.size > max) {
@@ -998,11 +936,9 @@ const validateStructure = (ctx) => {
998
936
  const primaryButtons = /* @__PURE__ */ new Map();
999
937
  walkElements(ctx.body, ctx, (el) => {
1000
938
  const tag = el.rawTagName?.toLowerCase();
1001
- if (tag !== "button")
1002
- return;
939
+ if (tag !== "button") return;
1003
940
  const classes = el.getAttribute?.("class") || "";
1004
- if (!classes.includes("primary") && !classes.includes("btn-primary"))
1005
- return;
941
+ if (!classes.includes("primary") && !classes.includes("btn-primary")) return;
1006
942
  const ref = el.getAttribute?.("ref") || "";
1007
943
  const text = el.textContent?.trim() || "";
1008
944
  const identity = `${ref}|${text}`;
@@ -1020,8 +956,7 @@ const validateStructure = (ctx) => {
1020
956
  return findings;
1021
957
  };
1022
958
  function resolveColorValue(value) {
1023
- if (value.startsWith("#"))
1024
- return value;
959
+ if (value.startsWith("#")) return value;
1025
960
  if (value.startsWith("rgb")) {
1026
961
  const match = value.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
1027
962
  if (match) {
@@ -1034,41 +969,33 @@ function resolveColorValue(value) {
1034
969
  return null;
1035
970
  }
1036
971
  function isLargeText(fontSize, fontWeight) {
1037
- if (!fontSize)
1038
- return false;
972
+ if (!fontSize) return false;
1039
973
  const sizeMatch = fontSize.match(/([\d.]+)(px|rem|em)/);
1040
- if (!sizeMatch)
1041
- return false;
974
+ if (!sizeMatch) return false;
1042
975
  let sizePx = parseFloat(sizeMatch[1]);
1043
976
  const unit = sizeMatch[2];
1044
- if (unit === "rem" || unit === "em")
1045
- sizePx *= 16;
977
+ if (unit === "rem" || unit === "em") sizePx *= 16;
1046
978
  const weight = fontWeight ? parseInt(fontWeight, 10) : 400;
1047
979
  return sizePx >= 24 || sizePx >= 18.66 && weight >= 700;
1048
980
  }
1049
981
  const validateContrast = (ctx) => {
1050
982
  const found = findDesignMd(ctx.filePath, ctx.projectRoot);
1051
- if (!found || !found.tokens.rules["require-contrast-aa"])
1052
- return [];
983
+ if (!found || !found.tokens.rules["require-contrast-aa"]) return [];
984
+ const { tokens } = found;
1053
985
  const findings = [];
1054
- if (!ctx.css)
1055
- return [];
986
+ if (!ctx.css) return [];
1056
987
  const cascade = resolveCascade([ctx.css], ctx.body);
1057
988
  for (const [el, styles] of cascade) {
1058
989
  const colorStyle = styles["color"];
1059
990
  const bgStyle = styles["background-color"] || styles["background"];
1060
- if (!colorStyle || !bgStyle)
1061
- continue;
1062
- if (colorStyle.allowed || bgStyle.allowed)
1063
- continue;
991
+ if (!colorStyle || !bgStyle) continue;
992
+ if (colorStyle.allowed || bgStyle.allowed) continue;
1064
993
  const fgHex = resolveColorValue(colorStyle.value);
1065
994
  const bgHex = resolveColorValue(bgStyle.value);
1066
- if (!fgHex || !bgHex)
1067
- continue;
995
+ if (!fgHex || !bgHex) continue;
1068
996
  const fgRgb = hexToRgbValues(fgHex);
1069
997
  const bgRgb = hexToRgbValues(bgHex);
1070
- if (!fgRgb || !bgRgb)
1071
- continue;
998
+ if (!fgRgb || !bgRgb) continue;
1072
999
  const fgLum = relativeLuminance(...fgRgb);
1073
1000
  const bgLum = relativeLuminance(...bgRgb);
1074
1001
  const ratio = contrastRatio(fgLum, bgLum);
@@ -1086,164 +1013,420 @@ const validateContrast = (ctx) => {
1086
1013
  }
1087
1014
  return findings;
1088
1015
  };
1089
- const ADD_MENU_OUTPUT_REL = "agent-kit/aiditor/add-menu/design-system.yaml";
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";
1016
+ const GUIDE_SUGGESTION = "See design-system-validator agent-kit/designer/font-fallback-patterns.md for usage guide";
1017
+ const METRIC_OVERRIDE_PROPERTIES = /* @__PURE__ */ new Set([
1018
+ "size-adjust",
1019
+ "ascent-override",
1020
+ "descent-override",
1021
+ "line-gap-override"
1022
+ ]);
1023
+ function unquote(value) {
1024
+ return value.replace(/^(['"])(.*)\1$/, "$2");
1098
1025
  }
1026
+ function parseFontFaces(css) {
1027
+ const result = [];
1028
+ const root = postcss.parse(css);
1029
+ root.walkAtRules("font-face", (atRule) => {
1030
+ let family = "";
1031
+ let hasUrlSrc = false;
1032
+ let hasLocalSrc = false;
1033
+ let hasMetricOverrides = false;
1034
+ atRule.walkDecls((decl) => {
1035
+ if (decl.prop === "font-family") {
1036
+ family = unquote(decl.value);
1037
+ }
1038
+ if (decl.prop === "src") {
1039
+ if (/url\s*\(/.test(decl.value)) hasUrlSrc = true;
1040
+ if (/local\s*\(/.test(decl.value)) hasLocalSrc = true;
1041
+ }
1042
+ if (METRIC_OVERRIDE_PROPERTIES.has(decl.prop)) {
1043
+ hasMetricOverrides = true;
1044
+ }
1045
+ });
1046
+ if (family) {
1047
+ result.push({ family, hasUrlSrc, hasLocalSrc, hasMetricOverrides });
1048
+ }
1049
+ });
1050
+ return result;
1051
+ }
1052
+ const FONT_SERVICE_HOSTS = [
1053
+ "fonts.googleapis.com",
1054
+ "fonts.bunny.net",
1055
+ "fonts.cdnfonts.com",
1056
+ "use.typekit.net"
1057
+ ];
1058
+ function parseFontServiceUrl(url) {
1059
+ if (!FONT_SERVICE_HOSTS.some((host) => url.includes(host))) return [];
1060
+ const queryStart = url.indexOf("?");
1061
+ if (queryStart === -1) return [];
1062
+ const query = url.substring(queryStart + 1);
1063
+ const families = [];
1064
+ for (const param of query.split("&")) {
1065
+ const eqIdx = param.indexOf("=");
1066
+ const key = eqIdx === -1 ? param : param.substring(0, eqIdx);
1067
+ const value = eqIdx === -1 ? "" : param.substring(eqIdx + 1);
1068
+ if (key === "family") {
1069
+ const parts = value.split("|");
1070
+ for (const part of parts) {
1071
+ const name = decodeURIComponent(part.split(":")[0]).replace(/\+/g, " ");
1072
+ if (name) families.push(name);
1073
+ }
1074
+ }
1075
+ }
1076
+ return families;
1077
+ }
1078
+ function parseFontImports(css) {
1079
+ const families = [];
1080
+ const root = postcss.parse(css);
1081
+ root.walkAtRules("import", (atRule) => {
1082
+ const raw = atRule.params;
1083
+ const urlMatch = raw.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/) || raw.match(/(['"]?)(.+?)\1/);
1084
+ if (!urlMatch) return;
1085
+ const url = urlMatch[2];
1086
+ families.push(...parseFontServiceUrl(url));
1087
+ });
1088
+ return families;
1089
+ }
1090
+ function parseFontLinks(head) {
1091
+ if (!head?.links) return [];
1092
+ const families = [];
1093
+ for (const link of head.links) {
1094
+ if (link.rel !== "stylesheet") continue;
1095
+ const staticParts = link.href.filter((p) => p.kind === "static");
1096
+ if (staticParts.length !== link.href.length) continue;
1097
+ const url = staticParts.map((p) => p.value).join("");
1098
+ families.push(...parseFontServiceUrl(url));
1099
+ }
1100
+ return families;
1101
+ }
1102
+ function parseFontFamilyStacks(css) {
1103
+ const stacks = /* @__PURE__ */ new Map();
1104
+ const root = postcss.parse(css);
1105
+ root.walkDecls("font-family", (decl) => {
1106
+ const parent = decl.parent;
1107
+ if (parent?.type === "atrule" && parent.name === "font-face") return;
1108
+ const families = decl.value.split(",").map((f) => unquote(f.trim()));
1109
+ const key = families.join(",");
1110
+ if (!stacks.has(key)) {
1111
+ stacks.set(key, families);
1112
+ }
1113
+ });
1114
+ return stacks;
1115
+ }
1116
+ const validateFontFallbacks = (ctx) => {
1117
+ const findings = [];
1118
+ const fontFaces = ctx.css ? parseFontFaces(ctx.css) : [];
1119
+ const importedFonts = ctx.css ? parseFontImports(ctx.css) : [];
1120
+ const linkedFonts = parseFontLinks(ctx.head);
1121
+ if (fontFaces.length === 0 && importedFonts.length === 0 && linkedFonts.length === 0)
1122
+ return findings;
1123
+ if (!ctx.css && linkedFonts.length === 0) return findings;
1124
+ const webFontFamilies = /* @__PURE__ */ new Set();
1125
+ const metricMatchedFallbacks = /* @__PURE__ */ new Set();
1126
+ for (const ff of fontFaces) {
1127
+ if (ff.hasUrlSrc) {
1128
+ webFontFamilies.add(ff.family);
1129
+ }
1130
+ if (ff.hasLocalSrc && ff.hasMetricOverrides) {
1131
+ metricMatchedFallbacks.add(ff.family);
1132
+ }
1133
+ }
1134
+ for (const family of importedFonts) {
1135
+ webFontFamilies.add(family);
1136
+ }
1137
+ for (const family of linkedFonts) {
1138
+ webFontFamilies.add(family);
1139
+ }
1140
+ if (webFontFamilies.size === 0) return findings;
1141
+ const fontStacks = ctx.css ? parseFontFamilyStacks(ctx.css) : /* @__PURE__ */ new Map();
1142
+ const designMd = findDesignMd(ctx.filePath, ctx.projectRoot);
1143
+ if (designMd) {
1144
+ for (const [, token] of Object.entries(designMd.tokens.typography)) {
1145
+ if (token.fontFamily) {
1146
+ const families = token.fontFamily.split(",").map((f) => unquote(f.trim()));
1147
+ const key = families.join(",");
1148
+ if (!fontStacks.has(key)) {
1149
+ fontStacks.set(key, families);
1150
+ }
1151
+ }
1152
+ }
1153
+ }
1154
+ const flagged = /* @__PURE__ */ new Set();
1155
+ function flagIfMissing(family) {
1156
+ if (flagged.has(family)) return;
1157
+ const hasFallbackFace = fontFaces.some(
1158
+ (ff) => ff.hasLocalSrc && ff.hasMetricOverrides && ff.family.startsWith(family) && ff.family !== family
1159
+ );
1160
+ const familyItselfIsMetricMatched = metricMatchedFallbacks.has(family);
1161
+ if (!hasFallbackFace && !familyItselfIsMetricMatched) {
1162
+ flagged.add(family);
1163
+ findings.push({
1164
+ severity: "warning",
1165
+ message: `font-family "${family}" loads from a URL but has no metric-matched fallback. This causes layout shift (CLS) when the font loads.`,
1166
+ suggestion: `Generate a fallback with: npx jay-stack-cli action design-system-validator/fontFallback --input '{"primary":"${family}","fallback":"Arial"}'`
1167
+ });
1168
+ }
1169
+ }
1170
+ for (const [, families] of fontStacks) {
1171
+ for (const family of families) {
1172
+ if (!webFontFamilies.has(family)) continue;
1173
+ flagIfMissing(family);
1174
+ }
1175
+ }
1176
+ for (const family of webFontFamilies) {
1177
+ flagIfMissing(family);
1178
+ }
1179
+ if (findings.length > 0) {
1180
+ findings.push({ severity: "warning", message: "", suggestion: GUIDE_SUGGESTION });
1181
+ }
1182
+ return findings;
1183
+ };
1184
+ async function loadMetrics(familyName) {
1185
+ const camelCase = fontFamilyToCamelCase(familyName);
1186
+ try {
1187
+ const mod = await import(`@capsizecss/metrics/${camelCase}`);
1188
+ return mod.default;
1189
+ } catch {
1190
+ throw new Error(
1191
+ `Font "${familyName}" not found in @capsizecss/metrics. Use a known Google Font or system font name (e.g. "Inter", "Arial", "Georgia").`
1192
+ );
1193
+ }
1194
+ }
1195
+ const fontFallback = makeJayAction("designSystem.fontFallback").withHandler(
1196
+ async (input) => {
1197
+ const primaryMetrics = await loadMetrics(input.primary);
1198
+ const fallbackMetrics = await loadMetrics(input.fallback);
1199
+ const result = createFontStack([primaryMetrics, fallbackMetrics]);
1200
+ return {
1201
+ fontFamily: result.fontFamily,
1202
+ fontFaces: result.fontFaces
1203
+ };
1204
+ }
1205
+ );
1206
+ const PLUGIN_ATTRS = {
1207
+ pluginName: "design-system-validator",
1208
+ packageName: "@jay-framework/design-system-validator"
1209
+ };
1099
1210
  function esc(s) {
1100
1211
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1101
1212
  }
1102
- function buildColorItems(tokens, category) {
1103
- return Object.entries(tokens.colors).map(([name, value]) => ({
1213
+ function scopedFragment(innerHtml, scopedCss = "") {
1214
+ const styleBlock = `<style>@scope { ${scopedCss} }</style>`;
1215
+ return `<div>${styleBlock}${innerHtml}</div>`;
1216
+ }
1217
+ function isLargeFontSize(fontSize) {
1218
+ if (!fontSize) return false;
1219
+ if (/\d+(\.\d+)?\s*(vw|vh|vmin|vmax)\b/.test(fontSize)) return true;
1220
+ const clampMatch = fontSize.match(/clamp\([^,]+,[^,]+,\s*([\d.]+)\s*(px|rem|em)/);
1221
+ if (clampMatch) {
1222
+ const max = parseFloat(clampMatch[1]);
1223
+ const unit = clampMatch[2];
1224
+ if (unit === "px") return max > 20;
1225
+ return max > 1.25;
1226
+ }
1227
+ const simple = parseFloat(fontSize);
1228
+ if (isNaN(simple)) return false;
1229
+ if (fontSize.includes("rem") || fontSize.includes("em")) return simple > 1.25;
1230
+ return simple > 20;
1231
+ }
1232
+ function spacingBrowseSize(value) {
1233
+ if (/calc|clamp|var\(/.test(value)) return "large";
1234
+ const px = parseFloat(value);
1235
+ if (!isNaN(px) && !value.includes("rem") && !value.includes("em")) {
1236
+ if (px <= 30) return "small";
1237
+ if (px > 100) return "large";
1238
+ return void 0;
1239
+ }
1240
+ const rem = parseFloat(value);
1241
+ if (!isNaN(rem)) {
1242
+ if (rem <= 2) return "small";
1243
+ if (rem > 6) return "large";
1244
+ }
1245
+ return void 0;
1246
+ }
1247
+ function buildColorItem(name, value, category) {
1248
+ return {
1104
1249
  id: `design-system:color-${name}`,
1105
- title: `${name} (${value})`,
1250
+ ...PLUGIN_ATTRS,
1251
+ title: name,
1106
1252
  category,
1107
1253
  subCategory: "Colors",
1254
+ browse: { size: "small" },
1108
1255
  prompt: `Use color token "${name}" with value ${value} from DESIGN.md.`,
1256
+ interaction: {
1257
+ mode: "stage-place",
1258
+ stagePromptTemplate: `Apply the color token "${name}" (${value}) from DESIGN.md at this location.`
1259
+ },
1109
1260
  presentation: {
1110
1261
  type: "html-fragment",
1111
- html: `<div style="display:flex;align-items:center;gap:10px;font-family:sans-serif;">
1112
- <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>
1113
- <div>
1114
- <div style="font-size:13px;font-weight:600;">${esc(name)}</div>
1115
- <div style="font-size:11px;color:#888;font-family:monospace;">${esc(value)}</div>
1116
- </div>
1117
- </div>`
1262
+ html: scopedFragment(`<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:sans-serif;height:100%;">
1263
+ <div style="width:40px;height:40px;border-radius:6px;background:${esc(value)};border:1px solid rgba(0,0,0,0.1);"></div>
1264
+ <div style="font-size:10px;color:#888;font-family:monospace;">${esc(value)}</div>
1265
+ </div>`)
1118
1266
  }
1119
- }));
1120
- }
1121
- function buildTypographyItems(tokens, category) {
1122
- return Object.entries(tokens.typography).map(([name, t]) => {
1123
- const parts = [];
1124
- if (t.fontFamily)
1125
- parts.push(t.fontFamily);
1126
- if (t.fontSize)
1127
- parts.push(t.fontSize);
1128
- if (t.fontWeight)
1129
- parts.push(`weight ${t.fontWeight}`);
1130
- if (t.lineHeight)
1131
- parts.push(`line-height ${t.lineHeight}`);
1132
- if (t.letterSpacing)
1133
- parts.push(`letter-spacing ${t.letterSpacing}`);
1134
- const desc = parts.join(", ");
1135
- const styles = [];
1136
- if (t.fontFamily)
1137
- styles.push(`font-family:${t.fontFamily},sans-serif`);
1138
- if (t.fontSize)
1139
- styles.push(`font-size:${t.fontSize}`);
1140
- if (t.fontWeight)
1141
- styles.push(`font-weight:${t.fontWeight}`);
1142
- if (t.lineHeight)
1143
- styles.push(`line-height:${t.lineHeight}`);
1144
- if (t.letterSpacing)
1145
- styles.push(`letter-spacing:${t.letterSpacing}`);
1146
- return {
1147
- id: `design-system:typography-${name}`,
1148
- title: name,
1149
- category,
1150
- subCategory: "Typography",
1151
- prompt: `Apply typography preset "${name}" from DESIGN.md: ${desc}.`,
1152
- presentation: {
1153
- type: "html-fragment",
1154
- html: `<div style="font-family:sans-serif;">
1155
- <div style="${styles.join(";")};margin:0;">The quick brown fox</div>
1156
- <div style="font-size:10px;color:#888;margin-top:6px;font-family:monospace;">${esc(name)}: ${esc(desc)}</div>
1157
- </div>`
1158
- }
1159
- };
1160
- });
1267
+ };
1161
1268
  }
1162
- function buildSpacingItems(tokens, category) {
1163
- return Object.entries(tokens.spacing).map(([name, value]) => ({
1269
+ function buildSpacingItem(name, value, category) {
1270
+ return {
1164
1271
  id: `design-system:spacing-${name}`,
1165
- title: `${name} (${value})`,
1272
+ ...PLUGIN_ATTRS,
1273
+ title: name,
1166
1274
  category,
1167
1275
  subCategory: "Spacing",
1168
1276
  prompt: `Use spacing token "${name}" with value ${value} from DESIGN.md for padding, margin, or gap.`,
1277
+ ...spacingBrowseSize(value) ? { browse: { size: spacingBrowseSize(value) } } : {},
1278
+ interaction: {
1279
+ mode: "stage-place",
1280
+ stagePromptTemplate: `Apply the spacing token "${name}" (${value}) from DESIGN.md at this location.`
1281
+ },
1169
1282
  presentation: {
1170
1283
  type: "html-fragment",
1171
- html: `<div style="display:flex;align-items:center;gap:8px;font-family:sans-serif;">
1284
+ html: scopedFragment(`<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:sans-serif;height:100%;">
1172
1285
  <div style="display:flex;align-items:center;gap:0;">
1173
1286
  <div style="width:16px;height:24px;background:#cbd5e1;border-radius:2px 0 0 2px;"></div>
1174
1287
  <div style="width:${esc(value)};height:24px;background:repeating-linear-gradient(45deg,#e0f2fe,#e0f2fe 2px,#bae6fd 2px,#bae6fd 4px);"></div>
1175
1288
  <div style="width:16px;height:24px;background:#cbd5e1;border-radius:0 2px 2px 0;"></div>
1176
1289
  </div>
1177
- <div>
1178
- <span style="font-size:12px;font-weight:600;">${esc(name)}</span>
1179
- <span style="font-size:11px;color:#888;font-family:monospace;margin-left:4px;">${esc(value)}</span>
1180
- </div>
1181
- </div>`
1290
+ <div style="font-size:10px;color:#888;font-family:monospace;">${esc(value)}</div>
1291
+ </div>`)
1182
1292
  }
1183
- }));
1293
+ };
1184
1294
  }
1185
- function buildRoundedItems(tokens, category) {
1186
- return Object.entries(tokens.rounded).map(([name, value]) => ({
1295
+ function buildRoundedItem(name, value, category) {
1296
+ return {
1187
1297
  id: `design-system:rounded-${name}`,
1188
- title: `${name} (${value})`,
1298
+ ...PLUGIN_ATTRS,
1299
+ title: name,
1189
1300
  category,
1190
1301
  subCategory: "Rounded",
1302
+ browse: { size: "small" },
1191
1303
  prompt: `Use border-radius token "${name}" with value ${value} from DESIGN.md.`,
1304
+ interaction: {
1305
+ mode: "stage-place",
1306
+ stagePromptTemplate: `Apply the border-radius token "${name}" (${value}) from DESIGN.md at this location.`
1307
+ },
1192
1308
  presentation: {
1193
1309
  type: "html-fragment",
1194
- html: `<div style="display:flex;align-items:center;gap:10px;font-family:sans-serif;">
1195
- <div style="width:40px;height:40px;border-radius:${esc(value)};background:#e2e8f0;border:1.5px solid #94a3b8;flex-shrink:0;"></div>
1196
- <div>
1197
- <div style="font-size:12px;font-weight:600;">${esc(name)}</div>
1198
- <div style="font-size:11px;color:#888;font-family:monospace;">${esc(value)}</div>
1199
- </div>
1200
- </div>`
1310
+ html: scopedFragment(`<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:sans-serif;height:100%;">
1311
+ <div style="width:40px;height:40px;border-radius:${esc(value)};background:#e2e8f0;border:1.5px solid #94a3b8;"></div>
1312
+ <div style="font-size:10px;color:#888;font-family:monospace;">${esc(value)}</div>
1313
+ </div>`)
1201
1314
  }
1202
- }));
1315
+ };
1316
+ }
1317
+ function formatTypographyCssValue(value) {
1318
+ return String(value);
1319
+ }
1320
+ function buildTypographyItem(name, typography, category) {
1321
+ const parts = [];
1322
+ if (typography.fontFamily) parts.push(typography.fontFamily);
1323
+ if (typography.fontSize) parts.push(typography.fontSize);
1324
+ if (typography.fontWeight != null) {
1325
+ parts.push(`weight ${formatTypographyCssValue(typography.fontWeight)}`);
1326
+ }
1327
+ if (typography.lineHeight != null) {
1328
+ parts.push(`line-height ${formatTypographyCssValue(typography.lineHeight)}`);
1329
+ }
1330
+ if (typography.letterSpacing) parts.push(`letter-spacing ${typography.letterSpacing}`);
1331
+ const desc = parts.join(", ");
1332
+ const styles = [];
1333
+ if (typography.fontFamily) styles.push(`font-family:${typography.fontFamily},sans-serif`);
1334
+ if (typography.fontSize) styles.push(`font-size:${typography.fontSize}`);
1335
+ if (typography.fontWeight != null) {
1336
+ styles.push(`font-weight:${formatTypographyCssValue(typography.fontWeight)}`);
1337
+ }
1338
+ if (typography.lineHeight != null) {
1339
+ styles.push(`line-height:${formatTypographyCssValue(typography.lineHeight)}`);
1340
+ }
1341
+ if (typography.letterSpacing) styles.push(`letter-spacing:${typography.letterSpacing}`);
1342
+ const isLargeFont = isLargeFontSize(typography.fontSize);
1343
+ return {
1344
+ id: `design-system:typography-${name}`,
1345
+ ...PLUGIN_ATTRS,
1346
+ title: name,
1347
+ category,
1348
+ subCategory: "Typography",
1349
+ prompt: `Apply typography preset "${name}" from DESIGN.md: ${desc}.`,
1350
+ interaction: {
1351
+ mode: "stage-place",
1352
+ stagePromptTemplate: `Apply the typography preset "${name}" (${desc}) from DESIGN.md at this location.`
1353
+ },
1354
+ ...isLargeFont ? { browse: { size: "large" } } : {},
1355
+ presentation: {
1356
+ type: "html-fragment",
1357
+ html: scopedFragment(`<div style="font-family:sans-serif;">
1358
+ <div style="${styles.join(";")};margin:0;">The quick brown fox</div>
1359
+ <div style="font-size:10px;color:#888;margin-top:6px;font-family:monospace;">${esc(name)}: ${esc(desc)}</div>
1360
+ </div>`)
1361
+ }
1362
+ };
1363
+ }
1364
+ function buildColorItems(tokens, category) {
1365
+ return Object.entries(tokens.colors).map(
1366
+ ([name, value]) => buildColorItem(name, value, category)
1367
+ );
1368
+ }
1369
+ function buildTypographyItems(tokens, category) {
1370
+ return Object.entries(tokens.typography).map(
1371
+ ([name, typography]) => buildTypographyItem(name, typography, category)
1372
+ );
1373
+ }
1374
+ function buildSpacingItems(tokens, category) {
1375
+ return Object.entries(tokens.spacing).map(
1376
+ ([name, value]) => buildSpacingItem(name, value, category)
1377
+ );
1378
+ }
1379
+ function buildRoundedItems(tokens, category) {
1380
+ return Object.entries(tokens.rounded).map(
1381
+ ([name, value]) => buildRoundedItem(name, value, category)
1382
+ );
1203
1383
  }
1204
1384
  function buildBreakpointItems(tokens, category) {
1205
1385
  return Object.entries(tokens.breakpoints).map(([name, value]) => ({
1206
1386
  id: `design-system:breakpoint-${name}`,
1387
+ ...PLUGIN_ATTRS,
1207
1388
  title: `${name} (${value})`,
1208
1389
  category,
1209
1390
  subCategory: "Breakpoints",
1210
- prompt: `Use breakpoint "${name}" at max-width ${value} from DESIGN.md: @media (max-width: ${value}) { ... }`
1391
+ prompt: `Use breakpoint "${name}" at max-width ${value} from DESIGN.MD: @media (max-width: ${value}) { ... }`,
1392
+ interaction: {
1393
+ mode: "stage-place",
1394
+ stagePromptTemplate: `Add a responsive breakpoint at "${name}" (${value}) from DESIGN.md for this element.`
1395
+ }
1211
1396
  }));
1212
1397
  }
1213
1398
  function buildAnimationItems(tokens, category) {
1214
1399
  return Object.entries(tokens.animations).map(([name, preset]) => {
1215
1400
  const parts = [];
1216
- if (preset.duration)
1217
- parts.push(preset.duration);
1218
- if (preset.easing)
1219
- parts.push(preset.easing);
1401
+ if (preset.duration) parts.push(preset.duration);
1402
+ if (preset.easing) parts.push(preset.easing);
1220
1403
  const desc = parts.join(", ");
1221
1404
  const dur = preset.duration || "300ms";
1222
1405
  const ease = preset.easing || "ease";
1223
1406
  return {
1224
1407
  id: `design-system:animation-${name}`,
1408
+ ...PLUGIN_ATTRS,
1225
1409
  title: name,
1226
1410
  category,
1227
1411
  subCategory: "Animations",
1228
1412
  prompt: `Use animation preset "${name}" from DESIGN.md: ${desc}. Apply to transition-duration and transition-timing-function.`,
1413
+ interaction: {
1414
+ mode: "stage-place",
1415
+ stagePromptTemplate: `Apply the animation preset "${name}" (${desc}) from DESIGN.md to this element's transitions.`
1416
+ },
1229
1417
  presentation: {
1230
1418
  type: "html-fragment",
1231
- html: `<div style="font-family:sans-serif;">
1232
- <style>
1233
- @scope {
1234
- .anim-preview { width:40px;height:40px;border-radius:6px;background:#8b5cf6;transition:transform ${esc(dur)} ${esc(ease)};cursor:pointer; }
1235
- .anim-preview:hover { transform:scale(1.3); }
1236
- }
1237
- </style>
1238
- <div style="display:flex;align-items:center;gap:10px;">
1419
+ html: scopedFragment(
1420
+ `<div style="display:flex;align-items:center;gap:10px;font-family:sans-serif;">
1239
1421
  <div class="anim-preview"></div>
1240
1422
  <div>
1241
1423
  <div style="font-size:12px;font-weight:600;">${esc(name)}</div>
1242
1424
  <div style="font-size:11px;color:#888;font-family:monospace;">${esc(desc)}</div>
1243
1425
  <div style="font-size:10px;color:#aaa;">hover to preview</div>
1244
1426
  </div>
1245
- </div>
1246
- </div>`
1427
+ </div>`,
1428
+ `.anim-preview { width:40px;height:40px;border-radius:6px;background:#8b5cf6;transition:transform ${esc(dur)} ${esc(ease)};cursor:pointer; } .anim-preview:hover { transform:scale(1.3); }`
1429
+ )
1247
1430
  }
1248
1431
  };
1249
1432
  });
@@ -1264,63 +1447,125 @@ function buildComponentItems(tokens, category) {
1264
1447
  borderColor: "border-color"
1265
1448
  };
1266
1449
  for (const [prop, value] of Object.entries(spec)) {
1267
- if (prop === "typography")
1268
- continue;
1450
+ if (prop === "typography") continue;
1269
1451
  const cssProp = propertyMapping[prop] || prop;
1270
1452
  previewStyles.push(`${cssProp}:${value}`);
1271
1453
  }
1272
- if (!spec.padding)
1273
- previewStyles.push("padding:8px 16px");
1274
- if (spec.borderColor)
1275
- previewStyles.push("border:1.5px solid " + spec.borderColor);
1454
+ if (!spec.padding) previewStyles.push("padding:8px 16px");
1455
+ if (spec.borderColor) previewStyles.push("border:1.5px solid " + spec.borderColor);
1276
1456
  return {
1277
1457
  id: `design-system:component-${name}`,
1458
+ ...PLUGIN_ATTRS,
1278
1459
  title: name,
1279
1460
  category,
1280
1461
  subCategory: "Components",
1281
1462
  prompt: `Apply the "${name}" component spec from DESIGN.md. Required styles: ${props}. The design-system validator will flag mismatches.`,
1463
+ interaction: {
1464
+ mode: "stage-place",
1465
+ stagePromptTemplate: `Apply the "${name}" component styles (${props}) from DESIGN.md to this element.`
1466
+ },
1282
1467
  presentation: {
1283
1468
  type: "html-fragment",
1284
- html: `<div style="font-family:sans-serif;">
1469
+ html: scopedFragment(`<div style="font-family:sans-serif;">
1285
1470
  <div style="${previewStyles.join(";")};display:inline-block;font-size:13px;min-width:80px;text-align:center;">${esc(name)}</div>
1286
1471
  <div style="font-size:10px;color:#888;margin-top:6px;font-family:monospace;">${esc(props)}</div>
1287
- </div>`
1472
+ </div>`)
1288
1473
  }
1289
1474
  };
1290
1475
  });
1291
1476
  }
1477
+ const ADD_MENU_GENERATED_REL = "agent-kit/aiditor/add-menu/design-system.generated.yaml";
1478
+ function readAddMenuCatalog(projectRoot) {
1479
+ const catalogPath = path.join(projectRoot, ADD_MENU_GENERATED_REL);
1480
+ if (!fs.existsSync(catalogPath)) {
1481
+ return [];
1482
+ }
1483
+ const parsed = load(fs.readFileSync(catalogPath, "utf-8"));
1484
+ if (!Array.isArray(parsed.items)) {
1485
+ return [];
1486
+ }
1487
+ return parsed.items;
1488
+ }
1489
+ function writeAddMenuCatalog(projectRoot, items) {
1490
+ const outputPath = path.join(projectRoot, ADD_MENU_GENERATED_REL);
1491
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1492
+ fs.writeFileSync(outputPath, dump({ items }, { lineWidth: 120, noRefs: true }), "utf-8");
1493
+ return ADD_MENU_GENERATED_REL;
1494
+ }
1495
+ const AIDITOR_SETTINGS_OUTPUT_REL = "agent-kit/aiditor/settings/design-system-validator.yaml";
1496
+ const SETTINGS_TEMPLATE_REL = "agent-kit/aiditor/settings.template.yaml";
1497
+ function resolvePackagedAgentKitPath(relativePath, moduleUrl = import.meta.url) {
1498
+ let directory = path.dirname(fileURLToPath(moduleUrl));
1499
+ for (let depth = 0; depth < 4; depth++) {
1500
+ const candidate = path.join(directory, relativePath);
1501
+ if (fs.existsSync(candidate)) {
1502
+ return candidate;
1503
+ }
1504
+ const parent = path.dirname(directory);
1505
+ if (parent === directory) {
1506
+ break;
1507
+ }
1508
+ directory = parent;
1509
+ }
1510
+ return null;
1511
+ }
1512
+ function writeAiditorSettingsContribution(projectRoot, templatePath, force = false) {
1513
+ const outputPath = path.join(projectRoot, AIDITOR_SETTINGS_OUTPUT_REL);
1514
+ if (fs.existsSync(outputPath) && !force) {
1515
+ return null;
1516
+ }
1517
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1518
+ fs.copyFileSync(templatePath, outputPath);
1519
+ return AIDITOR_SETTINGS_OUTPUT_REL;
1520
+ }
1521
+ function materializeDesignSystemAiditorSettings(projectRoot, force = false) {
1522
+ const templatePath = resolvePackagedAgentKitPath(SETTINGS_TEMPLATE_REL);
1523
+ if (!templatePath) {
1524
+ return null;
1525
+ }
1526
+ return writeAiditorSettingsContribution(projectRoot, templatePath, force);
1527
+ }
1292
1528
  function findAllDesignMdFiles(projectRoot) {
1293
1529
  const pagesRoot = path.join(projectRoot, "src", "pages");
1294
1530
  const files = [];
1295
1531
  const rootDesignMd = path.join(projectRoot, "DESIGN.md");
1296
- if (fs.existsSync(rootDesignMd))
1297
- files.push(rootDesignMd);
1532
+ if (fs.existsSync(rootDesignMd)) files.push(rootDesignMd);
1298
1533
  if (fs.existsSync(pagesRoot)) {
1299
- let walk = function(dir) {
1300
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1301
- if (entry.isFile() && entry.name === "DESIGN.md") {
1302
- files.push(path.join(dir, entry.name));
1303
- } else if (entry.isDirectory() && !entry.name.startsWith(".")) {
1304
- walk(path.join(dir, entry.name));
1305
- }
1306
- }
1307
- };
1308
- walk(pagesRoot);
1534
+ walkDesignMdDir(pagesRoot, files);
1309
1535
  }
1310
1536
  return files;
1311
1537
  }
1538
+ function walkDesignMdDir(dir, files) {
1539
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1540
+ if (entry.isFile() && entry.name === "DESIGN.md") {
1541
+ files.push(path.join(dir, entry.name));
1542
+ } else if (entry.isDirectory() && !entry.name.startsWith(".")) {
1543
+ walkDesignMdDir(path.join(dir, entry.name), files);
1544
+ }
1545
+ }
1546
+ }
1547
+ function categoryName(designMdPath, projectRoot, tokens) {
1548
+ if (tokens.name) return tokens.name;
1549
+ const rel = path.relative(projectRoot, designMdPath);
1550
+ if (rel === "DESIGN.md") return "Design System";
1551
+ const dir = path.dirname(rel).replace(/^src\/pages\/?/, "");
1552
+ return dir ? `Design System (${dir})` : "Design System";
1553
+ }
1312
1554
  async function generateDesignSystemAgentKit(ctx) {
1313
1555
  const designMdFiles = findAllDesignMdFiles(ctx.projectRoot);
1556
+ const settingsGenerated = materializeDesignSystemAiditorSettings(ctx.projectRoot, ctx.force);
1314
1557
  if (designMdFiles.length === 0) {
1315
- return { agentKitCreated: [], message: "No DESIGN.md found in project" };
1558
+ return {
1559
+ agentKitCreated: settingsGenerated ? [settingsGenerated] : [],
1560
+ message: settingsGenerated ? "AIditor settings materialized; no DESIGN.md found for add-menu" : "No DESIGN.md found in project"
1561
+ };
1316
1562
  }
1317
1563
  const allItems = [];
1318
1564
  const seen = /* @__PURE__ */ new Set();
1319
1565
  for (const filePath of designMdFiles) {
1320
1566
  const content = fs.readFileSync(filePath, "utf-8");
1321
1567
  const tokens = parseDesignMd(content);
1322
- if (!tokens)
1323
- continue;
1568
+ if (!tokens) continue;
1324
1569
  const category = categoryName(filePath, ctx.projectRoot, tokens);
1325
1570
  const items = [
1326
1571
  ...buildColorItems(tokens, category),
@@ -1339,20 +1584,244 @@ async function generateDesignSystemAgentKit(ctx) {
1339
1584
  }
1340
1585
  }
1341
1586
  if (allItems.length === 0) {
1342
- return { agentKitCreated: [], message: "DESIGN.md found but no tokens defined" };
1587
+ return {
1588
+ agentKitCreated: settingsGenerated ? [settingsGenerated] : [],
1589
+ message: "DESIGN.md found but no tokens defined"
1590
+ };
1591
+ }
1592
+ writeAddMenuCatalog(ctx.projectRoot, allItems);
1593
+ const agentKitCreated = [ADD_MENU_GENERATED_REL];
1594
+ if (settingsGenerated) {
1595
+ agentKitCreated.push(settingsGenerated);
1343
1596
  }
1344
- const outputPath = path.join(ctx.projectRoot, ADD_MENU_OUTPUT_REL);
1345
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1346
- fs.writeFileSync(outputPath, yaml.dump({ items: allItems }, { lineWidth: 120 }), "utf-8");
1347
1597
  return {
1348
- agentKitCreated: [ADD_MENU_OUTPUT_REL],
1598
+ agentKitCreated,
1349
1599
  message: `Generated ${allItems.length} add-menu items from ${designMdFiles.length} DESIGN.md file(s)`
1350
1600
  };
1351
1601
  }
1602
+ const DESIGN_VALIDATORS = [
1603
+ { name: "design-tokens", run: validateTokens },
1604
+ { name: "design-components", run: validateComponents },
1605
+ { name: "design-structure", run: validateStructure },
1606
+ { name: "design-contrast", run: validateContrast }
1607
+ ];
1608
+ async function findJayHtmlFiles(scanDir) {
1609
+ const files = [];
1610
+ async function walk(dir) {
1611
+ let entries;
1612
+ try {
1613
+ entries = await promises.readdir(dir, { withFileTypes: true });
1614
+ } catch {
1615
+ return;
1616
+ }
1617
+ for (const entry of entries) {
1618
+ const fullPath = path__default.join(dir, entry.name);
1619
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
1620
+ await walk(fullPath);
1621
+ } else if (entry.isFile() && entry.name.endsWith(JAY_EXTENSION)) {
1622
+ files.push(fullPath);
1623
+ }
1624
+ }
1625
+ }
1626
+ await walk(scanDir);
1627
+ return files;
1628
+ }
1629
+ function toAnalysisFinding(relativePath, validator, finding) {
1630
+ if (!finding.message) return null;
1631
+ return {
1632
+ file: relativePath,
1633
+ validator,
1634
+ severity: finding.severity === "error" ? "error" : "warning",
1635
+ message: finding.message,
1636
+ suggestion: finding.suggestion
1637
+ };
1638
+ }
1639
+ function buildValidationContext(projectRoot, relativePath, parsed) {
1640
+ return {
1641
+ filePath: relativePath,
1642
+ body: parsed.body,
1643
+ css: parsed.css,
1644
+ head: parsed.headMeta,
1645
+ headlessImports: parsed.headlessImports.map((imp) => ({
1646
+ key: imp.key,
1647
+ contractName: imp.contractName,
1648
+ contract: imp.contract ? {
1649
+ name: imp.contract.name,
1650
+ tags: imp.contract.tags,
1651
+ props: imp.contract.props,
1652
+ params: imp.contract.params
1653
+ } : void 0
1654
+ })),
1655
+ projectRoot
1656
+ };
1657
+ }
1658
+ async function runDesignSystemAnalysis(projectRoot) {
1659
+ const pagesDir = path__default.join(projectRoot, "src", "pages");
1660
+ const componentsDir = path__default.join(projectRoot, "src", "components");
1661
+ const jayHtmlFiles = [
1662
+ ...await findJayHtmlFiles(pagesDir).catch(() => []),
1663
+ ...await findJayHtmlFiles(componentsDir).catch(() => [])
1664
+ ];
1665
+ const findings = [];
1666
+ for (const jayHtmlPath of jayHtmlFiles) {
1667
+ const relativePath = path__default.relative(projectRoot, jayHtmlPath);
1668
+ const filename = path__default.basename(jayHtmlPath.replace(JAY_EXTENSION, ""));
1669
+ const dirname = path__default.dirname(jayHtmlPath);
1670
+ let parsed;
1671
+ try {
1672
+ const content = await promises.readFile(jayHtmlPath, "utf-8");
1673
+ const parseResult = await parseJayFile(
1674
+ content,
1675
+ filename,
1676
+ dirname,
1677
+ {},
1678
+ JAY_IMPORT_RESOLVER,
1679
+ projectRoot
1680
+ );
1681
+ if (!parseResult.value) continue;
1682
+ parsed = parseResult.value;
1683
+ } catch {
1684
+ continue;
1685
+ }
1686
+ const ctx = buildValidationContext(projectRoot, relativePath, parsed);
1687
+ for (const validator of DESIGN_VALIDATORS) {
1688
+ const validatorFindings = await Promise.resolve(validator.run(ctx));
1689
+ for (const finding of validatorFindings) {
1690
+ const mapped = toAnalysisFinding(relativePath, validator.name, finding);
1691
+ if (mapped) findings.push(mapped);
1692
+ }
1693
+ }
1694
+ }
1695
+ const errorCount = findings.filter((finding) => finding.severity === "error").length;
1696
+ const warningCount = findings.filter((finding) => finding.severity === "warning").length;
1697
+ return {
1698
+ filesScanned: jayHtmlFiles.length,
1699
+ findings,
1700
+ errorCount,
1701
+ warningCount
1702
+ };
1703
+ }
1704
+ function countAddMenuItems(projectRoot) {
1705
+ return readAddMenuCatalog(projectRoot).length;
1706
+ }
1707
+ function summarizeDesignMd(projectRoot, filePath) {
1708
+ const content = fs.readFileSync(filePath, "utf-8");
1709
+ const tokens = parseDesignMd(content);
1710
+ if (!tokens) return null;
1711
+ const relativePath = path.relative(projectRoot, filePath);
1712
+ const name = tokens.name ?? (relativePath === "DESIGN.md" ? "Design System" : `Design System (${path.dirname(relativePath)})`);
1713
+ return {
1714
+ relativePath,
1715
+ name,
1716
+ colorCount: Object.keys(tokens.colors).length,
1717
+ typographyCount: Object.keys(tokens.typography).length,
1718
+ spacingCount: Object.keys(tokens.spacing).length,
1719
+ componentCount: Object.keys(tokens.components).length
1720
+ };
1721
+ }
1722
+ const getDesignSystemSettingsStatus = makeJayQuery(
1723
+ "designSystem.getDesignSystemSettingsStatus"
1724
+ ).withHandler(async () => {
1725
+ const projectRoot = process.cwd();
1726
+ const designMdPaths = findAllDesignMdFiles(projectRoot);
1727
+ const designFiles = designMdPaths.map((filePath) => summarizeDesignMd(projectRoot, filePath)).filter((summary) => summary !== null);
1728
+ return {
1729
+ hasDesignMd: designFiles.length > 0,
1730
+ designFiles,
1731
+ addMenuItemCount: countAddMenuItems(projectRoot),
1732
+ addMenuCatalogRel: ADD_MENU_GENERATED_REL,
1733
+ message: designFiles.length > 0 ? `${designFiles.length} DESIGN.md file(s); ${countAddMenuItems(projectRoot)} Add Menu items.` : "No DESIGN.md found — add one at the project root or under src/pages/."
1734
+ };
1735
+ });
1736
+ const runDesignSystemAnalysisAction = makeJayQuery(
1737
+ "designSystem.runDesignSystemAnalysis"
1738
+ ).withHandler(async () => {
1739
+ const projectRoot = process.cwd();
1740
+ return runDesignSystemAnalysis(projectRoot);
1741
+ });
1742
+ const loadDesignSystemAddMenuCatalog = makeJayQuery(
1743
+ "designSystem.loadAddMenuCatalog"
1744
+ ).withHandler(async () => {
1745
+ const projectRoot = process.cwd();
1746
+ const items = readAddMenuCatalog(projectRoot);
1747
+ return {
1748
+ items,
1749
+ outputRel: ADD_MENU_GENERATED_REL,
1750
+ itemCount: items.length
1751
+ };
1752
+ });
1753
+ const saveDesignSystemAddMenuCatalog = makeJayAction(
1754
+ "designSystem.saveAddMenuCatalog"
1755
+ ).withHandler(async (input) => {
1756
+ const projectRoot = process.cwd();
1757
+ const outputRel = writeAddMenuCatalog(projectRoot, input.items);
1758
+ return {
1759
+ itemCount: input.items.length,
1760
+ outputRel,
1761
+ message: `Saved ${input.items.length} add-menu items to ${outputRel}.`
1762
+ };
1763
+ });
1764
+ const regenerateDesignSystemAddMenu = makeJayQuery(
1765
+ "designSystem.regenerateDesignSystemAddMenu"
1766
+ ).withHandler(async () => {
1767
+ const projectRoot = process.cwd();
1768
+ const ctx = {
1769
+ projectRoot,
1770
+ referencesDir: path.join(projectRoot, "agent-kit", "references", "design-system-validator"),
1771
+ force: true
1772
+ };
1773
+ const result = await generateDesignSystemAgentKit(ctx);
1774
+ const itemCount = countAddMenuItems(projectRoot);
1775
+ return {
1776
+ itemCount,
1777
+ outputRel: ADD_MENU_GENERATED_REL,
1778
+ agentKitCreated: result.agentKitCreated,
1779
+ message: result.message
1780
+ };
1781
+ });
1782
+ async function renderFast() {
1783
+ return phaseOutput({
1784
+ statusMessage: "",
1785
+ statusTone: "info",
1786
+ showStatusMessage: false,
1787
+ catalogLoading: true,
1788
+ showNoCatalog: false,
1789
+ addMenuListRows: [],
1790
+ showAddMenuEditor: false,
1791
+ editorTitle: "",
1792
+ editorSubCategory: "",
1793
+ editorTokenValue: "",
1794
+ editorFontSize: "",
1795
+ editorFontWeight: "",
1796
+ editorFontFamily: "",
1797
+ showTypographyFields: false,
1798
+ showTokenValueField: false,
1799
+ editorReadOnly: false,
1800
+ editorReadOnlyReason: "",
1801
+ unsavedChangeCount: 0,
1802
+ saveButtonLabel: "Save and sync",
1803
+ saveDisabled: true,
1804
+ discardDisabled: true,
1805
+ showAnalysisSection: false,
1806
+ showReport: false,
1807
+ reportSummary: "",
1808
+ reportBody: ""
1809
+ }, {});
1810
+ }
1811
+ const designSystemSettingsPage = makeJayStackComponent().withProps().withFastRender(renderFast).withInteractiveMark();
1352
1812
  export {
1813
+ ADD_MENU_GENERATED_REL,
1814
+ designSystemSettingsPage,
1815
+ fontFallback,
1353
1816
  generateDesignSystemAgentKit,
1817
+ getDesignSystemSettingsStatus,
1818
+ loadDesignSystemAddMenuCatalog,
1819
+ regenerateDesignSystemAddMenu,
1820
+ runDesignSystemAnalysisAction as runDesignSystemAnalysis,
1821
+ saveDesignSystemAddMenuCatalog,
1354
1822
  validateComponents,
1355
1823
  validateContrast,
1824
+ validateFontFallbacks,
1356
1825
  validateStructure,
1357
1826
  validateTokens
1358
1827
  };