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:
parent
92b9e67327
commit
8dd6d05d6c
8 changed files with 66 additions and 83 deletions
|
|
@ -304,9 +304,12 @@ h2, h3 {
|
|||
background: color-mix(in srgb, var(--purple) 6%, transparent);
|
||||
}
|
||||
|
||||
/* Header pill — inbox / loose-ends triggers. Compact, count-prominent. */
|
||||
/* Header pill — inbox / loose-ends triggers. Compact, count-prominent.
|
||||
Markup carries `hive-pill` alongside `header-pill` (shared pill.css
|
||||
supplies border/radius/padding/font-size/letter-spacing; `display`
|
||||
is the one thing overridden here, to `inline-flex`, for the icon+
|
||||
label+count row layout — see pill.css's own note on that). */
|
||||
.header-pill {
|
||||
/* border/radius/padding/font-size/letter-spacing: shared pill.css */
|
||||
background: transparent;
|
||||
border-color: var(--purple-dim);
|
||||
color: var(--fg);
|
||||
|
|
@ -603,10 +606,10 @@ pre.diff {
|
|||
font-size: 0.8em;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
/* Markup carries `hive-pill-sm` alongside each of these (shared
|
||||
pill.css supplies the shape); rules below are colour only. */
|
||||
.model-chip,
|
||||
.effort-chip {
|
||||
/* border/radius/padding/font-size/letter-spacing: shared pill.css */
|
||||
display: inline-block;
|
||||
border-color: var(--purple-dim);
|
||||
}
|
||||
.model-chip { color: var(--cyan); }
|
||||
|
|
@ -616,21 +619,15 @@ pre.diff {
|
|||
breakdown on hover. Sized/coloured like a peer of model-chip so
|
||||
the state row reads as one row of chrome. */
|
||||
.ctx-badge {
|
||||
/* border/radius/padding/font-size/letter-spacing: shared pill.css */
|
||||
display: inline-block;
|
||||
border-color: var(--purple-dim);
|
||||
color: var(--green);
|
||||
cursor: default;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* Harness reachability badge. Same chip shape + sizing as
|
||||
`.state-badge` / `.model-chip` so the state row stays visually
|
||||
uniform; colour communicates the actual reachability state. */
|
||||
.status-badge {
|
||||
/* border/radius/padding/font-size/letter-spacing: shared pill.css */
|
||||
display: inline-block;
|
||||
}
|
||||
/* Harness reachability badge. Markup carries `hive-pill` alongside
|
||||
`status-badge` (shared pill.css supplies the shape); colour
|
||||
communicates the actual reachability state. */
|
||||
.status-badge.status-loading { color: var(--muted); border-color: var(--purple-dim); }
|
||||
.status-badge.status-online { color: var(--green); border-color: var(--green);
|
||||
text-shadow: 0 0 6px color-mix(in srgb, var(--green) 55%, transparent); }
|
||||
|
|
@ -662,9 +659,10 @@ pre.diff {
|
|||
the overflow menu (`.overflow-item-new-session` covers it; the
|
||||
`:disabled` opacity treatment lives on the shared
|
||||
`.overflow-item:disabled` rule). */
|
||||
/* Markup carries `hive-pill` alongside `state-badge` (shared pill.css
|
||||
supplies the shape); this rule is only the transition + state-
|
||||
specific colour below. */
|
||||
.state-badge {
|
||||
/* border/radius/padding/font-size/letter-spacing: shared pill.css */
|
||||
display: inline-block;
|
||||
transition: color 280ms ease, border-color 280ms ease,
|
||||
box-shadow 280ms ease, background 280ms ease;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -793,7 +793,7 @@ window.marked = marked;
|
|||
const def = STATE_LABELS[stateName] || STATE_LABELS.loading;
|
||||
const age = fmtAge(Date.now() - stateSince);
|
||||
badge.textContent = def.glyph + ' ' + def.text + ' · ' + age;
|
||||
badge.className = 'state-badge state-' + stateName;
|
||||
badge.className = 'hive-pill state-badge state-' + stateName;
|
||||
badge.title = (STATE_TOOLTIPS[stateName] || '') + '\nin this state for ' + age;
|
||||
const cancelBtn = $('cancel-btn');
|
||||
if (cancelBtn) cancelBtn.hidden = stateName !== 'thinking';
|
||||
|
|
@ -1057,7 +1057,7 @@ window.marked = marked;
|
|||
if (!el_) return;
|
||||
const def = ALIVE_LABELS[status] || ALIVE_LABELS.loading;
|
||||
el_.textContent = def.glyph + ' ' + def.text;
|
||||
el_.className = 'status-badge ' + def.cls;
|
||||
el_.className = 'hive-pill status-badge ' + def.cls;
|
||||
}
|
||||
|
||||
function renderModelChip(model) {
|
||||
|
|
|
|||
|
|
@ -35,12 +35,12 @@
|
|||
</div>
|
||||
|
||||
<div id="state-row" class="agent-state-row agent-header-row">
|
||||
<span id="alive-badge" class="status-badge status-loading" title="harness reachability">…</span>
|
||||
<span id="state-badge" class="state-badge state-loading">… booting</span>
|
||||
<span id="model-chip" class="model-chip" hidden></span>
|
||||
<span id="effort-chip" class="effort-chip" hidden></span>
|
||||
<span id="ctx-badge" class="ctx-badge" hidden title="tokens used in the current context window"></span>
|
||||
<span id="cost-badge" class="ctx-badge" hidden title="cumulative tokens billed across the last turn (sum across every inference; tool-heavy turns rebill the cached prompt per call)"></span>
|
||||
<span id="alive-badge" class="hive-pill status-badge status-loading" title="harness reachability">…</span>
|
||||
<span id="state-badge" class="hive-pill state-badge state-loading">… booting</span>
|
||||
<span id="model-chip" class="hive-pill-sm model-chip" hidden></span>
|
||||
<span id="effort-chip" class="hive-pill-sm effort-chip" hidden></span>
|
||||
<span id="ctx-badge" class="hive-pill-sm ctx-badge" hidden title="tokens used in the current context window"></span>
|
||||
<span id="cost-badge" class="hive-pill-sm ctx-badge" hidden title="cumulative tokens billed across the last turn (sum across every inference; tool-heavy turns rebill the cached prompt per call)"></span>
|
||||
<span id="last-turn" class="last-turn" hidden></span>
|
||||
<button type="button" id="cancel-btn" class="btn-cancel-turn" hidden>■ cancel turn</button>
|
||||
</div>
|
||||
|
|
@ -52,13 +52,13 @@
|
|||
it — see docs/web-ui.md::Per-agent page (Overflow button)
|
||||
for the rare-destructive-extra-click rationale. -->
|
||||
<div class="agent-header-pills">
|
||||
<button type="button" id="inbox-pill" class="header-pill header-pill-inbox" hidden
|
||||
<button type="button" id="inbox-pill" class="hive-pill header-pill header-pill-inbox" hidden
|
||||
title="open inbox flyout">
|
||||
<span class="header-pill-icon" aria-hidden="true">📬</span>
|
||||
<span class="header-pill-label">inbox</span>
|
||||
<span class="header-pill-count" id="inbox-count">0</span>
|
||||
</button>
|
||||
<button type="button" id="todos-pill" class="header-pill header-pill-todos" hidden
|
||||
<button type="button" id="todos-pill" class="hive-pill header-pill header-pill-todos" hidden
|
||||
title="open todos flyout">
|
||||
<span class="header-pill-icon" aria-hidden="true">📋</span>
|
||||
<span class="header-pill-label">todos</span>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}`));
|
||||
|
|
|
|||
|
|
@ -1,52 +1,39 @@
|
|||
/* Shared pill/chip/badge shape — the "extract pill component" issue,
|
||||
resolved as shared CSS rather than a JS component. Every inline
|
||||
status/count chip across the dashboard + per-agent UI shares its
|
||||
structural shape (border, radius, padding, font-size, letter-
|
||||
spacing) from here instead of re-declaring the same values per
|
||||
site; only the per-semantic COLOR (ok/fail/warn/…) stays local to
|
||||
each page's own CSS — this file is shape only.
|
||||
resolved as shared CSS rather than a JS component. `.hive-pill` and
|
||||
`.hive-pill-sm` are the ONLY two classnames this file names — every
|
||||
inline status/count chip across the dashboard + per-agent UI carries
|
||||
one of them (added alongside its own semantic colour modifier class,
|
||||
e.g. `class="hive-pill-sm badge-ok"`) instead of this file having to
|
||||
enumerate every call site's own base classname. Per mara's review on
|
||||
the first cut of this: naming every use here just relocates the
|
||||
duplication rather than removing it — the fix is unifying the
|
||||
classnames themselves, not fanning shared CSS out across old names.
|
||||
|
||||
Two size tiers, matching what was already organically in use rather
|
||||
than inventing a third:
|
||||
.hive-pill — primary state chips (agent status/state/header pills)
|
||||
Two size tiers, matching what was already organically in use:
|
||||
.hive-pill — primary state chips (agent status/state chips)
|
||||
.hive-pill-sm — secondary meta chips (agent model/effort/ctx chips,
|
||||
the dashboard's whole `.badge` family)
|
||||
the dashboard's whole badge family)
|
||||
|
||||
Every pre-existing classname that drew its own copy of this shape
|
||||
(`.status-badge`/`.state-badge`/`.header-pill`/`.ctx-badge`/
|
||||
`.model-chip`/`.effort-chip`/`.badge`) is folded into the same
|
||||
selector groups below, so no markup changes were needed anywhere to
|
||||
pick this up — `display` stays per-site (inline-block for plain
|
||||
text, inline-flex for header-pill's icon+label+count layout), since
|
||||
that's a layout concern, not part of the pill's shape identity. */
|
||||
`display: inline-block` is the default here since that covers every
|
||||
plain-text chip; the one exception (the agent header's icon+label+
|
||||
count flyout pills) overrides `display` locally to `inline-flex` —
|
||||
layout mode isn't part of the shape these two classes exist to
|
||||
share, so a single site overriding it is expected, not a gap. */
|
||||
|
||||
.hive-pill,
|
||||
.hive-pill-sm,
|
||||
.status-badge,
|
||||
.state-badge,
|
||||
.header-pill,
|
||||
.ctx-badge,
|
||||
.model-chip,
|
||||
.effort-chip,
|
||||
.badge {
|
||||
.hive-pill-sm {
|
||||
display: inline-block;
|
||||
border: 1px solid;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.hive-pill,
|
||||
.status-badge,
|
||||
.state-badge,
|
||||
.header-pill {
|
||||
.hive-pill {
|
||||
padding: 0.25em 0.8em;
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.hive-pill-sm,
|
||||
.ctx-badge,
|
||||
.model-chip,
|
||||
.effort-chip,
|
||||
.badge {
|
||||
.hive-pill-sm {
|
||||
padding: 0.1em 0.6em;
|
||||
font-size: 0.78em;
|
||||
letter-spacing: 0.04em;
|
||||
|
|
|
|||
Loading…
Reference in a new issue