2025-12-03 21:56:50 +00:00
|
|
|
import { useEffect, useRef } from 'react';
|
2026-01-07 09:31:02 +00:00
|
|
|
import { createPortal } from 'react-dom';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { FocusTrap } from './focus-trap';
|
|
|
|
|
import { X } from 'lucide-react';
|
|
|
|
|
import { Button } from './button';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
|
|
|
|
|
export interface ModalProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
title?: string;
|
|
|
|
|
closeOnOverlayClick?: boolean;
|
|
|
|
|
closeOnEscape?: boolean;
|
|
|
|
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
|
|
|
className?: string;
|
2026-01-07 09:31:02 +00:00
|
|
|
footer?: React.ReactNode;
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sizeClasses = {
|
|
|
|
|
sm: 'max-w-sm',
|
|
|
|
|
md: 'max-w-md',
|
2026-01-07 09:31:02 +00:00
|
|
|
lg: 'max-w-2xl',
|
|
|
|
|
xl: 'max-w-4xl',
|
|
|
|
|
full: 'max-w-full m-4 h-[calc(100vh-2rem)]',
|
2025-12-03 21:56:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-07 09:31:02 +00:00
|
|
|
* Composant Modal avec design Kodo
|
2025-12-03 21:56:50 +00:00
|
|
|
*/
|
|
|
|
|
export function Modal({
|
|
|
|
|
open,
|
|
|
|
|
onClose,
|
|
|
|
|
children,
|
|
|
|
|
title,
|
|
|
|
|
closeOnOverlayClick = true,
|
|
|
|
|
closeOnEscape = true,
|
|
|
|
|
size = 'md',
|
|
|
|
|
className,
|
2026-01-07 09:31:02 +00:00
|
|
|
footer,
|
2025-12-03 21:56:50 +00:00
|
|
|
}: ModalProps) {
|
|
|
|
|
const modalRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
// Empêcher le scroll du body quand le modal est ouvert
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open) {
|
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
|
return () => {
|
|
|
|
|
document.body.style.overflow = '';
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-12-27 17:40:36 +00:00
|
|
|
return undefined;
|
2025-12-03 21:56:50 +00:00
|
|
|
}, [open]);
|
|
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
// Gestion de la touche Escape
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!closeOnEscape || !open) return;
|
|
|
|
|
|
|
|
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
onClose();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handleEscape);
|
|
|
|
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
|
|
|
}, [open, closeOnEscape, onClose]);
|
|
|
|
|
|
|
|
|
|
if (!open) return null;
|
|
|
|
|
|
|
|
|
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
2025-12-03 21:56:50 +00:00
|
|
|
if (closeOnOverlayClick && e.target === e.currentTarget) {
|
|
|
|
|
onClose();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
return createPortal(
|
2025-12-03 21:56:50 +00:00
|
|
|
<div
|
2026-01-07 09:31:02 +00:00
|
|
|
className="fixed inset-0 z-[100] flex items-center justify-center p-4"
|
2025-12-03 21:56:50 +00:00
|
|
|
onClick={handleOverlayClick}
|
|
|
|
|
>
|
2026-01-07 09:31:02 +00:00
|
|
|
{/* Backdrop */}
|
|
|
|
|
<div className="absolute inset-0 bg-kodo-void/90 backdrop-blur-sm animate-fadeIn" />
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
{/* Modal Content */}
|
2026-01-07 09:31:02 +00:00
|
|
|
<FocusTrap>
|
2025-12-03 21:56:50 +00:00
|
|
|
<div
|
|
|
|
|
ref={modalRef}
|
|
|
|
|
className={cn(
|
2026-01-07 09:31:02 +00:00
|
|
|
'relative w-full bg-kodo-graphite border border-kodo-steel rounded-xl shadow-2xl flex flex-col overflow-hidden animate-scaleIn',
|
2025-12-03 21:56:50 +00:00
|
|
|
sizeClasses[size],
|
2026-01-13 18:47:57 +00:00
|
|
|
className,
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
{/* Header */}
|
2026-01-07 09:31:02 +00:00
|
|
|
{title && (
|
|
|
|
|
<div className="p-4 border-b border-kodo-steel bg-kodo-ink flex justify-between items-center shrink-0">
|
2026-01-13 18:47:57 +00:00
|
|
|
<h3 className="font-bold text-white text-lg font-display">
|
|
|
|
|
{title}
|
|
|
|
|
</h3>
|
2026-01-07 09:31:02 +00:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
className="ml-auto"
|
|
|
|
|
>
|
|
|
|
|
<X className="w-5 h-5" />
|
|
|
|
|
</Button>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
{/* Body */}
|
|
|
|
|
<div className="p-6 overflow-y-auto custom-scrollbar flex-1">
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
{footer && (
|
|
|
|
|
<div className="p-4 border-t border-kodo-steel bg-kodo-ink shrink-0 flex justify-end gap-3">
|
|
|
|
|
{footer}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
</FocusTrap>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>,
|
2026-01-13 18:47:57 +00:00
|
|
|
document.body,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
}
|