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.d.ts +43 -3
- package/dist/index.js +763 -266
- package/package.json +2 -2
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/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
styles,
|
|
27
|
-
|
|
28
|
-
|
|
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/
|
|
228
|
+
// src/components/ViolationList.tsx
|
|
124
229
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
125
|
-
function
|
|
126
|
-
const {
|
|
127
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
290
|
+
`${violation.elementIds.join("-")}-${index}`
|
|
153
291
|
)) });
|
|
154
292
|
}
|
|
155
|
-
function
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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:
|
|
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__ */
|
|
333
|
+
/* @__PURE__ */ jsxs(
|
|
181
334
|
"div",
|
|
182
335
|
{
|
|
183
336
|
style: {
|
|
184
|
-
display: "
|
|
185
|
-
|
|
186
|
-
|
|
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:
|
|
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:
|
|
396
|
+
children: violation.message
|
|
207
397
|
}
|
|
208
398
|
),
|
|
209
|
-
|
|
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
|
-
|
|
220
|
-
/* @__PURE__ */ jsx("span", { style: { color: "#6B7280" }, children: "
|
|
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:
|
|
418
|
+
children: violation.details.property
|
|
231
419
|
}
|
|
232
420
|
)
|
|
233
421
|
] }),
|
|
234
|
-
|
|
235
|
-
/* @__PURE__ */ jsx("span", { style: { color: "#6B7280" }, children: "
|
|
236
|
-
/* @__PURE__ */
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
679
|
-
const
|
|
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
|
-
|
|
692
|
-
|
|
894
|
+
violationCount,
|
|
895
|
+
hasViolations,
|
|
693
896
|
isScanning
|
|
694
897
|
}
|
|
695
898
|
) });
|
|
696
899
|
}
|
|
697
900
|
function CollapsedButton({
|
|
698
901
|
onClick,
|
|
699
|
-
|
|
700
|
-
|
|
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:
|
|
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: ${
|
|
732
|
-
children: isScanning ? /* @__PURE__ */ jsx3(
|
|
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
|
|
737
|
-
|
|
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__ */
|
|
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:
|
|
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 === "
|
|
820
|
-
onClick: () => setActiveTab("
|
|
821
|
-
children: "
|
|
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 === "
|
|
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/
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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: "
|
|
906
|
-
top: rect.top -
|
|
907
|
-
left: rect.left -
|
|
908
|
-
width: rect.width +
|
|
909
|
-
height: rect.height +
|
|
910
|
-
border:
|
|
911
|
-
borderRadius:
|
|
912
|
-
backgroundColor: `${
|
|
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
|
-
|
|
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: "
|
|
925
|
-
top: rect.top -
|
|
1231
|
+
position: "fixed",
|
|
1232
|
+
top: rect.top - 12,
|
|
926
1233
|
left: rect.left - 4,
|
|
927
|
-
|
|
928
|
-
|
|
1234
|
+
minWidth: 20,
|
|
1235
|
+
height: 20,
|
|
1236
|
+
borderRadius: 10,
|
|
1237
|
+
backgroundColor: HIGHLIGHT_COLOR,
|
|
929
1238
|
color: "white",
|
|
930
|
-
fontSize:
|
|
931
|
-
fontWeight:
|
|
932
|
-
|
|
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:
|
|
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:
|
|
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 [
|
|
1341
|
+
const [violations, setViolations] = useState4([]);
|
|
962
1342
|
const [isScanning, setIsScanning] = useState4(false);
|
|
963
|
-
const [
|
|
964
|
-
const [
|
|
1343
|
+
const [elementCount, setElementCount] = useState4(0);
|
|
1344
|
+
const [selectedViolation, setSelectedViolation] = useState4(
|
|
965
1345
|
null
|
|
966
1346
|
);
|
|
967
|
-
const [
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
987
|
-
setStyleGuideContent(data.content ?? null);
|
|
988
|
-
} catch {
|
|
989
|
-
setStyleGuideExists(false);
|
|
990
|
-
setStyleGuideContent(null);
|
|
991
|
-
}
|
|
1360
|
+
};
|
|
992
1361
|
}, []);
|
|
993
|
-
const scan =
|
|
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 =
|
|
998
|
-
|
|
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
|
-
|
|
1379
|
+
"[UILint] Analysis failed:",
|
|
1380
|
+
errorData.error || response.statusText
|
|
1001
1381
|
);
|
|
1002
|
-
|
|
1382
|
+
setViolations([]);
|
|
1003
1383
|
return;
|
|
1004
1384
|
}
|
|
1005
|
-
const result = await
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
}, [
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
|
1416
|
+
}, [enabled, autoScan, scan]);
|
|
1030
1417
|
const contextValue = {
|
|
1031
|
-
|
|
1418
|
+
violations,
|
|
1032
1419
|
isScanning,
|
|
1033
|
-
|
|
1420
|
+
elementCount,
|
|
1034
1421
|
scan,
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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(
|
|
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
|
};
|