@tileflow/react 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @tileflow/react
2
+
3
+ React components for rendering Tileflow maps with MapLibre.
4
+
5
+ ```tsx
6
+ import { Map } from "@tileflow/react";
7
+
8
+ export function App() {
9
+ return <Map map="madrid" center={[-3.7038, 40.4168]} zoom={12} />;
10
+ }
11
+ ```
12
+
13
+ Docs: https://tileflow.dev/docs
@@ -0,0 +1,47 @@
1
+ import * as react from 'react';
2
+ import { CSSProperties, ImgHTMLAttributes } from 'react';
3
+ import { TileflowConfig, MapLibreStyle } from '@tileflow/core';
4
+ import { Map as Map$1 } from 'maplibre-gl';
5
+
6
+ type MapMarker = {
7
+ id: string;
8
+ coordinates: [number, number];
9
+ color?: string;
10
+ label?: string;
11
+ };
12
+ type TileflowAnalytics = {
13
+ apiUrl?: string;
14
+ endpoint?: string;
15
+ enabled?: boolean;
16
+ mapId?: string;
17
+ metadata?: Record<string, unknown>;
18
+ projectId?: string;
19
+ sdkVersion?: string;
20
+ source?: string;
21
+ styleId?: string;
22
+ };
23
+ type MapProps = {
24
+ map?: string;
25
+ mode?: "interactive" | "image";
26
+ config?: TileflowConfig;
27
+ style?: MapLibreStyle;
28
+ styleUrl?: string;
29
+ imageUrl?: string;
30
+ styleBaseUrl?: string;
31
+ tileBaseUrl?: string;
32
+ manifestUrl?: string;
33
+ preferLocalDev?: boolean;
34
+ alt?: string;
35
+ center?: [number, number];
36
+ zoom?: number;
37
+ className?: string;
38
+ height?: CSSProperties["height"];
39
+ imageLoading?: ImgHTMLAttributes<HTMLImageElement>["loading"];
40
+ interactive?: boolean;
41
+ markers?: MapMarker[];
42
+ analytics?: TileflowAnalytics;
43
+ onLoad?: (map: Map$1) => void;
44
+ };
45
+ declare function Map({ map: mapName, mode, config, style, styleUrl, imageUrl, styleBaseUrl, tileBaseUrl, manifestUrl, preferLocalDev, alt, center, zoom, className, height, imageLoading, interactive, markers, analytics, onLoad, }: MapProps): react.JSX.Element;
46
+
47
+ export { Map, type MapMarker, type MapProps };
package/dist/index.js ADDED
@@ -0,0 +1,486 @@
1
+ // src/map.tsx
2
+ import {
3
+ createStyle
4
+ } from "@tileflow/core";
5
+ import maplibregl from "maplibre-gl";
6
+ import {
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState
11
+ } from "react";
12
+ import { jsx } from "react/jsx-runtime";
13
+ var defaultCenter = [0, 20];
14
+ var defaultMarkers = [];
15
+ var defaultManifestUrl = "/tileflow/manifest.json";
16
+ var maxStaticImageDimension = 1280;
17
+ var maxStaticImagePixels = 1280 * 1280;
18
+ var manifestCache = new globalThis.Map();
19
+ function Map({
20
+ map: mapName,
21
+ mode = "interactive",
22
+ config,
23
+ style,
24
+ styleUrl,
25
+ imageUrl,
26
+ styleBaseUrl,
27
+ tileBaseUrl,
28
+ manifestUrl = defaultManifestUrl,
29
+ preferLocalDev = true,
30
+ alt = "",
31
+ center = defaultCenter,
32
+ zoom = 2,
33
+ className,
34
+ height = 420,
35
+ imageLoading = "eager",
36
+ interactive = true,
37
+ markers = defaultMarkers,
38
+ analytics,
39
+ onLoad
40
+ }) {
41
+ const containerRef = useRef(null);
42
+ const mapRef = useRef(null);
43
+ const sessionId = useMemo(() => createSessionId(), []);
44
+ const [manifestMap, setManifestMap] = useState(
45
+ null
46
+ );
47
+ const [imageSize, setImageSize] = useState(null);
48
+ const isImageMode = mode === "image";
49
+ const useExplicitStyleBaseUrl = useMemo(
50
+ () => Boolean(
51
+ !isImageMode && mapName && !style && !styleUrl && !config && styleBaseUrl
52
+ ),
53
+ [config, isImageMode, mapName, style, styleBaseUrl, styleUrl]
54
+ );
55
+ const useLocalDevFallback = useMemo(
56
+ () => Boolean(
57
+ !isImageMode && mapName && !style && !styleUrl && !config && !styleBaseUrl && preferLocalDev && isLocalDevHost()
58
+ ),
59
+ [
60
+ config,
61
+ isImageMode,
62
+ mapName,
63
+ preferLocalDev,
64
+ style,
65
+ styleBaseUrl,
66
+ styleUrl
67
+ ]
68
+ );
69
+ useEffect(() => {
70
+ const shouldLoadManifest = Boolean(
71
+ mapName && !config && (isImageMode && !imageUrl || !isImageMode && !style && !styleUrl && !useExplicitStyleBaseUrl)
72
+ );
73
+ if (!shouldLoadManifest) {
74
+ setManifestMap(null);
75
+ return;
76
+ }
77
+ let cancelled = false;
78
+ loadManifest(manifestUrl).then((manifest) => {
79
+ if (cancelled) {
80
+ return;
81
+ }
82
+ setManifestMap(
83
+ manifest ? resolveManifestMap(manifest, mapName) : null
84
+ );
85
+ }).catch((error) => {
86
+ if (!cancelled) {
87
+ console.error("Failed to load Tileflow manifest", error);
88
+ setManifestMap(null);
89
+ }
90
+ });
91
+ return () => {
92
+ cancelled = true;
93
+ };
94
+ }, [
95
+ config,
96
+ imageUrl,
97
+ isImageMode,
98
+ manifestUrl,
99
+ mapName,
100
+ style,
101
+ styleUrl,
102
+ useExplicitStyleBaseUrl
103
+ ]);
104
+ const runtimeStyle = useMemo(() => {
105
+ if (isImageMode) {
106
+ return null;
107
+ }
108
+ if (style) {
109
+ return { style };
110
+ }
111
+ if (styleUrl) {
112
+ return {
113
+ analytics: inferAnalyticsFromStyleUrl(styleUrl),
114
+ style: styleUrl
115
+ };
116
+ }
117
+ if (mapName && useExplicitStyleBaseUrl) {
118
+ return { style: resolveStyleUrl(mapName, styleBaseUrl) };
119
+ }
120
+ if (mapName) {
121
+ if (manifestMap?.styleUrl) {
122
+ return {
123
+ analytics: {
124
+ apiUrl: manifestMap.apiUrl,
125
+ mapId: manifestMap.mapId,
126
+ styleId: manifestMap.styleId
127
+ },
128
+ style: manifestMap.styleUrl
129
+ };
130
+ }
131
+ if (useLocalDevFallback) {
132
+ return { style: resolveStyleUrl(mapName) };
133
+ }
134
+ return null;
135
+ }
136
+ return { style: createStyle(config, { tileBaseUrl }) };
137
+ }, [
138
+ config,
139
+ isImageMode,
140
+ manifestMap,
141
+ mapName,
142
+ style,
143
+ styleBaseUrl,
144
+ styleUrl,
145
+ tileBaseUrl,
146
+ useExplicitStyleBaseUrl,
147
+ useLocalDevFallback
148
+ ]);
149
+ const resolvedAnalytics = useMemo(
150
+ () => mergeAnalytics(analytics, runtimeStyle?.analytics),
151
+ [analytics, runtimeStyle]
152
+ );
153
+ const runtimeImageUrl = useMemo(
154
+ () => imageUrl ?? (isImageMode ? resolveStaticImageUrl({
155
+ center,
156
+ imageSize,
157
+ manifestMap,
158
+ zoom
159
+ }) : void 0),
160
+ [center, imageSize, imageUrl, isImageMode, manifestMap, zoom]
161
+ );
162
+ const transformRequest = useMemo(
163
+ () => createTransformRequest(resolvedAnalytics, sessionId),
164
+ [resolvedAnalytics, sessionId]
165
+ );
166
+ useEffect(() => {
167
+ if (!isImageMode || imageUrl || !containerRef.current) {
168
+ setImageSize(null);
169
+ return;
170
+ }
171
+ const element = containerRef.current;
172
+ const updateImageSize = () => {
173
+ const nextSize = normalizeStaticImageSize({
174
+ height: element.clientHeight,
175
+ width: element.clientWidth
176
+ });
177
+ setImageSize(
178
+ (currentSize) => currentSize?.height === nextSize.height && currentSize.width === nextSize.width ? currentSize : nextSize
179
+ );
180
+ };
181
+ const resizeObserver = new ResizeObserver(updateImageSize);
182
+ resizeObserver.observe(element);
183
+ updateImageSize();
184
+ return () => {
185
+ resizeObserver.disconnect();
186
+ };
187
+ }, [imageUrl, isImageMode]);
188
+ useEffect(() => {
189
+ if (!containerRef.current || !runtimeStyle) {
190
+ return;
191
+ }
192
+ const map = new maplibregl.Map({
193
+ container: containerRef.current,
194
+ style: runtimeStyle.style,
195
+ center,
196
+ zoom,
197
+ interactive,
198
+ attributionControl: { compact: true },
199
+ transformRequest
200
+ });
201
+ mapRef.current = map;
202
+ const controls = interactive ? new maplibregl.NavigationControl() : null;
203
+ if (controls) {
204
+ map.addControl(controls, "top-right");
205
+ }
206
+ const markerInstances = markers.map((marker) => {
207
+ const element = document.createElement("div");
208
+ element.className = "tileflow-marker";
209
+ element.style.background = marker.color ?? "#C6A15B";
210
+ element.title = marker.label ?? marker.id;
211
+ return new maplibregl.Marker({ element }).setLngLat(marker.coordinates).addTo(map);
212
+ });
213
+ const resizeObserver = new ResizeObserver(() => {
214
+ map.resize();
215
+ });
216
+ resizeObserver.observe(containerRef.current);
217
+ map.on("load", () => {
218
+ onLoad?.(map);
219
+ startSession(resolvedAnalytics, {
220
+ mapId: resolvedAnalytics?.mapId,
221
+ sessionId,
222
+ styleId: resolvedAnalytics?.styleId ?? (typeof runtimeStyle.style === "string" ? runtimeStyle.style : mapName)
223
+ });
224
+ });
225
+ return () => {
226
+ resizeObserver.disconnect();
227
+ markerInstances.forEach((marker) => marker.remove());
228
+ map.remove();
229
+ mapRef.current = null;
230
+ };
231
+ }, [
232
+ center,
233
+ interactive,
234
+ mapName,
235
+ markers,
236
+ onLoad,
237
+ resolvedAnalytics,
238
+ runtimeStyle,
239
+ sessionId,
240
+ transformRequest,
241
+ zoom
242
+ ]);
243
+ const frameStyle = {
244
+ height,
245
+ minHeight: 240,
246
+ overflow: "hidden",
247
+ position: "relative",
248
+ width: "100%"
249
+ };
250
+ if (isImageMode) {
251
+ return /* @__PURE__ */ jsx("div", { className, ref: containerRef, style: frameStyle, children: runtimeImageUrl ? /* @__PURE__ */ jsx(
252
+ "img",
253
+ {
254
+ alt,
255
+ decoding: "async",
256
+ loading: imageLoading,
257
+ src: runtimeImageUrl,
258
+ style: {
259
+ display: "block",
260
+ height: "100%",
261
+ objectFit: "cover",
262
+ width: "100%"
263
+ }
264
+ }
265
+ ) : null });
266
+ }
267
+ return /* @__PURE__ */ jsx(
268
+ "div",
269
+ {
270
+ className,
271
+ ref: containerRef,
272
+ style: frameStyle
273
+ }
274
+ );
275
+ }
276
+ function resolveStyleUrl(mapName, styleBaseUrl) {
277
+ const baseUrl = normalizeUrl(styleBaseUrl ?? getDefaultStyleBaseUrl());
278
+ return `${baseUrl}/styles/${mapName}.json`;
279
+ }
280
+ function getDefaultStyleBaseUrl() {
281
+ const globalValue = globalThis.__TILEFLOW_STYLE_BASE_URL__;
282
+ return globalValue ?? "http://localhost:3333";
283
+ }
284
+ function normalizeUrl(value) {
285
+ return value.replace(/\/+$/, "");
286
+ }
287
+ function createSessionId() {
288
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
289
+ return `ses_${crypto.randomUUID()}`;
290
+ }
291
+ return `ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
292
+ }
293
+ function createTransformRequest(analytics, sessionId) {
294
+ if (!analytics || analytics.enabled === false) {
295
+ return void 0;
296
+ }
297
+ return (url) => {
298
+ const apiUrl = getAnalyticsApiUrl(analytics);
299
+ if (!apiUrl || !isTileflowTileUrl(url, apiUrl)) {
300
+ return void 0;
301
+ }
302
+ const nextUrl = new URL(url);
303
+ nextUrl.searchParams.set("session", sessionId);
304
+ if (analytics.mapId) {
305
+ nextUrl.searchParams.set("map", analytics.mapId);
306
+ }
307
+ if (analytics.styleId) {
308
+ nextUrl.searchParams.set("styleId", analytics.styleId);
309
+ }
310
+ return { url: nextUrl.toString() };
311
+ };
312
+ }
313
+ function startSession(analytics, input) {
314
+ if (!analytics || analytics.enabled === false || !input.mapId || typeof navigator === "undefined") {
315
+ return;
316
+ }
317
+ const endpoint = analytics.endpoint ?? `${getAnalyticsApiUrl(analytics)}/v1/sessions/start`;
318
+ const payload = JSON.stringify({
319
+ mapId: input.mapId,
320
+ metadata: analytics.metadata,
321
+ referrer: typeof document === "undefined" ? void 0 : document.referrer || "",
322
+ sdkVersion: analytics.sdkVersion,
323
+ sessionId: input.sessionId,
324
+ source: analytics.source ?? "react",
325
+ styleId: input.styleId,
326
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
327
+ });
328
+ const blob = new Blob([payload], { type: "application/json" });
329
+ if (navigator.sendBeacon?.(endpoint, blob)) {
330
+ return;
331
+ }
332
+ void fetch(endpoint, {
333
+ body: payload,
334
+ credentials: "omit",
335
+ headers: {
336
+ "Content-Type": "application/json"
337
+ },
338
+ keepalive: true,
339
+ method: "POST"
340
+ });
341
+ }
342
+ function getAnalyticsApiUrl(analytics) {
343
+ if (analytics.apiUrl) {
344
+ return normalizeUrl(analytics.apiUrl);
345
+ }
346
+ if (analytics.endpoint) {
347
+ const url = new URL(analytics.endpoint);
348
+ return url.origin;
349
+ }
350
+ return "https://api.tileflow.dev";
351
+ }
352
+ function mergeAnalytics(userAnalytics, runtimeAnalytics) {
353
+ if (userAnalytics?.enabled === false) {
354
+ return userAnalytics;
355
+ }
356
+ if (!userAnalytics && !runtimeAnalytics?.mapId) {
357
+ return void 0;
358
+ }
359
+ return {
360
+ ...runtimeAnalytics,
361
+ ...userAnalytics,
362
+ apiUrl: userAnalytics?.apiUrl ?? runtimeAnalytics?.apiUrl,
363
+ mapId: userAnalytics?.mapId ?? runtimeAnalytics?.mapId,
364
+ styleId: userAnalytics?.styleId ?? runtimeAnalytics?.styleId
365
+ };
366
+ }
367
+ function isTileflowTileUrl(url, apiUrl) {
368
+ try {
369
+ const parsedUrl = new URL(url);
370
+ const parsedApiUrl = new URL(apiUrl);
371
+ return parsedUrl.origin === parsedApiUrl.origin && isTilePath(parsedUrl);
372
+ } catch {
373
+ return false;
374
+ }
375
+ }
376
+ function isTilePath(url) {
377
+ return url.pathname.startsWith("/tiles/") || url.pathname.startsWith("/v1/tiles/");
378
+ }
379
+ function isLocalDevHost() {
380
+ if (typeof window === "undefined") {
381
+ return false;
382
+ }
383
+ return ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname);
384
+ }
385
+ function loadManifest(url) {
386
+ if (!manifestCache.has(url)) {
387
+ manifestCache.set(
388
+ url,
389
+ fetch(url, { cache: "no-store" }).then(async (response) => {
390
+ if (response.status === 404) {
391
+ return null;
392
+ }
393
+ if (!response.ok) {
394
+ throw new Error(`Tileflow manifest failed: ${response.status}`);
395
+ }
396
+ return await response.json();
397
+ })
398
+ );
399
+ }
400
+ return manifestCache.get(url);
401
+ }
402
+ function resolveManifestMap(manifest, mapName) {
403
+ const entry = manifest.maps?.[mapName] ?? manifest.styles?.[mapName];
404
+ if (!entry) {
405
+ return null;
406
+ }
407
+ if (typeof entry === "string") {
408
+ const analytics2 = inferAnalyticsFromStyleUrl(entry);
409
+ return {
410
+ apiUrl: analytics2?.apiUrl ?? manifest.apiUrl,
411
+ mapId: analytics2?.mapId,
412
+ styleId: analytics2?.styleId,
413
+ styleUrl: entry
414
+ };
415
+ }
416
+ const styleUrl = entry.styleUrl ?? entry.url;
417
+ if (!styleUrl && !entry.mapId) {
418
+ return null;
419
+ }
420
+ const analytics = styleUrl ? inferAnalyticsFromStyleUrl(styleUrl) : void 0;
421
+ return {
422
+ apiUrl: entry.apiUrl ?? manifest.apiUrl ?? analytics?.apiUrl,
423
+ mapId: entry.mapId ?? analytics?.mapId,
424
+ styleId: entry.styleId ?? analytics?.styleId,
425
+ styleUrl
426
+ };
427
+ }
428
+ function resolveStaticImageUrl(input) {
429
+ if (!input.manifestMap?.mapId || !input.imageSize) {
430
+ return void 0;
431
+ }
432
+ const apiUrl = normalizeUrl(
433
+ input.manifestMap.apiUrl ?? "https://api.tileflow.dev"
434
+ );
435
+ const url = new URL(`/maps/${input.manifestMap.mapId}/static.png`, apiUrl);
436
+ url.searchParams.set("center", input.center.join(","));
437
+ url.searchParams.set("zoom", String(input.zoom));
438
+ url.searchParams.set("width", String(input.imageSize.width));
439
+ url.searchParams.set("height", String(input.imageSize.height));
440
+ return url.toString();
441
+ }
442
+ function normalizeStaticImageSize(input) {
443
+ let width = clampDimension(input.width);
444
+ let height = clampDimension(input.height);
445
+ const pixelCount = width * height;
446
+ if (pixelCount > maxStaticImagePixels) {
447
+ const scale = Math.sqrt(maxStaticImagePixels / pixelCount);
448
+ width = clampDimension(width * scale);
449
+ height = clampDimension(height * scale);
450
+ }
451
+ return { height, width };
452
+ }
453
+ function clampDimension(value) {
454
+ if (!Number.isFinite(value) || value <= 0) {
455
+ return 640;
456
+ }
457
+ return Math.max(
458
+ 64,
459
+ Math.min(maxStaticImageDimension, Math.round(value))
460
+ );
461
+ }
462
+ function inferAnalyticsFromStyleUrl(styleUrl) {
463
+ try {
464
+ const url = new URL(styleUrl, getLocationOrigin());
465
+ const match = url.pathname.match(/\/(?:v1\/)?maps\/([^/]+)\/style\.json$/);
466
+ if (!match?.[1]) {
467
+ return void 0;
468
+ }
469
+ return {
470
+ apiUrl: url.origin,
471
+ mapId: match[1],
472
+ styleId: url.toString()
473
+ };
474
+ } catch {
475
+ return void 0;
476
+ }
477
+ }
478
+ function getLocationOrigin() {
479
+ if (typeof window === "undefined") {
480
+ return "http://localhost";
481
+ }
482
+ return window.location.origin;
483
+ }
484
+ export {
485
+ Map
486
+ };
@@ -0,0 +1,27 @@
1
+ import * as react from 'react';
2
+ import { CSSProperties, ImgHTMLAttributes } from 'react';
3
+ import { StaticSceneInput, StaticMapResult } from '@tileflow/static';
4
+
5
+ type StaticMapBaseProps = StaticSceneInput & {
6
+ alt?: string;
7
+ className?: string;
8
+ imageStyle?: CSSProperties;
9
+ keepPreviousImage?: boolean;
10
+ loading?: ImgHTMLAttributes<HTMLImageElement>["loading"];
11
+ onError?: (error: Error) => void;
12
+ onImageLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
13
+ onReady?: (result: StaticMapResult) => void;
14
+ style?: CSSProperties;
15
+ };
16
+ type StaticMapImageProps = {
17
+ createUrl?: never;
18
+ imageUrl: string;
19
+ };
20
+ type StaticMapCreateProps = {
21
+ createUrl: string;
22
+ imageUrl?: never;
23
+ };
24
+ 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
+
27
+ export { StaticMap, type StaticMapProps };
package/dist/static.js ADDED
@@ -0,0 +1,167 @@
1
+ // src/static-map.tsx
2
+ import {
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState
7
+ } from "react";
8
+ import { jsx } from "react/jsx-runtime";
9
+ var inFlightRequests = /* @__PURE__ */ new Map();
10
+ function StaticMap({
11
+ alt = "",
12
+ camera,
13
+ className,
14
+ createUrl,
15
+ imageStyle,
16
+ imageUrl,
17
+ keepPreviousImage = false,
18
+ loading = "lazy",
19
+ map,
20
+ onError,
21
+ onImageLoad,
22
+ onReady,
23
+ overlays,
24
+ size,
25
+ style
26
+ }) {
27
+ const [result, setResult] = useState(
28
+ imageUrl ? imageResult(imageUrl) : null
29
+ );
30
+ const [error, setError] = useState(null);
31
+ const onErrorRef = useRef(onError);
32
+ const onReadyRef = useRef(onReady);
33
+ const sceneKey = useMemo(
34
+ () => stableStringify({ camera, map, overlays, size }),
35
+ [camera, map, overlays, size]
36
+ );
37
+ const requestKey = imageUrl ? `image:${imageUrl}` : `create:${createUrl}:${sceneKey}`;
38
+ useEffect(() => {
39
+ onErrorRef.current = onError;
40
+ onReadyRef.current = onReady;
41
+ }, [onError, onReady]);
42
+ useEffect(() => {
43
+ if (imageUrl) {
44
+ const nextResult = imageResult(imageUrl);
45
+ setResult(nextResult);
46
+ setError(null);
47
+ onReadyRef.current?.(nextResult);
48
+ return;
49
+ }
50
+ let cancelled = false;
51
+ if (!createUrl) {
52
+ return;
53
+ }
54
+ if (!keepPreviousImage) {
55
+ setResult(null);
56
+ }
57
+ setError(null);
58
+ resolveStaticMap({
59
+ createUrl,
60
+ requestKey,
61
+ scene: JSON.parse(sceneKey)
62
+ }).then((nextResult) => {
63
+ if (cancelled) {
64
+ return;
65
+ }
66
+ setResult(nextResult);
67
+ setError(null);
68
+ onReadyRef.current?.(nextResult);
69
+ }).catch((nextError) => {
70
+ if (cancelled) {
71
+ return;
72
+ }
73
+ const normalizedError = nextError instanceof Error ? nextError : new Error("Static map failed");
74
+ if (!keepPreviousImage) {
75
+ setResult(null);
76
+ }
77
+ setError(normalizedError);
78
+ onErrorRef.current?.(normalizedError);
79
+ });
80
+ return () => {
81
+ cancelled = true;
82
+ };
83
+ }, [createUrl, imageUrl, keepPreviousImage, requestKey, sceneKey]);
84
+ return /* @__PURE__ */ jsx(
85
+ "div",
86
+ {
87
+ "aria-busy": !result && !error,
88
+ className,
89
+ style: {
90
+ aspectRatio: `${size.width} / ${size.height}`,
91
+ background: "#f4f4f2",
92
+ overflow: "hidden",
93
+ width: "100%",
94
+ ...style
95
+ },
96
+ children: result ? /* @__PURE__ */ jsx(
97
+ "img",
98
+ {
99
+ alt,
100
+ decoding: "async",
101
+ loading,
102
+ onError: () => {
103
+ const imageError = new Error("Static map image failed to load");
104
+ setError(imageError);
105
+ onErrorRef.current?.(imageError);
106
+ },
107
+ onLoad: onImageLoad,
108
+ src: result.imageUrl,
109
+ style: {
110
+ display: "block",
111
+ height: "100%",
112
+ objectFit: "cover",
113
+ width: "100%",
114
+ ...imageStyle
115
+ }
116
+ }
117
+ ) : null
118
+ }
119
+ );
120
+ }
121
+ async function resolveStaticMap(input) {
122
+ const existing = inFlightRequests.get(input.requestKey);
123
+ if (existing) {
124
+ return existing;
125
+ }
126
+ const promise = fetch(input.createUrl, {
127
+ body: JSON.stringify(input.scene),
128
+ headers: {
129
+ "Content-Type": "application/json"
130
+ },
131
+ method: "POST"
132
+ }).then(async (response) => {
133
+ if (!response.ok) {
134
+ throw new Error(`Static map endpoint failed: ${response.status}`);
135
+ }
136
+ return await response.json();
137
+ }).finally(() => {
138
+ inFlightRequests.delete(input.requestKey);
139
+ });
140
+ inFlightRequests.set(input.requestKey, promise);
141
+ return promise;
142
+ }
143
+ function imageResult(imageUrl) {
144
+ return {
145
+ cached: true,
146
+ hash: "",
147
+ imageUrl,
148
+ status: "ready"
149
+ };
150
+ }
151
+ function stableStringify(value) {
152
+ return JSON.stringify(sortJson(value));
153
+ }
154
+ function sortJson(value) {
155
+ if (Array.isArray(value)) {
156
+ return value.map(sortJson);
157
+ }
158
+ if (value && typeof value === "object") {
159
+ return Object.fromEntries(
160
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, sortJson(entry)])
161
+ );
162
+ }
163
+ return value;
164
+ }
165
+ export {
166
+ StaticMap
167
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@tileflow/react",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "React components for rendering Tileflow maps with MapLibre.",
5
+ "type": "module",
6
+ "homepage": "https://tileflow.dev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/tileflow/tileflow.git",
10
+ "directory": "packages/react"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/tileflow/tileflow/issues"
14
+ },
15
+ "keywords": [
16
+ "tileflow",
17
+ "react",
18
+ "maps",
19
+ "maplibre",
20
+ "tiles"
21
+ ],
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./static": {
34
+ "types": "./dist/static.d.ts",
35
+ "import": "./dist/static.js",
36
+ "default": "./dist/static.js"
37
+ }
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@tileflow/core": "0.1.0-alpha.0",
44
+ "@tileflow/static": "0.1.0-alpha.0"
45
+ },
46
+ "peerDependencies": {
47
+ "maplibre-gl": ">=5",
48
+ "react": ">=18",
49
+ "react-dom": ">=18"
50
+ },
51
+ "devDependencies": {
52
+ "maplibre-gl": "^5.24.0",
53
+ "react": "^19.2.7",
54
+ "react-dom": "^19.2.7",
55
+ "@types/react": "^19.2.17",
56
+ "@types/react-dom": "^19.2.3"
57
+ },
58
+ "scripts": {
59
+ "build": "tsup src/index.ts src/static.ts --format esm --dts --clean --external react --external react-dom --external maplibre-gl",
60
+ "typecheck": "tsc -p tsconfig.json --noEmit",
61
+ "lint": "tsc -p tsconfig.json --noEmit"
62
+ }
63
+ }