Give TV cards roomy spacing and edge-straddling focus rings
Some checks failed
review/code-gpt Spacing review: progress-bar inset regression; rail transition focus invalidation

This commit is contained in:
justinzeus 2026-11-06 12:53:51 +01:00
parent 9e15dbee8c
commit c17447c528
18 changed files with 210 additions and 142 deletions

View file

@ -23,7 +23,7 @@ fun CatalogueRecoveryCard(retry: Boolean, focused: Boolean, onClick: () -> Unit,
val shape = RoundedCornerShape(WolfskinTokens.Radius.card)
Column(modifier.size(WolfskinTokens.Card.posterW, WolfskinTokens.Card.posterH)
.focusScale(focused, WolfskinTokens.Focus.scaleCard)
.focusRing(focused, WolfskinTokens.Radius.card, WolfskinTokens.Focus.ringCardGap, WolfskinTokens.Focus.ringCardRing, C.accent, FocusLift.Card)
.focusRing(focused, WolfskinTokens.Radius.card, WolfskinTokens.Focus.ringCardGap, WolfskinTokens.Focus.ringCardRing, C.accent, FocusLift.Card, straddle = true)
.background(C.surface, shape).border(G.borderWidth, C.border, shape).clickable(onClick = onClick).padding(G.tailPadding),
verticalArrangement = Arrangement.spacedBy(G.tailGap, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally) {
Glyph(if (retry) "alert" else "down", if (retry) C.warn else C.accentText, Modifier.size(G.tailIcon))

View file

@ -70,7 +70,7 @@ fun DiscoverShelf(state: DiscoverState, lane: DiscoverLane, row: Int) {
modifier = Modifier.offset(y = L.rowHeadingBand - K.posterGap).height(height + K.posterGap * 2)) {
if (services) itemsIndexed(state.snapshot.services, key = { _, service -> "service:${service.id}" }, contentType = { _, _ -> "entry" }) { index, service ->
Box(Modifier.size(K.entryW, K.entryH).focusScale(active && index == column, WolfskinTokens.Focus.scaleCard)
.focusRing(active && index == column, WolfskinTokens.Radius.card, WolfskinTokens.Focus.ringCardGap, WolfskinTokens.Focus.ringCardRing, C.accent, FocusLift.Card)
.focusRing(active && index == column, WolfskinTokens.Radius.card, WolfskinTokens.Focus.ringCardGap, WolfskinTokens.Focus.ringCardRing, C.accent, FocusLift.Card, straddle = true)
.background(Brush.linearGradient(listOf(Color.hsl(service.hue, .30f, .26f), Color.hsl(service.hue, .30f, .12f))), RoundedCornerShape(WolfskinTokens.Radius.card))
.border(WolfskinTokens.Library.borderWidth, C.border.copy(alpha = D.entryBorderOpacity), RoundedCornerShape(WolfskinTokens.Radius.card))
.clickable { state.focus(row, index); state.openService(service) }, contentAlignment = Alignment.BottomStart) {

View file

@ -16,11 +16,13 @@ import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
/** Only transforms animate: focus never changes measured geometry. */
@ -37,9 +39,9 @@ fun Modifier.focusScale(focused: Boolean, scale: Float): Modifier {
/** Content lifts are distinct from the white person/gear rings. */
enum class FocusLift { None, Card, Button }
/** Draw the ring outside the slot; neither the gap nor the ring obscures content. */
/** Card rings straddle the art edge above content; control rings retain their outside gap. */
@Composable
fun Modifier.focusRing(focused: Boolean, radius: Dp, gap: Dp, width: Dp, color: Color, lift: FocusLift = FocusLift.None): Modifier {
fun Modifier.focusRing(focused: Boolean, radius: Dp, gap: Dp, width: Dp, color: Color, lift: FocusLift = FocusLift.None, straddle: Boolean = false): Modifier {
val ringAlpha by animateFloatAsState(if (focused) 1f else 0f,
tween(WolfskinTokens.Motion.fast, easing = CubicBezierEasing(.25f, .1f, .25f, 1f)), label = "focus-ring")
val shadow = if (lift == FocusLift.None) this else {
@ -54,6 +56,17 @@ fun Modifier.focusRing(focused: Boolean, radius: Dp, gap: Dp, width: Dp, color:
alpha = ringAlpha * if (lift == FocusLift.Card) WolfskinTokens.Lift.cardOpacity else WolfskinTokens.Lift.buttonOpacity
}
}
if (straddle) return shadow.drawWithCache {
val stroke = Stroke(width.toPx())
val corner = CornerRadius(radius.toPx())
onDrawWithContent {
drawContent()
if (ringAlpha > 0f) {
drawRoundRect(color.copy(alpha = color.alpha * ringAlpha),
cornerRadius = corner, style = stroke)
}
}
}
return shadow.drawBehind {
if (ringAlpha > 0f) {
val outer = (gap + width).toPx()

View file

@ -91,7 +91,7 @@ class LibraryGridState(
var inRail by mutableStateOf(false)
var railLetter by mutableStateOf('#')
private var railReturn = 0
val columns = WolfskinTokens.Behaviour.gridColumns.toInt()
val columns: Int get() = WolfskinTokens.Behaviour.gridColumns.toInt() - if (railVisible) 1 else 0
val mixed: Boolean get() = catalogue && source.any { it.type == "movie" } && source.any { it.type == "tv" }
val hasToolbar: Boolean get() = !catalogue || mixed
val items: List<LibraryTitle> by derivedStateOf {

View file

@ -94,7 +94,7 @@ fun LibraryScreen(
if (grid.toolbar || row == 0 && active) {
animateLibraryScroll(view, headerCount, 0f, density)
} else if (row > 0) {
val target = with(density) { (L.row_lhead + (if (headerCount == 2) L.row_chips else WolfskinTokens.Shell.zero) + rowHeight * row - L.pinnedLine).toPx() }
val target = with(density) { (L.row_lhead + (if (headerCount == 2) L.row_chipsGrid else WolfskinTokens.Shell.zero) + rowHeight * row - L.pinnedLine).toPx() }
animateLibraryScroll(view, headerCount, target, density)
}
}
@ -186,7 +186,7 @@ fun LibraryHeader(person: ShellPerson, title: String, subtitle: String = "", sav
@Composable
private fun LibraryToolbar(grid: LibraryGridState, active: Boolean) {
Row(Modifier.fillMaxWidth().height(L.row_chips).padding(start = L.safeSide, top = G.chipTop), horizontalArrangement = Arrangement.spacedBy(G.chipGap)) {
Row(Modifier.fillMaxWidth().height(L.row_chipsGrid).padding(start = L.safeSide, top = G.chipTop), horizontalArrangement = Arrangement.spacedBy(G.chipGap)) {
val labels = if (grid.catalogue) listOf("All", "Movies", "Series") else listOf("Sort ${listOf("A–Z", "Recently added", "Rating")[grid.sort]}", if (grid.unwatched) "Unwatched" else "All")
labels.forEachIndexed { index, label ->
val focused = active && grid.toolbar && grid.tool == index
@ -266,7 +266,7 @@ private suspend fun animateLibraryScroll(view: RememberedView, headerCount: Int,
val current = with(density) {
(if (first == 0) WolfskinTokens.Shell.zero else L.row_lhead +
if (first < headerCount) WolfskinTokens.Shell.zero else
(if (headerCount == 2) L.row_chips else WolfskinTokens.Shell.zero) + rowHeight * (first - headerCount)).toPx()
(if (headerCount == 2) L.row_chipsGrid else WolfskinTokens.Shell.zero) + rowHeight * (first - headerCount)).toPx()
} + view.listState.firstVisibleItemScrollOffset
animateFrameScroll(view.listState, target - current)
}

View file

@ -17,7 +17,7 @@ fun TitleCard(focused: Boolean, modifier: Modifier = Modifier, width: Dp = Wolfs
height: Dp = WolfskinTokens.Card.posterH, content: @Composable BoxScope.() -> Unit) {
val shape = RoundedCornerShape(WolfskinTokens.Radius.card)
Box(modifier.size(width, height).focusScale(focused, WolfskinTokens.Focus.scaleCard)
.focusRing(focused, WolfskinTokens.Radius.card, WolfskinTokens.Focus.ringCardGap, WolfskinTokens.Focus.ringCardRing, WolfskinTokens.Color.accent, FocusLift.Card)) {
.focusRing(focused, WolfskinTokens.Radius.card, WolfskinTokens.Focus.ringCardGap, WolfskinTokens.Focus.ringCardRing, WolfskinTokens.Color.accent, FocusLift.Card, straddle = true)) {
Box(Modifier.fillMaxSize().clip(shape).background(WolfskinTokens.Color.surface2), content = content)
}
}

View file

@ -51,9 +51,10 @@ fun TitleCast(state: TitleState, active: Boolean) {
val focusOwner = LocalTitleFocusOwner.current
val pitch = with(LocalDensity.current) { WolfskinTokens.Card.personPitch.toPx() }
LaunchedEffect(state.column, active) { if (active) track.keepColumnInWindow(state.column, WolfskinTokens.Card.personSlots, pitch) }
Column(Modifier.height(D.castHeight)) {
Box(Modifier.height(D.castHeight)) {
Text("Cast", color = C.fg, style = WolfskinTokens.Type.rowHeading, modifier = Modifier.padding(start = L.safeSide).height(L.rowHeadingBand))
LazyRow(state = track, contentPadding = PaddingValues(horizontal = L.safeSide), horizontalArrangement = Arrangement.spacedBy(WolfskinTokens.Card.personGap)) {
LazyRow(state = track, modifier = Modifier.fillMaxWidth().offset(y = L.rowHeadingBand - L.rowGapAfter).height(WolfskinTokens.Card.personH + L.rowGapAfter * 2),
contentPadding = PaddingValues(horizontal = L.safeSide, vertical = L.rowGapAfter), horizontalArrangement = Arrangement.spacedBy(WolfskinTokens.Card.personGap)) {
if (state.data.castStatus == TitleReadStatus.Loading) {
items(WolfskinTokens.Card.personSlots, key = { "cast-skeleton-$it" }, contentType = { "skeleton" }) {
Box(Modifier.size(L.castCircle).background(C.surface, CircleShape))
@ -63,7 +64,7 @@ fun TitleCast(state: TitleState, active: Boolean) {
Box(Modifier.size(WolfskinTokens.Card.personW, WolfskinTokens.Card.personH).zIndex(if (focused) 1f else 0f)
.onGloballyPositioned { state.recordCenter("cast", index, it.boundsInRoot().center.x) }
.titlePointerFocus { state.focus("cast", index) }.focusScale(focused, WolfskinTokens.Focus.scaleCard).clickable { focusOwner?.invoke(); state.click("cast", index) }.testTag("title-cast-${person.id}")) {
PersonPortrait(person, Modifier.size(L.castCircle).focusRing(focused, L.castCircle / 2, WolfskinTokens.Focus.ringCardGap, WolfskinTokens.Focus.ringCardRing, C.accent).clip(CircleShape))
PersonPortrait(person, Modifier.size(L.castCircle).focusRing(focused, L.castCircle / 2, WolfskinTokens.Focus.ringCardGap, WolfskinTokens.Focus.ringCardRing, C.accent, FocusLift.Card, straddle = true).clip(CircleShape))
Column(Modifier.offset(x = -D.castNameSide, y = D.castNameTop).wrapContentWidth(Alignment.Start, unbounded = true).width(L.castCircle + D.castNameSide * 2), horizontalAlignment = Alignment.CenterHorizontally) {
Text(person.name, color = C.fg, style = TitleTextStyles.castName, modifier = Modifier.exactLineBox(TitleTextStyles.castName), maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center)
Text(person.character, color = C.muted, style = TitleTextStyles.caption, modifier = Modifier.exactLineBox(TitleTextStyles.caption), maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center)
@ -85,9 +86,11 @@ fun TitleMediaShelf(state: TitleState, episodes: Boolean, active: Boolean) {
val visible = if (episodes) WolfskinTokens.Card.tileSlots else WolfskinTokens.Card.posterSlots
val pitch = with(LocalDensity.current) { (if (episodes) WolfskinTokens.Card.tilePitch else WolfskinTokens.Card.posterPitch).toPx() }
LaunchedEffect(state.column, active) { if (active) track.keepColumnInWindow(state.column, visible, pitch) }
Column(Modifier.height(L.rowHeadingBand + height + L.rowGapAfter + if (episodes) WolfskinTokens.Shell.zero else WolfskinTokens.Card.posterCaptionBand)) {
Box(Modifier.height(L.rowHeadingBand + height + L.rowGapAfter + if (episodes) WolfskinTokens.Shell.zero else WolfskinTokens.Card.posterCaptionBand)) {
Text(if (episodes) "Season ${state.season}" else "More like this", color = C.fg, style = WolfskinTokens.Type.rowHeading, modifier = Modifier.padding(start = L.safeSide).height(L.rowHeadingBand))
LazyRow(state = track, contentPadding = PaddingValues(horizontal = L.safeSide), horizontalArrangement = Arrangement.spacedBy(WolfskinTokens.Card.posterGap)) {
LazyRow(state = track, modifier = Modifier.fillMaxWidth().offset(y = L.rowHeadingBand - L.rowGapAfter)
.height(height + (if (episodes) WolfskinTokens.Shell.zero else WolfskinTokens.Card.posterCaptionBand) + L.rowGapAfter * 2),
contentPadding = PaddingValues(horizontal = L.safeSide, vertical = L.rowGapAfter), horizontalArrangement = Arrangement.spacedBy(WolfskinTokens.Card.posterGap)) {
if (episodes && state.data.episodesStatus == TitleReadStatus.Loading) {
items(visible, key = { "episode-skeleton-$it" }, contentType = { "skeleton" }) {
Box(Modifier.size(width, height).background(C.surface, RoundedCornerShape(WolfskinTokens.Radius.card)))
@ -112,7 +115,7 @@ fun TitlePoster(item: TitleItem, focused: Boolean, modifier: Modifier = Modifier
LibraryPoster(item, focused, focused && ratingsVisible, catalogue = catalogue,
modifier = Modifier.titlePointerFocus(onFocus).clickable { focusOwner?.invoke(); onClick() }, offlineArt = offlineArt)
} else Box(Modifier.size(WolfskinTokens.Card.tileW, WolfskinTokens.Card.tileH).titlePointerFocus(onFocus).focusScale(focused, WolfskinTokens.Focus.scaleCard)
.focusRing(focused, WolfskinTokens.Radius.card, WolfskinTokens.Focus.ringCardGap, WolfskinTokens.Focus.ringCardRing, C.accent)
.focusRing(focused, WolfskinTokens.Radius.card, WolfskinTokens.Focus.ringCardGap, WolfskinTokens.Focus.ringCardRing, C.accent, FocusLift.Card, straddle = true)
.clip(RoundedCornerShape(WolfskinTokens.Radius.card)).background(C.surface2).clickable { focusOwner?.invoke(); onClick() }) {
TitleTileArtwork(item)
Column(Modifier.align(Alignment.BottomStart).fillMaxWidth().background(Brush.verticalGradient(listOf(C.bg.copy(alpha = 0f), C.bg.copy(alpha = D.cardOverlayOpacity))))

View file

@ -112,9 +112,9 @@ object WolfskinTokens {
val navSolidAfter = 20.0.dp
val pinnedLine = 84.0.dp
val windowAhead = 350.0.dp
val rowHeadingBand = 30.0.dp
val rowGapAfter = 22.0.dp
val gridRowGap = 14.0.dp
val rowHeadingBand = 42.0.dp
val rowGapAfter = 34.0.dp
val gridRowGap = 22.0.dp
val row_hero = 410.0.dp
val row_phero = 370.0.dp
val row_section = 75.0.dp
@ -146,7 +146,7 @@ object WolfskinTokens {
val settingsRowWidth = 650.0.dp
val settingsRowHeight = 52.0.dp
val stateWidth = 550.0.dp
val captionOffset = 9.0.dp
val captionOffset = 19.0.dp
val badge = 24.0.dp
val badgeInset = 7.0.dp
val progressBar = 4.0.dp
@ -159,36 +159,37 @@ object WolfskinTokens {
val personPortrait = 120.0.dp
val thumbWidth = 48.0.dp
val thumbHeight = 72.0.dp
val row_chipsGrid = 68.0.dp
}
object Card {
val posterW = 128.0.dp
val posterH = 192.0.dp
val posterGap = 12.0.dp
val posterPitch = 140.0.dp
val posterCaptionBand = 38.0.dp
val posterGap = 20.0.dp
val posterPitch = 148.0.dp
val posterCaptionBand = 48.0.dp
const val posterSlots = 6
val tileW = 208.0.dp
val tileH = 117.0.dp
val tileGap = 12.0.dp
val tileW = 200.0.dp
val tileH = 112.5.dp
val tileGap = 20.0.dp
val tilePitch = 220.0.dp
val tileCaptionBand = 0.0.dp
const val tileSlots = 4
val wideW = 280.0.dp
val wideH = 157.5.dp
val wideGap = 12.0.dp
val wideW = 272.0.dp
val wideH = 153.0.dp
val wideGap = 20.0.dp
val widePitch = 292.0.dp
val wideCaptionBand = 0.0.dp
const val wideSlots = 3
val entryW = 128.0.dp
val entryH = 72.0.dp
val entryGap = 12.0.dp
val entryPitch = 140.0.dp
val entryGap = 20.0.dp
val entryPitch = 148.0.dp
val entryCaptionBand = 0.0.dp
const val entrySlots = 6
val personW = 100.0.dp
val personH = 145.0.dp
val personGap = 14.0.dp
val personPitch = 114.0.dp
val personH = 155.0.dp
val personGap = 22.0.dp
val personPitch = 122.0.dp
val personCaptionBand = 0.0.dp
const val personSlots = 7
}
@ -196,7 +197,7 @@ object WolfskinTokens {
const val scaleCard = 1.08f
const val scaleControl = 1.05f
const val scaleRow = 1.02f
val ringCardGap = 2.0.dp
val ringCardGap = 0.0.dp
val ringCardRing = 2.0.dp
val ringButtonGap = 2.0.dp
val ringButtonRing = 2.0.dp
@ -429,8 +430,8 @@ object WolfskinTokens {
val progressBottom = 15.0.dp
val progressGap = 9.0.dp
val progressLift = -7.0.dp
val castHeight = 197.0.dp
val castNameTop = 108.0.dp
val castHeight = 231.0.dp
val castNameTop = 118.0.dp
val castNameSide = 6.0.dp
val personTop = 85.0.dp
val personGap = 24.0.dp
@ -456,7 +457,7 @@ object WolfskinTokens {
val infoRowGap = 3.0.dp
val infoColumnGap = 10.0.dp
val infoKey = TextStyle(fontSize = 12.0.sp, fontWeight = FontWeight.Bold, lineHeight = 15.600000000000001.sp, letterSpacing = 0.0.em)
val gridHeight = 244.0.dp
val gridHeight = 262.0.dp
val minimumEndPadding = 40.0.dp
val ratingGap = 10.0.dp
val ratingMarkGap = 4.0.dp

View file

@ -115,5 +115,6 @@ class HomeCaptureTest {
@Test fun st101_unratedEpisode() = capture("st-101", person = "mindel", row = "nu", column = 2)
@Test fun st102_filmTile() = capture("st-102", person = "mindel", row = "new", column = 4)
@Test fun st103_heroRatings() = capture("st-103")
@Test fun spacingNextUpFirstFocused() = capture("spacing-next-up", row = "nu")
@Test fun xHomeDiscoverStart() = capture("x-home-discover-start", row = "pk-m")
}

View file

@ -82,6 +82,14 @@ class LibraryCaptureTest {
@Test fun st112_cachedWhileDown() = capture("st-112", reachable = false, cached = true)
@Test fun st113_uncachedWhileDown() = capture("st-113", reachable = false)
@Test fun unsignedRatingsHidden() = capture("library-unsigned", ratings = false)
@Test fun spacingShowsFirstFocused() = capture("spacing-shows", "mindel", "shows")
@Test fun spacingLetterRailClearsFocusedFifthColumn() = capture("spacing-letter-rail") { grid ->
val original = grid.source
grid.source = List(200) { index ->
original[index % original.size].copy(id = "rail-$index", title = "${('A'.code + index / 10).toChar()} Title $index")
}
grid.index = 4
}
}
fun libraryFixture(): LibraryFixture = LibraryFixture(LibraryCaptureTest::class.java.getResourceAsStream("/wolfskin-library.json")!!.bufferedReader().use { it.readText() }, "/library/")

View file

@ -112,18 +112,25 @@ class LibraryKeysTest {
val titles = (0 until 200).map { index -> LibraryTitle("$index", "${('A'.code + index / 10).toChar()}Title $index", "movie") }
val grid = LibraryGridState("Movies", titles)
assertTrue(grid.railVisible)
grid.index = 5
grid.index = 4
grid.key(ShellKey.Right)
assertTrue(grid.inRail)
grid.key(ShellKey.Down)
assertEquals('B', grid.railLetter)
grid.key(ShellKey.Back)
assertEquals(5, grid.index)
assertEquals(4, grid.index)
assertFalse(grid.inRail)
grid.key(ShellKey.Down)
assertEquals(9, grid.index)
grid.key(ShellKey.Up)
assertEquals(4, grid.index)
grid.key(ShellKey.Right); grid.key(ShellKey.Down); grid.key(ShellKey.Ok)
assertEquals('B', grid.focusedTitle!!.letter)
grid.source = titles.take(199)
assertFalse(grid.railVisible)
grid.index = 4
grid.key(ShellKey.Right)
assertEquals(5, grid.index)
grid.source = titles; grid.sort = 1
assertFalse(grid.railVisible)
}

View file

@ -1,6 +1,6 @@
{
"id": "library",
"title": "Libraries: Movies, Shows, Anime (one continuous 6-column grid)",
"title": "Libraries: Movies, Shows, Anime (continuous grid; five columns with letter rail)",
"specSection": "6.4",
"source": "index.html:1403-1426, 1563-1611",
"layout": [
@ -22,7 +22,7 @@
"token": null,
"title": "",
"yPx": 236,
"heightPx": 104,
"heightPx": 136,
"cards": null,
"focusable": true,
"endsInSeeAll": false,
@ -33,8 +33,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 340,
"heightPx": 488,
"yPx": 372,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -45,8 +45,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 828,
"heightPx": 488,
"yPx": 896,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -57,8 +57,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 1316,
"heightPx": 488,
"yPx": 1420,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -69,8 +69,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 1804,
"heightPx": 488,
"yPx": 1944,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -81,8 +81,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 2292,
"heightPx": 488,
"yPx": 2468,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -93,8 +93,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 2780,
"heightPx": 488,
"yPx": 2992,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -105,8 +105,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 3268,
"heightPx": 488,
"yPx": 3516,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -117,8 +117,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 3756,
"heightPx": 488,
"yPx": 4040,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -129,8 +129,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 4244,
"heightPx": 488,
"yPx": 4564,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -141,8 +141,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 4732,
"heightPx": 488,
"yPx": 5088,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -153,8 +153,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 5220,
"heightPx": 488,
"yPx": 5612,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -165,8 +165,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 5708,
"heightPx": 488,
"yPx": 6136,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -177,8 +177,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 6196,
"heightPx": 488,
"yPx": 6660,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,
@ -189,8 +189,8 @@
"type": "grid",
"token": "poster",
"title": "",
"yPx": 6684,
"heightPx": 488,
"yPx": 7184,
"heightPx": 524,
"cards": 6,
"focusable": true,
"endsInSeeAll": false,