fix(desktop): guarantee out-of-bounds composer is reclamped on load

Re-clamp once more on the next frame after pop-out so layout (sidebar widths,
fonts) has settled, and treat a degenerate pre-layout bounds rect as "unknown"
(fall back to the window) so we never clamp the box into a collapsed area. Net:
anyone who loads in with a stranded position is pulled back on-screen and the
fix is persisted, even if the first measure was premature.
This commit is contained in:
Brooklyn Nicholson 2026-06-22 13:59:26 -05:00
parent ea5fa505d9
commit de7ad8b78e
2 changed files with 15 additions and 6 deletions

View file

@ -543,9 +543,12 @@ export function ChatBar({
syncComposerMetrics()
}, [poppedOut, syncComposerMetrics])
// Keep the floating box on-screen: re-clamp (with the real measured size) when
// it pops out and whenever the window resizes — so a position persisted on a
// bigger/other monitor, or a shrunk window, can never strand it out of reach.
// Keep the floating box on-screen: re-clamp (with the real measured size +
// thread bounds) when it pops out and on every window resize — so a position
// persisted on a bigger/other monitor, a shrunk window, or now-wider sidebar
// can never strand it. The rAF pass re-clamps after layout settles (sidebar
// widths, fonts), so anyone loading in out of bounds is pulled back + saved
// even if the first measure was premature.
useEffect(() => {
if (!poppedOut) {
return undefined
@ -558,10 +561,14 @@ export function ChatBar({
}
reclamp(true)
const raf = requestAnimationFrame(() => reclamp(true))
const onResize = () => reclamp(false)
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
return () => {
cancelAnimationFrame(raf)
window.removeEventListener('resize', onResize)
}
}, [poppedOut])
useEffect(() => {

View file

@ -88,9 +88,11 @@ export function readPopoutBounds(composer: Element | null): PopoutBounds | undef
return undefined
}
const { bottom, left, right, top } = el.getBoundingClientRect()
const { bottom, height, left, right, top, width } = el.getBoundingClientRect()
return { bottom, left, right, top }
// Pre-layout (mount before first layout) the rect is empty — fall back to the
// window rather than clamping the box into a collapsed area.
return width > 0 && height > 0 ? { bottom, left, right, top } : undefined
}
// Bound the bottom/right inset so the WHOLE box stays inside `area` (the thread