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 pages: Ref<{ id: string }[]> active: Ref 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 } }