uilint-react 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  UILintToolbar
4
- } from "./chunk-OWX36QE3.js";
5
- import {
6
- SourceOverlays
7
- } from "./chunk-MEP7WO7U.js";
4
+ } from "./chunk-PBC3J267.js";
8
5
  import {
9
6
  InspectionPanel,
10
7
  clearSourceCache,
@@ -12,8 +9,10 @@ import {
12
9
  fetchSourceWithContext,
13
10
  getCachedSource,
14
11
  prefetchSources
15
- } from "./chunk-3TA6OKS6.js";
16
- import "./chunk-KUFV22FO.js";
12
+ } from "./chunk-NOISZ3XP.js";
13
+ import {
14
+ LocatorOverlay
15
+ } from "./chunk-VYCIUDU7.js";
17
16
  import {
18
17
  DATA_UILINT_ID,
19
18
  DEFAULT_SETTINGS,
@@ -31,19 +30,8 @@ import {
31
30
  isNodeModulesPath,
32
31
  scanDOMForSources,
33
32
  updateElementRects,
34
- useElementScan,
35
33
  useUILintContext
36
- } from "./chunk-7WYVWDRU.js";
37
-
38
- // src/components/UILint.tsx
39
- import {
40
- createContext,
41
- useContext,
42
- useState as useState4,
43
- useEffect as useEffect2,
44
- useCallback as useCallback2,
45
- useRef
46
- } from "react";
34
+ } from "./chunk-DAFFOBEU.js";
47
35
 
48
36
  // src/consistency/snapshot.ts
49
37
  var DATA_ELEMENTS_ATTR = "data-elements";
@@ -244,950 +232,10 @@ function getElementBySnapshotId(id) {
244
232
  return document.querySelector(`[${DATA_ELEMENTS_ATTR}="${id}"]`);
245
233
  }
246
234
 
247
- // src/scanner/environment.ts
248
- function isBrowser() {
249
- return typeof window !== "undefined" && typeof window.document !== "undefined";
250
- }
251
- function isJSDOM() {
252
- if (!isBrowser()) return false;
253
- const userAgent = window.navigator?.userAgent || "";
254
- return userAgent.includes("jsdom");
255
- }
256
- function isNode() {
257
- return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
258
- }
259
-
260
- // src/components/Overlay.tsx
261
- import { useState as useState2 } from "react";
262
-
263
- // src/components/ViolationList.tsx
264
- import { jsx, jsxs } from "react/jsx-runtime";
265
- function ViolationList() {
266
- const {
267
- violations,
268
- selectedViolation,
269
- setSelectedViolation,
270
- lockedViolation,
271
- setLockedViolation,
272
- isScanning
273
- } = useUILint();
274
- if (isScanning) {
275
- return /* @__PURE__ */ jsxs(
276
- "div",
277
- {
278
- style: {
279
- padding: "32px 16px",
280
- textAlign: "center",
281
- color: "#9CA3AF"
282
- },
283
- children: [
284
- /* @__PURE__ */ jsx("div", { style: { fontSize: "32px", marginBottom: "8px" }, children: "\u{1F50D}" }),
285
- /* @__PURE__ */ jsx("div", { style: { fontSize: "14px" }, children: "Analyzing page..." }),
286
- /* @__PURE__ */ jsx("div", { style: { fontSize: "12px", marginTop: "4px", color: "#6B7280" }, children: "This may take a moment" })
287
- ]
288
- }
289
- );
290
- }
291
- if (violations.length === 0) {
292
- return /* @__PURE__ */ jsxs(
293
- "div",
294
- {
295
- style: {
296
- padding: "32px 16px",
297
- textAlign: "center",
298
- color: "#9CA3AF"
299
- },
300
- children: [
301
- /* @__PURE__ */ jsx("div", { style: { fontSize: "32px", marginBottom: "8px" }, children: "\u2728" }),
302
- /* @__PURE__ */ jsx("div", { style: { fontSize: "14px" }, children: "No consistency issues found" }),
303
- /* @__PURE__ */ jsx("div", { style: { fontSize: "12px", marginTop: "4px" }, children: 'Click "Scan" to analyze the page' })
304
- ]
305
- }
306
- );
307
- }
308
- const handleClick = (violation) => {
309
- if (lockedViolation?.elementIds.join(",") === violation.elementIds.join(",")) {
310
- setLockedViolation(null);
311
- } else {
312
- setLockedViolation(violation);
313
- }
314
- };
315
- return /* @__PURE__ */ jsx("div", { style: { padding: "8px" }, children: violations.map((violation, index) => /* @__PURE__ */ jsx(
316
- ViolationCard,
317
- {
318
- violation,
319
- isSelected: selectedViolation?.elementIds.join(",") === violation.elementIds.join(","),
320
- isLocked: lockedViolation?.elementIds.join(",") === violation.elementIds.join(","),
321
- onHover: () => setSelectedViolation(violation),
322
- onLeave: () => setSelectedViolation(null),
323
- onClick: () => handleClick(violation)
324
- },
325
- `${violation.elementIds.join("-")}-${index}`
326
- )) });
327
- }
328
- function ViolationCard({
329
- violation,
330
- isSelected,
331
- isLocked,
332
- onHover,
333
- onLeave,
334
- onClick
335
- }) {
336
- const categoryColors = {
337
- spacing: "#10B981",
338
- color: "#F59E0B",
339
- typography: "#8B5CF6",
340
- sizing: "#3B82F6",
341
- borders: "#06B6D4",
342
- shadows: "#6B7280"
343
- };
344
- const severityIcons = {
345
- error: "\u2716",
346
- warning: "\u26A0",
347
- info: "\u2139"
348
- };
349
- const categoryColor = categoryColors[violation.category] || "#6B7280";
350
- const severityIcon = severityIcons[violation.severity] || "\u2022";
351
- const isHighlighted = isSelected || isLocked;
352
- return /* @__PURE__ */ jsxs(
353
- "div",
354
- {
355
- onMouseEnter: onHover,
356
- onMouseLeave: onLeave,
357
- onClick,
358
- style: {
359
- padding: "12px",
360
- marginBottom: "8px",
361
- backgroundColor: isHighlighted ? "#374151" : "#111827",
362
- borderRadius: "8px",
363
- border: isLocked ? "1px solid #3B82F6" : isSelected ? "1px solid #4B5563" : "1px solid transparent",
364
- cursor: "pointer",
365
- transition: "all 0.15s"
366
- },
367
- children: [
368
- /* @__PURE__ */ jsxs(
369
- "div",
370
- {
371
- style: {
372
- display: "flex",
373
- alignItems: "center",
374
- gap: "8px",
375
- marginBottom: "8px"
376
- },
377
- children: [
378
- /* @__PURE__ */ jsx(
379
- "div",
380
- {
381
- style: {
382
- display: "inline-block",
383
- padding: "2px 8px",
384
- borderRadius: "4px",
385
- backgroundColor: `${categoryColor}20`,
386
- color: categoryColor,
387
- fontSize: "11px",
388
- fontWeight: "600",
389
- textTransform: "uppercase"
390
- },
391
- children: violation.category
392
- }
393
- ),
394
- /* @__PURE__ */ jsx(
395
- "span",
396
- {
397
- style: {
398
- fontSize: "12px",
399
- color: violation.severity === "error" ? "#EF4444" : violation.severity === "warning" ? "#F59E0B" : "#9CA3AF"
400
- },
401
- children: severityIcon
402
- }
403
- ),
404
- /* @__PURE__ */ jsxs(
405
- "span",
406
- {
407
- style: {
408
- fontSize: "11px",
409
- color: "#6B7280",
410
- marginLeft: "auto"
411
- },
412
- children: [
413
- violation.elementIds.length,
414
- " element",
415
- violation.elementIds.length !== 1 ? "s" : ""
416
- ]
417
- }
418
- )
419
- ]
420
- }
421
- ),
422
- /* @__PURE__ */ jsx(
423
- "div",
424
- {
425
- style: {
426
- fontSize: "13px",
427
- color: "#F3F4F6",
428
- lineHeight: "1.4",
429
- marginBottom: "8px"
430
- },
431
- children: violation.message
432
- }
433
- ),
434
- violation.details && /* @__PURE__ */ jsxs(
435
- "div",
436
- {
437
- style: {
438
- fontSize: "12px",
439
- color: "#9CA3AF"
440
- },
441
- children: [
442
- violation.details.property && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "4px" }, children: [
443
- /* @__PURE__ */ jsx("span", { style: { color: "#6B7280" }, children: "Property: " }),
444
- /* @__PURE__ */ jsx(
445
- "code",
446
- {
447
- style: {
448
- padding: "2px 4px",
449
- backgroundColor: "#374151",
450
- borderRadius: "3px",
451
- fontSize: "11px"
452
- },
453
- children: violation.details.property
454
- }
455
- )
456
- ] }),
457
- violation.details.values.length > 0 && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "4px" }, children: [
458
- /* @__PURE__ */ jsx("span", { style: { color: "#6B7280" }, children: "Values: " }),
459
- violation.details.values.map((val, i) => /* @__PURE__ */ jsxs("span", { children: [
460
- /* @__PURE__ */ jsx(
461
- "code",
462
- {
463
- style: {
464
- padding: "2px 4px",
465
- backgroundColor: "#374151",
466
- borderRadius: "3px",
467
- fontSize: "11px"
468
- },
469
- children: val
470
- }
471
- ),
472
- i < violation.details.values.length - 1 && /* @__PURE__ */ jsx("span", { style: { margin: "0 4px", color: "#6B7280" }, children: "vs" })
473
- ] }, i))
474
- ] })
475
- ]
476
- }
477
- ),
478
- violation.details.suggestion && /* @__PURE__ */ jsxs(
479
- "div",
480
- {
481
- style: {
482
- marginTop: "8px",
483
- padding: "8px",
484
- backgroundColor: "#1E3A5F",
485
- borderRadius: "4px",
486
- fontSize: "12px",
487
- color: "#93C5FD"
488
- },
489
- children: [
490
- "\u{1F4A1} ",
491
- violation.details.suggestion
492
- ]
493
- }
494
- ),
495
- isLocked && /* @__PURE__ */ jsx(
496
- "div",
497
- {
498
- style: {
499
- marginTop: "8px",
500
- fontSize: "11px",
501
- color: "#3B82F6"
502
- },
503
- children: "\u{1F512} Click to unlock"
504
- }
505
- )
506
- ]
507
- }
508
- );
509
- }
510
-
511
- // src/components/QuestionPanel.tsx
512
- import { useState } from "react";
513
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
514
- function QuestionPanel() {
515
- const { issues } = useUILint();
516
- const [answers, setAnswers] = useState({});
517
- const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
518
- const [isSaving, setIsSaving] = useState(false);
519
- const [saveSuccess, setSaveSuccess] = useState(false);
520
- const [saveError, setSaveError] = useState(null);
521
- const questions = generateQuestionsFromIssues(issues);
522
- if (questions.length === 0) {
523
- return /* @__PURE__ */ jsxs2(
524
- "div",
525
- {
526
- style: {
527
- padding: "32px 16px",
528
- textAlign: "center",
529
- color: "#9CA3AF"
530
- },
531
- children: [
532
- /* @__PURE__ */ jsx2("div", { style: { fontSize: "32px", marginBottom: "8px" }, children: "\u{1F3AF}" }),
533
- /* @__PURE__ */ jsx2("div", { style: { fontSize: "14px" }, children: "No style conflicts to resolve" }),
534
- /* @__PURE__ */ jsx2("div", { style: { fontSize: "12px", marginTop: "4px" }, children: "Scan the page to detect inconsistencies" })
535
- ]
536
- }
537
- );
538
- }
539
- const currentQuestion = questions[currentQuestionIndex];
540
- const handleAnswer = (value) => {
541
- setAnswers((prev) => ({
542
- ...prev,
543
- [currentQuestion.id]: value
544
- }));
545
- if (currentQuestionIndex < questions.length - 1) {
546
- setCurrentQuestionIndex((prev) => prev + 1);
547
- }
548
- };
549
- const handleSaveToStyleGuide = async () => {
550
- console.log("[UILint] Saving preferences:", answers);
551
- setIsSaving(true);
552
- setSaveSuccess(false);
553
- setSaveError(null);
554
- try {
555
- const getResponse = await fetch("/api/uilint/styleguide");
556
- const data = await getResponse.json().catch(() => ({}));
557
- if (!getResponse.ok || !data?.exists || !data?.content) {
558
- throw new Error(
559
- data?.error || 'No style guide found. Create ".uilint/styleguide.md" at your workspace root first.'
560
- );
561
- }
562
- const updatedContent = applyAnswersToStyleGuide(data.content, answers);
563
- const postResponse = await fetch("/api/uilint/styleguide", {
564
- method: "POST",
565
- headers: { "Content-Type": "application/json" },
566
- body: JSON.stringify({ content: updatedContent })
567
- });
568
- if (!postResponse.ok) {
569
- const err = await postResponse.json().catch(() => ({}));
570
- throw new Error(err?.error || "Failed to save style guide");
571
- }
572
- console.log("[UILint] Style guide saved successfully!");
573
- setSaveSuccess(true);
574
- setTimeout(() => {
575
- setAnswers({});
576
- setCurrentQuestionIndex(0);
577
- setSaveSuccess(false);
578
- }, 1500);
579
- } catch (error) {
580
- console.error("[UILint] Error saving style guide:", error);
581
- setSaveError(error instanceof Error ? error.message : "Save failed");
582
- } finally {
583
- setIsSaving(false);
584
- }
585
- };
586
- const isComplete = Object.keys(answers).length === questions.length;
587
- return /* @__PURE__ */ jsxs2("div", { style: { padding: "16px" }, children: [
588
- /* @__PURE__ */ jsxs2(
589
- "div",
590
- {
591
- style: {
592
- display: "flex",
593
- justifyContent: "space-between",
594
- alignItems: "center",
595
- marginBottom: "16px"
596
- },
597
- children: [
598
- /* @__PURE__ */ jsxs2("span", { style: { fontSize: "12px", color: "#9CA3AF" }, children: [
599
- "Question ",
600
- currentQuestionIndex + 1,
601
- " of ",
602
- questions.length
603
- ] }),
604
- /* @__PURE__ */ jsx2(
605
- "div",
606
- {
607
- style: {
608
- width: "100px",
609
- height: "4px",
610
- backgroundColor: "#374151",
611
- borderRadius: "2px",
612
- overflow: "hidden"
613
- },
614
- children: /* @__PURE__ */ jsx2(
615
- "div",
616
- {
617
- style: {
618
- width: `${(currentQuestionIndex + 1) / questions.length * 100}%`,
619
- height: "100%",
620
- backgroundColor: "#3B82F6",
621
- transition: "width 0.3s"
622
- }
623
- }
624
- )
625
- }
626
- )
627
- ]
628
- }
629
- ),
630
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: "16px" }, children: [
631
- /* @__PURE__ */ jsx2(
632
- "div",
633
- {
634
- style: {
635
- fontSize: "14px",
636
- fontWeight: "500",
637
- color: "#F3F4F6",
638
- marginBottom: "8px"
639
- },
640
- children: currentQuestion.question
641
- }
642
- ),
643
- currentQuestion.context && /* @__PURE__ */ jsx2(
644
- "div",
645
- {
646
- style: {
647
- fontSize: "12px",
648
- color: "#9CA3AF",
649
- marginBottom: "12px"
650
- },
651
- children: currentQuestion.context
652
- }
653
- )
654
- ] }),
655
- /* @__PURE__ */ jsx2("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: currentQuestion.options.map((option) => /* @__PURE__ */ jsxs2(
656
- "button",
657
- {
658
- onClick: () => handleAnswer(option.value),
659
- style: {
660
- display: "flex",
661
- alignItems: "center",
662
- gap: "12px",
663
- padding: "12px",
664
- backgroundColor: answers[currentQuestion.id] === option.value ? "#374151" : "#111827",
665
- border: answers[currentQuestion.id] === option.value ? "1px solid #3B82F6" : "1px solid #374151",
666
- borderRadius: "8px",
667
- color: "#F3F4F6",
668
- fontSize: "13px",
669
- textAlign: "left",
670
- cursor: "pointer",
671
- transition: "all 0.15s"
672
- },
673
- children: [
674
- option.preview && /* @__PURE__ */ jsx2(
675
- "div",
676
- {
677
- style: {
678
- width: "32px",
679
- height: "32px",
680
- borderRadius: "4px",
681
- display: "flex",
682
- alignItems: "center",
683
- justifyContent: "center"
684
- },
685
- children: option.preview
686
- }
687
- ),
688
- /* @__PURE__ */ jsx2("span", { children: option.label })
689
- ]
690
- },
691
- option.value
692
- )) }),
693
- /* @__PURE__ */ jsxs2(
694
- "div",
695
- {
696
- style: {
697
- display: "flex",
698
- justifyContent: "space-between",
699
- marginTop: "16px"
700
- },
701
- children: [
702
- /* @__PURE__ */ jsx2(
703
- "button",
704
- {
705
- onClick: () => setCurrentQuestionIndex((prev) => Math.max(0, prev - 1)),
706
- disabled: currentQuestionIndex === 0,
707
- style: {
708
- padding: "8px 16px",
709
- backgroundColor: "transparent",
710
- border: "1px solid #374151",
711
- borderRadius: "6px",
712
- color: currentQuestionIndex === 0 ? "#4B5563" : "#9CA3AF",
713
- fontSize: "12px",
714
- cursor: currentQuestionIndex === 0 ? "not-allowed" : "pointer"
715
- },
716
- children: "\u2190 Back"
717
- }
718
- ),
719
- isComplete && /* @__PURE__ */ jsx2(
720
- "button",
721
- {
722
- onClick: handleSaveToStyleGuide,
723
- disabled: isSaving,
724
- style: {
725
- padding: "8px 16px",
726
- backgroundColor: saveSuccess ? "#059669" : isSaving ? "#6B7280" : "#10B981",
727
- border: "none",
728
- borderRadius: "6px",
729
- color: "white",
730
- fontSize: "12px",
731
- fontWeight: "500",
732
- cursor: isSaving ? "wait" : "pointer",
733
- opacity: isSaving ? 0.8 : 1,
734
- transition: "all 0.2s"
735
- },
736
- children: saveSuccess ? "\u2713 Saved!" : isSaving ? "Saving..." : "Save to Style Guide"
737
- }
738
- )
739
- ]
740
- }
741
- ),
742
- saveError && /* @__PURE__ */ jsx2(
743
- "div",
744
- {
745
- style: {
746
- marginTop: "12px",
747
- padding: "10px",
748
- borderRadius: "8px",
749
- backgroundColor: "#7F1D1D",
750
- border: "1px solid #EF4444",
751
- color: "#FEE2E2",
752
- fontSize: "12px",
753
- lineHeight: 1.4
754
- },
755
- children: saveError
756
- }
757
- )
758
- ] });
759
- }
760
- function generateQuestionsFromIssues(issues) {
761
- const questions = [];
762
- const colorIssues = issues.filter((i) => i.type === "color");
763
- if (colorIssues.length > 0) {
764
- const colors = /* @__PURE__ */ new Set();
765
- colorIssues.forEach((issue) => {
766
- if (issue.currentValue) colors.add(issue.currentValue);
767
- if (issue.expectedValue) colors.add(issue.expectedValue);
768
- });
769
- if (colors.size >= 2) {
770
- const colorArray = Array.from(colors);
771
- questions.push({
772
- id: "primary-color",
773
- question: "Which color should be used as the primary color?",
774
- context: "Multiple similar colors were detected. Choose one for consistency.",
775
- options: colorArray.slice(0, 4).map((color) => ({
776
- value: color,
777
- label: color,
778
- preview: /* @__PURE__ */ jsx2(
779
- "div",
780
- {
781
- style: {
782
- width: "100%",
783
- height: "100%",
784
- backgroundColor: color,
785
- borderRadius: "4px"
786
- }
787
- }
788
- )
789
- }))
790
- });
791
- }
792
- }
793
- const spacingIssues = issues.filter((i) => i.type === "spacing");
794
- if (spacingIssues.length > 0) {
795
- questions.push({
796
- id: "spacing-scale",
797
- question: "What spacing scale should be used?",
798
- context: "Choose a base unit for consistent spacing throughout the UI.",
799
- options: [
800
- { value: "4", label: "4px base (4, 8, 12, 16, 20, 24...)" },
801
- { value: "8", label: "8px base (8, 16, 24, 32, 40...)" },
802
- {
803
- value: "tailwind",
804
- label: "Tailwind scale (4, 8, 12, 16, 20, 24...)"
805
- }
806
- ]
807
- });
808
- }
809
- const typographyIssues = issues.filter((i) => i.type === "typography");
810
- if (typographyIssues.length > 0) {
811
- questions.push({
812
- id: "font-weights",
813
- question: "Which font weights should be used?",
814
- context: "Select the weights to use for consistency.",
815
- options: [
816
- {
817
- value: "400-600-700",
818
- label: "Regular (400), Semibold (600), Bold (700)"
819
- },
820
- {
821
- value: "400-500-700",
822
- label: "Regular (400), Medium (500), Bold (700)"
823
- },
824
- {
825
- value: "300-400-600",
826
- label: "Light (300), Regular (400), Semibold (600)"
827
- }
828
- ]
829
- });
830
- }
831
- return questions;
832
- }
833
- function applyAnswersToStyleGuide(existingContent, answers) {
834
- let content = existingContent;
835
- if (answers["primary-color"]) {
836
- content = upsertBulletInSection(
837
- content,
838
- "Colors",
839
- "Primary",
840
- answers["primary-color"]
841
- );
842
- }
843
- if (answers["font-weights"]) {
844
- const weightMap = {
845
- "400-600-700": "400 (Regular), 600 (Semibold), 700 (Bold)",
846
- "400-500-700": "400 (Regular), 500 (Medium), 700 (Bold)",
847
- "300-400-600": "300 (Light), 400 (Regular), 600 (Semibold)"
848
- };
849
- const value = weightMap[answers["font-weights"]] || answers["font-weights"];
850
- content = upsertBulletInSection(
851
- content,
852
- "Typography",
853
- "Font Weights",
854
- value
855
- );
856
- }
857
- if (answers["spacing-scale"]) {
858
- const spacingMap = {
859
- "4": "4px (4, 8, 12, 16, 20, 24, 32, 40, 48...)",
860
- "8": "8px (8, 16, 24, 32, 40, 48, 56, 64...)",
861
- tailwind: "Tailwind (4, 8, 12, 16, 20, 24, 32, 40, 48...)"
862
- };
863
- const value = spacingMap[answers["spacing-scale"]] || answers["spacing-scale"];
864
- content = upsertBulletInSection(content, "Spacing", "Base unit", value);
865
- }
866
- return content;
867
- }
868
- function upsertBulletInSection(markdown, sectionName, label, value) {
869
- const lines = markdown.split("\n");
870
- const sectionStart = lines.findIndex(
871
- (line) => line.match(new RegExp(`^##\\s+${sectionName}\\s*$`, "i"))
872
- );
873
- if (sectionStart === -1) {
874
- throw new Error(
875
- `Style guide is missing section "## ${sectionName}". Add it to your style guide and try again.`
876
- );
877
- }
878
- let sectionEnd = lines.length;
879
- for (let i = sectionStart + 1; i < lines.length; i++) {
880
- if (lines[i].startsWith("## ")) {
881
- sectionEnd = i;
882
- break;
883
- }
884
- }
885
- const bulletRe = new RegExp(
886
- `^-\\s+\\*\\*${escapeRegExp(label)}\\*\\*:\\s+.*$`
887
- );
888
- const newBullet = `- **${label}**: ${value}`;
889
- for (let i = sectionStart + 1; i < sectionEnd; i++) {
890
- if (bulletRe.test(lines[i])) {
891
- lines[i] = newBullet;
892
- return lines.join("\n");
893
- }
894
- }
895
- let insertAt = sectionStart + 1;
896
- while (insertAt < sectionEnd && lines[insertAt].trim() === "") insertAt++;
897
- lines.splice(insertAt, 0, newBullet);
898
- return lines.join("\n");
899
- }
900
- function escapeRegExp(s) {
901
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
902
- }
903
-
904
- // src/components/Overlay.tsx
905
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
906
- function Overlay({ position }) {
907
- const [isExpanded, setIsExpanded] = useState2(false);
908
- const { violations, isScanning, scan, elementCount } = useUILint();
909
- const positionStyles = {
910
- position: "fixed",
911
- zIndex: 99999,
912
- ...position.includes("bottom") ? { bottom: "16px" } : { top: "16px" },
913
- ...position.includes("left") ? { left: "16px" } : { right: "16px" }
914
- };
915
- const violationCount = violations.length;
916
- const hasViolations = violationCount > 0;
917
- return /* @__PURE__ */ jsx3("div", { style: positionStyles, children: isExpanded ? /* @__PURE__ */ jsx3(
918
- ExpandedPanel,
919
- {
920
- onCollapse: () => setIsExpanded(false),
921
- onScan: scan,
922
- isScanning,
923
- elementCount
924
- }
925
- ) : /* @__PURE__ */ jsx3(
926
- CollapsedButton,
927
- {
928
- onClick: () => setIsExpanded(true),
929
- violationCount,
930
- hasViolations,
931
- isScanning
932
- }
933
- ) });
934
- }
935
- function CollapsedButton({
936
- onClick,
937
- violationCount,
938
- hasViolations,
939
- isScanning
940
- }) {
941
- return /* @__PURE__ */ jsx3(
942
- "button",
943
- {
944
- onClick,
945
- style: {
946
- display: "flex",
947
- alignItems: "center",
948
- justifyContent: "center",
949
- width: "48px",
950
- height: "48px",
951
- borderRadius: "50%",
952
- border: "none",
953
- backgroundColor: isScanning ? "#3B82F6" : hasViolations ? "#EF4444" : "#10B981",
954
- color: "white",
955
- cursor: "pointer",
956
- boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
957
- transition: "transform 0.2s, box-shadow 0.2s",
958
- fontSize: "20px",
959
- fontWeight: "bold"
960
- },
961
- onMouseEnter: (e) => {
962
- e.currentTarget.style.transform = "scale(1.1)";
963
- e.currentTarget.style.boxShadow = "0 6px 16px rgba(0, 0, 0, 0.2)";
964
- },
965
- onMouseLeave: (e) => {
966
- e.currentTarget.style.transform = "scale(1)";
967
- e.currentTarget.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.15)";
968
- },
969
- title: isScanning ? "Analyzing..." : `UILint: ${violationCount} issue${violationCount !== 1 ? "s" : ""} found`,
970
- children: isScanning ? /* @__PURE__ */ jsx3(SpinnerIcon, {}) : hasViolations ? violationCount : "\u2713"
971
- }
972
- );
973
- }
974
- function SpinnerIcon() {
975
- return /* @__PURE__ */ jsxs3(
976
- "svg",
977
- {
978
- width: "20",
979
- height: "20",
980
- viewBox: "0 0 24 24",
981
- fill: "none",
982
- style: {
983
- animation: "uilint-spin 1s linear infinite"
984
- },
985
- children: [
986
- /* @__PURE__ */ jsx3("style", { children: `
987
- @keyframes uilint-spin {
988
- from { transform: rotate(0deg); }
989
- to { transform: rotate(360deg); }
990
- }
991
- ` }),
992
- /* @__PURE__ */ jsx3(
993
- "circle",
994
- {
995
- cx: "12",
996
- cy: "12",
997
- r: "10",
998
- stroke: "currentColor",
999
- strokeWidth: "3",
1000
- strokeLinecap: "round",
1001
- strokeDasharray: "31.4 31.4",
1002
- fill: "none"
1003
- }
1004
- )
1005
- ]
1006
- }
1007
- );
1008
- }
1009
- function ExpandedPanel({
1010
- onCollapse,
1011
- onScan,
1012
- isScanning,
1013
- elementCount
1014
- }) {
1015
- const [activeTab, setActiveTab] = useState2(
1016
- "violations"
1017
- );
1018
- return /* @__PURE__ */ jsxs3(
1019
- "div",
1020
- {
1021
- style: {
1022
- width: "380px",
1023
- maxHeight: "500px",
1024
- backgroundColor: "#1F2937",
1025
- borderRadius: "12px",
1026
- boxShadow: "0 8px 32px rgba(0, 0, 0, 0.3)",
1027
- overflow: "hidden",
1028
- fontFamily: "system-ui, -apple-system, sans-serif",
1029
- color: "#F9FAFB"
1030
- },
1031
- children: [
1032
- /* @__PURE__ */ jsxs3(
1033
- "div",
1034
- {
1035
- style: {
1036
- display: "flex",
1037
- alignItems: "center",
1038
- justifyContent: "space-between",
1039
- padding: "12px 16px",
1040
- borderBottom: "1px solid #374151",
1041
- backgroundColor: "#111827"
1042
- },
1043
- children: [
1044
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
1045
- /* @__PURE__ */ jsx3("span", { style: { fontSize: "16px" }, children: "\u{1F3A8}" }),
1046
- /* @__PURE__ */ jsx3("span", { style: { fontWeight: "600", fontSize: "14px" }, children: "UILint" }),
1047
- elementCount > 0 && !isScanning && /* @__PURE__ */ jsxs3(
1048
- "span",
1049
- {
1050
- style: {
1051
- fontSize: "11px",
1052
- color: "#6B7280",
1053
- marginLeft: "4px"
1054
- },
1055
- children: [
1056
- "(",
1057
- elementCount,
1058
- " elements)"
1059
- ]
1060
- }
1061
- )
1062
- ] }),
1063
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
1064
- /* @__PURE__ */ jsxs3(
1065
- "button",
1066
- {
1067
- onClick: onScan,
1068
- disabled: isScanning,
1069
- style: {
1070
- padding: "6px 12px",
1071
- borderRadius: "6px",
1072
- border: "none",
1073
- backgroundColor: "#3B82F6",
1074
- color: "white",
1075
- fontSize: "12px",
1076
- fontWeight: "500",
1077
- cursor: isScanning ? "not-allowed" : "pointer",
1078
- opacity: isScanning ? 0.7 : 1,
1079
- display: "flex",
1080
- alignItems: "center",
1081
- gap: "6px"
1082
- },
1083
- children: [
1084
- isScanning && /* @__PURE__ */ jsx3(
1085
- "svg",
1086
- {
1087
- width: "12",
1088
- height: "12",
1089
- viewBox: "0 0 24 24",
1090
- fill: "none",
1091
- style: {
1092
- animation: "uilint-spin 1s linear infinite"
1093
- },
1094
- children: /* @__PURE__ */ jsx3(
1095
- "circle",
1096
- {
1097
- cx: "12",
1098
- cy: "12",
1099
- r: "10",
1100
- stroke: "currentColor",
1101
- strokeWidth: "3",
1102
- strokeLinecap: "round",
1103
- strokeDasharray: "31.4 31.4",
1104
- fill: "none"
1105
- }
1106
- )
1107
- }
1108
- ),
1109
- isScanning ? "Analyzing..." : "Scan"
1110
- ]
1111
- }
1112
- ),
1113
- /* @__PURE__ */ jsx3(
1114
- "button",
1115
- {
1116
- onClick: onCollapse,
1117
- style: {
1118
- padding: "6px 8px",
1119
- borderRadius: "6px",
1120
- border: "none",
1121
- backgroundColor: "transparent",
1122
- color: "#9CA3AF",
1123
- fontSize: "16px",
1124
- cursor: "pointer"
1125
- },
1126
- children: "\u2715"
1127
- }
1128
- )
1129
- ] })
1130
- ]
1131
- }
1132
- ),
1133
- /* @__PURE__ */ jsxs3(
1134
- "div",
1135
- {
1136
- style: {
1137
- display: "flex",
1138
- borderBottom: "1px solid #374151"
1139
- },
1140
- children: [
1141
- /* @__PURE__ */ jsx3(
1142
- TabButton,
1143
- {
1144
- active: activeTab === "violations",
1145
- onClick: () => setActiveTab("violations"),
1146
- children: "Violations"
1147
- }
1148
- ),
1149
- /* @__PURE__ */ jsx3(
1150
- TabButton,
1151
- {
1152
- active: activeTab === "questions",
1153
- onClick: () => setActiveTab("questions"),
1154
- children: "Questions"
1155
- }
1156
- )
1157
- ]
1158
- }
1159
- ),
1160
- /* @__PURE__ */ jsx3("div", { style: { maxHeight: "380px", overflow: "auto" }, children: activeTab === "violations" ? /* @__PURE__ */ jsx3(ViolationList, {}) : /* @__PURE__ */ jsx3(QuestionPanel, {}) })
1161
- ]
1162
- }
1163
- );
1164
- }
1165
- function TabButton({ active, onClick, children }) {
1166
- return /* @__PURE__ */ jsx3(
1167
- "button",
1168
- {
1169
- onClick,
1170
- style: {
1171
- flex: 1,
1172
- padding: "10px 16px",
1173
- border: "none",
1174
- backgroundColor: "transparent",
1175
- color: active ? "#3B82F6" : "#9CA3AF",
1176
- fontSize: "13px",
1177
- fontWeight: "500",
1178
- cursor: "pointer",
1179
- borderBottom: active ? "2px solid #3B82F6" : "2px solid transparent",
1180
- marginBottom: "-1px"
1181
- },
1182
- children
1183
- }
1184
- );
1185
- }
1186
-
1187
235
  // src/consistency/highlights.tsx
1188
- import { useEffect, useState as useState3, useCallback } from "react";
236
+ import { useEffect, useState, useCallback } from "react";
1189
237
  import { createPortal } from "react-dom";
1190
- import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
238
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1191
239
  var HIGHLIGHT_COLOR = "#3b82f6";
1192
240
  var DOT_SIZE = 8;
1193
241
  var BORDER_WIDTH = 2;
@@ -1218,7 +266,7 @@ function getAllViolatingIds(violations) {
1218
266
  return ids;
1219
267
  }
1220
268
  function OverviewDot({ rect }) {
1221
- return /* @__PURE__ */ jsx4(
269
+ return /* @__PURE__ */ jsx(
1222
270
  "div",
1223
271
  {
1224
272
  style: {
@@ -1240,8 +288,8 @@ function HighlightRect({
1240
288
  rect,
1241
289
  badgeNumber
1242
290
  }) {
1243
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
1244
- /* @__PURE__ */ jsx4(
291
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
292
+ /* @__PURE__ */ jsx(
1245
293
  "div",
1246
294
  {
1247
295
  style: {
@@ -1259,7 +307,7 @@ function HighlightRect({
1259
307
  }
1260
308
  }
1261
309
  ),
1262
- badgeNumber !== void 0 && /* @__PURE__ */ jsx4(
310
+ badgeNumber !== void 0 && /* @__PURE__ */ jsx(
1263
311
  "div",
1264
312
  {
1265
313
  style: {
@@ -1291,11 +339,11 @@ function ConsistencyHighlighter({
1291
339
  selectedViolation,
1292
340
  lockedViolation
1293
341
  }) {
1294
- const [overviewHighlights, setOverviewHighlights] = useState3([]);
1295
- const [activeHighlights, setActiveHighlights] = useState3(
342
+ const [overviewHighlights, setOverviewHighlights] = useState([]);
343
+ const [activeHighlights, setActiveHighlights] = useState(
1296
344
  []
1297
345
  );
1298
- const [mounted, setMounted] = useState3(false);
346
+ const [mounted, setMounted] = useState(false);
1299
347
  const activeViolation = lockedViolation || selectedViolation;
1300
348
  const updateOverviewHighlights = useCallback(() => {
1301
349
  if (activeViolation) {
@@ -1348,135 +396,13 @@ function ConsistencyHighlighter({
1348
396
  }, [updateOverviewHighlights, updateActiveHighlights]);
1349
397
  if (!mounted) return null;
1350
398
  if (violations.length === 0) return null;
1351
- const content = /* @__PURE__ */ jsxs4(Fragment, { children: [
1352
- !activeViolation && overviewHighlights.map((h) => /* @__PURE__ */ jsx4(OverviewDot, { rect: h.rect }, h.id)),
1353
- activeViolation && activeHighlights.map((h) => /* @__PURE__ */ jsx4(HighlightRect, { rect: h.rect, badgeNumber: h.badgeNumber }, h.id))
399
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
400
+ !activeViolation && overviewHighlights.map((h) => /* @__PURE__ */ jsx(OverviewDot, { rect: h.rect }, h.id)),
401
+ activeViolation && activeHighlights.map((h) => /* @__PURE__ */ jsx(HighlightRect, { rect: h.rect, badgeNumber: h.badgeNumber }, h.id))
1354
402
  ] });
1355
403
  return createPortal(content, document.body);
1356
404
  }
1357
405
 
1358
- // src/components/UILint.tsx
1359
- import { countElements } from "uilint-core";
1360
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1361
- var UILintContext = createContext(null);
1362
- function useUILint() {
1363
- const context = useContext(UILintContext);
1364
- if (!context) {
1365
- throw new Error("useUILint must be used within a UILint component");
1366
- }
1367
- return context;
1368
- }
1369
- function UILint({
1370
- children,
1371
- enabled = true,
1372
- position = "bottom-left",
1373
- autoScan = false,
1374
- apiEndpoint = "/api/uilint/consistency"
1375
- }) {
1376
- const [violations, setViolations] = useState4([]);
1377
- const [isScanning, setIsScanning] = useState4(false);
1378
- const [elementCount, setElementCount] = useState4(0);
1379
- const [selectedViolation, setSelectedViolation] = useState4(
1380
- null
1381
- );
1382
- const [lockedViolation, setLockedViolation] = useState4(
1383
- null
1384
- );
1385
- const [isMounted, setIsMounted] = useState4(false);
1386
- const hasInitialized = useRef(false);
1387
- useEffect2(() => {
1388
- setIsMounted(true);
1389
- }, []);
1390
- useEffect2(() => {
1391
- return () => {
1392
- if (isBrowser()) {
1393
- cleanupDataElements();
1394
- }
1395
- };
1396
- }, []);
1397
- const scan = useCallback2(async () => {
1398
- if (!isBrowser()) return;
1399
- setIsScanning(true);
1400
- setSelectedViolation(null);
1401
- setLockedViolation(null);
1402
- try {
1403
- const snapshot = createSnapshot(document.body);
1404
- const count = countElements(snapshot);
1405
- setElementCount(count);
1406
- const response = await fetch(apiEndpoint, {
1407
- method: "POST",
1408
- headers: { "Content-Type": "application/json" },
1409
- body: JSON.stringify({ snapshot })
1410
- });
1411
- if (!response.ok) {
1412
- const errorData = await response.json().catch(() => ({}));
1413
- console.error(
1414
- "[UILint] Analysis failed:",
1415
- errorData.error || response.statusText
1416
- );
1417
- setViolations([]);
1418
- return;
1419
- }
1420
- const result = await response.json();
1421
- setViolations(result.violations);
1422
- if (result.violations.length === 0) {
1423
- console.log(`[UILint] No consistency issues found (${count} elements)`);
1424
- } else {
1425
- console.log(
1426
- `[UILint] Found ${result.violations.length} consistency issue(s)`
1427
- );
1428
- }
1429
- } catch (error) {
1430
- console.error("[UILint] Scan failed:", error);
1431
- setViolations([]);
1432
- } finally {
1433
- setIsScanning(false);
1434
- }
1435
- }, [apiEndpoint]);
1436
- const clearViolations = useCallback2(() => {
1437
- setViolations([]);
1438
- setSelectedViolation(null);
1439
- setLockedViolation(null);
1440
- cleanupDataElements();
1441
- setElementCount(0);
1442
- }, []);
1443
- useEffect2(() => {
1444
- if (!enabled || hasInitialized.current) return;
1445
- hasInitialized.current = true;
1446
- if (!isBrowser()) return;
1447
- if (autoScan) {
1448
- const timer = setTimeout(scan, 1e3);
1449
- return () => clearTimeout(timer);
1450
- }
1451
- }, [enabled, autoScan, scan]);
1452
- const contextValue = {
1453
- violations,
1454
- isScanning,
1455
- elementCount,
1456
- scan,
1457
- clearViolations,
1458
- selectedViolation,
1459
- setSelectedViolation,
1460
- lockedViolation,
1461
- setLockedViolation
1462
- };
1463
- const shouldRenderOverlay = enabled && isMounted;
1464
- return /* @__PURE__ */ jsxs5(UILintContext.Provider, { value: contextValue, children: [
1465
- children,
1466
- shouldRenderOverlay && /* @__PURE__ */ jsxs5(Fragment2, { children: [
1467
- /* @__PURE__ */ jsx5(Overlay, { position }),
1468
- /* @__PURE__ */ jsx5(
1469
- ConsistencyHighlighter,
1470
- {
1471
- violations,
1472
- selectedViolation,
1473
- lockedViolation
1474
- }
1475
- )
1476
- ] })
1477
- ] });
1478
- }
1479
-
1480
406
  // src/scanner/dom-scanner.ts
1481
407
  import {
1482
408
  extractStylesFromDOM,
@@ -1496,6 +422,19 @@ function scanDOM(root) {
1496
422
  };
1497
423
  }
1498
424
 
425
+ // src/scanner/environment.ts
426
+ function isBrowser() {
427
+ return typeof window !== "undefined" && typeof window.document !== "undefined";
428
+ }
429
+ function isJSDOM() {
430
+ if (!isBrowser()) return false;
431
+ const userAgent = window.navigator?.userAgent || "";
432
+ return userAgent.includes("jsdom");
433
+ }
434
+ function isNode() {
435
+ return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
436
+ }
437
+
1499
438
  // src/index.ts
1500
439
  import {
1501
440
  extractStylesFromDOM as extractStylesFromDOM2,
@@ -1589,8 +528,7 @@ export {
1589
528
  FILE_COLORS,
1590
529
  InspectionPanel,
1591
530
  LLMClient,
1592
- SourceOverlays,
1593
- UILint,
531
+ LocatorOverlay,
1594
532
  UILintProvider,
1595
533
  UILintToolbar,
1596
534
  buildEditorUrl,
@@ -1624,7 +562,5 @@ export {
1624
562
  scanDOMForSources,
1625
563
  serializeStyles2 as serializeStyles,
1626
564
  updateElementRects,
1627
- useElementScan,
1628
- useUILint,
1629
565
  useUILintContext
1630
566
  };