From 4f4df85250ac448ba5af293aea54e73c08dcf65f Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 22 Apr 2026 09:58:30 -0700 Subject: [PATCH] fix(live): restore scroll to element's viewport-relative top after reload When HMR misses and we fall back to window.location.reload(), the native scroll restoration landed the page somewhere near the right region but not on the selected element, because layout had shifted between the save and the reload. Capture the element's getBoundingClientRect().top into the session snapshot, disable native scroll restoration on resume, and manually scroll the element back to that exact viewport-relative position. Run a second correction pass after fonts and images settle to absorb late layout shifts without animating the fix. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ .pi/skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ public/index.html | 10 +++++-- .../skills/impeccable/scripts/live-browser.js | 29 +++++++++++++++++++ 13 files changed, 356 insertions(+), 2 deletions(-) diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); diff --git a/public/index.html b/public/index.html index f693bee2d..49dfd5d31 100644 --- a/public/index.html +++ b/public/index.html @@ -104,6 +104,8 @@
Get Started + +
Works with
@@ -118,6 +120,8 @@ Pi
+ +
@@ -713,10 +717,12 @@
+
-

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.

+

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.

+
Get in touch diff --git a/source/skills/impeccable/scripts/live-browser.js b/source/skills/impeccable/scripts/live-browser.js index cc1b5bd4f..d4de79ece 100644 --- a/source/skills/impeccable/scripts/live-browser.js +++ b/source/skills/impeccable/scripts/live-browser.js @@ -2135,6 +2135,15 @@ void main() { function saveSession() { if (!currentSessionId) return; + // Capture the selected element's current viewport-relative top so we + // can restore the same framing after a reload, even if layout shifts. + let scrollAnchor = null; + try { + if (selectedElement && selectedElement.isConnected) { + const r = selectedElement.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) scrollAnchor = { viewportTop: r.top }; + } + } catch {} try { localStorage.setItem(LS_KEY, JSON.stringify({ id: currentSessionId, @@ -2144,6 +2153,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + scrollAnchor, })); } catch { /* quota exceeded or private mode */ } } @@ -2288,6 +2298,25 @@ void main() { // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); + // Restore scroll: land the selected element back at the same + // viewport-relative top it had before the reload. Two passes — once + // synchronously, once after fonts/images settle — catch late layout + // shifts without animating the correction. + if (saved?.scrollAnchor && selectedElement) { + try { history.scrollRestoration = 'manual'; } catch {} + const targetTop = saved.scrollAnchor.viewportTop; + const correct = () => { + if (!selectedElement?.isConnected) return; + const delta = selectedElement.getBoundingClientRect().top - targetTop; + if (Math.abs(delta) > 0.5) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }; + correct(); + requestAnimationFrame(() => requestAnimationFrame(correct)); + if (document.fonts?.ready) { + document.fonts.ready.then(() => requestAnimationFrame(correct)).catch(() => {}); + } + } + state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking();