"use client" /** * AppSidebar — single-column nav matching the design reference: * Exxat One header, primary links, "Documents" group, utilities, user. * * Collapsed (icon) chrome is driven only by CSS (`group-data-[collapsible=icon]:…`) * on the ancestor from `ui/sidebar` — the same DOM is always rendered so Radix * `useId()` order matches between SSR and hydration (fixes downstream menus). */ import * as React from "react" import { Link, useNavigate } from "react-router-dom" import { useLocation } from "react-router-dom" import { motion, useReducedMotion } from "motion/react" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Shortcut, } from "@/components/ui/dropdown-menu" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, useRegisterNavFlyoutToggle, useSidebar, } from "@/components/ui/sidebar" import { SidebarNavLabel } from "@/components/ui/sidebar-nav-label" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Badge } from "@/components/ui/badge" import { StatusBadge } from "@/components/ui/status-badge" import { Separator } from "@/components/ui/separator" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Tip } from "@/components/ui/tip" import { requestOpenCommandMenu } from "@/components/command-menu" import { Kbd, KbdGroup } from "@/components/ui/kbd" import { useModKeyLabel } from "@/hooks/use-mod-key-label" import { useLocationHash } from "@/hooks/use-location-hash" import { useSidebarReflowZoom } from "@/hooks/use-sidebar-reflow-zoom" import { useProduct, type Product } from "@/contexts/product-context" import { useProductSwitch } from "@/contexts/product-route-sync" import { productSlug } from "@/stores/app-store" import { isListedCustomProduct } from "@/stores/app-store" import { NavUser } from "./nav-user" import { useSecondaryPanel } from "./secondary-panel" import { SidebarDrillIn } from "./sidebar-drill-in" import { LeoSidebarDrillInPanel } from "./leo-sidebar-drill-in-panel" import { ExxatProductLogo, ExxatProductMark } from "@/components/exxat-product-logo" import { motionHeaderEnter } from "@/lib/motion-ui" import { customProductBrandConfig, productBrandLabel } from "@/lib/product-brand" import { isProductRefHidden, type ProductRef } from "@/lib/product-ref" import { isLibraryPrimaryListNavActive, LIBRARY_PRIMARY_LIST_NAV_KEY, } from "@/lib/library-nav" import { NAV_BY_PRODUCT, getPrimaryNavForProduct, getPrimaryNavLayoutForProduct, type NavPrimaryLayout, type NavSection, NAV_DOCUMENTS, NAV_DOCUMENTS_LABEL, NAV_SCHOOL_DEFAULT, NAV_PROGRAM_DEFAULT, NAV_QUICK_ACTIONS, NAV_SCHOOLS, NAV_SITE_DEFAULT, NAV_LOCATION_DEFAULT, NAV_SITES, NAV_SECONDARY, getSecondaryNavForProduct, NAV_USER, type NavDrillInConfig, type NavLinkItem, type NavSecondaryItem, type NavSchool, type NavSite, } from "@/lib/mock/navigation" import { buildNavHashClaims, collectNavUrls, isNavHrefActive, navUrlPath, normalizedLocationHash, } from "@exxatdesignux/ui/lib/nav-active" import { customProductSlugFromSuffix, primaryNavLinksForSlug, } from "@exxatdesignux/product-framework" import { expandSwitcherProducts, resolveActiveSwitcherEntry, type SwitcherProductEntry, } from "@/lib/product-switcher-catalog" // Active-link disambiguation needs to know about every URL the sidebar can // expose in any product (longest-prefix wins). Spreading the registry keeps // the nav-active helper accurate even when the user switches products and // the *displayed* primary nav changes. const BUILTIN_PRODUCT_SETTINGS_URLS = ( ["exxat-prism", "exxat-design-os", "exxat-one-schools", "exxat-one-sites"] as const ).map(p => `/${productSlug(p)}/settings`) const STATIC_NAV_URLS = collectNavUrls([ ...Object.values(NAV_BY_PRODUCT).flat(), ...NAV_DOCUMENTS, ...NAV_SECONDARY, ...BUILTIN_PRODUCT_SETTINGS_URLS.map(url => ({ url })), { url: "/settings/profile" }, { url: "/settings/organization" }, ]) // Custom-product nav lives in the tenant registry, which is hydrated at // runtime — so we can't bake those URLs into a `const` at module load. // Instead `AppSidebar` calls `syncCustomNavUrls(extras)` once per render // (idempotent on URL-set signature) and the helpers consult the *current* // list. This keeps the existing `isNavActive(pathname, url, hash)` API // stable for ~15 callsites without threading `allNavUrls` through every // child component. let CURRENT_NAV_URLS: ReadonlyArray = STATIC_NAV_URLS let CURRENT_NAV_HASH_CLAIMS = buildNavHashClaims(STATIC_NAV_URLS) let CURRENT_NAV_URL_SIGNATURE = STATIC_NAV_URLS.length === 0 ? "" : STATIC_NAV_URLS.join("|") function syncCustomNavUrls(extras: ReadonlyArray): void { if (extras.length === 0 && CURRENT_NAV_URLS === STATIC_NAV_URLS) return const merged = extras.length === 0 ? STATIC_NAV_URLS : Array.from(new Set([...STATIC_NAV_URLS, ...extras])) const signature = merged.length === 0 ? "" : merged.join("|") if (signature === CURRENT_NAV_URL_SIGNATURE) return CURRENT_NAV_URLS = merged CURRENT_NAV_HASH_CLAIMS = buildNavHashClaims(merged) CURRENT_NAV_URL_SIGNATURE = signature } /** Single active primary/secondary sidebar row — longest matching path wins. */ function isNavActive(pathname: string, url: string, locationHash = ""): boolean { return isNavHrefActive(pathname, url, CURRENT_NAV_URLS, { locationHash, hashClaimsByPath: CURRENT_NAV_HASH_CLAIMS, }) } /** Sub-item active — catalog detail routes, hash fragments, duplicate hub URLs, or nested children. */ function isCollapsibleChildActive( pathname: string, parent: NavLinkItem, child: NavLinkItem, locationHash: string ): boolean { if (child.children?.length) { const anyNestedActive = child.children.some((grandchild) => isCollapsibleChildActive(pathname, child, grandchild, locationHash), ) if (anyNestedActive) return true return isNavActive(pathname, child.url, locationHash) } const children = parent.children if (!children?.length) return isNavActive(pathname, child.url, locationHash) const hasHashChild = children.some(c => c.url.includes("#")) if (hasHashChild) { const h = normalizedLocationHash(locationHash) const childHash = child.url.includes("#") ? child.url.split("#")[1] : "" if (parent.primaryHubChildKey && child.key === parent.primaryHubChildKey) { return h === "" } if (childHash) { return h === childHash } return false } if (!isNavActive(pathname, child.url, locationHash)) return false /** Primary “Library” (`library-all`) — list hub route, independent of secondary scope. */ if (child.key === LIBRARY_PRIMARY_LIST_NAV_KEY && parent.key === "library") { return isLibraryPrimaryListNavActive(pathname) } /** Hub entry (`/library`) must not stay “active” on `/library/all` etc. */ if (parent.primaryHubChildKey && child.key === parent.primaryHubChildKey) { const hubPath = navUrlPath(parent.url) if (hubPath) { const normalized = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname if (normalized !== hubPath) return false } } const urls = children.map(c => c.url) const allSameUrl = urls.length > 1 && urls.every(u => u === urls[0]) if (allSameUrl) { if (parent.primaryHubChildKey) { return child.key === parent.primaryHubChildKey } return false } return true } /** * “Selected” styling on a collapsible **parent** row in the **expanded** sidebar. * * Rule: when any descendant child is the current destination, the parent stays * visually NEUTRAL — the active child carries `data-active` on its own. The * parent is only highlighted when no child matches but the parent URL still * matches (edge case: route that isn't represented in the sub-list). * * Note: this is for the expanded view only. The collapsed icon rail uses * `iconRailActive = isAnyChildActive` because the parent icon is the only * visible affordance there (see `CollapsibleNavItem`). */ function isCollapsibleParentMenuButtonActive( pathname: string, item: NavLinkItem, locationHash: string, ): boolean { const children = item.children if (!children?.length) return isNavActive(pathname, item.url, locationHash) const anyChildActive = children.some(c => isCollapsibleChildActive(pathname, item, c, locationHash), ) if (anyChildActive) return false return isNavActive(pathname, item.url, locationHash) } /** Accessible suffix for sidebar badges (badge is rendered outside the link node). */ function badgeAccessibleSuffix(badge: number | string): string { if (typeof badge === "number") return `${badge} items` return String(badge) } function NavItemBadgeContent({ badge }: { badge: number | string }) { if (typeof badge === "number") { return ( {badge} ) } if (badge === "New") return if (badge === "Beta") return return ( {badge} ) } /** Child row for expandable nav items — shared by inline sub-menu and collapsed-rail popover. */ const SidebarNavChildLink = React.forwardRef< HTMLAnchorElement, { parent: NavLinkItem child: NavLinkItem pathname: string locationHash: string onNavigate?: () => void /** Inline sub-menu pins badge on `SidebarMenuSubItem`; flyout keeps badge in-row. */ hideBadge?: boolean /** Popover uses surface tokens; inline sub-menu uses `SidebarMenuSubButton`. */ linkClassName?: string } & Omit, "to"> >(function SidebarNavChildLink( { parent, child, pathname, locationHash, onNavigate, hideBadge = false, linkClassName, className: incomingClassName, onClick, ...linkRest }, ref, ) { const { openPanel } = useSecondaryPanel() const childActive = isCollapsibleChildActive(pathname, parent, child, locationHash) const childPath = navUrlPath(child.url) return ( { onNavigate?.() if (parent.secondaryPanel && !child.url.includes("#")) { const panelId = parent.secondaryPanel // Same-route only — first navigation to `/library/all` opens the panel // from `library/_layout.tsx` after the URL updates (avoids pathname race). if (pathname === childPath) { e.preventDefault() openPanel(panelId) } } onClick?.(e) }} {...linkRest} > {child.title} {!hideBadge && child.badge !== undefined ? ( ) : null} ) }) SidebarNavChildLink.displayName = "SidebarNavChildLink" /** Inline sub-menu row — badge pinned to far end (same contract as `SidebarMenuBadge`). */ function SidebarNavSubMenuRow({ parent, child, pathname, locationHash, }: { parent: NavLinkItem child: NavLinkItem pathname: string locationHash: string }) { const childActive = isCollapsibleChildActive(pathname, parent, child, locationHash) return ( {child.badge !== undefined ? ( ) : null} ) } /** Nested collapsible inside an expanded parent sub-menu (e.g. Compliance under Program). */ function CollapsibleNavSubItem({ item, pathname, }: { item: NavLinkItem pathname: string }) { const locationHash = useLocationHash() const isAnyNestedActive = item.children?.some((child) => isCollapsibleChildActive(pathname, item, child, locationHash)) ?? false const parentMenuButtonActive = isCollapsibleParentMenuButtonActive(pathname, item, locationHash) const [open, setOpen] = React.useState(false) const navRouteKey = `${pathname}|${locationHash}|${isAnyNestedActive}` const prevNavRouteKeyRef = React.useRef(navRouteKey) if (navRouteKey !== prevNavRouteKeyRef.current) { prevNavRouteKeyRef.current = navRouteKey setOpen(isAnyNestedActive) } if (!item.children?.length) return null return ( {item.title}