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

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,6 +14,7 @@ import {
13
14
  defaultTileflowManifestUrl,
14
15
  loadTileflowManifest,
15
16
  mergeTileflowAnalytics,
17
+ normalizeTileflowCaptureId,
16
18
  normalizeTileflowStaticImageSize,
17
19
  resolveTileflowAnalyticsRequestUrl,
18
20
  resolveTileflowManifestMap,
@@ -26,6 +28,7 @@ import { jsx } from "react/jsx-runtime";
26
28
  var defaultCenter = [0, 20];
27
29
  var defaultMarkers = [];
28
30
  function Map({
31
+ captureId,
29
32
  map: mapName,
30
33
  mode = "interactive",
31
34
  config,
@@ -50,6 +53,7 @@ function Map({
50
53
  onLoad
51
54
  }) {
52
55
  const containerRef = useRef(null);
56
+ const imageRef = useRef(null);
53
57
  const mapRef = useRef(null);
54
58
  const markerInstancesRef = useRef([]);
55
59
  const stableMapOptions = useStableMapOptions(mapOptions);
@@ -65,9 +69,12 @@ function Map({
65
69
  const mapNameRef = useRef(mapName);
66
70
  const resolvedAnalyticsRef = useRef(void 0);
67
71
  const sessionStartsRef = useRef(/* @__PURE__ */ new Set());
72
+ const readinessRunRef = useRef(0);
68
73
  const sessionId = useMemo(() => createTileflowSessionId(), []);
69
74
  const [manifestMap, setManifestMap] = useState(null);
70
75
  const [imageSize, setImageSize] = useState(null);
76
+ const [captureState, setCaptureState] = useState("loading");
77
+ const resolvedCaptureId = normalizeTileflowCaptureId(captureId);
71
78
  const resolvedMode = resolveTileflowMapMode({ imageUrl, mode, preferLocalDev });
72
79
  const isImageMode = resolvedMode === "image";
73
80
  useEffect(() => {
@@ -192,6 +199,25 @@ function Map({
192
199
  if (!containerRef.current || !runtimeStyle) {
193
200
  return;
194
201
  }
202
+ const setLoading = () => {
203
+ readinessRunRef.current += 1;
204
+ setCaptureState("loading");
205
+ };
206
+ const setError = () => {
207
+ readinessRunRef.current += 1;
208
+ setCaptureState("error");
209
+ };
210
+ const setIdleAfterFrames = () => {
211
+ const run = ++readinessRunRef.current;
212
+ requestAnimationFrame(() => {
213
+ requestAnimationFrame(() => {
214
+ if (readinessRunRef.current === run && mapRef.current === map) {
215
+ setCaptureState("idle");
216
+ }
217
+ });
218
+ });
219
+ };
220
+ setLoading();
195
221
  const map = new maplibregl.Map({
196
222
  ...stableMapOptions,
197
223
  container: containerRef.current,
@@ -228,7 +254,12 @@ function Map({
228
254
  }
229
255
  }
230
256
  });
257
+ map.on("dataloading", setLoading);
258
+ map.on("styledataloading", setLoading);
259
+ map.on("idle", setIdleAfterFrames);
260
+ map.on("error", setError);
231
261
  return () => {
262
+ readinessRunRef.current += 1;
232
263
  resizeObserver.disconnect();
233
264
  clearMarkers(markerInstancesRef.current);
234
265
  markerInstancesRef.current = [];
@@ -236,6 +267,28 @@ function Map({
236
267
  mapRef.current = null;
237
268
  };
238
269
  }, [resolvedInteractive, runtimeStyle, sessionId, stableMapOptions, transformRequest]);
270
+ useLayoutEffect(() => {
271
+ if (!isImageMode) return;
272
+ readinessRunRef.current += 1;
273
+ setCaptureState("loading");
274
+ const image = imageRef.current;
275
+ if (image?.complete) {
276
+ if (image.naturalWidth === 0) setCaptureState("error");
277
+ else void markImageReady(image);
278
+ }
279
+ }, [isImageMode, runtimeImageUrl]);
280
+ async function markImageReady(image) {
281
+ const run = ++readinessRunRef.current;
282
+ try {
283
+ if (typeof image.decode === "function") await image.decode();
284
+ } catch {
285
+ if (!image.complete || image.naturalWidth === 0) {
286
+ if (readinessRunRef.current === run) setCaptureState("error");
287
+ return;
288
+ }
289
+ }
290
+ if (readinessRunRef.current === run) setCaptureState("idle");
291
+ }
239
292
  useEffect(() => {
240
293
  const map = mapRef.current;
241
294
  clearMarkers(markerInstancesRef.current);
@@ -263,23 +316,50 @@ function Map({
263
316
  width: "100%"
264
317
  };
265
318
  if (isImageMode) {
266
- return /* @__PURE__ */ jsx("div", { className, ref: containerRef, style: frameStyle, children: runtimeImageUrl ? /* @__PURE__ */ jsx(
267
- "img",
319
+ return /* @__PURE__ */ jsx(
320
+ "div",
268
321
  {
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
- }
322
+ className,
323
+ "data-tileflow-capture-id": resolvedCaptureId,
324
+ "data-tileflow-map": mapName,
325
+ "data-tileflow-state": captureState,
326
+ ref: containerRef,
327
+ style: frameStyle,
328
+ children: runtimeImageUrl ? /* @__PURE__ */ jsx(
329
+ "img",
330
+ {
331
+ alt,
332
+ decoding: "async",
333
+ loading: imageLoading,
334
+ onError: () => {
335
+ readinessRunRef.current += 1;
336
+ setCaptureState("error");
337
+ },
338
+ onLoad: (event) => void markImageReady(event.currentTarget),
339
+ ref: imageRef,
340
+ src: runtimeImageUrl,
341
+ style: {
342
+ display: "block",
343
+ height: "100%",
344
+ objectFit: "cover",
345
+ width: "100%"
346
+ }
347
+ }
348
+ ) : null
279
349
  }
280
- ) : null });
350
+ );
281
351
  }
282
- return /* @__PURE__ */ jsx("div", { className, ref: containerRef, style: frameStyle });
352
+ return /* @__PURE__ */ jsx(
353
+ "div",
354
+ {
355
+ className,
356
+ "data-tileflow-capture-id": resolvedCaptureId,
357
+ "data-tileflow-map": mapName,
358
+ "data-tileflow-state": captureState,
359
+ ref: containerRef,
360
+ style: frameStyle
361
+ }
362
+ );
283
363
  }
284
364
  function createTransformRequest(getAnalytics, sessionId, userTransformRequest) {
285
365
  return (url, resourceType) => {
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.16",
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.16",
45
+ "@tileflow/static": "0.1.0-alpha.16"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "maplibre-gl": ">=5",