SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
2 h agolast push
JavaScript 56.7% TypeScript 42.6%
3.7 KB · 101 lines tsx
Raw Blame History
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