File
Blob: src/client/components/ui/card.tsx
| 1 | import { type HTMLAttributes, type ReactNode } from "react"; |
| 2 | |
| 3 | type Variant = "default" | "accent"; |
| 4 | |
| 5 | type CardProps = HTMLAttributes<HTMLDivElement> & { |
| 6 | variant?: Variant; |
| 7 | interactive?: boolean; |
| 8 | as?: "div" | "section" | "article"; |
| 9 | children?: ReactNode; |
| 10 | }; |
| 11 | |
| 12 | const base = "rounded-2xl p-5 sm:p-6"; |
| 13 | const variants: Record<Variant, string> = { |
| 14 | default: "border border-zinc-800/60 bg-zinc-900/50", |
| 15 | accent: "border border-accent-500/20 bg-gradient-to-br from-zinc-900/80 to-zinc-900/40", |
| 16 | }; |
| 17 | |
| 18 | export function Card({ variant = "default", interactive, className = "", as = "div", children, ...rest }: CardProps) { |
| 19 | const Tag = as; |
| 20 | const interactiveCls = interactive ? "hover:-translate-y-0.5 transition-transform cursor-pointer" : ""; |
| 21 | return ( |
| 22 | <Tag className={`${base} ${variants[variant]} ${interactiveCls} ${className}`} {...rest}> |
| 23 | {children} |
| 24 | </Tag> |
| 25 | ); |
| 26 | } |
| 27 | |
| 28 | export function CardHeader({ className = "", children, ...rest }: HTMLAttributes<HTMLDivElement>) { |
| 29 | return ( |
| 30 | <div className={`mb-4 flex items-start justify-between gap-3 ${className}`} {...rest}> |
| 31 | {children} |
| 32 | </div> |
| 33 | ); |
| 34 | } |
| 35 | |
| 36 | export function CardTitle({ className = "", children, ...rest }: HTMLAttributes<HTMLHeadingElement>) { |
| 37 | return ( |
| 38 | <h2 className={`text-base font-semibold text-zinc-100 ${className}`} {...rest}> |
| 39 | {children} |
| 40 | </h2> |
| 41 | ); |
| 42 | } |
| 43 | |
| 44 | export function CardDescription({ className = "", children, ...rest }: HTMLAttributes<HTMLParagraphElement>) { |
| 45 | return ( |
| 46 | <p className={`text-sm text-zinc-400 ${className}`} {...rest}> |
| 47 | {children} |
| 48 | </p> |
| 49 | ); |
| 50 | } |