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:
parent
b2e3b05198
commit
464b52004b
3 changed files with 192 additions and 239 deletions
|
|
@ -8,6 +8,7 @@
|
|||
@import "@hive/shared/jobq-rollup.css";
|
||||
@import "@hive/shared/jobq-graph.css";
|
||||
@import "@hive/shared/user-menu.css";
|
||||
@import "@hive/shared/links-menu.css";
|
||||
|
||||
/* ─── tabbed dashboard chrome ──────────────────────────────────────
|
||||
Top-of-page sticky header with banner + tab strip. SSE stays
|
||||
|
|
@ -43,71 +44,20 @@ body.dashboard-shell {
|
|||
padding: 0.3em 1em;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
/* Profile menu (user-menu.js): the avatar at the right end of the chrome.
|
||||
Its own look is `@hive/shared/user-menu.css`'s. */
|
||||
.user-menu-slot {
|
||||
/* The pages menu and the profile menu, together at the right end of the
|
||||
chrome. Their own looks are `@hive/shared/links-menu.css`'s and
|
||||
`@hive/shared/user-menu.css`'s. */
|
||||
.dashboard-chrome-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
flex: none;
|
||||
margin-left: auto;
|
||||
}
|
||||
/* Pages menu (links-menu.js): one icon button at the left of the chrome
|
||||
that opens a popover of the standalone pages and the swarm UI. Same
|
||||
shape as the swarm UI's LinksMenu — quiet until hovered or open. The
|
||||
explicit `[hidden]` rule is needed because `display: flex` on the
|
||||
popover would otherwise beat the UA `[hidden]` style. */
|
||||
.links-menu {
|
||||
position: relative;
|
||||
.links-menu-slot,
|
||||
.user-menu-slot {
|
||||
flex: none;
|
||||
}
|
||||
.links-menu-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.4em;
|
||||
background: none;
|
||||
color: var(--purple);
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.links-menu-button:hover,
|
||||
.links-menu-button[aria-expanded="true"] {
|
||||
border-color: var(--border);
|
||||
background: var(--bg-elev);
|
||||
}
|
||||
.links-menu-popover {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.4em);
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 10em;
|
||||
padding: 0.4em;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5em;
|
||||
background: var(--bg-elev);
|
||||
box-shadow: 0 0.25em 0.75em rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.links-menu-popover[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.links-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 0.4em 0.6em;
|
||||
border-radius: 0.35em;
|
||||
color: var(--fg);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.links-menu-item:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
.banner-thin {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
|
|
@ -125,7 +75,7 @@ body.dashboard-shell {
|
|||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 0.2em;
|
||||
/* On one row with the pages menu; the chrome owns the gutter +
|
||||
/* On one row with the chrome's menus; the chrome owns the gutter +
|
||||
the bottom divider, so no outer padding / border here. */
|
||||
}
|
||||
/* Shares the flat-pill aesthetic of the shared `.hive-tab` (sub-tabs on
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@
|
|||
button / shared links keep you on the same view. JS owns
|
||||
the show/hide. -->
|
||||
<header class="dashboard-chrome">
|
||||
<!-- Pages menu: the standalone pages (stats, API docs) and the
|
||||
swarm UI. Filled by links-menu.js from /api/state. The pages link
|
||||
back to `/`, which is this dashboard. -->
|
||||
<div class="links-menu" id="links-menu"></div>
|
||||
<nav class="tabbar" id="tabbar" role="tablist">
|
||||
<a
|
||||
class="tab"
|
||||
|
|
@ -74,11 +70,18 @@
|
|||
under 4G3NTS. See tab-pane-agents.) -->
|
||||
|
||||
<!-- ST4TS is a standalone page reached from the pages
|
||||
menu at the left of this strip, not tabs — the strip holds only
|
||||
menu at the right of this strip, not tabs — the strip holds only
|
||||
in-page tabs. -->
|
||||
</nav>
|
||||
<!-- Profile menu: the signed-in user, filled by user-menu.js. -->
|
||||
<div class="user-menu-slot" id="user-menu"></div>
|
||||
<!-- The pages menu and the profile menu, together at the right.
|
||||
Pages menu: the standalone pages (stats, API docs) and the swarm UI,
|
||||
filled by links-menu.js from /api/state; the pages link back to `/`,
|
||||
which is this dashboard. Profile menu: the signed-in user, filled by
|
||||
user-menu.js. -->
|
||||
<div class="dashboard-chrome-actions">
|
||||
<div class="links-menu-slot" id="links-menu"></div>
|
||||
<div class="user-menu-slot" id="user-menu"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tab panes. createTabStrip (@hive/shared/tabs.js) keeps exactly one
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue