Textes projet éditables suivant Grav + écriture atomique du store

- 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
This commit is contained in:
2026-07-06 13:26:06 +02:00
parent 59dfbe4155
commit ef1f14335d
22 changed files with 750 additions and 334 deletions
+39 -19
View File
@@ -7,28 +7,59 @@ import type { Background } from '~~/types'
const props = withDefaults(
defineProps<{
title: string // utilisé pour les métadonnées PDF, pas affiché
title: string // titre du portfolio — en-tête + métadonnées PDF
subtitle?: string
recipient?: string
background: Background
bandColor?: string // filet couleur en bas (définit aussi celui du sommaire)
date?: string // texte libre de l'en-tête (ex. « Juillet 2026 »)
siteUrl?: string
contactEmail?: string
renderMode?: 'preview' | 'print'
}>(),
{ renderMode: 'preview' },
)
// Le site affiche l'intro sur fond gris clair : « Blanc » rend le gris charte.
const strip = computed(() => props.bandColor || FL_INK)
// Fond : « Plein » = couleur du filet ; « Blanc » rend le gris charte du site
// (l'intro y est présentée sur gris clair) ; « Teinté » = aplat beige.
const pageStyle = computed(() => {
const s = backgroundStyle(props.background)
if (props.background.mode === 'white') s.backgroundColor = FL_BASE
return s
const base
= props.background.mode === 'full'
? strip.value
: props.background.mode === 'tint'
? TINT_COLOR
: FL_BASE
return { backgroundColor: base, color: inkOn(base) }
})
const strip = computed(() => props.bandColor || FL_INK)
// En-tête : coordonnées en liens actifs (le PDF et l'aperçu partagent le
// template ; Chromium convertit les <a href> en liens cliquables du PDF).
const siteHref = computed(() => props.siteUrl?.trim() || DEFAULT_SITE_URL)
const siteLabel = computed(() => siteHref.value.replace(/^https?:\/\//, '').replace(/\/$/, ''))
const email = computed(() => props.contactEmail?.trim() || DEFAULT_CONTACT_EMAIL)
// Laissée vide, la date affiche par défaut le mois courant (ex. « Juillet 2026 »).
const displayDate = computed(() => props.date?.trim() || formatMonthYear())
</script>
<template>
<div class="fl-page" :style="pageStyle">
<!-- En-tête : titre du portfolio + date, site et mail (liens actifs PDF/viewer) -->
<header
class="absolute inset-x-0 top-0 flex items-start justify-between gap-8"
:style="{ padding: 'var(--fl-margin)', fontFamily: 'Syne, sans-serif' }"
>
<div class="max-w-[60%]">
<h1 :style="{ fontSize: '30px', lineHeight: '1.2' }">{{ title }}</h1>
<p class="opacity-70" :style="{ fontSize: '19px', lineHeight: '1.5' }">{{ displayDate }}</p>
</div>
<div class="flex shrink-0 flex-col items-end" :style="{ fontSize: '19px', lineHeight: '1.5' }">
<a :href="siteHref" target="_blank" rel="noopener" class="text-current no-underline">{{ siteLabel }}</a>
<a :href="`mailto:${email}`" class="text-current no-underline">{{ email }}</a>
</div>
</header>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<!-- Phrase d'intro du site (pages/01.home) — Lato, keywords dans les
fontes d'« utilité » du thème Grav, non soulignés -->
@@ -40,20 +71,9 @@ const strip = computed(() => props.bandColor || FL_INK)
<span class="fl-font-culturelle">culturelle</span>
qui œuvre sur logiciels&nbsp;libres.
</p>
<!-- Flèche vers le bas (theme://images/pictos/arrow-down.svg) -->
<svg viewBox="0 0 24 30" :style="{ width: '60px', height: '54px', marginTop: '90px' }" aria-hidden="true">
<g transform="rotate(90,11,12.999996)">
<path
d="m 16.681932,5.2684395 0.08629,0.091376 5.042819,6.0548205 v 0 l 0.06362,0.100098 v 0 l 0.06561,0.144066 v 0 l 0.0389,0.137168 v 0 l 0.01717,0.117449 v 0 L 22,12 v 0 l -0.0025,0.07061 v 0 l -0.01594,0.12126 v 0 l -0.02359,0.09573 -0.0317,0.08965 v 0 l -0.06326,0.128377 v 0 l -0.09484,0.134549 -5,6 c -0.353564,0.424277 -0.984128,0.481601 -1.408405,0.128037 C 14.968124,18.441846 14.889154,17.879455 15.15741,17.46116 L 15.231779,17.359816 18.864,12.999975 3,13 C 2.4477153,13 2,12.552285 2,12 2,11.487164 2.3860402,11.064493 2.8833789,11.006728 L 3,11 18.864,10.999975 15.231779,6.6401844 c -0.353564,-0.4242769 -0.29624,-1.0548416 0.128037,-1.4084057 0.39164,-0.3263668 0.959052,-0.3026278 1.322116,0.036661 z"
fill="currentColor"
fill-rule="nonzero"
/>
</g>
</svg>
</div>
<!-- Filet couleur pleine largeur en bas -->
<div class="absolute inset-x-0 bottom-0" :style="{ height: 'var(--fl-strip)', backgroundColor: strip }" />
<!-- Filet couleur pleine largeur en bas (masqué en fond plein) -->
<div v-if="background.mode !== 'full'" class="absolute inset-x-0 bottom-0" :style="{ height: 'var(--fl-strip)', backgroundColor: strip }" />
</div>
</template>