Files
no-whatever/src/components/Modal.tsx
T
kurihada 948274bcb9 refactor: 提取 Modal 基础组件,消除 4 个弹窗的重复样板代码
将 backdrop 遮罩、点击关闭、AnimatePresence 动画封装为 Modal 组件,
支持 sheet(底部弹出)和 dialog(居中缩放)两种变体,净减约 110 行。
2026-02-26 17:45:52 +08:00

76 lines
2.0 KiB
TypeScript

"use client";
import { useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
type ModalVariant = "sheet" | "dialog";
interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
variant?: ModalVariant;
}
const sheet = {
backdrop:
"fixed inset-0 z-50 flex items-end justify-center bg-black/60 backdrop-blur-sm sm:items-center",
content:
"relative w-full max-w-sm rounded-t-3xl bg-surface px-5 pb-8 pt-5 shadow-2xl ring-1 ring-border sm:rounded-3xl sm:pb-6",
initial: { y: "100%" },
animate: { y: 0 },
exit: { y: "100%" },
transition: { type: "spring" as const, damping: 28, stiffness: 350 },
};
const dialog = {
backdrop:
"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
content:
"mx-6 w-full max-w-xs rounded-2xl bg-surface px-6 py-6 shadow-2xl ring-1 ring-border",
initial: { scale: 0.9, opacity: 0 },
animate: { scale: 1, opacity: 1 },
exit: { scale: 0.9, opacity: 0 },
transition: { type: "spring" as const, damping: 25, stiffness: 350 },
};
const variants = { sheet, dialog };
export default function Modal({
open,
onClose,
children,
variant = "sheet",
}: ModalProps) {
const backdropRef = useRef<HTMLDivElement>(null);
const v = variants[variant];
return (
<AnimatePresence>
{open && (
<motion.div
ref={backdropRef}
className={v.backdrop}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={(e) => {
if (e.target === backdropRef.current) onClose();
}}
>
<motion.div
className={v.content}
initial={v.initial}
animate={v.animate}
exit={v.exit}
transition={v.transition}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}