@tileflow/svelte 0.1.0-alpha.26 → 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
@@ -90,6 +90,12 @@ Omitting `theme` uses `defaultTheme`. `system` needs an explicit light/dark mapp
90
90
  supplies. Theme changes keep the MapLibre instance, camera, and interactions, and roll back on
91
91
  failure. `onThemeChange` reports transitions.
92
92
 
93
+ `themeTransition={{duration: 450}}` cross-fades theme changes over that many milliseconds.
94
+ `themeBlend={{themes: ['night', 'dusk', 'day'], position}}` shows a continuous blend of the map's
95
+ themes instead of `theme`: position 0 is the first theme and 1.5 is halfway from the second to the
96
+ third. Moving the position updates the map at once. See
97
+ [theme transitions and blends](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md).
98
+
93
99
  `mapOptions` accepts native MapLibre options except `container` and `style`. Direct props win over
94
100
  those options, then the manifest view, then shared defaults. `TileflowMap` always selects a Tileflow
95
101
  map through its manifest; it has no renderer discriminator or direct-style mode. The obsolete
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tileflow/svelte",
3
- "version": "0.1.0-alpha.26",
3
+ "version": "0.1.0-alpha.28",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Svelte component for rendering Tileflow maps with MapLibre.",
6
6
  "type": "module",
@@ -44,7 +44,7 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@tileflow/core": ">=0.1.0-alpha.29 <0.1.0-beta.0",
47
+ "@tileflow/core": ">=0.1.0-alpha.32 <0.1.0-beta.0",
48
48
  "@tileflow/interactions": ">=0.1.0-alpha.4 <0.1.0-beta.0"
49
49
  },
50
50
  "peerDependencies": {
@@ -52,8 +52,8 @@
52
52
  "svelte": ">=5 <6"
53
53
  },
54
54
  "devDependencies": {
55
- "maplibre-gl": "6.9.0",
56
- "svelte": "^5.43.17",
55
+ "maplibre-gl": "6.13.0",
56
+ "svelte": "^5.57.2",
57
57
  "svelte-check": "^4.3.4"
58
58
  },
59
59
  "scripts": {
@@ -15,15 +15,18 @@
15
15
  defaultTileflowRuntimeView,
16
16
  loadTileflowManifest,
17
17
  mergeTileflowAnalytics,
18
+ nearestTileflowBlendTheme,
18
19
  normalizeTileflowRuntimeCenter,
19
20
  normalizeTileflowStaticImageSize,
20
21
  resolveTileflowManifestMap,
21
22
  resolveTileflowMapMode,
22
23
  resolveTileflowRuntimeStyle,
23
24
  resolveTileflowRuntimeTheme,
25
+ resolveTileflowRuntimeThemeBlend,
24
26
  resolveTileflowRuntimeView,
25
27
  resolveTileflowStaticImageUrl,
26
28
  type TileflowRuntimeManifestMap,
29
+ type TileflowRuntimeStyle,
27
30
  } from '@tileflow/core/runtime';
28
31
  import {
29
32
  attachTileflowAtmosphere,
@@ -96,6 +99,8 @@
96
99
  export let popup: TileflowMapProps['popup'] = undefined;
97
100
  export let source: TileflowMapProps['source'];
98
101
  export let theme: TileflowMapProps['theme'] = undefined;
102
+ export let themeBlend: TileflowMapProps['themeBlend'] = undefined;
103
+ export let themeTransition: TileflowMapProps['themeTransition'] = undefined;
99
104
  export let tooltip: TileflowMapProps['tooltip'] = undefined;
100
105
  export let zoom: TileflowMapProps['zoom'] = undefined;
101
106
 
@@ -167,7 +172,7 @@
167
172
  : {defaultInteractionState: initialInteractionState}),
168
173
  });
169
174
  $: resolvedMode = resolveTileflowMapMode({mode});
170
- $: assertTileflowMapStyleInputs({source, theme});
175
+ $: assertTileflowMapStyleInputs({source, theme, themeBlend, themeTransition});
171
176
  $: resolvedCaptureId = normalizeTileflowCaptureId(captureId);
172
177
  $: isImageMode = resolvedMode === 'image';
173
178
  $: mapName = source.map;
@@ -183,7 +188,11 @@
183
188
  colorScheme: systemColorScheme,
184
189
  manifestMap,
185
190
  theme,
191
+ themeBlend,
186
192
  });
193
+ $: themeBlendKey = themeBlend ? JSON.stringify(themeBlend.themes) : undefined;
194
+ $: themeBlendPosition = themeBlend?.position;
195
+ $: themeBlendHold = themeBlend?.hold === true;
187
196
  $: resolvedThemeName = themeResolution.name;
188
197
  $: manifestView = resolveTileflowRuntimeView({manifestMap});
189
198
  $: manifestCenter = normalizeTileflowRuntimeCenter(manifestView.center);
@@ -204,6 +213,7 @@
204
213
  source,
205
214
  theme: themeResolution.name,
206
215
  });
216
+ $: blendStyles = resolveBlendStyles({isImageMode, key: themeBlendKey, manifestMap, source});
207
217
  $: resolvedAnalytics = mergeTileflowAnalytics(analytics, runtimeStyle?.analytics);
208
218
  $: runtimeImageUrl =
209
219
  imageUrl ??
@@ -295,8 +305,16 @@
295
305
  syncSystemColorSchemeSubscription();
296
306
  }
297
307
 
308
+ $: if (mounted) {
309
+ blendStyles;
310
+ themeBlendPosition;
311
+ themeBlendHold;
312
+ void switchBlend();
313
+ }
314
+
298
315
  $: if (mounted) {
299
316
  runtimeStyle;
317
+ themeBlendKey;
300
318
  void switchTheme();
301
319
  }
302
320
 
@@ -563,8 +581,6 @@
563
581
  pitch: resolvedPitch,
564
582
  style: runtime.style as StyleSpecification | string,
565
583
  transformRequest: createTileflowTransformRequest({
566
- always: true,
567
- asyncAnalyticsTiming: 'request',
568
584
  getAnalytics: () => resolvedAnalytics,
569
585
  sessionController: session,
570
586
  sessionId: session.sessionId,
@@ -755,14 +771,34 @@
755
771
  return () => subscription.unsubscribe();
756
772
  },
757
773
  });
758
- if (themeController && runtimeStyle && runtimeStyle !== runtime) {
774
+ if (themeController && blendStyles && themeBlend) {
775
+ void switchBlend();
776
+ } else if (themeController && runtimeStyle && runtimeStyle !== runtime) {
759
777
  activeRuntimeResource = runtimeStyle;
760
- void themeController.setTheme(runtimeStyle);
778
+ void themeController.setTheme(runtimeStyle, {transition: themeTransition});
779
+ }
780
+ }
781
+
782
+ async function switchBlend() {
783
+ const position = themeBlendPosition;
784
+ if (!themeController || !blendStyles || position === undefined) return;
785
+ if (runtimeStyle) activeRuntimeResource = runtimeStyle;
786
+ const result = await themeController.setBlend(
787
+ {hold: themeBlendHold, position, themes: blendStyles},
788
+ {transition: themeTransition},
789
+ );
790
+ if (result.status === 'failed') {
791
+ console.error('Failed to blend the Tileflow map themes', result.error);
761
792
  }
762
793
  }
763
794
 
764
795
  async function switchTheme() {
765
796
  if (!themeController) return;
797
+ if (themeBlend) {
798
+ // The blend names its nearest theme; the blend itself is applied by switchBlend.
799
+ if (runtimeStyle) activeRuntimeResource = runtimeStyle;
800
+ return;
801
+ }
766
802
  if (themeResolution.error) {
767
803
  await themeController.setTheme(themeController.getCurrent());
768
804
  return;
@@ -770,7 +806,7 @@
770
806
  const next = runtimeStyle;
771
807
  if (!next) return;
772
808
  activeRuntimeResource = next;
773
- const result = await themeController.setTheme(next);
809
+ const result = await themeController.setTheme(next, {transition: themeTransition});
774
810
  if (result.status === 'failed') {
775
811
  console.error('Failed to change the Tileflow map theme', result.error);
776
812
  }
@@ -999,11 +1035,7 @@
999
1035
  }
1000
1036
 
1001
1037
  type BridgeInteractionDiagnosticSource =
1002
- | 'annotation-runtime'
1003
- | 'portal'
1004
- | 'renderers'
1005
- | 'semantic-runtime'
1006
- | 'state';
1038
+ 'annotation-runtime' | 'portal' | 'renderers' | 'semantic-runtime' | 'state';
1007
1039
 
1008
1040
  function setBridgeInteractionDiagnostic(
1009
1041
  source: BridgeInteractionDiagnosticSource,
@@ -1206,24 +1238,48 @@
1206
1238
  colorScheme: 'dark' | 'light';
1207
1239
  manifestMap: TileflowRuntimeManifestMap | null;
1208
1240
  theme: TileflowMapProps['theme'];
1241
+ themeBlend: TileflowMapProps['themeBlend'];
1209
1242
  }): Readonly<{error: boolean; name: string | undefined}> {
1243
+ // A blend shows its nearest theme until the blend style is ready, and names it while blending.
1244
+ const selection = input.themeBlend ? nearestTileflowBlendTheme(input.themeBlend) : input.theme;
1210
1245
  if (!input.manifestMap) {
1211
1246
  return {
1212
1247
  error: false,
1213
- name: input.theme && input.theme !== 'system' ? input.theme : undefined,
1248
+ name: selection && selection !== 'system' ? selection : undefined,
1214
1249
  };
1215
1250
  }
1216
1251
 
1217
1252
  try {
1253
+ for (const name of input.themeBlend?.themes ?? []) {
1254
+ resolveTileflowRuntimeTheme(input.manifestMap, name);
1255
+ }
1218
1256
  return {
1219
1257
  error: false,
1220
- name: resolveTileflowRuntimeTheme(input.manifestMap, input.theme, input.colorScheme).name,
1258
+ name: resolveTileflowRuntimeTheme(input.manifestMap, selection, input.colorScheme).name,
1221
1259
  };
1222
1260
  } catch {
1223
1261
  return {error: true, name: undefined};
1224
1262
  }
1225
1263
  }
1226
1264
 
1265
+ function resolveBlendStyles(input: {
1266
+ isImageMode: boolean;
1267
+ key: string | undefined;
1268
+ manifestMap: TileflowRuntimeManifestMap | null;
1269
+ source: TileflowMapProps['source'];
1270
+ }): TileflowRuntimeStyle[] | null {
1271
+ if (input.isImageMode || !input.manifestMap || !input.key) return null;
1272
+ try {
1273
+ return resolveTileflowRuntimeThemeBlend({
1274
+ blend: {position: 0, themes: JSON.parse(input.key) as string[]},
1275
+ manifestMap: input.manifestMap,
1276
+ source: input.source,
1277
+ });
1278
+ } catch {
1279
+ return null;
1280
+ }
1281
+ }
1282
+
1227
1283
  function resolveRuntimeResolutionState(input: {
1228
1284
  currentManifestResolutionState: 'error' | 'loading' | 'ready';
1229
1285
  imageSize: {height: number; width: number} | null;
package/src/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type {Map as MapLibreMap, MapOptions as MapLibreMapOptions} from 'maplibre-gl';
2
2
  import type {Snippet, SvelteComponentTyped} from 'svelte';
3
- import type {TileflowThemeTransition} from '@tileflow/core/browser';
3
+ import type {
4
+ TileflowBrowserThemeBlendSelection,
5
+ TileflowThemeTransition,
6
+ TileflowThemeTransitionOptions,
7
+ } from '@tileflow/core/browser';
4
8
  import type {
5
9
  TileflowAnalytics,
6
10
  TileflowRuntimeSource,
@@ -45,6 +49,14 @@ type TileflowMapBaseProps = {
45
49
  mapOptions?: TileflowMapOptions;
46
50
  onThemeChange?: (transition: TileflowThemeTransition) => void;
47
51
  theme?: TileflowThemeSelection;
52
+ /**
53
+ * Shows a continuous blend of the map's themes instead of `theme`. Moving `position` changes the
54
+ * map at once; changing `themes` prepares a new blend. `hold` keeps labels, icons, and mixed
55
+ * artwork back during a camera move; clearing it brings them in at once.
56
+ */
57
+ themeBlend?: TileflowBrowserThemeBlendSelection;
58
+ /** Cross-fades theme changes, including entering and leaving a blend. */
59
+ themeTransition?: TileflowThemeTransitionOptions;
48
60
  zoom?: number;
49
61
  };
50
62
 
@@ -1,6 +1,8 @@
1
1
  type TileflowMapStyleInput = {
2
2
  source?: unknown;
3
3
  theme?: unknown;
4
+ themeBlend?: unknown;
5
+ themeTransition?: unknown;
4
6
  };
5
7
 
6
8
  type TileflowMapStyleInputValidation =
@@ -1,9 +1,13 @@
1
+ import {validateTileflowBrowserThemeBlendSelection} from '@tileflow/core/browser';
1
2
  import {
2
3
  validateTileflowRuntimeSource,
3
4
  validateTileflowThemeSelection,
4
5
  } from '@tileflow/core/runtime';
5
6
 
6
- /** @typedef {{source?: unknown, theme?: unknown}} TileflowMapStyleInput */
7
+ /**
8
+ * @typedef {{source?: unknown, theme?: unknown, themeBlend?: unknown, themeTransition?: unknown}}
9
+ * TileflowMapStyleInput
10
+ */
7
11
  /** @typedef {{ok: true} | {error: string; ok: false}} TileflowMapStyleInputValidation */
8
12
 
9
13
  /**
@@ -16,9 +20,38 @@ export function validateTileflowMapStyleInputs(input) {
16
20
  if (input.theme !== undefined && !validateTileflowThemeSelection(input.theme)) {
17
21
  return {error: 'theme must be a concrete portable theme name or "system"', ok: false};
18
22
  }
23
+ if (
24
+ input.themeBlend !== undefined &&
25
+ !validateTileflowBrowserThemeBlendSelection(input.themeBlend)
26
+ ) {
27
+ return {
28
+ error:
29
+ 'themeBlend must name two to eight concrete themes, a position from 0 to themes.length - 1, and an optional boolean hold',
30
+ ok: false,
31
+ };
32
+ }
33
+ if (input.themeTransition !== undefined && !isThemeTransition(input.themeTransition)) {
34
+ return {
35
+ error: 'themeTransition.duration must be a number of milliseconds from 0 to 5000',
36
+ ok: false,
37
+ };
38
+ }
19
39
  return {ok: true};
20
40
  }
21
41
 
42
+ /**
43
+ * @param {unknown} value
44
+ * @returns {boolean}
45
+ */
46
+ function isThemeTransition(value) {
47
+ if (!value || typeof value !== 'object') return false;
48
+ const {duration} = /** @type {{duration?: unknown}} */ (value);
49
+ return (
50
+ duration === undefined ||
51
+ (typeof duration === 'number' && Number.isFinite(duration) && duration >= 0 && duration <= 5000)
52
+ );
53
+ }
54
+
22
55
  /** @param {TileflowMapStyleInput} input */
23
56
  export function assertTileflowMapStyleInputs(input) {
24
57
  const validation = validateTileflowMapStyleInputs(input);