uilint-core 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,1024 @@
1
+ // src/ollama/prompts.ts
2
+ function buildAnalysisPrompt(styleSummary, styleGuide) {
3
+ const guideSection = styleGuide ? `## Current Style Guide
4
+ ${styleGuide}
5
+
6
+ ` : "## No Style Guide Found\nAnalyze the styles and identify inconsistencies.\n\n";
7
+ return `You are a UI consistency analyzer. Analyze the following extracted styles and identify inconsistencies.
8
+
9
+ ${guideSection}
10
+
11
+ ${styleSummary}
12
+
13
+ Respond with a JSON object containing an "issues" array. Each issue should have:
14
+ - id: unique string identifier
15
+ - type: one of "color", "typography", "spacing", "component", "responsive", "accessibility"
16
+ - message: human-readable description of the issue
17
+ - currentValue: the problematic value found
18
+ - expectedValue: what it should be (if known from style guide)
19
+ - suggestion: how to fix it
20
+
21
+ Focus on:
22
+ 1. Similar but not identical colors (e.g., #3B82F6 vs #3575E2)
23
+ 2. Inconsistent font sizes or weights
24
+ 3. Spacing values that don't follow a consistent scale (e.g., 4px base unit)
25
+ 4. Mixed border-radius values
26
+
27
+ Be concise and actionable. Only report significant inconsistencies.
28
+
29
+ Example response:
30
+ {
31
+ "issues": [
32
+ {
33
+ "id": "color-1",
34
+ "type": "color",
35
+ "message": "Found similar blue colors that should be consolidated",
36
+ "currentValue": "#3575E2",
37
+ "expectedValue": "#3B82F6",
38
+ "suggestion": "Use the primary blue #3B82F6 consistently"
39
+ }
40
+ ]
41
+ }`;
42
+ }
43
+ function buildStyleGuidePrompt(styleSummary) {
44
+ return `You are a design system expert. Based on the following detected styles, generate a clean, organized style guide in Markdown format.
45
+
46
+ ${styleSummary}
47
+
48
+ Generate a style guide with these sections:
49
+ 1. Colors - List the main colors with semantic names (Primary, Secondary, etc.)
50
+ 2. Typography - Font families, sizes, and weights
51
+ 3. Spacing - Base unit and common spacing values
52
+ 4. Components - Common component patterns
53
+
54
+ Use this format:
55
+ # UI Style Guide
56
+
57
+ ## Colors
58
+ - **Primary**: #HEXCODE (usage description)
59
+ - **Secondary**: #HEXCODE (usage description)
60
+ ...
61
+
62
+ ## Typography
63
+ - **Font Family**: FontName
64
+ - **Font Sizes**: list of sizes
65
+ - **Font Weights**: list of weights
66
+
67
+ ## Spacing
68
+ - **Base unit**: Xpx
69
+ - **Common values**: list of values
70
+
71
+ ## Components
72
+ - **Buttons**: styles
73
+ - **Cards**: styles
74
+ ...
75
+
76
+ Be concise and focus on the most used values.`;
77
+ }
78
+ function buildQueryPrompt(query, styleGuide) {
79
+ if (!styleGuide) {
80
+ return `The user is asking: "${query}"
81
+
82
+ No style guide has been created yet. Explain that they should run "uilint init" to create a style guide from their existing styles.`;
83
+ }
84
+ return `You are a helpful assistant that answers questions about a UI style guide.
85
+
86
+ ## Style Guide
87
+ ${styleGuide}
88
+
89
+ ## User Question
90
+ ${query}
91
+
92
+ 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
+ }
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
+
134
+ // src/ollama/client.ts
135
+ var DEFAULT_BASE_URL = "http://localhost:11434";
136
+ var DEFAULT_MODEL = "qwen2.5-coder:7b";
137
+ var DEFAULT_TIMEOUT = 6e4;
138
+ var OllamaClient = class {
139
+ baseUrl;
140
+ model;
141
+ timeout;
142
+ constructor(options = {}) {
143
+ this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;
144
+ this.model = options.model || DEFAULT_MODEL;
145
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
146
+ }
147
+ /**
148
+ * Analyzes styles and returns issues
149
+ */
150
+ async analyzeStyles(styleSummary, styleGuide) {
151
+ const startTime = Date.now();
152
+ const prompt = buildAnalysisPrompt(styleSummary, styleGuide);
153
+ try {
154
+ const response = await this.generate(prompt);
155
+ const issues = this.parseIssuesResponse(response);
156
+ return {
157
+ issues,
158
+ analysisTime: Date.now() - startTime
159
+ };
160
+ } catch (error) {
161
+ console.error("[UILint] Analysis failed:", error);
162
+ return {
163
+ issues: [],
164
+ analysisTime: Date.now() - startTime
165
+ };
166
+ }
167
+ }
168
+ /**
169
+ * Generates a style guide from detected styles
170
+ */
171
+ async generateStyleGuide(styleSummary) {
172
+ const prompt = buildStyleGuidePrompt(styleSummary);
173
+ try {
174
+ const response = await this.generate(prompt, false);
175
+ return response;
176
+ } catch (error) {
177
+ console.error("[UILint] Style guide generation failed:", error);
178
+ return null;
179
+ }
180
+ }
181
+ /**
182
+ * Queries the style guide for specific information
183
+ */
184
+ async queryStyleGuide(query, styleGuide) {
185
+ const prompt = buildQueryPrompt(query, styleGuide);
186
+ try {
187
+ const response = await this.generate(prompt, false);
188
+ return response;
189
+ } catch (error) {
190
+ console.error("[UILint] Query failed:", error);
191
+ return "Failed to query style guide. Please try again.";
192
+ }
193
+ }
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
+ /**
208
+ * Core generate method that calls Ollama API
209
+ */
210
+ async generate(prompt, jsonFormat = true) {
211
+ const controller = new AbortController();
212
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
213
+ try {
214
+ const response = await fetch(`${this.baseUrl}/api/generate`, {
215
+ method: "POST",
216
+ headers: { "Content-Type": "application/json" },
217
+ body: JSON.stringify({
218
+ model: this.model,
219
+ prompt,
220
+ stream: false,
221
+ ...jsonFormat && { format: "json" }
222
+ }),
223
+ signal: controller.signal
224
+ });
225
+ if (!response.ok) {
226
+ throw new Error(`Ollama API error: ${response.status}`);
227
+ }
228
+ const data = await response.json();
229
+ return data.response || "";
230
+ } finally {
231
+ clearTimeout(timeoutId);
232
+ }
233
+ }
234
+ /**
235
+ * Parses issues from LLM response
236
+ */
237
+ parseIssuesResponse(response) {
238
+ try {
239
+ const parsed = JSON.parse(response);
240
+ return parsed.issues || [];
241
+ } catch {
242
+ console.warn("[UILint] Failed to parse LLM response as JSON");
243
+ return [];
244
+ }
245
+ }
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
+ /**
262
+ * Checks if Ollama is available
263
+ */
264
+ async isAvailable() {
265
+ try {
266
+ const response = await fetch(`${this.baseUrl}/api/tags`, {
267
+ method: "GET",
268
+ signal: AbortSignal.timeout(5e3)
269
+ });
270
+ return response.ok;
271
+ } catch {
272
+ return false;
273
+ }
274
+ }
275
+ /**
276
+ * Gets the current model
277
+ */
278
+ getModel() {
279
+ return this.model;
280
+ }
281
+ /**
282
+ * Sets the model
283
+ */
284
+ setModel(model) {
285
+ this.model = model;
286
+ }
287
+ };
288
+ var defaultClient = null;
289
+ function getOllamaClient(options) {
290
+ if (!defaultClient || options) {
291
+ defaultClient = new OllamaClient(options);
292
+ }
293
+ return defaultClient;
294
+ }
295
+
296
+ // src/scanner/style-extractor.ts
297
+ function extractStyles(root, getComputedStyle) {
298
+ const styles = {
299
+ colors: /* @__PURE__ */ new Map(),
300
+ fontSizes: /* @__PURE__ */ new Map(),
301
+ fontFamilies: /* @__PURE__ */ new Map(),
302
+ fontWeights: /* @__PURE__ */ new Map(),
303
+ spacing: /* @__PURE__ */ new Map(),
304
+ borderRadius: /* @__PURE__ */ new Map()
305
+ };
306
+ const elements = root.querySelectorAll("*");
307
+ elements.forEach((element) => {
308
+ if (element.nodeType !== 1) return;
309
+ const computed = getComputedStyle(element);
310
+ extractColor(computed.color, styles.colors);
311
+ extractColor(computed.backgroundColor, styles.colors);
312
+ extractColor(computed.borderColor, styles.colors);
313
+ incrementMap(styles.fontSizes, computed.fontSize);
314
+ incrementMap(styles.fontFamilies, normalizeFontFamily(computed.fontFamily));
315
+ incrementMap(styles.fontWeights, computed.fontWeight);
316
+ extractSpacing(computed.margin, styles.spacing);
317
+ extractSpacing(computed.padding, styles.spacing);
318
+ incrementMap(styles.spacing, computed.gap);
319
+ incrementMap(styles.borderRadius, computed.borderRadius);
320
+ });
321
+ return styles;
322
+ }
323
+ function extractStylesFromDOM(root) {
324
+ const targetRoot = root || document.body;
325
+ return extractStyles(targetRoot, (el) => window.getComputedStyle(el));
326
+ }
327
+ function extractColor(color, map) {
328
+ if (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)") return;
329
+ const hex = rgbToHex(color);
330
+ if (hex) {
331
+ incrementMap(map, hex);
332
+ }
333
+ }
334
+ function extractSpacing(value, map) {
335
+ if (!value || value === "0px") return;
336
+ const values = value.split(" ").filter((v) => v && v !== "0px");
337
+ values.forEach((v) => incrementMap(map, v));
338
+ }
339
+ function incrementMap(map, value) {
340
+ if (!value || value === "normal" || value === "auto") return;
341
+ map.set(value, (map.get(value) || 0) + 1);
342
+ }
343
+ function normalizeFontFamily(fontFamily) {
344
+ const primary = fontFamily.split(",")[0].trim();
345
+ return primary.replace(/['"]/g, "");
346
+ }
347
+ function rgbToHex(rgb) {
348
+ if (rgb.startsWith("#")) return rgb.toUpperCase();
349
+ const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
350
+ if (!match) return null;
351
+ const [, r, g, b] = match;
352
+ const toHex = (n) => parseInt(n).toString(16).padStart(2, "0");
353
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
354
+ }
355
+ function serializeStyles(styles) {
356
+ return {
357
+ colors: Object.fromEntries(styles.colors),
358
+ fontSizes: Object.fromEntries(styles.fontSizes),
359
+ fontFamilies: Object.fromEntries(styles.fontFamilies),
360
+ fontWeights: Object.fromEntries(styles.fontWeights),
361
+ spacing: Object.fromEntries(styles.spacing),
362
+ borderRadius: Object.fromEntries(styles.borderRadius)
363
+ };
364
+ }
365
+ function deserializeStyles(serialized) {
366
+ return {
367
+ colors: new Map(Object.entries(serialized.colors)),
368
+ fontSizes: new Map(Object.entries(serialized.fontSizes)),
369
+ fontFamilies: new Map(Object.entries(serialized.fontFamilies)),
370
+ fontWeights: new Map(Object.entries(serialized.fontWeights)),
371
+ spacing: new Map(Object.entries(serialized.spacing)),
372
+ borderRadius: new Map(Object.entries(serialized.borderRadius))
373
+ };
374
+ }
375
+ function createStyleSummary(styles) {
376
+ const lines = [];
377
+ lines.push("## Detected Styles Summary\n");
378
+ lines.push("### Colors");
379
+ const sortedColors = [...styles.colors.entries()].sort((a, b) => b[1] - a[1]);
380
+ sortedColors.slice(0, 20).forEach(([color, count]) => {
381
+ lines.push(`- ${color}: ${count} occurrences`);
382
+ });
383
+ lines.push("");
384
+ lines.push("### Font Sizes");
385
+ const sortedFontSizes = [...styles.fontSizes.entries()].sort(
386
+ (a, b) => b[1] - a[1]
387
+ );
388
+ sortedFontSizes.forEach(([size, count]) => {
389
+ lines.push(`- ${size}: ${count} occurrences`);
390
+ });
391
+ lines.push("");
392
+ lines.push("### Font Families");
393
+ const sortedFontFamilies = [...styles.fontFamilies.entries()].sort(
394
+ (a, b) => b[1] - a[1]
395
+ );
396
+ sortedFontFamilies.forEach(([family, count]) => {
397
+ lines.push(`- ${family}: ${count} occurrences`);
398
+ });
399
+ lines.push("");
400
+ lines.push("### Font Weights");
401
+ const sortedFontWeights = [...styles.fontWeights.entries()].sort(
402
+ (a, b) => b[1] - a[1]
403
+ );
404
+ sortedFontWeights.forEach(([weight, count]) => {
405
+ lines.push(`- ${weight}: ${count} occurrences`);
406
+ });
407
+ lines.push("");
408
+ lines.push("### Spacing Values");
409
+ const sortedSpacing = [...styles.spacing.entries()].sort((a, b) => b[1] - a[1]);
410
+ sortedSpacing.slice(0, 15).forEach(([value, count]) => {
411
+ lines.push(`- ${value}: ${count} occurrences`);
412
+ });
413
+ lines.push("");
414
+ lines.push("### Border Radius");
415
+ const sortedBorderRadius = [...styles.borderRadius.entries()].sort(
416
+ (a, b) => b[1] - a[1]
417
+ );
418
+ sortedBorderRadius.forEach(([value, count]) => {
419
+ lines.push(`- ${value}: ${count} occurrences`);
420
+ });
421
+ return lines.join("\n");
422
+ }
423
+ function truncateHTML(html, maxLength = 5e4) {
424
+ if (html.length <= maxLength) return html;
425
+ return html.slice(0, maxLength) + "<!-- truncated -->";
426
+ }
427
+
428
+ // src/styleguide/schema.ts
429
+ function createEmptyStyleGuide() {
430
+ return {
431
+ colors: [],
432
+ typography: [],
433
+ spacing: [],
434
+ components: []
435
+ };
436
+ }
437
+ function validateStyleGuide(guide) {
438
+ if (!guide || typeof guide !== "object") return false;
439
+ const g = guide;
440
+ return Array.isArray(g.colors) && Array.isArray(g.typography) && Array.isArray(g.spacing) && Array.isArray(g.components);
441
+ }
442
+ function mergeStyleGuides(existing, detected) {
443
+ return {
444
+ colors: mergeColorRules(existing.colors, detected.colors || []),
445
+ typography: mergeTypographyRules(existing.typography, detected.typography || []),
446
+ spacing: mergeSpacingRules(existing.spacing, detected.spacing || []),
447
+ components: mergeComponentRules(existing.components, detected.components || [])
448
+ };
449
+ }
450
+ function mergeColorRules(existing, detected) {
451
+ const merged = [...existing];
452
+ detected.forEach((rule) => {
453
+ const existingIndex = merged.findIndex(
454
+ (e) => e.name === rule.name || e.value === rule.value
455
+ );
456
+ if (existingIndex === -1) {
457
+ merged.push(rule);
458
+ }
459
+ });
460
+ return merged;
461
+ }
462
+ function mergeTypographyRules(existing, detected) {
463
+ const merged = [...existing];
464
+ detected.forEach((rule) => {
465
+ const existingIndex = merged.findIndex((e) => e.element === rule.element);
466
+ if (existingIndex === -1) {
467
+ merged.push(rule);
468
+ }
469
+ });
470
+ return merged;
471
+ }
472
+ function mergeSpacingRules(existing, detected) {
473
+ const merged = [...existing];
474
+ detected.forEach((rule) => {
475
+ const existingIndex = merged.findIndex(
476
+ (e) => e.name === rule.name || e.value === rule.value
477
+ );
478
+ if (existingIndex === -1) {
479
+ merged.push(rule);
480
+ }
481
+ });
482
+ return merged;
483
+ }
484
+ function mergeComponentRules(existing, detected) {
485
+ const merged = [...existing];
486
+ detected.forEach((rule) => {
487
+ const existingIndex = merged.findIndex((e) => e.name === rule.name);
488
+ if (existingIndex === -1) {
489
+ merged.push(rule);
490
+ }
491
+ });
492
+ return merged;
493
+ }
494
+ function createColorRule(name, value, usage = "") {
495
+ return { name, value: value.toUpperCase(), usage };
496
+ }
497
+ function createTypographyRule(element, options = {}) {
498
+ return { element, ...options };
499
+ }
500
+ function createSpacingRule(name, value) {
501
+ return { name, value };
502
+ }
503
+ function createComponentRule(name, styles) {
504
+ return { name, styles };
505
+ }
506
+
507
+ // src/styleguide/parser.ts
508
+ function parseStyleGuide(markdown) {
509
+ const guide = createEmptyStyleGuide();
510
+ const sections = splitIntoSections(markdown);
511
+ sections.forEach(({ title, content }) => {
512
+ const lowerTitle = title.toLowerCase();
513
+ if (lowerTitle.includes("color")) {
514
+ guide.colors = parseColorSection(content);
515
+ } else if (lowerTitle.includes("typography") || lowerTitle.includes("font")) {
516
+ guide.typography = parseTypographySection(content);
517
+ } else if (lowerTitle.includes("spacing")) {
518
+ guide.spacing = parseSpacingSection(content);
519
+ } else if (lowerTitle.includes("component")) {
520
+ guide.components = parseComponentSection(content);
521
+ }
522
+ });
523
+ return guide;
524
+ }
525
+ function splitIntoSections(markdown) {
526
+ const sections = [];
527
+ const lines = markdown.split("\n");
528
+ let currentTitle = "";
529
+ let currentContent = [];
530
+ lines.forEach((line) => {
531
+ const headerMatch = line.match(/^##\s+(.+)$/);
532
+ if (headerMatch) {
533
+ if (currentTitle) {
534
+ sections.push({
535
+ title: currentTitle,
536
+ content: currentContent.join("\n")
537
+ });
538
+ }
539
+ currentTitle = headerMatch[1];
540
+ currentContent = [];
541
+ } else {
542
+ currentContent.push(line);
543
+ }
544
+ });
545
+ if (currentTitle) {
546
+ sections.push({
547
+ title: currentTitle,
548
+ content: currentContent.join("\n")
549
+ });
550
+ }
551
+ return sections;
552
+ }
553
+ function parseColorSection(content) {
554
+ const colors = [];
555
+ const lines = content.split("\n");
556
+ lines.forEach((line) => {
557
+ const match = line.match(
558
+ /[-*]\s*\*?\*?([^*:]+)\*?\*?:\s*(#[A-Fa-f0-9]{6})\s*(?:\(([^)]+)\))?/
559
+ );
560
+ if (match) {
561
+ colors.push({
562
+ name: match[1].trim(),
563
+ value: match[2].toUpperCase(),
564
+ usage: match[3] || ""
565
+ });
566
+ }
567
+ });
568
+ return colors;
569
+ }
570
+ function parseTypographySection(content) {
571
+ const typography = [];
572
+ const lines = content.split("\n");
573
+ lines.forEach((line) => {
574
+ const elementMatch = line.match(/[-*]\s*\*?\*?([^*:]+)\*?\*?:\s*(.+)/);
575
+ if (elementMatch) {
576
+ const rule = {
577
+ element: elementMatch[1].trim()
578
+ };
579
+ const props = elementMatch[2];
580
+ const fontFamilyMatch = props.match(/font-family:\s*"?([^",]+)"?/);
581
+ if (fontFamilyMatch) rule.fontFamily = fontFamilyMatch[1].trim();
582
+ const fontSizeMatch = props.match(/font-size:\s*(\d+px)/);
583
+ if (fontSizeMatch) rule.fontSize = fontSizeMatch[1];
584
+ const fontWeightMatch = props.match(/font-weight:\s*(\d+)/);
585
+ if (fontWeightMatch) rule.fontWeight = fontWeightMatch[1];
586
+ const lineHeightMatch = props.match(/line-height:\s*([\d.]+)/);
587
+ if (lineHeightMatch) rule.lineHeight = lineHeightMatch[1];
588
+ typography.push(rule);
589
+ }
590
+ });
591
+ return typography;
592
+ }
593
+ function parseSpacingSection(content) {
594
+ const spacing = [];
595
+ const lines = content.split("\n");
596
+ lines.forEach((line) => {
597
+ const match = line.match(/[-*]\s*\*?\*?([^*:]+)\*?\*?:\s*(.+)/);
598
+ if (match) {
599
+ spacing.push({
600
+ name: match[1].trim(),
601
+ value: match[2].trim()
602
+ });
603
+ }
604
+ });
605
+ return spacing;
606
+ }
607
+ function parseComponentSection(content) {
608
+ const components = [];
609
+ const lines = content.split("\n");
610
+ lines.forEach((line) => {
611
+ const match = line.match(/[-*]\s*\*?\*?([^*:]+)\*?\*?:\s*(.+)/);
612
+ if (match) {
613
+ components.push({
614
+ name: match[1].trim(),
615
+ styles: match[2].split(",").map((s) => s.trim())
616
+ });
617
+ }
618
+ });
619
+ return components;
620
+ }
621
+ function parseStyleGuideSections(content) {
622
+ const sections = {};
623
+ const lines = content.split("\n");
624
+ let currentSection = "intro";
625
+ let currentContent = [];
626
+ for (const line of lines) {
627
+ const headerMatch = line.match(/^##\s+(.+)$/);
628
+ if (headerMatch) {
629
+ if (currentContent.length > 0) {
630
+ sections[currentSection.toLowerCase()] = currentContent.join("\n").trim();
631
+ }
632
+ currentSection = headerMatch[1];
633
+ currentContent = [];
634
+ } else {
635
+ currentContent.push(line);
636
+ }
637
+ }
638
+ if (currentContent.length > 0) {
639
+ sections[currentSection.toLowerCase()] = currentContent.join("\n").trim();
640
+ }
641
+ return sections;
642
+ }
643
+ function extractStyleValues(content) {
644
+ const result = {
645
+ colors: [],
646
+ fontSizes: [],
647
+ fontFamilies: [],
648
+ spacing: [],
649
+ borderRadius: []
650
+ };
651
+ const colorMatches = content.matchAll(/#[A-Fa-f0-9]{6}\b/g);
652
+ for (const match of colorMatches) {
653
+ if (!result.colors.includes(match[0].toUpperCase())) {
654
+ result.colors.push(match[0].toUpperCase());
655
+ }
656
+ }
657
+ const fontSizeMatches = content.matchAll(/\b(\d+(?:\.\d+)?(?:px|rem|em))\b/g);
658
+ for (const match of fontSizeMatches) {
659
+ if (!result.fontSizes.includes(match[1])) {
660
+ result.fontSizes.push(match[1]);
661
+ }
662
+ }
663
+ const fontFamilyMatches = content.matchAll(/font-family:\s*["']?([^"',\n]+)/gi);
664
+ for (const match of fontFamilyMatches) {
665
+ const family = match[1].trim();
666
+ if (!result.fontFamilies.includes(family)) {
667
+ result.fontFamilies.push(family);
668
+ }
669
+ }
670
+ return result;
671
+ }
672
+
673
+ // src/styleguide/generator.ts
674
+ function generateStyleGuideFromStyles(styles) {
675
+ const lines = [];
676
+ lines.push("# UI Style Guide");
677
+ lines.push("");
678
+ lines.push("> Auto-generated by UILint. Edit this file to define your design system.");
679
+ lines.push("");
680
+ lines.push("## Colors");
681
+ lines.push("");
682
+ const sortedColors = [...styles.colors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
683
+ if (sortedColors.length > 0) {
684
+ const colorNames = [
685
+ "Primary",
686
+ "Secondary",
687
+ "Accent",
688
+ "Background",
689
+ "Text",
690
+ "Muted",
691
+ "Border",
692
+ "Success",
693
+ "Warning",
694
+ "Error"
695
+ ];
696
+ sortedColors.forEach(([color, count], index) => {
697
+ const name = colorNames[index] || `Color ${index + 1}`;
698
+ lines.push(`- **${name}**: ${color} (${count} occurrences)`);
699
+ });
700
+ } else {
701
+ lines.push("- No colors detected");
702
+ }
703
+ lines.push("");
704
+ lines.push("## Typography");
705
+ lines.push("");
706
+ const sortedFontFamilies = [...styles.fontFamilies.entries()].sort(
707
+ (a, b) => b[1] - a[1]
708
+ );
709
+ if (sortedFontFamilies.length > 0) {
710
+ lines.push(`- **Font Family**: ${sortedFontFamilies[0][0]}`);
711
+ }
712
+ const sortedFontSizes = [...styles.fontSizes.entries()].sort(
713
+ (a, b) => parseFloat(a[0]) - parseFloat(b[0])
714
+ );
715
+ if (sortedFontSizes.length > 0) {
716
+ const sizes = sortedFontSizes.map(([size]) => size).join(", ");
717
+ lines.push(`- **Font Sizes**: ${sizes}`);
718
+ }
719
+ const sortedFontWeights = [...styles.fontWeights.entries()].sort(
720
+ (a, b) => parseInt(a[0]) - parseInt(b[0])
721
+ );
722
+ if (sortedFontWeights.length > 0) {
723
+ const weights = sortedFontWeights.map(([weight]) => weight).join(", ");
724
+ lines.push(`- **Font Weights**: ${weights}`);
725
+ }
726
+ lines.push("");
727
+ lines.push("## Spacing");
728
+ lines.push("");
729
+ const sortedSpacing = [...styles.spacing.entries()].filter(([value]) => value.endsWith("px")).sort((a, b) => parseFloat(a[0]) - parseFloat(b[0]));
730
+ if (sortedSpacing.length > 0) {
731
+ const spacingValues = sortedSpacing.map(([value]) => parseFloat(value));
732
+ const gcd = findGCD(spacingValues.filter((v) => v > 0));
733
+ if (gcd >= 4) {
734
+ lines.push(`- **Base unit**: ${gcd}px`);
735
+ }
736
+ const uniqueSpacing = [...new Set(sortedSpacing.map(([v]) => v))].slice(0, 8);
737
+ lines.push(`- **Common values**: ${uniqueSpacing.join(", ")}`);
738
+ } else {
739
+ lines.push("- No spacing values detected");
740
+ }
741
+ lines.push("");
742
+ lines.push("## Border Radius");
743
+ lines.push("");
744
+ const sortedBorderRadius = [...styles.borderRadius.entries()].filter(([value]) => value !== "0px").sort((a, b) => b[1] - a[1]);
745
+ if (sortedBorderRadius.length > 0) {
746
+ sortedBorderRadius.slice(0, 5).forEach(([value, count]) => {
747
+ lines.push(`- ${value} (${count} occurrences)`);
748
+ });
749
+ } else {
750
+ lines.push("- No border radius values detected");
751
+ }
752
+ lines.push("");
753
+ lines.push("## Components");
754
+ lines.push("");
755
+ lines.push("- **Buttons**: Define button styles here");
756
+ lines.push("- **Cards**: Define card styles here");
757
+ lines.push("- **Inputs**: Define input styles here");
758
+ lines.push("");
759
+ return lines.join("\n");
760
+ }
761
+ function findGCD(numbers) {
762
+ if (numbers.length === 0) return 0;
763
+ if (numbers.length === 1) return numbers[0];
764
+ const gcd = (a, b) => {
765
+ a = Math.abs(Math.round(a));
766
+ b = Math.abs(Math.round(b));
767
+ while (b) {
768
+ const t = b;
769
+ b = a % b;
770
+ a = t;
771
+ }
772
+ return a;
773
+ };
774
+ return numbers.reduce((acc, n) => gcd(acc, n));
775
+ }
776
+ function styleGuideToMarkdown(guide) {
777
+ const lines = [];
778
+ lines.push("# UI Style Guide");
779
+ lines.push("");
780
+ lines.push("## Colors");
781
+ guide.colors.forEach((color) => {
782
+ const usage = color.usage ? ` (${color.usage})` : "";
783
+ lines.push(`- **${color.name}**: ${color.value}${usage}`);
784
+ });
785
+ lines.push("");
786
+ lines.push("## Typography");
787
+ guide.typography.forEach((typo) => {
788
+ const props = [];
789
+ if (typo.fontFamily) props.push(`font-family: "${typo.fontFamily}"`);
790
+ if (typo.fontSize) props.push(`font-size: ${typo.fontSize}`);
791
+ if (typo.fontWeight) props.push(`font-weight: ${typo.fontWeight}`);
792
+ if (typo.lineHeight) props.push(`line-height: ${typo.lineHeight}`);
793
+ lines.push(`- **${typo.element}**: ${props.join(", ")}`);
794
+ });
795
+ lines.push("");
796
+ lines.push("## Spacing");
797
+ guide.spacing.forEach((space) => {
798
+ lines.push(`- **${space.name}**: ${space.value}`);
799
+ });
800
+ lines.push("");
801
+ lines.push("## Components");
802
+ guide.components.forEach((comp) => {
803
+ lines.push(`- **${comp.name}**: ${comp.styles.join(", ")}`);
804
+ });
805
+ return lines.join("\n");
806
+ }
807
+
808
+ // src/validation/validate.ts
809
+ function validateCode(code, styleGuide) {
810
+ const issues = [];
811
+ if (!styleGuide) {
812
+ return {
813
+ valid: true,
814
+ issues: [
815
+ {
816
+ type: "warning",
817
+ message: "No style guide found. Create .uilint/styleguide.md to enable validation."
818
+ }
819
+ ]
820
+ };
821
+ }
822
+ const styleValues = extractStyleValues(styleGuide);
823
+ const codeColors = extractColorsFromCode(code);
824
+ for (const color of codeColors) {
825
+ if (!styleValues.colors.includes(color.toUpperCase())) {
826
+ const similar = findSimilarColor(color, styleValues.colors);
827
+ issues.push({
828
+ type: "warning",
829
+ message: `Color ${color} is not in the style guide`,
830
+ suggestion: similar ? `Consider using ${similar} instead` : `Add ${color} to the style guide if intentional`
831
+ });
832
+ }
833
+ }
834
+ const hardcodedPixels = code.matchAll(/(?:margin|padding|gap)[-:].*?(\d+)px/gi);
835
+ for (const match of hardcodedPixels) {
836
+ const value = parseInt(match[1]);
837
+ if (value % 4 !== 0) {
838
+ issues.push({
839
+ type: "warning",
840
+ message: `Spacing value ${value}px doesn't follow the 4px grid`,
841
+ suggestion: `Use ${Math.round(value / 4) * 4}px instead`
842
+ });
843
+ }
844
+ }
845
+ if (code.includes("style={{") || code.includes("style={")) {
846
+ const inlineStyleCount = (code.match(/style=\{/g) || []).length;
847
+ if (inlineStyleCount > 2) {
848
+ issues.push({
849
+ type: "warning",
850
+ message: `Found ${inlineStyleCount} inline styles. Consider using CSS classes for consistency.`
851
+ });
852
+ }
853
+ }
854
+ return {
855
+ valid: issues.filter((i) => i.type === "error").length === 0,
856
+ issues
857
+ };
858
+ }
859
+ function lintSnippet(code, styleGuide) {
860
+ const issues = [];
861
+ issues.push(...lintBasicPatterns(code));
862
+ if (styleGuide) {
863
+ issues.push(...lintAgainstStyleGuide(code, styleGuide));
864
+ }
865
+ const errorCount = issues.filter((i) => i.severity === "error").length;
866
+ const warningCount = issues.filter((i) => i.severity === "warning").length;
867
+ return {
868
+ issues,
869
+ summary: issues.length === 0 ? "No issues found" : `Found ${errorCount} errors and ${warningCount} warnings`
870
+ };
871
+ }
872
+ function lintBasicPatterns(code) {
873
+ const issues = [];
874
+ const magicNumbers = code.matchAll(
875
+ /(?:width|height|size):\s*(\d+)(?!px|rem|em|%)/g
876
+ );
877
+ for (const match of magicNumbers) {
878
+ issues.push({
879
+ severity: "warning",
880
+ type: "spacing",
881
+ message: `Magic number ${match[1]} found without unit`,
882
+ code: match[0],
883
+ suggestion: `Add a unit like ${match[1]}px or use a design token`
884
+ });
885
+ }
886
+ const hardcodedTailwindColors = code.matchAll(
887
+ /className=["'][^"']*(?:bg|text|border)-\[#[A-Fa-f0-9]+\][^"']*/g
888
+ );
889
+ for (const match of hardcodedTailwindColors) {
890
+ issues.push({
891
+ severity: "warning",
892
+ type: "color",
893
+ message: "Hardcoded color in Tailwind arbitrary value",
894
+ code: match[0],
895
+ suggestion: "Use a color from your Tailwind config or style guide"
896
+ });
897
+ }
898
+ if (code.includes("<img") && !code.includes("alt=")) {
899
+ issues.push({
900
+ severity: "error",
901
+ type: "accessibility",
902
+ message: "Image without alt attribute",
903
+ suggestion: 'Add alt="" for decorative images or descriptive alt text'
904
+ });
905
+ }
906
+ if (code.includes("<button") && !code.match(/<button[^>]*>.*\S.*<\/button>/s)) {
907
+ issues.push({
908
+ severity: "warning",
909
+ type: "accessibility",
910
+ message: "Button may be missing accessible text",
911
+ suggestion: "Ensure button has visible text or aria-label"
912
+ });
913
+ }
914
+ const singleQuotes = (code.match(/className='/g) || []).length;
915
+ const doubleQuotes = (code.match(/className="/g) || []).length;
916
+ if (singleQuotes > 0 && doubleQuotes > 0) {
917
+ issues.push({
918
+ severity: "info",
919
+ type: "component",
920
+ message: "Mixed quote styles in className attributes",
921
+ suggestion: "Use consistent quote style throughout"
922
+ });
923
+ }
924
+ return issues;
925
+ }
926
+ function lintAgainstStyleGuide(code, styleGuide) {
927
+ const issues = [];
928
+ const values = extractStyleValues(styleGuide);
929
+ const codeColors = code.matchAll(/#[A-Fa-f0-9]{6}\b/g);
930
+ for (const match of codeColors) {
931
+ const color = match[0].toUpperCase();
932
+ if (!values.colors.includes(color)) {
933
+ issues.push({
934
+ severity: "warning",
935
+ type: "color",
936
+ message: `Color ${color} not in style guide`,
937
+ code: match[0],
938
+ suggestion: `Allowed colors: ${values.colors.slice(0, 5).join(", ")}${values.colors.length > 5 ? "..." : ""}`
939
+ });
940
+ }
941
+ }
942
+ const spacingValues = code.matchAll(/(?:p|m|gap)-(\d+)/g);
943
+ for (const match of spacingValues) {
944
+ const value = parseInt(match[1]);
945
+ if (value > 12 && value % 4 !== 0) {
946
+ issues.push({
947
+ severity: "info",
948
+ type: "spacing",
949
+ message: `Spacing value ${match[0]} might not align with design system`,
950
+ suggestion: "Consider using standard Tailwind spacing values (1-12, 16, 20, 24...)"
951
+ });
952
+ }
953
+ }
954
+ return issues;
955
+ }
956
+ function extractColorsFromCode(code) {
957
+ const colors = [];
958
+ const hexMatches = code.matchAll(/#[A-Fa-f0-9]{6}\b/g);
959
+ for (const match of hexMatches) {
960
+ colors.push(match[0].toUpperCase());
961
+ }
962
+ const tailwindMatches = code.matchAll(/(?:bg|text|border)-(\w+)-(\d+)/g);
963
+ for (const match of tailwindMatches) {
964
+ colors.push(`tailwind:${match[1]}-${match[2]}`);
965
+ }
966
+ return [...new Set(colors)];
967
+ }
968
+ function findSimilarColor(color, allowedColors) {
969
+ const colorRgb = hexToRgb(color);
970
+ if (!colorRgb) return null;
971
+ let closest = null;
972
+ let closestDistance = Infinity;
973
+ for (const allowed of allowedColors) {
974
+ const allowedRgb = hexToRgb(allowed);
975
+ if (!allowedRgb) continue;
976
+ const distance = Math.sqrt(
977
+ Math.pow(colorRgb.r - allowedRgb.r, 2) + Math.pow(colorRgb.g - allowedRgb.g, 2) + Math.pow(colorRgb.b - allowedRgb.b, 2)
978
+ );
979
+ if (distance < closestDistance && distance < 50) {
980
+ closestDistance = distance;
981
+ closest = allowed;
982
+ }
983
+ }
984
+ return closest;
985
+ }
986
+ function hexToRgb(hex) {
987
+ const match = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
988
+ if (!match) return null;
989
+ return {
990
+ r: parseInt(match[1], 16),
991
+ g: parseInt(match[2], 16),
992
+ b: parseInt(match[3], 16)
993
+ };
994
+ }
995
+
996
+ export {
997
+ buildAnalysisPrompt,
998
+ buildStyleGuidePrompt,
999
+ buildQueryPrompt,
1000
+ buildValidationPrompt,
1001
+ OllamaClient,
1002
+ getOllamaClient,
1003
+ extractStyles,
1004
+ extractStylesFromDOM,
1005
+ serializeStyles,
1006
+ deserializeStyles,
1007
+ createStyleSummary,
1008
+ truncateHTML,
1009
+ createEmptyStyleGuide,
1010
+ validateStyleGuide,
1011
+ mergeStyleGuides,
1012
+ createColorRule,
1013
+ createTypographyRule,
1014
+ createSpacingRule,
1015
+ createComponentRule,
1016
+ parseStyleGuide,
1017
+ parseStyleGuideSections,
1018
+ extractStyleValues,
1019
+ generateStyleGuideFromStyles,
1020
+ styleGuideToMarkdown,
1021
+ validateCode,
1022
+ lintSnippet
1023
+ };
1024
+ //# sourceMappingURL=chunk-B4HNWMXB.js.map