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

96 lines
3.8 KiB
Vue

<script setup lang="ts">
// Page « couverture » de projet — gabarit_book_v3 p.1/3/5/7.
// Bandeau pleine largeur 55 mm : titre (fonte de la catégorie, 45 pt),
// pastille catégorie (10 pt), présentation (Syne 12/16 pt, colonne à 50,5 %).
// Sous le bandeau : image de couverture. Taille du titre et hauteur du
// bandeau (= calage haut de l'image) ajustables pour éviter les débords.
import type { InkMode, ResolvedMedia } from '~~/types'
const props = withDefaults(
defineProps<{
title: string
category: string // "publique" | "sociale" | "culturelle"
badge?: string // texte de la pastille (ex. « Utilité publique »)
intro?: string // ligne(s) d'intro en gras
text?: string // présentation, HTML
image?: ResolvedMedia
imageFit?: 'width' | 'height' // calage : pleine largeur ou pleine hauteur
imageAlign?: 'left' | 'center' | 'right' // fit hauteur : marge gauche / centrée / marge droite
titleScale?: number // % de la taille gabarit (100 = 45 pt)
bandHeight?: number // mm (gabarit : 55)
bandColor?: string
ink?: InkMode
}>(),
{ ink: 'auto', imageFit: 'width', imageAlign: 'center', titleScale: 100, bandHeight: 55 },
)
const PX_PER_MM = 3.7796
const band = computed(() => bandColorOf(props.category, props.bandColor))
const inkColor = computed(() => inkOn(band.value, props.ink))
const titleClass = computed(() => CATEGORY_TITLE_CLASS[props.category] ?? '')
// L'Avara est plus grasse : le gabarit la compose en 42 pt au lieu de 45 pt.
const titleSize = computed(() => {
const base = props.category === 'culturelle' ? 56 : 60
return `${(base * props.titleScale) / 100}px`
})
const bandPx = computed(() => `${props.bandHeight * PX_PER_MM}px`)
// Fit hauteur : calage horizontal sur la marge gauche, centré, ou marge droite
const alignStyle = computed(() => {
if (props.imageFit !== 'height') return { justifyContent: 'center' }
switch (props.imageAlign) {
case 'left': return { justifyContent: 'flex-start', paddingLeft: 'var(--fl-margin)' }
case 'right': return { justifyContent: 'flex-end', paddingRight: 'var(--fl-margin)' }
default: return { justifyContent: 'center' }
}
})
</script>
<template>
<div class="fl-page">
<!-- Bandeau -->
<div class="absolute inset-x-0 top-0" :style="{ height: bandPx, backgroundColor: band, color: inkColor }">
<h1
class="absolute"
:class="titleClass"
:style="{ left: 'var(--fl-margin)', top: '14px', fontSize: titleSize, lineHeight: '1.15', maxWidth: '620px' }"
>
{{ title }}
</h1>
<span
v-if="badge"
class="absolute whitespace-nowrap rounded-full border px-[12px] py-[3px]"
:style="{ left: '42.33%', top: '54px', transform: 'translateY(-50%)', fontSize: '13.3px', lineHeight: '1.2', borderColor: 'currentColor' }"
>
{{ badge }}
</span>
<div
class="absolute overflow-hidden"
:style="{ left: '50.47%', right: 'var(--fl-margin)', top: '33px', bottom: '12px' }"
>
<p v-if="intro" class="font-bold">{{ intro }}</p>
<div v-if="text" class="fl-prose" v-html="text" />
</div>
</div>
<!-- Image de couverture : calée sur la largeur (pleine largeur, fixée en
bas de page, débord haut coupé) ou sur la hauteur (pleine hauteur,
centrée). Le fond visible sous/à côté de l'image reprend la couleur
du bandeau. -->
<div
class="absolute inset-x-0 bottom-0 flex overflow-hidden"
:style="{ top: bandPx, backgroundColor: image ? band : '#ffffff', ...alignStyle }"
>
<img
v-if="image"
:src="image.url"
:alt="title"
:style="imageFit === 'height'
? { height: '100%', width: 'auto', maxWidth: 'none' }
: { width: '100%', height: 'auto', alignSelf: 'flex-end' }"
>
</div>
</div>
</template>