From 27c4a593bb155280c88da98d5607eec4812cd8f2 Mon Sep 17 00:00:00 2001 From: iris Date: Thu, 5 Nov 2026 23:18:33 +0100 Subject: [PATCH] swarm-ui: the agents page shows placeholder cards in the shape of the real ones while the roster loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/swarm/ui.md | 4 +- .../swarm-ui/src/agent/roster/AgentCard.css | 4 + .../swarm-ui/src/agent/roster/AgentCard.tsx | 97 ++++++++++--------- .../src/agent/roster/AgentCardLayout.tsx | 42 ++++++++ .../src/agent/roster/AgentCardSkeleton.tsx | 47 +++++++++ .../swarm-ui/src/pages/ComponentsPage.tsx | 10 ++ .../swarm-ui/src/pages/agents/AgentsPage.tsx | 8 +- .../swarm-ui/src/ui/skeleton/Skeleton.css | 67 +++++++++++++ .../swarm-ui/src/ui/skeleton/Skeleton.tsx | 48 +++++++++ 9 files changed, 277 insertions(+), 50 deletions(-) create mode 100644 frontend/packages/swarm-ui/src/agent/roster/AgentCardLayout.tsx create mode 100644 frontend/packages/swarm-ui/src/agent/roster/AgentCardSkeleton.tsx create mode 100644 frontend/packages/swarm-ui/src/ui/skeleton/Skeleton.css create mode 100644 frontend/packages/swarm-ui/src/ui/skeleton/Skeleton.tsx diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index db36ead9..cf17723c 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -77,7 +77,9 @@ card in place; select it again to close it. Several cards can be open at once, and a card stays open across the page's refresh, but not across a reload. The filter card, which also holds the **+ agent** button and the refresh picker, filters by name, hive, status, message, wanted state -and config PR. +and config PR. While the roster loads for the first time, the page shows grey +placeholder cards with the same layout as the real ones, so the cards don't jump +when the data arrives. The placeholders stand still where motion is off. An opened card has four sections, and each reads nothing until you open it. **status** and **terminal** start open, the other two folded: diff --git a/frontend/packages/swarm-ui/src/agent/roster/AgentCard.css b/frontend/packages/swarm-ui/src/agent/roster/AgentCard.css index c9e6ded6..e9b37bd1 100644 --- a/frontend/packages/swarm-ui/src/agent/roster/AgentCard.css +++ b/frontend/packages/swarm-ui/src/agent/roster/AgentCard.css @@ -66,6 +66,10 @@ transform: rotate(-45deg); transition: transform 0.15s ease; } +/* The placeholder card keeps the chevron's box without its arrow. */ +.ui-agent-card-chevron-blank { + border-color: transparent; +} .ui-agent-card-row[aria-expanded="true"] .ui-agent-card-chevron { transform: rotate(45deg); } diff --git a/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx b/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx index 5a0e6188..0f14d492 100644 --- a/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx +++ b/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx @@ -26,6 +26,7 @@ import { termPaths } from "../terminal/AgentTermPreview.js"; import { FRESHNESS, showsLoginNeeded, type AgentRow } from "../AgentTypes.js"; import type { AgentActions } from "./AgentActions.js"; import { WantedMenu } from "./WantedMenu.js"; +import { AgentCardLayout } from "./AgentCardLayout.js"; import "./AgentCard.css"; export function AgentCard({ @@ -44,24 +45,24 @@ export function AgentCard({ const term = termPaths(row.name); return ( -
{ - if (e.target !== e.currentTarget) return; - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - toggle(); - } + { + if (e.target !== e.currentTarget) return; + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggle(); + } + }, }} - > - -
-
- {row.name} + icon={} + name={{row.name}} + status={ + <> ) : null} - e.stopPropagation()} - > + + } + trailing={ + e.stopPropagation()} + > + } + title={`open ${row.name}'s terminal in its own tab`} + onClick={() => openTab({ href: term.page, label: term.label })} + /> + + {error ? ( } - title={`open ${row.name}'s terminal in its own tab`} - onClick={() => openTab({ href: term.page, label: term.label })} + tone="negative" + value="failed" + title={error.detail ?? "the declaration failed"} /> - - {error ? ( - - ) : null} - -
-
- {row.snapshot?.status_text ?? "—"} -
-
-
+ ) : null} + + } + message={row.snapshot?.status_text ?? "—"} + chevron={