react-grab 0.0.39 → 0.0.41

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
@@ -1,8 +1,7 @@
1
1
  import { render, createComponent, memo, template, effect, style, insert, setStyleProperty, use } from 'solid-js/web';
2
2
  import { createRoot, createSignal, createMemo, createEffect, on, onCleanup, Show, For, onMount } from 'solid-js';
3
- import { instrument, _fiberRoots, getFiberFromHostInstance, traverseFiber, isCompositeFiber, getDisplayName } from 'bippy';
4
- import { getSourceFromHostInstance, normalizeFileName, isSourceFile } from 'bippy/dist/source';
5
- import { finder } from '@medv/finder';
3
+ import { getFiberFromHostInstance, traverseFiber, isCompositeFiber, getDisplayName, isFiber, getLatestFiber, isHostFiber } from 'bippy';
4
+ import { isSourceFile, normalizeFileName, getSource } from 'bippy/source';
6
5
 
7
6
  /**
8
7
  * @license MIT
@@ -94,8 +93,12 @@ var mountRoot = () => {
94
93
  var VIEWPORT_MARGIN_PX = 8;
95
94
  var INDICATOR_CLAMP_PADDING_PX = 4;
96
95
  var CURSOR_OFFSET_PX = 14;
96
+ var OFFSCREEN_POSITION = -1e3;
97
97
  var SELECTION_LERP_FACTOR = 0.95;
98
98
  var SUCCESS_LABEL_DURATION_MS = 1700;
99
+ var PROGRESS_INDICATOR_DELAY_MS = 150;
100
+ var DRAG_THRESHOLD_PX = 2;
101
+ var Z_INDEX_LABEL = 2147483647;
99
102
 
100
103
  // src/utils/lerp.ts
101
104
  var lerp = (start, end, factor) => {
@@ -176,7 +179,7 @@ var SelectionBox = (props) => {
176
179
  position: "fixed",
177
180
  "box-sizing": "border-box",
178
181
  "pointer-events": props.variant === "drag" ? "none" : "auto",
179
- "z-index": "2147483646"
182
+ "z-index": props.variant === "grabbed" ? "2147483645" : "2147483646"
180
183
  };
181
184
  const variantStyle = () => {
182
185
  if (props.variant === "drag") {
@@ -188,10 +191,16 @@ var SelectionBox = (props) => {
188
191
  cursor: "crosshair"
189
192
  };
190
193
  }
194
+ if (props.variant === "selection") {
195
+ return {
196
+ border: "1px dashed rgba(210, 57, 192, 0.5)",
197
+ "background-color": "rgba(210, 57, 192, 0.08)"
198
+ };
199
+ }
191
200
  return {
192
201
  border: "1px solid rgb(210, 57, 192)",
193
- "background-color": "rgba(210, 57, 192, 0.2)",
194
- transition: props.variant === "grabbed" ? "opacity 0.3s ease-out" : void 0
202
+ "background-color": "rgba(210, 57, 192, 0.08)",
203
+ transition: "opacity 0.3s ease-out"
195
204
  };
196
205
  };
197
206
  return createComponent(Show, {
@@ -254,30 +263,22 @@ var getClampedElementPosition = (positionLeft, positionTop, elementWidth, elemen
254
263
  const clampedTop = Math.max(minTop, Math.min(positionTop, maxTop));
255
264
  return { left: clampedLeft, top: clampedTop };
256
265
  };
257
-
258
- // src/components/label.tsx
259
- var _tmpl$3 = /* @__PURE__ */ template(`<span style=display:inline-block;margin-right:4px;font-weight:600>\u2713`);
260
- var _tmpl$22 = /* @__PURE__ */ template(`<div style=margin-right:4px>Copied`);
261
- var _tmpl$32 = /* @__PURE__ */ template(`<span style="font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;font-variant-numeric:tabular-nums;vertical-align:middle">`);
262
- var _tmpl$4 = /* @__PURE__ */ template(`<span style=font-variant-numeric:tabular-nums;font-size:10px;margin-left:4px;vertical-align:middle>`);
263
- var _tmpl$5 = /* @__PURE__ */ template(`<div style=margin-left:4px>to clipboard`);
264
- var _tmpl$6 = /* @__PURE__ */ template(`<div style="position:fixed;padding:2px 6px;background-color:#fde7f7;color:#b21c8e;border:1px solid #f7c5ec;border-radius:4px;font-size:11px;font-weight:500;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;pointer-events:none;transition:opacity 0.2s ease-in-out;display:flex;align-items:center;max-width:calc(100vw - (16px + env(safe-area-inset-left) + env(safe-area-inset-right)));overflow:hidden;text-overflow:ellipsis;white-space:nowrap">`);
265
- var Label = (props) => {
266
- const [opacity, setOpacity] = createSignal(0);
267
- const [positionTick, setPositionTick] = createSignal(0);
268
- let labelRef;
269
- let currentX = props.x;
270
- let currentY = props.y;
271
- let targetX = props.x;
272
- let targetY = props.y;
266
+ var useAnimatedPosition = (options) => {
267
+ const lerpFactor = options.lerpFactor ?? 0.3;
268
+ const convergenceThreshold = options.convergenceThreshold ?? 0.5;
269
+ const [x, setX] = createSignal(options.x());
270
+ const [y, setY] = createSignal(options.y());
271
+ let targetX = options.x();
272
+ let targetY = options.y();
273
273
  let animationFrameId = null;
274
274
  let hasBeenRenderedOnce = false;
275
275
  const animate = () => {
276
- currentX = lerp(currentX, targetX, 0.3);
277
- currentY = lerp(currentY, targetY, 0.3);
278
- setPositionTick((tick) => tick + 1);
279
- const hasConvergedToTarget = Math.abs(currentX - targetX) < 0.5 && Math.abs(currentY - targetY) < 0.5;
280
- if (!hasConvergedToTarget) {
276
+ const currentX = lerp(x(), targetX, lerpFactor);
277
+ const currentY = lerp(y(), targetY, lerpFactor);
278
+ setX(currentX);
279
+ setY(currentY);
280
+ const hasConverged = Math.abs(currentX - targetX) < convergenceThreshold && Math.abs(currentY - targetY) < convergenceThreshold;
281
+ if (!hasConverged) {
281
282
  animationFrameId = requestAnimationFrame(animate);
282
283
  } else {
283
284
  animationFrameId = null;
@@ -287,36 +288,16 @@ var Label = (props) => {
287
288
  if (animationFrameId !== null) return;
288
289
  animationFrameId = requestAnimationFrame(animate);
289
290
  };
290
- const updateTarget = () => {
291
- targetX = props.x;
292
- targetY = props.y;
291
+ createEffect(() => {
292
+ targetX = options.x();
293
+ targetY = options.y();
293
294
  if (!hasBeenRenderedOnce) {
294
- currentX = targetX;
295
- currentY = targetY;
295
+ setX(targetX);
296
+ setY(targetY);
296
297
  hasBeenRenderedOnce = true;
297
- setPositionTick((tick) => tick + 1);
298
298
  return;
299
299
  }
300
300
  startAnimation();
301
- };
302
- createEffect(on(() => props.visible, (visible) => {
303
- if (visible !== false) {
304
- requestAnimationFrame(() => {
305
- setOpacity(1);
306
- });
307
- } else {
308
- setOpacity(0);
309
- return;
310
- }
311
- if (props.variant === "success") {
312
- const fadeOutTimer = setTimeout(() => {
313
- setOpacity(0);
314
- }, SUCCESS_LABEL_DURATION_MS);
315
- onCleanup(() => clearTimeout(fadeOutTimer));
316
- }
317
- }));
318
- createEffect(() => {
319
- updateTarget();
320
301
  });
321
302
  onCleanup(() => {
322
303
  if (animationFrameId !== null) {
@@ -324,34 +305,89 @@ var Label = (props) => {
324
305
  animationFrameId = null;
325
306
  }
326
307
  });
308
+ return { x, y };
309
+ };
310
+ var useFadeInOut = (options) => {
311
+ const [opacity, setOpacity] = createSignal(0);
312
+ createEffect(
313
+ on(
314
+ () => options.visible,
315
+ (isVisible) => {
316
+ if (isVisible !== false) {
317
+ requestAnimationFrame(() => {
318
+ setOpacity(1);
319
+ });
320
+ } else {
321
+ setOpacity(0);
322
+ return;
323
+ }
324
+ if (options.autoFadeOutAfter !== void 0) {
325
+ const fadeOutTimer = setTimeout(() => {
326
+ setOpacity(0);
327
+ }, options.autoFadeOutAfter);
328
+ onCleanup(() => clearTimeout(fadeOutTimer));
329
+ }
330
+ }
331
+ )
332
+ );
333
+ return opacity;
334
+ };
335
+
336
+ // src/utils/get-cursor-quadrants.ts
337
+ var getCursorQuadrants = (cursorX, cursorY, elementWidth, elementHeight, offset) => {
338
+ return [
339
+ {
340
+ left: Math.round(cursorX) + offset,
341
+ top: Math.round(cursorY) + offset
342
+ },
343
+ {
344
+ left: Math.round(cursorX) - elementWidth - offset,
345
+ top: Math.round(cursorY) + offset
346
+ },
347
+ {
348
+ left: Math.round(cursorX) + offset,
349
+ top: Math.round(cursorY) - elementHeight - offset
350
+ },
351
+ {
352
+ left: Math.round(cursorX) - elementWidth - offset,
353
+ top: Math.round(cursorY) - elementHeight - offset
354
+ }
355
+ ];
356
+ };
357
+
358
+ // src/components/label.tsx
359
+ var _tmpl$3 = /* @__PURE__ */ template(`<span style=display:inline-block;margin-right:4px;font-weight:600>\u2713`);
360
+ var _tmpl$22 = /* @__PURE__ */ template(`<div style=margin-right:4px>Copied`);
361
+ var _tmpl$32 = /* @__PURE__ */ template(`<span style="font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;font-variant-numeric:tabular-nums;vertical-align:middle">`);
362
+ var _tmpl$4 = /* @__PURE__ */ template(`<span style=font-variant-numeric:tabular-nums;font-size:10px;margin-left:4px;vertical-align:middle>`);
363
+ var _tmpl$5 = /* @__PURE__ */ template(`<div style=margin-left:4px>to clipboard`);
364
+ var _tmpl$6 = /* @__PURE__ */ template(`<div style="position:fixed;padding:2px 6px;background-color:#fde7f7;color:#b21c8e;border:1px solid #f7c5ec;border-radius:4px;font-size:11px;font-weight:500;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;pointer-events:none;transition:opacity 0.2s ease-in-out;display:flex;align-items:center;max-width:calc(100vw - (16px + env(safe-area-inset-left) + env(safe-area-inset-right)));overflow:hidden;text-overflow:ellipsis;white-space:nowrap">`);
365
+ var Label = (props) => {
366
+ let labelRef;
367
+ const position = useAnimatedPosition({
368
+ x: () => props.x,
369
+ y: () => props.y,
370
+ lerpFactor: 0.3
371
+ });
372
+ const opacity = useFadeInOut({
373
+ visible: props.visible,
374
+ autoFadeOutAfter: props.variant === "success" ? SUCCESS_LABEL_DURATION_MS : void 0
375
+ });
327
376
  const labelBoundingRect = () => labelRef?.getBoundingClientRect();
328
377
  const computedPosition = () => {
329
- positionTick();
330
378
  const boundingRect = labelBoundingRect();
331
379
  if (!boundingRect) return {
332
- left: currentX,
333
- top: currentY
380
+ left: position.x(),
381
+ top: position.y()
334
382
  };
335
383
  const viewportWidth = window.innerWidth;
336
384
  const viewportHeight = window.innerHeight;
337
- const quadrants = [{
338
- left: Math.round(currentX) + CURSOR_OFFSET_PX,
339
- top: Math.round(currentY) + CURSOR_OFFSET_PX
340
- }, {
341
- left: Math.round(currentX) - boundingRect.width - CURSOR_OFFSET_PX,
342
- top: Math.round(currentY) + CURSOR_OFFSET_PX
343
- }, {
344
- left: Math.round(currentX) + CURSOR_OFFSET_PX,
345
- top: Math.round(currentY) - boundingRect.height - CURSOR_OFFSET_PX
346
- }, {
347
- left: Math.round(currentX) - boundingRect.width - CURSOR_OFFSET_PX,
348
- top: Math.round(currentY) - boundingRect.height - CURSOR_OFFSET_PX
349
- }];
350
- for (const position of quadrants) {
351
- const fitsHorizontally = position.left >= VIEWPORT_MARGIN_PX && position.left + boundingRect.width <= viewportWidth - VIEWPORT_MARGIN_PX;
352
- const fitsVertically = position.top >= VIEWPORT_MARGIN_PX && position.top + boundingRect.height <= viewportHeight - VIEWPORT_MARGIN_PX;
385
+ const quadrants = getCursorQuadrants(position.x(), position.y(), boundingRect.width, boundingRect.height, CURSOR_OFFSET_PX);
386
+ for (const position2 of quadrants) {
387
+ const fitsHorizontally = position2.left >= VIEWPORT_MARGIN_PX && position2.left + boundingRect.width <= viewportWidth - VIEWPORT_MARGIN_PX;
388
+ const fitsVertically = position2.top >= VIEWPORT_MARGIN_PX && position2.top + boundingRect.height <= viewportHeight - VIEWPORT_MARGIN_PX;
353
389
  if (fitsHorizontally && fitsVertically) {
354
- return position;
390
+ return position2;
355
391
  }
356
392
  }
357
393
  const fallback = getClampedElementPosition(quadrants[0].left, quadrants[0].top, boundingRect.width, boundingRect.height);
@@ -458,21 +494,10 @@ var Label = (props) => {
458
494
  });
459
495
  };
460
496
  var _tmpl$7 = /* @__PURE__ */ template(`<div style="position:fixed;z-index:2147483647;pointer-events:none;transition:opacity 0.1s ease-in-out"><div style="width:32px;height:2px;background-color:rgba(178, 28, 142, 0.2);border-radius:1px;overflow:hidden;position:relative"><div style="height:100%;background-color:#b21c8e;border-radius:1px;transition:width 0.05s cubic-bezier(0.165, 0.84, 0.44, 1)">`);
461
- var useFadeInOut = (visible) => {
462
- const [opacity, setOpacity] = createSignal(0);
463
- createEffect(on(() => visible, (isVisible) => {
464
- if (isVisible !== false) {
465
- requestAnimationFrame(() => {
466
- setOpacity(1);
467
- });
468
- } else {
469
- setOpacity(0);
470
- }
471
- }));
472
- return opacity;
473
- };
474
497
  var ProgressIndicator = (props) => {
475
- const opacity = useFadeInOut(props.visible);
498
+ const opacity = useFadeInOut({
499
+ visible: props.visible
500
+ });
476
501
  let progressIndicatorRef;
477
502
  const computedPosition = () => {
478
503
  const boundingRect = progressIndicatorRef?.getBoundingClientRect();
@@ -517,12 +542,11 @@ var Crosshair = (props) => {
517
542
  let width = 0;
518
543
  let height = 0;
519
544
  let dpr = 1;
520
- let currentX = props.mouseX;
521
- let currentY = props.mouseY;
522
- let targetX = props.mouseX;
523
- let targetY = props.mouseY;
524
- let animationFrameId = null;
525
- let hasBeenRenderedOnce = false;
545
+ const position = useAnimatedPosition({
546
+ x: () => props.mouseX,
547
+ y: () => props.mouseY,
548
+ lerpFactor: 0.3
549
+ });
526
550
  const setupCanvas = () => {
527
551
  if (!canvasRef) return;
528
552
  dpr = Math.max(window.devicePixelRatio || 1, 2);
@@ -543,39 +567,12 @@ var Crosshair = (props) => {
543
567
  context.strokeStyle = "rgba(210, 57, 192)";
544
568
  context.lineWidth = 1;
545
569
  context.beginPath();
546
- context.moveTo(currentX, 0);
547
- context.lineTo(currentX, height);
548
- context.moveTo(0, currentY);
549
- context.lineTo(width, currentY);
570
+ context.moveTo(position.x(), 0);
571
+ context.lineTo(position.x(), height);
572
+ context.moveTo(0, position.y());
573
+ context.lineTo(width, position.y());
550
574
  context.stroke();
551
575
  };
552
- const animate = () => {
553
- currentX = lerp(currentX, targetX, 0.3);
554
- currentY = lerp(currentY, targetY, 0.3);
555
- render2();
556
- const hasConvergedToTarget = Math.abs(currentX - targetX) < 0.5 && Math.abs(currentY - targetY) < 0.5;
557
- if (!hasConvergedToTarget) {
558
- animationFrameId = requestAnimationFrame(animate);
559
- } else {
560
- animationFrameId = null;
561
- }
562
- };
563
- const startAnimation = () => {
564
- if (animationFrameId !== null) return;
565
- animationFrameId = requestAnimationFrame(animate);
566
- };
567
- const updateTarget = () => {
568
- targetX = props.mouseX;
569
- targetY = props.mouseY;
570
- if (!hasBeenRenderedOnce) {
571
- currentX = targetX;
572
- currentY = targetY;
573
- hasBeenRenderedOnce = true;
574
- render2();
575
- return;
576
- }
577
- startAnimation();
578
- };
579
576
  createEffect(() => {
580
577
  setupCanvas();
581
578
  render2();
@@ -586,14 +583,12 @@ var Crosshair = (props) => {
586
583
  window.addEventListener("resize", handleResize);
587
584
  onCleanup(() => {
588
585
  window.removeEventListener("resize", handleResize);
589
- if (animationFrameId !== null) {
590
- cancelAnimationFrame(animationFrameId);
591
- animationFrameId = null;
592
- }
593
586
  });
594
587
  });
595
588
  createEffect(() => {
596
- updateTarget();
589
+ position.x();
590
+ position.y();
591
+ render2();
597
592
  });
598
593
  return createComponent(Show, {
599
594
  get when() {
@@ -737,27 +732,62 @@ var ReactGrabRenderer = (props) => {
737
732
  var isCapitalized = (value) => value.length > 0 && /^[A-Z]/.test(value);
738
733
 
739
734
  // src/instrumentation.ts
740
- instrument({
741
- onCommitFiberRoot(_, fiberRoot) {
742
- _fiberRoots.add(fiberRoot);
743
- }
744
- });
745
- var generateCSSSelector = (element) => {
746
- return finder(element);
735
+ var NEXT_INTERNAL_COMPONENT_NAMES = [
736
+ "InnerLayoutRouter",
737
+ "RedirectErrorBoundary",
738
+ "RedirectBoundary",
739
+ "HTTPAccessFallbackErrorBoundary",
740
+ "HTTPAccessFallbackBoundary",
741
+ "LoadingBoundary",
742
+ "ErrorBoundary",
743
+ "InnerScrollAndFocusHandler",
744
+ "ScrollAndFocusHandler",
745
+ "RenderFromTemplateContext",
746
+ "OuterLayoutRouter",
747
+ "body",
748
+ "html",
749
+ "RedirectErrorBoundary",
750
+ "RedirectBoundary",
751
+ "HTTPAccessFallbackErrorBoundary",
752
+ "HTTPAccessFallbackBoundary",
753
+ "DevRootHTTPAccessFallbackBoundary",
754
+ "AppDevOverlayErrorBoundary",
755
+ "AppDevOverlay",
756
+ "HotReload",
757
+ "Router",
758
+ "ErrorBoundaryHandler",
759
+ "ErrorBoundary",
760
+ "AppRouter",
761
+ "ServerRoot",
762
+ "SegmentStateProvider",
763
+ "RootErrorBoundary"
764
+ ];
765
+ var checkIsNextProject = () => {
766
+ return Boolean(document.getElementById("__NEXT_DATA__"));
767
+ };
768
+ var checkIsInternalComponentName = (name) => {
769
+ if (name.startsWith("_")) return true;
770
+ if (NEXT_INTERNAL_COMPONENT_NAMES.includes(name)) return true;
771
+ return false;
772
+ };
773
+ var checkIsSourceComponentName = (name) => {
774
+ if (checkIsInternalComponentName(name)) return false;
775
+ if (!isCapitalized(name)) return false;
776
+ if (name.startsWith("Primitive.")) return false;
777
+ if (name.includes("Provider") && name.includes("Context")) return false;
778
+ return true;
747
779
  };
748
- var truncateString = (string, maxLength) => string.length > maxLength ? `${string.substring(0, maxLength)}...` : string;
749
- var isInternalComponent = (name) => !isCapitalized(name) || name.startsWith("_") || name.includes("Provider") && name.includes("Context");
750
- var getNearestComponentDisplayName = (element) => {
780
+ var getNearestComponentName = (element) => {
751
781
  const fiber = getFiberFromHostInstance(element);
752
782
  if (!fiber) return null;
753
- let componentName = null;
783
+ let foundComponentName = null;
754
784
  traverseFiber(
755
785
  fiber,
756
786
  (currentFiber) => {
757
787
  if (isCompositeFiber(currentFiber)) {
758
788
  const displayName = getDisplayName(currentFiber);
759
- if (displayName && !isInternalComponent(displayName)) {
760
- componentName = displayName;
789
+ if (displayName && checkIsSourceComponentName(displayName)) {
790
+ foundComponentName = displayName;
761
791
  return true;
762
792
  }
763
793
  }
@@ -765,213 +795,95 @@ var getNearestComponentDisplayName = (element) => {
765
795
  },
766
796
  true
767
797
  );
768
- return componentName;
798
+ return foundComponentName;
769
799
  };
770
- var formatComponentSourceLocation = async (el) => {
771
- const source = await getSourceFromHostInstance(el);
772
- if (!source) return null;
773
- const fileName = normalizeFileName(source.fileName);
774
- if (isSourceFile(fileName)) {
775
- return `${fileName}:${source.lineNumber}:${source.columnNumber}`;
776
- }
777
- if (fileName && (fileName.includes(".tsx") || fileName.includes(".ts") || fileName.includes(".jsx") || fileName.includes(".js"))) {
778
- const cleanedFileName = fileName.replace(/^webpack:\/\/_N_E\//, "").replace(/^webpack:\/\/\//, "").replace(/^webpack:\/\//, "").replace(/^\.\//, "");
779
- if (cleanedFileName && !cleanedFileName.startsWith("node_modules") && !cleanedFileName.includes(".next") && !cleanedFileName.startsWith("webpack")) {
780
- return `${cleanedFileName}:${source.lineNumber}:${source.columnNumber}`;
781
- }
782
- }
783
- return null;
784
- };
785
- var getHTMLSnippet = async (element) => {
786
- const semanticTags = /* @__PURE__ */ new Set([
787
- "article",
788
- "aside",
789
- "footer",
790
- "form",
791
- "header",
792
- "main",
793
- "nav",
794
- "section"
795
- ]);
796
- const hasDistinguishingFeatures = (el) => {
797
- const tagName = el.tagName.toLowerCase();
798
- if (semanticTags.has(tagName)) return true;
799
- if (el.id) return true;
800
- if (el.className && typeof el.className === "string") {
801
- const classes = el.className.trim();
802
- if (classes && classes.length > 0) return true;
803
- }
804
- return Array.from(el.attributes).some(
805
- (attr) => attr.name.startsWith("data-")
806
- );
807
- };
808
- const collectDistinguishingAncestors = (el, maxDepth = 10) => {
809
- const ancestors2 = [];
810
- let current = el.parentElement;
811
- let depth = 0;
812
- while (current && depth < maxDepth && current.tagName !== "BODY") {
813
- if (hasDistinguishingFeatures(current)) {
814
- ancestors2.push(current);
815
- if (ancestors2.length >= 3) break;
816
- }
817
- current = current.parentElement;
818
- depth++;
819
- }
820
- return ancestors2.reverse();
821
- };
822
- const formatElementOpeningTag = (el, compact = false) => {
823
- const tagName = el.tagName.toLowerCase();
824
- const attrs = [];
825
- if (el.id) {
826
- attrs.push(`id="${el.id}"`);
827
- }
828
- if (el.className && typeof el.className === "string") {
829
- const classes = el.className.trim().split(/\s+/);
830
- if (classes.length > 0 && classes[0]) {
831
- const displayClasses = compact ? classes.slice(0, 3) : classes;
832
- const classStr = truncateString(displayClasses.join(" "), 30);
833
- attrs.push(`class="${classStr}"`);
834
- }
835
- }
836
- const dataAttrs = Array.from(el.attributes).filter(
837
- (attr) => attr.name.startsWith("data-")
838
- );
839
- const displayDataAttrs = compact ? dataAttrs.slice(0, 1) : dataAttrs;
840
- for (const attr of displayDataAttrs) {
841
- attrs.push(`${attr.name}="${truncateString(attr.value, 20)}"`);
842
- }
843
- const ariaLabel = el.getAttribute("aria-label");
844
- if (ariaLabel && !compact) {
845
- attrs.push(`aria-label="${truncateString(ariaLabel, 20)}"`);
846
- }
847
- return attrs.length > 0 ? `<${tagName} ${attrs.join(" ")}>` : `<${tagName}>`;
848
- };
849
- const formatElementClosingTag = (el) => `</${el.tagName.toLowerCase()}>`;
850
- const extractTruncatedTextContent = (el) => {
851
- const text = (el.textContent || "").trim().replace(/\s+/g, " ");
852
- return truncateString(text, 60);
853
- };
854
- const extractSiblingIdentifier = (el) => {
855
- if (el.id) return `#${el.id}`;
856
- if (el.className && typeof el.className === "string") {
857
- const classes = el.className.trim().split(/\s+/);
858
- if (classes.length > 0 && classes[0]) {
859
- return `.${classes[0]}`;
800
+ var getStack = async (element) => {
801
+ const maybeFiber = getFiberFromHostInstance(element);
802
+ if (!maybeFiber || !isFiber(maybeFiber)) return [];
803
+ const fiber = getLatestFiber(maybeFiber);
804
+ const unresolvedStack = [];
805
+ traverseFiber(
806
+ fiber,
807
+ (currentFiber) => {
808
+ const displayName = isHostFiber(currentFiber) ? typeof currentFiber.type === "string" ? currentFiber.type : null : getDisplayName(currentFiber);
809
+ if (displayName && !checkIsInternalComponentName(displayName)) {
810
+ unresolvedStack.push({
811
+ name: displayName,
812
+ sourcePromise: getSource(currentFiber)
813
+ });
860
814
  }
861
- }
862
- return null;
863
- };
864
- const lines = [];
865
- const selector = generateCSSSelector(element);
866
- lines.push(`- selector: ${selector}`);
867
- const rect = element.getBoundingClientRect();
868
- lines.push(`- width: ${Math.round(rect.width)}`);
869
- lines.push(`- height: ${Math.round(rect.height)}`);
870
- lines.push("HTML snippet:");
871
- lines.push("```html");
872
- const ancestors = collectDistinguishingAncestors(element);
873
- const ancestorComponents = ancestors.map(
874
- (ancestor) => getNearestComponentDisplayName(ancestor)
815
+ },
816
+ true
875
817
  );
876
- const elementComponent = getNearestComponentDisplayName(element);
877
- const ancestorSources = await Promise.all(
878
- ancestors.map((ancestor) => formatComponentSourceLocation(ancestor))
818
+ const resolvedStack = await Promise.all(
819
+ unresolvedStack.map(async (frame) => ({
820
+ name: frame.name,
821
+ source: await frame.sourcePromise
822
+ }))
879
823
  );
880
- const elementSource = await formatComponentSourceLocation(element);
881
- for (let i = 0; i < ancestors.length; i++) {
882
- const indent2 = " ".repeat(i);
883
- const componentName = ancestorComponents[i];
884
- const source = ancestorSources[i];
885
- if (componentName && source && (i === 0 || ancestorComponents[i - 1] !== componentName)) {
886
- lines.push(`${indent2}<${componentName} source="${source}">`);
824
+ return resolvedStack.filter((frame) => frame.source !== null);
825
+ };
826
+ var formatStack = (stack) => {
827
+ const isNextProject = checkIsNextProject();
828
+ return stack.map(({ name, source }) => {
829
+ if (!source) return ` at ${name}`;
830
+ if (source.fileName.startsWith("about://React/Server")) {
831
+ return ` at ${name} (Server)`;
887
832
  }
888
- lines.push(`${indent2}${formatElementOpeningTag(ancestors[i], true)}`);
889
- }
890
- const parent = element.parentElement;
891
- let targetIndex = -1;
892
- if (parent) {
893
- const siblings = Array.from(parent.children);
894
- targetIndex = siblings.indexOf(element);
895
- if (targetIndex > 0) {
896
- const indent2 = " ".repeat(ancestors.length);
897
- if (targetIndex <= 2) {
898
- for (let i = 0; i < targetIndex; i++) {
899
- const sibling = siblings[i];
900
- const siblingId = extractSiblingIdentifier(sibling);
901
- if (siblingId) {
902
- lines.push(`${indent2} ${formatElementOpeningTag(sibling, true)}`);
903
- lines.push(`${indent2} </${sibling.tagName.toLowerCase()}>`);
904
- }
905
- }
906
- } else {
907
- lines.push(
908
- `${indent2} ... (${targetIndex} element${targetIndex === 1 ? "" : "s"})`
909
- );
910
- }
833
+ if (!isSourceFile(source.fileName)) return ` at ${name}`;
834
+ const framePart = ` at ${name} in ${normalizeFileName(source.fileName)}`;
835
+ if (isNextProject) {
836
+ return `${framePart}:${source.lineNumber}:${source.columnNumber}`;
911
837
  }
838
+ return framePart;
839
+ }).join("\n");
840
+ };
841
+ var getHTMLPreview = (element) => {
842
+ const tagName = element.tagName.toLowerCase();
843
+ if (!(element instanceof HTMLElement)) {
844
+ return `<${tagName} />`;
912
845
  }
913
- const indent = " ".repeat(ancestors.length);
914
- const lastAncestorComponent = ancestors.length > 0 ? ancestorComponents[ancestorComponents.length - 1] : null;
915
- const showElementComponent = elementComponent && elementSource && elementComponent !== lastAncestorComponent;
916
- if (showElementComponent) {
917
- lines.push(`${indent} <${elementComponent} used-at="${elementSource}">`);
918
- }
919
- lines.push(`${indent} <!-- IMPORTANT: selected element -->`);
920
- const textContent = extractTruncatedTextContent(element);
921
- const childrenCount = element.children.length;
922
- const elementIndent = `${indent}${showElementComponent ? " " : " "}`;
923
- if (textContent && childrenCount === 0 && textContent.length < 40) {
924
- lines.push(
925
- `${elementIndent}${formatElementOpeningTag(element)}${textContent}${formatElementClosingTag(
926
- element
927
- )}`
928
- );
929
- } else {
930
- lines.push(`${elementIndent}${formatElementOpeningTag(element)}`);
931
- if (textContent) {
932
- lines.push(`${elementIndent} ${textContent}`);
933
- }
934
- if (childrenCount > 0) {
935
- lines.push(
936
- `${elementIndent} ... (${childrenCount} element${childrenCount === 1 ? "" : "s"})`
937
- );
846
+ const text = element.innerText?.trim() ?? element.textContent?.trim() ?? "";
847
+ let attrsText = "";
848
+ const attributes = Array.from(element.attributes);
849
+ for (const attribute of attributes) {
850
+ const name = attribute.name;
851
+ let value = attribute.value;
852
+ if (value.length > 20) {
853
+ value = `${value.slice(0, 20)}...`;
938
854
  }
939
- lines.push(`${elementIndent}${formatElementClosingTag(element)}`);
940
- }
941
- if (showElementComponent) {
942
- lines.push(`${indent} </${elementComponent}>`);
855
+ attrsText += ` ${name}="${value}"`;
943
856
  }
944
- if (parent && targetIndex >= 0) {
945
- const siblings = Array.from(parent.children);
946
- const siblingsAfter = siblings.length - targetIndex - 1;
947
- if (siblingsAfter > 0) {
948
- if (siblingsAfter <= 2) {
949
- for (let i = targetIndex + 1; i < siblings.length; i++) {
950
- const sibling = siblings[i];
951
- const siblingId = extractSiblingIdentifier(sibling);
952
- if (siblingId) {
953
- lines.push(`${indent} ${formatElementOpeningTag(sibling, true)}`);
954
- lines.push(`${indent} </${sibling.tagName.toLowerCase()}>`);
955
- }
956
- }
857
+ let topElements = 0;
858
+ let bottomElements = 0;
859
+ let foundFirstText = false;
860
+ const childNodes = Array.from(element.childNodes);
861
+ for (const node of childNodes) {
862
+ if (node.nodeType === Node.COMMENT_NODE) continue;
863
+ if (node.nodeType === Node.TEXT_NODE) {
864
+ if (node.textContent && node.textContent.trim().length > 0) {
865
+ foundFirstText = true;
866
+ }
867
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
868
+ if (!foundFirstText) {
869
+ topElements++;
957
870
  } else {
958
- lines.push(
959
- `${indent} ... (${siblingsAfter} element${siblingsAfter === 1 ? "" : "s"})`
960
- );
871
+ bottomElements++;
961
872
  }
962
873
  }
963
874
  }
964
- for (let i = ancestors.length - 1; i >= 0; i--) {
965
- const indent2 = " ".repeat(i);
966
- lines.push(`${indent2}${formatElementClosingTag(ancestors[i])}`);
967
- const componentName = ancestorComponents[i];
968
- const source = ancestorSources[i];
969
- if (componentName && source && (i === ancestors.length - 1 || ancestorComponents[i + 1] !== componentName)) {
970
- lines.push(`${indent2}</${componentName}>`);
971
- }
875
+ let content = "";
876
+ if (topElements > 0) content += `
877
+ (${topElements} elements)`;
878
+ if (text.length > 0) content += `
879
+ ${text}`;
880
+ if (bottomElements > 0) content += `
881
+ (${bottomElements} elements)`;
882
+ if (content.length > 0) {
883
+ return `<${tagName}${attrsText}>${content}
884
+ </${tagName}>`;
972
885
  }
973
- lines.push("```");
974
- return lines.join("\n");
886
+ return `<${tagName}${attrsText} />`;
975
887
  };
976
888
 
977
889
  // src/utils/copy-content.ts
@@ -991,73 +903,13 @@ var waitForFocus = () => {
991
903
  var copyContent = async (content, onSuccess) => {
992
904
  await waitForFocus();
993
905
  try {
994
- if (Array.isArray(content)) {
995
- if (!navigator?.clipboard?.write) {
996
- for (const contentPart of content) {
997
- if (typeof contentPart === "string") {
998
- const result = copyContentFallback(contentPart, onSuccess);
999
- if (!result) return result;
1000
- }
1001
- }
1002
- onSuccess?.();
1003
- return true;
1004
- }
1005
- const mimeTypeMap = /* @__PURE__ */ new Map();
1006
- for (const contentPart of content) {
1007
- if (contentPart instanceof Blob) {
1008
- const mimeType = contentPart.type || "text/plain";
1009
- if (!mimeTypeMap.has(mimeType)) {
1010
- mimeTypeMap.set(mimeType, contentPart);
1011
- }
1012
- } else {
1013
- if (!mimeTypeMap.has("text/plain")) {
1014
- mimeTypeMap.set(
1015
- "text/plain",
1016
- new Blob([contentPart], { type: "text/plain" })
1017
- );
1018
- }
1019
- }
1020
- }
1021
- if (mimeTypeMap.size === 0) {
1022
- const plainTextFallback = content.find(
1023
- (contentPart) => typeof contentPart === "string"
1024
- );
1025
- if (typeof plainTextFallback === "string") {
1026
- return copyContentFallback(plainTextFallback, onSuccess);
1027
- }
1028
- return false;
1029
- }
1030
- try {
1031
- await navigator.clipboard.write([
1032
- new ClipboardItem(Object.fromEntries(mimeTypeMap))
1033
- ]);
1034
- onSuccess?.();
1035
- return true;
1036
- } catch {
1037
- const plainTextParts = content.filter(
1038
- (contentPart) => typeof contentPart === "string"
1039
- );
1040
- if (plainTextParts.length > 0) {
1041
- const combinedText = plainTextParts.join("\n\n");
1042
- return copyContentFallback(combinedText, onSuccess);
1043
- }
1044
- return false;
1045
- }
1046
- } else if (content instanceof Blob) {
1047
- await navigator.clipboard.write([
1048
- new ClipboardItem({ [content.type]: content })
1049
- ]);
906
+ try {
907
+ await navigator.clipboard.writeText(content);
1050
908
  onSuccess?.();
1051
909
  return true;
1052
- } else {
1053
- try {
1054
- await navigator.clipboard.writeText(String(content));
1055
- onSuccess?.();
1056
- return true;
1057
- } catch {
1058
- const result = copyContentFallback(content, onSuccess);
1059
- return result;
1060
- }
910
+ } catch {
911
+ const result = copyContentFallback(content, onSuccess);
912
+ return result;
1061
913
  }
1062
914
  } catch {
1063
915
  return false;
@@ -1146,13 +998,27 @@ var getElementAtPosition = (clientX, clientY) => {
1146
998
 
1147
999
  // src/utils/get-elements-in-drag.ts
1148
1000
  var DRAG_COVERAGE_THRESHOLD = 0.75;
1001
+ var calculateIntersectionArea = (rect1, rect2) => {
1002
+ const intersectionLeft = Math.max(rect1.left, rect2.left);
1003
+ const intersectionTop = Math.max(rect1.top, rect2.top);
1004
+ const intersectionRight = Math.min(rect1.right, rect2.right);
1005
+ const intersectionBottom = Math.min(rect1.bottom, rect2.bottom);
1006
+ const intersectionWidth = Math.max(0, intersectionRight - intersectionLeft);
1007
+ const intersectionHeight = Math.max(0, intersectionBottom - intersectionTop);
1008
+ return intersectionWidth * intersectionHeight;
1009
+ };
1010
+ var hasIntersection = (rect1, rect2) => {
1011
+ return rect1.left < rect2.right && rect1.right > rect2.left && rect1.top < rect2.bottom && rect1.bottom > rect2.top;
1012
+ };
1149
1013
  var filterElementsInDrag = (dragRect, isValidGrabbableElement2, shouldCheckCoverage) => {
1150
1014
  const elements = [];
1151
1015
  const allElements = Array.from(document.querySelectorAll("*"));
1152
- const dragLeft = dragRect.x;
1153
- const dragTop = dragRect.y;
1154
- const dragRight = dragRect.x + dragRect.width;
1155
- const dragBottom = dragRect.y + dragRect.height;
1016
+ const dragBounds = {
1017
+ left: dragRect.x,
1018
+ top: dragRect.y,
1019
+ right: dragRect.x + dragRect.width,
1020
+ bottom: dragRect.y + dragRect.height
1021
+ };
1156
1022
  for (const candidateElement of allElements) {
1157
1023
  if (!shouldCheckCoverage) {
1158
1024
  const tagName = (candidateElement.tagName || "").toUpperCase();
@@ -1162,26 +1028,21 @@ var filterElementsInDrag = (dragRect, isValidGrabbableElement2, shouldCheckCover
1162
1028
  continue;
1163
1029
  }
1164
1030
  const elementRect = candidateElement.getBoundingClientRect();
1165
- const elementLeft = elementRect.left;
1166
- const elementTop = elementRect.top;
1167
- const elementRight = elementRect.left + elementRect.width;
1168
- const elementBottom = elementRect.top + elementRect.height;
1031
+ const elementBounds = {
1032
+ left: elementRect.left,
1033
+ top: elementRect.top,
1034
+ right: elementRect.left + elementRect.width,
1035
+ bottom: elementRect.top + elementRect.height
1036
+ };
1169
1037
  if (shouldCheckCoverage) {
1170
- const intersectionLeft = Math.max(dragLeft, elementLeft);
1171
- const intersectionTop = Math.max(dragTop, elementTop);
1172
- const intersectionRight = Math.min(dragRight, elementRight);
1173
- const intersectionBottom = Math.min(dragBottom, elementBottom);
1174
- const intersectionWidth = Math.max(0, intersectionRight - intersectionLeft);
1175
- const intersectionHeight = Math.max(0, intersectionBottom - intersectionTop);
1176
- const intersectionArea = intersectionWidth * intersectionHeight;
1038
+ const intersectionArea = calculateIntersectionArea(dragBounds, elementBounds);
1177
1039
  const elementArea = Math.max(0, elementRect.width * elementRect.height);
1178
1040
  const hasMajorityCoverage = elementArea > 0 && intersectionArea / elementArea >= DRAG_COVERAGE_THRESHOLD;
1179
1041
  if (hasMajorityCoverage) {
1180
1042
  elements.push(candidateElement);
1181
1043
  }
1182
1044
  } else {
1183
- const hasIntersection = elementLeft < dragRight && elementRight > dragLeft && elementTop < dragBottom && elementBottom > dragTop;
1184
- if (hasIntersection) {
1045
+ if (hasIntersection(elementBounds, dragBounds)) {
1185
1046
  elements.push(candidateElement);
1186
1047
  }
1187
1048
  }
@@ -1220,14 +1081,7 @@ var createElementBounds = (element) => {
1220
1081
  };
1221
1082
  };
1222
1083
 
1223
- // src/utils/is-localhost.ts
1224
- var isLocalhost = () => {
1225
- const hostname = window.location.hostname;
1226
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
1227
- };
1228
-
1229
1084
  // src/core.tsx
1230
- var PROGRESS_INDICATOR_DELAY_MS = 150;
1231
1085
  var init = (rawOptions) => {
1232
1086
  const options = {
1233
1087
  enabled: true,
@@ -1250,7 +1104,6 @@ var init = (rawOptions) => {
1250
1104
  };
1251
1105
  }
1252
1106
  return createRoot((dispose) => {
1253
- const OFFSCREEN_POSITION = -1e3;
1254
1107
  const [isHoldingKeys, setIsHoldingKeys] = createSignal(false);
1255
1108
  const [mouseX, setMouseX] = createSignal(OFFSCREEN_POSITION);
1256
1109
  const [mouseY, setMouseY] = createSignal(OFFSCREEN_POSITION);
@@ -1260,10 +1113,11 @@ var init = (rawOptions) => {
1260
1113
  const [isCopying, setIsCopying] = createSignal(false);
1261
1114
  const [lastGrabbedElement, setLastGrabbedElement] = createSignal(null);
1262
1115
  const [progressStartTime, setProgressStartTime] = createSignal(null);
1263
- const [progressTick, setProgressTick] = createSignal(0);
1116
+ const [progress, setProgress] = createSignal(0);
1264
1117
  const [grabbedBoxes, setGrabbedBoxes] = createSignal([]);
1265
1118
  const [successLabels, setSuccessLabels] = createSignal([]);
1266
1119
  const [isActivated, setIsActivated] = createSignal(false);
1120
+ const [isToggleMode, setIsToggleMode] = createSignal(false);
1267
1121
  const [showProgressIndicator, setShowProgressIndicator] = createSignal(false);
1268
1122
  const [didJustDrag, setDidJustDrag] = createSignal(false);
1269
1123
  const [copyStartX, setCopyStartX] = createSignal(OFFSCREEN_POSITION);
@@ -1306,55 +1160,10 @@ var init = (rawOptions) => {
1306
1160
  const wrapInSelectedElementTags = (context) => `<selected_element>
1307
1161
  ${context}
1308
1162
  </selected_element>`;
1309
- const extractRelevantComputedStyles = (element) => {
1310
- const computed = window.getComputedStyle(element);
1311
- const rect = element.getBoundingClientRect();
1312
- return {
1313
- width: `${Math.round(rect.width)}px`,
1314
- height: `${Math.round(rect.height)}px`,
1315
- paddingTop: computed.paddingTop,
1316
- paddingRight: computed.paddingRight,
1317
- paddingBottom: computed.paddingBottom,
1318
- paddingLeft: computed.paddingLeft,
1319
- background: computed.background,
1320
- opacity: computed.opacity
1321
- };
1322
- };
1323
- const createStructuredClipboardHtmlBlob = (elements) => {
1324
- const structuredData = {
1325
- elements: elements.map((element) => ({
1326
- tagName: element.tagName,
1327
- content: element.content,
1328
- computedStyles: element.computedStyles
1329
- }))
1330
- };
1331
- const jsonString = JSON.stringify(structuredData);
1332
- const base64Data = btoa(encodeURIComponent(jsonString).replace(/%([0-9A-F]{2})/g, (_match, p1) => String.fromCharCode(parseInt(p1, 16))));
1333
- const htmlContent = `<div data-react-grab="${base64Data}"></div>`;
1334
- return new Blob([htmlContent], {
1335
- type: "text/html"
1336
- });
1337
- };
1338
1163
  const extractElementTagName = (element) => (element.tagName || "").toLowerCase();
1339
- const extractNearestComponentName = (element) => {
1340
- const fiber = getFiberFromHostInstance(element);
1341
- if (!fiber) return null;
1342
- let componentName = null;
1343
- traverseFiber(fiber, (currentFiber) => {
1344
- if (isCompositeFiber(currentFiber)) {
1345
- const displayName = getDisplayName(currentFiber);
1346
- if (displayName && isCapitalized(displayName) && !displayName.startsWith("_")) {
1347
- componentName = displayName;
1348
- return true;
1349
- }
1350
- }
1351
- return false;
1352
- }, true);
1353
- return componentName;
1354
- };
1355
1164
  const extractElementLabelText = (element) => {
1356
1165
  const tagName = extractElementTagName(element);
1357
- const componentName = extractNearestComponentName(element);
1166
+ const componentName = getNearestComponentName(element);
1358
1167
  if (tagName && componentName) {
1359
1168
  return `<${tagName}> in ${componentName}`;
1360
1169
  }
@@ -1376,8 +1185,6 @@ ${context}
1376
1185
  } catch {
1377
1186
  }
1378
1187
  };
1379
- const isExtensionEnvironment = () => window.__REACT_GRAB_EXTENSION_ACTIVE__ === true || options.isExtension === true;
1380
- const isExtensionTextOnlyMode = () => isExtensionEnvironment() && !isLocalhost();
1381
1188
  const executeCopyOperation = async (positionX, positionY, operation) => {
1382
1189
  setCopyStartX(positionX);
1383
1190
  setCopyStartY(positionY);
@@ -1386,6 +1193,13 @@ ${context}
1386
1193
  await operation().finally(() => {
1387
1194
  setIsCopying(false);
1388
1195
  stopProgressAnimation();
1196
+ if (isToggleMode()) {
1197
+ if (!isHoldingKeys()) {
1198
+ deactivateRenderer();
1199
+ } else {
1200
+ setIsToggleMode(false);
1201
+ }
1202
+ }
1389
1203
  });
1390
1204
  };
1391
1205
  const hasInnerText = (element) => "innerText" in element;
@@ -1396,38 +1210,37 @@ ${context}
1396
1210
  return element.textContent ?? "";
1397
1211
  };
1398
1212
  const createCombinedTextContent = (elements) => elements.map((element) => extractElementTextContent(element).trim()).filter((textContent) => textContent.length > 0).join("\n\n");
1399
- const copySingleElementToClipboard = async (targetElement2) => {
1400
- showTemporaryGrabbedBox(createElementBounds(targetElement2));
1401
- await new Promise((resolve) => requestAnimationFrame(resolve));
1213
+ const tryCopyWithFallback = async (elements) => {
1402
1214
  let didCopy = false;
1403
1215
  try {
1404
- if (isExtensionTextOnlyMode()) {
1405
- const plainTextContent = createCombinedTextContent([targetElement2]);
1406
- if (plainTextContent.length > 0) {
1407
- didCopy = await copyContent(plainTextContent, options.playCopySound ? playCopySound : void 0);
1408
- }
1409
- } else {
1410
- const content = await getHTMLSnippet(targetElement2);
1411
- const plainTextContent = wrapInSelectedElementTags(content);
1412
- const htmlContent = createStructuredClipboardHtmlBlob([{
1413
- tagName: extractElementTagName(targetElement2),
1414
- content,
1415
- computedStyles: extractRelevantComputedStyles(targetElement2)
1416
- }]);
1417
- didCopy = await copyContent([plainTextContent, htmlContent], options.playCopySound ? playCopySound : void 0);
1418
- if (!didCopy) {
1419
- const plainTextContentOnly = createCombinedTextContent([targetElement2]);
1420
- if (plainTextContentOnly.length > 0) {
1421
- didCopy = await copyContent(plainTextContentOnly, options.playCopySound ? playCopySound : void 0);
1422
- }
1216
+ const elementSnippetResults = await Promise.allSettled(elements.map(async (element) => `## HTML Frame:
1217
+ ${getHTMLPreview(element)}
1218
+
1219
+ ## Code Location:
1220
+ ${formatStack(await getStack(element))}`));
1221
+ const elementSnippets = elementSnippetResults.map((result) => result.status === "fulfilled" ? result.value : "").filter((snippet) => snippet.trim());
1222
+ if (elementSnippets.length > 0) {
1223
+ const plainTextContent = elementSnippets.map((snippet) => wrapInSelectedElementTags(snippet)).join("\n\n");
1224
+ didCopy = await copyContent(plainTextContent, options.playCopySound ? playCopySound : void 0);
1225
+ }
1226
+ if (!didCopy) {
1227
+ const plainTextContentOnly = createCombinedTextContent(elements);
1228
+ if (plainTextContentOnly.length > 0) {
1229
+ didCopy = await copyContent(plainTextContentOnly, options.playCopySound ? playCopySound : void 0);
1423
1230
  }
1424
1231
  }
1425
1232
  } catch {
1426
- const plainTextContentOnly = createCombinedTextContent([targetElement2]);
1233
+ const plainTextContentOnly = createCombinedTextContent(elements);
1427
1234
  if (plainTextContentOnly.length > 0) {
1428
1235
  didCopy = await copyContent(plainTextContentOnly, options.playCopySound ? playCopySound : void 0);
1429
1236
  }
1430
1237
  }
1238
+ return didCopy;
1239
+ };
1240
+ const copySingleElementToClipboard = async (targetElement2) => {
1241
+ showTemporaryGrabbedBox(createElementBounds(targetElement2));
1242
+ await new Promise((resolve) => requestAnimationFrame(resolve));
1243
+ const didCopy = await tryCopyWithFallback([targetElement2]);
1431
1244
  if (didCopy) {
1432
1245
  showTemporarySuccessLabel(extractElementLabelText(targetElement2));
1433
1246
  }
@@ -1439,40 +1252,7 @@ ${context}
1439
1252
  showTemporaryGrabbedBox(createElementBounds(element));
1440
1253
  }
1441
1254
  await new Promise((resolve) => requestAnimationFrame(resolve));
1442
- let didCopy = false;
1443
- try {
1444
- if (isExtensionTextOnlyMode()) {
1445
- const plainTextContent = createCombinedTextContent(targetElements);
1446
- if (plainTextContent.length > 0) {
1447
- didCopy = await copyContent(plainTextContent, options.playCopySound ? playCopySound : void 0);
1448
- }
1449
- } else {
1450
- const elementSnippetResults = await Promise.allSettled(targetElements.map((element) => getHTMLSnippet(element)));
1451
- const elementSnippets = elementSnippetResults.map((result) => result.status === "fulfilled" ? result.value : "").filter((snippet) => snippet.trim());
1452
- if (elementSnippets.length > 0) {
1453
- const plainTextContent = elementSnippets.map((snippet) => wrapInSelectedElementTags(snippet)).join("\n\n");
1454
- const structuredElements = elementSnippets.map((content, elementIndex) => ({
1455
- tagName: extractElementTagName(targetElements[elementIndex]),
1456
- content,
1457
- computedStyles: extractRelevantComputedStyles(targetElements[elementIndex])
1458
- }));
1459
- const htmlContent = createStructuredClipboardHtmlBlob(structuredElements);
1460
- didCopy = await copyContent([plainTextContent, htmlContent], options.playCopySound ? playCopySound : void 0);
1461
- if (!didCopy) {
1462
- const plainTextContentOnly = createCombinedTextContent(targetElements);
1463
- if (plainTextContentOnly.length > 0) {
1464
- didCopy = await copyContent(plainTextContentOnly, options.playCopySound ? playCopySound : void 0);
1465
- }
1466
- }
1467
- } else {
1468
- const plainTextContentOnly = createCombinedTextContent(targetElements);
1469
- if (plainTextContentOnly.length > 0) {
1470
- didCopy = await copyContent(plainTextContentOnly, options.playCopySound ? playCopySound : void 0);
1471
- }
1472
- }
1473
- }
1474
- } catch {
1475
- }
1255
+ const didCopy = await tryCopyWithFallback(targetElements);
1476
1256
  if (didCopy) {
1477
1257
  showTemporarySuccessLabel(`${targetElements.length} elements`);
1478
1258
  }
@@ -1496,7 +1276,6 @@ ${context}
1496
1276
  y: elementBounds.top
1497
1277
  };
1498
1278
  });
1499
- const DRAG_THRESHOLD_PX = 2;
1500
1279
  const calculateDragDistance = (endX, endY) => ({
1501
1280
  x: Math.abs(endX - dragStartX()),
1502
1281
  y: Math.abs(endY - dragStartY())
@@ -1554,30 +1333,23 @@ ${context}
1554
1333
  setLastGrabbedElement(null);
1555
1334
  }
1556
1335
  }));
1557
- const progress = createMemo(() => {
1558
- const startTime = progressStartTime();
1559
- progressTick();
1560
- if (startTime === null) return 0;
1561
- const elapsedTime = Date.now() - startTime;
1562
- const normalizedTime = elapsedTime / options.keyHoldDuration;
1563
- const easedProgress = 1 - Math.exp(-normalizedTime);
1564
- const maxProgressBeforeCompletion = 0.95;
1565
- if (isCopying()) {
1566
- return Math.min(easedProgress, maxProgressBeforeCompletion);
1567
- }
1568
- return 1;
1569
- });
1570
1336
  const startProgressAnimation = () => {
1571
- setProgressStartTime(Date.now());
1337
+ const startTime = Date.now();
1338
+ setProgressStartTime(startTime);
1572
1339
  setShowProgressIndicator(false);
1573
1340
  progressDelayTimerId = window.setTimeout(() => {
1574
1341
  setShowProgressIndicator(true);
1575
1342
  progressDelayTimerId = null;
1576
1343
  }, PROGRESS_INDICATOR_DELAY_MS);
1577
1344
  const animateProgress = () => {
1578
- if (progressStartTime() === null) return;
1579
- setProgressTick((tick) => tick + 1);
1580
- const currentProgress = progress();
1345
+ const currentStartTime = progressStartTime();
1346
+ if (currentStartTime === null) return;
1347
+ const elapsedTime = Date.now() - currentStartTime;
1348
+ const normalizedTime = elapsedTime / options.keyHoldDuration;
1349
+ const easedProgress = 1 - Math.exp(-normalizedTime);
1350
+ const maxProgressBeforeCompletion = 0.95;
1351
+ const currentProgress = isCopying() ? Math.min(easedProgress, maxProgressBeforeCompletion) : 1;
1352
+ setProgress(currentProgress);
1581
1353
  if (currentProgress < 1) {
1582
1354
  progressAnimationId = requestAnimationFrame(animateProgress);
1583
1355
  }
@@ -1594,6 +1366,7 @@ ${context}
1594
1366
  progressDelayTimerId = null;
1595
1367
  }
1596
1368
  setProgressStartTime(null);
1369
+ setProgress(1);
1597
1370
  setShowProgressIndicator(false);
1598
1371
  };
1599
1372
  const activateRenderer = () => {
@@ -1602,6 +1375,7 @@ ${context}
1602
1375
  document.body.style.cursor = "crosshair";
1603
1376
  };
1604
1377
  const deactivateRenderer = () => {
1378
+ setIsToggleMode(false);
1605
1379
  setIsHoldingKeys(false);
1606
1380
  setIsActivated(false);
1607
1381
  document.body.style.cursor = "";
@@ -1627,11 +1401,25 @@ ${context}
1627
1401
  deactivateRenderer();
1628
1402
  return;
1629
1403
  }
1404
+ if (event.key === "Enter" && isHoldingKeys()) {
1405
+ setIsToggleMode(true);
1406
+ if (keydownSpamTimerId !== null) {
1407
+ window.clearTimeout(keydownSpamTimerId);
1408
+ keydownSpamTimerId = null;
1409
+ }
1410
+ if (!isActivated()) {
1411
+ if (holdTimerId) window.clearTimeout(holdTimerId);
1412
+ activateRenderer();
1413
+ options.onActivate?.();
1414
+ }
1415
+ return;
1416
+ }
1630
1417
  if (!options.allowActivationInsideInput && isKeyboardEventTriggeredByInput(event)) {
1631
1418
  return;
1632
1419
  }
1633
1420
  if (!isTargetKeyCombination(event)) return;
1634
1421
  if (isActivated()) {
1422
+ if (isToggleMode()) return;
1635
1423
  if (keydownSpamTimerId !== null) {
1636
1424
  window.clearTimeout(keydownSpamTimerId);
1637
1425
  }
@@ -1652,13 +1440,15 @@ ${context}
1652
1440
  options.onActivate?.();
1653
1441
  }, options.keyHoldDuration);
1654
1442
  }, {
1655
- signal: eventListenerSignal
1443
+ signal: eventListenerSignal,
1444
+ capture: true
1656
1445
  });
1657
1446
  window.addEventListener("keyup", (event) => {
1658
1447
  if (!isHoldingKeys() && !isActivated()) return;
1659
1448
  const isReleasingModifier = !event.metaKey && !event.ctrlKey;
1660
1449
  const isReleasingC = event.key.toLowerCase() === "c";
1661
1450
  if (isReleasingC || isReleasingModifier) {
1451
+ if (isToggleMode()) return;
1662
1452
  deactivateRenderer();
1663
1453
  }
1664
1454
  }, {
@@ -1740,9 +1530,17 @@ ${context}
1740
1530
  if (isRendererActive() || isCopying() || didJustDrag()) {
1741
1531
  event.preventDefault();
1742
1532
  event.stopPropagation();
1743
- if (didJustDrag()) {
1533
+ const hadDrag = didJustDrag();
1534
+ if (hadDrag) {
1744
1535
  setDidJustDrag(false);
1745
1536
  }
1537
+ if (isToggleMode() && !isCopying()) {
1538
+ if (!isHoldingKeys()) {
1539
+ deactivateRenderer();
1540
+ } else {
1541
+ setIsToggleMode(false);
1542
+ }
1543
+ }
1746
1544
  }
1747
1545
  }, {
1748
1546
  signal: eventListenerSignal,
@@ -1765,10 +1563,14 @@ ${context}
1765
1563
  document.body.style.cursor = "";
1766
1564
  });
1767
1565
  const rendererRoot = mountRoot();
1768
- const selectionVisible = createMemo(() => false);
1566
+ const selectionVisible = createMemo(() => isRendererActive() && !isDragging() && Boolean(targetElement()));
1769
1567
  const dragVisible = createMemo(() => isRendererActive() && isDraggingBeyondThreshold());
1770
1568
  const labelVariant = createMemo(() => isCopying() ? "processing" : "hover");
1771
- const labelVisible = createMemo(() => isRendererActive() && !isDragging() && mouseHasSettled() && (Boolean(targetElement()) && !isSameAsLast() || !targetElement()) || isCopying());
1569
+ const labelVisible = createMemo(() => {
1570
+ if (isCopying()) return true;
1571
+ if (successLabels().length > 0) return false;
1572
+ return isRendererActive() && !isDragging() && mouseHasSettled() && (Boolean(targetElement()) && !isSameAsLast() || !targetElement());
1573
+ });
1772
1574
  const progressVisible = createMemo(() => isCopying() && showProgressIndicator() && hasValidMousePosition());
1773
1575
  const crosshairVisible = createMemo(() => isRendererActive() && !isDragging());
1774
1576
  render(() => createComponent(ReactGrabRenderer, {
@@ -1805,6 +1607,7 @@ ${context}
1805
1607
  get labelVisible() {
1806
1608
  return labelVisible();
1807
1609
  },
1610
+ labelZIndex: Z_INDEX_LABEL,
1808
1611
  get progressVisible() {
1809
1612
  return progressVisible();
1810
1613
  },
@@ -1850,9 +1653,6 @@ ${context}
1850
1653
  // src/index.ts
1851
1654
  var globalApi = null;
1852
1655
  var getGlobalApi = () => globalApi;
1853
- var EXTENSION_MARKER = "__REACT_GRAB_EXTENSION_ACTIVE__";
1854
- if (!window[EXTENSION_MARKER]) {
1855
- globalApi = init();
1856
- }
1656
+ globalApi = init();
1857
1657
 
1858
1658
  export { getGlobalApi, init, playCopySound };