From 33cbd0b836b61e4b7f15abcfdf2b9cbcd0850420 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 10 Jul 2026 20:25:48 +0300 Subject: [PATCH] fix(mobile): pad Android app below the status bar on Android 15 Android 15 enforces edge-to-edge and ignores the StatusBar overlay:false inset the app relied on, while every --oc-safe-area-* CSS definition was gated behind iOS-only conditions. Read the Capacitor-injected --safe-area-inset-* vars (with env() fallback) on the Android native shell so the header, top toasts, and connect screen clear the status bar; both sources report 0 where the native inset still applies. --- packages/ui/src/apps/MobileApp.tsx | 9 +++++---- packages/ui/src/styles/mobile.css | 27 ++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/apps/MobileApp.tsx b/packages/ui/src/apps/MobileApp.tsx index 2d1e58c4..de6f2877 100644 --- a/packages/ui/src/apps/MobileApp.tsx +++ b/packages/ui/src/apps/MobileApp.tsx @@ -242,9 +242,10 @@ const useNativeMobileChrome = (): void => { const platform = (window as typeof window & { Capacitor?: { getPlatform?: () => string } }).Capacitor?.getPlatform?.(); const applyStatusBar = async () => { if (platform === 'android') { - // Android doesn't feed env(safe-area-inset-top) to CSS, so overlaying the status bar - // makes content render under it. Inset the WebView below the bar instead and paint the - // bar with the resolved theme background (the splash colours the theme system persists). + // Inset the WebView below the bar and paint it with the resolved theme background + // (the splash colours the theme system persists). On Android 15+ edge-to-edge is + // enforced and both calls are no-ops — there the app pads itself via the + // Capacitor-injected --safe-area-inset-* CSS vars (see mobile.css, oc-platform-android). const isDark = document.documentElement.classList.contains('dark'); const themeBg = (isDark ? localStorage.getItem('splashBgDark') : localStorage.getItem('splashBgLight')) || @@ -763,7 +764,7 @@ const MobileConnectionWelcome: React.FC<{ onConnected: () => void }> = ({ onConn }, [conn]); return ( -
+
diff --git a/packages/ui/src/styles/mobile.css b/packages/ui/src/styles/mobile.css index ffc4454d..6a444ba8 100644 --- a/packages/ui/src/styles/mobile.css +++ b/packages/ui/src/styles/mobile.css @@ -519,7 +519,32 @@ not match — so nothing reserves bottom room in the native app. Expose it as a token so any native surface can consume it; the chat shell does so below. */ :root.oc-capacitor-app { - --oc-app-bottom-safe: max(16px, calc(env(safe-area-inset-bottom, 0px) * 0.5)); + --oc-app-bottom-safe: max(16px, calc(var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)) * 0.5)); +} + +/* Android native safe areas. Every --oc-safe-area-* definition above is gated + behind -webkit-touch-callout (iOS) or display-mode: standalone, neither of + which matches the Capacitor Android WebView — so the Android app used to rely + entirely on StatusBar overlay:false insetting the WebView natively. Android 15 + enforces edge-to-edge and ignores that call, leaving content under the status + bar. Capacitor injects the real insets as --safe-area-inset-* inline vars on + (env() still reads 0 on WebViews without the safe-area passthrough + fix), so read those first and fall back to env(). On devices where the native + inset still applies, both sources report 0 — no double padding. */ +:root.oc-capacitor-app.oc-platform-android { + --oc-safe-area-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px)); + --oc-safe-area-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px)); + --oc-safe-area-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)); + --oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px); + --oc-safe-area-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px)); +} + +:root.oc-capacitor-app.oc-platform-android .header-safe-area { + padding-top: var(--oc-safe-area-top); +} + +:root.oc-capacitor-app.oc-platform-android [data-sonner-toaster][data-y-position='top'] { + top: calc(var(--oc-safe-area-top) + 16px) !important; } /* Paint the document canvas with the theme background in the native app — the same