Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
87de956
feat: add plugin toolbar entries API
aidenybai Mar 27, 2026
cc0f51d
refactor: simplify toolbar entries API surface
aidenybai Mar 27, 2026
b617ef9
style: apply oxfmt formatting
aidenybai Mar 27, 2026
254010a
fix: restore toolbarEntryOverrides prop for SolidJS reactivity
aidenybai Mar 27, 2026
c654677
feat: replace toy toolbar entries with render monitor and FPS meter
aidenybai Mar 27, 2026
00db1cf
docs: add toolbar entries examples to README
aidenybai Mar 27, 2026
f333269
docs: restructure README plugins section and remove em-dashes
aidenybai Mar 27, 2026
b411dde
fix: address PR review issues
aidenybai Mar 27, 2026
3389246
fix: use reconcile for toolbar entry override cleanup on unregister
aidenybai Mar 27, 2026
a61b9d4
fix: improve render monitor accuracy and cleanup on unregister
aidenybai Mar 27, 2026
191dedc
fix: check fiber flags for context and parent-driven renders
aidenybai Mar 28, 2026
d74cf72
refactor: extract core/index.tsx into 7 internal plugins
aidenybai Mar 28, 2026
0dc9dde
refactor: address code review issues in plugin migration
aidenybai Mar 28, 2026
3a5a110
fix: restore missing FEEDBACK_DURATION_MS import in copy-pipeline
aidenybai Mar 29, 2026
5c5897e
fix: address Cursor Bugbot review comments on PR #269
aidenybai Mar 29, 2026
c07668f
style: auto-format README and gym toolbar-entries page
aidenybai Mar 29, 2026
2b57df5
fix: restore missing event wiring and signal tracking from plugin mig…
aidenybai Mar 29, 2026
3cd3c58
fix: restore contextmenu behavior after keyboard nav and comments pin…
aidenybai Mar 29, 2026
3ab872f
style: apply AGENTS.md code quality rules
aidenybai Mar 29, 2026
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
8 changes: 1 addition & 7 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [
[
"react-grab",
"grab",
"@react-grab/cli"
]
],
"linked": [["react-grab", "grab", "@react-grab/cli"]],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
Expand Down
36 changes: 18 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@

- MUST: Call signals as functions: `count()` not `count`.
- MUST: Use functional updates when new state depends on old: `setCount((prev) => prev + 1)`.
- MUST: Keep signals atomic (one per value)one big state object loses granularity.
- MUST: Keep signals atomic (one per value):one big state object loses granularity.
- MUST: Use derived functions `() => count() * 2` for cheap/infrequent derivations.
- MUST: Use `createMemo(() => ...)` for expensive/frequent derivationscaches result.
- MUST: Use `createMemo(() => ...)` for expensive/frequent derivations:caches result.
- MUST: Use `createEffect` for side effects only (DOM, localStorage, subscriptions).
- MUST: Call `onCleanup(() => ...)` inside effects for subscriptions/intervals/listeners.
- MUST: Use path syntax for store updates: `setStore("users", 0, "name", "Jane")`.
Expand All @@ -44,8 +44,8 @@
- SHOULD: Use `untrack(() => value())` to read without subscribing.
- SHOULD: Use `createStore({ ... })` for nested objects with fine-grained reactivity.
- SHOULD: Use `produce(draft => { ... })` for complex store mutations.
- NEVER: Derive state via `createEffect(() => setX(y()))`use memo or derived function.
- NEVER: Place side effects inside `createMemo`causes infinite loops/crashes.
- NEVER: Derive state via `createEffect(() => setX(y()))`:use memo or derived function.
- NEVER: Place side effects inside `createMemo`:causes infinite loops/crashes.

### Effect Taxonomy

Expand All @@ -54,10 +54,10 @@ Before writing `createEffect`, classify the work and pick the right primitive:
- MUST: Use `createMemo` when the result is pure derived state from other signals/stores. If no external system is touched, it is not an effect.
- MUST: Use event handlers and direct action calls when work happens because a user clicked, selected, or navigated. Do not watch a flag/token in an effect to trigger imperative logic.
- MUST: Use `onMount`/`onCleanup` for one-time lifecycle setup and teardown (subscriptions, timers, imperative DOM wiring) that should not rerun for reactive changes.
- MUST: Keep `createEffect` single-purposeone effect, one external bridge. Split mixed-responsibility effects.
- MUST: Keep `createEffect` single-purpose:one effect, one external bridge. Split mixed-responsibility effects.
- SHOULD: Use keyed ownership boundaries (keyed `<Show>`/`<For>`, or keyed `createRoot`) when local state should reset because an identity changed. Do not write a "watch key, clear state" effect.
- SHOULD: Normalize state at the write boundary, not via a repair effect that rewrites after the fact.
- NEVER: Use `createEffect` just to copy one store/signal into anotherfind the single source of truth.
- NEVER: Use `createEffect` just to copy one store/signal into another:find the single source of truth.
- NEVER: Use `createEffect` as an event bus (watching a trigger signal to run a command). Call the action directly from the event source.

### Props
Expand All @@ -67,29 +67,29 @@ Before writing `createEffect`, classify the work and pick the right primitive:
- SHOULD: Use `splitProps(props, ["keys"])` to separate local from pass-through props.
- SHOULD: Use `mergeProps(defaults, props)` for default values.
- SHOULD: Use `children(() => props.children)` only when transforming, otherwise `{props.children}`.
- NEVER: Destructure props `({ title })`breaks reactivity.
- NEVER: Destructure props `({ title })`:breaks reactivity.

### Control Flow

- MUST: Use `<For each={items()}>` for object arraysitem is value, index is signal.
- MUST: Use `<Index each={items()}>` for primitives/inputsitem is signal, index is number.
- MUST: Use `<For each={items()}>` for object arrays:item is value, index is signal.
- MUST: Use `<Index each={items()}>` for primitives/inputs:item is signal, index is number.
- MUST: Use `<Suspense fallback={...}>` for async, not `<Show when={!loading}>`.
- MUST: Access resource states via `data()`, `data.loading`, `data.error`, `data.latest`.
- SHOULD: Use `<Show when={cond()} fallback={...}>` for conditionals.
- SHOULD: Use `<Show when={val}>` callback for type narrowing: `{(v) => <div>{v().name}</div>}`.
- SHOULD: Use `<Switch>/<Match>` for multiple conditions.
- SHOULD: Use `createResource(source, fetcher)` for reactive async data.
- SHOULD: Use `<ErrorBoundary fallback={(err, reset) => ...}>` for render errors.
- NEVER: Use `.map()` in JSXuse `<For>` or `<Index>`.
- NEVER: Rely on ErrorBoundary for event handler or setTimeout errorsuse try/catch.
- NEVER: Use `.map()` in JSX:use `<For>` or `<Index>`.
- NEVER: Rely on ErrorBoundary for event handler or setTimeout errors:use try/catch.

### JSX & DOM

- MUST: Use `class` not `className`.
- MUST: Combine static `class="btn"` with reactive `classList={{ active: isActive() }}`.
- MUST: Use `onClick` for delegated events; `on:click` for native (element-level).
- MUST: Condition inside handler since events are not reactive: `onClick={() => props.onClick?.()}`.
- MUST: Read refs in `onMount` or effectsrefs connect after render.
- MUST: Read refs in `onMount` or effects:refs connect after render.
- MUST: Call `onCleanup` inside directives for cleanup.
- SHOULD: Use `on:click` for `stopPropagation`, capture, passive, or custom events.
- SHOULD: Use `style={{ color: color(), "--css-var": value() }}` for inline styles.
Expand Down Expand Up @@ -120,7 +120,7 @@ This is a pnpm + Turborepo monorepo (19 packages under `packages/`). No external

### Build before test

`pnpm build` must complete before `pnpm test` or `pnpm lint`Turborepo `dependsOn` enforces this, but be aware that `pnpm test` will rebuild if the build cache is cold. After modifying source files, always rebuild before running tests.
`pnpm build` must complete before `pnpm test` or `pnpm lint`:Turborepo `dependsOn` enforces this, but be aware that `pnpm test` will rebuild if the build cache is cold. After modifying source files, always rebuild before running tests.

### Approved build scripts

Expand All @@ -136,10 +136,10 @@ See root `package.json` scripts and `CONTRIBUTING.md` for the full list. Quick r

- **Install**: `ni` (or `pnpm install`)
- **Build**: `nr build` (or `pnpm build`)
- **Dev watch**: `nr dev` (or `pnpm dev`)watches core packages
- **Test**: `pnpm test`runs Playwright E2E + Vitest CLI tests
- **Lint**: `pnpm lint`oxlint on react-grab package
- **Typecheck**: `pnpm typecheck`tsc on react-grab package
- **Format**: `pnpm format`oxfmt
- **Dev watch**: `nr dev` (or `pnpm dev`):watches core packages
- **Test**: `pnpm test`:runs Playwright E2E + Vitest CLI tests
- **Lint**: `pnpm lint`:oxlint on react-grab package
- **Typecheck**: `pnpm typecheck`:tsc on react-grab package
- **Format**: `pnpm format`:oxfmt
- **CLI dev**: `npm_command=exec node packages/cli/dist/cli.js`
- **Test app**: `pnpm --filter @react-grab/e2e-app dev` (port 5175)
14 changes: 7 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ nr format # Format code with oxfmt
- **Use TypeScript interfaces** over types
- **Use arrow functions** over function declarations
- **Use kebab-case** for file names
- **Use descriptive variable names**avoid shorthands or 1-2 character names
- **Use descriptive variable names**:avoid shorthands or 1-2 character names
- Example: `innerElement` instead of `el`
- Example: `didPositionChange` instead of `moved`
- **Avoid type casting** (`as`) unless absolutely necessary
Expand Down Expand Up @@ -113,12 +113,12 @@ git commit -m "feat: add new feature"

We use conventional commits:

- `feat:`New feature
- `fix:`Bug fix
- `docs:`Documentation changes
- `chore:`Maintenance tasks
- `refactor:`Code refactoring
- `test:`Test additions or changes
- `feat:`:New feature
- `fix:`:Bug fix
- `docs:`:Documentation changes
- `chore:`:Maintenance tasks
- `refactor:`:Code refactoring
- `test:`:Test additions or changes

### Adding a Changeset

Expand Down
178 changes: 112 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,118 @@ This copies the element's context (file name, React component, and HTML source c
in LoginForm at components/login-form.tsx:46:19
```

## Plugins

Extend React Grab with custom toolbar buttons, context menu actions, lifecycle hooks, and theme overrides via the plugin API.

```js
import { registerPlugin, unregisterPlugin } from "react-grab";
```

### Toolbar Entries

Add custom buttons directly to the toolbar. Each entry can be a simple action button or open a dropdown panel:

```js
registerPlugin({
name: "my-devtools",
toolbarEntries: [
{
id: "fps",
icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/></svg>',
tooltip: "FPS Monitor",
// Action-only button (no dropdown), just toggle FPS tracking
onClick: (handle) => {
if (tracking) {
stopTracking();
handle.setBadge(undefined);
} else {
startTracking((fps) => handle.setBadge(fps));
}
},
},
{
id: "render-monitor",
icon: "🔍",
tooltip: "Render Monitor",
// Dropdown button: onRender receives a raw DOM container
onRender: (container, handle) => {
container.innerHTML = `<div style="padding:12px">
<strong>Renders: 0</strong>
<button id="clear">Clear</button>
</div>`;

container.querySelector("#clear").addEventListener("click", () => {
handle.setBadge(undefined);
});

// Return a cleanup function (called when dropdown closes)
return () => {
/* teardown */
};
},
},
],
});
```

The `handle` passed to callbacks provides:

- `handle.setBadge(value)` / `handle.setIcon(html)` / `handle.setTooltip(text)` to update the button at runtime
- `handle.open()` / `handle.close()` / `handle.toggle()` to control the dropdown
- `handle.api` for full React Grab API access

### Context Menu Actions

Add items to the right-click context menu or the toolbar dropdown menu:

```js
registerPlugin({
name: "my-plugin",
actions: [
{
id: "inspect",
label: "Inspect",
shortcut: "I",
onAction: (ctx) => console.dir(ctx.element),
},
{
id: "toggle-freeze",
label: "Freeze",
target: "toolbar",
isActive: () => isFrozen,
onAction: () => toggleFreeze(),
},
],
});
```

### Hooks

Listen to lifecycle events:

```js
registerPlugin({
name: "my-plugin",
hooks: {
onElementSelect: (element) => {
console.log("Selected:", element.tagName);
},
},
});
```

In React, register inside a `useEffect` and clean up on unmount:

```jsx
useEffect(() => {
registerPlugin({ name: "my-plugin" /* ... */ });
return () => unregisterPlugin("my-plugin");
}, []);
```

See [`packages/react-grab/src/types.ts`](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts) for the full `Plugin`, `PluginHooks`, `PluginConfig`, `ToolbarEntry`, and `ToolbarEntryHandle` interfaces.

## Manual Installation

If you're using a React framework or build tool, view instructions below:
Expand Down Expand Up @@ -127,72 +239,6 @@ if (process.env.NODE_ENV === "development") {
}
```

## 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.

Register a plugin using the `registerPlugin` and `unregisterPlugin` exports:

```js
import { registerPlugin } from "react-grab";

registerPlugin({
name: "my-plugin",
hooks: {
onElementSelect: (element) => {
console.log("Selected:", element.tagName);
},
},
});
```

In React, register inside a `useEffect`:

```jsx
import { registerPlugin, unregisterPlugin } from "react-grab";

useEffect(() => {
registerPlugin({
name: "my-plugin",
actions: [
{
id: "my-action",
label: "My Action",
shortcut: "M",
onAction: (context) => {
console.log("Action on:", context.element);
context.hideContextMenu();
},
},
],
});

return () => unregisterPlugin("my-plugin");
}, []);
```

Actions use a `target` field to control where they appear. Omit `target` (or set `"context-menu"`) for the right-click menu, or set `"toolbar"` for the toolbar dropdown:

```js
actions: [
{
id: "inspect",
label: "Inspect",
shortcut: "I",
onAction: (ctx) => console.dir(ctx.element),
},
{
id: "toggle-freeze",
label: "Freeze",
target: "toolbar",
isActive: () => isFrozen,
onAction: () => toggleFreeze(),
},
];
```

See [`packages/react-grab/src/types.ts`](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts) for the full `Plugin`, `PluginHooks`, and `PluginConfig` interfaces.

## Resources & Contributing Back

Want to try it out? Check out [our demo](https://react-grab.com).
Expand Down
5 changes: 1 addition & 4 deletions packages/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import { detectNonInteractive } from "../utils/is-non-interactive.js";
import { detectProject } from "../utils/detect.js";
import { handleError } from "../utils/handle-error.js";
import { highlighter } from "../utils/highlighter.js";
import {
installMcpServers,
promptMcpInstall,
} from "../utils/install-mcp.js";
import { installMcpServers, promptMcpInstall } from "../utils/install-mcp.js";
import { logger } from "../utils/logger.js";
import { spinner } from "../utils/spinner.js";

Expand Down
12 changes: 3 additions & 9 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import {
applyTransformWithFeedback,
installPackagesWithFeedback,
} from "../utils/cli-helpers.js";
import {
promptMcpInstall,
} from "../utils/install-mcp.js";
import { promptMcpInstall } from "../utils/install-mcp.js";
import {
detectProject,
findReactProjects,
Expand All @@ -23,14 +21,10 @@ import {
import { printDiff } from "../utils/diff.js";
import { handleError } from "../utils/handle-error.js";
import { highlighter } from "../utils/highlighter.js";
import {
getPackagesToInstall,
} from "../utils/install.js";
import { getPackagesToInstall } from "../utils/install.js";
import { logger } from "../utils/logger.js";
import { spinner } from "../utils/spinner.js";
import {
type AgentIntegration,
} from "../utils/templates.js";
import { type AgentIntegration } from "../utils/templates.js";
import {
previewOptionsTransform,
previewPackageJsonTransform,
Expand Down
4 changes: 1 addition & 3 deletions packages/cli/src/utils/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,7 @@ export const detectReactGrab = (projectRoot: string): boolean => {
return filesToCheck.some(hasReactGrabInFile);
};

const AGENT_PACKAGES = [
"@react-grab/mcp",
];
const AGENT_PACKAGES = ["@react-grab/mcp"];

export const detectUnsupportedFramework = (
projectRoot: string,
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/utils/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,3 @@ export const getPackagesToInstall = (

return packages;
};

4 changes: 3 additions & 1 deletion packages/cli/src/utils/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ export const TANSTACK_EFFECT = `useEffect(() => {
}
}, []);`;

export const TANSTACK_EFFECT_WITH_AGENT = (_agent: AgentIntegration): string => {
export const TANSTACK_EFFECT_WITH_AGENT = (
_agent: AgentIntegration,
): string => {
return TANSTACK_EFFECT;
};

Expand Down
Loading
Loading