Compare commits

..

No commits in common. "b8d5d8a12438343c459bbf6b61df098c61c3ef2a" and "439eea3ff8b3b148844cea5fcef029e3e176d05a" have entirely different histories.

9 changed files with 26 additions and 86 deletions

View file

@ -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

View file

@ -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" },

View file

@ -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)
}
}

View file

@ -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<ComingInRating>, 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)

View file

@ -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)

View file

@ -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)
}

View file

@ -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

View file

@ -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;

View file

@ -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