|
| 1 | +<!-- .vitepress/theme/components/PImg.vue --> |
| 2 | +<script setup lang="ts"> |
| 3 | +import { ref, computed, useSlots } from 'vue' |
| 4 | +import { withBase } from 'vitepress' |
| 5 | +
|
| 6 | +interface Props { |
| 7 | + src: string |
| 8 | + alt?: string |
| 9 | + title?: string |
| 10 | + width?: string | number |
| 11 | + height?: string | number |
| 12 | + align?: 'left' | 'center' | 'right' |
| 13 | + inline?: boolean // New inline prop |
| 14 | + loading?: "lazy" | "eager" |
| 15 | +} |
| 16 | +
|
| 17 | +const props = withDefaults(defineProps<Props>(), { |
| 18 | + loading: "lazy", |
| 19 | + align: 'center', |
| 20 | + inline: false |
| 21 | +}) |
| 22 | +
|
| 23 | +const slots = useSlots() |
| 24 | +const hasCaption = computed(() => !!slots.default) |
| 25 | +
|
| 26 | +// --- AUTOMATIC META --- |
| 27 | +const autoAlt = computed(() => { |
| 28 | + if (props.alt) return props.alt; |
| 29 | + const filename = props.src.split('/').pop()?.split('.')[0] || ''; |
| 30 | + return filename ? `Image of ${filename.replace(/[-_]/g, ' ').trim()}` : 'Image description not available'; |
| 31 | +}); |
| 32 | +
|
| 33 | +
|
| 34 | +const autoTitle = computed(() => props.title || autoAlt.value) |
| 35 | +
|
| 36 | +// --- DIMENSION DETECTION --- |
| 37 | +const autoWidth = ref<number | string | undefined>(props.width) |
| 38 | +const autoHeight = ref<number | string | undefined>(props.height) |
| 39 | +
|
| 40 | +const handleLoad = (event: Event) => { |
| 41 | + const img = event.target as HTMLImageElement |
| 42 | + if (!props.width) autoWidth.value = img.naturalWidth |
| 43 | + if (!props.height) autoHeight.value = img.naturalHeight |
| 44 | +} |
| 45 | +
|
| 46 | +// --- INLINE STYLES --- |
| 47 | +const containerStyle = computed(() => ({ |
| 48 | + display: props.inline ? 'inline-flex' : 'flex', |
| 49 | + flexDirection: 'column' as const, |
| 50 | + alignItems: props.align === 'left' ? 'flex-start' : props.align === 'right' ? 'flex-end' : 'center', |
| 51 | + width: props.inline ? 'auto' : '100%', |
| 52 | + margin: props.inline ? '0 0.5rem' : '1.5rem 0', |
| 53 | + verticalAlign: 'middle' |
| 54 | +})) |
| 55 | +
|
| 56 | +const imgStyle = computed(() => ({ |
| 57 | + maxWidth: '100%', |
| 58 | + height: 'auto', |
| 59 | + borderRadius: '0.75rem', |
| 60 | + boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)', |
| 61 | + aspectRatio: autoWidth.value && autoHeight.value ? `${autoWidth.value}/${autoHeight.value}` : 'auto', |
| 62 | + display: 'block', |
| 63 | + objectFit: 'cover' as const, |
| 64 | +})) |
| 65 | +
|
| 66 | +const captionStyle = { |
| 67 | + marginTop: '0.5rem', |
| 68 | + fontSize: '0.85rem', |
| 69 | + color: '#666666', |
| 70 | + fontStyle: 'italic' |
| 71 | +} |
| 72 | +</script> |
| 73 | + |
| 74 | +<template> |
| 75 | + <figure :style="containerStyle" itemscope itemtype="https://schema.org/ImageObject"> |
| 76 | + <img :src="withBase(src)" :alt="autoAlt" :title="autoTitle" :width="autoWidth" :height="autoHeight" |
| 77 | + :loading="loading" @load="handleLoad" :style="imgStyle" itemprop="image" /> |
| 78 | + |
| 79 | + <figcaption v-if="hasCaption && !inline" :style="captionStyle" itemprop="caption"> |
| 80 | + <slot /> |
| 81 | + </figcaption> |
| 82 | + |
| 83 | + <meta itemprop="contentUrl" :content="withBase(src)"> |
| 84 | + <meta itemprop="name" content="Image Title" /> |
| 85 | + <meta itemprop="description" content="A brief description of the image." /> |
| 86 | + <meta itemprop="author" content="AvN Learn" /> |
| 87 | + <meta itemprop="dateCreated" content="2026-03-14" /> |
| 88 | + </figure> |
| 89 | +</template> |
0 commit comments