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:
Paul Bakaus
2026-04-22 10:28:48 -07:00
parent 565381a3e7
commit a6aa98c616
13 changed files with 606 additions and 339 deletions
@@ -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);
+37 -28
View File
@@ -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);
+37 -28
View File
@@ -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);
+37 -28
View File
@@ -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
View File
@@ -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);