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 `