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}