feat(react-grab): add useReactGrab() hook + dials panel#512
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
commit: |
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
| | DialActionConfig | ||
| | DialConfig; | ||
|
|
||
| export interface DialConfig { |
There was a problem hiding this comment.
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>
… 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.
…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.
| export interface ReactGrabController<C extends DialConfig> { | ||
| values: ResolvedDialValues<C>; | ||
| setValue: (path: string, value: DialValue) => void; | ||
| setValues: (partial: Record<string, DialValue>) => void; |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 66171bc. Configure here.
There was a problem hiding this comment.
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
Resetting isPanelEngaged when the anchor clears prevents a reopened panel from hijacking arrow/enter keys before a fresh click inside it.
There was a problem hiding this comment.
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).
❌ 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> |
There was a problem hiding this comment.
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.
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; |


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 exposessetValue/setValues/resetValues/getValues. Bridged from the SolidJS overlay viauseSyncExternalStore../reactpackage export (esm/cjs + types) and wiressrc/react.tsinto the build.Test plan
pnpm build && pnpm typecheck && pnpm lint && pnpm formatall passapps/e2e-app-vite, theCardandScenehooks render as one combined panel with a section dividerNote
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 newreact-grab/reactexport, backed by a Soliddials-registryanduseSyncExternalStore(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
createMenuListrefactors edit-panel row navigation for both panels.E2E/CI: the Vite dials demo is lazy-loaded and gated behind
/?dialsso default specs and perf baselines avoid the PR-only export and toolbar overlap. Perf workflow fully swapsreact-grab/srcandvite.config.tson 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
useReactGrab()anduseReactGrabController()fromreact-grab/react(ESM/CJS + types) via new./reactentry; React bridge viauseSyncExternalStore(shim for React 17).apps/e2e-app-viteshows “Card” and “Scene” hooks in a combined panel; lazy-loaded and gated behind/?dials.Bug Fixes
readSpring.createMenuList; clip dials overscroll.react-grab/srcand revertspackages/react-grab/vite.config.tsto base to avoid unresolved entries.Written for commit b19f1bb. Summary will update on new commits.