mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
chore(live): add diagnostic logging to scroll lock
Log target-Y at Go, every mutation that triggers a correction (with the mutation type + added nodes), every correct-or-noop (with from/to/delta), every reanchor, and every external scroll event >5px. Lets us see which step is actually moving the page during wrap / variant insert.
This commit is contained in:
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
+162
-3
@@ -722,10 +722,169 @@
|
||||
|
||||
|
||||
|
||||
<div class="consulting-text">
|
||||
<h2 class="consulting-title">Work with me</h2>
|
||||
<p class="consulting-desc">Impeccable is built by <a href="https://renaissance-geek.ai" target="_blank" rel="noopener">Renaissance Geek</a>. I work with enterprise teams on large-scale rollouts, custom integrations, and training for designers and developers. If you're a frontier lab, design tool company, or enterprise looking to raise the bar on AI-generated design, let's talk.</p>
|
||||
<!-- impeccable-variants-start 3ec631fc -->
|
||||
<div data-impeccable-variants="3ec631fc" data-impeccable-variant-count="3" style="display: contents">
|
||||
<!-- Original -->
|
||||
<div data-impeccable-variant="original">
|
||||
<div class="consulting-text">
|
||||
<h2 class="consulting-title">Work with me</h2>
|
||||
<p class="consulting-desc">Impeccable is built by <a href="https://renaissance-geek.ai" target="_blank" rel="noopener">Renaissance Geek</a>. I work with enterprise teams on large-scale rollouts, custom integrations, and training for designers and developers. If you're a frontier lab, design tool company, or enterprise looking to raise the bar on AI-generated design, let's talk.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Variants: insert below this line -->
|
||||
<style data-impeccable-css="3ec631fc">
|
||||
@scope ([data-impeccable-variant="1"]) {
|
||||
.consulting-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 20px;
|
||||
padding: 16px 0;
|
||||
border-block: 1px solid var(--color-ink);
|
||||
}
|
||||
.v1-kicker {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.625rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3em;
|
||||
color: var(--color-accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.v1-line {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 1.375rem;
|
||||
line-height: 1.25;
|
||||
color: var(--color-ink);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.v1-line a {
|
||||
color: var(--color-ink);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
text-decoration-color: var(--color-accent);
|
||||
}
|
||||
.v1-line a:hover { color: var(--color-accent); }
|
||||
}
|
||||
|
||||
@scope ([data-impeccable-variant="2"]) {
|
||||
.consulting-text {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
max-width: 60ch;
|
||||
}
|
||||
.v2-title {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-size: clamp(3rem, 6vw, 4.5rem);
|
||||
line-height: 1;
|
||||
color: var(--color-ink);
|
||||
margin: 0;
|
||||
}
|
||||
.v2-title em {
|
||||
font-style: italic;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.v2-desc {
|
||||
font-family: var(--font-body);
|
||||
font-size: 1rem;
|
||||
line-height: 1.65;
|
||||
color: var(--color-charcoal);
|
||||
margin: 0;
|
||||
}
|
||||
.v2-desc a {
|
||||
color: var(--color-ink);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
text-decoration-color: var(--color-accent);
|
||||
}
|
||||
.v2-desc a:hover { color: var(--color-accent); }
|
||||
}
|
||||
|
||||
@scope ([data-impeccable-variant="3"]) {
|
||||
.consulting-text {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 64px;
|
||||
align-items: start;
|
||||
}
|
||||
.v3-col h2 {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: clamp(2.5rem, 5vw, 3.5rem);
|
||||
line-height: 1.05;
|
||||
color: var(--color-ink);
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
.v3-col-a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.v3-tag {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.625rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--color-ash);
|
||||
}
|
||||
.v3-lede {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.35;
|
||||
color: var(--color-accent);
|
||||
margin: 0;
|
||||
}
|
||||
.v3-col-b {
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid var(--color-ink);
|
||||
padding: 16px 0 0;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.65;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
.v3-col-b a {
|
||||
color: var(--color-ink);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
text-decoration-color: var(--color-accent);
|
||||
}
|
||||
.v3-col-b a:hover { color: var(--color-accent); }
|
||||
}
|
||||
</style>
|
||||
<div data-impeccable-variant="1">
|
||||
<div class="consulting-text">
|
||||
<span class="v1-kicker">Consulting</span>
|
||||
<p class="v1-line">Work with me on enterprise rollouts, custom integrations, and training. By <a href="https://renaissance-geek.ai" target="_blank" rel="noopener">Renaissance Geek</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div data-impeccable-variant="2" style="display: none">
|
||||
<div class="consulting-text">
|
||||
<h2 class="v2-title">Work with <em>me.</em></h2>
|
||||
<p class="v2-desc">Impeccable is built by <a href="https://renaissance-geek.ai" target="_blank" rel="noopener">Renaissance Geek</a>. Rollouts, integrations, and training for teams raising the bar on AI-generated design.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div data-impeccable-variant="3" style="display: none">
|
||||
<div class="consulting-text">
|
||||
<div class="v3-col v3-col-a">
|
||||
<span class="v3-tag">Studio · Renaissance Geek</span>
|
||||
<h2>Work with me.</h2>
|
||||
<p class="v3-lede">Frontier labs, design tool companies, enterprise teams.</p>
|
||||
</div>
|
||||
<div class="v3-col v3-col-b">
|
||||
Impeccable is built by <a href="https://renaissance-geek.ai" target="_blank" rel="noopener">Renaissance Geek</a>. I work with teams on large-scale rollouts, custom integrations, and training for designers and developers. If you're raising the bar on AI-generated design, let's talk.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end 3ec631fc -->
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1321,52 +1321,50 @@
|
||||
}
|
||||
|
||||
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps). The key
|
||||
// insight: we don't care where the selected element ends up, we just
|
||||
// don't want the page to jump. scrollY is a primitive that survives any
|
||||
// DOM destruction; element-viewport-top is fragile when the element
|
||||
// itself gets replaced.
|
||||
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
||||
function startScrollLock(sessionId, initialTargetY) {
|
||||
stopScrollLock();
|
||||
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
||||
? initialTargetY
|
||||
: window.scrollY;
|
||||
console.log('[impeccable.scroll] startScrollLock', { sessionId, scrollY: window.scrollY, targetY: scrollLockTargetY, initialOverride: initialTargetY });
|
||||
|
||||
try { history.scrollRestoration = 'manual'; } catch {}
|
||||
|
||||
// Disable the browser's own scroll anchoring during the session. Bun's
|
||||
// HMR destroys and re-inserts our target element, at which point the
|
||||
// browser picks a different anchor elsewhere on the page (e.g. the
|
||||
// nearest #downloads CTA) and scrolls to keep THAT stable. We own
|
||||
// scroll ourselves while active.
|
||||
const prevHtmlAnchor = document.documentElement.style.overflowAnchor;
|
||||
const prevBodyAnchor = document.body.style.overflowAnchor;
|
||||
document.documentElement.style.overflowAnchor = 'none';
|
||||
document.body.style.overflowAnchor = 'none';
|
||||
|
||||
const correct = () => {
|
||||
const correct = (why) => {
|
||||
scrollLockRaf = null;
|
||||
if (scrollLockTargetY == null) return;
|
||||
if (Math.abs(window.scrollY - scrollLockTargetY) < 0.5) return;
|
||||
const before = window.scrollY;
|
||||
const delta = before - scrollLockTargetY;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
console.log('[impeccable.scroll] correct noop', { why, scrollY: before, targetY: scrollLockTargetY });
|
||||
return;
|
||||
}
|
||||
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
|
||||
console.log('[impeccable.scroll] corrected', { why, from: before, to: scrollLockTargetY, delta, nowAt: window.scrollY });
|
||||
};
|
||||
const schedule = () => {
|
||||
const schedule = (why) => {
|
||||
if (scrollLockRaf != null) return;
|
||||
scrollLockRaf = requestAnimationFrame(correct);
|
||||
scrollLockRaf = requestAnimationFrame(() => correct(why));
|
||||
};
|
||||
|
||||
// Filter to mutations that touch our session's wrapper. Unrelated
|
||||
// mutations (shader animations, HMR indicators, tooltips) shouldn't
|
||||
// trigger corrections and fight the user.
|
||||
scrollLockObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
|
||||
schedule();
|
||||
const childAdds = Array.from(m.addedNodes).map(n => n.nodeType === 1 ? (n.tagName + (n.dataset?.impeccableVariant ? ('[variant=' + n.dataset.impeccableVariant + ']') : '')) : n.nodeType).join(',');
|
||||
console.log('[impeccable.scroll] mutation inside wrapper', { type: m.type, target: m.target?.tagName, adds: childAdds, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('mutation-in-wrapper');
|
||||
return;
|
||||
}
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
|
||||
schedule();
|
||||
console.log('[impeccable.scroll] wrapper node added', { tag: n.tagName, scrollYBefore: window.scrollY, targetY: scrollLockTargetY });
|
||||
schedule('wrapper-added');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1374,27 +1372,37 @@
|
||||
});
|
||||
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// User scroll intent updates the target — we never fight the user.
|
||||
scrollLockAbort = new AbortController();
|
||||
scrollLockAbort.signal.addEventListener('abort', () => {
|
||||
document.documentElement.style.overflowAnchor = prevHtmlAnchor;
|
||||
document.body.style.overflowAnchor = prevBodyAnchor;
|
||||
}, { once: true });
|
||||
const sig = { signal: scrollLockAbort.signal };
|
||||
const reanchor = () => {
|
||||
const reanchor = (why) => {
|
||||
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
||||
const prevTarget = scrollLockTargetY;
|
||||
scrollLockTargetY = window.scrollY;
|
||||
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
|
||||
};
|
||||
window.addEventListener('wheel', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', reanchor, { passive: true, ...sig });
|
||||
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
|
||||
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
|
||||
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor();
|
||||
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
|
||||
}, sig);
|
||||
|
||||
// Initial apply — primarily useful on resume after a true reload,
|
||||
// where the browser may have landed us somewhere wrong.
|
||||
schedule();
|
||||
// Also track raw scroll events for diagnostic — shows whether Bun or
|
||||
// some other mechanism is programmatically scrolling.
|
||||
let lastLoggedScrollY = window.scrollY;
|
||||
window.addEventListener('scroll', () => {
|
||||
const now = window.scrollY;
|
||||
if (Math.abs(now - lastLoggedScrollY) > 5) {
|
||||
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
|
||||
lastLoggedScrollY = now;
|
||||
}
|
||||
}, { passive: true, ...sig });
|
||||
|
||||
schedule('initial');
|
||||
}
|
||||
|
||||
function stopScrollLock() {
|
||||
@@ -1767,6 +1775,7 @@
|
||||
saveSession();
|
||||
if (variantObserver) variantObserver.disconnect();
|
||||
variantObserver = startVariantObserver(currentSessionId);
|
||||
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
|
||||
startScrollLock(currentSessionId);
|
||||
|
||||
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
||||
|
||||
Reference in New Issue
Block a user