@stinsky/xray 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Stinsky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @stinsky/xray
2
+
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.
4
+
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
+
8
+ ## Features
9
+
10
+ - **Click to open source** — click any element to open its source file in VS Code, WebStorm, IntelliJ, or any editor
11
+ - **Component name overlay** — hover to see the React component name + file path
12
+ - **Works with React 19** — uses compile-time AST injection, not `fiber._debugSource` (removed in React 19)
13
+ - **All bundlers** — Next.js (Turbopack & Webpack), Vite, Webpack, Rspack, esbuild
14
+ - **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
16
+ - **Keyboard shortcut** — `Cmd+Shift+X` to toggle (customizable)
17
+ - **Scroll-aware** — rAF-based tracking, works with smooth scrolling libraries (Lenis, etc.)
18
+ - **Interaction blocking** — all clicks/pointer events blocked while inspecting, no accidental navigation
19
+
20
+ ## Why not react-dev-inspector / click-to-react-component / LocatorJS?
21
+
22
+ React 19 removed `fiber._debugSource`, which broke every existing click-to-component tool. These packages rely on runtime fiber inspection to find source locations — an approach that no longer works.
23
+
24
+ `@stinsky/xray` uses [`code-inspector-plugin`](https://github.com/nicolo-ribaudo/code-inspector-plugin) to inject `data-insp-path` attributes at compile time via AST transformation. This is the only approach that works reliably across React 18, React 19, and all modern bundlers.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install -D @stinsky/xray
30
+ ```
31
+
32
+ ## Setup
33
+
34
+ ### 1. Add the build plugin
35
+
36
+ The plugin injects source location attributes on every DOM element at compile time. It does nothing in production builds.
37
+
38
+ #### Next.js (Turbopack)
39
+
40
+ ```ts
41
+ // next.config.ts
42
+ import { xrayPlugin } from '@stinsky/xray/plugin'
43
+
44
+ const nextConfig = {
45
+ turbopack: {
46
+ rules: xrayPlugin({ bundler: 'turbopack' }),
47
+ },
48
+ }
49
+
50
+ export default nextConfig
51
+ ```
52
+
53
+ #### Next.js (Webpack)
54
+
55
+ ```ts
56
+ // next.config.ts
57
+ import { xrayPlugin } from '@stinsky/xray/plugin'
58
+
59
+ const nextConfig = {
60
+ webpack: (config) => {
61
+ config.plugins.push(xrayPlugin({ bundler: 'webpack' }))
62
+ return config
63
+ },
64
+ }
65
+
66
+ export default nextConfig
67
+ ```
68
+
69
+ #### Vite
70
+
71
+ ```ts
72
+ // vite.config.ts
73
+ import { xrayPlugin } from '@stinsky/xray/plugin'
74
+
75
+ export default {
76
+ plugins: [xrayPlugin({ bundler: 'vite' })],
77
+ }
78
+ ```
79
+
80
+ #### Webpack
81
+
82
+ ```ts
83
+ // webpack.config.js
84
+ const { xrayPlugin } = require('@stinsky/xray/plugin')
85
+
86
+ module.exports = {
87
+ plugins: [xrayPlugin({ bundler: 'webpack' })],
88
+ }
89
+ ```
90
+
91
+ ### 2. Add the component
92
+
93
+ ```tsx
94
+ // app/layout.tsx (Next.js) or your root component
95
+ import { Xray } from '@stinsky/xray'
96
+
97
+ export default function Layout({ children }) {
98
+ return (
99
+ <html>
100
+ <body>
101
+ {children}
102
+ <Xray />
103
+ </body>
104
+ </html>
105
+ )
106
+ }
107
+ ```
108
+
109
+ The component is fully tree-shaken in production — zero bytes in your bundle. Bundlers replace `process.env.NODE_ENV`, detect the dead code path, and eliminate the entire inspector along with all its dependencies.
110
+
111
+ ## Usage
112
+
113
+ - **Toggle**: `Cmd+Shift+X` or click the floating button
114
+ - **Hover**: Shows component name + source file path
115
+ - **Click**: Opens the source file in your editor
116
+
117
+ All clicks and pointer events are blocked while the inspector is active, so you won't accidentally trigger links or buttons.
118
+
119
+ ## Props
120
+
121
+ | Prop | Type | Default | Description |
122
+ |------|------|---------|-------------|
123
+ | `hotKey` | `{ metaKey?, ctrlKey?, altKey?, shiftKey?, key }` | `{ metaKey: true, shiftKey: true, key: 'x' }` | Keyboard shortcut to toggle |
124
+ | `port` | `number` | `5678` | `code-inspector-plugin` server port |
125
+ | `color` | `string` | `'#6366f1'` | Accent color for overlay, tooltip, and button |
126
+ | `showButton` | `boolean` | `true` | Show the floating toggle button |
127
+ | `followNextIndicator` | `boolean` | `true` | Position button next to the Next.js dev indicator |
128
+
129
+ ## Plugin options
130
+
131
+ | Option | Type | Default | Description |
132
+ |--------|------|---------|-------------|
133
+ | `bundler` | `'webpack' \| 'vite' \| 'turbopack' \| 'rspack' \| 'esbuild'` | — | **Required.** Your bundler |
134
+ | `editor` | `string` | `'code'` | Editor to open files in (`code`, `webstorm`, `idea`, etc.) |
135
+
136
+ ## License
137
+
138
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,505 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ Xray: () => Xray
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+
28
+ // src/xray.tsx
29
+ var import_react4 = require("react");
30
+ var import_react_dom = require("react-dom");
31
+
32
+ // src/use-badge.ts
33
+ var import_react = require("react");
34
+ var BADGE_SIZE = 36;
35
+ var BADGE_GAP = 4;
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");
41
+ }
42
+ function useBadge({ badgeRef, show, followNextIndicator }) {
43
+ (0, import_react.useEffect)(() => {
44
+ if (!show) return;
45
+ const badge = badgeRef.current;
46
+ if (!badge) return;
47
+ badge.style.scale = "0";
48
+ 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;
56
+ let hideTimeout;
57
+ 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
+ }
82
+ };
83
+ 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
+ }
92
+ };
93
+ const onPointerMove = (e) => {
94
+ if (!dragging) return;
95
+ const dx = e.clientX - dragStartX;
96
+ 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
+ }
106
+ };
107
+ const onPointerUp = () => {
108
+ if (!dragging) return;
109
+ 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);
121
+ }
122
+ };
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
+ }
139
+ }
140
+ rafId = requestAnimationFrame(tick);
141
+ };
142
+ rafId = requestAnimationFrame(tick);
143
+ window.addEventListener("pointerdown", onPointerDown, true);
144
+ window.addEventListener("pointermove", onPointerMove, true);
145
+ window.addEventListener("pointerup", onPointerUp, true);
146
+ return () => {
147
+ cancelAnimationFrame(rafId);
148
+ clearTimeout(hideTimeout);
149
+ clearTimeout(reappearTimeout);
150
+ window.removeEventListener("pointerdown", onPointerDown, true);
151
+ window.removeEventListener("pointermove", onPointerMove, true);
152
+ window.removeEventListener("pointerup", onPointerUp, true);
153
+ };
154
+ }, [badgeRef, show, followNextIndicator]);
155
+ }
156
+
157
+ // src/use-hotkey.ts
158
+ var import_react2 = require("react");
159
+ var DEFAULT_HOT_KEY = { metaKey: true, shiftKey: true, key: "x" };
160
+ function useHotkey(hotKey, onToggle) {
161
+ (0, import_react2.useEffect)(() => {
162
+ const handler = (e) => {
163
+ if (e.key === hotKey.key && !!e.metaKey === !!hotKey.metaKey && !!e.ctrlKey === !!hotKey.ctrlKey && !!e.altKey === !!hotKey.altKey && !!e.shiftKey === !!hotKey.shiftKey) {
164
+ onToggle();
165
+ }
166
+ };
167
+ window.addEventListener("keydown", handler);
168
+ return () => window.removeEventListener("keydown", handler);
169
+ }, [hotKey.key, hotKey.metaKey, hotKey.ctrlKey, hotKey.altKey, hotKey.shiftKey, onToggle]);
170
+ }
171
+
172
+ // src/use-inspector.ts
173
+ var import_react3 = require("react");
174
+ function getFiberFromElement(el) {
175
+ const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
176
+ return key ? el[key] : null;
177
+ }
178
+ function getComponentName(fiber) {
179
+ if (!fiber.type || typeof fiber.type === "string") return null;
180
+ return fiber.type.displayName || fiber.type.name || null;
181
+ }
182
+ function getComponentInfo(el) {
183
+ let fiber = getFiberFromElement(el);
184
+ if (!fiber) return null;
185
+ let depth = 0;
186
+ while (fiber && depth < 30) {
187
+ const name = getComponentName(fiber);
188
+ if (name) return { name };
189
+ fiber = fiber.return;
190
+ depth++;
191
+ }
192
+ return null;
193
+ }
194
+ function parseInspPath(attr) {
195
+ const parts = attr.split(":");
196
+ parts.pop();
197
+ const column = parts.pop();
198
+ const line = parts.pop();
199
+ const filePath = parts.join(":");
200
+ return { filePath, line, column };
201
+ }
202
+ function findInspPath(el) {
203
+ while (el) {
204
+ const attr = el.getAttribute?.("data-insp-path");
205
+ if (attr) return attr;
206
+ el = el.parentElement;
207
+ }
208
+ return null;
209
+ }
210
+ function useInspector({
211
+ enabled,
212
+ port,
213
+ overlayRef,
214
+ tooltipRef,
215
+ ignoreRefs
216
+ }) {
217
+ const currentTarget = (0, import_react3.useRef)(null);
218
+ (0, import_react3.useEffect)(() => {
219
+ if (!enabled) return;
220
+ const isIgnored = (e) => ignoreRefs.some(
221
+ (ref) => ref.current && (e.target === ref.current || ref.current.contains(e.target))
222
+ );
223
+ const blockEvent = (e) => {
224
+ if (isIgnored(e)) return;
225
+ e.preventDefault();
226
+ e.stopPropagation();
227
+ };
228
+ const handleClick = (e) => {
229
+ if (isIgnored(e)) return;
230
+ e.preventDefault();
231
+ e.stopPropagation();
232
+ const attr = findInspPath(e.target);
233
+ if (!attr) return;
234
+ const { filePath, line, column } = parseInspPath(attr);
235
+ const url = `http://localhost:${port}/?file=${encodeURIComponent(filePath)}&line=${line}&column=${column}`;
236
+ const xhr = new XMLHttpRequest();
237
+ xhr.open("GET", url, true);
238
+ xhr.send();
239
+ };
240
+ const events = ["mousedown", "mouseup", "pointerdown", "pointerup"];
241
+ window.addEventListener("click", handleClick, true);
242
+ events.forEach((e) => window.addEventListener(e, blockEvent, true));
243
+ return () => {
244
+ window.removeEventListener("click", handleClick, true);
245
+ events.forEach((e) => window.removeEventListener(e, blockEvent, true));
246
+ };
247
+ }, [enabled, port, ignoreRefs]);
248
+ (0, import_react3.useEffect)(() => {
249
+ if (!enabled) {
250
+ if (overlayRef.current) overlayRef.current.style.display = "none";
251
+ if (tooltipRef.current) tooltipRef.current.style.display = "none";
252
+ currentTarget.current = null;
253
+ return;
254
+ }
255
+ let mouseX = 0;
256
+ let mouseY = 0;
257
+ const hideOverlay = () => {
258
+ if (overlayRef.current) overlayRef.current.style.display = "none";
259
+ if (tooltipRef.current) tooltipRef.current.style.display = "none";
260
+ };
261
+ const isIgnoredElement = (target) => ignoreRefs.some(
262
+ (ref) => ref.current && (target === ref.current || ref.current.contains(target))
263
+ );
264
+ const updateTooltipContent = (target, info) => {
265
+ const tooltip = tooltipRef.current;
266
+ if (!tooltip) return;
267
+ tooltip.textContent = "";
268
+ const nameLine = document.createElement("div");
269
+ nameLine.textContent = info.name;
270
+ tooltip.appendChild(nameLine);
271
+ const attr = findInspPath(target);
272
+ if (attr) {
273
+ const { filePath, line } = parseInspPath(attr);
274
+ const pathLine = document.createElement("div");
275
+ pathLine.textContent = `${filePath}:${line}`;
276
+ pathLine.style.opacity = "0.7";
277
+ pathLine.style.fontSize = "10px";
278
+ tooltip.appendChild(pathLine);
279
+ }
280
+ };
281
+ const inspectTarget = (target) => {
282
+ if (!target || target === overlayRef.current || target === tooltipRef.current || isIgnoredElement(target) || target === document.documentElement || target === document.body) {
283
+ if (target === document.documentElement || target === document.body) {
284
+ hideOverlay();
285
+ currentTarget.current = null;
286
+ }
287
+ return;
288
+ }
289
+ if (target === currentTarget.current) return;
290
+ currentTarget.current = target;
291
+ const info = getComponentInfo(target);
292
+ if (!info) {
293
+ hideOverlay();
294
+ return;
295
+ }
296
+ updateTooltipContent(target, info);
297
+ };
298
+ const updateOverlayPosition = () => {
299
+ const target = currentTarget.current;
300
+ if (!target || !target.isConnected) {
301
+ hideOverlay();
302
+ return;
303
+ }
304
+ const rect = target.getBoundingClientRect();
305
+ if (overlayRef.current) {
306
+ const s = overlayRef.current.style;
307
+ s.display = "block";
308
+ s.top = `${rect.top}px`;
309
+ s.left = `${rect.left}px`;
310
+ s.width = `${rect.width}px`;
311
+ s.height = `${rect.height}px`;
312
+ }
313
+ if (tooltipRef.current) {
314
+ const s = tooltipRef.current.style;
315
+ s.display = "block";
316
+ const gap = 12;
317
+ let top = mouseY + gap;
318
+ let left = mouseX + gap;
319
+ if (top + tooltipRef.current.offsetHeight > window.innerHeight) {
320
+ top = mouseY - tooltipRef.current.offsetHeight - gap;
321
+ }
322
+ if (left + tooltipRef.current.offsetWidth > window.innerWidth) {
323
+ left = mouseX - tooltipRef.current.offsetWidth - gap;
324
+ }
325
+ s.top = `${Math.max(0, top)}px`;
326
+ s.left = `${Math.max(0, left)}px`;
327
+ }
328
+ };
329
+ const handleMouseMove = (e) => {
330
+ mouseX = e.clientX;
331
+ mouseY = e.clientY;
332
+ inspectTarget(e.target);
333
+ updateOverlayPosition();
334
+ };
335
+ const handleMouseLeave = () => {
336
+ hideOverlay();
337
+ currentTarget.current = null;
338
+ };
339
+ let rafId;
340
+ const tick = () => {
341
+ const el = document.elementFromPoint(mouseX, mouseY);
342
+ if (el) inspectTarget(el);
343
+ updateOverlayPosition();
344
+ rafId = requestAnimationFrame(tick);
345
+ };
346
+ rafId = requestAnimationFrame(tick);
347
+ window.addEventListener("mousemove", handleMouseMove);
348
+ document.addEventListener("mouseleave", handleMouseLeave);
349
+ return () => {
350
+ cancelAnimationFrame(rafId);
351
+ window.removeEventListener("mousemove", handleMouseMove);
352
+ document.removeEventListener("mouseleave", handleMouseLeave);
353
+ };
354
+ }, [enabled, overlayRef, tooltipRef, ignoreRefs]);
355
+ }
356
+
357
+ // src/xray.tsx
358
+ var import_jsx_runtime = require("react/jsx-runtime");
359
+ var DEFAULT_PORT = 5678;
360
+ var DEFAULT_COLOR = "#6366f1";
361
+ function colorWithAlpha(hex, alpha) {
362
+ const r = parseInt(hex.slice(1, 3), 16);
363
+ const g = parseInt(hex.slice(3, 5), 16);
364
+ const b = parseInt(hex.slice(5, 7), 16);
365
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
366
+ }
367
+ function XrayImpl({
368
+ hotKey = DEFAULT_HOT_KEY,
369
+ port = DEFAULT_PORT,
370
+ color = DEFAULT_COLOR,
371
+ showButton = true,
372
+ followNextIndicator = true
373
+ } = {}) {
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), []);
379
+ useHotkey(hotKey, toggle);
380
+ useBadge({ badgeRef, show: showButton, followNextIndicator });
381
+ useInspector({
382
+ enabled,
383
+ port,
384
+ overlayRef,
385
+ tooltipRef,
386
+ ignoreRefs: [badgeRef]
387
+ });
388
+ return (0, import_react_dom.createPortal)(
389
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
390
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
391
+ "div",
392
+ {
393
+ ref: overlayRef,
394
+ style: {
395
+ position: "fixed",
396
+ pointerEvents: "none",
397
+ zIndex: 99998,
398
+ border: `2px solid ${color}`,
399
+ borderRadius: "3px",
400
+ backgroundColor: colorWithAlpha(color, 0.08),
401
+ transition: "none",
402
+ display: "none"
403
+ }
404
+ }
405
+ ),
406
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
407
+ "div",
408
+ {
409
+ ref: tooltipRef,
410
+ style: {
411
+ position: "fixed",
412
+ pointerEvents: "none",
413
+ zIndex: 99999,
414
+ backgroundColor: color,
415
+ color: "white",
416
+ fontSize: "11px",
417
+ fontFamily: "monospace",
418
+ fontWeight: 600,
419
+ padding: "2px 7px",
420
+ borderRadius: "4px",
421
+ whiteSpace: "nowrap",
422
+ display: "none",
423
+ boxShadow: "0 2px 8px rgba(0,0,0,0.25)"
424
+ }
425
+ }
426
+ ),
427
+ showButton && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
428
+ "div",
429
+ {
430
+ ref: badgeRef,
431
+ style: {
432
+ position: "fixed",
433
+ zIndex: 2147483646,
434
+ width: "36px",
435
+ height: "36px",
436
+ transformOrigin: "center center"
437
+ },
438
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
439
+ "button",
440
+ {
441
+ onClick: (e) => {
442
+ e.stopPropagation();
443
+ toggle();
444
+ },
445
+ style: {
446
+ width: "36px",
447
+ height: "36px",
448
+ display: "flex",
449
+ alignItems: "center",
450
+ justifyContent: "center",
451
+ background: "rgba(0, 0, 0, 0.8)",
452
+ backdropFilter: "blur(48px)",
453
+ border: "none",
454
+ borderRadius: "50%",
455
+ boxShadow: enabled ? `0 0 0 1px ${color}, inset 0 0 0 1px ${colorWithAlpha(color, 0.4)}, 0 16px 32px -8px rgba(0, 0, 0, 0.24)` : "0 0 0 1px #171717, inset 0 0 0 1px hsla(0, 0%, 100%, 0.14), 0 16px 32px -8px rgba(0, 0, 0, 0.24)",
456
+ opacity: enabled ? 1 : 0.4,
457
+ transition: "opacity 200ms ease, box-shadow 200ms ease",
458
+ cursor: "pointer",
459
+ userSelect: "none",
460
+ padding: 0
461
+ },
462
+ onMouseEnter: (e) => {
463
+ e.currentTarget.style.opacity = "1";
464
+ },
465
+ onMouseLeave: (e) => {
466
+ if (!enabled) e.currentTarget.style.opacity = "0.4";
467
+ },
468
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
469
+ "svg",
470
+ {
471
+ width: "20",
472
+ height: "20",
473
+ viewBox: "0 0 24 24",
474
+ fill: "none",
475
+ stroke: enabled ? color : "white",
476
+ strokeWidth: "2",
477
+ strokeLinecap: "round",
478
+ strokeLinejoin: "round",
479
+ style: {
480
+ transition: "stroke 200ms ease, transform 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)",
481
+ transform: enabled ? "rotate(90deg)" : "rotate(0deg)"
482
+ },
483
+ children: [
484
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "10", strokeOpacity: "0.5" }),
485
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "4" }),
486
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "2", x2: "12", y2: "6" }),
487
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "18", x2: "12", y2: "22" }),
488
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: "12", x2: "6", y2: "12" }),
489
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "12", x2: "22", y2: "12" })
490
+ ]
491
+ }
492
+ )
493
+ }
494
+ )
495
+ }
496
+ )
497
+ ] }),
498
+ document.body
499
+ );
500
+ }
501
+ var Xray = process.env.NODE_ENV === "development" ? XrayImpl : () => null;
502
+ // Annotate the CommonJS export names for ESM import in node:
503
+ 0 && (module.exports = {
504
+ Xray
505
+ });
@@ -0,0 +1,23 @@
1
+ interface HotKey {
2
+ metaKey?: boolean;
3
+ ctrlKey?: boolean;
4
+ altKey?: boolean;
5
+ shiftKey?: boolean;
6
+ key: string;
7
+ }
8
+
9
+ interface XrayProps {
10
+ /** Keyboard shortcut to toggle. Default: Cmd+Shift+X */
11
+ hotKey?: HotKey;
12
+ /** code-inspector-plugin server port. Default: 5678 */
13
+ port?: number;
14
+ /** Accent color for overlay/tooltip/button. Default: '#6366f1' (indigo) */
15
+ color?: string;
16
+ /** Whether to show the floating toggle button. Default: true */
17
+ showButton?: boolean;
18
+ /** Whether to position next to Next.js dev indicator. Default: true */
19
+ followNextIndicator?: boolean;
20
+ }
21
+ declare const Xray: (props?: XrayProps) => React.ReactNode;
22
+
23
+ export { Xray, type XrayProps };
@@ -0,0 +1,23 @@
1
+ interface HotKey {
2
+ metaKey?: boolean;
3
+ ctrlKey?: boolean;
4
+ altKey?: boolean;
5
+ shiftKey?: boolean;
6
+ key: string;
7
+ }
8
+
9
+ interface XrayProps {
10
+ /** Keyboard shortcut to toggle. Default: Cmd+Shift+X */
11
+ hotKey?: HotKey;
12
+ /** code-inspector-plugin server port. Default: 5678 */
13
+ port?: number;
14
+ /** Accent color for overlay/tooltip/button. Default: '#6366f1' (indigo) */
15
+ color?: string;
16
+ /** Whether to show the floating toggle button. Default: true */
17
+ showButton?: boolean;
18
+ /** Whether to position next to Next.js dev indicator. Default: true */
19
+ followNextIndicator?: boolean;
20
+ }
21
+ declare const Xray: (props?: XrayProps) => React.ReactNode;
22
+
23
+ export { Xray, type XrayProps };
package/dist/index.mjs ADDED
@@ -0,0 +1,479 @@
1
+ "use client";
2
+
3
+ // src/xray.tsx
4
+ import { useCallback, useRef as useRef2, useState } from "react";
5
+ import { createPortal } from "react-dom";
6
+
7
+ // src/use-badge.ts
8
+ import { useEffect } from "react";
9
+ var BADGE_SIZE = 36;
10
+ var BADGE_GAP = 4;
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");
16
+ }
17
+ function useBadge({ badgeRef, show, followNextIndicator }) {
18
+ useEffect(() => {
19
+ if (!show) return;
20
+ const badge = badgeRef.current;
21
+ if (!badge) return;
22
+ badge.style.scale = "0";
23
+ 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;
31
+ let hideTimeout;
32
+ 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
+ }
57
+ };
58
+ 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
+ }
67
+ };
68
+ const onPointerMove = (e) => {
69
+ if (!dragging) return;
70
+ const dx = e.clientX - dragStartX;
71
+ 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
+ }
81
+ };
82
+ const onPointerUp = () => {
83
+ if (!dragging) return;
84
+ 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);
96
+ }
97
+ };
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
+ }
114
+ }
115
+ rafId = requestAnimationFrame(tick);
116
+ };
117
+ rafId = requestAnimationFrame(tick);
118
+ window.addEventListener("pointerdown", onPointerDown, true);
119
+ window.addEventListener("pointermove", onPointerMove, true);
120
+ window.addEventListener("pointerup", onPointerUp, true);
121
+ return () => {
122
+ cancelAnimationFrame(rafId);
123
+ clearTimeout(hideTimeout);
124
+ clearTimeout(reappearTimeout);
125
+ window.removeEventListener("pointerdown", onPointerDown, true);
126
+ window.removeEventListener("pointermove", onPointerMove, true);
127
+ window.removeEventListener("pointerup", onPointerUp, true);
128
+ };
129
+ }, [badgeRef, show, followNextIndicator]);
130
+ }
131
+
132
+ // src/use-hotkey.ts
133
+ import { useEffect as useEffect2 } from "react";
134
+ var DEFAULT_HOT_KEY = { metaKey: true, shiftKey: true, key: "x" };
135
+ function useHotkey(hotKey, onToggle) {
136
+ useEffect2(() => {
137
+ const handler = (e) => {
138
+ if (e.key === hotKey.key && !!e.metaKey === !!hotKey.metaKey && !!e.ctrlKey === !!hotKey.ctrlKey && !!e.altKey === !!hotKey.altKey && !!e.shiftKey === !!hotKey.shiftKey) {
139
+ onToggle();
140
+ }
141
+ };
142
+ window.addEventListener("keydown", handler);
143
+ return () => window.removeEventListener("keydown", handler);
144
+ }, [hotKey.key, hotKey.metaKey, hotKey.ctrlKey, hotKey.altKey, hotKey.shiftKey, onToggle]);
145
+ }
146
+
147
+ // src/use-inspector.ts
148
+ import { useEffect as useEffect3, useRef } from "react";
149
+ function getFiberFromElement(el) {
150
+ const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
151
+ return key ? el[key] : null;
152
+ }
153
+ function getComponentName(fiber) {
154
+ if (!fiber.type || typeof fiber.type === "string") return null;
155
+ return fiber.type.displayName || fiber.type.name || null;
156
+ }
157
+ function getComponentInfo(el) {
158
+ let fiber = getFiberFromElement(el);
159
+ if (!fiber) return null;
160
+ let depth = 0;
161
+ while (fiber && depth < 30) {
162
+ const name = getComponentName(fiber);
163
+ if (name) return { name };
164
+ fiber = fiber.return;
165
+ depth++;
166
+ }
167
+ return null;
168
+ }
169
+ function parseInspPath(attr) {
170
+ const parts = attr.split(":");
171
+ parts.pop();
172
+ const column = parts.pop();
173
+ const line = parts.pop();
174
+ const filePath = parts.join(":");
175
+ return { filePath, line, column };
176
+ }
177
+ function findInspPath(el) {
178
+ while (el) {
179
+ const attr = el.getAttribute?.("data-insp-path");
180
+ if (attr) return attr;
181
+ el = el.parentElement;
182
+ }
183
+ return null;
184
+ }
185
+ function useInspector({
186
+ enabled,
187
+ port,
188
+ overlayRef,
189
+ tooltipRef,
190
+ ignoreRefs
191
+ }) {
192
+ const currentTarget = useRef(null);
193
+ useEffect3(() => {
194
+ if (!enabled) return;
195
+ const isIgnored = (e) => ignoreRefs.some(
196
+ (ref) => ref.current && (e.target === ref.current || ref.current.contains(e.target))
197
+ );
198
+ const blockEvent = (e) => {
199
+ if (isIgnored(e)) return;
200
+ e.preventDefault();
201
+ e.stopPropagation();
202
+ };
203
+ const handleClick = (e) => {
204
+ if (isIgnored(e)) return;
205
+ e.preventDefault();
206
+ e.stopPropagation();
207
+ const attr = findInspPath(e.target);
208
+ if (!attr) return;
209
+ const { filePath, line, column } = parseInspPath(attr);
210
+ const url = `http://localhost:${port}/?file=${encodeURIComponent(filePath)}&line=${line}&column=${column}`;
211
+ const xhr = new XMLHttpRequest();
212
+ xhr.open("GET", url, true);
213
+ xhr.send();
214
+ };
215
+ const events = ["mousedown", "mouseup", "pointerdown", "pointerup"];
216
+ window.addEventListener("click", handleClick, true);
217
+ events.forEach((e) => window.addEventListener(e, blockEvent, true));
218
+ return () => {
219
+ window.removeEventListener("click", handleClick, true);
220
+ events.forEach((e) => window.removeEventListener(e, blockEvent, true));
221
+ };
222
+ }, [enabled, port, ignoreRefs]);
223
+ useEffect3(() => {
224
+ if (!enabled) {
225
+ if (overlayRef.current) overlayRef.current.style.display = "none";
226
+ if (tooltipRef.current) tooltipRef.current.style.display = "none";
227
+ currentTarget.current = null;
228
+ return;
229
+ }
230
+ let mouseX = 0;
231
+ let mouseY = 0;
232
+ const hideOverlay = () => {
233
+ if (overlayRef.current) overlayRef.current.style.display = "none";
234
+ if (tooltipRef.current) tooltipRef.current.style.display = "none";
235
+ };
236
+ const isIgnoredElement = (target) => ignoreRefs.some(
237
+ (ref) => ref.current && (target === ref.current || ref.current.contains(target))
238
+ );
239
+ const updateTooltipContent = (target, info) => {
240
+ const tooltip = tooltipRef.current;
241
+ if (!tooltip) return;
242
+ tooltip.textContent = "";
243
+ const nameLine = document.createElement("div");
244
+ nameLine.textContent = info.name;
245
+ tooltip.appendChild(nameLine);
246
+ const attr = findInspPath(target);
247
+ if (attr) {
248
+ const { filePath, line } = parseInspPath(attr);
249
+ const pathLine = document.createElement("div");
250
+ pathLine.textContent = `${filePath}:${line}`;
251
+ pathLine.style.opacity = "0.7";
252
+ pathLine.style.fontSize = "10px";
253
+ tooltip.appendChild(pathLine);
254
+ }
255
+ };
256
+ const inspectTarget = (target) => {
257
+ if (!target || target === overlayRef.current || target === tooltipRef.current || isIgnoredElement(target) || target === document.documentElement || target === document.body) {
258
+ if (target === document.documentElement || target === document.body) {
259
+ hideOverlay();
260
+ currentTarget.current = null;
261
+ }
262
+ return;
263
+ }
264
+ if (target === currentTarget.current) return;
265
+ currentTarget.current = target;
266
+ const info = getComponentInfo(target);
267
+ if (!info) {
268
+ hideOverlay();
269
+ return;
270
+ }
271
+ updateTooltipContent(target, info);
272
+ };
273
+ const updateOverlayPosition = () => {
274
+ const target = currentTarget.current;
275
+ if (!target || !target.isConnected) {
276
+ hideOverlay();
277
+ return;
278
+ }
279
+ const rect = target.getBoundingClientRect();
280
+ if (overlayRef.current) {
281
+ const s = overlayRef.current.style;
282
+ s.display = "block";
283
+ s.top = `${rect.top}px`;
284
+ s.left = `${rect.left}px`;
285
+ s.width = `${rect.width}px`;
286
+ s.height = `${rect.height}px`;
287
+ }
288
+ if (tooltipRef.current) {
289
+ const s = tooltipRef.current.style;
290
+ s.display = "block";
291
+ const gap = 12;
292
+ let top = mouseY + gap;
293
+ let left = mouseX + gap;
294
+ if (top + tooltipRef.current.offsetHeight > window.innerHeight) {
295
+ top = mouseY - tooltipRef.current.offsetHeight - gap;
296
+ }
297
+ if (left + tooltipRef.current.offsetWidth > window.innerWidth) {
298
+ left = mouseX - tooltipRef.current.offsetWidth - gap;
299
+ }
300
+ s.top = `${Math.max(0, top)}px`;
301
+ s.left = `${Math.max(0, left)}px`;
302
+ }
303
+ };
304
+ const handleMouseMove = (e) => {
305
+ mouseX = e.clientX;
306
+ mouseY = e.clientY;
307
+ inspectTarget(e.target);
308
+ updateOverlayPosition();
309
+ };
310
+ const handleMouseLeave = () => {
311
+ hideOverlay();
312
+ currentTarget.current = null;
313
+ };
314
+ let rafId;
315
+ const tick = () => {
316
+ const el = document.elementFromPoint(mouseX, mouseY);
317
+ if (el) inspectTarget(el);
318
+ updateOverlayPosition();
319
+ rafId = requestAnimationFrame(tick);
320
+ };
321
+ rafId = requestAnimationFrame(tick);
322
+ window.addEventListener("mousemove", handleMouseMove);
323
+ document.addEventListener("mouseleave", handleMouseLeave);
324
+ return () => {
325
+ cancelAnimationFrame(rafId);
326
+ window.removeEventListener("mousemove", handleMouseMove);
327
+ document.removeEventListener("mouseleave", handleMouseLeave);
328
+ };
329
+ }, [enabled, overlayRef, tooltipRef, ignoreRefs]);
330
+ }
331
+
332
+ // src/xray.tsx
333
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
334
+ var DEFAULT_PORT = 5678;
335
+ var DEFAULT_COLOR = "#6366f1";
336
+ function colorWithAlpha(hex, alpha) {
337
+ const r = parseInt(hex.slice(1, 3), 16);
338
+ const g = parseInt(hex.slice(3, 5), 16);
339
+ const b = parseInt(hex.slice(5, 7), 16);
340
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
341
+ }
342
+ function XrayImpl({
343
+ hotKey = DEFAULT_HOT_KEY,
344
+ port = DEFAULT_PORT,
345
+ color = DEFAULT_COLOR,
346
+ showButton = true,
347
+ followNextIndicator = true
348
+ } = {}) {
349
+ const [enabled, setEnabled] = useState(false);
350
+ const overlayRef = useRef2(null);
351
+ const tooltipRef = useRef2(null);
352
+ const badgeRef = useRef2(null);
353
+ const toggle = useCallback(() => setEnabled((prev) => !prev), []);
354
+ useHotkey(hotKey, toggle);
355
+ useBadge({ badgeRef, show: showButton, followNextIndicator });
356
+ useInspector({
357
+ enabled,
358
+ port,
359
+ overlayRef,
360
+ tooltipRef,
361
+ ignoreRefs: [badgeRef]
362
+ });
363
+ return createPortal(
364
+ /* @__PURE__ */ jsxs(Fragment, { children: [
365
+ /* @__PURE__ */ jsx(
366
+ "div",
367
+ {
368
+ ref: overlayRef,
369
+ style: {
370
+ position: "fixed",
371
+ pointerEvents: "none",
372
+ zIndex: 99998,
373
+ border: `2px solid ${color}`,
374
+ borderRadius: "3px",
375
+ backgroundColor: colorWithAlpha(color, 0.08),
376
+ transition: "none",
377
+ display: "none"
378
+ }
379
+ }
380
+ ),
381
+ /* @__PURE__ */ jsx(
382
+ "div",
383
+ {
384
+ ref: tooltipRef,
385
+ style: {
386
+ position: "fixed",
387
+ pointerEvents: "none",
388
+ zIndex: 99999,
389
+ backgroundColor: color,
390
+ color: "white",
391
+ fontSize: "11px",
392
+ fontFamily: "monospace",
393
+ fontWeight: 600,
394
+ padding: "2px 7px",
395
+ borderRadius: "4px",
396
+ whiteSpace: "nowrap",
397
+ display: "none",
398
+ boxShadow: "0 2px 8px rgba(0,0,0,0.25)"
399
+ }
400
+ }
401
+ ),
402
+ showButton && /* @__PURE__ */ jsx(
403
+ "div",
404
+ {
405
+ ref: badgeRef,
406
+ style: {
407
+ position: "fixed",
408
+ zIndex: 2147483646,
409
+ width: "36px",
410
+ height: "36px",
411
+ transformOrigin: "center center"
412
+ },
413
+ children: /* @__PURE__ */ jsx(
414
+ "button",
415
+ {
416
+ onClick: (e) => {
417
+ e.stopPropagation();
418
+ toggle();
419
+ },
420
+ style: {
421
+ width: "36px",
422
+ height: "36px",
423
+ display: "flex",
424
+ alignItems: "center",
425
+ justifyContent: "center",
426
+ background: "rgba(0, 0, 0, 0.8)",
427
+ backdropFilter: "blur(48px)",
428
+ border: "none",
429
+ borderRadius: "50%",
430
+ boxShadow: enabled ? `0 0 0 1px ${color}, inset 0 0 0 1px ${colorWithAlpha(color, 0.4)}, 0 16px 32px -8px rgba(0, 0, 0, 0.24)` : "0 0 0 1px #171717, inset 0 0 0 1px hsla(0, 0%, 100%, 0.14), 0 16px 32px -8px rgba(0, 0, 0, 0.24)",
431
+ opacity: enabled ? 1 : 0.4,
432
+ transition: "opacity 200ms ease, box-shadow 200ms ease",
433
+ cursor: "pointer",
434
+ userSelect: "none",
435
+ padding: 0
436
+ },
437
+ onMouseEnter: (e) => {
438
+ e.currentTarget.style.opacity = "1";
439
+ },
440
+ onMouseLeave: (e) => {
441
+ if (!enabled) e.currentTarget.style.opacity = "0.4";
442
+ },
443
+ children: /* @__PURE__ */ jsxs(
444
+ "svg",
445
+ {
446
+ width: "20",
447
+ height: "20",
448
+ viewBox: "0 0 24 24",
449
+ fill: "none",
450
+ stroke: enabled ? color : "white",
451
+ strokeWidth: "2",
452
+ strokeLinecap: "round",
453
+ strokeLinejoin: "round",
454
+ style: {
455
+ transition: "stroke 200ms ease, transform 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)",
456
+ transform: enabled ? "rotate(90deg)" : "rotate(0deg)"
457
+ },
458
+ children: [
459
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10", strokeOpacity: "0.5" }),
460
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "4" }),
461
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "2", x2: "12", y2: "6" }),
462
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "18", x2: "12", y2: "22" }),
463
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "12", x2: "6", y2: "12" }),
464
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "12", x2: "22", y2: "12" })
465
+ ]
466
+ }
467
+ )
468
+ }
469
+ )
470
+ }
471
+ )
472
+ ] }),
473
+ document.body
474
+ );
475
+ }
476
+ var Xray = process.env.NODE_ENV === "development" ? XrayImpl : () => null;
477
+ export {
478
+ Xray
479
+ };
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/plugin.ts
21
+ var plugin_exports = {};
22
+ __export(plugin_exports, {
23
+ xrayPlugin: () => xrayPlugin
24
+ });
25
+ module.exports = __toCommonJS(plugin_exports);
26
+ var import_code_inspector_plugin = require("code-inspector-plugin");
27
+ function xrayPlugin(options) {
28
+ return (0, import_code_inspector_plugin.codeInspectorPlugin)({
29
+ bundler: options.bundler,
30
+ editor: options.editor ?? "code",
31
+ hotKeys: false,
32
+ showSwitch: false
33
+ });
34
+ }
35
+ // Annotate the CommonJS export names for ESM import in node:
36
+ 0 && (module.exports = {
37
+ xrayPlugin
38
+ });
@@ -0,0 +1,16 @@
1
+ interface XrayPluginOptions {
2
+ /** Which bundler you're using. Required. */
3
+ bundler: 'webpack' | 'vite' | 'turbopack' | 'rspack' | 'esbuild';
4
+ /** Editor to open files in. Default: 'code' (VS Code) */
5
+ editor?: string;
6
+ }
7
+ /**
8
+ * Build plugin that injects `data-insp-path` attributes on every DOM element.
9
+ * Wraps `code-inspector-plugin` with xray defaults.
10
+ *
11
+ * Returns a plugin object for webpack/vite/rspack/esbuild,
12
+ * or a rules object for turbopack (Next.js).
13
+ */
14
+ declare function xrayPlugin(options: XrayPluginOptions): any;
15
+
16
+ export { type XrayPluginOptions, xrayPlugin };
@@ -0,0 +1,16 @@
1
+ interface XrayPluginOptions {
2
+ /** Which bundler you're using. Required. */
3
+ bundler: 'webpack' | 'vite' | 'turbopack' | 'rspack' | 'esbuild';
4
+ /** Editor to open files in. Default: 'code' (VS Code) */
5
+ editor?: string;
6
+ }
7
+ /**
8
+ * Build plugin that injects `data-insp-path` attributes on every DOM element.
9
+ * Wraps `code-inspector-plugin` with xray defaults.
10
+ *
11
+ * Returns a plugin object for webpack/vite/rspack/esbuild,
12
+ * or a rules object for turbopack (Next.js).
13
+ */
14
+ declare function xrayPlugin(options: XrayPluginOptions): any;
15
+
16
+ export { type XrayPluginOptions, xrayPlugin };
@@ -0,0 +1,13 @@
1
+ // src/plugin.ts
2
+ import { codeInspectorPlugin } from "code-inspector-plugin";
3
+ function xrayPlugin(options) {
4
+ return codeInspectorPlugin({
5
+ bundler: options.bundler,
6
+ editor: options.editor ?? "code",
7
+ hotKeys: false,
8
+ showSwitch: false
9
+ });
10
+ }
11
+ export {
12
+ xrayPlugin
13
+ };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@stinsky/xray",
3
+ "version": "0.1.0",
4
+ "description": "React dev inspector — hover to see component names, click to open source in your editor. Works with React 19 + Turbopack.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.cjs"
12
+ },
13
+ "./plugin": {
14
+ "types": "./dist/plugin.d.ts",
15
+ "import": "./dist/plugin.mjs",
16
+ "require": "./dist/plugin.cjs"
17
+ }
18
+ },
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.mjs",
21
+ "types": "./dist/index.d.ts",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "dev": "tsup --watch",
28
+ "prepublishOnly": "tsup",
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "peerDependencies": {
32
+ "react": ">=18",
33
+ "react-dom": ">=18"
34
+ },
35
+ "dependencies": {
36
+ "code-inspector-plugin": "^0.19.1"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^25.5.0",
40
+ "@types/react": "^19.0.0",
41
+ "@types/react-dom": "^19.0.0",
42
+ "react": "^19.0.0",
43
+ "react-dom": "^19.0.0",
44
+ "tsup": "^8.4.0",
45
+ "typescript": "^5.7.0"
46
+ },
47
+ "keywords": [
48
+ "react",
49
+ "inspector",
50
+ "devtools",
51
+ "click-to-component",
52
+ "click-to-source",
53
+ "nextjs",
54
+ "turbopack",
55
+ "vite",
56
+ "webpack"
57
+ ],
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/stinsky/xray"
61
+ }
62
+ }