carconnect-gatherleads-ui-lib 2.1.5 → 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 +2400 -0
- package/dist/carconnect-gatherleads-ui-lib.css +1 -1
- package/dist/carconnect-gatherleads-ui-lib.d.ts +1013 -4
- package/dist/carconnect-gatherleads-ui-lib.js +15678 -1096
- package/dist/carconnect-gatherleads-ui-lib.umd.cjs +118 -9
- package/package.json +11 -1
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.
|