feat(card): add interactive state action card#433
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughIntroduces a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR introduces new interactive UI components and their accompanying documentation/storybook entries, adding a State Action Card (stateful actions + feedback animations) and a Swipe Deck carousel (snap scrolling + parallax + navigation).
Changes:
- Added
StateActionCardcomponent with state-driven action buttons, status badges, and completion confetti feedback. - Added
SwipeDeckcarousel component with snap scrolling, parallax motion, arrows, and indicators. - Added Storybook stories and MDX docs pages for both components.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| content/docs/carousel/swipe-deck.mdx | Adds documentation page for the new Swipe Deck carousel. |
| content/docs/card/state-action-card.mdx | Adds documentation page for the new State Action Card. |
| animata/carousel/swipe-deck.tsx | Implements the Swipe Deck carousel component (scroll + parallax + navigation). |
| animata/carousel/swipe-deck.stories.tsx | Adds Storybook story for Swipe Deck. |
| animata/card/state-action-card.tsx | Implements the State Action Card component (actions + badges + feedback animations). |
| animata/card/state-action-card.stories.tsx | Adds Storybook stories for multiple State Action Card use cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| className={cn( | ||
| "flex snap-x snap-mandatory gap-4 overflow-x-auto px-[9%] pb-2 pt-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", | ||
| "touch-pan-x cursor-grab active:cursor-grabbing", | ||
| { | ||
| "select-none": isDragging, | ||
| }, | ||
| )} | ||
| style={{ touchAction: "auto" }} | ||
| onPointerDown={(event) => { |
There was a problem hiding this comment.
The touch-pan-x class is effectively overridden by the inline style={{ touchAction: "auto" }} (inline style wins), so the intent is unclear. Either remove the inline style or set it to the intended value (likely pan-x) and drop the redundant class so touch behavior is consistent and easier to reason about.
| "use client"; | ||
|
|
||
| import { ChevronLeft, ChevronRight } from "lucide-react"; | ||
| import { type HTMLAttributes, useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||
|
|
||
| import { cn } from "@/lib/utils"; | ||
|
|
||
| export interface SwipeDeckItem { |
There was a problem hiding this comment.
The PR title/description focus on adding the State Action Card, but this change set also introduces a new Swipe Deck component + docs/stories. Please update the PR description to include Swipe Deck (or split it into a separate PR) so reviewers can track scope and release notes accurately.
| const triggerActionFeedback = (action: ActionType) => { | ||
| setLastAction(action); | ||
| window.setTimeout(() => { | ||
| setLastAction((previous) => (previous === action ? null : previous)); | ||
| }, 800); | ||
| }; |
There was a problem hiding this comment.
triggerActionFeedback schedules a new timeout on every click without clearing the previous one. If the same action is clicked repeatedly within 800ms, the earlier timeout can clear lastAction sooner than intended (the feedback duration becomes shorter than 800ms from the most recent click). Store the timeout id in a ref and clear it before scheduling a new one so the feedback window always resets based on the latest action.
| : { label: "In Progress", className: "bg-amber-100 text-amber-700" }, | ||
| isFavorite | ||
| ? { label: "Favorited", className: "bg-rose-100 text-rose-700" } | ||
| : { label: "Not Favorite", className: "bg-zinc-100 text-zinc-600" }, |
There was a problem hiding this comment.
The status label "Not Favorite" is grammatically inconsistent with "Favorited" and reads awkwardly. Consider renaming it to "Not Favorited" (or similar) for clarity.
| : { label: "Not Favorite", className: "bg-zinc-100 text-zinc-600" }, | |
| : { label: "Not Favorited", className: "bg-zinc-100 text-zinc-600" }, |
| const dragState = useRef({ | ||
| pointerId: -1, | ||
| startX: 0, | ||
| startScrollLeft: 0, | ||
| moved: false, | ||
| }); |
There was a problem hiding this comment.
dragState.current.moved is written to (and initialized) but never read anywhere. This is dead state and makes the drag logic harder to follow; either remove it or use it (e.g., to differentiate click vs drag and avoid snapping on click).
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
animata/carousel/swipe-deck.tsx (4)
219-225:touch-pan-xclass is overridden by inlinetouchAction: "auto".The Tailwind class on line 220 sets
touch-action: pan-x, but the inline style on line 225 resets it toauto. The inline style wins, so thetouch-pan-xhint is dead code. Pick one: keeptouch-pan-x(recommended for a horizontal carousel — it cleanly lets the browser pass vertical pans to the page while handling horizontal panning here) and drop the inline style, or remove the class and leaveauto.♻️ Suggested cleanup
className={cn( "flex snap-x snap-mandatory gap-4 overflow-x-auto px-[9%] pb-2 pt-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", "touch-pan-x cursor-grab active:cursor-grabbing", { "select-none": isDragging, }, )} - style={{ touchAction: "auto" }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@animata/carousel/swipe-deck.tsx` around lines 219 - 225, The inline style style={{ touchAction: "auto" }} in the JSX overrides the Tailwind touch-pan-x utility, so remove the inline touchAction to allow touch-pan-x to work; locate the JSX element in swipe-deck.tsx that combines the class list (including "touch-pan-x") and delete the style prop (or at least remove the touchAction property) so the browser honors the Tailwind touch-action: pan-x behavior for the horizontal carousel.
244-260: Use captured pointerId as source of truth instead ofisDraggingstate.
onPointerMoveguards onisDragging, which is async React state set inonPointerDown. There's a small window where the first move events arrive before the re‑render commits the new state, dropping those samples. SincedragState.current.pointerIdis already set synchronously and you callsetPointerCapture, keying the guard off the ref/pointerId is both more accurate and avoids re‑binding the closure on every state flip.♻️ Suggested change
onPointerMove={(event) => { const container = deckRef.current; if (!container) { return; } - if (!isDragging || dragState.current.pointerId !== event.pointerId) { + if (dragState.current.pointerId !== event.pointerId) { return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@animata/carousel/swipe-deck.tsx` around lines 244 - 260, The onPointerMove handler should use the synchronous dragState.current.pointerId as the source of truth instead of the async React state isDragging: update the onPointerMove guard to remove the isDragging check and instead only proceed when dragState.current.pointerId === event.pointerId (and dragState.current.pointerId is non-null); this ensures early move events aren't dropped and avoids relying on a state re-render—refer to the onPointerMove handler and dragState current fields to make the change (also ensure pointer capture/release logic in onPointerDown/onPointerUp uses the same dragState.current.pointerId).
171-174: Parallax won't refresh when item contents change with same length.This effect only re‑fires on
items.lengthchange. If the caller swapsitemsfor an array of the same length (different content/order), the transforms on[data-parallax-layer]persist until the next scroll. Small glitch, easy fix by keying onitemsitself.♻️ Suggested change
useEffect(() => { cardRefs.current = cardRefs.current.slice(0, items.length); updateParallax(); - }, [items.length, updateParallax]); + }, [items, updateParallax]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@animata/carousel/swipe-deck.tsx` around lines 171 - 174, The effect that sets cardRefs and calls updateParallax currently only depends on items.length so it won’t re-run when the items array is replaced with same-length content; update the useEffect dependency array to include the items reference (e.g., items) in addition to updateParallax so the effect re-runs when the caller swaps the array content/order and refreshes the [data-parallax-layer] transforms; locate the useEffect that references cardRefs.current = cardRefs.current.slice(0, items.length) and updateParallax and add items to its dependency list.
261-280: Extract shared pointer-release logic.
onPointerUpandonPointerCancelare byte-identical. Lift into a single handler to keep them from drifting.♻️ Suggested refactor
+ const endDrag = (event: React.PointerEvent<HTMLDivElement>) => { + const container = deckRef.current; + if (!container || dragState.current.pointerId !== event.pointerId) { + return; + } + container.releasePointerCapture(event.pointerId); + dragState.current.pointerId = -1; + setIsDragging(false); + scrollToIndex(nearestIndex(container)); + }; - onPointerUp={(event) => { /* ... */ }} - onPointerCancel={(event) => { /* ... */ }} + onPointerUp={endDrag} + onPointerCancel={endDrag}Note: also resetting
dragState.current.pointerIdto-1on release prevents a stale id from matching a later straypointermove.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@animata/carousel/swipe-deck.tsx` around lines 261 - 280, Extract the duplicated logic in onPointerUp and onPointerCancel into a single handler (e.g., handlePointerRelease) that takes the PointerEvent, reads deckRef.current, verifies dragState.current.pointerId matches event.pointerId, calls container.releasePointerCapture(event.pointerId), calls setIsDragging(false), calls scrollToIndex(nearestIndex(container)), and resets dragState.current.pointerId to -1 to avoid stale pointer matches; then replace both onPointerUp and onPointerCancel with references to this new handler.content/docs/carousel/swipe-deck.mdx (1)
1-40: Consider adding a Props/Usage section.Frontmatter, preview name (
carousel-swipe-deck--primary), and install steps all align with the component and story. One nit: most sibling docs (per the repo convention) include a short Props table or usage snippet below the preview — worth addingshowArrows,showIndicators, andparallaxStrengthso consumers don't have to read the source. Non-blocking.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@content/docs/carousel/swipe-deck.mdx` around lines 1 - 40, Add a short "Props / Usage" section under the ComponentPreview that documents the component exported in swipe-deck.tsx (referenced by the story name carousel-swipe-deck--primary); list the key props showArrows, showIndicators, and parallaxStrength with a one-line description for each and a minimal usage snippet example showing how to pass them to the component so consumers can understand configuration without opening the source.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@animata/card/state-action-card.tsx`:
- Around line 255-271: The ActionButton toggle lacks a programmatic pressed
state for assistive tech; update the ActionButton component to set
aria-pressed={active} on the <button> so the button exposes its toggle state to
screen readers (keep the existing type="button" and className logic, and use the
existing active prop to drive aria-pressed).
- Around line 98-121: The timers in triggerActionFeedback, onFavorite, and
onComplete currently use window.setTimeout without tracking handles, causing
state updates after unmount and race conditions when actions are retriggered;
change triggerActionFeedback to store its timeout id in a ref (e.g.,
actionTimerRef) and clearTimeout on new triggers and in a useEffect cleanup, and
likewise store the confetti timeout id in a separate ref (e.g.,
confettiTimerRef) used by onComplete and cleared before starting a new timeout
and on unmount; ensure setLastAction and setShowConfetti are only called from
timeouts you still hold and that you clear the refs after clearing the timers.
In `@content/docs/card/state-action-card.mdx`:
- Line 6: The frontmatter contains published: false which prevents the docs page
and the ComponentPreview (line 9) from appearing; if the component is ready to
ship, change the frontmatter flag published: false to published: true in this
file so the page is published, otherwise add a note to the PR description
stating the docs are intentionally staged (or leave published: false but
document that decision).
---
Nitpick comments:
In `@animata/carousel/swipe-deck.tsx`:
- Around line 219-225: The inline style style={{ touchAction: "auto" }} in the
JSX overrides the Tailwind touch-pan-x utility, so remove the inline touchAction
to allow touch-pan-x to work; locate the JSX element in swipe-deck.tsx that
combines the class list (including "touch-pan-x") and delete the style prop (or
at least remove the touchAction property) so the browser honors the Tailwind
touch-action: pan-x behavior for the horizontal carousel.
- Around line 244-260: The onPointerMove handler should use the synchronous
dragState.current.pointerId as the source of truth instead of the async React
state isDragging: update the onPointerMove guard to remove the isDragging check
and instead only proceed when dragState.current.pointerId === event.pointerId
(and dragState.current.pointerId is non-null); this ensures early move events
aren't dropped and avoids relying on a state re-render—refer to the
onPointerMove handler and dragState current fields to make the change (also
ensure pointer capture/release logic in onPointerDown/onPointerUp uses the same
dragState.current.pointerId).
- Around line 171-174: The effect that sets cardRefs and calls updateParallax
currently only depends on items.length so it won’t re-run when the items array
is replaced with same-length content; update the useEffect dependency array to
include the items reference (e.g., items) in addition to updateParallax so the
effect re-runs when the caller swaps the array content/order and refreshes the
[data-parallax-layer] transforms; locate the useEffect that references
cardRefs.current = cardRefs.current.slice(0, items.length) and updateParallax
and add items to its dependency list.
- Around line 261-280: Extract the duplicated logic in onPointerUp and
onPointerCancel into a single handler (e.g., handlePointerRelease) that takes
the PointerEvent, reads deckRef.current, verifies dragState.current.pointerId
matches event.pointerId, calls container.releasePointerCapture(event.pointerId),
calls setIsDragging(false), calls scrollToIndex(nearestIndex(container)), and
resets dragState.current.pointerId to -1 to avoid stale pointer matches; then
replace both onPointerUp and onPointerCancel with references to this new
handler.
In `@content/docs/carousel/swipe-deck.mdx`:
- Around line 1-40: Add a short "Props / Usage" section under the
ComponentPreview that documents the component exported in swipe-deck.tsx
(referenced by the story name carousel-swipe-deck--primary); list the key props
showArrows, showIndicators, and parallaxStrength with a one-line description for
each and a minimal usage snippet example showing how to pass them to the
component so consumers can understand configuration without opening the source.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f99006c2-6189-4cbc-948d-eb2f3350f908
📒 Files selected for processing (6)
animata/card/state-action-card.stories.tsxanimata/card/state-action-card.tsxanimata/carousel/swipe-deck.stories.tsxanimata/carousel/swipe-deck.tsxcontent/docs/card/state-action-card.mdxcontent/docs/carousel/swipe-deck.mdx
| const triggerActionFeedback = (action: ActionType) => { | ||
| setLastAction(action); | ||
| window.setTimeout(() => { | ||
| setLastAction((previous) => (previous === action ? null : previous)); | ||
| }, 800); | ||
| }; | ||
|
|
||
| const onFavorite = () => { | ||
| setIsFavorite((previous) => !previous); | ||
| triggerActionFeedback("favorite"); | ||
| }; | ||
|
|
||
| const onComplete = () => { | ||
| const nextValue = !isCompleted; | ||
| setIsCompleted(nextValue); | ||
| triggerActionFeedback("complete"); | ||
|
|
||
| if (nextValue) { | ||
| setShowConfetti(true); | ||
| window.setTimeout(() => { | ||
| setShowConfetti(false); | ||
| }, 1000); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Pending timeouts are not cleared on unmount or re-click.
Two issues with the timers:
- If the component unmounts before 800ms / 1000ms elapse,
setLastAction/setShowConfettirun on an unmounted tree. - Clicking the same action twice in <800ms lets the first timer clear the badge prematurely (the
previous === actionguard matches the second set as well).
Track timer handles in refs and clear them on new triggers / unmount.
♻️ Proposed fix
-import { useMemo, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
@@
- const [showConfetti, setShowConfetti] = useState(false);
+ const [showConfetti, setShowConfetti] = useState(false);
+ const feedbackTimerRef = useRef<number | null>(null);
+ const confettiTimerRef = useRef<number | null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (feedbackTimerRef.current) window.clearTimeout(feedbackTimerRef.current);
+ if (confettiTimerRef.current) window.clearTimeout(confettiTimerRef.current);
+ };
+ }, []);
@@
const triggerActionFeedback = (action: ActionType) => {
setLastAction(action);
- window.setTimeout(() => {
- setLastAction((previous) => (previous === action ? null : previous));
- }, 800);
+ if (feedbackTimerRef.current) window.clearTimeout(feedbackTimerRef.current);
+ feedbackTimerRef.current = window.setTimeout(() => {
+ setLastAction(null);
+ }, 800);
};
@@
if (nextValue) {
setShowConfetti(true);
- window.setTimeout(() => {
+ if (confettiTimerRef.current) window.clearTimeout(confettiTimerRef.current);
+ confettiTimerRef.current = window.setTimeout(() => {
setShowConfetti(false);
}, 1000);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@animata/card/state-action-card.tsx` around lines 98 - 121, The timers in
triggerActionFeedback, onFavorite, and onComplete currently use
window.setTimeout without tracking handles, causing state updates after unmount
and race conditions when actions are retriggered; change triggerActionFeedback
to store its timeout id in a ref (e.g., actionTimerRef) and clearTimeout on new
triggers and in a useEffect cleanup, and likewise store the confetti timeout id
in a separate ref (e.g., confettiTimerRef) used by onComplete and cleared before
starting a new timeout and on unmount; ensure setLastAction and setShowConfetti
are only called from timeouts you still hold and that you clear the refs after
clearing the timers.
| function ActionButton({ icon: Icon, label, active, onClick }: Readonly<ActionButtonProps>) { | ||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={onClick} | ||
| className={cn( | ||
| "inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition", | ||
| active | ||
| ? "border-emerald-200 bg-emerald-50 text-emerald-700" | ||
| : "border-zinc-200 bg-white text-zinc-600 hover:border-zinc-300 hover:bg-zinc-50", | ||
| )} | ||
| > | ||
| <Icon className="size-3.5" /> | ||
| {label} | ||
| </button> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Add aria-pressed to toggle buttons for accessibility.
The favorite/complete/share buttons toggle UI state but expose no programmatic pressed-state to assistive tech. Screen readers only announce the label swap, not that the control is a toggle.
♿ Proposed fix
function ActionButton({ icon: Icon, label, active, onClick }: Readonly<ActionButtonProps>) {
return (
<button
type="button"
onClick={onClick}
+ aria-pressed={active}
className={cn(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function ActionButton({ icon: Icon, label, active, onClick }: Readonly<ActionButtonProps>) { | |
| return ( | |
| <button | |
| type="button" | |
| onClick={onClick} | |
| className={cn( | |
| "inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition", | |
| active | |
| ? "border-emerald-200 bg-emerald-50 text-emerald-700" | |
| : "border-zinc-200 bg-white text-zinc-600 hover:border-zinc-300 hover:bg-zinc-50", | |
| )} | |
| > | |
| <Icon className="size-3.5" /> | |
| {label} | |
| </button> | |
| ); | |
| } | |
| function ActionButton({ icon: Icon, label, active, onClick }: Readonly<ActionButtonProps>) { | |
| return ( | |
| <button | |
| type="button" | |
| onClick={onClick} | |
| aria-pressed={active} | |
| className={cn( | |
| "inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition", | |
| active | |
| ? "border-emerald-200 bg-emerald-50 text-emerald-700" | |
| : "border-zinc-200 bg-white text-zinc-600 hover:border-zinc-300 hover:bg-zinc-50", | |
| )} | |
| > | |
| <Icon className="size-3.5" /> | |
| {label} | |
| </button> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@animata/card/state-action-card.tsx` around lines 255 - 271, The ActionButton
toggle lacks a programmatic pressed state for assistive tech; update the
ActionButton component to set aria-pressed={active} on the <button> so the
button exposes its toggle state to screen readers (keep the existing
type="button" and className logic, and use the existing active prop to drive
aria-pressed).
| description: Cards with state-based action buttons, status badges, hover-reveal actions, and success feedback animations. | ||
| labels: ["requires interaction", "hover", "state", "actions"] | ||
| author: ujjwalbasnet | ||
| published: false |
There was a problem hiding this comment.
Is published: false intentional?
With this flag the docs page (and the ComponentPreview on line 9) will not appear on the site after merge. If the component is ready to ship alongside this PR, flip it to true; otherwise consider clarifying in the PR description that the docs are intentionally staged.
✏️ Proposed change
-published: false
+published: true📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| published: false | |
| published: true |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@content/docs/card/state-action-card.mdx` at line 6, The frontmatter contains
published: false which prevents the docs page and the ComponentPreview (line 9)
from appearing; if the component is ready to ship, change the frontmatter flag
published: false to published: true in this file so the page is published,
otherwise add a note to the PR description stating the docs are intentionally
staged (or leave published: false but document that decision).
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
.github/workflows/deploy-v3.yml (2)
12-13:FORCE_JAVASCRIPT_ACTIONS_TO_NODE24has no consumer in this workflow.Every remaining action in this job (
actions/checkout@v6,actions/setup-node@v6,actions/cache@v5,cloudflare/wrangler-action@v3) is already on a Node 24–native (or non-JS) runtime, so this environment variable is effectively dead code here. It's harmless, but either remove it or add a comment explaining why it's retained so a later reader doesn't cargo-cult it.Proposed removal
environment: v3-deployment - env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/deploy-v3.yml around lines 12 - 13, The environment variable FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 is unused in this workflow (all actions are Node 24–native or non-JS), so either remove that env entry from the workflow or keep it but add a short inline comment next to FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 explaining why it is intentionally retained (e.g., compatibility guard or historical reason) so future readers won't cargo-cult it; reference the env key FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 and the job that sets env (the job using actions/checkout@v6, actions/setup-node@v6, actions/cache@v5, cloudflare/wrangler-action@v3) when making the change.
18-20: Node 20 vs Node 22 drift between the two deploy workflows.
deploy.ymlpinsnode-version: "22"(line 24) whiledeploy-v3.ymlpins"20"(line 20). Since both build the same Next.js app and run the sameyarn build/yarn storybook:build, this divergence can hide Node-version–specific build/test breakage (deploy-v3passing does not guaranteedeploywill). The repository's.node-versionfile specifies "22", aligning with the main workflow. If the v3 branch intentionally stays on Node 20 LTS for compatibility, add a comment documenting that decision; otherwise, consider aligning to Node 22 for consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/deploy-v3.yml around lines 18 - 20, The workflow deploy-v3.yml currently sets actions/setup-node@v6 with node-version: "20", which diverges from deploy.yml (node-version: "22") and the repository .node-version ("22"); update the node-version key in deploy-v3.yml from "20" to "22" so both deploy workflows use Node 22 (or, if the v3 workflow must stay on Node 20 for a documented compatibility reason, add a clear comment above the uses: actions/setup-node@v6 line explaining the intentional choice and update repository docs/.node-version to call out the exception).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/deploy.yml:
- Around line 14-15: This change mixes an unrelated CI runtime migration into a
feature PR and uses an unquoted boolean env value; separate the CI change into
its own commit/PR targeting only the workflow files (remove the
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 change from this feature branch), and in the
workflow files that remain in the CI PR (e.g., deploy.yml and deploy-v3.yml)
make the environment value explicit by quoting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"; ensure the feature PR contains only
the interactive state action card code and the CI PR contains only the Node 24
workflow updates so they can be reviewed and reverted independently.
- Around line 18-28: Upgrade the actions/github-script usage from v6 to v9 to
align it with Node.js 24 (so it no longer requires the
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 compatibility flag); locate the step
referencing actions/github-script (currently `@v6`) and change the version to `@v9`,
then verify workflow steps using actions/checkout@v6, actions/setup-node@v6 and
actions/cache@v5 still function and plan to remove the
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 flag in a follow-up once confirmed; ensure
any self-hosted runner requirements for actions/cache@v5 (runner >= 2.327.1) are
documented if applicable.
---
Nitpick comments:
In @.github/workflows/deploy-v3.yml:
- Around line 12-13: The environment variable FORCE_JAVASCRIPT_ACTIONS_TO_NODE24
is unused in this workflow (all actions are Node 24–native or non-JS), so either
remove that env entry from the workflow or keep it but add a short inline
comment next to FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 explaining why it is
intentionally retained (e.g., compatibility guard or historical reason) so
future readers won't cargo-cult it; reference the env key
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 and the job that sets env (the job using
actions/checkout@v6, actions/setup-node@v6, actions/cache@v5,
cloudflare/wrangler-action@v3) when making the change.
- Around line 18-20: The workflow deploy-v3.yml currently sets
actions/setup-node@v6 with node-version: "20", which diverges from deploy.yml
(node-version: "22") and the repository .node-version ("22"); update the
node-version key in deploy-v3.yml from "20" to "22" so both deploy workflows use
Node 22 (or, if the v3 workflow must stay on Node 20 for a documented
compatibility reason, add a clear comment above the uses: actions/setup-node@v6
line explaining the intentional choice and update repository docs/.node-version
to call out the exception).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5d657e47-acae-440e-a79d-30d490a544bd
📒 Files selected for processing (2)
.github/workflows/deploy-v3.yml.github/workflows/deploy.yml
| env: | ||
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true |
There was a problem hiding this comment.
PR scope: CI infrastructure change bundled into a feature PR.
The PR title/objective is "feat(card): add interactive state action card", but this file (and deploy-v3.yml) contains an unrelated Node 24 runtime migration. Consider splitting the chore(ci): upgrade actions for Node 24 runtime commit into its own PR so the card feature and the runner upgrade can be reviewed, reverted, and deployed independently.
Also, minor: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true is an unquoted boolean. GitHub will coerce it to the string "true", but being explicit avoids surprises:
Nit diff
env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/deploy.yml around lines 14 - 15, This change mixes an
unrelated CI runtime migration into a feature PR and uses an unquoted boolean
env value; separate the CI change into its own commit/PR targeting only the
workflow files (remove the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 change from this
feature branch), and in the workflow files that remain in the CI PR (e.g.,
deploy.yml and deploy-v3.yml) make the environment value explicit by quoting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"; ensure the feature PR contains only
the interactive state action card code and the CI PR contains only the Node 24
workflow updates so they can be reviewed and reverted independently.
| uses: actions/checkout@v6 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.sha }} | ||
| - name: Setup Node | ||
| uses: actions/setup-node@v4 | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "22" | ||
| - name: Install yarn | ||
| run: npm install -g yarn | ||
| - name: Restore cache | ||
| uses: actions/cache@v4 | ||
| uses: actions/cache@v5 |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What is the latest major version of actions/github-script and which Node runtime does it use?
💡 Result:
The latest major version of actions/github-script is v9.0.0. It uses Node.js 24 runtime.
Citations:
- 1: https://github.com/actions/github-script/releases/tag/v9.0.0
- 2: https://github.com/actions/github-script/blob/main/action.yml
- 3: https://github.com/actions/github-script/releases
- 4: https://github.com/actions/github-script/releases/tag/v8
🌐 Web query:
actions/github-script v8 Node.js runtime version
💡 Result:
The actions/github-script v8 action uses Node.js 24.x as its runtime.
Citations:
- 1: https://github.com/actions/github-script/releases/tag/v8
- 2: https://github.com/actions/github-script
- 3: actions/github-script@adc0eea
- 4: actions/github-script@v7...v8
- 5: https://github.com/marketplace/actions/github-script
Upgrade actions/github-script to v9 for Node.js 24 consistency — otherwise it is the only reason FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 is still needed here.
actions/checkout@v6, actions/setup-node@v6, and actions/cache@v5 all run on the Node.js 24 runtime natively, so for those three steps the compatibility flag is a no-op. However, line 57 still references actions/github-script@v6, which ships on an older Node runtime and is the actual consumer of FORCE_JAVASCRIPT_ACTIONS_TO_NODE24. Since the latest version (v9) uses Node.js 24, upgrade to actions/github-script@v9 to complete the Node 24 migration and allow removal of the forcing flag in a follow-up once GitHub fully retires the older runtime.
Proposed change
- name: Comment Deployment URL
- uses: actions/github-script@v6
+ uses: actions/github-script@v9
with:Also note: actions/cache@v5 requires a minimum Actions Runner version of 2.327.1. This is fine for GitHub-hosted runners, but worth calling out if any self-hosted runners are in use for this environment.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/deploy.yml around lines 18 - 28, Upgrade the
actions/github-script usage from v6 to v9 to align it with Node.js 24 (so it no
longer requires the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 compatibility flag);
locate the step referencing actions/github-script (currently `@v6`) and change the
version to `@v9`, then verify workflow steps using actions/checkout@v6,
actions/setup-node@v6 and actions/cache@v5 still function and plan to remove the
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 flag in a follow-up once confirmed; ensure
any self-hosted runner requirements for actions/cache@v5 (runner >= 2.327.1) are
documented if applicable.
4015e78 to
deba5ea
Compare
deba5ea to
fcdad83
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (2)
animata/card/state-action-card.tsx (2)
255-271:⚠️ Potential issue | 🟠 MajorToggle buttons still missing
aria-pressed.Previously flagged and not addressed: favorite/complete/share are toggle controls but don't expose pressed state to assistive tech. Add
aria-pressed={active}to the<button>.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@animata/card/state-action-card.tsx` around lines 255 - 271, ActionButton renders toggle-like buttons but doesn't expose their pressed state; update the <button> in the ActionButton function to include aria-pressed using the active prop (aria-pressed={active}) so assistive tech can report the toggle state; ensure you add this attribute on the same button that currently uses onClick and className inside ActionButton.
98-121:⚠️ Potential issue | 🟡 MinorTimers still leak on unmount and race on rapid re-clicks.
The previously flagged issue remains unaddressed in this revision —
window.setTimeouthandles are not stored or cleared, so state updates can fire on an unmounted tree and a second click within 800 ms is still cleared early by the first timer'sprevious === actionguard (which matches because the action is the same). Track handles in refs and clear them on new triggers and in auseEffectcleanup.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@animata/card/state-action-card.tsx` around lines 98 - 121, The timers created in triggerActionFeedback and onComplete leak and can race; modify triggerActionFeedback, onFavorite and onComplete to store timeout IDs in refs (e.g., actionTimeoutRef and confettiTimeoutRef), clear any existing timeout before creating a new one so a new click cancels prior timers, and on component unmount (useEffect cleanup) clear both refs to avoid state updates on unmounted components; keep using setLastAction, setShowConfetti and setIsCompleted but ensure timeouts are managed via the refs and cleared appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@animata/card/state-action-card.tsx`:
- Around line 255-271: ActionButton renders toggle-like buttons but doesn't
expose their pressed state; update the <button> in the ActionButton function to
include aria-pressed using the active prop (aria-pressed={active}) so assistive
tech can report the toggle state; ensure you add this attribute on the same
button that currently uses onClick and className inside ActionButton.
- Around line 98-121: The timers created in triggerActionFeedback and onComplete
leak and can race; modify triggerActionFeedback, onFavorite and onComplete to
store timeout IDs in refs (e.g., actionTimeoutRef and confettiTimeoutRef), clear
any existing timeout before creating a new one so a new click cancels prior
timers, and on component unmount (useEffect cleanup) clear both refs to avoid
state updates on unmounted components; keep using setLastAction, setShowConfetti
and setIsCompleted but ensure timeouts are managed via the refs and cleared
appropriately.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 09960af4-f2b4-4e20-bb9c-83ac9c0cc46d
📒 Files selected for processing (5)
.github/workflows/deploy-v3.yml.github/workflows/deploy.ymlanimata/card/state-action-card.stories.tsxanimata/card/state-action-card.tsxcontent/docs/card/state-action-card.mdx
✅ Files skipped from review due to trivial changes (2)
- content/docs/card/state-action-card.mdx
- animata/card/state-action-card.stories.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- .github/workflows/deploy.yml
- .github/workflows/deploy-v3.yml
|
🚀 Preview deployed to: https://9d2e8887.animata.pages.dev |
|
🚀 Preview deployed to: https://301008be.animata.pages.dev |
Summary
This PR introduces a new interactive card component for state-driven actions in UI workflows.
What’s Included
StateActionCardwith contextual actions:Add to favoritesMark completeShareTesting
yarn biome check animata/card/state-action-card.tsx animata/card/state-action-card.stories.tsxNotes
Summary by CodeRabbit
Release Notes
New Features
Documentation