diff --git a/CLAUDE.md b/CLAUDE.md index 6ef48d7..0669f03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,11 +40,18 @@ Mot de passe dev : `figureslibres` (`PORTFOLIO_PASSWORD`). ## Modèle de données (résumé) Un `Portfolio` = `cover` (CoverConfig) + `pages[]`. Types de page : `cover`, -`toc`, `project` (couverture bandeau + image), `project-grid` (grille d'images -entières), `free`. Le contenu Grav (`Project`, `MediaAsset`) est **en lecture -seule**. À la génération, le contenu utilisé est **figé** dans `data/frozen//` -(`FrozenSnapshot`) : régénérer redonne le même PDF ; « Réactualiser depuis Grav » -resynchronise volontairement. +`toc`, `project` (couverture d'un projet, disposition `band` = bandeau haut **ou** +`split` = colonne texte + image pleine hauteur à droite), `project-grid` (grille +d'images entières), `free`. Le contenu Grav (`Project`, `MediaAsset`) est **en +lecture seule**. À la génération, le contenu utilisé est **figé** dans +`data/frozen//` (`FrozenSnapshot`) : régénérer redonne le même PDF ; +« Réactualiser depuis Grav » resynchronise volontairement. + +Chaque page porte un **fond unifié** `background` à trois modes : +`white` | `tint` (aplat beige fixe `#efece3`) | `full` (la page — ou le bandeau / +la colonne d'une couverture projet — prend la couleur du filet `bandColor`, défaut +encre `FL_INK`). Il n'y a **plus** de couleur automatique par catégorie +(`bandColorOf`/`stripColorOf` retombent sur `FL_INK`). ## Contraintes fermes @@ -53,6 +60,19 @@ resynchronise volontairement. - Les vidéos ne sont pas sélectionnables dans les grilles. - Format PDF : A3 paysage aux dimensions exactes du gabarit. +## Conventions de rendu (templates) + +- **Fond `background`** : `useTheme` → `backgroundColorOf(bg, fullColor)` / + `backgroundStyle(bg, fullColor)`. Le mode `full` **dérive** sa couleur du filet + au rendu (`fullColor`) — ne rien stocker en dur ; le `BackgroundEditor` (Blanc / + Teinté / Plein) est le même sur tous les types de page. +- **Syne toujours en Regular** (poids 400), y compris titres et gras de prose ; + seule exception : le mot « Figures Libres » de la couverture (Bold). +- **Liens Grav** : `stripTargetParam` retire le `?target=_blank` que Grav accole à + chaque URL (lien projet + liens dans les corps de texte), à l'affichage. +- Filet : `--fl-strip` (5 mm, couverture/sommaire/page libre) ou `--fl-strip-light` + (2 mm, filet fin des grilles et des couvertures projet non « plein »). + ## Pièges Chromium print (avant de toucher aux templates) - **`columns-*` (CSS multicol) = page PDF blanche** dans Chromium print. Ne pas diff --git a/assets/css/theme.css b/assets/css/theme.css index e5fb5ef..c66a657 100644 --- a/assets/css/theme.css +++ b/assets/css/theme.css @@ -82,6 +82,7 @@ --fl-margin: 30.2px; /* 8 mm — marge extérieure */ --fl-gutter: 14.4px; /* 3,8 mm — gouttière des grilles */ --fl-strip: 18.9px; /* 5 mm — filet haut des pages grille */ + --fl-strip-light: 7.6px; /* 2 mm — filet fin du mode clair (couverture + grille) */ --fl-band: 208px; /* 55 mm — bandeau des pages couverture */ } @@ -100,8 +101,8 @@ .fl-page :is(h1, h2, h3) { font-family: 'Syne', sans-serif; - font-weight: 700; - line-height: 1.1; + font-weight: 400; /* Syne toujours en Regular (seul « Figures Libres » reste en Bold) */ + line-height: 1.05; } .fl-page img { @@ -133,5 +134,5 @@ .fl-prose ul, .fl-prose ol { margin: 0 0 0.75em; padding-left: 1.2em; } .fl-prose ul { list-style: disc; } .fl-prose ol { list-style: decimal; } -.fl-prose strong { font-weight: 700; } +.fl-prose strong { font-weight: 400; } /* Syne en Regular : pas de graisse dans le corps de texte */ .fl-prose em { font-style: italic; } diff --git a/components/BrandBar.vue b/components/BrandBar.vue new file mode 100644 index 0000000..f4dbe40 --- /dev/null +++ b/components/BrandBar.vue @@ -0,0 +1,26 @@ + + + diff --git a/components/RefreshButton.vue b/components/RefreshButton.vue new file mode 100644 index 0000000..dd9f5f5 --- /dev/null +++ b/components/RefreshButton.vue @@ -0,0 +1,42 @@ + + + diff --git a/components/composer/BackgroundEditor.vue b/components/composer/BackgroundEditor.vue index 9457958..017c2f6 100644 --- a/components/composer/BackgroundEditor.vue +++ b/components/composer/BackgroundEditor.vue @@ -1,50 +1,31 @@ @@ -206,9 +237,8 @@ const freeMedia = computed(() => { :media="gridMedia" :grid="page.grid" :orientation="gridOrientation" - :category="project.category" :band-color="gridStripOverride" - :grey-bg="page.greyBg" + :background="page.background" /> diff --git a/components/templates/CoverTemplate.vue b/components/templates/CoverTemplate.vue index 11600a7..946c338 100644 --- a/components/templates/CoverTemplate.vue +++ b/components/templates/CoverTemplate.vue @@ -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 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()) diff --git a/components/templates/FreeTextPage.vue b/components/templates/FreeTextPage.vue index 99754ae..b04acf2 100644 --- a/components/templates/FreeTextPage.vue +++ b/components/templates/FreeTextPage.vue @@ -11,23 +11,27 @@ const props = withDefaults( media?: ResolvedMedia[] background: Background bandColor?: string + textScale?: number // taille du texte de labeur en % (défaut 100 = 12 pt) page: { index: number; total: number } renderMode?: 'preview' | 'print' }>(), - { renderMode: 'preview' }, + { renderMode: 'preview', textScale: 100 }, ) const strip = computed(() => props.bandColor || FL_INK) +const pageStyle = computed(() => backgroundStyle(props.background, strip.value)) +// Texte de labeur : base 16 px (12 pt), agrandissable ; interligne proportionnel. +const bodyStyle = computed(() => ({ fontSize: `${(16 * props.textScale) / 100}px`, lineHeight: '1.35' }))