diff --git a/docker/README.md b/docker/README.md index a871b354..1a110199 100644 --- a/docker/README.md +++ b/docker/README.md @@ -142,8 +142,8 @@ Foreground refreshes once; an open sheet or unsettled requests refresh every 30 seconds while started. The immutable debug projection is loaded once, not decoded again on every poll. Thumbnails reuse `LibraryImages` with a 96×144-pixel request, RGB565, no crossfade, -the shared 64 MB memory/128 MB disk cache and four-read limit. Actor separator -dots use the surrounding text colour; the 40% rule applies only to poster captions. +the shared 64 MB memory/128 MB disk cache and four-read limit. Actor separators +reuse the shared caption-opacity token rather than defining another convention. Uncached focused ratings wait 350 ms; moving away cancels the read. Read ratings show immediately on return; sign-out clears visibility. This follows the frozen @@ -155,8 +155,7 @@ data replayed through the real shell. Home's hero content is not implemented here: its launch-to-nav shell suffix is the acceptance seam. Sheet dimensions, padding, glyphs, scales, colours and durations are generated -tokens. Scoped sheet line boxes retain CSS heights and baselines independently -of native leading rounding; shared shell/library typography is unchanged. Platform +tokens. Single-line sheet text retains CSS-style line-box leading. Platform Roboto remains required: the host browser used DejaVu Sans, so its width, wrapping, ellipsis and text-block positions are not a wrapping-parity target. For example, the 44-pixel “Coming in” ink is 239 pixels wide in the reference @@ -165,9 +164,7 @@ used. Native comparisons must report those structural differences explicitly. The six rating marks retain the exact locked SVG source in `wolfskin-design/request-marks/`; native-size PNGs and their hashes are generated -from the JSON dimensions with the locked contain fit (`.rr .rb` outranks `.crow img`). -Inside Coming-in rows they retain the prototype's dark `surface3` chips with 12-pixel rounded corners. -Loaded poster thumbnails have no placeholder fill beneath their clipped edge. +from the JSON dimensions, avoiding differences in older upstream conversions: ```sh uv run --with cairosvg==2.9.1 python wolfskin-design/generate_request_marks.py @@ -193,8 +190,8 @@ its unknown item count. Caption separators use 40% of the existing muted colour's alpha. The rail follows the locked prototype CSS: bold 24 px letters, with the focused letter bold 28 px, overriding the spec table's generic caption style for this component. -Library and Settings header initials use the muted colour over the person's fill; -navigation and picker avatars retain their dark initials. +Library header initials use the muted colour over the person's fill; navigation, +picker and Settings avatars retain their dark initials. The debug-only `wolfskin-library.json` and `library/assets/` contain the locked prototype's titles and local poster art. Release libraries read through diff --git a/wolfskin-design/shell-tokens.json b/wolfskin-design/shell-tokens.json index 50a8c5ec..8af37422 100644 --- a/wolfskin-design/shell-tokens.json +++ b/wolfskin-design/shell-tokens.json @@ -3,24 +3,14 @@ "sheetPaddingY": { "$type": "dimension", "px": 80, "dp": 40, "source": "index.html:227" }, "sheetPaddingX": { "$type": "dimension", "px": 64, "dp": 32, "source": "index.html:227" }, "sheetGap": { "$type": "dimension", "px": 18, "dp": 9, "source": "index.html:227" }, - "sheetTitleLineHeight": { "$type": "dimension", "px": 56, "dp": 28, "source": "index.html:228; locked st-083/st-086: content begins at 154px after 80px padding and 18px gap" }, - "sheetTitleBaseline": { "$type": "dimension", "px": 44, "dp": 22, "source": "locked st-083: modal ink-bottom 123px, native baseline boundary 124px minus 80px padding" }, - "titleLineHeight": { "$type": "dimension", "px": 39, "dp": 19.5, "source": "index.html:250; 30px font with 1.3 line box" }, - "titleBaseline": { "$type": "dimension", "px": 31, "dp": 15.5, "source": "locked st-082/st-086: ink-bottom 45px, 15px text-block inset, baseline boundary 1px below ink" }, - "factsLineHeight": { "$type": "dimension", "px": 31.2, "dp": 15.6, "source": "index.html:251; 24px font with 1.3 line box" }, - "factsBaseline": { "$type": "dimension", "px": 24, "dp": 12, "source": "locked st-082/st-086: ink-bottom 77px, 15px block inset, 39px title box and 1px baseline boundary" }, - "stageLineHeight": { "$type": "dimension", "px": 39.6, "dp": 19.8, "source": "index.html:251-252; inline-flex stage within the 28px/1.3 parent strut" }, - "stageLabelLineHeight": { "$type": "dimension", "px": 33.8, "dp": 16.9, "source": "index.html:220; 26px font with 1.3 leading" }, - "stageBaseline": { "$type": "dimension", "px": 24, "dp": 12, "source": "locked st-082/st-086: ink-bottom 112px, 15px block inset, 39px title, 31.2px facts, 4px margin and 1px baseline boundary" }, "emptyMargin": { "$type": "dimension", "px": 20, "dp": 10, "source": "index.html:1905" }, "rowPaddingY": { "$type": "dimension", "px": 14, "dp": 7, "source": "index.html:247" }, "rowPaddingX": { "$type": "dimension", "px": 18, "dp": 9, "source": "index.html:247" }, "rowGap": { "$type": "dimension", "px": 24, "dp": 12, "source": "index.html:247" }, "listGap": { "$type": "dimension", "px": 6, "dp": 3, "source": "index.html:255" }, "stageMargin": { "$type": "dimension", "px": 4, "dp": 2, "source": "index.html:252" }, - "stageGlyph": { "$type": "dimension", "px": 28, "dp": 14, "source": "index.html:221" }, - "stageGap": { "$type": "dimension", "px": 10, "dp": 5, "source": "index.html:220" }, - "readinessItemGap": { "$type": "dimension", "px": 8, "dp": 4, "source": "index.html:313" }, + "stageGlyph": { "$type": "dimension", "px": 28, "dp": 14, "source": "index.html:152" }, + "stageGap": { "$type": "dimension", "px": 8, "dp": 4, "source": "index.html:152" }, "readinessGapX": { "$type": "dimension", "px": 22, "dp": 11, "source": "index.html:312" }, "readinessGapY": { "$type": "dimension", "px": 10, "dp": 5, "source": "index.html:312" }, "readinessMargin": { "$type": "dimension", "px": 8, "dp": 4, "source": "index.html:312" }, diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInLineBox.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInLineBox.kt deleted file mode 100644 index e7ae86a9..00000000 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInLineBox.kt +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.design - -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.FirstBaseline -import androidx.compose.ui.layout.layout -import androidx.compose.ui.unit.Dp - -/** A CSS line box has its own height and baseline, independent of native leading rounding. */ -internal fun Modifier.comingInLineBox(height: Dp, baseline: Dp): Modifier = layout { measurable, constraints -> - val text = measurable.measure(constraints.copy(minHeight = 0)) - val baselineOffset = baseline.roundToPx() - text[FirstBaseline] - layout(text.width, height.roundToPx()) { - text.placeRelative(0, baselineOffset) - } -} diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInRatings.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInRatings.kt index 3f2b0da1..1aebb97b 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInRatings.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInRatings.kt @@ -11,10 +11,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.foundation.background -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale import androidx.tv.material3.Text import java.util.Locale import org.justinvisser.wolfskin.design.WolfskinTokens.Requests as T @@ -39,13 +35,9 @@ fun ComingInRatings(ratings: List, color: Color) { } val wide = rating.mark == "imdb" || rating.mark == "tmdb" Image(painterResource(mark), contentDescription = rating.mark, - modifier = Modifier.height(T.readinessGlyph).width(T.readinessGlyph * if (wide) T.ratingsWordmarkRatio else 1f) - .clip(ratingMarkShape).background(WolfskinTokens.Color.surface3), - contentScale = ContentScale.Fit) + modifier = Modifier.height(T.readinessGlyph).width(T.readinessGlyph * if (wide) T.ratingsWordmarkRatio else 1f)) Text(formatComingInRating(rating), style = ComingInTypography.rating, color = color) } } } } - -private val ratingMarkShape = RoundedCornerShape(WolfskinTokens.Radius.thumb) diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInSheet.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInSheet.kt index 4bb90514..de14a184 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInSheet.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ComingInSheet.kt @@ -27,8 +27,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.tv.material3.Text import coil3.compose.AsyncImage -import coil3.compose.AsyncImagePainter -import androidx.compose.runtime.mutableStateOf import org.justinvisser.wolfskin.design.WolfskinTokens.Color as C import org.justinvisser.wolfskin.design.WolfskinTokens.Layout as L import org.justinvisser.wolfskin.design.WolfskinTokens.Requests as T @@ -59,7 +57,7 @@ fun ComingInSheet(shell: ShellState, modifier: Modifier = Modifier) { } .padding(horizontal = T.sheetPaddingX, vertical = T.sheetPaddingY) .testTag("coming-in"), verticalArrangement = Arrangement.spacedBy(T.sheetGap)) { - Text("Coming in", Modifier.comingInLineBox(T.sheetTitleLineHeight, T.sheetTitleBaseline), style = ComingInTypography.title, color = C.fg) + Text("Coming in", style = ComingInTypography.title, color = C.fg) when { data.status == ComingInStatus.Loading && data.items.isEmpty() -> { LazyColumn(verticalArrangement = Arrangement.spacedBy(T.listGap)) { @@ -125,7 +123,6 @@ fun ComingInRow(item: ComingInRequest, viewer: ShellPerson, focused: Boolean, ex val secondary = if (focused) C.onFocus else C.muted val context = LocalContext.current val request = remember(item.poster, context) { LibraryImages.request(context, item.poster, L.thumbWidth, L.thumbHeight) } - val posterReady = remember(request) { mutableStateOf(false) } Row(Modifier.fillMaxWidth().focusScale(focused, WolfskinTokens.Focus.scaleRow) .background(if (focused) C.fg else Color.Transparent, RoundedCornerShape(WolfskinTokens.Radius.panel)) .pointerInput(item.id) { @@ -136,26 +133,26 @@ fun ComingInRow(item: ComingInRequest, viewer: ShellPerson, focused: Boolean, ex .padding(horizontal = T.rowPaddingX, vertical = T.rowPaddingY).testTag("coming-row-${item.id}"), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.rowGap)) { AsyncImage(model = request, imageLoader = LibraryImages.loader(context), contentDescription = null, - modifier = Modifier.size(L.thumbWidth, L.thumbHeight).clip(RoundedCornerShape(WolfskinTokens.Radius.thumb)) - .background(if (posterReady.value) Color.Transparent else C.surface3), - onState = { posterReady.value = it is AsyncImagePainter.State.Success }, + modifier = Modifier.size(L.thumbWidth, L.thumbHeight).clip(RoundedCornerShape(WolfskinTokens.Radius.thumb)).background(C.surface3), contentScale = androidx.compose.ui.layout.ContentScale.Crop) Column(Modifier.weight(1f)) { - Text(item.title, Modifier.comingInLineBox(T.titleLineHeight, T.titleBaseline), - style = ComingInTypography.listTitle, color = foreground, maxLines = 1, overflow = TextOverflow.Ellipsis) - Text("for ${item.recipient(viewer)} · by ${item.actor(viewer)}", Modifier.comingInLineBox(T.factsLineHeight, T.factsBaseline), - style = ComingInTypography.caption, color = secondary) - FlowRow(Modifier.padding(top = T.stageMargin).heightIn(min = T.stageLineHeight), horizontalArrangement = Arrangement.spacedBy(T.readinessGlyph)) { + Text(item.title, style = ComingInTypography.listTitle, color = foreground, maxLines = 1, overflow = TextOverflow.Ellipsis) + Row { + Text("for ${item.recipient(viewer)}", style = ComingInTypography.caption, color = secondary) + Text(" · ", style = ComingInTypography.caption, color = secondary.copy(alpha = secondary.alpha * WolfskinTokens.Library.captionSeparatorOpacity)) + Text("by ${item.actor(viewer)}", style = ComingInTypography.caption, color = secondary) + } + FlowRow(Modifier.padding(top = T.stageMargin), horizontalArrangement = Arrangement.spacedBy(T.readinessGlyph)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.stageGap)) { Glyph(item.stage.glyph, secondary, Modifier.size(T.stageGlyph)) - Text(item.stage.label, Modifier.comingInLineBox(T.stageLabelLineHeight, T.stageBaseline), style = ComingInTypography.label, color = secondary) + Text(item.stage.label, style = ComingInTypography.label, color = secondary) } if (showRatings) ComingInRatings(item.ratings, foreground) } if (expanded) FlowRow(Modifier.padding(top = T.readinessMargin), horizontalArrangement = Arrangement.spacedBy(T.readinessGapX), verticalArrangement = Arrangement.spacedBy(T.readinessGapY)) { item.stage.readiness.forEachIndexed { index, word -> - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.readinessItemGap)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.stageGap)) { val glyph = when (word) { "Done" -> "check"; "Failed" -> "alert"; "Not yet", "Unknown" -> null; else -> "clock" } if (glyph != null) Glyph(glyph, secondary, Modifier.size(T.readinessGlyph)) Text("${READINESS_NAMES[index]} · $word", style = ComingInTypography.readiness, color = secondary) diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt index 48a8220b..a7c0c54e 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt @@ -70,7 +70,7 @@ fun SettingsList(state: ShellState, modifier: Modifier = Modifier, openRequests: item(key = "settings-header") { Row(Modifier.height(L.row_lhead).padding(start = L.safeSide, top = S.settingsHeaderTop), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(S.settingsHeaderGap)) { - Avatar(state.person, S.settingsHeaderAvatar, WolfskinTokens.Type.avatarLetterLibHeader, letterColor = C.muted) + Avatar(state.person, S.settingsHeaderAvatar, WolfskinTokens.Type.avatarLetterLibHeader) Text("Settings", Modifier.alignByBaseline(), style = WolfskinTokens.Type.screenTitle, color = C.fg) Text("Requests and Wholphin settings on this TV · ${state.person.name}", Modifier.alignByBaseline(), style = WolfskinTokens.Type.nav.copy(fontWeight = FontWeight.Normal), color = C.muted) } diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt index bf874972..74ce6aec 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt @@ -233,15 +233,6 @@ object WolfskinTokens { val sheetPaddingY = 40.0.dp val sheetPaddingX = 32.0.dp val sheetGap = 9.0.dp - val sheetTitleLineHeight = 28.0.dp - val sheetTitleBaseline = 22.0.dp - val titleLineHeight = 19.5.dp - val titleBaseline = 15.5.dp - val factsLineHeight = 15.6.dp - val factsBaseline = 12.0.dp - val stageLineHeight = 19.8.dp - val stageLabelLineHeight = 16.9.dp - val stageBaseline = 12.0.dp val emptyMargin = 10.0.dp val rowPaddingY = 7.0.dp val rowPaddingX = 9.0.dp @@ -249,8 +240,7 @@ object WolfskinTokens { val listGap = 3.0.dp val stageMargin = 2.0.dp val stageGlyph = 14.0.dp - val stageGap = 5.0.dp - val readinessItemGap = 4.0.dp + val stageGap = 4.0.dp val readinessGapX = 11.0.dp val readinessGapY = 5.0.dp val readinessMargin = 4.0.dp diff --git a/wolfskin-design/tokens.css b/wolfskin-design/tokens.css index 7aba6641..694336d8 100644 --- a/wolfskin-design/tokens.css +++ b/wolfskin-design/tokens.css @@ -130,15 +130,6 @@ --requests-sheetPaddingY: 80.0px; --requests-sheetPaddingX: 64.0px; --requests-sheetGap: 18.0px; - --requests-sheetTitleLineHeight: 56.0px; - --requests-sheetTitleBaseline: 44.0px; - --requests-titleLineHeight: 39.0px; - --requests-titleBaseline: 31.0px; - --requests-factsLineHeight: 31.2px; - --requests-factsBaseline: 24.0px; - --requests-stageLineHeight: 39.6px; - --requests-stageLabelLineHeight: 33.8px; - --requests-stageBaseline: 24.0px; --requests-emptyMargin: 20.0px; --requests-rowPaddingY: 14.0px; --requests-rowPaddingX: 18.0px; @@ -146,8 +137,7 @@ --requests-listGap: 6.0px; --requests-stageMargin: 4.0px; --requests-stageGlyph: 28.0px; - --requests-stageGap: 10.0px; - --requests-readinessItemGap: 8.0px; + --requests-stageGap: 8.0px; --requests-readinessGapX: 22.0px; --requests-readinessGapY: 10.0px; --requests-readinessMargin: 8.0px; diff --git a/wolfskin-design/tokens.sha256 b/wolfskin-design/tokens.sha256 index 9143d9ac..17919f8b 100644 --- a/wolfskin-design/tokens.sha256 +++ b/wolfskin-design/tokens.sha256 @@ -1,5 +1,5 @@ 1438f7ca4a2b6f951c9e643ab0afacbf2f7fc98e57905295d518c6225e054de4 generate_tokens.py -61568cdb87c1b64984033e6d66b06c4aee1870580fb5640ed4a1a97100451c0a shell-tokens.json -cddf8798e731206e1adf1e66384ffcb1b679b27e2ee9f33fd128a04850fd829f src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt -60ac4a0fff7d7965a9c4fa25d71a10cb304c3e636031ccf036a33459571e04fe tokens.css +dafb97c1d26ce435dc4b28b57cc02475a13901191824568c39e88e0474acf86f shell-tokens.json +673d4ece482b480c88acb085438f1e03d979bfa9c85a960c69a5543987e42c9f src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt +16318b38182d7248402c4c448ead5444644eb59a7fd5b63c4e845ffee0085b4a tokens.css 5b939f1f3eb90b428008fda75720f6e3a4e844c486668931db3d52bffb987b53 tokens.json