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

@@ -1,50 +1,31 @@
<script setup lang="ts">
// Fond de page (spec §7.2) : blanc / teinté (aplat clair) / pleine couleur.
import type { Background } from '~~/types'
// Fond de page unifié : Blanc / Teinté (aplat clair fixe) / Plein (couleur du
// filet/bandeau de la page). Undefined = Blanc par défaut.
import type { Background, BgMode } from '~~/types'
const model = defineModel<Background>({ required: true })
const model = defineModel<Background | undefined>()
const modes = [
{ value: 'white', label: 'Blanc' },
{ value: 'tint', label: 'Teinté' },
{ value: 'color', label: 'Couleur' },
{ value: 'full', label: 'Plein' },
] as const
function setMode(mode: Background['mode']) {
if (mode === 'white') model.value = { mode: 'white' }
else if (mode === 'tint') model.value = { mode: 'tint', color: model.value.mode === 'tint' ? model.value.color : TINT_PRESETS[0] }
else model.value = { mode: 'color', color: model.value.mode === 'color' ? model.value.color : COLOR_PRESETS[1] }
}
const swatches = computed(() =>
model.value.mode === 'tint' ? TINT_PRESETS : model.value.mode === 'color' ? COLOR_PRESETS : [],
)
const current = computed<BgMode>(() => model.value?.mode ?? 'white')
</script>
<template>
<div class="space-y-2">
<p class="text-xs font-medium uppercase tracking-wide text-neutral-500">Fond de page</p>
<p class="text-xs font-medium uppercase tracking-wide text-neutral-500">Fond</p>
<div class="flex gap-1">
<UButton
v-for="m in modes"
:key="m.value"
:label="m.label"
size="xs"
:variant="model.mode === m.value ? 'solid' : 'outline'"
:variant="current === m.value ? 'solid' : 'outline'"
color="neutral"
@click="setMode(m.value)"
/>
</div>
<div v-if="swatches.length" class="flex flex-wrap gap-1.5 pt-1">
<button
v-for="c in swatches"
:key="c"
type="button"
class="size-7 rounded-full border transition-transform hover:scale-110"
:class="(model as any).color === c ? 'ring-2 ring-[var(--fl-ink)] ring-offset-1 dark:ring-white' : 'border-neutral-300 dark:border-neutral-600'"
:style="{ backgroundColor: c }"
:aria-label="c"
@click="model = { mode: model.mode as 'tint' | 'color', color: c }"
@click="model = { mode: m.value }"
/>
</div>
</div>

View File

@@ -11,6 +11,9 @@ const props = withDefaults(
)
const model = defineModel<string | undefined>()
// Une couleur choisie hors palette = couleur personnalisée (input natif).
const isCustom = computed(() => !!model.value && !BAND_COLORS.includes(model.value))
</script>
<template>
@@ -24,7 +27,7 @@ const model = defineModel<string | undefined>()
class="relative size-7 rounded-full border transition-transform hover:scale-110"
:class="!model ? 'ring-2 ring-[var(--fl-ink)] ring-offset-1 dark:ring-white' : 'border-neutral-300 dark:border-neutral-600'"
:style="{ backgroundColor: props.defaultColor }"
title="Couleur de la catégorie (défaut)"
title="Couleur par défaut"
@click="model = undefined"
>
<UIcon name="i-lucide-rotate-ccw" class="absolute inset-0 m-auto size-3.5 text-black/40" />
@@ -39,6 +42,21 @@ const model = defineModel<string | undefined>()
:aria-label="c"
@click="model = c"
/>
<!-- Couleur personnalisée (sélecteur natif) -->
<label
class="relative flex size-7 cursor-pointer items-center justify-center overflow-hidden rounded-full border transition-transform hover:scale-110"
:class="isCustom ? 'ring-2 ring-[var(--fl-ink)] ring-offset-1 dark:ring-white' : 'border-neutral-300 dark:border-neutral-600'"
:style="isCustom ? { backgroundColor: model } : undefined"
title="Couleur personnalisée"
>
<UIcon v-if="!isCustom" name="i-lucide-pipette" class="size-3.5 text-neutral-400" />
<input
type="color"
class="absolute inset-0 cursor-pointer opacity-0"
:value="isCustom ? model : '#000000'"
@input="model = ($event.target as HTMLInputElement).value"
>
</label>
</div>
</div>
</template>