docs(android): M12 phase 2 as landed

The radii-and-shadow phase, shipped as Android-app#35. Seven notes, of which
three are amendments to the spec rather than records of it.

Section 5.4's elevation map is not a no-op and now says so, with section 2 and
AC-1 amended to match. Material3's filled Card is Level0 and FeatureCard drew
none of the shadow its own KDoc claimed, so the app has been flat since M5 -
while the runic-gateway preset it was drawn from selects the "Default" shadow.
The org lead chose to apply the map as written rather than rebase it on the flat
baseline, because rebasing would have collapsed none/Soft/Default onto 0dp and
left only Deep doing anything on the phone. The radius half is untouched by this
and remains a provable no-op.

Section 5.4's exact-string match is corrected to a nearest-blur match. The
fantasy preset's own --shadow-card is not one of SHADOW_OPTIONS' four values,
because resolveThemeTokens copies a preset's tokens verbatim and they never pass
through the admin form's dropdown.

Section 5.2 gains the rule the pill needed: CircleShape is a percentage, so it
has no shipped dp for a ratio to scale and the resolved px is taken as dp below
the 500px floor. It is a literal because there is no app scale to preserve, not
as an exception to the ratio rule.

Also recorded: why all 24 Card( call sites became ShardCard( (Material takes
elevation as a default argument, not from the theme), that --radius-pill reaches
only StatusPill, and that Shapes - unlike ColorScheme - does implement equals,
so the structural no-op proof is one assertion rather than a reflection walk.

PLAN.md 9's M12 entry records phase 2 landed.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-08 05:15:21 -05:00
parent afe0e9245f
commit 1746ad4e37
2 changed files with 84 additions and 5 deletions

View File

@@ -1045,7 +1045,12 @@ push, and Play (M6M8) follow the designed app.
two — see THEMING_AND_NAV.md "Phase 0 as landed". **Phase 1 landed 2026-08-08**
(Android-app#34 / docs#113): the fifteen color tokens now resolve into a `ShardPalette` that
feeds both the Material scheme and a `LocalShardPalette`, and the no-op invariant is a test —
the shipped palette reproduces the pre-M12 `ColorScheme` role for role.
the shipped palette reproduces the pre-M12 `ColorScheme` role for role. **Phase 2 landed
2026-08-08** (Android-app#35 / docs#114): the four radii scale the app's own dp by their ratio
to the `runic-gateway` baseline, and `--shadow-card` maps onto card elevation. The shadow is
the milestone's **one deliberate change to an untouched instance** — the app has been flat
since M5 while the preset it was drawn from selects a shadow — and is why every `Card(` became
a `ShardCard(`: Material takes elevation as a default argument, not from the theme.
### Deferred (not a milestone)

View File

@@ -45,6 +45,13 @@ shipped value. A bad `--accent` must not discard a good `--bg` beside it, and a
settings call that fails is the same state as "no overrides" — never an error
screen, never a half-painted theme.
**The one sanctioned exception is card depth (§5.4).** The app has been flat
since M5 while the preset it was drawn from selects a shadow, so applying the
shadow map as specified gives an untouched instance a depth it did not have.
Approved by the org lead in phase 2 rather than rebased onto the flat baseline,
because the alternative left three of the admin's four choices doing nothing on
the phone. Radii and colors are unaffected: both are still provable no-ops.
## 3. What the server already publishes
No backend work. Everything below is live on `website/main` today.
@@ -193,6 +200,13 @@ it. Consequences, all of them wanted:
Round to whole dp and clamp at 0.
**The pill is the one field resolved as a literal**, settled in phase 2:
`CircleShape` is a *percentage*, so it has no shipped dp for a ratio to scale
against and the rule above has nothing to compute. Below the 500px floor the
resolved px is taken as dp directly — Fantasy's 4px → 4dp, Modern's 8px → 8dp —
which reads the same on a ~22dp chip as it does on the web. It is a literal
because there is no app scale to preserve here, not as an exception to the rule.
### 5.3 Fonts — bundled, mapped by first family
The shortlist is 12 options across three roles, spanning **eight** families:
@@ -253,8 +267,19 @@ it maps to card elevation:
| `0 14px 34px rgba(0,0,0,0.3)` (Default) | 4dp |
| `0 18px 44px rgba(0,0,0,0.45)` (Deep) | 8dp |
Matched by exact string against the server's `SHADOW_OPTIONS`; anything else is
the shipped default. Applied to `FeatureCard` and the Material `Card` defaults.
~~Matched by exact string against the server's `SHADOW_OPTIONS`~~ — **matched by
nearest blur**, corrected in phase 2. The `fantasy` preset publishes
`0 16px 38px rgba(0, 0, 0, 0.45)`, which `SHADOW_OPTIONS` does **not** contain,
because a preset's own tokens are copied verbatim by `resolveThemeTokens` and
never pass through the admin form's dropdown. An exact match would therefore have
missed the one preset whose point is a heavier shadow. Blur-matching puts any
future preset on the nearest step instead of silently on the default; Fantasy's
38px lands on Default's 34 rather than Deep's 44, and `none` is still matched as
a literal. Applied to `FeatureCard` and the Material `Card` defaults.
**This map is not a no-op, and that is the decision, not an oversight** — see §2.
An untouched instance has no `--shadow-card`, which resolves to the shipped
default of 4dp, while the app draws its cards flat today.
### 5.5 When the appearance is (re-)read
@@ -454,7 +479,10 @@ which is what an admin typing a label means, and matches the website.
- **AC-1 — the no-op proof.** With `theme` absent, `nav_public` absent and no
`brand_assets`, the resolved `ColorScheme`, `Shapes`, `Typography` and drawer
entry list are **equal** to today's shipped values. A unit test asserts the
full `ColorScheme` equality, not a spot check.
full `ColorScheme` equality, not a spot check. **Card elevation is excluded**
by the phase 2 decision in §2/§5.4 — an untouched instance gains the 4dp the
`runic-gateway` shadow resolves to, and the test asserts that value rather than
the app's former flat one.
- **AC-2 — per-field fallback.** A `theme` map carrying one valid token and four
malformed ones applies the one and falls back on the four.
- **AC-3 — the gates still hold.** An override marking a feature-gated or
@@ -485,7 +513,7 @@ PR against `main`.
|---|---|---|
| **0** ✅ | **Contract & appearance store** | `SettingsDto` gains `theme` and `nav_public`; `SiteAppearance` replaces the bare `BrandDto` in `AppViewModel`; second-stage JSON parse; resume refresh (§5.5). **No visual change** — this phase must be invisible |
| **1** ✅ | **Colors** | `ShardPalette` + `LocalShardPalette`; all direct `Color.kt` imports migrated; `RunicGatewayTheme(appearance)`; AC-1 + AC-2 tests |
| **2** | **Radii & shadow** | Ratio-scaled `Shapes` (§5.2), elevation map (§5.4) |
| **2** | **Radii & shadow** | Ratio-scaled `Shapes` (§5.2), elevation map (§5.4) |
| **3** | **Fonts** | Seven bundled families + licenses; stack → `FontFamily` resolution; `Type.kt` takes its three families from the resolved theme. APK size recorded |
| **4** | **Brand assets** | Logo in the drawer header and top bar, hero on Home (§5.6). Coil + `LocalAssetResolver` already exist; renders nothing when unset |
| **5** | **Public nav: label / order / hidden** | The path→route table (§6.2), `Routes.news(category)`, the merge, drawer wiring. AC-3 |
@@ -586,6 +614,52 @@ proven not to have moved anything.
`lintDebug` and `assembleDebug` clean. Not exercised on device — that is AC-5,
in phase 8.
### Phase 2 as landed
- **The app is flat, so §5.4 is the milestone's one visible default change.**
Material3 1.3.0's filled `Card` is `ElevationTokens.Level0` — 0dp, checked in
the artifact's bytecode, not assumed — and `FeatureCard` was a `Box` with a
clip, a gradient and a border, drawing none of the "soft shadow" its own KDoc
claimed. The `runic-gateway` preset meanwhile selects the *Default* shadow, so
reading §5.4 literally gives an untouched instance 4dp on every card. The org
lead chose that over rebasing the table on the flat baseline (which would have
collapsed `none`/`Soft`/`Default` onto 0dp and left only `Deep` doing
anything). §2 records the exception; the radius half remains a provable no-op.
- **`ShardCard` exists because Material's theme cannot carry elevation.** The
color scheme and the shape scale both reach screens through `MaterialTheme`,
but `Card` takes its elevation as a **default argument** — there is no
composition local behind `CardDefaults.cardElevation()`. So the phase migrated
all **24 `Card(` call sites across 20 files** to a one-line wrapper in
`ThemeComponents.kt`. Every one of them passed nothing but a modifier, which is
why the wrapper's signature is `(Modifier, ColumnScope.() -> Unit)` and the
migration is mechanical. A `Card(` outside that file is now, by construction, a
card the shard cannot theme.
- **`--radius-pill` reaches exactly one composable.** The app has three
`CircleShape` uses and two of them are 8dp status dots (`OnlineDot`,
`LiveChip`); a dot stays a dot however square an admin makes the site. Only
`StatusPill` takes the resolved shape. §5.2 records the literal-px rule the
pill needs because a percentage shape has no dp to scale.
- **`Shapes` *does* implement `equals`** — the opposite of phase 1's
`ColorScheme` finding, and also checked in the bytecode. So the structural
no-op proof is one assertion against a verbatim copy of the pre-M12 scale
rather than a reflection walk. Both the empty theme and the full
`runic-gateway` token map are asserted `==` to `ShardStructure.Shipped`.
- **`FeatureCard`'s literal 12dp became `MaterialTheme.shapes.medium`** — the
same value, so no-op, but now carried by the ratio. `StatBar`'s three
`RoundedCornerShape(3.dp)` stay literal: that is half the height of a 6dp
meter, not a member of the card radius family.
- **The structure resolution is pure, like the palette's.**
`ShardStructure.resolve(theme)` takes a token map and returns shapes + pill +
elevation, so every assertion is a plain JVM test with no Compose rule.
`RunicGatewayTheme` remains the only composable where resolution happens.
- Tests: `ShardStructureTest` (13 — the shipped scale against a verbatim pre-M12
copy, three no-op paths, Fantasy and Modern scaled onto the app's dp, per-field
fallback, a zero radius, the pill floor, all four shadow options, the
off-catalog blurs, and an unreadable shadow). **386 unit tests green**,
`lintDebug` and `assembleDebug` clean. Not exercised on device — that is AC-5,
in phase 8, where the new shadow should be looked at with the flat build beside
it.
### Phase 7, cancelled
Reading `nav_admin` / `nav_player` is **not scheduled**. The measurement in §6.4