feat(react-grab): canvas screenshot labels on Shift+Cmd / Win+Shift / PrintScreen#397
feat(react-grab): canvas screenshot labels on Shift+Cmd / Win+Shift / PrintScreen#397aidenybai wants to merge 4 commits into
Conversation
… 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
commit: |
There was a problem hiding this comment.
3 issues found across 8 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- 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>
There was a problem hiding this comment.
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
…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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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; |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 19c8419. Configure here.


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:
Shift+Cmd(the modifier combo forShift+Cmd+3/4/5)Win+Shift(the modifier combo forWin+Shift+S)PrintScreenEach label is rendered to the existing overlay canvas as:
(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:
PrintScreenkeyup (Linux)blur(so a hung modifier can't leave the overlay stuck)How
utils/is-linux.ts— platform detection mirroringis-mac.tsutils/is-screenshot-shortcut.ts—isScreenshotShortcutPressed/isScreenshotShortcutReleasedutils/collect-screenshot-labels.ts— walks all fiber roots via bippy'straverseFiber/getNearestHostFibers/isCompositeFiber/getDisplayName, filters with the existingisUsefulComponentName, dedupes by host element, and skips anything off-screen. File names come from fiber_debugSourcesynchronously when available, falling back to the existingresolveSource()async path (which handles Next.js symbolication).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.screenshotLabelsprop plumbed throughReactGrabRendererProps→ReactGrabRenderer→OverlayCanvas.screenshotLabelCandidates/screenshotLabelFileNamessignals incore/index.tsxwithactivateScreenshotLabels/clearScreenshotLabels. Positions are recomputed reactively against the existingviewportVersion()(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.keydown/keyup/blurlisteners. Activation guards: skip if disabled, in prompt mode, copying, in a text input, or already active. The trigger key must beMeta/Shift/PrintScreenso unrelated key presses while a modifier is incidentally held (e.g. Cmd+Shift+P) don't kick this off.Risks / coupling
screenshotLabels.length > 0. The reactive memo short-circuits withlength === 0before subscribing toviewportVersion, so the feature has zero cost when not active.PrintScreenis best-effort: some desktops capture before keyup, but labels at least appear during the press and clear cleanly afterward.Verification
pnpm typecheck— cleanpnpm lint— 0 errors / 0 warningspnpm format— cleanpnpm build— cleanpnpm test— 536 passed, 2 pre-existing flakes unrelated to this change (element-context.spec.tsSVG fallback andtouch-mode.spec.tsre-activation)Summary by cubic
Adds a pre-screenshot overlay in
react-grabthat 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
Bug Fixes
Written for commit 19c8419. Summary will update on new commits. Review in cubic