From a06eaa3d9d87ca05d633f50dc4c04d0fb2e05fae Mon Sep 17 00:00:00 2001 From: bkennedy Date: Tue, 30 Jun 2026 15:20:43 +0000 Subject: [PATCH 1/2] feat(nav): PROG-79 replace overflowing mobile top nav with a bottom tab bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a phone the header packed the logo, six text nav links, the New menu, and the avatar into one row, which overflowed and scrolled sideways. Swap the navigation pattern on small screens rather than dropping destinations. - Below `sm`, hide the header's inline nav and show a fixed bottom tab bar (MobileTabBar): Board · Outline · Agenda · Search as tabs, with a More tab whose sheet holds Structure · Archive (the iOS 5-slot pattern). At `sm`+ the bar is hidden and the inline nav returns unchanged. The mobile header is now just logo + New + avatar, so it can't overflow. - Keep the active page obvious: the current tab's icon + label light in the adobe accent, and More lights when its sheet's page is current. - Extract the nav destinations into a shared nav.tsx (adds an icon per item and a `primary` flag) imported by both the desktop header and the tab bar, so the two can't drift. - The tab bar uses the existing pwa-safe-bottom inset to clear the iOS home indicator; main gains mobile bottom padding (pb-24) and the toast stack + PWA install card float above the bar on phones. Verified in a phone-viewport browser pass: header width equals the 390px viewport (no horizontal scroll), the active tab follows navigation, the More sheet lists Structure/Archive and lights More on those pages, and the desktop inline nav is unchanged. --- src/client/App.tsx | 7 ++- src/client/Header.tsx | 17 ++---- src/client/MobileTabBar.tsx | 86 +++++++++++++++++++++++++++++ src/client/nav.tsx | 95 ++++++++++++++++++++++++++++++++ src/client/pwa/InstallPrompt.tsx | 2 +- src/client/toast.tsx | 4 +- 6 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 src/client/MobileTabBar.tsx create mode 100644 src/client/nav.tsx diff --git a/src/client/App.tsx b/src/client/App.tsx index f94a990..9c5af18 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,6 +1,7 @@ import { Link, Route, Switch } from "wouter"; import CommandLayer from "./commands/CommandLayer"; import Header from "./Header"; +import MobileTabBar from "./MobileTabBar"; import InstallPrompt from "./pwa/InstallPrompt"; import SignIn from "./SignIn"; import { UnauthenticatedError, useWorkspace } from "./store"; @@ -36,8 +37,9 @@ export default function App() { {/* Wide shell for the board; narrow pages re-constrain themselves. Tighter padding on phones — the board needs the width. Top gap is kept small so content sits just under the sticky header, with roomier - bottom padding for scroll breathing room (PROG-69). */} -
+ bottom padding for scroll breathing room (PROG-69). The extra mobile + bottom padding (pb-24) clears the fixed bottom tab bar (PROG-79). */} +
{/* Initial app load: the only permitted loading state (SPEC §8.2). */} {isPending &&

Loading workspace…

} {error &&

{String(error)}

} @@ -87,6 +89,7 @@ export default function App() { )}
+ {workspace && } diff --git a/src/client/Header.tsx b/src/client/Header.tsx index 72a3ecf..654feb7 100644 --- a/src/client/Header.tsx +++ b/src/client/Header.tsx @@ -7,6 +7,7 @@ import { useState } from "react"; import { Link, useLocation } from "wouter"; import { openCreateContainer, openCreateIssue, type ContainerDialogRequest } from "./commands/controller"; +import { NAV } from "./nav"; import { useWorkspaceSlice } from "./store"; // End the session, then reload — an unauthenticated load bounces to sign-in. @@ -18,17 +19,6 @@ async function signOut() { } } -type NavItem = { href: string; label: string; match: (path: string) => boolean }; - -const NAV: NavItem[] = [ - { href: "/", label: "Board", match: (p) => p === "/" }, - { href: "/outline", label: "Outline", match: (p) => p.startsWith("/outline") }, - { href: "/agenda", label: "Agenda", match: (p) => p.startsWith("/agenda") }, - { href: "/search", label: "Search", match: (p) => p.startsWith("/search") }, - { href: "/structure", label: "Structure", match: (p) => p.startsWith("/structure") }, - { href: "/archive", label: "Archive", match: (p) => p.startsWith("/archive") }, -]; - export default function Header() { const [path] = useLocation(); const [menuOpen, setMenuOpen] = useState(false); @@ -53,7 +43,10 @@ export default function Header() { Progress -