@tileflow/react 0.1.0-alpha.0 → 0.1.0-alpha.10

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
@@ -2,12 +2,57 @@
2
2
 
3
3
  React components for rendering Tileflow maps with MapLibre.
4
4
 
5
+ Install `maplibre-gl` alongside the package and import its CSS once in your app.
6
+
5
7
  ```tsx
6
- import { Map } from "@tileflow/react";
8
+ import {Map} from '@tileflow/react';
9
+ import 'maplibre-gl/dist/maplibre-gl.css';
7
10
 
8
11
  export function App() {
9
- return <Map map="madrid" center={[-3.7038, 40.4168]} zoom={12} />;
12
+ return (
13
+ <Map
14
+ map="madrid"
15
+ center={[-3.7038, 40.4168]}
16
+ zoom={12}
17
+ mapOptions={{
18
+ cooperativeGestures: true,
19
+ maxZoom: 18,
20
+ }}
21
+ />
22
+ );
10
23
  }
11
24
  ```
12
25
 
26
+ `mapOptions` accepts native MapLibre options except `container` and `style`, which
27
+ Tileflow resolves from the map name, manifest, or explicit style props. Direct
28
+ Tileflow props such as `center`, `zoom`, and `interactive` take priority when
29
+ provided.
30
+
31
+ To reuse one published map across multiple repos, point every app at the same
32
+ manifest:
33
+
34
+ ```tsx
35
+ <Map map="madrid" manifestUrl="https://cdn.example.com/tileflow/manifest.json" />
36
+ ```
37
+
38
+ Or bypass the manifest and load a hosted style directly:
39
+
40
+ ```tsx
41
+ <Map styleUrl="https://api.tileflow.dev/v1/styles/acme/madrid.json" />
42
+ ```
43
+
44
+ For simple hosted static images, use image mode:
45
+
46
+ ```tsx
47
+ <Map map="madrid" mode="image" center={[-3.7038, 40.4168]} zoom={12} />
48
+ ```
49
+
50
+ When `mode="image"` does not include an explicit `imageUrl`, local development
51
+ hosts render the interactive map while `preferLocalDev` is enabled. Production
52
+ hosts render the deployed static image. Set `preferLocalDev={false}` to preview
53
+ the production image behavior locally.
54
+
55
+ For full static scenes with bounds, overlays, and server-side precache, use
56
+ `StaticMap` from `@tileflow/react/static` or helpers from `@tileflow/static`.
57
+
13
58
  Docs: https://tileflow.dev/docs
package/dist/index.d.ts CHANGED
@@ -1,47 +1,34 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, ImgHTMLAttributes } from 'react';
3
- import { TileflowConfig, MapLibreStyle } from '@tileflow/core';
4
- import { Map as Map$1 } from 'maplibre-gl';
3
+ import { MapOptions, Map as Map$1 } from 'maplibre-gl';
4
+ import { TileflowConfig, MapLibreStyle, TileflowProjectThemes, TileflowMapMarker, TileflowAnalytics } from '@tileflow/core';
5
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
- };
6
+ type MapMarker = TileflowMapMarker;
7
+ type TileflowMapOptions = Omit<MapOptions, 'container' | 'style'>;
23
8
  type MapProps = {
24
9
  map?: string;
25
- mode?: "interactive" | "image";
10
+ mode?: 'interactive' | 'image';
26
11
  config?: TileflowConfig;
27
12
  style?: MapLibreStyle;
28
13
  styleUrl?: string;
29
14
  imageUrl?: string;
30
15
  styleBaseUrl?: string;
31
16
  tileBaseUrl?: string;
17
+ themes?: TileflowProjectThemes;
32
18
  manifestUrl?: string;
33
19
  preferLocalDev?: boolean;
34
20
  alt?: string;
35
21
  center?: [number, number];
36
22
  zoom?: number;
37
23
  className?: string;
38
- height?: CSSProperties["height"];
39
- imageLoading?: ImgHTMLAttributes<HTMLImageElement>["loading"];
24
+ height?: CSSProperties['height'];
25
+ imageLoading?: ImgHTMLAttributes<HTMLImageElement>['loading'];
40
26
  interactive?: boolean;
27
+ mapOptions?: TileflowMapOptions;
41
28
  markers?: MapMarker[];
42
29
  analytics?: TileflowAnalytics;
43
30
  onLoad?: (map: Map$1) => void;
44
31
  };
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;
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;
46
33
 
47
- export { Map, type MapMarker, type MapProps };
34
+ export { Map, type MapMarker, type MapProps, type TileflowMapOptions };
package/dist/index.js CHANGED
@@ -1,21 +1,30 @@
1
+ 'use client';
2
+
1
3
  // src/map.tsx
2
- import {
3
- createStyle
4
- } from "@tileflow/core";
5
- import maplibregl from "maplibre-gl";
6
4
  import {
7
5
  useEffect,
8
6
  useMemo,
9
7
  useRef,
10
8
  useState
11
9
  } from "react";
10
+ import maplibregl from "maplibre-gl";
11
+ import {
12
+ createTileflowSessionId,
13
+ defaultTileflowManifestUrl,
14
+ loadTileflowManifest,
15
+ mergeTileflowAnalytics,
16
+ normalizeTileflowStaticImageSize,
17
+ resolveTileflowAnalyticsRequestUrl,
18
+ resolveTileflowManifestMap,
19
+ resolveTileflowMapMode,
20
+ resolveTileflowRuntimeStyle,
21
+ resolveTileflowStaticImageUrl,
22
+ shouldLoadTileflowManifest,
23
+ startTileflowSession
24
+ } from "@tileflow/core";
12
25
  import { jsx } from "react/jsx-runtime";
13
26
  var defaultCenter = [0, 20];
14
27
  var defaultMarkers = [];
15
- var defaultManifestUrl = "/tileflow/manifest.json";
16
- var maxStaticImageDimension = 1280;
17
- var maxStaticImagePixels = 1280 * 1280;
18
- var manifestCache = new globalThis.Map();
19
28
  function Map({
20
29
  map: mapName,
21
30
  mode = "interactive",
@@ -25,63 +34,63 @@ function Map({
25
34
  imageUrl,
26
35
  styleBaseUrl,
27
36
  tileBaseUrl,
28
- manifestUrl = defaultManifestUrl,
37
+ themes,
38
+ manifestUrl = defaultTileflowManifestUrl,
29
39
  preferLocalDev = true,
30
40
  alt = "",
31
- center = defaultCenter,
32
- zoom = 2,
41
+ center,
42
+ zoom,
33
43
  className,
34
44
  height = 420,
35
45
  imageLoading = "eager",
36
- interactive = true,
46
+ interactive,
47
+ mapOptions,
37
48
  markers = defaultMarkers,
38
49
  analytics,
39
50
  onLoad
40
51
  }) {
41
52
  const containerRef = useRef(null);
42
53
  const mapRef = useRef(null);
43
- const sessionId = useMemo(() => createSessionId(), []);
44
- const [manifestMap, setManifestMap] = useState(
45
- null
54
+ const markerInstancesRef = useRef([]);
55
+ const stableMapOptions = useStableMapOptions(mapOptions);
56
+ const resolvedCenter = useStableMapOptionValue(
57
+ center ?? stableMapOptions?.center ?? defaultCenter
46
58
  );
59
+ const resolvedZoom = zoom ?? stableMapOptions?.zoom ?? 2;
60
+ const resolvedInteractive = interactive ?? stableMapOptions?.interactive ?? true;
61
+ const staticImageCenter = center ?? defaultCenter;
62
+ const centerRef = useRef(resolvedCenter);
63
+ const zoomRef = useRef(resolvedZoom);
64
+ const onLoadRef = useRef(onLoad);
65
+ const mapNameRef = useRef(mapName);
66
+ const resolvedAnalyticsRef = useRef(void 0);
67
+ const sessionStartsRef = useRef(/* @__PURE__ */ new Set());
68
+ const sessionId = useMemo(() => createTileflowSessionId(), []);
69
+ const [manifestMap, setManifestMap] = useState(null);
47
70
  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
- [
71
+ const resolvedMode = resolveTileflowMapMode({ imageUrl, mode, preferLocalDev });
72
+ const isImageMode = resolvedMode === "image";
73
+ useEffect(() => {
74
+ const shouldLoadManifest = shouldLoadTileflowManifest({
60
75
  config,
61
- isImageMode,
62
- mapName,
63
- preferLocalDev,
76
+ imageMode: isImageMode,
77
+ imageUrl,
78
+ map: mapName,
64
79
  style,
65
80
  styleBaseUrl,
66
81
  styleUrl
67
- ]
68
- );
69
- useEffect(() => {
70
- const shouldLoadManifest = Boolean(
71
- mapName && !config && (isImageMode && !imageUrl || !isImageMode && !style && !styleUrl && !useExplicitStyleBaseUrl)
72
- );
82
+ });
73
83
  if (!shouldLoadManifest) {
74
84
  setManifestMap(null);
75
85
  return;
76
86
  }
77
87
  let cancelled = false;
78
- loadManifest(manifestUrl).then((manifest) => {
88
+ setManifestMap(null);
89
+ loadTileflowManifest(manifestUrl).then((manifest) => {
79
90
  if (cancelled) {
80
91
  return;
81
92
  }
82
- setManifestMap(
83
- manifest ? resolveManifestMap(manifest, mapName) : null
84
- );
93
+ setManifestMap(manifest && mapName ? resolveTileflowManifestMap(manifest, mapName) : null);
85
94
  }).catch((error) => {
86
95
  if (!cancelled) {
87
96
  console.error("Failed to load Tileflow manifest", error);
@@ -91,77 +100,71 @@ function Map({
91
100
  return () => {
92
101
  cancelled = true;
93
102
  };
94
- }, [
95
- config,
96
- imageUrl,
97
- isImageMode,
98
- manifestUrl,
99
- mapName,
100
- style,
101
- styleUrl,
102
- useExplicitStyleBaseUrl
103
- ]);
103
+ }, [config, imageUrl, isImageMode, manifestUrl, mapName, style, styleUrl, styleBaseUrl]);
104
104
  const runtimeStyle = useMemo(() => {
105
105
  if (isImageMode) {
106
106
  return null;
107
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 }) };
108
+ return resolveTileflowRuntimeStyle({
109
+ config,
110
+ manifestMap,
111
+ map: mapName,
112
+ preferLocalDev,
113
+ style,
114
+ styleBaseUrl,
115
+ styleUrl,
116
+ themes,
117
+ tileBaseUrl
118
+ });
137
119
  }, [
138
120
  config,
139
121
  isImageMode,
140
122
  manifestMap,
141
123
  mapName,
124
+ preferLocalDev,
142
125
  style,
143
126
  styleBaseUrl,
144
127
  styleUrl,
145
- tileBaseUrl,
146
- useExplicitStyleBaseUrl,
147
- useLocalDevFallback
128
+ themes,
129
+ tileBaseUrl
148
130
  ]);
149
131
  const resolvedAnalytics = useMemo(
150
- () => mergeAnalytics(analytics, runtimeStyle?.analytics),
132
+ () => mergeTileflowAnalytics(analytics, runtimeStyle?.analytics),
151
133
  [analytics, runtimeStyle]
152
134
  );
135
+ useEffect(() => {
136
+ centerRef.current = resolvedCenter;
137
+ zoomRef.current = resolvedZoom;
138
+ mapRef.current?.jumpTo({
139
+ center: resolvedCenter,
140
+ zoom: resolvedZoom
141
+ });
142
+ }, [resolvedCenter, resolvedZoom]);
143
+ useEffect(() => {
144
+ onLoadRef.current = onLoad;
145
+ }, [onLoad]);
146
+ useEffect(() => {
147
+ mapNameRef.current = mapName;
148
+ }, [mapName]);
149
+ useEffect(() => {
150
+ resolvedAnalyticsRef.current = resolvedAnalytics;
151
+ }, [resolvedAnalytics]);
153
152
  const runtimeImageUrl = useMemo(
154
- () => imageUrl ?? (isImageMode ? resolveStaticImageUrl({
155
- center,
153
+ () => imageUrl ?? (isImageMode ? resolveTileflowStaticImageUrl({
154
+ center: staticImageCenter,
156
155
  imageSize,
157
156
  manifestMap,
158
- zoom
157
+ zoom: resolvedZoom
159
158
  }) : void 0),
160
- [center, imageSize, imageUrl, isImageMode, manifestMap, zoom]
159
+ [imageSize, imageUrl, isImageMode, manifestMap, resolvedZoom, staticImageCenter]
161
160
  );
162
161
  const transformRequest = useMemo(
163
- () => createTransformRequest(resolvedAnalytics, sessionId),
164
- [resolvedAnalytics, sessionId]
162
+ () => createTransformRequest(
163
+ () => resolvedAnalyticsRef.current,
164
+ sessionId,
165
+ stableMapOptions?.transformRequest
166
+ ),
167
+ [sessionId, stableMapOptions?.transformRequest]
165
168
  );
166
169
  useEffect(() => {
167
170
  if (!isImageMode || imageUrl || !containerRef.current) {
@@ -170,7 +173,7 @@ function Map({
170
173
  }
171
174
  const element = containerRef.current;
172
175
  const updateImageSize = () => {
173
- const nextSize = normalizeStaticImageSize({
176
+ const nextSize = normalizeTileflowStaticImageSize({
174
177
  height: element.clientHeight,
175
178
  width: element.clientWidth
176
179
  });
@@ -190,56 +193,68 @@ function Map({
190
193
  return;
191
194
  }
192
195
  const map = new maplibregl.Map({
196
+ ...stableMapOptions,
193
197
  container: containerRef.current,
194
198
  style: runtimeStyle.style,
195
- center,
196
- zoom,
197
- interactive,
198
- attributionControl: { compact: true },
199
+ center: centerRef.current,
200
+ zoom: zoomRef.current,
201
+ interactive: resolvedInteractive,
202
+ attributionControl: stableMapOptions?.attributionControl ?? { compact: true },
199
203
  transformRequest
200
204
  });
201
205
  mapRef.current = map;
202
- const controls = interactive ? new maplibregl.NavigationControl() : null;
206
+ const controls = resolvedInteractive ? new maplibregl.NavigationControl() : null;
203
207
  if (controls) {
204
208
  map.addControl(controls, "top-right");
205
209
  }
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
210
  const resizeObserver = new ResizeObserver(() => {
214
211
  map.resize();
215
212
  });
216
213
  resizeObserver.observe(containerRef.current);
217
214
  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
- });
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
+ }
229
+ }
224
230
  });
225
231
  return () => {
226
232
  resizeObserver.disconnect();
227
- markerInstances.forEach((marker) => marker.remove());
233
+ clearMarkers(markerInstancesRef.current);
234
+ markerInstancesRef.current = [];
228
235
  map.remove();
229
236
  mapRef.current = null;
230
237
  };
231
- }, [
232
- center,
233
- interactive,
234
- mapName,
235
- markers,
236
- onLoad,
237
- resolvedAnalytics,
238
- runtimeStyle,
239
- sessionId,
240
- transformRequest,
241
- zoom
242
- ]);
238
+ }, [resolvedInteractive, runtimeStyle, sessionId, stableMapOptions, transformRequest]);
239
+ useEffect(() => {
240
+ const map = mapRef.current;
241
+ clearMarkers(markerInstancesRef.current);
242
+ markerInstancesRef.current = [];
243
+ if (!map) {
244
+ return;
245
+ }
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
+ });
253
+ return () => {
254
+ clearMarkers(markerInstancesRef.current);
255
+ markerInstancesRef.current = [];
256
+ };
257
+ }, [markers, runtimeStyle]);
243
258
  const frameStyle = {
244
259
  height,
245
260
  minHeight: 240,
@@ -264,222 +279,70 @@ function Map({
264
279
  }
265
280
  ) : null });
266
281
  }
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(/\/+$/, "");
282
+ return /* @__PURE__ */ jsx("div", { className, ref: containerRef, style: frameStyle });
286
283
  }
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);
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
+ );
309
291
  }
310
- return { url: nextUrl.toString() };
292
+ return applyTileflowAnalyticsRequest(url, request, getAnalytics(), sessionId);
311
293
  };
312
294
  }
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;
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;
358
300
  }
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
- };
301
+ return request ? { ...request, url: nextUrl } : { url: nextUrl };
366
302
  }
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
- }
303
+ function isPromiseLike(value) {
304
+ return Boolean(value && typeof value.then === "function");
375
305
  }
376
- function isTilePath(url) {
377
- return url.pathname.startsWith("/tiles/") || url.pathname.startsWith("/v1/tiles/");
306
+ function clearMarkers(markerInstances) {
307
+ markerInstances.forEach((marker) => marker.remove());
378
308
  }
379
- function isLocalDevHost() {
380
- if (typeof window === "undefined") {
381
- return false;
382
- }
383
- return ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname);
309
+ function useStableMapOptions(options) {
310
+ return useStableMapOptionValue(options);
384
311
  }
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
- );
312
+ function useStableMapOptionValue(value) {
313
+ const valueRef = useRef(value);
314
+ if (!areEquivalentMapOptionValues(valueRef.current, value)) {
315
+ valueRef.current = value;
399
316
  }
400
- return manifestCache.get(url);
317
+ return valueRef.current;
401
318
  }
402
- function resolveManifestMap(manifest, mapName) {
403
- const entry = manifest.maps?.[mapName] ?? manifest.styles?.[mapName];
404
- if (!entry) {
405
- return null;
319
+ function areEquivalentMapOptionValues(left, right, seen = /* @__PURE__ */ new WeakMap()) {
320
+ if (Object.is(left, right)) {
321
+ return true;
406
322
  }
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
- };
323
+ if (!left || !right || typeof left !== "object" || typeof right !== "object") {
324
+ return false;
415
325
  }
416
- const styleUrl = entry.styleUrl ?? entry.url;
417
- if (!styleUrl && !entry.mapId) {
418
- return null;
326
+ const seenRight = seen.get(left);
327
+ if (seenRight) {
328
+ return seenRight === right;
419
329
  }
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);
330
+ seen.set(left, right);
331
+ if (Array.isArray(left) || Array.isArray(right)) {
332
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => areEquivalentMapOptionValues(value, right[index], seen));
450
333
  }
451
- return { height, width };
452
- }
453
- function clampDimension(value) {
454
- if (!Number.isFinite(value) || value <= 0) {
455
- return 640;
334
+ if (!isPlainObject(left) || !isPlainObject(right)) {
335
+ return false;
456
336
  }
457
- return Math.max(
458
- 64,
459
- Math.min(maxStaticImageDimension, Math.round(value))
337
+ const leftKeys = Object.keys(left);
338
+ const rightKeys = Object.keys(right);
339
+ return leftKeys.length === rightKeys.length && leftKeys.every(
340
+ (key) => Object.hasOwn(right, key) && areEquivalentMapOptionValues(left[key], right[key], seen)
460
341
  );
461
342
  }
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;
343
+ function isPlainObject(value) {
344
+ const prototype = Object.getPrototypeOf(value);
345
+ return prototype === Object.prototype || prototype === null;
483
346
  }
484
347
  export {
485
348
  Map
package/dist/static.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  // src/static-map.tsx
2
4
  import {
3
5
  useEffect,
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@tileflow/react",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.10",
4
4
  "description": "React components for rendering Tileflow maps with MapLibre.",
5
5
  "type": "module",
6
+ "sideEffects": false,
6
7
  "homepage": "https://tileflow.dev",
7
8
  "repository": {
8
9
  "type": "git",
@@ -40,23 +41,21 @@
40
41
  "access": "public"
41
42
  },
42
43
  "dependencies": {
43
- "@tileflow/core": "0.1.0-alpha.0",
44
- "@tileflow/static": "0.1.0-alpha.0"
44
+ "@tileflow/core": "0.1.0-alpha.10",
45
+ "@tileflow/static": "0.1.0-alpha.10"
45
46
  },
46
47
  "peerDependencies": {
47
48
  "maplibre-gl": ">=5",
48
- "react": ">=18",
49
- "react-dom": ">=18"
49
+ "react": ">=18"
50
50
  },
51
51
  "devDependencies": {
52
52
  "maplibre-gl": "^5.24.0",
53
53
  "react": "^19.2.7",
54
- "react-dom": "^19.2.7",
55
- "@types/react": "^19.2.17",
56
- "@types/react-dom": "^19.2.3"
54
+ "@types/react": "^19.2.17"
57
55
  },
58
56
  "scripts": {
59
- "build": "tsup src/index.ts src/static.ts --format esm --dts --clean --external react --external react-dom --external maplibre-gl",
57
+ "build": "tsup",
58
+ "verify": "pnpm run build && tsx --test test/*.test.ts",
60
59
  "typecheck": "tsc -p tsconfig.json --noEmit",
61
60
  "lint": "tsc -p tsconfig.json --noEmit"
62
61
  }