Files
Valentin Le Moign 59dfbe4155 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>
2026-07-05 18:37:09 +02:00

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>