shell-redesign V1 attempt-3 Phase 2 BOUNCE-FIX: Desktop top bar

Founder walk on Phase 2 commit 5f53d31 surfaced one structural addition:
desktop top bar with brand mark, symmetric with mobile. universal-shell.md
L11–L65 updated upstream — new "Desktop top bar" section L40–L53 makes
the top bar an explicit canonical region (h-14, OPENRIG brand left,
right-slot reserved, fixed, doesn't scroll with center).

Approach (a) per dispatch — single source of truth across viewports:

AppShell.tsx:
- Renamed testid app-mobile-topbar → app-topbar (top bar is now universal,
  not mobile-only).
- Removed lg:hidden from <header> so the SAME element renders at all sizes.
- Hamburger button keeps its own lg:hidden (mobile-only).
- Renamed brand-home-link-mobile → brand-home-link (single Link element).
- Added right-slot with minimal V1 'localhost' env indicator
  (font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant);
  hidden on narrow viewports (sm:flex) to preserve mobile space.

SharedDetailDrawer.tsx:
- Drawer fixed positioning adjusted: 'top-0 right-0 z-30 h-full' →
  'top-14 right-0 bottom-0 z-30' so drawer starts below the top bar.
  bottom-0 anchor avoids needing a calc() expression for height.

Existing positioning unchanged — Rail / Explorer / Center are inside the
inner <div className="flex flex-1 min-h-0 relative"> below the header,
positioned relative to that flex parent (NOT viewport), so adding the
top bar to the flex column automatically pushes them down by h-14 with
no further offsets needed.

Tests (test/app-shell.test.tsx):
- Updated SC-8 describe block (split into Top bar + Mobile rail collapse).
- Added 5 top-bar tests:
  * top bar renders at desktop (single source of truth — no lg:hidden)
  * brand link visible at desktop with href='/' (Dashboard)
  * right-slot env indicator present (V1 = 'localhost')
  * hamburger button is mobile-only (lg:hidden) — preserved Phase 2 behavior
  * legacy app-mobile-topbar / brand-home-link-mobile testids GONE
- Existing SC-1 strict count test unchanged and still passes (top bar is
  a horizontal region, NOT a left chrome).

Verification gates:
- tsc --noEmit: clean
- pnpm test: 597/598 PASS (single failure is pre-existing
  test/library-review.test.tsx > Send-to-seat modal; unrelated)
- pnpm build: ✓ in 1.96s
- agent-browser walk at 1440x900 /topology:
  * document.querySelectorAll('nav, aside').length === 2 (SC-1 unchanged)
  * data-testid='app-topbar' present
  * brand-home-link textContent = 'OPENRIG'; href = '/'
  * topbar-env-indicator textContent = 'localhost'
  * Top bar visible above rail+explore+center; thick black bottom border;
    paper-grid preserved.

ETA realized: ~25 min (within 25–30 estimate).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-06 12:29:21 -07:00
1 parent 5f53d31ec3
commit 87f81553d9
3 files changed
+85 -30

No files matched your search

+40 -21
View File
@@ -391,30 +391,49 @@ export function AppShell({ children }: AppShellProps) {
}}
>
<div className="h-screen flex flex-col">
{/* Mobile top-bar (lg:hidden) — hamburger + brand. Desktop has no top-bar. */}
{/* Top bar — universal across viewports per universal-shell.md
L40–L53. Single source of truth: same element renders at
all sizes. Hamburger button keeps its own lg:hidden so it
only appears at narrow viewports; brand mark + right-slot
stay visible everywhere. */}
<header
data-testid="app-mobile-topbar"
className="h-14 flex items-center justify-between px-4 bg-background border-b-2 border-stone-900 shrink-0 relative z-30 lg:hidden"
data-testid="app-topbar"
className="h-14 flex items-center justify-between px-4 bg-background border-b-2 border-stone-900 shrink-0 relative z-30"
>
<button
type="button"
data-testid="mobile-menu-toggle"
onClick={() => setExplorerOpen((open) => !open)}
aria-label="Toggle navigation"
className="flex flex-col gap-[3px] p-2"
<div className="flex items-center gap-3">
<button
type="button"
data-testid="mobile-menu-toggle"
onClick={() => setExplorerOpen((open) => !open)}
aria-label="Toggle navigation"
className="flex flex-col gap-[3px] p-2 lg:hidden"
>
<span className="block w-4 h-[1.5px] bg-stone-900" />
<span className="block w-4 h-[1.5px] bg-stone-900" />
<span className="block w-3 h-[1.5px] bg-stone-900" />
</button>
<Link
to="/"
data-testid="brand-home-link"
className="inline-flex items-center bg-stone-950 px-3 py-1 font-mono text-sm font-bold uppercase tracking-[0.08em] text-stone-50 hover:bg-stone-800"
>
OPENRIG
</Link>
</div>
{/* Right-slot — reserved for V2 global affordances. V1 carries
a minimal env indicator. Hidden on narrow viewports to
preserve mobile space. */}
<div
data-testid="topbar-right-slot"
className="hidden sm:flex items-center gap-2"
>
<span className="block w-4 h-[1.5px] bg-stone-900" />
<span className="block w-4 h-[1.5px] bg-stone-900" />
<span className="block w-3 h-[1.5px] bg-stone-900" />
</button>
<Link
to="/"
data-testid="brand-home-link-mobile"
className="inline-flex items-center bg-stone-950 px-3 py-1 font-mono text-sm font-bold uppercase tracking-[0.08em] text-stone-50"
>
OPENRIG
</Link>
<span className="w-8" /> {/* spacer to balance hamburger */}
<span
data-testid="topbar-env-indicator"
className="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant"
>
localhost
</span>
</div>
</header>
{/* Main: rail + explore + center + drawer */}
@@ -96,7 +96,10 @@ export function SharedDetailDrawer({
width="wide"
onClose={onClose}
testId="shared-detail-drawer"
className="fixed top-0 right-0 z-30 h-full"
// top-14 starts below the universal top bar (h-14, fixed at top); bottom-0
// anchors to viewport bottom so the drawer fills the remaining height
// without needing a calc() expression.
className="fixed top-14 right-0 bottom-0 z-30"
>
{inner}
</VellumSheet>
+41 -8
View File
@@ -187,18 +187,51 @@ describe("AppShell — Phase 2 chrome", () => {
});
});
describe("SC-8: mobile rail collapses to top-bar menu", () => {
it("mobile top-bar (hamburger + brand) is rendered with lg:hidden", async () => {
describe("Top bar — universal-shell.md L40–L53 (Phase 2 bounce-fix)", () => {
it("top bar renders at desktop (single source of truth — no lg:hidden)", async () => {
const { container } = await renderAt("/");
const topbar = container.querySelector("[data-testid='app-mobile-topbar']") as HTMLElement;
const topbar = container.querySelector("[data-testid='app-topbar']") as HTMLElement;
expect(topbar).toBeTruthy();
expect(topbar.className).toContain("lg:hidden");
const hamburger = container.querySelector("[data-testid='mobile-menu-toggle']");
expect(hamburger).toBeTruthy();
// Single source of truth — top bar is universal, NOT lg:hidden.
expect(topbar.className).not.toContain("lg:hidden");
expect(topbar.className).toContain("h-14");
});
it("brand link visible at desktop and links to / (Dashboard)", async () => {
const { container } = await renderAt("/topology");
const brand = container.querySelector("[data-testid='brand-home-link']") as HTMLAnchorElement;
expect(brand).toBeTruthy();
expect(brand.getAttribute("href")).toBe("/");
expect(brand.textContent).toContain("OPENRIG");
});
it("right-slot env indicator present (V1 = 'localhost')", async () => {
const { container } = await renderAt("/");
const envIndicator = container.querySelector(
"[data-testid='topbar-env-indicator']",
) as HTMLElement;
expect(envIndicator).toBeTruthy();
expect(envIndicator.textContent).toContain("localhost");
});
it("hamburger button is mobile-only (lg:hidden) — preserved Phase 2 behavior", async () => {
const { container } = await renderAt("/");
const hamburger = container.querySelector(
"[data-testid='mobile-menu-toggle']",
) as HTMLElement;
expect(hamburger).toBeTruthy();
expect(hamburger.className).toContain("lg:hidden");
});
it("legacy app-mobile-topbar testid is GONE (renamed to app-topbar)", async () => {
const { container } = await renderAt("/");
expect(container.querySelector("[data-testid='app-mobile-topbar']")).toBeNull();
expect(container.querySelector("[data-testid='brand-home-link-mobile']")).toBeNull();
});
});
describe("SC-8: mobile rail collapses to slide-over tray", () => {
it("mobile rail tray renders only at narrow viewport (conditional)", async () => {
// Switch to narrow viewport, then re-render.
const { router } = await import("../src/routes.js");
Object.defineProperty(window, "innerWidth", { configurable: true, value: 375, writable: true });
window.dispatchEvent(new Event("resize"));
@@ -206,7 +239,7 @@ describe("AppShell — Phase 2 chrome", () => {
const memoryRouter = createRouter({ routeTree: router.routeTree, history: memoryHistory });
const { container } = render(<RouterProvider router={memoryRouter} />);
await waitFor(() => {
const topbar = container.querySelector("[data-testid='app-mobile-topbar']") as HTMLElement;
const topbar = container.querySelector("[data-testid='app-topbar']") as HTMLElement;
expect(topbar).toBeTruthy();
});
const tray = container.querySelector("[data-testid='mobile-rail-tray']") as HTMLElement;