zenit-sdk 0.0.3 → 0.0.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,5 +1,5 @@
1
1
  // src/react/ZenitMap.tsx
2
- import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, forwardRef } from "react";
2
+ import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, forwardRef } from "react";
3
3
  import { GeoJSON, MapContainer, Marker, TileLayer, ZoomControl, useMap } from "react-leaflet";
4
4
  import L from "leaflet";
5
5
 
@@ -297,6 +297,121 @@ function getFeatureLayerId(feature) {
297
297
  function escapeHtml(value) {
298
298
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
299
299
  }
300
+ var DESCRIPTION_KEYS = /* @__PURE__ */ new Set(["descripcion", "description"]);
301
+ function normalizeDescriptionValue(value) {
302
+ if (value === void 0 || value === null) return null;
303
+ if (typeof value === "string") {
304
+ const trimmed = value.trim();
305
+ return trimmed ? trimmed : null;
306
+ }
307
+ if (typeof value === "number" || typeof value === "boolean") {
308
+ return String(value);
309
+ }
310
+ return null;
311
+ }
312
+ function extractDescriptionValue(properties) {
313
+ if (!properties) return null;
314
+ const matches = Object.entries(properties).find(
315
+ ([key]) => DESCRIPTION_KEYS.has(key.toLowerCase())
316
+ );
317
+ if (!matches) return null;
318
+ return normalizeDescriptionValue(matches[1]);
319
+ }
320
+ function safeJsonStringify(value) {
321
+ try {
322
+ const json = JSON.stringify(value, null, 2);
323
+ if (json !== void 0) return json;
324
+ } catch {
325
+ }
326
+ return String(value);
327
+ }
328
+ function renderPropertyValue(value) {
329
+ if (value === null || value === void 0) {
330
+ return '<span class="prop-empty">\u2014</span>';
331
+ }
332
+ if (typeof value === "object") {
333
+ const json = safeJsonStringify(value);
334
+ return `<pre class="prop-json">${escapeHtml(json)}</pre>`;
335
+ }
336
+ return `<span class="prop-text">${escapeHtml(String(value))}</span>`;
337
+ }
338
+ var POPUP_TITLE_KEYS = ["name", "title", "nombre", "label", "id"];
339
+ var POPUP_CHIP_KEYS = /* @__PURE__ */ new Set(["churn", "color", "sector"]);
340
+ function isChipValue(value) {
341
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
342
+ }
343
+ function getSanitizedChipColor(value) {
344
+ if (typeof value !== "string") return null;
345
+ const trimmed = value.trim();
346
+ if (/^#([0-9a-fA-F]{3}){1,2}$/.test(trimmed)) {
347
+ return trimmed;
348
+ }
349
+ if (/^rgb\((\s*\d+\s*,){2}\s*\d+\s*\)$/.test(trimmed)) {
350
+ return trimmed;
351
+ }
352
+ if (/^rgba\((\s*\d+\s*,){3}\s*(0|1|0?\.\d+)\s*\)$/.test(trimmed)) {
353
+ return trimmed;
354
+ }
355
+ return null;
356
+ }
357
+ function getPopupTitle(entries) {
358
+ for (const candidate of POPUP_TITLE_KEYS) {
359
+ const match = entries.find((entry) => entry.normalized === candidate);
360
+ if (!match || match.value === null || match.value === void 0) continue;
361
+ const value = String(match.value).trim();
362
+ if (!value) continue;
363
+ return { title: value, key: match.key };
364
+ }
365
+ return null;
366
+ }
367
+ function renderProperties(properties) {
368
+ const description = extractDescriptionValue(properties);
369
+ const entries = Object.entries(properties).filter(([key]) => !DESCRIPTION_KEYS.has(key.toLowerCase())).map(([key, value]) => ({
370
+ key,
371
+ value,
372
+ normalized: key.trim().toLowerCase()
373
+ }));
374
+ if (!description && entries.length === 0) return "";
375
+ const titleEntry = getPopupTitle(entries);
376
+ const titleText = titleEntry?.title ?? "Detalle del elemento";
377
+ const chipEntries = entries.filter(
378
+ (entry) => POPUP_CHIP_KEYS.has(entry.normalized) && isChipValue(entry.value)
379
+ );
380
+ const listEntries = entries.filter((entry) => {
381
+ if (titleEntry && entry.key === titleEntry.key) return false;
382
+ if (chipEntries.find((chip) => chip.key === entry.key)) return false;
383
+ return true;
384
+ });
385
+ const descriptionHtml = description ? `<div class="popup-description">${escapeHtml(description)}</div>` : "";
386
+ const chipsHtml = chipEntries.length ? `<div class="popup-chip-row">${chipEntries.map((entry) => {
387
+ const label = escapeHtml(entry.key.replace(/_/g, " "));
388
+ const value = escapeHtml(String(entry.value));
389
+ const color = getSanitizedChipColor(entry.value);
390
+ const colorStyle = color ? ` style="--chip-color: ${color}"` : "";
391
+ return `<span class="popup-chip"${colorStyle}><span class="popup-chip-label">${label}</span><span class="popup-chip-value">${value}</span></span>`;
392
+ }).join("")}</div>` : "";
393
+ const rowsHtml = listEntries.map((entry) => {
394
+ const label = escapeHtml(entry.key.replace(/_/g, " "));
395
+ const valueHtml = renderPropertyValue(entry.value);
396
+ return `<div class="prop-key">${label}</div><div class="prop-value">${valueHtml}</div>`;
397
+ }).join("");
398
+ const listHtml = rowsHtml ? `<div class="prop-list">${rowsHtml}</div>` : "";
399
+ return `
400
+ <div class="feature-popup">
401
+ <div class="feature-popup-card">
402
+ <div class="feature-popup-header">
403
+ <p class="popup-eyebrow">Informaci\xF3n</p>
404
+ <h3 class="popup-title">${escapeHtml(titleText)}</h3>
405
+ </div>
406
+ <div class="feature-popup-body">
407
+ ${descriptionHtml}
408
+ ${chipsHtml}
409
+ ${listHtml}
410
+ </div>
411
+ </div>
412
+ </div>
413
+ `;
414
+ }
300
415
  function withAlpha(color, alpha) {
301
416
  const trimmed = color.trim();
302
417
  if (trimmed.startsWith("#")) {
@@ -365,40 +480,39 @@ function getFeatureStyleOverrides(feature) {
365
480
  function buildFeaturePopupHtml(feature) {
366
481
  const properties = feature?.properties;
367
482
  if (!properties) return null;
368
- const layerName = properties.layerName ?? properties.layer_name ?? properties.name;
369
- const descripcion = properties.descripcion ?? properties.description;
370
- const reservedKeys = /* @__PURE__ */ new Set([
371
- "_style",
372
- "layerId",
373
- "layer_id",
374
- "__zenit_layerId",
375
- "layerName",
376
- "layer_name",
377
- "name",
378
- "descripcion",
379
- "description"
380
- ]);
381
- const extraEntries = Object.entries(properties).filter(([key, value]) => {
382
- if (reservedKeys.has(key)) return false;
383
- return ["string", "number", "boolean"].includes(typeof value);
384
- }).slice(0, 5);
385
- if (!layerName && !descripcion && extraEntries.length === 0) return null;
386
- const parts = [];
387
- if (layerName) {
388
- parts.push(`<div style="font-weight:600;margin-bottom:4px;">${escapeHtml(layerName)}</div>`);
389
- }
390
- if (descripcion) {
391
- parts.push(`<div style="margin-bottom:6px;">${escapeHtml(descripcion)}</div>`);
392
- }
393
- if (extraEntries.length > 0) {
394
- const rows = extraEntries.map(([key, value]) => {
395
- const label = escapeHtml(key.replace(/_/g, " "));
396
- const val = escapeHtml(String(value));
397
- return `<div><strong>${label}:</strong> ${val}</div>`;
398
- }).join("");
399
- parts.push(`<div style="font-size:12px;line-height:1.4;">${rows}</div>`);
483
+ const rendered = renderProperties(properties);
484
+ return rendered ? rendered : null;
485
+ }
486
+ var POINT_GEOMETRY_TYPES = /* @__PURE__ */ new Set(["Point", "MultiPoint"]);
487
+ function getGeometryType(feature) {
488
+ const t = feature?.geometry?.type;
489
+ return typeof t === "string" ? t : null;
490
+ }
491
+ function isPointGeometry(feature) {
492
+ const geometryType = getGeometryType(feature);
493
+ return geometryType !== null && POINT_GEOMETRY_TYPES.has(geometryType);
494
+ }
495
+ function isNonPointGeometry(feature) {
496
+ const geometryType = getGeometryType(feature);
497
+ return geometryType !== null && !POINT_GEOMETRY_TYPES.has(geometryType);
498
+ }
499
+ function buildFeatureCollection(features) {
500
+ return {
501
+ type: "FeatureCollection",
502
+ features
503
+ };
504
+ }
505
+ function pickIntersectFeature(baseFeature, candidates) {
506
+ if (!Array.isArray(candidates) || candidates.length === 0) return null;
507
+ const baseId = baseFeature?.id;
508
+ if (baseId !== void 0 && baseId !== null) {
509
+ const matchById = candidates.find((candidate) => candidate?.id === baseId);
510
+ if (matchById) return matchById;
400
511
  }
401
- return `<div>${parts.join("")}</div>`;
512
+ const matchWithDescription = candidates.find(
513
+ (candidate) => extractDescriptionValue(candidate?.properties)
514
+ );
515
+ return matchWithDescription ?? candidates[0];
402
516
  }
403
517
  function normalizeCenterTuple(center) {
404
518
  if (!center) return null;
@@ -519,6 +633,7 @@ var ZenitMap = forwardRef(({
519
633
  const [loadingMap, setLoadingMap] = useState(false);
520
634
  const [mapError, setMapError] = useState(null);
521
635
  const [mapInstance, setMapInstance] = useState(null);
636
+ const [panesReady, setPanesReady] = useState(false);
522
637
  const [currentZoom, setCurrentZoom] = useState(initialZoom ?? DEFAULT_ZOOM);
523
638
  const [isMobile, setIsMobile] = useState(() => {
524
639
  if (typeof window === "undefined") return false;
@@ -833,16 +948,23 @@ var ZenitMap = forwardRef(({
833
948
  (targetMap, targetLayers) => {
834
949
  const baseZIndex = 400;
835
950
  targetLayers.forEach((layer) => {
836
- const paneName = `zenit-layer-${layer.layerId}`;
837
- const pane = targetMap.getPane(paneName) ?? targetMap.createPane(paneName);
838
951
  const order = Number.isFinite(layer.displayOrder) ? layer.displayOrder : 0;
839
- pane.style.zIndex = String(baseZIndex + order);
952
+ const fillPaneName = `zenit-layer-${layer.layerId}-fill`;
953
+ const pointPaneName = `zenit-layer-${layer.layerId}-points`;
954
+ const labelPaneName = `zenit-layer-${layer.layerId}-labels`;
955
+ const fillPane = targetMap.getPane(fillPaneName) ?? targetMap.createPane(fillPaneName);
956
+ const pointPane = targetMap.getPane(pointPaneName) ?? targetMap.createPane(pointPaneName);
957
+ const labelPane = targetMap.getPane(labelPaneName) ?? targetMap.createPane(labelPaneName);
958
+ fillPane.style.zIndex = String(baseZIndex + order);
959
+ pointPane.style.zIndex = String(baseZIndex + order + 1e3);
960
+ labelPane.style.zIndex = String(baseZIndex + order + 2e3);
840
961
  });
841
962
  },
842
963
  []
843
964
  );
844
965
  const handleMapReady = useCallback(
845
966
  (instance) => {
967
+ setPanesReady(false);
846
968
  setMapInstance(instance);
847
969
  onMapReady?.(instance);
848
970
  },
@@ -850,6 +972,7 @@ var ZenitMap = forwardRef(({
850
972
  );
851
973
  useEffect(() => {
852
974
  if (!mapInstance) {
975
+ setPanesReady(false);
853
976
  return;
854
977
  }
855
978
  if (orderedLayers.length === 0) {
@@ -860,6 +983,11 @@ var ZenitMap = forwardRef(({
860
983
  displayOrder: layer.displayOrder
861
984
  }));
862
985
  ensureLayerPanes(mapInstance, layerTargets);
986
+ const first = layerTargets[0];
987
+ const testPane = mapInstance.getPane(`zenit-layer-${first.layerId}-labels`);
988
+ if (testPane) {
989
+ setPanesReady(true);
990
+ }
863
991
  }, [mapInstance, orderedLayers, ensureLayerPanes]);
864
992
  const overlayOnEachFeature = useMemo(() => {
865
993
  return (feature, layer) => {
@@ -877,7 +1005,14 @@ var ZenitMap = forwardRef(({
877
1005
  if (featureInfoMode === "popup") {
878
1006
  const content = buildFeaturePopupHtml(feature);
879
1007
  if (content) {
880
- layer.bindPopup(content, { maxWidth: 320 });
1008
+ layer.bindPopup(content, {
1009
+ maxWidth: 420,
1010
+ minWidth: 280,
1011
+ className: "zenit-feature-popup-shell",
1012
+ autoPan: true,
1013
+ closeButton: true,
1014
+ offset: L.point(0, -24)
1015
+ });
881
1016
  }
882
1017
  }
883
1018
  if (isPointFeature && layer.bindTooltip) {
@@ -888,7 +1023,37 @@ var ZenitMap = forwardRef(({
888
1023
  className: "zenit-map-tooltip"
889
1024
  });
890
1025
  }
891
- layer.on("click", () => onFeatureClick?.(feature, layerId));
1026
+ layer.on("click", () => {
1027
+ if (featureInfoMode === "popup" && client && layerId !== void 0 && !extractDescriptionValue(feature?.properties) && feature?.geometry) {
1028
+ const trackedFeature = feature;
1029
+ if (!trackedFeature.__zenit_popup_loaded) {
1030
+ trackedFeature.__zenit_popup_loaded = true;
1031
+ client.layers.getLayerGeoJsonIntersect({
1032
+ id: layerId,
1033
+ geometry: feature.geometry
1034
+ }).then((response) => {
1035
+ const candidates = response.data?.features ?? [];
1036
+ const resolved = pickIntersectFeature(feature, candidates);
1037
+ if (!resolved?.properties) return;
1038
+ const mergedProperties = {
1039
+ ...trackedFeature.properties ?? {},
1040
+ ...resolved.properties
1041
+ };
1042
+ trackedFeature.properties = mergedProperties;
1043
+ const updatedHtml = buildFeaturePopupHtml({
1044
+ ...feature,
1045
+ properties: mergedProperties
1046
+ });
1047
+ if (updatedHtml && layer.setPopupContent) {
1048
+ layer.setPopupContent(updatedHtml);
1049
+ }
1050
+ }).catch(() => {
1051
+ trackedFeature.__zenit_popup_loaded = false;
1052
+ });
1053
+ }
1054
+ }
1055
+ onFeatureClick?.(feature, layerId);
1056
+ });
892
1057
  layer.on("mouseover", () => {
893
1058
  if (layer instanceof L.Path && originalStyle) {
894
1059
  layer.setStyle({
@@ -912,7 +1077,7 @@ var ZenitMap = forwardRef(({
912
1077
  }
913
1078
  });
914
1079
  };
915
- }, [featureInfoMode, onFeatureClick, onFeatureHover]);
1080
+ }, [client, featureInfoMode, onFeatureClick, onFeatureHover]);
916
1081
  const buildLayerStyle = (layerId, baseOpacity, feature, layerType) => {
917
1082
  const style = resolveLayerStyle(layerId);
918
1083
  const featureStyleOverrides = getFeatureStyleOverrides(feature);
@@ -1093,22 +1258,50 @@ var ZenitMap = forwardRef(({
1093
1258
  /* @__PURE__ */ jsx(ZoomBasedOpacityHandler, { onZoomChange: handleZoomChange }),
1094
1259
  orderedLayers.map((layerState) => {
1095
1260
  const baseOpacity = layerState.effective?.baseOpacity ?? layerState.effective?.opacity ?? 1;
1096
- const paneName = `zenit-layer-${layerState.mapLayer.layerId}`;
1261
+ const fillPaneName = `zenit-layer-${layerState.mapLayer.layerId}-fill`;
1262
+ const pointsPaneName = `zenit-layer-${layerState.mapLayer.layerId}-points`;
1263
+ const labelPaneName = `zenit-layer-${layerState.mapLayer.layerId}-labels`;
1097
1264
  const layerType = layerState.layer?.layerType ?? layerState.mapLayer.layerType ?? void 0;
1098
- return /* @__PURE__ */ jsx(
1099
- GeoJSON,
1100
- {
1101
- data: layerState.data,
1102
- pane: mapInstance?.getPane(paneName) ? paneName : void 0,
1103
- style: (feature) => buildLayerStyle(layerState.mapLayer.layerId, baseOpacity, feature, layerType),
1104
- pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
1105
- radius: isMobile ? 8 : 6,
1106
- ...buildLayerStyle(layerState.mapLayer.layerId, baseOpacity, feature, layerType)
1107
- }),
1108
- onEachFeature: overlayOnEachFeature
1109
- },
1110
- layerState.mapLayer.layerId.toString()
1111
- );
1265
+ const data = layerState.data?.features ?? [];
1266
+ const fillFeatures = data.filter(isNonPointGeometry);
1267
+ const pointFeatures = data.filter(isPointGeometry);
1268
+ const fillData = fillFeatures.length > 0 ? buildFeatureCollection(fillFeatures) : null;
1269
+ const pointsData = pointFeatures.length > 0 ? buildFeatureCollection(pointFeatures) : null;
1270
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
1271
+ fillData && /* @__PURE__ */ jsx(
1272
+ GeoJSON,
1273
+ {
1274
+ data: fillData,
1275
+ pane: panesReady && mapInstance?.getPane(fillPaneName) ? fillPaneName : void 0,
1276
+ style: (feature) => buildLayerStyle(layerState.mapLayer.layerId, baseOpacity, feature, layerType),
1277
+ onEachFeature: overlayOnEachFeature
1278
+ }
1279
+ ),
1280
+ pointsData && /* @__PURE__ */ jsx(
1281
+ GeoJSON,
1282
+ {
1283
+ data: pointsData,
1284
+ pane: panesReady && mapInstance?.getPane(pointsPaneName) ? pointsPaneName : void 0,
1285
+ pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
1286
+ radius: isMobile ? 8 : 6,
1287
+ ...buildLayerStyle(layerState.mapLayer.layerId, baseOpacity, feature, layerType)
1288
+ }),
1289
+ onEachFeature: overlayOnEachFeature
1290
+ }
1291
+ ),
1292
+ panesReady && mapInstance?.getPane(labelPaneName) ? labelMarkers.filter(
1293
+ (marker) => String(marker.layerId) === String(layerState.mapLayer.layerId)
1294
+ ).map((marker) => /* @__PURE__ */ jsx(
1295
+ Marker,
1296
+ {
1297
+ position: marker.position,
1298
+ icon: buildLabelIcon(marker.label, marker.opacity, marker.color),
1299
+ interactive: false,
1300
+ pane: labelPaneName
1301
+ },
1302
+ marker.key
1303
+ )) : null
1304
+ ] }, layerState.mapLayer.layerId.toString());
1112
1305
  }),
1113
1306
  overlayGeojson && /* @__PURE__ */ jsx(
1114
1307
  GeoJSON,
@@ -1118,16 +1311,7 @@ var ZenitMap = forwardRef(({
1118
1311
  onEachFeature: overlayOnEachFeature
1119
1312
  },
1120
1313
  "zenit-overlay-geojson"
1121
- ),
1122
- labelMarkers.map((marker) => /* @__PURE__ */ jsx(
1123
- Marker,
1124
- {
1125
- position: marker.position,
1126
- icon: buildLabelIcon(marker.label, marker.opacity, marker.color),
1127
- interactive: false
1128
- },
1129
- marker.key
1130
- ))
1314
+ )
1131
1315
  ]
1132
1316
  },
1133
1317
  String(mapId)
@@ -2543,9 +2727,19 @@ var FloatingChatBox = ({
2543
2727
  getAccessToken,
2544
2728
  onActionClick,
2545
2729
  onOpenChange,
2546
- hideButton
2730
+ hideButton,
2731
+ open: openProp
2547
2732
  }) => {
2548
- const [open, setOpen] = useState4(false);
2733
+ const isControlled = openProp !== void 0;
2734
+ const [internalOpen, setInternalOpen] = useState4(false);
2735
+ const open = isControlled ? openProp : internalOpen;
2736
+ const setOpen = useCallback3((value) => {
2737
+ const newValue = typeof value === "function" ? value(open) : value;
2738
+ if (!isControlled) {
2739
+ setInternalOpen(newValue);
2740
+ }
2741
+ onOpenChange?.(newValue);
2742
+ }, [isControlled, open, onOpenChange]);
2549
2743
  const [expanded, setExpanded] = useState4(false);
2550
2744
  const [messages, setMessages] = useState4([]);
2551
2745
  const [inputValue, setInputValue] = useState4("");
@@ -2562,9 +2756,6 @@ var FloatingChatBox = ({
2562
2756
  }, [accessToken, baseUrl, getAccessToken]);
2563
2757
  const { sendMessage: sendMessage2, isStreaming, streamingText, completeResponse } = useSendMessageStream(chatConfig);
2564
2758
  const canSend = Boolean(mapId) && Boolean(baseUrl) && inputValue.trim().length > 0 && !isStreaming;
2565
- useEffect3(() => {
2566
- onOpenChange?.(open);
2567
- }, [open, onOpenChange]);
2568
2759
  useEffect3(() => {
2569
2760
  if (open && isMobile) {
2570
2761
  setExpanded(true);
@@ -2717,6 +2908,13 @@ var FloatingChatBox = ({
2717
2908
  ] }, index)) })
2718
2909
  ] });
2719
2910
  };
2911
+ const handleActionClick = useCallback3((action) => {
2912
+ if (isStreaming) return;
2913
+ setOpen(false);
2914
+ requestAnimationFrame(() => {
2915
+ onActionClick?.(action);
2916
+ });
2917
+ }, [isStreaming, setOpen, onActionClick]);
2720
2918
  const renderActions = (response) => {
2721
2919
  if (!response?.suggestedActions?.length) return null;
2722
2920
  return /* @__PURE__ */ jsxs4("div", { style: styles.actionsSection, children: [
@@ -2730,7 +2928,7 @@ var FloatingChatBox = ({
2730
2928
  opacity: isStreaming ? 0.5 : 1,
2731
2929
  cursor: isStreaming ? "not-allowed" : "pointer"
2732
2930
  },
2733
- onClick: () => !isStreaming && onActionClick?.(action),
2931
+ onClick: () => handleActionClick(action),
2734
2932
  disabled: isStreaming,
2735
2933
  onMouseEnter: (e) => {
2736
2934
  if (!isStreaming) {
@@ -3057,4 +3255,4 @@ export {
3057
3255
  useSendMessageStream,
3058
3256
  FloatingChatBox
3059
3257
  };
3060
- //# sourceMappingURL=chunk-R73LRYVJ.mjs.map
3258
+ //# sourceMappingURL=chunk-LX2N2BXV.mjs.map