Skip to content

feat: adopt transitions-dev animations for toolbar unread badge + sync label fade#314

Merged
aidenybai merged 6 commits intomainfrom
cursor/transitions-dev-adoption-7c48
Apr 28, 2026
Merged

feat: adopt transitions-dev animations for toolbar unread badge + sync label fade#314
aidenybai merged 6 commits intomainfrom
cursor/transitions-dev-adoption-7c48

Conversation

@aidenybai
Copy link
Copy Markdown
Owner

@aidenybai aidenybai commented Apr 28, 2026

Summary

Inspired by two of the nine recipes in Jakubantalik/transitions-devNotification badge (03) for slide-in, and Text states swap (04) in spirit for the digit transition — applied to the toolbar's unread comment count, plus a related fix that aligns the selection-label fade with the existing canvas pink-overlay fade so they finish together.

The implementation is all Tailwind v4 utilities in JSX. The only raw CSS is a single keyframe (rg-badge-slide) registered as --animate-rg-badge-slide in @theme so it becomes the animate-rg-badge-slide utility.

What changed

Notification badge slide-in — appears via data-[open=true]:animate-rg-badge-slide from a small offset.

Pill open/close pop — driven by group-data-[open=false]/badge: variants on the visible pill (scale 1 → 0, opacity 1 → 0, blur 0 → 2px). Open uses ease-badge-pop over 500ms; close uses ease-badge-close over 180ms.

Digit text-swap (in place) — the obvious naive port of recipe-02 (per-digit pop-in from translate(0, 8px)) doesn't suit a 10×10px status pill: the digit starts entirely below the visible pill, so subsequent updates flashed an empty black circle before the new number arrived. Replaced with the recipe-04 pattern using two persistent text slots stacked in the same grid cell:

  • Both slots share [grid-area:1/1] so they paint over each other, both centered.
  • Active slot: translate-y-0 opacity-100 blur-0.
  • Inactive slot: ±translate-y-2 opacity-0 blur-[2px].
  • On value change, the new value is written into whichever slot is currently inactive, then data-state flips. The new digit slides in while the old one slides out — the pill is never empty.

All toggling is group-data-[state=a]/badge: / group-data-[state=b]/badge: variants — no per-slot signals, no class rip-and-reapply replay.

Bonus fix: label fade timing

The <SelectionLabel> outer container animated opacity, filter on a hard-coded duration-150 Tailwind transition, but CompletionView calls onDismiss at FADE_DURATION_MS (100ms) after the fade trigger and the OverlayCanvas opacity-lerps the label-tagged pink box to converge in roughly the same window. The 50ms tail of the 150ms transition was being ripped away when Solid unmounted the label, so the pink box visibly disappeared before the label finished. Bound the label container's transition to FADE_DURATION_MS so both fades run on the same timeline.

Implementation notes

  • All animation surface is Tailwind utilities. New @theme tokens: --ease-badge-pop, --ease-badge-close, --ease-text-swap, --animate-rg-badge-slide. The single keyframe @keyframes rg-badge-slide lives in styles.css.
  • prefers-reduced-motion: reduce keys on [data-react-grab-unread-indicator] (and descendants) so the entire badge tree freezes without enumerating internal classes.
  • One new component: UnreadCountBadge (components/unread-count-badge.tsx).

Iteration history (in this PR)

  • ❌ Recipe 09 (Icon swap) on the comments dropdown's "Copy" → check feedback — reverted on review.
  • ❌ Recipe 02 (Number pop-in) per-digit replay — replaced with the in-place text-swap above; on a 10px pill the recipe's start-from-translate-y-8 made the digit invisible for ~150ms of every replay.
  • ❌ Hand-written .rg-t-* CSS classes with state-paired selectors — replaced with Tailwind group-data-* variants and @theme tokens.

Deliberately out of scope

  • Menu dropdown (05)createAnchoredDropdown in utils/ already implements a richer variant with viewport flip, anchor-edge selection, and safe-polygon hover bridges. Replacing it would be a regression.
  • Card resize (01) — the toolbar collapse uses grid-template-columns: 0fr ↔ 1fr plus opacity, which is more capable than recipe 01's plain width/height tween.
  • Modal (06), Panel reveal (07), Page side-by-side (08) — no current consumer in the codebase.
  • Text states swap (04) on completion-view.tsx — the status-text site renders structurally different parents in each branch (with vs without check icon and dismiss button); a clean two-span stacked swap would need restructuring that view. Skipped.

Validation

  • pnpm build
  • pnpm typecheck
  • pnpm lint

Files

packages/react-grab/src/components/unread-count-badge.tsx    new
packages/react-grab/src/components/toolbar/index.tsx         modified (UnreadCountBadge wiring)
packages/react-grab/src/components/selection-label/index.tsx modified (fade duration aligned with FADE_DURATION_MS)
packages/react-grab/src/styles.css                           modified (3 ease tokens + 1 keyframe)
Open in Web Open in Cursor 

Summary by cubic

Adds transitions-dev-style animations to the toolbar unread badge and syncs the selection-label fade with the canvas. Uses Tailwind utilities inside the react-grab shadow root and honors reduced motion.

  • New Features

    • Toolbar unread badge: slide-in on show; stacked-slot slide/fade swap on value change (no empty flash).
  • Bug Fixes

    • Restored unread badge pill centering and sizing.
    • Selection label fade now uses FADE_DURATION_MS to finish with the canvas.

Written for commit 9cab5bc. Summary will update on new commits. Review in cubic

…dback

Adds three transitions-dev recipes scoped to the react-grab shadow root
(:host), prefixed with rg-t- to avoid host-page collisions:

- Notification badge slide-in (recipe 03) on the unread comment count
- Number pop-in with trailing-digit stagger (recipe 02) replays as the
  count changes
- Icon-swap cross-fade (recipe 09) on the comments dropdown
  Copy/Confirmed swap

Introduces two small Solid components:
- IconSwap (stacked-grid two-icon cross-fade)
- UnreadCountBadge (combines the badge and digit pop-in, replays the
  digit animation on value change via remove-class -> reflow ->
  re-add)

All transitions honor prefers-reduced-motion: reduce. Existing
animation systems (anchored dropdown, toolbar collapse) are untouched
because they already implement equivalent or richer behavior.

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

vercel Bot commented Apr 28, 2026

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

Project Deployment Actions Updated (UTC)
react-grab-storybook Ready Ready Preview, Comment Apr 28, 2026 9:27am
react-grab-website Ready Ready Preview, Comment Apr 28, 2026 9:27am

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 28, 2026

Open in StackBlitz

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

commit: 9cab5bc

@aidenybai aidenybai marked this pull request as ready for review April 28, 2026 01:02
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 127a70c. Configure here.

Comment thread packages/react-grab/src/components/unread-count-badge.tsx Outdated
Copy link
Copy Markdown
Contributor

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

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

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/unread-count-badge.tsx">

<violation number="1" location="packages/react-grab/src/components/unread-count-badge.tsx:19">
P2: For single-digit badge values (1–9), the only character hits `characterIndex === lastIndex` and receives `staggerIndex: 2`, adding a 140ms `animation-delay`. Because the `rg-t-digit-pop-in` animation starts at `opacity: 0` with `fill-mode: both`, the digit is invisible for that entire delay — the badge dot slides in but shows no number. The same empty-flash problem affects two-digit values where both characters get stagger delays. Add an early return for the first character so at least one digit always appears immediately.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/react-grab/src/components/unread-count-badge.tsx Outdated
cursoragent and others added 3 commits April 28, 2026 01:11
Per feedback: revert the IconSwap cross-fade on the comments dropdown
Copy/Confirmed button. Restores the prior <Show> swap and removes the
IconSwap component, plus the rg-t-icon-swap CSS and tokens.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The previous structure put rg-t-badge-dot, rg-t-digit-group, and
Tailwind's flex centering on the same span. .rg-t-badge-dot's display:
block in styles.css clobbered display: flex from Tailwind, and
.rg-t-digit-group's display: inline-flex; align-items: baseline raced
against the centering classes. Result was a chunky, mis-centered black
pill overlapping the comment icon.

Split into three nodes: outer .rg-t-badge (absolute wrapper, slide
animation), middle .rg-t-badge-dot (the visible pill, owns flex
centering and Tailwind layout classes), inner .rg-t-digit-group
(inline-flex baseline row of per-digit spans, owns the .is-animating
replay).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The completion view dismissed at FEEDBACK_DURATION_MS - FADE_DURATION_MS
+ FADE_DURATION_MS, while the outer label container animated opacity
and filter on a hard-coded 150ms transition. The pink overlay-canvas
selection box converged in ~80-90ms and the label was unmounted at
100ms after fade start, ripping away the last 50ms of the label's
transition. Visible result: the pink box disappears noticeably before
the label finishes fading.

Bind the label container's opacity/filter transition to FADE_DURATION_MS
so the HTML fade and the canvas fade complete on the same timeline.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot changed the title feat: adopt transitions-dev animations for toolbar badge and copy feedback feat: adopt transitions-dev animations for toolbar unread badge + sync label fade Apr 28, 2026
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 5 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/comments-dropdown.tsx">

<violation number="1">
P3: Keep the copy feedback in a fixed swap envelope; this `Show` replacement drops the intended cross-fade and causes a width/layout jump when the state changes.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…ailwind utilities

Recipe 02 (number pop-in) doesn't suit a 10x10 status pill: each digit
re-enters from translate(0, 8px) with opacity 0, so on a pill that
small the digit was completely outside the visible area for the first
~150ms of every replay. Result: subsequent updates flashed an empty
black circle before the new number arrived.

Switch to recipe-04 (text states swap) in spirit, using two persistent
text slots stacked in the same grid cell:

- The active slot is at translate-y-0 / opacity 100 / blur 0
- The inactive slot is at translate-y +/- 2 / opacity 0 / blur 2px
- On value change, write the new value into whichever slot is currently
  inactive and flip data-state. The new digit slides in while the old
  one slides out; the pill is never empty.

Drop all hand-written rg-t-* CSS classes and rewrite the badge in pure
Tailwind v4 utilities. The only raw CSS remaining is the rg-badge-slide
keyframe registered as --animate-rg-badge-slide in @theme so it becomes
animate-rg-badge-slide. The data-driven open/close transitions and the
slot text-swap are expressed entirely with group-data-[...]/badge
variants. New @theme tokens: --ease-badge-pop, --ease-badge-close,
--ease-text-swap.

prefers-reduced-motion guard now keys on [data-react-grab-unread-indicator]
so we don't have to enumerate inner classes.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Comment thread .changeset/transitions-dev-adoption.md Outdated
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@aidenybai aidenybai merged commit b452d4f into main Apr 28, 2026
17 checks passed
@aidenybai aidenybai deleted the cursor/transitions-dev-adoption-7c48 branch April 29, 2026 03:30
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