56 lines
2.1 KiB
TypeScript
56 lines
2.1 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
|
|
interface FormModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
title: string;
|
|
description?: string;
|
|
children: React.ReactNode;
|
|
wide?: boolean;
|
|
}
|
|
|
|
export function FormModal({ open, onClose, title, description, children, wide }: FormModalProps) {
|
|
const overlayRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (open) document.body.style.overflow = 'hidden';
|
|
return () => { document.body.style.overflow = ''; };
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
function handleKey(e: KeyboardEvent) {
|
|
if (e.key === 'Escape' && open) onClose();
|
|
}
|
|
window.addEventListener('keydown', handleKey);
|
|
return () => window.removeEventListener('keydown', handleKey);
|
|
}, [open, onClose]);
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div
|
|
ref={overlayRef}
|
|
className="fixed inset-0 z-50 flex items-start justify-center pt-[10vh] px-4 bg-surface-900/40 backdrop-blur-sm overflow-y-auto"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="form-modal-title"
|
|
onClick={(e) => { if (e.target === overlayRef.current) onClose(); }}
|
|
>
|
|
<div className={`bg-white dark:bg-surface-800 rounded-xl shadow-xl w-full p-6 mb-10 animate-in fade-in slide-in-from-top-4 duration-200 ${wide ? 'max-w-2xl' : 'max-w-lg'}`}>
|
|
<div className="flex items-start justify-between mb-5">
|
|
<div>
|
|
<h2 id="form-modal-title" className="text-lg font-semibold text-surface-900 dark:text-surface-200">{title}</h2>
|
|
{description && <p className="mt-1 text-sm text-surface-500 dark:text-surface-400">{description}</p>}
|
|
</div>
|
|
<button onClick={onClose} className="p-1 text-surface-400 hover:text-surface-600 dark:hover:text-surface-300 transition-colors cursor-pointer rounded-lg hover:bg-surface-50 dark:hover:bg-surface-700" aria-label="Close">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><path d="M6 6l8 8M14 6l-8 8" /></svg>
|
|
</button>
|
|
</div>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|