Spaces:
Sleeping
Sleeping
| import clsx from "clsx"; | |
| /* Server-component-friendly skeleton primitives for route loading.tsx files. | |
| All shimmer comes from the `.skeleton` CSS class in globals.css (the | |
| existing `shimmer` keyframes) — no client JS, no framer-motion, and the | |
| global prefers-reduced-motion override collapses it to a static block. */ | |
| export interface SkeletonProps { | |
| className?: string; | |
| /** CSS width, e.g. "60%" or 120 (px) */ | |
| width?: string | number; | |
| /** CSS height, e.g. 16 (px) or "2rem" */ | |
| height?: string | number; | |
| } | |
| /** | |
| * Skeleton — a single shimmering placeholder block. | |
| * Size it via width/height props or Tailwind classes (h-4 w-32 …). | |
| */ | |
| export function Skeleton({ className, width, height }: SkeletonProps) { | |
| return ( | |
| <div | |
| aria-hidden | |
| className={clsx("skeleton", className)} | |
| style={{ width, height }} | |
| /> | |
| ); | |
| } | |
| export interface SkeletonTextProps { | |
| className?: string; | |
| /** number of text lines, default 3 */ | |
| lines?: number; | |
| /** width of the trailing line, default "60%" */ | |
| lastLineWidth?: string; | |
| } | |
| /** | |
| * SkeletonText — a stack of shimmering text lines; the last line is shorter | |
| * so the block reads like a paragraph. | |
| */ | |
| export function SkeletonText({ | |
| className, | |
| lines = 3, | |
| lastLineWidth = "60%", | |
| }: SkeletonTextProps) { | |
| return ( | |
| <div className={clsx("flex flex-col gap-2", className)}> | |
| {Array.from({ length: lines }).map((_, i) => ( | |
| <Skeleton | |
| key={i} | |
| className="h-3.5" | |
| width={i === lines - 1 ? lastLineWidth : "100%"} | |
| /> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| export interface SkeletonTableProps { | |
| className?: string; | |
| /** body rows, default 8 */ | |
| rows?: number; | |
| /** columns, default 6 */ | |
| columns?: number; | |
| } | |
| /** | |
| * SkeletonTable — header strip + hairline-separated rows; placeholder for | |
| * leaderboard-style tables while route data streams in. | |
| */ | |
| export function SkeletonTable({ | |
| className, | |
| rows = 8, | |
| columns = 6, | |
| }: SkeletonTableProps) { | |
| return ( | |
| <div | |
| aria-hidden | |
| className={clsx( | |
| "hairline overflow-hidden rounded-sm bg-surface", | |
| className, | |
| )} | |
| > | |
| {/* header */} | |
| <div className="hairline-b flex items-center gap-4 bg-surface-2/60 px-3 py-3"> | |
| {Array.from({ length: columns }).map((_, i) => ( | |
| <Skeleton key={i} className="h-3 flex-1" /> | |
| ))} | |
| </div> | |
| {/* rows */} | |
| {Array.from({ length: rows }).map((_, r) => ( | |
| <div | |
| key={r} | |
| className="hairline-b flex items-center gap-4 px-3 py-3.5 last:border-b-0" | |
| > | |
| {Array.from({ length: columns }).map((_, c) => ( | |
| <Skeleton | |
| key={c} | |
| className="h-3 flex-1" | |
| // vary line lengths slightly so the table doesn't look stamped | |
| width={c === 0 ? "70%" : undefined} | |
| /> | |
| ))} | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| export interface SkeletonCardProps { | |
| className?: string; | |
| /** body text lines, default 2 */ | |
| lines?: number; | |
| } | |
| /** | |
| * SkeletonCard — hairline card with a heading line + body text placeholder. | |
| */ | |
| export function SkeletonCard({ className, lines = 2 }: SkeletonCardProps) { | |
| return ( | |
| <div | |
| aria-hidden | |
| className={clsx("hairline rounded-sm bg-surface p-5", className)} | |
| > | |
| <Skeleton className="mb-4 h-4" width="40%" /> | |
| <SkeletonText lines={lines} /> | |
| </div> | |
| ); | |
| } | |
| export interface SkeletonStatsProps { | |
| className?: string; | |
| /** number of stat tiles, default 4 */ | |
| count?: number; | |
| } | |
| /** | |
| * SkeletonStats — grid of MetricCard-shaped stat tiles (big number + label). | |
| * Compose grid columns via className (e.g. "grid-cols-2 md:grid-cols-4"). | |
| */ | |
| export function SkeletonStats({ className, count = 4 }: SkeletonStatsProps) { | |
| return ( | |
| <div | |
| aria-hidden | |
| className={clsx( | |
| "hairline grid gap-px overflow-hidden rounded-sm bg-line", | |
| className, | |
| )} | |
| > | |
| {Array.from({ length: count }).map((_, i) => ( | |
| <div key={i} className="flex flex-col gap-2 bg-surface px-4 py-3"> | |
| <Skeleton className="h-3" width="55%" /> | |
| <Skeleton className="h-7" width="45%" /> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |