fix(web): use dynamic viewport height for app shell (#5500)
* fix(web): use dynamic viewport height for app shell On mobile web browsers, `100vh`/`h-screen` resolves to the large viewport (as if the URL bar were hidden), so the app shell rendered ~40-100px taller than the visible area while the URL bar was shown, forcing the user to scroll to see the full view. Switch the app-shell heights (body, #root, .app-layout, the App root, and the web connect/suspense screens) to dynamic viewport units (dvh) so they track the currently-visible viewport. In Electron there is no browser chrome, so dvh resolves identically to the window height — native is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(web): guard dynamic viewport shell heights Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
ef04f10dad
commit
53873f23fe
|
|
@ -1855,7 +1855,7 @@ function App(): React.JSX.Element {
|
|||
return (
|
||||
<div
|
||||
ref={setAppRootNode}
|
||||
className="flex flex-col h-screen w-screen overflow-hidden"
|
||||
className="flex flex-col h-dvh w-screen overflow-hidden"
|
||||
style={
|
||||
{
|
||||
'--collapsed-sidebar-header-width': `${collapsedSidebarHeaderWidth}px`,
|
||||
|
|
|
|||
|
|
@ -317,7 +317,11 @@
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
/* Why: dvh tracks the visible viewport so the web app shell isn't taller
|
||||
than the screen when a mobile browser's URL bar is shown (100vh = the
|
||||
large viewport, which forces a ~40-100px scroll). In Electron there is
|
||||
no browser chrome, so dvh resolves identically to the window height. */
|
||||
height: 100dvh;
|
||||
font-family: var(
|
||||
--app-font-family,
|
||||
'Geist',
|
||||
|
|
@ -506,7 +510,7 @@
|
|||
/* ── Layout ──────────────────────────────────────────── */
|
||||
|
||||
#root {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -514,7 +518,7 @@
|
|||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export default function WebConnect({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-6 text-foreground">
|
||||
<div className="flex min-h-dvh items-center justify-center bg-background px-4 py-6 text-foreground">
|
||||
<div className="flex w-full max-w-[520px] flex-col gap-5 rounded-lg border border-border bg-card p-5 shadow-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-muted">
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ function WebRoot(): React.JSX.Element {
|
|||
|
||||
installWebPreloadApi()
|
||||
return (
|
||||
<Suspense fallback={<div className="min-h-screen bg-background" />}>
|
||||
<Suspense fallback={<div className="min-h-dvh bg-background" />}>
|
||||
<App />
|
||||
</Suspense>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
function readSource(relativePath: string): string {
|
||||
return readFileSync(join(process.cwd(), relativePath), 'utf8')
|
||||
}
|
||||
|
||||
function cssBlock(css: string, selector: string): string {
|
||||
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
return css.match(new RegExp(`${escapedSelector}\\s*\\{(?<body>[^}]*)\\}`))?.groups?.body ?? ''
|
||||
}
|
||||
|
||||
describe('web viewport shell', () => {
|
||||
it('uses dynamic viewport height for document and app shell containers', () => {
|
||||
const css = readSource('src/renderer/src/assets/main.css')
|
||||
|
||||
for (const selector of ['body', '#root', '.app-layout']) {
|
||||
const block = cssBlock(css, selector)
|
||||
expect(block).toContain('height: 100dvh;')
|
||||
expect(block).not.toMatch(/height:\s*100vh\b/)
|
||||
}
|
||||
})
|
||||
|
||||
it('uses dynamic viewport Tailwind utilities for web shell entry points', () => {
|
||||
const source = [
|
||||
readSource('src/renderer/src/App.tsx'),
|
||||
readSource('src/renderer/src/web/main.tsx'),
|
||||
readSource('src/renderer/src/web/WebConnect.tsx')
|
||||
].join('\n')
|
||||
|
||||
expect(source).toContain('h-dvh')
|
||||
expect(source).toContain('min-h-dvh')
|
||||
expect(source).not.toMatch(/\b(?:min-)?h-screen\b/)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue