/** * Skeleton Loading Components * * Animated placeholder components for loading states. * Used throughout the app for smoother UX during async operations. */ import { ReactNode } from 'react' interface SkeletonTextProps { /** Number of lines to display */ lines?: number /** Width variants for varied line lengths */ variant?: 'uniform' | 'varied' | 'paragraph' /** Custom class names */ className?: string } /** * Animated skeleton text placeholder */ export function SkeletonText({ lines = 3, variant = 'varied', className = '' }: SkeletonTextProps) { const getLineWidth = (index: number) => { if (variant === 'uniform') return 'w-full' if (variant === 'paragraph') { // Last line is shorter for paragraph effect if (index === lines - 1) return 'w-3/5' return 'w-full' } // Varied widths const widths = ['w-full', 'w-4/5', 'w-3/4', 'w-5/6', 'w-2/3'] return widths[index % widths.length] } return (
{Array.from({ length: lines }).map((_, i) => (
))}
) } interface SkeletonBoxProps { /** Width (Tailwind class or px) */ width?: string /** Height (Tailwind class or px) */ height?: string /** Custom class names */ className?: string /** Border radius */ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full' } /** * Animated skeleton box for images, avatars, cards */ export function SkeletonBox({ width = 'w-full', height = 'h-32', className = '', rounded = 'lg' }: SkeletonBoxProps) { const roundedClass = { none: '', sm: 'rounded-sm', md: 'rounded-md', lg: 'rounded-lg', xl: 'rounded-xl', full: 'rounded-full' }[rounded] return (
) } interface SkeletonCardProps { /** Show image placeholder */ showImage?: boolean /** Number of text lines */ lines?: number /** Custom class names */ className?: string } /** * Skeleton card with optional image and text lines */ export function SkeletonCard({ showImage = true, lines = 3, className = '' }: SkeletonCardProps) { return (
{showImage && ( )}
) } interface SkeletonOCRResultProps { /** Custom class names */ className?: string } /** * Skeleton specifically for OCR results display * Shows loading state while OCR is processing */ export function SkeletonOCRResult({ className = '' }: SkeletonOCRResultProps) { return (
{/* Text area skeleton */}
{/* Metrics grid skeleton */}
{Array.from({ length: 4 }).map((_, i) => (
))}
) } interface SkeletonWrapperProps { /** Whether to show skeleton or children */ loading: boolean /** Content to show when not loading */ children: ReactNode /** Skeleton component or elements to show */ skeleton?: ReactNode /** Fallback skeleton lines (if skeleton prop not provided) */ lines?: number /** Custom class names */ className?: string } /** * Wrapper component that toggles between skeleton and content */ export function SkeletonWrapper({ loading, children, skeleton, lines = 3, className = '' }: SkeletonWrapperProps) { if (loading) { return skeleton ? <>{skeleton} : } return <>{children} } interface SkeletonProgressProps { /** Animation speed in seconds */ speed?: number /** Custom class names */ className?: string } /** * Animated progress skeleton with shimmer effect */ export function SkeletonProgress({ speed = 1.5, className = '' }: SkeletonProgressProps) { return (
) } /** * Pulsing dot indicator for inline loading */ export function SkeletonDots({ className = '' }: { className?: string }) { return ( {[0, 1, 2].map((i) => ( ))} ) }