npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

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 MINOR
  • fix: Corrección de errores → incrementa PATCH
  • chore: Tareas internas, sin impacto en la API
  • docs: Cambios en documentación
  • refactor: Refactorización de código
  • perf: Mejoras de rendimiento
  • test: Añadir o corregir tests
  • BREAKING 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 commit

Scripts 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 envuelven

3. 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:

  1. Scroll en selector de años - El selector ahora tiene altura máxima y scroll automático
  2. Control de años personalizables - pastYearsCount={10} muestra año actual + últimos 10 años
  3. Textos personalizables - Botones Apply/Cancel en cualquier idioma
  4. Callbacks para eventos - onApply, onCancel, onOpenChange para lógica personalizada
  5. Formatos de fecha flexibles - Personaliza cómo se muestran las fechas
  6. Auto-cierre inteligente - Cierre automático tras selección para UX más rápida
  7. 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