channel page: park the grid unconditionally, tighter header band

The old park ran only while `firstVisibleItemIndex > 0`, but the focus
pull that hands the first card its focus leaves the header row at index
0 while slid up by ~96 dp — so the guard never fired and the top of the
channel header stayed clipped off screen (#31 review). scrollToItem(0)
re-anchors both index and offset.

The banner band drops to 116 dp with a 40 dp description strip, so the
whole header is under one card row: the grid never reflows around it and
the header stays fully on screen when the first card takes focus.

Addresses #27, #31.
This commit is contained in:
Jeena 2026-10-19 13:08:00 +00:00
parent b9419e6a65
commit f371a21e17

View file

@ -92,10 +92,14 @@ fun VideoListScreen(
// handing the first card the focus animates the grid up (a focused card // handing the first card the focus animates the grid up (a focused card
// that sits partly off-screen is pulled into view), and this effect then // that sits partly off-screen is pulled into view), and this effect then
// snapped it back down — the jump that was still reported on the channel // snapped it back down — the jump that was still reported on the channel
// page when the first video got focus (#27 review). Doing the park first, // page when the first video got focus (#27 review).
// and only while the grid is actually displaced, leaves nothing to undo. // The park has to be unconditional, and has to reset the row's offset,
// not just its index: the focus pull leaves the header row at index 0 but
// slid up by ~96 dp, so a guard on firstVisibleItemIndex > 0 never fires
// in that state and the top of the channel header stays clipped off
// screen (#31 review). scrollToItem(0) re-anchors both.
LaunchedEffect(items.isNotEmpty(), source) { LaunchedEffect(items.isNotEmpty(), source) {
if (items.isNotEmpty() && gridState.firstVisibleItemIndex > 0) { if (items.isNotEmpty()) {
runCatching { gridState.scrollToItem(0) } runCatching { gridState.scrollToItem(0) }
} }
} }
@ -436,12 +440,14 @@ private fun ChannelHeader(
.clip(androidx.compose.foundation.shape.RoundedCornerShape(12.dp)) .clip(androidx.compose.foundation.shape.RoundedCornerShape(12.dp))
.background(Color.Black), .background(Color.Black),
) { ) {
// Banner area: a fixed height with the identity + stats anchored to its // Banner area: 116 dp, with the identity + stats anchored to its bottom; the
// bottom; the description sits in the black strip below it. // description sits in the black strip below it. The whole band is 160 dp
// (4 + 116 + 40) — under one card row — so the grid never reflows around it
// and the band stays fully on screen when the first card takes focus (#31).
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(178.dp) .height(116.dp)
.background(androidx.compose.ui.graphics.Brush.linearGradient(accent)), .background(androidx.compose.ui.graphics.Brush.linearGradient(accent)),
) { ) {
if (banner != null) { if (banner != null) {
@ -563,7 +569,7 @@ private fun ChannelHeader(
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(42.dp) .height(40.dp)
.padding(start = 16.dp, end = 16.dp, top = 6.dp), .padding(start = 16.dp, end = 16.dp, top = 6.dp),
) { ) {
if (description != null) { if (description != null) {