111 lines
3.1 KiB
TypeScript
111 lines
3.1 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { clsx } from 'clsx';
|
|
import { Menu, X } from 'lucide-react';
|
|
|
|
export interface SidebarProps {
|
|
children: React.ReactNode;
|
|
header?: React.ReactNode;
|
|
footer?: React.ReactNode;
|
|
collapsed?: boolean;
|
|
onToggle?: () => void;
|
|
className?: string;
|
|
width?: number;
|
|
}
|
|
|
|
export function Sidebar({
|
|
children,
|
|
header,
|
|
footer,
|
|
collapsed = false,
|
|
onToggle,
|
|
className,
|
|
width = 240,
|
|
}: SidebarProps) {
|
|
return (
|
|
<>
|
|
{/* Mobile toggle */}
|
|
{onToggle && (
|
|
<button
|
|
className={clsx(
|
|
'fixed top-3 left-3 z-[38] flex items-center justify-center w-10 h-10 rounded-lg',
|
|
'border border-[var(--bl-border,#2a2a4a)] bg-[var(--bl-bg-elevated,#12151c)]',
|
|
'text-[var(--bl-text-primary,#fff)] cursor-pointer',
|
|
'md:hidden'
|
|
)}
|
|
onClick={onToggle}
|
|
aria-label={collapsed ? 'Open menu' : 'Close menu'}
|
|
type="button"
|
|
>
|
|
{collapsed ? <Menu size={20} /> : <X size={20} />}
|
|
</button>
|
|
)}
|
|
|
|
{/* Overlay */}
|
|
{onToggle && !collapsed && (
|
|
<div
|
|
className="fixed inset-0 z-[39] bg-black/50 md:hidden"
|
|
onClick={onToggle}
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
|
|
<aside
|
|
aria-label="Primary"
|
|
className={clsx(
|
|
'flex flex-col h-full border-r shrink-0 transition-transform duration-200',
|
|
'border-[var(--bl-border,#2a2a4a)] bg-[var(--bl-bg-elevated,#12151c)]',
|
|
'fixed md:static z-40',
|
|
collapsed ? '-translate-x-full md:translate-x-0' : 'translate-x-0',
|
|
className
|
|
)}
|
|
style={{ width, minWidth: width }}
|
|
>
|
|
{header && <div className="border-b border-[var(--bl-border,#2a2a4a)] p-4">{header}</div>}
|
|
|
|
<nav aria-label="Main navigation" className="flex-1 overflow-y-auto p-2">
|
|
{children}
|
|
</nav>
|
|
|
|
{footer && (
|
|
<div className="border-t border-[var(--bl-border,#2a2a4a)] p-4 text-xs text-[var(--bl-text-secondary,#a0a0b0)]">
|
|
{footer}
|
|
</div>
|
|
)}
|
|
</aside>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export interface SidebarItemProps {
|
|
href: string;
|
|
label: string;
|
|
icon?: React.ReactNode;
|
|
active?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export function SidebarItem({ href, label, icon, active, className }: SidebarItemProps) {
|
|
return (
|
|
<a
|
|
href={href}
|
|
aria-current={active ? 'page' : undefined}
|
|
className={clsx(
|
|
'flex items-center gap-2.5 px-3 py-2 rounded-md text-sm transition-colors',
|
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bl-accent,#5A8CFF)]',
|
|
active
|
|
? 'bg-[var(--bl-surface-card,#1a1a2e)] text-[var(--bl-text-primary,#fff)] font-semibold'
|
|
: 'text-[var(--bl-text-secondary,#a0a0b0)] hover:bg-[var(--bl-surface-muted,#252540)] hover:text-[var(--bl-text-primary,#fff)]',
|
|
className
|
|
)}
|
|
>
|
|
{icon && <span className="shrink-0">{icon}</span>}
|
|
<span>{label}</span>
|
|
</a>
|
|
);
|
|
}
|
|
|
|
Sidebar.displayName = 'Sidebar';
|
|
SidebarItem.displayName = 'SidebarItem';
|