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.
@@ -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
+ }