uilint-react 0.1.38 → 0.1.40

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.
@@ -70,6 +70,7 @@ function shouldSkipElement(element) {
70
70
  function scanDOMForSources(root = document.body, hideNodeModules = true) {
71
71
  const elements = [];
72
72
  elementCounter = 0;
73
+ const occurrenceByDataLoc = /* @__PURE__ */ new Map();
73
74
  cleanupDataAttributes();
74
75
  const locElements = root.querySelectorAll("[data-loc]");
75
76
  for (const el of locElements) {
@@ -80,7 +81,9 @@ function scanDOMForSources(root = document.body, hideNodeModules = true) {
80
81
  continue;
81
82
  }
82
83
  const dataLoc = el.getAttribute("data-loc");
83
- const id = `loc:${dataLoc}`;
84
+ const occurrence = (occurrenceByDataLoc.get(dataLoc) ?? 0) + 1;
85
+ occurrenceByDataLoc.set(dataLoc, occurrence);
86
+ const id = `loc:${dataLoc}#${occurrence}`;
84
87
  el.setAttribute(DATA_ATTR, id);
85
88
  elements.push({
86
89
  id,
@@ -130,16 +133,21 @@ function updateElementRects(elements) {
130
133
  rect: el.element.getBoundingClientRect()
131
134
  }));
132
135
  }
133
- function buildEditorUrl(source, editor = "cursor") {
136
+ function buildEditorUrl(source, editor = "cursor", workspaceRoot) {
134
137
  const { fileName, lineNumber, columnNumber } = source;
135
138
  const column = columnNumber ?? 1;
139
+ let absolutePath = fileName;
140
+ if (workspaceRoot && !fileName.startsWith("/")) {
141
+ const root = workspaceRoot.endsWith("/") ? workspaceRoot.slice(0, -1) : workspaceRoot;
142
+ absolutePath = `${root}/${fileName}`;
143
+ }
136
144
  if (editor === "cursor") {
137
145
  return `cursor://file/${encodeURIComponent(
138
- fileName
146
+ absolutePath
139
147
  )}:${lineNumber}:${column}`;
140
148
  }
141
149
  return `vscode://file/${encodeURIComponent(
142
- fileName
150
+ absolutePath
143
151
  )}:${lineNumber}:${column}`;
144
152
  }
145
153
 
@@ -186,7 +194,8 @@ var DATA_UILINT_ID = "data-ui-lint-id";
186
194
  import { create } from "zustand";
187
195
  function getDataLocFromId(id) {
188
196
  if (id.startsWith("loc:")) {
189
- return id.slice(4);
197
+ const raw = id.slice(4);
198
+ return raw.split("#")[0] || null;
190
199
  }
191
200
  return null;
192
201
  }
@@ -211,21 +220,25 @@ async function scanFileForIssues(sourceFile, store) {
211
220
  return { issues };
212
221
  }
213
222
  function distributeIssuesToElements(issues, elements, updateElementIssue, hasError) {
214
- const dataLocToElementId = /* @__PURE__ */ new Map();
223
+ const dataLocToElementIds = /* @__PURE__ */ new Map();
215
224
  for (const el of elements) {
216
225
  const dataLoc = getDataLocFromId(el.id);
217
226
  if (dataLoc) {
218
- dataLocToElementId.set(dataLoc, el.id);
227
+ const existing = dataLocToElementIds.get(dataLoc);
228
+ if (existing) existing.push(el.id);
229
+ else dataLocToElementIds.set(dataLoc, [el.id]);
219
230
  }
220
231
  }
221
232
  const issuesByElement = /* @__PURE__ */ new Map();
222
233
  for (const issue of issues) {
223
234
  if (issue.dataLoc) {
224
- const elementId = dataLocToElementId.get(issue.dataLoc);
225
- if (elementId) {
226
- const existing = issuesByElement.get(elementId) || [];
227
- existing.push(issue);
228
- issuesByElement.set(elementId, existing);
235
+ const elementIds = dataLocToElementIds.get(issue.dataLoc);
236
+ if (elementIds) {
237
+ for (const elementId of elementIds) {
238
+ const existing = issuesByElement.get(elementId) || [];
239
+ existing.push(issue);
240
+ issuesByElement.set(elementId, existing);
241
+ }
229
242
  }
230
243
  }
231
244
  }
@@ -265,12 +278,11 @@ var useUILintStore = create()((set, get) => ({
265
278
  // ============ Inspection ============
266
279
  inspectedElement: null,
267
280
  setInspectedElement: (el) => set({ inspectedElement: el }),
268
- // ============ Auto-Scan ============
281
+ // ============ Live Scanning ============
282
+ liveScanEnabled: false,
269
283
  autoScanState: DEFAULT_AUTO_SCAN_STATE,
270
284
  elementIssuesCache: /* @__PURE__ */ new Map(),
271
285
  scanLock: false,
272
- scanPaused: false,
273
- scanAborted: false,
274
286
  _setScanState: (partial) => set((state) => ({
275
287
  autoScanState: { ...state.autoScanState, ...partial }
276
288
  })),
@@ -279,17 +291,15 @@ var useUILintStore = create()((set, get) => ({
279
291
  newCache.set(id, issue);
280
292
  return { elementIssuesCache: newCache };
281
293
  }),
282
- startAutoScan: async (hideNodeModules) => {
294
+ enableLiveScan: async (hideNodeModules) => {
283
295
  const state = get();
284
296
  if (state.scanLock) {
285
297
  console.warn("UILint: Scan already in progress");
286
298
  return;
287
299
  }
288
300
  set({
289
- scanLock: true,
290
- scanPaused: false,
291
- scanAborted: false,
292
- pendingNewElements: 0
301
+ liveScanEnabled: true,
302
+ scanLock: true
293
303
  });
294
304
  const elements = scanDOMForSources(document.body, hideNodeModules);
295
305
  const initialCache = /* @__PURE__ */ new Map();
@@ -309,60 +319,72 @@ var useUILintStore = create()((set, get) => ({
309
319
  elements
310
320
  }
311
321
  });
312
- await get()._runScanLoop(elements, 0);
313
- },
314
- pauseAutoScan: () => {
315
- set({ scanPaused: true });
316
- get()._setScanState({ status: "paused" });
322
+ await get()._runScanLoop(elements);
317
323
  },
318
- resumeAutoScan: () => {
319
- const state = get();
320
- if (state.autoScanState.status !== "paused") return;
321
- set({ scanPaused: false });
322
- get()._setScanState({ status: "scanning" });
323
- get()._runScanLoop(
324
- state.autoScanState.elements,
325
- state.autoScanState.currentIndex
326
- );
327
- },
328
- stopAutoScan: () => {
324
+ disableLiveScan: () => {
329
325
  set({
330
- scanAborted: true,
331
- scanPaused: false,
326
+ liveScanEnabled: false,
332
327
  scanLock: false,
333
328
  autoScanState: DEFAULT_AUTO_SCAN_STATE,
334
- elementIssuesCache: /* @__PURE__ */ new Map(),
335
- pendingNewElements: 0
329
+ elementIssuesCache: /* @__PURE__ */ new Map()
336
330
  });
337
331
  },
338
- _runScanLoop: async (elements, startIndex) => {
332
+ scanNewElements: async (newElements) => {
333
+ const state = get();
334
+ if (!state.liveScanEnabled) return;
335
+ if (newElements.length === 0) return;
336
+ set((s) => {
337
+ const newCache = new Map(s.elementIssuesCache);
338
+ for (const el of newElements) {
339
+ newCache.set(el.id, {
340
+ elementId: el.id,
341
+ issues: [],
342
+ status: "pending"
343
+ });
344
+ }
345
+ return {
346
+ elementIssuesCache: newCache,
347
+ autoScanState: {
348
+ ...s.autoScanState,
349
+ elements: [...s.autoScanState.elements, ...newElements],
350
+ totalElements: s.autoScanState.totalElements + newElements.length
351
+ }
352
+ };
353
+ });
354
+ const sourceFiles = groupBySourceFile(newElements);
355
+ for (const sourceFile of sourceFiles) {
356
+ for (const el of sourceFile.elements) {
357
+ get().updateElementIssue(el.id, {
358
+ elementId: el.id,
359
+ issues: [],
360
+ status: "scanning"
361
+ });
362
+ }
363
+ await new Promise((resolve) => requestAnimationFrame(resolve));
364
+ const { issues, error } = await scanFileForIssues(sourceFile, get());
365
+ distributeIssuesToElements(
366
+ issues,
367
+ sourceFile.elements,
368
+ get().updateElementIssue,
369
+ error ?? false
370
+ );
371
+ if (get().wsConnected && get().wsConnection) {
372
+ get().subscribeToFile(sourceFile.path);
373
+ }
374
+ await new Promise((resolve) => requestAnimationFrame(resolve));
375
+ }
376
+ },
377
+ _runScanLoop: async (elements) => {
339
378
  const sourceFiles = groupBySourceFile(elements);
340
379
  let processedElements = 0;
341
- let skipElements = startIndex;
342
380
  for (const sourceFile of sourceFiles) {
343
- if (skipElements >= sourceFile.elements.length) {
344
- skipElements -= sourceFile.elements.length;
345
- processedElements += sourceFile.elements.length;
346
- continue;
347
- }
348
- skipElements = 0;
349
- if (get().scanAborted) {
381
+ if (!get().liveScanEnabled) {
350
382
  set({
351
383
  scanLock: false,
352
- autoScanState: { ...get().autoScanState, status: "idle" }
384
+ autoScanState: DEFAULT_AUTO_SCAN_STATE
353
385
  });
354
386
  return;
355
387
  }
356
- while (get().scanPaused) {
357
- await new Promise((resolve) => setTimeout(resolve, 100));
358
- if (get().scanAborted) {
359
- set({
360
- scanLock: false,
361
- autoScanState: { ...get().autoScanState, status: "idle" }
362
- });
363
- return;
364
- }
365
- }
366
388
  get()._setScanState({ currentIndex: processedElements });
367
389
  for (const el of sourceFile.elements) {
368
390
  get().updateElementIssue(el.id, {
@@ -394,10 +416,7 @@ var useUILintStore = create()((set, get) => ({
394
416
  }
395
417
  });
396
418
  },
397
- // ============ Navigation Detection ============
398
- pendingNewElements: 0,
399
- setPendingNewElements: (count) => set({ pendingNewElements: count }),
400
- clearPendingNewElements: () => set({ pendingNewElements: 0 }),
419
+ // ============ DOM Observer ============
401
420
  removeStaleResults: (elementIds) => set((state) => {
402
421
  const newCache = new Map(state.elementIssuesCache);
403
422
  const newElements = state.autoScanState.elements.filter(
@@ -415,6 +434,15 @@ var useUILintStore = create()((set, get) => ({
415
434
  }
416
435
  };
417
436
  }),
437
+ // ============ File/Element Selection ============
438
+ hoveredFilePath: null,
439
+ selectedFilePath: null,
440
+ selectedElementId: null,
441
+ hoveredElementId: null,
442
+ setHoveredFilePath: (path) => set({ hoveredFilePath: path }),
443
+ setSelectedFilePath: (path) => set({ selectedFilePath: path }),
444
+ setSelectedElementId: (id) => set({ selectedElementId: id }),
445
+ setHoveredElementId: (id) => set({ hoveredElementId: id }),
418
446
  // ============ WebSocket ============
419
447
  wsConnection: null,
420
448
  wsConnected: false,
@@ -424,6 +452,9 @@ var useUILintStore = create()((set, get) => ({
424
452
  wsProgressPhase: /* @__PURE__ */ new Map(),
425
453
  wsLastActivity: null,
426
454
  wsRecentResults: [],
455
+ workspaceRoot: null,
456
+ appRoot: null,
457
+ serverCwd: null,
427
458
  connectWebSocket: (url) => {
428
459
  const targetUrl = url || get().wsUrl;
429
460
  const existing = get().wsConnection;
@@ -573,7 +604,7 @@ var useUILintStore = create()((set, get) => ({
573
604
  return { eslintIssuesCache: next };
574
605
  });
575
606
  const state = get();
576
- if (state.autoScanState.status !== "idle") {
607
+ if (state.liveScanEnabled) {
577
608
  const sourceFiles = groupBySourceFile(state.autoScanState.elements);
578
609
  const sf = sourceFiles.find((s) => s.path === filePath);
579
610
  if (sf) {
@@ -633,7 +664,7 @@ var useUILintStore = create()((set, get) => ({
633
664
  return { eslintIssuesCache: next };
634
665
  });
635
666
  const state = get();
636
- if (state.autoScanState.status !== "idle") {
667
+ if (state.liveScanEnabled) {
637
668
  const sourceFiles = groupBySourceFile(state.autoScanState.elements);
638
669
  const sf = sourceFiles.find((s) => s.path === filePath);
639
670
  if (sf) {
@@ -659,6 +690,16 @@ var useUILintStore = create()((set, get) => ({
659
690
  }
660
691
  break;
661
692
  }
693
+ case "workspace:info": {
694
+ const { appRoot, workspaceRoot, serverCwd } = data;
695
+ console.log("[UILint] Received workspace info:", {
696
+ appRoot,
697
+ workspaceRoot,
698
+ serverCwd
699
+ });
700
+ set({ appRoot, workspaceRoot, serverCwd });
701
+ break;
702
+ }
662
703
  }
663
704
  },
664
705
  _reconnectWebSocket: () => {
@@ -685,50 +726,53 @@ function useDOMObserver(enabled = true) {
685
726
  const reconcileTimeoutRef = useRef(
686
727
  null
687
728
  );
688
- const lastScanElementIdsRef = useRef(/* @__PURE__ */ new Set());
689
- const setPendingNewElements = useUILintStore(
690
- (s) => s.setPendingNewElements
691
- );
729
+ const knownElementIdsRef = useRef(/* @__PURE__ */ new Set());
730
+ const liveScanEnabled = useUILintStore((s) => s.liveScanEnabled);
731
+ const settings = useUILintStore((s) => s.settings);
732
+ const autoScanState = useUILintStore((s) => s.autoScanState);
692
733
  const removeStaleResults = useUILintStore(
693
734
  (s) => s.removeStaleResults
694
735
  );
695
- const autoScanState = useUILintStore((s) => s.autoScanState);
736
+ const scanNewElements = useUILintStore((s) => s.scanNewElements);
696
737
  useEffect(() => {
697
- if (autoScanState.status === "complete") {
738
+ if (autoScanState.elements.length > 0) {
698
739
  const ids = new Set(autoScanState.elements.map((el) => el.id));
699
- lastScanElementIdsRef.current = ids;
740
+ knownElementIdsRef.current = ids;
700
741
  }
701
- }, [autoScanState.status, autoScanState.elements]);
742
+ }, [autoScanState.elements]);
702
743
  const reconcileElements = useCallback(() => {
703
- const currentElements = document.querySelectorAll("[data-loc]");
704
- const currentIds = /* @__PURE__ */ new Set();
744
+ if (!liveScanEnabled) return;
745
+ const currentElements = scanDOMForSources(
746
+ document.body,
747
+ settings.hideNodeModules
748
+ );
749
+ const currentIds = new Set(currentElements.map((el) => el.id));
750
+ const knownIds = knownElementIdsRef.current;
751
+ const newElements = [];
705
752
  for (const el of currentElements) {
706
- const dataLoc = el.getAttribute("data-loc");
707
- if (dataLoc) {
708
- currentIds.add(`loc:${dataLoc}`);
709
- }
710
- }
711
- const lastScanIds = lastScanElementIdsRef.current;
712
- if (lastScanIds.size === 0) return;
713
- const newElementIds = [];
714
- for (const id of currentIds) {
715
- if (!lastScanIds.has(id)) {
716
- newElementIds.push(id);
753
+ if (!knownIds.has(el.id)) {
754
+ newElements.push(el);
717
755
  }
718
756
  }
719
757
  const removedElementIds = [];
720
- for (const id of lastScanIds) {
758
+ for (const id of knownIds) {
721
759
  if (!currentIds.has(id)) {
722
760
  removedElementIds.push(id);
723
761
  }
724
762
  }
725
- if (newElementIds.length > 0) {
726
- setPendingNewElements(newElementIds.length);
763
+ knownElementIdsRef.current = currentIds;
764
+ if (newElements.length > 0) {
765
+ scanNewElements(newElements);
727
766
  }
728
767
  if (removedElementIds.length > 0) {
729
768
  removeStaleResults(removedElementIds);
730
769
  }
731
- }, [setPendingNewElements, removeStaleResults]);
770
+ }, [
771
+ liveScanEnabled,
772
+ settings.hideNodeModules,
773
+ scanNewElements,
774
+ removeStaleResults
775
+ ]);
732
776
  const debouncedReconcile = useCallback(() => {
733
777
  if (reconcileTimeoutRef.current) {
734
778
  clearTimeout(reconcileTimeoutRef.current);
@@ -819,14 +863,15 @@ function UILintProvider({
819
863
  const setInspectedElement = useUILintStore(
820
864
  (s) => s.setInspectedElement
821
865
  );
866
+ const liveScanEnabled = useUILintStore((s) => s.liveScanEnabled);
822
867
  const autoScanState = useUILintStore((s) => s.autoScanState);
823
868
  const elementIssuesCache = useUILintStore(
824
869
  (s) => s.elementIssuesCache
825
870
  );
826
- const startAutoScan = useUILintStore((s) => s.startAutoScan);
827
- const pauseAutoScan = useUILintStore((s) => s.pauseAutoScan);
828
- const resumeAutoScan = useUILintStore((s) => s.resumeAutoScan);
829
- const stopAutoScan = useUILintStore((s) => s.stopAutoScan);
871
+ const storeEnableLiveScan = useUILintStore(
872
+ (s) => s.enableLiveScan
873
+ );
874
+ const disableLiveScan = useUILintStore((s) => s.disableLiveScan);
830
875
  const connectWebSocket = useUILintStore(
831
876
  (s) => s.connectWebSocket
832
877
  );
@@ -852,7 +897,7 @@ function UILintProvider({
852
897
  );
853
898
  const handleMouseMove = useCallback2(
854
899
  (e) => {
855
- if (!altKeyHeld && !inspectedElement) return;
900
+ if (!altKeyHeld) return;
856
901
  const elementAtPoint = document.elementFromPoint(e.clientX, e.clientY);
857
902
  if (!elementAtPoint) {
858
903
  setLocatorTarget(null);
@@ -869,16 +914,11 @@ function UILintProvider({
869
914
  }
870
915
  setLocatorTarget(null);
871
916
  },
872
- [
873
- altKeyHeld,
874
- inspectedElement,
875
- getLocatorTargetFromElement,
876
- setLocatorTarget
877
- ]
917
+ [altKeyHeld, getLocatorTargetFromElement, setLocatorTarget]
878
918
  );
879
919
  const handleLocatorClick = useCallback2(
880
920
  (e) => {
881
- if (!altKeyHeld && !inspectedElement || !locatorTarget) return;
921
+ if (!altKeyHeld || !locatorTarget) return;
882
922
  const targetEl = e.target;
883
923
  if (targetEl?.closest?.("[data-ui-lint]")) return;
884
924
  e.preventDefault();
@@ -890,13 +930,7 @@ function UILintProvider({
890
930
  });
891
931
  setLocatorTarget(null);
892
932
  },
893
- [
894
- altKeyHeld,
895
- locatorTarget,
896
- inspectedElement,
897
- setInspectedElement,
898
- setLocatorTarget
899
- ]
933
+ [altKeyHeld, locatorTarget, setInspectedElement, setLocatorTarget]
900
934
  );
901
935
  useEffect2(() => {
902
936
  if (!isBrowser() || !enabled) return;
@@ -926,20 +960,14 @@ function UILintProvider({
926
960
  }, [enabled, setAltKeyHeld, setLocatorTarget]);
927
961
  useEffect2(() => {
928
962
  if (!isBrowser() || !enabled) return;
929
- if (!altKeyHeld && !inspectedElement) return;
963
+ if (!altKeyHeld) return;
930
964
  window.addEventListener("mousemove", handleMouseMove);
931
965
  window.addEventListener("click", handleLocatorClick, true);
932
966
  return () => {
933
967
  window.removeEventListener("mousemove", handleMouseMove);
934
968
  window.removeEventListener("click", handleLocatorClick, true);
935
969
  };
936
- }, [
937
- enabled,
938
- altKeyHeld,
939
- inspectedElement,
940
- handleMouseMove,
941
- handleLocatorClick
942
- ]);
970
+ }, [enabled, altKeyHeld, handleMouseMove, handleLocatorClick]);
943
971
  useEffect2(() => {
944
972
  if (!isBrowser() || !enabled) return;
945
973
  const handleKeyDown = (e) => {
@@ -961,9 +989,9 @@ function UILintProvider({
961
989
  disconnectWebSocket();
962
990
  };
963
991
  }, [enabled, isMounted, connectWebSocket, disconnectWebSocket]);
964
- const wrappedStartAutoScan = useCallback2(() => {
965
- startAutoScan(settings.hideNodeModules);
966
- }, [startAutoScan, settings.hideNodeModules]);
992
+ const enableLiveScan = useCallback2(() => {
993
+ storeEnableLiveScan(settings.hideNodeModules);
994
+ }, [storeEnableLiveScan, settings.hideNodeModules]);
967
995
  const contextValue = useMemo(
968
996
  () => ({
969
997
  settings,
@@ -972,12 +1000,11 @@ function UILintProvider({
972
1000
  locatorTarget,
973
1001
  inspectedElement,
974
1002
  setInspectedElement,
1003
+ liveScanEnabled,
975
1004
  autoScanState,
976
1005
  elementIssuesCache,
977
- startAutoScan: wrappedStartAutoScan,
978
- pauseAutoScan,
979
- resumeAutoScan,
980
- stopAutoScan
1006
+ enableLiveScan,
1007
+ disableLiveScan
981
1008
  }),
982
1009
  [
983
1010
  settings,
@@ -986,12 +1013,11 @@ function UILintProvider({
986
1013
  locatorTarget,
987
1014
  inspectedElement,
988
1015
  setInspectedElement,
1016
+ liveScanEnabled,
989
1017
  autoScanState,
990
1018
  elementIssuesCache,
991
- wrappedStartAutoScan,
992
- pauseAutoScan,
993
- resumeAutoScan,
994
- stopAutoScan
1019
+ enableLiveScan,
1020
+ disableLiveScan
995
1021
  ]
996
1022
  );
997
1023
  const shouldRenderUI = enabled && isMounted;
@@ -1001,14 +1027,14 @@ function UILintProvider({
1001
1027
  ] });
1002
1028
  }
1003
1029
  function UILintUI() {
1004
- const { altKeyHeld, inspectedElement, autoScanState } = useUILintContext();
1030
+ const { altKeyHeld, inspectedElement, liveScanEnabled } = useUILintContext();
1005
1031
  const [components, setComponents] = useState(null);
1006
1032
  useEffect2(() => {
1007
1033
  Promise.all([
1008
- import("./UILintToolbar-OQY2V7Q7.js"),
1009
- import("./InspectionPanel-7N56XBWA.js"),
1010
- import("./LocatorOverlay-RDDLASGB.js"),
1011
- import("./ElementBadges-J2ELN2PU.js")
1034
+ import("./UILintToolbar-MJH7RUZK.js"),
1035
+ import("./InspectionPanel-4LOWGHW7.js"),
1036
+ import("./LocatorOverlay-ERRFPXKK.js"),
1037
+ import("./ElementBadges-2I25HN6W.js")
1012
1038
  ]).then(([toolbar, panel, locator, badges]) => {
1013
1039
  setComponents({
1014
1040
  Toolbar: toolbar.UILintToolbar,
@@ -1021,11 +1047,10 @@ function UILintUI() {
1021
1047
  }, []);
1022
1048
  if (!components) return null;
1023
1049
  const { Toolbar, Panel, LocatorOverlay, InspectedHighlight, ElementBadges } = components;
1024
- const showBadges = autoScanState.status !== "idle";
1025
1050
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1026
1051
  /* @__PURE__ */ jsx(Toolbar, {}),
1027
1052
  (altKeyHeld || inspectedElement) && /* @__PURE__ */ jsx(LocatorOverlay, {}),
1028
- showBadges && /* @__PURE__ */ jsx(ElementBadges, {}),
1053
+ liveScanEnabled && /* @__PURE__ */ jsx(ElementBadges, {}),
1029
1054
  inspectedElement && /* @__PURE__ */ jsxs(Fragment, { children: [
1030
1055
  /* @__PURE__ */ jsx(InspectedHighlight, {}),
1031
1056
  /* @__PURE__ */ jsx(Panel, {})
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  useUILintContext
4
- } from "./chunk-PU6XPNPN.js";
4
+ } from "./chunk-LAL3JTAA.js";
5
5
 
6
6
  // src/components/ui-lint/LocatorOverlay.tsx
7
7
  import { useState, useEffect, useMemo } from "react";
package/dist/index.d.ts CHANGED
@@ -19,7 +19,10 @@ interface SourceLocation {
19
19
  * Source is always present from data-loc attribute
20
20
  */
21
21
  interface ScannedElement {
22
- /** Unique ID from data-loc value (format: "loc:path:line:column") */
22
+ /**
23
+ * Unique per-instance ID derived from data-loc.
24
+ * Format: "loc:path:line:column#occurrence"
25
+ */
23
26
  id: string;
24
27
  element: Element;
25
28
  tagName: string;
@@ -109,18 +112,16 @@ interface UILintContextValue {
109
112
  inspectedElement: InspectedElement | null;
110
113
  /** Set the element to inspect (opens sidebar) */
111
114
  setInspectedElement: (element: InspectedElement | null) => void;
112
- /** Auto-scan state */
115
+ /** Whether live scanning is enabled */
116
+ liveScanEnabled: boolean;
117
+ /** Auto-scan state (for progress tracking) */
113
118
  autoScanState: AutoScanState;
114
- /** Cache of element issues from auto-scan */
119
+ /** Cache of element issues from scanning */
115
120
  elementIssuesCache: Map<string, ElementIssue>;
116
- /** Start auto-scanning all page elements */
117
- startAutoScan: () => void;
118
- /** Pause the auto-scan */
119
- pauseAutoScan: () => void;
120
- /** Resume the auto-scan */
121
- resumeAutoScan: () => void;
122
- /** Stop and reset the auto-scan */
123
- stopAutoScan: () => void;
121
+ /** Enable live scanning */
122
+ enableLiveScan: () => void;
123
+ /** Disable live scanning */
124
+ disableLiveScan: () => void;
124
125
  }
125
126
  /**
126
127
  * Props for the UILintProvider component
@@ -168,27 +169,31 @@ declare function useUILintContext(): UILintContextValue;
168
169
  declare function UILintProvider({ children, enabled, }: UILintProviderProps): react_jsx_runtime.JSX.Element;
169
170
 
170
171
  /**
171
- * UILint Toolbar - Segmented pill with Scan button and settings
172
+ * UILint Toolbar - Improved UX Version
172
173
  *
173
- * Design:
174
- * ┌─────────────────────────────────────┐
175
- * │ [🔍 Scan] │ [...] │
176
- * └─────────────────────────────────────┘
177
- * ⌥+Click to inspect
174
+ * Key improvements:
175
+ * - Clear visual hierarchy: Primary (toggle) → Secondary (issues) → Tertiary (settings)
176
+ * - Contextual hints that only show when relevant
177
+ * - CSS-based hover/focus states (no inline handlers)
178
+ * - Full keyboard navigation with visible focus rings
179
+ * - Smooth animations for all state changes
180
+ * - Better disabled state communication
181
+ * - Expanded panel that doesn't conflict with settings
182
+ * - Touch-friendly targets (min 44px)
183
+ * - ARIA labels and semantic markup
178
184
  */
179
185
 
180
- /**
181
- * Main Toolbar Component - Segmented pill with Scan + Settings
182
- */
183
186
  declare function UILintToolbar(): React$1.ReactPortal | null;
184
187
 
185
188
  /**
186
- * Inspection Panel - Slide-out sidebar showing element details, source preview,
187
- * and LLM analysis with clipboard-ready fix prompts
189
+ * Inspection Panel - Lightweight floating popover showing element issues
190
+ * with inline code previews and "Open in Cursor" actions
191
+ *
192
+ * Positions near the clicked badge, avoiding overlap with the element itself.
188
193
  */
189
194
 
190
195
  /**
191
- * Main Inspection Panel Component
196
+ * Main Inspection Panel Component - Floating Popover
192
197
  */
193
198
  declare function InspectionPanel(): React$1.ReactPortal | null;
194
199
 
@@ -247,8 +252,11 @@ declare function getElementById(id: string): Element | null;
247
252
  declare function updateElementRects(elements: ScannedElement[]): ScannedElement[];
248
253
  /**
249
254
  * Build an "Open in Editor" URL
255
+ * @param source - The source location (fileName may be relative or absolute)
256
+ * @param editor - The editor to open in (cursor or vscode)
257
+ * @param workspaceRoot - Optional workspace root to prepend for relative paths
250
258
  */
251
- declare function buildEditorUrl(source: SourceLocation, editor?: "cursor" | "vscode"): string;
259
+ declare function buildEditorUrl(source: SourceLocation, editor?: "cursor" | "vscode", workspaceRoot?: string | null): string;
252
260
 
253
261
  /**
254
262
  * Client for fetching source code from the dev API
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  UILintToolbar
4
- } from "./chunk-WEBVLQL5.js";
4
+ } from "./chunk-C6NUU5MF.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-GZOQ6QWC.js";
12
+ } from "./chunk-2VRWAMW7.js";
13
13
  import {
14
14
  LocatorOverlay
15
- } from "./chunk-V4273T5B.js";
15
+ } from "./chunk-UD6HPLEZ.js";
16
16
  import {
17
17
  DATA_UILINT_ID,
18
18
  DEFAULT_SETTINGS,
@@ -28,7 +28,7 @@ import {
28
28
  scanDOMForSources,
29
29
  updateElementRects,
30
30
  useUILintContext
31
- } from "./chunk-PU6XPNPN.js";
31
+ } from "./chunk-LAL3JTAA.js";
32
32
 
33
33
  // src/consistency/snapshot.ts
34
34
  var DATA_ELEMENTS_ATTR = "data-elements";