From 733f138dae2f50293dfbadb9c1b5ebe3daa91959 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:41:44 +1000 Subject: [PATCH] fix(app): remove bottom safe area while keyboard is open (#47712) --- packages/app/src/entry.tsx | 2 ++ packages/app/src/runtime/platform/keyboard.ts | 33 +++++++++++++++++++ packages/app/src/shell/mobile-drawer.css | 2 +- packages/app/src/shell/shell.tsx | 15 ++++++--- packages/app/src/shell/titlebar/titlebar.tsx | 6 ++-- 5 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 packages/app/src/runtime/platform/keyboard.ts diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 22ab7eb514f..cc866e12a3a 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -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]} > + {standalone && } diff --git a/packages/app/src/runtime/platform/keyboard.ts b/packages/app/src/runtime/platform/keyboard.ts new file mode 100644 index 00000000000..cd065005426 --- /dev/null +++ b/packages/app/src/runtime/platform/keyboard.ts @@ -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 +} diff --git a/packages/app/src/shell/mobile-drawer.css b/packages/app/src/shell/mobile-drawer.css index 34e7577fd7d..255e3e087c8 100644 --- a/packages/app/src/shell/mobile-drawer.css +++ b/packages/app/src/shell/mobile-drawer.css @@ -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; diff --git a/packages/app/src/shell/shell.tsx b/packages/app/src/shell/shell.tsx index f8a98cfd84a..d02237ab73e 100644 --- a/packages/app/src/shell/shell.tsx +++ b/packages/app/src/shell/shell.tsx @@ -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))))", }} > diff --git a/packages/app/src/shell/titlebar/titlebar.tsx b/packages/app/src/shell/titlebar/titlebar.tsx index c7f52036d27..83a4b5c62b7 100644 --- a/packages/app/src/shell/titlebar/titlebar.tsx +++ b/packages/app/src/shell/titlebar/titlebar.tsx @@ -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(),