import type { SettingsNavGroups, SettingsNavItem, SettingsNavScope } from '@documenso/lib/utils/settings-nav'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@documenso/ui/primitives/collapsible'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { ChevronRightIcon } from 'lucide-react'; import type { ReactNode } from 'react'; import { useEffect, useState } from 'react'; import { NavLink, useLocation } from 'react-router'; import { SettingsOrgSwitcher } from './settings-org-switcher'; import { SettingsTeamSwitcher } from './settings-team-switcher'; export type UnifiedSettingsSidebarProps = { groups: SettingsNavGroups; currentOrgUrl: string | null; currentTeamUrl: string | null; }; export const UnifiedSettingsSidebar = ({ groups, currentOrgUrl, currentTeamUrl }: UnifiedSettingsSidebarProps) => { return ( ); }; type SidebarGroupProps = { className?: string; headingClassName?: string; heading: ReactNode; activeBgClassName: string; switcher?: ReactNode; items: SettingsNavItem[]; scope: SettingsNavScope; emptyState?: ReactNode; }; type GroupedNavEntry = | { kind: 'flat'; item: SettingsNavItem } | { kind: 'collapsible'; parent: SettingsNavItem; children: SettingsNavItem[] }; /** * Walk a flat item list and group consecutive `isSubNav` items under their preceding * `isSubNavParent`. Anything else renders as a flat entry. */ const groupNavEntries = (items: SettingsNavItem[]): GroupedNavEntry[] => { const entries: GroupedNavEntry[] = []; let currentCollapsible: { parent: SettingsNavItem; children: SettingsNavItem[] } | null = null; for (const item of items) { if (item.isSubNavParent) { currentCollapsible = { parent: item, children: [] }; entries.push({ kind: 'collapsible', ...currentCollapsible }); continue; } if (item.isSubNav && currentCollapsible) { currentCollapsible.children.push(item); continue; } currentCollapsible = null; entries.push({ kind: 'flat', item }); } return entries; }; const SidebarGroup = ({ className, headingClassName, heading, activeBgClassName, switcher, items, scope, emptyState, }: SidebarGroupProps) => { const grouped = groupNavEntries(items); return (
{heading}
{switcher &&
{switcher}
} {items.length === 0 && emptyState}
); }; type FlatNavItemProps = { item: SettingsNavItem; activeBgClassName: string; scope: SettingsNavScope; }; const FlatNavItem = ({ item, activeBgClassName, scope }: FlatNavItemProps) => { const { _ } = useLingui(); const { pathname } = useLocation(); // The General items link to the settings root, which prefixes every other // item's path — those require an exact match. Everything else highlights on // its sub-routes too (e.g. Security on /settings/security/passkeys). const isActive = item.key === 'general' ? pathname === item.path : pathname === item.path || pathname.startsWith(`${item.path}/`); return ( ); }; type CollapsibleNavSectionProps = { parent: SettingsNavItem; childItems: SettingsNavItem[]; activeBgClassName: string; scope: SettingsNavScope; }; const CollapsibleNavSection = ({ parent, childItems, activeBgClassName, scope }: CollapsibleNavSectionProps) => { const { _ } = useLingui(); const { pathname } = useLocation(); const hasActiveChild = childItems.some((child) => pathname === child.path || pathname.startsWith(`${child.path}/`)); const [isOpen, setIsOpen] = useState(hasActiveChild); // Auto-open when navigating to a sub-route. Closing while on a sub-route is // respected (state stays closed) until the user navigates away and back. useEffect(() => { if (hasActiveChild) { setIsOpen(true); } }, [hasActiveChild]); const ParentIcon = parent.icon ? parent.icon : null; return ( {/* data-[state=closed]:hidden — the `flex` display class would otherwise defeat the `hidden` attribute Radix sets when closed, leaving the empty content box (and its mt-1) inflating the gap below the trigger. */} {childItems.map((child) => { const isActive = pathname === child.path || pathname.startsWith(`${child.path}/`); return ( ); })} ); };