91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
import { AnimatePresence, motion } from '@/lib/motion';
|
|
import { X } from 'lucide-react';
|
|
|
|
interface SlideOverProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
title: string;
|
|
icon?: React.ReactNode;
|
|
children: React.ReactNode;
|
|
size?: 'form' | 'compact';
|
|
}
|
|
|
|
const SIZE_CLASS = {
|
|
form: 'w-full sm:w-[78vw] lg:w-[min(50vw,600px)] sm:max-w-[650px]',
|
|
compact: 'w-full sm:w-[min(100%,380px)] sm:max-w-[380px]',
|
|
};
|
|
|
|
export function SlideOver({ open, onClose, title, icon, children, size = 'form' }: SlideOverProps) {
|
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const previousOverflow = document.body.style.overflow;
|
|
document.body.style.overflow = 'hidden';
|
|
return () => {
|
|
document.body.style.overflow = previousOverflow;
|
|
};
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') onClose();
|
|
};
|
|
window.addEventListener('keydown', onKeyDown);
|
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
}, [open, onClose]);
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{open && (
|
|
<div className="fixed inset-0 z-[80]" role="dialog" aria-modal="true" aria-labelledby="slideover-title">
|
|
<motion.button
|
|
type="button"
|
|
aria-label="Close drawer overlay"
|
|
className="absolute inset-0 bg-black/50 cursor-default"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.28, ease: [0.22, 1, 0.36, 1] }}
|
|
onClick={onClose}
|
|
/>
|
|
<motion.div
|
|
ref={panelRef}
|
|
className={`absolute right-0 top-0 flex h-full min-w-0 flex-col bg-card border-l border-border shadow-xl ${SIZE_CLASS[size]}`}
|
|
initial={{ x: '100%' }}
|
|
animate={{ x: 0 }}
|
|
exit={{ x: '100%' }}
|
|
transition={{ duration: 0.32, ease: [0.22, 1, 0.36, 1] }}
|
|
onAnimationComplete={() => {
|
|
if (!open) return;
|
|
const focusable = panelRef.current?.querySelector<HTMLElement>(
|
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
);
|
|
focusable?.focus();
|
|
}}
|
|
>
|
|
<div className="shrink-0 h-12 px-5 flex items-center justify-between border-b border-border">
|
|
<h2 id="slideover-title" className="text-[16px] font-semibold text-foreground flex items-center gap-2 min-w-0">
|
|
{icon}
|
|
<span className="truncate">{title}</span>
|
|
</h2>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label="Close"
|
|
className="w-8 h-8 rounded-[8px] text-primary hover:bg-muted cursor-pointer transition-colors flex items-center justify-center shrink-0"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
<div className="flex min-h-0 flex-1 flex-col">{children}</div>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|