Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica
- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind - Frontend Homenest (versión alternativa) - Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp) - WACRM fork (Next.js 16 + Supabase) - Hermes + Bridge + Skills (Qwen3.6 via Nan Builders) - Scripts de migración y operación - Documentación extensiva en docs/
This commit is contained in:
86
frontend/src/components/ui/Modal.tsx
Normal file
86
frontend/src/components/ui/Modal.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: ReactNode;
|
||||
children: ReactNode;
|
||||
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
||||
footer?: ReactNode;
|
||||
}
|
||||
|
||||
const maxWidthClasses: Record<NonNullable<ModalProps['maxWidth']>, string> = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-xl',
|
||||
'2xl': 'max-w-2xl',
|
||||
full: 'max-w-[calc(100vw-2rem)]',
|
||||
};
|
||||
|
||||
export const Modal: FC<ModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
maxWidth = 'md',
|
||||
footer,
|
||||
}) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const originalOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('keydown', handleKey);
|
||||
return () => {
|
||||
document.body.style.overflow = originalOverflow;
|
||||
document.removeEventListener('keydown', handleKey);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-brand-dark/40"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn(
|
||||
'relative bg-theme-surface rounded-2xl shadow-xl w-full flex flex-col max-h-[calc(100vh-2rem)]',
|
||||
maxWidthClasses[maxWidth]
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-theme-border">
|
||||
<h3 className="font-heading text-lg sm:text-xl text-theme-heading pr-4">{title}</h3>
|
||||
<Button variant="ghost" size="sm" onClick={onClose} aria-label="Cerrar">
|
||||
<X size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-5 sm:p-6">{children}</div>
|
||||
{footer && (
|
||||
<div className="px-5 py-4 border-t border-theme-border flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
Reference in New Issue
Block a user