@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.js CHANGED
@@ -29,28 +29,42 @@ module.exports = __toCommonJS(index_exports);
29
29
  function babelPluginDebugLabel() {
30
30
  return {
31
31
  visitor: {
32
- FunctionDeclaration(path) {
33
- const name = path.node.id?.name;
34
- if (!name || name[0] !== name[0].toUpperCase()) return;
35
- injectAllJSX(path, name);
36
- },
37
- VariableDeclarator(path) {
38
- const name = path.node.id?.name;
39
- if (!name || name[0] !== name[0].toUpperCase()) return;
40
- const init = path.node.init;
41
- if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
42
- injectAllJSX(path.get("init"), name);
43
- }
32
+ Program(programPath, state) {
33
+ const filename = state.file.opts.filename || state.filename || "";
34
+ const cwd = state.cwd || (typeof process !== "undefined" ? process.cwd() : "");
35
+ const getRelativePath = (absolutePath) => {
36
+ if (!absolutePath) return "unknown";
37
+ if (absolutePath.startsWith(cwd)) {
38
+ return absolutePath.slice(cwd.length + 1);
39
+ }
40
+ return absolutePath;
41
+ };
42
+ const relativePath = getRelativePath(filename);
43
+ programPath.traverse({
44
+ FunctionDeclaration(path) {
45
+ const name = path.node.id?.name;
46
+ if (!name || name[0] !== name[0].toUpperCase()) return;
47
+ injectAllJSX(path, name, relativePath);
48
+ },
49
+ VariableDeclarator(path) {
50
+ const name = path.node.id?.name;
51
+ if (!name || name[0] !== name[0].toUpperCase()) return;
52
+ const init = path.node.init;
53
+ if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
54
+ injectAllJSX(path.get("init"), name, relativePath);
55
+ }
56
+ }
57
+ });
44
58
  }
45
59
  }
46
60
  };
47
- function injectAllJSX(path, componentName) {
61
+ function injectAllJSX(path, componentName, filePath) {
48
62
  path.traverse({
49
63
  JSXElement(jsxPath) {
50
64
  const { openingElement } = jsxPath.node;
51
65
  const tagName = getTagName(openingElement.name);
52
66
  const line = openingElement.loc?.start.line || "0";
53
- const debugId = `${componentName}:${tagName}:${line}`;
67
+ const debugId = `${filePath}:${componentName}:${tagName}:${line}`;
54
68
  const alreadyHas = openingElement.attributes.some(
55
69
  (attr) => attr.type === "JSXAttribute" && attr.name?.name === "data-debug"
56
70
  );
@@ -75,7 +89,30 @@ function babelPluginDebugLabel() {
75
89
  // src/runtime.ts
76
90
  function initInspector() {
77
91
  if (typeof window === "undefined") return;
92
+ const win = window;
93
+ const doc = document;
94
+ const scopedWindow = win;
95
+ scopedWindow.__reactDebugInspectorCleanup__?.();
78
96
  let isInspecting = false;
97
+ let overlay = null;
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
+ 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
+ };
79
116
  const toggleBtn = document.createElement("button");
80
117
  toggleBtn.innerHTML = "\u{1F3AF}";
81
118
  toggleBtn.title = "\u5F00\u542F\u7EC4\u4EF6\u5B9A\u4F4D\u5668";
@@ -98,22 +135,397 @@ function initInspector() {
98
135
  justify-content: center;
99
136
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
100
137
  `;
101
- document.body.appendChild(toggleBtn);
102
- const overlay = document.createElement("div");
138
+ doc.body.appendChild(toggleBtn);
139
+ const applyAnchor = (anchor) => {
140
+ toggleBtn.style.top = anchor.startsWith("top") ? `${edgeOffset}px` : "";
141
+ toggleBtn.style.bottom = anchor.startsWith("bottom") ? `${edgeOffset}px` : "";
142
+ if (anchor.endsWith("left")) {
143
+ toggleBtn.style.left = `${edgeOffset}px`;
144
+ toggleBtn.style.right = "";
145
+ return;
146
+ }
147
+ toggleBtn.style.right = `${edgeOffset}px`;
148
+ toggleBtn.style.left = "";
149
+ };
150
+ const getVisibleDialogs = () => {
151
+ const candidates = Array.from(
152
+ document.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]')
153
+ );
154
+ return candidates.filter((node) => {
155
+ if (node.getAttribute("aria-hidden") === "true") return false;
156
+ if (node.getAttribute("data-aria-hidden") === "true") return false;
157
+ const style = window.getComputedStyle(node);
158
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
159
+ });
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
+ };
168
+ const ensureToggleHost = (host) => {
169
+ if (toggleBtn.parentElement !== host) {
170
+ host.appendChild(toggleBtn);
171
+ }
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
+ };
182
+ const isIgnorableObstacle = (el) => {
183
+ if (el === toggleBtn || el === overlay || el === tooltip || el === actionMenu) return true;
184
+ if (el instanceof HTMLElement) {
185
+ if (el.contains(toggleBtn) || toggleBtn.contains(el)) return true;
186
+ if (overlay && el.contains(overlay)) return true;
187
+ if (tooltip && el.contains(tooltip)) return true;
188
+ if (actionMenu && (el.contains(actionMenu) || actionMenu.contains(el))) return true;
189
+ if (el === doc.body || el === doc.documentElement) return true;
190
+ if (el.getAttribute("data-inspector-ignore") === "true") return true;
191
+ const style = window.getComputedStyle(el);
192
+ if (style.pointerEvents === "none") return true;
193
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return true;
194
+ }
195
+ return false;
196
+ };
197
+ const sampleAnchorObstructionScore = () => {
198
+ const pickStack = typeof document.elementsFromPoint === "function" ? (x, y) => document.elementsFromPoint(x, y) : (x, y) => {
199
+ const one = doc.elementFromPoint?.(x, y);
200
+ return one ? [one] : [];
201
+ };
202
+ const rect = toggleBtn.getBoundingClientRect();
203
+ const points = [
204
+ [rect.left + rect.width / 2, rect.top + rect.height / 2],
205
+ [rect.left + 2, rect.top + 2],
206
+ [rect.right - 2, rect.top + 2],
207
+ [rect.left + 2, rect.bottom - 2],
208
+ [rect.right - 2, rect.bottom - 2]
209
+ ];
210
+ let score = 0;
211
+ for (const [x, y] of points) {
212
+ const stack = pickStack(x, y);
213
+ const blocker = stack.find((el) => !isIgnorableObstacle(el));
214
+ if (blocker) score += 1;
215
+ }
216
+ return score;
217
+ };
218
+ const pickBestAnchor = (preferLeft = false) => {
219
+ const candidates = preferLeft ? ["bottom-left", "top-left", "bottom-right", "top-right"] : ["bottom-right", "top-right", "bottom-left", "top-left"];
220
+ let best = candidates[0];
221
+ let bestScore = Number.POSITIVE_INFINITY;
222
+ for (const anchor of candidates) {
223
+ applyAnchor(anchor);
224
+ const score = sampleAnchorObstructionScore();
225
+ if (score < bestScore) {
226
+ bestScore = score;
227
+ best = anchor;
228
+ }
229
+ if (score === 0) break;
230
+ }
231
+ applyAnchor(best);
232
+ };
233
+ const updateAnchorForDialogs = () => {
234
+ const dialogs = getVisibleDialogs();
235
+ ensureToggleHost(getPreferredHost(dialogs));
236
+ ensureToggleVisible();
237
+ if (dialogs.length > 0) {
238
+ pickBestAnchor(true);
239
+ return;
240
+ }
241
+ pickBestAnchor(false);
242
+ };
243
+ const scheduleAnchorUpdate = () => {
244
+ if (anchorUpdatePending) return;
245
+ anchorUpdatePending = true;
246
+ scheduleFrame(() => {
247
+ anchorUpdatePending = false;
248
+ updateAnchorForDialogs();
249
+ });
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
+ };
511
+ const dialogObserver = new MutationObserver(() => {
512
+ scheduleAnchorUpdate();
513
+ });
514
+ overlay = document.createElement("div");
103
515
  overlay.style.cssText = `
104
516
  position: fixed;
105
517
  pointer-events: none;
106
518
  z-index: 9999998;
107
- background: rgba(14, 165, 233, 0.15);
108
- border: 2px dashed #0ea5e9;
519
+ background: ${defaultOverlayBg};
520
+ border: 2px dashed ${defaultOverlayBorder};
109
521
  display: none;
110
522
  transition: all 0.1s ease-out;
111
523
  `;
112
- document.body.appendChild(overlay);
113
- const tooltip = document.createElement("div");
524
+ doc.body.appendChild(overlay);
525
+ tooltip = document.createElement("div");
114
526
  tooltip.style.cssText = `
115
527
  position: fixed;
116
- pointer-events: none;
528
+ pointer-events: auto;
117
529
  z-index: 9999999;
118
530
  background: #1e293b;
119
531
  color: #38bdf8;
@@ -126,79 +538,142 @@ function initInspector() {
126
538
  display: none;
127
539
  white-space: nowrap;
128
540
  transition: all 0.1s ease-out;
541
+ cursor: help;
129
542
  `;
130
- document.body.appendChild(tooltip);
131
- const stopInspecting = () => {
132
- isInspecting = false;
133
- toggleBtn.style.transform = "scale(1)";
134
- toggleBtn.style.background = "#0ea5e9";
135
- document.body.style.cursor = "";
136
- overlay.style.display = "none";
137
- tooltip.style.display = "none";
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);
138
592
  };
139
- toggleBtn.onclick = () => {
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);
140
599
  isInspecting = !isInspecting;
141
600
  if (isInspecting) {
142
601
  toggleBtn.style.transform = "scale(0.9)";
143
602
  toggleBtn.style.background = "#ef4444";
144
- document.body.style.cursor = "crosshair";
145
- } else {
146
- stopInspecting();
603
+ doc.body.style.cursor = "crosshair";
604
+ return;
147
605
  }
606
+ stopInspecting();
607
+ };
608
+ toggleBtn.onclick = handleToggleClick;
609
+ const handleMouseMove = (event) => {
610
+ if (!isInspecting) return;
611
+ latestHoverEvent = event;
612
+ if (pendingHoverFrame) return;
613
+ pendingHoverFrame = true;
614
+ scheduleFrame(() => {
615
+ pendingHoverFrame = false;
616
+ if (!latestHoverEvent) return;
617
+ inspectByPointer(latestHoverEvent);
618
+ });
148
619
  };
149
- window.addEventListener("mousemove", (e) => {
620
+ const handleWindowClick = (event) => {
150
621
  if (!isInspecting) return;
151
- const target = e.target;
152
- if (target === toggleBtn || target === overlay || target === tooltip) 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 });
153
628
  const debugEl = target.closest("[data-debug]");
154
- if (debugEl) {
155
- const debugId = debugEl.getAttribute("data-debug") || "";
156
- const rect = debugEl.getBoundingClientRect();
157
- overlay.style.display = "block";
158
- overlay.style.top = rect.top + "px";
159
- overlay.style.left = rect.left + "px";
160
- overlay.style.width = rect.width + "px";
161
- overlay.style.height = rect.height + "px";
162
- tooltip.style.display = "block";
163
- tooltip.textContent = debugId;
164
- tooltip.style.color = "#38bdf8";
165
- const tooltipY = rect.top < 30 ? rect.bottom + 4 : rect.top - 28;
166
- tooltip.style.top = tooltipY + "px";
167
- tooltip.style.left = rect.left + "px";
168
- } else {
169
- overlay.style.display = "none";
170
- tooltip.style.display = "none";
629
+ if (!debugEl) {
630
+ stopInspecting();
631
+ return;
171
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"]
172
651
  });
173
- window.addEventListener(
174
- "click",
175
- (e) => {
176
- if (!isInspecting) return;
177
- const target = e.target;
178
- if (target === toggleBtn) return;
179
- e.preventDefault();
180
- e.stopPropagation();
181
- const debugEl = target.closest("[data-debug]");
182
- if (debugEl) {
183
- const debugId = debugEl.getAttribute("data-debug");
184
- if (debugId) {
185
- navigator.clipboard.writeText(debugId).then(() => {
186
- tooltip.textContent = "\u2705 Copied!";
187
- tooltip.style.color = "#10b981";
188
- overlay.style.background = "rgba(16, 185, 129, 0.2)";
189
- overlay.style.borderColor = "#10b981";
190
- setTimeout(stopInspecting, 600);
191
- });
192
- }
193
- } else {
194
- stopInspecting();
195
- }
196
- },
197
- { capture: true }
198
- );
199
- window.addEventListener("keydown", (e) => {
200
- if (e.key === "Escape" && isInspecting) stopInspecting();
201
- });
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
+ };
202
677
  }
203
678
 
204
679
  // src/index.ts