mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
212 lines
4.7 KiB
TypeScript
212 lines
4.7 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
|
import { clsx } from 'clsx';
|
|
import { X } from 'lucide-react';
|
|
import Button from './Button';
|
|
|
|
export interface SheetProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
children: React.ReactNode;
|
|
side?: 'left' | 'right' | 'top' | 'bottom';
|
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
className?: string;
|
|
}
|
|
|
|
const sideClasses = {
|
|
left: 'left-0 top-0 bottom-0',
|
|
right: 'right-0 top-0 bottom-0',
|
|
top: 'top-0 left-0 right-0',
|
|
bottom: 'bottom-0 left-0 right-0',
|
|
};
|
|
|
|
const sizeClasses = {
|
|
sm: 'w-80',
|
|
md: 'w-96',
|
|
lg: 'w-[32rem]',
|
|
xl: 'w-[42rem]',
|
|
full: 'w-full',
|
|
};
|
|
|
|
const slideAnimations = {
|
|
left: 'animate-in slide-in-from-left duration-300',
|
|
right: 'animate-in slide-in-from-right duration-300',
|
|
top: 'animate-in slide-in-from-top duration-300',
|
|
bottom: 'animate-in slide-in-from-bottom duration-300',
|
|
};
|
|
|
|
/**
|
|
* Sheet/Drawer component - side panel
|
|
*/
|
|
export const Sheet = ({
|
|
open,
|
|
onOpenChange,
|
|
children,
|
|
side = 'right',
|
|
size = 'md',
|
|
className,
|
|
}: SheetProps) => {
|
|
const sheetRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
return () => {
|
|
document.body.style.overflow = '';
|
|
};
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape' && open) {
|
|
onOpenChange(false);
|
|
}
|
|
};
|
|
|
|
if (open) {
|
|
document.addEventListener('keydown', handleEscape);
|
|
// Focus trap
|
|
const focusableElements = sheetRef.current?.querySelectorAll(
|
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
);
|
|
const firstElement = focusableElements?.[0] as HTMLElement;
|
|
firstElement?.focus();
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleEscape);
|
|
};
|
|
}, [open, onOpenChange]);
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<>
|
|
{/* Backdrop */}
|
|
<div
|
|
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
|
onClick={() => onOpenChange(false)}
|
|
aria-hidden="true"
|
|
/>
|
|
|
|
{/* Sheet */}
|
|
<div
|
|
ref={sheetRef}
|
|
className={clsx(
|
|
'fixed z-50',
|
|
'bg-background border shadow-lg',
|
|
'flex flex-col',
|
|
sideClasses[side],
|
|
size === 'full' ? 'h-full' : sizeClasses[size],
|
|
slideAnimations[side],
|
|
className
|
|
)}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
>
|
|
{children}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export interface SheetHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const SheetHeader = ({ children, className, ...props }: SheetHeaderProps) => {
|
|
return (
|
|
<div
|
|
className={clsx('flex flex-col space-y-1.5 p-6 pb-4 border-b', className)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export interface SheetTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const SheetTitle = ({ children, className, ...props }: SheetTitleProps) => {
|
|
return (
|
|
<h2
|
|
className={clsx('text-lg font-semibold leading-none tracking-tight', className)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</h2>
|
|
);
|
|
};
|
|
|
|
export interface SheetDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const SheetDescription = ({
|
|
children,
|
|
className,
|
|
...props
|
|
}: SheetDescriptionProps) => {
|
|
return (
|
|
<p className={clsx('text-sm text-muted-foreground', className)} {...props}>
|
|
{children}
|
|
</p>
|
|
);
|
|
};
|
|
|
|
export interface SheetContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const SheetContent = ({ children, className, ...props }: SheetContentProps) => {
|
|
return (
|
|
<div
|
|
className={clsx('flex-1 overflow-y-auto p-6', className)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export interface SheetFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const SheetFooter = ({ children, className, ...props }: SheetFooterProps) => {
|
|
return (
|
|
<div
|
|
className={clsx('flex items-center justify-end gap-3 p-6 pt-4 border-t', className)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export interface SheetCloseProps {
|
|
onClose: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
export const SheetClose = ({ onClose, className }: SheetCloseProps) => {
|
|
return (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onClose}
|
|
className={clsx('absolute right-4 top-4', className)}
|
|
aria-label="Close"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
);
|
|
};
|
|
|