fix(live): separate scroll-key, pre-empt browser, snap on every scroll

Three concrete bugs from the diagnostic logs:

1. saveSession was writing scrollY alongside state, so every call during
   resumeSession clobbered the Go-time value with whatever the browser
   had left us at (typically 0). Move scrollY to its own localStorage
   key, touched only at Go and on user-scroll reanchor.

2. history.scrollRestoration='manual' was being set inside init() at
   DOMContentLoaded — by then the browser has already started animating
   its restore, especially with scroll-behavior: smooth on html. Apply
   it at script parse time, and apply the saved scrollY immediately
   there too, before the browser's animation starts.

3. Corrections only fired on MutationObserver. A programmatic smooth
   scroll (browser restore animation, or another script calling
   scrollIntoView) produces zero DOM mutations — so we never caught it
   walking scrollY from 0 up to 4800+ in the recorded session. Snap
   back on every scroll event, gated by a 250ms user-gesture window so
   we don't fight real user scrolls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-22 10:35:00 -07:00
co-authored by Claude Opus 4.7
parent a6aa98c616
commit 868d8c4126
13 changed files with 919 additions and 186 deletions
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
+70 -9
View File
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
+79 -78
View File
@@ -722,8 +722,9 @@
<!-- impeccable-variants-start 3ec631fc -->
<div data-impeccable-variants="3ec631fc" data-impeccable-variant-count="3" style="display: contents">
<!-- impeccable-variants-start 92cc894b -->
<div data-impeccable-variants="92cc894b" data-impeccable-variant-count="3" style="display: contents">
<!-- Original -->
<div data-impeccable-variant="original">
<div class="consulting-text">
@@ -732,66 +733,78 @@
</div>
</div>
<!-- Variants: insert below this line -->
<style data-impeccable-css="3ec631fc">
<style data-impeccable-css="92cc894b">
@scope ([data-impeccable-variant="1"]) {
.consulting-text {
display: flex;
align-items: baseline;
gap: 20px;
padding: 16px 0;
border-block: 1px solid var(--color-ink);
flex-direction: column;
gap: 16px;
max-width: 56ch;
}
.v1-kicker {
font-family: var(--font-mono);
font-size: 0.625rem;
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.3em;
letter-spacing: 0.25em;
color: var(--color-accent);
white-space: nowrap;
}
.v1-line {
.v1-title {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
font-size: 1.375rem;
line-height: 1.25;
font-weight: 300;
font-size: clamp(2.75rem, 5.5vw, 4rem);
line-height: 1;
color: var(--color-ink);
margin: 0;
flex: 1;
}
.v1-line a {
.v1-body {
font-family: var(--font-body);
font-size: 1rem;
line-height: 1.6;
color: var(--color-charcoal);
margin: 0;
}
.v1-body 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;
grid-template-columns: auto 1fr;
gap: 32px;
align-items: baseline;
padding: 20px 0;
border-block: 1px solid var(--color-ink);
}
.v2-num {
font-family: var(--font-display);
font-style: italic;
font-weight: 300;
font-size: 3.5rem;
line-height: 0.9;
color: var(--color-accent);
}
.v2-body {
display: flex;
flex-direction: column;
gap: 10px;
}
.v2-title {
font-family: var(--font-display);
font-style: italic;
font-weight: 300;
font-size: clamp(3rem, 6vw, 4.5rem);
line-height: 1;
font-size: 2rem;
line-height: 1.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;
font-size: 0.9375rem;
line-height: 1.6;
color: var(--color-charcoal);
margin: 0;
}
@@ -801,90 +814,78 @@
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;
grid-template-columns: 1fr;
gap: 24px;
max-width: 50ch;
}
.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 {
.v3-head {
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 {
align-items: baseline;
gap: 16px;
font-family: var(--font-display);
}
.v3-amp {
font-style: italic;
font-size: clamp(4rem, 9vw, 6rem);
line-height: 0.85;
color: var(--color-accent);
font-weight: 300;
}
.v3-title {
font-style: italic;
font-weight: 400;
font-size: 1.25rem;
line-height: 1.35;
color: var(--color-accent);
font-size: clamp(2rem, 4vw, 2.75rem);
line-height: 1;
color: var(--color-ink);
margin: 0;
}
.v3-col-b {
padding-top: 6px;
border-top: 1px solid var(--color-ink);
padding: 16px 0 0;
.v3-body {
font-family: var(--font-body);
font-size: 0.9375rem;
font-size: 1rem;
line-height: 1.65;
color: var(--color-charcoal);
margin: 0;
}
.v3-col-b a {
.v3-body 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>
<span class="v1-kicker">Consulting · Renaissance Geek</span>
<h2 class="v1-title">Work with me.</h2>
<p class="v1-body">Rollouts, integrations, and training for enterprise teams, frontier labs, and design tool companies. 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>
<span class="v2-num">§</span>
<div class="v2-body">
<h2 class="v2-title">Work with me.</h2>
<p class="v2-desc">Impeccable is built by <a href="https://renaissance-geek.ai" target="_blank" rel="noopener">Renaissance Geek</a>. Enterprise rollouts, custom integrations, and training for designers and developers.</p>
</div>
</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 class="v3-head">
<span class="v3-amp">&amp;</span>
<h2 class="v3-title">Work with me.</h2>
</div>
<p class="v3-body">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.</p>
</div>
</div>
</div>
<!-- impeccable-variants-end 3ec631fc -->
<!-- impeccable-variants-end 92cc894b -->
@@ -99,6 +99,40 @@
let scrollLockRaf = null;
let scrollLockAbort = null;
// Dedicated key for scroll position — SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
const SCROLL_KEY_SUFFIX = '-scroll';
function writeScrollY(y) {
try { localStorage.setItem(LS_KEY + SCROLL_KEY_SUFFIX, String(y)); } catch {}
}
function readScrollY() {
try {
const raw = localStorage.getItem(LS_KEY + SCROLL_KEY_SUFFIX);
if (raw == null) return null;
const n = parseFloat(raw);
return isFinite(n) ? n : null;
} catch { return null; }
}
function clearScrollY() {
try { localStorage.removeItem(LS_KEY + SCROLL_KEY_SUFFIX); } catch {}
}
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time (before DOMContentLoaded). If we
// wait until init(), the browser has already begun animating its own
// restore — especially bad when `scroll-behavior: smooth` is set on html.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null && Math.abs(window.scrollY - savedY) > 0.5) {
console.log('[impeccable.scroll] early restore', { from: window.scrollY, to: savedY });
window.scrollTo({ top: savedY, left: 0, behavior: 'instant' });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
@@ -1378,21 +1412,35 @@
document.body.style.overflowAnchor = prevBodyAnchor;
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
console.log('[impeccable.scroll] reanchor', { why, prevTarget, newTarget: scrollLockTargetY });
};
window.addEventListener('wheel', () => reanchor('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => reanchor('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => reanchor('touchmove'), { passive: true, ...sig });
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) reanchor('key:' + e.key);
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Also track raw scroll events for diagnostic — shows whether Bun or
// some other mechanism is programmatically scrolling.
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
let lastLoggedScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const now = window.scrollY;
@@ -1400,9 +1448,19 @@
console.log('[impeccable.scroll] scroll event', { from: lastLoggedScrollY, to: now, targetY: scrollLockTargetY });
lastLoggedScrollY = now;
}
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
console.log('[impeccable.scroll] scroll-event snap', { from: now, to: scrollLockTargetY });
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
schedule('initial');
// Apply target synchronously, not via rAF — racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
console.log('[impeccable.scroll] startScrollLock initial apply', { to: scrollLockTargetY });
}
}
function stopScrollLock() {
@@ -1410,6 +1468,7 @@
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
clearScrollY();
}
// ---------------------------------------------------------------------------
@@ -1773,6 +1832,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
console.log('[impeccable.scroll] Go pressed', { scrollY: window.scrollY, sessionId: currentSessionId });
@@ -2239,6 +2299,8 @@ void main() {
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
@@ -2248,7 +2310,6 @@ void main() {
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
scrollY: window.scrollY,
}));
} catch { /* quota exceeded or private mode */ }
}
@@ -2405,7 +2466,7 @@ void main() {
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, saved?.scrollY);
startScrollLock(currentSessionId, readScrollY());
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so