zenit-sdk 0.0.2 → 0.0.3

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.
@@ -0,0 +1,3060 @@
1
+ // src/react/ZenitMap.tsx
2
+ import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, forwardRef } from "react";
3
+ import { GeoJSON, MapContainer, Marker, TileLayer, ZoomControl, useMap } from "react-leaflet";
4
+ import L from "leaflet";
5
+
6
+ // src/maps/helpers.ts
7
+ function toNumber(value) {
8
+ if (typeof value === "number" && Number.isFinite(value)) {
9
+ return value;
10
+ }
11
+ if (typeof value === "string") {
12
+ const parsed = parseFloat(value);
13
+ if (Number.isFinite(parsed)) {
14
+ return parsed;
15
+ }
16
+ }
17
+ return void 0;
18
+ }
19
+ function clampOpacity(value) {
20
+ if (!Number.isFinite(value ?? NaN)) {
21
+ return 1;
22
+ }
23
+ return Math.min(1, Math.max(0, value ?? 1));
24
+ }
25
+ function resolveLayerId(mapLayer) {
26
+ if (mapLayer.layerId !== void 0 && mapLayer.layerId !== null) {
27
+ return mapLayer.layerId;
28
+ }
29
+ const embedded = mapLayer.layer;
30
+ return embedded?.id;
31
+ }
32
+ function extractMapDto(map) {
33
+ if (map && typeof map === "object" && "data" in map) {
34
+ const data = map.data;
35
+ return data ?? null;
36
+ }
37
+ if (map && typeof map === "object") {
38
+ return map;
39
+ }
40
+ return null;
41
+ }
42
+ function normalizeMapCenter(map) {
43
+ const center = map.settings?.center;
44
+ if (Array.isArray(center) && center.length >= 2) {
45
+ const [lon, lat] = center;
46
+ return {
47
+ ...map,
48
+ settings: {
49
+ ...map.settings,
50
+ center: { lon, lat }
51
+ }
52
+ };
53
+ }
54
+ return map;
55
+ }
56
+ function normalizeMapLayers(mapOrResponse) {
57
+ const map = extractMapDto(mapOrResponse);
58
+ if (!map?.mapLayers) {
59
+ return [];
60
+ }
61
+ const normalized = [];
62
+ map.mapLayers.forEach((mapLayer, index) => {
63
+ const layerId = resolveLayerId(mapLayer);
64
+ if (layerId === void 0) {
65
+ return;
66
+ }
67
+ const embeddedLayer = mapLayer.layer;
68
+ const visible = mapLayer.isVisible !== false;
69
+ const opacity = clampOpacity(toNumber(mapLayer.opacity) ?? 1);
70
+ const layerType = embeddedLayer?.layerType ?? mapLayer.layerType;
71
+ const geometryType = embeddedLayer?.geometryType ?? mapLayer.geometryType;
72
+ const layerName = embeddedLayer?.name ?? mapLayer.name;
73
+ const orderCandidate = toNumber(mapLayer.displayOrder) ?? toNumber(mapLayer.order) ?? toNumber(embeddedLayer?.displayOrder) ?? index;
74
+ normalized.push({
75
+ layerId,
76
+ displayOrder: orderCandidate ?? index,
77
+ isVisible: visible,
78
+ opacity,
79
+ layer: embeddedLayer,
80
+ layerType,
81
+ geometryType,
82
+ layerName,
83
+ mapLayer
84
+ });
85
+ });
86
+ return normalized.sort((a, b) => a.displayOrder - b.displayOrder);
87
+ }
88
+ function computeBBoxFromFeature(feature) {
89
+ if (!feature || typeof feature !== "object") return null;
90
+ const geometry = feature.geometry;
91
+ if (!geometry || !("coordinates" in geometry)) return null;
92
+ const coords = [];
93
+ const collect = (candidate) => {
94
+ if (!Array.isArray(candidate)) return;
95
+ if (candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number") {
96
+ coords.push([candidate[0], candidate[1]]);
97
+ return;
98
+ }
99
+ candidate.forEach((entry) => collect(entry));
100
+ };
101
+ collect(geometry.coordinates);
102
+ if (coords.length === 0) return null;
103
+ const [firstLon, firstLat] = coords[0];
104
+ const bbox = { minLon: firstLon, minLat: firstLat, maxLon: firstLon, maxLat: firstLat };
105
+ coords.forEach(([lon, lat]) => {
106
+ bbox.minLon = Math.min(bbox.minLon, lon);
107
+ bbox.minLat = Math.min(bbox.minLat, lat);
108
+ bbox.maxLon = Math.max(bbox.maxLon, lon);
109
+ bbox.maxLat = Math.max(bbox.maxLat, lat);
110
+ });
111
+ return bbox;
112
+ }
113
+ function centroidFromBBox(bbox) {
114
+ return [(bbox.minLat + bbox.maxLat) / 2, (bbox.minLon + bbox.maxLon) / 2];
115
+ }
116
+ function applyFilteredGeoJSONStrategy(options) {
117
+ const layerControls = options.normalizedLayers.map((entry) => ({
118
+ layerId: entry.layerId,
119
+ visible: entry.isVisible,
120
+ opacity: entry.opacity
121
+ }));
122
+ return {
123
+ overlay: options.filteredGeojson,
124
+ metadata: options.metadata,
125
+ layerControls
126
+ };
127
+ }
128
+
129
+ // src/engine/LayerStateEngine.ts
130
+ function clampOpacity2(value) {
131
+ if (typeof value === "number" && Number.isFinite(value)) {
132
+ return Math.min(1, Math.max(0, value));
133
+ }
134
+ if (typeof value === "string") {
135
+ const parsed = parseFloat(value);
136
+ if (Number.isFinite(parsed)) {
137
+ return Math.min(1, Math.max(0, parsed));
138
+ }
139
+ }
140
+ return 1;
141
+ }
142
+ var resolveBaseVisibility = (_layer) => false;
143
+ function resolveBaseOpacity(layer) {
144
+ if ("opacity" in layer) {
145
+ return clampOpacity2(layer.opacity);
146
+ }
147
+ return 1;
148
+ }
149
+ function toEffectiveState(base, override) {
150
+ const overrideVisible = override?.overrideVisible ?? void 0;
151
+ const overrideOpacity = override?.overrideOpacity ?? void 0;
152
+ const opacitySource = overrideOpacity !== void 0 && overrideOpacity !== null ? overrideOpacity : base.baseOpacity;
153
+ return {
154
+ layerId: base.layerId,
155
+ baseVisible: base.baseVisible,
156
+ baseOpacity: base.baseOpacity,
157
+ overrideVisible,
158
+ overrideOpacity,
159
+ visible: overrideVisible ?? base.baseVisible,
160
+ opacity: clampOpacity2(opacitySource)
161
+ };
162
+ }
163
+ function initLayerStates(mapLayers) {
164
+ const bases = mapLayers.filter((layer) => layer.layerId !== void 0).map((layer) => ({
165
+ layerId: layer.layerId,
166
+ baseVisible: resolveBaseVisibility(layer),
167
+ baseOpacity: resolveBaseOpacity(layer)
168
+ }));
169
+ return bases.map((base) => toEffectiveState(base));
170
+ }
171
+ function applyLayerOverrides(states, overrides) {
172
+ const overrideMap = /* @__PURE__ */ new Map();
173
+ overrides.forEach((entry) => overrideMap.set(entry.layerId, entry));
174
+ return states.map((state) => {
175
+ const nextOverride = overrideMap.get(state.layerId);
176
+ const mergedOverrideVisible = nextOverride && "overrideVisible" in nextOverride && nextOverride.overrideVisible !== void 0 ? nextOverride.overrideVisible : state.overrideVisible;
177
+ const mergedOverrideOpacity = nextOverride && "overrideOpacity" in nextOverride && nextOverride.overrideOpacity !== void 0 ? nextOverride.overrideOpacity : state.overrideOpacity;
178
+ return toEffectiveState(state, {
179
+ layerId: state.layerId,
180
+ overrideVisible: mergedOverrideVisible ?? void 0,
181
+ overrideOpacity: mergedOverrideOpacity ?? void 0
182
+ });
183
+ });
184
+ }
185
+ function resetOverrides(states) {
186
+ return states.map((state) => toEffectiveState(state));
187
+ }
188
+
189
+ // src/react/layerStyleHelpers.ts
190
+ function resolveLayerAccent(style) {
191
+ if (!style) return null;
192
+ return style.fillColor ?? style.color ?? null;
193
+ }
194
+ function getLayerColor(style, fallback = "#94a3b8") {
195
+ return resolveLayerAccent(style) ?? fallback;
196
+ }
197
+ function getStyleByLayerId(layerId, mapLayers) {
198
+ if (!mapLayers) return null;
199
+ const match = mapLayers.find((ml) => String(ml.layerId) === String(layerId));
200
+ return match?.style ?? null;
201
+ }
202
+ function getAccentByLayerId(layerId, mapLayers) {
203
+ const style = getStyleByLayerId(layerId, mapLayers);
204
+ return resolveLayerAccent(style);
205
+ }
206
+
207
+ // src/react/zoomOpacity.ts
208
+ var DEFAULT_OPTIONS = {
209
+ minZoom: 10,
210
+ maxZoom: 17,
211
+ minFactor: 0.6,
212
+ maxFactor: 1,
213
+ minOpacity: 0.1,
214
+ maxOpacity: 0.92
215
+ };
216
+ function clampNumber(value, min, max) {
217
+ return Math.min(max, Math.max(min, value));
218
+ }
219
+ function clampOpacity3(value) {
220
+ return clampNumber(value, 0, 1);
221
+ }
222
+ function isPolygonLayer(layerType, geometryType) {
223
+ const candidate = (layerType ?? geometryType ?? "").toLowerCase();
224
+ return candidate === "polygon" || candidate === "multipolygon";
225
+ }
226
+ function getZoomOpacityFactor(zoom, options) {
227
+ const settings = { ...DEFAULT_OPTIONS, ...options };
228
+ const { minZoom, maxZoom, minFactor, maxFactor } = settings;
229
+ if (maxZoom <= minZoom) return clampNumber(maxFactor, minFactor, maxFactor);
230
+ const t = clampNumber((zoom - minZoom) / (maxZoom - minZoom), 0, 1);
231
+ const factor = maxFactor + (minFactor - maxFactor) * t;
232
+ return clampNumber(factor, minFactor, maxFactor);
233
+ }
234
+ function getLayerZoomOpacityFactor(zoom, layerType, geometryType, options) {
235
+ if (!isPolygonLayer(layerType, geometryType)) return 1;
236
+ return getZoomOpacityFactor(zoom, options);
237
+ }
238
+ function getEffectiveLayerOpacity(baseOpacity, zoom, layerType, geometryType, options) {
239
+ if (!isPolygonLayer(layerType, geometryType)) {
240
+ return clampOpacity3(baseOpacity);
241
+ }
242
+ const settings = { ...DEFAULT_OPTIONS, ...options };
243
+ const factor = getZoomOpacityFactor(zoom, options);
244
+ const effective = clampOpacity3(baseOpacity) * factor;
245
+ return clampNumber(effective, settings.minOpacity, settings.maxOpacity);
246
+ }
247
+
248
+ // src/react/ZenitMap.tsx
249
+ import { jsx, jsxs } from "react/jsx-runtime";
250
+ var DEFAULT_CENTER = [0, 0];
251
+ var DEFAULT_ZOOM = 3;
252
+ function computeBBoxFromGeojson(geojson) {
253
+ if (!geojson) return null;
254
+ if (!Array.isArray(geojson.features) || geojson.features.length === 0) return null;
255
+ const coords = [];
256
+ const collect = (candidate) => {
257
+ if (!Array.isArray(candidate)) return;
258
+ if (candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number") {
259
+ coords.push([candidate[0], candidate[1]]);
260
+ return;
261
+ }
262
+ candidate.forEach((entry) => collect(entry));
263
+ };
264
+ geojson.features.forEach((feature) => {
265
+ collect(feature.geometry?.coordinates);
266
+ });
267
+ if (coords.length === 0) return null;
268
+ const [firstLon, firstLat] = coords[0];
269
+ const bbox = { minLon: firstLon, minLat: firstLat, maxLon: firstLon, maxLat: firstLat };
270
+ coords.forEach(([lon, lat]) => {
271
+ bbox.minLon = Math.min(bbox.minLon, lon);
272
+ bbox.minLat = Math.min(bbox.minLat, lat);
273
+ bbox.maxLon = Math.max(bbox.maxLon, lon);
274
+ bbox.maxLat = Math.max(bbox.maxLat, lat);
275
+ });
276
+ return bbox;
277
+ }
278
+ function mergeBBoxes(bboxes) {
279
+ const valid = bboxes.filter((bbox) => !!bbox);
280
+ if (valid.length === 0) return null;
281
+ const first = valid[0];
282
+ return valid.slice(1).reduce(
283
+ (acc, bbox) => ({
284
+ minLon: Math.min(acc.minLon, bbox.minLon),
285
+ minLat: Math.min(acc.minLat, bbox.minLat),
286
+ maxLon: Math.max(acc.maxLon, bbox.maxLon),
287
+ maxLat: Math.max(acc.maxLat, bbox.maxLat)
288
+ }),
289
+ { ...first }
290
+ );
291
+ }
292
+ function getFeatureLayerId(feature) {
293
+ const layerId = feature?.properties?.__zenit_layerId ?? feature?.properties?.layerId ?? feature?.properties?.layer_id;
294
+ if (layerId === void 0 || layerId === null) return null;
295
+ return layerId;
296
+ }
297
+ function escapeHtml(value) {
298
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
299
+ }
300
+ function withAlpha(color, alpha) {
301
+ const trimmed = color.trim();
302
+ if (trimmed.startsWith("#")) {
303
+ const hex = trimmed.replace("#", "");
304
+ const expanded = hex.length === 3 ? hex.split("").map((char) => `${char}${char}`).join("") : hex;
305
+ if (expanded.length === 6) {
306
+ const r = parseInt(expanded.slice(0, 2), 16);
307
+ const g = parseInt(expanded.slice(2, 4), 16);
308
+ const b = parseInt(expanded.slice(4, 6), 16);
309
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
310
+ }
311
+ }
312
+ if (trimmed.startsWith("rgb(")) {
313
+ const inner = trimmed.slice(4, -1);
314
+ return `rgba(${inner}, ${alpha})`;
315
+ }
316
+ if (trimmed.startsWith("rgba(")) {
317
+ const inner = trimmed.slice(5, -1).split(",").slice(0, 3).map((value) => value.trim());
318
+ return `rgba(${inner.join(", ")}, ${alpha})`;
319
+ }
320
+ return color;
321
+ }
322
+ function getRgbFromColor(color) {
323
+ const trimmed = color.trim();
324
+ if (trimmed.startsWith("#")) {
325
+ const hex = trimmed.replace("#", "");
326
+ const expanded = hex.length === 3 ? hex.split("").map((char) => `${char}${char}`).join("") : hex;
327
+ if (expanded.length === 6) {
328
+ const r = parseInt(expanded.slice(0, 2), 16);
329
+ const g = parseInt(expanded.slice(2, 4), 16);
330
+ const b = parseInt(expanded.slice(4, 6), 16);
331
+ return { r, g, b };
332
+ }
333
+ }
334
+ const rgbMatch = trimmed.match(/rgba?\(([^)]+)\)/i);
335
+ if (rgbMatch) {
336
+ const [r, g, b] = rgbMatch[1].split(",").map((value) => parseFloat(value.trim())).slice(0, 3);
337
+ if ([r, g, b].every((value) => Number.isFinite(value))) {
338
+ return { r, g, b };
339
+ }
340
+ }
341
+ return null;
342
+ }
343
+ function getLabelTextStyles(color) {
344
+ const rgb = getRgbFromColor(color);
345
+ if (!rgb) {
346
+ return { color: "#0f172a", shadow: "0 1px 2px rgba(255, 255, 255, 0.6)" };
347
+ }
348
+ const luminance = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255;
349
+ if (luminance > 0.6) {
350
+ return { color: "#0f172a", shadow: "0 1px 2px rgba(255, 255, 255, 0.7)" };
351
+ }
352
+ return { color: "#ffffff", shadow: "0 1px 2px rgba(0, 0, 0, 0.4)" };
353
+ }
354
+ function getFeatureStyleOverrides(feature) {
355
+ const candidate = feature?.properties?._style;
356
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return null;
357
+ const styleCandidate = candidate;
358
+ return {
359
+ color: styleCandidate.color,
360
+ weight: styleCandidate.weight,
361
+ fillColor: styleCandidate.fillColor,
362
+ fillOpacity: styleCandidate.fillOpacity
363
+ };
364
+ }
365
+ function buildFeaturePopupHtml(feature) {
366
+ const properties = feature?.properties;
367
+ if (!properties) return null;
368
+ const layerName = properties.layerName ?? properties.layer_name ?? properties.name;
369
+ const descripcion = properties.descripcion ?? properties.description;
370
+ const reservedKeys = /* @__PURE__ */ new Set([
371
+ "_style",
372
+ "layerId",
373
+ "layer_id",
374
+ "__zenit_layerId",
375
+ "layerName",
376
+ "layer_name",
377
+ "name",
378
+ "descripcion",
379
+ "description"
380
+ ]);
381
+ const extraEntries = Object.entries(properties).filter(([key, value]) => {
382
+ if (reservedKeys.has(key)) return false;
383
+ return ["string", "number", "boolean"].includes(typeof value);
384
+ }).slice(0, 5);
385
+ if (!layerName && !descripcion && extraEntries.length === 0) return null;
386
+ const parts = [];
387
+ if (layerName) {
388
+ parts.push(`<div style="font-weight:600;margin-bottom:4px;">${escapeHtml(layerName)}</div>`);
389
+ }
390
+ if (descripcion) {
391
+ parts.push(`<div style="margin-bottom:6px;">${escapeHtml(descripcion)}</div>`);
392
+ }
393
+ if (extraEntries.length > 0) {
394
+ const rows = extraEntries.map(([key, value]) => {
395
+ const label = escapeHtml(key.replace(/_/g, " "));
396
+ const val = escapeHtml(String(value));
397
+ return `<div><strong>${label}:</strong> ${val}</div>`;
398
+ }).join("");
399
+ parts.push(`<div style="font-size:12px;line-height:1.4;">${rows}</div>`);
400
+ }
401
+ return `<div>${parts.join("")}</div>`;
402
+ }
403
+ function normalizeCenterTuple(center) {
404
+ if (!center) return null;
405
+ if (Array.isArray(center) && center.length >= 2) {
406
+ const lat = center[0];
407
+ const lon = center[1];
408
+ if (typeof lat === "number" && typeof lon === "number") return [lat, lon];
409
+ return null;
410
+ }
411
+ const maybeObj = center;
412
+ if (typeof maybeObj.lat === "number" && typeof maybeObj.lon === "number") {
413
+ return [maybeObj.lat, maybeObj.lon];
414
+ }
415
+ return null;
416
+ }
417
+ var FitToBounds = ({ bbox }) => {
418
+ const mapInstance = useMap();
419
+ const lastAppliedBBox = useRef(null);
420
+ useEffect(() => {
421
+ const targetBBox = bbox;
422
+ if (!targetBBox) return;
423
+ const serialized = JSON.stringify(targetBBox);
424
+ if (lastAppliedBBox.current === serialized) return;
425
+ const bounds = [
426
+ [targetBBox.minLat, targetBBox.minLon],
427
+ [targetBBox.maxLat, targetBBox.maxLon]
428
+ ];
429
+ mapInstance.fitBounds(bounds, { padding: [12, 12] });
430
+ lastAppliedBBox.current = serialized;
431
+ }, [bbox, mapInstance]);
432
+ return null;
433
+ };
434
+ var AutoFitToBounds = ({
435
+ bbox,
436
+ enabled = true
437
+ }) => {
438
+ const mapInstance = useMap();
439
+ const lastAutoBBoxApplied = useRef(null);
440
+ const lastUserInteracted = useRef(false);
441
+ useEffect(() => {
442
+ if (!enabled) return;
443
+ const handleInteraction = () => {
444
+ lastUserInteracted.current = true;
445
+ };
446
+ mapInstance.on("dragstart", handleInteraction);
447
+ mapInstance.on("zoomstart", handleInteraction);
448
+ return () => {
449
+ mapInstance.off("dragstart", handleInteraction);
450
+ mapInstance.off("zoomstart", handleInteraction);
451
+ };
452
+ }, [enabled, mapInstance]);
453
+ useEffect(() => {
454
+ if (!enabled) return;
455
+ if (!bbox) return;
456
+ const serialized = JSON.stringify(bbox);
457
+ if (lastAutoBBoxApplied.current === serialized) return;
458
+ if (lastUserInteracted.current) {
459
+ lastUserInteracted.current = false;
460
+ }
461
+ const bounds = [
462
+ [bbox.minLat, bbox.minLon],
463
+ [bbox.maxLat, bbox.maxLon]
464
+ ];
465
+ mapInstance.fitBounds(bounds, { padding: [12, 12] });
466
+ lastAutoBBoxApplied.current = serialized;
467
+ }, [bbox, enabled, mapInstance]);
468
+ return null;
469
+ };
470
+ var ZoomBasedOpacityHandler = ({ onZoomChange }) => {
471
+ const mapInstance = useMap();
472
+ useEffect(() => {
473
+ const handleZoom = () => {
474
+ onZoomChange(mapInstance.getZoom());
475
+ };
476
+ mapInstance.on("zoomend", handleZoom);
477
+ handleZoom();
478
+ return () => {
479
+ mapInstance.off("zoomend", handleZoom);
480
+ };
481
+ }, [mapInstance, onZoomChange]);
482
+ return null;
483
+ };
484
+ var MapInstanceBridge = ({ onReady }) => {
485
+ const mapInstance = useMap();
486
+ useEffect(() => {
487
+ onReady(mapInstance);
488
+ }, [mapInstance, onReady]);
489
+ return null;
490
+ };
491
+ var ZenitMap = forwardRef(({
492
+ client,
493
+ mapId,
494
+ height = "500px",
495
+ width = "100%",
496
+ initialZoom,
497
+ initialCenter,
498
+ showLayerPanel = true,
499
+ overlayGeojson,
500
+ overlayStyle,
501
+ layerControls,
502
+ layerStates,
503
+ layerGeojson,
504
+ onLayerStateChange,
505
+ onError,
506
+ onLoadingChange,
507
+ onFeatureClick,
508
+ onFeatureHover,
509
+ featureInfoMode = "popup",
510
+ mapLayers,
511
+ zoomToBbox,
512
+ zoomToGeojson,
513
+ onZoomChange,
514
+ onMapReady
515
+ }, ref) => {
516
+ const [map, setMap] = useState(null);
517
+ const [layers, setLayers] = useState([]);
518
+ const [effectiveStates, setEffectiveStates] = useState([]);
519
+ const [loadingMap, setLoadingMap] = useState(false);
520
+ const [mapError, setMapError] = useState(null);
521
+ const [mapInstance, setMapInstance] = useState(null);
522
+ const [currentZoom, setCurrentZoom] = useState(initialZoom ?? DEFAULT_ZOOM);
523
+ const [isMobile, setIsMobile] = useState(() => {
524
+ if (typeof window === "undefined") return false;
525
+ return window.matchMedia("(max-width: 768px)").matches;
526
+ });
527
+ const normalizedLayers = useMemo(() => normalizeMapLayers(map), [map]);
528
+ useEffect(() => {
529
+ if (typeof window === "undefined") return;
530
+ const mql = window.matchMedia("(max-width: 768px)");
531
+ const onChange = (e) => {
532
+ setIsMobile(e.matches);
533
+ };
534
+ setIsMobile(mql.matches);
535
+ if (typeof mql.addEventListener === "function") {
536
+ mql.addEventListener("change", onChange);
537
+ return () => mql.removeEventListener("change", onChange);
538
+ }
539
+ const legacy = mql;
540
+ if (typeof legacy.addListener === "function") {
541
+ legacy.addListener(onChange);
542
+ return () => legacy.removeListener(onChange);
543
+ }
544
+ return;
545
+ }, []);
546
+ const layerStyleIndex = useMemo(() => {
547
+ const index = /* @__PURE__ */ new Map();
548
+ (map?.mapLayers ?? []).forEach((entry) => {
549
+ const layerStyle = entry.layer?.style ?? entry.mapLayer?.layer?.style ?? entry.style ?? null;
550
+ const layerId = entry.layerId ?? entry.mapLayer?.layerId ?? entry.mapLayer?.layer?.id ?? entry.layer?.id;
551
+ if (layerId !== void 0 && layerId !== null && layerStyle) {
552
+ index.set(String(layerId), layerStyle);
553
+ }
554
+ });
555
+ return index;
556
+ }, [map]);
557
+ const labelKeyIndex = useMemo(() => {
558
+ const index = /* @__PURE__ */ new Map();
559
+ normalizedLayers.forEach((entry) => {
560
+ const label = entry.layer?.label ?? entry.mapLayer?.label ?? entry.mapLayer.layerConfig?.label;
561
+ if (typeof label === "string" && label.trim().length > 0) {
562
+ index.set(String(entry.layerId), label);
563
+ }
564
+ });
565
+ return index;
566
+ }, [normalizedLayers]);
567
+ const layerMetaIndex = useMemo(() => {
568
+ const index = /* @__PURE__ */ new Map();
569
+ normalizedLayers.forEach((entry) => {
570
+ index.set(String(entry.layerId), {
571
+ layerType: typeof entry.layer?.layerType === "string" ? entry.layer.layerType : typeof entry.mapLayer.layerType === "string" ? entry.mapLayer.layerType : void 0,
572
+ geometryType: typeof entry.layer?.geometryType === "string" ? entry.layer.geometryType : typeof entry.mapLayer.layerConfig?.geometryType === "string" ? entry.mapLayer.layerConfig.geometryType : void 0
573
+ });
574
+ });
575
+ return index;
576
+ }, [normalizedLayers]);
577
+ const overlayStyleFunction = useMemo(() => {
578
+ return (feature) => {
579
+ const featureLayerId = getFeatureLayerId(feature);
580
+ const featureStyleOverrides = getFeatureStyleOverrides(feature);
581
+ let style = null;
582
+ if (featureLayerId !== null) {
583
+ style = getStyleByLayerId(featureLayerId, mapLayers) ?? layerStyleIndex.get(String(featureLayerId)) ?? null;
584
+ }
585
+ const resolvedStyle = featureStyleOverrides ? { ...style ?? {}, ...featureStyleOverrides } : style;
586
+ const defaultOptions = {
587
+ color: resolvedStyle?.color ?? resolvedStyle?.fillColor ?? "#2563eb",
588
+ weight: resolvedStyle?.weight ?? 2,
589
+ fillColor: resolvedStyle?.fillColor ?? resolvedStyle?.color ?? "#2563eb",
590
+ fillOpacity: resolvedStyle?.fillOpacity ?? 0.15,
591
+ ...overlayStyle ?? {}
592
+ };
593
+ return defaultOptions;
594
+ };
595
+ }, [layerStyleIndex, mapLayers, overlayStyle]);
596
+ const [baseStates, setBaseStates] = useState([]);
597
+ const [mapOverrides, setMapOverrides] = useState([]);
598
+ const [controlOverrides, setControlOverrides] = useState([]);
599
+ const [uiOverrides, setUiOverrides] = useState([]);
600
+ useEffect(() => {
601
+ let isMounted = true;
602
+ setLoadingMap(true);
603
+ setMapError(null);
604
+ onLoadingChange?.("map", true);
605
+ client.maps.getMap(mapId, true).then((mapResponse) => {
606
+ if (!isMounted) return;
607
+ const extracted = extractMapDto(mapResponse);
608
+ const resolved = extracted ? normalizeMapCenter(extracted) : null;
609
+ setMap(resolved);
610
+ }).catch((err) => {
611
+ if (!isMounted) return;
612
+ setMapError(err instanceof Error ? err.message : "No se pudo obtener el mapa");
613
+ onError?.(err, "map");
614
+ }).finally(() => {
615
+ if (!isMounted) return;
616
+ setLoadingMap(false);
617
+ onLoadingChange?.("map", false);
618
+ });
619
+ return () => {
620
+ isMounted = false;
621
+ };
622
+ }, [client.maps, mapId, onError, onLoadingChange]);
623
+ useEffect(() => {
624
+ if (normalizedLayers.length === 0) {
625
+ setLayers([]);
626
+ setBaseStates([]);
627
+ setMapOverrides([]);
628
+ setUiOverrides([]);
629
+ return;
630
+ }
631
+ const nextLayers = normalizedLayers.map((entry) => ({
632
+ mapLayer: { ...entry.mapLayer, layerId: entry.layerId },
633
+ layer: entry.layer,
634
+ displayOrder: entry.displayOrder
635
+ }));
636
+ const base = initLayerStates(
637
+ normalizedLayers.map((entry) => ({
638
+ ...entry.mapLayer,
639
+ layerId: entry.layerId,
640
+ opacity: entry.opacity,
641
+ isVisible: entry.isVisible
642
+ }))
643
+ );
644
+ const initialOverrides = normalizedLayers.map((entry) => ({
645
+ layerId: entry.layerId,
646
+ overrideVisible: entry.isVisible,
647
+ overrideOpacity: entry.opacity
648
+ }));
649
+ setLayers(nextLayers);
650
+ setBaseStates(base);
651
+ setMapOverrides(initialOverrides);
652
+ setUiOverrides([]);
653
+ }, [normalizedLayers]);
654
+ useEffect(() => {
655
+ if (!layerControls) {
656
+ setControlOverrides([]);
657
+ return;
658
+ }
659
+ const overrides = layerControls.map((entry) => ({
660
+ layerId: entry.layerId,
661
+ overrideVisible: entry.visible,
662
+ overrideOpacity: entry.opacity
663
+ }));
664
+ setControlOverrides(overrides);
665
+ }, [layerControls]);
666
+ useEffect(() => {
667
+ if (layerStates) {
668
+ return;
669
+ }
670
+ if (Array.isArray(layerControls) && layerControls.length === 0 && effectiveStates.length > 0) {
671
+ const reset = resetOverrides(baseStates);
672
+ setEffectiveStates(reset);
673
+ onLayerStateChange?.(reset);
674
+ }
675
+ }, [baseStates, effectiveStates.length, layerControls, layerStates, onLayerStateChange]);
676
+ useEffect(() => {
677
+ if (layerStates) {
678
+ setEffectiveStates(layerStates);
679
+ }
680
+ }, [layerStates]);
681
+ useEffect(() => {
682
+ if (layerStates) {
683
+ return;
684
+ }
685
+ if (mapOverrides.length === 0 && controlOverrides.length === 0 && uiOverrides.length === 0) {
686
+ setEffectiveStates(baseStates);
687
+ return;
688
+ }
689
+ const combined = [...mapOverrides, ...controlOverrides, ...uiOverrides];
690
+ const next = applyLayerOverrides(baseStates, combined);
691
+ setEffectiveStates(next);
692
+ onLayerStateChange?.(next);
693
+ }, [baseStates, controlOverrides, layerStates, mapOverrides, onLayerStateChange, uiOverrides]);
694
+ useEffect(() => {
695
+ if (!Array.isArray(layerControls) || layerControls.length > 0) return;
696
+ setUiOverrides([]);
697
+ }, [layerControls]);
698
+ useEffect(() => {
699
+ if (layerStates) {
700
+ return;
701
+ }
702
+ if (!Array.isArray(layerControls) && effectiveStates.length > 0 && baseStates.length > 0) {
703
+ const next = resetOverrides(baseStates);
704
+ setEffectiveStates(next);
705
+ onLayerStateChange?.(next);
706
+ }
707
+ }, [baseStates, effectiveStates.length, layerControls, layerStates, onLayerStateChange]);
708
+ const upsertUiOverride = (layerId, patch) => {
709
+ setUiOverrides((prev) => {
710
+ const filtered = prev.filter((entry) => entry.layerId !== layerId);
711
+ const nextEntry = {
712
+ layerId,
713
+ ...patch
714
+ };
715
+ return [...filtered, nextEntry];
716
+ });
717
+ };
718
+ const updateOpacityFromUi = useCallback(
719
+ (layerId, uiOpacity) => {
720
+ const meta = layerMetaIndex.get(String(layerId));
721
+ const zoomFactor = getLayerZoomOpacityFactor(
722
+ currentZoom,
723
+ meta?.layerType,
724
+ meta?.geometryType
725
+ );
726
+ const baseOpacity = clampOpacity3(uiOpacity / zoomFactor);
727
+ const effectiveOpacity = getEffectiveLayerOpacity(
728
+ baseOpacity,
729
+ currentZoom,
730
+ meta?.layerType,
731
+ meta?.geometryType
732
+ );
733
+ if (layerStates && onLayerStateChange) {
734
+ const next = effectiveStates.map(
735
+ (state) => state.layerId === layerId ? {
736
+ ...state,
737
+ baseOpacity,
738
+ opacity: effectiveOpacity,
739
+ overrideOpacity: uiOpacity
740
+ } : state
741
+ );
742
+ onLayerStateChange(next);
743
+ return;
744
+ }
745
+ setBaseStates(
746
+ (prev) => prev.map((state) => state.layerId === layerId ? { ...state, baseOpacity } : state)
747
+ );
748
+ setUiOverrides((prev) => {
749
+ const existing = prev.find((entry) => entry.layerId === layerId);
750
+ const filtered = prev.filter((entry) => entry.layerId !== layerId);
751
+ if (existing && existing.overrideVisible !== void 0) {
752
+ return [
753
+ ...filtered,
754
+ { layerId, overrideVisible: existing.overrideVisible }
755
+ ];
756
+ }
757
+ return filtered;
758
+ });
759
+ },
760
+ [currentZoom, effectiveStates, layerMetaIndex, layerStates, onLayerStateChange]
761
+ );
762
+ const center = useMemo(() => {
763
+ if (initialCenter) {
764
+ return initialCenter;
765
+ }
766
+ const normalized = normalizeCenterTuple(map?.settings?.center);
767
+ if (normalized) {
768
+ return normalized;
769
+ }
770
+ return DEFAULT_CENTER;
771
+ }, [initialCenter, map?.settings?.center]);
772
+ const zoom = initialZoom ?? map?.settings?.zoom ?? DEFAULT_ZOOM;
773
+ useEffect(() => {
774
+ setCurrentZoom(zoom);
775
+ }, [zoom]);
776
+ const decoratedLayers = useMemo(() => {
777
+ return layers.map((layer) => ({
778
+ ...layer,
779
+ effective: effectiveStates.find((state) => state.layerId === layer.mapLayer.layerId),
780
+ data: layerGeojson?.[layer.mapLayer.layerId] ?? layerGeojson?.[String(layer.mapLayer.layerId)] ?? null
781
+ }));
782
+ }, [effectiveStates, layerGeojson, layers]);
783
+ const orderedLayers = useMemo(() => {
784
+ return [...decoratedLayers].filter((layer) => layer.effective?.visible && layer.data).sort((a, b) => a.displayOrder - b.displayOrder);
785
+ }, [decoratedLayers]);
786
+ const explicitZoomBBox = useMemo(() => {
787
+ if (zoomToBbox) return zoomToBbox;
788
+ if (zoomToGeojson) return computeBBoxFromGeojson(zoomToGeojson);
789
+ return null;
790
+ }, [zoomToBbox, zoomToGeojson]);
791
+ const autoZoomBBox = useMemo(() => {
792
+ if (explicitZoomBBox) return null;
793
+ const visibleBBoxes = orderedLayers.map((layer) => computeBBoxFromGeojson(layer.data));
794
+ return mergeBBoxes(visibleBBoxes);
795
+ }, [explicitZoomBBox, orderedLayers]);
796
+ const resolveLayerStyle = useCallback(
797
+ (layerId) => {
798
+ return getStyleByLayerId(layerId, mapLayers) ?? layerStyleIndex.get(String(layerId)) ?? null;
799
+ },
800
+ [layerStyleIndex, mapLayers]
801
+ );
802
+ const labelMarkers = useMemo(() => {
803
+ const markers = [];
804
+ decoratedLayers.forEach((layerState) => {
805
+ if (!layerState.effective?.visible) return;
806
+ const labelKey = labelKeyIndex.get(String(layerState.mapLayer.layerId));
807
+ if (!labelKey) return;
808
+ const data = layerState.data;
809
+ if (!data) return;
810
+ const resolvedStyle = resolveLayerStyle(layerState.mapLayer.layerId);
811
+ const layerColor = resolvedStyle?.fillColor ?? resolvedStyle?.color ?? "rgba(37, 99, 235, 1)";
812
+ const opacity = layerState.effective?.opacity ?? 1;
813
+ data.features.forEach((feature, index) => {
814
+ const properties = feature.properties;
815
+ const value = properties?.[labelKey];
816
+ if (value === void 0 || value === null || value === "") return;
817
+ const bbox = computeBBoxFromFeature(feature);
818
+ if (!bbox) return;
819
+ const [lat, lng] = centroidFromBBox(bbox);
820
+ markers.push({
821
+ key: `${layerState.mapLayer.layerId}-label-${index}`,
822
+ position: [lat, lng],
823
+ label: String(value),
824
+ opacity,
825
+ layerId: layerState.mapLayer.layerId,
826
+ color: layerColor
827
+ });
828
+ });
829
+ });
830
+ return markers;
831
+ }, [decoratedLayers, labelKeyIndex, resolveLayerStyle]);
832
+ const ensureLayerPanes = useCallback(
833
+ (targetMap, targetLayers) => {
834
+ const baseZIndex = 400;
835
+ targetLayers.forEach((layer) => {
836
+ const paneName = `zenit-layer-${layer.layerId}`;
837
+ const pane = targetMap.getPane(paneName) ?? targetMap.createPane(paneName);
838
+ const order = Number.isFinite(layer.displayOrder) ? layer.displayOrder : 0;
839
+ pane.style.zIndex = String(baseZIndex + order);
840
+ });
841
+ },
842
+ []
843
+ );
844
+ const handleMapReady = useCallback(
845
+ (instance) => {
846
+ setMapInstance(instance);
847
+ onMapReady?.(instance);
848
+ },
849
+ [onMapReady]
850
+ );
851
+ useEffect(() => {
852
+ if (!mapInstance) {
853
+ return;
854
+ }
855
+ if (orderedLayers.length === 0) {
856
+ return;
857
+ }
858
+ const layerTargets = orderedLayers.map((layer) => ({
859
+ layerId: layer.mapLayer.layerId,
860
+ displayOrder: layer.displayOrder
861
+ }));
862
+ ensureLayerPanes(mapInstance, layerTargets);
863
+ }, [mapInstance, orderedLayers, ensureLayerPanes]);
864
+ const overlayOnEachFeature = useMemo(() => {
865
+ return (feature, layer) => {
866
+ const layerId = getFeatureLayerId(feature) ?? void 0;
867
+ const geometryType = feature?.geometry?.type;
868
+ const isPointFeature = geometryType === "Point" || geometryType === "MultiPoint" || layer instanceof L.CircleMarker;
869
+ const originalStyle = layer instanceof L.Path ? {
870
+ color: layer.options.color,
871
+ weight: layer.options.weight,
872
+ fillColor: layer.options.fillColor,
873
+ opacity: layer.options.opacity,
874
+ fillOpacity: layer.options.fillOpacity
875
+ } : null;
876
+ const originalRadius = layer instanceof L.CircleMarker ? layer.getRadius() : null;
877
+ if (featureInfoMode === "popup") {
878
+ const content = buildFeaturePopupHtml(feature);
879
+ if (content) {
880
+ layer.bindPopup(content, { maxWidth: 320 });
881
+ }
882
+ }
883
+ if (isPointFeature && layer.bindTooltip) {
884
+ layer.bindTooltip("Click para ver detalle", {
885
+ sticky: true,
886
+ direction: "top",
887
+ opacity: 0.9,
888
+ className: "zenit-map-tooltip"
889
+ });
890
+ }
891
+ layer.on("click", () => onFeatureClick?.(feature, layerId));
892
+ layer.on("mouseover", () => {
893
+ if (layer instanceof L.Path && originalStyle) {
894
+ layer.setStyle({
895
+ ...originalStyle,
896
+ weight: (originalStyle.weight ?? 2) + 1,
897
+ opacity: Math.min(1, (originalStyle.opacity ?? 1) + 0.2),
898
+ fillOpacity: Math.min(1, (originalStyle.fillOpacity ?? 0.8) + 0.1)
899
+ });
900
+ }
901
+ if (layer instanceof L.CircleMarker && typeof originalRadius === "number") {
902
+ layer.setRadius(originalRadius + 1);
903
+ }
904
+ onFeatureHover?.(feature, layerId);
905
+ });
906
+ layer.on("mouseout", () => {
907
+ if (layer instanceof L.Path && originalStyle) {
908
+ layer.setStyle(originalStyle);
909
+ }
910
+ if (layer instanceof L.CircleMarker && typeof originalRadius === "number") {
911
+ layer.setRadius(originalRadius);
912
+ }
913
+ });
914
+ };
915
+ }, [featureInfoMode, onFeatureClick, onFeatureHover]);
916
+ const buildLayerStyle = (layerId, baseOpacity, feature, layerType) => {
917
+ const style = resolveLayerStyle(layerId);
918
+ const featureStyleOverrides = getFeatureStyleOverrides(feature);
919
+ const resolvedStyle = featureStyleOverrides ? { ...style ?? {}, ...featureStyleOverrides } : style;
920
+ const geometryType = feature?.geometry?.type;
921
+ const resolvedLayerType = layerType ?? geometryType;
922
+ const sanitizedBaseOpacity = clampOpacity3(baseOpacity);
923
+ const normalizedStyleFill = typeof resolvedStyle?.fillOpacity === "number" ? clampOpacity3(resolvedStyle.fillOpacity) : 0.8;
924
+ const effectiveOpacity = getEffectiveLayerOpacity(
925
+ sanitizedBaseOpacity,
926
+ currentZoom,
927
+ resolvedLayerType,
928
+ geometryType
929
+ );
930
+ const fillOpacity = clampOpacity3(effectiveOpacity * normalizedStyleFill);
931
+ const strokeOpacity = clampOpacity3(Math.max(0.35, effectiveOpacity * 0.9));
932
+ return {
933
+ color: resolvedStyle?.color ?? resolvedStyle?.fillColor ?? "#2563eb",
934
+ weight: resolvedStyle?.weight ?? 2,
935
+ fillColor: resolvedStyle?.fillColor ?? resolvedStyle?.color ?? "#2563eb",
936
+ opacity: strokeOpacity,
937
+ fillOpacity
938
+ };
939
+ };
940
+ const buildLabelIcon = useCallback((label, opacity, color) => {
941
+ const size = 60;
942
+ const innerSize = 44;
943
+ const textStyles = getLabelTextStyles(color);
944
+ const safeLabel = escapeHtml(label);
945
+ const clampedOpacity = Math.min(1, Math.max(0.92, opacity));
946
+ const innerBackground = withAlpha(color, 0.9);
947
+ return L.divIcon({
948
+ className: "zenit-label-marker",
949
+ iconSize: [size, size],
950
+ iconAnchor: [size / 2, size / 2],
951
+ html: `
952
+ <div
953
+ title="${safeLabel}"
954
+ style="
955
+ width:${size}px;
956
+ height:${size}px;
957
+ border-radius:9999px;
958
+ background:rgba(255, 255, 255, 0.95);
959
+ border:3px solid rgba(255, 255, 255, 1);
960
+ display:flex;
961
+ align-items:center;
962
+ justify-content:center;
963
+ opacity:${clampedOpacity};
964
+ box-shadow:0 2px 6px rgba(0, 0, 0, 0.25);
965
+ pointer-events:none;
966
+ "
967
+ >
968
+ <div
969
+ style="
970
+ width:${innerSize}px;
971
+ height:${innerSize}px;
972
+ border-radius:9999px;
973
+ background:${innerBackground};
974
+ display:flex;
975
+ align-items:center;
976
+ justify-content:center;
977
+ box-shadow:inset 0 0 0 1px rgba(15, 23, 42, 0.12);
978
+ "
979
+ >
980
+ <span
981
+ style="
982
+ color:${textStyles.color};
983
+ font-size:20px;
984
+ font-weight:800;
985
+ text-shadow:${textStyles.shadow};
986
+ "
987
+ >
988
+ ${safeLabel}
989
+ </span>
990
+ </div>
991
+ </div>
992
+ `
993
+ });
994
+ }, []);
995
+ useImperativeHandle(ref, () => ({
996
+ setLayerOpacity: (layerId, opacity) => {
997
+ upsertUiOverride(layerId, { overrideOpacity: opacity });
998
+ },
999
+ setLayerVisibility: (layerId, visible) => {
1000
+ upsertUiOverride(layerId, { overrideVisible: visible });
1001
+ },
1002
+ fitBounds: (bbox, options) => {
1003
+ if (!mapInstance) return;
1004
+ if (typeof bbox.minLat !== "number" || typeof bbox.minLon !== "number" || typeof bbox.maxLat !== "number" || typeof bbox.maxLon !== "number" || !Number.isFinite(bbox.minLat) || !Number.isFinite(bbox.minLon) || !Number.isFinite(bbox.maxLat) || !Number.isFinite(bbox.maxLon)) {
1005
+ console.warn("[ZenitMap.fitBounds] Invalid bbox: missing or non-finite coordinates", bbox);
1006
+ return;
1007
+ }
1008
+ const bounds = [
1009
+ [bbox.minLat, bbox.minLon],
1010
+ [bbox.maxLat, bbox.maxLon]
1011
+ ];
1012
+ const fitOptions = {
1013
+ padding: options?.padding ?? [12, 12],
1014
+ animate: options?.animate ?? true
1015
+ };
1016
+ mapInstance.fitBounds(bounds, fitOptions);
1017
+ },
1018
+ setView: (coordinates, zoom2) => {
1019
+ if (!mapInstance) return;
1020
+ mapInstance.setView([coordinates.lat, coordinates.lon], zoom2 ?? mapInstance.getZoom(), {
1021
+ animate: true
1022
+ });
1023
+ },
1024
+ getLayerSnapshot: () => {
1025
+ return effectiveStates.map((state) => ({
1026
+ layerId: state.layerId,
1027
+ visible: state.visible,
1028
+ opacity: state.opacity
1029
+ }));
1030
+ },
1031
+ restoreLayerSnapshot: (snapshot) => {
1032
+ const overrides = snapshot.map((s) => ({
1033
+ layerId: s.layerId,
1034
+ overrideVisible: s.visible,
1035
+ overrideOpacity: s.opacity
1036
+ }));
1037
+ setUiOverrides(overrides);
1038
+ },
1039
+ highlightFeature: (layerId, featureId) => {
1040
+ upsertUiOverride(layerId, { overrideVisible: true, overrideOpacity: 1 });
1041
+ },
1042
+ getMapInstance: () => mapInstance
1043
+ }), [effectiveStates, mapInstance]);
1044
+ if (loadingMap) {
1045
+ return /* @__PURE__ */ jsx("div", { style: { padding: 16, height, width }, children: "Cargando mapa..." });
1046
+ }
1047
+ if (mapError) {
1048
+ return /* @__PURE__ */ jsxs("div", { style: { padding: 16, height, width, color: "red" }, children: [
1049
+ "Error al cargar mapa: ",
1050
+ mapError
1051
+ ] });
1052
+ }
1053
+ if (!map) {
1054
+ return null;
1055
+ }
1056
+ const handleZoomChange = (zoomValue) => {
1057
+ setCurrentZoom(zoomValue);
1058
+ onZoomChange?.(zoomValue);
1059
+ };
1060
+ return /* @__PURE__ */ jsxs(
1061
+ "div",
1062
+ {
1063
+ style: {
1064
+ display: "flex",
1065
+ height,
1066
+ width,
1067
+ border: "1px solid #e2e8f0",
1068
+ borderRadius: 4,
1069
+ overflow: "hidden",
1070
+ boxSizing: "border-box"
1071
+ },
1072
+ children: [
1073
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, position: "relative" }, children: /* @__PURE__ */ jsxs(
1074
+ MapContainer,
1075
+ {
1076
+ center,
1077
+ zoom,
1078
+ style: { height: "100%", width: "100%" },
1079
+ scrollWheelZoom: true,
1080
+ zoomControl: false,
1081
+ children: [
1082
+ /* @__PURE__ */ jsx(
1083
+ TileLayer,
1084
+ {
1085
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
1086
+ attribution: "\xA9 OpenStreetMap contributors"
1087
+ }
1088
+ ),
1089
+ /* @__PURE__ */ jsx(ZoomControl, { position: "topright" }),
1090
+ /* @__PURE__ */ jsx(MapInstanceBridge, { onReady: handleMapReady }),
1091
+ /* @__PURE__ */ jsx(FitToBounds, { bbox: explicitZoomBBox ?? void 0 }),
1092
+ /* @__PURE__ */ jsx(AutoFitToBounds, { bbox: autoZoomBBox ?? void 0, enabled: !explicitZoomBBox }),
1093
+ /* @__PURE__ */ jsx(ZoomBasedOpacityHandler, { onZoomChange: handleZoomChange }),
1094
+ orderedLayers.map((layerState) => {
1095
+ const baseOpacity = layerState.effective?.baseOpacity ?? layerState.effective?.opacity ?? 1;
1096
+ const paneName = `zenit-layer-${layerState.mapLayer.layerId}`;
1097
+ const layerType = layerState.layer?.layerType ?? layerState.mapLayer.layerType ?? void 0;
1098
+ return /* @__PURE__ */ jsx(
1099
+ GeoJSON,
1100
+ {
1101
+ data: layerState.data,
1102
+ pane: mapInstance?.getPane(paneName) ? paneName : void 0,
1103
+ style: (feature) => buildLayerStyle(layerState.mapLayer.layerId, baseOpacity, feature, layerType),
1104
+ pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
1105
+ radius: isMobile ? 8 : 6,
1106
+ ...buildLayerStyle(layerState.mapLayer.layerId, baseOpacity, feature, layerType)
1107
+ }),
1108
+ onEachFeature: overlayOnEachFeature
1109
+ },
1110
+ layerState.mapLayer.layerId.toString()
1111
+ );
1112
+ }),
1113
+ overlayGeojson && /* @__PURE__ */ jsx(
1114
+ GeoJSON,
1115
+ {
1116
+ data: overlayGeojson,
1117
+ style: overlayStyleFunction,
1118
+ onEachFeature: overlayOnEachFeature
1119
+ },
1120
+ "zenit-overlay-geojson"
1121
+ ),
1122
+ labelMarkers.map((marker) => /* @__PURE__ */ jsx(
1123
+ Marker,
1124
+ {
1125
+ position: marker.position,
1126
+ icon: buildLabelIcon(marker.label, marker.opacity, marker.color),
1127
+ interactive: false
1128
+ },
1129
+ marker.key
1130
+ ))
1131
+ ]
1132
+ },
1133
+ String(mapId)
1134
+ ) }),
1135
+ showLayerPanel && decoratedLayers.length > 0 && /* @__PURE__ */ jsxs(
1136
+ "div",
1137
+ {
1138
+ style: {
1139
+ width: 260,
1140
+ borderLeft: "1px solid #e2e8f0",
1141
+ padding: "12px 16px",
1142
+ background: "#fafafa",
1143
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1144
+ fontSize: 14,
1145
+ overflowY: "auto"
1146
+ },
1147
+ children: [
1148
+ overlayGeojson && /* @__PURE__ */ jsxs(
1149
+ "div",
1150
+ {
1151
+ style: {
1152
+ border: "1px solid #bfdbfe",
1153
+ background: "#eff6ff",
1154
+ color: "#1e40af",
1155
+ padding: "8px 10px",
1156
+ borderRadius: 8,
1157
+ marginBottom: 12
1158
+ },
1159
+ children: [
1160
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "Overlay activo" }),
1161
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 13 }, children: [
1162
+ "GeoJSON externo con ",
1163
+ (overlayGeojson.features?.length ?? 0).toLocaleString(),
1164
+ " elementos."
1165
+ ] })
1166
+ ]
1167
+ }
1168
+ ),
1169
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, marginBottom: 12 }, children: "Capas" }),
1170
+ decoratedLayers.map((layerState) => /* @__PURE__ */ jsxs(
1171
+ "div",
1172
+ {
1173
+ style: { borderBottom: "1px solid #e5e7eb", paddingBottom: 10, marginBottom: 10 },
1174
+ children: [
1175
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
1176
+ /* @__PURE__ */ jsx(
1177
+ "input",
1178
+ {
1179
+ type: "checkbox",
1180
+ checked: layerState.effective?.visible ?? false,
1181
+ onChange: (e) => {
1182
+ const visible = e.target.checked;
1183
+ upsertUiOverride(layerState.mapLayer.layerId, { overrideVisible: visible });
1184
+ }
1185
+ }
1186
+ ),
1187
+ /* @__PURE__ */ jsx("span", { children: layerState.layer?.name ?? `Capa ${layerState.mapLayer.layerId}` })
1188
+ ] }),
1189
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: 8 }, children: [
1190
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 4 }, children: [
1191
+ /* @__PURE__ */ jsx("span", { style: { color: "#4a5568" }, children: "Opacidad" }),
1192
+ /* @__PURE__ */ jsxs("span", { children: [
1193
+ Math.round((layerState.effective?.opacity ?? 1) * 100),
1194
+ "%"
1195
+ ] })
1196
+ ] }),
1197
+ /* @__PURE__ */ jsx(
1198
+ "input",
1199
+ {
1200
+ type: "range",
1201
+ min: 0,
1202
+ max: 1,
1203
+ step: 0.05,
1204
+ value: layerState.effective?.opacity ?? 1,
1205
+ onChange: (e) => {
1206
+ const value = Number(e.target.value);
1207
+ updateOpacityFromUi(layerState.mapLayer.layerId, value);
1208
+ },
1209
+ style: { width: "100%" }
1210
+ }
1211
+ )
1212
+ ] })
1213
+ ]
1214
+ },
1215
+ layerState.mapLayer.layerId.toString()
1216
+ ))
1217
+ ]
1218
+ }
1219
+ )
1220
+ ]
1221
+ }
1222
+ );
1223
+ });
1224
+ ZenitMap.displayName = "ZenitMap";
1225
+
1226
+ // src/react/ZenitLayerManager.tsx
1227
+ import React2, { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
1228
+
1229
+ // src/react/icons.tsx
1230
+ import { Eye, EyeOff, ChevronLeft, ChevronRight, Layers, Upload, X, ZoomIn } from "lucide-react";
1231
+
1232
+ // src/react/ZenitLayerManager.tsx
1233
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1234
+ var FLOAT_TOLERANCE = 1e-3;
1235
+ function areEffectiveStatesEqual(a, b) {
1236
+ if (a.length !== b.length) return false;
1237
+ return a.every((state, index) => {
1238
+ const other = b[index];
1239
+ if (!other) return false;
1240
+ const opacityDiff = Math.abs((state.opacity ?? 1) - (other.opacity ?? 1));
1241
+ return String(state.layerId) === String(other.layerId) && (state.visible ?? false) === (other.visible ?? false) && opacityDiff <= FLOAT_TOLERANCE;
1242
+ });
1243
+ }
1244
+ function getLayerColor2(style) {
1245
+ if (!style) return "#94a3b8";
1246
+ return style.fillColor ?? style.color ?? "#94a3b8";
1247
+ }
1248
+ function resolveDisplayOrder(value) {
1249
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1250
+ if (typeof value === "string") {
1251
+ const parsed = Number.parseFloat(value);
1252
+ if (Number.isFinite(parsed)) return parsed;
1253
+ }
1254
+ return 0;
1255
+ }
1256
+ var ZenitLayerManager = ({
1257
+ client,
1258
+ mapId,
1259
+ side = "right",
1260
+ className,
1261
+ style,
1262
+ height,
1263
+ layerStates,
1264
+ onLayerStatesChange,
1265
+ mapZoom,
1266
+ autoOpacityOnZoom = false,
1267
+ autoOpacityConfig,
1268
+ showUploadTab = true,
1269
+ showLayerVisibilityIcon = true,
1270
+ layerFeatureCounts,
1271
+ mapLayers
1272
+ }) => {
1273
+ const [map, setMap] = useState2(null);
1274
+ const [loadingMap, setLoadingMap] = useState2(false);
1275
+ const [mapError, setMapError] = useState2(null);
1276
+ const [layers, setLayers] = useState2([]);
1277
+ const [activeTab, setActiveTab] = useState2("layers");
1278
+ const [panelVisible, setPanelVisible] = useState2(true);
1279
+ const lastEmittedStatesRef = useRef2(null);
1280
+ const isControlled = Array.isArray(layerStates) && typeof onLayerStatesChange === "function";
1281
+ const baseStates = useMemo2(
1282
+ () => initLayerStates(
1283
+ layers.map((entry) => ({
1284
+ ...entry.mapLayer,
1285
+ layerId: entry.mapLayer.layerId,
1286
+ isVisible: entry.visible,
1287
+ opacity: entry.opacity
1288
+ }))
1289
+ ),
1290
+ [layers]
1291
+ );
1292
+ const overrideStates = useMemo2(
1293
+ () => layers.map(
1294
+ (entry) => ({
1295
+ layerId: entry.mapLayer.layerId,
1296
+ overrideVisible: entry.visible,
1297
+ overrideOpacity: entry.opacity
1298
+ })
1299
+ ),
1300
+ [layers]
1301
+ );
1302
+ const effectiveStates = useMemo2(
1303
+ () => layerStates ?? applyLayerOverrides(baseStates, overrideStates),
1304
+ [baseStates, layerStates, overrideStates]
1305
+ );
1306
+ const layerMetaIndex = useMemo2(() => {
1307
+ const index = /* @__PURE__ */ new Map();
1308
+ mapLayers?.forEach((entry) => {
1309
+ const key = String(entry.layerId);
1310
+ index.set(key, { layerType: entry.layerType ?? void 0, geometryType: entry.geometryType ?? void 0 });
1311
+ });
1312
+ map?.mapLayers?.forEach((entry) => {
1313
+ const key = String(entry.layerId);
1314
+ if (!index.has(key)) {
1315
+ index.set(key, { layerType: entry.layerType ?? void 0, geometryType: entry.geometryType ?? void 0 });
1316
+ }
1317
+ });
1318
+ return index;
1319
+ }, [map, mapLayers]);
1320
+ const resolveUserOpacity = React2.useCallback((state) => {
1321
+ if (typeof state.overrideOpacity === "number") return state.overrideOpacity;
1322
+ if (typeof state.overrideOpacity === "string") {
1323
+ const parsed = Number.parseFloat(state.overrideOpacity);
1324
+ if (Number.isFinite(parsed)) return parsed;
1325
+ }
1326
+ return state.opacity ?? 1;
1327
+ }, []);
1328
+ const resolveEffectiveOpacity = React2.useCallback(
1329
+ (layerId, userOpacity) => {
1330
+ if (!autoOpacityOnZoom || typeof mapZoom !== "number") {
1331
+ return userOpacity;
1332
+ }
1333
+ const meta = layerMetaIndex.get(String(layerId));
1334
+ return getEffectiveLayerOpacity(
1335
+ userOpacity,
1336
+ mapZoom,
1337
+ meta?.layerType,
1338
+ meta?.geometryType,
1339
+ autoOpacityConfig
1340
+ );
1341
+ },
1342
+ [autoOpacityConfig, autoOpacityOnZoom, layerMetaIndex, mapZoom]
1343
+ );
1344
+ const effectiveStatesWithZoom = useMemo2(() => {
1345
+ if (!autoOpacityOnZoom || typeof mapZoom !== "number") {
1346
+ return effectiveStates;
1347
+ }
1348
+ return effectiveStates.map((state) => {
1349
+ const userOpacity = resolveUserOpacity(state);
1350
+ const adjustedOpacity = resolveEffectiveOpacity(state.layerId, userOpacity);
1351
+ return {
1352
+ ...state,
1353
+ opacity: adjustedOpacity,
1354
+ overrideOpacity: userOpacity
1355
+ };
1356
+ });
1357
+ }, [autoOpacityOnZoom, effectiveStates, mapZoom, resolveEffectiveOpacity, resolveUserOpacity]);
1358
+ useEffect2(() => {
1359
+ let cancelled = false;
1360
+ setLoadingMap(true);
1361
+ setMapError(null);
1362
+ setLayers([]);
1363
+ client.maps.getMap(mapId, true).then((mapResponse) => {
1364
+ if (cancelled) return;
1365
+ const extractedMap = extractMapDto(mapResponse);
1366
+ const resolvedMap = extractedMap ? normalizeMapCenter(extractedMap) : null;
1367
+ setMap(resolvedMap);
1368
+ const normalizedLayers = normalizeMapLayers(resolvedMap);
1369
+ setLayers(
1370
+ normalizedLayers.map((entry, index) => ({
1371
+ mapLayer: { ...entry.mapLayer, layerId: entry.layerId },
1372
+ layer: entry.layer,
1373
+ visible: entry.isVisible,
1374
+ opacity: entry.opacity,
1375
+ displayOrder: entry.displayOrder,
1376
+ initialIndex: index
1377
+ }))
1378
+ );
1379
+ }).catch((err) => {
1380
+ if (cancelled) return;
1381
+ const message = err instanceof Error ? err.message : "No se pudo cargar el mapa";
1382
+ setMapError(message);
1383
+ }).finally(() => {
1384
+ if (!cancelled) setLoadingMap(false);
1385
+ });
1386
+ return () => {
1387
+ cancelled = true;
1388
+ };
1389
+ }, [client.maps, mapId]);
1390
+ useEffect2(() => {
1391
+ if (!showUploadTab && activeTab === "upload") {
1392
+ setActiveTab("layers");
1393
+ }
1394
+ }, [activeTab, showUploadTab]);
1395
+ useEffect2(() => {
1396
+ if (isControlled) return;
1397
+ if (!onLayerStatesChange) return;
1398
+ const emitStates = autoOpacityOnZoom && typeof mapZoom === "number" ? effectiveStatesWithZoom : effectiveStates;
1399
+ const previous = lastEmittedStatesRef.current;
1400
+ if (previous && areEffectiveStatesEqual(previous, emitStates)) {
1401
+ return;
1402
+ }
1403
+ lastEmittedStatesRef.current = emitStates;
1404
+ onLayerStatesChange(emitStates);
1405
+ }, [
1406
+ autoOpacityOnZoom,
1407
+ effectiveStates,
1408
+ effectiveStatesWithZoom,
1409
+ isControlled,
1410
+ mapZoom,
1411
+ onLayerStatesChange
1412
+ ]);
1413
+ const updateLayerVisible = React2.useCallback(
1414
+ (layerId, visible) => {
1415
+ if (!onLayerStatesChange) return;
1416
+ const next = effectiveStates.map(
1417
+ (state) => state.layerId === layerId ? { ...state, visible, overrideVisible: visible } : state
1418
+ );
1419
+ onLayerStatesChange(next);
1420
+ },
1421
+ [effectiveStates, onLayerStatesChange]
1422
+ );
1423
+ const updateLayerOpacity = React2.useCallback(
1424
+ (layerId, opacity) => {
1425
+ if (!onLayerStatesChange) return;
1426
+ const adjustedOpacity = resolveEffectiveOpacity(layerId, opacity);
1427
+ const next = effectiveStates.map(
1428
+ (state) => state.layerId === layerId ? { ...state, opacity: adjustedOpacity, overrideOpacity: opacity } : state
1429
+ );
1430
+ onLayerStatesChange(next);
1431
+ },
1432
+ [effectiveStates, onLayerStatesChange, resolveEffectiveOpacity]
1433
+ );
1434
+ const resolveFeatureCount = React2.useCallback(
1435
+ (layerId, layer) => {
1436
+ const resolvedFeatureCount = layerFeatureCounts?.[layerId] ?? layerFeatureCounts?.[String(layerId)];
1437
+ if (typeof resolvedFeatureCount === "number") return resolvedFeatureCount;
1438
+ const featureCount = layer?.featuresCount ?? layer?.featureCount ?? layer?.totalFeatures;
1439
+ return typeof featureCount === "number" ? featureCount : null;
1440
+ },
1441
+ [layerFeatureCounts]
1442
+ );
1443
+ const decoratedLayers = useMemo2(() => {
1444
+ return layers.map((entry) => ({
1445
+ ...entry,
1446
+ effective: effectiveStates.find((state) => state.layerId === entry.mapLayer.layerId),
1447
+ featureCount: resolveFeatureCount(entry.mapLayer.layerId, entry.layer),
1448
+ layerName: entry.layer?.name ?? entry.mapLayer.name ?? `Capa ${entry.mapLayer.layerId}`
1449
+ })).sort((a, b) => {
1450
+ const aOrder = resolveDisplayOrder(
1451
+ a.displayOrder ?? a.mapLayer.displayOrder ?? a.mapLayer.order
1452
+ );
1453
+ const bOrder = resolveDisplayOrder(
1454
+ b.displayOrder ?? b.mapLayer.displayOrder ?? b.mapLayer.order
1455
+ );
1456
+ const aHasOrder = a.displayOrder ?? a.mapLayer.displayOrder ?? a.mapLayer.order ?? null;
1457
+ const bHasOrder = b.displayOrder ?? b.mapLayer.displayOrder ?? b.mapLayer.order ?? null;
1458
+ if (aHasOrder !== null && bHasOrder !== null) {
1459
+ const orderCompare = aOrder - bOrder;
1460
+ if (orderCompare !== 0) return orderCompare;
1461
+ }
1462
+ const aInitial = a.initialIndex ?? 0;
1463
+ const bInitial = b.initialIndex ?? 0;
1464
+ if (aInitial !== bInitial) return aInitial - bInitial;
1465
+ const aName = a.layerName ?? String(a.mapLayer.layerId);
1466
+ const bName = b.layerName ?? String(b.mapLayer.layerId);
1467
+ const nameCompare = aName.localeCompare(bName, void 0, { sensitivity: "base" });
1468
+ if (nameCompare !== 0) return nameCompare;
1469
+ return String(a.mapLayer.layerId).localeCompare(String(b.mapLayer.layerId));
1470
+ });
1471
+ }, [effectiveStates, layers, resolveFeatureCount]);
1472
+ const resolveLayerStyle = React2.useCallback(
1473
+ (layerId) => {
1474
+ const layerKey = String(layerId);
1475
+ const fromProp = mapLayers?.find((entry) => String(entry.layerId) === layerKey)?.style;
1476
+ if (fromProp) return fromProp;
1477
+ const fromMapDto = map?.mapLayers?.find((entry) => String(entry.layerId) === layerKey);
1478
+ if (fromMapDto?.layer) {
1479
+ const layer = fromMapDto.layer;
1480
+ if (layer.style) return layer.style;
1481
+ }
1482
+ return void 0;
1483
+ },
1484
+ [map, mapLayers]
1485
+ );
1486
+ const panelStyle = {
1487
+ width: 360,
1488
+ borderLeft: side === "right" ? "1px solid #e2e8f0" : void 0,
1489
+ borderRight: side === "left" ? "1px solid #e2e8f0" : void 0,
1490
+ background: "#f1f5f9",
1491
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1492
+ fontSize: 14,
1493
+ boxSizing: "border-box",
1494
+ height: height ?? "100%",
1495
+ display: "flex",
1496
+ flexDirection: "column",
1497
+ boxShadow: "0 12px 28px rgba(15, 23, 42, 0.08)",
1498
+ ...style,
1499
+ ...height ? { height } : {}
1500
+ };
1501
+ if (loadingMap) {
1502
+ return /* @__PURE__ */ jsx2("div", { className, style: panelStyle, children: "Cargando capas\u2026" });
1503
+ }
1504
+ if (mapError) {
1505
+ return /* @__PURE__ */ jsxs2("div", { className, style: { ...panelStyle, color: "#c53030" }, children: [
1506
+ "Error al cargar mapa: ",
1507
+ mapError
1508
+ ] });
1509
+ }
1510
+ if (!map) {
1511
+ return null;
1512
+ }
1513
+ const headerStyle = {
1514
+ padding: "14px 16px",
1515
+ borderBottom: "1px solid #e2e8f0",
1516
+ background: "#fff",
1517
+ position: "sticky",
1518
+ top: 0,
1519
+ zIndex: 2,
1520
+ boxShadow: "0 1px 0 rgba(148, 163, 184, 0.25)"
1521
+ };
1522
+ const renderLayerCards = () => {
1523
+ return /* @__PURE__ */ jsx2("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: decoratedLayers.map((layerState) => {
1524
+ const layerId = layerState.mapLayer.layerId;
1525
+ const layerName = layerState.layerName ?? `Capa ${layerId}`;
1526
+ const visible = layerState.effective?.visible ?? false;
1527
+ const userOpacity = layerState.effective ? resolveUserOpacity(layerState.effective) : 1;
1528
+ const featureCount = layerState.featureCount;
1529
+ const layerColor = getLayerColor2(resolveLayerStyle(layerId));
1530
+ const muted = !visible;
1531
+ const opacityPercent = Math.round(userOpacity * 100);
1532
+ const sliderBackground = `linear-gradient(to right, ${layerColor} 0%, ${layerColor} ${opacityPercent}%, #e5e7eb ${opacityPercent}%, #e5e7eb 100%)`;
1533
+ return /* @__PURE__ */ jsxs2(
1534
+ "div",
1535
+ {
1536
+ className: `zlm-card${muted ? " is-muted" : ""}`,
1537
+ style: {
1538
+ border: "1px solid #e2e8f0",
1539
+ borderRadius: 12,
1540
+ padding: 12,
1541
+ background: "#fff",
1542
+ display: "flex",
1543
+ flexDirection: "column",
1544
+ gap: 10,
1545
+ width: "100%"
1546
+ },
1547
+ children: [
1548
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", justifyContent: "space-between", gap: 12 }, children: [
1549
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 10, alignItems: "flex-start", minWidth: 0, flex: 1 }, children: [
1550
+ /* @__PURE__ */ jsx2(
1551
+ "div",
1552
+ {
1553
+ style: {
1554
+ width: 14,
1555
+ height: 14,
1556
+ borderRadius: "50%",
1557
+ backgroundColor: layerColor,
1558
+ border: "2px solid #e2e8f0",
1559
+ flexShrink: 0,
1560
+ marginTop: 4
1561
+ },
1562
+ title: "Color de la capa"
1563
+ }
1564
+ ),
1565
+ showLayerVisibilityIcon && /* @__PURE__ */ jsx2(
1566
+ "button",
1567
+ {
1568
+ type: "button",
1569
+ className: `zlm-icon-button${visible ? " is-active" : ""}`,
1570
+ onClick: () => isControlled ? updateLayerVisible(layerId, !visible) : setLayers(
1571
+ (prev) => prev.map(
1572
+ (entry) => entry.mapLayer.layerId === layerId ? { ...entry, visible: !visible } : entry
1573
+ )
1574
+ ),
1575
+ "aria-label": visible ? "Ocultar capa" : "Mostrar capa",
1576
+ children: visible ? /* @__PURE__ */ jsx2(Eye, { size: 16 }) : /* @__PURE__ */ jsx2(EyeOff, { size: 16 })
1577
+ }
1578
+ ),
1579
+ /* @__PURE__ */ jsxs2("div", { style: { minWidth: 0, flex: 1 }, children: [
1580
+ /* @__PURE__ */ jsx2(
1581
+ "div",
1582
+ {
1583
+ className: "zlm-layer-name",
1584
+ style: {
1585
+ fontWeight: 700,
1586
+ display: "-webkit-box",
1587
+ WebkitLineClamp: 2,
1588
+ WebkitBoxOrient: "vertical",
1589
+ overflow: "hidden",
1590
+ overflowWrap: "anywhere",
1591
+ lineHeight: 1.2,
1592
+ color: muted ? "#64748b" : "#0f172a"
1593
+ },
1594
+ title: layerName,
1595
+ children: layerName
1596
+ }
1597
+ ),
1598
+ /* @__PURE__ */ jsxs2("div", { style: { color: muted ? "#94a3b8" : "#64748b", fontSize: 12 }, children: [
1599
+ "ID ",
1600
+ layerId
1601
+ ] })
1602
+ ] })
1603
+ ] }),
1604
+ /* @__PURE__ */ jsx2("div", { style: { display: "flex", alignItems: "flex-start", gap: 6, flexShrink: 0 }, children: typeof featureCount === "number" && /* @__PURE__ */ jsxs2("span", { className: "zlm-badge", children: [
1605
+ featureCount.toLocaleString(),
1606
+ " features"
1607
+ ] }) })
1608
+ ] }),
1609
+ /* @__PURE__ */ jsx2("div", { style: { display: "flex", gap: 10, alignItems: "center" }, children: /* @__PURE__ */ jsxs2("div", { style: { flex: 1 }, children: [
1610
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 6, color: "#64748b", fontSize: 12 }, children: [
1611
+ /* @__PURE__ */ jsx2("span", { children: "Opacidad" }),
1612
+ /* @__PURE__ */ jsxs2("span", { children: [
1613
+ opacityPercent,
1614
+ "%"
1615
+ ] })
1616
+ ] }),
1617
+ /* @__PURE__ */ jsx2(
1618
+ "input",
1619
+ {
1620
+ className: "zlm-range",
1621
+ type: "range",
1622
+ min: 0,
1623
+ max: 1,
1624
+ step: 0.05,
1625
+ value: userOpacity,
1626
+ style: {
1627
+ width: "100%",
1628
+ accentColor: layerColor,
1629
+ background: sliderBackground,
1630
+ height: 6,
1631
+ borderRadius: 999
1632
+ },
1633
+ onChange: (e) => {
1634
+ const value = Number(e.target.value);
1635
+ if (isControlled) {
1636
+ updateLayerOpacity(layerId, value);
1637
+ return;
1638
+ }
1639
+ setLayers(
1640
+ (prev) => prev.map(
1641
+ (entry) => entry.mapLayer.layerId === layerId ? { ...entry, opacity: value } : entry
1642
+ )
1643
+ );
1644
+ },
1645
+ "aria-label": `Opacidad de la capa ${layerName}`
1646
+ }
1647
+ )
1648
+ ] }) })
1649
+ ]
1650
+ },
1651
+ layerId.toString()
1652
+ );
1653
+ }) });
1654
+ };
1655
+ return /* @__PURE__ */ jsxs2("div", { className: ["zenit-layer-manager", className].filter(Boolean).join(" "), style: panelStyle, children: [
1656
+ /* @__PURE__ */ jsx2("style", { children: `
1657
+ .zenit-layer-manager .zlm-card {
1658
+ transition: box-shadow 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
1659
+ box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
1660
+ }
1661
+ .zenit-layer-manager .zlm-card.is-muted {
1662
+ opacity: 0.7;
1663
+ }
1664
+ .zenit-layer-manager .zlm-badge {
1665
+ display: inline-flex;
1666
+ align-items: center;
1667
+ gap: 4px;
1668
+ margin-top: 6px;
1669
+ padding: 2px 8px;
1670
+ border-radius: 999px;
1671
+ background: #f1f5f9;
1672
+ color: #475569;
1673
+ font-size: 11px;
1674
+ font-weight: 600;
1675
+ }
1676
+ .zenit-layer-manager .zlm-icon-button {
1677
+ border: 1px solid #e2e8f0;
1678
+ background: #f8fafc;
1679
+ color: #475569;
1680
+ border-radius: 8px;
1681
+ width: 34px;
1682
+ height: 34px;
1683
+ display: inline-flex;
1684
+ align-items: center;
1685
+ justify-content: center;
1686
+ cursor: pointer;
1687
+ transition: all 0.15s ease;
1688
+ }
1689
+ .zenit-layer-manager .zlm-icon-button.is-active {
1690
+ background: #0f172a;
1691
+ color: #fff;
1692
+ border-color: #0f172a;
1693
+ }
1694
+ .zenit-layer-manager .zlm-icon-button:hover {
1695
+ box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12);
1696
+ }
1697
+ .zenit-layer-manager .zlm-icon-button:focus-visible {
1698
+ outline: 2px solid #60a5fa;
1699
+ outline-offset: 2px;
1700
+ }
1701
+ .zenit-layer-manager .zlm-range {
1702
+ width: 100%;
1703
+ accent-color: #0f172a;
1704
+ }
1705
+ .zenit-layer-manager .zlm-tab {
1706
+ flex: 1;
1707
+ padding: 8px 12px;
1708
+ border: none;
1709
+ background: transparent;
1710
+ color: #475569;
1711
+ font-weight: 600;
1712
+ cursor: pointer;
1713
+ display: inline-flex;
1714
+ align-items: center;
1715
+ justify-content: center;
1716
+ gap: 6px;
1717
+ font-size: 13px;
1718
+ }
1719
+ .zenit-layer-manager .zlm-tab.is-active {
1720
+ background: #fff;
1721
+ color: #0f172a;
1722
+ box-shadow: 0 4px 10px rgba(15, 23, 42, 0.08);
1723
+ border-radius: 10px;
1724
+ }
1725
+ .zenit-layer-manager .zlm-tab:focus-visible {
1726
+ outline: 2px solid #60a5fa;
1727
+ outline-offset: 2px;
1728
+ }
1729
+ .zenit-layer-manager .zlm-panel-toggle {
1730
+ border: 1px solid #e2e8f0;
1731
+ background: #fff;
1732
+ color: #0f172a;
1733
+ border-radius: 10px;
1734
+ padding: 6px 10px;
1735
+ display: inline-flex;
1736
+ align-items: center;
1737
+ gap: 6px;
1738
+ font-size: 12px;
1739
+ font-weight: 600;
1740
+ cursor: pointer;
1741
+ transition: all 0.15s ease;
1742
+ }
1743
+ .zenit-layer-manager .zlm-panel-toggle:hover {
1744
+ box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12);
1745
+ }
1746
+ .zenit-layer-manager .zlm-panel-toggle:focus-visible {
1747
+ outline: 2px solid #60a5fa;
1748
+ outline-offset: 2px;
1749
+ }
1750
+ ` }),
1751
+ /* @__PURE__ */ jsxs2("div", { style: headerStyle, children: [
1752
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
1753
+ /* @__PURE__ */ jsxs2("div", { children: [
1754
+ /* @__PURE__ */ jsx2("div", { style: { fontWeight: 800, fontSize: 16, color: "#0f172a" }, children: "Gesti\xF3n de Capas" }),
1755
+ /* @__PURE__ */ jsxs2("div", { style: { color: "#64748b", fontSize: 12 }, children: [
1756
+ "Mapa #",
1757
+ map.id
1758
+ ] })
1759
+ ] }),
1760
+ /* @__PURE__ */ jsxs2(
1761
+ "button",
1762
+ {
1763
+ type: "button",
1764
+ onClick: () => setPanelVisible((prev) => !prev),
1765
+ className: "zlm-panel-toggle",
1766
+ "aria-label": panelVisible ? "Ocultar panel de capas" : "Mostrar panel de capas",
1767
+ children: [
1768
+ panelVisible ? /* @__PURE__ */ jsx2(Eye, { size: 16 }) : /* @__PURE__ */ jsx2(EyeOff, { size: 16 }),
1769
+ panelVisible ? "Ocultar" : "Mostrar"
1770
+ ]
1771
+ }
1772
+ )
1773
+ ] }),
1774
+ /* @__PURE__ */ jsxs2(
1775
+ "div",
1776
+ {
1777
+ style: {
1778
+ display: "flex",
1779
+ gap: 6,
1780
+ marginTop: 12,
1781
+ padding: 4,
1782
+ border: "1px solid #e2e8f0",
1783
+ borderRadius: 12,
1784
+ background: "#f1f5f9"
1785
+ },
1786
+ children: [
1787
+ /* @__PURE__ */ jsxs2(
1788
+ "button",
1789
+ {
1790
+ type: "button",
1791
+ className: `zlm-tab${activeTab === "layers" ? " is-active" : ""}`,
1792
+ onClick: () => setActiveTab("layers"),
1793
+ children: [
1794
+ /* @__PURE__ */ jsx2(Layers, { size: 16 }),
1795
+ "Capas"
1796
+ ]
1797
+ }
1798
+ ),
1799
+ showUploadTab && /* @__PURE__ */ jsxs2(
1800
+ "button",
1801
+ {
1802
+ type: "button",
1803
+ className: `zlm-tab${activeTab === "upload" ? " is-active" : ""}`,
1804
+ onClick: () => setActiveTab("upload"),
1805
+ children: [
1806
+ /* @__PURE__ */ jsx2(Upload, { size: 16 }),
1807
+ "Subir"
1808
+ ]
1809
+ }
1810
+ )
1811
+ ]
1812
+ }
1813
+ )
1814
+ ] }),
1815
+ panelVisible && /* @__PURE__ */ jsxs2("div", { style: { padding: "12px 10px 18px", overflowY: "auto", flex: 1, minHeight: 0 }, children: [
1816
+ activeTab === "layers" && renderLayerCards(),
1817
+ showUploadTab && activeTab === "upload" && /* @__PURE__ */ jsx2("div", { style: { color: "#475569", fontSize: 13 }, children: "Pr\xF3ximamente podr\xE1s subir capas desde este panel." })
1818
+ ] })
1819
+ ] });
1820
+ };
1821
+
1822
+ // src/react/ZenitFeatureFilterPanel.tsx
1823
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1824
+ var ZenitFeatureFilterPanel = ({
1825
+ title = "Filtros",
1826
+ description,
1827
+ className,
1828
+ style,
1829
+ children
1830
+ }) => {
1831
+ return /* @__PURE__ */ jsxs3(
1832
+ "section",
1833
+ {
1834
+ className,
1835
+ style: {
1836
+ border: "1px solid #e2e8f0",
1837
+ borderRadius: 12,
1838
+ padding: 16,
1839
+ background: "#fff",
1840
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1841
+ ...style
1842
+ },
1843
+ children: [
1844
+ /* @__PURE__ */ jsxs3("header", { style: { marginBottom: 12 }, children: [
1845
+ /* @__PURE__ */ jsx3("h3", { style: { margin: 0, fontSize: 16 }, children: title }),
1846
+ description && /* @__PURE__ */ jsx3("p", { style: { margin: "6px 0 0", color: "#475569", fontSize: 13 }, children: description })
1847
+ ] }),
1848
+ /* @__PURE__ */ jsx3("div", { children })
1849
+ ]
1850
+ }
1851
+ );
1852
+ };
1853
+
1854
+ // src/react/ai/FloatingChatBox.tsx
1855
+ import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
1856
+ import { createPortal } from "react-dom";
1857
+
1858
+ // src/react/hooks/use-chat.ts
1859
+ import { useCallback as useCallback2, useRef as useRef3, useState as useState3 } from "react";
1860
+
1861
+ // src/ai/chat.service.ts
1862
+ var DEFAULT_ERROR_MESSAGE = "No fue posible completar la solicitud al asistente.";
1863
+ var normalizeBaseUrl = (baseUrl) => baseUrl.replace(/\/$/, "");
1864
+ var resolveBaseUrl = (config, options) => {
1865
+ const baseUrl = options?.baseUrl ?? config?.baseUrl;
1866
+ if (!baseUrl) {
1867
+ throw new Error("baseUrl es requerido para usar el chat de Zenit AI.");
1868
+ }
1869
+ return normalizeBaseUrl(baseUrl);
1870
+ };
1871
+ var resolveAccessToken = async (config, options) => {
1872
+ if (options?.accessToken) return options.accessToken;
1873
+ if (config?.accessToken) return config.accessToken;
1874
+ if (options?.getAccessToken) return await options.getAccessToken();
1875
+ if (config?.getAccessToken) return await config.getAccessToken();
1876
+ return void 0;
1877
+ };
1878
+ var buildAuthHeaders = (token) => {
1879
+ if (!token) return {};
1880
+ return { Authorization: `Bearer ${token}` };
1881
+ };
1882
+ var parseError = async (response) => {
1883
+ try {
1884
+ const contentType = response.headers.get("content-type") ?? "";
1885
+ if (contentType.includes("application/json")) {
1886
+ const payload = await response.json();
1887
+ const message = payload?.message ?? DEFAULT_ERROR_MESSAGE;
1888
+ return new Error(message);
1889
+ }
1890
+ const text = await response.text();
1891
+ return new Error(text || DEFAULT_ERROR_MESSAGE);
1892
+ } catch (error) {
1893
+ return error instanceof Error ? error : new Error(DEFAULT_ERROR_MESSAGE);
1894
+ }
1895
+ };
1896
+ var sendMessage = async (mapId, request, options, config) => {
1897
+ const baseUrl = resolveBaseUrl(config, options);
1898
+ const token = await resolveAccessToken(config, options);
1899
+ const response = await fetch(`${baseUrl}/ai/maps/chat/${mapId}`, {
1900
+ method: "POST",
1901
+ headers: {
1902
+ "Content-Type": "application/json",
1903
+ ...buildAuthHeaders(token)
1904
+ },
1905
+ body: JSON.stringify(request),
1906
+ signal: options?.signal
1907
+ });
1908
+ if (!response.ok) {
1909
+ throw await parseError(response);
1910
+ }
1911
+ return await response.json();
1912
+ };
1913
+ var mergeResponse = (previous, chunk, aggregatedAnswer) => ({
1914
+ ...previous ?? chunk,
1915
+ ...chunk,
1916
+ answer: aggregatedAnswer
1917
+ });
1918
+ var resolveAggregatedAnswer = (current, incoming) => {
1919
+ if (!incoming) return current;
1920
+ if (incoming.startsWith(current)) return incoming;
1921
+ return current + incoming;
1922
+ };
1923
+ var sendMessageStream = async (mapId, request, callbacks = {}, options, config) => {
1924
+ const baseUrl = resolveBaseUrl(config, options);
1925
+ const token = await resolveAccessToken(config, options);
1926
+ const response = await fetch(`${baseUrl}/ai/maps/chat/${mapId}/stream`, {
1927
+ method: "POST",
1928
+ headers: {
1929
+ Accept: "text/event-stream",
1930
+ "Content-Type": "application/json",
1931
+ ...buildAuthHeaders(token)
1932
+ },
1933
+ body: JSON.stringify(request),
1934
+ signal: options?.signal
1935
+ });
1936
+ if (!response.ok) {
1937
+ const error = await parseError(response);
1938
+ callbacks.onError?.(error);
1939
+ throw error;
1940
+ }
1941
+ const reader = response.body?.getReader();
1942
+ if (!reader) {
1943
+ const error = new Error("No se pudo iniciar el streaming de Zenit AI.");
1944
+ callbacks.onError?.(error);
1945
+ throw error;
1946
+ }
1947
+ const decoder = new TextDecoder("utf-8");
1948
+ let buffer = "";
1949
+ let aggregated = "";
1950
+ let latestResponse = null;
1951
+ try {
1952
+ while (true) {
1953
+ const { value, done } = await reader.read();
1954
+ if (done) break;
1955
+ buffer += decoder.decode(value, { stream: true });
1956
+ const lines = buffer.split(/\r?\n/);
1957
+ buffer = lines.pop() ?? "";
1958
+ for (const line of lines) {
1959
+ const trimmed = line.trim();
1960
+ if (!trimmed || trimmed.startsWith("event:")) continue;
1961
+ if (!trimmed.startsWith("data:")) continue;
1962
+ const payload = trimmed.replace(/^data:\s*/, "");
1963
+ if (!payload || payload === "[DONE]") continue;
1964
+ try {
1965
+ const parsed = JSON.parse(payload);
1966
+ aggregated = resolveAggregatedAnswer(aggregated, parsed.answer ?? "");
1967
+ latestResponse = mergeResponse(latestResponse, parsed, aggregated);
1968
+ if (latestResponse) {
1969
+ callbacks.onChunk?.(latestResponse, aggregated);
1970
+ }
1971
+ } catch (error) {
1972
+ callbacks.onError?.(error);
1973
+ }
1974
+ }
1975
+ }
1976
+ } catch (error) {
1977
+ callbacks.onError?.(error);
1978
+ throw error;
1979
+ }
1980
+ if (!latestResponse) {
1981
+ const error = new Error(DEFAULT_ERROR_MESSAGE);
1982
+ callbacks.onError?.(error);
1983
+ throw error;
1984
+ }
1985
+ callbacks.onComplete?.(latestResponse);
1986
+ return latestResponse;
1987
+ };
1988
+ var createChatService = (config) => ({
1989
+ sendMessage: (mapId, request, options) => sendMessage(mapId, request, options, config),
1990
+ sendMessageStream: (mapId, request, callbacks, options) => sendMessageStream(mapId, request, callbacks, options, config)
1991
+ });
1992
+
1993
+ // src/react/hooks/use-chat.ts
1994
+ var useSendMessage = (config) => {
1995
+ const [isLoading, setIsLoading] = useState3(false);
1996
+ const [error, setError] = useState3(null);
1997
+ const send = useCallback2(
1998
+ async (mapId, request, options) => {
1999
+ setIsLoading(true);
2000
+ setError(null);
2001
+ try {
2002
+ return await sendMessage(mapId, request, options, config);
2003
+ } catch (err) {
2004
+ setError(err);
2005
+ throw err;
2006
+ } finally {
2007
+ setIsLoading(false);
2008
+ }
2009
+ },
2010
+ [config]
2011
+ );
2012
+ return { sendMessage: send, isLoading, error };
2013
+ };
2014
+ var useSendMessageStream = (config) => {
2015
+ const [isStreaming, setIsStreaming] = useState3(false);
2016
+ const [streamingText, setStreamingText] = useState3("");
2017
+ const [completeResponse, setCompleteResponse] = useState3(null);
2018
+ const [error, setError] = useState3(null);
2019
+ const requestIdRef = useRef3(0);
2020
+ const reset = useCallback2(() => {
2021
+ setIsStreaming(false);
2022
+ setStreamingText("");
2023
+ setCompleteResponse(null);
2024
+ setError(null);
2025
+ }, []);
2026
+ const send = useCallback2(
2027
+ async (mapId, request, options) => {
2028
+ const requestId = requestIdRef.current + 1;
2029
+ requestIdRef.current = requestId;
2030
+ setIsStreaming(true);
2031
+ setStreamingText("");
2032
+ setCompleteResponse(null);
2033
+ setError(null);
2034
+ try {
2035
+ const response = await sendMessageStream(
2036
+ mapId,
2037
+ request,
2038
+ {
2039
+ onChunk: (_chunk, aggregated) => {
2040
+ if (requestIdRef.current !== requestId) return;
2041
+ setStreamingText(aggregated);
2042
+ },
2043
+ onError: (err) => {
2044
+ if (requestIdRef.current !== requestId) return;
2045
+ setError(err);
2046
+ },
2047
+ onComplete: (finalResponse) => {
2048
+ if (requestIdRef.current !== requestId) return;
2049
+ setCompleteResponse(finalResponse);
2050
+ }
2051
+ },
2052
+ options,
2053
+ config
2054
+ );
2055
+ return response;
2056
+ } catch (err) {
2057
+ setError(err);
2058
+ throw err;
2059
+ } finally {
2060
+ if (requestIdRef.current === requestId) {
2061
+ setIsStreaming(false);
2062
+ }
2063
+ }
2064
+ },
2065
+ [config]
2066
+ );
2067
+ return {
2068
+ sendMessage: send,
2069
+ isStreaming,
2070
+ streamingText,
2071
+ completeResponse,
2072
+ error,
2073
+ reset
2074
+ };
2075
+ };
2076
+
2077
+ // src/react/components/MarkdownRenderer.tsx
2078
+ import ReactMarkdown from "react-markdown";
2079
+ import remarkGfm from "remark-gfm";
2080
+ import { jsx as jsx4 } from "react/jsx-runtime";
2081
+ function normalizeAssistantMarkdown(text) {
2082
+ if (!text || typeof text !== "string") return "";
2083
+ let normalized = text;
2084
+ normalized = normalized.replace(/\r\n/g, "\n");
2085
+ normalized = normalized.replace(/^\s*#{1,6}\s*$/gm, "");
2086
+ normalized = normalized.replace(/\n{3,}/g, "\n\n");
2087
+ normalized = normalized.split("\n").map((line) => line.trimEnd()).join("\n");
2088
+ normalized = normalized.trim();
2089
+ return normalized;
2090
+ }
2091
+ var MarkdownRenderer = ({ content, className }) => {
2092
+ const normalizedContent = normalizeAssistantMarkdown(content);
2093
+ if (!normalizedContent) {
2094
+ return null;
2095
+ }
2096
+ return /* @__PURE__ */ jsx4("div", { className, style: { wordBreak: "break-word" }, children: /* @__PURE__ */ jsx4(
2097
+ ReactMarkdown,
2098
+ {
2099
+ remarkPlugins: [remarkGfm],
2100
+ components: {
2101
+ // Headings with proper spacing
2102
+ h1: ({ children, ...props }) => /* @__PURE__ */ jsx4("h1", { style: { fontSize: "1.5em", fontWeight: 700, marginTop: "1em", marginBottom: "0.5em" }, ...props, children }),
2103
+ h2: ({ children, ...props }) => /* @__PURE__ */ jsx4("h2", { style: { fontSize: "1.3em", fontWeight: 700, marginTop: "0.9em", marginBottom: "0.45em" }, ...props, children }),
2104
+ h3: ({ children, ...props }) => /* @__PURE__ */ jsx4("h3", { style: { fontSize: "1.15em", fontWeight: 600, marginTop: "0.75em", marginBottom: "0.4em" }, ...props, children }),
2105
+ h4: ({ children, ...props }) => /* @__PURE__ */ jsx4("h4", { style: { fontSize: "1.05em", fontWeight: 600, marginTop: "0.6em", marginBottom: "0.35em" }, ...props, children }),
2106
+ h5: ({ children, ...props }) => /* @__PURE__ */ jsx4("h5", { style: { fontSize: "1em", fontWeight: 600, marginTop: "0.5em", marginBottom: "0.3em" }, ...props, children }),
2107
+ h6: ({ children, ...props }) => /* @__PURE__ */ jsx4("h6", { style: { fontSize: "0.95em", fontWeight: 600, marginTop: "0.5em", marginBottom: "0.3em" }, ...props, children }),
2108
+ // Paragraphs with comfortable line height
2109
+ p: ({ children, ...props }) => /* @__PURE__ */ jsx4("p", { style: { marginTop: "0.5em", marginBottom: "0.5em", lineHeight: 1.6 }, ...props, children }),
2110
+ // Lists with proper indentation
2111
+ ul: ({ children, ...props }) => /* @__PURE__ */ jsx4("ul", { style: { paddingLeft: "1.5em", marginTop: "0.5em", marginBottom: "0.5em" }, ...props, children }),
2112
+ ol: ({ children, ...props }) => /* @__PURE__ */ jsx4("ol", { style: { paddingLeft: "1.5em", marginTop: "0.5em", marginBottom: "0.5em" }, ...props, children }),
2113
+ li: ({ children, ...props }) => /* @__PURE__ */ jsx4("li", { style: { marginTop: "0.25em", marginBottom: "0.25em" }, ...props, children }),
2114
+ // Code blocks
2115
+ code: ({ inline, children, ...props }) => {
2116
+ if (inline) {
2117
+ return /* @__PURE__ */ jsx4(
2118
+ "code",
2119
+ {
2120
+ style: {
2121
+ backgroundColor: "rgba(0, 0, 0, 0.08)",
2122
+ padding: "0.15em 0.4em",
2123
+ borderRadius: "4px",
2124
+ fontSize: "0.9em",
2125
+ fontFamily: "monospace"
2126
+ },
2127
+ ...props,
2128
+ children
2129
+ }
2130
+ );
2131
+ }
2132
+ return /* @__PURE__ */ jsx4(
2133
+ "code",
2134
+ {
2135
+ style: {
2136
+ display: "block",
2137
+ backgroundColor: "rgba(0, 0, 0, 0.08)",
2138
+ padding: "0.75em",
2139
+ borderRadius: "6px",
2140
+ fontSize: "0.9em",
2141
+ fontFamily: "monospace",
2142
+ overflowX: "auto",
2143
+ marginTop: "0.5em",
2144
+ marginBottom: "0.5em"
2145
+ },
2146
+ ...props,
2147
+ children
2148
+ }
2149
+ );
2150
+ },
2151
+ // Pre (code block wrapper)
2152
+ pre: ({ children, ...props }) => /* @__PURE__ */ jsx4("pre", { style: { margin: 0 }, ...props, children }),
2153
+ // Blockquotes
2154
+ blockquote: ({ children, ...props }) => /* @__PURE__ */ jsx4(
2155
+ "blockquote",
2156
+ {
2157
+ style: {
2158
+ borderLeft: "4px solid rgba(0, 0, 0, 0.2)",
2159
+ paddingLeft: "1em",
2160
+ marginLeft: 0,
2161
+ marginTop: "0.5em",
2162
+ marginBottom: "0.5em",
2163
+ color: "rgba(0, 0, 0, 0.7)"
2164
+ },
2165
+ ...props,
2166
+ children
2167
+ }
2168
+ ),
2169
+ // Strong/bold
2170
+ strong: ({ children, ...props }) => /* @__PURE__ */ jsx4("strong", { style: { fontWeight: 600 }, ...props, children }),
2171
+ // Emphasis/italic
2172
+ em: ({ children, ...props }) => /* @__PURE__ */ jsx4("em", { style: { fontStyle: "italic" }, ...props, children }),
2173
+ // Horizontal rule
2174
+ hr: (props) => /* @__PURE__ */ jsx4(
2175
+ "hr",
2176
+ {
2177
+ style: {
2178
+ border: "none",
2179
+ borderTop: "1px solid rgba(0, 0, 0, 0.1)",
2180
+ marginTop: "1em",
2181
+ marginBottom: "1em"
2182
+ },
2183
+ ...props
2184
+ }
2185
+ ),
2186
+ // Tables (GFM)
2187
+ table: ({ children, ...props }) => /* @__PURE__ */ jsx4("div", { style: { overflowX: "auto", marginTop: "0.5em", marginBottom: "0.5em" }, children: /* @__PURE__ */ jsx4(
2188
+ "table",
2189
+ {
2190
+ style: {
2191
+ borderCollapse: "collapse",
2192
+ width: "100%",
2193
+ fontSize: "0.9em"
2194
+ },
2195
+ ...props,
2196
+ children
2197
+ }
2198
+ ) }),
2199
+ th: ({ children, ...props }) => /* @__PURE__ */ jsx4(
2200
+ "th",
2201
+ {
2202
+ style: {
2203
+ border: "1px solid rgba(0, 0, 0, 0.2)",
2204
+ padding: "0.5em",
2205
+ backgroundColor: "rgba(0, 0, 0, 0.05)",
2206
+ fontWeight: 600,
2207
+ textAlign: "left"
2208
+ },
2209
+ ...props,
2210
+ children
2211
+ }
2212
+ ),
2213
+ td: ({ children, ...props }) => /* @__PURE__ */ jsx4(
2214
+ "td",
2215
+ {
2216
+ style: {
2217
+ border: "1px solid rgba(0, 0, 0, 0.2)",
2218
+ padding: "0.5em"
2219
+ },
2220
+ ...props,
2221
+ children
2222
+ }
2223
+ )
2224
+ },
2225
+ children: normalizedContent
2226
+ }
2227
+ ) });
2228
+ };
2229
+
2230
+ // src/react/ai/FloatingChatBox.tsx
2231
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2232
+ var ChatIcon = () => /* @__PURE__ */ jsx5("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx5("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) });
2233
+ var CloseIcon = () => /* @__PURE__ */ jsxs4("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2234
+ /* @__PURE__ */ jsx5("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
2235
+ /* @__PURE__ */ jsx5("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
2236
+ ] });
2237
+ var ExpandIcon = () => /* @__PURE__ */ jsxs4("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2238
+ /* @__PURE__ */ jsx5("polyline", { points: "15 3 21 3 21 9" }),
2239
+ /* @__PURE__ */ jsx5("polyline", { points: "9 21 3 21 3 15" }),
2240
+ /* @__PURE__ */ jsx5("line", { x1: "21", y1: "3", x2: "14", y2: "10" }),
2241
+ /* @__PURE__ */ jsx5("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
2242
+ ] });
2243
+ var CollapseIcon = () => /* @__PURE__ */ jsxs4("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2244
+ /* @__PURE__ */ jsx5("polyline", { points: "4 14 10 14 10 20" }),
2245
+ /* @__PURE__ */ jsx5("polyline", { points: "20 10 14 10 14 4" }),
2246
+ /* @__PURE__ */ jsx5("line", { x1: "14", y1: "10", x2: "21", y2: "3" }),
2247
+ /* @__PURE__ */ jsx5("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
2248
+ ] });
2249
+ var SendIcon = () => /* @__PURE__ */ jsxs4("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2250
+ /* @__PURE__ */ jsx5("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
2251
+ /* @__PURE__ */ jsx5("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
2252
+ ] });
2253
+ var LayersIcon = () => /* @__PURE__ */ jsxs4("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2254
+ /* @__PURE__ */ jsx5("polygon", { points: "12 2 2 7 12 12 22 7 12 2" }),
2255
+ /* @__PURE__ */ jsx5("polyline", { points: "2 17 12 22 22 17" }),
2256
+ /* @__PURE__ */ jsx5("polyline", { points: "2 12 12 17 22 12" })
2257
+ ] });
2258
+ var styles = {
2259
+ root: {
2260
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
2261
+ },
2262
+ // Floating button (closed state - wide with text, open state - circular with X)
2263
+ floatingButton: {
2264
+ position: "fixed",
2265
+ bottom: 24,
2266
+ right: 24,
2267
+ borderRadius: "999px",
2268
+ border: "none",
2269
+ cursor: "pointer",
2270
+ background: "linear-gradient(135deg, #10b981, #059669)",
2271
+ color: "#fff",
2272
+ boxShadow: "0 12px 28px rgba(16, 185, 129, 0.4)",
2273
+ display: "flex",
2274
+ alignItems: "center",
2275
+ justifyContent: "center",
2276
+ fontSize: 15,
2277
+ fontWeight: 600,
2278
+ zIndex: 99999,
2279
+ transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
2280
+ },
2281
+ floatingButtonClosed: {
2282
+ padding: "14px 24px",
2283
+ gap: 8
2284
+ },
2285
+ floatingButtonOpen: {
2286
+ width: 56,
2287
+ height: 56,
2288
+ padding: 0
2289
+ },
2290
+ floatingButtonMobile: {
2291
+ width: 56,
2292
+ height: 56,
2293
+ padding: 0
2294
+ },
2295
+ // Panel (expandable)
2296
+ panel: {
2297
+ position: "fixed",
2298
+ bottom: 92,
2299
+ right: 24,
2300
+ background: "#fff",
2301
+ borderRadius: 16,
2302
+ boxShadow: "0 20px 60px rgba(15, 23, 42, 0.3), 0 0 0 1px rgba(15, 23, 42, 0.05)",
2303
+ display: "flex",
2304
+ flexDirection: "column",
2305
+ overflow: "hidden",
2306
+ zIndex: 99999,
2307
+ transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
2308
+ },
2309
+ panelNormal: {
2310
+ width: 400,
2311
+ height: 550
2312
+ },
2313
+ panelExpanded: {
2314
+ width: 520,
2315
+ height: 700
2316
+ },
2317
+ // Header with green gradient
2318
+ header: {
2319
+ padding: "16px 18px",
2320
+ background: "linear-gradient(135deg, #10b981, #059669)",
2321
+ color: "#fff",
2322
+ display: "flex",
2323
+ alignItems: "center",
2324
+ justifyContent: "space-between"
2325
+ },
2326
+ title: {
2327
+ margin: 0,
2328
+ fontSize: 16,
2329
+ fontWeight: 600,
2330
+ letterSpacing: "-0.01em"
2331
+ },
2332
+ headerButtons: {
2333
+ display: "flex",
2334
+ alignItems: "center",
2335
+ gap: 8
2336
+ },
2337
+ headerButton: {
2338
+ border: "none",
2339
+ background: "rgba(255, 255, 255, 0.15)",
2340
+ color: "#fff",
2341
+ width: 32,
2342
+ height: 32,
2343
+ borderRadius: 8,
2344
+ cursor: "pointer",
2345
+ display: "flex",
2346
+ alignItems: "center",
2347
+ justifyContent: "center",
2348
+ transition: "background 0.2s"
2349
+ },
2350
+ // Messages area
2351
+ messages: {
2352
+ flex: 1,
2353
+ padding: "20px 18px",
2354
+ overflowY: "auto",
2355
+ background: "#f8fafc",
2356
+ display: "flex",
2357
+ flexDirection: "column",
2358
+ gap: 16
2359
+ },
2360
+ // Message bubbles
2361
+ messageWrapper: {
2362
+ display: "flex",
2363
+ flexDirection: "column",
2364
+ gap: 8
2365
+ },
2366
+ messageBubble: {
2367
+ maxWidth: "85%",
2368
+ padding: "12px 14px",
2369
+ borderRadius: 16,
2370
+ lineHeight: 1.5,
2371
+ fontSize: 14,
2372
+ whiteSpace: "pre-wrap",
2373
+ wordBreak: "break-word"
2374
+ },
2375
+ userMessage: {
2376
+ alignSelf: "flex-end",
2377
+ background: "linear-gradient(135deg, #10b981, #059669)",
2378
+ color: "#fff",
2379
+ borderBottomRightRadius: 4
2380
+ },
2381
+ assistantMessage: {
2382
+ alignSelf: "flex-start",
2383
+ background: "#e2e8f0",
2384
+ color: "#0f172a",
2385
+ borderBottomLeftRadius: 4
2386
+ },
2387
+ // Streaming cursor
2388
+ cursor: {
2389
+ display: "inline-block",
2390
+ width: 2,
2391
+ height: 16,
2392
+ background: "#0f172a",
2393
+ marginLeft: 2,
2394
+ animation: "zenitBlink 1s infinite",
2395
+ verticalAlign: "text-bottom"
2396
+ },
2397
+ thinkingText: {
2398
+ fontStyle: "italic",
2399
+ opacity: 0.7,
2400
+ display: "flex",
2401
+ alignItems: "center",
2402
+ gap: 8
2403
+ },
2404
+ typingIndicator: {
2405
+ display: "flex",
2406
+ gap: 4,
2407
+ alignItems: "center"
2408
+ },
2409
+ typingDot: {
2410
+ width: 8,
2411
+ height: 8,
2412
+ borderRadius: "50%",
2413
+ backgroundColor: "#059669",
2414
+ animation: "typingDotBounce 1.4s infinite ease-in-out"
2415
+ },
2416
+ // Metadata section (Referenced Layers)
2417
+ metadataSection: {
2418
+ marginTop: 12,
2419
+ padding: "10px 12px",
2420
+ background: "rgba(255, 255, 255, 0.5)",
2421
+ borderRadius: 10,
2422
+ fontSize: 12
2423
+ },
2424
+ metadataTitle: {
2425
+ fontWeight: 600,
2426
+ color: "#059669",
2427
+ marginBottom: 6,
2428
+ display: "flex",
2429
+ alignItems: "center",
2430
+ gap: 6
2431
+ },
2432
+ metadataList: {
2433
+ margin: 0,
2434
+ paddingLeft: 18,
2435
+ color: "#475569"
2436
+ },
2437
+ metadataItem: {
2438
+ marginBottom: 2
2439
+ },
2440
+ // Suggested actions
2441
+ actionsSection: {
2442
+ marginTop: 12
2443
+ },
2444
+ sectionLabel: {
2445
+ fontSize: 11,
2446
+ fontWeight: 600,
2447
+ color: "#64748b",
2448
+ marginBottom: 8,
2449
+ textTransform: "uppercase",
2450
+ letterSpacing: "0.05em"
2451
+ },
2452
+ actionsGrid: {
2453
+ display: "flex",
2454
+ flexWrap: "wrap",
2455
+ gap: 6
2456
+ },
2457
+ actionButton: {
2458
+ border: "1px solid #d1fae5",
2459
+ background: "#d1fae5",
2460
+ color: "#065f46",
2461
+ borderRadius: 999,
2462
+ padding: "6px 12px",
2463
+ fontSize: 12,
2464
+ fontWeight: 500,
2465
+ cursor: "pointer",
2466
+ transition: "all 0.2s"
2467
+ },
2468
+ // Follow-up questions
2469
+ followUpButton: {
2470
+ border: "1px solid #cbd5e1",
2471
+ background: "#fff",
2472
+ color: "#475569",
2473
+ borderRadius: 10,
2474
+ padding: "8px 12px",
2475
+ fontSize: 12,
2476
+ fontWeight: 500,
2477
+ cursor: "pointer",
2478
+ textAlign: "left",
2479
+ width: "100%",
2480
+ transition: "all 0.2s"
2481
+ },
2482
+ // Input area
2483
+ inputWrapper: {
2484
+ borderTop: "1px solid #e2e8f0",
2485
+ padding: "14px 16px",
2486
+ display: "flex",
2487
+ gap: 10,
2488
+ alignItems: "flex-end",
2489
+ background: "#fff"
2490
+ },
2491
+ textarea: {
2492
+ flex: 1,
2493
+ resize: "none",
2494
+ borderRadius: 12,
2495
+ border: "1.5px solid #cbd5e1",
2496
+ padding: "10px 12px",
2497
+ fontSize: 14,
2498
+ fontFamily: "inherit",
2499
+ lineHeight: 1.4,
2500
+ transition: "border-color 0.2s"
2501
+ },
2502
+ textareaFocus: {
2503
+ borderColor: "#10b981",
2504
+ outline: "none"
2505
+ },
2506
+ sendButton: {
2507
+ borderRadius: 12,
2508
+ border: "none",
2509
+ padding: "10px 14px",
2510
+ background: "linear-gradient(135deg, #10b981, #059669)",
2511
+ color: "#fff",
2512
+ cursor: "pointer",
2513
+ fontSize: 14,
2514
+ fontWeight: 600,
2515
+ display: "flex",
2516
+ alignItems: "center",
2517
+ justifyContent: "center",
2518
+ transition: "opacity 0.2s, transform 0.2s",
2519
+ minWidth: 44,
2520
+ height: 44
2521
+ },
2522
+ // Status messages
2523
+ statusNote: {
2524
+ padding: "0 16px 14px",
2525
+ fontSize: 12,
2526
+ color: "#64748b",
2527
+ textAlign: "center"
2528
+ },
2529
+ errorText: {
2530
+ padding: "0 16px 14px",
2531
+ fontSize: 12,
2532
+ color: "#dc2626",
2533
+ textAlign: "center"
2534
+ }
2535
+ };
2536
+ var FloatingChatBox = ({
2537
+ mapId,
2538
+ filteredLayerIds,
2539
+ filters,
2540
+ userId,
2541
+ baseUrl,
2542
+ accessToken,
2543
+ getAccessToken,
2544
+ onActionClick,
2545
+ onOpenChange,
2546
+ hideButton
2547
+ }) => {
2548
+ const [open, setOpen] = useState4(false);
2549
+ const [expanded, setExpanded] = useState4(false);
2550
+ const [messages, setMessages] = useState4([]);
2551
+ const [inputValue, setInputValue] = useState4("");
2552
+ const [conversationId, setConversationId] = useState4();
2553
+ const [errorMessage, setErrorMessage] = useState4(null);
2554
+ const [isFocused, setIsFocused] = useState4(false);
2555
+ const [isMobile, setIsMobile] = useState4(false);
2556
+ const messagesEndRef = useRef4(null);
2557
+ const messagesContainerRef = useRef4(null);
2558
+ const chatBoxRef = useRef4(null);
2559
+ const chatConfig = useMemo3(() => {
2560
+ if (!baseUrl) return void 0;
2561
+ return { baseUrl, accessToken, getAccessToken };
2562
+ }, [accessToken, baseUrl, getAccessToken]);
2563
+ const { sendMessage: sendMessage2, isStreaming, streamingText, completeResponse } = useSendMessageStream(chatConfig);
2564
+ const canSend = Boolean(mapId) && Boolean(baseUrl) && inputValue.trim().length > 0 && !isStreaming;
2565
+ useEffect3(() => {
2566
+ onOpenChange?.(open);
2567
+ }, [open, onOpenChange]);
2568
+ useEffect3(() => {
2569
+ if (open && isMobile) {
2570
+ setExpanded(true);
2571
+ }
2572
+ }, [open, isMobile]);
2573
+ const scrollToBottom = useCallback3(() => {
2574
+ if (messagesEndRef.current) {
2575
+ messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
2576
+ }
2577
+ }, []);
2578
+ useEffect3(() => {
2579
+ if (open && messages.length === 0) {
2580
+ setMessages([
2581
+ {
2582
+ id: "welcome",
2583
+ role: "assistant",
2584
+ content: "Hola, soy el asistente de IA de ZENIT. Estoy aqu\xED para ayudarte a analizar tu mapa y responder tus preguntas sobre las capas y datos. \xBFEn qu\xE9 puedo ayudarte hoy?"
2585
+ }
2586
+ ]);
2587
+ }
2588
+ }, [open, messages.length]);
2589
+ useEffect3(() => {
2590
+ scrollToBottom();
2591
+ }, [messages, streamingText, scrollToBottom]);
2592
+ useEffect3(() => {
2593
+ if (!open) return;
2594
+ if (isMobile && expanded) return;
2595
+ const handleClickOutside = (event) => {
2596
+ if (chatBoxRef.current && !chatBoxRef.current.contains(event.target)) {
2597
+ setOpen(false);
2598
+ }
2599
+ };
2600
+ document.addEventListener("mousedown", handleClickOutside);
2601
+ return () => {
2602
+ document.removeEventListener("mousedown", handleClickOutside);
2603
+ };
2604
+ }, [open, isMobile, expanded]);
2605
+ useEffect3(() => {
2606
+ if (typeof window === "undefined") return;
2607
+ const mediaQuery = window.matchMedia("(max-width: 768px)");
2608
+ const updateMobile = () => setIsMobile(mediaQuery.matches);
2609
+ updateMobile();
2610
+ if (mediaQuery.addEventListener) {
2611
+ mediaQuery.addEventListener("change", updateMobile);
2612
+ } else {
2613
+ mediaQuery.addListener(updateMobile);
2614
+ }
2615
+ return () => {
2616
+ if (mediaQuery.removeEventListener) {
2617
+ mediaQuery.removeEventListener("change", updateMobile);
2618
+ } else {
2619
+ mediaQuery.removeListener(updateMobile);
2620
+ }
2621
+ };
2622
+ }, []);
2623
+ useEffect3(() => {
2624
+ if (typeof document === "undefined") return;
2625
+ if (!open || !isMobile) return;
2626
+ document.body.style.overflow = "hidden";
2627
+ return () => {
2628
+ document.body.style.overflow = "";
2629
+ };
2630
+ }, [open, isMobile]);
2631
+ const addMessage = useCallback3((message) => {
2632
+ setMessages((prev) => [...prev, message]);
2633
+ }, []);
2634
+ const handleSend = useCallback3(async () => {
2635
+ if (!mapId) {
2636
+ setErrorMessage("Selecciona un mapa para usar el asistente.");
2637
+ return;
2638
+ }
2639
+ if (!baseUrl) {
2640
+ setErrorMessage("Configura la baseUrl del SDK para usar Zenit AI.");
2641
+ return;
2642
+ }
2643
+ if (!inputValue.trim()) return;
2644
+ const messageText = inputValue.trim();
2645
+ setInputValue("");
2646
+ setErrorMessage(null);
2647
+ addMessage({
2648
+ id: `user-${Date.now()}`,
2649
+ role: "user",
2650
+ content: messageText
2651
+ });
2652
+ const request = {
2653
+ message: messageText,
2654
+ conversationId,
2655
+ filteredLayerIds,
2656
+ filters,
2657
+ userId
2658
+ };
2659
+ try {
2660
+ const response = await sendMessage2(mapId, request);
2661
+ setConversationId(response.conversationId ?? conversationId);
2662
+ addMessage({
2663
+ id: `assistant-${Date.now()}`,
2664
+ role: "assistant",
2665
+ content: response.answer,
2666
+ response
2667
+ });
2668
+ } catch (error) {
2669
+ setErrorMessage(error instanceof Error ? error.message : "Ocurri\xF3 un error inesperado.");
2670
+ addMessage({
2671
+ id: `error-${Date.now()}`,
2672
+ role: "assistant",
2673
+ content: `\u274C Error: ${error instanceof Error ? error.message : "Ocurri\xF3 un error inesperado."}`
2674
+ });
2675
+ }
2676
+ }, [
2677
+ addMessage,
2678
+ baseUrl,
2679
+ conversationId,
2680
+ filteredLayerIds,
2681
+ filters,
2682
+ inputValue,
2683
+ mapId,
2684
+ sendMessage2,
2685
+ userId
2686
+ ]);
2687
+ const handleKeyDown = useCallback3(
2688
+ (event) => {
2689
+ if (event.key === "Enter" && !event.shiftKey) {
2690
+ event.preventDefault();
2691
+ if (canSend) {
2692
+ void handleSend();
2693
+ }
2694
+ }
2695
+ },
2696
+ [canSend, handleSend]
2697
+ );
2698
+ const handleFollowUpClick = useCallback3((question) => {
2699
+ setInputValue(question);
2700
+ }, []);
2701
+ const renderMetadata = (response) => {
2702
+ if (!response?.metadata) return null;
2703
+ const referencedLayers = response.metadata.referencedLayers;
2704
+ if (!referencedLayers || referencedLayers.length === 0) return null;
2705
+ return /* @__PURE__ */ jsxs4("div", { style: styles.metadataSection, children: [
2706
+ /* @__PURE__ */ jsxs4("div", { style: styles.metadataTitle, children: [
2707
+ /* @__PURE__ */ jsx5(LayersIcon, {}),
2708
+ "Capas Analizadas"
2709
+ ] }),
2710
+ /* @__PURE__ */ jsx5("ul", { style: styles.metadataList, children: referencedLayers.map((layer, index) => /* @__PURE__ */ jsxs4("li", { style: styles.metadataItem, children: [
2711
+ /* @__PURE__ */ jsx5("strong", { children: layer.layerName }),
2712
+ " (",
2713
+ layer.featureCount,
2714
+ " ",
2715
+ layer.featureCount === 1 ? "elemento" : "elementos",
2716
+ ")"
2717
+ ] }, index)) })
2718
+ ] });
2719
+ };
2720
+ const renderActions = (response) => {
2721
+ if (!response?.suggestedActions?.length) return null;
2722
+ return /* @__PURE__ */ jsxs4("div", { style: styles.actionsSection, children: [
2723
+ /* @__PURE__ */ jsx5("div", { style: styles.sectionLabel, children: "Acciones Sugeridas" }),
2724
+ /* @__PURE__ */ jsx5("div", { style: styles.actionsGrid, children: response.suggestedActions.map((action, index) => /* @__PURE__ */ jsx5(
2725
+ "button",
2726
+ {
2727
+ type: "button",
2728
+ style: {
2729
+ ...styles.actionButton,
2730
+ opacity: isStreaming ? 0.5 : 1,
2731
+ cursor: isStreaming ? "not-allowed" : "pointer"
2732
+ },
2733
+ onClick: () => !isStreaming && onActionClick?.(action),
2734
+ disabled: isStreaming,
2735
+ onMouseEnter: (e) => {
2736
+ if (!isStreaming) {
2737
+ e.currentTarget.style.background = "#a7f3d0";
2738
+ e.currentTarget.style.borderColor = "#a7f3d0";
2739
+ }
2740
+ },
2741
+ onMouseLeave: (e) => {
2742
+ if (!isStreaming) {
2743
+ e.currentTarget.style.background = "#d1fae5";
2744
+ e.currentTarget.style.borderColor = "#d1fae5";
2745
+ }
2746
+ },
2747
+ children: action.label ?? action.action ?? "Acci\xF3n"
2748
+ },
2749
+ `${action.label ?? action.action ?? "action"}-${index}`
2750
+ )) })
2751
+ ] });
2752
+ };
2753
+ const renderFollowUps = (response) => {
2754
+ if (!response?.followUpQuestions?.length) return null;
2755
+ return /* @__PURE__ */ jsxs4("div", { style: styles.actionsSection, children: [
2756
+ /* @__PURE__ */ jsx5("div", { style: styles.sectionLabel, children: "Preguntas Relacionadas" }),
2757
+ /* @__PURE__ */ jsx5("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: response.followUpQuestions.map((question, index) => /* @__PURE__ */ jsx5(
2758
+ "button",
2759
+ {
2760
+ type: "button",
2761
+ style: {
2762
+ ...styles.followUpButton,
2763
+ opacity: isStreaming ? 0.5 : 1,
2764
+ cursor: isStreaming ? "not-allowed" : "pointer"
2765
+ },
2766
+ onClick: () => !isStreaming && handleFollowUpClick(question),
2767
+ disabled: isStreaming,
2768
+ onMouseEnter: (e) => {
2769
+ if (!isStreaming) {
2770
+ e.currentTarget.style.background = "#f8fafc";
2771
+ e.currentTarget.style.borderColor = "#10b981";
2772
+ }
2773
+ },
2774
+ onMouseLeave: (e) => {
2775
+ if (!isStreaming) {
2776
+ e.currentTarget.style.background = "#fff";
2777
+ e.currentTarget.style.borderColor = "#cbd5e1";
2778
+ }
2779
+ },
2780
+ children: question
2781
+ },
2782
+ `followup-${index}`
2783
+ )) })
2784
+ ] });
2785
+ };
2786
+ const chatContent = /* @__PURE__ */ jsxs4("div", { style: styles.root, children: [
2787
+ /* @__PURE__ */ jsx5("style", { children: `
2788
+ @keyframes zenitBlink {
2789
+ 0%, 49% { opacity: 1; }
2790
+ 50%, 100% { opacity: 0; }
2791
+ }
2792
+ @keyframes zenitPulse {
2793
+ 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
2794
+ 70% { box-shadow: 0 0 0 14px rgba(16, 185, 129, 0); }
2795
+ 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
2796
+ }
2797
+ @keyframes typingDotBounce {
2798
+ 0%, 60%, 100% { transform: translateY(0); }
2799
+ 30% { transform: translateY(-8px); }
2800
+ }
2801
+ .zenit-typing-dot:nth-child(1) { animation-delay: 0s; }
2802
+ .zenit-typing-dot:nth-child(2) { animation-delay: 0.2s; }
2803
+ .zenit-typing-dot:nth-child(3) { animation-delay: 0.4s; }
2804
+ .zenit-ai-button:not(.open) {
2805
+ animation: zenitPulse 2.5s infinite;
2806
+ }
2807
+ .zenit-ai-button:hover {
2808
+ transform: scale(1.05);
2809
+ }
2810
+ .zenit-ai-button:active {
2811
+ transform: scale(0.95);
2812
+ }
2813
+ .zenit-send-button:disabled {
2814
+ opacity: 0.5;
2815
+ cursor: not-allowed;
2816
+ }
2817
+ .zenit-send-button:not(:disabled):hover {
2818
+ opacity: 0.9;
2819
+ transform: translateY(-1px);
2820
+ }
2821
+ .zenit-send-button:not(:disabled):active {
2822
+ transform: translateY(0);
2823
+ }
2824
+ .zenit-chat-panel {
2825
+ box-sizing: border-box;
2826
+ }
2827
+ @media (max-width: 768px) {
2828
+ .zenit-chat-panel.zenit-chat-panel--fullscreen {
2829
+ position: fixed !important;
2830
+ inset: 0 !important;
2831
+ width: 100vw !important;
2832
+ max-width: 100vw !important;
2833
+ height: 100vh !important;
2834
+ height: 100dvh !important;
2835
+ border-radius: 0 !important;
2836
+ display: flex !important;
2837
+ flex-direction: column !important;
2838
+ overflow: hidden !important;
2839
+ z-index: 100000 !important;
2840
+ padding-top: env(safe-area-inset-top);
2841
+ }
2842
+ .zenit-chat-panel.zenit-chat-panel--fullscreen .zenit-ai-body {
2843
+ flex: 1;
2844
+ min-height: 0;
2845
+ overflow-y: auto;
2846
+ -webkit-overflow-scrolling: touch;
2847
+ }
2848
+ .zenit-chat-panel.zenit-chat-panel--fullscreen .zenit-ai-input-area {
2849
+ flex-shrink: 0;
2850
+ padding-bottom: max(14px, env(safe-area-inset-bottom));
2851
+ }
2852
+ .zenit-ai-button.zenit-ai-button--hidden-mobile {
2853
+ display: none !important;
2854
+ }
2855
+ }
2856
+ ` }),
2857
+ open && /* @__PURE__ */ jsxs4(
2858
+ "div",
2859
+ {
2860
+ ref: chatBoxRef,
2861
+ className: `zenit-chat-panel${expanded ? " zenit-chat-panel--expanded" : ""}${isMobile ? " zenit-chat-panel--fullscreen" : ""}`,
2862
+ style: {
2863
+ ...styles.panel,
2864
+ ...expanded ? styles.panelExpanded : styles.panelNormal
2865
+ },
2866
+ children: [
2867
+ /* @__PURE__ */ jsxs4("header", { style: styles.header, children: [
2868
+ /* @__PURE__ */ jsx5("h3", { style: styles.title, children: "Asistente Zenit AI" }),
2869
+ /* @__PURE__ */ jsxs4("div", { style: styles.headerButtons, children: [
2870
+ /* @__PURE__ */ jsx5(
2871
+ "button",
2872
+ {
2873
+ type: "button",
2874
+ style: styles.headerButton,
2875
+ onClick: () => setExpanded(!expanded),
2876
+ onMouseEnter: (e) => {
2877
+ e.currentTarget.style.background = "rgba(255, 255, 255, 0.25)";
2878
+ },
2879
+ onMouseLeave: (e) => {
2880
+ e.currentTarget.style.background = "rgba(255, 255, 255, 0.15)";
2881
+ },
2882
+ "aria-label": expanded ? "Contraer" : "Expandir",
2883
+ children: expanded ? /* @__PURE__ */ jsx5(CollapseIcon, {}) : /* @__PURE__ */ jsx5(ExpandIcon, {})
2884
+ }
2885
+ ),
2886
+ /* @__PURE__ */ jsx5(
2887
+ "button",
2888
+ {
2889
+ type: "button",
2890
+ style: styles.headerButton,
2891
+ onClick: () => setOpen(false),
2892
+ onMouseEnter: (e) => {
2893
+ e.currentTarget.style.background = "rgba(255, 255, 255, 0.25)";
2894
+ },
2895
+ onMouseLeave: (e) => {
2896
+ e.currentTarget.style.background = "rgba(255, 255, 255, 0.15)";
2897
+ },
2898
+ "aria-label": "Cerrar",
2899
+ children: /* @__PURE__ */ jsx5(CloseIcon, {})
2900
+ }
2901
+ )
2902
+ ] })
2903
+ ] }),
2904
+ /* @__PURE__ */ jsxs4("div", { ref: messagesContainerRef, className: "zenit-ai-body", style: styles.messages, children: [
2905
+ messages.map((message) => /* @__PURE__ */ jsx5(
2906
+ "div",
2907
+ {
2908
+ style: {
2909
+ ...styles.messageWrapper,
2910
+ alignItems: message.role === "user" ? "flex-end" : "flex-start"
2911
+ },
2912
+ children: /* @__PURE__ */ jsxs4(
2913
+ "div",
2914
+ {
2915
+ style: {
2916
+ ...styles.messageBubble,
2917
+ ...message.role === "user" ? styles.userMessage : styles.assistantMessage
2918
+ },
2919
+ children: [
2920
+ message.role === "assistant" ? /* @__PURE__ */ jsx5(MarkdownRenderer, { content: message.content }) : message.content,
2921
+ message.role === "assistant" && renderMetadata(message.response),
2922
+ message.role === "assistant" && renderActions(message.response),
2923
+ message.role === "assistant" && renderFollowUps(message.response)
2924
+ ]
2925
+ }
2926
+ )
2927
+ },
2928
+ message.id
2929
+ )),
2930
+ isStreaming && /* @__PURE__ */ jsx5(
2931
+ "div",
2932
+ {
2933
+ style: {
2934
+ ...styles.messageWrapper,
2935
+ alignItems: "flex-start"
2936
+ },
2937
+ children: /* @__PURE__ */ jsx5(
2938
+ "div",
2939
+ {
2940
+ style: {
2941
+ ...styles.messageBubble,
2942
+ ...styles.assistantMessage
2943
+ },
2944
+ children: streamingText ? /* @__PURE__ */ jsxs4(Fragment, { children: [
2945
+ /* @__PURE__ */ jsx5(MarkdownRenderer, { content: streamingText }),
2946
+ /* @__PURE__ */ jsx5("span", { style: styles.cursor })
2947
+ ] }) : /* @__PURE__ */ jsxs4("div", { style: styles.thinkingText, children: [
2948
+ /* @__PURE__ */ jsx5("span", { children: "Analizando" }),
2949
+ /* @__PURE__ */ jsxs4("div", { style: styles.typingIndicator, children: [
2950
+ /* @__PURE__ */ jsx5("div", { className: "zenit-typing-dot", style: styles.typingDot }),
2951
+ /* @__PURE__ */ jsx5("div", { className: "zenit-typing-dot", style: styles.typingDot }),
2952
+ /* @__PURE__ */ jsx5("div", { className: "zenit-typing-dot", style: styles.typingDot })
2953
+ ] })
2954
+ ] })
2955
+ }
2956
+ )
2957
+ }
2958
+ ),
2959
+ /* @__PURE__ */ jsx5("div", { ref: messagesEndRef })
2960
+ ] }),
2961
+ /* @__PURE__ */ jsxs4("div", { className: "zenit-ai-input-area", style: styles.inputWrapper, children: [
2962
+ /* @__PURE__ */ jsx5(
2963
+ "textarea",
2964
+ {
2965
+ style: {
2966
+ ...styles.textarea,
2967
+ ...isFocused ? styles.textareaFocus : {}
2968
+ },
2969
+ rows: 2,
2970
+ value: inputValue,
2971
+ onChange: (event) => setInputValue(event.target.value),
2972
+ onKeyDown: handleKeyDown,
2973
+ onFocus: () => setIsFocused(true),
2974
+ onBlur: () => setIsFocused(false),
2975
+ placeholder: !mapId ? "Selecciona un mapa para comenzar" : isStreaming ? "Esperando respuesta..." : "Escribe tu pregunta...",
2976
+ disabled: !mapId || !baseUrl || isStreaming
2977
+ }
2978
+ ),
2979
+ /* @__PURE__ */ jsx5(
2980
+ "button",
2981
+ {
2982
+ type: "button",
2983
+ className: "zenit-send-button",
2984
+ style: styles.sendButton,
2985
+ onClick: () => void handleSend(),
2986
+ disabled: !canSend,
2987
+ "aria-label": "Enviar mensaje",
2988
+ children: /* @__PURE__ */ jsx5(SendIcon, {})
2989
+ }
2990
+ )
2991
+ ] }),
2992
+ errorMessage && /* @__PURE__ */ jsx5("div", { style: styles.errorText, children: errorMessage }),
2993
+ !mapId && !errorMessage && /* @__PURE__ */ jsx5("div", { style: styles.statusNote, children: "Selecciona un mapa para usar el asistente" }),
2994
+ !baseUrl && !errorMessage && /* @__PURE__ */ jsx5("div", { style: styles.statusNote, children: "Configura la baseUrl del SDK" })
2995
+ ]
2996
+ }
2997
+ ),
2998
+ !(hideButton && !open) && /* @__PURE__ */ jsx5(
2999
+ "button",
3000
+ {
3001
+ type: "button",
3002
+ className: `zenit-ai-button ${open ? "open" : ""}${open && isMobile ? " zenit-ai-button--hidden-mobile" : ""}`,
3003
+ style: {
3004
+ ...styles.floatingButton,
3005
+ ...open ? styles.floatingButtonOpen : isMobile ? styles.floatingButtonMobile : styles.floatingButtonClosed
3006
+ },
3007
+ onClick: () => setOpen((prev) => !prev),
3008
+ "aria-label": open ? "Cerrar asistente" : "Abrir asistente Zenit AI",
3009
+ children: open ? /* @__PURE__ */ jsx5(CloseIcon, {}) : /* @__PURE__ */ jsxs4(Fragment, { children: [
3010
+ /* @__PURE__ */ jsx5(ChatIcon, {}),
3011
+ !isMobile && /* @__PURE__ */ jsx5("span", { children: "Asistente IA" })
3012
+ ] })
3013
+ }
3014
+ )
3015
+ ] });
3016
+ if (typeof document !== "undefined") {
3017
+ return createPortal(chatContent, document.body);
3018
+ }
3019
+ return chatContent;
3020
+ };
3021
+
3022
+ export {
3023
+ extractMapDto,
3024
+ normalizeMapCenter,
3025
+ normalizeMapLayers,
3026
+ computeBBoxFromFeature,
3027
+ centroidFromBBox,
3028
+ applyFilteredGeoJSONStrategy,
3029
+ initLayerStates,
3030
+ applyLayerOverrides,
3031
+ resetOverrides,
3032
+ sendMessage,
3033
+ sendMessageStream,
3034
+ createChatService,
3035
+ resolveLayerAccent,
3036
+ getLayerColor,
3037
+ getStyleByLayerId,
3038
+ getAccentByLayerId,
3039
+ clampNumber,
3040
+ clampOpacity3 as clampOpacity,
3041
+ isPolygonLayer,
3042
+ getZoomOpacityFactor,
3043
+ getLayerZoomOpacityFactor,
3044
+ getEffectiveLayerOpacity,
3045
+ ZenitMap,
3046
+ Eye,
3047
+ EyeOff,
3048
+ ChevronLeft,
3049
+ ChevronRight,
3050
+ Layers,
3051
+ Upload,
3052
+ X,
3053
+ ZoomIn,
3054
+ ZenitLayerManager,
3055
+ ZenitFeatureFilterPanel,
3056
+ useSendMessage,
3057
+ useSendMessageStream,
3058
+ FloatingChatBox
3059
+ };
3060
+ //# sourceMappingURL=chunk-R73LRYVJ.mjs.map