@linhey/react-debug-inspector 1.2.0 → 1.2.2

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