* feat: add Dossiê vivo motion to the public site Introduce a focal Home opening, chapter index, and restrained continuity reveals while keeping reduced-motion and visual baselines intact. Co-authored-by: Cursor <cursoragent@cursor.com> * test: refresh visual baselines from CI Ubuntu screenshots Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
4.7 KiB
JavaScript
167 lines
4.7 KiB
JavaScript
const MOTION_QUERY = '(prefers-reduced-motion: reduce)';
|
|
|
|
function prefersReducedMotion() {
|
|
return window.matchMedia(MOTION_QUERY).matches;
|
|
}
|
|
|
|
function enableEnhancement() {
|
|
if (prefersReducedMotion()) {
|
|
document.documentElement.removeAttribute('data-motion');
|
|
return false;
|
|
}
|
|
|
|
document.documentElement.dataset.motion = 'enhance';
|
|
return true;
|
|
}
|
|
|
|
function activateHero(enhance) {
|
|
const hero = document.querySelector('[data-motion="dossie-hero"]');
|
|
if (!hero) {
|
|
return;
|
|
}
|
|
|
|
const activate = () => hero.classList.add('is-active');
|
|
|
|
if (!enhance) {
|
|
activate();
|
|
return;
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(activate);
|
|
});
|
|
}
|
|
|
|
function activatePageOpen(enhance) {
|
|
document.querySelectorAll('[data-motion="page-open"]').forEach((node) => {
|
|
if (!enhance) {
|
|
node.classList.add('is-active');
|
|
return;
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => node.classList.add('is-active'));
|
|
});
|
|
});
|
|
}
|
|
|
|
function observeReveals(enhance) {
|
|
const nodes = Array.from(document.querySelectorAll('[data-reveal]'));
|
|
if (nodes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (!enhance || typeof IntersectionObserver !== 'function') {
|
|
nodes.forEach((node) => node.classList.add('is-revealed'));
|
|
return;
|
|
}
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (!entry.isIntersecting) {
|
|
return;
|
|
}
|
|
|
|
entry.target.classList.add('is-revealed');
|
|
observer.unobserve(entry.target);
|
|
});
|
|
},
|
|
{
|
|
rootMargin: '0px 0px -12% 0px',
|
|
threshold: 0.2,
|
|
},
|
|
);
|
|
|
|
nodes.forEach((node) => observer.observe(node));
|
|
}
|
|
|
|
function setupChapterIndex() {
|
|
const index = document.querySelector('[data-chapter-index]');
|
|
const progress = document.querySelector('[data-chapter-progress] span');
|
|
const chapters = Array.from(document.querySelectorAll('[data-chapter]'));
|
|
|
|
if (!index || chapters.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const links = Array.from(index.querySelectorAll('a[href^="#"]'));
|
|
|
|
const setActive = (id) => {
|
|
links.forEach((link) => {
|
|
const isCurrent = link.getAttribute('href') === `#${id}`;
|
|
if (isCurrent) {
|
|
link.setAttribute('aria-current', 'true');
|
|
} else {
|
|
link.removeAttribute('aria-current');
|
|
}
|
|
});
|
|
};
|
|
|
|
const updateProgress = (ratio) => {
|
|
if (!progress) {
|
|
return;
|
|
}
|
|
|
|
const clamped = Math.min(1, Math.max(0, ratio));
|
|
progress.style.setProperty('--chapter-progress', `${(clamped * 100).toFixed(2)}%`);
|
|
};
|
|
|
|
const sync = () => {
|
|
const marker = window.scrollY + Math.min(window.innerHeight * 0.35, 280);
|
|
let current = chapters[0];
|
|
|
|
chapters.forEach((chapter) => {
|
|
if (chapter.offsetTop <= marker) {
|
|
current = chapter;
|
|
}
|
|
});
|
|
|
|
const heading = current.querySelector('[id$="-heading"]') || document.getElementById(`${current.dataset.chapter}-heading`);
|
|
const headingId = heading?.id
|
|
|| current.getAttribute('aria-labelledby')
|
|
|| `${current.dataset.chapter}-heading`;
|
|
|
|
setActive(headingId);
|
|
|
|
const doc = document.documentElement;
|
|
const max = Math.max(1, doc.scrollHeight - window.innerHeight);
|
|
updateProgress(window.scrollY / max);
|
|
};
|
|
|
|
setActive(chapters[0].getAttribute('aria-labelledby') || 'hero-heading');
|
|
sync();
|
|
|
|
window.addEventListener('scroll', sync, { passive: true });
|
|
window.addEventListener('resize', sync);
|
|
}
|
|
|
|
function bootMotion() {
|
|
const enhance = enableEnhancement();
|
|
activateHero(enhance);
|
|
activatePageOpen(enhance);
|
|
observeReveals(enhance);
|
|
setupChapterIndex();
|
|
|
|
window.matchMedia(MOTION_QUERY).addEventListener('change', (event) => {
|
|
if (event.matches) {
|
|
document.documentElement.removeAttribute('data-motion');
|
|
document.querySelectorAll('[data-motion="dossie-hero"], [data-motion="page-open"]').forEach((node) => {
|
|
node.classList.add('is-active');
|
|
});
|
|
document.querySelectorAll('[data-reveal]').forEach((node) => {
|
|
node.classList.add('is-revealed');
|
|
});
|
|
return;
|
|
}
|
|
|
|
document.documentElement.dataset.motion = 'enhance';
|
|
});
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', bootMotion, { once: true });
|
|
} else {
|
|
bootMotion();
|
|
}
|