JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { motion, AnimatePresence } from "framer-motion";4import { X } from "lucide-react";5import { useEffect } from "react";67interface ModalProps {8 isOpen: boolean;9 onClose: () => void;10 title?: string;11 description?: string;12 children: React.ReactNode;13 maxWidth?: string;14 /** Pied de modale (boutons) — optionnel, sinon mettre les boutons dans children */15 footer?: React.ReactNode;16}1718export default function Modal({19 isOpen,20 onClose,21 title,22 description,23 children,24 maxWidth = "max-w-md",25 footer,26}: ModalProps) {27 useEffect(() => {28 if (!isOpen) return;29 const prev = document.body.style.overflow;30 document.body.style.overflow = "hidden";31 const onKey = (e: KeyboardEvent) => {32 if (e.key === "Escape") onClose();33 };34 window.addEventListener("keydown", onKey);35 return () => {36 document.body.style.overflow = prev;37 window.removeEventListener("keydown", onKey);38 };39 }, [isOpen, onClose]);4041 return (42 <AnimatePresence>43 {isOpen && (44 <div45 className="fixed inset-0 z-[70] flex items-end sm:items-center justify-center sm:p-4"46 role="dialog"47 aria-modal="true"48 aria-label={title}49 onMouseDown={(e) => e.stopPropagation()}50 onClick={(e) => e.stopPropagation()}51 onPointerDown={(e) => e.stopPropagation()}52 >53 <motion.div54 initial={{ opacity: 0 }}55 animate={{ opacity: 1 }}56 exit={{ opacity: 0 }}57 transition={{ duration: 0.18 }}58 className="absolute inset-0 bg-gray-950/40 dark:bg-black/65 backdrop-blur-sm"59 onClick={onClose}60 />61 {/* Feuille basse sur mobile, carte centrée sur desktop */}62 <motion.div63 initial={{ opacity: 0, y: 40, scale: 0.98 }}64 animate={{ opacity: 1, y: 0, scale: 1 }}65 exit={{ opacity: 0, y: 40, scale: 0.98 }}66 transition={{ type: "spring", damping: 28, stiffness: 340 }}67 className={`relative w-full ${maxWidth} bg-elevated border border-line dark:border-white/10 rounded-t-3xl sm:rounded-3xl shadow-pop overflow-hidden max-h-[92dvh] flex flex-col`}68 >69 {/* Poignée mobile */}70 <div className="sm:hidden flex justify-center pt-2.5 pb-1">71 <div className="w-10 h-1 rounded-full bg-gray-300 dark:bg-white/20" />72 </div>7374 {title && (75 <div className="flex items-start justify-between gap-3 px-5 sm:px-6 pt-4 sm:pt-5 pb-3">76 <div className="min-w-0">77 <h2 className="text-base sm:text-[17px] font-semibold text-gray-900 dark:text-white tracking-tight">{title}</h2>78 {description && <p className="text-[13px] text-gray-500 dark:text-gray-400 mt-0.5">{description}</p>}79 </div>80 <button81 onClick={onClose}82 className="text-gray-400 hover:text-gray-800 dark:hover:text-white transition-colors -mr-2 -mt-1 w-9 h-9 flex items-center justify-center rounded-xl hover:bg-gray-100 dark:hover:bg-white/10 shrink-0"83 aria-label="Fermer"84 >85 <X className="w-[18px] h-[18px]" />86 </button>87 </div>88 )}89 <div className={`px-5 sm:px-6 ${title ? "pb-5 sm:pb-6" : "py-5 sm:py-6"} overflow-auto flex-1 pb-safe`}>{children}</div>90 {footer && (91 <div className="px-5 sm:px-6 py-3.5 border-t border-line dark:border-white/10 bg-gray-50/60 dark:bg-white/[0.02] flex items-center justify-end gap-2 pb-safe">92 {footer}93 </div>94 )}95 </motion.div>96 </div>97 )}98 </AnimatePresence>99 );100}101