swarm-ui: delete StatusChip, migrate its 3 callers to shared Badge

mara: non-interactive Badge and StatusChip render identically (same
padding/radius/font, same tone-to-color mapping) and StatusChip's
4-tone/single-label shape is a strict subset of Badge's — no real
reason to keep both. Deletes StatusChip.tsx/.css, migrates AgentsPage's
config-PR chip, HivesPage's freshness chip, and ComponentsPage's own
table-status sample to Badge (tone/value, no onClick). Also drops the
now-redundant standalone StatusChip demo section on /components (the
Badge section already covers all 4 former chip tones plus accent).

Updated the two stale StatusChip references outside swarm-ui too:
design-guide.md's component-list example and colors.css's WCAG-
contrast-rationale comment.
This commit is contained in:
iris 2026-08-28 10:46:04 +02:00 committed by mara
commit 638e1ac2c6
8 changed files with 19 additions and 76 deletions

View file

@ -147,7 +147,7 @@ that needs it, not after it's been styled inline and left for later —
a page reaching for a component that's already there has ready-made
blocks to build with, instead of the next contributor duplicating
ad-hoc styling that someone then has to hunt down and consolidate.
`Panel`/`StatusChip`/`Table`/`TextField`/`SelectField`/`Button` are the
`Panel`/`Badge`/`Table`/`TextField`/`SelectField`/`Button` are the
primitives that exist; `/components` always has the current, complete
list — this doc won't try to keep a duplicate inventory in sync.

View file

@ -10,13 +10,15 @@
// "alive"). Same component either way so a page reads as one consistent
// row of badges regardless of which ones happen to be interactive.
//
// Distinct from swarm-ui's own `StatusChip` (`swarm-ui/src/ui/status-chip/`):
// that one is presentational-only and swarm-ui-local. This one lives here
// in `shared` because both swarm-ui *and* the per-agent page need the
// interactive shape, and `shared` is the one package both already depend
// on (see `docs/web-ui/design-guide.md`'s component-first + junk-drawer
// rules — this is the same visual language, applied where it's actually
// consumed).
// Formerly two components: swarm-ui had its own presentational-only
// `StatusChip`, a strict subset of this shape (no interactivity, no
// label/value split, no icon). Deleted in favour of this one — a
// non-interactive `Badge` renders identically, and having two components
// for the same visual pill just meant a page had to pick between them
// with no real distinction to justify it. Lives in `shared` because both
// swarm-ui *and* the per-agent page need the interactive shape, and
// `shared` is the one package both already depend on (see
// `docs/web-ui/design-guide.md`'s component-first + junk-drawer rules).
import type { ComponentChildren } from 'preact';
import './Badge.css';

View file

@ -49,7 +49,7 @@
(same hue/saturation, lower HSL lightness), not a literal upstream
port like base00-07 above. A review round computed real WCAG
contrast ratios and found the literal Latte accents fail badly as
`StatusChip` fill-text (green/amber/red/yellow on
`Badge`/chip fill-text (green/amber/red/yellow on
`--purple-dim`/base03: 1.4:1-3:1, need 4.5:1) and even as plain
text on `--bg`/base00 in the agent/dashboard packages that also
consume this same file (2.3:1-4.8:1). Root cause: Latte's own

View file

@ -21,6 +21,7 @@
import { useState } from 'preact/hooks';
import { ApiErrorPanel } from '@hive/shared/api-error-panel.js';
import { readApiError, type ProblemDetails } from '@hive/shared/api-error.js';
import { Badge } from '@hive/shared/badge.js';
import { Button } from '../ui/button/Button.js';
import { Dialog } from '../ui/dialog/Dialog.js';
import { Panel } from '../ui/panel/Panel.js';
@ -29,7 +30,6 @@ import {
useRefreshInterval,
type RefreshIntervalMs,
} from '../ui/refresh-interval/RefreshInterval.js';
import { StatusChip } from '../ui/status-chip/StatusChip.js';
import { Table, type TableColumn } from '../ui/table/Table.js';
import { CreateAgentForm } from './CreateAgentForm.js';
@ -50,9 +50,9 @@ const COLUMNS: TableColumn<AgentRow>[] = [
header: 'config PR',
render: (a) =>
a.configPr ? (
<StatusChip
<Badge
tone="warning"
label={
value={
a.configPr.html_url ? (
<a href={a.configPr.html_url} target="_blank" rel="noreferrer">
#{a.configPr.pr_number}

View file

@ -10,7 +10,6 @@ import type { ComponentChildren } from 'preact';
import { Panel } from '../ui/panel/Panel.js';
import { RelativeTime } from '../ui/relative-time/RelativeTime.js';
import { RefreshIntervalPicker, type RefreshIntervalMs } from '../ui/refresh-interval/RefreshInterval.js';
import { StatusChip, type ChipTone } from '../ui/status-chip/StatusChip.js';
import { Table, type TableColumn } from '../ui/table/Table.js';
import { TextField } from '../ui/text-field/TextField.js';
import { SelectField, type SelectOption } from '../ui/select-field/SelectField.js';
@ -37,8 +36,6 @@ function Sample({ label, children }: { label: string; children: ComponentChildre
);
}
const CHIP_TONES: ChipTone[] = ['neutral', 'positive', 'warning', 'negative'];
interface Row {
name: string;
detail: string;
@ -47,7 +44,7 @@ interface Row {
const TABLE_COLUMNS: TableColumn<Row>[] = [
{ key: 'name', header: 'name', render: (r) => r.name },
{ key: 'detail', header: 'detail', render: (r) => r.detail },
{ key: 'status', header: 'status', render: () => <StatusChip tone="positive" label="ok" /> },
{ key: 'status', header: 'status', render: () => <Badge tone="positive" value="ok" /> },
];
const TABLE_ROWS: Row[] = [
@ -170,16 +167,6 @@ export function ComponentsPage() {
</Sample>
</Section>
<Section title="StatusChip">
<div class="components-chip-row">
{CHIP_TONES.map((tone) => (
<Sample key={tone} label={tone}>
<StatusChip tone={tone} label={tone} />
</Sample>
))}
</div>
</Section>
<Section title="Table">
<Sample label="populated">
<Table columns={TABLE_COLUMNS} rows={TABLE_ROWS} rowKey={(r) => r.name} />

View file

@ -15,6 +15,7 @@
import { useState } from 'preact/hooks';
import { ApiErrorPanel } from '@hive/shared/api-error-panel.js';
import { readApiError, type ProblemDetails } from '@hive/shared/api-error.js';
import { Badge, type BadgeTone } from '@hive/shared/badge.js';
import { Panel } from '../ui/panel/Panel.js';
import { RelativeTime } from '../ui/relative-time/RelativeTime.js';
import {
@ -22,7 +23,6 @@ import {
useRefreshInterval,
type RefreshIntervalMs,
} from '../ui/refresh-interval/RefreshInterval.js';
import { StatusChip, type ChipTone } from '../ui/status-chip/StatusChip.js';
import { Table, type TableColumn } from '../ui/table/Table.js';
type Freshness = 'fresh' | 'stale' | 'never_reported' | 'unknown';
@ -41,7 +41,7 @@ interface HiveStatus {
// One tone + label per freshness value. `unknown` (reporting but absent
// from the roster) reads `negative` — it's the one case this endpoint
// cannot vouch for at all, per `status.rs`'s doc comment.
const FRESHNESS: Record<Freshness, { tone: ChipTone; label: string }> = {
const FRESHNESS: Record<Freshness, { tone: BadgeTone; label: string }> = {
fresh: { tone: 'positive', label: 'fresh' },
stale: { tone: 'warning', label: 'stale' },
never_reported: { tone: 'neutral', label: 'never reported' },
@ -68,9 +68,9 @@ const COLUMNS: TableColumn<HiveStatus>[] = [
render: (h) => {
const { tone, label } = FRESHNESS[h.freshness];
return (
<StatusChip
<Badge
tone={tone}
label={
value={
h.last_seen_unix !== null ? (
<>
{label} (<RelativeTime epochMs={h.last_seen_unix * 1000} />)

View file

@ -1,21 +0,0 @@
.ui-chip {
display: inline-block;
padding: 0.15em 0.6em;
border-radius: 1em;
font-size: 0.85em;
line-height: 1.4;
background: var(--purple-dim);
color: var(--fg);
}
.ui-chip-neutral {
color: var(--muted);
}
.ui-chip-positive {
color: var(--green);
}
.ui-chip-warning {
color: var(--amber);
}
.ui-chip-negative {
color: var(--red);
}

View file

@ -1,25 +0,0 @@
// <StatusChip> — a small labelled state indicator. Generic over
// `tone` rather than a fixed status vocabulary (`online`/`offline`/…)
// because the first real caller (the hive roster) starts with a
// single static "configured" tone and grows real online/stale/offline
// states once a status rollup lands server-side — same component,
// richer data later, no rebuild.
import type { ComponentChildren } from 'preact';
import './StatusChip.css';
export type ChipTone = 'neutral' | 'positive' | 'warning' | 'negative';
// `label` takes renderable children, not just a string — a freshness
// chip embeds a live `<RelativeTime>` alongside its static text (e.g.
// "fresh (5s ago)" where the age keeps ticking), and a plain string
// couldn't carry that without the chip reaching back into a caller's
// formatting choices.
export function StatusChip({
tone = 'neutral',
label,
}: {
tone?: ChipTone;
label: ComponentChildren;
}) {
return <span class={'ui-chip ui-chip-' + tone}>{label}</span>;
}