// — the "accessible from every screen" affordance for // swarm-wide services (authelia, matrix, forge, this UI's own API // docs, …): one fixed header button that never grows the header // itself as the list grows, opening a popover with everything // `GET /api/links` returns. Lives in `Shell` (not `ui/`) because it // owns its own fetch — every other `ui/` primitive is presentational // only, this one genuinely isn't reusable outside this one job. // // The list is entirely server-driven (see swarm-controller's // `services.hyperhive.swarm.controller.links` option) — 0 entries // hides the button rather than showing an empty popover, same "don't // render a dead affordance" rule the old dashboard's H0M3 tiles follow // for Forge/Matrix. import { useEffect, useRef, useState } from "preact/hooks"; import { LinkIcon } from "@hive/shared/icons.js"; import "./LinksMenu.css"; interface ServiceLink { label: string; icon: string; url: string; } export function LinksMenu() { const [links, setLinks] = useState(null); const [open, setOpen] = useState(false); const rootRef = useRef(null); useEffect(() => { fetch("/api/links") .then((r) => r.ok ? (r.json() as Promise) : Promise.reject(new Error(`http ${r.status}`)), ) .then(setLinks) .catch(() => setLinks([])); // best-effort: no button beats a broken one }, []); // Close on an outside click or Escape — only listens while open, so // this costs nothing on every other render. useEffect(() => { if (!open) return; function onPointerDown(e: MouseEvent) { if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false); } function onKeyDown(e: KeyboardEvent) { if (e.key === "Escape") setOpen(false); } document.addEventListener("pointerdown", onPointerDown); document.addEventListener("keydown", onKeyDown); return () => { document.removeEventListener("pointerdown", onPointerDown); document.removeEventListener("keydown", onKeyDown); }; }, [open]); if (!links || links.length === 0) return null; return ( ); }