fix(dashboard): compensate Android Chrome inflated ICB for fixed-position UI
Some Android Chrome builds (multi-window / split-screen / certain WebViews) leave window.innerWidth/Height stuck larger than the actual rendered canvas. DOM, body, and visualViewport report the true dimensions, but position:fixed uses the ICB, pinning fixed-bottom elements offscreen below the visible area. JS-side meta override (setAttribute and full replacement) does not force Chrome to recompute the ICB on those builds. index.html now publishes the ICB→visualViewport delta as CSS variables (--icb-bottom-offset, --icb-right-offset) on <html>. MobileNavBar.css and ExecutorStatusBar.css consume them so the bars pin to the visible viewport edge regardless of ICB drift. Math is visualViewport-relative so it also handles pinch-zoom in (offsets compensate) and pinch-zoom out (clamp at 0). Healthy browsers see 0px and behave unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
9
.changeset/mobile-icb-compensation.md
Normal file
9
.changeset/mobile-icb-compensation.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@fusion/dashboard": patch
|
||||
---
|
||||
|
||||
fix(dashboard): compensate Android Chrome inflated ICB for fixed-position UI
|
||||
|
||||
Android Chrome (multi-window / split-screen / certain WebView configs) can leave the initial containing block (window.innerWidth/Height) stuck larger than the actual rendered canvas — DOM, body, and visualViewport report the true dimensions, but `position: fixed` uses the ICB, pinning fixed-bottom elements off the bottom of the visible viewport. JS-side meta override (both setAttribute and full element replacement) does not force Chrome to recompute the ICB on these builds.
|
||||
|
||||
Instead, publish the ICB→visualViewport delta as CSS variables (`--icb-bottom-offset`, `--icb-right-offset`) on `<html>` and consume them in `MobileNavBar` and `ExecutorStatusBar` so they pin to the visible viewport edge regardless of ICB drift. The math also handles pinch-zoom in (offsets compensate) and pinch-zoom out (offsets clamp at 0). Healthy browsers see 0px and behave unchanged.
|
||||
@@ -6,9 +6,11 @@
|
||||
*/
|
||||
.executor-status-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
/* See MobileNavBar.css — ICB compensation for Android Chrome's inflated
|
||||
initial containing block. Defaults to 0px on healthy browsers. */
|
||||
bottom: var(--icb-bottom-offset, 0px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
right: var(--icb-right-offset, 0px);
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -289,7 +291,7 @@
|
||||
font-size: 11px;
|
||||
height: calc(var(--space-lg) * 2 + var(--space-xs));
|
||||
overflow: hidden;
|
||||
bottom: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom, 0px) + var(--standalone-bottom-gap));
|
||||
bottom: calc(var(--icb-bottom-offset, 0px) + var(--mobile-nav-height) + env(safe-area-inset-bottom, 0px) + var(--standalone-bottom-gap));
|
||||
}
|
||||
|
||||
.executor-status-bar__segment {
|
||||
|
||||
@@ -10,9 +10,14 @@
|
||||
|
||||
.mobile-nav-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
/* --icb-bottom-offset / --icb-right-offset are published by the viewport
|
||||
compensation script in index.html. On healthy browsers they stay 0px.
|
||||
On Android Chrome when the ICB is inflated past the visible canvas
|
||||
(multi-window / split-screen quirk) and on pinch-zoom, they pull
|
||||
fixed-position elements back into the visible viewport. */
|
||||
bottom: var(--icb-bottom-offset, 0px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
right: var(--icb-right-offset, 0px);
|
||||
z-index: 45;
|
||||
display: none;
|
||||
align-items: stretch;
|
||||
@@ -32,7 +37,7 @@
|
||||
}
|
||||
|
||||
.mobile-nav-bar--with-footer {
|
||||
bottom: 0;
|
||||
bottom: var(--icb-bottom-offset, 0px);
|
||||
}
|
||||
|
||||
/* Content padding: mobile nav only (no footer). Bar is flush at bottom (no safe-area pad). */
|
||||
|
||||
@@ -8,6 +8,54 @@
|
||||
this, Chrome decouples layout viewport (innerHeight) from visual
|
||||
viewport, leaving innerHeight stale and breaking keyboard-overlap math. -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<script>
|
||||
// Android Chrome (multi-window / split-screen / certain WebView modes)
|
||||
// can leave the initial containing block (window.innerWidth/Height)
|
||||
// stuck larger than the actual rendered canvas, while the DOM,
|
||||
// visualViewport, and body all report the true dimensions. JS cannot
|
||||
// force Chrome to recompute the ICB (neither setAttribute nor full
|
||||
// element replacement of <meta viewport> works on those builds). So
|
||||
// we publish the ICB→visible-viewport delta as CSS variables and
|
||||
// `position: fixed` elements (nav bar, status bar) compensate via
|
||||
// `bottom: var(--icb-bottom-offset)` etc. Healthy browsers see 0px.
|
||||
(function () {
|
||||
if (typeof window === 'undefined') return;
|
||||
var apply = function () {
|
||||
var vv = window.visualViewport;
|
||||
var de = document.documentElement;
|
||||
if (!de) return;
|
||||
var innerW = window.innerWidth;
|
||||
var innerH = window.innerHeight;
|
||||
// Position relative to the visual viewport (what the user actually
|
||||
// sees). This works correctly across states:
|
||||
// - Healthy: vv == ICB, offsets are 0.
|
||||
// - Chrome ICB-stuck-large bug: vv smaller than ICB → offset > 0
|
||||
// pulls fixed elements up/left into the visible area.
|
||||
// - Pinch-zoom in: vv smaller than ICB → ditto.
|
||||
// - Pinch-zoom out: vv larger than ICB → offsets clamp at 0.
|
||||
var vvW = vv ? Math.round(vv.width) : innerW;
|
||||
var vvH = vv ? Math.round(vv.height) : innerH;
|
||||
var vvOffTop = vv ? vv.offsetTop : 0;
|
||||
var vvOffLeft = vv ? vv.offsetLeft : 0;
|
||||
var rightOffset = Math.max(0, innerW - vvOffLeft - vvW);
|
||||
var bottomOffset = Math.max(0, innerH - vvOffTop - vvH);
|
||||
de.style.setProperty('--icb-right-offset', rightOffset + 'px');
|
||||
de.style.setProperty('--icb-bottom-offset', bottomOffset + 'px');
|
||||
};
|
||||
window.addEventListener('orientationchange', apply);
|
||||
window.addEventListener('resize', apply);
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', apply);
|
||||
window.visualViewport.addEventListener('scroll', apply);
|
||||
}
|
||||
apply();
|
||||
// Tail: ICB can drift after task content renders.
|
||||
setTimeout(apply, 100);
|
||||
setTimeout(apply, 500);
|
||||
setTimeout(apply, 1500);
|
||||
setTimeout(apply, 3000);
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Append ?vpdebug to the URL to show a fixed overlay with live viewport
|
||||
// + media-query state. Diagnostic only.
|
||||
|
||||
Reference in New Issue
Block a user