mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 17:31:56 -10:00
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:
1 parent
5f53d31ec3
commit
87f81553d9
3 files changed
+85
-30
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user