frontend: unify pill/chip/badge shape into shared CSS

Per mara's steer on #3053 ("chip/pill/badge is the same if you squint
... theme unification is part of the goal" then "make it common css
instead of component, thats fine. but make them look unified (not as
much per usage css)"): shared CSS, not a JS component.

New @hive/shared/pill.css defines two classes, `.hive-pill` (primary
state chips) and `.hive-pill-sm` (secondary meta chips) — border/
border-radius/padding/font-size/letter-spacing (colour stays per-site,
the meaningful semantic part). Every render call site across dashboard
(swarm.js/core.js/builds.js) and agent (index.html/app.js) now carries
one of the two shared classes directly, alongside its own existing
semantic-colour modifier class.

Second cut of this PR, per argus's approve + mara's follow-up review
comment on the first cut: the first version instead enumerated every
legacy classname (`.badge`, `.status-badge`, `.header-pill`, etc.)
straight into pill.css's own selector groups so no call sites needed
touching. Mara's correction: that just relocates the duplication
rather than removing it, and the shared CSS shouldn't have to keep
naming every consumer. This version does the real rename instead.

Most visible consequence, unchanged from the first cut: dashboard's
`.badge` family moves off its own shape (2px square corners, uppercase,
tighter padding) onto the shared rounded-pill shape + agent's "sm" tier
sizing. `npm run build` clean across all three packages; verified the
compiled bundles carry the new classnames at every call site (dashboard
JS, agent index.html + app.js), not just the source tree.

Fixes #3053
This commit is contained in:
iris 2026-08-12 17:38:07 +02:00 committed by mara
commit 8dd6d05d6c
8 changed files with 66 additions and 83 deletions

View file

@ -344,7 +344,7 @@ async function fetchBuild() {
const li = el('li', { class: 'build-logs-item', 'data-log-id': String(h.id) });
const live = !h.status;
const ok = h.status === 'ok';
const statusClass = live ? 'badge badge-running' : ok ? 'badge badge-ok' : 'badge badge-fail';
const statusClass = live ? 'hive-pill-sm badge-running' : ok ? 'hive-pill-sm badge-ok' : 'hive-pill-sm badge-fail';
const statusLabel = live ? 'live' : ok ? 'ok' : 'fail';
const age = h.finished_at ? fmtTs(h.finished_at) : (live ? '' : fmtTs(h.started_at));
const runtime = h.runtime_secs != null
@ -398,7 +398,7 @@ async function fetchBuild() {
);
if (live) {
const pre = el('pre', { class: 'build-logs-output build-logs-live' }, '');
const badge = el('span', { class: 'build-logs-live-badge badge badge-running' }, 'live');
const badge = el('span', { class: 'build-logs-live-badge hive-pill-sm badge-running' }, 'live');
detail.append(badge, pre);
streamEs = openBuildLogStream(h.id, pre, {
@ -408,7 +408,7 @@ async function fetchBuild() {
const elapsed = Math.floor(Date.now() / 1000) - h.started_at;
runtime.textContent = fmtDuration(Math.max(0, elapsed));
}
badge.className = status === 'ok' ? 'badge badge-ok' : 'badge badge-fail';
badge.className = status === 'ok' ? 'hive-pill-sm badge-ok' : 'hive-pill-sm badge-fail';
badge.textContent = status;
streamEs = null;
detail.dataset.loaded = '1';
@ -416,7 +416,7 @@ async function fetchBuild() {
onError: () => {
if (durTimer) { clearInterval(durTimer); durTimer = null; }
badge.textContent = 'stream error';
badge.className = 'badge badge-fail';
badge.className = 'hive-pill-sm badge-fail';
streamEs = null;
},
});

View file

@ -57,13 +57,11 @@ code {
.empty { color: var(--muted); font-style: italic; }
/* status badges
.badge base + semantic colour variants. Used on container rows
(tabs.js) and build-log rows (logs.js). Shape (border/radius/padding/
font-size/letter-spacing) is shared with the agent UI's pill/chip
family see @hive/shared/pill.css. */
.badge {
display: inline-block;
}
Semantic colour variants only shape comes from `hive-pill-sm`
(shared with the agent UI's pill/chip family, @hive/shared/pill.css),
carried directly in the markup alongside each of these classnames
(swarm.js/core.js/builds.js). Used on container rows and build-log
rows. */
.badge-warn {
color: var(--amber); border-color: var(--amber);
text-shadow: 0 0 6px color-mix(in srgb, var(--amber) 50%, transparent);

View file

@ -66,9 +66,9 @@ function renderTombstones(s) {
el('span', { class: 'name' }, t.name),
// Was `destroyed`, which the backend cannot actually know — see the
// caveat above. `offline` is what the absence of a container proves.
el('span', { class: 'badge badge-muted' }, 'offline'),
el('span', { class: 'hive-pill-sm badge-muted' }, 'offline'),
);
if (t.has_creds) head.append(el('span', { class: 'badge badge-muted' }, 'creds kept'));
if (t.has_creds) head.append(el('span', { class: 'hive-pill-sm badge-muted' }, 'creds kept'));
head.append(el('span', { class: 'meta' },
`${fmtBytes(t.state_bytes)} · ${fmtAgeDays(t.last_seen)}`));
li.append(head);
@ -117,7 +117,7 @@ function renderStalePerms(root, ghosts) {
for (const name of ghosts) {
const li = el('li', { class: 'tombstones-stale-row' });
li.append(el('span', { class: 'tombstones-stale-name' }, name));
li.append(el('span', { class: 'badge badge-muted' }, 'stale perms'));
li.append(el('span', { class: 'hive-pill-sm badge-muted' }, 'stale perms'));
const btn = el('button', {
type: 'button',
class: 'btn btn-destroy',
@ -398,7 +398,7 @@ function renderInfraContainers(rows) {
const head = el('div', { class: 'head' });
head.append(
el('span', { class: 'name' }, c.name),
el('span', { class: 'badge ' + (c.running ? 'badge-ok' : 'badge-fail') },
el('span', { class: 'hive-pill-sm ' + (c.running ? 'badge-ok' : 'badge-fail') },
c.running ? 'running' : 'stopped'),
);
li.append(head);

View file

@ -448,7 +448,7 @@ function buildContainerLi(c, node, opts) {
// already shown (pending / not-running already handled sync).
if (ds.rate_limited) {
head.append(el('span',
{ class: 'badge badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' },
{ class: 'hive-pill-sm badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' },
'⊘ rate limited'));
}
// ctx-window badge
@ -464,7 +464,7 @@ function buildContainerLi(c, node, opts) {
? `last turn context: ${ds.ctx_tokens.toLocaleString()} / ${win.toLocaleString()} `
+ `tokens (${Math.round((ds.ctx_tokens / win) * 100)}% of the window)`
: `last turn context size: ${ds.ctx_tokens.toLocaleString()} tokens`;
head.append(el('span', { class: `badge ${ctxClass}`, title }, `ctx·${k}k`));
head.append(el('span', { class: `hive-pill-sm ${ctxClass}`, title }, `ctx·${k}k`));
}
// -- agent status text (self-reported via set_status) ---------
if (ds.status_text) {
@ -516,32 +516,32 @@ function buildContainerLi(c, node, opts) {
} else if (!c.running && c.failed) {
head.append(el('span',
{
class: 'badge badge-fail',
class: 'hive-pill-sm badge-fail',
title: 'container gave up — its unit hit systemd\'s bounded restart limit and stopped '
+ 'on its own, not on operator request; start it to bring the harness back up',
},
'✖ gave up'));
} else if (!c.running) {
head.append(el('span',
{ class: 'badge badge-muted', title: 'container is shut down — start it to bring the harness back up' },
{ class: 'hive-pill-sm badge-muted', title: 'container is shut down — start it to bring the harness back up' },
'■ not running'));
} else if (c.needs_login) {
head.append(el('a',
{ class: 'badge badge-warn', href: url, target: '_blank', rel: 'noopener' },
{ class: 'hive-pill-sm badge-warn', href: url, target: '_blank', rel: 'noopener' },
'needs login →'));
}
if (c.paused) {
// Paused badge is also a resume button: clicking POSTs /api/resume/{name}
// which removes the marker and flips the badge off via the SSE rescan.
head.append(form(
'/api/resume/' + c.name, 'badge badge-paused btn-inline', '⏸ paused',
'/api/resume/' + c.name, 'hive-pill-sm badge-paused btn-inline', '⏸ paused',
`resume ${c.name}? the turn loop restarts and drains queued messages.`,
{}, { noRefresh: true },
));
}
if (c.needs_update) {
head.append(form(
'/api/rebuild/' + c.name, 'badge badge-warn btn-inline', 'needs update ↻',
'/api/rebuild/' + c.name, 'hive-pill-sm badge-warn btn-inline', 'needs update ↻',
'rebuild ' + c.name + '? hot-reloads the container.',
{}, { noRefresh: true },
));
@ -549,7 +549,7 @@ function buildContainerLi(c, node, opts) {
if (c.active_model) {
head.append(el('span',
{ class: 'badge badge-model', title: `active claude model: ${c.active_model}` },
{ class: 'hive-pill-sm badge-model', title: `active claude model: ${c.active_model}` },
c.active_model));
}
@ -563,7 +563,7 @@ function buildContainerLi(c, node, opts) {
if (targetCount > 0) parts.push(`${targetCount} to answer`);
head.append(el('span',
{
class: 'badge badge-loose-ends',
class: 'hive-pill-sm badge-loose-ends',
title: `pending questions: ${parts.join(', ')} — see the Q33R1ES tab`,
},
`${agentQCount}`));