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 30 seconds while started. The immutable debug projection is loaded once, not
decoded again on every poll. decoded again on every poll.
Thumbnails reuse `LibraryImages` with a 96×144-pixel request, RGB565, no crossfade, 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 the shared 64 MB memory/128 MB disk cache and four-read limit. Actor separators
dots use the surrounding text colour; the 40% rule applies only to poster captions. reuse the shared caption-opacity token rather than defining another convention.
Uncached focused ratings wait 350 ms; moving away cancels the read. Read ratings 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 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. here: its launch-to-nav shell suffix is the acceptance seam.
Sheet dimensions, padding, glyphs, scales, colours and durations are generated Sheet dimensions, padding, glyphs, scales, colours and durations are generated
tokens. Scoped sheet line boxes retain CSS heights and baselines independently tokens. Single-line sheet text retains CSS-style line-box leading. Platform
of native leading rounding; shared shell/library typography is unchanged. Platform
Roboto remains required: the host browser used DejaVu Sans, so its width, Roboto remains required: the host browser used DejaVu Sans, so its width,
wrapping, ellipsis and text-block positions are not a wrapping-parity target. 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 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 The six rating marks retain the exact locked SVG source in
`wolfskin-design/request-marks/`; native-size PNGs and their hashes are generated `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`). from the JSON dimensions, avoiding differences in older upstream conversions:
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.
```sh ```sh
uv run --with cairosvg==2.9.1 python wolfskin-design/generate_request_marks.py 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 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, 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. 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; Library header initials use the muted colour over the person's fill; navigation,
navigation and picker avatars retain their dark initials. picker and Settings avatars retain their dark initials.
The debug-only `wolfskin-library.json` and `library/assets/` contain the locked The debug-only `wolfskin-library.json` and `library/assets/` contain the locked
prototype's titles and local poster art. Release libraries read through 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" }, "sheetPaddingY": { "$type": "dimension", "px": 80, "dp": 40, "source": "index.html:227" },
"sheetPaddingX": { "$type": "dimension", "px": 64, "dp": 32, "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" }, "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" }, "emptyMargin": { "$type": "dimension", "px": 20, "dp": 10, "source": "index.html:1905" },
"rowPaddingY": { "$type": "dimension", "px": 14, "dp": 7, "source": "index.html:247" }, "rowPaddingY": { "$type": "dimension", "px": 14, "dp": 7, "source": "index.html:247" },
"rowPaddingX": { "$type": "dimension", "px": 18, "dp": 9, "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" }, "rowGap": { "$type": "dimension", "px": 24, "dp": 12, "source": "index.html:247" },
"listGap": { "$type": "dimension", "px": 6, "dp": 3, "source": "index.html:255" }, "listGap": { "$type": "dimension", "px": 6, "dp": 3, "source": "index.html:255" },
"stageMargin": { "$type": "dimension", "px": 4, "dp": 2, "source": "index.html:252" }, "stageMargin": { "$type": "dimension", "px": 4, "dp": 2, "source": "index.html:252" },
"stageGlyph": { "$type": "dimension", "px": 28, "dp": 14, "source": "index.html:221" }, "stageGlyph": { "$type": "dimension", "px": 28, "dp": 14, "source": "index.html:152" },
"stageGap": { "$type": "dimension", "px": 10, "dp": 5, "source": "index.html:220" }, "stageGap": { "$type": "dimension", "px": 8, "dp": 4, "source": "index.html:152" },
"readinessItemGap": { "$type": "dimension", "px": 8, "dp": 4, "source": "index.html:313" },
"readinessGapX": { "$type": "dimension", "px": 22, "dp": 11, "source": "index.html:312" }, "readinessGapX": { "$type": "dimension", "px": 22, "dp": 11, "source": "index.html:312" },
"readinessGapY": { "$type": "dimension", "px": 10, "dp": 5, "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" }, "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.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource 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 androidx.tv.material3.Text
import java.util.Locale import java.util.Locale
import org.justinvisser.wolfskin.design.WolfskinTokens.Requests as T 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" val wide = rating.mark == "imdb" || rating.mark == "tmdb"
Image(painterResource(mark), contentDescription = rating.mark, Image(painterResource(mark), contentDescription = rating.mark,
modifier = Modifier.height(T.readinessGlyph).width(T.readinessGlyph * if (wide) T.ratingsWordmarkRatio else 1f) modifier = Modifier.height(T.readinessGlyph).width(T.readinessGlyph * if (wide) T.ratingsWordmarkRatio else 1f))
.clip(ratingMarkShape).background(WolfskinTokens.Color.surface3),
contentScale = ContentScale.Fit)
Text(formatComingInRating(rating), style = ComingInTypography.rating, color = color) 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.compose.ui.text.style.TextOverflow
import androidx.tv.material3.Text import androidx.tv.material3.Text
import coil3.compose.AsyncImage 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.Color as C
import org.justinvisser.wolfskin.design.WolfskinTokens.Layout as L import org.justinvisser.wolfskin.design.WolfskinTokens.Layout as L
import org.justinvisser.wolfskin.design.WolfskinTokens.Requests as T 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) .padding(horizontal = T.sheetPaddingX, vertical = T.sheetPaddingY)
.testTag("coming-in"), verticalArrangement = Arrangement.spacedBy(T.sheetGap)) { .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 { when {
data.status == ComingInStatus.Loading && data.items.isEmpty() -> { data.status == ComingInStatus.Loading && data.items.isEmpty() -> {
LazyColumn(verticalArrangement = Arrangement.spacedBy(T.listGap)) { 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 secondary = if (focused) C.onFocus else C.muted
val context = LocalContext.current val context = LocalContext.current
val request = remember(item.poster, context) { LibraryImages.request(context, item.poster, L.thumbWidth, L.thumbHeight) } 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) Row(Modifier.fillMaxWidth().focusScale(focused, WolfskinTokens.Focus.scaleRow)
.background(if (focused) C.fg else Color.Transparent, RoundedCornerShape(WolfskinTokens.Radius.panel)) .background(if (focused) C.fg else Color.Transparent, RoundedCornerShape(WolfskinTokens.Radius.panel))
.pointerInput(item.id) { .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}"), .padding(horizontal = T.rowPaddingX, vertical = T.rowPaddingY).testTag("coming-row-${item.id}"),
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.rowGap)) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.rowGap)) {
AsyncImage(model = request, imageLoader = LibraryImages.loader(context), contentDescription = null, AsyncImage(model = request, imageLoader = LibraryImages.loader(context), contentDescription = null,
modifier = Modifier.size(L.thumbWidth, L.thumbHeight).clip(RoundedCornerShape(WolfskinTokens.Radius.thumb)) modifier = Modifier.size(L.thumbWidth, L.thumbHeight).clip(RoundedCornerShape(WolfskinTokens.Radius.thumb)).background(C.surface3),
.background(if (posterReady.value) Color.Transparent else C.surface3),
onState = { posterReady.value = it is AsyncImagePainter.State.Success },
contentScale = androidx.compose.ui.layout.ContentScale.Crop) contentScale = androidx.compose.ui.layout.ContentScale.Crop)
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Text(item.title, Modifier.comingInLineBox(T.titleLineHeight, T.titleBaseline), Text(item.title, style = ComingInTypography.listTitle, color = foreground, maxLines = 1, overflow = TextOverflow.Ellipsis)
style = ComingInTypography.listTitle, color = foreground, maxLines = 1, overflow = TextOverflow.Ellipsis) Row {
Text("for ${item.recipient(viewer)} · by ${item.actor(viewer)}", Modifier.comingInLineBox(T.factsLineHeight, T.factsBaseline), Text("for ${item.recipient(viewer)}", style = ComingInTypography.caption, color = secondary)
style = ComingInTypography.caption, color = secondary) Text(" · ", style = ComingInTypography.caption, color = secondary.copy(alpha = secondary.alpha * WolfskinTokens.Library.captionSeparatorOpacity))
FlowRow(Modifier.padding(top = T.stageMargin).heightIn(min = T.stageLineHeight), horizontalArrangement = Arrangement.spacedBy(T.readinessGlyph)) { 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)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(T.stageGap)) {
Glyph(item.stage.glyph, secondary, Modifier.size(T.stageGlyph)) 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 (showRatings) ComingInRatings(item.ratings, foreground)
} }
if (expanded) FlowRow(Modifier.padding(top = T.readinessMargin), if (expanded) FlowRow(Modifier.padding(top = T.readinessMargin),
horizontalArrangement = Arrangement.spacedBy(T.readinessGapX), verticalArrangement = Arrangement.spacedBy(T.readinessGapY)) { horizontalArrangement = Arrangement.spacedBy(T.readinessGapX), verticalArrangement = Arrangement.spacedBy(T.readinessGapY)) {
item.stage.readiness.forEachIndexed { index, word -> 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" } val glyph = when (word) { "Done" -> "check"; "Failed" -> "alert"; "Not yet", "Unknown" -> null; else -> "clock" }
if (glyph != null) Glyph(glyph, secondary, Modifier.size(T.readinessGlyph)) if (glyph != null) Glyph(glyph, secondary, Modifier.size(T.readinessGlyph))
Text("${READINESS_NAMES[index]} · $word", style = ComingInTypography.readiness, color = secondary) 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") { item(key = "settings-header") {
Row(Modifier.height(L.row_lhead).padding(start = L.safeSide, top = S.settingsHeaderTop), Row(Modifier.height(L.row_lhead).padding(start = L.safeSide, top = S.settingsHeaderTop),
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(S.settingsHeaderGap)) { 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("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) 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 sheetPaddingY = 40.0.dp
val sheetPaddingX = 32.0.dp val sheetPaddingX = 32.0.dp
val sheetGap = 9.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 emptyMargin = 10.0.dp
val rowPaddingY = 7.0.dp val rowPaddingY = 7.0.dp
val rowPaddingX = 9.0.dp val rowPaddingX = 9.0.dp
@ -249,8 +240,7 @@ object WolfskinTokens {
val listGap = 3.0.dp val listGap = 3.0.dp
val stageMargin = 2.0.dp val stageMargin = 2.0.dp
val stageGlyph = 14.0.dp val stageGlyph = 14.0.dp
val stageGap = 5.0.dp val stageGap = 4.0.dp
val readinessItemGap = 4.0.dp
val readinessGapX = 11.0.dp val readinessGapX = 11.0.dp
val readinessGapY = 5.0.dp val readinessGapY = 5.0.dp
val readinessMargin = 4.0.dp val readinessMargin = 4.0.dp

View file

@ -130,15 +130,6 @@
--requests-sheetPaddingY: 80.0px; --requests-sheetPaddingY: 80.0px;
--requests-sheetPaddingX: 64.0px; --requests-sheetPaddingX: 64.0px;
--requests-sheetGap: 18.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-emptyMargin: 20.0px;
--requests-rowPaddingY: 14.0px; --requests-rowPaddingY: 14.0px;
--requests-rowPaddingX: 18.0px; --requests-rowPaddingX: 18.0px;
@ -146,8 +137,7 @@
--requests-listGap: 6.0px; --requests-listGap: 6.0px;
--requests-stageMargin: 4.0px; --requests-stageMargin: 4.0px;
--requests-stageGlyph: 28.0px; --requests-stageGlyph: 28.0px;
--requests-stageGap: 10.0px; --requests-stageGap: 8.0px;
--requests-readinessItemGap: 8.0px;
--requests-readinessGapX: 22.0px; --requests-readinessGapX: 22.0px;
--requests-readinessGapY: 10.0px; --requests-readinessGapY: 10.0px;
--requests-readinessMargin: 8.0px; --requests-readinessMargin: 8.0px;

View file

@ -1,5 +1,5 @@
1438f7ca4a2b6f951c9e643ab0afacbf2f7fc98e57905295d518c6225e054de4 generate_tokens.py 1438f7ca4a2b6f951c9e643ab0afacbf2f7fc98e57905295d518c6225e054de4 generate_tokens.py
61568cdb87c1b64984033e6d66b06c4aee1870580fb5640ed4a1a97100451c0a shell-tokens.json dafb97c1d26ce435dc4b28b57cc02475a13901191824568c39e88e0474acf86f shell-tokens.json
cddf8798e731206e1adf1e66384ffcb1b679b27e2ee9f33fd128a04850fd829f src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt 673d4ece482b480c88acb085438f1e03d979bfa9c85a960c69a5543987e42c9f src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
60ac4a0fff7d7965a9c4fa25d71a10cb304c3e636031ccf036a33459571e04fe tokens.css 16318b38182d7248402c4c448ead5444644eb59a7fd5b63c4e845ffee0085b4a tokens.css
5b939f1f3eb90b428008fda75720f6e3a4e844c486668931db3d52bffb987b53 tokens.json 5b939f1f3eb90b428008fda75720f6e3a4e844c486668931db3d52bffb987b53 tokens.json