hyperhive/frontend/packages/agent/src/components/MetaNav.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

119 lines
4.4 KiB
TypeScript

// <MetaNav> — the header's meta-nav trigger: a single fixed-size Badge
// (`@hive/shared/icons.js`'s `LinkIcon` — an emoji glyph can't match a
// neighbouring icon's size/weight on any platform, see that file's
// comment) in the pills cluster that opens a popover listing
// stats/screen/forge/config + any `hyperhive.dashboardLinks` extras,
// sourced from the backend's `agent_links()` (the single source of
// truth — same list also feeds the dashboard card's icon strip) — plus
// a `🧭 dashboard` back-link.
//
// `variant="quiet"` — mara: "link and settings button should not have
// the badge bg". `Badge`'s default filled-pill look is right for a
// status/picker chip, wrong for this icon-only trigger, which should
// read as header chrome. See `@hive/shared/badge.js`'s `BadgeVariant`.
//
// A popover keeps the header height fixed — a variable-width inline
// link list would grow the title row past the fixed `--agent-header-h`
// the rest of the page is offset against (see Header.tsx's comment).
// Real `<a>` items, not `@hive/shared`'s `Dropdown` (its items are
// always `<button>`s for command dispatch, which would lose real link
// semantics like ctrl/middle-click and "copy link address"), but
// matching its `.ui-dropdown` visual values exactly (see MetaNav.css) —
// the same "reuse the values, not the component" call `LoginFlow`'s
// `.login-card` makes for the same reason.
import { useEffect, useRef, useState } from "preact/hooks";
import { Badge } from "@hive/shared/badge.js";
import { LinkIcon } from "@hive/shared/icons.js";
import type { AgentLink } from "../types.js";
import "./MetaNav.css";
export interface MetaNavProps {
links: AgentLink[];
forgePublicUrl: string | null;
/** Absolute base URL of the host dashboard (`resolveDashboardBase`)
* — the `🧭 dashboard` item links to `${dashboardBase}dashboard.html`. */
dashboardBase: string;
}
export function MetaNav({
links,
forgePublicUrl,
dashboardBase,
}: MetaNavProps) {
const [open, setOpen] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
// Same "outside click or Escape closes" contract as Dropdown/OverflowMenu.
useEffect(() => {
if (!open) return;
function onPointerDown(e: PointerEvent) {
if (
rootRef.current &&
e.target instanceof Node &&
!rootRef.current.contains(e.target)
)
setOpen(false);
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
document.addEventListener("pointerdown", onPointerDown, true);
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("pointerdown", onPointerDown, true);
document.removeEventListener("keydown", onKeyDown);
};
}, [open]);
// Same kind -> URL resolution as app.js's old refreshState meta-links
// loop: `forge` needs `forgePublicUrl` set or the link is dropped
// entirely (never guessed from `<host>:3000`); `external` is already
// absolute; `container` is a same-origin path.
const visible = links.filter((lnk) => lnk.kind !== "forge" || forgePublicUrl);
// No early-return-on-empty: the dashboard link below is always
// present, so the trigger always has at least one item.
return (
<div class="meta-nav-anchor" ref={rootRef}>
<Badge
value={<LinkIcon />}
title="agent links"
variant="quiet"
onClick={() => setOpen((o) => !o)}
expanded={open}
/>
{open ? (
<div class="meta-nav-popover" role="menu" aria-label="agent links">
<a
class="meta-nav-item"
href={`${dashboardBase}dashboard.html`}
target="_blank"
rel="noopener"
role="menuitem"
onClick={() => setOpen(false)}
>
🧭 dashboard
</a>
{visible.map((lnk) => {
const href =
lnk.kind === "forge" ? `${forgePublicUrl}${lnk.url}` : lnk.url;
return (
<a
key={lnk.url}
class="meta-nav-item"
href={href}
target="_blank"
rel="noopener"
role="menuitem"
onClick={() => setOpen(false)}
>
{lnk.icon ? <span aria-hidden="true">{lnk.icon}</span> : null}
{lnk.label}
</a>
);
})}
</div>
) : null}
</div>
);
}