@blogmcp/mdx-render-vue 0.1.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.
Files changed (249) hide show
  1. package/dist/_virtual/_plugin-vue_export-helper.js +10 -0
  2. package/dist/_virtual/constants.js +4 -0
  3. package/dist/_virtual/index.js +5 -0
  4. package/dist/_virtual/index2.js +4 -0
  5. package/dist/composables/index.d.ts +11 -0
  6. package/dist/composables/index.js +24 -0
  7. package/dist/composables/useAudioPlayer.d.ts +22 -0
  8. package/dist/composables/useAudioPlayer.js +58 -0
  9. package/dist/composables/useCallout.d.ts +43 -0
  10. package/dist/composables/useCallout.js +71 -0
  11. package/dist/composables/useCodeBlock.d.ts +23 -0
  12. package/dist/composables/useCodeBlock.js +67 -0
  13. package/dist/composables/useGallery.d.ts +37 -0
  14. package/dist/composables/useGallery.js +84 -0
  15. package/dist/composables/useMedia.d.ts +18 -0
  16. package/dist/composables/useMedia.js +9 -0
  17. package/dist/composables/useYouTube.d.ts +31 -0
  18. package/dist/composables/useYouTube.js +32 -0
  19. package/dist/edit/AlertEdit.vue.d.ts +12 -0
  20. package/dist/edit/AlertEdit.vue.js +4 -0
  21. package/dist/edit/AlertEdit.vue2.js +18 -0
  22. package/dist/edit/AudioPlayerEdit.vue.d.ts +3 -0
  23. package/dist/edit/AudioPlayerEdit.vue.js +117 -0
  24. package/dist/edit/AudioPlayerEdit.vue2.js +4 -0
  25. package/dist/edit/BadgeEdit.vue.d.ts +2 -0
  26. package/dist/edit/BadgeEdit.vue.js +4 -0
  27. package/dist/edit/BadgeEdit.vue2.js +13 -0
  28. package/dist/edit/ButtonEdit.vue.d.ts +2 -0
  29. package/dist/edit/ButtonEdit.vue.js +4 -0
  30. package/dist/edit/ButtonEdit.vue2.js +13 -0
  31. package/dist/edit/CalloutEdit.vue.d.ts +13 -0
  32. package/dist/edit/CalloutEdit.vue.js +85 -0
  33. package/dist/edit/CalloutEdit.vue2.js +4 -0
  34. package/dist/edit/CardEdit.vue.d.ts +12 -0
  35. package/dist/edit/CardEdit.vue.js +4 -0
  36. package/dist/edit/CardEdit.vue2.js +18 -0
  37. package/dist/edit/CodeBlockEdit.vue.d.ts +12 -0
  38. package/dist/edit/CodeBlockEdit.vue.js +7 -0
  39. package/dist/edit/CodeBlockEdit.vue2.js +89 -0
  40. package/dist/edit/GalleryEdit.vue.d.ts +3 -0
  41. package/dist/edit/GalleryEdit.vue.js +64 -0
  42. package/dist/edit/GalleryEdit.vue2.js +4 -0
  43. package/dist/edit/IconEdit.vue.d.ts +2 -0
  44. package/dist/edit/IconEdit.vue.js +4 -0
  45. package/dist/edit/IconEdit.vue2.js +13 -0
  46. package/dist/edit/TooltipEdit.vue.d.ts +16 -0
  47. package/dist/edit/TooltipEdit.vue.js +4 -0
  48. package/dist/edit/TooltipEdit.vue2.js +24 -0
  49. package/dist/edit/YouTubeEdit.vue.d.ts +3 -0
  50. package/dist/edit/YouTubeEdit.vue.js +41 -0
  51. package/dist/edit/YouTubeEdit.vue2.js +4 -0
  52. package/dist/edit/index.d.ts +29 -0
  53. package/dist/edit/index.js +35 -0
  54. package/dist/index.d.ts +12 -0
  55. package/dist/index.js +75 -0
  56. package/dist/mdx-render-vue.css +31 -0
  57. package/dist/node_modules/@ark-ui/vue/dist/_virtual/_rolldown/runtime.js +13 -0
  58. package/dist/node_modules/@ark-ui/vue/dist/components/factory.js +35 -0
  59. package/dist/node_modules/@ark-ui/vue/dist/components/presence/use-presence-context.js +6 -0
  60. package/dist/node_modules/@ark-ui/vue/dist/components/presence/use-presence.js +40 -0
  61. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-arrow-tip.js +5 -0
  62. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-arrow-tip.vue_vue_type_script_setup_true_lang.js +21 -0
  63. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-arrow.js +5 -0
  64. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-arrow.vue_vue_type_script_setup_true_lang.js +21 -0
  65. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-content.js +5 -0
  66. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-content.vue_vue_type_script_setup_true_lang.js +25 -0
  67. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-context.js +5 -0
  68. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-context.vue_vue_type_script_setup_true_lang.js +14 -0
  69. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-positioner.js +5 -0
  70. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-positioner.vue_vue_type_script_setup_true_lang.js +23 -0
  71. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-root-provider.js +5 -0
  72. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-root-provider.vue_vue_type_script_setup_true_lang.js +38 -0
  73. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-root.js +5 -0
  74. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-root.vue_vue_type_script_setup_true_lang.js +68 -0
  75. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-trigger.js +5 -0
  76. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-trigger.vue_vue_type_script_setup_true_lang.js +25 -0
  77. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip.js +22 -0
  78. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/use-tooltip-context.js +6 -0
  79. package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/use-tooltip.js +36 -0
  80. package/dist/node_modules/@ark-ui/vue/dist/providers/environment/use-environment-context.js +13 -0
  81. package/dist/node_modules/@ark-ui/vue/dist/providers/locale/use-locale-context.js +12 -0
  82. package/dist/node_modules/@ark-ui/vue/dist/utils/clean-props.js +8 -0
  83. package/dist/node_modules/@ark-ui/vue/dist/utils/create-context.js +14 -0
  84. package/dist/node_modules/@ark-ui/vue/dist/utils/dynamic.js +34 -0
  85. package/dist/node_modules/@ark-ui/vue/dist/utils/unref-element.js +8 -0
  86. package/dist/node_modules/@ark-ui/vue/dist/utils/use-forward-expose.js +46 -0
  87. package/dist/node_modules/@ark-ui/vue/dist/utils/use-render-strategy.js +5 -0
  88. package/dist/node_modules/@braintree/sanitize-url/dist/constants.js +20 -0
  89. package/dist/node_modules/@braintree/sanitize-url/dist/index.js +75 -0
  90. package/dist/node_modules/@floating-ui/core/dist/floating-ui.core.js +728 -0
  91. package/dist/node_modules/@floating-ui/dom/dist/floating-ui.dom.js +629 -0
  92. package/dist/node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.js +176 -0
  93. package/dist/node_modules/@floating-ui/utils/dist/floating-ui.utils.js +151 -0
  94. package/dist/node_modules/@iconify/vue/dist/iconify.js +1691 -0
  95. package/dist/node_modules/@zag-js/anatomy/dist/create-anatomy.js +29 -0
  96. package/dist/node_modules/@zag-js/core/dist/create-machine.js +28 -0
  97. package/dist/node_modules/@zag-js/core/dist/merge-props.js +55 -0
  98. package/dist/node_modules/@zag-js/core/dist/scope.js +20 -0
  99. package/dist/node_modules/@zag-js/core/dist/state.js +176 -0
  100. package/dist/node_modules/@zag-js/core/dist/types.js +11 -0
  101. package/dist/node_modules/@zag-js/dom-query/dist/chunk-QZ7TP4HQ.js +6 -0
  102. package/dist/node_modules/@zag-js/dom-query/dist/computed-style.js +11 -0
  103. package/dist/node_modules/@zag-js/dom-query/dist/event.js +37 -0
  104. package/dist/node_modules/@zag-js/dom-query/dist/node.js +64 -0
  105. package/dist/node_modules/@zag-js/dom-query/dist/overflow.js +28 -0
  106. package/dist/node_modules/@zag-js/dom-query/dist/platform.js +23 -0
  107. package/dist/node_modules/@zag-js/dom-query/dist/query.js +6 -0
  108. package/dist/node_modules/@zag-js/dom-query/dist/raf.js +52 -0
  109. package/dist/node_modules/@zag-js/dom-query/dist/set.js +22 -0
  110. package/dist/node_modules/@zag-js/dom-query/dist/shared.js +8 -0
  111. package/dist/node_modules/@zag-js/focus-visible/dist/index.js +159 -0
  112. package/dist/node_modules/@zag-js/popper/dist/get-anchor.js +39 -0
  113. package/dist/node_modules/@zag-js/popper/dist/get-placement.js +296 -0
  114. package/dist/node_modules/@zag-js/popper/dist/get-styles.js +47 -0
  115. package/dist/node_modules/@zag-js/popper/dist/middleware.js +79 -0
  116. package/dist/node_modules/@zag-js/popper/dist/placement.js +7 -0
  117. package/dist/node_modules/@zag-js/presence/dist/presence.connect.js +18 -0
  118. package/dist/node_modules/@zag-js/presence/dist/presence.machine.js +175 -0
  119. package/dist/node_modules/@zag-js/tooltip/dist/tooltip.anatomy.js +7 -0
  120. package/dist/node_modules/@zag-js/tooltip/dist/tooltip.connect.js +155 -0
  121. package/dist/node_modules/@zag-js/tooltip/dist/tooltip.dom.js +24 -0
  122. package/dist/node_modules/@zag-js/tooltip/dist/tooltip.machine.js +416 -0
  123. package/dist/node_modules/@zag-js/tooltip/dist/tooltip.store.js +9 -0
  124. package/dist/node_modules/@zag-js/types/dist/prop-types.js +14 -0
  125. package/dist/node_modules/@zag-js/utils/dist/array.js +7 -0
  126. package/dist/node_modules/@zag-js/utils/dist/equal.js +36 -0
  127. package/dist/node_modules/@zag-js/utils/dist/functions.js +11 -0
  128. package/dist/node_modules/@zag-js/utils/dist/guard.js +26 -0
  129. package/dist/node_modules/@zag-js/utils/dist/object.js +16 -0
  130. package/dist/node_modules/@zag-js/utils/dist/store.js +44 -0
  131. package/dist/node_modules/@zag-js/utils/dist/warning.js +31 -0
  132. package/dist/node_modules/@zag-js/vue/dist/bindable.js +48 -0
  133. package/dist/node_modules/@zag-js/vue/dist/machine.js +258 -0
  134. package/dist/node_modules/@zag-js/vue/dist/normalize-props.js +42 -0
  135. package/dist/node_modules/@zag-js/vue/dist/refs.js +14 -0
  136. package/dist/node_modules/@zag-js/vue/dist/track.js +22 -0
  137. package/dist/node_modules/dompurify/dist/purify.es.js +1190 -0
  138. package/dist/registry.d.ts +76 -0
  139. package/dist/registry.js +164 -0
  140. package/dist/render/alert/Alert.vue.d.ts +18 -0
  141. package/dist/render/alert/Alert.vue.js +4 -0
  142. package/dist/render/alert/Alert.vue2.js +43 -0
  143. package/dist/render/alert/index.d.ts +2 -0
  144. package/dist/render/audioplayer/AudioPlayerDefault.vue.d.ts +3 -0
  145. package/dist/render/audioplayer/AudioPlayerDefault.vue.js +116 -0
  146. package/dist/render/audioplayer/AudioPlayerDefault.vue2.js +4 -0
  147. package/dist/render/audioplayer/index.d.ts +5 -0
  148. package/dist/render/audioplayer/index.js +5 -0
  149. package/dist/render/badge/Badge.vue.d.ts +10 -0
  150. package/dist/render/badge/Badge.vue.js +4 -0
  151. package/dist/render/badge/Badge.vue2.js +28 -0
  152. package/dist/render/badge/index.d.ts +2 -0
  153. package/dist/render/button/Button.vue.d.ts +13 -0
  154. package/dist/render/button/Button.vue.js +4 -0
  155. package/dist/render/button/Button.vue2.js +42 -0
  156. package/dist/render/button/index.d.ts +2 -0
  157. package/dist/render/callout/CalloutDefault.vue.d.ts +13 -0
  158. package/dist/render/callout/CalloutDefault.vue.js +86 -0
  159. package/dist/render/callout/CalloutDefault.vue2.js +4 -0
  160. package/dist/render/callout/CalloutMinimal.vue.d.ts +13 -0
  161. package/dist/render/callout/CalloutMinimal.vue.js +27 -0
  162. package/dist/render/callout/CalloutMinimal.vue2.js +4 -0
  163. package/dist/render/callout/index.d.ts +6 -0
  164. package/dist/render/callout/index.js +7 -0
  165. package/dist/render/card/Card.vue.d.ts +15 -0
  166. package/dist/render/card/Card.vue.js +4 -0
  167. package/dist/render/card/Card.vue2.js +29 -0
  168. package/dist/render/card/index.d.ts +2 -0
  169. package/dist/render/codeblock/CodeBlockDefault.vue.d.ts +12 -0
  170. package/dist/render/codeblock/CodeBlockDefault.vue.js +7 -0
  171. package/dist/render/codeblock/CodeBlockDefault.vue2.js +92 -0
  172. package/dist/render/codeblock/index.d.ts +5 -0
  173. package/dist/render/codeblock/index.js +5 -0
  174. package/dist/render/gallery/GalleryGrid.vue.d.ts +3 -0
  175. package/dist/render/gallery/GalleryGrid.vue.js +125 -0
  176. package/dist/render/gallery/GalleryGrid.vue2.js +4 -0
  177. package/dist/render/gallery/GalleryLightbox.vue.d.ts +16 -0
  178. package/dist/render/gallery/GalleryLightbox.vue.js +7 -0
  179. package/dist/render/gallery/GalleryLightbox.vue2.js +170 -0
  180. package/dist/render/gallery/index.d.ts +6 -0
  181. package/dist/render/gallery/index.js +7 -0
  182. package/dist/render/icon/Icon.vue.d.ts +9 -0
  183. package/dist/render/icon/Icon.vue.js +4 -0
  184. package/dist/render/icon/Icon.vue2.js +25 -0
  185. package/dist/render/icon/index.d.ts +2 -0
  186. package/dist/render/index.d.ts +40 -0
  187. package/dist/render/index.js +63 -0
  188. package/dist/render/tooltip/Tooltip.vue.d.ts +18 -0
  189. package/dist/render/tooltip/Tooltip.vue.js +4 -0
  190. package/dist/render/tooltip/Tooltip.vue2.js +58 -0
  191. package/dist/render/tooltip/index.d.ts +2 -0
  192. package/dist/render/youtube/YouTubeDefault.vue.d.ts +3 -0
  193. package/dist/render/youtube/YouTubeDefault.vue.js +79 -0
  194. package/dist/render/youtube/YouTubeDefault.vue2.js +4 -0
  195. package/dist/render/youtube/index.d.ts +5 -0
  196. package/dist/render/youtube/index.js +5 -0
  197. package/dist/useAudioPlayer.d.ts +22 -0
  198. package/dist/useCallout.d.ts +46 -0
  199. package/dist/useCodeBlock.d.ts +25 -0
  200. package/dist/useGallery.d.ts +44 -0
  201. package/dist/useMedia.d.ts +16 -0
  202. package/dist/useYouTube.d.ts +32 -0
  203. package/package.json +125 -0
  204. package/src/composables/index.ts +16 -0
  205. package/src/composables/useAudioPlayer.ts +84 -0
  206. package/src/composables/useCallout.ts +103 -0
  207. package/src/composables/useCodeBlock.ts +90 -0
  208. package/src/composables/useGallery.ts +143 -0
  209. package/src/composables/useMedia.ts +23 -0
  210. package/src/composables/useYouTube.ts +58 -0
  211. package/src/edit/AlertEdit.vue +9 -0
  212. package/src/edit/AudioPlayerEdit.vue +92 -0
  213. package/src/edit/BadgeEdit.vue +7 -0
  214. package/src/edit/ButtonEdit.vue +10 -0
  215. package/src/edit/CalloutEdit.vue +107 -0
  216. package/src/edit/CardEdit.vue +9 -0
  217. package/src/edit/CodeBlockEdit.vue +76 -0
  218. package/src/edit/GalleryEdit.vue +65 -0
  219. package/src/edit/IconEdit.vue +7 -0
  220. package/src/edit/TooltipEdit.vue +15 -0
  221. package/src/edit/YouTubeEdit.vue +33 -0
  222. package/src/edit/index.ts +32 -0
  223. package/src/index.ts +62 -0
  224. package/src/registry.ts +272 -0
  225. package/src/render/alert/Alert.vue +32 -0
  226. package/src/render/alert/index.ts +2 -0
  227. package/src/render/audioplayer/AudioPlayerDefault.vue +89 -0
  228. package/src/render/audioplayer/index.ts +8 -0
  229. package/src/render/badge/Badge.vue +25 -0
  230. package/src/render/badge/index.ts +2 -0
  231. package/src/render/button/Button.vue +41 -0
  232. package/src/render/button/index.ts +2 -0
  233. package/src/render/callout/CalloutDefault.vue +101 -0
  234. package/src/render/callout/CalloutMinimal.vue +31 -0
  235. package/src/render/callout/index.ts +9 -0
  236. package/src/render/card/Card.vue +15 -0
  237. package/src/render/card/index.ts +2 -0
  238. package/src/render/codeblock/CodeBlockDefault.vue +76 -0
  239. package/src/render/codeblock/index.ts +8 -0
  240. package/src/render/gallery/GalleryGrid.vue +162 -0
  241. package/src/render/gallery/GalleryLightbox.vue +162 -0
  242. package/src/render/gallery/index.ts +9 -0
  243. package/src/render/icon/Icon.vue +22 -0
  244. package/src/render/icon/index.ts +2 -0
  245. package/src/render/index.ts +76 -0
  246. package/src/render/tooltip/Tooltip.vue +35 -0
  247. package/src/render/tooltip/index.ts +2 -0
  248. package/src/render/youtube/YouTubeDefault.vue +72 -0
  249. package/src/render/youtube/index.ts +8 -0
@@ -0,0 +1,76 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * CodeBlock Default Render Component
4
+ *
5
+ * Standard code block styling for blog display.
6
+ * Light theme with syntax highlighting support.
7
+ */
8
+
9
+ import { ref } from 'vue'
10
+ import { useCodeBlock } from '../../composables/useCodeBlock'
11
+ import type { CodeBlockProps } from '@blogmcp/mdx-schema'
12
+
13
+ const props = defineProps<CodeBlockProps>()
14
+
15
+ defineSlots<{
16
+ default(): unknown
17
+ }>()
18
+
19
+ const { copied, languageLabel, languageClass, copyCode } = useCodeBlock(props)
20
+
21
+ const codeRef = ref<HTMLElement | null>(null)
22
+
23
+ async function handleCopy() {
24
+ if (codeRef.value) {
25
+ await copyCode(codeRef.value.textContent || '')
26
+ }
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <div class="code-block my-6 rounded-lg overflow-hidden border border-gray-200 bg-gray-50" data-mdx-component="CodeBlock">
32
+ <!-- Header -->
33
+ <div class="flex items-center justify-between px-4 py-2 bg-gray-100 border-b border-gray-200">
34
+ <div class="flex items-center gap-2">
35
+ <span class="text-xs text-gray-600 font-medium">
36
+ {{ languageLabel }}
37
+ </span>
38
+ <span v-if="title" class="text-xs text-gray-500">
39
+ {{ title }}
40
+ </span>
41
+ </div>
42
+ <button
43
+ class="flex items-center gap-1 px-2 py-1 text-xs rounded transition-colors"
44
+ :class="copied ? 'bg-green-500 text-white' : 'hover:bg-gray-200 text-gray-500 hover:text-gray-700'"
45
+ @click="handleCopy"
46
+ >
47
+ <svg v-if="!copied" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
49
+ </svg>
50
+ <svg v-else class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
51
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
52
+ </svg>
53
+ {{ copied ? 'Copied!' : 'Copy' }}
54
+ </button>
55
+ </div>
56
+
57
+ <!-- Code Content -->
58
+ <div class="code-block-content p-4 overflow-x-auto">
59
+ <pre class="m-0"><code ref="codeRef" :class="languageClass"><slot /></code></pre>
60
+ </div>
61
+ </div>
62
+ </template>
63
+
64
+ <style scoped>
65
+ .code-block-content pre {
66
+ margin: 0;
67
+ background: transparent;
68
+ }
69
+
70
+ .code-block-content code {
71
+ font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
72
+ font-size: 0.875rem;
73
+ line-height: 1.6;
74
+ color: #374151;
75
+ }
76
+ </style>
@@ -0,0 +1,8 @@
1
+ /**
2
+ * CodeBlock Render Component Variants
3
+ */
4
+
5
+ export { default as CodeBlockDefault } from './CodeBlockDefault.vue'
6
+
7
+ // Default export for convenience
8
+ export { default } from './CodeBlockDefault.vue'
@@ -0,0 +1,162 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Gallery Grid Render Component
4
+ *
5
+ * Standard grid gallery for blog display.
6
+ * Clean Medium-style layout with natural aspect ratios.
7
+ * Click on images to open fullscreen lightbox.
8
+ */
9
+
10
+ import { ref, computed, watch, onUnmounted } from 'vue'
11
+ import { useGallery } from '../../composables/useGallery'
12
+ import { useOptimizedImg } from '../../composables/useMedia'
13
+ import GalleryLightbox from './GalleryLightbox.vue'
14
+ import type { GalleryProps } from '@blogmcp/mdx-schema'
15
+
16
+ const props = defineProps<GalleryProps>()
17
+
18
+ const { gridColsClass, gapClass, tileLayout, getImageSrc, getImageAlt, getImageDescription, getImageCaption, isVideo, hasImages } = useGallery(props)
19
+ const ImgTag = useOptimizedImg()
20
+
21
+ // Lightbox state
22
+ const lightboxOpen = ref(false)
23
+ const lightboxIndex = ref(0)
24
+
25
+ // Map between full images array indices and image-only indices
26
+ const imageOnlyIndices = computed(() => {
27
+ const indices: number[] = []
28
+ for (let i = 0; i < props.images.length; i++) {
29
+ if (!isVideo(i)) indices.push(i)
30
+ }
31
+ return indices
32
+ })
33
+
34
+ const imageOnlyCount = computed(() => imageOnlyIndices.value.length)
35
+
36
+ function openLightbox(fullIndex: number) {
37
+ if (isVideo(fullIndex)) return
38
+ const lightboxIdx = imageOnlyIndices.value.indexOf(fullIndex)
39
+ if (lightboxIdx === -1) return
40
+ lightboxIndex.value = lightboxIdx
41
+ lightboxOpen.value = true
42
+ }
43
+
44
+ function closeLightbox() {
45
+ lightboxOpen.value = false
46
+ }
47
+
48
+ function navigateLightbox(newIndex: number) {
49
+ lightboxIndex.value = newIndex
50
+ }
51
+
52
+ // Map lightbox index back to full array index for getters
53
+ function lightboxGetSrc(idx: number): string {
54
+ return getImageSrc(imageOnlyIndices.value[idx])
55
+ }
56
+
57
+ function lightboxGetAlt(idx: number): string {
58
+ return getImageAlt(imageOnlyIndices.value[idx])
59
+ }
60
+
61
+ function lightboxGetDescription(idx: number): string | undefined {
62
+ return getImageDescription(imageOnlyIndices.value[idx])
63
+ }
64
+
65
+ // Scroll lock
66
+ watch(lightboxOpen, (open) => {
67
+ if (open) {
68
+ document.body.style.overflow = 'hidden'
69
+ } else {
70
+ document.body.style.overflow = ''
71
+ }
72
+ })
73
+
74
+ onUnmounted(() => {
75
+ document.body.style.overflow = ''
76
+ })
77
+ </script>
78
+
79
+ <template>
80
+ <!-- not-prose: the blog renders articles inside Tailwind `prose`, which puts a
81
+ ~2em margin on every img/video. Inside these fixed-aspect tiles that margin
82
+ pushed the media down (dark strip on top) and clipped the bottom (video
83
+ controls). The gallery owns its own spacing, so opt out of prose here. -->
84
+ <div
85
+ v-if="hasImages"
86
+ class="gallery not-prose my-6"
87
+ :class="[gridColsClass, gapClass]"
88
+ style="display: grid"
89
+ data-mdx-component="Gallery"
90
+ >
91
+ <div
92
+ v-for="(_, index) in images"
93
+ :key="index"
94
+ class="gallery-item"
95
+ >
96
+ <!-- Tile geometry comes from `tileLayout` (aspectRatio/fit/position props),
97
+ defaulting to a square object-cover tile — matches the md-editor preview
98
+ so the blog is WYSIWYG. Full uncropped image opens in the lightbox.
99
+ Styles are inline (not Tailwind classes) so a consumer's JIT purge can't
100
+ drop arbitrary aspect-/object- utilities, mirroring `display:grid` above. -->
101
+ <div
102
+ class="gallery-media group relative overflow-hidden rounded"
103
+ :style="tileLayout.tileStyle"
104
+ :class="isVideo(index) ? 'bg-gray-900' : 'bg-gray-100 cursor-pointer'"
105
+ @click="openLightbox(index)"
106
+ >
107
+ <!-- Tile background is media-type-specific: VIDEO tiles use a dark cell
108
+ (bg-gray-900) so a posterless video — the poster route 404s → a 1×1
109
+ GIF — never reads as blank white before the first frame paints (we
110
+ seek to #t=0.1 below for a real thumbnail). IMAGE tiles use a light
111
+ neutral (bg-gray-100): for `fit="contain"` the letterbox bars and the
112
+ pre-load state blend with the light blog/prose theme instead of the
113
+ old baked-in dark backdrop (a dark-mode leftover). -->
114
+ <video
115
+ v-if="isVideo(index)"
116
+ :src="`${getImageSrc(index)}#t=0.1`"
117
+ controls
118
+ preload="metadata"
119
+ playsinline
120
+ :class="tileLayout.mediaSizeClass"
121
+ :style="tileLayout.mediaStyle"
122
+ />
123
+ <template v-else>
124
+ <component
125
+ :is="ImgTag"
126
+ :src="getImageSrc(index)"
127
+ :alt="getImageAlt(index)"
128
+ sizes="100vw sm:50vw md:33vw lg:299px"
129
+ :class="[tileLayout.mediaSizeClass, 'transition-transform duration-200 group-hover:scale-[1.03]']"
130
+ :style="tileLayout.mediaStyle"
131
+ loading="lazy"
132
+ />
133
+ <div class="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-200 pointer-events-none" />
134
+ <svg
135
+ class="gallery-expand-icon absolute top-2 right-2 text-white opacity-50 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-80 transition-opacity duration-200 drop-shadow-md pointer-events-none"
136
+ xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
137
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
138
+ >
139
+ <polyline points="15 3 21 3 21 9" />
140
+ <polyline points="9 21 3 21 3 15" />
141
+ <line x1="21" y1="3" x2="14" y2="10" />
142
+ <line x1="3" y1="21" x2="10" y2="14" />
143
+ </svg>
144
+ </template>
145
+ </div>
146
+ <p v-if="getImageCaption(index)" class="gallery-description text-center text-sm text-gray-500 mt-2">
147
+ {{ getImageCaption(index) }}
148
+ </p>
149
+ </div>
150
+ </div>
151
+
152
+ <GalleryLightbox
153
+ :is-open="lightboxOpen"
154
+ :current-index="lightboxIndex"
155
+ :image-count="imageOnlyCount"
156
+ :get-image-src="lightboxGetSrc"
157
+ :get-image-alt="lightboxGetAlt"
158
+ :get-image-description="lightboxGetDescription"
159
+ @close="closeLightbox"
160
+ @navigate="navigateLightbox"
161
+ />
162
+ </template>
@@ -0,0 +1,162 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Gallery Lightbox Component
4
+ *
5
+ * Fullscreen image viewer with keyboard and touch navigation.
6
+ * Teleported to body with dark backdrop overlay.
7
+ */
8
+
9
+ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
10
+
11
+ const props = defineProps<{
12
+ isOpen: boolean
13
+ currentIndex: number
14
+ imageCount: number
15
+ getImageSrc: (index: number) => string
16
+ getImageAlt: (index: number) => string
17
+ getImageDescription: (index: number) => string | undefined
18
+ }>()
19
+
20
+ const emit = defineEmits<{
21
+ close: []
22
+ navigate: [index: number]
23
+ }>()
24
+
25
+ const hasPrev = computed(() => props.currentIndex > 0)
26
+ const hasNext = computed(() => props.currentIndex < props.imageCount - 1)
27
+ const showNav = computed(() => props.imageCount > 1)
28
+
29
+ // Keyboard navigation
30
+ function onKeydown(e: KeyboardEvent) {
31
+ if (!props.isOpen) return
32
+ if (e.key === 'Escape') {
33
+ emit('close')
34
+ } else if (e.key === 'ArrowLeft' && hasPrev.value) {
35
+ emit('navigate', props.currentIndex - 1)
36
+ } else if (e.key === 'ArrowRight' && hasNext.value) {
37
+ emit('navigate', props.currentIndex + 1)
38
+ }
39
+ }
40
+
41
+ onMounted(() => {
42
+ document.addEventListener('keydown', onKeydown)
43
+ })
44
+
45
+ onUnmounted(() => {
46
+ document.removeEventListener('keydown', onKeydown)
47
+ })
48
+
49
+ // Touch swipe
50
+ const touchStartX = ref(0)
51
+ const touchStartY = ref(0)
52
+
53
+ function onTouchStart(e: TouchEvent) {
54
+ touchStartX.value = e.touches[0].clientX
55
+ touchStartY.value = e.touches[0].clientY
56
+ }
57
+
58
+ function onTouchEnd(e: TouchEvent) {
59
+ const dx = e.changedTouches[0].clientX - touchStartX.value
60
+ const dy = e.changedTouches[0].clientY - touchStartY.value
61
+ // Only trigger if horizontal swipe is dominant and exceeds threshold
62
+ if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) {
63
+ if (dx < 0 && hasNext.value) {
64
+ emit('navigate', props.currentIndex + 1)
65
+ } else if (dx > 0 && hasPrev.value) {
66
+ emit('navigate', props.currentIndex - 1)
67
+ }
68
+ }
69
+ }
70
+
71
+ function onBackdropClick(e: MouseEvent) {
72
+ // Only close if clicking the backdrop itself, not children
73
+ if (e.target === e.currentTarget) {
74
+ emit('close')
75
+ }
76
+ }
77
+ </script>
78
+
79
+ <template>
80
+ <Teleport to="body">
81
+ <Transition name="lightbox-fade">
82
+ <div
83
+ v-if="isOpen"
84
+ class="lightbox-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/90"
85
+ @click="onBackdropClick"
86
+ @touchstart="onTouchStart"
87
+ @touchend="onTouchEnd"
88
+ >
89
+ <!-- Close button -->
90
+ <button
91
+ class="lightbox-close absolute top-4 right-4 z-10 text-white/80 hover:text-white p-2"
92
+ aria-label="Close lightbox"
93
+ @click="emit('close')"
94
+ >
95
+ <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
96
+ <line x1="18" y1="6" x2="6" y2="18" />
97
+ <line x1="6" y1="6" x2="18" y2="18" />
98
+ </svg>
99
+ </button>
100
+
101
+ <!-- Image counter -->
102
+ <div
103
+ v-if="showNav"
104
+ class="lightbox-counter absolute top-4 left-4 text-white/80 text-sm select-none"
105
+ >
106
+ {{ currentIndex + 1 }} / {{ imageCount }}
107
+ </div>
108
+
109
+ <!-- Previous button -->
110
+ <button
111
+ v-if="showNav && hasPrev"
112
+ class="lightbox-prev absolute left-4 top-1/2 -translate-y-1/2 z-10 text-white/80 hover:text-white p-2"
113
+ aria-label="Previous image"
114
+ @click="emit('navigate', currentIndex - 1)"
115
+ >
116
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
117
+ <polyline points="15 18 9 12 15 6" />
118
+ </svg>
119
+ </button>
120
+
121
+ <!-- Next button -->
122
+ <button
123
+ v-if="showNav && hasNext"
124
+ class="lightbox-next absolute right-4 top-1/2 -translate-y-1/2 z-10 text-white/80 hover:text-white p-2"
125
+ aria-label="Next image"
126
+ @click="emit('navigate', currentIndex + 1)"
127
+ >
128
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
129
+ <polyline points="9 18 15 12 9 6" />
130
+ </svg>
131
+ </button>
132
+
133
+ <!-- Image + caption -->
134
+ <div class="flex flex-col items-center max-h-[95vh] max-w-[90vw]" @click.stop>
135
+ <img
136
+ :src="getImageSrc(currentIndex)"
137
+ :alt="getImageAlt(currentIndex)"
138
+ class="lightbox-image max-h-[85vh] max-w-[90vw] object-contain"
139
+ />
140
+ <p
141
+ v-if="getImageDescription(currentIndex)"
142
+ class="lightbox-caption text-white/80 text-sm mt-3 text-center max-w-[80vw]"
143
+ >
144
+ {{ getImageDescription(currentIndex) }}
145
+ </p>
146
+ </div>
147
+ </div>
148
+ </Transition>
149
+ </Teleport>
150
+ </template>
151
+
152
+ <style scoped>
153
+ .lightbox-fade-enter-active,
154
+ .lightbox-fade-leave-active {
155
+ transition: opacity 0.2s ease;
156
+ }
157
+
158
+ .lightbox-fade-enter-from,
159
+ .lightbox-fade-leave-to {
160
+ opacity: 0;
161
+ }
162
+ </style>
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Gallery Render Component Variants
3
+ */
4
+
5
+ export { default as GalleryGrid } from './GalleryGrid.vue'
6
+ export { default as GalleryLightbox } from './GalleryLightbox.vue'
7
+
8
+ // Default export for convenience
9
+ export { default } from './GalleryGrid.vue'
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import { Icon as IconifyIcon } from '@iconify/vue'
3
+
4
+ interface Props {
5
+ name: string
6
+ size?: number
7
+ color?: string
8
+ }
9
+
10
+ withDefaults(defineProps<Props>(), { size: 20 })
11
+ </script>
12
+
13
+ <template>
14
+ <IconifyIcon
15
+ :icon="name"
16
+ :width="size"
17
+ :height="size"
18
+ :style="color ? { color } : undefined"
19
+ class="inline-block align-[-0.125em]"
20
+ data-mdx-component="Icon"
21
+ />
22
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as Icon } from './Icon.vue'
2
+ export { default } from './Icon.vue'
@@ -0,0 +1,76 @@
1
+ /**
2
+ * MDX Render Components
3
+ *
4
+ * These components are used for rendering MDX content in blogs/frontends.
5
+ * Multiple variants available for each component type.
6
+ *
7
+ * Prefer using the async resolver (getRenderComponentAsync) for dynamic loading.
8
+ * Named exports below are kept for backward compatibility but load all components eagerly.
9
+ */
10
+
11
+ // Import all variants (for sync registry and re-exports)
12
+ import { CalloutDefault, CalloutMinimal } from './callout'
13
+ import { CodeBlockDefault } from './codeblock'
14
+ import { GalleryGrid, GalleryLightbox } from './gallery'
15
+ import { YouTubeDefault } from './youtube'
16
+ import { AudioPlayerDefault } from './audioplayer'
17
+ import { Button as ButtonDefault } from './button'
18
+ import { Card as CardDefault } from './card'
19
+ import { Alert as AlertDefault } from './alert'
20
+ import { Badge as BadgeDefault } from './badge'
21
+ import { Icon as IconDefault } from './icon'
22
+ import { Tooltip as TooltipDefault } from './tooltip'
23
+
24
+ // Re-export named variants (backward compat)
25
+ export { CalloutDefault, CalloutMinimal }
26
+ export { CodeBlockDefault }
27
+ export { GalleryGrid, GalleryLightbox }
28
+ export { YouTubeDefault }
29
+ export { AudioPlayerDefault }
30
+ export { ButtonDefault, CardDefault, AlertDefault, BadgeDefault, IconDefault, TooltipDefault }
31
+
32
+ // Default exports for convenience
33
+ export { default as Callout } from './callout'
34
+ export { default as CodeBlock } from './codeblock'
35
+ export { default as Gallery } from './gallery'
36
+ export { default as YouTube } from './youtube'
37
+ export { default as AudioPlayer } from './audioplayer'
38
+ export { default as Button } from './button'
39
+ export { default as Card } from './card'
40
+ export { default as Alert } from './alert'
41
+ export { default as Badge } from './badge'
42
+ export { default as Icon } from './icon'
43
+ export { default as Tooltip } from './tooltip'
44
+
45
+ // Async resolver and helpers from central registry (preferred for new code)
46
+ export {
47
+ getRenderComponentAsync,
48
+ getComponentVariants,
49
+ getEditorConfig,
50
+ componentRegistry,
51
+ } from '../registry'
52
+
53
+ // Sync resolver (backward compat — loads all components eagerly)
54
+ const syncRegistry: Record<string, Record<string, unknown>> = {
55
+ Callout: { default: CalloutDefault, minimal: CalloutMinimal },
56
+ CodeBlock: { default: CodeBlockDefault },
57
+ Gallery: { default: GalleryGrid, grid: GalleryGrid },
58
+ YouTube: { default: YouTubeDefault },
59
+ AudioPlayer: { default: AudioPlayerDefault },
60
+ Button: { default: ButtonDefault },
61
+ Card: { default: CardDefault },
62
+ Alert: { default: AlertDefault },
63
+ Badge: { default: BadgeDefault },
64
+ Icon: { default: IconDefault },
65
+ Tooltip: { default: TooltipDefault },
66
+ }
67
+
68
+ /** @deprecated Use getRenderComponentAsync from registry instead */
69
+ export function getRenderComponent(
70
+ componentName: string,
71
+ variant: string = 'default',
72
+ ): unknown {
73
+ const componentVariants = syncRegistry[componentName]
74
+ if (!componentVariants) return undefined
75
+ return componentVariants[variant] || componentVariants.default
76
+ }
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { Tooltip as ArkTooltip } from '@ark-ui/vue'
4
+
5
+ interface Props {
6
+ label: string
7
+ placement?: 'top' | 'bottom' | 'left' | 'right'
8
+ }
9
+
10
+ const props = withDefaults(defineProps<Props>(), { placement: 'top' })
11
+
12
+ const positioning = computed(() => ({ placement: props.placement }))
13
+ </script>
14
+
15
+ <template>
16
+ <ArkTooltip.Root :positioning="positioning" :open-delay="200" :close-delay="100">
17
+ <ArkTooltip.Trigger as-child>
18
+ <span class="inline-flex" data-mdx-component="Tooltip">
19
+ <slot />
20
+ </span>
21
+ </ArkTooltip.Trigger>
22
+ <Teleport to="body">
23
+ <ArkTooltip.Positioner>
24
+ <ArkTooltip.Content
25
+ class="z-50 rounded-md bg-gray-900 px-2.5 py-1.5 text-xs text-white shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out"
26
+ >
27
+ <ArkTooltip.Arrow>
28
+ <ArkTooltip.ArrowTip class="border-gray-900" />
29
+ </ArkTooltip.Arrow>
30
+ {{ label }}
31
+ </ArkTooltip.Content>
32
+ </ArkTooltip.Positioner>
33
+ </Teleport>
34
+ </ArkTooltip.Root>
35
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as Tooltip } from './Tooltip.vue'
2
+ export { default } from './Tooltip.vue'
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * YouTube Default Render Component
4
+ *
5
+ * Facade pattern: shows optimized thumbnail + play button,
6
+ * loads iframe only on click. Saves ~500KB per embed.
7
+ */
8
+
9
+ import { ref } from 'vue'
10
+ import { useYouTube } from '../../composables/useYouTube'
11
+ import { useOptimizedImg } from '../../composables/useMedia'
12
+ import type { YouTubeProps } from '@blogmcp/mdx-schema'
13
+
14
+ const props = defineProps<YouTubeProps>()
15
+
16
+ const { embedUrl, iframeTitle, thumbnailUrl, hasTitle, YOUTUBE_IFRAME_ALLOW } = useYouTube(props)
17
+ const ImgTag = useOptimizedImg()
18
+
19
+ const activated = ref(false)
20
+
21
+ function activate() {
22
+ activated.value = true
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <div class="youtube-embed my-6" data-mdx-component="YouTube">
28
+ <div class="relative w-full rounded-lg overflow-hidden shadow-md" style="padding-bottom: 56.25%">
29
+ <!-- Facade: thumbnail + play button (before click) -->
30
+ <button
31
+ v-if="!activated"
32
+ class="absolute inset-0 w-full h-full cursor-pointer border-0 p-0 bg-black group"
33
+ :aria-label="`Play ${iframeTitle}`"
34
+ @click="activate"
35
+ >
36
+ <component
37
+ :is="ImgTag"
38
+ :src="thumbnailUrl"
39
+ :alt="iframeTitle"
40
+ sizes="100vw md:768px lg:1024px"
41
+ class="absolute inset-0 w-full h-full object-cover"
42
+ />
43
+ <!-- Play button overlay -->
44
+ <svg
45
+ class="absolute inset-0 m-auto w-16 h-16 text-white drop-shadow-lg opacity-90 group-hover:opacity-100 transition-opacity"
46
+ viewBox="0 0 68 48"
47
+ aria-hidden="true"
48
+ >
49
+ <path
50
+ d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55C3.97 2.33 2.27 4.81 1.48 7.74.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z"
51
+ fill="red"
52
+ />
53
+ <path d="M45 24L27 14v20" fill="white" />
54
+ </svg>
55
+ </button>
56
+
57
+ <!-- Iframe: loaded only after click -->
58
+ <iframe
59
+ v-if="activated"
60
+ :src="`${embedUrl}?autoplay=1`"
61
+ :title="iframeTitle"
62
+ class="absolute inset-0 w-full h-full"
63
+ frameborder="0"
64
+ :allow="YOUTUBE_IFRAME_ALLOW"
65
+ allowfullscreen
66
+ />
67
+ </div>
68
+ <p v-if="hasTitle" class="text-center text-sm text-gray-600 mt-3">
69
+ {{ title }}
70
+ </p>
71
+ </div>
72
+ </template>
@@ -0,0 +1,8 @@
1
+ /**
2
+ * YouTube Render Component Variants
3
+ */
4
+
5
+ export { default as YouTubeDefault } from './YouTubeDefault.vue'
6
+
7
+ // Default export for convenience
8
+ export { default } from './YouTubeDefault.vue'