Générateur de portfolios PDF Figures Libres (app Nuxt autonome)
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>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user