Files
portfolio-generator-app/components/templates/TocTemplate.vue
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

78 lines
3.3 KiB
Vue

<script setup lang="ts">
// Sommaire — langage du gabarit : filet couleur 5 mm en haut, marges 8 mm,
// titre Syne 45 pt. La taille des entrées et leur disposition (colonne unique
// ou grille) s'adaptent au nombre de projets listés.
import type { Background } from '~~/types'
const props = withDefaults(
defineProps<{
title?: string
entries: { title: string; subtitle?: string; category?: string; pageNumber: number }[]
background: Background
bandColor?: string
page: { index: number; total: number }
renderMode?: 'preview' | 'print'
}>(),
{ title: 'Sommaire', renderMode: 'preview' },
)
const emit = defineEmits<{ navigate: [pageNumber: number] }>()
const strip = computed(() => props.bandColor || FL_INK)
const isPrint = computed(() => props.renderMode === 'print')
// En print, chaque entrée est un lien interne #page-N (converti en lien PDF par
// Chromium). En aperçu, le même clic défile jusqu'à la page dans la preview.
function hrefFor(n: number) {
return isPrint.value ? `#page-${n}` : `#preview-page-${n}`
}
function onEntryClick(n: number, e: MouseEvent) {
if (isPrint.value) return // laisse le lien natif au PDF
e.preventDefault()
e.stopPropagation() // n'active pas la sélection de la page-sommaire elle-même
emit('navigate', n)
}
// Peu de projets = gros titres sur une colonne ; beaucoup = grille dense.
const layout = computed(() => {
const n = props.entries.length
if (n <= 4) return { cols: 1, size: 48, sub: 18.7, gapX: 0, gapY: 44 }
if (n <= 8) return { cols: 1, size: 32, sub: 14.7, gapX: 0, gapY: 30 }
if (n <= 16) return { cols: 2, size: 24, sub: 13.3, gapX: 96, gapY: 24 }
if (n <= 26) return { cols: 2, size: 18.7, sub: 12, gapX: 96, gapY: 18 }
return { cols: 3, size: 14.7, sub: 10.7, gapX: 60, gapY: 14 }
})
const gridStyle = computed(() => ({
gridTemplateColumns: `repeat(${layout.value.cols}, minmax(0, 1fr))`,
columnGap: `${layout.value.gapX}px`,
rowGap: `${layout.value.gapY}px`,
gridAutoRows: 'min-content',
}))
</script>
<template>
<div class="fl-page" :style="backgroundStyle(background)">
<div class="absolute inset-x-0 top-0" :style="{ height: 'var(--fl-strip)', backgroundColor: strip }" />
<div class="absolute flex flex-col" :style="{ inset: 'var(--fl-margin)', top: '60px' }">
<h2 :style="{ fontSize: '60px', lineHeight: '1.1' }">{{ title }}</h2>
<div class="mt-14 grid min-h-0 flex-1 content-start" :style="gridStyle">
<a
v-for="entry in entries"
:key="`${entry.pageNumber}-${entry.title}`"
:href="hrefFor(entry.pageNumber)"
class="flex items-baseline gap-3 text-current no-underline"
:class="{ 'cursor-pointer': !isPrint }"
@click="onEntryClick(entry.pageNumber, $event)"
>
<span :class="CATEGORY_TITLE_CLASS[entry.category ?? '']" :style="{ fontSize: `${layout.size}px`, lineHeight: '1.2' }">{{ entry.title }}</span>
<span v-if="entry.subtitle" class="whitespace-nowrap opacity-70" :style="{ fontSize: `${layout.sub}px` }">{{ entry.subtitle }}</span>
<span class="flex-1 border-b border-dotted border-current opacity-40" />
<span class="tabular-nums opacity-80" :style="{ fontSize: `${layout.sub + 2}px` }">{{ entry.pageNumber }}</span>
</a>
</div>
</div>
</div>
</template>