/**
* 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, Label } 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 ? (
{/* Same spec as every other group heading in the rail — this one
had its own copy of the retired 10px/600/0.16em values. */}
) : 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}
);
}