@jay-framework/design-system-validator 0.22.1 → 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/agent-kit/aiditor/settings.template.yaml +2 -0
- 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 +89 -1
- package/dist/index.js +760 -291
- 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 +20 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { makeJayStackComponent } from "@jay-framework/fullstack-component";
|
|
2
|
+
import { createSignal, createMemo, createEffect } from "@jay-framework/component";
|
|
3
|
+
import { createActionCaller } from "@jay-framework/stack-client-runtime";
|
|
4
|
+
const PLUGIN_ATTRS = {
|
|
5
|
+
pluginName: "design-system-validator",
|
|
6
|
+
packageName: "@jay-framework/design-system-validator"
|
|
7
|
+
};
|
|
8
|
+
function esc(s) {
|
|
9
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
10
|
+
}
|
|
11
|
+
function scopedFragment(innerHtml, scopedCss = "") {
|
|
12
|
+
const styleBlock = `<style>@scope { ${scopedCss} }</style>`;
|
|
13
|
+
return `<div>${styleBlock}${innerHtml}</div>`;
|
|
14
|
+
}
|
|
15
|
+
function isLargeFontSize(fontSize) {
|
|
16
|
+
if (!fontSize) return false;
|
|
17
|
+
if (/\d+(\.\d+)?\s*(vw|vh|vmin|vmax)\b/.test(fontSize)) return true;
|
|
18
|
+
const clampMatch = fontSize.match(/clamp\([^,]+,[^,]+,\s*([\d.]+)\s*(px|rem|em)/);
|
|
19
|
+
if (clampMatch) {
|
|
20
|
+
const max = parseFloat(clampMatch[1]);
|
|
21
|
+
const unit = clampMatch[2];
|
|
22
|
+
if (unit === "px") return max > 20;
|
|
23
|
+
return max > 1.25;
|
|
24
|
+
}
|
|
25
|
+
const simple = parseFloat(fontSize);
|
|
26
|
+
if (isNaN(simple)) return false;
|
|
27
|
+
if (fontSize.includes("rem") || fontSize.includes("em")) return simple > 1.25;
|
|
28
|
+
return simple > 20;
|
|
29
|
+
}
|
|
30
|
+
function spacingBrowseSize(value) {
|
|
31
|
+
if (/calc|clamp|var\(/.test(value)) return "large";
|
|
32
|
+
const px = parseFloat(value);
|
|
33
|
+
if (!isNaN(px) && !value.includes("rem") && !value.includes("em")) {
|
|
34
|
+
if (px <= 30) return "small";
|
|
35
|
+
if (px > 100) return "large";
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
const rem = parseFloat(value);
|
|
39
|
+
if (!isNaN(rem)) {
|
|
40
|
+
if (rem <= 2) return "small";
|
|
41
|
+
if (rem > 6) return "large";
|
|
42
|
+
}
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
function buildColorItem(name, value, category) {
|
|
46
|
+
return {
|
|
47
|
+
id: `design-system:color-${name}`,
|
|
48
|
+
...PLUGIN_ATTRS,
|
|
49
|
+
title: name,
|
|
50
|
+
category,
|
|
51
|
+
subCategory: "Colors",
|
|
52
|
+
browse: { size: "small" },
|
|
53
|
+
prompt: `Use color token "${name}" with value ${value} from DESIGN.md.`,
|
|
54
|
+
interaction: {
|
|
55
|
+
mode: "stage-place",
|
|
56
|
+
stagePromptTemplate: `Apply the color token "${name}" (${value}) from DESIGN.md at this location.`
|
|
57
|
+
},
|
|
58
|
+
presentation: {
|
|
59
|
+
type: "html-fragment",
|
|
60
|
+
html: scopedFragment(`<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:sans-serif;height:100%;">
|
|
61
|
+
<div style="width:40px;height:40px;border-radius:6px;background:${esc(value)};border:1px solid rgba(0,0,0,0.1);"></div>
|
|
62
|
+
<div style="font-size:10px;color:#888;font-family:monospace;">${esc(value)}</div>
|
|
63
|
+
</div>`)
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function buildSpacingItem(name, value, category) {
|
|
68
|
+
return {
|
|
69
|
+
id: `design-system:spacing-${name}`,
|
|
70
|
+
...PLUGIN_ATTRS,
|
|
71
|
+
title: name,
|
|
72
|
+
category,
|
|
73
|
+
subCategory: "Spacing",
|
|
74
|
+
prompt: `Use spacing token "${name}" with value ${value} from DESIGN.md for padding, margin, or gap.`,
|
|
75
|
+
...spacingBrowseSize(value) ? { browse: { size: spacingBrowseSize(value) } } : {},
|
|
76
|
+
interaction: {
|
|
77
|
+
mode: "stage-place",
|
|
78
|
+
stagePromptTemplate: `Apply the spacing token "${name}" (${value}) from DESIGN.md at this location.`
|
|
79
|
+
},
|
|
80
|
+
presentation: {
|
|
81
|
+
type: "html-fragment",
|
|
82
|
+
html: scopedFragment(`<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:sans-serif;height:100%;">
|
|
83
|
+
<div style="display:flex;align-items:center;gap:0;">
|
|
84
|
+
<div style="width:16px;height:24px;background:#cbd5e1;border-radius:2px 0 0 2px;"></div>
|
|
85
|
+
<div style="width:${esc(value)};height:24px;background:repeating-linear-gradient(45deg,#e0f2fe,#e0f2fe 2px,#bae6fd 2px,#bae6fd 4px);"></div>
|
|
86
|
+
<div style="width:16px;height:24px;background:#cbd5e1;border-radius:0 2px 2px 0;"></div>
|
|
87
|
+
</div>
|
|
88
|
+
<div style="font-size:10px;color:#888;font-family:monospace;">${esc(value)}</div>
|
|
89
|
+
</div>`)
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function buildRoundedItem(name, value, category) {
|
|
94
|
+
return {
|
|
95
|
+
id: `design-system:rounded-${name}`,
|
|
96
|
+
...PLUGIN_ATTRS,
|
|
97
|
+
title: name,
|
|
98
|
+
category,
|
|
99
|
+
subCategory: "Rounded",
|
|
100
|
+
browse: { size: "small" },
|
|
101
|
+
prompt: `Use border-radius token "${name}" with value ${value} from DESIGN.md.`,
|
|
102
|
+
interaction: {
|
|
103
|
+
mode: "stage-place",
|
|
104
|
+
stagePromptTemplate: `Apply the border-radius token "${name}" (${value}) from DESIGN.md at this location.`
|
|
105
|
+
},
|
|
106
|
+
presentation: {
|
|
107
|
+
type: "html-fragment",
|
|
108
|
+
html: scopedFragment(`<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:sans-serif;height:100%;">
|
|
109
|
+
<div style="width:40px;height:40px;border-radius:${esc(value)};background:#e2e8f0;border:1.5px solid #94a3b8;"></div>
|
|
110
|
+
<div style="font-size:10px;color:#888;font-family:monospace;">${esc(value)}</div>
|
|
111
|
+
</div>`)
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function formatTypographyCssValue(value) {
|
|
116
|
+
return String(value);
|
|
117
|
+
}
|
|
118
|
+
function buildTypographyItem(name, typography, category) {
|
|
119
|
+
const parts = [];
|
|
120
|
+
if (typography.fontFamily) parts.push(typography.fontFamily);
|
|
121
|
+
if (typography.fontSize) parts.push(typography.fontSize);
|
|
122
|
+
if (typography.fontWeight != null) {
|
|
123
|
+
parts.push(`weight ${formatTypographyCssValue(typography.fontWeight)}`);
|
|
124
|
+
}
|
|
125
|
+
if (typography.lineHeight != null) {
|
|
126
|
+
parts.push(`line-height ${formatTypographyCssValue(typography.lineHeight)}`);
|
|
127
|
+
}
|
|
128
|
+
if (typography.letterSpacing) parts.push(`letter-spacing ${typography.letterSpacing}`);
|
|
129
|
+
const desc = parts.join(", ");
|
|
130
|
+
const styles = [];
|
|
131
|
+
if (typography.fontFamily) styles.push(`font-family:${typography.fontFamily},sans-serif`);
|
|
132
|
+
if (typography.fontSize) styles.push(`font-size:${typography.fontSize}`);
|
|
133
|
+
if (typography.fontWeight != null) {
|
|
134
|
+
styles.push(`font-weight:${formatTypographyCssValue(typography.fontWeight)}`);
|
|
135
|
+
}
|
|
136
|
+
if (typography.lineHeight != null) {
|
|
137
|
+
styles.push(`line-height:${formatTypographyCssValue(typography.lineHeight)}`);
|
|
138
|
+
}
|
|
139
|
+
if (typography.letterSpacing) styles.push(`letter-spacing:${typography.letterSpacing}`);
|
|
140
|
+
const isLargeFont = isLargeFontSize(typography.fontSize);
|
|
141
|
+
return {
|
|
142
|
+
id: `design-system:typography-${name}`,
|
|
143
|
+
...PLUGIN_ATTRS,
|
|
144
|
+
title: name,
|
|
145
|
+
category,
|
|
146
|
+
subCategory: "Typography",
|
|
147
|
+
prompt: `Apply typography preset "${name}" from DESIGN.md: ${desc}.`,
|
|
148
|
+
interaction: {
|
|
149
|
+
mode: "stage-place",
|
|
150
|
+
stagePromptTemplate: `Apply the typography preset "${name}" (${desc}) from DESIGN.md at this location.`
|
|
151
|
+
},
|
|
152
|
+
...isLargeFont ? { browse: { size: "large" } } : {},
|
|
153
|
+
presentation: {
|
|
154
|
+
type: "html-fragment",
|
|
155
|
+
html: scopedFragment(`<div style="font-family:sans-serif;">
|
|
156
|
+
<div style="${styles.join(";")};margin:0;">The quick brown fox</div>
|
|
157
|
+
<div style="font-size:10px;color:#888;margin-top:6px;font-family:monospace;">${esc(name)}: ${esc(desc)}</div>
|
|
158
|
+
</div>`)
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function parseAddMenuItemId(id) {
|
|
163
|
+
const match = id.match(
|
|
164
|
+
/^design-system:(color|spacing|rounded|typography|breakpoint|animation|component)-(.+)$/
|
|
165
|
+
);
|
|
166
|
+
if (!match) {
|
|
167
|
+
return { kind: "unknown", tokenName: id };
|
|
168
|
+
}
|
|
169
|
+
return { kind: match[1], tokenName: match[2] };
|
|
170
|
+
}
|
|
171
|
+
function extractQuotedValue(prompt, label) {
|
|
172
|
+
const pattern = new RegExp(
|
|
173
|
+
`${label}\\s+"([^"]+)"\\s+with value\\s+(.+?)\\s+from DESIGN\\.md`,
|
|
174
|
+
"i"
|
|
175
|
+
);
|
|
176
|
+
const match = prompt.match(pattern);
|
|
177
|
+
if (!match) return void 0;
|
|
178
|
+
return match[2].trim().replace(/\.$/, "");
|
|
179
|
+
}
|
|
180
|
+
function extractTypographyFromPrompt(prompt) {
|
|
181
|
+
const nameMatch = prompt.match(/typography preset "([^"]+)"/i);
|
|
182
|
+
const afterColon = prompt.split(":").slice(1).join(":").replace(/\.\s*$/, "").trim();
|
|
183
|
+
const edit = {};
|
|
184
|
+
const fontFamilyMatch = afterColon.match(/([^,]+),\s*[\d.]+(?:px|rem|em)/);
|
|
185
|
+
if (fontFamilyMatch) {
|
|
186
|
+
edit.fontFamily = fontFamilyMatch[1].trim();
|
|
187
|
+
}
|
|
188
|
+
const fontSizeMatch = afterColon.match(/([\d.]+(?:px|rem|em|vw|vh|%|clamp\([^)]+\)))/);
|
|
189
|
+
if (fontSizeMatch) {
|
|
190
|
+
edit.fontSize = fontSizeMatch[1];
|
|
191
|
+
}
|
|
192
|
+
const weightMatch = afterColon.match(/weight\s+(\d+)/i);
|
|
193
|
+
if (weightMatch) {
|
|
194
|
+
edit.fontWeight = weightMatch[1];
|
|
195
|
+
}
|
|
196
|
+
const lineHeightMatch = afterColon.match(/line-height\s+([^,]+)/i);
|
|
197
|
+
if (lineHeightMatch) {
|
|
198
|
+
edit.lineHeight = lineHeightMatch[1].trim();
|
|
199
|
+
}
|
|
200
|
+
const letterSpacingMatch = afterColon.match(/letter-spacing\s+([^,]+)/i);
|
|
201
|
+
if (letterSpacingMatch) {
|
|
202
|
+
edit.letterSpacing = letterSpacingMatch[1].trim();
|
|
203
|
+
}
|
|
204
|
+
if (!nameMatch && Object.keys(edit).length === 0) {
|
|
205
|
+
return { fontSize: "16px", fontWeight: "400" };
|
|
206
|
+
}
|
|
207
|
+
return edit;
|
|
208
|
+
}
|
|
209
|
+
function extractAddMenuItemEditorFields(item) {
|
|
210
|
+
const { kind, tokenName } = parseAddMenuItemId(item.id);
|
|
211
|
+
const base = {
|
|
212
|
+
kind,
|
|
213
|
+
tokenName,
|
|
214
|
+
title: item.title,
|
|
215
|
+
tokenValue: "",
|
|
216
|
+
fontSize: "",
|
|
217
|
+
fontWeight: "",
|
|
218
|
+
fontFamily: "",
|
|
219
|
+
editable: false,
|
|
220
|
+
readOnlyReason: ""
|
|
221
|
+
};
|
|
222
|
+
switch (kind) {
|
|
223
|
+
case "color":
|
|
224
|
+
return {
|
|
225
|
+
...base,
|
|
226
|
+
editable: true,
|
|
227
|
+
tokenValue: extractQuotedValue(item.prompt, "color token") ?? extractColorFromPresentation(item) ?? ""
|
|
228
|
+
};
|
|
229
|
+
case "spacing":
|
|
230
|
+
return {
|
|
231
|
+
...base,
|
|
232
|
+
editable: true,
|
|
233
|
+
tokenValue: extractQuotedValue(item.prompt, "spacing token") ?? ""
|
|
234
|
+
};
|
|
235
|
+
case "rounded":
|
|
236
|
+
return {
|
|
237
|
+
...base,
|
|
238
|
+
editable: true,
|
|
239
|
+
tokenValue: extractQuotedValue(item.prompt, "border-radius token") ?? ""
|
|
240
|
+
};
|
|
241
|
+
case "typography": {
|
|
242
|
+
const typography = extractTypographyFromPrompt(item.prompt);
|
|
243
|
+
return {
|
|
244
|
+
...base,
|
|
245
|
+
editable: true,
|
|
246
|
+
fontFamily: typography.fontFamily ?? "",
|
|
247
|
+
fontSize: typography.fontSize ?? "",
|
|
248
|
+
fontWeight: typography.fontWeight != null ? String(typography.fontWeight) : ""
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
case "breakpoint":
|
|
252
|
+
case "animation":
|
|
253
|
+
case "component":
|
|
254
|
+
return {
|
|
255
|
+
...base,
|
|
256
|
+
editable: false,
|
|
257
|
+
readOnlyReason: `${kind} items are read-only here — edit DESIGN.md and regenerate the catalog.`
|
|
258
|
+
};
|
|
259
|
+
default:
|
|
260
|
+
return {
|
|
261
|
+
...base,
|
|
262
|
+
editable: false,
|
|
263
|
+
readOnlyReason: "This item type cannot be edited in the settings panel."
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function extractColorFromPresentation(item) {
|
|
268
|
+
if (item.presentation?.type !== "html-fragment") return void 0;
|
|
269
|
+
const match = item.presentation.html.match(/background:([^;"]+)/);
|
|
270
|
+
return match?.[1]?.trim();
|
|
271
|
+
}
|
|
272
|
+
function patchAddMenuItem(item, patch) {
|
|
273
|
+
const { kind, tokenName } = parseAddMenuItemId(item.id);
|
|
274
|
+
const category = item.category;
|
|
275
|
+
const title = patch.title ?? item.title;
|
|
276
|
+
switch (kind) {
|
|
277
|
+
case "color": {
|
|
278
|
+
const value = patch.tokenValue ?? extractQuotedValue(item.prompt, "color token") ?? "";
|
|
279
|
+
const rebuilt = buildColorItem(tokenName, value, category);
|
|
280
|
+
return { ...rebuilt, title };
|
|
281
|
+
}
|
|
282
|
+
case "spacing": {
|
|
283
|
+
const value = patch.tokenValue ?? extractQuotedValue(item.prompt, "spacing token") ?? "";
|
|
284
|
+
const rebuilt = buildSpacingItem(tokenName, value, category);
|
|
285
|
+
return { ...rebuilt, title };
|
|
286
|
+
}
|
|
287
|
+
case "rounded": {
|
|
288
|
+
const value = patch.tokenValue ?? extractQuotedValue(item.prompt, "border-radius token") ?? "";
|
|
289
|
+
const rebuilt = buildRoundedItem(tokenName, value, category);
|
|
290
|
+
return { ...rebuilt, title };
|
|
291
|
+
}
|
|
292
|
+
case "typography": {
|
|
293
|
+
const current = extractTypographyFromPrompt(item.prompt);
|
|
294
|
+
const rebuilt = buildTypographyItem(
|
|
295
|
+
tokenName,
|
|
296
|
+
{
|
|
297
|
+
fontFamily: patch.fontFamily ?? current.fontFamily,
|
|
298
|
+
fontSize: patch.fontSize ?? current.fontSize,
|
|
299
|
+
fontWeight: patch.fontWeight ?? current.fontWeight,
|
|
300
|
+
lineHeight: current.lineHeight,
|
|
301
|
+
letterSpacing: current.letterSpacing
|
|
302
|
+
},
|
|
303
|
+
category
|
|
304
|
+
);
|
|
305
|
+
return { ...rebuilt, title };
|
|
306
|
+
}
|
|
307
|
+
default:
|
|
308
|
+
return { ...item, title };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function getAddMenuItemPreviewHtml(item) {
|
|
312
|
+
if (item.presentation?.type === "html-fragment") {
|
|
313
|
+
return item.presentation.html;
|
|
314
|
+
}
|
|
315
|
+
return '<div style="padding:12px;color:#64748b;font-family:sans-serif;font-size:13px;">No preview for this item.</div>';
|
|
316
|
+
}
|
|
317
|
+
function serializeAddMenuCatalogForCompare(items) {
|
|
318
|
+
return JSON.stringify(items);
|
|
319
|
+
}
|
|
320
|
+
createActionCaller("designSystem.getDesignSystemSettingsStatus", "GET");
|
|
321
|
+
const runDesignSystemAnalysisAction = createActionCaller("designSystem.runDesignSystemAnalysis", "GET");
|
|
322
|
+
const loadDesignSystemAddMenuCatalog = createActionCaller("designSystem.loadAddMenuCatalog", "GET");
|
|
323
|
+
const saveDesignSystemAddMenuCatalog = createActionCaller("designSystem.saveAddMenuCatalog", "POST");
|
|
324
|
+
const regenerateDesignSystemAddMenu = createActionCaller("designSystem.regenerateDesignSystemAddMenu", "GET");
|
|
325
|
+
function buildDesignAnalysisAgentPrompt(result) {
|
|
326
|
+
const lines = [
|
|
327
|
+
"Fix design-system validation findings in this Jay project.",
|
|
328
|
+
"",
|
|
329
|
+
`Scanned ${result.filesScanned} .jay-html file(s).`,
|
|
330
|
+
`Errors: ${result.errorCount}, warnings: ${result.warningCount}.`,
|
|
331
|
+
"",
|
|
332
|
+
"Use DESIGN.md tokens and component specs. Prefer design tokens over hardcoded CSS values.",
|
|
333
|
+
""
|
|
334
|
+
];
|
|
335
|
+
if (result.findings.length === 0) {
|
|
336
|
+
lines.push("No design-system findings — project conforms to DESIGN.md.");
|
|
337
|
+
return lines.join("\n");
|
|
338
|
+
}
|
|
339
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
340
|
+
for (const finding of result.findings) {
|
|
341
|
+
const group = byFile.get(finding.file) ?? [];
|
|
342
|
+
group.push(finding);
|
|
343
|
+
byFile.set(finding.file, group);
|
|
344
|
+
}
|
|
345
|
+
for (const [file, fileFindings] of byFile) {
|
|
346
|
+
lines.push(`## ${file}`);
|
|
347
|
+
for (const finding of fileFindings) {
|
|
348
|
+
const prefix = finding.severity === "error" ? "ERROR" : "WARN";
|
|
349
|
+
lines.push(`- [${prefix}] (${finding.validator}) ${finding.message}`);
|
|
350
|
+
if (finding.suggestion) {
|
|
351
|
+
lines.push(` Suggestion: ${finding.suggestion}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
lines.push("");
|
|
355
|
+
}
|
|
356
|
+
return lines.join("\n").trimEnd();
|
|
357
|
+
}
|
|
358
|
+
function notifyAddMenuChanged() {
|
|
359
|
+
window.parent.postMessage({ type: "aiditor:addMenuCatalogChanged" }, window.location.origin);
|
|
360
|
+
}
|
|
361
|
+
function formatReportBody(result) {
|
|
362
|
+
if (result.findings.length === 0) {
|
|
363
|
+
return "No design-system findings.";
|
|
364
|
+
}
|
|
365
|
+
const lines = [];
|
|
366
|
+
let currentFile = "";
|
|
367
|
+
for (const finding of result.findings) {
|
|
368
|
+
if (finding.file !== currentFile) {
|
|
369
|
+
currentFile = finding.file;
|
|
370
|
+
lines.push(`
|
|
371
|
+
${currentFile}`);
|
|
372
|
+
}
|
|
373
|
+
const prefix = finding.severity === "error" ? "ERROR" : "WARN";
|
|
374
|
+
lines.push(` [${prefix}] (${finding.validator}) ${finding.message}`);
|
|
375
|
+
if (finding.suggestion) {
|
|
376
|
+
lines.push(` → ${finding.suggestion}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return lines.join("\n").trim();
|
|
380
|
+
}
|
|
381
|
+
function listRowClass(itemId, selectedId) {
|
|
382
|
+
return itemId === selectedId ? "catalog-item catalog-item--selected" : "catalog-item";
|
|
383
|
+
}
|
|
384
|
+
function settingsPageConstructor(_props, refs, _fastViewState) {
|
|
385
|
+
const [statusMessage, setStatusMessage] = createSignal("");
|
|
386
|
+
const [statusTone, setStatusTone] = createSignal("info");
|
|
387
|
+
const [showStatusMessage, setShowStatusMessage] = createSignal(false);
|
|
388
|
+
const [catalogLoading, setCatalogLoading] = createSignal(true);
|
|
389
|
+
const [showNoCatalog, setShowNoCatalog] = createSignal(false);
|
|
390
|
+
const [draftItems, setDraftItems] = createSignal([]);
|
|
391
|
+
const [savedSnapshot, setSavedSnapshot] = createSignal("");
|
|
392
|
+
const [selectedId, setSelectedId] = createSignal(null);
|
|
393
|
+
const [editorTitle, setEditorTitle] = createSignal("");
|
|
394
|
+
const [editorSubCategory, setEditorSubCategory] = createSignal("");
|
|
395
|
+
const [editorTokenValue, setEditorTokenValue] = createSignal("");
|
|
396
|
+
const [editorFontSize, setEditorFontSize] = createSignal("");
|
|
397
|
+
const [editorFontWeight, setEditorFontWeight] = createSignal("");
|
|
398
|
+
const [editorFontFamily, setEditorFontFamily] = createSignal("");
|
|
399
|
+
const [showTypographyFields, setShowTypographyFields] = createSignal(false);
|
|
400
|
+
const [showTokenValueField, setShowTokenValueField] = createSignal(false);
|
|
401
|
+
const [editorReadOnly, setEditorReadOnly] = createSignal(false);
|
|
402
|
+
const [editorReadOnlyReason, setEditorReadOnlyReason] = createSignal("");
|
|
403
|
+
const [showAnalysisSection, setShowAnalysisSection] = createSignal(false);
|
|
404
|
+
const [showReport, setShowReport] = createSignal(false);
|
|
405
|
+
const [reportSummary, setReportSummary] = createSignal("");
|
|
406
|
+
const [reportBody, setReportBody] = createSignal("");
|
|
407
|
+
let lastAnalysis = null;
|
|
408
|
+
let syncingEditorFromSelection = false;
|
|
409
|
+
const unsavedChangeCount = createMemo(() => {
|
|
410
|
+
const draft = draftItems();
|
|
411
|
+
const saved = savedSnapshot();
|
|
412
|
+
if (!saved)
|
|
413
|
+
return 0;
|
|
414
|
+
return serializeAddMenuCatalogForCompare(draft) === saved ? 0 : 1;
|
|
415
|
+
});
|
|
416
|
+
const addMenuListRows = createMemo(() => {
|
|
417
|
+
const selected = selectedId();
|
|
418
|
+
return draftItems().map((item) => ({
|
|
419
|
+
rowKey: item.id,
|
|
420
|
+
itemId: item.id,
|
|
421
|
+
title: item.title,
|
|
422
|
+
subCategory: item.subCategory ?? "Other",
|
|
423
|
+
rowClass: listRowClass(item.id, selected)
|
|
424
|
+
}));
|
|
425
|
+
});
|
|
426
|
+
const showAddMenuEditor = createMemo(() => selectedId() !== null);
|
|
427
|
+
const saveDisabled = createMemo(() => catalogLoading() || unsavedChangeCount() === 0 || draftItems().length === 0);
|
|
428
|
+
const discardDisabled = createMemo(() => catalogLoading() || unsavedChangeCount() === 0 || draftItems().length === 0);
|
|
429
|
+
const saveButtonLabel = createMemo(() => "Save and sync");
|
|
430
|
+
function updatePreview() {
|
|
431
|
+
const item = draftItems().find((entry) => entry.id === selectedId());
|
|
432
|
+
if (!item)
|
|
433
|
+
return;
|
|
434
|
+
const html = getAddMenuItemPreviewHtml(item);
|
|
435
|
+
refs.previewHost.exec$((element) => {
|
|
436
|
+
element.innerHTML = html;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function loadEditorFieldsFromItem(item) {
|
|
440
|
+
syncingEditorFromSelection = true;
|
|
441
|
+
const fields = extractAddMenuItemEditorFields(item);
|
|
442
|
+
setEditorTitle(fields.title);
|
|
443
|
+
setEditorSubCategory(item.subCategory ?? "");
|
|
444
|
+
setEditorTokenValue(fields.tokenValue);
|
|
445
|
+
setEditorFontSize(fields.fontSize);
|
|
446
|
+
setEditorFontWeight(fields.fontWeight);
|
|
447
|
+
setEditorFontFamily(fields.fontFamily);
|
|
448
|
+
setShowTypographyFields(fields.kind === "typography");
|
|
449
|
+
setShowTokenValueField(fields.kind === "color" || fields.kind === "spacing" || fields.kind === "rounded");
|
|
450
|
+
setEditorReadOnly(!fields.editable);
|
|
451
|
+
setEditorReadOnlyReason(fields.readOnlyReason);
|
|
452
|
+
syncingEditorFromSelection = false;
|
|
453
|
+
updatePreview();
|
|
454
|
+
}
|
|
455
|
+
function applyEditorPatchToDraft() {
|
|
456
|
+
if (syncingEditorFromSelection || editorReadOnly())
|
|
457
|
+
return;
|
|
458
|
+
const currentId = selectedId();
|
|
459
|
+
if (!currentId)
|
|
460
|
+
return;
|
|
461
|
+
setDraftItems((items) => items.map((item) => {
|
|
462
|
+
if (item.id !== currentId)
|
|
463
|
+
return item;
|
|
464
|
+
return patchAddMenuItem(item, {
|
|
465
|
+
title: editorTitle(),
|
|
466
|
+
tokenValue: editorTokenValue(),
|
|
467
|
+
fontSize: editorFontSize(),
|
|
468
|
+
fontWeight: editorFontWeight(),
|
|
469
|
+
fontFamily: editorFontFamily()
|
|
470
|
+
});
|
|
471
|
+
}));
|
|
472
|
+
updatePreview();
|
|
473
|
+
}
|
|
474
|
+
function selectItem(itemId) {
|
|
475
|
+
setSelectedId(itemId);
|
|
476
|
+
const item = draftItems().find((entry) => entry.id === itemId);
|
|
477
|
+
if (item) {
|
|
478
|
+
loadEditorFieldsFromItem(item);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async function loadCatalog() {
|
|
482
|
+
setCatalogLoading(true);
|
|
483
|
+
try {
|
|
484
|
+
const result = await loadDesignSystemAddMenuCatalog({});
|
|
485
|
+
const items = result.items;
|
|
486
|
+
setDraftItems(items);
|
|
487
|
+
const snapshot = serializeAddMenuCatalogForCompare(items);
|
|
488
|
+
setSavedSnapshot(snapshot);
|
|
489
|
+
setShowNoCatalog(items.length === 0);
|
|
490
|
+
if (items.length > 0) {
|
|
491
|
+
selectItem(items[0].id);
|
|
492
|
+
} else {
|
|
493
|
+
setSelectedId(null);
|
|
494
|
+
}
|
|
495
|
+
} catch (error) {
|
|
496
|
+
setShowNoCatalog(true);
|
|
497
|
+
setStatusMessage(error instanceof Error ? error.message : "Failed to load add-menu catalog.");
|
|
498
|
+
setStatusTone("error");
|
|
499
|
+
setShowStatusMessage(true);
|
|
500
|
+
} finally {
|
|
501
|
+
setCatalogLoading(false);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
createEffect(() => {
|
|
505
|
+
void loadCatalog();
|
|
506
|
+
});
|
|
507
|
+
refs.catalogList.onclick(({ event }) => {
|
|
508
|
+
const target = event.target?.closest("[data-catalog-item]");
|
|
509
|
+
if (!target)
|
|
510
|
+
return;
|
|
511
|
+
const itemId = target.getAttribute("data-catalog-item");
|
|
512
|
+
if (itemId) {
|
|
513
|
+
selectItem(itemId);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
refs.editorTitleInput.oninput(({ event }) => {
|
|
517
|
+
setEditorTitle(event.target.value);
|
|
518
|
+
applyEditorPatchToDraft();
|
|
519
|
+
});
|
|
520
|
+
refs.editorTokenValueInput.oninput(({ event }) => {
|
|
521
|
+
setEditorTokenValue(event.target.value);
|
|
522
|
+
applyEditorPatchToDraft();
|
|
523
|
+
});
|
|
524
|
+
refs.editorFontSizeInput.oninput(({ event }) => {
|
|
525
|
+
setEditorFontSize(event.target.value);
|
|
526
|
+
applyEditorPatchToDraft();
|
|
527
|
+
});
|
|
528
|
+
refs.editorFontWeightInput.oninput(({ event }) => {
|
|
529
|
+
setEditorFontWeight(event.target.value);
|
|
530
|
+
applyEditorPatchToDraft();
|
|
531
|
+
});
|
|
532
|
+
refs.editorFontFamilyInput.oninput(({ event }) => {
|
|
533
|
+
setEditorFontFamily(event.target.value);
|
|
534
|
+
applyEditorPatchToDraft();
|
|
535
|
+
});
|
|
536
|
+
refs.saveCatalogBtn.onclick(async () => {
|
|
537
|
+
setStatusMessage("Saving add-menu catalog…");
|
|
538
|
+
setStatusTone("info");
|
|
539
|
+
setShowStatusMessage(true);
|
|
540
|
+
try {
|
|
541
|
+
const result = await saveDesignSystemAddMenuCatalog({ items: draftItems() });
|
|
542
|
+
setSavedSnapshot(serializeAddMenuCatalogForCompare(draftItems()));
|
|
543
|
+
setStatusMessage(result.message);
|
|
544
|
+
setStatusTone("success");
|
|
545
|
+
notifyAddMenuChanged();
|
|
546
|
+
} catch (error) {
|
|
547
|
+
setStatusMessage(error instanceof Error ? error.message : "Save failed.");
|
|
548
|
+
setStatusTone("error");
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
refs.discardCatalogBtn.onclick(() => {
|
|
552
|
+
const saved = savedSnapshot();
|
|
553
|
+
if (!saved)
|
|
554
|
+
return;
|
|
555
|
+
const items = JSON.parse(saved);
|
|
556
|
+
setDraftItems(items);
|
|
557
|
+
const currentId = selectedId();
|
|
558
|
+
const nextItem = items.find((item) => item.id === currentId) ?? items[0];
|
|
559
|
+
if (nextItem) {
|
|
560
|
+
selectItem(nextItem.id);
|
|
561
|
+
}
|
|
562
|
+
setStatusMessage("Discarded unsaved changes.");
|
|
563
|
+
setStatusTone("info");
|
|
564
|
+
setShowStatusMessage(true);
|
|
565
|
+
});
|
|
566
|
+
refs.regenerateCatalogBtn.onclick(async () => {
|
|
567
|
+
if (unsavedChangeCount() > 0) {
|
|
568
|
+
setStatusMessage("Save or discard your changes before regenerating from DESIGN.md.");
|
|
569
|
+
setStatusTone("warning");
|
|
570
|
+
setShowStatusMessage(true);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
setStatusMessage("Regenerating add-menu catalog from DESIGN.md…");
|
|
574
|
+
setStatusTone("info");
|
|
575
|
+
setShowStatusMessage(true);
|
|
576
|
+
try {
|
|
577
|
+
const result = await regenerateDesignSystemAddMenu({});
|
|
578
|
+
await loadCatalog();
|
|
579
|
+
setStatusMessage(result.message);
|
|
580
|
+
setStatusTone("success");
|
|
581
|
+
notifyAddMenuChanged();
|
|
582
|
+
} catch (error) {
|
|
583
|
+
setStatusMessage(error instanceof Error ? error.message : "Regenerate failed.");
|
|
584
|
+
setStatusTone("error");
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
refs.toggleAnalysisBtn.onclick(() => {
|
|
588
|
+
setShowAnalysisSection((value) => !value);
|
|
589
|
+
});
|
|
590
|
+
refs.runAnalysisBtn.onclick(async () => {
|
|
591
|
+
setStatusMessage("Running design-system analysis…");
|
|
592
|
+
setStatusTone("info");
|
|
593
|
+
setShowStatusMessage(true);
|
|
594
|
+
try {
|
|
595
|
+
const result = await runDesignSystemAnalysisAction({});
|
|
596
|
+
lastAnalysis = result;
|
|
597
|
+
setShowReport(true);
|
|
598
|
+
setReportSummary(`Scanned ${result.filesScanned} file(s) — ${result.errorCount} error(s), ${result.warningCount} warning(s).`);
|
|
599
|
+
setReportBody(formatReportBody(result));
|
|
600
|
+
setStatusMessage("Analysis complete.");
|
|
601
|
+
setStatusTone(result.errorCount > 0 ? "error" : result.warningCount > 0 ? "warning" : "success");
|
|
602
|
+
} catch (error) {
|
|
603
|
+
setShowReport(false);
|
|
604
|
+
setStatusMessage(error instanceof Error ? error.message : "Analysis failed.");
|
|
605
|
+
setStatusTone("error");
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
refs.sendToAgentBtn.onclick(() => {
|
|
609
|
+
if (!lastAnalysis || lastAnalysis.findings.length === 0) {
|
|
610
|
+
setStatusMessage("Run analysis first and ensure there are findings to send.");
|
|
611
|
+
setStatusTone("warning");
|
|
612
|
+
setShowStatusMessage(true);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
window.parent.postMessage({
|
|
616
|
+
type: "aiditor:submitAgentTask",
|
|
617
|
+
prompt: buildDesignAnalysisAgentPrompt(lastAnalysis),
|
|
618
|
+
context: {
|
|
619
|
+
pageRoute: "/",
|
|
620
|
+
renderedUrl: window.location.origin
|
|
621
|
+
}
|
|
622
|
+
}, window.location.origin);
|
|
623
|
+
});
|
|
624
|
+
return {
|
|
625
|
+
render: () => ({
|
|
626
|
+
statusMessage: statusMessage(),
|
|
627
|
+
statusTone: statusTone(),
|
|
628
|
+
showStatusMessage: showStatusMessage(),
|
|
629
|
+
catalogLoading: catalogLoading(),
|
|
630
|
+
showNoCatalog: showNoCatalog(),
|
|
631
|
+
addMenuListRows: addMenuListRows(),
|
|
632
|
+
showAddMenuEditor: showAddMenuEditor(),
|
|
633
|
+
editorTitle: editorTitle(),
|
|
634
|
+
editorSubCategory: editorSubCategory(),
|
|
635
|
+
editorTokenValue: editorTokenValue(),
|
|
636
|
+
editorFontSize: editorFontSize(),
|
|
637
|
+
editorFontWeight: editorFontWeight(),
|
|
638
|
+
editorFontFamily: editorFontFamily(),
|
|
639
|
+
showTypographyFields: showTypographyFields(),
|
|
640
|
+
showTokenValueField: showTokenValueField(),
|
|
641
|
+
editorReadOnly: editorReadOnly(),
|
|
642
|
+
editorReadOnlyReason: editorReadOnlyReason(),
|
|
643
|
+
unsavedChangeCount: unsavedChangeCount(),
|
|
644
|
+
saveButtonLabel: saveButtonLabel(),
|
|
645
|
+
saveDisabled: saveDisabled(),
|
|
646
|
+
discardDisabled: discardDisabled(),
|
|
647
|
+
showAnalysisSection: showAnalysisSection(),
|
|
648
|
+
showReport: showReport(),
|
|
649
|
+
reportSummary: reportSummary(),
|
|
650
|
+
reportBody: reportBody()
|
|
651
|
+
})
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const designSystemSettingsPage = makeJayStackComponent().withProps().withInteractive(settingsPageConstructor);
|
|
655
|
+
export {
|
|
656
|
+
designSystemSettingsPage
|
|
657
|
+
};
|