Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
217
admin-compliance/components/common/SkeletonText.tsx
Normal file
217
admin-compliance/components/common/SkeletonText.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 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 (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
{Array.from({ length: lines }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-4 bg-slate-200 rounded animate-pulse ${getLineWidth(i)}`}
|
||||
style={{ animationDelay: `${i * 100}ms` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={`bg-slate-200 animate-pulse ${width} ${height} ${roundedClass} ${className}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={`bg-white rounded-xl shadow-sm border p-4 ${className}`}>
|
||||
{showImage && (
|
||||
<SkeletonBox height="h-32" className="mb-4" />
|
||||
)}
|
||||
<SkeletonBox width="w-2/3" height="h-5" className="mb-3" />
|
||||
<SkeletonText lines={lines} variant="paragraph" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={`bg-slate-50 rounded-lg p-4 ${className}`}>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-4 h-4 rounded-full bg-purple-200 animate-pulse" />
|
||||
<SkeletonBox width="w-32" height="h-4" rounded="md" />
|
||||
</div>
|
||||
|
||||
{/* Text area skeleton */}
|
||||
<div className="bg-white border p-3 rounded space-y-2 mb-4">
|
||||
<SkeletonText lines={4} variant="paragraph" />
|
||||
</div>
|
||||
|
||||
{/* Metrics grid skeleton */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="bg-white border rounded p-2">
|
||||
<SkeletonBox width="w-16" height="h-3" className="mb-2" rounded="sm" />
|
||||
<SkeletonBox width="w-12" height="h-5" rounded="sm" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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}</> : <SkeletonText lines={lines} className={className} />
|
||||
}
|
||||
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 (
|
||||
<div className={`relative overflow-hidden bg-slate-200 rounded-full h-2 ${className}`}>
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-slate-200 via-slate-300 to-slate-200 animate-shimmer"
|
||||
style={{
|
||||
backgroundSize: '200% 100%',
|
||||
animation: `shimmer ${speed}s infinite linear`
|
||||
}}
|
||||
/>
|
||||
<style jsx>{`
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulsing dot indicator for inline loading
|
||||
*/
|
||||
export function SkeletonDots({ className = '' }: { className?: string }) {
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 ${className}`}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="w-1.5 h-1.5 rounded-full bg-purple-500 animate-pulse"
|
||||
style={{ animationDelay: `${i * 200}ms` }}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user