fix: mobile viewport cutoff (100dvh) + fit view content anchoring (flex-end)
Mobile: 100vh includes browser chrome on mobile — bottom row clipped. Fix: 100dvh with 100vh fallback (progressive enhancement) in .view and .session-tile--expanded. Fit view: scrollTop hack reset every 2s by poll cycle and had no effect when content didn't overflow. Fix: replace position:absolute+scrollTop with flex+justify-content:flex-end on tile-body. Content naturally anchors to bottom without JS. No scrollbar hiding needed. Tests: remove test_fit_view_pre_has_top_zero (old behavior), add test_view_uses_dvh_fallback, test_fit_view_tile_body_uses_flex_end, test_fit_view_pre_is_static. Update JS test for new flex approach.
This commit is contained in:
@@ -656,10 +656,7 @@ function renderGrid(sessions) {
|
|||||||
grid.classList.add('session-grid--fit');
|
grid.classList.add('session-grid--fit');
|
||||||
requestAnimationFrame(function() {
|
requestAnimationFrame(function() {
|
||||||
applyFitLayout(grid);
|
applyFitLayout(grid);
|
||||||
// Scroll each tile's pre to the bottom so content anchors at the bottom (like a real terminal)
|
// No scrollTop hack needed — CSS flex + justify-content:flex-end anchors content to bottom
|
||||||
grid.querySelectorAll('.tile-body pre').forEach(function(pre) {
|
|
||||||
pre.scrollTop = pre.scrollHeight;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-10
@@ -83,7 +83,8 @@ body {
|
|||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
.view {
|
.view {
|
||||||
height: 100vh;
|
height: 100vh; /* fallback for browsers without dvh support */
|
||||||
|
height: 100dvh; /* dynamic viewport height — adjusts for mobile browser chrome */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,7 +594,8 @@ body {
|
|||||||
top: 0 !important;
|
top: 0 !important;
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
height: 100vh !important;
|
height: 100vh !important; /* fallback for browsers without dvh support */
|
||||||
|
height: 100dvh !important; /* dynamic viewport height — adjusts for mobile browser chrome */
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1380,16 +1382,18 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* In fit mode, stretch pre to fill the full tile body height and anchor content to bottom */
|
/* In fit mode, tile body uses flex to anchor content at the bottom (like a real terminal) */
|
||||||
.session-grid--fit .tile-body pre {
|
.session-grid--fit .tile-body {
|
||||||
top: 0; /* stretch to fill full tile body height in fit mode */
|
display: flex;
|
||||||
overflow-y: scroll; /* enable scrollTop positioning (content anchored via JS scrollTop=scrollHeight) */
|
flex-direction: column;
|
||||||
scrollbar-width: none; /* Firefox: hide scrollbar */
|
justify-content: flex-end; /* content anchored to bottom */
|
||||||
-ms-overflow-style: none; /* IE/Edge: hide scrollbar */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-grid--fit .tile-body pre::-webkit-scrollbar {
|
/* In fit mode, pre participates in flex layout (not absolute-positioned) */
|
||||||
display: none; /* Chrome/Safari: hide scrollbar */
|
.session-grid--fit .tile-body pre {
|
||||||
|
position: static; /* remove absolute positioning — flex layout controls position */
|
||||||
|
max-height: 100%; /* don't overflow the tile body */
|
||||||
|
overflow: hidden; /* clip excess content at the top */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|||||||
@@ -2234,10 +2234,17 @@ test('buildTileHTML shows up to 80 lines in fit mode', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('CSS style.css has scrollbar-width none for fit mode pre to hide scrollbar', () => {
|
test('CSS style.css uses flex layout (not scrollbar hack) for fit mode content anchoring', () => {
|
||||||
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
|
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
|
||||||
|
// New approach: flex + justify-content:flex-end anchors content to bottom without JS scrollTop hacks
|
||||||
assert.ok(
|
assert.ok(
|
||||||
source.includes('scrollbar-width: none'),
|
source.includes('.session-grid--fit .tile-body'),
|
||||||
'style.css must have scrollbar-width: none for hidden scrollbar in fit mode pre'
|
'style.css must have .session-grid--fit .tile-body rule for flex layout'
|
||||||
);
|
);
|
||||||
|
assert.ok(
|
||||||
|
source.includes('justify-content: flex-end') || source.includes('justify-content:flex-end'),
|
||||||
|
'style.css must use justify-content: flex-end to anchor fit mode content at bottom'
|
||||||
|
);
|
||||||
|
// Old scrollbar-hiding hack is no longer needed with flex approach
|
||||||
|
// (scrollbar-width: none was only needed when overflow-y:scroll was used for scrollTop positioning)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1712,19 +1712,73 @@ def test_css_no_compact_tile_height() -> None:
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
def test_fit_view_pre_has_top_zero() -> None:
|
# ============================================================
|
||||||
"""In fit mode, .tile-body pre must have top:0 to fill the full tile height.
|
# Mobile viewport + fit view content anchoring fixes
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
Bug: .tile-body pre uses position:absolute with bottom:0 but no top:0.
|
|
||||||
In fit mode where tiles are taller than auto mode, the pre is anchored
|
def test_view_uses_dvh_fallback() -> None:
|
||||||
to the bottom but only takes natural content height, leaving a black gap above.
|
"""Bug fix: .view must use 100dvh (dynamic viewport height) for mobile.
|
||||||
Fix: .session-grid--fit .tile-body pre { top: 0 } stretches it to fill the tile.
|
|
||||||
|
On mobile browsers, 100vh includes the browser chrome (address bar + bottom nav),
|
||||||
|
causing the bottom row of tiles to be cut off under overflow:hidden.
|
||||||
|
Fix: height: 100dvh with 100vh fallback (progressive enhancement).
|
||||||
|
The 100vh MUST appear before 100dvh (browsers ignore unknown values, so
|
||||||
|
100dvh overrides 100vh for browsers that support it).
|
||||||
|
"""
|
||||||
|
css = read_css()
|
||||||
|
# .view block must contain 100dvh
|
||||||
|
block = _extract_rule_block(css, ".view {")
|
||||||
|
assert "100dvh" in block, (
|
||||||
|
".view must use height: 100dvh for mobile — 100vh includes browser chrome, "
|
||||||
|
"causing the bottom row to be cut off on mobile devices"
|
||||||
|
)
|
||||||
|
# 100vh must still be present as fallback (appears before 100dvh in file)
|
||||||
|
assert "100vh" in block, (
|
||||||
|
".view must keep height: 100vh as fallback for browsers without dvh support"
|
||||||
|
)
|
||||||
|
# Verify order: 100vh must come before 100dvh in the block (fallback first)
|
||||||
|
assert block.index("100vh") < block.index("100dvh"), (
|
||||||
|
"height: 100vh (fallback) must appear BEFORE height: 100dvh in .view rule — "
|
||||||
|
"browsers that don't support dvh will use the last valid value"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fit_view_tile_body_uses_flex_end() -> None:
|
||||||
|
"""Fit mode tile-body must use flex + justify-content: flex-end for bottom anchoring.
|
||||||
|
|
||||||
|
Bug: .tile-body pre had position:absolute + scrollTop=scrollHeight hack.
|
||||||
|
The scrollTop hack resets every 2s when innerHTML is rebuilt by the poll cycle,
|
||||||
|
and has no effect when content doesn't overflow.
|
||||||
|
Fix: make tile-body a flex container with justify-content:flex-end so the pre
|
||||||
|
naturally anchors to the bottom without JS.
|
||||||
|
"""
|
||||||
|
css = read_css()
|
||||||
|
assert ".session-grid--fit .tile-body" in css, (
|
||||||
|
"Missing .session-grid--fit .tile-body rule — needed for flex-end bottom anchoring"
|
||||||
|
)
|
||||||
|
block = _extract_rule_block(css, ".session-grid--fit .tile-body {")
|
||||||
|
assert "display: flex" in block or "display:flex" in block, (
|
||||||
|
".session-grid--fit .tile-body must use display: flex"
|
||||||
|
)
|
||||||
|
assert "justify-content: flex-end" in block or "justify-content:flex-end" in block, (
|
||||||
|
".session-grid--fit .tile-body must use justify-content: flex-end to anchor content to bottom"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fit_view_pre_is_static() -> None:
|
||||||
|
"""Fit mode pre must use position:static (not absolute) for flex layout.
|
||||||
|
|
||||||
|
Bug: .tile-body pre uses position:absolute which takes it out of flex flow.
|
||||||
|
In fit mode, we need position:static so flex layout controls the pre's position.
|
||||||
|
The parent .tile-body is a flex column with justify-content:flex-end, so the
|
||||||
|
pre will naturally sit at the bottom.
|
||||||
"""
|
"""
|
||||||
css = read_css()
|
css = read_css()
|
||||||
assert ".session-grid--fit .tile-body pre" in css, (
|
assert ".session-grid--fit .tile-body pre" in css, (
|
||||||
"Missing .session-grid--fit .tile-body pre rule — needed to fix pre height in fit mode"
|
"Missing .session-grid--fit .tile-body pre rule"
|
||||||
)
|
)
|
||||||
block = _extract_rule_block(css, ".session-grid--fit .tile-body pre {")
|
block = _extract_rule_block(css, ".session-grid--fit .tile-body pre {")
|
||||||
assert "top: 0" in block or "top:0" in block, (
|
assert "position: static" in block or "position:static" in block, (
|
||||||
".session-grid--fit .tile-body pre must have top: 0 to fill full tile body height"
|
".session-grid--fit .tile-body pre must have position: static to participate in flex layout"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user