@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 +17 -5
- package/dist/index.cjs +352 -104
- package/dist/index.mjs +354 -106
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.mjs +2 -0
- package/package.json +13 -9
package/README.md
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.svg" width="160" alt="xray logo" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">@stinsky/xray</h1>
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
badge.style.
|
|
70
|
-
|
|
71
|
-
badge.style.
|
|
72
|
-
badge.
|
|
73
|
-
badge.style.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 (!
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
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 [
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
const
|
|
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,
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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,
|
|
79
|
+
function useBadge({ badgeRef, show, anchor, anchorDragging, onTap }) {
|
|
80
|
+
const hasDraggedRef = useRef(false);
|
|
18
81
|
useEffect(() => {
|
|
19
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
badge.style.
|
|
45
|
-
|
|
46
|
-
badge.style.
|
|
47
|
-
badge.
|
|
48
|
-
badge.style.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 (!
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 [
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
const
|
|
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,
|
|
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.
|
|
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
|
+
}
|