@trackunit/react-map 0.2.208 → 0.2.210-alpha-7afddbbe4cc.0

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.
package/index.cjs.js CHANGED
@@ -5888,10 +5888,10 @@ const useFitToContent = (api, handles, options) => {
5888
5888
  };
5889
5889
 
5890
5890
  /**
5891
- * Single dev-warning channel for `useHeatmap` (invalid data).
5891
+ * Single dev-warning channel for `useHeatmap` (invalid data, invalid style).
5892
5892
  */
5893
5893
  const warnHeatmap = (message, ...details) => {
5894
- // eslint-disable-next-line no-console -- Intentional: the one heatmap warning channel; invalid input is dropped and must stay visible
5894
+ // eslint-disable-next-line no-console -- Intentional: the one heatmap warning channel; invalid input is dropped or defaulted and must stay visible
5895
5895
  console.warn(`[useHeatmap] ${message}`, ...details);
5896
5896
  };
5897
5897
 
@@ -5938,6 +5938,99 @@ const normalizeHeatmapData = ({ data, getPosition, getWeight, }) => {
5938
5938
  };
5939
5939
  };
5940
5940
 
5941
+ const HEX_PATTERN = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
5942
+ const RGB_PATTERN = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i;
5943
+ const parseHex = (hex) => {
5944
+ const expanded = hex.length === 3
5945
+ ? hex
5946
+ .split("")
5947
+ .map(char => char + char)
5948
+ .join("")
5949
+ : hex;
5950
+ const channel = (index) => parseInt(expanded.slice(index * 2, index * 2 + 2), 16);
5951
+ const alpha = expanded.length === 8 ? channel(3) / 255 : 1;
5952
+ return [channel(0), channel(1), channel(2), alpha];
5953
+ };
5954
+ const isChannel = (value) => Number.isFinite(value) && value >= 0 && value <= 255;
5955
+ /**
5956
+ * Parses a CSS colour into `[r, g, b, a]`.
5957
+ *
5958
+ * Supports `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()` and `rgba()`; returns `null` otherwise.
5959
+ */
5960
+ const parseHeatmapColor = (input) => {
5961
+ const color = input.trim();
5962
+ const hexMatch = HEX_PATTERN.exec(color);
5963
+ if (hexMatch?.[1] !== undefined)
5964
+ return parseHex(hexMatch[1]);
5965
+ const rgbMatch = RGB_PATTERN.exec(color);
5966
+ if (rgbMatch === null)
5967
+ return null;
5968
+ const red = Number(rgbMatch[1]);
5969
+ const green = Number(rgbMatch[2]);
5970
+ const blue = Number(rgbMatch[3]);
5971
+ const alpha = rgbMatch[4] === undefined ? 1 : Number(rgbMatch[4]);
5972
+ if (!isChannel(red) || !isChannel(green) || !isChannel(blue))
5973
+ return null;
5974
+ if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1)
5975
+ return null;
5976
+ return [red, green, blue, alpha];
5977
+ };
5978
+
5979
+ /** Movement page gradient (legacy Google heatmap), low to high density. */
5980
+ const DEFAULT_HEATMAP_COLOR_RANGE = [
5981
+ "rgba(0, 255, 255, 0)",
5982
+ "#0000ff",
5983
+ "#00ffff",
5984
+ "#00ff00",
5985
+ "#ffff00",
5986
+ "#ff0000",
5987
+ ];
5988
+ const DEFAULT_PARSED_COLOR_RANGE = DEFAULT_HEATMAP_COLOR_RANGE.map(parseHeatmapColor).filter(sharedUtils.nonNullable);
5989
+ const resolveColorRange = (colorRange) => {
5990
+ if (colorRange === undefined)
5991
+ return DEFAULT_PARSED_COLOR_RANGE;
5992
+ const parsed = colorRange.map(parseHeatmapColor);
5993
+ const valid = parsed.filter(sharedUtils.nonNullable);
5994
+ if (colorRange.length >= 2 && valid.length === parsed.length)
5995
+ return valid;
5996
+ warnHeatmap("colorRange needs at least two supported colours (#rgb, #rrggbb, #rrggbbaa, rgb(), rgba()); using the default.", colorRange);
5997
+ return DEFAULT_PARSED_COLOR_RANGE;
5998
+ };
5999
+ const HEATMAP_STYLE_RULES = {
6000
+ radiusPixels: { defaultValue: 30, min: 1, max: 100 },
6001
+ intensity: { defaultValue: 1, min: 0, max: Number.POSITIVE_INFINITY },
6002
+ opacity: { defaultValue: 1, min: 0, max: 1 },
6003
+ };
6004
+ /**
6005
+ * Applies defaults and supported ranges so both adapters receive identical style values.
6006
+ *
6007
+ * Finite values are clamped; non-finite values fall back to the default with one warning.
6008
+ */
6009
+ const resolveHeatmapStyle = (style) => {
6010
+ const nonFiniteKeys = [];
6011
+ const resolveValue = (key) => {
6012
+ const rule = HEATMAP_STYLE_RULES[key];
6013
+ const value = style?.[key];
6014
+ if (value === undefined)
6015
+ return rule.defaultValue;
6016
+ if (!Number.isFinite(value)) {
6017
+ nonFiniteKeys.push(key);
6018
+ return rule.defaultValue;
6019
+ }
6020
+ return Math.min(rule.max, Math.max(rule.min, value));
6021
+ };
6022
+ const resolved = {
6023
+ radiusPixels: resolveValue("radiusPixels"),
6024
+ intensity: resolveValue("intensity"),
6025
+ opacity: resolveValue("opacity"),
6026
+ colorRange: resolveColorRange(style?.colorRange),
6027
+ };
6028
+ if (nonFiniteKeys.length > 0) {
6029
+ warnHeatmap(`Non-finite style value(s) for ${nonFiniteKeys.join(", ")} replaced with defaults.`);
6030
+ }
6031
+ return resolved;
6032
+ };
6033
+
5941
6034
  const EMPTY_CONTROLS$2 = {};
5942
6035
  /**
5943
6036
  * Weighted point heatmap layer handle (provider-agnostic).
@@ -5958,11 +6051,18 @@ const useHeatmap = (options) => {
5958
6051
  setPrevData(data);
5959
6052
  setNormalized(normalizeHeatmapData({ data, getPosition, getWeight }));
5960
6053
  }
5961
- const resolvedStyle = react.useMemo(() => ({
5962
- radiusPixels: style?.radiusPixels ?? 30,
5963
- intensity: style?.intensity ?? 1,
5964
- opacity: style?.opacity ?? 1,
5965
- }), [style?.radiusPixels, style?.intensity, style?.opacity]);
6054
+ // Holds the last structurally-equal reference so an inline `colorRange` literal
6055
+ // doesn't change the handle every render; the original array is validated as-is.
6056
+ const [colorRange, setColorRange] = react.useState(style?.colorRange);
6057
+ if (style?.colorRange !== colorRange && !esToolkit.isEqual(style?.colorRange, colorRange)) {
6058
+ setColorRange(style?.colorRange);
6059
+ }
6060
+ const resolvedStyle = react.useMemo(() => resolveHeatmapStyle({
6061
+ radiusPixels: style?.radiusPixels,
6062
+ intensity: style?.intensity,
6063
+ opacity: style?.opacity,
6064
+ colorRange,
6065
+ }), [style?.radiusPixels, style?.intensity, style?.opacity, colorRange]);
5966
6066
  const getBounds = react.useMemo(() => createLazyGetter(() => computeBoundsFromPositions(normalized.positions)), [normalized.positions]);
5967
6067
  const counts = react.useMemo(() => ({ total: data.length, points: normalized.features.features.length }), [data.length, normalized.features.features.length]);
5968
6068
  return react.useMemo(() => ({
package/index.esm.js CHANGED
@@ -12,7 +12,7 @@ import { isEqual } from 'es-toolkit';
12
12
  import { useGeolocation, useErrorHandlerOrNull } from '@trackunit/react-core-hooks';
13
13
  import { Checkbox, ToggleSwitch, RadioGroup as RadioGroup$1, RadioItem, FormGroup, Search as Search$1, SelectField, BaseSelect } from '@trackunit/react-form-components';
14
14
  import { z } from 'zod';
15
- import { readLocalStorageValue } from '@trackunit/shared-utils';
15
+ import { readLocalStorageValue, nonNullable } from '@trackunit/shared-utils';
16
16
  import { PieChart } from 'react-minimal-pie-chart';
17
17
  import { tailwindPalette } from '@trackunit/ui-design-tokens';
18
18
  import { lngLatToMercatorPxWS, validateBboxWithFallback, extractPositionsFromGeometry, geoJsonPositionSchema, validateFeatureCollection, EMPTY_FEATURE_COLLECTION, splitAntimeridianCrossingFeatures, validateBbox, projectPolygonalToWebMercator, geoJsonPolygonDifference, unprojectPolygonalFromWebMercator, geoJsonPolygonSchema, geoJsonMultiPolygonSchema, distanceToGeoJsonPolygonBoundary, getGeoJsonPolygonIntersection, isFullyContainedInGeometry, isGeoJsonPointInPolygon, isBboxInsideFeatureCollection, extractEdges, computeGeometryCentroid, mercatorPxToLngLatWS, isPositionInsideRing, lngLatToWebMercatorPx, edgePixelLength, isFullyContainedInGeoJsonGeometry } from '@trackunit/geo-json-utils';
@@ -5887,10 +5887,10 @@ const useFitToContent = (api, handles, options) => {
5887
5887
  };
5888
5888
 
5889
5889
  /**
5890
- * Single dev-warning channel for `useHeatmap` (invalid data).
5890
+ * Single dev-warning channel for `useHeatmap` (invalid data, invalid style).
5891
5891
  */
5892
5892
  const warnHeatmap = (message, ...details) => {
5893
- // eslint-disable-next-line no-console -- Intentional: the one heatmap warning channel; invalid input is dropped and must stay visible
5893
+ // eslint-disable-next-line no-console -- Intentional: the one heatmap warning channel; invalid input is dropped or defaulted and must stay visible
5894
5894
  console.warn(`[useHeatmap] ${message}`, ...details);
5895
5895
  };
5896
5896
 
@@ -5937,6 +5937,99 @@ const normalizeHeatmapData = ({ data, getPosition, getWeight, }) => {
5937
5937
  };
5938
5938
  };
5939
5939
 
5940
+ const HEX_PATTERN = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
5941
+ const RGB_PATTERN = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i;
5942
+ const parseHex = (hex) => {
5943
+ const expanded = hex.length === 3
5944
+ ? hex
5945
+ .split("")
5946
+ .map(char => char + char)
5947
+ .join("")
5948
+ : hex;
5949
+ const channel = (index) => parseInt(expanded.slice(index * 2, index * 2 + 2), 16);
5950
+ const alpha = expanded.length === 8 ? channel(3) / 255 : 1;
5951
+ return [channel(0), channel(1), channel(2), alpha];
5952
+ };
5953
+ const isChannel = (value) => Number.isFinite(value) && value >= 0 && value <= 255;
5954
+ /**
5955
+ * Parses a CSS colour into `[r, g, b, a]`.
5956
+ *
5957
+ * Supports `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()` and `rgba()`; returns `null` otherwise.
5958
+ */
5959
+ const parseHeatmapColor = (input) => {
5960
+ const color = input.trim();
5961
+ const hexMatch = HEX_PATTERN.exec(color);
5962
+ if (hexMatch?.[1] !== undefined)
5963
+ return parseHex(hexMatch[1]);
5964
+ const rgbMatch = RGB_PATTERN.exec(color);
5965
+ if (rgbMatch === null)
5966
+ return null;
5967
+ const red = Number(rgbMatch[1]);
5968
+ const green = Number(rgbMatch[2]);
5969
+ const blue = Number(rgbMatch[3]);
5970
+ const alpha = rgbMatch[4] === undefined ? 1 : Number(rgbMatch[4]);
5971
+ if (!isChannel(red) || !isChannel(green) || !isChannel(blue))
5972
+ return null;
5973
+ if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1)
5974
+ return null;
5975
+ return [red, green, blue, alpha];
5976
+ };
5977
+
5978
+ /** Movement page gradient (legacy Google heatmap), low to high density. */
5979
+ const DEFAULT_HEATMAP_COLOR_RANGE = [
5980
+ "rgba(0, 255, 255, 0)",
5981
+ "#0000ff",
5982
+ "#00ffff",
5983
+ "#00ff00",
5984
+ "#ffff00",
5985
+ "#ff0000",
5986
+ ];
5987
+ const DEFAULT_PARSED_COLOR_RANGE = DEFAULT_HEATMAP_COLOR_RANGE.map(parseHeatmapColor).filter(nonNullable);
5988
+ const resolveColorRange = (colorRange) => {
5989
+ if (colorRange === undefined)
5990
+ return DEFAULT_PARSED_COLOR_RANGE;
5991
+ const parsed = colorRange.map(parseHeatmapColor);
5992
+ const valid = parsed.filter(nonNullable);
5993
+ if (colorRange.length >= 2 && valid.length === parsed.length)
5994
+ return valid;
5995
+ warnHeatmap("colorRange needs at least two supported colours (#rgb, #rrggbb, #rrggbbaa, rgb(), rgba()); using the default.", colorRange);
5996
+ return DEFAULT_PARSED_COLOR_RANGE;
5997
+ };
5998
+ const HEATMAP_STYLE_RULES = {
5999
+ radiusPixels: { defaultValue: 30, min: 1, max: 100 },
6000
+ intensity: { defaultValue: 1, min: 0, max: Number.POSITIVE_INFINITY },
6001
+ opacity: { defaultValue: 1, min: 0, max: 1 },
6002
+ };
6003
+ /**
6004
+ * Applies defaults and supported ranges so both adapters receive identical style values.
6005
+ *
6006
+ * Finite values are clamped; non-finite values fall back to the default with one warning.
6007
+ */
6008
+ const resolveHeatmapStyle = (style) => {
6009
+ const nonFiniteKeys = [];
6010
+ const resolveValue = (key) => {
6011
+ const rule = HEATMAP_STYLE_RULES[key];
6012
+ const value = style?.[key];
6013
+ if (value === undefined)
6014
+ return rule.defaultValue;
6015
+ if (!Number.isFinite(value)) {
6016
+ nonFiniteKeys.push(key);
6017
+ return rule.defaultValue;
6018
+ }
6019
+ return Math.min(rule.max, Math.max(rule.min, value));
6020
+ };
6021
+ const resolved = {
6022
+ radiusPixels: resolveValue("radiusPixels"),
6023
+ intensity: resolveValue("intensity"),
6024
+ opacity: resolveValue("opacity"),
6025
+ colorRange: resolveColorRange(style?.colorRange),
6026
+ };
6027
+ if (nonFiniteKeys.length > 0) {
6028
+ warnHeatmap(`Non-finite style value(s) for ${nonFiniteKeys.join(", ")} replaced with defaults.`);
6029
+ }
6030
+ return resolved;
6031
+ };
6032
+
5940
6033
  const EMPTY_CONTROLS$2 = {};
5941
6034
  /**
5942
6035
  * Weighted point heatmap layer handle (provider-agnostic).
@@ -5957,11 +6050,18 @@ const useHeatmap = (options) => {
5957
6050
  setPrevData(data);
5958
6051
  setNormalized(normalizeHeatmapData({ data, getPosition, getWeight }));
5959
6052
  }
5960
- const resolvedStyle = useMemo(() => ({
5961
- radiusPixels: style?.radiusPixels ?? 30,
5962
- intensity: style?.intensity ?? 1,
5963
- opacity: style?.opacity ?? 1,
5964
- }), [style?.radiusPixels, style?.intensity, style?.opacity]);
6053
+ // Holds the last structurally-equal reference so an inline `colorRange` literal
6054
+ // doesn't change the handle every render; the original array is validated as-is.
6055
+ const [colorRange, setColorRange] = useState(style?.colorRange);
6056
+ if (style?.colorRange !== colorRange && !isEqual(style?.colorRange, colorRange)) {
6057
+ setColorRange(style?.colorRange);
6058
+ }
6059
+ const resolvedStyle = useMemo(() => resolveHeatmapStyle({
6060
+ radiusPixels: style?.radiusPixels,
6061
+ intensity: style?.intensity,
6062
+ opacity: style?.opacity,
6063
+ colorRange,
6064
+ }), [style?.radiusPixels, style?.intensity, style?.opacity, colorRange]);
5965
6065
  const getBounds = useMemo(() => createLazyGetter(() => computeBoundsFromPositions(normalized.positions)), [normalized.positions]);
5966
6066
  const counts = useMemo(() => ({ total: data.length, points: normalized.features.features.length }), [data.length, normalized.features.features.length]);
5967
6067
  return useMemo(() => ({
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/map/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "@trackunit/react-map",
3
- "version": "0.2.208",
3
+ "version": "0.2.210-alpha-7afddbbe4cc.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
7
7
  "node": ">=24.x"
8
8
  },
9
9
  "dependencies": {
10
- "@trackunit/react-components": "3.8.2",
11
- "@trackunit/css-class-variance-utilities": "2.3.5",
12
- "@trackunit/react-form-components": "2.10.9",
13
- "@trackunit/react-core-hooks": "1.25.5",
14
- "@trackunit/geo-json-utils": "1.18.5",
15
- "@trackunit/i18n-library-translation": "2.8.5",
10
+ "@trackunit/react-components": "3.8.4-alpha-7afddbbe4cc.0",
11
+ "@trackunit/css-class-variance-utilities": "2.3.7-alpha-7afddbbe4cc.0",
12
+ "@trackunit/react-form-components": "2.10.11-alpha-7afddbbe4cc.0",
13
+ "@trackunit/react-core-hooks": "1.25.7-alpha-7afddbbe4cc.0",
14
+ "@trackunit/geo-json-utils": "1.18.7-alpha-7afddbbe4cc.0",
15
+ "@trackunit/i18n-library-translation": "2.8.7-alpha-7afddbbe4cc.0",
16
16
  "react-minimal-pie-chart": "^8.4.0",
17
- "@trackunit/react-map-adapter-shared": "0.0.156",
18
- "@trackunit/react-map-color-utils": "0.0.131",
19
- "@trackunit/shared-utils": "1.19.5",
20
- "@trackunit/ui-design-tokens": "1.18.5",
17
+ "@trackunit/react-map-adapter-shared": "0.0.158-alpha-7afddbbe4cc.0",
18
+ "@trackunit/react-map-color-utils": "0.0.133-alpha-7afddbbe4cc.0",
19
+ "@trackunit/shared-utils": "1.19.7-alpha-7afddbbe4cc.0",
20
+ "@trackunit/ui-design-tokens": "1.18.7-alpha-7afddbbe4cc.0",
21
21
  "@floating-ui/react": "^0.26.25",
22
22
  "es-toolkit": "^1.52.0",
23
23
  "tailwind-merge": "^2.0.0",
package/src/index.d.ts CHANGED
@@ -16,15 +16,15 @@ export type { ControlStackItem } from "./controls/defineControlStack";
16
16
  export type { UseControlsAreas } from "./controls/types";
17
17
  export { useControls } from "./controls/useControls";
18
18
  export { useDefaultControls, type UseDefaultControlsOptions } from "./controls/useDefaultControls";
19
- export type { AppearanceAction, AppearanceOption, AppearanceOptions, MapAppearanceApi, PersistedAppearance, UseMapAppearanceControlsParams, } from "./appearance/appearanceTypes";
20
19
  export { DEFAULT_APPEARANCE_PERSISTENCE_KEY } from "./appearance/appearanceTypes";
20
+ export type { AppearanceAction, AppearanceOption, AppearanceOptions, MapAppearanceApi, PersistedAppearance, UseMapAppearanceControlsParams, } from "./appearance/appearanceTypes";
21
21
  export { useMapAppearanceControls } from "./appearance/useMapAppearanceControls";
22
22
  export { usePersistedAppearanceSeed, type MapAppearanceSeed, type UsePersistedAppearanceSeedParams, } from "./appearance/usePersistedAppearanceSeed";
23
23
  export type { ControlMiddleware, MiddlewareContext } from "./controls/renderingRules";
24
24
  export type { CategoryMetadata } from "./controls/collapseControls";
25
25
  export type { ClusterEntity, Entity, MapInteractionState, MarkerEntity, RouteEntity, ShapeEntity, ShapeType, } from "@trackunit/react-map-adapter-shared";
26
26
  export { geometryTypeToShapeType, INITIAL_INTERACTION_STATE } from "@trackunit/react-map-adapter-shared";
27
- export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterInfo, ClusterRenderState, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, FitParticipation, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, HeatmapLayerHandle, ImageOverlayHandle, LayerHandle, LayerMeta, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, MarkerLayerHandle, RenderConfig, RenderMedium, ResolutionContext, RouteLayerHandle, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeLayerHandle, ShapeOverlapConfig, ShapeStyle, ShapeStyleOverrides, ShapesUnderCursorHit, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "./layers/types";
27
+ export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterInfo, ClusterRenderState, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, FitParticipation, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, HeatmapLayerHandle, HeatmapRgbaColor, ImageOverlayHandle, LayerHandle, LayerMeta, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, MarkerLayerHandle, RenderConfig, RenderMedium, ResolutionContext, RouteLayerHandle, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeLayerHandle, ShapeOverlapConfig, ShapeStyle, ShapeStyleOverrides, ShapesUnderCursorHit, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "./layers/types";
28
28
  export { computeMarkerDomPortalZIndex } from "./layers/types";
29
29
  export { ANCHOR_SELECTOR, HIT_SURFACE_SELECTOR } from "@trackunit/react-map-adapter-shared";
30
30
  export { ClusterMarker, type ClusterMarkerProps, type ClusterMarkerStickInput, type ClusterSegment, } from "./clusters/ClusterMarker";
@@ -6,7 +6,7 @@ import type { ShapesUnderCursorHit } from "./useShapes/shapeFillTiling";
6
6
  import type { ShapeLabelResolutionContext } from "./useShapes/shapeLabelResolution";
7
7
  import type { ResolveShapeStackOrder } from "./useShapes/shapeStackOrder";
8
8
  export type { ShapesUnderCursorHit };
9
- export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterDomRenderConfig, ClusterInfo, ClusterRenderConfig, ClusterRenderState, ClusterSymbolRenderConfig, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, HeatmapRenderStyle, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, PixelOffset, RenderConfig, RenderMedium, ResolutionContext, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeStyle, ShapeStyleOverrides, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "@trackunit/react-map-adapter-shared";
9
+ export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterDomRenderConfig, ClusterInfo, ClusterRenderConfig, ClusterRenderState, ClusterSymbolRenderConfig, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, HeatmapRenderStyle, HeatmapRgbaColor, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, PixelOffset, RenderConfig, RenderMedium, ResolutionContext, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeStyle, ShapeStyleOverrides, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "@trackunit/react-map-adapter-shared";
10
10
  export { computeMarkerDomPortalZIndex } from "@trackunit/react-map-adapter-shared";
11
11
  /**
12
12
  * Controls whether a layer's bounds are included in fit-to-content operations.
@@ -2,9 +2,17 @@ import type { GeoJsonPosition } from "@trackunit/geo-json-utils";
2
2
  import type { CategoryKey, ControlConfig } from "../../controls/types";
3
3
  import type { FitParticipation, HeatmapLayerHandle } from "../types";
4
4
  export type HeatmapStyle = Readonly<{
5
+ /** Point radius in pixels. Default `30`, clamped to 1–100. */
5
6
  radiusPixels?: number;
7
+ /** Density multiplier. Default `1`, minimum `0`. */
6
8
  intensity?: number;
9
+ /** Layer opacity. Default `1`, clamped to 0–1. */
7
10
  opacity?: number;
11
+ /**
12
+ * Low-to-high density colours (at least two). Supports `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()` and `rgba()`.
13
+ * Defaults to the Movement page gradient (transparent cyan, blue, cyan, green, yellow, red).
14
+ */
15
+ colorRange?: ReadonlyArray<string>;
8
16
  }>;
9
17
  export type UseHeatmapOptions<TItem> = Readonly<{
10
18
  id: string;
@@ -0,0 +1,7 @@
1
+ import type { HeatmapRgbaColor } from "@trackunit/react-map-adapter-shared";
2
+ /**
3
+ * Parses a CSS colour into `[r, g, b, a]`.
4
+ *
5
+ * Supports `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()` and `rgba()`; returns `null` otherwise.
6
+ */
7
+ export declare const parseHeatmapColor: (input: string) => HeatmapRgbaColor | null;
@@ -0,0 +1,16 @@
1
+ import type { HeatmapRenderStyle } from "@trackunit/react-map-adapter-shared";
2
+ type HeatmapStyleInput = Readonly<{
3
+ radiusPixels: number | undefined;
4
+ intensity: number | undefined;
5
+ opacity: number | undefined;
6
+ colorRange: ReadonlyArray<string> | undefined;
7
+ }>;
8
+ /** Movement page gradient (legacy Google heatmap), low to high density. */
9
+ export declare const DEFAULT_HEATMAP_COLOR_RANGE: ReadonlyArray<string>;
10
+ /**
11
+ * Applies defaults and supported ranges so both adapters receive identical style values.
12
+ *
13
+ * Finite values are clamped; non-finite values fall back to the default with one warning.
14
+ */
15
+ export declare const resolveHeatmapStyle: (style: HeatmapStyleInput | undefined) => HeatmapRenderStyle;
16
+ export {};
@@ -1,4 +1,4 @@
1
1
  /**
2
- * Single dev-warning channel for `useHeatmap` (invalid data).
2
+ * Single dev-warning channel for `useHeatmap` (invalid data, invalid style).
3
3
  */
4
4
  export declare const warnHeatmap: (message: string, ...details: ReadonlyArray<unknown>) => void;