zenit-sdk 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  // src/react/ZenitMap.tsx
2
- import React4, { useCallback as useCallback2, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo2, useRef as useRef5, useState as useState2, forwardRef } from "react";
2
+ import React4, { useCallback as useCallback2, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as useRef5, useState as useState2, forwardRef } from "react";
3
3
  import { MapContainer, Marker as Marker2, TileLayer, ZoomControl } from "react-leaflet";
4
4
  import L4 from "leaflet";
5
5
 
@@ -246,11 +246,125 @@ function getEffectiveLayerOpacity(baseOpacity, zoom, layerType, geometryType, op
246
246
  }
247
247
 
248
248
  // src/react/map/layer-geojson.tsx
249
- import { useEffect, useRef } from "react";
249
+ import { useEffect, useMemo, useRef } from "react";
250
250
  import { GeoJSON } from "react-leaflet";
251
251
  import L from "leaflet";
252
+
253
+ // src/react/map/geojson-sanitize.ts
254
+ var warnedKeys = /* @__PURE__ */ new Set();
255
+ function isFiniteNumber(value) {
256
+ if (typeof value === "number") return Number.isFinite(value);
257
+ if (typeof value === "string") return Number.isFinite(Number(value));
258
+ return false;
259
+ }
260
+ function isValidLonLat(lon, lat) {
261
+ const lonN = typeof lon === "string" ? Number(lon) : lon;
262
+ const latN = typeof lat === "string" ? Number(lat) : lat;
263
+ if (!isFiniteNumber(lonN) || !isFiniteNumber(latN)) return false;
264
+ if (typeof lonN !== "number" || typeof latN !== "number") return false;
265
+ return lonN >= -180 && lonN <= 180 && latN >= -90 && latN <= 90;
266
+ }
267
+ function asFeatureCollection(input) {
268
+ const raw = input && typeof input === "object" && "data" in input ? input.data : input;
269
+ if (!raw || typeof raw !== "object") {
270
+ return { type: "FeatureCollection", features: [] };
271
+ }
272
+ if (raw.type === "FeatureCollection") {
273
+ const collection = raw;
274
+ const features = Array.isArray(collection.features) ? collection.features : [];
275
+ return { ...collection, features };
276
+ }
277
+ if (raw.type === "Feature") {
278
+ return {
279
+ type: "FeatureCollection",
280
+ features: [raw]
281
+ };
282
+ }
283
+ return { type: "FeatureCollection", features: [] };
284
+ }
285
+ function sanitizeFeature(feature) {
286
+ if (!feature || typeof feature !== "object") return null;
287
+ const geometry = feature.geometry;
288
+ if (!geometry || typeof geometry !== "object") return null;
289
+ const geometryType = geometry.type;
290
+ const coordinates = geometry.coordinates;
291
+ if (geometryType === "Point") {
292
+ if (!Array.isArray(coordinates) || coordinates.length < 2) return null;
293
+ return isValidLonLat(coordinates[0], coordinates[1]) ? feature : null;
294
+ }
295
+ if (geometryType === "MultiPoint") {
296
+ if (!Array.isArray(coordinates)) return null;
297
+ const validCoordinates = coordinates.filter((coordinate) => {
298
+ if (!Array.isArray(coordinate) || coordinate.length < 2) return false;
299
+ return isValidLonLat(coordinate[0], coordinate[1]);
300
+ });
301
+ if (validCoordinates.length === 0) return null;
302
+ if (validCoordinates.length === coordinates.length) {
303
+ return feature;
304
+ }
305
+ return {
306
+ ...feature,
307
+ geometry: {
308
+ ...geometry,
309
+ coordinates: validCoordinates
310
+ }
311
+ };
312
+ }
313
+ if (geometryType === "LineString" || geometryType === "Polygon" || geometryType === "MultiPolygon" || geometryType === "MultiLineString") {
314
+ return Array.isArray(coordinates) && coordinates.length > 0 ? feature : null;
315
+ }
316
+ return feature;
317
+ }
318
+ function logSanitizeStats(stats, debugKey) {
319
+ if (process.env.NODE_ENV === "production") return;
320
+ if (stats.droppedFeatures === 0 && stats.droppedPointsFromMultiPoint === 0) return;
321
+ const resolvedKey = debugKey ?? "__default__";
322
+ if (warnedKeys.has(resolvedKey)) return;
323
+ warnedKeys.add(resolvedKey);
324
+ console.warn("[zenit-sdk] GeoJSON sanitization dropped invalid geometries.", {
325
+ layerId: debugKey,
326
+ ...stats
327
+ });
328
+ }
329
+ function sanitizeGeoJson(input, debugKey) {
330
+ const featureCollection = asFeatureCollection(input);
331
+ const safeFeatures = [];
332
+ const stats = {
333
+ droppedFeatures: 0,
334
+ droppedPointsFromMultiPoint: 0
335
+ };
336
+ const rawFeatures = Array.isArray(featureCollection.features) ? featureCollection.features : [];
337
+ rawFeatures.forEach((rawFeature) => {
338
+ const feature = rawFeature;
339
+ const geometryType = feature?.geometry?.type;
340
+ const originalCoords = feature?.geometry?.coordinates;
341
+ const sanitized = sanitizeFeature(feature);
342
+ if (!sanitized) {
343
+ stats.droppedFeatures += 1;
344
+ if (geometryType === "MultiPoint" && Array.isArray(originalCoords)) {
345
+ stats.droppedPointsFromMultiPoint += originalCoords.length;
346
+ }
347
+ return;
348
+ }
349
+ if (geometryType === "MultiPoint" && Array.isArray(originalCoords)) {
350
+ const safeCoords = sanitized.geometry?.coordinates;
351
+ const safeCount = Array.isArray(safeCoords) ? safeCoords.length : 0;
352
+ stats.droppedPointsFromMultiPoint += Math.max(0, originalCoords.length - safeCount);
353
+ }
354
+ safeFeatures.push(sanitized);
355
+ });
356
+ logSanitizeStats(stats, debugKey);
357
+ return {
358
+ ...featureCollection,
359
+ type: "FeatureCollection",
360
+ features: safeFeatures
361
+ };
362
+ }
363
+
364
+ // src/react/map/layer-geojson.tsx
252
365
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
253
366
  var POINT_GEOMETRY_TYPES = /* @__PURE__ */ new Set(["Point", "MultiPoint"]);
367
+ var DEV_MODE = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
254
368
  function normalizeBboxFromData(data) {
255
369
  const bboxCandidate = data.bbox;
256
370
  if (!Array.isArray(bboxCandidate) || bboxCandidate.length < 4) return null;
@@ -283,6 +397,45 @@ function isNonPointGeometry(feature) {
283
397
  const geometryType = getGeometryType(feature);
284
398
  return geometryType !== null && !POINT_GEOMETRY_TYPES.has(geometryType);
285
399
  }
400
+ function isValidLatLng(latlng) {
401
+ const candidate = latlng;
402
+ return Boolean(
403
+ candidate && Number.isFinite(candidate.lat) && Number.isFinite(candidate.lng)
404
+ );
405
+ }
406
+ function createInvisibleFallbackMarker(latlng) {
407
+ return L.circleMarker(latlng ?? [0, 0], {
408
+ radius: 0,
409
+ opacity: 0,
410
+ fillOpacity: 0,
411
+ interactive: false
412
+ });
413
+ }
414
+ function createInvisibleFallbackClusterMarker(latlng) {
415
+ return L.marker(latlng ?? [0, 0], {
416
+ opacity: 0,
417
+ interactive: false
418
+ });
419
+ }
420
+ function ensurePaneName(mapInstance, paneName, fallbackPaneName) {
421
+ if (!mapInstance) return fallbackPaneName;
422
+ const existingPane = mapInstance.getPane(paneName) ?? mapInstance.createPane(paneName);
423
+ if (!existingPane && DEV_MODE) {
424
+ console.warn("[LayerGeoJson] pane unavailable, using fallback", { paneName, fallbackPaneName });
425
+ }
426
+ return mapInstance.getPane(paneName) ? paneName : fallbackPaneName;
427
+ }
428
+ function createPointDivIcon(style, isMobile) {
429
+ const size = isMobile ? 18 : 14;
430
+ const backgroundColor = style.fillColor ?? style.color ?? "#3388ff";
431
+ const borderColor = style.color ?? style.fillColor ?? "#3388ff";
432
+ return L.divIcon({
433
+ className: "zenit-point-marker",
434
+ html: `<div style="width:${size}px;height:${size}px;border-radius:50%;background:${backgroundColor};border:2px solid ${borderColor};"></div>`,
435
+ iconSize: [size, size],
436
+ iconAnchor: [size / 2, size / 2]
437
+ });
438
+ }
286
439
  function buildFeatureCollection(features) {
287
440
  return {
288
441
  type: "FeatureCollection",
@@ -303,14 +456,15 @@ var LayerGeoJson = ({
303
456
  onEachFeature,
304
457
  onPolygonLabel
305
458
  }) => {
306
- const features = data.features ?? [];
307
- const fillFeatures = features.filter(isNonPointGeometry);
308
- const pointFeatures = features.filter(isPointGeometry);
459
+ const safeData = useMemo(() => sanitizeGeoJson(data, String(layerId)), [data, layerId]);
460
+ const features = useMemo(() => safeData.features ?? [], [safeData]);
461
+ const fillFeatures = useMemo(() => features.filter(isNonPointGeometry), [features]);
462
+ const pointFeatures = useMemo(() => features.filter(isPointGeometry), [features]);
309
463
  const dataVersionRef = useRef(0);
310
464
  const prevSignatureRef = useRef("");
311
465
  const firstId = features.length > 0 ? String(features[0]?.id ?? "") : "";
312
466
  const lastId = features.length > 0 ? String(features[features.length - 1]?.id ?? "") : "";
313
- const bbox = normalizeBboxFromData(data);
467
+ const bbox = normalizeBboxFromData(safeData);
314
468
  const idsSample = buildIdsSample(features);
315
469
  const signature = bbox ? `${layerId}|${features.length}|${firstId}|${lastId}|${idsSample}|${bbox[0]}|${bbox[1]}|${bbox[2]}|${bbox[3]}` : `${layerId}|${features.length}|${firstId}|${lastId}|${idsSample}`;
316
470
  const signatureToken = signature.replace(/[^a-zA-Z0-9_-]/g, "_");
@@ -318,25 +472,40 @@ var LayerGeoJson = ({
318
472
  dataVersionRef.current += 1;
319
473
  prevSignatureRef.current = signature;
320
474
  }
321
- const fillData = fillFeatures.length > 0 ? buildFeatureCollection(fillFeatures) : null;
322
- const pointsData = pointFeatures.length > 0 ? buildFeatureCollection(pointFeatures) : null;
475
+ const fillData = useMemo(() => fillFeatures.length > 0 ? buildFeatureCollection(fillFeatures) : null, [fillFeatures]);
476
+ const pointsData = useMemo(() => pointFeatures.length > 0 ? buildFeatureCollection(pointFeatures) : null, [pointFeatures]);
323
477
  const clusterLayerRef = useRef(null);
324
478
  const canCluster = typeof L.markerClusterGroup === "function";
479
+ const resolvedFillPane = useMemo(
480
+ () => ensurePaneName(mapInstance, fillPaneName, "overlayPane"),
481
+ [fillPaneName, mapInstance]
482
+ );
483
+ const resolvedPointsPane = useMemo(
484
+ () => ensurePaneName(mapInstance, pointsPaneName, "markerPane"),
485
+ [mapInstance, pointsPaneName]
486
+ );
325
487
  useEffect(() => {
326
488
  if (!mapInstance || !panesReady || !pointsData || !canCluster) return;
327
489
  const markerClusterGroup = L.markerClusterGroup;
328
- const clusterLayer = clusterLayerRef.current ?? markerClusterGroup();
490
+ const clusterPaneName = resolvedPointsPane;
491
+ const clusterLayer = clusterLayerRef.current ?? markerClusterGroup({ pane: clusterPaneName, clusterPane: clusterPaneName });
329
492
  clusterLayerRef.current = clusterLayer;
330
493
  if (!mapInstance.hasLayer(clusterLayer)) {
331
494
  mapInstance.addLayer(clusterLayer);
332
495
  }
333
496
  clusterLayer.clearLayers();
334
497
  const geoJsonLayer = L.geoJSON(pointsData, {
335
- pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
336
- radius: isMobile ? 8 : 6,
337
- pane: mapInstance.getPane(pointsPaneName) ? pointsPaneName : void 0,
338
- ...styleFn(feature, layerType, baseOpacity)
339
- }),
498
+ pointToLayer: (feature, latlng) => {
499
+ if (!isValidLatLng(latlng)) {
500
+ return createInvisibleFallbackClusterMarker();
501
+ }
502
+ const style = styleFn(feature, layerType, baseOpacity);
503
+ return L.marker(latlng, {
504
+ icon: createPointDivIcon(style, isMobile),
505
+ pane: clusterPaneName,
506
+ interactive: true
507
+ });
508
+ },
340
509
  onEachFeature
341
510
  });
342
511
  clusterLayer.addLayer(geoJsonLayer);
@@ -355,7 +524,7 @@ var LayerGeoJson = ({
355
524
  onEachFeature,
356
525
  panesReady,
357
526
  pointsData,
358
- pointsPaneName,
527
+ resolvedPointsPane,
359
528
  styleFn
360
529
  ]);
361
530
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -363,7 +532,7 @@ var LayerGeoJson = ({
363
532
  GeoJSON,
364
533
  {
365
534
  data: fillData,
366
- pane: panesReady && mapInstance?.getPane(fillPaneName) ? fillPaneName : void 0,
535
+ pane: resolvedFillPane,
367
536
  style: (feature) => styleFn(feature, layerType, baseOpacity),
368
537
  onEachFeature: (feature, layer) => {
369
538
  onEachFeature(feature, layer);
@@ -376,11 +545,16 @@ var LayerGeoJson = ({
376
545
  GeoJSON,
377
546
  {
378
547
  data: pointsData,
379
- pane: panesReady && mapInstance?.getPane(pointsPaneName) ? pointsPaneName : void 0,
380
- pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
381
- radius: isMobile ? 8 : 6,
382
- ...styleFn(feature, layerType, baseOpacity)
383
- }),
548
+ pane: resolvedPointsPane,
549
+ pointToLayer: (feature, latlng) => {
550
+ if (!isValidLatLng(latlng)) {
551
+ return createInvisibleFallbackMarker();
552
+ }
553
+ return L.circleMarker(latlng, {
554
+ radius: isMobile ? 8 : 6,
555
+ ...styleFn(feature, layerType, baseOpacity)
556
+ });
557
+ },
384
558
  onEachFeature
385
559
  },
386
560
  `points-${layerId}-${signatureToken}-v${dataVersionRef.current}`
@@ -389,7 +563,7 @@ var LayerGeoJson = ({
389
563
  };
390
564
 
391
565
  // src/react/map/location-control.tsx
392
- import { useEffect as useEffect3, useMemo, useRef as useRef3 } from "react";
566
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3 } from "react";
393
567
  import { createPortal } from "react-dom";
394
568
  import { Circle, Marker, useMap } from "react-leaflet";
395
569
  import L3 from "leaflet";
@@ -462,7 +636,22 @@ function useGeolocation(options) {
462
636
  import L2 from "leaflet";
463
637
  var POPUP_STYLE_ID = "zenit-leaflet-popup-styles";
464
638
  var POPUP_EXCLUDED_KEYS = /* @__PURE__ */ new Set(["geom", "geometry", "_private"]);
465
- var POPUP_HEADER_KEYS = ["nombre", "name", "title", "titulo"];
639
+ var POPUP_TITLE_KEYS = ["id", "nombre", "name", "title", "titulo", "cluster"];
640
+ var POPUP_BADGE_KEYS = ["tipo", "type", "category", "categoria", "estado", "status"];
641
+ var POPUP_DESCRIPTION_KEYS = ["descripcion", "description", "desc"];
642
+ var CURRENCY_KEYWORDS = [
643
+ "capital",
644
+ "monto",
645
+ "precio",
646
+ "costo",
647
+ "valor",
648
+ "ingreso",
649
+ "egreso",
650
+ "saldo",
651
+ "subtotal",
652
+ "venta",
653
+ "compra"
654
+ ];
466
655
  var DESKTOP_POPUP_DIMENSIONS = { maxWidth: 360, minWidth: 280, maxHeight: 520 };
467
656
  var MOBILE_POPUP_DIMENSIONS = { maxWidth: 300, minWidth: 240, maxHeight: 420 };
468
657
  var ZENIT_LEAFLET_POPUP_STYLES = `
@@ -580,6 +769,68 @@ function escapeHtml(value) {
580
769
  function formatLabel(key) {
581
770
  return key.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim();
582
771
  }
772
+ function isHexColor(value) {
773
+ return /^#([0-9A-F]{3}){1,2}$/i.test(value.trim());
774
+ }
775
+ function isCurrencyField(key) {
776
+ const normalized = key.trim().toLowerCase();
777
+ return CURRENCY_KEYWORDS.some((keyword) => normalized.includes(keyword));
778
+ }
779
+ function formatCurrency(value) {
780
+ return new Intl.NumberFormat("es-GT", { style: "currency", currency: "GTQ" }).format(value);
781
+ }
782
+ function getBadgeForProperty(key, value) {
783
+ if (value === null || value === void 0) return null;
784
+ const normalizedKey = key.trim().toLowerCase();
785
+ const normalizedValue = String(value).trim().toLowerCase();
786
+ const byType = {
787
+ mora: { label: "Mora", bg: "#fee2e2", color: "#b91c1c", border: "#fecaca" },
788
+ castigo: { label: "Castigo", bg: "#ffedd5", color: "#c2410c", border: "#fed7aa" },
789
+ colocacion: { label: "Colocaci\xF3n", bg: "#dbeafe", color: "#1d4ed8", border: "#bfdbfe" },
790
+ sano: { label: "Sano", bg: "#dcfce7", color: "#15803d", border: "#bbf7d0" }
791
+ };
792
+ const byStatus = {
793
+ activo: { label: "Activo", bg: "#dcfce7", color: "#15803d", border: "#bbf7d0" },
794
+ inactivo: { label: "Inactivo", bg: "#fef9c3", color: "#a16207", border: "#fde68a" },
795
+ pendiente: { label: "Pendiente", bg: "#fef9c3", color: "#a16207", border: "#fde68a" },
796
+ cancelado: { label: "Cancelado", bg: "#fee2e2", color: "#b91c1c", border: "#fecaca" },
797
+ rechazado: { label: "Rechazado", bg: "#fee2e2", color: "#b91c1c", border: "#fecaca" }
798
+ };
799
+ if (["tipo", "type", "category", "categoria"].includes(normalizedKey)) {
800
+ return byType[normalizedValue] ?? {
801
+ label: String(value),
802
+ bg: "#e0e7ff",
803
+ color: "#4338ca",
804
+ border: "#c7d2fe"
805
+ };
806
+ }
807
+ if (["estado", "status"].includes(normalizedKey)) {
808
+ return byStatus[normalizedValue] ?? {
809
+ label: String(value),
810
+ bg: "#f1f5f9",
811
+ color: "#475569",
812
+ border: "#cbd5e1"
813
+ };
814
+ }
815
+ return null;
816
+ }
817
+ function findHeaderProperties(properties) {
818
+ const entries = Object.entries(properties);
819
+ const titleEntry = entries.find(
820
+ ([key, value]) => POPUP_TITLE_KEYS.includes(key.trim().toLowerCase()) && String(value ?? "").trim().length > 0
821
+ );
822
+ const badgeEntry = entries.find(
823
+ ([key, value]) => POPUP_BADGE_KEYS.includes(key.trim().toLowerCase()) && String(value ?? "").trim().length > 0
824
+ );
825
+ const descriptionEntry = entries.find(
826
+ ([key, value]) => POPUP_DESCRIPTION_KEYS.includes(key.trim().toLowerCase()) && String(value ?? "").trim().length > 0
827
+ );
828
+ return {
829
+ title: titleEntry ? { key: titleEntry[0], value: String(titleEntry[1]).trim() } : void 0,
830
+ badge: badgeEntry ? { key: badgeEntry[0], value: badgeEntry[1] } : void 0,
831
+ description: descriptionEntry ? { key: descriptionEntry[0], value: String(descriptionEntry[1]).trim() } : void 0
832
+ };
833
+ }
583
834
  function renderPopupValue(value) {
584
835
  if (value === null || value === void 0) return '<span style="color:#94a3b8;">Sin datos</span>';
585
836
  if (value instanceof Date) {
@@ -605,12 +856,8 @@ function renderPopupValue(value) {
605
856
  return escapeHtml(String(value));
606
857
  }
607
858
  function extractPopupHeader(properties) {
608
- const entry = Object.entries(properties).find(([key, value]) => {
609
- if (typeof value !== "string") return false;
610
- const normalized = key.trim().toLowerCase();
611
- return POPUP_HEADER_KEYS.includes(normalized) && value.trim().length > 0;
612
- });
613
- return entry ? entry[1].trim() : null;
859
+ const header = findHeaderProperties(properties);
860
+ return header.title?.value ?? null;
614
861
  }
615
862
  function shouldIncludePopupEntry(key, value) {
616
863
  if (!key) return false;
@@ -623,21 +870,36 @@ function shouldIncludePopupEntry(key, value) {
623
870
  return true;
624
871
  }
625
872
  function createPopupContent(properties) {
626
- const headerText = extractPopupHeader(properties);
873
+ const header = findHeaderProperties(properties);
874
+ const headerText = header.title?.value ?? extractPopupHeader(properties);
875
+ const usedKeys = new Set([header.title?.key, header.badge?.key, header.description?.key].filter(Boolean));
876
+ const badge = header.badge ? getBadgeForProperty(header.badge.key, header.badge.value) : null;
877
+ const colorValue = properties.color;
878
+ const colorBar = typeof colorValue === "string" && isHexColor(colorValue) ? `<div style="height:6px; border-radius:8px; margin:-2px -2px 10px; background:linear-gradient(90deg, ${colorValue}, rgba(255,255,255,0.95));"></div>` : "";
627
879
  const entries = Object.entries(properties).filter(([key, value]) => {
628
880
  if (!shouldIncludePopupEntry(key, value)) return false;
629
- if (headerText && POPUP_HEADER_KEYS.includes(key.trim().toLowerCase())) return false;
881
+ if (usedKeys.has(key)) return false;
630
882
  return true;
631
883
  });
632
884
  if (entries.length === 0) {
633
885
  return '<div style="padding:8px 0; color:#64748b; text-align:center;">Sin datos disponibles</div>';
634
886
  }
635
- const headerHtml = headerText ? `<div style="font-weight:700; font-size:14px; margin-bottom:8px; color:#0f172a;">${escapeHtml(
636
- headerText
887
+ const headerHtml = headerText || badge ? `<div style="display:flex; justify-content:space-between; gap:8px; align-items:flex-start; margin-bottom:8px;">
888
+ <div style="font-weight:700; font-size:14px; color:#0f172a;">${escapeHtml(headerText ?? "Detalle")}</div>
889
+ ${badge ? `<span style="display:inline-flex; padding:2px 8px; border-radius:999px; font-size:11px; font-weight:700; background:${badge.bg}; color:${badge.color}; border:1px solid ${badge.border}; white-space:nowrap;">${escapeHtml(
890
+ badge.label
891
+ )}</span>` : ""}
892
+ </div>` : "";
893
+ const descriptionHtml = header.description ? `<div style="margin-bottom:10px; padding:8px 10px; background:#f8fafc; border-left:3px solid #38bdf8; border-radius:6px; color:#334155; font-size:12px;">${escapeHtml(
894
+ header.description.value
637
895
  )}</div>` : "";
638
896
  const rowsHtml = entries.map(([key, value]) => {
639
897
  const label = escapeHtml(formatLabel(key));
640
- const valueHtml = renderPopupValue(value);
898
+ const normalizedKey = key.trim().toLowerCase();
899
+ let valueHtml = renderPopupValue(value);
900
+ if (typeof value === "number") {
901
+ valueHtml = isCurrencyField(normalizedKey) ? `<span style="color:#15803d; font-weight:700;">${escapeHtml(formatCurrency(value))}</span>` : `<span style="color:#0369a1; font-weight:600;">${escapeHtml(value.toLocaleString("es-GT"))}</span>`;
902
+ }
641
903
  return `
642
904
  <div style="display:grid; grid-template-columns:minmax(90px, 35%) 1fr; gap:8px; padding:6px 0; border-bottom:1px solid #e2e8f0;">
643
905
  <div style="font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:0.04em; color:#64748b;">${label}</div>
@@ -645,7 +907,7 @@ function createPopupContent(properties) {
645
907
  </div>
646
908
  `;
647
909
  }).join("");
648
- return `<div>${headerHtml}${rowsHtml}</div>`;
910
+ return `<div>${colorBar}${headerHtml}${descriptionHtml}${rowsHtml}</div>`;
649
911
  }
650
912
  function isPolygonType(layerType, geometryType) {
651
913
  const candidate = (layerType ?? geometryType ?? "").toLowerCase();
@@ -655,21 +917,27 @@ function calculateZoomBasedOpacity(zoom, baseOpacity, layerType, geometryType) {
655
917
  if (!isPolygonType(layerType, geometryType)) return clampOpacity4(baseOpacity);
656
918
  const minZoom = 11;
657
919
  const maxZoom = 15;
658
- const minFactor = 0.3;
920
+ const minFactor = 0.5;
659
921
  if (maxZoom <= minZoom) return clampOpacity4(baseOpacity * minFactor);
660
922
  const t = clampNumber2((zoom - minZoom) / (maxZoom - minZoom), 0, 1);
661
923
  const factor = 1 - (1 - minFactor) * t;
662
924
  return clampOpacity4(baseOpacity * factor);
663
925
  }
664
926
  function layerStyleToLeaflet(options) {
665
- const { baseOpacity, zoom, layerStyle, geometryType, layerType } = options;
927
+ const { baseOpacity, zoom, layerStyle, geometryType, layerType, properties } = options;
666
928
  const sanitizedOpacity = clampOpacity4(baseOpacity);
667
929
  const zoomOpacity = calculateZoomBasedOpacity(zoom, sanitizedOpacity, layerType, geometryType);
668
- const styleFillOpacity = typeof layerStyle?.fillOpacity === "number" ? clampOpacity4(layerStyle.fillOpacity) : 0.8;
930
+ const badgeForType = getBadgeForProperty("type", properties?.type ?? properties?.tipo ?? properties?.category ?? properties?.categoria);
931
+ const badgeForStatus = getBadgeForProperty("status", properties?.status ?? properties?.estado);
932
+ const featureColor = (typeof properties?.color === "string" && properties.color.trim().length > 0 ? properties.color : void 0) ?? badgeForType?.color ?? badgeForStatus?.color;
933
+ const styleFillOpacity = typeof layerStyle?.fillOpacity === "number" ? clampOpacity4(layerStyle.fillOpacity) : 0.65;
934
+ const fallbackColor = "#2563eb";
935
+ const resolvedColor = featureColor ?? layerStyle?.color ?? layerStyle?.fillColor ?? fallbackColor;
936
+ const resolvedFillColor = featureColor ?? layerStyle?.fillColor ?? layerStyle?.color ?? fallbackColor;
669
937
  return {
670
- color: layerStyle?.color ?? layerStyle?.fillColor ?? "#2563eb",
938
+ color: resolvedColor,
671
939
  weight: layerStyle?.weight ?? 2,
672
- fillColor: layerStyle?.fillColor ?? layerStyle?.color ?? "#2563eb",
940
+ fillColor: resolvedFillColor,
673
941
  opacity: clampOpacity4(Math.max(0.35, zoomOpacity * 0.9)),
674
942
  fillOpacity: clampOpacity4(zoomOpacity * styleFillOpacity)
675
943
  };
@@ -900,7 +1168,7 @@ var LocationControl = ({ position = "bottomleft", zoom = 16 }) => {
900
1168
  hasCenteredRef.current = true;
901
1169
  map.flyTo([location.lat, location.lon], zoom, { animate: true });
902
1170
  }, [isTracking, location, map, zoom]);
903
- const markerIcon = useMemo(() => createLocationIcon(), []);
1171
+ const markerIcon = useMemo2(() => createLocationIcon(), []);
904
1172
  return /* @__PURE__ */ jsxs2(Fragment2, { children: [
905
1173
  controlRef.current && createPortal(
906
1174
  /* @__PURE__ */ jsxs2("div", { children: [
@@ -1067,6 +1335,7 @@ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1067
1335
  var DEFAULT_CENTER = [0, 0];
1068
1336
  var DEFAULT_ZOOM = 3;
1069
1337
  var LABELS_PANE_NAME = "zenit-labels-pane";
1338
+ var DEV_MODE2 = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
1070
1339
  function isRecord(value) {
1071
1340
  return typeof value === "object" && value !== null;
1072
1341
  }
@@ -1090,6 +1359,8 @@ function getFeatureLayerId(feature) {
1090
1359
  return layerId;
1091
1360
  }
1092
1361
  var DESCRIPTION_KEYS = /* @__PURE__ */ new Set(["descripcion", "description"]);
1362
+ var POINT_GEOMETRY_TYPES2 = /* @__PURE__ */ new Set(["Point", "MultiPoint"]);
1363
+ var POLYGON_GEOMETRY_TYPES = /* @__PURE__ */ new Set(["Polygon", "MultiPolygon"]);
1093
1364
  function normalizeDescriptionValue(value) {
1094
1365
  if (value === void 0 || value === null) return null;
1095
1366
  if (typeof value === "string") {
@@ -1109,6 +1380,31 @@ function extractDescriptionValue(properties) {
1109
1380
  if (!matches) return null;
1110
1381
  return normalizeDescriptionValue(matches[1]);
1111
1382
  }
1383
+ function normalizeLayerId(value) {
1384
+ if (value === null || value === void 0) return null;
1385
+ const normalized = String(value).trim();
1386
+ return normalized ? normalized : null;
1387
+ }
1388
+ function isLayerIdMatch(a, b) {
1389
+ const normalizedA = normalizeLayerId(a);
1390
+ const normalizedB = normalizeLayerId(b);
1391
+ return normalizedA !== null && normalizedB !== null && normalizedA === normalizedB;
1392
+ }
1393
+ function getClickIntent(params) {
1394
+ const geometryType = params.feature?.geometry?.type;
1395
+ if (geometryType && POINT_GEOMETRY_TYPES2.has(geometryType)) return "point";
1396
+ if (params.leafletLayer instanceof L4.Marker) return "point";
1397
+ if (geometryType && POLYGON_GEOMETRY_TYPES.has(geometryType)) return "polygon";
1398
+ if (params.leafletLayer instanceof L4.Path) return "polygon";
1399
+ return "unknown";
1400
+ }
1401
+ function candidateLayerId(candidate) {
1402
+ return getFeatureLayerId(candidate);
1403
+ }
1404
+ function isCandidateGeometryType(candidate, allowedTypes) {
1405
+ const geometryType = candidate?.geometry?.type;
1406
+ return Boolean(geometryType && allowedTypes.has(geometryType));
1407
+ }
1112
1408
  function getFeatureStyleOverrides(feature) {
1113
1409
  const candidate = feature?.properties?._style;
1114
1410
  if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return null;
@@ -1126,17 +1422,50 @@ function buildFeaturePopupHtml(feature) {
1126
1422
  const rendered = createPopupContent(properties);
1127
1423
  return rendered ? rendered : null;
1128
1424
  }
1129
- function pickIntersectFeature(baseFeature, candidates) {
1425
+ function pickIntersectFeature(params) {
1426
+ const { baseFeature, candidates, expectedLayerId, clickIntent } = params;
1130
1427
  if (!Array.isArray(candidates) || candidates.length === 0) return null;
1428
+ const pickByGeometry = (pool, intent) => {
1429
+ const preferredTypes = intent === "point" ? POINT_GEOMETRY_TYPES2 : intent === "polygon" ? POLYGON_GEOMETRY_TYPES : null;
1430
+ if (!preferredTypes) return null;
1431
+ const found = pool.find((candidate) => isCandidateGeometryType(candidate, preferredTypes));
1432
+ if (!found) return null;
1433
+ return { feature: found, reason: `geometry:${intent}` };
1434
+ };
1435
+ const getResult = (feature, reason) => ({
1436
+ feature,
1437
+ selectedIdx: candidates.findIndex((candidate) => candidate === feature),
1438
+ reason
1439
+ });
1440
+ const sameLayer = candidates.filter(
1441
+ (candidate) => isLayerIdMatch(candidateLayerId(candidate), expectedLayerId)
1442
+ );
1443
+ const geometryFromSameLayer = pickByGeometry(sameLayer, clickIntent);
1444
+ if (geometryFromSameLayer?.feature) {
1445
+ return getResult(geometryFromSameLayer.feature, `sameLayer>${geometryFromSameLayer.reason}`);
1446
+ }
1447
+ if (sameLayer.length > 0) {
1448
+ const sameLayerWithDescription = sameLayer.find(
1449
+ (candidate) => extractDescriptionValue(candidate?.properties)
1450
+ );
1451
+ if (sameLayerWithDescription) {
1452
+ return getResult(sameLayerWithDescription, "sameLayer>description");
1453
+ }
1454
+ }
1455
+ const geometryFallback = pickByGeometry(candidates, clickIntent);
1456
+ if (geometryFallback?.feature) {
1457
+ return getResult(geometryFallback.feature, `anyLayer>${geometryFallback.reason}`);
1458
+ }
1131
1459
  const baseId = baseFeature?.id;
1132
1460
  if (baseId !== void 0 && baseId !== null) {
1133
1461
  const matchById = candidates.find((candidate) => candidate?.id === baseId);
1134
- if (matchById) return matchById;
1462
+ if (matchById) return getResult(matchById, "fallback>baseId");
1135
1463
  }
1136
1464
  const matchWithDescription = candidates.find(
1137
1465
  (candidate) => extractDescriptionValue(candidate?.properties)
1138
1466
  );
1139
- return matchWithDescription ?? candidates[0];
1467
+ if (matchWithDescription) return getResult(matchWithDescription, "fallback>description");
1468
+ return getResult(candidates[0], "fallback>first");
1140
1469
  }
1141
1470
  function normalizeCenterTuple(center) {
1142
1471
  if (!center) return null;
@@ -1192,7 +1521,7 @@ var ZenitMap = forwardRef(({
1192
1521
  if (typeof window === "undefined") return false;
1193
1522
  return window.matchMedia("(max-width: 768px)").matches;
1194
1523
  });
1195
- const normalizedLayers = useMemo2(() => normalizeMapLayers(map), [map]);
1524
+ const normalizedLayers = useMemo3(() => normalizeMapLayers(map), [map]);
1196
1525
  useEffect5(() => {
1197
1526
  if (typeof window === "undefined") return;
1198
1527
  const mql = window.matchMedia("(max-width: 768px)");
@@ -1241,7 +1570,7 @@ var ZenitMap = forwardRef(({
1241
1570
  mapInstance.off("popupclose", handlePopupClose);
1242
1571
  };
1243
1572
  }, [mapInstance]);
1244
- const layerStyleIndex = useMemo2(() => {
1573
+ const layerStyleIndex = useMemo3(() => {
1245
1574
  const index = /* @__PURE__ */ new Map();
1246
1575
  (map?.mapLayers ?? []).forEach((entry) => {
1247
1576
  const layerStyle = entry.layer?.style ?? entry.mapLayer?.layer?.style ?? entry.style ?? null;
@@ -1252,7 +1581,7 @@ var ZenitMap = forwardRef(({
1252
1581
  });
1253
1582
  return index;
1254
1583
  }, [map]);
1255
- const labelKeyIndex = useMemo2(() => {
1584
+ const labelKeyIndex = useMemo3(() => {
1256
1585
  const index = /* @__PURE__ */ new Map();
1257
1586
  normalizedLayers.forEach((entry) => {
1258
1587
  const label = entry.layer?.label ?? entry.mapLayer?.label ?? entry.mapLayer.layerConfig?.label;
@@ -1262,7 +1591,7 @@ var ZenitMap = forwardRef(({
1262
1591
  });
1263
1592
  return index;
1264
1593
  }, [normalizedLayers]);
1265
- const layerMetaIndex = useMemo2(() => {
1594
+ const layerMetaIndex = useMemo3(() => {
1266
1595
  const index = /* @__PURE__ */ new Map();
1267
1596
  normalizedLayers.forEach((entry) => {
1268
1597
  index.set(String(entry.layerId), {
@@ -1272,7 +1601,7 @@ var ZenitMap = forwardRef(({
1272
1601
  });
1273
1602
  return index;
1274
1603
  }, [normalizedLayers]);
1275
- const overlayStyleFunction = useMemo2(() => {
1604
+ const overlayStyleFunction = useMemo3(() => {
1276
1605
  return (feature) => {
1277
1606
  const featureLayerId = getFeatureLayerId(feature);
1278
1607
  const featureStyleOverrides = getFeatureStyleOverrides(feature);
@@ -1456,7 +1785,7 @@ var ZenitMap = forwardRef(({
1456
1785
  },
1457
1786
  [currentZoom, effectiveStates, layerMetaIndex, layerStates, onLayerStateChange]
1458
1787
  );
1459
- const center = useMemo2(() => {
1788
+ const center = useMemo3(() => {
1460
1789
  if (initialCenter) {
1461
1790
  return initialCenter;
1462
1791
  }
@@ -1480,7 +1809,7 @@ var ZenitMap = forwardRef(({
1480
1809
  }
1481
1810
  });
1482
1811
  }, [layerGeojson, layers]);
1483
- const decoratedLayers = useMemo2(() => {
1812
+ const decoratedLayers = useMemo3(() => {
1484
1813
  return layers.map((layer) => {
1485
1814
  const layerKey = String(layer.mapLayer.layerId);
1486
1815
  const override = layerGeojsonOverrides[layerKey];
@@ -1491,10 +1820,10 @@ var ZenitMap = forwardRef(({
1491
1820
  };
1492
1821
  });
1493
1822
  }, [effectiveStates, layerGeojson, layerGeojsonOverrides, layers]);
1494
- const orderedLayers = useMemo2(() => {
1823
+ const orderedLayers = useMemo3(() => {
1495
1824
  return [...decoratedLayers].filter((layer) => layer.effective?.visible && layer.data).sort((a, b) => a.displayOrder - b.displayOrder);
1496
1825
  }, [decoratedLayers]);
1497
- const autoZoomGeojson = useMemo2(
1826
+ const autoZoomGeojson = useMemo3(
1498
1827
  () => orderedLayers.map((layer) => layer.data).filter((collection) => !!collection),
1499
1828
  [orderedLayers]
1500
1829
  );
@@ -1504,7 +1833,7 @@ var ZenitMap = forwardRef(({
1504
1833
  },
1505
1834
  [layerStyleIndex, mapLayers]
1506
1835
  );
1507
- const labelMarkers = useMemo2(() => {
1836
+ const labelMarkers = useMemo3(() => {
1508
1837
  const markers = [];
1509
1838
  decoratedLayers.forEach((layerState) => {
1510
1839
  if (!layerState.effective?.visible) return;
@@ -1543,7 +1872,8 @@ var ZenitMap = forwardRef(({
1543
1872
  }, [currentZoom, decoratedLayers, labelKeyIndex, layerMetaIndex, resolveLayerStyle]);
1544
1873
  const ensureLayerPanes = useCallback2(
1545
1874
  (targetMap, targetLayers) => {
1546
- const baseZIndex = 400;
1875
+ const fillBaseZIndex = 400;
1876
+ const pointsBaseZIndex = 700;
1547
1877
  targetLayers.forEach((layer) => {
1548
1878
  const order = Number.isFinite(layer.displayOrder) ? layer.displayOrder : 0;
1549
1879
  const orderOffset = Math.max(0, Math.min(order, 150));
@@ -1551,8 +1881,8 @@ var ZenitMap = forwardRef(({
1551
1881
  const pointPaneName = `zenit-layer-${layer.layerId}-points`;
1552
1882
  const fillPane = targetMap.getPane(fillPaneName) ?? targetMap.createPane(fillPaneName);
1553
1883
  const pointPane = targetMap.getPane(pointPaneName) ?? targetMap.createPane(pointPaneName);
1554
- fillPane.style.zIndex = String(baseZIndex + orderOffset);
1555
- pointPane.style.zIndex = String(baseZIndex + orderOffset + 100);
1884
+ fillPane.style.zIndex = String(fillBaseZIndex + orderOffset);
1885
+ pointPane.style.zIndex = String(pointsBaseZIndex + orderOffset);
1556
1886
  });
1557
1887
  },
1558
1888
  []
@@ -1570,114 +1900,149 @@ var ZenitMap = forwardRef(({
1570
1900
  setPanesReady(false);
1571
1901
  return;
1572
1902
  }
1573
- if (orderedLayers.length === 0) {
1903
+ if (orderedLayers.length === 0 && !overlayGeojson) {
1574
1904
  return;
1575
1905
  }
1576
1906
  const layerTargets = orderedLayers.map((layer) => ({
1577
1907
  layerId: layer.mapLayer.layerId,
1578
1908
  displayOrder: layer.displayOrder
1579
1909
  }));
1910
+ if (overlayGeojson) {
1911
+ layerTargets.push({ layerId: "overlay-geojson", displayOrder: 999 });
1912
+ }
1580
1913
  ensureLayerPanes(mapInstance, layerTargets);
1581
1914
  const first = layerTargets[0];
1582
- const testPane = mapInstance.getPane(`zenit-layer-${first.layerId}-fill`);
1915
+ const testPane = first ? mapInstance.getPane(`zenit-layer-${first.layerId}-fill`) : null;
1583
1916
  const labelsPane = mapInstance.getPane(LABELS_PANE_NAME);
1584
1917
  if (testPane && labelsPane) {
1585
1918
  setPanesReady(true);
1586
1919
  }
1587
- }, [mapInstance, orderedLayers, ensureLayerPanes]);
1588
- const overlayOnEachFeature = useMemo2(() => {
1589
- return (feature, layer) => {
1590
- const layerId = getFeatureLayerId(feature) ?? void 0;
1591
- const geometryType = feature?.geometry?.type;
1592
- const isPointFeature = geometryType === "Point" || geometryType === "MultiPoint" || layer instanceof L4.CircleMarker;
1593
- const originalStyle = layer instanceof L4.Path ? {
1594
- color: layer.options.color,
1595
- weight: layer.options.weight,
1596
- fillColor: layer.options.fillColor,
1597
- opacity: layer.options.opacity,
1598
- fillOpacity: layer.options.fillOpacity
1599
- } : null;
1600
- const originalRadius = layer instanceof L4.CircleMarker ? layer.getRadius() : null;
1601
- if (featureInfoMode === "popup") {
1602
- const content = buildFeaturePopupHtml(feature);
1603
- if (content) {
1604
- const { maxWidth, minWidth, maxHeight } = getPopupDimensions();
1605
- layer.bindPopup(content, {
1606
- maxWidth,
1607
- minWidth,
1608
- maxHeight,
1609
- className: "custom-leaflet-popup",
1610
- autoPan: true,
1611
- closeButton: true,
1612
- keepInView: true
1613
- });
1614
- }
1615
- }
1616
- if (isPointFeature && layer.bindTooltip) {
1617
- layer.bindTooltip("Click para ver detalle", {
1618
- sticky: true,
1619
- direction: "top",
1620
- opacity: 0.9,
1621
- className: "zenit-map-tooltip"
1920
+ }, [mapInstance, orderedLayers, overlayGeojson, ensureLayerPanes]);
1921
+ const overlayOnEachFeature = useCallback2((feature, layer) => {
1922
+ const layerId = getFeatureLayerId(feature) ?? void 0;
1923
+ const geometryType = feature?.geometry?.type;
1924
+ const clickIntent = getClickIntent({ feature, leafletLayer: layer });
1925
+ const isPointFeature = clickIntent === "point";
1926
+ const originalStyle = layer instanceof L4.Path ? {
1927
+ color: layer.options.color,
1928
+ weight: layer.options.weight,
1929
+ fillColor: layer.options.fillColor,
1930
+ opacity: layer.options.opacity,
1931
+ fillOpacity: layer.options.fillOpacity
1932
+ } : null;
1933
+ const originalRadius = layer instanceof L4.CircleMarker ? layer.getRadius() : null;
1934
+ if (featureInfoMode === "popup") {
1935
+ const content = buildFeaturePopupHtml(feature);
1936
+ if (content) {
1937
+ const { maxWidth, minWidth, maxHeight } = getPopupDimensions();
1938
+ layer.bindPopup(content, {
1939
+ maxWidth,
1940
+ minWidth,
1941
+ maxHeight,
1942
+ className: "custom-leaflet-popup",
1943
+ autoClose: true,
1944
+ closeOnClick: true,
1945
+ autoPan: true,
1946
+ closeButton: true,
1947
+ keepInView: true,
1948
+ autoPanPadding: [50, 50]
1622
1949
  });
1623
1950
  }
1624
- layer.on("click", () => {
1625
- if (featureInfoMode === "popup" && client && layerId !== void 0 && !extractDescriptionValue(feature?.properties) && feature?.geometry) {
1626
- const trackedFeature = feature;
1627
- if (!trackedFeature.__zenit_popup_loaded) {
1628
- trackedFeature.__zenit_popup_loaded = true;
1629
- client.layers.getLayerGeoJsonIntersect({
1630
- id: layerId,
1631
- geometry: feature.geometry
1632
- }).then((response) => {
1633
- const geo = extractGeoJsonFeatureCollection(response);
1634
- const candidates = geo?.features ?? [];
1635
- const resolved = pickIntersectFeature(feature, candidates);
1636
- if (!resolved?.properties) return;
1637
- const mergedProperties = {
1638
- ...trackedFeature.properties ?? {},
1639
- ...resolved.properties
1640
- };
1641
- trackedFeature.properties = mergedProperties;
1642
- const updatedHtml = buildFeaturePopupHtml({
1643
- ...feature,
1644
- properties: mergedProperties
1645
- });
1646
- if (updatedHtml && layer.setPopupContent) {
1647
- layer.setPopupContent(updatedHtml);
1648
- }
1649
- }).catch(() => {
1650
- trackedFeature.__zenit_popup_loaded = false;
1651
- });
1652
- }
1653
- }
1654
- onFeatureClick?.(feature, layerId);
1951
+ }
1952
+ if (isPointFeature && layer.bindTooltip) {
1953
+ layer.bindTooltip("Click para ver detalle", {
1954
+ sticky: true,
1955
+ direction: "top",
1956
+ opacity: 0.9,
1957
+ className: "zenit-map-tooltip"
1655
1958
  });
1656
- layer.on("mouseover", () => {
1657
- if (layer instanceof L4.Path && originalStyle) {
1658
- layer.setStyle({
1659
- ...originalStyle,
1660
- weight: (originalStyle.weight ?? 2) + 1,
1661
- opacity: Math.min(1, (originalStyle.opacity ?? 1) + 0.2),
1662
- fillOpacity: Math.min(1, (originalStyle.fillOpacity ?? 0.8) + 0.1)
1959
+ }
1960
+ layer.on("click", () => {
1961
+ if (featureInfoMode === "popup" && client && layerId !== void 0 && !extractDescriptionValue(feature?.properties) && feature?.geometry) {
1962
+ if (DEV_MODE2) {
1963
+ console.debug("[ZenitMap] click/intersect:start", {
1964
+ expectedLayerId: layerId,
1965
+ geometryType,
1966
+ clickIntent,
1967
+ leafletLayerType: layer?.constructor?.name,
1968
+ pane: layer?.options?.pane
1663
1969
  });
1664
1970
  }
1665
- if (layer instanceof L4.CircleMarker && typeof originalRadius === "number") {
1666
- layer.setRadius(originalRadius + 1);
1667
- }
1668
- onFeatureHover?.(feature, layerId);
1669
- });
1670
- layer.on("mouseout", () => {
1671
- if (layer instanceof L4.Path && originalStyle) {
1672
- layer.setStyle(originalStyle);
1673
- }
1674
- if (layer instanceof L4.CircleMarker && typeof originalRadius === "number") {
1675
- layer.setRadius(originalRadius);
1971
+ const trackedFeature = feature;
1972
+ if (!trackedFeature.__zenit_popup_loaded) {
1973
+ trackedFeature.__zenit_popup_loaded = true;
1974
+ client.layers.getLayerGeoJsonIntersect({
1975
+ id: layerId,
1976
+ geometry: feature.geometry
1977
+ }).then((response) => {
1978
+ const geo = extractGeoJsonFeatureCollection(response);
1979
+ const candidates = geo?.features ?? [];
1980
+ const selection = pickIntersectFeature({
1981
+ baseFeature: feature,
1982
+ candidates,
1983
+ expectedLayerId: layerId,
1984
+ clickIntent
1985
+ });
1986
+ const resolved = selection?.feature;
1987
+ if (DEV_MODE2) {
1988
+ console.debug("[ZenitMap] click/intersect:result", {
1989
+ expectedLayerId: layerId,
1990
+ clickIntent,
1991
+ candidatesCount: candidates.length,
1992
+ candidates: candidates.map((candidate, idx) => ({
1993
+ idx,
1994
+ geomType: candidate?.geometry?.type,
1995
+ candidateLayerId: candidateLayerId(candidate),
1996
+ hasDescription: Boolean(extractDescriptionValue(candidate?.properties))
1997
+ })),
1998
+ selectedIdx: selection?.selectedIdx ?? -1,
1999
+ selectionRule: selection?.reason ?? "none"
2000
+ });
2001
+ }
2002
+ if (!resolved?.properties) return;
2003
+ const mergedProperties = {
2004
+ ...trackedFeature.properties ?? {},
2005
+ ...resolved.properties
2006
+ };
2007
+ trackedFeature.properties = mergedProperties;
2008
+ const updatedHtml = buildFeaturePopupHtml({
2009
+ ...feature,
2010
+ properties: mergedProperties
2011
+ });
2012
+ if (updatedHtml && layer.setPopupContent) {
2013
+ layer.setPopupContent(updatedHtml);
2014
+ }
2015
+ }).catch(() => {
2016
+ trackedFeature.__zenit_popup_loaded = false;
2017
+ });
1676
2018
  }
1677
- });
1678
- };
2019
+ }
2020
+ onFeatureClick?.(feature, layerId);
2021
+ });
2022
+ layer.on("mouseover", () => {
2023
+ if (layer instanceof L4.Path && originalStyle) {
2024
+ layer.setStyle({
2025
+ ...originalStyle,
2026
+ weight: (originalStyle.weight ?? 2) + 1,
2027
+ opacity: Math.min(1, (originalStyle.opacity ?? 1) + 0.2),
2028
+ fillOpacity: Math.min(1, (originalStyle.fillOpacity ?? 0.8) + 0.1)
2029
+ });
2030
+ }
2031
+ if (layer instanceof L4.CircleMarker && typeof originalRadius === "number") {
2032
+ layer.setRadius(originalRadius + 1);
2033
+ }
2034
+ onFeatureHover?.(feature, layerId);
2035
+ });
2036
+ layer.on("mouseout", () => {
2037
+ if (layer instanceof L4.Path && originalStyle) {
2038
+ layer.setStyle(originalStyle);
2039
+ }
2040
+ if (layer instanceof L4.CircleMarker && typeof originalRadius === "number") {
2041
+ layer.setRadius(originalRadius);
2042
+ }
2043
+ });
1679
2044
  }, [client, featureInfoMode, onFeatureClick, onFeatureHover]);
1680
- const buildLayerStyle = (layerId, baseOpacity, feature, layerType) => {
2045
+ const buildLayerStyle = useCallback2((layerId, baseOpacity, feature, layerType) => {
1681
2046
  const style = resolveLayerStyle(layerId);
1682
2047
  const featureStyleOverrides = getFeatureStyleOverrides(feature);
1683
2048
  const resolvedStyle = featureStyleOverrides ? { ...style ?? {}, ...featureStyleOverrides } : style;
@@ -1688,14 +2053,15 @@ var ZenitMap = forwardRef(({
1688
2053
  zoom: currentZoom,
1689
2054
  layerStyle: resolvedStyle,
1690
2055
  geometryType,
1691
- layerType: resolvedLayerType
2056
+ layerType: resolvedLayerType,
2057
+ properties: feature?.properties ?? void 0
1692
2058
  });
1693
- };
1694
- const makeStyleFnForLayer = (layerId) => {
2059
+ }, [currentZoom, resolveLayerStyle]);
2060
+ const makeStyleFnForLayer = useCallback2((layerId) => {
1695
2061
  return (feature, layerType, baseOpacity) => {
1696
2062
  return buildLayerStyle(layerId, baseOpacity ?? 1, feature, layerType);
1697
2063
  };
1698
- };
2064
+ }, [buildLayerStyle]);
1699
2065
  useImperativeHandle(ref, () => ({
1700
2066
  setLayerOpacity: (layerId, opacity) => {
1701
2067
  upsertUiOverride(layerId, { overrideOpacity: opacity });
@@ -1850,7 +2216,7 @@ var ZenitMap = forwardRef(({
1850
2216
  const layerType = layerState.layer?.layerType ?? layerState.mapLayer.layerType ?? void 0;
1851
2217
  const labelKey = labelKeyIndex.get(String(layerState.mapLayer.layerId));
1852
2218
  return /* @__PURE__ */ jsxs3(React4.Fragment, { children: [
1853
- layerState.data && /* @__PURE__ */ jsx3(
2219
+ layerState.data && panesReady && /* @__PURE__ */ jsx3(
1854
2220
  LayerGeoJson,
1855
2221
  {
1856
2222
  layerId: layerState.mapLayer.layerId,
@@ -1893,7 +2259,7 @@ var ZenitMap = forwardRef(({
1893
2259
  )) : null
1894
2260
  ] }, layerState.mapLayer.layerId.toString());
1895
2261
  }),
1896
- overlayGeojson && /* @__PURE__ */ jsx3(
2262
+ overlayGeojson && panesReady && /* @__PURE__ */ jsx3(
1897
2263
  LayerGeoJson,
1898
2264
  {
1899
2265
  layerId: "overlay-geojson",
@@ -1927,7 +2293,7 @@ var ZenitMap = forwardRef(({
1927
2293
  overflowY: "auto"
1928
2294
  },
1929
2295
  children: [
1930
- overlayGeojson && /* @__PURE__ */ jsxs3(
2296
+ overlayGeojson && panesReady && /* @__PURE__ */ jsxs3(
1931
2297
  "div",
1932
2298
  {
1933
2299
  style: {
@@ -2006,7 +2372,7 @@ var ZenitMap = forwardRef(({
2006
2372
  ZenitMap.displayName = "ZenitMap";
2007
2373
 
2008
2374
  // src/react/ZenitLayerManager.tsx
2009
- import React6, { useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState3 } from "react";
2375
+ import React6, { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef6, useState as useState3 } from "react";
2010
2376
 
2011
2377
  // src/react/icons.tsx
2012
2378
  import { Eye, EyeOff, ChevronDown, ChevronLeft, ChevronRight, Layers, Upload, X, ZoomIn } from "lucide-react";
@@ -2424,7 +2790,7 @@ var ZenitLayerManager = ({
2424
2790
  const catalogAbortRef = useRef6(null);
2425
2791
  const lastEmittedStatesRef = useRef6(null);
2426
2792
  const isControlled = Array.isArray(layerStates) && typeof onLayerStatesChange === "function";
2427
- const baseStates = useMemo3(
2793
+ const baseStates = useMemo4(
2428
2794
  () => initLayerStates(
2429
2795
  layers.map((entry) => ({
2430
2796
  ...entry.mapLayer,
@@ -2435,7 +2801,7 @@ var ZenitLayerManager = ({
2435
2801
  ),
2436
2802
  [layers]
2437
2803
  );
2438
- const overrideStates = useMemo3(
2804
+ const overrideStates = useMemo4(
2439
2805
  () => layers.map(
2440
2806
  (entry) => ({
2441
2807
  layerId: entry.mapLayer.layerId,
@@ -2445,11 +2811,11 @@ var ZenitLayerManager = ({
2445
2811
  ),
2446
2812
  [layers]
2447
2813
  );
2448
- const effectiveStates = useMemo3(
2814
+ const effectiveStates = useMemo4(
2449
2815
  () => layerStates ?? applyLayerOverrides(baseStates, overrideStates),
2450
2816
  [baseStates, layerStates, overrideStates]
2451
2817
  );
2452
- const layerMetaIndex = useMemo3(() => {
2818
+ const layerMetaIndex = useMemo4(() => {
2453
2819
  const index = /* @__PURE__ */ new Map();
2454
2820
  mapLayers?.forEach((entry) => {
2455
2821
  const key = String(entry.layerId);
@@ -2487,7 +2853,7 @@ var ZenitLayerManager = ({
2487
2853
  },
2488
2854
  [autoOpacityConfig, autoOpacityOnZoom, layerMetaIndex, mapZoom]
2489
2855
  );
2490
- const effectiveStatesWithZoom = useMemo3(() => {
2856
+ const effectiveStatesWithZoom = useMemo4(() => {
2491
2857
  if (!autoOpacityOnZoom || typeof mapZoom !== "number") {
2492
2858
  return effectiveStates;
2493
2859
  }
@@ -2586,7 +2952,7 @@ var ZenitLayerManager = ({
2586
2952
  },
2587
2953
  [layerFeatureCounts]
2588
2954
  );
2589
- const decoratedLayers = useMemo3(() => {
2955
+ const decoratedLayers = useMemo4(() => {
2590
2956
  return layers.map((entry) => ({
2591
2957
  ...entry,
2592
2958
  effective: effectiveStates.find((state) => state.layerId === entry.mapLayer.layerId),
@@ -2615,17 +2981,25 @@ var ZenitLayerManager = ({
2615
2981
  return String(a.mapLayer.layerId).localeCompare(String(b.mapLayer.layerId));
2616
2982
  });
2617
2983
  }, [effectiveStates, layers, resolveFeatureCount]);
2618
- const filterableLayers = useMemo3(() => {
2984
+ const hasPrefilters = useMemo4(() => {
2985
+ const candidates = [...mapLayers ?? [], ...map?.mapLayers ?? []];
2986
+ return candidates.some((layer) => {
2987
+ const record = layer;
2988
+ const applied = record.appliedFilters ?? record.prefilters ?? record.initialFilters ?? record.filters ?? (record.layerConfig?.appliedFilters ?? record.layerConfig?.prefilters);
2989
+ return !!applied && typeof applied === "object" && Object.keys(applied).length > 0;
2990
+ });
2991
+ }, [map?.mapLayers, mapLayers]);
2992
+ const filterableLayers = useMemo4(() => {
2619
2993
  return decoratedLayers.filter((entry) => {
2620
2994
  const prefilters = entry.mapLayer.layerConfig?.prefilters;
2621
2995
  return !!prefilters && Object.keys(prefilters).length > 0;
2622
2996
  });
2623
2997
  }, [decoratedLayers]);
2624
- const selectedFilterLayer = useMemo3(
2998
+ const selectedFilterLayer = useMemo4(
2625
2999
  () => filterableLayers.find((layer) => String(layer.mapLayer.layerId) === selectedFilterLayerId) ?? null,
2626
3000
  [filterableLayers, selectedFilterLayerId]
2627
3001
  );
2628
- const filterFields = useMemo3(() => {
3002
+ const filterFields = useMemo4(() => {
2629
3003
  const prefilters = selectedFilterLayer?.mapLayer.layerConfig?.prefilters;
2630
3004
  return prefilters ? Object.keys(prefilters) : [];
2631
3005
  }, [selectedFilterLayer]);
@@ -2687,6 +3061,11 @@ var ZenitLayerManager = ({
2687
3061
  setSelectedFilterValue("");
2688
3062
  }
2689
3063
  }, [filterFields, selectedFilterField]);
3064
+ useEffect6(() => {
3065
+ if (hasPrefilters && activeTab === "filters") {
3066
+ setActiveTab("layers");
3067
+ }
3068
+ }, [activeTab, hasPrefilters]);
2690
3069
  useEffect6(() => {
2691
3070
  if (activeTab !== "filters") return;
2692
3071
  if (!selectedFilterLayer || !selectedFilterField || !activeCatalogKey) return;
@@ -3087,7 +3466,7 @@ var ZenitLayerManager = ({
3087
3466
  ]
3088
3467
  }
3089
3468
  ),
3090
- /* @__PURE__ */ jsxs5(
3469
+ !hasPrefilters && /* @__PURE__ */ jsxs5(
3091
3470
  "button",
3092
3471
  {
3093
3472
  type: "button",
@@ -3104,8 +3483,9 @@ var ZenitLayerManager = ({
3104
3483
  )
3105
3484
  ] }),
3106
3485
  panelVisible && /* @__PURE__ */ jsxs5("div", { style: { padding: "12px 10px 18px", overflowY: "auto", flex: 1, minHeight: 0 }, children: [
3486
+ hasPrefilters && /* @__PURE__ */ jsx5("div", { className: "zlm-badge", style: { marginBottom: 10 }, children: "Este mapa ya incluye filtros preaplicados" }),
3107
3487
  activeTab === "layers" && renderLayerCards(),
3108
- activeTab === "filters" && /* @__PURE__ */ jsx5("div", { className: "zlm-filter-panel", style: { display: "flex", flexDirection: "column", gap: 12, background: "#fff", border: "1px solid #e2e8f0", borderRadius: 12, padding: 12 }, children: !filterableLayers.length ? /* @__PURE__ */ jsx5("div", { style: { color: "#64748b", fontSize: 13 }, children: "No hay filtros disponibles para las capas de este mapa." }) : /* @__PURE__ */ jsxs5(Fragment3, { children: [
3488
+ !hasPrefilters && activeTab === "filters" && /* @__PURE__ */ jsx5("div", { className: "zlm-filter-panel", style: { display: "flex", flexDirection: "column", gap: 12, background: "#fff", border: "1px solid #e2e8f0", borderRadius: 12, padding: 12 }, children: !filterableLayers.length ? /* @__PURE__ */ jsx5("div", { style: { color: "#64748b", fontSize: 13 }, children: "No hay filtros disponibles para las capas de este mapa." }) : /* @__PURE__ */ jsxs5(Fragment3, { children: [
3109
3489
  filterableLayers.length > 1 && /* @__PURE__ */ jsxs5("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: 12, color: "#475569" }, children: [
3110
3490
  "Capa",
3111
3491
  /* @__PURE__ */ jsx5(
@@ -3223,7 +3603,7 @@ var ZenitFeatureFilterPanel = ({
3223
3603
  };
3224
3604
 
3225
3605
  // src/react/ai/FloatingChatBox.tsx
3226
- import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef8, useState as useState5 } from "react";
3606
+ import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef8, useState as useState5 } from "react";
3227
3607
  import { createPortal as createPortal3 } from "react-dom";
3228
3608
 
3229
3609
  // src/react/hooks/use-chat.ts
@@ -3948,7 +4328,7 @@ var FloatingChatBox = ({
3948
4328
  const messagesEndRef = useRef8(null);
3949
4329
  const messagesContainerRef = useRef8(null);
3950
4330
  const chatBoxRef = useRef8(null);
3951
- const chatConfig = useMemo4(() => {
4331
+ const chatConfig = useMemo5(() => {
3952
4332
  if (!baseUrl) return void 0;
3953
4333
  return { baseUrl, accessToken, getAccessToken };
3954
4334
  }, [accessToken, baseUrl, getAccessToken]);
@@ -4447,4 +4827,4 @@ export {
4447
4827
  useSendMessageStream,
4448
4828
  FloatingChatBox
4449
4829
  };
4450
- //# sourceMappingURL=chunk-R5WJ7K2D.mjs.map
4830
+ //# sourceMappingURL=chunk-J2YWF2TS.mjs.map