Skip to content

feat(react-grab): add useReactGrab() hook + dials panel#512

Open
aidenybai wants to merge 8 commits into
mainfrom
feat/use-react-grab-dials
Open

feat(react-grab): add useReactGrab() hook + dials panel#512
aidenybai wants to merge 8 commits into
mainfrom
feat/use-react-grab-dials

Conversation

@aidenybai

@aidenybai aidenybai commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a dialkit-style useReactGrab() React hook for defining tweakable component parameters, surfaced through a SolidJS dials panel overlay. The panel reuses react-grab's existing edit-panel controls and anchors to the toolbar under the same affordance as style editing.

  • useReactGrab(name, config) (react-grab/react): declare tweakable params and read live values back. useReactGrabController() additionally exposes setValue/setValues/resetValues/getValues. Bridged from the SolidJS overlay via useSyncExternalStore.
  • Dials registry merges values on re-register (keeps current values for surviving paths, drops removed ones) and supports multiple hooks combined into one panel, with a divider between sections.
  • Full control parity: numbers, colors, selects, toggles, text, springs (with step-response curve visualization), actions, and nested/collapsible folders.
  • Keyboard navigation: engagement-gated window keydown handler — click into the panel to engage, then up/down to move rows (across panel boundaries), left/right to tweak (shift = large step), Enter to activate. Pointer activation only fires on genuine cursor movement, avoiding phantom re-activation when the active row's content swaps under a stationary cursor.
  • Exposes a new ./react package export (esm/cjs + types) and wires src/react.ts into the build.

Test plan

  • pnpm build && pnpm typecheck && pnpm lint && pnpm format all pass
  • In apps/e2e-app-vite, the Card and Scene hooks render as one combined panel with a section divider
  • Click a control row, then up/down navigates rows (including across the two panels), left/right tweaks values, Enter toggles/activates
  • Spring folder shows the curve viz with Duration/Bounce sub-fields
  • Color picker, cycle/select, toggle, and text controls all commit values and reflect live in the demo card
  • Arrow keys do not hijack the host page when the panel isn't engaged

Note

Medium Risk
Large new overlay surface (global key handlers, toolbar integration, React↔Solid state bridge) touches core UX; mitigations include engagement gating and merge-on-reregister, but regressions in selection/edit flows are plausible.

Overview
Introduces a dialkit-style dev workflow: React apps declare tweakable params with useReactGrab() / useReactGrabController() on the new react-grab/react export, backed by a Solid dials-registry and useSyncExternalStore (with a React 17 shim).

The overlay dials panel anchors to the toolbar (auto-opens after fade-in; Style toggles it when panels exist), merges multiple hook registrations into one searchable UI, and supports numbers, colors, selects, toggles, text, springs (with curve viz), actions, and nested folders. Shared createMenuList refactors edit-panel row navigation for both panels.

E2E/CI: the Vite dials demo is lazy-loaded and gated behind /?dials so default specs and perf baselines avoid the PR-only export and toolbar overlap. Perf workflow fully swaps react-grab/src and vite.config.ts on baseline runs so PR-only files and build entries do not leak.

Reviewed by Cursor Bugbot for commit b19f1bb. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Adds a dialkit-style useReactGrab() React hook and a SolidJS dials panel with a search bar to tweak component params live. The panel anchors to the toolbar under the Style button and merges multiple hooks into one view.

  • New Features

    • Exported useReactGrab() and useReactGrabController() from react-grab/react (ESM/CJS + types) via new ./react entry; React bridge via useSyncExternalStore (shim for React 17).
    • Dials panel overlay anchored to the toolbar; multiple hooks combine into one panel with section dividers.
    • Search filters by label across all panels; folders flatten in results; clearing restores the nested view.
    • Full control set: numbers, colors, selects, toggles, text, springs (with curve), actions, nested folders.
    • Keyboard UX: click to engage; Up/Down move; Left/Right tweak (Shift = larger); Enter activates; guards phantom pointer events.
    • Dials registry with merge-on-reregister and stable values across config changes.
    • Demo: apps/e2e-app-vite shows “Card” and “Scene” hooks in a combined panel; lazy-loaded and gated behind /?dials.
  • Bug Fixes

    • Preserve dial values and keep dismissed panels closed across re-registration; bridge pre-init subscriptions.
    • Fix number range inference for negative defaults; guard select cycling on empty options.
    • Keep spring control labels in search; reject non-spring objects in readSpring.
    • Share menu-list interactions with the style panel via createMenuList; clip dials overscroll.
    • Tear down dials panel position tracking on root disposal (fix RAF leak).
    • Drop empty listener sets so transient hook IDs don’t accumulate listeners.
    • Reset keyboard engagement on dismiss so reopened panels don’t hijack arrow/enter; ignore global keys while dismissed.
    • CI/perf: baseline swap removes PR-added react-grab/src and reverts packages/react-grab/vite.config.ts to base to avoid unresolved entries.

Written for commit b19f1bb. Summary will update on new commits.

Review in cubic

Introduce a dialkit-style useReactGrab() React hook for defining tweakable
component parameters, surfaced through a SolidJS "dials panel" overlay that
reuses the edit-panel controls (sliders, color picker, cycle, toggle, text)
and anchors to the toolbar under the same affordance as style editing.

- React<->Solid bridge via useSyncExternalStore with a dials registry that
  merges values on re-register and supports multiple hooks combined into one
  panel (with section dividers).
- Full control parity: numbers, colors, selects, toggles, text, springs
  (with curve visualization), actions, and nested folders.
- Keyboard navigation: engagement-gated window keydown handler with up/down
  row navigation, left/right value tweaking, and Enter to activate; pointer
  activation only fires on genuine cursor movement to avoid phantom events.
@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-grab-storybook Ready Ready Preview, Comment Jun 30, 2026 6:20am
react-grab-website Ready Ready Preview, Comment Jun 30, 2026 6:20am

@pkg-pr-new

pkg-pr-new Bot commented Jun 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@react-grab/cli@512
npm i https://pkg.pr.new/grab@512
npm i https://pkg.pr.new/react-grab@512

commit: b19f1bb

Comment thread packages/react-grab/src/react.ts
Comment thread packages/react-grab/src/core/index.tsx
Comment thread packages/react-grab/src/utils/resolve-dial-config.ts Outdated
Comment thread packages/react-grab/src/react.ts Outdated
Adds a search input at the top of the dials panel that filters controls by
label (case-insensitive substring). Matching controls render as a flat list
across all panels, folders are flattened, and keyboard navigation works over
the filtered set. Empty query restores the full nested view.
Comment thread packages/react-grab/src/components/dials-panel/view-rows.ts
Comment thread packages/react-grab/src/react.ts
Comment thread packages/react-grab/src/core/index.tsx

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/react-grab/src/components/dials-panel/view-rows.ts">

<violation number="1" location="packages/react-grab/src/components/dials-panel/view-rows.ts:251">
P1: Spring controls lose their label in search results. The search branch calls emitSpringRows directly without pushCollapsibleHeader, so spring-viz/spring-field rows render with no identifying label — users can't tell which spring they're adjusting.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/react-grab/src/components/dials-panel/view-rows.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/react-grab/src/constants.ts">

<violation number="1" location="packages/react-grab/src/constants.ts:148">
P2: TOOLBAR_FADE_IN_TRANSITION_MS is a fragile CSS-JS sync point: the JS value (400) must match the Tailwind `duration-400` class in the toolbar. A CSS class change silently breaks timing. Consider deriving from a single source (e.g. a shared token or CSS variable read) or adding a runtime assert.</violation>
</file>

<file name="packages/react-grab/src/types.ts">

<violation number="1" location="packages/react-grab/src/types.ts:513">
P2: `DialConfig` is over-permissive, so malformed typed-control objects can compile and then disappear at runtime. Tighten folder typing to reject objects with a `type` discriminator.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/react-grab/src/components/dials-panel/dial-control.tsx
Comment thread packages/react-grab/src/utils/resolve-dial-config.ts Outdated
Comment thread packages/react-grab/src/react.ts
Comment thread packages/react-grab/src/core/index.tsx
Comment thread packages/react-grab/src/components/dials-panel/controls-tree.tsx Outdated
Comment thread packages/react-grab/src/components/dials-panel/view-rows.ts Outdated
Comment thread packages/react-grab/src/components/dials-panel/index.tsx Outdated
| DialActionConfig
| DialConfig;

export interface DialConfig {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: DialConfig is over-permissive, so malformed typed-control objects can compile and then disappear at runtime. Tighten folder typing to reject objects with a type discriminator.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/types.ts, line 513:

<comment>`DialConfig` is over-permissive, so malformed typed-control objects can compile and then disappear at runtime. Tighten folder typing to reject objects with a `type` discriminator.</comment>

<file context>
@@ -438,6 +438,192 @@ export interface ReactGrabAPI {
+  | DialActionConfig
+  | DialConfig;
+
+export interface DialConfig {
+  [key: string]: DialControlConfig | boolean | undefined;
+  _collapsed?: boolean;
</file context>

Comment thread packages/react-grab/src/core/dials-registry.ts
Comment thread packages/react-grab/src/core/index.tsx Outdated
… with style panel

- preserve dial values and keep dismissed panels closed across config-change
  re-registration; bridge pre-init subscriptions to the live store
- fix inferNumberRange producing an inverted range for negative defaults
- guard select cycling against empty options
- keep spring control labels in search results
- extract createMenuList so the dials panel and style PropertyList share
  highlight / scroll-into-view / pointer handling; clip dials overscroll
Keeps the always-on demo out of the default page so it can't intercept the
selection prompt other specs click, and out of the static import graph so the
perf baseline build doesn't depend on PR-only react-grab/react exports.
Comment thread packages/react-grab/src/components/dials-panel/index.tsx
Comment thread packages/react-grab/src/components/dials-panel/view-rows.ts Outdated
…t 17)

- tear down dials panel position tracking on root disposal (RAF leak)
- ignore global arrow/enter keys once the panel is dismissed, not just disengaged
- reject non-spring objects in readSpring so keyboard adjusts can't corrupt values
- drop empty listener sets so listenersById can't grow across transient hook ids
- import useSyncExternalStore from use-sync-external-store/shim for the react>=17 peer
git checkout base -- path only updates files present in the base tree, so files
a PR adds linger and import symbols the base ref never exported, breaking the
baseline build. Remove HEAD src first so the baseline measures a consistent
base-only tree.
A PR adding a new build entry (e.g. ./src/react.ts) left the base build
pointing at a file the base ref lacks (UNRESOLVED_ENTRY). Reverting
vite.config.ts alongside src reproduces a self-consistent base library build.
Comment thread packages/react-grab/src/components/dials-panel/index.tsx
export interface ReactGrabController<C extends DialConfig> {
values: ResolvedDialValues<C>;
setValue: (path: string, value: DialValue) => void;
setValues: (partial: Record<string, DialValue>) => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setValues ignores dotted paths

Medium Severity

useReactGrabController exposes setValues as a flat Record<string, DialValue>, but the implementation passes the object through flattenPartial, which only reads top-level keys that match dial control keys (nested folders require nested objects). Callers who pass dotted paths—the same shape setValue uses—get a silent no-op instead of updated values.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 66171bc. Configure here.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/react-grab/src/constants.ts">

<violation number="1" location="packages/react-grab/src/constants.ts:148">
P2: TOOLBAR_FADE_IN_TRANSITION_MS is a fragile CSS-JS sync point: the JS value (400) must match the Tailwind `duration-400` class in the toolbar. A CSS class change silently breaks timing. Consider deriving from a single source (e.g. a shared token or CSS variable read) or adding a runtime assert.</violation>
</file>

<file name="packages/react-grab/src/types.ts">

<violation number="1" location="packages/react-grab/src/types.ts:513">
P2: `DialConfig` is over-permissive, so malformed typed-control objects can compile and then disappear at runtime. Tighten folder typing to reject objects with a `type` discriminator.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/react-grab/src/components/dials-panel/index.tsx
Resetting isPanelEngaged when the anchor clears prevents a reopened panel from
hijacking arrow/enter keys before a fresh click inside it.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b19f1bb. Configure here.

onTrigger={() => props.onTriggerAction(leafRow().panelId, control().path)}
/>
</div>
</Show>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested buttons on action rows

Medium Severity

Action dial rows render an outer navigable button whose children include ActionControl, which is another button. That invalid nesting can block or misroute clicks in some browsers and breaks expected menu semantics, so action dials may not fire reliably from the UI.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b19f1bb. Configure here.

export interface ReactGrabController<C extends DialConfig> {
values: ResolvedDialValues<C>;
setValue: (path: string, value: DialValue) => void;
setValues: (partial: Record<string, DialValue>) => void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useReactGrabController().setValues silently ignores dotted-path keys for nested (folder) controls because its implementation expects a nested object keyed by control.key, which contradicts its declared flat Record<string, DialValue> type.

Fix on Vercel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant