@tileflow/react 0.1.0-alpha.14 → 0.1.0-alpha.17

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
@@ -55,4 +55,18 @@ the production image behavior locally.
55
55
  For full static scenes with bounds, overlays, and server-side precache, use
56
56
  `StaticMap` from `@tileflow/react/static` or helpers from `@tileflow/static`.
57
57
 
58
+ ## Headless capture readiness
59
+
60
+ Use `captureId` to disambiguate multiple maps with the same configured name:
61
+
62
+ ```tsx
63
+ <Map map="madrid" captureId="checkout-map" />
64
+ ```
65
+
66
+ The root element exposes `data-tileflow-map`, optional `data-tileflow-capture-id`, and
67
+ `data-tileflow-state="loading|idle|error"`. Interactive mode becomes idle after MapLibre is idle
68
+ and two animation frames; style/data work returns it to loading. Image mode waits for decode or a
69
+ successful load fallback, including cached hydration. `tileflow capture` uses these markers for
70
+ application scenes and requires exactly one target.
71
+
58
72
  Docs: https://tileflow.dev/docs
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ import { TileflowConfig, MapLibreStyle, TileflowProjectThemes, TileflowMapMarker
6
6
  type MapMarker = TileflowMapMarker;
7
7
  type TileflowMapOptions = Omit<MapOptions, 'container' | 'style'>;
8
8
  type MapProps = {
9
+ captureId?: string;
9
10
  map?: string;
10
11
  mode?: 'interactive' | 'image';
11
12
  config?: TileflowConfig;
@@ -29,6 +30,6 @@ type MapProps = {
29
30
  analytics?: TileflowAnalytics;
30
31
  onLoad?: (map: Map$1) => void;
31
32
  };
32
- declare function Map({ map: mapName, mode, config, style, styleUrl, imageUrl, styleBaseUrl, tileBaseUrl, themes, manifestUrl, preferLocalDev, alt, center, zoom, className, height, imageLoading, interactive, mapOptions, markers, analytics, onLoad, }: MapProps): react.JSX.Element;
33
+ declare function Map({ captureId, map: mapName, mode, config, style, styleUrl, imageUrl, styleBaseUrl, tileBaseUrl, themes, manifestUrl, preferLocalDev, alt, center, zoom, className, height, imageLoading, interactive, mapOptions, markers, analytics, onLoad, }: MapProps): react.JSX.Element;
33
34
 
34
35
  export { Map, type MapMarker, type MapProps, type TileflowMapOptions };
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // src/map.tsx
4
4
  import {
5
5
  useEffect,
6
+ useLayoutEffect,
6
7
  useMemo,
7
8
  useRef,
8
9
  useState
@@ -13,19 +14,25 @@ import {
13
14
  defaultTileflowManifestUrl,
14
15
  loadTileflowManifest,
15
16
  mergeTileflowAnalytics,
17
+ normalizeTileflowCaptureId,
16
18
  normalizeTileflowStaticImageSize,
17
- resolveTileflowAnalyticsRequestUrl,
18
19
  resolveTileflowManifestMap,
19
20
  resolveTileflowMapMode,
20
21
  resolveTileflowRuntimeStyle,
21
22
  resolveTileflowStaticImageUrl,
22
- shouldLoadTileflowManifest,
23
- startTileflowSession
23
+ shouldLoadTileflowManifest
24
24
  } from "@tileflow/core";
25
+ import {
26
+ attachTileflowMapLifecycle,
27
+ createTileflowMarkerController,
28
+ createTileflowSessionStarter,
29
+ createTileflowTransformRequest
30
+ } from "@tileflow/core/browser";
25
31
  import { jsx } from "react/jsx-runtime";
26
32
  var defaultCenter = [0, 20];
27
33
  var defaultMarkers = [];
28
34
  function Map({
35
+ captureId,
29
36
  map: mapName,
30
37
  mode = "interactive",
31
38
  config,
@@ -50,8 +57,8 @@ function Map({
50
57
  onLoad
51
58
  }) {
52
59
  const containerRef = useRef(null);
60
+ const imageRef = useRef(null);
53
61
  const mapRef = useRef(null);
54
- const markerInstancesRef = useRef([]);
55
62
  const stableMapOptions = useStableMapOptions(mapOptions);
56
63
  const resolvedCenter = useStableMapOptionValue(
57
64
  center ?? stableMapOptions?.center ?? defaultCenter
@@ -64,10 +71,33 @@ function Map({
64
71
  const onLoadRef = useRef(onLoad);
65
72
  const mapNameRef = useRef(mapName);
66
73
  const resolvedAnalyticsRef = useRef(void 0);
67
- const sessionStartsRef = useRef(/* @__PURE__ */ new Set());
74
+ const readinessRunRef = useRef(0);
68
75
  const sessionId = useMemo(() => createTileflowSessionId(), []);
76
+ const sessionStarter = useMemo(
77
+ () => createTileflowSessionStarter({ sessionId, source: "react" }),
78
+ [sessionId]
79
+ );
80
+ const markerController = useMemo(
81
+ () => createTileflowMarkerController({
82
+ attach(markerInstance, map, marker) {
83
+ markerInstance.setLngLat(marker.coordinates).addTo(map);
84
+ markerInstance.getElement().title = marker.label ?? marker.id;
85
+ },
86
+ create(marker) {
87
+ return new maplibregl.Marker({
88
+ color: marker.color ?? "#C6A15B"
89
+ });
90
+ },
91
+ remove(markerInstance) {
92
+ markerInstance.remove();
93
+ }
94
+ }),
95
+ []
96
+ );
69
97
  const [manifestMap, setManifestMap] = useState(null);
70
98
  const [imageSize, setImageSize] = useState(null);
99
+ const [captureState, setCaptureState] = useState("loading");
100
+ const resolvedCaptureId = normalizeTileflowCaptureId(captureId);
71
101
  const resolvedMode = resolveTileflowMapMode({ imageUrl, mode, preferLocalDev });
72
102
  const isImageMode = resolvedMode === "image";
73
103
  useEffect(() => {
@@ -159,11 +189,13 @@ function Map({
159
189
  [imageSize, imageUrl, isImageMode, manifestMap, resolvedZoom, staticImageCenter]
160
190
  );
161
191
  const transformRequest = useMemo(
162
- () => createTransformRequest(
163
- () => resolvedAnalyticsRef.current,
192
+ () => createTileflowTransformRequest({
193
+ always: true,
194
+ asyncAnalyticsTiming: "resolution",
195
+ getAnalytics: () => resolvedAnalyticsRef.current,
164
196
  sessionId,
165
- stableMapOptions?.transformRequest
166
- ),
197
+ transformRequest: stableMapOptions?.transformRequest ?? void 0
198
+ }),
167
199
  [sessionId, stableMapOptions?.transformRequest]
168
200
  );
169
201
  useEffect(() => {
@@ -192,6 +224,8 @@ function Map({
192
224
  if (!containerRef.current || !runtimeStyle) {
193
225
  return;
194
226
  }
227
+ readinessRunRef.current += 1;
228
+ setCaptureState("loading");
195
229
  const map = new maplibregl.Map({
196
230
  ...stableMapOptions,
197
231
  container: containerRef.current,
@@ -211,50 +245,90 @@ function Map({
211
245
  map.resize();
212
246
  });
213
247
  resizeObserver.observe(containerRef.current);
214
- map.on("load", () => {
215
- const analyticsForLoad = resolvedAnalyticsRef.current;
216
- const styleId = analyticsForLoad?.styleId ?? (typeof runtimeStyle.style === "string" ? runtimeStyle.style : mapNameRef.current);
217
- onLoadRef.current?.(map);
218
- if (analyticsForLoad?.mapId) {
219
- const sessionKey = `${sessionId}:${analyticsForLoad.mapId}:${styleId ?? ""}`;
220
- if (!sessionStartsRef.current.has(sessionKey)) {
221
- sessionStartsRef.current.add(sessionKey);
222
- startTileflowSession(analyticsForLoad, {
223
- mapId: analyticsForLoad.mapId,
224
- sessionId,
225
- source: "react",
226
- styleId
227
- });
228
- }
248
+ const lifecycle = attachTileflowMapLifecycle({
249
+ getSession: () => {
250
+ const analyticsForLoad = resolvedAnalyticsRef.current;
251
+ return {
252
+ analytics: analyticsForLoad,
253
+ styleId: analyticsForLoad?.styleId ?? (typeof runtimeStyle.style === "string" ? runtimeStyle.style : mapNameRef.current)
254
+ };
255
+ },
256
+ map,
257
+ onLoad: (loadedMap) => {
258
+ onLoadRef.current?.(loadedMap);
259
+ },
260
+ scheduler: {
261
+ cancelFrame: (frame) => cancelAnimationFrame(frame),
262
+ requestFrame: (callback) => requestAnimationFrame(callback)
263
+ },
264
+ sessionStarter,
265
+ setState: setCaptureState,
266
+ subscribe: (subscribedMap, event, listener) => {
267
+ const subscription = subscribedMap.on(event, listener);
268
+ return () => subscription.unsubscribe();
229
269
  }
230
270
  });
231
271
  return () => {
232
- resizeObserver.disconnect();
233
- clearMarkers(markerInstancesRef.current);
234
- markerInstancesRef.current = [];
235
- map.remove();
236
- mapRef.current = null;
272
+ readinessRunRef.current += 1;
273
+ try {
274
+ lifecycle.dispose();
275
+ } finally {
276
+ try {
277
+ resizeObserver.disconnect();
278
+ } finally {
279
+ try {
280
+ markerController.clear();
281
+ } finally {
282
+ try {
283
+ map.remove();
284
+ } finally {
285
+ if (mapRef.current === map) mapRef.current = null;
286
+ }
287
+ }
288
+ }
289
+ }
237
290
  };
238
- }, [resolvedInteractive, runtimeStyle, sessionId, stableMapOptions, transformRequest]);
291
+ }, [
292
+ markerController,
293
+ resolvedInteractive,
294
+ runtimeStyle,
295
+ sessionStarter,
296
+ stableMapOptions,
297
+ transformRequest
298
+ ]);
299
+ useLayoutEffect(() => {
300
+ if (!isImageMode) return;
301
+ readinessRunRef.current += 1;
302
+ setCaptureState("loading");
303
+ const image = imageRef.current;
304
+ if (image?.complete) {
305
+ if (image.naturalWidth === 0) setCaptureState("error");
306
+ else void markImageReady(image);
307
+ }
308
+ }, [isImageMode, runtimeImageUrl]);
309
+ async function markImageReady(image) {
310
+ const run = ++readinessRunRef.current;
311
+ try {
312
+ if (typeof image.decode === "function") await image.decode();
313
+ } catch {
314
+ if (!image.complete || image.naturalWidth === 0) {
315
+ if (readinessRunRef.current === run) setCaptureState("error");
316
+ return;
317
+ }
318
+ }
319
+ if (readinessRunRef.current === run) setCaptureState("idle");
320
+ }
239
321
  useEffect(() => {
240
322
  const map = mapRef.current;
241
- clearMarkers(markerInstancesRef.current);
242
- markerInstancesRef.current = [];
323
+ markerController.clear();
243
324
  if (!map) {
244
325
  return;
245
326
  }
246
- markerInstancesRef.current = markers.map((marker) => {
247
- const markerInstance = new maplibregl.Marker({
248
- color: marker.color ?? "#C6A15B"
249
- }).setLngLat(marker.coordinates).addTo(map);
250
- markerInstance.getElement().title = marker.label ?? marker.id;
251
- return markerInstance;
252
- });
327
+ markerController.replace(map, markers);
253
328
  return () => {
254
- clearMarkers(markerInstancesRef.current);
255
- markerInstancesRef.current = [];
329
+ markerController.clear();
256
330
  };
257
- }, [markers, runtimeStyle]);
331
+ }, [markerController, markers, runtimeStyle]);
258
332
  const frameStyle = {
259
333
  height,
260
334
  minHeight: 240,
@@ -263,48 +337,50 @@ function Map({
263
337
  width: "100%"
264
338
  };
265
339
  if (isImageMode) {
266
- return /* @__PURE__ */ jsx("div", { className, ref: containerRef, style: frameStyle, children: runtimeImageUrl ? /* @__PURE__ */ jsx(
267
- "img",
340
+ return /* @__PURE__ */ jsx(
341
+ "div",
268
342
  {
269
- alt,
270
- decoding: "async",
271
- loading: imageLoading,
272
- src: runtimeImageUrl,
273
- style: {
274
- display: "block",
275
- height: "100%",
276
- objectFit: "cover",
277
- width: "100%"
278
- }
343
+ className,
344
+ "data-tileflow-capture-id": resolvedCaptureId,
345
+ "data-tileflow-map": mapName,
346
+ "data-tileflow-state": captureState,
347
+ ref: containerRef,
348
+ style: frameStyle,
349
+ children: runtimeImageUrl ? /* @__PURE__ */ jsx(
350
+ "img",
351
+ {
352
+ alt,
353
+ decoding: "async",
354
+ loading: imageLoading,
355
+ onError: () => {
356
+ readinessRunRef.current += 1;
357
+ setCaptureState("error");
358
+ },
359
+ onLoad: (event) => void markImageReady(event.currentTarget),
360
+ ref: imageRef,
361
+ src: runtimeImageUrl,
362
+ style: {
363
+ display: "block",
364
+ height: "100%",
365
+ objectFit: "cover",
366
+ width: "100%"
367
+ }
368
+ }
369
+ ) : null
279
370
  }
280
- ) : null });
371
+ );
281
372
  }
282
- return /* @__PURE__ */ jsx("div", { className, ref: containerRef, style: frameStyle });
283
- }
284
- function createTransformRequest(getAnalytics, sessionId, userTransformRequest) {
285
- return (url, resourceType) => {
286
- const request = userTransformRequest?.(url, resourceType);
287
- if (isPromiseLike(request)) {
288
- return request.then(
289
- (resolvedRequest) => applyTileflowAnalyticsRequest(url, resolvedRequest, getAnalytics(), sessionId) ?? resolvedRequest
290
- );
373
+ return /* @__PURE__ */ jsx(
374
+ "div",
375
+ {
376
+ className,
377
+ "data-tileflow-capture-id": resolvedCaptureId,
378
+ "data-tileflow-map": mapName,
379
+ "data-tileflow-state": captureState,
380
+ ref: containerRef,
381
+ style: frameStyle
291
382
  }
292
- return applyTileflowAnalyticsRequest(url, request, getAnalytics(), sessionId);
293
- };
294
- }
295
- function applyTileflowAnalyticsRequest(url, request, analytics, sessionId) {
296
- const requestUrl = request?.url ?? url;
297
- const nextUrl = resolveTileflowAnalyticsRequestUrl(requestUrl, analytics, sessionId);
298
- if (!nextUrl) {
299
- return request;
300
- }
301
- return request ? { ...request, url: nextUrl } : { url: nextUrl };
302
- }
303
- function isPromiseLike(value) {
304
- return Boolean(value && typeof value.then === "function");
305
- }
306
- function clearMarkers(markerInstances) {
307
- markerInstances.forEach((marker) => marker.remove());
383
+ );
308
384
  }
309
385
  function useStableMapOptions(options) {
310
386
  return useStableMapOptionValue(options);
package/dist/static.d.ts CHANGED
@@ -5,11 +5,12 @@ import { StaticSceneInput, StaticMapResult } from '@tileflow/static';
5
5
  type StaticMapBaseProps = StaticSceneInput & {
6
6
  alt?: string;
7
7
  className?: string;
8
+ captureId?: string;
8
9
  imageStyle?: CSSProperties;
9
10
  keepPreviousImage?: boolean;
10
- loading?: ImgHTMLAttributes<HTMLImageElement>["loading"];
11
+ loading?: ImgHTMLAttributes<HTMLImageElement>['loading'];
11
12
  onError?: (error: Error) => void;
12
- onImageLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
13
+ onImageLoad?: ImgHTMLAttributes<HTMLImageElement>['onLoad'];
13
14
  onReady?: (result: StaticMapResult) => void;
14
15
  style?: CSSProperties;
15
16
  };
@@ -22,6 +23,6 @@ type StaticMapCreateProps = {
22
23
  imageUrl?: never;
23
24
  };
24
25
  type StaticMapProps = StaticMapBaseProps & (StaticMapCreateProps | StaticMapImageProps);
25
- declare function StaticMap({ alt, camera, className, createUrl, imageStyle, imageUrl, keepPreviousImage, loading, map, onError, onImageLoad, onReady, overlays, size, style, }: StaticMapProps): react.JSX.Element;
26
+ declare function StaticMap({ alt, camera, className, captureId, createUrl, imageStyle, imageUrl, keepPreviousImage, loading, map, onError, onImageLoad, onReady, overlays, size, style, }: StaticMapProps): react.JSX.Element;
26
27
 
27
28
  export { StaticMap, type StaticMapProps };
package/dist/static.js CHANGED
@@ -3,16 +3,19 @@
3
3
  // src/static-map.tsx
4
4
  import {
5
5
  useEffect,
6
+ useLayoutEffect,
6
7
  useMemo,
7
8
  useRef,
8
9
  useState
9
10
  } from "react";
11
+ import { normalizeTileflowCaptureId } from "@tileflow/core";
10
12
  import { jsx } from "react/jsx-runtime";
11
13
  var inFlightRequests = /* @__PURE__ */ new Map();
12
14
  function StaticMap({
13
15
  alt = "",
14
16
  camera,
15
17
  className,
18
+ captureId,
16
19
  createUrl,
17
20
  imageStyle,
18
21
  imageUrl,
@@ -30,6 +33,10 @@ function StaticMap({
30
33
  imageUrl ? imageResult(imageUrl) : null
31
34
  );
32
35
  const [error, setError] = useState(null);
36
+ const [captureState, setCaptureState] = useState("loading");
37
+ const imageLoadRunRef = useRef(0);
38
+ const imageRef = useRef(null);
39
+ const resolvedCaptureId = normalizeTileflowCaptureId(captureId);
33
40
  const onErrorRef = useRef(onError);
34
41
  const onReadyRef = useRef(onReady);
35
42
  const sceneKey = useMemo(
@@ -37,6 +44,27 @@ function StaticMap({
37
44
  [camera, map, overlays, size]
38
45
  );
39
46
  const requestKey = imageUrl ? `image:${imageUrl}` : `create:${createUrl}:${sceneKey}`;
47
+ useLayoutEffect(() => {
48
+ imageLoadRunRef.current += 1;
49
+ setCaptureState("loading");
50
+ const image = imageRef.current;
51
+ if (image?.complete) {
52
+ if (image.naturalWidth === 0) setCaptureState("error");
53
+ else void markImageReady(image);
54
+ }
55
+ }, [requestKey]);
56
+ async function markImageReady(image) {
57
+ const runId = ++imageLoadRunRef.current;
58
+ try {
59
+ if (typeof image.decode === "function") await image.decode();
60
+ } catch {
61
+ if (!image.complete || image.naturalWidth === 0) {
62
+ if (runId === imageLoadRunRef.current) setCaptureState("error");
63
+ return;
64
+ }
65
+ }
66
+ if (runId === imageLoadRunRef.current) setCaptureState("idle");
67
+ }
40
68
  useEffect(() => {
41
69
  onErrorRef.current = onError;
42
70
  onReadyRef.current = onReady;
@@ -77,6 +105,8 @@ function StaticMap({
77
105
  setResult(null);
78
106
  }
79
107
  setError(normalizedError);
108
+ imageLoadRunRef.current += 1;
109
+ setCaptureState("error");
80
110
  onErrorRef.current?.(normalizedError);
81
111
  });
82
112
  return () => {
@@ -88,6 +118,9 @@ function StaticMap({
88
118
  {
89
119
  "aria-busy": !result && !error,
90
120
  className,
121
+ "data-tileflow-capture-id": resolvedCaptureId,
122
+ "data-tileflow-map": map,
123
+ "data-tileflow-state": captureState,
91
124
  style: {
92
125
  aspectRatio: `${size.width} / ${size.height}`,
93
126
  background: "#f4f4f2",
@@ -104,9 +137,15 @@ function StaticMap({
104
137
  onError: () => {
105
138
  const imageError = new Error("Static map image failed to load");
106
139
  setError(imageError);
140
+ imageLoadRunRef.current += 1;
141
+ setCaptureState("error");
107
142
  onErrorRef.current?.(imageError);
108
143
  },
109
- onLoad: onImageLoad,
144
+ onLoad: (event) => {
145
+ onImageLoad?.(event);
146
+ void markImageReady(event.currentTarget);
147
+ },
148
+ ref: imageRef,
110
149
  src: result.imageUrl,
111
150
  style: {
112
151
  display: "block",
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@tileflow/react",
3
- "version": "0.1.0-alpha.14",
3
+ "version": "0.1.0-alpha.17",
4
4
  "description": "React components for rendering Tileflow maps with MapLibre.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "homepage": "https://tileflow.dev",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/tileflow/tileflow.git",
10
+ "url": "git+https://github.com/tileflow/tileflow-sdk.git",
11
11
  "directory": "packages/react"
12
12
  },
13
13
  "bugs": {
14
- "url": "https://github.com/tileflow/tileflow/issues"
14
+ "url": "https://github.com/tileflow/tileflow-sdk/issues"
15
15
  },
16
16
  "keywords": [
17
17
  "tileflow",
@@ -41,8 +41,8 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@tileflow/core": "0.1.0-alpha.14",
45
- "@tileflow/static": "0.1.0-alpha.14"
44
+ "@tileflow/core": ">=0.1.0-alpha.17 <0.1.0-beta.0",
45
+ "@tileflow/static": ">=0.1.0-alpha.16 <0.1.0-beta.0"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "maplibre-gl": ">=5",