@tileflow/vue 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 ADDED
@@ -0,0 +1,31 @@
1
+ # @tileflow/vue
2
+
3
+ Vue component for rendering Tileflow maps with MapLibre.
4
+
5
+ Install `maplibre-gl` alongside the package and import its CSS once in your app.
6
+
7
+ ```vue
8
+ <script setup lang="ts">
9
+ import 'maplibre-gl/dist/maplibre-gl.css';
10
+ import {TileflowMap} from '@tileflow/vue';
11
+ </script>
12
+
13
+ <template>
14
+ <TileflowMap
15
+ map="madrid"
16
+ :center="[-3.7038, 40.4168]"
17
+ :zoom="12"
18
+ :map-options="{
19
+ cooperativeGestures: true,
20
+ maxZoom: 18,
21
+ }"
22
+ />
23
+ </template>
24
+ ```
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
+ Docs: https://tileflow.dev/docs
@@ -0,0 +1,140 @@
1
+ import * as vue from 'vue';
2
+ import { PropType } from 'vue';
3
+ import maplibregl, { MapOptions, Map } from 'maplibre-gl';
4
+ import { TileflowAnalytics, TileflowConfig, MapLibreStyle, TileflowMapMarker, TileflowProjectThemes } from '@tileflow/core';
5
+
6
+ type TileflowMapMode = 'interactive' | 'image';
7
+ type TileflowMapOptions = Omit<MapOptions, 'container' | 'style'>;
8
+ type TileflowMapProps = {
9
+ alt?: string;
10
+ analytics?: TileflowAnalytics;
11
+ center?: [number, number];
12
+ className?: string;
13
+ config?: TileflowConfig;
14
+ height?: number | string;
15
+ imageLoading?: HTMLImageElement['loading'];
16
+ imageUrl?: string;
17
+ interactive?: boolean;
18
+ manifestUrl?: string;
19
+ map?: string;
20
+ mapOptions?: TileflowMapOptions;
21
+ mapStyle?: MapLibreStyle;
22
+ markers?: TileflowMapMarker[];
23
+ mode?: TileflowMapMode;
24
+ preferLocalDev?: boolean;
25
+ styleBaseUrl?: string;
26
+ styleUrl?: string;
27
+ themes?: TileflowProjectThemes;
28
+ tileBaseUrl?: string;
29
+ zoom?: number;
30
+ };
31
+ declare const TileflowMap: vue.DefineComponent<vue.ExtractPropTypes<{
32
+ alt: {
33
+ default: string;
34
+ type: StringConstructor;
35
+ };
36
+ analytics: PropType<TileflowAnalytics>;
37
+ center: PropType<[number, number]>;
38
+ className: StringConstructor;
39
+ config: PropType<TileflowConfig>;
40
+ height: {
41
+ default: number;
42
+ type: PropType<number | string>;
43
+ };
44
+ imageLoading: {
45
+ default: string;
46
+ type: PropType<HTMLImageElement["loading"]>;
47
+ };
48
+ imageUrl: StringConstructor;
49
+ interactive: {
50
+ default: undefined;
51
+ type: PropType<boolean | undefined>;
52
+ };
53
+ manifestUrl: {
54
+ default: string;
55
+ type: StringConstructor;
56
+ };
57
+ map: StringConstructor;
58
+ mapOptions: PropType<TileflowMapOptions>;
59
+ mapStyle: PropType<MapLibreStyle>;
60
+ markers: {
61
+ default: () => never[];
62
+ type: PropType<TileflowMapMarker[]>;
63
+ };
64
+ mode: {
65
+ default: string;
66
+ type: PropType<TileflowMapMode>;
67
+ };
68
+ preferLocalDev: {
69
+ default: boolean;
70
+ type: BooleanConstructor;
71
+ };
72
+ styleBaseUrl: StringConstructor;
73
+ styleUrl: StringConstructor;
74
+ themes: PropType<TileflowProjectThemes>;
75
+ tileBaseUrl: StringConstructor;
76
+ zoom: NumberConstructor;
77
+ }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
78
+ [key: string]: any;
79
+ }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {
80
+ load: (_map: Map) => true;
81
+ }, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
82
+ alt: {
83
+ default: string;
84
+ type: StringConstructor;
85
+ };
86
+ analytics: PropType<TileflowAnalytics>;
87
+ center: PropType<[number, number]>;
88
+ className: StringConstructor;
89
+ config: PropType<TileflowConfig>;
90
+ height: {
91
+ default: number;
92
+ type: PropType<number | string>;
93
+ };
94
+ imageLoading: {
95
+ default: string;
96
+ type: PropType<HTMLImageElement["loading"]>;
97
+ };
98
+ imageUrl: StringConstructor;
99
+ interactive: {
100
+ default: undefined;
101
+ type: PropType<boolean | undefined>;
102
+ };
103
+ manifestUrl: {
104
+ default: string;
105
+ type: StringConstructor;
106
+ };
107
+ map: StringConstructor;
108
+ mapOptions: PropType<TileflowMapOptions>;
109
+ mapStyle: PropType<MapLibreStyle>;
110
+ markers: {
111
+ default: () => never[];
112
+ type: PropType<TileflowMapMarker[]>;
113
+ };
114
+ mode: {
115
+ default: string;
116
+ type: PropType<TileflowMapMode>;
117
+ };
118
+ preferLocalDev: {
119
+ default: boolean;
120
+ type: BooleanConstructor;
121
+ };
122
+ styleBaseUrl: StringConstructor;
123
+ styleUrl: StringConstructor;
124
+ themes: PropType<TileflowProjectThemes>;
125
+ tileBaseUrl: StringConstructor;
126
+ zoom: NumberConstructor;
127
+ }>> & Readonly<{
128
+ onLoad?: ((_map: maplibregl.Map) => any) | undefined;
129
+ }>, {
130
+ interactive: boolean | undefined;
131
+ alt: string;
132
+ height: string | number;
133
+ imageLoading: "eager" | "lazy";
134
+ manifestUrl: string;
135
+ markers: TileflowMapMarker[];
136
+ mode: TileflowMapMode;
137
+ preferLocalDev: boolean;
138
+ }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
139
+
140
+ export { TileflowMap, type TileflowMapMode, type TileflowMapOptions, type TileflowMapProps, TileflowMap as default };
package/dist/index.js ADDED
@@ -0,0 +1,389 @@
1
+ // src/index.ts
2
+ import maplibregl from "maplibre-gl";
3
+ import {
4
+ computed,
5
+ defineComponent,
6
+ h,
7
+ onBeforeUnmount,
8
+ onMounted,
9
+ ref,
10
+ shallowRef,
11
+ watch
12
+ } from "vue";
13
+ import {
14
+ createTileflowSessionId,
15
+ defaultTileflowManifestUrl,
16
+ loadTileflowManifest,
17
+ mergeTileflowAnalytics,
18
+ normalizeTileflowStaticImageSize,
19
+ resolveTileflowAnalyticsRequestUrl,
20
+ resolveTileflowManifestMap,
21
+ resolveTileflowMapMode,
22
+ resolveTileflowRuntimeStyle,
23
+ resolveTileflowStaticImageUrl,
24
+ shouldLoadTileflowManifest,
25
+ startTileflowSession
26
+ } from "@tileflow/core";
27
+ var defaultCenter = [0, 20];
28
+ var defaultZoom = 2;
29
+ var TileflowMap = defineComponent({
30
+ name: "TileflowMap",
31
+ inheritAttrs: false,
32
+ props: {
33
+ alt: {
34
+ default: "",
35
+ type: String
36
+ },
37
+ analytics: Object,
38
+ center: Array,
39
+ className: String,
40
+ config: Object,
41
+ height: {
42
+ default: 420,
43
+ type: [Number, String]
44
+ },
45
+ imageLoading: {
46
+ default: "eager",
47
+ type: String
48
+ },
49
+ imageUrl: String,
50
+ interactive: {
51
+ default: void 0,
52
+ type: Boolean
53
+ },
54
+ manifestUrl: {
55
+ default: defaultTileflowManifestUrl,
56
+ type: String
57
+ },
58
+ map: String,
59
+ mapOptions: Object,
60
+ mapStyle: Object,
61
+ markers: {
62
+ default: () => [],
63
+ type: Array
64
+ },
65
+ mode: {
66
+ default: "interactive",
67
+ type: String
68
+ },
69
+ preferLocalDev: {
70
+ default: true,
71
+ type: Boolean
72
+ },
73
+ styleBaseUrl: String,
74
+ styleUrl: String,
75
+ themes: Object,
76
+ tileBaseUrl: String,
77
+ zoom: Number
78
+ },
79
+ emits: {
80
+ load: (_map) => true
81
+ },
82
+ setup(props, { attrs, emit }) {
83
+ const containerRef = ref(null);
84
+ const manifestMap = shallowRef(null);
85
+ const imageSize = shallowRef(null);
86
+ const mapRef = shallowRef(null);
87
+ const sessionId = createTileflowSessionId();
88
+ const markerInstances = [];
89
+ const sessionStarts = /* @__PURE__ */ new Set();
90
+ let imageResizeObserver = null;
91
+ let mapResizeObserver = null;
92
+ let manifestLoadId = 0;
93
+ const resolvedMode = computed(
94
+ () => resolveTileflowMapMode({
95
+ imageUrl: props.imageUrl,
96
+ mode: props.mode,
97
+ preferLocalDev: props.preferLocalDev
98
+ })
99
+ );
100
+ const isImageMode = computed(() => resolvedMode.value === "image");
101
+ const imageCenter = computed(() => props.center ?? defaultCenter);
102
+ const imageZoom = computed(() => props.zoom ?? defaultZoom);
103
+ const resolvedCenter = computed(
104
+ () => props.center ?? props.mapOptions?.center ?? defaultCenter
105
+ );
106
+ const resolvedZoom = computed(() => props.zoom ?? props.mapOptions?.zoom ?? defaultZoom);
107
+ const resolvedInteractive = computed(
108
+ () => props.interactive ?? props.mapOptions?.interactive ?? true
109
+ );
110
+ const runtimeStyle = computed(
111
+ () => isImageMode.value ? null : resolveTileflowRuntimeStyle({
112
+ config: props.config,
113
+ manifestMap: manifestMap.value,
114
+ map: props.map,
115
+ preferLocalDev: props.preferLocalDev,
116
+ style: props.mapStyle,
117
+ styleBaseUrl: props.styleBaseUrl,
118
+ styleUrl: props.styleUrl,
119
+ themes: props.themes,
120
+ tileBaseUrl: props.tileBaseUrl
121
+ })
122
+ );
123
+ const resolvedAnalytics = computed(
124
+ () => mergeTileflowAnalytics(props.analytics, runtimeStyle.value?.analytics)
125
+ );
126
+ const runtimeImageUrl = computed(
127
+ () => props.imageUrl ?? (isImageMode.value ? resolveTileflowStaticImageUrl({
128
+ center: imageCenter.value,
129
+ imageSize: imageSize.value,
130
+ manifestMap: manifestMap.value,
131
+ zoom: imageZoom.value
132
+ }) : void 0)
133
+ );
134
+ const frameStyle = computed(() => ({
135
+ height: formatHeight(props.height),
136
+ minHeight: "240px",
137
+ overflow: "hidden",
138
+ position: "relative",
139
+ width: "100%"
140
+ }));
141
+ const refreshManifest = async () => {
142
+ const shouldLoad = shouldLoadTileflowManifest({
143
+ config: props.config,
144
+ imageMode: isImageMode.value,
145
+ imageUrl: props.imageUrl,
146
+ map: props.map,
147
+ style: props.mapStyle,
148
+ styleBaseUrl: props.styleBaseUrl,
149
+ styleUrl: props.styleUrl
150
+ });
151
+ if (!shouldLoad) {
152
+ manifestMap.value = null;
153
+ return;
154
+ }
155
+ const loadId = ++manifestLoadId;
156
+ manifestMap.value = null;
157
+ try {
158
+ const manifest = await loadTileflowManifest(props.manifestUrl);
159
+ if (loadId === manifestLoadId) {
160
+ manifestMap.value = manifest && props.map ? resolveTileflowManifestMap(manifest, props.map) : null;
161
+ }
162
+ } catch (error) {
163
+ if (loadId === manifestLoadId) {
164
+ console.error("Failed to load Tileflow manifest", error);
165
+ manifestMap.value = null;
166
+ }
167
+ }
168
+ };
169
+ const updateImageResizeObserver = () => {
170
+ imageResizeObserver?.disconnect();
171
+ imageResizeObserver = null;
172
+ imageSize.value = null;
173
+ if (!isImageMode.value || props.imageUrl || !containerRef.value) {
174
+ return;
175
+ }
176
+ const element = containerRef.value;
177
+ const updateImageSize = () => {
178
+ const nextSize = normalizeTileflowStaticImageSize({
179
+ height: element.clientHeight,
180
+ width: element.clientWidth
181
+ });
182
+ imageSize.value = imageSize.value?.height === nextSize.height && imageSize.value.width === nextSize.width ? imageSize.value : nextSize;
183
+ };
184
+ imageResizeObserver = new ResizeObserver(updateImageSize);
185
+ imageResizeObserver.observe(element);
186
+ updateImageSize();
187
+ };
188
+ const destroyMap = () => {
189
+ mapResizeObserver?.disconnect();
190
+ mapResizeObserver = null;
191
+ clearMarkers(markerInstances);
192
+ mapRef.value?.remove();
193
+ mapRef.value = null;
194
+ };
195
+ const recreateMap = () => {
196
+ destroyMap();
197
+ if (!containerRef.value || !runtimeStyle.value || isImageMode.value) {
198
+ return;
199
+ }
200
+ const runtime = runtimeStyle.value;
201
+ const map = new maplibregl.Map({
202
+ ...props.mapOptions,
203
+ attributionControl: props.mapOptions?.attributionControl ?? { compact: true },
204
+ center: resolvedCenter.value,
205
+ container: containerRef.value,
206
+ interactive: resolvedInteractive.value,
207
+ style: runtime.style,
208
+ transformRequest: createTransformRequest(
209
+ resolvedAnalytics.value,
210
+ sessionId,
211
+ props.mapOptions?.transformRequest
212
+ ),
213
+ zoom: resolvedZoom.value
214
+ });
215
+ mapRef.value = map;
216
+ if (resolvedInteractive.value) {
217
+ map.addControl(new maplibregl.NavigationControl(), "top-right");
218
+ }
219
+ mapResizeObserver = new ResizeObserver(() => {
220
+ map.resize();
221
+ });
222
+ mapResizeObserver.observe(containerRef.value);
223
+ map.on("load", () => {
224
+ const analyticsForLoad = resolvedAnalytics.value;
225
+ const styleId = analyticsForLoad?.styleId ?? (typeof runtime.style === "string" ? runtime.style : props.map);
226
+ emit("load", map);
227
+ if (analyticsForLoad?.mapId) {
228
+ const sessionKey = `${sessionId}:${analyticsForLoad.mapId}:${styleId ?? ""}`;
229
+ if (!sessionStarts.has(sessionKey)) {
230
+ sessionStarts.add(sessionKey);
231
+ startTileflowSession(analyticsForLoad, {
232
+ mapId: analyticsForLoad.mapId,
233
+ sessionId,
234
+ source: "vue",
235
+ styleId
236
+ });
237
+ }
238
+ }
239
+ });
240
+ syncMarkers();
241
+ };
242
+ const syncView = () => {
243
+ mapRef.value?.jumpTo({
244
+ center: resolvedCenter.value,
245
+ zoom: resolvedZoom.value
246
+ });
247
+ };
248
+ const syncMarkers = () => {
249
+ const map = mapRef.value;
250
+ clearMarkers(markerInstances);
251
+ if (!map) {
252
+ return;
253
+ }
254
+ for (const marker of props.markers) {
255
+ const markerInstance = new maplibregl.Marker({
256
+ color: marker.color ?? "#C6A15B"
257
+ }).setLngLat(marker.coordinates).addTo(map);
258
+ markerInstance.getElement().title = marker.label ?? marker.id;
259
+ markerInstances.push(markerInstance);
260
+ }
261
+ };
262
+ onMounted(() => {
263
+ void refreshManifest();
264
+ updateImageResizeObserver();
265
+ recreateMap();
266
+ });
267
+ onBeforeUnmount(() => {
268
+ imageResizeObserver?.disconnect();
269
+ destroyMap();
270
+ });
271
+ watch(
272
+ () => [
273
+ props.config,
274
+ props.imageUrl,
275
+ props.manifestUrl,
276
+ props.map,
277
+ props.mapStyle,
278
+ props.mode,
279
+ props.preferLocalDev,
280
+ props.styleBaseUrl,
281
+ props.styleUrl
282
+ ],
283
+ () => {
284
+ void refreshManifest();
285
+ }
286
+ );
287
+ watch(
288
+ () => [props.imageUrl, props.mode, props.preferLocalDev],
289
+ () => updateImageResizeObserver(),
290
+ { flush: "post" }
291
+ );
292
+ watch(
293
+ () => [
294
+ props.config,
295
+ props.analytics,
296
+ props.interactive,
297
+ props.map,
298
+ props.mapOptions,
299
+ props.mapStyle,
300
+ props.mode,
301
+ props.preferLocalDev,
302
+ props.styleBaseUrl,
303
+ props.styleUrl,
304
+ props.themes,
305
+ props.tileBaseUrl,
306
+ manifestMap.value
307
+ ],
308
+ () => recreateMap(),
309
+ { flush: "post" }
310
+ );
311
+ watch(
312
+ () => [props.center, props.zoom, props.mapOptions?.center, props.mapOptions?.zoom],
313
+ () => syncView(),
314
+ { flush: "post" }
315
+ );
316
+ watch(
317
+ () => props.markers,
318
+ () => syncMarkers(),
319
+ { flush: "post" }
320
+ );
321
+ return () => {
322
+ const rootAttrs = { ...attrs };
323
+ const classValue = [attrs.class, props.className];
324
+ const styleValue = [attrs.style, frameStyle.value];
325
+ delete rootAttrs.class;
326
+ delete rootAttrs.style;
327
+ return h(
328
+ "div",
329
+ {
330
+ ...rootAttrs,
331
+ class: classValue,
332
+ ref: containerRef,
333
+ style: styleValue
334
+ },
335
+ isImageMode.value && runtimeImageUrl.value ? h("img", {
336
+ alt: props.alt,
337
+ decoding: "async",
338
+ loading: props.imageLoading,
339
+ src: runtimeImageUrl.value,
340
+ style: {
341
+ display: "block",
342
+ height: "100%",
343
+ objectFit: "cover",
344
+ width: "100%"
345
+ }
346
+ }) : void 0
347
+ );
348
+ };
349
+ }
350
+ });
351
+ var index_default = TileflowMap;
352
+ function createTransformRequest(analytics, sessionId, userTransformRequest) {
353
+ if ((!analytics || analytics.enabled === false) && !userTransformRequest) {
354
+ return void 0;
355
+ }
356
+ return (url, resourceType) => {
357
+ const request = userTransformRequest?.(url, resourceType);
358
+ if (isPromiseLike(request)) {
359
+ return request.then(
360
+ (resolvedRequest) => applyTileflowAnalyticsRequest(url, resolvedRequest, analytics, sessionId) ?? resolvedRequest
361
+ );
362
+ }
363
+ return applyTileflowAnalyticsRequest(url, request, analytics, sessionId);
364
+ };
365
+ }
366
+ function applyTileflowAnalyticsRequest(url, request, analytics, sessionId) {
367
+ const requestUrl = request?.url ?? url;
368
+ const nextUrl = resolveTileflowAnalyticsRequestUrl(requestUrl, analytics, sessionId);
369
+ if (!nextUrl) {
370
+ return request;
371
+ }
372
+ return request ? { ...request, url: nextUrl } : { url: nextUrl };
373
+ }
374
+ function isPromiseLike(value) {
375
+ return Boolean(value && typeof value.then === "function");
376
+ }
377
+ function formatHeight(height) {
378
+ return typeof height === "number" ? `${height}px` : height;
379
+ }
380
+ function clearMarkers(markerInstances) {
381
+ for (const marker of markerInstances) {
382
+ marker.remove();
383
+ }
384
+ markerInstances.splice(0);
385
+ }
386
+ export {
387
+ TileflowMap,
388
+ index_default as default
389
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@tileflow/vue",
3
+ "version": "0.1.0-alpha.10",
4
+ "description": "Vue component for rendering Tileflow maps with MapLibre.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "homepage": "https://tileflow.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/tileflow/tileflow.git",
11
+ "directory": "packages/vue"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/tileflow/tileflow/issues"
15
+ },
16
+ "keywords": [
17
+ "tileflow",
18
+ "vue",
19
+ "maps",
20
+ "maplibre",
21
+ "tiles"
22
+ ],
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "default": "./dist/index.js"
33
+ }
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@tileflow/core": "0.1.0-alpha.10"
40
+ },
41
+ "peerDependencies": {
42
+ "maplibre-gl": ">=5",
43
+ "vue": ">=3.3"
44
+ },
45
+ "devDependencies": {
46
+ "maplibre-gl": "^5.24.0",
47
+ "vue": "^3.5.26"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup src/index.ts --format esm --dts --clean --external vue --external maplibre-gl",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit",
52
+ "lint": "tsc -p tsconfig.json --noEmit"
53
+ }
54
+ }