uilint-react 0.1.24 → 0.1.25

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,7 +2,7 @@
2
2
  "use client";
3
3
  import {
4
4
  useUILintContext
5
- } from "./chunk-I4C3NAUH.js";
5
+ } from "./chunk-45MPASAN.js";
6
6
 
7
7
  // src/components/ui-lint/ElementBadges.tsx
8
8
  import React, { useState, useEffect, useCallback, useMemo } from "react";
@@ -19,7 +19,16 @@ var STYLES = {
19
19
  font: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
20
20
  shadow: "0 2px 8px rgba(0, 0, 0, 0.3)"
21
21
  };
22
- var PROXIMITY_THRESHOLD = 100;
22
+ var NEAR_DISTANCE = 0;
23
+ var FAR_DISTANCE = 150;
24
+ var MIN_SCALE = 0.5;
25
+ var MAX_SCALE = 1;
26
+ function getScaleFromDistance(distance) {
27
+ if (distance <= NEAR_DISTANCE) return MAX_SCALE;
28
+ if (distance >= FAR_DISTANCE) return MIN_SCALE;
29
+ const t = (distance - NEAR_DISTANCE) / (FAR_DISTANCE - NEAR_DISTANCE);
30
+ return MAX_SCALE - t * (MAX_SCALE - MIN_SCALE);
31
+ }
23
32
  var CLUSTER_THRESHOLD = 24;
24
33
  function getBadgeColor(issueCount) {
25
34
  if (issueCount === 0) return STYLES.success;
@@ -29,7 +38,7 @@ function getBadgeColor(issueCount) {
29
38
  function ElementBadge({
30
39
  element,
31
40
  issue,
32
- isNearCursor,
41
+ distance,
33
42
  onSelect
34
43
  }) {
35
44
  const [rect, setRect] = useState(null);
@@ -64,16 +73,17 @@ function ElementBadge({
64
73
  if (!rect) return null;
65
74
  if (rect.top < -50 || rect.top > window.innerHeight + 50) return null;
66
75
  if (rect.left < -50 || rect.left > window.innerWidth + 50) return null;
76
+ const scale = isHovered ? 1.1 : getScaleFromDistance(distance);
67
77
  const badgeStyle = {
68
78
  position: "fixed",
69
79
  top: rect.top - 8,
70
80
  left: rect.right - 8,
71
81
  zIndex: isHovered ? 99999 : 99995,
72
82
  cursor: "pointer",
73
- transition: "transform 0.15s ease-out",
74
- transform: isHovered ? "scale(1.1)" : "scale(1)"
83
+ transition: "transform 0.1s ease-out",
84
+ transform: `scale(${scale})`,
85
+ transformOrigin: "center center"
75
86
  };
76
- const badgeColor = issue.status === "complete" ? getBadgeColor(issue.issues.length) : issue.status === "error" ? STYLES.error : issue.status === "scanning" ? STYLES.highlight : "rgba(156, 163, 175, 0.5)";
77
87
  return /* @__PURE__ */ jsxs(Fragment, { children: [
78
88
  isHovered && /* @__PURE__ */ jsx(
79
89
  "div",
@@ -93,7 +103,7 @@ function ElementBadge({
93
103
  "data-ui-lint": true
94
104
  }
95
105
  ),
96
- /* @__PURE__ */ jsx(
106
+ /* @__PURE__ */ jsxs(
97
107
  "div",
98
108
  {
99
109
  style: badgeStyle,
@@ -101,32 +111,16 @@ function ElementBadge({
101
111
  onMouseEnter: () => setIsHovered(true),
102
112
  onMouseLeave: () => setIsHovered(false),
103
113
  onClick: handleClick,
104
- children: isNearCursor || isHovered ? /* @__PURE__ */ jsxs(Fragment, { children: [
114
+ children: [
105
115
  issue.status === "scanning" && /* @__PURE__ */ jsx(ScanningBadge, {}),
106
116
  issue.status === "complete" && /* @__PURE__ */ jsx(IssueBadge, { count: issue.issues.length }),
107
117
  issue.status === "error" && /* @__PURE__ */ jsx(ErrorBadge, {}),
108
118
  issue.status === "pending" && /* @__PURE__ */ jsx(PendingBadge, {})
109
- ] }) : /* @__PURE__ */ jsx(MinimizedBadge, { color: badgeColor })
119
+ ]
110
120
  }
111
121
  )
112
122
  ] });
113
123
  }
114
- function MinimizedBadge({ color }) {
115
- return /* @__PURE__ */ jsx(
116
- "div",
117
- {
118
- style: {
119
- width: "6px",
120
- height: "6px",
121
- borderRadius: "50%",
122
- backgroundColor: color,
123
- opacity: 0.7,
124
- transition: "all 0.15s ease-out",
125
- boxShadow: STYLES.shadow
126
- }
127
- }
128
- );
129
- }
130
124
  function IssueBadge({ count }) {
131
125
  const color = getBadgeColor(count);
132
126
  if (count === 0) {
@@ -357,13 +351,12 @@ function ElementBadges() {
357
351
  if (cluster.badges.length === 1) {
358
352
  const { element, issue, x, y } = cluster.badges[0];
359
353
  const distance = Math.hypot(x - cursorPos.x, y - cursorPos.y);
360
- const isNearCursor = distance <= PROXIMITY_THRESHOLD;
361
354
  return /* @__PURE__ */ jsx(
362
355
  ElementBadge,
363
356
  {
364
357
  element,
365
358
  issue,
366
- isNearCursor,
359
+ distance,
367
360
  onSelect: handleSelect
368
361
  },
369
362
  element.id
@@ -373,12 +366,11 @@ function ElementBadges() {
373
366
  cluster.centroidX - cursorPos.x,
374
367
  cluster.centroidY - cursorPos.y
375
368
  );
376
- const isNearCursor = distance <= PROXIMITY_THRESHOLD;
377
369
  return /* @__PURE__ */ jsx(
378
370
  ClusteredBadge,
379
371
  {
380
372
  cluster,
381
- isNearCursor,
373
+ distance,
382
374
  onSelect: handleSelect
383
375
  },
384
376
  cluster.id
@@ -387,11 +379,7 @@ function ElementBadges() {
387
379
  }) });
388
380
  return createPortal(content, document.body);
389
381
  }
390
- function ClusteredBadge({
391
- cluster,
392
- isNearCursor,
393
- onSelect
394
- }) {
382
+ function ClusteredBadge({ cluster, distance, onSelect }) {
395
383
  const [isExpanded, setIsExpanded] = useState(false);
396
384
  const [hoveredIndex, setHoveredIndex] = useState(null);
397
385
  const closeTimeoutRef = React.useRef(null);
@@ -413,17 +401,6 @@ function ClusteredBadge({
413
401
  }
414
402
  });
415
403
  }, [cluster.badges]);
416
- const worstColor = useMemo(() => {
417
- let worst = 0;
418
- for (const { issue } of cluster.badges) {
419
- if (issue.status === "complete") {
420
- worst = Math.max(worst, issue.issues.length);
421
- } else if (issue.status === "error") {
422
- worst = Math.max(worst, 10);
423
- }
424
- }
425
- return getBadgeColor(worst);
426
- }, [cluster.badges]);
427
404
  const handleMouseEnter = useCallback(() => {
428
405
  if (closeTimeoutRef.current) {
429
406
  clearTimeout(closeTimeoutRef.current);
@@ -484,12 +461,15 @@ function ClusteredBadge({
484
461
  top: cluster.centroidY - 9,
485
462
  left: cluster.centroidX - 9,
486
463
  zIndex: isExpanded ? 99999 : 99995,
487
- cursor: "pointer"
464
+ cursor: "pointer",
465
+ transition: "transform 0.1s ease-out",
466
+ transform: `scale(${isExpanded ? 1.1 : getScaleFromDistance(distance)})`,
467
+ transformOrigin: "center center"
488
468
  },
489
469
  "data-ui-lint": true,
490
470
  onMouseEnter: handleMouseEnter,
491
471
  onMouseLeave: handleMouseLeave,
492
- children: isNearCursor || isExpanded ? /* @__PURE__ */ jsx(
472
+ children: /* @__PURE__ */ jsx(
493
473
  "div",
494
474
  {
495
475
  style: {
@@ -500,9 +480,7 @@ function ClusteredBadge({
500
480
  backgroundColor: STYLES.bg,
501
481
  boxShadow: STYLES.shadow,
502
482
  border: `1px solid ${STYLES.border}`,
503
- overflow: "hidden",
504
- transition: "transform 0.15s ease-out",
505
- transform: isExpanded ? "scale(1.1)" : "scale(1)"
483
+ overflow: "hidden"
506
484
  },
507
485
  children: badgeSegments.map((segment, index) => /* @__PURE__ */ jsxs(
508
486
  "div",
@@ -560,7 +538,7 @@ function ClusteredBadge({
560
538
  index
561
539
  ))
562
540
  }
563
- ) : /* @__PURE__ */ jsx(MinimizedBadge, { color: worstColor })
541
+ )
564
542
  }
565
543
  ),
566
544
  isExpanded && /* @__PURE__ */ jsx(
@@ -2,8 +2,8 @@
2
2
  "use client";
3
3
  import {
4
4
  InspectionPanel
5
- } from "./chunk-FWYNI6JG.js";
6
- import "./chunk-I4C3NAUH.js";
5
+ } from "./chunk-SIVHTQ2P.js";
6
+ import "./chunk-45MPASAN.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-EB6K2KGR.js";
7
- import "./chunk-I4C3NAUH.js";
6
+ } from "./chunk-RA27RIJ2.js";
7
+ import "./chunk-45MPASAN.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-GFURSJEQ.js";
6
- import "./chunk-I4C3NAUH.js";
5
+ } from "./chunk-ORYG2TNM.js";
6
+ import "./chunk-45MPASAN.js";
7
7
  export {
8
8
  UILintToolbar
9
9
  };
@@ -1,44 +1,5 @@
1
1
  "use client";
2
2
 
3
- // src/components/ui-lint/types.ts
4
- var FILE_COLORS = [
5
- "#3B82F6",
6
- // blue
7
- "#8B5CF6",
8
- // violet
9
- "#EC4899",
10
- // pink
11
- "#10B981",
12
- // emerald
13
- "#F59E0B",
14
- // amber
15
- "#06B6D4",
16
- // cyan
17
- "#EF4444",
18
- // red
19
- "#84CC16",
20
- // lime
21
- "#6366F1",
22
- // indigo
23
- "#F97316",
24
- // orange
25
- "#14B8A6",
26
- // teal
27
- "#A855F7"
28
- // purple
29
- ];
30
- var DEFAULT_SETTINGS = {
31
- hideNodeModules: true,
32
- autoScanEnabled: false
33
- };
34
- var DEFAULT_AUTO_SCAN_STATE = {
35
- status: "idle",
36
- currentIndex: 0,
37
- totalElements: 0,
38
- elements: []
39
- };
40
- var DATA_UILINT_ID = "data-ui-lint-id";
41
-
42
3
  // src/components/ui-lint/fiber-utils.ts
43
4
  var DATA_ATTR = "data-ui-lint-id";
44
5
  var COLORS = [
@@ -66,6 +27,16 @@ var SKIP_TAGS = /* @__PURE__ */ new Set([
66
27
  "LINK"
67
28
  ]);
68
29
  var elementCounter = 0;
30
+ function generateStableId(element, source) {
31
+ const dataLoc = element.getAttribute("data-loc");
32
+ if (dataLoc) {
33
+ return `loc:${dataLoc}`;
34
+ }
35
+ if (source) {
36
+ return `src:${source.fileName}:${source.lineNumber}:${source.columnNumber ?? 0}`;
37
+ }
38
+ return `uilint-${++elementCounter}`;
39
+ }
69
40
  function getFiberFromElement(element) {
70
41
  const keys = Object.keys(element);
71
42
  const fiberKey = keys.find((k) => k.startsWith("__reactFiber$"));
@@ -145,9 +116,6 @@ function shouldSkipElement(element) {
145
116
  if (rect.width === 0 || rect.height === 0) return true;
146
117
  return false;
147
118
  }
148
- function generateElementId() {
149
- return `uilint-${++elementCounter}`;
150
- }
151
119
  function scanDOMForSources(root = document.body, hideNodeModules = true) {
152
120
  const elements = [];
153
121
  elementCounter = 0;
@@ -186,7 +154,7 @@ function scanDOMForSources(root = document.body, hideNodeModules = true) {
186
154
  }
187
155
  }
188
156
  if (source) {
189
- const id = generateElementId();
157
+ const id = generateStableId(node, source);
190
158
  node.setAttribute(DATA_ATTR, id);
191
159
  const scannedElement = {
192
160
  id,
@@ -254,6 +222,45 @@ function buildEditorUrl(source, editor = "cursor") {
254
222
  )}:${lineNumber}:${column}`;
255
223
  }
256
224
 
225
+ // src/components/ui-lint/types.ts
226
+ var FILE_COLORS = [
227
+ "#3B82F6",
228
+ // blue
229
+ "#8B5CF6",
230
+ // violet
231
+ "#EC4899",
232
+ // pink
233
+ "#10B981",
234
+ // emerald
235
+ "#F59E0B",
236
+ // amber
237
+ "#06B6D4",
238
+ // cyan
239
+ "#EF4444",
240
+ // red
241
+ "#84CC16",
242
+ // lime
243
+ "#6366F1",
244
+ // indigo
245
+ "#F97316",
246
+ // orange
247
+ "#14B8A6",
248
+ // teal
249
+ "#A855F7"
250
+ // purple
251
+ ];
252
+ var DEFAULT_SETTINGS = {
253
+ hideNodeModules: true,
254
+ autoScanEnabled: false
255
+ };
256
+ var DEFAULT_AUTO_SCAN_STATE = {
257
+ status: "idle",
258
+ currentIndex: 0,
259
+ totalElements: 0,
260
+ elements: []
261
+ };
262
+ var DATA_UILINT_ID = "data-ui-lint-id";
263
+
257
264
  // src/components/ui-lint/UILintProvider.tsx
258
265
  import {
259
266
  createContext,
@@ -261,9 +268,210 @@ import {
261
268
  useState,
262
269
  useEffect,
263
270
  useCallback,
264
- useMemo,
265
- useRef
271
+ useMemo
266
272
  } from "react";
273
+
274
+ // src/components/ui-lint/store.ts
275
+ import { create } from "zustand";
276
+ async function scanElementForIssues(element) {
277
+ if (!element.source) {
278
+ return {
279
+ elementId: element.id,
280
+ issues: [],
281
+ status: "complete"
282
+ };
283
+ }
284
+ try {
285
+ const sourceResponse = await fetch(
286
+ `/api/.uilint/source?path=${encodeURIComponent(element.source.fileName)}`
287
+ );
288
+ if (!sourceResponse.ok) {
289
+ return {
290
+ elementId: element.id,
291
+ issues: [],
292
+ status: "error"
293
+ };
294
+ }
295
+ const sourceData = await sourceResponse.json();
296
+ const analyzeResponse = await fetch("/api/.uilint/analyze", {
297
+ method: "POST",
298
+ headers: { "Content-Type": "application/json" },
299
+ body: JSON.stringify({
300
+ sourceCode: sourceData.content,
301
+ filePath: sourceData.relativePath || element.source.fileName,
302
+ componentName: element.componentStack[0]?.name || element.tagName,
303
+ componentLine: element.source.lineNumber
304
+ })
305
+ });
306
+ if (!analyzeResponse.ok) {
307
+ return {
308
+ elementId: element.id,
309
+ issues: [],
310
+ status: "error"
311
+ };
312
+ }
313
+ const result = await analyzeResponse.json();
314
+ return {
315
+ elementId: element.id,
316
+ issues: result.issues || [],
317
+ status: "complete"
318
+ };
319
+ } catch {
320
+ return {
321
+ elementId: element.id,
322
+ issues: [],
323
+ status: "error"
324
+ };
325
+ }
326
+ }
327
+ var useUILintStore = create()((set, get) => ({
328
+ // ============ Settings ============
329
+ settings: DEFAULT_SETTINGS,
330
+ updateSettings: (partial) => set((state) => ({
331
+ settings: { ...state.settings, ...partial }
332
+ })),
333
+ // ============ Locator Mode ============
334
+ altKeyHeld: false,
335
+ setAltKeyHeld: (held) => set({ altKeyHeld: held }),
336
+ locatorTarget: null,
337
+ setLocatorTarget: (target) => set({ locatorTarget: target }),
338
+ locatorStackIndex: 0,
339
+ setLocatorStackIndex: (index) => set({ locatorStackIndex: index }),
340
+ locatorGoUp: () => {
341
+ const { locatorTarget, locatorStackIndex } = get();
342
+ if (!locatorTarget) return;
343
+ const maxIndex = locatorTarget.componentStack.length;
344
+ set({ locatorStackIndex: Math.min(locatorStackIndex + 1, maxIndex) });
345
+ },
346
+ locatorGoDown: () => {
347
+ const { locatorStackIndex } = get();
348
+ set({ locatorStackIndex: Math.max(locatorStackIndex - 1, 0) });
349
+ },
350
+ // ============ Inspection ============
351
+ inspectedElement: null,
352
+ setInspectedElement: (el) => set({ inspectedElement: el }),
353
+ // ============ Auto-Scan ============
354
+ autoScanState: DEFAULT_AUTO_SCAN_STATE,
355
+ elementIssuesCache: /* @__PURE__ */ new Map(),
356
+ scanLock: false,
357
+ scanPaused: false,
358
+ scanAborted: false,
359
+ _setScanState: (partial) => set((state) => ({
360
+ autoScanState: { ...state.autoScanState, ...partial }
361
+ })),
362
+ updateElementIssue: (id, issue) => set((state) => {
363
+ const newCache = new Map(state.elementIssuesCache);
364
+ newCache.set(id, issue);
365
+ return { elementIssuesCache: newCache };
366
+ }),
367
+ startAutoScan: async (hideNodeModules) => {
368
+ const state = get();
369
+ if (state.scanLock) {
370
+ console.warn("UILint: Scan already in progress");
371
+ return;
372
+ }
373
+ set({
374
+ scanLock: true,
375
+ scanPaused: false,
376
+ scanAborted: false
377
+ });
378
+ const elements = scanDOMForSources(document.body, hideNodeModules);
379
+ const initialCache = /* @__PURE__ */ new Map();
380
+ for (const el of elements) {
381
+ initialCache.set(el.id, {
382
+ elementId: el.id,
383
+ issues: [],
384
+ status: "pending"
385
+ });
386
+ }
387
+ set({
388
+ elementIssuesCache: initialCache,
389
+ autoScanState: {
390
+ status: "scanning",
391
+ currentIndex: 0,
392
+ totalElements: elements.length,
393
+ elements
394
+ }
395
+ });
396
+ await get()._runScanLoop(elements, 0);
397
+ },
398
+ pauseAutoScan: () => {
399
+ set({ scanPaused: true });
400
+ get()._setScanState({ status: "paused" });
401
+ },
402
+ resumeAutoScan: () => {
403
+ const state = get();
404
+ if (state.autoScanState.status !== "paused") return;
405
+ set({ scanPaused: false });
406
+ get()._setScanState({ status: "scanning" });
407
+ get()._runScanLoop(
408
+ state.autoScanState.elements,
409
+ state.autoScanState.currentIndex
410
+ );
411
+ },
412
+ stopAutoScan: () => {
413
+ set({
414
+ scanAborted: true,
415
+ scanPaused: false,
416
+ scanLock: false,
417
+ autoScanState: DEFAULT_AUTO_SCAN_STATE,
418
+ elementIssuesCache: /* @__PURE__ */ new Map()
419
+ });
420
+ },
421
+ _runScanLoop: async (elements, startIndex) => {
422
+ for (let i = startIndex; i < elements.length; i++) {
423
+ if (get().scanAborted) {
424
+ set({
425
+ scanLock: false,
426
+ autoScanState: { ...get().autoScanState, status: "idle" }
427
+ });
428
+ return;
429
+ }
430
+ while (get().scanPaused) {
431
+ await new Promise((resolve) => setTimeout(resolve, 100));
432
+ if (get().scanAborted) {
433
+ set({
434
+ scanLock: false,
435
+ autoScanState: { ...get().autoScanState, status: "idle" }
436
+ });
437
+ return;
438
+ }
439
+ }
440
+ const element = elements[i];
441
+ get()._setScanState({ currentIndex: i });
442
+ get().updateElementIssue(element.id, {
443
+ elementId: element.id,
444
+ issues: [],
445
+ status: "scanning"
446
+ });
447
+ await new Promise((resolve) => requestAnimationFrame(resolve));
448
+ const result = await scanElementForIssues(element);
449
+ get().updateElementIssue(element.id, result);
450
+ await new Promise((resolve) => requestAnimationFrame(resolve));
451
+ }
452
+ set({
453
+ scanLock: false,
454
+ autoScanState: {
455
+ ...get().autoScanState,
456
+ status: "complete",
457
+ currentIndex: elements.length
458
+ }
459
+ });
460
+ }
461
+ }));
462
+ function useEffectiveLocatorTarget() {
463
+ const locatorTarget = useUILintStore((s) => s.locatorTarget);
464
+ const locatorStackIndex = useUILintStore(
465
+ (s) => s.locatorStackIndex
466
+ );
467
+ if (!locatorTarget) return null;
468
+ return {
469
+ ...locatorTarget,
470
+ stackIndex: locatorStackIndex
471
+ };
472
+ }
473
+
474
+ // src/components/ui-lint/UILintProvider.tsx
267
475
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
268
476
  var UILintContext = createContext(null);
269
477
  function useUILintContext() {
@@ -280,201 +488,37 @@ function UILintProvider({
280
488
  children,
281
489
  enabled = true
282
490
  }) {
283
- const [settings, setSettings] = useState(DEFAULT_SETTINGS);
284
491
  const [isMounted, setIsMounted] = useState(false);
285
- const [altKeyHeld, setAltKeyHeld] = useState(false);
286
- const [locatorTarget, setLocatorTarget] = useState(
287
- null
492
+ const settings = useUILintStore((s) => s.settings);
493
+ const updateSettings = useUILintStore((s) => s.updateSettings);
494
+ const altKeyHeld = useUILintStore((s) => s.altKeyHeld);
495
+ const setAltKeyHeld = useUILintStore((s) => s.setAltKeyHeld);
496
+ const setLocatorTarget = useUILintStore(
497
+ (s) => s.setLocatorTarget
288
498
  );
289
- const [locatorStackIndex, setLocatorStackIndex] = useState(0);
290
- const [inspectedElement, setInspectedElement] = useState(null);
291
- const [autoScanState, setAutoScanState] = useState(
292
- DEFAULT_AUTO_SCAN_STATE
499
+ const locatorStackIndex = useUILintStore(
500
+ (s) => s.locatorStackIndex
293
501
  );
294
- const [elementIssuesCache, setElementIssuesCache] = useState(/* @__PURE__ */ new Map());
295
- const scanPausedRef = useRef(false);
296
- const scanAbortRef = useRef(false);
297
- const updateSettings = useCallback((partial) => {
298
- setSettings((prev) => ({ ...prev, ...partial }));
299
- }, []);
300
- const scanElementForIssues = useCallback(
301
- async (element) => {
302
- if (!element.source) {
303
- return {
304
- elementId: element.id,
305
- issues: [],
306
- status: "complete"
307
- };
308
- }
309
- try {
310
- const sourceResponse = await fetch(
311
- `/api/.uilint/source?path=${encodeURIComponent(
312
- element.source.fileName
313
- )}`
314
- );
315
- if (!sourceResponse.ok) {
316
- return {
317
- elementId: element.id,
318
- issues: [],
319
- status: "error"
320
- };
321
- }
322
- const sourceData = await sourceResponse.json();
323
- const analyzeResponse = await fetch("/api/.uilint/analyze", {
324
- method: "POST",
325
- headers: { "Content-Type": "application/json" },
326
- body: JSON.stringify({
327
- sourceCode: sourceData.content,
328
- filePath: sourceData.relativePath || element.source.fileName,
329
- componentName: element.componentStack[0]?.name || element.tagName,
330
- componentLine: element.source.lineNumber
331
- })
332
- });
333
- if (!analyzeResponse.ok) {
334
- return {
335
- elementId: element.id,
336
- issues: [],
337
- status: "error"
338
- };
339
- }
340
- const result = await analyzeResponse.json();
341
- return {
342
- elementId: element.id,
343
- issues: result.issues || [],
344
- status: "complete"
345
- };
346
- } catch {
347
- return {
348
- elementId: element.id,
349
- issues: [],
350
- status: "error"
351
- };
352
- }
353
- },
354
- []
502
+ const setLocatorStackIndex = useUILintStore(
503
+ (s) => s.setLocatorStackIndex
355
504
  );
356
- const runScanLoop = useCallback(
357
- async (elements, startIndex) => {
358
- const fileToElements = /* @__PURE__ */ new Map();
359
- const scannedFiles = /* @__PURE__ */ new Set();
360
- for (const el of elements) {
361
- if (el.source) {
362
- const file = el.source.fileName;
363
- const existing = fileToElements.get(file) || [];
364
- existing.push(el);
365
- fileToElements.set(file, existing);
366
- }
367
- }
368
- for (let i = startIndex; i < elements.length; i++) {
369
- if (scanAbortRef.current) {
370
- setAutoScanState((prev) => ({ ...prev, status: "idle" }));
371
- return;
372
- }
373
- while (scanPausedRef.current) {
374
- await new Promise((resolve) => setTimeout(resolve, 100));
375
- if (scanAbortRef.current) {
376
- setAutoScanState((prev) => ({ ...prev, status: "idle" }));
377
- return;
378
- }
379
- }
380
- const element = elements[i];
381
- setAutoScanState((prev) => ({
382
- ...prev,
383
- currentIndex: i
384
- }));
385
- if (element.source && scannedFiles.has(element.source.fileName)) {
386
- const existingElements = fileToElements.get(element.source.fileName);
387
- if (existingElements && existingElements.length > 0) {
388
- const firstId = existingElements[0].id;
389
- setElementIssuesCache((prev) => {
390
- const cached = prev.get(firstId);
391
- if (cached) {
392
- const newCache = new Map(prev);
393
- newCache.set(element.id, { ...cached, elementId: element.id });
394
- return newCache;
395
- }
396
- return prev;
397
- });
398
- }
399
- continue;
400
- }
401
- setElementIssuesCache((prev) => {
402
- const newCache = new Map(prev);
403
- newCache.set(element.id, {
404
- elementId: element.id,
405
- issues: [],
406
- status: "scanning"
407
- });
408
- return newCache;
409
- });
410
- const result = await scanElementForIssues(element);
411
- setElementIssuesCache((prev) => {
412
- const newCache = new Map(prev);
413
- newCache.set(element.id, result);
414
- return newCache;
415
- });
416
- if (element.source) {
417
- scannedFiles.add(element.source.fileName);
418
- }
419
- await new Promise((resolve) => setTimeout(resolve, 100));
420
- }
421
- setAutoScanState((prev) => ({
422
- ...prev,
423
- status: "complete",
424
- currentIndex: elements.length
425
- }));
426
- },
427
- [scanElementForIssues]
505
+ const locatorGoUp = useUILintStore((s) => s.locatorGoUp);
506
+ const locatorGoDown = useUILintStore((s) => s.locatorGoDown);
507
+ const inspectedElement = useUILintStore(
508
+ (s) => s.inspectedElement
428
509
  );
429
- const startAutoScan = useCallback(() => {
430
- scanPausedRef.current = false;
431
- scanAbortRef.current = false;
432
- const elements = scanDOMForSources(document.body, settings.hideNodeModules);
433
- const initialCache = /* @__PURE__ */ new Map();
434
- for (const el of elements) {
435
- initialCache.set(el.id, {
436
- elementId: el.id,
437
- issues: [],
438
- status: "pending"
439
- });
440
- }
441
- setElementIssuesCache(initialCache);
442
- setAutoScanState({
443
- status: "scanning",
444
- currentIndex: 0,
445
- totalElements: elements.length,
446
- elements
447
- });
448
- runScanLoop(elements, 0);
449
- }, [settings.hideNodeModules, runScanLoop]);
450
- const pauseAutoScan = useCallback(() => {
451
- scanPausedRef.current = true;
452
- setAutoScanState((prev) => ({ ...prev, status: "paused" }));
453
- }, []);
454
- const resumeAutoScan = useCallback(() => {
455
- scanPausedRef.current = false;
456
- setAutoScanState((prev) => {
457
- if (prev.status === "paused") {
458
- runScanLoop(prev.elements, prev.currentIndex);
459
- return { ...prev, status: "scanning" };
460
- }
461
- return prev;
462
- });
463
- }, [runScanLoop]);
464
- const stopAutoScan = useCallback(() => {
465
- scanAbortRef.current = true;
466
- scanPausedRef.current = false;
467
- setAutoScanState(DEFAULT_AUTO_SCAN_STATE);
468
- setElementIssuesCache(/* @__PURE__ */ new Map());
469
- }, []);
470
- const locatorGoUp = useCallback(() => {
471
- if (!locatorTarget) return;
472
- const maxIndex = locatorTarget.componentStack.length;
473
- setLocatorStackIndex((prev) => Math.min(prev + 1, maxIndex));
474
- }, [locatorTarget]);
475
- const locatorGoDown = useCallback(() => {
476
- setLocatorStackIndex((prev) => Math.max(prev - 1, 0));
477
- }, []);
510
+ const setInspectedElement = useUILintStore(
511
+ (s) => s.setInspectedElement
512
+ );
513
+ const autoScanState = useUILintStore((s) => s.autoScanState);
514
+ const elementIssuesCache = useUILintStore(
515
+ (s) => s.elementIssuesCache
516
+ );
517
+ const startAutoScan = useUILintStore((s) => s.startAutoScan);
518
+ const pauseAutoScan = useUILintStore((s) => s.pauseAutoScan);
519
+ const resumeAutoScan = useUILintStore((s) => s.resumeAutoScan);
520
+ const stopAutoScan = useUILintStore((s) => s.stopAutoScan);
521
+ const effectiveLocatorTarget = useEffectiveLocatorTarget();
478
522
  const getLocatorTargetFromElement = useCallback(
479
523
  (element) => {
480
524
  if (element.closest("[data-ui-lint]")) return null;
@@ -530,31 +574,44 @@ function UILintProvider({
530
574
  }
531
575
  setLocatorTarget(null);
532
576
  },
533
- [altKeyHeld, inspectedElement, getLocatorTargetFromElement]
577
+ [
578
+ altKeyHeld,
579
+ inspectedElement,
580
+ getLocatorTargetFromElement,
581
+ setLocatorTarget
582
+ ]
534
583
  );
535
584
  const handleLocatorClick = useCallback(
536
585
  (e) => {
537
- if (!altKeyHeld || !locatorTarget) return;
586
+ if (!altKeyHeld || !effectiveLocatorTarget) return;
538
587
  e.preventDefault();
539
588
  e.stopPropagation();
540
- let source = locatorTarget.source;
541
- if (locatorStackIndex > 0 && locatorTarget.componentStack.length > 0) {
542
- const stackItem = locatorTarget.componentStack[locatorStackIndex - 1];
589
+ let source = effectiveLocatorTarget.source;
590
+ if (locatorStackIndex > 0 && effectiveLocatorTarget.componentStack.length > 0) {
591
+ const stackItem = effectiveLocatorTarget.componentStack[locatorStackIndex - 1];
543
592
  if (stackItem?.source) {
544
593
  source = stackItem.source;
545
594
  }
546
595
  }
547
596
  setInspectedElement({
548
- element: locatorTarget.element,
597
+ element: effectiveLocatorTarget.element,
549
598
  source,
550
- componentStack: locatorTarget.componentStack,
551
- rect: locatorTarget.rect
599
+ componentStack: effectiveLocatorTarget.componentStack,
600
+ rect: effectiveLocatorTarget.rect
552
601
  });
553
602
  setAltKeyHeld(false);
554
603
  setLocatorTarget(null);
555
604
  setLocatorStackIndex(0);
556
605
  },
557
- [altKeyHeld, locatorTarget, locatorStackIndex]
606
+ [
607
+ altKeyHeld,
608
+ effectiveLocatorTarget,
609
+ locatorStackIndex,
610
+ setInspectedElement,
611
+ setAltKeyHeld,
612
+ setLocatorTarget,
613
+ setLocatorStackIndex
614
+ ]
558
615
  );
559
616
  useEffect(() => {
560
617
  if (!isBrowser() || !enabled) return;
@@ -584,7 +641,7 @@ function UILintProvider({
584
641
  window.removeEventListener("keyup", handleKeyUp);
585
642
  window.removeEventListener("blur", handleBlur);
586
643
  };
587
- }, [enabled]);
644
+ }, [enabled, setAltKeyHeld, setLocatorTarget, setLocatorStackIndex]);
588
645
  useEffect(() => {
589
646
  if (!isBrowser() || !enabled) return;
590
647
  if (!altKeyHeld && !inspectedElement) return;
@@ -606,7 +663,7 @@ function UILintProvider({
606
663
  useEffect(() => {
607
664
  if (!isBrowser() || !enabled || !altKeyHeld) return;
608
665
  const handleWheel = (e) => {
609
- if (!locatorTarget) return;
666
+ if (!effectiveLocatorTarget) return;
610
667
  e.preventDefault();
611
668
  if (e.deltaY > 0) {
612
669
  locatorGoUp();
@@ -616,7 +673,7 @@ function UILintProvider({
616
673
  };
617
674
  window.addEventListener("wheel", handleWheel, { passive: false });
618
675
  return () => window.removeEventListener("wheel", handleWheel);
619
- }, [enabled, altKeyHeld, locatorTarget, locatorGoUp, locatorGoDown]);
676
+ }, [enabled, altKeyHeld, effectiveLocatorTarget, locatorGoUp, locatorGoDown]);
620
677
  useEffect(() => {
621
678
  if (!isBrowser() || !enabled) return;
622
679
  const handleKeyDown = (e) => {
@@ -626,17 +683,13 @@ function UILintProvider({
626
683
  };
627
684
  window.addEventListener("keydown", handleKeyDown);
628
685
  return () => window.removeEventListener("keydown", handleKeyDown);
629
- }, [enabled, inspectedElement]);
686
+ }, [enabled, inspectedElement, setInspectedElement]);
630
687
  useEffect(() => {
631
688
  setIsMounted(true);
632
689
  }, []);
633
- const effectiveLocatorTarget = useMemo(() => {
634
- if (!locatorTarget) return null;
635
- return {
636
- ...locatorTarget,
637
- stackIndex: locatorStackIndex
638
- };
639
- }, [locatorTarget, locatorStackIndex]);
690
+ const wrappedStartAutoScan = useCallback(() => {
691
+ startAutoScan(settings.hideNodeModules);
692
+ }, [startAutoScan, settings.hideNodeModules]);
640
693
  const contextValue = useMemo(
641
694
  () => ({
642
695
  settings,
@@ -649,7 +702,7 @@ function UILintProvider({
649
702
  setInspectedElement,
650
703
  autoScanState,
651
704
  elementIssuesCache,
652
- startAutoScan,
705
+ startAutoScan: wrappedStartAutoScan,
653
706
  pauseAutoScan,
654
707
  resumeAutoScan,
655
708
  stopAutoScan
@@ -662,9 +715,10 @@ function UILintProvider({
662
715
  locatorGoUp,
663
716
  locatorGoDown,
664
717
  inspectedElement,
718
+ setInspectedElement,
665
719
  autoScanState,
666
720
  elementIssuesCache,
667
- startAutoScan,
721
+ wrappedStartAutoScan,
668
722
  pauseAutoScan,
669
723
  resumeAutoScan,
670
724
  stopAutoScan
@@ -681,10 +735,10 @@ function UILintUI() {
681
735
  const [components, setComponents] = useState(null);
682
736
  useEffect(() => {
683
737
  Promise.all([
684
- import("./UILintToolbar-5PG6WVW6.js"),
685
- import("./InspectionPanel-47JBBKBL.js"),
686
- import("./LocatorOverlay-ADJUWU2H.js"),
687
- import("./ElementBadges-T4N3VQRI.js")
738
+ import("./UILintToolbar-57XAWTGK.js"),
739
+ import("./InspectionPanel-FRJB6CJ6.js"),
740
+ import("./LocatorOverlay-O4XZCAPC.js"),
741
+ import("./ElementBadges-2WRRHFLI.js")
688
742
  ]).then(([toolbar, panel, locator, badges]) => {
689
743
  setComponents({
690
744
  Toolbar: toolbar.UILintToolbar,
@@ -710,9 +764,6 @@ function UILintUI() {
710
764
  }
711
765
 
712
766
  export {
713
- FILE_COLORS,
714
- DEFAULT_SETTINGS,
715
- DATA_UILINT_ID,
716
767
  getFiberFromElement,
717
768
  getDebugSource,
718
769
  getDebugOwner,
@@ -725,6 +776,9 @@ export {
725
776
  getElementById,
726
777
  updateElementRects,
727
778
  buildEditorUrl,
779
+ FILE_COLORS,
780
+ DEFAULT_SETTINGS,
781
+ DATA_UILINT_ID,
728
782
  useUILintContext,
729
783
  UILintProvider
730
784
  };
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  useUILintContext
4
- } from "./chunk-I4C3NAUH.js";
4
+ } from "./chunk-45MPASAN.js";
5
5
 
6
6
  // src/components/ui-lint/UILintToolbar.tsx
7
7
  import { useState, useRef, useEffect } from "react";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  useUILintContext
4
- } from "./chunk-I4C3NAUH.js";
4
+ } from "./chunk-45MPASAN.js";
5
5
 
6
6
  // src/components/ui-lint/LocatorOverlay.tsx
7
7
  import { useState, useEffect, useMemo } from "react";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  buildEditorUrl,
4
4
  useUILintContext
5
- } from "./chunk-I4C3NAUH.js";
5
+ } from "./chunk-45MPASAN.js";
6
6
 
7
7
  // src/components/ui-lint/InspectionPanel.tsx
8
8
  import { useState, useEffect, useCallback, useMemo } from "react";
package/dist/index.d.ts CHANGED
@@ -159,6 +159,7 @@ declare const DATA_UILINT_ID = "data-ui-lint-id";
159
159
 
160
160
  /**
161
161
  * Hook to access UILint context
162
+ * For backwards compatibility - delegates to Zustand store
162
163
  */
163
164
  declare function useUILintContext(): UILintContextValue;
164
165
  /**
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  UILintToolbar
4
- } from "./chunk-GFURSJEQ.js";
4
+ } from "./chunk-ORYG2TNM.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-FWYNI6JG.js";
12
+ } from "./chunk-SIVHTQ2P.js";
13
13
  import {
14
14
  LocatorOverlay
15
- } from "./chunk-EB6K2KGR.js";
15
+ } from "./chunk-RA27RIJ2.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-I4C3NAUH.js";
34
+ } from "./chunk-45MPASAN.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.24",
3
+ "version": "0.1.25",
4
4
  "description": "React component for AI-powered UI consistency checking",
5
5
  "author": "Peter Suggate",
6
6
  "repository": {
@@ -34,7 +34,8 @@
34
34
  "node": ">=20.0.0"
35
35
  },
36
36
  "dependencies": {
37
- "uilint-core": "^0.1.24"
37
+ "uilint-core": "^0.1.25",
38
+ "zustand": "^5.0.5"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "react": "^19.0.0",