From 3595cd54bb2aa1f1d4ee9143da60f4810584b145 Mon Sep 17 00:00:00 2001 From: sjcoetzeesj-coder Date: Tue, 27 Oct 2026 12:16:58 +0200 Subject: [PATCH] feat(nav): replace squeezed mobile tab bar with a header menu The mobile bottom bar rendered all 15 routes into a single row. On a 390px iPhone that works out at ~17px per tab: the icons overlap, every label truncates to a single letter and the tap targets sit far below the 44px minimum, so the tabs could not be read or hit reliably. Mobile now uses a hamburger in the header that opens a full-screen menu listing every section, grouped (Main / CIPC & BizPortal / SARS / Practice). The panel locks background scroll, closes on Escape or on navigation, and carries the theme toggle so the mobile chrome keeps parity with the desktop cluster. Desktop had the same problem in a different shape. The full 15-link row needed 1752px inside a 1280px container, so the page scrolled sideways even at 1920px (measured scrollWidth 2065 vs a 1920 viewport) and the right-hand cluster was pushed off screen. The inline nav now carries six primary links plus a "More" dropdown, which fits exactly. - NavBar: mobile tab bar replaced by the header menu panel; desktop nav reduced to primary links + More dropdown; grouped list shared by both so the two surfaces cannot drift apart. - RouteChrome: dropped the pb-24 md:pb-0 padding that only existed to clear the old bottom bar. - .gitignore / Workflow/.dockerignore: ignore .next-stale* so a stale build directory cannot bloat the Docker build context. --- .gitignore | 9 + Workflow/.dockerignore | 1 + Workflow/components/NavBar.tsx | 389 +++++++++++++++++++++++----- Workflow/components/RouteChrome.tsx | 2 +- 4 files changed, 330 insertions(+), 71 deletions(-) diff --git a/.gitignore b/.gitignore index 57a14e5..03154b7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # ----------------------------------------------- deps, build & tooling --- node_modules/ .next/ +.next-stale*/ out/ dist/ build/ @@ -29,6 +30,14 @@ npm-debug.log* scratch-*/ _backups/ +# --------------------------- CIPC report run state (generated) -------- +# The report explorer writes its run folder, produced PDFs and sample +# page renders at runtime — none of that belongs in the repo. +Workflow/.cipc-report/ +Workflow/reports/ +Workflow/scripts/one-id.txt +Workflow/scripts/cipc-report/_sample*/ + # --------------------------------- debug captures & one-off probe output --- # Ad-hoc dumps kept while debugging SARS / CIPC screens. Not project source. _probe*.json diff --git a/Workflow/.dockerignore b/Workflow/.dockerignore index b903cfb..0f8db9f 100644 --- a/Workflow/.dockerignore +++ b/Workflow/.dockerignore @@ -14,6 +14,7 @@ # --- deps, build output, git history ---------------------------------- node_modules .next +.next-stale* out dist build diff --git a/Workflow/components/NavBar.tsx b/Workflow/components/NavBar.tsx index c438d23..7c61e00 100644 --- a/Workflow/components/NavBar.tsx +++ b/Workflow/components/NavBar.tsx @@ -3,11 +3,17 @@ // center, secondary status on the right. Sticky on scroll with the // signature hairline border-bottom. // -// Mobile ( = Object.fromEntries( + LINKS.map((l) => [l.href, l]), +) as Record; + +/** Sections that stay one click away in the desktop header. */ +const DESKTOP_PRIMARY = [ + "/", + "/notes", + "/companies", + "/directors", + "/tasks", + "/compliance", +] as const; + +/** Every section, grouped — the shared source of truth for the mobile + * menu panel and the desktop "More" dropdown. */ +const MENU_GROUPS: { title: string; hrefs: string[] }[] = [ + { title: "Main", hrefs: ["/", "/tasks", "/directors", "/companies"] }, + { + title: "CIPC & BizPortal", + hrefs: ["/bizportal", "/bizportal-2", "/cipc", "/cipc-2", "/cipc-report"], + }, + { title: "SARS", hrefs: ["/sars-returns", "/sars-registrations"] }, + { title: "Practice", hrefs: ["/notes", "/groups", "/compliance", "/resolutions"] }, +]; + export default function NavBar() { const pathname = usePathname() ?? "/"; + const [menuOpen, setMenuOpen] = useState(false); + const [moreOpen, setMoreOpen] = useState(false); + const moreRef = useRef(null); + const closeRef = useRef(null); + + // Navigating away closes whichever surface was open. + useEffect(() => { + setMenuOpen(false); + setMoreOpen(false); + }, [pathname]); + + // The mobile panel is a modal surface: lock the page behind it. + useEffect(() => { + if (!menuOpen) return; + const previous = document.body.style.overflow; + document.body.style.overflow = "hidden"; + closeRef.current?.focus(); + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setMenuOpen(false); + }; + document.addEventListener("keydown", onKey); + return () => { + document.body.style.overflow = previous; + document.removeEventListener("keydown", onKey); + }; + }, [menuOpen]); + + // The desktop dropdown closes on an outside click or Escape. + useEffect(() => { + if (!moreOpen) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setMoreOpen(false); + }; + const onPointer = (e: MouseEvent) => { + if (!moreRef.current?.contains(e.target as Node)) setMoreOpen(false); + }; + document.addEventListener("keydown", onKey); + document.addEventListener("mousedown", onPointer); + return () => { + document.removeEventListener("keydown", onKey); + document.removeEventListener("mousedown", onPointer); + }; + }, [moreOpen]); + + const primaryLinks = LINKS.filter((l) => + (DESKTOP_PRIMARY as readonly string[]).includes(l.href), + ); return ( <> @@ -47,7 +129,7 @@ export default function NavBar() { data-od-id="navbar" className="sticky top-0 z-30 bg-surface border-b border-border" > -
+
- {LINKS.map((link) => { + {primaryLinks.map((link) => { const isActive = isActiveLink(pathname, link.href); return ( {link.label} - {"badge" in link && link.badge ? ( - - {link.badge} - - ) : null} ); })} + + {/* Overflow dropdown — the desktop counterpart of the + mobile menu panel. */} +
+ + + {moreOpen ? ( +
+ setMoreOpen(false)} /> +
+ ) : null} +
{/* Right-side desktop cluster: label + theme toggle. Hidden - on mobile — the bottom tab bar carries the brand context. */} + on mobile — the menu panel carries both. */}
- Registry Browser + + Registry Browser +
- {/* Spacer for mobile so the brand lockup stays left-aligned - and the bottom-bar height is the only vertical change. */} - - {/* Mobile bottom tab bar */} - + Y + + + Workflow + + + +
+
+ + setMenuOpen(false)} /> + +
+ Registry Browser + +
+ + ) : null} ); } +/** + * Grouped list of every section, shared by the mobile menu panel and + * the desktop "More" dropdown. + */ +function GroupedList({ onNavigate }: { onNavigate?: () => void }) { + const pathname = usePathname() ?? "/"; + return ( +
+ {MENU_GROUPS.map((group) => ( +
+

+ {group.title} +

+
    + {group.hrefs.map((href) => { + const link = LINK_BY_HREF[href]; + if (!link) return null; + const Icon = link.icon; + const active = isActiveLink(pathname, href); + return ( +
  • + + + + + + {link.label} + + {"badge" in link && link.badge ? ( + + {link.badge} + + ) : null} + + +
  • + ); + })} +
+
+ ))} +
+ ); +} + function isActiveLink(pathname: string, href: string): boolean { if (href === "/") return pathname === "/"; return pathname === href || pathname.startsWith(href + "/"); } +/** True when the current route lives behind the "More" dropdown. */ +function isOverflowActive(pathname: string): boolean { + return LINKS.some( + (l) => + !(DESKTOP_PRIMARY as readonly string[]).includes(l.href) && + isActiveLink(pathname, l.href), + ); +} + /* ─── Icon set (inline SVG, stroke-currentColor, 1.6px stroke) ──── */ interface IconProps { @@ -201,7 +395,7 @@ function IconUsers({ className }: IconProps): ReactNode { viewBox="0 0 24 24" fill="none" stroke="currentColor" - strokeWidth={1.6} + strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" className={className} @@ -221,7 +415,7 @@ function IconBuilding({ className }: IconProps): ReactNode { viewBox="0 0 24 24" fill="none" stroke="currentColor" - strokeWidth={1.6} + strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" className={className} @@ -407,6 +601,61 @@ function IconNotepad({ className }: IconProps): ReactNode { ); } +/** Hamburger glyph for the mobile menu button. */ +function IconMenu({ className }: IconProps): ReactNode { + return ( + + ); +} + +/** Close glyph for the mobile menu panel. */ +function IconClose({ className }: IconProps): ReactNode { + return ( + + ); +} + +/** Right-chevron used as the list affordance in the grouped lists. */ +function IconChevronRight({ className }: IconProps): ReactNode { + return ( + + ); +} + /* ─── Theme toggle ────────────────────────────────────────────────── */ /** diff --git a/Workflow/components/RouteChrome.tsx b/Workflow/components/RouteChrome.tsx index 4a61c83..4e1ed65 100644 --- a/Workflow/components/RouteChrome.tsx +++ b/Workflow/components/RouteChrome.tsx @@ -49,7 +49,7 @@ export default function RouteChrome({ children }: { children: ReactNode }) { return ( <> -
+
{children}