/**
* `v-reveal` — a dependency-free scroll-reveal directive.
*
* Elements start hidden (see the `[data-reveal]` rules in core.css) and transition in
* the first time they enter the viewport. This replaces a heavier animation library:
* the whole effect is one IntersectionObserver plus a CSS transition.
*
* Usage:
*
reveal on scroll
* reveal on scroll, delayed by 120ms (for staggering lists)
*
* The marker attribute is emitted during SSR via `getSSRProps`, so the pre-rendered HTML
* is already hidden and there is no flash before hydration. Users who prefer reduced
* motion get the content revealed immediately, with no observer.
*/
const observers = new WeakMap()
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.directive('reveal', {
// Runs on the server: mark the element so it renders hidden from the first paint.
getSSRProps() {
return { 'data-reveal': '' }
},
// Runs in the browser once the element is mounted.
mounted(el, binding) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
el.classList.add('reveal-in')
return
}
if (typeof binding.value === 'number') {
el.style.setProperty('--reveal-delay', `${binding.value}ms`)
}
// Client-only renders (and HMR) never go through getSSRProps, so ensure the marker
// is present before we start observing.
el.setAttribute('data-reveal', '')
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
el.classList.add('reveal-in')
observer.disconnect()
observers.delete(el)
}
}
}, { threshold: 0.1, rootMargin: '0px 0px -10% 0px' })
observer.observe(el)
observers.set(el, observer)
},
unmounted(el) {
observers.get(el)?.disconnect()
observers.delete(el)
},
})
})