feat(05-02): add Columns/Details/Video/Badge MDC components + full showcase article

This commit is contained in:
2026-04-21 15:31:00 +02:00
parent b63869f042
commit 60e05f7a56
5 changed files with 386 additions and 23 deletions
+27
View File
@@ -0,0 +1,27 @@
<script setup lang="ts">
interface Props {
color?: 'gray' | 'blue' | 'green' | 'red' | 'yellow' | 'purple' | 'orange'
}
const props = withDefaults(defineProps<Props>(), { color: 'gray' })
const colorClass = {
gray: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300',
blue: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
green: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
red: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
yellow: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
purple: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300',
orange: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300',
}
</script>
<template>
<span
:class="[
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium font-mono',
colorClass[props.color],
]"
>
<slot />
</span>
</template>
+22
View File
@@ -0,0 +1,22 @@
<script setup lang="ts">
interface Props {
cols?: 2 | 3 | 4
gap?: 'sm' | 'md' | 'lg'
}
const props = withDefaults(defineProps<Props>(), {
cols: 2,
gap: 'md',
})
const gridClass = computed(() => {
const cols = { 2: 'md:grid-cols-2', 3: 'md:grid-cols-3', 4: 'md:grid-cols-4' }[props.cols]
const gap = { sm: 'gap-4', md: 'gap-6', lg: 'gap-10' }[props.gap]
return `not-prose my-6 grid grid-cols-1 ${cols} ${gap}`
})
</script>
<template>
<div :class="gridClass">
<slot />
</div>
</template>
+48
View File
@@ -0,0 +1,48 @@
<script setup lang="ts">
interface Props {
summary?: string
open?: boolean
}
const props = withDefaults(defineProps<Props>(), {
summary: 'Voir plus',
open: false,
})
</script>
<template>
<details
:open="props.open"
class="not-prose my-4 rounded-lg border border-neutral-200 dark:border-neutral-800 overflow-hidden"
>
<summary
class="flex cursor-pointer select-none items-center justify-between px-4 py-3
text-sm font-medium text-neutral-700 dark:text-neutral-300
hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors list-none"
>
{{ props.summary }}
<svg
class="size-4 shrink-0 text-neutral-400 transition-transform details-arrow"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
</summary>
<div class="px-4 py-3 prose prose-neutral dark:prose-invert max-w-none
prose-code:before:content-none prose-code:after:content-none
prose-pre:p-0 prose-pre:bg-transparent text-sm">
<slot />
</div>
</details>
</template>
<style scoped>
details[open] .details-arrow {
transform: rotate(180deg);
}
</style>
+77
View File
@@ -0,0 +1,77 @@
<script setup lang="ts">
interface Props {
src: string
title?: string
aspect?: '16/9' | '4/3' | '1/1'
autoplay?: boolean
loop?: boolean
muted?: boolean
}
const props = withDefaults(defineProps<Props>(), {
title: '',
aspect: '16/9',
autoplay: false,
loop: false,
muted: false,
})
const isYoutube = computed(() =>
/youtube\.com|youtu\.be/.test(props.src)
)
const youtubeId = computed(() => {
const match = props.src.match(
/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
)
return match?.[1] ?? ''
})
const youtubeUrl = computed(() => {
const params = new URLSearchParams({
...(props.autoplay ? { autoplay: '1' } : {}),
...(props.loop ? { loop: '1', playlist: youtubeId.value } : {}),
modestbranding: '1',
rel: '0',
})
return `https://www.youtube.com/embed/${youtubeId.value}?${params}`
})
const aspectClass = computed(() => ({
'16/9': 'aspect-video',
'4/3': 'aspect-[4/3]',
'1/1': 'aspect-square',
}[props.aspect]))
</script>
<template>
<figure class="not-prose my-6 w-full overflow-hidden rounded-lg bg-black">
<!-- YouTube embed -->
<iframe
v-if="isYoutube"
:src="youtubeUrl"
:title="props.title"
:class="['w-full', aspectClass]"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
loading="lazy"
/>
<!-- Local video -->
<video
v-else
:src="props.src"
:title="props.title"
:class="['w-full', aspectClass]"
:autoplay="props.autoplay"
:loop="props.loop"
:muted="props.muted || props.autoplay"
controls
playsinline
/>
<figcaption
v-if="props.title"
class="bg-neutral-900 px-4 py-2 text-center text-xs text-neutral-400 italic"
>
{{ props.title }}
</figcaption>
</figure>
</template>