ADD new frontend

This commit is contained in:
Urtzi Alfaro
2025-08-28 10:41:04 +02:00
parent 9c247a5f99
commit 0fd273cfce
492 changed files with 114979 additions and 1632 deletions

View File

@@ -0,0 +1,359 @@
import React, { forwardRef, useEffect, useRef, HTMLAttributes } from 'react';
import { clsx } from 'clsx';
export interface ModalProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onClose'> {
isOpen: boolean;
onClose: () => void;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
variant?: 'default' | 'centered' | 'drawer-left' | 'drawer-right' | 'drawer-top' | 'drawer-bottom';
closeOnOverlayClick?: boolean;
closeOnEscape?: boolean;
showCloseButton?: boolean;
preventScroll?: boolean;
initialFocus?: React.RefObject<HTMLElement>;
finalFocus?: React.RefObject<HTMLElement>;
overlayClassName?: string;
contentClassName?: string;
portalId?: string;
}
export interface ModalHeaderProps extends HTMLAttributes<HTMLDivElement> {
title?: string;
subtitle?: string;
showCloseButton?: boolean;
onClose?: () => void;
}
export interface ModalBodyProps extends HTMLAttributes<HTMLDivElement> {
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
}
export interface ModalFooterProps extends HTMLAttributes<HTMLDivElement> {
justify?: 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
}
const Modal = forwardRef<HTMLDivElement, ModalProps>(({
isOpen,
onClose,
size = 'md',
variant = 'default',
closeOnOverlayClick = true,
closeOnEscape = true,
showCloseButton = true,
preventScroll = true,
initialFocus,
finalFocus,
overlayClassName,
contentClassName,
className,
children,
...props
}, ref) => {
const overlayRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
// Handle escape key
useEffect(() => {
if (!isOpen || !closeOnEscape) return;
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, closeOnEscape, onClose]);
// Handle body scroll lock
useEffect(() => {
if (!isOpen || !preventScroll) return;
const originalStyle = window.getComputedStyle(document.body).overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = originalStyle;
};
}, [isOpen, preventScroll]);
// Handle focus management
useEffect(() => {
if (!isOpen) return;
// Store the currently focused element
previousFocusRef.current = document.activeElement as HTMLElement;
// Set focus to initial focus element or modal content
const timer = setTimeout(() => {
const focusTarget = initialFocus?.current || contentRef.current;
if (focusTarget) {
focusTarget.focus();
}
}, 100);
return () => {
clearTimeout(timer);
// Return focus to previously focused element
if (finalFocus?.current) {
finalFocus.current.focus();
} else if (previousFocusRef.current) {
previousFocusRef.current.focus();
}
};
}, [isOpen, initialFocus, finalFocus]);
// Handle overlay click
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (closeOnOverlayClick && event.target === event.currentTarget) {
onClose();
}
};
if (!isOpen) return null;
const sizeClasses = {
xs: 'max-w-xs',
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
full: 'max-w-full',
};
const variantClasses = {
default: {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex items-center justify-center p-4',
content: 'w-full transform transition-all duration-300 ease-out scale-100 opacity-100',
},
centered: {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex items-center justify-center p-4',
content: 'w-full transform transition-all duration-300 ease-out scale-100 opacity-100',
},
'drawer-left': {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex justify-start',
content: 'h-full w-full max-w-md transform transition-all duration-300 ease-out translate-x-0',
},
'drawer-right': {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex justify-end',
content: 'h-full w-full max-w-md transform transition-all duration-300 ease-out translate-x-0',
},
'drawer-top': {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex flex-col justify-start',
content: 'w-full transform transition-all duration-300 ease-out translate-y-0',
},
'drawer-bottom': {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex flex-col justify-end',
content: 'w-full transform transition-all duration-300 ease-out translate-y-0',
},
};
const overlayClasses = clsx(
variantClasses[variant].overlay,
'animate-in fade-in duration-200',
overlayClassName
);
const contentClasses = clsx(
'relative bg-modal-bg border border-modal-border rounded-lg shadow-xl',
'animate-in zoom-in-95 duration-200',
variantClasses[variant].content,
sizeClasses[size],
{
'rounded-none': variant.includes('drawer'),
'rounded-t-lg rounded-b-none': variant === 'drawer-bottom',
'rounded-b-lg rounded-t-none': variant === 'drawer-top',
'rounded-r-lg rounded-l-none': variant === 'drawer-left',
'rounded-l-lg rounded-r-none': variant === 'drawer-right',
},
contentClassName,
className
);
return (
<div
ref={overlayRef}
className={overlayClasses}
onClick={handleOverlayClick}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<div
ref={ref || contentRef}
className={contentClasses}
{...props}
>
{showCloseButton && (
<button
type="button"
className="absolute top-4 right-4 text-text-tertiary hover:text-text-primary transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-color-primary/20 rounded-md p-1"
onClick={onClose}
aria-label="Cerrar modal"
>
<svg
className="w-5 h-5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
{children}
</div>
</div>
);
});
const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(({
title,
subtitle,
showCloseButton = false,
onClose,
className,
children,
...props
}, ref) => {
const classes = clsx(
'px-6 py-4 border-b border-border-primary',
className
);
return (
<div
ref={ref}
className={classes}
{...props}
>
<div className="flex items-start justify-between">
<div className="flex-1">
{title && (
<h2
id="modal-title"
className="text-lg font-semibold text-text-primary"
>
{title}
</h2>
)}
{subtitle && (
<p className="mt-1 text-sm text-text-secondary">
{subtitle}
</p>
)}
{children}
</div>
{showCloseButton && onClose && (
<button
type="button"
className="ml-4 text-text-tertiary hover:text-text-primary transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-color-primary/20 rounded-md p-1"
onClick={onClose}
aria-label="Cerrar modal"
>
<svg
className="w-5 h-5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
</div>
);
});
const ModalBody = forwardRef<HTMLDivElement, ModalBodyProps>(({
padding = 'md',
className,
children,
...props
}, ref) => {
const paddingClasses = {
none: '',
sm: 'p-3',
md: 'p-6',
lg: 'p-8',
xl: 'p-10',
};
const classes = clsx(
paddingClasses[padding],
className
);
return (
<div
ref={ref}
id="modal-description"
className={classes}
{...props}
>
{children}
</div>
);
});
const ModalFooter = forwardRef<HTMLDivElement, ModalFooterProps>(({
justify = 'end',
padding = 'md',
className,
children,
...props
}, ref) => {
const paddingClasses = {
none: '',
sm: 'p-3',
md: 'px-6 py-4',
lg: 'px-8 py-5',
xl: 'px-10 py-6',
};
const justifyClasses = {
start: 'justify-start',
end: 'justify-end',
center: 'justify-center',
between: 'justify-between',
around: 'justify-around',
evenly: 'justify-evenly',
};
const classes = clsx(
'flex items-center gap-3 border-t border-border-primary',
paddingClasses[padding],
justifyClasses[justify],
className
);
return (
<div
ref={ref}
className={classes}
{...props}
>
{children}
</div>
);
});
Modal.displayName = 'Modal';
ModalHeader.displayName = 'ModalHeader';
ModalBody.displayName = 'ModalBody';
ModalFooter.displayName = 'ModalFooter';
export default Modal;
export { ModalHeader, ModalBody, ModalFooter };

View File

@@ -0,0 +1,3 @@
export { default } from './Modal';
export { default as Modal, ModalHeader, ModalBody, ModalFooter } from './Modal';
export type { ModalProps, ModalHeaderProps, ModalBodyProps, ModalFooterProps } from './Modal';