@tileflow/interactions 0.1.0-alpha.6 → 0.1.0-alpha.8

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @tileflow/interactions
2
2
 
3
- Portable annotations, tooltips, popups, state, and interaction bindings for Tileflow maps.
3
+ Portable annotations, clusters, tooltips, popups, state, and interaction bindings for Tileflow maps.
4
4
 
5
5
  The package has two boundaries:
6
6
 
@@ -58,6 +58,17 @@ Every new annotation needs a stable `id`, singular `coordinate`, and non-empty `
58
58
  Coordinates use `[longitude, latitude]`. Optional `data` must be recursively JSON-safe. Call `validateTileflowAnnotations` at an untrusted
59
59
  boundary to receive atomic validation and structured JSON-Pointer diagnostics.
60
60
 
61
+ ## Clusters, heatmaps, and drawing
62
+
63
+ Group up to 1,000 annotations with the `annotationClusters` prop of a framework adapter. Each group
64
+ is a focusable button that zooms in. For larger sets, pass GeoJSON through `dataSources` and
65
+ `dataOverlays`, using `geojsonSource()`, `clusterOverlay()`, and `heatmapOverlay()` from Tileflow
66
+ Core. Polygon drawing is a documented Terra Draw integration.
67
+
68
+ The [interactions reference](https://github.com/tileflow/tileflow-sdk/blob/main/packages/interactions/REFERENCE.md)
69
+ covers each path, events, and what React Native does not support. `REFERENCE.md` is also included in
70
+ the installed package.
71
+
61
72
  ## Popup state
62
73
 
63
74
  <!-- docs:check -->
@@ -114,6 +125,3 @@ Direct MapLibre consumers can use `createTileflowMapLibreDomRuntime` for annotat
114
125
  objects and schedulers are injected at attach time, so both subpath imports remain SSR-safe.
115
126
  `createTileflowMapLibreInteractionCoordinator` joins them around one controlled or uncontrolled
116
127
  popup state and commits the closing runtime before the opening runtime.
117
-
118
- See [`docs/contracts/map-interactions.md`](https://github.com/tileflow/tileflow-sdk/blob/main/docs/contracts/map-interactions.md) for lifecycle,
119
- accessibility, security, capture, and framework parity requirements.
package/REFERENCE.md ADDED
@@ -0,0 +1,185 @@
1
+ # Interactions reference: clusters, heatmaps, and drawing
2
+
3
+ Public contracts for grouping application points and drawing on a mounted map. Option and layer
4
+ names follow the [MapLibre style specification](https://maplibre.org/maplibre-style-spec/) and match
5
+ Mapbox: `cluster`, `clusterRadius`, `clusterMaxZoom`, `clusterMinPoints`, `clusterProperties`,
6
+ `point_count`, `point_count_abbreviated`, and `cluster_id`.
7
+
8
+ ## Choose a path
9
+
10
+ | Data | Use | Limit |
11
+ | ---------------------------------------- | ------------------------------------------------------- | ------------------------------ |
12
+ | Up to 1,000 points with popups | `annotations` with `annotationClusters` | 1,000 annotations |
13
+ | Thousands of points, filtered at runtime | `dataSources` with `dataOverlays` | MapLibre's GeoJSON worker |
14
+ | Fixed points or a design in config | `geojsonSource()` and `clusterOverlay()` in `defineMap` | 512,000 bytes inline, or a URL |
15
+ | Density of points | `heatmapOverlay()` over GeoJSON or a vector tileset | Vector tilesets cannot cluster |
16
+
17
+ ## Runtime data
18
+
19
+ `dataSources` and `dataOverlays` take the same values as `sources` and `overlays` in `defineMap`, so
20
+ a map on an official style needs no config. Import the helpers from `@tileflow/core/runtime`.
21
+
22
+ <!-- docs:check -->
23
+
24
+ ```ts
25
+ import {clusterOverlay, geojsonSource, heatmapOverlay} from '@tileflow/core/runtime';
26
+
27
+ const data = {
28
+ type: 'FeatureCollection',
29
+ features: [
30
+ {
31
+ type: 'Feature',
32
+ id: 'store-42',
33
+ geometry: {type: 'Point', coordinates: [-3.7038, 40.4168]},
34
+ properties: {name: 'Sol', revenue: 1200},
35
+ },
36
+ ],
37
+ } as const;
38
+
39
+ export const dataSources = {
40
+ stores: geojsonSource({
41
+ cluster: true,
42
+ clusterProperties: {revenue: ['+', ['get', 'revenue']]},
43
+ clusterRadius: 50,
44
+ data,
45
+ }),
46
+ };
47
+
48
+ export const dataOverlays = {
49
+ demand: heatmapOverlay({id: 'demand', placement: 'below-labels', source: 'stores'}),
50
+ stores: clusterOverlay({id: 'stores', placement: 'above-labels', source: 'stores'}),
51
+ };
52
+ ```
53
+
54
+ Pass both to the map component of `@tileflow/react`, `@tileflow/vue`, or `@tileflow/svelte`:
55
+
56
+ - A key the compiled style already declares is filled with `setData` and keeps its compiled options.
57
+ An overlay layer the style already draws keeps its compiled form.
58
+ - A key the style does not declare is added at the overlay's semantic `placement`. Sources and
59
+ overlays return after a theme change.
60
+ - New `data` replaces the source's contents. Keep the value stable between renders, because each new
61
+ reference is sent to the worker. Changing a cluster option rebuilds the source.
62
+ - Pass plain data. Svelte `$state` proxies cannot reach a worker, so use `$state.raw`.
63
+ - Remove a key to remove what it added.
64
+
65
+ A problem is reported through the interaction diagnostic callback with code `INVALID_DATA_LAYER`
66
+ and a JSON Pointer such as `/dataOverlays/stores/source`.
67
+
68
+ ## Cluster overlays
69
+
70
+ `clusterOverlay({id, source, placement})` draws three layers: `<id>-clusters`, `<id>-cluster-count`,
71
+ and `<id>-points`. Options are `colors`, `radii`, `steps`, `pointColor`, `textColor`, `font`, and
72
+ `expandOnClick`. The count uses the map's label font unless `font` names another stack. For anything
73
+ else, write the layers with `maplibreOverlay()` and the same `point_count` expressions.
74
+
75
+ Web adapters find these layers through their metadata, in compiled and runtime styles alike:
76
+
77
+ - Clicking or tapping a bubble eases to `getClusterExpansionZoom()` unless `expandOnClick` is false.
78
+ - The interaction event callback receives `target:activate` with a `cluster` target: `clusterId`,
79
+ `coordinate`, `pointCount`, `properties`, and `source`.
80
+ - A separate point arrives as a `style-feature` target with its `feature.id` and `properties`.
81
+ - A click a cluster takes does not also open a semantic POI below it.
82
+
83
+ Read a cluster's members from the map with `map.getSource(source).getClusterLeaves(clusterId)`.
84
+ Points at one coordinate separate only above `clusterMaxZoom`, so keep that below the map's maximum
85
+ zoom. Tileflow clusters are WebGL features and cannot take keyboard focus: use annotation clusters,
86
+ or a list beside the map, when each point must be reachable by keyboard.
87
+
88
+ ## Annotation clusters
89
+
90
+ ```tsx
91
+ <Map annotations={stores} annotationClusters={{clusterRadius: 60}} source={source} />
92
+ ```
93
+
94
+ `annotationClusters` accepts `clusterRadius`, `clusterMaxZoom`, `clusterMinPoints`, `expandOnClick`,
95
+ and `formatAriaLabel(count)`. Each group is a real `button.tileflow-interaction-cluster`, so it takes
96
+ focus and Enter, and the map needs no glyphs or extra source. The default accessible name is
97
+ `12 places`; localize it with `formatAriaLabel`. Set the colour with
98
+ `--tileflow-interaction-cluster-color`, or restyle the class.
99
+
100
+ The `cluster` target also carries `annotationIds`. A marker hidden by a cluster keeps its annotation
101
+ and state. An open popup keeps its marker visible. Focus moves to the map when the button pressed
102
+ is replaced.
103
+
104
+ ## Heatmaps
105
+
106
+ `heatmapOverlay({id, source, placement})` accepts `weight`, `radius`, `intensity`, `opacity`,
107
+ `colors`, `maxZoom`, and `sourceLayer`. `weight`, `radius`, `intensity`, and `opacity` also take
108
+ MapLibre expressions. Use `maxZoom` with a cluster overlay to hand over from density to points.
109
+ Put heatmaps at `below-labels` so that labels stay readable.
110
+
111
+ ## Polygon drawing
112
+
113
+ Tileflow does not include a drawing tool. Use [Terra Draw](https://terradraw.io) (MIT): vertex
114
+ editing, snapping, touch, and self-intersection rules are a product of their own, and Terra Draw
115
+ keeps them maintained behind a MapLibre adapter. The map component already gives you the map.
116
+
117
+ ```sh
118
+ npm install terra-draw terra-draw-maplibre-gl-adapter
119
+ ```
120
+
121
+ ```tsx
122
+ import {TerraDraw, TerraDrawPolygonMode, TerraDrawSelectMode} from 'terra-draw';
123
+ import {TerraDrawMapLibreGLAdapter} from 'terra-draw-maplibre-gl-adapter';
124
+
125
+ // A theme change replaces the style, so keep Terra Draw's own sources and layers.
126
+ const mapOptions = {
127
+ transformStyle(previous, next) {
128
+ if (!previous) return next;
129
+ const keep = (id) => id.startsWith('td-');
130
+ return {
131
+ ...next,
132
+ layers: [...next.layers, ...previous.layers.filter(({id}) => keep(id))],
133
+ sources: {
134
+ ...next.sources,
135
+ ...Object.fromEntries(Object.entries(previous.sources).filter(([id]) => keep(id))),
136
+ },
137
+ };
138
+ },
139
+ };
140
+
141
+ <Map
142
+ mapOptions={mapOptions}
143
+ onLoad={(map) => {
144
+ const draw = new TerraDraw({
145
+ adapter: new TerraDrawMapLibreGLAdapter({map}),
146
+ modes: [new TerraDrawPolygonMode(), new TerraDrawSelectMode()],
147
+ });
148
+ draw.start();
149
+ draw.setMode('polygon');
150
+ draw.on('finish', (id) => savePolygon(draw.getSnapshotFeature(id)));
151
+ }}
152
+ source={source}
153
+ />;
154
+ ```
155
+
156
+ The result is GeoJSON, which `geojsonSource()` can draw and an API can store. Create Terra Draw once
157
+ per map, in `onLoad` (Vue `@load`, Svelte `on:load`), and call `draw.stop()` before unmounting.
158
+
159
+ ## Support by surface
160
+
161
+ | Capability | React, Vue, Svelte | `tileflow preview`, `capture` | React Native |
162
+ | ----------------------------------------- | ------------------ | ----------------------------- | ------------ |
163
+ | Compiled GeoJSON clusters and heatmaps | Yes | Yes | Yes |
164
+ | Click or tap to expand a compiled cluster | Yes | No | Yes |
165
+ | `dataSources`, `dataOverlays` | Yes | No | Yes |
166
+ | `annotationClusters` | Yes | No | Yes |
167
+ | Terra Draw | Yes | No | No |
168
+
169
+ `mode="image"` draws no runtime layers and reports `UNSUPPORTED_MODE`.
170
+
171
+ React Native takes the same three props and reports the same `cluster` and `style-feature` targets
172
+ through `onInteractionEvent`. It differs in four ways:
173
+
174
+ - Data reaches MapLibre React Native's own `GeoJSONSource`, which reuses a compiled source of the
175
+ same ID and returns after a theme change. A reused source keeps its compiled cluster options.
176
+ - A tapped point reports the first point layer of its source as `layerId`.
177
+ - A map with a controlled `view` reports the cluster but does not move the camera, because the
178
+ application owns the view. Use the target's coordinate to set it.
179
+ - Annotation clusters are native buttons with `accessibilityRole="button"`, grouped by the same
180
+ `createTileflowAnnotationClusterIndex` as the web, so both platforms split at the same zooms.
181
+
182
+ Terra Draw is a GL JS library and has no native equivalent here.
183
+
184
+ Direct MapLibre consumers use `createTileflowMapLibreDataRuntime` from
185
+ `@tileflow/interactions/maplibre`.
package/dist/index.d.ts CHANGED
@@ -1,11 +1,18 @@
1
- import { T as TileflowInteractionState, a as TileflowInteractionAction, b as TileflowInteractionTargetRef } from './validation-Cy6Tz3lz.js';
2
- export { c as TileflowAnnotation, d as TileflowAnnotationInteractionEvent, e as TileflowAnnotationMarker, f as TileflowAnnotationSurface, g as TileflowAnnotationValidationResult, h as TileflowAnnotationViewContext, i as TileflowInteractionBinding, j as TileflowInteractionBindingValidationResult, k as TileflowInteractionContent, l as TileflowInteractionCoordinate, m as TileflowInteractionDiagnostic, n as TileflowInteractionDiagnosticCode, o as TileflowInteractionEvent, p as TileflowInteractionEventType, q as TileflowInteractionFieldContent, r as TileflowInteractionInputModality, s as TileflowInteractionJsonPrimitive, t as TileflowInteractionJsonValue, u as TileflowInteractionTarget, v as TileflowInteractionTextContent, w as TileflowInteractionViewContent, x as TileflowInteractionViewContext, y as TileflowPoiCategory, z as TileflowPoiFilterRank, A as TileflowPoiProperties, B as TileflowPoiSizeRank, C as TileflowResolvedAnnotationTarget, D as TileflowResolvedInteractionTarget, E as TileflowResolvedMapTarget, F as TileflowResolvedPoiFeature, G as TileflowResolvedPoiFeatureTarget, H as TileflowResolvedSemanticFeature, I as TileflowResolvedSemanticFeatureTarget, J as TileflowResolvedStyleFeatureTarget, K as tileflowAnnotationSchema, L as tileflowAnnotationsSchema, M as tileflowInteractionActionSchema, N as tileflowInteractionBindingSchema, O as tileflowInteractionBindingsSchema, P as tileflowInteractionContentSchema, Q as tileflowInteractionJsonValueSchema, R as tileflowInteractionLimits, S as tileflowInteractionSchemaVersion, U as tileflowInteractionStateSchema, V as tileflowInteractionTargetRefSchema, W as tileflowInteractionTargetSchema, X as tileflowPoiCategories, Y as validateTileflowAnnotations, Z as validateTileflowInteractionBindings } from './validation-Cy6Tz3lz.js';
1
+ import { T as TileflowInteractionState, a as TileflowInteractionAction, b as TileflowInteractionTargetRef } from './validation-DBRKrV__.js';
2
+ export { c as TileflowAnnotation, d as TileflowAnnotationCluster, e as TileflowAnnotationClusterIndex, f as TileflowAnnotationClusterOptions, g as TileflowAnnotationClusterView, h as TileflowAnnotationInteractionEvent, i as TileflowAnnotationMarker, j as TileflowAnnotationSurface, k as TileflowAnnotationValidationResult, l as TileflowAnnotationViewContext, m as TileflowInteractionBinding, n as TileflowInteractionBindingValidationResult, o as TileflowInteractionContent, p as TileflowInteractionCoordinate, q as TileflowInteractionDiagnostic, r as TileflowInteractionDiagnosticCode, s as TileflowInteractionEvent, t as TileflowInteractionEventType, u as TileflowInteractionFieldContent, v as TileflowInteractionInputModality, w as TileflowInteractionJsonPrimitive, x as TileflowInteractionJsonValue, y as TileflowInteractionTarget, z as TileflowInteractionTextContent, A as TileflowInteractionViewContent, B as TileflowInteractionViewContext, C as TileflowPoiCategory, D as TileflowPoiFilterRank, E as TileflowPoiProperties, F as TileflowPoiSizeRank, G as TileflowResolvedAnnotationTarget, H as TileflowResolvedClusterTarget, I as TileflowResolvedInteractionTarget, J as TileflowResolvedMapTarget, K as TileflowResolvedPoiFeature, L as TileflowResolvedPoiFeatureTarget, M as TileflowResolvedSemanticFeature, N as TileflowResolvedSemanticFeatureTarget, O as TileflowResolvedStyleFeatureTarget, P as createTileflowAnnotationClusterIndex, Q as tileflowAnnotationSchema, R as tileflowAnnotationsSchema, S as tileflowInteractionActionSchema, U as tileflowInteractionBindingSchema, V as tileflowInteractionBindingsSchema, W as tileflowInteractionContentSchema, X as tileflowInteractionJsonValueSchema, Y as tileflowInteractionLimits, Z as tileflowInteractionSchemaVersion, _ as tileflowInteractionStateSchema, $ as tileflowInteractionTargetRefSchema, a0 as tileflowInteractionTargetSchema, a1 as tileflowPoiCategories, a2 as validateTileflowAnnotations, a3 as validateTileflowInteractionBindings } from './validation-DBRKrV__.js';
3
3
  import 'zod/mini';
4
4
 
5
5
  declare const initialTileflowInteractionState: TileflowInteractionState;
6
6
  declare function reduceTileflowInteractionState(state: TileflowInteractionState, action: TileflowInteractionAction): TileflowInteractionState;
7
7
  declare function tileflowInteractionTargetRefsEqual(left: TileflowInteractionTargetRef | null, right: TileflowInteractionTargetRef | null): boolean;
8
8
 
9
+ type Json = Readonly<Record<string, unknown>>;
10
+ /**
11
+ * Gives a `clusterOverlay()` count layer the style's label font. Other layers and counts with their
12
+ * own font return unchanged; `undefined` means the style has no label font to share.
13
+ */
14
+ declare function withTileflowClusterLabelFont<TLayer extends Json>(layer: TLayer, styleLayers: readonly Json[]): TLayer | undefined;
15
+
9
16
  /** Compact machine-readable vocabulary for agents and code generators. */
10
17
  declare const tileflowInteractionReference: Readonly<{
11
18
  readonly actions: readonly ["open-popup", "close-popup"];
@@ -50,4 +57,4 @@ declare const tileflowInteractionReference: Readonly<{
50
57
  readonly version: 1;
51
58
  }>;
52
59
 
53
- export { TileflowInteractionAction, TileflowInteractionState, TileflowInteractionTargetRef, initialTileflowInteractionState, reduceTileflowInteractionState, tileflowInteractionReference, tileflowInteractionTargetRefsEqual };
60
+ export { TileflowInteractionAction, TileflowInteractionState, TileflowInteractionTargetRef, initialTileflowInteractionState, reduceTileflowInteractionState, tileflowInteractionReference, tileflowInteractionTargetRefsEqual, withTileflowClusterLabelFont };
package/dist/index.js CHANGED
@@ -398,6 +398,74 @@ function tileflowInteractionTargetRefsEqual(left, right) {
398
398
  }
399
399
  }
400
400
 
401
+ // src/annotation-clusters.ts
402
+ import Supercluster from "supercluster";
403
+ function createTileflowAnnotationClusterIndex(annotations, options = {}) {
404
+ const index = new Supercluster({
405
+ extent: 512,
406
+ maxZoom: Math.floor(options.clusterMaxZoom ?? 14),
407
+ minPoints: options.clusterMinPoints ?? 2,
408
+ radius: options.clusterRadius ?? 50
409
+ }).load(
410
+ annotations.map(({ coordinate, id }) => ({
411
+ geometry: { coordinates: [coordinate[0], coordinate[1]], type: "Point" },
412
+ properties: { id },
413
+ type: "Feature"
414
+ }))
415
+ );
416
+ const views = /* @__PURE__ */ new Map();
417
+ return Object.freeze({
418
+ at(zoom) {
419
+ const level = Math.max(0, Math.floor(Number.isFinite(zoom) ? zoom : 0));
420
+ const cached = views.get(level);
421
+ if (cached) return cached;
422
+ const hidden = new Set(annotations.map(({ id }) => id));
423
+ const clusters = [];
424
+ for (const feature of index.getClusters([-180, -90, 180, 90], level)) {
425
+ const properties = feature.properties;
426
+ if (!properties.cluster) {
427
+ hidden.delete(properties.id);
428
+ continue;
429
+ }
430
+ const clusterId = properties.cluster_id;
431
+ const [longitude, latitude] = feature.geometry.coordinates;
432
+ clusters.push(
433
+ Object.freeze({
434
+ annotationIds: Object.freeze(
435
+ index.getLeaves(clusterId, Infinity).map((leaf) => leaf.properties.id)
436
+ ),
437
+ clusterId,
438
+ coordinate: Object.freeze([longitude, latitude]),
439
+ expansionZoom: index.getClusterExpansionZoom(clusterId),
440
+ label: String(properties.point_count_abbreviated),
441
+ pointCount: properties.point_count
442
+ })
443
+ );
444
+ }
445
+ const view = Object.freeze({ clusters: Object.freeze(clusters), hiddenAnnotationIds: hidden });
446
+ views.set(level, view);
447
+ return view;
448
+ }
449
+ });
450
+ }
451
+
452
+ // src/cluster-label-font.ts
453
+ var clusterMetadataKey = "tileflow:cluster";
454
+ var lineBackgroundStack = / lines-v1-t\d+-l\d+-p\d+$/u;
455
+ function withTileflowClusterLabelFont(layer, styleLayers) {
456
+ const metadata = layer.metadata;
457
+ const layout = layer.layout ?? {};
458
+ if (metadata?.[clusterMetadataKey]?.role !== "count") return layer;
459
+ if (layout["text-font"] !== void 0) return layer;
460
+ for (const candidate of styleLayers) {
461
+ const font = candidate.layout?.["text-font"];
462
+ if (Array.isArray(font) && font.length > 0 && font.every((face) => typeof face === "string" && !lineBackgroundStack.test(face))) {
463
+ return { ...layer, layout: { ...layout, "text-font": font } };
464
+ }
465
+ }
466
+ return void 0;
467
+ }
468
+
401
469
  // src/reference.ts
402
470
  var tileflowInteractionReference = Object.freeze({
403
471
  actions: ["open-popup", "close-popup"],
@@ -617,6 +685,7 @@ function jsonPointer(path) {
617
685
  return path.map((segment) => String(segment).replaceAll("~", "~0").replaceAll("/", "~1")).map((segment) => `/${segment}`).join("");
618
686
  }
619
687
  export {
688
+ createTileflowAnnotationClusterIndex,
620
689
  initialTileflowInteractionState,
621
690
  reduceTileflowInteractionState,
622
691
  tileflowAnnotationSchema,
@@ -635,5 +704,6 @@ export {
635
704
  tileflowInteractionTargetSchema,
636
705
  tileflowPoiCategories,
637
706
  validateTileflowAnnotations,
638
- validateTileflowInteractionBindings
707
+ validateTileflowInteractionBindings,
708
+ withTileflowClusterLabelFont
639
709
  };
@@ -1,4 +1,4 @@
1
- import { m as TileflowInteractionDiagnostic, T as TileflowInteractionState, c as TileflowAnnotation, d as TileflowAnnotationInteractionEvent, i as TileflowInteractionBinding, G as TileflowResolvedPoiFeatureTarget, r as TileflowInteractionInputModality, o as TileflowInteractionEvent, I as TileflowResolvedSemanticFeatureTarget, k as TileflowInteractionContent } from './validation-Cy6Tz3lz.js';
1
+ import { q as TileflowInteractionDiagnostic, T as TileflowInteractionState, c as TileflowAnnotation, h as TileflowAnnotationInteractionEvent, f as TileflowAnnotationClusterOptions, s as TileflowInteractionEvent, m as TileflowInteractionBinding, L as TileflowResolvedPoiFeatureTarget, v as TileflowInteractionInputModality, N as TileflowResolvedSemanticFeatureTarget, o as TileflowInteractionContent } from './validation-DBRKrV__.js';
2
2
  import 'zod/mini';
3
3
 
4
4
  /**
@@ -135,6 +135,8 @@ type TileflowMapLibreDomRuntime<TAnnotation extends TileflowAnnotation = Tileflo
135
135
  openPopup: (annotationId: string) => boolean;
136
136
  reconcile: (annotations: readonly TAnnotation[]) => void;
137
137
  setCustomRenderers: (renderers: TileflowMapLibreDomCustomRenderers) => void;
138
+ /** Hide markers that a cluster stands in for. An open popup keeps its own marker visible. */
139
+ setHiddenAnnotations: (annotationIds: ReadonlySet<string>) => void;
138
140
  setInteractionState: (state: TileflowInteractionState) => void;
139
141
  subscribeEvents: (listener: (event: TileflowAnnotationInteractionEvent<TAnnotation>) => void) => () => void;
140
142
  subscribeDiagnostics: (listener: TileflowMapLibreDomDiagnosticListener) => () => void;
@@ -149,6 +151,94 @@ type TileflowMapLibreDomRuntime<TAnnotation extends TileflowAnnotation = Tileflo
149
151
  */
150
152
  declare function createTileflowMapLibreDomRuntime<TMap, TMarker extends TileflowMapLibrePositioned<TMap>, TOverlay extends TileflowMapLibrePositioned<TMap>, TAnnotation extends TileflowAnnotation = TileflowAnnotation>(options: TileflowMapLibreDomRuntimeOptions<TMap, TMarker, TOverlay, TAnnotation>): TileflowMapLibreDomRuntime<TAnnotation>;
151
153
 
154
+ type Json = Readonly<Record<string, unknown>>;
155
+ /** A `geojsonSource()` value from `@tileflow/core`. */
156
+ type TileflowMapLibreDataSource = Json & Readonly<{
157
+ data: unknown;
158
+ kind: 'geojson';
159
+ }>;
160
+ /** A `clusterOverlay()`, `heatmapOverlay()` or `maplibreOverlay()` value from `@tileflow/core`. */
161
+ type TileflowMapLibreDataOverlay = Readonly<{
162
+ kind: 'maplibre-overlay';
163
+ layers: readonly (Json & Readonly<{
164
+ id: string;
165
+ type: string;
166
+ }>)[];
167
+ placement: string;
168
+ source: string;
169
+ }>;
170
+ type TileflowMapLibreDataInput<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = Readonly<{
171
+ annotationClusters?: TileflowAnnotationClusterOptions | undefined;
172
+ annotations?: readonly TAnnotation[] | undefined;
173
+ dataOverlays?: Readonly<Record<string, TileflowMapLibreDataOverlay>> | undefined;
174
+ dataSources?: Readonly<Record<string, TileflowMapLibreDataSource>> | undefined;
175
+ }>;
176
+ type TileflowMapLibreDataFeature = Readonly<{
177
+ geometry?: unknown;
178
+ id?: number | string;
179
+ layer: Readonly<{
180
+ id: string;
181
+ }>;
182
+ properties?: Json | null;
183
+ source?: string;
184
+ }>;
185
+ /** The MapLibre methods the runtime uses; a `maplibregl.Map` satisfies this. */
186
+ type TileflowMapLibreDataMap = Readonly<{
187
+ addLayer: (layer: never, beforeId?: string) => unknown;
188
+ addSource: (id: string, source: never) => unknown;
189
+ easeTo: (options: {
190
+ center: [number, number];
191
+ zoom: number;
192
+ }) => unknown;
193
+ getCanvas: () => Readonly<{
194
+ style: {
195
+ cursor: string;
196
+ };
197
+ focus?: (o?: FocusOptions) => void;
198
+ }>;
199
+ getLayer: (id: string) => Readonly<{
200
+ metadata?: unknown;
201
+ }> | undefined;
202
+ getLayersOrder: () => readonly string[];
203
+ getSource: (id: string) => unknown;
204
+ getStyle: () => unknown;
205
+ getZoom: () => number;
206
+ isStyleLoaded: () => boolean | void;
207
+ off: (event: never, listener: never) => unknown;
208
+ on: (event: never, listener: never) => unknown;
209
+ queryRenderedFeatures: (point: never, options: Readonly<{
210
+ layers: string[];
211
+ }>) => readonly TileflowMapLibreDataFeature[];
212
+ removeLayer: (id: string) => unknown;
213
+ removeSource: (id: string) => unknown;
214
+ }>;
215
+ type TileflowMapLibreDataRuntimeOptions<TMap, TMarker extends TileflowMapLibrePositioned<TMap>> = Readonly<{
216
+ /** Builds the marker that carries one annotation cluster's button. */
217
+ createMarker: (input: Readonly<{
218
+ element: HTMLElement;
219
+ }>) => TMarker;
220
+ document: Pick<Document, 'createElement'>;
221
+ map: TMap & TileflowMapLibreDataMap;
222
+ onDiagnostic?: (diagnostic: TileflowInteractionDiagnostic) => void;
223
+ onInteractionEvent?: (event: TileflowInteractionEvent) => void;
224
+ /** Receives the annotation IDs a cluster stands in for. */
225
+ setHiddenAnnotations?: (annotationIds: ReadonlySet<string>) => void;
226
+ }>;
227
+ type TileflowMapLibreDataRuntime<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = Readonly<{
228
+ dispose: () => void;
229
+ reconcile: (input: TileflowMapLibreDataInput<TAnnotation>) => void;
230
+ }>;
231
+ /**
232
+ * Keeps application GeoJSON, its overlays and clustered annotations on a mounted map.
233
+ *
234
+ * Runtime sources and overlays take the same `geojsonSource()` and overlay values as a map's config.
235
+ * A source the compiled style already declares is filled rather than added, and an overlay layer
236
+ * it already draws keeps its compiled form. Both return after a style change, which removes
237
+ * whatever the new style does not declare. Layers built by `clusterOverlay()` are found through
238
+ * their metadata, so compiled and runtime clusters expand the same way.
239
+ */
240
+ declare function createTileflowMapLibreDataRuntime<TMap, TMarker extends TileflowMapLibrePositioned<TMap>, TAnnotation extends TileflowAnnotation = TileflowAnnotation>(options: TileflowMapLibreDataRuntimeOptions<TMap, TMarker>): TileflowMapLibreDataRuntime<TAnnotation>;
241
+
152
242
  type TileflowMapLibreInteractionParticipantKind = 'annotation' | 'semantic';
153
243
  type TileflowMapLibreInteractionParticipant = Readonly<{
154
244
  setInteractionState: (state: TileflowInteractionState) => void;
@@ -287,4 +377,4 @@ type TileflowMapLibreSemanticDomRuntime = Readonly<{
287
377
  */
288
378
  declare function createTileflowMapLibreSemanticDomRuntime<TMap, TOverlay extends TileflowMapLibrePositioned<TMap>>(options: TileflowMapLibreSemanticDomRuntimeOptions<TMap, TOverlay>): TileflowMapLibreSemanticDomRuntime;
289
379
 
290
- export { type TileflowAnnotationRegistry, type TileflowAnnotationRegistryAdapter, type TileflowAnnotationRegistryEntry, type TileflowMapLibreDomCustomRenderers, type TileflowMapLibreDomDiagnosticListener, type TileflowMapLibreDomDocument, type TileflowMapLibreDomInteractionStateListener, type TileflowMapLibreDomMarkerFactoryInput, type TileflowMapLibreDomOverlayFactoryInput, type TileflowMapLibreDomOverlayKind, type TileflowMapLibreDomRenderTarget, type TileflowMapLibreDomRenderTargetKind, type TileflowMapLibreDomRenderTargetListener, type TileflowMapLibreDomRuntime, type TileflowMapLibreDomRuntimeOptions, type TileflowMapLibreDomStateChangeReason, type TileflowMapLibreInteractionCoordinator, type TileflowMapLibreInteractionCoordinatorOptions, type TileflowMapLibreInteractionParticipant, type TileflowMapLibreInteractionParticipantKind, type TileflowMapLibrePoiController, type TileflowMapLibrePoiControllerOptions, type TileflowMapLibrePoiFeature, type TileflowMapLibrePoiMap, type TileflowMapLibrePoiMatch, type TileflowMapLibrePoiPointerEvent, type TileflowMapLibrePositioned, type TileflowMapLibreSemanticDomOverlayFactoryInput, type TileflowMapLibreSemanticDomRenderTarget, type TileflowMapLibreSemanticDomRuntime, type TileflowMapLibreSemanticDomRuntimeOptions, type TileflowOverlayState, type TileflowOverlayStateChangeReason, type TileflowOverlayStateController, type TileflowOverlayStateListener, createTileflowAnnotationRegistry, createTileflowMapLibreDomRuntime, createTileflowMapLibreInteractionCoordinator, createTileflowMapLibrePoiController, createTileflowMapLibreSemanticDomRuntime, createTileflowOverlayStateController };
380
+ export { TileflowAnnotationClusterOptions, type TileflowAnnotationRegistry, type TileflowAnnotationRegistryAdapter, type TileflowAnnotationRegistryEntry, type TileflowMapLibreDataFeature, type TileflowMapLibreDataInput, type TileflowMapLibreDataMap, type TileflowMapLibreDataOverlay, type TileflowMapLibreDataRuntime, type TileflowMapLibreDataRuntimeOptions, type TileflowMapLibreDataSource, type TileflowMapLibreDomCustomRenderers, type TileflowMapLibreDomDiagnosticListener, type TileflowMapLibreDomDocument, type TileflowMapLibreDomInteractionStateListener, type TileflowMapLibreDomMarkerFactoryInput, type TileflowMapLibreDomOverlayFactoryInput, type TileflowMapLibreDomOverlayKind, type TileflowMapLibreDomRenderTarget, type TileflowMapLibreDomRenderTargetKind, type TileflowMapLibreDomRenderTargetListener, type TileflowMapLibreDomRuntime, type TileflowMapLibreDomRuntimeOptions, type TileflowMapLibreDomStateChangeReason, type TileflowMapLibreInteractionCoordinator, type TileflowMapLibreInteractionCoordinatorOptions, type TileflowMapLibreInteractionParticipant, type TileflowMapLibreInteractionParticipantKind, type TileflowMapLibrePoiController, type TileflowMapLibrePoiControllerOptions, type TileflowMapLibrePoiFeature, type TileflowMapLibrePoiMap, type TileflowMapLibrePoiMatch, type TileflowMapLibrePoiPointerEvent, type TileflowMapLibrePositioned, type TileflowMapLibreSemanticDomOverlayFactoryInput, type TileflowMapLibreSemanticDomRenderTarget, type TileflowMapLibreSemanticDomRuntime, type TileflowMapLibreSemanticDomRuntimeOptions, type TileflowOverlayState, type TileflowOverlayStateChangeReason, type TileflowOverlayStateController, type TileflowOverlayStateListener, createTileflowAnnotationRegistry, createTileflowMapLibreDataRuntime, createTileflowMapLibreDomRuntime, createTileflowMapLibreInteractionCoordinator, createTileflowMapLibrePoiController, createTileflowMapLibreSemanticDomRuntime, createTileflowOverlayStateController };
@@ -330,6 +330,12 @@ function createTileflowMapLibreDomRuntime(options) {
330
330
  let disposeStarted = false;
331
331
  let disposeComplete = false;
332
332
  let overlaySequence = 0;
333
+ let hiddenAnnotations = /* @__PURE__ */ new Set();
334
+ const applyAnnotationVisibility = (instance, annotationId) => {
335
+ const hidden = hiddenAnnotations.has(annotationId) && activePopup?.annotation.id !== annotationId;
336
+ if (hidden) closeTooltipVisual(annotationId);
337
+ instance.element.style.display = hidden ? "none" : "";
338
+ };
333
339
  const emit = (type, annotation, inputModality2) => {
334
340
  const event = Object.freeze({
335
341
  coordinate: annotation.coordinate,
@@ -388,6 +394,7 @@ function createTileflowMapLibreDomRuntime(options) {
388
394
  }
389
395
  marker?.element.setAttribute("aria-expanded", "false");
390
396
  marker?.element.removeAttribute("aria-controls");
397
+ if (marker) applyAnnotationVisibility(marker, current.annotation.id);
391
398
  const errors = safelyRunAll([() => current.overlay.remove(), () => current.shell.remove()]);
392
399
  publishRenderTargets();
393
400
  emit("popup:close", current.annotation, inputModality2);
@@ -537,6 +544,7 @@ function createTileflowMapLibreDomRuntime(options) {
537
544
  clearOverlayFailure(entry.definition.id, "popup");
538
545
  entry.instance.element.setAttribute("aria-controls", shell.id);
539
546
  entry.instance.element.setAttribute("aria-expanded", "true");
547
+ applyAnnotationVisibility(entry.instance, annotationId);
540
548
  publishRenderTargets();
541
549
  emit("popup:open", entry.definition, inputModality2);
542
550
  closeButton.focus({ preventScroll: true });
@@ -766,6 +774,7 @@ function createTileflowMapLibreDomRuntime(options) {
766
774
  container.className = "tileflow-interaction-marker-content";
767
775
  element.append(container);
768
776
  configureAnnotationElement({ annotation, container, element }, annotation);
777
+ if (hiddenAnnotations.has(annotation.id)) element.style.display = "none";
769
778
  const marker = options.createMarker({ annotation, element });
770
779
  const instance = {
771
780
  annotation,
@@ -1021,6 +1030,11 @@ function createTileflowMapLibreDomRuntime(options) {
1021
1030
  publishRenderTargets();
1022
1031
  }
1023
1032
  },
1033
+ setHiddenAnnotations(annotationIds) {
1034
+ assertRuntimeActive(disposeStarted);
1035
+ hiddenAnnotations = annotationIds;
1036
+ for (const entry of registry.entries()) applyAnnotationVisibility(entry.instance, entry.key);
1037
+ },
1024
1038
  setInteractionState(state) {
1025
1039
  assertRuntimeActive(disposeStarted);
1026
1040
  const popupId = getAnnotationPopupId(state);
@@ -1180,6 +1194,455 @@ function throwCollectedErrors(errors, message) {
1180
1194
  throw new AggregateError(errors, message);
1181
1195
  }
1182
1196
 
1197
+ // src/annotation-clusters.ts
1198
+ import Supercluster from "supercluster";
1199
+ function createTileflowAnnotationClusterIndex(annotations, options = {}) {
1200
+ const index = new Supercluster({
1201
+ extent: 512,
1202
+ maxZoom: Math.floor(options.clusterMaxZoom ?? 14),
1203
+ minPoints: options.clusterMinPoints ?? 2,
1204
+ radius: options.clusterRadius ?? 50
1205
+ }).load(
1206
+ annotations.map(({ coordinate, id }) => ({
1207
+ geometry: { coordinates: [coordinate[0], coordinate[1]], type: "Point" },
1208
+ properties: { id },
1209
+ type: "Feature"
1210
+ }))
1211
+ );
1212
+ const views = /* @__PURE__ */ new Map();
1213
+ return Object.freeze({
1214
+ at(zoom) {
1215
+ const level = Math.max(0, Math.floor(Number.isFinite(zoom) ? zoom : 0));
1216
+ const cached = views.get(level);
1217
+ if (cached) return cached;
1218
+ const hidden = new Set(annotations.map(({ id }) => id));
1219
+ const clusters = [];
1220
+ for (const feature of index.getClusters([-180, -90, 180, 90], level)) {
1221
+ const properties = feature.properties;
1222
+ if (!properties.cluster) {
1223
+ hidden.delete(properties.id);
1224
+ continue;
1225
+ }
1226
+ const clusterId = properties.cluster_id;
1227
+ const [longitude, latitude] = feature.geometry.coordinates;
1228
+ clusters.push(
1229
+ Object.freeze({
1230
+ annotationIds: Object.freeze(
1231
+ index.getLeaves(clusterId, Infinity).map((leaf) => leaf.properties.id)
1232
+ ),
1233
+ clusterId,
1234
+ coordinate: Object.freeze([longitude, latitude]),
1235
+ expansionZoom: index.getClusterExpansionZoom(clusterId),
1236
+ label: String(properties.point_count_abbreviated),
1237
+ pointCount: properties.point_count
1238
+ })
1239
+ );
1240
+ }
1241
+ const view = Object.freeze({ clusters: Object.freeze(clusters), hiddenAnnotationIds: hidden });
1242
+ views.set(level, view);
1243
+ return view;
1244
+ }
1245
+ });
1246
+ }
1247
+
1248
+ // src/cluster-label-font.ts
1249
+ var clusterMetadataKey = "tileflow:cluster";
1250
+ var lineBackgroundStack = / lines-v1-t\d+-l\d+-p\d+$/u;
1251
+ function withTileflowClusterLabelFont(layer, styleLayers) {
1252
+ const metadata = layer.metadata;
1253
+ const layout = layer.layout ?? {};
1254
+ if (metadata?.[clusterMetadataKey]?.role !== "count") return layer;
1255
+ if (layout["text-font"] !== void 0) return layer;
1256
+ for (const candidate of styleLayers) {
1257
+ const font = candidate.layout?.["text-font"];
1258
+ if (Array.isArray(font) && font.length > 0 && font.every((face) => typeof face === "string" && !lineBackgroundStack.test(face))) {
1259
+ return { ...layer, layout: { ...layout, "text-font": font } };
1260
+ }
1261
+ }
1262
+ return void 0;
1263
+ }
1264
+
1265
+ // src/maplibre-data.ts
1266
+ var clusterMetadataKey2 = "tileflow:cluster";
1267
+ var overlayMetadataKey = "tileflow:overlay";
1268
+ var placementMetadataKey = "tileflow:overlay-placement-manifest";
1269
+ function createTileflowMapLibreDataRuntime(options) {
1270
+ const map = options.map;
1271
+ const overlays = /* @__PURE__ */ new Map();
1272
+ const sources = /* @__PURE__ */ new Map();
1273
+ const clusterMarkers = /* @__PURE__ */ new Map();
1274
+ const reported = /* @__PURE__ */ new Set();
1275
+ let input = {};
1276
+ let annotationClusterKey = "";
1277
+ let index;
1278
+ let renderedZoom;
1279
+ let clusterLayers = [];
1280
+ let pointLayers = [];
1281
+ let hovered = [];
1282
+ let cursorOwned = false;
1283
+ let applying = false;
1284
+ let disposed = false;
1285
+ const on = (event, listener) => {
1286
+ map.on(event, listener);
1287
+ return () => map.off(event, listener);
1288
+ };
1289
+ const onLayers = (event, layers, listener) => {
1290
+ map.on(
1291
+ event,
1292
+ [...layers],
1293
+ listener
1294
+ );
1295
+ return () => map.off(
1296
+ event,
1297
+ [...layers],
1298
+ listener
1299
+ );
1300
+ };
1301
+ const report = (message, path) => {
1302
+ if (reported.has(message)) return;
1303
+ reported.add(message);
1304
+ options.onDiagnostic?.({ code: "INVALID_DATA_LAYER", level: "error", message, path });
1305
+ };
1306
+ const removeOverlay = (id) => {
1307
+ for (const layer of overlays.get(id)?.layers ?? []) {
1308
+ if (map.getLayer(layer)) map.removeLayer(layer);
1309
+ }
1310
+ overlays.delete(id);
1311
+ };
1312
+ const removeSource = (id) => {
1313
+ for (const [overlayId, overlay] of [...overlays]) {
1314
+ if (overlay.source === id) removeOverlay(overlayId);
1315
+ }
1316
+ if (map.getSource(id)) map.removeSource(id);
1317
+ sources.delete(id);
1318
+ };
1319
+ const syncSources = () => {
1320
+ for (const id of sources.keys()) {
1321
+ if (!input.dataSources?.[id]) removeSource(id);
1322
+ }
1323
+ for (const [id, source] of Object.entries(input.dataSources ?? {}).sort(compareEntries)) {
1324
+ const { data, kind: _kind, ...definition } = source;
1325
+ const signature = JSON.stringify(definition);
1326
+ const owned = sources.get(id);
1327
+ if (owned && owned.signature !== signature) removeSource(id);
1328
+ const current = map.getSource(id);
1329
+ if (!current) {
1330
+ map.addSource(id, { ...definition, data, type: "geojson" });
1331
+ sources.set(id, { signature });
1332
+ } else if (typeof current.setData !== "function") {
1333
+ report(`Source "${id}" already exists and is not GeoJSON.`, `/dataSources/${id}`);
1334
+ } else if (current.serialize?.().data !== data) {
1335
+ current.setData(data);
1336
+ }
1337
+ }
1338
+ };
1339
+ const syncOverlays = () => {
1340
+ for (const id of overlays.keys()) {
1341
+ if (!input.dataOverlays?.[id]) removeOverlay(id);
1342
+ }
1343
+ const style = map.getStyle();
1344
+ const anchors = style?.metadata?.[placementMetadataKey]?.anchors;
1345
+ for (const [id, overlay] of Object.entries(input.dataOverlays ?? {}).sort(compareEntries)) {
1346
+ const path = `/dataOverlays/${id}`;
1347
+ if (overlay.kind !== "maplibre-overlay") {
1348
+ report(`Overlay "${id}" is not a Tileflow overlay.`, path);
1349
+ continue;
1350
+ }
1351
+ if (!map.getSource(overlay.source)) {
1352
+ report(`Overlay "${id}" references unknown source "${overlay.source}".`, `${path}/source`);
1353
+ continue;
1354
+ }
1355
+ const signature = JSON.stringify(overlay);
1356
+ if (overlays.get(id)?.signature !== signature) removeOverlay(id);
1357
+ const anchor = anchors?.[overlay.placement];
1358
+ const beforeId = typeof anchor === "string" && map.getLayer(anchor) ? anchor : void 0;
1359
+ const owned = overlays.get(id)?.layers ?? [];
1360
+ for (const layer of overlay.layers) {
1361
+ if (map.getLayer(layer.id)) continue;
1362
+ const prepared = withTileflowClusterLabelFont(layer, style?.layers ?? []);
1363
+ if (!prepared) {
1364
+ report(`Layer "${layer.id}" needs font: the style has no label font.`, path);
1365
+ continue;
1366
+ }
1367
+ const metadata = { ...layer.metadata, [overlayMetadataKey]: id };
1368
+ map.addLayer({ ...prepared, metadata, source: overlay.source }, beforeId);
1369
+ if (!owned.includes(layer.id)) owned.push(layer.id);
1370
+ }
1371
+ overlays.set(id, { layers: owned, signature, source: overlay.source });
1372
+ }
1373
+ };
1374
+ const syncClusterLayers = () => {
1375
+ const clusters = [];
1376
+ const points = [];
1377
+ for (const id of map.getLayersOrder()) {
1378
+ const metadata = map.getLayer(id)?.metadata;
1379
+ const cluster = metadata?.[clusterMetadataKey2];
1380
+ if (cluster?.role === "cluster") clusters.push(id);
1381
+ if (cluster?.role === "point") points.push(id);
1382
+ }
1383
+ clusterLayers = clusters;
1384
+ pointLayers = points;
1385
+ const interactive = [...clusters, ...points];
1386
+ if (interactive.join("\0") === hovered.join("\0")) return;
1387
+ stopHover();
1388
+ hovered = interactive;
1389
+ if (interactive.length === 0) return;
1390
+ const enter = () => {
1391
+ cursorOwned = true;
1392
+ map.getCanvas().style.cursor = "pointer";
1393
+ };
1394
+ const leave = () => {
1395
+ if (cursorOwned) map.getCanvas().style.cursor = "";
1396
+ cursorOwned = false;
1397
+ };
1398
+ const stops2 = [
1399
+ onLayers("mouseenter", interactive, enter),
1400
+ onLayers("mouseleave", interactive, leave)
1401
+ ];
1402
+ stopHover = () => {
1403
+ for (const stop of stops2) stop();
1404
+ leave();
1405
+ stopHover = () => void 0;
1406
+ };
1407
+ };
1408
+ let stopHover = () => void 0;
1409
+ const syncStyle = () => {
1410
+ if (applying || disposed) return;
1411
+ applying = true;
1412
+ try {
1413
+ syncSources();
1414
+ syncOverlays();
1415
+ syncClusterLayers();
1416
+ } catch (error) {
1417
+ if (map.isStyleLoaded() === true) {
1418
+ report(error instanceof Error ? error.message : "Tileflow data layers failed.", "");
1419
+ }
1420
+ } finally {
1421
+ applying = false;
1422
+ }
1423
+ };
1424
+ const activateCluster = (target, modality) => options.onInteractionEvent?.({
1425
+ coordinate: target.coordinate,
1426
+ inputModality: modality,
1427
+ target,
1428
+ type: "target:activate"
1429
+ });
1430
+ const expand = (center, zoom) => map.easeTo({ center: [center[0], center[1]], zoom });
1431
+ const handleClick = (event) => {
1432
+ const layers = [...clusterLayers, ...pointLayers];
1433
+ if (disposed || layers.length === 0 || !event.point) return;
1434
+ const feature = map.queryRenderedFeatures(event.point, { layers })[0];
1435
+ if (!feature) return;
1436
+ event.preventDefault?.();
1437
+ const properties = jsonProperties(feature.properties);
1438
+ const modality = pointerModality(event);
1439
+ const coordinate = pointCoordinate(feature.geometry) ?? eventCoordinate(event);
1440
+ if (!coordinate) return;
1441
+ if (!clusterLayers.includes(feature.layer.id)) {
1442
+ options.onInteractionEvent?.({
1443
+ coordinate,
1444
+ inputModality: modality,
1445
+ target: {
1446
+ coordinate,
1447
+ feature: { ...feature.id === void 0 ? {} : { id: feature.id }, properties },
1448
+ kind: "style-feature",
1449
+ layerId: feature.layer.id
1450
+ },
1451
+ type: "target:activate"
1452
+ });
1453
+ return;
1454
+ }
1455
+ const clusterId = properties.cluster_id;
1456
+ if (typeof clusterId !== "number" || !feature.source) return;
1457
+ activateCluster(
1458
+ {
1459
+ clusterId,
1460
+ coordinate,
1461
+ kind: "cluster",
1462
+ pointCount: typeof properties.point_count === "number" ? properties.point_count : 0,
1463
+ properties,
1464
+ source: feature.source
1465
+ },
1466
+ modality
1467
+ );
1468
+ const metadata = map.getLayer(feature.layer.id)?.metadata;
1469
+ if (metadata?.[clusterMetadataKey2]?.expandOnClick === false) return;
1470
+ const source = map.getSource(feature.source);
1471
+ void source?.getClusterExpansionZoom?.(clusterId).then(
1472
+ (zoom) => {
1473
+ if (!disposed) expand(coordinate, zoom);
1474
+ },
1475
+ () => void 0
1476
+ );
1477
+ };
1478
+ const isFocused = ({ element }) => element.ownerDocument?.activeElement === element;
1479
+ const hasFocusedClusterMarker = () => [...clusterMarkers.values()].some(isFocused);
1480
+ const removeClusterMarker = (id, entry) => {
1481
+ entry.element.removeEventListener("click", entry.listener);
1482
+ entry.marker.remove();
1483
+ entry.element.remove();
1484
+ clusterMarkers.delete(id);
1485
+ };
1486
+ const clusterLabel = (count) => input.annotationClusters?.formatAriaLabel?.(count) ?? `${count} places`;
1487
+ const createClusterMarker = (cluster) => {
1488
+ const element = options.document.createElement("button");
1489
+ element.type = "button";
1490
+ element.className = "tileflow-interaction-cluster";
1491
+ element.textContent = cluster.label;
1492
+ element.setAttribute("aria-label", clusterLabel(cluster.pointCount));
1493
+ element.setAttribute("data-tileflow-interaction", "cluster");
1494
+ configureClusterStyles(element);
1495
+ const listener = (event) => {
1496
+ activateCluster(
1497
+ {
1498
+ annotationIds: cluster.annotationIds,
1499
+ clusterId: cluster.clusterId,
1500
+ coordinate: cluster.coordinate,
1501
+ kind: "cluster",
1502
+ pointCount: cluster.pointCount,
1503
+ properties: { point_count: cluster.pointCount }
1504
+ },
1505
+ // Browsers report a click from Enter or Space with a detail of zero.
1506
+ event.detail === 0 ? "keyboard" : "pointer"
1507
+ );
1508
+ if (input.annotationClusters?.expandOnClick !== false) {
1509
+ expand(cluster.coordinate, cluster.expansionZoom);
1510
+ }
1511
+ };
1512
+ element.addEventListener("click", listener);
1513
+ const marker = options.createMarker({ element });
1514
+ marker.setLngLat([cluster.coordinate[0], cluster.coordinate[1]]);
1515
+ marker.addTo(map);
1516
+ clusterMarkers.set(cluster.clusterId, {
1517
+ element,
1518
+ listener,
1519
+ marker,
1520
+ pointCount: cluster.pointCount
1521
+ });
1522
+ };
1523
+ const renderAnnotationClusters = () => {
1524
+ if (disposed) return;
1525
+ if (!index) {
1526
+ for (const [id, entry] of [...clusterMarkers]) removeClusterMarker(id, entry);
1527
+ options.setHiddenAnnotations?.(/* @__PURE__ */ new Set());
1528
+ renderedZoom = void 0;
1529
+ return;
1530
+ }
1531
+ const zoom = Math.floor(map.getZoom());
1532
+ renderedZoom = zoom;
1533
+ const view = index.at(zoom);
1534
+ const clusters = new Map(view.clusters.map((cluster) => [cluster.clusterId, cluster]));
1535
+ let focusLost = false;
1536
+ for (const [id, entry] of [...clusterMarkers]) {
1537
+ if (clusters.has(id)) continue;
1538
+ focusLost ||= isFocused(entry);
1539
+ removeClusterMarker(id, entry);
1540
+ }
1541
+ for (const [id, cluster] of clusters) {
1542
+ if (!clusterMarkers.has(id)) createClusterMarker(cluster);
1543
+ }
1544
+ options.setHiddenAnnotations?.(view.hiddenAnnotationIds);
1545
+ if (focusLost) map.getCanvas().focus?.({ preventScroll: true });
1546
+ };
1547
+ const handleZoom = () => {
1548
+ if (index && Math.floor(map.getZoom()) !== renderedZoom) renderAnnotationClusters();
1549
+ };
1550
+ const syncAnnotationClusters = () => {
1551
+ const settings = input.annotationClusters;
1552
+ let focusLost = false;
1553
+ if (!settings) {
1554
+ index = void 0;
1555
+ } else {
1556
+ index = createTileflowAnnotationClusterIndex(input.annotations ?? [], settings);
1557
+ focusLost = hasFocusedClusterMarker();
1558
+ for (const [id, entry] of [...clusterMarkers]) removeClusterMarker(id, entry);
1559
+ }
1560
+ renderAnnotationClusters();
1561
+ if (focusLost) map.getCanvas().focus?.({ preventScroll: true });
1562
+ };
1563
+ const refreshClusterLabels = () => {
1564
+ for (const { element, pointCount } of clusterMarkers.values()) {
1565
+ element.setAttribute("aria-label", clusterLabel(pointCount));
1566
+ }
1567
+ };
1568
+ const stops = [
1569
+ on("style.load", syncStyle),
1570
+ on("styledata", syncStyle),
1571
+ on("click", handleClick),
1572
+ on("zoom", handleZoom)
1573
+ ];
1574
+ return Object.freeze({
1575
+ dispose() {
1576
+ if (disposed) return;
1577
+ disposed = true;
1578
+ for (const stop of stops) stop();
1579
+ stopHover();
1580
+ for (const [id, entry] of [...clusterMarkers]) removeClusterMarker(id, entry);
1581
+ try {
1582
+ if (index) options.setHiddenAnnotations?.(/* @__PURE__ */ new Set());
1583
+ } catch {
1584
+ }
1585
+ try {
1586
+ for (const id of [...overlays.keys()]) removeOverlay(id);
1587
+ for (const id of [...sources.keys()]) removeSource(id);
1588
+ } catch {
1589
+ }
1590
+ },
1591
+ reconcile(next) {
1592
+ if (disposed) return;
1593
+ const key = clusterInputKey(next);
1594
+ const changed = key !== annotationClusterKey;
1595
+ annotationClusterKey = key;
1596
+ input = next;
1597
+ reported.clear();
1598
+ syncStyle();
1599
+ if (changed) syncAnnotationClusters();
1600
+ else refreshClusterLabels();
1601
+ }
1602
+ });
1603
+ }
1604
+ function clusterInputKey(input) {
1605
+ if (!input.annotationClusters) return "";
1606
+ const positions = (input.annotations ?? []).map(({ coordinate, id }) => [id, ...coordinate]);
1607
+ return JSON.stringify([input.annotationClusters, positions]);
1608
+ }
1609
+ function compareEntries([left], [right]) {
1610
+ return left < right ? -1 : left > right ? 1 : 0;
1611
+ }
1612
+ function pointerModality(event) {
1613
+ const original = event.originalEvent;
1614
+ return original?.pointerType === "touch" || original?.touches !== void 0 ? "touch" : "pointer";
1615
+ }
1616
+ function eventCoordinate(event) {
1617
+ return event.lngLat ? [event.lngLat.lng, event.lngLat.lat] : void 0;
1618
+ }
1619
+ function pointCoordinate(geometry) {
1620
+ const value = geometry;
1621
+ const coordinates = value?.coordinates;
1622
+ return value?.type === "Point" && Array.isArray(coordinates) && typeof coordinates[0] === "number" && typeof coordinates[1] === "number" ? [coordinates[0], coordinates[1]] : void 0;
1623
+ }
1624
+ function jsonProperties(properties) {
1625
+ return Object.fromEntries(
1626
+ Object.entries(properties ?? {}).filter(([, value]) => value !== void 0)
1627
+ );
1628
+ }
1629
+ function configureClusterStyles(element) {
1630
+ element.style.alignItems = "center";
1631
+ element.style.background = "var(--tileflow-interaction-cluster-color, #2563eb)";
1632
+ element.style.borderColor = "var(--tileflow-interaction-border-color, #ffffff)";
1633
+ element.style.borderRadius = "9999px";
1634
+ element.style.borderStyle = "solid";
1635
+ element.style.borderWidth = "2px";
1636
+ element.style.boxShadow = "var(--tileflow-interaction-shadow, 0 1px 4px rgb(0 0 0 / 0.25))";
1637
+ element.style.color = "var(--tileflow-interaction-foreground, #ffffff)";
1638
+ element.style.cursor = "pointer";
1639
+ element.style.display = "inline-flex";
1640
+ element.style.justifyContent = "center";
1641
+ element.style.minHeight = "2.25rem";
1642
+ element.style.minWidth = "2.25rem";
1643
+ element.style.padding = "0.25rem";
1644
+ }
1645
+
1183
1646
  // src/reducer.ts
1184
1647
  var initialTileflowInteractionState = Object.freeze({
1185
1648
  popup: null
@@ -1789,7 +2252,9 @@ function createTileflowMapLibrePoiController(options) {
1789
2252
  };
1790
2253
  const handleClick = (value) => {
1791
2254
  const event = parsePointerEvent(value);
1792
- if (!event || disposed) return;
2255
+ if (!event || disposed || value.defaultPrevented === true) {
2256
+ return;
2257
+ }
1793
2258
  const modality = inputModality(event);
1794
2259
  const match = hitTest(event);
1795
2260
  if (!match) return;
@@ -2888,6 +3353,7 @@ function unsubscribe(values, value) {
2888
3353
  }
2889
3354
  export {
2890
3355
  createTileflowAnnotationRegistry,
3356
+ createTileflowMapLibreDataRuntime,
2891
3357
  createTileflowMapLibreDomRuntime,
2892
3358
  createTileflowMapLibreInteractionCoordinator,
2893
3359
  createTileflowMapLibrePoiController,
@@ -152,7 +152,20 @@ type TileflowResolvedMapTarget = {
152
152
  coordinate: TileflowInteractionCoordinate;
153
153
  kind: 'map';
154
154
  };
155
- type TileflowResolvedInteractionTarget<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = TileflowResolvedAnnotationTarget<TAnnotation> | TileflowResolvedSemanticFeatureTarget | TileflowResolvedStyleFeatureTarget | TileflowResolvedMapTarget;
155
+ /** One clustered group of annotations or GeoJSON points. */
156
+ type TileflowResolvedClusterTarget = {
157
+ /** The annotations inside the group, when annotations were clustered. */
158
+ annotationIds?: readonly string[];
159
+ clusterId: number;
160
+ coordinate: TileflowInteractionCoordinate;
161
+ kind: 'cluster';
162
+ pointCount: number;
163
+ /** Cluster properties, including any `clusterProperties` aggregates. */
164
+ properties: Readonly<Record<string, TileflowInteractionJsonValue>>;
165
+ /** The GeoJSON source, when a source was clustered. */
166
+ source?: string;
167
+ };
168
+ type TileflowResolvedInteractionTarget<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = TileflowResolvedAnnotationTarget<TAnnotation> | TileflowResolvedClusterTarget | TileflowResolvedSemanticFeatureTarget | TileflowResolvedStyleFeatureTarget | TileflowResolvedMapTarget;
156
169
  type TileflowAnnotationViewContext<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = {
157
170
  annotation: TAnnotation;
158
171
  close: () => void;
@@ -427,10 +440,10 @@ declare const tileflowInteractionTargetRefSchema: z.ZodMiniDiscriminatedUnion<[z
427
440
  kind: z.ZodMiniLiteral<"annotation">;
428
441
  }, z.core.$strict>, z.ZodMiniObject<{
429
442
  domain: z.ZodMiniString<string>;
430
- featureId: z.ZodMiniUnion<readonly [z.ZodMiniNumberFormat, z.ZodMiniString<string>]>;
443
+ featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
431
444
  kind: z.ZodMiniLiteral<"semantic-feature">;
432
445
  }, z.core.$strict>, z.ZodMiniObject<{
433
- featureId: z.ZodMiniUnion<readonly [z.ZodMiniNumberFormat, z.ZodMiniString<string>]>;
446
+ featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
434
447
  kind: z.ZodMiniLiteral<"style-feature">;
435
448
  layerId: z.ZodMiniString<string>;
436
449
  }, z.core.$strict>, z.ZodMiniObject<{
@@ -443,10 +456,10 @@ declare const tileflowInteractionStateSchema: z.ZodMiniObject<{
443
456
  kind: z.ZodMiniLiteral<"annotation">;
444
457
  }, z.core.$strict>, z.ZodMiniObject<{
445
458
  domain: z.ZodMiniString<string>;
446
- featureId: z.ZodMiniUnion<readonly [z.ZodMiniNumberFormat, z.ZodMiniString<string>]>;
459
+ featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
447
460
  kind: z.ZodMiniLiteral<"semantic-feature">;
448
461
  }, z.core.$strict>, z.ZodMiniObject<{
449
- featureId: z.ZodMiniUnion<readonly [z.ZodMiniNumberFormat, z.ZodMiniString<string>]>;
462
+ featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
450
463
  kind: z.ZodMiniLiteral<"style-feature">;
451
464
  layerId: z.ZodMiniString<string>;
452
465
  }, z.core.$strict>, z.ZodMiniObject<{
@@ -460,10 +473,10 @@ declare const tileflowInteractionActionSchema: z.ZodMiniDiscriminatedUnion<[z.Zo
460
473
  kind: z.ZodMiniLiteral<"annotation">;
461
474
  }, z.core.$strict>, z.ZodMiniObject<{
462
475
  domain: z.ZodMiniString<string>;
463
- featureId: z.ZodMiniUnion<readonly [z.ZodMiniNumberFormat, z.ZodMiniString<string>]>;
476
+ featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
464
477
  kind: z.ZodMiniLiteral<"semantic-feature">;
465
478
  }, z.core.$strict>, z.ZodMiniObject<{
466
- featureId: z.ZodMiniUnion<readonly [z.ZodMiniNumberFormat, z.ZodMiniString<string>]>;
479
+ featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
467
480
  kind: z.ZodMiniLiteral<"style-feature">;
468
481
  layerId: z.ZodMiniString<string>;
469
482
  }, z.core.$strict>, z.ZodMiniObject<{
@@ -475,7 +488,44 @@ declare const tileflowInteractionActionSchema: z.ZodMiniDiscriminatedUnion<[z.Zo
475
488
  type: z.ZodMiniLiteral<"close-popup">;
476
489
  }, z.core.$strict>], "type">;
477
490
 
478
- type TileflowInteractionDiagnosticCode = 'DUPLICATE_ANNOTATION_ID' | 'INVALID_FIELD' | 'INVALID_ANNOTATION' | 'INVALID_DOCUMENT' | 'LIMIT_EXCEEDED' | 'MISSING_VIEW' | 'OVERLAY_FAILURE' | 'SEMANTIC_MANIFEST_MISMATCH' | 'STALE_TARGET' | 'UNSTABLE_FEATURE_IDENTITY' | 'UNSUPPORTED_MODE' | 'UNSUPPORTED_TARGET';
491
+ type TileflowAnnotationClusterOptions = Readonly<{
492
+ /** The last zoom that clusters; annotations are separate above it. Defaults to 14. */
493
+ clusterMaxZoom?: number;
494
+ /** The fewest annotations that form a cluster. Defaults to 2. */
495
+ clusterMinPoints?: number;
496
+ /** Cluster radius in CSS pixels or density-independent points. Defaults to 50. */
497
+ clusterRadius?: number;
498
+ /** Zoom in when a cluster is activated. Defaults to true. */
499
+ expandOnClick?: boolean;
500
+ /** The accessible name of a cluster. Defaults to `${count} places`. */
501
+ formatAriaLabel?: (count: number) => string;
502
+ }>;
503
+ type TileflowAnnotationCluster = Readonly<{
504
+ annotationIds: readonly string[];
505
+ clusterId: number;
506
+ coordinate: TileflowInteractionCoordinate;
507
+ /** The zoom at which this cluster splits. */
508
+ expansionZoom: number;
509
+ /** `point_count_abbreviated`, such as `1.2k`. */
510
+ label: string;
511
+ pointCount: number;
512
+ }>;
513
+ type TileflowAnnotationClusterView = Readonly<{
514
+ clusters: readonly TileflowAnnotationCluster[];
515
+ /** Annotations a cluster stands in for at this zoom. */
516
+ hiddenAnnotationIds: ReadonlySet<string>;
517
+ }>;
518
+ type TileflowAnnotationClusterIndex = Readonly<{
519
+ /** Groups for one map zoom. Results depend on its integer part only. */
520
+ at: (zoom: number) => TileflowAnnotationClusterView;
521
+ }>;
522
+ /**
523
+ * Groups annotations the way a clustering GeoJSON source groups points, with the same option names.
524
+ * It is pure, so web and native renderers share one result.
525
+ */
526
+ declare function createTileflowAnnotationClusterIndex(annotations: readonly TileflowAnnotation[], options?: TileflowAnnotationClusterOptions): TileflowAnnotationClusterIndex;
527
+
528
+ type TileflowInteractionDiagnosticCode = 'DUPLICATE_ANNOTATION_ID' | 'INVALID_FIELD' | 'INVALID_ANNOTATION' | 'INVALID_DATA_LAYER' | 'INVALID_DOCUMENT' | 'LIMIT_EXCEEDED' | 'MISSING_VIEW' | 'OVERLAY_FAILURE' | 'SEMANTIC_MANIFEST_MISMATCH' | 'STALE_TARGET' | 'UNSTABLE_FEATURE_IDENTITY' | 'UNSUPPORTED_MODE' | 'UNSUPPORTED_TARGET';
479
529
  type TileflowInteractionDiagnostic = {
480
530
  code: TileflowInteractionDiagnosticCode;
481
531
  level: 'error' | 'warning';
@@ -503,4 +553,4 @@ type TileflowInteractionBindingValidationResult = {
503
553
  declare function validateTileflowAnnotations(input: unknown): TileflowAnnotationValidationResult;
504
554
  declare function validateTileflowInteractionBindings(input: unknown): TileflowInteractionBindingValidationResult;
505
555
 
506
- export { type TileflowPoiProperties as A, type TileflowPoiSizeRank as B, type TileflowResolvedAnnotationTarget as C, type TileflowResolvedInteractionTarget as D, type TileflowResolvedMapTarget as E, type TileflowResolvedPoiFeature as F, type TileflowResolvedPoiFeatureTarget as G, type TileflowResolvedSemanticFeature as H, type TileflowResolvedSemanticFeatureTarget as I, type TileflowResolvedStyleFeatureTarget as J, tileflowAnnotationSchema as K, tileflowAnnotationsSchema as L, tileflowInteractionActionSchema as M, tileflowInteractionBindingSchema as N, tileflowInteractionBindingsSchema as O, tileflowInteractionContentSchema as P, tileflowInteractionJsonValueSchema as Q, tileflowInteractionLimits as R, tileflowInteractionSchemaVersion as S, type TileflowInteractionState as T, tileflowInteractionStateSchema as U, tileflowInteractionTargetRefSchema as V, tileflowInteractionTargetSchema as W, tileflowPoiCategories as X, validateTileflowAnnotations as Y, validateTileflowInteractionBindings as Z, type TileflowInteractionAction as a, type TileflowInteractionTargetRef as b, type TileflowAnnotation as c, type TileflowAnnotationInteractionEvent as d, type TileflowAnnotationMarker as e, type TileflowAnnotationSurface as f, type TileflowAnnotationValidationResult as g, type TileflowAnnotationViewContext as h, type TileflowInteractionBinding as i, type TileflowInteractionBindingValidationResult as j, type TileflowInteractionContent as k, type TileflowInteractionCoordinate as l, type TileflowInteractionDiagnostic as m, type TileflowInteractionDiagnosticCode as n, type TileflowInteractionEvent as o, type TileflowInteractionEventType as p, type TileflowInteractionFieldContent as q, type TileflowInteractionInputModality as r, type TileflowInteractionJsonPrimitive as s, type TileflowInteractionJsonValue as t, type TileflowInteractionTarget as u, type TileflowInteractionTextContent as v, type TileflowInteractionViewContent as w, type TileflowInteractionViewContext as x, type TileflowPoiCategory as y, type TileflowPoiFilterRank as z };
556
+ export { tileflowInteractionTargetRefSchema as $, type TileflowInteractionViewContent as A, type TileflowInteractionViewContext as B, type TileflowPoiCategory as C, type TileflowPoiFilterRank as D, type TileflowPoiProperties as E, type TileflowPoiSizeRank as F, type TileflowResolvedAnnotationTarget as G, type TileflowResolvedClusterTarget as H, type TileflowResolvedInteractionTarget as I, type TileflowResolvedMapTarget as J, type TileflowResolvedPoiFeature as K, type TileflowResolvedPoiFeatureTarget as L, type TileflowResolvedSemanticFeature as M, type TileflowResolvedSemanticFeatureTarget as N, type TileflowResolvedStyleFeatureTarget as O, createTileflowAnnotationClusterIndex as P, tileflowAnnotationSchema as Q, tileflowAnnotationsSchema as R, tileflowInteractionActionSchema as S, type TileflowInteractionState as T, tileflowInteractionBindingSchema as U, tileflowInteractionBindingsSchema as V, tileflowInteractionContentSchema as W, tileflowInteractionJsonValueSchema as X, tileflowInteractionLimits as Y, tileflowInteractionSchemaVersion as Z, tileflowInteractionStateSchema as _, type TileflowInteractionAction as a, tileflowInteractionTargetSchema as a0, tileflowPoiCategories as a1, validateTileflowAnnotations as a2, validateTileflowInteractionBindings as a3, type TileflowInteractionTargetRef as b, type TileflowAnnotation as c, type TileflowAnnotationCluster as d, type TileflowAnnotationClusterIndex as e, type TileflowAnnotationClusterOptions as f, type TileflowAnnotationClusterView as g, type TileflowAnnotationInteractionEvent as h, type TileflowAnnotationMarker as i, type TileflowAnnotationSurface as j, type TileflowAnnotationValidationResult as k, type TileflowAnnotationViewContext as l, type TileflowInteractionBinding as m, type TileflowInteractionBindingValidationResult as n, type TileflowInteractionContent as o, type TileflowInteractionCoordinate as p, type TileflowInteractionDiagnostic as q, type TileflowInteractionDiagnosticCode as r, type TileflowInteractionEvent as s, type TileflowInteractionEventType as t, type TileflowInteractionFieldContent as u, type TileflowInteractionInputModality as v, type TileflowInteractionJsonPrimitive as w, type TileflowInteractionJsonValue as x, type TileflowInteractionTarget as y, type TileflowInteractionTextContent as z };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@tileflow/interactions",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.8",
4
4
  "license": "Apache-2.0",
5
- "description": "Portable annotation and interaction contracts for Tileflow maps.",
5
+ "description": "Portable annotation, clustering and interaction contracts for Tileflow maps.",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
8
  "homepage": "https://tileflow.dev",
@@ -24,7 +24,8 @@
24
24
  "types": "./dist/index.d.ts",
25
25
  "files": [
26
26
  "dist",
27
- "LICENSE"
27
+ "LICENSE",
28
+ "REFERENCE.md"
28
29
  ],
29
30
  "exports": {
30
31
  ".": {
@@ -43,7 +44,8 @@
43
44
  "access": "public"
44
45
  },
45
46
  "dependencies": {
46
- "zod": "^4.4.3"
47
+ "supercluster": "^8.0.1",
48
+ "zod": "^4.6.7"
47
49
  },
48
50
  "peerDependencies": {
49
51
  "maplibre-gl": ">=6.4.1 <7"
@@ -53,6 +55,10 @@
53
55
  "optional": true
54
56
  }
55
57
  },
58
+ "devDependencies": {
59
+ "@types/supercluster": "^7.1.3",
60
+ "maplibre-gl": "6.16.0"
61
+ },
56
62
  "scripts": {
57
63
  "build": "tsup src/index.ts src/maplibre-entry.ts --format esm --dts --clean --no-splitting",
58
64
  "verify": "pnpm run build && tsx --test test/*.test.ts",