diff --git a/android/THEMING_AND_NAV.md b/android/THEMING_AND_NAV.md index cd13b5a..bc63fb9 100644 --- a/android/THEMING_AND_NAV.md +++ b/android/THEMING_AND_NAV.md @@ -239,21 +239,57 @@ is constructed server-side and the only part that carries the choice: → the role's shipped family ``` +**The lookup is one global map, never scoped to the role's option list** — +settled in phase 3, and the same trap §5.4 hit with `--shadow-card`. The server +validates an *admin-entered* font against `FONT_OPTIONS[role]`, but a preset's +tokens are copied verbatim by `resolveThemeTokens` and never pass through that +list: `modern` publishes `--display: 'Work Sans', Arial, sans-serif`, which the +display dropdown does not offer, and `fantasy` publishes +`--sans: 'EB Garamond', Georgia, serif`, which the sans dropdown does not either. +A per-role lookup would have missed the display face of one preset and the label +face of the other. + The three roles map onto `Type.kt`'s existing three groups verbatim: `--display` → the Cinzel display/headline/title block, `--serif` → the `AppSerif` body block, `--sans` → the `AppSans` label block. Sizes, weights and tracking do not move — only the family. -**IM Fell English has no bold weight** (the website doc records the same). A -`FontWeight.Bold` request against it must resolve to its single weight rather -than synthesize; check what Compose does here on device and pin the behavior in -the phase's notes. +Because a family is not confined to the role its dropdown lives in, **every +bundled family supplies all four weights the scale asks for** — 400 (body), 500 +and 700 (labels), 600 (display) — pinned through `FontVariation` on the variable +faces. Two exceptions, both upstream facts rather than choices: -APK cost: roughly **1.5–2.5 MB** across seven families, variable-axis where Google -Fonts publishes one (Cinzel, EB Garamond, Merriweather, Playfair Display, Inter, -Work Sans, Source Sans 3) and single-weight for IM Fell English. Measure the -release APK before and after, and record both numbers in the PR — R8 does not -shrink `res/font/`. +- **IM Fell English has one weight per style.** Its 400 face answers all four + requests and Android synthesises the bold; the website's dropdown labels it + "(no bold weight)" for the same reason. What that synthesis actually looks like + is an AC-5 observation, not something a unit test can pin. +- **Cinzel keeps the 500/600/700 it shipped with in M5.** It is the only family + the server offers in the display role alone, so nothing can ask it for 400 and + adding an instance would have edited M5's type for no reachable case. + +**Italics mirror the website's set**, decided in phase 3: EB Garamond, IM Fell +English, Merriweather and Playfair Display carry a true italic, exactly the four +`client/index.html` requests one for. Inter, Work Sans, Source Sans 3 and Cinzel +are upright-only and Compose skews them — which is what the app already did for +every family before this milestone, and what the web does for its own +upright-only faces. The app draws italic in two places. + +APK cost, **measured, not estimated** — the drafted "roughly 1.5–2.5 MB" was +wrong by more than 3×. Unsigned release APK, R8 full-mode + resource shrink, on +`edge` at the phase 2 merge: + +| | added (compressed) | release APK | +|---|---|---| +| before phase 3 | — | 5,031,411 B — 4.80 MiB | +| after phase 3 | 8,543,292 B — 8.15 MiB | 13,574,703 B — **12.94 MiB** | + +**Merriweather is 6.08 MiB of the 8.15** — upstream ships it as a three-axis +`[opsz,wdth,wght]` variable font with a full charset, 4.6 MB per style, and it +deflates only 31% where the others manage 50–60%. Bundling it verbatim anyway was +the org lead's call, taken with the cheaper options costed: Google's own static +400/700 builds would have held the whole app near 6.8 MiB, and not bundling it at +all near 6.2 MiB, at the price of a serif option that silently does nothing on +Android. R8 does not shrink `res/font/`; the APK's deflate is the only saving. ### 5.4 Shadow depth @@ -514,7 +550,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) | -| **3** | **Fonts** | Seven bundled families + licenses; stack → `FontFamily` resolution; `Type.kt` takes its three families from the resolved theme. APK size recorded | +| **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 | | **6** | **Public nav: sections & added links** | Drawer groups, `pruneNav` port, link path validation, native-route resolution + Custom Tab fallback (§6.3) | @@ -660,6 +696,49 @@ proven not to have moved anything. in phase 8, where the new shadow should be looked at with the flat build beside it. +### Phase 3 as landed + +- **The APK nearly tripled, and that was a decision rather than a discovery.** + §5.3 now carries the measured before/after and the two cheaper options that + were costed and declined. The one number worth remembering: Merriweather is + 75% of the payload, because upstream publishes it as a three-axis variable font + that barely compresses. Anyone revisiting the app's size should start there and + nowhere else. +- **The per-role font list is not the set of values a role can hold.** Two of the + three presets publish a font their own role's dropdown does not offer (§5.3). + The resolution map is therefore global, keyed by the lowercased first family + name, and the tests assert both preset cases by name so a future per-role + "tidy-up" fails loudly. +- **`Typography` implements `equals`** — like phase 2's `Shapes` and unlike phase + 1's `ColorScheme`, checked the same way in the material3 1.3.0 bytecode. AC-1's + type half is one comparison against a verbatim copy of the pre-M12 scale held in + the test, so a stray edit to a size or a letter-spacing in `Type.kt` fails there + rather than quietly redefining what "shipped" means. +- **No `LocalShardTypeface`, deliberately.** The palette and the structure each + needed a composition local for the parts `MaterialTheme` cannot carry; the + families need none. Every text style in the app comes from + `MaterialTheme.typography`, and the two places that override anything + (`NotificationsScreen`'s hint, `BlockRenderer`'s italic title) override the + *style*, not the family. `FontFamily.Monospace` on the recovery-codes screen + stays fixed, as a semantic choice rather than a themed one. +- **`Type.kt`'s `val Typography` became `shardTypography(faces)`**, which is the + whole migration: the three families were referenced from that one file and + nowhere else, so unlike phase 1's colour imports and phase 2's 24 `Card(` sites + there was no call-site sweep at all. +- **Licences live in `app/licenses/`**, one `*-OFL.txt` per family, never under + `res/font/` — aapt rejects a `.txt` there, the M5 gotcha. Three of the seven + carry a Reserved Font Name (Merriweather, Playfair Display, Source Sans 3), + which is a further reason the binaries are taken verbatim rather than subsetted + or instanced locally. +- Tests: `ShardTypefaceTest` (15 — the shipped families and the pre-M12 scale, the + `runic-gateway` no-op, both preset bypasses by name, all thirteen shortlist + options, first-name parsing against quoting/casing/whitespace, per-field + fallback, a colours-only theme, blank and comma-only stacks, and a themed scale + proved to differ from the shipped one *only* in its families). **401 unit tests + green** (386 + 15), `lintDebug` and `assembleDebug` clean. Not exercised on + device — that is AC-5, in phase 8, where IM Fell English's synthesised bold is + the thing to look at. + ### Phase 7, cancelled Reading `nav_admin` / `nav_player` is **not scheduled**. The measurement in §6.4