Spaces:
Running
Running
| import React, { Fragment, useEffect } from 'react'; | |
| interface ModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| title?: string; | |
| children: React.ReactNode; | |
| showCloseButton?: boolean; | |
| size?: 'sm' | 'md' | 'lg' | 'xl'; | |
| closeOnOverlayClick?: boolean; | |
| } | |
| export const Modal: React.FC<ModalProps> = ({ | |
| isOpen, | |
| onClose, | |
| title, | |
| children, | |
| showCloseButton = true, | |
| size = 'md', | |
| closeOnOverlayClick = true, | |
| }) => { | |
| // Close on ESC key press | |
| useEffect(() => { | |
| const handleEscKey = (e: KeyboardEvent) => { | |
| if (e.key === 'Escape' && isOpen) { | |
| onClose(); | |
| } | |
| }; | |
| if (isOpen) { | |
| document.addEventListener('keydown', handleEscKey); | |
| // Prevent body scrolling when modal is open | |
| document.body.style.overflow = 'hidden'; | |
| } | |
| return () => { | |
| document.removeEventListener('keydown', handleEscKey); | |
| document.body.style.overflow = 'auto'; | |
| }; | |
| }, [isOpen, onClose]); | |
| // Handle overlay click | |
| const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => { | |
| if (e.target === e.currentTarget && closeOnOverlayClick) { | |
| onClose(); | |
| } | |
| }; | |
| if (!isOpen) return null; | |
| // Size mapping | |
| const sizeClasses = { | |
| sm: 'max-w-md', | |
| md: 'max-w-lg', | |
| lg: 'max-w-2xl', | |
| xl: 'max-w-4xl', | |
| }; | |
| return ( | |
| <Fragment> | |
| {/* Modal backdrop */} | |
| <div | |
| className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto bg-gray-900 bg-opacity-75 backdrop-blur-sm transition-opacity" | |
| onClick={handleOverlayClick} | |
| aria-modal="true" | |
| role="dialog" | |
| aria-labelledby="modal-title" | |
| > | |
| {/* Modal panel */} | |
| <div | |
| className={`relative mx-auto w-full ${sizeClasses[size]} rounded-lg bg-gray-800 border border-gray-700 shadow-xl transition-all`} | |
| onClick={e => e.stopPropagation()} | |
| > | |
| {/* Modal header */} | |
| {(title || showCloseButton) && ( | |
| <div className="flex items-center justify-between rounded-t-lg border-b border-gray-700 bg-gray-850 px-6 py-4"> | |
| {title && ( | |
| <h3 id="modal-title" className="text-xl font-semibold text-gray-100"> | |
| {title} | |
| </h3> | |
| )} | |
| {showCloseButton && ( | |
| <button | |
| type="button" | |
| className="ml-auto inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| onClick={onClose} | |
| aria-label="Close modal" | |
| > | |
| <svg | |
| className="h-5 w-5" | |
| xmlns="http://www.w3.org/2000/svg" | |
| fill="none" | |
| viewBox="0 0 24 24" | |
| stroke="currentColor" | |
| > | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| )} | |
| </div> | |
| )} | |
| {/* Modal content */} | |
| <div className="px-6 py-4">{children}</div> | |
| </div> | |
| </div> | |
| </Fragment> | |
| ); | |
| }; | |