74 lines
2.7 KiB
JavaScript
74 lines
2.7 KiB
JavaScript
import { useEffect } from "react";
|
|
import { AnimatePresence, motion } from "framer-motion";
|
|
import { X } from "lucide-react";
|
|
|
|
const WIDTHS = {
|
|
sm: "max-w-md",
|
|
md: "max-w-xl",
|
|
lg: "max-w-3xl",
|
|
xl: "max-w-5xl",
|
|
};
|
|
|
|
export default function Modal({ open, onClose, title, subtitle, icon: Icon, size = "md", children, footer }) {
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
function onKey(event) {
|
|
if (event.key === "Escape") onClose();
|
|
}
|
|
window.addEventListener("keydown", onKey);
|
|
document.body.style.overflow = "hidden";
|
|
return () => {
|
|
window.removeEventListener("keydown", onKey);
|
|
document.body.style.overflow = "";
|
|
};
|
|
}, [open, onClose]);
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{open && (
|
|
<motion.div
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
>
|
|
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
|
<motion.div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={title}
|
|
className={`relative w-full ${WIDTHS[size]} overflow-hidden rounded-2xl border border-line bg-surface shadow-2xl shadow-black/50`}
|
|
initial={{ opacity: 0, y: 18, scale: 0.97 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: 12, scale: 0.98 }}
|
|
transition={{ type: "spring", stiffness: 320, damping: 28 }}
|
|
>
|
|
<header className="flex items-start justify-between gap-4 border-b border-line p-5">
|
|
<div className="flex items-center gap-3">
|
|
{Icon && (
|
|
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500/15 to-indigo-500/15 text-accent">
|
|
<Icon size={20} />
|
|
</span>
|
|
)}
|
|
<div>
|
|
<h2 className="text-base font-bold text-ink">{title}</h2>
|
|
{subtitle && <p className="mt-0.5 text-xs text-ink-muted">{subtitle}</p>}
|
|
</div>
|
|
</div>
|
|
<button
|
|
aria-label="Close"
|
|
className="rounded-lg p-1.5 text-ink-muted transition hover:bg-surface-2 hover:text-ink"
|
|
onClick={onClose}
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
</header>
|
|
<div className="max-h-[68vh] overflow-y-auto p-5">{children}</div>
|
|
{footer && <footer className="flex flex-wrap justify-end gap-3 border-t border-line bg-surface-2/50 p-4">{footer}</footer>}
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|