Badge's default look is a filled pill, right for status/picker chips but wrong for an icon-only header button like the settings gear or agent links trigger, which should read as chrome. Add variant='quiet' (same naming/shape as Button's ButtonVariant) that drops the idle background, keeping the existing hover/expanded feedback. Apply it to the two icon-only Badge triggers: shared SettingsMenu (used by both swarm-ui and the agent page) and the agent page's own MetaNav links trigger. Add a components-page showcase sample so the variant has a visible regression check going forward.
123 lines
5.2 KiB
TypeScript
123 lines
5.2 KiB
TypeScript
// <MetaNav> — the header's meta-nav trigger: a single fixed-size Badge
|
|
// (`@hive/shared/icons.js`'s `LinkIcon` — an emoji glyph here can't
|
|
// match a neighbouring icon's size/weight on any platform, see that
|
|
// file's comment; mara: "agent page link icons is different from
|
|
// swarm ui" was this trigger still on the old `🔗` emoji after
|
|
// swarm-ui's own link trigger had already moved to the SVG) 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, formerly the overflow menu's job (mara:
|
|
// "remove rebuild button, move link to dashboards into links menu" —
|
|
// the overflow `⋯` trigger existed for exactly two items, dashboard-
|
|
// link and rebuild; rebuild's gone outright, the dashboard's own
|
|
// R3BU1LD button already covers it, dashboard-link moves here, so
|
|
// `OverflowMenu` had nothing left to justify existing and is deleted).
|
|
//
|
|
// `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`.
|
|
|
|
// v1 of this rendered every link inline in the header's title row —
|
|
// mara, live: "the login card looks like it is behind the header"
|
|
// (a variable-width link list can grow the title row past the fixed
|
|
// `--agent-header-h` the rest of the page is offset against — see
|
|
// Header.tsx's comment) and "the links should have the same popout as
|
|
// the links in the nav bar of swarm-ui." This is that: one quiet icon
|
|
// trigger (reuses `@hive/shared`'s `Badge`, icon-only), a popover on
|
|
// click, 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` already made 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 any more — 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>
|
|
);
|
|
}
|