fix(story): filmstrip de prints com sticky+scrub dedado e ReelDirector pos-hidratacao sem mutation no DOM
This commit is contained in:
1 parent
ecadb7c540
commit
a3d080d4d0
2 files changed
+285
-74
No files matched your search
@@ -8,13 +8,19 @@ import { useEffect, useRef } from "react";
|
||||
* Contrato (declarativo, via atributos data-*):
|
||||
* [data-reel="left|right|top|bottom|zoom|fade"]
|
||||
* elemento entra em scrub contínuo: rolar p/ baixo avança, p/ cima REBOBINA.
|
||||
* [data-reel-stagger="left|right|top|bottom|zoom|fade"]
|
||||
* [data-reel-stagger="left|right|top|zoom|fade|bottom"]
|
||||
* container: os FILHOS DIRETOS entram em cascata no mesmo trilho.
|
||||
* data-reel-start="top 88%" data-reel-end="top 26%" (opcional: janela do scrub)
|
||||
* data-reel-distance="64" (opcional: px de deslocamento)
|
||||
*
|
||||
* Nunca one-shot — tudo ligado ao progresso do scroll, como o reel do hero.
|
||||
* prefers-reduced-motion: nada é inicializado; conteúdo fica no fluxo natural.
|
||||
*
|
||||
* Anti-hidratação: elementos inicializados vivem num WeakSet (nunca um
|
||||
* atributo data-reel-init no DOM) e o primeiro scan roda depois que a
|
||||
* hidratação assentou (double-rAF + debounce) — MutationObserver dispara em
|
||||
* microtask e pegaria nós ainda em hidratação, produzindo mismatch de
|
||||
* atributos no overlay do Next.
|
||||
*/
|
||||
|
||||
type Direction = "left" | "right" | "top" | "bottom" | "zoom" | "fade";
|
||||
@@ -49,6 +55,9 @@ export default function ReelDirector() {
|
||||
return undefined;
|
||||
|
||||
let teardown = () => {};
|
||||
let raf1 = 0;
|
||||
let raf2 = 0;
|
||||
let timer = 0;
|
||||
|
||||
const boot = async () => {
|
||||
const [{ gsap }, { ScrollTrigger }] = await Promise.all([
|
||||
@@ -58,12 +67,16 @@ export default function ReelDirector() {
|
||||
if (!alive.current) return;
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const done = new WeakSet<Element>();
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
const scan = () => {
|
||||
if (!alive.current) return;
|
||||
for (const el of document.querySelectorAll<HTMLElement>(
|
||||
"[data-reel]:not([data-reel-init])",
|
||||
"[data-reel]",
|
||||
)) {
|
||||
el.dataset.reelInit = "1";
|
||||
if (done.has(el)) continue;
|
||||
done.add(el);
|
||||
const dir = (el.dataset.reel || "bottom") as Direction;
|
||||
const dist = Number(el.dataset.reelDistance || DISTANCE);
|
||||
gsap.fromTo(el, fromVars(dir, dist), {
|
||||
@@ -77,9 +90,10 @@ export default function ReelDirector() {
|
||||
});
|
||||
}
|
||||
for (const container of document.querySelectorAll<HTMLElement>(
|
||||
"[data-reel-stagger]:not([data-reel-init])",
|
||||
"[data-reel-stagger]",
|
||||
)) {
|
||||
container.dataset.reelInit = "1";
|
||||
if (done.has(container)) continue;
|
||||
done.add(container);
|
||||
const dir = (container.dataset.reelStagger || "bottom") as Direction;
|
||||
const dist = Number(container.dataset.reelDistance || DISTANCE);
|
||||
const children = Array.from(container.children) as HTMLElement[];
|
||||
@@ -101,12 +115,25 @@ export default function ReelDirector() {
|
||||
}
|
||||
};
|
||||
|
||||
scan();
|
||||
// seções chegam via next/dynamic depois do boot — observa nós novos
|
||||
const observer = new MutationObserver(() => scan());
|
||||
// debounce: nós de seções dinâmicas (next/dynamic) chegam depois
|
||||
let pending = 0;
|
||||
const scheduleScan = () => {
|
||||
window.clearTimeout(pending);
|
||||
pending = window.setTimeout(scan, 120);
|
||||
};
|
||||
|
||||
// espera a hidratação assentar antes de tocar o DOM
|
||||
raf1 = window.requestAnimationFrame(() => {
|
||||
raf2 = window.requestAnimationFrame(() => {
|
||||
timer = window.setTimeout(scan, 250);
|
||||
});
|
||||
});
|
||||
|
||||
const observer = new MutationObserver(scheduleScan);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
teardown = () => {
|
||||
window.clearTimeout(pending);
|
||||
observer.disconnect();
|
||||
ctx.revert();
|
||||
};
|
||||
@@ -116,6 +143,9 @@ export default function ReelDirector() {
|
||||
void boot();
|
||||
return () => {
|
||||
alive.current = false;
|
||||
window.cancelAnimationFrame(raf1);
|
||||
window.cancelAnimationFrame(raf2);
|
||||
window.clearTimeout(timer);
|
||||
teardown();
|
||||
};
|
||||
}, []);
|
||||
|
||||
Reference in new issue
Block a user