Skip to content

feat(card): add interactive state action card#433

Open
leazi99 wants to merge 1 commit into
codse:mainfrom
leazi99:feat/state-action-card
Open

feat(card): add interactive state action card#433
leazi99 wants to merge 1 commit into
codse:mainfrom
leazi99:feat/state-action-card

Conversation

@leazi99
Copy link
Copy Markdown

@leazi99 leazi99 commented Apr 24, 2026

Summary

This PR introduces a new interactive card component for state-driven actions in UI workflows.

What’s Included

  • Added StateActionCard with contextual actions:
  • Add to favorites
  • Mark complete
  • Share
  • Added dynamic status badges that update with action state.
  • Added hover-reveal action controls (mobile-friendly fallback included).
  • Added success feedback animations:
  • check-style action confirmation
  • confetti burst on completion
  • Added Storybook stories for key use cases:
  • Task manager cards
  • Social cards
  • Order dashboard cards
  • Added docs page for installation and usage.

Testing

  • yarn biome check animata/card/state-action-card.tsx animata/card/state-action-card.stories.tsx

Notes

  • Preview/story wiring was aligned with the repo’s custom docs story-id loader.

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced State Action Card component with interactive action buttons (favorite, complete, share), dynamic status badges, and animated feedback including action confirmation and celebratory animations. Supports task management, social features, and order tracking use cases.
  • Documentation

    • Added comprehensive documentation with installation instructions and usage examples for State Action Card component.

Copilot AI review requested due to automatic review settings April 24, 2026 10:26
@leazi99 leazi99 had a problem deploying to preview-deployment April 24, 2026 10:26 — with GitHub Actions Failure
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85368006-e3e5-464e-96f0-cc64a05ac8cc

📥 Commits

Reviewing files that changed from the base of the PR and between deba5ea and fcdad83.

📒 Files selected for processing (3)
  • animata/card/state-action-card.stories.tsx
  • animata/card/state-action-card.tsx
  • content/docs/card/state-action-card.mdx
✅ Files skipped from review due to trivial changes (1)
  • content/docs/card/state-action-card.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
  • animata/card/state-action-card.stories.tsx

📝 Walkthrough

Walkthrough

Introduces a new StateActionCard component with animated state-based action buttons, status badges, and success feedback animations. Includes Storybook stories demonstrating three use cases (task, social, order) and documentation describing the component's functionality and installation steps.

Changes

Cohort / File(s) Summary
StateActionCard Component
animata/card/state-action-card.tsx, animata/card/state-action-card.stories.tsx
New React component featuring animated cards with interactive state-based action buttons (favorite, complete, share), dynamic status pills, success feedback animations, and confetti effects triggered on completion. Includes three Storybook stories showcasing task manager, social card, and order card implementations.
Documentation
content/docs/card/state-action-card.mdx
New MDX documentation page describing the StateActionCard component, including feature overview, installation instructions, use cases, and author credits.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hop along, action buttons bright,
State-tracked cards dance through the night,
Confetti swirls when tasks complete,
Animated feedback, oh so sweet!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: adding a new interactive StateActionCard component to the card component library.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 StateActionCard component with state-driven action buttons, status badges, and completion confetti feedback.
  • Added SwipeDeck carousel 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.

Comment thread animata/carousel/swipe-deck.tsx Outdated
Comment on lines +218 to +226
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) => {
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread animata/carousel/swipe-deck.tsx Outdated
Comment on lines +1 to +8
"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 {
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +103
const triggerActionFeedback = (action: ActionType) => {
setLastAction(action);
window.setTimeout(() => {
setLastAction((previous) => (previous === action ? null : previous));
}, 800);
};
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
: { 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" },
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The status label "Not Favorite" is grammatically inconsistent with "Favorited" and reads awkwardly. Consider renaming it to "Not Favorited" (or similar) for clarity.

Suggested change
: { label: "Not Favorite", className: "bg-zinc-100 text-zinc-600" },
: { label: "Not Favorited", className: "bg-zinc-100 text-zinc-600" },

Copilot uses AI. Check for mistakes.
Comment thread animata/carousel/swipe-deck.tsx Outdated
Comment on lines +62 to +67
const dragState = useRef({
pointerId: -1,
startX: 0,
startScrollLeft: 0,
moved: false,
});
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
animata/carousel/swipe-deck.tsx (4)

219-225: touch-pan-x class is overridden by inline touchAction: "auto".

The Tailwind class on line 220 sets touch-action: pan-x, but the inline style on line 225 resets it to auto. The inline style wins, so the touch-pan-x hint is dead code. Pick one: keep touch-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 leave auto.

♻️ 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 of isDragging state.

onPointerMove guards on isDragging, which is async React state set in onPointerDown. There's a small window where the first move events arrive before the re‑render commits the new state, dropping those samples. Since dragState.current.pointerId is already set synchronously and you call setPointerCapture, 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.length change. If the caller swaps items for 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 on items itself.

♻️ 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.

onPointerUp and onPointerCancel are 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.pointerId to -1 on release prevents a stale id from matching a later stray pointermove.

🤖 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 adding showArrows, showIndicators, and parallaxStrength so 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

📥 Commits

Reviewing files that changed from the base of the PR and between b35730d and 8def0ee.

📒 Files selected for processing (6)
  • animata/card/state-action-card.stories.tsx
  • animata/card/state-action-card.tsx
  • animata/carousel/swipe-deck.stories.tsx
  • animata/carousel/swipe-deck.tsx
  • content/docs/card/state-action-card.mdx
  • content/docs/carousel/swipe-deck.mdx

Comment on lines +98 to +121
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);
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pending timeouts are not cleared on unmount or re-click.

Two issues with the timers:

  1. If the component unmounts before 800ms / 1000ms elapse, setLastAction / setShowConfetti run on an unmounted tree.
  2. Clicking the same action twice in <800ms lets the first timer clear the badge prematurely (the previous === action guard 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.

Comment on lines +255 to +271
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>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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).

@leazi99 leazi99 changed the title feat(card): add state action card with contextual actions feat(card): add interactive state action card Apr 24, 2026
@leazi99 leazi99 requested a deployment to preview-deployment April 24, 2026 10:51 — with GitHub Actions Waiting
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
.github/workflows/deploy-v3.yml (2)

12-13: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 has 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.yml pins node-version: "22" (line 24) while deploy-v3.yml pins "20" (line 20). Since both build the same Next.js app and run the same yarn build/yarn storybook:build, this divergence can hide Node-version–specific build/test breakage (deploy-v3 passing does not guarantee deploy will). The repository's .node-version file 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8def0ee and 4015e78.

📒 Files selected for processing (2)
  • .github/workflows/deploy-v3.yml
  • .github/workflows/deploy.yml

Comment thread .github/workflows/deploy.yml Outdated
Comment on lines +14 to +15
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment thread .github/workflows/deploy.yml Outdated
Comment on lines +18 to +28
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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:


🌐 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:


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.

@leazi99 leazi99 force-pushed the feat/state-action-card branch from 4015e78 to deba5ea Compare April 24, 2026 10:55
@leazi99 leazi99 temporarily deployed to preview-deployment April 24, 2026 10:55 — with GitHub Actions Inactive
@leazi99 leazi99 force-pushed the feat/state-action-card branch from deba5ea to fcdad83 Compare April 24, 2026 10:57
@leazi99 leazi99 temporarily deployed to preview-deployment April 24, 2026 10:57 — with GitHub Actions Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
animata/card/state-action-card.tsx (2)

255-271: ⚠️ Potential issue | 🟠 Major

Toggle 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 | 🟡 Minor

Timers still leak on unmount and race on rapid re-clicks.

The previously flagged issue remains unaddressed in this revision — window.setTimeout handles 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's previous === action guard (which matches because the action is the same). Track handles in refs and clear them on new triggers and in a useEffect cleanup.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4015e78 and deba5ea.

📒 Files selected for processing (5)
  • .github/workflows/deploy-v3.yml
  • .github/workflows/deploy.yml
  • animata/card/state-action-card.stories.tsx
  • animata/card/state-action-card.tsx
  • content/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

@github-actions
Copy link
Copy Markdown

🚀 Preview deployed to: https://9d2e8887.animata.pages.dev

@github-actions
Copy link
Copy Markdown

🚀 Preview deployed to: https://301008be.animata.pages.dev

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