- Champ Présentation prérempli avec le texte Grav, suivi modifié/synchro (textOverride undefined = suit Grav, sinon figé ; bouton Réinitialiser) - Fix génération PDF : savePortfolio écrit en atomique (temp+rename) pour éviter la corruption de lecture par l'autosave concurrent du rendu print - Toast d'échec de génération explicite (message serveur) - Refonte architecture visuelle, fond unifié white/tint/full, dispositions couverture bandeau/colonne, instantané public complet, index 3 lignes
175 lines
9.0 KiB
Vue
175 lines
9.0 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 { Background, InkMode, ResolvedMedia } from '~~/types'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
title: string
|
|
category: string // "publique" | "sociale" | "culturelle"
|
|
badge?: string // pastille d'utilité (ex. « Culturel »)
|
|
workType?: string // type du projet, ligne d'info (ex. « édition »)
|
|
year?: string // année du projet, ligne d'info
|
|
subtitle?: string // sous-titre sous le titre (fonte du titre, plus petit)
|
|
link?: string // lien du projet sous le titre (taille labeur, actif PDF/aperçu)
|
|
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
|
|
background?: Background // fond : blanc / teinté (filet fin) / plein (bandeau ou colonne coloré)
|
|
layout?: 'band' | 'split' // 'band' = bandeau haut ; 'split' = colonne texte + image droite
|
|
}>(),
|
|
{ ink: 'auto', imageFit: 'width', imageAlign: 'center', titleScale: 100, bandHeight: 55, layout: 'band' },
|
|
)
|
|
|
|
const PX_PER_MM = 3.7796
|
|
|
|
// Couleur choisie du projet (filet / bandeau). Fond « plein » = bandeau/colonne
|
|
// à cette couleur ; sinon fond blanc/teinté avec un fin filet.
|
|
const catColor = computed(() => filetColor(props.bandColor))
|
|
const isFull = computed(() => props.background?.mode === 'full')
|
|
// Fond de page (blanc ou teinté) — sous le bandeau clair et autour de l'image.
|
|
const pageBg = computed(() => (props.background?.mode === 'tint' ? TINT_COLOR : '#ffffff'))
|
|
|
|
const bandBg = computed(() => (isFull.value ? catColor.value : pageBg.value))
|
|
const inkColor = computed(() => (isFull.value ? inkOn(catColor.value, props.ink) : '#000000'))
|
|
// Pastille pleine, sans contour. Fond plein : fond = couleur du texte, texte
|
|
// opposé. Sinon : fond = couleur du projet, texte lisible dessus.
|
|
const pillBg = computed(() => (isFull.value ? inkColor.value : catColor.value))
|
|
const pillInk = computed(() =>
|
|
isFull.value ? (inkColor.value === '#ffffff' ? '#000000' : '#ffffff') : inkOn(catColor.value),
|
|
)
|
|
// Disposition « colonne ». Clair : texte sur le fond de page, fin filet en haut.
|
|
// Fond plein : la colonne prend la couleur du projet, texte contrasté (pas de filet).
|
|
const splitBg = computed(() => (isFull.value ? catColor.value : pageBg.value))
|
|
const splitInk = computed(() => (isFull.value ? inkOn(catColor.value, props.ink) : inkOn(pageBg.value)))
|
|
const splitPillBg = computed(() => (isFull.value ? splitInk.value : catColor.value))
|
|
const splitPillInk = computed(() =>
|
|
isFull.value ? (splitInk.value === '#ffffff' ? '#000000' : '#ffffff') : inkOn(catColor.value),
|
|
)
|
|
// Lien affiché sans le protocole
|
|
const linkLabel = computed(() => props.link?.replace(/^https?:\/\//, '').replace(/\/$/, ''))
|
|
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>
|
|
<!-- Disposition « colonne + image » : texte à gauche, image pleine hauteur à droite -->
|
|
<div v-if="layout === 'split'" class="fl-page" :style="{ backgroundColor: pageBg }">
|
|
<div class="absolute inset-0 flex">
|
|
<!-- Colonne gauche : filet (mode clair) ou fond plein coloré, infos, titre, sous-titre, lien, description -->
|
|
<div class="flex min-w-0 flex-1 flex-col" :style="{ backgroundColor: splitBg, color: splitInk }">
|
|
<div v-if="bandColor && !isFull" class="shrink-0" :style="{ height: 'var(--fl-strip-light)', backgroundColor: catColor }" />
|
|
<div class="flex min-h-0 flex-1 flex-col" :style="{ padding: 'var(--fl-margin)' }">
|
|
<div class="flex items-center whitespace-nowrap" :style="{ gap: '12px', fontSize: '13.3px', lineHeight: '1.2' }">
|
|
<span
|
|
v-if="badge"
|
|
class="rounded-full px-[12px] py-[3px]"
|
|
:style="{ backgroundColor: splitPillBg, color: splitPillInk }"
|
|
>{{ badge }}</span>
|
|
<span v-if="workType">{{ workType }}</span>
|
|
<span v-if="year" class="opacity-70">{{ year }}</span>
|
|
</div>
|
|
<h1 :class="titleClass" :style="{ fontSize: titleSize, lineHeight: '1.05', marginTop: '16px' }">{{ title }}</h1>
|
|
<p v-if="subtitle" :class="titleClass" :style="{ fontSize: `calc(${titleSize} * 0.5)`, lineHeight: '1.2', marginTop: '4px' }">{{ subtitle }}</p>
|
|
<a
|
|
v-if="link"
|
|
:href="link"
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="text-current no-underline"
|
|
:style="{ marginTop: '8px' }"
|
|
>{{ linkLabel }}</a>
|
|
<div v-if="text" class="fl-prose min-h-0 overflow-hidden" :style="{ marginTop: '28px' }" v-html="text" />
|
|
</div>
|
|
</div>
|
|
<!-- Image pleine hauteur, rognée au-delà de ~70 % de la largeur, centrée horizontalement -->
|
|
<div v-if="image" class="flex h-full shrink-0 justify-center overflow-hidden" :style="{ maxWidth: '70%' }">
|
|
<img :src="image.url" :alt="title" :style="{ height: '100%', width: 'auto', maxWidth: 'none' }">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Disposition « bandeau » (par défaut) -->
|
|
<div v-else class="fl-page" :style="{ backgroundColor: pageBg }">
|
|
<!-- Bandeau (pleine couleur) ou fond de page (mode clair) -->
|
|
<div class="absolute inset-x-0 top-0" :style="{ height: bandPx, backgroundColor: bandBg, color: inkColor }">
|
|
<!-- Mode clair : la couleur choisie n'est plus qu'un filet fin en haut -->
|
|
<div v-if="!isFull" class="absolute inset-x-0 top-0" :style="{ height: 'var(--fl-strip-light)', backgroundColor: catColor }" />
|
|
|
|
<!-- Ligne d'info au-dessus du titre (pastille · type · année) -->
|
|
<div
|
|
class="absolute flex items-center whitespace-nowrap"
|
|
:style="{ left: 'var(--fl-margin)', right: '51%', top: '34px', gap: '12px', fontSize: '13.3px', lineHeight: '1.2' }"
|
|
>
|
|
<span
|
|
v-if="badge"
|
|
class="rounded-full px-[12px] py-[3px]"
|
|
:style="{ backgroundColor: pillBg, color: pillInk }"
|
|
>{{ badge }}</span>
|
|
<span v-if="workType">{{ workType }}</span>
|
|
<span v-if="year" class="opacity-70">{{ year }}</span>
|
|
</div>
|
|
|
|
<!-- Titre (+ sous-titre, lien) et présentation, alignés en haut sous la ligne d'info -->
|
|
<div class="absolute flex" :style="{ left: 'var(--fl-margin)', right: 'var(--fl-margin)', top: '66px', bottom: '12px', gap: 'var(--fl-gutter)' }">
|
|
<div class="flex flex-col" :style="{ flex: '0 0 47%' }">
|
|
<h1 :class="titleClass" :style="{ fontSize: titleSize, lineHeight: '1.05' }">{{ title }}</h1>
|
|
<p v-if="subtitle" :class="titleClass" :style="{ fontSize: `calc(${titleSize} * 0.5)`, lineHeight: '1.2', marginTop: '4px' }">{{ subtitle }}</p>
|
|
<a
|
|
v-if="link"
|
|
:href="link"
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="text-current no-underline"
|
|
:style="{ marginTop: '8px' }"
|
|
>{{ linkLabel }}</a>
|
|
</div>
|
|
<div class="flex-1 overflow-hidden" :style="{ marginTop: '6px' }">
|
|
<div v-if="text" class="fl-prose" v-html="text" />
|
|
</div>
|
|
</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 (blanc en mode clair). -->
|
|
<div
|
|
class="absolute inset-x-0 bottom-0 flex overflow-hidden"
|
|
:style="{ top: bandPx, backgroundColor: isFull ? catColor : pageBg, ...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>
|