@dryui/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2099 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ function __accessProp(key) {
7
+ return this[key];
8
+ }
9
+ var __toESMCache_node;
10
+ var __toESMCache_esm;
11
+ var __toESM = (mod, isNodeMode, target) => {
12
+ var canCache = mod != null && typeof mod === "object";
13
+ if (canCache) {
14
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
15
+ var cached = cache.get(mod);
16
+ if (cached)
17
+ return cached;
18
+ }
19
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
20
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
21
+ for (let key of __getOwnPropNames(mod))
22
+ if (!__hasOwnProp.call(to, key))
23
+ __defProp(to, key, {
24
+ get: __accessProp.bind(mod, key),
25
+ enumerable: true
26
+ });
27
+ if (canCache)
28
+ cache.set(mod, to);
29
+ return to;
30
+ };
31
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
32
+ var __returnValue = (v) => v;
33
+ function __exportSetter(name, newValue) {
34
+ this[name] = __returnValue.bind(null, newValue);
35
+ }
36
+ var __export = (target, all) => {
37
+ for (var name in all)
38
+ __defProp(target, name, {
39
+ get: all[name],
40
+ enumerable: true,
41
+ configurable: true,
42
+ set: __exportSetter.bind(all, name)
43
+ });
44
+ };
45
+
46
+ // src/utils.ts
47
+ function buildLineOffsets(text) {
48
+ const offsets = [0];
49
+ for (let i = 0;i < text.length; i++) {
50
+ if (text[i] === `
51
+ `)
52
+ offsets.push(i + 1);
53
+ }
54
+ return offsets;
55
+ }
56
+ function lineAtOffset(lineOffsets, offset) {
57
+ let lo = 0;
58
+ let hi = lineOffsets.length - 1;
59
+ while (lo < hi) {
60
+ const mid = lo + hi + 1 >> 1;
61
+ const midVal = lineOffsets[mid];
62
+ if (midVal !== undefined && midVal <= offset)
63
+ lo = mid;
64
+ else
65
+ hi = mid - 1;
66
+ }
67
+ return lo + 1;
68
+ }
69
+
70
+ // src/reviewer.ts
71
+ function extractImports(code) {
72
+ const imports = new Set;
73
+ const scriptMatch = code.match(/<script[^>]*>([\s\S]*?)<\/script>/);
74
+ if (!scriptMatch)
75
+ return imports;
76
+ const scriptContent = scriptMatch[1] ?? "";
77
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*['"]@dryui\/(ui|primitives)['"]/g;
78
+ let match;
79
+ while ((match = importRegex.exec(scriptContent)) !== null) {
80
+ const raw = match[1] ?? "";
81
+ const names = raw.split(",");
82
+ for (const name of names) {
83
+ const trimmed = name.trim();
84
+ if (trimmed)
85
+ imports.add(trimmed);
86
+ }
87
+ }
88
+ return imports;
89
+ }
90
+ function extractTags(code) {
91
+ const template = code.replace(/<script[^>]*>[\s\S]*?<\/script>/g, (m) => `
92
+ `.repeat(countNewlines(m))).replace(/<style[^>]*>[\s\S]*?<\/style>/g, (m) => `
93
+ `.repeat(countNewlines(m)));
94
+ const lineOffsets = buildLineOffsets(code);
95
+ const tagRegex = /<([A-Z][a-zA-Z0-9]*(?:\.[A-Z][a-zA-Z0-9]*)*)\s*([^>]*?)(\/)?>/g;
96
+ const tags = [];
97
+ let match;
98
+ while ((match = tagRegex.exec(template)) !== null) {
99
+ const name = match[1] ?? "";
100
+ const attrsStr = match[2] ?? "";
101
+ const selfClosing = match[3] === "/";
102
+ const line = lineAtOffset(lineOffsets, match.index);
103
+ const props = extractPropsFromAttrs(attrsStr);
104
+ const hasSpread = /\{\.\.\./.test(attrsStr);
105
+ tags.push({ name, line, props, hasSpread, selfClosing });
106
+ }
107
+ return tags;
108
+ }
109
+ function extractStyles(code) {
110
+ const styleMatch = code.match(/<style[^>]*>([\s\S]*?)<\/style>/);
111
+ return styleMatch ? styleMatch[1] ?? null : null;
112
+ }
113
+ function countNewlines(str) {
114
+ let count = 0;
115
+ for (let i = 0;i < str.length; i++) {
116
+ if (str[i] === `
117
+ `)
118
+ count++;
119
+ }
120
+ return count;
121
+ }
122
+ function stripBraceExpressions(str) {
123
+ let result = "";
124
+ let depth = 0;
125
+ for (let i = 0;i < str.length; i++) {
126
+ if (str[i] === "{") {
127
+ if (depth === 0)
128
+ result += "{}";
129
+ depth++;
130
+ } else if (str[i] === "}") {
131
+ depth--;
132
+ } else if (depth === 0) {
133
+ result += str[i];
134
+ }
135
+ }
136
+ return result;
137
+ }
138
+ function extractPropsFromAttrs(attrsStr) {
139
+ const props = [];
140
+ if (!attrsStr.trim())
141
+ return props;
142
+ const stripped = stripBraceExpressions(attrsStr.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''"));
143
+ const bindRegex = /\bbind:([a-zA-Z_][a-zA-Z0-9_]*)/g;
144
+ let m;
145
+ while ((m = bindRegex.exec(stripped)) !== null) {
146
+ const bound = m[1];
147
+ if (bound)
148
+ props.push("bind:" + bound);
149
+ }
150
+ const namedRegex = /\b([a-zA-Z_][a-zA-Z0-9_-]*)\s*=/g;
151
+ while ((m = namedRegex.exec(stripped)) !== null) {
152
+ const propName = m[1];
153
+ if (propName && !props.includes(propName)) {
154
+ props.push(propName);
155
+ }
156
+ }
157
+ const boolRegex = /(?<!\.)(?<![:{])\b([a-zA-Z_][a-zA-Z0-9_-]*)\b(?!\s*=)/g;
158
+ while ((m = boolRegex.exec(stripped)) !== null) {
159
+ const propName = m[1];
160
+ if (!propName)
161
+ continue;
162
+ if (props.includes(propName) || props.includes("bind:" + propName) || propName === "bind") {
163
+ continue;
164
+ }
165
+ props.push(propName);
166
+ }
167
+ return props;
168
+ }
169
+ var NATIVE_HTML_ATTRS = new Set([
170
+ "id",
171
+ "class",
172
+ "style",
173
+ "title",
174
+ "lang",
175
+ "dir",
176
+ "tabindex",
177
+ "hidden",
178
+ "role",
179
+ "slot",
180
+ "is",
181
+ "part",
182
+ "translate",
183
+ "draggable",
184
+ "contenteditable",
185
+ "spellcheck",
186
+ "autocapitalize",
187
+ "inputmode",
188
+ "enterkeyhint",
189
+ "children",
190
+ "name",
191
+ "value",
192
+ "type",
193
+ "placeholder",
194
+ "required",
195
+ "readonly",
196
+ "disabled",
197
+ "checked",
198
+ "selected",
199
+ "multiple",
200
+ "autofocus",
201
+ "autocomplete",
202
+ "pattern",
203
+ "min",
204
+ "max",
205
+ "step",
206
+ "minlength",
207
+ "maxlength",
208
+ "form",
209
+ "formaction",
210
+ "formmethod",
211
+ "formtarget",
212
+ "formnovalidate",
213
+ "accept",
214
+ "capture",
215
+ "list",
216
+ "size",
217
+ "href",
218
+ "target",
219
+ "rel",
220
+ "download",
221
+ "src",
222
+ "alt",
223
+ "width",
224
+ "height",
225
+ "loading",
226
+ "decoding",
227
+ "crossorigin",
228
+ "referrerpolicy",
229
+ "for",
230
+ "htmlFor"
231
+ ]);
232
+ function isPropAllowed(propName) {
233
+ if (NATIVE_HTML_ATTRS.has(propName))
234
+ return true;
235
+ if (propName.startsWith("aria-"))
236
+ return true;
237
+ if (propName.startsWith("data-"))
238
+ return true;
239
+ if (propName.startsWith("on"))
240
+ return true;
241
+ if (propName.startsWith("bind:"))
242
+ return true;
243
+ return false;
244
+ }
245
+ function checkBareCompound(tags, spec) {
246
+ const issues = [];
247
+ for (const tag of tags) {
248
+ if (tag.name.includes("."))
249
+ continue;
250
+ const def = spec.components[tag.name];
251
+ if (!def?.compound)
252
+ continue;
253
+ const partNames = Object.keys(def.parts ?? {});
254
+ const hasRootPart = partNames.includes("Root");
255
+ const firstPart = partNames.find((part) => part !== "Root");
256
+ if (hasRootPart) {
257
+ issues.push({
258
+ severity: "error",
259
+ code: "bare-compound",
260
+ line: tag.line,
261
+ message: `<${tag.name}> is a compound component — use <${tag.name}.Root>`,
262
+ fix: `<${tag.name}.Root>`
263
+ });
264
+ continue;
265
+ }
266
+ issues.push({
267
+ severity: "error",
268
+ code: "bare-compound",
269
+ line: tag.line,
270
+ message: `<${tag.name}> is a namespaced component — use a part like <${tag.name}.${firstPart ?? "Text"}>`,
271
+ fix: `<${tag.name}.${firstPart ?? "Text"}>`
272
+ });
273
+ }
274
+ return issues;
275
+ }
276
+ function checkUnknownComponent(tags, imports, spec) {
277
+ const issues = [];
278
+ for (const tag of tags) {
279
+ const root = tag.name.split(".")[0] ?? tag.name;
280
+ if (imports.has(root) && !spec.components[root]) {
281
+ issues.push({
282
+ severity: "error",
283
+ code: "unknown-component",
284
+ line: tag.line,
285
+ message: `<${tag.name}> is not a known DryUI component`,
286
+ fix: null
287
+ });
288
+ }
289
+ }
290
+ return issues;
291
+ }
292
+ function checkInvalidPartName(tags, spec) {
293
+ const issues = [];
294
+ for (const tag of tags) {
295
+ if (!tag.name.includes("."))
296
+ continue;
297
+ const parts = tag.name.split(".");
298
+ const root = parts[0] ?? "";
299
+ const part = parts[1] ?? "";
300
+ if (!root || !part)
301
+ continue;
302
+ const def = spec.components[root];
303
+ if (!def?.compound || !def.parts)
304
+ continue;
305
+ if (!def.parts[part]) {
306
+ const validParts = Object.keys(def.parts);
307
+ issues.push({
308
+ severity: "error",
309
+ code: "invalid-part",
310
+ line: tag.line,
311
+ message: `<${root}.${part}> — "${part}" is not a valid part of ${root}. Valid parts: ${validParts.join(", ")}`,
312
+ fix: null
313
+ });
314
+ }
315
+ }
316
+ return issues;
317
+ }
318
+ function checkInvalidProp(tags, spec) {
319
+ const issues = [];
320
+ for (const tag of tags) {
321
+ const segments = tag.name.split(".");
322
+ const root = segments[0] ?? "";
323
+ const part = segments[1];
324
+ if (!root)
325
+ continue;
326
+ const def = spec.components[root];
327
+ if (!def)
328
+ continue;
329
+ let specProps;
330
+ if (part) {
331
+ specProps = def.parts?.[part]?.props;
332
+ } else if (!def.compound) {
333
+ specProps = def.props;
334
+ } else {
335
+ continue;
336
+ }
337
+ if (!specProps)
338
+ continue;
339
+ for (const prop of tag.props) {
340
+ const checkName = prop.startsWith("bind:") ? prop.slice(5) : prop;
341
+ if (isPropAllowed(prop))
342
+ continue;
343
+ if (!specProps[checkName]) {
344
+ issues.push({
345
+ severity: "error",
346
+ code: "invalid-prop",
347
+ line: tag.line,
348
+ message: `<${tag.name}> does not accept prop "${checkName}"`,
349
+ fix: null
350
+ });
351
+ }
352
+ }
353
+ }
354
+ return issues;
355
+ }
356
+ function checkMissingRequiredProp(tags, spec) {
357
+ const issues = [];
358
+ for (const tag of tags) {
359
+ if (tag.hasSpread)
360
+ continue;
361
+ const segments = tag.name.split(".");
362
+ const root = segments[0] ?? "";
363
+ const part = segments[1];
364
+ if (!root)
365
+ continue;
366
+ const def = spec.components[root];
367
+ if (!def)
368
+ continue;
369
+ let specProps;
370
+ if (part) {
371
+ specProps = def.parts?.[part]?.props;
372
+ } else if (!def.compound) {
373
+ specProps = def.props;
374
+ } else {
375
+ continue;
376
+ }
377
+ if (!specProps)
378
+ continue;
379
+ const tagPropNames = new Set(tag.props.map((p) => p.startsWith("bind:") ? p.slice(5) : p));
380
+ for (const [propName, propDef] of Object.entries(specProps)) {
381
+ if (propDef.required && !tagPropNames.has(propName)) {
382
+ if (propName === "children" && !tag.selfClosing)
383
+ continue;
384
+ issues.push({
385
+ severity: "error",
386
+ code: "missing-required-prop",
387
+ line: tag.line,
388
+ message: `<${tag.name}> is missing required prop "${propName}"`,
389
+ fix: `${propName}={...}`
390
+ });
391
+ }
392
+ }
393
+ }
394
+ return issues;
395
+ }
396
+ function checkOrphanedPart(tags, spec) {
397
+ const issues = [];
398
+ const allNames = new Set(tags.map((t) => t.name));
399
+ for (const tag of tags) {
400
+ if (!tag.name.includes("."))
401
+ continue;
402
+ const root = tag.name.split(".")[0] ?? "";
403
+ if (!root)
404
+ continue;
405
+ const def = spec.components[root];
406
+ if (!def?.compound || !def.parts?.Root)
407
+ continue;
408
+ if (!allNames.has(`${root}.Root`)) {
409
+ issues.push({
410
+ severity: "warning",
411
+ code: "orphaned-part",
412
+ line: tag.line,
413
+ message: `<${tag.name}> used without <${root}.Root> in the template`,
414
+ fix: `Wrap in <${root}.Root>`
415
+ });
416
+ }
417
+ }
418
+ return issues;
419
+ }
420
+ function checkMissingLabel(tags, code) {
421
+ const issues = [];
422
+ const template = code.replace(/<script[^>]*>[\s\S]*?<\/script>/g, (m) => `
423
+ `.repeat(countNewlines(m))).replace(/<style[^>]*>[\s\S]*?<\/style>/g, (m) => `
424
+ `.repeat(countNewlines(m)));
425
+ const lineOffsets = buildLineOffsets(code);
426
+ for (const tag of tags) {
427
+ if (tag.name !== "Input" && tag.name !== "Select.Root" && tag.name !== "Combobox.Input")
428
+ continue;
429
+ const hasAriaLabel = tag.props.some((p) => p === "aria-label");
430
+ if (hasAriaLabel)
431
+ continue;
432
+ const tagOffset = findTagOffset(template, tag.name, tag.line, lineOffsets);
433
+ const wrappedByField = tagOffset !== -1 && template.lastIndexOf("<Field.Root", tagOffset) !== -1 && template.indexOf("</Field.Root>", tagOffset) !== -1;
434
+ if (!wrappedByField) {
435
+ issues.push({
436
+ severity: "warning",
437
+ code: "missing-label",
438
+ line: tag.line,
439
+ message: `<${tag.name}> may be missing an accessible label — add aria-label or wrap in <Field.Root> with <Label>`,
440
+ fix: 'aria-label="..."'
441
+ });
442
+ }
443
+ }
444
+ return issues;
445
+ }
446
+ function findTagOffset(template, tagName, targetLine, lineOffsets) {
447
+ const regex = new RegExp(`<${tagName.replace(".", "\\.")}[\\s/>]`, "g");
448
+ let match;
449
+ while ((match = regex.exec(template)) !== null) {
450
+ const line = lineAtOffset(lineOffsets, match.index);
451
+ if (line === targetLine)
452
+ return match.index;
453
+ }
454
+ return -1;
455
+ }
456
+ function checkMissingThumbnail(imports, spec) {
457
+ if (!spec.thumbnails)
458
+ return [];
459
+ const issues = [];
460
+ const thumbnailSet = new Set(spec.thumbnails);
461
+ for (const name of imports) {
462
+ if (!spec.components[name])
463
+ continue;
464
+ if (!thumbnailSet.has(name)) {
465
+ issues.push({
466
+ severity: "warning",
467
+ code: "missing-thumbnail",
468
+ line: 1,
469
+ message: `Component '${name}' has no SVG thumbnail — run 'bun run thumbnail:create ${name}'`,
470
+ fix: null
471
+ });
472
+ }
473
+ }
474
+ return issues;
475
+ }
476
+ function checkImageWithoutAlt(tags) {
477
+ const issues = [];
478
+ for (const tag of tags) {
479
+ if (tag.name !== "Avatar")
480
+ continue;
481
+ const hasAlt = tag.props.includes("alt");
482
+ const hasFallback = tag.props.includes("fallback");
483
+ if (!hasAlt && !hasFallback) {
484
+ issues.push({
485
+ severity: "warning",
486
+ code: "missing-alt",
487
+ line: tag.line,
488
+ message: '<Avatar> is missing "alt" and "fallback" props for accessibility',
489
+ fix: 'alt="..."'
490
+ });
491
+ }
492
+ }
493
+ return issues;
494
+ }
495
+ function checkCustomGridLayout(styles, code) {
496
+ const issues = [];
497
+ if (/display:\s*grid/.test(styles)) {
498
+ const startLine = getStyleBlockStartLine(code);
499
+ const localLine = findLineInBlock(styles, /display:\s*grid/);
500
+ issues.push({
501
+ severity: "warning",
502
+ code: "use-grid-component",
503
+ line: styleLineToFileLine(localLine, startLine),
504
+ message: "Use DryUI's <Grid> component instead of custom CSS grid. Grid provides responsive columns, gap tokens, and breakpoint handling.",
505
+ fix: '<Grid --dry-grid-columns="repeat(2, 1fr)" --dry-grid-gap="var(--dry-space-4)">'
506
+ });
507
+ }
508
+ return issues;
509
+ }
510
+ function checkCustomFlexLayout(styles, code) {
511
+ const issues = [];
512
+ if (/display:\s*flex/.test(styles)) {
513
+ const startLine = getStyleBlockStartLine(code);
514
+ const localLine = findLineInBlock(styles, /display:\s*flex/);
515
+ issues.push({
516
+ severity: "warning",
517
+ code: "use-flex-component",
518
+ line: styleLineToFileLine(localLine, startLine),
519
+ message: "Use DryUI's <Flex> or <Stack> component instead of custom CSS flexbox. These provide gap, alignment, and responsive behavior with theme tokens.",
520
+ fix: "<Flex> or <Stack>"
521
+ });
522
+ }
523
+ return issues;
524
+ }
525
+ function checkCustomFieldMarkup(code) {
526
+ const issues = [];
527
+ const template = code.replace(/<script[^>]*>[\s\S]*?<\/script>/g, (m) => `
528
+ `.repeat(countNewlines(m))).replace(/<style[^>]*>[\s\S]*?<\/style>/g, (m) => `
529
+ `.repeat(countNewlines(m)));
530
+ const lineOffsets = buildLineOffsets(code);
531
+ const fieldClassRegex = /class=["']field["']/g;
532
+ let match;
533
+ while ((match = fieldClassRegex.exec(template)) !== null) {
534
+ const line = lineAtOffset(lineOffsets, match.index);
535
+ issues.push({
536
+ severity: "warning",
537
+ code: "use-field-component",
538
+ line,
539
+ message: "Use <Field.Root> + <Label> instead of custom field markup. Field provides accessible labeling, error states, and consistent spacing.",
540
+ fix: "<Field.Root> + <Label>"
541
+ });
542
+ }
543
+ const manualFieldRegex = /<span[^>]*>[\s\S]*?<\/span>\s*<(?:input|select|textarea)[\s/>]/g;
544
+ while ((match = manualFieldRegex.exec(template)) !== null) {
545
+ const line = lineAtOffset(lineOffsets, match.index);
546
+ issues.push({
547
+ severity: "warning",
548
+ code: "use-field-component",
549
+ line,
550
+ message: "Use <Field.Root> + <Label> instead of custom field markup. Field provides accessible labeling, error states, and consistent spacing.",
551
+ fix: "<Field.Root> + <Label>"
552
+ });
553
+ }
554
+ return issues;
555
+ }
556
+ function checkRawStyledButton(code) {
557
+ const issues = [];
558
+ const template = code.replace(/<script[^>]*>[\s\S]*?<\/script>/g, (m) => `
559
+ `.repeat(countNewlines(m))).replace(/<style[^>]*>[\s\S]*?<\/style>/g, (m) => `
560
+ `.repeat(countNewlines(m)));
561
+ const lineOffsets = buildLineOffsets(code);
562
+ const rawBtnRegex = /<button\s[^>]*class=/g;
563
+ let match;
564
+ while ((match = rawBtnRegex.exec(template)) !== null) {
565
+ const line = lineAtOffset(lineOffsets, match.index);
566
+ issues.push({
567
+ severity: "warning",
568
+ code: "use-button-component",
569
+ line,
570
+ message: "Use DryUI's <Button> component instead of raw <button> with custom classes. Button provides variants, sizes, loading states, and theme-consistent styling.",
571
+ fix: "<Button>"
572
+ });
573
+ }
574
+ return issues;
575
+ }
576
+ function checkCustomMaxWidthCentering(styles, code) {
577
+ const issues = [];
578
+ if (/max-width/.test(styles) && /margin[^;]*auto/.test(styles)) {
579
+ const startLine = getStyleBlockStartLine(code);
580
+ const localLine = findLineInBlock(styles, /max-width/);
581
+ issues.push({
582
+ severity: "warning",
583
+ code: "use-container-component",
584
+ line: styleLineToFileLine(localLine, startLine),
585
+ message: "Use DryUI's <Container> component instead of custom max-width + margin centering.",
586
+ fix: "<Container>"
587
+ });
588
+ }
589
+ return issues;
590
+ }
591
+ function getStyleBlockStartLine(code) {
592
+ const idx = code.search(/<style[^>]*>/);
593
+ if (idx === -1)
594
+ return 1;
595
+ const lineOffsets = buildLineOffsets(code);
596
+ const tagEndIdx = code.indexOf(">", idx) + 1;
597
+ return lineAtOffset(lineOffsets, tagEndIdx);
598
+ }
599
+ function styleLineToFileLine(lineInStyleBlock, styleStartLine) {
600
+ return styleStartLine + lineInStyleBlock - 1;
601
+ }
602
+ function findLineInBlock(block, regex) {
603
+ const lines = block.split(`
604
+ `);
605
+ for (let i = 0;i < lines.length; i++) {
606
+ const line = lines[i];
607
+ if (line !== undefined && regex.test(line))
608
+ return i + 1;
609
+ }
610
+ return 1;
611
+ }
612
+ function checkManualFlex(styles, code) {
613
+ const issues = [];
614
+ if (/display:\s*flex/.test(styles)) {
615
+ const startLine = getStyleBlockStartLine(code);
616
+ const localLine = findLineInBlock(styles, /display:\s*flex/);
617
+ issues.push({
618
+ severity: "suggestion",
619
+ code: "prefer-layout",
620
+ line: styleLineToFileLine(localLine, startLine),
621
+ message: "Manual `display: flex` detected — consider using <Flex> or <Stack> instead",
622
+ fix: "<Flex> or <Stack>"
623
+ });
624
+ }
625
+ return issues;
626
+ }
627
+ function checkManualGrid(styles, code) {
628
+ const issues = [];
629
+ if (/display:\s*grid/.test(styles) && /grid-template-columns/.test(styles)) {
630
+ const startLine = getStyleBlockStartLine(code);
631
+ const localLine = findLineInBlock(styles, /display:\s*grid/);
632
+ issues.push({
633
+ severity: "suggestion",
634
+ code: "prefer-layout",
635
+ line: styleLineToFileLine(localLine, startLine),
636
+ message: 'Manual CSS grid detected — consider using <Grid --dry-grid-columns="repeat(2, 1fr)"> instead',
637
+ fix: '<Grid --dry-grid-columns="repeat(2, 1fr)" --dry-grid-gap="var(--dry-space-4)">'
638
+ });
639
+ }
640
+ return issues;
641
+ }
642
+ function checkHardcodedColors(styles, code) {
643
+ const issues = [];
644
+ const colorRegex = /(?:^|;)\s*(?:color|background(?:-color)?)\s*:\s*(?!.*var\s*\()/m;
645
+ if (colorRegex.test(styles)) {
646
+ const startLine = getStyleBlockStartLine(code);
647
+ const localLine = findLineInBlock(styles, /(?:color|background(?:-color)?)\s*:\s*(?!.*var\s*\()/);
648
+ issues.push({
649
+ severity: "suggestion",
650
+ code: "hardcoded-color",
651
+ line: styleLineToFileLine(localLine, startLine),
652
+ message: "Hardcoded color value — consider using `--dry-*` CSS custom properties for theming",
653
+ fix: "var(--dry-*)"
654
+ });
655
+ }
656
+ return issues;
657
+ }
658
+ function checkManualCentering(styles, code) {
659
+ const issues = [];
660
+ if (/max-width/.test(styles) && /margin:\s*[^;]*auto/.test(styles)) {
661
+ const startLine = getStyleBlockStartLine(code);
662
+ const localLine = findLineInBlock(styles, /max-width/);
663
+ issues.push({
664
+ severity: "suggestion",
665
+ code: "prefer-container",
666
+ line: styleLineToFileLine(localLine, startLine),
667
+ message: "Manual centering with max-width + margin auto — consider using <Container> instead",
668
+ fix: "<Container>"
669
+ });
670
+ }
671
+ return issues;
672
+ }
673
+ function checkCustomThemeOverrides(styles, code) {
674
+ if (!styles || !/--dry-/.test(styles))
675
+ return [];
676
+ return [
677
+ {
678
+ severity: "suggestion",
679
+ code: "theme-in-style",
680
+ line: getStyleBlockStartLine(code),
681
+ message: "Custom --dry-* variable overrides detected in <style> — run the `diagnose` tool on your theme CSS for a full health check",
682
+ fix: null
683
+ }
684
+ ];
685
+ }
686
+ function checkRawHr(code) {
687
+ const issues = [];
688
+ const template = code.replace(/<script[^>]*>[\s\S]*?<\/script>/g, (m) => `
689
+ `.repeat(countNewlines(m))).replace(/<style[^>]*>[\s\S]*?<\/style>/g, (m) => `
690
+ `.repeat(countNewlines(m)));
691
+ const hrRegex = /<hr[\s/>]/g;
692
+ const lineOffsets = buildLineOffsets(code);
693
+ let match;
694
+ while ((match = hrRegex.exec(template)) !== null) {
695
+ const line = lineAtOffset(lineOffsets, match.index);
696
+ issues.push({
697
+ severity: "suggestion",
698
+ code: "prefer-separator",
699
+ line,
700
+ message: "Raw <hr> element — consider using <Separator /> for consistent styling",
701
+ fix: "<Separator />"
702
+ });
703
+ }
704
+ return issues;
705
+ }
706
+ function reviewComponent(code, spec, filename) {
707
+ const imports = extractImports(code);
708
+ const tags = extractTags(code);
709
+ const styles = extractStyles(code);
710
+ const issues = [];
711
+ issues.push(...checkBareCompound(tags, spec));
712
+ issues.push(...checkUnknownComponent(tags, imports, spec));
713
+ issues.push(...checkInvalidPartName(tags, spec));
714
+ issues.push(...checkInvalidProp(tags, spec));
715
+ issues.push(...checkMissingRequiredProp(tags, spec));
716
+ issues.push(...checkOrphanedPart(tags, spec));
717
+ issues.push(...checkMissingLabel(tags, code));
718
+ issues.push(...checkImageWithoutAlt(tags));
719
+ issues.push(...checkMissingThumbnail(imports, spec));
720
+ issues.push(...checkCustomFieldMarkup(code));
721
+ issues.push(...checkRawStyledButton(code));
722
+ if (styles) {
723
+ issues.push(...checkCustomGridLayout(styles, code));
724
+ issues.push(...checkCustomFlexLayout(styles, code));
725
+ issues.push(...checkCustomMaxWidthCentering(styles, code));
726
+ }
727
+ if (styles) {
728
+ issues.push(...checkManualFlex(styles, code));
729
+ issues.push(...checkManualGrid(styles, code));
730
+ issues.push(...checkHardcodedColors(styles, code));
731
+ issues.push(...checkManualCentering(styles, code));
732
+ issues.push(...checkCustomThemeOverrides(styles, code));
733
+ }
734
+ issues.push(...checkRawHr(code));
735
+ issues.sort((a, b) => a.line - b.line);
736
+ const errors = issues.filter((i) => i.severity === "error").length;
737
+ const warnings = issues.filter((i) => i.severity === "warning").length;
738
+ const suggestions = issues.filter((i) => i.severity === "suggestion").length;
739
+ const summary = issues.length === 0 ? "No issues found" : `${errors} error${errors !== 1 ? "s" : ""}, ${warnings} warning${warnings !== 1 ? "s" : ""}, ${suggestions} suggestion${suggestions !== 1 ? "s" : ""}`;
740
+ return { issues, summary, ...filename ? { filename } : {} };
741
+ }
742
+
743
+ // src/theme-checker.ts
744
+ function capture(match, index) {
745
+ const value = match[index];
746
+ if (value === undefined)
747
+ throw new Error(`Missing capture group ${index}`);
748
+ return value;
749
+ }
750
+ var REQUIRED_TOKENS = [
751
+ "--dry-color-text-strong",
752
+ "--dry-color-text-weak",
753
+ "--dry-color-icon",
754
+ "--dry-color-stroke-strong",
755
+ "--dry-color-stroke-weak",
756
+ "--dry-color-fill",
757
+ "--dry-color-fill-hover",
758
+ "--dry-color-fill-active",
759
+ "--dry-color-brand",
760
+ "--dry-color-text-brand",
761
+ "--dry-color-fill-brand",
762
+ "--dry-color-fill-brand-hover",
763
+ "--dry-color-fill-brand-active",
764
+ "--dry-color-fill-brand-weak",
765
+ "--dry-color-stroke-brand",
766
+ "--dry-color-on-brand",
767
+ "--dry-color-focus-ring",
768
+ "--dry-color-bg-base",
769
+ "--dry-color-bg-raised",
770
+ "--dry-color-bg-overlay",
771
+ "--dry-color-text-error",
772
+ "--dry-color-fill-error",
773
+ "--dry-color-fill-error-hover",
774
+ "--dry-color-fill-error-weak",
775
+ "--dry-color-stroke-error",
776
+ "--dry-color-on-error",
777
+ "--dry-color-text-warning",
778
+ "--dry-color-fill-warning",
779
+ "--dry-color-fill-warning-hover",
780
+ "--dry-color-fill-warning-weak",
781
+ "--dry-color-stroke-warning",
782
+ "--dry-color-on-warning",
783
+ "--dry-color-text-success",
784
+ "--dry-color-fill-success",
785
+ "--dry-color-fill-success-hover",
786
+ "--dry-color-fill-success-weak",
787
+ "--dry-color-stroke-success",
788
+ "--dry-color-on-success",
789
+ "--dry-color-text-info",
790
+ "--dry-color-fill-info",
791
+ "--dry-color-fill-info-hover",
792
+ "--dry-color-fill-info-weak",
793
+ "--dry-color-stroke-info",
794
+ "--dry-color-on-info",
795
+ "--dry-shadow-raised",
796
+ "--dry-shadow-overlay",
797
+ "--dry-color-overlay-backdrop",
798
+ "--dry-color-overlay-backdrop-strong"
799
+ ];
800
+ var FULL_THEME_THRESHOLD = 4;
801
+ var NAMED_COLORS = new Set([
802
+ "aliceblue",
803
+ "antiquewhite",
804
+ "aqua",
805
+ "aquamarine",
806
+ "azure",
807
+ "beige",
808
+ "bisque",
809
+ "black",
810
+ "blanchedalmond",
811
+ "blue",
812
+ "blueviolet",
813
+ "brown",
814
+ "burlywood",
815
+ "cadetblue",
816
+ "chartreuse",
817
+ "chocolate",
818
+ "coral",
819
+ "cornflowerblue",
820
+ "cornsilk",
821
+ "crimson",
822
+ "cyan",
823
+ "darkblue",
824
+ "darkcyan",
825
+ "darkgoldenrod",
826
+ "darkgray",
827
+ "darkgreen",
828
+ "darkgrey",
829
+ "darkkhaki",
830
+ "darkmagenta",
831
+ "darkolivegreen",
832
+ "darkorange",
833
+ "darkorchid",
834
+ "darkred",
835
+ "darksalmon",
836
+ "darkseagreen",
837
+ "darkslateblue",
838
+ "darkslategray",
839
+ "darkslategrey",
840
+ "darkturquoise",
841
+ "darkviolet",
842
+ "deeppink",
843
+ "deepskyblue",
844
+ "dimgray",
845
+ "dimgrey",
846
+ "dodgerblue",
847
+ "firebrick",
848
+ "floralwhite",
849
+ "forestgreen",
850
+ "fuchsia",
851
+ "gainsboro",
852
+ "ghostwhite",
853
+ "gold",
854
+ "goldenrod",
855
+ "gray",
856
+ "green",
857
+ "greenyellow",
858
+ "grey",
859
+ "honeydew",
860
+ "hotpink",
861
+ "indianred",
862
+ "indigo",
863
+ "ivory",
864
+ "khaki",
865
+ "lavender",
866
+ "lavenderblush",
867
+ "lawngreen",
868
+ "lemonchiffon",
869
+ "lightblue",
870
+ "lightcoral",
871
+ "lightcyan",
872
+ "lightgoldenrodyellow",
873
+ "lightgray",
874
+ "lightgreen",
875
+ "lightgrey",
876
+ "lightpink",
877
+ "lightsalmon",
878
+ "lightseagreen",
879
+ "lightskyblue",
880
+ "lightslategray",
881
+ "lightslategrey",
882
+ "lightsteelblue",
883
+ "lightyellow",
884
+ "lime",
885
+ "limegreen",
886
+ "linen",
887
+ "magenta",
888
+ "maroon",
889
+ "mediumaquamarine",
890
+ "mediumblue",
891
+ "mediumorchid",
892
+ "mediumpurple",
893
+ "mediumseagreen",
894
+ "mediumslateblue",
895
+ "mediumspringgreen",
896
+ "mediumturquoise",
897
+ "mediumvioletred",
898
+ "midnightblue",
899
+ "mintcream",
900
+ "mistyrose",
901
+ "moccasin",
902
+ "navajowhite",
903
+ "navy",
904
+ "oldlace",
905
+ "olive",
906
+ "olivedrab",
907
+ "orange",
908
+ "orangered",
909
+ "orchid",
910
+ "palegoldenrod",
911
+ "palegreen",
912
+ "paleturquoise",
913
+ "palevioletred",
914
+ "papayawhip",
915
+ "peachpuff",
916
+ "peru",
917
+ "pink",
918
+ "plum",
919
+ "powderblue",
920
+ "purple",
921
+ "rebeccapurple",
922
+ "red",
923
+ "rosybrown",
924
+ "royalblue",
925
+ "saddlebrown",
926
+ "salmon",
927
+ "sandybrown",
928
+ "seagreen",
929
+ "seashell",
930
+ "sienna",
931
+ "silver",
932
+ "skyblue",
933
+ "slateblue",
934
+ "slategray",
935
+ "slategrey",
936
+ "snow",
937
+ "springgreen",
938
+ "steelblue",
939
+ "tan",
940
+ "teal",
941
+ "thistle",
942
+ "tomato",
943
+ "turquoise",
944
+ "violet",
945
+ "wheat",
946
+ "white",
947
+ "whitesmoke",
948
+ "yellow",
949
+ "yellowgreen",
950
+ "transparent",
951
+ "currentcolor",
952
+ "inherit"
953
+ ]);
954
+ var SURFACE_TOKENS = new Set([
955
+ "--dry-color-bg-base",
956
+ "--dry-color-bg-raised",
957
+ "--dry-color-bg-overlay"
958
+ ]);
959
+ function stripComments(css) {
960
+ return css.replace(/\/\*[\s\S]*?\*\//g, "");
961
+ }
962
+ function extractDryVariables(css) {
963
+ const cleaned = stripComments(css);
964
+ const vars = new Map;
965
+ const regex = /(--dry-[a-zA-Z0-9-]+)\s*:\s*([^;]*);/g;
966
+ let match;
967
+ const lineOffsets = buildLineOffsets(cleaned);
968
+ while ((match = regex.exec(cleaned)) !== null) {
969
+ const name = capture(match, 1);
970
+ const value = (match[2] ?? "").trim();
971
+ const line = lineAtOffset(lineOffsets, match.index);
972
+ vars.set(name, { value, line });
973
+ }
974
+ return vars;
975
+ }
976
+ function extractAllVariables(css) {
977
+ const cleaned = stripComments(css);
978
+ const vars = new Map;
979
+ const regex = /(--[a-zA-Z0-9-]+)\s*:\s*([^;]*);/g;
980
+ let match;
981
+ while ((match = regex.exec(cleaned)) !== null) {
982
+ const name = capture(match, 1);
983
+ const value = (match[2] ?? "").trim();
984
+ vars.set(name, value);
985
+ }
986
+ return vars;
987
+ }
988
+ function parseVarFunc(value) {
989
+ const prefix = /^var\(\s*/.exec(value);
990
+ if (!prefix)
991
+ return null;
992
+ let pos = prefix[0].length;
993
+ const nameMatch = /^(--[a-zA-Z0-9-]+)/.exec(value.slice(pos));
994
+ if (!nameMatch)
995
+ return null;
996
+ const refName = capture(nameMatch, 1);
997
+ pos += refName.length;
998
+ while (pos < value.length && /\s/.test(value[pos]))
999
+ pos++;
1000
+ if (value[pos] === ")")
1001
+ return { refName, fallback: null };
1002
+ if (value[pos] !== ",")
1003
+ return { refName, fallback: null };
1004
+ pos++;
1005
+ while (pos < value.length && /\s/.test(value[pos]))
1006
+ pos++;
1007
+ let depth = 1;
1008
+ const fallbackStart = pos;
1009
+ while (pos < value.length && depth > 0) {
1010
+ if (value[pos] === "(")
1011
+ depth++;
1012
+ else if (value[pos] === ")")
1013
+ depth--;
1014
+ if (depth > 0)
1015
+ pos++;
1016
+ }
1017
+ const fallback = value.slice(fallbackStart, pos).trim();
1018
+ return { refName, fallback: fallback || null };
1019
+ }
1020
+ function resolveVarReferences(dryVars, allVars) {
1021
+ const resolved = new Map;
1022
+ for (const [name, entry] of dryVars) {
1023
+ const parsed = parseVarFunc(entry.value);
1024
+ if (!parsed) {
1025
+ resolved.set(name, { original: entry.value, resolved: entry.value, line: entry.line });
1026
+ continue;
1027
+ }
1028
+ const { refName, fallback } = parsed;
1029
+ const refValue = allVars.get(refName);
1030
+ if (refValue !== undefined) {
1031
+ resolved.set(name, { original: entry.value, resolved: refValue, line: entry.line });
1032
+ } else if (fallback !== null) {
1033
+ resolved.set(name, { original: entry.value, resolved: fallback, line: entry.line });
1034
+ } else {
1035
+ resolved.set(name, { original: entry.value, resolved: entry.value, line: entry.line });
1036
+ }
1037
+ }
1038
+ return resolved;
1039
+ }
1040
+ function classifyValue(value) {
1041
+ const v = value.trim();
1042
+ if (!v)
1043
+ return "other";
1044
+ if (/^#([0-9a-fA-F]{3,8})$/.test(v))
1045
+ return "color";
1046
+ if (/^rgba?\s*\(/.test(v))
1047
+ return "color";
1048
+ if (/^hsla?\s*\(/.test(v))
1049
+ return "color";
1050
+ if (/^color-mix\s*\(/.test(v))
1051
+ return "color";
1052
+ if (NAMED_COLORS.has(v.toLowerCase()))
1053
+ return "color";
1054
+ if (/^(?:oklch|lch|lab|oklab|color)\s*\(/.test(v))
1055
+ return "color";
1056
+ if (/^-?[\d.]+(?:px|rem|em|%|vw|vh)$/.test(v))
1057
+ return "length";
1058
+ if (/^-?[\d.]+(?:ms|s)$/.test(v))
1059
+ return "time";
1060
+ if (/^-?[\d.]+(?:px|rem|em)\s+-?[\d.]+(?:px|rem|em)/.test(v))
1061
+ return "shadow";
1062
+ if (/^["']/.test(v))
1063
+ return "font";
1064
+ if (/,\s*["']?[a-zA-Z]/.test(v) && !/^(rgb|hsl)/.test(v))
1065
+ return "font";
1066
+ return "other";
1067
+ }
1068
+ function parseColor(value) {
1069
+ const v = value.trim();
1070
+ const hexMatch = v.match(/^#([0-9a-fA-F]{3,8})$/);
1071
+ if (hexMatch) {
1072
+ const hex = capture(hexMatch, 1);
1073
+ if (hex.length === 3) {
1074
+ return {
1075
+ r: parseInt(hex.charAt(0) + hex.charAt(0), 16),
1076
+ g: parseInt(hex.charAt(1) + hex.charAt(1), 16),
1077
+ b: parseInt(hex.charAt(2) + hex.charAt(2), 16)
1078
+ };
1079
+ }
1080
+ if (hex.length === 6 || hex.length === 8) {
1081
+ return {
1082
+ r: parseInt(hex.slice(0, 2), 16),
1083
+ g: parseInt(hex.slice(2, 4), 16),
1084
+ b: parseInt(hex.slice(4, 6), 16)
1085
+ };
1086
+ }
1087
+ return null;
1088
+ }
1089
+ const rgbMatch = v.match(/^rgba?\(\s*(\d+)\s*[,/]\s*(\d+)\s*[,/]\s*(\d+)/);
1090
+ if (rgbMatch) {
1091
+ return {
1092
+ r: parseInt(capture(rgbMatch, 1), 10),
1093
+ g: parseInt(capture(rgbMatch, 2), 10),
1094
+ b: parseInt(capture(rgbMatch, 3), 10)
1095
+ };
1096
+ }
1097
+ const hslMatch = v.match(/^hsla?\(\s*(\d+)\s*[,/]\s*([\d.]+)%?\s*[,/]\s*([\d.]+)%?/);
1098
+ if (hslMatch) {
1099
+ const h = parseInt(capture(hslMatch, 1), 10);
1100
+ const s = parseFloat(capture(hslMatch, 2)) / 100;
1101
+ const l = parseFloat(capture(hslMatch, 3)) / 100;
1102
+ return hslToRgb(h, s, l);
1103
+ }
1104
+ const modernRgbMatch = v.match(/^rgba?\(\s*(\d+)\s+(\d+)\s+(\d+)/);
1105
+ if (modernRgbMatch) {
1106
+ return {
1107
+ r: parseInt(capture(modernRgbMatch, 1), 10),
1108
+ g: parseInt(capture(modernRgbMatch, 2), 10),
1109
+ b: parseInt(capture(modernRgbMatch, 3), 10)
1110
+ };
1111
+ }
1112
+ const modernHslMatch = v.match(/^hsla?\(\s*(\d+)\s+([\d.]+)%?\s+([\d.]+)%?/);
1113
+ if (modernHslMatch) {
1114
+ const h = parseInt(capture(modernHslMatch, 1), 10);
1115
+ const s = parseFloat(capture(modernHslMatch, 2)) / 100;
1116
+ const l = parseFloat(capture(modernHslMatch, 3)) / 100;
1117
+ return hslToRgb(h, s, l);
1118
+ }
1119
+ return null;
1120
+ }
1121
+ function hslToRgb(h, s, l) {
1122
+ const hue = (h % 360 + 360) % 360;
1123
+ if (s === 0) {
1124
+ const val = Math.round(l * 255);
1125
+ return { r: val, g: val, b: val };
1126
+ }
1127
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1128
+ const p = 2 * l - q;
1129
+ function hueToChannel(t) {
1130
+ let tc = t;
1131
+ if (tc < 0)
1132
+ tc += 1;
1133
+ if (tc > 1)
1134
+ tc -= 1;
1135
+ if (tc < 1 / 6)
1136
+ return p + (q - p) * 6 * tc;
1137
+ if (tc < 1 / 2)
1138
+ return q;
1139
+ if (tc < 2 / 3)
1140
+ return p + (q - p) * (2 / 3 - tc) * 6;
1141
+ return p;
1142
+ }
1143
+ return {
1144
+ r: Math.round(hueToChannel(hue / 360 + 1 / 3) * 255),
1145
+ g: Math.round(hueToChannel(hue / 360) * 255),
1146
+ b: Math.round(hueToChannel(hue / 360 - 1 / 3) * 255)
1147
+ };
1148
+ }
1149
+ function brightness(r, g, b) {
1150
+ return (r * 299 + g * 587 + b * 114) / 1000;
1151
+ }
1152
+ function extractAlpha(value) {
1153
+ const v = value.trim();
1154
+ const modernRgbMatch = v.match(/^rgba?\(\s*[\d.]+\s+[\d.]+\s+[\d.]+\s*\/\s*([\d.]+%?)\s*\)/);
1155
+ if (modernRgbMatch) {
1156
+ const val = capture(modernRgbMatch, 1);
1157
+ return val.endsWith("%") ? parseFloat(val) / 100 : parseFloat(val);
1158
+ }
1159
+ const modernHslMatch = v.match(/^hsla?\(\s*[\d.]+\s+[\d.]+%?\s+[\d.]+%?\s*\/\s*([\d.]+%?)\s*\)/);
1160
+ if (modernHslMatch) {
1161
+ const val = capture(modernHslMatch, 1);
1162
+ return val.endsWith("%") ? parseFloat(val) / 100 : parseFloat(val);
1163
+ }
1164
+ const rgbaMatch = v.match(/^rgba\(\s*\d+\s*[,/]\s*\d+\s*[,/]\s*\d+\s*[,/]\s*([\d.]+)\s*\)/);
1165
+ if (rgbaMatch) {
1166
+ return parseFloat(capture(rgbaMatch, 1));
1167
+ }
1168
+ const hslaMatch = v.match(/^hsla\(\s*\d+\s*[,/]\s*[\d.]+%?\s*[,/]\s*[\d.]+%?\s*[,/]\s*([\d.]+)\s*\)/);
1169
+ if (hslaMatch) {
1170
+ return parseFloat(capture(hslaMatch, 1));
1171
+ }
1172
+ const hex8Match = v.match(/^#[0-9a-fA-F]{8}$/);
1173
+ if (hex8Match) {
1174
+ const alphaHex = v.slice(7, 9);
1175
+ return parseInt(alphaHex, 16) / 255;
1176
+ }
1177
+ return 1;
1178
+ }
1179
+ function checkMissingTokens(vars) {
1180
+ const semanticCount = REQUIRED_TOKENS.filter((t) => vars.has(t)).length;
1181
+ if (semanticCount < FULL_THEME_THRESHOLD)
1182
+ return [];
1183
+ const issues = [];
1184
+ for (const token of REQUIRED_TOKENS) {
1185
+ if (!vars.has(token)) {
1186
+ issues.push({
1187
+ severity: "error",
1188
+ code: "missing-token",
1189
+ variable: token,
1190
+ message: `Required semantic token ${token} is not defined`,
1191
+ fix: `Add ${token} with a color that fits your theme`
1192
+ });
1193
+ }
1194
+ }
1195
+ return issues;
1196
+ }
1197
+ function checkValueTypes(vars) {
1198
+ const issues = [];
1199
+ for (const [name, entry] of vars) {
1200
+ const val = entry.resolved;
1201
+ if (/^var\(\s*--/.test(val))
1202
+ continue;
1203
+ const classified = classifyValue(val);
1204
+ if (/^--dry-color-/.test(name) && classified !== "color" && classified !== "other") {
1205
+ issues.push({
1206
+ severity: "error",
1207
+ code: "wrong-type",
1208
+ variable: name,
1209
+ value: entry.original,
1210
+ message: `${name} expects a color value but got "${val}" (classified as ${classified})`,
1211
+ fix: `Replace with a color value (e.g., #2563eb, rgb(37,99,235), hsl(217,91%,60%))`
1212
+ });
1213
+ }
1214
+ if (/^--dry-space-/.test(name) && classified !== "length" && classified !== "other") {
1215
+ issues.push({
1216
+ severity: "error",
1217
+ code: "wrong-type",
1218
+ variable: name,
1219
+ value: entry.original,
1220
+ message: `${name} expects a length value but got "${val}" (classified as ${classified})`,
1221
+ fix: `Replace with a length value (e.g., 0.5rem, 4px, 1em)`
1222
+ });
1223
+ }
1224
+ if (/^--dry-radius-/.test(name) && classified !== "length" && classified !== "other") {
1225
+ issues.push({
1226
+ severity: "error",
1227
+ code: "wrong-type",
1228
+ variable: name,
1229
+ value: entry.original,
1230
+ message: `${name} expects a length value but got "${val}" (classified as ${classified})`,
1231
+ fix: `Replace with a length value (e.g., 4px, 0.375rem)`
1232
+ });
1233
+ }
1234
+ if (/^--dry-duration-/.test(name) && classified !== "time" && classified !== "other") {
1235
+ issues.push({
1236
+ severity: "error",
1237
+ code: "wrong-type",
1238
+ variable: name,
1239
+ value: entry.original,
1240
+ message: `${name} expects a time value but got "${val}" (classified as ${classified})`,
1241
+ fix: `Replace with a time value (e.g., 100ms, 0.2s)`
1242
+ });
1243
+ }
1244
+ }
1245
+ return issues;
1246
+ }
1247
+ function checkContrastHeuristics(vars) {
1248
+ const issues = [];
1249
+ for (const token of SURFACE_TOKENS) {
1250
+ const entry = vars.get(token);
1251
+ if (!entry)
1252
+ continue;
1253
+ const alpha = extractAlpha(entry.resolved);
1254
+ if (alpha < 0.3) {
1255
+ issues.push({
1256
+ severity: "warning",
1257
+ code: "transparent-surface",
1258
+ variable: token,
1259
+ value: entry.original,
1260
+ message: `Surface color has very low opacity (${alpha}) — cards and elevated elements will be nearly invisible`,
1261
+ fix: `Use a solid color (e.g., #1e293b for dark themes, #f8fafc for light themes)`
1262
+ });
1263
+ }
1264
+ }
1265
+ const textStrongEntry = vars.get("--dry-color-text-strong");
1266
+ const textWeakEntry = vars.get("--dry-color-text-weak");
1267
+ const textBrandEntry = vars.get("--dry-color-text-brand");
1268
+ const bgBaseEntry = vars.get("--dry-color-bg-base");
1269
+ if (bgBaseEntry) {
1270
+ const bgRgb = parseColor(bgBaseEntry.resolved);
1271
+ if (bgRgb) {
1272
+ const bgBrightness = brightness(bgRgb.r, bgRgb.g, bgRgb.b);
1273
+ const textPairs = [
1274
+ ["--dry-color-text-strong", textStrongEntry],
1275
+ ["--dry-color-text-weak", textWeakEntry],
1276
+ ["--dry-color-text-brand", textBrandEntry]
1277
+ ];
1278
+ for (const [tokenName, textEntry] of textPairs) {
1279
+ if (!textEntry)
1280
+ continue;
1281
+ const textRgb = parseColor(textEntry.resolved);
1282
+ if (!textRgb)
1283
+ continue;
1284
+ const textBrightness = brightness(textRgb.r, textRgb.g, textRgb.b);
1285
+ if (Math.abs(textBrightness - bgBrightness) < 125) {
1286
+ issues.push({
1287
+ severity: "warning",
1288
+ code: "low-contrast-text",
1289
+ variable: tokenName,
1290
+ value: textEntry.original,
1291
+ message: `Low contrast between ${tokenName} and --dry-color-bg-base (brightness difference: ${Math.round(Math.abs(textBrightness - bgBrightness))})`,
1292
+ fix: `Increase brightness difference between text and background to at least 125`
1293
+ });
1294
+ }
1295
+ }
1296
+ }
1297
+ }
1298
+ const bgRaisedEntry = vars.get("--dry-color-bg-raised");
1299
+ const bgOverlayEntry = vars.get("--dry-color-bg-overlay");
1300
+ if (bgBaseEntry && bgRaisedEntry) {
1301
+ const baseRgb = parseColor(bgBaseEntry.resolved);
1302
+ const raisedRgb = parseColor(bgRaisedEntry.resolved);
1303
+ if (baseRgb && raisedRgb) {
1304
+ const baseBr = brightness(baseRgb.r, baseRgb.g, baseRgb.b);
1305
+ const raisedBr = brightness(raisedRgb.r, raisedRgb.g, raisedRgb.b);
1306
+ if (Math.abs(baseBr - raisedBr) < 3) {
1307
+ issues.push({
1308
+ severity: "warning",
1309
+ code: "no-elevation",
1310
+ variable: "--dry-color-bg-raised",
1311
+ value: bgRaisedEntry.original,
1312
+ message: `--dry-color-bg-base and --dry-color-bg-raised have near-identical brightness (difference: ${Math.round(Math.abs(baseBr - raisedBr))}) — raised surfaces won't have visual separation`,
1313
+ fix: `Make --dry-color-bg-raised 1-2 steps lighter/darker than --dry-color-bg-base`
1314
+ });
1315
+ }
1316
+ }
1317
+ }
1318
+ if (bgRaisedEntry && bgOverlayEntry) {
1319
+ const raisedRgb = parseColor(bgRaisedEntry.resolved);
1320
+ const overlayRgb = parseColor(bgOverlayEntry.resolved);
1321
+ if (raisedRgb && overlayRgb) {
1322
+ const raisedBr = brightness(raisedRgb.r, raisedRgb.g, raisedRgb.b);
1323
+ const overlayBr = brightness(overlayRgb.r, overlayRgb.g, overlayRgb.b);
1324
+ if (Math.abs(raisedBr - overlayBr) < 3) {
1325
+ issues.push({
1326
+ severity: "warning",
1327
+ code: "no-elevation",
1328
+ variable: "--dry-color-bg-overlay",
1329
+ value: bgOverlayEntry.original,
1330
+ message: `--dry-color-bg-raised and --dry-color-bg-overlay have near-identical brightness (difference: ${Math.round(Math.abs(raisedBr - overlayBr))}) — overlay surfaces won't have visual separation`,
1331
+ fix: `Make --dry-color-bg-overlay 1-2 steps lighter/darker than --dry-color-bg-raised`
1332
+ });
1333
+ }
1334
+ }
1335
+ }
1336
+ const pairings = [
1337
+ ["--dry-color-fill-brand", "--dry-color-on-brand"],
1338
+ ["--dry-color-fill-error", "--dry-color-on-error"],
1339
+ ["--dry-color-fill-warning", "--dry-color-on-warning"],
1340
+ ["--dry-color-fill-success", "--dry-color-on-success"],
1341
+ ["--dry-color-fill-info", "--dry-color-on-info"]
1342
+ ];
1343
+ for (const [a, b] of pairings) {
1344
+ if (vars.has(a) && !vars.has(b)) {
1345
+ issues.push({
1346
+ severity: "warning",
1347
+ code: "missing-pairing",
1348
+ variable: b,
1349
+ message: `${a} is defined but its pair ${b} is missing`,
1350
+ fix: `Add ${b} to complete the color pairing`
1351
+ });
1352
+ }
1353
+ if (vars.has(b) && !vars.has(a)) {
1354
+ issues.push({
1355
+ severity: "warning",
1356
+ code: "missing-pairing",
1357
+ variable: a,
1358
+ message: `${b} is defined but its pair ${a} is missing`,
1359
+ fix: `Add ${a} to complete the color pairing`
1360
+ });
1361
+ }
1362
+ }
1363
+ return issues;
1364
+ }
1365
+ function checkComponentTokens(vars, spec) {
1366
+ const issues = [];
1367
+ const validComponentVars = new Set;
1368
+ for (const comp of Object.values(spec.components)) {
1369
+ for (const varName of Object.keys(comp.cssVars)) {
1370
+ validComponentVars.add(varName);
1371
+ }
1372
+ }
1373
+ for (const [name, entry] of vars) {
1374
+ if (/^--dry-(?:color|space|radius|duration|shadow|text|font)-/.test(name))
1375
+ continue;
1376
+ if (!validComponentVars.has(name)) {
1377
+ issues.push({
1378
+ severity: "warning",
1379
+ code: "unknown-component-token",
1380
+ variable: name,
1381
+ value: entry.original,
1382
+ message: `${name} is not a recognized component token in the spec`,
1383
+ fix: `Check spelling against the component's cssVars in the spec`
1384
+ });
1385
+ continue;
1386
+ }
1387
+ if (/-bg$/.test(name)) {
1388
+ const lower = entry.resolved.toLowerCase();
1389
+ if (lower === "transparent") {
1390
+ issues.push({
1391
+ severity: "warning",
1392
+ code: "transparent-component-bg",
1393
+ variable: name,
1394
+ value: entry.original,
1395
+ message: `${name} is set to transparent — the component background will be invisible`,
1396
+ fix: `Use a solid color or reference a background token (e.g., var(--dry-color-bg-raised))`
1397
+ });
1398
+ } else {
1399
+ const alpha = extractAlpha(entry.resolved);
1400
+ if (alpha < 0.3) {
1401
+ issues.push({
1402
+ severity: "warning",
1403
+ code: "transparent-component-bg",
1404
+ variable: name,
1405
+ value: entry.original,
1406
+ message: `${name} has very low opacity (${alpha}) — the component background will be nearly invisible`,
1407
+ fix: `Use a solid color or reference a background token (e.g., var(--dry-color-bg-raised))`
1408
+ });
1409
+ }
1410
+ }
1411
+ }
1412
+ }
1413
+ return issues;
1414
+ }
1415
+ function detectDarkScheme(css, allVars) {
1416
+ const issues = [];
1417
+ const signals = [];
1418
+ if (/color-scheme\s*:\s*dark/i.test(css)) {
1419
+ signals.push("color-scheme: dark");
1420
+ }
1421
+ const bgVarNames = ["--bg", "--background", "--bg-color", "--background-color", "--bg-base"];
1422
+ for (const name of bgVarNames) {
1423
+ const value = allVars.get(name);
1424
+ if (value) {
1425
+ const rgb = parseColor(value);
1426
+ if (rgb && brightness(rgb.r, rgb.g, rgb.b) < 50) {
1427
+ signals.push(`${name}: ${value} (dark)`);
1428
+ }
1429
+ }
1430
+ }
1431
+ const bgPropMatch = css.match(/(?:html|body|\*|:root)\s*\{[^}]*background(?:-color)?\s*:\s*([^;]+)/i);
1432
+ if (bgPropMatch) {
1433
+ const val = (bgPropMatch[1] ?? "").trim();
1434
+ const varRef = val.match(/var\(\s*(--[a-zA-Z0-9-]+)/);
1435
+ const resolvedVal = varRef?.[1] ? allVars.get(varRef[1]) : undefined;
1436
+ const colorStr = resolvedVal ?? val;
1437
+ const rgb = parseColor(colorStr);
1438
+ if (rgb && brightness(rgb.r, rgb.g, rgb.b) < 50) {
1439
+ signals.push(`background: ${val} (dark)`);
1440
+ }
1441
+ }
1442
+ if (signals.length > 0) {
1443
+ issues.push({
1444
+ severity: "warning",
1445
+ code: "dark-scheme-no-overrides",
1446
+ variable: "--dry-color-*",
1447
+ message: `Project uses a dark color scheme (${signals.join(", ")}) but has no --dry-color-* overrides. DryUI's default theme is light — components will have poor contrast on dark backgrounds. Either use theme: "dark" in the generate tool, or add --dry-color-* overrides to map DryUI tokens to your dark palette.`,
1448
+ fix: 'Use theme: "dark" in dryui.page(), or override --dry-color-bg-base, --dry-color-bg-raised, --dry-color-text-strong, --dry-color-text-weak, and other semantic tokens with dark-appropriate values'
1449
+ });
1450
+ }
1451
+ return issues;
1452
+ }
1453
+ function diagnoseTheme(css, spec) {
1454
+ const dryVars = extractDryVariables(css);
1455
+ const allVars = extractAllVariables(css);
1456
+ const resolved = resolveVarReferences(dryVars, allVars);
1457
+ const infoIssues = [];
1458
+ for (const [name, entry] of resolved) {
1459
+ if (/^var\(\s*--/.test(entry.resolved) && entry.resolved === entry.original) {
1460
+ const varRefMatch = entry.original.match(/var\(\s*(--[a-zA-Z0-9-]+)/);
1461
+ const refName = varRefMatch ? varRefMatch[1] : "unknown";
1462
+ infoIssues.push({
1463
+ severity: "info",
1464
+ code: "unresolvable-var",
1465
+ variable: name,
1466
+ value: entry.original,
1467
+ message: `${name} references ${refName} which is not defined in this CSS — type and contrast checks skipped`,
1468
+ fix: null
1469
+ });
1470
+ }
1471
+ }
1472
+ const tier1 = checkMissingTokens(resolved);
1473
+ const tier2 = checkValueTypes(resolved);
1474
+ const tier3 = checkContrastHeuristics(resolved);
1475
+ const tier4 = checkComponentTokens(resolved, spec);
1476
+ const darkSchemeIssues = dryVars.size === 0 ? detectDarkScheme(css, allVars) : [];
1477
+ const allIssues = [...tier1, ...tier2, ...tier3, ...tier4, ...darkSchemeIssues, ...infoIssues];
1478
+ const severityOrder = { error: 0, warning: 1, info: 2 };
1479
+ allIssues.sort((a, b) => (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2));
1480
+ const errors = allIssues.filter((i) => i.severity === "error").length;
1481
+ const warnings = allIssues.filter((i) => i.severity === "warning").length;
1482
+ const infos = allIssues.filter((i) => i.severity === "info").length;
1483
+ const summary = allIssues.length === 0 ? "No issues found" : `${errors} error${errors !== 1 ? "s" : ""}, ${warnings} warning${warnings !== 1 ? "s" : ""}, ${infos} info`;
1484
+ const found = dryVars.size;
1485
+ const requiredSet = new Set(REQUIRED_TOKENS);
1486
+ const requiredFound = [...dryVars.keys()].filter((k) => requiredSet.has(k)).length;
1487
+ const extra = found - requiredFound;
1488
+ return {
1489
+ variables: { found, required: requiredFound, extra },
1490
+ issues: allIssues,
1491
+ summary
1492
+ };
1493
+ }
1494
+
1495
+ // src/project-planner.ts
1496
+ import { existsSync, readFileSync, statSync } from "node:fs";
1497
+ import { dirname, resolve } from "node:path";
1498
+ var DIR_OVERRIDES = {
1499
+ QRCode: "qr-code"
1500
+ };
1501
+ function componentDir(name) {
1502
+ return DIR_OVERRIDES[name] ?? name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1503
+ }
1504
+ function resolveStart(inputPath) {
1505
+ const candidate = resolve(inputPath ?? process.cwd());
1506
+ return existsSync(candidate) && statSync(candidate).isFile() ? dirname(candidate) : candidate;
1507
+ }
1508
+ function findUp(start, fileName) {
1509
+ let current = start;
1510
+ while (true) {
1511
+ const candidate = resolve(current, fileName);
1512
+ if (existsSync(candidate))
1513
+ return candidate;
1514
+ const parent = dirname(current);
1515
+ if (parent === current)
1516
+ return null;
1517
+ current = parent;
1518
+ }
1519
+ }
1520
+ function findUpAny(start, fileNames) {
1521
+ let current = start;
1522
+ while (true) {
1523
+ for (const fileName of fileNames) {
1524
+ const candidate = resolve(current, fileName);
1525
+ if (existsSync(candidate))
1526
+ return candidate;
1527
+ }
1528
+ const parent = dirname(current);
1529
+ if (parent === current)
1530
+ return null;
1531
+ current = parent;
1532
+ }
1533
+ }
1534
+ function detectPackageManager(root) {
1535
+ if (!root)
1536
+ return "unknown";
1537
+ const lockfilePath = findUpAny(root, [
1538
+ "bun.lock",
1539
+ "bun.lockb",
1540
+ "pnpm-lock.yaml",
1541
+ "package-lock.json",
1542
+ "yarn.lock"
1543
+ ]);
1544
+ if (!lockfilePath)
1545
+ return "unknown";
1546
+ if (lockfilePath.endsWith("bun.lock") || lockfilePath.endsWith("bun.lockb"))
1547
+ return "bun";
1548
+ if (lockfilePath.endsWith("pnpm-lock.yaml"))
1549
+ return "pnpm";
1550
+ if (lockfilePath.endsWith("package-lock.json"))
1551
+ return "npm";
1552
+ if (lockfilePath.endsWith("yarn.lock"))
1553
+ return "yarn";
1554
+ return "unknown";
1555
+ }
1556
+ function readPackageJson(packageJsonPath) {
1557
+ if (!packageJsonPath)
1558
+ return null;
1559
+ return JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1560
+ }
1561
+ function getDependencyNames(pkg) {
1562
+ return new Set([
1563
+ ...Object.keys(pkg?.dependencies ?? {}),
1564
+ ...Object.keys(pkg?.devDependencies ?? {})
1565
+ ]);
1566
+ }
1567
+ function detectFramework(dependencyNames) {
1568
+ if (dependencyNames.has("@sveltejs/kit"))
1569
+ return "sveltekit";
1570
+ if (dependencyNames.has("svelte"))
1571
+ return "svelte";
1572
+ return "unknown";
1573
+ }
1574
+ function hasImport(filePath, importPath) {
1575
+ if (!filePath)
1576
+ return false;
1577
+ return readFileSync(filePath, "utf-8").includes(importPath);
1578
+ }
1579
+ function hasThemeAuto(appHtmlPath) {
1580
+ if (!appHtmlPath)
1581
+ return false;
1582
+ return readFileSync(appHtmlPath, "utf-8").includes("theme-auto");
1583
+ }
1584
+ function hasAnyImport(filePaths, importPath) {
1585
+ return filePaths.some((filePath) => hasImport(filePath, importPath));
1586
+ }
1587
+ function importsAppCss(rootLayoutPath) {
1588
+ if (!rootLayoutPath)
1589
+ return false;
1590
+ const content = readFileSync(rootLayoutPath, "utf-8");
1591
+ return content.includes("app.css");
1592
+ }
1593
+ function buildStatus(framework, hasPackageJson, stepsNeeded) {
1594
+ if (!hasPackageJson || framework === "unknown")
1595
+ return "unsupported";
1596
+ return stepsNeeded === 0 ? "ready" : "partial";
1597
+ }
1598
+ function installCommand(packageManager) {
1599
+ switch (packageManager) {
1600
+ case "bun":
1601
+ return "bun add @dryui/ui";
1602
+ case "pnpm":
1603
+ return "pnpm add @dryui/ui";
1604
+ case "yarn":
1605
+ return "yarn add @dryui/ui";
1606
+ default:
1607
+ return "npm install @dryui/ui";
1608
+ }
1609
+ }
1610
+ function buildThemeImportSnippet(spec) {
1611
+ return [
1612
+ '<script lang="ts">',
1613
+ ` import '${spec.themeImports.default}';`,
1614
+ ` import '${spec.themeImports.dark}';`,
1615
+ "</script>"
1616
+ ].join(`
1617
+ `);
1618
+ }
1619
+ function buildThemeImportCssSnippet(spec) {
1620
+ return [`@import '${spec.themeImports.default}';`, `@import '${spec.themeImports.dark}';`].join(`
1621
+ `);
1622
+ }
1623
+ function getSuggestedTarget(root, explicitTarget) {
1624
+ if (explicitTarget)
1625
+ return resolve(root ?? process.cwd(), explicitTarget);
1626
+ if (!root)
1627
+ return null;
1628
+ const rootPage = resolve(root, "src/routes/+page.svelte");
1629
+ return existsSync(rootPage) ? rootPage : null;
1630
+ }
1631
+ function getImportStatement(name, component, subpath = false) {
1632
+ if (subpath && component.import === "@dryui/ui") {
1633
+ return `import { ${name} } from '${component.import}/${componentDir(name)}';`;
1634
+ }
1635
+ return `import { ${name} } from '${component.import}';`;
1636
+ }
1637
+ function findComponent(spec, query) {
1638
+ const exact = spec.components[query];
1639
+ if (exact)
1640
+ return { name: query, def: exact };
1641
+ const lower = query.toLowerCase();
1642
+ for (const [name, def] of Object.entries(spec.components)) {
1643
+ if (name.toLowerCase() === lower)
1644
+ return { name, def };
1645
+ }
1646
+ return null;
1647
+ }
1648
+ function detectProject(spec, inputPath) {
1649
+ const start = resolveStart(inputPath);
1650
+ const packageJsonPath = findUp(start, "package.json");
1651
+ const root = packageJsonPath ? dirname(packageJsonPath) : null;
1652
+ const dependencyNames = getDependencyNames(readPackageJson(packageJsonPath));
1653
+ const framework = detectFramework(dependencyNames);
1654
+ const appHtmlPath = root ? resolve(root, "src/app.html") : null;
1655
+ const appCssPath = root ? resolve(root, "src/app.css") : null;
1656
+ const rootLayoutPath = root ? resolve(root, "src/routes/+layout.svelte") : null;
1657
+ const rootPagePath = root ? resolve(root, "src/routes/+page.svelte") : null;
1658
+ const appHtml = appHtmlPath && existsSync(appHtmlPath) ? appHtmlPath : null;
1659
+ const appCss = appCssPath && existsSync(appCssPath) ? appCssPath : null;
1660
+ const rootLayout = rootLayoutPath && existsSync(rootLayoutPath) ? rootLayoutPath : null;
1661
+ const rootPage = rootPagePath && existsSync(rootPagePath) ? rootPagePath : null;
1662
+ const themeImportFiles = rootLayout && importsAppCss(rootLayout) ? [rootLayout, appCss] : [rootLayout];
1663
+ const defaultImported = hasAnyImport(themeImportFiles, spec.themeImports.default);
1664
+ const darkImported = hasAnyImport(themeImportFiles, spec.themeImports.dark);
1665
+ const stepsNeeded = Number(!dependencyNames.has("@dryui/ui")) + Number(!defaultImported) + Number(!darkImported) + Number(!(appHtml && hasThemeAuto(appHtml)));
1666
+ const warnings = [];
1667
+ if (!packageJsonPath)
1668
+ warnings.push("No package.json found above the provided path.");
1669
+ if (framework === "unknown")
1670
+ warnings.push("DryUI planning currently targets Svelte and SvelteKit projects.");
1671
+ return {
1672
+ inputPath: start,
1673
+ root,
1674
+ packageJsonPath,
1675
+ framework,
1676
+ packageManager: detectPackageManager(root),
1677
+ status: buildStatus(framework, Boolean(packageJsonPath), stepsNeeded),
1678
+ dependencies: {
1679
+ ui: dependencyNames.has("@dryui/ui"),
1680
+ primitives: dependencyNames.has("@dryui/primitives")
1681
+ },
1682
+ files: {
1683
+ appHtml,
1684
+ appCss,
1685
+ rootLayout,
1686
+ rootPage
1687
+ },
1688
+ theme: {
1689
+ defaultImported,
1690
+ darkImported,
1691
+ themeAuto: hasThemeAuto(appHtml)
1692
+ },
1693
+ warnings
1694
+ };
1695
+ }
1696
+ function planInstall(spec, inputPath) {
1697
+ const detection = detectProject(spec, inputPath);
1698
+ const steps = [];
1699
+ if (detection.status === "unsupported") {
1700
+ steps.push({
1701
+ kind: "blocked",
1702
+ status: "blocked",
1703
+ title: "Project detection incomplete",
1704
+ description: detection.warnings.join(" ") || "DryUI install planning requires a Svelte or SvelteKit project with a package.json."
1705
+ });
1706
+ return { detection, steps };
1707
+ }
1708
+ if (!detection.dependencies.ui) {
1709
+ steps.push({
1710
+ kind: "install-package",
1711
+ status: "pending",
1712
+ title: "Install @dryui/ui",
1713
+ description: "Add the styled DryUI package to the current project.",
1714
+ command: installCommand(detection.packageManager)
1715
+ });
1716
+ }
1717
+ if (!detection.theme.defaultImported || !detection.theme.darkImported) {
1718
+ if (detection.files.appCss && detection.files.rootLayout && importsAppCss(detection.files.rootLayout)) {
1719
+ steps.push({
1720
+ kind: "edit-file",
1721
+ status: "pending",
1722
+ title: "Add theme imports to app.css",
1723
+ description: "Ensure the app-level stylesheet imports both default and dark DryUI themes.",
1724
+ path: detection.files.appCss,
1725
+ snippet: buildThemeImportCssSnippet(spec)
1726
+ });
1727
+ } else if (!detection.files.rootLayout) {
1728
+ const path = detection.root ? resolve(detection.root, "src/routes/+layout.svelte") : null;
1729
+ steps.push({
1730
+ kind: "create-file",
1731
+ status: "pending",
1732
+ title: "Create root layout with theme imports",
1733
+ description: "Create src/routes/+layout.svelte and add the required DryUI theme imports.",
1734
+ ...path ? { path } : {},
1735
+ snippet: buildThemeImportSnippet(spec)
1736
+ });
1737
+ } else {
1738
+ steps.push({
1739
+ kind: "edit-file",
1740
+ status: "pending",
1741
+ title: "Add theme imports to the root layout",
1742
+ description: "Ensure the root layout imports both default and dark DryUI themes.",
1743
+ path: detection.files.rootLayout,
1744
+ snippet: buildThemeImportSnippet(spec)
1745
+ });
1746
+ }
1747
+ }
1748
+ if (!detection.files.appHtml) {
1749
+ steps.push({
1750
+ kind: "blocked",
1751
+ status: "blocked",
1752
+ title: "app.html not found",
1753
+ description: "DryUI expects src/app.html so the document can default to theme-auto mode."
1754
+ });
1755
+ } else if (!detection.theme.themeAuto) {
1756
+ steps.push({
1757
+ kind: "edit-file",
1758
+ status: "pending",
1759
+ title: "Set html theme mode to auto",
1760
+ description: 'Add class="theme-auto" to the html element in src/app.html.',
1761
+ path: detection.files.appHtml,
1762
+ snippet: '<html class="theme-auto">'
1763
+ });
1764
+ }
1765
+ if (steps.length === 0) {
1766
+ steps.push({
1767
+ kind: "note",
1768
+ status: "done",
1769
+ title: "DryUI install plan is complete",
1770
+ description: "The project already has @dryui/ui, theme imports, and theme-auto configured."
1771
+ });
1772
+ }
1773
+ return { detection, steps };
1774
+ }
1775
+ function planAdd(spec, query, options = {}) {
1776
+ const installPlan = planInstall(spec, options.cwd);
1777
+ const component = findComponent(spec, query);
1778
+ if (component) {
1779
+ const target = getSuggestedTarget(installPlan.detection.root, options.target);
1780
+ const steps = [];
1781
+ const warnings = [...installPlan.detection.warnings];
1782
+ if (installPlan.steps.some((step) => step.status === "pending" || step.status === "blocked")) {
1783
+ steps.push({
1784
+ kind: "note",
1785
+ status: "info",
1786
+ title: "Complete install plan first",
1787
+ description: "Apply the install plan before inserting DryUI components into project files."
1788
+ });
1789
+ }
1790
+ steps.push(target ? {
1791
+ kind: "edit-file",
1792
+ status: "pending",
1793
+ title: "Insert component into the target file",
1794
+ description: "Add the import and snippet below to the chosen Svelte file.",
1795
+ path: target,
1796
+ snippet: `${getImportStatement(component.name, component.def, options.subpath)}
1797
+
1798
+ ${component.def.example}`
1799
+ } : {
1800
+ kind: "note",
1801
+ status: "info",
1802
+ title: "Choose a target Svelte file",
1803
+ description: "No root page was found. Pick a target file and reuse the import and snippet in this plan."
1804
+ });
1805
+ return {
1806
+ detection: installPlan.detection,
1807
+ installPlan,
1808
+ targetType: "component",
1809
+ name: component.name,
1810
+ importStatement: getImportStatement(component.name, component.def, options.subpath),
1811
+ snippet: component.def.example,
1812
+ target,
1813
+ steps,
1814
+ warnings
1815
+ };
1816
+ }
1817
+ throw new Error(`Unknown component: "${query}"`);
1818
+ }
1819
+
1820
+ // src/workspace-audit.ts
1821
+ import { execFileSync } from "node:child_process";
1822
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, realpathSync, statSync as statSync2 } from "node:fs";
1823
+ import { dirname as dirname2, relative, resolve as resolve2 } from "node:path";
1824
+ var DEFAULT_INCLUDE = [
1825
+ "packages/ui",
1826
+ "packages/primitives",
1827
+ "apps/docs",
1828
+ "apps/playground"
1829
+ ];
1830
+ var DEFAULT_EXCLUDED_SEGMENTS = new Set([
1831
+ ".git",
1832
+ ".svelte-kit",
1833
+ "__fixtures__",
1834
+ "build",
1835
+ "coverage",
1836
+ "dist",
1837
+ "fixtures",
1838
+ "node_modules",
1839
+ "reports",
1840
+ "test",
1841
+ "tests"
1842
+ ]);
1843
+ var DEFAULT_EXCLUDED_PATHS = new Set([
1844
+ "apps/studio",
1845
+ "packages/canvas",
1846
+ "packages/hand-tracking",
1847
+ "packages/studio-server"
1848
+ ]);
1849
+ var MAX_SEVERITY = { info: 0, warning: 1, error: 2 };
1850
+ function normalizePath(path) {
1851
+ return path.replaceAll("\\", "/").replace(/^\.\/+/, "");
1852
+ }
1853
+ function resolveRoot(inputPath) {
1854
+ const candidate = resolve2(inputPath ?? process.cwd());
1855
+ if (existsSync2(candidate) && statSync2(candidate).isFile()) {
1856
+ return dirname2(candidate);
1857
+ }
1858
+ return candidate;
1859
+ }
1860
+ function hasGlob(pattern) {
1861
+ return /[*?[{]/.test(pattern);
1862
+ }
1863
+ function matchesPattern(path, pattern) {
1864
+ const normalizedPath = normalizePath(path);
1865
+ const normalizedPattern = normalizePath(pattern).replace(/\/+$/, "");
1866
+ if (!normalizedPattern || normalizedPattern === ".")
1867
+ return true;
1868
+ if (!hasGlob(normalizedPattern)) {
1869
+ return normalizedPath === normalizedPattern || normalizedPath.startsWith(`${normalizedPattern}/`);
1870
+ }
1871
+ const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "(?:.*/)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
1872
+ return new RegExp(`^${escaped}$`).test(normalizedPath);
1873
+ }
1874
+ function collectFiles(root, current = root) {
1875
+ const files = [];
1876
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
1877
+ const absPath = resolve2(current, entry.name);
1878
+ if (entry.isDirectory()) {
1879
+ if (entry.name === "node_modules" || entry.name === ".git")
1880
+ continue;
1881
+ files.push(...collectFiles(root, absPath));
1882
+ continue;
1883
+ }
1884
+ if (!entry.isFile())
1885
+ continue;
1886
+ files.push(normalizePath(relative(root, absPath)));
1887
+ }
1888
+ return files;
1889
+ }
1890
+ function collectChangedFiles(root) {
1891
+ try {
1892
+ execFileSync("git", ["-C", root, "rev-parse", "--verify", "HEAD"], { stdio: "ignore" });
1893
+ const canonicalRoot = realpathSync.native(root);
1894
+ const repoRoot = realpathSync.native(execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim());
1895
+ const files = new Set;
1896
+ const outputs = [
1897
+ execFileSync("git", ["-C", repoRoot, "diff", "--name-only", "HEAD", "--"], {
1898
+ encoding: "utf8"
1899
+ }),
1900
+ execFileSync("git", ["-C", repoRoot, "diff", "--name-only", "--cached", "HEAD", "--"], {
1901
+ encoding: "utf8"
1902
+ }),
1903
+ execFileSync("git", ["-C", repoRoot, "ls-files", "--others", "--exclude-standard"], {
1904
+ encoding: "utf8"
1905
+ })
1906
+ ];
1907
+ for (const output of outputs) {
1908
+ for (const line of output.split(`
1909
+ `)) {
1910
+ const changedPath = line.trim();
1911
+ if (!changedPath)
1912
+ continue;
1913
+ const absPath = resolve2(repoRoot, changedPath);
1914
+ const relativePath = normalizePath(relative(canonicalRoot, absPath));
1915
+ if (relativePath.startsWith(".."))
1916
+ continue;
1917
+ files.add(relativePath);
1918
+ }
1919
+ }
1920
+ return files;
1921
+ } catch {
1922
+ throw new Error("The --changed option requires a Git repository with an existing HEAD commit.");
1923
+ }
1924
+ }
1925
+ function defaultInclude(root) {
1926
+ const included = DEFAULT_INCLUDE.filter((entry) => existsSync2(resolve2(root, entry)));
1927
+ return included.length > 0 ? included : ["."];
1928
+ }
1929
+ function isDefaultExcluded(path) {
1930
+ const normalized = normalizePath(path);
1931
+ if (normalized.endsWith(".d.ts") || normalized.endsWith(".svelte.d.ts"))
1932
+ return true;
1933
+ if (DEFAULT_EXCLUDED_PATHS.has(normalized))
1934
+ return true;
1935
+ const segments = normalized.split("/");
1936
+ return segments.some((segment) => DEFAULT_EXCLUDED_SEGMENTS.has(segment));
1937
+ }
1938
+ function shouldScanFile(path, include, exclude, changedFiles, explicitInclude) {
1939
+ const normalized = normalizePath(path);
1940
+ const included = include.length > 0 ? include.some((pattern) => matchesPattern(normalized, pattern)) : DEFAULT_INCLUDE.some((pattern) => matchesPattern(normalized, pattern));
1941
+ if (!included)
1942
+ return false;
1943
+ if (!explicitInclude && isDefaultExcluded(normalized))
1944
+ return false;
1945
+ if (exclude.some((pattern) => matchesPattern(normalized, pattern)))
1946
+ return false;
1947
+ if (changedFiles && !matchesChangedFile(normalized, changedFiles))
1948
+ return false;
1949
+ return true;
1950
+ }
1951
+ function matchesChangedFile(path, changedFiles) {
1952
+ const normalized = normalizePath(path);
1953
+ for (const changed of changedFiles) {
1954
+ if (changed === normalized)
1955
+ return true;
1956
+ if (changed.endsWith(`/${normalized}`))
1957
+ return true;
1958
+ if (normalized.endsWith(`/${changed}`))
1959
+ return true;
1960
+ }
1961
+ return false;
1962
+ }
1963
+ function severityAtOrAbove(severity, maxSeverity) {
1964
+ if (!maxSeverity)
1965
+ return true;
1966
+ return MAX_SEVERITY[severity] >= MAX_SEVERITY[maxSeverity];
1967
+ }
1968
+ function compareFindings(left, right) {
1969
+ const severityDelta = MAX_SEVERITY[right.severity] - MAX_SEVERITY[left.severity];
1970
+ if (severityDelta !== 0)
1971
+ return severityDelta;
1972
+ const fileDelta = left.file.localeCompare(right.file);
1973
+ if (fileDelta !== 0)
1974
+ return fileDelta;
1975
+ const lineDelta = (left.line ?? Number.MAX_SAFE_INTEGER) - (right.line ?? Number.MAX_SAFE_INTEGER);
1976
+ if (lineDelta !== 0)
1977
+ return lineDelta;
1978
+ return left.ruleId.localeCompare(right.ruleId);
1979
+ }
1980
+ function toFinding(file, line, ruleId, severity, message, fix) {
1981
+ return {
1982
+ file,
1983
+ line,
1984
+ column: null,
1985
+ ruleId,
1986
+ severity,
1987
+ fixable: fix !== null,
1988
+ message,
1989
+ suggestedFixes: fix ? [{ description: "Apply suggested fix", replacement: fix }] : []
1990
+ };
1991
+ }
1992
+ function projectFindings(root, packageJsonPath, spec) {
1993
+ const result = detectProject(spec, packageJsonPath);
1994
+ const findings = [];
1995
+ const warnings = [...result.warnings];
1996
+ const packageFile = normalizePath(relative(root, packageJsonPath));
1997
+ if (result.framework === "unknown") {
1998
+ return { findings, project: result, warnings };
1999
+ }
2000
+ if (!result.dependencies.ui) {
2001
+ findings.push(toFinding(packageFile, null, "project/missing-ui-dependency", "error", "Install @dryui/ui before using DryUI components.", "bun add @dryui/ui"));
2002
+ }
2003
+ if (!result.theme.defaultImported || !result.theme.darkImported) {
2004
+ const targetPath = result.files.appCss ?? result.files.rootLayout ?? result.files.appHtml ?? packageJsonPath;
2005
+ findings.push(toFinding(normalizePath(relative(root, targetPath)), null, "project/missing-theme-import", "error", "Add both DryUI theme imports to the app shell.", `import '${spec.themeImports.default}';
2006
+ import '${spec.themeImports.dark}';`));
2007
+ }
2008
+ if (!result.theme.themeAuto) {
2009
+ const targetPath = result.files.appHtml ?? packageJsonPath;
2010
+ findings.push(toFinding(normalizePath(relative(root, targetPath)), null, "project/missing-theme-auto", "error", "Set the root html element to theme-auto mode.", '<html class="theme-auto">'));
2011
+ }
2012
+ return { findings, project: result, warnings };
2013
+ }
2014
+ function reviewFindings(root, filePath, content, spec) {
2015
+ const result = reviewComponent(content, spec, normalizePath(relative(root, filePath)));
2016
+ return result.issues.map((issue) => toFinding(normalizePath(relative(root, filePath)), issue.line, `component/${issue.code}`, issue.severity === "suggestion" ? "info" : issue.severity, issue.message, issue.fix));
2017
+ }
2018
+ function themeFindings(root, filePath, content, spec) {
2019
+ const result = diagnoseTheme(content, spec);
2020
+ return result.issues.map((issue) => toFinding(normalizePath(relative(root, filePath)), null, `theme/${issue.code}`, issue.severity, issue.message, issue.fix));
2021
+ }
2022
+ function buildWorkspaceReport(spec, options = {}) {
2023
+ const root = resolveRoot(options.cwd);
2024
+ const explicitInclude = Boolean(options.include?.length);
2025
+ const include = explicitInclude ? [...options.include ?? []] : defaultInclude(root);
2026
+ const exclude = options.exclude ? [...options.exclude] : [];
2027
+ const files = collectFiles(root);
2028
+ const changed = options.changed ?? false;
2029
+ const changedFiles = changed ? collectChangedFiles(root) : null;
2030
+ const findings = [];
2031
+ const projects = [];
2032
+ const warnings = [];
2033
+ if (!options.include?.length && include.every((entry) => DEFAULT_INCLUDE.includes(entry))) {
2034
+ warnings.push("Using the default DryUI workspace scan scope and excluding experimental packages.");
2035
+ }
2036
+ if (changed) {
2037
+ warnings.push("Scanning only modified, staged, and untracked files relative to the current HEAD.");
2038
+ }
2039
+ const scannedFiles = files.filter((file) => shouldScanFile(file, include, exclude, changedFiles, explicitInclude)).length;
2040
+ const skippedFiles = files.length - scannedFiles;
2041
+ for (const file of files) {
2042
+ if (!shouldScanFile(file, include, exclude, changedFiles, explicitInclude))
2043
+ continue;
2044
+ const absPath = resolve2(root, file);
2045
+ const content = readFileSync2(absPath, "utf8");
2046
+ if (file.endsWith("package.json")) {
2047
+ try {
2048
+ const parsed = JSON.parse(content);
2049
+ const dependencyNames = new Set([
2050
+ ...Object.keys(parsed.dependencies ?? {}),
2051
+ ...Object.keys(parsed.devDependencies ?? {})
2052
+ ]);
2053
+ if (dependencyNames.has("svelte") || dependencyNames.has("@sveltejs/kit")) {
2054
+ const project = projectFindings(root, absPath, spec);
2055
+ projects.push(project.project);
2056
+ findings.push(...project.findings);
2057
+ warnings.push(...project.warnings);
2058
+ }
2059
+ } catch {
2060
+ findings.push(toFinding(file, null, "workspace/invalid-package-json", "warning", "Package metadata could not be parsed.", null));
2061
+ }
2062
+ continue;
2063
+ }
2064
+ if (file.endsWith(".svelte") && (content.includes("@dryui/ui") || content.includes("@dryui/primitives"))) {
2065
+ findings.push(...reviewFindings(root, absPath, content, spec));
2066
+ continue;
2067
+ }
2068
+ if (file.endsWith(".css") && content.includes("--dry-")) {
2069
+ findings.push(...themeFindings(root, absPath, content, spec));
2070
+ }
2071
+ }
2072
+ const filteredFindings = findings.filter((finding) => severityAtOrAbove(finding.severity, options.maxSeverity)).sort(compareFindings);
2073
+ const summary = filteredFindings.reduce((acc, finding) => {
2074
+ acc[finding.severity] += 1;
2075
+ acc.byRule[finding.ruleId] = (acc.byRule[finding.ruleId] ?? 0) + 1;
2076
+ return acc;
2077
+ }, { error: 0, warning: 0, info: 0, byRule: {} });
2078
+ return {
2079
+ root,
2080
+ projects,
2081
+ scope: {
2082
+ include,
2083
+ exclude,
2084
+ changed
2085
+ },
2086
+ scannedFiles,
2087
+ skippedFiles,
2088
+ findings: filteredFindings,
2089
+ warnings,
2090
+ summary
2091
+ };
2092
+ }
2093
+ function scanWorkspace(spec, options = {}) {
2094
+ return buildWorkspaceReport(spec, options);
2095
+ }
2096
+ export {
2097
+ scanWorkspace,
2098
+ buildWorkspaceReport
2099
+ };