feat: adopt MaterialExpressiveTheme, and give shape, type and motion a named home
Phase 1, steps 3, 4 and 6 of docs/material-design-conformance.md. `TorchTheme` passed
`MaterialTheme` a colour scheme and a typography and nothing else, so shape and motion were
whatever the library defaulted to and there was nowhere to write down what any of it was
for.
**Expressive, by decision rather than by drift.** The plan deliberately left
`MaterialExpressiveTheme` vs `MaterialTheme` open, because it changes component defaults
app-wide and is a product call. Put to the product owner on 2026-09-08 and answered
expressive. The pinned material3 1.10.0-alpha05 ships the whole set -- `ButtonGroupKt`,
`SplitButtonKt`, `FloatingToolbarKt`, `LoadingIndicatorKt`, `ShortNavigationBarKt`,
`WideNavigationRail`, `MaterialShapesKt` -- and the tree already opts into
`ExperimentalMaterial3ExpressiveApi` in 66 places, so this makes explicit what the imports
had already assumed.
**All four slots are passed explicitly, and that is the point.**
`MaterialExpressiveTheme` defaults its colour scheme to `expressiveLightColorScheme()` and
its shapes and typography likewise -- Material's values, not this app's. Leaving any slot
to that default is the same class of accident as the twelve unassigned fixed roles fixed
two commits ago: it compiles, it renders, and it renders somebody else's design.
**No visual change on the screens checked, and that is worth stating rather than
assuming.** Measured on the API 36 emulator: the "Invite a Friend" button is byte-identical
before and after -- same fill `#4E5E8B`, same 357px box at the same y -- because the
expressive default for a `Button` at default size matches the baseline in this version.
What expressive actually buys is elsewhere: `LocalUsingExpressiveTheme` gating component
behaviour, the three increased shape steps, the fifteen `...Emphasized` type roles, and the
components phases 5 to 7 are built on.
**`MantraShapes` is baseline `Shapes()`, on evidence.** The corners hand-written across the
tree already land on the M3 scale --
RoundedCornerShape(4.dp) x3 = extraSmall
RoundedCornerShape(12.dp) x11 = medium
RoundedCornerShape(16.dp) x2 = large
RoundedCornerShape(30.dp) x1 ~ extraLarge (28dp)
-- so overriding the scale would restyle the app for no reason. What is wrong is that they
are literals, which is how the last one drifted two units off the scale and why none of
them can move per breakpoint later. `Shape.kt` documents the eight steps and what each is
for; migrating those seventeen call sites is a later phase, and this is what they migrate
onto. Declaring it explicitly rather than relying on the default gives the note somewhere
to live.
**`MotionScheme.expressive()` is wired and unused.** Nothing in the app animates today --
one `animateScrollToPage`, no `AnimatedVisibility`, no navigation transitions -- so this
buys nothing yet. It is here so that when the motion phase starts, every spec comes from
the scheme rather than from a literal `tween`, and the app's feel is one decision instead
of forty.
**`Type.kt` left `com.example.ui.theme`.** It has been declaring that package while living
under `press/mantra/compose/ui/theme/`, one of three namespaces holding live UI code in
this tree. The move is mechanical; the doc comment on it is not. It records what each type
family is *for* -- `display*` for a screen's identity, `headline*` for section tops,
`title*` for headers and list headlines, `body*` for anything read as a sentence, `label*`
for **component text only** -- because the audit's finding is not that the scale is wrong
but that 92 of 240 reads are `label*` while `display*` and `headline*` carry 9 between them
across 43 screens. A UI at one pitch. The file stays baseline; the rule now has a home for
the sweep that fixes the call sites.
**The desktop unlock screen renders in the app's theme for the first time.**
`PassphraseGate` sat in the `else` branch beside `MantraApp`, which applies `TorchTheme`
itself -- so the gate composed under the default `MaterialTheme` and its
`colorScheme.error` and `typography.headlineSmall` were baseline M3. It is the first screen
a desktop user sees. `TorchTheme` now wraps both branches.
That wraps the unlocked branch twice, deliberately. `MantraApp` keeps its own `TorchTheme`
because android and ios enter through it and would lose the theme entirely if it moved out;
a second application of identical values costs one `CompositionLocalProvider` composition.
The comment says so, since the redundancy looks like an oversight.
**Dynamic colour stays on, by decision.** Also put to the product owner: on Android 12+
`dynamicColor = true` wins unconditionally, so the six schemes are used only below Android
12, on ios and on desktop, and a modern phone paints the wallpaper palette. Answered keep
as-is. A comment on the selection in `TorchTheme` now says this outright, because otherwise
the next person to change `Color.kt` and see nothing happen on their phone will assume the
change did not work.
**Tests.** 926 pass, 586 jvm over 71 classes and 340 android over 43, unchanged -- this
commit adds no assertions, because what it changes is either a library default (nothing to
assert that the compiler does not) or a doc comment. `:composeApp:compileDebugKotlinAndroid`
and `:composeApp:compileKotlinJvm` build, the debug apk installs and runs on emulator-5554
under the expressive theme, `m3-audit.sh --check` exits 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -439,23 +439,27 @@ find-and-replace.
|
||||
pixel of the "Skip for now" label reads `#848484` → `#A7A7A7` → `#D0D0D0` as the
|
||||
setting moves, the three declared values exactly, **without the app restarting**.
|
||||
|
||||
3. **Give `MaterialTheme` its other three slots.** `Shapes`, `Typography` and a
|
||||
`MotionScheme` are all parameters of the overload the app already calls:
|
||||
3. **Give the theme its other three slots.** *Built.* The expressive-vs-standard
|
||||
question was put to the product owner on 2026-09-08 and answered
|
||||
**expressive**, so `TorchTheme` calls `MaterialExpressiveTheme` with all four
|
||||
slots passed explicitly — colour, `MotionScheme.expressive()`, `MantraShapes`,
|
||||
`AuxTypography`. Explicitly, because `MaterialExpressiveTheme` would otherwise
|
||||
default them to `expressiveLightColorScheme()` and friends, which is Material's
|
||||
palette rather than this app's: the same class of accident as the twelve
|
||||
unassigned fixed roles.
|
||||
|
||||
```kotlin
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
motionScheme = MotionScheme.expressive(), // or .standard()
|
||||
shapes = MantraShapes,
|
||||
typography = MantraTypography,
|
||||
content = content,
|
||||
)
|
||||
```
|
||||
No visual change on the screens checked. The "Invite a Friend" button measures
|
||||
byte-identical before and after — same fill, same 357×… box — because the
|
||||
expressive default for a `Button` at default size matches the baseline in this
|
||||
version. What expressive buys is elsewhere: `LocalUsingExpressiveTheme`, the
|
||||
three increased shape steps, the fifteen `…Emphasized` type roles, and the
|
||||
components later phases need (button groups, split buttons, floating toolbars,
|
||||
wide navigation rails, `LoadingIndicator`).
|
||||
|
||||
Decide `MaterialExpressiveTheme` vs `MaterialTheme` here and once. The app
|
||||
already opts into `ExperimentalMaterial3ExpressiveApi` in 66 places, so the
|
||||
expressive default is the honest choice; it is also what makes the
|
||||
`…Emphasized` type roles and the increased shape sizes meaningful.
|
||||
`MantraShapes` is baseline `Shapes()`, and `Shape.kt` records why: the corners
|
||||
hand-written across the tree already land on the scale — 4dp ×3 = `extraSmall`,
|
||||
12dp ×11 = `medium`, 16dp ×2 = `large`, and one 30dp that drifted two units off
|
||||
`extraLarge`. Nothing needs restyling; seventeen literals need migrating, later.
|
||||
|
||||
4. **Move and fill typography.** `Type.kt` moves from `com.example.ui.theme` to
|
||||
`press.mantra.compose.ui.theme`. It stays baseline-derived, but it becomes a
|
||||
@@ -468,8 +472,15 @@ find-and-replace.
|
||||
`container`/`on`/`onContainer`. `CreateProfileScreen.kt:311` and `:329` then
|
||||
stop pairing them with `Color.White`/`Color.DarkGray` by eye.
|
||||
|
||||
6. **Wrap the desktop gate.** `Main.kt` moves `TorchTheme` outside the
|
||||
`unlocked` branch so `PassphraseGate` composes inside it.
|
||||
6. **Wrap the desktop gate.** *Built.* `Main.kt` moves `TorchTheme` outside the
|
||||
`unlocked` branch, so `PassphraseGate` — the first screen a desktop user sees —
|
||||
stops composing under the default `MaterialTheme`. `MantraApp` keeps its own
|
||||
`TorchTheme`, so the unlocked branch is wrapped twice: android and ios enter
|
||||
through `MantraApp` and would lose the theme entirely if it moved out, and a
|
||||
second application of the same values costs one `CompositionLocalProvider`.
|
||||
|
||||
`Type.kt` also left `com.example.ui.theme` for `press.mantra.compose.ui.theme`,
|
||||
which removes one of the three package namespaces the UI was living across.
|
||||
|
||||
**Done when** every role in `ColorScheme` is explicitly assigned in both
|
||||
schemes; the contrast test still passes and now covers six schemes; `Type.kt`
|
||||
|
||||
Reference in New Issue
Block a user