@tileflow/react 0.1.0-alpha.27 → 0.1.0-alpha.28

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
@@ -99,6 +99,12 @@ for assets.
99
99
  explicit light/dark mapping, which Streets supplies. Theme changes preserve the MapLibre instance,
100
100
  camera, and interactions, and roll back on failure. `onThemeChange` reports transitions.
101
101
 
102
+ `themeTransition={{duration: 450}}` cross-fades theme changes over that many milliseconds.
103
+ `themeBlend={{themes: ['night', 'dusk', 'day'], position}}` shows a continuous blend of the map's
104
+ themes instead of `theme`: position 0 is the first theme and 1.5 is halfway from the second to the
105
+ third. Moving the position updates the map at once. See
106
+ [theme transitions and blends](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md).
107
+
102
108
  `mapOptions` accepts native MapLibre options except `container` and `style`. Direct camera props
103
109
  take priority over `mapOptions`, then the manifest view, then shared defaults. `Map` always selects
104
110
  a Tileflow map through its manifest; it has no renderer discriminator or direct-style mode.
package/dist/index.d.ts CHANGED
@@ -1,13 +1,20 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, ImgHTMLAttributes, ReactNode } from 'react';
3
3
  import { MapOptions, Map as Map$1 } from 'maplibre-gl';
4
- import { TileflowThemeTransition } from '@tileflow/core/browser';
5
- import { TileflowRuntimeSource, TileflowThemeSelection, TileflowAnalytics } from '@tileflow/core/runtime';
4
+ import { TileflowThemeTransitionOptions, TileflowThemeTransition } from '@tileflow/core/browser';
5
+ import { TileflowRuntimeSource, TileflowThemeSelection, TileflowThemeBlendSelection, TileflowAnalytics } from '@tileflow/core/runtime';
6
6
  import { TileflowAnnotation, TileflowInteractionState, TileflowAnnotationViewContext, TileflowInteractionBinding, TileflowInteractionViewContext, TileflowInteractionDiagnostic, TileflowInteractionEvent } from '@tileflow/interactions';
7
7
 
8
8
  type TileflowMapStyleSourceProps = {
9
9
  source: TileflowRuntimeSource;
10
10
  theme?: TileflowThemeSelection;
11
+ /**
12
+ * Shows a continuous blend of the map's themes instead of `theme`. Moving `position` changes the
13
+ * map at once; changing `themes` prepares a new blend.
14
+ */
15
+ themeBlend?: TileflowThemeBlendSelection;
16
+ /** Cross-fades theme changes, including entering and leaving a blend. */
17
+ themeTransition?: TileflowThemeTransitionOptions;
11
18
  };
12
19
 
13
20
  type TileflowMapSource = TileflowRuntimeSource;
package/dist/index.js CHANGED
@@ -31,12 +31,14 @@ import {
31
31
  defaultTileflowRuntimeView,
32
32
  loadTileflowManifest,
33
33
  mergeTileflowAnalytics,
34
+ nearestTileflowBlendTheme,
34
35
  normalizeTileflowRuntimeCenter,
35
36
  normalizeTileflowStaticImageSize,
36
37
  resolveTileflowManifestMap,
37
38
  resolveTileflowMapMode,
38
39
  resolveTileflowRuntimeStyle,
39
40
  resolveTileflowRuntimeTheme,
41
+ resolveTileflowRuntimeThemeBlend,
40
42
  resolveTileflowRuntimeView,
41
43
  resolveTileflowStaticImageUrl
42
44
  } from "@tileflow/core/runtime";
@@ -167,6 +169,7 @@ function prepareTileflowReactInteractionState(interactionState, defaultInteracti
167
169
  // src/map-style-inputs.ts
168
170
  import {
169
171
  validateTileflowRuntimeSource,
172
+ validateTileflowThemeBlendSelection,
170
173
  validateTileflowThemeSelection
171
174
  } from "@tileflow/core/runtime";
172
175
  function validateTileflowMapStyleInputs(input) {
@@ -175,8 +178,25 @@ function validateTileflowMapStyleInputs(input) {
175
178
  if (input.theme !== void 0 && !validateTileflowThemeSelection(input.theme)) {
176
179
  return { error: 'theme must be a concrete portable theme name or "system"', ok: false };
177
180
  }
181
+ if (input.themeBlend !== void 0 && !validateTileflowThemeBlendSelection(input.themeBlend)) {
182
+ return {
183
+ error: "themeBlend must name two to eight concrete themes and a position from 0 to themes.length - 1",
184
+ ok: false
185
+ };
186
+ }
187
+ if (input.themeTransition !== void 0 && !isThemeTransition(input.themeTransition)) {
188
+ return {
189
+ error: "themeTransition.duration must be a number of milliseconds from 0 to 5000",
190
+ ok: false
191
+ };
192
+ }
178
193
  return { ok: true };
179
194
  }
195
+ function isThemeTransition(value) {
196
+ if (!value || typeof value !== "object") return false;
197
+ const { duration } = value;
198
+ return duration === void 0 || typeof duration === "number" && Number.isFinite(duration) && duration >= 0 && duration <= 5e3;
199
+ }
180
200
  function assertTileflowMapStyleInputs(input) {
181
201
  const validation = validateTileflowMapStyleInputs(input);
182
202
  if (!validation.ok) {
@@ -238,6 +258,8 @@ function Map2(props) {
238
258
  mapOptions,
239
259
  analytics,
240
260
  theme,
261
+ themeBlend,
262
+ themeTransition,
241
263
  interactionState,
242
264
  interactions = defaultInteractions,
243
265
  onInteractionDiagnostic,
@@ -326,19 +348,31 @@ function Map2(props) {
326
348
  const manifestRequestKey = JSON.stringify([manifestUrl, mapName]);
327
349
  const currentManifestResolution = manifestResolution.key === manifestRequestKey ? manifestResolution : { key: manifestRequestKey, map: null, state: "loading" };
328
350
  const manifestMap = currentManifestResolution.state === "ready" ? currentManifestResolution.map : null;
351
+ const themeBlendKey = themeBlend ? JSON.stringify(themeBlend.themes) : void 0;
352
+ const themeBlendPosition = themeBlend?.position;
353
+ const blendThemeName = themeBlend ? nearestTileflowBlendTheme(themeBlend) : void 0;
329
354
  const themeResolution = useMemo(() => {
355
+ const selection = blendThemeName ?? theme;
330
356
  if (!manifestMap) {
331
- return { error: false, name: theme && theme !== "system" ? theme : void 0 };
357
+ return {
358
+ error: false,
359
+ name: selection && selection !== "system" ? selection : void 0
360
+ };
332
361
  }
333
362
  try {
363
+ if (themeBlendKey) {
364
+ for (const name of JSON.parse(themeBlendKey)) {
365
+ resolveTileflowRuntimeTheme(manifestMap, name);
366
+ }
367
+ }
334
368
  return {
335
369
  error: false,
336
- name: resolveTileflowRuntimeTheme(manifestMap, theme, systemColorScheme).name
370
+ name: resolveTileflowRuntimeTheme(manifestMap, selection, systemColorScheme).name
337
371
  };
338
372
  } catch {
339
373
  return { error: true, name: void 0 };
340
374
  }
341
- }, [manifestMap, systemColorScheme, theme]);
375
+ }, [blendThemeName, manifestMap, systemColorScheme, theme, themeBlendKey]);
342
376
  const resolvedThemeName = themeResolution.name;
343
377
  const manifestView = resolveTileflowRuntimeView({ manifestMap });
344
378
  const manifestCenter = useStableMapOptionValue(
@@ -477,6 +511,22 @@ function Map2(props) {
477
511
  });
478
512
  }, [isImageMode, manifestMap, runtimeSource, systemColorScheme, themeResolution]);
479
513
  runtimeStyleRef.current = runtimeStyle;
514
+ const blendStyles = useMemo(() => {
515
+ if (isImageMode || !manifestMap || !themeBlendKey) return null;
516
+ try {
517
+ return resolveTileflowRuntimeThemeBlend({
518
+ blend: { position: 0, themes: JSON.parse(themeBlendKey) },
519
+ manifestMap,
520
+ source: runtimeSource
521
+ });
522
+ } catch {
523
+ return null;
524
+ }
525
+ }, [isImageMode, manifestMap, runtimeSource, themeBlendKey]);
526
+ const blendRequestRef = useRef(null);
527
+ blendRequestRef.current = blendStyles && themeBlendPosition !== void 0 ? { position: themeBlendPosition, themes: blendStyles } : null;
528
+ const themeTransitionRef = useRef(themeTransition);
529
+ themeTransitionRef.current = themeTransition;
480
530
  const runtimeMapIdentity = `${manifestUrl}\0${runtimeSource.map}`;
481
531
  const runtimeStyleReady = runtimeStyle !== null || themeResolution.error && mapRef.current !== null;
482
532
  const resolvedAnalytics = useMemo(
@@ -919,9 +969,15 @@ function Map2(props) {
919
969
  });
920
970
  registerCleanup(() => lifecycle.dispose());
921
971
  const latestRuntimeStyle = runtimeStyleRef.current;
922
- if (themeController && latestRuntimeStyle && latestRuntimeStyle !== initialRuntimeStyle) {
972
+ const latestBlend = blendRequestRef.current;
973
+ if (themeController && latestBlend) {
974
+ if (latestRuntimeStyle) setActiveRuntimeResource(latestRuntimeStyle);
975
+ void themeController.setBlend(latestBlend, { transition: themeTransitionRef.current });
976
+ } else if (themeController && latestRuntimeStyle && latestRuntimeStyle !== initialRuntimeStyle) {
923
977
  setActiveRuntimeResource(latestRuntimeStyle);
924
- void themeController.setTheme(latestRuntimeStyle);
978
+ void themeController.setTheme(latestRuntimeStyle, {
979
+ transition: themeTransitionRef.current
980
+ });
925
981
  }
926
982
  }).catch((error) => {
927
983
  try {
@@ -942,19 +998,33 @@ function Map2(props) {
942
998
  }, [resolvedInteractive, runtimeMapIdentity, runtimeStyleReady, stableMapOptions]);
943
999
  useEffect(() => {
944
1000
  const controller = themeControllerRef.current;
945
- if (!controller) return;
1001
+ if (!controller || themeBlendKey) return;
946
1002
  if (themeResolution.error) {
947
1003
  void controller.setTheme(controller.getCurrent());
948
1004
  return;
949
1005
  }
950
- if (!runtimeStyle || controller.getCurrent() === runtimeStyle) return;
1006
+ if (!runtimeStyle || controller.getCurrent() === runtimeStyle && !controller.getBlend())
1007
+ return;
951
1008
  setActiveRuntimeResource(runtimeStyle);
952
- void controller.setTheme(runtimeStyle).then((result) => {
1009
+ void controller.setTheme(runtimeStyle, { transition: themeTransitionRef.current }).then((result) => {
953
1010
  if (result.status === "failed") {
954
1011
  console.error("Failed to change the Tileflow map theme", result.error);
955
1012
  }
956
1013
  });
957
- }, [runtimeStyle, themeResolution.error]);
1014
+ }, [runtimeStyle, themeBlendKey, themeResolution.error]);
1015
+ useEffect(() => {
1016
+ const controller = themeControllerRef.current;
1017
+ if (!controller || !blendStyles || themeBlendPosition === void 0) return;
1018
+ if (runtimeStyle) setActiveRuntimeResource(runtimeStyle);
1019
+ void controller.setBlend(
1020
+ { position: themeBlendPosition, themes: blendStyles },
1021
+ { transition: themeTransitionRef.current }
1022
+ ).then((result) => {
1023
+ if (result.status === "failed") {
1024
+ console.error("Failed to blend the Tileflow map themes", result.error);
1025
+ }
1026
+ });
1027
+ }, [blendStyles, runtimeStyle, themeBlendPosition]);
958
1028
  useLayoutEffect(() => {
959
1029
  if (!isImageMode) return;
960
1030
  readinessRunRef.current += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tileflow/react",
3
- "version": "0.1.0-alpha.27",
3
+ "version": "0.1.0-alpha.28",
4
4
  "license": "Apache-2.0",
5
5
  "description": "React components for rendering Tileflow maps with MapLibre.",
6
6
  "type": "module",
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@tileflow/core": ">=0.1.0-alpha.29 <0.1.0-beta.0",
46
+ "@tileflow/core": ">=0.1.0-alpha.30 <0.1.0-beta.0",
47
47
  "@tileflow/interactions": ">=0.1.0-alpha.4 <0.1.0-beta.0",
48
48
  "@tileflow/static": ">=0.1.0-alpha.27 <0.1.0-beta.0"
49
49
  },