Skip to content

Commit 3d6c107

Browse files
authored
feat: add Card component
1 parent 70ab63e commit 3d6c107

File tree

5 files changed

+204
-0
lines changed

5 files changed

+204
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`Card component > renders correctly 1`] = `
4+
<div>
5+
<div
6+
class="rounded-lg border-none text-card-foreground shadow-f-card bg-white"
7+
>
8+
<div
9+
class="p-6 w-full"
10+
>
11+
<h3
12+
class="text-lg font-semibold leading-none tracking-tight text-primary"
13+
>
14+
Card Title
15+
</h3>
16+
<p
17+
class="text-sm text-muted-foreground"
18+
>
19+
Card Description
20+
</p>
21+
</div>
22+
<div
23+
class="p-6 pt-0"
24+
>
25+
<p>
26+
Text in the card content.
27+
</p>
28+
</div>
29+
<div
30+
class="flex items-center p-6 pt-0"
31+
>
32+
<button
33+
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 gap-2 bg-dodger-blue text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
34+
>
35+
Footer Button
36+
</button>
37+
</div>
38+
</div>
39+
</div>
40+
`;

src/components/card.spec.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { render } from "@testing-library/react";
2+
import { describe, expect, it } from "vitest";
3+
import { Button } from "./button";
4+
import {
5+
Card,
6+
CardContent,
7+
CardDescription,
8+
CardFooter,
9+
CardHeader,
10+
CardTitle,
11+
} from "./card";
12+
13+
describe("Card component", () => {
14+
it("renders correctly", () => {
15+
const { container } = render(
16+
<Card>
17+
<CardHeader>
18+
<CardTitle>Card Title</CardTitle>
19+
<CardDescription>Card Description</CardDescription>
20+
</CardHeader>
21+
22+
<CardContent>
23+
<p>Text in the card content.</p>
24+
</CardContent>
25+
26+
<CardFooter>
27+
<Button>Footer Button</Button>
28+
</CardFooter>
29+
</Card>
30+
);
31+
expect(container).toMatchSnapshot();
32+
});
33+
});

src/components/card.stories.tsx

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import {
3+
Card,
4+
CardContent,
5+
CardDescription,
6+
CardFooter,
7+
CardHeader,
8+
CardTitle,
9+
} from "./card";
10+
import { Button } from "./button";
11+
12+
const meta = {
13+
title: "Card",
14+
component: Card,
15+
parameters: {
16+
layout: "centered",
17+
},
18+
argTypes: {
19+
alternate: {
20+
control: "boolean",
21+
},
22+
},
23+
} satisfies Meta<typeof Card>;
24+
25+
export default meta;
26+
type Story = StoryObj<typeof meta>;
27+
28+
export const Default: Story = {
29+
render(props) {
30+
return (
31+
<Card {...props} className="w-[320px]">
32+
<CardHeader>
33+
<CardTitle>Card Title</CardTitle>
34+
<CardDescription>Card Description</CardDescription>
35+
</CardHeader>
36+
37+
<CardContent>
38+
<p>Long text in card content that should span multiple lines.</p>
39+
</CardContent>
40+
41+
<CardFooter>
42+
<Button>Footer Button</Button>
43+
</CardFooter>
44+
</Card>
45+
);
46+
},
47+
};

src/components/card.tsx

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { cn } from "@/lib/utils";
2+
import { forwardRef, HTMLAttributes } from "react";
3+
4+
interface AlternateCardProps extends HTMLAttributes<HTMLDivElement> {
5+
alternate?: boolean;
6+
}
7+
8+
const Card = forwardRef<HTMLDivElement, AlternateCardProps>(
9+
({ className, alternate, ...props }, ref) => (
10+
<div
11+
ref={ref}
12+
className={cn(
13+
"rounded-lg border-none text-card-foreground shadow-f-card bg-white",
14+
{ "bg-background shadow-f-card-inset": alternate },
15+
className
16+
)}
17+
{...props}
18+
/>
19+
)
20+
);
21+
Card.displayName = "Card";
22+
23+
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
24+
({ className, ...props }, ref) => (
25+
<div ref={ref} className={cn("p-6 w-full", className)} {...props} />
26+
)
27+
);
28+
CardHeader.displayName = "CardHeader";
29+
30+
const CardTitle = forwardRef<
31+
HTMLParagraphElement,
32+
HTMLAttributes<HTMLHeadingElement>
33+
>(({ className, ...props }, ref) => (
34+
<h3
35+
ref={ref}
36+
className={cn(
37+
"text-lg font-semibold leading-none tracking-tight text-primary",
38+
className
39+
)}
40+
{...props}
41+
/>
42+
));
43+
CardTitle.displayName = "CardTitle";
44+
45+
const CardDescription = forwardRef<
46+
HTMLParagraphElement,
47+
HTMLAttributes<HTMLParagraphElement>
48+
>(({ className, ...props }, ref) => (
49+
<p
50+
ref={ref}
51+
className={cn("text-sm text-muted-foreground", className)}
52+
{...props}
53+
/>
54+
));
55+
CardDescription.displayName = "CardDescription";
56+
57+
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
58+
({ className, ...props }, ref) => (
59+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
60+
)
61+
);
62+
CardContent.displayName = "CardContent";
63+
64+
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
65+
({ className, ...props }, ref) => (
66+
<div
67+
ref={ref}
68+
className={cn("flex items-center p-6 pt-0", className)}
69+
{...props}
70+
/>
71+
)
72+
);
73+
CardFooter.displayName = "CardFooter";
74+
75+
export {
76+
Card,
77+
CardHeader,
78+
CardFooter,
79+
CardTitle,
80+
CardDescription,
81+
CardContent,
82+
};

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from "@/components/button";
2+
export * from "@/components/card";
23
export * from "@/components/input";
34
export * from "@/components/label";
5+
export { cn } from "@/lib/utils";

0 commit comments

Comments
 (0)