zenit-sdk 0.1.4 → 0.1.5

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