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

View File

@@ -57,30 +57,52 @@ const projectCoverImage = computed<ResolvedMedia | undefined>(() => {
const projectText = computed(() => {
if (props.page.kind !== 'project' || !project.value) return ''
if (props.page.textOverride?.trim()) return marked.parse(props.page.textOverride) as string
return project.value.bodyHtml
// `textOverride` undefined = suit Grav (bodyHtml) ; toute chaîne définie
// (y compris vide) est un texte figé à l'édition et rendue telle quelle.
const override = props.page.textOverride
const html = override === undefined
? project.value.bodyHtml
: (marked.parse(override) as string)
return stripTargetParam(html)
})
const projectIntro = computed(() => {
if (props.page.kind !== 'project' || !project.value) return ''
return props.page.subtitleOverride?.trim() || project.value.categories.join(', ')
// Ligne d'info de la couverture de projet : type de travail + année.
// Type par défaut = taxonomy.category de Grav (« Site web », « Édition »…),
// c.-à-d. Project.categories — surchargée par subtitleOverride si renseignée.
const projectWorkType = computed(() => {
if (props.page.kind !== 'project') return ''
return props.page.subtitleOverride?.trim() || project.value?.categories.join(', ') || ''
})
const projectYear = computed(() => {
if (props.page.kind !== 'project') return ''
if (props.page.year?.trim()) return props.page.year.trim()
const iso = project.value?.date
const y = iso ? new Date(iso).getFullYear() : NaN
return Number.isNaN(y) ? '' : String(y)
})
const projectLink = computed(() => {
if (props.page.kind !== 'project') return ''
return stripTargetParam(props.page.link?.trim() || project.value?.externalUrl || '')
})
const projectBadge = computed(() => {
const c = project.value?.category
return c ? `Utilité ${CATEGORY_LABELS[c]?.toLowerCase() ?? c}` : undefined
return c ? CATEGORY_BADGE[c] ?? c : undefined
})
// ---- Page grille de projet ----
// Le filet hérite de la couleur du bandeau de la page couverture du projet.
const gridStripOverride = computed(() => {
// Filet et mode clair hérités de la page couverture du même projet.
const gridCover = computed(() => {
if (props.page.kind !== 'project-grid') return undefined
const projectId = props.page.projectId
const cover = props.portfolio.pages.find(
return props.portfolio.pages.find(
(p): p is Extract<Page, { kind: 'project' }> => p.kind === 'project' && p.projectId === projectId,
)
return cover?.bandColor
})
// Le filet des grilles hérite de la couleur du bandeau de la couverture du projet.
const gridStripOverride = computed(() => gridCover.value?.bandColor)
// Orientation dominante des images posées : la grille adaptative en dépend
const gridOrientation = computed(() => {
@@ -132,7 +154,7 @@ const tocEntries = computed(() =>
// Page libre : texte markdown + médias "projectId/filename"
const freeText = computed(() => {
if (props.page.kind !== 'free' || !props.page.body?.trim()) return ''
return marked.parse(props.page.body) as string
return stripTargetParam(marked.parse(props.page.body) as string)
})
const freeMedia = computed<ResolvedMedia[]>(() => {
@@ -149,6 +171,9 @@ const freeMedia = computed<ResolvedMedia[]>(() => {
<CoverTemplate
v-if="page.kind === 'cover'"
:title="portfolio.cover.title"
:date="portfolio.cover.date"
:site-url="portfolio.cover.siteUrl"
:contact-email="portfolio.cover.contactEmail"
:background="page.background"
:band-color="page.bandColor"
:render-mode="renderMode"
@@ -171,7 +196,8 @@ const freeMedia = computed<ResolvedMedia[]>(() => {
:text="freeText"
:media="freeMedia"
:background="page.background"
:band-color="page.bandColor"
:band-color="page.bandColor ?? coverBandColor"
:text-scale="page.textScale"
:page="pageInfo"
:render-mode="renderMode"
/>
@@ -185,7 +211,10 @@ const freeMedia = computed<ResolvedMedia[]>(() => {
:title="project.title"
:category="project.category"
:badge="projectBadge"
:intro="projectIntro"
:work-type="projectWorkType"
:year="projectYear"
:subtitle="page.subtitle"
:link="projectLink"
:text="projectText"
:image="projectCoverImage"
:image-fit="page.coverFit"
@@ -194,6 +223,8 @@ const freeMedia = computed<ResolvedMedia[]>(() => {
:band-height="page.bandHeight"
:band-color="page.bandColor"
:ink="page.ink"
:background="page.background"
:layout="page.layout"
/>
</template>
@@ -206,9 +237,8 @@ const freeMedia = computed<ResolvedMedia[]>(() => {
:media="gridMedia"
:grid="page.grid"
:orientation="gridOrientation"
:category="project.category"
:band-color="gridStripOverride"
:grey-bg="page.greyBg"
:background="page.background"
/>
</template>
</template>