218 lines
5.1 KiB
TypeScript
218 lines
5.1 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
}
|