Files
AnthoLume/frontend/src/components/Skeleton.tsx
2026-03-22 17:21:34 -04:00

216 lines
5.4 KiB
TypeScript

import { cn } from '../utils/cn';
interface SkeletonProps {
className?: string;
variant?: 'default' | 'text' | 'circular' | 'rectangular';
width?: string | number;
height?: string | number;
animation?: 'pulse' | 'wave' | 'none';
}
export function Skeleton({
className = '',
variant = 'default',
width,
height,
animation = 'pulse',
}: SkeletonProps) {
const baseClasses = 'bg-surface-strong';
const variantClasses = {
default: 'rounded',
text: 'h-4 rounded-md',
circular: 'rounded-full',
rectangular: 'rounded-none',
};
const animationClasses = {
pulse: 'animate-pulse',
wave: 'animate-wave',
none: '',
};
const style = {
width: width !== undefined ? (typeof width === 'number' ? `${width}px` : width) : undefined,
height:
height !== undefined ? (typeof height === 'number' ? `${height}px` : height) : undefined,
};
return (
<div
className={cn(baseClasses, variantClasses[variant], animationClasses[animation], className)}
style={style}
/>
);
}
interface SkeletonTextProps {
lines?: number;
className?: string;
lineClassName?: string;
}
export function SkeletonText({ lines = 3, className = '', lineClassName = '' }: SkeletonTextProps) {
return (
<div className={cn('space-y-2', className)}>
{Array.from({ length: lines }).map((_, i) => (
<Skeleton
key={i}
variant="text"
className={cn(lineClassName, i === lines - 1 && lines > 1 ? 'w-3/4' : 'w-full')}
/>
))}
</div>
);
}
interface SkeletonAvatarProps {
size?: number | 'sm' | 'md' | 'lg';
className?: string;
}
export function SkeletonAvatar({ size = 'md', className = '' }: SkeletonAvatarProps) {
const sizeMap = {
sm: 32,
md: 40,
lg: 56,
};
const pixelSize = typeof size === 'number' ? size : sizeMap[size];
return <Skeleton variant="circular" width={pixelSize} height={pixelSize} className={className} />;
}
interface SkeletonCardProps {
className?: string;
showAvatar?: boolean;
showTitle?: boolean;
showText?: boolean;
textLines?: number;
}
export function SkeletonCard({
className = '',
showAvatar = false,
showTitle = true,
showText = true,
textLines = 3,
}: SkeletonCardProps) {
return (
<div className={cn('rounded-lg border border-border bg-surface p-4', className)}>
{showAvatar && (
<div className="mb-4 flex items-start gap-4">
<SkeletonAvatar />
<div className="flex-1">
<Skeleton variant="text" className="mb-2 w-3/4" />
<Skeleton variant="text" className="w-1/2" />
</div>
</div>
)}
{showTitle && <Skeleton variant="text" className="mb-4 h-6 w-1/2" />}
{showText && <SkeletonText lines={textLines} />}
</div>
);
}
interface SkeletonTableProps {
rows?: number;
columns?: number;
className?: string;
showHeader?: boolean;
}
export function SkeletonTable({
rows = 5,
columns = 4,
className = '',
showHeader = true,
}: SkeletonTableProps) {
return (
<div className={cn('overflow-hidden rounded-lg bg-surface', className)}>
<table className="min-w-full">
{showHeader && (
<thead>
<tr className="border-b border-border">
{Array.from({ length: columns }).map((_, i) => (
<th key={i} className="p-3">
<Skeleton variant="text" className="h-5 w-3/4" />
</th>
))}
</tr>
</thead>
)}
<tbody>
{Array.from({ length: rows }).map((_, rowIndex) => (
<tr key={rowIndex} className="border-b border-border last:border-0">
{Array.from({ length: columns }).map((_, colIndex) => (
<td key={colIndex} className="p-3">
<Skeleton
variant="text"
className={colIndex === columns - 1 ? 'w-1/2' : 'w-full'}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
interface SkeletonButtonProps {
className?: string;
width?: string | number;
}
export function SkeletonButton({ className = '', width }: SkeletonButtonProps) {
return (
<Skeleton
variant="rectangular"
height={36}
width={width || '100%'}
className={cn('rounded', className)}
/>
);
}
interface PageLoaderProps {
message?: string;
className?: string;
}
export function PageLoader({ message = 'Loading...', className = '' }: PageLoaderProps) {
return (
<div className={cn('flex min-h-[400px] flex-col items-center justify-center gap-4', className)}>
<div className="relative">
<div className="size-12 animate-spin rounded-full border-4 border-surface-strong border-t-secondary-500" />
</div>
<p className="text-sm font-medium text-content-muted">{message}</p>
</div>
);
}
interface InlineLoaderProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function InlineLoader({ size = 'md', className = '' }: InlineLoaderProps) {
const sizeMap = {
sm: 'h-4 w-4 border-2',
md: 'h-6 w-6 border-[3px]',
lg: 'h-8 w-8 border-4',
};
return (
<div className={cn('flex items-center justify-center', className)}>
<div
className={`${sizeMap[size]} animate-spin rounded-full border-surface-strong border-t-secondary-500`}
/>
</div>
);
}
export { SkeletonTable as SkeletonTableExport };