Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/storybook/stories/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const COMMENT_PRESET_KEYS: readonly CommentPreset[] = [

export const createMenuActions = (openEnabled: boolean): ContextMenuAction[] => [
{ id: "copy", label: "Copy", shortcut: "C", onAction: noop },
{ id: "copy-html", label: "Copy HTML", onAction: noop },
{ id: "copy-details", label: "Copy details", onAction: noop },
{ id: "open", label: "Open", shortcut: "O", enabled: openEnabled, onAction: noop },
{ id: "comment", label: "Comment", shortcut: "Enter", onAction: noop },
];
Expand Down
6 changes: 6 additions & 0 deletions apps/website-v2/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
40 changes: 0 additions & 40 deletions packages/react-grab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,46 +124,6 @@ if (process.env.NODE_ENV === "development") {
}
```

## Primitives

`react-grab/primitives` exposes low-level building blocks for tools that need to freeze the page, hit-test elements, or gather React source context outside of React Grab's UI.

| Export | Description |
| --- | --- |
| `freeze(elements?)` | Pause React updates, animations, and pseudo-states. Pass elements to scope animation freezing, or omit to freeze the whole page. |
| `unfreeze()` | Resume everything `freeze()` paused. |
| `isFreezeActive()` | Returns `true` while frozen. |
| `getElementsAtPosition(x, y)` | Hit-test elements at viewport coordinates while frozen (temporarily lifts the pointer-events block). |
| `getElementContext(element)` | Returns the same context React Grab copies to clipboard (`snippet`), plus structured source location, component name, owner stack, CSS selector, computed styles, HTML preview, and fiber. |
| `copyContent(text, options?)` | Copies text to the clipboard in the same three-format layout React Grab uses (plain text, HTML, structured metadata). Returns `true` on success. |
| `openFile(path, line?)` | Open a source file in the user's editor via the dev server or `vscode://` protocol. |

```js
import {
freeze,
unfreeze,
getElementsAtPosition,
getElementContext,
copyContent,
} from "react-grab/primitives";

freeze();

document.addEventListener("pointermove", (e) => {
const [topElement] = getElementsAtPosition(e.clientX, e.clientY);
highlight(topElement);
});

document.addEventListener("click", async (e) => {
const [target] = getElementsAtPosition(e.clientX, e.clientY);
const context = await getElementContext(target);
console.log(context.snippet); // formatted text identical to clipboard output
console.log(context.filePath); // "/src/components/Button.tsx"
console.log(context.lineNumber); // 42
unfreeze();
});
```

## Plugins

Use plugins to extend React Grab's built-in UI with context menu actions, toolbar menu items, lifecycle hooks, and theme overrides. Plugins run within React Grab.
Expand Down
140 changes: 0 additions & 140 deletions packages/react-grab/e2e/copy-styles.spec.ts

This file was deleted.

5 changes: 2 additions & 3 deletions packages/react-grab/e2e/drag-selection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ test.describe("Drag Selection", () => {

const clipboardContent = await reactGrab.getClipboardContent();

expect(clipboardContent).toContain("Buy groceries");
expect(clipboardContent).toContain("TodoItem");
});

test("should cancel drag selection on Escape", async ({ reactGrab }) => {
Expand Down Expand Up @@ -199,8 +199,7 @@ test.describe("Drag Selection", () => {

const clipboardContent = await reactGrab.getClipboardContent();
expect(clipboardContent).toBeTruthy();
expect(clipboardContent).toContain("Buy groceries");
expect(clipboardContent).toContain("Write tests");
expect(clipboardContent).toContain("TodoItem");
});

test("should show visual feedback during drag", async ({ reactGrab }) => {
Expand Down
44 changes: 12 additions & 32 deletions packages/react-grab/e2e/element-context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from "./fixtures.js";

test.describe("Element Context Fallback", () => {
test.describe("React Elements", () => {
test("should include component names in clipboard for React elements", async ({
test("should copy a compact reference or verbose context for React elements", async ({
reactGrab,
}) => {
await reactGrab.activate();
Expand All @@ -12,35 +12,12 @@ test.describe("Element Context Fallback", () => {
await reactGrab.clickElement("[data-testid='todo-list'] h1");

const clipboard = await reactGrab.getClipboardContent();
expect(clipboard).toContain("TodoList");
const isCompactFormat = /^\[<\w+>/.test(clipboard) && clipboard.includes("TodoList");
const isVerboseFormat = clipboard.includes("TodoList");
expect(isCompactFormat || isVerboseFormat).toBe(true);
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
});

test("should include HTML preview with tag and content", async ({ reactGrab }) => {
await reactGrab.activate();

await reactGrab.hoverElement("[data-testid='main-title']");
await reactGrab.waitForSelectionBox();
await reactGrab.clickElement("[data-testid='main-title']");

const clipboard = await reactGrab.getClipboardContent();
expect(clipboard).toContain("<h1");
expect(clipboard).toContain("React Grab");
});

test("should include nested component names for deeply nested elements", async ({
reactGrab,
}) => {
await reactGrab.activate();

await reactGrab.hoverElement("[data-testid='nested-button']");
await reactGrab.waitForSelectionBox();
await reactGrab.clickElement("[data-testid='nested-button']");

const clipboard = await reactGrab.getClipboardContent();
expect(clipboard).toContain("NestedCard");
});

test("should include parent components in stack, not just immediate component", async ({
test("should produce useful context for nested elements", async ({
reactGrab,
}) => {
await reactGrab.activate();
Expand All @@ -50,11 +27,12 @@ test.describe("Element Context Fallback", () => {
await reactGrab.clickElement("[data-testid='nested-button']");

const clipboard = await reactGrab.getClipboardContent();
const inMatches = clipboard.match(/in\s+\S+/g) ?? [];
expect(inMatches.length).toBeGreaterThanOrEqual(2);
const isCompactFormat = /^\[<\w+>/.test(clipboard) && clipboard.includes("NestedCard");
const isVerboseFormat = clipboard.includes("NestedCard");
expect(isCompactFormat || isVerboseFormat).toBe(true);
});

test("should include ancestor component for todo item", async ({ reactGrab }) => {
test("should produce useful context for todo items", async ({ reactGrab }) => {
await reactGrab.activate();

const todoItem = "[data-testid='todo-list'] ul li:first-child span";
Expand All @@ -63,7 +41,9 @@ test.describe("Element Context Fallback", () => {
await reactGrab.clickElement(todoItem);

const clipboard = await reactGrab.getClipboardContent();
expect(clipboard).toContain("TodoItem");
const isCompactFormat = /^\[<\w+>/.test(clipboard) && clipboard.includes("TodoItem");
const isVerboseFormat = clipboard.includes("TodoItem");
expect(isCompactFormat || isVerboseFormat).toBe(true);
});
});

Expand Down
3 changes: 3 additions & 0 deletions packages/react-grab/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const PREVIEW_PRIORITY_ATTRS: readonly string[] = [
"title",
];

export const COMPACT_IDENTIFYING_ATTRS: readonly string[] = ["id", "data-testid"];
export const COMPACT_TEXT_MAX_LENGTH = 30;

export const PREVIEW_IDENTIFYING_ATTRS = new Set([
"id",
"data-testid",
Expand Down
23 changes: 22 additions & 1 deletion packages/react-grab/src/core/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ const formatAttrsForPreview = (element: Element): string => {
return identifyingParts.join("") + remainingParts.join("") + classAttr;
};

const getDirectTextContent = (element: Element): string => {
export const getDirectTextContent = (element: Element): string => {
let directText = "";
for (const node of element.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
Expand All @@ -553,6 +553,27 @@ const formatChildElements = (elements: Array<Element>): string => {
return `(${elements.length} elements)`;
};

export const getInlineHTMLPreview = (element: Element): string => {
const tagName = getTagName(element);

if (!(element instanceof HTMLElement)) {
const attrsHint = formatPriorityAttrs(element, {
truncate: false,
maxAttrs: PREVIEW_PRIORITY_ATTRS.length,
});
return `<${tagName}${attrsHint} />`;
}

const attrsText = formatAttrsForPreview(element);
const directText = getDirectTextContent(element);
const truncatedText = truncateString(directText, PREVIEW_TEXT_MAX_LENGTH);

if (truncatedText) {
return `<${tagName}${attrsText}>${truncatedText}</${tagName}>`;
}
return `<${tagName}${attrsText} />`;
};
Comment thread
cursor[bot] marked this conversation as resolved.

export const getHTMLPreview = (element: Element): string => {
const tagName = getTagName(element);
const attrsText = formatAttrsForPreview(element);
Expand Down
Loading
Loading