265 lines
6.3 KiB
TypeScript
265 lines
6.3 KiB
TypeScript
import React, { forwardRef, ReactNode, HTMLAttributes } from 'react';
|
|
import { cn } from '../../lib/utils';
|
|
|
|
// Card variants
|
|
type CardVariant = 'elevated' | 'flat' | 'bordered';
|
|
|
|
// Card props interface
|
|
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
|
variant?: CardVariant;
|
|
children?: ReactNode;
|
|
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
|
hoverable?: boolean;
|
|
shadow?: boolean;
|
|
}
|
|
|
|
// Card header props
|
|
interface CardHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
title?: ReactNode;
|
|
subtitle?: ReactNode;
|
|
icon?: ReactNode;
|
|
action?: ReactNode;
|
|
}
|
|
|
|
// Card body props
|
|
interface CardBodyProps extends HTMLAttributes<HTMLDivElement> {
|
|
children?: ReactNode;
|
|
}
|
|
|
|
// Card footer props
|
|
interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {
|
|
children?: ReactNode;
|
|
align?: 'left' | 'center' | 'right';
|
|
}
|
|
|
|
// Card image props
|
|
interface CardImageProps extends HTMLAttributes<HTMLDivElement> {
|
|
src: string;
|
|
alt?: string;
|
|
height?: 'sm' | 'md' | 'lg' | 'xl';
|
|
position?: 'top' | 'background';
|
|
}
|
|
|
|
// Helper function to get variant styles
|
|
const getVariantStyles = (variant: CardVariant) => {
|
|
switch (variant) {
|
|
case 'elevated':
|
|
return 'bg-white shadow-lg shadow-gray-200/50 border border-gray-100';
|
|
case 'flat':
|
|
return 'bg-white shadow-sm border border-gray-100';
|
|
case 'bordered':
|
|
return 'bg-white border-2 border-gray-200';
|
|
default:
|
|
return 'bg-white shadow-md border border-gray-100';
|
|
}
|
|
};
|
|
|
|
// Helper function to get padding styles
|
|
const getPaddingStyles = (padding: CardProps['padding']) => {
|
|
switch (padding) {
|
|
case 'none':
|
|
return '';
|
|
case 'sm':
|
|
return 'p-3';
|
|
case 'md':
|
|
return 'p-4';
|
|
case 'lg':
|
|
return 'p-6';
|
|
case 'xl':
|
|
return 'p-8';
|
|
default:
|
|
return 'p-4';
|
|
}
|
|
};
|
|
|
|
// Helper function to get image height
|
|
const getImageHeight = (height: CardImageProps['height']) => {
|
|
switch (height) {
|
|
case 'sm':
|
|
return 'h-32';
|
|
case 'md':
|
|
return 'h-48';
|
|
case 'lg':
|
|
return 'h-64';
|
|
case 'xl':
|
|
return 'h-80';
|
|
default:
|
|
return 'h-48';
|
|
}
|
|
};
|
|
|
|
// Main Card Component
|
|
export const Card = forwardRef<HTMLDivElement, CardProps>(
|
|
(
|
|
{
|
|
variant = 'elevated',
|
|
padding = 'md',
|
|
hoverable = false,
|
|
shadow = true,
|
|
className = '',
|
|
children,
|
|
...props
|
|
},
|
|
ref
|
|
) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'rounded-lg',
|
|
'transition-all duration-200 ease-in-out',
|
|
// Variant styles
|
|
getVariantStyles(variant),
|
|
// Padding
|
|
getPaddingStyles(padding),
|
|
// Hover effect
|
|
hoverable && 'hover:shadow-xl hover:shadow-gray-200/70 hover:-translate-y-1',
|
|
// Shadow override
|
|
!shadow && 'shadow-none',
|
|
// Custom classes
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
Card.displayName = 'Card';
|
|
|
|
// Card Header Component
|
|
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
|
|
({ title, subtitle, icon, action, className = '', children, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'flex items-start justify-between gap-4',
|
|
'border-b border-gray-100 pb-4 mb-4',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div className="flex items-start gap-3 flex-1">
|
|
{icon && <div className="text-gray-500 mt-0.5">{icon}</div>}
|
|
<div className="flex-1">
|
|
{title && (
|
|
<div className="text-lg font-semibold text-gray-900 leading-tight">
|
|
{title}
|
|
</div>
|
|
)}
|
|
{subtitle && (
|
|
<div className="text-sm text-gray-600 mt-1 leading-relaxed">
|
|
{subtitle}
|
|
</div>
|
|
)}
|
|
{children}
|
|
</div>
|
|
</div>
|
|
{action && <div className="flex-shrink-0">{action}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
CardHeader.displayName = 'CardHeader';
|
|
|
|
// Card Body Component
|
|
export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
|
|
({ className = '', children, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn('space-y-3', className)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
CardBody.displayName = 'CardBody';
|
|
|
|
// Card Footer Component
|
|
export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
|
|
({ align = 'left', className = '', children, ...props }, ref) => {
|
|
const alignmentClasses = {
|
|
left: 'justify-start',
|
|
center: 'justify-center',
|
|
right: 'justify-end',
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'flex items-center gap-3',
|
|
'border-t border-gray-100 pt-4 mt-4',
|
|
alignmentClasses[align],
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
CardFooter.displayName = 'CardFooter';
|
|
|
|
// Card Image Component
|
|
export const CardImage = forwardRef<HTMLDivElement, CardImageProps>(
|
|
({ src, alt, height = 'md', position = 'top', className = '', ...props }, ref) => {
|
|
const heightClasses = getImageHeight(height);
|
|
|
|
if (position === 'background') {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'relative w-full overflow-hidden rounded-t-lg',
|
|
heightClasses,
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={src}
|
|
alt={alt || ''}
|
|
className="absolute inset-0 w-full h-full object-cover"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'w-full overflow-hidden rounded-t-lg',
|
|
heightClasses,
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={src}
|
|
alt={alt || ''}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
CardImage.displayName = 'CardImage';
|
|
|
|
// Export types for external use
|
|
export type { CardProps, CardHeaderProps, CardBodyProps, CardFooterProps, CardImageProps, CardVariant }; |