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