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:
Kgothatso Ngako
2026-09-08 00:17:41 +02:00
parent 21d57eba54
commit 52b57769e1
5 changed files with 161 additions and 32 deletions

View File

@@ -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`