/** * The left navigation pane. * * One component for both treatments the sidebar primitive provides: the * collapsible desktop rail and the phone Sheet. Deliberately not two, because * the previous shell had the desktop list and the tab bar as separate JSX and * they had already drifted — the tab bar's active pill and the sidebar's * active row used different tokens. */ import { Fragment } from 'react'; import { X } from 'lucide-react'; import { Link, useMatch, useResolvedPath } from 'react-router-dom'; import { useIdentity } from '@/lib/identity'; import { NAV_GROUP_HEADING, NAV_GROUPS, visibleNav, type NavItem } from '@/lib/nav'; import { AccountSwitcher } from './AccountSwitcher'; import { Button } from './ui'; import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, SidebarSeparator, useSidebar, } from './ui/sidebar'; export function AppSidebar() { const identity = useIdentity(); const items = visibleNav(identity); const { isMobile, setOpenMobile } = useSidebar(); return ( {/* The Sheet's own close button is suppressed because it lands on top of the account switcher. This one replaces it — Escape and the overlay work, but a visible close is not optional on a touch device where neither is discoverable. */} {isMobile ? (
Navigate
) : null}
{NAV_GROUPS.map((group) => { const groupItems = items.filter((item) => item.group === group); // A heading over nothing is worse than a missing section: it reads // as a section that failed to load rather than one you cannot use. if (!groupItems.length) return null; const heading = NAV_GROUP_HEADING[group]; return ( {heading ? {heading} : null} {groupItems.map((item) => ( ))} {/* An unlabelled group has no heading to separate it from the next one, so it gets a rule instead. This is also the only separation that survives collapse: at icon width every heading is pulled up and faded out, so without the rule the front door would be just one more glyph in an undifferentiated stack of them. */} {heading === null ? : null} ); })} {/* No footer. The old shell ended with "Prime Intellect Growth / Compute revenue system", which the account switcher at the top now says better — and at 900px the fourteen nav rows do not all fit, so a restatement of the workspace name was costing two of them. */}
); } function NavItemRow({ item }: { item: NavItem }) { const { setOpenMobile, isMobile } = useSidebar(); // `asChild` renders the row *as* the link rather than wrapping one, so there // is a single focusable element per row. Active state is asked of the router // instead of compared against a pathname, so `/demand/abc` still lights // Demand and `/` does not light everything. const resolved = useResolvedPath(item.to); const isActive = useMatch({ path: resolved.pathname, end: item.to === '/' }) !== null; return ( { if (isMobile) setOpenMobile(false); }} > {item.label} ); }