fix(web): use h-dvh for app shells so mobile tool controls stay reachable (#559)

On mobile browsers 100vh is the tall viewport (URL bar retracted), so the
full-height shells (h-screen + overflow-hidden) rendered their bottom strip
below the visible area with no way to scroll to it. After an upload the tool
page's bottom control is the "Process" peek bar, so it landed off-screen on
every tool.

Switch the fixed-height shells from h-screen (100vh) to h-dvh (100dvh), which
tracks the visible viewport. AppLayout covers every tool page; the same sweep
covers the editor, login, change-password, 404, and the error/loading screens.
On desktop dvh equals vh, so desktop is unchanged. Adds a source-level guard
(tests/unit/web/app-shell-viewport.test.ts) since headless Chromium has no URL
bar and cannot reproduce the bug.
This commit is contained in:
SnapOtter
2026-07-17 23:53:26 +08:00
committed by GitHub
parent 80330a8b07
commit 3f7214bac2
7 changed files with 53 additions and 8 deletions
+3 -3
View File
@@ -64,7 +64,7 @@ class ErrorBoundary extends Component<
render() {
if (this.state.hasError) {
return (
<div className="flex h-screen items-center justify-center bg-background text-foreground">
<div className="flex h-dvh items-center justify-center bg-background text-foreground">
<div className="text-center space-y-4 max-w-md px-6">
<h1 className="text-xl font-semibold">{en.common.somethingWentWrong}</h1>
<p className="text-sm text-muted-foreground">
@@ -108,7 +108,7 @@ function AuthGuard({ children }: { children: React.ReactNode }) {
if (loading) {
return (
<div className="flex h-screen items-center justify-center bg-background text-foreground">
<div className="flex h-dvh items-center justify-center bg-background text-foreground">
<div className="text-center space-y-3">
<div className="h-8 w-8 border-2 border-primary border-t-transparent rounded-full animate-spin mx-auto" />
<p className="text-sm text-muted-foreground">{en.common.loading}</p>
@@ -132,7 +132,7 @@ function AuthGuard({ children }: { children: React.ReactNode }) {
// Single page-level loading fallback — shown while JS for a route downloads.
function PageLoader() {
return (
<div className="flex h-screen items-center justify-center bg-background text-foreground">
<div className="flex h-dvh items-center justify-center bg-background text-foreground">
<div className="h-8 w-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
);
@@ -35,9 +35,14 @@ export function AppLayout({ children, breadcrumb, navVariant }: AppLayoutProps)
}, [fetchSettings]);
return (
// h-dvh (dynamic viewport height), not h-screen/100vh: on mobile browsers
// 100vh is the *tall* viewport (URL bar retracted), so with overflow-hidden
// the fixed bottom nav and the in-flow "Process" peek bar render below the
// visible area and can't be scrolled to after a file is uploaded. dvh tracks
// the actual visible viewport, keeping the bottom controls reachable.
<div
className={cn(
"flex flex-col h-screen bg-background text-foreground overflow-hidden",
"flex flex-col h-dvh bg-background text-foreground overflow-hidden",
bannerVisible && "pt-9",
)}
>
+1 -1
View File
@@ -118,7 +118,7 @@ export function ChangePasswordPage() {
};
return (
<div className="flex h-screen bg-background">
<div className="flex h-dvh bg-background">
<div className="flex-1 flex items-center justify-center p-8">
<div className="w-full max-w-md space-y-8">
<div>
+1 -1
View File
@@ -157,7 +157,7 @@ export function EditorPage() {
}
return (
<main className="flex flex-col h-screen overflow-hidden bg-background text-foreground">
<main className="flex flex-col h-dvh overflow-hidden bg-background text-foreground">
<h1 className="sr-only">{t.editor.welcome.heading}</h1>
<EditorMenuBar
onNewDocument={() => setShowNewDocument(true)}
+1 -1
View File
@@ -246,7 +246,7 @@ export function LoginPage() {
};
return (
<main className="flex h-screen bg-background">
<main className="flex h-dvh bg-background">
<div className="flex-1 flex items-center justify-center p-8">
<div className="w-full max-w-md space-y-8">
<div>
+1 -1
View File
@@ -5,7 +5,7 @@ export function NotFoundPage() {
const { t } = useTranslation();
return (
<div className="flex h-screen items-center justify-center bg-background text-foreground">
<div className="flex h-dvh items-center justify-center bg-background text-foreground">
<div className="text-center space-y-4 max-w-md px-6">
<h1 className="text-6xl font-bold text-primary">404</h1>
<h2 className="text-xl font-semibold">{t.common.pageNotFound}</h2>