react-grab 0.0.39 → 0.0.40

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
@@ -3,6 +3,7 @@ import { createRoot, createSignal, createMemo, createEffect, on, onCleanup, Show
3
3
  import { instrument, _fiberRoots, getFiberFromHostInstance, traverseFiber, isCompositeFiber, getDisplayName } from 'bippy';
4
4
  import { getSourceFromHostInstance, normalizeFileName, isSourceFile } from 'bippy/dist/source';
5
5
  import { finder } from '@medv/finder';
6
+ import TurndownService from 'turndown';
6
7
 
7
8
  /**
8
9
  * @license MIT
@@ -176,7 +177,7 @@ var SelectionBox = (props) => {
176
177
  position: "fixed",
177
178
  "box-sizing": "border-box",
178
179
  "pointer-events": props.variant === "drag" ? "none" : "auto",
179
- "z-index": "2147483646"
180
+ "z-index": props.variant === "grabbed" ? "2147483645" : "2147483646"
180
181
  };
181
182
  const variantStyle = () => {
182
183
  if (props.variant === "drag") {
@@ -188,10 +189,16 @@ var SelectionBox = (props) => {
188
189
  cursor: "crosshair"
189
190
  };
190
191
  }
192
+ if (props.variant === "selection") {
193
+ return {
194
+ border: "1px dashed rgba(210, 57, 192, 0.5)",
195
+ "background-color": "rgba(210, 57, 192, 0.08)"
196
+ };
197
+ }
191
198
  return {
192
199
  border: "1px solid rgb(210, 57, 192)",
193
- "background-color": "rgba(210, 57, 192, 0.2)",
194
- transition: props.variant === "grabbed" ? "opacity 0.3s ease-out" : void 0
200
+ "background-color": "rgba(210, 57, 192, 0.08)",
201
+ transition: "opacity 0.3s ease-out"
195
202
  };
196
203
  };
197
204
  return createComponent(Show, {
@@ -746,7 +753,7 @@ var generateCSSSelector = (element) => {
746
753
  return finder(element);
747
754
  };
748
755
  var truncateString = (string, maxLength) => string.length > maxLength ? `${string.substring(0, maxLength)}...` : string;
749
- var isInternalComponent = (name) => !isCapitalized(name) || name.startsWith("_") || name.includes("Provider") && name.includes("Context");
756
+ var isInternalComponent = (name) => !isCapitalized(name) || name.startsWith("_") || name.startsWith("Primitive.") || name.includes("Provider") && name.includes("Context");
750
757
  var getNearestComponentDisplayName = (element) => {
751
758
  const fiber = getFiberFromHostInstance(element);
752
759
  if (!fiber) return null;
@@ -878,14 +885,26 @@ var getHTMLSnippet = async (element) => {
878
885
  ancestors.map((ancestor) => formatComponentSourceLocation(ancestor))
879
886
  );
880
887
  const elementSource = await formatComponentSourceLocation(element);
888
+ const ancestorElementIndents = [];
889
+ const ancestorComponentIndents = [];
890
+ let currentIndentLevel = 0;
891
+ const getIndent = (level) => " ".repeat(level);
881
892
  for (let i = 0; i < ancestors.length; i++) {
882
- const indent2 = " ".repeat(i);
883
893
  const componentName = ancestorComponents[i];
884
894
  const source = ancestorSources[i];
885
- if (componentName && source && (i === 0 || ancestorComponents[i - 1] !== componentName)) {
886
- lines.push(`${indent2}<${componentName} source="${source}">`);
895
+ const isNewComponent = componentName && source && (i === 0 || ancestorComponents[i - 1] !== componentName);
896
+ if (isNewComponent) {
897
+ ancestorComponentIndents[i] = currentIndentLevel;
898
+ lines.push(
899
+ `${getIndent(currentIndentLevel)}<${componentName} used-at="${source}">`
900
+ );
901
+ currentIndentLevel++;
887
902
  }
888
- lines.push(`${indent2}${formatElementOpeningTag(ancestors[i], true)}`);
903
+ ancestorElementIndents[i] = currentIndentLevel;
904
+ lines.push(
905
+ `${getIndent(currentIndentLevel)}${formatElementOpeningTag(ancestors[i], true)}`
906
+ );
907
+ currentIndentLevel++;
889
908
  }
890
909
  const parent = element.parentElement;
891
910
  let targetIndex = -1;
@@ -893,33 +912,37 @@ var getHTMLSnippet = async (element) => {
893
912
  const siblings = Array.from(parent.children);
894
913
  targetIndex = siblings.indexOf(element);
895
914
  if (targetIndex > 0) {
896
- const indent2 = " ".repeat(ancestors.length);
915
+ const indent = getIndent(currentIndentLevel);
897
916
  if (targetIndex <= 2) {
898
917
  for (let i = 0; i < targetIndex; i++) {
899
918
  const sibling = siblings[i];
900
919
  const siblingId = extractSiblingIdentifier(sibling);
901
920
  if (siblingId) {
902
- lines.push(`${indent2} ${formatElementOpeningTag(sibling, true)}`);
903
- lines.push(`${indent2} </${sibling.tagName.toLowerCase()}>`);
921
+ lines.push(`${indent}${formatElementOpeningTag(sibling, true)}`);
922
+ lines.push(`${indent}</${sibling.tagName.toLowerCase()}>`);
904
923
  }
905
924
  }
906
925
  } else {
907
926
  lines.push(
908
- `${indent2} ... (${targetIndex} element${targetIndex === 1 ? "" : "s"})`
927
+ `${indent}... (${targetIndex} element${targetIndex === 1 ? "" : "s"})`
909
928
  );
910
929
  }
911
930
  }
912
931
  }
913
- const indent = " ".repeat(ancestors.length);
914
932
  const lastAncestorComponent = ancestors.length > 0 ? ancestorComponents[ancestorComponents.length - 1] : null;
915
933
  const showElementComponent = elementComponent && elementSource && elementComponent !== lastAncestorComponent;
934
+ let elementIndentLevel = currentIndentLevel;
935
+ const elementComponentIndentLevel = currentIndentLevel;
916
936
  if (showElementComponent) {
917
- lines.push(`${indent} <${elementComponent} used-at="${elementSource}">`);
937
+ lines.push(
938
+ `${getIndent(elementIndentLevel)}<${elementComponent} used-at="${elementSource}">`
939
+ );
940
+ elementIndentLevel++;
918
941
  }
919
- lines.push(`${indent} <!-- IMPORTANT: selected element -->`);
942
+ const elementIndent = getIndent(elementIndentLevel);
943
+ lines.push(`${elementIndent}<!-- IMPORTANT: selected element -->`);
920
944
  const textContent = extractTruncatedTextContent(element);
921
945
  const childrenCount = element.children.length;
922
- const elementIndent = `${indent}${showElementComponent ? " " : " "}`;
923
946
  if (textContent && childrenCount === 0 && textContent.length < 40) {
924
947
  lines.push(
925
948
  `${elementIndent}${formatElementOpeningTag(element)}${textContent}${formatElementClosingTag(
@@ -939,35 +962,37 @@ var getHTMLSnippet = async (element) => {
939
962
  lines.push(`${elementIndent}${formatElementClosingTag(element)}`);
940
963
  }
941
964
  if (showElementComponent) {
942
- lines.push(`${indent} </${elementComponent}>`);
965
+ lines.push(
966
+ `${getIndent(elementComponentIndentLevel)}</${elementComponent}>`
967
+ );
943
968
  }
944
969
  if (parent && targetIndex >= 0) {
945
970
  const siblings = Array.from(parent.children);
946
971
  const siblingsAfter = siblings.length - targetIndex - 1;
947
972
  if (siblingsAfter > 0) {
973
+ const indent = getIndent(currentIndentLevel);
948
974
  if (siblingsAfter <= 2) {
949
975
  for (let i = targetIndex + 1; i < siblings.length; i++) {
950
976
  const sibling = siblings[i];
951
977
  const siblingId = extractSiblingIdentifier(sibling);
952
978
  if (siblingId) {
953
- lines.push(`${indent} ${formatElementOpeningTag(sibling, true)}`);
954
- lines.push(`${indent} </${sibling.tagName.toLowerCase()}>`);
979
+ lines.push(`${indent}${formatElementOpeningTag(sibling, true)}`);
980
+ lines.push(`${indent}</${sibling.tagName.toLowerCase()}>`);
955
981
  }
956
982
  }
957
983
  } else {
958
984
  lines.push(
959
- `${indent} ... (${siblingsAfter} element${siblingsAfter === 1 ? "" : "s"})`
985
+ `${indent}... (${siblingsAfter} element${siblingsAfter === 1 ? "" : "s"})`
960
986
  );
961
987
  }
962
988
  }
963
989
  }
964
990
  for (let i = ancestors.length - 1; i >= 0; i--) {
965
- const indent2 = " ".repeat(i);
966
- lines.push(`${indent2}${formatElementClosingTag(ancestors[i])}`);
967
- const componentName = ancestorComponents[i];
968
- const source = ancestorSources[i];
969
- if (componentName && source && (i === ancestors.length - 1 || ancestorComponents[i + 1] !== componentName)) {
970
- lines.push(`${indent2}</${componentName}>`);
991
+ const elementIndent2 = getIndent(ancestorElementIndents[i]);
992
+ lines.push(`${elementIndent2}${formatElementClosingTag(ancestors[i])}`);
993
+ if (ancestorComponentIndents[i] !== void 0) {
994
+ const compIndent = getIndent(ancestorComponentIndents[i]);
995
+ lines.push(`${compIndent}</${ancestorComponents[i]}>`);
971
996
  }
972
997
  }
973
998
  lines.push("```");
@@ -1225,6 +1250,62 @@ var isLocalhost = () => {
1225
1250
  const hostname = window.location.hostname;
1226
1251
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
1227
1252
  };
1253
+ var turndownService = null;
1254
+ var getTurndownService = () => {
1255
+ if (!turndownService) {
1256
+ turndownService = new TurndownService({
1257
+ headingStyle: "atx",
1258
+ codeBlockStyle: "fenced",
1259
+ emDelimiter: "_",
1260
+ bulletListMarker: "-",
1261
+ linkStyle: "inlined",
1262
+ linkReferenceStyle: "full"
1263
+ });
1264
+ turndownService.addRule("strikethrough", {
1265
+ filter: ["del", "s"],
1266
+ replacement: (content) => `~~${content}~~`
1267
+ });
1268
+ turndownService.addRule("removeHidden", {
1269
+ filter: (node) => {
1270
+ if (node instanceof HTMLElement) {
1271
+ const style = window.getComputedStyle(node);
1272
+ return style.display === "none" || style.visibility === "hidden" || style.opacity === "0";
1273
+ }
1274
+ return false;
1275
+ },
1276
+ replacement: () => ""
1277
+ });
1278
+ turndownService.addRule("preserveButtons", {
1279
+ filter: ["button"],
1280
+ replacement: (content) => content ? `[${content}]` : ""
1281
+ });
1282
+ turndownService.addRule("preserveInputs", {
1283
+ filter: (node) => {
1284
+ if (node.nodeName === "INPUT" && node instanceof HTMLInputElement) {
1285
+ return true;
1286
+ }
1287
+ return false;
1288
+ },
1289
+ replacement: (_, node) => {
1290
+ if (node instanceof HTMLInputElement) {
1291
+ const value = node.value || node.placeholder;
1292
+ return value ? `[${value}]` : "";
1293
+ }
1294
+ return "";
1295
+ }
1296
+ });
1297
+ }
1298
+ return turndownService;
1299
+ };
1300
+ var htmlToMarkdown = (html) => {
1301
+ const service = getTurndownService();
1302
+ return service.turndown(html).trim();
1303
+ };
1304
+ var elementToMarkdown = (element) => {
1305
+ const clonedElement = element.cloneNode(true);
1306
+ const service = getTurndownService();
1307
+ return service.turndown(clonedElement).trim();
1308
+ };
1228
1309
 
1229
1310
  // src/core.tsx
1230
1311
  var PROGRESS_INDICATOR_DELAY_MS = 150;
@@ -1264,6 +1345,7 @@ var init = (rawOptions) => {
1264
1345
  const [grabbedBoxes, setGrabbedBoxes] = createSignal([]);
1265
1346
  const [successLabels, setSuccessLabels] = createSignal([]);
1266
1347
  const [isActivated, setIsActivated] = createSignal(false);
1348
+ const [isToggleMode, setIsToggleMode] = createSignal(false);
1267
1349
  const [showProgressIndicator, setShowProgressIndicator] = createSignal(false);
1268
1350
  const [didJustDrag, setDidJustDrag] = createSignal(false);
1269
1351
  const [copyStartX, setCopyStartX] = createSignal(OFFSCREEN_POSITION);
@@ -1343,7 +1425,7 @@ ${context}
1343
1425
  traverseFiber(fiber, (currentFiber) => {
1344
1426
  if (isCompositeFiber(currentFiber)) {
1345
1427
  const displayName = getDisplayName(currentFiber);
1346
- if (displayName && isCapitalized(displayName) && !displayName.startsWith("_")) {
1428
+ if (displayName && isCapitalized(displayName) && !displayName.startsWith("_") && !displayName.startsWith("Primitive.")) {
1347
1429
  componentName = displayName;
1348
1430
  return true;
1349
1431
  }
@@ -1386,6 +1468,13 @@ ${context}
1386
1468
  await operation().finally(() => {
1387
1469
  setIsCopying(false);
1388
1470
  stopProgressAnimation();
1471
+ if (isToggleMode()) {
1472
+ if (!isHoldingKeys()) {
1473
+ deactivateRenderer();
1474
+ } else {
1475
+ setIsToggleMode(false);
1476
+ }
1477
+ }
1389
1478
  });
1390
1479
  };
1391
1480
  const hasInnerText = (element) => "innerText" in element;
@@ -1396,15 +1485,16 @@ ${context}
1396
1485
  return element.textContent ?? "";
1397
1486
  };
1398
1487
  const createCombinedTextContent = (elements) => elements.map((element) => extractElementTextContent(element).trim()).filter((textContent) => textContent.length > 0).join("\n\n");
1488
+ const createCombinedMarkdownContent = (elements) => elements.map((element) => elementToMarkdown(element).trim()).filter((markdownContent) => markdownContent.length > 0).join("\n\n");
1399
1489
  const copySingleElementToClipboard = async (targetElement2) => {
1400
1490
  showTemporaryGrabbedBox(createElementBounds(targetElement2));
1401
1491
  await new Promise((resolve) => requestAnimationFrame(resolve));
1402
1492
  let didCopy = false;
1403
1493
  try {
1404
1494
  if (isExtensionTextOnlyMode()) {
1405
- const plainTextContent = createCombinedTextContent([targetElement2]);
1406
- if (plainTextContent.length > 0) {
1407
- didCopy = await copyContent(plainTextContent, options.playCopySound ? playCopySound : void 0);
1495
+ const markdownContent = createCombinedMarkdownContent([targetElement2]);
1496
+ if (markdownContent.length > 0) {
1497
+ didCopy = await copyContent(markdownContent, options.playCopySound ? playCopySound : void 0);
1408
1498
  }
1409
1499
  } else {
1410
1500
  const content = await getHTMLSnippet(targetElement2);
@@ -1442,9 +1532,9 @@ ${context}
1442
1532
  let didCopy = false;
1443
1533
  try {
1444
1534
  if (isExtensionTextOnlyMode()) {
1445
- const plainTextContent = createCombinedTextContent(targetElements);
1446
- if (plainTextContent.length > 0) {
1447
- didCopy = await copyContent(plainTextContent, options.playCopySound ? playCopySound : void 0);
1535
+ const markdownContent = createCombinedMarkdownContent(targetElements);
1536
+ if (markdownContent.length > 0) {
1537
+ didCopy = await copyContent(markdownContent, options.playCopySound ? playCopySound : void 0);
1448
1538
  }
1449
1539
  } else {
1450
1540
  const elementSnippetResults = await Promise.allSettled(targetElements.map((element) => getHTMLSnippet(element)));
@@ -1602,6 +1692,7 @@ ${context}
1602
1692
  document.body.style.cursor = "crosshair";
1603
1693
  };
1604
1694
  const deactivateRenderer = () => {
1695
+ setIsToggleMode(false);
1605
1696
  setIsHoldingKeys(false);
1606
1697
  setIsActivated(false);
1607
1698
  document.body.style.cursor = "";
@@ -1627,11 +1718,25 @@ ${context}
1627
1718
  deactivateRenderer();
1628
1719
  return;
1629
1720
  }
1721
+ if (event.key === "Enter" && isHoldingKeys()) {
1722
+ setIsToggleMode(true);
1723
+ if (keydownSpamTimerId !== null) {
1724
+ window.clearTimeout(keydownSpamTimerId);
1725
+ keydownSpamTimerId = null;
1726
+ }
1727
+ if (!isActivated()) {
1728
+ if (holdTimerId) window.clearTimeout(holdTimerId);
1729
+ activateRenderer();
1730
+ options.onActivate?.();
1731
+ }
1732
+ return;
1733
+ }
1630
1734
  if (!options.allowActivationInsideInput && isKeyboardEventTriggeredByInput(event)) {
1631
1735
  return;
1632
1736
  }
1633
1737
  if (!isTargetKeyCombination(event)) return;
1634
1738
  if (isActivated()) {
1739
+ if (isToggleMode()) return;
1635
1740
  if (keydownSpamTimerId !== null) {
1636
1741
  window.clearTimeout(keydownSpamTimerId);
1637
1742
  }
@@ -1652,13 +1757,15 @@ ${context}
1652
1757
  options.onActivate?.();
1653
1758
  }, options.keyHoldDuration);
1654
1759
  }, {
1655
- signal: eventListenerSignal
1760
+ signal: eventListenerSignal,
1761
+ capture: true
1656
1762
  });
1657
1763
  window.addEventListener("keyup", (event) => {
1658
1764
  if (!isHoldingKeys() && !isActivated()) return;
1659
1765
  const isReleasingModifier = !event.metaKey && !event.ctrlKey;
1660
1766
  const isReleasingC = event.key.toLowerCase() === "c";
1661
1767
  if (isReleasingC || isReleasingModifier) {
1768
+ if (isToggleMode()) return;
1662
1769
  deactivateRenderer();
1663
1770
  }
1664
1771
  }, {
@@ -1740,9 +1847,17 @@ ${context}
1740
1847
  if (isRendererActive() || isCopying() || didJustDrag()) {
1741
1848
  event.preventDefault();
1742
1849
  event.stopPropagation();
1743
- if (didJustDrag()) {
1850
+ const hadDrag = didJustDrag();
1851
+ if (hadDrag) {
1744
1852
  setDidJustDrag(false);
1745
1853
  }
1854
+ if (isToggleMode() && !isCopying()) {
1855
+ if (!isHoldingKeys()) {
1856
+ deactivateRenderer();
1857
+ } else {
1858
+ setIsToggleMode(false);
1859
+ }
1860
+ }
1746
1861
  }
1747
1862
  }, {
1748
1863
  signal: eventListenerSignal,
@@ -1765,10 +1880,14 @@ ${context}
1765
1880
  document.body.style.cursor = "";
1766
1881
  });
1767
1882
  const rendererRoot = mountRoot();
1768
- const selectionVisible = createMemo(() => false);
1883
+ const selectionVisible = createMemo(() => isRendererActive() && !isDragging() && Boolean(targetElement()));
1769
1884
  const dragVisible = createMemo(() => isRendererActive() && isDraggingBeyondThreshold());
1770
1885
  const labelVariant = createMemo(() => isCopying() ? "processing" : "hover");
1771
- const labelVisible = createMemo(() => isRendererActive() && !isDragging() && mouseHasSettled() && (Boolean(targetElement()) && !isSameAsLast() || !targetElement()) || isCopying());
1886
+ const labelVisible = createMemo(() => {
1887
+ if (isCopying()) return true;
1888
+ if (successLabels().length > 0) return false;
1889
+ return isRendererActive() && !isDragging() && mouseHasSettled() && (Boolean(targetElement()) && !isSameAsLast() || !targetElement());
1890
+ });
1772
1891
  const progressVisible = createMemo(() => isCopying() && showProgressIndicator() && hasValidMousePosition());
1773
1892
  const crosshairVisible = createMemo(() => isRendererActive() && !isDragging());
1774
1893
  render(() => createComponent(ReactGrabRenderer, {
@@ -1805,6 +1924,7 @@ ${context}
1805
1924
  get labelVisible() {
1806
1925
  return labelVisible();
1807
1926
  },
1927
+ labelZIndex: 2147483646,
1808
1928
  get progressVisible() {
1809
1929
  return progressVisible();
1810
1930
  },
@@ -1855,4 +1975,4 @@ if (!window[EXTENSION_MARKER]) {
1855
1975
  globalApi = init();
1856
1976
  }
1857
1977
 
1858
- export { getGlobalApi, init, playCopySound };
1978
+ export { elementToMarkdown, getGlobalApi, htmlToMarkdown, init, playCopySound };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-grab",
3
- "version": "0.0.39",
4
- "description": "",
3
+ "version": "0.0.40",
4
+ "description": "Grab any element in your app and give it to Cursor, Claude Code, or other AI coding agents.",
5
5
  "keywords": [
6
6
  "react",
7
7
  "grab",
@@ -54,6 +54,7 @@
54
54
  "devDependencies": {
55
55
  "@babel/core": "^7.28.5",
56
56
  "@babel/preset-typescript": "^7.28.5",
57
+ "@types/turndown": "^5.0.6",
57
58
  "babel-preset-solid": "^1.9.10",
58
59
  "esbuild-plugin-babel": "^0.2.3",
59
60
  "eslint": "^9.37.0",
@@ -70,7 +71,8 @@
70
71
  "bippy": "^0.5.14",
71
72
  "modern-screenshot": "^4.6.6",
72
73
  "solid-js": "^1.9.10",
73
- "solid-sonner": "^0.2.8"
74
+ "solid-sonner": "^0.2.8",
75
+ "turndown": "^7.2.2"
74
76
  },
75
77
  "scripts": {
76
78
  "build": "NODE_ENV=production tsup",