Refactor components and modals

This commit is contained in:
Urtzi Alfaro
2025-09-26 07:46:25 +02:00
parent cf4405b771
commit d573c38621
80 changed files with 3421 additions and 4617 deletions

View File

@@ -0,0 +1,201 @@
import React, { forwardRef } from 'react';
import { clsx } from 'clsx';
import { Button } from '../../ui';
import type { ButtonProps } from '../../ui';
export interface EmptyStateAction {
/** Texto del botón */
label: string;
/** Función al hacer click */
onClick: () => void;
/** Variante del botón */
variant?: ButtonProps['variant'];
/** Icono del botón */
icon?: React.ReactNode;
/** Mostrar loading en el botón */
isLoading?: boolean;
}
export interface EmptyStateProps {
/** Icono o ilustración */
icon?: React.ReactNode;
/** Título del estado vacío */
title?: string;
/** Descripción del estado vacío */
description?: string;
/** Variante del estado vacío */
variant?: 'no-data' | 'error' | 'search' | 'filter';
/** Acción principal */
primaryAction?: EmptyStateAction;
/** Acción secundaria */
secondaryAction?: EmptyStateAction;
/** Componente personalizado para ilustración */
illustration?: React.ReactNode;
/** Clase CSS adicional */
className?: string;
/** Tamaño del componente */
size?: 'sm' | 'md' | 'lg';
}
// Iconos SVG por defecto para cada variante
const DefaultIcons = {
'no-data': (
<svg className="w-16 h-16 text-text-tertiary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
),
'error': (
<svg className="w-16 h-16 text-color-error" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
),
'search': (
<svg className="w-16 h-16 text-text-tertiary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
),
'filter': (
<svg className="w-16 h-16 text-text-tertiary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.707A1 1 0 013 7V4z" />
</svg>
)
};
// Mensajes por defecto en español para cada variante
const DefaultMessages = {
'no-data': {
title: 'No hay datos disponibles',
description: 'Aún no se han registrado elementos en esta sección. Comience agregando su primer elemento.'
},
'error': {
title: 'Ha ocurrido un error',
description: 'No se pudieron cargar los datos. Por favor, inténtelo de nuevo más tarde.'
},
'search': {
title: 'Sin resultados de búsqueda',
description: 'No se encontraron elementos que coincidan con su búsqueda. Intente con términos diferentes.'
},
'filter': {
title: 'Sin resultados con estos filtros',
description: 'No se encontraron elementos que coincidan con los filtros aplicados. Ajuste los filtros para ver más resultados.'
}
};
const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(({
icon,
title,
description,
variant = 'no-data',
primaryAction,
secondaryAction,
illustration,
className,
size = 'md',
...props
}, ref) => {
const defaultMessage = DefaultMessages[variant];
const displayTitle = title || defaultMessage.title;
const displayDescription = description || defaultMessage.description;
const displayIcon = illustration || icon || DefaultIcons[variant];
const sizeClasses = {
sm: 'py-8 px-4',
md: 'py-12 px-6',
lg: 'py-20 px-8'
};
const titleSizeClasses = {
sm: 'text-lg',
md: 'text-xl',
lg: 'text-2xl'
};
const descriptionSizeClasses = {
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg'
};
const iconContainerClasses = {
sm: 'mb-4',
md: 'mb-6',
lg: 'mb-8'
};
const containerClasses = clsx(
'flex flex-col items-center justify-center text-center',
'min-h-[200px] max-w-md mx-auto',
sizeClasses[size],
className
);
return (
<div
ref={ref}
className={containerClasses}
role="status"
aria-live="polite"
{...props}
>
{/* Icono o Ilustración */}
{displayIcon && (
<div className={clsx('flex-shrink-0', iconContainerClasses[size])}>
{displayIcon}
</div>
)}
{/* Título */}
{displayTitle && (
<h3 className={clsx(
'font-semibold text-text-primary mb-2',
titleSizeClasses[size]
)}>
{displayTitle}
</h3>
)}
{/* Descripción */}
{displayDescription && (
<p className={clsx(
'text-text-secondary mb-6 leading-relaxed',
descriptionSizeClasses[size]
)}>
{displayDescription}
</p>
)}
{/* Acciones */}
{(primaryAction || secondaryAction) && (
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
{primaryAction && (
<Button
variant={primaryAction.variant || 'primary'}
onClick={primaryAction.onClick}
isLoading={primaryAction.isLoading}
leftIcon={primaryAction.icon}
className="w-full sm:w-auto"
>
{primaryAction.label}
</Button>
)}
{secondaryAction && (
<Button
variant={secondaryAction.variant || 'outline'}
onClick={secondaryAction.onClick}
isLoading={secondaryAction.isLoading}
leftIcon={secondaryAction.icon}
className="w-full sm:w-auto"
>
{secondaryAction.label}
</Button>
)}
</div>
)}
</div>
);
});
EmptyState.displayName = 'EmptyState';
export default EmptyState;

View File

@@ -0,0 +1,2 @@
export { default as EmptyState } from './EmptyState';
export type { EmptyStateProps, EmptyStateAction } from './EmptyState';