Reuse baseline-aligned headers with source subtitle typography

This commit is contained in:
justinzeus 2026-11-05 18:37:58 +01:00
parent b1b15b384b
commit 4e62aad638
6 changed files with 18 additions and 20 deletions

View file

@ -94,7 +94,7 @@ fun DiscoverScreen(state: DiscoverState, modifier: Modifier = Modifier) {
if (state.hasHero && state.service == null && list.firstVisibleItemIndex == 0) state.hero?.let { DiscoverBackdrop(it, modifier = Modifier.graphicsLayer { translationY = -list.firstVisibleItemScrollOffset.toFloat() }) }
LazyColumn(state = list, contentPadding = PaddingValues(bottom = (WolfskinTokens.Shell.stageHeight - L.pinnedLine - heights.last()).coerceAtLeast(D.frameBottomMin)), modifier = Modifier.fillMaxSize()) {
item(key = "discover-header", contentType = if (state.hasHero && state.service == null) "hero" else "header") {
if (state.hasHero && state.service == null) DiscoverHero(state) else DiscoverHeader(state.shell.person, state.service?.name ?: "Discover", if (state.service != null) "General interest" else "")
if (state.hasHero && state.service == null) DiscoverHero(state) else LibraryHeader(state.shell.person, state.service?.name ?: "Discover", if (state.service != null) "General interest" else "")
}
item(key = "discover-chips", contentType = "chips") {
if (state.snapshot.status !in setOf(DiscoverStatus.NeedsSignIn, DiscoverStatus.Unavailable, DiscoverStatus.Loading)) DiscoverChips(state)
@ -116,16 +116,6 @@ fun DiscoverScreen(state: DiscoverState, modifier: Modifier = Modifier) {
}
}
/** Header and type chips use the same safe area as the shell. */
@Composable
private fun DiscoverHeader(person: ShellPerson, title: String, subtitle: String) {
Row(Modifier.fillMaxWidth().height(L.row_lhead).padding(start = L.safeSide, top = D.headerTop), horizontalArrangement = Arrangement.spacedBy(D.headerGap), verticalAlignment = Alignment.CenterVertically) {
Avatar(person, WolfskinTokens.Shell.settingsHeaderAvatar, WolfskinTokens.Type.avatarLetterLibHeader, letterColor = C.muted)
Text(title, color = C.fg, style = WolfskinTokens.Type.screenTitle)
if (subtitle.isNotEmpty()) Text(subtitle, color = C.muted, style = WolfskinTokens.Type.nav)
}
}
@Composable
private fun DiscoverHero(state: DiscoverState) {
val title = state.hero ?: return

View file

@ -48,6 +48,7 @@ fun LibraryScreen(
val active = !shell.navFocused && !shell.pickerVisible
val items = grid.items
val focused = grid.focusedTitle
val count = if (grid.catalogue) items.size else grid.source.size
var plateKey by remember { mutableStateOf<String?>(null) }
DisposableEffect(shell, grid, registerFocus) {
if (registerFocus) shell.contentFocus = grid
@ -104,7 +105,11 @@ fun LibraryScreen(
}
}
}, contentPadding = PaddingValues(bottom = WolfskinTokens.Shell.stageHeight - L.pinnedLine - rowHeight)) {
item(key = "header", contentType = "header") { LibraryHeader(grid, shell.person) }
item(key = "header", contentType = "header") {
LibraryHeader(shell.person, grid.title,
if (grid.status != LibraryStatus.Ready) "" else "$count ${if (grid.catalogue) "titles" else if (grid.title == "Movies") if (count == 1) "film" else "films" else "series"}",
grid.savedAt)
}
if (headerCount == 2) item(key = "toolbar", contentType = "chips") { LibraryToolbar(grid, active) }
when (grid.status) {
LibraryStatus.Loading -> items(3, key = { "skeleton-$it" }, contentType = { "grid" }) {
@ -157,15 +162,15 @@ fun LibraryScreen(
}
}
/** Library, catalogue and service headers share one baseline and person treatment. */
@Composable
private fun LibraryHeader(grid: LibraryGridState, person: ShellPerson) {
val count = if (grid.catalogue) grid.items.size else grid.source.size
fun LibraryHeader(person: ShellPerson, title: String, subtitle: String = "", savedAt: String? = null) {
Box(Modifier.fillMaxWidth().height(L.row_lhead)) {
Row(Modifier.offset(L.safeSide, L.navBand), horizontalArrangement = Arrangement.spacedBy(WolfskinTokens.Shell.settingsHeaderGap), verticalAlignment = Alignment.CenterVertically) {
Avatar(person, WolfskinTokens.Shell.settingsHeaderAvatar, WolfskinTokens.Type.avatarLetterLibHeader, letterColor = C.muted)
Text(grid.title, modifier = Modifier.alignByBaseline(), color = C.fg, style = WolfskinTokens.Type.screenTitle)
Text(if (grid.status != LibraryStatus.Ready) "" else "$count ${if (grid.catalogue) "titles" else if (grid.title == "Movies") if (count == 1) "film" else "films" else "series"}", modifier = Modifier.alignByBaseline(), color = C.muted, style = WolfskinTokens.Type.nav.copy(fontWeight = FontWeight.Normal))
grid.savedAt?.let { Text("Saved $it", color = C.warn, style = WolfskinTokens.Type.caption) }
Text(title, modifier = Modifier.alignByBaseline(), color = C.fg, style = WolfskinTokens.Type.screenTitle)
if (subtitle.isNotEmpty()) Text(subtitle, modifier = Modifier.alignByBaseline(), color = C.muted, style = G.headerSubtitle)
savedAt?.let { Text("Saved $it", color = C.warn, style = WolfskinTokens.Type.caption) }
}
}
}

View file

@ -325,6 +325,7 @@ object WolfskinTokens {
val badgeIcon = 14.0.dp
val seeAllGap = 4.0.dp
val tailDescription = TextStyle(fontSize = 12.0.sp, fontWeight = FontWeight(500), lineHeight = 15.600000000000001.sp, letterSpacing = 0.0.em)
val headerSubtitle = TextStyle(fontSize = 15.0.sp, fontWeight = FontWeight(400), lineHeight = 19.5.sp, letterSpacing = 0.0.em)
const val imageReads = 4.0f
const val unknownCriticOpacity = 0.55f
val fallbackPadding = 11.0.dp