Skip to content

[TV] Provide design tokens through a TvTheme provider - #5703

Merged
sztomek merged 16 commits into
mainfrom
feat/tv-theme-provider-prototype
Aug 6, 2026
Merged

[TV] Provide design tokens through a TvTheme provider#5703
sztomek merged 16 commits into
mainfrom
feat/tv-theme-provider-prototype

Conversation

@sztomek

@sztomek sztomek commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Description

Moves the TV design tokens out of static singletons (TvColors, TvTextStyles) and behind a proper theme provider, so tokens are delivered through the composition instead of referenced as global constants.

Design (hybrid, mirroring how the phone app and wear already theme):

  • TvColorScheme / TvTypography@Immutable data classes holding the Figma tokens (names preserved, now lowerCamel), provided via staticCompositionLocalOf from a new root TvTheme composable.
  • Call sites read MaterialTheme.tvColors.textPrimary / MaterialTheme.tvTypography.caption2 via extension properties on androidx.tv.material3.MaterialTheme — the same idiom as the phone app's MaterialTheme.theme.colors….
  • TvTheme also derives a tv-material3 ColorScheme + Typography from the tokens (the analogue of the phone's buildMaterialColors shim), so bare tv-material3 components, contentColorFor, and previews get sensible defaults instead of stock Material dark.
  • TvTheme wraps AppTheme(EXTRA_DARK) internally, keeping the shared LocalColors available (folder colors in TvFolderCard). Root wiring moved to TvActivity; the old AppTheme { MaterialTheme { … } } wrapper is gone from TvOnboardingNavHost, and all previews collapsed to a single TvTheme { } wrapper.

Testing Instructions

  1. Install the tv debug build on an Android TV device/emulator.
  2. Walk welcome → sign-in → home tabs → podcast/playlist details → up next.
  3. Check focus behavior everywhere: tiles/buttons/episode rows must invert to white background + dark text when focused; resting buttons keep the 20 % white background.
  4. Check the caption1 texts listed above and the home error retry button.
  5. Open a few previews in Android Studio (TvTilePreview, TvTabBarPreview, …) — they should render with the design-system colors under the single TvTheme wrapper.

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml
  • Any jetpack compose components I added or changed are covered by compose previews
  • I have updated (or requested that someone edit) the Event Horizon schema to reflect any new or changed analytics.

I have tested any UI changes...

  • with different themes
  • with a landscape orientation
  • with the device set to have a large display and font size
  • for accessibility with TalkBack

@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude is reviewing this PR…

  • Gather context (diff, base branch, repo guidelines)
  • Review new theme infrastructure (TvTheme, TvColorScheme, TvTypography)
  • Review call-site migrations for token drift / behavior changes
  • Verify root wiring (TvActivity, previews, AppTheme bridge)
  • Post review

View job run • branch feat/tv-theme-provider-prototype

@dangermattic

dangermattic commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator
2 Warnings
⚠️ View files have been modified, but no screenshot or video is included in the pull request. Consider adding some for clarity.
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

@sztomek sztomek added this to the 8.19 milestone Aug 5, 2026
@sztomek sztomek added [Type] Enhancement Improve an existing feature. [Area] Theme [Area] TV labels Aug 5, 2026
@sztomek
sztomek marked this pull request as ready for review August 5, 2026 16:03
@sztomek
sztomek requested a review from a team as a code owner August 5, 2026 16:03
@sztomek
sztomek requested review from geekygecko and removed request for a team August 5, 2026 16:03
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @sztomek's task in 7m 18s —— View job


Code review

  • Gather context (diff, base branch, repo guidelines)
  • Review new theme infrastructure (TvTheme, TvColorScheme, TvTypography)
  • Review call-site migrations for token drift / behavior changes
  • Verify root wiring (TvActivity, previews, AppTheme bridge)
  • Post review

Solid refactor — the structure mirrors the phone app's idiom closely and the mechanical part is clean. I verified the things most likely to go wrong in a 985/985 rename-heavy PR and they check out; the substantive comments are all about the derived tv-material3 scheme, which is the genuinely new behavior here.

What I verified

  • No token drift. All 16 colors in TvColorScheme and all 17 styles in TvTypography are byte-identical to the deleted TvColors / TvTextStyles (values, weights, line heights, letterSpacing, textAlign, includeFontPadding). Nothing was silently rounded or dropped in the lowerCamel rename.
  • Call sites are 1:1. I diffed the removed TvColors.X/TvTextStyles.X reads against the added tvColors.x/tvTypography.x reads — every substitution maps to the same token, including the if (isFocused) … else … pairs, the multi-stop Brush gradients in TvFeaturedTile/TvWelcomeScreen/TvScaffold, and the TabDefaults.pillIndicatorTabColors block in TvTabBar.
  • No leftovers. Zero references to TvColors/TvTextStyles remain, and no file imports androidx.compose.material3.MaterialTheme (so MaterialTheme.tvColors always resolves against the tv-material3 object as intended).
  • Root wiring is complete. TvActivity is the only activity in the module and TvOnboardingNavHost has no other caller, so moving the wrapper up is safe. AppTheme → tv MaterialTheme nesting order is unchanged from before, so LocalColors (folder colors in TvFolderCard) still resolves. Dialog in TvModal inherits the parent composition's locals, so the modals keep their tokens.

Findings

1. The bodyLarge = caption1 mapping changes 11 labels, not 6 + the retry button (inline · theme/TvTheme.kt:89)

tv-material3's MaterialTheme ends with ProvideTextStyle(typography.bodyLarge), so bodyLarge is the app-wide default LocalTextStyle, not just what bare components read. Every Text without an explicit style now renders 16.67sp/w510 instead of 16sp/w400: TvVideoTile.kt:95,101, TvFeaturedTile.kt:157,163, TvDropdownMenu.kt:136, TvPodcastDetailsScreen.kt:245,251, TvPlaylistDetailsScreen.kt:358, TvUpNextScreen.kt:214, plus the documented TvHomeScreen.kt:106. Effectively every button label in the app. Probably the intent, but the testing instructions should point at those screens — and it's worth deciding whether button labels should be explicit (caption2, matching TvArchivedFilterButton at TvEpisodeListControls.kt:49) rather than inherited.

2. The retry button isn't the only default-styled component (component/TvFeaturedTile.kt:153,159 — outside the diff so I couldn't anchor an inline comment)

Both featured-tile OutlinedButtons pass colors = tileButtonColors(…) but not border, so their border now derives from colorScheme.border = backgroundActive20 (20 % white) instead of the stock tv-material3 dark outline. Add the featured row to the eyeball list.

3. Duplicate values make contentColorFor() order-dependent (inline · theme/TvTheme.kt:53-77)

backgroundOverlay is both secondaryContainer (→ textPrimary) and surfaceVariant (→ textSecondary); backgroundActive fills four slots; backgroundBase fills two. Since contentColorFor matches by value through a fixed when chain, contentColorFor(backgroundOverlay) resolves to whichever slot comes first rather than the one you meant. Filling only the slots you need would avoid this.

4. TvModal's overlay colors became PascalCase composable-getter properties (inline · component/TvModal.kt:121-129)

The one place the migration produced a new idiom instead of a substitution: PascalCase names on @Composable get() properties, a fresh Color allocation per read, and TvOverlayBorderColor next door still a plain val on raw Color.White. Given you already have "promote the overlay colors into TvColorScheme" as a follow-up, doing it now is likely cheaper than landing this shape and revisiting.

5. Locals: silent fallback + dual access path (inline · theme/TvTheme.kt:15-19)

Nothing needs the staticCompositionLocalOf defaults any more (every preview wraps in TvTheme), and a real default turns "forgot the wrapper" into a half-themed subtree rather than a crash. The locals could also be private, and tvColors returning a TvColorScheme is a slight name mismatch.

Optional

TvTheme could own the base background. Around 15 previews still open with Box(modifier = Modifier.background(MaterialTheme.tvColors.backgroundSunken)). An opt-in param (applyBackground: Boolean = false, or a separate TvThemePreview wrapper) would collapse that boilerplate — TvScaffoldContent paints its own gradient, so it can't be unconditional.

Not covered

I couldn't run ./gradlew :tv:compileDebugKotlin or spotlessCheck — Gradle isn't in this environment's allowed commands, so compilation and formatting are unverified here and rest on CI. Everything above is from reading the diff and the surrounding files. No tests were added; per AGENTS.md that's a judgement call, though a small assertion over the TvColorScheme() / TvTypography() token values would cheaply guard against the drift I had to check by hand.
• branch feat/tv-theme-provider-prototype

Comment thread tv/src/main/java/au/com/shiftyjelly/pocketcasts/theme/TvTheme.kt
Comment thread tv/src/main/java/au/com/shiftyjelly/pocketcasts/theme/TvTheme.kt
Comment thread tv/src/main/java/au/com/shiftyjelly/pocketcasts/component/TvModal.kt Outdated
Comment thread tv/src/main/java/au/com/shiftyjelly/pocketcasts/theme/TvTheme.kt Outdated
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @sztomek's task in 4m 55s —— View job


Code review — incremental (new push)

  • Identify what changed since the previous review
  • Re-check the theme infrastructure (TvTheme, TvColorScheme, TvTypography)
  • Verify whether the earlier findings are addressed
  • Look for new issues introduced by the latest commits
  • Post review

One new commit since my last pass: 5491d28 "Keep tv-material3 default label styles so button text matches the previous look" — it drops the labelLarge/labelMedium/labelSmall mappings from toMaterialTypography().

That commit is the right fix, and it fully resolves finding 1

I re-derived it end to end rather than taking the message at face value:

  • Every styleless Text in the module — all 11 I listed, plus TvCreateAccountScreen.kt:97, TvCreateAccountModal.kt:148, TvSignInScreen.kt:134, TvWelcomeScreen.kt:106,112,125 that I'd missed — sits inside a tv-material3 Button/OutlinedButton, which pushes labelLarge over LocalTextStyle. So reverting the three label* slots restores the pre-PR rendering for all of them, including the Home retry button.
  • The remaining bodyLarge = caption1 still sets the app-wide default LocalTextStyle, but no current call site reaches it — nothing outside a button renders styleless. So it's forward-looking only, not a behavior change.
  • I re-confirmed the deliberate typography changes are now exactly the 6 in the description: TvEmptyState.kt, TvHomeScreen.kt, TvPlaylistDetailsScreen.kt (×2), TvPodcastDetailsScreen.kt, TvUpNextScreen.kt — the six MaterialTheme.typography.bodyLarge reads on the base branch, now explicit caption1. git grep on origin/feat/tv-design-system-tokens finds no seventh.
  • No MaterialTheme.typography / MaterialTheme.colorScheme / TvColors. / TvTextStyles references remain anywhere in tv/, and the only tv-material3 component that could inject a style behind your back (Card in TvTile) doesn't — ClassicCard/CompactCard/ListItem/WideButton aren't used. TvTabBar's tab labels set caption1 explicitly.

Also checked 206537d: remember(colors) / remember(typography) key correctly off the @Immutable data classes, and hoisting the defaults to private top-level vals means TvTheme() with default args no longer allocates a new scheme per call — the previews all hit the same instance.

One description update needed: the "Deliberate visual changes" section still says the retry OutlinedButton gets a "caption1 label". After 5491d28 its label is stock labelLarge again; only the 20 %-white border changes. Worth fixing so the eyeball pass looks at the right thing.

Consequence worth a follow-up ticket

Button labels are now the one text class the design system doesn't govern: they render stock tv-material3 labelLarge, while TvEmptyState.kt:73 and TvDownloadAppModal.kt:86 set caption1 on theirs and TvEpisodeListControls.kt:49 sets caption2. That split is pre-existing, not a regression, and "no visual change" is a defensible call for this PR — but it does mean the most common text in the app is off-token. Either mapping labelLarge = caption1 (and dropping the two explicit caption1s) or making every button label explicit would close it; a design decision either way. Fix this →

Still open from the previous pass

# Item Status
2 TvFeaturedTile.kt:153,159 OutlinedButtons pass colors but no border, so their border now derives from colorScheme.border = backgroundActive20 unchanged — add the featured row to the eyeball list
3 Duplicate values across ColorScheme slots make contentColorFor() order-dependent unchanged
4 TvModal.kt:120-129 PascalCase @Composable get() color properties, with TvOverlayBorderColor still a plain val on raw Color.White unchanged
5 Locals still public with real defaults (silent half-themed fallback); tvColors returns a TvColorScheme defaults hoisted in 206537d; visibility + error {} default still open

Refining 3 with the actual lookup order, since my earlier wording overstated it: tv-material3's contentColorFor checks secondaryContainer before surfaceVariant, so contentColorFor(backgroundOverlay) resolves to textPrimary today — which is the sensible answer. The real cost is that surfaceVariant's onSurfaceVariant = textSecondary is simply unreachable, and the correct-today resolution depends on a when order in the library rather than on anything in this file. Filling only the slots you need is still the more durable shape. (Order inferred from the library's source, not read in this checkout.)

None of the above blocks merge in my view — 2 is the one to verify on device.

Not covered

Gradle isn't available in this environment, so :tv:compileDebugKotlin and spotlessCheck are unverified here. The Buildkite build isn't visible from this job either; the three GitHub checks that are visible (Gradle wrapper validation ×2, Danger trigger) are green.
• branch feat/tv-theme-provider-prototype

@sztomek
sztomek force-pushed the feat/tv-theme-provider-prototype branch from 5491d28 to e41cb49 Compare August 5, 2026 18:15
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @sztomek's task in 4m 26s —— View job


Code review — incremental (new push)

  • Identify what changed since the previous review
  • Re-check TvTypography reduction for token drift
  • Verify which earlier findings are addressed
  • Look for new issues
  • Post review

Two new commits, and they close two of the five open findings. Note the base branch also moved under this PR (TvTextStyles on feat/tv-design-system-tokens is now the 9-token ramp, not the 17-entry set my first pass compared against), so I re-derived the drift check from scratch rather than trusting the earlier verification.

792a65a — Reduce TvTypography to the ramp

Touches only TvTypography.kt, and it lands TvTypography exactly on the current base TvTextStyles:

  • Same 9 tokens (title1caption2, including body), same fontSize / lineHeight / fontWeight, same letterSpacing = 0.sp, same includeFontPadding = false. Byte-identical values.
  • The 9 removed entries (videoTilePodcastTitle, featuredTile*, welcome*, signInSubtitle, modalBody) are dead on the new base too — grep finds zero references anywhere in tv/, so nothing needed re-pointing.
  • Restoring the explicit letterSpacing = 0.sp matters more than it looks now that bodyLarge = caption1 is the app-wide LocalTextStyle: an unspecified letterSpacing inherits from the ambient style on merge, so pinning it keeps every token self-contained.

No token drift, confirmed by count. Base: Body 10, Callout 2, Caption1 11, Caption2 19, Headline 9, Title1 5, Title3 8 (+ 5 MaterialTheme.typography.bodyLarge). HEAD: identical, except caption1 11 → 16. That +5 is exactly the 5 bodyLarge reads — TvEmptyState.kt:60, TvHomeScreen.kt:102, TvPlaylistDetailsScreen.kt:224,306, TvPodcastDetailsScreen.kt:367 — now explicit caption1. Those are the deliberate visual changes; there is no sixth. (The description still lists six from the pre-rebase base — worth trimming.)

Colors likewise: every count matches base 1:1, with backgroundSunken 34 → 32 accounted for entirely by the two TvModal overlay definitions moving into TvColorScheme (overlayContainer ×3, translucentOverlayContainer ×1, overlayBorder ×3 all line up with the old constants' call sites).

e41cb49 — Address PR comments

  • Finding 4 resolved. TranslucentContainerColor / TvOverlayContainerColor / TvOverlayBorderColor are gone; the three overlay colors are real TvColorScheme members read through MaterialTheme.tvColors in TvModal, TvDropdownMenu and TvToast. No PascalCase composable getters remain, and hoisting the isTranslucent branch into a val above the Column is cleaner than the inline conditional. ReadOnlyComposable import correctly dropped.
  • Finding 5 resolved. LocalTvColorScheme / LocalTvTypography are now private with error("TvTheme is required"). I checked the crash surface: TvActivity:14-18 is the only setContent in the module and wraps in TvTheme; Dialog/Popup sub-compositions inherit locals; every preview wraps. So the error {} is unreachable in practice and turns a missing wrapper into an immediate failure instead of a half-themed subtree. Good trade.
  • Two small notes on the new TvColorScheme members (inline · theme/TvColorScheme.kt:24-26): the derived defaults silently don't re-derive through copy(), and overlayBorder is pure white rather than 0xFFFBFBFC like every other white in the scheme. Neither is a bug today — one construction site, and the color is carried over verbatim.

Still open

# Item Status
2 The three OutlinedButtons with no explicit border (TvFeaturedTile.kt:153,159, TvHomeScreen.kt:105) now derive theirs from colorScheme.border = backgroundActive20 instead of the stock tv-material3 outline unchanged — the one to eyeball on device
3 Duplicate values across ColorScheme slots (backgroundOverlay as both secondaryContainer and surfaceVariant, etc.) make contentColorFor() resolution depend on the library's when order unchanged (TvTheme.kt:53-77)
bodyLarge = caption1 is still the app-wide default LocalTextStyle, but no call site reaches it (everything styleless is inside a button, which pushes stock labelLarge) — forward-looking only, and button labels remain the one text class the design system doesn't govern as before; a design call, not a regression

Nothing here blocks merge in my view. The two new commits are both strict improvements, and the typography reduction is provably parity-preserving.

Not covered

Gradle isn't available in this environment, so :tv:compileDebugKotlin and spotlessCheck are unverified here and rest on CI. The tv-material3 sources aren't in this checkout either, so the contentColorFor slot order and the stock OutlinedButton border default in findings 2–3 are inferred from the library's API rather than read.
• branch feat/tv-theme-provider-prototype

Comment thread tv/src/main/java/au/com/shiftyjelly/pocketcasts/theme/TvColorScheme.kt Outdated
@sztomek
sztomek force-pushed the feat/tv-design-system-tokens branch from 3809384 to fc3bc34 Compare August 6, 2026 10:52
Base automatically changed from feat/tv-design-system-tokens to main August 6, 2026 12:59
@sztomek
sztomek force-pushed the feat/tv-theme-provider-prototype branch from e41cb49 to aea97e0 Compare August 6, 2026 13:22
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Code review — incremental (new push)

  • Identify what changed since the previous review (rebase vs. new work)
  • Re-verify the theme infrastructure against the new base
  • Check status of the previously-open findings
  • Look for new issues
  • Post review

View job run • branch feat/tv-theme-provider-prototype

@sztomek
sztomek force-pushed the feat/tv-theme-provider-prototype branch from aea97e0 to e871009 Compare August 6, 2026 13:27
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Claude finished @sztomek's task in 4m 29s —— View job


Code review — incremental (new push)

  • Identify what changed in this push
  • Re-check TvTypography against the merged base on main
  • Re-verify colour tokens + new TvScreenBackgroundBrush
  • Check status of previously-open findings
  • Post review

What changed in this push: nothing, in substance

This trigger and my previous pass (13:27 UTC) cover the same tree. The rebase onto 7bf15a7 landed in stages — commit dates run 13:16 → 13:27:53 UTC, with e871009 re-committed 5 s before that review posted — so the synchronize event fired again on the settled head. TvTypography.kt still has the same values at the same line numbers my inline comment anchored to (body block at 59–65), and every other file matches what that pass described. I re-derived the whole verification from scratch rather than assume it.

Re-verified against origin/main (7bf15a7)

Colours: clean 1:1. Every one of the 16 TvColors values carries over byte-identical, and call-site counts match exactly:

textPrimary 46, textSecondary 35, backgroundActive 11, textPrimaryActive 10, backgroundActive20 6, backgroundBase 4, backgroundOverlay 3, textSecondaryActive 2, textPrimary70 2 — all identical to base. backgroundSunken 34 → 32, accounted for entirely by the two old TvModal overlay constants that now derive inside TvColorScheme (overlayContainer ×3 + translucentOverlayContainer ×1 + overlayBorder ×3 line up with the old constants' call sites).

Typography counts: 1:1 except the documented change. caption2 20, body 9, title3 8, headline 8, title1 5, callout 3 — all identical. caption1 11 → 16; the +5 is exactly the five MaterialTheme.typography.bodyLarge reads on base (TvEmptyState.kt:61, TvHomeScreen.kt:122, TvPlaylistDetailsScreen.kt:223,310, TvPodcastDetailsScreen.kt:362) now explicit caption1. No sixth.

No leftovers. Zero TvColors. / TvTextStyles / MaterialTheme.typography. / MaterialTheme.colorScheme. references remain in tv/, and nothing imports androidx.compose.material3.MaterialTheme.

The error("TvTheme is required") locals are safe. All 32 files containing @Preview also contain a TvTheme { wrapper — a 32/32 match — and TvActivity.kt:14 is the module's only setContent. Dialog/Popup sub-compositions inherit locals, so TvModal is covered too.

Open findings

1. TvTypography is behind the merged base — still the one to fix (theme/TvTypography.kt:59-65, and the fontWeight lines throughout)

Unchanged since the last pass; see the inline thread on that file. TvTextStyles as merged in 7bf15a7 differs from what this branch carries:

token main this branch
body 16.67sp / 21.33sp / w500 19.33sp / 24sp / w400
title1callout, caption1, caption2 FontWeight(500) FontWeight(510)
KDoc "Figma's Google Sans family… FontWeight 500 (400 for Subtitle1)" "Figma's SF Pro family… FontWeight 510"

Sizes, line heights, letterSpacing, includeFontPadding and subtitle1 at w400 are otherwise identical, so this is isolated to the ramp. body has visible fallout at 9 call sites (TvToast.kt:101, TvCreateAccountModal.kt:74,138, TvCreateAccountScreen.kt:72,90, TvSignInQrContent.kt:105, TvSignInScreen.kt:126, TvDownloadAppModal.kt:63,77) — ~16 % larger and lighter. 500 → 510 is subtler: on API 28+ FontWeight goes through Typeface.create(family, weight, italic), so with a variable system font it renders marginally heavier; below 28 both sit under the ≥600 bold cutoff and are indistinguishable.

Worth noting for whoever decides: on main, Body and Caption1 are byte-identical (both 16.67/21.33/w500), which is plausibly why an earlier iteration had body at 19.33/w400. So either main's ramp collapsed two Figma steps, or this branch is a stale copy. For a PR that describes itself as pure plumbing ("names preserved, now lowerCamel"), matching main is the safe move — and if main's ramp is wrong, fix it there so both agree. Either way the KDoc wording fix that's on main shouldn't be reverted here. Fix this →

2. TvScreenBackgroundBrush is a PascalCase @Composable get() (theme/TvScreenBackgroundBrush.kt:9-17)

This reintroduces the exact shape that aea97e0 removed from TvModal. On main it was a plain top-level val, so the conversion is new here. PascalCase is reserved for composables that emit UI; the two other new accessors in this PR (tvColors, tvTypography) follow that. It also builds a fresh Brush.horizontalGradient per read instead of the single app-wide instance — LinearGradient implements equals, so Modifier.background won't re-apply spuriously; it's allocation churn at TvDetailOverlay.kt:80 and TvScaffold.kt:124, not a correctness bug. @ReadOnlyComposable rules out remember, so the fix is structural: either a screenBackground: Brush member of TvColorScheme (computed once per scheme, but inherits the copy() caveat below) or @Composable fun tvScreenBackgroundBrush(): Brush.

3. TvColorScheme lost the KDoc that TvColors carried (theme/TvColorScheme.kt:7)

TvTypography kept its equivalent, so this is the one file whose docs didn't come across. The *Active sentence is the only written record of the naming convention, and the "defined even where a token has no call site yet" note is what stops someone pruning textTertiaryActive / textDisabledActive as dead code — they still have zero call sites.

4. Derived defaults don't survive copy() (theme/TvColorScheme.kt:24-26)

TvColorScheme().copy(backgroundSunken = Color.Red) leaves overlayContainer / translucentOverlayContainer / overlayBorder deriving from the old value. Only one construction site today (TvTheme.kt:15), but copy() is the point of a data class, so it's a quiet trap for whoever first themes a variant.

5. Three OutlinedButtons have no explicit border (TvFeaturedTile.kt:153,159, TvHomeScreen.kt:125)

They now derive it from colorScheme.border = backgroundActive20 (20 % white) instead of the stock tv-material3 outline. Still the one to eyeball on device — add the featured row to the testing instructions.

6. Duplicate values make contentColorFor() resolution library-order-dependent (theme/TvTheme.kt:53-77)

backgroundOverlay is both secondaryContainer (→ textPrimary) and surfaceVariant (→ textSecondary); backgroundActive fills four slots; backgroundBase two. It resolves correctly today (tv-material3 checks secondaryContainer first), but onSurfaceVariant = textSecondary is unreachable and correctness rests on a when order in the library. Filling only the slots you need is the more durable shape.

7. HighlightBrush is the last non-tokenized overlay colour (TvModal.kt:125-130)

Raw Color.White.copy(alpha = 0.08f), now sitting next to three tokenized overlay colours. Pre-existing, but it's the obvious remaining candidate.

Assessment

Findings 2 and 3 are new in the rebase, 1 is the substantive one, and 4–7 are polish. Everything mechanical checks out — the colour migration is provably 1:1 and the typography migration would be too once the ramp is re-synced. Nothing here is a merge blocker except 1, which I'd want resolved (or explicitly decided) since it silently changes 9 texts in a PR that presents itself as pure plumbing.

Not covered

Gradle isn't in this environment's allowed commands, so :tv:compileDebugKotlin and spotlessCheck are unverified here and rest on CI. The tv-material3 sources aren't in this checkout, so the contentColorFor slot order and the stock OutlinedButton border default in 5–6 are inferred from the library's API rather than read. I also couldn't query the PR timeline for the previous head SHA, so "no content change in this push" is inferred from file contents and line numbers matching my previous pass, not from a SHA-to-SHA diff.
• branch feat/tv-theme-provider-prototype

@sztomek
sztomek force-pushed the feat/tv-theme-provider-prototype branch from e871009 to ce87594 Compare August 6, 2026 13:38
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Claude finished @sztomek's task in 7m 12s —— View job


Code review — incremental (new push)

  • Identify what changed in this push
  • Re-check TvTypography / TvColorScheme against origin/main
  • Check status of previously-open findings
  • Look for new issues
  • Post review

What changed in this push

One thing, and it's a doc-only edit: TvTypography's KDoc lost two sentences. The block is now 4 lines (TvTypography.kt:9-12) where my previous pass saw 6 — the body block shifted 59-65 → 57-63 accordingly. What's gone is the family/weight sentence and the "full ramp is defined even where a step has no call site yet" note.

Everything else in the tree is byte-for-byte what I reviewed at 13:28 UTC. TvTypography still has body at 19.33/24/w400 and FontWeight(510) on 7 tokens; TvScreenBackgroundBrush is still a PascalCase @Composable get(); TvColorScheme still has no KDoc; TvTheme.kt:53-77 still fills every scheme slot. (Caveat: gh api isn't in this environment's allowed commands and the reflog has no prior head, so "doc-only" is derived from file contents plus the line shift, not a SHA-to-SHA diff.)

Net effect: this makes finding 1 slightly worse rather than better. Removing the sentence removes the only written record of why the ramp uses a synthetic weight, while FontWeight(510) still disagrees with main's FontWeight(500) and body still differs in size and weight. Details and a suggested restore in the inline thread.

Re-verified from scratch against origin/main (7bf15a7)

Colours: clean 1:1. All 16 values byte-identical, and call-site counts match exactly — textPrimary 46, textSecondary 35, backgroundActive 11, textPrimaryActive 10, backgroundActive20 6, backgroundBase 4, backgroundOverlay 3, textSecondaryActive 2, textPrimary70 2. backgroundSunken 34 → 32 is fully accounted for by the two old TvModal constants that now derive inside TvColorScheme (overlayContainer ×3 + translucentOverlayContainer ×1 + overlayBorder ×3 line up with the old call sites).

Typography counts: 1:1 except the documented change. caption2 20, body 9, title3 8, headline 8, title1 5, callout 3 — all identical. caption1 11 → 16, and the +5 is exactly the five MaterialTheme.typography.bodyLarge reads on base now explicit. No sixth.

Structural migrations are clean. TvButtonDefaults, TvTileButtonState.tileButtonColors, TvModal, TvActivity are all 1:1 substitutions — the hoisted containerColor val in TvModalSurface and the multi-line if in tileButtonColors preserve the same branches.

No leftovers. Zero TvColors. / TvTextStyles / MaterialTheme.typography. / MaterialTheme.colorScheme. references in tv/, and nothing imports androidx.compose.material3.MaterialTheme.

The error("TvTheme is required") locals are safe — I checked this per-function this time, not per-file. All 46 @Preview functions across 32 files are covered: 44 have TvTheme { as the first statement, and the two that don't (TvPodcastDetailsScreen.kt:380,391,402 and TvEpisodeActionsModal.kt:185,199,215) delegate to shared helpers that wrap at :416 and :234. Every preview is parameterless, so no default argument can be evaluated outside the wrapper, and no preview declares a theme-reading val above the TvTheme call. TvActivity.kt:14 is the module's only setContent, wrapped at :15. Dialog/Popup sub-compositions inherit locals.

Spotless won't fail on the PascalCase accessor. build.gradle.kts:139-177 enables the compose ruleset with only compositionlocal-allowlist disabled, and PascalCase top-level vals already pass in this module (HighlightBrush, ModalShape, DefaultModalWidth), so finding 2 is a convention point, not a CI break.

Open findings

# Item Where Status
1 Ramp values behind the merged base: body 19.33/24/w400 vs main's 16.67/21.33/w500; FontWeight(510) vs 500 on 7 tokens; KDoc now silent on both theme/TvTypography.kt:9-12, :57-63 worse this push — doc removed, values unchanged
2 TvScreenBackgroundBrush is a PascalCase @Composable get() building a fresh Brush per read; was a plain top-level val on main theme/TvScreenBackgroundBrush.kt:9-17 unchanged
3 TvColorScheme lost the KDoc TvColors carried — including the *Active naming convention and the "defined even where unused" note. Six fields have zero call sites: textTertiary, textDisabled, textTertiaryActive, textDisabledActive, backgroundSurface, backgroundActive50 theme/TvColorScheme.kt:7 unchanged, and now mirrored in TvTypography
4 Derived defaults don't survive copy()TvColorScheme().copy(backgroundSunken = X) leaves the three overlay colours deriving from the old value theme/TvColorScheme.kt:24-26 unchanged
5 Three OutlinedButtons with no explicit border now derive it from colorScheme.border = backgroundActive20 instead of the stock tv-material3 outline TvFeaturedTile.kt:153,159, TvHomeScreen.kt:125 unchanged — the one to eyeball on device
6 Duplicate values across scheme slots make contentColorFor() resolution depend on the library's when order; onSurfaceVariant = textSecondary is unreachable theme/TvTheme.kt:53-77 unchanged
7 HighlightBrush is the last non-tokenized overlay colour — raw Color.White.copy(alpha = 0.08f) next to three tokenized ones TvModal.kt:125-130 unchanged

Also still true, and worth a follow-up ticket rather than a change here: bodyLarge = caption1 sets the app-wide default LocalTextStyle, but no call site reaches it — everything styleless sits inside a button, which pushes stock labelLarge. So button labels remain the one text class the design system doesn't govern.

Assessment

The mechanical migration is provably parity-preserving on colours, and would be on typography too once the ramp is re-synced. Finding 1 is the only one I'd treat as blocking: a PR that describes itself as "names preserved, now lowerCamel" silently changes 9 texts, and this push removed the doc that was the last hint the divergence was deliberate. Either re-sync with main or split the revalue into its own commit with the reasoning stated. 2-7 are polish.

Not covered

Gradle isn't in this environment's allowed commands, so :tv:compileDebugKotlin and spotlessCheck are unverified here and rest on CI. gh api isn't available either, so I couldn't diff this push against the previous head by SHA — the "doc-only change" conclusion is inferred from file contents and line offsets. The tv-material3 sources aren't in this checkout, so the contentColorFor slot order and the stock OutlinedButton border default in 5-6 are inferred from the library's API rather than read.
• branch feat/tv-theme-provider-prototype

@sztomek
sztomek merged commit a2ee0ac into main Aug 6, 2026
21 checks passed
@sztomek
sztomek deleted the feat/tv-theme-provider-prototype branch August 6, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants