Skip to content

Add build-time demo mode for React Grab#472

Open
aidenybai wants to merge 6 commits into
mainfrom
demo-mode
Open

Add build-time demo mode for React Grab#472
aidenybai wants to merge 6 commits into
mainfrom
demo-mode

Conversation

@aidenybai

@aidenybai aidenybai commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds an opt-in, build-time demo mode (process.env.IS_DEMO) that compiles React Grab into a display-only showcase, plus a react-grab/demo driver (createGrabDemo) for scripting it with synthetic events.
  • Demo mode never touches the host page: real (isTrusted) input is ignored, the clipboard and localStorage are never written, host animations / React updates / cursor / body styles are never frozen, and the overlay is click-through.
  • Every demo-only behavior is gated behind a build-time IS_DEMO constant that dead-code-eliminates from normal library builds — the default bundle's behavior and size are unchanged (verified: zero demo markers in index.global.js).
  • The demo is scoped to a container (hit-testing, toolbar viewport, scroll re-anchor) and mounts its own shadow host (data-react-grab-demo) so it can coexist with a normal React Grab instance on the same page.

How it works

  • process.env.IS_DEMO is a build-time define: library entries compile it to "" (so if (IS_DEMO) branches are DCE'd), demo entries to "true". pnpm build:demo emits the extra demo.* bundles (opt-in, so the published package stays lean).
  • createGrabDemo({ container }) mounts demo-mode React Grab scoped to the container, paints an animated cursor, and returns low-level synthetic-event drivers (moveCursor, pulseCursor, click, pressKey, typeText, getInput, cancel, dispose, …). The consumer scripts the showcase; the library doesn't bake in a sequence.
  • Single-instance by design: createGrabDemo throws on a second concurrent instance and restores the container's position + scope on dispose.

Also in here

  • Unified the four toolbar-state writers through updateToolbarState / a new syncEnabledState, with a canonical DEFAULT_TOOLBAR_STATE.
  • New additive reset() API (returns React Grab to a clean slate without disposing).
  • makeIifePack / makeModulePack build factories replace four near-identical pack configs.

Test plan

  • pnpm typecheck, pnpm lint, pnpm format
  • e2e: full functional suite (667), plus toolbar / keyboard / freeze-updates / selection green
  • Browser: demo grabs via synthetic events; real clicks/keys + Cmd/Ctrl+C ignored; host never frozen (html pointer-events: auto, body styles untouched); overlay click-through; library + demo coexist with separate shadow hosts; reset() and the single-instance/dispose lifecycle behave
  • CI

Note

Medium Risk
Touches core interaction paths (event listeners, hit-testing, freeze/copy) and init lifecycle; demo gating limits production impact, but scoped container behavior could affect future non-demo uses of container.

Overview
Introduces an opt-in demo build (pnpm build:demo / process.env.IS_DEMO) that compiles React Grab as a display-only showcase: trusted user input is dropped, clipboard and localStorage are no-ops, host freezing (React updates, animations, pseudo-states, body styles, global cursor) is skipped, and the shadow overlay is click-through. Demo-only paths use a build-time IS_DEMO constant so normal library bundles should DCE those branches.

Adds init({ container }) scope via a runtime singleton: hit-testing, drag selection, and toolbar viewport math are confined to the container; the toolbar can re-anchor on scroll in demo builds when scoped.

Ships createGrabDemo in demo.ts—mounts scoped React Grab, paints a synthetic cursor, and exposes choreography primitives (moveCursor, click, typeText, pressKey, etc.) that dispatch untrusted pointer/keyboard events. Also adds api.reset() for clearing selection between showcase loops without dispose, and refactors Vite pack config into makeIifePack / makeModulePack factories with separate demo entry outputs.

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


Summary by cubic

Adds an opt-in build-time demo mode that compiles React Grab into a display-only, container‑scoped showcase with a react-grab/demo driver for scripted synthetic events. Normal builds are unchanged; demo-only code is gated by process.env.IS_DEMO and renders a click‑through overlay.

  • New Features

    • Opt-in demo build via process.env.IS_DEMO; pnpm build:demo emits demo.* bundles.
    • Demo ignores trusted input, never writes clipboard or localStorage, and never freezes host React updates/animations/cursor/body; overlay is click-through.
    • Container-scoped demo; hit-testing and toolbar viewport are confined, and the toolbar re-anchors on container scroll.
    • react-grab/demo driver: createGrabDemo({ container }) with scripted-event primitives (moveCursor, click, pulseCursor, pressKey, typeText, setInputValue, getInput, cancel, dispose, …); always paints an animated cursor.
    • Added api.reset() to clear selection and grabbed boxes between runs.
  • Bug Fixes

    • getToolbarState() now reads the live currentToolbarState() (fallbacks to loadToolbarState()), so it returns state in demo builds.
    • setToolbarState() bases partial updates on the live state first, preserving prior fields when persistence is gated off in demo.
    • Scoped hit-testing now falls back to the next in-scope grabbable when the topmost element is out of scope; elementsFromPoint and drag selection respect scope.
    • Scope lifecycle is owned by init(): it applies the container after the single-init guard and clears it on cleanup, preventing dangling scope from a no-op init while keeping first-paint toolbar anchoring.

Written for commit 79d921d. Summary will update on new commits.

Review in cubic

Adds an opt-in, build-time demo mode (process.env.IS_DEMO) that compiles
React Grab into a display-only showcase, plus a react-grab/demo driver
(createGrabDemo) for scripting it with synthetic events.

Demo mode never touches the host page: real (isTrusted) input is ignored,
the clipboard and localStorage are never written, host animations / React
updates / cursor / body styles are never frozen, and the overlay is
click-through. Every demo-only behavior is gated behind a build-time
IS_DEMO constant that dead-code-eliminates from normal library builds, so
the default bundle is unchanged. The demo is scoped to a container
(hit-testing, toolbar viewport, scroll re-anchor) and mounts its own shadow
host (data-react-grab-demo) so it can coexist with a normal React Grab
instance on the same page.

Also unifies the toolbar-state writers through updateToolbarState /
syncEnabledState with a canonical DEFAULT_TOOLBAR_STATE, adds a reset()
API, and extracts makeIifePack / makeModulePack build factories.
@vercel

vercel Bot commented Jun 14, 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 14, 2026 7:40am
react-grab-website Ready Ready Preview, Comment Jun 14, 2026 7:40am

@pkg-pr-new

pkg-pr-new Bot commented Jun 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 79d921d

Comment thread packages/react-grab/src/core/index.tsx 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.

3 issues found across 21 files

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

Re-trigger cubic

Comment thread packages/react-grab/src/demo.ts Outdated
Comment thread packages/react-grab/src/utils/get-element-at-position.ts Outdated
Comment thread packages/react-grab/src/components/toolbar/state.ts
- getToolbarState() now reads the live currentToolbarState() (falling back to
  loadToolbarState()), so it no longer returns null in demo builds where
  loadToolbarState is gated off. (flagged by cursor + cubic)
- Scoped hit-testing folds isWithinScope into the grabbable search so an
  out-of-scope topmost element falls back to the next in-scope grabbable
  instead of nulling the hit. (cubic P2)
Comment thread packages/react-grab/src/demo.ts Outdated
Comment thread packages/react-grab/src/demo.ts
createGrabDemo set the scope container before calling init(), so a no-op
init (already-initialized) would leave the scope singleton pointing at the
demo container while returning a noop API. init() now applies the scope
after its single-init guard (and clears it on cleanup), so a no-op init can
never set scope. Scope is still applied before the renderer mounts, so the
toolbar still anchors to the container on first paint. (flagged by cursor,
cubic, and Vercel Agent)
Drop refactors that were scope creep beyond the demo feature:

- Revert the toolbar-state writer unification (syncEnabledState + routing
  setEnabled/setToolbarState/the toolbar handler through it). Those were a
  DRY refactor of pre-existing code unrelated to demo mode. reset() now just
  clears the active selection / grabbed boxes instead of also rewriting the
  toolbar (the demo never changes toolbar state).
- Revert the demo-specific shadow-host attribute. The demo reuses the normal
  data-react-grab host; the separate host only mattered if the auto-init
  library and the demo loaded on the same page, which the demo page doesn't do.

Keeps the actual demo surface: the IS_DEMO build flag and its gates, the
scope container, the react-grab/demo driver, and toolbar-persistence gating.
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 6 files (changes from recent commits).

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

Re-trigger cubic

Comment thread packages/react-grab/src/core/index.tsx Outdated
After gating persistence off in demo, setToolbarState based partial updates
on loadToolbarState() (null in demo), dropping previously set fields. Read
currentToolbarState() first (matching getToolbarState), falling back to
storage. (flagged by cursor + cubic)
Comment thread packages/react-grab/src/core/index.tsx
- Reuse the existing Position type for GrabDemoPoint instead of redefining it.
- Drop the speculative cursor?:boolean option and the nullable cursorElement
  it forced (4 null guards removed); the demo always paints its cursor.
- Inline the one-off resetAll helper into the reset() API member.
- Remove low-information comments that just restated `if (IS_DEMO) return`;
  keep the substantive "why" comments (runtime-mode header, ignoreRealInput
  DCE, copy-content feedback, freeze-updates toolbar path).
- Fix an inaccurate saveToolbarState comment (clipboard -> storage), drop a
  stale react-grab/demo entrypoint reference, and trim an init-internal detail
  from the public Options.container doc.

@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 2 potential issues.

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 79d921d. Configure here.

Comment thread packages/react-grab/src/components/toolbar/index.tsx
Comment thread packages/react-grab/src/utils/runtime-mode.ts
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