From 74033ee1da402396e61b4e0dc0502ecd43000d74 Mon Sep 17 00:00:00 2001 From: justinzeus Date: Thu, 5 Nov 2026 16:28:09 +0100 Subject: [PATCH 1/3] Match the settings pill border to the prototype --- docker/README.md | 5 ++++ wolfskin-design/shell-tokens.json | 26 +++++++++++++++++++ .../wolfskin/design/SettingsList.kt | 21 +++++++++++---- .../wolfskin/design/WolfskinTokens.kt | 5 ++++ wolfskin-design/tokens.css | 3 +++ wolfskin-design/tokens.sha256 | 6 ++--- 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/docker/README.md b/docker/README.md index 205f190a..0ccf48aa 100644 --- a/docker/README.md +++ b/docker/README.md @@ -77,6 +77,11 @@ resolves consistently in native captures; the approved JSON weights stay unchang Nav, picker names, labels and pill weights use that same mapping. Nav label colour fades for 200 ms ease-out; the blob retains its critically damped spring. +Settings pills have a separate 1 dp stroke (2 px on the stage), not a focus ring. +The stroke is inset within the fixed 20 dp / 40 px height; horizontal border-box +padding includes it. The 6 px dash / 4 px gap follows the browser's 3:2 stroke-width +ratios. Resting borders are white at 22% opacity; focused borders use ink at 40%. + The small gear under the final person opens Settings with Requests focused. Settings groups hand off to existing upstream pages; the Requests entry explicitly has no request data yet. **Advanced settings → Wolfskin screens** is the persistent diff --git a/wolfskin-design/shell-tokens.json b/wolfskin-design/shell-tokens.json index d5e0c688..b24e8e97 100644 --- a/wolfskin-design/shell-tokens.json +++ b/wolfskin-design/shell-tokens.json @@ -215,6 +215,32 @@ "dp": 20.0, "source": "locked index.html:71-92, 256-263, 303-328; spec 6.1 and nav-shell measured bounds" }, + "settingsPillStroke": { + "$type": "dimension", + "px": 2, + "dp": 1.0, + "source": "index.html:122; pill border 2 px inside its 40 px border-box" + }, + "settingsPillDashRatio": { + "$type": "number", + "$value": 3, + "source": "st-091 frozen browser top border: 6 px dashes at 2 px stroke width" + }, + "settingsPillDashGapRatio": { + "$type": "number", + "$value": 2, + "source": "st-091 frozen browser top border: 4 px gaps at 2 px stroke width" + }, + "settingsPillBorder": { + "$type": "color", + "$value": "#ffffff38", + "source": "index.html:122; white at 22 percent opacity, rounded to 8-bit alpha" + }, + "settingsPillFocusedBorder": { + "$type": "color", + "$value": "#0b0b1066", + "source": "index.html:328; focused pill ink at 40 percent opacity" + }, "settingsPillPadding": { "$type": "dimension", "px": 16, 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 9823c319..91bf3bb3 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 @@ -24,8 +24,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalDensity @@ -108,10 +110,19 @@ fun SettingsRow(group: SettingsGroup, focused: Boolean, openRequests: Int?, modi @Composable private fun SettingsPill(label: String, focused: Boolean) { val foreground = if (focused) C.onFocus else C.dim - Box(Modifier.height(S.settingsPillHeight).drawBehind { - val stroke = WolfskinTokens.Focus.ringButtonRing.toPx() - drawRoundRect(foreground.copy(alpha = .4f), cornerRadius = CornerRadius(WolfskinTokens.Radius.pill.toPx()), style = Stroke(stroke, pathEffect = PathEffect.dashPathEffect(floatArrayOf(stroke * 2, stroke * 2)))) - }.padding(horizontal = S.settingsPillPadding), contentAlignment = Alignment.Center) { + val border = if (focused) S.settingsPillFocusedBorder else S.settingsPillBorder + Box(Modifier.height(S.settingsPillHeight).drawWithCache { + val stroke = S.settingsPillStroke.toPx() + val inset = Offset(stroke / 2, stroke / 2) + val bounds = Size(size.width - stroke, size.height - stroke) + val outline = Stroke(stroke, pathEffect = PathEffect.dashPathEffect( + floatArrayOf(stroke * S.settingsPillDashRatio, stroke * S.settingsPillDashGapRatio), + )) + val radius = CornerRadius(WolfskinTokens.Radius.pill.toPx()) + onDrawBehind { + drawRoundRect(border, topLeft = inset, size = bounds, cornerRadius = radius, style = outline) + } + }.padding(horizontal = S.settingsPillPadding + S.settingsPillStroke), contentAlignment = Alignment.Center) { Text(label, color = foreground, style = WolfskinTokens.Type.caption.copy(fontWeight = WolfskinTokens.Type.label.fontWeight)) } } 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 234a5be8..e4071964 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 @@ -266,6 +266,11 @@ object WolfskinTokens { val settingsRowGap = 12.0.dp val settingsIcon = 18.0.dp val settingsPillHeight = 20.0.dp + val settingsPillStroke = 1.0.dp + const val settingsPillDashRatio = 3.0f + const val settingsPillDashGapRatio = 2.0f + val settingsPillBorder = ComposeColor(0x38FFFFFF) + val settingsPillFocusedBorder = ComposeColor(0x660B0B10) val settingsPillPadding = 8.0.dp val settingsPillGap = 6.0.dp val settingsChevron = 15.0.dp diff --git a/wolfskin-design/tokens.css b/wolfskin-design/tokens.css index 120ab55e..47b010b0 100644 --- a/wolfskin-design/tokens.css +++ b/wolfskin-design/tokens.css @@ -163,6 +163,9 @@ --shell-settingsRowGap: 24.0px; --shell-settingsIcon: 36.0px; --shell-settingsPillHeight: 40.0px; + --shell-settingsPillStroke: 2.0px; + --shell-settingsPillBorder: #ffffff38; + --shell-settingsPillFocusedBorder: #0b0b1066; --shell-settingsPillPadding: 16.0px; --shell-settingsPillGap: 12.0px; --shell-settingsChevron: 30.0px; diff --git a/wolfskin-design/tokens.sha256 b/wolfskin-design/tokens.sha256 index 8816c8ee..128b08d1 100644 --- a/wolfskin-design/tokens.sha256 +++ b/wolfskin-design/tokens.sha256 @@ -1,5 +1,5 @@ 1438f7ca4a2b6f951c9e643ab0afacbf2f7fc98e57905295d518c6225e054de4 generate_tokens.py -6d1d19656b40e6a442cb548bb238bb12244adb6b3b717ed9e564af61119559c8 shell-tokens.json -58131daf2bf889dcc2cf2a55f4a179fd6ad9bafa2da798b9a49a65ae3d58ae71 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt -9143548adf883d934ee86d5d4dd140a9a2d8116c6c8a905a41d2bf7d828bc371 tokens.css +408b7c3f4db2028deda97ac0570a57a4da4877917fd0980b2bfa5afe8f8fa790 shell-tokens.json +1037c46c9d804f9349e927257de13ba32cbd1c05e208ec0bc1d345f4b465f1eb src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt +c133085ef60612df9753ef3681446d9ccb2eb29b1ee1955b86254714c67525e3 tokens.css 5b939f1f3eb90b428008fda75720f6e3a4e844c486668931db3d52bffb987b53 tokens.json From a6dd04f390ce6ba06ce6d36b8afcc69c80ea21d6 Mon Sep 17 00:00:00 2001 From: justinzeus Date: Thu, 5 Nov 2026 16:44:21 +0100 Subject: [PATCH 2/3] Keep library focus on visible rows and match caption facts --- wolfskin-design/shell-tokens.json | 3 ++ .../justinvisser/wolfskin/design/Caption.kt | 24 +++++++++++--- .../wolfskin/design/LibraryGridState.kt | 6 ++-- .../wolfskin/design/LibraryScreen.kt | 5 +-- .../wolfskin/design/WolfskinTokens.kt | 3 ++ .../wolfskin/design/LibraryKeysTest.kt | 31 +++++++++++++++++++ wolfskin-design/tokens.css | 2 ++ wolfskin-design/tokens.sha256 | 6 ++-- 8 files changed, 68 insertions(+), 12 deletions(-) diff --git a/wolfskin-design/shell-tokens.json b/wolfskin-design/shell-tokens.json index 94aa9cb6..2f2bcdf6 100644 --- a/wolfskin-design/shell-tokens.json +++ b/wolfskin-design/shell-tokens.json @@ -298,6 +298,7 @@ "chipTop": {"$type":"dimension","px":16,"dp":8,"source":"index.html:184"}, "chipIcon": {"$type":"dimension","px":26,"dp":13,"source":"index.html:185"}, "captionWidth": {"$type":"dimension","px":640,"dp":320,"source":"index.html:151"}, + "captionSeparatorOpacity": {"$type":"number","$value":0.4,"source":"index.html:1207; spec 4.4"}, "plateTop": {"$type":"dimension","px":14,"dp":7,"source":"index.html:162"}, "plateSide": {"$type":"dimension","px":16,"dp":8,"source":"index.html:162"}, "plateBottom": {"$type":"dimension","px":46,"dp":23,"source":"index.html:162"}, @@ -312,6 +313,8 @@ "railWidth": {"$type":"dimension","px":52,"dp":26,"source":"index.html:204"}, "railLetterHeight": {"$type":"dimension","px":30,"dp":15,"source":"index.html:205"}, "railFocusHeight": {"$type":"dimension","px":44,"dp":22,"source":"index.html:208"}, + "railLetter": {"$type":"typography","px":24,"sp":12,"weight":700,"$value":{"fontWeight":700},"source":"index.html:205; prototype wins over spec 4.3 caption weight"}, + "railFocusedLetter": {"$type":"typography","px":28,"sp":14,"weight":700,"$value":{"fontWeight":700},"source":"index.html:208; prototype wins over spec 4.3 caption size"}, "panelPaddingX": {"$type":"dimension","px":40,"dp":20,"source":"index.html:194"}, "panelPaddingY": {"$type":"dimension","px":34,"dp":17,"source":"index.html:194"}, "panelTextGap": {"$type":"dimension","px":24,"dp":12,"source":"index.html:198"}, diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/Caption.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/Caption.kt index 147022bb..4a66fb88 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/Caption.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/Caption.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.tv.material3.Text import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle /** Captions fade without moving the reserved band under the focused poster. */ @Composable @@ -23,10 +22,25 @@ fun Caption(title: String, facts: String, visible: Boolean, modifier: Modifier = } Column(modifier.graphicsLayer { this.alpha = alpha.value }) { Text(title, color = WolfskinTokens.Color.fg, style = WolfskinTokens.Type.button, maxLines = 1) - val styledFacts = buildAnnotatedString { - val suffix = if (accentLastFact) facts.lastIndexOf(" · ").takeIf { it >= 0 }?.plus(" · ".length) ?: facts.length else facts.length - append(facts.substring(0, suffix)) - withStyle(SpanStyle(color = WolfskinTokens.Color.accentText)) { append(facts.substring(suffix)) } + val styledFacts = remember(facts, accentLastFact) { + buildAnnotatedString { + val separator = " · " + append(facts) + if (accentLastFact) { + val suffix = facts.lastIndexOf(separator).takeIf { it >= 0 }?.plus(separator.length) ?: facts.length + if (suffix < facts.length) addStyle(SpanStyle(color = WolfskinTokens.Color.accentText), suffix, facts.length) + } + var start = facts.indexOf(separator) + if (start >= 0) { + val muted = WolfskinTokens.Color.muted + val separatorStyle = SpanStyle(color = muted.copy(alpha = muted.alpha * WolfskinTokens.Library.captionSeparatorOpacity)) + val dotOffset = separator.indexOf('·') + while (start >= 0) { + addStyle(separatorStyle, start + dotOffset, start + dotOffset + 1) + start = facts.indexOf(separator, start + separator.length) + } + } + } } Text(styledFacts, color = WolfskinTokens.Color.muted, style = WolfskinTokens.Type.caption, maxLines = 1) } diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryGridState.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryGridState.kt index 16d95d3f..0921f582 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryGridState.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryGridState.kt @@ -109,10 +109,12 @@ class LibraryGridState( } if (toolbar) return toolbarKey(key) val currentItems = items - val last = if (tail != GridTail.None) tailIndex else currentItems.lastIndex when (key) { ShellKey.Up -> if (index >= columns) index -= columns else if (hasToolbar) toolbar = true else return false - ShellKey.Down -> if (index + columns <= last) index = (index + columns).coerceAtMost(last) else if (index / columns < last / columns) index = last + ShellKey.Down -> when { + index / columns < currentItems.lastIndex / columns -> index = (index + columns).coerceAtMost(currentItems.lastIndex) + tail != GridTail.None && index < tailIndex -> index = tailIndex + } ShellKey.Left -> if (index % columns > 0) index-- ShellKey.Right -> if (index % columns < columns - 1 && index < currentItems.lastIndex) index++ else if (railVisible) { inRail = true; railReturn = index; railLetter = currentItems[index].letter } ShellKey.Ok -> currentItems.getOrNull(index)?.let(onTitle) ?: if (pagination != null) pageRevision++ else onRecover() diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryScreen.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryScreen.kt index cdeb4d85..729f7bcd 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryScreen.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryScreen.kt @@ -162,7 +162,7 @@ private fun LibraryHeader(grid: LibraryGridState, person: ShellPerson) { Row(Modifier.offset(L.safeSide, L.navBand), horizontalArrangement = Arrangement.spacedBy(WolfskinTokens.Shell.settingsHeaderGap), verticalAlignment = Alignment.CenterVertically) { Avatar(person, WolfskinTokens.Shell.settingsHeaderAvatar, WolfskinTokens.Type.avatarLetterLibHeader) Text(grid.title, modifier = Modifier.alignByBaseline(), color = C.fg, style = WolfskinTokens.Type.screenTitle) - Text(if (grid.status == LibraryStatus.Loading) "" 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)) + 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) } } } @@ -245,7 +245,8 @@ private fun BoxScope.LibraryRail(grid: LibraryGridState) { Box(Modifier.width(G.railWidth).height(if (current && grid.inRail) G.railFocusHeight else G.railLetterHeight) .background(if (current && grid.inRail) C.fg else androidx.compose.ui.graphics.Color.Transparent, CircleShape) .clickable(enabled = letter in letters) { grid.railLetter = letter; grid.inRail = true; grid.key(ShellKey.Ok) }, contentAlignment = Alignment.Center) { - Text(letter.toString(), color = if (current) if (grid.inRail) C.onFocus else C.accent else if (letter in letters) C.muted else C.railEmpty, style = WolfskinTokens.Type.caption) + Text(letter.toString(), color = if (current) if (grid.inRail) C.onFocus else C.accent else if (letter in letters) C.muted else C.railEmpty, + style = if (current && grid.inRail) G.railFocusedLetter else G.railLetter) } } } 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 32d0841e..bb9d3e7d 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 @@ -287,6 +287,7 @@ object WolfskinTokens { val chipTop = 8.0.dp val chipIcon = 13.0.dp val captionWidth = 320.0.dp + const val captionSeparatorOpacity = 0.4f val plateTop = 7.0.dp val plateSide = 8.0.dp val plateBottom = 23.0.dp @@ -301,6 +302,8 @@ object WolfskinTokens { val railWidth = 26.0.dp val railLetterHeight = 15.0.dp val railFocusHeight = 22.0.dp + val railLetter = TextStyle(fontSize = 12.0.sp, fontWeight = FontWeight.Bold, lineHeight = 15.600000000000001.sp, letterSpacing = 0.0.em) + val railFocusedLetter = TextStyle(fontSize = 14.0.sp, fontWeight = FontWeight.Bold, lineHeight = 18.2.sp, letterSpacing = 0.0.em) val panelPaddingX = 20.0.dp val panelPaddingY = 17.0.dp val panelTextGap = 12.0.dp diff --git a/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/LibraryKeysTest.kt b/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/LibraryKeysTest.kt index a2bcba48..b8d27262 100644 --- a/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/LibraryKeysTest.kt +++ b/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/LibraryKeysTest.kt @@ -141,6 +141,37 @@ class LibraryKeysTest { assertEquals(8, grid.items.size) } + @Test fun downClampsShortLastRowBeforeEnteringRecoveryTail() { + var recoveries = 0 + val grid = LibraryGridState("Trending", + (0 until 20).map { LibraryTitle("$it", "Title $it", "movie") }, + catalogue = true, onRecover = { recoveries++ }, + ).apply { tail = GridTail.Retry; index = 14 } + shell.pickerVisible = false + shell.enterDestination("discover") + rule.setContent { + WolfskinTheme { + ShellSurface(shell) { modifier -> + LibraryScreen(shell, grid, modifier) + } + } + } + press(Key.DirectionDown) + assertEquals(19, grid.index) + assertEquals("Title 19", grid.focusedTitle!!.title) + assertEquals(0, recoveries) + press(Key.DirectionDown) + assertEquals(24, grid.index) + assertEquals("grid-tail", grid.cardKey) + press(Key.DirectionDown) + assertEquals(24, grid.index) + press(Key.DirectionCenter) + assertEquals(1, recoveries) + press(Key.DirectionUp) + assertEquals(18, grid.index) + assertEquals("Title 18", grid.focusedTitle!!.title) + } + @Test fun scoresFollowLocaleAndUnratedSortsLast() { assertEquals("7,8", LibraryRating("tmdb", 7.8).formatted(Locale.forLanguageTag("nl-NL"))) assertEquals("7.8", LibraryRating("tmdb", 7.8).formatted(Locale.US)) diff --git a/wolfskin-design/tokens.css b/wolfskin-design/tokens.css index a2133791..be4c0c23 100644 --- a/wolfskin-design/tokens.css +++ b/wolfskin-design/tokens.css @@ -186,6 +186,8 @@ --library-railWidth: 52.0px; --library-railLetterHeight: 30.0px; --library-railFocusHeight: 44.0px; + --library-railLetter: 24.0px; + --library-railFocusedLetter: 28.0px; --library-panelPaddingX: 40.0px; --library-panelPaddingY: 34.0px; --library-panelTextGap: 24.0px; diff --git a/wolfskin-design/tokens.sha256 b/wolfskin-design/tokens.sha256 index 6ebb4285..dc2c36e9 100644 --- a/wolfskin-design/tokens.sha256 +++ b/wolfskin-design/tokens.sha256 @@ -1,5 +1,5 @@ 1438f7ca4a2b6f951c9e643ab0afacbf2f7fc98e57905295d518c6225e054de4 generate_tokens.py -9d37e43db493a52cc5be6c9418d0c3c132dae875f70cfd0cd1bf97608e5a1425 shell-tokens.json -3f8a3a996f1dd1743dac970e9815431df5c72336f338598e8f6608aae6e16377 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt -25c87d3e0cf602764b20df29248b091764709c3b9b8bf6e7f966ed30ac8f7435 tokens.css +2d98b185e68c2f964cb744bb640dea94d3c5eaad183816d422646806b41327a3 shell-tokens.json +965e802974d8fd440c65a1178f49e7824e986648c49f4068efcc587f9b5b384f src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt +3db2663abf8fc15dc8ddee8adb18ad411065aff5ca8486b533c344596514e1d9 tokens.css 5b939f1f3eb90b428008fda75720f6e3a4e844c486668931db3d52bffb987b53 tokens.json From 60a0da13a2d222dc177ed85ad465d8cbedb3c541 Mon Sep 17 00:00:00 2001 From: justinzeus Date: Thu, 5 Nov 2026 16:55:12 +0100 Subject: [PATCH 3/3] Document visible tail navigation and library typography --- docker/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docker/README.md b/docker/README.md index c7ec0528..6103ea54 100644 --- a/docker/README.md +++ b/docker/README.md @@ -127,6 +127,13 @@ lazy grid. Up from its first row reaches Sort/Filter, then the nav; Back preserv 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. +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 +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. The debug-only `wolfskin-library.json` and `library/assets/` contain the locked prototype's titles and local poster art. Release libraries read through