170 lines
4.4 KiB
TypeScript
170 lines
4.4 KiB
TypeScript
import * as React from 'react';
|
|
import { cn } from '@slate-test-1770505673/ui';
|
|
import { ChevronRight, type LucideIcon } from 'lucide-react';
|
|
|
|
export interface NavItem {
|
|
/** Display label */
|
|
label: string;
|
|
/** URL to navigate to */
|
|
href: string;
|
|
/** Icon component from lucide-react */
|
|
icon?: LucideIcon;
|
|
/** Whether this item is currently active */
|
|
active?: boolean;
|
|
/** Badge text to display */
|
|
badge?: string;
|
|
/** Nested items for collapsible sections */
|
|
children?: NavItem[];
|
|
}
|
|
|
|
export interface SidebarProps {
|
|
/** Logo or brand element to display at the top */
|
|
logo?: React.ReactNode;
|
|
/** Navigation items */
|
|
items: NavItem[];
|
|
/** Footer element (e.g., user menu, settings) */
|
|
footer?: React.ReactNode;
|
|
/** Additional class names */
|
|
className?: string;
|
|
/** Click handler for navigation items */
|
|
onNavigate?: (href: string) => void;
|
|
}
|
|
|
|
/**
|
|
* Sidebar provides navigation for dashboard applications.
|
|
* Supports icons, nested items, badges, and active state highlighting.
|
|
*
|
|
* @example
|
|
* <Sidebar
|
|
* logo={<Logo />}
|
|
* items={[
|
|
* { label: 'Dashboard', href: '/', icon: Home, active: true },
|
|
* { label: 'Users', href: '/users', icon: Users },
|
|
* { label: 'Settings', href: '/settings', icon: Settings },
|
|
* ]}
|
|
* footer={<UserMenu />}
|
|
* />
|
|
*/
|
|
export function Sidebar({
|
|
logo,
|
|
items,
|
|
footer,
|
|
className,
|
|
onNavigate,
|
|
}: SidebarProps) {
|
|
const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});
|
|
|
|
const toggleExpanded = (label: string) => {
|
|
setExpanded((prev) => ({ ...prev, [label]: !prev[label] }));
|
|
};
|
|
|
|
const handleClick = (item: NavItem, e: React.MouseEvent) => {
|
|
if (item.children) {
|
|
e.preventDefault();
|
|
toggleExpanded(item.label);
|
|
} else if (onNavigate) {
|
|
e.preventDefault();
|
|
onNavigate(item.href);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={cn('flex flex-col h-full', className)}>
|
|
{/* Logo area */}
|
|
{logo && (
|
|
<div className="flex h-16 items-center px-4 border-b border-[var(--border-muted)]">
|
|
{logo}
|
|
</div>
|
|
)}
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 overflow-y-auto py-4">
|
|
<ul className="space-y-1 px-2">
|
|
{items.map((item) => (
|
|
<NavItemComponent
|
|
key={item.href}
|
|
item={item}
|
|
isExpanded={expanded[item.label]}
|
|
onClick={handleClick}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
))}
|
|
</ul>
|
|
</nav>
|
|
|
|
{/* Footer */}
|
|
{footer && (
|
|
<div className="border-t border-[var(--border-muted)] p-4">
|
|
{footer}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface NavItemComponentProps {
|
|
item: NavItem;
|
|
isExpanded?: boolean;
|
|
depth?: number;
|
|
onClick: (item: NavItem, e: React.MouseEvent) => void;
|
|
onNavigate?: (href: string) => void;
|
|
}
|
|
|
|
function NavItemComponent({
|
|
item,
|
|
isExpanded = false,
|
|
depth = 0,
|
|
onClick,
|
|
onNavigate,
|
|
}: NavItemComponentProps) {
|
|
const Icon = item.icon;
|
|
const hasChildren = item.children && item.children.length > 0;
|
|
|
|
return (
|
|
<li>
|
|
<a
|
|
href={item.href}
|
|
onClick={(e) => onClick(item, e)}
|
|
className={cn(
|
|
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
|
item.active
|
|
? 'bg-[var(--surface-200)] text-[var(--text-primary)]'
|
|
: 'text-[var(--text-secondary)] hover:bg-[var(--surface-100)] hover:text-[var(--text-primary)]',
|
|
depth > 0 && 'ml-6'
|
|
)}
|
|
>
|
|
{Icon && <Icon className="h-4 w-4 shrink-0" />}
|
|
<span className="flex-1">{item.label}</span>
|
|
{item.badge && (
|
|
<span className="rounded-full bg-[var(--accent)] px-2 py-0.5 text-xs font-medium text-[var(--accent-foreground)]">
|
|
{item.badge}
|
|
</span>
|
|
)}
|
|
{hasChildren && (
|
|
<ChevronRight
|
|
className={cn(
|
|
'h-4 w-4 transition-transform',
|
|
isExpanded && 'rotate-90'
|
|
)}
|
|
/>
|
|
)}
|
|
</a>
|
|
|
|
{/* Nested items */}
|
|
{hasChildren && isExpanded && (
|
|
<ul className="mt-1 space-y-1">
|
|
{item.children!.map((child) => (
|
|
<NavItemComponent
|
|
key={child.href}
|
|
item={child}
|
|
depth={depth + 1}
|
|
onClick={onClick}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</li>
|
|
);
|
|
}
|