fix(app): remove bottom safe area while keyboard is open (#47712)

This commit is contained in:
opencode-agent[bot]
2026-09-07 13:41:44 +10:00
committed by GitHub
Unverified
parent 0ff9ccb227
commit 733f138dae
5 changed files with 50 additions and 8 deletions
+2
View File
@@ -7,6 +7,7 @@ import { loadInitialLocale } from "@/runtime/i18n/language"
import { PlatformProvider } from "@/runtime/platform/platform"
import { createWebPlatform } from "@/runtime/platform/web"
import { isStandalone, PwaRoutePersistence, restorePwaRoute } from "@/runtime/platform/pwa"
import { KeyboardInsets } from "@/runtime/platform/keyboard"
import en from "@/runtime/i18n/en"
import zh from "@/runtime/i18n/zh"
import { authFromToken } from "@/runtime/server/api"
@@ -92,6 +93,7 @@ if (root instanceof HTMLElement && root.dataset.opencodeMounted === undefined) {
canonicalLocalServer={ServerConnection.key(server)}
servers={[server]}
>
<KeyboardInsets />
{standalone && <PwaRoutePersistence />}
</AppInterface>
</AppBaseProviders>
@@ -0,0 +1,33 @@
import { onCleanup, onMount } from "solid-js"
export function KeyboardInsets() {
onMount(() => {
const viewport = window.visualViewport
if (!viewport) return
const root = document.documentElement
const sync = () => {
const active = document.activeElement
const editing =
active instanceof HTMLElement &&
(active.isContentEditable || active.matches("input:not([readonly]), textarea:not([readonly])"))
// iOS retains the home-indicator inset above its keyboard. Ignore pinch zoom
// and small viewport changes from browser chrome, not just editor focus.
const keyboard = editing && root.clientHeight - viewport.height * viewport.scale > 100
if (keyboard) root.style.setProperty("--safe-area-inset-bottom", "0px")
if (!keyboard) root.style.removeProperty("--safe-area-inset-bottom")
}
sync()
viewport.addEventListener("resize", sync)
window.addEventListener("resize", sync)
document.addEventListener("focusin", sync)
document.addEventListener("focusout", sync)
onCleanup(() => {
viewport.removeEventListener("resize", sync)
window.removeEventListener("resize", sync)
document.removeEventListener("focusin", sync)
document.removeEventListener("focusout", sync)
root.style.removeProperty("--safe-area-inset-bottom")
})
})
return null
}
+1 -1
View File
@@ -19,7 +19,7 @@
display: flex;
flex-direction: column;
max-height: min(75dvh, calc(100dvh - env(safe-area-inset-top, 0px) - 16px));
padding: 0 12px max(12px, env(safe-area-inset-bottom, 0px));
padding: 0 12px max(12px, var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
padding-left: max(12px, env(safe-area-inset-left, 0px));
padding-right: max(12px, env(safe-area-inset-right, 0px));
border-radius: 16px 16px 0 0;
+11 -4
View File
@@ -47,7 +47,9 @@ export default function Layout(props: ParentProps) {
: platform.platform === "desktop" && platform.os === "windows"
? "1px"
: "8px",
"--shell-bottom-inset": bottomTitlebar() ? "8px" : "max(0px, calc(8px - env(safe-area-inset-bottom, 0px)))",
"--shell-bottom-inset": bottomTitlebar()
? "8px"
: "max(0px, calc(8px - var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))))",
}}
>
<Titlebar
@@ -67,7 +69,7 @@ export default function Layout(props: ParentProps) {
class="relative flex h-full min-h-0 shrink-0 flex-col bg-v2-background-bg-deep pe-0.5 ps-2.5 pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]"
style={{
width: `${state.tabsWidth}px`,
"padding-bottom": "max(10px, env(safe-area-inset-bottom, 0px))",
"padding-bottom": "max(10px, var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)))",
}}
>
<ResizeHandle
@@ -85,8 +87,13 @@ export default function Layout(props: ParentProps) {
class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-content"
style={{
"padding-top": bottomTitlebar() ? "env(safe-area-inset-top, 0px)" : "0px",
"padding-bottom": bottomTitlebar() || settings.active() ? "0px" : "env(safe-area-inset-bottom, 0px)",
"--settings-bottom-inset": bottomTitlebar() ? "40px" : "env(safe-area-inset-bottom, 0px)",
"padding-bottom":
bottomTitlebar() || settings.active()
? "0px"
: "var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))",
"--settings-bottom-inset": bottomTitlebar()
? "40px"
: "var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))",
"--settings-top-inset": mobile() && !bottomTitlebar() ? "0px" : "var(--shell-top-inset, 8px)",
}}
>
+3 -3
View File
@@ -155,11 +155,11 @@ export function Titlebar(props: {
height:
platform.platform === "web"
? bottom()
? "calc(28px + max(8px, env(safe-area-inset-bottom, 0px)))"
? "calc(28px + max(8px, var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))))"
: "calc(28px + max(8px, env(safe-area-inset-top, 0px)))"
: undefined,
"padding-top": bottom() ? "0px" : "env(safe-area-inset-top, 0px)",
"padding-bottom": bottom() ? "env(safe-area-inset-bottom, 0px)" : "0px",
"padding-bottom": bottom() ? "var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))" : "0px",
"min-height": minHeight(),
// Keep native macOS traffic lights clear even when the desktop window is narrow.
"padding-left": macTrafficLights() ? `${macTrafficLightsBaseWidth / zoom()}px` : 0,
@@ -440,7 +440,7 @@ export function Titlebar(props: {
class="h-full flex-1 overflow-hidden flex flex-row items-center gap-1.5 px-2 md:pe-3"
classList={{
"pt-[max(0px,calc(8px-env(safe-area-inset-top,0px)))]": !bottom() && !windows(),
"pb-[max(0px,calc(8px-env(safe-area-inset-bottom,0px)))]": bottom(),
"pb-[max(0px,calc(8px-var(--safe-area-inset-bottom,env(safe-area-inset-bottom,0px))))]": bottom(),
"pl-4": macTrafficLights(),
// Center the 20px app icon over the sidebar's 16px icon column.
"ps-3.5": windows(),