/**
* 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) => (
))}
)
}