Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/agent/src/components/Header.tsx
atlas 39b95c2ede treefmt: apply prettier
Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.

Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.

None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
2026-09-02 15:25:07 +02:00

99 lines
4.3 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). Three columns: icon, main
// (title + hive-label), pills — see the `pills` prop doc below for why
// the third column is the one that grows. 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 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) plus `children` (the status badges), all in
* `.agent-header-pills`. `.agent-header-main` only ever holds the
* title + hive-label rows, which don't wrap in practice and so stay
* a stable height; only this pills-cluster column (already sized for
* a variable pill count) grows to fit badges+pills together, keeping
* the header a stable height even on widescreen. Meta-nav
* (`<MetaNav>`) lives here too, as a fixed-size trigger, not an
* inline link list in the title row (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>
);
}