/ src / components / Skeleton / Skeleton.tsx
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'