@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.
- package/dist/_virtual/_plugin-vue_export-helper.js +10 -0
- package/dist/_virtual/constants.js +4 -0
- package/dist/_virtual/index.js +5 -0
- package/dist/_virtual/index2.js +4 -0
- package/dist/composables/index.d.ts +11 -0
- package/dist/composables/index.js +24 -0
- package/dist/composables/useAudioPlayer.d.ts +22 -0
- package/dist/composables/useAudioPlayer.js +58 -0
- package/dist/composables/useCallout.d.ts +43 -0
- package/dist/composables/useCallout.js +71 -0
- package/dist/composables/useCodeBlock.d.ts +23 -0
- package/dist/composables/useCodeBlock.js +67 -0
- package/dist/composables/useGallery.d.ts +37 -0
- package/dist/composables/useGallery.js +84 -0
- package/dist/composables/useMedia.d.ts +18 -0
- package/dist/composables/useMedia.js +9 -0
- package/dist/composables/useYouTube.d.ts +31 -0
- package/dist/composables/useYouTube.js +32 -0
- package/dist/edit/AlertEdit.vue.d.ts +12 -0
- package/dist/edit/AlertEdit.vue.js +4 -0
- package/dist/edit/AlertEdit.vue2.js +18 -0
- package/dist/edit/AudioPlayerEdit.vue.d.ts +3 -0
- package/dist/edit/AudioPlayerEdit.vue.js +117 -0
- package/dist/edit/AudioPlayerEdit.vue2.js +4 -0
- package/dist/edit/BadgeEdit.vue.d.ts +2 -0
- package/dist/edit/BadgeEdit.vue.js +4 -0
- package/dist/edit/BadgeEdit.vue2.js +13 -0
- package/dist/edit/ButtonEdit.vue.d.ts +2 -0
- package/dist/edit/ButtonEdit.vue.js +4 -0
- package/dist/edit/ButtonEdit.vue2.js +13 -0
- package/dist/edit/CalloutEdit.vue.d.ts +13 -0
- package/dist/edit/CalloutEdit.vue.js +85 -0
- package/dist/edit/CalloutEdit.vue2.js +4 -0
- package/dist/edit/CardEdit.vue.d.ts +12 -0
- package/dist/edit/CardEdit.vue.js +4 -0
- package/dist/edit/CardEdit.vue2.js +18 -0
- package/dist/edit/CodeBlockEdit.vue.d.ts +12 -0
- package/dist/edit/CodeBlockEdit.vue.js +7 -0
- package/dist/edit/CodeBlockEdit.vue2.js +89 -0
- package/dist/edit/GalleryEdit.vue.d.ts +3 -0
- package/dist/edit/GalleryEdit.vue.js +64 -0
- package/dist/edit/GalleryEdit.vue2.js +4 -0
- package/dist/edit/IconEdit.vue.d.ts +2 -0
- package/dist/edit/IconEdit.vue.js +4 -0
- package/dist/edit/IconEdit.vue2.js +13 -0
- package/dist/edit/TooltipEdit.vue.d.ts +16 -0
- package/dist/edit/TooltipEdit.vue.js +4 -0
- package/dist/edit/TooltipEdit.vue2.js +24 -0
- package/dist/edit/YouTubeEdit.vue.d.ts +3 -0
- package/dist/edit/YouTubeEdit.vue.js +41 -0
- package/dist/edit/YouTubeEdit.vue2.js +4 -0
- package/dist/edit/index.d.ts +29 -0
- package/dist/edit/index.js +35 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +75 -0
- package/dist/mdx-render-vue.css +31 -0
- package/dist/node_modules/@ark-ui/vue/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/factory.js +35 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/presence/use-presence-context.js +6 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/presence/use-presence.js +40 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-arrow-tip.js +5 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-arrow-tip.vue_vue_type_script_setup_true_lang.js +21 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-arrow.js +5 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-arrow.vue_vue_type_script_setup_true_lang.js +21 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-content.js +5 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-content.vue_vue_type_script_setup_true_lang.js +25 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-context.js +5 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-context.vue_vue_type_script_setup_true_lang.js +14 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-positioner.js +5 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-positioner.vue_vue_type_script_setup_true_lang.js +23 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-root-provider.js +5 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-root-provider.vue_vue_type_script_setup_true_lang.js +38 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-root.js +5 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-root.vue_vue_type_script_setup_true_lang.js +68 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-trigger.js +5 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip-trigger.vue_vue_type_script_setup_true_lang.js +25 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/tooltip.js +22 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/use-tooltip-context.js +6 -0
- package/dist/node_modules/@ark-ui/vue/dist/components/tooltip/use-tooltip.js +36 -0
- package/dist/node_modules/@ark-ui/vue/dist/providers/environment/use-environment-context.js +13 -0
- package/dist/node_modules/@ark-ui/vue/dist/providers/locale/use-locale-context.js +12 -0
- package/dist/node_modules/@ark-ui/vue/dist/utils/clean-props.js +8 -0
- package/dist/node_modules/@ark-ui/vue/dist/utils/create-context.js +14 -0
- package/dist/node_modules/@ark-ui/vue/dist/utils/dynamic.js +34 -0
- package/dist/node_modules/@ark-ui/vue/dist/utils/unref-element.js +8 -0
- package/dist/node_modules/@ark-ui/vue/dist/utils/use-forward-expose.js +46 -0
- package/dist/node_modules/@ark-ui/vue/dist/utils/use-render-strategy.js +5 -0
- package/dist/node_modules/@braintree/sanitize-url/dist/constants.js +20 -0
- package/dist/node_modules/@braintree/sanitize-url/dist/index.js +75 -0
- package/dist/node_modules/@floating-ui/core/dist/floating-ui.core.js +728 -0
- package/dist/node_modules/@floating-ui/dom/dist/floating-ui.dom.js +629 -0
- package/dist/node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.js +176 -0
- package/dist/node_modules/@floating-ui/utils/dist/floating-ui.utils.js +151 -0
- package/dist/node_modules/@iconify/vue/dist/iconify.js +1691 -0
- package/dist/node_modules/@zag-js/anatomy/dist/create-anatomy.js +29 -0
- package/dist/node_modules/@zag-js/core/dist/create-machine.js +28 -0
- package/dist/node_modules/@zag-js/core/dist/merge-props.js +55 -0
- package/dist/node_modules/@zag-js/core/dist/scope.js +20 -0
- package/dist/node_modules/@zag-js/core/dist/state.js +176 -0
- package/dist/node_modules/@zag-js/core/dist/types.js +11 -0
- package/dist/node_modules/@zag-js/dom-query/dist/chunk-QZ7TP4HQ.js +6 -0
- package/dist/node_modules/@zag-js/dom-query/dist/computed-style.js +11 -0
- package/dist/node_modules/@zag-js/dom-query/dist/event.js +37 -0
- package/dist/node_modules/@zag-js/dom-query/dist/node.js +64 -0
- package/dist/node_modules/@zag-js/dom-query/dist/overflow.js +28 -0
- package/dist/node_modules/@zag-js/dom-query/dist/platform.js +23 -0
- package/dist/node_modules/@zag-js/dom-query/dist/query.js +6 -0
- package/dist/node_modules/@zag-js/dom-query/dist/raf.js +52 -0
- package/dist/node_modules/@zag-js/dom-query/dist/set.js +22 -0
- package/dist/node_modules/@zag-js/dom-query/dist/shared.js +8 -0
- package/dist/node_modules/@zag-js/focus-visible/dist/index.js +159 -0
- package/dist/node_modules/@zag-js/popper/dist/get-anchor.js +39 -0
- package/dist/node_modules/@zag-js/popper/dist/get-placement.js +296 -0
- package/dist/node_modules/@zag-js/popper/dist/get-styles.js +47 -0
- package/dist/node_modules/@zag-js/popper/dist/middleware.js +79 -0
- package/dist/node_modules/@zag-js/popper/dist/placement.js +7 -0
- package/dist/node_modules/@zag-js/presence/dist/presence.connect.js +18 -0
- package/dist/node_modules/@zag-js/presence/dist/presence.machine.js +175 -0
- package/dist/node_modules/@zag-js/tooltip/dist/tooltip.anatomy.js +7 -0
- package/dist/node_modules/@zag-js/tooltip/dist/tooltip.connect.js +155 -0
- package/dist/node_modules/@zag-js/tooltip/dist/tooltip.dom.js +24 -0
- package/dist/node_modules/@zag-js/tooltip/dist/tooltip.machine.js +416 -0
- package/dist/node_modules/@zag-js/tooltip/dist/tooltip.store.js +9 -0
- package/dist/node_modules/@zag-js/types/dist/prop-types.js +14 -0
- package/dist/node_modules/@zag-js/utils/dist/array.js +7 -0
- package/dist/node_modules/@zag-js/utils/dist/equal.js +36 -0
- package/dist/node_modules/@zag-js/utils/dist/functions.js +11 -0
- package/dist/node_modules/@zag-js/utils/dist/guard.js +26 -0
- package/dist/node_modules/@zag-js/utils/dist/object.js +16 -0
- package/dist/node_modules/@zag-js/utils/dist/store.js +44 -0
- package/dist/node_modules/@zag-js/utils/dist/warning.js +31 -0
- package/dist/node_modules/@zag-js/vue/dist/bindable.js +48 -0
- package/dist/node_modules/@zag-js/vue/dist/machine.js +258 -0
- package/dist/node_modules/@zag-js/vue/dist/normalize-props.js +42 -0
- package/dist/node_modules/@zag-js/vue/dist/refs.js +14 -0
- package/dist/node_modules/@zag-js/vue/dist/track.js +22 -0
- package/dist/node_modules/dompurify/dist/purify.es.js +1190 -0
- package/dist/registry.d.ts +76 -0
- package/dist/registry.js +164 -0
- package/dist/render/alert/Alert.vue.d.ts +18 -0
- package/dist/render/alert/Alert.vue.js +4 -0
- package/dist/render/alert/Alert.vue2.js +43 -0
- package/dist/render/alert/index.d.ts +2 -0
- package/dist/render/audioplayer/AudioPlayerDefault.vue.d.ts +3 -0
- package/dist/render/audioplayer/AudioPlayerDefault.vue.js +116 -0
- package/dist/render/audioplayer/AudioPlayerDefault.vue2.js +4 -0
- package/dist/render/audioplayer/index.d.ts +5 -0
- package/dist/render/audioplayer/index.js +5 -0
- package/dist/render/badge/Badge.vue.d.ts +10 -0
- package/dist/render/badge/Badge.vue.js +4 -0
- package/dist/render/badge/Badge.vue2.js +28 -0
- package/dist/render/badge/index.d.ts +2 -0
- package/dist/render/button/Button.vue.d.ts +13 -0
- package/dist/render/button/Button.vue.js +4 -0
- package/dist/render/button/Button.vue2.js +42 -0
- package/dist/render/button/index.d.ts +2 -0
- package/dist/render/callout/CalloutDefault.vue.d.ts +13 -0
- package/dist/render/callout/CalloutDefault.vue.js +86 -0
- package/dist/render/callout/CalloutDefault.vue2.js +4 -0
- package/dist/render/callout/CalloutMinimal.vue.d.ts +13 -0
- package/dist/render/callout/CalloutMinimal.vue.js +27 -0
- package/dist/render/callout/CalloutMinimal.vue2.js +4 -0
- package/dist/render/callout/index.d.ts +6 -0
- package/dist/render/callout/index.js +7 -0
- package/dist/render/card/Card.vue.d.ts +15 -0
- package/dist/render/card/Card.vue.js +4 -0
- package/dist/render/card/Card.vue2.js +29 -0
- package/dist/render/card/index.d.ts +2 -0
- package/dist/render/codeblock/CodeBlockDefault.vue.d.ts +12 -0
- package/dist/render/codeblock/CodeBlockDefault.vue.js +7 -0
- package/dist/render/codeblock/CodeBlockDefault.vue2.js +92 -0
- package/dist/render/codeblock/index.d.ts +5 -0
- package/dist/render/codeblock/index.js +5 -0
- package/dist/render/gallery/GalleryGrid.vue.d.ts +3 -0
- package/dist/render/gallery/GalleryGrid.vue.js +125 -0
- package/dist/render/gallery/GalleryGrid.vue2.js +4 -0
- package/dist/render/gallery/GalleryLightbox.vue.d.ts +16 -0
- package/dist/render/gallery/GalleryLightbox.vue.js +7 -0
- package/dist/render/gallery/GalleryLightbox.vue2.js +170 -0
- package/dist/render/gallery/index.d.ts +6 -0
- package/dist/render/gallery/index.js +7 -0
- package/dist/render/icon/Icon.vue.d.ts +9 -0
- package/dist/render/icon/Icon.vue.js +4 -0
- package/dist/render/icon/Icon.vue2.js +25 -0
- package/dist/render/icon/index.d.ts +2 -0
- package/dist/render/index.d.ts +40 -0
- package/dist/render/index.js +63 -0
- package/dist/render/tooltip/Tooltip.vue.d.ts +18 -0
- package/dist/render/tooltip/Tooltip.vue.js +4 -0
- package/dist/render/tooltip/Tooltip.vue2.js +58 -0
- package/dist/render/tooltip/index.d.ts +2 -0
- package/dist/render/youtube/YouTubeDefault.vue.d.ts +3 -0
- package/dist/render/youtube/YouTubeDefault.vue.js +79 -0
- package/dist/render/youtube/YouTubeDefault.vue2.js +4 -0
- package/dist/render/youtube/index.d.ts +5 -0
- package/dist/render/youtube/index.js +5 -0
- package/dist/useAudioPlayer.d.ts +22 -0
- package/dist/useCallout.d.ts +46 -0
- package/dist/useCodeBlock.d.ts +25 -0
- package/dist/useGallery.d.ts +44 -0
- package/dist/useMedia.d.ts +16 -0
- package/dist/useYouTube.d.ts +32 -0
- package/package.json +125 -0
- package/src/composables/index.ts +16 -0
- package/src/composables/useAudioPlayer.ts +84 -0
- package/src/composables/useCallout.ts +103 -0
- package/src/composables/useCodeBlock.ts +90 -0
- package/src/composables/useGallery.ts +143 -0
- package/src/composables/useMedia.ts +23 -0
- package/src/composables/useYouTube.ts +58 -0
- package/src/edit/AlertEdit.vue +9 -0
- package/src/edit/AudioPlayerEdit.vue +92 -0
- package/src/edit/BadgeEdit.vue +7 -0
- package/src/edit/ButtonEdit.vue +10 -0
- package/src/edit/CalloutEdit.vue +107 -0
- package/src/edit/CardEdit.vue +9 -0
- package/src/edit/CodeBlockEdit.vue +76 -0
- package/src/edit/GalleryEdit.vue +65 -0
- package/src/edit/IconEdit.vue +7 -0
- package/src/edit/TooltipEdit.vue +15 -0
- package/src/edit/YouTubeEdit.vue +33 -0
- package/src/edit/index.ts +32 -0
- package/src/index.ts +62 -0
- package/src/registry.ts +272 -0
- package/src/render/alert/Alert.vue +32 -0
- package/src/render/alert/index.ts +2 -0
- package/src/render/audioplayer/AudioPlayerDefault.vue +89 -0
- package/src/render/audioplayer/index.ts +8 -0
- package/src/render/badge/Badge.vue +25 -0
- package/src/render/badge/index.ts +2 -0
- package/src/render/button/Button.vue +41 -0
- package/src/render/button/index.ts +2 -0
- package/src/render/callout/CalloutDefault.vue +101 -0
- package/src/render/callout/CalloutMinimal.vue +31 -0
- package/src/render/callout/index.ts +9 -0
- package/src/render/card/Card.vue +15 -0
- package/src/render/card/index.ts +2 -0
- package/src/render/codeblock/CodeBlockDefault.vue +76 -0
- package/src/render/codeblock/index.ts +8 -0
- package/src/render/gallery/GalleryGrid.vue +162 -0
- package/src/render/gallery/GalleryLightbox.vue +162 -0
- package/src/render/gallery/index.ts +9 -0
- package/src/render/icon/Icon.vue +22 -0
- package/src/render/icon/index.ts +2 -0
- package/src/render/index.ts +76 -0
- package/src/render/tooltip/Tooltip.vue +35 -0
- package/src/render/tooltip/index.ts +2 -0
- package/src/render/youtube/YouTubeDefault.vue +72 -0
- 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,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,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,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,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>
|