@linhey/react-debug-inspector 1.0.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
@@ -2,28 +2,42 @@
2
2
  function babelPluginDebugLabel() {
3
3
  return {
4
4
  visitor: {
5
- FunctionDeclaration(path) {
6
- const name = path.node.id?.name;
7
- if (!name || name[0] !== name[0].toUpperCase()) return;
8
- injectAllJSX(path, name);
9
- },
10
- VariableDeclarator(path) {
11
- const name = path.node.id?.name;
12
- if (!name || name[0] !== name[0].toUpperCase()) return;
13
- const init = path.node.init;
14
- if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
15
- injectAllJSX(path.get("init"), name);
16
- }
5
+ Program(programPath, state) {
6
+ const filename = state.file.opts.filename || state.filename || "";
7
+ const cwd = state.cwd || (typeof process !== "undefined" ? process.cwd() : "");
8
+ const getRelativePath = (absolutePath) => {
9
+ if (!absolutePath) return "unknown";
10
+ if (absolutePath.startsWith(cwd)) {
11
+ return absolutePath.slice(cwd.length + 1);
12
+ }
13
+ return absolutePath;
14
+ };
15
+ const relativePath = getRelativePath(filename);
16
+ programPath.traverse({
17
+ FunctionDeclaration(path) {
18
+ const name = path.node.id?.name;
19
+ if (!name || name[0] !== name[0].toUpperCase()) return;
20
+ injectAllJSX(path, name, relativePath);
21
+ },
22
+ VariableDeclarator(path) {
23
+ const name = path.node.id?.name;
24
+ if (!name || name[0] !== name[0].toUpperCase()) return;
25
+ const init = path.node.init;
26
+ if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
27
+ injectAllJSX(path.get("init"), name, relativePath);
28
+ }
29
+ }
30
+ });
17
31
  }
18
32
  }
19
33
  };
20
- function injectAllJSX(path, componentName) {
34
+ function injectAllJSX(path, componentName, filePath) {
21
35
  path.traverse({
22
36
  JSXElement(jsxPath) {
23
37
  const { openingElement } = jsxPath.node;
24
38
  const tagName = getTagName(openingElement.name);
25
39
  const line = openingElement.loc?.start.line || "0";
26
- const debugId = `${componentName}:${tagName}:${line}`;
40
+ const debugId = `${filePath}:${componentName}:${tagName}:${line}`;
27
41
  const alreadyHas = openingElement.attributes.some(
28
42
  (attr) => attr.type === "JSXAttribute" && attr.name?.name === "data-debug"
29
43
  );
@@ -48,7 +62,30 @@ function babelPluginDebugLabel() {
48
62
  // src/runtime.ts
49
63
  function initInspector() {
50
64
  if (typeof window === "undefined") return;
65
+ const win = window;
66
+ const doc = document;
67
+ const scopedWindow = win;
68
+ scopedWindow.__reactDebugInspectorCleanup__?.();
51
69
  let isInspecting = false;
70
+ let overlay = null;
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
+ 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
+ };
52
89
  const toggleBtn = document.createElement("button");
53
90
  toggleBtn.innerHTML = "\u{1F3AF}";
54
91
  toggleBtn.title = "\u5F00\u542F\u7EC4\u4EF6\u5B9A\u4F4D\u5668";
@@ -71,22 +108,397 @@ function initInspector() {
71
108
  justify-content: center;
72
109
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
73
110
  `;
74
- document.body.appendChild(toggleBtn);
75
- const overlay = document.createElement("div");
111
+ doc.body.appendChild(toggleBtn);
112
+ const applyAnchor = (anchor) => {
113
+ toggleBtn.style.top = anchor.startsWith("top") ? `${edgeOffset}px` : "";
114
+ toggleBtn.style.bottom = anchor.startsWith("bottom") ? `${edgeOffset}px` : "";
115
+ if (anchor.endsWith("left")) {
116
+ toggleBtn.style.left = `${edgeOffset}px`;
117
+ toggleBtn.style.right = "";
118
+ return;
119
+ }
120
+ toggleBtn.style.right = `${edgeOffset}px`;
121
+ toggleBtn.style.left = "";
122
+ };
123
+ const getVisibleDialogs = () => {
124
+ const candidates = Array.from(
125
+ document.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]')
126
+ );
127
+ return candidates.filter((node) => {
128
+ if (node.getAttribute("aria-hidden") === "true") return false;
129
+ if (node.getAttribute("data-aria-hidden") === "true") return false;
130
+ const style = window.getComputedStyle(node);
131
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
132
+ });
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
+ };
141
+ const ensureToggleHost = (host) => {
142
+ if (toggleBtn.parentElement !== host) {
143
+ host.appendChild(toggleBtn);
144
+ }
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
+ };
155
+ const isIgnorableObstacle = (el) => {
156
+ if (el === toggleBtn || el === overlay || el === tooltip || el === actionMenu) return true;
157
+ if (el instanceof HTMLElement) {
158
+ if (el.contains(toggleBtn) || toggleBtn.contains(el)) return true;
159
+ if (overlay && el.contains(overlay)) return true;
160
+ if (tooltip && el.contains(tooltip)) return true;
161
+ if (actionMenu && (el.contains(actionMenu) || actionMenu.contains(el))) return true;
162
+ if (el === doc.body || el === doc.documentElement) return true;
163
+ if (el.getAttribute("data-inspector-ignore") === "true") return true;
164
+ const style = window.getComputedStyle(el);
165
+ if (style.pointerEvents === "none") return true;
166
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return true;
167
+ }
168
+ return false;
169
+ };
170
+ const sampleAnchorObstructionScore = () => {
171
+ const pickStack = typeof document.elementsFromPoint === "function" ? (x, y) => document.elementsFromPoint(x, y) : (x, y) => {
172
+ const one = doc.elementFromPoint?.(x, y);
173
+ return one ? [one] : [];
174
+ };
175
+ const rect = toggleBtn.getBoundingClientRect();
176
+ const points = [
177
+ [rect.left + rect.width / 2, rect.top + rect.height / 2],
178
+ [rect.left + 2, rect.top + 2],
179
+ [rect.right - 2, rect.top + 2],
180
+ [rect.left + 2, rect.bottom - 2],
181
+ [rect.right - 2, rect.bottom - 2]
182
+ ];
183
+ let score = 0;
184
+ for (const [x, y] of points) {
185
+ const stack = pickStack(x, y);
186
+ const blocker = stack.find((el) => !isIgnorableObstacle(el));
187
+ if (blocker) score += 1;
188
+ }
189
+ return score;
190
+ };
191
+ const pickBestAnchor = (preferLeft = false) => {
192
+ const candidates = preferLeft ? ["bottom-left", "top-left", "bottom-right", "top-right"] : ["bottom-right", "top-right", "bottom-left", "top-left"];
193
+ let best = candidates[0];
194
+ let bestScore = Number.POSITIVE_INFINITY;
195
+ for (const anchor of candidates) {
196
+ applyAnchor(anchor);
197
+ const score = sampleAnchorObstructionScore();
198
+ if (score < bestScore) {
199
+ bestScore = score;
200
+ best = anchor;
201
+ }
202
+ if (score === 0) break;
203
+ }
204
+ applyAnchor(best);
205
+ };
206
+ const updateAnchorForDialogs = () => {
207
+ const dialogs = getVisibleDialogs();
208
+ ensureToggleHost(getPreferredHost(dialogs));
209
+ ensureToggleVisible();
210
+ if (dialogs.length > 0) {
211
+ pickBestAnchor(true);
212
+ return;
213
+ }
214
+ pickBestAnchor(false);
215
+ };
216
+ const scheduleAnchorUpdate = () => {
217
+ if (anchorUpdatePending) return;
218
+ anchorUpdatePending = true;
219
+ scheduleFrame(() => {
220
+ anchorUpdatePending = false;
221
+ updateAnchorForDialogs();
222
+ });
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
+ };
484
+ const dialogObserver = new MutationObserver(() => {
485
+ scheduleAnchorUpdate();
486
+ });
487
+ overlay = document.createElement("div");
76
488
  overlay.style.cssText = `
77
489
  position: fixed;
78
490
  pointer-events: none;
79
491
  z-index: 9999998;
80
- background: rgba(14, 165, 233, 0.15);
81
- border: 2px dashed #0ea5e9;
492
+ background: ${defaultOverlayBg};
493
+ border: 2px dashed ${defaultOverlayBorder};
82
494
  display: none;
83
495
  transition: all 0.1s ease-out;
84
496
  `;
85
- document.body.appendChild(overlay);
86
- const tooltip = document.createElement("div");
497
+ doc.body.appendChild(overlay);
498
+ tooltip = document.createElement("div");
87
499
  tooltip.style.cssText = `
88
500
  position: fixed;
89
- pointer-events: none;
501
+ pointer-events: auto;
90
502
  z-index: 9999999;
91
503
  background: #1e293b;
92
504
  color: #38bdf8;
@@ -99,79 +511,142 @@ function initInspector() {
99
511
  display: none;
100
512
  white-space: nowrap;
101
513
  transition: all 0.1s ease-out;
514
+ cursor: help;
102
515
  `;
103
- document.body.appendChild(tooltip);
104
- const stopInspecting = () => {
105
- isInspecting = false;
106
- toggleBtn.style.transform = "scale(1)";
107
- toggleBtn.style.background = "#0ea5e9";
108
- document.body.style.cursor = "";
109
- overlay.style.display = "none";
110
- tooltip.style.display = "none";
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);
111
565
  };
112
- toggleBtn.onclick = () => {
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);
113
572
  isInspecting = !isInspecting;
114
573
  if (isInspecting) {
115
574
  toggleBtn.style.transform = "scale(0.9)";
116
575
  toggleBtn.style.background = "#ef4444";
117
- document.body.style.cursor = "crosshair";
118
- } else {
119
- stopInspecting();
576
+ doc.body.style.cursor = "crosshair";
577
+ return;
120
578
  }
579
+ stopInspecting();
580
+ };
581
+ toggleBtn.onclick = handleToggleClick;
582
+ const handleMouseMove = (event) => {
583
+ if (!isInspecting) return;
584
+ latestHoverEvent = event;
585
+ if (pendingHoverFrame) return;
586
+ pendingHoverFrame = true;
587
+ scheduleFrame(() => {
588
+ pendingHoverFrame = false;
589
+ if (!latestHoverEvent) return;
590
+ inspectByPointer(latestHoverEvent);
591
+ });
121
592
  };
122
- window.addEventListener("mousemove", (e) => {
593
+ const handleWindowClick = (event) => {
123
594
  if (!isInspecting) return;
124
- const target = e.target;
125
- if (target === toggleBtn || target === overlay || target === tooltip) 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 });
126
601
  const debugEl = target.closest("[data-debug]");
127
- if (debugEl) {
128
- const debugId = debugEl.getAttribute("data-debug") || "";
129
- const rect = debugEl.getBoundingClientRect();
130
- overlay.style.display = "block";
131
- overlay.style.top = rect.top + "px";
132
- overlay.style.left = rect.left + "px";
133
- overlay.style.width = rect.width + "px";
134
- overlay.style.height = rect.height + "px";
135
- tooltip.style.display = "block";
136
- tooltip.textContent = debugId;
137
- tooltip.style.color = "#38bdf8";
138
- const tooltipY = rect.top < 30 ? rect.bottom + 4 : rect.top - 28;
139
- tooltip.style.top = tooltipY + "px";
140
- tooltip.style.left = rect.left + "px";
141
- } else {
142
- overlay.style.display = "none";
143
- tooltip.style.display = "none";
602
+ if (!debugEl) {
603
+ stopInspecting();
604
+ return;
144
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"]
145
624
  });
146
- window.addEventListener(
147
- "click",
148
- (e) => {
149
- if (!isInspecting) return;
150
- const target = e.target;
151
- if (target === toggleBtn) return;
152
- e.preventDefault();
153
- e.stopPropagation();
154
- const debugEl = target.closest("[data-debug]");
155
- if (debugEl) {
156
- const debugId = debugEl.getAttribute("data-debug");
157
- if (debugId) {
158
- navigator.clipboard.writeText(debugId).then(() => {
159
- tooltip.textContent = "\u2705 Copied!";
160
- tooltip.style.color = "#10b981";
161
- overlay.style.background = "rgba(16, 185, 129, 0.2)";
162
- overlay.style.borderColor = "#10b981";
163
- setTimeout(stopInspecting, 600);
164
- });
165
- }
166
- } else {
167
- stopInspecting();
168
- }
169
- },
170
- { capture: true }
171
- );
172
- window.addEventListener("keydown", (e) => {
173
- if (e.key === "Escape" && isInspecting) stopInspecting();
174
- });
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
+ };
175
650
  }
176
651
 
177
652
  // src/index.ts