neogestify-ui-components 1.2.21 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +352 -2
- package/dist/components/VenueMapEditor/index.d.mts +202 -0
- package/dist/components/VenueMapEditor/index.d.ts +202 -0
- package/dist/components/VenueMapEditor/index.js +2684 -0
- package/dist/components/VenueMapEditor/index.js.map +1 -0
- package/dist/components/VenueMapEditor/index.mjs +2676 -0
- package/dist/components/VenueMapEditor/index.mjs.map +1 -0
- package/dist/components/alerts/index.js.map +1 -1
- package/dist/components/alerts/index.mjs.map +1 -1
- package/dist/components/html/index.d.mts +2 -0
- package/dist/components/html/index.d.ts +2 -0
- package/dist/components/html/index.js +24 -58
- package/dist/components/html/index.js.map +1 -1
- package/dist/components/html/index.mjs +24 -58
- package/dist/components/html/index.mjs.map +1 -1
- package/dist/components/icons/index.d.mts +18 -2
- package/dist/components/icons/index.d.ts +18 -2
- package/dist/components/icons/index.js +97 -11
- package/dist/components/icons/index.js.map +1 -1
- package/dist/components/icons/index.mjs +82 -12
- package/dist/components/icons/index.mjs.map +1 -1
- package/dist/context/theme/index.js.map +1 -1
- package/dist/context/theme/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2734 -69
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2713 -71
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -4
- package/src/components/VenueMapEditor/VenueMapEditor.tsx +851 -0
- package/src/components/VenueMapEditor/VenueMapViewer.tsx +13 -0
- package/src/components/VenueMapEditor/components/Artboard.tsx +405 -0
- package/src/components/VenueMapEditor/components/EditorCanvas.tsx +472 -0
- package/src/components/VenueMapEditor/components/ElementNode.tsx +357 -0
- package/src/components/VenueMapEditor/components/FloorTabs.tsx +137 -0
- package/src/components/VenueMapEditor/components/GridOverlay.tsx +67 -0
- package/src/components/VenueMapEditor/components/PropertiesPanel.tsx +198 -0
- package/src/components/VenueMapEditor/components/Toolbar.tsx +254 -0
- package/src/components/VenueMapEditor/components/WallLayer.tsx +117 -0
- package/src/components/VenueMapEditor/hooks/useDrag.ts +79 -0
- package/src/components/VenueMapEditor/hooks/useHistory.ts +74 -0
- package/src/components/VenueMapEditor/hooks/usePanZoom.ts +114 -0
- package/src/components/VenueMapEditor/hooks/useSelection.ts +42 -0
- package/src/components/VenueMapEditor/index.ts +34 -0
- package/src/components/VenueMapEditor/types.ts +173 -0
- package/src/components/VenueMapEditor/utils/idGen.ts +2 -0
- package/src/components/VenueMapEditor/utils/snapUtils.ts +38 -0
- package/src/components/VenueMapEditor/utils/wallGeometry.ts +83 -0
- package/src/components/html/Input.tsx +48 -80
- package/src/components/icons/icons.tsx +153 -14
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -290,9 +290,359 @@ const modalRef = useRef<ModalRef>(null);
|
|
|
290
290
|
</Modal>
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
-
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## VenueMapEditor
|
|
296
|
+
|
|
297
|
+
Editor de mapas de recintos interactivo basado en SVG puro. Permite diseñar la planta de cualquier espacio (restaurantes, parqueaderos, estadios, oficinas, eventos, etc.) con herramientas de dibujo de paredes, colocación de objetos, múltiples plantas y sistema de librerías de elementos personalizados.
|
|
298
|
+
|
|
299
|
+
### Importación
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
import {
|
|
303
|
+
VenueMapEditor, // editor completo
|
|
304
|
+
VenueMapViewer, // modo solo lectura
|
|
305
|
+
} from 'neogestify-ui-components/VenueMapEditor';
|
|
306
|
+
|
|
307
|
+
// Tipos TypeScript
|
|
308
|
+
import type {
|
|
309
|
+
VenueMap, Floor, MapElement,
|
|
310
|
+
ElementTypeDef, ElementGroup, ElementLibrary,
|
|
311
|
+
ElementStatus, VenueMapEditorProps,
|
|
312
|
+
} from 'neogestify-ui-components/VenueMapEditor';
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### Uso básico
|
|
318
|
+
|
|
319
|
+
El componente funciona sin ninguna prop — crea un mapa vacío con una planta por defecto:
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
<VenueMapEditor />
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Con configuración mínima:
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
<VenueMapEditor
|
|
329
|
+
width="100%"
|
|
330
|
+
height="700px"
|
|
331
|
+
onChange={(map) => console.log('Mapa actualizado:', map)}
|
|
332
|
+
/>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### Cargar y guardar un mapa desde código
|
|
338
|
+
|
|
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.
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
import { useState, useEffect } from 'react';
|
|
343
|
+
import { VenueMapEditor } from 'neogestify-ui-components/VenueMapEditor';
|
|
344
|
+
import type { VenueMap } from 'neogestify-ui-components/VenueMapEditor';
|
|
345
|
+
|
|
346
|
+
function App() {
|
|
347
|
+
const [map, setMap] = useState<VenueMap | undefined>();
|
|
348
|
+
|
|
349
|
+
// Carga asíncrona desde API
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
fetch('/api/maps/1')
|
|
352
|
+
.then(r => r.json())
|
|
353
|
+
.then(setMap);
|
|
354
|
+
}, []);
|
|
355
|
+
|
|
356
|
+
// Guarda automáticamente en cada cambio
|
|
357
|
+
const handleChange = (updated: VenueMap) => {
|
|
358
|
+
setMap(updated);
|
|
359
|
+
fetch('/api/maps/1', {
|
|
360
|
+
method: 'PUT',
|
|
361
|
+
body: JSON.stringify(updated),
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
return (
|
|
366
|
+
<VenueMapEditor
|
|
367
|
+
initialMap={map}
|
|
368
|
+
onChange={handleChange}
|
|
369
|
+
height="600px"
|
|
370
|
+
/>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
### Props
|
|
378
|
+
|
|
379
|
+
| Prop | Tipo | Default | Descripción |
|
|
380
|
+
|------|------|---------|-------------|
|
|
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
|
+
| `domainConfig` | `DomainConfig` | vacío | Tipos de elementos predefinidos disponibles en la paleta (opcional). |
|
|
384
|
+
| `width` | `string \| number` | `'100%'` | Ancho del componente. |
|
|
385
|
+
| `height` | `string \| number` | `'600px'` | Alto del componente. |
|
|
386
|
+
| `gridSize` | `number` | `20` | Tamaño de la cuadrícula en unidades de canvas. |
|
|
387
|
+
| `showGrid` | `boolean` | `true` | Mostrar/ocultar cuadrícula al iniciar. |
|
|
388
|
+
| `snapToGrid` | `boolean` | `false` | Activar snap de elementos a la cuadrícula. |
|
|
389
|
+
| `readOnly` | `boolean` | `false` | Modo lectura: no se puede editar pero sí hacer pan/zoom. |
|
|
390
|
+
| `fixed` | `boolean` | `false` | Igual que `readOnly` pero además oculta la barra de herramientas. Pensado para el viewer en producción. |
|
|
391
|
+
| `elementStatus` | `ElementStatus[]` | — | Array de estados visuales por elemento (libre, ocupado, reservado, deshabilitado). |
|
|
392
|
+
| `onElementClick` | `(el: MapElement) => void` | — | Callback genérico al hacer click en cualquier elemento (en modo viewer). |
|
|
393
|
+
| `onElementTypeClick` | `Record<string, (el: MapElement) => void>` | — | Callbacks por tipo de elemento. El tipo específico tiene prioridad sobre `onElementClick`. |
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
### Modo Viewer
|
|
398
|
+
|
|
399
|
+
`VenueMapViewer` es un alias de `VenueMapEditor` con `fixed={true}`. Úsalo para mostrar el mapa en producción con elementos interactivos:
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
import { VenueMapViewer } from 'neogestify-ui-components/VenueMapEditor';
|
|
403
|
+
import type { ElementStatus } from 'neogestify-ui-components/VenueMapEditor';
|
|
404
|
+
|
|
405
|
+
const estados: ElementStatus[] = [
|
|
406
|
+
{ elementId: 'mesa-1', status: 'occupied' },
|
|
407
|
+
{ elementId: 'mesa-2', status: 'free' },
|
|
408
|
+
{ elementId: 'mesa-3', status: 'reserved' },
|
|
409
|
+
{ elementId: 'spot-4', status: 'disabled' },
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
<VenueMapViewer
|
|
413
|
+
initialMap={myMap}
|
|
414
|
+
elementStatus={estados}
|
|
415
|
+
onElementTypeClick={{
|
|
416
|
+
// El key es el `id` del tipo definido en la librería JSON
|
|
417
|
+
TABLE_ROUND: (el) => abrirReserva(el.id),
|
|
418
|
+
TABLE_RECT: (el) => abrirReserva(el.id),
|
|
419
|
+
PARKING_SPOT:(el) => asignarEspacio(el.id),
|
|
420
|
+
}}
|
|
421
|
+
// Fallback para tipos sin handler específico
|
|
422
|
+
onElementClick={(el) => console.log('click en', el.type, el.id)}
|
|
423
|
+
/>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Colores de estado:**
|
|
427
|
+
|
|
428
|
+
| `status` | Color |
|
|
429
|
+
|----------|-------|
|
|
430
|
+
| `free` | Verde claro |
|
|
431
|
+
| `occupied` | Rojo claro |
|
|
432
|
+
| `reserved` | Amarillo |
|
|
433
|
+
| `disabled` | Gris |
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
### Crear una librería de elementos (JSON)
|
|
438
|
+
|
|
439
|
+
Los elementos que aparecen en la paleta del editor se definen en archivos JSON que el usuario carga desde el botón **⊞** (Cargar librería) de la barra de herramientas. Una vez cargada, la librería queda **embebida dentro del propio mapa** y se exporta junto a él — no se pierde al reabrir el archivo.
|
|
440
|
+
|
|
441
|
+
#### Formato del JSON
|
|
442
|
+
|
|
443
|
+
```json
|
|
444
|
+
{
|
|
445
|
+
"grupoDeMesas": {
|
|
446
|
+
"name": "Mesas de restaurante",
|
|
447
|
+
"objects": [
|
|
448
|
+
{
|
|
449
|
+
"id": "TABLE_ROUND_2",
|
|
450
|
+
"label": "Mesa 2 pers.",
|
|
451
|
+
"shape": "circle",
|
|
452
|
+
"defaultWidth": 60,
|
|
453
|
+
"defaultHeight": 60,
|
|
454
|
+
"color": "#fef3c7",
|
|
455
|
+
"strokeColor": "#d97706"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
"id": "TABLE_RECT_4",
|
|
459
|
+
"label": "Mesa 4 pers.",
|
|
460
|
+
"shape": "rect",
|
|
461
|
+
"defaultWidth": 110,
|
|
462
|
+
"defaultHeight": 70,
|
|
463
|
+
"color": "#fef3c7",
|
|
464
|
+
"strokeColor": "#d97706"
|
|
465
|
+
}
|
|
466
|
+
]
|
|
467
|
+
},
|
|
468
|
+
"infraestructura": {
|
|
469
|
+
"name": "Infraestructura",
|
|
470
|
+
"objects": [
|
|
471
|
+
{
|
|
472
|
+
"id": "PILLAR",
|
|
473
|
+
"label": "Columna",
|
|
474
|
+
"shape": "circle",
|
|
475
|
+
"defaultWidth": 25,
|
|
476
|
+
"defaultHeight": 25,
|
|
477
|
+
"color": "#e5e7eb",
|
|
478
|
+
"strokeColor": "#6b7280"
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
"id": "ENTRANCE",
|
|
482
|
+
"label": "Entrada",
|
|
483
|
+
"shape": "arrow",
|
|
484
|
+
"defaultWidth": 80,
|
|
485
|
+
"defaultHeight": 30,
|
|
486
|
+
"color": "#dcfce7",
|
|
487
|
+
"strokeColor": "#16a34a"
|
|
488
|
+
}
|
|
489
|
+
]
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
#### Propiedades de cada objeto
|
|
495
|
+
|
|
496
|
+
| Campo | Tipo | Descripción |
|
|
497
|
+
|-------|------|-------------|
|
|
498
|
+
| `id` | `string` | Identificador único del tipo. Debe ser **único en toda la librería**. Se usa como key en `onElementTypeClick`. |
|
|
499
|
+
| `label` | `string` | Nombre visible en la paleta. |
|
|
500
|
+
| `shape` | `"rect" \| "circle" \| "arrow"` | Forma del objeto en el canvas. |
|
|
501
|
+
| `defaultWidth` | `number` | Ancho inicial al colocar el elemento (unidades de canvas ≈ píxeles a zoom 1×). |
|
|
502
|
+
| `defaultHeight` | `number` | Alto inicial. |
|
|
503
|
+
| `color` | `string` | Color de relleno (cualquier valor CSS: `#hex`, `rgb()`, `hsl()`, etc.). |
|
|
504
|
+
| `strokeColor` | `string` | Color del borde. |
|
|
505
|
+
|
|
506
|
+
#### Formas disponibles
|
|
507
|
+
|
|
508
|
+
| `shape` | Descripción | Caso de uso típico |
|
|
509
|
+
|---------|-------------|-------------------|
|
|
510
|
+
| `rect` | Rectángulo | Mesas, espacios de parqueo, habitaciones |
|
|
511
|
+
| `circle` | Elipse (círculo si `width === height`) | Mesas redondas, columnas, plantas |
|
|
512
|
+
| `arrow` | Flecha apuntando a la derecha | Entradas, salidas, sentidos de circulación |
|
|
513
|
+
|
|
514
|
+
#### Varios grupos en un archivo
|
|
515
|
+
|
|
516
|
+
Un mismo archivo puede tener tantos grupos como necesites. Cada grupo aparece como una sección separada en la paleta. Se pueden cargar múltiples archivos — los grupos se acumulan. Cada grupo importado muestra un botón **×** para eliminarlo.
|
|
517
|
+
|
|
518
|
+
```json
|
|
519
|
+
{
|
|
520
|
+
"sillas": { "name": "Sillas y asientos", "objects": [ ... ] },
|
|
521
|
+
"servicio": { "name": "Zona de servicio", "objects": [ ... ] },
|
|
522
|
+
"decoracion": { "name": "Decoración", "objects": [ ... ] }
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
#### Librería de ejemplo — Parqueadero
|
|
527
|
+
|
|
528
|
+
```json
|
|
529
|
+
{
|
|
530
|
+
"spots": {
|
|
531
|
+
"name": "Espacios",
|
|
532
|
+
"objects": [
|
|
533
|
+
{ "id": "SPOT", "label": "Normal", "shape": "rect", "defaultWidth": 60, "defaultHeight": 120, "color": "#dbeafe", "strokeColor": "#3b82f6" },
|
|
534
|
+
{ "id": "SPOT_DISCAP", "label": "Discapacidad", "shape": "rect", "defaultWidth": 80, "defaultHeight": 120, "color": "#dcfce7", "strokeColor": "#22c55e" },
|
|
535
|
+
{ "id": "SPOT_EV", "label": "Carga EV", "shape": "rect", "defaultWidth": 65, "defaultHeight": 120, "color": "#d1fae5", "strokeColor": "#059669" },
|
|
536
|
+
{ "id": "SPOT_MOTO", "label": "Moto", "shape": "rect", "defaultWidth": 35, "defaultHeight": 75, "color": "#fef9c3", "strokeColor": "#eab308" }
|
|
537
|
+
]
|
|
538
|
+
},
|
|
539
|
+
"circulacion": {
|
|
540
|
+
"name": "Circulación",
|
|
541
|
+
"objects": [
|
|
542
|
+
{ "id": "ENTRANCE", "label": "Entrada", "shape": "arrow", "defaultWidth": 85, "defaultHeight": 35, "color": "#dcfce7", "strokeColor": "#16a34a" },
|
|
543
|
+
{ "id": "EXIT", "label": "Salida", "shape": "arrow", "defaultWidth": 85, "defaultHeight": 35, "color": "#fee2e2", "strokeColor": "#dc2626" },
|
|
544
|
+
{ "id": "LANE", "label": "Carril", "shape": "rect", "defaultWidth": 300,"defaultHeight": 60, "color": "#f3f4f6", "strokeColor": "#9ca3af" }
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
### Modelo de datos TypeScript
|
|
553
|
+
|
|
554
|
+
El estado del editor se serializa en un objeto `VenueMap`. Puedes guardarlo en tu base de datos como JSON y restaurarlo con `initialMap`.
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
VenueMap
|
|
558
|
+
├── id: string
|
|
559
|
+
├── name: string
|
|
560
|
+
├── libraries?: ElementLibrary ← librerías importadas (embebidas en el mapa)
|
|
561
|
+
└── floors: Floor[]
|
|
562
|
+
├── id: string
|
|
563
|
+
├── name: string
|
|
564
|
+
├── order: number
|
|
565
|
+
├── area: FloorArea ← forma del piso (rect | polygon)
|
|
566
|
+
│ ├── shape: 'rect' | 'polygon'
|
|
567
|
+
│ ├── x?, y?, width?, height? ← para shape: 'rect'
|
|
568
|
+
│ └── points?: [number,number][] ← para shape: 'polygon'
|
|
569
|
+
├── wallNodes: WallNode[] ← vértices del grafo de paredes
|
|
570
|
+
├── walls: Wall[] ← segmentos de pared con grosor y material
|
|
571
|
+
└── elements: MapElement[]
|
|
572
|
+
├── id: string
|
|
573
|
+
├── type: string ← id del ElementTypeDef de la librería
|
|
574
|
+
├── x, y, width, height: number
|
|
575
|
+
├── rotation: number ← grados
|
|
576
|
+
├── label?: string
|
|
577
|
+
└── metadata?: Record<string, unknown> ← datos propios de tu app
|
|
578
|
+
```
|
|
294
579
|
|
|
295
|
-
|
|
580
|
+
El campo `metadata` en `MapElement` está disponible para que cada app guarde datos propios por elemento (ej. ID de reserva, capacidad, propietario, estado personalizado).
|
|
581
|
+
|
|
582
|
+
```tsx
|
|
583
|
+
// Ejemplo: guardar datos de negocio en metadata al crear elementos
|
|
584
|
+
const handleClick = (el: MapElement) => {
|
|
585
|
+
// El metadata lo pone tu app, no el editor
|
|
586
|
+
const reservaId = el.metadata?.reservaId as string;
|
|
587
|
+
abrirModal(reservaId);
|
|
588
|
+
};
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
### Herramientas del editor
|
|
594
|
+
|
|
595
|
+
| Tecla | Herramienta | Función |
|
|
596
|
+
|-------|-------------|---------|
|
|
597
|
+
| `V` | Seleccionar | Mover, redimensionar y rotar elementos. Arrastra el fondo del piso para moverlo. |
|
|
598
|
+
| `H` | Desplazar | Pan del canvas con click izquierdo. |
|
|
599
|
+
| `W` | Pared | Click fija el inicio; siguiente click termina el segmento (encadenado). Click derecho cancela. |
|
|
600
|
+
| `P` | Colocar | Click en el piso coloca el elemento seleccionado en la paleta. |
|
|
601
|
+
| `E` | Borrar | Click sobre un elemento o pared los elimina. |
|
|
602
|
+
| `Esc` | — | Vuelve a Seleccionar. |
|
|
603
|
+
| `Ctrl+Z / Y` | — | Deshacer / Rehacer. |
|
|
604
|
+
| `Ctrl+D` | — | Duplicar selección. |
|
|
605
|
+
| `Del / Backspace` | — | Eliminar selección. |
|
|
606
|
+
| `+ / -` | — | Zoom in / out. |
|
|
607
|
+
| Rueda ratón | — | Zoom centrado en el cursor. |
|
|
608
|
+
| Click medio + drag | — | Pan del canvas en cualquier modo. |
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
### Gestión de plantas
|
|
613
|
+
|
|
614
|
+
La barra de pestañas (visible incluso en viewer) permite:
|
|
615
|
+
|
|
616
|
+
- **Click** → cambiar de planta activa
|
|
617
|
+
- **Doble click** en el nombre → renombrar en línea
|
|
618
|
+
- **◀ ▶** → reordenar la planta activa
|
|
619
|
+
- **×** → eliminar la planta (mínimo 1)
|
|
620
|
+
- **+** → añadir nueva planta
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
### Forma del piso (Rect vs Polígono)
|
|
625
|
+
|
|
626
|
+
El botón **Rect / Poly** de la barra de herramientas alterna entre:
|
|
627
|
+
|
|
628
|
+
- **Rect**: rectángulo con 8 handles de redimensión en los bordes y esquinas.
|
|
629
|
+
- **Poly**: polígono libre. Arrastra los vértices (cuadrados azules). Click en el diamante central de una arista añade un vértice. Doble-click en un vértice lo elimina (mínimo 3).
|
|
630
|
+
|
|
631
|
+
Los elementos y paredes siempre se mantienen dentro del piso al moverlos o colocarlos.
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
### Exportar / Importar el mapa
|
|
636
|
+
|
|
637
|
+
| Botón | Función |
|
|
638
|
+
|-------|---------|
|
|
639
|
+
| ⬇ Exportar mapa | Descarga el estado actual como `.json` (incluye las librerías embebidas). |
|
|
640
|
+
| ⬆ Importar mapa | Carga un `.json` exportado previamente, reemplazando el mapa actual. |
|
|
641
|
+
| ⊞ Cargar librería | Carga un `.json` de elementos y añade sus grupos a la paleta sin reemplazar los existentes. |
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Showcase / Demo
|
|
296
646
|
|
|
297
647
|
```bash
|
|
298
648
|
cd showcase
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { WheelEvent, MouseEvent } from 'react';
|
|
4
|
+
|
|
5
|
+
type WallMaterial = 'concrete' | 'brick' | 'glass' | 'drywall' | 'wood';
|
|
6
|
+
type AreaShape = 'rect' | 'polygon';
|
|
7
|
+
type ElementShape = 'rect' | 'circle' | 'arrow' | 'path';
|
|
8
|
+
type ToolMode = 'SELECT' | 'WALL' | 'PLACE' | 'PAN' | 'ERASE';
|
|
9
|
+
interface WallNode {
|
|
10
|
+
id: string;
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
}
|
|
14
|
+
interface Wall {
|
|
15
|
+
id: string;
|
|
16
|
+
nodeAId: string;
|
|
17
|
+
nodeBId: string;
|
|
18
|
+
/** Thickness in canvas px */
|
|
19
|
+
thickness: number;
|
|
20
|
+
material: WallMaterial;
|
|
21
|
+
}
|
|
22
|
+
interface MapElement {
|
|
23
|
+
id: string;
|
|
24
|
+
/** e.g. 'TABLE_ROUND', 'PARKING_SPOT', 'DOOR' */
|
|
25
|
+
type: string;
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
/** Rotation in degrees */
|
|
31
|
+
rotation: number;
|
|
32
|
+
label?: string;
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
interface FloorArea {
|
|
36
|
+
shape: AreaShape;
|
|
37
|
+
x?: number;
|
|
38
|
+
y?: number;
|
|
39
|
+
width?: number;
|
|
40
|
+
height?: number;
|
|
41
|
+
points?: [number, number][];
|
|
42
|
+
}
|
|
43
|
+
interface Floor {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
order: number;
|
|
47
|
+
area: FloorArea;
|
|
48
|
+
wallNodes: WallNode[];
|
|
49
|
+
walls: Wall[];
|
|
50
|
+
elements: MapElement[];
|
|
51
|
+
}
|
|
52
|
+
interface VenueMap {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
floors: Floor[];
|
|
56
|
+
/** Custom element libraries imported by the user; persisted with the map. */
|
|
57
|
+
libraries?: ElementLibrary;
|
|
58
|
+
}
|
|
59
|
+
interface ElementTypeDef {
|
|
60
|
+
id: string;
|
|
61
|
+
label: string;
|
|
62
|
+
shape: ElementShape;
|
|
63
|
+
defaultWidth: number;
|
|
64
|
+
defaultHeight: number;
|
|
65
|
+
/** SVG fill color */
|
|
66
|
+
color: string;
|
|
67
|
+
strokeColor: string;
|
|
68
|
+
/** Emoji or icon name */
|
|
69
|
+
icon?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Raw SVG path `d` attribute for `shape === 'path'`.
|
|
72
|
+
* Define the path in the coordinate space of `viewBox` (default `"0 0 100 100"`).
|
|
73
|
+
* It will be automatically scaled to fit the element's `width × height` bounding box.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // A 5-pointed star in a 100×100 viewBox
|
|
77
|
+
* svgPath: "M50 5 L61 35 L95 35 L68 57 L79 91 L50 70 L21 91 L32 57 L5 35 L39 35 Z"
|
|
78
|
+
*/
|
|
79
|
+
svgPath?: string;
|
|
80
|
+
/**
|
|
81
|
+
* ViewBox for `svgPath`. Format: `"minX minY width height"`.
|
|
82
|
+
* Defaults to `"0 0 100 100"` when omitted.
|
|
83
|
+
*/
|
|
84
|
+
viewBox?: string;
|
|
85
|
+
}
|
|
86
|
+
interface DomainConfig {
|
|
87
|
+
id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
elementTypes: ElementTypeDef[];
|
|
90
|
+
}
|
|
91
|
+
interface ElementGroup {
|
|
92
|
+
name: string;
|
|
93
|
+
objects: ElementTypeDef[];
|
|
94
|
+
}
|
|
95
|
+
/** A library JSON file: top-level keys are group IDs. */
|
|
96
|
+
type ElementLibrary = Record<string, ElementGroup>;
|
|
97
|
+
interface ElementStatus {
|
|
98
|
+
elementId: string;
|
|
99
|
+
status: 'free' | 'occupied' | 'reserved' | 'disabled';
|
|
100
|
+
tooltip?: string;
|
|
101
|
+
}
|
|
102
|
+
interface VenueMapEditorProps {
|
|
103
|
+
/**
|
|
104
|
+
* Optional built-in element type catalog.
|
|
105
|
+
* If omitted the palette is empty until the user imports a library JSON.
|
|
106
|
+
*/
|
|
107
|
+
domainConfig?: DomainConfig;
|
|
108
|
+
/**
|
|
109
|
+
* Map to render. When this prop changes (by reference) from outside the
|
|
110
|
+
* component, the editor resets its history to the new map — allowing the
|
|
111
|
+
* parent to hydrate the editor from an API or local storage without causing
|
|
112
|
+
* a render loop (changes made inside the editor that are echoed back via
|
|
113
|
+
* `onChange` are detected and ignored).
|
|
114
|
+
*/
|
|
115
|
+
initialMap?: VenueMap;
|
|
116
|
+
/** Called every time the internal map state changes. */
|
|
117
|
+
onChange?: (map: VenueMap) => void;
|
|
118
|
+
width?: string | number;
|
|
119
|
+
height?: string | number;
|
|
120
|
+
gridSize?: number;
|
|
121
|
+
showGrid?: boolean;
|
|
122
|
+
snapToGrid?: boolean;
|
|
123
|
+
readOnly?: boolean;
|
|
124
|
+
/** Viewer-only mode: pan and zoom are allowed but nothing can be edited. */
|
|
125
|
+
fixed?: boolean;
|
|
126
|
+
elementStatus?: ElementStatus[];
|
|
127
|
+
onElementClick?: (element: MapElement) => void;
|
|
128
|
+
/**
|
|
129
|
+
* Per-type click handlers active in viewer/fixed mode.
|
|
130
|
+
* Keys are element type IDs (e.g. `'TABLE_ROUND'`).
|
|
131
|
+
* When an element is clicked, its type-specific handler fires first;
|
|
132
|
+
* if none is registered, `onElementClick` is used as fallback.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```tsx
|
|
136
|
+
* <VenueMapViewer
|
|
137
|
+
* onElementTypeClick={{
|
|
138
|
+
* TABLE_ROUND: (el) => openReservation(el.id),
|
|
139
|
+
* CHAIR: (el) => showInfo(el),
|
|
140
|
+
* }}
|
|
141
|
+
* />
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
onElementTypeClick?: Record<string, (element: MapElement) => void>;
|
|
145
|
+
}
|
|
146
|
+
type VenueMapViewerProps = VenueMapEditorProps;
|
|
147
|
+
|
|
148
|
+
declare function VenueMapEditor({ domainConfig, initialMap, onChange, width, height, gridSize, showGrid: showGridProp, snapToGrid: snapEnabled, readOnly, fixed, elementStatus, onElementClick, onElementTypeClick, }: VenueMapEditorProps): react_jsx_runtime.JSX.Element;
|
|
149
|
+
|
|
150
|
+
declare function VenueMapViewer({ elementStatus, onElementClick, ...rest }: VenueMapViewerProps): react_jsx_runtime.JSX.Element;
|
|
151
|
+
|
|
152
|
+
interface PaletteGroup {
|
|
153
|
+
id: string;
|
|
154
|
+
name: string;
|
|
155
|
+
/** True for the built-in domain config group; false for imported library groups. */
|
|
156
|
+
isBase?: boolean;
|
|
157
|
+
types: ElementTypeDef[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface PanZoomState {
|
|
161
|
+
panX: number;
|
|
162
|
+
panY: number;
|
|
163
|
+
zoom: number;
|
|
164
|
+
}
|
|
165
|
+
declare function usePanZoom(initialZoom?: number, leftClickPan?: boolean): {
|
|
166
|
+
state: PanZoomState;
|
|
167
|
+
setState: React.Dispatch<React.SetStateAction<PanZoomState>>;
|
|
168
|
+
isPanning: boolean;
|
|
169
|
+
handleWheel: (e: WheelEvent<SVGSVGElement>) => void;
|
|
170
|
+
handleMouseDown: (e: MouseEvent<SVGSVGElement>) => void;
|
|
171
|
+
handleMouseMove: (e: MouseEvent<SVGSVGElement>) => void;
|
|
172
|
+
handleMouseUp: (_e: MouseEvent<SVGSVGElement>) => void;
|
|
173
|
+
handleMouseLeave: () => void;
|
|
174
|
+
zoomBy: (factor: number, cx?: number, cy?: number) => void;
|
|
175
|
+
resetView: () => void;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** Generates a globally-unique id using the Web Crypto API. */
|
|
179
|
+
declare const genId: () => string;
|
|
180
|
+
|
|
181
|
+
/** Snap a single value to the nearest grid line. */
|
|
182
|
+
declare const snapToGrid: (value: number, gridSize: number) => number;
|
|
183
|
+
/** Snap a 2-D point to the grid if `enabled`, otherwise return it unchanged. */
|
|
184
|
+
declare const snapPoint: (x: number, y: number, gridSize: number, enabled: boolean) => {
|
|
185
|
+
x: number;
|
|
186
|
+
y: number;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Find the closest WallNode within `threshold` canvas units.
|
|
190
|
+
* Returns the node's id and position, or null when nothing is close enough.
|
|
191
|
+
*/
|
|
192
|
+
declare const findNearestNode: (x: number, y: number, nodes: Array<{
|
|
193
|
+
id: string;
|
|
194
|
+
x: number;
|
|
195
|
+
y: number;
|
|
196
|
+
}>, threshold: number) => {
|
|
197
|
+
id: string;
|
|
198
|
+
x: number;
|
|
199
|
+
y: number;
|
|
200
|
+
} | null;
|
|
201
|
+
|
|
202
|
+
export { type AreaShape, type DomainConfig, type ElementGroup, type ElementLibrary, type ElementShape, type ElementStatus, type ElementTypeDef, type Floor, type FloorArea, type MapElement, type PaletteGroup, type PanZoomState, type ToolMode, type VenueMap, VenueMapEditor, type VenueMapEditorProps, VenueMapViewer, type VenueMapViewerProps, type Wall, type WallMaterial, type WallNode, findNearestNode, genId, snapPoint, snapToGrid, usePanZoom };
|