@linhey/react-debug-inspector 1.2.0 → 1.2.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/dist/index.mjs CHANGED
@@ -62,17 +62,30 @@ function babelPluginDebugLabel() {
62
62
  // src/runtime.ts
63
63
  function initInspector() {
64
64
  if (typeof window === "undefined") return;
65
+ const win = window;
66
+ const doc = document;
67
+ const scopedWindow = win;
68
+ scopedWindow.__reactDebugInspectorCleanup__?.();
65
69
  let isInspecting = false;
66
- let hasUserPosition = false;
67
- let isDragging = false;
68
- let pointerMoved = false;
69
- let dragOffsetX = 0;
70
- let dragOffsetY = 0;
71
- let dragStartX = 0;
72
- let dragStartY = 0;
73
70
  let overlay = null;
74
71
  let tooltip = null;
72
+ let actionMenu = null;
73
+ let latestContext = null;
74
+ let lastHoveredDebugEl = null;
75
+ let latestHoverEvent = null;
76
+ let pendingHoverFrame = false;
77
+ let anchorUpdatePending = false;
75
78
  const edgeOffset = 24;
79
+ const successColor = "#10b981";
80
+ const defaultOverlayBg = "rgba(14, 165, 233, 0.15)";
81
+ const defaultOverlayBorder = "#0ea5e9";
82
+ const actionButtons = {};
83
+ const scheduleFrame = (cb) => {
84
+ if (typeof win.requestAnimationFrame === "function") {
85
+ return win.requestAnimationFrame(cb);
86
+ }
87
+ return win.setTimeout(() => cb(Date.now()), 16);
88
+ };
76
89
  const toggleBtn = document.createElement("button");
77
90
  toggleBtn.innerHTML = "\u{1F3AF}";
78
91
  toggleBtn.title = "\u5F00\u542F\u7EC4\u4EF6\u5B9A\u4F4D\u5668";
@@ -95,9 +108,8 @@ function initInspector() {
95
108
  justify-content: center;
96
109
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
97
110
  `;
98
- document.body.appendChild(toggleBtn);
111
+ doc.body.appendChild(toggleBtn);
99
112
  const applyAnchor = (anchor) => {
100
- if (hasUserPosition) return;
101
113
  toggleBtn.style.top = anchor.startsWith("top") ? `${edgeOffset}px` : "";
102
114
  toggleBtn.style.bottom = anchor.startsWith("bottom") ? `${edgeOffset}px` : "";
103
115
  if (anchor.endsWith("left")) {
@@ -109,7 +121,6 @@ function initInspector() {
109
121
  toggleBtn.style.left = "";
110
122
  };
111
123
  const getVisibleDialogs = () => {
112
- if (typeof document === "undefined") return [];
113
124
  const candidates = Array.from(
114
125
  document.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]')
115
126
  );
@@ -120,18 +131,35 @@ function initInspector() {
120
131
  return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
121
132
  });
122
133
  };
134
+ const getPreferredHost = (dialogs) => {
135
+ if (dialogs.length === 0) return document.body;
136
+ const topDialog = dialogs[dialogs.length - 1];
137
+ const portalHost = topDialog.closest("[data-radix-portal]");
138
+ if (portalHost) return topDialog;
139
+ return document.body;
140
+ };
123
141
  const ensureToggleHost = (host) => {
124
142
  if (toggleBtn.parentElement !== host) {
125
143
  host.appendChild(toggleBtn);
126
144
  }
127
145
  };
146
+ const ensureToggleVisible = () => {
147
+ toggleBtn.removeAttribute("aria-hidden");
148
+ toggleBtn.removeAttribute("data-aria-hidden");
149
+ toggleBtn.removeAttribute("inert");
150
+ if ("inert" in toggleBtn) {
151
+ toggleBtn.inert = false;
152
+ }
153
+ toggleBtn.style.pointerEvents = "auto";
154
+ };
128
155
  const isIgnorableObstacle = (el) => {
129
- if (el === toggleBtn || el === overlay || el === tooltip) return true;
156
+ if (el === toggleBtn || el === overlay || el === tooltip || el === actionMenu) return true;
130
157
  if (el instanceof HTMLElement) {
131
158
  if (el.contains(toggleBtn) || toggleBtn.contains(el)) return true;
132
159
  if (overlay && el.contains(overlay)) return true;
133
160
  if (tooltip && el.contains(tooltip)) return true;
134
- if (el === document.body || el === document.documentElement) return true;
161
+ if (actionMenu && (el.contains(actionMenu) || actionMenu.contains(el))) return true;
162
+ if (el === doc.body || el === doc.documentElement) return true;
135
163
  if (el.getAttribute("data-inspector-ignore") === "true") return true;
136
164
  const style = window.getComputedStyle(el);
137
165
  if (style.pointerEvents === "none") return true;
@@ -141,7 +169,7 @@ function initInspector() {
141
169
  };
142
170
  const sampleAnchorObstructionScore = () => {
143
171
  const pickStack = typeof document.elementsFromPoint === "function" ? (x, y) => document.elementsFromPoint(x, y) : (x, y) => {
144
- const one = document.elementFromPoint?.(x, y);
172
+ const one = doc.elementFromPoint?.(x, y);
145
173
  return one ? [one] : [];
146
174
  };
147
175
  const rect = toggleBtn.getBoundingClientRect();
@@ -176,27 +204,283 @@ function initInspector() {
176
204
  applyAnchor(best);
177
205
  };
178
206
  const updateAnchorForDialogs = () => {
179
- if (typeof document === "undefined") return;
180
207
  const dialogs = getVisibleDialogs();
208
+ ensureToggleHost(getPreferredHost(dialogs));
209
+ ensureToggleVisible();
181
210
  if (dialogs.length > 0) {
182
- ensureToggleHost(dialogs[dialogs.length - 1]);
183
211
  pickBestAnchor(true);
184
212
  return;
185
213
  }
186
- ensureToggleHost(document.body);
187
214
  pickBestAnchor(false);
188
215
  };
189
- let anchorUpdatePending = false;
190
216
  const scheduleAnchorUpdate = () => {
191
- if (typeof window === "undefined") return;
192
217
  if (anchorUpdatePending) return;
193
218
  anchorUpdatePending = true;
194
- const schedule = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : (cb) => window.setTimeout(() => cb(Date.now()), 16);
195
- schedule(() => {
219
+ scheduleFrame(() => {
196
220
  anchorUpdatePending = false;
197
221
  updateAnchorForDialogs();
198
222
  });
199
223
  };
224
+ const formatDebugId = (debugId) => {
225
+ const parts = debugId.split(":");
226
+ if (parts.length === 4) {
227
+ const [filePath, componentName, tagName, line] = parts;
228
+ const fileName = filePath.split("/").pop() || filePath;
229
+ return `${fileName} \u203A ${componentName} \u203A ${tagName}:${line}`;
230
+ }
231
+ return debugId.replace(/:/g, " \u203A ");
232
+ };
233
+ const normalizeText = (value) => value.replace(/\s+/g, " ").trim();
234
+ const truncateText = (value, limit = 500) => {
235
+ if (value.length <= limit) return value;
236
+ return `${value.slice(0, limit - 3)}...`;
237
+ };
238
+ const suppressEvent = (event, { preventDefault = false, immediate = false } = {}) => {
239
+ if (preventDefault) {
240
+ event.preventDefault();
241
+ }
242
+ event.stopPropagation();
243
+ if (immediate) {
244
+ event.stopImmediatePropagation?.();
245
+ }
246
+ };
247
+ const extractTextContent = (target) => {
248
+ const ariaLabel = normalizeText(target.getAttribute("aria-label") || "");
249
+ if (ariaLabel) return truncateText(ariaLabel);
250
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
251
+ const inputValue = normalizeText(target.value || "");
252
+ if (inputValue) return truncateText(inputValue);
253
+ }
254
+ const alt = normalizeText(target.getAttribute("alt") || "");
255
+ if (alt) return truncateText(alt);
256
+ const title = normalizeText(target.getAttribute("title") || "");
257
+ if (title) return truncateText(title);
258
+ const textValue = normalizeText(target.innerText || target.textContent || "");
259
+ if (textValue) return truncateText(textValue);
260
+ return null;
261
+ };
262
+ const resolveImageTarget = (target) => {
263
+ const imageEl = target instanceof HTMLImageElement ? target : target.querySelector("img") || target.closest("img");
264
+ if (imageEl instanceof HTMLImageElement) {
265
+ const url = imageEl.currentSrc || imageEl.src;
266
+ if (!url) return null;
267
+ return {
268
+ url,
269
+ alt: imageEl.alt || "",
270
+ title: imageEl.title || "",
271
+ filename: url.split("/").pop()?.split("?")[0] || "",
272
+ source: "img"
273
+ };
274
+ }
275
+ let node = target;
276
+ while (node) {
277
+ const bg = window.getComputedStyle(node).backgroundImage;
278
+ const match = bg.match(/url\(["']?(.*?)["']?\)/);
279
+ if (match?.[1]) {
280
+ const url = match[1];
281
+ return {
282
+ url,
283
+ alt: node.getAttribute("aria-label") || "",
284
+ title: node.getAttribute("title") || "",
285
+ filename: url.split("/").pop()?.split("?")[0] || "",
286
+ source: "background"
287
+ };
288
+ }
289
+ node = node.parentElement;
290
+ }
291
+ return null;
292
+ };
293
+ const buildImageMetadataText = (image, debugId) => [
294
+ "[image]",
295
+ `url: ${image.url}`,
296
+ `alt: ${image.alt}`,
297
+ `title: ${image.title}`,
298
+ `filename: ${image.filename}`,
299
+ `debugId: ${debugId}`
300
+ ].join("\n");
301
+ const buildCopyAllPayload = (context) => {
302
+ const textValue = extractTextContent(context.debugEl);
303
+ const image = resolveImageTarget(context.debugEl);
304
+ const lines = [
305
+ "[debug]",
306
+ `id: ${context.debugId}`,
307
+ `display: ${formatDebugId(context.debugId)}`
308
+ ];
309
+ if (textValue) {
310
+ lines.push("", "[text]", `value: ${textValue}`);
311
+ }
312
+ if (image) {
313
+ lines.push("", "[image]", `url: ${image.url}`, `alt: ${image.alt}`, `title: ${image.title}`, `filename: ${image.filename}`);
314
+ }
315
+ return lines.join("\n");
316
+ };
317
+ const syncActionMenuVisibility = (context) => {
318
+ actionButtons.id && (actionButtons.id.style.display = "");
319
+ actionButtons.all && (actionButtons.all.style.display = "");
320
+ if (actionButtons.text) {
321
+ actionButtons.text.style.display = extractTextContent(context.debugEl) ? "" : "none";
322
+ }
323
+ if (actionButtons.image) {
324
+ actionButtons.image.style.display = resolveImageTarget(context.debugEl) ? "" : "none";
325
+ }
326
+ };
327
+ const setOverlayTone = (tone) => {
328
+ if (!overlay) return;
329
+ if (tone === "success") {
330
+ overlay.style.background = "rgba(16, 185, 129, 0.2)";
331
+ overlay.style.borderColor = successColor;
332
+ return;
333
+ }
334
+ overlay.style.background = "rgba(245, 158, 11, 0.18)";
335
+ overlay.style.borderColor = "#f59e0b";
336
+ };
337
+ const resetOverlayTone = () => {
338
+ if (!overlay) return;
339
+ overlay.style.background = defaultOverlayBg;
340
+ overlay.style.borderColor = defaultOverlayBorder;
341
+ };
342
+ const showCopyFeedback = (message, tone) => {
343
+ if (!tooltip) return;
344
+ tooltip.textContent = tone === "success" ? `\u2705 ${message}` : `\u26A0\uFE0F ${message}`;
345
+ tooltip.style.color = tone === "success" ? successColor : "#f59e0b";
346
+ tooltip.title = latestContext?.debugId || "";
347
+ setOverlayTone(tone);
348
+ };
349
+ const resetTooltipContent = (context) => {
350
+ if (!tooltip) return;
351
+ tooltip.textContent = formatDebugId(context.debugId);
352
+ tooltip.style.color = "#38bdf8";
353
+ tooltip.title = context.debugId;
354
+ resetOverlayTone();
355
+ };
356
+ const positionActionMenu = (context) => {
357
+ if (!tooltip || !actionMenu) return;
358
+ const rect = context.rect;
359
+ const menuWidth = actionMenu.offsetWidth || 248;
360
+ const menuHeight = actionMenu.offsetHeight || 40;
361
+ const tooltipRect = tooltip.getBoundingClientRect();
362
+ let left = tooltipRect.right + 8;
363
+ if (left + menuWidth > win.innerWidth - 8) {
364
+ left = Math.max(8, tooltipRect.left - menuWidth - 8);
365
+ }
366
+ let top = tooltipRect.top;
367
+ if (top + menuHeight > win.innerHeight - 8) {
368
+ top = Math.max(8, rect.bottom - menuHeight);
369
+ }
370
+ if (top < 8) {
371
+ top = Math.min(win.innerHeight - menuHeight - 8, rect.bottom + 8);
372
+ }
373
+ actionMenu.style.left = `${left}px`;
374
+ actionMenu.style.top = `${top}px`;
375
+ };
376
+ const showActionMenu = (context) => {
377
+ if (!actionMenu) return;
378
+ syncActionMenuVisibility(context);
379
+ actionMenu.style.display = "flex";
380
+ positionActionMenu(context);
381
+ };
382
+ const hideActionMenu = () => {
383
+ if (!actionMenu) return;
384
+ actionMenu.style.display = "none";
385
+ };
386
+ const copyText = async (value) => {
387
+ await navigator.clipboard.writeText(value);
388
+ };
389
+ const copyImageBinary = async (image, debugId) => {
390
+ if (image.url.startsWith("data:")) {
391
+ await copyText(buildImageMetadataText(image, debugId));
392
+ return "metadata";
393
+ }
394
+ const canWriteBinary = typeof navigator.clipboard?.write === "function" && typeof window.ClipboardItem === "function";
395
+ if (canWriteBinary) {
396
+ try {
397
+ const response = await win.fetch(image.url);
398
+ if (!response.ok) throw new Error(`failed:${response.status}`);
399
+ const blob = await response.blob();
400
+ if (!blob.type.startsWith("image/")) throw new Error("not-image");
401
+ const item = new window.ClipboardItem({ [blob.type]: blob });
402
+ await navigator.clipboard.write([item]);
403
+ return "binary";
404
+ } catch {
405
+ }
406
+ }
407
+ await copyText(buildImageMetadataText(image, debugId));
408
+ return "metadata";
409
+ };
410
+ const performCopyAction = async (action, context) => {
411
+ if (action === "id") {
412
+ await copyText(context.debugId);
413
+ showCopyFeedback("\u5DF2\u590D\u5236 Debug ID", "success");
414
+ return;
415
+ }
416
+ if (action === "text") {
417
+ const textValue = extractTextContent(context.debugEl);
418
+ if (!textValue) return;
419
+ await copyText(textValue);
420
+ showCopyFeedback("\u5DF2\u590D\u5236\u6587\u6848", "success");
421
+ return;
422
+ }
423
+ if (action === "image") {
424
+ const image = resolveImageTarget(context.debugEl);
425
+ if (!image) {
426
+ showCopyFeedback("\u672A\u627E\u5230\u56FE\u7247", "warning");
427
+ return;
428
+ }
429
+ const copyResult = await copyImageBinary(image, context.debugId);
430
+ showCopyFeedback(copyResult === "binary" ? "\u5DF2\u590D\u5236\u56FE\u7247" : "\u5DF2\u590D\u5236\u56FE\u7247\u4FE1\u606F", "success");
431
+ return;
432
+ }
433
+ await copyText(buildCopyAllPayload(context));
434
+ showCopyFeedback("\u5DF2\u590D\u5236\u5168\u90E8\u4FE1\u606F", "success");
435
+ };
436
+ const inspectByPointer = (event) => {
437
+ if (!isInspecting || !overlay || !tooltip) return;
438
+ const target = event.target;
439
+ if (!target) return;
440
+ if (target === toggleBtn || target === overlay || target === tooltip || target.closest('[data-inspector-ignore="true"]')) {
441
+ return;
442
+ }
443
+ const debugEl = target.closest("[data-debug]");
444
+ if (debugEl && debugEl === lastHoveredDebugEl && latestContext) {
445
+ showActionMenu(latestContext);
446
+ return;
447
+ }
448
+ if (!debugEl) {
449
+ overlay.style.display = "none";
450
+ tooltip.style.display = "none";
451
+ hideActionMenu();
452
+ latestContext = null;
453
+ lastHoveredDebugEl = null;
454
+ return;
455
+ }
456
+ const debugId = debugEl.getAttribute("data-debug") || "";
457
+ const rect = debugEl.getBoundingClientRect();
458
+ latestContext = { debugEl, debugId, rect };
459
+ lastHoveredDebugEl = debugEl;
460
+ overlay.style.display = "block";
461
+ overlay.style.top = `${rect.top}px`;
462
+ overlay.style.left = `${rect.left}px`;
463
+ overlay.style.width = `${rect.width}px`;
464
+ overlay.style.height = `${rect.height}px`;
465
+ tooltip.style.display = "block";
466
+ resetTooltipContent(latestContext);
467
+ const tooltipY = rect.top < 30 ? rect.bottom + 4 : rect.top - 28;
468
+ tooltip.style.top = `${tooltipY}px`;
469
+ tooltip.style.left = `${rect.left}px`;
470
+ showActionMenu(latestContext);
471
+ };
472
+ const stopInspecting = () => {
473
+ isInspecting = false;
474
+ latestContext = null;
475
+ lastHoveredDebugEl = null;
476
+ toggleBtn.style.transform = "scale(1)";
477
+ toggleBtn.style.background = "#0ea5e9";
478
+ document.body.style.cursor = "";
479
+ if (overlay) overlay.style.display = "none";
480
+ if (tooltip) tooltip.style.display = "none";
481
+ hideActionMenu();
482
+ resetOverlayTone();
483
+ };
200
484
  const dialogObserver = new MutationObserver(() => {
201
485
  scheduleAnchorUpdate();
202
486
  });
@@ -205,12 +489,12 @@ function initInspector() {
205
489
  position: fixed;
206
490
  pointer-events: none;
207
491
  z-index: 9999998;
208
- background: rgba(14, 165, 233, 0.15);
209
- border: 2px dashed #0ea5e9;
492
+ background: ${defaultOverlayBg};
493
+ border: 2px dashed ${defaultOverlayBorder};
210
494
  display: none;
211
495
  transition: all 0.1s ease-out;
212
496
  `;
213
- document.body.appendChild(overlay);
497
+ doc.body.appendChild(overlay);
214
498
  tooltip = document.createElement("div");
215
499
  tooltip.style.cssText = `
216
500
  position: fixed;
@@ -229,153 +513,140 @@ function initInspector() {
229
513
  transition: all 0.1s ease-out;
230
514
  cursor: help;
231
515
  `;
232
- document.body.appendChild(tooltip);
233
- dialogObserver.observe(document.body, {
234
- childList: true,
235
- subtree: true,
236
- attributes: true,
237
- attributeFilter: ["style", "class", "open", "aria-hidden"]
238
- });
239
- window.addEventListener("beforeunload", () => dialogObserver.disconnect(), { once: true });
240
- window.addEventListener("resize", scheduleAnchorUpdate);
241
- window.addEventListener("scroll", scheduleAnchorUpdate, true);
242
- updateAnchorForDialogs();
243
- const stopInspecting = () => {
244
- isInspecting = false;
245
- toggleBtn.style.transform = "scale(1)";
246
- toggleBtn.style.background = "#0ea5e9";
247
- document.body.style.cursor = "";
248
- overlay.style.display = "none";
249
- tooltip.style.display = "none";
250
- };
251
- const onPointerMove = (e) => {
252
- if (!isDragging) return;
253
- const deltaX = Math.abs(e.clientX - dragStartX);
254
- const deltaY = Math.abs(e.clientY - dragStartY);
255
- if (deltaX > 3 || deltaY > 3) {
256
- pointerMoved = true;
257
- }
258
- const left = Math.max(8, Math.min(window.innerWidth - 52, e.clientX - dragOffsetX));
259
- const top = Math.max(8, Math.min(window.innerHeight - 52, e.clientY - dragOffsetY));
260
- toggleBtn.style.left = `${left}px`;
261
- toggleBtn.style.top = `${top}px`;
262
- toggleBtn.style.right = "";
263
- toggleBtn.style.bottom = "";
264
- hasUserPosition = true;
265
- };
266
- const onPointerUp = () => {
267
- isDragging = false;
268
- window.removeEventListener("mousemove", onPointerMove);
269
- window.removeEventListener("mouseup", onPointerUp);
270
- };
271
- toggleBtn.addEventListener("mousedown", (e) => {
272
- const rect = toggleBtn.getBoundingClientRect();
273
- isDragging = true;
274
- pointerMoved = false;
275
- dragStartX = e.clientX;
276
- dragStartY = e.clientY;
277
- dragOffsetX = e.clientX - rect.left;
278
- dragOffsetY = e.clientY - rect.top;
279
- window.addEventListener("mousemove", onPointerMove);
280
- window.addEventListener("mouseup", onPointerUp);
281
- });
282
- toggleBtn.onclick = (e) => {
283
- if (pointerMoved) {
284
- e.preventDefault();
285
- e.stopPropagation();
286
- pointerMoved = false;
287
- return;
288
- }
516
+ doc.body.appendChild(tooltip);
517
+ actionMenu = document.createElement("div");
518
+ actionMenu.setAttribute("data-inspector-ignore", "true");
519
+ actionMenu.style.cssText = `
520
+ position: fixed;
521
+ z-index: 10000000;
522
+ display: none;
523
+ gap: 6px;
524
+ padding: 6px;
525
+ border-radius: 10px;
526
+ background: rgba(15, 23, 42, 0.96);
527
+ box-shadow: 0 10px 30px rgba(15, 23, 42, 0.28);
528
+ border: 1px solid rgba(148, 163, 184, 0.25);
529
+ align-items: center;
530
+ `;
531
+ const actionDefinitions = [
532
+ { action: "id", label: "\u590D\u5236 ID" },
533
+ { action: "text", label: "\u590D\u5236\u6587\u6848" },
534
+ { action: "image", label: "\u590D\u5236\u56FE\u7247" },
535
+ { action: "all", label: "\u5168\u90E8\u590D\u5236" }
536
+ ];
537
+ for (const definition of actionDefinitions) {
538
+ const button = document.createElement("button");
539
+ button.type = "button";
540
+ button.textContent = definition.label;
541
+ button.dataset.inspectorIgnore = "true";
542
+ button.dataset.inspectorAction = definition.action;
543
+ button.style.cssText = `
544
+ border: 0;
545
+ border-radius: 8px;
546
+ padding: 6px 10px;
547
+ font-size: 12px;
548
+ font-weight: 600;
549
+ color: #e2e8f0;
550
+ background: rgba(30, 41, 59, 0.95);
551
+ cursor: pointer;
552
+ white-space: nowrap;
553
+ `;
554
+ button.addEventListener("click", async (event) => {
555
+ suppressEvent(event, { preventDefault: true });
556
+ if (!latestContext) return;
557
+ await performCopyAction(definition.action, latestContext);
558
+ });
559
+ actionButtons[definition.action] = button;
560
+ actionMenu.appendChild(button);
561
+ }
562
+ doc.body.appendChild(actionMenu);
563
+ const stopTogglePropagation = (event) => {
564
+ suppressEvent(event);
565
+ };
566
+ const shieldedEvents = ["pointerdown", "pointerup", "mousedown", "mouseup", "click", "touchstart", "touchend"];
567
+ for (const eventName of shieldedEvents) {
568
+ toggleBtn.addEventListener(eventName, stopTogglePropagation);
569
+ }
570
+ const handleToggleClick = (event) => {
571
+ suppressEvent(event);
289
572
  isInspecting = !isInspecting;
290
573
  if (isInspecting) {
291
574
  toggleBtn.style.transform = "scale(0.9)";
292
575
  toggleBtn.style.background = "#ef4444";
293
- document.body.style.cursor = "crosshair";
294
- } else {
295
- stopInspecting();
296
- }
297
- };
298
- const formatDebugId = (debugId) => {
299
- const parts = debugId.split(":");
300
- if (parts.length === 4) {
301
- const [filePath, componentName, tagName, line] = parts;
302
- const fileName = filePath.split("/").pop() || filePath;
303
- return `${fileName} \u203A ${componentName} \u203A ${tagName}:${line}`;
304
- }
305
- return debugId.replace(/:/g, " \u203A ");
306
- };
307
- const inspectByPointer = (e) => {
308
- if (!isInspecting) return;
309
- const target = e.target;
310
- if (target === toggleBtn || target === overlay || target === tooltip) return;
311
- const debugEl = target.closest("[data-debug]");
312
- if (debugEl && debugEl === lastHoveredDebugEl) return;
313
- if (debugEl) {
314
- const debugId = debugEl.getAttribute("data-debug") || "";
315
- const rect = debugEl.getBoundingClientRect();
316
- overlay.style.display = "block";
317
- overlay.style.top = rect.top + "px";
318
- overlay.style.left = rect.left + "px";
319
- overlay.style.width = rect.width + "px";
320
- overlay.style.height = rect.height + "px";
321
- tooltip.style.display = "block";
322
- tooltip.textContent = formatDebugId(debugId);
323
- tooltip.style.color = "#38bdf8";
324
- tooltip.title = debugId;
325
- const tooltipY = rect.top < 30 ? rect.bottom + 4 : rect.top - 28;
326
- tooltip.style.top = tooltipY + "px";
327
- tooltip.style.left = rect.left + "px";
328
- lastHoveredDebugEl = debugEl;
329
- } else {
330
- overlay.style.display = "none";
331
- tooltip.style.display = "none";
332
- lastHoveredDebugEl = null;
576
+ doc.body.style.cursor = "crosshair";
577
+ return;
333
578
  }
579
+ stopInspecting();
334
580
  };
335
- let pendingHoverFrame = false;
336
- let latestHoverEvent = null;
337
- let lastHoveredDebugEl = null;
338
- document.addEventListener("mousemove", (e) => {
581
+ toggleBtn.onclick = handleToggleClick;
582
+ const handleMouseMove = (event) => {
339
583
  if (!isInspecting) return;
340
- latestHoverEvent = e;
584
+ latestHoverEvent = event;
341
585
  if (pendingHoverFrame) return;
342
586
  pendingHoverFrame = true;
343
- const schedule = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : (cb) => window.setTimeout(() => cb(Date.now()), 16);
344
- schedule(() => {
587
+ scheduleFrame(() => {
345
588
  pendingHoverFrame = false;
346
589
  if (!latestHoverEvent) return;
347
590
  inspectByPointer(latestHoverEvent);
348
591
  });
592
+ };
593
+ const handleWindowClick = (event) => {
594
+ if (!isInspecting) return;
595
+ const target = event.target;
596
+ if (!target || target === toggleBtn) return;
597
+ if (target.closest('[data-inspector-ignore="true"]')) {
598
+ return;
599
+ }
600
+ suppressEvent(event, { preventDefault: true, immediate: true });
601
+ const debugEl = target.closest("[data-debug]");
602
+ if (!debugEl) {
603
+ stopInspecting();
604
+ return;
605
+ }
606
+ const debugId = debugEl.getAttribute("data-debug");
607
+ if (!debugId) return;
608
+ copyText(debugId).then(() => {
609
+ showCopyFeedback("Copied!", "success");
610
+ win.setTimeout(stopInspecting, 600);
611
+ });
612
+ };
613
+ const handleKeyDown = (event) => {
614
+ if (event.key === "Escape" && isInspecting) stopInspecting();
615
+ };
616
+ const handleBeforeUnload = () => {
617
+ dialogObserver.disconnect();
618
+ };
619
+ dialogObserver.observe(doc.body, {
620
+ childList: true,
621
+ subtree: true,
622
+ attributes: true,
623
+ attributeFilter: ["style", "class", "open", "aria-hidden", "data-aria-hidden", "inert"]
349
624
  });
350
- window.addEventListener(
351
- "click",
352
- (e) => {
353
- if (!isInspecting) return;
354
- const target = e.target;
355
- if (target === toggleBtn) return;
356
- e.preventDefault();
357
- e.stopPropagation();
358
- const debugEl = target.closest("[data-debug]");
359
- if (debugEl) {
360
- const debugId = debugEl.getAttribute("data-debug");
361
- if (debugId) {
362
- navigator.clipboard.writeText(debugId).then(() => {
363
- tooltip.textContent = "\u2705 Copied!";
364
- tooltip.style.color = "#10b981";
365
- overlay.style.background = "rgba(16, 185, 129, 0.2)";
366
- overlay.style.borderColor = "#10b981";
367
- setTimeout(stopInspecting, 600);
368
- });
369
- }
370
- } else {
371
- stopInspecting();
372
- }
373
- },
374
- { capture: true }
375
- );
376
- window.addEventListener("keydown", (e) => {
377
- if (e.key === "Escape" && isInspecting) stopInspecting();
378
- });
625
+ win.addEventListener("beforeunload", handleBeforeUnload, { once: true });
626
+ win.addEventListener("resize", scheduleAnchorUpdate);
627
+ win.addEventListener("scroll", scheduleAnchorUpdate, true);
628
+ doc.addEventListener("mousemove", handleMouseMove);
629
+ win.addEventListener("click", handleWindowClick, { capture: true });
630
+ win.addEventListener("keydown", handleKeyDown);
631
+ updateAnchorForDialogs();
632
+ scopedWindow.__reactDebugInspectorCleanup__ = () => {
633
+ dialogObserver.disconnect();
634
+ win.removeEventListener("beforeunload", handleBeforeUnload);
635
+ win.removeEventListener("resize", scheduleAnchorUpdate);
636
+ win.removeEventListener("scroll", scheduleAnchorUpdate, true);
637
+ doc.removeEventListener("mousemove", handleMouseMove);
638
+ win.removeEventListener("click", handleWindowClick, { capture: true });
639
+ win.removeEventListener("keydown", handleKeyDown);
640
+ for (const eventName of shieldedEvents) {
641
+ toggleBtn.removeEventListener(eventName, stopTogglePropagation);
642
+ }
643
+ toggleBtn.onclick = null;
644
+ overlay?.remove();
645
+ tooltip?.remove();
646
+ actionMenu?.remove();
647
+ toggleBtn.remove();
648
+ delete scopedWindow.__reactDebugInspectorCleanup__;
649
+ };
379
650
  }
380
651
 
381
652
  // src/index.ts