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:
parent
55ee52b213
commit
e1c276a06e
7 changed files with 103 additions and 60 deletions
|
|
@ -6,7 +6,7 @@
|
|||
so this inherits theme-swap safety for free. Call sites keep a thin
|
||||
variant class for size/spacing deltas; richer affordances (count
|
||||
pills, the dashboard's responsive overflow ⋮ menu) layer on top in the
|
||||
per-page CSS. See docs/web-ui/css-vars.md + #1464. */
|
||||
per-page CSS. See docs/web-ui/css-vars.md. */
|
||||
|
||||
.hive-tabbar {
|
||||
display: flex;
|
||||
|
|
|
|||
71
frontend/packages/shared/src/tabs.js
Normal file
71
frontend/packages/shared/src/tabs.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// 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 };
|
||||
}
|
||||
Loading…
Reference in a new issue