carconnect-gatherleads-ui-lib 2.1.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,3 +64,2403 @@ npm install carconnect-gatherleads-ui-lib
64
64
  # o con yarn
65
65
  yarn add carconnect-gatherleads-ui-lib
66
66
  ```
67
+
68
+ # 📋 Guía Completa: GatherFormBuilder
69
+
70
+ ## 🚀 Introducción
71
+
72
+ `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.
73
+
74
+ ## 📦 Uso Básico
75
+
76
+ ```tsx
77
+ import GatherFormBuilder from '@/components/GatherFormBuilder';
78
+
79
+ function MiFormulario() {
80
+ const handleSubmit = async (data) => {
81
+ console.log('Datos enviados:', data);
82
+ };
83
+
84
+ return (
85
+ <GatherFormBuilder
86
+ config={{
87
+ title: "Registro de Usuario",
88
+ orderFields:["email", "nombre"]; // Array con el orden específico de los campos (usando los nombres de los campos)
89
+ description: "Complete sus datos personales",
90
+ fields: [
91
+ {
92
+ name: "nombre",
93
+ label: "Nombre completo",
94
+ type: "text",
95
+ required: true
96
+ },
97
+ {
98
+ name: "email",
99
+ label: "Correo electrónico",
100
+ type: "email",
101
+ required: true
102
+ }
103
+ ],
104
+ submitButton: {
105
+ text: "Registrarse",
106
+ variant: "gather-primary"
107
+ }
108
+ }}
109
+ onSubmit={handleSubmit}
110
+ />
111
+ );
112
+ }
113
+ ```
114
+
115
+ # 🎨 Propiedades de Layout y sus Combinaciones en GatherFormBuilder
116
+
117
+ ## 📊 Matriz de Compatibilidad: Layout + Propiedades
118
+
119
+ ### 1. **Layout VERTICAL**
120
+
121
+ El layout por defecto, apila campos uno debajo del otro.
122
+
123
+ **Propiedades que aplican:**
124
+
125
+ - ✅ `verticalSpacing` - Espacio entre campos
126
+ - ✅ `gap` - Espacio general (como fallback)
127
+ - ✅ `className` - Clases CSS adicionales
128
+ - ❌ Todas las demás propiedades de layout se ignoran
129
+
130
+ ```tsx
131
+ const verticalExample = {
132
+ layout: 'vertical', // o sin especificar (default)
133
+
134
+ // PROPIEDADES QUE FUNCIONAN:
135
+ verticalSpacing: '4', // space-y-4 = 16px entre campos
136
+ gap: 'gap-6', // se usa si no hay verticalSpacing
137
+ className: 'custom-class',
138
+
139
+ // PROPIEDADES IGNORADAS (no tienen efecto):
140
+ gridCols: 3, // ❌ ignorado
141
+ alignment: 'center', // ❌ ignorado
142
+ noWrap: true, // ❌ ignorado
143
+
144
+ fields: [
145
+ { name: 'field1', label: 'Campo 1', type: 'text' },
146
+ { name: 'field2', label: 'Campo 2', type: 'text' },
147
+ ],
148
+ };
149
+
150
+ // Clases CSS resultantes: "space-y-4"
151
+ ```
152
+
153
+ ### 2. **Layout HORIZONTAL**
154
+
155
+ Dispone los campos en línea horizontal con flexbox.
156
+
157
+ **Propiedades que aplican:**
158
+
159
+ - ✅ `gap` - Espacio entre elementos
160
+ - ✅ `alignment` - Alineación horizontal (start, center, end, between, around, evenly)
161
+ - ✅ `itemsAlignment` - Alineación vertical (start, center, end, stretch)
162
+ - ✅ `noWrap` - Previene el wrap de elementos
163
+ - ✅ `className` - Clases CSS adicionales
164
+ - ❌ Propiedades de grid se ignoran
165
+
166
+ ```tsx
167
+ const horizontalExample = {
168
+ layout: 'horizontal',
169
+
170
+ // PROPIEDADES QUE FUNCIONAN:
171
+ gap: 'gap-4', // 16px entre campos
172
+ alignment: 'between', // justify-between: espacio entre elementos
173
+ itemsAlignment: 'center', // items-center: centrado vertical
174
+ noWrap: false, // permite wrap (por defecto)
175
+
176
+ // PROPIEDADES IGNORADAS:
177
+ gridCols: 2, // ❌ ignorado
178
+ verticalSpacing: '4', // ❌ ignorado
179
+ showDividers: true, // ❌ ignorado
180
+
181
+ fields: [
182
+ {
183
+ name: 'search',
184
+ label: 'Buscar',
185
+ type: 'text',
186
+ width: 'w-2/3', // ocupa 2/3 del ancho
187
+ },
188
+ {
189
+ name: 'filter',
190
+ label: 'Filtro',
191
+ type: 'select',
192
+ width: 'w-1/3', // ocupa 1/3 del ancho
193
+ },
194
+ ],
195
+ };
196
+
197
+ // Clases CSS resultantes: "flex flex-wrap gap-4 justify-between items-center"
198
+ ```
199
+
200
+ **Ejemplo con `noWrap`:**
201
+
202
+ ```tsx
203
+ const horizontalNoWrap = {
204
+ layout: 'horizontal',
205
+ noWrap: true, // NO permite que los elementos pasen a nueva línea
206
+ alignment: 'start', // justify-start: alineados a la izquierda
207
+ itemsAlignment: 'stretch', // items-stretch: altura completa
208
+ gap: 'gap-2',
209
+
210
+ fields: [
211
+ { name: 'field1', label: 'Campo 1', type: 'text' },
212
+ { name: 'field2', label: 'Campo 2', type: 'text' },
213
+ { name: 'field3', label: 'Campo 3', type: 'text' },
214
+ ],
215
+ };
216
+
217
+ // Clases CSS resultantes: "flex gap-2 justify-start items-stretch"
218
+ // Nota: sin "flex-wrap", los elementos NO se envuelven
219
+ ```
220
+
221
+ ### 3. **Layout GRID**
222
+
223
+ Organiza campos en una cuadrícula con sistema de columnas.
224
+
225
+ **Propiedades que aplican:**
226
+
227
+ - ✅ `gridCols` - Número de columnas base (1-12)
228
+ - ✅ `smCols` - Columnas en pantallas pequeñas (640px+)
229
+ - ✅ `lgCols` - Columnas en pantallas grandes (1024px+)
230
+ - ✅ `xlCols` - Columnas en pantallas extra grandes (1280px+)
231
+ - ✅ `autoFit` - Auto-ajuste de columnas
232
+ - ✅ `gap` - Espacio entre celdas
233
+ - ✅ `gridAlignment` - Alineación del contenido de la grilla
234
+ - ✅ `gridItemsAlignment` - Alineación de items en sus celdas
235
+ - ✅ `className` - Clases CSS adicionales
236
+ - ❌ Propiedades de flex se ignoran
237
+
238
+ ```tsx
239
+ const gridResponsive = {
240
+ layout: 'grid',
241
+
242
+ // PROPIEDADES QUE FUNCIONAN:
243
+ gridCols: 2, // 2 columnas en tablets (768px+)
244
+ smCols: 1, // 1 columna en móviles pequeños (640px+)
245
+ lgCols: 3, // 3 columnas en desktop (1024px+)
246
+ xlCols: 4, // 4 columnas en pantallas grandes (1280px+)
247
+ gap: 'gap-6', // 24px entre celdas
248
+ gridAlignment: 'center', // justify-items-center: contenido centrado
249
+ gridItemsAlignment: 'start', // items-start: alineados arriba
250
+
251
+ // PROPIEDADES IGNORADAS:
252
+ alignment: 'between', // ❌ ignorado (es para flex)
253
+ noWrap: true, // ❌ ignorado (es para flex)
254
+ verticalSpacing: '4', // ❌ ignorado
255
+
256
+ fields: [
257
+ {
258
+ name: 'fullWidth',
259
+ label: 'Campo ancho completo',
260
+ type: 'text',
261
+ colSpan: 2, // ocupa 2 columnas
262
+ },
263
+ { name: 'half1', label: 'Mitad 1', type: 'text' },
264
+ { name: 'half2', label: 'Mitad 2', type: 'text' },
265
+ {
266
+ name: 'triple',
267
+ label: 'Triple ancho',
268
+ type: 'textarea',
269
+ colSpan: 3, // ocupa 3 columnas
270
+ },
271
+ ],
272
+ };
273
+
274
+ // Clases CSS resultantes:
275
+ // "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"
276
+ ```
277
+
278
+ **Ejemplo con `autoFit`:**
279
+
280
+ ```tsx
281
+ const gridAutoFit = {
282
+ layout: 'grid',
283
+ autoFit: true, // ACTIVA el auto-ajuste (min 250px por columna)
284
+ gap: 'gap-4',
285
+
286
+ // CUANDO autoFit está activo, estas se IGNORAN:
287
+ gridCols: 3, // ❌ ignorado con autoFit
288
+ smCols: 2, // ❌ ignorado con autoFit
289
+ lgCols: 4, // ❌ ignorado con autoFit
290
+
291
+ fields: [
292
+ { name: 'field1', label: 'Campo 1', type: 'text' },
293
+ { name: 'field2', label: 'Campo 2', type: 'text' },
294
+ { name: 'field3', label: 'Campo 3', type: 'text' },
295
+ ],
296
+ };
297
+
298
+ // Clases CSS resultantes:
299
+ // "grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4"
300
+ ```
301
+
302
+ ### 4. **Layout MASONRY**
303
+
304
+ Similar a Pinterest, para campos de diferentes alturas.
305
+
306
+ **Propiedades que aplican:**
307
+
308
+ - ✅ `gridCols` - Número de columnas
309
+ - ✅ `gap` - Espacio entre elementos
310
+ - ✅ `className` - Clases CSS adicionales
311
+ - ❌ Otras propiedades de grid/flex se ignoran
312
+
313
+ ```tsx
314
+ const masonryExample = {
315
+ layout: 'masonry',
316
+
317
+ // PROPIEDADES QUE FUNCIONAN:
318
+ gridCols: 3, // 3 columnas en desktop
319
+ gap: 'gap-4', // 16px entre elementos
320
+
321
+ // PROPIEDADES IGNORADAS:
322
+ smCols: 2, // ❌ ignorado (masonry no es responsive)
323
+ lgCols: 4, // ❌ ignorado
324
+ autoFit: true, // ❌ ignorado
325
+ alignment: 'center', // ❌ ignorado
326
+
327
+ fields: [
328
+ {
329
+ name: 'short',
330
+ label: 'Texto corto',
331
+ type: 'text',
332
+ },
333
+ {
334
+ name: 'tall',
335
+ label: 'Campo alto',
336
+ type: 'textarea',
337
+ rows: 8, // más alto que otros
338
+ },
339
+ {
340
+ name: 'medium',
341
+ label: 'Altura media',
342
+ type: 'multiselect',
343
+ options: [
344
+ /*...*/
345
+ ],
346
+ },
347
+ ],
348
+ };
349
+
350
+ // Clases CSS resultantes:
351
+ // "grid grid-cols-1 md:grid-cols-3 gap-4 auto-rows-max"
352
+ ```
353
+
354
+ ### 5. **Layout INLINE**
355
+
356
+ Campos en línea con wrap automático, centrados verticalmente.
357
+
358
+ **Propiedades que aplican:**
359
+
360
+ - ✅ `gap` - Espacio entre elementos
361
+ - ✅ `className` - Clases CSS adicionales
362
+ - ❌ Todas las demás propiedades se ignoran
363
+
364
+ ```tsx
365
+ const inlineExample = {
366
+ layout: 'inline',
367
+
368
+ // PROPIEDADES QUE FUNCIONAN:
369
+ gap: 'gap-3', // 12px entre elementos
370
+
371
+ // PROPIEDADES IGNORADAS (inline es muy simple):
372
+ alignment: 'center', // ❌ ignorado (siempre flex-wrap)
373
+ gridCols: 2, // ❌ ignorado
374
+ verticalSpacing: '4', // ❌ ignorado
375
+ noWrap: true, // ❌ ignorado (siempre hace wrap)
376
+
377
+ fields: [
378
+ {
379
+ name: 'tag1',
380
+ label: 'Tag',
381
+ type: 'text',
382
+ width: 'w-auto', // ancho automático
383
+ },
384
+ {
385
+ name: 'tag2',
386
+ label: 'Otro Tag',
387
+ type: 'text',
388
+ width: 'w-auto',
389
+ },
390
+ ],
391
+ };
392
+
393
+ // Clases CSS resultantes:
394
+ // "flex flex-wrap gap-3 items-center"
395
+ ```
396
+
397
+ ### 6. **Layout STACK**
398
+
399
+ Vertical con opciones especiales de espaciado y divisores.
400
+
401
+ **Propiedades que aplican:**
402
+
403
+ - ✅ `stackSpacing` - Espaciado específico del stack
404
+ - ✅ `showDividers` - Muestra líneas divisorias
405
+ - ✅ `className` - Clases CSS adicionales
406
+ - ❌ Propiedades de grid/flex se ignoran
407
+
408
+ ```tsx
409
+ const stackExample = {
410
+ layout: 'stack',
411
+
412
+ // PROPIEDADES QUE FUNCIONAN:
413
+ stackSpacing: '6', // space-y-6 = 24px entre elementos
414
+ showDividers: true, // añade líneas divisorias
415
+
416
+ // PROPIEDADES IGNORADAS:
417
+ gap: 'gap-4', // ❌ ignorado (usa stackSpacing)
418
+ verticalSpacing: '2', // ❌ ignorado (usa stackSpacing)
419
+ gridCols: 2, // ❌ ignorado
420
+ alignment: 'center', // ❌ ignorado
421
+
422
+ fields: [
423
+ {
424
+ name: 'section1',
425
+ label: 'Sección 1',
426
+ type: 'text',
427
+ },
428
+ {
429
+ name: 'section2',
430
+ label: 'Sección 2',
431
+ type: 'textarea',
432
+ },
433
+ {
434
+ name: 'section3',
435
+ label: 'Sección 3',
436
+ type: 'select',
437
+ options: [
438
+ /*...*/
439
+ ],
440
+ },
441
+ ],
442
+ };
443
+
444
+ // Clases CSS resultantes:
445
+ // "space-y-6 divide-y divide-gray-200"
446
+ ```
447
+
448
+ ## 📋 Tabla Resumen de Compatibilidad
449
+
450
+ | Propiedad | vertical | horizontal | grid | masonry | inline | stack |
451
+ | -------------------- | -------- | ---------- | ---- | ------- | ------ | ----- |
452
+ | `verticalSpacing` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
453
+ | `gap` | ✅\* | ✅ | ✅ | ✅ | ✅ | ❌ |
454
+ | `alignment` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
455
+ | `itemsAlignment` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
456
+ | `noWrap` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
457
+ | `gridCols` | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
458
+ | `smCols` | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
459
+ | `lgCols` | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
460
+ | `xlCols` | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
461
+ | `autoFit` | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
462
+ | `gridAlignment` | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
463
+ | `gridItemsAlignment` | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
464
+ | `stackSpacing` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
465
+ | `showDividers` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
466
+ | `className` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
467
+
468
+ \*`gap` en vertical solo se usa si no hay `verticalSpacing`
469
+
470
+ ## 🎯 Ejemplo Completo Combinando Propiedades
471
+
472
+ ```tsx
473
+ const complexFormConfig = {
474
+ title: 'Formulario Complejo',
475
+ layout: 'grid',
476
+
477
+ // TODAS estas propiedades SÍ funcionan con layout="grid":
478
+ gridCols: 2, // 2 columnas base
479
+ smCols: 1, // 1 columna en móvil
480
+ lgCols: 3, // 3 columnas en desktop
481
+ xlCols: 4, // 4 columnas en pantallas grandes
482
+ gap: 'gap-6', // 24px entre celdas
483
+ gridAlignment: 'stretch', // contenido estirado
484
+ gridItemsAlignment: 'center', // items centrados verticalmente
485
+ className: 'bg-gray-50', // fondo gris
486
+
487
+ // Estas propiedades se IGNORAN porque no son para grid:
488
+ noWrap: true, // ❌ ignorado (es para horizontal)
489
+ stackSpacing: '4', // ❌ ignorado (es para stack)
490
+ showDividers: true, // ❌ ignorado (es para stack)
491
+ alignment: 'between', // ❌ ignorado (es para horizontal)
492
+
493
+ fields: [
494
+ {
495
+ name: 'fullRow',
496
+ label: 'Campo de ancho completo',
497
+ type: 'text',
498
+ colSpan: 4, // ocupa todas las columnas en xl
499
+ },
500
+ // más campos...
501
+ ],
502
+ };
503
+ ```
504
+
505
+ # 🎯 Guía Completa de Tipos de Campos en GatherFormBuilder
506
+
507
+ ## 📝 Tipos de Campos y sus Propiedades
508
+
509
+ ### 1. **GatherInputFieldConfig** - Campos de Entrada de Texto
510
+
511
+ Campos básicos de entrada para diferentes tipos de datos textuales y numéricos.
512
+
513
+ ```tsx
514
+ interface GatherInputFieldConfig extends BaseFieldConfig {
515
+ type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
516
+ leftIcon?: React.ReactNode;
517
+ rightIcon?: React.ReactNode;
518
+ minLength?: number;
519
+ maxLength?: number;
520
+ pattern?: string;
521
+ patternMessage?: string;
522
+ min?: number; // Solo para type="number"
523
+ max?: number; // Solo para type="number"
524
+ size?: 'sm' | 'md' | 'lg';
525
+ variant?: 'default' | 'success' | 'warning' | 'error';
526
+ }
527
+ ```
528
+
529
+ **Ejemplos por tipo:**
530
+
531
+ ```tsx
532
+ // CAMPO DE TEXTO SIMPLE
533
+ {
534
+ name: "username",
535
+ label: "Nombre de usuario",
536
+ type: "text",
537
+ placeholder: "Ingrese su usuario",
538
+ required: true,
539
+ minLength: 3, // Mínimo 3 caracteres
540
+ maxLength: 20, // Máximo 20 caracteres
541
+ leftIcon: <UserIcon />, // Icono a la izquierda
542
+ size: "md", // Tamaño mediano
543
+ variant: "default", // Estilo por defecto
544
+ helperText: "Entre 3 y 20 caracteres",
545
+ width: "w-full" // Ancho completo
546
+ }
547
+
548
+ // CAMPO EMAIL
549
+ {
550
+ name: "email",
551
+ label: "Correo electrónico",
552
+ type: "email",
553
+ required: true,
554
+ pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
555
+ patternMessage: "Ingrese un email válido",
556
+ leftIcon: <MailIcon />,
557
+ variant: "default"
558
+ }
559
+
560
+ // CAMPO PASSWORD
561
+ {
562
+ name: "password",
563
+ label: "Contraseña",
564
+ type: "password",
565
+ required: true,
566
+ minLength: 8,
567
+ maxLength: 50,
568
+ pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
569
+ patternMessage: "Debe contener mayúsculas, minúsculas y números",
570
+ rightIcon: <EyeIcon />, // Para mostrar/ocultar
571
+ size: "lg" // Tamaño grande
572
+ }
573
+
574
+ // CAMPO NUMÉRICO
575
+ {
576
+ name: "age",
577
+ label: "Edad",
578
+ type: "number",
579
+ min: 18, // Valor mínimo
580
+ max: 120, // Valor máximo
581
+ required: true,
582
+ placeholder: "18",
583
+ variant: "default"
584
+ }
585
+
586
+ // CAMPO TELÉFONO
587
+ {
588
+ name: "phone",
589
+ label: "Teléfono",
590
+ type: "tel",
591
+ pattern: "^\\+?[1-9]\\d{1,14}$", // Formato internacional
592
+ patternMessage: "Formato: +1234567890",
593
+ leftIcon: <PhoneIcon />,
594
+ placeholder: "+52 555 123 4567"
595
+ }
596
+
597
+ // CAMPO URL
598
+ {
599
+ name: "website",
600
+ label: "Sitio web",
601
+ type: "url",
602
+ placeholder: "https://ejemplo.com",
603
+ pattern: "^https?://.*",
604
+ patternMessage: "Debe comenzar con http:// o https://",
605
+ leftIcon: <GlobeIcon />
606
+ }
607
+ ```
608
+
609
+ ### 2. **GatherInputSelectFieldConfig** - Campo Combinado (Select + Input)
610
+
611
+ Combina un selector con un campo de entrada en un solo componente.
612
+
613
+ ```tsx
614
+ interface GatherInputSelectFieldConfig extends BaseFieldConfig {
615
+ type: 'inputSelect' | 'identification';
616
+ selectFieldName: string;
617
+ inputFieldName: string;
618
+ options: SelectInputOption[];
619
+ selectValue?: string;
620
+ inputValue?: string;
621
+ inputPlaceholder?: string;
622
+ selectPlaceholder?: string;
623
+ selectWidth?: number; // Porcentaje (0-100)
624
+ size?: 'sm' | 'md' | 'lg';
625
+ variant?: 'default' | 'success' | 'warning' | 'error';
626
+ }
627
+ ```
628
+
629
+ **Ejemplos:**
630
+
631
+ ```tsx
632
+ // CAMPO DE IDENTIFICACIÓN
633
+ {
634
+ name: "identification",
635
+ label: "Documento de identidad",
636
+ type: "inputSelect",
637
+ selectFieldName: "docType", // Campo para tipo de documento
638
+ inputFieldName: "docNumber", // Campo para número
639
+ selectWidth: 30, // Select ocupa 30% del ancho
640
+ options: [
641
+ { value: "dni", label: "DNI" , validateFn:(val)=>validateDni(val), errorMessage:"Campo invalido"},
642
+ { value: "passport", label: "Pasaporte" validatePattern: "^[A-Z0-9]{6,9}$", errorMessage: "El número de pasaporte no es válido"},
643
+ { value: "ruc", label: "RUC", maxLength:13 },
644
+ { value: "cedula", label: "Cédula" }
645
+ ],
646
+ selectPlaceholder: "Tipo",
647
+ inputPlaceholder: "Número de documento",
648
+ required: true,
649
+ size: "md"
650
+ }
651
+
652
+ // CAMPO DE MONEDA Y MONTO
653
+ {
654
+ name: "amount",
655
+ label: "Monto a pagar",
656
+ type: "inputSelect",
657
+ selectFieldName: "currency",
658
+ inputFieldName: "value",
659
+ selectWidth: 25, // Select ocupa 25%
660
+ options: [
661
+ { value: "usd", label: "USD" },
662
+ { value: "eur", label: "EUR" },
663
+ { value: "mxn", label: "MXN" }
664
+ ],
665
+ selectValue: "usd", // Valor por defecto
666
+ inputPlaceholder: "0.00",
667
+ variant: "success"
668
+ }
669
+
670
+ // CAMPO DE CÓDIGO DE ÁREA Y TELÉFONO
671
+ {
672
+ name: "phoneWithCode",
673
+ label: "Teléfono con código",
674
+ type: "inputSelect",
675
+ selectFieldName: "countryCode",
676
+ inputFieldName: "phoneNumber",
677
+ selectWidth: 20,
678
+ options: [
679
+ { value: "+1", label: "+1 USA" },
680
+ { value: "+52", label: "+52 MEX" },
681
+ { value: "+34", label: "+34 ESP" }
682
+ ],
683
+ inputPlaceholder: "555 123 4567"
684
+ }
685
+ ```
686
+
687
+ ### 3. **GatherSelectFieldConfig** - Campo de Selección Simple
688
+
689
+ ```tsx
690
+ interface GatherSelectFieldConfig extends BaseFieldConfig {
691
+ type: 'select';
692
+ value?: OptionType[];
693
+ options: Array<{
694
+ value: string | number;
695
+ label: string;
696
+ disabled?: boolean;
697
+ }>;
698
+ leftIcon?: React.ReactNode;
699
+ size?: 'sm' | 'md' | 'lg';
700
+ variant?: 'default' | 'success' | 'warning' | 'error';
701
+ }
702
+ ```
703
+
704
+ **Ejemplos:**
705
+
706
+ ```tsx
707
+ // SELECT SIMPLE
708
+ {
709
+ name: "country",
710
+ label: "País",
711
+ type: "select",
712
+ placeholder: "Seleccione un país",
713
+ required: true,
714
+ options: [
715
+ { value: "mx", label: "México" },
716
+ { value: "us", label: "Estados Unidos" },
717
+ { value: "ca", label: "Canadá", disabled: true }, // Opción deshabilitada
718
+ { value: "ar", label: "Argentina" }
719
+ ],
720
+ leftIcon: <MapIcon />,
721
+ size: "md",
722
+ width: "w-full"
723
+ }
724
+
725
+ // SELECT DE CATEGORÍAS
726
+ {
727
+ name: "category",
728
+ label: "Categoría",
729
+ type: "select",
730
+ options: [
731
+ { value: 1, label: "Tecnología" }, // value numérico
732
+ { value: 2, label: "Salud" },
733
+ { value: 3, label: "Educación" },
734
+ { value: 4, label: "Finanzas" }
735
+ ],
736
+ variant: "default",
737
+ helperText: "Seleccione la categoría principal"
738
+ }
739
+
740
+ // SELECT DE ESTADO/STATUS
741
+ {
742
+ name: "status",
743
+ label: "Estado",
744
+ type: "select",
745
+ options: [
746
+ { value: "active", label: "✅ Activo" },
747
+ { value: "pending", label: "⏳ Pendiente" },
748
+ { value: "inactive", label: "❌ Inactivo" }
749
+ ],
750
+ variant: "warning",
751
+ size: "sm"
752
+ }
753
+ ```
754
+
755
+ ### 4. **GatherMultiSelectFieldConfig** - Selección Múltiple
756
+
757
+ ```tsx
758
+ interface GatherMultiSelectFieldConfig extends BaseFieldConfig {
759
+ type: 'multiselect';
760
+ options: OptionType[];
761
+ leftIcon?: React.ReactNode;
762
+ isClearable?: boolean; // Botón para limpiar selección
763
+ isSearchable?: boolean; // Permite buscar opciones
764
+ closeMenuOnSelect?: boolean; // Cierra menú al seleccionar
765
+ maxMenuHeight?: number; // Altura máxima en px
766
+ noOptionsMessage?: string; // Mensaje sin opciones
767
+ size?: 'sm' | 'md' | 'lg';
768
+ variant?: 'default' | 'success' | 'warning' | 'error';
769
+ }
770
+ ```
771
+
772
+ **Ejemplos:**
773
+
774
+ ```tsx
775
+ // MULTISELECT DE HABILIDADES
776
+ {
777
+ name: "skills",
778
+ label: "Habilidades técnicas",
779
+ type: "multiselect",
780
+ required: true,
781
+ options: [
782
+ { value: "js", label: "JavaScript" },
783
+ { value: "react", label: "React" },
784
+ { value: "node", label: "Node.js" },
785
+ { value: "python", label: "Python" },
786
+ { value: "docker", label: "Docker" }
787
+ ],
788
+ isSearchable: true, // Permite buscar
789
+ isClearable: true, // Botón limpiar todo
790
+ closeMenuOnSelect: false, // Mantiene menú abierto
791
+ maxMenuHeight: 200, // Altura máxima 200px
792
+ noOptionsMessage: "Sin resultados",
793
+ placeholder: "Seleccione múltiples habilidades",
794
+ helperText: "Puede seleccionar varias opciones"
795
+ }
796
+
797
+ // MULTISELECT DE DÍAS
798
+ {
799
+ name: "workDays",
800
+ label: "Días laborales",
801
+ type: "multiselect",
802
+ options: [
803
+ { value: "mon", label: "Lunes" },
804
+ { value: "tue", label: "Martes" },
805
+ { value: "wed", label: "Miércoles" },
806
+ { value: "thu", label: "Jueves" },
807
+ { value: "fri", label: "Viernes" },
808
+ { value: "sat", label: "Sábado" },
809
+ { value: "sun", label: "Domingo" }
810
+ ],
811
+ closeMenuOnSelect: true, // Cierra al seleccionar
812
+ size: "lg"
813
+ }
814
+ ```
815
+
816
+ ### 5. **GatherTextareaFieldConfig** - Área de Texto
817
+
818
+ ```tsx
819
+ interface GatherTextareaFieldConfig extends BaseFieldConfig {
820
+ type: 'textarea';
821
+ rows?: number; // Filas visibles
822
+ cols?: number; // Columnas
823
+ maxLength?: number; // Máximo de caracteres
824
+ showCharCount?: boolean; // Mostrar contador
825
+ size?: 'sm' | 'md' | 'lg';
826
+ variant?: 'default' | 'success' | 'warning' | 'error';
827
+ resize?: 'none' | 'both' | 'horizontal' | 'vertical';
828
+ }
829
+ ```
830
+
831
+ **Ejemplos:**
832
+
833
+ ```tsx
834
+ // TEXTAREA SIMPLE
835
+ {
836
+ name: "description",
837
+ label: "Descripción",
838
+ type: "textarea",
839
+ placeholder: "Escriba una descripción detallada...",
840
+ rows: 4, // 4 filas visibles
841
+ maxLength: 500, // Máximo 500 caracteres
842
+ showCharCount: true, // Muestra contador: "50/500"
843
+ resize: "vertical", // Solo resize vertical
844
+ required: true,
845
+ helperText: "Mínimo 50 caracteres"
846
+ }
847
+
848
+ // TEXTAREA PARA COMENTARIOS
849
+ {
850
+ name: "comments",
851
+ label: "Comentarios adicionales",
852
+ type: "textarea",
853
+ rows: 6,
854
+ cols: 50,
855
+ resize: "both", // Resize en ambas direcciones
856
+ variant: "default",
857
+ size: "md"
858
+ }
859
+
860
+ // TEXTAREA NO REDIMENSIONABLE
861
+ {
862
+ name: "address",
863
+ label: "Dirección completa",
864
+ type: "textarea",
865
+ rows: 3,
866
+ resize: "none", // No permite redimensionar
867
+ maxLength: 200,
868
+ showCharCount: false,
869
+ width: "w-full"
870
+ }
871
+ ```
872
+
873
+ ### 6. **GatherDateFieldConfig** - Campos de Fecha
874
+
875
+ ```tsx
876
+ interface GatherDateFieldConfig extends BaseFieldConfig {
877
+ type: 'date' | 'daterange';
878
+ maxDate?: Date;
879
+ minDate?: Date;
880
+ singleDate?: boolean; // Solo para daterange
881
+ displayFormat?: string; // Formato de visualización
882
+ size?: 'sm' | 'md' | 'lg';
883
+ }
884
+ ```
885
+
886
+ **Ejemplos:**
887
+
888
+ ```tsx
889
+ // FECHA SIMPLE
890
+ {
891
+ name: "birthDate",
892
+ label: "Fecha de nacimiento",
893
+ type: "date",
894
+ maxDate: new Date(), // No futuro
895
+ minDate: new Date("1900-01-01"), // Desde 1900
896
+ displayFormat: "DD/MM/YYYY", // Formato día/mes/año
897
+ required: true,
898
+ size: "md"
899
+ }
900
+
901
+ // RANGO DE FECHAS
902
+ {
903
+ name: "vacationPeriod",
904
+ label: "Período de vacaciones",
905
+ type: "daterange",
906
+ singleData: false,
907
+ minDate: new Date(), // Desde hoy
908
+ maxDate: new Date("2025-12-31"), // Hasta fin de año
909
+ displayFormat: "DD/MM/YYYY",
910
+ helperText: "Seleccione fecha de inicio y fin"
911
+ }
912
+
913
+ // FECHA CON HORA
914
+ {
915
+ name: "appointmentDateTime",
916
+ label: "Fecha y hora de la cita",
917
+ type: "date",
918
+ minDate: new Date(),
919
+ displayFormat: "DD/MM/YYYY HH:mm", // Con hora
920
+ size: "lg"
921
+ }
922
+
923
+ // FECHA ÚNICA EN RANGO
924
+ {
925
+ name: "singleDateInRange",
926
+ label: "Seleccione una fecha",
927
+ type: "daterange",
928
+ singleDate: true, // Solo una fecha aunque sea daterange
929
+ displayFormat: "MMMM D, YYYY" // "Enero 15, 2025"
930
+ }
931
+ ```
932
+
933
+ ### 7. **GatherCheckboxGroupFieldConfig** - Grupo de Checkboxes
934
+
935
+ ```tsx
936
+ interface GatherCheckboxGroupFieldConfig extends BaseFieldConfig {
937
+ type: 'checkbox';
938
+ options: CheckboxOption[];
939
+ direction?: 'row' | 'column';
940
+ labelPosition?: 'left' | 'right';
941
+ }
942
+ ```
943
+
944
+ **Ejemplos:**
945
+
946
+ ```tsx
947
+ // CHECKBOXES EN COLUMNA
948
+ {
949
+ name: "interests",
950
+ label: "Áreas de interés",
951
+ type: "checkbox",
952
+ options: [
953
+ { value: "tech", label: "Tecnología" },
954
+ { value: "design", label: "Diseño" },
955
+ { value: "marketing", label: "Marketing" },
956
+ { value: "sales", label: "Ventas" }
957
+ ],
958
+ direction: "column", // Disposición vertical
959
+ labelPosition: "right", // Etiqueta a la derecha del checkbox
960
+ helperText: "Seleccione todas las que apliquen"
961
+ }
962
+
963
+ // CHECKBOXES EN FILA
964
+ {
965
+ name: "permissions",
966
+ label: "Permisos",
967
+ type: "checkbox",
968
+ options: [
969
+ { value: "read", label: "Lectura" },
970
+ { value: "write", label: "Escritura" },
971
+ { value: "delete", label: "Eliminar" }
972
+ ],
973
+ direction: "row", // Disposición horizontal
974
+ labelPosition: "right",
975
+ required: true
976
+ }
977
+
978
+ // CHECKBOX DE TÉRMINOS
979
+ {
980
+ name: "agreements",
981
+ label: "Acuerdos legales",
982
+ type: "checkbox",
983
+ options: [
984
+ {
985
+ value: "terms",
986
+ label: "Acepto los términos y condiciones",
987
+ disabled:true // Desabilitar la opcion
988
+ },
989
+ {
990
+ value: "newsletter",
991
+ label: "Deseo recibir newsletter"
992
+ }
993
+ ],
994
+ direction: "column"
995
+ }
996
+ ```
997
+
998
+ ### 8. **GatherRadioButtonFieldConfig** - Radio Buttons
999
+
1000
+ ```tsx
1001
+ interface GatherRadioButtonFieldConfig extends BaseFieldConfig {
1002
+ type: 'radioButton';
1003
+ options: RadioOption[];
1004
+ direction?: 'row' | 'column';
1005
+ labelPosition?: 'left' | 'right';
1006
+ }
1007
+ ```
1008
+
1009
+ **Ejemplos:**
1010
+
1011
+ ```tsx
1012
+ // RADIO BUTTONS BÁSICOS
1013
+ {
1014
+ name: "gender",
1015
+ label: "Género",
1016
+ type: "radioButton",
1017
+ options: [
1018
+ { value: "male", label: "Masculino" },
1019
+ { value: "female", label: "Femenino" },
1020
+ { value: "other", label: "Otro" },
1021
+ { value: "na", label: "Prefiero no decir" }
1022
+ ],
1023
+ direction: "column",
1024
+ labelPosition: "right",
1025
+ required: true
1026
+ }
1027
+
1028
+ // RADIO BUTTONS DE PLAN
1029
+ {
1030
+ name: "subscriptionPlan",
1031
+ label: "Seleccione su plan",
1032
+ type: "radioButton",
1033
+ options: [
1034
+ {
1035
+ value: "basic",
1036
+ label: "Básico - $9/mes",
1037
+ },
1038
+ {
1039
+ value: "pro",
1040
+ label: "Pro - $29/mes",
1041
+ },
1042
+ {
1043
+ value: "enterprise",
1044
+ label: "Empresarial - $99/mes",
1045
+ }
1046
+ ],
1047
+ direction: "column",
1048
+ helperText: "Puede cambiar su plan en cualquier momento"
1049
+ }
1050
+
1051
+ // RADIO BUTTONS HORIZONTALES
1052
+ {
1053
+ name: "priority",
1054
+ label: "Prioridad",
1055
+ type: "radioButton",
1056
+ options: [
1057
+ { value: "low", label: "🟢 Baja" },
1058
+ { value: "medium", label: "🟡 Media" },
1059
+ { value: "high", label: "🔴 Alta" }
1060
+ ],
1061
+ direction: "row", // Disposición horizontal
1062
+ labelPosition: "right"
1063
+ }
1064
+ ```
1065
+
1066
+ ### 9. **GatherSwitchFieldConfig** - Switch/Toggle
1067
+
1068
+ ```tsx
1069
+ interface GatherSwitchFieldConfig extends BaseFieldConfig {
1070
+ type: 'switch';
1071
+ defaultChecked?: boolean;
1072
+ leftLabel?: string;
1073
+ rightLabel?: string;
1074
+ size?: 'sm' | 'md' | 'lg';
1075
+ }
1076
+ ```
1077
+
1078
+ **Ejemplos:**
1079
+
1080
+ ```tsx
1081
+ // SWITCH SIMPLE
1082
+ {
1083
+ name: "notifications",
1084
+ label: "Notificaciones",
1085
+ type: "switch",
1086
+ defaultChecked: true, // Activado por defecto
1087
+ leftLabel: "No", // Etiqueta izquierda
1088
+ rightLabel: "Sí", // Etiqueta derecha
1089
+ size: "md",
1090
+ helperText: "Recibir notificaciones por email"
1091
+ }
1092
+
1093
+ // SWITCH DE MODO
1094
+ {
1095
+ name: "darkMode",
1096
+ label: "Tema de la aplicación",
1097
+ type: "switch",
1098
+ defaultChecked: false,
1099
+ leftLabel: "☀️ Claro",
1100
+ rightLabel: "🌙 Oscuro",
1101
+ size: "lg"
1102
+ }
1103
+
1104
+ // SWITCH DE ESTADO
1105
+ {
1106
+ name: "isActive",
1107
+ label: "Estado de la cuenta",
1108
+ type: "switch",
1109
+ leftLabel: "Inactiva",
1110
+ rightLabel: "Activa",
1111
+ defaultChecked: true,
1112
+ size: "sm"
1113
+ }
1114
+
1115
+ // SWITCH SIN ETIQUETAS LATERALES
1116
+ {
1117
+ name: "rememberMe",
1118
+ label: "Recordar sesión",
1119
+ type: "switch",
1120
+ defaultChecked: false,
1121
+ size: "md",
1122
+ helperText: "Mantener la sesión iniciada"
1123
+ }
1124
+ ```
1125
+
1126
+ ## 📊 Propiedades Comunes (BaseFieldConfig)
1127
+
1128
+ Todas las configuraciones de campo extienden de `BaseFieldConfig`:
1129
+
1130
+ ```tsx
1131
+ interface BaseFieldConfig {
1132
+ // IDENTIFICACIÓN
1133
+ id?: string; // ID único del elemento HTML
1134
+ name: string; // Nombre del campo (requerido)
1135
+ label: string; // Etiqueta visible (requerido)
1136
+
1137
+ // CONTENIDO
1138
+ placeholder?: string; // Texto de ayuda en campo vacío
1139
+ helperText?: string; // Texto de ayuda debajo del campo
1140
+
1141
+ // ESTADO
1142
+ required?: boolean; // Campo obligatorio
1143
+ disabled?: boolean; // Campo deshabilitado
1144
+ error?: boolean; // Indica error de validación
1145
+
1146
+ // DISEÑO
1147
+ width?: 'w-full' | 'w-auto' | 'w-1/2' | 'w-1/3' | 'w-2/3' | 'w-1/4' | 'w-3/4';
1148
+ colSpan?: number; // Columnas que ocupa en grid
1149
+ fullWidth?: boolean; // Fuerza ancho completo
1150
+ className?: string; // Clases CSS adicionales
1151
+ }
1152
+ ```
1153
+
1154
+ ## 🎯 Ejemplo Completo: Formulario con Todos los Tipos
1155
+
1156
+ ```tsx
1157
+ const formularioCompleto = {
1158
+ title: 'Formulario de Registro Completo',
1159
+ layout: 'grid',
1160
+ gridCols: 2,
1161
+ gap: 'gap-4',
1162
+ fields: [
1163
+ // INPUT TEXT
1164
+ {
1165
+ name: 'firstName',
1166
+ label: 'Nombre',
1167
+ type: 'text',
1168
+ required: true,
1169
+ leftIcon: <UserIcon />,
1170
+ },
1171
+
1172
+ // INPUT EMAIL
1173
+ {
1174
+ name: 'email',
1175
+ label: 'Email',
1176
+ type: 'email',
1177
+ required: true,
1178
+ colSpan: 2,
1179
+ },
1180
+
1181
+ // INPUT SELECT (Combinado)
1182
+ {
1183
+ name: 'phone',
1184
+ label: 'Teléfono',
1185
+ type: 'inputSelect',
1186
+ selectFieldName: 'countryCode',
1187
+ inputFieldName: 'number',
1188
+ selectWidth: 25,
1189
+ options: [
1190
+ { value: '+1', label: '+1' },
1191
+ { value: '+52', label: '+52' },
1192
+ ],
1193
+ },
1194
+
1195
+ // SELECT
1196
+ {
1197
+ name: 'country',
1198
+ label: 'País',
1199
+ type: 'select',
1200
+ options: [
1201
+ { value: 'mx', label: 'México' },
1202
+ { value: 'us', label: 'USA' },
1203
+ ],
1204
+ },
1205
+
1206
+ // MULTISELECT
1207
+ {
1208
+ name: 'languages',
1209
+ label: 'Idiomas',
1210
+ type: 'multiselect',
1211
+ options: [
1212
+ { value: 'es', label: 'Español' },
1213
+ { value: 'en', label: 'Inglés' },
1214
+ { value: 'fr', label: 'Francés' },
1215
+ ],
1216
+ isSearchable: true,
1217
+ colSpan: 2,
1218
+ },
1219
+
1220
+ // TEXTAREA
1221
+ {
1222
+ name: 'bio',
1223
+ label: 'Biografía',
1224
+ type: 'textarea',
1225
+ rows: 4,
1226
+ maxLength: 500,
1227
+ showCharCount: true,
1228
+ colSpan: 2,
1229
+ },
1230
+
1231
+ // DATE
1232
+ {
1233
+ name: 'birthDate',
1234
+ label: 'Fecha de nacimiento',
1235
+ type: 'date',
1236
+ maxDate: new Date(),
1237
+ },
1238
+
1239
+ // DATERANGE
1240
+ {
1241
+ name: 'availability',
1242
+ label: 'Disponibilidad',
1243
+ type: 'daterange',
1244
+ singleDate: false,
1245
+ },
1246
+
1247
+ // CHECKBOX GROUP
1248
+ {
1249
+ name: 'interests',
1250
+ label: 'Intereses',
1251
+ type: 'checkbox',
1252
+ options: [
1253
+ { value: 'tech', label: 'Tecnología' },
1254
+ { value: 'art', label: 'Arte' },
1255
+ ],
1256
+ direction: 'column',
1257
+ },
1258
+
1259
+ // RADIO BUTTONS
1260
+ {
1261
+ name: 'plan',
1262
+ label: 'Plan',
1263
+ type: 'radioButton',
1264
+ options: [
1265
+ { value: 'free', label: 'Gratis' },
1266
+ { value: 'pro', label: 'Pro' },
1267
+ ],
1268
+ },
1269
+
1270
+ // SWITCH
1271
+ {
1272
+ name: 'newsletter',
1273
+ label: 'Newsletter',
1274
+ type: 'switch',
1275
+ defaultChecked: false,
1276
+ leftLabel: 'No',
1277
+ rightLabel: 'Sí',
1278
+ colSpan: 2,
1279
+ },
1280
+ ],
1281
+ submitButton: {
1282
+ text: 'Registrarse',
1283
+ variant: 'gather-primary',
1284
+ size: 'lg',
1285
+ },
1286
+ };
1287
+ ```
1288
+
1289
+ # 🔧 Guía de Validaciones y Props del GatherFormBuilder
1290
+
1291
+ ## ⚡ Props Principales del Componente
1292
+
1293
+ ### **GatherFormBuilderProps** - Configuración del Componente
1294
+
1295
+ ```tsx
1296
+ interface GatherFormBuilderProps {
1297
+ config: FormConfig; // Requerido
1298
+ onSubmit: (data: any) => void; // Requerido
1299
+ onAction?: () => void; // Opcional
1300
+ defaultValues?: Record<string, any>; // Opcional
1301
+ resetData?: boolean; // Default: false
1302
+ isLoading?: boolean; // Default: false
1303
+ className?: string; // Opcional
1304
+ validationMode?: 'onBlur' | 'onChange' | 'onSubmit' | 'onTouched' | 'all';
1305
+ loadingComponent?: React.ReactNode; // Opcional
1306
+ }
1307
+ ```
1308
+
1309
+ ### Ejemplos de Props del Componente:
1310
+
1311
+ ```tsx
1312
+ // EJEMPLO BÁSICO
1313
+ <GatherFormBuilder
1314
+ config={formConfig}
1315
+ onSubmit={(data) => console.log(data)}
1316
+ />
1317
+
1318
+ // EJEMPLO CON TODAS LAS PROPS
1319
+ <GatherFormBuilder
1320
+ // CONFIG: Configuración del formulario (requerida)
1321
+ config={{
1322
+ title: "Mi Formulario",
1323
+ fields: [...],
1324
+ layout: "grid"
1325
+ }}
1326
+
1327
+ // ONSUBMIT: Maneja el envío (requerido)
1328
+ onSubmit={async (data) => {
1329
+ try {
1330
+ await api.saveData(data);
1331
+ toast.success("Guardado exitosamente");
1332
+ } catch (error) {
1333
+ toast.error("Error al guardar");
1334
+ }
1335
+ }}
1336
+
1337
+ // ONACTION: Acción secundaria (opcional)
1338
+ onAction={() => {
1339
+ navigate("/cancelar");
1340
+ // o resetear el formulario
1341
+ // o mostrar modal de confirmación
1342
+ }}
1343
+
1344
+ // DEFAULTVALUES: Valores iniciales (opcional)
1345
+ defaultValues={{
1346
+ nombre: "Juan",
1347
+ email: "juan@email.com",
1348
+ pais: "mx",
1349
+ newsletter: true
1350
+ }}
1351
+
1352
+ // RESETDATA: Limpiar después de enviar (opcional)
1353
+ resetData={true} // Si true, limpia el form después del submit exitoso
1354
+
1355
+ // ISLOADING: Estado de carga global (opcional)
1356
+ isLoading={isSubmitting} // Deshabilita todo el formulario
1357
+
1358
+ // CLASSNAME: Estilos del contenedor (opcional)
1359
+ className="bg-blue-50 shadow-xl rounded-2xl p-8"
1360
+
1361
+ // VALIDATIONMODE: Cuándo validar (opcional)
1362
+ validationMode="onChange" // Valida en cada cambio
1363
+
1364
+ // LOADINGCOMPONENT: Componente de carga (opcional)
1365
+ loadingComponent={<CustomSpinner />}
1366
+ />
1367
+ ```
1368
+
1369
+ ### Detalles de cada Prop:
1370
+
1371
+ #### 1. **validationMode** - Modos de Validación
1372
+
1373
+ ```tsx
1374
+ // ON BLUR - Valida cuando el campo pierde el foco
1375
+ <GatherFormBuilder
1376
+ validationMode="onBlur"
1377
+ // El usuario escribe, al cambiar de campo se valida
1378
+ config={config}
1379
+ onSubmit={handleSubmit}
1380
+ />
1381
+
1382
+ // ON CHANGE - Valida en cada tecla/cambio
1383
+ <GatherFormBuilder
1384
+ validationMode="onChange" // Validación en tiempo real
1385
+ // Muestra errores mientras el usuario escribe
1386
+ config={config}
1387
+ onSubmit={handleSubmit}
1388
+ />
1389
+
1390
+ // ON SUBMIT - Solo valida al enviar
1391
+ <GatherFormBuilder
1392
+ validationMode="onSubmit" // No molesta hasta el submit
1393
+ // Ideal para formularios cortos
1394
+ config={config}
1395
+ onSubmit={handleSubmit}
1396
+ />
1397
+
1398
+ // ON TOUCHED - Valida después de tocar el campo
1399
+ <GatherFormBuilder
1400
+ validationMode="onTouched"
1401
+ // Valida cuando el campo ha sido visitado
1402
+ config={config}
1403
+ onSubmit={handleSubmit}
1404
+ />
1405
+
1406
+ // ALL - Combina todos los modos
1407
+ <GatherFormBuilder
1408
+ validationMode="all" // Máxima validación
1409
+ // Valida en blur, change, touched y submit
1410
+ config={config}
1411
+ onSubmit={handleSubmit}
1412
+ />
1413
+ ```
1414
+
1415
+ #### 2. **defaultValues** - Valores Iniciales
1416
+
1417
+ ```tsx
1418
+ // VALORES SIMPLES
1419
+ const defaultValues = {
1420
+ // Campos de texto
1421
+ nombre: 'María García',
1422
+ email: 'maria@example.com',
1423
+ edad: 25,
1424
+
1425
+ // Selects
1426
+ pais: 'mx',
1427
+ categoria: 2,
1428
+
1429
+ // Multi-select (array)
1430
+ habilidades: ['js', 'react', 'node'],
1431
+
1432
+ // Checkbox group (array)
1433
+ intereses: ['tech', 'design'],
1434
+
1435
+ // Radio button (single value)
1436
+ plan: 'pro',
1437
+
1438
+ // Switch (boolean)
1439
+ newsletter: true,
1440
+ notificaciones: false,
1441
+
1442
+ // Fechas
1443
+ fechaNacimiento: new Date('1990-01-15'),
1444
+ periodo: {
1445
+ start: new Date('2024-01-01'),
1446
+ end: new Date('2024-12-31'),
1447
+ },
1448
+
1449
+ // Input-Select combinado
1450
+ identificacion: {
1451
+ tipoDoc: 'dni',
1452
+ numeroDoc: '12345678',
1453
+ },
1454
+ };
1455
+
1456
+ <GatherFormBuilder
1457
+ config={config}
1458
+ defaultValues={defaultValues}
1459
+ onSubmit={handleSubmit}
1460
+ />;
1461
+
1462
+ // VALORES DESDE API
1463
+ const [defaultValues, setDefaultValues] = useState({});
1464
+
1465
+ useEffect(() => {
1466
+ api.getUserData().then((data) => {
1467
+ setDefaultValues({
1468
+ nombre: data.fullName,
1469
+ email: data.email,
1470
+ telefono: data.phone,
1471
+ direccion: data.address,
1472
+ });
1473
+ });
1474
+ }, []);
1475
+
1476
+ <GatherFormBuilder
1477
+ config={config}
1478
+ defaultValues={defaultValues}
1479
+ onSubmit={handleSubmit}
1480
+ />;
1481
+ ```
1482
+
1483
+ #### 3. **resetData** - Comportamiento después del Submit
1484
+
1485
+ ```tsx
1486
+ // RESETEAR DESPUÉS DE ENVIAR (true)
1487
+ <GatherFormBuilder
1488
+ config={config}
1489
+ resetData={true} // Limpia el formulario después del submit exitoso
1490
+ defaultValues={{ nombre: "" }} // Vuelve a estos valores
1491
+ onSubmit={async (data) => {
1492
+ await saveData(data);
1493
+ // El formulario se limpia automáticamente
1494
+ }}
1495
+ />
1496
+
1497
+ // MANTENER DATOS DESPUÉS DE ENVIAR (false)
1498
+ <GatherFormBuilder
1499
+ config={config}
1500
+ resetData={false} // Mantiene los datos después del submit
1501
+ onSubmit={async (data) => {
1502
+ await saveData(data);
1503
+ // Los datos permanecen en el formulario
1504
+ }}
1505
+ />
1506
+ ```
1507
+
1508
+ ## ✅ Sistema de Validaciones
1509
+
1510
+ ### **ValidationRule** - Reglas de Validación
1511
+
1512
+ ```tsx
1513
+ interface ValidationRule {
1514
+ required?: boolean | string;
1515
+ minLength?: { value: number; message: string };
1516
+ maxLength?: { value: number; message: string };
1517
+ pattern?: { value: RegExp; message: string };
1518
+ min?: { value: number; message: string };
1519
+ max?: { value: number; message: string };
1520
+ validate?: (value: any, formData?: any) => boolean | string;
1521
+ }
1522
+ ```
1523
+
1524
+ ### Ejemplos de Validaciones:
1525
+
1526
+ #### 1. **Required** - Campo Obligatorio
1527
+
1528
+ ```tsx
1529
+ // REQUIRED SIMPLE
1530
+ {
1531
+ name: "email",
1532
+ label: "Email",
1533
+ type: "email",
1534
+ required: true // Mensaje por defecto: "Este campo es requerido"
1535
+ }
1536
+
1537
+ ```
1538
+
1539
+ #### 2. **MinLength / MaxLength** - Longitud de Texto
1540
+
1541
+ ```tsx
1542
+ {
1543
+ name: "password",
1544
+ label: "Contraseña",
1545
+ type: "password",
1546
+ minLength: 8, // Se convierte internamente a objeto de validación
1547
+ maxLength: 20,
1548
+ // Genera automáticamente:
1549
+ // minLength: { value: 8, message: "Mínimo 8 caracteres" }
1550
+ // maxLength: { value: 20, message: "Máximo 20 caracteres" }
1551
+ }
1552
+
1553
+ // CON MENSAJES PERSONALIZADOS (en la función generateValidationRules)
1554
+ {
1555
+ name: "username",
1556
+ label: "Usuario",
1557
+ type: "text",
1558
+ minLength: 3,
1559
+ maxLength: 15,
1560
+ // Los mensajes se personalizan en generateValidationRules
1561
+ }
1562
+ ```
1563
+
1564
+ #### 3. **Pattern** - Expresiones Regulares - Inputs
1565
+
1566
+ ```tsx
1567
+ // VALIDACIÓN DE EMAIL
1568
+ {
1569
+ name: "email",
1570
+ label: "Email",
1571
+ type: "email",
1572
+ pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
1573
+ patternMessage: "Ingrese un email válido (ej: usuario@dominio.com)"
1574
+ }
1575
+
1576
+ // VALIDACIÓN DE TELÉFONO
1577
+ {
1578
+ name: "phone",
1579
+ label: "Teléfono",
1580
+ type: "tel",
1581
+ pattern: "^[0-9]{10}$",
1582
+ patternMessage: "El teléfono debe tener 10 dígitos"
1583
+ }
1584
+
1585
+ // VALIDACIÓN DE CÓDIGO POSTAL
1586
+ {
1587
+ name: "zipCode",
1588
+ label: "Código Postal",
1589
+ type: "text",
1590
+ pattern: "^[0-9]{5}$",
1591
+ patternMessage: "El código postal debe tener 5 dígitos"
1592
+ }
1593
+
1594
+ // VALIDACIÓN DE CONTRASEÑA FUERTE
1595
+ {
1596
+ name: "password",
1597
+ label: "Contraseña",
1598
+ type: "password",
1599
+ pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$",
1600
+ patternMessage: "Debe incluir mayúsculas, minúsculas, números y caracteres especiales"
1601
+ }
1602
+ ```
1603
+
1604
+ #### 4. **Min / Max** - Valores Numéricos
1605
+
1606
+ ```tsx
1607
+ // EDAD
1608
+ {
1609
+ name: "age",
1610
+ label: "Edad",
1611
+ type: "number",
1612
+ min: 18,
1613
+ max: 100,
1614
+ required: true
1615
+ // Mensajes automáticos:
1616
+ // "El valor mínimo es 18"
1617
+ // "El valor máximo es 100"
1618
+ }
1619
+
1620
+ // CANTIDAD
1621
+ {
1622
+ name: "quantity",
1623
+ label: "Cantidad",
1624
+ type: "number",
1625
+ min: 1,
1626
+ max: 999,
1627
+ required: true
1628
+ }
1629
+
1630
+ // PRECIO
1631
+ {
1632
+ name: "price",
1633
+ label: "Precio",
1634
+ type: "number",
1635
+ min: 0.01,
1636
+ max: 999999.99,
1637
+ placeholder: "0.00"
1638
+ }
1639
+ ```
1640
+
1641
+ #### 5. **Validate** - Validación Personalizada
1642
+
1643
+ ```tsx
1644
+ // La función validate se implementaría en generateValidationRules
1645
+ // Aquí ejemplos de cómo se usaría:
1646
+
1647
+ // VALIDACIÓN PERSONALIZADA SIMPLE
1648
+ {
1649
+ name: "confirmPassword",
1650
+ label: "Confirmar Contraseña",
1651
+ type: "password",
1652
+ // En generateValidationRules se agregaría:
1653
+ // validate: (value, formData) => {
1654
+ // return value === formData.password || "Las contraseñas no coinciden";
1655
+ // }
1656
+ }
1657
+
1658
+ // VALIDACIÓN DE EMAIL ÚNICO
1659
+ {
1660
+ name: "email",
1661
+ label: "Email",
1662
+ type: "email",
1663
+ // validate: async (value) => {
1664
+ // const exists = await checkEmailExists(value);
1665
+ // return !exists || "Este email ya está registrado";
1666
+ // }
1667
+ }
1668
+
1669
+ // VALIDACIÓN CONDICIONAL
1670
+ {
1671
+ name: "companyName",
1672
+ label: "Nombre de Empresa",
1673
+ type: "text",
1674
+ // validate: (value, formData) => {
1675
+ // if (formData.userType === "business" && !value) {
1676
+ // return "El nombre de empresa es requerido para cuentas empresariales";
1677
+ // }
1678
+ // return true;
1679
+ // }
1680
+ }
1681
+ ```
1682
+
1683
+ ## 🎨 Configuración de Botones
1684
+
1685
+ ### **SubmitButton** - Botón Principal
1686
+
1687
+ ```tsx
1688
+ // CONFIGURACIÓN COMPLETA
1689
+ submitButton: {
1690
+ // TEXTO Y CONTENIDO
1691
+ text: "Enviar Formulario", // Texto del botón
1692
+ children: <CustomContent />, // O contenido JSX personalizado
1693
+
1694
+ // VISUAL
1695
+ variant: "gather-primary", // Estilo visual
1696
+ size: "lg", // Tamaño
1697
+ className: "w-full mt-6", // Clases adicionales
1698
+
1699
+ // ICONOS
1700
+ leftIcon: <SendIcon />, // Icono izquierdo
1701
+ rightIcon: <ArrowRightIcon />, // Icono derecho
1702
+
1703
+ // ESTADO
1704
+ disabled: false, // Deshabilitado
1705
+ loading: isSubmitting, // Estado de carga
1706
+ loadingText: "Procesando..." // Texto durante carga
1707
+ }
1708
+ ```
1709
+
1710
+ ### **ActionButton** - Botón Secundario
1711
+
1712
+ ```tsx
1713
+ // BOTÓN DE LIMPIAR
1714
+ actionButton: {
1715
+ text: "Limpiar Formulario",
1716
+ variant: "gather-outline",
1717
+ size: "default",
1718
+ leftIcon: <RefreshIcon />,
1719
+ className: "min-w-32"
1720
+ }
1721
+
1722
+ // BOTÓN DE CANCELAR
1723
+ actionButton: {
1724
+ text: "Cancelar",
1725
+ variant: "gather-ghost",
1726
+ leftIcon: <XIcon />,
1727
+ // onAction ejecutará la función definida en props
1728
+ }
1729
+
1730
+ // BOTÓN DE GUARDAR BORRADOR
1731
+ actionButton: {
1732
+ text: "Guardar Borrador",
1733
+ variant: "gather-secondary",
1734
+ leftIcon: <SaveIcon />
1735
+ }
1736
+ ```
1737
+
1738
+ ## 📋 Ejemplos Completos de Uso
1739
+
1740
+ ### Formulario con Validaciones Complejas:
1741
+
1742
+ ```tsx
1743
+ const FormularioConValidaciones = () => {
1744
+ const [isLoading, setIsLoading] = useState(false);
1745
+
1746
+ const config: FormConfig = {
1747
+ title: 'Registro con Validaciones',
1748
+ layout: 'grid',
1749
+ gridCols: 2,
1750
+ gap: 'gap-4',
1751
+ fields: [
1752
+ {
1753
+ name: 'username',
1754
+ label: 'Usuario',
1755
+ type: 'text',
1756
+ required: 'El nombre de usuario es obligatorio',
1757
+ minLength: 3,
1758
+ maxLength: 20,
1759
+ pattern: '^[a-zA-Z0-9_]+$',
1760
+ patternMessage: 'Solo letras, números y guión bajo',
1761
+ leftIcon: <UserIcon />,
1762
+ },
1763
+ {
1764
+ name: 'email',
1765
+ label: 'Email',
1766
+ type: 'email',
1767
+ required: true,
1768
+ colSpan: 2,
1769
+ },
1770
+ {
1771
+ name: 'age',
1772
+ label: 'Edad',
1773
+ type: 'number',
1774
+ required: true,
1775
+ min: 18,
1776
+ max: 100,
1777
+ },
1778
+ {
1779
+ name: 'password',
1780
+ label: 'Contraseña',
1781
+ type: 'password',
1782
+ required: true,
1783
+ minLength: 8,
1784
+ pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$',
1785
+ patternMessage: 'Debe tener mayúsculas, minúsculas y números',
1786
+ },
1787
+ ],
1788
+ submitButton: {
1789
+ text: 'Registrarse',
1790
+ variant: 'gather-primary',
1791
+ size: 'lg',
1792
+ loading: isLoading,
1793
+ loadingText: 'Registrando...',
1794
+ },
1795
+ actionButton: {
1796
+ text: 'Limpiar',
1797
+ variant: 'gather-outline',
1798
+ },
1799
+ };
1800
+
1801
+ const handleSubmit = async (data: any) => {
1802
+ setIsLoading(true);
1803
+ try {
1804
+ await api.register(data);
1805
+ toast.success('Registro exitoso');
1806
+ } catch (error) {
1807
+ toast.error('Error en el registro');
1808
+ } finally {
1809
+ setIsLoading(false);
1810
+ }
1811
+ };
1812
+
1813
+ const handleAction = () => {
1814
+ console.log('Formulario limpiado');
1815
+ };
1816
+
1817
+ return (
1818
+ <GatherFormBuilder
1819
+ config={config}
1820
+ onSubmit={handleSubmit}
1821
+ onAction={handleAction}
1822
+ validationMode='onChange'
1823
+ resetData={true}
1824
+ isLoading={isLoading}
1825
+ defaultValues={{
1826
+ age: 18,
1827
+ }}
1828
+ className='mx-auto max-w-2xl'
1829
+ />
1830
+ );
1831
+ };
1832
+ ```
1833
+
1834
+ Esta guía completa detalla todas las validaciones y props disponibles en GatherFormBuilder con ejemplos prácticos de implementación.
1835
+
1836
+ # 📊 Guía Completa de GatherTable
1837
+
1838
+ ## 🎯 Introducción
1839
+
1840
+ `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.
1841
+
1842
+ ## 🏗️ Arquitectura de Componentes
1843
+
1844
+ ```
1845
+ GatherTable (Contenedor principal)
1846
+ ├── GatherTableCaption (Título/descripción)
1847
+ ├── GatherTableHeader (Encabezados con ordenamiento)
1848
+ ├── GatherTableBody (Cuerpo con datos)
1849
+ │ ├── GatherTableRow (Filas)
1850
+ │ │ └── TableCell (Celdas)
1851
+ └── GatherTableFooter (Pie de tabla)
1852
+ ```
1853
+
1854
+ ## 📋 Uso Básico con renderCell por Defecto
1855
+
1856
+ ### Ejemplo Simple con Datos Básicos
1857
+
1858
+ ```tsx
1859
+ import {
1860
+ GatherTable,
1861
+ GatherTableHeader,
1862
+ GatherTableBody,
1863
+ } from '@/components/table';
1864
+ import { createColumns } from '@/utils/table';
1865
+
1866
+ interface User {
1867
+ id: number;
1868
+ name: string;
1869
+ email: string;
1870
+ role: string;
1871
+ isActive: boolean;
1872
+ }
1873
+
1874
+ const BasicTableExample = () => {
1875
+ // Crear columnas tipadas con autocompletado
1876
+ const columns = createColumns<User>([
1877
+ {
1878
+ key: 'id',
1879
+ label: 'ID',
1880
+ width: 80,
1881
+ align: 'center',
1882
+ sortable: true,
1883
+ },
1884
+ {
1885
+ key: 'name',
1886
+ label: 'Nombre',
1887
+ sortable: true,
1888
+ },
1889
+ {
1890
+ key: 'email',
1891
+ label: 'Correo Electrónico',
1892
+ sortable: true,
1893
+ },
1894
+ {
1895
+ key: 'role',
1896
+ label: 'Rol',
1897
+ width: 150,
1898
+ },
1899
+ {
1900
+ key: 'isActive',
1901
+ label: 'Estado',
1902
+ align: 'center',
1903
+ width: 100,
1904
+ sortable: true,
1905
+ },
1906
+ ]);
1907
+
1908
+ const users: User[] = [
1909
+ {
1910
+ id: 1,
1911
+ name: 'Juan Pérez',
1912
+ email: 'juan@example.com',
1913
+ role: 'Admin',
1914
+ isActive: true,
1915
+ },
1916
+ {
1917
+ id: 2,
1918
+ name: 'María García',
1919
+ email: 'maria@example.com',
1920
+ role: 'Usuario',
1921
+ isActive: false,
1922
+ },
1923
+ {
1924
+ id: 3,
1925
+ name: 'Carlos López',
1926
+ email: 'carlos@example.com',
1927
+ role: 'Editor',
1928
+ isActive: true,
1929
+ },
1930
+ ];
1931
+
1932
+ return (
1933
+ <GatherTable maxHeight='400px'>
1934
+ <GatherTableHeader
1935
+ columns={columns}
1936
+ sticky // Header fijo al hacer scroll
1937
+ />
1938
+ <GatherTableBody
1939
+ columns={columns}
1940
+ data={users}
1941
+ emptyMessage='No hay usuarios registrados'
1942
+ />
1943
+ </GatherTable>
1944
+ );
1945
+ };
1946
+ ```
1947
+
1948
+ ### RenderCell por Defecto - Manejo Automático de Tipos
1949
+
1950
+ El `defaultRenderCell` maneja automáticamente diferentes tipos de datos:
1951
+
1952
+ ```tsx
1953
+ const defaultRenderCell = (
1954
+ item: T,
1955
+ column: GatherTableColumn
1956
+ ): React.ReactNode => {
1957
+ const value = item[column.key];
1958
+
1959
+ // null o undefined → cadena vacía
1960
+ if (value === null || value === undefined) return '';
1961
+
1962
+ // boolean → "true" o "false"
1963
+ if (typeof value === 'boolean') return String(value);
1964
+
1965
+ // number → formato con separadores de miles
1966
+ if (typeof value === 'number') return value.toLocaleString();
1967
+
1968
+ // Date → formato de fecha local
1969
+ if (value instanceof Date) return value.toLocaleDateString();
1970
+
1971
+ // React Element → renderiza directamente
1972
+ if (React.isValidElement(value)) return value;
1973
+
1974
+ // Objeto o función → intenta renderizar como ReactNode
1975
+ if (typeof value === 'object' || typeof value === 'function') {
1976
+ return value as React.ReactNode;
1977
+ }
1978
+
1979
+ // Todo lo demás → convierte a string
1980
+ return String(value);
1981
+ };
1982
+ ```
1983
+
1984
+ ## 🎨 Uso con renderCell por defecto - utilizando una interfaz de tabla
1985
+
1986
+ ### Ejemplo Completo con Componentes Personalizados
1987
+
1988
+ ```tsx
1989
+ import { Badge } from '@/components/ui/badge';
1990
+ import { GatherButton } from '@/components/button';
1991
+ import { useTableSort } from '@/hooks/useTableSort';
1992
+
1993
+ interface BenefitDataTable {
1994
+ name: string;
1995
+ type: React.ReactNode; // Badge component
1996
+ detail: string;
1997
+ actions: React.ReactNode; // Button components
1998
+ }
1999
+
2000
+ const BenefitsTableExample = () => {
2001
+ const benefitsColumns = createColumns<BenefitDataTable>([
2002
+ {
2003
+ key: 'name',
2004
+ label: 'Nombre del Beneficio',
2005
+ sortable: true,
2006
+ width: '25%',
2007
+ },
2008
+ {
2009
+ key: 'type',
2010
+ label: 'Tipo',
2011
+ sortable: true,
2012
+ align: 'center',
2013
+ width: 150,
2014
+ },
2015
+ {
2016
+ key: 'detail',
2017
+ label: 'Detalle',
2018
+ sortable: true,
2019
+ },
2020
+ {
2021
+ key: 'actions',
2022
+ label: 'Acciones',
2023
+ align: 'center',
2024
+ width: 200,
2025
+ },
2026
+ ]);
2027
+
2028
+ // Preparar datos con componentes React
2029
+ const benefitsData: BenefitDataTable[] = benefits.map((benefit) => ({
2030
+ name: benefit.name,
2031
+
2032
+ // Componente Badge personalizado
2033
+ type: (
2034
+ <Badge
2035
+ className={cn(
2036
+ 'text-sm',
2037
+ benefit.type === 'GiftCard'
2038
+ ? 'border-[#E0F3D9] bg-[#F1F9EE] text-[#68B14B]'
2039
+ : benefit.type === 'Discount'
2040
+ ? 'border-[#CFF4E4] bg-[#E1FDF1] text-[#34D399]'
2041
+ : 'border-[#DBEAFE] bg-[#EFF6FF] text-[#3B82F6]'
2042
+ )}
2043
+ >
2044
+ {benefit.type === 'GiftCard'
2045
+ ? '🎁 Tarjeta Regalo'
2046
+ : benefit.type === 'Discount'
2047
+ ? '💰 Descuento'
2048
+ : '💵 Monto Monetario'}
2049
+ </Badge>
2050
+ ),
2051
+
2052
+ // Detalle condicional basado en tipo
2053
+ detail:
2054
+ (benefit.type === 'Discount' &&
2055
+ benefit.percentage != null &&
2056
+ `${benefit.percentage}%`) ||
2057
+ (benefit.type === 'MonetaryAmount' &&
2058
+ benefit.amount != null &&
2059
+ `$${benefit.amount} USD`) ||
2060
+ (benefit.type === 'GiftCard' &&
2061
+ benefit.amount != null &&
2062
+ `$${benefit.amount} USD`) ||
2063
+ '',
2064
+
2065
+ // Botones de acción
2066
+ actions: (
2067
+ <div className='flex space-x-2'>
2068
+ <GatherButton
2069
+ variant='gather-outline'
2070
+ size='sm'
2071
+ onClick={() => handleOpenEdit(benefit)}
2072
+ >
2073
+ ✏️ Editar
2074
+ </GatherButton>
2075
+ <GatherButton
2076
+ variant='gather-outline'
2077
+ size='sm'
2078
+ className='text-red-500 hover:bg-red-50'
2079
+ onClick={() => handleOpenDelete(benefit)}
2080
+ >
2081
+ 🗑️ Eliminar
2082
+ </GatherButton>
2083
+ </div>
2084
+ ),
2085
+ }));
2086
+
2087
+ // Hook de ordenamiento
2088
+ const { sortedData, sortConfig, handleSort } = useTableSort(benefitsData);
2089
+
2090
+ return (
2091
+ <GatherTable maxHeight='500px'>
2092
+ <GatherTableCaption>
2093
+ Lista de beneficios disponibles para el programa de referidos
2094
+ </GatherTableCaption>
2095
+
2096
+ <GatherTableHeader
2097
+ columns={benefitsColumns}
2098
+ sortConfig={sortConfig}
2099
+ onSort={handleSort}
2100
+ sticky
2101
+ />
2102
+
2103
+ <GatherTableBody
2104
+ columns={benefitsColumns}
2105
+ data={sortedData}
2106
+ emptyMessage='No hay beneficios configurados'
2107
+ onRowClick={(row, index) => {
2108
+ console.log('Fila seleccionada:', row, 'Índice:', index);
2109
+ }}
2110
+ />
2111
+ </GatherTable>
2112
+ );
2113
+ };
2114
+ ```
2115
+
2116
+ ## 🔧 Uso Manual con Componentes Individuales
2117
+
2118
+ ### Construcción Manual de Tabla
2119
+
2120
+ ```tsx
2121
+ import {
2122
+ GatherTable,
2123
+ GatherTableHeader,
2124
+ GatherTableBody,
2125
+ GatherTableRow,
2126
+ } from '@/components/table';
2127
+ import { TableCell } from '@/components/ui/table';
2128
+
2129
+ const ManualTableExample = () => {
2130
+ const users = [
2131
+ { id: 1, name: 'Ana', email: 'ana@email.com', status: 'active' },
2132
+ { id: 2, name: 'Luis', email: 'luis@email.com', status: 'inactive' },
2133
+ { id: 3, name: 'Carmen', email: 'carmen@email.com', status: 'pending' },
2134
+ ];
2135
+
2136
+ return (
2137
+ <GatherTable maxHeight='400px'>
2138
+ {/* Header manual */}
2139
+ <GatherTableHeader>
2140
+ <GatherTableRow>
2141
+ <TableCell className='bg-gray-100 font-bold'>ID</TableCell>
2142
+ <TableCell className='bg-gray-100 font-bold'>Nombre</TableCell>
2143
+ <TableCell className='bg-gray-100 font-bold'>Email</TableCell>
2144
+ <TableCell className='bg-gray-100 font-bold'>Estado</TableCell>
2145
+ </GatherTableRow>
2146
+ </GatherTableHeader>
2147
+
2148
+ {/* Body manual */}
2149
+ <GatherTableBody>
2150
+ {users.map((user, index) => (
2151
+ <GatherTableRow
2152
+ key={user.id}
2153
+ rowIndex={index}
2154
+ isLastRow={index === users.length - 1}
2155
+ clickable
2156
+ onClick={() => console.log('Usuario:', user)}
2157
+ >
2158
+ <TableCell>{user.id}</TableCell>
2159
+ <TableCell>{user.name}</TableCell>
2160
+ <TableCell>{user.email}</TableCell>
2161
+ <TableCell>
2162
+ <Badge
2163
+ variant={
2164
+ user.status === 'active'
2165
+ ? 'success'
2166
+ : user.status === 'inactive'
2167
+ ? 'destructive'
2168
+ : 'warning'
2169
+ }
2170
+ >
2171
+ {user.status === 'active'
2172
+ ? '✅ Activo'
2173
+ : user.status === 'inactive'
2174
+ ? '❌ Inactivo'
2175
+ : '⏳ Pendiente'}
2176
+ </Badge>
2177
+ </TableCell>
2178
+ </GatherTableRow>
2179
+ ))}
2180
+ </GatherTableBody>
2181
+ </GatherTable>
2182
+ );
2183
+ };
2184
+ ```
2185
+
2186
+ ## 🎣 Hook useTableSort - renderCell como Prop
2187
+
2188
+ ### Implementación Completa con Ordenamiento
2189
+
2190
+ ```tsx
2191
+ const SortableTableExample = () => {
2192
+ const [loading, setLoading] = useState(false);
2193
+
2194
+ interface Product {
2195
+ id: number;
2196
+ name: string;
2197
+ price: number;
2198
+ stock: number;
2199
+ category: string;
2200
+ createdAt: Date;
2201
+ }
2202
+
2203
+ const products: Product[] = [
2204
+ {
2205
+ id: 1,
2206
+ name: 'Laptop',
2207
+ price: 1299.99,
2208
+ stock: 15,
2209
+ category: 'Electrónica',
2210
+ createdAt: new Date('2024-01-15'),
2211
+ },
2212
+ {
2213
+ id: 2,
2214
+ name: 'Mouse',
2215
+ price: 29.99,
2216
+ stock: 150,
2217
+ category: 'Accesorios',
2218
+ createdAt: new Date('2024-02-20'),
2219
+ },
2220
+ {
2221
+ id: 3,
2222
+ name: 'Teclado',
2223
+ price: 89.99,
2224
+ stock: 45,
2225
+ category: 'Accesorios',
2226
+ createdAt: new Date('2024-01-10'),
2227
+ },
2228
+ ];
2229
+
2230
+ // Hook de ordenamiento con configuración inicial
2231
+ const {
2232
+ sortedData,
2233
+ sortConfig,
2234
+ handleSort,
2235
+ clearSort,
2236
+ isSorted,
2237
+ getSortDirection,
2238
+ } = useTableSort(products, {
2239
+ key: 'name',
2240
+ direction: 'asc',
2241
+ });
2242
+
2243
+ const productColumns = createColumns<Product>([
2244
+ {
2245
+ key: 'id',
2246
+ label: 'ID',
2247
+ width: 60,
2248
+ sortable: true,
2249
+ },
2250
+ {
2251
+ key: 'name',
2252
+ label: 'Producto',
2253
+ sortable: true,
2254
+ },
2255
+ {
2256
+ key: 'price',
2257
+ label: 'Precio',
2258
+ align: 'right',
2259
+ sortable: true,
2260
+ width: 120,
2261
+ },
2262
+ {
2263
+ key: 'stock',
2264
+ label: 'Stock',
2265
+ align: 'center',
2266
+ sortable: true,
2267
+ width: 100,
2268
+ },
2269
+ {
2270
+ key: 'category',
2271
+ label: 'Categoría',
2272
+ sortable: true,
2273
+ },
2274
+ {
2275
+ key: 'createdAt',
2276
+ label: 'Fecha',
2277
+ sortable: true,
2278
+ width: 150,
2279
+ },
2280
+ ]);
2281
+
2282
+ // RenderCell personalizado para formateo
2283
+ const renderCell = (
2284
+ item: Product,
2285
+ column: GatherTableColumn<Product>
2286
+ ): React.ReactNode => {
2287
+ switch (column.key) {
2288
+ case 'price':
2289
+ return (
2290
+ <span className='font-mono text-green-600'>
2291
+ ${item.price.toFixed(2)}
2292
+ </span>
2293
+ );
2294
+
2295
+ case 'stock':
2296
+ return (
2297
+ <Badge
2298
+ variant={
2299
+ item.stock > 50
2300
+ ? 'success'
2301
+ : item.stock > 10
2302
+ ? 'warning'
2303
+ : 'destructive'
2304
+ }
2305
+ >
2306
+ {item.stock} unidades
2307
+ </Badge>
2308
+ );
2309
+
2310
+ case 'createdAt':
2311
+ return new Intl.DateTimeFormat('es-MX', {
2312
+ year: 'numeric',
2313
+ month: 'short',
2314
+ day: 'numeric',
2315
+ }).format(item.createdAt);
2316
+
2317
+ default:
2318
+ return item[column.key as keyof Product];
2319
+ }
2320
+ };
2321
+
2322
+ return (
2323
+ <div className='space-y-4'>
2324
+ {/* Controles de ordenamiento */}
2325
+ <div className='flex gap-2'>
2326
+ <GatherButton variant='gather-outline' size='sm' onClick={clearSort}>
2327
+ 🔄 Limpiar Orden
2328
+ </GatherButton>
2329
+
2330
+ {sortConfig && (
2331
+ <Badge variant='secondary'>
2332
+ Ordenado por: {sortConfig.key} ({sortConfig.direction})
2333
+ </Badge>
2334
+ )}
2335
+ </div>
2336
+
2337
+ {/* Tabla */}
2338
+ <GatherTable maxHeight='600px'>
2339
+ <GatherTableHeader
2340
+ columns={productColumns}
2341
+ sortConfig={sortConfig}
2342
+ onSort={handleSort}
2343
+ sticky
2344
+ />
2345
+
2346
+ <GatherTableBody
2347
+ columns={productColumns}
2348
+ data={sortedData}
2349
+ renderCell={renderCell}
2350
+ loading={loading}
2351
+ loadingRows={3}
2352
+ emptyMessage={
2353
+ <div className='py-8 text-center'>
2354
+ <p className='text-gray-500'>No hay productos disponibles</p>
2355
+ <GatherButton variant='gather-primary' size='sm' className='mt-4'>
2356
+ Agregar Producto
2357
+ </GatherButton>
2358
+ </div>
2359
+ }
2360
+ onRowClick={(product, index) => {
2361
+ console.log(`Producto seleccionado:`, product);
2362
+ console.log(`Índice:`, index);
2363
+ }}
2364
+ rowClassName={(product, index) => {
2365
+ // Resaltar filas con stock bajo
2366
+ if (product.stock < 10) {
2367
+ return 'bg-red-50 hover:bg-red-100';
2368
+ }
2369
+ // Filas pares/impares
2370
+ return index % 2 === 0 ? 'bg-gray-50' : '';
2371
+ }}
2372
+ />
2373
+ </GatherTable>
2374
+ </div>
2375
+ );
2376
+ };
2377
+ ```
2378
+
2379
+ ## 🛠️ Funciones Auxiliares
2380
+
2381
+ ### createColumns - Con Type Safety
2382
+
2383
+ ```tsx
2384
+ // CON TYPE SAFETY COMPLETO
2385
+ interface Employee {
2386
+ id: number;
2387
+ firstName: string;
2388
+ lastName: string;
2389
+ department: string;
2390
+ salary: number;
2391
+ hireDate: Date;
2392
+ }
2393
+
2394
+ const employeeColumns = createColumns<Employee>([
2395
+ { key: 'id', label: 'ID', width: 60 },
2396
+ { key: 'firstName', label: 'Nombre', sortable: true },
2397
+ { key: 'lastName', label: 'Apellido', sortable: true },
2398
+ { key: 'department', label: 'Departamento' },
2399
+ { key: 'salary', label: 'Salario', align: 'right', sortable: true },
2400
+ { key: 'hireDate', label: 'Fecha Contratación', sortable: true },
2401
+ // { key: 'invalid', label: 'Error' } // ❌ TypeScript error!
2402
+ ]);
2403
+ ```
2404
+
2405
+ ### createFlexibleColumns - Para Datos Dinámicos
2406
+
2407
+ ```tsx
2408
+ // PARA DATOS DINÁMICOS O APIs EXTERNAS
2409
+ const apiColumns = createFlexibleColumns([
2410
+ { label: 'Usuario', key: 'user.profile.fullName' },
2411
+ { label: 'Avatar', key: 'user.profile.avatar_url' },
2412
+ { label: 'Configuración', key: 'settings.preferences.theme' },
2413
+ { label: 'Último Acceso', key: 'meta.lastLoginAt' },
2414
+ { label: 'Campo Calculado', key: 'computed_field' },
2415
+ ]);
2416
+
2417
+ // Para usar con datos de API
2418
+ const apiData = await fetch('/api/users').then((r) => r.json());
2419
+
2420
+ <GatherTableBody
2421
+ columns={apiColumns}
2422
+ data={apiData}
2423
+ renderCell={(item, column) => {
2424
+ // Acceso a propiedades anidadas
2425
+ const keys = column.key.split('.');
2426
+ let value = item;
2427
+ for (const key of keys) {
2428
+ value = value?.[key];
2429
+ }
2430
+ return value || '-';
2431
+ }}
2432
+ />;
2433
+ ```
2434
+
2435
+ ## 📊 Estados de la Tabla
2436
+
2437
+ ### Estado de Carga (Loading)
2438
+
2439
+ ```tsx
2440
+ <GatherTableBody
2441
+ columns={columns}
2442
+ data={[]}
2443
+ loading={true}
2444
+ loadingRows={5} // Muestra 5 filas skeleton
2445
+ columnsNumber={4} // Si no hay columns definidas
2446
+ />
2447
+ ```
2448
+
2449
+ ### Estado Vacío Personalizado
2450
+
2451
+ ```tsx
2452
+ <GatherTableBody
2453
+ columns={columns}
2454
+ data={[]}
2455
+ emptyMessage={
2456
+ <div className='flex flex-col items-center py-12'>
2457
+ <EmptyStateIcon className='mb-4 h-16 w-16 text-gray-400' />
2458
+ <h3 className='mb-2 text-lg font-semibold'>Sin resultados</h3>
2459
+ <p className='mb-4 text-gray-500'>No se encontraron registros</p>
2460
+ <GatherButton variant='gather-primary'>Crear Nuevo Registro</GatherButton>
2461
+ </div>
2462
+ }
2463
+ />
2464
+ ```
2465
+
2466
+ Esta guía completa cubre todos los aspectos de GatherTable, desde uso básico hasta implementaciones avanzadas con ordenamiento, filtrado y renderizado personalizado.