/** * Bind the app shell's height *and position* to the *visual* viewport. * * On mobile the layout viewport does not shrink when the on-screen keyboard * opens — only the visual viewport does. A shell sized at `height: 100%` (or * `100dvh`) therefore extends behind the keyboard, hiding anything pinned to the * bottom: the attach view's special-key bar (NAP-342). Mirroring * `visualViewport.height` onto a CSS custom property keeps the shell exactly as * tall as the visible area, so the key bar lands just above the keyboard. * * Height alone isn't enough: when the keyboard opens the browser also scrolls * the layout viewport to reveal the focused element, which drags the shell * (anchored at layout-top) up and out of view — the key bar ends up at the top * of the screen and the user has to scroll back down. `visualViewport.offsetTop` * is how far the layout viewport has scrolled under the keyboard; translating * the shell down by that amount re-pins it to the visible region. (Chrome's * `interactive-widget=resizes-content`, set in the viewport meta, prevents the * scroll entirely on Android; this covers iOS and older browsers that ignore * it, where offsetTop stays non-zero.) * * No-ops where `visualViewport` is unavailable; the CSS falls back to `100%`. * * It also toggles an `html.kb-open` class that reflects whether the soft * keyboard is actually up, used to gate the mobile key bar. Focus is the wrong * signal: dismissing the keyboard (e.g. the Android back button) keeps the * terminal focused, so the bar would linger. Instead we watch the visual * viewport shrink: while an editable element is focused, a height noticeably * below the tallest height seen with nothing focused means the keyboard is open. * The baseline resets whenever nothing is being edited, so it self-corrects * across orientation changes and URL-bar collapse. */ const KB_OPEN_THRESHOLD = 150; export function installViewportSizing(): void { const vv = window.visualViewport; if (!vv) return; const root = document.documentElement; let raf = 0; let baseline = vv.height; const isEditing = () => { const a = document.activeElement as HTMLElement | null; if (!a) return false; return a.tagName === "TEXTAREA" || a.tagName === "INPUT" || a.isContentEditable; }; const sync = () => { raf = 0; root.style.setProperty("--app-vh", `${Math.round(vv.height)}px`); root.style.setProperty("--app-top", `${Math.round(vv.offsetTop)}px`); // Nothing focused ⇒ keyboard can't be open ⇒ this height is the baseline. const editing = isEditing(); if (!editing) baseline = vv.height; const kbOpen = editing && baseline - vv.height > KB_OPEN_THRESHOLD; root.classList.toggle("kb-open", kbOpen); }; const schedule = () => { if (raf) return; raf = requestAnimationFrame(sync); }; vv.addEventListener("resize", schedule); vv.addEventListener("scroll", schedule); sync(); }