@blogmcp/mdx-render-vue 0.2.2 → 0.4.0
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/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +6 -0
- package/dist/composables/useTripMap.d.ts +59 -0
- package/dist/composables/useTripMap.js +41 -0
- package/dist/edit/TripMapEdit.vue.d.ts +3 -0
- package/dist/edit/TripMapEdit.vue.js +50 -0
- package/dist/edit/TripMapEdit.vue2.js +4 -0
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +28 -17
- package/dist/registry.js +7 -0
- package/dist/render/index.d.ts +3 -0
- package/dist/render/index.js +46 -42
- package/dist/render/tripmap/TripMapDefault.vue.d.ts +9 -0
- package/dist/render/tripmap/TripMapDefault.vue.js +197 -0
- package/dist/render/tripmap/TripMapDefault.vue2.js +4 -0
- package/dist/render/tripmap/index.d.ts +5 -0
- package/dist/render/tripmap/index.js +5 -0
- package/dist/useTripMap.d.ts +64 -0
- package/package.json +12 -3
- package/src/composables/index.ts +9 -0
- package/src/composables/useTripMap.ts +107 -0
- package/src/edit/TripMapEdit.vue +31 -0
- package/src/edit/index.ts +2 -0
- package/src/index.ts +3 -0
- package/src/registry.ts +7 -0
- package/src/render/index.ts +4 -0
- package/src/render/tripmap/TripMapDefault.vue +176 -0
- package/src/render/tripmap/index.ts +8 -0
|
@@ -9,3 +9,4 @@ export { useGallery, gridColsClasses, isMediaObject, isVideoUrl } from './useGal
|
|
|
9
9
|
export { useYouTube, YOUTUBE_EMBED_BASE, YOUTUBE_IFRAME_ALLOW, } from './useYouTube';
|
|
10
10
|
export { useAudioPlayer } from './useAudioPlayer';
|
|
11
11
|
export { useOptimizedImg, IMG_COMPONENT_KEY } from './useMedia';
|
|
12
|
+
export { useTripMap, parseHandNodes, tripMapFormatTime, MDX_ARTICLE_CONTEXT_KEY, type MdxArticleContext, type TripMapGeoNode, type TripMapGeoResponse, } from './useTripMap';
|
|
@@ -4,8 +4,11 @@ import { gridColsClasses, isMediaObject, isVideoUrl, useGallery } from "./useGal
|
|
|
4
4
|
import { YOUTUBE_EMBED_BASE, YOUTUBE_IFRAME_ALLOW, useYouTube } from "./useYouTube.js";
|
|
5
5
|
import { useAudioPlayer } from "./useAudioPlayer.js";
|
|
6
6
|
import { IMG_COMPONENT_KEY, useOptimizedImg } from "./useMedia.js";
|
|
7
|
+
import { MDX_ARTICLE_CONTEXT_KEY, useTripMap } from "./useTripMap.js";
|
|
8
|
+
import { parseTripMapHandNodes, tripMapFormatTime } from "@blogmcp/mdx-schema/tripmap";
|
|
7
9
|
export {
|
|
8
10
|
IMG_COMPONENT_KEY,
|
|
11
|
+
MDX_ARTICLE_CONTEXT_KEY,
|
|
9
12
|
YOUTUBE_EMBED_BASE,
|
|
10
13
|
YOUTUBE_IFRAME_ALLOW,
|
|
11
14
|
calloutConfigs,
|
|
@@ -15,10 +18,13 @@ export {
|
|
|
15
18
|
isMediaObject,
|
|
16
19
|
isVideoUrl,
|
|
17
20
|
languageLabels,
|
|
21
|
+
parseTripMapHandNodes as parseHandNodes,
|
|
22
|
+
tripMapFormatTime,
|
|
18
23
|
useAudioPlayer,
|
|
19
24
|
useCallout,
|
|
20
25
|
useCodeBlock,
|
|
21
26
|
useGallery,
|
|
22
27
|
useOptimizedImg,
|
|
28
|
+
useTripMap,
|
|
23
29
|
useYouTube
|
|
24
30
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TripMap composable — Vue binding over the shared framework-free helpers
|
|
3
|
+
* in @blogmcp/mdx-schema/tripmap (prop parsing, geo-URL contract, texts).
|
|
4
|
+
*
|
|
5
|
+
* HARD CONSTRAINT: this module (and every module statically reachable from
|
|
6
|
+
* the TripMap SFCs) must NOT statically import '@blogmcp/tripmap-core' —
|
|
7
|
+
* not even its pure '/nodes' subpath. The article-review MCP-App bundles
|
|
8
|
+
* this package from source into ONE file with that package externalized;
|
|
9
|
+
* a top-level bare import would break the whole bundle at load time. The
|
|
10
|
+
* core is reached exclusively via `import('@blogmcp/tripmap-core')` inside
|
|
11
|
+
* the SFC's intersection handler (rejects harmlessly in the iframe).
|
|
12
|
+
*/
|
|
13
|
+
import { type ComputedRef, type InjectionKey } from 'vue';
|
|
14
|
+
import type { TripMapHandNode, TripMapProps } from '@blogmcp/mdx-schema';
|
|
15
|
+
import { parseTripMapHandNodes, tripMapFormatTime, type TripMapGeoNode, type TripMapGeoResponse, type TripMapUiTexts } from '@blogmcp/mdx-schema/tripmap';
|
|
16
|
+
export { tripMapFormatTime };
|
|
17
|
+
export { parseTripMapHandNodes as parseHandNodes };
|
|
18
|
+
export type { TripMapGeoNode, TripMapGeoResponse };
|
|
19
|
+
/**
|
|
20
|
+
* Article identity for AUTO mode (map every geotagged media of the article).
|
|
21
|
+
* The host app provides it around its MDX render tree (the Nuxt blog does it
|
|
22
|
+
* in `components/article/MdxContent.vue` for article + preview pages; a bare
|
|
23
|
+
* consumer that doesn't provide it simply has no auto mode — TripMap then
|
|
24
|
+
* needs explicit `slugs` or `nodes`).
|
|
25
|
+
*/
|
|
26
|
+
export interface MdxArticleContext {
|
|
27
|
+
filename: string;
|
|
28
|
+
language?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare const MDX_ARTICLE_CONTEXT_KEY: InjectionKey<MdxArticleContext | null>;
|
|
31
|
+
export interface UseTripMapResult {
|
|
32
|
+
slugList: ComputedRef<string[]>;
|
|
33
|
+
excludeList: ComputedRef<string[]>;
|
|
34
|
+
handNodes: ComputedRef<TripMapHandNode[]>;
|
|
35
|
+
/** Deterministic SSR thumbs for explicit-slugs mode (author order). */
|
|
36
|
+
facadeThumbs: ComputedRef<{
|
|
37
|
+
slug: string;
|
|
38
|
+
src: string;
|
|
39
|
+
}[]>;
|
|
40
|
+
heightStyle: ComputedRef<{
|
|
41
|
+
height: string;
|
|
42
|
+
}>;
|
|
43
|
+
articleContext: MdxArticleContext | null;
|
|
44
|
+
isAutoMode: ComputedRef<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* Nothing selects media (no slugs, no article context): the geo call only
|
|
47
|
+
* fetches the tenant basemap config, so its failure must not sink a map
|
|
48
|
+
* of hand-made nodes — the wrapper falls back to the core default style.
|
|
49
|
+
*/
|
|
50
|
+
isConfigOnly: ComputedRef<boolean>;
|
|
51
|
+
/** Always set: slugs / article selection, or the config-only URL. */
|
|
52
|
+
geoUrl: ComputedRef<string>;
|
|
53
|
+
labels: ComputedRef<{
|
|
54
|
+
play: string;
|
|
55
|
+
stop: string;
|
|
56
|
+
} | undefined>;
|
|
57
|
+
texts: ComputedRef<TripMapUiTexts>;
|
|
58
|
+
}
|
|
59
|
+
export declare function useTripMap(props: TripMapProps): UseTripMapResult;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { inject, computed } from "vue";
|
|
2
|
+
import { splitTripMapList, parseTripMapHandNodes, TRIPMAP_DEFAULT_HEIGHT, buildTripMapGeoUrl, tripMapPlaybackLabels, tripMapUiTexts } from "@blogmcp/mdx-schema/tripmap";
|
|
3
|
+
import { parseTripMapHandNodes as parseTripMapHandNodes2, tripMapFormatTime } from "@blogmcp/mdx-schema/tripmap";
|
|
4
|
+
const MDX_ARTICLE_CONTEXT_KEY = /* @__PURE__ */ Symbol.for("blogmcp:mdx-article-context");
|
|
5
|
+
function useTripMap(props) {
|
|
6
|
+
const articleContext = inject(MDX_ARTICLE_CONTEXT_KEY, null);
|
|
7
|
+
const slugList = computed(() => splitTripMapList(props.slugs));
|
|
8
|
+
const excludeList = computed(() => splitTripMapList(props.exclude));
|
|
9
|
+
const handNodes = computed(() => parseTripMapHandNodes(props.nodes));
|
|
10
|
+
const isAutoMode = computed(() => slugList.value.length === 0);
|
|
11
|
+
const isConfigOnly = computed(() => slugList.value.length === 0 && !articleContext?.filename);
|
|
12
|
+
const facadeThumbs = computed(
|
|
13
|
+
() => slugList.value.map((slug) => ({
|
|
14
|
+
slug,
|
|
15
|
+
src: `/api/media/thumb/${encodeURIComponent(slug)}?width=192`
|
|
16
|
+
}))
|
|
17
|
+
);
|
|
18
|
+
const heightStyle = computed(() => ({ height: props.height || TRIPMAP_DEFAULT_HEIGHT }));
|
|
19
|
+
const geoUrl = computed(() => buildTripMapGeoUrl(props, articleContext));
|
|
20
|
+
const labels = computed(() => tripMapPlaybackLabels(articleContext?.language));
|
|
21
|
+
const texts = computed(() => tripMapUiTexts(articleContext?.language));
|
|
22
|
+
return {
|
|
23
|
+
slugList,
|
|
24
|
+
excludeList,
|
|
25
|
+
handNodes,
|
|
26
|
+
facadeThumbs,
|
|
27
|
+
heightStyle,
|
|
28
|
+
articleContext,
|
|
29
|
+
isAutoMode,
|
|
30
|
+
isConfigOnly,
|
|
31
|
+
geoUrl,
|
|
32
|
+
labels,
|
|
33
|
+
texts
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
MDX_ARTICLE_CONTEXT_KEY,
|
|
38
|
+
parseTripMapHandNodes2 as parseHandNodes,
|
|
39
|
+
tripMapFormatTime,
|
|
40
|
+
useTripMap
|
|
41
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { TripMapProps } from '@blogmcp/mdx-schema';
|
|
2
|
+
declare const _default: import("vue").DefineComponent<TripMapProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<TripMapProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineComponent, computed, createElementBlock, openBlock, createElementVNode, toDisplayString, createCommentVNode, Fragment, createTextVNode } from "vue";
|
|
2
|
+
import { parseTripMapHandNodes } from "@blogmcp/mdx-schema/tripmap";
|
|
3
|
+
const _hoisted_1 = {
|
|
4
|
+
class: "flex items-center gap-3 rounded-lg border border-blue-500/40 bg-blue-500/5 px-3 py-2",
|
|
5
|
+
"data-mdx-component": "TripMap"
|
|
6
|
+
};
|
|
7
|
+
const _hoisted_2 = { class: "font-medium" };
|
|
8
|
+
const _hoisted_3 = { class: "text-sm opacity-70" };
|
|
9
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
10
|
+
__name: "TripMapEdit",
|
|
11
|
+
props: {
|
|
12
|
+
slugs: {},
|
|
13
|
+
exclude: {},
|
|
14
|
+
nodes: {},
|
|
15
|
+
height: {},
|
|
16
|
+
line: { type: Boolean },
|
|
17
|
+
filmstrip: { type: Boolean },
|
|
18
|
+
playback: { type: Boolean },
|
|
19
|
+
cluster: { type: Boolean },
|
|
20
|
+
theme: {},
|
|
21
|
+
styleUrl: {},
|
|
22
|
+
center: {},
|
|
23
|
+
zoom: {},
|
|
24
|
+
title: {}
|
|
25
|
+
},
|
|
26
|
+
setup(__props) {
|
|
27
|
+
const props = __props;
|
|
28
|
+
const slugCount = computed(() => (props.slugs ?? "").split(",").filter((s) => s.trim()).length);
|
|
29
|
+
const nodeCount = computed(() => parseTripMapHandNodes(props.nodes).length);
|
|
30
|
+
return (_ctx, _cache) => {
|
|
31
|
+
return openBlock(), createElementBlock("div", _hoisted_1, [
|
|
32
|
+
_cache[0] || (_cache[0] = createElementVNode("span", { "aria-hidden": "true" }, "🗺", -1)),
|
|
33
|
+
createElementVNode("span", _hoisted_2, toDisplayString(__props.title || "TripMap"), 1),
|
|
34
|
+
createElementVNode("span", _hoisted_3, [
|
|
35
|
+
slugCount.value > 0 ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
|
|
36
|
+
createTextVNode(toDisplayString(slugCount.value) + " photos", 1)
|
|
37
|
+
], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
38
|
+
createTextVNode("auto (article media)")
|
|
39
|
+
], 64)),
|
|
40
|
+
nodeCount.value > 0 ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
|
|
41
|
+
createTextVNode(" · " + toDisplayString(nodeCount.value) + " waypoints", 1)
|
|
42
|
+
], 64)) : createCommentVNode("", true)
|
|
43
|
+
])
|
|
44
|
+
]);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
export {
|
|
49
|
+
_sfc_main as default
|
|
50
|
+
};
|
package/dist/edit/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { default as AlertEdit } from './AlertEdit.vue';
|
|
|
16
16
|
export { default as BadgeEdit } from './BadgeEdit.vue';
|
|
17
17
|
export { default as IconEdit } from './IconEdit.vue';
|
|
18
18
|
export { default as TooltipEdit } from './TooltipEdit.vue';
|
|
19
|
+
export { default as TripMapEdit } from './TripMapEdit.vue';
|
|
19
20
|
export { default as Callout } from './CalloutEdit.vue';
|
|
20
21
|
export { default as CodeBlock } from './CodeBlockEdit.vue';
|
|
21
22
|
export { default as Gallery } from './GalleryEdit.vue';
|
|
@@ -27,3 +28,4 @@ export { default as Alert } from './AlertEdit.vue';
|
|
|
27
28
|
export { default as Badge } from './BadgeEdit.vue';
|
|
28
29
|
export { default as Icon } from './IconEdit.vue';
|
|
29
30
|
export { default as Tooltip } from './TooltipEdit.vue';
|
|
31
|
+
export { default as TripMap } from './TripMapEdit.vue';
|
package/dist/edit/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { default as default16, default as default17 } from "./AlertEdit.vue2.js"
|
|
|
9
9
|
import { default as default18, default as default19 } from "./BadgeEdit.vue2.js";
|
|
10
10
|
import { default as default20, default as default21 } from "./IconEdit.vue2.js";
|
|
11
11
|
import { default as default22, default as default23 } from "./TooltipEdit.vue2.js";
|
|
12
|
+
import { default as default24, default as default25 } from "./TripMapEdit.vue.js";
|
|
12
13
|
export {
|
|
13
14
|
default16 as Alert,
|
|
14
15
|
default17 as AlertEdit,
|
|
@@ -30,6 +31,8 @@ export {
|
|
|
30
31
|
default21 as IconEdit,
|
|
31
32
|
default22 as Tooltip,
|
|
32
33
|
default23 as TooltipEdit,
|
|
34
|
+
default24 as TripMap,
|
|
35
|
+
default25 as TripMapEdit,
|
|
33
36
|
default8 as YouTube,
|
|
34
37
|
default9 as YouTubeEdit
|
|
35
38
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* md-editor edit preview (dark theme) and KB blog (light theme, variants).
|
|
7
7
|
*/
|
|
8
8
|
export * from './composables';
|
|
9
|
-
export { CalloutEdit, CodeBlockEdit, GalleryEdit, YouTubeEdit, AudioPlayerEdit, Callout as EditCallout, CodeBlock as EditCodeBlock, Gallery as EditGallery, YouTube as EditYouTube, AudioPlayer as EditAudioPlayer, } from './edit';
|
|
10
|
-
export { Callout as RenderCallout, CalloutDefault, CalloutMinimal, CodeBlock as RenderCodeBlock, CodeBlockDefault, Gallery as RenderGallery, GalleryGrid, YouTube as RenderYouTube, YouTubeDefault, AudioPlayer as RenderAudioPlayer, AudioPlayerDefault, getRenderComponent, getComponentVariants, } from './render';
|
|
9
|
+
export { CalloutEdit, CodeBlockEdit, GalleryEdit, YouTubeEdit, AudioPlayerEdit, TripMapEdit, Callout as EditCallout, CodeBlock as EditCodeBlock, Gallery as EditGallery, YouTube as EditYouTube, AudioPlayer as EditAudioPlayer, } from './edit';
|
|
10
|
+
export { Callout as RenderCallout, CalloutDefault, CalloutMinimal, CodeBlock as RenderCodeBlock, CodeBlockDefault, Gallery as RenderGallery, GalleryGrid, YouTube as RenderYouTube, YouTubeDefault, AudioPlayer as RenderAudioPlayer, AudioPlayerDefault, TripMap as RenderTripMap, TripMapDefault, getRenderComponent, getComponentVariants, } from './render';
|
|
11
11
|
export { Callout, CodeBlock, Gallery, YouTube, AudioPlayer, } from './edit';
|
|
12
12
|
export { componentRegistry, getComponentVariants as getRegistryVariants, getRenderComponentAsync, getEditComponentAsync, getEditorConfig, type ComponentRegistration, type EditorConfig, } from './registry';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { gridColsClasses, isMediaObject, isVideoUrl, useGallery } from "./compos
|
|
|
4
4
|
import { YOUTUBE_EMBED_BASE, YOUTUBE_IFRAME_ALLOW, useYouTube } from "./composables/useYouTube.js";
|
|
5
5
|
import { useAudioPlayer } from "./composables/useAudioPlayer.js";
|
|
6
6
|
import { IMG_COMPONENT_KEY, useOptimizedImg } from "./composables/useMedia.js";
|
|
7
|
+
import { MDX_ARTICLE_CONTEXT_KEY, useTripMap } from "./composables/useTripMap.js";
|
|
7
8
|
import { default as default2, default as default3, default as default4 } from "./edit/CalloutEdit.vue.js";
|
|
8
9
|
import { default as default5, default as default6, default as default7 } from "./edit/CodeBlockEdit.vue.js";
|
|
9
10
|
import { default as default8, default as default9, default as default10 } from "./edit/GalleryEdit.vue.js";
|
|
@@ -14,24 +15,27 @@ import "@blogmcp/mdx-schema/styles";
|
|
|
14
15
|
import "./packages/sanitize/dist/html.js";
|
|
15
16
|
import "./_virtual/index.js";
|
|
16
17
|
import "./node_modules/@iconify/vue/dist/iconify.js";
|
|
18
|
+
import { default as default17 } from "./edit/TripMapEdit.vue.js";
|
|
17
19
|
import { getRenderComponent } from "./render/index.js";
|
|
18
20
|
import { componentRegistry, getComponentVariants, getEditComponentAsync, getEditorConfig, getComponentVariants as getComponentVariants2, getRenderComponentAsync } from "./registry.js";
|
|
19
|
-
import { default as
|
|
20
|
-
import { default as
|
|
21
|
-
import { default as
|
|
22
|
-
import { default as
|
|
23
|
-
import { default as
|
|
24
|
-
import { default as
|
|
21
|
+
import { default as default18, default as default19 } from "./render/callout/CalloutDefault.vue.js";
|
|
22
|
+
import { default as default20 } from "./render/callout/CalloutMinimal.vue.js";
|
|
23
|
+
import { default as default21, default as default22 } from "./render/gallery/GalleryGrid.vue.js";
|
|
24
|
+
import { default as default23, default as default24 } from "./render/youtube/YouTubeDefault.vue.js";
|
|
25
|
+
import { default as default25, default as default26 } from "./render/audioplayer/AudioPlayerDefault.vue.js";
|
|
26
|
+
import { default as default27, default as default28 } from "./render/tripmap/TripMapDefault.vue.js";
|
|
27
|
+
import { default as default29, default as default30 } from "./render/codeblock/CodeBlockDefault.vue.js";
|
|
28
|
+
import { parseTripMapHandNodes, tripMapFormatTime } from "@blogmcp/mdx-schema/tripmap";
|
|
25
29
|
export {
|
|
26
30
|
default14 as AudioPlayer,
|
|
27
|
-
|
|
31
|
+
default25 as AudioPlayerDefault,
|
|
28
32
|
default15 as AudioPlayerEdit,
|
|
29
33
|
default2 as Callout,
|
|
30
|
-
|
|
34
|
+
default18 as CalloutDefault,
|
|
31
35
|
default3 as CalloutEdit,
|
|
32
|
-
|
|
36
|
+
default20 as CalloutMinimal,
|
|
33
37
|
default5 as CodeBlock,
|
|
34
|
-
|
|
38
|
+
default29 as CodeBlockDefault,
|
|
35
39
|
default6 as CodeBlockEdit,
|
|
36
40
|
default16 as EditAudioPlayer,
|
|
37
41
|
default4 as EditCallout,
|
|
@@ -40,17 +44,21 @@ export {
|
|
|
40
44
|
default11 as EditYouTube,
|
|
41
45
|
default9 as Gallery,
|
|
42
46
|
default10 as GalleryEdit,
|
|
43
|
-
|
|
47
|
+
default21 as GalleryGrid,
|
|
44
48
|
IMG_COMPONENT_KEY,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
default22 as
|
|
49
|
+
MDX_ARTICLE_CONTEXT_KEY,
|
|
50
|
+
default26 as RenderAudioPlayer,
|
|
51
|
+
default19 as RenderCallout,
|
|
52
|
+
default30 as RenderCodeBlock,
|
|
53
|
+
default22 as RenderGallery,
|
|
54
|
+
default27 as RenderTripMap,
|
|
55
|
+
default23 as RenderYouTube,
|
|
56
|
+
default28 as TripMapDefault,
|
|
57
|
+
default17 as TripMapEdit,
|
|
50
58
|
YOUTUBE_EMBED_BASE,
|
|
51
59
|
YOUTUBE_IFRAME_ALLOW,
|
|
52
60
|
default12 as YouTube,
|
|
53
|
-
|
|
61
|
+
default24 as YouTubeDefault,
|
|
54
62
|
default13 as YouTubeEdit,
|
|
55
63
|
calloutConfigs,
|
|
56
64
|
calloutIconPaths,
|
|
@@ -66,10 +74,13 @@ export {
|
|
|
66
74
|
isMediaObject,
|
|
67
75
|
isVideoUrl,
|
|
68
76
|
languageLabels,
|
|
77
|
+
parseTripMapHandNodes as parseHandNodes,
|
|
78
|
+
tripMapFormatTime,
|
|
69
79
|
useAudioPlayer,
|
|
70
80
|
useCallout,
|
|
71
81
|
useCodeBlock,
|
|
72
82
|
useGallery,
|
|
73
83
|
useOptimizedImg,
|
|
84
|
+
useTripMap,
|
|
74
85
|
useYouTube
|
|
75
86
|
};
|
package/dist/registry.js
CHANGED
|
@@ -82,6 +82,13 @@ const vueWiring = {
|
|
|
82
82
|
default: () => import("./render/tooltip/Tooltip.vue.js")
|
|
83
83
|
},
|
|
84
84
|
edit: () => import("./edit/TooltipEdit.vue.js")
|
|
85
|
+
},
|
|
86
|
+
TripMap: {
|
|
87
|
+
editor: { icon: "map", color: "#3b82f6", borderColor: "#3b82f6", bgColor: "rgba(59, 130, 246, 0.08)" },
|
|
88
|
+
render: {
|
|
89
|
+
default: () => import("./render/tripmap/TripMapDefault.vue2.js")
|
|
90
|
+
},
|
|
91
|
+
edit: () => import("./edit/TripMapEdit.vue2.js")
|
|
85
92
|
}
|
|
86
93
|
};
|
|
87
94
|
const componentRegistry = Object.fromEntries(
|
package/dist/render/index.d.ts
CHANGED
|
@@ -18,12 +18,14 @@ import { Alert as AlertDefault } from './alert';
|
|
|
18
18
|
import { Badge as BadgeDefault } from './badge';
|
|
19
19
|
import { Icon as IconDefault } from './icon';
|
|
20
20
|
import { Tooltip as TooltipDefault } from './tooltip';
|
|
21
|
+
import { TripMapDefault } from './tripmap';
|
|
21
22
|
export { CalloutDefault, CalloutMinimal };
|
|
22
23
|
export { CodeBlockDefault };
|
|
23
24
|
export { GalleryGrid, GalleryLightbox };
|
|
24
25
|
export { YouTubeDefault };
|
|
25
26
|
export { AudioPlayerDefault };
|
|
26
27
|
export { ButtonDefault, CardDefault, AlertDefault, BadgeDefault, IconDefault, TooltipDefault };
|
|
28
|
+
export { TripMapDefault };
|
|
27
29
|
export { default as Callout } from './callout';
|
|
28
30
|
export { default as CodeBlock } from './codeblock';
|
|
29
31
|
export { default as Gallery } from './gallery';
|
|
@@ -35,6 +37,7 @@ export { default as Alert } from './alert';
|
|
|
35
37
|
export { default as Badge } from './badge';
|
|
36
38
|
export { default as Icon } from './icon';
|
|
37
39
|
export { default as Tooltip } from './tooltip';
|
|
40
|
+
export { default as TripMap } from './tripmap';
|
|
38
41
|
export { getRenderComponentAsync, getComponentVariants, getEditorConfig, componentRegistry, } from '../registry';
|
|
39
42
|
/** @deprecated Use getRenderComponentAsync from registry instead */
|
|
40
43
|
export declare function getRenderComponent(componentName: string, variant?: string): unknown;
|
package/dist/render/index.js
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
import _sfc_main$
|
|
2
|
-
import _sfc_main$
|
|
1
|
+
import _sfc_main$b from "./callout/CalloutDefault.vue.js";
|
|
2
|
+
import _sfc_main$a from "./callout/CalloutMinimal.vue.js";
|
|
3
3
|
import CodeBlockDefault from "./codeblock/CodeBlockDefault.vue.js";
|
|
4
|
-
import _sfc_main$
|
|
4
|
+
import _sfc_main$9 from "./gallery/GalleryGrid.vue.js";
|
|
5
5
|
import { default as default2 } from "./gallery/GalleryLightbox.vue.js";
|
|
6
|
-
import _sfc_main$
|
|
7
|
-
import _sfc_main$
|
|
8
|
-
import _sfc_main$
|
|
9
|
-
import _sfc_main$
|
|
10
|
-
import _sfc_main$
|
|
11
|
-
import _sfc_main$
|
|
12
|
-
import _sfc_main$
|
|
13
|
-
import _sfc_main from "./tooltip/Tooltip.vue2.js";
|
|
6
|
+
import _sfc_main$8 from "./youtube/YouTubeDefault.vue.js";
|
|
7
|
+
import _sfc_main$7 from "./audioplayer/AudioPlayerDefault.vue.js";
|
|
8
|
+
import _sfc_main$6 from "./button/Button.vue2.js";
|
|
9
|
+
import _sfc_main$5 from "./card/Card.vue2.js";
|
|
10
|
+
import _sfc_main$4 from "./alert/Alert.vue2.js";
|
|
11
|
+
import _sfc_main$3 from "./badge/Badge.vue2.js";
|
|
12
|
+
import _sfc_main$2 from "./icon/Icon.vue2.js";
|
|
13
|
+
import _sfc_main$1 from "./tooltip/Tooltip.vue2.js";
|
|
14
|
+
import _sfc_main from "./tripmap/TripMapDefault.vue.js";
|
|
14
15
|
import { componentRegistry, getComponentVariants, getEditorConfig, getRenderComponentAsync } from "../registry.js";
|
|
15
16
|
const syncRegistry = {
|
|
16
|
-
Callout: { default: _sfc_main$
|
|
17
|
+
Callout: { default: _sfc_main$b, minimal: _sfc_main$a },
|
|
17
18
|
CodeBlock: { default: CodeBlockDefault },
|
|
18
|
-
Gallery: { default: _sfc_main$
|
|
19
|
-
YouTube: { default: _sfc_main$
|
|
20
|
-
AudioPlayer: { default: _sfc_main$
|
|
21
|
-
Button: { default: _sfc_main$
|
|
22
|
-
Card: { default: _sfc_main$
|
|
23
|
-
Alert: { default: _sfc_main$
|
|
24
|
-
Badge: { default: _sfc_main$
|
|
25
|
-
Icon: { default: _sfc_main$
|
|
26
|
-
Tooltip: { default: _sfc_main }
|
|
19
|
+
Gallery: { default: _sfc_main$9, grid: _sfc_main$9 },
|
|
20
|
+
YouTube: { default: _sfc_main$8 },
|
|
21
|
+
AudioPlayer: { default: _sfc_main$7 },
|
|
22
|
+
Button: { default: _sfc_main$6 },
|
|
23
|
+
Card: { default: _sfc_main$5 },
|
|
24
|
+
Alert: { default: _sfc_main$4 },
|
|
25
|
+
Badge: { default: _sfc_main$3 },
|
|
26
|
+
Icon: { default: _sfc_main$2 },
|
|
27
|
+
Tooltip: { default: _sfc_main$1 },
|
|
28
|
+
TripMap: { default: _sfc_main }
|
|
27
29
|
};
|
|
28
30
|
function getRenderComponent(componentName, variant = "default") {
|
|
29
31
|
const componentVariants = syncRegistry[componentName];
|
|
@@ -31,30 +33,32 @@ function getRenderComponent(componentName, variant = "default") {
|
|
|
31
33
|
return componentVariants[variant] || componentVariants.default;
|
|
32
34
|
}
|
|
33
35
|
export {
|
|
34
|
-
_sfc_main$
|
|
35
|
-
_sfc_main$
|
|
36
|
-
_sfc_main$
|
|
37
|
-
_sfc_main$
|
|
38
|
-
_sfc_main$
|
|
39
|
-
_sfc_main$
|
|
40
|
-
_sfc_main$
|
|
41
|
-
_sfc_main$
|
|
42
|
-
_sfc_main$
|
|
43
|
-
_sfc_main$
|
|
44
|
-
_sfc_main$
|
|
45
|
-
_sfc_main$
|
|
46
|
-
_sfc_main$
|
|
36
|
+
_sfc_main$4 as Alert,
|
|
37
|
+
_sfc_main$4 as AlertDefault,
|
|
38
|
+
_sfc_main$7 as AudioPlayer,
|
|
39
|
+
_sfc_main$7 as AudioPlayerDefault,
|
|
40
|
+
_sfc_main$3 as Badge,
|
|
41
|
+
_sfc_main$3 as BadgeDefault,
|
|
42
|
+
_sfc_main$6 as Button,
|
|
43
|
+
_sfc_main$6 as ButtonDefault,
|
|
44
|
+
_sfc_main$b as Callout,
|
|
45
|
+
_sfc_main$b as CalloutDefault,
|
|
46
|
+
_sfc_main$a as CalloutMinimal,
|
|
47
|
+
_sfc_main$5 as Card,
|
|
48
|
+
_sfc_main$5 as CardDefault,
|
|
47
49
|
CodeBlockDefault as CodeBlock,
|
|
48
50
|
CodeBlockDefault,
|
|
49
|
-
_sfc_main$
|
|
50
|
-
_sfc_main$
|
|
51
|
+
_sfc_main$9 as Gallery,
|
|
52
|
+
_sfc_main$9 as GalleryGrid,
|
|
51
53
|
default2 as GalleryLightbox,
|
|
52
|
-
_sfc_main$
|
|
53
|
-
_sfc_main$
|
|
54
|
-
_sfc_main as Tooltip,
|
|
55
|
-
_sfc_main as TooltipDefault,
|
|
56
|
-
_sfc_main
|
|
57
|
-
_sfc_main
|
|
54
|
+
_sfc_main$2 as Icon,
|
|
55
|
+
_sfc_main$2 as IconDefault,
|
|
56
|
+
_sfc_main$1 as Tooltip,
|
|
57
|
+
_sfc_main$1 as TooltipDefault,
|
|
58
|
+
_sfc_main as TripMap,
|
|
59
|
+
_sfc_main as TripMapDefault,
|
|
60
|
+
_sfc_main$8 as YouTube,
|
|
61
|
+
_sfc_main$8 as YouTubeDefault,
|
|
58
62
|
componentRegistry,
|
|
59
63
|
getComponentVariants,
|
|
60
64
|
getEditorConfig,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TripMapProps } from '@blogmcp/mdx-schema';
|
|
2
|
+
declare const _default: import("vue").DefineComponent<TripMapProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<TripMapProps> & Readonly<{}>, {
|
|
3
|
+
line: boolean;
|
|
4
|
+
filmstrip: boolean;
|
|
5
|
+
playback: boolean;
|
|
6
|
+
cluster: boolean;
|
|
7
|
+
theme: "light" | "dark" | "auto";
|
|
8
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { defineComponent, ref, onMounted, onBeforeUnmount, createElementBlock, openBlock, normalizeClass, unref, createCommentVNode, createElementVNode, toDisplayString, Fragment, renderList, normalizeStyle } from "vue";
|
|
2
|
+
import { tripMapStyles } from "@blogmcp/mdx-schema/styles";
|
|
3
|
+
import { useTripMap } from "../../composables/useTripMap.js";
|
|
4
|
+
import { tripMapFormatTime } from "@blogmcp/mdx-schema/tripmap";
|
|
5
|
+
const _hoisted_1 = ["aria-label"];
|
|
6
|
+
const _hoisted_2 = ["src"];
|
|
7
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
8
|
+
__name: "TripMapDefault",
|
|
9
|
+
props: {
|
|
10
|
+
slugs: {},
|
|
11
|
+
exclude: {},
|
|
12
|
+
nodes: {},
|
|
13
|
+
height: {},
|
|
14
|
+
line: { type: Boolean, default: true },
|
|
15
|
+
filmstrip: { type: Boolean, default: true },
|
|
16
|
+
playback: { type: Boolean, default: true },
|
|
17
|
+
cluster: { type: Boolean, default: true },
|
|
18
|
+
theme: { default: "auto" },
|
|
19
|
+
styleUrl: {},
|
|
20
|
+
center: {},
|
|
21
|
+
zoom: {},
|
|
22
|
+
title: {}
|
|
23
|
+
},
|
|
24
|
+
setup(__props) {
|
|
25
|
+
const props = __props;
|
|
26
|
+
const { slugList, handNodes, facadeThumbs, heightStyle, geoUrl, isConfigOnly, labels, texts } = useTripMap(props);
|
|
27
|
+
const rootEl = ref(null);
|
|
28
|
+
const mapEl = ref(null);
|
|
29
|
+
const status = ref("facade");
|
|
30
|
+
const skippedCount = ref(0);
|
|
31
|
+
let destroyMap = null;
|
|
32
|
+
let observer = null;
|
|
33
|
+
let started = false;
|
|
34
|
+
async function fetchGeoNodes() {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(geoUrl.value, { headers: { accept: "application/json" } });
|
|
37
|
+
if (!res.ok) throw new Error(`media/geo ${res.status}`);
|
|
38
|
+
return await res.json();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (isConfigOnly.value) return { nodes: [] };
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function start() {
|
|
45
|
+
if (started) return;
|
|
46
|
+
started = true;
|
|
47
|
+
status.value = "loading";
|
|
48
|
+
try {
|
|
49
|
+
const [core, geo] = await Promise.all([import("@blogmcp/tripmap-core"), fetchGeoNodes()]);
|
|
50
|
+
const mediaNodes = (geo.nodes ?? []).filter((n) => typeof n.latitude === "number" && typeof n.longitude === "number").map((n) => ({
|
|
51
|
+
id: n.slug,
|
|
52
|
+
lat: n.latitude,
|
|
53
|
+
lng: n.longitude,
|
|
54
|
+
capturedAt: n.capturedAt ?? void 0,
|
|
55
|
+
thumbnailUrl: n.thumbnailUrl ?? void 0,
|
|
56
|
+
imageUrl: n.url ?? void 0,
|
|
57
|
+
altText: n.altText ?? void 0,
|
|
58
|
+
title: n.placeName ?? void 0
|
|
59
|
+
}));
|
|
60
|
+
const nodes = core.buildTripNodes(mediaNodes, handNodes.value);
|
|
61
|
+
skippedCount.value = geo.skipped ?? 0;
|
|
62
|
+
if (nodes.length === 0 || !mapEl.value) {
|
|
63
|
+
status.value = "empty";
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const map = new core.TripMap(mapEl.value, {
|
|
67
|
+
styleUrl: props.styleUrl || geo.config?.styleUrl || "",
|
|
68
|
+
darkStyleUrl: geo.config?.darkStyleUrl,
|
|
69
|
+
theme: props.theme,
|
|
70
|
+
cluster: props.cluster,
|
|
71
|
+
showLine: props.line,
|
|
72
|
+
showFilmstrip: props.filmstrip,
|
|
73
|
+
showPlayback: props.playback,
|
|
74
|
+
extraAttribution: geo.config?.attribution,
|
|
75
|
+
labels: labels.value,
|
|
76
|
+
initialView: initialViewFromProps(),
|
|
77
|
+
events: {}
|
|
78
|
+
});
|
|
79
|
+
map.setNodes(nodes);
|
|
80
|
+
await map.init();
|
|
81
|
+
destroyMap = () => map.destroy();
|
|
82
|
+
status.value = "live";
|
|
83
|
+
} catch {
|
|
84
|
+
status.value = started && status.value === "loading" ? "facade" : status.value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function initialViewFromProps() {
|
|
88
|
+
if (!props.center) return void 0;
|
|
89
|
+
const [lat, lng] = props.center.split(",").map((v) => Number.parseFloat(v.trim()));
|
|
90
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return void 0;
|
|
91
|
+
return { center: [lng, lat], zoom: props.zoom };
|
|
92
|
+
}
|
|
93
|
+
onMounted(() => {
|
|
94
|
+
const el = rootEl.value;
|
|
95
|
+
if (!el) return;
|
|
96
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
97
|
+
void start();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
observer = new IntersectionObserver(
|
|
101
|
+
(entries) => {
|
|
102
|
+
if (entries.some((e) => e.isIntersecting)) {
|
|
103
|
+
observer?.disconnect();
|
|
104
|
+
observer = null;
|
|
105
|
+
void start();
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{ rootMargin: "300px" }
|
|
109
|
+
);
|
|
110
|
+
observer.observe(el);
|
|
111
|
+
});
|
|
112
|
+
onBeforeUnmount(() => {
|
|
113
|
+
observer?.disconnect();
|
|
114
|
+
destroyMap?.();
|
|
115
|
+
});
|
|
116
|
+
return (_ctx, _cache) => {
|
|
117
|
+
return openBlock(), createElementBlock("div", {
|
|
118
|
+
ref_key: "rootEl",
|
|
119
|
+
ref: rootEl,
|
|
120
|
+
class: normalizeClass(unref(tripMapStyles).container),
|
|
121
|
+
"data-mdx-component": "TripMap",
|
|
122
|
+
"aria-label": __props.title
|
|
123
|
+
}, [
|
|
124
|
+
__props.title ? (openBlock(), createElementBlock("p", {
|
|
125
|
+
key: 0,
|
|
126
|
+
class: normalizeClass(unref(tripMapStyles).head)
|
|
127
|
+
}, toDisplayString(__props.title), 3)) : createCommentVNode("", true),
|
|
128
|
+
unref(facadeThumbs).length && status.value !== "live" ? (openBlock(), createElementBlock("ol", {
|
|
129
|
+
key: 1,
|
|
130
|
+
class: normalizeClass(unref(tripMapStyles).strip)
|
|
131
|
+
}, [
|
|
132
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(facadeThumbs), (thumb) => {
|
|
133
|
+
return openBlock(), createElementBlock("li", {
|
|
134
|
+
key: thumb.slug,
|
|
135
|
+
class: normalizeClass(unref(tripMapStyles).stripItem)
|
|
136
|
+
}, [
|
|
137
|
+
createElementVNode("img", {
|
|
138
|
+
src: thumb.src,
|
|
139
|
+
alt: "",
|
|
140
|
+
width: "80",
|
|
141
|
+
height: "80",
|
|
142
|
+
loading: "lazy",
|
|
143
|
+
decoding: "async",
|
|
144
|
+
class: normalizeClass(unref(tripMapStyles).stripImg)
|
|
145
|
+
}, null, 10, _hoisted_2)
|
|
146
|
+
], 2);
|
|
147
|
+
}), 128))
|
|
148
|
+
], 2)) : createCommentVNode("", true),
|
|
149
|
+
createElementVNode("div", {
|
|
150
|
+
class: normalizeClass(unref(tripMapStyles).mapWrap),
|
|
151
|
+
style: normalizeStyle(unref(heightStyle))
|
|
152
|
+
}, [
|
|
153
|
+
createElementVNode("div", {
|
|
154
|
+
ref_key: "mapEl",
|
|
155
|
+
ref: mapEl,
|
|
156
|
+
class: "absolute inset-0"
|
|
157
|
+
}, null, 512),
|
|
158
|
+
status.value !== "live" ? (openBlock(), createElementBlock("div", {
|
|
159
|
+
key: 0,
|
|
160
|
+
class: normalizeClass([unref(tripMapStyles).mapOverlay])
|
|
161
|
+
}, [
|
|
162
|
+
createElementVNode("div", {
|
|
163
|
+
class: normalizeClass(unref(tripMapStyles).mapBox)
|
|
164
|
+
}, [
|
|
165
|
+
createElementVNode("span", {
|
|
166
|
+
class: normalizeClass(unref(tripMapStyles).mapHint)
|
|
167
|
+
}, toDisplayString(status.value === "empty" ? unref(texts).empty : unref(texts).hint), 3)
|
|
168
|
+
], 2)
|
|
169
|
+
], 2)) : createCommentVNode("", true)
|
|
170
|
+
], 6),
|
|
171
|
+
unref(handNodes).length && status.value !== "live" ? (openBlock(), createElementBlock("ol", {
|
|
172
|
+
key: 2,
|
|
173
|
+
class: normalizeClass(unref(tripMapStyles).stopList)
|
|
174
|
+
}, [
|
|
175
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(handNodes), (node, i) => {
|
|
176
|
+
return openBlock(), createElementBlock("li", {
|
|
177
|
+
key: node.id || i
|
|
178
|
+
}, [
|
|
179
|
+
node.capturedAt ? (openBlock(), createElementBlock("span", {
|
|
180
|
+
key: 0,
|
|
181
|
+
class: normalizeClass(unref(tripMapStyles).stopTime)
|
|
182
|
+
}, toDisplayString(unref(tripMapFormatTime)(node.capturedAt)), 3)) : createCommentVNode("", true),
|
|
183
|
+
createElementVNode("span", null, toDisplayString(node.title || `${node.lat}, ${node.lng}`), 1)
|
|
184
|
+
]);
|
|
185
|
+
}), 128))
|
|
186
|
+
], 2)) : createCommentVNode("", true),
|
|
187
|
+
status.value !== "live" ? (openBlock(), createElementBlock("p", {
|
|
188
|
+
key: 3,
|
|
189
|
+
class: normalizeClass(unref(tripMapStyles).note)
|
|
190
|
+
}, toDisplayString(unref(texts).unavailable), 3)) : createCommentVNode("", true)
|
|
191
|
+
], 10, _hoisted_1);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
export {
|
|
196
|
+
_sfc_main as default
|
|
197
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ComputedRef } from 'vue';
|
|
2
|
+
import { InjectionKey } from 'vue';
|
|
3
|
+
import { parseTripMapHandNodes as parseHandNodes } from '@blogmcp/mdx-schema/tripmap';
|
|
4
|
+
import { tripMapFormatTime } from '@blogmcp/mdx-schema/tripmap';
|
|
5
|
+
import { TripMapGeoNode } from '@blogmcp/mdx-schema/tripmap';
|
|
6
|
+
import { TripMapGeoResponse } from '@blogmcp/mdx-schema/tripmap';
|
|
7
|
+
import { TripMapHandNode } from '@blogmcp/mdx-schema';
|
|
8
|
+
import { TripMapProps } from '@blogmcp/mdx-schema';
|
|
9
|
+
import { TripMapUiTexts } from '@blogmcp/mdx-schema/tripmap';
|
|
10
|
+
|
|
11
|
+
export declare const MDX_ARTICLE_CONTEXT_KEY: InjectionKey<MdxArticleContext | null>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Article identity for AUTO mode (map every geotagged media of the article).
|
|
15
|
+
* The host app provides it around its MDX render tree (the Nuxt blog does it
|
|
16
|
+
* in `components/article/MdxContent.vue` for article + preview pages; a bare
|
|
17
|
+
* consumer that doesn't provide it simply has no auto mode — TripMap then
|
|
18
|
+
* needs explicit `slugs` or `nodes`).
|
|
19
|
+
*/
|
|
20
|
+
export declare interface MdxArticleContext {
|
|
21
|
+
filename: string;
|
|
22
|
+
language?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { parseHandNodes }
|
|
26
|
+
|
|
27
|
+
export { tripMapFormatTime }
|
|
28
|
+
|
|
29
|
+
export { TripMapGeoNode }
|
|
30
|
+
|
|
31
|
+
export { TripMapGeoResponse }
|
|
32
|
+
|
|
33
|
+
export declare function useTripMap(props: TripMapProps): UseTripMapResult;
|
|
34
|
+
|
|
35
|
+
export declare interface UseTripMapResult {
|
|
36
|
+
slugList: ComputedRef<string[]>;
|
|
37
|
+
excludeList: ComputedRef<string[]>;
|
|
38
|
+
handNodes: ComputedRef<TripMapHandNode[]>;
|
|
39
|
+
/** Deterministic SSR thumbs for explicit-slugs mode (author order). */
|
|
40
|
+
facadeThumbs: ComputedRef<{
|
|
41
|
+
slug: string;
|
|
42
|
+
src: string;
|
|
43
|
+
}[]>;
|
|
44
|
+
heightStyle: ComputedRef<{
|
|
45
|
+
height: string;
|
|
46
|
+
}>;
|
|
47
|
+
articleContext: MdxArticleContext | null;
|
|
48
|
+
isAutoMode: ComputedRef<boolean>;
|
|
49
|
+
/**
|
|
50
|
+
* Nothing selects media (no slugs, no article context): the geo call only
|
|
51
|
+
* fetches the tenant basemap config, so its failure must not sink a map
|
|
52
|
+
* of hand-made nodes — the wrapper falls back to the core default style.
|
|
53
|
+
*/
|
|
54
|
+
isConfigOnly: ComputedRef<boolean>;
|
|
55
|
+
/** Always set: slugs / article selection, or the config-only URL. */
|
|
56
|
+
geoUrl: ComputedRef<string>;
|
|
57
|
+
labels: ComputedRef<{
|
|
58
|
+
play: string;
|
|
59
|
+
stop: string;
|
|
60
|
+
} | undefined>;
|
|
61
|
+
texts: ComputedRef<TripMapUiTexts>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blogmcp/mdx-render-vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Vue renderer for MDX components — edit + render SFCs wired to @blogmcp/mdx-schema",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -73,6 +73,14 @@
|
|
|
73
73
|
"./composables/audioplayer": {
|
|
74
74
|
"types": "./src/composables/useAudioPlayer.ts",
|
|
75
75
|
"import": "./dist/composables/useAudioPlayer.js"
|
|
76
|
+
},
|
|
77
|
+
"./render/tripmap": {
|
|
78
|
+
"types": "./src/render/tripmap/index.ts",
|
|
79
|
+
"import": "./dist/render/tripmap/index.js"
|
|
80
|
+
},
|
|
81
|
+
"./composables/tripmap": {
|
|
82
|
+
"types": "./src/composables/useTripMap.ts",
|
|
83
|
+
"import": "./dist/composables/useTripMap.js"
|
|
76
84
|
}
|
|
77
85
|
},
|
|
78
86
|
"files": [
|
|
@@ -90,8 +98,9 @@
|
|
|
90
98
|
"typecheck": "vue-tsc --noEmit"
|
|
91
99
|
},
|
|
92
100
|
"dependencies": {
|
|
93
|
-
"@blogmcp/mdx-schema": "^0.
|
|
94
|
-
"@blogmcp/sanitize": "^0.
|
|
101
|
+
"@blogmcp/mdx-schema": "^0.4.0",
|
|
102
|
+
"@blogmcp/sanitize": "^0.3.0",
|
|
103
|
+
"@blogmcp/tripmap-core": "^0.1.0",
|
|
95
104
|
"@ark-ui/vue": "^5.0.0",
|
|
96
105
|
"@iconify/vue": "^5.0.0"
|
|
97
106
|
},
|
package/src/composables/index.ts
CHANGED
|
@@ -14,3 +14,12 @@ export {
|
|
|
14
14
|
} from './useYouTube'
|
|
15
15
|
export { useAudioPlayer } from './useAudioPlayer'
|
|
16
16
|
export { useOptimizedImg, IMG_COMPONENT_KEY } from './useMedia'
|
|
17
|
+
export {
|
|
18
|
+
useTripMap,
|
|
19
|
+
parseHandNodes,
|
|
20
|
+
tripMapFormatTime,
|
|
21
|
+
MDX_ARTICLE_CONTEXT_KEY,
|
|
22
|
+
type MdxArticleContext,
|
|
23
|
+
type TripMapGeoNode,
|
|
24
|
+
type TripMapGeoResponse,
|
|
25
|
+
} from './useTripMap'
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TripMap composable — Vue binding over the shared framework-free helpers
|
|
3
|
+
* in @blogmcp/mdx-schema/tripmap (prop parsing, geo-URL contract, texts).
|
|
4
|
+
*
|
|
5
|
+
* HARD CONSTRAINT: this module (and every module statically reachable from
|
|
6
|
+
* the TripMap SFCs) must NOT statically import '@blogmcp/tripmap-core' —
|
|
7
|
+
* not even its pure '/nodes' subpath. The article-review MCP-App bundles
|
|
8
|
+
* this package from source into ONE file with that package externalized;
|
|
9
|
+
* a top-level bare import would break the whole bundle at load time. The
|
|
10
|
+
* core is reached exclusively via `import('@blogmcp/tripmap-core')` inside
|
|
11
|
+
* the SFC's intersection handler (rejects harmlessly in the iframe).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { computed, inject, type ComputedRef, type InjectionKey } from 'vue'
|
|
15
|
+
import type { TripMapHandNode, TripMapProps } from '@blogmcp/mdx-schema'
|
|
16
|
+
import {
|
|
17
|
+
buildTripMapGeoUrl,
|
|
18
|
+
parseTripMapHandNodes,
|
|
19
|
+
splitTripMapList,
|
|
20
|
+
TRIPMAP_DEFAULT_HEIGHT,
|
|
21
|
+
tripMapFormatTime,
|
|
22
|
+
tripMapPlaybackLabels,
|
|
23
|
+
tripMapUiTexts,
|
|
24
|
+
type TripMapGeoNode,
|
|
25
|
+
type TripMapGeoResponse,
|
|
26
|
+
type TripMapUiTexts,
|
|
27
|
+
} from '@blogmcp/mdx-schema/tripmap'
|
|
28
|
+
|
|
29
|
+
// Re-export the shared helpers under this package's established names so
|
|
30
|
+
// consumers (md-editor, tests) have one import site on the Vue side.
|
|
31
|
+
export { tripMapFormatTime }
|
|
32
|
+
export { parseTripMapHandNodes as parseHandNodes }
|
|
33
|
+
export type { TripMapGeoNode, TripMapGeoResponse }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Article identity for AUTO mode (map every geotagged media of the article).
|
|
37
|
+
* The host app provides it around its MDX render tree (the Nuxt blog does it
|
|
38
|
+
* in `components/article/MdxContent.vue` for article + preview pages; a bare
|
|
39
|
+
* consumer that doesn't provide it simply has no auto mode — TripMap then
|
|
40
|
+
* needs explicit `slugs` or `nodes`).
|
|
41
|
+
*/
|
|
42
|
+
export interface MdxArticleContext {
|
|
43
|
+
filename: string
|
|
44
|
+
language?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const MDX_ARTICLE_CONTEXT_KEY: InjectionKey<MdxArticleContext | null> =
|
|
48
|
+
Symbol.for('blogmcp:mdx-article-context')
|
|
49
|
+
|
|
50
|
+
export interface UseTripMapResult {
|
|
51
|
+
slugList: ComputedRef<string[]>
|
|
52
|
+
excludeList: ComputedRef<string[]>
|
|
53
|
+
handNodes: ComputedRef<TripMapHandNode[]>
|
|
54
|
+
/** Deterministic SSR thumbs for explicit-slugs mode (author order). */
|
|
55
|
+
facadeThumbs: ComputedRef<{ slug: string; src: string }[]>
|
|
56
|
+
heightStyle: ComputedRef<{ height: string }>
|
|
57
|
+
articleContext: MdxArticleContext | null
|
|
58
|
+
isAutoMode: ComputedRef<boolean>
|
|
59
|
+
/**
|
|
60
|
+
* Nothing selects media (no slugs, no article context): the geo call only
|
|
61
|
+
* fetches the tenant basemap config, so its failure must not sink a map
|
|
62
|
+
* of hand-made nodes — the wrapper falls back to the core default style.
|
|
63
|
+
*/
|
|
64
|
+
isConfigOnly: ComputedRef<boolean>
|
|
65
|
+
/** Always set: slugs / article selection, or the config-only URL. */
|
|
66
|
+
geoUrl: ComputedRef<string>
|
|
67
|
+
labels: ComputedRef<{ play: string; stop: string } | undefined>
|
|
68
|
+
texts: ComputedRef<TripMapUiTexts>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function useTripMap(props: TripMapProps): UseTripMapResult {
|
|
72
|
+
const articleContext = inject(MDX_ARTICLE_CONTEXT_KEY, null)
|
|
73
|
+
|
|
74
|
+
const slugList = computed(() => splitTripMapList(props.slugs))
|
|
75
|
+
const excludeList = computed(() => splitTripMapList(props.exclude))
|
|
76
|
+
const handNodes = computed(() => parseTripMapHandNodes(props.nodes))
|
|
77
|
+
const isAutoMode = computed(() => slugList.value.length === 0)
|
|
78
|
+
const isConfigOnly = computed(() => slugList.value.length === 0 && !articleContext?.filename)
|
|
79
|
+
|
|
80
|
+
const facadeThumbs = computed(() =>
|
|
81
|
+
slugList.value.map((slug) => ({
|
|
82
|
+
slug,
|
|
83
|
+
src: `/api/media/thumb/${encodeURIComponent(slug)}?width=192`,
|
|
84
|
+
})),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const heightStyle = computed(() => ({ height: props.height || TRIPMAP_DEFAULT_HEIGHT }))
|
|
88
|
+
|
|
89
|
+
const geoUrl = computed(() => buildTripMapGeoUrl(props, articleContext))
|
|
90
|
+
|
|
91
|
+
const labels = computed(() => tripMapPlaybackLabels(articleContext?.language))
|
|
92
|
+
const texts = computed(() => tripMapUiTexts(articleContext?.language))
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
slugList,
|
|
96
|
+
excludeList,
|
|
97
|
+
handNodes,
|
|
98
|
+
facadeThumbs,
|
|
99
|
+
heightStyle,
|
|
100
|
+
articleContext,
|
|
101
|
+
isAutoMode,
|
|
102
|
+
isConfigOnly,
|
|
103
|
+
geoUrl,
|
|
104
|
+
labels,
|
|
105
|
+
texts,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TripMap edit placeholder for the md-editor Tiptap NodeView.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately STATIC — no map canvas, no fetch, no maplibre import. The
|
|
6
|
+
* NodeView re-renders on keystroke-adjacent updates; mounting a WebGL map
|
|
7
|
+
* there would thrash. Authors see a compact summary chip row instead and
|
|
8
|
+
* check the real map through the preview panel / published page.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { computed } from 'vue'
|
|
12
|
+
import type { TripMapProps } from '@blogmcp/mdx-schema'
|
|
13
|
+
import { parseHandNodes } from '../composables/useTripMap'
|
|
14
|
+
|
|
15
|
+
const props = defineProps<TripMapProps>()
|
|
16
|
+
|
|
17
|
+
const slugCount = computed(() => (props.slugs ?? '').split(',').filter((s) => s.trim()).length)
|
|
18
|
+
const nodeCount = computed(() => parseHandNodes(props.nodes).length)
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div class="flex items-center gap-3 rounded-lg border border-blue-500/40 bg-blue-500/5 px-3 py-2" data-mdx-component="TripMap">
|
|
23
|
+
<span aria-hidden="true">🗺</span>
|
|
24
|
+
<span class="font-medium">{{ title || 'TripMap' }}</span>
|
|
25
|
+
<span class="text-sm opacity-70">
|
|
26
|
+
<template v-if="slugCount > 0">{{ slugCount }} photos</template>
|
|
27
|
+
<template v-else>auto (article media)</template>
|
|
28
|
+
<template v-if="nodeCount > 0"> · {{ nodeCount }} waypoints</template>
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
package/src/edit/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { default as AlertEdit } from './AlertEdit.vue'
|
|
|
17
17
|
export { default as BadgeEdit } from './BadgeEdit.vue'
|
|
18
18
|
export { default as IconEdit } from './IconEdit.vue'
|
|
19
19
|
export { default as TooltipEdit } from './TooltipEdit.vue'
|
|
20
|
+
export { default as TripMapEdit } from './TripMapEdit.vue'
|
|
20
21
|
|
|
21
22
|
// Re-export as generic names for backward compatibility
|
|
22
23
|
export { default as Callout } from './CalloutEdit.vue'
|
|
@@ -30,3 +31,4 @@ export { default as Alert } from './AlertEdit.vue'
|
|
|
30
31
|
export { default as Badge } from './BadgeEdit.vue'
|
|
31
32
|
export { default as Icon } from './IconEdit.vue'
|
|
32
33
|
export { default as Tooltip } from './TooltipEdit.vue'
|
|
34
|
+
export { default as TripMap } from './TripMapEdit.vue'
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export {
|
|
|
16
16
|
GalleryEdit,
|
|
17
17
|
YouTubeEdit,
|
|
18
18
|
AudioPlayerEdit,
|
|
19
|
+
TripMapEdit,
|
|
19
20
|
// Aliases for backward compatibility
|
|
20
21
|
Callout as EditCallout,
|
|
21
22
|
CodeBlock as EditCodeBlock,
|
|
@@ -37,6 +38,8 @@ export {
|
|
|
37
38
|
YouTubeDefault,
|
|
38
39
|
AudioPlayer as RenderAudioPlayer,
|
|
39
40
|
AudioPlayerDefault,
|
|
41
|
+
TripMap as RenderTripMap,
|
|
42
|
+
TripMapDefault,
|
|
40
43
|
getRenderComponent,
|
|
41
44
|
getComponentVariants,
|
|
42
45
|
} from './render'
|
package/src/registry.ts
CHANGED
|
@@ -142,6 +142,13 @@ const vueWiring: Record<string, VueWiring> = {
|
|
|
142
142
|
},
|
|
143
143
|
edit: () => import('./edit/TooltipEdit.vue'),
|
|
144
144
|
},
|
|
145
|
+
TripMap: {
|
|
146
|
+
editor: { icon: 'map', color: '#3b82f6', borderColor: '#3b82f6', bgColor: 'rgba(59, 130, 246, 0.08)' },
|
|
147
|
+
render: {
|
|
148
|
+
default: () => import('./render/tripmap/TripMapDefault.vue'),
|
|
149
|
+
},
|
|
150
|
+
edit: () => import('./edit/TripMapEdit.vue'),
|
|
151
|
+
},
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
/** Merged registry: schema metadata + Vue wiring, keyed by component name. */
|
package/src/render/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { Alert as AlertDefault } from './alert'
|
|
|
20
20
|
import { Badge as BadgeDefault } from './badge'
|
|
21
21
|
import { Icon as IconDefault } from './icon'
|
|
22
22
|
import { Tooltip as TooltipDefault } from './tooltip'
|
|
23
|
+
import { TripMapDefault } from './tripmap'
|
|
23
24
|
|
|
24
25
|
// Re-export named variants (backward compat)
|
|
25
26
|
export { CalloutDefault, CalloutMinimal }
|
|
@@ -28,6 +29,7 @@ export { GalleryGrid, GalleryLightbox }
|
|
|
28
29
|
export { YouTubeDefault }
|
|
29
30
|
export { AudioPlayerDefault }
|
|
30
31
|
export { ButtonDefault, CardDefault, AlertDefault, BadgeDefault, IconDefault, TooltipDefault }
|
|
32
|
+
export { TripMapDefault }
|
|
31
33
|
|
|
32
34
|
// Default exports for convenience
|
|
33
35
|
export { default as Callout } from './callout'
|
|
@@ -41,6 +43,7 @@ export { default as Alert } from './alert'
|
|
|
41
43
|
export { default as Badge } from './badge'
|
|
42
44
|
export { default as Icon } from './icon'
|
|
43
45
|
export { default as Tooltip } from './tooltip'
|
|
46
|
+
export { default as TripMap } from './tripmap'
|
|
44
47
|
|
|
45
48
|
// Async resolver and helpers from central registry (preferred for new code)
|
|
46
49
|
export {
|
|
@@ -63,6 +66,7 @@ const syncRegistry: Record<string, Record<string, unknown>> = {
|
|
|
63
66
|
Badge: { default: BadgeDefault },
|
|
64
67
|
Icon: { default: IconDefault },
|
|
65
68
|
Tooltip: { default: TooltipDefault },
|
|
69
|
+
TripMap: { default: TripMapDefault },
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
/** @deprecated Use getRenderComponentAsync from registry instead */
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TripMap Default Render Component
|
|
4
|
+
*
|
|
5
|
+
* SSR-safe facade → lazy MapLibre island. The server (and any no-JS
|
|
6
|
+
* surface, incl. the sanitized article-review embed) sees a static strip
|
|
7
|
+
* of photo thumbnails + waypoint list + a placeholder box; when the block
|
|
8
|
+
* scrolls near the viewport we fetch the geo nodes and dynamically import
|
|
9
|
+
* @blogmcp/tripmap-core (the ONLY path to maplibre-gl — see useTripMap's
|
|
10
|
+
* module docblock for why this must never become a static import). If the
|
|
11
|
+
* import or fetch fails, the facade simply stays.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
15
|
+
import { tripMapStyles } from '@blogmcp/mdx-schema/styles'
|
|
16
|
+
import type { TripMapProps } from '@blogmcp/mdx-schema'
|
|
17
|
+
import { tripMapFormatTime, useTripMap, type TripMapGeoResponse } from '../../composables/useTripMap'
|
|
18
|
+
|
|
19
|
+
// withDefaults is load-bearing for the boolean props: Vue boolean-casts an
|
|
20
|
+
// ABSENT `filmstrip?: boolean` prop to false, which would silently flip all
|
|
21
|
+
// the default-true toggles off for every author who doesn't spell them out.
|
|
22
|
+
const props = withDefaults(defineProps<TripMapProps>(), {
|
|
23
|
+
line: true,
|
|
24
|
+
filmstrip: true,
|
|
25
|
+
playback: true,
|
|
26
|
+
cluster: true,
|
|
27
|
+
theme: 'auto',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const { slugList, handNodes, facadeThumbs, heightStyle, geoUrl, isConfigOnly, labels, texts } =
|
|
31
|
+
useTripMap(props)
|
|
32
|
+
|
|
33
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
34
|
+
const mapEl = ref<HTMLElement | null>(null)
|
|
35
|
+
const status = ref<'facade' | 'loading' | 'live' | 'empty'>('facade')
|
|
36
|
+
const skippedCount = ref(0)
|
|
37
|
+
|
|
38
|
+
let destroyMap: (() => void) | null = null
|
|
39
|
+
let observer: IntersectionObserver | null = null
|
|
40
|
+
let started = false
|
|
41
|
+
|
|
42
|
+
async function fetchGeoNodes(): Promise<TripMapGeoResponse> {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(geoUrl.value, { headers: { accept: 'application/json' } })
|
|
45
|
+
if (!res.ok) throw new Error(`media/geo ${res.status}`)
|
|
46
|
+
return (await res.json()) as TripMapGeoResponse
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// Config-only call (hand-made nodes, nothing selected): the map still has
|
|
49
|
+
// its nodes — run on the core default style rather than staying a facade.
|
|
50
|
+
// With a selection the nodes ARE the response, so the failure propagates
|
|
51
|
+
// and the static facade stays (designed degradation).
|
|
52
|
+
if (isConfigOnly.value) return { nodes: [] }
|
|
53
|
+
throw err
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function start(): Promise<void> {
|
|
58
|
+
if (started) return
|
|
59
|
+
started = true
|
|
60
|
+
status.value = 'loading'
|
|
61
|
+
try {
|
|
62
|
+
const [core, geo] = await Promise.all([import('@blogmcp/tripmap-core'), fetchGeoNodes()])
|
|
63
|
+
const mediaNodes = (geo.nodes ?? [])
|
|
64
|
+
.filter((n) => typeof n.latitude === 'number' && typeof n.longitude === 'number')
|
|
65
|
+
.map((n) => ({
|
|
66
|
+
id: n.slug,
|
|
67
|
+
lat: n.latitude,
|
|
68
|
+
lng: n.longitude,
|
|
69
|
+
capturedAt: n.capturedAt ?? undefined,
|
|
70
|
+
thumbnailUrl: n.thumbnailUrl ?? undefined,
|
|
71
|
+
imageUrl: n.url ?? undefined,
|
|
72
|
+
altText: n.altText ?? undefined,
|
|
73
|
+
title: n.placeName ?? undefined,
|
|
74
|
+
}))
|
|
75
|
+
const nodes = core.buildTripNodes(mediaNodes, handNodes.value)
|
|
76
|
+
skippedCount.value = geo.skipped ?? 0
|
|
77
|
+
if (nodes.length === 0 || !mapEl.value) {
|
|
78
|
+
status.value = 'empty'
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
const map = new core.TripMap(mapEl.value, {
|
|
82
|
+
styleUrl: props.styleUrl || geo.config?.styleUrl || '',
|
|
83
|
+
darkStyleUrl: geo.config?.darkStyleUrl,
|
|
84
|
+
theme: props.theme,
|
|
85
|
+
cluster: props.cluster,
|
|
86
|
+
showLine: props.line,
|
|
87
|
+
showFilmstrip: props.filmstrip,
|
|
88
|
+
showPlayback: props.playback,
|
|
89
|
+
extraAttribution: geo.config?.attribution,
|
|
90
|
+
labels: labels.value,
|
|
91
|
+
initialView: initialViewFromProps(),
|
|
92
|
+
events: {},
|
|
93
|
+
})
|
|
94
|
+
map.setNodes(nodes)
|
|
95
|
+
await map.init()
|
|
96
|
+
destroyMap = () => map.destroy()
|
|
97
|
+
status.value = 'live'
|
|
98
|
+
} catch {
|
|
99
|
+
// Core unavailable (review-app iframe, offline) or fetch failed —
|
|
100
|
+
// the static facade stays, which is the designed degradation.
|
|
101
|
+
status.value = started && status.value === 'loading' ? 'facade' : status.value
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function initialViewFromProps(): { center?: [number, number]; zoom?: number } | undefined {
|
|
106
|
+
if (!props.center) return undefined
|
|
107
|
+
const [lat, lng] = props.center.split(',').map((v) => Number.parseFloat(v.trim()))
|
|
108
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return undefined
|
|
109
|
+
return { center: [lng, lat], zoom: props.zoom }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onMounted(() => {
|
|
113
|
+
const el = rootEl.value
|
|
114
|
+
if (!el) return
|
|
115
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
116
|
+
void start()
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
observer = new IntersectionObserver(
|
|
120
|
+
(entries) => {
|
|
121
|
+
if (entries.some((e) => e.isIntersecting)) {
|
|
122
|
+
observer?.disconnect()
|
|
123
|
+
observer = null
|
|
124
|
+
void start()
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{ rootMargin: '300px' },
|
|
128
|
+
)
|
|
129
|
+
observer.observe(el)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
onBeforeUnmount(() => {
|
|
133
|
+
observer?.disconnect()
|
|
134
|
+
destroyMap?.()
|
|
135
|
+
})
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<div ref="rootEl" :class="tripMapStyles.container" data-mdx-component="TripMap" :aria-label="title">
|
|
140
|
+
<p v-if="title" :class="tripMapStyles.head">{{ title }}</p>
|
|
141
|
+
|
|
142
|
+
<!-- Facade thumb strip (explicit-slugs mode); replaced by the live filmstrip. -->
|
|
143
|
+
<ol v-if="facadeThumbs.length && status !== 'live'" :class="tripMapStyles.strip">
|
|
144
|
+
<li v-for="thumb in facadeThumbs" :key="thumb.slug" :class="tripMapStyles.stripItem">
|
|
145
|
+
<img :src="thumb.src" alt="" width="80" height="80" loading="lazy" decoding="async" :class="tripMapStyles.stripImg" />
|
|
146
|
+
</li>
|
|
147
|
+
</ol>
|
|
148
|
+
|
|
149
|
+
<!--
|
|
150
|
+
Sized wrapper: the engine host is a static absolutely-positioned child
|
|
151
|
+
that Vue never re-patches (reactive bindings on the engine's container
|
|
152
|
+
would clobber the classes/children the engine adds); the facade box is
|
|
153
|
+
an overlay that simply unmounts when the map goes live.
|
|
154
|
+
-->
|
|
155
|
+
<div :class="tripMapStyles.mapWrap" :style="heightStyle">
|
|
156
|
+
<div ref="mapEl" class="absolute inset-0"></div>
|
|
157
|
+
<div v-if="status !== 'live'" :class="[tripMapStyles.mapOverlay]">
|
|
158
|
+
<div :class="tripMapStyles.mapBox">
|
|
159
|
+
<span :class="tripMapStyles.mapHint">
|
|
160
|
+
{{ status === 'empty' ? texts.empty : texts.hint }}
|
|
161
|
+
</span>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- Waypoint stop list (hand-made nodes) for the static facade. -->
|
|
167
|
+
<ol v-if="handNodes.length && status !== 'live'" :class="tripMapStyles.stopList">
|
|
168
|
+
<li v-for="(node, i) in handNodes" :key="node.id || i">
|
|
169
|
+
<span v-if="node.capturedAt" :class="tripMapStyles.stopTime">{{ tripMapFormatTime(node.capturedAt) }}</span>
|
|
170
|
+
<span>{{ node.title || `${node.lat}, ${node.lng}` }}</span>
|
|
171
|
+
</li>
|
|
172
|
+
</ol>
|
|
173
|
+
|
|
174
|
+
<p v-if="status !== 'live'" :class="tripMapStyles.note">{{ texts.unavailable }}</p>
|
|
175
|
+
</div>
|
|
176
|
+
</template>
|