Générateur de portfolios PDF Figures Libres (app Nuxt autonome)
App Nuxt 3 + Nuxt UI qui lit les projets du CMS Grav et compose des portfolios A3 paysage exportés en PDF (gabarit book_v3) : composeur WYSIWYG, templates cover/toc/projet/grille/page libre, pipeline Playwright + Ghostscript, gel du contenu à la génération, partage public opt-in. Dépôt autonome : Dockerfile (base Playwright + Ghostscript + ffmpeg + rsync) inclus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Scroll-spy des pages empilées : la page active suit en temps réel la page au
|
||||
* centre du conteneur défilable, et permet de défiler vers une page donnée.
|
||||
* Partagé par le composeur (pages/portfolios/[id].vue) et la vue publique
|
||||
* (pages/view/[id].vue) — chacun câble sa propre ref `active` et son `idPrefix`.
|
||||
*
|
||||
* - `container` : élément défilable (l'aperçu).
|
||||
* - `pages` : liste courante des pages (on lit `.id` et l'ordre).
|
||||
* - `active` : ref d'id écrite à chaque défilement (jamais de re-scroll).
|
||||
* - `idPrefix` : préfixe des id DOM des pages (`preview-page` / `view-page`),
|
||||
* suffixés par la position 1-based.
|
||||
*/
|
||||
export function usePageScrollSpy(options: {
|
||||
container: Ref<HTMLElement | undefined>
|
||||
pages: Ref<{ id: string }[]>
|
||||
active: Ref<string | undefined>
|
||||
idPrefix: string
|
||||
}) {
|
||||
const { container, pages, active, idPrefix } = options
|
||||
|
||||
function elementFor(index: number): HTMLElement | null {
|
||||
return document.getElementById(`${idPrefix}-${index + 1}`)
|
||||
}
|
||||
|
||||
function scrollToIndex(i: number) {
|
||||
nextTick(() => {
|
||||
elementFor(i)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
})
|
||||
}
|
||||
|
||||
function scrollToId(id: string) {
|
||||
const i = pages.value.findIndex(p => p.id === id)
|
||||
if (i !== -1) scrollToIndex(i)
|
||||
}
|
||||
|
||||
// Une mise à jour par frame (rAF) pour rester fluide sans recalcul redondant.
|
||||
let rafPending = false
|
||||
function onScroll() {
|
||||
if (rafPending) return
|
||||
rafPending = true
|
||||
requestAnimationFrame(() => {
|
||||
rafPending = false
|
||||
updateActiveFromScroll()
|
||||
})
|
||||
}
|
||||
|
||||
function updateActiveFromScroll() {
|
||||
const cont = container.value
|
||||
if (!cont) return
|
||||
const mid = cont.getBoundingClientRect().top + cont.clientHeight / 2
|
||||
let bestId: string | undefined
|
||||
let bestDist = Infinity
|
||||
pages.value.forEach((page, i) => {
|
||||
const el = elementFor(i)
|
||||
if (!el) return
|
||||
const r = el.getBoundingClientRect()
|
||||
const dist = Math.abs(r.top + r.height / 2 - mid)
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist
|
||||
bestId = page.id
|
||||
}
|
||||
})
|
||||
if (bestId && bestId !== active.value) active.value = bestId
|
||||
}
|
||||
|
||||
return { onScroll, scrollToIndex, scrollToId }
|
||||
}
|
||||
Reference in New Issue
Block a user