@@ -143,6 +143,28 @@ function openOnboarding(): void {
143143 showOnboarding .value = true ;
144144}
145145
146+ // iOS Safari does not shrink `dvh` for the on-screen keyboard. Instead it pans
147+ // the visual viewport (offsetTop > 0) to reveal the focused field, which a
148+ // 100dvh in-flow shell cannot follow: the dock ends up behind the keyboard, or
149+ // the page shows a blank band past the shell's bottom edge. Pin the shell to
150+ // the VISUAL viewport instead: position:fixed + top/height mirrored from
151+ // visualViewport (height shrinks with the keyboard, offsetTop tracks the pan).
152+ // No-ops on desktop, where offsetTop is 0 and height equals innerHeight.
153+ let appHeightRaf = 0 ;
154+ function setAppHeight(): void {
155+ const vv = window .visualViewport ;
156+ const root = document .documentElement .style ;
157+ root .setProperty (' --app-height' , ` ${vv ?.height ?? window .innerHeight }px ` );
158+ root .setProperty (' --app-top' , ` ${vv ?.offsetTop ?? 0 }px ` );
159+ }
160+ function syncAppHeight(): void {
161+ if (appHeightRaf ) return ;
162+ appHeightRaf = requestAnimationFrame (() => {
163+ appHeightRaf = 0 ;
164+ setAppHeight ();
165+ });
166+ }
167+
146168onMounted (() => {
147169 // Register the 401 listener before the first requests go out, so a token
148170 // rejection during the initial load() can never be missed.
@@ -154,13 +176,26 @@ onMounted(() => {
154176 });
155177 void client .load ();
156178 loadSidebarCollapsed ();
179+ setAppHeight ();
180+ window .visualViewport ?.addEventListener (' resize' , syncAppHeight );
181+ window .visualViewport ?.addEventListener (' scroll' , syncAppHeight );
182+ window .addEventListener (' resize' , syncAppHeight );
157183 // Capture-phase so Escape closes the side detail layer BEFORE the
158184 // conversation pane's bubble-phase handler interrupts a running prompt.
159185 document .addEventListener (' keydown' , onGlobalKeydown , true );
160186});
161187
162188onUnmounted (() => {
163189 document .removeEventListener (' keydown' , onGlobalKeydown , true );
190+ window .visualViewport ?.removeEventListener (' resize' , syncAppHeight );
191+ window .visualViewport ?.removeEventListener (' scroll' , syncAppHeight );
192+ window .removeEventListener (' resize' , syncAppHeight );
193+ if (appHeightRaf ) {
194+ cancelAnimationFrame (appHeightRaf );
195+ appHeightRaf = 0 ;
196+ }
197+ document .documentElement .style .removeProperty (' --app-height' );
198+ document .documentElement .style .removeProperty (' --app-top' );
164199 if (offAuthRequired !== null ) {
165200 offAuthRequired ();
166201 offAuthRequired = null ;
@@ -1070,8 +1105,17 @@ function openPr(url: string): void {
10701105.gload-fade-leave-to { opacity : 0 ; }
10711106
10721107.app-shell {
1108+ /* Pinned to the visual viewport (see setAppHeight): --app-top tracks iOS's
1109+ keyboard pan and --app-height shrinks with the keyboard, so the shell
1110+ always covers exactly the visible area. Fixed positioning keeps it out of
1111+ the document flow that iOS pans. */
1112+ position : fixed ;
1113+ top : var (--app-top , 0px );
1114+ left : 0 ;
1115+ right : 0 ;
10731116 height : 100vh ;
10741117 height : 100 dvh;
1118+ height : var (--app-height , 100 dvh);
10751119 display : flex ;
10761120 flex-direction : column ;
10771121 overflow : hidden ;
@@ -1247,10 +1291,10 @@ function openPr(url: string): void {
12471291 .auth-page {
12481292 align-items : flex-start ;
12491293 padding :
1250- max (48px , env(safe-area-inset -top ))
1251- max (20px , env ( safe-area-inset -right ))
1252- max (24px , env ( safe-area-inset -bottom ))
1253- max (20px , env ( safe-area-inset -left ));
1294+ max (48px , var ( --safe -top ))
1295+ max (20px , var ( --safe -right ))
1296+ max (24px , var ( --safe -bottom ))
1297+ max (20px , var ( --safe -left ));
12541298 }
12551299 .auth-page-copy h1 {
12561300 font-size : 26px ;
0 commit comments