Files
doormilxpress_astryx/src/components/ds/Skeleton.jsx
2026-08-07 23:53:15 +05:30

141 lines
4.8 KiB
JavaScript

import * as React from 'react';
import { cn } from '@/lib/utils';
/**
* Skeleton — a loading placeholder.
*
* Uses a travelling shimmer rather than a pulse: on the app's translucent glass
* surfaces a pulsing block reads as a rendering glitch, while a shimmer reads as
* progress. The shimmer is disabled automatically under
* `prefers-reduced-motion` (see index.css).
*/
export function Skeleton({ className, radius = 'md', ...props }) {
return (
<div
aria-hidden="true"
className={cn(
'relative overflow-hidden bg-surface-sunken',
radius === 'full' ? 'rounded-full' : radius === 'lg' ? 'rounded-xl' : 'rounded-md',
'after:absolute after:inset-0 after:shimmer',
className
)}
{...props}
/>
);
}
/**
* The composed patterns below exist so pages never hand-assemble skeletons.
* Each mirrors the real component's geometry, which is what makes the swap from
* loading to loaded invisible.
*/
/** Paragraph placeholder. The last line is short, as real text is. */
export function SkeletonText({ lines = 3, className }) {
return (
<div className={cn('space-y-2', className)} role="status" aria-label="Loading content">
{Array.from({ length: lines }).map((_, i) => (
<Skeleton
key={i}
className={cn('h-3', i === lines - 1 && lines > 1 ? 'w-2/3' : 'w-full')}
/>
))}
</div>
);
}
/** Matches KpiCard: icon tile, big number, caption. */
export function SkeletonKpi({ className }) {
return (
<div className={cn('glass-card border border-white/60 rounded-2xl p-4 shadow-sm', className)}>
<Skeleton className="w-10 h-10 rounded-xl mb-3" />
<Skeleton className="h-7 w-14 mb-2" />
<Skeleton className="h-2.5 w-20" />
</div>
);
}
/** Matches a list row: avatar, two lines of text, trailing value. */
export function SkeletonRow({ className }) {
return (
<div className={cn('flex items-center gap-3 py-3', className)}>
<Skeleton className="w-9 h-9" radius="full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-3 w-40" />
<Skeleton className="h-2.5 w-24" />
</div>
<Skeleton className="h-6 w-10" />
</div>
);
}
/** Matches a card in a grid: title, meta, stat row. */
export function SkeletonCard({ className }) {
return (
<div className={cn('glass-card border border-white/60 rounded-2xl p-5 shadow-sm', className)}>
<div className="flex items-center gap-3 mb-4">
<Skeleton className="w-10 h-10 rounded-xl" />
<div className="flex-1 space-y-2">
<Skeleton className="h-3.5 w-32" />
<Skeleton className="h-2.5 w-20" />
</div>
</div>
<Skeleton className="h-2.5 w-28 mb-4" />
<div className="grid grid-cols-3 gap-3 pt-4 border-t border-border">
{[0, 1, 2].map((i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-5 w-8" />
<Skeleton className="h-2 w-12" />
</div>
))}
</div>
</div>
);
}
/** Matches DataTable: header row plus n body rows. */
export function SkeletonTable({ rows = 5, columns = 4, className }) {
return (
<div className={cn('overflow-hidden', className)} role="status" aria-label="Loading table">
<div className="flex items-center gap-4 px-4 py-3 border-b border-border bg-surface-subtle">
{Array.from({ length: columns }).map((_, i) => (
<Skeleton key={i} className={cn('h-2.5', i === 0 ? 'w-32' : 'flex-1 max-w-24')} />
))}
</div>
{Array.from({ length: rows }).map((_, r) => (
<div key={r} className="flex items-center gap-4 px-4 py-3.5 border-b border-border last:border-0">
{Array.from({ length: columns }).map((_, c) => (
<Skeleton key={c} className={cn('h-3', c === 0 ? 'w-32' : 'flex-1 max-w-24')} />
))}
</div>
))}
</div>
);
}
/** Matches ChartContainer: title, legend, plot area. */
export function SkeletonChart({ className, height = 240 }) {
return (
<div className={cn('glass-card border border-white/60 rounded-2xl p-6 shadow-sm', className)}>
<Skeleton className="h-3.5 w-40 mb-2" />
<Skeleton className="h-2.5 w-56 mb-6" />
<div className="flex items-end gap-3" style={{ height }}>
{[0.5, 0.8, 0.35, 0.95, 0.6, 0.75].map((h, i) => (
<Skeleton key={i} className="flex-1" style={{ height: `${h * 100}%` }} />
))}
</div>
</div>
);
}
/** n SkeletonRows inside a surface — the common "loading a list" case. */
export function SkeletonList({ rows = 5, className }) {
return (
<div className={cn('divide-y divide-border', className)} role="status" aria-label="Loading list">
{Array.from({ length: rows }).map((_, i) => (
<SkeletonRow key={i} />
))}
</div>
);
}