neogestify-ui-components 2.0.1 → 2.1.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 +108 -19
- package/dist/components/VenueMapEditor/index.d.mts +49 -4
- package/dist/components/VenueMapEditor/index.d.ts +49 -4
- package/dist/components/VenueMapEditor/index.js +124 -31
- package/dist/components/VenueMapEditor/index.js.map +1 -1
- package/dist/components/VenueMapEditor/index.mjs +125 -33
- package/dist/components/VenueMapEditor/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +124 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +125 -33
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/VenueMapEditor/VenueMapEditor.tsx +79 -20
- package/src/components/VenueMapEditor/components/ElementNode.tsx +1 -0
- package/src/components/VenueMapEditor/components/Toolbar.tsx +59 -35
- package/src/components/VenueMapEditor/hooks/useLibraryStorage.ts +46 -0
- package/src/components/VenueMapEditor/index.ts +1 -0
- package/src/components/VenueMapEditor/types.ts +34 -2
package/README.md
CHANGED
|
@@ -380,7 +380,9 @@ function App() {
|
|
|
380
380
|
|------|------|---------|-------------|
|
|
381
381
|
| `initialMap` | `VenueMap` | mapa vacío | Mapa inicial. Se puede actualizar desde fuera para recargar el editor. |
|
|
382
382
|
| `onChange` | `(map: VenueMap) => void` | — | Se llama en cada cambio del estado interno. |
|
|
383
|
-
| `
|
|
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. |
|
|
384
386
|
| `width` | `string \| number` | `'100%'` | Ancho del componente. |
|
|
385
387
|
| `height` | `string \| number` | `'600px'` | Alto del componente. |
|
|
386
388
|
| `gridSize` | `number` | `20` | Tamaño de la cuadrícula en unidades de canvas. |
|
|
@@ -434,9 +436,60 @@ const estados: ElementStatus[] = [
|
|
|
434
436
|
|
|
435
437
|
---
|
|
436
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.
|
|
442
|
+
|
|
443
|
+
```tsx
|
|
444
|
+
import { VenueMapEditor } from 'neogestify-ui-components/VenueMapEditor';
|
|
445
|
+
import type { DomainConfig } from 'neogestify-ui-components/VenueMapEditor';
|
|
446
|
+
|
|
447
|
+
const mobiliario: DomainConfig = {
|
|
448
|
+
id: 'furniture',
|
|
449
|
+
name: 'Mobiliario',
|
|
450
|
+
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' },
|
|
453
|
+
],
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const iluminacion: DomainConfig = {
|
|
457
|
+
id: 'lighting',
|
|
458
|
+
name: 'Iluminación',
|
|
459
|
+
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' },
|
|
462
|
+
],
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
<VenueMapEditor domainConfigs={[mobiliario, iluminacion]} />
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
La paleta mostrará:
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
[ Mobiliario ] [ Iluminación ]
|
|
472
|
+
─────────────────────────────
|
|
473
|
+
[Silla] [Mesa rect.]
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
437
478
|
### Crear una librería de elementos (JSON)
|
|
438
479
|
|
|
439
|
-
Los elementos que aparecen en la paleta
|
|
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
|
+
```
|
|
440
493
|
|
|
441
494
|
#### Formato del JSON
|
|
442
495
|
|
|
@@ -527,17 +580,18 @@ Ahora puedes definir cualquier figura SVG usando un path:
|
|
|
527
580
|
|
|
528
581
|
#### Propiedades de cada objeto
|
|
529
582
|
|
|
530
|
-
| Campo | Tipo | Descripción |
|
|
531
|
-
|
|
532
|
-
| `id` | `string` | Identificador único del tipo.
|
|
533
|
-
| `label` | `string` | Nombre visible en la paleta. |
|
|
534
|
-
| `shape` | `"rect" \| "circle" \| "arrow" \| "path"` | Forma del objeto
|
|
535
|
-
| `defaultWidth` | `number` | Ancho inicial al colocar el elemento (unidades de canvas ≈
|
|
536
|
-
| `defaultHeight` | `number` | Alto inicial. |
|
|
537
|
-
| `color` | `string` | Color de relleno (cualquier valor CSS: `#hex`, `rgb()`, `hsl()`, etc.). |
|
|
538
|
-
| `strokeColor` | `string` | Color del borde. |
|
|
539
|
-
| `svgPath` | `string` |
|
|
540
|
-
| `viewBox` | `string` |
|
|
583
|
+
| Campo | Tipo | Requerido | Descripción |
|
|
584
|
+
|-------|------|-----------|-------------|
|
|
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"` | ✓ | 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
|
+
| `viewBox` | `string` | — | Espacio de coordenadas del `svgPath`. Formato: `"minX minY w h"`. Default: `"0 0 100 100"`. |
|
|
594
|
+
| `fillRule` | `"nonzero" \| "evenodd"` | — | Regla de relleno SVG. Usa `"evenodd"` para crear huecos con sub-paths (engranajes, letras, donuts). Default: `"nonzero"`. |
|
|
541
595
|
|
|
542
596
|
#### Formas disponibles
|
|
543
597
|
|
|
@@ -546,15 +600,50 @@ Ahora puedes definir cualquier figura SVG usando un path:
|
|
|
546
600
|
| `rect` | Rectángulo | Mesas, espacios de parqueo, habitaciones |
|
|
547
601
|
| `circle` | Elipse (círculo si `width === height`) | Mesas redondas, columnas, plantas |
|
|
548
602
|
| `arrow` | Flecha apuntando a la derecha | Entradas, salidas, sentidos de circulación |
|
|
549
|
-
| `path` | Forma SVG personalizada |
|
|
603
|
+
| `path` | Forma SVG personalizada libre | Cualquier figura: estrella, engranaje, piano, logo... |
|
|
604
|
+
|
|
605
|
+
#### Formas personalizadas con `shape: "path"`
|
|
606
|
+
|
|
607
|
+
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.
|
|
550
608
|
|
|
551
|
-
|
|
609
|
+
```json
|
|
610
|
+
{
|
|
611
|
+
"especiales": {
|
|
612
|
+
"name": "Especiales",
|
|
613
|
+
"objects": [
|
|
614
|
+
{
|
|
615
|
+
"id": "STAR",
|
|
616
|
+
"label": "Estrella",
|
|
617
|
+
"shape": "path",
|
|
618
|
+
"viewBox": "0 0 100 100",
|
|
619
|
+
"svgPath": "M50 5 L61 35 L95 35 L68 57 L79 91 L50 70 L21 91 L32 57 L5 35 L39 35 Z",
|
|
620
|
+
"defaultWidth": 60,
|
|
621
|
+
"defaultHeight": 60,
|
|
622
|
+
"color": "#facc15",
|
|
623
|
+
"strokeColor": "#ca8a04"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
"id": "GEAR",
|
|
627
|
+
"label": "Engranaje",
|
|
628
|
+
"shape": "path",
|
|
629
|
+
"viewBox": "0 0 100 100",
|
|
630
|
+
"fillRule": "evenodd",
|
|
631
|
+
"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",
|
|
632
|
+
"defaultWidth": 70,
|
|
633
|
+
"defaultHeight": 70,
|
|
634
|
+
"color": "#94a3b8",
|
|
635
|
+
"strokeColor": "#334155"
|
|
636
|
+
}
|
|
637
|
+
]
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
```
|
|
552
641
|
|
|
553
|
-
|
|
642
|
+
> **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.
|
|
554
643
|
|
|
555
644
|
#### Varios grupos en un archivo
|
|
556
645
|
|
|
557
|
-
Un mismo archivo puede tener tantos grupos como necesites. Cada grupo aparece como una
|
|
646
|
+
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.
|
|
558
647
|
|
|
559
648
|
```json
|
|
560
649
|
{
|
|
@@ -677,9 +766,9 @@ Los elementos y paredes siempre se mantienen dentro del piso al moverlos o coloc
|
|
|
677
766
|
|
|
678
767
|
| Botón | Función |
|
|
679
768
|
|-------|---------|
|
|
680
|
-
| ⬇ Exportar mapa | Descarga el estado actual como `.json` (incluye las librerías embebidas). |
|
|
769
|
+
| ⬇ Exportar mapa | Descarga el estado actual como `.json` (incluye las librerías embebidas para portabilidad). |
|
|
681
770
|
| ⬆ Importar mapa | Carga un `.json` exportado previamente, reemplazando el mapa actual. |
|
|
682
|
-
| ⊞ Cargar librería | Carga un `.json` de elementos
|
|
771
|
+
| ⊞ 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`. |
|
|
683
772
|
|
|
684
773
|
---
|
|
685
774
|
|
|
@@ -82,6 +82,13 @@ interface ElementTypeDef {
|
|
|
82
82
|
* Defaults to `"0 0 100 100"` when omitted.
|
|
83
83
|
*/
|
|
84
84
|
viewBox?: string;
|
|
85
|
+
/**
|
|
86
|
+
* SVG fill rule for `shape === 'path'`.
|
|
87
|
+
* Use `'evenodd'` when the path contains sub-paths that should appear as holes
|
|
88
|
+
* (e.g. a gear with a circular cutout, a donut, a letter with counter-forms).
|
|
89
|
+
* Defaults to `'nonzero'`.
|
|
90
|
+
*/
|
|
91
|
+
fillRule?: 'nonzero' | 'evenodd';
|
|
85
92
|
}
|
|
86
93
|
interface DomainConfig {
|
|
87
94
|
id: string;
|
|
@@ -101,10 +108,35 @@ interface ElementStatus {
|
|
|
101
108
|
}
|
|
102
109
|
interface VenueMapEditorProps {
|
|
103
110
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
111
|
+
* One or more built-in element type catalogs shown as separate palette groups.
|
|
112
|
+
* Each `DomainConfig` becomes its own named section in the element palette.
|
|
113
|
+
* Takes precedence over the legacy `domainConfig` singular prop.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* <VenueMapEditor
|
|
118
|
+
* domainConfigs={[furnitureConfig, lightingConfig, audioConfig]}
|
|
119
|
+
* />
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
domainConfigs?: DomainConfig[];
|
|
123
|
+
/**
|
|
124
|
+
* @deprecated Use `domainConfigs` (array) instead.
|
|
125
|
+
* Single built-in element type catalog. Ignored when `domainConfigs` is provided.
|
|
106
126
|
*/
|
|
107
127
|
domainConfig?: DomainConfig;
|
|
128
|
+
/**
|
|
129
|
+
* localStorage key used to persist user-imported libraries across sessions.
|
|
130
|
+
* Libraries are loaded **synchronously** on mount so all type definitions are
|
|
131
|
+
* available before the map renders — preventing "unknown element type" errors.
|
|
132
|
+
*
|
|
133
|
+
* Set to `''` to disable persistence (libraries are lost on page reload).
|
|
134
|
+
* Defaults to `'venueMapEditor:libraries'`.
|
|
135
|
+
*
|
|
136
|
+
* Multiple editor instances on the same page should use different keys if
|
|
137
|
+
* they manage independent library sets.
|
|
138
|
+
*/
|
|
139
|
+
libraryStorageKey?: string;
|
|
108
140
|
/**
|
|
109
141
|
* Map to render. When this prop changes (by reference) from outside the
|
|
110
142
|
* component, the editor resets its history to the new map — allowing the
|
|
@@ -145,7 +177,7 @@ interface VenueMapEditorProps {
|
|
|
145
177
|
}
|
|
146
178
|
type VenueMapViewerProps = VenueMapEditorProps;
|
|
147
179
|
|
|
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;
|
|
180
|
+
declare function VenueMapEditor({ domainConfigs, domainConfig, libraryStorageKey, initialMap, onChange, width, height, gridSize, showGrid: showGridProp, snapToGrid: snapEnabled, readOnly, fixed, elementStatus, onElementClick, onElementTypeClick, }: VenueMapEditorProps): react_jsx_runtime.JSX.Element;
|
|
149
181
|
|
|
150
182
|
declare function VenueMapViewer({ elementStatus, onElementClick, ...rest }: VenueMapViewerProps): react_jsx_runtime.JSX.Element;
|
|
151
183
|
|
|
@@ -175,6 +207,19 @@ declare function usePanZoom(initialZoom?: number, leftClickPan?: boolean): {
|
|
|
175
207
|
resetView: () => void;
|
|
176
208
|
};
|
|
177
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Persists an `ElementLibrary` in `localStorage` and exposes it as React state.
|
|
212
|
+
*
|
|
213
|
+
* - Initialises **synchronously** from localStorage so type definitions are
|
|
214
|
+
* available before the map renders (no flash of unknown element types).
|
|
215
|
+
* - Gracefully degrades when localStorage is unavailable (SSR, private mode,
|
|
216
|
+
* storage quota exceeded) — falls back to plain in-memory state.
|
|
217
|
+
*
|
|
218
|
+
* @param storageKey localStorage key to read/write.
|
|
219
|
+
* Pass `''` or `undefined` to disable persistence entirely.
|
|
220
|
+
*/
|
|
221
|
+
declare function useLibraryStorage(storageKey: string | undefined): [ElementLibrary, (libs: ElementLibrary) => void];
|
|
222
|
+
|
|
178
223
|
/** Generates a globally-unique id using the Web Crypto API. */
|
|
179
224
|
declare const genId: () => string;
|
|
180
225
|
|
|
@@ -199,4 +244,4 @@ declare const findNearestNode: (x: number, y: number, nodes: Array<{
|
|
|
199
244
|
y: number;
|
|
200
245
|
} | null;
|
|
201
246
|
|
|
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 };
|
|
247
|
+
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, useLibraryStorage, usePanZoom };
|
|
@@ -82,6 +82,13 @@ interface ElementTypeDef {
|
|
|
82
82
|
* Defaults to `"0 0 100 100"` when omitted.
|
|
83
83
|
*/
|
|
84
84
|
viewBox?: string;
|
|
85
|
+
/**
|
|
86
|
+
* SVG fill rule for `shape === 'path'`.
|
|
87
|
+
* Use `'evenodd'` when the path contains sub-paths that should appear as holes
|
|
88
|
+
* (e.g. a gear with a circular cutout, a donut, a letter with counter-forms).
|
|
89
|
+
* Defaults to `'nonzero'`.
|
|
90
|
+
*/
|
|
91
|
+
fillRule?: 'nonzero' | 'evenodd';
|
|
85
92
|
}
|
|
86
93
|
interface DomainConfig {
|
|
87
94
|
id: string;
|
|
@@ -101,10 +108,35 @@ interface ElementStatus {
|
|
|
101
108
|
}
|
|
102
109
|
interface VenueMapEditorProps {
|
|
103
110
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
111
|
+
* One or more built-in element type catalogs shown as separate palette groups.
|
|
112
|
+
* Each `DomainConfig` becomes its own named section in the element palette.
|
|
113
|
+
* Takes precedence over the legacy `domainConfig` singular prop.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* <VenueMapEditor
|
|
118
|
+
* domainConfigs={[furnitureConfig, lightingConfig, audioConfig]}
|
|
119
|
+
* />
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
domainConfigs?: DomainConfig[];
|
|
123
|
+
/**
|
|
124
|
+
* @deprecated Use `domainConfigs` (array) instead.
|
|
125
|
+
* Single built-in element type catalog. Ignored when `domainConfigs` is provided.
|
|
106
126
|
*/
|
|
107
127
|
domainConfig?: DomainConfig;
|
|
128
|
+
/**
|
|
129
|
+
* localStorage key used to persist user-imported libraries across sessions.
|
|
130
|
+
* Libraries are loaded **synchronously** on mount so all type definitions are
|
|
131
|
+
* available before the map renders — preventing "unknown element type" errors.
|
|
132
|
+
*
|
|
133
|
+
* Set to `''` to disable persistence (libraries are lost on page reload).
|
|
134
|
+
* Defaults to `'venueMapEditor:libraries'`.
|
|
135
|
+
*
|
|
136
|
+
* Multiple editor instances on the same page should use different keys if
|
|
137
|
+
* they manage independent library sets.
|
|
138
|
+
*/
|
|
139
|
+
libraryStorageKey?: string;
|
|
108
140
|
/**
|
|
109
141
|
* Map to render. When this prop changes (by reference) from outside the
|
|
110
142
|
* component, the editor resets its history to the new map — allowing the
|
|
@@ -145,7 +177,7 @@ interface VenueMapEditorProps {
|
|
|
145
177
|
}
|
|
146
178
|
type VenueMapViewerProps = VenueMapEditorProps;
|
|
147
179
|
|
|
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;
|
|
180
|
+
declare function VenueMapEditor({ domainConfigs, domainConfig, libraryStorageKey, initialMap, onChange, width, height, gridSize, showGrid: showGridProp, snapToGrid: snapEnabled, readOnly, fixed, elementStatus, onElementClick, onElementTypeClick, }: VenueMapEditorProps): react_jsx_runtime.JSX.Element;
|
|
149
181
|
|
|
150
182
|
declare function VenueMapViewer({ elementStatus, onElementClick, ...rest }: VenueMapViewerProps): react_jsx_runtime.JSX.Element;
|
|
151
183
|
|
|
@@ -175,6 +207,19 @@ declare function usePanZoom(initialZoom?: number, leftClickPan?: boolean): {
|
|
|
175
207
|
resetView: () => void;
|
|
176
208
|
};
|
|
177
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Persists an `ElementLibrary` in `localStorage` and exposes it as React state.
|
|
212
|
+
*
|
|
213
|
+
* - Initialises **synchronously** from localStorage so type definitions are
|
|
214
|
+
* available before the map renders (no flash of unknown element types).
|
|
215
|
+
* - Gracefully degrades when localStorage is unavailable (SSR, private mode,
|
|
216
|
+
* storage quota exceeded) — falls back to plain in-memory state.
|
|
217
|
+
*
|
|
218
|
+
* @param storageKey localStorage key to read/write.
|
|
219
|
+
* Pass `''` or `undefined` to disable persistence entirely.
|
|
220
|
+
*/
|
|
221
|
+
declare function useLibraryStorage(storageKey: string | undefined): [ElementLibrary, (libs: ElementLibrary) => void];
|
|
222
|
+
|
|
178
223
|
/** Generates a globally-unique id using the Web Crypto API. */
|
|
179
224
|
declare const genId: () => string;
|
|
180
225
|
|
|
@@ -199,4 +244,4 @@ declare const findNearestNode: (x: number, y: number, nodes: Array<{
|
|
|
199
244
|
y: number;
|
|
200
245
|
} | null;
|
|
201
246
|
|
|
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 };
|
|
247
|
+
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, useLibraryStorage, usePanZoom };
|
|
@@ -4,6 +4,33 @@ var react = require('react');
|
|
|
4
4
|
var jsxRuntime = require('react/jsx-runtime');
|
|
5
5
|
|
|
6
6
|
// src/components/VenueMapEditor/VenueMapEditor.tsx
|
|
7
|
+
function useLibraryStorage(storageKey) {
|
|
8
|
+
const [libs, setLibs] = react.useState(() => {
|
|
9
|
+
if (!storageKey) return {};
|
|
10
|
+
try {
|
|
11
|
+
const raw = localStorage.getItem(storageKey);
|
|
12
|
+
return raw ? JSON.parse(raw) : {};
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const setAndPersist = react.useCallback(
|
|
18
|
+
(newLibs) => {
|
|
19
|
+
setLibs(newLibs);
|
|
20
|
+
if (!storageKey) return;
|
|
21
|
+
try {
|
|
22
|
+
if (Object.keys(newLibs).length === 0) {
|
|
23
|
+
localStorage.removeItem(storageKey);
|
|
24
|
+
} else {
|
|
25
|
+
localStorage.setItem(storageKey, JSON.stringify(newLibs));
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
[storageKey]
|
|
31
|
+
);
|
|
32
|
+
return [libs, setAndPersist];
|
|
33
|
+
}
|
|
7
34
|
function IconCursor({ className }) {
|
|
8
35
|
return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 1l12 5.5-5.5 1.5L7 13.5 2 1z" }) });
|
|
9
36
|
}
|
|
@@ -130,6 +157,19 @@ function Toolbar({
|
|
|
130
157
|
onLoadLibrary,
|
|
131
158
|
onRemoveLibraryGroup
|
|
132
159
|
}) {
|
|
160
|
+
const [activeGroupId, setActiveGroupId] = react.useState(
|
|
161
|
+
() => paletteGroups[0]?.id ?? null
|
|
162
|
+
);
|
|
163
|
+
react.useEffect(() => {
|
|
164
|
+
if (paletteGroups.length === 0) {
|
|
165
|
+
setActiveGroupId(null);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!paletteGroups.find((g) => g.id === activeGroupId)) {
|
|
169
|
+
setActiveGroupId(paletteGroups[0].id);
|
|
170
|
+
}
|
|
171
|
+
}, [paletteGroups, activeGroupId]);
|
|
172
|
+
const activeGroup = paletteGroups.find((g) => g.id === activeGroupId) ?? null;
|
|
133
173
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col bg-white border-b border-slate-200 shadow-sm shrink-0", children: [
|
|
134
174
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 px-2 py-1.5", children: [
|
|
135
175
|
/* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Seleccionar (V)", active: tool === "SELECT", onClick: () => onToolChange("SELECT"), children: /* @__PURE__ */ jsxRuntime.jsx(IconCursor, { className: "w-4 h-4" }) }),
|
|
@@ -167,21 +207,34 @@ function Toolbar({
|
|
|
167
207
|
/* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: areaShape === "polygon" ? "Cambiar a rect\xE1ngulo" : "Cambiar a pol\xEDgono", onClick: () => onToggleAreaShape?.(), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium", children: areaShape === "polygon" ? "Poly" : "Rect" }) })
|
|
168
208
|
] })
|
|
169
209
|
] }),
|
|
170
|
-
tool === "PLACE" && /* @__PURE__ */ jsxRuntime.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
210
|
+
tool === "PLACE" && paletteGroups.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col border-t border-slate-100", children: [
|
|
211
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-end gap-0 overflow-x-auto bg-slate-50 border-b border-slate-200 px-2 pt-1", children: paletteGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
212
|
+
"button",
|
|
213
|
+
{
|
|
214
|
+
onClick: () => setActiveGroupId(group.id),
|
|
215
|
+
className: [
|
|
216
|
+
"flex items-center gap-1 px-3 py-1 text-xs font-medium rounded-t border-x border-t transition-colors whitespace-nowrap",
|
|
217
|
+
group.id === activeGroupId ? "bg-white border-slate-200 text-slate-800 -mb-px pb-[5px]" : "bg-slate-50 border-transparent text-slate-400 hover:text-slate-600 hover:bg-slate-100"
|
|
218
|
+
].join(" "),
|
|
219
|
+
children: [
|
|
220
|
+
group.name || "Sin nombre",
|
|
221
|
+
!group.isBase && onRemoveLibraryGroup && /* @__PURE__ */ jsxRuntime.jsx(
|
|
222
|
+
"span",
|
|
223
|
+
{
|
|
224
|
+
role: "button",
|
|
225
|
+
title: `Eliminar "${group.name}"`,
|
|
226
|
+
onClick: (e) => {
|
|
227
|
+
e.stopPropagation();
|
|
228
|
+
onRemoveLibraryGroup(group.id);
|
|
229
|
+
},
|
|
230
|
+
className: "ml-0.5 text-slate-300 hover:text-red-400 transition-colors leading-none",
|
|
231
|
+
children: "\xD7"
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
) }, group.id)) }),
|
|
237
|
+
activeGroup && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-wrap px-2 py-1.5 bg-white min-h-[36px]", children: activeGroup.types.map((typeDef) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
185
238
|
TypeChip,
|
|
186
239
|
{
|
|
187
240
|
typeDef,
|
|
@@ -190,7 +243,7 @@ function Toolbar({
|
|
|
190
243
|
},
|
|
191
244
|
typeDef.id
|
|
192
245
|
)) })
|
|
193
|
-
] }
|
|
246
|
+
] })
|
|
194
247
|
] });
|
|
195
248
|
}
|
|
196
249
|
var ZOOM_MIN = 0.1;
|
|
@@ -1130,6 +1183,7 @@ function ElementNode({
|
|
|
1130
1183
|
{
|
|
1131
1184
|
d: typeDef.svgPath,
|
|
1132
1185
|
fill: fillColor,
|
|
1186
|
+
fillRule: typeDef.fillRule ?? "nonzero",
|
|
1133
1187
|
stroke: isSelected ? "#3b82f6" : typeDef.strokeColor,
|
|
1134
1188
|
strokeWidth: isSelected ? customPath.strokeWidth * 1.5 : customPath.strokeWidth,
|
|
1135
1189
|
style: { cursor: bodyCursor },
|
|
@@ -1977,7 +2031,23 @@ function createDefaultMap() {
|
|
|
1977
2031
|
]
|
|
1978
2032
|
};
|
|
1979
2033
|
}
|
|
1980
|
-
var
|
|
2034
|
+
var DEFAULT_LIBRARY_KEY = "venueMapEditor:libraries";
|
|
2035
|
+
function mergeLibraries(existing, incoming) {
|
|
2036
|
+
const result = { ...existing };
|
|
2037
|
+
for (const [groupId, incomingGroup] of Object.entries(incoming)) {
|
|
2038
|
+
if (result[groupId]) {
|
|
2039
|
+
const existingIds = new Set(result[groupId].objects.map((o) => o.id));
|
|
2040
|
+
const newObjects = incomingGroup.objects.filter((o) => !existingIds.has(o.id));
|
|
2041
|
+
result[groupId] = {
|
|
2042
|
+
...result[groupId],
|
|
2043
|
+
objects: [...result[groupId].objects, ...newObjects]
|
|
2044
|
+
};
|
|
2045
|
+
} else {
|
|
2046
|
+
result[groupId] = incomingGroup;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
return result;
|
|
2050
|
+
}
|
|
1981
2051
|
function updateFloor(map, updatedFloor) {
|
|
1982
2052
|
return {
|
|
1983
2053
|
...map,
|
|
@@ -2017,7 +2087,9 @@ function polygonToRect(area) {
|
|
|
2017
2087
|
};
|
|
2018
2088
|
}
|
|
2019
2089
|
function VenueMapEditor({
|
|
2020
|
-
|
|
2090
|
+
domainConfigs,
|
|
2091
|
+
domainConfig,
|
|
2092
|
+
libraryStorageKey = DEFAULT_LIBRARY_KEY,
|
|
2021
2093
|
initialMap,
|
|
2022
2094
|
onChange,
|
|
2023
2095
|
width = "100%",
|
|
@@ -2031,7 +2103,13 @@ function VenueMapEditor({
|
|
|
2031
2103
|
onElementClick,
|
|
2032
2104
|
onElementTypeClick
|
|
2033
2105
|
}) {
|
|
2106
|
+
const effectiveConfigs = react.useMemo(() => {
|
|
2107
|
+
if (domainConfigs && domainConfigs.length > 0) return domainConfigs;
|
|
2108
|
+
if (domainConfig) return [domainConfig];
|
|
2109
|
+
return [];
|
|
2110
|
+
}, [domainConfigs, domainConfig]);
|
|
2034
2111
|
const initialMapRef = react.useRef(initialMap ?? createDefaultMap());
|
|
2112
|
+
const [persistedLibs, setPersistedLibs] = useLibraryStorage(libraryStorageKey);
|
|
2035
2113
|
const { map, canUndo, canRedo, push, replace, undo, redo } = useHistory(
|
|
2036
2114
|
initialMapRef.current
|
|
2037
2115
|
);
|
|
@@ -2047,31 +2125,40 @@ function VenueMapEditor({
|
|
|
2047
2125
|
const resetViewRef = react.useRef(() => void 0);
|
|
2048
2126
|
const importInputRef = react.useRef(null);
|
|
2049
2127
|
const libraryInputRef = react.useRef(null);
|
|
2128
|
+
const effectiveLibs = react.useMemo(() => ({
|
|
2129
|
+
...map.libraries ?? {},
|
|
2130
|
+
...persistedLibs
|
|
2131
|
+
}), [map.libraries, persistedLibs]);
|
|
2050
2132
|
const buildTypeDefs = react.useCallback(() => {
|
|
2051
|
-
const m = new Map(
|
|
2052
|
-
const
|
|
2053
|
-
|
|
2133
|
+
const m = /* @__PURE__ */ new Map();
|
|
2134
|
+
for (const cfg of effectiveConfigs) {
|
|
2135
|
+
for (const t of cfg.elementTypes) {
|
|
2136
|
+
if (!m.has(t.id)) m.set(t.id, t);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
for (const group of Object.values(effectiveLibs)) {
|
|
2054
2140
|
for (const t of group.objects) {
|
|
2055
2141
|
if (!m.has(t.id)) m.set(t.id, t);
|
|
2056
2142
|
}
|
|
2057
2143
|
}
|
|
2058
2144
|
return m;
|
|
2059
|
-
}, [
|
|
2145
|
+
}, [effectiveConfigs, effectiveLibs]);
|
|
2060
2146
|
const elementTypeDefs = react.useRef(buildTypeDefs());
|
|
2061
2147
|
react.useEffect(() => {
|
|
2062
2148
|
elementTypeDefs.current = buildTypeDefs();
|
|
2063
2149
|
}, [buildTypeDefs]);
|
|
2064
2150
|
const paletteGroups = react.useMemo(() => {
|
|
2065
2151
|
const groups = [];
|
|
2066
|
-
|
|
2067
|
-
|
|
2152
|
+
for (const cfg of effectiveConfigs) {
|
|
2153
|
+
if (cfg.elementTypes.length > 0) {
|
|
2154
|
+
groups.push({ id: cfg.id, name: cfg.name, types: cfg.elementTypes, isBase: true });
|
|
2155
|
+
}
|
|
2068
2156
|
}
|
|
2069
|
-
const
|
|
2070
|
-
for (const [gid, group] of Object.entries(libs)) {
|
|
2157
|
+
for (const [gid, group] of Object.entries(effectiveLibs)) {
|
|
2071
2158
|
groups.push({ id: gid, name: group.name, types: group.objects, isBase: false });
|
|
2072
2159
|
}
|
|
2073
2160
|
return groups;
|
|
2074
|
-
}, [
|
|
2161
|
+
}, [effectiveConfigs, effectiveLibs]);
|
|
2075
2162
|
react.useEffect(() => {
|
|
2076
2163
|
if (activePlaceTypeId) return;
|
|
2077
2164
|
const firstType = paletteGroups[0]?.types[0];
|
|
@@ -2211,22 +2298,27 @@ function VenueMapEditor({
|
|
|
2211
2298
|
reader.onload = (e) => {
|
|
2212
2299
|
try {
|
|
2213
2300
|
const parsed = JSON.parse(e.target?.result);
|
|
2214
|
-
const
|
|
2215
|
-
|
|
2301
|
+
const mergedPersisted = mergeLibraries(persistedLibs, parsed);
|
|
2302
|
+
setPersistedLibs(mergedPersisted);
|
|
2303
|
+
const mergedMap = mergeLibraries(map.libraries ?? {}, parsed);
|
|
2304
|
+
push({ ...map, libraries: mergedMap });
|
|
2216
2305
|
} catch {
|
|
2217
2306
|
}
|
|
2218
2307
|
};
|
|
2219
2308
|
reader.readAsText(file);
|
|
2220
2309
|
},
|
|
2221
|
-
[map, push]
|
|
2310
|
+
[map, push, persistedLibs, setPersistedLibs]
|
|
2222
2311
|
);
|
|
2223
2312
|
const handleRemoveLibraryGroup = react.useCallback(
|
|
2224
2313
|
(groupId) => {
|
|
2314
|
+
const newPersistedLibs = { ...persistedLibs };
|
|
2315
|
+
delete newPersistedLibs[groupId];
|
|
2316
|
+
setPersistedLibs(newPersistedLibs);
|
|
2225
2317
|
const libs = { ...map.libraries ?? {} };
|
|
2226
2318
|
delete libs[groupId];
|
|
2227
2319
|
push({ ...map, libraries: Object.keys(libs).length > 0 ? libs : void 0 });
|
|
2228
2320
|
},
|
|
2229
|
-
[map, push]
|
|
2321
|
+
[map, push, persistedLibs, setPersistedLibs]
|
|
2230
2322
|
);
|
|
2231
2323
|
const DEFAULT_WALL_THICKNESS = 8;
|
|
2232
2324
|
const handleAddWall = react.useCallback(
|
|
@@ -2679,6 +2771,7 @@ exports.findNearestNode = findNearestNode;
|
|
|
2679
2771
|
exports.genId = genId;
|
|
2680
2772
|
exports.snapPoint = snapPoint;
|
|
2681
2773
|
exports.snapToGrid = snapToGrid;
|
|
2774
|
+
exports.useLibraryStorage = useLibraryStorage;
|
|
2682
2775
|
exports.usePanZoom = usePanZoom;
|
|
2683
2776
|
//# sourceMappingURL=index.js.map
|
|
2684
2777
|
//# sourceMappingURL=index.js.map
|