uilint-react 0.1.28 → 0.1.30

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.
@@ -2,11 +2,133 @@
2
2
  "use client";
3
3
  import {
4
4
  useUILintContext
5
- } from "./chunk-5VJ2Q2QW.js";
5
+ } from "./chunk-GJJH5B4Y.js";
6
6
 
7
7
  // src/components/ui-lint/ElementBadges.tsx
8
8
  import React, { useState, useEffect, useCallback, useMemo } from "react";
9
9
  import { createPortal } from "react-dom";
10
+
11
+ // src/components/ui-lint/badge-layout.ts
12
+ var DEFAULT_CONFIG = {
13
+ repulsionForce: 50,
14
+ anchorStrength: 0.3,
15
+ minDistance: 24,
16
+ iterations: 50,
17
+ damping: 0.9
18
+ };
19
+ function computeLayout(positions, config) {
20
+ if (positions.length === 0) return [];
21
+ if (positions.length === 1) {
22
+ return [
23
+ { ...positions[0], nudgedX: positions[0].x, nudgedY: positions[0].y }
24
+ ];
25
+ }
26
+ const nodes = positions.map((p) => ({
27
+ position: p,
28
+ nudgedX: p.x,
29
+ nudgedY: p.y,
30
+ velocityX: 0,
31
+ velocityY: 0
32
+ }));
33
+ for (let iter = 0; iter < config.iterations; iter++) {
34
+ for (let i = 0; i < nodes.length; i++) {
35
+ let fx = 0;
36
+ let fy = 0;
37
+ for (let j = 0; j < nodes.length; j++) {
38
+ if (i === j) continue;
39
+ const dx = nodes[i].nudgedX - nodes[j].nudgedX;
40
+ const dy = nodes[i].nudgedY - nodes[j].nudgedY;
41
+ const dist = Math.max(Math.hypot(dx, dy), 1);
42
+ if (dist < config.minDistance) {
43
+ const force = config.repulsionForce / (dist * dist);
44
+ fx += dx / dist * force;
45
+ fy += dy / dist * force;
46
+ }
47
+ }
48
+ const anchorDx = positions[i].x - nodes[i].nudgedX;
49
+ const anchorDy = positions[i].y - nodes[i].nudgedY;
50
+ fx += anchorDx * config.anchorStrength;
51
+ fy += anchorDy * config.anchorStrength;
52
+ nodes[i].velocityX = (nodes[i].velocityX + fx) * config.damping;
53
+ nodes[i].velocityY = (nodes[i].velocityY + fy) * config.damping;
54
+ nodes[i].nudgedX += nodes[i].velocityX;
55
+ nodes[i].nudgedY += nodes[i].velocityY;
56
+ }
57
+ }
58
+ return nodes.map((node) => ({
59
+ ...node.position,
60
+ nudgedX: node.nudgedX,
61
+ nudgedY: node.nudgedY
62
+ }));
63
+ }
64
+ var BadgeLayoutBuilder = class _BadgeLayoutBuilder {
65
+ config;
66
+ positions;
67
+ constructor(positions) {
68
+ this.positions = positions;
69
+ this.config = { ...DEFAULT_CONFIG };
70
+ }
71
+ /**
72
+ * Create a new layout builder with badge positions
73
+ */
74
+ static create(positions) {
75
+ return new _BadgeLayoutBuilder(positions);
76
+ }
77
+ /**
78
+ * Set the repulsion force (how strongly badges push apart)
79
+ * Higher values = badges spread more aggressively
80
+ */
81
+ repulsion(force) {
82
+ this.config.repulsionForce = force;
83
+ return this;
84
+ }
85
+ /**
86
+ * Set the anchor strength (how strongly badges stay near origin)
87
+ * Higher values = badges stay closer to their original positions
88
+ */
89
+ anchorStrength(strength) {
90
+ this.config.anchorStrength = strength;
91
+ return this;
92
+ }
93
+ /**
94
+ * Set the minimum distance between badge centers
95
+ * Badges closer than this will be pushed apart
96
+ */
97
+ minDistance(distance) {
98
+ this.config.minDistance = distance;
99
+ return this;
100
+ }
101
+ /**
102
+ * Set the number of simulation iterations
103
+ * More iterations = more stable but slower
104
+ */
105
+ iterations(count) {
106
+ this.config.iterations = count;
107
+ return this;
108
+ }
109
+ /**
110
+ * Set the damping factor (velocity decay per step)
111
+ * Lower values = system settles faster but may be less stable
112
+ */
113
+ damping(factor) {
114
+ this.config.damping = factor;
115
+ return this;
116
+ }
117
+ /**
118
+ * Run the simulation and return nudged positions
119
+ */
120
+ compute() {
121
+ return computeLayout(this.positions, this.config);
122
+ }
123
+ };
124
+ function findNearbyBadges(positions, x, y, threshold) {
125
+ return positions.filter((p) => {
126
+ const dist = Math.hypot(p.nudgedX - x, p.nudgedY - y);
127
+ return dist <= threshold;
128
+ });
129
+ }
130
+
131
+ // src/components/ui-lint/ElementBadges.tsx
10
132
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
133
  var STYLES = {
12
134
  bg: "rgba(17, 24, 39, 0.95)",
@@ -29,260 +151,29 @@ function getScaleFromDistance(distance) {
29
151
  const t = (distance - NEAR_DISTANCE) / (FAR_DISTANCE - NEAR_DISTANCE);
30
152
  return MAX_SCALE - t * (MAX_SCALE - MIN_SCALE);
31
153
  }
32
- var CLUSTER_THRESHOLD = 24;
33
154
  function getBadgeColor(issueCount) {
34
155
  if (issueCount === 0) return STYLES.success;
35
156
  if (issueCount <= 2) return STYLES.warning;
36
157
  return STYLES.error;
37
158
  }
38
- function ElementBadge({
39
- element,
40
- issue,
41
- distance,
42
- onSelect
43
- }) {
44
- const [rect, setRect] = useState(null);
45
- const [isHovered, setIsHovered] = useState(false);
46
- useEffect(() => {
47
- const updateRect = () => {
48
- if (element.element && document.contains(element.element)) {
49
- setRect(element.element.getBoundingClientRect());
50
- } else {
51
- setRect(null);
52
- }
53
- };
54
- updateRect();
55
- let rafId;
56
- const handleUpdate = () => {
57
- updateRect();
58
- rafId = requestAnimationFrame(handleUpdate);
59
- };
60
- rafId = requestAnimationFrame(handleUpdate);
61
- return () => {
62
- cancelAnimationFrame(rafId);
63
- };
64
- }, [element.element]);
65
- const handleClick = useCallback(
66
- (e) => {
67
- e.preventDefault();
68
- e.stopPropagation();
69
- onSelect(element, issue);
70
- },
71
- [element, issue, onSelect]
72
- );
73
- if (!rect) return null;
74
- if (rect.top < -50 || rect.top > window.innerHeight + 50) return null;
75
- if (rect.left < -50 || rect.left > window.innerWidth + 50) return null;
76
- const scale = isHovered ? 1.1 : getScaleFromDistance(distance);
77
- const badgeStyle = {
78
- position: "fixed",
79
- top: rect.top - 8,
80
- left: rect.right - 8,
81
- zIndex: isHovered ? 99999 : 99995,
82
- cursor: "pointer",
83
- transition: "transform 0.1s ease-out",
84
- transform: `scale(${scale})`,
85
- transformOrigin: "center center"
86
- };
87
- return /* @__PURE__ */ jsxs(Fragment, { children: [
88
- isHovered && /* @__PURE__ */ jsx(
89
- "div",
90
- {
91
- style: {
92
- position: "fixed",
93
- top: rect.top - 2,
94
- left: rect.left - 2,
95
- width: rect.width + 4,
96
- height: rect.height + 4,
97
- border: `2px solid ${STYLES.highlight}`,
98
- borderRadius: "4px",
99
- pointerEvents: "none",
100
- zIndex: 99994,
101
- boxShadow: `0 0 0 1px rgba(59, 130, 246, 0.3)`
102
- },
103
- "data-ui-lint": true
104
- }
105
- ),
106
- /* @__PURE__ */ jsxs(
107
- "div",
108
- {
109
- style: badgeStyle,
110
- "data-ui-lint": true,
111
- onMouseEnter: () => setIsHovered(true),
112
- onMouseLeave: () => setIsHovered(false),
113
- onClick: handleClick,
114
- children: [
115
- issue.status === "scanning" && /* @__PURE__ */ jsx(ScanningBadge, {}),
116
- issue.status === "complete" && /* @__PURE__ */ jsx(IssueBadge, { count: issue.issues.length }),
117
- issue.status === "error" && /* @__PURE__ */ jsx(ErrorBadge, {}),
118
- issue.status === "pending" && /* @__PURE__ */ jsx(PendingBadge, {})
119
- ]
120
- }
121
- )
122
- ] });
123
- }
124
- function IssueBadge({ count }) {
125
- const color = getBadgeColor(count);
126
- if (count === 0) {
127
- return /* @__PURE__ */ jsx(
128
- "div",
129
- {
130
- style: {
131
- display: "flex",
132
- alignItems: "center",
133
- justifyContent: "center",
134
- width: "18px",
135
- height: "18px",
136
- borderRadius: "50%",
137
- backgroundColor: color,
138
- boxShadow: STYLES.shadow,
139
- border: `1px solid ${STYLES.border}`
140
- },
141
- children: /* @__PURE__ */ jsx(CheckIcon, {})
142
- }
143
- );
159
+ function formatElementLabel(element) {
160
+ const tag = element.tagName.toLowerCase();
161
+ const source = element.source;
162
+ if (source) {
163
+ const fileName = source.fileName.split("/").pop() || "Unknown";
164
+ return `${tag} > ${fileName}`;
144
165
  }
145
- return /* @__PURE__ */ jsx(
146
- "div",
147
- {
148
- style: {
149
- display: "flex",
150
- alignItems: "center",
151
- justifyContent: "center",
152
- minWidth: "18px",
153
- height: "18px",
154
- padding: "0 5px",
155
- borderRadius: "9px",
156
- backgroundColor: color,
157
- color: STYLES.text,
158
- fontSize: "10px",
159
- fontWeight: 700,
160
- fontFamily: STYLES.font,
161
- boxShadow: STYLES.shadow,
162
- border: `1px solid ${STYLES.border}`
163
- },
164
- children: count > 9 ? "9+" : count
165
- }
166
- );
166
+ const componentName = element.componentStack[0]?.name;
167
+ return componentName ? `${tag} > ${componentName}` : tag;
167
168
  }
168
- function ScanningBadge() {
169
- return /* @__PURE__ */ jsxs(
170
- "div",
171
- {
172
- style: {
173
- display: "flex",
174
- alignItems: "center",
175
- justifyContent: "center",
176
- width: "18px",
177
- height: "18px",
178
- borderRadius: "50%",
179
- backgroundColor: STYLES.bg,
180
- boxShadow: STYLES.shadow,
181
- border: `1px solid ${STYLES.border}`
182
- },
183
- children: [
184
- /* @__PURE__ */ jsx("style", { children: `
185
- @keyframes uilint-badge-spin {
186
- from { transform: rotate(0deg); }
187
- to { transform: rotate(360deg); }
188
- }
189
- ` }),
190
- /* @__PURE__ */ jsx(
191
- "div",
192
- {
193
- style: {
194
- width: "10px",
195
- height: "10px",
196
- border: "2px solid rgba(59, 130, 246, 0.3)",
197
- borderTopColor: "#3B82F6",
198
- borderRadius: "50%",
199
- animation: "uilint-badge-spin 0.8s linear infinite"
200
- }
201
- }
202
- )
203
- ]
204
- }
205
- );
206
- }
207
- function PendingBadge() {
208
- return /* @__PURE__ */ jsx(
209
- "div",
210
- {
211
- style: {
212
- width: "10px",
213
- height: "10px",
214
- borderRadius: "50%",
215
- backgroundColor: "rgba(156, 163, 175, 0.5)",
216
- boxShadow: STYLES.shadow
169
+ var NEARBY_THRESHOLD = 30;
170
+ function BadgeAnimationStyles() {
171
+ return /* @__PURE__ */ jsx("style", { children: `
172
+ @keyframes uilint-badge-spin {
173
+ from { transform: rotate(0deg); }
174
+ to { transform: rotate(360deg); }
217
175
  }
218
- }
219
- );
220
- }
221
- function ErrorBadge() {
222
- return /* @__PURE__ */ jsx(
223
- "div",
224
- {
225
- style: {
226
- display: "flex",
227
- alignItems: "center",
228
- justifyContent: "center",
229
- width: "18px",
230
- height: "18px",
231
- borderRadius: "50%",
232
- backgroundColor: STYLES.error,
233
- boxShadow: STYLES.shadow,
234
- border: `1px solid ${STYLES.border}`
235
- },
236
- children: /* @__PURE__ */ jsx(ExclamationIcon, {})
237
- }
238
- );
239
- }
240
- var UnionFind = class {
241
- parent = /* @__PURE__ */ new Map();
242
- find(x) {
243
- if (!this.parent.has(x)) {
244
- this.parent.set(x, x);
245
- }
246
- if (this.parent.get(x) !== x) {
247
- this.parent.set(x, this.find(this.parent.get(x)));
248
- }
249
- return this.parent.get(x);
250
- }
251
- union(x, y) {
252
- const px = this.find(x);
253
- const py = this.find(y);
254
- if (px !== py) {
255
- this.parent.set(px, py);
256
- }
257
- }
258
- };
259
- function clusterBadges(positions, threshold) {
260
- if (positions.length === 0) return [];
261
- const uf = new UnionFind();
262
- for (let i = 0; i < positions.length; i++) {
263
- for (let j = i + 1; j < positions.length; j++) {
264
- const dist = Math.hypot(
265
- positions[i].x - positions[j].x,
266
- positions[i].y - positions[j].y
267
- );
268
- if (dist <= threshold) {
269
- uf.union(positions[i].element.id, positions[j].element.id);
270
- }
271
- }
272
- }
273
- const clusters = /* @__PURE__ */ new Map();
274
- for (const pos of positions) {
275
- const root = uf.find(pos.element.id);
276
- if (!clusters.has(root)) {
277
- clusters.set(root, []);
278
- }
279
- clusters.get(root).push(pos);
280
- }
281
- return Array.from(clusters.entries()).map(([id, badges]) => {
282
- const centroidX = badges.reduce((sum, b) => sum + b.x, 0) / badges.length;
283
- const centroidY = badges.reduce((sum, b) => sum + b.y, 0) / badges.length;
284
- return { id, badges, centroidX, centroidY };
285
- });
176
+ ` });
286
177
  }
287
178
  function ElementBadges() {
288
179
  const { autoScanState, elementIssuesCache, setInspectedElement } = useUILintContext();
@@ -341,66 +232,61 @@ function ElementBadges() {
341
232
  },
342
233
  [setInspectedElement]
343
234
  );
344
- const clusters = useMemo(
345
- () => clusterBadges(badgePositions, CLUSTER_THRESHOLD),
235
+ const nudgedPositions = useMemo(
236
+ () => BadgeLayoutBuilder.create(badgePositions).minDistance(24).repulsion(50).anchorStrength(0.3).iterations(50).compute(),
346
237
  [badgePositions]
347
238
  );
348
239
  if (!mounted) return null;
349
240
  if (autoScanState.status === "idle") return null;
350
- const content = /* @__PURE__ */ jsx("div", { "data-ui-lint": true, children: clusters.map((cluster) => {
351
- if (cluster.badges.length === 1) {
352
- const { element, issue, x, y } = cluster.badges[0];
353
- const distance = Math.hypot(x - cursorPos.x, y - cursorPos.y);
354
- return /* @__PURE__ */ jsx(
355
- ElementBadge,
356
- {
357
- element,
358
- issue,
359
- distance,
360
- onSelect: handleSelect
361
- },
362
- element.id
363
- );
364
- } else {
241
+ const content = /* @__PURE__ */ jsxs("div", { "data-ui-lint": true, children: [
242
+ /* @__PURE__ */ jsx(BadgeAnimationStyles, {}),
243
+ nudgedPositions.map((nudgedPos) => {
365
244
  const distance = Math.hypot(
366
- cluster.centroidX - cursorPos.x,
367
- cluster.centroidY - cursorPos.y
245
+ nudgedPos.nudgedX - cursorPos.x,
246
+ nudgedPos.nudgedY - cursorPos.y
247
+ );
248
+ const nearbyBadges = findNearbyBadges(
249
+ nudgedPositions,
250
+ nudgedPos.nudgedX,
251
+ nudgedPos.nudgedY,
252
+ NEARBY_THRESHOLD
368
253
  );
369
254
  return /* @__PURE__ */ jsx(
370
- ClusteredBadge,
255
+ NudgedBadge,
371
256
  {
372
- cluster,
257
+ position: nudgedPos,
373
258
  distance,
259
+ nearbyBadges,
260
+ cursorPos,
374
261
  onSelect: handleSelect
375
262
  },
376
- cluster.id
263
+ nudgedPos.element.id
377
264
  );
378
- }
379
- }) });
265
+ })
266
+ ] });
380
267
  return createPortal(content, document.body);
381
268
  }
382
- function ClusteredBadge({ cluster, distance, onSelect }) {
269
+ function NudgedBadge({
270
+ position,
271
+ distance,
272
+ nearbyBadges,
273
+ cursorPos,
274
+ onSelect
275
+ }) {
383
276
  const [isExpanded, setIsExpanded] = useState(false);
384
277
  const [hoveredIndex, setHoveredIndex] = useState(null);
385
278
  const closeTimeoutRef = React.useRef(null);
386
- const badgeSegments = useMemo(() => {
387
- return cluster.badges.map(({ issue }) => {
388
- if (issue.status === "complete") {
389
- const count = issue.issues.length;
390
- return {
391
- type: "count",
392
- count,
393
- color: getBadgeColor(count)
394
- };
395
- } else if (issue.status === "error") {
396
- return { type: "error", color: STYLES.error };
397
- } else if (issue.status === "scanning") {
398
- return { type: "scanning", color: STYLES.highlight };
399
- } else {
400
- return { type: "pending", color: "rgba(156, 163, 175, 0.5)" };
401
- }
402
- });
403
- }, [cluster.badges]);
279
+ const { element, issue, rect, nudgedX, nudgedY } = position;
280
+ const hasNearbyBadges = nearbyBadges.length > 1;
281
+ const badgeColor = useMemo(() => {
282
+ if (issue.status === "error") return STYLES.error;
283
+ if (issue.status === "scanning") return STYLES.highlight;
284
+ if (issue.status === "pending") return "rgba(156, 163, 175, 0.7)";
285
+ if (issue.status === "complete") {
286
+ return getBadgeColor(issue.issues.length);
287
+ }
288
+ return STYLES.success;
289
+ }, [issue]);
404
290
  const handleMouseEnter = useCallback(() => {
405
291
  if (closeTimeoutRef.current) {
406
292
  clearTimeout(closeTimeoutRef.current);
@@ -414,27 +300,58 @@ function ClusteredBadge({ cluster, distance, onSelect }) {
414
300
  setHoveredIndex(null);
415
301
  }, 150);
416
302
  }, []);
417
- const hoveredBadge = hoveredIndex !== null ? cluster.badges[hoveredIndex] : null;
303
+ const handleClick = useCallback(
304
+ (e) => {
305
+ e.preventDefault();
306
+ e.stopPropagation();
307
+ onSelect(element, issue);
308
+ },
309
+ [element, issue, onSelect]
310
+ );
311
+ const hoveredBadge = useMemo(() => {
312
+ if (hoveredIndex === null) return null;
313
+ return nearbyBadges[hoveredIndex] ?? null;
314
+ }, [hoveredIndex, nearbyBadges]);
418
315
  const dropdownStyle = useMemo(() => {
419
- const preferRight = cluster.centroidX < window.innerWidth - 200;
420
- const preferBelow = cluster.centroidY < window.innerHeight - 200;
316
+ const preferRight = nudgedX < window.innerWidth - 220;
317
+ const preferBelow = nudgedY < window.innerHeight - 200;
421
318
  return {
422
319
  position: "fixed",
423
- top: preferBelow ? cluster.centroidY + 12 : void 0,
424
- bottom: preferBelow ? void 0 : window.innerHeight - cluster.centroidY + 12,
425
- left: preferRight ? cluster.centroidX - 8 : void 0,
426
- right: preferRight ? void 0 : window.innerWidth - cluster.centroidX - 8,
320
+ top: preferBelow ? nudgedY + 12 : void 0,
321
+ bottom: preferBelow ? void 0 : window.innerHeight - nudgedY + 12,
322
+ left: preferRight ? nudgedX - 8 : void 0,
323
+ right: preferRight ? void 0 : window.innerWidth - nudgedX - 8,
427
324
  zIndex: 1e5,
428
325
  backgroundColor: STYLES.bg,
429
326
  borderRadius: "8px",
430
327
  border: `1px solid ${STYLES.border}`,
431
328
  boxShadow: "0 4px 20px rgba(0, 0, 0, 0.4)",
432
329
  padding: "4px 0",
433
- minWidth: "180px",
330
+ minWidth: "200px",
434
331
  fontFamily: STYLES.font
435
332
  };
436
- }, [cluster.centroidX, cluster.centroidY]);
333
+ }, [nudgedX, nudgedY]);
334
+ const scale = isExpanded ? 1.1 : getScaleFromDistance(distance);
335
+ const issueCount = issue.status === "complete" ? issue.issues.length : 0;
437
336
  return /* @__PURE__ */ jsxs(Fragment, { children: [
337
+ isExpanded && !hoveredBadge && /* @__PURE__ */ jsx(
338
+ "div",
339
+ {
340
+ style: {
341
+ position: "fixed",
342
+ top: rect.top - 2,
343
+ left: rect.left - 2,
344
+ width: rect.width + 4,
345
+ height: rect.height + 4,
346
+ border: `2px solid ${STYLES.highlight}`,
347
+ borderRadius: "4px",
348
+ pointerEvents: "none",
349
+ zIndex: 99994,
350
+ boxShadow: `0 0 0 1px rgba(59, 130, 246, 0.3)`
351
+ },
352
+ "data-ui-lint": true
353
+ }
354
+ ),
438
355
  hoveredBadge && /* @__PURE__ */ jsx(
439
356
  "div",
440
357
  {
@@ -458,98 +375,79 @@ function ClusteredBadge({ cluster, distance, onSelect }) {
458
375
  {
459
376
  style: {
460
377
  position: "fixed",
461
- top: cluster.centroidY - 9,
462
- left: cluster.centroidX - 9,
378
+ top: nudgedY - 9,
379
+ left: nudgedX - 9,
463
380
  zIndex: isExpanded ? 99999 : 99995,
464
381
  cursor: "pointer",
465
- transition: "transform 0.1s ease-out",
466
- transform: `scale(${isExpanded ? 1.1 : getScaleFromDistance(distance)})`,
382
+ transition: "transform 0.1s ease-out, top 0.15s ease-out, left 0.15s ease-out",
383
+ transform: `scale(${scale})`,
467
384
  transformOrigin: "center center"
468
385
  },
469
386
  "data-ui-lint": true,
470
387
  onMouseEnter: handleMouseEnter,
471
388
  onMouseLeave: handleMouseLeave,
389
+ onClick: handleClick,
472
390
  children: /* @__PURE__ */ jsx(
473
391
  "div",
474
392
  {
475
393
  style: {
476
394
  display: "flex",
477
395
  alignItems: "center",
478
- height: "20px",
479
- borderRadius: "10px",
480
- backgroundColor: STYLES.bg,
396
+ justifyContent: "center",
397
+ width: "18px",
398
+ height: "18px",
399
+ borderRadius: "50%",
400
+ backgroundColor: badgeColor,
481
401
  boxShadow: STYLES.shadow,
482
- border: `1px solid ${STYLES.border}`,
483
- overflow: "hidden"
402
+ border: `1px solid ${STYLES.border}`
484
403
  },
485
- children: badgeSegments.map((segment, index) => /* @__PURE__ */ jsxs(
404
+ children: issue.status === "scanning" ? /* @__PURE__ */ jsx(
405
+ "div",
406
+ {
407
+ style: {
408
+ width: "10px",
409
+ height: "10px",
410
+ border: "2px solid rgba(255, 255, 255, 0.3)",
411
+ borderTopColor: "#FFFFFF",
412
+ borderRadius: "50%",
413
+ animation: "uilint-badge-spin 0.8s linear infinite"
414
+ }
415
+ }
416
+ ) : issue.status === "error" ? /* @__PURE__ */ jsx(ExclamationIconTiny, {}) : issue.status === "pending" ? /* @__PURE__ */ jsx(
486
417
  "div",
487
418
  {
488
419
  style: {
489
- display: "flex",
490
- alignItems: "center",
491
- justifyContent: "center",
492
- minWidth: "18px",
493
- height: "100%",
494
- padding: "0 4px",
495
- backgroundColor: segment.color,
496
- borderRight: index < badgeSegments.length - 1 ? `1px solid rgba(0, 0, 0, 0.2)` : void 0
420
+ width: "6px",
421
+ height: "6px",
422
+ borderRadius: "50%",
423
+ backgroundColor: "rgba(255, 255, 255, 0.4)"
424
+ }
425
+ }
426
+ ) : issueCount === 0 ? /* @__PURE__ */ jsx(CheckIconTiny, {}) : /* @__PURE__ */ jsx(
427
+ "span",
428
+ {
429
+ style: {
430
+ color: STYLES.text,
431
+ fontSize: "10px",
432
+ fontWeight: 700,
433
+ fontFamily: STYLES.font
497
434
  },
498
- children: [
499
- segment.type === "count" && (segment.count === 0 ? /* @__PURE__ */ jsx(CheckIconTiny, {}) : /* @__PURE__ */ jsx(
500
- "span",
501
- {
502
- style: {
503
- color: STYLES.text,
504
- fontSize: "10px",
505
- fontWeight: 700,
506
- fontFamily: STYLES.font
507
- },
508
- children: segment.count > 9 ? "9+" : segment.count
509
- }
510
- )),
511
- segment.type === "error" && /* @__PURE__ */ jsx(ExclamationIconTiny, {}),
512
- segment.type === "scanning" && /* @__PURE__ */ jsx(
513
- "div",
514
- {
515
- style: {
516
- width: "8px",
517
- height: "8px",
518
- border: "1.5px solid rgba(255, 255, 255, 0.3)",
519
- borderTopColor: "#FFFFFF",
520
- borderRadius: "50%",
521
- animation: "uilint-badge-spin 0.8s linear infinite"
522
- }
523
- }
524
- ),
525
- segment.type === "pending" && /* @__PURE__ */ jsx(
526
- "div",
527
- {
528
- style: {
529
- width: "6px",
530
- height: "6px",
531
- borderRadius: "50%",
532
- backgroundColor: "rgba(255, 255, 255, 0.4)"
533
- }
534
- }
535
- )
536
- ]
537
- },
538
- index
539
- ))
435
+ children: issueCount > 9 ? "9+" : issueCount
436
+ }
437
+ )
540
438
  }
541
439
  )
542
440
  }
543
441
  ),
544
- isExpanded && /* @__PURE__ */ jsx(
442
+ isExpanded && hasNearbyBadges && /* @__PURE__ */ jsx(
545
443
  "div",
546
444
  {
547
445
  style: dropdownStyle,
548
446
  "data-ui-lint": true,
549
447
  onMouseEnter: handleMouseEnter,
550
448
  onMouseLeave: handleMouseLeave,
551
- children: cluster.badges.map((badge, index) => /* @__PURE__ */ jsx(
552
- ClusterDropdownItem,
449
+ children: nearbyBadges.map((badge, index) => /* @__PURE__ */ jsx(
450
+ DropdownItem,
553
451
  {
554
452
  badge,
555
453
  isHovered: hoveredIndex === index,
@@ -563,14 +461,14 @@ function ClusteredBadge({ cluster, distance, onSelect }) {
563
461
  )
564
462
  ] });
565
463
  }
566
- function ClusterDropdownItem({
464
+ function DropdownItem({
567
465
  badge,
568
466
  isHovered,
569
467
  onMouseEnter,
570
468
  onMouseLeave,
571
469
  onClick
572
470
  }) {
573
- const componentName = badge.element.componentStack[0]?.name || badge.element.tagName.toLowerCase();
471
+ const elementLabel = formatElementLabel(badge.element);
574
472
  const issueCount = badge.issue.status === "complete" ? badge.issue.issues.length : 0;
575
473
  const color = getBadgeColor(issueCount);
576
474
  return /* @__PURE__ */ jsxs(
@@ -607,12 +505,12 @@ function ClusterDropdownItem({
607
505
  style: {
608
506
  fontSize: "12px",
609
507
  color: STYLES.text,
610
- maxWidth: "120px",
508
+ maxWidth: "160px",
611
509
  overflow: "hidden",
612
510
  textOverflow: "ellipsis",
613
511
  whiteSpace: "nowrap"
614
512
  },
615
- children: componentName
513
+ children: elementLabel
616
514
  }
617
515
  )
618
516
  ] }),
@@ -2,8 +2,8 @@
2
2
  "use client";
3
3
  import {
4
4
  InspectionPanel
5
- } from "./chunk-QYRESGFG.js";
6
- import "./chunk-5VJ2Q2QW.js";
5
+ } from "./chunk-5CWWYEQM.js";
6
+ import "./chunk-GJJH5B4Y.js";
7
7
  export {
8
8
  InspectionPanel
9
9
  };
@@ -3,8 +3,8 @@
3
3
  import {
4
4
  InspectedElementHighlight,
5
5
  LocatorOverlay
6
- } from "./chunk-XLIDEQXH.js";
7
- import "./chunk-5VJ2Q2QW.js";
6
+ } from "./chunk-IMPRRMMY.js";
7
+ import "./chunk-GJJH5B4Y.js";
8
8
  export {
9
9
  InspectedElementHighlight,
10
10
  LocatorOverlay
@@ -2,8 +2,8 @@
2
2
  "use client";
3
3
  import {
4
4
  UILintToolbar
5
- } from "./chunk-7X5HN55P.js";
6
- import "./chunk-5VJ2Q2QW.js";
5
+ } from "./chunk-KBEQSZMW.js";
6
+ import "./chunk-GJJH5B4Y.js";
7
7
  export {
8
8
  UILintToolbar
9
9
  };
@@ -3,7 +3,7 @@ import {
3
3
  buildEditorUrl,
4
4
  useUILintContext,
5
5
  useUILintStore
6
- } from "./chunk-5VJ2Q2QW.js";
6
+ } from "./chunk-GJJH5B4Y.js";
7
7
 
8
8
  // src/components/ui-lint/InspectionPanel.tsx
9
9
  import { useState, useEffect, useCallback, useMemo } from "react";
@@ -570,7 +570,10 @@ function ScanSection({ element }) {
570
570
  const manualScan = useUILintStore(
571
571
  (s) => s.manualScanCache.get(manualKey)
572
572
  );
573
- const upsertManualScan = useUILintStore((s) => s.upsertManualScan);
573
+ const upsertManualScan = useUILintStore(
574
+ (s) => s.upsertManualScan
575
+ );
576
+ const includeChildren = manualScan?.includeChildren ?? false;
574
577
  const componentName = element.componentStack[0]?.name || element.element.tagName.toLowerCase();
575
578
  const componentLine = element.source?.lineNumber;
576
579
  const cachedIssue = useMemo(() => {
@@ -626,6 +629,22 @@ Please update this component to match our styleguide.`;
626
629
  });
627
630
  return;
628
631
  }
632
+ const MAX_DATALOCS = 80;
633
+ const selectedDataLoc = element.element.getAttribute("data-loc");
634
+ let dataLocList = [];
635
+ if (includeChildren) {
636
+ const nodes = [
637
+ element.element,
638
+ ...Array.from(element.element.querySelectorAll("[data-loc]"))
639
+ ];
640
+ for (const n of nodes) {
641
+ const v = n.getAttribute("data-loc");
642
+ if (v) dataLocList.push(v);
643
+ }
644
+ } else if (selectedDataLoc) {
645
+ dataLocList = [selectedDataLoc];
646
+ }
647
+ dataLocList = Array.from(new Set(dataLocList)).slice(0, MAX_DATALOCS);
629
648
  upsertManualScan(manualKey, {
630
649
  status: "scanning",
631
650
  error: void 0,
@@ -645,7 +664,6 @@ Please update this component to match our styleguide.`;
645
664
  const sourceData = await sourceResponse.json();
646
665
  const sourceCode = sourceData.content;
647
666
  const relativePath = sourceData.relativePath || element.source.fileName;
648
- const dataLoc = element.element.getAttribute("data-loc");
649
667
  const analyzeResponse = await fetch("/api/.uilint/analyze", {
650
668
  method: "POST",
651
669
  headers: { "Content-Type": "application/json" },
@@ -654,7 +672,8 @@ Please update this component to match our styleguide.`;
654
672
  filePath: relativePath,
655
673
  componentName,
656
674
  componentLine,
657
- dataLocs: dataLoc ? [dataLoc] : void 0,
675
+ includeChildren,
676
+ dataLocs: dataLocList.length > 0 ? dataLocList : void 0,
658
677
  stream: true
659
678
  })
660
679
  });
@@ -732,20 +751,24 @@ Please update this component to match our styleguide.`;
732
751
  componentLine,
733
752
  generateFixPrompt,
734
753
  manualKey,
735
- upsertManualScan
754
+ upsertManualScan,
755
+ includeChildren
736
756
  ]);
737
- const handleCopy = useCallback(async (text) => {
738
- try {
739
- await navigator.clipboard.writeText(text);
740
- setCopied(true);
741
- setTimeout(() => setCopied(false), 2e3);
742
- } catch {
743
- upsertManualScan(manualKey, {
744
- status: "error",
745
- error: "Failed to copy to clipboard"
746
- });
747
- }
748
- }, [manualKey, upsertManualScan]);
757
+ const handleCopy = useCallback(
758
+ async (text) => {
759
+ try {
760
+ await navigator.clipboard.writeText(text);
761
+ setCopied(true);
762
+ setTimeout(() => setCopied(false), 2e3);
763
+ } catch {
764
+ upsertManualScan(manualKey, {
765
+ status: "error",
766
+ error: "Failed to copy to clipboard"
767
+ });
768
+ }
769
+ },
770
+ [manualKey, upsertManualScan]
771
+ );
749
772
  const manualStatus = manualScan?.status ?? "idle";
750
773
  const showCachedScanning = cachedIssue?.status === "scanning" && manualStatus === "idle";
751
774
  const showCachedPending = cachedIssue?.status === "pending" && manualStatus === "idle";
@@ -757,6 +780,7 @@ Please update this component to match our styleguide.`;
757
780
  const error = manualScan?.status === "error" ? manualScan.error : null;
758
781
  const fixPrompt = manualScan?.fixPrompt ?? null;
759
782
  const progressLine = manualScan?.progressLine ?? null;
783
+ const scopeLabel = includeChildren ? "Element + children" : "Element only";
760
784
  return /* @__PURE__ */ jsx(
761
785
  "div",
762
786
  {
@@ -1025,6 +1049,39 @@ Please update this component to match our styleguide.`;
1025
1049
  ) })
1026
1050
  ] }),
1027
1051
  showScanButton && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "16px 0" }, children: [
1052
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "10px" }, children: [
1053
+ /* @__PURE__ */ jsxs(
1054
+ "label",
1055
+ {
1056
+ style: {
1057
+ display: "inline-flex",
1058
+ alignItems: "center",
1059
+ gap: "8px",
1060
+ fontSize: "12px",
1061
+ color: STYLES.textMuted,
1062
+ cursor: "pointer",
1063
+ userSelect: "none"
1064
+ },
1065
+ children: [
1066
+ /* @__PURE__ */ jsx(
1067
+ "input",
1068
+ {
1069
+ type: "checkbox",
1070
+ checked: includeChildren,
1071
+ onChange: (e) => upsertManualScan(manualKey, {
1072
+ includeChildren: e.currentTarget.checked
1073
+ })
1074
+ }
1075
+ ),
1076
+ "Include children"
1077
+ ]
1078
+ }
1079
+ ),
1080
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: "6px", fontSize: "11px", color: STYLES.textDim }, children: [
1081
+ "Scope: ",
1082
+ scopeLabel
1083
+ ] })
1084
+ ] }),
1028
1085
  /* @__PURE__ */ jsxs(
1029
1086
  "button",
1030
1087
  {
@@ -796,10 +796,10 @@ function UILintUI() {
796
796
  const [components, setComponents] = useState(null);
797
797
  useEffect(() => {
798
798
  Promise.all([
799
- import("./UILintToolbar-GMZ6YSI2.js"),
800
- import("./InspectionPanel-4OWY4FVY.js"),
801
- import("./LocatorOverlay-JJDOKNOS.js"),
802
- import("./ElementBadges-HFQNIIO2.js")
799
+ import("./UILintToolbar-AX67REOL.js"),
800
+ import("./InspectionPanel-7K662MF5.js"),
801
+ import("./LocatorOverlay-C4IVDVBW.js"),
802
+ import("./ElementBadges-ZDGDJFQU.js")
803
803
  ]).then(([toolbar, panel, locator, badges]) => {
804
804
  setComponents({
805
805
  Toolbar: toolbar.UILintToolbar,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  useUILintContext
4
- } from "./chunk-5VJ2Q2QW.js";
4
+ } from "./chunk-GJJH5B4Y.js";
5
5
 
6
6
  // src/components/ui-lint/LocatorOverlay.tsx
7
7
  import { useState, useEffect, useMemo } from "react";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  useUILintContext
4
- } from "./chunk-5VJ2Q2QW.js";
4
+ } from "./chunk-GJJH5B4Y.js";
5
5
 
6
6
  // src/components/ui-lint/UILintToolbar.tsx
7
7
  import { useState, useRef, useEffect } from "react";
@@ -176,14 +176,6 @@ function SettingsPopover({
176
176
  children: "UILint Settings"
177
177
  }
178
178
  ),
179
- /* @__PURE__ */ jsx(
180
- SettingToggle,
181
- {
182
- label: "Hide node_modules",
183
- checked: settings.hideNodeModules,
184
- onChange: (checked) => onUpdate({ hideNodeModules: checked })
185
- }
186
- ),
187
179
  /* @__PURE__ */ jsxs(
188
180
  "div",
189
181
  {
@@ -432,57 +424,6 @@ function SettingsPopover({
432
424
  }
433
425
  );
434
426
  }
435
- function SettingToggle({
436
- label,
437
- checked,
438
- onChange
439
- }) {
440
- return /* @__PURE__ */ jsxs(
441
- "label",
442
- {
443
- style: {
444
- display: "flex",
445
- alignItems: "center",
446
- justifyContent: "space-between",
447
- padding: "8px 0",
448
- cursor: "pointer"
449
- },
450
- children: [
451
- /* @__PURE__ */ jsx("span", { style: { fontSize: "12px", color: STYLES.textMuted }, children: label }),
452
- /* @__PURE__ */ jsx(
453
- "div",
454
- {
455
- onClick: () => onChange(!checked),
456
- style: {
457
- width: "36px",
458
- height: "20px",
459
- borderRadius: "10px",
460
- backgroundColor: checked ? STYLES.accent : "rgba(75, 85, 99, 0.5)",
461
- position: "relative",
462
- transition: "background-color 0.2s"
463
- },
464
- children: /* @__PURE__ */ jsx(
465
- "div",
466
- {
467
- style: {
468
- position: "absolute",
469
- top: "2px",
470
- left: checked ? "18px" : "2px",
471
- width: "16px",
472
- height: "16px",
473
- borderRadius: "50%",
474
- backgroundColor: "#FFFFFF",
475
- transition: "left 0.2s",
476
- boxShadow: "0 1px 3px rgba(0, 0, 0, 0.2)"
477
- }
478
- }
479
- )
480
- }
481
- )
482
- ]
483
- }
484
- );
485
- }
486
427
  function UILintIcon({ active }) {
487
428
  return /* @__PURE__ */ jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", children: [
488
429
  /* @__PURE__ */ jsx(
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  UILintToolbar
4
- } from "./chunk-7X5HN55P.js";
4
+ } from "./chunk-KBEQSZMW.js";
5
5
  import {
6
6
  InspectionPanel,
7
7
  clearSourceCache,
@@ -9,10 +9,10 @@ import {
9
9
  fetchSourceWithContext,
10
10
  getCachedSource,
11
11
  prefetchSources
12
- } from "./chunk-QYRESGFG.js";
12
+ } from "./chunk-5CWWYEQM.js";
13
13
  import {
14
14
  LocatorOverlay
15
- } from "./chunk-XLIDEQXH.js";
15
+ } from "./chunk-IMPRRMMY.js";
16
16
  import {
17
17
  DATA_UILINT_ID,
18
18
  DEFAULT_SETTINGS,
@@ -31,7 +31,7 @@ import {
31
31
  scanDOMForSources,
32
32
  updateElementRects,
33
33
  useUILintContext
34
- } from "./chunk-5VJ2Q2QW.js";
34
+ } from "./chunk-GJJH5B4Y.js";
35
35
 
36
36
  // src/consistency/snapshot.ts
37
37
  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.28",
3
+ "version": "0.1.30",
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.28",
37
+ "uilint-core": "^0.1.30",
38
38
  "zustand": "^5.0.5"
39
39
  },
40
40
  "peerDependencies": {