Files
HartOMat/frontend/src/components/shared/Modal.tsx
T
2026-03-05 22:12:38 +01:00

82 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef } from 'react'
import { X } from 'lucide-react'
import { cn } from '../../utils/format'
interface ModalProps {
title: string
onClose: () => void
children: React.ReactNode
/** Extra classes applied to the inner panel */
className?: string
/** Width preset defaults to 'md' */
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
}
const sizeMap: Record<NonNullable<ModalProps['size']>, string> = {
sm: 'max-w-sm',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
full: 'max-w-full mx-4',
}
export default function Modal({ title, onClose, children, className, size = 'md' }: ModalProps) {
const backdropRef = useRef<HTMLDivElement>(null)
/* Close on Escape */
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [onClose])
/* Prevent scroll on body while modal is open */
useEffect(() => {
document.body.style.overflow = 'hidden'
return () => { document.body.style.overflow = '' }
}, [])
function handleBackdropClick(e: React.MouseEvent<HTMLDivElement>) {
if (e.target === backdropRef.current) onClose()
}
return (
<div
ref={backdropRef}
onClick={handleBackdropClick}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
>
<div
className={cn(
'relative w-full rounded-xl shadow-2xl flex flex-col max-h-[90vh]',
sizeMap[size],
className,
)}
style={{ backgroundColor: 'var(--color-bg-surface)' }}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border-default shrink-0">
<h2 id="modal-title" className="text-lg font-semibold text-content">
{title}
</h2>
<button
onClick={onClose}
className="p-1.5 rounded-md text-content-muted hover:text-content-secondary hover:bg-surface-muted transition-colors"
aria-label="Close"
>
<X size={18} />
</button>
</div>
{/* Body */}
<div className="overflow-y-auto flex-1">{children}</div>
</div>
</div>
)
}