"use client"; import React, { useState } from 'react'; export type SidebarNavItem = { type?: 'link' | 'header' | 'divider' | 'submenu'; label?: React.ReactNode; href?: string; icon?: React.ReactNode; iconClass?: string; badge?: React.ReactNode; active?: boolean; disabled?: boolean; children?: SidebarNavItem[]; onClick?: () => void; /** Whether submenu is open (controlled externally) */ isOpen?: boolean; /** Callback to toggle submenu open state */ onToggle?: () => void; /** Custom link component (e.g., NavLink from Remix) */ LinkComponent?: React.ComponentType; }; export type SidebarUserInfo = { name: string; subtitle?: string; avatar?: string; menuItems?: SidebarNavItem[]; }; export type SidebarProps = { /** Sidebar variant: 'main' | 'secondary' | 'right' */ variant?: 'main' | 'secondary' | 'right'; /** Light or dark color scheme */ color?: 'light' | 'dark'; /** User info block (Layout 3 material style) */ user?: SidebarUserInfo; /** Optional header content at top of sidebar */ header?: React.ReactNode; /** Navigation items */ items: SidebarNavItem[]; /** Additional className */ className?: string; /** Mobile toggler title */ mobileTitle?: string; /** Sidebar expand breakpoint */ expandBreakpoint?: 'sm' | 'md' | 'lg' | 'xl'; /** Whether sidebar is collapsed */ collapsed?: boolean; /** Callback when mobile sidebar close is clicked */ onMobileClose?: () => void; /** Custom link component for all nav items (e.g., NavLink from Remix) */ LinkComponent?: React.ComponentType; /** Current pathname for active state detection */ pathname?: string; }; /** * Layout 3 detached sidebar with nav-sidebar navigation. * Supports user menu, headers, dividers, and multi-level submenus. */ export function Sidebar({ variant = 'main', color = 'light', user, header, items, className = '', mobileTitle = 'Main sidebar', expandBreakpoint = 'lg', collapsed = false, onMobileClose, LinkComponent, pathname = '', }: SidebarProps) { const variantClass = variant === 'main' ? 'sidebar-main' : variant === 'secondary' ? 'sidebar-secondary' : 'sidebar-right'; const colorClass = color === 'dark' ? 'sidebar-dark' : 'sidebar-light'; const expandClass = `sidebar-expand-${expandBreakpoint}`; return (
{/* Sidebar mobile toggler */}
{ e.preventDefault(); onMobileClose?.(); }}> {mobileTitle}
{/* Sidebar content */}
{header &&
{header}
} {/* User menu (material style) */} {user && } {/* Navigation */}
    {items.map((item, idx) => ( ))}
); } function SidebarUserMenu({ user }: { user: SidebarUserInfo }) { const [isOpen, setIsOpen] = useState(false); return (
{user.avatar ? ( ) : (
{user.name.charAt(0).toUpperCase()}
)}
{user.name}
{user.subtitle && {user.subtitle}}
{ e.preventDefault(); setIsOpen(!isOpen); }} > My account
{user.menuItems && (
    {user.menuItems.map((item, idx) => ( ))}
)}
); } function SidebarNavNode({ item, level = 0, collapsed = false, LinkComponent, pathname = '', }: { item: SidebarNavItem; level?: number; collapsed?: boolean; LinkComponent?: React.ComponentType; pathname?: string; }) { // Check if any child is active (current path matches) const hasActiveChild = item.children?.some(child => child.href && pathname.startsWith(child.href)) || false; // Use external isOpen if provided, otherwise fall back to local state const [localIsOpen, setLocalIsOpen] = useState(hasActiveChild); const isOpen = item.isOpen !== undefined ? item.isOpen : localIsOpen; if (item.type === 'header') { if (collapsed) return null; return (
  • {item.label}
    {item.icon && }
  • ); } if (item.type === 'divider') { return
  • ; } const hasChildren = item.children && item.children.length > 0; const isSubmenu = item.type === 'submenu' || hasChildren; // Determine if item is active based on pathname const isItemActive = item.active ?? (item.href ? pathname.startsWith(item.href) : item.children?.some(child => child.href && pathname.startsWith(child.href)) || false); const linkClasses = [ 'nav-link', 'd-flex', 'align-items-center', isItemActive ? 'active' : '', item.disabled ? 'disabled' : '', collapsed ? 'justify-content-center' : '', ].filter(Boolean).join(' '); const handleClick = (e: React.MouseEvent) => { if (isSubmenu) { e.preventDefault(); if (item.onToggle) { item.onToggle(); } else { setLocalIsOpen(!isOpen); } } if (item.onClick) { item.onClick(); } }; // Render icon const renderIcon = () => { if (item.icon) { return {item.icon}; } if (item.iconClass) { return ; } return null; }; if (isSubmenu) { // Use button for submenu toggle to avoid anchor navigation issues const handleToggle = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (collapsed) { if (item.onClick) { item.onClick(); } return; } if (item.onToggle) { item.onToggle(); } else { setLocalIsOpen(prev => !prev); } if (item.onClick) { item.onClick(); } }; return (
    • {item.children!.map((child, idx) => ( ))}
  • ); } // Use custom LinkComponent if provided, otherwise use const Link = LinkComponent || 'a'; const linkProps = LinkComponent ? { to: item.href || '#', className: linkClasses, title: collapsed ? String(item.label) : undefined, onClick: (e: React.MouseEvent) => { // Stop propagation to prevent parent handlers from interfering e.stopPropagation(); if (item.onClick) { item.onClick(); } // Don't prevent default - let navigation proceed }, } : { href: item.href || '#', className: linkClasses, onClick: handleClick, title: collapsed ? String(item.label) : undefined }; return (
  • {renderIcon()} {!collapsed && {item.label}} {!collapsed && item.badge && {item.badge}}
  • ); } // Re-export for backwards compatibility export type SidebarItem = SidebarNavItem;