uilint-react 0.1.38 → 0.1.39

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.
@@ -3,10 +3,10 @@ import {
3
3
  groupBySourceFile,
4
4
  useUILintContext,
5
5
  useUILintStore
6
- } from "./chunk-PU6XPNPN.js";
6
+ } from "./chunk-ILK73X6L.js";
7
7
 
8
8
  // src/components/ui-lint/UILintToolbar.tsx
9
- import { useState, useRef, useEffect, useCallback } from "react";
9
+ import { useState as useState2, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
10
10
  import { createPortal } from "react-dom";
11
11
 
12
12
  // src/components/ui-lint/toolbar-styles.ts
@@ -57,21 +57,11 @@ var STYLES = {
57
57
  };
58
58
  function getStatusColor(issueCount) {
59
59
  if (issueCount === 0) return STYLES.success;
60
- if (issueCount <= 2) return STYLES.warning;
61
- return STYLES.error;
60
+ return STYLES.warning;
62
61
  }
63
62
 
64
63
  // src/components/ui-lint/toolbar-icons.tsx
65
64
  import { jsx, jsxs } from "react/jsx-runtime";
66
- function PauseIcon() {
67
- return /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: [
68
- /* @__PURE__ */ jsx("rect", { x: "6", y: "4", width: "4", height: "16", rx: "1", fill: "currentColor" }),
69
- /* @__PURE__ */ jsx("rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", fill: "currentColor" })
70
- ] });
71
- }
72
- function PlayIcon() {
73
- return /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M8 5v14l11-7L8 5z", fill: "currentColor" }) });
74
- }
75
65
  function StopIcon() {
76
66
  return /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "1", fill: "currentColor" }) });
77
67
  }
@@ -82,30 +72,6 @@ function EllipsisIcon() {
82
72
  /* @__PURE__ */ jsx("circle", { cx: "19", cy: "12", r: "2", fill: "currentColor" })
83
73
  ] });
84
74
  }
85
- function MagnifyingGlassIcon() {
86
- return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: [
87
- /* @__PURE__ */ jsx(
88
- "circle",
89
- {
90
- cx: "11",
91
- cy: "11",
92
- r: "7",
93
- stroke: "currentColor",
94
- strokeWidth: "2",
95
- strokeLinecap: "round"
96
- }
97
- ),
98
- /* @__PURE__ */ jsx(
99
- "path",
100
- {
101
- d: "M21 21l-4.35-4.35",
102
- stroke: "currentColor",
103
- strokeWidth: "2",
104
- strokeLinecap: "round"
105
- }
106
- )
107
- ] });
108
- }
109
75
  function CheckCircleIcon() {
110
76
  return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: [
111
77
  /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9", stroke: "currentColor", strokeWidth: "2" }),
@@ -156,6 +122,69 @@ function SpinnerIcon() {
156
122
  }
157
123
  );
158
124
  }
125
+ function EyeIcon() {
126
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: [
127
+ /* @__PURE__ */ jsx(
128
+ "path",
129
+ {
130
+ d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z",
131
+ stroke: "currentColor",
132
+ strokeWidth: "2",
133
+ strokeLinecap: "round",
134
+ strokeLinejoin: "round"
135
+ }
136
+ ),
137
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3", stroke: "currentColor", strokeWidth: "2" })
138
+ ] });
139
+ }
140
+ function EyeOffIcon() {
141
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: [
142
+ /* @__PURE__ */ jsx(
143
+ "path",
144
+ {
145
+ d: "M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24",
146
+ stroke: "currentColor",
147
+ strokeWidth: "2",
148
+ strokeLinecap: "round",
149
+ strokeLinejoin: "round"
150
+ }
151
+ ),
152
+ /* @__PURE__ */ jsx(
153
+ "path",
154
+ {
155
+ d: "M1 1l22 22",
156
+ stroke: "currentColor",
157
+ strokeWidth: "2",
158
+ strokeLinecap: "round",
159
+ strokeLinejoin: "round"
160
+ }
161
+ )
162
+ ] });
163
+ }
164
+ function WarningIcon() {
165
+ return /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx(
166
+ "path",
167
+ {
168
+ d: "M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z",
169
+ stroke: "currentColor",
170
+ strokeWidth: "2",
171
+ strokeLinecap: "round",
172
+ strokeLinejoin: "round"
173
+ }
174
+ ) });
175
+ }
176
+ function ChevronIcon() {
177
+ return /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx(
178
+ "path",
179
+ {
180
+ d: "M9 6l6 6-6 6",
181
+ stroke: "currentColor",
182
+ strokeWidth: "2",
183
+ strokeLinecap: "round",
184
+ strokeLinejoin: "round"
185
+ }
186
+ ) });
187
+ }
159
188
 
160
189
  // src/components/ui-lint/SettingsPopover.tsx
161
190
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -174,7 +203,7 @@ function SettingsPopover({ settings }) {
174
203
  style: {
175
204
  position: "absolute",
176
205
  bottom: "100%",
177
- right: 0,
206
+ left: 0,
178
207
  marginBottom: "8px",
179
208
  width: "260px",
180
209
  padding: "14px",
@@ -324,57 +353,130 @@ function SettingsPopover({ settings }) {
324
353
  );
325
354
  }
326
355
 
356
+ // src/components/ui-lint/ScanPanelStack.tsx
357
+ import { useRef, useEffect } from "react";
358
+
327
359
  // src/components/ui-lint/ScanResultsPopover.tsx
360
+ import { useState, useCallback, useMemo } from "react";
328
361
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
362
+ function getDisambiguatedName(path, allPaths) {
363
+ const parts = path.split("/");
364
+ const fileName = parts[parts.length - 1] || path;
365
+ const duplicates = allPaths.filter((p) => {
366
+ const pParts = p.split("/");
367
+ return pParts[pParts.length - 1] === fileName && p !== path;
368
+ });
369
+ if (duplicates.length === 0) {
370
+ return fileName;
371
+ }
372
+ if (parts.length >= 2) {
373
+ return `${parts[parts.length - 2]}/${fileName}`;
374
+ }
375
+ return fileName;
376
+ }
329
377
  function ScanResultsPopover() {
330
- const {
331
- autoScanState,
332
- pauseAutoScan,
333
- resumeAutoScan,
334
- stopAutoScan
335
- } = useUILintContext();
378
+ const { autoScanState, liveScanEnabled, disableLiveScan } = useUILintContext();
336
379
  const elementIssuesCache = useUILintStore(
337
380
  (s) => s.elementIssuesCache
338
381
  );
382
+ const setLocatorTarget = useUILintStore(
383
+ (s) => s.setLocatorTarget
384
+ );
385
+ const setInspectedElement = useUILintStore(
386
+ (s) => s.setInspectedElement
387
+ );
388
+ const [expandedFiles, setExpandedFiles] = useState(/* @__PURE__ */ new Set());
339
389
  const isScanning = autoScanState.status === "scanning";
340
- const isPaused = autoScanState.status === "paused";
341
390
  const isComplete = autoScanState.status === "complete";
342
- const sourceFiles = groupBySourceFile(autoScanState.elements);
343
- const fileResults = sourceFiles.map((sf) => {
344
- let issueCount = 0;
345
- for (const el of sf.elements) {
346
- const cached = elementIssuesCache.get(el.id);
347
- if (cached) {
348
- issueCount += cached.issues.length;
391
+ const filesWithIssues = useMemo(() => {
392
+ const sourceFiles = groupBySourceFile(autoScanState.elements);
393
+ const allPaths = sourceFiles.map((sf) => sf.path);
394
+ const result = [];
395
+ for (const sf of sourceFiles) {
396
+ const elementsWithIssues = [];
397
+ for (const el of sf.elements) {
398
+ const cached = elementIssuesCache.get(el.id);
399
+ const issueCount = cached?.issues.length || 0;
400
+ if (issueCount > 0) {
401
+ elementsWithIssues.push({ element: el, issueCount });
402
+ }
403
+ }
404
+ if (elementsWithIssues.length > 0) {
405
+ elementsWithIssues.sort(
406
+ (a, b) => a.element.source.lineNumber - b.element.source.lineNumber
407
+ );
408
+ const totalIssues2 = elementsWithIssues.reduce(
409
+ (sum, e) => sum + e.issueCount,
410
+ 0
411
+ );
412
+ result.push({
413
+ path: sf.path,
414
+ displayName: sf.displayName,
415
+ disambiguatedName: getDisambiguatedName(sf.path, allPaths),
416
+ issueCount: totalIssues2,
417
+ elementsWithIssues
418
+ });
349
419
  }
350
420
  }
351
- return {
352
- path: sf.path,
353
- displayName: sf.displayName,
354
- issueCount,
355
- elementCount: sf.elements.length
356
- };
357
- });
358
- fileResults.sort((a, b) => {
359
- if (b.issueCount !== a.issueCount) {
360
- return b.issueCount - a.issueCount;
361
- }
362
- return a.displayName.localeCompare(b.displayName);
363
- });
364
- const totalFiles = fileResults.length;
365
- const totalIssues = fileResults.reduce((sum, f) => sum + f.issueCount, 0);
366
- const filesWithIssues = fileResults.filter((f) => f.issueCount > 0).length;
421
+ result.sort((a, b) => b.issueCount - a.issueCount);
422
+ return result;
423
+ }, [autoScanState.elements, elementIssuesCache]);
424
+ const totalIssues = filesWithIssues.reduce((sum, f) => sum + f.issueCount, 0);
367
425
  const progress = autoScanState.totalElements > 0 ? autoScanState.currentIndex / autoScanState.totalElements * 100 : 0;
426
+ const toggleFile = useCallback((path) => {
427
+ setExpandedFiles((prev) => {
428
+ const next = new Set(prev);
429
+ if (next.has(path)) {
430
+ next.delete(path);
431
+ } else {
432
+ next.add(path);
433
+ }
434
+ return next;
435
+ });
436
+ }, []);
437
+ const handleElementHover = useCallback(
438
+ (element) => {
439
+ if (!element) {
440
+ setLocatorTarget(null);
441
+ return;
442
+ }
443
+ const target = {
444
+ element: element.element,
445
+ source: element.source,
446
+ rect: element.element.getBoundingClientRect()
447
+ };
448
+ setLocatorTarget(target);
449
+ },
450
+ [setLocatorTarget]
451
+ );
452
+ const handleElementClick = useCallback(
453
+ (element) => {
454
+ element.element.scrollIntoView({
455
+ behavior: "smooth",
456
+ inline: "nearest",
457
+ block: "nearest"
458
+ });
459
+ setInspectedElement({
460
+ element: element.element,
461
+ source: element.source,
462
+ rect: element.element.getBoundingClientRect(),
463
+ scannedElementId: element.id
464
+ });
465
+ setLocatorTarget(null);
466
+ },
467
+ [setInspectedElement, setLocatorTarget]
468
+ );
368
469
  return /* @__PURE__ */ jsxs3(
369
470
  "div",
370
471
  {
472
+ "data-ui-lint": true,
371
473
  style: {
372
474
  position: "absolute",
373
475
  bottom: "100%",
374
- right: 0,
476
+ left: 0,
375
477
  marginBottom: "8px",
376
- width: "280px",
377
- maxHeight: "400px",
478
+ width: "320px",
479
+ maxHeight: "450px",
378
480
  borderRadius: STYLES.popoverRadius,
379
481
  border: `1px solid ${STYLES.border}`,
380
482
  backgroundColor: STYLES.bgPopover,
@@ -404,7 +506,7 @@ function ScanResultsPopover() {
404
506
  justifyContent: "space-between"
405
507
  },
406
508
  children: [
407
- /* @__PURE__ */ jsxs3(
509
+ /* @__PURE__ */ jsx3(
408
510
  "div",
409
511
  {
410
512
  style: {
@@ -412,102 +514,34 @@ function ScanResultsPopover() {
412
514
  fontWeight: 600,
413
515
  color: STYLES.text
414
516
  },
415
- children: [
416
- totalFiles,
417
- " files \xB7 ",
418
- totalIssues,
419
- " issues"
420
- ]
517
+ children: filesWithIssues.length === 0 ? "No issues found" : `${filesWithIssues.length} ${filesWithIssues.length === 1 ? "file" : "files"} with ${totalIssues} ${totalIssues === 1 ? "issue" : "issues"}`
421
518
  }
422
519
  ),
423
- (isScanning || isPaused) && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "4px" }, children: [
424
- isScanning && /* @__PURE__ */ jsx3(
425
- "button",
426
- {
427
- onClick: pauseAutoScan,
428
- style: {
429
- display: "flex",
430
- alignItems: "center",
431
- justifyContent: "center",
432
- width: "24px",
433
- height: "24px",
434
- borderRadius: "6px",
435
- border: `1px solid ${STYLES.border}`,
436
- backgroundColor: "transparent",
437
- color: STYLES.textMuted,
438
- cursor: "pointer",
439
- transition: STYLES.transitionFast
440
- },
441
- title: "Pause scan",
442
- children: /* @__PURE__ */ jsx3(PauseIcon, {})
443
- }
444
- ),
445
- isPaused && /* @__PURE__ */ jsx3(
446
- "button",
447
- {
448
- onClick: resumeAutoScan,
449
- style: {
450
- display: "flex",
451
- alignItems: "center",
452
- justifyContent: "center",
453
- width: "24px",
454
- height: "24px",
455
- borderRadius: "6px",
456
- border: "none",
457
- backgroundColor: STYLES.accent,
458
- color: "#FFFFFF",
459
- cursor: "pointer",
460
- transition: STYLES.transitionFast
461
- },
462
- title: "Resume scan",
463
- children: /* @__PURE__ */ jsx3(PlayIcon, {})
464
- }
465
- ),
466
- /* @__PURE__ */ jsx3(
467
- "button",
468
- {
469
- onClick: stopAutoScan,
470
- style: {
471
- display: "flex",
472
- alignItems: "center",
473
- justifyContent: "center",
474
- width: "24px",
475
- height: "24px",
476
- borderRadius: "6px",
477
- border: `1px solid ${STYLES.border}`,
478
- backgroundColor: "transparent",
479
- color: STYLES.textMuted,
480
- cursor: "pointer",
481
- transition: STYLES.transitionFast
482
- },
483
- title: "Stop scan",
484
- children: /* @__PURE__ */ jsx3(StopIcon, {})
485
- }
486
- )
487
- ] }),
488
- isComplete && /* @__PURE__ */ jsx3(
520
+ liveScanEnabled && /* @__PURE__ */ jsx3(
489
521
  "button",
490
522
  {
491
- onClick: stopAutoScan,
523
+ onClick: disableLiveScan,
492
524
  style: {
493
- padding: "4px 8px",
525
+ display: "flex",
526
+ alignItems: "center",
527
+ justifyContent: "center",
528
+ width: "24px",
529
+ height: "24px",
494
530
  borderRadius: "6px",
495
531
  border: `1px solid ${STYLES.border}`,
496
532
  backgroundColor: "transparent",
497
533
  color: STYLES.textMuted,
498
- fontSize: "10px",
499
- fontWeight: 500,
500
534
  cursor: "pointer",
501
535
  transition: STYLES.transitionFast
502
536
  },
503
- title: "Clear results",
504
- children: "Clear"
537
+ title: "Disable live scanning",
538
+ children: /* @__PURE__ */ jsx3(StopIcon, {})
505
539
  }
506
540
  )
507
541
  ]
508
542
  }
509
543
  ),
510
- (isScanning || isPaused) && /* @__PURE__ */ jsxs3("div", { style: { marginTop: "10px" }, children: [
544
+ isScanning && /* @__PURE__ */ jsxs3("div", { style: { marginTop: "10px" }, children: [
511
545
  /* @__PURE__ */ jsxs3(
512
546
  "div",
513
547
  {
@@ -519,7 +553,7 @@ function ScanResultsPopover() {
519
553
  marginBottom: "4px"
520
554
  },
521
555
  children: [
522
- /* @__PURE__ */ jsx3("span", { children: isPaused ? "Paused" : "Scanning..." }),
556
+ /* @__PURE__ */ jsx3("span", { children: "Scanning..." }),
523
557
  /* @__PURE__ */ jsxs3("span", { children: [
524
558
  autoScanState.currentIndex,
525
559
  " / ",
@@ -543,7 +577,7 @@ function ScanResultsPopover() {
543
577
  style: {
544
578
  height: "100%",
545
579
  width: `${progress}%`,
546
- backgroundColor: isPaused ? STYLES.warning : STYLES.accent,
580
+ backgroundColor: STYLES.accent,
547
581
  transition: "width 0.2s ease-out"
548
582
  }
549
583
  }
@@ -560,92 +594,224 @@ function ScanResultsPopover() {
560
594
  style: {
561
595
  flex: 1,
562
596
  overflowY: "auto",
563
- padding: "6px 0"
597
+ padding: "4px 0"
564
598
  },
565
- children: fileResults.length === 0 ? /* @__PURE__ */ jsx3(
599
+ onMouseLeave: () => handleElementHover(null),
600
+ children: filesWithIssues.length === 0 ? /* @__PURE__ */ jsx3(
566
601
  "div",
567
602
  {
568
603
  style: {
569
- padding: "20px 14px",
604
+ padding: "24px 14px",
570
605
  textAlign: "center",
571
606
  fontSize: "11px",
572
607
  color: STYLES.textMuted
573
608
  },
574
- children: isScanning ? "Scanning page elements..." : "No files scanned"
609
+ children: isScanning ? "Scanning page elements..." : isComplete ? /* @__PURE__ */ jsx3("span", { style: { color: STYLES.success }, children: "\u2713 No issues found" }) : "No files scanned"
575
610
  }
576
- ) : fileResults.map((file) => /* @__PURE__ */ jsxs3(
577
- "div",
611
+ ) : filesWithIssues.map((file) => /* @__PURE__ */ jsx3(
612
+ FileRow,
578
613
  {
579
- style: {
580
- display: "flex",
581
- alignItems: "center",
582
- justifyContent: "space-between",
583
- padding: "8px 14px",
584
- cursor: "default",
585
- transition: STYLES.transitionFast
586
- },
587
- title: file.path,
588
- children: [
589
- /* @__PURE__ */ jsx3(
590
- "span",
591
- {
592
- style: {
593
- fontSize: "12px",
594
- fontFamily: STYLES.fontMono,
595
- color: STYLES.text,
596
- overflow: "hidden",
597
- textOverflow: "ellipsis",
598
- whiteSpace: "nowrap",
599
- flex: 1,
600
- marginRight: "12px"
601
- },
602
- children: file.displayName
603
- }
604
- ),
605
- /* @__PURE__ */ jsx3(
606
- "span",
607
- {
608
- style: {
609
- display: "inline-flex",
610
- alignItems: "center",
611
- justifyContent: "center",
612
- minWidth: "24px",
613
- height: "20px",
614
- padding: "0 6px",
615
- borderRadius: "10px",
616
- backgroundColor: file.issueCount > 0 ? getStatusColor(file.issueCount) : "rgba(75, 85, 99, 0.4)",
617
- color: file.issueCount > 0 ? "#FFFFFF" : STYLES.textMuted,
618
- fontSize: "10px",
619
- fontWeight: 700
620
- },
621
- children: file.issueCount
622
- }
623
- )
624
- ]
614
+ file,
615
+ isExpanded: expandedFiles.has(file.path),
616
+ onToggle: () => toggleFile(file.path),
617
+ onElementHover: handleElementHover,
618
+ onElementClick: handleElementClick
625
619
  },
626
620
  file.path
627
621
  ))
628
622
  }
629
- ),
630
- isComplete && totalFiles > 0 && /* @__PURE__ */ jsx3(
623
+ )
624
+ ]
625
+ }
626
+ );
627
+ }
628
+ function FileRow({
629
+ file,
630
+ isExpanded,
631
+ onToggle,
632
+ onElementHover,
633
+ onElementClick
634
+ }) {
635
+ return /* @__PURE__ */ jsxs3("div", { children: [
636
+ /* @__PURE__ */ jsxs3(
637
+ "div",
638
+ {
639
+ style: {
640
+ display: "flex",
641
+ alignItems: "center",
642
+ padding: "8px 12px",
643
+ cursor: "pointer",
644
+ transition: STYLES.transitionFast,
645
+ backgroundColor: isExpanded ? "rgba(59, 130, 246, 0.08)" : "transparent"
646
+ },
647
+ title: file.path,
648
+ onClick: onToggle,
649
+ onMouseEnter: (e) => {
650
+ if (!isExpanded) {
651
+ e.currentTarget.style.backgroundColor = "rgba(55, 65, 81, 0.5)";
652
+ }
653
+ },
654
+ onMouseLeave: (e) => {
655
+ e.currentTarget.style.backgroundColor = isExpanded ? "rgba(59, 130, 246, 0.08)" : "transparent";
656
+ },
657
+ children: [
658
+ /* @__PURE__ */ jsx3(
659
+ "span",
660
+ {
661
+ style: {
662
+ display: "flex",
663
+ alignItems: "center",
664
+ justifyContent: "center",
665
+ width: "16px",
666
+ height: "16px",
667
+ marginRight: "6px",
668
+ color: STYLES.textMuted,
669
+ transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)",
670
+ transition: "transform 0.15s ease-out"
671
+ },
672
+ children: /* @__PURE__ */ jsx3(ChevronIcon, {})
673
+ }
674
+ ),
675
+ /* @__PURE__ */ jsx3(
676
+ "span",
677
+ {
678
+ style: {
679
+ fontSize: "12px",
680
+ fontFamily: STYLES.fontMono,
681
+ color: STYLES.text,
682
+ overflow: "hidden",
683
+ textOverflow: "ellipsis",
684
+ whiteSpace: "nowrap",
685
+ flex: 1,
686
+ marginRight: "12px"
687
+ },
688
+ children: file.disambiguatedName
689
+ }
690
+ ),
691
+ /* @__PURE__ */ jsx3(
692
+ "span",
693
+ {
694
+ style: {
695
+ display: "inline-flex",
696
+ alignItems: "center",
697
+ justifyContent: "center",
698
+ minWidth: "22px",
699
+ height: "18px",
700
+ padding: "0 6px",
701
+ borderRadius: "9px",
702
+ backgroundColor: getStatusColor(file.issueCount),
703
+ color: "#FFFFFF",
704
+ fontSize: "10px",
705
+ fontWeight: 700
706
+ },
707
+ children: file.issueCount
708
+ }
709
+ )
710
+ ]
711
+ }
712
+ ),
713
+ isExpanded && /* @__PURE__ */ jsx3(
714
+ "div",
715
+ {
716
+ style: {
717
+ backgroundColor: "rgba(17, 24, 39, 0.4)",
718
+ borderTop: `1px solid ${STYLES.border}`,
719
+ borderBottom: `1px solid ${STYLES.border}`
720
+ },
721
+ children: file.elementsWithIssues.map((item) => /* @__PURE__ */ jsx3(
722
+ ElementRow,
723
+ {
724
+ item,
725
+ onHover: onElementHover,
726
+ onClick: onElementClick
727
+ },
728
+ item.element.id
729
+ ))
730
+ }
731
+ )
732
+ ] });
733
+ }
734
+ function ElementRow({ item, onHover, onClick }) {
735
+ const { element, issueCount } = item;
736
+ return /* @__PURE__ */ jsxs3(
737
+ "div",
738
+ {
739
+ style: {
740
+ display: "flex",
741
+ alignItems: "center",
742
+ padding: "6px 12px 6px 34px",
743
+ cursor: "pointer",
744
+ transition: STYLES.transitionFast,
745
+ backgroundColor: "transparent"
746
+ },
747
+ onMouseEnter: (e) => {
748
+ e.currentTarget.style.backgroundColor = "rgba(59, 130, 246, 0.15)";
749
+ onHover(element);
750
+ },
751
+ onMouseLeave: (e) => {
752
+ e.currentTarget.style.backgroundColor = "transparent";
753
+ },
754
+ onClick: () => onClick(element),
755
+ children: [
756
+ /* @__PURE__ */ jsxs3(
631
757
  "div",
632
758
  {
633
759
  style: {
634
- padding: "10px 14px",
635
- borderTop: `1px solid ${STYLES.border}`,
636
- fontSize: "10px",
637
- color: STYLES.textMuted
760
+ display: "flex",
761
+ alignItems: "center",
762
+ gap: "4px",
763
+ flex: 1,
764
+ marginRight: "12px"
765
+ },
766
+ children: [
767
+ /* @__PURE__ */ jsxs3(
768
+ "span",
769
+ {
770
+ style: {
771
+ fontSize: "11px",
772
+ fontFamily: STYLES.fontMono,
773
+ color: STYLES.accent
774
+ },
775
+ children: [
776
+ "<",
777
+ element.tagName,
778
+ ">"
779
+ ]
780
+ }
781
+ ),
782
+ /* @__PURE__ */ jsxs3(
783
+ "span",
784
+ {
785
+ style: {
786
+ fontSize: "10px",
787
+ color: STYLES.textDim
788
+ },
789
+ children: [
790
+ ":",
791
+ element.source.lineNumber
792
+ ]
793
+ }
794
+ )
795
+ ]
796
+ }
797
+ ),
798
+ /* @__PURE__ */ jsx3(
799
+ "span",
800
+ {
801
+ style: {
802
+ display: "inline-flex",
803
+ alignItems: "center",
804
+ justifyContent: "center",
805
+ minWidth: "18px",
806
+ height: "16px",
807
+ padding: "0 5px",
808
+ borderRadius: "8px",
809
+ backgroundColor: getStatusColor(issueCount),
810
+ color: "#FFFFFF",
811
+ fontSize: "9px",
812
+ fontWeight: 700
638
813
  },
639
- children: totalIssues === 0 ? /* @__PURE__ */ jsxs3("span", { style: { color: STYLES.success }, children: [
640
- "\u2713 No issues found across ",
641
- totalFiles,
642
- " files"
643
- ] }) : /* @__PURE__ */ jsxs3("span", { children: [
644
- filesWithIssues,
645
- " of ",
646
- totalFiles,
647
- " files have issues"
648
- ] })
814
+ children: issueCount
649
815
  }
650
816
  )
651
817
  ]
@@ -653,23 +819,66 @@ function ScanResultsPopover() {
653
819
  );
654
820
  }
655
821
 
822
+ // src/components/ui-lint/ScanPanelStack.tsx
823
+ import { jsx as jsx4 } from "react/jsx-runtime";
824
+ function ScanPanelStack({ show, onClose }) {
825
+ const containerRef = useRef(null);
826
+ useEffect(() => {
827
+ if (!show) return;
828
+ const handleClickOutside = (e) => {
829
+ const target = e.target;
830
+ if (containerRef.current && !containerRef.current.contains(target)) {
831
+ const isUILintElement = target.closest?.("[data-ui-lint]");
832
+ if (!isUILintElement) {
833
+ onClose();
834
+ }
835
+ }
836
+ };
837
+ const timeoutId = setTimeout(() => {
838
+ document.addEventListener("mousedown", handleClickOutside);
839
+ }, 100);
840
+ return () => {
841
+ clearTimeout(timeoutId);
842
+ document.removeEventListener("mousedown", handleClickOutside);
843
+ };
844
+ }, [show, onClose]);
845
+ useEffect(() => {
846
+ if (!show) return;
847
+ const handleKeyDown = (e) => {
848
+ if (e.key === "Escape") {
849
+ onClose();
850
+ }
851
+ };
852
+ window.addEventListener("keydown", handleKeyDown);
853
+ return () => window.removeEventListener("keydown", handleKeyDown);
854
+ }, [show, onClose]);
855
+ if (!show) return null;
856
+ return /* @__PURE__ */ jsx4("div", { ref: containerRef, style: { position: "relative" }, children: /* @__PURE__ */ jsx4(ScanResultsPopover, {}) });
857
+ }
858
+
656
859
  // src/components/ui-lint/UILintToolbar.tsx
657
- import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
860
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
658
861
  function UILintToolbar() {
659
- const { settings, inspectedElement, autoScanState, startAutoScan } = useUILintContext();
862
+ const {
863
+ settings,
864
+ inspectedElement,
865
+ liveScanEnabled,
866
+ autoScanState,
867
+ enableLiveScan,
868
+ disableLiveScan
869
+ } = useUILintContext();
660
870
  const elementIssuesCache = useUILintStore(
661
871
  (s) => s.elementIssuesCache
662
872
  );
663
- const [showSettings, setShowSettings] = useState(false);
664
- const [showResults, setShowResults] = useState(false);
665
- const [mounted, setMounted] = useState(false);
666
- const toolbarRef = useRef(null);
667
- const settingsRef = useRef(null);
668
- const resultsRef = useRef(null);
669
- useEffect(() => {
873
+ const [showSettings, setShowSettings] = useState2(false);
874
+ const [showResults, setShowResults] = useState2(false);
875
+ const [mounted, setMounted] = useState2(false);
876
+ const toolbarRef = useRef2(null);
877
+ const settingsRef = useRef2(null);
878
+ useEffect2(() => {
670
879
  setMounted(true);
671
880
  }, []);
672
- useEffect(() => {
881
+ useEffect2(() => {
673
882
  const handleClickOutside = (e) => {
674
883
  const target = e.target;
675
884
  if (showSettings && settingsRef.current) {
@@ -677,70 +886,63 @@ function UILintToolbar() {
677
886
  setShowSettings(false);
678
887
  }
679
888
  }
680
- if (showResults && resultsRef.current) {
681
- if (!resultsRef.current.contains(target) && !toolbarRef.current?.contains(target)) {
682
- setShowResults(false);
683
- }
684
- }
685
889
  };
686
- if (showSettings || showResults) {
890
+ if (showSettings) {
687
891
  document.addEventListener("mousedown", handleClickOutside);
688
892
  return () => document.removeEventListener("mousedown", handleClickOutside);
689
893
  }
690
- }, [showSettings, showResults]);
691
- const handleScanClick = useCallback(() => {
692
- if (autoScanState.status === "idle") {
693
- startAutoScan();
694
- setShowResults(true);
894
+ }, [showSettings]);
895
+ const handleToggleClick = useCallback2(() => {
896
+ if (liveScanEnabled) {
897
+ disableLiveScan();
898
+ setShowResults(false);
695
899
  } else {
696
- setShowResults(!showResults);
900
+ enableLiveScan();
697
901
  }
698
902
  setShowSettings(false);
699
- }, [autoScanState.status, startAutoScan, showResults]);
700
- const handleSettingsClick = useCallback(() => {
903
+ }, [liveScanEnabled, enableLiveScan, disableLiveScan]);
904
+ const handleIssuesClick = useCallback2(() => {
905
+ if (!liveScanEnabled) return;
906
+ setShowResults(!showResults);
907
+ setShowSettings(false);
908
+ }, [liveScanEnabled, showResults]);
909
+ const handleSettingsClick = useCallback2(() => {
701
910
  setShowSettings(!showSettings);
702
911
  setShowResults(false);
703
912
  }, [showSettings]);
704
913
  if (!mounted) return null;
705
- if (inspectedElement) return null;
706
914
  const isScanning = autoScanState.status === "scanning";
707
- const isPaused = autoScanState.status === "paused";
708
915
  const isComplete = autoScanState.status === "complete";
709
- const hasResults = autoScanState.status !== "idle";
710
916
  let totalIssues = 0;
711
917
  elementIssuesCache.forEach((el) => {
712
918
  totalIssues += el.issues.length;
713
919
  });
714
- const sourceFiles = hasResults ? groupBySourceFile(autoScanState.elements) : [];
715
- const filesWithIssues = sourceFiles.filter((sf) => {
716
- const issues = sf.elements.reduce((sum, el) => {
717
- const cached = elementIssuesCache.get(el.id);
718
- return sum + (cached?.issues.length || 0);
719
- }, 0);
720
- return issues > 0;
721
- }).length;
722
- const getScanButtonContent = () => {
920
+ const hasIssues = totalIssues > 0;
921
+ const getToggleContent = () => {
723
922
  if (isScanning) {
724
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
725
- /* @__PURE__ */ jsx4(SpinnerIcon, {}),
726
- /* @__PURE__ */ jsx4("span", { children: "Scanning..." })
727
- ] });
923
+ return /* @__PURE__ */ jsx5(SpinnerIcon, {});
728
924
  }
729
- if (isPaused) {
730
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
731
- /* @__PURE__ */ jsx4(MagnifyingGlassIcon, {}),
732
- /* @__PURE__ */ jsx4("span", { children: "Paused" })
733
- ] });
925
+ if (liveScanEnabled) {
926
+ return /* @__PURE__ */ jsx5(EyeIcon, {});
927
+ }
928
+ return /* @__PURE__ */ jsx5(EyeOffIcon, {});
929
+ };
930
+ const getIssuesContent = () => {
931
+ if (!liveScanEnabled) {
932
+ return /* @__PURE__ */ jsx5("span", { style: { opacity: 0.5 }, children: "--" });
933
+ }
934
+ if (isScanning) {
935
+ return /* @__PURE__ */ jsx5("span", { style: { opacity: 0.7 }, children: "..." });
734
936
  }
735
- if (isComplete) {
736
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
737
- /* @__PURE__ */ jsx4(CheckCircleIcon, {}),
738
- /* @__PURE__ */ jsx4("span", { children: totalIssues === 0 ? "All clear" : `${totalIssues} issues` })
937
+ if (hasIssues) {
938
+ return /* @__PURE__ */ jsxs4("span", { style: { display: "flex", alignItems: "center", gap: "4px" }, children: [
939
+ /* @__PURE__ */ jsx5(WarningIcon, {}),
940
+ /* @__PURE__ */ jsx5("span", { children: totalIssues })
739
941
  ] });
740
942
  }
741
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
742
- /* @__PURE__ */ jsx4(MagnifyingGlassIcon, {}),
743
- /* @__PURE__ */ jsx4("span", { children: "Scan" })
943
+ return /* @__PURE__ */ jsxs4("span", { style: { display: "flex", alignItems: "center", gap: "4px" }, children: [
944
+ /* @__PURE__ */ jsx5(CheckCircleIcon, {}),
945
+ /* @__PURE__ */ jsx5("span", { children: "0" })
744
946
  ] });
745
947
  };
746
948
  const content = /* @__PURE__ */ jsxs4(
@@ -749,13 +951,13 @@ function UILintToolbar() {
749
951
  "data-ui-lint": true,
750
952
  style: {
751
953
  position: "fixed",
752
- bottom: "24px",
753
- right: "24px",
954
+ bottom: "70px",
955
+ left: "20px",
754
956
  zIndex: 99999,
755
957
  fontFamily: STYLES.font
756
958
  },
757
959
  children: [
758
- /* @__PURE__ */ jsx4("style", { children: `
960
+ /* @__PURE__ */ jsx5("style", { children: `
759
961
  @keyframes uilint-fade-in {
760
962
  from { opacity: 0; transform: translateY(8px); }
761
963
  to { opacity: 1; transform: translateY(0); }
@@ -776,7 +978,7 @@ function UILintToolbar() {
776
978
  letterSpacing: "0.01em"
777
979
  },
778
980
  children: [
779
- /* @__PURE__ */ jsx4("span", { style: { color: STYLES.textMuted }, children: "\u2325+Click" }),
981
+ /* @__PURE__ */ jsx5("span", { style: { color: STYLES.textMuted }, children: "\u2325+Click" }),
780
982
  " to inspect"
781
983
  ]
782
984
  }
@@ -799,61 +1001,85 @@ function UILintToolbar() {
799
1001
  overflow: "hidden"
800
1002
  },
801
1003
  children: [
802
- /* @__PURE__ */ jsxs4(
1004
+ /* @__PURE__ */ jsx5(
1005
+ "button",
1006
+ {
1007
+ onClick: handleToggleClick,
1008
+ style: {
1009
+ display: "flex",
1010
+ alignItems: "center",
1011
+ justifyContent: "center",
1012
+ height: "100%",
1013
+ width: "40px",
1014
+ border: "none",
1015
+ backgroundColor: liveScanEnabled ? STYLES.bgSegmentHover : "transparent",
1016
+ color: liveScanEnabled ? STYLES.accent : STYLES.textMuted,
1017
+ cursor: "pointer",
1018
+ transition: STYLES.transition
1019
+ },
1020
+ onMouseEnter: (e) => {
1021
+ if (!liveScanEnabled) {
1022
+ e.currentTarget.style.backgroundColor = STYLES.bgSegmentHover;
1023
+ e.currentTarget.style.color = STYLES.text;
1024
+ }
1025
+ },
1026
+ onMouseLeave: (e) => {
1027
+ if (!liveScanEnabled) {
1028
+ e.currentTarget.style.backgroundColor = "transparent";
1029
+ e.currentTarget.style.color = STYLES.textMuted;
1030
+ }
1031
+ },
1032
+ title: liveScanEnabled ? "Disable live scanning" : "Enable live scanning",
1033
+ children: getToggleContent()
1034
+ }
1035
+ ),
1036
+ /* @__PURE__ */ jsx5(
1037
+ "div",
1038
+ {
1039
+ style: {
1040
+ width: "1px",
1041
+ height: "20px",
1042
+ backgroundColor: STYLES.divider
1043
+ }
1044
+ }
1045
+ ),
1046
+ /* @__PURE__ */ jsx5(
803
1047
  "button",
804
1048
  {
805
- onClick: handleScanClick,
1049
+ onClick: handleIssuesClick,
1050
+ disabled: !liveScanEnabled,
806
1051
  style: {
807
1052
  display: "flex",
808
1053
  alignItems: "center",
809
- gap: "6px",
1054
+ justifyContent: "center",
1055
+ gap: "4px",
810
1056
  height: "100%",
811
- padding: "0 16px",
1057
+ padding: "0 12px",
812
1058
  border: "none",
813
- backgroundColor: showResults || hasResults ? STYLES.bgSegmentHover : "transparent",
814
- color: hasResults ? totalIssues > 0 ? STYLES.warning : STYLES.success : STYLES.text,
1059
+ backgroundColor: showResults && liveScanEnabled ? STYLES.bgSegmentHover : "transparent",
1060
+ color: !liveScanEnabled ? STYLES.textDim : hasIssues ? STYLES.warning : isComplete ? STYLES.success : STYLES.text,
815
1061
  fontSize: "13px",
816
1062
  fontWeight: 500,
817
1063
  fontFamily: STYLES.font,
818
- cursor: "pointer",
819
- transition: STYLES.transition
1064
+ cursor: liveScanEnabled ? "pointer" : "default",
1065
+ transition: STYLES.transition,
1066
+ opacity: liveScanEnabled ? 1 : 0.6
820
1067
  },
821
1068
  onMouseEnter: (e) => {
822
- if (!showResults && !hasResults) {
1069
+ if (liveScanEnabled && !showResults) {
823
1070
  e.currentTarget.style.backgroundColor = STYLES.bgSegmentHover;
824
1071
  }
825
1072
  },
826
1073
  onMouseLeave: (e) => {
827
- if (!showResults && !hasResults) {
1074
+ if (liveScanEnabled && !showResults) {
828
1075
  e.currentTarget.style.backgroundColor = "transparent";
829
1076
  }
830
1077
  },
831
- title: hasResults ? `${sourceFiles.length} files scanned, ${totalIssues} issues found` : "Scan page for style issues",
832
- children: [
833
- getScanButtonContent(),
834
- isComplete && filesWithIssues > 0 && /* @__PURE__ */ jsx4(
835
- "span",
836
- {
837
- style: {
838
- display: "inline-flex",
839
- alignItems: "center",
840
- justifyContent: "center",
841
- minWidth: "18px",
842
- height: "18px",
843
- padding: "0 5px",
844
- borderRadius: "9px",
845
- backgroundColor: STYLES.error,
846
- color: STYLES.badgeText,
847
- fontSize: "10px",
848
- fontWeight: 700
849
- },
850
- children: filesWithIssues
851
- }
852
- )
853
- ]
1078
+ title: liveScanEnabled ? `${totalIssues} issues found` : "Enable scanning to see issues",
1079
+ children: getIssuesContent()
854
1080
  }
855
1081
  ),
856
- /* @__PURE__ */ jsx4(
1082
+ /* @__PURE__ */ jsx5(
857
1083
  "div",
858
1084
  {
859
1085
  style: {
@@ -863,7 +1089,7 @@ function UILintToolbar() {
863
1089
  }
864
1090
  }
865
1091
  ),
866
- /* @__PURE__ */ jsx4(
1092
+ /* @__PURE__ */ jsx5(
867
1093
  "button",
868
1094
  {
869
1095
  onClick: handleSettingsClick,
@@ -892,14 +1118,20 @@ function UILintToolbar() {
892
1118
  }
893
1119
  },
894
1120
  title: "Settings",
895
- children: /* @__PURE__ */ jsx4(EllipsisIcon, {})
1121
+ children: /* @__PURE__ */ jsx5(EllipsisIcon, {})
896
1122
  }
897
1123
  )
898
1124
  ]
899
1125
  }
900
1126
  ),
901
- showSettings && /* @__PURE__ */ jsx4("div", { ref: settingsRef, children: /* @__PURE__ */ jsx4(SettingsPopover, { settings }) }),
902
- showResults && hasResults && /* @__PURE__ */ jsx4("div", { ref: resultsRef, children: /* @__PURE__ */ jsx4(ScanResultsPopover, {}) })
1127
+ showSettings && /* @__PURE__ */ jsx5("div", { ref: settingsRef, children: /* @__PURE__ */ jsx5(SettingsPopover, { settings }) }),
1128
+ /* @__PURE__ */ jsx5(
1129
+ ScanPanelStack,
1130
+ {
1131
+ show: showResults && liveScanEnabled,
1132
+ onClose: () => setShowResults(false)
1133
+ }
1134
+ )
903
1135
  ]
904
1136
  }
905
1137
  );