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

@@ -0,0 +1,30 @@
// Copie presse-papier robuste : l'API `navigator.clipboard` n'existe qu'en
// contexte sécurisé (HTTPS ou localhost) ; en HTTP sur IP LAN elle est absente,
// d'où un repli via un <textarea> temporaire + `document.execCommand('copy')`.
export async function copyToClipboard(text: string): Promise<boolean> {
if (import.meta.server) return false
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text)
return true
} catch {
// contexte sécurisé mais copie refusée → on tente le repli
}
}
try {
const ta = document.createElement('textarea')
ta.value = text
ta.setAttribute('readonly', '')
ta.style.position = 'fixed'
ta.style.top = '-9999px'
document.body.appendChild(ta)
ta.select()
const ok = document.execCommand('copy')
document.body.removeChild(ta)
return ok
} catch {
return false
}
}

View File

@@ -8,6 +8,25 @@ export function formatProjectDate(iso?: string): string {
return Number.isNaN(d.getTime()) ? '' : dateFormatter.format(d)
}
/** Mois + année capitalisés (ex. « Juillet 2026 ») ; sans argument = aujourd'hui.
* Sert de date par défaut de l'en-tête de couverture. */
export function formatMonthYear(iso?: string): string {
const d = iso ? new Date(iso) : new Date()
if (Number.isNaN(d.getTime())) return ''
const s = dateFormatter.format(d)
return s.charAt(0).toUpperCase() + s.slice(1)
}
/** Retire le paramètre `target=_blank` que Grav colle à chaque URL — dans une
* URL simple comme dans une chaîne HTML (href des liens du corps de texte). */
export function stripTargetParam(s?: string): string {
if (!s) return ''
return s
.replace(/\?target=_blank&/g, '?') // seul paramètre en tête, suivi d'autres
.replace(/&target=_blank/g, '') // paramètre en fin de chaîne de requête
.replace(/\?target=_blank\b/g, '') // seul paramètre
}
export const CATEGORY_LABELS: Record<string, string> = {
culturelle: 'Culturelle',
publique: 'Publique',

View File

@@ -5,39 +5,22 @@ import type { Background, InkMode } from '~~/types'
export const FL_INK = '#0e1229'
export const FL_BASE = '#f5f5f5'
/** Couleurs de bandeau du gabarit PDF (aplats CMJN convertis). */
export const PDF_COLORS = [
'#ec008c', // magenta 100% — utilité publique
'#8bd261', // vert C45 J71 — utilité sociale
'#fff766', // jaune 60% — utilité culturelle (bandeau)
'#fff200', // jaune 100% — utilité culturelle (filet)
/** Palette proposée pour la couleur du filet/bandeau des pages projet
* (magenta et vert du gabarit + roses/jaune/menthe du site). */
export const BAND_COLORS = [
'#ec008c', // magenta 100% (publique)
'#8bd261', // vert (sociale)
'#ffaeab', // rose (publique, site)
'#feff74', // jaune pâle (culturelle, site)
'#fabbde', // rose (commanditaires, site)
'#4bffc9', // menthe (projets, site)
]
/** Couleurs du site Grav (hover des keywords de l'index). */
export const SITE_COLORS = [
'#ffaeab', // publique
'#71ff94', // sociale
'#feff74', // culturelle
'#fabbde', // commanditaires
'#82f8ee', // figures libres
'#4bffc9', // projets
]
/** Palette proposée pour les bandeaux de pages projet. */
export const BAND_COLORS = [...PDF_COLORS, ...SITE_COLORS]
/** Couleur de bandeau par catégorie (page couverture de projet, gabarit). */
export const CATEGORY_BAND: Record<string, string> = {
publique: '#ec008c',
sociale: '#8bd261',
culturelle: '#fff766',
}
/** Couleur de filet par catégorie (pages grille — jaune à 100 %). */
export const CATEGORY_STRIP: Record<string, string> = {
publique: '#ec008c',
sociale: '#8bd261',
culturelle: '#fff200',
/** Libellé court de la pastille d'utilité (sans le mot « Utilité »). */
export const CATEGORY_BADGE: Record<string, string> = {
publique: 'Publique',
sociale: 'Social',
culturelle: 'Culturel',
}
/** Classe CSS de la fonte de titre par catégorie (mixins du thème Grav). */
@@ -47,18 +30,8 @@ export const CATEGORY_TITLE_CLASS: Record<string, string> = {
culturelle: 'fl-font-culturelle',
}
/** Aplats clairs proposés pour le mode « teinté ». */
export const TINT_PRESETS = [
'#f5f5f5', // gris clair (base)
'#efece3', // beige
'#ffeceb', // rose pâle
'#fbfbdc', // jaune pâle
'#e9fbee', // vert pâle
'#e2fff6', // menthe pâle
]
/** Pleines couleurs de la charte pour le mode « couleur ». */
export const COLOR_PRESETS = [FL_INK, ...PDF_COLORS, ...SITE_COLORS]
/** Aplat unique du mode « teinté » (beige de la charte). */
export const TINT_COLOR = '#efece3'
function luminance(hex: string): number {
const m = hex.replace('#', '')
@@ -74,22 +47,23 @@ export function inkOn(color: string, ink: InkMode = 'auto'): string {
return luminance(color) < 0.45 ? '#ffffff' : '#000000'
}
export function bandColorOf(category: string | undefined, override?: string): string {
return override || CATEGORY_BAND[category ?? ''] || FL_INK
/** Couleur du filet/bandeau : couleur choisie, sinon l'encre neutre (plus de
* couleur automatique par catégorie). */
export function filetColor(override?: string): string {
return override || FL_INK
}
export function stripColorOf(category: string | undefined, override?: string): string {
return override || CATEGORY_STRIP[category ?? ''] || FL_INK
}
export function backgroundColorOf(bg: Background): string {
if (bg.mode === 'white') return '#ffffff'
return bg.color || (bg.mode === 'tint' ? FL_BASE : FL_INK)
/** Couleur de fond d'une page : blanc, teinté (aplat fixe) ou plein (`fullColor`,
* la couleur du filet/bandeau de la page). */
export function backgroundColorOf(bg: Background | undefined, fullColor = FL_INK): string {
if (bg?.mode === 'full') return fullColor
if (bg?.mode === 'tint') return TINT_COLOR
return '#ffffff'
}
/** Style inline d'une page : fond + couleur d'encre lisible. */
export function backgroundStyle(bg: Background): Record<string, string> {
const color = backgroundColorOf(bg)
export function backgroundStyle(bg: Background | undefined, fullColor = FL_INK): Record<string, string> {
const color = backgroundColorOf(bg, fullColor)
return {
backgroundColor: color,
color: luminance(color) < 0.45 ? '#ffffff' : FL_INK,