react-grab 0.0.29 → 0.0.30

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,7 +1,9 @@
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 } from 'bippy';
4
- import { getOwnerStack, getSourcesFromStack, normalizeFileName, isSourceFile } from 'bippy/dist/source';
3
+ import { domToPng } from 'modern-screenshot';
4
+ import { instrument, _fiberRoots, getFiberFromHostInstance, traverseFiber, isCompositeFiber, getDisplayName } from 'bippy';
5
+ import { getSourceFromHostInstance, normalizeFileName, isSourceFile } from 'bippy/dist/source';
6
+ import { finder } from '@medv/finder';
5
7
 
6
8
  /**
7
9
  * @license MIT
@@ -94,6 +96,7 @@ var VIEWPORT_MARGIN_PX = 8;
94
96
  var INDICATOR_CLAMP_PADDING_PX = 4;
95
97
  var CURSOR_OFFSET_PX = 14;
96
98
  var SELECTION_LERP_FACTOR = 0.95;
99
+ var SUCCESS_LABEL_DURATION_MS = 1700;
97
100
 
98
101
  // src/utils/lerp.ts
99
102
  var lerp = (start, end, factor) => {
@@ -256,9 +259,9 @@ var getClampedElementPosition = (positionLeft, positionTop, elementWidth, elemen
256
259
  // src/components/label.tsx
257
260
  var _tmpl$3 = /* @__PURE__ */ template(`<span style=display:inline-block;margin-right:4px;font-weight:600>\u2713`);
258
261
  var _tmpl$22 = /* @__PURE__ */ template(`<div style=margin-right:4px>Copied`);
259
- var _tmpl$32 = /* @__PURE__ */ template(`<div style=margin-left:4px>to clipboard`);
260
- var _tmpl$4 = /* @__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">`);
261
- var _tmpl$5 = /* @__PURE__ */ template(`<span style="font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;font-variant-numeric:tabular-nums">`);
262
+ 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">`);
263
+ var _tmpl$4 = /* @__PURE__ */ template(`<div style=margin-left:4px>to clipboard`);
264
+ var _tmpl$5 = /* @__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">`);
262
265
  var Label = (props) => {
263
266
  const [opacity, setOpacity] = createSignal(0);
264
267
  const [positionTick, setPositionTick] = createSignal(0);
@@ -308,7 +311,7 @@ var Label = (props) => {
308
311
  if (props.variant === "success") {
309
312
  const fadeOutTimer = setTimeout(() => {
310
313
  setOpacity(0);
311
- }, 1500);
314
+ }, SUCCESS_LABEL_DURATION_MS);
312
315
  onCleanup(() => clearTimeout(fadeOutTimer));
313
316
  }
314
317
  }));
@@ -329,34 +332,20 @@ var Label = (props) => {
329
332
  left: currentX,
330
333
  top: currentY
331
334
  };
332
- if (props.variant === "success") {
333
- const indicatorLeft = Math.round(currentX);
334
- const indicatorTop = Math.round(currentY) - boundingRect.height - 6;
335
- const willClampLeft = indicatorLeft < VIEWPORT_MARGIN_PX;
336
- const willClampTop = indicatorTop < VIEWPORT_MARGIN_PX;
337
- const isClamped = willClampLeft || willClampTop;
338
- const clamped = getClampedElementPosition(indicatorLeft, indicatorTop, boundingRect.width, boundingRect.height);
339
- if (isClamped) {
340
- clamped.left += INDICATOR_CLAMP_PADDING_PX;
341
- clamped.top += INDICATOR_CLAMP_PADDING_PX;
342
- }
343
- return clamped;
344
- }
345
- const CROSSHAIR_OFFSET = 12;
346
335
  const viewportWidth = window.innerWidth;
347
336
  const viewportHeight = window.innerHeight;
348
337
  const quadrants = [{
349
- left: Math.round(currentX) + CROSSHAIR_OFFSET,
350
- top: Math.round(currentY) + CROSSHAIR_OFFSET
338
+ left: Math.round(currentX) + CURSOR_OFFSET_PX,
339
+ top: Math.round(currentY) + CURSOR_OFFSET_PX
351
340
  }, {
352
- left: Math.round(currentX) - boundingRect.width - CROSSHAIR_OFFSET,
353
- top: Math.round(currentY) + CROSSHAIR_OFFSET
341
+ left: Math.round(currentX) - boundingRect.width - CURSOR_OFFSET_PX,
342
+ top: Math.round(currentY) + CURSOR_OFFSET_PX
354
343
  }, {
355
- left: Math.round(currentX) + CROSSHAIR_OFFSET,
356
- top: Math.round(currentY) - boundingRect.height - CROSSHAIR_OFFSET
344
+ left: Math.round(currentX) + CURSOR_OFFSET_PX,
345
+ top: Math.round(currentY) - boundingRect.height - CURSOR_OFFSET_PX
357
346
  }, {
358
- left: Math.round(currentX) - boundingRect.width - CROSSHAIR_OFFSET,
359
- top: Math.round(currentY) - boundingRect.height - CROSSHAIR_OFFSET
347
+ left: Math.round(currentX) - boundingRect.width - CURSOR_OFFSET_PX,
348
+ top: Math.round(currentY) - boundingRect.height - CURSOR_OFFSET_PX
360
349
  }];
361
350
  for (const position of quadrants) {
362
351
  const fitsHorizontally = position.left >= VIEWPORT_MARGIN_PX && position.left + boundingRect.width <= viewportWidth - VIEWPORT_MARGIN_PX;
@@ -375,7 +364,7 @@ var Label = (props) => {
375
364
  return props.visible !== false;
376
365
  },
377
366
  get children() {
378
- var _el$ = _tmpl$4();
367
+ var _el$ = _tmpl$5();
379
368
  var _ref$ = labelRef;
380
369
  typeof _ref$ === "function" ? use(_ref$, _el$) : labelRef = _el$;
381
370
  insert(_el$, createComponent(Show, {
@@ -410,17 +399,12 @@ var Label = (props) => {
410
399
  }), null);
411
400
  insert(_el$, createComponent(Show, {
412
401
  get when() {
413
- return props.text.startsWith("(");
414
- },
415
- get fallback() {
416
- return (() => {
417
- var _el$5 = _tmpl$5();
418
- insert(_el$5, () => props.text);
419
- return _el$5;
420
- })();
402
+ return props.variant !== "processing";
421
403
  },
422
404
  get children() {
423
- return props.text;
405
+ var _el$4 = _tmpl$32();
406
+ insert(_el$4, () => props.text);
407
+ return _el$4;
424
408
  }
425
409
  }), null);
426
410
  insert(_el$, createComponent(Show, {
@@ -428,7 +412,7 @@ var Label = (props) => {
428
412
  return props.variant === "success";
429
413
  },
430
414
  get children() {
431
- return _tmpl$32();
415
+ return _tmpl$4();
432
416
  }
433
417
  }), null);
434
418
  effect((_p$) => {
@@ -528,53 +512,16 @@ var Crosshair = (props) => {
528
512
  context.scale(dpr, dpr);
529
513
  }
530
514
  };
531
- const getSmallestElementBounds = (x, y) => {
532
- const element = document.elementFromPoint(x, y);
533
- if (!element || element === canvasRef || element === document.body || element === document.documentElement) {
534
- return null;
535
- }
536
- const rect = element.getBoundingClientRect();
537
- if (rect.width === 0 || rect.height === 0) {
538
- return null;
539
- }
540
- return {
541
- top: rect.top,
542
- bottom: rect.bottom,
543
- left: rect.left,
544
- right: rect.right
545
- };
546
- };
547
- const scanBoundary = (startX, startY, deltaX, deltaY, maxDistance) => {
548
- const baseBounds = getSmallestElementBounds(startX, startY);
549
- if (!baseBounds) return maxDistance;
550
- const stepSize = 5;
551
- for (let distance = stepSize; distance < maxDistance; distance += stepSize) {
552
- const checkX = startX + deltaX * distance;
553
- const checkY = startY + deltaY * distance;
554
- if (checkX < 0 || checkX >= width || checkY < 0 || checkY >= height) {
555
- return distance;
556
- }
557
- const currentBounds = getSmallestElementBounds(checkX, checkY);
558
- if (!currentBounds || currentBounds.top !== baseBounds.top || currentBounds.bottom !== baseBounds.bottom || currentBounds.left !== baseBounds.left || currentBounds.right !== baseBounds.right) {
559
- return distance;
560
- }
561
- }
562
- return maxDistance;
563
- };
564
515
  const render2 = () => {
565
516
  if (!context) return;
566
517
  context.clearRect(0, 0, width, height);
567
518
  context.strokeStyle = "rgba(210, 57, 192)";
568
519
  context.lineWidth = 1;
569
520
  context.beginPath();
570
- const topBoundary = scanBoundary(currentX, currentY, 0, -1, currentY);
571
- const bottomBoundary = scanBoundary(currentX, currentY, 0, 1, height - currentY);
572
- const leftBoundary = scanBoundary(currentX, currentY, -1, 0, currentX);
573
- const rightBoundary = scanBoundary(currentX, currentY, 1, 0, width - currentX);
574
- context.moveTo(currentX, currentY - topBoundary);
575
- context.lineTo(currentX, currentY + bottomBoundary);
576
- context.moveTo(currentX - leftBoundary, currentY);
577
- context.lineTo(currentX + rightBoundary, currentY);
521
+ context.moveTo(currentX, 0);
522
+ context.lineTo(currentX, height);
523
+ context.moveTo(0, currentY);
524
+ context.lineTo(width, currentY);
578
525
  context.stroke();
579
526
  };
580
527
  const animate = () => {
@@ -765,26 +712,43 @@ instrument({
765
712
  _fiberRoots.add(fiberRoot);
766
713
  }
767
714
  });
768
- var getSourceTrace = async (element) => {
769
- const fiber = getFiberFromHostInstance(element);
770
- if (!fiber) return null;
771
- const ownerStack = getOwnerStack(fiber);
772
- const sources = await getSourcesFromStack(
773
- ownerStack,
774
- Number.MAX_SAFE_INTEGER
775
- );
776
- if (!sources) return null;
777
- console.log(sources);
778
- return sources.map((source) => {
779
- return {
780
- ...source,
781
- fileName: normalizeFileName(source.fileName)
782
- };
783
- }).filter((source) => {
784
- return isSourceFile(source.fileName);
785
- });
715
+ var generateCSSSelector = (element) => {
716
+ return finder(element);
786
717
  };
787
- var getHTMLSnippet = (element) => {
718
+ var getHTMLSnippet = async (element) => {
719
+ const truncateString = (string, maxLength) => string.length > maxLength ? `${string.substring(0, maxLength)}...` : string;
720
+ const isInternalComponent = (name) => {
721
+ if (name.startsWith("_")) return true;
722
+ if (name.includes("Provider") && name.includes("Context")) return true;
723
+ return false;
724
+ };
725
+ const extractReactComponentName = (el) => {
726
+ const fiber = getFiberFromHostInstance(el);
727
+ if (!fiber) return null;
728
+ let componentName = null;
729
+ traverseFiber(
730
+ fiber,
731
+ (currentFiber) => {
732
+ if (isCompositeFiber(currentFiber)) {
733
+ const displayName = getDisplayName(currentFiber);
734
+ if (displayName && !isInternalComponent(displayName)) {
735
+ componentName = displayName;
736
+ return true;
737
+ }
738
+ }
739
+ return false;
740
+ },
741
+ true
742
+ );
743
+ return componentName;
744
+ };
745
+ const formatComponentSourceLocation = async (el) => {
746
+ const source = await getSourceFromHostInstance(el);
747
+ if (!source) return null;
748
+ const fileName = normalizeFileName(source.fileName);
749
+ if (!isSourceFile(fileName)) return null;
750
+ return `${fileName}:${source.lineNumber}:${source.columnNumber}`;
751
+ };
788
752
  const semanticTags = /* @__PURE__ */ new Set([
789
753
  "article",
790
754
  "aside",
@@ -807,7 +771,7 @@ var getHTMLSnippet = (element) => {
807
771
  (attr) => attr.name.startsWith("data-")
808
772
  );
809
773
  };
810
- const getAncestorChain = (el, maxDepth = 10) => {
774
+ const collectDistinguishingAncestors = (el, maxDepth = 10) => {
811
775
  const ancestors2 = [];
812
776
  let current = el.parentElement;
813
777
  let depth = 0;
@@ -821,35 +785,7 @@ var getHTMLSnippet = (element) => {
821
785
  }
822
786
  return ancestors2.reverse();
823
787
  };
824
- const getCSSPath = (el) => {
825
- const parts = [];
826
- let current = el;
827
- let depth = 0;
828
- const maxDepth = 5;
829
- while (current && depth < maxDepth && current.tagName !== "BODY") {
830
- let selector = current.tagName.toLowerCase();
831
- if (current.id) {
832
- selector += `#${current.id}`;
833
- parts.unshift(selector);
834
- break;
835
- } else if (current.className && typeof current.className === "string" && current.className.trim()) {
836
- const classes = current.className.trim().split(/\s+/).slice(0, 2);
837
- selector += `.${classes.join(".")}`;
838
- }
839
- if (!current.id && (!current.className || !current.className.trim()) && current.parentElement) {
840
- const siblings = Array.from(current.parentElement.children);
841
- const index = siblings.indexOf(current);
842
- if (index >= 0 && siblings.length > 1) {
843
- selector += `:nth-child(${index + 1})`;
844
- }
845
- }
846
- parts.unshift(selector);
847
- current = current.parentElement;
848
- depth++;
849
- }
850
- return parts.join(" > ");
851
- };
852
- const getElementTag = (el, compact = false) => {
788
+ const formatElementOpeningTag = (el, compact = false) => {
853
789
  const tagName = el.tagName.toLowerCase();
854
790
  const attrs = [];
855
791
  if (el.id) {
@@ -859,10 +795,7 @@ var getHTMLSnippet = (element) => {
859
795
  const classes = el.className.trim().split(/\s+/);
860
796
  if (classes.length > 0 && classes[0]) {
861
797
  const displayClasses = compact ? classes.slice(0, 3) : classes;
862
- let classStr = displayClasses.join(" ");
863
- if (classStr.length > 30) {
864
- classStr = classStr.substring(0, 30) + "...";
865
- }
798
+ const classStr = truncateString(displayClasses.join(" "), 30);
866
799
  attrs.push(`class="${classStr}"`);
867
800
  }
868
801
  }
@@ -871,35 +804,20 @@ var getHTMLSnippet = (element) => {
871
804
  );
872
805
  const displayDataAttrs = compact ? dataAttrs.slice(0, 1) : dataAttrs;
873
806
  for (const attr of displayDataAttrs) {
874
- let value = attr.value;
875
- if (value.length > 20) {
876
- value = value.substring(0, 20) + "...";
877
- }
878
- attrs.push(`${attr.name}="${value}"`);
807
+ attrs.push(`${attr.name}="${truncateString(attr.value, 20)}"`);
879
808
  }
880
809
  const ariaLabel = el.getAttribute("aria-label");
881
810
  if (ariaLabel && !compact) {
882
- let value = ariaLabel;
883
- if (value.length > 20) {
884
- value = value.substring(0, 20) + "...";
885
- }
886
- attrs.push(`aria-label="${value}"`);
811
+ attrs.push(`aria-label="${truncateString(ariaLabel, 20)}"`);
887
812
  }
888
813
  return attrs.length > 0 ? `<${tagName} ${attrs.join(" ")}>` : `<${tagName}>`;
889
814
  };
890
- const getClosingTag = (el) => {
891
- return `</${el.tagName.toLowerCase()}>`;
892
- };
893
- const getTextContent = (el) => {
894
- let text = el.textContent || "";
895
- text = text.trim().replace(/\s+/g, " ");
896
- const maxLength = 60;
897
- if (text.length > maxLength) {
898
- text = text.substring(0, maxLength) + "...";
899
- }
900
- return text;
815
+ const formatElementClosingTag = (el) => `</${el.tagName.toLowerCase()}>`;
816
+ const extractTruncatedTextContent = (el) => {
817
+ const text = (el.textContent || "").trim().replace(/\s+/g, " ");
818
+ return truncateString(text, 60);
901
819
  };
902
- const getSiblingIdentifier = (el) => {
820
+ const extractSiblingIdentifier = (el) => {
903
821
  if (el.id) return `#${el.id}`;
904
822
  if (el.className && typeof el.className === "string") {
905
823
  const classes = el.className.trim().split(/\s+/);
@@ -910,12 +828,31 @@ var getHTMLSnippet = (element) => {
910
828
  return null;
911
829
  };
912
830
  const lines = [];
913
- lines.push(`Path: ${getCSSPath(element)}`);
914
- lines.push("");
915
- const ancestors = getAncestorChain(element);
831
+ const selector = generateCSSSelector(element);
832
+ lines.push(`Locate this element in the codebase:`);
833
+ lines.push(`- selector: ${selector}`);
834
+ const rect = element.getBoundingClientRect();
835
+ lines.push(`- width: ${Math.round(rect.width)}`);
836
+ lines.push(`- height: ${Math.round(rect.height)}`);
837
+ lines.push("HTML snippet:");
838
+ lines.push("```html");
839
+ const ancestors = collectDistinguishingAncestors(element);
840
+ const ancestorComponents = ancestors.map(
841
+ (ancestor) => extractReactComponentName(ancestor)
842
+ );
843
+ const elementComponent = extractReactComponentName(element);
844
+ const ancestorSources = await Promise.all(
845
+ ancestors.map((ancestor) => formatComponentSourceLocation(ancestor))
846
+ );
847
+ const elementSource = await formatComponentSourceLocation(element);
916
848
  for (let i = 0; i < ancestors.length; i++) {
917
849
  const indent2 = " ".repeat(i);
918
- lines.push(indent2 + getElementTag(ancestors[i], true));
850
+ const componentName = ancestorComponents[i];
851
+ const source = ancestorSources[i];
852
+ if (componentName && source && (i === 0 || ancestorComponents[i - 1] !== componentName)) {
853
+ lines.push(`${indent2}<${componentName} source="${source}">`);
854
+ }
855
+ lines.push(`${indent2}${formatElementOpeningTag(ancestors[i], true)}`);
919
856
  }
920
857
  const parent = element.parentElement;
921
858
  let targetIndex = -1;
@@ -924,10 +861,10 @@ var getHTMLSnippet = (element) => {
924
861
  targetIndex = siblings.indexOf(element);
925
862
  if (targetIndex > 0) {
926
863
  const prevSibling = siblings[targetIndex - 1];
927
- const prevId = getSiblingIdentifier(prevSibling);
864
+ const prevId = extractSiblingIdentifier(prevSibling);
928
865
  if (prevId && targetIndex <= 2) {
929
866
  const indent2 = " ".repeat(ancestors.length);
930
- lines.push(`${indent2} ${getElementTag(prevSibling, true)}`);
867
+ lines.push(`${indent2} ${formatElementOpeningTag(prevSibling, true)}`);
931
868
  lines.push(`${indent2} </${prevSibling.tagName.toLowerCase()}>`);
932
869
  } else if (targetIndex > 0) {
933
870
  const indent2 = " ".repeat(ancestors.length);
@@ -938,35 +875,44 @@ var getHTMLSnippet = (element) => {
938
875
  }
939
876
  }
940
877
  const indent = " ".repeat(ancestors.length);
941
- lines.push(indent + " <!-- SELECTED -->");
942
- const textContent = getTextContent(element);
878
+ const lastAncestorComponent = ancestors.length > 0 ? ancestorComponents[ancestorComponents.length - 1] : null;
879
+ const showElementComponent = elementComponent && elementSource && elementComponent !== lastAncestorComponent;
880
+ if (showElementComponent) {
881
+ lines.push(`${indent} <${elementComponent} used-at="${elementSource}">`);
882
+ }
883
+ lines.push(`${indent} <!-- IMPORTANT: selected element -->`);
884
+ const textContent = extractTruncatedTextContent(element);
943
885
  const childrenCount = element.children.length;
886
+ const elementIndent = `${indent}${showElementComponent ? " " : " "}`;
944
887
  if (textContent && childrenCount === 0 && textContent.length < 40) {
945
888
  lines.push(
946
- `${indent} ${getElementTag(element)}${textContent}${getClosingTag(
889
+ `${elementIndent}${formatElementOpeningTag(element)}${textContent}${formatElementClosingTag(
947
890
  element
948
891
  )}`
949
892
  );
950
893
  } else {
951
- lines.push(indent + " " + getElementTag(element));
894
+ lines.push(`${elementIndent}${formatElementOpeningTag(element)}`);
952
895
  if (textContent) {
953
- lines.push(`${indent} ${textContent}`);
896
+ lines.push(`${elementIndent} ${textContent}`);
954
897
  }
955
898
  if (childrenCount > 0) {
956
899
  lines.push(
957
- `${indent} ... (${childrenCount} element${childrenCount === 1 ? "" : "s"})`
900
+ `${elementIndent} ... (${childrenCount} element${childrenCount === 1 ? "" : "s"})`
958
901
  );
959
902
  }
960
- lines.push(indent + " " + getClosingTag(element));
903
+ lines.push(`${elementIndent}${formatElementClosingTag(element)}`);
904
+ }
905
+ if (showElementComponent) {
906
+ lines.push(`${indent} </${elementComponent}>`);
961
907
  }
962
908
  if (parent && targetIndex >= 0) {
963
909
  const siblings = Array.from(parent.children);
964
910
  const siblingsAfter = siblings.length - targetIndex - 1;
965
911
  if (siblingsAfter > 0) {
966
912
  const nextSibling = siblings[targetIndex + 1];
967
- const nextId = getSiblingIdentifier(nextSibling);
913
+ const nextId = extractSiblingIdentifier(nextSibling);
968
914
  if (nextId && siblingsAfter <= 2) {
969
- lines.push(`${indent} ${getElementTag(nextSibling, true)}`);
915
+ lines.push(`${indent} ${formatElementOpeningTag(nextSibling, true)}`);
970
916
  lines.push(`${indent} </${nextSibling.tagName.toLowerCase()}>`);
971
917
  } else {
972
918
  lines.push(
@@ -977,8 +923,14 @@ var getHTMLSnippet = (element) => {
977
923
  }
978
924
  for (let i = ancestors.length - 1; i >= 0; i--) {
979
925
  const indent2 = " ".repeat(i);
980
- lines.push(indent2 + getClosingTag(ancestors[i]));
926
+ lines.push(`${indent2}${formatElementClosingTag(ancestors[i])}`);
927
+ const componentName = ancestorComponents[i];
928
+ const source = ancestorSources[i];
929
+ if (componentName && source && (i === ancestors.length - 1 || ancestorComponents[i + 1] !== componentName)) {
930
+ lines.push(`${indent2}</${componentName}>`);
931
+ }
981
932
  }
933
+ lines.push("```");
982
934
  return lines.join("\n");
983
935
  };
984
936
 
@@ -1208,8 +1160,8 @@ var createElementBounds = (element) => {
1208
1160
  };
1209
1161
 
1210
1162
  // src/core.tsx
1211
- var SUCCESS_LABEL_DURATION_MS = 1700;
1212
1163
  var PROGRESS_INDICATOR_DELAY_MS = 150;
1164
+ var QUICK_REPRESS_THRESHOLD_MS = 150;
1213
1165
  var init = (rawOptions) => {
1214
1166
  const options = {
1215
1167
  enabled: true,
@@ -1235,14 +1187,19 @@ var init = (rawOptions) => {
1235
1187
  const [successLabels, setSuccessLabels] = createSignal([]);
1236
1188
  const [isActivated, setIsActivated] = createSignal(false);
1237
1189
  const [showProgressIndicator, setShowProgressIndicator] = createSignal(false);
1190
+ const [didJustDrag, setDidJustDrag] = createSignal(false);
1191
+ const [isModifierHeld, setIsModifierHeld] = createSignal(false);
1192
+ const [copyStartX, setCopyStartX] = createSignal(OFFSCREEN_POSITION);
1193
+ const [copyStartY, setCopyStartY] = createSignal(OFFSCREEN_POSITION);
1238
1194
  let holdTimerId = null;
1239
1195
  let progressAnimationId = null;
1240
1196
  let progressDelayTimerId = null;
1241
1197
  let keydownSpamTimerId = null;
1198
+ let lastDeactivationTime = null;
1242
1199
  const isRendererActive = createMemo(() => isActivated() && !isCopying());
1243
1200
  const hasValidMousePosition = createMemo(() => mouseX() > OFFSCREEN_POSITION && mouseY() > OFFSCREEN_POSITION);
1244
1201
  const isTargetKeyCombination = (event) => (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "c";
1245
- const addGrabbedBox = (bounds) => {
1202
+ const showTemporaryGrabbedBox = (bounds) => {
1246
1203
  const boxId = `grabbed-${Date.now()}-${Math.random()}`;
1247
1204
  const createdAt = Date.now();
1248
1205
  const newBox = {
@@ -1256,7 +1213,7 @@ var init = (rawOptions) => {
1256
1213
  setGrabbedBoxes((previousBoxes) => previousBoxes.filter((box) => box.id !== boxId));
1257
1214
  }, SUCCESS_LABEL_DURATION_MS);
1258
1215
  };
1259
- const addSuccessLabel = (text, positionX, positionY) => {
1216
+ const showTemporarySuccessLabel = (text, positionX, positionY) => {
1260
1217
  const labelId = `success-${Date.now()}-${Math.random()}`;
1261
1218
  setSuccessLabels((previousLabels) => [...previousLabels, {
1262
1219
  id: labelId,
@@ -1268,24 +1225,15 @@ var init = (rawOptions) => {
1268
1225
  setSuccessLabels((previousLabels) => previousLabels.filter((label) => label.id !== labelId));
1269
1226
  }, SUCCESS_LABEL_DURATION_MS);
1270
1227
  };
1271
- const formatStackTrace = (stackTrace) => {
1272
- return stackTrace.map((source) => {
1273
- const functionName = source.functionName ?? "anonymous";
1274
- const fileName = source.fileName ?? "unknown";
1275
- const lineNumber = source.lineNumber ?? 0;
1276
- const columnNumber = source.columnNumber ?? 0;
1277
- return ` at ${functionName} (${fileName}:${lineNumber}:${columnNumber})`;
1278
- }).join("\n");
1279
- };
1280
- const wrapContextInXmlTags = (context) => {
1281
- return `<selected_element>${context}</selected_element>`;
1282
- };
1283
- const getComputedStyles = (element) => {
1228
+ const wrapInSelectedElementTags = (context) => `<selected_element>
1229
+ ${context}
1230
+ </selected_element>`;
1231
+ const extractRelevantComputedStyles = (element) => {
1284
1232
  const computed = window.getComputedStyle(element);
1285
1233
  const rect = element.getBoundingClientRect();
1286
1234
  return {
1287
- width: `${rect.width}px`,
1288
- height: `${rect.height}px`,
1235
+ width: `${Math.round(rect.width)}px`,
1236
+ height: `${Math.round(rect.height)}px`,
1289
1237
  paddingTop: computed.paddingTop,
1290
1238
  paddingRight: computed.paddingRight,
1291
1239
  paddingBottom: computed.paddingBottom,
@@ -1294,7 +1242,7 @@ var init = (rawOptions) => {
1294
1242
  opacity: computed.opacity
1295
1243
  };
1296
1244
  };
1297
- const createStructuredClipboardHtml = (elements) => {
1245
+ const createStructuredClipboardHtmlBlob = (elements) => {
1298
1246
  const structuredData = {
1299
1247
  elements: elements.map((element) => ({
1300
1248
  tagName: element.tagName,
@@ -1308,60 +1256,74 @@ var init = (rawOptions) => {
1308
1256
  type: "text/html"
1309
1257
  });
1310
1258
  };
1311
- const getElementContentWithTrace = async (element) => {
1312
- const elementHtml = getHTMLSnippet(element);
1313
- const componentStackTrace = await getSourceTrace(element);
1314
- if (componentStackTrace?.length) {
1315
- const formattedStackTrace = formatStackTrace(componentStackTrace);
1316
- return `${elementHtml}
1317
-
1318
- Component owner stack:
1319
- ${formattedStackTrace}`;
1320
- }
1321
- return elementHtml;
1259
+ const extractElementTagName = (element) => (element.tagName || "").toLowerCase();
1260
+ const executeCopyOperation = async (positionX, positionY, operation) => {
1261
+ setCopyStartX(positionX);
1262
+ setCopyStartY(positionY);
1263
+ setIsCopying(true);
1264
+ await operation().finally(() => {
1265
+ setIsCopying(false);
1266
+ });
1322
1267
  };
1323
- const getElementTagName = (element) => (element.tagName || "").toLowerCase();
1324
- const handleCopy = async (targetElement2) => {
1325
- const elementBounds = targetElement2.getBoundingClientRect();
1326
- const tagName = getElementTagName(targetElement2);
1327
- addGrabbedBox(createElementBounds(targetElement2));
1268
+ const copySingleElementToClipboard = async (targetElement2) => {
1269
+ const tagName = extractElementTagName(targetElement2);
1270
+ showTemporaryGrabbedBox(createElementBounds(targetElement2));
1328
1271
  try {
1329
- const content = await getElementContentWithTrace(targetElement2);
1330
- const plainTextContent = wrapContextInXmlTags(content);
1331
- const htmlContent = createStructuredClipboardHtml([{
1272
+ const content = await getHTMLSnippet(targetElement2);
1273
+ const plainTextContent = wrapInSelectedElementTags(content);
1274
+ const htmlContent = createStructuredClipboardHtmlBlob([{
1332
1275
  tagName,
1333
- content: await getElementContentWithTrace(targetElement2),
1334
- computedStyles: getComputedStyles(targetElement2)
1276
+ content,
1277
+ computedStyles: extractRelevantComputedStyles(targetElement2)
1335
1278
  }]);
1336
- await copyContent([plainTextContent, htmlContent]);
1279
+ const clipboardData = [plainTextContent, htmlContent];
1280
+ try {
1281
+ const screenshotDataUrl = await domToPng(targetElement2);
1282
+ const response = await fetch(screenshotDataUrl);
1283
+ const pngBlob = await response.blob();
1284
+ const imagePngBlob = new Blob([pngBlob], {
1285
+ type: "image/png"
1286
+ });
1287
+ clipboardData.push(imagePngBlob);
1288
+ } catch {
1289
+ }
1290
+ await copyContent(clipboardData);
1337
1291
  } catch {
1338
1292
  }
1339
- addSuccessLabel(tagName ? `<${tagName}>` : "<element>", elementBounds.left, elementBounds.top);
1293
+ showTemporarySuccessLabel(tagName ? `<${tagName}>` : "<element>", copyStartX(), copyStartY());
1340
1294
  };
1341
- const handleMultipleCopy = async (targetElements) => {
1295
+ const copyMultipleElementsToClipboard = async (targetElements) => {
1342
1296
  if (targetElements.length === 0) return;
1343
- let minPositionX = Infinity;
1344
- let minPositionY = Infinity;
1345
1297
  for (const element of targetElements) {
1346
- const elementBounds = element.getBoundingClientRect();
1347
- minPositionX = Math.min(minPositionX, elementBounds.left);
1348
- minPositionY = Math.min(minPositionY, elementBounds.top);
1349
- addGrabbedBox(createElementBounds(element));
1298
+ showTemporaryGrabbedBox(createElementBounds(element));
1350
1299
  }
1351
1300
  try {
1352
- const elementSnippets = await Promise.all(targetElements.map((element) => getElementContentWithTrace(element)));
1301
+ const elementSnippets = await Promise.all(targetElements.map((element) => getHTMLSnippet(element)));
1353
1302
  const combinedContent = elementSnippets.join("\n\n---\n\n");
1354
- const plainTextContent = wrapContextInXmlTags(combinedContent);
1355
- const structuredElements = await Promise.all(targetElements.map(async (element) => ({
1356
- tagName: getElementTagName(element),
1357
- content: await getElementContentWithTrace(element),
1358
- computedStyles: getComputedStyles(element)
1359
- })));
1360
- const htmlContent = createStructuredClipboardHtml(structuredElements);
1361
- await copyContent([plainTextContent, htmlContent]);
1303
+ const plainTextContent = wrapInSelectedElementTags(combinedContent);
1304
+ const structuredElements = elementSnippets.map((content, index) => ({
1305
+ tagName: extractElementTagName(targetElements[index]),
1306
+ content,
1307
+ computedStyles: extractRelevantComputedStyles(targetElements[index])
1308
+ }));
1309
+ const htmlContent = createStructuredClipboardHtmlBlob(structuredElements);
1310
+ const clipboardData = [plainTextContent, htmlContent];
1311
+ if (targetElements.length > 0) {
1312
+ try {
1313
+ const screenshotDataUrl = await domToPng(targetElements[0]);
1314
+ const response = await fetch(screenshotDataUrl);
1315
+ const pngBlob = await response.blob();
1316
+ const imagePngBlob = new Blob([pngBlob], {
1317
+ type: "image/png"
1318
+ });
1319
+ clipboardData.push(imagePngBlob);
1320
+ } catch {
1321
+ }
1322
+ }
1323
+ await copyContent(clipboardData);
1362
1324
  } catch {
1363
1325
  }
1364
- addSuccessLabel(`${targetElements.length} elements`, minPositionX, minPositionY);
1326
+ showTemporarySuccessLabel(`${targetElements.length} elements`, copyStartX(), copyStartY());
1365
1327
  };
1366
1328
  const targetElement = createMemo(() => {
1367
1329
  if (!isRendererActive() || isDragging()) return null;
@@ -1382,16 +1344,16 @@ ${formattedStackTrace}`;
1382
1344
  };
1383
1345
  });
1384
1346
  const DRAG_THRESHOLD_PX = 2;
1385
- const getDragDistance = (endX, endY) => ({
1347
+ const calculateDragDistance = (endX, endY) => ({
1386
1348
  x: Math.abs(endX - dragStartX()),
1387
1349
  y: Math.abs(endY - dragStartY())
1388
1350
  });
1389
1351
  const isDraggingBeyondThreshold = createMemo(() => {
1390
1352
  if (!isDragging()) return false;
1391
- const dragDistance = getDragDistance(mouseX(), mouseY());
1353
+ const dragDistance = calculateDragDistance(mouseX(), mouseY());
1392
1354
  return dragDistance.x > DRAG_THRESHOLD_PX || dragDistance.y > DRAG_THRESHOLD_PX;
1393
1355
  });
1394
- const getDragRect = (endX, endY) => {
1356
+ const calculateDragRectangle = (endX, endY) => {
1395
1357
  const dragX = Math.min(dragStartX(), endX);
1396
1358
  const dragY = Math.min(dragStartY(), endY);
1397
1359
  const dragWidth = Math.abs(endX - dragStartX());
@@ -1405,7 +1367,7 @@ ${formattedStackTrace}`;
1405
1367
  };
1406
1368
  const dragBounds = createMemo(() => {
1407
1369
  if (!isDraggingBeyondThreshold()) return void 0;
1408
- const drag = getDragRect(mouseX(), mouseY());
1370
+ const drag = calculateDragRectangle(mouseX(), mouseY());
1409
1371
  return {
1410
1372
  borderRadius: "0px",
1411
1373
  height: drag.height,
@@ -1417,19 +1379,16 @@ ${formattedStackTrace}`;
1417
1379
  });
1418
1380
  const labelText = createMemo(() => {
1419
1381
  const element = targetElement();
1420
- return element ? `<${getElementTagName(element)}>` : "<element>";
1382
+ return element ? `<${extractElementTagName(element)}>` : "<element>";
1421
1383
  });
1422
- const labelPosition = createMemo(() => {
1423
- return {
1424
- x: mouseX(),
1425
- y: mouseY()
1426
- };
1427
- });
1428
- const isSameAsLast = createMemo(() => {
1429
- const currentElement = targetElement();
1430
- const lastElement = lastGrabbedElement();
1431
- return Boolean(currentElement) && currentElement === lastElement;
1384
+ const labelPosition = createMemo(() => isCopying() ? {
1385
+ x: copyStartX(),
1386
+ y: copyStartY()
1387
+ } : {
1388
+ x: mouseX(),
1389
+ y: mouseY()
1432
1390
  });
1391
+ const isSameAsLast = createMemo(() => Boolean(targetElement() && targetElement() === lastGrabbedElement()));
1433
1392
  createEffect(on(() => [targetElement(), lastGrabbedElement()], ([currentElement, lastElement]) => {
1434
1393
  if (lastElement && currentElement && lastElement !== currentElement) {
1435
1394
  setLastGrabbedElement(null);
@@ -1476,9 +1435,12 @@ ${formattedStackTrace}`;
1476
1435
  setIsActivated(true);
1477
1436
  document.body.style.cursor = "crosshair";
1478
1437
  };
1479
- const deactivateRenderer = () => {
1438
+ const deactivateRenderer = (shouldResetModifier = true) => {
1480
1439
  setIsHoldingKeys(false);
1481
1440
  setIsActivated(false);
1441
+ if (shouldResetModifier) {
1442
+ setIsModifierHeld(false);
1443
+ }
1482
1444
  document.body.style.cursor = "";
1483
1445
  if (isDragging()) {
1484
1446
  setIsDragging(false);
@@ -1487,23 +1449,38 @@ ${formattedStackTrace}`;
1487
1449
  if (holdTimerId) window.clearTimeout(holdTimerId);
1488
1450
  if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);
1489
1451
  stopProgressAnimation();
1452
+ lastDeactivationTime = Date.now();
1490
1453
  };
1491
1454
  const abortController = new AbortController();
1492
1455
  const eventListenerSignal = abortController.signal;
1493
1456
  window.addEventListener("keydown", (event) => {
1457
+ if (event.metaKey || event.ctrlKey) {
1458
+ setIsModifierHeld(true);
1459
+ }
1494
1460
  if (event.key === "Escape" && isHoldingKeys()) {
1495
1461
  deactivateRenderer();
1496
1462
  return;
1497
1463
  }
1498
1464
  if (isKeyboardEventTriggeredByInput(event)) return;
1499
1465
  if (isTargetKeyCombination(event)) {
1466
+ const wasRecentlyDeactivated = lastDeactivationTime !== null && Date.now() - lastDeactivationTime < QUICK_REPRESS_THRESHOLD_MS;
1500
1467
  if (!isHoldingKeys()) {
1501
1468
  setIsHoldingKeys(true);
1502
- startProgressAnimation();
1503
- holdTimerId = window.setTimeout(() => {
1469
+ if (wasRecentlyDeactivated && isModifierHeld()) {
1504
1470
  activateRenderer();
1505
1471
  options.onActivate?.();
1506
- }, options.keyHoldDuration);
1472
+ const element = getElementAtPosition(mouseX(), mouseY());
1473
+ if (element) {
1474
+ setLastGrabbedElement(element);
1475
+ void executeCopyOperation(mouseX(), mouseY(), () => copySingleElementToClipboard(element));
1476
+ }
1477
+ } else {
1478
+ startProgressAnimation();
1479
+ holdTimerId = window.setTimeout(() => {
1480
+ activateRenderer();
1481
+ options.onActivate?.();
1482
+ }, options.keyHoldDuration);
1483
+ }
1507
1484
  }
1508
1485
  if (isActivated()) {
1509
1486
  if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);
@@ -1516,11 +1493,16 @@ ${formattedStackTrace}`;
1516
1493
  signal: eventListenerSignal
1517
1494
  });
1518
1495
  window.addEventListener("keyup", (event) => {
1519
- if (!isHoldingKeys() && !isActivated()) return;
1520
- const isReleasingC = event.key.toLowerCase() === "c";
1521
1496
  const isReleasingModifier = !event.metaKey && !event.ctrlKey;
1522
- if (isReleasingC || isReleasingModifier) {
1523
- deactivateRenderer();
1497
+ const isReleasingC = event.key.toLowerCase() === "c";
1498
+ if (isReleasingModifier) {
1499
+ setIsModifierHeld(false);
1500
+ }
1501
+ if (!isHoldingKeys() && !isActivated()) return;
1502
+ if (isReleasingC) {
1503
+ deactivateRenderer(false);
1504
+ } else if (isReleasingModifier) {
1505
+ deactivateRenderer(true);
1524
1506
  }
1525
1507
  }, {
1526
1508
  signal: eventListenerSignal,
@@ -1544,39 +1526,41 @@ ${formattedStackTrace}`;
1544
1526
  });
1545
1527
  window.addEventListener("mouseup", (event) => {
1546
1528
  if (!isDragging()) return;
1547
- const dragDistance = getDragDistance(event.clientX, event.clientY);
1529
+ const dragDistance = calculateDragDistance(event.clientX, event.clientY);
1548
1530
  const wasDragGesture = dragDistance.x > DRAG_THRESHOLD_PX || dragDistance.y > DRAG_THRESHOLD_PX;
1549
1531
  setIsDragging(false);
1550
1532
  document.body.style.userSelect = "";
1551
1533
  if (wasDragGesture) {
1552
- const dragRect = getDragRect(event.clientX, event.clientY);
1534
+ setDidJustDrag(true);
1535
+ const dragRect = calculateDragRectangle(event.clientX, event.clientY);
1553
1536
  const elements = getElementsInDrag(dragRect, isValidGrabbableElement);
1554
1537
  if (elements.length > 0) {
1555
- setIsCopying(true);
1556
- void handleMultipleCopy(elements).finally(() => {
1557
- setIsCopying(false);
1558
- });
1538
+ void executeCopyOperation(event.clientX, event.clientY, () => copyMultipleElementsToClipboard(elements));
1559
1539
  } else {
1560
1540
  const fallbackElements = getElementsInDragLoose(dragRect, isValidGrabbableElement);
1561
1541
  if (fallbackElements.length > 0) {
1562
- setIsCopying(true);
1563
- void handleMultipleCopy(fallbackElements).finally(() => {
1564
- setIsCopying(false);
1565
- });
1542
+ void executeCopyOperation(event.clientX, event.clientY, () => copyMultipleElementsToClipboard(fallbackElements));
1566
1543
  }
1567
1544
  }
1568
1545
  } else {
1569
1546
  const element = getElementAtPosition(event.clientX, event.clientY);
1570
1547
  if (!element) return;
1571
- setIsCopying(true);
1572
1548
  setLastGrabbedElement(element);
1573
- void handleCopy(element).finally(() => {
1574
- setIsCopying(false);
1575
- });
1549
+ void executeCopyOperation(event.clientX, event.clientY, () => copySingleElementToClipboard(element));
1576
1550
  }
1577
1551
  }, {
1578
1552
  signal: eventListenerSignal
1579
1553
  });
1554
+ window.addEventListener("click", (event) => {
1555
+ if (didJustDrag()) {
1556
+ event.preventDefault();
1557
+ event.stopPropagation();
1558
+ setDidJustDrag(false);
1559
+ }
1560
+ }, {
1561
+ signal: eventListenerSignal,
1562
+ capture: true
1563
+ });
1580
1564
  document.addEventListener("visibilitychange", () => {
1581
1565
  if (document.hidden) {
1582
1566
  setGrabbedBoxes([]);