Files
workshop-manager/src/components/ui/Modal.tsx

124 lines
3.1 KiB
TypeScript

'use client';
import { Fragment, ReactNode, useEffect, useSyncExternalStore } from 'react';
import { createPortal } from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl';
}
const sizeStyles = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
};
function subscribe() {
return () => {};
}
function useIsMounted() {
return useSyncExternalStore(
subscribe,
() => true,
() => false
);
}
export function Modal({ isOpen, onClose, title, children, size = 'md' }: ModalProps) {
const isMounted = useIsMounted();
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, onClose]);
if (!isMounted || !isOpen) return null;
return createPortal(
<Fragment>
{/* Backdrop */}
<div
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
onClick={onClose}
aria-hidden="true"
/>
{/* Modal */}
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div
className={`
w-full ${sizeStyles[size]} rounded-xl border border-border
bg-card shadow-xl
animate-in fade-in-0 zoom-in-95
`}
role="dialog"
aria-modal="true"
aria-labelledby={title ? 'modal-title' : undefined}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
{title && (
<div className="flex items-center justify-between border-b border-border px-6 py-4">
<h2 id="modal-title" className="text-lg font-semibold text-foreground">
{title}
</h2>
<button
onClick={onClose}
className="rounded-lg p-1 text-muted hover:bg-card-hover hover:text-foreground transition-colors"
aria-label="Fermer"
>
<svg
className="h-5 w-5"
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>
)}
{/* Content */}
<div className="p-6">{children}</div>
</div>
</div>
</Fragment>,
document.body
);
}
interface ModalFooterProps {
children: ReactNode;
}
export function ModalFooter({ children }: ModalFooterProps) {
return (
<div className="flex items-center justify-end gap-3 border-t border-border -mx-6 -mb-6 mt-6 px-6 py-4 bg-card-hover/50 rounded-b-xl">
{children}
</div>
);
}