Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions animata/card/flip-card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,54 @@ type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
image:
" https://images.unsplash.com/photo-1525373698358-041e3a460346?w=600&auto=format&fit=crop&q=60&ixlib=rb-4.0.3",
title: "Programming",
subtitle: "What is programming?",
description:
"Computer programming or coding is the composition of sequences of instructions, called programs, that computers can follow to perform tasks.",
rotate: "y",
},
render: ({ rotate }) => (
<FlipCard rotate={rotate}>
<FlipCard.Front>
<img
src="https://images.unsplash.com/photo-1525373698358-041e3a460346?w=600&auto=format&fit=crop&q=60&ixlib=rb-4.0.3"
alt="Programming"
className="h-full w-full rounded-2xl object-cover shadow-2xl shadow-black/40"
/>
<div className="absolute bottom-4 left-4 text-xl font-bold text-white">Programming</div>
</FlipCard.Front>
<FlipCard.Back className="rounded-2xl bg-black/80 p-4 text-slate-200">
<div className="flex min-h-full flex-col gap-2">
<h1 className="text-base font-bold text-white">What is programming?</h1>
<p className="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100">
Computer programming or coding is the composition of sequences of instructions, called
programs, that computers can follow to perform tasks.
</p>
</div>
</FlipCard.Back>
</FlipCard>
),
};

export const Secondary: Story = {
args: {
image:
"https://images.unsplash.com/photo-1717966313670-a42f6908be92?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3",
title: "Bibek Bhattarai",
subtitle: "Software Engineer",
description:
"I am a full-stack developer with a passion for building beautiful and functional applications.",
rotate: "x",
},
render: ({ rotate }) => (
<FlipCard rotate={rotate}>
<FlipCard.Front>
<img
src="https://images.unsplash.com/photo-1717966313670-a42f6908be92?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Bibek Bhattarai"
className="h-full w-full rounded-2xl object-cover shadow-2xl shadow-black/40"
/>
<div className="absolute bottom-4 left-4 text-xl font-bold text-white">Bibek Bhattarai</div>
</FlipCard.Front>
<FlipCard.Back className="rounded-2xl bg-black/80 p-4 text-slate-200">
<div className="flex min-h-full flex-col gap-2">
<h1 className="text-base font-bold text-white">Software Engineer</h1>
<p className="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100">
I am a full-stack developer with a passion for building beautiful and functional
applications.
</p>
</div>
</FlipCard.Back>
</FlipCard>
),
};
114 changes: 69 additions & 45 deletions animata/card/flip-card.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,83 @@
import { type ComponentProps, createContext, use, useMemo } from "react";

import { cn } from "@/lib/utils";

interface FlipCardProps extends React.HTMLAttributes<HTMLDivElement> {
image: string;
title: string;
description: string;
subtitle?: string;
rotate?: "x" | "y";
export type FlipCardRotate = "x" | "y";

type FlipCardContextValue = {
rotate: FlipCardRotate;
};

const FlipCardContext = createContext<FlipCardContextValue | null>(null);

const ROTATION_CLASS = {
x: {
hover: "group-hover/card:rotate-x-180 motion-reduce:group-hover/card:rotate-x-0",
back: "rotate-x-180",
},
y: {
hover: "group-hover/card:rotate-y-180 motion-reduce:group-hover/card:rotate-y-0",
back: "rotate-y-180",
},
Comment on lines +13 to +21

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 | ⚡ Quick win

Reduced-motion users can no longer access the back face.

Lines 15 and 19 cancel the hover rotation under prefers-reduced-motion, so the card never flips while FlipCardBack stays pre-rotated at 180°. That makes the back content visually unreachable for those users. motion-reduce:transition-none on Line 44 already removes the animation; the rotation override should be dropped so the flip still happens instantly.

Suggested fix
 const ROTATION_CLASS = {
   x: {
-    hover: "group-hover/card:rotate-x-180 motion-reduce:group-hover/card:rotate-x-0",
+    hover: "group-hover/card:rotate-x-180",
     back: "rotate-x-180",
   },
   y: {
-    hover: "group-hover/card:rotate-y-180 motion-reduce:group-hover/card:rotate-y-0",
+    hover: "group-hover/card:rotate-y-180",
     back: "rotate-y-180",
   },
 } as const;
📝 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
const ROTATION_CLASS = {
x: {
hover: "group-hover/card:rotate-x-180 motion-reduce:group-hover/card:rotate-x-0",
back: "rotate-x-180",
},
y: {
hover: "group-hover/card:rotate-y-180 motion-reduce:group-hover/card:rotate-y-0",
back: "rotate-y-180",
},
const ROTATION_CLASS = {
x: {
hover: "group-hover/card:rotate-x-180",
back: "rotate-x-180",
},
y: {
hover: "group-hover/card:rotate-y-180",
back: "rotate-y-180",
},
} as const;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/card/flip-card.tsx` around lines 13 - 21, The hover rotation entries
in ROTATION_CLASS (the hover strings for keys x and y) cancel the flip under
prefers-reduced-motion causing FlipCardBack to remain pre-rotated and
inaccessible; remove the motion-reduce override tokens
("motion-reduce:group-hover/card:rotate-x-0" and
"motion-reduce:group-hover/card:rotate-y-0") from the hover values so the hover
still applies instantly for reduced-motion users (the existing
motion-reduce:transition-none handling at the transition site can remain as-is);
keep the back rotations ("rotate-x-180" / "rotate-y-180") unchanged.

} as const;

function useFlipCard() {
const context = use(FlipCardContext);
if (!context) {
throw new Error("FlipCard.Front and FlipCard.Back must be used within <FlipCard>.");
}
return context;
}

export default function FlipCard({
image,
title,
description,
subtitle,
rotate = "y",
className,
...props
}: FlipCardProps) {
const rotationClass = {
x: ["group-hover/card:rotate-x-180", "rotate-x-180"],
y: ["group-hover/card:rotate-y-180", "rotate-y-180"],
} as const;
type FlipCardRootProps = ComponentProps<"div"> & {
rotate?: FlipCardRotate;
};

function FlipCardRoot({ rotate = "y", className, children, ...props }: FlipCardRootProps) {
const value = useMemo(() => ({ rotate }), [rotate]);

return (
<div className={cn("group/card h-72 w-56 perspective-[1000px]", className)} {...props}>
<div
className={cn(
"relative h-full rounded-2xl transition-transform duration-500 transform-3d",
rotationClass[rotate][0],
)}
>
{/* Front */}
<div className="absolute inset-0 backface-hidden">
<img
src={image}
alt={title}
className="h-full w-full rounded-2xl object-cover shadow-2xl shadow-black/40"
/>
<div className="absolute bottom-4 left-4 text-xl font-bold text-white">{title}</div>
</div>
{/* Back */}
<FlipCardContext.Provider value={value}>
<div className={cn("group/card h-72 w-56 perspective-[1000px]", className)} {...props}>
<div
className={cn(
"absolute inset-0 rounded-2xl bg-black/80 p-4 text-slate-200 backface-hidden",
rotationClass[rotate][1],
"relative h-full rounded-2xl transition-transform duration-500 ease-out transform-3d will-change-transform motion-reduce:transition-none",
ROTATION_CLASS[rotate].hover,
)}
>
<div className="flex min-h-full flex-col gap-2">
<h1 className="text-base font-bold text-white">{subtitle}</h1>
<p className="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100">
{description}
</p>
</div>
{children}
</div>
</div>
</div>
</FlipCardContext.Provider>
);
}

type FlipCardFaceProps = ComponentProps<"div">;

function FlipCardFront({ className, ...props }: FlipCardFaceProps) {
useFlipCard();

return <div className={cn("absolute inset-0 backface-hidden", className)} {...props} />;
}

function FlipCardBack({ className, ...props }: FlipCardFaceProps) {
const { rotate } = useFlipCard();

return (
<div
className={cn("absolute inset-0 backface-hidden", ROTATION_CLASS[rotate].back, className)}
{...props}
/>
);
}

const FlipCard = Object.assign(FlipCardRoot, {
Front: FlipCardFront,
Back: FlipCardBack,
}) as typeof FlipCardRoot & {
Front: typeof FlipCardFront;
Back: typeof FlipCardBack;
};

export default FlipCard;
export { FlipCard, FlipCardBack, FlipCardFront, FlipCardRoot };
46 changes: 34 additions & 12 deletions animata/card/swap-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,23 @@ export default function SwapCard({
firstImageClass,
)}
>
<FlipCard
className="h-72"
title={firstTitle}
description={firstDescription}
image={firstImage}
/>
<FlipCard className="h-72">
<FlipCard.Front>
<img
src={firstImage}
alt={firstTitle}
className="h-full w-full rounded-2xl object-cover shadow-2xl shadow-black/40"
/>
<div className="absolute bottom-4 left-4 text-xl font-bold text-white">
{firstTitle}
</div>
</FlipCard.Front>
<FlipCard.Back className="rounded-2xl bg-black/80 p-4 text-slate-200">
<p className="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100">
{firstDescription}
</p>
</FlipCard.Back>
</FlipCard>
</div>
<div
className={cn(
Expand All @@ -63,12 +74,23 @@ export default function SwapCard({
secondImageClass,
)}
>
<FlipCard
className="h-72"
title={secondTitle}
description={secondDescription}
image={secondImage}
/>
<FlipCard className="h-72">
<FlipCard.Front>
<img
src={secondImage}
alt={secondTitle}
className="h-full w-full rounded-2xl object-cover shadow-2xl shadow-black/40"
/>
<div className="absolute bottom-4 left-4 text-xl font-bold text-white">
{secondTitle}
</div>
</FlipCard.Front>
<FlipCard.Back className="rounded-2xl bg-black/80 p-4 text-slate-200">
<p className="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100">
{secondDescription}
</p>
</FlipCard.Back>
</FlipCard>
</div>
</div>
</div>
Expand Down
122 changes: 78 additions & 44 deletions animata/list/flipping-cards.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,92 @@
import type { Meta, StoryObj } from "@storybook/react";
import FlippingCard from "@/animata/list/flipping-cards";
import { PlusCircle } from "lucide-react";

import Marquee from "@/animata/container/marquee";
import FlippingCards, { getFlippingCardsAccent } from "@/animata/list/flipping-cards";

const demoItems = [
{
font: "Antonov AN-255",
title: "Aa",
image:
"https://images.unsplash.com/photo-1718889874468-3a56b84bb2e7?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
font: "Boeing 747",
title: "Bb",
image:
"https://plus.unsplash.com/premium_photo-1717916843908-7bbee16bad20?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
font: "Cessna 172",
title: "Cc",
image:
"https://images.unsplash.com/photo-1718743256288-e77382a88aaf?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
font: "Dassault Falcon 7X",
title: "Dd",
image:
"https://images.unsplash.com/photo-1718889874468-3a56b84bb2e7?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
font: "Embraer EMB 120 ",
title: "Ee",
image:
"https://images.unsplash.com/photo-1718792679559-5cfd607bb564?q=80&w=1956&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
font: "Fokker F100",
title: "Ff",
image:
"https://images.unsplash.com/photo-1718397172443-48185c6bb4e1?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
];

const meta = {
title: "List/Flipping Cards",
component: FlippingCard,
component: FlippingCards,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {},
} satisfies Meta<typeof FlippingCard>;
} satisfies Meta<typeof FlippingCards>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
list: [
{
font: "Antonov AN-255",
title: "Aa",
image:
"https://images.unsplash.com/photo-1718889874468-3a56b84bb2e7?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
font: "Boeing 747",
title: "Bb",
image:
"https://plus.unsplash.com/premium_photo-1717916843908-7bbee16bad20?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
font: "Cessna 172",
title: "Cc",
image:
"https://images.unsplash.com/photo-1718743256288-e77382a88aaf?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
font: "Dassault Falcon 7X",
title: "Dd",
image:
"https://images.unsplash.com/photo-1718889874468-3a56b84bb2e7?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
font: "Embraer EMB 120 ",
title: "Ee",
image:
"https://images.unsplash.com/photo-1718792679559-5cfd607bb564?q=80&w=1956&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
font: "Fokker F100",
title: "Ff",
image:
"https://images.unsplash.com/photo-1718397172443-48185c6bb4e1?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
],
},
render: () => (
<FlippingCards>
{demoItems.map((item, index) => (
<FlippingCards.Item key={item.title}>
<FlippingCards.Item.Front className="flex bg-white">
<div className="flex w-full flex-col border border-black/15 px-3 py-4 text-sm">
<span className="border-t-2 border-black pt-1 text-black">{item.font}</span>
<span className="mt-4 border-b-2 border-black px-1 font-serif text-8xl text-black">
{item.title}
Comment on lines +63 to +67

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 | ⚡ Quick win

Make the story face markup theme-responsive.

The face content hardcodes light-only colors (bg-white, text-black, border-black), so the demo does not adapt properly in dark mode.

Suggested token-based class update
-          <FlippingCards.Item.Front className="flex bg-white">
-            <div className="flex w-full flex-col border border-black/15 px-3 py-4 text-sm">
-              <span className="border-t-2 border-black pt-1 text-black">{item.font}</span>
-              <span className="mt-4 border-b-2 border-black px-1 font-serif text-8xl text-black">
+          <FlippingCards.Item.Front className="flex bg-card text-card-foreground">
+            <div className="flex w-full flex-col border border-border/60 px-3 py-4 text-sm">
+              <span className="border-t-2 border-border pt-1">{item.font}</span>
+              <span className="mt-4 border-b-2 border-border px-1 font-serif text-8xl">
                 {item.title}
               </span>
@@
-              <span className="text-black">See more</span>
-              <PlusCircle size={18} color="black" />
+              <span className="text-card-foreground">See more</span>
+              <PlusCircle size={18} className="text-card-foreground" />

As per coding guidelines, "All new components must be theme-responsive with support for both light and dark themes".

Also applies to: 84-85

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/list/flipping-cards.stories.tsx` around lines 63 - 67, The story
hardcodes light-only colors in FlippingCards.Item.Front and its children; make
these classes theme-responsive by replacing static color classes with
light/dark-aware classes. Update FlippingCards.Item.Front’s className from "flex
bg-white" to include a dark variant (e.g. "flex bg-white dark:bg-slate-900" or
your design token like "flex bg-surface dark:bg-surface-dark"), change the inner
div’s border/text classes from "border border-black/15 ... text-sm" to use
dual-mode classes (e.g. "border border-black/15 dark:border-white/15 text-black
dark:text-white px-3 py-4 text-sm"), and update the span classes that use
"text-black" and "border-black" to corresponding "text-black dark:text-white"
and "border-black/15 dark:border-white/15" (or your token equivalents) so the
face adapts in dark mode. Ensure all color uses within FlippingCards.Item.Front,
the inner container, and both spans are converted to light/dark variants or
theme tokens.

Source: Coding guidelines

</span>
<div className="mt-12 flex items-center justify-between">
<span>{index + 1}</span>
<PlusCircle size={18} />
</div>
</div>
</FlippingCards.Item.Front>
<FlippingCards.Item.Back
className="flex flex-col justify-between overflow-hidden py-4 text-sm"
style={{ backgroundColor: getFlippingCardsAccent(index) }}
>
<img alt="" src={item.image} className="size-32 px-2" />
<Marquee className="font-serif text-5xl text-white" applyMask={false}>
{item.font.split(" ")[0]}
</Marquee>
<div className="flex items-center justify-between px-3">
<span className="text-black">See more</span>
<PlusCircle size={18} color="black" />
</div>
</FlippingCards.Item.Back>
</FlippingCards.Item>
))}
</FlippingCards>
),
};
Loading
Loading