veza/apps/web/src/components/ui/Sidebar.tsx

218 lines
5.1 KiB
TypeScript
Raw Normal View History

import * as React from 'react';
import { ChevronLeft, ChevronRight, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from './button';
import { Card } from './card';
export interface SidebarProps {
/**
* Content to display in the sidebar
*/
children: React.ReactNode;
/**
* Position of the sidebar
* @default 'left'
*/
position?: 'left' | 'right';
/**
* Width of the sidebar when open
* @default 'w-64'
*/
width?: string;
/**
* Whether the sidebar is open
* @default true
*/
open?: boolean;
/**
* Callback when open state changes
*/
onOpenChange?: (open: boolean) => void;
/**
* Whether the sidebar is collapsible
* @default true
*/
collapsible?: boolean;
/**
* Title/header for the sidebar
*/
title?: React.ReactNode;
/**
* Optional icon to display next to the title
*/
icon?: React.ReactNode;
/**
* Additional CSS classes for the container
*/
className?: string;
/**
* Additional CSS classes for the sidebar content
*/
contentClassName?: string;
/**
* Whether to show a backdrop on mobile when open
* @default true
*/
showBackdrop?: boolean;
}
/**
* Sidebar - Reusable sidebar component for filters and content
*
* A generic sidebar component that can be used to display filters,
* additional content, or any sidebar-worthy information. Supports
* collapsible functionality and positioning.
*
* @example
* ```tsx
* <Sidebar title="Filters" position="left" collapsible>
* <div>Filter content here</div>
* </Sidebar>
* ```
*
* @example
* ```tsx
* <Sidebar
* title="Filters"
* icon={<Filter />}
* position="right"
* open={isOpen}
* onOpenChange={setIsOpen}
* width="w-80"
* >
* <FilterContent />
* </Sidebar>
* ```
*/
export function Sidebar({
children,
position = 'left',
width = 'w-64',
open: controlledOpen,
onOpenChange,
collapsible = true,
title,
icon,
className,
contentClassName,
showBackdrop = true,
}: SidebarProps) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(true);
// Use controlled state if provided, otherwise use uncontrolled
const isOpen = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen;
const handleToggle = () => {
const newOpen = !isOpen;
if (controlledOpen === undefined) {
setUncontrolledOpen(newOpen);
}
onOpenChange?.(newOpen);
};
const ChevronIcon = position === 'left' ? ChevronLeft : ChevronRight;
const isCollapsed = collapsible && !isOpen;
return (
<>
{/* Mobile Backdrop */}
{showBackdrop && isOpen && (
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm lg:hidden z-40"
onClick={handleToggle}
aria-hidden="true"
/>
)}
<aside
className={cn(
'relative flex flex-col transition-all duration-300 ease-in-out',
position === 'left' ? 'border-r' : 'border-l',
'border-white/10 bg-kodo-ink/40 backdrop-blur-md',
isCollapsed ? (position === 'left' ? '-ml-64' : '-mr-64') : width,
className,
)}
>
{/* Header */}
{(title || collapsible) && (
<div className="flex items-center justify-between p-4 border-b border-white/10">
{title && (
<div className="flex items-center gap-2 flex-1">
{icon && <div className="text-kodo-cyan">{icon}</div>}
<h3 className="text-sm font-semibold text-white">{title}</h3>
</div>
)}
{collapsible && (
<Button
variant="ghost"
size="icon"
onClick={handleToggle}
className="h-8 w-8"
aria-label={isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
>
<ChevronIcon className={cn(
'w-4 h-4 transition-transform',
isOpen && position === 'right' && 'rotate-180',
isOpen && position === 'left' && 'rotate-180',
)} />
</Button>
)}
</div>
)}
{/* Content */}
<div
className={cn(
'flex-1 overflow-y-auto custom-scrollbar',
isCollapsed && 'hidden',
contentClassName,
)}
>
{children}
</div>
</aside>
</>
);
}
/**
* SidebarCard - Sidebar with Card styling
*
* A sidebar variant that wraps content in a Card for consistent styling.
*/
export interface SidebarCardProps extends Omit<SidebarProps, 'children'> {
/**
* Content to display in the sidebar card
*/
children: React.ReactNode;
}
export function SidebarCard({
children,
className,
contentClassName,
...sidebarProps
}: SidebarCardProps) {
return (
<Sidebar
className={cn('p-0', className)}
contentClassName={cn('p-4', contentClassName)}
{...sidebarProps}
>
<Card className="border-0 bg-transparent shadow-none">
{children}
</Card>
</Sidebar>
);
}