diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top; diff --git a/public/index.html b/public/index.html index bcddf0b88..9f88cfe71 100644 --- a/public/index.html +++ b/public/index.html @@ -719,8 +719,9 @@
- -
+ + +
@@ -729,143 +730,171 @@
-
-

Work with me.

- /// -

Built by Renaissance Geek · Rollouts · Integrations · Training · Say hello

+
+ Transmission · 001 + Consulting +
+

Work with me.

+
    +
  • 01Enterprise rollouts
  • +
  • 02Custom integrations
  • +
  • 03Team training for designers and developers
  • +
+

Impeccable is built by Renaissance Geek. If you're a frontier lab, design tool company, or enterprise team, let's talk.

-

Work with me.

-

Rollouts, integrations, and training for teams that take AI-generated design seriously.

-

Impeccable is built by Renaissance Geek. If you're a frontier lab, design tool company, or enterprise looking to raise the bar on AI-generated design, let's talk.

+
RG
+
+ Consulting · Renaissance Geek +

Work with me on the design side of AI.

+

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.

+
-
- Work - with - me. -
-
- Consulting · Renaissance Geek -

Impeccable is built by Renaissance Geek. 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.

+

Work with me

+

Impeccable is built by Renaissance Geek. 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.

+
+ Consulting + Renaissance Geek · v3.0
- + +
diff --git a/source/skills/impeccable/scripts/live-browser.js b/source/skills/impeccable/scripts/live-browser.js index 4eea9a860..c3b293fe7 100644 --- a/source/skills/impeccable/scripts/live-browser.js +++ b/source/skills/impeccable/scripts/live-browser.js @@ -1349,15 +1349,45 @@ try { history.scrollRestoration = 'manual'; } catch {} + // Disable browser scroll anchoring on root elements during the session. + // When Bun's HMR destroys our target element and re-inserts it, the + // browser picks a different anchor nearby (often the wrong one — Get + // Started, say) and scrolls the page to keep THAT stable. We want to + // own scroll ourselves, so turn it off while we're active. + const prevHtmlAnchor = document.documentElement.style.overflowAnchor; + const prevBodyAnchor = document.body.style.overflowAnchor; + document.documentElement.style.overflowAnchor = 'none'; + document.body.style.overflowAnchor = 'none'; + + // Grace window after any user-scroll intent: suppress corrections so + // momentum scrolls can't be yanked back by a mutation firing mid-scroll. + let lastUserScrollAt = 0; + const USER_SCROLL_GRACE_MS = 400; + const correct = () => { scrollLockRaf = null; if (scrollLockTargetTop == null) return; const el = resolveScrollLockTarget(sessionId); if (!el) return; - const delta = el.getBoundingClientRect().top - scrollLockTargetTop; - if (Math.abs(delta) > 0.5) { - window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + if (performance.now() - lastUserScrollAt < USER_SCROLL_GRACE_MS) { + // User just scrolled — just re-anchor and let them be. + scrollLockTargetTop = el.getBoundingClientRect().top; + return; } + const currentTop = el.getBoundingClientRect().top; + const delta = currentTop - scrollLockTargetTop; + if (Math.abs(delta) < 0.5) return; + // Always correct, even for huge deltas — a huge delta typically + // means the browser's anchor drifted (common with Bun's HMR + // wholesale-replace) and is exactly when we most need to restore. + window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + + // Restore overflow-anchor on stop. Stash the restorer on the abort + // controller so stopScrollLock picks it up. + const restoreAnchor = () => { + document.documentElement.style.overflowAnchor = prevHtmlAnchor; + document.body.style.overflowAnchor = prevBodyAnchor; }; const schedule = () => { if (scrollLockRaf != null) return; @@ -1388,8 +1418,10 @@ // correction, then update the target top to the element's new position // so we don't drag them back on the next mutation. scrollLockAbort = new AbortController(); + scrollLockAbort.signal.addEventListener('abort', restoreAnchor, { once: true }); const sig = { signal: scrollLockAbort.signal }; const reanchor = () => { + lastUserScrollAt = performance.now(); if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; } const el = resolveScrollLockTarget(sessionId); if (el) scrollLockTargetTop = el.getBoundingClientRect().top;