uilint-react 0.1.44 → 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-HECINZHM.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-GCZ53K35.js";
7
- import "./chunk-HECINZHM.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-HECINZHM.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 ]+/);
@@ -246,7 +156,7 @@ var STYLES = {
246
156
  font: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
247
157
  fontMono: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace'
248
158
  };
249
- var POPOVER_WIDTH = 380;
159
+ var POPOVER_WIDTH = 450;
250
160
  var POPOVER_MAX_HEIGHT = 450;
251
161
  function InspectionPanel() {
252
162
  const {
@@ -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-HECINZHM.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-G5AVKNXA.js"),
1067
- import("./InspectionPanel-UW5UBZIK.js"),
1068
- import("./LocatorOverlay-RMU6JIFI.js"),
1069
- import("./ElementBadges-OQC6YCQD.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-HECINZHM.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
  }
@@ -1312,6 +1391,50 @@ var globalStyles = `
1312
1391
  .uilint-popover--closing {
1313
1392
  animation: uilint-fade-out ${TOKENS.transitionBase} forwards;
1314
1393
  }
1394
+
1395
+ /* Custom scrollbar styling for dark mode - scoped to uilint components */
1396
+ [data-ui-lint] *,
1397
+ [data-ui-lint] {
1398
+ /* Firefox */
1399
+ scrollbar-width: thin;
1400
+ scrollbar-color: rgba(255, 255, 255, 0.15) rgba(15, 15, 15, 0.3);
1401
+ }
1402
+
1403
+ /* WebKit browsers (Chrome, Safari, Edge) */
1404
+ [data-ui-lint] *::-webkit-scrollbar,
1405
+ [data-ui-lint]::-webkit-scrollbar {
1406
+ width: 8px;
1407
+ height: 8px;
1408
+ }
1409
+
1410
+ [data-ui-lint] *::-webkit-scrollbar-track,
1411
+ [data-ui-lint]::-webkit-scrollbar-track {
1412
+ background: rgba(15, 15, 15, 0.3);
1413
+ border-radius: 4px;
1414
+ }
1415
+
1416
+ [data-ui-lint] *::-webkit-scrollbar-thumb,
1417
+ [data-ui-lint]::-webkit-scrollbar-thumb {
1418
+ background: rgba(255, 255, 255, 0.15);
1419
+ border-radius: 4px;
1420
+ border: 1px solid rgba(15, 15, 15, 0.2);
1421
+ transition: background ${TOKENS.transitionFast};
1422
+ }
1423
+
1424
+ [data-ui-lint] *::-webkit-scrollbar-thumb:hover,
1425
+ [data-ui-lint]::-webkit-scrollbar-thumb:hover {
1426
+ background: rgba(255, 255, 255, 0.25);
1427
+ }
1428
+
1429
+ [data-ui-lint] *::-webkit-scrollbar-thumb:active,
1430
+ [data-ui-lint]::-webkit-scrollbar-thumb:active {
1431
+ background: rgba(255, 255, 255, 0.35);
1432
+ }
1433
+
1434
+ [data-ui-lint] *::-webkit-scrollbar-corner,
1435
+ [data-ui-lint]::-webkit-scrollbar-corner {
1436
+ background: rgba(15, 15, 15, 0.3);
1437
+ }
1315
1438
  `;
1316
1439
  function ToolbarButton({
1317
1440
  onClick,
@@ -1489,7 +1612,7 @@ function UILintToolbar() {
1489
1612
  const [nextjsOverlayVisible, setNextjsOverlayVisible] = useState2(false);
1490
1613
  const toolbarRef = useRef2(null);
1491
1614
  const settingsRef = useRef2(null);
1492
- useEffect2(() => {
1615
+ useEffect3(() => {
1493
1616
  const checkForNextOverlay = () => {
1494
1617
  const overlaySelectors = [
1495
1618
  "nextjs-portal",
@@ -1528,12 +1651,15 @@ function UILintToolbar() {
1528
1651
  });
1529
1652
  const totalIssues = elementIssues + fileLevelIssues;
1530
1653
  const hasIssues = totalIssues > 0;
1531
- useEffect2(() => {
1654
+ useEffect3(() => {
1532
1655
  setMounted(true);
1533
1656
  }, []);
1534
- useEffect2(() => {
1657
+ useEffect3(() => {
1535
1658
  const handleClickOutside = (e) => {
1536
1659
  const target = e.target;
1660
+ if (target?.closest?.("[data-ui-lint]")) {
1661
+ return;
1662
+ }
1537
1663
  if (showSettings && settingsRef.current && toolbarRef.current) {
1538
1664
  if (!settingsRef.current.contains(target) && !toolbarRef.current.contains(target)) {
1539
1665
  handleCloseSettings();
@@ -1546,10 +1672,10 @@ function UILintToolbar() {
1546
1672
  if (showResults) setShowResults(false);
1547
1673
  }
1548
1674
  };
1549
- document.addEventListener("mousedown", handleClickOutside);
1675
+ document.addEventListener("mousedown", handleClickOutside, true);
1550
1676
  document.addEventListener("keydown", handleEscape);
1551
1677
  return () => {
1552
- document.removeEventListener("mousedown", handleClickOutside);
1678
+ document.removeEventListener("mousedown", handleClickOutside, true);
1553
1679
  document.removeEventListener("keydown", handleEscape);
1554
1680
  };
1555
1681
  }, [showSettings, showResults]);
@@ -1584,6 +1710,12 @@ function UILintToolbar() {
1584
1710
  setSettingsClosing(false);
1585
1711
  }, 150);
1586
1712
  }, []);
1713
+ const handleUILintInteraction = useCallback2(
1714
+ (e) => {
1715
+ e.stopPropagation();
1716
+ },
1717
+ []
1718
+ );
1587
1719
  if (!mounted) return null;
1588
1720
  const issueVariant = !liveScanEnabled ? "default" : hasIssues ? "warning" : isComplete ? "success" : "default";
1589
1721
  const bottomPosition = nextjsOverlayVisible ? "80px" : "20px";
@@ -1591,13 +1723,18 @@ function UILintToolbar() {
1591
1723
  "div",
1592
1724
  {
1593
1725
  "data-ui-lint": true,
1726
+ onMouseDown: handleUILintInteraction,
1727
+ onClick: handleUILintInteraction,
1728
+ onKeyDown: handleUILintInteraction,
1594
1729
  style: {
1595
1730
  position: "fixed",
1596
1731
  bottom: bottomPosition,
1597
1732
  left: "20px",
1598
1733
  zIndex: 99999,
1599
1734
  fontFamily: TOKENS.fontFamily,
1600
- transition: `bottom ${TOKENS.transitionSlow}`
1735
+ transition: `bottom ${TOKENS.transitionSlow}`,
1736
+ pointerEvents: "none"
1737
+ // Allow clicks to pass through empty space
1601
1738
  },
1602
1739
  children: [
1603
1740
  /* @__PURE__ */ jsx5("style", { children: globalStyles }),
@@ -1610,7 +1747,9 @@ function UILintToolbar() {
1610
1747
  marginBottom: "10px",
1611
1748
  fontSize: "11px",
1612
1749
  color: TOKENS.textMuted,
1613
- letterSpacing: "0.02em"
1750
+ letterSpacing: "0.02em",
1751
+ pointerEvents: "auto"
1752
+ // Re-enable pointer events for hint
1614
1753
  },
1615
1754
  "aria-hidden": !liveScanEnabled,
1616
1755
  children: /* @__PURE__ */ jsx5(
@@ -1653,7 +1792,9 @@ function UILintToolbar() {
1653
1792
  `${TOKENS.warning}30`
1654
1793
  )}` : TOKENS.shadowMd,
1655
1794
  overflow: "hidden",
1656
- transition: `box-shadow ${TOKENS.transitionBase}`
1795
+ transition: `box-shadow ${TOKENS.transitionBase}`,
1796
+ pointerEvents: "auto"
1797
+ // Re-enable pointer events for interactive toolbar
1657
1798
  },
1658
1799
  children: [
1659
1800
  /* @__PURE__ */ jsx5(
@@ -1711,7 +1852,9 @@ function UILintToolbar() {
1711
1852
  position: "absolute",
1712
1853
  bottom: "100%",
1713
1854
  left: 0,
1714
- marginBottom: "8px"
1855
+ marginBottom: "8px",
1856
+ pointerEvents: "auto"
1857
+ // Re-enable pointer events for popover
1715
1858
  },
1716
1859
  children: /* @__PURE__ */ jsx5(SettingsPopover, { settings })
1717
1860
  }
package/dist/index.js CHANGED
@@ -1,19 +1,20 @@
1
1
  "use client";
2
2
  import {
3
3
  UILintToolbar
4
- } from "./chunk-RULQAXE4.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-X4PRYONL.js";
13
- import "./chunk-W42PI2OF.js";
14
+ } from "./chunk-S4IWHBOQ.js";
14
15
  import {
15
16
  LocatorOverlay
16
- } from "./chunk-GCZ53K35.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-HECINZHM.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.44",
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.44",
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-X4PRYONL.js";
6
- import "./chunk-W42PI2OF.js";
7
- import "./chunk-HECINZHM.js";
8
- export {
9
- InspectionPanel
10
- };
@@ -1,10 +0,0 @@
1
- "use client";
2
- "use client";
3
- import {
4
- UILintToolbar
5
- } from "./chunk-RULQAXE4.js";
6
- import "./chunk-W42PI2OF.js";
7
- import "./chunk-HECINZHM.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
- };