refactor(frontend): add createTabStrip + migrate logs sub-tabs (#1464 step 1)

Adds @hive/shared/tabs.js — createTabStrip(tabbar, {defaultId, onShow}):
a hash-routed tab strip that resolves controls by `[data-tab="<id>"]`
inside the passed container and panels by `[data-tab-pane="<id>"]`,
toggling `.hive-tab--active` + aria-selected + the panel's `hidden`, and
firing onShow(id) for per-tab side-effects. Always hash-routed (deep-
linkable + back/forward), no element-resolver callbacks (convention over
config), per the #1464 design review.

Wires it up: @hive/shared exports `./tabs.js` + `./tabs.css`, and
common.css @imports tabs.css (component structure, not a swap target, so
inlining is fine — unlike theme.css).

Migrates the logs sub-tabs as the first consumer: markup uses
`.hive-tab`/`data-tab`/`data-tab-pane`, logs.css drops the duplicated
base styles (keeps only its `flex:1` layout delta), and logs.js swaps its
activeTab/showTab/hashchange for createTabStrip (onShow lazy-loads the
SYSTEM tab). Behaviour-preserving. aria-selected is now standardised.
This commit is contained in:
iris 2026-06-08 22:03:39 +02:00
commit e1c276a06e
7 changed files with 103 additions and 60 deletions

View file

@ -22,37 +22,18 @@
import {
$, el, fmtAgeSecs, openStream, initServerWarnings,
} from './common.js';
import { createTabStrip } from '@hive/shared/tabs.js';
(() => {
initServerWarnings();
// ─── tab routing ──────────────────────────────────────────────────────
const TABS = ['build', 'agent', 'system'];
function activeTab() {
const hash = location.hash.replace('#', '');
return TABS.includes(hash) ? hash : 'build';
}
function showTab(name) {
for (const t of TABS) {
const pane = document.getElementById('logs-pane-' + t);
const tab = document.getElementById('logs-tab-' + t);
const active = t === name;
if (pane) pane.hidden = !active;
if (tab) {
tab.classList.toggle('logs-tab-active', active);
tab.setAttribute('aria-selected', String(active));
}
}
}
window.addEventListener('hashchange', () => {
const tab = activeTab();
showTab(tab);
if (tab === 'system') fetchSystem();
});
// The shared hash-routed tab strip (@hive/shared/tabs.js) handles the
// active toggle + aria-selected + pane visibility + the SYSTEM lazy
// fetch via onShow. Constructed in the init block below (after the
// fetch fns + element refs it depends on exist). `logTabs.active()`
// replaces the old `activeTab()`.
let logTabs;
// ─── helpers ──────────────────────────────────────────────────────────
@ -252,7 +233,7 @@ import {
{
let buildRefreshTimer = null;
const debouncedRefreshBuild = () => {
if (activeTab() !== 'build') return;
if (logTabs.active() !== 'build') return;
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
buildRefreshTimer = setTimeout(fetchBuild, 2000);
};
@ -389,11 +370,15 @@ import {
// ─── init ─────────────────────────────────────────────────────────────
const tab = activeTab();
showTab(tab);
// Wire the shared tab strip now that fetchSystem + the element refs it
// needs are defined. Its initial show() paints the active pane and, if
// the deep-linked tab is SYSTEM, kicks off the lazy fetch via onShow.
logTabs = createTabStrip(document.getElementById('logs-tabbar'), {
defaultId: 'build',
onShow: (id) => { if (id === 'system') fetchSystem(); },
});
loadAgentList();
fetchBuild();
if (tab === 'system') fetchSystem();
// Tick the last-fetched timestamps every 30s so "fetched 1m ago" stays
// accurate without a manual refresh.