// components/NavBar.tsx // Top-of-page navigation. Logo lockup on the left, primary nav // 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 ( <>
Workflow {/* Right-side desktop cluster: label + theme toggle. Hidden on mobile — the menu panel carries both. */}
Registry Browser
{/* Mobile: hamburger opens the full-screen menu. */}
{/* Mobile full-screen menu panel. */} {menuOpen ? (
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 { className?: string; /** Active state hints — used to swap fill behavior on the home icon * and to thicken the stroke on outlined icons. Always defaults to * the brand "muted" rendering since the active state is owned by * the parent (text color + scale already encodes it). */ active?: boolean; } function IconHome({ className, active }: IconProps): ReactNode { return ( ); } function IconUsers({ className }: IconProps): ReactNode { return ( ); } function IconBuilding({ className }: IconProps): ReactNode { return ( ); } function IconDocument({ className }: IconProps): ReactNode { return ( ); } function IconClipboard({ className }: IconProps): ReactNode { return ( ); } function IconSearch({ className }: IconProps): ReactNode { return ( ); } function IconTag({ className }: IconProps): ReactNode { return ( ); } function IconCheckSquare({ className }: IconProps): ReactNode { return ( ); } /** * Tax-receipt glyph for the SARS Returns tab. Deliberately a torn- * edge receipt (zig-zag bottom) to distinguish it from IconDocument * (which is a sealed page) and IconClipboard (which has a clip). */ function IconReceipt({ className }: IconProps): ReactNode { return ( ); } /** * Registry / ID-badge glyph for the SARS Registrations tab. A card with * two horizontal ruled lines and a small "stamp" circle in the corner — * visually distinct from IconReceipt (torn-edge paper), IconNotepad * (folded top-right page), IconDocument (sealed page), and * IconClipboard (task list with a clip). */ function IconRegistry({ className }: IconProps): ReactNode { return ( ); } /** * Notepad glyph for the Notes tab. Rounded rectangle with a horizontal * ruled line and a folded top-right corner — distinguishes it from * IconClipboard (task list with a clip) and IconDocument (sealed page). */ function IconNotepad({ className }: IconProps): ReactNode { return ( ); } /** 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 ────────────────────────────────────────────────── */ /** * Sun/moon button. Uses next-themes' `resolvedTheme` so system * preference is honoured. The icon shown represents the mode you'd * switch TO (moon in light, sun in dark). * * `mounted` guard avoids SSR/CSR mismatch — `useTheme` returns * undefined on the first render. Until mounted, we render a neutral * placeholder so the button's box doesn't pop in late. */ function ThemeToggle() { const [mounted, setMounted] = useState(false); const { resolvedTheme, setTheme } = useTheme(); useEffect(() => setMounted(true), []); const isDark = mounted && resolvedTheme === "dark"; const next = isDark ? "light" : "dark"; return ( ); } function IconSun({ className }: IconProps): ReactNode { return ( ); } function IconMoon({ className }: IconProps): ReactNode { return ( ); }