uilint-react 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.
- package/dist/index.d.ts +67 -0
- package/dist/index.js +1074 -0
- package/dist/node.d.ts +40 -0
- package/dist/node.js +111 -0
- package/package.json +59 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { DOMSnapshot, ExtractedStyles, AnalysisResult } from 'uilint-core';
|
|
4
|
+
export { AnalysisResult, DOMSnapshot, ExtractedStyles, SerializedStyles, StyleGuide, UILintIssue, createEmptyStyleGuide, createStyleSummary, extractStylesFromDOM, generateStyleGuideFromStyles as generateStyleGuide, mergeStyleGuides, parseStyleGuide, serializeStyles } from 'uilint-core';
|
|
5
|
+
|
|
6
|
+
interface UILintProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
position?: "bottom-left" | "bottom-right" | "top-left" | "top-right";
|
|
10
|
+
autoScan?: boolean;
|
|
11
|
+
apiEndpoint?: string;
|
|
12
|
+
}
|
|
13
|
+
declare function UILint({ children, enabled, position, autoScan, apiEndpoint, }: UILintProps): react_jsx_runtime.JSX.Element;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* DOM scanner for browser environment
|
|
17
|
+
* Re-exports and wraps uilint-core functions for browser use
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Scans the DOM and extracts a snapshot of all styles
|
|
22
|
+
*/
|
|
23
|
+
declare function scanDOM(root?: Element | Document): DOMSnapshot;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Environment detection utilities
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Checks if we're running in a browser environment
|
|
30
|
+
*/
|
|
31
|
+
declare function isBrowser(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Checks if we're running in a JSDOM environment (tests)
|
|
34
|
+
*/
|
|
35
|
+
declare function isJSDOM(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Checks if we're running in Node.js
|
|
38
|
+
*/
|
|
39
|
+
declare function isNode(): boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* LLM client for browser environment
|
|
43
|
+
* Uses uilint-core for prompts and wraps API calls
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
interface LLMClientOptions {
|
|
47
|
+
apiEndpoint?: string;
|
|
48
|
+
model?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Client for communicating with the LLM via API route (browser environment)
|
|
52
|
+
*/
|
|
53
|
+
declare class LLMClient {
|
|
54
|
+
private apiEndpoint;
|
|
55
|
+
private model;
|
|
56
|
+
constructor(options?: LLMClientOptions);
|
|
57
|
+
/**
|
|
58
|
+
* Analyzes extracted styles and returns issues
|
|
59
|
+
*/
|
|
60
|
+
analyze(styles: ExtractedStyles, styleGuide: string | null): Promise<AnalysisResult>;
|
|
61
|
+
/**
|
|
62
|
+
* Generates a style guide from detected styles
|
|
63
|
+
*/
|
|
64
|
+
generateStyleGuide(styles: ExtractedStyles): Promise<string | null>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { LLMClient, UILint, type UILintProps, isBrowser, isJSDOM, isNode, scanDOM };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/UILint.tsx
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
7
|
+
useState as useState4,
|
|
8
|
+
useEffect as useEffect2,
|
|
9
|
+
useCallback,
|
|
10
|
+
useRef
|
|
11
|
+
} from "react";
|
|
12
|
+
import { generateStyleGuideFromStyles as generateStyleGuide } from "uilint-core";
|
|
13
|
+
|
|
14
|
+
// src/scanner/dom-scanner.ts
|
|
15
|
+
import {
|
|
16
|
+
extractStylesFromDOM,
|
|
17
|
+
createStyleSummary,
|
|
18
|
+
serializeStyles,
|
|
19
|
+
truncateHTML
|
|
20
|
+
} from "uilint-core";
|
|
21
|
+
function scanDOM(root) {
|
|
22
|
+
const targetRoot = root || document.body;
|
|
23
|
+
const styles = extractStylesFromDOM(targetRoot);
|
|
24
|
+
const html = targetRoot instanceof Element ? targetRoot.outerHTML : targetRoot.body?.outerHTML || "";
|
|
25
|
+
return {
|
|
26
|
+
html: truncateHTML(html, 5e4),
|
|
27
|
+
styles,
|
|
28
|
+
elementCount: targetRoot.querySelectorAll("*").length,
|
|
29
|
+
timestamp: Date.now()
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/scanner/environment.ts
|
|
34
|
+
function isBrowser() {
|
|
35
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
36
|
+
}
|
|
37
|
+
function isJSDOM() {
|
|
38
|
+
if (!isBrowser()) return false;
|
|
39
|
+
const userAgent = window.navigator?.userAgent || "";
|
|
40
|
+
return userAgent.includes("jsdom");
|
|
41
|
+
}
|
|
42
|
+
function isNode() {
|
|
43
|
+
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/analyzer/llm-client.ts
|
|
47
|
+
import {
|
|
48
|
+
createStyleSummary as createStyleSummary2,
|
|
49
|
+
buildAnalysisPrompt,
|
|
50
|
+
buildStyleGuidePrompt
|
|
51
|
+
} from "uilint-core";
|
|
52
|
+
var DEFAULT_API_ENDPOINT = "/api/uilint/analyze";
|
|
53
|
+
var DEFAULT_MODEL = "qwen2.5-coder:7b";
|
|
54
|
+
var LLMClient = class {
|
|
55
|
+
apiEndpoint;
|
|
56
|
+
model;
|
|
57
|
+
constructor(options = {}) {
|
|
58
|
+
this.apiEndpoint = options.apiEndpoint || DEFAULT_API_ENDPOINT;
|
|
59
|
+
this.model = options.model || DEFAULT_MODEL;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Analyzes extracted styles and returns issues
|
|
63
|
+
*/
|
|
64
|
+
async analyze(styles, styleGuide) {
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
const styleSummary = createStyleSummary2(styles);
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(this.apiEndpoint, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
styleSummary,
|
|
73
|
+
styleGuide,
|
|
74
|
+
model: this.model
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(`API request failed: ${response.status}`);
|
|
79
|
+
}
|
|
80
|
+
const data = await response.json();
|
|
81
|
+
return {
|
|
82
|
+
issues: data.issues || [],
|
|
83
|
+
suggestedStyleGuide: data.suggestedStyleGuide,
|
|
84
|
+
analysisTime: Date.now() - startTime
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error("[UILint] Analysis failed:", error);
|
|
88
|
+
return {
|
|
89
|
+
issues: [],
|
|
90
|
+
analysisTime: Date.now() - startTime
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Generates a style guide from detected styles
|
|
96
|
+
*/
|
|
97
|
+
async generateStyleGuide(styles) {
|
|
98
|
+
const styleSummary = createStyleSummary2(styles);
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(this.apiEndpoint, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
styleSummary,
|
|
105
|
+
generateGuide: true,
|
|
106
|
+
model: this.model
|
|
107
|
+
})
|
|
108
|
+
});
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw new Error(`API request failed: ${response.status}`);
|
|
111
|
+
}
|
|
112
|
+
const data = await response.json();
|
|
113
|
+
return data.styleGuide || null;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("[UILint] Style guide generation failed:", error);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/components/Overlay.tsx
|
|
122
|
+
import { useState as useState2 } from "react";
|
|
123
|
+
|
|
124
|
+
// src/components/IssueList.tsx
|
|
125
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
126
|
+
function IssueList() {
|
|
127
|
+
const { issues, highlightedIssue, setHighlightedIssue } = useUILint();
|
|
128
|
+
if (issues.length === 0) {
|
|
129
|
+
return /* @__PURE__ */ jsxs(
|
|
130
|
+
"div",
|
|
131
|
+
{
|
|
132
|
+
style: {
|
|
133
|
+
padding: "32px 16px",
|
|
134
|
+
textAlign: "center",
|
|
135
|
+
color: "#9CA3AF"
|
|
136
|
+
},
|
|
137
|
+
children: [
|
|
138
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: "32px", marginBottom: "8px" }, children: "\u2728" }),
|
|
139
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: "14px" }, children: "No issues found" }),
|
|
140
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: "12px", marginTop: "4px" }, children: 'Click "Scan" to analyze the page' })
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return /* @__PURE__ */ jsx("div", { style: { padding: "8px" }, children: issues.map((issue) => /* @__PURE__ */ jsx(
|
|
146
|
+
IssueCard,
|
|
147
|
+
{
|
|
148
|
+
issue,
|
|
149
|
+
isHighlighted: highlightedIssue?.id === issue.id,
|
|
150
|
+
onHover: () => setHighlightedIssue(issue),
|
|
151
|
+
onLeave: () => setHighlightedIssue(null)
|
|
152
|
+
},
|
|
153
|
+
issue.id
|
|
154
|
+
)) });
|
|
155
|
+
}
|
|
156
|
+
function IssueCard({ issue, isHighlighted, onHover, onLeave }) {
|
|
157
|
+
const typeColors = {
|
|
158
|
+
color: "#F59E0B",
|
|
159
|
+
typography: "#8B5CF6",
|
|
160
|
+
spacing: "#10B981",
|
|
161
|
+
component: "#3B82F6",
|
|
162
|
+
responsive: "#EC4899",
|
|
163
|
+
accessibility: "#EF4444"
|
|
164
|
+
};
|
|
165
|
+
const typeColor = typeColors[issue.type] || "#6B7280";
|
|
166
|
+
return /* @__PURE__ */ jsxs(
|
|
167
|
+
"div",
|
|
168
|
+
{
|
|
169
|
+
onMouseEnter: onHover,
|
|
170
|
+
onMouseLeave: onLeave,
|
|
171
|
+
style: {
|
|
172
|
+
padding: "12px",
|
|
173
|
+
marginBottom: "8px",
|
|
174
|
+
backgroundColor: isHighlighted ? "#374151" : "#111827",
|
|
175
|
+
borderRadius: "8px",
|
|
176
|
+
border: isHighlighted ? "1px solid #4B5563" : "1px solid transparent",
|
|
177
|
+
cursor: "pointer",
|
|
178
|
+
transition: "all 0.15s"
|
|
179
|
+
},
|
|
180
|
+
children: [
|
|
181
|
+
/* @__PURE__ */ jsx(
|
|
182
|
+
"div",
|
|
183
|
+
{
|
|
184
|
+
style: {
|
|
185
|
+
display: "inline-block",
|
|
186
|
+
padding: "2px 8px",
|
|
187
|
+
borderRadius: "4px",
|
|
188
|
+
backgroundColor: `${typeColor}20`,
|
|
189
|
+
color: typeColor,
|
|
190
|
+
fontSize: "11px",
|
|
191
|
+
fontWeight: "600",
|
|
192
|
+
textTransform: "uppercase",
|
|
193
|
+
marginBottom: "8px"
|
|
194
|
+
},
|
|
195
|
+
children: issue.type
|
|
196
|
+
}
|
|
197
|
+
),
|
|
198
|
+
/* @__PURE__ */ jsx(
|
|
199
|
+
"div",
|
|
200
|
+
{
|
|
201
|
+
style: {
|
|
202
|
+
fontSize: "13px",
|
|
203
|
+
color: "#F3F4F6",
|
|
204
|
+
lineHeight: "1.4",
|
|
205
|
+
marginBottom: "8px"
|
|
206
|
+
},
|
|
207
|
+
children: issue.message
|
|
208
|
+
}
|
|
209
|
+
),
|
|
210
|
+
(issue.currentValue || issue.expectedValue) && /* @__PURE__ */ jsxs(
|
|
211
|
+
"div",
|
|
212
|
+
{
|
|
213
|
+
style: {
|
|
214
|
+
display: "flex",
|
|
215
|
+
gap: "12px",
|
|
216
|
+
fontSize: "12px",
|
|
217
|
+
color: "#9CA3AF"
|
|
218
|
+
},
|
|
219
|
+
children: [
|
|
220
|
+
issue.currentValue && /* @__PURE__ */ jsxs("div", { children: [
|
|
221
|
+
/* @__PURE__ */ jsx("span", { style: { color: "#6B7280" }, children: "Current: " }),
|
|
222
|
+
/* @__PURE__ */ jsx(
|
|
223
|
+
"code",
|
|
224
|
+
{
|
|
225
|
+
style: {
|
|
226
|
+
padding: "2px 4px",
|
|
227
|
+
backgroundColor: "#374151",
|
|
228
|
+
borderRadius: "3px",
|
|
229
|
+
fontSize: "11px"
|
|
230
|
+
},
|
|
231
|
+
children: issue.currentValue
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
] }),
|
|
235
|
+
issue.expectedValue && /* @__PURE__ */ jsxs("div", { children: [
|
|
236
|
+
/* @__PURE__ */ jsx("span", { style: { color: "#6B7280" }, children: "Expected: " }),
|
|
237
|
+
/* @__PURE__ */ jsx(
|
|
238
|
+
"code",
|
|
239
|
+
{
|
|
240
|
+
style: {
|
|
241
|
+
padding: "2px 4px",
|
|
242
|
+
backgroundColor: "#374151",
|
|
243
|
+
borderRadius: "3px",
|
|
244
|
+
fontSize: "11px"
|
|
245
|
+
},
|
|
246
|
+
children: issue.expectedValue
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
] })
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
),
|
|
253
|
+
issue.suggestion && /* @__PURE__ */ jsxs(
|
|
254
|
+
"div",
|
|
255
|
+
{
|
|
256
|
+
style: {
|
|
257
|
+
marginTop: "8px",
|
|
258
|
+
padding: "8px",
|
|
259
|
+
backgroundColor: "#1E3A5F",
|
|
260
|
+
borderRadius: "4px",
|
|
261
|
+
fontSize: "12px",
|
|
262
|
+
color: "#93C5FD"
|
|
263
|
+
},
|
|
264
|
+
children: [
|
|
265
|
+
"\u{1F4A1} ",
|
|
266
|
+
issue.suggestion
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/components/QuestionPanel.tsx
|
|
276
|
+
import { useState } from "react";
|
|
277
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
278
|
+
function QuestionPanel() {
|
|
279
|
+
const { issues } = useUILint();
|
|
280
|
+
const [answers, setAnswers] = useState({});
|
|
281
|
+
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
|
282
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
283
|
+
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
284
|
+
const questions = generateQuestionsFromIssues(issues);
|
|
285
|
+
if (questions.length === 0) {
|
|
286
|
+
return /* @__PURE__ */ jsxs2(
|
|
287
|
+
"div",
|
|
288
|
+
{
|
|
289
|
+
style: {
|
|
290
|
+
padding: "32px 16px",
|
|
291
|
+
textAlign: "center",
|
|
292
|
+
color: "#9CA3AF"
|
|
293
|
+
},
|
|
294
|
+
children: [
|
|
295
|
+
/* @__PURE__ */ jsx2("div", { style: { fontSize: "32px", marginBottom: "8px" }, children: "\u{1F3AF}" }),
|
|
296
|
+
/* @__PURE__ */ jsx2("div", { style: { fontSize: "14px" }, children: "No style conflicts to resolve" }),
|
|
297
|
+
/* @__PURE__ */ jsx2("div", { style: { fontSize: "12px", marginTop: "4px" }, children: "Scan the page to detect inconsistencies" })
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const currentQuestion = questions[currentQuestionIndex];
|
|
303
|
+
const handleAnswer = (value) => {
|
|
304
|
+
setAnswers((prev) => ({
|
|
305
|
+
...prev,
|
|
306
|
+
[currentQuestion.id]: value
|
|
307
|
+
}));
|
|
308
|
+
if (currentQuestionIndex < questions.length - 1) {
|
|
309
|
+
setCurrentQuestionIndex((prev) => prev + 1);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const handleSaveToStyleGuide = async () => {
|
|
313
|
+
console.log("[UILint] Saving preferences:", answers);
|
|
314
|
+
setIsSaving(true);
|
|
315
|
+
setSaveSuccess(false);
|
|
316
|
+
try {
|
|
317
|
+
const getResponse = await fetch("/api/uilint/styleguide");
|
|
318
|
+
const { exists, content: existingContent } = await getResponse.json();
|
|
319
|
+
const updatedContent = buildUpdatedStyleGuide(
|
|
320
|
+
exists ? existingContent : null,
|
|
321
|
+
answers
|
|
322
|
+
);
|
|
323
|
+
const postResponse = await fetch("/api/uilint/styleguide", {
|
|
324
|
+
method: "POST",
|
|
325
|
+
headers: { "Content-Type": "application/json" },
|
|
326
|
+
body: JSON.stringify({ content: updatedContent })
|
|
327
|
+
});
|
|
328
|
+
if (!postResponse.ok) {
|
|
329
|
+
throw new Error("Failed to save style guide");
|
|
330
|
+
}
|
|
331
|
+
console.log("[UILint] Style guide saved successfully!");
|
|
332
|
+
setSaveSuccess(true);
|
|
333
|
+
setTimeout(() => {
|
|
334
|
+
setAnswers({});
|
|
335
|
+
setCurrentQuestionIndex(0);
|
|
336
|
+
setSaveSuccess(false);
|
|
337
|
+
}, 1500);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error("[UILint] Error saving style guide:", error);
|
|
340
|
+
} finally {
|
|
341
|
+
setIsSaving(false);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const isComplete = Object.keys(answers).length === questions.length;
|
|
345
|
+
return /* @__PURE__ */ jsxs2("div", { style: { padding: "16px" }, children: [
|
|
346
|
+
/* @__PURE__ */ jsxs2(
|
|
347
|
+
"div",
|
|
348
|
+
{
|
|
349
|
+
style: {
|
|
350
|
+
display: "flex",
|
|
351
|
+
justifyContent: "space-between",
|
|
352
|
+
alignItems: "center",
|
|
353
|
+
marginBottom: "16px"
|
|
354
|
+
},
|
|
355
|
+
children: [
|
|
356
|
+
/* @__PURE__ */ jsxs2("span", { style: { fontSize: "12px", color: "#9CA3AF" }, children: [
|
|
357
|
+
"Question ",
|
|
358
|
+
currentQuestionIndex + 1,
|
|
359
|
+
" of ",
|
|
360
|
+
questions.length
|
|
361
|
+
] }),
|
|
362
|
+
/* @__PURE__ */ jsx2(
|
|
363
|
+
"div",
|
|
364
|
+
{
|
|
365
|
+
style: {
|
|
366
|
+
width: "100px",
|
|
367
|
+
height: "4px",
|
|
368
|
+
backgroundColor: "#374151",
|
|
369
|
+
borderRadius: "2px",
|
|
370
|
+
overflow: "hidden"
|
|
371
|
+
},
|
|
372
|
+
children: /* @__PURE__ */ jsx2(
|
|
373
|
+
"div",
|
|
374
|
+
{
|
|
375
|
+
style: {
|
|
376
|
+
width: `${(currentQuestionIndex + 1) / questions.length * 100}%`,
|
|
377
|
+
height: "100%",
|
|
378
|
+
backgroundColor: "#3B82F6",
|
|
379
|
+
transition: "width 0.3s"
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
),
|
|
388
|
+
/* @__PURE__ */ jsxs2("div", { style: { marginBottom: "16px" }, children: [
|
|
389
|
+
/* @__PURE__ */ jsx2(
|
|
390
|
+
"div",
|
|
391
|
+
{
|
|
392
|
+
style: {
|
|
393
|
+
fontSize: "14px",
|
|
394
|
+
fontWeight: "500",
|
|
395
|
+
color: "#F3F4F6",
|
|
396
|
+
marginBottom: "8px"
|
|
397
|
+
},
|
|
398
|
+
children: currentQuestion.question
|
|
399
|
+
}
|
|
400
|
+
),
|
|
401
|
+
currentQuestion.context && /* @__PURE__ */ jsx2(
|
|
402
|
+
"div",
|
|
403
|
+
{
|
|
404
|
+
style: {
|
|
405
|
+
fontSize: "12px",
|
|
406
|
+
color: "#9CA3AF",
|
|
407
|
+
marginBottom: "12px"
|
|
408
|
+
},
|
|
409
|
+
children: currentQuestion.context
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
] }),
|
|
413
|
+
/* @__PURE__ */ jsx2("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: currentQuestion.options.map((option) => /* @__PURE__ */ jsxs2(
|
|
414
|
+
"button",
|
|
415
|
+
{
|
|
416
|
+
onClick: () => handleAnswer(option.value),
|
|
417
|
+
style: {
|
|
418
|
+
display: "flex",
|
|
419
|
+
alignItems: "center",
|
|
420
|
+
gap: "12px",
|
|
421
|
+
padding: "12px",
|
|
422
|
+
backgroundColor: answers[currentQuestion.id] === option.value ? "#374151" : "#111827",
|
|
423
|
+
border: answers[currentQuestion.id] === option.value ? "1px solid #3B82F6" : "1px solid #374151",
|
|
424
|
+
borderRadius: "8px",
|
|
425
|
+
color: "#F3F4F6",
|
|
426
|
+
fontSize: "13px",
|
|
427
|
+
textAlign: "left",
|
|
428
|
+
cursor: "pointer",
|
|
429
|
+
transition: "all 0.15s"
|
|
430
|
+
},
|
|
431
|
+
children: [
|
|
432
|
+
option.preview && /* @__PURE__ */ jsx2(
|
|
433
|
+
"div",
|
|
434
|
+
{
|
|
435
|
+
style: {
|
|
436
|
+
width: "32px",
|
|
437
|
+
height: "32px",
|
|
438
|
+
borderRadius: "4px",
|
|
439
|
+
display: "flex",
|
|
440
|
+
alignItems: "center",
|
|
441
|
+
justifyContent: "center"
|
|
442
|
+
},
|
|
443
|
+
children: option.preview
|
|
444
|
+
}
|
|
445
|
+
),
|
|
446
|
+
/* @__PURE__ */ jsx2("span", { children: option.label })
|
|
447
|
+
]
|
|
448
|
+
},
|
|
449
|
+
option.value
|
|
450
|
+
)) }),
|
|
451
|
+
/* @__PURE__ */ jsxs2(
|
|
452
|
+
"div",
|
|
453
|
+
{
|
|
454
|
+
style: {
|
|
455
|
+
display: "flex",
|
|
456
|
+
justifyContent: "space-between",
|
|
457
|
+
marginTop: "16px"
|
|
458
|
+
},
|
|
459
|
+
children: [
|
|
460
|
+
/* @__PURE__ */ jsx2(
|
|
461
|
+
"button",
|
|
462
|
+
{
|
|
463
|
+
onClick: () => setCurrentQuestionIndex((prev) => Math.max(0, prev - 1)),
|
|
464
|
+
disabled: currentQuestionIndex === 0,
|
|
465
|
+
style: {
|
|
466
|
+
padding: "8px 16px",
|
|
467
|
+
backgroundColor: "transparent",
|
|
468
|
+
border: "1px solid #374151",
|
|
469
|
+
borderRadius: "6px",
|
|
470
|
+
color: currentQuestionIndex === 0 ? "#4B5563" : "#9CA3AF",
|
|
471
|
+
fontSize: "12px",
|
|
472
|
+
cursor: currentQuestionIndex === 0 ? "not-allowed" : "pointer"
|
|
473
|
+
},
|
|
474
|
+
children: "\u2190 Back"
|
|
475
|
+
}
|
|
476
|
+
),
|
|
477
|
+
isComplete && /* @__PURE__ */ jsx2(
|
|
478
|
+
"button",
|
|
479
|
+
{
|
|
480
|
+
onClick: handleSaveToStyleGuide,
|
|
481
|
+
disabled: isSaving,
|
|
482
|
+
style: {
|
|
483
|
+
padding: "8px 16px",
|
|
484
|
+
backgroundColor: saveSuccess ? "#059669" : isSaving ? "#6B7280" : "#10B981",
|
|
485
|
+
border: "none",
|
|
486
|
+
borderRadius: "6px",
|
|
487
|
+
color: "white",
|
|
488
|
+
fontSize: "12px",
|
|
489
|
+
fontWeight: "500",
|
|
490
|
+
cursor: isSaving ? "wait" : "pointer",
|
|
491
|
+
opacity: isSaving ? 0.8 : 1,
|
|
492
|
+
transition: "all 0.2s"
|
|
493
|
+
},
|
|
494
|
+
children: saveSuccess ? "\u2713 Saved!" : isSaving ? "Saving..." : "Save to Style Guide"
|
|
495
|
+
}
|
|
496
|
+
)
|
|
497
|
+
]
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
] });
|
|
501
|
+
}
|
|
502
|
+
function generateQuestionsFromIssues(issues) {
|
|
503
|
+
const questions = [];
|
|
504
|
+
const colorIssues = issues.filter((i) => i.type === "color");
|
|
505
|
+
if (colorIssues.length > 0) {
|
|
506
|
+
const colors = /* @__PURE__ */ new Set();
|
|
507
|
+
colorIssues.forEach((issue) => {
|
|
508
|
+
if (issue.currentValue) colors.add(issue.currentValue);
|
|
509
|
+
if (issue.expectedValue) colors.add(issue.expectedValue);
|
|
510
|
+
});
|
|
511
|
+
if (colors.size >= 2) {
|
|
512
|
+
const colorArray = Array.from(colors);
|
|
513
|
+
questions.push({
|
|
514
|
+
id: "primary-color",
|
|
515
|
+
question: "Which color should be used as the primary color?",
|
|
516
|
+
context: "Multiple similar colors were detected. Choose one for consistency.",
|
|
517
|
+
options: colorArray.slice(0, 4).map((color) => ({
|
|
518
|
+
value: color,
|
|
519
|
+
label: color,
|
|
520
|
+
preview: /* @__PURE__ */ jsx2(
|
|
521
|
+
"div",
|
|
522
|
+
{
|
|
523
|
+
style: {
|
|
524
|
+
width: "100%",
|
|
525
|
+
height: "100%",
|
|
526
|
+
backgroundColor: color,
|
|
527
|
+
borderRadius: "4px"
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
}))
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const spacingIssues = issues.filter((i) => i.type === "spacing");
|
|
536
|
+
if (spacingIssues.length > 0) {
|
|
537
|
+
questions.push({
|
|
538
|
+
id: "spacing-scale",
|
|
539
|
+
question: "What spacing scale should be used?",
|
|
540
|
+
context: "Choose a base unit for consistent spacing throughout the UI.",
|
|
541
|
+
options: [
|
|
542
|
+
{ value: "4", label: "4px base (4, 8, 12, 16, 20, 24...)" },
|
|
543
|
+
{ value: "8", label: "8px base (8, 16, 24, 32, 40...)" },
|
|
544
|
+
{
|
|
545
|
+
value: "tailwind",
|
|
546
|
+
label: "Tailwind scale (4, 8, 12, 16, 20, 24...)"
|
|
547
|
+
}
|
|
548
|
+
]
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
const typographyIssues = issues.filter((i) => i.type === "typography");
|
|
552
|
+
if (typographyIssues.length > 0) {
|
|
553
|
+
questions.push({
|
|
554
|
+
id: "font-weights",
|
|
555
|
+
question: "Which font weights should be used?",
|
|
556
|
+
context: "Select the weights to use for consistency.",
|
|
557
|
+
options: [
|
|
558
|
+
{
|
|
559
|
+
value: "400-600-700",
|
|
560
|
+
label: "Regular (400), Semibold (600), Bold (700)"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
value: "400-500-700",
|
|
564
|
+
label: "Regular (400), Medium (500), Bold (700)"
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
value: "300-400-600",
|
|
568
|
+
label: "Light (300), Regular (400), Semibold (600)"
|
|
569
|
+
}
|
|
570
|
+
]
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
return questions;
|
|
574
|
+
}
|
|
575
|
+
function buildUpdatedStyleGuide(existingContent, answers) {
|
|
576
|
+
const lines = [];
|
|
577
|
+
lines.push("# UI Style Guide");
|
|
578
|
+
lines.push("");
|
|
579
|
+
lines.push(
|
|
580
|
+
"> Auto-generated by UILint. Edit this file to define your design system."
|
|
581
|
+
);
|
|
582
|
+
lines.push("");
|
|
583
|
+
lines.push("## Colors");
|
|
584
|
+
lines.push("");
|
|
585
|
+
if (answers["primary-color"]) {
|
|
586
|
+
lines.push(`- **Primary**: ${answers["primary-color"]}`);
|
|
587
|
+
}
|
|
588
|
+
const existingColors = extractSection(existingContent, "Colors");
|
|
589
|
+
existingColors.filter((line) => !line.includes("**Primary**")).forEach((line) => lines.push(line));
|
|
590
|
+
if (!answers["primary-color"] && existingColors.length === 0) {
|
|
591
|
+
lines.push("- Define your color palette here");
|
|
592
|
+
}
|
|
593
|
+
lines.push("");
|
|
594
|
+
lines.push("## Typography");
|
|
595
|
+
lines.push("");
|
|
596
|
+
if (answers["font-weights"]) {
|
|
597
|
+
const weightMap = {
|
|
598
|
+
"400-600-700": "400 (Regular), 600 (Semibold), 700 (Bold)",
|
|
599
|
+
"400-500-700": "400 (Regular), 500 (Medium), 700 (Bold)",
|
|
600
|
+
"300-400-600": "300 (Light), 400 (Regular), 600 (Semibold)"
|
|
601
|
+
};
|
|
602
|
+
lines.push(
|
|
603
|
+
`- **Font Weights**: ${weightMap[answers["font-weights"]] || answers["font-weights"]}`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
const existingTypography = extractSection(existingContent, "Typography");
|
|
607
|
+
existingTypography.filter((line) => !line.includes("**Font Weights**")).forEach((line) => lines.push(line));
|
|
608
|
+
if (!answers["font-weights"] && existingTypography.length === 0) {
|
|
609
|
+
lines.push("- Define your typography scale here");
|
|
610
|
+
}
|
|
611
|
+
lines.push("");
|
|
612
|
+
lines.push("## Spacing");
|
|
613
|
+
lines.push("");
|
|
614
|
+
if (answers["spacing-scale"]) {
|
|
615
|
+
const spacingMap = {
|
|
616
|
+
"4": "4px (4, 8, 12, 16, 20, 24, 32, 40, 48...)",
|
|
617
|
+
"8": "8px (8, 16, 24, 32, 40, 48, 56, 64...)",
|
|
618
|
+
tailwind: "Tailwind (4, 8, 12, 16, 20, 24, 32, 40, 48...)"
|
|
619
|
+
};
|
|
620
|
+
lines.push(
|
|
621
|
+
`- **Base unit**: ${spacingMap[answers["spacing-scale"]] || answers["spacing-scale"]}`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
const existingSpacing = extractSection(existingContent, "Spacing");
|
|
625
|
+
existingSpacing.filter((line) => !line.includes("**Base unit**")).forEach((line) => lines.push(line));
|
|
626
|
+
if (!answers["spacing-scale"] && existingSpacing.length === 0) {
|
|
627
|
+
lines.push("- Define your spacing scale here");
|
|
628
|
+
}
|
|
629
|
+
lines.push("");
|
|
630
|
+
const otherSections = ["Border Radius", "Components"];
|
|
631
|
+
otherSections.forEach((section) => {
|
|
632
|
+
const sectionLines = extractSection(existingContent, section);
|
|
633
|
+
if (sectionLines.length > 0) {
|
|
634
|
+
lines.push(`## ${section}`);
|
|
635
|
+
lines.push("");
|
|
636
|
+
sectionLines.forEach((line) => lines.push(line));
|
|
637
|
+
lines.push("");
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
if (!existingContent?.includes("## Components")) {
|
|
641
|
+
lines.push("## Components");
|
|
642
|
+
lines.push("");
|
|
643
|
+
lines.push("- **Buttons**: Define button styles here");
|
|
644
|
+
lines.push("- **Cards**: Define card styles here");
|
|
645
|
+
lines.push("- **Inputs**: Define input styles here");
|
|
646
|
+
lines.push("");
|
|
647
|
+
}
|
|
648
|
+
return lines.join("\n");
|
|
649
|
+
}
|
|
650
|
+
function extractSection(content, sectionName) {
|
|
651
|
+
if (!content) return [];
|
|
652
|
+
const lines = content.split("\n");
|
|
653
|
+
const sectionStart = lines.findIndex(
|
|
654
|
+
(line) => line.match(new RegExp(`^##\\s+${sectionName}`, "i"))
|
|
655
|
+
);
|
|
656
|
+
if (sectionStart === -1) return [];
|
|
657
|
+
const result = [];
|
|
658
|
+
for (let i = sectionStart + 1; i < lines.length; i++) {
|
|
659
|
+
const line = lines[i];
|
|
660
|
+
if (line.startsWith("## ")) break;
|
|
661
|
+
if (result.length === 0 && line.trim() === "") continue;
|
|
662
|
+
if (line.startsWith("- ")) {
|
|
663
|
+
result.push(line);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return result;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/components/Overlay.tsx
|
|
670
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
671
|
+
function Overlay({ position }) {
|
|
672
|
+
const [isExpanded, setIsExpanded] = useState2(false);
|
|
673
|
+
const { issues, isScanning, scan } = useUILint();
|
|
674
|
+
const positionStyles = {
|
|
675
|
+
position: "fixed",
|
|
676
|
+
zIndex: 99999,
|
|
677
|
+
...position.includes("bottom") ? { bottom: "16px" } : { top: "16px" },
|
|
678
|
+
...position.includes("left") ? { left: "16px" } : { right: "16px" }
|
|
679
|
+
};
|
|
680
|
+
const issueCount = issues.length;
|
|
681
|
+
const hasIssues = issueCount > 0;
|
|
682
|
+
return /* @__PURE__ */ jsx3("div", { style: positionStyles, children: isExpanded ? /* @__PURE__ */ jsx3(
|
|
683
|
+
ExpandedPanel,
|
|
684
|
+
{
|
|
685
|
+
onCollapse: () => setIsExpanded(false),
|
|
686
|
+
onScan: scan,
|
|
687
|
+
isScanning
|
|
688
|
+
}
|
|
689
|
+
) : /* @__PURE__ */ jsx3(
|
|
690
|
+
CollapsedButton,
|
|
691
|
+
{
|
|
692
|
+
onClick: () => setIsExpanded(true),
|
|
693
|
+
issueCount,
|
|
694
|
+
hasIssues,
|
|
695
|
+
isScanning
|
|
696
|
+
}
|
|
697
|
+
) });
|
|
698
|
+
}
|
|
699
|
+
function CollapsedButton({ onClick, issueCount, hasIssues, isScanning }) {
|
|
700
|
+
return /* @__PURE__ */ jsx3(
|
|
701
|
+
"button",
|
|
702
|
+
{
|
|
703
|
+
onClick,
|
|
704
|
+
style: {
|
|
705
|
+
display: "flex",
|
|
706
|
+
alignItems: "center",
|
|
707
|
+
justifyContent: "center",
|
|
708
|
+
width: "48px",
|
|
709
|
+
height: "48px",
|
|
710
|
+
borderRadius: "50%",
|
|
711
|
+
border: "none",
|
|
712
|
+
backgroundColor: hasIssues ? "#EF4444" : "#10B981",
|
|
713
|
+
color: "white",
|
|
714
|
+
cursor: "pointer",
|
|
715
|
+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
|
716
|
+
transition: "transform 0.2s, box-shadow 0.2s",
|
|
717
|
+
fontSize: "20px",
|
|
718
|
+
fontWeight: "bold"
|
|
719
|
+
},
|
|
720
|
+
onMouseEnter: (e) => {
|
|
721
|
+
e.currentTarget.style.transform = "scale(1.1)";
|
|
722
|
+
e.currentTarget.style.boxShadow = "0 6px 16px rgba(0, 0, 0, 0.2)";
|
|
723
|
+
},
|
|
724
|
+
onMouseLeave: (e) => {
|
|
725
|
+
e.currentTarget.style.transform = "scale(1)";
|
|
726
|
+
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.15)";
|
|
727
|
+
},
|
|
728
|
+
title: `UILint: ${issueCount} issues found`,
|
|
729
|
+
children: isScanning ? /* @__PURE__ */ jsx3("span", { style: { animation: "spin 1s linear infinite" }, children: "\u27F3" }) : hasIssues ? issueCount : "\u2713"
|
|
730
|
+
}
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
function ExpandedPanel({ onCollapse, onScan, isScanning }) {
|
|
734
|
+
const [activeTab, setActiveTab] = useState2("issues");
|
|
735
|
+
return /* @__PURE__ */ jsxs3(
|
|
736
|
+
"div",
|
|
737
|
+
{
|
|
738
|
+
style: {
|
|
739
|
+
width: "380px",
|
|
740
|
+
maxHeight: "500px",
|
|
741
|
+
backgroundColor: "#1F2937",
|
|
742
|
+
borderRadius: "12px",
|
|
743
|
+
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.3)",
|
|
744
|
+
overflow: "hidden",
|
|
745
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
746
|
+
color: "#F9FAFB"
|
|
747
|
+
},
|
|
748
|
+
children: [
|
|
749
|
+
/* @__PURE__ */ jsxs3(
|
|
750
|
+
"div",
|
|
751
|
+
{
|
|
752
|
+
style: {
|
|
753
|
+
display: "flex",
|
|
754
|
+
alignItems: "center",
|
|
755
|
+
justifyContent: "space-between",
|
|
756
|
+
padding: "12px 16px",
|
|
757
|
+
borderBottom: "1px solid #374151",
|
|
758
|
+
backgroundColor: "#111827"
|
|
759
|
+
},
|
|
760
|
+
children: [
|
|
761
|
+
/* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
|
|
762
|
+
/* @__PURE__ */ jsx3("span", { style: { fontSize: "16px" }, children: "\u{1F3A8}" }),
|
|
763
|
+
/* @__PURE__ */ jsx3("span", { style: { fontWeight: "600", fontSize: "14px" }, children: "UILint" })
|
|
764
|
+
] }),
|
|
765
|
+
/* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
|
|
766
|
+
/* @__PURE__ */ jsx3(
|
|
767
|
+
"button",
|
|
768
|
+
{
|
|
769
|
+
onClick: onScan,
|
|
770
|
+
disabled: isScanning,
|
|
771
|
+
style: {
|
|
772
|
+
padding: "6px 12px",
|
|
773
|
+
borderRadius: "6px",
|
|
774
|
+
border: "none",
|
|
775
|
+
backgroundColor: "#3B82F6",
|
|
776
|
+
color: "white",
|
|
777
|
+
fontSize: "12px",
|
|
778
|
+
fontWeight: "500",
|
|
779
|
+
cursor: isScanning ? "not-allowed" : "pointer",
|
|
780
|
+
opacity: isScanning ? 0.7 : 1
|
|
781
|
+
},
|
|
782
|
+
children: isScanning ? "Scanning..." : "Scan"
|
|
783
|
+
}
|
|
784
|
+
),
|
|
785
|
+
/* @__PURE__ */ jsx3(
|
|
786
|
+
"button",
|
|
787
|
+
{
|
|
788
|
+
onClick: onCollapse,
|
|
789
|
+
style: {
|
|
790
|
+
padding: "6px 8px",
|
|
791
|
+
borderRadius: "6px",
|
|
792
|
+
border: "none",
|
|
793
|
+
backgroundColor: "transparent",
|
|
794
|
+
color: "#9CA3AF",
|
|
795
|
+
fontSize: "16px",
|
|
796
|
+
cursor: "pointer"
|
|
797
|
+
},
|
|
798
|
+
children: "\u2715"
|
|
799
|
+
}
|
|
800
|
+
)
|
|
801
|
+
] })
|
|
802
|
+
]
|
|
803
|
+
}
|
|
804
|
+
),
|
|
805
|
+
/* @__PURE__ */ jsxs3(
|
|
806
|
+
"div",
|
|
807
|
+
{
|
|
808
|
+
style: {
|
|
809
|
+
display: "flex",
|
|
810
|
+
borderBottom: "1px solid #374151"
|
|
811
|
+
},
|
|
812
|
+
children: [
|
|
813
|
+
/* @__PURE__ */ jsx3(
|
|
814
|
+
TabButton,
|
|
815
|
+
{
|
|
816
|
+
active: activeTab === "issues",
|
|
817
|
+
onClick: () => setActiveTab("issues"),
|
|
818
|
+
children: "Issues"
|
|
819
|
+
}
|
|
820
|
+
),
|
|
821
|
+
/* @__PURE__ */ jsx3(
|
|
822
|
+
TabButton,
|
|
823
|
+
{
|
|
824
|
+
active: activeTab === "questions",
|
|
825
|
+
onClick: () => setActiveTab("questions"),
|
|
826
|
+
children: "Questions"
|
|
827
|
+
}
|
|
828
|
+
)
|
|
829
|
+
]
|
|
830
|
+
}
|
|
831
|
+
),
|
|
832
|
+
/* @__PURE__ */ jsx3("div", { style: { maxHeight: "380px", overflow: "auto" }, children: activeTab === "issues" ? /* @__PURE__ */ jsx3(IssueList, {}) : /* @__PURE__ */ jsx3(QuestionPanel, {}) })
|
|
833
|
+
]
|
|
834
|
+
}
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
function TabButton({ active, onClick, children }) {
|
|
838
|
+
return /* @__PURE__ */ jsx3(
|
|
839
|
+
"button",
|
|
840
|
+
{
|
|
841
|
+
onClick,
|
|
842
|
+
style: {
|
|
843
|
+
flex: 1,
|
|
844
|
+
padding: "10px 16px",
|
|
845
|
+
border: "none",
|
|
846
|
+
backgroundColor: "transparent",
|
|
847
|
+
color: active ? "#3B82F6" : "#9CA3AF",
|
|
848
|
+
fontSize: "13px",
|
|
849
|
+
fontWeight: "500",
|
|
850
|
+
cursor: "pointer",
|
|
851
|
+
borderBottom: active ? "2px solid #3B82F6" : "2px solid transparent",
|
|
852
|
+
marginBottom: "-1px"
|
|
853
|
+
},
|
|
854
|
+
children
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/components/Highlighter.tsx
|
|
860
|
+
import { useEffect, useState as useState3 } from "react";
|
|
861
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
862
|
+
function Highlighter() {
|
|
863
|
+
const { highlightedIssue } = useUILint();
|
|
864
|
+
const [rect, setRect] = useState3(null);
|
|
865
|
+
useEffect(() => {
|
|
866
|
+
if (!highlightedIssue?.selector) {
|
|
867
|
+
setRect(null);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
const element = document.querySelector(highlightedIssue.selector);
|
|
872
|
+
if (element) {
|
|
873
|
+
const domRect = element.getBoundingClientRect();
|
|
874
|
+
setRect({
|
|
875
|
+
top: domRect.top + window.scrollY,
|
|
876
|
+
left: domRect.left + window.scrollX,
|
|
877
|
+
width: domRect.width,
|
|
878
|
+
height: domRect.height
|
|
879
|
+
});
|
|
880
|
+
} else {
|
|
881
|
+
setRect(null);
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
setRect(null);
|
|
885
|
+
}
|
|
886
|
+
}, [highlightedIssue]);
|
|
887
|
+
if (!rect) return null;
|
|
888
|
+
const typeColors = {
|
|
889
|
+
color: "#F59E0B",
|
|
890
|
+
typography: "#8B5CF6",
|
|
891
|
+
spacing: "#10B981",
|
|
892
|
+
component: "#3B82F6",
|
|
893
|
+
responsive: "#EC4899",
|
|
894
|
+
accessibility: "#EF4444"
|
|
895
|
+
};
|
|
896
|
+
const color = highlightedIssue?.type ? typeColors[highlightedIssue.type] || "#EF4444" : "#EF4444";
|
|
897
|
+
return /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
898
|
+
/* @__PURE__ */ jsx4(
|
|
899
|
+
"div",
|
|
900
|
+
{
|
|
901
|
+
style: {
|
|
902
|
+
position: "absolute",
|
|
903
|
+
top: rect.top - 4,
|
|
904
|
+
left: rect.left - 4,
|
|
905
|
+
width: rect.width + 8,
|
|
906
|
+
height: rect.height + 8,
|
|
907
|
+
border: `2px solid ${color}`,
|
|
908
|
+
borderRadius: "4px",
|
|
909
|
+
backgroundColor: `${color}15`,
|
|
910
|
+
pointerEvents: "none",
|
|
911
|
+
zIndex: 99998,
|
|
912
|
+
boxShadow: `0 0 0 4px ${color}30`,
|
|
913
|
+
transition: "all 0.2s ease-out"
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
),
|
|
917
|
+
/* @__PURE__ */ jsx4(
|
|
918
|
+
"div",
|
|
919
|
+
{
|
|
920
|
+
style: {
|
|
921
|
+
position: "absolute",
|
|
922
|
+
top: rect.top - 28,
|
|
923
|
+
left: rect.left - 4,
|
|
924
|
+
padding: "4px 8px",
|
|
925
|
+
backgroundColor: color,
|
|
926
|
+
color: "white",
|
|
927
|
+
fontSize: "11px",
|
|
928
|
+
fontWeight: "600",
|
|
929
|
+
borderRadius: "4px 4px 0 0",
|
|
930
|
+
pointerEvents: "none",
|
|
931
|
+
zIndex: 99998,
|
|
932
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
933
|
+
textTransform: "uppercase"
|
|
934
|
+
},
|
|
935
|
+
children: highlightedIssue?.type || "Issue"
|
|
936
|
+
}
|
|
937
|
+
)
|
|
938
|
+
] });
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/components/UILint.tsx
|
|
942
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
943
|
+
var UILintContext = createContext(null);
|
|
944
|
+
function useUILint() {
|
|
945
|
+
const context = useContext(UILintContext);
|
|
946
|
+
if (!context) {
|
|
947
|
+
throw new Error("useUILint must be used within a UILint component");
|
|
948
|
+
}
|
|
949
|
+
return context;
|
|
950
|
+
}
|
|
951
|
+
function UILint({
|
|
952
|
+
children,
|
|
953
|
+
enabled = true,
|
|
954
|
+
position = "bottom-left",
|
|
955
|
+
autoScan = false,
|
|
956
|
+
apiEndpoint
|
|
957
|
+
}) {
|
|
958
|
+
const [issues, setIssues] = useState4([]);
|
|
959
|
+
const [isScanning, setIsScanning] = useState4(false);
|
|
960
|
+
const [styleGuideExists, setStyleGuideExists] = useState4(false);
|
|
961
|
+
const [styleGuideContent, setStyleGuideContent] = useState4(
|
|
962
|
+
null
|
|
963
|
+
);
|
|
964
|
+
const [highlightedIssue, setHighlightedIssue] = useState4(
|
|
965
|
+
null
|
|
966
|
+
);
|
|
967
|
+
const [isMounted, setIsMounted] = useState4(false);
|
|
968
|
+
const llmClient = useRef(new LLMClient({ apiEndpoint }));
|
|
969
|
+
const hasInitialized = useRef(false);
|
|
970
|
+
useEffect2(() => {
|
|
971
|
+
setIsMounted(true);
|
|
972
|
+
}, []);
|
|
973
|
+
const checkStyleGuide = useCallback(async () => {
|
|
974
|
+
if (!isBrowser()) return;
|
|
975
|
+
try {
|
|
976
|
+
const response = await fetch("/api/uilint/styleguide");
|
|
977
|
+
const data = await response.json();
|
|
978
|
+
setStyleGuideExists(data.exists);
|
|
979
|
+
setStyleGuideContent(data.content);
|
|
980
|
+
} catch {
|
|
981
|
+
setStyleGuideExists(false);
|
|
982
|
+
}
|
|
983
|
+
}, []);
|
|
984
|
+
const saveStyleGuide = useCallback(async (content) => {
|
|
985
|
+
if (!isBrowser()) return;
|
|
986
|
+
try {
|
|
987
|
+
await fetch("/api/uilint/styleguide", {
|
|
988
|
+
method: "POST",
|
|
989
|
+
headers: { "Content-Type": "application/json" },
|
|
990
|
+
body: JSON.stringify({ content })
|
|
991
|
+
});
|
|
992
|
+
setStyleGuideExists(true);
|
|
993
|
+
setStyleGuideContent(content);
|
|
994
|
+
} catch (error) {
|
|
995
|
+
console.error("[UILint] Failed to save style guide:", error);
|
|
996
|
+
}
|
|
997
|
+
}, []);
|
|
998
|
+
const scan = useCallback(async () => {
|
|
999
|
+
if (!isBrowser()) return;
|
|
1000
|
+
setIsScanning(true);
|
|
1001
|
+
try {
|
|
1002
|
+
const snapshot = scanDOM(document.body);
|
|
1003
|
+
if (!styleGuideContent) {
|
|
1004
|
+
const generatedGuide = generateStyleGuide(snapshot.styles);
|
|
1005
|
+
await saveStyleGuide(generatedGuide);
|
|
1006
|
+
}
|
|
1007
|
+
const result = await llmClient.current.analyze(
|
|
1008
|
+
snapshot.styles,
|
|
1009
|
+
styleGuideContent
|
|
1010
|
+
);
|
|
1011
|
+
setIssues(result.issues);
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
console.error("[UILint] Scan failed:", error);
|
|
1014
|
+
} finally {
|
|
1015
|
+
setIsScanning(false);
|
|
1016
|
+
}
|
|
1017
|
+
}, [styleGuideContent, saveStyleGuide]);
|
|
1018
|
+
const clearIssues = useCallback(() => {
|
|
1019
|
+
setIssues([]);
|
|
1020
|
+
setHighlightedIssue(null);
|
|
1021
|
+
}, []);
|
|
1022
|
+
useEffect2(() => {
|
|
1023
|
+
if (!enabled || hasInitialized.current) return;
|
|
1024
|
+
hasInitialized.current = true;
|
|
1025
|
+
if (!isBrowser()) return;
|
|
1026
|
+
checkStyleGuide();
|
|
1027
|
+
if (autoScan) {
|
|
1028
|
+
const timer = setTimeout(scan, 1e3);
|
|
1029
|
+
return () => clearTimeout(timer);
|
|
1030
|
+
}
|
|
1031
|
+
}, [enabled, autoScan, scan, checkStyleGuide]);
|
|
1032
|
+
const contextValue = {
|
|
1033
|
+
issues,
|
|
1034
|
+
isScanning,
|
|
1035
|
+
styleGuideExists,
|
|
1036
|
+
scan,
|
|
1037
|
+
clearIssues,
|
|
1038
|
+
highlightedIssue,
|
|
1039
|
+
setHighlightedIssue
|
|
1040
|
+
};
|
|
1041
|
+
const shouldRenderOverlay = enabled && isMounted;
|
|
1042
|
+
return /* @__PURE__ */ jsxs5(UILintContext.Provider, { value: contextValue, children: [
|
|
1043
|
+
children,
|
|
1044
|
+
shouldRenderOverlay && /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
1045
|
+
/* @__PURE__ */ jsx5(Overlay, { position }),
|
|
1046
|
+
/* @__PURE__ */ jsx5(Highlighter, {})
|
|
1047
|
+
] })
|
|
1048
|
+
] });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/index.ts
|
|
1052
|
+
import {
|
|
1053
|
+
extractStylesFromDOM as extractStylesFromDOM2,
|
|
1054
|
+
serializeStyles as serializeStyles2,
|
|
1055
|
+
createStyleSummary as createStyleSummary3
|
|
1056
|
+
} from "uilint-core";
|
|
1057
|
+
import { parseStyleGuide } from "uilint-core";
|
|
1058
|
+
import { generateStyleGuideFromStyles } from "uilint-core";
|
|
1059
|
+
import { createEmptyStyleGuide, mergeStyleGuides } from "uilint-core";
|
|
1060
|
+
export {
|
|
1061
|
+
LLMClient,
|
|
1062
|
+
UILint,
|
|
1063
|
+
createEmptyStyleGuide,
|
|
1064
|
+
createStyleSummary3 as createStyleSummary,
|
|
1065
|
+
extractStylesFromDOM2 as extractStylesFromDOM,
|
|
1066
|
+
generateStyleGuideFromStyles as generateStyleGuide,
|
|
1067
|
+
isBrowser,
|
|
1068
|
+
isJSDOM,
|
|
1069
|
+
isNode,
|
|
1070
|
+
mergeStyleGuides,
|
|
1071
|
+
parseStyleGuide,
|
|
1072
|
+
scanDOM,
|
|
1073
|
+
serializeStyles2 as serializeStyles
|
|
1074
|
+
};
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AnalysisResult, UILintIssue } from 'uilint-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if we're running in a JSDOM environment (tests)
|
|
5
|
+
*/
|
|
6
|
+
declare function isJSDOM(): boolean;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Adapter for running UILint in JSDOM/Node.js test environments
|
|
10
|
+
* Uses uilint-core for analysis
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Adapter for running UILint in JSDOM/Node.js test environments
|
|
15
|
+
* Calls Ollama directly and outputs to console.warn()
|
|
16
|
+
*/
|
|
17
|
+
declare class JSDOMAdapter {
|
|
18
|
+
private styleGuideContent;
|
|
19
|
+
private styleGuidePath;
|
|
20
|
+
private client;
|
|
21
|
+
constructor(styleGuidePath?: string);
|
|
22
|
+
/**
|
|
23
|
+
* Loads the style guide from the filesystem (Node.js only)
|
|
24
|
+
*/
|
|
25
|
+
loadStyleGuide(): Promise<string | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Analyzes the current DOM and returns issues
|
|
28
|
+
*/
|
|
29
|
+
analyze(root?: Element | Document): Promise<AnalysisResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Outputs issues to console.warn (for test visibility)
|
|
32
|
+
*/
|
|
33
|
+
outputWarnings(issues: UILintIssue[]): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Convenience function for running UILint in tests
|
|
37
|
+
*/
|
|
38
|
+
declare function runUILintInTest(root?: Element | Document): Promise<UILintIssue[]>;
|
|
39
|
+
|
|
40
|
+
export { JSDOMAdapter, isJSDOM, runUILintInTest };
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/scanner/jsdom-adapter.ts
|
|
2
|
+
import {
|
|
3
|
+
OllamaClient,
|
|
4
|
+
createStyleSummary as createStyleSummary2
|
|
5
|
+
} from "uilint-core";
|
|
6
|
+
|
|
7
|
+
// src/scanner/dom-scanner.ts
|
|
8
|
+
import {
|
|
9
|
+
extractStylesFromDOM,
|
|
10
|
+
createStyleSummary,
|
|
11
|
+
serializeStyles,
|
|
12
|
+
truncateHTML
|
|
13
|
+
} from "uilint-core";
|
|
14
|
+
function scanDOM(root) {
|
|
15
|
+
const targetRoot = root || document.body;
|
|
16
|
+
const styles = extractStylesFromDOM(targetRoot);
|
|
17
|
+
const html = targetRoot instanceof Element ? targetRoot.outerHTML : targetRoot.body?.outerHTML || "";
|
|
18
|
+
return {
|
|
19
|
+
html: truncateHTML(html, 5e4),
|
|
20
|
+
styles,
|
|
21
|
+
elementCount: targetRoot.querySelectorAll("*").length,
|
|
22
|
+
timestamp: Date.now()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/scanner/environment.ts
|
|
27
|
+
function isBrowser() {
|
|
28
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
29
|
+
}
|
|
30
|
+
function isJSDOM() {
|
|
31
|
+
if (!isBrowser()) return false;
|
|
32
|
+
const userAgent = window.navigator?.userAgent || "";
|
|
33
|
+
return userAgent.includes("jsdom");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/scanner/jsdom-adapter.ts
|
|
37
|
+
var JSDOMAdapter = class {
|
|
38
|
+
styleGuideContent = null;
|
|
39
|
+
styleGuidePath;
|
|
40
|
+
client;
|
|
41
|
+
constructor(styleGuidePath = ".uilint/styleguide.md") {
|
|
42
|
+
this.styleGuidePath = styleGuidePath;
|
|
43
|
+
this.client = new OllamaClient();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Loads the style guide from the filesystem (Node.js only)
|
|
47
|
+
*/
|
|
48
|
+
async loadStyleGuide() {
|
|
49
|
+
if (typeof process === "undefined") return null;
|
|
50
|
+
try {
|
|
51
|
+
const fs = await import("fs/promises");
|
|
52
|
+
const path = await import("path");
|
|
53
|
+
const fullPath = path.resolve(process.cwd(), this.styleGuidePath);
|
|
54
|
+
this.styleGuideContent = await fs.readFile(fullPath, "utf-8");
|
|
55
|
+
return this.styleGuideContent;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Analyzes the current DOM and returns issues
|
|
62
|
+
*/
|
|
63
|
+
async analyze(root) {
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
const snapshot = scanDOM(root);
|
|
66
|
+
const styleSummary = createStyleSummary2(snapshot.styles);
|
|
67
|
+
if (!this.styleGuideContent) {
|
|
68
|
+
await this.loadStyleGuide();
|
|
69
|
+
}
|
|
70
|
+
const result = await this.client.analyzeStyles(
|
|
71
|
+
styleSummary,
|
|
72
|
+
this.styleGuideContent
|
|
73
|
+
);
|
|
74
|
+
return {
|
|
75
|
+
issues: result.issues,
|
|
76
|
+
analysisTime: Date.now() - startTime
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Outputs issues to console.warn (for test visibility)
|
|
81
|
+
*/
|
|
82
|
+
outputWarnings(issues) {
|
|
83
|
+
if (issues.length === 0) {
|
|
84
|
+
console.warn("[UILint] No UI consistency issues found");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
issues.forEach((issue) => {
|
|
88
|
+
const parts = [`\u26A0\uFE0F [UILint] ${issue.message}`];
|
|
89
|
+
if (issue.currentValue && issue.expectedValue) {
|
|
90
|
+
parts.push(
|
|
91
|
+
`Current: ${issue.currentValue}, Expected: ${issue.expectedValue}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (issue.suggestion) {
|
|
95
|
+
parts.push(`Suggestion: ${issue.suggestion}`);
|
|
96
|
+
}
|
|
97
|
+
console.warn(parts.join(" | "));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
async function runUILintInTest(root) {
|
|
102
|
+
const adapter = new JSDOMAdapter();
|
|
103
|
+
const result = await adapter.analyze(root);
|
|
104
|
+
adapter.outputWarnings(result.issues);
|
|
105
|
+
return result.issues;
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
JSDOMAdapter,
|
|
109
|
+
isJSDOM,
|
|
110
|
+
runUILintInTest
|
|
111
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uilint-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React component for AI-powered UI consistency checking",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./node": {
|
|
15
|
+
"types": "./dist/node.d.ts",
|
|
16
|
+
"import": "./dist/node.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"uilint-core": "^0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": "^19.0.0",
|
|
30
|
+
"react-dom": "^19.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^19.2.7",
|
|
34
|
+
"@types/react-dom": "^19.2.3",
|
|
35
|
+
"react": "^19.2.3",
|
|
36
|
+
"react-dom": "^19.2.3",
|
|
37
|
+
"tsup": "^8.5.1",
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"react",
|
|
42
|
+
"ui",
|
|
43
|
+
"lint",
|
|
44
|
+
"consistency",
|
|
45
|
+
"design-system",
|
|
46
|
+
"ai",
|
|
47
|
+
"llm"
|
|
48
|
+
],
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"dev": "tsup --watch",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"lint": "eslint src/"
|
|
58
|
+
}
|
|
59
|
+
}
|