From e20dc46fb42d628480d7786465bdd62d4b16557e Mon Sep 17 00:00:00 2001 From: "burrito.space" Date: Thu, 21 May 2026 19:05:13 +0200 Subject: [PATCH] fix(mobile): gate iOS-notch header padding to iOS only The header's --safe-area-top fallback was hard-coded to 40px everywhere, leaking the iOS notch reserve onto Android. On Android, env(safe-area-inset-top) returns 0 and the Kotlin bridge sets the real WindowInsets value, so the 40px default was visible as wasted vertical space above the header on every Android launch. Changes: - App.tsx: replace the 40px JS default with a CSS probe of env(safe-area-inset-top,0px). On iOS this reads the actual notch height; on Android WebView it returns 0 and the Kotlin bridge overwrites with the correct status-bar inset. - App.css header: change fallback from 40px to env(safe-area-inset-top,0px) so iOS gets the correct notch inset even before JS runs; Android sees 0 until the bridge fires. - App.css .edit-overlay: same fallback fix. --- apps/mobile/src/App.css | 10 ++++++---- apps/mobile/src/App.tsx | 19 +++++++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/mobile/src/App.css b/apps/mobile/src/App.css index e32ad5c8..d495ff86 100644 --- a/apps/mobile/src/App.css +++ b/apps/mobile/src/App.css @@ -124,9 +124,11 @@ body.dark .auto-sync-indicator { header { background: #ffffff; padding: 0.25rem 0.75rem; - /* Safe area: --safe-area-top is set by JS on mount (env() for iOS, Kotlin bridge for Android). - Fallback 40px ensures header clears notch/status bar even before JS runs. */ - padding-top: calc(var(--safe-area-top, 40px) + 0.4rem); + /* Safe area: --safe-area-top is set by JS on mount (env() probe for iOS, Kotlin bridge for + Android). Fallback to env(safe-area-inset-top) so iOS gets the correct notch inset even + before JS runs; Android WebView returns 0 for env() and the Kotlin bridge sets the real + value, so no hard-coded iOS offset leaks onto Android. */ + padding-top: calc(var(--safe-area-top, env(safe-area-inset-top, 0px)) + 0.4rem); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); position: sticky; top: 0; @@ -2527,7 +2529,7 @@ body.dark .captured-image-preview { flex-direction: column; align-items: center; padding: 0.5rem; - padding-top: var(--safe-area-top, 40px); + padding-top: var(--safe-area-top, env(safe-area-inset-top, 0px)); /* Bottom padding handled by .editor-segment-buttons safe-area padding */ padding-bottom: 0; /* Prevent iOS from scrolling the viewport when keyboard appears */ diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 5250c224..a665f203 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -220,13 +220,24 @@ const EditorOverlay: React.FC = ({ children, onDismiss, keyb }, []); // Set --safe-area-top CSS variable for status bar / notch clearance. - // On iOS: read from env(safe-area-inset-top) via a CSS trick. - // On Android: Kotlin bridge overwrites with real WindowInsets value. - // Default 40px ensures header clears notch even if bridge is slow. + // On iOS: probe env(safe-area-inset-top) via a hidden div — this is the correct + // notch/dynamic-island inset; set it immediately so CSS picks it up. + // On Android: env(safe-area-inset-top) is not supported in WebView, so the probe + // returns 0. The Kotlin bridge overwrites --safe-area-top with the real + // WindowInsets value when the page loads. We must NOT default to a hard-coded + // iOS-specific value (e.g. 40px) here — that leaks iOS-notch spacing onto + // Android devices that have no notch. useEffect(() => { const root = document.documentElement; if (!root.style.getPropertyValue('--safe-area-top')) { - root.style.setProperty('--safe-area-top', '40px'); + // Probe env(safe-area-inset-top): non-zero on iOS, 0 on Android WebView. + const probe = document.createElement('div'); + probe.style.cssText = 'position:fixed;top:0;height:env(safe-area-inset-top,0px);visibility:hidden;pointer-events:none;'; + document.body.appendChild(probe); + const insetPx = probe.offsetHeight; + document.body.removeChild(probe); + // On iOS this is the notch height; on Android it's 0 (bridge sets real value). + root.style.setProperty('--safe-area-top', `${insetPx}px`); } }, []); -- 2.51.2