neogestify-ui-components 2.2.2 → 2.3.1
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/LICENSE +21 -0
- package/README.md +803 -349
- package/dist/components/ElementLibraryBuilder/index.js +299 -120
- package/dist/components/ElementLibraryBuilder/index.js.map +1 -1
- package/dist/components/ElementLibraryBuilder/index.mjs +300 -121
- package/dist/components/ElementLibraryBuilder/index.mjs.map +1 -1
- package/dist/components/VenueMapEditor/index.js.map +1 -1
- package/dist/components/VenueMapEditor/index.mjs.map +1 -1
- package/dist/components/alerts/index.js +108 -51
- package/dist/components/alerts/index.js.map +1 -1
- package/dist/components/alerts/index.mjs +109 -52
- package/dist/components/alerts/index.mjs.map +1 -1
- package/dist/components/html/index.d.mts +101 -22
- package/dist/components/html/index.d.ts +101 -22
- package/dist/components/html/index.js +506 -166
- package/dist/components/html/index.js.map +1 -1
- package/dist/components/html/index.mjs +507 -167
- package/dist/components/html/index.mjs.map +1 -1
- package/dist/components/icons/index.d.mts +5 -1
- package/dist/components/icons/index.d.ts +5 -1
- package/dist/components/icons/index.js +16 -0
- package/dist/components/icons/index.js.map +1 -1
- package/dist/components/icons/index.mjs +13 -1
- package/dist/components/icons/index.mjs.map +1 -1
- package/dist/context/theme/index.js +59 -37
- package/dist/context/theme/index.js.map +1 -1
- package/dist/context/theme/index.mjs +59 -37
- package/dist/context/theme/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +510 -166
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +508 -168
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/html/Button.tsx +84 -38
- package/src/components/html/Form.tsx +33 -13
- package/src/components/html/Input.tsx +110 -31
- package/src/components/html/Loading.tsx +58 -33
- package/src/components/html/Modal.tsx +67 -20
- package/src/components/html/Select.tsx +92 -39
- package/src/components/html/Table.tsx +274 -50
- package/src/components/html/TextArea.tsx +81 -31
- 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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
72
|
+
Importa todo desde un solo punto:
|
|
78
73
|
|
|
79
|
-
|
|
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
|
-
|
|
116
|
+
<Button variant="primary" size="lg" isLoading loadingText="Guardando...">
|
|
117
|
+
Guardar
|
|
118
|
+
</Button>
|
|
83
119
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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={
|
|
677
|
+
<Button variant="danger" onClick={handleAdvertencia}>
|
|
164
678
|
Eliminar
|
|
165
679
|
</Button>
|
|
166
680
|
);
|
|
167
681
|
}
|
|
168
682
|
```
|
|
169
683
|
|
|
170
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
776
|
+
### 2. Usar el ThemeToggle
|
|
192
777
|
|
|
193
778
|
```tsx
|
|
194
|
-
import { ThemeToggle } from 'neogestify-ui-components
|
|
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
|
-
|
|
790
|
+
### 3. Usar el hook useTheme
|
|
206
791
|
|
|
207
792
|
```tsx
|
|
208
|
-
import { useTheme } from 'neogestify-ui-components
|
|
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,
|
|
304
|
-
VenueMapViewer,
|
|
305
|
-
} from 'neogestify-ui-components
|
|
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
|
|
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
|
|
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
|
|
344
|
-
import type { VenueMap } from 'neogestify-ui-components
|
|
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
|
|
382
|
-
| `onChange` | `(map: VenueMap) => void` | — |
|
|
383
|
-
| `domainConfigs` | `DomainConfig[]` | `[]` |
|
|
384
|
-
| `domainConfig` | `DomainConfig` | — | **Obsoleto** — usa `domainConfigs
|
|
385
|
-
| `libraryStorageKey` | `string` | `'venueMapEditor:libraries'` | Clave de
|
|
386
|
-
| `width` | `string \| number` | `'100%'` | Ancho
|
|
387
|
-
| `height` | `string \| number` | `'600px'` | Alto
|
|
388
|
-
| `gridSize` | `number` | `20` | Tamaño de
|
|
389
|
-
| `showGrid` | `boolean` | `true` | Mostrar
|
|
390
|
-
| `snapToGrid` | `boolean` | `false` |
|
|
391
|
-
| `readOnly` | `boolean` | `false` | Modo lectura
|
|
392
|
-
| `fixed` | `boolean` | `false` |
|
|
393
|
-
| `elementStatus` | `ElementStatus[]` | — |
|
|
394
|
-
| `onElementClick` | `(el: MapElement) => void` | — |
|
|
395
|
-
| `onElementTypeClick` | `Record<string, (el: MapElement) => void>` | — |
|
|
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}
|
|
908
|
+
`VenueMapViewer` es un alias de `VenueMapEditor` con `fixed={true}`:
|
|
402
909
|
|
|
403
910
|
```tsx
|
|
404
|
-
import { VenueMapViewer } from 'neogestify-ui-components
|
|
405
|
-
import type { ElementStatus } from 'neogestify-ui-components
|
|
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:
|
|
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
|
-
|
|
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',
|
|
452
|
-
{ id: 'TABLE_RECT', label: 'Mesa rect.',
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
983
|
+
### Formas personalizadas
|
|
548
984
|
|
|
549
|
-
|
|
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
|
-
"
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
|
575
|
-
|
|
576
|
-
| `
|
|
577
|
-
| `
|
|
1024
|
+
| `status` | Color |
|
|
1025
|
+
|----------|-------|
|
|
1026
|
+
| `free` | Verde claro |
|
|
1027
|
+
| `occupied` | Rojo claro |
|
|
1028
|
+
| `reserved` | Amarillo |
|
|
1029
|
+
| `disabled` | Gris |
|
|
578
1030
|
|
|
579
|
-
|
|
1031
|
+
### Persistencia de librerías
|
|
580
1032
|
|
|
581
|
-
|
|
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
|
|
586
|
-
| `label` | `string` | ✓ | Nombre visible en la paleta
|
|
587
|
-
| `shape` | `"rect" \| "circle" \| "arrow" \| "path" \| "svg"` | ✓ | Forma del objeto
|
|
588
|
-
| `defaultWidth` | `number` | ✓ | Ancho inicial
|
|
589
|
-
| `defaultHeight` | `number` | ✓ | Alto inicial
|
|
590
|
-
| `color` | `string` | ✓ | Color de relleno (
|
|
591
|
-
| `strokeColor` | `string` | ✓ | Color del borde
|
|
592
|
-
| `svgPath` | `string` | solo para `shape:"path"` | Atributo `d`
|
|
593
|
-
| `svgMarkup` | `string` | solo para `shape:"svg"` | Markup SVG completo
|
|
594
|
-
| `viewBox` | `string` | — | Espacio de coordenadas del
|
|
595
|
-
| `fillRule` | `"nonzero" \| "evenodd"` | — | Regla de relleno SVG
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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\"/><
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
|
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/
|
|
846
|
-
│ │ ├── icons/
|
|
847
|
-
│ │
|
|
848
|
-
│
|
|
849
|
-
|
|
850
|
-
|
|
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',
|
|
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
|