BadgeSkeleton is the twin of Badge: the real Badge frame, as tall as an interactive one (the settings values are buttons), with a placeholder for its value. Permissions, resource caps, internet and allowed domains use it in place of their "loading…" text, the details popover shows lines of text, and the AGENTS.md editor a block, in place of theirs. SkeletonLines is a few stacked lines for such blocks.
70 lines
2 KiB
TypeScript
70 lines
2 KiB
TypeScript
// <Skeleton> — one grey placeholder shape that shimmers, for a view that is
|
||
// waiting on the server. A component that shows loaded data has a twin that
|
||
// fills the same layout with these shapes instead of its sub components (see
|
||
// `AgentCardSkeleton`), so the page does not jump when the data lands.
|
||
//
|
||
// <SkeletonRegion> wraps a group of them for assistive tech: the shapes
|
||
// themselves are `aria-hidden`, and the region says once that something is
|
||
// loading.
|
||
import type { ComponentChildren } from "preact";
|
||
import "./Skeleton.css";
|
||
|
||
export type SkeletonShape = "line" | "circle" | "block";
|
||
|
||
export function Skeleton({
|
||
shape = "line",
|
||
width,
|
||
height,
|
||
class: extraClass,
|
||
}: {
|
||
/** `line`: a rounded bar the height of a line of text; `circle`; `block`: a
|
||
* rounded square that fills `width` × `height`. */
|
||
shape?: SkeletonShape;
|
||
/** Any CSS length; a line fills its row when this is left out. */
|
||
width?: string;
|
||
height?: string;
|
||
class?: string;
|
||
}) {
|
||
const classes = ["ui-skeleton", `ui-skeleton-${shape}`, extraClass]
|
||
.filter(Boolean)
|
||
.join(" ");
|
||
return <span class={classes} style={{ width, height }} aria-hidden="true" />;
|
||
}
|
||
|
||
export function SkeletonRegion({
|
||
label,
|
||
children,
|
||
}: {
|
||
/** What is loading, as a screen reader says it: "the agents". */
|
||
label: string;
|
||
children: ComponentChildren;
|
||
}) {
|
||
return (
|
||
<div class="ui-skeleton-region" role="status">
|
||
<span class="ui-skeleton-label">loading {label}</span>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const LINE_WIDTHS = ["90%", "75%", "85%", "60%"];
|
||
|
||
/** A few lines of text, for a block of prose or a list still loading. */
|
||
export function SkeletonLines({
|
||
label,
|
||
count = 3,
|
||
}: {
|
||
/** What is loading, as a screen reader says it. */
|
||
label: string;
|
||
count?: number;
|
||
}) {
|
||
return (
|
||
<SkeletonRegion label={label}>
|
||
<div class="ui-skeleton-lines">
|
||
{Array.from({ length: count }, (_, i) => (
|
||
<Skeleton key={i} width={LINE_WIDTHS[i % LINE_WIDTHS.length]} />
|
||
))}
|
||
</div>
|
||
</SkeletonRegion>
|
||
);
|
||
}
|