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.
This commit is contained in:
iris 2026-08-03 18:21:01 +02:00 committed by mara
commit dfd92a7da9
9 changed files with 99 additions and 39 deletions

View file

@ -0,0 +1,49 @@
/* hive-warn.css shadow-DOM stylesheet for the <hive-warn> autonomous
custom element (hive-warn.js). `severity` is an attribute on the HOST
(how callers set it, `hive-warn.js`'s own header explains why)
`:host([severity="…"])` sets `color`/`border-color`/`background`,
which the shadow content then inherits/reads via `currentColor`, same
pattern as hive-btn.css's `[variant]`. Default (no `severity` set) is
amber the more common case among the three call sites this replaces.
Tint is a single canonical 8% for both severities `.cred-warning`
(the amber call site) was independently written at 10%, `.tombstone-warn`
at 8%; picking one number is the whole point of this component
existing (mara, on review: "so they are all themed consistently"). */
: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([severity="red"]) {
border-color: var(--red);
color: var(--red);
background: color-mix(in srgb, var(--red) 8%, transparent);
}
/* `pulse`: opt-in, for "something is wrong right now" rather than a
standing caveat deliberately not the default. A banner that's
always present and always pulsing just trains you to stop seeing it
(the reasoning on the original `.tombstone-warn`, which stays
non-pulsing; `.port-conflict severity="red" pulse` is the one
call site that wants it). `currentColor` throughout so the glow
matches whichever severity is active without a second color switch. */
:host([pulse]) {
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,37 @@
// 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. `severity` ('amber' | 'red', default
// amber) and `pulse` (boolean) are read directly by hive-warn.css's
// `:host([...])` selectors; 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.
//
// 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>...</hive-warn>` (amber, static HTML)
// `el('hive-warn', {}, ...)` (amber, JS-built)
// `el('hive-warn', { severity: 'red', pulse: '' }, ...)` (urgent)
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);