uilint-react 0.1.15 → 0.1.17

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/index.js CHANGED
@@ -6,28 +6,208 @@ import {
6
6
  useContext,
7
7
  useState as useState4,
8
8
  useEffect as useEffect2,
9
- useCallback,
9
+ useCallback as useCallback2,
10
10
  useRef
11
11
  } from "react";
12
12
 
13
- // src/scanner/dom-scanner.ts
14
- import {
15
- extractStylesFromDOM,
16
- createStyleSummary,
17
- serializeStyles,
18
- truncateHTML
19
- } from "uilint-core";
20
- function scanDOM(root) {
21
- const targetRoot = root || document.body;
22
- const styles = extractStylesFromDOM(targetRoot);
23
- const html = targetRoot instanceof Element ? targetRoot.outerHTML : targetRoot.body?.outerHTML || "";
13
+ // src/consistency/snapshot.ts
14
+ var DATA_ELEMENTS_ATTR = "data-elements";
15
+ var SKIP_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "SVG", "NOSCRIPT", "TEMPLATE"]);
16
+ var CONTEXT_ELEMENTS = /* @__PURE__ */ new Set([
17
+ "HEADER",
18
+ "NAV",
19
+ "MAIN",
20
+ "FOOTER",
21
+ "SECTION",
22
+ "ARTICLE",
23
+ "ASIDE"
24
+ ]);
25
+ var CLASS_PATTERNS = {
26
+ button: /\b(btn|button)\b/i,
27
+ card: /\b(card)\b/i,
28
+ input: /\b(input|field|form-control)\b/i,
29
+ link: /\b(link)\b/i
30
+ };
31
+ var elementCounter = 0;
32
+ function cleanupDataElements() {
33
+ const elements = document.querySelectorAll(`[${DATA_ELEMENTS_ATTR}]`);
34
+ elements.forEach((el) => el.removeAttribute(DATA_ELEMENTS_ATTR));
35
+ elementCounter = 0;
36
+ }
37
+ function generateElementId() {
38
+ return `el-${++elementCounter}`;
39
+ }
40
+ function truncateText(text, maxLen = 50) {
41
+ const cleaned = text.trim().replace(/\s+/g, " ");
42
+ if (cleaned.length <= maxLen) return cleaned;
43
+ return cleaned.slice(0, maxLen - 3) + "...";
44
+ }
45
+ function inferRole(el, styles) {
46
+ const ariaRole = el.getAttribute("role");
47
+ if (ariaRole) {
48
+ if (ariaRole === "button") return "button";
49
+ if (ariaRole === "link") return "link";
50
+ if (ariaRole === "textbox" || ariaRole === "searchbox") return "input";
51
+ if (ariaRole === "heading") return "heading";
52
+ }
53
+ const tag = el.tagName.toUpperCase();
54
+ if (tag === "BUTTON") return "button";
55
+ if (tag === "A") return "link";
56
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return "input";
57
+ if (/^H[1-6]$/.test(tag)) return "heading";
58
+ const component = el.getAttribute("data-ui-component");
59
+ if (component) {
60
+ const lower = component.toLowerCase();
61
+ if (lower.includes("button")) return "button";
62
+ if (lower.includes("card")) return "card";
63
+ if (lower.includes("input") || lower.includes("field")) return "input";
64
+ if (lower.includes("link")) return "link";
65
+ }
66
+ const className = el.className;
67
+ if (typeof className === "string") {
68
+ if (CLASS_PATTERNS.button.test(className)) return "button";
69
+ if (CLASS_PATTERNS.card.test(className)) return "card";
70
+ if (CLASS_PATTERNS.input.test(className)) return "input";
71
+ if (CLASS_PATTERNS.link.test(className)) return "link";
72
+ }
73
+ if (isCard(el, styles)) return "card";
74
+ if (isContainer(styles)) return "container";
75
+ const text = el.textContent?.trim() || "";
76
+ if (text.length > 0 && text.length < 200) return "text";
77
+ return "other";
78
+ }
79
+ function isCard(el, styles) {
80
+ if (el.children.length === 0) return false;
81
+ const hasBoxShadow = styles.boxShadow && styles.boxShadow !== "none";
82
+ const hasBg = styles.backgroundColor && styles.backgroundColor !== "transparent" && styles.backgroundColor !== "rgba(0, 0, 0, 0)";
83
+ const hasBorder = styles.border && styles.border !== "none" && !styles.border.includes("0px");
84
+ const hasRadius = styles.borderRadius && styles.borderRadius !== "0px";
85
+ return Boolean(hasBoxShadow || (hasBg || hasBorder) && hasRadius);
86
+ }
87
+ function isContainer(styles) {
88
+ const display = styles.display;
89
+ if (display !== "flex" && display !== "grid") return false;
90
+ const gap = styles.gap;
91
+ if (gap && gap !== "normal" && gap !== "0px") return true;
92
+ const padding = styles.padding;
93
+ if (padding && padding !== "0px") {
94
+ const match = padding.match(/(\d+)px/);
95
+ if (match && parseInt(match[1], 10) > 8) return true;
96
+ }
97
+ return false;
98
+ }
99
+ function buildContext(el) {
100
+ const parts = [];
101
+ let current = el.parentElement;
102
+ while (current && parts.length < 3) {
103
+ const tag = current.tagName.toUpperCase();
104
+ if (CONTEXT_ELEMENTS.has(tag)) {
105
+ parts.unshift(tag.toLowerCase());
106
+ }
107
+ current = current.parentElement;
108
+ }
109
+ return parts.join(" > ") || "root";
110
+ }
111
+ function extractStyles(styles) {
24
112
  return {
25
- html: truncateHTML(html, 5e4),
26
- styles,
27
- elementCount: targetRoot.querySelectorAll("*").length,
28
- timestamp: Date.now()
113
+ fontSize: styles.fontSize || void 0,
114
+ fontWeight: styles.fontWeight || void 0,
115
+ color: styles.color || void 0,
116
+ backgroundColor: styles.backgroundColor === "rgba(0, 0, 0, 0)" ? void 0 : styles.backgroundColor || void 0,
117
+ padding: styles.padding === "0px" ? void 0 : styles.padding || void 0,
118
+ borderRadius: styles.borderRadius === "0px" ? void 0 : styles.borderRadius || void 0,
119
+ border: styles.border === "none" || styles.border?.includes("0px") ? void 0 : styles.border || void 0,
120
+ boxShadow: styles.boxShadow === "none" ? void 0 : styles.boxShadow || void 0,
121
+ gap: styles.gap === "normal" || styles.gap === "0px" ? void 0 : styles.gap || void 0
122
+ };
123
+ }
124
+ function shouldSkip(el) {
125
+ if (SKIP_TAGS.has(el.tagName.toUpperCase())) return true;
126
+ if (el.hasAttribute("data-ui-lint-ignore")) return true;
127
+ if (el.getAttribute("aria-hidden") === "true") return true;
128
+ const styles = window.getComputedStyle(el);
129
+ if (styles.display === "none" || styles.visibility === "hidden") return true;
130
+ return false;
131
+ }
132
+ function snapshotElement(el) {
133
+ if (shouldSkip(el)) return null;
134
+ const styles = window.getComputedStyle(el);
135
+ const role = inferRole(el, styles);
136
+ if (role === "other") return null;
137
+ const id = generateElementId();
138
+ el.setAttribute(DATA_ELEMENTS_ATTR, id);
139
+ const rect = el.getBoundingClientRect();
140
+ const text = el.textContent?.trim().slice(0, 100) || el.getAttribute("aria-label") || "";
141
+ return {
142
+ id,
143
+ tag: el.tagName.toLowerCase(),
144
+ role,
145
+ text: truncateText(text),
146
+ component: el.getAttribute("data-ui-component") || void 0,
147
+ context: buildContext(el),
148
+ styles: extractStyles(styles),
149
+ rect: {
150
+ width: Math.round(rect.width),
151
+ height: Math.round(rect.height)
152
+ }
29
153
  };
30
154
  }
155
+ function createSnapshot(root) {
156
+ cleanupDataElements();
157
+ const targetRoot = root || document.body;
158
+ const snapshot = {
159
+ buttons: [],
160
+ headings: [],
161
+ cards: [],
162
+ links: [],
163
+ inputs: [],
164
+ containers: []
165
+ };
166
+ const walker = document.createTreeWalker(
167
+ targetRoot,
168
+ NodeFilter.SHOW_ELEMENT,
169
+ {
170
+ acceptNode: (node2) => {
171
+ const el = node2;
172
+ if (shouldSkip(el)) return NodeFilter.FILTER_REJECT;
173
+ return NodeFilter.FILTER_ACCEPT;
174
+ }
175
+ }
176
+ );
177
+ let node = walker.currentNode;
178
+ while (node) {
179
+ if (node instanceof Element) {
180
+ const elementSnapshot = snapshotElement(node);
181
+ if (elementSnapshot) {
182
+ switch (elementSnapshot.role) {
183
+ case "button":
184
+ snapshot.buttons.push(elementSnapshot);
185
+ break;
186
+ case "heading":
187
+ snapshot.headings.push(elementSnapshot);
188
+ break;
189
+ case "card":
190
+ snapshot.cards.push(elementSnapshot);
191
+ break;
192
+ case "link":
193
+ snapshot.links.push(elementSnapshot);
194
+ break;
195
+ case "input":
196
+ snapshot.inputs.push(elementSnapshot);
197
+ break;
198
+ case "container":
199
+ snapshot.containers.push(elementSnapshot);
200
+ break;
201
+ }
202
+ }
203
+ }
204
+ node = walker.nextNode();
205
+ }
206
+ return snapshot;
207
+ }
208
+ function getElementBySnapshotId(id) {
209
+ return document.querySelector(`[${DATA_ELEMENTS_ATTR}="${id}"]`);
210
+ }
31
211
 
32
212
  // src/scanner/environment.ts
33
213
  function isBrowser() {
@@ -42,89 +222,38 @@ function isNode() {
42
222
  return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
43
223
  }
44
224
 
45
- // src/analyzer/llm-client.ts
46
- import {
47
- createStyleSummary as createStyleSummary2,
48
- buildAnalysisPrompt,
49
- buildStyleGuidePrompt,
50
- UILINT_DEFAULT_OLLAMA_MODEL
51
- } from "uilint-core";
52
- var DEFAULT_API_ENDPOINT = "/api/uilint/analyze";
53
- var LLMClient = class {
54
- apiEndpoint;
55
- model;
56
- constructor(options = {}) {
57
- this.apiEndpoint = options.apiEndpoint || DEFAULT_API_ENDPOINT;
58
- this.model = options.model || UILINT_DEFAULT_OLLAMA_MODEL;
59
- }
60
- /**
61
- * Analyzes extracted styles and returns issues
62
- */
63
- async analyze(styles, styleGuide) {
64
- const startTime = Date.now();
65
- const styleSummary = createStyleSummary2(styles);
66
- try {
67
- const response = await fetch(this.apiEndpoint, {
68
- method: "POST",
69
- headers: { "Content-Type": "application/json" },
70
- body: JSON.stringify({
71
- styleSummary,
72
- styleGuide,
73
- model: this.model
74
- })
75
- });
76
- if (!response.ok) {
77
- throw new Error(`API request failed: ${response.status}`);
78
- }
79
- const data = await response.json();
80
- return {
81
- issues: data.issues || [],
82
- suggestedStyleGuide: data.suggestedStyleGuide,
83
- analysisTime: Date.now() - startTime
84
- };
85
- } catch (error) {
86
- console.error("[UILint] Analysis failed:", error);
87
- return {
88
- issues: [],
89
- analysisTime: Date.now() - startTime
90
- };
91
- }
92
- }
93
- /**
94
- * Generates a style guide from detected styles
95
- */
96
- async generateStyleGuide(styles) {
97
- const styleSummary = createStyleSummary2(styles);
98
- try {
99
- const response = await fetch(this.apiEndpoint, {
100
- method: "POST",
101
- headers: { "Content-Type": "application/json" },
102
- body: JSON.stringify({
103
- styleSummary,
104
- generateGuide: true,
105
- model: this.model
106
- })
107
- });
108
- if (!response.ok) {
109
- throw new Error(`API request failed: ${response.status}`);
110
- }
111
- const data = await response.json();
112
- return data.styleGuide || null;
113
- } catch (error) {
114
- console.error("[UILint] Style guide generation failed:", error);
115
- return null;
116
- }
117
- }
118
- };
119
-
120
225
  // src/components/Overlay.tsx
121
226
  import { useState as useState2 } from "react";
122
227
 
123
- // src/components/IssueList.tsx
228
+ // src/components/ViolationList.tsx
124
229
  import { jsx, jsxs } from "react/jsx-runtime";
125
- function IssueList() {
126
- const { issues, highlightedIssue, setHighlightedIssue } = useUILint();
127
- if (issues.length === 0) {
230
+ function ViolationList() {
231
+ const {
232
+ violations,
233
+ selectedViolation,
234
+ setSelectedViolation,
235
+ lockedViolation,
236
+ setLockedViolation,
237
+ isScanning
238
+ } = useUILint();
239
+ if (isScanning) {
240
+ return /* @__PURE__ */ jsxs(
241
+ "div",
242
+ {
243
+ style: {
244
+ padding: "32px 16px",
245
+ textAlign: "center",
246
+ color: "#9CA3AF"
247
+ },
248
+ children: [
249
+ /* @__PURE__ */ jsx("div", { style: { fontSize: "32px", marginBottom: "8px" }, children: "\u{1F50D}" }),
250
+ /* @__PURE__ */ jsx("div", { style: { fontSize: "14px" }, children: "Analyzing page..." }),
251
+ /* @__PURE__ */ jsx("div", { style: { fontSize: "12px", marginTop: "4px", color: "#6B7280" }, children: "This may take a moment" })
252
+ ]
253
+ }
254
+ );
255
+ }
256
+ if (violations.length === 0) {
128
257
  return /* @__PURE__ */ jsxs(
129
258
  "div",
130
259
  {
@@ -135,63 +264,124 @@ function IssueList() {
135
264
  },
136
265
  children: [
137
266
  /* @__PURE__ */ jsx("div", { style: { fontSize: "32px", marginBottom: "8px" }, children: "\u2728" }),
138
- /* @__PURE__ */ jsx("div", { style: { fontSize: "14px" }, children: "No issues found" }),
267
+ /* @__PURE__ */ jsx("div", { style: { fontSize: "14px" }, children: "No consistency issues found" }),
139
268
  /* @__PURE__ */ jsx("div", { style: { fontSize: "12px", marginTop: "4px" }, children: 'Click "Scan" to analyze the page' })
140
269
  ]
141
270
  }
142
271
  );
143
272
  }
144
- return /* @__PURE__ */ jsx("div", { style: { padding: "8px" }, children: issues.map((issue) => /* @__PURE__ */ jsx(
145
- IssueCard,
273
+ const handleClick = (violation) => {
274
+ if (lockedViolation?.elementIds.join(",") === violation.elementIds.join(",")) {
275
+ setLockedViolation(null);
276
+ } else {
277
+ setLockedViolation(violation);
278
+ }
279
+ };
280
+ return /* @__PURE__ */ jsx("div", { style: { padding: "8px" }, children: violations.map((violation, index) => /* @__PURE__ */ jsx(
281
+ ViolationCard,
146
282
  {
147
- issue,
148
- isHighlighted: highlightedIssue?.id === issue.id,
149
- onHover: () => setHighlightedIssue(issue),
150
- onLeave: () => setHighlightedIssue(null)
283
+ violation,
284
+ isSelected: selectedViolation?.elementIds.join(",") === violation.elementIds.join(","),
285
+ isLocked: lockedViolation?.elementIds.join(",") === violation.elementIds.join(","),
286
+ onHover: () => setSelectedViolation(violation),
287
+ onLeave: () => setSelectedViolation(null),
288
+ onClick: () => handleClick(violation)
151
289
  },
152
- issue.id
290
+ `${violation.elementIds.join("-")}-${index}`
153
291
  )) });
154
292
  }
155
- function IssueCard({ issue, isHighlighted, onHover, onLeave }) {
156
- const typeColors = {
293
+ function ViolationCard({
294
+ violation,
295
+ isSelected,
296
+ isLocked,
297
+ onHover,
298
+ onLeave,
299
+ onClick
300
+ }) {
301
+ const categoryColors = {
302
+ spacing: "#10B981",
157
303
  color: "#F59E0B",
158
304
  typography: "#8B5CF6",
159
- spacing: "#10B981",
160
- component: "#3B82F6",
161
- responsive: "#EC4899",
162
- accessibility: "#EF4444"
305
+ sizing: "#3B82F6",
306
+ borders: "#06B6D4",
307
+ shadows: "#6B7280"
308
+ };
309
+ const severityIcons = {
310
+ error: "\u2716",
311
+ warning: "\u26A0",
312
+ info: "\u2139"
163
313
  };
164
- const typeColor = typeColors[issue.type] || "#6B7280";
314
+ const categoryColor = categoryColors[violation.category] || "#6B7280";
315
+ const severityIcon = severityIcons[violation.severity] || "\u2022";
316
+ const isHighlighted = isSelected || isLocked;
165
317
  return /* @__PURE__ */ jsxs(
166
318
  "div",
167
319
  {
168
320
  onMouseEnter: onHover,
169
321
  onMouseLeave: onLeave,
322
+ onClick,
170
323
  style: {
171
324
  padding: "12px",
172
325
  marginBottom: "8px",
173
326
  backgroundColor: isHighlighted ? "#374151" : "#111827",
174
327
  borderRadius: "8px",
175
- border: isHighlighted ? "1px solid #4B5563" : "1px solid transparent",
328
+ border: isLocked ? "1px solid #3B82F6" : isSelected ? "1px solid #4B5563" : "1px solid transparent",
176
329
  cursor: "pointer",
177
330
  transition: "all 0.15s"
178
331
  },
179
332
  children: [
180
- /* @__PURE__ */ jsx(
333
+ /* @__PURE__ */ jsxs(
181
334
  "div",
182
335
  {
183
336
  style: {
184
- display: "inline-block",
185
- padding: "2px 8px",
186
- borderRadius: "4px",
187
- backgroundColor: `${typeColor}20`,
188
- color: typeColor,
189
- fontSize: "11px",
190
- fontWeight: "600",
191
- textTransform: "uppercase",
337
+ display: "flex",
338
+ alignItems: "center",
339
+ gap: "8px",
192
340
  marginBottom: "8px"
193
341
  },
194
- children: issue.type
342
+ children: [
343
+ /* @__PURE__ */ jsx(
344
+ "div",
345
+ {
346
+ style: {
347
+ display: "inline-block",
348
+ padding: "2px 8px",
349
+ borderRadius: "4px",
350
+ backgroundColor: `${categoryColor}20`,
351
+ color: categoryColor,
352
+ fontSize: "11px",
353
+ fontWeight: "600",
354
+ textTransform: "uppercase"
355
+ },
356
+ children: violation.category
357
+ }
358
+ ),
359
+ /* @__PURE__ */ jsx(
360
+ "span",
361
+ {
362
+ style: {
363
+ fontSize: "12px",
364
+ color: violation.severity === "error" ? "#EF4444" : violation.severity === "warning" ? "#F59E0B" : "#9CA3AF"
365
+ },
366
+ children: severityIcon
367
+ }
368
+ ),
369
+ /* @__PURE__ */ jsxs(
370
+ "span",
371
+ {
372
+ style: {
373
+ fontSize: "11px",
374
+ color: "#6B7280",
375
+ marginLeft: "auto"
376
+ },
377
+ children: [
378
+ violation.elementIds.length,
379
+ " element",
380
+ violation.elementIds.length !== 1 ? "s" : ""
381
+ ]
382
+ }
383
+ )
384
+ ]
195
385
  }
196
386
  ),
197
387
  /* @__PURE__ */ jsx(
@@ -203,21 +393,19 @@ function IssueCard({ issue, isHighlighted, onHover, onLeave }) {
203
393
  lineHeight: "1.4",
204
394
  marginBottom: "8px"
205
395
  },
206
- children: issue.message
396
+ children: violation.message
207
397
  }
208
398
  ),
209
- (issue.currentValue || issue.expectedValue) && /* @__PURE__ */ jsxs(
399
+ violation.details && /* @__PURE__ */ jsxs(
210
400
  "div",
211
401
  {
212
402
  style: {
213
- display: "flex",
214
- gap: "12px",
215
403
  fontSize: "12px",
216
404
  color: "#9CA3AF"
217
405
  },
218
406
  children: [
219
- issue.currentValue && /* @__PURE__ */ jsxs("div", { children: [
220
- /* @__PURE__ */ jsx("span", { style: { color: "#6B7280" }, children: "Current: " }),
407
+ violation.details.property && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "4px" }, children: [
408
+ /* @__PURE__ */ jsx("span", { style: { color: "#6B7280" }, children: "Property: " }),
221
409
  /* @__PURE__ */ jsx(
222
410
  "code",
223
411
  {
@@ -227,29 +415,32 @@ function IssueCard({ issue, isHighlighted, onHover, onLeave }) {
227
415
  borderRadius: "3px",
228
416
  fontSize: "11px"
229
417
  },
230
- children: issue.currentValue
418
+ children: violation.details.property
231
419
  }
232
420
  )
233
421
  ] }),
234
- issue.expectedValue && /* @__PURE__ */ jsxs("div", { children: [
235
- /* @__PURE__ */ jsx("span", { style: { color: "#6B7280" }, children: "Expected: " }),
236
- /* @__PURE__ */ jsx(
237
- "code",
238
- {
239
- style: {
240
- padding: "2px 4px",
241
- backgroundColor: "#374151",
242
- borderRadius: "3px",
243
- fontSize: "11px"
244
- },
245
- children: issue.expectedValue
246
- }
247
- )
422
+ violation.details.values.length > 0 && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "4px" }, children: [
423
+ /* @__PURE__ */ jsx("span", { style: { color: "#6B7280" }, children: "Values: " }),
424
+ violation.details.values.map((val, i) => /* @__PURE__ */ jsxs("span", { children: [
425
+ /* @__PURE__ */ jsx(
426
+ "code",
427
+ {
428
+ style: {
429
+ padding: "2px 4px",
430
+ backgroundColor: "#374151",
431
+ borderRadius: "3px",
432
+ fontSize: "11px"
433
+ },
434
+ children: val
435
+ }
436
+ ),
437
+ i < violation.details.values.length - 1 && /* @__PURE__ */ jsx("span", { style: { margin: "0 4px", color: "#6B7280" }, children: "vs" })
438
+ ] }, i))
248
439
  ] })
249
440
  ]
250
441
  }
251
442
  ),
252
- issue.suggestion && /* @__PURE__ */ jsxs(
443
+ violation.details.suggestion && /* @__PURE__ */ jsxs(
253
444
  "div",
254
445
  {
255
446
  style: {
@@ -262,9 +453,20 @@ function IssueCard({ issue, isHighlighted, onHover, onLeave }) {
262
453
  },
263
454
  children: [
264
455
  "\u{1F4A1} ",
265
- issue.suggestion
456
+ violation.details.suggestion
266
457
  ]
267
458
  }
459
+ ),
460
+ isLocked && /* @__PURE__ */ jsx(
461
+ "div",
462
+ {
463
+ style: {
464
+ marginTop: "8px",
465
+ fontSize: "11px",
466
+ color: "#3B82F6"
467
+ },
468
+ children: "\u{1F512} Click to unlock"
469
+ }
268
470
  )
269
471
  ]
270
472
  }
@@ -668,36 +870,37 @@ function escapeRegExp(s) {
668
870
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
669
871
  function Overlay({ position }) {
670
872
  const [isExpanded, setIsExpanded] = useState2(false);
671
- const { issues, isScanning, scan } = useUILint();
873
+ const { violations, isScanning, scan, elementCount } = useUILint();
672
874
  const positionStyles = {
673
875
  position: "fixed",
674
876
  zIndex: 99999,
675
877
  ...position.includes("bottom") ? { bottom: "16px" } : { top: "16px" },
676
878
  ...position.includes("left") ? { left: "16px" } : { right: "16px" }
677
879
  };
678
- const issueCount = issues.length;
679
- const hasIssues = issueCount > 0;
880
+ const violationCount = violations.length;
881
+ const hasViolations = violationCount > 0;
680
882
  return /* @__PURE__ */ jsx3("div", { style: positionStyles, children: isExpanded ? /* @__PURE__ */ jsx3(
681
883
  ExpandedPanel,
682
884
  {
683
885
  onCollapse: () => setIsExpanded(false),
684
886
  onScan: scan,
685
- isScanning
887
+ isScanning,
888
+ elementCount
686
889
  }
687
890
  ) : /* @__PURE__ */ jsx3(
688
891
  CollapsedButton,
689
892
  {
690
893
  onClick: () => setIsExpanded(true),
691
- issueCount,
692
- hasIssues,
894
+ violationCount,
895
+ hasViolations,
693
896
  isScanning
694
897
  }
695
898
  ) });
696
899
  }
697
900
  function CollapsedButton({
698
901
  onClick,
699
- issueCount,
700
- hasIssues,
902
+ violationCount,
903
+ hasViolations,
701
904
  isScanning
702
905
  }) {
703
906
  return /* @__PURE__ */ jsx3(
@@ -712,7 +915,7 @@ function CollapsedButton({
712
915
  height: "48px",
713
916
  borderRadius: "50%",
714
917
  border: "none",
715
- backgroundColor: hasIssues ? "#EF4444" : "#10B981",
918
+ backgroundColor: isScanning ? "#3B82F6" : hasViolations ? "#EF4444" : "#10B981",
716
919
  color: "white",
717
920
  cursor: "pointer",
718
921
  boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
@@ -728,13 +931,55 @@ function CollapsedButton({
728
931
  e.currentTarget.style.transform = "scale(1)";
729
932
  e.currentTarget.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.15)";
730
933
  },
731
- title: `UILint: ${issueCount} issues found`,
732
- children: isScanning ? /* @__PURE__ */ jsx3("span", { style: { animation: "spin 1s linear infinite" }, children: "\u27F3" }) : hasIssues ? issueCount : "\u2713"
934
+ title: isScanning ? "Analyzing..." : `UILint: ${violationCount} issue${violationCount !== 1 ? "s" : ""} found`,
935
+ children: isScanning ? /* @__PURE__ */ jsx3(SpinnerIcon, {}) : hasViolations ? violationCount : "\u2713"
733
936
  }
734
937
  );
735
938
  }
736
- function ExpandedPanel({ onCollapse, onScan, isScanning }) {
737
- const [activeTab, setActiveTab] = useState2("issues");
939
+ function SpinnerIcon() {
940
+ return /* @__PURE__ */ jsxs3(
941
+ "svg",
942
+ {
943
+ width: "20",
944
+ height: "20",
945
+ viewBox: "0 0 24 24",
946
+ fill: "none",
947
+ style: {
948
+ animation: "uilint-spin 1s linear infinite"
949
+ },
950
+ children: [
951
+ /* @__PURE__ */ jsx3("style", { children: `
952
+ @keyframes uilint-spin {
953
+ from { transform: rotate(0deg); }
954
+ to { transform: rotate(360deg); }
955
+ }
956
+ ` }),
957
+ /* @__PURE__ */ jsx3(
958
+ "circle",
959
+ {
960
+ cx: "12",
961
+ cy: "12",
962
+ r: "10",
963
+ stroke: "currentColor",
964
+ strokeWidth: "3",
965
+ strokeLinecap: "round",
966
+ strokeDasharray: "31.4 31.4",
967
+ fill: "none"
968
+ }
969
+ )
970
+ ]
971
+ }
972
+ );
973
+ }
974
+ function ExpandedPanel({
975
+ onCollapse,
976
+ onScan,
977
+ isScanning,
978
+ elementCount
979
+ }) {
980
+ const [activeTab, setActiveTab] = useState2(
981
+ "violations"
982
+ );
738
983
  return /* @__PURE__ */ jsxs3(
739
984
  "div",
740
985
  {
@@ -763,10 +1008,25 @@ function ExpandedPanel({ onCollapse, onScan, isScanning }) {
763
1008
  children: [
764
1009
  /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
765
1010
  /* @__PURE__ */ jsx3("span", { style: { fontSize: "16px" }, children: "\u{1F3A8}" }),
766
- /* @__PURE__ */ jsx3("span", { style: { fontWeight: "600", fontSize: "14px" }, children: "UILint" })
1011
+ /* @__PURE__ */ jsx3("span", { style: { fontWeight: "600", fontSize: "14px" }, children: "UILint" }),
1012
+ elementCount > 0 && !isScanning && /* @__PURE__ */ jsxs3(
1013
+ "span",
1014
+ {
1015
+ style: {
1016
+ fontSize: "11px",
1017
+ color: "#6B7280",
1018
+ marginLeft: "4px"
1019
+ },
1020
+ children: [
1021
+ "(",
1022
+ elementCount,
1023
+ " elements)"
1024
+ ]
1025
+ }
1026
+ )
767
1027
  ] }),
768
1028
  /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
769
- /* @__PURE__ */ jsx3(
1029
+ /* @__PURE__ */ jsxs3(
770
1030
  "button",
771
1031
  {
772
1032
  onClick: onScan,
@@ -780,9 +1040,39 @@ function ExpandedPanel({ onCollapse, onScan, isScanning }) {
780
1040
  fontSize: "12px",
781
1041
  fontWeight: "500",
782
1042
  cursor: isScanning ? "not-allowed" : "pointer",
783
- opacity: isScanning ? 0.7 : 1
1043
+ opacity: isScanning ? 0.7 : 1,
1044
+ display: "flex",
1045
+ alignItems: "center",
1046
+ gap: "6px"
784
1047
  },
785
- children: isScanning ? "Scanning..." : "Scan"
1048
+ children: [
1049
+ isScanning && /* @__PURE__ */ jsx3(
1050
+ "svg",
1051
+ {
1052
+ width: "12",
1053
+ height: "12",
1054
+ viewBox: "0 0 24 24",
1055
+ fill: "none",
1056
+ style: {
1057
+ animation: "uilint-spin 1s linear infinite"
1058
+ },
1059
+ children: /* @__PURE__ */ jsx3(
1060
+ "circle",
1061
+ {
1062
+ cx: "12",
1063
+ cy: "12",
1064
+ r: "10",
1065
+ stroke: "currentColor",
1066
+ strokeWidth: "3",
1067
+ strokeLinecap: "round",
1068
+ strokeDasharray: "31.4 31.4",
1069
+ fill: "none"
1070
+ }
1071
+ )
1072
+ }
1073
+ ),
1074
+ isScanning ? "Analyzing..." : "Scan"
1075
+ ]
786
1076
  }
787
1077
  ),
788
1078
  /* @__PURE__ */ jsx3(
@@ -816,9 +1106,9 @@ function ExpandedPanel({ onCollapse, onScan, isScanning }) {
816
1106
  /* @__PURE__ */ jsx3(
817
1107
  TabButton,
818
1108
  {
819
- active: activeTab === "issues",
820
- onClick: () => setActiveTab("issues"),
821
- children: "Issues"
1109
+ active: activeTab === "violations",
1110
+ onClick: () => setActiveTab("violations"),
1111
+ children: "Violations"
822
1112
  }
823
1113
  ),
824
1114
  /* @__PURE__ */ jsx3(
@@ -832,7 +1122,7 @@ function ExpandedPanel({ onCollapse, onScan, isScanning }) {
832
1122
  ]
833
1123
  }
834
1124
  ),
835
- /* @__PURE__ */ jsx3("div", { style: { maxHeight: "380px", overflow: "auto" }, children: activeTab === "issues" ? /* @__PURE__ */ jsx3(IssueList, {}) : /* @__PURE__ */ jsx3(QuestionPanel, {}) })
1125
+ /* @__PURE__ */ jsx3("div", { style: { maxHeight: "380px", overflow: "auto" }, children: activeTab === "violations" ? /* @__PURE__ */ jsx3(ViolationList, {}) : /* @__PURE__ */ jsx3(QuestionPanel, {}) })
836
1126
  ]
837
1127
  }
838
1128
  );
@@ -859,89 +1149,179 @@ function TabButton({ active, onClick, children }) {
859
1149
  );
860
1150
  }
861
1151
 
862
- // src/components/Highlighter.tsx
863
- import { useEffect, useState as useState3 } from "react";
1152
+ // src/consistency/highlights.tsx
1153
+ import { useEffect, useState as useState3, useCallback } from "react";
1154
+ import { createPortal } from "react-dom";
864
1155
  import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
865
- function Highlighter() {
866
- const { highlightedIssue } = useUILint();
867
- const [rect, setRect] = useState3(null);
868
- useEffect(() => {
869
- if (!highlightedIssue?.selector) {
870
- setRect(null);
871
- return;
872
- }
873
- try {
874
- const element = document.querySelector(highlightedIssue.selector);
875
- if (element) {
876
- const domRect = element.getBoundingClientRect();
877
- setRect({
878
- top: domRect.top + window.scrollY,
879
- left: domRect.left + window.scrollX,
880
- width: domRect.width,
881
- height: domRect.height
882
- });
883
- } else {
884
- setRect(null);
1156
+ var HIGHLIGHT_COLOR = "#3b82f6";
1157
+ var DOT_SIZE = 8;
1158
+ var BORDER_WIDTH = 2;
1159
+ function calculateHighlights(elementIds, withBadges = false) {
1160
+ const highlights = [];
1161
+ elementIds.forEach((id, index) => {
1162
+ const el = getElementBySnapshotId(id);
1163
+ if (!el) return;
1164
+ const rect = el.getBoundingClientRect();
1165
+ highlights.push({
1166
+ id,
1167
+ rect: {
1168
+ top: rect.top,
1169
+ left: rect.left,
1170
+ width: rect.width,
1171
+ height: rect.height
1172
+ },
1173
+ badgeNumber: withBadges && elementIds.length > 1 ? index + 1 : void 0
1174
+ });
1175
+ });
1176
+ return highlights;
1177
+ }
1178
+ function getAllViolatingIds(violations) {
1179
+ const ids = /* @__PURE__ */ new Set();
1180
+ violations.forEach((v) => {
1181
+ v.elementIds.forEach((id) => ids.add(id));
1182
+ });
1183
+ return ids;
1184
+ }
1185
+ function OverviewDot({ rect }) {
1186
+ return /* @__PURE__ */ jsx4(
1187
+ "div",
1188
+ {
1189
+ style: {
1190
+ position: "fixed",
1191
+ top: rect.top - DOT_SIZE / 2,
1192
+ left: rect.left + rect.width - DOT_SIZE / 2,
1193
+ width: DOT_SIZE,
1194
+ height: DOT_SIZE,
1195
+ borderRadius: "50%",
1196
+ backgroundColor: HIGHLIGHT_COLOR,
1197
+ pointerEvents: "none",
1198
+ zIndex: 99997,
1199
+ boxShadow: "0 0 4px rgba(59, 130, 246, 0.5)"
885
1200
  }
886
- } catch {
887
- setRect(null);
888
1201
  }
889
- }, [highlightedIssue]);
890
- if (!rect) return null;
891
- const typeColors = {
892
- color: "#F59E0B",
893
- typography: "#8B5CF6",
894
- spacing: "#10B981",
895
- component: "#3B82F6",
896
- responsive: "#EC4899",
897
- accessibility: "#EF4444"
898
- };
899
- const color = highlightedIssue?.type ? typeColors[highlightedIssue.type] || "#EF4444" : "#EF4444";
1202
+ );
1203
+ }
1204
+ function HighlightRect({
1205
+ rect,
1206
+ badgeNumber
1207
+ }) {
900
1208
  return /* @__PURE__ */ jsxs4(Fragment, { children: [
901
1209
  /* @__PURE__ */ jsx4(
902
1210
  "div",
903
1211
  {
904
1212
  style: {
905
- position: "absolute",
906
- top: rect.top - 4,
907
- left: rect.left - 4,
908
- width: rect.width + 8,
909
- height: rect.height + 8,
910
- border: `2px solid ${color}`,
911
- borderRadius: "4px",
912
- backgroundColor: `${color}15`,
1213
+ position: "fixed",
1214
+ top: rect.top - BORDER_WIDTH,
1215
+ left: rect.left - BORDER_WIDTH,
1216
+ width: rect.width + BORDER_WIDTH * 2,
1217
+ height: rect.height + BORDER_WIDTH * 2,
1218
+ border: `${BORDER_WIDTH}px solid ${HIGHLIGHT_COLOR}`,
1219
+ borderRadius: 4,
1220
+ backgroundColor: `${HIGHLIGHT_COLOR}10`,
913
1221
  pointerEvents: "none",
914
1222
  zIndex: 99998,
915
- boxShadow: `0 0 0 4px ${color}30`,
916
- transition: "all 0.2s ease-out"
1223
+ transition: "all 0.15s ease-out"
917
1224
  }
918
1225
  }
919
1226
  ),
920
- /* @__PURE__ */ jsx4(
1227
+ badgeNumber !== void 0 && /* @__PURE__ */ jsx4(
921
1228
  "div",
922
1229
  {
923
1230
  style: {
924
- position: "absolute",
925
- top: rect.top - 28,
1231
+ position: "fixed",
1232
+ top: rect.top - 12,
926
1233
  left: rect.left - 4,
927
- padding: "4px 8px",
928
- backgroundColor: color,
1234
+ minWidth: 20,
1235
+ height: 20,
1236
+ borderRadius: 10,
1237
+ backgroundColor: HIGHLIGHT_COLOR,
929
1238
  color: "white",
930
- fontSize: "11px",
931
- fontWeight: "600",
932
- borderRadius: "4px 4px 0 0",
1239
+ fontSize: 11,
1240
+ fontWeight: 600,
1241
+ display: "flex",
1242
+ alignItems: "center",
1243
+ justifyContent: "center",
1244
+ padding: "0 6px",
933
1245
  pointerEvents: "none",
934
- zIndex: 99998,
935
- fontFamily: "system-ui, -apple-system, sans-serif",
936
- textTransform: "uppercase"
1246
+ zIndex: 99999,
1247
+ fontFamily: "system-ui, -apple-system, sans-serif"
937
1248
  },
938
- children: highlightedIssue?.type || "Issue"
1249
+ children: badgeNumber
939
1250
  }
940
1251
  )
941
1252
  ] });
942
1253
  }
1254
+ function ConsistencyHighlighter({
1255
+ violations,
1256
+ selectedViolation,
1257
+ lockedViolation
1258
+ }) {
1259
+ const [overviewHighlights, setOverviewHighlights] = useState3([]);
1260
+ const [activeHighlights, setActiveHighlights] = useState3(
1261
+ []
1262
+ );
1263
+ const [mounted, setMounted] = useState3(false);
1264
+ const activeViolation = lockedViolation || selectedViolation;
1265
+ const updateOverviewHighlights = useCallback(() => {
1266
+ if (activeViolation) {
1267
+ setOverviewHighlights([]);
1268
+ return;
1269
+ }
1270
+ const allIds = getAllViolatingIds(violations);
1271
+ const highlights = [];
1272
+ allIds.forEach((id) => {
1273
+ const el = getElementBySnapshotId(id);
1274
+ if (!el) return;
1275
+ const rect = el.getBoundingClientRect();
1276
+ highlights.push({
1277
+ id,
1278
+ rect: {
1279
+ top: rect.top,
1280
+ left: rect.left,
1281
+ width: rect.width,
1282
+ height: rect.height
1283
+ }
1284
+ });
1285
+ });
1286
+ setOverviewHighlights(highlights);
1287
+ }, [violations, activeViolation]);
1288
+ const updateActiveHighlights = useCallback(() => {
1289
+ if (!activeViolation) {
1290
+ setActiveHighlights([]);
1291
+ return;
1292
+ }
1293
+ const highlights = calculateHighlights(activeViolation.elementIds, true);
1294
+ setActiveHighlights(highlights);
1295
+ }, [activeViolation]);
1296
+ useEffect(() => {
1297
+ setMounted(true);
1298
+ return () => setMounted(false);
1299
+ }, []);
1300
+ useEffect(() => {
1301
+ updateOverviewHighlights();
1302
+ updateActiveHighlights();
1303
+ const handleUpdate = () => {
1304
+ updateOverviewHighlights();
1305
+ updateActiveHighlights();
1306
+ };
1307
+ window.addEventListener("scroll", handleUpdate, true);
1308
+ window.addEventListener("resize", handleUpdate);
1309
+ return () => {
1310
+ window.removeEventListener("scroll", handleUpdate, true);
1311
+ window.removeEventListener("resize", handleUpdate);
1312
+ };
1313
+ }, [updateOverviewHighlights, updateActiveHighlights]);
1314
+ if (!mounted) return null;
1315
+ if (violations.length === 0) return null;
1316
+ const content = /* @__PURE__ */ jsxs4(Fragment, { children: [
1317
+ !activeViolation && overviewHighlights.map((h) => /* @__PURE__ */ jsx4(OverviewDot, { rect: h.rect }, h.id)),
1318
+ activeViolation && activeHighlights.map((h) => /* @__PURE__ */ jsx4(HighlightRect, { rect: h.rect, badgeNumber: h.badgeNumber }, h.id))
1319
+ ] });
1320
+ return createPortal(content, document.body);
1321
+ }
943
1322
 
944
1323
  // src/components/UILint.tsx
1324
+ import { countElements } from "uilint-core";
945
1325
  import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
946
1326
  var UILintContext = createContext(null);
947
1327
  function useUILint() {
@@ -956,117 +1336,234 @@ function UILint({
956
1336
  enabled = true,
957
1337
  position = "bottom-left",
958
1338
  autoScan = false,
959
- apiEndpoint
1339
+ apiEndpoint = "/api/uilint/consistency"
960
1340
  }) {
961
- const [issues, setIssues] = useState4([]);
1341
+ const [violations, setViolations] = useState4([]);
962
1342
  const [isScanning, setIsScanning] = useState4(false);
963
- const [styleGuideExists, setStyleGuideExists] = useState4(false);
964
- const [styleGuideContent, setStyleGuideContent] = useState4(
1343
+ const [elementCount, setElementCount] = useState4(0);
1344
+ const [selectedViolation, setSelectedViolation] = useState4(
965
1345
  null
966
1346
  );
967
- const [highlightedIssue, setHighlightedIssue] = useState4(
1347
+ const [lockedViolation, setLockedViolation] = useState4(
968
1348
  null
969
1349
  );
970
1350
  const [isMounted, setIsMounted] = useState4(false);
971
- const llmClient = useRef(new LLMClient({ apiEndpoint }));
972
1351
  const hasInitialized = useRef(false);
973
1352
  useEffect2(() => {
974
1353
  setIsMounted(true);
975
1354
  }, []);
976
- const checkStyleGuide = useCallback(async () => {
977
- if (!isBrowser()) return;
978
- try {
979
- const response = await fetch("/api/uilint/styleguide");
980
- const data = await response.json().catch(() => ({}));
981
- if (!response.ok) {
982
- setStyleGuideExists(false);
983
- setStyleGuideContent(null);
984
- return;
1355
+ useEffect2(() => {
1356
+ return () => {
1357
+ if (isBrowser()) {
1358
+ cleanupDataElements();
985
1359
  }
986
- setStyleGuideExists(!!data.exists);
987
- setStyleGuideContent(data.content ?? null);
988
- } catch {
989
- setStyleGuideExists(false);
990
- setStyleGuideContent(null);
991
- }
1360
+ };
992
1361
  }, []);
993
- const scan = useCallback(async () => {
1362
+ const scan = useCallback2(async () => {
994
1363
  if (!isBrowser()) return;
995
1364
  setIsScanning(true);
1365
+ setSelectedViolation(null);
1366
+ setLockedViolation(null);
996
1367
  try {
997
- const snapshot = scanDOM(document.body);
998
- if (!styleGuideContent) {
1368
+ const snapshot = createSnapshot(document.body);
1369
+ const count = countElements(snapshot);
1370
+ setElementCount(count);
1371
+ const response = await fetch(apiEndpoint, {
1372
+ method: "POST",
1373
+ headers: { "Content-Type": "application/json" },
1374
+ body: JSON.stringify({ snapshot })
1375
+ });
1376
+ if (!response.ok) {
1377
+ const errorData = await response.json().catch(() => ({}));
999
1378
  console.error(
1000
- '[UILint] No style guide found. Create ".uilint/styleguide.md" at your workspace root.'
1379
+ "[UILint] Analysis failed:",
1380
+ errorData.error || response.statusText
1001
1381
  );
1002
- setIssues([]);
1382
+ setViolations([]);
1003
1383
  return;
1004
1384
  }
1005
- const result = await llmClient.current.analyze(
1006
- snapshot.styles,
1007
- styleGuideContent
1008
- );
1009
- setIssues(result.issues);
1385
+ const result = await response.json();
1386
+ setViolations(result.violations);
1387
+ if (result.violations.length === 0) {
1388
+ console.log(`[UILint] No consistency issues found (${count} elements)`);
1389
+ } else {
1390
+ console.log(
1391
+ `[UILint] Found ${result.violations.length} consistency issue(s)`
1392
+ );
1393
+ }
1010
1394
  } catch (error) {
1011
1395
  console.error("[UILint] Scan failed:", error);
1396
+ setViolations([]);
1012
1397
  } finally {
1013
1398
  setIsScanning(false);
1014
1399
  }
1015
- }, [styleGuideContent]);
1016
- const clearIssues = useCallback(() => {
1017
- setIssues([]);
1018
- setHighlightedIssue(null);
1400
+ }, [apiEndpoint]);
1401
+ const clearViolations = useCallback2(() => {
1402
+ setViolations([]);
1403
+ setSelectedViolation(null);
1404
+ setLockedViolation(null);
1405
+ cleanupDataElements();
1406
+ setElementCount(0);
1019
1407
  }, []);
1020
1408
  useEffect2(() => {
1021
1409
  if (!enabled || hasInitialized.current) return;
1022
1410
  hasInitialized.current = true;
1023
1411
  if (!isBrowser()) return;
1024
- checkStyleGuide();
1025
1412
  if (autoScan) {
1026
1413
  const timer = setTimeout(scan, 1e3);
1027
1414
  return () => clearTimeout(timer);
1028
1415
  }
1029
- }, [enabled, autoScan, scan, checkStyleGuide]);
1416
+ }, [enabled, autoScan, scan]);
1030
1417
  const contextValue = {
1031
- issues,
1418
+ violations,
1032
1419
  isScanning,
1033
- styleGuideExists,
1420
+ elementCount,
1034
1421
  scan,
1035
- clearIssues,
1036
- highlightedIssue,
1037
- setHighlightedIssue
1422
+ clearViolations,
1423
+ selectedViolation,
1424
+ setSelectedViolation,
1425
+ lockedViolation,
1426
+ setLockedViolation
1038
1427
  };
1039
1428
  const shouldRenderOverlay = enabled && isMounted;
1040
1429
  return /* @__PURE__ */ jsxs5(UILintContext.Provider, { value: contextValue, children: [
1041
1430
  children,
1042
1431
  shouldRenderOverlay && /* @__PURE__ */ jsxs5(Fragment2, { children: [
1043
1432
  /* @__PURE__ */ jsx5(Overlay, { position }),
1044
- /* @__PURE__ */ jsx5(Highlighter, {})
1433
+ /* @__PURE__ */ jsx5(
1434
+ ConsistencyHighlighter,
1435
+ {
1436
+ violations,
1437
+ selectedViolation,
1438
+ lockedViolation
1439
+ }
1440
+ )
1045
1441
  ] })
1046
1442
  ] });
1047
1443
  }
1048
1444
 
1445
+ // src/scanner/dom-scanner.ts
1446
+ import {
1447
+ extractStylesFromDOM,
1448
+ createStyleSummary,
1449
+ serializeStyles,
1450
+ truncateHTML
1451
+ } from "uilint-core";
1452
+ function scanDOM(root) {
1453
+ const targetRoot = root || document.body;
1454
+ const styles = extractStylesFromDOM(targetRoot);
1455
+ const html = targetRoot instanceof Element ? targetRoot.outerHTML : targetRoot.body?.outerHTML || "";
1456
+ return {
1457
+ html: truncateHTML(html, 5e4),
1458
+ styles,
1459
+ elementCount: targetRoot.querySelectorAll("*").length,
1460
+ timestamp: Date.now()
1461
+ };
1462
+ }
1463
+
1049
1464
  // src/index.ts
1050
1465
  import {
1051
1466
  extractStylesFromDOM as extractStylesFromDOM2,
1052
1467
  serializeStyles as serializeStyles2,
1053
1468
  createStyleSummary as createStyleSummary3
1054
1469
  } from "uilint-core";
1470
+
1471
+ // src/analyzer/llm-client.ts
1472
+ import {
1473
+ createStyleSummary as createStyleSummary2,
1474
+ buildAnalysisPrompt,
1475
+ buildStyleGuidePrompt,
1476
+ UILINT_DEFAULT_OLLAMA_MODEL
1477
+ } from "uilint-core";
1478
+ var DEFAULT_API_ENDPOINT = "/api/uilint/analyze";
1479
+ var LLMClient = class {
1480
+ apiEndpoint;
1481
+ model;
1482
+ constructor(options = {}) {
1483
+ this.apiEndpoint = options.apiEndpoint || DEFAULT_API_ENDPOINT;
1484
+ this.model = options.model || UILINT_DEFAULT_OLLAMA_MODEL;
1485
+ }
1486
+ /**
1487
+ * Analyzes extracted styles and returns issues
1488
+ */
1489
+ async analyze(styles, styleGuide) {
1490
+ const startTime = Date.now();
1491
+ const styleSummary = createStyleSummary2(styles);
1492
+ try {
1493
+ const response = await fetch(this.apiEndpoint, {
1494
+ method: "POST",
1495
+ headers: { "Content-Type": "application/json" },
1496
+ body: JSON.stringify({
1497
+ styleSummary,
1498
+ styleGuide,
1499
+ model: this.model
1500
+ })
1501
+ });
1502
+ if (!response.ok) {
1503
+ throw new Error(`API request failed: ${response.status}`);
1504
+ }
1505
+ const data = await response.json();
1506
+ return {
1507
+ issues: data.issues || [],
1508
+ suggestedStyleGuide: data.suggestedStyleGuide,
1509
+ analysisTime: Date.now() - startTime
1510
+ };
1511
+ } catch (error) {
1512
+ console.error("[UILint] Analysis failed:", error);
1513
+ return {
1514
+ issues: [],
1515
+ analysisTime: Date.now() - startTime
1516
+ };
1517
+ }
1518
+ }
1519
+ /**
1520
+ * Generates a style guide from detected styles
1521
+ */
1522
+ async generateStyleGuide(styles) {
1523
+ const styleSummary = createStyleSummary2(styles);
1524
+ try {
1525
+ const response = await fetch(this.apiEndpoint, {
1526
+ method: "POST",
1527
+ headers: { "Content-Type": "application/json" },
1528
+ body: JSON.stringify({
1529
+ styleSummary,
1530
+ generateGuide: true,
1531
+ model: this.model
1532
+ })
1533
+ });
1534
+ if (!response.ok) {
1535
+ throw new Error(`API request failed: ${response.status}`);
1536
+ }
1537
+ const data = await response.json();
1538
+ return data.styleGuide || null;
1539
+ } catch (error) {
1540
+ console.error("[UILint] Style guide generation failed:", error);
1541
+ return null;
1542
+ }
1543
+ }
1544
+ };
1545
+
1546
+ // src/index.ts
1055
1547
  import { parseStyleGuide } from "uilint-core";
1056
1548
  import { generateStyleGuideFromStyles } from "uilint-core";
1057
1549
  import { createEmptyStyleGuide, mergeStyleGuides } from "uilint-core";
1058
1550
  export {
1551
+ ConsistencyHighlighter,
1059
1552
  LLMClient,
1060
1553
  UILint,
1554
+ cleanupDataElements,
1061
1555
  createEmptyStyleGuide,
1556
+ createSnapshot,
1062
1557
  createStyleSummary3 as createStyleSummary,
1063
1558
  extractStylesFromDOM2 as extractStylesFromDOM,
1064
1559
  generateStyleGuideFromStyles as generateStyleGuide,
1560
+ getElementBySnapshotId,
1065
1561
  isBrowser,
1066
1562
  isJSDOM,
1067
1563
  isNode,
1068
1564
  mergeStyleGuides,
1069
1565
  parseStyleGuide,
1070
1566
  scanDOM,
1071
- serializeStyles2 as serializeStyles
1567
+ serializeStyles2 as serializeStyles,
1568
+ useUILint
1072
1569
  };