From 8eab8b562bb5daf462a99b8264371d2919c4aea4 Mon Sep 17 00:00:00 2001 From: julioest Date: Wed, 21 Oct 2026 17:25:32 -0400 Subject: [PATCH] Move the gateway nav back into the header The primary nav returns to the header strip as a row between the profile switcher and the right cluster, instead of a column beside the content. The cells, groups, and colors stay as they are. Under 1024px the nav takes its own row below the switcher and actions so all seven links stay visible, and under 710px it shows only icons. --- .../ui/src/components/tab-bar.test.mjs | 47 ++++++++++------- .../config-ui/ui/src/components/tab-bar.ts | 21 ++++---- crates/gateway/config-ui/ui/src/main.ts | 10 ++-- .../config-ui/ui/src/styles/layout.css | 51 ++++++++++++------- 4 files changed, 76 insertions(+), 53 deletions(-) diff --git a/crates/gateway/config-ui/ui/src/components/tab-bar.test.mjs b/crates/gateway/config-ui/ui/src/components/tab-bar.test.mjs index 9fcac64db..d5e95352b 100644 --- a/crates/gateway/config-ui/ui/src/components/tab-bar.test.mjs +++ b/crates/gateway/config-ui/ui/src/components/tab-bar.test.mjs @@ -1,8 +1,8 @@ // Pins the tab bar: the right-side actions cluster shows the baked crate // version (or the dev fallback) as a muted `vX.Y.Z` span, and the seven -// tabs are Cursor's settings nav - a sticky left column of grouped links -// that marks the current route, keeps a label for every icon, and -// collapses to a 40px icon-only column under 710px. +// tabs are the primary nav - a row of grouped links in the header strip +// that marks the current route, keeps a label for every icon, and shows +// only the icons under 710px. import assert from "node:assert/strict"; import test from "node:test"; @@ -10,6 +10,7 @@ import { bundledDeclarations, squash } from "../css-support.mjs"; import { bootApp, gatewayStub, modelsFixture, navigate, settle } from "../test-support.mjs"; const NARROW = "(max-width: 709.98px)"; +const MID = "(max-width: 1023.98px)"; async function openDesk() { const stub = gatewayStub({ key: "k", config: modelsFixture() }); @@ -27,11 +28,13 @@ test("the tab bar shows the baked version in the actions cluster", async () => { ); }); -test("the primary nav is a column beside the content, with a divider between each of three groups", async () => { +test("the primary nav sits in the header strip, with a divider between each of three groups", async () => { const { root } = await openDesk(); - const nav = root.querySelector(".desk-layout > nav.tab-nav"); - assert.ok(nav, "the nav sits in the desk layout, not in the header strip"); + const nav = root.querySelector("header.tab-bar > nav.tab-nav"); + assert.ok(nav, "the nav sits in the header strip"); assert.equal(nav.getAttribute("aria-label"), "Primary"); + assert.equal(root.querySelector(".desk-layout nav"), null, "no nav beside the content"); + assert.ok(nav.nextElementSibling.matches(".tab-actions"), "the right cluster follows the nav"); assert.ok( root.querySelector(".desk-layout > .desk-content > main#main"), "the content column holds main", @@ -64,7 +67,7 @@ test("the nav marks exactly the current route and follows the hash", async () => } }); -test("every nav cell has a 12px icon and a label that survives the icon-only column", async () => { +test("every nav cell has a 12px icon and a label that survives the icon-only row", async () => { const { root } = await openDesk(); const tabs = [...root.querySelectorAll(".tab-nav .tab")]; assert.equal(tabs.length, 7); @@ -76,18 +79,17 @@ test("every nav cell has a 12px icon and a label that survives the icon-only col assert.equal(icon.getAttribute("aria-hidden"), "true"); const label = tab.querySelector(".tab-label"); assert.ok(label && label.textContent !== "", "the label stays in the DOM for assistive tech"); - assert.equal(tab.getAttribute("title"), label.textContent, "the icon-only column keeps a tooltip"); + assert.equal(tab.getAttribute("title"), label.textContent, "the icon-only row keeps a tooltip"); } }); -test("the nav column is sticky and clamp-wide, 48px from the content, and icon-only under 710px", async () => { - const layout = await bundledDeclarations(".desk-layout"); - assert.equal(layout.get("display"), "flex"); - assert.equal(squash(layout.get("gap")), "48px", "a 48px gap to the content"); - +test("the nav is a row with vertical dividers that takes its own row under 1024px and is icon-only under 710px", async () => { const nav = await bundledDeclarations(".tab-nav"); - assert.equal(nav.get("position"), "sticky"); - assert.equal(squash(nav.get("width")), "clamp(100px,25%,200px)"); + assert.equal(nav.get("display"), "flex"); + assert.equal(nav.get("flex-direction"), undefined, "a row, the flex default"); + assert.equal(nav.get("position"), undefined, "no longer a sticky column"); + assert.equal(nav.get("overflow-x"), "auto", "a row that still runs out of room scrolls"); + assert.equal(nav.get("min-width"), "0", "the row can shrink inside the strip"); const cell = await bundledDeclarations(".tab"); assert.equal(squash(cell.get("padding")), "4px6px", "cells are padded 4px 6px"); @@ -100,11 +102,20 @@ test("the nav column is sticky and clamp-wide, 48px from the content, and icon-o assert.equal(hover.get("background"), "var(--cursor-bg-quaternary)", "6% hover fill"); assert.equal(selected.get("background"), "var(--cursor-bg-quaternary)", "6% selected fill"); + const cellRule = await bundledDeclarations(".tab"); + assert.equal(cellRule.get("white-space"), "nowrap", "a label never wraps inside its cell"); + const focus = await bundledDeclarations(".tab:focus-visible"); + assert.equal(focus.get("outline-offset"), "-2px", "the focus ring stays inside the scrolling row"); + const divider = await bundledDeclarations(".tab-divider"); - assert.equal(squash(divider.get("border-top")), "1pxsolidvar(--cursor-stroke-tertiary)"); + assert.equal(squash(divider.get("border-left")), "1pxsolidvar(--cursor-stroke-tertiary)"); + assert.equal(divider.get("align-self"), "stretch", "a vertical hairline the height of the row"); + const midBar = await bundledDeclarations(".tab-bar", { media: MID }); + assert.equal(midBar.get("flex-wrap"), "wrap", "under 1024px the strip wraps"); + const midNav = await bundledDeclarations(".tab-nav", { media: MID }); + assert.equal(midNav.get("order"), "1", "the nav moves after the actions"); + assert.equal(midNav.get("flex-basis"), "100%", "onto its own full-width row"); - const narrow = await bundledDeclarations(".tab-nav", { media: NARROW }); - assert.equal(narrow.get("width"), "40px", "a 40px column"); const label = await bundledDeclarations(".tab-label", { media: NARROW }); assert.equal(squash(label.get("clip-path")), "inset(50%)", "the label hides but stays readable"); }); diff --git a/crates/gateway/config-ui/ui/src/components/tab-bar.ts b/crates/gateway/config-ui/ui/src/components/tab-bar.ts index 8e151a0ef..4635fb64c 100644 --- a/crates/gateway/config-ui/ui/src/components/tab-bar.ts +++ b/crates/gateway/config-ui/ui/src/components/tab-bar.ts @@ -1,10 +1,9 @@ // The tab bar: a slim header strip with the medallion (standalone only), -// the profile switcher, and the right cluster holding the connection dot -// [Adapted: llama-swap] plus the container the Apply/Revert pair mounts -// into; and, as its own element, Cursor's settings nav - seven icon+label -// links in three groups, set in a sticky left column that drops its labels -// and becomes a 40px icon column in a narrow window. The caller mounts the -// strip above the desk and the nav beside the content. +// the profile switcher, the primary nav - seven icon+label links in three +// groups, in a row that drops its labels in a narrow window - and the +// right cluster holding the connection dot [Adapted: llama-swap] plus the +// container the Apply/Revert pair mounts into. The caller mounts the strip +// above the desk. import { Cloud, @@ -66,10 +65,8 @@ export interface TabBarOptions { /** The mounted tab bar and its live-update handles. */ export interface TabBar { - /** The `
` strip: medallion, profile switcher, actions. */ + /** The `
` strip: medallion, profile switcher, nav, actions. */ element: HTMLElement; - /** The `