@tileflow/react 0.1.0-alpha.1 → 0.1.0-alpha.11

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,20 +2,56 @@
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
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
+
13
44
  For simple hosted static images, use image mode:
14
45
 
15
46
  ```tsx
16
47
  <Map map="madrid" mode="image" center={[-3.7038, 40.4168]} zoom={12} />
17
48
  ```
18
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
+
19
55
  For full static scenes with bounds, overlays, and server-side precache, use
20
56
  `StaticMap` from `@tileflow/react/static` or helpers from `@tileflow/static`.
21
57
 
package/dist/index.d.ts CHANGED
@@ -1,25 +1,10 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, ImgHTMLAttributes } from 'react';
3
- import { Map as Map$1 } from 'maplibre-gl';
4
- import { TileflowConfig, MapLibreStyle, TileflowThemeConfig } from '@tileflow/core';
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
10
  mode?: 'interactive' | 'image';
@@ -29,7 +14,7 @@ type MapProps = {
29
14
  imageUrl?: string;
30
15
  styleBaseUrl?: string;
31
16
  tileBaseUrl?: string;
32
- themes?: Record<string, TileflowThemeConfig>;
17
+ themes?: TileflowProjectThemes;
33
18
  manifestUrl?: string;
34
19
  preferLocalDev?: boolean;
35
20
  alt?: string;
@@ -39,10 +24,11 @@ type MapProps = {
39
24
  height?: CSSProperties['height'];
40
25
  imageLoading?: ImgHTMLAttributes<HTMLImageElement>['loading'];
41
26
  interactive?: boolean;
27
+ mapOptions?: TileflowMapOptions;
42
28
  markers?: MapMarker[];
43
29
  analytics?: TileflowAnalytics;
44
30
  onLoad?: (map: Map$1) => void;
45
31
  };
46
- declare function Map({ map: mapName, mode, config, style, styleUrl, imageUrl, styleBaseUrl, tileBaseUrl, themes, 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;
47
33
 
48
- export { Map, type MapMarker, type MapProps };
34
+ export { Map, type MapMarker, type MapProps, type TileflowMapOptions };
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  // src/map.tsx
2
4
  import {
3
5
  useEffect,
@@ -7,15 +9,22 @@ import {
7
9
  } from "react";
8
10
  import maplibregl from "maplibre-gl";
9
11
  import {
10
- createStyle
12
+ createTileflowSessionId,
13
+ defaultTileflowManifestUrl,
14
+ loadTileflowManifest,
15
+ mergeTileflowAnalytics,
16
+ normalizeTileflowStaticImageSize,
17
+ resolveTileflowAnalyticsRequestUrl,
18
+ resolveTileflowManifestMap,
19
+ resolveTileflowMapMode,
20
+ resolveTileflowRuntimeStyle,
21
+ resolveTileflowStaticImageUrl,
22
+ shouldLoadTileflowManifest,
23
+ startTileflowSession
11
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",
@@ -26,49 +35,62 @@ function Map({
26
35
  styleBaseUrl,
27
36
  tileBaseUrl,
28
37
  themes,
29
- manifestUrl = defaultManifestUrl,
38
+ manifestUrl = defaultTileflowManifestUrl,
30
39
  preferLocalDev = true,
31
40
  alt = "",
32
- center = defaultCenter,
33
- zoom = 2,
41
+ center,
42
+ zoom,
34
43
  className,
35
44
  height = 420,
36
45
  imageLoading = "eager",
37
- interactive = true,
46
+ interactive,
47
+ mapOptions,
38
48
  markers = defaultMarkers,
39
49
  analytics,
40
50
  onLoad
41
51
  }) {
42
52
  const containerRef = useRef(null);
43
53
  const mapRef = useRef(null);
44
- const sessionId = useMemo(() => createSessionId(), []);
54
+ const markerInstancesRef = useRef([]);
55
+ const stableMapOptions = useStableMapOptions(mapOptions);
56
+ const resolvedCenter = useStableMapOptionValue(
57
+ center ?? stableMapOptions?.center ?? defaultCenter
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(), []);
45
69
  const [manifestMap, setManifestMap] = useState(null);
46
70
  const [imageSize, setImageSize] = useState(null);
47
- const isImageMode = mode === "image";
48
- const useExplicitStyleBaseUrl = useMemo(
49
- () => Boolean(!isImageMode && mapName && !style && !styleUrl && !config && styleBaseUrl),
50
- [config, isImageMode, mapName, style, styleBaseUrl, styleUrl]
51
- );
52
- const useLocalDevFallback = useMemo(
53
- () => Boolean(
54
- !isImageMode && mapName && !style && !styleUrl && !config && !styleBaseUrl && preferLocalDev && isLocalDevHost()
55
- ),
56
- [config, isImageMode, mapName, preferLocalDev, style, styleBaseUrl, styleUrl]
57
- );
71
+ const resolvedMode = resolveTileflowMapMode({ imageUrl, mode, preferLocalDev });
72
+ const isImageMode = resolvedMode === "image";
58
73
  useEffect(() => {
59
- const shouldLoadManifest = Boolean(
60
- mapName && !config && (isImageMode && !imageUrl || !isImageMode && !style && !styleUrl && !useExplicitStyleBaseUrl)
61
- );
74
+ const shouldLoadManifest = shouldLoadTileflowManifest({
75
+ config,
76
+ imageMode: isImageMode,
77
+ imageUrl,
78
+ map: mapName,
79
+ style,
80
+ styleBaseUrl,
81
+ styleUrl
82
+ });
62
83
  if (!shouldLoadManifest) {
63
84
  setManifestMap(null);
64
85
  return;
65
86
  }
66
87
  let cancelled = false;
67
- loadManifest(manifestUrl).then((manifest) => {
88
+ setManifestMap(null);
89
+ loadTileflowManifest(manifestUrl).then((manifest) => {
68
90
  if (cancelled) {
69
91
  return;
70
92
  }
71
- setManifestMap(manifest ? resolveManifestMap(manifest, mapName) : null);
93
+ setManifestMap(manifest && mapName ? resolveTileflowManifestMap(manifest, mapName) : null);
72
94
  }).catch((error) => {
73
95
  if (!cancelled) {
74
96
  console.error("Failed to load Tileflow manifest", error);
@@ -78,78 +100,71 @@ function Map({
78
100
  return () => {
79
101
  cancelled = true;
80
102
  };
81
- }, [
82
- config,
83
- imageUrl,
84
- isImageMode,
85
- manifestUrl,
86
- mapName,
87
- style,
88
- styleUrl,
89
- useExplicitStyleBaseUrl
90
- ]);
103
+ }, [config, imageUrl, isImageMode, manifestUrl, mapName, style, styleUrl, styleBaseUrl]);
91
104
  const runtimeStyle = useMemo(() => {
92
105
  if (isImageMode) {
93
106
  return null;
94
107
  }
95
- if (style) {
96
- return { style };
97
- }
98
- if (styleUrl) {
99
- return {
100
- analytics: inferAnalyticsFromStyleUrl(styleUrl),
101
- style: styleUrl
102
- };
103
- }
104
- if (mapName && useExplicitStyleBaseUrl) {
105
- return { style: resolveStyleUrl(mapName, styleBaseUrl) };
106
- }
107
- if (mapName) {
108
- if (manifestMap?.styleUrl) {
109
- return {
110
- analytics: {
111
- apiUrl: manifestMap.apiUrl,
112
- mapId: manifestMap.mapId,
113
- styleId: manifestMap.styleId
114
- },
115
- style: manifestMap.styleUrl
116
- };
117
- }
118
- if (useLocalDevFallback) {
119
- return { style: resolveStyleUrl(mapName) };
120
- }
121
- return null;
122
- }
123
- return { style: createStyle(config, { themes, tileBaseUrl }) };
108
+ return resolveTileflowRuntimeStyle({
109
+ config,
110
+ manifestMap,
111
+ map: mapName,
112
+ preferLocalDev,
113
+ style,
114
+ styleBaseUrl,
115
+ styleUrl,
116
+ themes,
117
+ tileBaseUrl
118
+ });
124
119
  }, [
125
120
  config,
126
121
  isImageMode,
127
122
  manifestMap,
128
123
  mapName,
124
+ preferLocalDev,
129
125
  style,
130
126
  styleBaseUrl,
131
127
  styleUrl,
132
128
  themes,
133
- tileBaseUrl,
134
- useExplicitStyleBaseUrl,
135
- useLocalDevFallback
129
+ tileBaseUrl
136
130
  ]);
137
131
  const resolvedAnalytics = useMemo(
138
- () => mergeAnalytics(analytics, runtimeStyle?.analytics),
132
+ () => mergeTileflowAnalytics(analytics, runtimeStyle?.analytics),
139
133
  [analytics, runtimeStyle]
140
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]);
141
152
  const runtimeImageUrl = useMemo(
142
- () => imageUrl ?? (isImageMode ? resolveStaticImageUrl({
143
- center,
153
+ () => imageUrl ?? (isImageMode ? resolveTileflowStaticImageUrl({
154
+ center: staticImageCenter,
144
155
  imageSize,
145
156
  manifestMap,
146
- zoom
157
+ zoom: resolvedZoom
147
158
  }) : void 0),
148
- [center, imageSize, imageUrl, isImageMode, manifestMap, zoom]
159
+ [imageSize, imageUrl, isImageMode, manifestMap, resolvedZoom, staticImageCenter]
149
160
  );
150
161
  const transformRequest = useMemo(
151
- () => createTransformRequest(resolvedAnalytics, sessionId),
152
- [resolvedAnalytics, sessionId]
162
+ () => createTransformRequest(
163
+ () => resolvedAnalyticsRef.current,
164
+ sessionId,
165
+ stableMapOptions?.transformRequest
166
+ ),
167
+ [sessionId, stableMapOptions?.transformRequest]
153
168
  );
154
169
  useEffect(() => {
155
170
  if (!isImageMode || imageUrl || !containerRef.current) {
@@ -158,7 +173,7 @@ function Map({
158
173
  }
159
174
  const element = containerRef.current;
160
175
  const updateImageSize = () => {
161
- const nextSize = normalizeStaticImageSize({
176
+ const nextSize = normalizeTileflowStaticImageSize({
162
177
  height: element.clientHeight,
163
178
  width: element.clientWidth
164
179
  });
@@ -178,56 +193,68 @@ function Map({
178
193
  return;
179
194
  }
180
195
  const map = new maplibregl.Map({
196
+ ...stableMapOptions,
181
197
  container: containerRef.current,
182
198
  style: runtimeStyle.style,
183
- center,
184
- zoom,
185
- interactive,
186
- attributionControl: { compact: true },
199
+ center: centerRef.current,
200
+ zoom: zoomRef.current,
201
+ interactive: resolvedInteractive,
202
+ attributionControl: stableMapOptions?.attributionControl ?? { compact: true },
187
203
  transformRequest
188
204
  });
189
205
  mapRef.current = map;
190
- const controls = interactive ? new maplibregl.NavigationControl() : null;
206
+ const controls = resolvedInteractive ? new maplibregl.NavigationControl() : null;
191
207
  if (controls) {
192
208
  map.addControl(controls, "top-right");
193
209
  }
194
- const markerInstances = markers.map((marker) => {
195
- const element = document.createElement("div");
196
- element.className = "tileflow-marker";
197
- element.style.background = marker.color ?? "#C6A15B";
198
- element.title = marker.label ?? marker.id;
199
- return new maplibregl.Marker({ element }).setLngLat(marker.coordinates).addTo(map);
200
- });
201
210
  const resizeObserver = new ResizeObserver(() => {
202
211
  map.resize();
203
212
  });
204
213
  resizeObserver.observe(containerRef.current);
205
214
  map.on("load", () => {
206
- onLoad?.(map);
207
- startSession(resolvedAnalytics, {
208
- mapId: resolvedAnalytics?.mapId,
209
- sessionId,
210
- styleId: resolvedAnalytics?.styleId ?? (typeof runtimeStyle.style === "string" ? runtimeStyle.style : mapName)
211
- });
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
+ }
212
230
  });
213
231
  return () => {
214
232
  resizeObserver.disconnect();
215
- markerInstances.forEach((marker) => marker.remove());
233
+ clearMarkers(markerInstancesRef.current);
234
+ markerInstancesRef.current = [];
216
235
  map.remove();
217
236
  mapRef.current = null;
218
237
  };
219
- }, [
220
- center,
221
- interactive,
222
- mapName,
223
- markers,
224
- onLoad,
225
- resolvedAnalytics,
226
- runtimeStyle,
227
- sessionId,
228
- transformRequest,
229
- zoom
230
- ]);
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]);
231
258
  const frameStyle = {
232
259
  height,
233
260
  minHeight: 240,
@@ -254,208 +281,68 @@ function Map({
254
281
  }
255
282
  return /* @__PURE__ */ jsx("div", { className, ref: containerRef, style: frameStyle });
256
283
  }
257
- function resolveStyleUrl(mapName, styleBaseUrl) {
258
- const baseUrl = normalizeUrl(styleBaseUrl ?? getDefaultStyleBaseUrl());
259
- return `${baseUrl}/styles/${mapName}.json`;
260
- }
261
- function getDefaultStyleBaseUrl() {
262
- const globalValue = globalThis.__TILEFLOW_STYLE_BASE_URL__;
263
- return globalValue ?? "http://localhost:3333";
264
- }
265
- function normalizeUrl(value) {
266
- return value.replace(/\/+$/, "");
267
- }
268
- function createSessionId() {
269
- if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
270
- return `ses_${crypto.randomUUID()}`;
271
- }
272
- return `ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
273
- }
274
- function createTransformRequest(analytics, sessionId) {
275
- if (!analytics || analytics.enabled === false) {
276
- return void 0;
277
- }
278
- return (url) => {
279
- const apiUrl = getAnalyticsApiUrl(analytics);
280
- if (!apiUrl || !isTileflowTileUrl(url, apiUrl)) {
281
- return void 0;
282
- }
283
- const nextUrl = new URL(url);
284
- nextUrl.searchParams.set("session", sessionId);
285
- if (analytics.mapId) {
286
- nextUrl.searchParams.set("map", analytics.mapId);
287
- }
288
- if (analytics.styleId) {
289
- 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
+ );
290
291
  }
291
- return { url: nextUrl.toString() };
292
+ return applyTileflowAnalyticsRequest(url, request, getAnalytics(), sessionId);
292
293
  };
293
294
  }
294
- function startSession(analytics, input) {
295
- if (!analytics || analytics.enabled === false || !input.mapId || typeof navigator === "undefined") {
296
- return;
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;
297
300
  }
298
- const endpoint = analytics.endpoint ?? `${getAnalyticsApiUrl(analytics)}/v1/sessions/start`;
299
- const payload = JSON.stringify({
300
- mapId: input.mapId,
301
- metadata: analytics.metadata,
302
- referrer: typeof document === "undefined" ? void 0 : document.referrer || "",
303
- sdkVersion: analytics.sdkVersion,
304
- sessionId: input.sessionId,
305
- source: analytics.source ?? "react",
306
- styleId: input.styleId,
307
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
308
- });
309
- const blob = new Blob([payload], { type: "application/json" });
310
- if (navigator.sendBeacon?.(endpoint, blob)) {
311
- return;
312
- }
313
- void fetch(endpoint, {
314
- body: payload,
315
- credentials: "omit",
316
- headers: {
317
- "Content-Type": "application/json"
318
- },
319
- keepalive: true,
320
- method: "POST"
321
- });
301
+ return request ? { ...request, url: nextUrl } : { url: nextUrl };
322
302
  }
323
- function getAnalyticsApiUrl(analytics) {
324
- if (analytics.apiUrl) {
325
- return normalizeUrl(analytics.apiUrl);
326
- }
327
- if (analytics.endpoint) {
328
- const url = new URL(analytics.endpoint);
329
- return url.origin;
330
- }
331
- return "https://api.tileflow.dev";
332
- }
333
- function mergeAnalytics(userAnalytics, runtimeAnalytics) {
334
- if (userAnalytics?.enabled === false) {
335
- return userAnalytics;
336
- }
337
- if (!userAnalytics && !runtimeAnalytics?.mapId) {
338
- return void 0;
339
- }
340
- return {
341
- ...runtimeAnalytics,
342
- ...userAnalytics,
343
- apiUrl: userAnalytics?.apiUrl ?? runtimeAnalytics?.apiUrl,
344
- mapId: userAnalytics?.mapId ?? runtimeAnalytics?.mapId,
345
- styleId: userAnalytics?.styleId ?? runtimeAnalytics?.styleId
346
- };
347
- }
348
- function isTileflowTileUrl(url, apiUrl) {
349
- try {
350
- const parsedUrl = new URL(url);
351
- const parsedApiUrl = new URL(apiUrl);
352
- return parsedUrl.origin === parsedApiUrl.origin && isTilePath(parsedUrl);
353
- } catch {
354
- return false;
355
- }
303
+ function isPromiseLike(value) {
304
+ return Boolean(value && typeof value.then === "function");
356
305
  }
357
- function isTilePath(url) {
358
- return url.pathname.startsWith("/tiles/") || url.pathname.startsWith("/v1/tiles/");
306
+ function clearMarkers(markerInstances) {
307
+ markerInstances.forEach((marker) => marker.remove());
359
308
  }
360
- function isLocalDevHost() {
361
- if (typeof window === "undefined") {
362
- return false;
363
- }
364
- return ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname);
309
+ function useStableMapOptions(options) {
310
+ return useStableMapOptionValue(options);
365
311
  }
366
- function loadManifest(url) {
367
- if (!manifestCache.has(url)) {
368
- manifestCache.set(
369
- url,
370
- fetch(url, { cache: "no-store" }).then(async (response) => {
371
- if (response.status === 404) {
372
- return null;
373
- }
374
- if (!response.ok) {
375
- throw new Error(`Tileflow manifest failed: ${response.status}`);
376
- }
377
- return await response.json();
378
- })
379
- );
312
+ function useStableMapOptionValue(value) {
313
+ const valueRef = useRef(value);
314
+ if (!areEquivalentMapOptionValues(valueRef.current, value)) {
315
+ valueRef.current = value;
380
316
  }
381
- return manifestCache.get(url);
317
+ return valueRef.current;
382
318
  }
383
- function resolveManifestMap(manifest, mapName) {
384
- const entry = manifest.maps?.[mapName] ?? manifest.styles?.[mapName];
385
- if (!entry) {
386
- return null;
319
+ function areEquivalentMapOptionValues(left, right, seen = /* @__PURE__ */ new WeakMap()) {
320
+ if (Object.is(left, right)) {
321
+ return true;
387
322
  }
388
- if (typeof entry === "string") {
389
- const analytics2 = inferAnalyticsFromStyleUrl(entry);
390
- return {
391
- apiUrl: analytics2?.apiUrl ?? manifest.apiUrl,
392
- mapId: analytics2?.mapId,
393
- styleId: analytics2?.styleId,
394
- styleUrl: entry
395
- };
396
- }
397
- const styleUrl = entry.styleUrl ?? entry.url;
398
- if (!styleUrl && !entry.mapId) {
399
- return null;
400
- }
401
- const analytics = styleUrl ? inferAnalyticsFromStyleUrl(styleUrl) : void 0;
402
- return {
403
- apiUrl: entry.apiUrl ?? manifest.apiUrl ?? analytics?.apiUrl,
404
- mapId: entry.mapId ?? analytics?.mapId,
405
- styleId: entry.styleId ?? analytics?.styleId,
406
- styleUrl
407
- };
408
- }
409
- function resolveStaticImageUrl(input) {
410
- if (!input.manifestMap?.mapId || !input.imageSize) {
411
- return void 0;
323
+ if (!left || !right || typeof left !== "object" || typeof right !== "object") {
324
+ return false;
412
325
  }
413
- const apiUrl = normalizeUrl(input.manifestMap.apiUrl ?? "https://api.tileflow.dev");
414
- const url = new URL(`/maps/${input.manifestMap.mapId}/static.png`, apiUrl);
415
- url.searchParams.set("center", input.center.join(","));
416
- url.searchParams.set("zoom", String(input.zoom));
417
- url.searchParams.set("width", String(input.imageSize.width));
418
- url.searchParams.set("height", String(input.imageSize.height));
419
- return url.toString();
420
- }
421
- function normalizeStaticImageSize(input) {
422
- let width = clampDimension(input.width);
423
- let height = clampDimension(input.height);
424
- const pixelCount = width * height;
425
- if (pixelCount > maxStaticImagePixels) {
426
- const scale = Math.sqrt(maxStaticImagePixels / pixelCount);
427
- width = clampDimension(width * scale);
428
- height = clampDimension(height * scale);
326
+ const seenRight = seen.get(left);
327
+ if (seenRight) {
328
+ return seenRight === right;
429
329
  }
430
- return { height, width };
431
- }
432
- function clampDimension(value) {
433
- if (!Number.isFinite(value) || value <= 0) {
434
- return 640;
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));
435
333
  }
436
- return Math.max(64, Math.min(maxStaticImageDimension, Math.round(value)));
437
- }
438
- function inferAnalyticsFromStyleUrl(styleUrl) {
439
- try {
440
- const url = new URL(styleUrl, getLocationOrigin());
441
- const match = url.pathname.match(/\/(?:v1\/)?maps\/([^/]+)\/style\.json$/);
442
- if (!match?.[1]) {
443
- return void 0;
444
- }
445
- return {
446
- apiUrl: url.origin,
447
- mapId: match[1],
448
- styleId: url.toString()
449
- };
450
- } catch {
451
- return void 0;
334
+ if (!isPlainObject(left) || !isPlainObject(right)) {
335
+ return false;
452
336
  }
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)
341
+ );
453
342
  }
454
- function getLocationOrigin() {
455
- if (typeof window === "undefined") {
456
- return "http://localhost";
457
- }
458
- return window.location.origin;
343
+ function isPlainObject(value) {
344
+ const prototype = Object.getPrototypeOf(value);
345
+ return prototype === Object.prototype || prototype === null;
459
346
  }
460
347
  export {
461
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.1",
3
+ "version": "0.1.0-alpha.11",
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/static": "0.1.0-alpha.1",
44
- "@tileflow/core": "0.1.0-alpha.1"
44
+ "@tileflow/core": "0.1.0-alpha.11",
45
+ "@tileflow/static": "0.1.0-alpha.11"
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
  }