98 lines
2.9 KiB
TypeScript
98 lines
2.9 KiB
TypeScript
|
|
/* Skeleton helpers — standardized loading shells.
|
||
|
|
*
|
||
|
|
* Use these instead of one-off `<Skeleton className="h-12" />` so the
|
||
|
|
* loading state matches the post-load layout. Each helper renders a
|
||
|
|
* shape that approximates a specific final element.
|
||
|
|
*
|
||
|
|
* Available helpers:
|
||
|
|
* • SkeletonRow — single line row (table row, list item)
|
||
|
|
* • SkeletonRows — repeated rows with a stagger
|
||
|
|
* • SkeletonCard — KPI / summary card body
|
||
|
|
* • SkeletonChart — recharts placeholder of fixed height
|
||
|
|
* • SkeletonHeader — page sub-section title placeholder */
|
||
|
|
|
||
|
|
import { cn } from '@/lib/utils';
|
||
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
||
|
|
|
||
|
|
interface SkeletonProps {
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function SkeletonRow({ className }: SkeletonProps) {
|
||
|
|
return (
|
||
|
|
<div className={cn('flex items-center gap-3 py-2', className)}>
|
||
|
|
<Skeleton className="h-4 flex-1" />
|
||
|
|
<Skeleton className="h-4 w-16" />
|
||
|
|
<Skeleton className="h-4 w-12" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function SkeletonRows({ count = 5, className }: SkeletonProps & { count?: number }) {
|
||
|
|
return (
|
||
|
|
<div className={cn('space-y-1', className)}>
|
||
|
|
{Array.from({ length: count }).map((_, i) => (
|
||
|
|
<SkeletonRow key={i} />
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function SkeletonCard({ className }: SkeletonProps) {
|
||
|
|
return (
|
||
|
|
<div className={cn('rounded-md border p-4 space-y-3', className)}>
|
||
|
|
<Skeleton className="h-3 w-24" />
|
||
|
|
<Skeleton className="h-8 w-32" />
|
||
|
|
<Skeleton className="h-3 w-40" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function SkeletonChart({
|
||
|
|
className,
|
||
|
|
height = 180,
|
||
|
|
}: SkeletonProps & { height?: number }) {
|
||
|
|
return (
|
||
|
|
<div className={cn('w-full', className)} style={{ height }}>
|
||
|
|
<div className="relative h-full w-full overflow-hidden rounded-md bg-muted/40">
|
||
|
|
<Skeleton className="h-full w-full opacity-60" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function SkeletonHeader({ className }: SkeletonProps) {
|
||
|
|
return (
|
||
|
|
<div className={cn('space-y-2', className)}>
|
||
|
|
<Skeleton className="h-3 w-20" />
|
||
|
|
<Skeleton className="h-7 w-48" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** SkeletonTable — header row + N body rows. Wrap in Card with p-0 for best fit. */
|
||
|
|
export function SkeletonTable({
|
||
|
|
rows = 5,
|
||
|
|
cols = 4,
|
||
|
|
className,
|
||
|
|
}: SkeletonProps & { rows?: number; cols?: number }) {
|
||
|
|
return (
|
||
|
|
<div className={cn('overflow-hidden', className)}>
|
||
|
|
<div className="bg-muted/40 px-4 py-2.5 flex gap-4">
|
||
|
|
{Array.from({ length: cols }).map((_, i) => (
|
||
|
|
<Skeleton key={i} className="h-3 flex-1" />
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<div className="divide-y divide-border">
|
||
|
|
{Array.from({ length: rows }).map((_, i) => (
|
||
|
|
<div key={i} className="px-4 py-2.5 flex gap-4">
|
||
|
|
{Array.from({ length: cols }).map((_, j) => (
|
||
|
|
<Skeleton key={j} className="h-4 flex-1" />
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|