import { AppShell, Box, Group, NavLink, ScrollArea, Stack, Text, UnstyledButton, } from "@mantine/core"; import { ChevronDown, X } from "lucide-react"; import { type ReactNode, useCallback, useEffect, useMemo, useState, } from "react"; import type { SidebarItem, SidebarSection } from "./types"; export interface FreightSidebarProps { sections: SidebarSection[]; activeHref?: string; onNavigate?: (href: string) => void; /** Close handler for the mobile drawer (X button, hidden on desktop). */ onClose?: () => void; } const BRAND_LOGO = "/assets/logo.svg"; // Active / inactive NavLink styling, expressed through the shared edr-* theme // tokens (bridged into Tailwind in index.css). Items are pills floating on the // page background — the navbar itself has no surface of its own. const navClassNames = (active: boolean) => active ? { root: "rounded-md transition-all duration-150 bg-edr-soft! ring-1 ring-inset ring-edr-primary/40 [&_svg]:size-[16px]", label: "text-edr-primary-dark! font-medium! text-sm!", section: "text-edr-primary-dark!", } : { root: "rounded-md transition-all duration-150 hover:bg-[#EEF2F6]! [&_svg]:size-4", label: "text-edr-text! font-medium! text-sm! hover:text-edr-ink!", section: "text-edr-text!", }; const itemKey = (parentKey: string, item: SidebarItem, index: number) => `${parentKey}/${item.href ?? item.label}/${index}`; const collectHrefs = (items: SidebarItem[]): string[] => items.flatMap((item) => [ ...(item.href ? [item.href.toLowerCase()] : []), ...(item.children?.length ? collectHrefs(item.children) : []), ]); const FreightSidebar = ({ sections, activeHref, onNavigate, onClose, }: FreightSidebarProps) => { const activePath = activeHref?.toLowerCase() ?? ""; const isHrefActive = useCallback( (href: string) => { const normalized = href.toLowerCase(); return activePath === normalized || activePath.startsWith(`${normalized}/`); }, [activePath], ); const branchActive = useCallback( (items: SidebarItem[]) => collectHrefs(items).some(isHrefActive), [isHrefActive], ); // Branches containing the active route start expanded; manual toggles win // afterwards (merge keeps user intent while still opening newly-active paths). const defaultOpen = useMemo(() => { const acc: Record = {}; const walk = (items: SidebarItem[], parentKey: string) => { items.forEach((item, i) => { if (!item.children?.length) return; const key = itemKey(parentKey, item, i); acc[key] = (item.href ? isHrefActive(item.href) : false) || branchActive(item.children); walk(item.children, key); }); }; sections.forEach((section) => walk(section.items, section.title)); return acc; }, [sections, isHrefActive, branchActive]); const [openMap, setOpenMap] = useState(defaultOpen); useEffect(() => { setOpenMap((current) => ({ ...defaultOpen, ...current })); }, [defaultOpen]); const toggle = useCallback( (key: string) => setOpenMap((m) => ({ ...m, [key]: !m[key] })), [], ); const renderItem = useCallback( (item: SidebarItem, key: string): ReactNode => { const hasChildren = !!item.children?.length; if (hasChildren) { const isLink = !!item.href; const active = (isLink ? isHrefActive(item.href!) : false) || branchActive(item.children!); const isOpen = openMap[key] ?? false; return ( // `opened` is controlled so a link-parent navigates on row click // without collapsing; the chevron is the only toggle affordance. toggle(key)} rightSection={ { e.preventDefault(); e.stopPropagation(); toggle(key); }} className="flex cursor-pointer items-center" > } > {item.children!.map((child, i) => renderItem(child, itemKey(key, child, i)), )} ); } if (!item.href) return null; const active = isHrefActive(item.href); return ( onNavigate?.(item.href!)} /> ); }, [branchActive, isHrefActive, onNavigate, openMap, toggle], ); const renderedSections = useMemo( () => sections.map((section) => ( {section.title} {section.items.map((item, i) => renderItem(item, itemKey(section.title, item, i)), )} )), [renderItem, sections], ); return ( {/* Brand — aligns with the 64px header for a continuous top edge */} EDR Freight EDR Freight Backoffice Console {onClose && ( )} {/* Nav */} {renderedSections} ); }; export default FreightSidebar;