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

@ -357,6 +357,7 @@
"badgeIcon": {"$type":"dimension","px":28,"dp":14,"source":"index.html:142"}, "badgeIcon": {"$type":"dimension","px":28,"dp":14,"source":"index.html:142"},
"seeAllGap": {"$type":"dimension","px":8,"dp":4,"source":"index.html:146"}, "seeAllGap": {"$type":"dimension","px":8,"dp":4,"source":"index.html:146"},
"tailDescription": {"$type":"typography","px":24,"sp":12,"weight":500,"$value":{"fontWeight":500},"source":"index.html:301"}, "tailDescription": {"$type":"typography","px":24,"sp":12,"weight":500,"$value":{"fontWeight":500},"source":"index.html:301"},
"headerSubtitle": {"$type":"typography","px":30,"sp":15,"weight":400,"$value":{"fontWeight":400},"source":"index.html:201"},
"imageReads": {"$type":"number","$value":4,"source":"spec 4.9, tokens.json image.concurrentReads"}, "imageReads": {"$type":"number","$value":4,"source":"spec 4.9, tokens.json image.concurrentReads"},
"unknownCriticOpacity": {"$type":"number","$value":0.55,"source":"spec 4.7, index.html:161"}, "unknownCriticOpacity": {"$type":"number","$value":0.55,"source":"spec 4.7, index.html:161"},
"fallbackPadding": {"$type":"dimension","px":22,"dp":11,"source":"index.html:130"}, "fallbackPadding": {"$type":"dimension","px":22,"dp":11,"source":"index.html:130"},

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() }) } 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()) { 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") { 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") { item(key = "discover-chips", contentType = "chips") {
if (state.snapshot.status !in setOf(DiscoverStatus.NeedsSignIn, DiscoverStatus.Unavailable, DiscoverStatus.Loading)) DiscoverChips(state) 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 @Composable
private fun DiscoverHero(state: DiscoverState) { private fun DiscoverHero(state: DiscoverState) {
val title = state.hero ?: return val title = state.hero ?: return

View file

@ -48,6 +48,7 @@ fun LibraryScreen(
val active = !shell.navFocused && !shell.pickerVisible val active = !shell.navFocused && !shell.pickerVisible
val items = grid.items val items = grid.items
val focused = grid.focusedTitle val focused = grid.focusedTitle
val count = if (grid.catalogue) items.size else grid.source.size
var plateKey by remember { mutableStateOf<String?>(null) } var plateKey by remember { mutableStateOf<String?>(null) }
DisposableEffect(shell, grid, registerFocus) { DisposableEffect(shell, grid, registerFocus) {
if (registerFocus) shell.contentFocus = grid if (registerFocus) shell.contentFocus = grid
@ -104,7 +105,11 @@ fun LibraryScreen(
} }
} }
}, contentPadding = PaddingValues(bottom = WolfskinTokens.Shell.stageHeight - L.pinnedLine - rowHeight)) { }, 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) } if (headerCount == 2) item(key = "toolbar", contentType = "chips") { LibraryToolbar(grid, active) }
when (grid.status) { when (grid.status) {
LibraryStatus.Loading -> items(3, key = { "skeleton-$it" }, contentType = { "grid" }) { 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 @Composable
private fun LibraryHeader(grid: LibraryGridState, person: ShellPerson) { fun LibraryHeader(person: ShellPerson, title: String, subtitle: String = "", savedAt: String? = null) {
val count = if (grid.catalogue) grid.items.size else grid.source.size
Box(Modifier.fillMaxWidth().height(L.row_lhead)) { Box(Modifier.fillMaxWidth().height(L.row_lhead)) {
Row(Modifier.offset(L.safeSide, L.navBand), horizontalArrangement = Arrangement.spacedBy(WolfskinTokens.Shell.settingsHeaderGap), verticalAlignment = Alignment.CenterVertically) { 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) 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(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)) if (subtitle.isNotEmpty()) Text(subtitle, modifier = Modifier.alignByBaseline(), color = C.muted, style = G.headerSubtitle)
grid.savedAt?.let { Text("Saved $it", color = C.warn, style = WolfskinTokens.Type.caption) } 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 badgeIcon = 14.0.dp
val seeAllGap = 4.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 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 imageReads = 4.0f
const val unknownCriticOpacity = 0.55f const val unknownCriticOpacity = 0.55f
val fallbackPadding = 11.0.dp val fallbackPadding = 11.0.dp

View file

@ -206,6 +206,7 @@
--library-badgeIcon: 28.0px; --library-badgeIcon: 28.0px;
--library-seeAllGap: 8.0px; --library-seeAllGap: 8.0px;
--library-tailDescription: 24.0px; --library-tailDescription: 24.0px;
--library-headerSubtitle: 30.0px;
--library-fallbackPadding: 22.0px; --library-fallbackPadding: 22.0px;
--library-fallbackIcon: 44.0px; --library-fallbackIcon: 44.0px;
--discover-sheetPaddingX: 64.0px; --discover-sheetPaddingX: 64.0px;

View file

@ -1,6 +1,6 @@
ffc070ec958ece59f2e8eecd1144f43cdaf7fdb53fe51b178f1b17457ea4151a discover-tokens.json ffc070ec958ece59f2e8eecd1144f43cdaf7fdb53fe51b178f1b17457ea4151a discover-tokens.json
fcff646d59fb39e859c34c05037e05a7fb7583de2fe07315ec0176cf4d597980 generate_tokens.py fcff646d59fb39e859c34c05037e05a7fb7583de2fe07315ec0176cf4d597980 generate_tokens.py
17c45415eb8e334a31a8b187ad2e2c960c35b58e565b19fbbcc9cb5e10717a3a shell-tokens.json 050823bdd2bb71181a7b7c849e412129f312a150a27c2e5b9b3fbc1f5ccece0f shell-tokens.json
9359aaa6f81650ccfa46c3f0a76d128ea229ff16d79573b342804cc4cb12330e src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt ab20caec74fcbedadfbc59449b227a11a62cf79e8bc862d5f95ec95d1b8574db src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
b230411658c4b82e7f9e1677842c60bbf61939567958075fff277bc0561e00ec tokens.css 4b741830cda8b905d71f97bf88ee8d6cca3e1cf0f881bc06c5525cbf05378808 tokens.css
5b939f1f3eb90b428008fda75720f6e3a4e844c486668931db3d52bffb987b53 tokens.json 5b939f1f3eb90b428008fda75720f6e3a4e844c486668931db3d52bffb987b53 tokens.json