carconnect-gatherleads-ui-lib
v3.1.9
Published
`carconnect-gatherleads-ui-lib` es una librería de componentes UI desarrollada para **CarConnect**, construida con **React**, **Tailwind v4** y **shadcn/ui**. Permite reutilizar componentes consistentes en distintas aplicaciones del ecosistema CarConnect.
Readme
carconnect-gatherleads-ui-lib
carconnect-gatherleads-ui-lib es una librería de componentes UI desarrollada para CarConnect, construida con React, Tailwind v4 y shadcn/ui. Permite reutilizar componentes consistentes en distintas aplicaciones del ecosistema CarConnect.
Gestión de Versiones con Semantic Release
La librería utiliza Semantic Release para automatizar el versionado y el registro de cambios.
El versionado sigue SemVer (MAJOR.MINOR.PATCH), y los commits deben seguir la convención Conventional Commits:
Tipos de commits más comunes
feat: Nueva funcionalidad → incrementa MINORfix: Corrección de errores → incrementa PATCHchore: Tareas internas, sin impacto en la APIdocs: Cambios en documentaciónrefactor: Refactorización de códigoperf: Mejoras de rendimientotest: Añadir o corregir testsBREAKING CHANGE: Cambios incompatibles → incrementa MAJOR
Gestión de Commits y Versiones
La librería utiliza Commitizen para automatizar y estandarizar los commits siguiendo la convención Conventional Commits.
Esto permite que Semantic Release pueda generar automáticamente la versión correcta y actualizar el CHANGELOG.md.
Script para commits
npm run commitScripts disponibles de desarrollo
Levanta Storybook para visualizar los componentes en desarrollo_
npm run storybook
Inicia el entorno de desarrollo de la librería_
npm run dev
Construye la librería para producción_
npm run build
Utiliza Commitizen para generar commits estandarizados.
npm run commit
Instalación
Puedes instalar la librería desde npm:
npm install carconnect-gatherleads-ui-lib
# o con yarn
yarn add carconnect-gatherleads-ui-lib📋 Guía Completa: GatherFormBuilder
🚀 Introducción
GatherFormBuilder es un componente React que permite crear formularios dinámicos y responsivos de manera declarativa. Solo necesitas definir una configuración y el componente se encarga de renderizar todos los campos, validaciones y layouts automáticamente.
📦 Uso Básico
import GatherFormBuilder from '@/components/GatherFormBuilder';
function MiFormulario() {
const handleSubmit = async (data) => {
console.log('Datos enviados:', data);
};
return (
<GatherFormBuilder
config={{
title: "Registro de Usuario",
orderFields:["email", "nombre"]; // Array con el orden específico de los campos (usando los nombres de los campos)
description: "Complete sus datos personales",
fields: [
{
name: "nombre",
label: "Nombre completo",
type: "text",
required: true
},
{
name: "email",
label: "Correo electrónico",
type: "email",
required: true
}
],
submitButton: {
text: "Registrarse",
variant: "gather-primary"
}
}}
onSubmit={handleSubmit}
/>
);
}🎨 Propiedades de Layout y sus Combinaciones en GatherFormBuilder
📊 Matriz de Compatibilidad: Layout + Propiedades
1. Layout VERTICAL
El layout por defecto, apila campos uno debajo del otro.
Propiedades que aplican:
- ✅
verticalSpacing- Espacio entre campos - ✅
gap- Espacio general (como fallback) - ✅
className- Clases CSS adicionales - ❌ Todas las demás propiedades de layout se ignoran
const verticalExample: GatherFormBuilderProps['config'] = {
layout: 'vertical', // o sin especificar (default)
// PROPIEDADES QUE FUNCIONAN:
verticalSpacing: '4', // space-y-4 = 16px entre campos
gap: 'gap-6', // se usa si no hay verticalSpacing
className: 'custom-class',
// PROPIEDADES IGNORADAS (no tienen efecto):
gridCols: 3, // ❌ ignorado
alignment: 'center', // ❌ ignorado
noWrap: true, // ❌ ignorado
fields: [
{ name: 'field1', label: 'Campo 1', type: 'text' },
{ name: 'field2', label: 'Campo 2', type: 'text' },
],
};
// Clases CSS resultantes: "space-y-4"2. Layout HORIZONTAL
Dispone los campos en línea horizontal con flexbox.
Propiedades que aplican:
- ✅
gap- Espacio entre elementos - ✅
alignment- Alineación horizontal (start, center, end, between, around, evenly) - ✅
itemsAlignment- Alineación vertical (start, center, end, stretch) - ✅
noWrap- Previene el wrap de elementos - ✅
className- Clases CSS adicionales - ❌ Propiedades de grid se ignoran
const horizontalExample: GatherFormBuilderProps['config'] = {
layout: 'horizontal',
// PROPIEDADES QUE FUNCIONAN:
gap: 'gap-4', // 16px entre campos
alignment: 'between', // justify-between: espacio entre elementos
itemsAlignment: 'center', // items-center: centrado vertical
noWrap: false, // permite wrap (por defecto)
// PROPIEDADES IGNORADAS:
gridCols: 2, // ❌ ignorado
verticalSpacing: '4', // ❌ ignorado
showDividers: true, // ❌ ignorado
fields: [
{
name: 'search',
label: 'Buscar',
type: 'text',
width: 'w-2/3', // ocupa 2/3 del ancho
},
{
name: 'filter',
label: 'Filtro',
type: 'select',
width: 'w-1/3', // ocupa 1/3 del ancho
},
],
};
// Clases CSS resultantes: "flex flex-wrap gap-4 justify-between items-center"Ejemplo con noWrap:
const horizontalNoWrap: GatherFormBuilderProps['config'] = {
layout: 'horizontal',
noWrap: true, // NO permite que los elementos pasen a nueva línea
alignment: 'start', // justify-start: alineados a la izquierda
itemsAlignment: 'stretch', // items-stretch: altura completa
gap: 'gap-2',
fields: [
{ name: 'field1', label: 'Campo 1', type: 'text' },
{ name: 'field2', label: 'Campo 2', type: 'text' },
{ name: 'field3', label: 'Campo 3', type: 'text' },
],
};
// Clases CSS resultantes: "flex gap-2 justify-start items-stretch"
// Nota: sin "flex-wrap", los elementos NO se envuelven3. Layout GRID
Organiza campos en una cuadrícula con sistema de columnas.
Propiedades que aplican:
- ✅
gridCols- Número de columnas base (1-12) - ✅
smCols- Columnas en pantallas pequeñas (640px+) - ✅
lgCols- Columnas en pantallas grandes (1024px+) - ✅
xlCols- Columnas en pantallas extra grandes (1280px+) - ✅
autoFit- Auto-ajuste de columnas - ✅
gap- Espacio entre celdas - ✅
gridAlignment- Alineación del contenido de la grilla - ✅
gridItemsAlignment- Alineación de items en sus celdas - ✅
className- Clases CSS adicionales - ❌ Propiedades de flex se ignoran
const gridResponsive: GatherFormBuilderProps['config'] = {
layout: 'grid',
// PROPIEDADES QUE FUNCIONAN:
gridCols: 2, // 2 columnas en tablets (768px+)
smCols: 1, // 1 columna en móviles pequeños (640px+)
lgCols: 3, // 3 columnas en desktop (1024px+)
xlCols: 4, // 4 columnas en pantallas grandes (1280px+)
gap: 'gap-6', // 24px entre celdas
gridAlignment: 'center', // justify-items-center: contenido centrado
gridItemsAlignment: 'start', // items-start: alineados arriba
// PROPIEDADES IGNORADAS:
alignment: 'between', // ❌ ignorado (es para flex)
noWrap: true, // ❌ ignorado (es para flex)
verticalSpacing: '4', // ❌ ignorado
fields: [
{
name: 'fullWidth',
label: 'Campo ancho completo',
type: 'text',
colSpan: 2, // ocupa 2 columnas
},
{ name: 'half1', label: 'Mitad 1', type: 'text' },
{ name: 'half2', label: 'Mitad 2', type: 'text' },
{
name: 'triple',
label: 'Triple ancho',
type: 'textarea',
colSpan: 3, // ocupa 3 columnas
},
],
};
// Clases CSS resultantes:
// "grid grid-cols-1 md:grid-cols-2 sm:grid-cols-1 lg:grid-cols-3 xl:grid-cols-4 gap-6 justify-items-center items-start"Ejemplo con autoFit:
const gridAutoFit: GatherFormBuilderProps['config'] = {
layout: 'grid',
autoFit: true, // ACTIVA el auto-ajuste (min 250px por columna)
gap: 'gap-4',
// CUANDO autoFit está activo, estas se IGNORAN:
gridCols: 3, // ❌ ignorado con autoFit
smCols: 2, // ❌ ignorado con autoFit
lgCols: 4, // ❌ ignorado con autoFit
fields: [
{ name: 'field1', label: 'Campo 1', type: 'text' },
{ name: 'field2', label: 'Campo 2', type: 'text' },
{ name: 'field3', label: 'Campo 3', type: 'text' },
],
};
// Clases CSS resultantes:
// "grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4"4. Layout MASONRY
Similar a Pinterest, para campos de diferentes alturas.
Propiedades que aplican:
- ✅
gridCols- Número de columnas - ✅
gap- Espacio entre elementos - ✅
className- Clases CSS adicionales - ❌ Otras propiedades de grid/flex se ignoran
const masonryExample: GatherFormBuilderProps['config'] = {
layout: 'masonry',
// PROPIEDADES QUE FUNCIONAN:
gridCols: 3, // 3 columnas en desktop
gap: 'gap-4', // 16px entre elementos
// PROPIEDADES IGNORADAS:
smCols: 2, // ❌ ignorado (masonry no es responsive)
lgCols: 4, // ❌ ignorado
autoFit: true, // ❌ ignorado
alignment: 'center', // ❌ ignorado
fields: [
{
name: 'short',
label: 'Texto corto',
type: 'text',
},
{
name: 'tall',
label: 'Campo alto',
type: 'textarea',
rows: 8, // más alto que otros
},
{
name: 'medium',
label: 'Altura media',
type: 'multiselect',
options: [
/*...*/
],
},
],
};
// Clases CSS resultantes:
// "grid grid-cols-1 md:grid-cols-3 gap-4 auto-rows-max"5. Layout INLINE
Campos en línea con wrap automático, centrados verticalmente.
Propiedades que aplican:
- ✅
gap- Espacio entre elementos - ✅
className- Clases CSS adicionales - ❌ Todas las demás propiedades se ignoran
const inlineExample: GatherFormBuilderProps['config'] = {
layout: 'inline',
// PROPIEDADES QUE FUNCIONAN:
gap: 'gap-3', // 12px entre elementos
// PROPIEDADES IGNORADAS (inline es muy simple):
alignment: 'center', // ❌ ignorado (siempre flex-wrap)
gridCols: 2, // ❌ ignorado
verticalSpacing: '4', // ❌ ignorado
noWrap: true, // ❌ ignorado (siempre hace wrap)
fields: [
{
name: 'tag1',
label: 'Tag',
type: 'text',
width: 'w-auto', // ancho automático
},
{
name: 'tag2',
label: 'Otro Tag',
type: 'text',
width: 'w-auto',
},
],
};
// Clases CSS resultantes:
// "flex flex-wrap gap-3 items-center"6. Layout STACK
Vertical con opciones especiales de espaciado y divisores.
Propiedades que aplican:
- ✅
stackSpacing- Espaciado específico del stack - ✅
showDividers- Muestra líneas divisorias - ✅
className- Clases CSS adicionales - ❌ Propiedades de grid/flex se ignoran
const stackExample: GatherFormBuilderProps['config'] = {
layout: 'stack',
// PROPIEDADES QUE FUNCIONAN:
stackSpacing: '6', // space-y-6 = 24px entre elementos
showDividers: true, // añade líneas divisorias
// PROPIEDADES IGNORADAS:
gap: 'gap-4', // ❌ ignorado (usa stackSpacing)
verticalSpacing: '2', // ❌ ignorado (usa stackSpacing)
gridCols: 2, // ❌ ignorado
alignment: 'center', // ❌ ignorado
fields: [
{
name: 'section1',
label: 'Sección 1',
type: 'text',
},
{
name: 'section2',
label: 'Sección 2',
type: 'textarea',
},
{
name: 'section3',
label: 'Sección 3',
type: 'select',
options: [
/*...*/
],
},
],
};
// Clases CSS resultantes:
// "space-y-6 divide-y divide-gray-200"📋 Tabla Resumen de Compatibilidad
| Propiedad | vertical | horizontal | grid | masonry | inline | stack |
| -------------------- | -------- | ---------- | ---- | ------- | ------ | ----- |
| verticalSpacing | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| gap | ✅* | ✅ | ✅ | ✅ | ✅ | ❌ |
| alignment | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| itemsAlignment | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| noWrap | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| gridCols | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
| smCols | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| lgCols | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| xlCols | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| autoFit | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| gridAlignment | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| gridItemsAlignment | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| stackSpacing | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| showDividers | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| className | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
*gap en vertical solo se usa si no hay verticalSpacing
🎯 Ejemplo Completo Combinando Propiedades
const complexFormConfig: GatherFormBuilderProps['config'] = {
title: 'Formulario Complejo',
layout: 'grid',
// TODAS estas propiedades SÍ funcionan con layout="grid":
gridCols: 2, // 2 columnas base
smCols: 1, // 1 columna en móvil
lgCols: 3, // 3 columnas en desktop
xlCols: 4, // 4 columnas en pantallas grandes
gap: 'gap-6', // 24px entre celdas
gridAlignment: 'stretch', // contenido estirado
gridItemsAlignment: 'center', // items centrados verticalmente
className: 'bg-gray-50', // fondo gris
// Estas propiedades se IGNORAN porque no son para grid:
noWrap: true, // ❌ ignorado (es para horizontal)
stackSpacing: '4', // ❌ ignorado (es para stack)
showDividers: true, // ❌ ignorado (es para stack)
alignment: 'between', // ❌ ignorado (es para horizontal)
fields: [
{
name: 'fullRow',
label: 'Campo de ancho completo',
type: 'text',
colSpan: 4, // ocupa todas las columnas en xl
},
// más campos...
],
};🎯 Guía Completa de Tipos de Campos en GatherFormBuilder
📝 Tipos de Campos y sus Propiedades
1. GatherInputFieldConfig - Campos de Entrada de Texto
Campos básicos de entrada para diferentes tipos de datos textuales y numéricos.
interface GatherInputFieldConfig extends BaseFieldConfig {
type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
minLength?: number;
maxLength?: number;
pattern?: string;
patternMessage?: string;
min?: number; // Solo para type="number"
max?: number; // Solo para type="number"
size?: 'sm' | 'md' | 'lg';
variant?: 'default' | 'success' | 'warning' | 'error';
}Ejemplos por tipo:
// CAMPO DE TEXTO SIMPLE
{
name: "username",
label: "Nombre de usuario",
type: "text",
placeholder: "Ingrese su usuario",
required: true,
minLength: 3, // Mínimo 3 caracteres
maxLength: 20, // Máximo 20 caracteres
leftIcon: <UserIcon />, // Icono a la izquierda
size: "md", // Tamaño mediano
variant: "default", // Estilo por defecto
helperText: "Entre 3 y 20 caracteres",
width: "w-full" // Ancho completo
}
// CAMPO EMAIL
{
name: "email",
label: "Correo electrónico",
type: "email",
required: true,
pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
patternMessage: "Ingrese un email válido",
leftIcon: <MailIcon />,
variant: "default"
}
// CAMPO PASSWORD
{
name: "password",
label: "Contraseña",
type: "password",
required: true,
minLength: 8,
maxLength: 50,
pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
patternMessage: "Debe contener mayúsculas, minúsculas y números",
rightIcon: <EyeIcon />, // Para mostrar/ocultar
size: "lg" // Tamaño grande
}
// CAMPO NUMÉRICO
{
name: "age",
label: "Edad",
type: "number",
min: 18, // Valor mínimo
max: 120, // Valor máximo
required: true,
placeholder: "18",
variant: "default"
}
// CAMPO TELÉFONO
{
name: "phone",
label: "Teléfono",
type: "tel",
pattern: "^\\+?[1-9]\\d{1,14}$", // Formato internacional
patternMessage: "Formato: +1234567890",
leftIcon: <PhoneIcon />,
placeholder: "+52 555 123 4567"
}
// CAMPO URL
{
name: "website",
label: "Sitio web",
type: "url",
placeholder: "https://ejemplo.com",
pattern: "^https?://.*",
patternMessage: "Debe comenzar con http:// o https://",
leftIcon: <GlobeIcon />
}2. GatherInputSelectFieldConfig - Campo Combinado (Select + Input)
Combina un selector con un campo de entrada en un solo componente.
interface GatherInputSelectFieldConfig extends BaseFieldConfig {
type: 'inputSelect' | 'identification';
selectFieldName: string;
inputFieldName: string;
options: SelectInputOption[];
selectValue?: string;
inputValue?: string;
inputPlaceholder?: string;
selectPlaceholder?: string;
selectWidth?: number; // Porcentaje (0-100)
size?: 'sm' | 'md' | 'lg';
variant?: 'default' | 'success' | 'warning' | 'error';
}Ejemplos:
// CAMPO DE IDENTIFICACIÓN
{
name: "identification",
label: "Documento de identidad",
type: "inputSelect",
selectFieldName: "docType", // Campo para tipo de documento
inputFieldName: "docNumber", // Campo para número
selectWidth: 30, // Select ocupa 30% del ancho
options: [
{ value: "dni", label: "DNI" , validateFn:(val)=>validateDni(val), errorMessage:"Campo invalido"},
{ value: "passport", label: "Pasaporte" validatePattern: "^[A-Z0-9]{6,9}$", errorMessage: "El número de pasaporte no es válido"},
{ value: "ruc", label: "RUC", maxLength:13 },
{ value: "cedula", label: "Cédula" }
],
selectPlaceholder: "Tipo",
inputPlaceholder: "Número de documento",
required: true,
size: "md"
}
// CAMPO DE MONEDA Y MONTO
{
name: "amount",
label: "Monto a pagar",
type: "inputSelect",
selectFieldName: "currency",
inputFieldName: "value",
selectWidth: 25, // Select ocupa 25%
options: [
{ value: "usd", label: "USD" },
{ value: "eur", label: "EUR" },
{ value: "mxn", label: "MXN" }
],
selectValue: "usd", // Valor por defecto
inputPlaceholder: "0.00",
variant: "success"
}
// CAMPO DE CÓDIGO DE ÁREA Y TELÉFONO
{
name: "phoneWithCode",
label: "Teléfono con código",
type: "inputSelect",
selectFieldName: "countryCode",
inputFieldName: "phoneNumber",
selectWidth: 20,
options: [
{ value: "+1", label: "+1 USA" },
{ value: "+52", label: "+52 MEX" },
{ value: "+34", label: "+34 ESP" }
],
inputPlaceholder: "555 123 4567"
}3. GatherSelectFieldConfig - Campo de Selección Simple
interface GatherSelectFieldConfig extends BaseFieldConfig {
type: 'select';
value?: OptionType[];
options: Array<{
value: string | number;
label: string;
disabled?: boolean;
}>;
leftIcon?: React.ReactNode;
size?: 'sm' | 'md' | 'lg';
variant?: 'default' | 'success' | 'warning' | 'error';
}Ejemplos:
// SELECT SIMPLE
{
name: "country",
label: "País",
type: "select",
placeholder: "Seleccione un país",
required: true,
options: [
{ value: "mx", label: "México" },
{ value: "us", label: "Estados Unidos" },
{ value: "ca", label: "Canadá", disabled: true }, // Opción deshabilitada
{ value: "ar", label: "Argentina" }
],
leftIcon: <MapIcon />,
size: "md",
width: "w-full"
}
// SELECT DE CATEGORÍAS
{
name: "category",
label: "Categoría",
type: "select",
options: [
{ value: 1, label: "Tecnología" }, // value numérico
{ value: 2, label: "Salud" },
{ value: 3, label: "Educación" },
{ value: 4, label: "Finanzas" }
],
variant: "default",
helperText: "Seleccione la categoría principal"
}
// SELECT DE ESTADO/STATUS
{
name: "status",
label: "Estado",
type: "select",
options: [
{ value: "active", label: "✅ Activo" },
{ value: "pending", label: "⏳ Pendiente" },
{ value: "inactive", label: "❌ Inactivo" }
],
variant: "warning",
size: "sm"
}4. GatherMultiSelectFieldConfig - Selección Múltiple
interface GatherMultiSelectFieldConfig extends BaseFieldConfig {
type: 'multiselect';
options: OptionType[];
leftIcon?: React.ReactNode;
isClearable?: boolean; // Botón para limpiar selección
isSearchable?: boolean; // Permite buscar opciones
closeMenuOnSelect?: boolean; // Cierra menú al seleccionar
maxMenuHeight?: number; // Altura máxima en px
noOptionsMessage?: string; // Mensaje sin opciones
size?: 'sm' | 'md' | 'lg';
variant?: 'default' | 'success' | 'warning' | 'error';
}Ejemplos:
// MULTISELECT DE HABILIDADES
{
name: "skills",
label: "Habilidades técnicas",
type: "multiselect",
required: true,
options: [
{ value: "js", label: "JavaScript" },
{ value: "react", label: "React" },
{ value: "node", label: "Node.js" },
{ value: "python", label: "Python" },
{ value: "docker", label: "Docker" }
],
isSearchable: true, // Permite buscar
isClearable: true, // Botón limpiar todo
closeMenuOnSelect: false, // Mantiene menú abierto
maxMenuHeight: 200, // Altura máxima 200px
noOptionsMessage: "Sin resultados",
placeholder: "Seleccione múltiples habilidades",
helperText: "Puede seleccionar varias opciones"
}
// MULTISELECT DE DÍAS
{
name: "workDays",
label: "Días laborales",
type: "multiselect",
options: [
{ value: "mon", label: "Lunes" },
{ value: "tue", label: "Martes" },
{ value: "wed", label: "Miércoles" },
{ value: "thu", label: "Jueves" },
{ value: "fri", label: "Viernes" },
{ value: "sat", label: "Sábado" },
{ value: "sun", label: "Domingo" }
],
closeMenuOnSelect: true, // Cierra al seleccionar
size: "lg"
}5. GatherTextareaFieldConfig - Área de Texto
interface GatherTextareaFieldConfig extends BaseFieldConfig {
type: 'textarea';
rows?: number; // Filas visibles
cols?: number; // Columnas
maxLength?: number; // Máximo de caracteres
showCharCount?: boolean; // Mostrar contador
size?: 'sm' | 'md' | 'lg';
variant?: 'default' | 'success' | 'warning' | 'error';
resize?: 'none' | 'both' | 'horizontal' | 'vertical';
}Ejemplos:
// TEXTAREA SIMPLE
{
name: "description",
label: "Descripción",
type: "textarea",
placeholder: "Escriba una descripción detallada...",
rows: 4, // 4 filas visibles
maxLength: 500, // Máximo 500 caracteres
showCharCount: true, // Muestra contador: "50/500"
resize: "vertical", // Solo resize vertical
required: true,
helperText: "Mínimo 50 caracteres"
}
// TEXTAREA PARA COMENTARIOS
{
name: "comments",
label: "Comentarios adicionales",
type: "textarea",
rows: 6,
cols: 50,
resize: "both", // Resize en ambas direcciones
variant: "default",
size: "md"
}
// TEXTAREA NO REDIMENSIONABLE
{
name: "address",
label: "Dirección completa",
type: "textarea",
rows: 3,
resize: "none", // No permite redimensionar
maxLength: 200,
showCharCount: false,
width: "w-full"
}Características Destacadas
✨ Nuevas Funcionalidades:
- Scroll en selector de años - El selector ahora tiene altura máxima y scroll automático
- Control de años personalizables -
pastYearsCount={10}muestra año actual + últimos 10 años - Textos personalizables - Botones Apply/Cancel en cualquier idioma
- Callbacks para eventos -
onApply,onCancel,onOpenChangepara lógica personalizada - Formatos de fecha flexibles - Personaliza cómo se muestran las fechas
- Auto-cierre inteligente - Cierre automático tras selección para UX más rápida
- Modo sin botones -
hideActionButtons={true}para aplicar cambios inmediatamente
Ejemplos de Uso Avanzado
// EJEMPLO 1: Date Picker rápido sin confirmación
<GatherDatePicker
mode="single"
selected={date}
onSelect={setDate}
autoClose={true}
hideActionButtons={true}
placeholder="Selección rápida"
/>
// EJEMPLO 2: Con validación y callbacks
<GatherDatePicker
mode="single"
selected={date}
onSelect={setDate}
applyButtonText="Guardar Fecha"
cancelButtonText="Descartar"
onApply={(selectedDate) => {
// Validar y guardar
if (selectedDate) {
saveToAPI(selectedDate);
toast.success('Fecha guardada');
}
}}
onCancel={() => {
toast.info('Cambios descartados');
}}
/>
// EJEMPLO 3: Rango con formato personalizado y años limitados
<GatherDatePicker
mode="range"
selected={dateRange}
onSelect={setDateRange}
rangeFromFormat="dd/MM/yy"
rangeToFormat="dd/MM/yy"
pastYearsCount={5} // Solo últimos 5 años
placeholder="Del - Al"
/>
// EJEMPLO 4: Input de formulario completo
<GatherDatePickerInput
label="Fecha de Registro"
description="Seleccione la fecha de registro del cliente"
placeholder="dd/mm/aaaa"
mode="single"
selected={date}
onSelect={setDate}
dateFormat="dd/MM/yyyy"
showSelectedDate={true}
selectedDateLabel="Registrado el:"
autoClose={true}
minDate={subDays(new Date(), 365)}
maxDate={new Date()}
/>7. GatherCheckboxGroupFieldConfig - Grupo de Checkboxes
interface GatherCheckboxGroupFieldConfig extends BaseFieldConfig {
type: 'checkbox';
options: CheckboxOption[];
direction?: 'row' | 'column';
labelPosition?: 'left' | 'right';
}Ejemplos:
// CHECKBOXES EN COLUMNA
{
name: "interests",
label: "Áreas de interés",
type: "checkbox",
options: [
{ value: "tech", label: "Tecnología" },
{ value: "design", label: "Diseño" },
{ value: "marketing", label: "Marketing" },
{ value: "sales", label: "Ventas" }
],
direction: "column", // Disposición vertical
labelPosition: "right", // Etiqueta a la derecha del checkbox
helperText: "Seleccione todas las que apliquen"
}
// CHECKBOXES EN FILA
{
name: "permissions",
label: "Permisos",
type: "checkbox",
options: [
{ value: "read", label: "Lectura" },
{ value: "write", label: "Escritura" },
{ value: "delete", label: "Eliminar" }
],
direction: "row", // Disposición horizontal
labelPosition: "right",
required: true
}
// CHECKBOX DE TÉRMINOS
{
name: "agreements",
label: "Acuerdos legales",
type: "checkbox",
options: [
{
value: "terms",
label: "Acepto los términos y condiciones",
disabled:true // Desabilitar la opcion
},
{
value: "newsletter",
label: "Deseo recibir newsletter"
}
],
direction: "column"
}8. GatherRadioButtonFieldConfig - Radio Buttons
interface GatherRadioButtonFieldConfig extends BaseFieldConfig {
type: 'radioButton';
options: RadioOption[];
direction?: 'row' | 'column';
labelPosition?: 'left' | 'right';
}Ejemplos:
// RADIO BUTTONS BÁSICOS
{
name: "gender",
label: "Género",
type: "radioButton",
options: [
{ value: "male", label: "Masculino" },
{ value: "female", label: "Femenino" },
{ value: "other", label: "Otro" },
{ value: "na", label: "Prefiero no decir" }
],
direction: "column",
labelPosition: "right",
required: true
}
// RADIO BUTTONS DE PLAN
{
name: "subscriptionPlan",
label: "Seleccione su plan",
type: "radioButton",
options: [
{
value: "basic",
label: "Básico - $9/mes",
},
{
value: "pro",
label: "Pro - $29/mes",
},
{
value: "enterprise",
label: "Empresarial - $99/mes",
}
],
direction: "column",
helperText: "Puede cambiar su plan en cualquier momento"
}
// RADIO BUTTONS HORIZONTALES
{
name: "priority",
label: "Prioridad",
type: "radioButton",
options: [
{ value: "low", label: "🟢 Baja" },
{ value: "medium", label: "🟡 Media" },
{ value: "high", label: "🔴 Alta" }
],
direction: "row", // Disposición horizontal
labelPosition: "right"
}9. GatherSwitchFieldConfig - Switch/Toggle
interface GatherSwitchFieldConfig extends BaseFieldConfig {
type: 'switch';
defaultChecked?: boolean;
leftLabel?: string;
rightLabel?: string;
size?: 'sm' | 'md' | 'lg';
}Ejemplos:
// SWITCH SIMPLE
{
name: "notifications",
label: "Notificaciones",
type: "switch",
defaultChecked: true, // Activado por defecto
leftLabel: "No", // Etiqueta izquierda
rightLabel: "Sí", // Etiqueta derecha
size: "md",
helperText: "Recibir notificaciones por email"
}
// SWITCH DE MODO
{
name: "darkMode",
label: "Tema de la aplicación",
type: "switch",
defaultChecked: false,
leftLabel: "☀️ Claro",
rightLabel: "🌙 Oscuro",
size: "lg"
}
// SWITCH DE ESTADO
{
name: "isActive",
label: "Estado de la cuenta",
type: "switch",
leftLabel: "Inactiva",
rightLabel: "Activa",
defaultChecked: true,
size: "sm"
}
// SWITCH SIN ETIQUETAS LATERALES
{
name: "rememberMe",
label: "Recordar sesión",
type: "switch",
defaultChecked: false,
size: "md",
helperText: "Mantener la sesión iniciada"
}📊 Propiedades Comunes (BaseFieldConfig)
Todas las configuraciones de campo extienden de BaseFieldConfig:
interface BaseFieldConfig {
// IDENTIFICACIÓN
id?: string; // ID único del elemento HTML
name: string; // Nombre del campo (requerido)
label: string; // Etiqueta visible (requerido)
// CONTENIDO
placeholder?: string; // Texto de ayuda en campo vacío
helperText?: string; // Texto de ayuda debajo del campo
// ESTADO
required?: boolean; // Campo obligatorio
disabled?: boolean; // Campo deshabilitado
error?: boolean; // Indica error de validación
// DISEÑO
width?: 'w-full' | 'w-auto' | 'w-1/2' | 'w-1/3' | 'w-2/3' | 'w-1/4' | 'w-3/4';
colSpan?: number; // Columnas que ocupa en grid
fullWidth?: boolean; // Fuerza ancho completo
className?: string; // Clases CSS adicionales
}🎯 Ejemplo Completo: Formulario con Todos los Tipos
const formularioCompleto = {
title: 'Formulario de Registro Completo',
layout: 'grid',
gridCols: 2,
gap: 'gap-4',
fields: [
// INPUT TEXT
{
name: 'firstName',
label: 'Nombre',
type: 'text',
required: true,
leftIcon: <UserIcon />,
},
// INPUT EMAIL
{
name: 'email',
label: 'Email',
type: 'email',
required: true,
colSpan: 2,
},
// INPUT SELECT (Combinado)
{
name: 'phone',
label: 'Teléfono',
type: 'inputSelect',
selectFieldName: 'countryCode',
inputFieldName: 'number',
selectWidth: 25,
options: [
{ value: '+1', label: '+1' },
{ value: '+52', label: '+52' },
],
},
// SELECT
{
name: 'country',
label: 'País',
type: 'select',
options: [
{ value: 'mx', label: 'México' },
{ value: 'us', label: 'USA' },
],
},
// MULTISELECT
{
name: 'languages',
label: 'Idiomas',
type: 'multiselect',
options: [
{ value: 'es', label: 'Español' },
{ value: 'en', label: 'Inglés' },
{ value: 'fr', label: 'Francés' },
],
isSearchable: true,
colSpan: 2,
},
// TEXTAREA
{
name: 'bio',
label: 'Biografía',
type: 'textarea',
rows: 4,
maxLength: 500,
showCharCount: true,
colSpan: 2,
},
// DATE
{
name: 'birthDate',
label: 'Fecha de nacimiento',
type: 'date',
maxDate: new Date(),
},
// DATERANGE
{
name: 'availability',
label: 'Disponibilidad',
type: 'daterange',
singleDate: false,
},
// CHECKBOX GROUP
{
name: 'interests',
label: 'Intereses',
type: 'checkbox',
options: [
{ value: 'tech', label: 'Tecnología' },
{ value: 'art', label: 'Arte' },
],
direction: 'column',
},
// RADIO BUTTONS
{
name: 'plan',
label: 'Plan',
type: 'radioButton',
options: [
{ value: 'free', label: 'Gratis' },
{ value: 'pro', label: 'Pro' },
],
},
// SWITCH
{
name: 'newsletter',
label: 'Newsletter',
type: 'switch',
defaultChecked: false,
leftLabel: 'No',
rightLabel: 'Sí',
colSpan: 2,
},
],
submitButton: {
text: 'Registrarse',
variant: 'gather-primary',
size: 'lg',
},
};🔧 Guía de Validaciones y Props del GatherFormBuilder
⚡ Props Principales del Componente
GatherFormBuilderProps - Configuración del Componente
interface GatherFormBuilderProps {
config: FormConfig; // Requerido
onSubmit: (data: any) => void; // Requerido
onAction?: () => void; // Opcional
defaultValues?: Record<string, any>; // Opcional
resetData?: boolean; // Default: false
isLoading?: boolean; // Default: false
className?: string; // Opcional
validationMode?: 'onBlur' | 'onChange' | 'onSubmit' | 'onTouched' | 'all';
loadingComponent?: React.ReactNode; // Opcional
}Ejemplos de Props del Componente:
// EJEMPLO BÁSICO
<GatherFormBuilder
config={formConfig}
onSubmit={(data) => console.log(data)}
/>
// EJEMPLOS CON ONCHANGE
// Ejemplo 1:
<GatherFormBuilder
config={{
fields: [
{ name: 'user.firstName', label: 'Nombre', type: 'text' },
{ name: 'user.email', label: 'Email', type: 'email' },
{ name: 'phone', label: 'Telefono', type: 'tel' },
{ name: 'address.country', label: 'País', type: 'select' },
]
}}
watchFields={['user.email','phone']}
onChange={(data, changedField, formMethods) => {
// ✅ Funciona con datos anidados
if (changedField.name === 'address.country') {
formMethods.setValue('address.city', '');
}
}}
defaultValues={{
user: { firstName: '', email: '' },
address: { country: '', city: '' }
}}
onSubmit={(data) => console.log(data)}
/>
// Ejemplo 2:
const nestedFormConfig = {
fields: [
{ name: 'user.firstName', label: 'Nombre', type: 'text' },
{ name: 'user.lastName', label: 'Apellido', type: 'text' },
{ name: 'user.email', label: 'Email', type: 'email' },
{ name: 'address.country', label: 'País', type: 'select' },
{ name: 'address.city', label: 'Ciudad', type: 'select' },
{ name: 'address.street', label: 'Calle', type: 'text' },
{ name: 'preferences.notifications', label: 'Notificaciones', type: 'switch' },
]
};
<GatherFormBuilder
config={nestedFormConfig}
watchFields={['address.country','preferences.notifications']}
onChange={(data, changedField, formMethods) => {
console.log('Campo que cambió:', changedField.name); // ej: 'user.firstName'
console.log('Nuevo valor:', changedField.value);
console.log('Datos completos:', data); // { user: { firstName: '...', lastName: '...' }, address: { ... } }
// Validación condicional con campos anidados
if (changedField.name === 'address.country') {
// Limpiar ciudad cuando cambia país
formMethods.setValue('address.city', '');
formMethods.clearErrors('address.city');
}
// Lógica basada en preferencias anidadas
if (changedField.name === 'preferences.notifications' && !changedField.value) {
// Si deshabilita notificaciones, limpiar campos relacionados
formMethods.setValue('preferences.emailFrequency', 'never');
}
}}
defaultValues={{
user: {
firstName: '',
lastName: '',
email: ''
},
address: {
country: '',
city: '',
street: ''
},
preferences: {
notifications: true
}
}}
onSubmit={(data) => console.log('Submit:', data)}
/>
// Ejemplo 3:
<GatherFormBuilder
config={nestedFormConfig}
onChange={(data, changedField, formMethods) => {
if (changedField.name === 'user.email') {
// Validar email en tiempo real
validateEmail(changedField.value).then(isValid => {
if (!isValid) {
formMethods.setError('user.email', { message: 'Email inválido' });
}
});
}
}}
watchFields={['user.email', 'address.country']} // Solo observa estos campos anidados
debounceMs={300}
onSubmit={(data) => submitForm(data)}
/>
// Ejemplo 3:
const calculatorFormConfig = {
fields: [
{ name: 'invoice.items[0].quantity', label: 'Cantidad Item 1', type: 'number' },
{ name: 'invoice.items[0].price', label: 'Precio Item 1', type: 'number' },
{ name: 'invoice.items[1].quantity', label: 'Cantidad Item 2', type: 'number' },
{ name: 'invoice.items[1].price', label: 'Precio Item 2', type: 'number' },
{ name: 'invoice.discount', label: 'Descuento %', type: 'number' },
{ name: 'invoice.total', label: 'Total', type: 'number', disabled: true },
]
};
<GatherFormBuilder
config={calculatorFormConfig}
onChange={(data, changedField, formMethods) => {
// Recalcular total cuando cambien items o descuento
if (changedField.name.startsWith('invoice.items') || changedField.name === 'invoice.discount') {
const items = data.invoice?.items || [];
const subtotal = items.reduce((sum: number, item: any) => {
return sum + ((item?.quantity || 0) * (item?.price || 0));
}, 0);
const discount = (data.invoice?.discount || 0) / 100;
const total = subtotal * (1 - discount);
formMethods.setValue('invoice.total', total.toFixed(2));
}
}}
defaultValues={{
invoice: {
items: [
{ quantity: 0, price: 0 },
{ quantity: 0, price: 0 }
],
discount: 0,
total: 0
}
}}
onSubmit={(data) => console.log('Invoice:', data)}
/>
// EJEMPLO CON TODAS LAS PROPS
<GatherFormBuilder
// CONFIG: Configuración del formulario (requerida)
config={{
title: "Mi Formulario",
fields: [...],
layout: "grid"
}}
// ONSUBMIT: Maneja el envío (requerido)
onSubmit={async (data) => {
try {
await api.saveData(data);
toast.success("Guardado exitosamente");
} catch (error) {
toast.error("Error al guardar");
}
}}
// ONACTION: Acción secundaria (opcional)
onAction={() => {
navigate("/cancelar");
// o resetear el formulario
// o mostrar modal de confirmación
}}
// DEFAULTVALUES: Valores iniciales (opcional)
defaultValues={{
nombre: "Juan",
email: "[email protected]",
pais: "mx",
newsletter: true
}}
// RESETDATA: Limpiar después de enviar (opcional)
resetData={true} // Si true, limpia el form después del submit exitoso
// ISLOADING: Estado de carga global (opcional)
isLoading={isSubmitting} // Deshabilita todo el formulario
// CLASSNAME: Estilos del contenedor (opcional)
className="bg-blue-50 shadow-xl rounded-2xl p-8"
// VALIDATIONMODE: Cuándo validar (opcional)
validationMode="onChange" // Valida en cada cambio
// DEBOUCEMS: tiempo de respuesta para cualquier cambio en el form
debouceMs={100}
// WATCHFIELDS: campos del form que se vizualizaran en tiempo real
watchFields={['nombre', 'pais']}
// ONCHANGE: cambios en tiempo real del form
onChange={(data,changedField, formMethods )=> {
console.log({data,changedField,formMethods})
}}
// LOADINGCOMPONENT: Componente de carga (opcional)
loadingComponent={<CustomSpinner />}
/>Detalles de cada Prop:
1. validationMode - Modos de Validación
// ON BLUR - Valida cuando el campo pierde el foco
<GatherFormBuilder
validationMode="onBlur"
// El usuario escribe, al cambiar de campo se valida
config={config}
onSubmit={handleSubmit}
/>
// ON CHANGE - Valida en cada tecla/cambio
<GatherFormBuilder
validationMode="onChange" // Validación en tiempo real
// Muestra errores mientras el usuario escribe
config={config}
onSubmit={handleSubmit}
/>
// ON SUBMIT - Solo valida al enviar
<GatherFormBuilder
validationMode="onSubmit" // No molesta hasta el submit
// Ideal para formularios cortos
config={config}
onSubmit={handleSubmit}
/>
// ON TOUCHED - Valida después de tocar el campo
<GatherFormBuilder
validationMode="onTouched"
// Valida cuando el campo ha sido visitado
config={config}
onSubmit={handleSubmit}
/>
// ALL - Combina todos los modos
<GatherFormBuilder
validationMode="all" // Máxima validación
// Valida en blur, change, touched y submit
config={config}
onSubmit={handleSubmit}
/>2. defaultValues - Valores Iniciales
// VALORES SIMPLES
const defaultValues = {
// Campos de texto
nombre: 'María García',
email: '[email protected]',
edad: 25,
// Selects
pais: 'mx',
categoria: 2,
// Multi-select (array)
habilidades: ['js', 'react', 'node'],
// Checkbox group (array)
intereses: ['tech', 'design'],
// Radio button (single value)
plan: 'pro',
// Switch (boolean)
newsletter: true,
notificaciones: false,
// Fechas
fechaNacimiento: new Date('1990-01-15'),
periodo: {
start: new Date('2024-01-01'),
end: new Date('2024-12-31'),
},
// Input-Select combinado
identificacion: {
tipoDoc: 'dni',
numeroDoc: '12345678',
},
};
<GatherFormBuilder
config={config}
defaultValues={defaultValues}
onSubmit={handleSubmit}
/>;
// VALORES DESDE API
const [defaultValues, setDefaultValues] = useState({});
useEffect(() => {
api.getUserData().then((data) => {
setDefaultValues({
nombre: data.fullName,
email: data.email,
telefono: data.phone,
direccion: data.address,
});
});
}, []);
<GatherFormBuilder
config={config}
defaultValues={defaultValues}
onSubmit={handleSubmit}
/>;3. resetData - Comportamiento después del Submit
// RESETEAR DESPUÉS DE ENVIAR (true)
<GatherFormBuilder
config={config}
resetData={true} // Limpia el formulario después del submit exitoso
defaultValues={{ nombre: "" }} // Vuelve a estos valores
onSubmit={async (data) => {
await saveData(data);
// El formulario se limpia automáticamente
}}
/>
// MANTENER DATOS DESPUÉS DE ENVIAR (false)
<GatherFormBuilder
config={config}
resetData={false} // Mantiene los datos después del submit
onSubmit={async (data) => {
await saveData(data);
// Los datos permanecen en el formulario
}}
/>✅ Sistema de Validaciones
ValidationRule - Reglas de Validación
interface ValidationRule {
required?: boolean | string;
minLength?: { value: number; message: string };
maxLength?: { value: number; message: string };
pattern?: { value: RegExp; message: string };
min?: { value: number; message: string };
max?: { value: number; message: string };
validate?: (value: any, formData?: any) => boolean | string;
}Ejemplos de Validaciones:
1. Required - Campo Obligatorio
// REQUIRED SIMPLE
{
name: "email",
label: "Email",
type: "email",
required: true // Mensaje por defecto: "Este campo es requerido"
}
2. MinLength / MaxLength - Longitud de Texto
{
name: "password",
label: "Contraseña",
type: "password",
minLength: 8, // Se convierte internamente a objeto de validación
maxLength: 20,
// Genera automáticamente:
// minLength: { value: 8, message: "Mínimo 8 caracteres" }
// maxLength: { value: 20, message: "Máximo 20 caracteres" }
}
// CON MENSAJES PERSONALIZADOS (en la función generateValidationRules)
{
name: "username",
label: "Usuario",
type: "text",
minLength: 3,
maxLength: 15,
// Los mensajes se personalizan en generateValidationRules
}3. Pattern - Expresiones Regulares - Inputs
// VALIDACIÓN DE EMAIL
{
name: "email",
label: "Email",
type: "email",
pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
patternMessage: "Ingrese un email válido (ej: [email protected])"
}
// VALIDACIÓN DE TELÉFONO
{
name: "phone",
label: "Teléfono",
type: "tel",
pattern: "^[0-9]{10}$",
patternMessage: "El teléfono debe tener 10 dígitos"
}
// VALIDACIÓN DE CÓDIGO POSTAL
{
name: "zipCode",
label: "Código Postal",
type: "text",
pattern: "^[0-9]{5}$",
patternMessage: "El código postal debe tener 5 dígitos"
}
// VALIDACIÓN DE CONTRASEÑA FUERTE
{
name: "password",
label: "Contraseña",
type: "password",
pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$",
patternMessage: "Debe incluir mayúsculas, minúsculas, números y caracteres especiales"
}4. Min / Max - Valores Numéricos
// EDAD
{
name: "age",
label: "Edad",
type: "number",
min: 18,
max: 100,
required: true
// Mensajes automáticos:
// "El valor mínimo es 18"
// "El valor máximo es 100"
}
// CANTIDAD
{
name: "quantity",
label: "Cantidad",
type: "number",
min: 1,
max: 999,
required: true
}
// PRECIO
{
name: "price",
label: "Precio",
type: "number",
min: 0.01,
max: 999999.99,
placeholder: "0.00"
}5. Validate - Validación Personalizada
// La función validate se implementaría en generateValidationRules
// Aquí ejemplos de cómo se usaría:
// VALIDACIÓN PERSONALIZADA SIMPLE
{
name: "confirmPassword",
label: "Confirmar Contraseña",
type: "password",
// En generateValidationRules se agregaría:
// validate: (value, formData) => {
// return value === formData.password || "Las contraseñas no coinciden";
// }
}
// VALIDACIÓN DE EMAIL ÚNICO
{
name: "email",
label: "Email",
type: "email",
// validate: async (value) => {
// const exists = await checkEmailExists(value);
// return !exists || "Este email ya está registrado";
// }
}
// VALIDACIÓN CONDICIONAL
{
name: "companyName",
label: "Nombre de Empresa",
type: "text",
// validate: (value, formData) => {
// if (formData.userType === "business" && !value) {
// return "El nombre de empresa es requerido para cuentas empresariales";
// }
// return true;
// }
}🎨 Configuración de Botones
SubmitButton - Botón Principal
// CONFIGURACIÓN COMPLETA
submitButton: {
// TEXTO Y CONTENIDO
text: "Enviar Formulario", // Texto del botón
children: <CustomContent />, // O contenido JSX personalizado
// VISUAL
variant: "gather-primary", // Estilo visual
size: "lg", // Tamaño
className: "w-full mt-6", // Clases adicionales
// ICONOS
leftIcon: <SendIcon />, // Icono izquierdo
rightIcon: <ArrowRightIcon />, // Icono derecho
// ESTADO
disabled: false, // Deshabilitado
loading: isSubmitting, // Estado de carga
loadingText: "Procesando..." // Texto durante carga
}ActionButton - Botón Secundario
// BOTÓN DE LIMPIAR
actionButton: {
text: "Limpiar Formulario",
variant: "gather-outline",
size: "default",
leftIcon: <RefreshIcon />,
className: "min-w-32"
}
// BOTÓN DE CANCELAR
actionButton: {
text: "Cancelar",
variant: "gather-ghost",
leftIcon: <XIcon />,
// onAction ejecutará la función definida en props
}
// BOTÓN DE GUARDAR BORRADOR
actionButton: {
text: "Guardar Borrador",
variant: "gather-secondary",
leftIcon: <SaveIcon />
}📋 Ejemplos Completos de Uso
Formulario con Validaciones Complejas:
const FormularioConValidaciones = () => {
const [isLoading, setIsLoading] = useState(false);
const config: GatherFormBuilderProps['config'] = {
title: 'Registro con Validaciones',
layout: 'grid',
gridCols: 2,
gap: 'gap-4',
fields: [
{
name: 'username',
label: 'Usuario',
type: 'text',
required: 'El nombre de usuario es obligatorio',
minLength: 3,
maxLength: 20,
pattern: '^[a-zA-Z0-9_]+$',
patternMessage: 'Solo letras, números y guión bajo',
leftIcon: <UserIcon />,
},
{
name: 'email',
label: 'Email',
type: 'email',
required: true,
colSpan: 2,
},
{
name: 'age',
label: 'Edad',
type: 'number',
required: true,
min: 18,
max: 100,
},
{
name: 'password',
label: 'Contraseña',
type: 'password',
required: true,
minLength: 8,
pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$',
patternMessage: 'Debe tener mayúsculas, minúsculas y números',
},
],
submitButton: {
text: 'Registrarse',
variant: 'gather-primary',
size: 'lg',
loading: isLoading,
loadingText: 'Registrando...',
},
actionButton: {
text: 'Limpiar',
variant: 'gather-outline',
},
};
const handleSubmit = async (data: any) => {
setIsLoading(true);
try {
await api.register(data);
toast.success('Registro exitoso');
} catch (error) {
toast.error('Error en el registro');
} finally {
setIsLoading(false);
}
};
const handleAction = () => {
console.log('Formulario limpiado');
};
return (
<GatherFormBuilder
config={config}
onSubmit={handleSubmit}
onAction={handleAction}
validationMode='onChange'
resetData={true}
isLoading={isLoading}
defaultValues={{
age: 18,
}}
className='mx-auto max-w-2xl'
/>
);
};Esta guía completa detalla todas las validaciones y props disponibles en GatherFormBuilder con ejemplos prácticos de implementación.
📊 Guía Completa de GatherTable
🎯 Introducción
GatherTable es un sistema completo de componentes para crear tablas dinámicas, responsivas y con funcionalidades avanzadas como ordenamiento, scroll automático, estados de carga y renderizado personalizado.
🏗️ Arquitectura de Componentes
GatherTable (Contenedor principal)
├── GatherTableCaption (Título/descripción)
├── GatherTableHeader (Encabezados con ordenamiento)
├── GatherTableBody (Cuerpo con datos)
│ ├── GatherTableRow (Filas)
│ │ └── TableCell (Celdas)
└── GatherTableFooter (Pie de tabla)📋 Uso Básico con renderCell por Defecto
Ejemplo Simple con Datos Básicos
import {
GatherTable,
GatherTableHeader,
GatherTableBody,
} from '@/components/table';
import { createColumns } from '@/utils/table';
interface User {
id: number;
name: string;
email: string;
role: string;
isActive: boolean;
}
const BasicTableExample = () => {
// Crear columnas tipadas con autocompletado
const columns = createColumns<User>([
{
key: 'id',
label: 'ID',
width: 80,
align: 'center',
sortable: true,
},
{
key: 'name',
label: 'Nombre',
sortable: true,
},
{
key: 'email',
label: 'Correo Electrónico',
sortable: true,
},
{
key: 'role',
label: 'Rol',
width: 150,
},
{
key: 'isActive',
label: 'Estado',
align: 'center',
width: 100,
sortable: true,
},
]);
const users: User[] = [
{
id: 1,
name: 'Juan Pérez',
email: '[email protected]',
role: 'Admin',
isActive: true,
},
{
id: 2,
name: 'María García',
email: '[email protected]',
role: 'Usuario',
isActive: false,
},
{
id: 3,
name: 'Carlos López',
email: '[email protected]',
role: 'Editor',
isActive: true,
},
];
return (
<GatherTable maxHeight='400px'>
<GatherTableHeader
columns={columns}
sticky // Header fijo al hacer scroll
/>
<GatherTableBody
columns={columns}
data={users}
emptyMessage='No hay usuarios registrados'
/>
</GatherTable>
);
};RenderCell por Defecto - Manejo Automático de Tipos
El defaultRenderCell maneja automáticamente diferentes tipos de datos:
const defaultRenderCell = (
item: T,
column: GatherTableColumn
): React.ReactNode => {
const value = item[column.key];
// null o undefined → cadena vacía
if (value === null || value === undefined) return '';
// boolean → "true" o "false"
if (typeof value === 'boolean') return String(value);
// number → formato con separadores de miles
if (typeof value === 'number') return value.toLocaleString();
// Date → formato de fecha local
if (value instanceof Date) return value.toLocaleDateString();
// React Element → renderiza directamente
if (React.isValidElement(value)) return value;
// Objeto o función → intenta renderizar como ReactNode
if (typeof value === 'object' || typeof value === 'function') {
return value as React.ReactNode;
}
// Todo lo demás → convierte a string
return String(value);
};🎨 Uso con renderCell por defecto - utilizando una interfaz de tabla
Ejemplo Completo con Componentes Personalizados
import { Badge } from '@/components/ui/badge';
import { GatherButton } from '@/components/button';
import { useTableSort } from '@/hooks/useTableSort';
interface BenefitDataTable {
name: string;
type: React.ReactNode; // Badge component
detail: string;
actions: React.ReactNode; // Button components
}
const BenefitsTableExample = () => {
const benefitsColumns = createColumns<BenefitDataTable>([
{
key: 'name',
label: 'Nombre del Beneficio',
sortable: true,
width: '25%',
},
{
key: 'type',
label: 'Tipo',
sortable: true,
align: 'center',
width: 150,
},
{
key: 'detail',
label: 'Detalle',
sortable: true,
},
{
key: 'actions',
label: 'Acciones',
align: 'center',
width: 200,
},
]);
// Preparar datos con componentes React
const benefitsData: BenefitDataTable[] = benefits.map((benefit) => ({
name: benefit.name,
// Componente Badge personalizado
type: (
<Badge
className={cn(
'text-sm',
benefit.type === 'GiftCard'
? 'border-[#E0F3D9] bg-[#F1F9EE] text-[#68B14B]'
: benefit.type === 'Discount'
? 'border-[#CFF4E4] bg-[#E1FDF1] text-[#34D399]'
: 'border-[#DBEAFE] bg-[#EFF6FF] text-[#3B82F6]'
)}
>
{benefit.type === 'GiftCard'
? '🎁 Tarjeta Regalo'
: benefit.type === 'Discount'
? '💰 Descuento'
: '💵 Monto Monetario'}
</Badge>
),
// Detalle condicional basado en tipo
detail:
(benefit.type === 'Discount' &&
benefit.percentage != null &&
`${benefit.percentage}%`) ||
(benefit.type === 'MonetaryAmount' &&
benefit.amount != null &&
`$${benefit.amount} USD`) ||
(benefit.type === 'GiftCard' &&
benefit.amount != null &&
`$${benefit.amount} USD`) ||
'',
// Botones de acción
actions: (
<div className='flex space-x-2'>
<GatherButton
variant='gather-outline'
size='sm'
onClick={() => handleOpenEdit(benefit)}
>
✏️ Editar
</GatherButton>
<GatherButton
variant='gather-outline'
size='sm'
className='text-red-500 hover:bg-red-50'
onClick={() => handleOpenDelete(benefit)}
>
🗑️ Eliminar
</GatherButton>
</div>
),
}));
// Hook de ordenamiento
const { sortedData, sortConfig, handleSort } = useTableSort(benefitsData);
return (
<GatherTable maxHeight='500px'>
<GatherTableCaption>
Lista de beneficios disponibles para el programa de referidos
</GatherTableCaption>
<GatherTableHeader
columns={benefitsColumns}
sortConfig={sortConfig}
onSort={handleSort}
sticky
/>
<GatherTableBody
columns={benefitsColumns}
data={sortedData}
emptyMessage='No hay beneficios configurados'
onRowClick={(row, index) => {
console.log('Fila seleccionada:', row, 'Índice:', index);
}}
/>
</GatherTable>
);
};🔧 Uso Manual con Componentes Individuales
Construcción Manual de Tabla
import {
GatherTable,
GatherTableHeader,
GatherTableBody,
GatherTableRow,
} from '@/components/table';
import { TableCell } from '@/components/ui/table';
const ManualTableExample = () => {
const users = [
{ id: 1, name: 'Ana', email: '[email protected]', status: 'active' },
{ id: 2, name: 'Luis', email: '[email protected]', status: 'inactive' },
{ id: 3, name: 'Carmen', email: '[email protected]', status: 'pending' },
];
return (
<GatherTable maxHeight='400px'>
{/* Header manual */}
<GatherTableHeader>
<GatherTableRow>
<TableCell className='bg-gray-100 font-bold'>ID</TableCell>
<TableCell className='bg-gray-100 font-bold'>Nombre</TableCell>
<TableCell className='bg-gray-100 font-bold'>Email</TableCell>
<TableCell className='bg-gray-100 font-bold'>Estado</TableCell>
</GatherTableRow>
</GatherTableHeader>
{/* Body manual */}
<GatherTableBody>
{users.map((user, index) => (
<GatherTableRow
key={user.id}
rowIndex={index}
isLastRow={index === users.length - 1}
clickable
onClick={() => console.log('Usuario:', user)}
>
<TableCell>{user.id}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge
variant={
user.status === 'active'
? 'success'
: user.status === 'inactive'
? 'destructive'
: 'warning'
}
>
{user.status === 'active'
? '✅ Activo'
: user.status === 'inactive'
? '❌ Inactivo'
: '⏳ Pendiente'}
</Badge>
</TableCell>
</GatherTableRow>
))}
</GatherTableBody>
</GatherTable>
);
};🎣 Hook useTableSort - renderCell como Prop
Implementación Completa con Ordenamiento
const SortableTableExample = () => {
const [loading, setLoading] = useState(false);
interface Product {
id: number;
name: string;
price: number;
stock: number;
category: string;
createdAt: Date;
}
const products: Product[] = [
{
id: 1,
name: 'Laptop',
price: 1299.99,
stock: 15,
category: 'Electrónica',
createdAt: new Date('2024-01-15'),
},
{
id: 2,
name: 'Mouse',
price: 29.99,
stock: 150,
category: 'Accesorios',
createdAt: new Date('2024-02-20'),
},
{
id: 3,
name: 'Teclado',
price: 89.99,
stock: 45,
category: 'Accesorios',
createdAt: new Date('2024-01-10'),
},
];
// Hook de ordenamiento con configuración inicial
const {
sortedData,
sortConfig,
handleSort,
clearSort,
isSorted,
getSortDirection,
} = useTableSort(products, {
key: 'name',
direction: 'asc',
});
const productColumns = createColumns<Product>([
{
key: 'id',
label: 'ID',
width: 60,
sortable: true,
},
{
key: 'name',
label: 'Producto',
sortable: true,
},
{
key: 'price',
label: 'Precio',
align: 'right',
sortable: true,
width: 120,
},
{
key: 'stock',
label: 'Stock',
align: 'center',
sortable: true,
width: 100,
},
{
key: 'category',
label: 'Categoría',
sortable: true,
},
{
key: 'createdAt',
label: 'Fecha',
sortable: true,
width: 150,
},
]);
// RenderCell personalizado para formateo
const renderCell = (
item: Product,
column: GatherTableColumn<Product>
): React.ReactNode => {
switch (column.key) {
case 'price':
return (
<span className='font-mono text-green-600'>
${item.price.toFixed(2)}
</span>
);
case 'stock':
return (
<Badge
variant={
item.stock > 50
? 'success'
: item.stock > 10
? 'warning'
: 'destructive'
}
>
{item.stock} unidades
</Badge>
);
case 'createdAt':
return new Intl.DateTimeFormat('es-MX', {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(item.createdAt);
default:
return item[column.key as keyof Product];
}
};
return (
<div className='space-y-4'>
{/* Controles de ordenamiento */}
<div className='flex gap-2'>
<GatherButton variant='gather-outline' size='sm' onClick={clearSort}>
🔄 Limpiar Orden
</GatherButton>
{sortConfig && (
<Badge variant='secondary'>
Ordenado por: {sortConfig.key} ({sortConfig.direction})
</Badge>
)}
</div>
{/* Tabla */}
<GatherTable maxHeight='600px'>
<GatherTableHeader
columns={productColumns}
sortConfig={sortConfig}
onSort={handleSort}
sticky
/>
<GatherTableBody
columns={productColumns}
data={sortedData}
renderCell={renderCell}
loading={loading}
loadingRows={3}
emptyMessage={
<div className='py-8 text-center'>
<p className='text-gray-500'>No hay productos disponibles</p>
<GatherButton variant='gather-primary' size='sm' className='mt-4'>
Agregar Producto
</GatherButton>
</div>
}
onRowClick={(product, index) => {
console.log(`Producto seleccionado:`, product);
console.log(`Índice:`, index);
}}
rowClassName={(product, index) => {
// Resaltar filas con stock bajo
if (product.stock < 10) {
return 'bg-red-50 hover:bg-red-100';
}
// Filas pares/impares
return index % 2 === 0 ? 'bg-gray-50' : '';
}}
/>
</GatherTable>
</div>
);
};🛠️ Funciones Auxiliares
createColumns - Con Type Safety
// CON TYPE SAFETY COMPLETO
interface Employee {
id: number;
firstName: string;
lastName: string;
department: string;
salary: number;
hireDate: Date;
}
const employeeColumns = createColumns<Employee>([
{ key: 'id', label: 'ID', width: 60 },
{ key: 'firstName', label: 'Nombre', sortable: true },
{ key: 'lastName', label: 'Apellido', sortable: true },
{ key: 'department', label: 'Departamento' },
{ key: 'salary', label: 'Salario', align: 'right', sortable: true },
{ key: 'hireDate', label: 'Fecha Contratación', sortable: true },
// { key: 'invalid', label: 'Error' } // ❌ TypeScript error!
]);createFlexibleColumns - Para Datos Dinámicos
// PARA DATOS DINÁMICOS O APIs EXTERNAS
const apiColumns = createFlexibleColumns([
{ label: 'Usuario', key: 'user.profile.fullName' },
{ label: 'Avatar', key: 'user.profile.avatar_url' },
{ label: 'Configuración', key: 'settings.preferences.theme' },
{ label: 'Último Acceso', key: 'meta.lastLoginAt' },
{ label: 'Campo Calculado', key: 'computed_field' },
]);
// Para usar con datos de API
const apiData = await fetch('/api/users').then((r) => r.json());
<GatherTableBody
columns={apiColumns}
data={apiData}
renderCell={(item, column) => {
// Acceso a propiedades anidadas
const keys = column.key.split('.');
let value = item;
for (const key of keys) {
value = value?.[key];
}
return value || '-';
}}
/>;📊 Estados de la Tabla
Estado de Carga (Loading)
<GatherTableBody
columns={columns}
data={[]}
loading={true}
loadingRows={5} // Muestra 5 filas skeleton
columnsNumber={4} // Si no hay columns definidas
/>Estado Vacío Personalizado
<GatherTableBody
columns={columns}
data={[]}
emptyMessage={
<div className='flex flex-col items-center py-12'>
<EmptyStateIcon className='mb-4 h-16 w-16 text-gray-400' />
<h3 className='mb-2 text-lg font-semibold'>Sin resultados</h3>
<p className='mb-4 text-gray-500'>No se encontraron registros</p>
<GatherButton variant='gather-primary'>Crear Nuevo Registro</GatherButton>
</div>
}
/>Esta guía completa cubre todos los aspectos de GatherTable, desde uso básico hasta implementaciones avanzadas con ordenamiento, filtrado y renderizado personalizado.
🪟 Guía Completa de GatherModal
🎯 Introducción
GatherModal es un componente modal genérico y altamente personalizable que proporciona diálogos flexibles con soporte completo para diferentes posiciones, tamaños, animaciones y accesibilidad avanzada.
🏗️ Características Principales
- Posiciones: center, left, right (ideal para paneles laterales)
- Tamaños: sm, md, lg, xl, full
- Header personalizable: Título simple o contenido completamente custom
- Footer flexible: Botones por defecto o con
