uilint-core 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-P5RPW6PI.js → chunk-P2QQGDQS.js} +257 -266
- package/dist/chunk-P2QQGDQS.js.map +1 -0
- package/dist/index.d.ts +49 -50
- package/dist/index.js +3 -7
- package/dist/node.d.ts +42 -3
- package/dist/node.js +268 -7
- package/dist/node.js.map +1 -1
- package/package.json +14 -2
- package/dist/chunk-P5RPW6PI.js.map +0 -1
|
@@ -23,6 +23,7 @@ Focus on:
|
|
|
23
23
|
2. Inconsistent font sizes or weights
|
|
24
24
|
3. Spacing values that don't follow a consistent scale (e.g., 4px base unit)
|
|
25
25
|
4. Mixed border-radius values
|
|
26
|
+
5. If utility/Tailwind classes are present in the summary, treat them as the styling surface area and flag inconsistent utility usage (e.g., mixing px-4 and px-5 for the same component type)
|
|
26
27
|
|
|
27
28
|
Be concise and actionable. Only report significant inconsistencies.
|
|
28
29
|
|
|
@@ -50,6 +51,7 @@ Generate a style guide with these sections:
|
|
|
50
51
|
2. Typography - Font families, sizes, and weights
|
|
51
52
|
3. Spacing - Base unit and common spacing values
|
|
52
53
|
4. Components - Common component patterns
|
|
54
|
+
5. Tailwind (if utility classes are present) - list commonly used utilities and any relevant theme tokens
|
|
53
55
|
|
|
54
56
|
Use this format:
|
|
55
57
|
# UI Style Guide
|
|
@@ -91,45 +93,6 @@ ${query}
|
|
|
91
93
|
|
|
92
94
|
Provide a clear, concise answer based on the style guide above. If the style guide doesn't contain the information needed, say so and suggest what might be missing.`;
|
|
93
95
|
}
|
|
94
|
-
function buildValidationPrompt(code, styleGuide) {
|
|
95
|
-
const guideSection = styleGuide ? `## Style Guide
|
|
96
|
-
${styleGuide}
|
|
97
|
-
|
|
98
|
-
` : "## No Style Guide\nValidate for general best practices.\n\n";
|
|
99
|
-
return `You are a UI code validator. Check the following code against the style guide and best practices.
|
|
100
|
-
|
|
101
|
-
${guideSection}
|
|
102
|
-
|
|
103
|
-
## Code to Validate
|
|
104
|
-
\`\`\`tsx
|
|
105
|
-
${code}
|
|
106
|
-
\`\`\`
|
|
107
|
-
|
|
108
|
-
Respond with a JSON object containing:
|
|
109
|
-
- valid: boolean (true if no errors found)
|
|
110
|
-
- issues: array of issues, each with:
|
|
111
|
-
- type: "error" or "warning"
|
|
112
|
-
- message: description of the issue
|
|
113
|
-
- suggestion: how to fix it
|
|
114
|
-
|
|
115
|
-
Focus on:
|
|
116
|
-
1. Colors not in the style guide
|
|
117
|
-
2. Spacing values not following the design system
|
|
118
|
-
3. Typography inconsistencies
|
|
119
|
-
4. Accessibility issues (missing alt text, etc.)
|
|
120
|
-
|
|
121
|
-
Example response:
|
|
122
|
-
{
|
|
123
|
-
"valid": false,
|
|
124
|
-
"issues": [
|
|
125
|
-
{
|
|
126
|
-
"type": "warning",
|
|
127
|
-
"message": "Color #FF0000 is not in the style guide",
|
|
128
|
-
"suggestion": "Use the error color #EF4444 instead"
|
|
129
|
-
}
|
|
130
|
-
]
|
|
131
|
-
}`;
|
|
132
|
-
}
|
|
133
96
|
|
|
134
97
|
// src/ollama/client.ts
|
|
135
98
|
var DEFAULT_BASE_URL = "http://localhost:11434";
|
|
@@ -191,19 +154,6 @@ var OllamaClient = class {
|
|
|
191
154
|
return "Failed to query style guide. Please try again.";
|
|
192
155
|
}
|
|
193
156
|
}
|
|
194
|
-
/**
|
|
195
|
-
* Validates code against the style guide
|
|
196
|
-
*/
|
|
197
|
-
async validateCode(code, styleGuide) {
|
|
198
|
-
const prompt = buildValidationPrompt(code, styleGuide);
|
|
199
|
-
try {
|
|
200
|
-
const response = await this.generate(prompt);
|
|
201
|
-
return this.parseValidationResponse(response);
|
|
202
|
-
} catch (error) {
|
|
203
|
-
console.error("[UILint] Validation failed:", error);
|
|
204
|
-
return { valid: true, issues: [] };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
157
|
/**
|
|
208
158
|
* Core generate method that calls Ollama API
|
|
209
159
|
*/
|
|
@@ -243,21 +193,6 @@ var OllamaClient = class {
|
|
|
243
193
|
return [];
|
|
244
194
|
}
|
|
245
195
|
}
|
|
246
|
-
/**
|
|
247
|
-
* Parses validation result from LLM response
|
|
248
|
-
*/
|
|
249
|
-
parseValidationResponse(response) {
|
|
250
|
-
try {
|
|
251
|
-
const parsed = JSON.parse(response);
|
|
252
|
-
return {
|
|
253
|
-
valid: parsed.valid ?? true,
|
|
254
|
-
issues: parsed.issues || []
|
|
255
|
-
};
|
|
256
|
-
} catch {
|
|
257
|
-
console.warn("[UILint] Failed to parse validation response");
|
|
258
|
-
return { valid: true, issues: [] };
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
196
|
/**
|
|
262
197
|
* Checks if Ollama is available
|
|
263
198
|
*/
|
|
@@ -293,6 +228,65 @@ function getOllamaClient(options) {
|
|
|
293
228
|
return defaultClient;
|
|
294
229
|
}
|
|
295
230
|
|
|
231
|
+
// src/tailwind/class-tokens.ts
|
|
232
|
+
function extractClassTokensFromHtml(html, options = {}) {
|
|
233
|
+
const { maxTokens = 2e4 } = options;
|
|
234
|
+
const utilities = /* @__PURE__ */ new Map();
|
|
235
|
+
const variants = /* @__PURE__ */ new Map();
|
|
236
|
+
if (!html) return { utilities, variants };
|
|
237
|
+
const attrPattern = /\b(?:class|className)\s*=\s*["']([^"']+)["']/g;
|
|
238
|
+
let tokenBudget = maxTokens;
|
|
239
|
+
let match;
|
|
240
|
+
while ((match = attrPattern.exec(html)) && tokenBudget > 0) {
|
|
241
|
+
const raw = match[1];
|
|
242
|
+
if (!raw) continue;
|
|
243
|
+
const tokens = raw.split(/\s+/g).filter(Boolean);
|
|
244
|
+
for (const token of tokens) {
|
|
245
|
+
if (tokenBudget-- <= 0) break;
|
|
246
|
+
const { base, variantList } = splitVariants(token);
|
|
247
|
+
const normalizedBase = normalizeUtility(base);
|
|
248
|
+
if (!normalizedBase) continue;
|
|
249
|
+
increment(utilities, normalizedBase);
|
|
250
|
+
for (const v of variantList) increment(variants, v);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { utilities, variants };
|
|
254
|
+
}
|
|
255
|
+
function topEntries(map, limit) {
|
|
256
|
+
return [...map.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit).map(([token, count]) => ({ token, count }));
|
|
257
|
+
}
|
|
258
|
+
function increment(map, key) {
|
|
259
|
+
map.set(key, (map.get(key) || 0) + 1);
|
|
260
|
+
}
|
|
261
|
+
function normalizeUtility(token) {
|
|
262
|
+
const t = token.trim();
|
|
263
|
+
if (!t) return null;
|
|
264
|
+
const noImportant = t.startsWith("!") ? t.slice(1) : t;
|
|
265
|
+
if (!noImportant || noImportant === "{" || noImportant === "}") return null;
|
|
266
|
+
return noImportant;
|
|
267
|
+
}
|
|
268
|
+
function splitVariants(token) {
|
|
269
|
+
const parts = [];
|
|
270
|
+
let buf = "";
|
|
271
|
+
let bracketDepth = 0;
|
|
272
|
+
for (let i = 0; i < token.length; i++) {
|
|
273
|
+
const ch = token[i];
|
|
274
|
+
if (ch === "[") bracketDepth++;
|
|
275
|
+
if (ch === "]" && bracketDepth > 0) bracketDepth--;
|
|
276
|
+
if (ch === ":" && bracketDepth === 0) {
|
|
277
|
+
parts.push(buf);
|
|
278
|
+
buf = "";
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
buf += ch;
|
|
282
|
+
}
|
|
283
|
+
parts.push(buf);
|
|
284
|
+
if (parts.length <= 1) return { base: token, variantList: [] };
|
|
285
|
+
const base = parts[parts.length - 1] || "";
|
|
286
|
+
const variantList = parts.slice(0, -1).map((v) => v.trim()).filter(Boolean);
|
|
287
|
+
return { base, variantList };
|
|
288
|
+
}
|
|
289
|
+
|
|
296
290
|
// src/scanner/style-extractor.ts
|
|
297
291
|
function extractStyles(root, getComputedStyle) {
|
|
298
292
|
const styles = {
|
|
@@ -372,7 +366,10 @@ function deserializeStyles(serialized) {
|
|
|
372
366
|
borderRadius: new Map(Object.entries(serialized.borderRadius))
|
|
373
367
|
};
|
|
374
368
|
}
|
|
375
|
-
function createStyleSummary(styles) {
|
|
369
|
+
function createStyleSummary(styles, options = {}) {
|
|
370
|
+
return createStyleSummaryWithOptions(styles, options);
|
|
371
|
+
}
|
|
372
|
+
function createStyleSummaryWithOptions(styles, options = {}) {
|
|
376
373
|
const lines = [];
|
|
377
374
|
lines.push("## Detected Styles Summary\n");
|
|
378
375
|
lines.push("### Colors");
|
|
@@ -406,7 +403,9 @@ function createStyleSummary(styles) {
|
|
|
406
403
|
});
|
|
407
404
|
lines.push("");
|
|
408
405
|
lines.push("### Spacing Values");
|
|
409
|
-
const sortedSpacing = [...styles.spacing.entries()].sort(
|
|
406
|
+
const sortedSpacing = [...styles.spacing.entries()].sort(
|
|
407
|
+
(a, b) => b[1] - a[1]
|
|
408
|
+
);
|
|
410
409
|
sortedSpacing.slice(0, 15).forEach(([value, count]) => {
|
|
411
410
|
lines.push(`- ${value}: ${count} occurrences`);
|
|
412
411
|
});
|
|
@@ -418,6 +417,38 @@ function createStyleSummary(styles) {
|
|
|
418
417
|
sortedBorderRadius.forEach(([value, count]) => {
|
|
419
418
|
lines.push(`- ${value}: ${count} occurrences`);
|
|
420
419
|
});
|
|
420
|
+
if (options.html) {
|
|
421
|
+
const tokens = extractClassTokensFromHtml(options.html);
|
|
422
|
+
const topUtilities = topEntries(tokens.utilities, 40);
|
|
423
|
+
const topVariants = topEntries(tokens.variants, 15);
|
|
424
|
+
lines.push("");
|
|
425
|
+
lines.push("### Utility Classes (from markup)");
|
|
426
|
+
if (topUtilities.length === 0) {
|
|
427
|
+
lines.push("- (none detected)");
|
|
428
|
+
} else {
|
|
429
|
+
topUtilities.forEach(({ token, count }) => {
|
|
430
|
+
lines.push(`- ${token}: ${count} occurrences`);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
if (topVariants.length > 0) {
|
|
434
|
+
lines.push("");
|
|
435
|
+
lines.push("### Common Variants");
|
|
436
|
+
topVariants.forEach(({ token, count }) => {
|
|
437
|
+
lines.push(`- ${token}: ${count} occurrences`);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (options.tailwindTheme) {
|
|
442
|
+
const tt = options.tailwindTheme;
|
|
443
|
+
lines.push("");
|
|
444
|
+
lines.push("### Tailwind Theme Tokens (from config)");
|
|
445
|
+
lines.push(`- configPath: ${tt.configPath}`);
|
|
446
|
+
lines.push(`- colors: ${tt.colors.length}`);
|
|
447
|
+
lines.push(`- spacingKeys: ${tt.spacingKeys.length}`);
|
|
448
|
+
lines.push(`- borderRadiusKeys: ${tt.borderRadiusKeys.length}`);
|
|
449
|
+
lines.push(`- fontFamilyKeys: ${tt.fontFamilyKeys.length}`);
|
|
450
|
+
lines.push(`- fontSizeKeys: ${tt.fontSizeKeys.length}`);
|
|
451
|
+
}
|
|
421
452
|
return lines.join("\n");
|
|
422
453
|
}
|
|
423
454
|
function truncateHTML(html, maxLength = 5e4) {
|
|
@@ -671,9 +702,87 @@ function extractStyleValues(content) {
|
|
|
671
702
|
}
|
|
672
703
|
return result;
|
|
673
704
|
}
|
|
705
|
+
function extractTailwindAllowlist(content) {
|
|
706
|
+
const empty = {
|
|
707
|
+
allowAnyColor: false,
|
|
708
|
+
allowStandardSpacing: false,
|
|
709
|
+
allowedTailwindColors: /* @__PURE__ */ new Set(),
|
|
710
|
+
allowedUtilities: /* @__PURE__ */ new Set(),
|
|
711
|
+
allowedSpacingKeys: /* @__PURE__ */ new Set(),
|
|
712
|
+
allowedBorderRadiusKeys: /* @__PURE__ */ new Set(),
|
|
713
|
+
allowedFontSizeKeys: /* @__PURE__ */ new Set(),
|
|
714
|
+
allowedFontFamilyKeys: /* @__PURE__ */ new Set()
|
|
715
|
+
};
|
|
716
|
+
const sections = parseStyleGuideSections(content);
|
|
717
|
+
const tailwindSection = sections["tailwind"] ?? // defensive: some styleguides use different casing/spacing
|
|
718
|
+
sections["tailwind utilities"] ?? "";
|
|
719
|
+
if (!tailwindSection) return empty;
|
|
720
|
+
const parsed = tryParseFirstJsonCodeBlock(tailwindSection);
|
|
721
|
+
if (parsed && typeof parsed === "object") {
|
|
722
|
+
const allowAnyColor = Boolean(parsed.allowAnyColor);
|
|
723
|
+
const allowStandardSpacing = Boolean(parsed.allowStandardSpacing);
|
|
724
|
+
const allowedUtilitiesArr = Array.isArray(parsed.allowedUtilities) ? parsed.allowedUtilities.filter(
|
|
725
|
+
(u) => typeof u === "string"
|
|
726
|
+
) : [];
|
|
727
|
+
const themeTokens = parsed.themeTokens ?? {};
|
|
728
|
+
const themeColors = Array.isArray(themeTokens.colors) ? themeTokens.colors.filter((c) => typeof c === "string") : [];
|
|
729
|
+
const spacingKeys = Array.isArray(themeTokens.spacingKeys) ? themeTokens.spacingKeys.filter((k) => typeof k === "string") : [];
|
|
730
|
+
const borderRadiusKeys = Array.isArray(themeTokens.borderRadiusKeys) ? themeTokens.borderRadiusKeys.filter((k) => typeof k === "string") : [];
|
|
731
|
+
const fontFamilyKeys = Array.isArray(themeTokens.fontFamilyKeys) ? themeTokens.fontFamilyKeys.filter((k) => typeof k === "string") : [];
|
|
732
|
+
const fontSizeKeys = Array.isArray(themeTokens.fontSizeKeys) ? themeTokens.fontSizeKeys.filter((k) => typeof k === "string") : [];
|
|
733
|
+
const allowedTailwindColors = /* @__PURE__ */ new Set();
|
|
734
|
+
for (const c of themeColors) {
|
|
735
|
+
const raw = c.trim();
|
|
736
|
+
if (!raw) continue;
|
|
737
|
+
if (raw.toLowerCase().startsWith("tailwind:")) {
|
|
738
|
+
allowedTailwindColors.add(raw.toLowerCase());
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const m = raw.match(/^([a-zA-Z]+)-(\d{2,3})$/);
|
|
742
|
+
if (m) {
|
|
743
|
+
allowedTailwindColors.add(`tailwind:${m[1].toLowerCase()}-${m[2]}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
allowAnyColor,
|
|
748
|
+
allowStandardSpacing,
|
|
749
|
+
allowedTailwindColors,
|
|
750
|
+
allowedUtilities: new Set(allowedUtilitiesArr.map((s) => s.trim()).filter(Boolean)),
|
|
751
|
+
allowedSpacingKeys: new Set(spacingKeys.map((s) => s.trim()).filter(Boolean)),
|
|
752
|
+
allowedBorderRadiusKeys: new Set(borderRadiusKeys.map((s) => s.trim()).filter(Boolean)),
|
|
753
|
+
allowedFontSizeKeys: new Set(fontSizeKeys.map((s) => s.trim()).filter(Boolean)),
|
|
754
|
+
allowedFontFamilyKeys: new Set(fontFamilyKeys.map((s) => s.trim()).filter(Boolean))
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const backticked = [];
|
|
758
|
+
for (const m of tailwindSection.matchAll(/`([^`]+)`/g)) {
|
|
759
|
+
backticked.push(m[1]);
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
...empty,
|
|
763
|
+
allowedUtilities: new Set(
|
|
764
|
+
backticked.flatMap((s) => s.split(/[,\s]+/g)).map((s) => s.trim()).filter(Boolean)
|
|
765
|
+
)
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function tryParseFirstJsonCodeBlock(section) {
|
|
769
|
+
const jsonBlocks = [...section.matchAll(/```json\s*([\s\S]*?)```/gi)];
|
|
770
|
+
const anyBlocks = [...section.matchAll(/```\s*([\s\S]*?)```/g)];
|
|
771
|
+
const candidates = (jsonBlocks.length ? jsonBlocks : anyBlocks).map((m) => m[1]);
|
|
772
|
+
for (const raw of candidates) {
|
|
773
|
+
const trimmed = raw.trim();
|
|
774
|
+
if (!trimmed) continue;
|
|
775
|
+
try {
|
|
776
|
+
return JSON.parse(trimmed);
|
|
777
|
+
} catch {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
674
783
|
|
|
675
784
|
// src/styleguide/generator.ts
|
|
676
|
-
function generateStyleGuideFromStyles(styles) {
|
|
785
|
+
function generateStyleGuideFromStyles(styles, options = {}) {
|
|
677
786
|
const lines = [];
|
|
678
787
|
lines.push("# UI Style Guide");
|
|
679
788
|
lines.push("");
|
|
@@ -681,9 +790,18 @@ function generateStyleGuideFromStyles(styles) {
|
|
|
681
790
|
"> Auto-generated by UILint. Edit this file to define your design system."
|
|
682
791
|
);
|
|
683
792
|
lines.push("");
|
|
793
|
+
const html = options.html || "";
|
|
794
|
+
const mergedColors = new Map(styles.colors);
|
|
795
|
+
if (html) {
|
|
796
|
+
for (const m of html.matchAll(/#[A-Fa-f0-9]{6}\b/g)) {
|
|
797
|
+
const hex = (m[0] || "").toUpperCase();
|
|
798
|
+
if (!hex) continue;
|
|
799
|
+
mergedColors.set(hex, (mergedColors.get(hex) || 0) + 1);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
684
802
|
lines.push("## Colors");
|
|
685
803
|
lines.push("");
|
|
686
|
-
const sortedColors = [...
|
|
804
|
+
const sortedColors = [...mergedColors.entries()].sort((a, b) => b[1] - a[1]);
|
|
687
805
|
if (sortedColors.length > 0) {
|
|
688
806
|
const colorNames = [
|
|
689
807
|
"Primary",
|
|
@@ -763,6 +881,71 @@ function generateStyleGuideFromStyles(styles) {
|
|
|
763
881
|
lines.push("- **Cards**: Define card styles here");
|
|
764
882
|
lines.push("- **Inputs**: Define input styles here");
|
|
765
883
|
lines.push("");
|
|
884
|
+
const classTokens = html ? extractClassTokensFromHtml(html) : null;
|
|
885
|
+
const topUtilities = classTokens ? topEntries(classTokens.utilities, 50) : [];
|
|
886
|
+
const topVariants = classTokens ? topEntries(classTokens.variants, 20) : [];
|
|
887
|
+
const allUtilities = classTokens ? [...classTokens.utilities.entries()].sort((a, b) => b[1] - a[1] ? b[1] - a[1] : a[0].localeCompare(b[0])).map(([token]) => token) : [];
|
|
888
|
+
const theme = options.tailwindTheme;
|
|
889
|
+
const themeColors = theme?.colors || [];
|
|
890
|
+
const themeSpacingKeys = theme?.spacingKeys || [];
|
|
891
|
+
const themeBorderRadiusKeys = theme?.borderRadiusKeys || [];
|
|
892
|
+
const themeFontFamilyKeys = theme?.fontFamilyKeys || [];
|
|
893
|
+
const themeFontSizeKeys = theme?.fontSizeKeys || [];
|
|
894
|
+
const hasTailwindSection = topUtilities.length > 0 || themeColors.length > 0 || themeSpacingKeys.length > 0 || themeBorderRadiusKeys.length > 0 || themeFontFamilyKeys.length > 0 || themeFontSizeKeys.length > 0;
|
|
895
|
+
if (hasTailwindSection) {
|
|
896
|
+
lines.push("## Tailwind");
|
|
897
|
+
lines.push("");
|
|
898
|
+
lines.push(
|
|
899
|
+
"> Optional. Captures Tailwind/utility class conventions for validation and consistency."
|
|
900
|
+
);
|
|
901
|
+
lines.push("");
|
|
902
|
+
if (topUtilities.length > 0) {
|
|
903
|
+
const utilityList = topUtilities.slice(0, 25).map((u) => `\`${u.token}\``).join(", ");
|
|
904
|
+
lines.push(`- **Observed utilities (top)**: ${utilityList}`);
|
|
905
|
+
} else {
|
|
906
|
+
lines.push("- **Observed utilities (top)**: (none detected)");
|
|
907
|
+
}
|
|
908
|
+
if (topVariants.length > 0) {
|
|
909
|
+
const variantList = topVariants.slice(0, 12).map((v) => `\`${v.token}\``).join(", ");
|
|
910
|
+
lines.push(`- **Common variants**: ${variantList}`);
|
|
911
|
+
}
|
|
912
|
+
if (theme) {
|
|
913
|
+
lines.push(`- **Config path**: \`${theme.configPath}\``);
|
|
914
|
+
if (themeColors.length > 0) {
|
|
915
|
+
lines.push(
|
|
916
|
+
`- **Theme colors (tokens)**: ${themeColors.slice(0, 25).map((t) => `\`${t}\``).join(", ")}${themeColors.length > 25 ? ", ..." : ""}`
|
|
917
|
+
);
|
|
918
|
+
} else {
|
|
919
|
+
lines.push("- **Theme colors (tokens)**: (none specified in config)");
|
|
920
|
+
}
|
|
921
|
+
if (themeSpacingKeys.length > 0) {
|
|
922
|
+
lines.push(
|
|
923
|
+
`- **Theme spacing keys**: ${themeSpacingKeys.slice(0, 25).map((k) => `\`${k}\``).join(", ")}${themeSpacingKeys.length > 25 ? ", ..." : ""}`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const payload = {
|
|
928
|
+
// IMPORTANT: include all observed utilities (not just "top N"), otherwise
|
|
929
|
+
// init+validate on the same file can be inconsistent.
|
|
930
|
+
allowedUtilities: allUtilities,
|
|
931
|
+
allowAnyColor: !!theme && themeColors.length === 0,
|
|
932
|
+
// avoid noisy warnings when config doesn't define colors
|
|
933
|
+
allowStandardSpacing: !!theme && themeSpacingKeys.length === 0,
|
|
934
|
+
themeTokens: theme ? {
|
|
935
|
+
configPath: theme.configPath,
|
|
936
|
+
colors: themeColors,
|
|
937
|
+
spacingKeys: themeSpacingKeys,
|
|
938
|
+
borderRadiusKeys: themeBorderRadiusKeys,
|
|
939
|
+
fontFamilyKeys: themeFontFamilyKeys,
|
|
940
|
+
fontSizeKeys: themeFontSizeKeys
|
|
941
|
+
} : null
|
|
942
|
+
};
|
|
943
|
+
lines.push("");
|
|
944
|
+
lines.push("```json");
|
|
945
|
+
lines.push(JSON.stringify(payload, null, 2));
|
|
946
|
+
lines.push("```");
|
|
947
|
+
lines.push("");
|
|
948
|
+
}
|
|
766
949
|
return lines.join("\n");
|
|
767
950
|
}
|
|
768
951
|
function findGCD(numbers) {
|
|
@@ -812,201 +995,10 @@ function styleGuideToMarkdown(guide) {
|
|
|
812
995
|
return lines.join("\n");
|
|
813
996
|
}
|
|
814
997
|
|
|
815
|
-
// src/validation/validate.ts
|
|
816
|
-
function validateCode(code, styleGuide) {
|
|
817
|
-
const issues = [];
|
|
818
|
-
if (!styleGuide) {
|
|
819
|
-
return {
|
|
820
|
-
valid: true,
|
|
821
|
-
issues: [
|
|
822
|
-
{
|
|
823
|
-
type: "warning",
|
|
824
|
-
message: "No style guide found. Create .uilint/styleguide.md to enable validation."
|
|
825
|
-
}
|
|
826
|
-
]
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
const styleValues = extractStyleValues(styleGuide);
|
|
830
|
-
const codeColors = extractColorsFromCode(code);
|
|
831
|
-
for (const color of codeColors) {
|
|
832
|
-
if (!styleValues.colors.includes(color.toUpperCase())) {
|
|
833
|
-
const similar = findSimilarColor(color, styleValues.colors);
|
|
834
|
-
issues.push({
|
|
835
|
-
type: "warning",
|
|
836
|
-
message: `Color ${color} is not in the style guide`,
|
|
837
|
-
suggestion: similar ? `Consider using ${similar} instead` : `Add ${color} to the style guide if intentional`
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
const hardcodedPixels = code.matchAll(
|
|
842
|
-
/(?:margin|padding|gap)[-:].*?(\d+)px/gi
|
|
843
|
-
);
|
|
844
|
-
for (const match of hardcodedPixels) {
|
|
845
|
-
const value = parseInt(match[1]);
|
|
846
|
-
if (value % 4 !== 0) {
|
|
847
|
-
issues.push({
|
|
848
|
-
type: "warning",
|
|
849
|
-
message: `Spacing value ${value}px doesn't follow the 4px grid`,
|
|
850
|
-
suggestion: `Use ${Math.round(value / 4) * 4}px instead`
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
if (code.includes("style={{") || code.includes("style={")) {
|
|
855
|
-
const inlineStyleCount = (code.match(/style=\{/g) || []).length;
|
|
856
|
-
if (inlineStyleCount > 2) {
|
|
857
|
-
issues.push({
|
|
858
|
-
type: "warning",
|
|
859
|
-
message: `Found ${inlineStyleCount} inline styles. Consider using CSS classes for consistency.`
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
return {
|
|
864
|
-
valid: issues.filter((i) => i.type === "error").length === 0,
|
|
865
|
-
issues
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
function lintSnippet(code, styleGuide) {
|
|
869
|
-
const issues = [];
|
|
870
|
-
issues.push(...lintBasicPatterns(code));
|
|
871
|
-
if (styleGuide) {
|
|
872
|
-
issues.push(...lintAgainstStyleGuide(code, styleGuide));
|
|
873
|
-
}
|
|
874
|
-
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
875
|
-
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
876
|
-
return {
|
|
877
|
-
issues,
|
|
878
|
-
summary: issues.length === 0 ? "No issues found" : `Found ${errorCount} errors and ${warningCount} warnings`
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
function lintBasicPatterns(code) {
|
|
882
|
-
const issues = [];
|
|
883
|
-
const magicNumbers = code.matchAll(
|
|
884
|
-
/(?:width|height|size):\s*(\d+)(?!px|rem|em|%)/g
|
|
885
|
-
);
|
|
886
|
-
for (const match of magicNumbers) {
|
|
887
|
-
issues.push({
|
|
888
|
-
severity: "warning",
|
|
889
|
-
type: "spacing",
|
|
890
|
-
message: `Magic number ${match[1]} found without unit`,
|
|
891
|
-
code: match[0],
|
|
892
|
-
suggestion: `Add a unit like ${match[1]}px or use a design token`
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
const hardcodedTailwindColors = code.matchAll(
|
|
896
|
-
/className=["'][^"']*(?:bg|text|border)-\[#[A-Fa-f0-9]+\][^"']*/g
|
|
897
|
-
);
|
|
898
|
-
for (const match of hardcodedTailwindColors) {
|
|
899
|
-
issues.push({
|
|
900
|
-
severity: "warning",
|
|
901
|
-
type: "color",
|
|
902
|
-
message: "Hardcoded color in Tailwind arbitrary value",
|
|
903
|
-
code: match[0],
|
|
904
|
-
suggestion: "Use a color from your Tailwind config or style guide"
|
|
905
|
-
});
|
|
906
|
-
}
|
|
907
|
-
if (code.includes("<img") && !code.includes("alt=")) {
|
|
908
|
-
issues.push({
|
|
909
|
-
severity: "error",
|
|
910
|
-
type: "accessibility",
|
|
911
|
-
message: "Image without alt attribute",
|
|
912
|
-
suggestion: 'Add alt="" for decorative images or descriptive alt text'
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
if (code.includes("<button") && !code.match(/<button[^>]*>.*\S.*<\/button>/s)) {
|
|
916
|
-
issues.push({
|
|
917
|
-
severity: "warning",
|
|
918
|
-
type: "accessibility",
|
|
919
|
-
message: "Button may be missing accessible text",
|
|
920
|
-
suggestion: "Ensure button has visible text or aria-label"
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
const singleQuotes = (code.match(/className='/g) || []).length;
|
|
924
|
-
const doubleQuotes = (code.match(/className="/g) || []).length;
|
|
925
|
-
if (singleQuotes > 0 && doubleQuotes > 0) {
|
|
926
|
-
issues.push({
|
|
927
|
-
severity: "info",
|
|
928
|
-
type: "component",
|
|
929
|
-
message: "Mixed quote styles in className attributes",
|
|
930
|
-
suggestion: "Use consistent quote style throughout"
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
return issues;
|
|
934
|
-
}
|
|
935
|
-
function lintAgainstStyleGuide(code, styleGuide) {
|
|
936
|
-
const issues = [];
|
|
937
|
-
const values = extractStyleValues(styleGuide);
|
|
938
|
-
const codeColors = code.matchAll(/#[A-Fa-f0-9]{6}\b/g);
|
|
939
|
-
for (const match of codeColors) {
|
|
940
|
-
const color = match[0].toUpperCase();
|
|
941
|
-
if (!values.colors.includes(color)) {
|
|
942
|
-
issues.push({
|
|
943
|
-
severity: "warning",
|
|
944
|
-
type: "color",
|
|
945
|
-
message: `Color ${color} not in style guide`,
|
|
946
|
-
code: match[0],
|
|
947
|
-
suggestion: `Allowed colors: ${values.colors.slice(0, 5).join(", ")}${values.colors.length > 5 ? "..." : ""}`
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
const spacingValues = code.matchAll(/(?:p|m|gap)-(\d+)/g);
|
|
952
|
-
for (const match of spacingValues) {
|
|
953
|
-
const value = parseInt(match[1]);
|
|
954
|
-
if (value > 12 && value % 4 !== 0) {
|
|
955
|
-
issues.push({
|
|
956
|
-
severity: "info",
|
|
957
|
-
type: "spacing",
|
|
958
|
-
message: `Spacing value ${match[0]} might not align with design system`,
|
|
959
|
-
suggestion: "Consider using standard Tailwind spacing values (1-12, 16, 20, 24...)"
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
return issues;
|
|
964
|
-
}
|
|
965
|
-
function extractColorsFromCode(code) {
|
|
966
|
-
const colors = [];
|
|
967
|
-
const hexMatches = code.matchAll(/#[A-Fa-f0-9]{6}\b/g);
|
|
968
|
-
for (const match of hexMatches) {
|
|
969
|
-
colors.push(match[0].toUpperCase());
|
|
970
|
-
}
|
|
971
|
-
const tailwindMatches = code.matchAll(/(?:bg|text|border)-(\w+)-(\d+)/g);
|
|
972
|
-
for (const match of tailwindMatches) {
|
|
973
|
-
colors.push(`tailwind:${match[1]}-${match[2]}`);
|
|
974
|
-
}
|
|
975
|
-
return [...new Set(colors)];
|
|
976
|
-
}
|
|
977
|
-
function findSimilarColor(color, allowedColors) {
|
|
978
|
-
const colorRgb = hexToRgb(color);
|
|
979
|
-
if (!colorRgb) return null;
|
|
980
|
-
let closest = null;
|
|
981
|
-
let closestDistance = Infinity;
|
|
982
|
-
for (const allowed of allowedColors) {
|
|
983
|
-
const allowedRgb = hexToRgb(allowed);
|
|
984
|
-
if (!allowedRgb) continue;
|
|
985
|
-
const distance = Math.sqrt(
|
|
986
|
-
Math.pow(colorRgb.r - allowedRgb.r, 2) + Math.pow(colorRgb.g - allowedRgb.g, 2) + Math.pow(colorRgb.b - allowedRgb.b, 2)
|
|
987
|
-
);
|
|
988
|
-
if (distance < closestDistance && distance < 50) {
|
|
989
|
-
closestDistance = distance;
|
|
990
|
-
closest = allowed;
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
return closest;
|
|
994
|
-
}
|
|
995
|
-
function hexToRgb(hex) {
|
|
996
|
-
const match = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
|
|
997
|
-
if (!match) return null;
|
|
998
|
-
return {
|
|
999
|
-
r: parseInt(match[1], 16),
|
|
1000
|
-
g: parseInt(match[2], 16),
|
|
1001
|
-
b: parseInt(match[3], 16)
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
998
|
export {
|
|
1006
999
|
buildAnalysisPrompt,
|
|
1007
1000
|
buildStyleGuidePrompt,
|
|
1008
1001
|
buildQueryPrompt,
|
|
1009
|
-
buildValidationPrompt,
|
|
1010
1002
|
OllamaClient,
|
|
1011
1003
|
getOllamaClient,
|
|
1012
1004
|
extractStyles,
|
|
@@ -1025,9 +1017,8 @@ export {
|
|
|
1025
1017
|
parseStyleGuide,
|
|
1026
1018
|
parseStyleGuideSections,
|
|
1027
1019
|
extractStyleValues,
|
|
1020
|
+
extractTailwindAllowlist,
|
|
1028
1021
|
generateStyleGuideFromStyles,
|
|
1029
|
-
styleGuideToMarkdown
|
|
1030
|
-
validateCode,
|
|
1031
|
-
lintSnippet
|
|
1022
|
+
styleGuideToMarkdown
|
|
1032
1023
|
};
|
|
1033
|
-
//# sourceMappingURL=chunk-
|
|
1024
|
+
//# sourceMappingURL=chunk-P2QQGDQS.js.map
|