App Nuxt 3 + Nuxt UI qui lit les projets du CMS Grav et compose des portfolios A3 paysage exportés en PDF (gabarit book_v3) : composeur WYSIWYG, templates cover/toc/projet/grille/page libre, pipeline Playwright + Ghostscript, gel du contenu à la génération, partage public opt-in. Dépôt autonome : Dockerfile (base Playwright + Ghostscript + ffmpeg + rsync) inclus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
2.6 KiB
Vue
70 lines
2.6 KiB
Vue
<script setup lang="ts">
|
|
// Curation des médias d'une page grille (spec §8.2) : cocher/décocher +
|
|
// réordonner, sans recadrage. Images uniquement — les vidéos ne sont pas
|
|
// sélectionnables (un PDF est statique).
|
|
import draggable from 'vuedraggable'
|
|
import type { Project } from '~~/types'
|
|
|
|
const props = defineProps<{
|
|
project: Project
|
|
/** Nombre de cases de la grille choisie. */
|
|
max?: number
|
|
}>()
|
|
const selection = defineModel<string[]>({ required: true })
|
|
|
|
const images = computed(() => props.project.media.filter(m => m.type === 'image'))
|
|
|
|
function toggle(filename: string) {
|
|
if (selection.value.includes(filename)) {
|
|
selection.value = selection.value.filter(f => f !== filename)
|
|
} else if (!props.max || selection.value.length < props.max) {
|
|
selection.value = [...selection.value, filename]
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-3">
|
|
<p class="text-xs font-medium uppercase tracking-wide text-neutral-500">
|
|
Images de la grille ({{ selection.length }}{{ max ? ` / ${max} cases` : '' }})
|
|
</p>
|
|
|
|
<!-- Ordre des images sélectionnées (glisser-déposer) -->
|
|
<draggable
|
|
v-if="selection.length"
|
|
v-model="selection"
|
|
item-key="."
|
|
class="flex flex-wrap gap-2"
|
|
:animation="150"
|
|
>
|
|
<template #item="{ element, index }">
|
|
<div class="relative size-16 cursor-grab overflow-hidden rounded border border-neutral-300 active:cursor-grabbing dark:border-neutral-600">
|
|
<img :src="mediaUrl(project.id, element, 160)" class="size-full object-cover" :alt="element">
|
|
<span class="absolute left-0.5 top-0.5 flex size-4 items-center justify-center rounded-full bg-[var(--fl-ink)] text-[10px] text-white">{{ index + 1 }}</span>
|
|
</div>
|
|
</template>
|
|
</draggable>
|
|
|
|
<!-- Toutes les images du projet : clic = sélection/désélection -->
|
|
<div class="grid grid-cols-4 gap-1.5">
|
|
<button
|
|
v-for="m in images"
|
|
:key="m.filename"
|
|
type="button"
|
|
class="relative aspect-square overflow-hidden rounded border transition-opacity"
|
|
:class="selection.includes(m.filename)
|
|
? 'border-[var(--fl-ink)] dark:border-white'
|
|
: 'border-transparent opacity-45 hover:opacity-80'"
|
|
@click="toggle(m.filename)"
|
|
>
|
|
<img :src="mediaUrl(project.id, m.filename, 160)" class="size-full object-cover" :alt="m.filename">
|
|
<UIcon
|
|
v-if="selection.includes(m.filename)"
|
|
name="i-lucide-check-circle-2"
|
|
class="absolute right-1 top-1 size-4 rounded-full bg-white text-[var(--fl-ink)]"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|