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.cjs CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  var web = require('solid-js/web');
4
4
  var solidJs = require('solid-js');
5
+ var modernScreenshot = require('modern-screenshot');
5
6
  var bippy = require('bippy');
6
7
  var source = require('bippy/dist/source');
8
+ var finder = require('@medv/finder');
7
9
 
8
10
  /**
9
11
  * @license MIT
@@ -96,6 +98,7 @@ var VIEWPORT_MARGIN_PX = 8;
96
98
  var INDICATOR_CLAMP_PADDING_PX = 4;
97
99
  var CURSOR_OFFSET_PX = 14;
98
100
  var SELECTION_LERP_FACTOR = 0.95;
101
+ var SUCCESS_LABEL_DURATION_MS = 1700;
99
102
 
100
103
  // src/utils/lerp.ts
101
104
  var lerp = (start, end, factor) => {
@@ -258,9 +261,9 @@ var getClampedElementPosition = (positionLeft, positionTop, elementWidth, elemen
258
261
  // src/components/label.tsx
259
262
  var _tmpl$3 = /* @__PURE__ */ web.template(`<span style=display:inline-block;margin-right:4px;font-weight:600>\u2713`);
260
263
  var _tmpl$22 = /* @__PURE__ */ web.template(`<div style=margin-right:4px>Copied`);
261
- var _tmpl$32 = /* @__PURE__ */ web.template(`<div style=margin-left:4px>to clipboard`);
262
- var _tmpl$4 = /* @__PURE__ */ web.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">`);
263
- var _tmpl$5 = /* @__PURE__ */ web.template(`<span style="font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;font-variant-numeric:tabular-nums">`);
264
+ var _tmpl$32 = /* @__PURE__ */ web.template(`<span style="font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;font-variant-numeric:tabular-nums">`);
265
+ var _tmpl$4 = /* @__PURE__ */ web.template(`<div style=margin-left:4px>to clipboard`);
266
+ var _tmpl$5 = /* @__PURE__ */ web.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">`);
264
267
  var Label = (props) => {
265
268
  const [opacity, setOpacity] = solidJs.createSignal(0);
266
269
  const [positionTick, setPositionTick] = solidJs.createSignal(0);
@@ -310,7 +313,7 @@ var Label = (props) => {
310
313
  if (props.variant === "success") {
311
314
  const fadeOutTimer = setTimeout(() => {
312
315
  setOpacity(0);
313
- }, 1500);
316
+ }, SUCCESS_LABEL_DURATION_MS);
314
317
  solidJs.onCleanup(() => clearTimeout(fadeOutTimer));
315
318
  }
316
319
  }));
@@ -331,34 +334,20 @@ var Label = (props) => {
331
334
  left: currentX,
332
335
  top: currentY
333
336
  };
334
- if (props.variant === "success") {
335
- const indicatorLeft = Math.round(currentX);
336
- const indicatorTop = Math.round(currentY) - boundingRect.height - 6;
337
- const willClampLeft = indicatorLeft < VIEWPORT_MARGIN_PX;
338
- const willClampTop = indicatorTop < VIEWPORT_MARGIN_PX;
339
- const isClamped = willClampLeft || willClampTop;
340
- const clamped = getClampedElementPosition(indicatorLeft, indicatorTop, boundingRect.width, boundingRect.height);
341
- if (isClamped) {
342
- clamped.left += INDICATOR_CLAMP_PADDING_PX;
343
- clamped.top += INDICATOR_CLAMP_PADDING_PX;
344
- }
345
- return clamped;
346
- }
347
- const CROSSHAIR_OFFSET = 12;
348
337
  const viewportWidth = window.innerWidth;
349
338
  const viewportHeight = window.innerHeight;
350
339
  const quadrants = [{
351
- left: Math.round(currentX) + CROSSHAIR_OFFSET,
352
- top: Math.round(currentY) + CROSSHAIR_OFFSET
340
+ left: Math.round(currentX) + CURSOR_OFFSET_PX,
341
+ top: Math.round(currentY) + CURSOR_OFFSET_PX
353
342
  }, {
354
- left: Math.round(currentX) - boundingRect.width - CROSSHAIR_OFFSET,
355
- top: Math.round(currentY) + CROSSHAIR_OFFSET
343
+ left: Math.round(currentX) - boundingRect.width - CURSOR_OFFSET_PX,
344
+ top: Math.round(currentY) + CURSOR_OFFSET_PX
356
345
  }, {
357
- left: Math.round(currentX) + CROSSHAIR_OFFSET,
358
- top: Math.round(currentY) - boundingRect.height - CROSSHAIR_OFFSET
346
+ left: Math.round(currentX) + CURSOR_OFFSET_PX,
347
+ top: Math.round(currentY) - boundingRect.height - CURSOR_OFFSET_PX
359
348
  }, {
360
- left: Math.round(currentX) - boundingRect.width - CROSSHAIR_OFFSET,
361
- top: Math.round(currentY) - boundingRect.height - CROSSHAIR_OFFSET
349
+ left: Math.round(currentX) - boundingRect.width - CURSOR_OFFSET_PX,
350
+ top: Math.round(currentY) - boundingRect.height - CURSOR_OFFSET_PX
362
351
  }];
363
352
  for (const position of quadrants) {
364
353
  const fitsHorizontally = position.left >= VIEWPORT_MARGIN_PX && position.left + boundingRect.width <= viewportWidth - VIEWPORT_MARGIN_PX;
@@ -377,7 +366,7 @@ var Label = (props) => {
377
366
  return props.visible !== false;
378
367
  },
379
368
  get children() {
380
- var _el$ = _tmpl$4();
369
+ var _el$ = _tmpl$5();
381
370
  var _ref$ = labelRef;
382
371
  typeof _ref$ === "function" ? web.use(_ref$, _el$) : labelRef = _el$;
383
372
  web.insert(_el$, web.createComponent(solidJs.Show, {
@@ -412,17 +401,12 @@ var Label = (props) => {
412
401
  }), null);
413
402
  web.insert(_el$, web.createComponent(solidJs.Show, {
414
403
  get when() {
415
- return props.text.startsWith("(");
416
- },
417
- get fallback() {
418
- return (() => {
419
- var _el$5 = _tmpl$5();
420
- web.insert(_el$5, () => props.text);
421
- return _el$5;
422
- })();
404
+ return props.variant !== "processing";
423
405
  },
424
406
  get children() {
425
- return props.text;
407
+ var _el$4 = _tmpl$32();
408
+ web.insert(_el$4, () => props.text);
409
+ return _el$4;
426
410
  }
427
411
  }), null);
428
412
  web.insert(_el$, web.createComponent(solidJs.Show, {
@@ -430,7 +414,7 @@ var Label = (props) => {
430
414
  return props.variant === "success";
431
415
  },
432
416
  get children() {
433
- return _tmpl$32();
417
+ return _tmpl$4();
434
418
  }
435
419
  }), null);
436
420
  web.effect((_p$) => {
@@ -730,26 +714,43 @@ bippy.instrument({
730
714
  bippy._fiberRoots.add(fiberRoot);
731
715
  }
732
716
  });
733
- var getSourceTrace = async (element) => {
734
- const fiber = bippy.getFiberFromHostInstance(element);
735
- if (!fiber) return null;
736
- const ownerStack = source.getOwnerStack(fiber);
737
- const sources = await source.getSourcesFromStack(
738
- ownerStack,
739
- Number.MAX_SAFE_INTEGER
740
- );
741
- if (!sources) return null;
742
- console.log(sources);
743
- return sources.map((source$1) => {
744
- return {
745
- ...source$1,
746
- fileName: source.normalizeFileName(source$1.fileName)
747
- };
748
- }).filter((source$1) => {
749
- return source.isSourceFile(source$1.fileName);
750
- });
717
+ var generateCSSSelector = (element) => {
718
+ return finder.finder(element);
751
719
  };
752
- var getHTMLSnippet = (element) => {
720
+ var getHTMLSnippet = async (element) => {
721
+ const truncateString = (string, maxLength) => string.length > maxLength ? `${string.substring(0, maxLength)}...` : string;
722
+ const isInternalComponent = (name) => {
723
+ if (name.startsWith("_")) return true;
724
+ if (name.includes("Provider") && name.includes("Context")) return true;
725
+ return false;
726
+ };
727
+ const extractReactComponentName = (el) => {
728
+ const fiber = bippy.getFiberFromHostInstance(el);
729
+ if (!fiber) return null;
730
+ let componentName = null;
731
+ bippy.traverseFiber(
732
+ fiber,
733
+ (currentFiber) => {
734
+ if (bippy.isCompositeFiber(currentFiber)) {
735
+ const displayName = bippy.getDisplayName(currentFiber);
736
+ if (displayName && !isInternalComponent(displayName)) {
737
+ componentName = displayName;
738
+ return true;
739
+ }
740
+ }
741
+ return false;
742
+ },
743
+ true
744
+ );
745
+ return componentName;
746
+ };
747
+ const formatComponentSourceLocation = async (el) => {
748
+ const source$1 = await source.getSourceFromHostInstance(el);
749
+ if (!source$1) return null;
750
+ const fileName = source.normalizeFileName(source$1.fileName);
751
+ if (!source.isSourceFile(fileName)) return null;
752
+ return `${fileName}:${source$1.lineNumber}:${source$1.columnNumber}`;
753
+ };
753
754
  const semanticTags = /* @__PURE__ */ new Set([
754
755
  "article",
755
756
  "aside",
@@ -772,7 +773,7 @@ var getHTMLSnippet = (element) => {
772
773
  (attr) => attr.name.startsWith("data-")
773
774
  );
774
775
  };
775
- const getAncestorChain = (el, maxDepth = 10) => {
776
+ const collectDistinguishingAncestors = (el, maxDepth = 10) => {
776
777
  const ancestors2 = [];
777
778
  let current = el.parentElement;
778
779
  let depth = 0;
@@ -786,35 +787,7 @@ var getHTMLSnippet = (element) => {
786
787
  }
787
788
  return ancestors2.reverse();
788
789
  };
789
- const getCSSPath = (el) => {
790
- const parts = [];
791
- let current = el;
792
- let depth = 0;
793
- const maxDepth = 5;
794
- while (current && depth < maxDepth && current.tagName !== "BODY") {
795
- let selector = current.tagName.toLowerCase();
796
- if (current.id) {
797
- selector += `#${current.id}`;
798
- parts.unshift(selector);
799
- break;
800
- } else if (current.className && typeof current.className === "string" && current.className.trim()) {
801
- const classes = current.className.trim().split(/\s+/).slice(0, 2);
802
- selector += `.${classes.join(".")}`;
803
- }
804
- if (!current.id && (!current.className || !current.className.trim()) && current.parentElement) {
805
- const siblings = Array.from(current.parentElement.children);
806
- const index = siblings.indexOf(current);
807
- if (index >= 0 && siblings.length > 1) {
808
- selector += `:nth-child(${index + 1})`;
809
- }
810
- }
811
- parts.unshift(selector);
812
- current = current.parentElement;
813
- depth++;
814
- }
815
- return parts.join(" > ");
816
- };
817
- const getElementTag = (el, compact = false) => {
790
+ const formatElementOpeningTag = (el, compact = false) => {
818
791
  const tagName = el.tagName.toLowerCase();
819
792
  const attrs = [];
820
793
  if (el.id) {
@@ -824,10 +797,7 @@ var getHTMLSnippet = (element) => {
824
797
  const classes = el.className.trim().split(/\s+/);
825
798
  if (classes.length > 0 && classes[0]) {
826
799
  const displayClasses = compact ? classes.slice(0, 3) : classes;
827
- let classStr = displayClasses.join(" ");
828
- if (classStr.length > 30) {
829
- classStr = classStr.substring(0, 30) + "...";
830
- }
800
+ const classStr = truncateString(displayClasses.join(" "), 30);
831
801
  attrs.push(`class="${classStr}"`);
832
802
  }
833
803
  }
@@ -836,35 +806,20 @@ var getHTMLSnippet = (element) => {
836
806
  );
837
807
  const displayDataAttrs = compact ? dataAttrs.slice(0, 1) : dataAttrs;
838
808
  for (const attr of displayDataAttrs) {
839
- let value = attr.value;
840
- if (value.length > 20) {
841
- value = value.substring(0, 20) + "...";
842
- }
843
- attrs.push(`${attr.name}="${value}"`);
809
+ attrs.push(`${attr.name}="${truncateString(attr.value, 20)}"`);
844
810
  }
845
811
  const ariaLabel = el.getAttribute("aria-label");
846
812
  if (ariaLabel && !compact) {
847
- let value = ariaLabel;
848
- if (value.length > 20) {
849
- value = value.substring(0, 20) + "...";
850
- }
851
- attrs.push(`aria-label="${value}"`);
813
+ attrs.push(`aria-label="${truncateString(ariaLabel, 20)}"`);
852
814
  }
853
815
  return attrs.length > 0 ? `<${tagName} ${attrs.join(" ")}>` : `<${tagName}>`;
854
816
  };
855
- const getClosingTag = (el) => {
856
- return `</${el.tagName.toLowerCase()}>`;
817
+ const formatElementClosingTag = (el) => `</${el.tagName.toLowerCase()}>`;
818
+ const extractTruncatedTextContent = (el) => {
819
+ const text = (el.textContent || "").trim().replace(/\s+/g, " ");
820
+ return truncateString(text, 60);
857
821
  };
858
- const getTextContent = (el) => {
859
- let text = el.textContent || "";
860
- text = text.trim().replace(/\s+/g, " ");
861
- const maxLength = 60;
862
- if (text.length > maxLength) {
863
- text = text.substring(0, maxLength) + "...";
864
- }
865
- return text;
866
- };
867
- const getSiblingIdentifier = (el) => {
822
+ const extractSiblingIdentifier = (el) => {
868
823
  if (el.id) return `#${el.id}`;
869
824
  if (el.className && typeof el.className === "string") {
870
825
  const classes = el.className.trim().split(/\s+/);
@@ -875,12 +830,31 @@ var getHTMLSnippet = (element) => {
875
830
  return null;
876
831
  };
877
832
  const lines = [];
878
- lines.push(`Path: ${getCSSPath(element)}`);
879
- lines.push("");
880
- const ancestors = getAncestorChain(element);
833
+ const selector = generateCSSSelector(element);
834
+ lines.push(`Locate this element in the codebase:`);
835
+ lines.push(`- selector: ${selector}`);
836
+ const rect = element.getBoundingClientRect();
837
+ lines.push(`- width: ${Math.round(rect.width)}`);
838
+ lines.push(`- height: ${Math.round(rect.height)}`);
839
+ lines.push("HTML snippet:");
840
+ lines.push("```html");
841
+ const ancestors = collectDistinguishingAncestors(element);
842
+ const ancestorComponents = ancestors.map(
843
+ (ancestor) => extractReactComponentName(ancestor)
844
+ );
845
+ const elementComponent = extractReactComponentName(element);
846
+ const ancestorSources = await Promise.all(
847
+ ancestors.map((ancestor) => formatComponentSourceLocation(ancestor))
848
+ );
849
+ const elementSource = await formatComponentSourceLocation(element);
881
850
  for (let i = 0; i < ancestors.length; i++) {
882
851
  const indent2 = " ".repeat(i);
883
- lines.push(indent2 + getElementTag(ancestors[i], true));
852
+ const componentName = ancestorComponents[i];
853
+ const source = ancestorSources[i];
854
+ if (componentName && source && (i === 0 || ancestorComponents[i - 1] !== componentName)) {
855
+ lines.push(`${indent2}<${componentName} source="${source}">`);
856
+ }
857
+ lines.push(`${indent2}${formatElementOpeningTag(ancestors[i], true)}`);
884
858
  }
885
859
  const parent = element.parentElement;
886
860
  let targetIndex = -1;
@@ -889,10 +863,10 @@ var getHTMLSnippet = (element) => {
889
863
  targetIndex = siblings.indexOf(element);
890
864
  if (targetIndex > 0) {
891
865
  const prevSibling = siblings[targetIndex - 1];
892
- const prevId = getSiblingIdentifier(prevSibling);
866
+ const prevId = extractSiblingIdentifier(prevSibling);
893
867
  if (prevId && targetIndex <= 2) {
894
868
  const indent2 = " ".repeat(ancestors.length);
895
- lines.push(`${indent2} ${getElementTag(prevSibling, true)}`);
869
+ lines.push(`${indent2} ${formatElementOpeningTag(prevSibling, true)}`);
896
870
  lines.push(`${indent2} </${prevSibling.tagName.toLowerCase()}>`);
897
871
  } else if (targetIndex > 0) {
898
872
  const indent2 = " ".repeat(ancestors.length);
@@ -903,35 +877,44 @@ var getHTMLSnippet = (element) => {
903
877
  }
904
878
  }
905
879
  const indent = " ".repeat(ancestors.length);
906
- lines.push(indent + " <!-- SELECTED -->");
907
- const textContent = getTextContent(element);
880
+ const lastAncestorComponent = ancestors.length > 0 ? ancestorComponents[ancestorComponents.length - 1] : null;
881
+ const showElementComponent = elementComponent && elementSource && elementComponent !== lastAncestorComponent;
882
+ if (showElementComponent) {
883
+ lines.push(`${indent} <${elementComponent} used-at="${elementSource}">`);
884
+ }
885
+ lines.push(`${indent} <!-- IMPORTANT: selected element -->`);
886
+ const textContent = extractTruncatedTextContent(element);
908
887
  const childrenCount = element.children.length;
888
+ const elementIndent = `${indent}${showElementComponent ? " " : " "}`;
909
889
  if (textContent && childrenCount === 0 && textContent.length < 40) {
910
890
  lines.push(
911
- `${indent} ${getElementTag(element)}${textContent}${getClosingTag(
891
+ `${elementIndent}${formatElementOpeningTag(element)}${textContent}${formatElementClosingTag(
912
892
  element
913
893
  )}`
914
894
  );
915
895
  } else {
916
- lines.push(indent + " " + getElementTag(element));
896
+ lines.push(`${elementIndent}${formatElementOpeningTag(element)}`);
917
897
  if (textContent) {
918
- lines.push(`${indent} ${textContent}`);
898
+ lines.push(`${elementIndent} ${textContent}`);
919
899
  }
920
900
  if (childrenCount > 0) {
921
901
  lines.push(
922
- `${indent} ... (${childrenCount} element${childrenCount === 1 ? "" : "s"})`
902
+ `${elementIndent} ... (${childrenCount} element${childrenCount === 1 ? "" : "s"})`
923
903
  );
924
904
  }
925
- lines.push(indent + " " + getClosingTag(element));
905
+ lines.push(`${elementIndent}${formatElementClosingTag(element)}`);
906
+ }
907
+ if (showElementComponent) {
908
+ lines.push(`${indent} </${elementComponent}>`);
926
909
  }
927
910
  if (parent && targetIndex >= 0) {
928
911
  const siblings = Array.from(parent.children);
929
912
  const siblingsAfter = siblings.length - targetIndex - 1;
930
913
  if (siblingsAfter > 0) {
931
914
  const nextSibling = siblings[targetIndex + 1];
932
- const nextId = getSiblingIdentifier(nextSibling);
915
+ const nextId = extractSiblingIdentifier(nextSibling);
933
916
  if (nextId && siblingsAfter <= 2) {
934
- lines.push(`${indent} ${getElementTag(nextSibling, true)}`);
917
+ lines.push(`${indent} ${formatElementOpeningTag(nextSibling, true)}`);
935
918
  lines.push(`${indent} </${nextSibling.tagName.toLowerCase()}>`);
936
919
  } else {
937
920
  lines.push(
@@ -942,8 +925,14 @@ var getHTMLSnippet = (element) => {
942
925
  }
943
926
  for (let i = ancestors.length - 1; i >= 0; i--) {
944
927
  const indent2 = " ".repeat(i);
945
- lines.push(indent2 + getClosingTag(ancestors[i]));
928
+ lines.push(`${indent2}${formatElementClosingTag(ancestors[i])}`);
929
+ const componentName = ancestorComponents[i];
930
+ const source = ancestorSources[i];
931
+ if (componentName && source && (i === ancestors.length - 1 || ancestorComponents[i + 1] !== componentName)) {
932
+ lines.push(`${indent2}</${componentName}>`);
933
+ }
946
934
  }
935
+ lines.push("```");
947
936
  return lines.join("\n");
948
937
  };
949
938
 
@@ -1173,8 +1162,8 @@ var createElementBounds = (element) => {
1173
1162
  };
1174
1163
 
1175
1164
  // src/core.tsx
1176
- var SUCCESS_LABEL_DURATION_MS = 1700;
1177
1165
  var PROGRESS_INDICATOR_DELAY_MS = 150;
1166
+ var QUICK_REPRESS_THRESHOLD_MS = 150;
1178
1167
  var init = (rawOptions) => {
1179
1168
  const options = {
1180
1169
  enabled: true,
@@ -1200,14 +1189,19 @@ var init = (rawOptions) => {
1200
1189
  const [successLabels, setSuccessLabels] = solidJs.createSignal([]);
1201
1190
  const [isActivated, setIsActivated] = solidJs.createSignal(false);
1202
1191
  const [showProgressIndicator, setShowProgressIndicator] = solidJs.createSignal(false);
1192
+ const [didJustDrag, setDidJustDrag] = solidJs.createSignal(false);
1193
+ const [isModifierHeld, setIsModifierHeld] = solidJs.createSignal(false);
1194
+ const [copyStartX, setCopyStartX] = solidJs.createSignal(OFFSCREEN_POSITION);
1195
+ const [copyStartY, setCopyStartY] = solidJs.createSignal(OFFSCREEN_POSITION);
1203
1196
  let holdTimerId = null;
1204
1197
  let progressAnimationId = null;
1205
1198
  let progressDelayTimerId = null;
1206
1199
  let keydownSpamTimerId = null;
1200
+ let lastDeactivationTime = null;
1207
1201
  const isRendererActive = solidJs.createMemo(() => isActivated() && !isCopying());
1208
1202
  const hasValidMousePosition = solidJs.createMemo(() => mouseX() > OFFSCREEN_POSITION && mouseY() > OFFSCREEN_POSITION);
1209
1203
  const isTargetKeyCombination = (event) => (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "c";
1210
- const addGrabbedBox = (bounds) => {
1204
+ const showTemporaryGrabbedBox = (bounds) => {
1211
1205
  const boxId = `grabbed-${Date.now()}-${Math.random()}`;
1212
1206
  const createdAt = Date.now();
1213
1207
  const newBox = {
@@ -1221,7 +1215,7 @@ var init = (rawOptions) => {
1221
1215
  setGrabbedBoxes((previousBoxes) => previousBoxes.filter((box) => box.id !== boxId));
1222
1216
  }, SUCCESS_LABEL_DURATION_MS);
1223
1217
  };
1224
- const addSuccessLabel = (text, positionX, positionY) => {
1218
+ const showTemporarySuccessLabel = (text, positionX, positionY) => {
1225
1219
  const labelId = `success-${Date.now()}-${Math.random()}`;
1226
1220
  setSuccessLabels((previousLabels) => [...previousLabels, {
1227
1221
  id: labelId,
@@ -1233,24 +1227,15 @@ var init = (rawOptions) => {
1233
1227
  setSuccessLabels((previousLabels) => previousLabels.filter((label) => label.id !== labelId));
1234
1228
  }, SUCCESS_LABEL_DURATION_MS);
1235
1229
  };
1236
- const formatStackTrace = (stackTrace) => {
1237
- return stackTrace.map((source) => {
1238
- const functionName = source.functionName ?? "anonymous";
1239
- const fileName = source.fileName ?? "unknown";
1240
- const lineNumber = source.lineNumber ?? 0;
1241
- const columnNumber = source.columnNumber ?? 0;
1242
- return ` at ${functionName} (${fileName}:${lineNumber}:${columnNumber})`;
1243
- }).join("\n");
1244
- };
1245
- const wrapContextInXmlTags = (context) => {
1246
- return `<selected_element>${context}</selected_element>`;
1247
- };
1248
- const getComputedStyles = (element) => {
1230
+ const wrapInSelectedElementTags = (context) => `<selected_element>
1231
+ ${context}
1232
+ </selected_element>`;
1233
+ const extractRelevantComputedStyles = (element) => {
1249
1234
  const computed = window.getComputedStyle(element);
1250
1235
  const rect = element.getBoundingClientRect();
1251
1236
  return {
1252
- width: `${rect.width}px`,
1253
- height: `${rect.height}px`,
1237
+ width: `${Math.round(rect.width)}px`,
1238
+ height: `${Math.round(rect.height)}px`,
1254
1239
  paddingTop: computed.paddingTop,
1255
1240
  paddingRight: computed.paddingRight,
1256
1241
  paddingBottom: computed.paddingBottom,
@@ -1259,7 +1244,7 @@ var init = (rawOptions) => {
1259
1244
  opacity: computed.opacity
1260
1245
  };
1261
1246
  };
1262
- const createStructuredClipboardHtml = (elements) => {
1247
+ const createStructuredClipboardHtmlBlob = (elements) => {
1263
1248
  const structuredData = {
1264
1249
  elements: elements.map((element) => ({
1265
1250
  tagName: element.tagName,
@@ -1273,60 +1258,74 @@ var init = (rawOptions) => {
1273
1258
  type: "text/html"
1274
1259
  });
1275
1260
  };
1276
- const getElementContentWithTrace = async (element) => {
1277
- const elementHtml = getHTMLSnippet(element);
1278
- const componentStackTrace = await getSourceTrace(element);
1279
- if (componentStackTrace?.length) {
1280
- const formattedStackTrace = formatStackTrace(componentStackTrace);
1281
- return `${elementHtml}
1282
-
1283
- Component owner stack:
1284
- ${formattedStackTrace}`;
1285
- }
1286
- return elementHtml;
1261
+ const extractElementTagName = (element) => (element.tagName || "").toLowerCase();
1262
+ const executeCopyOperation = async (positionX, positionY, operation) => {
1263
+ setCopyStartX(positionX);
1264
+ setCopyStartY(positionY);
1265
+ setIsCopying(true);
1266
+ await operation().finally(() => {
1267
+ setIsCopying(false);
1268
+ });
1287
1269
  };
1288
- const getElementTagName = (element) => (element.tagName || "").toLowerCase();
1289
- const handleCopy = async (targetElement2) => {
1290
- const elementBounds = targetElement2.getBoundingClientRect();
1291
- const tagName = getElementTagName(targetElement2);
1292
- addGrabbedBox(createElementBounds(targetElement2));
1270
+ const copySingleElementToClipboard = async (targetElement2) => {
1271
+ const tagName = extractElementTagName(targetElement2);
1272
+ showTemporaryGrabbedBox(createElementBounds(targetElement2));
1293
1273
  try {
1294
- const content = await getElementContentWithTrace(targetElement2);
1295
- const plainTextContent = wrapContextInXmlTags(content);
1296
- const htmlContent = createStructuredClipboardHtml([{
1274
+ const content = await getHTMLSnippet(targetElement2);
1275
+ const plainTextContent = wrapInSelectedElementTags(content);
1276
+ const htmlContent = createStructuredClipboardHtmlBlob([{
1297
1277
  tagName,
1298
- content: await getElementContentWithTrace(targetElement2),
1299
- computedStyles: getComputedStyles(targetElement2)
1278
+ content,
1279
+ computedStyles: extractRelevantComputedStyles(targetElement2)
1300
1280
  }]);
1301
- await copyContent([plainTextContent, htmlContent]);
1281
+ const clipboardData = [plainTextContent, htmlContent];
1282
+ try {
1283
+ const screenshotDataUrl = await modernScreenshot.domToPng(targetElement2);
1284
+ const response = await fetch(screenshotDataUrl);
1285
+ const pngBlob = await response.blob();
1286
+ const imagePngBlob = new Blob([pngBlob], {
1287
+ type: "image/png"
1288
+ });
1289
+ clipboardData.push(imagePngBlob);
1290
+ } catch {
1291
+ }
1292
+ await copyContent(clipboardData);
1302
1293
  } catch {
1303
1294
  }
1304
- addSuccessLabel(tagName ? `<${tagName}>` : "<element>", elementBounds.left, elementBounds.top);
1295
+ showTemporarySuccessLabel(tagName ? `<${tagName}>` : "<element>", copyStartX(), copyStartY());
1305
1296
  };
1306
- const handleMultipleCopy = async (targetElements) => {
1297
+ const copyMultipleElementsToClipboard = async (targetElements) => {
1307
1298
  if (targetElements.length === 0) return;
1308
- let minPositionX = Infinity;
1309
- let minPositionY = Infinity;
1310
1299
  for (const element of targetElements) {
1311
- const elementBounds = element.getBoundingClientRect();
1312
- minPositionX = Math.min(minPositionX, elementBounds.left);
1313
- minPositionY = Math.min(minPositionY, elementBounds.top);
1314
- addGrabbedBox(createElementBounds(element));
1300
+ showTemporaryGrabbedBox(createElementBounds(element));
1315
1301
  }
1316
1302
  try {
1317
- const elementSnippets = await Promise.all(targetElements.map((element) => getElementContentWithTrace(element)));
1303
+ const elementSnippets = await Promise.all(targetElements.map((element) => getHTMLSnippet(element)));
1318
1304
  const combinedContent = elementSnippets.join("\n\n---\n\n");
1319
- const plainTextContent = wrapContextInXmlTags(combinedContent);
1320
- const structuredElements = await Promise.all(targetElements.map(async (element) => ({
1321
- tagName: getElementTagName(element),
1322
- content: await getElementContentWithTrace(element),
1323
- computedStyles: getComputedStyles(element)
1324
- })));
1325
- const htmlContent = createStructuredClipboardHtml(structuredElements);
1326
- await copyContent([plainTextContent, htmlContent]);
1305
+ const plainTextContent = wrapInSelectedElementTags(combinedContent);
1306
+ const structuredElements = elementSnippets.map((content, index) => ({
1307
+ tagName: extractElementTagName(targetElements[index]),
1308
+ content,
1309
+ computedStyles: extractRelevantComputedStyles(targetElements[index])
1310
+ }));
1311
+ const htmlContent = createStructuredClipboardHtmlBlob(structuredElements);
1312
+ const clipboardData = [plainTextContent, htmlContent];
1313
+ if (targetElements.length > 0) {
1314
+ try {
1315
+ const screenshotDataUrl = await modernScreenshot.domToPng(targetElements[0]);
1316
+ const response = await fetch(screenshotDataUrl);
1317
+ const pngBlob = await response.blob();
1318
+ const imagePngBlob = new Blob([pngBlob], {
1319
+ type: "image/png"
1320
+ });
1321
+ clipboardData.push(imagePngBlob);
1322
+ } catch {
1323
+ }
1324
+ }
1325
+ await copyContent(clipboardData);
1327
1326
  } catch {
1328
1327
  }
1329
- addSuccessLabel(`${targetElements.length} elements`, minPositionX, minPositionY);
1328
+ showTemporarySuccessLabel(`${targetElements.length} elements`, copyStartX(), copyStartY());
1330
1329
  };
1331
1330
  const targetElement = solidJs.createMemo(() => {
1332
1331
  if (!isRendererActive() || isDragging()) return null;
@@ -1347,16 +1346,16 @@ ${formattedStackTrace}`;
1347
1346
  };
1348
1347
  });
1349
1348
  const DRAG_THRESHOLD_PX = 2;
1350
- const getDragDistance = (endX, endY) => ({
1349
+ const calculateDragDistance = (endX, endY) => ({
1351
1350
  x: Math.abs(endX - dragStartX()),
1352
1351
  y: Math.abs(endY - dragStartY())
1353
1352
  });
1354
1353
  const isDraggingBeyondThreshold = solidJs.createMemo(() => {
1355
1354
  if (!isDragging()) return false;
1356
- const dragDistance = getDragDistance(mouseX(), mouseY());
1355
+ const dragDistance = calculateDragDistance(mouseX(), mouseY());
1357
1356
  return dragDistance.x > DRAG_THRESHOLD_PX || dragDistance.y > DRAG_THRESHOLD_PX;
1358
1357
  });
1359
- const getDragRect = (endX, endY) => {
1358
+ const calculateDragRectangle = (endX, endY) => {
1360
1359
  const dragX = Math.min(dragStartX(), endX);
1361
1360
  const dragY = Math.min(dragStartY(), endY);
1362
1361
  const dragWidth = Math.abs(endX - dragStartX());
@@ -1370,7 +1369,7 @@ ${formattedStackTrace}`;
1370
1369
  };
1371
1370
  const dragBounds = solidJs.createMemo(() => {
1372
1371
  if (!isDraggingBeyondThreshold()) return void 0;
1373
- const drag = getDragRect(mouseX(), mouseY());
1372
+ const drag = calculateDragRectangle(mouseX(), mouseY());
1374
1373
  return {
1375
1374
  borderRadius: "0px",
1376
1375
  height: drag.height,
@@ -1382,21 +1381,16 @@ ${formattedStackTrace}`;
1382
1381
  });
1383
1382
  const labelText = solidJs.createMemo(() => {
1384
1383
  const element = targetElement();
1385
- if (!element) return "(click or drag to select element(s))";
1386
- const tagName = getElementTagName(element);
1387
- return tagName ? `<${tagName}>` : "<element>";
1388
- });
1389
- const labelPosition = solidJs.createMemo(() => {
1390
- return {
1391
- x: mouseX(),
1392
- y: mouseY()
1393
- };
1384
+ return element ? `<${extractElementTagName(element)}>` : "<element>";
1394
1385
  });
1395
- const isSameAsLast = solidJs.createMemo(() => {
1396
- const currentElement = targetElement();
1397
- const lastElement = lastGrabbedElement();
1398
- return !!currentElement && currentElement === lastElement;
1386
+ const labelPosition = solidJs.createMemo(() => isCopying() ? {
1387
+ x: copyStartX(),
1388
+ y: copyStartY()
1389
+ } : {
1390
+ x: mouseX(),
1391
+ y: mouseY()
1399
1392
  });
1393
+ const isSameAsLast = solidJs.createMemo(() => Boolean(targetElement() && targetElement() === lastGrabbedElement()));
1400
1394
  solidJs.createEffect(solidJs.on(() => [targetElement(), lastGrabbedElement()], ([currentElement, lastElement]) => {
1401
1395
  if (lastElement && currentElement && lastElement !== currentElement) {
1402
1396
  setLastGrabbedElement(null);
@@ -1443,9 +1437,12 @@ ${formattedStackTrace}`;
1443
1437
  setIsActivated(true);
1444
1438
  document.body.style.cursor = "crosshair";
1445
1439
  };
1446
- const deactivateRenderer = () => {
1440
+ const deactivateRenderer = (shouldResetModifier = true) => {
1447
1441
  setIsHoldingKeys(false);
1448
1442
  setIsActivated(false);
1443
+ if (shouldResetModifier) {
1444
+ setIsModifierHeld(false);
1445
+ }
1449
1446
  document.body.style.cursor = "";
1450
1447
  if (isDragging()) {
1451
1448
  setIsDragging(false);
@@ -1454,23 +1451,38 @@ ${formattedStackTrace}`;
1454
1451
  if (holdTimerId) window.clearTimeout(holdTimerId);
1455
1452
  if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);
1456
1453
  stopProgressAnimation();
1454
+ lastDeactivationTime = Date.now();
1457
1455
  };
1458
1456
  const abortController = new AbortController();
1459
1457
  const eventListenerSignal = abortController.signal;
1460
1458
  window.addEventListener("keydown", (event) => {
1459
+ if (event.metaKey || event.ctrlKey) {
1460
+ setIsModifierHeld(true);
1461
+ }
1461
1462
  if (event.key === "Escape" && isHoldingKeys()) {
1462
1463
  deactivateRenderer();
1463
1464
  return;
1464
1465
  }
1465
1466
  if (isKeyboardEventTriggeredByInput(event)) return;
1466
1467
  if (isTargetKeyCombination(event)) {
1468
+ const wasRecentlyDeactivated = lastDeactivationTime !== null && Date.now() - lastDeactivationTime < QUICK_REPRESS_THRESHOLD_MS;
1467
1469
  if (!isHoldingKeys()) {
1468
1470
  setIsHoldingKeys(true);
1469
- startProgressAnimation();
1470
- holdTimerId = window.setTimeout(() => {
1471
+ if (wasRecentlyDeactivated && isModifierHeld()) {
1471
1472
  activateRenderer();
1472
1473
  options.onActivate?.();
1473
- }, options.keyHoldDuration);
1474
+ const element = getElementAtPosition(mouseX(), mouseY());
1475
+ if (element) {
1476
+ setLastGrabbedElement(element);
1477
+ void executeCopyOperation(mouseX(), mouseY(), () => copySingleElementToClipboard(element));
1478
+ }
1479
+ } else {
1480
+ startProgressAnimation();
1481
+ holdTimerId = window.setTimeout(() => {
1482
+ activateRenderer();
1483
+ options.onActivate?.();
1484
+ }, options.keyHoldDuration);
1485
+ }
1474
1486
  }
1475
1487
  if (isActivated()) {
1476
1488
  if (keydownSpamTimerId) window.clearTimeout(keydownSpamTimerId);
@@ -1483,11 +1495,16 @@ ${formattedStackTrace}`;
1483
1495
  signal: eventListenerSignal
1484
1496
  });
1485
1497
  window.addEventListener("keyup", (event) => {
1486
- if (!isHoldingKeys() && !isActivated()) return;
1487
- const isReleasingC = event.key.toLowerCase() === "c";
1488
1498
  const isReleasingModifier = !event.metaKey && !event.ctrlKey;
1489
- if (isReleasingC || isReleasingModifier) {
1490
- deactivateRenderer();
1499
+ const isReleasingC = event.key.toLowerCase() === "c";
1500
+ if (isReleasingModifier) {
1501
+ setIsModifierHeld(false);
1502
+ }
1503
+ if (!isHoldingKeys() && !isActivated()) return;
1504
+ if (isReleasingC) {
1505
+ deactivateRenderer(false);
1506
+ } else if (isReleasingModifier) {
1507
+ deactivateRenderer(true);
1491
1508
  }
1492
1509
  }, {
1493
1510
  signal: eventListenerSignal,
@@ -1511,39 +1528,41 @@ ${formattedStackTrace}`;
1511
1528
  });
1512
1529
  window.addEventListener("mouseup", (event) => {
1513
1530
  if (!isDragging()) return;
1514
- const dragDistance = getDragDistance(event.clientX, event.clientY);
1531
+ const dragDistance = calculateDragDistance(event.clientX, event.clientY);
1515
1532
  const wasDragGesture = dragDistance.x > DRAG_THRESHOLD_PX || dragDistance.y > DRAG_THRESHOLD_PX;
1516
1533
  setIsDragging(false);
1517
1534
  document.body.style.userSelect = "";
1518
1535
  if (wasDragGesture) {
1519
- const dragRect = getDragRect(event.clientX, event.clientY);
1536
+ setDidJustDrag(true);
1537
+ const dragRect = calculateDragRectangle(event.clientX, event.clientY);
1520
1538
  const elements = getElementsInDrag(dragRect, isValidGrabbableElement);
1521
1539
  if (elements.length > 0) {
1522
- setIsCopying(true);
1523
- void handleMultipleCopy(elements).finally(() => {
1524
- setIsCopying(false);
1525
- });
1540
+ void executeCopyOperation(event.clientX, event.clientY, () => copyMultipleElementsToClipboard(elements));
1526
1541
  } else {
1527
1542
  const fallbackElements = getElementsInDragLoose(dragRect, isValidGrabbableElement);
1528
1543
  if (fallbackElements.length > 0) {
1529
- setIsCopying(true);
1530
- void handleMultipleCopy(fallbackElements).finally(() => {
1531
- setIsCopying(false);
1532
- });
1544
+ void executeCopyOperation(event.clientX, event.clientY, () => copyMultipleElementsToClipboard(fallbackElements));
1533
1545
  }
1534
1546
  }
1535
1547
  } else {
1536
1548
  const element = getElementAtPosition(event.clientX, event.clientY);
1537
1549
  if (!element) return;
1538
- setIsCopying(true);
1539
1550
  setLastGrabbedElement(element);
1540
- void handleCopy(element).finally(() => {
1541
- setIsCopying(false);
1542
- });
1551
+ void executeCopyOperation(event.clientX, event.clientY, () => copySingleElementToClipboard(element));
1543
1552
  }
1544
1553
  }, {
1545
1554
  signal: eventListenerSignal
1546
1555
  });
1556
+ window.addEventListener("click", (event) => {
1557
+ if (didJustDrag()) {
1558
+ event.preventDefault();
1559
+ event.stopPropagation();
1560
+ setDidJustDrag(false);
1561
+ }
1562
+ }, {
1563
+ signal: eventListenerSignal,
1564
+ capture: true
1565
+ });
1547
1566
  document.addEventListener("visibilitychange", () => {
1548
1567
  if (document.hidden) {
1549
1568
  setGrabbedBoxes([]);
@@ -1563,7 +1582,7 @@ ${formattedStackTrace}`;
1563
1582
  const selectionVisible = solidJs.createMemo(() => false);
1564
1583
  const dragVisible = solidJs.createMemo(() => isRendererActive() && isDraggingBeyondThreshold());
1565
1584
  const labelVariant = solidJs.createMemo(() => isCopying() ? "processing" : "hover");
1566
- const labelVisible = solidJs.createMemo(() => isRendererActive() && !isDragging() && (!!targetElement() && !isSameAsLast() || !targetElement()) || isCopying());
1585
+ const labelVisible = solidJs.createMemo(() => isRendererActive() && !isDragging() && (Boolean(targetElement()) && !isSameAsLast() || !targetElement()) || isCopying());
1567
1586
  const progressVisible = solidJs.createMemo(() => isHoldingKeys() && showProgressIndicator() && hasValidMousePosition());
1568
1587
  const crosshairVisible = solidJs.createMemo(() => isRendererActive() && !isDragging());
1569
1588
  web.render(() => web.createComponent(ReactGrabRenderer, {