Give TV cards roomy spacing and edge-straddling focus rings #40
20 changed files with 280 additions and 149 deletions
|
|
@ -87,7 +87,7 @@ class TitleCaptureTest {
|
||||||
data.cast.forEach { person -> person.portrait?.let { add(Triple(it, 200, 200)) } }
|
data.cast.forEach { person -> person.portrait?.let { add(Triple(it, 200, 200)) } }
|
||||||
data.related.forEach { item -> item.poster?.let { add(Triple(it, 256, 384)) } }
|
data.related.forEach { item -> item.poster?.let { add(Triple(it, 256, 384)) } }
|
||||||
data.episodes.forEach { item ->
|
data.episodes.forEach { item ->
|
||||||
(if (item.isPosterBackdrop) item.poster else item.backdrop)?.let { add(Triple(it, 416, 234)) }
|
(if (item.isPosterBackdrop) item.poster else item.backdrop)?.let { add(Triple(it, (WolfskinTokens.Card.tileW.value * 2).toInt(), (WolfskinTokens.Card.tileH.value * 2).toInt())) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return requests.distinctBy { it.first }.associate { (path, width, height) ->
|
return requests.distinctBy { it.first }.associate { (path, width, height) ->
|
||||||
|
|
@ -215,4 +215,5 @@ class TitleCaptureTest {
|
||||||
@Test fun st107_seriesEpisodeRatings() = capture(107, "Andor")
|
@Test fun st107_seriesEpisodeRatings() = capture(107, "Andor")
|
||||||
@Test fun st108_resumingEpisodeRatings() = capture(108, "episode-sex-and-the-country-2001-s4e9", "mindel")
|
@Test fun st108_resumingEpisodeRatings() = capture(108, "episode-sex-and-the-country-2001-s4e9", "mindel")
|
||||||
@Test fun st109_noRatings() = capture(109, "The Amazing Spider-Man")
|
@Test fun st109_noRatings() = capture(109, "The Amazing Spider-Man")
|
||||||
|
@Test fun spacingCastFirstFocused() = capture(114) { state, _ -> state.focus("cast", 0) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -310,9 +310,10 @@ and device deployment remain separate.
|
||||||
## Libraries and See all
|
## Libraries and See all
|
||||||
|
|
||||||
Movies, Shows and Anime use the shell's retained view and a continuous six-column
|
Movies, Shows and Anime use the shell's retained view and a continuous six-column
|
||||||
lazy grid. Up from its first row reaches Sort/Filter, then the nav; Back preserves
|
lazy grid, reduced to five columns while the letter rail is visible so scaled
|
||||||
the card and scroll. Sort cycles A–Z, Recently added and Rating. The letter rail
|
posters cannot overlap it. Up from the first row reaches Sort/Filter, then the nav;
|
||||||
exists only at 200 titles with A–Z selected. Card OK activates on key-up through
|
Back preserves the card and scroll. Sort cycles A–Z, Recently added and Rating.
|
||||||
|
The letter rail exists only at 200 titles with A–Z selected. Card OK activates on key-up through
|
||||||
the same shell hold guard, without a long-press menu.
|
the same shell hold guard, without a long-press menu.
|
||||||
Down clamps a short final title row before entering the separate Try again / Show
|
Down clamps a short final title row before entering the separate Try again / Show
|
||||||
more row; an empty slot cannot activate recovery. An unavailable library hides
|
more row; an empty slot cannot activate recovery. An unavailable library hides
|
||||||
|
|
@ -347,6 +348,39 @@ music keep their upstream destinations.
|
||||||
failure/retry, stale copies, and person/server isolation. Library captures are written
|
failure/retry, stale copies, and person/server isolation. Library captures are written
|
||||||
to `build/wolfskin-captures/library/`; Discover's independent captures use `discover/`.
|
to `build/wolfskin-captures/library/`; Discover's independent captures use `discover/`.
|
||||||
|
|
||||||
|
### Roomy card spacing
|
||||||
|
|
||||||
|
The generated design tokens use 40 px card gaps (44 px for cast), 84 px shelf
|
||||||
|
heading bands, 68 px shelf bottom gaps and 524 px grid rows at 1920×1080.
|
||||||
|
Posters remain 256×384 px; tiles are 400×225 px and wide cards 544×306 px,
|
||||||
|
preserving six/four/three shelf slots. Library and See all toolbars occupy 136 px:
|
||||||
|
chips stay at y 252–320 and the first poster row starts at y 372 (52 px clear).
|
||||||
|
Discover and season-chip rows retain their 104 px height.
|
||||||
|
|
||||||
|
Cards and cast circles draw a single 4 px accent stroke centred on the art edge,
|
||||||
|
above the content, with no background gap. Card lift parameters are unchanged;
|
||||||
|
cast portraits and episode tiles now receive that lift too. The stroke and art
|
||||||
|
scale together at 1.08; buttons, avatars, chips and picker focus are unchanged.
|
||||||
|
Title shelves reserve vertical drawing room for the scaled ring.
|
||||||
|
Before scaling the stroke, ring-to-neighbour clearances round to 28/22/16 px
|
||||||
|
for poster/tile/wide cards; including stroke scale they are 27.6/21.8/16.1 px.
|
||||||
|
|
||||||
|
Poster progress uses the independent 18 px title-card inset, not caption spacing:
|
||||||
|
the track remains 220 px wide on a 256 px poster. Library focus-follow restarts
|
||||||
|
when the effective column count, focused row or header geometry changes, retaining
|
||||||
|
deep focus across live snapshots that add or remove the letter rail.
|
||||||
|
`LibrarySpacingRegressionTest` renders both progress geometry and the unchanged
|
||||||
|
focused index across the 199↔200-title boundary.
|
||||||
|
|
||||||
|
`LibraryCaptureTest.spacingShowsFirstFocused`,
|
||||||
|
`HomeCaptureTest.spacingNextUpFirstFocused` and
|
||||||
|
`TitleCaptureTest.spacingCastFirstFocused` render the amended reference states
|
||||||
|
at native resolution. Their files are `library/spacing-shows.png`,
|
||||||
|
`home/spacing-next-up.png` and `st-114.png` under `build/wolfskin-captures/`.
|
||||||
|
These captures are recordings, not automated reference-pixel assertions; pair
|
||||||
|
measurements are a separate check. Platform Roboto metrics and fixture ordering
|
||||||
|
are not browser pixel parity.
|
||||||
|
|
||||||
## Title pages and people
|
## Title pages and people
|
||||||
|
|
||||||
`TitleSurface` owns the film/series/episode header, season chips, episode tiles,
|
`TitleSurface` owns the film/series/episode header, season chips, episode tiles,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ fun CatalogueRecoveryCard(retry: Boolean, focused: Boolean, onClick: () -> Unit,
|
||||||
val shape = RoundedCornerShape(WolfskinTokens.Radius.card)
|
val shape = RoundedCornerShape(WolfskinTokens.Radius.card)
|
||||||
Column(modifier.size(WolfskinTokens.Card.posterW, WolfskinTokens.Card.posterH)
|
Column(modifier.size(WolfskinTokens.Card.posterW, WolfskinTokens.Card.posterH)
|
||||||
.focusScale(focused, WolfskinTokens.Focus.scaleCard)
|
.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),
|
.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) {
|
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))
|
Glyph(if (retry) "alert" else "down", if (retry) C.warn else C.accentText, Modifier.size(G.tailIcon))
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
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 ->
|
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)
|
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))
|
.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))
|
.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) {
|
.clickable { state.focus(row, index); state.openService(service) }, contentAlignment = Alignment.BottomStart) {
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@ import androidx.compose.ui.draw.dropShadow
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
/** Only transforms animate: focus never changes measured geometry. */
|
/** 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. */
|
/** Content lifts are distinct from the white person/gear rings. */
|
||||||
enum class FocusLift { None, Card, Button }
|
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
|
@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,
|
val ringAlpha by animateFloatAsState(if (focused) 1f else 0f,
|
||||||
tween(WolfskinTokens.Motion.fast, easing = CubicBezierEasing(.25f, .1f, .25f, 1f)), label = "focus-ring")
|
tween(WolfskinTokens.Motion.fast, easing = CubicBezierEasing(.25f, .1f, .25f, 1f)), label = "focus-ring")
|
||||||
val shadow = if (lift == FocusLift.None) this else {
|
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
|
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 {
|
return shadow.drawBehind {
|
||||||
if (ringAlpha > 0f) {
|
if (ringAlpha > 0f) {
|
||||||
val outer = (gap + width).toPx()
|
val outer = (gap + width).toPx()
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ class LibraryGridState(
|
||||||
var inRail by mutableStateOf(false)
|
var inRail by mutableStateOf(false)
|
||||||
var railLetter by mutableStateOf('#')
|
var railLetter by mutableStateOf('#')
|
||||||
private var railReturn = 0
|
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 mixed: Boolean get() = catalogue && source.any { it.type == "movie" } && source.any { it.type == "tv" }
|
||||||
val hasToolbar: Boolean get() = !catalogue || mixed
|
val hasToolbar: Boolean get() = !catalogue || mixed
|
||||||
val items: List<LibraryTitle> by derivedStateOf {
|
val items: List<LibraryTitle> by derivedStateOf {
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ fun LibraryPoster(
|
||||||
if (title.ratings.isNotEmpty()) LibraryRatingPlate(title.ratings, visible = ratingsVisible)
|
if (title.ratings.isNotEmpty()) LibraryRatingPlate(title.ratings, visible = ratingsVisible)
|
||||||
val progress = title.progress
|
val progress = title.progress
|
||||||
if (progress > 0f && progress < 1f) {
|
if (progress > 0f && progress < 1f) {
|
||||||
Box(Modifier.align(Alignment.BottomCenter).padding(horizontal = WolfskinTokens.Layout.captionOffset, vertical = WolfskinTokens.Layout.navItemGap)
|
Box(Modifier.align(Alignment.BottomCenter).padding(horizontal = WolfskinTokens.Title.cardProgressInset, vertical = WolfskinTokens.Layout.navItemGap)
|
||||||
.fillMaxWidth().height(WolfskinTokens.Layout.progressBar).background(C.progressTrack, CircleShape)) {
|
.fillMaxWidth().height(WolfskinTokens.Layout.progressBar).testTag("poster-progress-${title.id}").background(C.progressTrack, CircleShape)) {
|
||||||
Box(Modifier.fillMaxHeight().fillMaxWidth(progress).background(C.accent, CircleShape))
|
Box(Modifier.fillMaxHeight().fillMaxWidth(progress).background(C.accent, CircleShape))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,13 +88,13 @@ fun LibraryScreen(
|
||||||
}
|
}
|
||||||
val headerCount = if (grid.hasToolbar && grid.status != LibraryStatus.Loading && grid.status != LibraryStatus.Unavailable) 2 else 1
|
val headerCount = if (grid.hasToolbar && grid.status != LibraryStatus.Loading && grid.status != LibraryStatus.Unavailable) 2 else 1
|
||||||
val rowHeight = K.posterH + K.posterCaptionBand + L.gridRowGap
|
val rowHeight = K.posterH + K.posterCaptionBand + L.gridRowGap
|
||||||
LaunchedEffect(grid.index, grid.toolbar, grid.inRail, grid.railLetter, shell.navFocused, view) {
|
val focusedRow = (if (grid.inRail) items.indexOfFirst { it.letter == grid.railLetter } else grid.index).coerceAtLeast(0) / grid.columns
|
||||||
val index = if (grid.inRail) items.indexOfFirst { it.letter == grid.railLetter } else grid.index
|
LaunchedEffect(grid.index, grid.columns, focusedRow, headerCount, rowHeight, density,
|
||||||
val row = index.coerceAtLeast(0) / grid.columns
|
grid.toolbar, grid.inRail, grid.railLetter, shell.navFocused, view) {
|
||||||
if (grid.toolbar || row == 0 && active) {
|
if (grid.toolbar || focusedRow == 0 && active) {
|
||||||
animateLibraryScroll(view, headerCount, 0f, density)
|
animateLibraryScroll(view, headerCount, 0f, density)
|
||||||
} else if (row > 0) {
|
} else if (focusedRow > 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 * focusedRow - L.pinnedLine).toPx() }
|
||||||
animateLibraryScroll(view, headerCount, target, density)
|
animateLibraryScroll(view, headerCount, target, density)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +186,7 @@ fun LibraryHeader(person: ShellPerson, title: String, subtitle: String = "", sav
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LibraryToolbar(grid: LibraryGridState, active: Boolean) {
|
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")
|
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 ->
|
labels.forEachIndexed { index, label ->
|
||||||
val focused = active && grid.toolbar && grid.tool == index
|
val focused = active && grid.toolbar && grid.tool == index
|
||||||
|
|
@ -266,7 +266,7 @@ private suspend fun animateLibraryScroll(view: RememberedView, headerCount: Int,
|
||||||
val current = with(density) {
|
val current = with(density) {
|
||||||
(if (first == 0) WolfskinTokens.Shell.zero else L.row_lhead +
|
(if (first == 0) WolfskinTokens.Shell.zero else L.row_lhead +
|
||||||
if (first < headerCount) WolfskinTokens.Shell.zero else
|
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
|
} + view.listState.firstVisibleItemScrollOffset
|
||||||
animateFrameScroll(view.listState, target - current)
|
animateFrameScroll(view.listState, target - current)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ fun TitleCard(focused: Boolean, modifier: Modifier = Modifier, width: Dp = Wolfs
|
||||||
height: Dp = WolfskinTokens.Card.posterH, content: @Composable BoxScope.() -> Unit) {
|
height: Dp = WolfskinTokens.Card.posterH, content: @Composable BoxScope.() -> Unit) {
|
||||||
val shape = RoundedCornerShape(WolfskinTokens.Radius.card)
|
val shape = RoundedCornerShape(WolfskinTokens.Radius.card)
|
||||||
Box(modifier.size(width, height).focusScale(focused, WolfskinTokens.Focus.scaleCard)
|
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)
|
Box(Modifier.fillMaxSize().clip(shape).background(WolfskinTokens.Color.surface2), content = content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,10 @@ fun TitleCast(state: TitleState, active: Boolean) {
|
||||||
val focusOwner = LocalTitleFocusOwner.current
|
val focusOwner = LocalTitleFocusOwner.current
|
||||||
val pitch = with(LocalDensity.current) { WolfskinTokens.Card.personPitch.toPx() }
|
val pitch = with(LocalDensity.current) { WolfskinTokens.Card.personPitch.toPx() }
|
||||||
LaunchedEffect(state.column, active) { if (active) track.keepColumnInWindow(state.column, WolfskinTokens.Card.personSlots, pitch) }
|
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))
|
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) {
|
if (state.data.castStatus == TitleReadStatus.Loading) {
|
||||||
items(WolfskinTokens.Card.personSlots, key = { "cast-skeleton-$it" }, contentType = { "skeleton" }) {
|
items(WolfskinTokens.Card.personSlots, key = { "cast-skeleton-$it" }, contentType = { "skeleton" }) {
|
||||||
Box(Modifier.size(L.castCircle).background(C.surface, CircleShape))
|
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)
|
Box(Modifier.size(WolfskinTokens.Card.personW, WolfskinTokens.Card.personH).zIndex(if (focused) 1f else 0f)
|
||||||
.onGloballyPositioned { state.recordCenter("cast", index, it.boundsInRoot().center.x) }
|
.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}")) {
|
.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) {
|
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.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)
|
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 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() }
|
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) }
|
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))
|
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) {
|
if (episodes && state.data.episodesStatus == TitleReadStatus.Loading) {
|
||||||
items(visible, key = { "episode-skeleton-$it" }, contentType = { "skeleton" }) {
|
items(visible, key = { "episode-skeleton-$it" }, contentType = { "skeleton" }) {
|
||||||
Box(Modifier.size(width, height).background(C.surface, RoundedCornerShape(WolfskinTokens.Radius.card)))
|
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,
|
LibraryPoster(item, focused, focused && ratingsVisible, catalogue = catalogue,
|
||||||
modifier = Modifier.titlePointerFocus(onFocus).clickable { focusOwner?.invoke(); onClick() }, offlineArt = offlineArt)
|
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)
|
} 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() }) {
|
.clip(RoundedCornerShape(WolfskinTokens.Radius.card)).background(C.surface2).clickable { focusOwner?.invoke(); onClick() }) {
|
||||||
TitleTileArtwork(item)
|
TitleTileArtwork(item)
|
||||||
Column(Modifier.align(Alignment.BottomStart).fillMaxWidth().background(Brush.verticalGradient(listOf(C.bg.copy(alpha = 0f), C.bg.copy(alpha = D.cardOverlayOpacity))))
|
Column(Modifier.align(Alignment.BottomStart).fillMaxWidth().background(Brush.verticalGradient(listOf(C.bg.copy(alpha = 0f), C.bg.copy(alpha = D.cardOverlayOpacity))))
|
||||||
|
|
|
||||||
|
|
@ -112,9 +112,9 @@ object WolfskinTokens {
|
||||||
val navSolidAfter = 20.0.dp
|
val navSolidAfter = 20.0.dp
|
||||||
val pinnedLine = 84.0.dp
|
val pinnedLine = 84.0.dp
|
||||||
val windowAhead = 350.0.dp
|
val windowAhead = 350.0.dp
|
||||||
val rowHeadingBand = 30.0.dp
|
val rowHeadingBand = 42.0.dp
|
||||||
val rowGapAfter = 22.0.dp
|
val rowGapAfter = 34.0.dp
|
||||||
val gridRowGap = 14.0.dp
|
val gridRowGap = 22.0.dp
|
||||||
val row_hero = 410.0.dp
|
val row_hero = 410.0.dp
|
||||||
val row_phero = 370.0.dp
|
val row_phero = 370.0.dp
|
||||||
val row_section = 75.0.dp
|
val row_section = 75.0.dp
|
||||||
|
|
@ -146,7 +146,7 @@ object WolfskinTokens {
|
||||||
val settingsRowWidth = 650.0.dp
|
val settingsRowWidth = 650.0.dp
|
||||||
val settingsRowHeight = 52.0.dp
|
val settingsRowHeight = 52.0.dp
|
||||||
val stateWidth = 550.0.dp
|
val stateWidth = 550.0.dp
|
||||||
val captionOffset = 9.0.dp
|
val captionOffset = 19.0.dp
|
||||||
val badge = 24.0.dp
|
val badge = 24.0.dp
|
||||||
val badgeInset = 7.0.dp
|
val badgeInset = 7.0.dp
|
||||||
val progressBar = 4.0.dp
|
val progressBar = 4.0.dp
|
||||||
|
|
@ -159,36 +159,37 @@ object WolfskinTokens {
|
||||||
val personPortrait = 120.0.dp
|
val personPortrait = 120.0.dp
|
||||||
val thumbWidth = 48.0.dp
|
val thumbWidth = 48.0.dp
|
||||||
val thumbHeight = 72.0.dp
|
val thumbHeight = 72.0.dp
|
||||||
|
val row_chipsGrid = 68.0.dp
|
||||||
}
|
}
|
||||||
object Card {
|
object Card {
|
||||||
val posterW = 128.0.dp
|
val posterW = 128.0.dp
|
||||||
val posterH = 192.0.dp
|
val posterH = 192.0.dp
|
||||||
val posterGap = 12.0.dp
|
val posterGap = 20.0.dp
|
||||||
val posterPitch = 140.0.dp
|
val posterPitch = 148.0.dp
|
||||||
val posterCaptionBand = 38.0.dp
|
val posterCaptionBand = 48.0.dp
|
||||||
const val posterSlots = 6
|
const val posterSlots = 6
|
||||||
val tileW = 208.0.dp
|
val tileW = 200.0.dp
|
||||||
val tileH = 117.0.dp
|
val tileH = 112.5.dp
|
||||||
val tileGap = 12.0.dp
|
val tileGap = 20.0.dp
|
||||||
val tilePitch = 220.0.dp
|
val tilePitch = 220.0.dp
|
||||||
val tileCaptionBand = 0.0.dp
|
val tileCaptionBand = 0.0.dp
|
||||||
const val tileSlots = 4
|
const val tileSlots = 4
|
||||||
val wideW = 280.0.dp
|
val wideW = 272.0.dp
|
||||||
val wideH = 157.5.dp
|
val wideH = 153.0.dp
|
||||||
val wideGap = 12.0.dp
|
val wideGap = 20.0.dp
|
||||||
val widePitch = 292.0.dp
|
val widePitch = 292.0.dp
|
||||||
val wideCaptionBand = 0.0.dp
|
val wideCaptionBand = 0.0.dp
|
||||||
const val wideSlots = 3
|
const val wideSlots = 3
|
||||||
val entryW = 128.0.dp
|
val entryW = 128.0.dp
|
||||||
val entryH = 72.0.dp
|
val entryH = 72.0.dp
|
||||||
val entryGap = 12.0.dp
|
val entryGap = 20.0.dp
|
||||||
val entryPitch = 140.0.dp
|
val entryPitch = 148.0.dp
|
||||||
val entryCaptionBand = 0.0.dp
|
val entryCaptionBand = 0.0.dp
|
||||||
const val entrySlots = 6
|
const val entrySlots = 6
|
||||||
val personW = 100.0.dp
|
val personW = 100.0.dp
|
||||||
val personH = 145.0.dp
|
val personH = 155.0.dp
|
||||||
val personGap = 14.0.dp
|
val personGap = 22.0.dp
|
||||||
val personPitch = 114.0.dp
|
val personPitch = 122.0.dp
|
||||||
val personCaptionBand = 0.0.dp
|
val personCaptionBand = 0.0.dp
|
||||||
const val personSlots = 7
|
const val personSlots = 7
|
||||||
}
|
}
|
||||||
|
|
@ -196,7 +197,7 @@ object WolfskinTokens {
|
||||||
const val scaleCard = 1.08f
|
const val scaleCard = 1.08f
|
||||||
const val scaleControl = 1.05f
|
const val scaleControl = 1.05f
|
||||||
const val scaleRow = 1.02f
|
const val scaleRow = 1.02f
|
||||||
val ringCardGap = 2.0.dp
|
val ringCardGap = 0.0.dp
|
||||||
val ringCardRing = 2.0.dp
|
val ringCardRing = 2.0.dp
|
||||||
val ringButtonGap = 2.0.dp
|
val ringButtonGap = 2.0.dp
|
||||||
val ringButtonRing = 2.0.dp
|
val ringButtonRing = 2.0.dp
|
||||||
|
|
@ -429,8 +430,8 @@ object WolfskinTokens {
|
||||||
val progressBottom = 15.0.dp
|
val progressBottom = 15.0.dp
|
||||||
val progressGap = 9.0.dp
|
val progressGap = 9.0.dp
|
||||||
val progressLift = -7.0.dp
|
val progressLift = -7.0.dp
|
||||||
val castHeight = 197.0.dp
|
val castHeight = 231.0.dp
|
||||||
val castNameTop = 108.0.dp
|
val castNameTop = 118.0.dp
|
||||||
val castNameSide = 6.0.dp
|
val castNameSide = 6.0.dp
|
||||||
val personTop = 85.0.dp
|
val personTop = 85.0.dp
|
||||||
val personGap = 24.0.dp
|
val personGap = 24.0.dp
|
||||||
|
|
@ -456,7 +457,7 @@ object WolfskinTokens {
|
||||||
val infoRowGap = 3.0.dp
|
val infoRowGap = 3.0.dp
|
||||||
val infoColumnGap = 10.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 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 minimumEndPadding = 40.0.dp
|
||||||
val ratingGap = 10.0.dp
|
val ratingGap = 10.0.dp
|
||||||
val ratingMarkGap = 4.0.dp
|
val ratingMarkGap = 4.0.dp
|
||||||
|
|
|
||||||
|
|
@ -115,5 +115,6 @@ class HomeCaptureTest {
|
||||||
@Test fun st101_unratedEpisode() = capture("st-101", person = "mindel", row = "nu", column = 2)
|
@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 st102_filmTile() = capture("st-102", person = "mindel", row = "new", column = 4)
|
||||||
@Test fun st103_heroRatings() = capture("st-103")
|
@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")
|
@Test fun xHomeDiscoverStart() = capture("x-home-discover-start", row = "pk-m")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,14 @@ class LibraryCaptureTest {
|
||||||
@Test fun st112_cachedWhileDown() = capture("st-112", reachable = false, cached = true)
|
@Test fun st112_cachedWhileDown() = capture("st-112", reachable = false, cached = true)
|
||||||
@Test fun st113_uncachedWhileDown() = capture("st-113", reachable = false)
|
@Test fun st113_uncachedWhileDown() = capture("st-113", reachable = false)
|
||||||
@Test fun unsignedRatingsHidden() = capture("library-unsigned", ratings = 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/")
|
fun libraryFixture(): LibraryFixture = LibraryFixture(LibraryCaptureTest::class.java.getResourceAsStream("/wolfskin-library.json")!!.bufferedReader().use { it.readText() }, "/library/")
|
||||||
|
|
|
||||||
|
|
@ -112,18 +112,25 @@ class LibraryKeysTest {
|
||||||
val titles = (0 until 200).map { index -> LibraryTitle("$index", "${('A'.code + index / 10).toChar()}Title $index", "movie") }
|
val titles = (0 until 200).map { index -> LibraryTitle("$index", "${('A'.code + index / 10).toChar()}Title $index", "movie") }
|
||||||
val grid = LibraryGridState("Movies", titles)
|
val grid = LibraryGridState("Movies", titles)
|
||||||
assertTrue(grid.railVisible)
|
assertTrue(grid.railVisible)
|
||||||
grid.index = 5
|
grid.index = 4
|
||||||
grid.key(ShellKey.Right)
|
grid.key(ShellKey.Right)
|
||||||
assertTrue(grid.inRail)
|
assertTrue(grid.inRail)
|
||||||
grid.key(ShellKey.Down)
|
grid.key(ShellKey.Down)
|
||||||
assertEquals('B', grid.railLetter)
|
assertEquals('B', grid.railLetter)
|
||||||
grid.key(ShellKey.Back)
|
grid.key(ShellKey.Back)
|
||||||
assertEquals(5, grid.index)
|
assertEquals(4, grid.index)
|
||||||
assertFalse(grid.inRail)
|
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)
|
grid.key(ShellKey.Right); grid.key(ShellKey.Down); grid.key(ShellKey.Ok)
|
||||||
assertEquals('B', grid.focusedTitle!!.letter)
|
assertEquals('B', grid.focusedTitle!!.letter)
|
||||||
grid.source = titles.take(199)
|
grid.source = titles.take(199)
|
||||||
assertFalse(grid.railVisible)
|
assertFalse(grid.railVisible)
|
||||||
|
grid.index = 4
|
||||||
|
grid.key(ShellKey.Right)
|
||||||
|
assertEquals(5, grid.index)
|
||||||
grid.source = titles; grid.sort = 1
|
grid.source = titles; grid.sort = 1
|
||||||
assertFalse(grid.railVisible)
|
assertFalse(grid.railVisible)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package org.justinvisser.wolfskin.design
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
/** Caption spacing and live rail changes must preserve poster geometry and retained focus. */
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||||
|
class LibrarySpacingRegressionTest {
|
||||||
|
@get:Rule val rule = createComposeRule()
|
||||||
|
|
||||||
|
@Test fun partiallyWatchedPosterKeepsItsOriginalProgressInsets() {
|
||||||
|
val title = LibraryTitle("partial", "Partially watched", "movie", progress = .5f)
|
||||||
|
rule.setContent { WolfskinTheme { LibraryPoster(title, false, false, false) } }
|
||||||
|
val poster = rule.onNodeWithTag("poster-partial").fetchSemanticsNode().boundsInRoot
|
||||||
|
val track = rule.onNodeWithTag("poster-progress-partial").fetchSemanticsNode().boundsInRoot
|
||||||
|
assertEquals(256f, poster.width, .1f)
|
||||||
|
assertEquals(220f, track.width, .1f)
|
||||||
|
assertEquals(18f, track.left - poster.left, .1f)
|
||||||
|
assertEquals(18f, poster.right - track.right, .1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun crossingRailThresholdKeepsTheSameDeepFocusedPosterVisible() {
|
||||||
|
val titles = List(200) { LibraryTitle("$it", "Title ${it.toString().padStart(3, '0')}", "tv") }
|
||||||
|
val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false; enterDestination("shows") }
|
||||||
|
val grid = LibraryGridState("Shows", titles.take(199)).apply { index = 198 }
|
||||||
|
rule.setContent { WolfskinTheme { ShellSurface(shell) { LibraryScreen(shell, grid, it) } } }
|
||||||
|
rule.mainClock.advanceTimeBy(1000)
|
||||||
|
rule.waitForIdle()
|
||||||
|
val poster = rule.onNodeWithTag("poster-198")
|
||||||
|
poster.assertIsDisplayed()
|
||||||
|
val original = poster.fetchSemanticsNode().boundsInRoot
|
||||||
|
for (count in listOf(200, 199)) {
|
||||||
|
rule.runOnIdle { grid.source = titles.take(count) }
|
||||||
|
rule.mainClock.advanceTimeBy(1000)
|
||||||
|
rule.waitForIdle()
|
||||||
|
poster.assertIsDisplayed()
|
||||||
|
val current = poster.fetchSemanticsNode().boundsInRoot
|
||||||
|
assertEquals(198, grid.index)
|
||||||
|
assertEquals("198", grid.focusedTitle!!.id)
|
||||||
|
assertEquals(original.top, current.top, 1f)
|
||||||
|
assertEquals(original.height, current.height, 1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"id": "library",
|
"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",
|
"specSection": "6.4",
|
||||||
"source": "index.html:1403-1426, 1563-1611",
|
"source": "index.html:1403-1426, 1563-1611",
|
||||||
"layout": [
|
"layout": [
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"token": null,
|
"token": null,
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 236,
|
"yPx": 236,
|
||||||
"heightPx": 104,
|
"heightPx": 136,
|
||||||
"cards": null,
|
"cards": null,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -33,8 +33,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 340,
|
"yPx": 372,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -45,8 +45,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 828,
|
"yPx": 896,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -57,8 +57,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 1316,
|
"yPx": 1420,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -69,8 +69,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 1804,
|
"yPx": 1944,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -81,8 +81,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 2292,
|
"yPx": 2468,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -93,8 +93,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 2780,
|
"yPx": 2992,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -105,8 +105,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 3268,
|
"yPx": 3516,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -117,8 +117,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 3756,
|
"yPx": 4040,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -129,8 +129,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 4244,
|
"yPx": 4564,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -141,8 +141,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 4732,
|
"yPx": 5088,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -153,8 +153,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 5220,
|
"yPx": 5612,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -165,8 +165,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 5708,
|
"yPx": 6136,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -177,8 +177,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 6196,
|
"yPx": 6660,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
@ -189,8 +189,8 @@
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"token": "poster",
|
"token": "poster",
|
||||||
"title": "",
|
"title": "",
|
||||||
"yPx": 6684,
|
"yPx": 7184,
|
||||||
"heightPx": 488,
|
"heightPx": 524,
|
||||||
"cards": 6,
|
"cards": 6,
|
||||||
"focusable": true,
|
"focusable": true,
|
||||||
"endsInSeeAll": false,
|
"endsInSeeAll": false,
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
"progressBottom": {"dp": 15, "px": 30, "source": "index.html:217"},
|
"progressBottom": {"dp": 15, "px": 30, "source": "index.html:217"},
|
||||||
"progressGap": {"dp": 9, "px": 18, "source": "index.html:217"},
|
"progressGap": {"dp": 9, "px": 18, "source": "index.html:217"},
|
||||||
"progressLift": {"dp": -7, "px": -14, "source": "index.html:217"},
|
"progressLift": {"dp": -7, "px": -14, "source": "index.html:217"},
|
||||||
"castHeight": {"dp": 197, "px": 394, "source": "index.html:1018"},
|
"castHeight": {"dp": 231, "px": 462, "source": "index.html:1018"},
|
||||||
"castNameTop": {"dp": 108, "px": 216, "source": "index.html:289"},
|
"castNameTop": {"dp": 118, "px": 236, "source": "index.html:289"},
|
||||||
"castNameSide": {"dp": 6, "px": 12, "source": "index.html:289"},
|
"castNameSide": {"dp": 6, "px": 12, "source": "index.html:289"},
|
||||||
"personTop": {"dp": 85, "px": 170, "source": "index.html:293"},
|
"personTop": {"dp": 85, "px": 170, "source": "index.html:293"},
|
||||||
"personGap": {"dp": 24, "px": 48, "source": "index.html:293"},
|
"personGap": {"dp": 24, "px": 48, "source": "index.html:293"},
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
"infoRowGap": {"dp": 3, "px": 6, "source": "index.html:318"},
|
"infoRowGap": {"dp": 3, "px": 6, "source": "index.html:318"},
|
||||||
"infoColumnGap": {"dp": 10, "px": 20, "source": "index.html:318"},
|
"infoColumnGap": {"dp": 10, "px": 20, "source": "index.html:318"},
|
||||||
"infoKey": {"$type": "typography", "sp": 12, "px": 24, "weight": 650, "$value": {"fontWeight": 650}, "source": "index.html:319"},
|
"infoKey": {"$type": "typography", "sp": 12, "px": 24, "weight": 650, "$value": {"fontWeight": 650}, "source": "index.html:319"},
|
||||||
"gridHeight": {"dp": 244, "px": 488, "source": "index.html:1019"},
|
"gridHeight": {"dp": 262, "px": 524, "source": "index.html:1019"},
|
||||||
"minimumEndPadding": {"dp": 40, "px": 80, "source": "index.html:1042"},
|
"minimumEndPadding": {"dp": 40, "px": 80, "source": "index.html:1042"},
|
||||||
"ratingGap": {"dp": 10, "px": 20, "source": "index.html:156"},
|
"ratingGap": {"dp": 10, "px": 20, "source": "index.html:156"},
|
||||||
"ratingMarkGap": {"dp": 4, "px": 8, "source": "index.html:157"},
|
"ratingMarkGap": {"dp": 4, "px": 8, "source": "index.html:157"},
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,9 @@
|
||||||
--layout-navSolidAfter: 40.0px;
|
--layout-navSolidAfter: 40.0px;
|
||||||
--layout-pinnedLine: 168.0px;
|
--layout-pinnedLine: 168.0px;
|
||||||
--layout-windowAhead: 700.0px;
|
--layout-windowAhead: 700.0px;
|
||||||
--layout-rowHeadingBand: 60.0px;
|
--layout-rowHeadingBand: 84.0px;
|
||||||
--layout-rowGapAfter: 44.0px;
|
--layout-rowGapAfter: 68.0px;
|
||||||
--layout-gridRowGap: 28.0px;
|
--layout-gridRowGap: 44.0px;
|
||||||
--layout-row_hero: 820.0px;
|
--layout-row_hero: 820.0px;
|
||||||
--layout-row_phero: 740.0px;
|
--layout-row_phero: 740.0px;
|
||||||
--layout-row_section: 150.0px;
|
--layout-row_section: 150.0px;
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
--layout-sheetWidth: 800.0px;
|
--layout-sheetWidth: 800.0px;
|
||||||
--layout-sheetRow: 100.0px;
|
--layout-sheetRow: 100.0px;
|
||||||
--layout-stateWidth: 1100.0px;
|
--layout-stateWidth: 1100.0px;
|
||||||
--layout-captionOffset: 18.0px;
|
--layout-captionOffset: 38.0px;
|
||||||
--layout-badge: 48.0px;
|
--layout-badge: 48.0px;
|
||||||
--layout-badgeInset: 14.0px;
|
--layout-badgeInset: 14.0px;
|
||||||
--layout-progressBar: 8.0px;
|
--layout-progressBar: 8.0px;
|
||||||
|
|
@ -122,6 +122,7 @@
|
||||||
--layout-navAvatar: 64.0px;
|
--layout-navAvatar: 64.0px;
|
||||||
--layout-castCircle: 200.0px;
|
--layout-castCircle: 200.0px;
|
||||||
--layout-personPortrait: 240.0px;
|
--layout-personPortrait: 240.0px;
|
||||||
|
--layout-row_chipsGrid: 136.0px;
|
||||||
--motion-fast: 140ms;
|
--motion-fast: 140ms;
|
||||||
--motion-base: 160ms;
|
--motion-base: 160ms;
|
||||||
--motion-scroll: 180ms;
|
--motion-scroll: 180ms;
|
||||||
|
|
@ -273,8 +274,8 @@
|
||||||
--title-progressBottom: 30.0px;
|
--title-progressBottom: 30.0px;
|
||||||
--title-progressGap: 18.0px;
|
--title-progressGap: 18.0px;
|
||||||
--title-progressLift: -14.0px;
|
--title-progressLift: -14.0px;
|
||||||
--title-castHeight: 394.0px;
|
--title-castHeight: 462.0px;
|
||||||
--title-castNameTop: 216.0px;
|
--title-castNameTop: 236.0px;
|
||||||
--title-castNameSide: 12.0px;
|
--title-castNameSide: 12.0px;
|
||||||
--title-personTop: 170.0px;
|
--title-personTop: 170.0px;
|
||||||
--title-personGap: 48.0px;
|
--title-personGap: 48.0px;
|
||||||
|
|
@ -300,7 +301,7 @@
|
||||||
--title-infoRowGap: 6.0px;
|
--title-infoRowGap: 6.0px;
|
||||||
--title-infoColumnGap: 20.0px;
|
--title-infoColumnGap: 20.0px;
|
||||||
--title-infoKey: 24.0px;
|
--title-infoKey: 24.0px;
|
||||||
--title-gridHeight: 488.0px;
|
--title-gridHeight: 524.0px;
|
||||||
--title-minimumEndPadding: 80.0px;
|
--title-minimumEndPadding: 80.0px;
|
||||||
--title-ratingGap: 20.0px;
|
--title-ratingGap: 20.0px;
|
||||||
--title-ratingMarkGap: 8.0px;
|
--title-ratingMarkGap: 8.0px;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$description": "Wolfskin TV design tokens, extracted by script from the locked prototype (data/tv-v2/opus-final-2026-11-05/index.html, sha256 ee8844fc...). Values are CSS px on the 1920x1080 stage; the box renders 1920x1080 at 320 dpi = density 2.0, so dp/sp = px / 2. Format follows the W3C design-tokens draft ($type/$value); extra keys: px, dp|sp, source (page file:line), usedBy, page (the page value where q1 consolidated it).",
|
"$description": "Wolfskin TV design tokens, extracted by script from the locked prototype (originally data/tv-v2/opus-final-2026-11-05/index.html, sha256 ee8844fc...; now amended in data/tv-v2/spacing-2026-11-06/index.html, sha256 fcc5d879...). Values are CSS px on the 1920x1080 stage; the box renders 1920x1080 at 320 dpi = density 2.0, so dp/sp = px / 2. Format follows the W3C design-tokens draft ($type/$value); extra keys: px, dp|sp, source (page file:line), usedBy, page (the page value where q1 consolidated it). Amended 2026-11-06 (spec revision 3, Level 2 spacing + card ring; data/tv-card-spacing/answers.md): card, layout.row*/gridRowGap and focus.ringCard changed; every other token is as extracted. The prototype source line numbers in `source` fields predate the amendment and drift by a few lines after index.html:8.",
|
||||||
"meta": {
|
"meta": {
|
||||||
"density": 2.0,
|
"density": 2.0,
|
||||||
"stagePx": [
|
"stagePx": [
|
||||||
|
|
@ -10,8 +10,8 @@
|
||||||
960,
|
960,
|
||||||
540
|
540
|
||||||
],
|
],
|
||||||
"source": "data/tv-v2/opus-final-2026-11-05/index.html",
|
"source": "data/tv-v2/spacing-2026-11-06/index.html",
|
||||||
"sourceSha256": "ee8844fc6d0237a3727de0d9454db8e5cf3bb14083fb7877d7bb0f4233c6e23d",
|
"sourceSha256": "fcc5d879c34da868b817f8d9d306193743d3933db3096da605bda659d8e633b2",
|
||||||
"generator": "data/tv-spec-lane/scratch/extract.py + build_tokens.py"
|
"generator": "data/tv-spec-lane/scratch/extract.py + build_tokens.py"
|
||||||
},
|
},
|
||||||
"color": {
|
"color": {
|
||||||
|
|
@ -773,27 +773,27 @@
|
||||||
},
|
},
|
||||||
"rowHeadingBand": {
|
"rowHeadingBand": {
|
||||||
"$type": "dimension",
|
"$type": "dimension",
|
||||||
"$value": "30dp",
|
"$value": "42dp",
|
||||||
"px": 60,
|
"px": 84,
|
||||||
"dp": 30.0,
|
"dp": 42.0,
|
||||||
"source": "index.html:1018",
|
"source": "index.html:1018",
|
||||||
"usedBy": "shelf row: heading band (44 px heading at top 0) before the cards start at y + 60"
|
"usedBy": "shelf row: heading band (44 px heading at top 0, then 40 px) before the cards start at y + 84 (HEAD_OFF)"
|
||||||
},
|
},
|
||||||
"rowGapAfter": {
|
"rowGapAfter": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$value": "34dp",
|
||||||
|
"px": 68,
|
||||||
|
"dp": 34.0,
|
||||||
|
"source": "index.html:1018",
|
||||||
|
"usedBy": "shelf row: space after cards (+ caption band) before the next row; shelf row height = 84 + card h + caption + 68"
|
||||||
|
},
|
||||||
|
"gridRowGap": {
|
||||||
"$type": "dimension",
|
"$type": "dimension",
|
||||||
"$value": "22dp",
|
"$value": "22dp",
|
||||||
"px": 44,
|
"px": 44,
|
||||||
"dp": 22.0,
|
"dp": 22.0,
|
||||||
"source": "index.html:1018",
|
|
||||||
"usedBy": "shelf row: space after cards (+ caption band) before the next row; shelf row height = 60 + card h + caption + 44"
|
|
||||||
},
|
|
||||||
"gridRowGap": {
|
|
||||||
"$type": "dimension",
|
|
||||||
"$value": "14dp",
|
|
||||||
"px": 28,
|
|
||||||
"dp": 14.0,
|
|
||||||
"source": "index.html:1019",
|
"source": "index.html:1019",
|
||||||
"usedBy": "grid row height = 384 + 76 + 28 = 488 (library, See all, person)"
|
"usedBy": "grid row height = 384 + 96 + 44 = 524 (library, See all, person)"
|
||||||
},
|
},
|
||||||
"row_hero": {
|
"row_hero": {
|
||||||
"$type": "dimension",
|
"$type": "dimension",
|
||||||
|
|
@ -825,7 +825,7 @@
|
||||||
"px": 104,
|
"px": 104,
|
||||||
"dp": 52.0,
|
"dp": 52.0,
|
||||||
"source": "index.html:1021",
|
"source": "index.html:1021",
|
||||||
"usedBy": "chip row (Discover filters, library toolbar, See all/service type chips, seasons)"
|
"usedBy": "chip row above shelves (Discover filters, service type chips, seasons); 104 px"
|
||||||
},
|
},
|
||||||
"row_note": {
|
"row_note": {
|
||||||
"$type": "dimension",
|
"$type": "dimension",
|
||||||
|
|
@ -1041,10 +1041,10 @@
|
||||||
},
|
},
|
||||||
"captionOffset": {
|
"captionOffset": {
|
||||||
"$type": "dimension",
|
"$type": "dimension",
|
||||||
"px": 18,
|
"px": 38,
|
||||||
"dp": 9,
|
"dp": 19,
|
||||||
"source": "index.html:1242",
|
"source": "index.html:1242",
|
||||||
"usedBy": "caption under a focused poster starts 18 px below the card bottom (capHtml top = headH + h + 18); width 640"
|
"usedBy": "caption under a focused poster starts 38 px below the card bottom (capHtml top = headH + h + CAP_OFF; was 18, amended 2026-11-06); width 640"
|
||||||
},
|
},
|
||||||
"plateInset": {
|
"plateInset": {
|
||||||
"$type": "dimension",
|
"$type": "dimension",
|
||||||
|
|
@ -1171,6 +1171,14 @@
|
||||||
},
|
},
|
||||||
"source": "index.html:204",
|
"source": "index.html:204",
|
||||||
"usedBy": "letter rail (only for destinations with >= 200 titles)"
|
"usedBy": "letter rail (only for destinations with >= 200 titles)"
|
||||||
|
},
|
||||||
|
"row_chipsGrid": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$value": "68dp",
|
||||||
|
"px": 136,
|
||||||
|
"dp": 68.0,
|
||||||
|
"source": "index.html:rowH",
|
||||||
|
"usedBy": "chip row directly above a grid (library toolbar, See all type chips): 136 px, so 52 px from the chips to the first card row (was 104 / 20 px)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"card": {
|
"card": {
|
||||||
|
|
@ -1179,16 +1187,16 @@
|
||||||
"px": {
|
"px": {
|
||||||
"w": 256,
|
"w": 256,
|
||||||
"h": 384,
|
"h": 384,
|
||||||
"gap": 24,
|
"gap": 40,
|
||||||
"pitch": 280,
|
"pitch": 296,
|
||||||
"captionBand": 76
|
"captionBand": 96
|
||||||
},
|
},
|
||||||
"dp": {
|
"dp": {
|
||||||
"w": 128.0,
|
"w": 128.0,
|
||||||
"h": 192.0,
|
"h": 192.0,
|
||||||
"gap": 12.0,
|
"gap": 20.0,
|
||||||
"pitch": 140.0,
|
"pitch": 148.0,
|
||||||
"captionBand": 38.0
|
"captionBand": 48.0
|
||||||
},
|
},
|
||||||
"ratio": "2:3",
|
"ratio": "2:3",
|
||||||
"fullSlotsPerRow": 6,
|
"fullSlotsPerRow": 6,
|
||||||
|
|
@ -1198,16 +1206,16 @@
|
||||||
"tile": {
|
"tile": {
|
||||||
"$type": "card",
|
"$type": "card",
|
||||||
"px": {
|
"px": {
|
||||||
"w": 416,
|
"w": 400,
|
||||||
"h": 234,
|
"h": 225,
|
||||||
"gap": 24,
|
"gap": 40,
|
||||||
"pitch": 440,
|
"pitch": 440,
|
||||||
"captionBand": 0
|
"captionBand": 0
|
||||||
},
|
},
|
||||||
"dp": {
|
"dp": {
|
||||||
"w": 208.0,
|
"w": 200.0,
|
||||||
"h": 117.0,
|
"h": 112.5,
|
||||||
"gap": 12.0,
|
"gap": 20.0,
|
||||||
"pitch": 220.0,
|
"pitch": 220.0,
|
||||||
"captionBand": 0.0
|
"captionBand": 0.0
|
||||||
},
|
},
|
||||||
|
|
@ -1219,16 +1227,16 @@
|
||||||
"wide": {
|
"wide": {
|
||||||
"$type": "card",
|
"$type": "card",
|
||||||
"px": {
|
"px": {
|
||||||
"w": 560,
|
"w": 544,
|
||||||
"h": 315,
|
"h": 306,
|
||||||
"gap": 24,
|
"gap": 40,
|
||||||
"pitch": 584,
|
"pitch": 584,
|
||||||
"captionBand": 0
|
"captionBand": 0
|
||||||
},
|
},
|
||||||
"dp": {
|
"dp": {
|
||||||
"w": 280.0,
|
"w": 272.0,
|
||||||
"h": 157.5,
|
"h": 153.0,
|
||||||
"gap": 12.0,
|
"gap": 20.0,
|
||||||
"pitch": 292.0,
|
"pitch": 292.0,
|
||||||
"captionBand": 0.0
|
"captionBand": 0.0
|
||||||
},
|
},
|
||||||
|
|
@ -1242,15 +1250,15 @@
|
||||||
"px": {
|
"px": {
|
||||||
"w": 256,
|
"w": 256,
|
||||||
"h": 144,
|
"h": 144,
|
||||||
"gap": 24,
|
"gap": 40,
|
||||||
"pitch": 280,
|
"pitch": 296,
|
||||||
"captionBand": 0
|
"captionBand": 0
|
||||||
},
|
},
|
||||||
"dp": {
|
"dp": {
|
||||||
"w": 128.0,
|
"w": 128.0,
|
||||||
"h": 72.0,
|
"h": 72.0,
|
||||||
"gap": 12.0,
|
"gap": 20.0,
|
||||||
"pitch": 140.0,
|
"pitch": 148.0,
|
||||||
"captionBand": 0.0
|
"captionBand": 0.0
|
||||||
},
|
},
|
||||||
"ratio": "16:9",
|
"ratio": "16:9",
|
||||||
|
|
@ -1262,22 +1270,22 @@
|
||||||
"$type": "card",
|
"$type": "card",
|
||||||
"px": {
|
"px": {
|
||||||
"w": 200,
|
"w": 200,
|
||||||
"h": 290,
|
"h": 310,
|
||||||
"gap": 28,
|
"gap": 44,
|
||||||
"pitch": 228,
|
"pitch": 244,
|
||||||
"captionBand": 0
|
"captionBand": 0
|
||||||
},
|
},
|
||||||
"dp": {
|
"dp": {
|
||||||
"w": 100.0,
|
"w": 100.0,
|
||||||
"h": 145.0,
|
"h": 155.0,
|
||||||
"gap": 14.0,
|
"gap": 22.0,
|
||||||
"pitch": 114.0,
|
"pitch": 122.0,
|
||||||
"captionBand": 0.0
|
"captionBand": 0.0
|
||||||
},
|
},
|
||||||
"ratio": "1:1 circle",
|
"ratio": "1:1 circle",
|
||||||
"fullSlotsPerRow": 7,
|
"fullSlotsPerRow": 7,
|
||||||
"source": "index.html:660",
|
"source": "index.html:660",
|
||||||
"usedBy": "Cast row: 200 px circle + name + character, 290 px tall"
|
"usedBy": "Cast row: 200 px circle + name + character, 310 tall (name starts 236 below the top)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"focus": {
|
"focus": {
|
||||||
|
|
@ -1318,16 +1326,17 @@
|
||||||
},
|
},
|
||||||
"ringCard": {
|
"ringCard": {
|
||||||
"$type": "shadow",
|
"$type": "shadow",
|
||||||
"$value": "0 0 0 4px var(--bg),0 0 0 8px var(--accent),0 18px 50px rgb(0 0 0 / .65)",
|
"$value": "0 0 0 2px var(--accent), inset 0 0 0 2px var(--accent) (drawn above the art), 0 18px 50px rgb(0 0 0 / .65)",
|
||||||
"gapPx": 4,
|
"gapPx": 0,
|
||||||
"ringPx": 4,
|
"ringPx": 4,
|
||||||
"gapDp": 2,
|
"gapDp": 0,
|
||||||
"ringDp": 2,
|
"ringDp": 2,
|
||||||
"gapColor": "color.bg",
|
|
||||||
"ringColor": "color.accent",
|
"ringColor": "color.accent",
|
||||||
"liftShadow": "0 18px 50px rgb(0 0 0 / .65)",
|
"liftShadow": "0 18px 50px rgb(0 0 0 / .65)",
|
||||||
"source": "index.html:134",
|
"source": "index.html:134",
|
||||||
"usedBy": "focused card: a 4 px bg-coloured gap then a 4 px lilac ring OUTSIDE the art (never covers it), plus a lift shadow; drawn on the circle for cast cards"
|
"usedBy": "focused card (poster, tile, wide, entry) and cast circle: one 4 px lilac ring centred on the art edge, 2 px outside and 2 px over the art, no gap, plus a lift shadow (Justin 2026-11-06: \"2px inset, 4px ring\")",
|
||||||
|
"outsidePx": 2,
|
||||||
|
"insidePx": 2
|
||||||
},
|
},
|
||||||
"ringButton": {
|
"ringButton": {
|
||||||
"$type": "shadow",
|
"$type": "shadow",
|
||||||
|
|
@ -1589,4 +1598,4 @@
|
||||||
"usedBy": "Coming-in sheet shows 5 rows (4 while one is expanded), window follows focus"
|
"usedBy": "Coming-in sheet shows 5 rows (4 while one is expanded), window follows focus"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
85109c394255f6120a621fbefe8e58df82832974ccd6a6a975a61567f641ea2a generate_tokens.py
|
85109c394255f6120a621fbefe8e58df82832974ccd6a6a975a61567f641ea2a generate_tokens.py
|
||||||
25985087986a46f00e2819917d0421ff2b437ec21c54f6ce6412b84c0eb05302 home-tokens.json
|
25985087986a46f00e2819917d0421ff2b437ec21c54f6ce6412b84c0eb05302 home-tokens.json
|
||||||
be3af0ddcb0d67abef0805d19d7a33cdb55428bcace656b50f0a38935f212028 shell-tokens.json
|
be3af0ddcb0d67abef0805d19d7a33cdb55428bcace656b50f0a38935f212028 shell-tokens.json
|
||||||
1c805ae8eacbca1c63fbf91e19d7f9fea6c27b54e709e91f6bd46720d5048a41 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
|
4f19801799936db4c126cd7442942ff8552774f141af0de42eb5b3c1ecac92fc src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
|
||||||
7ed093cc63b0fd6f47fe951da5df26ee7258865b8d0a51345a408ab64f037de0 title-tokens.json
|
8ae28b149e38a00952c07a6c27bb5df667101acf09b7b571b658774f453b2063 title-tokens.json
|
||||||
3b14b60264e1f97c02be3e57d0c0bd9477e982044ee944c8c8a5b71e939fa8fb tokens.css
|
b482e5c532b50cffae9923b86efd03d40f1a56300e1c3c6d969b965f41ea1670 tokens.css
|
||||||
5b939f1f3eb90b428008fda75720f6e3a4e844c486668931db3d52bffb987b53 tokens.json
|
6764fa02146c3f37cc60e298f7b13c5d2186fb2be575a39e45c7b207da5429af tokens.json
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue