Skeleton.tsx
1 /** 2 * ACDC Skeleton Component 3 * Loading placeholder with pulse animation 4 */ 5 6 import React from 'react' 7 import { cn } from '../../lib/utils' 8 9 export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> { 10 /** Skeleton variant */ 11 variant?: 'text' | 'circular' | 'rectangular' | 'rounded' 12 /** Width (CSS value or number for px) */ 13 width?: string | number 14 /** Height (CSS value or number for px) */ 15 height?: string | number 16 /** Animation type */ 17 animation?: 'pulse' | 'wave' | 'none' 18 } 19 20 export const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>( 21 ( 22 { 23 variant = 'text', 24 width, 25 height, 26 animation = 'pulse', 27 className = '', 28 style, 29 ...props 30 }, 31 ref 32 ) => { 33 const variantStyles = { 34 text: 'rounded h-4', 35 circular: 'rounded-full', 36 rectangular: '', 37 rounded: 'rounded-lg', 38 } 39 40 const animationStyles = { 41 pulse: 'animate-pulse', 42 wave: 'animate-shimmer', 43 none: '', 44 } 45 46 const computedWidth = typeof width === 'number' ? `${width}px` : width 47 const computedHeight = typeof height === 'number' ? `${height}px` : height 48 49 return ( 50 <div 51 ref={ref} 52 className={cn( 53 'bg-[var(--bg-tertiary)]', 54 variantStyles[variant], 55 animationStyles[animation], 56 className 57 )} 58 style={{ 59 width: computedWidth, 60 height: computedHeight, 61 ...style, 62 }} 63 aria-hidden="true" 64 {...props} 65 /> 66 ) 67 } 68 ) 69 70 Skeleton.displayName = 'Skeleton' 71 72 /** 73 * SkeletonText - Multiple lines of text skeleton 74 */ 75 export interface SkeletonTextProps extends React.HTMLAttributes<HTMLDivElement> { 76 /** Number of lines */ 77 lines?: number 78 /** Width of the last line (percentage) */ 79 lastLineWidth?: string 80 /** Gap between lines */ 81 gap?: 'sm' | 'md' | 'lg' 82 } 83 84 const gapStyles = { 85 sm: 'gap-1.5', 86 md: 'gap-2', 87 lg: 'gap-3', 88 } 89 90 export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>( 91 ( 92 { 93 lines = 3, 94 lastLineWidth = '60%', 95 gap = 'md', 96 className = '', 97 ...props 98 }, 99 ref 100 ) => { 101 return ( 102 <div 103 ref={ref} 104 className={cn('flex flex-col', gapStyles[gap], className)} 105 {...props} 106 > 107 {Array.from({ length: lines }).map((_, index) => ( 108 <Skeleton 109 key={index} 110 variant="text" 111 width={index === lines - 1 ? lastLineWidth : '100%'} 112 /> 113 ))} 114 </div> 115 ) 116 } 117 ) 118 119 SkeletonText.displayName = 'SkeletonText' 120 121 /** 122 * StatSkeleton - Skeleton for stat cards 123 */ 124 export interface StatSkeletonProps extends React.HTMLAttributes<HTMLDivElement> { 125 /** Show label skeleton */ 126 showLabel?: boolean 127 } 128 129 export const StatSkeleton = React.forwardRef<HTMLDivElement, StatSkeletonProps>( 130 ( 131 { 132 showLabel = true, 133 className = '', 134 ...props 135 }, 136 ref 137 ) => { 138 return ( 139 <div ref={ref} className={cn('', className)} {...props}> 140 {showLabel && ( 141 <Skeleton variant="text" width={80} height={12} className="mb-2" /> 142 )} 143 <Skeleton variant="text" width={120} height={28} /> 144 </div> 145 ) 146 } 147 ) 148 149 StatSkeleton.displayName = 'StatSkeleton'