- 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
79 lines
3.4 KiB
Vue
79 lines
3.4 KiB
Vue
<script setup lang="ts">
|
|
// Sommaire — langage du gabarit : filet couleur 5 mm en haut, marges 8 mm,
|
|
// titre Syne 45 pt. La taille des entrées et leur disposition (colonne unique
|
|
// ou grille) s'adaptent au nombre de projets listés.
|
|
import type { Background } from '~~/types'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
title?: string
|
|
entries: { title: string; subtitle?: string; category?: string; pageNumber: number }[]
|
|
background: Background
|
|
bandColor?: string
|
|
page: { index: number; total: number }
|
|
renderMode?: 'preview' | 'print'
|
|
}>(),
|
|
{ title: 'Sommaire', renderMode: 'preview' },
|
|
)
|
|
|
|
const emit = defineEmits<{ navigate: [pageNumber: number] }>()
|
|
|
|
const strip = computed(() => props.bandColor || FL_INK)
|
|
const pageStyle = computed(() => backgroundStyle(props.background, strip.value))
|
|
const isPrint = computed(() => props.renderMode === 'print')
|
|
|
|
// En print, chaque entrée est un lien interne #page-N (converti en lien PDF par
|
|
// Chromium). En aperçu, le même clic défile jusqu'à la page dans la preview.
|
|
function hrefFor(n: number) {
|
|
return isPrint.value ? `#page-${n}` : `#preview-page-${n}`
|
|
}
|
|
function onEntryClick(n: number, e: MouseEvent) {
|
|
if (isPrint.value) return // laisse le lien natif au PDF
|
|
e.preventDefault()
|
|
e.stopPropagation() // n'active pas la sélection de la page-sommaire elle-même
|
|
emit('navigate', n)
|
|
}
|
|
|
|
// Peu de projets = gros titres sur une colonne ; beaucoup = grille dense.
|
|
const layout = computed(() => {
|
|
const n = props.entries.length
|
|
if (n <= 4) return { cols: 1, size: 48, sub: 18.7, gapX: 0, gapY: 44 }
|
|
if (n <= 8) return { cols: 1, size: 32, sub: 14.7, gapX: 0, gapY: 30 }
|
|
if (n <= 16) return { cols: 2, size: 24, sub: 13.3, gapX: 96, gapY: 24 }
|
|
if (n <= 26) return { cols: 2, size: 18.7, sub: 12, gapX: 96, gapY: 18 }
|
|
return { cols: 3, size: 14.7, sub: 10.7, gapX: 60, gapY: 14 }
|
|
})
|
|
|
|
const gridStyle = computed(() => ({
|
|
gridTemplateColumns: `repeat(${layout.value.cols}, minmax(0, 1fr))`,
|
|
columnGap: `${layout.value.gapX}px`,
|
|
rowGap: `${layout.value.gapY}px`,
|
|
gridAutoRows: 'min-content',
|
|
}))
|
|
</script>
|
|
|
|
<template>
|
|
<div class="fl-page" :style="pageStyle">
|
|
<div v-if="background.mode !== 'full'" class="absolute inset-x-0 top-0" :style="{ height: 'var(--fl-strip)', backgroundColor: strip }" />
|
|
|
|
<div class="absolute flex flex-col" :style="{ inset: 'var(--fl-margin)', top: '60px' }">
|
|
<h2 :style="{ fontSize: '60px', lineHeight: '1.05' }">{{ title }}</h2>
|
|
<div class="mt-14 grid min-h-0 flex-1 content-start" :style="gridStyle">
|
|
<a
|
|
v-for="entry in entries"
|
|
:key="`${entry.pageNumber}-${entry.title}`"
|
|
:href="hrefFor(entry.pageNumber)"
|
|
class="flex items-baseline gap-3 text-current no-underline"
|
|
:class="{ 'cursor-pointer': !isPrint }"
|
|
@click="onEntryClick(entry.pageNumber, $event)"
|
|
>
|
|
<span :class="CATEGORY_TITLE_CLASS[entry.category ?? '']" :style="{ fontSize: `${layout.size}px`, lineHeight: '1.2' }">{{ entry.title }}</span>
|
|
<span v-if="entry.subtitle" class="whitespace-nowrap opacity-70" :style="{ fontSize: `${layout.sub}px` }">{{ entry.subtitle }}</span>
|
|
<span class="flex-1 border-b border-dotted border-current opacity-40" />
|
|
<span class="tabular-nums opacity-80" :style="{ fontSize: `${layout.sub + 2}px` }">{{ entry.pageNumber }}</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|