zenit-sdk 0.1.5 → 0.1.6

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,4830 @@
1
+ // src/react/ZenitMap.tsx
2
+ import React4, { useCallback as useCallback2, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as useRef5, useState as useState2, forwardRef } from "react";
3
+ import { MapContainer, Marker as Marker2, TileLayer, ZoomControl } from "react-leaflet";
4
+ import L4 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: 11,
210
+ maxZoom: 15,
211
+ minFactor: 0.3,
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/map/layer-geojson.tsx
249
+ import { useEffect, useMemo, useRef } from "react";
250
+ import { GeoJSON } from "react-leaflet";
251
+ import L from "leaflet";
252
+
253
+ // src/react/map/geojson-sanitize.ts
254
+ var warnedKeys = /* @__PURE__ */ new Set();
255
+ function isFiniteNumber(value) {
256
+ if (typeof value === "number") return Number.isFinite(value);
257
+ if (typeof value === "string") return Number.isFinite(Number(value));
258
+ return false;
259
+ }
260
+ function isValidLonLat(lon, lat) {
261
+ const lonN = typeof lon === "string" ? Number(lon) : lon;
262
+ const latN = typeof lat === "string" ? Number(lat) : lat;
263
+ if (!isFiniteNumber(lonN) || !isFiniteNumber(latN)) return false;
264
+ if (typeof lonN !== "number" || typeof latN !== "number") return false;
265
+ return lonN >= -180 && lonN <= 180 && latN >= -90 && latN <= 90;
266
+ }
267
+ function asFeatureCollection(input) {
268
+ const raw = input && typeof input === "object" && "data" in input ? input.data : input;
269
+ if (!raw || typeof raw !== "object") {
270
+ return { type: "FeatureCollection", features: [] };
271
+ }
272
+ if (raw.type === "FeatureCollection") {
273
+ const collection = raw;
274
+ const features = Array.isArray(collection.features) ? collection.features : [];
275
+ return { ...collection, features };
276
+ }
277
+ if (raw.type === "Feature") {
278
+ return {
279
+ type: "FeatureCollection",
280
+ features: [raw]
281
+ };
282
+ }
283
+ return { type: "FeatureCollection", features: [] };
284
+ }
285
+ function sanitizeFeature(feature) {
286
+ if (!feature || typeof feature !== "object") return null;
287
+ const geometry = feature.geometry;
288
+ if (!geometry || typeof geometry !== "object") return null;
289
+ const geometryType = geometry.type;
290
+ const coordinates = geometry.coordinates;
291
+ if (geometryType === "Point") {
292
+ if (!Array.isArray(coordinates) || coordinates.length < 2) return null;
293
+ return isValidLonLat(coordinates[0], coordinates[1]) ? feature : null;
294
+ }
295
+ if (geometryType === "MultiPoint") {
296
+ if (!Array.isArray(coordinates)) return null;
297
+ const validCoordinates = coordinates.filter((coordinate) => {
298
+ if (!Array.isArray(coordinate) || coordinate.length < 2) return false;
299
+ return isValidLonLat(coordinate[0], coordinate[1]);
300
+ });
301
+ if (validCoordinates.length === 0) return null;
302
+ if (validCoordinates.length === coordinates.length) {
303
+ return feature;
304
+ }
305
+ return {
306
+ ...feature,
307
+ geometry: {
308
+ ...geometry,
309
+ coordinates: validCoordinates
310
+ }
311
+ };
312
+ }
313
+ if (geometryType === "LineString" || geometryType === "Polygon" || geometryType === "MultiPolygon" || geometryType === "MultiLineString") {
314
+ return Array.isArray(coordinates) && coordinates.length > 0 ? feature : null;
315
+ }
316
+ return feature;
317
+ }
318
+ function logSanitizeStats(stats, debugKey) {
319
+ if (process.env.NODE_ENV === "production") return;
320
+ if (stats.droppedFeatures === 0 && stats.droppedPointsFromMultiPoint === 0) return;
321
+ const resolvedKey = debugKey ?? "__default__";
322
+ if (warnedKeys.has(resolvedKey)) return;
323
+ warnedKeys.add(resolvedKey);
324
+ console.warn("[zenit-sdk] GeoJSON sanitization dropped invalid geometries.", {
325
+ layerId: debugKey,
326
+ ...stats
327
+ });
328
+ }
329
+ function sanitizeGeoJson(input, debugKey) {
330
+ const featureCollection = asFeatureCollection(input);
331
+ const safeFeatures = [];
332
+ const stats = {
333
+ droppedFeatures: 0,
334
+ droppedPointsFromMultiPoint: 0
335
+ };
336
+ const rawFeatures = Array.isArray(featureCollection.features) ? featureCollection.features : [];
337
+ rawFeatures.forEach((rawFeature) => {
338
+ const feature = rawFeature;
339
+ const geometryType = feature?.geometry?.type;
340
+ const originalCoords = feature?.geometry?.coordinates;
341
+ const sanitized = sanitizeFeature(feature);
342
+ if (!sanitized) {
343
+ stats.droppedFeatures += 1;
344
+ if (geometryType === "MultiPoint" && Array.isArray(originalCoords)) {
345
+ stats.droppedPointsFromMultiPoint += originalCoords.length;
346
+ }
347
+ return;
348
+ }
349
+ if (geometryType === "MultiPoint" && Array.isArray(originalCoords)) {
350
+ const safeCoords = sanitized.geometry?.coordinates;
351
+ const safeCount = Array.isArray(safeCoords) ? safeCoords.length : 0;
352
+ stats.droppedPointsFromMultiPoint += Math.max(0, originalCoords.length - safeCount);
353
+ }
354
+ safeFeatures.push(sanitized);
355
+ });
356
+ logSanitizeStats(stats, debugKey);
357
+ return {
358
+ ...featureCollection,
359
+ type: "FeatureCollection",
360
+ features: safeFeatures
361
+ };
362
+ }
363
+
364
+ // src/react/map/layer-geojson.tsx
365
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
366
+ var POINT_GEOMETRY_TYPES = /* @__PURE__ */ new Set(["Point", "MultiPoint"]);
367
+ var DEV_MODE = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
368
+ function normalizeBboxFromData(data) {
369
+ const bboxCandidate = data.bbox;
370
+ if (!Array.isArray(bboxCandidate) || bboxCandidate.length < 4) return null;
371
+ const [minLon, minLat, maxLon, maxLat] = bboxCandidate;
372
+ const values = [minLon, minLat, maxLon, maxLat].map((value) => Number(value));
373
+ if (values.every((value) => Number.isFinite(value))) {
374
+ return values;
375
+ }
376
+ return null;
377
+ }
378
+ function buildIdsSample(features) {
379
+ return features.slice(0, 10).map((feature) => {
380
+ const typedFeature = feature;
381
+ if (typedFeature.id !== void 0 && typedFeature.id !== null) {
382
+ return String(typedFeature.id);
383
+ }
384
+ const featureId = typedFeature.properties?.featureId;
385
+ return featureId !== void 0 && featureId !== null ? String(featureId) : "";
386
+ }).join(",");
387
+ }
388
+ function getGeometryType(feature) {
389
+ const t = feature?.geometry?.type;
390
+ return typeof t === "string" ? t : null;
391
+ }
392
+ function isPointGeometry(feature) {
393
+ const geometryType = getGeometryType(feature);
394
+ return geometryType !== null && POINT_GEOMETRY_TYPES.has(geometryType);
395
+ }
396
+ function isNonPointGeometry(feature) {
397
+ const geometryType = getGeometryType(feature);
398
+ return geometryType !== null && !POINT_GEOMETRY_TYPES.has(geometryType);
399
+ }
400
+ function isValidLatLng(latlng) {
401
+ const candidate = latlng;
402
+ return Boolean(
403
+ candidate && Number.isFinite(candidate.lat) && Number.isFinite(candidate.lng)
404
+ );
405
+ }
406
+ function createInvisibleFallbackMarker(latlng) {
407
+ return L.circleMarker(latlng ?? [0, 0], {
408
+ radius: 0,
409
+ opacity: 0,
410
+ fillOpacity: 0,
411
+ interactive: false
412
+ });
413
+ }
414
+ function createInvisibleFallbackClusterMarker(latlng) {
415
+ return L.marker(latlng ?? [0, 0], {
416
+ opacity: 0,
417
+ interactive: false
418
+ });
419
+ }
420
+ function ensurePaneName(mapInstance, paneName, fallbackPaneName) {
421
+ if (!mapInstance) return fallbackPaneName;
422
+ const existingPane = mapInstance.getPane(paneName) ?? mapInstance.createPane(paneName);
423
+ if (!existingPane && DEV_MODE) {
424
+ console.warn("[LayerGeoJson] pane unavailable, using fallback", { paneName, fallbackPaneName });
425
+ }
426
+ return mapInstance.getPane(paneName) ? paneName : fallbackPaneName;
427
+ }
428
+ function createPointDivIcon(style, isMobile) {
429
+ const size = isMobile ? 18 : 14;
430
+ const backgroundColor = style.fillColor ?? style.color ?? "#3388ff";
431
+ const borderColor = style.color ?? style.fillColor ?? "#3388ff";
432
+ return L.divIcon({
433
+ className: "zenit-point-marker",
434
+ html: `<div style="width:${size}px;height:${size}px;border-radius:50%;background:${backgroundColor};border:2px solid ${borderColor};"></div>`,
435
+ iconSize: [size, size],
436
+ iconAnchor: [size / 2, size / 2]
437
+ });
438
+ }
439
+ function buildFeatureCollection(features) {
440
+ return {
441
+ type: "FeatureCollection",
442
+ features
443
+ };
444
+ }
445
+ var LayerGeoJson = ({
446
+ layerId,
447
+ data,
448
+ baseOpacity,
449
+ isMobile,
450
+ panesReady,
451
+ mapInstance,
452
+ fillPaneName,
453
+ pointsPaneName,
454
+ layerType,
455
+ styleFn,
456
+ onEachFeature,
457
+ onPolygonLabel
458
+ }) => {
459
+ const safeData = useMemo(() => sanitizeGeoJson(data, String(layerId)), [data, layerId]);
460
+ const features = useMemo(() => safeData.features ?? [], [safeData]);
461
+ const fillFeatures = useMemo(() => features.filter(isNonPointGeometry), [features]);
462
+ const pointFeatures = useMemo(() => features.filter(isPointGeometry), [features]);
463
+ const dataVersionRef = useRef(0);
464
+ const prevSignatureRef = useRef("");
465
+ const firstId = features.length > 0 ? String(features[0]?.id ?? "") : "";
466
+ const lastId = features.length > 0 ? String(features[features.length - 1]?.id ?? "") : "";
467
+ const bbox = normalizeBboxFromData(safeData);
468
+ const idsSample = buildIdsSample(features);
469
+ const signature = bbox ? `${layerId}|${features.length}|${firstId}|${lastId}|${idsSample}|${bbox[0]}|${bbox[1]}|${bbox[2]}|${bbox[3]}` : `${layerId}|${features.length}|${firstId}|${lastId}|${idsSample}`;
470
+ const signatureToken = signature.replace(/[^a-zA-Z0-9_-]/g, "_");
471
+ if (prevSignatureRef.current !== signature) {
472
+ dataVersionRef.current += 1;
473
+ prevSignatureRef.current = signature;
474
+ }
475
+ const fillData = useMemo(() => fillFeatures.length > 0 ? buildFeatureCollection(fillFeatures) : null, [fillFeatures]);
476
+ const pointsData = useMemo(() => pointFeatures.length > 0 ? buildFeatureCollection(pointFeatures) : null, [pointFeatures]);
477
+ const clusterLayerRef = useRef(null);
478
+ const canCluster = typeof L.markerClusterGroup === "function";
479
+ const resolvedFillPane = useMemo(
480
+ () => ensurePaneName(mapInstance, fillPaneName, "overlayPane"),
481
+ [fillPaneName, mapInstance]
482
+ );
483
+ const resolvedPointsPane = useMemo(
484
+ () => ensurePaneName(mapInstance, pointsPaneName, "markerPane"),
485
+ [mapInstance, pointsPaneName]
486
+ );
487
+ useEffect(() => {
488
+ if (!mapInstance || !panesReady || !pointsData || !canCluster) return;
489
+ const markerClusterGroup = L.markerClusterGroup;
490
+ const clusterPaneName = resolvedPointsPane;
491
+ const clusterLayer = clusterLayerRef.current ?? markerClusterGroup({ pane: clusterPaneName, clusterPane: clusterPaneName });
492
+ clusterLayerRef.current = clusterLayer;
493
+ if (!mapInstance.hasLayer(clusterLayer)) {
494
+ mapInstance.addLayer(clusterLayer);
495
+ }
496
+ clusterLayer.clearLayers();
497
+ const geoJsonLayer = L.geoJSON(pointsData, {
498
+ pointToLayer: (feature, latlng) => {
499
+ if (!isValidLatLng(latlng)) {
500
+ return createInvisibleFallbackClusterMarker();
501
+ }
502
+ const style = styleFn(feature, layerType, baseOpacity);
503
+ return L.marker(latlng, {
504
+ icon: createPointDivIcon(style, isMobile),
505
+ pane: clusterPaneName,
506
+ interactive: true
507
+ });
508
+ },
509
+ onEachFeature
510
+ });
511
+ clusterLayer.addLayer(geoJsonLayer);
512
+ return () => {
513
+ clusterLayer.clearLayers();
514
+ if (mapInstance.hasLayer(clusterLayer)) {
515
+ mapInstance.removeLayer(clusterLayer);
516
+ }
517
+ };
518
+ }, [
519
+ baseOpacity,
520
+ canCluster,
521
+ isMobile,
522
+ layerType,
523
+ mapInstance,
524
+ onEachFeature,
525
+ panesReady,
526
+ pointsData,
527
+ resolvedPointsPane,
528
+ styleFn
529
+ ]);
530
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
531
+ fillData && /* @__PURE__ */ jsx(
532
+ GeoJSON,
533
+ {
534
+ data: fillData,
535
+ pane: resolvedFillPane,
536
+ style: (feature) => styleFn(feature, layerType, baseOpacity),
537
+ onEachFeature: (feature, layer) => {
538
+ onEachFeature(feature, layer);
539
+ onPolygonLabel?.(feature, layer);
540
+ }
541
+ },
542
+ `fill-${layerId}-${signatureToken}-v${dataVersionRef.current}`
543
+ ),
544
+ pointsData && !canCluster && /* @__PURE__ */ jsx(
545
+ GeoJSON,
546
+ {
547
+ data: pointsData,
548
+ pane: resolvedPointsPane,
549
+ pointToLayer: (feature, latlng) => {
550
+ if (!isValidLatLng(latlng)) {
551
+ return createInvisibleFallbackMarker();
552
+ }
553
+ return L.circleMarker(latlng, {
554
+ radius: isMobile ? 8 : 6,
555
+ ...styleFn(feature, layerType, baseOpacity)
556
+ });
557
+ },
558
+ onEachFeature
559
+ },
560
+ `points-${layerId}-${signatureToken}-v${dataVersionRef.current}`
561
+ )
562
+ ] });
563
+ };
564
+
565
+ // src/react/map/location-control.tsx
566
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3 } from "react";
567
+ import { createPortal } from "react-dom";
568
+ import { Circle, Marker, useMap } from "react-leaflet";
569
+ import L3 from "leaflet";
570
+
571
+ // src/react/hooks/use-geolocation.ts
572
+ import { useCallback, useEffect as useEffect2, useRef as useRef2, useState } from "react";
573
+ function useGeolocation(options) {
574
+ const [isTracking, setIsTracking] = useState(false);
575
+ const [location, setLocation] = useState(null);
576
+ const [error, setError] = useState(null);
577
+ const watchIdRef = useRef2(null);
578
+ const stopTracking = useCallback(() => {
579
+ if (watchIdRef.current !== null && typeof navigator !== "undefined" && navigator.geolocation) {
580
+ navigator.geolocation.clearWatch(watchIdRef.current);
581
+ }
582
+ watchIdRef.current = null;
583
+ setIsTracking(false);
584
+ }, []);
585
+ const startTracking = useCallback(() => {
586
+ if (typeof navigator === "undefined" || !navigator.geolocation) {
587
+ setError({ code: 0, message: "La geolocalizaci\xF3n no est\xE1 disponible en este navegador." });
588
+ return;
589
+ }
590
+ setError(null);
591
+ watchIdRef.current = navigator.geolocation.watchPosition(
592
+ (position) => {
593
+ setLocation({
594
+ lat: position.coords.latitude,
595
+ lon: position.coords.longitude,
596
+ accuracy: position.coords.accuracy
597
+ });
598
+ setIsTracking(true);
599
+ },
600
+ (err) => {
601
+ setError({ code: err.code, message: err.message });
602
+ stopTracking();
603
+ },
604
+ {
605
+ enableHighAccuracy: options?.enableHighAccuracy ?? true,
606
+ timeout: options?.timeout ?? 12e3,
607
+ maximumAge: options?.maximumAge ?? 0
608
+ }
609
+ );
610
+ }, [options?.enableHighAccuracy, options?.maximumAge, options?.timeout, stopTracking]);
611
+ const toggleTracking = useCallback(() => {
612
+ if (isTracking) {
613
+ stopTracking();
614
+ } else {
615
+ startTracking();
616
+ }
617
+ }, [isTracking, startTracking, stopTracking]);
618
+ const clearError = useCallback(() => setError(null), []);
619
+ useEffect2(() => {
620
+ return () => {
621
+ stopTracking();
622
+ };
623
+ }, [stopTracking]);
624
+ return {
625
+ isTracking,
626
+ location,
627
+ error,
628
+ startTracking,
629
+ stopTracking,
630
+ toggleTracking,
631
+ clearError
632
+ };
633
+ }
634
+
635
+ // src/react/map/map-utils.ts
636
+ import L2 from "leaflet";
637
+ var POPUP_STYLE_ID = "zenit-leaflet-popup-styles";
638
+ var POPUP_EXCLUDED_KEYS = /* @__PURE__ */ new Set(["geom", "geometry", "_private"]);
639
+ var POPUP_TITLE_KEYS = ["id", "nombre", "name", "title", "titulo", "cluster"];
640
+ var POPUP_BADGE_KEYS = ["tipo", "type", "category", "categoria", "estado", "status"];
641
+ var POPUP_DESCRIPTION_KEYS = ["descripcion", "description", "desc"];
642
+ var CURRENCY_KEYWORDS = [
643
+ "capital",
644
+ "monto",
645
+ "precio",
646
+ "costo",
647
+ "valor",
648
+ "ingreso",
649
+ "egreso",
650
+ "saldo",
651
+ "subtotal",
652
+ "venta",
653
+ "compra"
654
+ ];
655
+ var DESKTOP_POPUP_DIMENSIONS = { maxWidth: 360, minWidth: 280, maxHeight: 520 };
656
+ var MOBILE_POPUP_DIMENSIONS = { maxWidth: 300, minWidth: 240, maxHeight: 420 };
657
+ var ZENIT_LEAFLET_POPUP_STYLES = `
658
+ .custom-leaflet-popup .leaflet-popup-content-wrapper {
659
+ border-radius: 12px;
660
+ padding: 0;
661
+ background: #ffffff;
662
+ box-shadow: 0 12px 24px rgba(15, 23, 42, 0.18);
663
+ border: 1px solid rgba(15, 23, 42, 0.08);
664
+ }
665
+
666
+ .custom-leaflet-popup .leaflet-popup-content {
667
+ margin: 0;
668
+ padding: 12px 14px;
669
+ font-size: 13px;
670
+ line-height: 1.4;
671
+ color: #0f172a;
672
+ max-height: min(70vh, 520px);
673
+ overflow: auto;
674
+ scrollbar-width: thin;
675
+ scrollbar-color: rgba(148, 163, 184, 0.6) transparent;
676
+ }
677
+
678
+ .custom-leaflet-popup .leaflet-popup-close-button {
679
+ color: #64748b;
680
+ font-size: 16px;
681
+ padding: 6px 8px;
682
+ border-radius: 8px;
683
+ transition: background-color 0.15s ease, color 0.15s ease;
684
+ }
685
+
686
+ .custom-leaflet-popup .leaflet-popup-close-button:hover {
687
+ color: #0f172a;
688
+ background: rgba(148, 163, 184, 0.2);
689
+ }
690
+
691
+ .custom-leaflet-popup .leaflet-popup-content::-webkit-scrollbar {
692
+ width: 6px;
693
+ }
694
+
695
+ .custom-leaflet-popup .leaflet-popup-content::-webkit-scrollbar-thumb {
696
+ background: rgba(148, 163, 184, 0.5);
697
+ border-radius: 999px;
698
+ }
699
+
700
+ .custom-leaflet-popup .leaflet-popup-content::-webkit-scrollbar-track {
701
+ background: transparent;
702
+ }
703
+
704
+ @media (max-width: 640px) {
705
+ .custom-leaflet-popup .leaflet-popup-content {
706
+ font-size: 12px;
707
+ padding: 10px 12px;
708
+ max-height: min(65vh, 420px);
709
+ }
710
+
711
+ .custom-leaflet-popup .leaflet-popup-close-button {
712
+ font-size: 14px;
713
+ padding: 4px 6px;
714
+ }
715
+ }
716
+
717
+ .zenit-map-tooltip {
718
+ background-color: rgba(31, 41, 55, 0.95);
719
+ border: none;
720
+ border-radius: 6px;
721
+ color: #ffffff;
722
+ font-size: 12px;
723
+ font-weight: 500;
724
+ padding: 6px 10px;
725
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
726
+ }
727
+
728
+ .zenit-map-tooltip::before {
729
+ border-top-color: rgba(31, 41, 55, 0.95);
730
+ }
731
+
732
+ .polygon-label-tooltip {
733
+ background: rgba(15, 23, 42, 0.92);
734
+ border: none;
735
+ border-radius: 6px;
736
+ color: #f8fafc;
737
+ font-weight: 600;
738
+ font-size: 12px;
739
+ padding: 4px 8px;
740
+ }
741
+ `;
742
+ function clampNumber2(value, min, max) {
743
+ return Math.min(max, Math.max(min, value));
744
+ }
745
+ function clampOpacity4(value) {
746
+ return clampNumber2(value, 0, 1);
747
+ }
748
+ function ensurePopupStyles() {
749
+ if (typeof document === "undefined") return;
750
+ if (document.getElementById(POPUP_STYLE_ID)) return;
751
+ const styleTag = document.createElement("style");
752
+ styleTag.id = POPUP_STYLE_ID;
753
+ styleTag.textContent = ZENIT_LEAFLET_POPUP_STYLES;
754
+ document.head.appendChild(styleTag);
755
+ }
756
+ function getPopupDimensions() {
757
+ if (typeof window === "undefined") {
758
+ return DESKTOP_POPUP_DIMENSIONS;
759
+ }
760
+ const isSmallWidth = window.innerWidth < 640;
761
+ const isShortHeight = window.innerHeight < 768;
762
+ const base = isSmallWidth ? MOBILE_POPUP_DIMENSIONS : DESKTOP_POPUP_DIMENSIONS;
763
+ const maxHeight = isShortHeight ? Math.min(base.maxHeight, 360) : base.maxHeight;
764
+ return { ...base, maxHeight };
765
+ }
766
+ function escapeHtml(value) {
767
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
768
+ }
769
+ function formatLabel(key) {
770
+ return key.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim();
771
+ }
772
+ function isHexColor(value) {
773
+ return /^#([0-9A-F]{3}){1,2}$/i.test(value.trim());
774
+ }
775
+ function isCurrencyField(key) {
776
+ const normalized = key.trim().toLowerCase();
777
+ return CURRENCY_KEYWORDS.some((keyword) => normalized.includes(keyword));
778
+ }
779
+ function formatCurrency(value) {
780
+ return new Intl.NumberFormat("es-GT", { style: "currency", currency: "GTQ" }).format(value);
781
+ }
782
+ function getBadgeForProperty(key, value) {
783
+ if (value === null || value === void 0) return null;
784
+ const normalizedKey = key.trim().toLowerCase();
785
+ const normalizedValue = String(value).trim().toLowerCase();
786
+ const byType = {
787
+ mora: { label: "Mora", bg: "#fee2e2", color: "#b91c1c", border: "#fecaca" },
788
+ castigo: { label: "Castigo", bg: "#ffedd5", color: "#c2410c", border: "#fed7aa" },
789
+ colocacion: { label: "Colocaci\xF3n", bg: "#dbeafe", color: "#1d4ed8", border: "#bfdbfe" },
790
+ sano: { label: "Sano", bg: "#dcfce7", color: "#15803d", border: "#bbf7d0" }
791
+ };
792
+ const byStatus = {
793
+ activo: { label: "Activo", bg: "#dcfce7", color: "#15803d", border: "#bbf7d0" },
794
+ inactivo: { label: "Inactivo", bg: "#fef9c3", color: "#a16207", border: "#fde68a" },
795
+ pendiente: { label: "Pendiente", bg: "#fef9c3", color: "#a16207", border: "#fde68a" },
796
+ cancelado: { label: "Cancelado", bg: "#fee2e2", color: "#b91c1c", border: "#fecaca" },
797
+ rechazado: { label: "Rechazado", bg: "#fee2e2", color: "#b91c1c", border: "#fecaca" }
798
+ };
799
+ if (["tipo", "type", "category", "categoria"].includes(normalizedKey)) {
800
+ return byType[normalizedValue] ?? {
801
+ label: String(value),
802
+ bg: "#e0e7ff",
803
+ color: "#4338ca",
804
+ border: "#c7d2fe"
805
+ };
806
+ }
807
+ if (["estado", "status"].includes(normalizedKey)) {
808
+ return byStatus[normalizedValue] ?? {
809
+ label: String(value),
810
+ bg: "#f1f5f9",
811
+ color: "#475569",
812
+ border: "#cbd5e1"
813
+ };
814
+ }
815
+ return null;
816
+ }
817
+ function findHeaderProperties(properties) {
818
+ const entries = Object.entries(properties);
819
+ const titleEntry = entries.find(
820
+ ([key, value]) => POPUP_TITLE_KEYS.includes(key.trim().toLowerCase()) && String(value ?? "").trim().length > 0
821
+ );
822
+ const badgeEntry = entries.find(
823
+ ([key, value]) => POPUP_BADGE_KEYS.includes(key.trim().toLowerCase()) && String(value ?? "").trim().length > 0
824
+ );
825
+ const descriptionEntry = entries.find(
826
+ ([key, value]) => POPUP_DESCRIPTION_KEYS.includes(key.trim().toLowerCase()) && String(value ?? "").trim().length > 0
827
+ );
828
+ return {
829
+ title: titleEntry ? { key: titleEntry[0], value: String(titleEntry[1]).trim() } : void 0,
830
+ badge: badgeEntry ? { key: badgeEntry[0], value: badgeEntry[1] } : void 0,
831
+ description: descriptionEntry ? { key: descriptionEntry[0], value: String(descriptionEntry[1]).trim() } : void 0
832
+ };
833
+ }
834
+ function renderPopupValue(value) {
835
+ if (value === null || value === void 0) return '<span style="color:#94a3b8;">Sin datos</span>';
836
+ if (value instanceof Date) {
837
+ return escapeHtml(value.toLocaleDateString("es-GT"));
838
+ }
839
+ if (typeof value === "number") {
840
+ return escapeHtml(value.toLocaleString("es-GT"));
841
+ }
842
+ if (typeof value === "string") {
843
+ const trimmed = value.trim();
844
+ if (!trimmed) return '<span style="color:#94a3b8;">Sin datos</span>';
845
+ return escapeHtml(trimmed);
846
+ }
847
+ if (typeof value === "object") {
848
+ try {
849
+ return `<pre style="margin:0; white-space:pre-wrap; font-size:11px; background:#f8fafc; border:1px solid #e2e8f0; padding:6px 8px; border-radius:6px;">${escapeHtml(
850
+ JSON.stringify(value, null, 2)
851
+ )}</pre>`;
852
+ } catch {
853
+ return escapeHtml(String(value));
854
+ }
855
+ }
856
+ return escapeHtml(String(value));
857
+ }
858
+ function extractPopupHeader(properties) {
859
+ const header = findHeaderProperties(properties);
860
+ return header.title?.value ?? null;
861
+ }
862
+ function shouldIncludePopupEntry(key, value) {
863
+ if (!key) return false;
864
+ const normalized = key.trim().toLowerCase();
865
+ if (!normalized) return false;
866
+ if (normalized.startsWith("_")) return false;
867
+ if (POPUP_EXCLUDED_KEYS.has(normalized)) return false;
868
+ if (value === null || value === void 0) return false;
869
+ if (typeof value === "string" && !value.trim()) return false;
870
+ return true;
871
+ }
872
+ function createPopupContent(properties) {
873
+ const header = findHeaderProperties(properties);
874
+ const headerText = header.title?.value ?? extractPopupHeader(properties);
875
+ const usedKeys = new Set([header.title?.key, header.badge?.key, header.description?.key].filter(Boolean));
876
+ const badge = header.badge ? getBadgeForProperty(header.badge.key, header.badge.value) : null;
877
+ const colorValue = properties.color;
878
+ const colorBar = typeof colorValue === "string" && isHexColor(colorValue) ? `<div style="height:6px; border-radius:8px; margin:-2px -2px 10px; background:linear-gradient(90deg, ${colorValue}, rgba(255,255,255,0.95));"></div>` : "";
879
+ const entries = Object.entries(properties).filter(([key, value]) => {
880
+ if (!shouldIncludePopupEntry(key, value)) return false;
881
+ if (usedKeys.has(key)) return false;
882
+ return true;
883
+ });
884
+ if (entries.length === 0) {
885
+ return '<div style="padding:8px 0; color:#64748b; text-align:center;">Sin datos disponibles</div>';
886
+ }
887
+ const headerHtml = headerText || badge ? `<div style="display:flex; justify-content:space-between; gap:8px; align-items:flex-start; margin-bottom:8px;">
888
+ <div style="font-weight:700; font-size:14px; color:#0f172a;">${escapeHtml(headerText ?? "Detalle")}</div>
889
+ ${badge ? `<span style="display:inline-flex; padding:2px 8px; border-radius:999px; font-size:11px; font-weight:700; background:${badge.bg}; color:${badge.color}; border:1px solid ${badge.border}; white-space:nowrap;">${escapeHtml(
890
+ badge.label
891
+ )}</span>` : ""}
892
+ </div>` : "";
893
+ const descriptionHtml = header.description ? `<div style="margin-bottom:10px; padding:8px 10px; background:#f8fafc; border-left:3px solid #38bdf8; border-radius:6px; color:#334155; font-size:12px;">${escapeHtml(
894
+ header.description.value
895
+ )}</div>` : "";
896
+ const rowsHtml = entries.map(([key, value]) => {
897
+ const label = escapeHtml(formatLabel(key));
898
+ const normalizedKey = key.trim().toLowerCase();
899
+ let valueHtml = renderPopupValue(value);
900
+ if (typeof value === "number") {
901
+ valueHtml = isCurrencyField(normalizedKey) ? `<span style="color:#15803d; font-weight:700;">${escapeHtml(formatCurrency(value))}</span>` : `<span style="color:#0369a1; font-weight:600;">${escapeHtml(value.toLocaleString("es-GT"))}</span>`;
902
+ }
903
+ return `
904
+ <div style="display:grid; grid-template-columns:minmax(90px, 35%) 1fr; gap:8px; padding:6px 0; border-bottom:1px solid #e2e8f0;">
905
+ <div style="font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:0.04em; color:#64748b;">${label}</div>
906
+ <div style="font-size:13px; color:#0f172a; word-break:break-word;">${valueHtml}</div>
907
+ </div>
908
+ `;
909
+ }).join("");
910
+ return `<div>${colorBar}${headerHtml}${descriptionHtml}${rowsHtml}</div>`;
911
+ }
912
+ function isPolygonType(layerType, geometryType) {
913
+ const candidate = (layerType ?? geometryType ?? "").toLowerCase();
914
+ return candidate === "polygon" || candidate === "multipolygon";
915
+ }
916
+ function calculateZoomBasedOpacity(zoom, baseOpacity, layerType, geometryType) {
917
+ if (!isPolygonType(layerType, geometryType)) return clampOpacity4(baseOpacity);
918
+ const minZoom = 11;
919
+ const maxZoom = 15;
920
+ const minFactor = 0.5;
921
+ if (maxZoom <= minZoom) return clampOpacity4(baseOpacity * minFactor);
922
+ const t = clampNumber2((zoom - minZoom) / (maxZoom - minZoom), 0, 1);
923
+ const factor = 1 - (1 - minFactor) * t;
924
+ return clampOpacity4(baseOpacity * factor);
925
+ }
926
+ function layerStyleToLeaflet(options) {
927
+ const { baseOpacity, zoom, layerStyle, geometryType, layerType, properties } = options;
928
+ const sanitizedOpacity = clampOpacity4(baseOpacity);
929
+ const zoomOpacity = calculateZoomBasedOpacity(zoom, sanitizedOpacity, layerType, geometryType);
930
+ const badgeForType = getBadgeForProperty("type", properties?.type ?? properties?.tipo ?? properties?.category ?? properties?.categoria);
931
+ const badgeForStatus = getBadgeForProperty("status", properties?.status ?? properties?.estado);
932
+ const featureColor = (typeof properties?.color === "string" && properties.color.trim().length > 0 ? properties.color : void 0) ?? badgeForType?.color ?? badgeForStatus?.color;
933
+ const styleFillOpacity = typeof layerStyle?.fillOpacity === "number" ? clampOpacity4(layerStyle.fillOpacity) : 0.65;
934
+ const fallbackColor = "#2563eb";
935
+ const resolvedColor = featureColor ?? layerStyle?.color ?? layerStyle?.fillColor ?? fallbackColor;
936
+ const resolvedFillColor = featureColor ?? layerStyle?.fillColor ?? layerStyle?.color ?? fallbackColor;
937
+ return {
938
+ color: resolvedColor,
939
+ weight: layerStyle?.weight ?? 2,
940
+ fillColor: resolvedFillColor,
941
+ opacity: clampOpacity4(Math.max(0.35, zoomOpacity * 0.9)),
942
+ fillOpacity: clampOpacity4(zoomOpacity * styleFillOpacity)
943
+ };
944
+ }
945
+ function getRgbFromColor(color) {
946
+ const trimmed = color.trim();
947
+ if (trimmed.startsWith("#")) {
948
+ const hex = trimmed.replace("#", "");
949
+ const expanded = hex.length === 3 ? hex.split("").map((char) => `${char}${char}`).join("") : hex;
950
+ if (expanded.length === 6) {
951
+ const r = parseInt(expanded.slice(0, 2), 16);
952
+ const g = parseInt(expanded.slice(2, 4), 16);
953
+ const b = parseInt(expanded.slice(4, 6), 16);
954
+ return { r, g, b };
955
+ }
956
+ }
957
+ const rgbMatch = trimmed.match(/rgba?\(([^)]+)\)/i);
958
+ if (rgbMatch) {
959
+ const [r, g, b] = rgbMatch[1].split(",").map((value) => parseFloat(value.trim())).slice(0, 3);
960
+ if ([r, g, b].every((value) => Number.isFinite(value))) {
961
+ return { r, g, b };
962
+ }
963
+ }
964
+ return null;
965
+ }
966
+ function getLabelTextStyles(color) {
967
+ const rgb = getRgbFromColor(color);
968
+ if (!rgb) {
969
+ return { color: "#0f172a", shadow: "0 1px 2px rgba(255, 255, 255, 0.6)" };
970
+ }
971
+ const luminance = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255;
972
+ if (luminance > 0.6) {
973
+ return { color: "#0f172a", shadow: "0 1px 2px rgba(255, 255, 255, 0.7)" };
974
+ }
975
+ return { color: "#ffffff", shadow: "0 1px 2px rgba(0, 0, 0, 0.4)" };
976
+ }
977
+ function withAlpha(color, alpha) {
978
+ const trimmed = color.trim();
979
+ if (trimmed.startsWith("#")) {
980
+ const hex = trimmed.replace("#", "");
981
+ const expanded = hex.length === 3 ? hex.split("").map((char) => `${char}${char}`).join("") : hex;
982
+ if (expanded.length === 6) {
983
+ const r = parseInt(expanded.slice(0, 2), 16);
984
+ const g = parseInt(expanded.slice(2, 4), 16);
985
+ const b = parseInt(expanded.slice(4, 6), 16);
986
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
987
+ }
988
+ }
989
+ if (trimmed.startsWith("rgb(")) {
990
+ const inner = trimmed.slice(4, -1);
991
+ return `rgba(${inner}, ${alpha})`;
992
+ }
993
+ if (trimmed.startsWith("rgba(")) {
994
+ const inner = trimmed.slice(5, -1).split(",").slice(0, 3).map((value) => value.trim());
995
+ return `rgba(${inner.join(", ")}, ${alpha})`;
996
+ }
997
+ return color;
998
+ }
999
+ function createCustomIcon(label, opacity, color) {
1000
+ const size = 60;
1001
+ const innerSize = 44;
1002
+ const textStyles = getLabelTextStyles(color);
1003
+ const safeLabel = escapeHtml(label);
1004
+ const clampedOpacity = Math.min(1, Math.max(0.92, opacity));
1005
+ const innerBackground = withAlpha(color, 0.9);
1006
+ return L2.divIcon({
1007
+ className: "zenit-label-marker",
1008
+ iconSize: [size, size],
1009
+ iconAnchor: [size / 2, size / 2],
1010
+ html: `
1011
+ <div
1012
+ title="${safeLabel}"
1013
+ style="
1014
+ width:${size}px;
1015
+ height:${size}px;
1016
+ border-radius:9999px;
1017
+ background:rgba(255, 255, 255, 0.95);
1018
+ border:3px solid rgba(255, 255, 255, 1);
1019
+ display:flex;
1020
+ align-items:center;
1021
+ justify-content:center;
1022
+ opacity:${clampedOpacity};
1023
+ box-shadow:0 2px 6px rgba(0, 0, 0, 0.25);
1024
+ pointer-events:none;
1025
+ "
1026
+ >
1027
+ <div
1028
+ style="
1029
+ width:${innerSize}px;
1030
+ height:${innerSize}px;
1031
+ border-radius:9999px;
1032
+ background:${innerBackground};
1033
+ display:flex;
1034
+ align-items:center;
1035
+ justify-content:center;
1036
+ box-shadow:inset 0 0 0 1px rgba(15, 23, 42, 0.12);
1037
+ "
1038
+ >
1039
+ <span
1040
+ style="
1041
+ color:${textStyles.color};
1042
+ font-size:20px;
1043
+ font-weight:800;
1044
+ text-shadow:${textStyles.shadow};
1045
+ "
1046
+ >
1047
+ ${safeLabel}
1048
+ </span>
1049
+ </div>
1050
+ </div>
1051
+ `
1052
+ });
1053
+ }
1054
+ function createLocationIcon() {
1055
+ return L2.divIcon({
1056
+ className: "zenit-location-marker",
1057
+ iconSize: [18, 18],
1058
+ iconAnchor: [9, 9],
1059
+ html: `
1060
+ <div style="width:18px;height:18px;border-radius:9999px;background:#2563eb;border:2px solid #fff;box-shadow:0 0 0 4px rgba(37, 99, 235, 0.25);"></div>
1061
+ `
1062
+ });
1063
+ }
1064
+
1065
+ // src/react/map/location-control.tsx
1066
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1067
+ var LOCATION_STYLE_ID = "zenit-location-control-styles";
1068
+ var LOCATION_STYLES = `
1069
+ .zenit-location-control {
1070
+ display: flex;
1071
+ flex-direction: column;
1072
+ gap: 8px;
1073
+ }
1074
+
1075
+ .zenit-location-button {
1076
+ width: 42px;
1077
+ height: 42px;
1078
+ border-radius: 12px;
1079
+ border: none;
1080
+ background: #ffffff;
1081
+ color: #0f172a;
1082
+ box-shadow: 0 8px 18px rgba(15, 23, 42, 0.2);
1083
+ display: inline-flex;
1084
+ align-items: center;
1085
+ justify-content: center;
1086
+ cursor: pointer;
1087
+ position: relative;
1088
+ }
1089
+
1090
+ .zenit-location-button.zenit-location-button--tracking {
1091
+ animation: zenitLocationPulse 1.8s infinite;
1092
+ }
1093
+
1094
+ .zenit-location-button:hover {
1095
+ background: #f8fafc;
1096
+ }
1097
+
1098
+ .zenit-location-badge {
1099
+ position: absolute;
1100
+ top: -6px;
1101
+ right: -6px;
1102
+ width: 18px;
1103
+ height: 18px;
1104
+ border-radius: 999px;
1105
+ background: #ef4444;
1106
+ color: #fff;
1107
+ font-size: 11px;
1108
+ font-weight: 700;
1109
+ display: inline-flex;
1110
+ align-items: center;
1111
+ justify-content: center;
1112
+ box-shadow: 0 4px 10px rgba(239, 68, 68, 0.35);
1113
+ }
1114
+
1115
+ .zenit-location-error {
1116
+ background: rgba(15, 23, 42, 0.92);
1117
+ color: #f8fafc;
1118
+ border-radius: 10px;
1119
+ padding: 8px 10px;
1120
+ font-size: 12px;
1121
+ max-width: 200px;
1122
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.3);
1123
+ }
1124
+
1125
+ @keyframes zenitLocationPulse {
1126
+ 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.35); }
1127
+ 70% { box-shadow: 0 0 0 12px rgba(16, 185, 129, 0); }
1128
+ 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
1129
+ }
1130
+ `;
1131
+ var LocateIcon = () => /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1132
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "3" }),
1133
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "2", x2: "12", y2: "5" }),
1134
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "19", x2: "12", y2: "22" }),
1135
+ /* @__PURE__ */ jsx2("line", { x1: "2", y1: "12", x2: "5", y2: "12" }),
1136
+ /* @__PURE__ */ jsx2("line", { x1: "19", y1: "12", x2: "22", y2: "12" })
1137
+ ] });
1138
+ var LocationControl = ({ position = "bottomleft", zoom = 16 }) => {
1139
+ const map = useMap();
1140
+ const controlRef = useRef3(null);
1141
+ const hasCenteredRef = useRef3(false);
1142
+ const { isTracking, location, error, toggleTracking, clearError } = useGeolocation();
1143
+ useEffect3(() => {
1144
+ if (typeof document === "undefined") return;
1145
+ if (document.getElementById(LOCATION_STYLE_ID)) return;
1146
+ const styleTag = document.createElement("style");
1147
+ styleTag.id = LOCATION_STYLE_ID;
1148
+ styleTag.textContent = LOCATION_STYLES;
1149
+ document.head.appendChild(styleTag);
1150
+ }, []);
1151
+ useEffect3(() => {
1152
+ const control = L3.control({ position });
1153
+ control.onAdd = () => {
1154
+ const container = L3.DomUtil.create("div", "zenit-location-control");
1155
+ L3.DomEvent.disableClickPropagation(container);
1156
+ controlRef.current = container;
1157
+ return container;
1158
+ };
1159
+ control.addTo(map);
1160
+ return () => {
1161
+ control.remove();
1162
+ controlRef.current = null;
1163
+ };
1164
+ }, [map, position]);
1165
+ useEffect3(() => {
1166
+ if (!location || !isTracking) return;
1167
+ if (hasCenteredRef.current) return;
1168
+ hasCenteredRef.current = true;
1169
+ map.flyTo([location.lat, location.lon], zoom, { animate: true });
1170
+ }, [isTracking, location, map, zoom]);
1171
+ const markerIcon = useMemo2(() => createLocationIcon(), []);
1172
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
1173
+ controlRef.current && createPortal(
1174
+ /* @__PURE__ */ jsxs2("div", { children: [
1175
+ /* @__PURE__ */ jsxs2(
1176
+ "button",
1177
+ {
1178
+ type: "button",
1179
+ className: `zenit-location-button${isTracking ? " zenit-location-button--tracking" : ""}`,
1180
+ onClick: () => {
1181
+ if (error) {
1182
+ clearError();
1183
+ }
1184
+ toggleTracking();
1185
+ },
1186
+ "aria-label": isTracking ? "Detener ubicaci\xF3n" : "Mostrar mi ubicaci\xF3n",
1187
+ children: [
1188
+ /* @__PURE__ */ jsx2(LocateIcon, {}),
1189
+ error && /* @__PURE__ */ jsx2("span", { className: "zenit-location-badge", children: "!" })
1190
+ ]
1191
+ }
1192
+ ),
1193
+ error && /* @__PURE__ */ jsx2("div", { className: "zenit-location-error", children: error.message || "No se pudo acceder a tu ubicaci\xF3n." })
1194
+ ] }),
1195
+ controlRef.current
1196
+ ),
1197
+ location && /* @__PURE__ */ jsxs2(Fragment2, { children: [
1198
+ /* @__PURE__ */ jsx2(Marker, { position: [location.lat, location.lon], icon: markerIcon }),
1199
+ /* @__PURE__ */ jsx2(
1200
+ Circle,
1201
+ {
1202
+ center: [location.lat, location.lon],
1203
+ radius: location.accuracy,
1204
+ pathOptions: { color: "#2563eb", fillColor: "#2563eb", fillOpacity: 0.15 }
1205
+ }
1206
+ )
1207
+ ] })
1208
+ ] });
1209
+ };
1210
+
1211
+ // src/react/map/map-handlers.tsx
1212
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
1213
+ import { useMap as useMap2 } from "react-leaflet";
1214
+ var MapInvalidator = ({ trigger }) => {
1215
+ const map = useMap2();
1216
+ const lastTrigger = useRef4(void 0);
1217
+ useEffect4(() => {
1218
+ if (lastTrigger.current === trigger) return;
1219
+ lastTrigger.current = trigger;
1220
+ requestAnimationFrame(() => {
1221
+ map.invalidateSize();
1222
+ });
1223
+ }, [map, trigger]);
1224
+ return null;
1225
+ };
1226
+ function computeBBoxFromGeojson(geojson) {
1227
+ if (!geojson || !Array.isArray(geojson.features)) return null;
1228
+ const coords = [];
1229
+ const collect = (candidate) => {
1230
+ if (!Array.isArray(candidate)) return;
1231
+ if (candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number" && Number.isFinite(candidate[0]) && Number.isFinite(candidate[1])) {
1232
+ coords.push([candidate[0], candidate[1]]);
1233
+ return;
1234
+ }
1235
+ candidate.forEach((item) => collect(item));
1236
+ };
1237
+ geojson.features.forEach((feature) => {
1238
+ collect(feature.geometry?.coordinates);
1239
+ });
1240
+ if (coords.length === 0) return null;
1241
+ const [firstLon, firstLat] = coords[0];
1242
+ const bbox = { minLon: firstLon, minLat: firstLat, maxLon: firstLon, maxLat: firstLat };
1243
+ coords.forEach(([lon, lat]) => {
1244
+ bbox.minLon = Math.min(bbox.minLon, lon);
1245
+ bbox.minLat = Math.min(bbox.minLat, lat);
1246
+ bbox.maxLon = Math.max(bbox.maxLon, lon);
1247
+ bbox.maxLat = Math.max(bbox.maxLat, lat);
1248
+ });
1249
+ return bbox;
1250
+ }
1251
+ function mergeBBoxes(bboxes) {
1252
+ const valid = bboxes.filter((bbox) => !!bbox);
1253
+ if (valid.length === 0) return null;
1254
+ const first = valid[0];
1255
+ return valid.slice(1).reduce(
1256
+ (acc, bbox) => ({
1257
+ minLon: Math.min(acc.minLon, bbox.minLon),
1258
+ minLat: Math.min(acc.minLat, bbox.minLat),
1259
+ maxLon: Math.max(acc.maxLon, bbox.maxLon),
1260
+ maxLat: Math.max(acc.maxLat, bbox.maxLat)
1261
+ }),
1262
+ { ...first }
1263
+ );
1264
+ }
1265
+ var BBoxZoomHandler = ({
1266
+ bbox,
1267
+ geojson,
1268
+ autoGeojson = [],
1269
+ enabled = true
1270
+ }) => {
1271
+ const map = useMap2();
1272
+ const lastAppliedBBox = useRef4(null);
1273
+ const lastUserInteracted = useRef4(false);
1274
+ useEffect4(() => {
1275
+ const handleInteraction = () => {
1276
+ lastUserInteracted.current = true;
1277
+ };
1278
+ map.on("dragstart", handleInteraction);
1279
+ map.on("zoomstart", handleInteraction);
1280
+ return () => {
1281
+ map.off("dragstart", handleInteraction);
1282
+ map.off("zoomstart", handleInteraction);
1283
+ };
1284
+ }, [map]);
1285
+ useEffect4(() => {
1286
+ if (!enabled) return;
1287
+ let resolvedBBox = bbox ?? null;
1288
+ if (!resolvedBBox && geojson) {
1289
+ resolvedBBox = computeBBoxFromGeojson(geojson);
1290
+ }
1291
+ if (!resolvedBBox && autoGeojson.length > 0) {
1292
+ const bboxes = autoGeojson.map((collection) => computeBBoxFromGeojson(collection));
1293
+ resolvedBBox = mergeBBoxes(bboxes);
1294
+ }
1295
+ if (!resolvedBBox) return;
1296
+ const serialized = JSON.stringify(resolvedBBox);
1297
+ if (lastAppliedBBox.current === serialized) return;
1298
+ if (lastUserInteracted.current && !bbox && !geojson) {
1299
+ lastUserInteracted.current = false;
1300
+ return;
1301
+ }
1302
+ const bounds = [
1303
+ [resolvedBBox.minLat, resolvedBBox.minLon],
1304
+ [resolvedBBox.maxLat, resolvedBBox.maxLon]
1305
+ ];
1306
+ map.fitBounds(bounds, { padding: [12, 12] });
1307
+ lastAppliedBBox.current = serialized;
1308
+ }, [autoGeojson, bbox, enabled, geojson, map]);
1309
+ return null;
1310
+ };
1311
+ var ZoomBasedOpacityHandler = ({ onZoomChange }) => {
1312
+ const map = useMap2();
1313
+ useEffect4(() => {
1314
+ const handleZoom = () => {
1315
+ onZoomChange(map.getZoom());
1316
+ };
1317
+ map.on("zoomend", handleZoom);
1318
+ handleZoom();
1319
+ return () => {
1320
+ map.off("zoomend", handleZoom);
1321
+ };
1322
+ }, [map, onZoomChange]);
1323
+ return null;
1324
+ };
1325
+ var MapInstanceBridge = ({ onReady }) => {
1326
+ const map = useMap2();
1327
+ useEffect4(() => {
1328
+ onReady(map);
1329
+ }, [map, onReady]);
1330
+ return null;
1331
+ };
1332
+
1333
+ // src/react/ZenitMap.tsx
1334
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1335
+ var DEFAULT_CENTER = [0, 0];
1336
+ var DEFAULT_ZOOM = 3;
1337
+ var LABELS_PANE_NAME = "zenit-labels-pane";
1338
+ var DEV_MODE2 = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
1339
+ function isRecord(value) {
1340
+ return typeof value === "object" && value !== null;
1341
+ }
1342
+ function isGeoJsonFeatureCollection(value) {
1343
+ if (!isRecord(value)) return false;
1344
+ const features = value.features;
1345
+ if (!Array.isArray(features)) return false;
1346
+ const type = value.type;
1347
+ return type === void 0 || type === "FeatureCollection";
1348
+ }
1349
+ function extractGeoJsonFeatureCollection(value) {
1350
+ if (isRecord(value) && "data" in value) {
1351
+ const data = value.data;
1352
+ return isGeoJsonFeatureCollection(data) ? data : null;
1353
+ }
1354
+ return isGeoJsonFeatureCollection(value) ? value : null;
1355
+ }
1356
+ function getFeatureLayerId(feature) {
1357
+ const layerId = feature?.properties?.__zenit_layerId ?? feature?.properties?.layerId ?? feature?.properties?.layer_id;
1358
+ if (layerId === void 0 || layerId === null) return null;
1359
+ return layerId;
1360
+ }
1361
+ var DESCRIPTION_KEYS = /* @__PURE__ */ new Set(["descripcion", "description"]);
1362
+ var POINT_GEOMETRY_TYPES2 = /* @__PURE__ */ new Set(["Point", "MultiPoint"]);
1363
+ var POLYGON_GEOMETRY_TYPES = /* @__PURE__ */ new Set(["Polygon", "MultiPolygon"]);
1364
+ function normalizeDescriptionValue(value) {
1365
+ if (value === void 0 || value === null) return null;
1366
+ if (typeof value === "string") {
1367
+ const trimmed = value.trim();
1368
+ return trimmed ? trimmed : null;
1369
+ }
1370
+ if (typeof value === "number" || typeof value === "boolean") {
1371
+ return String(value);
1372
+ }
1373
+ return null;
1374
+ }
1375
+ function extractDescriptionValue(properties) {
1376
+ if (!properties) return null;
1377
+ const matches = Object.entries(properties).find(
1378
+ ([key]) => DESCRIPTION_KEYS.has(key.toLowerCase())
1379
+ );
1380
+ if (!matches) return null;
1381
+ return normalizeDescriptionValue(matches[1]);
1382
+ }
1383
+ function normalizeLayerId(value) {
1384
+ if (value === null || value === void 0) return null;
1385
+ const normalized = String(value).trim();
1386
+ return normalized ? normalized : null;
1387
+ }
1388
+ function isLayerIdMatch(a, b) {
1389
+ const normalizedA = normalizeLayerId(a);
1390
+ const normalizedB = normalizeLayerId(b);
1391
+ return normalizedA !== null && normalizedB !== null && normalizedA === normalizedB;
1392
+ }
1393
+ function getClickIntent(params) {
1394
+ const geometryType = params.feature?.geometry?.type;
1395
+ if (geometryType && POINT_GEOMETRY_TYPES2.has(geometryType)) return "point";
1396
+ if (params.leafletLayer instanceof L4.Marker) return "point";
1397
+ if (geometryType && POLYGON_GEOMETRY_TYPES.has(geometryType)) return "polygon";
1398
+ if (params.leafletLayer instanceof L4.Path) return "polygon";
1399
+ return "unknown";
1400
+ }
1401
+ function candidateLayerId(candidate) {
1402
+ return getFeatureLayerId(candidate);
1403
+ }
1404
+ function isCandidateGeometryType(candidate, allowedTypes) {
1405
+ const geometryType = candidate?.geometry?.type;
1406
+ return Boolean(geometryType && allowedTypes.has(geometryType));
1407
+ }
1408
+ function getFeatureStyleOverrides(feature) {
1409
+ const candidate = feature?.properties?._style;
1410
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return null;
1411
+ const styleCandidate = candidate;
1412
+ return {
1413
+ color: styleCandidate.color,
1414
+ weight: styleCandidate.weight,
1415
+ fillColor: styleCandidate.fillColor,
1416
+ fillOpacity: styleCandidate.fillOpacity
1417
+ };
1418
+ }
1419
+ function buildFeaturePopupHtml(feature) {
1420
+ const properties = feature?.properties;
1421
+ if (!properties) return null;
1422
+ const rendered = createPopupContent(properties);
1423
+ return rendered ? rendered : null;
1424
+ }
1425
+ function pickIntersectFeature(params) {
1426
+ const { baseFeature, candidates, expectedLayerId, clickIntent } = params;
1427
+ if (!Array.isArray(candidates) || candidates.length === 0) return null;
1428
+ const pickByGeometry = (pool, intent) => {
1429
+ const preferredTypes = intent === "point" ? POINT_GEOMETRY_TYPES2 : intent === "polygon" ? POLYGON_GEOMETRY_TYPES : null;
1430
+ if (!preferredTypes) return null;
1431
+ const found = pool.find((candidate) => isCandidateGeometryType(candidate, preferredTypes));
1432
+ if (!found) return null;
1433
+ return { feature: found, reason: `geometry:${intent}` };
1434
+ };
1435
+ const getResult = (feature, reason) => ({
1436
+ feature,
1437
+ selectedIdx: candidates.findIndex((candidate) => candidate === feature),
1438
+ reason
1439
+ });
1440
+ const sameLayer = candidates.filter(
1441
+ (candidate) => isLayerIdMatch(candidateLayerId(candidate), expectedLayerId)
1442
+ );
1443
+ const geometryFromSameLayer = pickByGeometry(sameLayer, clickIntent);
1444
+ if (geometryFromSameLayer?.feature) {
1445
+ return getResult(geometryFromSameLayer.feature, `sameLayer>${geometryFromSameLayer.reason}`);
1446
+ }
1447
+ if (sameLayer.length > 0) {
1448
+ const sameLayerWithDescription = sameLayer.find(
1449
+ (candidate) => extractDescriptionValue(candidate?.properties)
1450
+ );
1451
+ if (sameLayerWithDescription) {
1452
+ return getResult(sameLayerWithDescription, "sameLayer>description");
1453
+ }
1454
+ }
1455
+ const geometryFallback = pickByGeometry(candidates, clickIntent);
1456
+ if (geometryFallback?.feature) {
1457
+ return getResult(geometryFallback.feature, `anyLayer>${geometryFallback.reason}`);
1458
+ }
1459
+ const baseId = baseFeature?.id;
1460
+ if (baseId !== void 0 && baseId !== null) {
1461
+ const matchById = candidates.find((candidate) => candidate?.id === baseId);
1462
+ if (matchById) return getResult(matchById, "fallback>baseId");
1463
+ }
1464
+ const matchWithDescription = candidates.find(
1465
+ (candidate) => extractDescriptionValue(candidate?.properties)
1466
+ );
1467
+ if (matchWithDescription) return getResult(matchWithDescription, "fallback>description");
1468
+ return getResult(candidates[0], "fallback>first");
1469
+ }
1470
+ function normalizeCenterTuple(center) {
1471
+ if (!center) return null;
1472
+ if (Array.isArray(center) && center.length >= 2) {
1473
+ const lat = center[0];
1474
+ const lon = center[1];
1475
+ if (typeof lat === "number" && typeof lon === "number") return [lat, lon];
1476
+ return null;
1477
+ }
1478
+ const maybeObj = center;
1479
+ if (typeof maybeObj.lat === "number" && typeof maybeObj.lon === "number") {
1480
+ return [maybeObj.lat, maybeObj.lon];
1481
+ }
1482
+ return null;
1483
+ }
1484
+ var ZenitMap = forwardRef(({
1485
+ client,
1486
+ mapId,
1487
+ height = "500px",
1488
+ width = "100%",
1489
+ initialZoom,
1490
+ initialCenter,
1491
+ showLayerPanel = true,
1492
+ overlayGeojson,
1493
+ overlayStyle,
1494
+ layerControls,
1495
+ layerStates,
1496
+ layerGeojson,
1497
+ onLayerStateChange,
1498
+ onError,
1499
+ onLoadingChange,
1500
+ onFeatureClick,
1501
+ onFeatureHover,
1502
+ featureInfoMode = "popup",
1503
+ mapLayers,
1504
+ zoomToBbox,
1505
+ zoomToGeojson,
1506
+ onZoomChange,
1507
+ onMapReady
1508
+ }, ref) => {
1509
+ const [map, setMap] = useState2(null);
1510
+ const [layers, setLayers] = useState2([]);
1511
+ const [effectiveStates, setEffectiveStates] = useState2([]);
1512
+ const [loadingMap, setLoadingMap] = useState2(false);
1513
+ const [mapError, setMapError] = useState2(null);
1514
+ const [mapInstance, setMapInstance] = useState2(null);
1515
+ const [layerGeojsonOverrides, setLayerGeojsonOverrides] = useState2({});
1516
+ const originalGeojsonByLayerIdRef = useRef5({});
1517
+ const [panesReady, setPanesReady] = useState2(false);
1518
+ const [currentZoom, setCurrentZoom] = useState2(initialZoom ?? DEFAULT_ZOOM);
1519
+ const [isPopupOpen, setIsPopupOpen] = useState2(false);
1520
+ const [isMobile, setIsMobile] = useState2(() => {
1521
+ if (typeof window === "undefined") return false;
1522
+ return window.matchMedia("(max-width: 768px)").matches;
1523
+ });
1524
+ const normalizedLayers = useMemo3(() => normalizeMapLayers(map), [map]);
1525
+ useEffect5(() => {
1526
+ if (typeof window === "undefined") return;
1527
+ const mql = window.matchMedia("(max-width: 768px)");
1528
+ const onChange = (e) => {
1529
+ setIsMobile(e.matches);
1530
+ };
1531
+ setIsMobile(mql.matches);
1532
+ if (typeof mql.addEventListener === "function") {
1533
+ mql.addEventListener("change", onChange);
1534
+ return () => mql.removeEventListener("change", onChange);
1535
+ }
1536
+ const legacy = mql;
1537
+ if (typeof legacy.addListener === "function") {
1538
+ legacy.addListener(onChange);
1539
+ return () => legacy.removeListener(onChange);
1540
+ }
1541
+ return;
1542
+ }, []);
1543
+ useEffect5(() => {
1544
+ if (featureInfoMode === "popup") {
1545
+ ensurePopupStyles();
1546
+ }
1547
+ }, [featureInfoMode]);
1548
+ useEffect5(() => {
1549
+ if (featureInfoMode !== "popup") {
1550
+ setIsPopupOpen(false);
1551
+ }
1552
+ }, [featureInfoMode]);
1553
+ useEffect5(() => {
1554
+ if (!mapInstance) return;
1555
+ const popupPane = mapInstance.getPane("popupPane");
1556
+ if (popupPane) {
1557
+ popupPane.style.zIndex = "800";
1558
+ }
1559
+ const labelsPane = mapInstance.getPane(LABELS_PANE_NAME) ?? mapInstance.createPane(LABELS_PANE_NAME);
1560
+ labelsPane.style.zIndex = "600";
1561
+ }, [mapInstance]);
1562
+ useEffect5(() => {
1563
+ if (!mapInstance) return;
1564
+ const handlePopupOpen = () => setIsPopupOpen(true);
1565
+ const handlePopupClose = () => setIsPopupOpen(false);
1566
+ mapInstance.on("popupopen", handlePopupOpen);
1567
+ mapInstance.on("popupclose", handlePopupClose);
1568
+ return () => {
1569
+ mapInstance.off("popupopen", handlePopupOpen);
1570
+ mapInstance.off("popupclose", handlePopupClose);
1571
+ };
1572
+ }, [mapInstance]);
1573
+ const layerStyleIndex = useMemo3(() => {
1574
+ const index = /* @__PURE__ */ new Map();
1575
+ (map?.mapLayers ?? []).forEach((entry) => {
1576
+ const layerStyle = entry.layer?.style ?? entry.mapLayer?.layer?.style ?? entry.style ?? null;
1577
+ const layerId = entry.layerId ?? entry.mapLayer?.layerId ?? entry.mapLayer?.layer?.id ?? entry.layer?.id;
1578
+ if (layerId !== void 0 && layerId !== null && layerStyle) {
1579
+ index.set(String(layerId), layerStyle);
1580
+ }
1581
+ });
1582
+ return index;
1583
+ }, [map]);
1584
+ const labelKeyIndex = useMemo3(() => {
1585
+ const index = /* @__PURE__ */ new Map();
1586
+ normalizedLayers.forEach((entry) => {
1587
+ const label = entry.layer?.label ?? entry.mapLayer?.label ?? entry.mapLayer.layerConfig?.label;
1588
+ if (typeof label === "string" && label.trim().length > 0) {
1589
+ index.set(String(entry.layerId), label);
1590
+ }
1591
+ });
1592
+ return index;
1593
+ }, [normalizedLayers]);
1594
+ const layerMetaIndex = useMemo3(() => {
1595
+ const index = /* @__PURE__ */ new Map();
1596
+ normalizedLayers.forEach((entry) => {
1597
+ index.set(String(entry.layerId), {
1598
+ layerType: typeof entry.layer?.layerType === "string" ? entry.layer.layerType : typeof entry.mapLayer.layerType === "string" ? entry.mapLayer.layerType : void 0,
1599
+ geometryType: typeof entry.layer?.geometryType === "string" ? entry.layer.geometryType : typeof entry.mapLayer.layerConfig?.geometryType === "string" ? entry.mapLayer.layerConfig.geometryType : void 0
1600
+ });
1601
+ });
1602
+ return index;
1603
+ }, [normalizedLayers]);
1604
+ const overlayStyleFunction = useMemo3(() => {
1605
+ return (feature) => {
1606
+ const featureLayerId = getFeatureLayerId(feature);
1607
+ const featureStyleOverrides = getFeatureStyleOverrides(feature);
1608
+ let style = null;
1609
+ if (featureLayerId !== null) {
1610
+ style = getStyleByLayerId(featureLayerId, mapLayers) ?? layerStyleIndex.get(String(featureLayerId)) ?? null;
1611
+ }
1612
+ const resolvedStyle = featureStyleOverrides ? { ...style ?? {}, ...featureStyleOverrides } : style;
1613
+ const defaultOptions = {
1614
+ color: resolvedStyle?.color ?? resolvedStyle?.fillColor ?? "#2563eb",
1615
+ weight: resolvedStyle?.weight ?? 2,
1616
+ fillColor: resolvedStyle?.fillColor ?? resolvedStyle?.color ?? "#2563eb",
1617
+ fillOpacity: resolvedStyle?.fillOpacity ?? 0.15,
1618
+ ...overlayStyle ?? {}
1619
+ };
1620
+ return defaultOptions;
1621
+ };
1622
+ }, [layerStyleIndex, mapLayers, overlayStyle]);
1623
+ const overlayStyleFn = useCallback2(
1624
+ (feature, _layerType, _baseOpacity) => overlayStyleFunction(feature),
1625
+ [overlayStyleFunction]
1626
+ );
1627
+ const [baseStates, setBaseStates] = useState2([]);
1628
+ const [mapOverrides, setMapOverrides] = useState2([]);
1629
+ const [controlOverrides, setControlOverrides] = useState2([]);
1630
+ const [uiOverrides, setUiOverrides] = useState2([]);
1631
+ useEffect5(() => {
1632
+ let isMounted = true;
1633
+ setLoadingMap(true);
1634
+ setMapError(null);
1635
+ onLoadingChange?.("map", true);
1636
+ client.maps.getMap(mapId, true).then((mapResponse) => {
1637
+ if (!isMounted) return;
1638
+ const extracted = extractMapDto(mapResponse);
1639
+ const resolved = extracted ? normalizeMapCenter(extracted) : null;
1640
+ setMap(resolved);
1641
+ }).catch((err) => {
1642
+ if (!isMounted) return;
1643
+ setMapError(err instanceof Error ? err.message : "No se pudo obtener el mapa");
1644
+ onError?.(err, "map");
1645
+ }).finally(() => {
1646
+ if (!isMounted) return;
1647
+ setLoadingMap(false);
1648
+ onLoadingChange?.("map", false);
1649
+ });
1650
+ return () => {
1651
+ isMounted = false;
1652
+ };
1653
+ }, [client.maps, mapId, onError, onLoadingChange]);
1654
+ useEffect5(() => {
1655
+ if (normalizedLayers.length === 0) {
1656
+ setLayers([]);
1657
+ setBaseStates([]);
1658
+ setMapOverrides([]);
1659
+ setUiOverrides([]);
1660
+ return;
1661
+ }
1662
+ const nextLayers = normalizedLayers.map((entry) => ({
1663
+ mapLayer: { ...entry.mapLayer, layerId: entry.layerId },
1664
+ layer: entry.layer,
1665
+ displayOrder: entry.displayOrder
1666
+ }));
1667
+ const base = initLayerStates(
1668
+ normalizedLayers.map((entry) => ({
1669
+ ...entry.mapLayer,
1670
+ layerId: entry.layerId,
1671
+ opacity: entry.opacity,
1672
+ isVisible: entry.isVisible
1673
+ }))
1674
+ );
1675
+ const initialOverrides = normalizedLayers.map((entry) => ({
1676
+ layerId: entry.layerId,
1677
+ overrideVisible: entry.isVisible,
1678
+ overrideOpacity: entry.opacity
1679
+ }));
1680
+ setLayers(nextLayers);
1681
+ setBaseStates(base);
1682
+ setMapOverrides(initialOverrides);
1683
+ setUiOverrides([]);
1684
+ }, [normalizedLayers]);
1685
+ useEffect5(() => {
1686
+ if (!layerControls) {
1687
+ setControlOverrides([]);
1688
+ return;
1689
+ }
1690
+ const overrides = layerControls.map((entry) => ({
1691
+ layerId: entry.layerId,
1692
+ overrideVisible: entry.visible,
1693
+ overrideOpacity: entry.opacity
1694
+ }));
1695
+ setControlOverrides(overrides);
1696
+ }, [layerControls]);
1697
+ useEffect5(() => {
1698
+ if (layerStates) {
1699
+ return;
1700
+ }
1701
+ if (Array.isArray(layerControls) && layerControls.length === 0 && effectiveStates.length > 0) {
1702
+ const reset = resetOverrides(baseStates);
1703
+ setEffectiveStates(reset);
1704
+ onLayerStateChange?.(reset);
1705
+ }
1706
+ }, [baseStates, effectiveStates.length, layerControls, layerStates, onLayerStateChange]);
1707
+ useEffect5(() => {
1708
+ if (layerStates) {
1709
+ setEffectiveStates(layerStates);
1710
+ }
1711
+ }, [layerStates]);
1712
+ useEffect5(() => {
1713
+ if (layerStates) {
1714
+ return;
1715
+ }
1716
+ if (mapOverrides.length === 0 && controlOverrides.length === 0 && uiOverrides.length === 0) {
1717
+ setEffectiveStates(baseStates);
1718
+ return;
1719
+ }
1720
+ const combined = [...mapOverrides, ...controlOverrides, ...uiOverrides];
1721
+ const next = applyLayerOverrides(baseStates, combined);
1722
+ setEffectiveStates(next);
1723
+ onLayerStateChange?.(next);
1724
+ }, [baseStates, controlOverrides, layerStates, mapOverrides, onLayerStateChange, uiOverrides]);
1725
+ useEffect5(() => {
1726
+ if (!Array.isArray(layerControls) || layerControls.length > 0) return;
1727
+ setUiOverrides([]);
1728
+ }, [layerControls]);
1729
+ useEffect5(() => {
1730
+ if (layerStates) {
1731
+ return;
1732
+ }
1733
+ if (!Array.isArray(layerControls) && effectiveStates.length > 0 && baseStates.length > 0) {
1734
+ const next = resetOverrides(baseStates);
1735
+ setEffectiveStates(next);
1736
+ onLayerStateChange?.(next);
1737
+ }
1738
+ }, [baseStates, effectiveStates.length, layerControls, layerStates, onLayerStateChange]);
1739
+ const upsertUiOverride = (layerId, patch) => {
1740
+ setUiOverrides((prev) => {
1741
+ const filtered = prev.filter((entry) => entry.layerId !== layerId);
1742
+ const nextEntry = {
1743
+ layerId,
1744
+ ...patch
1745
+ };
1746
+ return [...filtered, nextEntry];
1747
+ });
1748
+ };
1749
+ const updateOpacityFromUi = useCallback2(
1750
+ (layerId, uiOpacity) => {
1751
+ const meta = layerMetaIndex.get(String(layerId));
1752
+ const baseOpacity = clampOpacity3(uiOpacity);
1753
+ const effectiveOpacity = calculateZoomBasedOpacity(
1754
+ currentZoom,
1755
+ baseOpacity,
1756
+ meta?.layerType,
1757
+ meta?.geometryType
1758
+ );
1759
+ if (layerStates && onLayerStateChange) {
1760
+ const next = effectiveStates.map(
1761
+ (state) => state.layerId === layerId ? {
1762
+ ...state,
1763
+ baseOpacity,
1764
+ opacity: effectiveOpacity,
1765
+ overrideOpacity: uiOpacity
1766
+ } : state
1767
+ );
1768
+ onLayerStateChange(next);
1769
+ return;
1770
+ }
1771
+ setBaseStates(
1772
+ (prev) => prev.map((state) => state.layerId === layerId ? { ...state, baseOpacity } : state)
1773
+ );
1774
+ setUiOverrides((prev) => {
1775
+ const existing = prev.find((entry) => entry.layerId === layerId);
1776
+ const filtered = prev.filter((entry) => entry.layerId !== layerId);
1777
+ if (existing && existing.overrideVisible !== void 0) {
1778
+ return [
1779
+ ...filtered,
1780
+ { layerId, overrideVisible: existing.overrideVisible }
1781
+ ];
1782
+ }
1783
+ return filtered;
1784
+ });
1785
+ },
1786
+ [currentZoom, effectiveStates, layerMetaIndex, layerStates, onLayerStateChange]
1787
+ );
1788
+ const center = useMemo3(() => {
1789
+ if (initialCenter) {
1790
+ return initialCenter;
1791
+ }
1792
+ const normalized = normalizeCenterTuple(map?.settings?.center);
1793
+ if (normalized) {
1794
+ return normalized;
1795
+ }
1796
+ return DEFAULT_CENTER;
1797
+ }, [initialCenter, map?.settings?.center]);
1798
+ const zoom = initialZoom ?? map?.settings?.zoom ?? DEFAULT_ZOOM;
1799
+ useEffect5(() => {
1800
+ setCurrentZoom(zoom);
1801
+ }, [zoom]);
1802
+ useEffect5(() => {
1803
+ if (!layerGeojson) return;
1804
+ layers.forEach((layer) => {
1805
+ const layerKey = String(layer.mapLayer.layerId);
1806
+ const incoming = layerGeojson[layer.mapLayer.layerId] ?? layerGeojson[layerKey] ?? null;
1807
+ if (incoming && !originalGeojsonByLayerIdRef.current[layerKey]) {
1808
+ originalGeojsonByLayerIdRef.current[layerKey] = incoming;
1809
+ }
1810
+ });
1811
+ }, [layerGeojson, layers]);
1812
+ const decoratedLayers = useMemo3(() => {
1813
+ return layers.map((layer) => {
1814
+ const layerKey = String(layer.mapLayer.layerId);
1815
+ const override = layerGeojsonOverrides[layerKey];
1816
+ return {
1817
+ ...layer,
1818
+ effective: effectiveStates.find((state) => state.layerId === layer.mapLayer.layerId),
1819
+ data: override ?? layerGeojson?.[layer.mapLayer.layerId] ?? layerGeojson?.[layerKey] ?? null
1820
+ };
1821
+ });
1822
+ }, [effectiveStates, layerGeojson, layerGeojsonOverrides, layers]);
1823
+ const orderedLayers = useMemo3(() => {
1824
+ return [...decoratedLayers].filter((layer) => layer.effective?.visible && layer.data).sort((a, b) => a.displayOrder - b.displayOrder);
1825
+ }, [decoratedLayers]);
1826
+ const autoZoomGeojson = useMemo3(
1827
+ () => orderedLayers.map((layer) => layer.data).filter((collection) => !!collection),
1828
+ [orderedLayers]
1829
+ );
1830
+ const resolveLayerStyle = useCallback2(
1831
+ (layerId) => {
1832
+ return getStyleByLayerId(layerId, mapLayers) ?? layerStyleIndex.get(String(layerId)) ?? null;
1833
+ },
1834
+ [layerStyleIndex, mapLayers]
1835
+ );
1836
+ const labelMarkers = useMemo3(() => {
1837
+ const markers = [];
1838
+ decoratedLayers.forEach((layerState) => {
1839
+ if (!layerState.effective?.visible) return;
1840
+ const labelKey = labelKeyIndex.get(String(layerState.mapLayer.layerId));
1841
+ if (!labelKey) return;
1842
+ const data = layerState.data;
1843
+ if (!data) return;
1844
+ const resolvedStyle = resolveLayerStyle(layerState.mapLayer.layerId);
1845
+ const layerColor = resolvedStyle?.fillColor ?? resolvedStyle?.color ?? "rgba(37, 99, 235, 1)";
1846
+ const meta = layerMetaIndex.get(String(layerState.mapLayer.layerId));
1847
+ const baseOpacity = layerState.effective?.baseOpacity ?? layerState.effective?.opacity ?? 1;
1848
+ const opacity = calculateZoomBasedOpacity(
1849
+ currentZoom,
1850
+ baseOpacity,
1851
+ meta?.layerType,
1852
+ meta?.geometryType
1853
+ );
1854
+ data.features.forEach((feature, index) => {
1855
+ const properties = feature.properties;
1856
+ const value = properties?.[labelKey];
1857
+ if (value === void 0 || value === null || value === "") return;
1858
+ const bbox = computeBBoxFromFeature(feature);
1859
+ if (!bbox) return;
1860
+ const [lat, lng] = centroidFromBBox(bbox);
1861
+ markers.push({
1862
+ key: `${layerState.mapLayer.layerId}-label-${index}`,
1863
+ position: [lat, lng],
1864
+ label: String(value),
1865
+ opacity,
1866
+ layerId: layerState.mapLayer.layerId,
1867
+ color: layerColor
1868
+ });
1869
+ });
1870
+ });
1871
+ return markers;
1872
+ }, [currentZoom, decoratedLayers, labelKeyIndex, layerMetaIndex, resolveLayerStyle]);
1873
+ const ensureLayerPanes = useCallback2(
1874
+ (targetMap, targetLayers) => {
1875
+ const fillBaseZIndex = 400;
1876
+ const pointsBaseZIndex = 700;
1877
+ targetLayers.forEach((layer) => {
1878
+ const order = Number.isFinite(layer.displayOrder) ? layer.displayOrder : 0;
1879
+ const orderOffset = Math.max(0, Math.min(order, 150));
1880
+ const fillPaneName = `zenit-layer-${layer.layerId}-fill`;
1881
+ const pointPaneName = `zenit-layer-${layer.layerId}-points`;
1882
+ const fillPane = targetMap.getPane(fillPaneName) ?? targetMap.createPane(fillPaneName);
1883
+ const pointPane = targetMap.getPane(pointPaneName) ?? targetMap.createPane(pointPaneName);
1884
+ fillPane.style.zIndex = String(fillBaseZIndex + orderOffset);
1885
+ pointPane.style.zIndex = String(pointsBaseZIndex + orderOffset);
1886
+ });
1887
+ },
1888
+ []
1889
+ );
1890
+ const handleMapReady = useCallback2(
1891
+ (instance) => {
1892
+ setPanesReady(false);
1893
+ setMapInstance(instance);
1894
+ onMapReady?.(instance);
1895
+ },
1896
+ [onMapReady]
1897
+ );
1898
+ useEffect5(() => {
1899
+ if (!mapInstance) {
1900
+ setPanesReady(false);
1901
+ return;
1902
+ }
1903
+ if (orderedLayers.length === 0 && !overlayGeojson) {
1904
+ return;
1905
+ }
1906
+ const layerTargets = orderedLayers.map((layer) => ({
1907
+ layerId: layer.mapLayer.layerId,
1908
+ displayOrder: layer.displayOrder
1909
+ }));
1910
+ if (overlayGeojson) {
1911
+ layerTargets.push({ layerId: "overlay-geojson", displayOrder: 999 });
1912
+ }
1913
+ ensureLayerPanes(mapInstance, layerTargets);
1914
+ const first = layerTargets[0];
1915
+ const testPane = first ? mapInstance.getPane(`zenit-layer-${first.layerId}-fill`) : null;
1916
+ const labelsPane = mapInstance.getPane(LABELS_PANE_NAME);
1917
+ if (testPane && labelsPane) {
1918
+ setPanesReady(true);
1919
+ }
1920
+ }, [mapInstance, orderedLayers, overlayGeojson, ensureLayerPanes]);
1921
+ const overlayOnEachFeature = useCallback2((feature, layer) => {
1922
+ const layerId = getFeatureLayerId(feature) ?? void 0;
1923
+ const geometryType = feature?.geometry?.type;
1924
+ const clickIntent = getClickIntent({ feature, leafletLayer: layer });
1925
+ const isPointFeature = clickIntent === "point";
1926
+ const originalStyle = layer instanceof L4.Path ? {
1927
+ color: layer.options.color,
1928
+ weight: layer.options.weight,
1929
+ fillColor: layer.options.fillColor,
1930
+ opacity: layer.options.opacity,
1931
+ fillOpacity: layer.options.fillOpacity
1932
+ } : null;
1933
+ const originalRadius = layer instanceof L4.CircleMarker ? layer.getRadius() : null;
1934
+ if (featureInfoMode === "popup") {
1935
+ const content = buildFeaturePopupHtml(feature);
1936
+ if (content) {
1937
+ const { maxWidth, minWidth, maxHeight } = getPopupDimensions();
1938
+ layer.bindPopup(content, {
1939
+ maxWidth,
1940
+ minWidth,
1941
+ maxHeight,
1942
+ className: "custom-leaflet-popup",
1943
+ autoClose: true,
1944
+ closeOnClick: true,
1945
+ autoPan: true,
1946
+ closeButton: true,
1947
+ keepInView: true,
1948
+ autoPanPadding: [50, 50]
1949
+ });
1950
+ }
1951
+ }
1952
+ if (isPointFeature && layer.bindTooltip) {
1953
+ layer.bindTooltip("Click para ver detalle", {
1954
+ sticky: true,
1955
+ direction: "top",
1956
+ opacity: 0.9,
1957
+ className: "zenit-map-tooltip"
1958
+ });
1959
+ }
1960
+ layer.on("click", () => {
1961
+ if (featureInfoMode === "popup" && client && layerId !== void 0 && !extractDescriptionValue(feature?.properties) && feature?.geometry) {
1962
+ if (DEV_MODE2) {
1963
+ console.debug("[ZenitMap] click/intersect:start", {
1964
+ expectedLayerId: layerId,
1965
+ geometryType,
1966
+ clickIntent,
1967
+ leafletLayerType: layer?.constructor?.name,
1968
+ pane: layer?.options?.pane
1969
+ });
1970
+ }
1971
+ const trackedFeature = feature;
1972
+ if (!trackedFeature.__zenit_popup_loaded) {
1973
+ trackedFeature.__zenit_popup_loaded = true;
1974
+ client.layers.getLayerGeoJsonIntersect({
1975
+ id: layerId,
1976
+ geometry: feature.geometry
1977
+ }).then((response) => {
1978
+ const geo = extractGeoJsonFeatureCollection(response);
1979
+ const candidates = geo?.features ?? [];
1980
+ const selection = pickIntersectFeature({
1981
+ baseFeature: feature,
1982
+ candidates,
1983
+ expectedLayerId: layerId,
1984
+ clickIntent
1985
+ });
1986
+ const resolved = selection?.feature;
1987
+ if (DEV_MODE2) {
1988
+ console.debug("[ZenitMap] click/intersect:result", {
1989
+ expectedLayerId: layerId,
1990
+ clickIntent,
1991
+ candidatesCount: candidates.length,
1992
+ candidates: candidates.map((candidate, idx) => ({
1993
+ idx,
1994
+ geomType: candidate?.geometry?.type,
1995
+ candidateLayerId: candidateLayerId(candidate),
1996
+ hasDescription: Boolean(extractDescriptionValue(candidate?.properties))
1997
+ })),
1998
+ selectedIdx: selection?.selectedIdx ?? -1,
1999
+ selectionRule: selection?.reason ?? "none"
2000
+ });
2001
+ }
2002
+ if (!resolved?.properties) return;
2003
+ const mergedProperties = {
2004
+ ...trackedFeature.properties ?? {},
2005
+ ...resolved.properties
2006
+ };
2007
+ trackedFeature.properties = mergedProperties;
2008
+ const updatedHtml = buildFeaturePopupHtml({
2009
+ ...feature,
2010
+ properties: mergedProperties
2011
+ });
2012
+ if (updatedHtml && layer.setPopupContent) {
2013
+ layer.setPopupContent(updatedHtml);
2014
+ }
2015
+ }).catch(() => {
2016
+ trackedFeature.__zenit_popup_loaded = false;
2017
+ });
2018
+ }
2019
+ }
2020
+ onFeatureClick?.(feature, layerId);
2021
+ });
2022
+ layer.on("mouseover", () => {
2023
+ if (layer instanceof L4.Path && originalStyle) {
2024
+ layer.setStyle({
2025
+ ...originalStyle,
2026
+ weight: (originalStyle.weight ?? 2) + 1,
2027
+ opacity: Math.min(1, (originalStyle.opacity ?? 1) + 0.2),
2028
+ fillOpacity: Math.min(1, (originalStyle.fillOpacity ?? 0.8) + 0.1)
2029
+ });
2030
+ }
2031
+ if (layer instanceof L4.CircleMarker && typeof originalRadius === "number") {
2032
+ layer.setRadius(originalRadius + 1);
2033
+ }
2034
+ onFeatureHover?.(feature, layerId);
2035
+ });
2036
+ layer.on("mouseout", () => {
2037
+ if (layer instanceof L4.Path && originalStyle) {
2038
+ layer.setStyle(originalStyle);
2039
+ }
2040
+ if (layer instanceof L4.CircleMarker && typeof originalRadius === "number") {
2041
+ layer.setRadius(originalRadius);
2042
+ }
2043
+ });
2044
+ }, [client, featureInfoMode, onFeatureClick, onFeatureHover]);
2045
+ const buildLayerStyle = useCallback2((layerId, baseOpacity, feature, layerType) => {
2046
+ const style = resolveLayerStyle(layerId);
2047
+ const featureStyleOverrides = getFeatureStyleOverrides(feature);
2048
+ const resolvedStyle = featureStyleOverrides ? { ...style ?? {}, ...featureStyleOverrides } : style;
2049
+ const geometryType = feature?.geometry?.type;
2050
+ const resolvedLayerType = layerType ?? geometryType;
2051
+ return layerStyleToLeaflet({
2052
+ baseOpacity,
2053
+ zoom: currentZoom,
2054
+ layerStyle: resolvedStyle,
2055
+ geometryType,
2056
+ layerType: resolvedLayerType,
2057
+ properties: feature?.properties ?? void 0
2058
+ });
2059
+ }, [currentZoom, resolveLayerStyle]);
2060
+ const makeStyleFnForLayer = useCallback2((layerId) => {
2061
+ return (feature, layerType, baseOpacity) => {
2062
+ return buildLayerStyle(layerId, baseOpacity ?? 1, feature, layerType);
2063
+ };
2064
+ }, [buildLayerStyle]);
2065
+ useImperativeHandle(ref, () => ({
2066
+ setLayerOpacity: (layerId, opacity) => {
2067
+ upsertUiOverride(layerId, { overrideOpacity: opacity });
2068
+ },
2069
+ setLayerVisibility: (layerId, visible) => {
2070
+ upsertUiOverride(layerId, { overrideVisible: visible });
2071
+ },
2072
+ fitBounds: (bbox, options) => {
2073
+ if (!mapInstance) return;
2074
+ 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)) {
2075
+ console.warn("[ZenitMap.fitBounds] Invalid bbox: missing or non-finite coordinates", bbox);
2076
+ return;
2077
+ }
2078
+ const bounds = [
2079
+ [bbox.minLat, bbox.minLon],
2080
+ [bbox.maxLat, bbox.maxLon]
2081
+ ];
2082
+ const fitOptions = {
2083
+ padding: options?.padding ?? [12, 12],
2084
+ animate: options?.animate ?? true
2085
+ };
2086
+ mapInstance.fitBounds(bounds, fitOptions);
2087
+ },
2088
+ fitToBbox: (bbox, padding) => {
2089
+ if (!mapInstance) return;
2090
+ 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)) {
2091
+ console.warn("[ZenitMap.fitToBbox] Invalid bbox", bbox);
2092
+ return;
2093
+ }
2094
+ const resolvedPadding = padding ?? (isMobile ? 40 : 24);
2095
+ const bounds = L4.latLngBounds([bbox.minLat, bbox.minLon], [bbox.maxLat, bbox.maxLon]);
2096
+ mapInstance.fitBounds(bounds, { padding: [resolvedPadding, resolvedPadding], animate: true });
2097
+ },
2098
+ fitToGeoJson: (fc, padding) => {
2099
+ if (!mapInstance) return;
2100
+ if (!fc?.features?.length) return;
2101
+ const bounds = L4.geoJSON(fc).getBounds();
2102
+ if (!bounds.isValid()) return;
2103
+ const resolvedPadding = padding ?? (isMobile ? 40 : 24);
2104
+ mapInstance.fitBounds(bounds, { padding: [resolvedPadding, resolvedPadding], animate: true });
2105
+ },
2106
+ setView: (coordinates, zoom2) => {
2107
+ if (!mapInstance) return;
2108
+ mapInstance.setView([coordinates.lat, coordinates.lon], zoom2 ?? mapInstance.getZoom(), {
2109
+ animate: true
2110
+ });
2111
+ },
2112
+ getLayerSnapshot: () => {
2113
+ return effectiveStates.map((state) => ({
2114
+ layerId: state.layerId,
2115
+ visible: state.visible,
2116
+ opacity: state.opacity
2117
+ }));
2118
+ },
2119
+ restoreLayerSnapshot: (snapshot) => {
2120
+ const overrides = snapshot.map((s) => ({
2121
+ layerId: s.layerId,
2122
+ overrideVisible: s.visible,
2123
+ overrideOpacity: s.opacity
2124
+ }));
2125
+ setUiOverrides(overrides);
2126
+ },
2127
+ highlightFeature: (layerId, featureId) => {
2128
+ upsertUiOverride(layerId, { overrideVisible: true, overrideOpacity: 1 });
2129
+ },
2130
+ updateLayerGeoJson: (layerId, featureCollection) => {
2131
+ const layerKey = String(layerId);
2132
+ setLayerGeojsonOverrides((prev) => ({ ...prev, [layerKey]: featureCollection }));
2133
+ },
2134
+ restoreLayerGeoJson: (layerId) => {
2135
+ const layerKey = String(layerId);
2136
+ const original = originalGeojsonByLayerIdRef.current[layerKey];
2137
+ setLayerGeojsonOverrides((prev) => {
2138
+ const next = { ...prev };
2139
+ if (original) {
2140
+ next[layerKey] = original;
2141
+ } else {
2142
+ delete next[layerKey];
2143
+ }
2144
+ return next;
2145
+ });
2146
+ },
2147
+ getMapInstance: () => mapInstance
2148
+ }), [effectiveStates, isMobile, mapInstance]);
2149
+ if (loadingMap) {
2150
+ return /* @__PURE__ */ jsx3("div", { style: { padding: 16, height, width }, children: "Cargando mapa..." });
2151
+ }
2152
+ if (mapError) {
2153
+ return /* @__PURE__ */ jsxs3("div", { style: { padding: 16, height, width, color: "red" }, children: [
2154
+ "Error al cargar mapa: ",
2155
+ mapError
2156
+ ] });
2157
+ }
2158
+ if (!map) {
2159
+ return null;
2160
+ }
2161
+ const handleZoomChange = (zoomValue) => {
2162
+ setCurrentZoom(zoomValue);
2163
+ onZoomChange?.(zoomValue);
2164
+ };
2165
+ return /* @__PURE__ */ jsxs3(
2166
+ "div",
2167
+ {
2168
+ style: {
2169
+ display: "flex",
2170
+ height,
2171
+ width,
2172
+ border: "1px solid #e2e8f0",
2173
+ borderRadius: 4,
2174
+ overflow: "hidden",
2175
+ boxSizing: "border-box"
2176
+ },
2177
+ children: [
2178
+ /* @__PURE__ */ jsx3(
2179
+ "div",
2180
+ {
2181
+ className: `zenit-map-shell${isPopupOpen ? " popup-open" : ""}`,
2182
+ style: { flex: 1, position: "relative" },
2183
+ children: /* @__PURE__ */ jsxs3(
2184
+ MapContainer,
2185
+ {
2186
+ center,
2187
+ zoom,
2188
+ style: { height: "100%", width: "100%" },
2189
+ scrollWheelZoom: true,
2190
+ zoomControl: false,
2191
+ children: [
2192
+ /* @__PURE__ */ jsx3(
2193
+ TileLayer,
2194
+ {
2195
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
2196
+ attribution: "\xA9 OpenStreetMap contributors"
2197
+ }
2198
+ ),
2199
+ /* @__PURE__ */ jsx3(ZoomControl, { position: "topright" }),
2200
+ /* @__PURE__ */ jsx3(MapInstanceBridge, { onReady: handleMapReady }),
2201
+ /* @__PURE__ */ jsx3(MapInvalidator, { trigger: mapId }),
2202
+ /* @__PURE__ */ jsx3(
2203
+ BBoxZoomHandler,
2204
+ {
2205
+ bbox: zoomToBbox ?? void 0,
2206
+ geojson: zoomToGeojson ?? void 0,
2207
+ autoGeojson: autoZoomGeojson
2208
+ }
2209
+ ),
2210
+ /* @__PURE__ */ jsx3(ZoomBasedOpacityHandler, { onZoomChange: handleZoomChange }),
2211
+ /* @__PURE__ */ jsx3(LocationControl, {}),
2212
+ orderedLayers.map((layerState) => {
2213
+ const baseOpacity = layerState.effective?.baseOpacity ?? layerState.effective?.opacity ?? 1;
2214
+ const fillPaneName = `zenit-layer-${layerState.mapLayer.layerId}-fill`;
2215
+ const pointsPaneName = `zenit-layer-${layerState.mapLayer.layerId}-points`;
2216
+ const layerType = layerState.layer?.layerType ?? layerState.mapLayer.layerType ?? void 0;
2217
+ const labelKey = labelKeyIndex.get(String(layerState.mapLayer.layerId));
2218
+ return /* @__PURE__ */ jsxs3(React4.Fragment, { children: [
2219
+ layerState.data && panesReady && /* @__PURE__ */ jsx3(
2220
+ LayerGeoJson,
2221
+ {
2222
+ layerId: layerState.mapLayer.layerId,
2223
+ data: layerState.data,
2224
+ baseOpacity,
2225
+ isMobile,
2226
+ panesReady,
2227
+ mapInstance,
2228
+ fillPaneName,
2229
+ pointsPaneName,
2230
+ layerType,
2231
+ styleFn: makeStyleFnForLayer(layerState.mapLayer.layerId),
2232
+ onEachFeature: overlayOnEachFeature,
2233
+ onPolygonLabel: labelKey ? (feature, layer) => {
2234
+ const geometryType = feature?.geometry?.type;
2235
+ if (geometryType !== "Polygon" && geometryType !== "MultiPolygon") return;
2236
+ const properties = feature?.properties;
2237
+ const value = properties?.[labelKey];
2238
+ if (!value || !layer.bindTooltip) return;
2239
+ layer.bindTooltip(String(value), {
2240
+ sticky: true,
2241
+ direction: "center",
2242
+ opacity: 0.9,
2243
+ className: "polygon-label-tooltip"
2244
+ });
2245
+ } : void 0
2246
+ }
2247
+ ),
2248
+ panesReady && mapInstance?.getPane(LABELS_PANE_NAME) ? labelMarkers.filter(
2249
+ (marker) => String(marker.layerId) === String(layerState.mapLayer.layerId)
2250
+ ).map((marker) => /* @__PURE__ */ jsx3(
2251
+ Marker2,
2252
+ {
2253
+ position: marker.position,
2254
+ icon: createCustomIcon(marker.label, marker.opacity, marker.color),
2255
+ interactive: false,
2256
+ pane: LABELS_PANE_NAME
2257
+ },
2258
+ marker.key
2259
+ )) : null
2260
+ ] }, layerState.mapLayer.layerId.toString());
2261
+ }),
2262
+ overlayGeojson && panesReady && /* @__PURE__ */ jsx3(
2263
+ LayerGeoJson,
2264
+ {
2265
+ layerId: "overlay-geojson",
2266
+ data: overlayGeojson,
2267
+ baseOpacity: 1,
2268
+ isMobile,
2269
+ panesReady,
2270
+ mapInstance,
2271
+ fillPaneName: "zenit-overlay-fill",
2272
+ pointsPaneName: "zenit-overlay-points",
2273
+ styleFn: overlayStyleFn,
2274
+ onEachFeature: overlayOnEachFeature
2275
+ }
2276
+ )
2277
+ ]
2278
+ },
2279
+ String(mapId)
2280
+ )
2281
+ }
2282
+ ),
2283
+ showLayerPanel && decoratedLayers.length > 0 && /* @__PURE__ */ jsxs3(
2284
+ "div",
2285
+ {
2286
+ style: {
2287
+ width: 260,
2288
+ borderLeft: "1px solid #e2e8f0",
2289
+ padding: "12px 16px",
2290
+ background: "#fafafa",
2291
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
2292
+ fontSize: 14,
2293
+ overflowY: "auto"
2294
+ },
2295
+ children: [
2296
+ overlayGeojson && panesReady && /* @__PURE__ */ jsxs3(
2297
+ "div",
2298
+ {
2299
+ style: {
2300
+ border: "1px solid #bfdbfe",
2301
+ background: "#eff6ff",
2302
+ color: "#1e40af",
2303
+ padding: "8px 10px",
2304
+ borderRadius: 8,
2305
+ marginBottom: 12
2306
+ },
2307
+ children: [
2308
+ /* @__PURE__ */ jsx3("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "Overlay activo" }),
2309
+ /* @__PURE__ */ jsxs3("div", { style: { fontSize: 13 }, children: [
2310
+ "GeoJSON externo con ",
2311
+ (overlayGeojson.features?.length ?? 0).toLocaleString(),
2312
+ " elementos."
2313
+ ] })
2314
+ ]
2315
+ }
2316
+ ),
2317
+ /* @__PURE__ */ jsx3("div", { style: { fontWeight: 600, marginBottom: 12 }, children: "Capas" }),
2318
+ decoratedLayers.map((layerState) => /* @__PURE__ */ jsxs3(
2319
+ "div",
2320
+ {
2321
+ style: { borderBottom: "1px solid #e5e7eb", paddingBottom: 10, marginBottom: 10 },
2322
+ children: [
2323
+ /* @__PURE__ */ jsxs3("label", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
2324
+ /* @__PURE__ */ jsx3(
2325
+ "input",
2326
+ {
2327
+ type: "checkbox",
2328
+ checked: layerState.effective?.visible ?? false,
2329
+ onChange: (e) => {
2330
+ const visible = e.target.checked;
2331
+ upsertUiOverride(layerState.mapLayer.layerId, { overrideVisible: visible });
2332
+ }
2333
+ }
2334
+ ),
2335
+ /* @__PURE__ */ jsx3("span", { children: layerState.layer?.name ?? `Capa ${layerState.mapLayer.layerId}` })
2336
+ ] }),
2337
+ /* @__PURE__ */ jsxs3("div", { style: { marginTop: 8 }, children: [
2338
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 4 }, children: [
2339
+ /* @__PURE__ */ jsx3("span", { style: { color: "#4a5568" }, children: "Opacidad" }),
2340
+ /* @__PURE__ */ jsxs3("span", { children: [
2341
+ Math.round((layerState.effective?.opacity ?? 1) * 100),
2342
+ "%"
2343
+ ] })
2344
+ ] }),
2345
+ /* @__PURE__ */ jsx3(
2346
+ "input",
2347
+ {
2348
+ type: "range",
2349
+ min: 0,
2350
+ max: 1,
2351
+ step: 0.05,
2352
+ value: layerState.effective?.opacity ?? 1,
2353
+ onChange: (e) => {
2354
+ const value = Number(e.target.value);
2355
+ updateOpacityFromUi(layerState.mapLayer.layerId, value);
2356
+ },
2357
+ style: { width: "100%" }
2358
+ }
2359
+ )
2360
+ ] })
2361
+ ]
2362
+ },
2363
+ layerState.mapLayer.layerId.toString()
2364
+ ))
2365
+ ]
2366
+ }
2367
+ )
2368
+ ]
2369
+ }
2370
+ );
2371
+ });
2372
+ ZenitMap.displayName = "ZenitMap";
2373
+
2374
+ // src/react/ZenitLayerManager.tsx
2375
+ import React6, { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef6, useState as useState3 } from "react";
2376
+
2377
+ // src/react/icons.tsx
2378
+ import { Eye, EyeOff, ChevronDown, ChevronLeft, ChevronRight, Layers, Upload, X, ZoomIn } from "lucide-react";
2379
+
2380
+ // src/react/ui/ZenitSelect.tsx
2381
+ import React5 from "react";
2382
+ import { createPortal as createPortal2 } from "react-dom";
2383
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2384
+ var DEFAULT_SAFE_MENU_Z_INDEX = 4e3;
2385
+ function readCssMenuZIndex() {
2386
+ if (typeof document === "undefined") return null;
2387
+ const rootStyles = getComputedStyle(document.documentElement);
2388
+ const configuredValue = rootStyles.getPropertyValue("--zenit-select-z-index").trim() || rootStyles.getPropertyValue("--zenit-overlay-z-index").trim();
2389
+ if (!configuredValue) return null;
2390
+ const parsed = Number.parseInt(configuredValue, 10);
2391
+ return Number.isFinite(parsed) ? parsed : null;
2392
+ }
2393
+ function shouldUseNativeSelect() {
2394
+ if (typeof window === "undefined" || typeof navigator === "undefined") return false;
2395
+ const hasCoarsePointer = typeof window.matchMedia === "function" ? window.matchMedia("(pointer: coarse)").matches : false;
2396
+ return hasCoarsePointer || navigator.maxTouchPoints > 0;
2397
+ }
2398
+ var ZenitSelect = ({
2399
+ id,
2400
+ value,
2401
+ placeholder = "Seleccionar\u2026",
2402
+ options,
2403
+ onValueChange,
2404
+ disabled = false,
2405
+ className,
2406
+ menuZIndex,
2407
+ maxMenuHeight = 384,
2408
+ ariaLabel,
2409
+ useNativeOnMobile = true
2410
+ }) => {
2411
+ const rootRef = React5.useRef(null);
2412
+ const triggerRef = React5.useRef(null);
2413
+ const menuRef = React5.useRef(null);
2414
+ const lastTouchTsRef = React5.useRef(0);
2415
+ const [isOpen, setIsOpen] = React5.useState(false);
2416
+ const [menuPosition, setMenuPosition] = React5.useState({ top: 0, left: 0, width: 0 });
2417
+ const selectedOption = React5.useMemo(
2418
+ () => options.find((option) => option.value === value),
2419
+ [options, value]
2420
+ );
2421
+ const effectiveMenuZIndex = React5.useMemo(
2422
+ () => menuZIndex ?? readCssMenuZIndex() ?? DEFAULT_SAFE_MENU_Z_INDEX,
2423
+ [menuZIndex]
2424
+ );
2425
+ const useNative = React5.useMemo(
2426
+ () => useNativeOnMobile && shouldUseNativeSelect(),
2427
+ [useNativeOnMobile]
2428
+ );
2429
+ const updateMenuPosition = React5.useCallback(() => {
2430
+ const trigger = triggerRef.current;
2431
+ if (!trigger) return;
2432
+ const rect = trigger.getBoundingClientRect();
2433
+ const viewportPadding = 8;
2434
+ const minWidth = 200;
2435
+ const width = Math.max(rect.width, minWidth);
2436
+ const maxLeft = window.innerWidth - width - viewportPadding;
2437
+ const nextLeft = Math.max(viewportPadding, Math.min(rect.left, maxLeft));
2438
+ const nextTop = rect.bottom + 4;
2439
+ setMenuPosition({ top: nextTop, left: nextLeft, width });
2440
+ }, []);
2441
+ const getEffectiveMaxHeight = React5.useCallback(() => {
2442
+ const available = Math.max(160, window.innerHeight - menuPosition.top - 12);
2443
+ return Math.min(maxMenuHeight, available);
2444
+ }, [maxMenuHeight, menuPosition.top]);
2445
+ React5.useEffect(() => {
2446
+ if (!isOpen) return;
2447
+ updateMenuPosition();
2448
+ const isEventInsideSelect = (targetNode) => Boolean(rootRef.current?.contains(targetNode) || menuRef.current?.contains(targetNode));
2449
+ const handleOutsideTouchStart = (event) => {
2450
+ lastTouchTsRef.current = Date.now();
2451
+ const targetNode = event.target;
2452
+ if (isEventInsideSelect(targetNode)) return;
2453
+ setIsOpen(false);
2454
+ };
2455
+ const handleOutsideMouseDown = (event) => {
2456
+ if (Date.now() - lastTouchTsRef.current < 500) return;
2457
+ const targetNode = event.target;
2458
+ if (isEventInsideSelect(targetNode)) return;
2459
+ setIsOpen(false);
2460
+ };
2461
+ const handleEscape = (event) => {
2462
+ if (event.key === "Escape") {
2463
+ setIsOpen(false);
2464
+ triggerRef.current?.focus();
2465
+ }
2466
+ };
2467
+ const handleWindowChanges = () => updateMenuPosition();
2468
+ document.addEventListener("touchstart", handleOutsideTouchStart, { capture: true });
2469
+ document.addEventListener("mousedown", handleOutsideMouseDown, true);
2470
+ document.addEventListener("keydown", handleEscape);
2471
+ window.addEventListener("resize", handleWindowChanges);
2472
+ window.addEventListener("scroll", handleWindowChanges, true);
2473
+ return () => {
2474
+ document.removeEventListener("touchstart", handleOutsideTouchStart, true);
2475
+ document.removeEventListener("mousedown", handleOutsideMouseDown, true);
2476
+ document.removeEventListener("keydown", handleEscape);
2477
+ window.removeEventListener("resize", handleWindowChanges);
2478
+ window.removeEventListener("scroll", handleWindowChanges, true);
2479
+ };
2480
+ }, [isOpen, updateMenuPosition]);
2481
+ const handleTriggerDown = (event) => {
2482
+ if (event.type === "touchstart") {
2483
+ lastTouchTsRef.current = Date.now();
2484
+ }
2485
+ if (event.type === "mousedown" && Date.now() - lastTouchTsRef.current < 500) {
2486
+ return;
2487
+ }
2488
+ event.preventDefault();
2489
+ event.stopPropagation();
2490
+ if (disabled) return;
2491
+ if (!isOpen) updateMenuPosition();
2492
+ setIsOpen((prev) => !prev);
2493
+ };
2494
+ const handleTriggerClick = (event) => {
2495
+ if (event.detail !== 0) return;
2496
+ event.preventDefault();
2497
+ event.stopPropagation();
2498
+ if (disabled) return;
2499
+ if (!isOpen) updateMenuPosition();
2500
+ setIsOpen((prev) => !prev);
2501
+ };
2502
+ const selectValue = (nextValue, isDisabled) => {
2503
+ if (isDisabled) return;
2504
+ onValueChange?.(nextValue);
2505
+ setIsOpen(false);
2506
+ triggerRef.current?.focus();
2507
+ };
2508
+ if (useNative) {
2509
+ return /* @__PURE__ */ jsxs4("div", { ref: rootRef, className: [className, "zenit-select-root"].filter(Boolean).join(" "), children: [
2510
+ /* @__PURE__ */ jsx4("style", { children: `
2511
+ .zenit-select-native {
2512
+ width: 100%;
2513
+ min-height: 40px;
2514
+ height: 40px;
2515
+ border: 1px solid #cbd5e1;
2516
+ border-radius: 6px;
2517
+ background: #ffffff;
2518
+ color: #0f172a;
2519
+ padding: 0 12px;
2520
+ font-size: 14px;
2521
+ line-height: 1.25;
2522
+ cursor: pointer;
2523
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
2524
+ appearance: none;
2525
+ -webkit-appearance: none;
2526
+ }
2527
+ .zenit-select-native:disabled {
2528
+ opacity: 0.6;
2529
+ cursor: not-allowed;
2530
+ }
2531
+ .zenit-select-native:focus-visible {
2532
+ outline: none;
2533
+ border-color: #60a5fa;
2534
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.35);
2535
+ }
2536
+
2537
+ @media (max-width: 640px) {
2538
+ .zenit-select-native {
2539
+ min-height: 38px;
2540
+ height: 38px;
2541
+ }
2542
+ }
2543
+ ` }),
2544
+ /* @__PURE__ */ jsxs4(
2545
+ "select",
2546
+ {
2547
+ id,
2548
+ className: "zenit-select-native",
2549
+ value: value ?? "",
2550
+ "aria-label": ariaLabel,
2551
+ disabled,
2552
+ onChange: (event) => {
2553
+ onValueChange?.(event.target.value);
2554
+ },
2555
+ children: [
2556
+ /* @__PURE__ */ jsx4("option", { value: "", disabled: true, hidden: true, children: placeholder }),
2557
+ options.map((option) => /* @__PURE__ */ jsx4("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value))
2558
+ ]
2559
+ }
2560
+ )
2561
+ ] });
2562
+ }
2563
+ return /* @__PURE__ */ jsxs4("div", { ref: rootRef, className: [className, "zenit-select-root"].filter(Boolean).join(" "), children: [
2564
+ /* @__PURE__ */ jsx4("style", { children: `
2565
+ .zenit-select-trigger {
2566
+ width: 100%;
2567
+ min-height: 40px;
2568
+ height: 40px;
2569
+ border: 1px solid #cbd5e1;
2570
+ border-radius: 6px;
2571
+ background: #ffffff;
2572
+ color: #0f172a;
2573
+ display: inline-flex;
2574
+ align-items: center;
2575
+ justify-content: space-between;
2576
+ gap: 8px;
2577
+ padding: 0 12px;
2578
+ font-size: 14px;
2579
+ line-height: 1.25;
2580
+ cursor: pointer;
2581
+ text-align: left;
2582
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
2583
+ touch-action: manipulation;
2584
+ -webkit-tap-highlight-color: transparent;
2585
+ }
2586
+ .zenit-select-trigger.is-disabled {
2587
+ opacity: 0.6;
2588
+ cursor: not-allowed;
2589
+ }
2590
+ .zenit-select-trigger:focus-visible {
2591
+ outline: none;
2592
+ border-color: #60a5fa;
2593
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.35);
2594
+ }
2595
+ .zenit-select-label {
2596
+ white-space: nowrap;
2597
+ overflow: hidden;
2598
+ text-overflow: ellipsis;
2599
+ }
2600
+ .zenit-select-label.is-placeholder {
2601
+ color: #64748b;
2602
+ }
2603
+ .zenit-select-chevron {
2604
+ opacity: 0.5;
2605
+ flex-shrink: 0;
2606
+ }
2607
+
2608
+ /* Contenedor: aqu\xED debe GANAR el scroll en m\xF3vil */
2609
+ .zenit-select-content {
2610
+ position: fixed;
2611
+ border: 1px solid #cbd5e1;
2612
+ border-radius: 6px;
2613
+ background: #ffffff;
2614
+ box-shadow: 0 10px 25px rgba(15, 23, 42, 0.18);
2615
+ overflow-y: auto;
2616
+ padding: 4px;
2617
+ pointer-events: auto;
2618
+
2619
+ /* Lo importante: permitir pan vertical dentro del men\xFA */
2620
+ touch-action: pan-y;
2621
+ overscroll-behavior: contain;
2622
+ -webkit-overflow-scrolling: touch;
2623
+ }
2624
+
2625
+ /* Items NO son button: evitamos que WebView \u201Csecuestr\xE9\u201D el gesto */
2626
+ .zenit-select-item {
2627
+ width: 100%;
2628
+ border-radius: 4px;
2629
+ text-align: left;
2630
+ color: #0f172a;
2631
+ font-size: 14px;
2632
+ min-height: 34px;
2633
+ padding: 8px 10px;
2634
+ cursor: pointer;
2635
+ -webkit-tap-highlight-color: transparent;
2636
+ user-select: none;
2637
+ }
2638
+ .zenit-select-item:hover {
2639
+ background: #f1f5f9;
2640
+ }
2641
+ .zenit-select-item.is-selected {
2642
+ background: #e2e8f0;
2643
+ font-weight: 600;
2644
+ }
2645
+ .zenit-select-item.is-disabled {
2646
+ color: #94a3b8;
2647
+ cursor: not-allowed;
2648
+ }
2649
+ .zenit-select-item:focus-visible {
2650
+ outline: none;
2651
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.35);
2652
+ }
2653
+
2654
+ @media (max-width: 640px) {
2655
+ .zenit-select-trigger {
2656
+ height: 38px;
2657
+ min-height: 38px;
2658
+ }
2659
+ .zenit-select-content {
2660
+ max-width: calc(100vw - 16px);
2661
+ }
2662
+ }
2663
+ ` }),
2664
+ /* @__PURE__ */ jsxs4(
2665
+ "button",
2666
+ {
2667
+ id,
2668
+ ref: triggerRef,
2669
+ type: "button",
2670
+ className: `zenit-select-trigger${disabled ? " is-disabled" : ""}`,
2671
+ onTouchStart: handleTriggerDown,
2672
+ onMouseDown: handleTriggerDown,
2673
+ onClick: handleTriggerClick,
2674
+ "aria-haspopup": "listbox",
2675
+ "aria-expanded": isOpen,
2676
+ "aria-label": ariaLabel,
2677
+ disabled,
2678
+ children: [
2679
+ /* @__PURE__ */ jsx4("span", { className: `zenit-select-label${selectedOption ? "" : " is-placeholder"}`, children: selectedOption?.label ?? placeholder }),
2680
+ /* @__PURE__ */ jsx4(ChevronDown, { className: "zenit-select-chevron", size: 16, "aria-hidden": "true" })
2681
+ ]
2682
+ }
2683
+ ),
2684
+ isOpen && typeof document !== "undefined" && createPortal2(
2685
+ /* @__PURE__ */ jsx4(
2686
+ "div",
2687
+ {
2688
+ ref: menuRef,
2689
+ className: "zenit-select-content",
2690
+ role: "listbox",
2691
+ style: {
2692
+ top: menuPosition.top,
2693
+ left: menuPosition.left,
2694
+ width: menuPosition.width,
2695
+ zIndex: effectiveMenuZIndex,
2696
+ maxHeight: getEffectiveMaxHeight()
2697
+ },
2698
+ children: options.map((option) => {
2699
+ const isSelected = option.value === value;
2700
+ return /* @__PURE__ */ jsx4(
2701
+ "div",
2702
+ {
2703
+ role: "option",
2704
+ "aria-selected": isSelected,
2705
+ tabIndex: option.disabled ? -1 : 0,
2706
+ className: `zenit-select-item${isSelected ? " is-selected" : ""}${option.disabled ? " is-disabled" : ""}`,
2707
+ onClick: (event) => {
2708
+ event.preventDefault();
2709
+ event.stopPropagation();
2710
+ if (option.disabled) return;
2711
+ selectValue(option.value, option.disabled);
2712
+ },
2713
+ onKeyDown: (event) => {
2714
+ if (option.disabled) return;
2715
+ if (event.key === "Enter" || event.key === " ") {
2716
+ event.preventDefault();
2717
+ event.stopPropagation();
2718
+ selectValue(option.value, option.disabled);
2719
+ }
2720
+ },
2721
+ children: option.label
2722
+ },
2723
+ option.value
2724
+ );
2725
+ })
2726
+ }
2727
+ ),
2728
+ document.body
2729
+ )
2730
+ ] });
2731
+ };
2732
+
2733
+ // src/react/ZenitLayerManager.tsx
2734
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2735
+ var FLOAT_TOLERANCE = 1e-3;
2736
+ function areEffectiveStatesEqual(a, b) {
2737
+ if (a.length !== b.length) return false;
2738
+ return a.every((state, index) => {
2739
+ const other = b[index];
2740
+ if (!other) return false;
2741
+ const opacityDiff = Math.abs((state.opacity ?? 1) - (other.opacity ?? 1));
2742
+ return String(state.layerId) === String(other.layerId) && (state.visible ?? false) === (other.visible ?? false) && opacityDiff <= FLOAT_TOLERANCE;
2743
+ });
2744
+ }
2745
+ function getLayerColor2(style) {
2746
+ if (!style) return "#94a3b8";
2747
+ return style.fillColor ?? style.color ?? "#94a3b8";
2748
+ }
2749
+ function resolveDisplayOrder(value) {
2750
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2751
+ if (typeof value === "string") {
2752
+ const parsed = Number.parseFloat(value);
2753
+ if (Number.isFinite(parsed)) return parsed;
2754
+ }
2755
+ return 0;
2756
+ }
2757
+ var ZenitLayerManager = ({
2758
+ client,
2759
+ mapId,
2760
+ side = "right",
2761
+ className,
2762
+ style,
2763
+ height,
2764
+ layerStates,
2765
+ onLayerStatesChange,
2766
+ mapZoom,
2767
+ autoOpacityOnZoom = false,
2768
+ autoOpacityConfig,
2769
+ showUploadTab = true,
2770
+ showLayerVisibilityIcon = true,
2771
+ layerFeatureCounts,
2772
+ mapLayers,
2773
+ onApplyLayerFilter,
2774
+ onClearLayerFilter
2775
+ }) => {
2776
+ const [map, setMap] = useState3(null);
2777
+ const [loadingMap, setLoadingMap] = useState3(false);
2778
+ const [mapError, setMapError] = useState3(null);
2779
+ const [layers, setLayers] = useState3([]);
2780
+ const [activeTab, setActiveTab] = useState3("layers");
2781
+ const [panelVisible, setPanelVisible] = useState3(true);
2782
+ const [selectedFilterLayerId, setSelectedFilterLayerId] = useState3("");
2783
+ const [selectedFilterField, setSelectedFilterField] = useState3("");
2784
+ const [selectedFilterValue, setSelectedFilterValue] = useState3("");
2785
+ const [catalogByLayerField, setCatalogByLayerField] = useState3({});
2786
+ const [loadingCatalog, setLoadingCatalog] = useState3(false);
2787
+ const [applyingFilter, setApplyingFilter] = useState3(false);
2788
+ const [filterError, setFilterError] = useState3(null);
2789
+ const [appliedFilter, setAppliedFilter] = useState3(null);
2790
+ const catalogAbortRef = useRef6(null);
2791
+ const lastEmittedStatesRef = useRef6(null);
2792
+ const isControlled = Array.isArray(layerStates) && typeof onLayerStatesChange === "function";
2793
+ const baseStates = useMemo4(
2794
+ () => initLayerStates(
2795
+ layers.map((entry) => ({
2796
+ ...entry.mapLayer,
2797
+ layerId: entry.mapLayer.layerId,
2798
+ isVisible: entry.visible,
2799
+ opacity: entry.opacity
2800
+ }))
2801
+ ),
2802
+ [layers]
2803
+ );
2804
+ const overrideStates = useMemo4(
2805
+ () => layers.map(
2806
+ (entry) => ({
2807
+ layerId: entry.mapLayer.layerId,
2808
+ overrideVisible: entry.visible,
2809
+ overrideOpacity: entry.opacity
2810
+ })
2811
+ ),
2812
+ [layers]
2813
+ );
2814
+ const effectiveStates = useMemo4(
2815
+ () => layerStates ?? applyLayerOverrides(baseStates, overrideStates),
2816
+ [baseStates, layerStates, overrideStates]
2817
+ );
2818
+ const layerMetaIndex = useMemo4(() => {
2819
+ const index = /* @__PURE__ */ new Map();
2820
+ mapLayers?.forEach((entry) => {
2821
+ const key = String(entry.layerId);
2822
+ index.set(key, { layerType: entry.layerType ?? void 0, geometryType: entry.geometryType ?? void 0 });
2823
+ });
2824
+ map?.mapLayers?.forEach((entry) => {
2825
+ const key = String(entry.layerId);
2826
+ if (!index.has(key)) {
2827
+ index.set(key, { layerType: entry.layerType ?? void 0, geometryType: entry.geometryType ?? void 0 });
2828
+ }
2829
+ });
2830
+ return index;
2831
+ }, [map, mapLayers]);
2832
+ const resolveUserOpacity = React6.useCallback((state) => {
2833
+ if (typeof state.overrideOpacity === "number") return state.overrideOpacity;
2834
+ if (typeof state.overrideOpacity === "string") {
2835
+ const parsed = Number.parseFloat(state.overrideOpacity);
2836
+ if (Number.isFinite(parsed)) return parsed;
2837
+ }
2838
+ return state.opacity ?? 1;
2839
+ }, []);
2840
+ const resolveEffectiveOpacity = React6.useCallback(
2841
+ (layerId, userOpacity) => {
2842
+ if (!autoOpacityOnZoom || typeof mapZoom !== "number") {
2843
+ return userOpacity;
2844
+ }
2845
+ const meta = layerMetaIndex.get(String(layerId));
2846
+ return getEffectiveLayerOpacity(
2847
+ userOpacity,
2848
+ mapZoom,
2849
+ meta?.layerType,
2850
+ meta?.geometryType,
2851
+ autoOpacityConfig
2852
+ );
2853
+ },
2854
+ [autoOpacityConfig, autoOpacityOnZoom, layerMetaIndex, mapZoom]
2855
+ );
2856
+ const effectiveStatesWithZoom = useMemo4(() => {
2857
+ if (!autoOpacityOnZoom || typeof mapZoom !== "number") {
2858
+ return effectiveStates;
2859
+ }
2860
+ return effectiveStates.map((state) => {
2861
+ const userOpacity = resolveUserOpacity(state);
2862
+ const adjustedOpacity = resolveEffectiveOpacity(state.layerId, userOpacity);
2863
+ return {
2864
+ ...state,
2865
+ opacity: adjustedOpacity,
2866
+ overrideOpacity: userOpacity
2867
+ };
2868
+ });
2869
+ }, [autoOpacityOnZoom, effectiveStates, mapZoom, resolveEffectiveOpacity, resolveUserOpacity]);
2870
+ useEffect6(() => {
2871
+ let cancelled = false;
2872
+ setLoadingMap(true);
2873
+ setMapError(null);
2874
+ setLayers([]);
2875
+ client.maps.getMap(mapId, true).then((mapResponse) => {
2876
+ if (cancelled) return;
2877
+ const extractedMap = extractMapDto(mapResponse);
2878
+ const resolvedMap = extractedMap ? normalizeMapCenter(extractedMap) : null;
2879
+ setMap(resolvedMap);
2880
+ const normalizedLayers = normalizeMapLayers(resolvedMap);
2881
+ setLayers(
2882
+ normalizedLayers.map((entry, index) => ({
2883
+ mapLayer: { ...entry.mapLayer, layerId: entry.layerId },
2884
+ layer: entry.layer,
2885
+ visible: entry.isVisible,
2886
+ opacity: entry.opacity,
2887
+ displayOrder: entry.displayOrder,
2888
+ initialIndex: index
2889
+ }))
2890
+ );
2891
+ }).catch((err) => {
2892
+ if (cancelled) return;
2893
+ const message = err instanceof Error ? err.message : "No se pudo cargar el mapa";
2894
+ setMapError(message);
2895
+ }).finally(() => {
2896
+ if (!cancelled) setLoadingMap(false);
2897
+ });
2898
+ return () => {
2899
+ cancelled = true;
2900
+ };
2901
+ }, [client.maps, mapId]);
2902
+ useEffect6(() => {
2903
+ if (!showUploadTab && activeTab === "upload") {
2904
+ setActiveTab("layers");
2905
+ }
2906
+ }, [activeTab, showUploadTab]);
2907
+ useEffect6(() => {
2908
+ if (isControlled) return;
2909
+ if (!onLayerStatesChange) return;
2910
+ const emitStates = autoOpacityOnZoom && typeof mapZoom === "number" ? effectiveStatesWithZoom : effectiveStates;
2911
+ const previous = lastEmittedStatesRef.current;
2912
+ if (previous && areEffectiveStatesEqual(previous, emitStates)) {
2913
+ return;
2914
+ }
2915
+ lastEmittedStatesRef.current = emitStates;
2916
+ onLayerStatesChange(emitStates);
2917
+ }, [
2918
+ autoOpacityOnZoom,
2919
+ effectiveStates,
2920
+ effectiveStatesWithZoom,
2921
+ isControlled,
2922
+ mapZoom,
2923
+ onLayerStatesChange
2924
+ ]);
2925
+ const updateLayerVisible = React6.useCallback(
2926
+ (layerId, visible) => {
2927
+ if (!onLayerStatesChange) return;
2928
+ const next = effectiveStates.map(
2929
+ (state) => state.layerId === layerId ? { ...state, visible, overrideVisible: visible } : state
2930
+ );
2931
+ onLayerStatesChange(next);
2932
+ },
2933
+ [effectiveStates, onLayerStatesChange]
2934
+ );
2935
+ const updateLayerOpacity = React6.useCallback(
2936
+ (layerId, opacity) => {
2937
+ if (!onLayerStatesChange) return;
2938
+ const adjustedOpacity = resolveEffectiveOpacity(layerId, opacity);
2939
+ const next = effectiveStates.map(
2940
+ (state) => state.layerId === layerId ? { ...state, opacity: adjustedOpacity, overrideOpacity: opacity } : state
2941
+ );
2942
+ onLayerStatesChange(next);
2943
+ },
2944
+ [effectiveStates, onLayerStatesChange, resolveEffectiveOpacity]
2945
+ );
2946
+ const resolveFeatureCount = React6.useCallback(
2947
+ (layerId, layer) => {
2948
+ const resolvedFeatureCount = layerFeatureCounts?.[layerId] ?? layerFeatureCounts?.[String(layerId)];
2949
+ if (typeof resolvedFeatureCount === "number") return resolvedFeatureCount;
2950
+ const featureCount = layer?.featuresCount ?? layer?.featureCount ?? layer?.totalFeatures;
2951
+ return typeof featureCount === "number" ? featureCount : null;
2952
+ },
2953
+ [layerFeatureCounts]
2954
+ );
2955
+ const decoratedLayers = useMemo4(() => {
2956
+ return layers.map((entry) => ({
2957
+ ...entry,
2958
+ effective: effectiveStates.find((state) => state.layerId === entry.mapLayer.layerId),
2959
+ featureCount: resolveFeatureCount(entry.mapLayer.layerId, entry.layer),
2960
+ layerName: entry.layer?.name ?? entry.mapLayer.name ?? `Capa ${entry.mapLayer.layerId}`
2961
+ })).sort((a, b) => {
2962
+ const aOrder = resolveDisplayOrder(
2963
+ a.displayOrder ?? a.mapLayer.displayOrder ?? a.mapLayer.order
2964
+ );
2965
+ const bOrder = resolveDisplayOrder(
2966
+ b.displayOrder ?? b.mapLayer.displayOrder ?? b.mapLayer.order
2967
+ );
2968
+ const aHasOrder = a.displayOrder ?? a.mapLayer.displayOrder ?? a.mapLayer.order ?? null;
2969
+ const bHasOrder = b.displayOrder ?? b.mapLayer.displayOrder ?? b.mapLayer.order ?? null;
2970
+ if (aHasOrder !== null && bHasOrder !== null) {
2971
+ const orderCompare = aOrder - bOrder;
2972
+ if (orderCompare !== 0) return orderCompare;
2973
+ }
2974
+ const aInitial = a.initialIndex ?? 0;
2975
+ const bInitial = b.initialIndex ?? 0;
2976
+ if (aInitial !== bInitial) return aInitial - bInitial;
2977
+ const aName = a.layerName ?? String(a.mapLayer.layerId);
2978
+ const bName = b.layerName ?? String(b.mapLayer.layerId);
2979
+ const nameCompare = aName.localeCompare(bName, void 0, { sensitivity: "base" });
2980
+ if (nameCompare !== 0) return nameCompare;
2981
+ return String(a.mapLayer.layerId).localeCompare(String(b.mapLayer.layerId));
2982
+ });
2983
+ }, [effectiveStates, layers, resolveFeatureCount]);
2984
+ const hasPrefilters = useMemo4(() => {
2985
+ const candidates = [...mapLayers ?? [], ...map?.mapLayers ?? []];
2986
+ return candidates.some((layer) => {
2987
+ const record = layer;
2988
+ const applied = record.appliedFilters ?? record.prefilters ?? record.initialFilters ?? record.filters ?? (record.layerConfig?.appliedFilters ?? record.layerConfig?.prefilters);
2989
+ return !!applied && typeof applied === "object" && Object.keys(applied).length > 0;
2990
+ });
2991
+ }, [map?.mapLayers, mapLayers]);
2992
+ const filterableLayers = useMemo4(() => {
2993
+ return decoratedLayers.filter((entry) => {
2994
+ const prefilters = entry.mapLayer.layerConfig?.prefilters;
2995
+ return !!prefilters && Object.keys(prefilters).length > 0;
2996
+ });
2997
+ }, [decoratedLayers]);
2998
+ const selectedFilterLayer = useMemo4(
2999
+ () => filterableLayers.find((layer) => String(layer.mapLayer.layerId) === selectedFilterLayerId) ?? null,
3000
+ [filterableLayers, selectedFilterLayerId]
3001
+ );
3002
+ const filterFields = useMemo4(() => {
3003
+ const prefilters = selectedFilterLayer?.mapLayer.layerConfig?.prefilters;
3004
+ return prefilters ? Object.keys(prefilters) : [];
3005
+ }, [selectedFilterLayer]);
3006
+ const activeCatalogKey = selectedFilterLayer ? `${selectedFilterLayer.mapLayer.layerId}:${selectedFilterField}` : null;
3007
+ const activeCatalogValues = activeCatalogKey ? catalogByLayerField[activeCatalogKey] ?? [] : [];
3008
+ const extractCatalogValues = React6.useCallback((catalogData, field) => {
3009
+ const values = /* @__PURE__ */ new Set();
3010
+ const pushValue = (value) => {
3011
+ if (value === null || value === void 0) return;
3012
+ const normalized = String(value).trim();
3013
+ if (normalized) values.add(normalized);
3014
+ };
3015
+ if (catalogData && typeof catalogData === "object") {
3016
+ const maybeRecord = catalogData;
3017
+ const directField = maybeRecord[field];
3018
+ if (Array.isArray(directField)) {
3019
+ directField.forEach(pushValue);
3020
+ }
3021
+ const items = maybeRecord.items;
3022
+ if (Array.isArray(items)) {
3023
+ items.forEach((item) => {
3024
+ if (!item || typeof item !== "object") return;
3025
+ const row = item;
3026
+ const rowField = row.field;
3027
+ if (String(rowField ?? "").toUpperCase() === field.toUpperCase() && Array.isArray(row.values)) {
3028
+ row.values.forEach(pushValue);
3029
+ }
3030
+ });
3031
+ }
3032
+ const features = maybeRecord.features;
3033
+ if (Array.isArray(features)) {
3034
+ features.forEach((feature) => {
3035
+ if (!feature || typeof feature !== "object") return;
3036
+ const properties = feature.properties;
3037
+ if (properties && field in properties) {
3038
+ pushValue(properties[field]);
3039
+ }
3040
+ });
3041
+ }
3042
+ }
3043
+ return [...values].sort((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
3044
+ }, []);
3045
+ useEffect6(() => {
3046
+ if (!filterableLayers.length) {
3047
+ setSelectedFilterLayerId("");
3048
+ return;
3049
+ }
3050
+ if (!selectedFilterLayerId || !filterableLayers.some((layer) => String(layer.mapLayer.layerId) === selectedFilterLayerId)) {
3051
+ setSelectedFilterLayerId(String(filterableLayers[0].mapLayer.layerId));
3052
+ }
3053
+ }, [filterableLayers, selectedFilterLayerId]);
3054
+ useEffect6(() => {
3055
+ if (!filterFields.length) {
3056
+ setSelectedFilterField("");
3057
+ return;
3058
+ }
3059
+ if (!selectedFilterField || !filterFields.includes(selectedFilterField)) {
3060
+ setSelectedFilterField(filterFields[0]);
3061
+ setSelectedFilterValue("");
3062
+ }
3063
+ }, [filterFields, selectedFilterField]);
3064
+ useEffect6(() => {
3065
+ if (hasPrefilters && activeTab === "filters") {
3066
+ setActiveTab("layers");
3067
+ }
3068
+ }, [activeTab, hasPrefilters]);
3069
+ useEffect6(() => {
3070
+ if (activeTab !== "filters") return;
3071
+ if (!selectedFilterLayer || !selectedFilterField || !activeCatalogKey) return;
3072
+ if (catalogByLayerField[activeCatalogKey]) return;
3073
+ catalogAbortRef.current?.abort();
3074
+ const controller = new AbortController();
3075
+ catalogAbortRef.current = controller;
3076
+ setLoadingCatalog(true);
3077
+ setFilterError(null);
3078
+ client.layers.getLayerFeaturesCatalog(selectedFilterLayer.mapLayer.layerId).then((response) => {
3079
+ if (controller.signal.aborted) return;
3080
+ const values = extractCatalogValues(response.data, selectedFilterField);
3081
+ setCatalogByLayerField((prev) => ({ ...prev, [activeCatalogKey]: values }));
3082
+ }).catch((error) => {
3083
+ if (controller.signal.aborted) return;
3084
+ const message = error instanceof Error ? error.message : "No se pudo cargar el cat\xE1logo";
3085
+ setFilterError(message);
3086
+ }).finally(() => {
3087
+ if (!controller.signal.aborted) setLoadingCatalog(false);
3088
+ });
3089
+ return () => {
3090
+ controller.abort();
3091
+ };
3092
+ }, [activeCatalogKey, activeTab, catalogByLayerField, client.layers, extractCatalogValues, selectedFilterField, selectedFilterLayer]);
3093
+ const handleApplyFilter = React6.useCallback(async () => {
3094
+ if (!selectedFilterLayer || !selectedFilterField || !selectedFilterValue || !onApplyLayerFilter) return;
3095
+ setApplyingFilter(true);
3096
+ setFilterError(null);
3097
+ try {
3098
+ await onApplyLayerFilter({
3099
+ layerId: selectedFilterLayer.mapLayer.layerId,
3100
+ field: selectedFilterField,
3101
+ value: selectedFilterValue
3102
+ });
3103
+ setAppliedFilter({
3104
+ layerId: selectedFilterLayer.mapLayer.layerId,
3105
+ field: selectedFilterField,
3106
+ value: selectedFilterValue
3107
+ });
3108
+ } catch (error) {
3109
+ const message = error instanceof Error ? error.message : "No se pudo aplicar el filtro";
3110
+ setFilterError(message);
3111
+ } finally {
3112
+ setApplyingFilter(false);
3113
+ }
3114
+ }, [onApplyLayerFilter, selectedFilterField, selectedFilterLayer, selectedFilterValue]);
3115
+ const handleClearFilter = React6.useCallback(async () => {
3116
+ if (!selectedFilterLayer) return;
3117
+ setApplyingFilter(true);
3118
+ setFilterError(null);
3119
+ try {
3120
+ await onClearLayerFilter?.({ layerId: selectedFilterLayer.mapLayer.layerId, field: selectedFilterField || void 0 });
3121
+ setSelectedFilterValue("");
3122
+ setAppliedFilter(null);
3123
+ } catch (error) {
3124
+ const message = error instanceof Error ? error.message : "No se pudo limpiar el filtro";
3125
+ setFilterError(message);
3126
+ } finally {
3127
+ setApplyingFilter(false);
3128
+ }
3129
+ }, [onClearLayerFilter, selectedFilterField, selectedFilterLayer]);
3130
+ const resolveLayerStyle = React6.useCallback(
3131
+ (layerId) => {
3132
+ const layerKey = String(layerId);
3133
+ const fromProp = mapLayers?.find((entry) => String(entry.layerId) === layerKey)?.style;
3134
+ if (fromProp) return fromProp;
3135
+ const fromMapDto = map?.mapLayers?.find((entry) => String(entry.layerId) === layerKey);
3136
+ if (fromMapDto?.layer) {
3137
+ const layer = fromMapDto.layer;
3138
+ if (layer.style) return layer.style;
3139
+ }
3140
+ return void 0;
3141
+ },
3142
+ [map, mapLayers]
3143
+ );
3144
+ const panelStyle = {
3145
+ width: 360,
3146
+ borderLeft: side === "right" ? "1px solid #e2e8f0" : void 0,
3147
+ borderRight: side === "left" ? "1px solid #e2e8f0" : void 0,
3148
+ background: "#f1f5f9",
3149
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
3150
+ fontSize: 14,
3151
+ boxSizing: "border-box",
3152
+ height: height ?? "100%",
3153
+ display: "flex",
3154
+ flexDirection: "column",
3155
+ boxShadow: "0 12px 28px rgba(15, 23, 42, 0.08)",
3156
+ ...style,
3157
+ ...height ? { height } : {}
3158
+ };
3159
+ if (loadingMap) {
3160
+ return /* @__PURE__ */ jsx5("div", { className, style: panelStyle, children: "Cargando capas\u2026" });
3161
+ }
3162
+ if (mapError) {
3163
+ return /* @__PURE__ */ jsxs5("div", { className, style: { ...panelStyle, color: "#c53030" }, children: [
3164
+ "Error al cargar mapa: ",
3165
+ mapError
3166
+ ] });
3167
+ }
3168
+ if (!map) {
3169
+ return null;
3170
+ }
3171
+ const headerStyle = {
3172
+ padding: "14px 16px",
3173
+ borderBottom: "1px solid #e2e8f0",
3174
+ background: "#fff",
3175
+ position: "sticky",
3176
+ top: 0,
3177
+ zIndex: 2,
3178
+ boxShadow: "0 1px 0 rgba(148, 163, 184, 0.25)"
3179
+ };
3180
+ const renderLayerCards = () => {
3181
+ return /* @__PURE__ */ jsx5("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: decoratedLayers.map((layerState) => {
3182
+ const layerId = layerState.mapLayer.layerId;
3183
+ const layerName = layerState.layerName ?? `Capa ${layerId}`;
3184
+ const visible = layerState.effective?.visible ?? false;
3185
+ const userOpacity = layerState.effective ? resolveUserOpacity(layerState.effective) : 1;
3186
+ const featureCount = layerState.featureCount;
3187
+ const layerColor = getLayerColor2(resolveLayerStyle(layerId));
3188
+ const muted = !visible;
3189
+ const opacityPercent = Math.round(userOpacity * 100);
3190
+ const sliderBackground = `linear-gradient(to right, ${layerColor} 0%, ${layerColor} ${opacityPercent}%, #e5e7eb ${opacityPercent}%, #e5e7eb 100%)`;
3191
+ return /* @__PURE__ */ jsxs5(
3192
+ "div",
3193
+ {
3194
+ className: `zlm-card${muted ? " is-muted" : ""}`,
3195
+ style: {
3196
+ border: "1px solid #e2e8f0",
3197
+ borderRadius: 12,
3198
+ padding: 12,
3199
+ background: "#fff",
3200
+ display: "flex",
3201
+ flexDirection: "column",
3202
+ gap: 10,
3203
+ width: "100%"
3204
+ },
3205
+ children: [
3206
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", justifyContent: "space-between", gap: 12 }, children: [
3207
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: 10, alignItems: "flex-start", minWidth: 0, flex: 1 }, children: [
3208
+ /* @__PURE__ */ jsx5(
3209
+ "div",
3210
+ {
3211
+ style: {
3212
+ width: 14,
3213
+ height: 14,
3214
+ borderRadius: "50%",
3215
+ backgroundColor: layerColor,
3216
+ border: "2px solid #e2e8f0",
3217
+ flexShrink: 0,
3218
+ marginTop: 4
3219
+ },
3220
+ title: "Color de la capa"
3221
+ }
3222
+ ),
3223
+ showLayerVisibilityIcon && /* @__PURE__ */ jsx5(
3224
+ "button",
3225
+ {
3226
+ type: "button",
3227
+ className: `zlm-icon-button${visible ? " is-active" : ""}`,
3228
+ onClick: () => isControlled ? updateLayerVisible(layerId, !visible) : setLayers(
3229
+ (prev) => prev.map(
3230
+ (entry) => entry.mapLayer.layerId === layerId ? { ...entry, visible: !visible } : entry
3231
+ )
3232
+ ),
3233
+ "aria-label": visible ? "Ocultar capa" : "Mostrar capa",
3234
+ children: visible ? /* @__PURE__ */ jsx5(Eye, { size: 16 }) : /* @__PURE__ */ jsx5(EyeOff, { size: 16 })
3235
+ }
3236
+ ),
3237
+ /* @__PURE__ */ jsxs5("div", { style: { minWidth: 0, flex: 1 }, children: [
3238
+ /* @__PURE__ */ jsx5(
3239
+ "div",
3240
+ {
3241
+ className: "zlm-layer-name",
3242
+ style: {
3243
+ fontWeight: 700,
3244
+ display: "-webkit-box",
3245
+ WebkitLineClamp: 2,
3246
+ WebkitBoxOrient: "vertical",
3247
+ overflow: "hidden",
3248
+ overflowWrap: "anywhere",
3249
+ lineHeight: 1.2,
3250
+ color: muted ? "#64748b" : "#0f172a"
3251
+ },
3252
+ title: layerName,
3253
+ children: layerName
3254
+ }
3255
+ ),
3256
+ /* @__PURE__ */ jsxs5("div", { style: { color: muted ? "#94a3b8" : "#64748b", fontSize: 12 }, children: [
3257
+ "ID ",
3258
+ layerId
3259
+ ] })
3260
+ ] })
3261
+ ] }),
3262
+ /* @__PURE__ */ jsx5("div", { style: { display: "flex", alignItems: "flex-start", gap: 6, flexShrink: 0 }, children: typeof featureCount === "number" && /* @__PURE__ */ jsxs5("span", { className: "zlm-badge", children: [
3263
+ featureCount.toLocaleString(),
3264
+ " features"
3265
+ ] }) })
3266
+ ] }),
3267
+ /* @__PURE__ */ jsx5("div", { style: { display: "flex", gap: 10, alignItems: "center" }, children: /* @__PURE__ */ jsxs5("div", { style: { flex: 1 }, children: [
3268
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 6, color: "#64748b", fontSize: 12 }, children: [
3269
+ /* @__PURE__ */ jsx5("span", { children: "Opacidad" }),
3270
+ /* @__PURE__ */ jsxs5("span", { children: [
3271
+ opacityPercent,
3272
+ "%"
3273
+ ] })
3274
+ ] }),
3275
+ /* @__PURE__ */ jsx5(
3276
+ "input",
3277
+ {
3278
+ className: "zlm-range",
3279
+ type: "range",
3280
+ min: 0,
3281
+ max: 1,
3282
+ step: 0.05,
3283
+ value: userOpacity,
3284
+ style: {
3285
+ width: "100%",
3286
+ accentColor: layerColor,
3287
+ background: sliderBackground,
3288
+ height: 6,
3289
+ borderRadius: 999
3290
+ },
3291
+ onChange: (e) => {
3292
+ const value = Number(e.target.value);
3293
+ if (isControlled) {
3294
+ updateLayerOpacity(layerId, value);
3295
+ return;
3296
+ }
3297
+ setLayers(
3298
+ (prev) => prev.map(
3299
+ (entry) => entry.mapLayer.layerId === layerId ? { ...entry, opacity: value } : entry
3300
+ )
3301
+ );
3302
+ },
3303
+ "aria-label": `Opacidad de la capa ${layerName}`
3304
+ }
3305
+ )
3306
+ ] }) })
3307
+ ]
3308
+ },
3309
+ layerId.toString()
3310
+ );
3311
+ }) });
3312
+ };
3313
+ return /* @__PURE__ */ jsxs5("div", { className: ["zenit-layer-manager", className].filter(Boolean).join(" "), style: panelStyle, children: [
3314
+ /* @__PURE__ */ jsx5("style", { children: `
3315
+ .zenit-layer-manager .zlm-card {
3316
+ transition: box-shadow 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
3317
+ box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
3318
+ }
3319
+ .zenit-layer-manager .zlm-card.is-muted {
3320
+ opacity: 0.7;
3321
+ }
3322
+ .zenit-layer-manager .zlm-badge {
3323
+ display: inline-flex;
3324
+ align-items: center;
3325
+ gap: 4px;
3326
+ margin-top: 6px;
3327
+ padding: 2px 8px;
3328
+ border-radius: 999px;
3329
+ background: #f1f5f9;
3330
+ color: #475569;
3331
+ font-size: 11px;
3332
+ font-weight: 600;
3333
+ }
3334
+ .zenit-layer-manager .zlm-icon-button {
3335
+ border: 1px solid #e2e8f0;
3336
+ background: #f8fafc;
3337
+ color: #475569;
3338
+ border-radius: 8px;
3339
+ width: 34px;
3340
+ height: 34px;
3341
+ display: inline-flex;
3342
+ align-items: center;
3343
+ justify-content: center;
3344
+ cursor: pointer;
3345
+ transition: all 0.15s ease;
3346
+ }
3347
+ .zenit-layer-manager .zlm-icon-button.is-active {
3348
+ background: #0f172a;
3349
+ color: #fff;
3350
+ border-color: #0f172a;
3351
+ }
3352
+ .zenit-layer-manager .zlm-icon-button:hover {
3353
+ box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12);
3354
+ }
3355
+ .zenit-layer-manager .zlm-icon-button:focus-visible {
3356
+ outline: 2px solid #60a5fa;
3357
+ outline-offset: 2px;
3358
+ }
3359
+ .zenit-layer-manager .zlm-range {
3360
+ width: 100%;
3361
+ accent-color: #0f172a;
3362
+ }
3363
+ .zenit-layer-manager .zlm-tab {
3364
+ flex: 1;
3365
+ padding: 8px 12px;
3366
+ border: none;
3367
+ background: transparent;
3368
+ color: #475569;
3369
+ font-weight: 600;
3370
+ cursor: pointer;
3371
+ display: inline-flex;
3372
+ align-items: center;
3373
+ justify-content: center;
3374
+ gap: 6px;
3375
+ font-size: 13px;
3376
+ }
3377
+ .zenit-layer-manager .zlm-tab.is-active {
3378
+ background: #fff;
3379
+ color: #0f172a;
3380
+ box-shadow: 0 4px 10px rgba(15, 23, 42, 0.08);
3381
+ border-radius: 10px;
3382
+ }
3383
+ .zenit-layer-manager .zlm-tab:focus-visible {
3384
+ outline: 2px solid #60a5fa;
3385
+ outline-offset: 2px;
3386
+ }
3387
+ .zenit-layer-manager .zlm-panel-toggle {
3388
+ border: 1px solid #e2e8f0;
3389
+ background: #fff;
3390
+ color: #0f172a;
3391
+ border-radius: 10px;
3392
+ padding: 6px 10px;
3393
+ display: inline-flex;
3394
+ align-items: center;
3395
+ gap: 6px;
3396
+ font-size: 12px;
3397
+ font-weight: 600;
3398
+ cursor: pointer;
3399
+ transition: all 0.15s ease;
3400
+ }
3401
+ .zenit-layer-manager .zlm-panel-toggle:hover {
3402
+ box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12);
3403
+ }
3404
+ .zenit-layer-manager .zlm-panel-toggle:focus-visible {
3405
+ outline: 2px solid #60a5fa;
3406
+ outline-offset: 2px;
3407
+ }
3408
+ ` }),
3409
+ /* @__PURE__ */ jsxs5("div", { style: headerStyle, children: [
3410
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
3411
+ /* @__PURE__ */ jsxs5("div", { children: [
3412
+ /* @__PURE__ */ jsx5("div", { style: { fontWeight: 800, fontSize: 16, color: "#0f172a" }, children: "Gesti\xF3n de Capas" }),
3413
+ /* @__PURE__ */ jsxs5("div", { style: { color: "#64748b", fontSize: 12 }, children: [
3414
+ "Mapa #",
3415
+ map.id
3416
+ ] })
3417
+ ] }),
3418
+ /* @__PURE__ */ jsxs5(
3419
+ "button",
3420
+ {
3421
+ type: "button",
3422
+ onClick: () => setPanelVisible((prev) => !prev),
3423
+ className: "zlm-panel-toggle",
3424
+ "aria-label": panelVisible ? "Ocultar panel de capas" : "Mostrar panel de capas",
3425
+ children: [
3426
+ panelVisible ? /* @__PURE__ */ jsx5(Eye, { size: 16 }) : /* @__PURE__ */ jsx5(EyeOff, { size: 16 }),
3427
+ panelVisible ? "Ocultar" : "Mostrar"
3428
+ ]
3429
+ }
3430
+ )
3431
+ ] }),
3432
+ /* @__PURE__ */ jsxs5(
3433
+ "div",
3434
+ {
3435
+ style: {
3436
+ display: "flex",
3437
+ gap: 6,
3438
+ marginTop: 12,
3439
+ padding: 4,
3440
+ border: "1px solid #e2e8f0",
3441
+ borderRadius: 12,
3442
+ background: "#f1f5f9"
3443
+ },
3444
+ children: [
3445
+ /* @__PURE__ */ jsxs5(
3446
+ "button",
3447
+ {
3448
+ type: "button",
3449
+ className: `zlm-tab${activeTab === "layers" ? " is-active" : ""}`,
3450
+ onClick: () => setActiveTab("layers"),
3451
+ children: [
3452
+ /* @__PURE__ */ jsx5(Layers, { size: 16 }),
3453
+ "Capas"
3454
+ ]
3455
+ }
3456
+ ),
3457
+ showUploadTab && /* @__PURE__ */ jsxs5(
3458
+ "button",
3459
+ {
3460
+ type: "button",
3461
+ className: `zlm-tab${activeTab === "upload" ? " is-active" : ""}`,
3462
+ onClick: () => setActiveTab("upload"),
3463
+ children: [
3464
+ /* @__PURE__ */ jsx5(Upload, { size: 16 }),
3465
+ "Subir"
3466
+ ]
3467
+ }
3468
+ ),
3469
+ !hasPrefilters && /* @__PURE__ */ jsxs5(
3470
+ "button",
3471
+ {
3472
+ type: "button",
3473
+ className: `zlm-tab${activeTab === "filters" ? " is-active" : ""}`,
3474
+ onClick: () => setActiveTab("filters"),
3475
+ children: [
3476
+ /* @__PURE__ */ jsx5(Layers, { size: 16 }),
3477
+ "Filtros"
3478
+ ]
3479
+ }
3480
+ )
3481
+ ]
3482
+ }
3483
+ )
3484
+ ] }),
3485
+ panelVisible && /* @__PURE__ */ jsxs5("div", { style: { padding: "12px 10px 18px", overflowY: "auto", flex: 1, minHeight: 0 }, children: [
3486
+ hasPrefilters && /* @__PURE__ */ jsx5("div", { className: "zlm-badge", style: { marginBottom: 10 }, children: "Este mapa ya incluye filtros preaplicados" }),
3487
+ activeTab === "layers" && renderLayerCards(),
3488
+ !hasPrefilters && activeTab === "filters" && /* @__PURE__ */ jsx5("div", { className: "zlm-filter-panel", style: { display: "flex", flexDirection: "column", gap: 12, background: "#fff", border: "1px solid #e2e8f0", borderRadius: 12, padding: 12 }, children: !filterableLayers.length ? /* @__PURE__ */ jsx5("div", { style: { color: "#64748b", fontSize: 13 }, children: "No hay filtros disponibles para las capas de este mapa." }) : /* @__PURE__ */ jsxs5(Fragment3, { children: [
3489
+ filterableLayers.length > 1 && /* @__PURE__ */ jsxs5("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: 12, color: "#475569" }, children: [
3490
+ "Capa",
3491
+ /* @__PURE__ */ jsx5(
3492
+ ZenitSelect,
3493
+ {
3494
+ ariaLabel: "Seleccionar capa para filtrar",
3495
+ value: selectedFilterLayerId,
3496
+ onValueChange: setSelectedFilterLayerId,
3497
+ options: filterableLayers.map((layer) => ({
3498
+ value: String(layer.mapLayer.layerId),
3499
+ label: layer.layerName ?? `Capa ${layer.mapLayer.layerId}`
3500
+ }))
3501
+ }
3502
+ )
3503
+ ] }),
3504
+ /* @__PURE__ */ jsxs5("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: 12, color: "#475569" }, children: [
3505
+ "Campo",
3506
+ /* @__PURE__ */ jsx5(
3507
+ ZenitSelect,
3508
+ {
3509
+ ariaLabel: "Seleccionar campo de filtrado",
3510
+ value: selectedFilterField,
3511
+ onValueChange: (nextField) => {
3512
+ setSelectedFilterField(nextField);
3513
+ setSelectedFilterValue("");
3514
+ },
3515
+ options: filterFields.map((field) => ({ value: field, label: field }))
3516
+ }
3517
+ )
3518
+ ] }),
3519
+ /* @__PURE__ */ jsxs5("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: 12, color: "#475569" }, children: [
3520
+ "Valor",
3521
+ /* @__PURE__ */ jsx5(
3522
+ ZenitSelect,
3523
+ {
3524
+ ariaLabel: "Seleccionar valor de filtrado",
3525
+ value: selectedFilterValue,
3526
+ placeholder: "Seleccionar\u2026",
3527
+ onValueChange: (nextValue) => setSelectedFilterValue(nextValue),
3528
+ disabled: loadingCatalog || activeCatalogValues.length === 0,
3529
+ options: activeCatalogValues.map((catalogValue) => ({
3530
+ value: catalogValue,
3531
+ label: catalogValue
3532
+ }))
3533
+ }
3534
+ )
3535
+ ] }),
3536
+ loadingCatalog && /* @__PURE__ */ jsx5("div", { style: { color: "#64748b", fontSize: 12 }, children: "Cargando cat\xE1logo\u2026" }),
3537
+ !loadingCatalog && selectedFilterField && activeCatalogValues.length === 0 && /* @__PURE__ */ jsx5("div", { style: { color: "#64748b", fontSize: 12 }, children: "No hay cat\xE1logo disponible para este filtro." }),
3538
+ filterError && /* @__PURE__ */ jsx5("div", { style: { color: "#b91c1c", fontSize: 12 }, children: filterError }),
3539
+ appliedFilter && /* @__PURE__ */ jsxs5("div", { className: "zlm-badge", style: { alignSelf: "flex-start" }, children: [
3540
+ "Activo: ",
3541
+ appliedFilter.field,
3542
+ " = ",
3543
+ appliedFilter.value
3544
+ ] }),
3545
+ /* @__PURE__ */ jsxs5("div", { className: "zlm-filter-actions", style: { display: "flex", gap: 8, flexWrap: "wrap" }, children: [
3546
+ /* @__PURE__ */ jsx5(
3547
+ "button",
3548
+ {
3549
+ type: "button",
3550
+ className: "zlm-panel-toggle",
3551
+ disabled: !selectedFilterValue || applyingFilter || !onApplyLayerFilter,
3552
+ onClick: handleApplyFilter,
3553
+ children: applyingFilter ? "Aplicando\u2026" : "Aplicar"
3554
+ }
3555
+ ),
3556
+ /* @__PURE__ */ jsx5(
3557
+ "button",
3558
+ {
3559
+ type: "button",
3560
+ className: "zlm-panel-toggle",
3561
+ disabled: applyingFilter,
3562
+ onClick: handleClearFilter,
3563
+ children: "Limpiar"
3564
+ }
3565
+ )
3566
+ ] })
3567
+ ] }) }),
3568
+ showUploadTab && activeTab === "upload" && /* @__PURE__ */ jsx5("div", { style: { color: "#475569", fontSize: 13 }, children: "Pr\xF3ximamente podr\xE1s subir capas desde este panel." })
3569
+ ] })
3570
+ ] });
3571
+ };
3572
+
3573
+ // src/react/ZenitFeatureFilterPanel.tsx
3574
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3575
+ var ZenitFeatureFilterPanel = ({
3576
+ title = "Filtros",
3577
+ description,
3578
+ className,
3579
+ style,
3580
+ children
3581
+ }) => {
3582
+ return /* @__PURE__ */ jsxs6(
3583
+ "section",
3584
+ {
3585
+ className,
3586
+ style: {
3587
+ border: "1px solid #e2e8f0",
3588
+ borderRadius: 12,
3589
+ padding: 16,
3590
+ background: "#fff",
3591
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
3592
+ ...style
3593
+ },
3594
+ children: [
3595
+ /* @__PURE__ */ jsxs6("header", { style: { marginBottom: 12 }, children: [
3596
+ /* @__PURE__ */ jsx6("h3", { style: { margin: 0, fontSize: 16 }, children: title }),
3597
+ description && /* @__PURE__ */ jsx6("p", { style: { margin: "6px 0 0", color: "#475569", fontSize: 13 }, children: description })
3598
+ ] }),
3599
+ /* @__PURE__ */ jsx6("div", { children })
3600
+ ]
3601
+ }
3602
+ );
3603
+ };
3604
+
3605
+ // src/react/ai/FloatingChatBox.tsx
3606
+ import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef8, useState as useState5 } from "react";
3607
+ import { createPortal as createPortal3 } from "react-dom";
3608
+
3609
+ // src/react/hooks/use-chat.ts
3610
+ import { useCallback as useCallback3, useRef as useRef7, useState as useState4 } from "react";
3611
+
3612
+ // src/ai/chat.service.ts
3613
+ var DEFAULT_ERROR_MESSAGE = "No fue posible completar la solicitud al asistente.";
3614
+ var normalizeBaseUrl = (baseUrl) => baseUrl.replace(/\/$/, "");
3615
+ var resolveBaseUrl = (config, options) => {
3616
+ const baseUrl = options?.baseUrl ?? config?.baseUrl;
3617
+ if (!baseUrl) {
3618
+ throw new Error("baseUrl es requerido para usar el chat de Zenit AI.");
3619
+ }
3620
+ return normalizeBaseUrl(baseUrl);
3621
+ };
3622
+ var resolveAccessToken = async (config, options) => {
3623
+ if (options?.accessToken) return options.accessToken;
3624
+ if (config?.accessToken) return config.accessToken;
3625
+ if (options?.getAccessToken) return await options.getAccessToken();
3626
+ if (config?.getAccessToken) return await config.getAccessToken();
3627
+ return void 0;
3628
+ };
3629
+ var buildAuthHeaders = (token) => {
3630
+ if (!token) return {};
3631
+ return { Authorization: `Bearer ${token}` };
3632
+ };
3633
+ var parseError = async (response) => {
3634
+ try {
3635
+ const contentType = response.headers.get("content-type") ?? "";
3636
+ if (contentType.includes("application/json")) {
3637
+ const payload = await response.json();
3638
+ const message = payload?.message ?? DEFAULT_ERROR_MESSAGE;
3639
+ return new Error(message);
3640
+ }
3641
+ const text = await response.text();
3642
+ return new Error(text || DEFAULT_ERROR_MESSAGE);
3643
+ } catch (error) {
3644
+ return error instanceof Error ? error : new Error(DEFAULT_ERROR_MESSAGE);
3645
+ }
3646
+ };
3647
+ var sendMessage = async (mapId, request, options, config) => {
3648
+ const baseUrl = resolveBaseUrl(config, options);
3649
+ const token = await resolveAccessToken(config, options);
3650
+ const response = await fetch(`${baseUrl}/ai/maps/chat/${mapId}`, {
3651
+ method: "POST",
3652
+ headers: {
3653
+ "Content-Type": "application/json",
3654
+ ...buildAuthHeaders(token)
3655
+ },
3656
+ body: JSON.stringify(request),
3657
+ signal: options?.signal
3658
+ });
3659
+ if (!response.ok) {
3660
+ throw await parseError(response);
3661
+ }
3662
+ return await response.json();
3663
+ };
3664
+ var mergeResponse = (previous, chunk, aggregatedAnswer) => ({
3665
+ ...previous ?? chunk,
3666
+ ...chunk,
3667
+ answer: aggregatedAnswer
3668
+ });
3669
+ var resolveAggregatedAnswer = (current, incoming) => {
3670
+ if (!incoming) return current;
3671
+ if (incoming.startsWith(current)) return incoming;
3672
+ return current + incoming;
3673
+ };
3674
+ var sendMessageStream = async (mapId, request, callbacks = {}, options, config) => {
3675
+ const baseUrl = resolveBaseUrl(config, options);
3676
+ const token = await resolveAccessToken(config, options);
3677
+ const response = await fetch(`${baseUrl}/ai/maps/chat/${mapId}/stream`, {
3678
+ method: "POST",
3679
+ headers: {
3680
+ Accept: "text/event-stream",
3681
+ "Content-Type": "application/json",
3682
+ ...buildAuthHeaders(token)
3683
+ },
3684
+ body: JSON.stringify(request),
3685
+ signal: options?.signal
3686
+ });
3687
+ if (!response.ok) {
3688
+ const error = await parseError(response);
3689
+ callbacks.onError?.(error);
3690
+ throw error;
3691
+ }
3692
+ const reader = response.body?.getReader();
3693
+ if (!reader) {
3694
+ const error = new Error("No se pudo iniciar el streaming de Zenit AI.");
3695
+ callbacks.onError?.(error);
3696
+ throw error;
3697
+ }
3698
+ const decoder = new TextDecoder("utf-8");
3699
+ let buffer = "";
3700
+ let aggregated = "";
3701
+ let latestResponse = null;
3702
+ try {
3703
+ while (true) {
3704
+ const { value, done } = await reader.read();
3705
+ if (done) break;
3706
+ buffer += decoder.decode(value, { stream: true });
3707
+ const lines = buffer.split(/\r?\n/);
3708
+ buffer = lines.pop() ?? "";
3709
+ for (const line of lines) {
3710
+ const trimmed = line.trim();
3711
+ if (!trimmed || trimmed.startsWith("event:")) continue;
3712
+ if (!trimmed.startsWith("data:")) continue;
3713
+ const payload = trimmed.replace(/^data:\s*/, "");
3714
+ if (!payload || payload === "[DONE]") continue;
3715
+ try {
3716
+ const parsed = JSON.parse(payload);
3717
+ aggregated = resolveAggregatedAnswer(aggregated, parsed.answer ?? "");
3718
+ latestResponse = mergeResponse(latestResponse, parsed, aggregated);
3719
+ if (latestResponse) {
3720
+ callbacks.onChunk?.(latestResponse, aggregated);
3721
+ }
3722
+ } catch (error) {
3723
+ callbacks.onError?.(error);
3724
+ }
3725
+ }
3726
+ }
3727
+ } catch (error) {
3728
+ callbacks.onError?.(error);
3729
+ throw error;
3730
+ }
3731
+ if (!latestResponse) {
3732
+ const error = new Error(DEFAULT_ERROR_MESSAGE);
3733
+ callbacks.onError?.(error);
3734
+ throw error;
3735
+ }
3736
+ callbacks.onComplete?.(latestResponse);
3737
+ return latestResponse;
3738
+ };
3739
+ var createChatService = (config) => ({
3740
+ sendMessage: (mapId, request, options) => sendMessage(mapId, request, options, config),
3741
+ sendMessageStream: (mapId, request, callbacks, options) => sendMessageStream(mapId, request, callbacks, options, config)
3742
+ });
3743
+
3744
+ // src/react/hooks/use-chat.ts
3745
+ var useSendMessage = (config) => {
3746
+ const [isLoading, setIsLoading] = useState4(false);
3747
+ const [error, setError] = useState4(null);
3748
+ const send = useCallback3(
3749
+ async (mapId, request, options) => {
3750
+ setIsLoading(true);
3751
+ setError(null);
3752
+ try {
3753
+ return await sendMessage(mapId, request, options, config);
3754
+ } catch (err) {
3755
+ setError(err);
3756
+ throw err;
3757
+ } finally {
3758
+ setIsLoading(false);
3759
+ }
3760
+ },
3761
+ [config]
3762
+ );
3763
+ return { sendMessage: send, isLoading, error };
3764
+ };
3765
+ var useSendMessageStream = (config) => {
3766
+ const [isStreaming, setIsStreaming] = useState4(false);
3767
+ const [streamingText, setStreamingText] = useState4("");
3768
+ const [completeResponse, setCompleteResponse] = useState4(null);
3769
+ const [error, setError] = useState4(null);
3770
+ const requestIdRef = useRef7(0);
3771
+ const reset = useCallback3(() => {
3772
+ setIsStreaming(false);
3773
+ setStreamingText("");
3774
+ setCompleteResponse(null);
3775
+ setError(null);
3776
+ }, []);
3777
+ const send = useCallback3(
3778
+ async (mapId, request, options) => {
3779
+ const requestId = requestIdRef.current + 1;
3780
+ requestIdRef.current = requestId;
3781
+ setIsStreaming(true);
3782
+ setStreamingText("");
3783
+ setCompleteResponse(null);
3784
+ setError(null);
3785
+ try {
3786
+ const response = await sendMessageStream(
3787
+ mapId,
3788
+ request,
3789
+ {
3790
+ onChunk: (_chunk, aggregated) => {
3791
+ if (requestIdRef.current !== requestId) return;
3792
+ setStreamingText(aggregated);
3793
+ },
3794
+ onError: (err) => {
3795
+ if (requestIdRef.current !== requestId) return;
3796
+ setError(err);
3797
+ },
3798
+ onComplete: (finalResponse) => {
3799
+ if (requestIdRef.current !== requestId) return;
3800
+ setCompleteResponse(finalResponse);
3801
+ }
3802
+ },
3803
+ options,
3804
+ config
3805
+ );
3806
+ return response;
3807
+ } catch (err) {
3808
+ setError(err);
3809
+ throw err;
3810
+ } finally {
3811
+ if (requestIdRef.current === requestId) {
3812
+ setIsStreaming(false);
3813
+ }
3814
+ }
3815
+ },
3816
+ [config]
3817
+ );
3818
+ return {
3819
+ sendMessage: send,
3820
+ isStreaming,
3821
+ streamingText,
3822
+ completeResponse,
3823
+ error,
3824
+ reset
3825
+ };
3826
+ };
3827
+
3828
+ // src/react/components/MarkdownRenderer.tsx
3829
+ import ReactMarkdown from "react-markdown";
3830
+ import remarkGfm from "remark-gfm";
3831
+ import { jsx as jsx7 } from "react/jsx-runtime";
3832
+ function normalizeAssistantMarkdown(text) {
3833
+ if (!text || typeof text !== "string") return "";
3834
+ let normalized = text;
3835
+ normalized = normalized.replace(/\r\n/g, "\n");
3836
+ normalized = normalized.replace(/^\s*#{1,6}\s*$/gm, "");
3837
+ normalized = normalized.replace(/\n{3,}/g, "\n\n");
3838
+ normalized = normalized.split("\n").map((line) => line.trimEnd()).join("\n");
3839
+ normalized = normalized.trim();
3840
+ return normalized;
3841
+ }
3842
+ var MarkdownRenderer = ({ content, className }) => {
3843
+ const normalizedContent = normalizeAssistantMarkdown(content);
3844
+ if (!normalizedContent) {
3845
+ return null;
3846
+ }
3847
+ return /* @__PURE__ */ jsx7("div", { className, style: { wordBreak: "break-word" }, children: /* @__PURE__ */ jsx7(
3848
+ ReactMarkdown,
3849
+ {
3850
+ remarkPlugins: [remarkGfm],
3851
+ components: {
3852
+ // Headings with proper spacing
3853
+ h1: ({ children, ...props }) => /* @__PURE__ */ jsx7("h1", { style: { fontSize: "1.5em", fontWeight: 700, marginTop: "1em", marginBottom: "0.5em" }, ...props, children }),
3854
+ h2: ({ children, ...props }) => /* @__PURE__ */ jsx7("h2", { style: { fontSize: "1.3em", fontWeight: 700, marginTop: "0.9em", marginBottom: "0.45em" }, ...props, children }),
3855
+ h3: ({ children, ...props }) => /* @__PURE__ */ jsx7("h3", { style: { fontSize: "1.15em", fontWeight: 600, marginTop: "0.75em", marginBottom: "0.4em" }, ...props, children }),
3856
+ h4: ({ children, ...props }) => /* @__PURE__ */ jsx7("h4", { style: { fontSize: "1.05em", fontWeight: 600, marginTop: "0.6em", marginBottom: "0.35em" }, ...props, children }),
3857
+ h5: ({ children, ...props }) => /* @__PURE__ */ jsx7("h5", { style: { fontSize: "1em", fontWeight: 600, marginTop: "0.5em", marginBottom: "0.3em" }, ...props, children }),
3858
+ h6: ({ children, ...props }) => /* @__PURE__ */ jsx7("h6", { style: { fontSize: "0.95em", fontWeight: 600, marginTop: "0.5em", marginBottom: "0.3em" }, ...props, children }),
3859
+ // Paragraphs with comfortable line height
3860
+ p: ({ children, ...props }) => /* @__PURE__ */ jsx7("p", { style: { marginTop: "0.5em", marginBottom: "0.5em", lineHeight: 1.6 }, ...props, children }),
3861
+ // Lists with proper indentation
3862
+ ul: ({ children, ...props }) => /* @__PURE__ */ jsx7("ul", { style: { paddingLeft: "1.5em", marginTop: "0.5em", marginBottom: "0.5em" }, ...props, children }),
3863
+ ol: ({ children, ...props }) => /* @__PURE__ */ jsx7("ol", { style: { paddingLeft: "1.5em", marginTop: "0.5em", marginBottom: "0.5em" }, ...props, children }),
3864
+ li: ({ children, ...props }) => /* @__PURE__ */ jsx7("li", { style: { marginTop: "0.25em", marginBottom: "0.25em" }, ...props, children }),
3865
+ // Code blocks
3866
+ code: ({ inline, children, ...props }) => {
3867
+ if (inline) {
3868
+ return /* @__PURE__ */ jsx7(
3869
+ "code",
3870
+ {
3871
+ style: {
3872
+ backgroundColor: "rgba(0, 0, 0, 0.08)",
3873
+ padding: "0.15em 0.4em",
3874
+ borderRadius: "4px",
3875
+ fontSize: "0.9em",
3876
+ fontFamily: "monospace"
3877
+ },
3878
+ ...props,
3879
+ children
3880
+ }
3881
+ );
3882
+ }
3883
+ return /* @__PURE__ */ jsx7(
3884
+ "code",
3885
+ {
3886
+ style: {
3887
+ display: "block",
3888
+ backgroundColor: "rgba(0, 0, 0, 0.08)",
3889
+ padding: "0.75em",
3890
+ borderRadius: "6px",
3891
+ fontSize: "0.9em",
3892
+ fontFamily: "monospace",
3893
+ overflowX: "auto",
3894
+ marginTop: "0.5em",
3895
+ marginBottom: "0.5em"
3896
+ },
3897
+ ...props,
3898
+ children
3899
+ }
3900
+ );
3901
+ },
3902
+ // Pre (code block wrapper)
3903
+ pre: ({ children, ...props }) => /* @__PURE__ */ jsx7("pre", { style: { margin: 0 }, ...props, children }),
3904
+ // Blockquotes
3905
+ blockquote: ({ children, ...props }) => /* @__PURE__ */ jsx7(
3906
+ "blockquote",
3907
+ {
3908
+ style: {
3909
+ borderLeft: "4px solid rgba(0, 0, 0, 0.2)",
3910
+ paddingLeft: "1em",
3911
+ marginLeft: 0,
3912
+ marginTop: "0.5em",
3913
+ marginBottom: "0.5em",
3914
+ color: "rgba(0, 0, 0, 0.7)"
3915
+ },
3916
+ ...props,
3917
+ children
3918
+ }
3919
+ ),
3920
+ // Strong/bold
3921
+ strong: ({ children, ...props }) => /* @__PURE__ */ jsx7("strong", { style: { fontWeight: 600 }, ...props, children }),
3922
+ // Emphasis/italic
3923
+ em: ({ children, ...props }) => /* @__PURE__ */ jsx7("em", { style: { fontStyle: "italic" }, ...props, children }),
3924
+ // Horizontal rule
3925
+ hr: (props) => /* @__PURE__ */ jsx7(
3926
+ "hr",
3927
+ {
3928
+ style: {
3929
+ border: "none",
3930
+ borderTop: "1px solid rgba(0, 0, 0, 0.1)",
3931
+ marginTop: "1em",
3932
+ marginBottom: "1em"
3933
+ },
3934
+ ...props
3935
+ }
3936
+ ),
3937
+ // Tables (GFM)
3938
+ table: ({ children, ...props }) => /* @__PURE__ */ jsx7("div", { style: { overflowX: "auto", marginTop: "0.5em", marginBottom: "0.5em" }, children: /* @__PURE__ */ jsx7(
3939
+ "table",
3940
+ {
3941
+ style: {
3942
+ borderCollapse: "collapse",
3943
+ width: "100%",
3944
+ fontSize: "0.9em"
3945
+ },
3946
+ ...props,
3947
+ children
3948
+ }
3949
+ ) }),
3950
+ th: ({ children, ...props }) => /* @__PURE__ */ jsx7(
3951
+ "th",
3952
+ {
3953
+ style: {
3954
+ border: "1px solid rgba(0, 0, 0, 0.2)",
3955
+ padding: "0.5em",
3956
+ backgroundColor: "rgba(0, 0, 0, 0.05)",
3957
+ fontWeight: 600,
3958
+ textAlign: "left"
3959
+ },
3960
+ ...props,
3961
+ children
3962
+ }
3963
+ ),
3964
+ td: ({ children, ...props }) => /* @__PURE__ */ jsx7(
3965
+ "td",
3966
+ {
3967
+ style: {
3968
+ border: "1px solid rgba(0, 0, 0, 0.2)",
3969
+ padding: "0.5em"
3970
+ },
3971
+ ...props,
3972
+ children
3973
+ }
3974
+ )
3975
+ },
3976
+ children: normalizedContent
3977
+ }
3978
+ ) });
3979
+ };
3980
+
3981
+ // src/react/ai/FloatingChatBox.tsx
3982
+ import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3983
+ var ChatIcon = () => /* @__PURE__ */ jsx8("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx8("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) });
3984
+ var CloseIcon = () => /* @__PURE__ */ jsxs7("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3985
+ /* @__PURE__ */ jsx8("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
3986
+ /* @__PURE__ */ jsx8("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
3987
+ ] });
3988
+ var ExpandIcon = () => /* @__PURE__ */ jsxs7("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3989
+ /* @__PURE__ */ jsx8("polyline", { points: "15 3 21 3 21 9" }),
3990
+ /* @__PURE__ */ jsx8("polyline", { points: "9 21 3 21 3 15" }),
3991
+ /* @__PURE__ */ jsx8("line", { x1: "21", y1: "3", x2: "14", y2: "10" }),
3992
+ /* @__PURE__ */ jsx8("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
3993
+ ] });
3994
+ var CollapseIcon = () => /* @__PURE__ */ jsxs7("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3995
+ /* @__PURE__ */ jsx8("polyline", { points: "4 14 10 14 10 20" }),
3996
+ /* @__PURE__ */ jsx8("polyline", { points: "20 10 14 10 14 4" }),
3997
+ /* @__PURE__ */ jsx8("line", { x1: "14", y1: "10", x2: "21", y2: "3" }),
3998
+ /* @__PURE__ */ jsx8("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
3999
+ ] });
4000
+ var SendIcon = () => /* @__PURE__ */ jsxs7("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
4001
+ /* @__PURE__ */ jsx8("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
4002
+ /* @__PURE__ */ jsx8("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
4003
+ ] });
4004
+ var LayersIcon = () => /* @__PURE__ */ jsxs7("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
4005
+ /* @__PURE__ */ jsx8("polygon", { points: "12 2 2 7 12 12 22 7 12 2" }),
4006
+ /* @__PURE__ */ jsx8("polyline", { points: "2 17 12 22 22 17" }),
4007
+ /* @__PURE__ */ jsx8("polyline", { points: "2 12 12 17 22 12" })
4008
+ ] });
4009
+ var styles = {
4010
+ root: {
4011
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
4012
+ },
4013
+ // Floating button (closed state - wide with text, open state - circular with X)
4014
+ floatingButton: {
4015
+ position: "fixed",
4016
+ bottom: 16,
4017
+ right: 16,
4018
+ borderRadius: "999px",
4019
+ border: "none",
4020
+ cursor: "pointer",
4021
+ background: "linear-gradient(135deg, #10b981, #059669)",
4022
+ color: "#fff",
4023
+ boxShadow: "0 12px 28px rgba(16, 185, 129, 0.4)",
4024
+ display: "flex",
4025
+ alignItems: "center",
4026
+ justifyContent: "center",
4027
+ fontSize: 14,
4028
+ fontWeight: 600,
4029
+ zIndex: 99999,
4030
+ transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
4031
+ },
4032
+ floatingButtonClosed: {
4033
+ padding: "12px 20px",
4034
+ gap: 8
4035
+ },
4036
+ floatingButtonOpen: {
4037
+ width: 52,
4038
+ height: 52,
4039
+ padding: 0
4040
+ },
4041
+ floatingButtonMobile: {
4042
+ width: 52,
4043
+ height: 52,
4044
+ padding: 0
4045
+ },
4046
+ // Panel (expandable)
4047
+ panel: {
4048
+ position: "fixed",
4049
+ bottom: 80,
4050
+ right: 16,
4051
+ background: "#fff",
4052
+ borderRadius: 16,
4053
+ boxShadow: "0 20px 60px rgba(15, 23, 42, 0.3), 0 0 0 1px rgba(15, 23, 42, 0.05)",
4054
+ display: "flex",
4055
+ flexDirection: "column",
4056
+ overflow: "hidden",
4057
+ zIndex: 99999,
4058
+ transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
4059
+ },
4060
+ panelNormal: {
4061
+ width: 360,
4062
+ height: 520
4063
+ },
4064
+ panelExpanded: {
4065
+ width: 480,
4066
+ height: 640
4067
+ },
4068
+ panelMobileFullscreen: {
4069
+ position: "fixed",
4070
+ top: 0,
4071
+ left: 0,
4072
+ right: 0,
4073
+ bottom: 0,
4074
+ width: "100%",
4075
+ height: "100%",
4076
+ borderRadius: 0,
4077
+ margin: 0
4078
+ },
4079
+ // Header with green gradient
4080
+ header: {
4081
+ padding: "14px 16px",
4082
+ background: "linear-gradient(135deg, #10b981, #059669)",
4083
+ color: "#fff",
4084
+ display: "flex",
4085
+ alignItems: "center",
4086
+ justifyContent: "space-between"
4087
+ },
4088
+ title: {
4089
+ margin: 0,
4090
+ fontSize: 15,
4091
+ fontWeight: 600,
4092
+ letterSpacing: "-0.01em"
4093
+ },
4094
+ headerButtons: {
4095
+ display: "flex",
4096
+ alignItems: "center",
4097
+ gap: 8
4098
+ },
4099
+ headerButton: {
4100
+ border: "none",
4101
+ background: "rgba(255, 255, 255, 0.15)",
4102
+ color: "#fff",
4103
+ width: 30,
4104
+ height: 30,
4105
+ borderRadius: 8,
4106
+ cursor: "pointer",
4107
+ display: "flex",
4108
+ alignItems: "center",
4109
+ justifyContent: "center",
4110
+ transition: "background 0.2s"
4111
+ },
4112
+ // Messages area
4113
+ messages: {
4114
+ flex: 1,
4115
+ padding: "16px",
4116
+ overflowY: "auto",
4117
+ background: "#f8fafc",
4118
+ display: "flex",
4119
+ flexDirection: "column",
4120
+ gap: 16
4121
+ },
4122
+ // Message bubbles
4123
+ messageWrapper: {
4124
+ display: "flex",
4125
+ flexDirection: "column",
4126
+ gap: 8
4127
+ },
4128
+ messageBubble: {
4129
+ maxWidth: "85%",
4130
+ padding: "10px 12px",
4131
+ borderRadius: 16,
4132
+ lineHeight: 1.5,
4133
+ fontSize: 13,
4134
+ whiteSpace: "pre-wrap",
4135
+ wordBreak: "break-word"
4136
+ },
4137
+ userMessage: {
4138
+ alignSelf: "flex-end",
4139
+ background: "linear-gradient(135deg, #10b981, #059669)",
4140
+ color: "#fff",
4141
+ borderBottomRightRadius: 4
4142
+ },
4143
+ assistantMessage: {
4144
+ alignSelf: "flex-start",
4145
+ background: "#e2e8f0",
4146
+ color: "#0f172a",
4147
+ borderBottomLeftRadius: 4
4148
+ },
4149
+ // Streaming cursor
4150
+ cursor: {
4151
+ display: "inline-block",
4152
+ width: 2,
4153
+ height: 16,
4154
+ background: "#0f172a",
4155
+ marginLeft: 2,
4156
+ animation: "zenitBlink 1s infinite",
4157
+ verticalAlign: "text-bottom"
4158
+ },
4159
+ thinkingText: {
4160
+ fontStyle: "italic",
4161
+ opacity: 0.7,
4162
+ display: "flex",
4163
+ alignItems: "center",
4164
+ gap: 8
4165
+ },
4166
+ typingIndicator: {
4167
+ display: "flex",
4168
+ gap: 4,
4169
+ alignItems: "center"
4170
+ },
4171
+ typingDot: {
4172
+ width: 8,
4173
+ height: 8,
4174
+ borderRadius: "50%",
4175
+ backgroundColor: "#059669",
4176
+ animation: "typingDotBounce 1.4s infinite ease-in-out"
4177
+ },
4178
+ // Metadata section (Referenced Layers)
4179
+ metadataSection: {
4180
+ marginTop: 12,
4181
+ padding: "10px 12px",
4182
+ background: "rgba(255, 255, 255, 0.5)",
4183
+ borderRadius: 10,
4184
+ fontSize: 12
4185
+ },
4186
+ metadataTitle: {
4187
+ fontWeight: 600,
4188
+ color: "#059669",
4189
+ marginBottom: 6,
4190
+ display: "flex",
4191
+ alignItems: "center",
4192
+ gap: 6
4193
+ },
4194
+ metadataList: {
4195
+ margin: 0,
4196
+ paddingLeft: 18,
4197
+ color: "#475569"
4198
+ },
4199
+ metadataItem: {
4200
+ marginBottom: 2
4201
+ },
4202
+ // Suggested actions
4203
+ actionsSection: {
4204
+ marginTop: 12
4205
+ },
4206
+ sectionLabel: {
4207
+ fontSize: 11,
4208
+ fontWeight: 600,
4209
+ color: "#64748b",
4210
+ marginBottom: 8,
4211
+ textTransform: "uppercase",
4212
+ letterSpacing: "0.05em"
4213
+ },
4214
+ actionsGrid: {
4215
+ display: "flex",
4216
+ flexWrap: "wrap",
4217
+ gap: 6
4218
+ },
4219
+ actionButton: {
4220
+ border: "1px solid #d1fae5",
4221
+ background: "#d1fae5",
4222
+ color: "#065f46",
4223
+ borderRadius: 999,
4224
+ padding: "6px 12px",
4225
+ fontSize: 12,
4226
+ fontWeight: 500,
4227
+ cursor: "pointer",
4228
+ transition: "all 0.2s"
4229
+ },
4230
+ // Follow-up questions
4231
+ followUpButton: {
4232
+ border: "1px solid #cbd5e1",
4233
+ background: "#fff",
4234
+ color: "#475569",
4235
+ borderRadius: 10,
4236
+ padding: "8px 12px",
4237
+ fontSize: 12,
4238
+ fontWeight: 500,
4239
+ cursor: "pointer",
4240
+ textAlign: "left",
4241
+ width: "100%",
4242
+ transition: "all 0.2s"
4243
+ },
4244
+ // Input area
4245
+ inputWrapper: {
4246
+ borderTop: "1px solid #e2e8f0",
4247
+ padding: "10px 14px",
4248
+ display: "flex",
4249
+ gap: 10,
4250
+ alignItems: "flex-end",
4251
+ background: "#fff"
4252
+ },
4253
+ textarea: {
4254
+ flex: 1,
4255
+ resize: "none",
4256
+ borderRadius: 12,
4257
+ border: "1.5px solid #cbd5e1",
4258
+ padding: "8px 10px",
4259
+ fontSize: 13,
4260
+ fontFamily: "inherit",
4261
+ lineHeight: 1.4,
4262
+ transition: "border-color 0.2s"
4263
+ },
4264
+ textareaFocus: {
4265
+ borderColor: "#10b981",
4266
+ outline: "none"
4267
+ },
4268
+ sendButton: {
4269
+ borderRadius: 12,
4270
+ border: "none",
4271
+ padding: "8px 12px",
4272
+ background: "linear-gradient(135deg, #10b981, #059669)",
4273
+ color: "#fff",
4274
+ cursor: "pointer",
4275
+ fontSize: 13,
4276
+ fontWeight: 600,
4277
+ display: "flex",
4278
+ alignItems: "center",
4279
+ justifyContent: "center",
4280
+ transition: "opacity 0.2s, transform 0.2s",
4281
+ minWidth: 40,
4282
+ height: 40
4283
+ },
4284
+ // Status messages
4285
+ statusNote: {
4286
+ padding: "0 16px 14px",
4287
+ fontSize: 12,
4288
+ color: "#64748b",
4289
+ textAlign: "center"
4290
+ },
4291
+ errorText: {
4292
+ padding: "0 16px 14px",
4293
+ fontSize: 12,
4294
+ color: "#dc2626",
4295
+ textAlign: "center"
4296
+ }
4297
+ };
4298
+ var FloatingChatBox = ({
4299
+ mapId,
4300
+ filteredLayerIds,
4301
+ filters,
4302
+ userId,
4303
+ baseUrl,
4304
+ accessToken,
4305
+ getAccessToken,
4306
+ onActionClick,
4307
+ onOpenChange,
4308
+ hideButton,
4309
+ open: openProp
4310
+ }) => {
4311
+ const isControlled = openProp !== void 0;
4312
+ const [internalOpen, setInternalOpen] = useState5(false);
4313
+ const open = isControlled ? openProp : internalOpen;
4314
+ const setOpen = useCallback4((value) => {
4315
+ const newValue = typeof value === "function" ? value(open) : value;
4316
+ if (!isControlled) {
4317
+ setInternalOpen(newValue);
4318
+ }
4319
+ onOpenChange?.(newValue);
4320
+ }, [isControlled, open, onOpenChange]);
4321
+ const [expanded, setExpanded] = useState5(false);
4322
+ const [messages, setMessages] = useState5([]);
4323
+ const [inputValue, setInputValue] = useState5("");
4324
+ const [conversationId, setConversationId] = useState5();
4325
+ const [errorMessage, setErrorMessage] = useState5(null);
4326
+ const [isFocused, setIsFocused] = useState5(false);
4327
+ const [isMobile, setIsMobile] = useState5(false);
4328
+ const messagesEndRef = useRef8(null);
4329
+ const messagesContainerRef = useRef8(null);
4330
+ const chatBoxRef = useRef8(null);
4331
+ const chatConfig = useMemo5(() => {
4332
+ if (!baseUrl) return void 0;
4333
+ return { baseUrl, accessToken, getAccessToken };
4334
+ }, [accessToken, baseUrl, getAccessToken]);
4335
+ const { sendMessage: sendMessage2, isStreaming, streamingText, completeResponse } = useSendMessageStream(chatConfig);
4336
+ const canSend = Boolean(mapId) && Boolean(baseUrl) && inputValue.trim().length > 0 && !isStreaming;
4337
+ useEffect7(() => {
4338
+ if (open && isMobile) {
4339
+ setExpanded(true);
4340
+ }
4341
+ }, [open, isMobile]);
4342
+ const scrollToBottom = useCallback4(() => {
4343
+ if (messagesEndRef.current) {
4344
+ messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
4345
+ }
4346
+ }, []);
4347
+ useEffect7(() => {
4348
+ if (open && messages.length === 0) {
4349
+ setMessages([
4350
+ {
4351
+ id: "welcome",
4352
+ role: "assistant",
4353
+ 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?"
4354
+ }
4355
+ ]);
4356
+ }
4357
+ }, [open, messages.length]);
4358
+ useEffect7(() => {
4359
+ scrollToBottom();
4360
+ }, [messages, streamingText, scrollToBottom]);
4361
+ useEffect7(() => {
4362
+ if (!open) return;
4363
+ if (isMobile && expanded) return;
4364
+ const handleClickOutside = (event) => {
4365
+ if (chatBoxRef.current && !chatBoxRef.current.contains(event.target)) {
4366
+ setOpen(false);
4367
+ }
4368
+ };
4369
+ document.addEventListener("mousedown", handleClickOutside);
4370
+ return () => {
4371
+ document.removeEventListener("mousedown", handleClickOutside);
4372
+ };
4373
+ }, [open, isMobile, expanded]);
4374
+ useEffect7(() => {
4375
+ if (typeof window === "undefined") return;
4376
+ const mediaQuery = window.matchMedia("(max-width: 768px)");
4377
+ const updateMobile = () => setIsMobile(mediaQuery.matches);
4378
+ updateMobile();
4379
+ if (mediaQuery.addEventListener) {
4380
+ mediaQuery.addEventListener("change", updateMobile);
4381
+ } else {
4382
+ mediaQuery.addListener(updateMobile);
4383
+ }
4384
+ return () => {
4385
+ if (mediaQuery.removeEventListener) {
4386
+ mediaQuery.removeEventListener("change", updateMobile);
4387
+ } else {
4388
+ mediaQuery.removeListener(updateMobile);
4389
+ }
4390
+ };
4391
+ }, []);
4392
+ useEffect7(() => {
4393
+ if (typeof document === "undefined") return;
4394
+ if (!open || !isMobile) return;
4395
+ document.body.style.overflow = "hidden";
4396
+ return () => {
4397
+ document.body.style.overflow = "";
4398
+ };
4399
+ }, [open, isMobile]);
4400
+ const addMessage = useCallback4((message) => {
4401
+ setMessages((prev) => [...prev, message]);
4402
+ }, []);
4403
+ const handleSend = useCallback4(async () => {
4404
+ if (!mapId) {
4405
+ setErrorMessage("Selecciona un mapa para usar el asistente.");
4406
+ return;
4407
+ }
4408
+ if (!baseUrl) {
4409
+ setErrorMessage("Configura la baseUrl del SDK para usar Zenit AI.");
4410
+ return;
4411
+ }
4412
+ if (!inputValue.trim()) return;
4413
+ const messageText = inputValue.trim();
4414
+ setInputValue("");
4415
+ setErrorMessage(null);
4416
+ addMessage({
4417
+ id: `user-${Date.now()}`,
4418
+ role: "user",
4419
+ content: messageText
4420
+ });
4421
+ const request = {
4422
+ message: messageText,
4423
+ conversationId,
4424
+ filteredLayerIds,
4425
+ filters,
4426
+ userId
4427
+ };
4428
+ try {
4429
+ const response = await sendMessage2(mapId, request);
4430
+ setConversationId(response.conversationId ?? conversationId);
4431
+ addMessage({
4432
+ id: `assistant-${Date.now()}`,
4433
+ role: "assistant",
4434
+ content: response.answer,
4435
+ response
4436
+ });
4437
+ } catch (error) {
4438
+ setErrorMessage("Ocurri\xF3 un error al generar la respuesta.");
4439
+ addMessage({
4440
+ id: `error-${Date.now()}`,
4441
+ role: "assistant",
4442
+ content: "\u274C Ocurri\xF3 un error al generar la respuesta."
4443
+ });
4444
+ }
4445
+ }, [
4446
+ addMessage,
4447
+ baseUrl,
4448
+ conversationId,
4449
+ filteredLayerIds,
4450
+ filters,
4451
+ inputValue,
4452
+ mapId,
4453
+ sendMessage2,
4454
+ userId
4455
+ ]);
4456
+ const handleKeyDown = useCallback4(
4457
+ (event) => {
4458
+ if (event.key === "Enter" && !event.shiftKey) {
4459
+ event.preventDefault();
4460
+ if (canSend) {
4461
+ void handleSend();
4462
+ }
4463
+ }
4464
+ },
4465
+ [canSend, handleSend]
4466
+ );
4467
+ const handleFollowUpClick = useCallback4((question) => {
4468
+ setInputValue(question);
4469
+ }, []);
4470
+ const renderMetadata = (response) => {
4471
+ if (!response?.metadata) return null;
4472
+ const referencedLayers = response.metadata.referencedLayers;
4473
+ if (!referencedLayers || referencedLayers.length === 0) return null;
4474
+ return /* @__PURE__ */ jsxs7("div", { style: styles.metadataSection, children: [
4475
+ /* @__PURE__ */ jsxs7("div", { style: styles.metadataTitle, children: [
4476
+ /* @__PURE__ */ jsx8(LayersIcon, {}),
4477
+ "Capas Analizadas"
4478
+ ] }),
4479
+ /* @__PURE__ */ jsx8("ul", { style: styles.metadataList, children: referencedLayers.map((layer, index) => /* @__PURE__ */ jsxs7("li", { style: styles.metadataItem, children: [
4480
+ /* @__PURE__ */ jsx8("strong", { children: layer.layerName }),
4481
+ " (",
4482
+ layer.featureCount,
4483
+ " ",
4484
+ layer.featureCount === 1 ? "elemento" : "elementos",
4485
+ ")"
4486
+ ] }, index)) })
4487
+ ] });
4488
+ };
4489
+ const handleActionClick = useCallback4((action) => {
4490
+ if (isStreaming) return;
4491
+ setOpen(false);
4492
+ requestAnimationFrame(() => {
4493
+ onActionClick?.(action);
4494
+ });
4495
+ }, [isStreaming, setOpen, onActionClick]);
4496
+ const renderActions = (response) => {
4497
+ if (!response?.suggestedActions?.length) return null;
4498
+ return /* @__PURE__ */ jsxs7("div", { style: styles.actionsSection, children: [
4499
+ /* @__PURE__ */ jsx8("div", { style: styles.sectionLabel, children: "Acciones Sugeridas" }),
4500
+ /* @__PURE__ */ jsx8("div", { style: styles.actionsGrid, children: response.suggestedActions.map((action, index) => /* @__PURE__ */ jsx8(
4501
+ "button",
4502
+ {
4503
+ type: "button",
4504
+ style: {
4505
+ ...styles.actionButton,
4506
+ opacity: isStreaming ? 0.5 : 1,
4507
+ cursor: isStreaming ? "not-allowed" : "pointer"
4508
+ },
4509
+ onClick: () => handleActionClick(action),
4510
+ disabled: isStreaming,
4511
+ onMouseEnter: (e) => {
4512
+ if (!isStreaming) {
4513
+ e.currentTarget.style.background = "#a7f3d0";
4514
+ e.currentTarget.style.borderColor = "#a7f3d0";
4515
+ }
4516
+ },
4517
+ onMouseLeave: (e) => {
4518
+ if (!isStreaming) {
4519
+ e.currentTarget.style.background = "#d1fae5";
4520
+ e.currentTarget.style.borderColor = "#d1fae5";
4521
+ }
4522
+ },
4523
+ children: action.label ?? action.action ?? "Acci\xF3n"
4524
+ },
4525
+ `${action.label ?? action.action ?? "action"}-${index}`
4526
+ )) })
4527
+ ] });
4528
+ };
4529
+ const renderFollowUps = (response) => {
4530
+ if (!response?.followUpQuestions?.length) return null;
4531
+ return /* @__PURE__ */ jsxs7("div", { style: styles.actionsSection, children: [
4532
+ /* @__PURE__ */ jsx8("div", { style: styles.sectionLabel, children: "Preguntas Relacionadas" }),
4533
+ /* @__PURE__ */ jsx8("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: response.followUpQuestions.map((question, index) => /* @__PURE__ */ jsx8(
4534
+ "button",
4535
+ {
4536
+ type: "button",
4537
+ style: {
4538
+ ...styles.followUpButton,
4539
+ opacity: isStreaming ? 0.5 : 1,
4540
+ cursor: isStreaming ? "not-allowed" : "pointer"
4541
+ },
4542
+ onClick: () => !isStreaming && handleFollowUpClick(question),
4543
+ disabled: isStreaming,
4544
+ onMouseEnter: (e) => {
4545
+ if (!isStreaming) {
4546
+ e.currentTarget.style.background = "#f8fafc";
4547
+ e.currentTarget.style.borderColor = "#10b981";
4548
+ }
4549
+ },
4550
+ onMouseLeave: (e) => {
4551
+ if (!isStreaming) {
4552
+ e.currentTarget.style.background = "#fff";
4553
+ e.currentTarget.style.borderColor = "#cbd5e1";
4554
+ }
4555
+ },
4556
+ children: question
4557
+ },
4558
+ `followup-${index}`
4559
+ )) })
4560
+ ] });
4561
+ };
4562
+ const chatContent = /* @__PURE__ */ jsxs7("div", { style: styles.root, children: [
4563
+ /* @__PURE__ */ jsx8("style", { children: `
4564
+ @keyframes zenitBlink {
4565
+ 0%, 49% { opacity: 1; }
4566
+ 50%, 100% { opacity: 0; }
4567
+ }
4568
+ @keyframes zenitPulse {
4569
+ 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
4570
+ 70% { box-shadow: 0 0 0 14px rgba(16, 185, 129, 0); }
4571
+ 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
4572
+ }
4573
+ @keyframes typingDotBounce {
4574
+ 0%, 60%, 100% { transform: translateY(0); }
4575
+ 30% { transform: translateY(-8px); }
4576
+ }
4577
+ .zenit-typing-dot:nth-child(1) { animation-delay: 0s; }
4578
+ .zenit-typing-dot:nth-child(2) { animation-delay: 0.2s; }
4579
+ .zenit-typing-dot:nth-child(3) { animation-delay: 0.4s; }
4580
+ .zenit-ai-button:not(.open) {
4581
+ animation: zenitPulse 2.5s infinite;
4582
+ }
4583
+ .zenit-ai-button:hover {
4584
+ transform: scale(1.05);
4585
+ }
4586
+ .zenit-ai-button:active {
4587
+ transform: scale(0.95);
4588
+ }
4589
+ .zenit-send-button:disabled {
4590
+ opacity: 0.5;
4591
+ cursor: not-allowed;
4592
+ }
4593
+ .zenit-send-button:not(:disabled):hover {
4594
+ opacity: 0.9;
4595
+ transform: translateY(-1px);
4596
+ }
4597
+ .zenit-send-button:not(:disabled):active {
4598
+ transform: translateY(0);
4599
+ }
4600
+ .zenit-chat-panel {
4601
+ box-sizing: border-box;
4602
+ }
4603
+ @media (max-width: 768px) {
4604
+ .zenit-chat-panel.zenit-chat-panel--fullscreen .zenit-ai-body {
4605
+ flex: 1;
4606
+ min-height: 0;
4607
+ overflow-y: auto;
4608
+ -webkit-overflow-scrolling: touch;
4609
+ }
4610
+ .zenit-chat-panel.zenit-chat-panel--fullscreen .zenit-ai-input-area {
4611
+ flex-shrink: 0;
4612
+ padding-bottom: max(14px, env(safe-area-inset-bottom));
4613
+ }
4614
+ .zenit-ai-button.zenit-ai-button--hidden-mobile {
4615
+ display: none !important;
4616
+ }
4617
+ }
4618
+ ` }),
4619
+ open && /* @__PURE__ */ jsxs7(
4620
+ "div",
4621
+ {
4622
+ ref: chatBoxRef,
4623
+ className: `zenit-chat-panel${expanded && !isMobile ? " zenit-chat-panel--expanded" : ""}${isMobile ? " zenit-chat-panel--fullscreen" : ""}`,
4624
+ style: (() => {
4625
+ const desktopStyle = expanded ? styles.panelExpanded : styles.panelNormal;
4626
+ const mobileStyle = styles.panelMobileFullscreen;
4627
+ return {
4628
+ ...styles.panel,
4629
+ ...isMobile ? mobileStyle : desktopStyle
4630
+ };
4631
+ })(),
4632
+ children: [
4633
+ /* @__PURE__ */ jsxs7("header", { style: styles.header, children: [
4634
+ /* @__PURE__ */ jsx8("h3", { style: styles.title, children: "Asistente Zenit AI" }),
4635
+ /* @__PURE__ */ jsxs7("div", { style: styles.headerButtons, children: [
4636
+ !isMobile && /* @__PURE__ */ jsx8(
4637
+ "button",
4638
+ {
4639
+ type: "button",
4640
+ style: styles.headerButton,
4641
+ onClick: () => setExpanded(!expanded),
4642
+ onMouseEnter: (e) => {
4643
+ e.currentTarget.style.background = "rgba(255, 255, 255, 0.25)";
4644
+ },
4645
+ onMouseLeave: (e) => {
4646
+ e.currentTarget.style.background = "rgba(255, 255, 255, 0.15)";
4647
+ },
4648
+ "aria-label": expanded ? "Contraer" : "Expandir",
4649
+ children: expanded ? /* @__PURE__ */ jsx8(CollapseIcon, {}) : /* @__PURE__ */ jsx8(ExpandIcon, {})
4650
+ }
4651
+ ),
4652
+ /* @__PURE__ */ jsx8(
4653
+ "button",
4654
+ {
4655
+ type: "button",
4656
+ style: styles.headerButton,
4657
+ onClick: () => setOpen(false),
4658
+ onMouseEnter: (e) => {
4659
+ e.currentTarget.style.background = "rgba(255, 255, 255, 0.25)";
4660
+ },
4661
+ onMouseLeave: (e) => {
4662
+ e.currentTarget.style.background = "rgba(255, 255, 255, 0.15)";
4663
+ },
4664
+ "aria-label": "Cerrar",
4665
+ children: /* @__PURE__ */ jsx8(CloseIcon, {})
4666
+ }
4667
+ )
4668
+ ] })
4669
+ ] }),
4670
+ /* @__PURE__ */ jsxs7("div", { ref: messagesContainerRef, className: "zenit-ai-body", style: styles.messages, children: [
4671
+ messages.map((message) => /* @__PURE__ */ jsx8(
4672
+ "div",
4673
+ {
4674
+ style: {
4675
+ ...styles.messageWrapper,
4676
+ alignItems: message.role === "user" ? "flex-end" : "flex-start"
4677
+ },
4678
+ children: /* @__PURE__ */ jsxs7(
4679
+ "div",
4680
+ {
4681
+ style: {
4682
+ ...styles.messageBubble,
4683
+ ...message.role === "user" ? styles.userMessage : styles.assistantMessage
4684
+ },
4685
+ children: [
4686
+ message.role === "assistant" ? /* @__PURE__ */ jsx8(MarkdownRenderer, { content: message.content }) : message.content,
4687
+ message.role === "assistant" && renderMetadata(message.response),
4688
+ message.role === "assistant" && renderActions(message.response),
4689
+ message.role === "assistant" && renderFollowUps(message.response)
4690
+ ]
4691
+ }
4692
+ )
4693
+ },
4694
+ message.id
4695
+ )),
4696
+ isStreaming && /* @__PURE__ */ jsx8(
4697
+ "div",
4698
+ {
4699
+ style: {
4700
+ ...styles.messageWrapper,
4701
+ alignItems: "flex-start"
4702
+ },
4703
+ children: /* @__PURE__ */ jsx8(
4704
+ "div",
4705
+ {
4706
+ style: {
4707
+ ...styles.messageBubble,
4708
+ ...styles.assistantMessage
4709
+ },
4710
+ children: streamingText ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
4711
+ /* @__PURE__ */ jsx8(MarkdownRenderer, { content: streamingText }),
4712
+ /* @__PURE__ */ jsx8("span", { style: styles.cursor })
4713
+ ] }) : /* @__PURE__ */ jsxs7("div", { style: styles.thinkingText, children: [
4714
+ /* @__PURE__ */ jsx8("span", { children: "Pensando" }),
4715
+ /* @__PURE__ */ jsxs7("div", { style: styles.typingIndicator, children: [
4716
+ /* @__PURE__ */ jsx8("div", { className: "zenit-typing-dot", style: styles.typingDot }),
4717
+ /* @__PURE__ */ jsx8("div", { className: "zenit-typing-dot", style: styles.typingDot }),
4718
+ /* @__PURE__ */ jsx8("div", { className: "zenit-typing-dot", style: styles.typingDot })
4719
+ ] })
4720
+ ] })
4721
+ }
4722
+ )
4723
+ }
4724
+ ),
4725
+ /* @__PURE__ */ jsx8("div", { ref: messagesEndRef })
4726
+ ] }),
4727
+ /* @__PURE__ */ jsxs7("div", { className: "zenit-ai-input-area", style: styles.inputWrapper, children: [
4728
+ /* @__PURE__ */ jsx8(
4729
+ "textarea",
4730
+ {
4731
+ style: {
4732
+ ...styles.textarea,
4733
+ ...isFocused ? styles.textareaFocus : {}
4734
+ },
4735
+ rows: 2,
4736
+ value: inputValue,
4737
+ onChange: (event) => setInputValue(event.target.value),
4738
+ onKeyDown: handleKeyDown,
4739
+ onFocus: () => setIsFocused(true),
4740
+ onBlur: () => setIsFocused(false),
4741
+ placeholder: !mapId ? "Selecciona un mapa para comenzar" : isStreaming ? "Esperando respuesta..." : "Escribe tu pregunta...",
4742
+ disabled: !mapId || !baseUrl || isStreaming
4743
+ }
4744
+ ),
4745
+ /* @__PURE__ */ jsx8(
4746
+ "button",
4747
+ {
4748
+ type: "button",
4749
+ className: "zenit-send-button",
4750
+ style: styles.sendButton,
4751
+ onClick: () => void handleSend(),
4752
+ disabled: !canSend,
4753
+ "aria-label": "Enviar mensaje",
4754
+ children: /* @__PURE__ */ jsx8(SendIcon, {})
4755
+ }
4756
+ )
4757
+ ] }),
4758
+ errorMessage && /* @__PURE__ */ jsx8("div", { style: styles.errorText, children: errorMessage }),
4759
+ isStreaming && !errorMessage && /* @__PURE__ */ jsx8("div", { style: styles.statusNote, children: "Generando sugerencias..." }),
4760
+ !mapId && !errorMessage && /* @__PURE__ */ jsx8("div", { style: styles.statusNote, children: "Selecciona un mapa para usar el asistente" }),
4761
+ !baseUrl && !errorMessage && /* @__PURE__ */ jsx8("div", { style: styles.statusNote, children: "Configura la baseUrl del SDK" })
4762
+ ]
4763
+ }
4764
+ ),
4765
+ !(hideButton && !open) && /* @__PURE__ */ jsx8(
4766
+ "button",
4767
+ {
4768
+ type: "button",
4769
+ className: `zenit-ai-button ${open ? "open" : ""}${open && isMobile ? " zenit-ai-button--hidden-mobile" : ""}`,
4770
+ style: {
4771
+ ...styles.floatingButton,
4772
+ ...open ? styles.floatingButtonOpen : isMobile ? styles.floatingButtonMobile : styles.floatingButtonClosed,
4773
+ zIndex: open ? 100001 : 99999
4774
+ },
4775
+ onClick: () => setOpen((prev) => !prev),
4776
+ "aria-label": open ? "Cerrar asistente" : "Abrir asistente Zenit AI",
4777
+ children: open ? /* @__PURE__ */ jsx8(CloseIcon, {}) : /* @__PURE__ */ jsxs7(Fragment4, { children: [
4778
+ /* @__PURE__ */ jsx8(ChatIcon, {}),
4779
+ !isMobile && /* @__PURE__ */ jsx8("span", { children: "Asistente IA" })
4780
+ ] })
4781
+ }
4782
+ )
4783
+ ] });
4784
+ if (typeof document !== "undefined") {
4785
+ return createPortal3(chatContent, document.body);
4786
+ }
4787
+ return chatContent;
4788
+ };
4789
+
4790
+ export {
4791
+ extractMapDto,
4792
+ normalizeMapCenter,
4793
+ normalizeMapLayers,
4794
+ computeBBoxFromFeature,
4795
+ centroidFromBBox,
4796
+ applyFilteredGeoJSONStrategy,
4797
+ initLayerStates,
4798
+ applyLayerOverrides,
4799
+ resetOverrides,
4800
+ sendMessage,
4801
+ sendMessageStream,
4802
+ createChatService,
4803
+ resolveLayerAccent,
4804
+ getLayerColor,
4805
+ getStyleByLayerId,
4806
+ getAccentByLayerId,
4807
+ clampNumber,
4808
+ clampOpacity3 as clampOpacity,
4809
+ isPolygonLayer,
4810
+ getZoomOpacityFactor,
4811
+ getLayerZoomOpacityFactor,
4812
+ getEffectiveLayerOpacity,
4813
+ ZenitMap,
4814
+ Eye,
4815
+ EyeOff,
4816
+ ChevronDown,
4817
+ ChevronLeft,
4818
+ ChevronRight,
4819
+ Layers,
4820
+ Upload,
4821
+ X,
4822
+ ZoomIn,
4823
+ ZenitSelect,
4824
+ ZenitLayerManager,
4825
+ ZenitFeatureFilterPanel,
4826
+ useSendMessage,
4827
+ useSendMessageStream,
4828
+ FloatingChatBox
4829
+ };
4830
+ //# sourceMappingURL=chunk-J2YWF2TS.mjs.map