diff --git a/neode-ui/src/main.ts b/neode-ui/src/main.ts index 9e984a56..66a115b2 100644 --- a/neode-ui/src/main.ts +++ b/neode-ui/src/main.ts @@ -114,6 +114,24 @@ function recordError(source: string, err: unknown, info?: string) { app.config.errorHandler = (err, _instance, info) => recordError('Vue Error', err, info) +// Keep --visual-viewport-height in sync with the REAL visible viewport. +// Mobile browsers (notably Brave Android with its custom bottom toolbar) +// misreport dvh, leaving 100dvh layouts taller than the visible area — the +// bottom of every page then hides behind the browser chrome and can never be +// scrolled into view. window.innerHeight shares the reference frame of +// position:fixed elements (the mobile tab bar), so sizing the dashboard off +// it keeps scroll containers and the fixed bars consistent on every browser. +// CSS uses var(--visual-viewport-height, 100dvh) so this is a progressive fix. +function syncViewportHeightVar() { + try { + document.documentElement.style.setProperty('--visual-viewport-height', `${window.innerHeight}px`) + } catch { /* non-browser environment */ } +} +syncViewportHeightVar() +window.addEventListener('resize', syncViewportHeightVar) +window.addEventListener('orientationchange', syncViewportHeightVar) +window.visualViewport?.addEventListener('resize', syncViewportHeightVar) + // After a frontend deploy the browser's cached index.html still points at the // OLD hashed chunks (e.g. AppSession-Cq93o4ao.js), which 404 — Vite then throws // "Failed to fetch dynamically imported module". The fix is to reload once so diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css index 9ee7de7f..3e4872f8 100644 --- a/neode-ui/src/style.css +++ b/neode-ui/src/style.css @@ -175,11 +175,13 @@ select:focus-visible { `.min-h-screen` (100vh) utility on specificity. */ .dashboard-view.dashboard-view { height: 100dvh; + height: var(--visual-viewport-height, 100dvh); min-height: 100dvh; + min-height: var(--visual-viewport-height, 100dvh); } .mobile-scroll-pad { - padding-bottom: calc(var(--mobile-tab-bar-height, 88px) + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) + var(--audio-player-height, 0px) + 16px); + padding-bottom: calc(var(--mobile-tab-bar-height, 88px) + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) + var(--audio-player-height, 0px) + 28px); } .mobile-scroll-pad-back { padding-bottom: calc(var(--mobile-tab-bar-height, 88px) + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) + var(--audio-player-height, 0px) + 64px); @@ -202,7 +204,9 @@ select:focus-visible { fully clear of the tab bar, with the .mobile-scroll-pad margin visible. */ .dashboard-view.dashboard-view { height: 100dvh; + height: var(--visual-viewport-height, 100dvh); min-height: 100dvh; + min-height: var(--visual-viewport-height, 100dvh); } .dashboard-view .app-header-inline-tabs { @@ -224,7 +228,7 @@ select:focus-visible { } .mobile-scroll-pad { - padding-bottom: calc(var(--mobile-tab-bar-height, 88px) + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) + var(--audio-player-height, 0px) + 16px); + padding-bottom: calc(var(--mobile-tab-bar-height, 88px) + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) + var(--audio-player-height, 0px) + 28px); } .mobile-scroll-pad-back { @@ -622,6 +626,7 @@ input[type="radio"]:active + * { .chat-fullscreen { max-height: 100vh; max-height: 100dvh; + max-height: var(--visual-viewport-height, 100dvh); } } @@ -648,6 +653,7 @@ input[type="radio"]:active + * { .chat-iframe-mobile { height: calc(100vh - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px)) - var(--audio-player-height, 0px) - 16px) !important; height: calc(100dvh - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px)) - var(--audio-player-height, 0px) - 16px) !important; + height: calc(var(--visual-viewport-height, 100dvh) - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px)) - var(--audio-player-height, 0px) - 16px) !important; flex: none; } } diff --git a/neode-ui/src/views/AppSession.vue b/neode-ui/src/views/AppSession.vue index 44acd43b..e1ee9078 100644 --- a/neode-ui/src/views/AppSession.vue +++ b/neode-ui/src/views/AppSession.vue @@ -604,6 +604,7 @@ onBeforeUnmount(() => { .app-session-fullscreen { height: 100vh; height: 100dvh; + height: var(--visual-viewport-height, 100dvh); padding-top: var(--safe-area-top, env(safe-area-inset-top, 0px)); background: black; } @@ -621,6 +622,7 @@ onBeforeUnmount(() => { flex: none !important; height: calc(100vh - var(--app-session-mobile-bar-height, 84px) - var(--safe-area-top, env(safe-area-inset-top, 0px))); height: calc(100dvh - var(--app-session-mobile-bar-height, 84px) - var(--safe-area-top, env(safe-area-inset-top, 0px))); + height: calc(var(--visual-viewport-height, 100dvh) - var(--app-session-mobile-bar-height, 84px) - var(--safe-area-top, env(safe-area-inset-top, 0px))); padding-bottom: 0; } }