@stinsky/xray 0.2.1 → 0.3.1

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.
package/README.md CHANGED
@@ -1,9 +1,17 @@
1
- # @stinsky/xray
1
+ <p align="center">
2
+ <img src="assets/logo.svg" width="160" alt="xray logo" />
3
+ </p>
2
4
 
3
- Click-to-component for React 19. Hover any element to see its React component name and source file, click to open in your editor. The only click-to-source inspector that works with React 19, Next.js 15+, and Turbopack.
5
+ <h1 align="center">@stinsky/xray</h1>
4
6
 
5
- [![npm version](https://img.shields.io/npm/v/@stinsky/xray)](https://www.npmjs.com/package/@stinsky/xray)
6
- [![license](https://img.shields.io/npm/l/@stinsky/xray)](./LICENSE)
7
+ <p align="center">Click-to-component for React 19. Hover any element to see its React component name and source file, click to open in your editor. The only click-to-source inspector that works with React 19, Next.js 15+, and Turbopack.</p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@stinsky/xray"><img src="https://img.shields.io/npm/v/@stinsky/xray" alt="npm version" /></a>
11
+ <a href="./LICENSE"><img src="https://img.shields.io/npm/l/@stinsky/xray" alt="license" /></a>
12
+ <img src="https://badgen.net/bundlephobia/tree-shaking/@stinsky/xray" alt="tree-shaking" />
13
+ <img src="https://badgen.net/bundlephobia/minzip/@stinsky/xray" alt="minzipped size" />
14
+ </p>
7
15
 
8
16
  ## Features
9
17
 
@@ -12,7 +20,7 @@ Click-to-component for React 19. Hover any element to see its React component na
12
20
  - **Works with React 19** — uses compile-time AST injection, not `fiber._debugSource` (removed in React 19)
13
21
  - **All bundlers** — Next.js (Turbopack & Webpack), Vite, Webpack, Rspack, esbuild
14
22
  - **Zero production cost** — fully tree-shaken, zero bytes in your production bundle
15
- - **Floating toggle button** — auto-positions next to the Next.js dev indicator, or bottom-left in other setups
23
+ - **Floating toggle button** — auto-follows the Next.js dev indicator, or freely draggable with snap-to-corner in other setups
16
24
  - **Keyboard shortcut** — `Cmd+Shift+X` to toggle (customizable)
17
25
  - **Scroll-aware** — rAF-based tracking, works with smooth scrolling libraries (Lenis, etc.)
18
26
  - **Interaction blocking** — all clicks/pointer events blocked while inspecting, no accidental navigation
@@ -133,6 +141,10 @@ All clicks and pointer events are blocked while the inspector is active, so you
133
141
  | `bundler` | `'webpack' \| 'vite' \| 'turbopack' \| 'rspack' \| 'esbuild'` | — | **Required.** Your bundler |
134
142
  | `editor` | `string` | `'code'` | Editor to open files in (`code`, `webstorm`, `idea`, etc.) |
135
143
 
144
+ ## Acknowledgments
145
+
146
+ Built on top of [`code-inspector-plugin`](https://github.com/zh-lx/code-inspector), which handles the compile-time AST injection and editor integration.
147
+
136
148
  ## License
137
149
 
138
150
  MIT
package/dist/index.cjs CHANGED
@@ -26,7 +26,7 @@ __export(src_exports, {
26
26
  module.exports = __toCommonJS(src_exports);
27
27
 
28
28
  // src/xray.tsx
29
- var import_react4 = require("react");
29
+ var import_react5 = require("react");
30
30
  var import_react_dom = require("react-dom");
31
31
 
32
32
  // src/use-badge.ts
@@ -34,124 +34,258 @@ var import_react = require("react");
34
34
  var BADGE_SIZE = 36;
35
35
  var BADGE_GAP = 4;
36
36
  var DRAG_THRESHOLD = 10;
37
- function findNextjsIndicator() {
38
- const portal = document.querySelector("nextjs-portal");
39
- if (!portal?.shadowRoot) return null;
40
- return portal.shadowRoot.querySelector("[data-nextjs-toast]") ?? portal.shadowRoot.querySelector("div");
37
+ var EDGE_MARGIN = 12;
38
+ var TOAST_ATTR = "data-xray-toast";
39
+ var MESSAGES = [
40
+ "here you are",
41
+ "found you",
42
+ "right behind you",
43
+ "missed me?",
44
+ "can't escape me"
45
+ ];
46
+ var TOAST_GAP = 24;
47
+ function showToast(badge) {
48
+ removeToast();
49
+ const msg = MESSAGES[Math.floor(Math.random() * MESSAGES.length)];
50
+ const toast = document.createElement("div");
51
+ toast.setAttribute(TOAST_ATTR, "");
52
+ toast.textContent = msg;
53
+ Object.assign(toast.style, {
54
+ position: "fixed",
55
+ zIndex: "2147483647",
56
+ opacity: "0",
57
+ whiteSpace: "nowrap",
58
+ fontSize: "11px",
59
+ fontFamily: "system-ui, sans-serif",
60
+ fontWeight: "600",
61
+ color: "#fff",
62
+ background: "rgba(0, 0, 0, 0.75)",
63
+ backdropFilter: "blur(12px)",
64
+ padding: "4px 10px",
65
+ borderRadius: "8px",
66
+ pointerEvents: "none",
67
+ transition: "opacity 300ms ease, transform 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)"
68
+ });
69
+ document.body.appendChild(toast);
70
+ const badgeRect = badge.getBoundingClientRect();
71
+ const toastRect = toast.getBoundingClientRect();
72
+ const isTop = badgeRect.top + BADGE_SIZE / 2 < window.innerHeight / 2;
73
+ const top = isTop ? badgeRect.bottom + TOAST_GAP : badgeRect.top - TOAST_GAP - toastRect.height;
74
+ const badgeCenterX = badgeRect.left + badgeRect.width / 2;
75
+ const left = badgeCenterX - toastRect.width / 2;
76
+ const slideFrom = isTop ? -8 : 8;
77
+ toast.style.top = `${top}px`;
78
+ toast.style.left = `${left}px`;
79
+ toast.style.transform = `translateY(${slideFrom}px)`;
80
+ void toast.offsetHeight;
81
+ toast.style.opacity = "1";
82
+ toast.style.transform = "translateY(0)";
83
+ }
84
+ function removeToast(immediate = false) {
85
+ const existing = document.querySelector(`[${TOAST_ATTR}]`);
86
+ if (!existing) return;
87
+ if (immediate) {
88
+ existing.remove();
89
+ return;
90
+ }
91
+ existing.style.opacity = "0";
92
+ existing.addEventListener("transitionend", () => existing.remove(), { once: true });
41
93
  }
42
- function useBadge({ badgeRef, show, followNextIndicator }) {
94
+ var SPRING = "scale 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)";
95
+ function reveal(badge) {
96
+ badge.style.transition = SPRING;
97
+ badge.style.scale = "1";
98
+ const onEnd = () => {
99
+ badge.style.transition = "";
100
+ badge.removeEventListener("transitionend", onEnd);
101
+ };
102
+ badge.addEventListener("transitionend", onEnd);
103
+ }
104
+ function useBadge({ badgeRef, show, anchor, anchorDragging, onTap }) {
105
+ const hasDraggedRef = (0, import_react.useRef)(false);
43
106
  (0, import_react.useEffect)(() => {
44
- if (!show) return;
107
+ hasDraggedRef.current = false;
108
+ }, [anchor]);
109
+ (0, import_react.useEffect)(() => {
110
+ if (!show || !anchor) return;
45
111
  const badge = badgeRef.current;
46
112
  if (!badge) return;
47
113
  badge.style.scale = "0";
114
+ let revealed = false;
48
115
  let rafId;
49
- let dragging = false;
50
- let dragStartX = 0;
51
- let dragStartY = 0;
52
- let hidden = false;
53
- let settling = false;
54
- let positioned = false;
55
- let settled = false;
116
+ let lastKey = "";
117
+ const position = () => {
118
+ const rect = anchor.getBoundingClientRect();
119
+ const midX = rect.left + rect.width / 2;
120
+ const isRight = midX > window.innerWidth / 2;
121
+ badge.style.bottom = "";
122
+ badge.style.left = isRight ? `${rect.left - BADGE_SIZE - BADGE_GAP}px` : `${rect.right + BADGE_GAP}px`;
123
+ badge.style.top = `${rect.top + (rect.height - BADGE_SIZE) / 2}px`;
124
+ };
125
+ const tick = () => {
126
+ const rect = anchor.getBoundingClientRect();
127
+ const key = `${rect.top},${rect.left},${rect.right},${rect.bottom}`;
128
+ if (key !== lastKey) {
129
+ lastKey = key;
130
+ position();
131
+ if (!revealed) {
132
+ revealed = true;
133
+ reveal(badge);
134
+ }
135
+ }
136
+ rafId = requestAnimationFrame(tick);
137
+ };
138
+ rafId = requestAnimationFrame(tick);
139
+ return () => cancelAnimationFrame(rafId);
140
+ }, [badgeRef, show, anchor]);
141
+ (0, import_react.useEffect)(() => {
142
+ if (!show || !anchor) return;
143
+ const badge = badgeRef.current;
144
+ if (!badge) return;
56
145
  let hideTimeout;
57
146
  let reappearTimeout;
58
- const getIndicator = () => followNextIndicator ? findNextjsIndicator() : null;
59
- const positionBadge = () => {
60
- if (!badge) return;
61
- const indicator = getIndicator();
62
- if (indicator) {
63
- const rect = indicator.getBoundingClientRect();
64
- const midX = rect.left + rect.width / 2;
65
- const isRight = midX > window.innerWidth / 2;
66
- badge.style.left = "";
67
- badge.style.bottom = "";
68
- badge.style.left = isRight ? `${rect.left - BADGE_SIZE - BADGE_GAP}px` : `${rect.right + BADGE_GAP}px`;
69
- badge.style.top = `${rect.top + (rect.height - BADGE_SIZE) / 2}px`;
70
- } else {
71
- badge.style.top = "";
72
- badge.style.bottom = "12px";
73
- badge.style.left = "12px";
74
- settled = true;
75
- }
76
- if (!positioned) {
77
- positioned = true;
78
- requestAnimationFrame(() => {
79
- badge.style.scale = "1";
80
- });
81
- }
147
+ let toastTimeout;
148
+ if (anchorDragging) {
149
+ hasDraggedRef.current = true;
150
+ badge.style.transition = "scale 200ms ease";
151
+ badge.style.scale = "0";
152
+ hideTimeout = setTimeout(() => {
153
+ badge.style.display = "none";
154
+ }, 200);
155
+ } else {
156
+ clearTimeout(hideTimeout);
157
+ reappearTimeout = setTimeout(() => {
158
+ badge.style.scale = "0";
159
+ badge.style.display = "";
160
+ badge.style.transition = SPRING;
161
+ void badge.offsetHeight;
162
+ badge.style.scale = "1";
163
+ if (hasDraggedRef.current) {
164
+ showToast(badge);
165
+ toastTimeout = setTimeout(() => removeToast(), 2e3);
166
+ }
167
+ }, 1e3);
168
+ }
169
+ return () => {
170
+ clearTimeout(hideTimeout);
171
+ clearTimeout(reappearTimeout);
172
+ clearTimeout(toastTimeout);
173
+ removeToast();
82
174
  };
175
+ }, [badgeRef, show, anchor, anchorDragging]);
176
+ (0, import_react.useEffect)(() => {
177
+ if (!show || anchor) return;
178
+ const badge = badgeRef.current;
179
+ if (!badge) return;
180
+ badge.style.scale = "0";
181
+ badge.style.top = "";
182
+ badge.style.bottom = `${EDGE_MARGIN}px`;
183
+ badge.style.left = `${EDGE_MARGIN}px`;
184
+ requestAnimationFrame(() => reveal(badge));
185
+ let dragging = false;
186
+ let dragStartX = 0;
187
+ let dragStartY = 0;
188
+ let badgeStartX = 0;
189
+ let badgeStartY = 0;
190
+ let moved = false;
83
191
  const onPointerDown = (e) => {
84
- const indicator = getIndicator();
85
- if (!indicator) return;
86
- const rect = indicator.getBoundingClientRect();
87
- if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) {
88
- dragging = true;
89
- dragStartX = e.clientX;
90
- dragStartY = e.clientY;
91
- }
192
+ if (!badge.contains(e.target)) return;
193
+ dragging = true;
194
+ moved = false;
195
+ dragStartX = e.clientX;
196
+ dragStartY = e.clientY;
197
+ const rect = badge.getBoundingClientRect();
198
+ badgeStartX = rect.left;
199
+ badgeStartY = rect.top;
200
+ badge.setPointerCapture(e.pointerId);
92
201
  };
93
202
  const onPointerMove = (e) => {
94
203
  if (!dragging) return;
95
204
  const dx = e.clientX - dragStartX;
96
205
  const dy = e.clientY - dragStartY;
97
- if (!hidden && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
98
- hidden = true;
99
- settling = true;
100
- badge.style.transition = "scale 200ms ease";
101
- badge.style.scale = "0";
102
- hideTimeout = setTimeout(() => {
103
- badge.style.display = "none";
104
- }, 200);
105
- }
206
+ if (!moved && Math.sqrt(dx * dx + dy * dy) < DRAG_THRESHOLD) return;
207
+ if (!moved) badge.style.cursor = "grabbing";
208
+ moved = true;
209
+ let x = badgeStartX + dx;
210
+ let y = badgeStartY + dy;
211
+ x = Math.max(0, Math.min(x, window.innerWidth - BADGE_SIZE));
212
+ y = Math.max(0, Math.min(y, window.innerHeight - BADGE_SIZE));
213
+ badge.style.bottom = "";
214
+ badge.style.left = `${x}px`;
215
+ badge.style.top = `${y}px`;
106
216
  };
107
- const onPointerUp = () => {
217
+ const snapToCorner = (x, y) => {
218
+ const midX = x + BADGE_SIZE / 2;
219
+ const midY = y + BADGE_SIZE / 2;
220
+ const snapLeft = midX < window.innerWidth / 2;
221
+ const snapTop = midY < window.innerHeight / 2;
222
+ badge.style.transition = "left 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92), top 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)";
223
+ badge.style.left = snapLeft ? `${EDGE_MARGIN}px` : `${window.innerWidth - BADGE_SIZE - EDGE_MARGIN}px`;
224
+ badge.style.top = snapTop ? `${EDGE_MARGIN}px` : `${window.innerHeight - BADGE_SIZE - EDGE_MARGIN}px`;
225
+ const onEnd = () => {
226
+ badge.style.transition = "";
227
+ badge.removeEventListener("transitionend", onEnd);
228
+ };
229
+ badge.addEventListener("transitionend", onEnd);
230
+ };
231
+ const onPointerUp = (e) => {
108
232
  if (!dragging) return;
109
233
  dragging = false;
110
- if (hidden) {
111
- reappearTimeout = setTimeout(() => {
112
- hidden = false;
113
- settling = false;
114
- settled = false;
115
- badge.style.scale = "0";
116
- badge.style.display = "";
117
- badge.style.transition = "scale 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)";
118
- void badge.offsetHeight;
119
- badge.style.scale = "1";
120
- }, 1e3);
234
+ badge.style.cursor = "";
235
+ badge.releasePointerCapture(e.pointerId);
236
+ if (moved) {
237
+ const rect = badge.getBoundingClientRect();
238
+ snapToCorner(rect.left, rect.top);
121
239
  }
122
240
  };
123
- let lastKey = "";
124
- const tick = () => {
125
- if (settled) return;
126
- if (!hidden && !settling) {
127
- const indicator = getIndicator();
128
- if (indicator) {
129
- const rect = indicator.getBoundingClientRect();
130
- const key = `${rect.top},${rect.left},${rect.right},${rect.bottom}`;
131
- if (key !== lastKey) {
132
- lastKey = key;
133
- positionBadge();
134
- }
135
- } else if (lastKey !== "fallback") {
136
- lastKey = "fallback";
137
- positionBadge();
138
- }
241
+ badge.addEventListener("pointerdown", onPointerDown);
242
+ window.addEventListener("pointermove", onPointerMove, true);
243
+ window.addEventListener("pointerup", onPointerUp, true);
244
+ return () => {
245
+ badge.removeEventListener("pointerdown", onPointerDown);
246
+ window.removeEventListener("pointermove", onPointerMove, true);
247
+ window.removeEventListener("pointerup", onPointerUp, true);
248
+ };
249
+ }, [badgeRef, show, anchor]);
250
+ const onTapRef = (0, import_react.useRef)(onTap);
251
+ onTapRef.current = onTap;
252
+ (0, import_react.useEffect)(() => {
253
+ if (!show) return;
254
+ const badge = badgeRef.current;
255
+ if (!badge) return;
256
+ let down = false;
257
+ let startX = 0;
258
+ let startY = 0;
259
+ let moved = false;
260
+ const onPointerDown = (e) => {
261
+ if (!badge.contains(e.target)) return;
262
+ down = true;
263
+ moved = false;
264
+ startX = e.clientX;
265
+ startY = e.clientY;
266
+ };
267
+ const onPointerMove = (e) => {
268
+ if (!down || moved) return;
269
+ const dx = e.clientX - startX;
270
+ const dy = e.clientY - startY;
271
+ if (dx * dx + dy * dy > DRAG_THRESHOLD * DRAG_THRESHOLD) {
272
+ moved = true;
139
273
  }
140
- rafId = requestAnimationFrame(tick);
141
274
  };
142
- rafId = requestAnimationFrame(tick);
143
- window.addEventListener("pointerdown", onPointerDown, true);
275
+ const onPointerUp = () => {
276
+ if (!down) return;
277
+ down = false;
278
+ if (!moved) onTapRef.current?.();
279
+ };
280
+ badge.addEventListener("pointerdown", onPointerDown);
144
281
  window.addEventListener("pointermove", onPointerMove, true);
145
282
  window.addEventListener("pointerup", onPointerUp, true);
146
283
  return () => {
147
- cancelAnimationFrame(rafId);
148
- clearTimeout(hideTimeout);
149
- clearTimeout(reappearTimeout);
150
- window.removeEventListener("pointerdown", onPointerDown, true);
284
+ badge.removeEventListener("pointerdown", onPointerDown);
151
285
  window.removeEventListener("pointermove", onPointerMove, true);
152
286
  window.removeEventListener("pointerup", onPointerUp, true);
153
287
  };
154
- }, [badgeRef, show, followNextIndicator]);
288
+ }, [badgeRef, show]);
155
289
  }
156
290
 
157
291
  // src/use-hotkey.ts
@@ -194,8 +328,8 @@ function getComponentInfo(el) {
194
328
  function parseInspPath(attr) {
195
329
  const parts = attr.split(":");
196
330
  parts.pop();
197
- const column = parts.pop();
198
- const line = parts.pop();
331
+ const column = parts.pop() ?? "";
332
+ const line = parts.pop() ?? "";
199
333
  const filePath = parts.join(":");
200
334
  return { filePath, line, column };
201
335
  }
@@ -354,6 +488,119 @@ function useInspector({
354
488
  }, [enabled, overlayRef, tooltipRef, ignoreRefs]);
355
489
  }
356
490
 
491
+ // src/use-next-indicator.ts
492
+ var import_react4 = require("react");
493
+ var DRAG_THRESHOLD2 = 10;
494
+ function findIndicator() {
495
+ for (const portal of document.querySelectorAll("nextjs-portal")) {
496
+ if (!portal.shadowRoot) continue;
497
+ const toast = portal.shadowRoot.querySelector("[data-nextjs-toast]");
498
+ if (toast) return toast;
499
+ }
500
+ return null;
501
+ }
502
+ var DISCOVERY_TIMEOUT = 5e3;
503
+ function useNextIndicator(enabled) {
504
+ const [element, setElement] = (0, import_react4.useState)(null);
505
+ const [isDragging, setDragging] = (0, import_react4.useState)(false);
506
+ const [searching, setSearching] = (0, import_react4.useState)(enabled);
507
+ const elementRef = (0, import_react4.useRef)(null);
508
+ (0, import_react4.useEffect)(() => {
509
+ if (!enabled) {
510
+ elementRef.current = null;
511
+ setElement(null);
512
+ setSearching(false);
513
+ return;
514
+ }
515
+ const existing = findIndicator();
516
+ if (existing) {
517
+ elementRef.current = existing;
518
+ setElement(existing);
519
+ return;
520
+ }
521
+ const hasPortals = document.querySelectorAll("nextjs-portal").length > 0;
522
+ if (!hasPortals) {
523
+ setSearching(false);
524
+ return;
525
+ }
526
+ setSearching(true);
527
+ const observers = [];
528
+ const found = (el) => {
529
+ elementRef.current = el;
530
+ setElement(el);
531
+ setSearching(false);
532
+ observers.forEach((o) => o.disconnect());
533
+ clearTimeout(timeout);
534
+ };
535
+ const timeout = setTimeout(() => {
536
+ setSearching(false);
537
+ observers.forEach((o) => o.disconnect());
538
+ }, DISCOVERY_TIMEOUT);
539
+ const observeShadowRoots = () => {
540
+ for (const portal of document.querySelectorAll("nextjs-portal")) {
541
+ if (!portal.shadowRoot) continue;
542
+ const shadowObserver = new MutationObserver(() => {
543
+ const indicator = findIndicator();
544
+ if (indicator) found(indicator);
545
+ });
546
+ shadowObserver.observe(portal.shadowRoot, { childList: true, subtree: true });
547
+ observers.push(shadowObserver);
548
+ }
549
+ };
550
+ observeShadowRoots();
551
+ const bodyObserver = new MutationObserver(() => {
552
+ const indicator = findIndicator();
553
+ if (indicator) {
554
+ found(indicator);
555
+ } else {
556
+ observeShadowRoots();
557
+ }
558
+ });
559
+ bodyObserver.observe(document.body, { childList: true, subtree: true });
560
+ observers.push(bodyObserver);
561
+ return () => {
562
+ clearTimeout(timeout);
563
+ observers.forEach((o) => o.disconnect());
564
+ };
565
+ }, [enabled]);
566
+ (0, import_react4.useEffect)(() => {
567
+ if (!element) return;
568
+ let dragging = false;
569
+ let startX = 0;
570
+ let startY = 0;
571
+ const onPointerDown = (e) => {
572
+ const rect = element.getBoundingClientRect();
573
+ if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) {
574
+ dragging = true;
575
+ startX = e.clientX;
576
+ startY = e.clientY;
577
+ }
578
+ };
579
+ const onPointerMove = (e) => {
580
+ if (!dragging) return;
581
+ const dx = e.clientX - startX;
582
+ const dy = e.clientY - startY;
583
+ if (dx * dx + dy * dy > DRAG_THRESHOLD2 * DRAG_THRESHOLD2) {
584
+ setDragging(true);
585
+ }
586
+ };
587
+ const onPointerUp = () => {
588
+ if (!dragging) return;
589
+ dragging = false;
590
+ setDragging(false);
591
+ };
592
+ window.addEventListener("pointerdown", onPointerDown, true);
593
+ window.addEventListener("pointermove", onPointerMove, true);
594
+ window.addEventListener("pointerup", onPointerUp, true);
595
+ return () => {
596
+ window.removeEventListener("pointerdown", onPointerDown, true);
597
+ window.removeEventListener("pointermove", onPointerMove, true);
598
+ window.removeEventListener("pointerup", onPointerUp, true);
599
+ };
600
+ }, [element]);
601
+ return { element, isDragging, searching };
602
+ }
603
+
357
604
  // src/xray.tsx
358
605
  var import_jsx_runtime = require("react/jsx-runtime");
359
606
  var DEFAULT_PORT = 5678;
@@ -371,13 +618,16 @@ function XrayImpl({
371
618
  showButton = true,
372
619
  followNextIndicator = true
373
620
  } = {}) {
374
- const [enabled, setEnabled] = (0, import_react4.useState)(false);
375
- const overlayRef = (0, import_react4.useRef)(null);
376
- const tooltipRef = (0, import_react4.useRef)(null);
377
- const badgeRef = (0, import_react4.useRef)(null);
378
- const toggle = (0, import_react4.useCallback)(() => setEnabled((prev) => !prev), []);
621
+ const [mounted, setMounted] = (0, import_react5.useState)(false);
622
+ const [enabled, setEnabled] = (0, import_react5.useState)(false);
623
+ const overlayRef = (0, import_react5.useRef)(null);
624
+ const tooltipRef = (0, import_react5.useRef)(null);
625
+ const badgeRef = (0, import_react5.useRef)(null);
626
+ const toggle = (0, import_react5.useCallback)(() => setEnabled((prev) => !prev), []);
627
+ (0, import_react5.useEffect)(() => setMounted(true), []);
628
+ const { element: anchor, isDragging, searching } = useNextIndicator(followNextIndicator);
379
629
  useHotkey(hotKey, toggle);
380
- useBadge({ badgeRef, show: showButton, followNextIndicator });
630
+ useBadge({ badgeRef, show: showButton && !searching, anchor, anchorDragging: isDragging, onTap: toggle });
381
631
  useInspector({
382
632
  enabled,
383
633
  port,
@@ -385,6 +635,7 @@ function XrayImpl({
385
635
  tooltipRef,
386
636
  ignoreRefs: [badgeRef]
387
637
  });
638
+ if (!mounted) return null;
388
639
  return (0, import_react_dom.createPortal)(
389
640
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
390
641
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -433,15 +684,12 @@ function XrayImpl({
433
684
  zIndex: 2147483646,
434
685
  width: "36px",
435
686
  height: "36px",
436
- transformOrigin: "center center"
687
+ transformOrigin: "center center",
688
+ scale: "0"
437
689
  },
438
690
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
439
691
  "button",
440
692
  {
441
- onClick: (e) => {
442
- e.stopPropagation();
443
- toggle();
444
- },
445
693
  style: {
446
694
  width: "36px",
447
695
  height: "36px",
package/dist/index.mjs CHANGED
@@ -1,132 +1,266 @@
1
1
  "use client";
2
2
 
3
3
  // src/xray.tsx
4
- import { useCallback, useRef as useRef2, useState } from "react";
4
+ import { useCallback, useEffect as useEffect5, useRef as useRef4, useState as useState2 } from "react";
5
5
  import { createPortal } from "react-dom";
6
6
 
7
7
  // src/use-badge.ts
8
- import { useEffect } from "react";
8
+ import { useEffect, useRef } from "react";
9
9
  var BADGE_SIZE = 36;
10
10
  var BADGE_GAP = 4;
11
11
  var DRAG_THRESHOLD = 10;
12
- function findNextjsIndicator() {
13
- const portal = document.querySelector("nextjs-portal");
14
- if (!portal?.shadowRoot) return null;
15
- return portal.shadowRoot.querySelector("[data-nextjs-toast]") ?? portal.shadowRoot.querySelector("div");
12
+ var EDGE_MARGIN = 12;
13
+ var TOAST_ATTR = "data-xray-toast";
14
+ var MESSAGES = [
15
+ "here you are",
16
+ "found you",
17
+ "right behind you",
18
+ "missed me?",
19
+ "can't escape me"
20
+ ];
21
+ var TOAST_GAP = 24;
22
+ function showToast(badge) {
23
+ removeToast();
24
+ const msg = MESSAGES[Math.floor(Math.random() * MESSAGES.length)];
25
+ const toast = document.createElement("div");
26
+ toast.setAttribute(TOAST_ATTR, "");
27
+ toast.textContent = msg;
28
+ Object.assign(toast.style, {
29
+ position: "fixed",
30
+ zIndex: "2147483647",
31
+ opacity: "0",
32
+ whiteSpace: "nowrap",
33
+ fontSize: "11px",
34
+ fontFamily: "system-ui, sans-serif",
35
+ fontWeight: "600",
36
+ color: "#fff",
37
+ background: "rgba(0, 0, 0, 0.75)",
38
+ backdropFilter: "blur(12px)",
39
+ padding: "4px 10px",
40
+ borderRadius: "8px",
41
+ pointerEvents: "none",
42
+ transition: "opacity 300ms ease, transform 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)"
43
+ });
44
+ document.body.appendChild(toast);
45
+ const badgeRect = badge.getBoundingClientRect();
46
+ const toastRect = toast.getBoundingClientRect();
47
+ const isTop = badgeRect.top + BADGE_SIZE / 2 < window.innerHeight / 2;
48
+ const top = isTop ? badgeRect.bottom + TOAST_GAP : badgeRect.top - TOAST_GAP - toastRect.height;
49
+ const badgeCenterX = badgeRect.left + badgeRect.width / 2;
50
+ const left = badgeCenterX - toastRect.width / 2;
51
+ const slideFrom = isTop ? -8 : 8;
52
+ toast.style.top = `${top}px`;
53
+ toast.style.left = `${left}px`;
54
+ toast.style.transform = `translateY(${slideFrom}px)`;
55
+ void toast.offsetHeight;
56
+ toast.style.opacity = "1";
57
+ toast.style.transform = "translateY(0)";
58
+ }
59
+ function removeToast(immediate = false) {
60
+ const existing = document.querySelector(`[${TOAST_ATTR}]`);
61
+ if (!existing) return;
62
+ if (immediate) {
63
+ existing.remove();
64
+ return;
65
+ }
66
+ existing.style.opacity = "0";
67
+ existing.addEventListener("transitionend", () => existing.remove(), { once: true });
68
+ }
69
+ var SPRING = "scale 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)";
70
+ function reveal(badge) {
71
+ badge.style.transition = SPRING;
72
+ badge.style.scale = "1";
73
+ const onEnd = () => {
74
+ badge.style.transition = "";
75
+ badge.removeEventListener("transitionend", onEnd);
76
+ };
77
+ badge.addEventListener("transitionend", onEnd);
16
78
  }
17
- function useBadge({ badgeRef, show, followNextIndicator }) {
79
+ function useBadge({ badgeRef, show, anchor, anchorDragging, onTap }) {
80
+ const hasDraggedRef = useRef(false);
18
81
  useEffect(() => {
19
- if (!show) return;
82
+ hasDraggedRef.current = false;
83
+ }, [anchor]);
84
+ useEffect(() => {
85
+ if (!show || !anchor) return;
20
86
  const badge = badgeRef.current;
21
87
  if (!badge) return;
22
88
  badge.style.scale = "0";
89
+ let revealed = false;
23
90
  let rafId;
24
- let dragging = false;
25
- let dragStartX = 0;
26
- let dragStartY = 0;
27
- let hidden = false;
28
- let settling = false;
29
- let positioned = false;
30
- let settled = false;
91
+ let lastKey = "";
92
+ const position = () => {
93
+ const rect = anchor.getBoundingClientRect();
94
+ const midX = rect.left + rect.width / 2;
95
+ const isRight = midX > window.innerWidth / 2;
96
+ badge.style.bottom = "";
97
+ badge.style.left = isRight ? `${rect.left - BADGE_SIZE - BADGE_GAP}px` : `${rect.right + BADGE_GAP}px`;
98
+ badge.style.top = `${rect.top + (rect.height - BADGE_SIZE) / 2}px`;
99
+ };
100
+ const tick = () => {
101
+ const rect = anchor.getBoundingClientRect();
102
+ const key = `${rect.top},${rect.left},${rect.right},${rect.bottom}`;
103
+ if (key !== lastKey) {
104
+ lastKey = key;
105
+ position();
106
+ if (!revealed) {
107
+ revealed = true;
108
+ reveal(badge);
109
+ }
110
+ }
111
+ rafId = requestAnimationFrame(tick);
112
+ };
113
+ rafId = requestAnimationFrame(tick);
114
+ return () => cancelAnimationFrame(rafId);
115
+ }, [badgeRef, show, anchor]);
116
+ useEffect(() => {
117
+ if (!show || !anchor) return;
118
+ const badge = badgeRef.current;
119
+ if (!badge) return;
31
120
  let hideTimeout;
32
121
  let reappearTimeout;
33
- const getIndicator = () => followNextIndicator ? findNextjsIndicator() : null;
34
- const positionBadge = () => {
35
- if (!badge) return;
36
- const indicator = getIndicator();
37
- if (indicator) {
38
- const rect = indicator.getBoundingClientRect();
39
- const midX = rect.left + rect.width / 2;
40
- const isRight = midX > window.innerWidth / 2;
41
- badge.style.left = "";
42
- badge.style.bottom = "";
43
- badge.style.left = isRight ? `${rect.left - BADGE_SIZE - BADGE_GAP}px` : `${rect.right + BADGE_GAP}px`;
44
- badge.style.top = `${rect.top + (rect.height - BADGE_SIZE) / 2}px`;
45
- } else {
46
- badge.style.top = "";
47
- badge.style.bottom = "12px";
48
- badge.style.left = "12px";
49
- settled = true;
50
- }
51
- if (!positioned) {
52
- positioned = true;
53
- requestAnimationFrame(() => {
54
- badge.style.scale = "1";
55
- });
56
- }
122
+ let toastTimeout;
123
+ if (anchorDragging) {
124
+ hasDraggedRef.current = true;
125
+ badge.style.transition = "scale 200ms ease";
126
+ badge.style.scale = "0";
127
+ hideTimeout = setTimeout(() => {
128
+ badge.style.display = "none";
129
+ }, 200);
130
+ } else {
131
+ clearTimeout(hideTimeout);
132
+ reappearTimeout = setTimeout(() => {
133
+ badge.style.scale = "0";
134
+ badge.style.display = "";
135
+ badge.style.transition = SPRING;
136
+ void badge.offsetHeight;
137
+ badge.style.scale = "1";
138
+ if (hasDraggedRef.current) {
139
+ showToast(badge);
140
+ toastTimeout = setTimeout(() => removeToast(), 2e3);
141
+ }
142
+ }, 1e3);
143
+ }
144
+ return () => {
145
+ clearTimeout(hideTimeout);
146
+ clearTimeout(reappearTimeout);
147
+ clearTimeout(toastTimeout);
148
+ removeToast();
57
149
  };
150
+ }, [badgeRef, show, anchor, anchorDragging]);
151
+ useEffect(() => {
152
+ if (!show || anchor) return;
153
+ const badge = badgeRef.current;
154
+ if (!badge) return;
155
+ badge.style.scale = "0";
156
+ badge.style.top = "";
157
+ badge.style.bottom = `${EDGE_MARGIN}px`;
158
+ badge.style.left = `${EDGE_MARGIN}px`;
159
+ requestAnimationFrame(() => reveal(badge));
160
+ let dragging = false;
161
+ let dragStartX = 0;
162
+ let dragStartY = 0;
163
+ let badgeStartX = 0;
164
+ let badgeStartY = 0;
165
+ let moved = false;
58
166
  const onPointerDown = (e) => {
59
- const indicator = getIndicator();
60
- if (!indicator) return;
61
- const rect = indicator.getBoundingClientRect();
62
- if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) {
63
- dragging = true;
64
- dragStartX = e.clientX;
65
- dragStartY = e.clientY;
66
- }
167
+ if (!badge.contains(e.target)) return;
168
+ dragging = true;
169
+ moved = false;
170
+ dragStartX = e.clientX;
171
+ dragStartY = e.clientY;
172
+ const rect = badge.getBoundingClientRect();
173
+ badgeStartX = rect.left;
174
+ badgeStartY = rect.top;
175
+ badge.setPointerCapture(e.pointerId);
67
176
  };
68
177
  const onPointerMove = (e) => {
69
178
  if (!dragging) return;
70
179
  const dx = e.clientX - dragStartX;
71
180
  const dy = e.clientY - dragStartY;
72
- if (!hidden && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
73
- hidden = true;
74
- settling = true;
75
- badge.style.transition = "scale 200ms ease";
76
- badge.style.scale = "0";
77
- hideTimeout = setTimeout(() => {
78
- badge.style.display = "none";
79
- }, 200);
80
- }
181
+ if (!moved && Math.sqrt(dx * dx + dy * dy) < DRAG_THRESHOLD) return;
182
+ if (!moved) badge.style.cursor = "grabbing";
183
+ moved = true;
184
+ let x = badgeStartX + dx;
185
+ let y = badgeStartY + dy;
186
+ x = Math.max(0, Math.min(x, window.innerWidth - BADGE_SIZE));
187
+ y = Math.max(0, Math.min(y, window.innerHeight - BADGE_SIZE));
188
+ badge.style.bottom = "";
189
+ badge.style.left = `${x}px`;
190
+ badge.style.top = `${y}px`;
81
191
  };
82
- const onPointerUp = () => {
192
+ const snapToCorner = (x, y) => {
193
+ const midX = x + BADGE_SIZE / 2;
194
+ const midY = y + BADGE_SIZE / 2;
195
+ const snapLeft = midX < window.innerWidth / 2;
196
+ const snapTop = midY < window.innerHeight / 2;
197
+ badge.style.transition = "left 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92), top 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)";
198
+ badge.style.left = snapLeft ? `${EDGE_MARGIN}px` : `${window.innerWidth - BADGE_SIZE - EDGE_MARGIN}px`;
199
+ badge.style.top = snapTop ? `${EDGE_MARGIN}px` : `${window.innerHeight - BADGE_SIZE - EDGE_MARGIN}px`;
200
+ const onEnd = () => {
201
+ badge.style.transition = "";
202
+ badge.removeEventListener("transitionend", onEnd);
203
+ };
204
+ badge.addEventListener("transitionend", onEnd);
205
+ };
206
+ const onPointerUp = (e) => {
83
207
  if (!dragging) return;
84
208
  dragging = false;
85
- if (hidden) {
86
- reappearTimeout = setTimeout(() => {
87
- hidden = false;
88
- settling = false;
89
- settled = false;
90
- badge.style.scale = "0";
91
- badge.style.display = "";
92
- badge.style.transition = "scale 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)";
93
- void badge.offsetHeight;
94
- badge.style.scale = "1";
95
- }, 1e3);
209
+ badge.style.cursor = "";
210
+ badge.releasePointerCapture(e.pointerId);
211
+ if (moved) {
212
+ const rect = badge.getBoundingClientRect();
213
+ snapToCorner(rect.left, rect.top);
96
214
  }
97
215
  };
98
- let lastKey = "";
99
- const tick = () => {
100
- if (settled) return;
101
- if (!hidden && !settling) {
102
- const indicator = getIndicator();
103
- if (indicator) {
104
- const rect = indicator.getBoundingClientRect();
105
- const key = `${rect.top},${rect.left},${rect.right},${rect.bottom}`;
106
- if (key !== lastKey) {
107
- lastKey = key;
108
- positionBadge();
109
- }
110
- } else if (lastKey !== "fallback") {
111
- lastKey = "fallback";
112
- positionBadge();
113
- }
216
+ badge.addEventListener("pointerdown", onPointerDown);
217
+ window.addEventListener("pointermove", onPointerMove, true);
218
+ window.addEventListener("pointerup", onPointerUp, true);
219
+ return () => {
220
+ badge.removeEventListener("pointerdown", onPointerDown);
221
+ window.removeEventListener("pointermove", onPointerMove, true);
222
+ window.removeEventListener("pointerup", onPointerUp, true);
223
+ };
224
+ }, [badgeRef, show, anchor]);
225
+ const onTapRef = useRef(onTap);
226
+ onTapRef.current = onTap;
227
+ useEffect(() => {
228
+ if (!show) return;
229
+ const badge = badgeRef.current;
230
+ if (!badge) return;
231
+ let down = false;
232
+ let startX = 0;
233
+ let startY = 0;
234
+ let moved = false;
235
+ const onPointerDown = (e) => {
236
+ if (!badge.contains(e.target)) return;
237
+ down = true;
238
+ moved = false;
239
+ startX = e.clientX;
240
+ startY = e.clientY;
241
+ };
242
+ const onPointerMove = (e) => {
243
+ if (!down || moved) return;
244
+ const dx = e.clientX - startX;
245
+ const dy = e.clientY - startY;
246
+ if (dx * dx + dy * dy > DRAG_THRESHOLD * DRAG_THRESHOLD) {
247
+ moved = true;
114
248
  }
115
- rafId = requestAnimationFrame(tick);
116
249
  };
117
- rafId = requestAnimationFrame(tick);
118
- window.addEventListener("pointerdown", onPointerDown, true);
250
+ const onPointerUp = () => {
251
+ if (!down) return;
252
+ down = false;
253
+ if (!moved) onTapRef.current?.();
254
+ };
255
+ badge.addEventListener("pointerdown", onPointerDown);
119
256
  window.addEventListener("pointermove", onPointerMove, true);
120
257
  window.addEventListener("pointerup", onPointerUp, true);
121
258
  return () => {
122
- cancelAnimationFrame(rafId);
123
- clearTimeout(hideTimeout);
124
- clearTimeout(reappearTimeout);
125
- window.removeEventListener("pointerdown", onPointerDown, true);
259
+ badge.removeEventListener("pointerdown", onPointerDown);
126
260
  window.removeEventListener("pointermove", onPointerMove, true);
127
261
  window.removeEventListener("pointerup", onPointerUp, true);
128
262
  };
129
- }, [badgeRef, show, followNextIndicator]);
263
+ }, [badgeRef, show]);
130
264
  }
131
265
 
132
266
  // src/use-hotkey.ts
@@ -145,7 +279,7 @@ function useHotkey(hotKey, onToggle) {
145
279
  }
146
280
 
147
281
  // src/use-inspector.ts
148
- import { useEffect as useEffect3, useRef } from "react";
282
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
149
283
  function getFiberFromElement(el) {
150
284
  const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
151
285
  return key ? el[key] : null;
@@ -169,8 +303,8 @@ function getComponentInfo(el) {
169
303
  function parseInspPath(attr) {
170
304
  const parts = attr.split(":");
171
305
  parts.pop();
172
- const column = parts.pop();
173
- const line = parts.pop();
306
+ const column = parts.pop() ?? "";
307
+ const line = parts.pop() ?? "";
174
308
  const filePath = parts.join(":");
175
309
  return { filePath, line, column };
176
310
  }
@@ -189,7 +323,7 @@ function useInspector({
189
323
  tooltipRef,
190
324
  ignoreRefs
191
325
  }) {
192
- const currentTarget = useRef(null);
326
+ const currentTarget = useRef2(null);
193
327
  useEffect3(() => {
194
328
  if (!enabled) return;
195
329
  const isIgnored = (e) => ignoreRefs.some(
@@ -329,6 +463,119 @@ function useInspector({
329
463
  }, [enabled, overlayRef, tooltipRef, ignoreRefs]);
330
464
  }
331
465
 
466
+ // src/use-next-indicator.ts
467
+ import { useEffect as useEffect4, useRef as useRef3, useState } from "react";
468
+ var DRAG_THRESHOLD2 = 10;
469
+ function findIndicator() {
470
+ for (const portal of document.querySelectorAll("nextjs-portal")) {
471
+ if (!portal.shadowRoot) continue;
472
+ const toast = portal.shadowRoot.querySelector("[data-nextjs-toast]");
473
+ if (toast) return toast;
474
+ }
475
+ return null;
476
+ }
477
+ var DISCOVERY_TIMEOUT = 5e3;
478
+ function useNextIndicator(enabled) {
479
+ const [element, setElement] = useState(null);
480
+ const [isDragging, setDragging] = useState(false);
481
+ const [searching, setSearching] = useState(enabled);
482
+ const elementRef = useRef3(null);
483
+ useEffect4(() => {
484
+ if (!enabled) {
485
+ elementRef.current = null;
486
+ setElement(null);
487
+ setSearching(false);
488
+ return;
489
+ }
490
+ const existing = findIndicator();
491
+ if (existing) {
492
+ elementRef.current = existing;
493
+ setElement(existing);
494
+ return;
495
+ }
496
+ const hasPortals = document.querySelectorAll("nextjs-portal").length > 0;
497
+ if (!hasPortals) {
498
+ setSearching(false);
499
+ return;
500
+ }
501
+ setSearching(true);
502
+ const observers = [];
503
+ const found = (el) => {
504
+ elementRef.current = el;
505
+ setElement(el);
506
+ setSearching(false);
507
+ observers.forEach((o) => o.disconnect());
508
+ clearTimeout(timeout);
509
+ };
510
+ const timeout = setTimeout(() => {
511
+ setSearching(false);
512
+ observers.forEach((o) => o.disconnect());
513
+ }, DISCOVERY_TIMEOUT);
514
+ const observeShadowRoots = () => {
515
+ for (const portal of document.querySelectorAll("nextjs-portal")) {
516
+ if (!portal.shadowRoot) continue;
517
+ const shadowObserver = new MutationObserver(() => {
518
+ const indicator = findIndicator();
519
+ if (indicator) found(indicator);
520
+ });
521
+ shadowObserver.observe(portal.shadowRoot, { childList: true, subtree: true });
522
+ observers.push(shadowObserver);
523
+ }
524
+ };
525
+ observeShadowRoots();
526
+ const bodyObserver = new MutationObserver(() => {
527
+ const indicator = findIndicator();
528
+ if (indicator) {
529
+ found(indicator);
530
+ } else {
531
+ observeShadowRoots();
532
+ }
533
+ });
534
+ bodyObserver.observe(document.body, { childList: true, subtree: true });
535
+ observers.push(bodyObserver);
536
+ return () => {
537
+ clearTimeout(timeout);
538
+ observers.forEach((o) => o.disconnect());
539
+ };
540
+ }, [enabled]);
541
+ useEffect4(() => {
542
+ if (!element) return;
543
+ let dragging = false;
544
+ let startX = 0;
545
+ let startY = 0;
546
+ const onPointerDown = (e) => {
547
+ const rect = element.getBoundingClientRect();
548
+ if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) {
549
+ dragging = true;
550
+ startX = e.clientX;
551
+ startY = e.clientY;
552
+ }
553
+ };
554
+ const onPointerMove = (e) => {
555
+ if (!dragging) return;
556
+ const dx = e.clientX - startX;
557
+ const dy = e.clientY - startY;
558
+ if (dx * dx + dy * dy > DRAG_THRESHOLD2 * DRAG_THRESHOLD2) {
559
+ setDragging(true);
560
+ }
561
+ };
562
+ const onPointerUp = () => {
563
+ if (!dragging) return;
564
+ dragging = false;
565
+ setDragging(false);
566
+ };
567
+ window.addEventListener("pointerdown", onPointerDown, true);
568
+ window.addEventListener("pointermove", onPointerMove, true);
569
+ window.addEventListener("pointerup", onPointerUp, true);
570
+ return () => {
571
+ window.removeEventListener("pointerdown", onPointerDown, true);
572
+ window.removeEventListener("pointermove", onPointerMove, true);
573
+ window.removeEventListener("pointerup", onPointerUp, true);
574
+ };
575
+ }, [element]);
576
+ return { element, isDragging, searching };
577
+ }
578
+
332
579
  // src/xray.tsx
333
580
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
334
581
  var DEFAULT_PORT = 5678;
@@ -346,13 +593,16 @@ function XrayImpl({
346
593
  showButton = true,
347
594
  followNextIndicator = true
348
595
  } = {}) {
349
- const [enabled, setEnabled] = useState(false);
350
- const overlayRef = useRef2(null);
351
- const tooltipRef = useRef2(null);
352
- const badgeRef = useRef2(null);
596
+ const [mounted, setMounted] = useState2(false);
597
+ const [enabled, setEnabled] = useState2(false);
598
+ const overlayRef = useRef4(null);
599
+ const tooltipRef = useRef4(null);
600
+ const badgeRef = useRef4(null);
353
601
  const toggle = useCallback(() => setEnabled((prev) => !prev), []);
602
+ useEffect5(() => setMounted(true), []);
603
+ const { element: anchor, isDragging, searching } = useNextIndicator(followNextIndicator);
354
604
  useHotkey(hotKey, toggle);
355
- useBadge({ badgeRef, show: showButton, followNextIndicator });
605
+ useBadge({ badgeRef, show: showButton && !searching, anchor, anchorDragging: isDragging, onTap: toggle });
356
606
  useInspector({
357
607
  enabled,
358
608
  port,
@@ -360,6 +610,7 @@ function XrayImpl({
360
610
  tooltipRef,
361
611
  ignoreRefs: [badgeRef]
362
612
  });
613
+ if (!mounted) return null;
363
614
  return createPortal(
364
615
  /* @__PURE__ */ jsxs(Fragment, { children: [
365
616
  /* @__PURE__ */ jsx(
@@ -408,15 +659,12 @@ function XrayImpl({
408
659
  zIndex: 2147483646,
409
660
  width: "36px",
410
661
  height: "36px",
411
- transformOrigin: "center center"
662
+ transformOrigin: "center center",
663
+ scale: "0"
412
664
  },
413
665
  children: /* @__PURE__ */ jsx(
414
666
  "button",
415
667
  {
416
- onClick: (e) => {
417
- e.stopPropagation();
418
- toggle();
419
- },
420
668
  style: {
421
669
  width: "36px",
422
670
  height: "36px",
package/dist/plugin.cjs CHANGED
@@ -26,7 +26,9 @@ module.exports = __toCommonJS(plugin_exports);
26
26
  var import_code_inspector_plugin = require("code-inspector-plugin");
27
27
  function xrayPlugin(options) {
28
28
  return (0, import_code_inspector_plugin.codeInspectorPlugin)({
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- code-inspector-plugin accepts wider types than our union
29
30
  bundler: options.bundler,
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
32
  editor: options.editor ?? "code",
31
33
  hotKeys: false,
32
34
  showSwitch: false
package/dist/plugin.mjs CHANGED
@@ -2,7 +2,9 @@
2
2
  import { codeInspectorPlugin } from "code-inspector-plugin";
3
3
  function xrayPlugin(options) {
4
4
  return codeInspectorPlugin({
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- code-inspector-plugin accepts wider types than our union
5
6
  bundler: options.bundler,
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
8
  editor: options.editor ?? "code",
7
9
  hotKeys: false,
8
10
  showSwitch: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stinsky/xray",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "React dev inspector — hover to see component names, click to open source in your editor. Works with React 19 + Turbopack.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,12 +22,6 @@
22
22
  "files": [
23
23
  "dist"
24
24
  ],
25
- "scripts": {
26
- "build": "tsup",
27
- "dev": "tsup --watch",
28
- "prepublishOnly": "tsup",
29
- "typecheck": "tsc --noEmit"
30
- },
31
25
  "peerDependencies": {
32
26
  "react": ">=18",
33
27
  "react-dom": ">=18"
@@ -36,13 +30,17 @@
36
30
  "code-inspector-plugin": "^1.4.5"
37
31
  },
38
32
  "devDependencies": {
33
+ "@eslint/js": "^9.39.4",
39
34
  "@types/node": "^25.5.0",
40
35
  "@types/react": "^19.0.0",
41
36
  "@types/react-dom": "^19.0.0",
37
+ "eslint": "^9.39.4",
38
+ "eslint-plugin-react-hooks": "^5.2.0",
42
39
  "react": "^19.0.0",
43
40
  "react-dom": "^19.0.0",
44
41
  "tsup": "^8.4.0",
45
- "typescript": "^5.7.0"
42
+ "typescript": "^5.7.0",
43
+ "typescript-eslint": "^8.57.2"
46
44
  },
47
45
  "keywords": [
48
46
  "react",
@@ -58,5 +56,11 @@
58
56
  "repository": {
59
57
  "type": "git",
60
58
  "url": "https://github.com/ivanstnsk/xray"
59
+ },
60
+ "scripts": {
61
+ "build": "tsup",
62
+ "dev": "tsup --watch",
63
+ "typecheck": "tsc --noEmit",
64
+ "lint": "eslint src/"
61
65
  }
62
- }
66
+ }