fix(#640): clamp dark Material You surface family to static Slate ramp#642
Conversation
When Dark mode + Material You is enabled on Android 12+, dynamicDarkColorScheme
returns a wallpaper-derived dark palette whose surface / surfaceContainerHigh /
surfaceContainerHighest roles can resolve to high-luminance values for some
wallpapers. The Routines screen reads these directly via MaterialTheme.colorScheme:
- top app bar = surface (EnhancedMainScreen.kt:335)
- bottom nav = surfaceContainerHigh (EnhancedMainScreen.kt:500)
- expanded RoutineCard = surfaceContainerHighest (RoutinesTab.kt:1083)
at elevation 8.dp (flickers
to light vs collapsed 2.dp)
Meanwhile surfaceContainerLowest stays dark, so the gradient anchors dark and
the screen reads half-light/half-dark on the same scheme. The per-card
expand/collapse flicker is the elevation tint amplifying that divergence.
Fix: when dynamicColorEnabled=true and the resolved scheme is the dynamic dark
scheme, route it through clampDynamicDarkScheme(dynamic, DarkColorScheme), which
returns a ColorScheme whose surface family comes from the static Slate ramp
(Slate950/900/800/700) while primary / secondary / tertiary / error / outline /
surfaceVariant / background keep the wallpaper hue. Light + Material You and
Material You = OFF are unchanged; iOS never invokes the helper.
Tests:
- ClampedDynamicDarkSchemeTest (9): boundary luminance 0/0.49/0.5/0.51/1.0,
surface family swap, accent preservation, no-mutation, ≤ 6% adjacent shift
- MaterialYouDarkClampContractTest (4): Theme.kt wires the clamp only on the
dark branch; helper co-located with Theme.kt; DarkColorScheme unmodified
- RoutinesChromeLuminanceContractTest (4): TopAppBar/BottomNav/RoutineCard
continue to read from MaterialTheme.colorScheme (so the clamp propagates);
no hardcoded Color.White containerColor
- RoutineCardElevationContractTest (2): CardDefaults.cardElevation with
if (expanded) 8.dp else 2.dp preserved so the elevation-tint shift is bounded
Closes the Routines screen: expanded card flickers to light when Dark + Material
You is enabled (issue #640).
Refs: rca.md in ~/.hermes/phoenix-bug-recreations/640/rca.md
There was a problem hiding this comment.
Code Review
This pull request addresses issue #640 by introducing a clampDynamicDarkScheme helper to prevent high-luminance wallpaper colors from leaking into dark-mode surfaces, along with several contract and unit tests to verify the behavior. Feedback suggests clamping inverseSurface and inverseOnSurface as well to avoid accessibility and contrast issues with components like Snackbars. Additionally, the reviewer advises against reading physical source files in contract tests due to environment and refactoring fragility, recommending static analysis tools instead.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Code Review Roast 🔥Verdict: No Issues Found | Recommendation: Merge Oh wait, this increment is actually clean too. Two for two. Either the author is listening, or they've installed a code-review-avoidance script that strips out anything I might roast. I'll take it. Overview
No new findings on changed code in this increment (commit Re-verification of previous findings
No other prior findings touch changed lines in this increment. What the increment does (correctness check)
Ponytail Review
Ponytail net: 0 lines (this increment is itself the smallest possible diff to extend the surface-family clamp — 1 production line, 2 doc-comment paragraphs, 2 source-grep test lines, 1 test fixture field, 2 assertions, 1 boundary test extension, 0 new abstractions, 0 new imports). Suggested Minimal PatchNo patch needed. The prior Final Merge GuidanceCan merge as-is. This increment is the minimum viable diff to close the surfaceTint leak: one production-code line plus the doc/test contract to prove it stays closed. The only optional carry-forward is the boundary-test asymmetry, which has been on the optional-cleanup pile since before this PR and is not made materially worse by shipping this increment. 🏆 Best part: The 💀 Worst part: The doc comment on 📊 Overall: Like a clean compile after a refactor — boring in the best possible way. Same shape as the previous increment: small, targeted, no scope creep, no new helpers, no new dependencies. The fact that the prior review's Files Reviewed (3 files in this increment)
Reviewed by minimax-m3 · incremental review of 1 commit · Fix these issues in Kilo Cloud Previous Review Summaries (6 snapshots, latest commit 39a3a35)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 39a3a35)Verdict: No Issues Found | Recommendation: Merge Oh wait, this PR is actually clean now. I had my flamethrower warmed up and everything, and the author went and deleted the exact two things I asked them to delete. That's either growth or surrender. I'll take it. Overview
No new findings on changed code in this increment (commit Re-verification of previous findings
Both fixes are exactly the shape the previous review asked for: pure deletion, no new imports, no new test scaffolding, no new abstractions introduced to fix the old ones. Ponytail Review
Ponytail net: 0 lines (this increment is the Ponytail — 16 lines of redundant test code deleted, no new code added). Suggested Minimal PatchNo patch needed. Both prior findings are correctly addressed. Final Merge GuidanceCan merge as-is. This increment is the smallest possible diff to close the previous review's two findings: delete the redundant test, delete the redundant assertion, delete the now-orphan 🏆 Best part: The two cleanups are surgical. No "well actually let me also…" follow-on edits, no scope creep, no new helpers to manage the old helpers. Just 💀 Worst part: 📊 Overall: Like the third pancake — the previous two were over- or under-done, this one's the right shape. Same batter, no extra ingredients, no missing ingredients. Eat it now. Files Reviewed (2 files in this increment)
Reviewed by minimax-m3 · incremental review of 1 commit Previous review (commit 8dd6985)Verdict: Approve with optional cleanup | Recommendation: Merge after the two redundancies are removed (or leave them — they're harmless). Overview
Issue Details (click to expand)
Correctness / Safety Findingsnitpick: suggestion: Ponytail Review
Ponytail net: ~-22 lines if all Ponytail suggestions are taken (delete Suggested Minimal PatchOptional cleanup:
Final Merge GuidanceCan merge. The helper-extraction approach to retire the source-scanning tests is a legitimate answer to the prior "fragile .kt-string-matching" concern (the previous reviewer declined tightening the regex; you took the other exit and made the call sites addressable). The two findings are dead-code redundancies, not correctness bugs. Ship if you don't want to touch it; the optional cleanup takes 2 minutes if you do. 🏆 Best part: The contract tests now read like behavior assertions instead of regex prisoners. 💀 Worst part: 📊 Overall: Like a fourth pancake — slightly overworked compared to the third, but the ingredients are sound and the stack is consistent. Eat it before it gets cold. Files Reviewed (5 files in this increment)
Reviewed by minimax-m3 · Input: ~30K · Output: ~4K Previous review (commit baea6ab)Verdict: Approve | Recommendation: Both prior findings resolved cleanly. Ship. Overview
Correctness / Safety FindingsNo correctness or safety findings on changed code. Ponytail ReviewPonytail: Lean already. Ship. Ponytail net: 0 lines (this increment is the Ponytail — -7 lines net). Suggested Minimal PatchNo patch needed. Final Merge GuidanceCan merge. The previous nitpick (partial 4-token enumeration in the second KDoc paragraph) and suggestion (3rd/4th-copy 🏆 Best part: The author actually listened. Two findings, two clean fixes, net -7 lines, and the new comment at 💀 Worst part: Honestly, this is one of those annoying increments where the diff is so small and so on-point that roasting it would be picking a fight with a parking meter. The most roastable thing here is that the precondition assert was reduced from 5 conditions to 3 in 📊 Overall: Like the second pancake — same batter, better shape. The conservative clamp, the Snackbar rationale, and the test composition are all in agreement now, and the file is shorter for it. Ship before someone "improves" it. Files Reviewed (2 files in this increment)
Reviewed by minimax-m3 · Input: ~25K · Output: ~3K Previous review (commit 3098c57)Verdict: Approve | Recommendation: Gemini high-priority concern correctly resolved; one optional Ponytail cleanup and one KDoc polish. Overview
Issue Details (click to expand)
🏆 Best part: The Gemini high-priority concern (Snackbar dark-on-dark via 💀 Worst part: The 📊 Overall: The increment does what the Gemini review asked for — extends the conservative clamp to Files Reviewed (3 files in this increment)
Fix these issues in Kilo Cloud Previous review (commit 478cc6f)Verdict: Can Merge | Recommendation: Previous warning resolved; one Ponytail cleanup note. Overview
No new correctness or safety findings on changed code. 🏆 Best part: The KDoc on 💀 Worst part: The new regression test is doing the same job twice. The first test ( 📊 Overall: The increment does exactly what the previous review asked for — reconciles docs, code, and a regression test, all on the dark-branch only. The fix can land; one optional collapse will keep the test file from sprawling. Files Reviewed (2 changed in this increment)
Correctness / Safety FindingsNo correctness or safety findings on changed code. Ponytail Review
Ponytail net: ~35 lines removable. Suggested Minimal Patch
Final Merge GuidanceCan merge. The previous warning is properly addressed (KDoc reconciled with the conservative implementation, regression test added — redundantly). The Ponytail note is optional cleanup; it does not block merge. Fix these issues in Kilo Cloud Previous review (commit cc5beb1)Verdict: 3 Issues Found | Recommendation: Address the warning before merge; suggestions are optional cleanup. Overview
Issue Details (click to expand)
🏆 Best part: The actual fix in 💀 Worst part: The acceptance criteria in the PR description promise wallpaper hue on 📊 Overall: The fix is correct and the helper is well-scoped, but the PR is selling wallpaper hue it isn't shipping. Reconcile the criteria, the KDoc, and the code — and trim the brittle source-grep contract tests before they become a maintenance tax the next reviewer has to pay. Files Reviewed (6 files)
Correctness / Safety Findings
[Snapshot truncated.] Additional previous summary content was truncated to keep this comment within platform limits. Reviewed by minimax-m3 · Input: 33.1K · Output: 13.1K · Cached: 198.8K |
Kilo Code Review flagged that the KDoc + acceptance list claimed
surfaceVariant / onSurfaceVariant / surfaceDim / surfaceBright /
background / onBackground all come from the dynamic scheme, while the
implementation correctly routes them to fallback (per the RCA
fix_direction). The code matches the reporter's observed bug surface
(those tokens are part of the same surface family that wallpaper-derived
palettes miscalibrate), so the liar was the doc, not the code.
Changes:
- ClampedDynamicDarkScheme.kt: rewrite KDoc to explicitly justify the
conservative clamp: the surface family is clamped to fallback as a
unit because wallpaper miscalibration can hit any of those tokens;
only the user-interactive accents (primary/secondary/tertiary/error/
outline/outlineVariant) flow through from dynamic.
- ClampedDynamicDarkSchemeTest.kt: add a regression test pinning the
conservative clamp on surfaceVariant/onSurfaceVariant/background/
onBackground/surfaceDim/surfaceBright so future drift is caught.
Refs: PR #642 Kilo Code Review (warning on lines ClampedDynamicDarkScheme.kt:24-26).
|
Thanks for the thorough review — addressing the warning on You were right: the code correctly routes
The code itself was correct and matches the RCA's On the two suggestions (brittle source-grep contract tests + narrow CI will re-run automatically with the new commit (force-push avoided — just a follow-up commit on the same branch). |
…dark-on-dark PR #642 review (Gemini Code Assist, high priority) flagged that Material 3 components like Snackbar read inverseSurface for their container color and inverseOnSurface for content. The conservative clamp originally stopped at background/onBackground, leaving inverseSurface dynamic — meaning a wallpaper that produces a light 'surface' would simultaneously produce a dark inverseSurface, and Snackbar would render dark-on-dark against the just-clamped dark screen. The exact same bug class #640 is trying to tame. This commit extends the conservative clamp to also pin inverseSurface and inverseOnSurface from fallback. The justification is the same as for the other surface family tokens: wallpaper-derived tonal palettes miscalibrate inverseSurface for some wallpapers, and Snackbar / Tooltip / similar components render in user-facing surfaces that must remain readable. Changes: - ClampedDynamicDarkScheme.kt: add inverseSurface / inverseOnSurface to the .copy(...) composition from fallback. KDoc updated to spell out the Snackbar dark-on-dark failure mode that this fixes. - ClampedDynamicDarkSchemeTest.kt: - extend the high-luminance dynamic fixture with explicit non-default inverseSurface / inverseOnSurface so the clamp assertion is meaningful - extend the conservative-clamp regression test to assert inverseSurface / inverseOnSurface == fallback - extend the broader surface-family equality test to assert inverseSurface / inverseOnSurface == fallback - fix the file-leading KDoc (was still listing surfaceVariant / background as 'from dynamic', contradicting the helper KDoc that commit 478cc6f had already reconciled) - extend the precondition assert to require the dynamic fixture differs from fallback on inverseSurface / inverseOnSurface too - MaterialYouDarkClampContractTest.kt: pin the new composition lines so a future refactor cannot drop inverseSurface / inverseOnSurface from the clamp without breaking a contract test. Verified: ClampedDynamicDarkSchemeTest (10/10), MaterialYouDarkClampContractTest (5/5 incl. new clampHelper_clampInverseSurfaceFromFallback), RoutinesChromeLuminanceContractTest (4/4), RoutineCardElevationContractTest (2/2) all green locally via :shared:testAndroidHostTest.
…ken list Follow-up to commit 3098c57 addressing two Kilo Code Review nitpicks posted on the new clamp composition: 1. **Dedupe clamp assertions.** The conservative-clamp regression test added the inverseSurface / inverseOnSurface assertions alongside the new composition lines, but the broader 'clamped surface family equals the static dark fallback surface' test (which I also extended in 3098c57) already covers the same 15 tokens including inverseSurface / inverseOnSurface. Keeping both copies is a duplicate clamp check. Drop the assertions from the conservative-clamp test, leave a comment pointing at the broader test, and revert the precondition assert to the pre-3098c573 shape (no longer needs inverse tokens to differ). 2. **Reconcile KDoc token list.** The helper KDoc first paragraph enumerates 9 clamped tokens (incl. the inverse pair), but the second paragraph 'Why not pass these through from dynamic?' only listed 4. Rewrite the second paragraph to talk about the family as a whole ('Why not pass any of those tokens through from dynamic? They are all part of the same wallpaper-miscalibrated surface family.') so the rationale matches the enumeration width without spelling out the same long list twice. Verified: theme/chrome tests (10 ClampedDynamicDarkSchemeTest + 5 contract + 4 luminance + 2 elevation + 3 design system + 3 theme mode + 2 design token) all green via :shared:testAndroidHostTest. The composition pin in 'MaterialYouDarkClampContractTest.clampHelper_clampInverseSurfaceFromFallback' is unchanged and continues to enforce the new clamp lines.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 39a3a35e15
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Summary
Fixes #640 — Routines screen: expanded card flickers to light when Dark + Material You is enabled.
On Android 12+ with Dark mode + Material You,
dynamicDarkColorScheme(context)returns a wallpaper-derived dark palette whosesurface/surfaceContainerHigh/surfaceContainerHighestroles can resolve to high-luminance values for some wallpapers. The Routines screen reads these directly viaMaterialTheme.colorScheme:MaterialTheme.colorScheme.surfaceMaterialTheme.colorScheme.surfaceContainerHighRoutineCard(8.dp elevation)MaterialTheme.colorScheme.surfaceContainerHighestMeanwhile
surfaceContainerLoweststays dark, so the gradient anchors dark and the screen reads half-light/half-dark on the same scheme. The per-card expand/collapse flicker is thesurfaceColorAtElevationtint amplifying that divergence.Fix
New
clampDynamicDarkScheme(dynamic, fallback)helper inshared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/ClampedDynamicDarkScheme.ktreturns aColorSchemewhose:surface/onSurface/surfaceContainerLowest..Highest/surfaceVariant/onSurfaceVariant/surfaceDim/surfaceBright/background/onBackgroundall come fromfallback(the staticDarkColorSchemeSlate950/900/800/700 ramp). These tokens are clamped as a unit because they are part of the same wallpaper-miscalibrated surface family that the reporter saw flip light — a wallpaper that produces high-luminancesurfaceContainerHighestcan just as easily produce high-luminancesurfaceVariantorbackground.primary/secondary/tertiary/error/outline/outlineVariantcome fromdynamic, so the wallpaper hue still flows through toggles, sliders, selection chips, badges, and error states — the user-interactive accents that the user expects to be wallpaper-colored.Wired into
Theme.kton the dark + dynamic branch only:Light + Material You and Material You = OFF are unchanged; iOS never invokes the helper (its
platformDynamicColorSchemereturnsnull).Acceptance criteria (RCA for #640, §5)
RoutineCard, collapsedRoutineCardall render with the staticDarkColorSchemesurface family.surfaceContainerLowest).primary/secondary/tertiary/error/outline/outlineVariantcome from the dynamic scheme (wallpaper hue preserved on user-interactive accents).surface/onSurface/surfaceVariant/onSurfaceVariant/surfaceContainerLowest..Highest/surfaceDim/surfaceBright/background/onBackgroundcome from the staticDarkColorScheme(conservative clamp on the surface family as a unit).DarkColorScheme).ThemeViewModelTest,ThemeModeUiContractGuardTest,ThemeToggleTest,ColorSchemeTest,DesignSystemRatchetTest) pass unmodified.Tests added (20 new, all green, full suite 2409/2409 passing)
ClampedDynamicDarkSchemeTest(10 cases,commonTest): boundary luminance 0/0.49/0.5/0.51/1.0, surface family equalsDarkColorScheme, conservative-clamp regression onsurfaceVariant/onSurfaceVariant/background/onBackground/surfaceDim/surfaceBright, accent preservation, no-mutation contract, ≤ 6% adjacent-shift invariant, identity-when-equal.MaterialYouDarkClampContractTest(4 cases,androidHostTest):Theme.ktinvokes the clamp on the dark branch only; helper co-located withTheme.kt;DarkColorSchemeunmodified.RoutinesChromeLuminanceContractTest(4 cases,androidHostTest):TopAppBar/PhoenixBottomNavigationBar/RoutineCardcontinue to read fromMaterialTheme.colorScheme.*; no hardcodedColor.WhitecontainerColorcrept into the chrome.RoutineCardElevationContractTest(2 cases,androidHostTest):Card.defaultElevation = if (expanded) 8.dp else 2.dppreserved so the elevation tint shift stays bounded.Compose UI tests / Robolectric would add equivalent coverage at the rendered-tree level but are not configured by this repo today; the helper-level coverage plus the read-site contract tests close the same loop with zero new build dependencies.
Files changed
Files NOT changed (per RCA non-goals)
EnhancedMainScreen.kt,RoutinesTab.kt,RoutineGroupHeader.kt— chrome reads fromMaterialTheme.colorScheme.*; the fix propagates through the theme.ThemeViewModel.kt— no theme persistence / race changes.androidApp/.../AndroidTheme.kt— deprecated, unreachable fromApp.kt.shared/src/iosMain/.DarkColorScheme/LightColorSchemedefinitions — they remain the brand contract for iOS / non-dynamic Android.Fixes #640