react-grab 0.0.28 → 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$) => {
@@ -728,26 +712,43 @@ instrument({
728
712
  _fiberRoots.add(fiberRoot);
729
713
  }
730
714
  });
731
- var getSourceTrace = async (element) => {
732
- const fiber = getFiberFromHostInstance(element);
733
- if (!fiber) return null;
734
- const ownerStack = getOwnerStack(fiber);
735
- const sources = await getSourcesFromStack(
736
- ownerStack,
737
- Number.MAX_SAFE_INTEGER
738
- );
739
- if (!sources) return null;
740
- console.log(sources);
741
- return sources.map((source) => {
742
- return {
743
- ...source,
744
- fileName: normalizeFileName(source.fileName)
745
- };
746
- }).filter((source) => {
747
- return isSourceFile(source.fileName);
748
- });
715
+ var generateCSSSelector = (element) => {
716
+ return finder(element);
749
717
  };
750
- 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
+ };
751
752
  const semanticTags = /* @__PURE__ */ new Set([
752
753
  "article",
753
754
  "aside",
@@ -770,7 +771,7 @@ var getHTMLSnippet = (element) => {
770
771
  (attr) => attr.name.startsWith("data-")
771
772
  );
772
773
  };
773
- const getAncestorChain = (el, maxDepth = 10) => {
774
+ const collectDistinguishingAncestors = (el, maxDepth = 10) => {
774
775
  const ancestors2 = [];
775
776
  let current = el.parentElement;
776
777
  let depth = 0;
@@ -784,35 +785,7 @@ var getHTMLSnippet = (element) => {
784
785
  }
785
786
  return ancestors2.reverse();
786
787
  };
787
- const getCSSPath = (el) => {
788
- const parts = [];
789
- let current = el;
790
- let depth = 0;
791
- const maxDepth = 5;
792
- while (current && depth < maxDepth && current.tagName !== "BODY") {
793
- let selector = current.tagName.toLowerCase();
794
- if (current.id) {
795
- selector += `#${current.id}`;
796
- parts.unshift(selector);
797
- break;
798
- } else if (current.className && typeof current.className === "string" && current.className.trim()) {
799
- const classes = current.className.trim().split(/\s+/).slice(0, 2);
800
- selector += `.${classes.join(".")}`;
801
- }
802
- if (!current.id && (!current.className || !current.className.trim()) && current.parentElement) {
803
- const siblings = Array.from(current.parentElement.children);
804
- const index = siblings.indexOf(current);
805
- if (index >= 0 && siblings.length > 1) {
806
- selector += `:nth-child(${index + 1})`;
807
- }
808
- }
809
- parts.unshift(selector);
810
- current = current.parentElement;
811
- depth++;
812
- }
813
- return parts.join(" > ");
814
- };
815
- const getElementTag = (el, compact = false) => {
788
+ const formatElementOpeningTag = (el, compact = false) => {
816
789
  const tagName = el.tagName.toLowerCase();
817
790
  const attrs = [];
818
791
  if (el.id) {
@@ -822,10 +795,7 @@ var getHTMLSnippet = (element) => {
822
795
  const classes = el.className.trim().split(/\s+/);
823
796
  if (classes.length > 0 && classes[0]) {
824
797
  const displayClasses = compact ? classes.slice(0, 3) : classes;
825
- let classStr = displayClasses.join(" ");
826
- if (classStr.length > 30) {
827
- classStr = classStr.substring(0, 30) + "...";
828
- }
798
+ const classStr = truncateString(displayClasses.join(" "), 30);
829
799
  attrs.push(`class="${classStr}"`);
830
800
  }
831
801
  }
@@ -834,35 +804,20 @@ var getHTMLSnippet = (element) => {
834
804
  );
835
805
  const displayDataAttrs = compact ? dataAttrs.slice(0, 1) : dataAttrs;
836
806
  for (const attr of displayDataAttrs) {
837
- let value = attr.value;
838
- if (value.length > 20) {
839
- value = value.substring(0, 20) + "...";
840
- }
841
- attrs.push(`${attr.name}="${value}"`);
807
+ attrs.push(`${attr.name}="${truncateString(attr.value, 20)}"`);
842
808
  }
843
809
  const ariaLabel = el.getAttribute("aria-label");
844
810
  if (ariaLabel && !compact) {
845
- let value = ariaLabel;
846
- if (value.length > 20) {
847
- value = value.substring(0, 20) + "...";
848
- }
849
- attrs.push(`aria-label="${value}"`);
811
+ attrs.push(`aria-label="${truncateString(ariaLabel, 20)}"`);
850
812
  }
851
813
  return attrs.length > 0 ? `<${tagName} ${attrs.join(" ")}>` : `<${tagName}>`;
852
814
  };
853
- const getClosingTag = (el) => {
854
- return `</${el.tagName.toLowerCase()}>`;
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);
855
819
  };
856
- const getTextContent = (el) => {
857
- let text = el.textContent || "";
858
- text = text.trim().replace(/\s+/g, " ");
859
- const maxLength = 60;
860
- if (text.length > maxLength) {
861
- text = text.substring(0, maxLength) + "...";
862
- }
863
- return text;
864
- };
865
- const getSiblingIdentifier = (el) => {
820
+ const extractSiblingIdentifier = (el) => {
866
821
  if (el.id) return `#${el.id}`;
867
822
  if (el.className && typeof el.className === "string") {
868
823
  const classes = el.className.trim().split(/\s+/);
@@ -873,12 +828,31 @@ var getHTMLSnippet = (element) => {
873
828
  return null;
874
829
  };
875
830
  const lines = [];
876
- lines.push(`Path: ${getCSSPath(element)}`);
877
- lines.push("");
878
- 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);
879
848
  for (let i = 0; i < ancestors.length; i++) {
880
849
  const indent2 = " ".repeat(i);
881
- 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)}`);
882
856
  }
883
857
  const parent = element.parentElement;
884
858
  let targetIndex = -1;
@@ -887,10 +861,10 @@ var getHTMLSnippet = (element) => {
887
861
  targetIndex = siblings.indexOf(element);
888
862
  if (targetIndex > 0) {
889
863
  const prevSibling = siblings[targetIndex - 1];
890
- const prevId = getSiblingIdentifier(prevSibling);
864
+ const prevId = extractSiblingIdentifier(prevSibling);
891
865
  if (prevId && targetIndex <= 2) {
892
866
  const indent2 = " ".repeat(ancestors.length);
893
- lines.push(`${indent2} ${getElementTag(prevSibling, true)}`);
867
+ lines.push(`${indent2} ${formatElementOpeningTag(prevSibling, true)}`);
894
868
  lines.push(`${indent2} </${prevSibling.tagName.toLowerCase()}>`);
895
869
  } else if (targetIndex > 0) {
896
870
  const indent2 = " ".repeat(ancestors.length);
@@ -901,35 +875,44 @@ var getHTMLSnippet = (element) => {
901
875
  }
902
876
  }
903
877
  const indent = " ".repeat(ancestors.length);
904
- lines.push(indent + " <!-- SELECTED -->");
905
- 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);
906
885
  const childrenCount = element.children.length;
886
+ const elementIndent = `${indent}${showElementComponent ? " " : " "}`;
907
887
  if (textContent && childrenCount === 0 && textContent.length < 40) {
908
888
  lines.push(
909
- `${indent} ${getElementTag(element)}${textContent}${getClosingTag(
889
+ `${elementIndent}${formatElementOpeningTag(element)}${textContent}${formatElementClosingTag(
910
890
  element
911
891
  )}`
912
892
  );
913
893
  } else {
914
- lines.push(indent + " " + getElementTag(element));
894
+ lines.push(`${elementIndent}${formatElementOpeningTag(element)}`);
915
895
  if (textContent) {
916
- lines.push(`${indent} ${textContent}`);
896
+ lines.push(`${elementIndent} ${textContent}`);
917
897
  }
918
898
  if (childrenCount > 0) {
919
899
  lines.push(
920
- `${indent} ... (${childrenCount} element${childrenCount === 1 ? "" : "s"})`
900
+ `${elementIndent} ... (${childrenCount} element${childrenCount === 1 ? "" : "s"})`
921
901
  );
922
902
  }
923
- lines.push(indent + " " + getClosingTag(element));
903
+ lines.push(`${elementIndent}${formatElementClosingTag(element)}`);
904
+ }
905
+ if (showElementComponent) {
906
+ lines.push(`${indent} </${elementComponent}>`);
924
907
  }
925
908
  if (parent && targetIndex >= 0) {
926
909
  const siblings = Array.from(parent.children);
927
910
  const siblingsAfter = siblings.length - targetIndex - 1;
928
911
  if (siblingsAfter > 0) {
929
912
  const nextSibling = siblings[targetIndex + 1];
930
- const nextId = getSiblingIdentifier(nextSibling);
913
+ const nextId = extractSiblingIdentifier(nextSibling);
931
914
  if (nextId && siblingsAfter <= 2) {
932
- lines.push(`${indent} ${getElementTag(nextSibling, true)}`);
915
+ lines.push(`${indent} ${formatElementOpeningTag(nextSibling, true)}`);
933
916
  lines.push(`${indent} </${nextSibling.tagName.toLowerCase()}>`);
934
917
  } else {
935
918
  lines.push(
@@ -940,8 +923,14 @@ var getHTMLSnippet = (element) => {
940
923
  }
941
924
  for (let i = ancestors.length - 1; i >= 0; i--) {
942
925
  const indent2 = " ".repeat(i);
943
- 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
+ }
944
932
  }
933
+ lines.push("```");
945
934
  return lines.join("\n");
946
935
  };
947
936
 
@@ -1171,8 +1160,8 @@ var createElementBounds = (element) => {
1171
1160
  };
1172
1161
 
1173
1162
  // src/core.tsx
1174
- var SUCCESS_LABEL_DURATION_MS = 1700;
1175
1163
  var PROGRESS_INDICATOR_DELAY_MS = 150;
1164
+ var QUICK_REPRESS_THRESHOLD_MS = 150;
1176
1165
  var init = (rawOptions) => {
1177
1166
  const options = {
1178
1167
  enabled: true,
@@ -1198,14 +1187,19 @@ var init = (rawOptions) => {
1198
1187
  const [successLabels, setSuccessLabels] = createSignal([]);
1199
1188
  const [isActivated, setIsActivated] = createSignal(false);
1200
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);
1201
1194
  let holdTimerId = null;
1202
1195
  let progressAnimationId = null;
1203
1196
  let progressDelayTimerId = null;
1204
1197
  let keydownSpamTimerId = null;
1198
+ let lastDeactivationTime = null;
1205
1199
  const isRendererActive = createMemo(() => isActivated() && !isCopying());
1206
1200
  const hasValidMousePosition = createMemo(() => mouseX() > OFFSCREEN_POSITION && mouseY() > OFFSCREEN_POSITION);
1207
1201
  const isTargetKeyCombination = (event) => (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "c";
1208
- const addGrabbedBox = (bounds) => {
1202
+ const showTemporaryGrabbedBox = (bounds) => {
1209
1203
  const boxId = `grabbed-${Date.now()}-${Math.random()}`;
1210
1204
  const createdAt = Date.now();
1211
1205
  const newBox = {
@@ -1219,7 +1213,7 @@ var init = (rawOptions) => {
1219
1213
  setGrabbedBoxes((previousBoxes) => previousBoxes.filter((box) => box.id !== boxId));
1220
1214
  }, SUCCESS_LABEL_DURATION_MS);
1221
1215
  };
1222
- const addSuccessLabel = (text, positionX, positionY) => {
1216
+ const showTemporarySuccessLabel = (text, positionX, positionY) => {
1223
1217
  const labelId = `success-${Date.now()}-${Math.random()}`;
1224
1218
  setSuccessLabels((previousLabels) => [...previousLabels, {
1225
1219
  id: labelId,
@@ -1231,24 +1225,15 @@ var init = (rawOptions) => {
1231
1225
  setSuccessLabels((previousLabels) => previousLabels.filter((label) => label.id !== labelId));
1232
1226
  }, SUCCESS_LABEL_DURATION_MS);
1233
1227
  };
1234
- const formatStackTrace = (stackTrace) => {
1235
- return stackTrace.map((source) => {
1236
- const functionName = source.functionName ?? "anonymous";
1237
- const fileName = source.fileName ?? "unknown";
1238
- const lineNumber = source.lineNumber ?? 0;
1239
- const columnNumber = source.columnNumber ?? 0;
1240
- return ` at ${functionName} (${fileName}:${lineNumber}:${columnNumber})`;
1241
- }).join("\n");
1242
- };
1243
- const wrapContextInXmlTags = (context) => {
1244
- return `<selected_element>${context}</selected_element>`;
1245
- };
1246
- const getComputedStyles = (element) => {
1228
+ const wrapInSelectedElementTags = (context) => `<selected_element>
1229
+ ${context}
1230
+ </selected_element>`;
1231
+ const extractRelevantComputedStyles = (element) => {
1247
1232
  const computed = window.getComputedStyle(element);
1248
1233
  const rect = element.getBoundingClientRect();
1249
1234
  return {
1250
- width: `${rect.width}px`,
1251
- height: `${rect.height}px`,
1235
+ width: `${Math.round(rect.width)}px`,
1236
+ height: `${Math.round(rect.height)}px`,
1252
1237
  paddingTop: computed.paddingTop,
1253
1238
  paddingRight: computed.paddingRight,
1254
1239
  paddingBottom: computed.paddingBottom,
@@ -1257,7 +1242,7 @@ var init = (rawOptions) => {
1257
1242
  opacity: computed.opacity
1258
1243
  };
1259
1244
  };
1260
- const createStructuredClipboardHtml = (elements) => {
1245
+ const createStructuredClipboardHtmlBlob = (elements) => {
1261
1246
  const structuredData = {
1262
1247
  elements: elements.map((element) => ({
1263
1248
  tagName: element.tagName,
@@ -1271,60 +1256,74 @@ var init = (rawOptions) => {
1271
1256
  type: "text/html"
1272
1257
  });
1273
1258
  };
1274
- const getElementContentWithTrace = async (element) => {
1275
- const elementHtml = getHTMLSnippet(element);
1276
- const componentStackTrace = await getSourceTrace(element);
1277
- if (componentStackTrace?.length) {
1278
- const formattedStackTrace = formatStackTrace(componentStackTrace);
1279
- return `${elementHtml}
1280
-
1281
- Component owner stack:
1282
- ${formattedStackTrace}`;
1283
- }
1284
- 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
+ });
1285
1267
  };
1286
- const getElementTagName = (element) => (element.tagName || "").toLowerCase();
1287
- const handleCopy = async (targetElement2) => {
1288
- const elementBounds = targetElement2.getBoundingClientRect();
1289
- const tagName = getElementTagName(targetElement2);
1290
- addGrabbedBox(createElementBounds(targetElement2));
1268
+ const copySingleElementToClipboard = async (targetElement2) => {
1269
+ const tagName = extractElementTagName(targetElement2);
1270
+ showTemporaryGrabbedBox(createElementBounds(targetElement2));
1291
1271
  try {
1292
- const content = await getElementContentWithTrace(targetElement2);
1293
- const plainTextContent = wrapContextInXmlTags(content);
1294
- const htmlContent = createStructuredClipboardHtml([{
1272
+ const content = await getHTMLSnippet(targetElement2);
1273
+ const plainTextContent = wrapInSelectedElementTags(content);
1274
+ const htmlContent = createStructuredClipboardHtmlBlob([{
1295
1275
  tagName,
1296
- content: await getElementContentWithTrace(targetElement2),
1297
- computedStyles: getComputedStyles(targetElement2)
1276
+ content,
1277
+ computedStyles: extractRelevantComputedStyles(targetElement2)
1298
1278
  }]);
1299
- 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);
1300
1291
  } catch {
1301
1292
  }
1302
- addSuccessLabel(tagName ? `<${tagName}>` : "<element>", elementBounds.left, elementBounds.top);
1293
+ showTemporarySuccessLabel(tagName ? `<${tagName}>` : "<element>", copyStartX(), copyStartY());
1303
1294
  };
1304
- const handleMultipleCopy = async (targetElements) => {
1295
+ const copyMultipleElementsToClipboard = async (targetElements) => {
1305
1296
  if (targetElements.length === 0) return;
1306
- let minPositionX = Infinity;
1307
- let minPositionY = Infinity;
1308
1297
  for (const element of targetElements) {
1309
- const elementBounds = element.getBoundingClientRect();
1310
- minPositionX = Math.min(minPositionX, elementBounds.left);
1311
- minPositionY = Math.min(minPositionY, elementBounds.top);
1312
- addGrabbedBox(createElementBounds(element));
1298
+ showTemporaryGrabbedBox(createElementBounds(element));
1313
1299
  }
1314
1300
  try {
1315
- const elementSnippets = await Promise.all(targetElements.map((element) => getElementContentWithTrace(element)));
1301
+ const elementSnippets = await Promise.all(targetElements.map((element) => getHTMLSnippet(element)));
1316
1302
  const combinedContent = elementSnippets.join("\n\n---\n\n");
1317
- const plainTextContent = wrapContextInXmlTags(combinedContent);
1318
- const structuredElements = await Promise.all(targetElements.map(async (element) => ({
1319
- tagName: getElementTagName(element),
1320
- content: await getElementContentWithTrace(element),
1321
- computedStyles: getComputedStyles(element)
1322
- })));
1323
- const htmlContent = createStructuredClipboardHtml(structuredElements);
1324
- 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);
1325
1324
  } catch {
1326
1325
  }
1327
- addSuccessLabel(`${targetElements.length} elements`, minPositionX, minPositionY);
1326
+ showTemporarySuccessLabel(`${targetElements.length} elements`, copyStartX(), copyStartY());
1328
1327
  };
1329
1328
  const targetElement = createMemo(() => {
1330
1329
  if (!isRendererActive() || isDragging()) return null;
@@ -1345,16 +1344,16 @@ ${formattedStackTrace}`;
1345
1344
  };
1346
1345
  });
1347
1346
  const DRAG_THRESHOLD_PX = 2;
1348
- const getDragDistance = (endX, endY) => ({
1347
+ const calculateDragDistance = (endX, endY) => ({
1349
1348
  x: Math.abs(endX - dragStartX()),
1350
1349
  y: Math.abs(endY - dragStartY())
1351
1350
  });
1352
1351
  const isDraggingBeyondThreshold = createMemo(() => {
1353
1352
  if (!isDragging()) return false;
1354
- const dragDistance = getDragDistance(mouseX(), mouseY());
1353
+ const dragDistance = calculateDragDistance(mouseX(), mouseY());
1355
1354
  return dragDistance.x > DRAG_THRESHOLD_PX || dragDistance.y > DRAG_THRESHOLD_PX;
1356
1355
  });
1357
- const getDragRect = (endX, endY) => {
1356
+ const calculateDragRectangle = (endX, endY) => {
1358
1357
  const dragX = Math.min(dragStartX(), endX);
1359
1358
  const dragY = Math.min(dragStartY(), endY);
1360
1359
  const dragWidth = Math.abs(endX - dragStartX());
@@ -1368,7 +1367,7 @@ ${formattedStackTrace}`;
1368
1367
  };
1369
1368
  const dragBounds = createMemo(() => {
1370
1369
  if (!isDraggingBeyondThreshold()) return void 0;
1371
- const drag = getDragRect(mouseX(), mouseY());
1370
+ const drag = calculateDragRectangle(mouseX(), mouseY());
1372
1371
  return {
1373
1372
  borderRadius: "0px",
1374
1373
  height: drag.height,
@@ -1380,21 +1379,16 @@ ${formattedStackTrace}`;
1380
1379
  });
1381
1380
  const labelText = createMemo(() => {
1382
1381
  const element = targetElement();
1383
- if (!element) return "(click or drag to select element(s))";
1384
- const tagName = getElementTagName(element);
1385
- return tagName ? `<${tagName}>` : "<element>";
1386
- });
1387
- const labelPosition = createMemo(() => {
1388
- return {
1389
- x: mouseX(),
1390
- y: mouseY()
1391
- };
1382
+ return element ? `<${extractElementTagName(element)}>` : "<element>";
1392
1383
  });
1393
- const isSameAsLast = createMemo(() => {
1394
- const currentElement = targetElement();
1395
- const lastElement = lastGrabbedElement();
1396
- return !!currentElement && currentElement === lastElement;
1384
+ const labelPosition = createMemo(() => isCopying() ? {
1385
+ x: copyStartX(),
1386
+ y: copyStartY()
1387
+ } : {
1388
+ x: mouseX(),
1389
+ y: mouseY()
1397
1390
  });
1391
+ const isSameAsLast = createMemo(() => Boolean(targetElement() && targetElement() === lastGrabbedElement()));
1398
1392
  createEffect(on(() => [targetElement(), lastGrabbedElement()], ([currentElement, lastElement]) => {
1399
1393
  if (lastElement && currentElement && lastElement !== currentElement) {
1400
1394
  setLastGrabbedElement(null);
@@ -1441,9 +1435,12 @@ ${formattedStackTrace}`;
1441
1435
  setIsActivated(true);
1442
1436
  document.body.style.cursor = "crosshair";
1443
1437
  };
1444
- const deactivateRenderer = () => {
1438
+ const deactivateRenderer = (shouldResetModifier = true) => {
1445
1439
  setIsHoldingKeys(false);
1446
1440
  setIsActivated(false);
1441
+ if (shouldResetModifier) {
1442
+ setIsModifierHeld(false);
1443
+ }
1447
1444
  document.body.style.cursor = "";
1448
1445
  if (isDragging()) {
1449
1446
  setIsDragging(false);
@@ -1452,23 +1449,38 @@ ${formattedStackTrace}`;
1452
1449
  if (holdTimerId) window.clearTimeout(holdTimerId);
1453
1450
  if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);
1454
1451
  stopProgressAnimation();
1452
+ lastDeactivationTime = Date.now();
1455
1453
  };
1456
1454
  const abortController = new AbortController();
1457
1455
  const eventListenerSignal = abortController.signal;
1458
1456
  window.addEventListener("keydown", (event) => {
1457
+ if (event.metaKey || event.ctrlKey) {
1458
+ setIsModifierHeld(true);
1459
+ }
1459
1460
  if (event.key === "Escape" && isHoldingKeys()) {
1460
1461
  deactivateRenderer();
1461
1462
  return;
1462
1463
  }
1463
1464
  if (isKeyboardEventTriggeredByInput(event)) return;
1464
1465
  if (isTargetKeyCombination(event)) {
1466
+ const wasRecentlyDeactivated = lastDeactivationTime !== null && Date.now() - lastDeactivationTime < QUICK_REPRESS_THRESHOLD_MS;
1465
1467
  if (!isHoldingKeys()) {
1466
1468
  setIsHoldingKeys(true);
1467
- startProgressAnimation();
1468
- holdTimerId = window.setTimeout(() => {
1469
+ if (wasRecentlyDeactivated && isModifierHeld()) {
1469
1470
  activateRenderer();
1470
1471
  options.onActivate?.();
1471
- }, 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
+ }
1472
1484
  }
1473
1485
  if (isActivated()) {
1474
1486
  if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);
@@ -1481,11 +1493,16 @@ ${formattedStackTrace}`;
1481
1493
  signal: eventListenerSignal
1482
1494
  });
1483
1495
  window.addEventListener("keyup", (event) => {
1484
- if (!isHoldingKeys() && !isActivated()) return;
1485
- const isReleasingC = event.key.toLowerCase() === "c";
1486
1496
  const isReleasingModifier = !event.metaKey && !event.ctrlKey;
1487
- if (isReleasingC || isReleasingModifier) {
1488
- 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);
1489
1506
  }
1490
1507
  }, {
1491
1508
  signal: eventListenerSignal,
@@ -1509,39 +1526,41 @@ ${formattedStackTrace}`;
1509
1526
  });
1510
1527
  window.addEventListener("mouseup", (event) => {
1511
1528
  if (!isDragging()) return;
1512
- const dragDistance = getDragDistance(event.clientX, event.clientY);
1529
+ const dragDistance = calculateDragDistance(event.clientX, event.clientY);
1513
1530
  const wasDragGesture = dragDistance.x > DRAG_THRESHOLD_PX || dragDistance.y > DRAG_THRESHOLD_PX;
1514
1531
  setIsDragging(false);
1515
1532
  document.body.style.userSelect = "";
1516
1533
  if (wasDragGesture) {
1517
- const dragRect = getDragRect(event.clientX, event.clientY);
1534
+ setDidJustDrag(true);
1535
+ const dragRect = calculateDragRectangle(event.clientX, event.clientY);
1518
1536
  const elements = getElementsInDrag(dragRect, isValidGrabbableElement);
1519
1537
  if (elements.length > 0) {
1520
- setIsCopying(true);
1521
- void handleMultipleCopy(elements).finally(() => {
1522
- setIsCopying(false);
1523
- });
1538
+ void executeCopyOperation(event.clientX, event.clientY, () => copyMultipleElementsToClipboard(elements));
1524
1539
  } else {
1525
1540
  const fallbackElements = getElementsInDragLoose(dragRect, isValidGrabbableElement);
1526
1541
  if (fallbackElements.length > 0) {
1527
- setIsCopying(true);
1528
- void handleMultipleCopy(fallbackElements).finally(() => {
1529
- setIsCopying(false);
1530
- });
1542
+ void executeCopyOperation(event.clientX, event.clientY, () => copyMultipleElementsToClipboard(fallbackElements));
1531
1543
  }
1532
1544
  }
1533
1545
  } else {
1534
1546
  const element = getElementAtPosition(event.clientX, event.clientY);
1535
1547
  if (!element) return;
1536
- setIsCopying(true);
1537
1548
  setLastGrabbedElement(element);
1538
- void handleCopy(element).finally(() => {
1539
- setIsCopying(false);
1540
- });
1549
+ void executeCopyOperation(event.clientX, event.clientY, () => copySingleElementToClipboard(element));
1541
1550
  }
1542
1551
  }, {
1543
1552
  signal: eventListenerSignal
1544
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
+ });
1545
1564
  document.addEventListener("visibilitychange", () => {
1546
1565
  if (document.hidden) {
1547
1566
  setGrabbedBoxes([]);
@@ -1561,7 +1580,7 @@ ${formattedStackTrace}`;
1561
1580
  const selectionVisible = createMemo(() => false);
1562
1581
  const dragVisible = createMemo(() => isRendererActive() && isDraggingBeyondThreshold());
1563
1582
  const labelVariant = createMemo(() => isCopying() ? "processing" : "hover");
1564
- const labelVisible = createMemo(() => isRendererActive() && !isDragging() && (!!targetElement() && !isSameAsLast() || !targetElement()) || isCopying());
1583
+ const labelVisible = createMemo(() => isRendererActive() && !isDragging() && (Boolean(targetElement()) && !isSameAsLast() || !targetElement()) || isCopying());
1565
1584
  const progressVisible = createMemo(() => isHoldingKeys() && showProgressIndicator() && hasValidMousePosition());
1566
1585
  const crosshairVisible = createMemo(() => isRendererActive() && !isDragging());
1567
1586
  render(() => createComponent(ReactGrabRenderer, {