Skip to content

feat(react-grab): canvas screenshot labels on Shift+Cmd / Win+Shift / PrintScreen#397

Open
aidenybai wants to merge 4 commits into
mainfrom
cursor/screenshot-labels-0cb3
Open

feat(react-grab): canvas screenshot labels on Shift+Cmd / Win+Shift / PrintScreen#397
aidenybai wants to merge 4 commits into
mainfrom
cursor/screenshot-labels-0cb3

Conversation

@aidenybai

@aidenybai aidenybai commented May 26, 2026

Copy link
Copy Markdown
Owner

What

Adds a pre-screenshot helper: hold the platform screenshot modifier and react-grab will draw a small label on top of every visible React component on the page so the next OS screenshot captures component names and the file they live in.

Trigger:

  • macOS: hold Shift + Cmd (the modifier combo for Shift+Cmd+3/4/5)
  • Windows: hold Win + Shift (the modifier combo for Win+Shift+S)
  • Linux: press PrintScreen

Each label is rendered to the existing overlay canvas as:

ComponentName · page.tsx

(component display name + middot + basename of the source file).

No boxes, fills, or background highlights are drawn on the components themselves — only the small text pill above each top-left corner, which is what you'd actually want baked into a screenshot.

Labels disappear on:

  • modifier keyup (Mac/Windows)
  • PrintScreen keyup (Linux)
  • window blur (so a hung modifier can't leave the overlay stuck)

How

  • New utilities
    • utils/is-linux.ts — platform detection mirroring is-mac.ts
    • utils/is-screenshot-shortcut.tsisScreenshotShortcutPressed / isScreenshotShortcutReleased
    • utils/collect-screenshot-labels.ts — walks all fiber roots via bippy's traverseFiber / getNearestHostFibers / isCompositeFiber / getDisplayName, filters with the existing isUsefulComponentName, dedupes by host element, and skips anything off-screen. File names come from fiber _debugSource synchronously when available, falling back to the existing resolveSource() async path (which handles Next.js symbolication).
  • New render pass in OverlayCanvas (renderScreenshotLabels) that draws each label as a rounded pill: white component name + 60% white middot/file. Composited last so it sits above all the existing drag/selection/grabbed layers but never paints a box or fill on the underlying element.
  • New screenshotLabels prop plumbed through ReactGrabRendererPropsReactGrabRendererOverlayCanvas.
  • New screenshotLabelCandidates / screenshotLabelFileNames signals in core/index.tsx with activateScreenshotLabels / clearScreenshotLabels. Positions are recomputed reactively against the existing viewportVersion() (so scroll/resize updates label positions for free). Async file resolution is gated by a token so stale resolutions can't overwrite a fresh activation.
  • New keydown/keyup/blur listeners. Activation guards: skip if disabled, in prompt mode, copying, in a text input, or already active. The trigger key must be Meta / Shift / PrintScreen so unrelated key presses while a modifier is incidentally held (e.g. Cmd+Shift+P) don't kick this off.

Risks / coupling

  • Hot-path safe: the canvas pass only allocates per label and only runs while screenshotLabels.length > 0. The reactive memo short-circuits with length === 0 before subscribing to viewportVersion, so the feature has zero cost when not active.
  • No new boxes on top of components; no changes to existing drag/selection/grabbed rendering or to the label/toolbar DOM tree.
  • Linux PrintScreen is best-effort: some desktops capture before keyup, but labels at least appear during the press and clear cleanly afterward.

Verification

  • pnpm typecheck — clean
  • pnpm lint — 0 errors / 0 warnings
  • pnpm format — clean
  • pnpm build — clean
  • pnpm test — 536 passed, 2 pre-existing flakes unrelated to this change (element-context.spec.ts SVG fallback and touch-mode.spec.ts re-activation)
Open in Web Open in Cursor 

Summary by cubic

Adds a pre-screenshot overlay in react-grab that draws a small "ComponentName · file.tsx" label on every visible React component so OS screenshots capture names and source files. Labels render on the overlay canvas only while the screenshot shortcut is held, skipping tiny elements and avoiding overlap for readability.

  • New Features

    • Trigger: macOS Shift+Cmd, Windows Win+Shift, Linux PrintScreen; clears on keyup/blur; positions auto-update; zero idle cost when inactive.
    • Placement: skip elements under 80×28 px, sort by element area, and greedily avoid overlapping labels.
  • Bug Fixes

    • Reliable label collection: manual, iterative fiber walk with nearest host lookup; supports multiple React roots.
    • Hardened activation: gated by isEnabled; listen on window and document (capture) so stopPropagation can’t block.
    • Platform/path correctness: avoid misdetecting Android as Linux; normalize Windows-style paths so file names resolve correctly.

Written for commit 19c8419. Summary will update on new commits. Review in cubic

… PrintScreen

Hold Shift+Cmd (Mac), Win+Shift (Windows), or press PrintScreen (Linux)
to overlay component labels on every visible React component before
taking a screenshot.

Each label is rendered onto the existing overlay canvas as 'ComponentName
\u00b7 page.tsx' (component display name + basename of the source file)
with no selection boxes or background highlights on the components
themselves.

Implementation:
- new utilities: is-linux, is-screenshot-shortcut, collect-screenshot-labels
- new OverlayCanvas render pass (text-only, no per-element fill/stroke)
- new keydown/keyup/blur handlers in core to activate/clear labels
- component file path is resolved synchronously from fiber _debugSource
  when present and asynchronously via resolveSource() as a fallback so
  the label upgrades from name-only to name + file once available

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@vercel

vercel Bot commented May 26, 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 May 26, 2026 1:56am
react-grab-website Ready Ready Preview, Comment May 26, 2026 1:56am

@pkg-pr-new

pkg-pr-new Bot commented May 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cli@397
npm i https://pkg.pr.new/aidenybai/react-grab/grab@397
npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/mcp@397
npm i https://pkg.pr.new/aidenybai/react-grab@397

commit: 19c8419

@aidenybai aidenybai marked this pull request as ready for review May 26, 2026 00:58

@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 8 files

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

Re-trigger cubic

Comment thread packages/react-grab/src/utils/is-linux.ts Outdated
Comment thread packages/react-grab/src/utils/collect-screenshot-labels.ts Outdated
Comment thread packages/react-grab/src/utils/collect-screenshot-labels.ts Outdated
- isLinux(): also check navigator.userAgent for 'Android' so an Android
  device reporting navigator.platform = 'Linux armv8l' is not treated as
  desktop Linux (and doesn't get PrintScreen handling on a touch device).
- collectFiberRoots() fallback: don't abort sibling traversal after the
  first root is found. Pages mounting multiple React roots (e.g. portals,
  micro-frontends) were missing labels for every root past the first.
- resolveScreenshotLabelFileName(): run resolveSource()'s file path
  through normalizeFilePath() before basename extraction so Windows-style
  backslash paths split correctly. getFileBaseName() also now coerces
  backslashes defensively for sync _debugSource paths.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… a no-op + isEnabled gate

Two bugs, both stopped labels from ever rendering:

1. collectScreenshotLabels() used bippy's traverseFiber(root, cb, true),
   which in the published bippy build only visits the passed-in fiber and
   does not recurse into .child/.sibling. e2e instrumentation showed:
       roots: 1, visited: 1, composite: 0
   for a tree with 312 fibers and 24 composite components. Replaced with
   a manual recursive walker (and a manual nearest-host-element finder)
   so candidate collection is bippy-version-independent.

2. The keydown handler gated activation on isEnabled() (the toolbar's
   collapsed/enabled signal). With the toolbar collapsed the shortcut
   was silently swallowed even though the screenshot helper has nothing
   to do with selection mode. Removed the gate.

Also hardened activation so an upstream stopPropagation on either window
or document cannot swallow the shortcut: same handler is registered on
both targets in capture phase, idempotent via the existing
'already-active' early-return.

Added e2e/screenshot-labels.spec.ts with 9 tests covering all three
platforms (macOS Cmd+Shift, Windows Win+Shift, Linux PrintScreen),
their negative cases (Cmd-only, Shift-only, Ctrl+Shift, Meta+Shift on
Linux), and clean teardown on keyup. Tests mock navigator.platform via
addInitScript so the platform branches can be exercised on Linux CI.
Confirms the activation path actually paints to the canvas.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
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.

2 issues found across 3 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/utils/collect-screenshot-labels.ts Outdated
Comment thread packages/react-grab/src/core/index.tsx
…erlap)

Three changes to cut visual clutter:

1. Only consider components whose host element is at least
   SCREENSHOT_LABEL_MIN_ELEMENT_WIDTH_PX (80) and
   SCREENSHOT_LABEL_MIN_ELEMENT_HEIGHT_PX (28). This drops icon-sized
   wrappers and styled-component leaves that the user almost never
   wants labeled in a screenshot.

2. Render labels sorted by host element area (largest first), so when
   we drop overlapping labels we keep the most structurally meaningful
   parent and skip the smaller children sharing the same region.

3. Greedy non-overlap placement in the canvas pass: a label is only
   painted if its rounded-rect bounds do not intersect any already
   painted label (with SCREENSHOT_LABEL_COLLISION_PADDING_PX gap).
   This keeps the overlay readable on dense apps where the previous
   pass produced a stack of pills sitting on top of each other.

Also address review feedback on the prior commit:

- restore isEnabled() guard on the screenshot keydown handler
  (cubic + Cursor Bugbot both flagged that disabling the toolbar
  should also silence the screenshot shortcut)
- convert the fiber traversal in collectScreenshotLabels and the
  fallback DOM walk in collectFiberRoots from recursive to
  explicit-stack iterative (cubic flagged stack-overflow risk on
  deep React trees)

ScreenshotLabel grows an area field so the renderer can sort; the
collection pass adds the candidate's host bounds area into the
candidate via the memo. No state shape changes outside that.

Adds an e2e test that verifies setEnabled(false) actually silences
the shortcut, alongside the previously added platform/keypress
coverage. All 10 screenshot-labels tests still pass.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

@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 and found 1 potential issue.

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 19c8419. Configure here.

});
}

return labels;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Labels stale on nested scroll

Medium Severity

While the screenshot shortcut is held, label coordinates are recomputed only when viewportVersion changes. That counter is driven by window resize/scroll and the periodic bounds sync, which does not run during screenshot-only use. Scrolling inside a nested overflow container does not update it, so canvas labels can stay offset from their components.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 19c8419. Configure here.

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.

2 participants