fix(ui): size mobile layout from live visible viewport (browser tab bar cutoff) #77
@ -114,6 +114,24 @@ function recordError(source: string, err: unknown, info?: string) {
|
|||||||
|
|
||||||
app.config.errorHandler = (err, _instance, info) => recordError('Vue Error', err, info)
|
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
|
// 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
|
// 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
|
// "Failed to fetch dynamically imported module". The fix is to reload once so
|
||||||
|
|||||||
@ -175,11 +175,13 @@ select:focus-visible {
|
|||||||
`.min-h-screen` (100vh) utility on specificity. */
|
`.min-h-screen` (100vh) utility on specificity. */
|
||||||
.dashboard-view.dashboard-view {
|
.dashboard-view.dashboard-view {
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
|
height: var(--visual-viewport-height, 100dvh);
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
|
min-height: var(--visual-viewport-height, 100dvh);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-scroll-pad {
|
.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 {
|
.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);
|
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. */
|
fully clear of the tab bar, with the .mobile-scroll-pad margin visible. */
|
||||||
.dashboard-view.dashboard-view {
|
.dashboard-view.dashboard-view {
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
|
height: var(--visual-viewport-height, 100dvh);
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
|
min-height: var(--visual-viewport-height, 100dvh);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-view .app-header-inline-tabs {
|
.dashboard-view .app-header-inline-tabs {
|
||||||
@ -224,7 +228,7 @@ select:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-scroll-pad {
|
.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 {
|
.mobile-scroll-pad-back {
|
||||||
@ -622,6 +626,7 @@ input[type="radio"]:active + * {
|
|||||||
.chat-fullscreen {
|
.chat-fullscreen {
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
max-height: 100dvh;
|
max-height: 100dvh;
|
||||||
|
max-height: var(--visual-viewport-height, 100dvh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -648,6 +653,7 @@ input[type="radio"]:active + * {
|
|||||||
.chat-iframe-mobile {
|
.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(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(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;
|
flex: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -604,6 +604,7 @@ onBeforeUnmount(() => {
|
|||||||
.app-session-fullscreen {
|
.app-session-fullscreen {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
|
height: var(--visual-viewport-height, 100dvh);
|
||||||
padding-top: var(--safe-area-top, env(safe-area-inset-top, 0px));
|
padding-top: var(--safe-area-top, env(safe-area-inset-top, 0px));
|
||||||
background: black;
|
background: black;
|
||||||
}
|
}
|
||||||
@ -621,6 +622,7 @@ onBeforeUnmount(() => {
|
|||||||
flex: none !important;
|
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(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(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;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user