Skip to content

Commit 564bae8

Browse files
committed
Fix iOS Safari safe-area overlap on mobile
- JS probes actual env(safe-area-inset-*) values via hidden element - Sets --safe-top / --safe-bottom CSS custom properties with device- specific minimum floors (47px for notched iPhones, 20px for older) - Targeted .is-ios / .is-android body classes for platform-specific CSS - Hardened channel-header and message-input-area padding via !important to override theme specificity issues - Handles both Safari browser and PWA standalone modes
1 parent e4db71d commit 564bae8

2 files changed

Lines changed: 118 additions & 3 deletions

File tree

public/css/style.css

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9084,6 +9084,73 @@ select.cfn-select.cfn-input {
90849084
}
90859085

90869086

9087+
/* ═══════════════════════════════════════════════════════════
9088+
iOS / MOBILE SAFE-AREA HARDENING
9089+
JS probes env(safe-area-inset-*) and sets --safe-top /
9090+
--safe-bottom as CSS custom properties with device-specific
9091+
minimum floors. These override env() when the browser
9092+
returns 0 despite a notch / Dynamic Island being present.
9093+
═══════════════════════════════════════════════════════════ */
9094+
9095+
@media (max-width: 768px) {
9096+
/* Header: use JS-measured safe-top when available, falling back to env() */
9097+
body.is-ios .channel-header,
9098+
body.is-android .channel-header {
9099+
padding-top: calc(8px + var(--safe-top, env(safe-area-inset-top, 0px))) !important;
9100+
min-height: calc(48px + var(--safe-top, env(safe-area-inset-top, 0px))) !important;
9101+
}
9102+
9103+
/* Left sidebar overlay */
9104+
body.is-ios .sidebar,
9105+
body.is-android .sidebar {
9106+
padding-top: var(--safe-top, env(safe-area-inset-top, 0px)) !important;
9107+
}
9108+
9109+
/* Right sidebar */
9110+
body.is-ios .right-sidebar,
9111+
body.is-android .right-sidebar {
9112+
padding-top: var(--safe-top, env(safe-area-inset-top, 0px)) !important;
9113+
}
9114+
9115+
/* Server bar */
9116+
body.is-ios .server-bar,
9117+
body.is-android .server-bar {
9118+
padding-top: calc(8px + var(--safe-top, env(safe-area-inset-top, 0px))) !important;
9119+
}
9120+
9121+
/* Message input — bottom safe area */
9122+
body.is-ios .message-input-area,
9123+
body.is-android .message-input-area {
9124+
padding-bottom: calc(8px + var(--safe-bottom, env(safe-area-inset-bottom, 0px))) !important;
9125+
}
9126+
9127+
/* Toasts */
9128+
body.is-ios #toast-container,
9129+
body.is-android #toast-container {
9130+
top: calc(8px + var(--safe-top, env(safe-area-inset-top, 0px)));
9131+
}
9132+
}
9133+
9134+
@media (max-width: 480px) {
9135+
body.is-ios .channel-header,
9136+
body.is-android .channel-header {
9137+
padding-top: calc(6px + var(--safe-top, env(safe-area-inset-top, 0px))) !important;
9138+
min-height: calc(44px + var(--safe-top, env(safe-area-inset-top, 0px))) !important;
9139+
}
9140+
9141+
body.is-ios .message-input-area,
9142+
body.is-android .message-input-area {
9143+
padding-bottom: calc(6px + var(--safe-bottom, env(safe-area-inset-bottom, 0px))) !important;
9144+
}
9145+
}
9146+
9147+
/* Remove safe-top padding when iOS keyboard is open (bottom padding already
9148+
handled by .ios-keyboard-open rule above) */
9149+
body.is-ios.ios-keyboard-open .message-input-area {
9150+
padding-bottom: 6px !important;
9151+
}
9152+
9153+
90879154
/* ═══════════════════════════════════════════════════════════
90889155
TOUCH & ACCESSIBILITY
90899156
═══════════════════════════════════════════════════════════ */

public/js/modules/app-ui.js

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3302,12 +3302,60 @@ _submitPoll() {
33023302
// the message input stays visible above the keyboard.
33033303

33043304
_setupIOSKeyboard() {
3305-
if (!window.visualViewport) return;
3306-
33073305
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
33083306
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
33093307

3310-
if (!isIOS) return;
3308+
// ── Safe-area probing (all mobile, but especially iOS) ──
3309+
// env(safe-area-inset-*) sometimes returns 0 even on notched devices
3310+
// (e.g. certain iOS versions in browser vs PWA mode, or when CSS env()
3311+
// isn't evaluated). We probe the actual value via a hidden element and
3312+
// set CSS custom properties with a minimum floor as fallback.
3313+
if (isIOS || /Android/.test(navigator.userAgent)) {
3314+
const isMobile = window.innerWidth <= 768;
3315+
if (isMobile) {
3316+
document.body.classList.add(isIOS ? 'is-ios' : 'is-android');
3317+
3318+
// Detect standalone PWA mode
3319+
if (isIOS && (window.navigator.standalone || window.matchMedia('(display-mode: standalone)').matches)) {
3320+
document.body.classList.add('is-ios-pwa');
3321+
}
3322+
3323+
// Probe env(safe-area-inset-top) by measuring a hidden div
3324+
const probe = document.createElement('div');
3325+
probe.style.cssText = 'position:fixed;top:0;left:0;width:1px;pointer-events:none;visibility:hidden;'
3326+
+ 'height:env(safe-area-inset-top,0px);height:constant(safe-area-inset-top)';
3327+
document.body.appendChild(probe);
3328+
3329+
requestAnimationFrame(() => {
3330+
const measuredTop = probe.offsetHeight;
3331+
probe.style.cssText = 'position:fixed;bottom:0;left:0;width:1px;pointer-events:none;visibility:hidden;'
3332+
+ 'height:env(safe-area-inset-bottom,0px);height:constant(safe-area-inset-bottom)';
3333+
3334+
requestAnimationFrame(() => {
3335+
const measuredBottom = probe.offsetHeight;
3336+
document.body.removeChild(probe);
3337+
3338+
// Determine minimum safe-area for this device
3339+
let minTop = 0, minBottom = 0;
3340+
if (isIOS) {
3341+
const h = window.screen.height;
3342+
// iPhone X+ / Dynamic Island (screen height >= 812pt)
3343+
if (h >= 812) { minTop = 47; minBottom = 34; }
3344+
// Older iPhones
3345+
else { minTop = 20; minBottom = 0; }
3346+
}
3347+
3348+
const safeTop = Math.max(measuredTop, minTop);
3349+
const safeBottom = Math.max(measuredBottom, minBottom);
3350+
const root = document.documentElement;
3351+
root.style.setProperty('--safe-top', safeTop + 'px');
3352+
root.style.setProperty('--safe-bottom', safeBottom + 'px');
3353+
});
3354+
});
3355+
}
3356+
}
3357+
3358+
if (!window.visualViewport || !isIOS) return;
33113359

33123360
const app = document.getElementById('app');
33133361
const messages = document.getElementById('messages');

0 commit comments

Comments
 (0)