@trackunit/react-map-adapter-shared 0.0.4-alpha-9d327375fc1.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.
@@ -0,0 +1,352 @@
1
+ import type { GeoJsonBbox, GeoJsonPosition } from "@trackunit/geo-json-utils";
2
+ import type { DomPortalStackingInput, DomPortalStackingResult } from "./markerDomPortalStacking";
3
+ import type { MapTheme } from "./primitiveMapTypes";
4
+ type ReactNode = import("react").ReactNode;
5
+ export type { DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, } from "./markerDomPortalStacking";
6
+ export { computeMarkerDomPortalZIndex } from "./markerDomPortalStacking";
7
+ /**
8
+ * The medium an adaptive marker is rendered through: a map-native symbol
9
+ * (WebGL/canvas circle) or a DOM portal. The indicator-vs-stick distinction
10
+ * is a component-level concern (signaled by `MapMarker`'s `stickPositioning`
11
+ * presence), not part of the adaptive vocabulary (ADR-0009 / ADR-0010).
12
+ */
13
+ export type RenderMedium = "symbol" | "dom";
14
+ /**
15
+ * Fields common to every render-state shape (ADR-0009).
16
+ *
17
+ * Threaded through `<Layers>`; passed to `render` callbacks regardless of
18
+ * mode or medium so consumers can branch on selection / hover / theme without
19
+ * looking these up themselves.
20
+ */
21
+ export type CommonRenderState = Readonly<{
22
+ selected: boolean;
23
+ hovered: boolean;
24
+ /** Basemap theme from `Map` appearance (threaded through `<Layers>`). */
25
+ theme: MapTheme;
26
+ }>;
27
+ /**
28
+ * Render-state for `mode: "symbol"` and the symbol arm of `mode: "adaptive"`
29
+ * (ADR-0009).
30
+ *
31
+ * Symbol render returns a {@link CircleSymbolDescriptor}; no DOM-only fields
32
+ * (`labelVisible`, `isMounting`) are surfaced here — they have no meaning for
33
+ * a WebGL/canvas circle.
34
+ */
35
+ export type SymbolRenderState = CommonRenderState & Readonly<{
36
+ medium: "symbol";
37
+ }>;
38
+ /**
39
+ * Render-state for `mode: "dom"` and the DOM arm of `mode: "adaptive"`
40
+ * (ADR-0009).
41
+ *
42
+ * - `labelVisible`: `false` when collision avoidance forces the marker to
43
+ * stay in circle form (plan §4 / §8). DOM-only.
44
+ * - `isMounting`: `true` for the very first frame after a symbol→DOM
45
+ * transition so consumers can suppress the "first-paint" entry animation
46
+ * on adaptive markers. Always `false` until slice 6 (SAGA-424) introduces
47
+ * `useMarkerMountBridge`.
48
+ */
49
+ export type DomRenderState = CommonRenderState & Readonly<{
50
+ medium: "dom";
51
+ labelVisible: boolean;
52
+ isMounting: boolean;
53
+ }>;
54
+ /**
55
+ * Render-state for `mode: "adaptive"` (ADR-0009).
56
+ *
57
+ * Discriminated on `medium`. Adaptive `render` is invoked with whichever
58
+ * shape matches the medium chosen by `resolveMode` for the current frame.
59
+ */
60
+ export type AdaptiveRenderState = SymbolRenderState | DomRenderState;
61
+ /**
62
+ * Inputs to `resolveMode` for adaptive rendering.
63
+ */
64
+ export type AdaptiveResolutionContext = Readonly<{
65
+ zoom: number;
66
+ markersInViewport: number;
67
+ isHovered: boolean;
68
+ isSelected: boolean;
69
+ }>;
70
+ /**
71
+ * Inputs to `resolveGroups` for cluster entity-shape decisions (ADR-0016).
72
+ *
73
+ * Mirrors {@link AdaptiveResolutionContext} but adds `viewportBounds` so the
74
+ * consumer can branch on geographic extent in addition to zoom and density.
75
+ */
76
+ export type ResolutionContext = Readonly<{
77
+ zoom: number;
78
+ markersInViewport: number;
79
+ isHovered: boolean;
80
+ isSelected: boolean;
81
+ viewportBounds: GeoJsonBbox | null;
82
+ }>;
83
+ /**
84
+ * Render-state passed as the second argument to `clusterRender`'s render
85
+ * callback (ADR-0016).
86
+ *
87
+ * Extends {@link CommonRenderState} with `memberItems` — the cluster's
88
+ * members cross-referenced from the hook's `data` array via `getMarkerIds`.
89
+ * Null when any member ID is missing from the index (the consumer cannot
90
+ * resolve the full member set in this frame).
91
+ *
92
+ * Cluster renders never receive the marker-only fields `medium`,
93
+ * `labelVisible`, or `isMounting` — clusters have no adaptive symbol↔DOM
94
+ * transition (ADR-0016: `MapCluster` is DOM-only; symbol cluster mode still
95
+ * returns a static {@link SymbolDescriptor}) and no per-item collision label.
96
+ */
97
+ export type ClusterRenderState<TItem> = CommonRenderState & Readonly<{
98
+ memberItems: ReadonlyArray<TItem> | null;
99
+ }>;
100
+ /**
101
+ * Symbol cluster render: cluster bubble drawn as a map-native circle
102
+ * (WebGL/canvas) sized to the cluster count.
103
+ *
104
+ * Receives {@link ClusterRenderState} — `memberItems` is always `null` here
105
+ * because the symbol pipeline doesn't materialize the member set; consumers
106
+ * that need `memberItems` should use {@link ClusterDomRenderConfig}.
107
+ */
108
+ export type ClusterSymbolRenderConfig<TCluster, TItem> = Readonly<{
109
+ mode: "symbol";
110
+ render(cluster: TCluster, state: ClusterRenderState<TItem>): SymbolDescriptor;
111
+ }>;
112
+ /**
113
+ * DOM cluster render: cluster bubble rendered as a React subtree inside a
114
+ * portal positioned by the adapter.
115
+ *
116
+ * Receives {@link ClusterRenderState} with `memberItems` cross-referenced
117
+ * from the hook's `data` array via `getMarkerIds`. Consumers can use it to
118
+ * build the `sticks` array for `MapCluster` directly, without maintaining
119
+ * a separate lookup table.
120
+ */
121
+ export type ClusterDomRenderConfig<TCluster, TItem> = Readonly<{
122
+ mode: "dom";
123
+ /** Anchor position relative to the cluster coordinate. */
124
+ anchor: MarkerAnchor;
125
+ /** Additional pixel displacement applied after anchor alignment. */
126
+ pixelOffset?: PixelOffset;
127
+ /** Optional portal stacking resolver, mirrors {@link DomRenderConfig.resolveDomPortalStacking}. */
128
+ resolveDomPortalStacking?(cluster: TCluster, input: DomPortalStackingInput): DomPortalStackingResult;
129
+ render(cluster: TCluster, state: ClusterRenderState<TItem>): ReactNode;
130
+ }>;
131
+ /**
132
+ * Cluster render configuration (ADR-0016).
133
+ *
134
+ * Unlike marker `RenderConfig`, clusters have no `adaptive` mode — the
135
+ * cluster entity-shape decision is owned by the `resolveGroups` callback,
136
+ * and `MapCluster` is DOM-only. If a future cluster type needs symbol↔DOM
137
+ * adaptive rendering, it can be added without breaking changes by extending
138
+ * this union.
139
+ */
140
+ export type ClusterRenderConfig<TCluster, TItem> = Readonly<ClusterSymbolRenderConfig<TCluster, TItem> | ClusterDomRenderConfig<TCluster, TItem>>;
141
+ /**
142
+ * Anchor position for a marker relative to its coordinate.
143
+ * WebGL circle layers (Mapbox) are always center-anchored; this field
144
+ * is consumed by DOM-based markers (Google Maps, promoted overlays).
145
+ */
146
+ export type MarkerAnchor = "center" | "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
147
+ /**
148
+ * Additional pixel displacement applied after anchor alignment.
149
+ * Positive x moves right, positive y moves down (screen coordinates).
150
+ */
151
+ export type PixelOffset = Readonly<{
152
+ x: number;
153
+ y: number;
154
+ }>;
155
+ /**
156
+ * Provider-agnostic descriptor for a circular symbol marker (WebGL/canvas).
157
+ * Per ADR-0011: the only symbol kind today is a circle; explicit pixel sizing
158
+ * (no unitless `scale`) and opt-in border (no implicit white stroke).
159
+ */
160
+ export type CircleSymbolDescriptor = Readonly<{
161
+ /** Fill color as a CSS color string. */
162
+ color: string;
163
+ /** Diameter in pixels. Default: 16. */
164
+ diameterPx?: number;
165
+ /** Opacity from 0 to 1. Default: 1. */
166
+ opacity?: number;
167
+ /** Border (stroke) color, or `null` for no border. Default: null. */
168
+ borderColor?: string | null;
169
+ /** Border (stroke) width in pixels. Default: 0. */
170
+ borderWidthPx?: number;
171
+ }>;
172
+ /**
173
+ * Symbol descriptor returned by symbol/adaptive render callbacks.
174
+ *
175
+ * Today this is exactly a {@link CircleSymbolDescriptor}. Kept as a type alias
176
+ * so a future symbol kind can extend it without breaking changes (the alias
177
+ * becomes a discriminated union with a `kind` discriminator defaulting to
178
+ * `"circle"`).
179
+ */
180
+ export type SymbolDescriptor = CircleSymbolDescriptor;
181
+ /**
182
+ * Provider-agnostic style for cluster symbols (separate from individual marker
183
+ * symbols since clusters carry a count label and dynamic sizing).
184
+ */
185
+ export type ClusterSymbolStyle = Readonly<{
186
+ /** Background color of the cluster indicator */
187
+ color?: string;
188
+ /** Text color for the count label */
189
+ textColor?: string;
190
+ /** Size multiplier (1 = default) */
191
+ scale?: number;
192
+ /** Opacity from 0 to 1 */
193
+ opacity?: number;
194
+ }>;
195
+ /**
196
+ * Symbol mode: map-native rendering (WebGL/canvas), high performance.
197
+ *
198
+ * Note: `render` uses method syntax (bivariant) to allow type erasure at the
199
+ * layer handle boundary (`RenderConfig<TItem>` → `RenderConfig<unknown>`).
200
+ * For static styles, use `render: () => mySymbol`.
201
+ *
202
+ * Per ADR-0009, symbol mode receives a {@link SymbolRenderState}; per-item
203
+ * `selected` / `hovered` variations are not surfaced through this callback —
204
+ * they are handled by the adapter's paint expression / canvas redraw.
205
+ * `selected` and `hovered` are always `false` here.
206
+ */
207
+ export type SymbolRenderConfig<TData> = Readonly<{
208
+ mode: "symbol";
209
+ /** Render per item. For static symbols use `() => mySymbol`. */
210
+ render(item: TData, state: SymbolRenderState): SymbolDescriptor;
211
+ }>;
212
+ /**
213
+ * DOM mode: React components, maximum flexibility.
214
+ *
215
+ * Per ADR-0009, DOM mode receives a {@link DomRenderState} with `medium: "dom"`
216
+ * plus the DOM-only fields `labelVisible` and `isMounting` (`isMounting` is
217
+ * always `false` until slice 6 / SAGA-424 wires `useMarkerMountBridge`).
218
+ */
219
+ export type DomRenderConfig<TData> = Readonly<{
220
+ mode: "dom";
221
+ /**
222
+ * Anchor position relative to the coordinate. Anchor is geometric input
223
+ * (where the DOM box meets the geo coordinate), not a styling preference.
224
+ * Mirrors {@link AdaptiveRenderConfig.anchor}.
225
+ *
226
+ * Use `"center"` for plain indicator/stick markers rendered with `<MapMarker>`.
227
+ */
228
+ anchor: MarkerAnchor;
229
+ /** Additional pixel displacement applied after anchor alignment. */
230
+ pixelOffset?: PixelOffset;
231
+ /**
232
+ * Optional: map geometry + interaction phase → portal `z-index` via {@link computeMarkerDomPortalZIndex}.
233
+ * Omit to keep legacy interaction-only stacking (equivalent to `circle` + selected/hover/idle).
234
+ */
235
+ resolveDomPortalStacking?(item: TData, input: DomPortalStackingInput): DomPortalStackingResult;
236
+ /**
237
+ * Render function. Receives the original data item and interaction state.
238
+ *
239
+ * Custom DOM markers can opt into the same hover-safe-area behavior as
240
+ * `<MapMarker>` by adding `data-map-marker-hit-surface` to the visible
241
+ * interactive element and, when the visible surface is spatially separated
242
+ * from the geographic anchor, `data-map-marker-anchor` to the anchor element.
243
+ * See `HIT_SURFACE_SELECTOR` and `ANCHOR_SELECTOR`.
244
+ */
245
+ render(item: TData, state: DomRenderState): ReactNode;
246
+ }>;
247
+ /**
248
+ * Adaptive mode: each item is either a WebGL/canvas symbol or a DOM marker,
249
+ * chosen per frame by `resolveMode` (plan §8).
250
+ *
251
+ * Per ADR-0009, a single `render(item, state)` callback covers both media.
252
+ * `state.medium` discriminates: when it is `"symbol"` the callback must
253
+ * return a {@link SymbolDescriptor}; when it is `"dom"` it must return a
254
+ * `ReactNode`. Adapters route the result via
255
+ * `discriminateRenderResult`.
256
+ *
257
+ * `anchor` and `pixelOffset` apply to the DOM-medium branch (mirrors
258
+ * {@link DomRenderConfig}); the symbol branch is map-native and ignores them.
259
+ *
260
+ * Note: `render` uses method syntax (bivariant) to allow type erasure at
261
+ * the layer-handle boundary (`RenderConfig<TItem>` → `RenderConfig<unknown>`).
262
+ */
263
+ export type AdaptiveRenderConfig<TData> = Readonly<{
264
+ mode: "adaptive";
265
+ /**
266
+ * Anchor for DOM-medium renders. Symbol circles are always center-anchored,
267
+ * so DOM indicators should use `"center"` unless you have a deliberate reason
268
+ * to deviate.
269
+ *
270
+ * Use `"center"` for plain indicator/stick markers rendered with `<MapMarker>`.
271
+ */
272
+ anchor: MarkerAnchor;
273
+ /** Pixel offset for DOM-medium renders. */
274
+ pixelOffset?: PixelOffset;
275
+ /**
276
+ * Optional: DOM portal stacking; see {@link DomRenderConfig.resolveDomPortalStacking}.
277
+ * Invoked only when the resolved medium is `dom`.
278
+ */
279
+ resolveDomPortalStacking?(item: TData, input: DomPortalStackingInput): DomPortalStackingResult;
280
+ resolveMode(item: TData, ctx: AdaptiveResolutionContext): RenderMedium;
281
+ /**
282
+ * Called per item, per frame, with whichever render-state matches the
283
+ * medium chosen by {@link resolveMode}. Return a {@link SymbolDescriptor}
284
+ * for `state.medium === "symbol"` and a `ReactNode` for
285
+ * `state.medium === "dom"`.
286
+ *
287
+ * DOM-medium custom markers can opt into hover-safe-area detection with
288
+ * `data-map-marker-hit-surface` and `data-map-marker-anchor`; see
289
+ * {@link DomRenderConfig.render}.
290
+ */
291
+ render(item: TData, state: AdaptiveRenderState): SymbolDescriptor | ReactNode;
292
+ }>;
293
+ /**
294
+ * Per-feature modes computed in `<Layers>` for adaptive marker sources.
295
+ */
296
+ export type AdaptiveMarkerResolution = Readonly<{
297
+ modesByFeatureId: ReadonlyMap<string, RenderMedium>;
298
+ previousModesByFeatureId: ReadonlyMap<string, RenderMedium>;
299
+ }>;
300
+ /**
301
+ * Discriminated union of all render configurations.
302
+ * Used for both marker rendering and cluster rendering.
303
+ */
304
+ export type RenderConfig<TData> = Readonly<SymbolRenderConfig<TData> | DomRenderConfig<TData> | AdaptiveRenderConfig<TData>>;
305
+ /**
306
+ * Information about a cluster produced by client-side clustering.
307
+ * This is what the adapter provides when it handles clustering internally.
308
+ */
309
+ export type ClusterInfo = Readonly<{
310
+ id: string;
311
+ position: GeoJsonPosition;
312
+ markerIds: ReadonlyArray<string>;
313
+ count: number;
314
+ /** Extent of clustered members when known; null if not provided */
315
+ bbox: GeoJsonBbox | null;
316
+ }>;
317
+ /**
318
+ * Client-side clustering: the adapter handles clustering
319
+ * (Mapbox native, Google Supercluster, etc.)
320
+ */
321
+ export type ClientClusterConfig = Readonly<{
322
+ mode: "client";
323
+ /** Cluster radius in pixels */
324
+ radius?: number;
325
+ /** Max zoom at which clusters are created */
326
+ maxZoom?: number;
327
+ }>;
328
+ /**
329
+ * Server-side clustering: clusters are pre-computed and provided as data.
330
+ * Generic over TCluster so the render function gets full access to the
331
+ * original server cluster object (e.g., breakdown counts, chart data).
332
+ */
333
+ export type ServerClusterConfig<TCluster> = Readonly<{
334
+ mode: "server";
335
+ /** Pre-computed cluster data from the server */
336
+ data: ReadonlyArray<TCluster>;
337
+ /** Extract cluster center position */
338
+ getPosition(cluster: TCluster): GeoJsonPosition;
339
+ /** Extract cluster unique ID */
340
+ getId(cluster: TCluster): string;
341
+ /** Extract IDs of markers contained in this cluster */
342
+ getMarkerIds(cluster: TCluster): ReadonlyArray<string>;
343
+ /** Extract total count of items in this cluster */
344
+ getCount(cluster: TCluster): number;
345
+ /** Geographic extent as GeoJSON bbox `[minLon, minLat, maxLon, maxLat]`, or null when not provided */
346
+ getBbox(cluster: TCluster): GeoJsonBbox | null;
347
+ }>;
348
+ /**
349
+ * Discriminated union for clustering strategy.
350
+ * `mode` makes the clustering strategy explicit at the type level.
351
+ */
352
+ export type ClusterConfig<TCluster> = Readonly<ClientClusterConfig | ServerClusterConfig<TCluster>>;
@@ -0,0 +1,152 @@
1
+ import type { GeoJsonBbox, GeoJsonPosition } from "@trackunit/geo-json-utils";
2
+ import { z } from "zod";
3
+ type CSSProperties = import("react").CSSProperties;
4
+ type ReactNode = import("react").ReactNode;
5
+ export type { GeoJsonBbox, GeoJsonPosition } from "@trackunit/geo-json-utils";
6
+ /**
7
+ * Mutable holder for the container element.
8
+ * Behaves like a React ref but is a plain object so that passing it
9
+ * into `useMemo`/`createMapComponent` doesn't trigger the react-hooks/refs lint.
10
+ */
11
+ export type ContainerRefHolder = {
12
+ current: HTMLDivElement | null;
13
+ };
14
+ export declare const mapThemeSchema: z.ZodEnum<["light", "dark"]>;
15
+ export type MapTheme = z.infer<typeof mapThemeSchema>;
16
+ export declare const mapTypeSchema: z.ZodEnum<["roadmap", "satellite", "hybrid"]>;
17
+ export type MapType = z.infer<typeof mapTypeSchema>;
18
+ export declare const mapAppearanceSchema: z.ZodObject<{
19
+ theme: z.ZodEnum<["light", "dark"]>;
20
+ mapType: z.ZodEnum<["roadmap", "satellite", "hybrid"]>;
21
+ showRoads: z.ZodBoolean;
22
+ }, "strip", z.ZodTypeAny, {
23
+ theme: "light" | "dark";
24
+ mapType: "roadmap" | "satellite" | "hybrid";
25
+ showRoads: boolean;
26
+ }, {
27
+ theme: "light" | "dark";
28
+ mapType: "roadmap" | "satellite" | "hybrid";
29
+ showRoads: boolean;
30
+ }>;
31
+ /**
32
+ * Map appearance - grouped visual properties (theme, base layer, road overlay).
33
+ * Nested inside MapState as `state.appearance` so it can be watched and
34
+ * passed around as a single stable reference.
35
+ */
36
+ export type MapAppearance = Readonly<z.infer<typeof mapAppearanceSchema>>;
37
+ /** Default appearance before any user preference is applied */
38
+ export declare const DEFAULT_MAP_APPEARANCE: MapAppearance;
39
+ /**
40
+ * Camera state — viewport properties that change at up to 60fps during panning/zooming.
41
+ * React consumers subscribe to this channel with `useCameraState(api)`.
42
+ * Components that only need lifecycle or appearance data should read MapStatus
43
+ * from `api.state` instead.
44
+ */
45
+ export type CameraState = Readonly<{
46
+ /** Current center position as [longitude, latitude] */
47
+ center: Readonly<GeoJsonPosition>;
48
+ /** Current zoom level (0-22) */
49
+ zoom: number;
50
+ /** Current visible bounds as [minLon, minLat, maxLon, maxLat], null if not available */
51
+ bounds: Readonly<GeoJsonBbox> | null;
52
+ /** True when the map is idle (not animating/panning) */
53
+ isIdle: boolean;
54
+ }>;
55
+ /**
56
+ * Map status — slow-changing properties (initialization lifecycle and visual appearance).
57
+ * This is the shape exposed as `api.state` by `useMap`.
58
+ */
59
+ export type MapStatus = Readonly<{
60
+ /** Whether the map is ready for interactions */
61
+ isReady: boolean;
62
+ /** True when map initialization failed permanently (e.g. API script error). Implies `isReady` is false. */
63
+ initializationFailed: boolean;
64
+ /** Current visual appearance (theme, map type, road overlay) */
65
+ appearance: MapAppearance;
66
+ /** Base tile size in pixels used by the map provider (256 for Google Maps, 512 for Mapbox GL) */
67
+ tileSize: number;
68
+ }>;
69
+ /**
70
+ * Full map state — union of CameraState and MapStatus.
71
+ * Used internally by adapters and for event payloads (e.g. moveend).
72
+ * External consumers should prefer `api.state` for MapStatus and
73
+ * `useCameraState(api)` for CameraState.
74
+ */
75
+ export type MapState = CameraState & MapStatus;
76
+ export declare const INITIAL_CAMERA_STATE: CameraState;
77
+ export declare const INITIAL_MAP_STATUS: MapStatus;
78
+ /**
79
+ * Initial map state - default values before map loads
80
+ */
81
+ export declare const INITIAL_MAP_STATE: MapState;
82
+ /**
83
+ * Map events - discriminated union for type-safe event handling
84
+ */
85
+ export type MapEvent = {
86
+ readonly type: "idle";
87
+ } | {
88
+ readonly type: "movestart";
89
+ } | {
90
+ readonly type: "moveend";
91
+ readonly state: MapState;
92
+ } | {
93
+ readonly type: "click";
94
+ readonly position: GeoJsonPosition;
95
+ readonly originalEvent: MouseEvent;
96
+ };
97
+ /**
98
+ * Extract a specific event type from MapEvent union
99
+ */
100
+ export type MapEventOfType<TEventType extends MapEvent["type"]> = Extract<MapEvent, {
101
+ type: TEventType;
102
+ }>;
103
+ /**
104
+ * Event handler type for a specific event
105
+ */
106
+ export type MapEventHandler<TEventType extends MapEvent["type"]> = (event: MapEventOfType<TEventType>) => void;
107
+ /**
108
+ * Internal handler type for storing event listeners (receives full MapEvent union).
109
+ * Used by adapters when dispatching to typed handlers.
110
+ */
111
+ export type StoredEventHandler = (event: MapEvent) => void;
112
+ /**
113
+ * Options for fitBounds action
114
+ */
115
+ export type FitBoundsOptions = Readonly<{
116
+ /** Padding in pixels around the bounds. Defaults to 0 if omitted. */
117
+ padding?: number;
118
+ /** Maximum zoom level to use */
119
+ maxZoom?: number;
120
+ /** Whether to animate the transition */
121
+ animate?: boolean;
122
+ }>;
123
+ /**
124
+ * Initial viewport: either center+zoom or bounds. Mutually exclusive.
125
+ */
126
+ export type InitialViewport = {
127
+ readonly type: "center";
128
+ readonly center: Readonly<GeoJsonPosition>;
129
+ readonly zoom?: number;
130
+ } | {
131
+ readonly type: "bounds";
132
+ readonly bounds: Readonly<GeoJsonBbox>;
133
+ readonly padding?: number;
134
+ readonly maxZoom?: number;
135
+ };
136
+ /**
137
+ * Props for the Map component returned by useMap
138
+ */
139
+ export type MapComponentProps = Readonly<{
140
+ /** Children rendered inside the map */
141
+ children?: ReactNode;
142
+ /** CSS class name for the map container */
143
+ className?: string;
144
+ /** Inline styles for the map container */
145
+ style?: CSSProperties;
146
+ /** Test ID for testing */
147
+ "data-testid"?: string;
148
+ /** Accessible label for the map */
149
+ "aria-label"?: string;
150
+ /** ID of element describing the map */
151
+ "aria-describedby"?: string;
152
+ }>;
@@ -0,0 +1,12 @@
1
+ import { type GeoJsonBbox } from "@trackunit/geo-json-utils";
2
+ /**
3
+ * Resolve effective restrict bounds from config.
4
+ * Returns null if no restriction; GeoJsonBbox otherwise.
5
+ * Validates custom bbox with geoJsonBboxSchema; on failure, console.warn and fallback to null.
6
+ *
7
+ * @note - Why fallback to null and not WORLD_BBOX?
8
+ * Different map providers handle full world bounds differently.
9
+ * Eg. -/+180 degrees longitude is disregarded by Google Maps allowing for infinite horizontal scroll.
10
+ * while Mapbox respects the full world bounds, leading to inconsistent behavior between adapters.
11
+ */
12
+ export declare function getEffectiveRestrictBounds(configValue: GeoJsonBbox | null | undefined): GeoJsonBbox | null;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared test helpers for safe-area unit tests. Co-located with the source
3
+ * (under `safeArea/`) so per-module specs can find them locally.
4
+ */
5
+ export declare const makeRect: (w: number, h: number, x?: number, y?: number) => DOMRect;
6
+ export declare const firePointermove: (x: number, y: number) => void;
7
+ export declare const fireMouseenter: (el: HTMLElement) => void;
8
+ export declare const fireMouseleave: (el: HTMLElement, x: number, y: number) => void;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared mutable state box for cross-marker hover watcher coordination. One
3
+ * instance should be held per adapter instance and passed to every
4
+ * {@link attachSafeAreaHoverListeners} call. When `mouseenter` fires on any
5
+ * marker, the previous marker's document-level hover/guard listeners are
6
+ * cancelled via this reference.
7
+ */
8
+ export type SafeAreaHoverState = {
9
+ activeWatcherCancel: (() => void) | null;
10
+ activeGuardCancel: (() => void) | null;
11
+ };
12
+ /**
13
+ * Attach `mouseenter` and `mouseleave` listeners to a DOM marker element with
14
+ * the full spurious-mouseleave-safe hover contract:
15
+ *
16
+ * - Records the hit-surface rect at entry time (`hoverEntryRect`).
17
+ * - On `mouseleave`, computes the safe area: convex hull of the live hit
18
+ * surface, the entry rect, and the anchor rect. If the pointer is still
19
+ * inside, installs a {@link watchSafeAreaLeave} instead of firing onLeave
20
+ * immediately.
21
+ * - Cancels any watcher from a previous marker (via the shared `state` box)
22
+ * when a new `mouseenter` arrives.
23
+ *
24
+ * Both `MapboxLayerPort` and `GoogleMapsLayerPort` share this implementation
25
+ * so the safe-area logic has one source of truth.
26
+ */
27
+ export declare const attachSafeAreaHoverListeners: (el: HTMLElement, onEnter: () => void, onLeave: () => void, state: SafeAreaHoverState) => void;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The DOM-attribute contract that the safe-area hover system relies on. Any
3
+ * marker (built-in or third-party) opts into the spurious-mouseleave-safe
4
+ * hover by tagging two elements inside its DOM tree:
5
+ *
6
+ * - {@link HIT_SURFACE_SELECTOR} on the visible interactive surface (e.g.
7
+ * the pill at the tip of a stick marker).
8
+ * - {@link ANCHOR_SELECTOR} on the geographic-anchor element. Prefer the
9
+ * visible anchor footprint when one exists (e.g. a stick marker's anchor
10
+ * dot); a 0×0 coordinate node also works when no visible anchor exists.
11
+ * The anchor is only required when the visible surface is *spatially
12
+ * separated* from the anchor (the classic stick-marker case); plain
13
+ * non-stick markers can omit it without consequence.
14
+ */
15
+ /** Stable attribute on the visible interactive surface element. */
16
+ export declare const HIT_SURFACE_SELECTOR = "[data-map-marker-hit-surface]";
17
+ /** Stable attribute on the geographic-anchor element. */
18
+ export declare const ANCHOR_SELECTOR = "[data-map-marker-anchor]";
19
+ /**
20
+ * Outward buffer applied to each rect's corners when building the safe-area
21
+ * polygon. Guards against OS pointer-jitter at the polygon boundary without
22
+ * masking intentional outward movement.
23
+ */
24
+ export declare const SAFE_AREA_DEFAULT_BUFFER_PX = 1;
25
+ /**
26
+ * Resolve the rect to use for hit-testing a DOM marker. Prefers the inner
27
+ * hit-surface element (when present and non-empty); falls back to the host's
28
+ * own rect; returns null when both are empty.
29
+ */
30
+ export declare const getHitSurfaceRect: (host: HTMLElement) => DOMRect | null;
31
+ /**
32
+ * Return the page-space rect of the geographic anchor element, or null when
33
+ * the host does not contain one (no `data-map-marker-anchor`). A visible
34
+ * anchor dot gives the safe area its real footprint; a 0×0 coordinate node is
35
+ * still valid when no visible anchor exists.
36
+ */
37
+ export declare const getAnchorRect: (host: HTMLElement) => DOMRect | null;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pure 2D polygon primitives used by the safe-area system. No DOM dependency,
3
+ * no React dependency — just geometry.
4
+ */
5
+ /** A 2D point in page-space coordinates (matches `clientX`/`clientY`). */
6
+ export type Point2D = Readonly<{
7
+ x: number;
8
+ y: number;
9
+ }>;
10
+ /**
11
+ * Return the four corners of `rect`, outward-buffered by `px` on every axis.
12
+ * A 0×0 rect with a positive buffer still contributes a 4-point square (used
13
+ * to give the geographic anchor a non-zero footprint in the safe area).
14
+ */
15
+ export declare const bufferRectCorners: (rect: DOMRect, px: number) => ReadonlyArray<Point2D>;
16
+ /**
17
+ * Andrew's monotone-chain convex hull. Returns hull points in counter-clockwise
18
+ * order (in screen coordinates where y grows downward, this is visually
19
+ * clockwise, but the orientation does not affect the even-odd point-in-polygon
20
+ * test). The first point is **not** repeated at the end.
21
+ *
22
+ * Inputs are typically ≤ 12 (3 rects × 4 corners) so the O(n log n) sort is
23
+ * negligible. Empty input returns an empty hull.
24
+ */
25
+ export declare const convexHull: (points: ReadonlyArray<Point2D>) => ReadonlyArray<Point2D>;
26
+ /**
27
+ * Even-odd ray-cast point-in-polygon test. The standard formulation: edges are
28
+ * classified consistently (one side "inside", the other "outside") so a point
29
+ * that lies exactly on a polygon boundary may go either way depending on which
30
+ * edge was tested first — relying on edge classification is fragile. Callers
31
+ * should use the outward buffer in {@link ./safePolygon#safePolygon} as the
32
+ * tolerance margin and treat strictly-inside checks as the contract.
33
+ */
34
+ export declare const pointInPolygon: (x: number, y: number, polygon: ReadonlyArray<Point2D>) => boolean;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Dev-only visual overlay for the safe-area polygon computed by
3
+ * {@link ./safePolygon#safePolygon}. Lets you see the polygon the
4
+ * leave-detection logic is actually testing against, so shape regressions
5
+ * (e.g. "this doesn't look like a funnel at all") are debuggable from the
6
+ * browser instead of via unit tests alone.
7
+ *
8
+ * Design:
9
+ * - Off by default. Zero work when disabled.
10
+ * - One singleton `<svg>` overlay attached to `document.body` on first enable.
11
+ * - Per-area polygons keyed by string id; replace-or-clear by id.
12
+ * - Toggle from any code path:
13
+ * enableSafeAreaDebug(); // turn on
14
+ * disableSafeAreaDebug(); // turn off
15
+ */
16
+ import type { Point2D } from "./polygon";
17
+ /**
18
+ * Enable the debug overlay. Idempotent. Persists until {@link disableSafeAreaDebug}
19
+ * is called or the page reloads.
20
+ */
21
+ export declare const enableSafeAreaDebug: () => void;
22
+ /**
23
+ * Disable the overlay, remove the SVG element, and forget all registered
24
+ * safe areas. Call when you're done debugging.
25
+ */
26
+ export declare const disableSafeAreaDebug: () => void;
27
+ /** Read-only check used by the wiring hooks to skip work when off. */
28
+ export declare const isSafeAreaDebugEnabled: () => boolean;
29
+ /**
30
+ * Register or update a safe-area polygon. Use a stable id per active hover so
31
+ * repeated calls during pointermove/animation update the same shape in place.
32
+ */
33
+ export declare const renderSafeArea: (id: string, polygon: ReadonlyArray<Point2D>, rects: ReadonlyArray<DOMRect | null>) => void;
34
+ /** Remove a registered safe area by id. No-op if id is unknown. */
35
+ export declare const clearSafeArea: (id: string) => void;