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:
95
components/templates/ProjectCoverPage.vue
Normal file
95
components/templates/ProjectCoverPage.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user