fix(dashboard): pin --vv-offset-top to resize/focus, ignore pan scrolls

iOS fires visualViewport scroll events at 60fps during a pan with the
keyboard up. Routing those through React state and into the .chat-thread
translateY(--vv-offset-top) transform amplified the pan into a visible
judder + ~300px shift + body background exposure.

useMobileKeyboard now uses two listeners: a full update (resize +
focusin/focusout) that re-snapshots all metrics including offsetTop, and
a scroll-only update that updates only height/keyboardOpen. offsetTop
is therefore frozen between keyboard open/close events — the transform
correctly compensates for iOS's initial visualViewport shift on focus
without following pan-time movement.

Restores the translateY anchor (so the thread isn't off-screen on
first focus) while keeping the swipe-jitter fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-06 20:58:04 -07:00
parent d5858b44b9
commit 4708e453ae
20 changed files with 803 additions and 42 deletions

View File

@@ -1105,14 +1105,11 @@
.chat-thread[style*="--keyboard-overlap"] {
height: calc(var(--vv-height, calc(100dvh - var(--keyboard-overlap, 0px))) - var(--header-height));
max-height: calc(var(--vv-height, calc(100dvh - var(--keyboard-overlap, 0px))) - var(--header-height));
/* The previous translateY(--vv-offset-top) compensated for iOS
shifting the visual viewport on focus, but it also amplified
jitter during a swipe — visualViewport scroll events update
--vv-offset-top rapidly, the transform follows, and the thread
jutters / shifts ~300px / exposes the body background. Dropping
the transform lets the browser place the thread where iOS
expects; height-shrink to vv-height still keeps it inside the
visible area. */
/* useMobileKeyboard pins --vv-offset-top across pan-time scroll
events (only resize/focus update it), so this transform anchors
the thread on initial focus without juddering during swipes. */
transform: translateY(var(--vv-offset-top, 0px));
will-change: transform;
}
/* On mobile, the "New Chat" affordance uses a full-width pinned footer. */