79 lines
2.2 KiB
TypeScript
79 lines
2.2 KiB
TypeScript
import { useEffect, type ReactNode } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { X } from 'lucide-react';
|
|
import { cn } from '@/lib/cn';
|
|
import { Button } from './Button';
|
|
|
|
interface ModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
title: ReactNode;
|
|
description?: ReactNode;
|
|
children: ReactNode;
|
|
footer?: ReactNode;
|
|
size?: 'md' | 'lg';
|
|
}
|
|
|
|
export function Modal({
|
|
open,
|
|
onClose,
|
|
title,
|
|
description,
|
|
children,
|
|
footer,
|
|
size = 'md',
|
|
}: ModalProps) {
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
};
|
|
window.addEventListener('keydown', onKey);
|
|
const prev = document.body.style.overflow;
|
|
document.body.style.overflow = 'hidden';
|
|
return () => {
|
|
window.removeEventListener('keydown', onKey);
|
|
document.body.style.overflow = prev;
|
|
};
|
|
}, [open, onClose]);
|
|
|
|
if (!open) return null;
|
|
|
|
return createPortal(
|
|
<div className="fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-6">
|
|
<div
|
|
className="absolute inset-0 bg-ink/40 backdrop-blur-sm animate-fade-in-up"
|
|
onClick={onClose}
|
|
aria-hidden
|
|
/>
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
className={cn(
|
|
'relative w-full bg-surface shadow-pop animate-fade-in-up',
|
|
'rounded-t-xl sm:rounded-xl',
|
|
size === 'md' ? 'sm:max-w-lg' : 'sm:max-w-2xl',
|
|
'max-h-[90vh] overflow-hidden flex flex-col',
|
|
)}
|
|
>
|
|
<header className="flex items-start justify-between gap-4 border-b border-border px-5 py-4">
|
|
<div className="min-w-0">
|
|
<h2 className="text-lg font-semibold text-ink">{title}</h2>
|
|
{description && <p className="mt-1 text-sm text-muted">{description}</p>}
|
|
</div>
|
|
<Button variant="ghost" size="icon" onClick={onClose} aria-label="Close">
|
|
<X className="h-5 w-5" />
|
|
</Button>
|
|
</header>
|
|
<div className="overflow-y-auto px-5 py-5">{children}</div>
|
|
{footer && (
|
|
<footer className="flex items-center justify-end gap-2 border-t border-border px-5 py-4">
|
|
{footer}
|
|
</footer>
|
|
)}
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|