Compare commits

..
Author SHA1 Message Date
iris
2ff4cddf9d frontend: hive-warn — replace severity+pulse with info/warning/error levels
mara, PR review: "i dont like that they still have different styling.
if anything, there should be distinction between info,warning,error
(semantics). all warnings should be styled identically."

Replaces the severity ('amber'|'red') + standalone pulse boolean with
a single level ('info'|'warning'|'error') attribute -- a fixed
three-tier ladder instead of an open combination. Colours reuse
theme.css's own already-documented semantics rather than inventing new
ones: --cyan is already "info accents", --amber already "warnings",
--red already "errors, fail state". Pulse is now baked into `error`
specifically rather than a separate opt-in knob, since the one call
site that wanted attention-grabbing (an active incident) is also the
one that's semantically error -- tying the two together removes a
combination that shouldn't exist independently of the tier.

Reclassified the three call sites explicitly (no implicit default
relied on): credentials.html's GitHub PAT advisory and core.js's K3PT
ST4T3 caveat are both `level="warning"` (same as before, and now
identical to each other by construction, not by coincidence);
swarm.js's port-collision banner is `level="error"` (an active
incident needing operator action now, not a standing caveat).

npm run build clean both packages, grepped for leftover
severity/pulse references (only prose mentions describing what this
replaces).
2026-08-03 18:34:31 +02:00
iris
dfd92a7da9 frontend: shared <hive-warn> component for inline warning banners
Consolidates three independently-written instances of the same thing:
.cred-warning (credentials.html, static markup), .tombstone-warn
(core.js, JS-built), .port-conflict (swarm.js, JS-built) -- the first
two differed only by an undeliberate 10% vs 8% tint, the strongest
argument this was drift rather than three genuinely different needs.

New autonomous custom element, frontend/packages/shared/src/hive-warn/,
matching the established one-component-one-dir shadow-DOM pattern
(hive-btn, hive-toast, hive-dialog). Purely presentational -- no
lifecycle beyond attaching its shadow root once, no
attributeChangedCallback needed since severity/pulse are read directly
by :host([...]) CSS selectors rather than mirrored onto an inner
element. Content passes through via a single default <slot>, so every
call site keeps writing its existing <strong>/<code> markup unchanged.

API: <hive-warn> (amber, default) / <hive-warn severity="red"> for an
active incident vs a standing caveat, plus an opt-in pulse boolean
modifier (only the port-collision banner wants it -- a banner that's
always present and always pulsing just trains you to stop seeing it).
Tint is a single canonical 8% for both severities now, replacing the
10%/8% split.

Registered once in dashboard's common.js (same side-effect-import
pattern as <hive-side-panel>) so every dashboard page picks it up
without a per-file import, since all three call sites (core.js,
swarm.js, credentials.js) already transitively import it.

Verified: npm run build clean for both dashboard and agent packages,
grepped source for leftover cred-warning/tombstone-warn/port-conflict
references (none), confirmed hive-warn/HiveWarn/hive-warn-pulse present
in every affected dist bundle, confirmed the still-live
questions-pulse keyframe (.questions) untouched.
2026-08-03 18:34:31 +02:00
9 changed files with 107 additions and 39 deletions

View file

@ -6,6 +6,7 @@
import { linkify as termLinkify } from '@hive/shared/terminal.js'; import { linkify as termLinkify } from '@hive/shared/terminal.js';
import { el } from '@hive/shared/dom.js'; import { el } from '@hive/shared/dom.js';
import '@hive/shared/side-panel.js'; // registers <hive-side-panel> — side-effect import import '@hive/shared/side-panel.js'; // registers <hive-side-panel> — side-effect import
import '@hive/shared/hive-warn.js'; // registers <hive-warn> — side-effect import
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
// ─── helpers ──────────────────────────────────────────────────────────── // ─── helpers ────────────────────────────────────────────────────────────

View file

@ -50,7 +50,7 @@ function renderTombstones(s) {
// Wording is deliberately about what the list *is* rather than what it // Wording is deliberately about what the list *is* rather than what it
// isn't: nothing records a destroy, so "container absent" is the only thing // isn't: nothing records a destroy, so "container absent" is the only thing
// the backend can actually tell. // the backend can actually tell.
const warn = el('p', { class: 'tombstone-warn' }); const warn = el('hive-warn', { level: 'warning' });
warn.append( warn.append(
el('strong', {}, 'shows every agent whose container is absent'), el('strong', {}, 'shows every agent whose container is absent'),
' — not only destroyed ones. An agent part-way through being spawned ' + ' — not only destroyed ones. An agent part-way through being spawned ' +

View file

@ -27,15 +27,6 @@ body.cred-shell {
.cred-pane[hidden] { display: none; } .cred-pane[hidden] { display: none; }
.cred-warning {
border: 1px solid var(--amber);
border-radius: 4px;
padding: 0.7rem 0.9rem;
margin: 0.6rem 0 1.1rem;
font-size: 0.9rem;
background: color-mix(in srgb, var(--amber) 10%, transparent);
}
.gh-status { margin: 0.5rem 0 1.2rem; } .gh-status { margin: 0.5rem 0 1.2rem; }
.gh-status-line { .gh-status-line {
display: flex; display: flex;

View file

@ -90,7 +90,7 @@
Security-warning banner + a link to generate a PAT. --> Security-warning banner + a link to generate a PAT. -->
<section class="cred-pane" id="cred-pane-github" data-tab-pane="github" <section class="cred-pane" id="cred-pane-github" data-tab-pane="github"
role="tabpanel" aria-labelledby="cred-tab-github" hidden> role="tabpanel" aria-labelledby="cred-tab-github" hidden>
<div class="cred-warning"> <hive-warn level="warning">
⚠ use a <strong>dedicated bot account</strong>, not a human's &mdash; ⚠ use a <strong>dedicated bot account</strong>, not a human's &mdash;
and a <strong>minimally-scoped</strong> personal access token (only and a <strong>minimally-scoped</strong> personal access token (only
the repos/scopes the agent actually needs, e.g. <code>repo</code> + the repos/scopes the agent actually needs, e.g. <code>repo</code> +
@ -98,7 +98,7 @@
anything within the token's scopes is reachable if the agent is anything within the token's scopes is reachable if the agent is
ever compromised. the token is injected into the agent's state dir ever compromised. the token is injected into the agent's state dir
and is <strong>never displayed back</strong> on this page. and is <strong>never displayed back</strong> on this page.
</div> </hive-warn>
<h3>◇ status</h3> <h3>◇ status</h3>
<div id="gh-status" class="gh-status"><p class="meta">select an agent to see its github credential status.</p></div> <div id="gh-status" class="gh-status"><p class="meta">select an agent to see its github credential status.</p></div>

View file

@ -444,33 +444,14 @@ hive-agent-menu {
} }
.container-row.tombstone .name { color: var(--muted); } .container-row.tombstone .name { color: var(--muted); }
/* K3PT ST4T3 caveat. Amber, not red like `.port-conflict`, and no pulse: this /* K3PT ST4T3 caveat and the port-collision banner both moved to the
is a standing "read the list this way" note, not an incident a pulsing shared <hive-warn> component amber/no-pulse for
banner that is always present just teaches you to stop seeing it. the standing caveat (a pulsing banner that's always present just
Nothing records a destroy, so this list is every agent whose container is teaches you to stop seeing it), `severity="red" pulse` for the port
absent which includes one being spawned right now. */ collision (an incident, not a caveat). See core.js/swarm.js and
.tombstone-warn { hive-warn.css for the actual styling now.
background: color-mix(in srgb, var(--amber) 8%, transparent);
border: 1px solid var(--amber);
color: var(--amber);
padding: 0.5em 0.8em;
margin-bottom: 0.6em;
border-radius: 4px;
}
.tombstone-warn strong { color: var(--amber); }
/* Notification controls — sit between the banner and the containers. */ Notification controls below sit between the banner and the containers. */
.port-conflict {
background: color-mix(in srgb, var(--red) 8%, transparent);
border: 1px solid var(--red);
color: var(--red);
padding: 0.5em 0.8em;
margin-bottom: 0.6em;
border-radius: 4px;
text-shadow: 0 0 6px color-mix(in srgb, var(--red) 40%, transparent);
animation: questions-pulse 2.4s ease-in-out infinite;
}
.port-conflict strong { color: var(--red); }
/* Build-queue summary banner on the SW4RM tab: one compact line /* Build-queue summary banner on the SW4RM tab: one compact line
when the rebuild queue has active work, with a link to the full queue on when the rebuild queue has active work, with a link to the full queue on

View file

@ -582,7 +582,7 @@ export function renderContainers(s) {
// rebuild. The banner sits above the agent list so it's the // rebuild. The banner sits above the agent list so it's the
// first thing the operator sees when something's wedged. // first thing the operator sees when something's wedged.
if (portConflicts.length) { if (portConflicts.length) {
const banner = el('div', { class: 'port-conflict' }, const banner = el('hive-warn', { level: 'error' },
el('strong', {}, '⚠ port collision'), ' — '); el('strong', {}, '⚠ port collision'), ' — ');
const groups = portConflicts.map((c) => const groups = portConflicts.map((c) =>
`:${c.port} (${c.agents.join(' + ')})`).join('; '); `:${c.port} (${c.agents.join(' + ')})`).join('; ');

View file

@ -22,6 +22,7 @@
"./modal.js": "./src/modal.js", "./modal.js": "./src/modal.js",
"./shadow-css.js": "./src/shadow-css.js", "./shadow-css.js": "./src/shadow-css.js",
"./hive-menu.js": "./src/hive-menu/hive-menu.js", "./hive-menu.js": "./src/hive-menu/hive-menu.js",
"./hive-warn.js": "./src/hive-warn/hive-warn.js",
"./side-panel.js": "./src/side-panel/hive-side-panel.js", "./side-panel.js": "./src/side-panel/hive-side-panel.js",
"./jobq-graph.js": "./src/jobq-graph/hive-jobq-graph.js" "./jobq-graph.js": "./src/jobq-graph/hive-jobq-graph.js"
}, },

View file

@ -0,0 +1,53 @@
/* hive-warn.css shadow-DOM stylesheet for the <hive-warn> autonomous
custom element (hive-warn.js). `level` is an attribute on the HOST
(how callers set it, `hive-warn.js`'s own header explains why)
`:host([level="…"])` sets `color`/`border-color`/`background`/
whether it pulses, same pattern as hive-btn.css's `[variant]`.
Three levels, not an open-ended severity+modifier combination (mara,
on review: "there should be distinction between info, warning, error
all warnings should be styled identically"). Colours reuse
theme.css's own documented semantics rather than inventing new
ones: --cyan is already "info accents", --amber already "warnings",
--red already "errors, fail state" (see theme.css's :root comments).
Pulse is baked into `error` specifically, not a separate opt-in
knob the one call site that wants attention-grabbing (an active
incident) is also the one call site that's semantically `error`, so
tying the two together removes a combination that shouldn't exist
independently of the tier. */
:host {
display: block;
border: 1px solid var(--amber);
border-radius: 4px;
padding: 0.5em 0.8em;
margin-bottom: 0.6em;
color: var(--amber);
background: color-mix(in srgb, var(--amber) 8%, transparent);
}
:host([level="info"]) {
border-color: var(--cyan);
color: var(--cyan);
background: color-mix(in srgb, var(--cyan) 8%, transparent);
}
:host([level="error"]) {
border-color: var(--red);
color: var(--red);
background: color-mix(in srgb, var(--red) 8%, transparent);
/* Only `error` pulses an active incident, not a standing caveat.
A banner that's always present and always pulsing just trains you
to stop seeing it, so `info`/`warning` deliberately stay still.
`currentColor` so the glow matches without a second colour switch. */
text-shadow: 0 0 6px color-mix(in srgb, currentColor 40%, transparent);
animation: hive-warn-pulse 2.4s ease-in-out infinite;
}
@keyframes hive-warn-pulse {
0%, 100% { box-shadow: 0 0 12px -4px color-mix(in srgb, currentColor 55%, transparent); }
50% { box-shadow: 0 0 22px -2px color-mix(in srgb, currentColor 95%, transparent); }
}
/* Explicit rather than relying on plain inheritance matches what
every one of the three original rules declared, kept for parity. */
::slotted(strong) {
color: inherit;
}

View file

@ -0,0 +1,41 @@
// hive-warn.js — <hive-warn>, the shared inline warning-banner
// component. Consolidates three independently-written instances of the
// same thing: `.cred-warning` (credentials.html, static
// markup), `.tombstone-warn` (core.js, JS-built), `.port-conflict`
// (swarm.js, JS-built) — the first two differed only by an
// undeliberate 10% vs 8% tint, the strongest evidence this was drift,
// not three genuinely different needs.
//
// Purely presentational — no internal state, no lifecycle beyond
// attaching its shadow root once. `level` ('info' | 'warning' | 'error',
// default 'warning') is read directly by hive-warn.css's
// `:host([level="…"])` selectors — a fixed three-tier semantic ladder,
// not an open severity+modifier combination (mara, on review: "there
// should be distinction between info, warning, error — all warnings
// should be styled identically"). Unlike `<hive-btn>`'s
// `variant`/`disabled` there's no inner native element that needs the
// attribute mirrored onto it, so no `attributeChangedCallback` is
// needed at all — the CSS reads the host attribute directly.
//
// Content is arbitrary light-DOM children through the shadow tree's
// single default `<slot>` — same reuse of the existing `<strong>`/
// `<code>` markup every call site already writes, no caller-side
// rewrite of their message content needed.
//
// Usage:
// `<hive-warn level="warning">...</hive-warn>` (static HTML)
// `el('hive-warn', { level: 'warning' }, ...)` (JS-built)
// `el('hive-warn', { level: 'error' }, ...)` (active incident, pulses)
import { attachShadowCss } from '../shadow-css.js';
import hiveWarnCss from './hive-warn.css';
class HiveWarn extends HTMLElement {
connectedCallback() {
if (this._built) return; // re-parenting re-fires connectedCallback
const root = attachShadowCss(this, hiveWarnCss);
root.append(document.createElement('slot'));
this._built = true;
}
}
customElements.define('hive-warn', HiveWarn);