Watch
0
0
Fork
You've already forked hyperhive
0

the hive dashboard uses the swarm UI's links menu, beside the profile avatar

The links menu moves into @hive/shared and takes its links as a prop. The swarm
UI feeds it /api/links as before; the dashboard feeds it stats, API docs and the
swarm, and mounts it next to the profile menu in place of its own hamburger on
the left. A link opens in a new tab unless its entry says it is a page of the
same site.
This commit is contained in:
iris 2026-11-01 12:12:38 +01:00 • committed by mara
commit 464b52004b

View file

@ -1,98 +1,36 @@
// Pages menu in the dashboard chrome: one button that opens a popover
// linking the standalone pages (stats, API docs) and the swarm UI.
// Modelled on the swarm UI's `LinksMenu`. The swarm entry only exists
// when `/api/state` carries `swarm_public_url`, so a hive without a
// configured swarm domain shows no dead link.
// Pages menu in the dashboard chrome: one icon button, beside the profile
// avatar at the right, that opens a popover linking the standalone pages
// (stats, API docs) and the swarm UI. It is `@hive/shared`'s `LinksMenu`, the
// same component the swarm UI mounts for its service links, rendered with
// Preact like the profile menu. The swarm entry only exists when `/api/state`
// carries `swarm_public_url`, so a hive without a configured swarm domain
// shows no dead link.
import { h, render } from "preact";
import { LinksMenu } from "@hive/shared/links-menu.js";
import { $ } from "./common.js";
import { el } from "@hive/shared/dom.js";
/** Menu entries for a state snapshot. `external` opens in a new tab. */
/** Menu entries for a state snapshot. The two pages open in place; the swarm
* UI is another site, so it opens in a new tab. */
export function menuLinks(state) {
const links = [
{ icon: "📊", label: "stats", url: "/stats.html" },
{ icon: "🧬", label: "api docs", url: "/api/docs/" },
{ icon: "📊", label: "stats", url: "/stats.html", external: false },
{ icon: "🧬", label: "api docs", url: "/api/docs/", external: false },
];
if (state && state.swarm_public_url) {
links.push({
icon: "🐝",
label: "swarm",
url: state.swarm_public_url,
external: true,
});
}
return links;
}
let wired = false;
function closeMenu(root) {
const pop = root.querySelector(".links-menu-popover");
const btn = root.querySelector(".links-menu-button");
if (pop) pop.hidden = true;
if (btn) btn.setAttribute("aria-expanded", "false");
}
function wire(root) {
if (wired) return;
wired = true;
root.querySelector(".links-menu-button").addEventListener("click", () => {
const pop = root.querySelector(".links-menu-popover");
const btn = root.querySelector(".links-menu-button");
pop.hidden = !pop.hidden;
btn.setAttribute("aria-expanded", String(!pop.hidden));
});
// Outside click / Escape close the popover.
document.addEventListener("pointerdown", (e) => {
if (!root.contains(e.target)) closeMenu(root);
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeMenu(root);
});
}
/** (Re)render the menu into `#links-menu`. Safe to call on every state
* refresh: the open/closed state lives on the popover element, which is
* kept; only its items are rebuilt. */
* refresh: Preact diffs the same component, so an open popover stays open. */
export function renderLinksMenu(state) {
const root = $("links-menu");
if (!root) return;
if (!root.firstChild) {
root.append(
el(
"button",
{
type: "button",
class: "links-menu-button",
"aria-haspopup": "true",
"aria-expanded": "false",
"aria-label": "pages",
title: "pages",
},
"☰",
),
el("div", { class: "links-menu-popover", role: "menu", hidden: "" }),
);
}
const pop = root.querySelector(".links-menu-popover");
pop.replaceChildren(
...menuLinks(state).map((link) => {
const attrs = {
class: "links-menu-item",
href: link.url,
role: "menuitem",
};
if (link.external) {
attrs.target = "_blank";
attrs.rel = "noreferrer";
}
return el(
"a",
attrs,
el("span", { "aria-hidden": "true" }, link.icon),
link.label,
);
}),
);
wire(root);
render(h(LinksMenu, { links: menuLinks(state), label: "pages" }), root);
}