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