@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.
@@ -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,4 @@
1
+ import _sfc_main from "./TripMapDefault.vue.js";
2
+ export {
3
+ _sfc_main as default
4
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * TripMap Render Component Variants
3
+ */
4
+ export { default as TripMapDefault } from './TripMapDefault.vue';
5
+ export { default } from './TripMapDefault.vue';
@@ -0,0 +1,5 @@
1
+ import { default as default2, default as default3 } from "./TripMapDefault.vue.js";
2
+ export {
3
+ default2 as TripMapDefault,
4
+ default3 as default
5
+ };
@@ -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.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.3.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
  },
@@ -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. */
@@ -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 */