@blogmcp/mdx-render-vue 0.3.0 โ 0.5.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/LICENSE +21 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +6 -0
- package/dist/composables/useTripMap.d.ts +67 -0
- package/dist/composables/useTripMap.js +39 -0
- package/dist/edit/TripMapEdit.vue.d.ts +3 -0
- package/dist/edit/TripMapEdit.vue.js +51 -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 +10 -0
- package/dist/render/tripmap/TripMapDefault.vue.js +209 -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 +76 -0
- package/package.json +11 -2
- package/src/composables/index.ts +9 -0
- package/src/composables/useTripMap.ts +124 -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 +189 -0
- package/src/render/tripmap/index.ts +8 -0
|
@@ -0,0 +1,209 @@
|
|
|
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", "alt"];
|
|
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
|
+
detail: { default: "popup" },
|
|
20
|
+
styleUrl: {},
|
|
21
|
+
center: {},
|
|
22
|
+
zoom: {},
|
|
23
|
+
title: {}
|
|
24
|
+
},
|
|
25
|
+
setup(__props) {
|
|
26
|
+
const props = __props;
|
|
27
|
+
const { slugList, handNodes, facadeThumbs, facadeStops, heightStyle, geoUrl, isConfigOnly, labels, texts } = useTripMap(props);
|
|
28
|
+
const rootEl = ref(null);
|
|
29
|
+
const mapEl = ref(null);
|
|
30
|
+
const detailEl = ref(null);
|
|
31
|
+
const status = ref("facade");
|
|
32
|
+
const skippedCount = ref(0);
|
|
33
|
+
let destroyMap = null;
|
|
34
|
+
let observer = null;
|
|
35
|
+
let started = false;
|
|
36
|
+
async function fetchGeoNodes() {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(geoUrl.value, { headers: { accept: "application/json" } });
|
|
39
|
+
if (!res.ok) throw new Error(`media/geo ${res.status}`);
|
|
40
|
+
return await res.json();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (isConfigOnly.value) return { nodes: [] };
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function start() {
|
|
47
|
+
if (started) return;
|
|
48
|
+
started = true;
|
|
49
|
+
status.value = "loading";
|
|
50
|
+
try {
|
|
51
|
+
const [core, geo] = await Promise.all([import("@blogmcp/tripmap-core"), fetchGeoNodes()]);
|
|
52
|
+
const mediaNodes = (geo.nodes ?? []).filter((n) => typeof n.latitude === "number" && typeof n.longitude === "number").map((n) => ({
|
|
53
|
+
id: n.slug,
|
|
54
|
+
lat: n.latitude,
|
|
55
|
+
lng: n.longitude,
|
|
56
|
+
capturedAt: n.capturedAt ?? void 0,
|
|
57
|
+
thumbnailUrl: n.thumbnailUrl ?? void 0,
|
|
58
|
+
imageUrl: n.url ?? void 0,
|
|
59
|
+
largeImageUrl: n.largeUrl ?? void 0,
|
|
60
|
+
altText: n.altText ?? void 0,
|
|
61
|
+
title: n.placeName ?? void 0
|
|
62
|
+
}));
|
|
63
|
+
const nodes = core.buildTripNodes(mediaNodes, handNodes.value);
|
|
64
|
+
skippedCount.value = geo.skipped ?? 0;
|
|
65
|
+
if (nodes.length === 0 || !mapEl.value) {
|
|
66
|
+
status.value = "empty";
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const map = new core.TripMap(mapEl.value, {
|
|
70
|
+
styleUrl: props.styleUrl || geo.config?.styleUrl || "",
|
|
71
|
+
darkStyleUrl: geo.config?.darkStyleUrl,
|
|
72
|
+
theme: props.theme,
|
|
73
|
+
cluster: props.cluster,
|
|
74
|
+
showLine: props.line,
|
|
75
|
+
showFilmstrip: props.filmstrip,
|
|
76
|
+
showPlayback: props.playback,
|
|
77
|
+
title: props.title,
|
|
78
|
+
detail: props.detail,
|
|
79
|
+
detailHost: props.detail === "panel" ? detailEl.value ?? void 0 : void 0,
|
|
80
|
+
extraAttribution: geo.config?.attribution,
|
|
81
|
+
labels: labels.value,
|
|
82
|
+
initialView: initialViewFromProps(),
|
|
83
|
+
events: {}
|
|
84
|
+
});
|
|
85
|
+
map.setNodes(nodes);
|
|
86
|
+
await map.init();
|
|
87
|
+
destroyMap = () => map.destroy();
|
|
88
|
+
status.value = "live";
|
|
89
|
+
} catch {
|
|
90
|
+
status.value = started && status.value === "loading" ? "facade" : status.value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function initialViewFromProps() {
|
|
94
|
+
if (!props.center) return void 0;
|
|
95
|
+
const [lat, lng] = props.center.split(",").map((v) => Number.parseFloat(v.trim()));
|
|
96
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return void 0;
|
|
97
|
+
return { center: [lng, lat], zoom: props.zoom };
|
|
98
|
+
}
|
|
99
|
+
onMounted(() => {
|
|
100
|
+
const el = rootEl.value;
|
|
101
|
+
if (!el) return;
|
|
102
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
103
|
+
void start();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
observer = new IntersectionObserver(
|
|
107
|
+
(entries) => {
|
|
108
|
+
if (entries.some((e) => e.isIntersecting)) {
|
|
109
|
+
observer?.disconnect();
|
|
110
|
+
observer = null;
|
|
111
|
+
void start();
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{ rootMargin: "300px" }
|
|
115
|
+
);
|
|
116
|
+
observer.observe(el);
|
|
117
|
+
});
|
|
118
|
+
onBeforeUnmount(() => {
|
|
119
|
+
observer?.disconnect();
|
|
120
|
+
destroyMap?.();
|
|
121
|
+
});
|
|
122
|
+
return (_ctx, _cache) => {
|
|
123
|
+
return openBlock(), createElementBlock("div", {
|
|
124
|
+
ref_key: "rootEl",
|
|
125
|
+
ref: rootEl,
|
|
126
|
+
class: normalizeClass(unref(tripMapStyles).container),
|
|
127
|
+
"data-mdx-component": "TripMap",
|
|
128
|
+
"aria-label": __props.title
|
|
129
|
+
}, [
|
|
130
|
+
__props.title ? (openBlock(), createElementBlock("p", {
|
|
131
|
+
key: 0,
|
|
132
|
+
class: normalizeClass(unref(tripMapStyles).head)
|
|
133
|
+
}, toDisplayString(__props.title), 3)) : createCommentVNode("", true),
|
|
134
|
+
unref(facadeThumbs).length && status.value !== "live" ? (openBlock(), createElementBlock("ol", {
|
|
135
|
+
key: 1,
|
|
136
|
+
class: normalizeClass(unref(tripMapStyles).strip)
|
|
137
|
+
}, [
|
|
138
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(facadeThumbs), (thumb) => {
|
|
139
|
+
return openBlock(), createElementBlock("li", {
|
|
140
|
+
key: thumb.slug,
|
|
141
|
+
class: normalizeClass(unref(tripMapStyles).stripItem)
|
|
142
|
+
}, [
|
|
143
|
+
createElementVNode("img", {
|
|
144
|
+
src: thumb.src,
|
|
145
|
+
alt: thumb.alt,
|
|
146
|
+
width: "80",
|
|
147
|
+
height: "80",
|
|
148
|
+
loading: "lazy",
|
|
149
|
+
decoding: "async",
|
|
150
|
+
class: normalizeClass(unref(tripMapStyles).stripImg)
|
|
151
|
+
}, null, 10, _hoisted_2)
|
|
152
|
+
], 2);
|
|
153
|
+
}), 128))
|
|
154
|
+
], 2)) : createCommentVNode("", true),
|
|
155
|
+
createElementVNode("div", {
|
|
156
|
+
class: normalizeClass(unref(tripMapStyles).mapWrap),
|
|
157
|
+
style: normalizeStyle(unref(heightStyle))
|
|
158
|
+
}, [
|
|
159
|
+
createElementVNode("div", {
|
|
160
|
+
ref_key: "mapEl",
|
|
161
|
+
ref: mapEl,
|
|
162
|
+
class: "absolute inset-0"
|
|
163
|
+
}, null, 512),
|
|
164
|
+
status.value !== "live" ? (openBlock(), createElementBlock("div", {
|
|
165
|
+
key: 0,
|
|
166
|
+
class: normalizeClass([unref(tripMapStyles).mapOverlay])
|
|
167
|
+
}, [
|
|
168
|
+
createElementVNode("div", {
|
|
169
|
+
class: normalizeClass(unref(tripMapStyles).mapBox)
|
|
170
|
+
}, [
|
|
171
|
+
createElementVNode("span", {
|
|
172
|
+
class: normalizeClass(unref(tripMapStyles).mapHint)
|
|
173
|
+
}, toDisplayString(status.value === "empty" ? unref(texts).empty : unref(texts).hint), 3)
|
|
174
|
+
], 2)
|
|
175
|
+
], 2)) : createCommentVNode("", true)
|
|
176
|
+
], 6),
|
|
177
|
+
props.detail === "panel" ? (openBlock(), createElementBlock("div", {
|
|
178
|
+
key: 2,
|
|
179
|
+
ref_key: "detailEl",
|
|
180
|
+
ref: detailEl,
|
|
181
|
+
class: normalizeClass(unref(tripMapStyles).detailHost)
|
|
182
|
+
}, null, 2)) : createCommentVNode("", true),
|
|
183
|
+
unref(facadeStops).length && status.value !== "live" ? (openBlock(), createElementBlock("ol", {
|
|
184
|
+
key: 3,
|
|
185
|
+
class: normalizeClass(unref(tripMapStyles).stopList)
|
|
186
|
+
}, [
|
|
187
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(facadeStops), (stop) => {
|
|
188
|
+
return openBlock(), createElementBlock("li", {
|
|
189
|
+
key: stop.key
|
|
190
|
+
}, [
|
|
191
|
+
stop.capturedAt ? (openBlock(), createElementBlock("span", {
|
|
192
|
+
key: 0,
|
|
193
|
+
class: normalizeClass(unref(tripMapStyles).stopTime)
|
|
194
|
+
}, toDisplayString(unref(tripMapFormatTime)(stop.capturedAt)), 3)) : createCommentVNode("", true),
|
|
195
|
+
createElementVNode("span", null, toDisplayString(stop.title), 1)
|
|
196
|
+
]);
|
|
197
|
+
}), 128))
|
|
198
|
+
], 2)) : createCommentVNode("", true),
|
|
199
|
+
status.value !== "live" ? (openBlock(), createElementBlock("p", {
|
|
200
|
+
key: 4,
|
|
201
|
+
class: normalizeClass(unref(tripMapStyles).note)
|
|
202
|
+
}, toDisplayString(unref(texts).unavailable), 3)) : createCommentVNode("", true)
|
|
203
|
+
], 10, _hoisted_1);
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
export {
|
|
208
|
+
_sfc_main as default
|
|
209
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ComputedRef } from 'vue';
|
|
2
|
+
import { InjectionKey } from 'vue';
|
|
3
|
+
import { parseTripMapHandNodes as parseHandNodes } from '@blogmcp/mdx-schema/tripmap';
|
|
4
|
+
import { TripMapFacadeMedia } from '@blogmcp/mdx-schema/tripmap';
|
|
5
|
+
import { TripMapFacadeStop } from '@blogmcp/mdx-schema/tripmap';
|
|
6
|
+
import { TripMapFacadeThumb } from '@blogmcp/mdx-schema/tripmap';
|
|
7
|
+
import { tripMapFormatTime } from '@blogmcp/mdx-schema/tripmap';
|
|
8
|
+
import { TripMapGeoNode } from '@blogmcp/mdx-schema/tripmap';
|
|
9
|
+
import { TripMapGeoResponse } from '@blogmcp/mdx-schema/tripmap';
|
|
10
|
+
import { TripMapHandNode } from '@blogmcp/mdx-schema';
|
|
11
|
+
import { TripMapProps } from '@blogmcp/mdx-schema';
|
|
12
|
+
import { TripMapUiLabels } from '@blogmcp/mdx-schema/tripmap';
|
|
13
|
+
import { TripMapUiTexts } from '@blogmcp/mdx-schema/tripmap';
|
|
14
|
+
|
|
15
|
+
export declare const MDX_ARTICLE_CONTEXT_KEY: InjectionKey<MdxArticleContext | null>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Article identity for AUTO mode (map every geotagged media of the article).
|
|
19
|
+
* The host app provides it around its MDX render tree (the Nuxt blog does it
|
|
20
|
+
* in `components/article/MdxContent.vue` for article + preview pages; a bare
|
|
21
|
+
* consumer that doesn't provide it simply has no auto mode โ TripMap then
|
|
22
|
+
* needs explicit `slugs` or `nodes`).
|
|
23
|
+
*/
|
|
24
|
+
export declare interface MdxArticleContext {
|
|
25
|
+
filename: string;
|
|
26
|
+
language?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The article's geotagged media (the GraphQL `contentImagesGeo` rows),
|
|
29
|
+
* when the host already has them. Lets the SERVER-RENDERED facade carry
|
|
30
|
+
* real thumbnails with alt text and named stops in AUTO mode โ the part of
|
|
31
|
+
* a TripMap that crawlers and answer engines read. Optional: without it
|
|
32
|
+
* the live map still fetches everything, only the no-JS view is poorer.
|
|
33
|
+
*/
|
|
34
|
+
media?: readonly TripMapFacadeMedia[] | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { parseHandNodes }
|
|
38
|
+
|
|
39
|
+
export { tripMapFormatTime }
|
|
40
|
+
|
|
41
|
+
export { TripMapGeoNode }
|
|
42
|
+
|
|
43
|
+
export { TripMapGeoResponse }
|
|
44
|
+
|
|
45
|
+
export declare function useTripMap(props: TripMapProps): UseTripMapResult;
|
|
46
|
+
|
|
47
|
+
export declare interface UseTripMapResult {
|
|
48
|
+
slugList: ComputedRef<string[]>;
|
|
49
|
+
excludeList: ComputedRef<string[]>;
|
|
50
|
+
handNodes: ComputedRef<TripMapHandNode[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Deterministic SSR thumbs: author order for explicit `slugs`, capture
|
|
53
|
+
* order for AUTO mode when the host provided `context.media`. Alt text
|
|
54
|
+
* comes from that media (empty when unknown).
|
|
55
|
+
*/
|
|
56
|
+
facadeThumbs: ComputedRef<TripMapFacadeThumb[]>;
|
|
57
|
+
/** Named stops for the SSR facade: place-labelled photos + hand-made nodes, chronological. */
|
|
58
|
+
facadeStops: ComputedRef<TripMapFacadeStop[]>;
|
|
59
|
+
heightStyle: ComputedRef<{
|
|
60
|
+
height: string;
|
|
61
|
+
}>;
|
|
62
|
+
articleContext: MdxArticleContext | null;
|
|
63
|
+
isAutoMode: ComputedRef<boolean>;
|
|
64
|
+
/**
|
|
65
|
+
* Nothing selects media (no slugs, no article context): the geo call only
|
|
66
|
+
* fetches the tenant basemap config, so its failure must not sink a map
|
|
67
|
+
* of hand-made nodes โ the wrapper falls back to the core default style.
|
|
68
|
+
*/
|
|
69
|
+
isConfigOnly: ComputedRef<boolean>;
|
|
70
|
+
/** Always set: slugs / article selection, or the config-only URL. */
|
|
71
|
+
geoUrl: ComputedRef<string>;
|
|
72
|
+
labels: ComputedRef<TripMapUiLabels | undefined>;
|
|
73
|
+
texts: ComputedRef<TripMapUiTexts>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blogmcp/mdx-render-vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.
|
|
101
|
+
"@blogmcp/mdx-schema": "^0.5.0",
|
|
94
102
|
"@blogmcp/sanitize": "^0.3.0",
|
|
103
|
+
"@blogmcp/tripmap-core": "^0.1.2",
|
|
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,124 @@
|
|
|
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
|
+
tripMapFacade,
|
|
23
|
+
tripMapPlaybackLabels,
|
|
24
|
+
type TripMapFacadeMedia,
|
|
25
|
+
type TripMapFacadeStop,
|
|
26
|
+
type TripMapFacadeThumb,
|
|
27
|
+
type TripMapUiLabels,
|
|
28
|
+
tripMapUiTexts,
|
|
29
|
+
type TripMapGeoNode,
|
|
30
|
+
type TripMapGeoResponse,
|
|
31
|
+
type TripMapUiTexts,
|
|
32
|
+
} from '@blogmcp/mdx-schema/tripmap'
|
|
33
|
+
|
|
34
|
+
// Re-export the shared helpers under this package's established names so
|
|
35
|
+
// consumers (md-editor, tests) have one import site on the Vue side.
|
|
36
|
+
export { tripMapFormatTime }
|
|
37
|
+
export { parseTripMapHandNodes as parseHandNodes }
|
|
38
|
+
export type { TripMapGeoNode, TripMapGeoResponse }
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Article identity for AUTO mode (map every geotagged media of the article).
|
|
42
|
+
* The host app provides it around its MDX render tree (the Nuxt blog does it
|
|
43
|
+
* in `components/article/MdxContent.vue` for article + preview pages; a bare
|
|
44
|
+
* consumer that doesn't provide it simply has no auto mode โ TripMap then
|
|
45
|
+
* needs explicit `slugs` or `nodes`).
|
|
46
|
+
*/
|
|
47
|
+
export interface MdxArticleContext {
|
|
48
|
+
filename: string
|
|
49
|
+
language?: string
|
|
50
|
+
/**
|
|
51
|
+
* The article's geotagged media (the GraphQL `contentImagesGeo` rows),
|
|
52
|
+
* when the host already has them. Lets the SERVER-RENDERED facade carry
|
|
53
|
+
* real thumbnails with alt text and named stops in AUTO mode โ the part of
|
|
54
|
+
* a TripMap that crawlers and answer engines read. Optional: without it
|
|
55
|
+
* the live map still fetches everything, only the no-JS view is poorer.
|
|
56
|
+
*/
|
|
57
|
+
media?: readonly TripMapFacadeMedia[] | null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const MDX_ARTICLE_CONTEXT_KEY: InjectionKey<MdxArticleContext | null> =
|
|
61
|
+
Symbol.for('blogmcp:mdx-article-context')
|
|
62
|
+
|
|
63
|
+
export interface UseTripMapResult {
|
|
64
|
+
slugList: ComputedRef<string[]>
|
|
65
|
+
excludeList: ComputedRef<string[]>
|
|
66
|
+
handNodes: ComputedRef<TripMapHandNode[]>
|
|
67
|
+
/**
|
|
68
|
+
* Deterministic SSR thumbs: author order for explicit `slugs`, capture
|
|
69
|
+
* order for AUTO mode when the host provided `context.media`. Alt text
|
|
70
|
+
* comes from that media (empty when unknown).
|
|
71
|
+
*/
|
|
72
|
+
facadeThumbs: ComputedRef<TripMapFacadeThumb[]>
|
|
73
|
+
/** Named stops for the SSR facade: place-labelled photos + hand-made nodes, chronological. */
|
|
74
|
+
facadeStops: ComputedRef<TripMapFacadeStop[]>
|
|
75
|
+
heightStyle: ComputedRef<{ height: string }>
|
|
76
|
+
articleContext: MdxArticleContext | null
|
|
77
|
+
isAutoMode: ComputedRef<boolean>
|
|
78
|
+
/**
|
|
79
|
+
* Nothing selects media (no slugs, no article context): the geo call only
|
|
80
|
+
* fetches the tenant basemap config, so its failure must not sink a map
|
|
81
|
+
* of hand-made nodes โ the wrapper falls back to the core default style.
|
|
82
|
+
*/
|
|
83
|
+
isConfigOnly: ComputedRef<boolean>
|
|
84
|
+
/** Always set: slugs / article selection, or the config-only URL. */
|
|
85
|
+
geoUrl: ComputedRef<string>
|
|
86
|
+
labels: ComputedRef<TripMapUiLabels | undefined>
|
|
87
|
+
texts: ComputedRef<TripMapUiTexts>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function useTripMap(props: TripMapProps): UseTripMapResult {
|
|
91
|
+
const articleContext = inject(MDX_ARTICLE_CONTEXT_KEY, null)
|
|
92
|
+
|
|
93
|
+
const slugList = computed(() => splitTripMapList(props.slugs))
|
|
94
|
+
const excludeList = computed(() => splitTripMapList(props.exclude))
|
|
95
|
+
const handNodes = computed(() => parseTripMapHandNodes(props.nodes))
|
|
96
|
+
const isAutoMode = computed(() => slugList.value.length === 0)
|
|
97
|
+
const isConfigOnly = computed(() => slugList.value.length === 0 && !articleContext?.filename)
|
|
98
|
+
|
|
99
|
+
const facade = computed(() => tripMapFacade(props, articleContext))
|
|
100
|
+
const facadeThumbs = computed(() => facade.value.thumbs)
|
|
101
|
+
const facadeStops = computed(() => facade.value.stops)
|
|
102
|
+
|
|
103
|
+
const heightStyle = computed(() => ({ height: props.height || TRIPMAP_DEFAULT_HEIGHT }))
|
|
104
|
+
|
|
105
|
+
const geoUrl = computed(() => buildTripMapGeoUrl(props, articleContext))
|
|
106
|
+
|
|
107
|
+
const labels = computed(() => tripMapPlaybackLabels(articleContext?.language))
|
|
108
|
+
const texts = computed(() => tripMapUiTexts(articleContext?.language))
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
slugList,
|
|
112
|
+
excludeList,
|
|
113
|
+
handNodes,
|
|
114
|
+
facadeThumbs,
|
|
115
|
+
facadeStops,
|
|
116
|
+
heightStyle,
|
|
117
|
+
articleContext,
|
|
118
|
+
isAutoMode,
|
|
119
|
+
isConfigOnly,
|
|
120
|
+
geoUrl,
|
|
121
|
+
labels,
|
|
122
|
+
texts,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -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 */
|