Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: the agents page shows placeholder cards in the shape of the real ones while the roster loads

A new Skeleton (a shimmering shape, still where motion is off) and a region
that tells assistive tech once that something is loading. The agent card's
header is split into AgentCardLayout, a layout with a slot for each part: the
card fills the slots with the real icon, badges and controls, its new
AgentCardSkeleton twin fills the same slots with placeholder shapes, so the
card that replaces it lands in the same place. The agents page uses the twin in
place of its "loading…" text.
This commit is contained in:
iris 2026-11-05 23:18:33 +01:00 • committed by mara
commit 27c4a593bb
2 changed files with 277 additions and 50 deletions

View file

@ -0,0 +1,67 @@
/* <Skeleton>: a tinted shape with a light band moving across it. Motion
follows the page's setting, the same three rules the page switch uses
(`shell/Shell.css`): off for an OS that asks for less motion, off when the
setting says reduce, back on when the setting says allow. Where it is off
the shape is a plain tinted block. */
.ui-skeleton {
display: block;
border-radius: 0.3em;
background-color: color-mix(in srgb, var(--muted) 22%, transparent);
background-image: linear-gradient(
90deg,
transparent 0,
color-mix(in srgb, var(--fg) 12%, transparent) 50%,
transparent 100%
);
background-size: 200% 100%;
background-repeat: no-repeat;
background-position: 150% 0;
animation: ui-skeleton-shimmer 1.4s ease-in-out infinite;
}
.ui-skeleton-line {
height: 1em;
max-width: 100%;
}
.ui-skeleton-circle {
border-radius: 50%;
}
.ui-skeleton-block {
border-radius: 0.5em;
}
@keyframes ui-skeleton-shimmer {
to {
background-position: -50% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.ui-skeleton {
animation: none;
background-image: none;
}
}
:root[data-motion="reduce"] .ui-skeleton {
animation: none;
background-image: none;
}
:root[data-motion="allow"] .ui-skeleton {
background-image: linear-gradient(
90deg,
transparent 0,
color-mix(in srgb, var(--fg) 12%, transparent) 50%,
transparent 100%
);
animation: ui-skeleton-shimmer 1.4s ease-in-out infinite;
}
/* The words a screen reader gets, out of sight. */
.ui-skeleton-label {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.ui-skeleton-region {
display: contents;
}

View file

@ -0,0 +1,48 @@
// <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>
);
}