mara, live, correcting the prior round: 'oh i meant move badges to where the dropdowns are not the other way around. this way the height does not change even on widescreen.' Prior commit nested pills INTO a new row inside .agent-header-main (badges + pills sharing a row under the title). Backwards from what she wanted: badges now join .agent-header-pills instead — the same header-level column pills already lived in, which already handles a variable child count via flex-wrap + justify-content: flex-end. .agent-header-main goes back to just the title + hive-label rows, which don't wrap in practice, so its height stays stable regardless of how many badges/pills are showing. The ResizeObserver dynamic-height fix from the prior commit is untouched and still does the real work of keeping .agent-status- overlay's offset correct if anything does wrap — this is a pure layout-preference change on top of that, not a new bugfix. Verified at 500/1024/1400px — badges+pills form one right-aligned cluster next to the title, no overlap, no ResizeObserver console error. tsc --noEmit clean, build clean, both pre-push lints clean.
99 lines
4.7 KiB
TypeScript
99 lines
4.7 KiB
TypeScript
// <Header> — the agent page's identity strip: icon, glyphic title, the
|
|
// "swarm / hive" label row, and a slot for the status row
|
|
// (`<StatusChips>` — ../status-chips/, kept separate so this component
|
|
// has no opinion on what badges exist). Same three-column shape as the
|
|
// old `#agent-header` markup (icon · main · pills) — see the `pills`
|
|
// prop doc below for what moved into that third column and why.
|
|
// Reuses `agent.css`'s existing `.agent-header*`/`.agent-icon` rules
|
|
// (loaded globally by index.html) rather than a component-scoped
|
|
// stylesheet — no new visual language needed for the chrome itself.
|
|
|
|
// Measures its own rendered height via ResizeObserver, writes it to
|
|
// `--agent-header-real-h` (agent.css's downstream consumers —
|
|
// `.agent-status-overlay`, `.agent-main`'s scroll padding — prefer
|
|
// this over the static `--agent-header-h` guess). A static guess was
|
|
// the root cause of two "login card behind the header" reports on
|
|
// this PR (mara, live): any header content that can wrap an extra
|
|
// line breaks the guess at whatever width triggers it, and no width
|
|
// can be promised safe. Deliberately a SEPARATE var, not an overwrite
|
|
// of `--agent-header-h` in place: `.agent-header`'s own `min-height`
|
|
// also reads that var — overwriting it in place would make the
|
|
// header's own size react to its own just-measured height every
|
|
// tick, a real ResizeObserver feedback loop (confirmed live on the
|
|
// first pass of this fix). `--agent-header-h` stays the static floor;
|
|
// `--agent-header-real-h` is output-only.
|
|
import { useEffect, useRef } from 'preact/hooks';
|
|
import type { ComponentChildren } from 'preact';
|
|
|
|
// Icon 404s when the agent has no `hyperhive.icon` override (no
|
|
// bundled server-side default any more — see
|
|
// `hive_agent::web_ui::screen::serve_icon`). Ports app.js's
|
|
// `bindHeaderIconFallback` exactly: fire-and-forget load, swap to
|
|
// `/favicon.svg` on failure, `dataset.fallback` guards against a
|
|
// 404-on-the-fallback-itself loop.
|
|
function handleIconError(e: Event) {
|
|
const img = e.currentTarget as HTMLImageElement;
|
|
if (img.dataset.fallback) return;
|
|
img.dataset.fallback = '1';
|
|
img.src = '/favicon.svg';
|
|
}
|
|
|
|
export interface HeaderProps {
|
|
label: string;
|
|
hiveLabel?: string | null;
|
|
/** The status/badge row (`<StatusChips>`) — lives in the
|
|
* `.agent-header-pills` cluster now, alongside `pills` (see below),
|
|
* not stacked as its own row under the title. */
|
|
children?: ComponentChildren;
|
|
/** Flyout triggers (inbox/todos/links pills, the overflow menu
|
|
* button) — and, since this round, `children` (the status badges)
|
|
* too, both in `.agent-header-pills`, the SAME header-level column
|
|
* the old markup used for pills alone. First pass of this ask
|
|
* nested pills into a row inside `.agent-header-main` instead —
|
|
* backwards, mara corrected live: "i meant move badges to where the
|
|
* dropdowns are, not the other way around. this way the height does
|
|
* not change even on widescreen" — `.agent-header-main` now only
|
|
* ever holds the title + hive-label rows, which don't wrap in
|
|
* practice, so it stays a stable height; only this pills-cluster
|
|
* column (already used to absorbing a variable pill count) grows to
|
|
* fit badges+pills together. Meta-nav (`<MetaNav>`) lives in here
|
|
* too, as a fixed-size trigger — not the old markup's inline list in
|
|
* the title row (`<nav id="meta-links">`, docs/web-ui/agent.md). */
|
|
pills?: ComponentChildren;
|
|
}
|
|
|
|
export function Header({ label, hiveLabel, children, pills }: HeaderProps) {
|
|
const ref = useRef<HTMLElement>(null);
|
|
|
|
useEffect(() => {
|
|
const el = ref.current;
|
|
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
// getBoundingClientRect() (not ResizeObserver's own contentRect) —
|
|
// we want the full visual box padding+border included, the thing
|
|
// content below the fixed header actually needs to clear, not the
|
|
// content-box-only measurement ResizeObserver's entry defaults to.
|
|
const ro = new ResizeObserver(() => {
|
|
document.documentElement.style.setProperty('--agent-header-real-h', `${el.getBoundingClientRect().height}px`);
|
|
});
|
|
ro.observe(el);
|
|
return () => ro.disconnect();
|
|
}, []);
|
|
|
|
return (
|
|
<header class="agent-header" ref={ref}>
|
|
<img class="agent-icon" src="icon" alt="" onError={handleIconError} />
|
|
<div class="agent-header-main">
|
|
<div class="agent-header-row agent-header-title-row">
|
|
<h2 class="agent-header-title">◆ {label} ◆</h2>
|
|
</div>
|
|
{hiveLabel ? <div class="agent-header-row agent-hive-row">{hiveLabel}</div> : null}
|
|
</div>
|
|
{children || pills ? (
|
|
<div class="agent-header-pills">
|
|
{children}
|
|
{pills}
|
|
</div>
|
|
) : null}
|
|
</header>
|
|
);
|
|
}
|