import { AppShell, Badge, NavLink, ScrollArea, Stack, Text, Tooltip, UnstyledButton, rem, } from '@mantine/core'; import { IconChevronLeft, IconChevronRight, } from '@tabler/icons-react'; import type { Icon } from '@tabler/icons-react'; import { useTranslation } from 'react-i18next'; import { BrandMark } from '@ema-platform/auth'; export interface NavItem { label: string; icon: Icon; to?: string; /** Not yet connected to real data — surfaced as a "Soon" badge. */ soon?: boolean; } /** A labelled group of nav items. */ export interface NavSection { /** i18n key or literal heading. Omit for an ungrouped leading block. */ label?: string; items: NavItem[]; } /** Both shapes are accepted so callers can migrate to sections gradually. */ export type NavEntries = NavItem[] | NavSection[]; function toSections(entries: NavEntries): NavSection[] { if (entries.length === 0) return []; const isSectioned = (entries as NavSection[])[0]?.items !== undefined; return isSectioned ? (entries as NavSection[]) : [{ items: entries as NavItem[] }]; } interface AppSidebarProps { navItems: NavEntries; collapsed: boolean; activePath: string; onToggleCollapse: () => void; onNavigate: (item: NavItem) => void; brandName: string; brandSubtitle: string; } export function AppSidebar({ navItems, collapsed, activePath, onToggleCollapse, onNavigate, brandName, brandSubtitle, }: AppSidebarProps) { const { t } = useTranslation(); const activeNavItem = (item: NavItem) => !!item.to && (activePath === item.to || activePath.startsWith(`${item.to}/`)); return ( <> {/* Brand header */}
{!collapsed && (
{brandName} {brandSubtitle}
)}
{/* Navigation items */} {toSections(navItems).map((section, sectionIndex) => ( {/* Headings are noise when only icons are visible. */} {section.label && !collapsed && ( {t(section.label)} )} {section.label && collapsed && sectionIndex > 0 && (
)} {section.items.map((item) => { const ItemIcon = item.icon; const active = activeNavItem(item); if (collapsed) { return ( onNavigate(item)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: rem(40), borderRadius: rem(10), opacity: item.soon ? 0.55 : 1, color: active ? 'var(--mantine-color-blue-6)' : undefined, backgroundColor: active ? 'var(--mantine-color-blue-light)' : undefined, }} > ); } return ( } // Tells a reviewer at a glance which screens are wired up. rightSection={ item.soon ? ( {t('nav.soon', 'Soon')} ) : undefined } onClick={() => onNavigate(item)} variant="light" styles={{ root: { borderRadius: rem(10), opacity: item.soon ? 0.7 : 1 }, label: { fontWeight: 500 }, }} /> ); })} ))} {/* Collapse toggle */}
{ e.currentTarget.style.background = 'var(--mantine-color-gray-0)'; e.currentTarget.style.color = 'var(--mantine-color-gray-7)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--mantine-color-gray-5)'; }} > {collapsed ? ( ) : ( <> {t('nav.collapseSidebar', 'Collapse')} )}
); }