neogestify-ui-components 2.2.2 → 2.3.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +803 -349
  3. package/dist/components/ElementLibraryBuilder/index.js +299 -120
  4. package/dist/components/ElementLibraryBuilder/index.js.map +1 -1
  5. package/dist/components/ElementLibraryBuilder/index.mjs +300 -121
  6. package/dist/components/ElementLibraryBuilder/index.mjs.map +1 -1
  7. package/dist/components/VenueMapEditor/index.js.map +1 -1
  8. package/dist/components/VenueMapEditor/index.mjs.map +1 -1
  9. package/dist/components/alerts/index.js +108 -51
  10. package/dist/components/alerts/index.js.map +1 -1
  11. package/dist/components/alerts/index.mjs +109 -52
  12. package/dist/components/alerts/index.mjs.map +1 -1
  13. package/dist/components/html/index.d.mts +101 -22
  14. package/dist/components/html/index.d.ts +101 -22
  15. package/dist/components/html/index.js +506 -166
  16. package/dist/components/html/index.js.map +1 -1
  17. package/dist/components/html/index.mjs +507 -167
  18. package/dist/components/html/index.mjs.map +1 -1
  19. package/dist/components/icons/index.d.mts +5 -1
  20. package/dist/components/icons/index.d.ts +5 -1
  21. package/dist/components/icons/index.js +16 -0
  22. package/dist/components/icons/index.js.map +1 -1
  23. package/dist/components/icons/index.mjs +13 -1
  24. package/dist/components/icons/index.mjs.map +1 -1
  25. package/dist/context/theme/index.js +59 -37
  26. package/dist/context/theme/index.js.map +1 -1
  27. package/dist/context/theme/index.mjs +59 -37
  28. package/dist/context/theme/index.mjs.map +1 -1
  29. package/dist/index.d.mts +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +510 -166
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +508 -168
  34. package/dist/index.mjs.map +1 -1
  35. package/package.json +1 -1
  36. package/src/components/html/Button.tsx +84 -38
  37. package/src/components/html/Form.tsx +33 -13
  38. package/src/components/html/Input.tsx +110 -31
  39. package/src/components/html/Loading.tsx +58 -33
  40. package/src/components/html/Modal.tsx +67 -20
  41. package/src/components/html/Select.tsx +92 -39
  42. package/src/components/html/Table.tsx +274 -50
  43. package/src/components/html/TextArea.tsx +81 -31
  44. package/src/components/icons/icons.tsx +32 -0
package/README.md CHANGED
@@ -4,37 +4,34 @@ Biblioteca de componentes UI reutilizables con React, Tailwind CSS y SweetAlert2
4
4
 
5
5
  ## Características
6
6
 
7
- - Componentes HTML preestilizados (Button, Input, Form, Select, Table, Modal)
8
- - Colección de iconos SVG
9
- - Alertas preconfiguradas con SweetAlert2
7
+ - Componentes HTML preestilizados (Button, Input, TextArea, Form, Select, Table, Modal, Loading)
8
+ - Colección de iconos SVG (50+ iconos)
9
+ - Alertas preconfiguradas con SweetAlert2 + componente InfoAlert
10
+ - Sistema de tema (light/dark) con Context Provider
11
+ - Editor de mapas interactivo (VenueMapEditor/VenueMapViewer)
12
+ - Constructor de librerías de elementos (ElementLibraryBuilder)
10
13
  - Soporte para modo claro/oscuro
11
14
  - TypeScript incluido
12
- - Compatible con Tailwind CSS 4.1
15
+ - Compatible con Tailwind CSS 4.x
13
16
 
14
17
  ## Instalación
15
18
 
16
- Si estás usando workspaces con npm/bun:
17
-
18
19
  ### NPM
19
-
20
20
  ```bash
21
- # En tu proyecto
22
21
  npm i neogestify-ui-components
23
22
  ```
24
23
 
25
24
  ### BUN
26
25
  ```bash
27
- # En tu proyecto
28
- npm i neogestify-ui-components
26
+ bun add neogestify-ui-components
29
27
  ```
30
28
 
31
-
32
29
  ## Configuración
33
30
 
34
31
  ### 1. Asegúrate de tener Tailwind CSS configurado en tu proyecto
35
32
 
36
33
  ```bash
37
- bun add -D tailwindcss@4.1.0
34
+ bun add -D tailwindcss
38
35
  ```
39
36
 
40
37
  Tu proyecto debe tener Tailwind configurado ya que los componentes solo usan clases de Tailwind (no incluyen CSS compilado).
@@ -43,8 +40,6 @@ Tu proyecto debe tener Tailwind configurado ya que los componentes solo usan cla
43
40
 
44
41
  **⚠️ IMPORTANTE:** Esta librería requiere que configures Tailwind para escanear sus archivos fuente.
45
42
 
46
- **Para Tailwind CSS v4:**
47
-
48
43
  En tu archivo CSS principal (por ejemplo `src/index.css`):
49
44
 
50
45
  ```css
@@ -74,48 +69,533 @@ bun add react react-dom sweetalert2 sweetalert2-react-content
74
69
 
75
70
  ## Uso
76
71
 
77
- La biblioteca está organizada en módulos independientes:
72
+ Importa todo desde un solo punto:
78
73
 
79
- ### Componentes HTML
74
+ ```tsx
75
+ import {
76
+ Button,
77
+ Input,
78
+ TextArea,
79
+ Form,
80
+ Select,
81
+ Table,
82
+ Modal,
83
+ Loading,
84
+ // Iconos
85
+ HomeIcon,
86
+ SaveIcon,
87
+ DeleteIcon,
88
+ // Alertas
89
+ AlertaExito,
90
+ AlertaError,
91
+ AlertaAdvertencia,
92
+ AlertaConfirmacion,
93
+ AlertaToast,
94
+ InfoAlert,
95
+ // Theme
96
+ ThemeProvider,
97
+ useTheme,
98
+ ThemeToggle,
99
+ // VenueMapEditor
100
+ VenueMapEditor,
101
+ VenueMapViewer,
102
+ // ElementLibraryBuilder
103
+ ElementLibraryBuilder,
104
+ } from 'neogestify-ui-components';
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Componentes HTML
110
+
111
+ ### Button
112
+
113
+ Variantes: `primary`, `secondary`, `danger`, `success`, `warning`, `outline`, `ghost`, `icon`, `nav`, `link`, `toggle`, `custom`
80
114
 
81
115
  ```tsx
82
- import { Button, Input, Form, Select, Table, Modal } from 'neogestify-ui-components/html';
116
+ <Button variant="primary" size="lg" isLoading loadingText="Guardando...">
117
+ Guardar
118
+ </Button>
83
119
 
84
- function MiComponente() {
85
- return (
86
- <Form onSubmit={handleSubmit}>
87
- <Input
88
- label="Nombre"
89
- placeholder="Tu nombre"
90
- value={nombre}
91
- onChange={(e) => setNombre(e.target.value)}
92
- />
93
-
94
- <Select
95
- label="País"
96
- options={[
97
- { value: 'mx', label: 'México' },
98
- { value: 'ar', label: 'Argentina' }
99
- ]}
100
- />
101
-
102
- <Button variant="primary" type="submit">
103
- Enviar
104
- </Button>
105
- </Form>
106
- );
120
+ <Button variant="ghost" leftIcon={<SaveIcon className="w-4 h-4" />}>
121
+ Exportar
122
+ </Button>
123
+
124
+ <Button variant="primary" fullWidth shape="pill">
125
+ Continuar
126
+ </Button>
127
+
128
+ <Button variant="toggle" isActive={active} onClick={toggle}>
129
+ Toggle
130
+ </Button>
131
+ ```
132
+
133
+ Props:
134
+ - `variant`: Variante del botón (`primary` | `secondary` | `icon` | `danger` | `success` | `outline` | `ghost` | `nav` | `custom` | `link` | `warning` | `toggle`)
135
+ - `size`: Tamaño (`'sm'` | `'md'` | `'lg'`). Default: `'md'`
136
+ - `shape`: Forma del borde (`'rounded'` | `'pill'` | `'square'`). Default: `'rounded'` (`'pill'` para `icon`)
137
+ - `leftIcon`: Icono antes del texto (ReactNode)
138
+ - `rightIcon`: Icono después del texto (ReactNode)
139
+ - `fullWidth`: Ocupa el 100% del ancho (boolean)
140
+ - `isLoading`: Muestra estado de carga (boolean)
141
+ - `loadingText`: Texto durante carga
142
+ - `isActive`: Estado activo para variant `toggle` o `nav` (boolean)
143
+ - `disabled`: Deshabilita el botón
144
+ - `type`: Tipo HTML (`button`, `submit`, `reset`)
145
+ - `className`: Clases adicionales
146
+ - `children`: Contenido del botón
147
+
148
+ ---
149
+
150
+ ### Input
151
+
152
+ Soporta tipos: `text`, `email`, `password`, `number`, `checkbox`, `radio`, `date`, `tel`, `url`, `file`
153
+
154
+ ```tsx
155
+ <Input
156
+ label="Email"
157
+ type="email"
158
+ required
159
+ error="Email inválido"
160
+ helperText="Ingresa tu correo electrónico"
161
+ />
162
+
163
+ {/* Variantes visuales */}
164
+ <Input label="Nombre" variant="filled" size="lg" />
165
+ <Input label="Buscar" variant="minimal" />
166
+
167
+ {/* Con icono */}
168
+ <Input
169
+ label="Buscar"
170
+ icon={<SearchIcon className="w-4 h-4" />}
171
+ iconSide="left"
172
+ />
173
+
174
+ {/* Addons de texto (prefix / suffix) */}
175
+ <Input label="Precio" prefix="$" suffix="USD" />
176
+ <Input label="Sitio web" prefix="https://" suffix=".com" />
177
+
178
+ {/* Clearable */}
179
+ <Input
180
+ label="Filtrar"
181
+ value={filtro}
182
+ onChange={e => setFiltro(e.target.value)}
183
+ clearable
184
+ onClear={() => setFiltro('')}
185
+ />
186
+
187
+ {/* Checkbox */}
188
+ <Input type="checkbox" label="Acepto términos" />
189
+ ```
190
+
191
+ Props:
192
+ - `label`: Etiqueta del campo (string | ReactNode)
193
+ - `type`: Tipo de input HTML (`text`, `email`, `password`, `number`, `checkbox`, `radio`, `date`, `tel`, `url`, `file`)
194
+ - `variant`: Variante visual (`'default'` | `'outline'` | `'filled'` | `'minimal'`). Default: `'default'`
195
+ - `size`: Tamaño (`'sm'` | `'md'` | `'lg'`). Default: `'md'`
196
+ - `prefix`: Addon pegado al borde izquierdo (ReactNode)
197
+ - `suffix`: Addon pegado al borde derecho (ReactNode)
198
+ - `clearable`: Muestra botón `×` para limpiar cuando hay valor (boolean)
199
+ - `onClear`: Callback al hacer click en el botón limpiar
200
+ - `placeholder`: Placeholder
201
+ - `value`: Valor controlado
202
+ - `onChange`: Handler de cambio
203
+ - `error`: Mensaje de error (string)
204
+ - `helperText`: Texto de ayuda
205
+ - `icon`: Icono a mostrar (ReactNode)
206
+ - `iconSide`: Lado del icono (`'left'` | `'right'`)
207
+ - `required`: Muestra asterisco `*` en el label (boolean)
208
+ - `disabled`: Deshabilitado
209
+ - `className`: Clases adicionales
210
+ - `id`: ID del input (auto-generado si no se provee)
211
+
212
+ ---
213
+
214
+ ### TextArea
215
+
216
+ ```tsx
217
+ <TextArea
218
+ label="Descripción"
219
+ placeholder="Escribe una descripción..."
220
+ variant="outline"
221
+ size="large"
222
+ autoResize
223
+ />
224
+
225
+ {/* Con contador de caracteres */}
226
+ <TextArea
227
+ label="Bio"
228
+ value={bio}
229
+ onChange={e => setBio(e.target.value)}
230
+ maxLength={200}
231
+ showCount
232
+ variant="filled"
233
+ />
234
+
235
+ {/* Sin redimensión */}
236
+ <TextArea label="Notas" resize="none" rows={4} />
237
+ ```
238
+
239
+ Props:
240
+ - `label`: Etiqueta (string | ReactNode)
241
+ - `placeholder`: Placeholder
242
+ - `value`: Valor controlado
243
+ - `onChange`: Handler de cambio
244
+ - `rows`: Número de filas (heredado de HTML)
245
+ - `variant`: Variante visual (`'default'` | `'outline'` | `'filled'` | `'minimal'`)
246
+ - `size`: Tamaño (`'small'` | `'medium'` | `'large'`)
247
+ - `autoResize`: Crece automáticamente al escribir (boolean)
248
+ - `showCount`: Muestra contador de caracteres. Con `maxLength` muestra `12 / 200` (boolean)
249
+ - `resize`: Control de redimensión (`'vertical'` | `'horizontal'` | `'both'` | `'none'`). Default: `'vertical'`
250
+ - `required`: Muestra asterisco `*` en el label (boolean)
251
+ - `error`: Mensaje de error
252
+ - `helperText`: Texto de ayuda
253
+ - `disabled`: Deshabilitado
254
+ - `className`: Clases adicionales
255
+ - `id`: ID del textarea (auto-generado si no se provee)
256
+
257
+ ---
258
+
259
+ ### Form
260
+
261
+ ```tsx
262
+ {/* Variante card con borde y sombra reales */}
263
+ <Form onSubmit={handleSubmit} variant="card">
264
+ <Input label="Nombre" placeholder="Tu nombre" />
265
+ <Input label="Email" type="email" />
266
+ <Button variant="primary" type="submit">Enviar</Button>
267
+ </Form>
268
+
269
+ {/* Grid de 2 columnas */}
270
+ <Form variant="card" columns={2}>
271
+ <Input label="Nombre" />
272
+ <Input label="Apellido" />
273
+ <Input label="Email" type="email" />
274
+ <Input label="Teléfono" type="tel" />
275
+ <Button variant="primary" type="submit" fullWidth>Registrar</Button>
276
+ </Form>
277
+
278
+ {/* Grid de 3 columnas */}
279
+ <Form columns={3}>
280
+ <Input label="Calle" />
281
+ <Input label="Ciudad" />
282
+ <Input label="País" />
283
+ </Form>
284
+
285
+ <Form variant="inline">
286
+ <Input label="Buscar" placeholder="..." />
287
+ <Button variant="secondary">Buscar</Button>
288
+ </Form>
289
+ ```
290
+
291
+ Props:
292
+ - `onSubmit`: Handler del submit
293
+ - `variant`: Variante del layout (`'default'` | `'modal'` | `'card'` | `'inline'` | `'compact'`)
294
+ - `card`: Ahora incluye fondo blanco/oscuro, borde y sombra reales
295
+ - `columns`: Número de columnas del grid CSS (cualquier entero ≥ 2 activa el layout de grid con `gap` de `1rem`; con `1` se comporta como `default`)
296
+ - `className`: Clases adicionales
297
+ - Hereda props de `<form>` (method, action, etc.)
298
+
299
+ ---
300
+
301
+ ### Select
302
+
303
+ ```tsx
304
+ <Select
305
+ label="Categoría"
306
+ placeholder="Selecciona..."
307
+ required
308
+ options={[
309
+ { value: '1', label: 'Opción 1' },
310
+ { value: '2', label: 'Opción 2', disabled: true },
311
+ { value: '3', label: 'Opción 3', selected: true },
312
+ ]}
313
+ error="Debes seleccionar una categoría"
314
+ />
315
+
316
+ {/* Variantes visuales */}
317
+ <Select label="País" variant="outline" size="lg" />
318
+ <Select label="Estado" variant="filled" />
319
+ <Select label="Tipo" variant="minimal" />
320
+
321
+ {/* Con icono izquierdo */}
322
+ <Select
323
+ label="Categoría"
324
+ icon={<CategorieIcon className="w-4 h-4" />}
325
+ options={opciones}
326
+ />
327
+ ```
328
+
329
+ Props:
330
+ - `label`: Etiqueta (string | ReactNode)
331
+ - `placeholder`: Placeholder
332
+ - `options`: Array de opciones:
333
+ - `value`: Valor de la opción (string | number)
334
+ - `label`: Texto a mostrar
335
+ - `disabled`: Deshabilita la opción (boolean)
336
+ - `selected`: Pre-selecciona la opción en modo no controlado (boolean)
337
+ - `variant`: Variante visual (`'default'` | `'outline'` | `'filled'` | `'minimal'` | `'custom'`). `'small'` sigue siendo válido por compatibilidad (equivale a `size='sm'`)
338
+ - `size`: Tamaño (`'sm'` | `'md'` | `'lg'`). Default: `'md'`
339
+ - `icon`: Icono en el lado izquierdo (ReactNode)
340
+ - `value`: Valor seleccionado (controlado)
341
+ - `onChange`: Handler de cambio
342
+ - `error`: Estado de error. Si es `string` muestra el mensaje; si es `true` solo aplica estilos de error
343
+ - `helperText`: Texto de ayuda (se muestra si no hay `error` string)
344
+ - `required`: Muestra asterisco `*` en el label (boolean)
345
+ - `disabled`: Deshabilita el select
346
+ - `className`: Clases adicionales
347
+ - `id`: ID del select (auto-generado si no se provee)
348
+
349
+ ---
350
+
351
+ ### Table
352
+
353
+ ```tsx
354
+ <Table
355
+ columns={[
356
+ { header: 'ID', align: 'center', width: 60 },
357
+ { header: 'Nombre', className: 'font-bold', sticky: true },
358
+ { header: 'Email' },
359
+ { header: 'Ventas', key: 'ventas', sortable: true, align: 'right' },
360
+ ]}
361
+ rows={[
362
+ ['1', 'Juan', 'juan@ejemplo.com', '$1,200'],
363
+ ['2', 'María', 'maria@ejemplo.com', '$3,400'],
364
+ ]}
365
+ variant="striped"
366
+ size="sm"
367
+ rounded
368
+ shadow
369
+ onRowClick={(index) => console.log('Click fila', index)}
370
+ sortState={{ key: 'ventas', direction: 'desc' }}
371
+ onSort={(key) => console.log('Ordenar por', key)}
372
+ />
373
+ ```
374
+
375
+ #### Variantes
376
+
377
+ | Variante | Descripción |
378
+ |----------|-------------|
379
+ | `default` | Fondo blanco con divisores horizontales y hover gris |
380
+ | `striped` | Filas alternas gris/blanco con hover azul |
381
+ | `bordered` | Bordes en todas las celdas |
382
+ | `minimal` | Sin fondos, solo línea inferior en header y celdas |
383
+ | `ghost` | Sin fondos, borde inferior doble en header, divisores sutiles |
384
+ | `card` | Header con fondo suave, divisores finos entre filas |
385
+ | `accent` | Header azul (`bg-blue-600`) con texto blanco |
386
+ | `dark` | Header oscuro (`bg-gray-800`) con texto claro |
387
+ | `custom` | Sin estilos predefinidos, control total vía clases |
388
+
389
+ #### ColumnDef
390
+
391
+ ```tsx
392
+ interface ColumnDef {
393
+ header: ReactNode; // Contenido del encabezado
394
+ className?: string; // Clase para th y td de esta columna
395
+ align?: 'left' | 'center' | 'right';
396
+ width?: string | number; // Ancho fijo (px, %, rem…)
397
+ minWidth?: string | number; // Ancho mínimo
398
+ sticky?: boolean; // Fija la columna a la izquierda en scroll horizontal
399
+ thStyle?: CSSProperties; // Estilos inline solo para <th>
400
+ tdStyle?: CSSProperties; // Estilos inline solo para <td>
401
+ sortable?: boolean; // Muestra indicador de ordenación (requiere key)
402
+ key?: string; // Clave usada en sortState y onSort
107
403
  }
108
404
  ```
109
405
 
110
- ### Iconos
406
+ #### Props
407
+
408
+ - `columns`: Array de `ColumnDef` o strings/ReactNode simples
409
+ - `rows`: Datos del cuerpo (`ReactNode[][]`)
410
+ - `variant`: Variante visual (ver tabla arriba). Default: `'default'`
411
+ - `size`: Tamaño de padding (`'sm'` | `'md'` | `'lg'`). Default: `'md'`
412
+ - `className`: Clases adicionales para el wrapper `<div>`
413
+ - `tableClassName`: Clases adicionales para el `<table>`
414
+ - `thClassName`: Clases adicionales para cada `<th>`
415
+ - `tdClassName`: Clases adicionales para cada `<td>`
416
+ - `trClassName`: Clases por fila (`string` | `(rowIndex: number) => string`)
417
+ - `emptyState`: Contenido cuando no hay datos (ReactNode)
418
+ - `onRowClick`: Callback al hacer click en una fila (`(rowIndex) => void`)
419
+ - `hideHeader`: Oculta el `<thead>` (boolean)
420
+ - `style`: Estilos inline para el `<table>`
421
+ - `stickyHeader`: Fija el `<thead>` al hacer scroll vertical (boolean)
422
+ - `caption`: Caption accesible renderizado en `<caption>`
423
+ - `footerRows`: Filas del `<tfoot>` (`ReactNode[][]`)
424
+ - `loading`: Muestra esqueleto animado en lugar de filas (boolean)
425
+ - `loadingRows`: Número de filas esqueleto cuando `loading=true`. Default: `4`
426
+ - `getRowStyle`: Estilo inline por fila (`(rowIndex: number) => CSSProperties`)
427
+ - `rounded`: Agrega `rounded-lg` al wrapper (boolean)
428
+ - `shadow`: Agrega sombra al wrapper (boolean)
429
+ - `hoverable`: Desactiva el efecto hover si es `false`. Default: `true`
430
+ - `sortState`: Estado de ordenación activo (`{ key: string, direction: 'asc' | 'desc' }`)
431
+ - `onSort`: Callback al hacer click en un `<th>` sortable (`(key: string) => void`)
432
+
433
+ #### Ejemplos adicionales
434
+
435
+ ```tsx
436
+ {/* Con loading skeleton */}
437
+ <Table columns={['Nombre', 'Email', 'Rol']} rows={[]} loading loadingRows={5} />
438
+
439
+ {/* Con footer de totales */}
440
+ <Table
441
+ columns={['Producto', 'Cantidad', 'Total']}
442
+ rows={[['Teclado', '2', '$60'], ['Mouse', '3', '$45']]}
443
+ footerRows={[['', 'Total', '$105']]}
444
+ variant="card"
445
+ rounded
446
+ shadow
447
+ />
448
+
449
+ {/* Header fijo + columna sticky + sort */}
450
+ <Table
451
+ columns={[
452
+ { header: '#', sticky: true, width: 50 },
453
+ { header: 'Nombre', sticky: true },
454
+ { header: 'Fecha', key: 'fecha', sortable: true },
455
+ { header: 'Monto', key: 'monto', sortable: true, align: 'right' },
456
+ ]}
457
+ rows={data}
458
+ stickyHeader
459
+ sortState={sort}
460
+ onSort={(key) => setSort(prev => ({ key, direction: prev?.key === key && prev.direction === 'asc' ? 'desc' : 'asc' }))}
461
+ />
462
+
463
+ {/* Filas coloreadas dinámicamente */}
464
+ <Table
465
+ columns={['Estado', 'Mensaje']}
466
+ rows={logs.map(l => [l.level, l.message])}
467
+ getRowStyle={(i) => logs[i].level === 'error' ? { background: '#fef2f2' } : {}}
468
+ variant="minimal"
469
+ />
470
+ ```
471
+
472
+ ---
473
+
474
+ ### Modal
475
+
476
+ ```tsx
477
+ const modalRef = useRef<ModalRef>(null);
478
+
479
+ <Modal
480
+ ref={modalRef}
481
+ title="Confirmar acción"
482
+ size="md"
483
+ variant="danger"
484
+ closeOnBackdrop
485
+ closeOnEsc
486
+ onClose={() => setShowModal(false)}
487
+ footer={
488
+ <>
489
+ <Button variant="secondary" onClick={() => modalRef.current?.handleClose()}>
490
+ Cancelar
491
+ </Button>
492
+ <Button variant="danger" onClick={handleConfirm}>
493
+ Eliminar
494
+ </Button>
495
+ </>
496
+ }
497
+ >
498
+ <p>¿Estás seguro de que deseas continuar?</p>
499
+ </Modal>
500
+
501
+ {/* Con title como ReactNode */}
502
+ <Modal
503
+ title={<span className="flex items-center gap-2"><InfoIcon className="w-5 h-5" /> Información</span>}
504
+ size="lg"
505
+ onClose={onClose}
506
+ >
507
+ {children}
508
+ </Modal>
509
+ ```
510
+
511
+ #### Variantes de header
512
+
513
+ | Variante | Descripción |
514
+ |----------|-------------|
515
+ | `default` | Header gris neutro |
516
+ | `danger` | Header rojo para acciones destructivas |
517
+ | `success` | Header verde para confirmaciones positivas |
518
+ | `warning` | Header amarillo para advertencias |
519
+
520
+ #### Tamaños
521
+
522
+ | Size | Ancho máximo |
523
+ |------|-------------|
524
+ | `sm` | `max-w-sm` |
525
+ | `md` | `max-w-md` |
526
+ | `lg` | `max-w-2xl` |
527
+ | `xl` | `max-w-4xl` |
528
+ | `full` | `95vw` |
529
+
530
+ Props:
531
+ - `title`: Título del modal (string | ReactNode)
532
+ - `children`: Contenido
533
+ - `footer`: Contenido del pie
534
+ - `onClose`: Handler al cerrar
535
+ - `size`: Tamaño predefinido (`'sm'` | `'md'` | `'lg'` | `'xl'` | `'full'`)
536
+ - `maxWidth`: Clase de ancho personalizada (deprecated, usar `size`)
537
+ - `variant`: Estilo del header (`'default'` | `'danger'` | `'success'` | `'warning'`)
538
+ - `closeOnBackdrop`: Cierra al hacer click fuera del modal (boolean, default: `false`)
539
+ - `closeOnEsc`: Cierra al presionar Escape (boolean, default: `false`)
540
+ - `showCloseButton`: Muestra botón de cerrar (boolean, default: `true`)
541
+ - `zIndex`: Z-index del modal (number, default: `50`)
542
+
543
+ Métodos del ref (`ModalRef`):
544
+ - `handleClose()`: Cierra el modal con animación
545
+
546
+ ---
547
+
548
+ ### Loading
549
+
550
+ ```tsx
551
+ <Loading variant="spinner" size="large" color="primary" label="Cargando..." />
552
+
553
+ <Loading variant="dots" size="medium" color="white" />
554
+ <Loading variant="pulse" size="small" color="success" />
555
+ <Loading variant="bars" size="xl" color="danger" />
556
+ <Loading variant="ring" color="warning" />
557
+ <Loading variant="cube" size="large" />
558
+
559
+ {/* Overlay sobre el contenedor (el padre debe tener position: relative) */}
560
+ <div className="relative h-48">
561
+ <MiContenido />
562
+ {cargando && <Loading overlay variant="ring" color="primary" />}
563
+ </div>
564
+
565
+ {/* Overlay de página completa */}
566
+ {cargando && <Loading fullPage label="Procesando..." />}
567
+ ```
568
+
569
+ Props:
570
+ - `variant`: Variante del loader (`'spinner'` | `'dots'` | `'pulse'` | `'bars'` | `'ring'` | `'cube'`)
571
+ - `size`: Tamaño (`'small'` | `'medium'` | `'large'` | `'xl'`)
572
+ - `color`: Color (`'primary'` | `'white'` | `'gray'` | `'success'` | `'danger'` | `'warning'`)
573
+ - `label`: Texto debajo del icono
574
+ - `overlay`: Cubre el contenedor más cercano con `position: relative` con fondo semitransparente (boolean)
575
+ - `fullPage`: Overlay `fixed` que cubre toda la pantalla (`z-50`) (boolean)
576
+ - `className`: Clases adicionales
577
+
578
+ ---
579
+
580
+ ## Iconos SVG
581
+
582
+ La biblioteca incluye más de 50 iconos SVG:
111
583
 
112
584
  ```tsx
113
585
  import {
114
586
  HomeIcon,
115
587
  SaveIcon,
116
588
  DeleteIcon,
117
- EditIcon
118
- } from 'neogestify-ui-components/icons';
589
+ EditIcon,
590
+ SearchIcon,
591
+ AddIcon,
592
+ CloseIcon,
593
+ MenuIcon,
594
+ CheckIcon,
595
+ ArrowLeftIcon,
596
+ ArrowRightIcon,
597
+ // ... y muchos más
598
+ } from 'neogestify-ui-components';
119
599
 
120
600
  function MiComponente() {
121
601
  return (
@@ -127,7 +607,29 @@ function MiComponente() {
127
607
  }
128
608
  ```
129
609
 
130
- ### Alertas
610
+ **Lista completa de iconos:**
611
+
612
+ - SpinnerIcon, AnimateSpin, GearIcon, CheckIcon, BackIcon
613
+ - NotFoundIcon, BoxIcon, ChartIcon, UsersIcon, DocumentIcon
614
+ - LogoutIcon, HomeIcon, BuildingIcon, CashIcon, MenuIcon
615
+ - CloseIcon, AddIcon, SearchIcon, SaveIcon, CancelIcon
616
+ - DeleteIcon, EditIcon, CategorieIcon, FolderIcon, ArrowIcon
617
+ - FilterIcon, QuestionIcon, LocationIcon, CalendarIcon, InfoIcon
618
+ - MoonIcon, SunIcon, CamaraIcon, ArrowLeftIcon, ArrowRightIcon
619
+ - TrashIcon, MinusIcon, MoneyIcon, PercentIcon, StackIcon
620
+ - ClockIcon, CheckCircleIcon, CajasIcon, PrinterIcon, NetworkIcon
621
+ - TestIcon, FacturacionIcon, WhatsAppIcon, ArchiveIcon, CopyIcon
622
+ - PasteIcon, RestaurantMenuIcon, CloudIcon, ShieldIcon
623
+ - BarsChartsIcon, LightingIcon, LifeGuardIcon, MonitorIcon
624
+ - TruckIcon, IconCursor, IconHand, IconGrid, IconZoomIn
625
+ - IconZoomOut, IconReset, IconUndo, IconRedo, IconPlace
626
+ - IconErase, IconDuplicate, IconWall, IconDownload, IconUpload
627
+ - IconPolygon, IconLayers
628
+ - ChevronDownIcon, SortAscIcon, SortDescIcon, SortBothIcon
629
+
630
+ ---
631
+
632
+ ## Alertas (SweetAlert2)
131
633
 
132
634
  ```tsx
133
635
  import {
@@ -135,49 +637,132 @@ import {
135
637
  AlertaError,
136
638
  AlertaAdvertencia,
137
639
  AlertaConfirmacion,
138
- AlertaToast
139
- } from 'neogestify-ui-components/alerts';
640
+ AlertaToast,
641
+ AlertaInfo,
642
+ Alerta, // función genérica
643
+ } from 'neogestify-ui-components';
140
644
 
141
645
  function MiComponente() {
142
646
  const handleGuardar = async () => {
143
- try {
144
- await guardarDatos();
145
- AlertaExito('¡Guardado!', 'Los datos se guardaron correctamente');
146
- } catch (error) {
147
- AlertaError('Error', 'No se pudieron guardar los datos');
148
- }
647
+ await guardarDatos();
648
+ AlertaExito('¡Guardado!', 'Los datos se guardaron correctamente');
649
+ };
650
+
651
+ const handleError = () => {
652
+ AlertaError('Error', 'No se pudieron guardar los datos');
149
653
  };
150
654
 
151
- const handleEliminar = () => {
655
+ const handleAdvertencia = () => {
152
656
  AlertaAdvertencia(
153
657
  '¿Estás seguro?',
154
658
  'Esta acción no se puede deshacer',
155
- async () => {
156
- await eliminarDatos();
157
- AlertaToast('Eliminado', 'Registro eliminado', 'success');
158
- }
659
+ async () => { await eliminarDatos(); }
159
660
  );
160
661
  };
161
662
 
663
+ const handleConfirmacion = () => {
664
+ AlertaConfirmacion(
665
+ '¿Continuar?',
666
+ '¿Deseas proceder con la acción?',
667
+ () => { console.log('Confirmado'); },
668
+ () => { console.log('Cancelado'); }
669
+ );
670
+ };
671
+
672
+ const handleToast = () => {
673
+ AlertaToast('Éxito', 'Operación completada', 'success', 3000, 'top-end');
674
+ };
675
+
162
676
  return (
163
- <Button variant="danger" onClick={handleEliminar}>
677
+ <Button variant="danger" onClick={handleAdvertencia}>
164
678
  Eliminar
165
679
  </Button>
166
680
  );
167
681
  }
168
682
  ```
169
683
 
170
- ### Sistema de Tema
684
+ ### Funciones disponibles
685
+
686
+ | Función | Descripción |
687
+ |---------|-------------|
688
+ | `Alerta(options)` | Función genérica con todas las opciones |
689
+ | `AlertaExito(title, text, onConfirm?, options?)` | Alerta de éxito |
690
+ | `AlertaError(title, text, onConfirm?, options?)` | Alerta de error |
691
+ | `AlertaInfo(title, text, onConfirm?, options?)` | Alerta informativa |
692
+ | `AlertaAdvertencia(title, text, onConfirm?, onCancel?, options?)` | Alerta de advertencia |
693
+ | `AlertaConfirmacion(title, text, onConfirm?, onCancel?, options?)` | Alerta de confirmación |
694
+ | `AlertaToast(title, text, icon?, timer?, position?)` | Notificación toast |
695
+
696
+ ### Opciones de Alerta genérica
697
+
698
+ ```tsx
699
+ Alerta({
700
+ title: 'Título',
701
+ text: 'Descripción',
702
+ icon: 'success' | 'error' | 'warning' | 'info' | 'question',
703
+ confirmButtonText: 'Aceptar',
704
+ showCancelButton: true,
705
+ cancelButtonText: 'Cancelar',
706
+ showDenyButton: true,
707
+ denyButtonText: 'No',
708
+ onConfirm: () => {},
709
+ onCancel: () => {},
710
+ onDeny: () => {},
711
+ toast: true,
712
+ timer: 3000,
713
+ position: 'top-end',
714
+ allowOutsideClick: true,
715
+ allowEscapeKey: true,
716
+ input: 'text' | 'email' | 'password' | 'number' | 'textarea' | 'select',
717
+ inputLabel: 'Label',
718
+ inputPlaceholder: 'Placeholder',
719
+ inputValue: 'Valor inicial',
720
+ inputValidator: (value) => null | 'Error message',
721
+ });
722
+ ```
171
723
 
172
- El sistema de tema incluye un Context Provider y un componente toggle listo para usar.
724
+ ---
725
+
726
+ ## InfoAlert (Componente)
727
+
728
+ Componente visual de alerta en línea:
729
+
730
+ ```tsx
731
+ import { InfoAlert } from 'neogestify-ui-components';
173
732
 
174
- #### 1. Configurar el ThemeProvider
733
+ <InfoAlert>
734
+ Este es un mensaje informativo
735
+ </InfoAlert>
736
+
737
+ <InfoAlert type="success">
738
+ Operación exitosa
739
+ </InfoAlert>
740
+
741
+ <InfoAlert type="warning">
742
+ Advertencia importante
743
+ </InfoAlert>
744
+
745
+ <InfoAlert type="error">
746
+ Ha ocurrido un error
747
+ </InfoAlert>
748
+ ```
749
+
750
+ Props:
751
+ - `type`: Variante (`info` | `success` | `warning` | `error`)
752
+ - `children`: Contenido
753
+ - `className`: Clases adicionales
754
+
755
+ ---
756
+
757
+ ## Sistema de Tema
758
+
759
+ ### 1. Configurar el ThemeProvider
175
760
 
176
761
  Envuelve tu aplicación con el `ThemeProvider`:
177
762
 
178
763
  ```tsx
179
764
  // main.tsx o App.tsx
180
- import { ThemeProvider } from 'neogestify-ui-components/theme';
765
+ import { ThemeProvider } from 'neogestify-ui-components';
181
766
 
182
767
  function Main() {
183
768
  return (
@@ -188,10 +773,10 @@ function Main() {
188
773
  }
189
774
  ```
190
775
 
191
- #### 2. Usar el ThemeToggle
776
+ ### 2. Usar el ThemeToggle
192
777
 
193
778
  ```tsx
194
- import { ThemeToggle } from 'neogestify-ui-components/theme';
779
+ import { ThemeToggle } from 'neogestify-ui-components';
195
780
 
196
781
  function Header() {
197
782
  return (
@@ -202,10 +787,10 @@ function Header() {
202
787
  }
203
788
  ```
204
789
 
205
- #### 3. Usar el hook useTheme
790
+ ### 3. Usar el hook useTheme
206
791
 
207
792
  ```tsx
208
- import { useTheme } from 'neogestify-ui-components/theme';
793
+ import { useTheme } from 'neogestify-ui-components';
209
794
 
210
795
  function MiComponente() {
211
796
  const { theme, toggleTheme, setTheme } = useTheme();
@@ -223,73 +808,6 @@ function MiComponente() {
223
808
 
224
809
  El tema se guarda automáticamente en `localStorage` y se aplica al cargar la página.
225
810
 
226
- ## Componentes Disponibles
227
-
228
- ### Button
229
-
230
- Variantes: `primary`, `secondary`, `danger`, `success`, `warning`, `outline`, `icon`, `nav`, `link`, `toggle`
231
-
232
- ```tsx
233
- <Button variant="primary" isLoading loadingText="Guardando...">
234
- Guardar
235
- </Button>
236
- ```
237
-
238
- ### Input
239
-
240
- Soporta tipos: `text`, `email`, `password`, `number`, `checkbox`, etc.
241
-
242
- ```tsx
243
- <Input
244
- label="Email"
245
- type="email"
246
- error="Email inválido"
247
- helperText="Ingresa tu correo electrónico"
248
- />
249
- ```
250
-
251
- ### Select
252
-
253
- ```tsx
254
- <Select
255
- label="Categoría"
256
- placeholder="Selecciona..."
257
- options={categorias}
258
- variant="default"
259
- />
260
- ```
261
-
262
- ### Table
263
-
264
- ```tsx
265
- <Table
266
- headers={['ID', 'Nombre', 'Email']}
267
- rows={[
268
- ['1', 'Juan', 'juan@ejemplo.com'],
269
- ['2', 'María', 'maria@ejemplo.com']
270
- ]}
271
- />
272
- ```
273
-
274
- ### Modal
275
-
276
- ```tsx
277
- const modalRef = useRef<ModalRef>(null);
278
-
279
- <Modal
280
- ref={modalRef}
281
- title="Mi Modal"
282
- onClose={() => setShowModal(false)}
283
- footer={
284
- <Button onClick={() => modalRef.current?.handleClose()}>
285
- Cerrar
286
- </Button>
287
- }
288
- >
289
- <p>Contenido del modal</p>
290
- </Modal>
291
- ```
292
-
293
811
  ---
294
812
 
295
813
  ## VenueMapEditor
@@ -300,20 +818,17 @@ Editor de mapas de recintos interactivo basado en SVG puro. Permite diseñar la
300
818
 
301
819
  ```tsx
302
820
  import {
303
- VenueMapEditor, // editor completo
304
- VenueMapViewer, // modo solo lectura
305
- } from 'neogestify-ui-components/VenueMapEditor';
821
+ VenueMapEditor,
822
+ VenueMapViewer,
823
+ } from 'neogestify-ui-components';
306
824
 
307
- // Tipos TypeScript
308
825
  import type {
309
826
  VenueMap, Floor, MapElement,
310
827
  ElementTypeDef, ElementGroup, ElementLibrary,
311
828
  ElementStatus, VenueMapEditorProps,
312
- } from 'neogestify-ui-components/VenueMapEditor';
829
+ } from 'neogestify-ui-components';
313
830
  ```
314
831
 
315
- ---
316
-
317
832
  ### Uso básico
318
833
 
319
834
  El componente funciona sin ninguna prop — crea un mapa vacío con una planta por defecto:
@@ -332,28 +847,24 @@ Con configuración mínima:
332
847
  />
333
848
  ```
334
849
 
335
- ---
336
-
337
850
  ### Cargar y guardar un mapa desde código
338
851
 
339
- El prop `initialMap` acepta un `VenueMap` (del estado de la app, de una API, de `localStorage`, etc.). Cuando el valor cambia por referencia, el editor reinicia su historial al nuevo mapa. El ciclo `onChange → initialMap` es **seguro** — el componente detecta el eco de su propio `onChange` y no genera bucles infinitos.
852
+ El prop `initialMap` acepta un `VenueMap`. Cuando el valor cambia por referencia, el editor reinicia su historial al nuevo mapa.
340
853
 
341
854
  ```tsx
342
855
  import { useState, useEffect } from 'react';
343
- import { VenueMapEditor } from 'neogestify-ui-components/VenueMapEditor';
344
- import type { VenueMap } from 'neogestify-ui-components/VenueMapEditor';
856
+ import { VenueMapEditor } from 'neogestify-ui-components';
857
+ import type { VenueMap } from 'neogestify-ui-components';
345
858
 
346
859
  function App() {
347
860
  const [map, setMap] = useState<VenueMap | undefined>();
348
861
 
349
- // Carga asíncrona desde API
350
862
  useEffect(() => {
351
863
  fetch('/api/maps/1')
352
864
  .then(r => r.json())
353
865
  .then(setMap);
354
866
  }, []);
355
867
 
356
- // Guarda automáticamente en cada cambio
357
868
  const handleChange = (updated: VenueMap) => {
358
869
  setMap(updated);
359
870
  fetch('/api/maps/1', {
@@ -372,84 +883,59 @@ function App() {
372
883
  }
373
884
  ```
374
885
 
375
- ---
376
-
377
886
  ### Props
378
887
 
379
888
  | Prop | Tipo | Default | Descripción |
380
889
  |------|------|---------|-------------|
381
- | `initialMap` | `VenueMap` | mapa vacío | Mapa inicial. Se puede actualizar desde fuera para recargar el editor. |
382
- | `onChange` | `(map: VenueMap) => void` | — | Se llama en cada cambio del estado interno. |
383
- | `domainConfigs` | `DomainConfig[]` | `[]` | Array de catálogos de tipos predefinidos. Cada uno aparece como una pestaña separada en la paleta. |
384
- | `domainConfig` | `DomainConfig` | — | **Obsoleto** — usa `domainConfigs`. Catálogo único (se convierte internamente a un array de un elemento). |
385
- | `libraryStorageKey` | `string` | `'venueMapEditor:libraries'` | Clave de `localStorage` donde se persisten las librerías importadas. Pasa `''` para deshabilitar la persistencia. |
386
- | `width` | `string \| number` | `'100%'` | Ancho del componente. |
387
- | `height` | `string \| number` | `'600px'` | Alto del componente. |
388
- | `gridSize` | `number` | `20` | Tamaño de la cuadrícula en unidades de canvas. |
389
- | `showGrid` | `boolean` | `true` | Mostrar/ocultar cuadrícula al iniciar. |
390
- | `snapToGrid` | `boolean` | `false` | Activar snap de elementos a la cuadrícula. |
391
- | `readOnly` | `boolean` | `false` | Modo lectura: no se puede editar pero sí hacer pan/zoom. |
392
- | `fixed` | `boolean` | `false` | Igual que `readOnly` pero además oculta la barra de herramientas. Pensado para el viewer en producción. |
393
- | `elementStatus` | `ElementStatus[]` | — | Array de estados visuales por elemento (libre, ocupado, reservado, deshabilitado). |
394
- | `onElementClick` | `(el: MapElement) => void` | — | Callback genérico al hacer click en cualquier elemento (en modo viewer). |
395
- | `onElementTypeClick` | `Record<string, (el: MapElement) => void>` | — | Callbacks por tipo de elemento. El tipo específico tiene prioridad sobre `onElementClick`. |
396
-
397
- ---
890
+ | `initialMap` | `VenueMap` | mapa vacío | Mapa inicial |
891
+ | `onChange` | `(map: VenueMap) => void` | — | Callback en cada cambio |
892
+ | `domainConfigs` | `DomainConfig[]` | `[]` | Catálogos de tipos predefinidos |
893
+ | `domainConfig` | `DomainConfig` | — | **Obsoleto** — usa `domainConfigs` |
894
+ | `libraryStorageKey` | `string` | `'venueMapEditor:libraries'` | Clave de localStorage |
895
+ | `width` | `string \| number` | `'100%'` | Ancho |
896
+ | `height` | `string \| number` | `'600px'` | Alto |
897
+ | `gridSize` | `number` | `20` | Tamaño de cuadrícula |
898
+ | `showGrid` | `boolean` | `true` | Mostrar cuadrícula |
899
+ | `snapToGrid` | `boolean` | `false` | Snap a cuadrícula |
900
+ | `readOnly` | `boolean` | `false` | Modo lectura (edición deshabilitada) |
901
+ | `fixed` | `boolean` | `false` | Modo lectura + oculta toolbar |
902
+ | `elementStatus` | `ElementStatus[]` | — | Estados visuales por elemento |
903
+ | `onElementClick` | `(el: MapElement) => void` | — | Click genérico |
904
+ | `onElementTypeClick` | `Record<string, (el: MapElement) => void>` | — | Click por tipo |
398
905
 
399
906
  ### Modo Viewer
400
907
 
401
- `VenueMapViewer` es un alias de `VenueMapEditor` con `fixed={true}`. Úsalo para mostrar el mapa en producción con elementos interactivos:
908
+ `VenueMapViewer` es un alias de `VenueMapEditor` con `fixed={true}`:
402
909
 
403
910
  ```tsx
404
- import { VenueMapViewer } from 'neogestify-ui-components/VenueMapEditor';
405
- import type { ElementStatus } from 'neogestify-ui-components/VenueMapEditor';
911
+ import { VenueMapViewer } from 'neogestify-ui-components';
912
+ import type { ElementStatus } from 'neogestify-ui-components';
406
913
 
407
914
  const estados: ElementStatus[] = [
408
915
  { elementId: 'mesa-1', status: 'occupied' },
409
916
  { elementId: 'mesa-2', status: 'free' },
410
917
  { elementId: 'mesa-3', status: 'reserved' },
411
- { elementId: 'spot-4', status: 'disabled' },
412
918
  ];
413
919
 
414
920
  <VenueMapViewer
415
921
  initialMap={myMap}
416
922
  elementStatus={estados}
417
923
  onElementTypeClick={{
418
- // El key es el `id` del tipo definido en la librería JSON
419
924
  TABLE_ROUND: (el) => abrirReserva(el.id),
420
- TABLE_RECT: (el) => abrirReserva(el.id),
421
- PARKING_SPOT:(el) => asignarEspacio(el.id),
925
+ TABLE_RECT: (el) => abrirReserva(el.id),
422
926
  }}
423
- // Fallback para tipos sin handler específico
424
- onElementClick={(el) => console.log('click en', el.type, el.id)}
425
927
  />
426
928
  ```
427
929
 
428
- **Colores de estado:**
429
-
430
- | `status` | Color |
431
- |----------|-------|
432
- | `free` | Verde claro |
433
- | `occupied` | Rojo claro |
434
- | `reserved` | Amarillo |
435
- | `disabled` | Gris |
436
-
437
- ---
438
-
439
- ### Múltiples catálogos de elementos (domainConfigs)
440
-
441
- Pasa varios `DomainConfig` vía la prop `domainConfigs`. Cada catálogo aparece como una **pestaña separada** en la paleta — los tipos nunca se mezclan entre tabs.
930
+ ### Múltiples catálogos (domainConfigs)
442
931
 
443
932
  ```tsx
444
- import { VenueMapEditor } from 'neogestify-ui-components/VenueMapEditor';
445
- import type { DomainConfig } from 'neogestify-ui-components/VenueMapEditor';
446
-
447
933
  const mobiliario: DomainConfig = {
448
934
  id: 'furniture',
449
935
  name: 'Mobiliario',
450
936
  elementTypes: [
451
- { id: 'CHAIR', label: 'Silla', shape: 'circle', defaultWidth: 30, defaultHeight: 30, color: '#fef3c7', strokeColor: '#d97706' },
452
- { id: 'TABLE_RECT', label: 'Mesa rect.', shape: 'rect', defaultWidth: 100, defaultHeight: 60, color: '#fef3c7', strokeColor: '#d97706' },
937
+ { id: 'CHAIR', label: 'Silla', shape: 'circle', defaultWidth: 30, defaultHeight: 30, color: '#fef3c7', strokeColor: '#d97706' },
938
+ { id: 'TABLE_RECT', label: 'Mesa rect.', shape: 'rect', defaultWidth: 100, defaultHeight: 60, color: '#fef3c7', strokeColor: '#d97706' },
453
939
  ],
454
940
  };
455
941
 
@@ -457,41 +943,14 @@ const iluminacion: DomainConfig = {
457
943
  id: 'lighting',
458
944
  name: 'Iluminación',
459
945
  elementTypes: [
460
- { id: 'SPOT_LIGHT', label: 'Foco', shape: 'circle', defaultWidth: 40, defaultHeight: 40, color: '#fef9c3', strokeColor: '#ca8a04' },
461
- { id: 'STRIP_LIGHT', label: 'Tira LED', shape: 'rect', defaultWidth: 120, defaultHeight: 15, color: '#fef9c3', strokeColor: '#ca8a04' },
946
+ { id: 'SPOT_LIGHT', label: 'Foco', shape: 'circle', defaultWidth: 40, defaultHeight: 40, color: '#fef9c3', strokeColor: '#ca8a04' },
462
947
  ],
463
948
  };
464
949
 
465
950
  <VenueMapEditor domainConfigs={[mobiliario, iluminacion]} />
466
951
  ```
467
952
 
468
- La paleta mostrará:
469
-
470
- ```
471
- [ Mobiliario ] [ Iluminación ]
472
- ─────────────────────────────
473
- [Silla] [Mesa rect.]
474
- ```
475
-
476
- ---
477
-
478
- ### Crear una librería de elementos (JSON)
479
-
480
- Los elementos que aparecen en la paleta también se pueden definir en archivos JSON que el usuario carga desde el botón **⊞** (Cargar librería).
481
-
482
- **Persistencia automática:** las librerías importadas se guardan en `localStorage` bajo la clave `libraryStorageKey` (por defecto `'venueMapEditor:libraries'`). Al recargar la página se restauran automáticamente **antes** de que el mapa renderice, evitando errores de "tipo de elemento desconocido".
483
-
484
- **Merge inteligente al importar:** si un grupo con el mismo `id` ya existe, se añaden únicamente los elementos cuyo `id` no esté duplicado. Los elementos existentes nunca se sobreescriben.
485
-
486
- ```tsx
487
- // Cambiar la clave de almacenamiento (útil con múltiples editores en la misma app)
488
- <VenueMapEditor libraryStorageKey="mi-proyecto:libs" />
489
-
490
- // Deshabilitar persistencia
491
- <VenueMapEditor libraryStorageKey="" />
492
- ```
493
-
494
- #### Formato del JSON
953
+ ### Formato JSON de librería
495
954
 
496
955
  ```json
497
956
  {
@@ -517,96 +976,93 @@ Los elementos que aparecen en la paleta también se pueden definir en archivos J
517
976
  "strokeColor": "#d97706"
518
977
  }
519
978
  ]
520
- },
521
- "infraestructura": {
522
- "name": "Infraestructura",
523
- "objects": [
524
- {
525
- "id": "PILLAR",
526
- "label": "Columna",
527
- "shape": "circle",
528
- "defaultWidth": 25,
529
- "defaultHeight": 25,
530
- "color": "#e5e7eb",
531
- "strokeColor": "#6b7280"
532
- },
533
- {
534
- "id": "ENTRANCE",
535
- "label": "Entrada",
536
- "shape": "arrow",
537
- "defaultWidth": 80,
538
- "defaultHeight": 30,
539
- "color": "#dcfce7",
540
- "strokeColor": "#16a34a"
541
- }
542
- ]
543
979
  }
544
980
  }
545
981
  ```
546
982
 
547
- #### Formas personalizadas SVG (`shape: "path"`)
983
+ ### Formas personalizadas
548
984
 
549
- Ahora puedes definir cualquier figura SVG usando un path:
985
+ | `shape` | Descripción |
986
+ |---------|-------------|
987
+ | `rect` | Rectángulo |
988
+ | `circle` | Elipse/círculo |
989
+ | `arrow` | Flecha |
990
+ | `path` | SVG path personalizado |
991
+ | `svg` | SVG completo inline |
550
992
 
993
+ **Shape `path`:**
551
994
  ```json
552
995
  {
553
- "mi_libreria": {
554
- "name": "Mi librería",
555
- "objects": [
556
- {
557
- "id": "STAR",
558
- "label": "Estrella",
559
- "shape": "path",
560
- "svgPath": "M50 5 L61 35 L95 35 L68 57 L79 91 L50 70 L21 91 L32 57 L5 35 L39 35 Z",
561
- "viewBox": "0 0 100 100",
562
- "defaultWidth": 60,
563
- "defaultHeight": 60,
564
- "color": "#facc15",
565
- "strokeColor": "#ca8a04"
566
- }
567
- ]
568
- }
996
+ "id": "STAR",
997
+ "label": "Estrella",
998
+ "shape": "path",
999
+ "svgPath": "M50 5 L61 35 ...",
1000
+ "viewBox": "0 0 100 100",
1001
+ "defaultWidth": 60,
1002
+ "defaultHeight": 60,
1003
+ "color": "#facc15",
1004
+ "strokeColor": "#ca8a04"
569
1005
  }
570
1006
  ```
571
1007
 
572
- **Propiedades para `shape: "path"`:**
1008
+ **Shape `svg`:**
1009
+ ```json
1010
+ {
1011
+ "id": "CAR",
1012
+ "label": "Carro",
1013
+ "shape": "svg",
1014
+ "svgMarkup": "<svg viewBox=\"0 0 100 100\"><rect .../></svg>",
1015
+ "defaultWidth": 80,
1016
+ "defaultHeight": 80,
1017
+ "color": "#3b82f6",
1018
+ "strokeColor": "#1e40af"
1019
+ }
1020
+ ```
1021
+
1022
+ ### Colores de estado
573
1023
 
574
- | Campo | Tipo | Default | Descripción |
575
- |-------|------|---------|-------------|
576
- | `svgPath` | `string` | **requerido** | El atributo `d` del elemento `<path>` SVG |
577
- | `viewBox` | `string` | `"0 0 100 100"` | Espacio de coordenadas del path (formato: `"minX minY width height"`) |
1024
+ | `status` | Color |
1025
+ |----------|-------|
1026
+ | `free` | Verde claro |
1027
+ | `occupied` | Rojo claro |
1028
+ | `reserved` | Amarillo |
1029
+ | `disabled` | Gris |
578
1030
 
579
- > **Nota:** El path se escala automáticamente para llenar el bounding box `width × height` del elemento. El `strokeWidth` se compensa por el factor de escala para que sea visualmente consistente con los otros shapes.
1031
+ ### Persistencia de librerías
580
1032
 
581
- #### Propiedades de cada objeto
1033
+ Las librerías importadas se guardan en `localStorage` bajo la clave `libraryStorageKey` (por defecto `'venueMapEditor:libraries'`). Al recargar la página se restauran automáticamente.
1034
+
1035
+ **Merge inteligente al importar:** si un grupo con el mismo `id` ya existe, se añaden únicamente los elementos cuyo `id` no esté duplicado. Los elementos existentes nunca se sobrescriben.
1036
+
1037
+ ```tsx
1038
+ // Cambiar la clave de almacenamiento (útil con múltiples editores)
1039
+ <VenueMapEditor libraryStorageKey="mi-proyecto:libs" />
1040
+
1041
+ // Deshabilitar persistencia
1042
+ <VenueMapEditor libraryStorageKey="" />
1043
+ ```
1044
+
1045
+ ### Propiedades de cada objeto
582
1046
 
583
1047
  | Campo | Tipo | Requerido | Descripción |
584
1048
  |-------|------|-----------|-------------|
585
- | `id` | `string` | ✓ | Identificador único del tipo. Se usa como key en `onElementTypeClick`. |
586
- | `label` | `string` | ✓ | Nombre visible en la paleta y en el canvas. |
587
- | `shape` | `"rect" \| "circle" \| "arrow" \| "path" \| "svg"` | ✓ | Forma del objeto. |
588
- | `defaultWidth` | `number` | ✓ | Ancho inicial al colocar el elemento (unidades de canvas ≈ px a zoom 1×). |
589
- | `defaultHeight` | `number` | ✓ | Alto inicial. |
590
- | `color` | `string` | ✓ | Color de relleno (cualquier valor CSS: `#hex`, `rgb()`, `hsl()`, etc.). |
591
- | `strokeColor` | `string` | ✓ | Color del borde. |
592
- | `svgPath` | `string` | solo para `shape:"path"` | Atributo `d` de un `<path>` SVG. Se escala automáticamente al bounding box del elemento. |
593
- | `svgMarkup` | `string` | solo para `shape:"svg"` | Markup SVG completo `<svg>...</svg>`. Se extrae el contenido interno y se escala al bounding box del elemento. Debe incluir `viewBox`. |
594
- | `viewBox` | `string` | — | Espacio de coordenadas del `svgPath`. Formato: `"minX minY w h"`. Default: `"0 0 100 100"`. |
595
- | `fillRule` | `"nonzero" \| "evenodd"` | — | Regla de relleno SVG. Usa `"evenodd"` para crear huecos con sub-paths (engranajes, letras, donuts). Default: `"nonzero"`. |
596
-
597
- #### Formas disponibles
598
-
599
- | `shape` | Descripción | Caso de uso típico |
600
- |---------|-------------|-------------------|
601
- | `rect` | Rectángulo | Mesas, espacios de parqueo, habitaciones |
602
- | `circle` | Elipse (círculo si `width === height`) | Mesas redondas, columnas, plantas |
603
- | `arrow` | Flecha apuntando a la derecha | Entradas, salidas, sentidos de circulación |
604
- | `path` | Forma SVG personalizada libre | Cualquier figura: estrella, engranaje, piano, logo... |
605
- | `svg` | SVG completo inline | Cualquier SVG con múltiples elementos, gradientes, etc. |
606
-
607
- #### Formas personalizadas con `shape: "path"`
608
-
609
- El campo `svgPath` acepta el atributo `d` de cualquier `<path>` SVG estándar. El sistema escala la figura para que ocupe exactamente el bounding box `width × height` del elemento. Puedes diseñar tus formas con Inkscape, Figma u otro editor vectorial y copiar el `d=` directamente.
1049
+ | `id` | `string` | ✓ | Identificador único del tipo |
1050
+ | `label` | `string` | ✓ | Nombre visible en la paleta |
1051
+ | `shape` | `"rect" \| "circle" \| "arrow" \| "path" \| "svg"` | ✓ | Forma del objeto |
1052
+ | `defaultWidth` | `number` | ✓ | Ancho inicial (unidades de canvas) |
1053
+ | `defaultHeight` | `number` | ✓ | Alto inicial |
1054
+ | `color` | `string` | ✓ | Color de relleno (#hex, rgb(), hsl()) |
1055
+ | `strokeColor` | `string` | ✓ | Color del borde |
1056
+ | `svgPath` | `string` | solo para `shape:"path"` | Atributo `d` del path SVG |
1057
+ | `svgMarkup` | `string` | solo para `shape:"svg"` | Markup SVG completo |
1058
+ | `viewBox` | `string` | — | Espacio de coordenadas del path |
1059
+ | `fillRule` | `"nonzero" \| "evenodd"` | — | Regla de relleno SVG |
1060
+
1061
+ > **Hitbox de piso:** para formas personalizadas que no llenan su bounding box (estrellas, logos), la detección de bordes usa un cuadrado de lado `min(width, height)` centrado en el elemento.
1062
+
1063
+ ### Shape `path` detallado
1064
+
1065
+ El campo `svgPath` acepta el atributo `d` de cualquier `<path>` SVG estándar. El sistema escala la figura para que ocupe exactamente el bounding box `width × height`.
610
1066
 
611
1067
  ```json
612
1068
  {
@@ -630,7 +1086,7 @@ El campo `svgPath` acepta el atributo `d` de cualquier `<path>` SVG estándar. E
630
1086
  "shape": "path",
631
1087
  "viewBox": "0 0 100 100",
632
1088
  "fillRule": "evenodd",
633
- "svgPath": "M36.61,17.66 L44.13,5.39 L55.87,5.39 L63.39,17.66 A35,35 0 0,1 77.40,14.30 L85.70,22.60 L82.34,36.61 A35,35 0 0,1 94.61,44.13 L94.61,55.87 L82.34,63.39 A35,35 0 0,1 85.70,77.40 L77.40,85.70 L63.39,82.34 A35,35 0 0,1 55.87,94.61 L44.13,94.61 L36.61,82.34 A35,35 0 0,1 22.60,85.70 L14.30,77.40 L17.66,63.39 A35,35 0 0,1 5.39,55.87 L5.39,44.13 L17.66,36.61 A35,35 0 0,1 14.30,22.60 L22.60,14.30 Z M65,50 A15,15 0 1,0 35,50 A15,15 0 1,0 65,50 Z",
1089
+ "svgPath": "M36.61,17.66 ...",
634
1090
  "defaultWidth": 70,
635
1091
  "defaultHeight": 70,
636
1092
  "color": "#94a3b8",
@@ -641,11 +1097,9 @@ El campo `svgPath` acepta el atributo `d` de cualquier `<path>` SVG estándar. E
641
1097
  }
642
1098
  ```
643
1099
 
644
- > **Hitbox de piso:** para formas personalizadas que no llenan su bounding box (estrellas, logos, etc.), la detección de bordes usa un cuadrado de lado `min(width, height)` centrado en el elemento — esto evita que la figura quede demasiado restringida al área del piso.
645
-
646
- #### Formas personalizadas con `shape: "svg"`
1100
+ ### Shape `svg` detallado
647
1101
 
648
- El campo `svgMarkup` acepta un **SVG completo** como string. El sistema extrae el `viewBox` del tag `<svg>` y renderiza los elementos internos escalados al bounding box del elemento. Esto permite usar figuras con múltiples paths, círculos, rectángulos, textos, etc.
1102
+ El campo `svgMarkup` acepta un **SVG completo** como string. El sistema extrae el `viewBox` del tag `<svg>` y renderiza los elementos internos escalados.
649
1103
 
650
1104
  > **Seguridad:** el markup se sanitiza automáticamente eliminando `<script>`, `on*` event handlers, `javascript:` URIs y tags peligrosos.
651
1105
 
@@ -658,37 +1112,20 @@ El campo `svgMarkup` acepta un **SVG completo** como string. El sistema extrae e
658
1112
  "id": "CAR",
659
1113
  "label": "Carro",
660
1114
  "shape": "svg",
661
- "svgMarkup": "<svg viewBox=\"0 0 100 100\"><rect x=\"10\" y=\"40\" width=\"80\" height=\"35\" rx=\"5\" fill=\"currentColor\"/><rect x=\"5\" y=\"50\" width=\"90\" height=\"20\" rx=\"3\" fill=\"currentColor\"/><circle cx=\"28\" cy=\"75\" r=\"9\" fill=\"currentColor\"/><circle cx=\"72\" cy=\"75\" r=\"9\" fill=\"currentColor\"/><rect x=\"25\" y=\"44\" width=\"20\" height=\"12\" rx=\"2\" fill=\"white\" opacity=\"0.4\"/><rect x=\"55\" y=\"44\" width=\"20\" height=\"12\" rx=\"2\" fill=\"white\" opacity=\"0.4\"/></svg>",
1115
+ "svgMarkup": "<svg viewBox=\"0 0 100 100\"><rect x=\"10\" y=\"40\" width=\"80\" height=\"35\" rx=\"5\" fill=\"currentColor\"/><circle cx=\"28\" cy=\"75\" r=\"9\" fill=\"currentColor\"/></svg>",
662
1116
  "defaultWidth": 80,
663
1117
  "defaultHeight": 80,
664
1118
  "color": "#3b82f6",
665
1119
  "strokeColor": "#1e40af"
666
- },
667
- {
668
- "id": "PEOPLE",
669
- "label": "Persona",
670
- "shape": "svg",
671
- "svgMarkup": "<svg viewBox=\"0 0 100 100\"><circle cx=\"50\" cy=\"25\" r=\"15\"/><path d=\"M30 90 L30 50 Q30 40 40 40 L60 40 Q70 40 70 50 L70 90 M20 60 L80 60\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"6\" stroke-linecap=\"round\"/></svg>",
672
- "defaultWidth": 50,
673
- "defaultHeight": 50,
674
- "color": "#f97316",
675
- "strokeColor": "#c2410c"
676
1120
  }
677
1121
  ]
678
1122
  }
679
1123
  }
680
1124
  ```
681
1125
 
682
- **Notas sobre `shape: "svg"`:**
683
-
684
- - El string `svgMarkup` **debe** ser un `<svg>` válido con atributo `viewBox`.
685
- - Los atributos `color` y `strokeColor` del `ElementTypeDef` se aplican como `fill` y `stroke` en el `<g>` contenedor. Usa `currentColor` en tu SVG para heredar el color.
686
- - El `viewBox` se extrae automáticamente del `<svg>` — no necesitas especificarlo por separado.
687
- - Funciona con cualquier combinación de elementos SVG internos: `<path>`, `<circle>`, `<rect>`, `<g>`, `<line>`, `<polygon>`, etc.
1126
+ ### Varios grupos en un archivo JSON
688
1127
 
689
- #### Varios grupos en un archivo
690
-
691
- Un mismo archivo puede tener tantos grupos como necesites. Cada grupo aparece como una **pestaña separada** en la paleta. Se pueden cargar múltiples archivos — los grupos se acumulan. Cada grupo importado muestra un botón **×** en su pestaña para eliminarlo.
1128
+ Un mismo archivo puede tener tantos grupos como necesites. Cada grupo aparece como una **pestaña separada** en la paleta.
692
1129
 
693
1130
  ```json
694
1131
  {
@@ -698,7 +1135,7 @@ Un mismo archivo puede tener tantos grupos como necesites. Cada grupo aparece co
698
1135
  }
699
1136
  ```
700
1137
 
701
- #### Librería de ejemplo — Parqueadero
1138
+ ### Librería de ejemplo — Parqueadero
702
1139
 
703
1140
  ```json
704
1141
  {
@@ -722,11 +1159,7 @@ Un mismo archivo puede tener tantos grupos como necesites. Cada grupo aparece co
722
1159
  }
723
1160
  ```
724
1161
 
725
- ---
726
-
727
- ### Modelo de datos TypeScript
728
-
729
- El estado del editor se serializa en un objeto `VenueMap`. Puedes guardarlo en tu base de datos como JSON y restaurarlo con `initialMap`.
1162
+ ### Modelo de datos completo
730
1163
 
731
1164
  ```
732
1165
  VenueMap
@@ -752,19 +1185,15 @@ VenueMap
752
1185
  └── metadata?: Record<string, unknown> ← datos propios de tu app
753
1186
  ```
754
1187
 
755
- El campo `metadata` en `MapElement` está disponible para que cada app guarde datos propios por elemento (ej. ID de reserva, capacidad, propietario, estado personalizado).
1188
+ El campo `metadata` está disponible para que cada app guarde datos propios por elemento (ej. ID de reserva, capacidad, propietario).
756
1189
 
757
1190
  ```tsx
758
- // Ejemplo: guardar datos de negocio en metadata al crear elementos
759
1191
  const handleClick = (el: MapElement) => {
760
- // El metadata lo pone tu app, no el editor
761
1192
  const reservaId = el.metadata?.reservaId as string;
762
1193
  abrirModal(reservaId);
763
1194
  };
764
1195
  ```
765
1196
 
766
- ---
767
-
768
1197
  ### Herramientas del editor
769
1198
 
770
1199
  | Tecla | Herramienta | Función |
@@ -782,8 +1211,6 @@ const handleClick = (el: MapElement) => {
782
1211
  | Rueda ratón | — | Zoom centrado en el cursor. |
783
1212
  | Click medio + drag | — | Pan del canvas en cualquier modo. |
784
1213
 
785
- ---
786
-
787
1214
  ### Gestión de plantas
788
1215
 
789
1216
  La barra de pestañas (visible incluso en viewer) permite:
@@ -794,8 +1221,6 @@ La barra de pestañas (visible incluso en viewer) permite:
794
1221
  - **×** → eliminar la planta (mínimo 1)
795
1222
  - **+** → añadir nueva planta
796
1223
 
797
- ---
798
-
799
1224
  ### Forma del piso (Rect vs Polígono)
800
1225
 
801
1226
  El botón **Rect / Poly** de la barra de herramientas alterna entre:
@@ -805,15 +1230,40 @@ El botón **Rect / Poly** de la barra de herramientas alterna entre:
805
1230
 
806
1231
  Los elementos y paredes siempre se mantienen dentro del piso al moverlos o colocarlos.
807
1232
 
808
- ---
809
-
810
1233
  ### Exportar / Importar el mapa
811
1234
 
812
1235
  | Botón | Función |
813
1236
  |-------|---------|
814
1237
  | ⬇ Exportar mapa | Descarga el estado actual como `.json` (incluye las librerías embebidas para portabilidad). |
815
1238
  | ⬆ Importar mapa | Carga un `.json` exportado previamente, reemplazando el mapa actual. |
816
- | ⊞ Cargar librería | Carga un `.json` de elementos. Los grupos se añaden a la paleta como nuevas pestañas. Si el grupo ya existe, sólo se añaden los objetos con `id` nuevo (sin sobrescribir). La librería se persiste automáticamente en `localStorage`. |
1239
+ | ⊞ Cargar librería | Carga un `.json` de elementos. Los grupos se añaden a la paleta como nuevas pestañas. Si el grupo ya existe, sólo se añaden los objetos con `id` nuevo. La librería se persiste automáticamente en `localStorage`. |
1240
+
1241
+ ---
1242
+
1243
+ ## ElementLibraryBuilder
1244
+
1245
+ Interfaz visual para crear librerías de elementos JSON para el VenueMapEditor:
1246
+
1247
+ ```tsx
1248
+ import { ElementLibraryBuilder } from 'neogestify-ui-components';
1249
+
1250
+ function App() {
1251
+ return (
1252
+ <div style={{ height: '800px' }}>
1253
+ <ElementLibraryBuilder />
1254
+ </div>
1255
+ );
1256
+ }
1257
+ ```
1258
+
1259
+ Características:
1260
+ - Crear/renombrar/eliminar grupos de elementos
1261
+ - Añadir/editar/eliminar elementos
1262
+ - Configurar forma, tamaño, colores
1263
+ - Soporte para shapes: rect, circle, arrow, path, svg
1264
+ - Vista previa del JSON generado
1265
+ - Descargar como archivo .json
1266
+ - Copiar al portapapeles
817
1267
 
818
1268
  ---
819
1269
 
@@ -842,12 +1292,16 @@ bun run build
842
1292
  ui-components/
843
1293
  ├── src/
844
1294
  │ ├── components/
845
- │ │ ├── html/ # Componentes HTML
846
- │ │ ├── icons/ # Iconos SVG
847
- │ │ └── alerts/ # Alertas SweetAlert2
848
- └── types/ # Tipos TypeScript
849
- ├── showcase/ # Demo/Showcase
850
- └── dist/ # Build output
1295
+ │ │ ├── html/ # Componentes HTML
1296
+ │ │ ├── icons/ # Iconos SVG
1297
+ │ │ ├── alerts/ # Alertas SweetAlert2
1298
+ │ ├── VenueMapEditor/ # Editor de mapas
1299
+ │ │ └── ElementLibraryBuilder/ # Constructor de librerías
1300
+ │ ├── context/
1301
+ │ │ └── theme/ # Sistema de tema
1302
+ │ └── types/ # Tipos TypeScript
1303
+ ├── showcase/ # Demo/Showcase
1304
+ └── dist/ # Build output
851
1305
  ```
852
1306
 
853
1307
  ## Modo Oscuro
@@ -857,18 +1311,18 @@ Los componentes soportan modo oscuro automáticamente usando las clases `dark:`
857
1311
  ```js
858
1312
  // tailwind.config.js
859
1313
  export default {
860
- darkMode: 'class', // o 'media'
861
- // ...
1314
+ darkMode: 'class',
862
1315
  }
863
1316
  ```
864
1317
 
865
1318
  Para activar el modo oscuro:
866
1319
 
867
1320
  ```tsx
868
- // Agregar/quitar la clase 'dark' en el html
869
1321
  document.documentElement.classList.add('dark');
870
1322
  ```
871
1323
 
1324
+ O usa el sistema de tema de la librería (ThemeProvider + ThemeToggle).
1325
+
872
1326
  ## Licencia
873
1327
 
874
- MIT
1328
+ MIT