uilint-react 0.1.45 → 0.1.46

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,7 +3,7 @@
3
3
  import {
4
4
  useUILintContext,
5
5
  useUILintStore
6
- } from "./chunk-BJD2V2LF.js";
6
+ } from "./chunk-OU5EEQT6.js";
7
7
 
8
8
  // src/components/ui-lint/ElementBadges.tsx
9
9
  import React, { useState, useEffect, useCallback, useMemo } from "react";
@@ -129,6 +129,24 @@ function findNearbyBadges(positions, x, y, threshold) {
129
129
  });
130
130
  }
131
131
 
132
+ // src/components/ui-lint/visibility-utils.ts
133
+ function isElementCoveredByOverlay(element, badgeX, badgeY) {
134
+ const elementsAtPoint = document.elementsFromPoint(badgeX, badgeY);
135
+ for (const el of elementsAtPoint) {
136
+ if (el.hasAttribute("data-ui-lint")) continue;
137
+ if (el === element || element.contains(el)) continue;
138
+ if (el.contains(element)) continue;
139
+ const style = window.getComputedStyle(el);
140
+ const position = style.position;
141
+ const zIndex = parseInt(style.zIndex, 10);
142
+ const isOverlay = (position === "fixed" || position === "absolute") && (zIndex > 0 || style.zIndex === "auto" || style.zIndex === "inherit");
143
+ if (isOverlay) {
144
+ return true;
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+
132
150
  // src/components/ui-lint/ElementBadges.tsx
133
151
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
134
152
  var STYLES = {
@@ -256,6 +274,9 @@ function ElementBadges() {
256
274
  const y = rect.top - 8;
257
275
  if (rect.top < -50 || rect.top > window.innerHeight + 50) continue;
258
276
  if (rect.left < -50 || rect.left > window.innerWidth + 50) continue;
277
+ if (isElementCoveredByOverlay(element.element, x, y)) {
278
+ continue;
279
+ }
259
280
  positions.push({ element, issue, x, y, rect });
260
281
  }
261
282
  setBadgePositions(positions);
@@ -316,37 +337,53 @@ function ElementBadges() {
316
337
  }
317
338
  return filtered;
318
339
  }, [nudgedPositions, isAltKeyPressed, selectedFilePath, hoveredFilePath]);
340
+ const handleUILintInteraction = useCallback(
341
+ (e) => {
342
+ e.stopPropagation();
343
+ },
344
+ []
345
+ );
319
346
  if (!mounted) return null;
320
347
  if (autoScanState.status === "idle") return null;
321
- const content = /* @__PURE__ */ jsxs("div", { "data-ui-lint": true, children: [
322
- /* @__PURE__ */ jsx(BadgeAnimationStyles, {}),
323
- visibleBadges.filter((pos, idx, arr) => {
324
- const id = pos.element.id;
325
- return arr.findIndex((p) => p.element.id === id) === idx;
326
- }).map((nudgedPos) => {
327
- const distance = Math.hypot(
328
- nudgedPos.nudgedX - cursorPos.x,
329
- nudgedPos.nudgedY - cursorPos.y
330
- );
331
- const nearbyBadges = findNearbyBadges(
332
- visibleBadges,
333
- nudgedPos.nudgedX,
334
- nudgedPos.nudgedY,
335
- NEARBY_THRESHOLD
336
- );
337
- return /* @__PURE__ */ jsx(
338
- NudgedBadge,
339
- {
340
- position: nudgedPos,
341
- distance,
342
- nearbyBadges,
343
- cursorPos,
344
- onSelect: handleSelect
345
- },
346
- nudgedPos.element.id
347
- );
348
- })
349
- ] });
348
+ const content = /* @__PURE__ */ jsxs(
349
+ "div",
350
+ {
351
+ "data-ui-lint": true,
352
+ onMouseDown: handleUILintInteraction,
353
+ onClick: handleUILintInteraction,
354
+ onKeyDown: handleUILintInteraction,
355
+ style: { pointerEvents: "none" },
356
+ children: [
357
+ /* @__PURE__ */ jsx(BadgeAnimationStyles, {}),
358
+ visibleBadges.filter((pos, idx, arr) => {
359
+ const id = pos.element.id;
360
+ return arr.findIndex((p) => p.element.id === id) === idx;
361
+ }).map((nudgedPos) => {
362
+ const distance = Math.hypot(
363
+ nudgedPos.nudgedX - cursorPos.x,
364
+ nudgedPos.nudgedY - cursorPos.y
365
+ );
366
+ const nearbyBadges = findNearbyBadges(
367
+ visibleBadges,
368
+ nudgedPos.nudgedX,
369
+ nudgedPos.nudgedY,
370
+ NEARBY_THRESHOLD
371
+ );
372
+ return /* @__PURE__ */ jsx(
373
+ NudgedBadge,
374
+ {
375
+ position: nudgedPos,
376
+ distance,
377
+ nearbyBadges,
378
+ cursorPos,
379
+ onSelect: handleSelect
380
+ },
381
+ nudgedPos.element.id
382
+ );
383
+ })
384
+ ]
385
+ }
386
+ );
350
387
  return createPortal(content, document.body);
351
388
  }
352
389
  function NudgedBadge({
@@ -415,7 +452,9 @@ function NudgedBadge({
415
452
  boxShadow: "0 4px 20px rgba(0, 0, 0, 0.4)",
416
453
  padding: "4px 0",
417
454
  minWidth: "200px",
418
- fontFamily: STYLES.font
455
+ fontFamily: STYLES.font,
456
+ pointerEvents: "auto"
457
+ // Re-enable pointer events for interactive dropdown
419
458
  };
420
459
  }, [snappedPosition]);
421
460
  const scale = isExpanded ? 1.1 : getScaleFromDistance(distance);
@@ -468,7 +507,9 @@ function NudgedBadge({
468
507
  cursor: "pointer",
469
508
  transition: "transform 0.1s ease-out",
470
509
  transform: `scale(${scale})`,
471
- transformOrigin: "center center"
510
+ transformOrigin: "center center",
511
+ pointerEvents: "auto"
512
+ // Re-enable pointer events for interactive badge
472
513
  },
473
514
  "data-ui-lint": true,
474
515
  onMouseEnter: handleMouseEnter,
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ "use client";
3
+ import {
4
+ InspectionPanel
5
+ } from "./chunk-JURUYCUC.js";
6
+ import "./chunk-S4IWHBOQ.js";
7
+ import "./chunk-OU5EEQT6.js";
8
+ export {
9
+ InspectionPanel
10
+ };
@@ -3,8 +3,8 @@
3
3
  import {
4
4
  InspectedElementHighlight,
5
5
  LocatorOverlay
6
- } from "./chunk-YLTHKMTO.js";
7
- import "./chunk-BJD2V2LF.js";
6
+ } from "./chunk-LQ3WQYIF.js";
7
+ import "./chunk-OU5EEQT6.js";
8
8
  export {
9
9
  InspectedElementHighlight,
10
10
  LocatorOverlay
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ "use client";
3
+ import {
4
+ UILintToolbar
5
+ } from "./chunk-XUMLILUN.js";
6
+ import "./chunk-S4IWHBOQ.js";
7
+ import "./chunk-OU5EEQT6.js";
8
+ export {
9
+ UILintToolbar
10
+ };
@@ -1,12 +1,13 @@
1
1
  "use client";
2
2
  import {
3
- Badge
4
- } from "./chunk-W42PI2OF.js";
3
+ Badge,
4
+ fetchSourceWithWindow
5
+ } from "./chunk-S4IWHBOQ.js";
5
6
  import {
6
7
  buildEditorUrl,
7
8
  useUILintContext,
8
9
  useUILintStore
9
- } from "./chunk-BJD2V2LF.js";
10
+ } from "./chunk-OU5EEQT6.js";
10
11
 
11
12
  // src/components/ui-lint/InspectionPanel.tsx
12
13
  import React, {
@@ -18,97 +19,6 @@ import React, {
18
19
  } from "react";
19
20
  import { createPortal } from "react-dom";
20
21
 
21
- // src/components/ui-lint/source-fetcher.ts
22
- var sourceCache = /* @__PURE__ */ new Map();
23
- var CACHE_TTL = 5 * 60 * 1e3;
24
- var API_ENDPOINT = "/api/.uilint/source";
25
- async function fetchSource(filePath) {
26
- const cached = sourceCache.get(filePath);
27
- if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
28
- return {
29
- content: cached.content,
30
- relativePath: cached.relativePath
31
- };
32
- }
33
- try {
34
- const response = await fetch(
35
- `${API_ENDPOINT}?path=${encodeURIComponent(filePath)}`
36
- );
37
- if (!response.ok) {
38
- console.warn(`[UILint] Failed to fetch source: ${response.statusText}`);
39
- return null;
40
- }
41
- const data = await response.json();
42
- sourceCache.set(filePath, {
43
- ...data,
44
- fetchedAt: Date.now()
45
- });
46
- return data;
47
- } catch (error) {
48
- console.error("[UILint] Error fetching source:", error);
49
- return null;
50
- }
51
- }
52
- async function fetchSourceWithContext(source, contextLines = 5) {
53
- const result = await fetchSource(source.fileName);
54
- if (!result) return null;
55
- const allLines = result.content.split("\n");
56
- const targetLine = source.lineNumber - 1;
57
- const startLine = Math.max(0, targetLine - contextLines);
58
- const endLine = Math.min(allLines.length, targetLine + contextLines + 1);
59
- return {
60
- lines: allLines.slice(startLine, endLine),
61
- startLine: startLine + 1,
62
- // Back to 1-indexed
63
- highlightLine: source.lineNumber,
64
- relativePath: result.relativePath
65
- };
66
- }
67
- async function fetchSourceWithWindow(source, window2) {
68
- const result = await fetchSource(source.fileName);
69
- if (!result) return null;
70
- const allLines = result.content.split("\n");
71
- const targetLine = source.lineNumber - 1;
72
- const startLine = Math.max(0, targetLine - Math.max(0, window2.linesAbove));
73
- const endLine = Math.min(
74
- allLines.length,
75
- targetLine + Math.max(0, window2.linesBelow) + 1
76
- );
77
- return {
78
- lines: allLines.slice(startLine, endLine),
79
- startLine: startLine + 1,
80
- // Back to 1-indexed
81
- highlightLine: source.lineNumber,
82
- relativePath: result.relativePath
83
- };
84
- }
85
- function clearSourceCache() {
86
- sourceCache.clear();
87
- }
88
- function getCachedSource(filePath) {
89
- const cached = sourceCache.get(filePath);
90
- if (!cached) return null;
91
- if (Date.now() - cached.fetchedAt >= CACHE_TTL) {
92
- sourceCache.delete(filePath);
93
- return null;
94
- }
95
- return {
96
- content: cached.content,
97
- relativePath: cached.relativePath
98
- };
99
- }
100
- async function prefetchSources(filePaths) {
101
- const uniquePaths = [...new Set(filePaths)].filter((path) => {
102
- const cached = sourceCache.get(path);
103
- return !cached || Date.now() - cached.fetchedAt >= CACHE_TTL;
104
- });
105
- const BATCH_SIZE = 5;
106
- for (let i = 0; i < uniquePaths.length; i += BATCH_SIZE) {
107
- const batch = uniquePaths.slice(i, i + BATCH_SIZE);
108
- await Promise.all(batch.map(fetchSource));
109
- }
110
- }
111
-
112
22
  // src/components/ui-lint/code-formatting.ts
113
23
  function leadingWhitespace(s) {
114
24
  const match = s.match(/^[\t ]+/);
@@ -278,11 +188,11 @@ function InspectionPanel() {
278
188
  setInspectedElement(null);
279
189
  };
280
190
  const timer = setTimeout(() => {
281
- document.addEventListener("click", handleClickOutside);
191
+ document.addEventListener("click", handleClickOutside, true);
282
192
  }, 50);
283
193
  return () => {
284
194
  clearTimeout(timer);
285
- document.removeEventListener("click", handleClickOutside);
195
+ document.removeEventListener("click", handleClickOutside, true);
286
196
  };
287
197
  }, [inspectedElement, setInspectedElement]);
288
198
  const isFileLevelIssue = useMemo(() => {
@@ -439,13 +349,21 @@ function InspectionPanel() {
439
349
  );
440
350
  window.open(url, "_blank");
441
351
  }, [inspectedElement, editorBaseDir]);
352
+ const handleUILintInteraction = useCallback(
353
+ (e) => {
354
+ e.stopPropagation();
355
+ },
356
+ []
357
+ );
442
358
  if (!mounted || !inspectedElement) return null;
443
359
  const content = /* @__PURE__ */ jsxs(
444
360
  "div",
445
361
  {
446
362
  ref: popoverRef,
447
363
  "data-ui-lint": true,
448
- onClick: (e) => e.stopPropagation(),
364
+ onMouseDown: handleUILintInteraction,
365
+ onClick: handleUILintInteraction,
366
+ onKeyDown: handleUILintInteraction,
449
367
  style: {
450
368
  position: "fixed",
451
369
  top: position.top,
@@ -462,7 +380,9 @@ function InspectionPanel() {
462
380
  color: STYLES.text,
463
381
  overflow: "hidden",
464
382
  zIndex: 99998,
465
- animation: "uilint-popover-appear 0.15s ease-out"
383
+ animation: "uilint-popover-appear 0.15s ease-out",
384
+ pointerEvents: "auto"
385
+ // Ensure panel is interactive
466
386
  },
467
387
  children: [
468
388
  /* @__PURE__ */ jsx("style", { children: `
@@ -1008,10 +928,5 @@ function CollapseIcon() {
1008
928
  }
1009
929
 
1010
930
  export {
1011
- fetchSource,
1012
- fetchSourceWithContext,
1013
- clearSourceCache,
1014
- getCachedSource,
1015
- prefetchSources,
1016
931
  InspectionPanel
1017
932
  };
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  useUILintContext
4
- } from "./chunk-BJD2V2LF.js";
4
+ } from "./chunk-OU5EEQT6.js";
5
5
 
6
6
  // src/components/ui-lint/LocatorOverlay.tsx
7
7
  import { useState, useEffect, useMemo } from "react";
@@ -1063,10 +1063,10 @@ function UILintUI() {
1063
1063
  const [components, setComponents] = useState(null);
1064
1064
  useEffect2(() => {
1065
1065
  Promise.all([
1066
- import("./UILintToolbar-5NK77IQJ.js"),
1067
- import("./InspectionPanel-QC6WR5IG.js"),
1068
- import("./LocatorOverlay-Z24VU27L.js"),
1069
- import("./ElementBadges-RV7I6QXS.js")
1066
+ import("./UILintToolbar-XJN6LFWZ.js"),
1067
+ import("./InspectionPanel-EOW6OJFT.js"),
1068
+ import("./LocatorOverlay-BF6EED4N.js"),
1069
+ import("./ElementBadges-E35MQ2SV.js")
1070
1070
  ]).then(([toolbar, panel, locator, badges]) => {
1071
1071
  setComponents({
1072
1072
  Toolbar: toolbar.UILintToolbar,
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ // src/components/ui-lint/Badge.tsx
4
+ import { jsx } from "react/jsx-runtime";
5
+ var BADGE_COLORS = {
6
+ success: "#68d391",
7
+ // Soft green
8
+ warning: "#f6ad55",
9
+ // Warm orange
10
+ error: "#ef4444"
11
+ // Red (for future use)
12
+ };
13
+ var FONT_MONO = `"SF Mono", Monaco, "Cascadia Code", monospace`;
14
+ function getBadgeTextColor(issueCount) {
15
+ if (issueCount === 0) return BADGE_COLORS.success;
16
+ return BADGE_COLORS.warning;
17
+ }
18
+ function getBadgeBackgroundColor(issueCount) {
19
+ const color = getBadgeTextColor(issueCount);
20
+ return `${color}20`;
21
+ }
22
+ var BADGE_STYLES = {
23
+ default: {
24
+ minWidth: "20px",
25
+ height: "20px",
26
+ padding: "0 6px",
27
+ borderRadius: "10px",
28
+ fontSize: "11px",
29
+ fontWeight: 600,
30
+ letterSpacing: "-0.02em"
31
+ },
32
+ small: {
33
+ minWidth: "18px",
34
+ height: "18px",
35
+ padding: "0 5px",
36
+ borderRadius: "9px",
37
+ fontSize: "10px",
38
+ fontWeight: 700,
39
+ letterSpacing: "0"
40
+ },
41
+ // For file row badges (slightly larger than small)
42
+ medium: {
43
+ minWidth: "22px",
44
+ height: "18px",
45
+ padding: "0 6px",
46
+ borderRadius: "9px",
47
+ fontSize: "10px",
48
+ fontWeight: 700,
49
+ letterSpacing: "0"
50
+ }
51
+ };
52
+ function Badge({
53
+ count,
54
+ size = "default",
55
+ backgroundColor,
56
+ color
57
+ }) {
58
+ const sizeStyles = BADGE_STYLES[size];
59
+ const bgColor = backgroundColor ?? getBadgeBackgroundColor(count);
60
+ const textColor = color ?? getBadgeTextColor(count);
61
+ return /* @__PURE__ */ jsx(
62
+ "span",
63
+ {
64
+ style: {
65
+ display: "inline-flex",
66
+ alignItems: "center",
67
+ justifyContent: "center",
68
+ ...sizeStyles,
69
+ backgroundColor: bgColor,
70
+ color: textColor,
71
+ fontFamily: FONT_MONO
72
+ },
73
+ children: count
74
+ }
75
+ );
76
+ }
77
+
78
+ // src/components/ui-lint/source-fetcher.ts
79
+ var sourceCache = /* @__PURE__ */ new Map();
80
+ var CACHE_TTL = 5 * 60 * 1e3;
81
+ var API_ENDPOINT = "/api/.uilint/source";
82
+ async function fetchSource(filePath) {
83
+ const cached = sourceCache.get(filePath);
84
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
85
+ return {
86
+ content: cached.content,
87
+ relativePath: cached.relativePath
88
+ };
89
+ }
90
+ try {
91
+ const response = await fetch(
92
+ `${API_ENDPOINT}?path=${encodeURIComponent(filePath)}`
93
+ );
94
+ if (!response.ok) {
95
+ console.warn(`[UILint] Failed to fetch source: ${response.statusText}`);
96
+ return null;
97
+ }
98
+ const data = await response.json();
99
+ sourceCache.set(filePath, {
100
+ ...data,
101
+ fetchedAt: Date.now()
102
+ });
103
+ return data;
104
+ } catch (error) {
105
+ console.error("[UILint] Error fetching source:", error);
106
+ return null;
107
+ }
108
+ }
109
+ async function fetchSourceWithContext(source, contextLines = 5) {
110
+ const result = await fetchSource(source.fileName);
111
+ if (!result) return null;
112
+ const allLines = result.content.split("\n");
113
+ const targetLine = source.lineNumber - 1;
114
+ const startLine = Math.max(0, targetLine - contextLines);
115
+ const endLine = Math.min(allLines.length, targetLine + contextLines + 1);
116
+ return {
117
+ lines: allLines.slice(startLine, endLine),
118
+ startLine: startLine + 1,
119
+ // Back to 1-indexed
120
+ highlightLine: source.lineNumber,
121
+ relativePath: result.relativePath
122
+ };
123
+ }
124
+ async function fetchSourceWithWindow(source, window) {
125
+ const result = await fetchSource(source.fileName);
126
+ if (!result) return null;
127
+ const allLines = result.content.split("\n");
128
+ const targetLine = source.lineNumber - 1;
129
+ const startLine = Math.max(0, targetLine - Math.max(0, window.linesAbove));
130
+ const endLine = Math.min(
131
+ allLines.length,
132
+ targetLine + Math.max(0, window.linesBelow) + 1
133
+ );
134
+ return {
135
+ lines: allLines.slice(startLine, endLine),
136
+ startLine: startLine + 1,
137
+ // Back to 1-indexed
138
+ highlightLine: source.lineNumber,
139
+ relativePath: result.relativePath
140
+ };
141
+ }
142
+ function clearSourceCache() {
143
+ sourceCache.clear();
144
+ }
145
+ function getCachedSource(filePath) {
146
+ const cached = sourceCache.get(filePath);
147
+ if (!cached) return null;
148
+ if (Date.now() - cached.fetchedAt >= CACHE_TTL) {
149
+ sourceCache.delete(filePath);
150
+ return null;
151
+ }
152
+ return {
153
+ content: cached.content,
154
+ relativePath: cached.relativePath
155
+ };
156
+ }
157
+ async function prefetchSources(filePaths) {
158
+ const uniquePaths = [...new Set(filePaths)].filter((path) => {
159
+ const cached = sourceCache.get(path);
160
+ return !cached || Date.now() - cached.fetchedAt >= CACHE_TTL;
161
+ });
162
+ const BATCH_SIZE = 5;
163
+ for (let i = 0; i < uniquePaths.length; i += BATCH_SIZE) {
164
+ const batch = uniquePaths.slice(i, i + BATCH_SIZE);
165
+ await Promise.all(batch.map(fetchSource));
166
+ }
167
+ }
168
+
169
+ export {
170
+ BADGE_COLORS,
171
+ Badge,
172
+ fetchSource,
173
+ fetchSourceWithContext,
174
+ fetchSourceWithWindow,
175
+ clearSourceCache,
176
+ getCachedSource,
177
+ prefetchSources
178
+ };
@@ -1,16 +1,18 @@
1
1
  "use client";
2
2
  import {
3
3
  BADGE_COLORS,
4
- Badge
5
- } from "./chunk-W42PI2OF.js";
4
+ Badge,
5
+ fetchSource,
6
+ getCachedSource
7
+ } from "./chunk-S4IWHBOQ.js";
6
8
  import {
7
9
  groupBySourceFile,
8
10
  useUILintContext,
9
11
  useUILintStore
10
- } from "./chunk-BJD2V2LF.js";
12
+ } from "./chunk-OU5EEQT6.js";
11
13
 
12
14
  // src/components/ui-lint/UILintToolbar.tsx
13
- import { useState as useState2, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
15
+ import { useState as useState2, useRef as useRef2, useEffect as useEffect3, useCallback as useCallback2 } from "react";
14
16
  import { createPortal } from "react-dom";
15
17
 
16
18
  // src/components/ui-lint/toolbar-styles.ts
@@ -228,10 +230,10 @@ function SettingsPopover({ settings }) {
228
230
  }
229
231
 
230
232
  // src/components/ui-lint/ScanPanelStack.tsx
231
- import { useRef, useEffect } from "react";
233
+ import { useRef, useEffect as useEffect2 } from "react";
232
234
 
233
235
  // src/components/ui-lint/ScanResultsPopover.tsx
234
- import { useState, useCallback, useMemo } from "react";
236
+ import { useState, useCallback, useMemo, useEffect } from "react";
235
237
 
236
238
  // src/components/ui-lint/toolbar-icons.tsx
237
239
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -860,12 +862,65 @@ function ElementRow({ item, onHover, onClick }) {
860
862
  }
861
863
  );
862
864
  }
865
+ function extractTagName(line, column) {
866
+ if (!line) return null;
867
+ if (column !== void 0) {
868
+ const searchStart = Math.max(0, column - 50);
869
+ const searchEnd = Math.min(line.length, column + 20);
870
+ const searchSlice = line.substring(searchStart, searchEnd);
871
+ const tagMatches = [
872
+ ...searchSlice.matchAll(/<([a-zA-Z][a-zA-Z0-9.-]*|Fragment)\b/g)
873
+ ];
874
+ for (const match of tagMatches) {
875
+ const tagStart = searchStart + match.index;
876
+ const tagEnd = tagStart + match[0].length;
877
+ if (column >= tagStart && column <= tagEnd + 30) {
878
+ return match[1];
879
+ }
880
+ }
881
+ }
882
+ const trimmed = line.trim();
883
+ const jsxTagMatch = trimmed.match(/^<([a-zA-Z][a-zA-Z0-9.-]*|Fragment)\b/);
884
+ if (jsxTagMatch) {
885
+ return jsxTagMatch[1];
886
+ }
887
+ return null;
888
+ }
863
889
  function FileLevelIssueRow({
864
890
  filePath,
865
891
  issue,
866
892
  onClick
867
893
  }) {
868
894
  const fileName = filePath.split("/").pop() || filePath;
895
+ const [tagName, setTagName] = useState(null);
896
+ useEffect(() => {
897
+ const cached = getCachedSource(filePath);
898
+ if (cached) {
899
+ const lines = cached.content.split("\n");
900
+ const lineIndex = issue.line - 1;
901
+ if (lineIndex >= 0 && lineIndex < lines.length) {
902
+ const line = lines[lineIndex];
903
+ const tag = extractTagName(line, issue.column);
904
+ if (tag) {
905
+ setTagName(tag);
906
+ return;
907
+ }
908
+ }
909
+ }
910
+ fetchSource(filePath).then((result) => {
911
+ if (result) {
912
+ const lines = result.content.split("\n");
913
+ const lineIndex = issue.line - 1;
914
+ if (lineIndex >= 0 && lineIndex < lines.length) {
915
+ const line = lines[lineIndex];
916
+ const tag = extractTagName(line, issue.column);
917
+ if (tag) {
918
+ setTagName(tag);
919
+ }
920
+ }
921
+ }
922
+ });
923
+ }, [filePath, issue.line, issue.column]);
869
924
  return /* @__PURE__ */ jsxs3(
870
925
  "div",
871
926
  {
@@ -896,7 +951,22 @@ function FileLevelIssueRow({
896
951
  marginRight: "12px"
897
952
  },
898
953
  children: [
899
- /* @__PURE__ */ jsx3(
954
+ tagName && /* @__PURE__ */ jsxs3(
955
+ "span",
956
+ {
957
+ style: {
958
+ fontSize: "11px",
959
+ fontFamily: STYLES.fontMono,
960
+ color: STYLES.accent
961
+ },
962
+ children: [
963
+ "<",
964
+ tagName,
965
+ ">"
966
+ ]
967
+ }
968
+ ),
969
+ !tagName && /* @__PURE__ */ jsx3(
900
970
  "span",
901
971
  {
902
972
  style: {
@@ -992,26 +1062,26 @@ function CloseIcon() {
992
1062
  import { jsx as jsx4 } from "react/jsx-runtime";
993
1063
  function ScanPanelStack({ show, onClose }) {
994
1064
  const containerRef = useRef(null);
995
- useEffect(() => {
1065
+ useEffect2(() => {
996
1066
  if (!show) return;
997
1067
  const handleClickOutside = (e) => {
998
1068
  const target = e.target;
1069
+ if (target?.closest?.("[data-ui-lint]")) {
1070
+ return;
1071
+ }
999
1072
  if (containerRef.current && !containerRef.current.contains(target)) {
1000
- const isUILintElement = target.closest?.("[data-ui-lint]");
1001
- if (!isUILintElement) {
1002
- onClose();
1003
- }
1073
+ onClose();
1004
1074
  }
1005
1075
  };
1006
1076
  const timeoutId = setTimeout(() => {
1007
- document.addEventListener("mousedown", handleClickOutside);
1077
+ document.addEventListener("mousedown", handleClickOutside, true);
1008
1078
  }, 100);
1009
1079
  return () => {
1010
1080
  clearTimeout(timeoutId);
1011
- document.removeEventListener("mousedown", handleClickOutside);
1081
+ document.removeEventListener("mousedown", handleClickOutside, true);
1012
1082
  };
1013
1083
  }, [show, onClose]);
1014
- useEffect(() => {
1084
+ useEffect2(() => {
1015
1085
  if (!show) return;
1016
1086
  const handleKeyDown = (e) => {
1017
1087
  if (e.key === "Escape") {
@@ -1021,16 +1091,25 @@ function ScanPanelStack({ show, onClose }) {
1021
1091
  window.addEventListener("keydown", handleKeyDown);
1022
1092
  return () => window.removeEventListener("keydown", handleKeyDown);
1023
1093
  }, [show, onClose]);
1094
+ const handleUILintInteraction = (e) => {
1095
+ e.stopPropagation();
1096
+ };
1024
1097
  if (!show) return null;
1025
1098
  return /* @__PURE__ */ jsx4(
1026
1099
  "div",
1027
1100
  {
1028
1101
  ref: containerRef,
1102
+ "data-ui-lint": true,
1103
+ onMouseDown: handleUILintInteraction,
1104
+ onClick: handleUILintInteraction,
1105
+ onKeyDown: handleUILintInteraction,
1029
1106
  style: {
1030
1107
  position: "absolute",
1031
1108
  bottom: "100%",
1032
1109
  left: 0,
1033
- marginBottom: "8px"
1110
+ marginBottom: "8px",
1111
+ pointerEvents: "auto"
1112
+ // Ensure panel is interactive
1034
1113
  },
1035
1114
  children: /* @__PURE__ */ jsx4(ScanResultsPopover, { onClose })
1036
1115
  }
@@ -1533,7 +1612,7 @@ function UILintToolbar() {
1533
1612
  const [nextjsOverlayVisible, setNextjsOverlayVisible] = useState2(false);
1534
1613
  const toolbarRef = useRef2(null);
1535
1614
  const settingsRef = useRef2(null);
1536
- useEffect2(() => {
1615
+ useEffect3(() => {
1537
1616
  const checkForNextOverlay = () => {
1538
1617
  const overlaySelectors = [
1539
1618
  "nextjs-portal",
@@ -1572,12 +1651,15 @@ function UILintToolbar() {
1572
1651
  });
1573
1652
  const totalIssues = elementIssues + fileLevelIssues;
1574
1653
  const hasIssues = totalIssues > 0;
1575
- useEffect2(() => {
1654
+ useEffect3(() => {
1576
1655
  setMounted(true);
1577
1656
  }, []);
1578
- useEffect2(() => {
1657
+ useEffect3(() => {
1579
1658
  const handleClickOutside = (e) => {
1580
1659
  const target = e.target;
1660
+ if (target?.closest?.("[data-ui-lint]")) {
1661
+ return;
1662
+ }
1581
1663
  if (showSettings && settingsRef.current && toolbarRef.current) {
1582
1664
  if (!settingsRef.current.contains(target) && !toolbarRef.current.contains(target)) {
1583
1665
  handleCloseSettings();
@@ -1590,10 +1672,10 @@ function UILintToolbar() {
1590
1672
  if (showResults) setShowResults(false);
1591
1673
  }
1592
1674
  };
1593
- document.addEventListener("mousedown", handleClickOutside);
1675
+ document.addEventListener("mousedown", handleClickOutside, true);
1594
1676
  document.addEventListener("keydown", handleEscape);
1595
1677
  return () => {
1596
- document.removeEventListener("mousedown", handleClickOutside);
1678
+ document.removeEventListener("mousedown", handleClickOutside, true);
1597
1679
  document.removeEventListener("keydown", handleEscape);
1598
1680
  };
1599
1681
  }, [showSettings, showResults]);
@@ -1628,6 +1710,12 @@ function UILintToolbar() {
1628
1710
  setSettingsClosing(false);
1629
1711
  }, 150);
1630
1712
  }, []);
1713
+ const handleUILintInteraction = useCallback2(
1714
+ (e) => {
1715
+ e.stopPropagation();
1716
+ },
1717
+ []
1718
+ );
1631
1719
  if (!mounted) return null;
1632
1720
  const issueVariant = !liveScanEnabled ? "default" : hasIssues ? "warning" : isComplete ? "success" : "default";
1633
1721
  const bottomPosition = nextjsOverlayVisible ? "80px" : "20px";
@@ -1635,13 +1723,18 @@ function UILintToolbar() {
1635
1723
  "div",
1636
1724
  {
1637
1725
  "data-ui-lint": true,
1726
+ onMouseDown: handleUILintInteraction,
1727
+ onClick: handleUILintInteraction,
1728
+ onKeyDown: handleUILintInteraction,
1638
1729
  style: {
1639
1730
  position: "fixed",
1640
1731
  bottom: bottomPosition,
1641
1732
  left: "20px",
1642
1733
  zIndex: 99999,
1643
1734
  fontFamily: TOKENS.fontFamily,
1644
- transition: `bottom ${TOKENS.transitionSlow}`
1735
+ transition: `bottom ${TOKENS.transitionSlow}`,
1736
+ pointerEvents: "none"
1737
+ // Allow clicks to pass through empty space
1645
1738
  },
1646
1739
  children: [
1647
1740
  /* @__PURE__ */ jsx5("style", { children: globalStyles }),
@@ -1654,7 +1747,9 @@ function UILintToolbar() {
1654
1747
  marginBottom: "10px",
1655
1748
  fontSize: "11px",
1656
1749
  color: TOKENS.textMuted,
1657
- letterSpacing: "0.02em"
1750
+ letterSpacing: "0.02em",
1751
+ pointerEvents: "auto"
1752
+ // Re-enable pointer events for hint
1658
1753
  },
1659
1754
  "aria-hidden": !liveScanEnabled,
1660
1755
  children: /* @__PURE__ */ jsx5(
@@ -1697,7 +1792,9 @@ function UILintToolbar() {
1697
1792
  `${TOKENS.warning}30`
1698
1793
  )}` : TOKENS.shadowMd,
1699
1794
  overflow: "hidden",
1700
- transition: `box-shadow ${TOKENS.transitionBase}`
1795
+ transition: `box-shadow ${TOKENS.transitionBase}`,
1796
+ pointerEvents: "auto"
1797
+ // Re-enable pointer events for interactive toolbar
1701
1798
  },
1702
1799
  children: [
1703
1800
  /* @__PURE__ */ jsx5(
@@ -1755,7 +1852,9 @@ function UILintToolbar() {
1755
1852
  position: "absolute",
1756
1853
  bottom: "100%",
1757
1854
  left: 0,
1758
- marginBottom: "8px"
1855
+ marginBottom: "8px",
1856
+ pointerEvents: "auto"
1857
+ // Re-enable pointer events for popover
1759
1858
  },
1760
1859
  children: /* @__PURE__ */ jsx5(SettingsPopover, { settings })
1761
1860
  }
package/dist/index.js CHANGED
@@ -1,19 +1,20 @@
1
1
  "use client";
2
2
  import {
3
3
  UILintToolbar
4
- } from "./chunk-EQKEHJI4.js";
4
+ } from "./chunk-XUMLILUN.js";
5
+ import {
6
+ InspectionPanel
7
+ } from "./chunk-JURUYCUC.js";
5
8
  import {
6
- InspectionPanel,
7
9
  clearSourceCache,
8
10
  fetchSource,
9
11
  fetchSourceWithContext,
10
12
  getCachedSource,
11
13
  prefetchSources
12
- } from "./chunk-ZZOKTGSU.js";
13
- import "./chunk-W42PI2OF.js";
14
+ } from "./chunk-S4IWHBOQ.js";
14
15
  import {
15
16
  LocatorOverlay
16
- } from "./chunk-YLTHKMTO.js";
17
+ } from "./chunk-LQ3WQYIF.js";
17
18
  import {
18
19
  DATA_UILINT_ID,
19
20
  DEFAULT_SETTINGS,
@@ -29,7 +30,7 @@ import {
29
30
  scanDOMForSources,
30
31
  updateElementRects,
31
32
  useUILintContext
32
- } from "./chunk-BJD2V2LF.js";
33
+ } from "./chunk-OU5EEQT6.js";
33
34
 
34
35
  // src/consistency/snapshot.ts
35
36
  var DATA_ELEMENTS_ATTR = "data-elements";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uilint-react",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "description": "React component for AI-powered UI consistency checking",
5
5
  "author": "Peter Suggate",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "node": ">=20.0.0"
35
35
  },
36
36
  "dependencies": {
37
- "uilint-core": "^0.1.45",
37
+ "uilint-core": "^0.1.46",
38
38
  "zustand": "^5.0.5"
39
39
  },
40
40
  "peerDependencies": {
@@ -1,10 +0,0 @@
1
- "use client";
2
- "use client";
3
- import {
4
- InspectionPanel
5
- } from "./chunk-ZZOKTGSU.js";
6
- import "./chunk-W42PI2OF.js";
7
- import "./chunk-BJD2V2LF.js";
8
- export {
9
- InspectionPanel
10
- };
@@ -1,10 +0,0 @@
1
- "use client";
2
- "use client";
3
- import {
4
- UILintToolbar
5
- } from "./chunk-EQKEHJI4.js";
6
- import "./chunk-W42PI2OF.js";
7
- import "./chunk-BJD2V2LF.js";
8
- export {
9
- UILintToolbar
10
- };
@@ -1,81 +0,0 @@
1
- "use client";
2
-
3
- // src/components/ui-lint/Badge.tsx
4
- import { jsx } from "react/jsx-runtime";
5
- var BADGE_COLORS = {
6
- success: "#68d391",
7
- // Soft green
8
- warning: "#f6ad55",
9
- // Warm orange
10
- error: "#ef4444"
11
- // Red (for future use)
12
- };
13
- var FONT_MONO = `"SF Mono", Monaco, "Cascadia Code", monospace`;
14
- function getBadgeTextColor(issueCount) {
15
- if (issueCount === 0) return BADGE_COLORS.success;
16
- return BADGE_COLORS.warning;
17
- }
18
- function getBadgeBackgroundColor(issueCount) {
19
- const color = getBadgeTextColor(issueCount);
20
- return `${color}20`;
21
- }
22
- var BADGE_STYLES = {
23
- default: {
24
- minWidth: "20px",
25
- height: "20px",
26
- padding: "0 6px",
27
- borderRadius: "10px",
28
- fontSize: "11px",
29
- fontWeight: 600,
30
- letterSpacing: "-0.02em"
31
- },
32
- small: {
33
- minWidth: "18px",
34
- height: "18px",
35
- padding: "0 5px",
36
- borderRadius: "9px",
37
- fontSize: "10px",
38
- fontWeight: 700,
39
- letterSpacing: "0"
40
- },
41
- // For file row badges (slightly larger than small)
42
- medium: {
43
- minWidth: "22px",
44
- height: "18px",
45
- padding: "0 6px",
46
- borderRadius: "9px",
47
- fontSize: "10px",
48
- fontWeight: 700,
49
- letterSpacing: "0"
50
- }
51
- };
52
- function Badge({
53
- count,
54
- size = "default",
55
- backgroundColor,
56
- color
57
- }) {
58
- const sizeStyles = BADGE_STYLES[size];
59
- const bgColor = backgroundColor ?? getBadgeBackgroundColor(count);
60
- const textColor = color ?? getBadgeTextColor(count);
61
- return /* @__PURE__ */ jsx(
62
- "span",
63
- {
64
- style: {
65
- display: "inline-flex",
66
- alignItems: "center",
67
- justifyContent: "center",
68
- ...sizeStyles,
69
- backgroundColor: bgColor,
70
- color: textColor,
71
- fontFamily: FONT_MONO
72
- },
73
- children: count
74
- }
75
- );
76
- }
77
-
78
- export {
79
- BADGE_COLORS,
80
- Badge
81
- };