@jay-framework/design-system-validator 0.22.2 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-kit/aiditor/settings.template.yaml +2 -0
- package/agent-kit/designer/design-system.md +16 -2
- package/agent-kit/designer/font-fallback-patterns.md +78 -0
- package/dist/actions/font-fallback.jay-action +12 -0
- package/dist/index.client.d.ts +7 -0
- package/dist/index.client.js +657 -0
- package/dist/index.d.ts +91 -1
- package/dist/index.js +823 -297
- package/dist/pages/settings/page.jay-html +415 -0
- package/dist/pages/settings/page.jay-html.d.ts +74 -0
- package/package.json +24 -8
- package/plugin.yaml +24 -0
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
|
-
|
|
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,19 +81,17 @@ 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;
|
|
96
93
|
}
|
|
97
|
-
const ALLOW_COMMENT = "design-system: allow";
|
|
94
|
+
const ALLOW_COMMENT$1 = "design-system: allow";
|
|
98
95
|
function computeSpecificity(selector) {
|
|
99
96
|
let result = [0, 0, 0];
|
|
100
97
|
selectorParser((selectors) => {
|
|
@@ -106,15 +103,15 @@ function computeSpecificity(selector) {
|
|
|
106
103
|
}).processSync(selector);
|
|
107
104
|
return result;
|
|
108
105
|
}
|
|
109
|
-
function hasAllowComment(decl) {
|
|
106
|
+
function hasAllowComment$1(decl) {
|
|
110
107
|
const next = decl.next();
|
|
111
|
-
if (next?.type === "comment" && next.text.trim() === ALLOW_COMMENT) {
|
|
108
|
+
if (next?.type === "comment" && next.text.trim() === ALLOW_COMMENT$1) {
|
|
112
109
|
return true;
|
|
113
110
|
}
|
|
114
111
|
const raw = decl.raws.value;
|
|
115
112
|
if (raw && typeof raw === "object" && "raw" in raw) {
|
|
116
113
|
const rawStr = raw.raw;
|
|
117
|
-
if (rawStr.includes(`/*${ALLOW_COMMENT}*/`) || rawStr.includes(`/* ${ALLOW_COMMENT} */`)) {
|
|
114
|
+
if (rawStr.includes(`/*${ALLOW_COMMENT$1}*/`) || rawStr.includes(`/* ${ALLOW_COMMENT$1} */`)) {
|
|
118
115
|
return true;
|
|
119
116
|
}
|
|
120
117
|
}
|
|
@@ -151,14 +148,13 @@ function parseRules(css, sourceIndexOffset = 0, customProperties) {
|
|
|
151
148
|
property: decl.prop,
|
|
152
149
|
value: decl.value,
|
|
153
150
|
important: decl.important,
|
|
154
|
-
allowed: hasAllowComment(decl)
|
|
151
|
+
allowed: hasAllowComment$1(decl)
|
|
155
152
|
});
|
|
156
153
|
});
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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$
|
|
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
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,476 @@ const validateContrast = (ctx) => {
|
|
|
1086
1013
|
}
|
|
1087
1014
|
return findings;
|
|
1088
1015
|
};
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
return
|
|
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");
|
|
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;
|
|
1098
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
|
+
const ALLOW_COMMENT = "design-system: allow";
|
|
1185
|
+
const VAR_REGEX = /var\(\s*(--[a-zA-Z0-9-]+)(?:\s*,\s*([^)]+))?\s*\)/g;
|
|
1186
|
+
function hasAllowComment(decl) {
|
|
1187
|
+
const next = decl.next();
|
|
1188
|
+
if (next?.type === "comment" && next.text.trim() === ALLOW_COMMENT) {
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
const raw = decl.raws.value;
|
|
1192
|
+
if (raw && typeof raw === "object" && "raw" in raw) {
|
|
1193
|
+
const rawStr = raw.raw;
|
|
1194
|
+
if (rawStr.includes(`/*${ALLOW_COMMENT}*/`) || rawStr.includes(`/* ${ALLOW_COMMENT} */`)) {
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
const validateUndefinedVars = (ctx) => {
|
|
1201
|
+
if (!ctx.css) return [];
|
|
1202
|
+
const findings = [];
|
|
1203
|
+
const definedVars = /* @__PURE__ */ new Set();
|
|
1204
|
+
const usedVars = /* @__PURE__ */ new Map();
|
|
1205
|
+
const root = postcss.parse(ctx.css);
|
|
1206
|
+
root.walkDecls((decl) => {
|
|
1207
|
+
if (decl.prop.startsWith("--")) {
|
|
1208
|
+
definedVars.add(decl.prop);
|
|
1209
|
+
}
|
|
1210
|
+
if (decl.value.includes("var(") && !hasAllowComment(decl)) {
|
|
1211
|
+
const selector = decl.parent && "selector" in decl.parent ? decl.parent.selector : "[unknown]";
|
|
1212
|
+
let match;
|
|
1213
|
+
VAR_REGEX.lastIndex = 0;
|
|
1214
|
+
while ((match = VAR_REGEX.exec(decl.value)) !== null) {
|
|
1215
|
+
const varName = match[1];
|
|
1216
|
+
if (!usedVars.has(varName)) {
|
|
1217
|
+
usedVars.set(varName, {
|
|
1218
|
+
property: decl.prop,
|
|
1219
|
+
selector,
|
|
1220
|
+
hasFallback: match[2] !== void 0,
|
|
1221
|
+
fallbackValue: match[2]?.trim()
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
for (const [varName, usage] of usedVars) {
|
|
1228
|
+
if (definedVars.has(varName)) continue;
|
|
1229
|
+
const fallbackNote = usage.hasFallback ? ` (falls back to "${usage.fallbackValue}")` : "";
|
|
1230
|
+
findings.push({
|
|
1231
|
+
severity: "warning",
|
|
1232
|
+
message: `CSS variable "${varName}" is used but never defined${fallbackNote}`,
|
|
1233
|
+
suggestion: `Define ${varName} in a :root block, or replace with a DESIGN.md token value directly.
|
|
1234
|
+
To suppress: add /* design-system: allow */ on the same line or the line after the declaration.
|
|
1235
|
+
See agent-kit/designer/design-system.md for usage guide.`
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
return findings;
|
|
1239
|
+
};
|
|
1240
|
+
async function loadMetrics(familyName) {
|
|
1241
|
+
const camelCase = fontFamilyToCamelCase(familyName);
|
|
1242
|
+
try {
|
|
1243
|
+
const mod = await import(`@capsizecss/metrics/${camelCase}`);
|
|
1244
|
+
return mod.default;
|
|
1245
|
+
} catch {
|
|
1246
|
+
throw new Error(
|
|
1247
|
+
`Font "${familyName}" not found in @capsizecss/metrics. Use a known Google Font or system font name (e.g. "Inter", "Arial", "Georgia").`
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const fontFallback = makeJayAction("designSystem.fontFallback").withHandler(
|
|
1252
|
+
async (input) => {
|
|
1253
|
+
const primaryMetrics = await loadMetrics(input.primary);
|
|
1254
|
+
const fallbackMetrics = await loadMetrics(input.fallback);
|
|
1255
|
+
const result = createFontStack([primaryMetrics, fallbackMetrics]);
|
|
1256
|
+
return {
|
|
1257
|
+
fontFamily: result.fontFamily,
|
|
1258
|
+
fontFaces: result.fontFaces
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
);
|
|
1262
|
+
const PLUGIN_ATTRS = {
|
|
1263
|
+
pluginName: "design-system-validator",
|
|
1264
|
+
packageName: "@jay-framework/design-system-validator"
|
|
1265
|
+
};
|
|
1099
1266
|
function esc(s) {
|
|
1100
1267
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1101
1268
|
}
|
|
1102
|
-
function
|
|
1103
|
-
|
|
1269
|
+
function scopedFragment(innerHtml, scopedCss = "") {
|
|
1270
|
+
const styleBlock = `<style>@scope { ${scopedCss} }</style>`;
|
|
1271
|
+
return `<div>${styleBlock}${innerHtml}</div>`;
|
|
1272
|
+
}
|
|
1273
|
+
function isLargeFontSize(fontSize) {
|
|
1274
|
+
if (!fontSize) return false;
|
|
1275
|
+
if (/\d+(\.\d+)?\s*(vw|vh|vmin|vmax)\b/.test(fontSize)) return true;
|
|
1276
|
+
const clampMatch = fontSize.match(/clamp\([^,]+,[^,]+,\s*([\d.]+)\s*(px|rem|em)/);
|
|
1277
|
+
if (clampMatch) {
|
|
1278
|
+
const max = parseFloat(clampMatch[1]);
|
|
1279
|
+
const unit = clampMatch[2];
|
|
1280
|
+
if (unit === "px") return max > 20;
|
|
1281
|
+
return max > 1.25;
|
|
1282
|
+
}
|
|
1283
|
+
const simple = parseFloat(fontSize);
|
|
1284
|
+
if (isNaN(simple)) return false;
|
|
1285
|
+
if (fontSize.includes("rem") || fontSize.includes("em")) return simple > 1.25;
|
|
1286
|
+
return simple > 20;
|
|
1287
|
+
}
|
|
1288
|
+
function spacingBrowseSize(value) {
|
|
1289
|
+
if (/calc|clamp|var\(/.test(value)) return "large";
|
|
1290
|
+
const px = parseFloat(value);
|
|
1291
|
+
if (!isNaN(px) && !value.includes("rem") && !value.includes("em")) {
|
|
1292
|
+
if (px <= 30) return "small";
|
|
1293
|
+
if (px > 100) return "large";
|
|
1294
|
+
return void 0;
|
|
1295
|
+
}
|
|
1296
|
+
const rem = parseFloat(value);
|
|
1297
|
+
if (!isNaN(rem)) {
|
|
1298
|
+
if (rem <= 2) return "small";
|
|
1299
|
+
if (rem > 6) return "large";
|
|
1300
|
+
}
|
|
1301
|
+
return void 0;
|
|
1302
|
+
}
|
|
1303
|
+
function buildColorItem(name, value, category) {
|
|
1304
|
+
return {
|
|
1104
1305
|
id: `design-system:color-${name}`,
|
|
1105
|
-
|
|
1306
|
+
...PLUGIN_ATTRS,
|
|
1307
|
+
title: name,
|
|
1106
1308
|
category,
|
|
1107
1309
|
subCategory: "Colors",
|
|
1310
|
+
browse: { size: "small" },
|
|
1108
1311
|
prompt: `Use color token "${name}" with value ${value} from DESIGN.md.`,
|
|
1312
|
+
interaction: {
|
|
1313
|
+
mode: "stage-place",
|
|
1314
|
+
stagePromptTemplate: `Apply the color token "${name}" (${value}) from DESIGN.md at this location.`
|
|
1315
|
+
},
|
|
1109
1316
|
presentation: {
|
|
1110
1317
|
type: "html-fragment",
|
|
1111
|
-
html: `<div style="display:flex;align-items:center;gap:
|
|
1112
|
-
<div style="width:40px;height:40px;border-radius:6px;background:${esc(value)};border:1px solid rgba(0,0,0,0.1);
|
|
1113
|
-
<div>
|
|
1114
|
-
|
|
1115
|
-
<div style="font-size:11px;color:#888;font-family:monospace;">${esc(value)}</div>
|
|
1116
|
-
</div>
|
|
1117
|
-
</div>`
|
|
1318
|
+
html: scopedFragment(`<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:sans-serif;height:100%;">
|
|
1319
|
+
<div style="width:40px;height:40px;border-radius:6px;background:${esc(value)};border:1px solid rgba(0,0,0,0.1);"></div>
|
|
1320
|
+
<div style="font-size:10px;color:#888;font-family:monospace;">${esc(value)}</div>
|
|
1321
|
+
</div>`)
|
|
1118
1322
|
}
|
|
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
|
-
});
|
|
1323
|
+
};
|
|
1161
1324
|
}
|
|
1162
|
-
function
|
|
1163
|
-
return
|
|
1325
|
+
function buildSpacingItem(name, value, category) {
|
|
1326
|
+
return {
|
|
1164
1327
|
id: `design-system:spacing-${name}`,
|
|
1165
|
-
|
|
1328
|
+
...PLUGIN_ATTRS,
|
|
1329
|
+
title: name,
|
|
1166
1330
|
category,
|
|
1167
1331
|
subCategory: "Spacing",
|
|
1168
1332
|
prompt: `Use spacing token "${name}" with value ${value} from DESIGN.md for padding, margin, or gap.`,
|
|
1333
|
+
...spacingBrowseSize(value) ? { browse: { size: spacingBrowseSize(value) } } : {},
|
|
1334
|
+
interaction: {
|
|
1335
|
+
mode: "stage-place",
|
|
1336
|
+
stagePromptTemplate: `Apply the spacing token "${name}" (${value}) from DESIGN.md at this location.`
|
|
1337
|
+
},
|
|
1169
1338
|
presentation: {
|
|
1170
1339
|
type: "html-fragment",
|
|
1171
|
-
html: `<div style="display:flex;align-items:center;gap:
|
|
1340
|
+
html: scopedFragment(`<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:sans-serif;height:100%;">
|
|
1172
1341
|
<div style="display:flex;align-items:center;gap:0;">
|
|
1173
1342
|
<div style="width:16px;height:24px;background:#cbd5e1;border-radius:2px 0 0 2px;"></div>
|
|
1174
1343
|
<div style="width:${esc(value)};height:24px;background:repeating-linear-gradient(45deg,#e0f2fe,#e0f2fe 2px,#bae6fd 2px,#bae6fd 4px);"></div>
|
|
1175
1344
|
<div style="width:16px;height:24px;background:#cbd5e1;border-radius:0 2px 2px 0;"></div>
|
|
1176
1345
|
</div>
|
|
1177
|
-
<div>
|
|
1178
|
-
|
|
1179
|
-
<span style="font-size:11px;color:#888;font-family:monospace;margin-left:4px;">${esc(value)}</span>
|
|
1180
|
-
</div>
|
|
1181
|
-
</div>`
|
|
1346
|
+
<div style="font-size:10px;color:#888;font-family:monospace;">${esc(value)}</div>
|
|
1347
|
+
</div>`)
|
|
1182
1348
|
}
|
|
1183
|
-
}
|
|
1349
|
+
};
|
|
1184
1350
|
}
|
|
1185
|
-
function
|
|
1186
|
-
return
|
|
1351
|
+
function buildRoundedItem(name, value, category) {
|
|
1352
|
+
return {
|
|
1187
1353
|
id: `design-system:rounded-${name}`,
|
|
1188
|
-
|
|
1354
|
+
...PLUGIN_ATTRS,
|
|
1355
|
+
title: name,
|
|
1189
1356
|
category,
|
|
1190
1357
|
subCategory: "Rounded",
|
|
1358
|
+
browse: { size: "small" },
|
|
1191
1359
|
prompt: `Use border-radius token "${name}" with value ${value} from DESIGN.md.`,
|
|
1360
|
+
interaction: {
|
|
1361
|
+
mode: "stage-place",
|
|
1362
|
+
stagePromptTemplate: `Apply the border-radius token "${name}" (${value}) from DESIGN.md at this location.`
|
|
1363
|
+
},
|
|
1192
1364
|
presentation: {
|
|
1193
1365
|
type: "html-fragment",
|
|
1194
|
-
html: `<div style="display:flex;align-items:center;gap:
|
|
1195
|
-
<div style="width:40px;height:40px;border-radius:${esc(value)};background:#e2e8f0;border:1.5px solid #94a3b8;
|
|
1196
|
-
<div>
|
|
1197
|
-
|
|
1198
|
-
<div style="font-size:11px;color:#888;font-family:monospace;">${esc(value)}</div>
|
|
1199
|
-
</div>
|
|
1200
|
-
</div>`
|
|
1366
|
+
html: scopedFragment(`<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:sans-serif;height:100%;">
|
|
1367
|
+
<div style="width:40px;height:40px;border-radius:${esc(value)};background:#e2e8f0;border:1.5px solid #94a3b8;"></div>
|
|
1368
|
+
<div style="font-size:10px;color:#888;font-family:monospace;">${esc(value)}</div>
|
|
1369
|
+
</div>`)
|
|
1201
1370
|
}
|
|
1202
|
-
}
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
function formatTypographyCssValue(value) {
|
|
1374
|
+
return String(value);
|
|
1375
|
+
}
|
|
1376
|
+
function buildTypographyItem(name, typography, category) {
|
|
1377
|
+
const parts = [];
|
|
1378
|
+
if (typography.fontFamily) parts.push(typography.fontFamily);
|
|
1379
|
+
if (typography.fontSize) parts.push(typography.fontSize);
|
|
1380
|
+
if (typography.fontWeight != null) {
|
|
1381
|
+
parts.push(`weight ${formatTypographyCssValue(typography.fontWeight)}`);
|
|
1382
|
+
}
|
|
1383
|
+
if (typography.lineHeight != null) {
|
|
1384
|
+
parts.push(`line-height ${formatTypographyCssValue(typography.lineHeight)}`);
|
|
1385
|
+
}
|
|
1386
|
+
if (typography.letterSpacing) parts.push(`letter-spacing ${typography.letterSpacing}`);
|
|
1387
|
+
const desc = parts.join(", ");
|
|
1388
|
+
const styles = [];
|
|
1389
|
+
if (typography.fontFamily) styles.push(`font-family:${typography.fontFamily},sans-serif`);
|
|
1390
|
+
if (typography.fontSize) styles.push(`font-size:${typography.fontSize}`);
|
|
1391
|
+
if (typography.fontWeight != null) {
|
|
1392
|
+
styles.push(`font-weight:${formatTypographyCssValue(typography.fontWeight)}`);
|
|
1393
|
+
}
|
|
1394
|
+
if (typography.lineHeight != null) {
|
|
1395
|
+
styles.push(`line-height:${formatTypographyCssValue(typography.lineHeight)}`);
|
|
1396
|
+
}
|
|
1397
|
+
if (typography.letterSpacing) styles.push(`letter-spacing:${typography.letterSpacing}`);
|
|
1398
|
+
const isLargeFont = isLargeFontSize(typography.fontSize);
|
|
1399
|
+
return {
|
|
1400
|
+
id: `design-system:typography-${name}`,
|
|
1401
|
+
...PLUGIN_ATTRS,
|
|
1402
|
+
title: name,
|
|
1403
|
+
category,
|
|
1404
|
+
subCategory: "Typography",
|
|
1405
|
+
prompt: `Apply typography preset "${name}" from DESIGN.md: ${desc}.`,
|
|
1406
|
+
interaction: {
|
|
1407
|
+
mode: "stage-place",
|
|
1408
|
+
stagePromptTemplate: `Apply the typography preset "${name}" (${desc}) from DESIGN.md at this location.`
|
|
1409
|
+
},
|
|
1410
|
+
...isLargeFont ? { browse: { size: "large" } } : {},
|
|
1411
|
+
presentation: {
|
|
1412
|
+
type: "html-fragment",
|
|
1413
|
+
html: scopedFragment(`<div style="font-family:sans-serif;">
|
|
1414
|
+
<div style="${styles.join(";")};margin:0;">The quick brown fox</div>
|
|
1415
|
+
<div style="font-size:10px;color:#888;margin-top:6px;font-family:monospace;">${esc(name)}: ${esc(desc)}</div>
|
|
1416
|
+
</div>`)
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function buildColorItems(tokens, category) {
|
|
1421
|
+
return Object.entries(tokens.colors).map(
|
|
1422
|
+
([name, value]) => buildColorItem(name, value, category)
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
function buildTypographyItems(tokens, category) {
|
|
1426
|
+
return Object.entries(tokens.typography).map(
|
|
1427
|
+
([name, typography]) => buildTypographyItem(name, typography, category)
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
function buildSpacingItems(tokens, category) {
|
|
1431
|
+
return Object.entries(tokens.spacing).map(
|
|
1432
|
+
([name, value]) => buildSpacingItem(name, value, category)
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
function buildRoundedItems(tokens, category) {
|
|
1436
|
+
return Object.entries(tokens.rounded).map(
|
|
1437
|
+
([name, value]) => buildRoundedItem(name, value, category)
|
|
1438
|
+
);
|
|
1203
1439
|
}
|
|
1204
1440
|
function buildBreakpointItems(tokens, category) {
|
|
1205
1441
|
return Object.entries(tokens.breakpoints).map(([name, value]) => ({
|
|
1206
1442
|
id: `design-system:breakpoint-${name}`,
|
|
1443
|
+
...PLUGIN_ATTRS,
|
|
1207
1444
|
title: `${name} (${value})`,
|
|
1208
1445
|
category,
|
|
1209
1446
|
subCategory: "Breakpoints",
|
|
1210
|
-
prompt: `Use breakpoint "${name}" at max-width ${value} from DESIGN.
|
|
1447
|
+
prompt: `Use breakpoint "${name}" at max-width ${value} from DESIGN.MD: @media (max-width: ${value}) { ... }`,
|
|
1448
|
+
interaction: {
|
|
1449
|
+
mode: "stage-place",
|
|
1450
|
+
stagePromptTemplate: `Add a responsive breakpoint at "${name}" (${value}) from DESIGN.md for this element.`
|
|
1451
|
+
}
|
|
1211
1452
|
}));
|
|
1212
1453
|
}
|
|
1213
1454
|
function buildAnimationItems(tokens, category) {
|
|
1214
1455
|
return Object.entries(tokens.animations).map(([name, preset]) => {
|
|
1215
1456
|
const parts = [];
|
|
1216
|
-
if (preset.duration)
|
|
1217
|
-
|
|
1218
|
-
if (preset.easing)
|
|
1219
|
-
parts.push(preset.easing);
|
|
1457
|
+
if (preset.duration) parts.push(preset.duration);
|
|
1458
|
+
if (preset.easing) parts.push(preset.easing);
|
|
1220
1459
|
const desc = parts.join(", ");
|
|
1221
1460
|
const dur = preset.duration || "300ms";
|
|
1222
1461
|
const ease = preset.easing || "ease";
|
|
1223
1462
|
return {
|
|
1224
1463
|
id: `design-system:animation-${name}`,
|
|
1464
|
+
...PLUGIN_ATTRS,
|
|
1225
1465
|
title: name,
|
|
1226
1466
|
category,
|
|
1227
1467
|
subCategory: "Animations",
|
|
1228
1468
|
prompt: `Use animation preset "${name}" from DESIGN.md: ${desc}. Apply to transition-duration and transition-timing-function.`,
|
|
1469
|
+
interaction: {
|
|
1470
|
+
mode: "stage-place",
|
|
1471
|
+
stagePromptTemplate: `Apply the animation preset "${name}" (${desc}) from DESIGN.md to this element's transitions.`
|
|
1472
|
+
},
|
|
1229
1473
|
presentation: {
|
|
1230
1474
|
type: "html-fragment",
|
|
1231
|
-
html:
|
|
1232
|
-
|
|
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;">
|
|
1475
|
+
html: scopedFragment(
|
|
1476
|
+
`<div style="display:flex;align-items:center;gap:10px;font-family:sans-serif;">
|
|
1239
1477
|
<div class="anim-preview"></div>
|
|
1240
1478
|
<div>
|
|
1241
1479
|
<div style="font-size:12px;font-weight:600;">${esc(name)}</div>
|
|
1242
1480
|
<div style="font-size:11px;color:#888;font-family:monospace;">${esc(desc)}</div>
|
|
1243
1481
|
<div style="font-size:10px;color:#aaa;">hover to preview</div>
|
|
1244
1482
|
</div>
|
|
1245
|
-
</div
|
|
1246
|
-
|
|
1483
|
+
</div>`,
|
|
1484
|
+
`.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); }`
|
|
1485
|
+
)
|
|
1247
1486
|
}
|
|
1248
1487
|
};
|
|
1249
1488
|
});
|
|
@@ -1264,63 +1503,125 @@ function buildComponentItems(tokens, category) {
|
|
|
1264
1503
|
borderColor: "border-color"
|
|
1265
1504
|
};
|
|
1266
1505
|
for (const [prop, value] of Object.entries(spec)) {
|
|
1267
|
-
if (prop === "typography")
|
|
1268
|
-
continue;
|
|
1506
|
+
if (prop === "typography") continue;
|
|
1269
1507
|
const cssProp = propertyMapping[prop] || prop;
|
|
1270
1508
|
previewStyles.push(`${cssProp}:${value}`);
|
|
1271
1509
|
}
|
|
1272
|
-
if (!spec.padding)
|
|
1273
|
-
|
|
1274
|
-
if (spec.borderColor)
|
|
1275
|
-
previewStyles.push("border:1.5px solid " + spec.borderColor);
|
|
1510
|
+
if (!spec.padding) previewStyles.push("padding:8px 16px");
|
|
1511
|
+
if (spec.borderColor) previewStyles.push("border:1.5px solid " + spec.borderColor);
|
|
1276
1512
|
return {
|
|
1277
1513
|
id: `design-system:component-${name}`,
|
|
1514
|
+
...PLUGIN_ATTRS,
|
|
1278
1515
|
title: name,
|
|
1279
1516
|
category,
|
|
1280
1517
|
subCategory: "Components",
|
|
1281
1518
|
prompt: `Apply the "${name}" component spec from DESIGN.md. Required styles: ${props}. The design-system validator will flag mismatches.`,
|
|
1519
|
+
interaction: {
|
|
1520
|
+
mode: "stage-place",
|
|
1521
|
+
stagePromptTemplate: `Apply the "${name}" component styles (${props}) from DESIGN.md to this element.`
|
|
1522
|
+
},
|
|
1282
1523
|
presentation: {
|
|
1283
1524
|
type: "html-fragment",
|
|
1284
|
-
html: `<div style="font-family:sans-serif;">
|
|
1525
|
+
html: scopedFragment(`<div style="font-family:sans-serif;">
|
|
1285
1526
|
<div style="${previewStyles.join(";")};display:inline-block;font-size:13px;min-width:80px;text-align:center;">${esc(name)}</div>
|
|
1286
1527
|
<div style="font-size:10px;color:#888;margin-top:6px;font-family:monospace;">${esc(props)}</div>
|
|
1287
|
-
</div>`
|
|
1528
|
+
</div>`)
|
|
1288
1529
|
}
|
|
1289
1530
|
};
|
|
1290
1531
|
});
|
|
1291
1532
|
}
|
|
1533
|
+
const ADD_MENU_GENERATED_REL = "agent-kit/aiditor/add-menu/design-system.generated.yaml";
|
|
1534
|
+
function readAddMenuCatalog(projectRoot) {
|
|
1535
|
+
const catalogPath = path.join(projectRoot, ADD_MENU_GENERATED_REL);
|
|
1536
|
+
if (!fs.existsSync(catalogPath)) {
|
|
1537
|
+
return [];
|
|
1538
|
+
}
|
|
1539
|
+
const parsed = load(fs.readFileSync(catalogPath, "utf-8"));
|
|
1540
|
+
if (!Array.isArray(parsed.items)) {
|
|
1541
|
+
return [];
|
|
1542
|
+
}
|
|
1543
|
+
return parsed.items;
|
|
1544
|
+
}
|
|
1545
|
+
function writeAddMenuCatalog(projectRoot, items) {
|
|
1546
|
+
const outputPath = path.join(projectRoot, ADD_MENU_GENERATED_REL);
|
|
1547
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1548
|
+
fs.writeFileSync(outputPath, dump({ items }, { lineWidth: 120, noRefs: true }), "utf-8");
|
|
1549
|
+
return ADD_MENU_GENERATED_REL;
|
|
1550
|
+
}
|
|
1551
|
+
const AIDITOR_SETTINGS_OUTPUT_REL = "agent-kit/aiditor/settings/design-system-validator.yaml";
|
|
1552
|
+
const SETTINGS_TEMPLATE_REL = "agent-kit/aiditor/settings.template.yaml";
|
|
1553
|
+
function resolvePackagedAgentKitPath(relativePath, moduleUrl = import.meta.url) {
|
|
1554
|
+
let directory = path.dirname(fileURLToPath(moduleUrl));
|
|
1555
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
1556
|
+
const candidate = path.join(directory, relativePath);
|
|
1557
|
+
if (fs.existsSync(candidate)) {
|
|
1558
|
+
return candidate;
|
|
1559
|
+
}
|
|
1560
|
+
const parent = path.dirname(directory);
|
|
1561
|
+
if (parent === directory) {
|
|
1562
|
+
break;
|
|
1563
|
+
}
|
|
1564
|
+
directory = parent;
|
|
1565
|
+
}
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
function writeAiditorSettingsContribution(projectRoot, templatePath, force = false) {
|
|
1569
|
+
const outputPath = path.join(projectRoot, AIDITOR_SETTINGS_OUTPUT_REL);
|
|
1570
|
+
if (fs.existsSync(outputPath) && !force) {
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1574
|
+
fs.copyFileSync(templatePath, outputPath);
|
|
1575
|
+
return AIDITOR_SETTINGS_OUTPUT_REL;
|
|
1576
|
+
}
|
|
1577
|
+
function materializeDesignSystemAiditorSettings(projectRoot, force = false) {
|
|
1578
|
+
const templatePath = resolvePackagedAgentKitPath(SETTINGS_TEMPLATE_REL);
|
|
1579
|
+
if (!templatePath) {
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
return writeAiditorSettingsContribution(projectRoot, templatePath, force);
|
|
1583
|
+
}
|
|
1292
1584
|
function findAllDesignMdFiles(projectRoot) {
|
|
1293
1585
|
const pagesRoot = path.join(projectRoot, "src", "pages");
|
|
1294
1586
|
const files = [];
|
|
1295
1587
|
const rootDesignMd = path.join(projectRoot, "DESIGN.md");
|
|
1296
|
-
if (fs.existsSync(rootDesignMd))
|
|
1297
|
-
files.push(rootDesignMd);
|
|
1588
|
+
if (fs.existsSync(rootDesignMd)) files.push(rootDesignMd);
|
|
1298
1589
|
if (fs.existsSync(pagesRoot)) {
|
|
1299
|
-
|
|
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);
|
|
1590
|
+
walkDesignMdDir(pagesRoot, files);
|
|
1309
1591
|
}
|
|
1310
1592
|
return files;
|
|
1311
1593
|
}
|
|
1594
|
+
function walkDesignMdDir(dir, files) {
|
|
1595
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1596
|
+
if (entry.isFile() && entry.name === "DESIGN.md") {
|
|
1597
|
+
files.push(path.join(dir, entry.name));
|
|
1598
|
+
} else if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
1599
|
+
walkDesignMdDir(path.join(dir, entry.name), files);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
function categoryName(designMdPath, projectRoot, tokens) {
|
|
1604
|
+
if (tokens.name) return tokens.name;
|
|
1605
|
+
const rel = path.relative(projectRoot, designMdPath);
|
|
1606
|
+
if (rel === "DESIGN.md") return "Design System";
|
|
1607
|
+
const dir = path.dirname(rel).replace(/^src\/pages\/?/, "");
|
|
1608
|
+
return dir ? `Design System (${dir})` : "Design System";
|
|
1609
|
+
}
|
|
1312
1610
|
async function generateDesignSystemAgentKit(ctx) {
|
|
1313
1611
|
const designMdFiles = findAllDesignMdFiles(ctx.projectRoot);
|
|
1612
|
+
const settingsGenerated = materializeDesignSystemAiditorSettings(ctx.projectRoot, ctx.force);
|
|
1314
1613
|
if (designMdFiles.length === 0) {
|
|
1315
|
-
return {
|
|
1614
|
+
return {
|
|
1615
|
+
agentKitCreated: settingsGenerated ? [settingsGenerated] : [],
|
|
1616
|
+
message: settingsGenerated ? "AIditor settings materialized; no DESIGN.md found for add-menu" : "No DESIGN.md found in project"
|
|
1617
|
+
};
|
|
1316
1618
|
}
|
|
1317
1619
|
const allItems = [];
|
|
1318
1620
|
const seen = /* @__PURE__ */ new Set();
|
|
1319
1621
|
for (const filePath of designMdFiles) {
|
|
1320
1622
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
1321
1623
|
const tokens = parseDesignMd(content);
|
|
1322
|
-
if (!tokens)
|
|
1323
|
-
continue;
|
|
1624
|
+
if (!tokens) continue;
|
|
1324
1625
|
const category = categoryName(filePath, ctx.projectRoot, tokens);
|
|
1325
1626
|
const items = [
|
|
1326
1627
|
...buildColorItems(tokens, category),
|
|
@@ -1339,20 +1640,245 @@ async function generateDesignSystemAgentKit(ctx) {
|
|
|
1339
1640
|
}
|
|
1340
1641
|
}
|
|
1341
1642
|
if (allItems.length === 0) {
|
|
1342
|
-
return {
|
|
1643
|
+
return {
|
|
1644
|
+
agentKitCreated: settingsGenerated ? [settingsGenerated] : [],
|
|
1645
|
+
message: "DESIGN.md found but no tokens defined"
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
writeAddMenuCatalog(ctx.projectRoot, allItems);
|
|
1649
|
+
const agentKitCreated = [ADD_MENU_GENERATED_REL];
|
|
1650
|
+
if (settingsGenerated) {
|
|
1651
|
+
agentKitCreated.push(settingsGenerated);
|
|
1343
1652
|
}
|
|
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
1653
|
return {
|
|
1348
|
-
agentKitCreated
|
|
1654
|
+
agentKitCreated,
|
|
1349
1655
|
message: `Generated ${allItems.length} add-menu items from ${designMdFiles.length} DESIGN.md file(s)`
|
|
1350
1656
|
};
|
|
1351
1657
|
}
|
|
1658
|
+
const DESIGN_VALIDATORS = [
|
|
1659
|
+
{ name: "design-tokens", run: validateTokens },
|
|
1660
|
+
{ name: "design-components", run: validateComponents },
|
|
1661
|
+
{ name: "design-structure", run: validateStructure },
|
|
1662
|
+
{ name: "design-contrast", run: validateContrast }
|
|
1663
|
+
];
|
|
1664
|
+
async function findJayHtmlFiles(scanDir) {
|
|
1665
|
+
const files = [];
|
|
1666
|
+
async function walk(dir) {
|
|
1667
|
+
let entries;
|
|
1668
|
+
try {
|
|
1669
|
+
entries = await promises.readdir(dir, { withFileTypes: true });
|
|
1670
|
+
} catch {
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
for (const entry of entries) {
|
|
1674
|
+
const fullPath = path__default.join(dir, entry.name);
|
|
1675
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1676
|
+
await walk(fullPath);
|
|
1677
|
+
} else if (entry.isFile() && entry.name.endsWith(JAY_EXTENSION)) {
|
|
1678
|
+
files.push(fullPath);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
await walk(scanDir);
|
|
1683
|
+
return files;
|
|
1684
|
+
}
|
|
1685
|
+
function toAnalysisFinding(relativePath, validator, finding) {
|
|
1686
|
+
if (!finding.message) return null;
|
|
1687
|
+
return {
|
|
1688
|
+
file: relativePath,
|
|
1689
|
+
validator,
|
|
1690
|
+
severity: finding.severity === "error" ? "error" : "warning",
|
|
1691
|
+
message: finding.message,
|
|
1692
|
+
suggestion: finding.suggestion
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
function buildValidationContext(projectRoot, relativePath, parsed) {
|
|
1696
|
+
return {
|
|
1697
|
+
filePath: relativePath,
|
|
1698
|
+
body: parsed.body,
|
|
1699
|
+
css: parsed.css,
|
|
1700
|
+
head: parsed.headMeta,
|
|
1701
|
+
headlessImports: parsed.headlessImports.map((imp) => ({
|
|
1702
|
+
key: imp.key,
|
|
1703
|
+
contractName: imp.contractName,
|
|
1704
|
+
contract: imp.contract ? {
|
|
1705
|
+
name: imp.contract.name,
|
|
1706
|
+
tags: imp.contract.tags,
|
|
1707
|
+
props: imp.contract.props,
|
|
1708
|
+
params: imp.contract.params
|
|
1709
|
+
} : void 0
|
|
1710
|
+
})),
|
|
1711
|
+
projectRoot
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
async function runDesignSystemAnalysis(projectRoot) {
|
|
1715
|
+
const pagesDir = path__default.join(projectRoot, "src", "pages");
|
|
1716
|
+
const componentsDir = path__default.join(projectRoot, "src", "components");
|
|
1717
|
+
const jayHtmlFiles = [
|
|
1718
|
+
...await findJayHtmlFiles(pagesDir).catch(() => []),
|
|
1719
|
+
...await findJayHtmlFiles(componentsDir).catch(() => [])
|
|
1720
|
+
];
|
|
1721
|
+
const findings = [];
|
|
1722
|
+
for (const jayHtmlPath of jayHtmlFiles) {
|
|
1723
|
+
const relativePath = path__default.relative(projectRoot, jayHtmlPath);
|
|
1724
|
+
const filename = path__default.basename(jayHtmlPath.replace(JAY_EXTENSION, ""));
|
|
1725
|
+
const dirname = path__default.dirname(jayHtmlPath);
|
|
1726
|
+
let parsed;
|
|
1727
|
+
try {
|
|
1728
|
+
const content = await promises.readFile(jayHtmlPath, "utf-8");
|
|
1729
|
+
const parseResult = await parseJayFile(
|
|
1730
|
+
content,
|
|
1731
|
+
filename,
|
|
1732
|
+
dirname,
|
|
1733
|
+
{},
|
|
1734
|
+
JAY_IMPORT_RESOLVER,
|
|
1735
|
+
projectRoot
|
|
1736
|
+
);
|
|
1737
|
+
if (!parseResult.value) continue;
|
|
1738
|
+
parsed = parseResult.value;
|
|
1739
|
+
} catch {
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
const ctx = buildValidationContext(projectRoot, relativePath, parsed);
|
|
1743
|
+
for (const validator of DESIGN_VALIDATORS) {
|
|
1744
|
+
const validatorFindings = await Promise.resolve(validator.run(ctx));
|
|
1745
|
+
for (const finding of validatorFindings) {
|
|
1746
|
+
const mapped = toAnalysisFinding(relativePath, validator.name, finding);
|
|
1747
|
+
if (mapped) findings.push(mapped);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
const errorCount = findings.filter((finding) => finding.severity === "error").length;
|
|
1752
|
+
const warningCount = findings.filter((finding) => finding.severity === "warning").length;
|
|
1753
|
+
return {
|
|
1754
|
+
filesScanned: jayHtmlFiles.length,
|
|
1755
|
+
findings,
|
|
1756
|
+
errorCount,
|
|
1757
|
+
warningCount
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
function countAddMenuItems(projectRoot) {
|
|
1761
|
+
return readAddMenuCatalog(projectRoot).length;
|
|
1762
|
+
}
|
|
1763
|
+
function summarizeDesignMd(projectRoot, filePath) {
|
|
1764
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1765
|
+
const tokens = parseDesignMd(content);
|
|
1766
|
+
if (!tokens) return null;
|
|
1767
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
1768
|
+
const name = tokens.name ?? (relativePath === "DESIGN.md" ? "Design System" : `Design System (${path.dirname(relativePath)})`);
|
|
1769
|
+
return {
|
|
1770
|
+
relativePath,
|
|
1771
|
+
name,
|
|
1772
|
+
colorCount: Object.keys(tokens.colors).length,
|
|
1773
|
+
typographyCount: Object.keys(tokens.typography).length,
|
|
1774
|
+
spacingCount: Object.keys(tokens.spacing).length,
|
|
1775
|
+
componentCount: Object.keys(tokens.components).length
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
const getDesignSystemSettingsStatus = makeJayQuery(
|
|
1779
|
+
"designSystem.getDesignSystemSettingsStatus"
|
|
1780
|
+
).withHandler(async () => {
|
|
1781
|
+
const projectRoot = process.cwd();
|
|
1782
|
+
const designMdPaths = findAllDesignMdFiles(projectRoot);
|
|
1783
|
+
const designFiles = designMdPaths.map((filePath) => summarizeDesignMd(projectRoot, filePath)).filter((summary) => summary !== null);
|
|
1784
|
+
return {
|
|
1785
|
+
hasDesignMd: designFiles.length > 0,
|
|
1786
|
+
designFiles,
|
|
1787
|
+
addMenuItemCount: countAddMenuItems(projectRoot),
|
|
1788
|
+
addMenuCatalogRel: ADD_MENU_GENERATED_REL,
|
|
1789
|
+
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/."
|
|
1790
|
+
};
|
|
1791
|
+
});
|
|
1792
|
+
const runDesignSystemAnalysisAction = makeJayQuery(
|
|
1793
|
+
"designSystem.runDesignSystemAnalysis"
|
|
1794
|
+
).withHandler(async () => {
|
|
1795
|
+
const projectRoot = process.cwd();
|
|
1796
|
+
return runDesignSystemAnalysis(projectRoot);
|
|
1797
|
+
});
|
|
1798
|
+
const loadDesignSystemAddMenuCatalog = makeJayQuery(
|
|
1799
|
+
"designSystem.loadAddMenuCatalog"
|
|
1800
|
+
).withHandler(async () => {
|
|
1801
|
+
const projectRoot = process.cwd();
|
|
1802
|
+
const items = readAddMenuCatalog(projectRoot);
|
|
1803
|
+
return {
|
|
1804
|
+
items,
|
|
1805
|
+
outputRel: ADD_MENU_GENERATED_REL,
|
|
1806
|
+
itemCount: items.length
|
|
1807
|
+
};
|
|
1808
|
+
});
|
|
1809
|
+
const saveDesignSystemAddMenuCatalog = makeJayAction(
|
|
1810
|
+
"designSystem.saveAddMenuCatalog"
|
|
1811
|
+
).withHandler(async (input) => {
|
|
1812
|
+
const projectRoot = process.cwd();
|
|
1813
|
+
const outputRel = writeAddMenuCatalog(projectRoot, input.items);
|
|
1814
|
+
return {
|
|
1815
|
+
itemCount: input.items.length,
|
|
1816
|
+
outputRel,
|
|
1817
|
+
message: `Saved ${input.items.length} add-menu items to ${outputRel}.`
|
|
1818
|
+
};
|
|
1819
|
+
});
|
|
1820
|
+
const regenerateDesignSystemAddMenu = makeJayQuery(
|
|
1821
|
+
"designSystem.regenerateDesignSystemAddMenu"
|
|
1822
|
+
).withHandler(async () => {
|
|
1823
|
+
const projectRoot = process.cwd();
|
|
1824
|
+
const ctx = {
|
|
1825
|
+
projectRoot,
|
|
1826
|
+
referencesDir: path.join(projectRoot, "agent-kit", "references", "design-system-validator"),
|
|
1827
|
+
force: true
|
|
1828
|
+
};
|
|
1829
|
+
const result = await generateDesignSystemAgentKit(ctx);
|
|
1830
|
+
const itemCount = countAddMenuItems(projectRoot);
|
|
1831
|
+
return {
|
|
1832
|
+
itemCount,
|
|
1833
|
+
outputRel: ADD_MENU_GENERATED_REL,
|
|
1834
|
+
agentKitCreated: result.agentKitCreated,
|
|
1835
|
+
message: result.message
|
|
1836
|
+
};
|
|
1837
|
+
});
|
|
1838
|
+
async function renderFast() {
|
|
1839
|
+
return phaseOutput({
|
|
1840
|
+
statusMessage: "",
|
|
1841
|
+
statusTone: "info",
|
|
1842
|
+
showStatusMessage: false,
|
|
1843
|
+
catalogLoading: true,
|
|
1844
|
+
showNoCatalog: false,
|
|
1845
|
+
addMenuListRows: [],
|
|
1846
|
+
showAddMenuEditor: false,
|
|
1847
|
+
editorTitle: "",
|
|
1848
|
+
editorSubCategory: "",
|
|
1849
|
+
editorTokenValue: "",
|
|
1850
|
+
editorFontSize: "",
|
|
1851
|
+
editorFontWeight: "",
|
|
1852
|
+
editorFontFamily: "",
|
|
1853
|
+
showTypographyFields: false,
|
|
1854
|
+
showTokenValueField: false,
|
|
1855
|
+
editorReadOnly: false,
|
|
1856
|
+
editorReadOnlyReason: "",
|
|
1857
|
+
unsavedChangeCount: 0,
|
|
1858
|
+
saveButtonLabel: "Save and sync",
|
|
1859
|
+
saveDisabled: true,
|
|
1860
|
+
discardDisabled: true,
|
|
1861
|
+
showAnalysisSection: false,
|
|
1862
|
+
showReport: false,
|
|
1863
|
+
reportSummary: "",
|
|
1864
|
+
reportBody: ""
|
|
1865
|
+
}, {});
|
|
1866
|
+
}
|
|
1867
|
+
const designSystemSettingsPage = makeJayStackComponent().withProps().withFastRender(renderFast).withInteractiveMark();
|
|
1352
1868
|
export {
|
|
1869
|
+
ADD_MENU_GENERATED_REL,
|
|
1870
|
+
designSystemSettingsPage,
|
|
1871
|
+
fontFallback,
|
|
1353
1872
|
generateDesignSystemAgentKit,
|
|
1873
|
+
getDesignSystemSettingsStatus,
|
|
1874
|
+
loadDesignSystemAddMenuCatalog,
|
|
1875
|
+
regenerateDesignSystemAddMenu,
|
|
1876
|
+
runDesignSystemAnalysisAction as runDesignSystemAnalysis,
|
|
1877
|
+
saveDesignSystemAddMenuCatalog,
|
|
1354
1878
|
validateComponents,
|
|
1355
1879
|
validateContrast,
|
|
1880
|
+
validateFontFallbacks,
|
|
1356
1881
|
validateStructure,
|
|
1357
|
-
validateTokens
|
|
1882
|
+
validateTokens,
|
|
1883
|
+
validateUndefinedVars
|
|
1358
1884
|
};
|