feat(design): skeleton first loads by shape on the shared reveal clock (E28 S3d2, B8)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import clsx from "clsx";
|
||||
import { SURFACE } from "./surfaces";
|
||||
|
||||
// B8 (docs/DESIGN.md): a first load of a grid, list or table shows the SHAPE of what is coming, so
|
||||
// nothing jumps when the data lands. One block primitive (a tonal fill, the Badge's 8% of the text
|
||||
// colour, pulsing unless the user asked for less motion) and the four page shapes built from it. The
|
||||
// shapes are shown through `LoadingState shape=…`, which owns the 300 ms reveal and the status role.
|
||||
|
||||
export function Skeleton({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={clsx("rounded-lg bg-fg/8 animate-pulse motion-reduce:animate-none", className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type SkeletonShape = "grid" | "poster" | "list" | "table";
|
||||
|
||||
const COUNT: Record<SkeletonShape, number> = { grid: 8, poster: 12, list: 6, table: 8 };
|
||||
|
||||
/** The placeholder for a page's first load, by the shape of its content. */
|
||||
export function SkeletonShapes({ shape }: { shape: SkeletonShape }) {
|
||||
const n = Array.from({ length: COUNT[shape] }, (_, i) => i);
|
||||
if (shape === "grid")
|
||||
return (
|
||||
<div className="grid w-full grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-4">
|
||||
{n.map((i) => (
|
||||
// The video card's own surface, bones inside it, as the card will stand.
|
||||
<div key={i} className={`${SURFACE.card} flex flex-col gap-2 p-1.5`}>
|
||||
<Skeleton className="aspect-video w-full" />
|
||||
<Skeleton className="h-3.5 w-4/5" />
|
||||
<Skeleton className="mb-1 h-3 w-1/2" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
if (shape === "poster")
|
||||
return (
|
||||
<div className="grid w-full grid-cols-[repeat(auto-fill,minmax(8.5rem,1fr))] gap-4">
|
||||
{n.map((i) => (
|
||||
<div key={i} className="flex flex-col gap-2">
|
||||
<Skeleton className="aspect-2/3 w-full rounded-xl" />
|
||||
<Skeleton className="h-3 w-3/4" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
if (shape === "list")
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
{n.map((i) => (
|
||||
<div key={i} className="flex items-center gap-3 p-2">
|
||||
<Skeleton className="h-10 w-16 shrink-0" />
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||
<Skeleton className="h-3.5 w-2/3" />
|
||||
<Skeleton className="h-3 w-1/3" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<Skeleton className="mb-1 h-8 w-full" />
|
||||
{n.map((i) => (
|
||||
<Skeleton key={i} className="h-9 w-full opacity-70" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user