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.
71 lines
2.7 KiB
JavaScript
71 lines
2.7 KiB
JavaScript
// Shared hash-routed tab strip — the generic behaviour behind the
|
|
// dashboard tabbar, the logs sub-tabs, and the agent stat-window
|
|
// selector. Pairs with `@hive/shared/tabs.css` (the .hive-tab* styles).
|
|
//
|
|
// Convention over configuration (no element-resolver callbacks):
|
|
// - Tab controls are the `[data-tab="<id>"]` elements inside the
|
|
// `tabbar` you pass in (any element — `<a>` or `<button>`; give
|
|
// them class `hive-tab`).
|
|
// - Panels are matched by `[data-tab-pane="<id>"]` anywhere in the
|
|
// document.
|
|
//
|
|
// Routing is always via `location.hash` so tabs are deep-linkable and
|
|
// honour back/forward. A page may only host one strip (one hash); if a
|
|
// second independent strip is ever needed we add a key prefix then.
|
|
//
|
|
// On show: toggles `.hive-tab--active` + `aria-selected` on each control
|
|
// and `hidden` on each panel, then fires `onShow(id)` for per-tab
|
|
// side-effects (lazy loads, etc.). See docs/web-ui/css-vars.md.
|
|
|
|
/**
|
|
* @param {Element|null} tabbar container holding the `[data-tab]` controls
|
|
* @param {{ defaultId?: string, onShow?: (id: string) => void }} [opts]
|
|
* @returns {{ show: (id: string) => void, active: () => string|null }}
|
|
*/
|
|
export function createTabStrip(tabbar, opts = {}) {
|
|
const { defaultId, onShow } = opts;
|
|
if (!tabbar) {
|
|
return { show() {}, active: () => null };
|
|
}
|
|
const tabs = Array.from(tabbar.querySelectorAll("[data-tab]"));
|
|
const ids = tabs.map((t) => t.getAttribute("data-tab"));
|
|
const fallback = defaultId && ids.includes(defaultId) ? defaultId : ids[0];
|
|
|
|
const paneFor = (id) => document.querySelector(`[data-tab-pane="${id}"]`);
|
|
|
|
const active = () => {
|
|
const hash = location.hash.replace(/^#/, "");
|
|
return ids.includes(hash) ? hash : fallback;
|
|
};
|
|
|
|
const show = (id) => {
|
|
const target = ids.includes(id) ? id : fallback;
|
|
for (const tab of tabs) {
|
|
const tid = tab.getAttribute("data-tab");
|
|
const on = tid === target;
|
|
tab.classList.toggle("hive-tab--active", on);
|
|
tab.setAttribute("aria-selected", String(on));
|
|
const pane = paneFor(tid);
|
|
if (pane) pane.hidden = !on;
|
|
}
|
|
if (typeof onShow === "function") onShow(target);
|
|
};
|
|
|
|
// Drive every activation through the hash so deep-links + back/forward
|
|
// stay authoritative; the hashchange listener does the actual showing.
|
|
for (const tab of tabs) {
|
|
tab.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const id = tab.getAttribute("data-tab");
|
|
if (location.hash.replace(/^#/, "") === id) {
|
|
show(id); // same hash → no hashchange, re-show directly
|
|
} else {
|
|
location.hash = id;
|
|
}
|
|
});
|
|
}
|
|
window.addEventListener("hashchange", () => show(active()));
|
|
|
|
show(active()); // initial render from the current hash
|
|
return { show, active };
|
|
}
|