import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; import { cn } from "./cn"; /* * A skeleton is a placeholder for content that has not arrived. It has no * behaviour and no state, so as with Badge there is no primitive here, only * `useRender` for the host-element choice. That choice is load-bearing rather * than cosmetic: the two products that want this are both loading file lists, * and a placeholder inside a table has to be able to render as a `td` or a * `tr`. A `div` nested in a table row is markup the browser silently hoists * out of the table, which is exactly the layout jump a skeleton exists to * prevent. * * Fill reuses `--color-action-neutral`, the tonal grey the quiet button and * disabled input already share, so a brand gets a placeholder in its own * neutral for free — NOKL's is warm, and that falls out of the theme rather * than a second colour role invented here. Only the shape is a component * token, matching Badge: GoodSync placeholders are rectangles, NOKL's are * pills. * * `aria-hidden` is the default rather than each caller's job. Twenty * placeholders read aloud are twenty announcements of nothing; the container * that owns the loading state is what should carry the accessible name. Same * division of labour findings.md #17 and #21 reached for icon-only controls, * arrived at from the other direction. A caller that really does want one * announced can still pass `aria-hidden={false}`, since caller props win. * * The pulse is the one place a `motion-reduce:` variant is written by hand * instead of being left to the token layer. Reduced motion there resolves * durations to 1ms, which is right for a transition and wrong for a looping * animation — a 1ms pulse is a strobe. It has to stop, not hurry. Tailwind's * own 2s rhythm is used as-is: no brand has asked for a different one, and the * evolution policy warns off a token per CSS property. */ export interface SkeletonProps extends useRender.ComponentProps<"div"> {} function Skeleton({ className, render, ...props }: SkeletonProps) { return useRender({ defaultTagName: "div", render, props: mergeProps<"div">( { className: cn( "block rounded-(--skeleton-radius)", "bg-(color:--color-action-neutral)", "animate-pulse motion-reduce:animate-none", className, ), } as useRender.ComponentProps<"div">, { "data-slot": "skeleton", "aria-hidden": true, } as useRender.ComponentProps<"div">, props, ), }); } export { Skeleton };