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:
parent
7bf4cfbc4c
commit
88c386c96c
2 changed files with 55 additions and 46 deletions
|
|
@ -165,33 +165,22 @@
|
|||
.shell-nav-indicator.shell-nav-indicator-instant {
|
||||
transition: none;
|
||||
}
|
||||
/* Dynamic tab row — sits below `.shell-header`, its own sticky
|
||||
strip rather than folded into the header itself: the header's own
|
||||
sticky offset (`top: 0`) is already claimed, and a header that grows
|
||||
taller every time a tab opens would shift `.shell-nav`'s measured
|
||||
positions (`measureIndex`, Shell.tsx) out from under the sliding
|
||||
indicator mid-animation. `top` below is `.shell-header`'s own
|
||||
footprint — `0.75em` padding top+bottom either side of the
|
||||
`2.75em`-min-height nav row (`.shell-nav-link`, this file), so ~4.25em
|
||||
rounded up to 4.5em for the 1px border + a little slack — not
|
||||
measured, same fixed-estimate tradeoff `AgentTerminalPage.css`'s own
|
||||
comment makes for the same reason (not worth a ResizeObserver for one
|
||||
sticky offset), and the same 4.5em value that file's own height calc
|
||||
already uses for this header, so the two stay consistent with each
|
||||
other rather than drifting to two different guesses at one thing.
|
||||
Undershooting this would mean the sticky header (z-index 10, above
|
||||
this row's 9) visibly covers the top of the tab row once scrolled —
|
||||
worth erring high, not low, if the real value ever drifts. */
|
||||
/* Dynamic tabs group — inline in `.shell-nav` itself, next to the
|
||||
static route links (mara, PR review: "the dynamic tab should be in
|
||||
the top bar, not a new one below" — an earlier cut had this as its
|
||||
own sticky row under the header; that version and the reasoning for
|
||||
it are still in this file's git history if the split ever needs to
|
||||
come back). `margin-left: 0.5em` + `border-left` reads as "a group
|
||||
attached to the nav, not another nav item" without a second landmark
|
||||
or a fixed pixel gap that'd fight `.shell-nav`'s own `gap`. */
|
||||
.shell-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
padding: 0.4em 1.25em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-elev);
|
||||
position: sticky;
|
||||
top: 4.5em;
|
||||
z-index: 9; /* one below .shell-header's 10, so the header still wins if both ever overlap during a fast scroll */
|
||||
margin-left: 0.5em;
|
||||
padding-left: 0.75em;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.shell-tab {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue