feat(05-02): add Columns/Details/Video/Badge MDC components + full showcase article
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user