Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: move dynamic agent-term tabs into the top nav bar

mara, PR review: "the dynamic tab should be in the top bar, not a new
one below". Moves the .shell-tabs group from its own sticky row under
the header into .shell-nav itself, right after the nav indicator.

Also adds a third re-measure effect for the sliding nav indicator,
keyed on tabs.length: with tabs inline in the same flex row the
indicator measures, closing a background tab (no navigation) can
shrink the row without the hop effect's own re-measure ever firing.
Same reflow-not-navigation reasoning as the existing resize-listener
effect.
This commit is contained in:
iris 2026-09-28 22:17:10 +02:00 • committed by mara
commit 88c386c96c
2 changed files with 55 additions and 46 deletions

View file

@ -214,9 +214,13 @@ export function Shell({ children }: { children: ComponentChildren }) {
// Every subsequent change (a real navigation) still animates normally.
const [indicatorSettledOnce, setIndicatorSettledOnce] = useState(false);
// Open agent-terminal tabs — the row below `.shell-nav` that
// grows/shrinks as tabs open/close, entirely separate from the fixed
// `NAV_ITEMS` list above.
// Open agent-terminal tabs — rendered inline in `.shell-nav` itself
// (mara, PR review: "the dynamic tab should be in the top bar, not a
// new one below" — an earlier cut had these as their own row under
// the header; see the git history on this file for that version and
// why it was reverted). Entirely separate from the fixed `NAV_ITEMS`
// list above, so they get their own small component (`DynamicTabLink`)
// rather than reusing `NavLink`.
const { tabs, close: closeTab } = useDynamicTabs();
// Applied once here, not inside `SettingsMenu` — every route mounts
@ -313,6 +317,18 @@ export function Shell({ children }: { children: ComponentChildren }) {
return () => window.removeEventListener("resize", onResize);
}, []);
// Same re-measure, for the same "a reflow, not a navigation" reason —
// now that the dynamic tabs render inline in `.shell-nav` (see the
// `useDynamicTabs()` comment above), *closing* a background tab (one
// that isn't the active route) shrinks the row without any navigation
// firing to trigger the hop effect's own re-measure. Opening one
// doesn't need this: `useDynamicTabs().open()` always navigates, so
// the hop effect already covers that case.
useEffect(() => {
if (settledIndexRef.current === -1) return;
setIndicator(measureIndex(settledIndexRef.current));
}, [tabs.length]);
// Fetched once here, not per-page: every route mounts inside one
// `<Shell>`, and the swarm's name doesn't change within a page
// visit. A fetch failure is silently ignored — `swarmName` just stays
@ -387,6 +403,30 @@ export function Shell({ children }: { children: ComponentChildren }) {
backgroundColor: indicator?.accent ?? "transparent",
}}
/>
{/* Open agent-terminal tabs — inline in the same nav row as
the static routes (mara's review call, see the
`useDynamicTabs()` comment above). Only rendered when
there's something to show — an always-present empty group
would just be dead chrome for the far more common case of
no open terminal tabs. `role="group"`, not a second `<nav>`:
there's already exactly one nav landmark in this header. */}
{tabs.length > 0 && (
<div
class="shell-tabs"
role="group"
aria-label="open agent terminals"
>
{tabs.map((tab) => (
<DynamicTabLink
key={tab.href}
href={tab.href}
label={tab.label}
active={tab.href === location}
onClose={() => closeTab(tab.href)}
/>
))}
</div>
)}
</nav>
{/* Single `margin-left: auto` on the wrapper, not on each menu
individually — two adjacent flex items both set to
@ -398,26 +438,6 @@ export function Shell({ children }: { children: ComponentChildren }) {
<UserMenu />
</div>
</header>
{/* Second row, only when there's something to show — an always-
present empty bar would just be dead chrome for the far more
common case of no open terminal tabs. Own `<nav>`, not folded
into `.shell-nav` above: these come and go independently of
the static route list and shouldn't shift `NAV_ITEMS`'
measured positions (`measureIndex` above) by changing the
header's height mid-sweep. */}
{tabs.length > 0 && (
<nav class="shell-tabs" aria-label="open agent terminals">
{tabs.map((tab) => (
<DynamicTabLink
key={tab.href}
href={tab.href}
label={tab.label}
active={tab.href === location}
onClose={() => closeTab(tab.href)}
/>
))}
</nav>
)}
{/* Keyed by route so it remounts (and replays its entrance
animation) on every navigation — see the file-top comment.
`shell-body-wide` is additive (see `WIDE_BODY_ROUTES` above),