"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" import { useLocation } from "react-router" import { motion, useReducedMotion } from "motion/react" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "../ui/collapsible" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, PRODUCT_SWITCHER_MENU_SURFACE_CLASS, SHELL_IDENTITY_MENU_SURFACE_CLASS, Shortcut, } from "../ui/dropdown-menu" import { Popover, PopoverContent, PopoverTrigger, } from "../ui/popover" import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarMenuSeparator, SidebarTrigger, useRegisterNavFlyoutToggle, useSidebar, } from "../ui/sidebar" import { SidebarNavLabel } from "../ui/sidebar" import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar" import { Badge } from "../ui/badge" import { StatusBadge } from "../ui/status-badge" import { Tooltip, TooltipContent, TooltipTrigger, } from "../ui/tooltip" import { cn } from "../../lib/utils" import { Button } from "../ui/button" import { Tip } from "../ui/tip" import { Kbd, KbdGroup } from "../ui/kbd" import { useModKeyLabel } from "../../hooks/use-mod-key-label" import { useAppShellSlots } from "./app-shell-slots" import { useLocationHash } from "../../hooks/use-location-hash" import { useSidebarReflowZoom } from "../../hooks/use-sidebar-reflow-zoom" import { SIDEBAR_DRILL_IN_WIDTH_MAX, SIDEBAR_DRILL_IN_WIDTH_MIN, SIDEBAR_PRIMARY_WIDTH_MAX, SIDEBAR_PRIMARY_WIDTH_MIN, useSidebarDrillInWidth, useSidebarPrimaryWidth, } from "../../hooks/use-sidebar-drill-in-width" import { useProduct, type Product } from "./product-context" import { useShellLayout } from "../../hooks/shell-layout-context" import { movesSidebarHeaderToBar, showsProductInSidebar, showsProfileInSidebar, showsScopeInSidebar, } from "../../lib/shell-layout" import { productSlug } from "@exxatdesignux/product-framework" import { useSecondaryPanel } from "./secondary-panel" import { NavUser } from "./nav-user" import { SidebarDrillIn } from "../ui/sidebar-drill-in" import { SidebarRailResizeHandle } from "./sidebar-drill-in-resize-handle" import { motionHeaderEnter } from "../../lib/motion-ui" import { createNavActiveState, type NavActiveState } from "../../lib/nav-active-state" import type { NavDrillInConfig, NavLinkItem, NavSecondaryItem, NavSection, } from "./nav-render-types" import { navUrlPath } from "../../lib/nav-active" /** * Which row is highlighted, resolved from the nav the app registered. * * The rules live in `@exxatdesignux/ui/lib/nav-active-state`; what stays here is * getting them to the ~15 row components that ask. A context rather than a * parameter because the answer depends on the *whole* URL corpus, so every row * has to consult the same instance or two rows can both think they won. * * The empty default is for rows rendered outside `AppSidebar` (tests, previews): * nothing is highlighted, rather than a crash. */ const NavActiveContext = React.createContext( createNavActiveState({ urls: [] }), ) function useNavActive(): NavActiveState { return React.useContext(NavActiveContext) } /** 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 = useNavActive().isChildActive(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 = useNavActive().isChildActive(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 navActive = useNavActive() const isAnyNestedActive = item.children?.some((child) => navActive.isChildActive(pathname, item, child, locationHash)) ?? false const parentMenuButtonActive = navActive.isParentActive(pathname, item, locationHash) const [open, setOpen] = React.useState(false) React.useEffect(() => { setOpen(isAnyNestedActive) }, [isAnyNestedActive, pathname, locationHash]) if (!item.children?.length) return null return ( {item.title}