docs(android): M12 theming & navigation, all phases as landed (cutover) #120

Merged
whitlocktech merged 16 commits from edge into main 2026-08-08 16:14:30 +00:00
Member

What & why

The docs half of the M12 cutoveredgemain, pairing with Android-app#41. Same shape the website side used for its own theming & nav cutover (docs#109).

Everything here is the record of a milestone that is now built: android/THEMING_AND_NAV.md (the design of record, plus an "as landed" section per phase), android/PLAN.md §9's M12 entry, and an amendment to android/COVERAGE_PLAN.md.

What the "as landed" sections are for

Each phase's section records what the build learned that the spec did not know — the measured facts, the decisions taken before code, and the places the spec was simply wrong about the website and had to be corrected against its source. The ones most worth keeping:

  • §6.2's row order and §6.3's link table were both wrong. Phase 5 found the nav's row order did not match SiteHeader.jsx; phase 6 found three of the link table's paths were routes the site does not serve and two real detail routes were missing. Both times the fix was the same — quote the website's own source beside the table — so NavPaths.kt now carries App.jsx's route list next to SiteHeader.jsx's NAV. This is the cross-repo coupling to distrust first.
  • An untouched row's sort key is its index in the website's sixteen-row nav, not the app's nine. A stored order is a web index; app indices would put explicit and implicit keys on incomparable number lines.
  • Material takes some themed values as default arguments, not from the theme. Phase 2 found it for card elevation. Phase 8's AC-5 walk found the same thing for the card container colour (26 sites) and the drawer row's shape. Phase 8's section carries the role sweep — which ColorScheme roles are still unmapped and which of them have a live reader — so nobody has to redo that measurement.
  • Three deliberate changes to an untouched instance, named and reasoned: card shadow, card container, and a hero on Home. The last is a correction to the record rather than a code change — brand.hero carries the BRAND_HERO_IMAGE env default, so "the admin set nothing" is not "the slot is empty".
  • Phase 7 is cancelled, with the measurement that killed it kept. The two authenticated navs reach four app rows between them; the sidebar's other ~18 rows are admin configuration the app excludes. Nothing in phases 0–6 was scaffolding for it, and the player/staff drawer rows keep their coded — therefore localized — labels as a result.
  • COVERAGE_PLAN.md's amendment: a ui/theme/** directory glob went stale the moment M12 put three pure resolvers behind it, and the rule that follows — prefer file globs to directory globs once a directory is mixed.

How it was tested

Documentation only. Every claim recorded here was verified in the paired code PRs as each phase landed — 477 unit tests green at the end, lintDebug and assembleDebug clean, and a two-pass AC-5 on-device walk across four role rungs. Material facts (FilledCardTokens.ContainerColor, ElevationTokens.Level0, whether ColorScheme/Shapes/Typography implement equals) were read out of the material3 1.3.0 artifact's bytecode rather than assumed.

Both repos' edge were checked to agree before opening this pair, which is the trap the website cutover hit once (docs#108 merged while website#125 was still open, leaving docs edge documenting a feature the code edge did not have).

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand
    every change, and take responsibility for it. AI-authored commits are
    marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license
    (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why **The docs half of the M12 cutover** — `edge` → `main`, pairing with **Android-app#41**. Same shape the website side used for its own theming & nav cutover (docs#109). Everything here is the record of a milestone that is now built: `android/THEMING_AND_NAV.md` (the design of record, plus an "as landed" section per phase), `android/PLAN.md` §9's M12 entry, and an amendment to `android/COVERAGE_PLAN.md`. ### What the "as landed" sections are for Each phase's section records what the build learned that the spec did not know — the measured facts, the decisions taken before code, and the places the spec was simply **wrong about the website** and had to be corrected against its source. The ones most worth keeping: - **§6.2's row order and §6.3's link table were both wrong.** Phase 5 found the nav's row order did not match `SiteHeader.jsx`; phase 6 found three of the link table's paths were routes the site does not serve and two real detail routes were missing. Both times the fix was the same — quote the website's own source *beside* the table — so `NavPaths.kt` now carries `App.jsx`'s route list next to `SiteHeader.jsx`'s `NAV`. **This is the cross-repo coupling to distrust first.** - **An untouched row's sort key is its index in the *website's* sixteen-row nav**, not the app's nine. A stored `order` is a web index; app indices would put explicit and implicit keys on incomparable number lines. - **Material takes some themed values as default arguments, not from the theme.** Phase 2 found it for card elevation. Phase 8's AC-5 walk found the same thing for the card *container colour* (26 sites) and the drawer row's *shape*. Phase 8's section carries the **role sweep** — which `ColorScheme` roles are still unmapped and which of them have a live reader — so nobody has to redo that measurement. - **Three deliberate changes to an untouched instance**, named and reasoned: card shadow, card container, and a hero on Home. The last is a correction to the record rather than a code change — `brand.hero` carries the `BRAND_HERO_IMAGE` env default, so "the admin set nothing" is not "the slot is empty". - **Phase 7 is cancelled, with the measurement that killed it kept.** The two authenticated navs reach four app rows between them; the sidebar's other ~18 rows are admin *configuration* the app excludes. Nothing in phases 0–6 was scaffolding for it, and the player/staff drawer rows keep their coded — therefore **localized** — labels as a result. - **`COVERAGE_PLAN.md`'s amendment**: a `ui/theme/**` directory glob went stale the moment M12 put three pure resolvers behind it, and the rule that follows — prefer file globs to directory globs once a directory is mixed. ## How it was tested Documentation only. Every claim recorded here was verified in the paired code PRs as each phase landed — 477 unit tests green at the end, `lintDebug` and `assembleDebug` clean, and a two-pass AC-5 on-device walk across four role rungs. Material facts (`FilledCardTokens.ContainerColor`, `ElevationTokens.Level0`, whether `ColorScheme`/`Shapes`/`Typography` implement `equals`) were read out of the material3 1.3.0 artifact's bytecode rather than assumed. Both repos' `edge` were checked to agree before opening this pair, which is the trap the website cutover hit once (docs#108 merged while website#125 was still open, leaving docs `edge` documenting a feature the code `edge` did not have). ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 16 commits 2026-08-08 16:12:52 +00:00
Two records, one milestone.

Phase 7 (reading nav_admin / nav_player) is cancelled. Section 6.4 already
made the case against it and scheduled it last so the call could be taken on
its merits with the rest working: the two authenticated navs reach four app
rows between them, which does not pay for a new authenticated fetch, a
session-keyed cache and its teardown. The app therefore makes no authenticated
settings call at all, and the player and staff drawer rows keep their coded --
and so localized -- labels, which is the one thing given up. Section 4's locked
decision, section 3's endpoint row and section 5.5's lifecycle paragraph are
amended to match so the doc does not contradict itself, and section 8 gains
the standing "Phase 7, cancelled" note.

Phase 0 (the contract and the appearance store) landed as Android-app#33, and
its "as landed" notes record two departures from what section 8 specified:

- `theme` is modeled as a raw JsonElement rather than Map<String,String>?.
  kotlinx fails the decode of the whole object on a value of an unexpected
  kind, and `theme` shares its payload with `brand` and `push` -- so one odd
  token would have blanked the branding and dropped the push relay URL, the
  opposite of section 2. It is coerced field-by-field instead.
- A failed refresh keeps the last good appearance rather than falling back to
  NONE. Section 5.5 said best-effort; the distinction it did not draw is that
  a moment of no connectivity on resume must not repaint a themed shard back
  to the shipped defaults.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #112
The colors phase, shipped as Android-app#34. Seven notes, of which three are
departures the build forced rather than choices it made:

ColorScheme has no equals in material3 1.3.0, so AC-1's "full equality, not a
spot check" is a field-by-field compare by reflection over all 36 color roles,
against a verbatim copy of the pre-M12 scheme held in the test.

ShardPillFg is derived from --accent-bright rather than being a sixteenth
token, under the rule 5.1 already states for ShardOnCta.

An instance with an env accent and no theme_visual row loses its accented CTA
buttons, because brand.accent now seeds --accent alone. That is 5.1's "wrong
twice over" being corrected, and it is the one thing on screen that moves.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #113
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>
Reviewed-on: #114
§5.3 rewritten against what the build measured rather than what the plan
estimated, plus a "Phase 3 as landed" section and the  in §8.

Three corrections to the drafted section:

- The APK estimate was wrong by more than 3×. "Roughly 1.5-2.5 MB" becomes a
  measured before/after table: 5,031,411 B → 13,574,703 B, +8.15 MiB. The
  section now records that Merriweather is 6.08 MiB of that on its own, and the
  two cheaper options the org lead costed and declined, so the number is not
  re-litigated from scratch later.
- Family lookup must be global, never scoped to the role's option list. Two of
  the three presets publish a font their own role's dropdown does not offer,
  because resolveThemeTokens copies preset tokens verbatim — the same bypass
  §5.4 already records for --shadow-card.
- Italics mirror the website's four rather than being skipped, and every
  bundled family supplies the four weights the scale asks for (with IM Fell
  English's single weight and Cinzel's untouched 500/600/700 as the two
  documented exceptions), because a family is not confined to the role its
  dropdown lives in.

Android-app: RunicGateway/Android-app#TBD

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #115
§5.6 gains the three decisions the drafted section left open — the top bar's
text fallback, the hero's fixed cropped band, and the accessibility split
between a decorative logo and a named one — each with the reasoning that
settled it, so none is re-litigated from scratch. Adds the 32dp/24dp logo
heights, the six-times-height width cap, and the note that the app takes no
fallback hero image where the website substitutes its own emblem.

"Phase 4 as landed" records how the empty-slot rule is enforced by layout
rather than by a conditional at each call site, why the blank check runs on
both sides of the resolver, and the honest limit of the phase's tests: the
drawing cannot be tested here at all, since the app has no Robolectric and no
androidTest source set, so only the decision of whether to draw is pure.

Phase 4 ticked in §8.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #116
§6.2 corrected and amended, §6.1 given the Home decision, and a "Phase 5 as
landed" section added with the  in §8.

**§6.2's table was in the wrong order, and the order is now load-bearing.** The
three news categories were listed Five on Friday / Newsletter / Screenshots; the
website's array has them Screenshots / Five on Friday / Newsletter. That did not
matter while the table was only a mapping, but a stored `order` is an index into
the site's nav, so a row the admin never moved takes its sort key from this list —
and a key from the wrong list scrambles a partially-overridden nav. The rows are
now numbered 0-15 and the order is called out as verbatim.

Also written into §6.2: the `feature` values are deliberately **not** mirrored
into the table (`APP_MENU` stays the app's own source of truth for gating), and
the "no `APP_MENU` row" rule covers **seven** entries rather than four — the
three news categories as well as the hub boards.

Two decisions the org lead settled before code, both recorded where the rule
lives rather than only in the phase notes:

- **The news categories are ignored for the drawer**, on the same rule as the hub
  four. The mapping still exists for phase 6's added links, where a category tab
  is a perfectly good destination because the admin named it by path. The
  alternative — surfacing such a row only when overridden — keeps AC-1 but lets an
  override introduce navigation after all, so it was rejected.
- **An admin may hide the Home row**, mirroring the website, where `/` is hideable
  from the public header. Home stays the start destination and stays reachable by
  back-press. The contrast that makes it safe is with `/admin/navigation`, which
  has three guards because hiding it would strip the only way to undo an override;
  nothing about a hidden Home row is unrecoverable.

"Phase 5 as landed" records the sort-key finding above as the thing the spec did
not settle, that an untouched instance gets `APP_MENU` back by **identity** so
AC-1's drawer claim is an `assertSame`, that the app's own rows are partitioned
off rather than sorted (and what that would mean for a future interleaved row),
that `group`/`section` are read and dropped, and why `Routes.NEWS_ROUTE` is
declared beside `Routes.NEWS` rather than replacing it — with the consequence that
`destination.route` now carries a query and is compared on `substringBefore('?')`.

Android-app: RunicGateway/Android-app#38.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #117
Corrects §6.3's link-resolution table, which named three website paths the site
does not serve (/site/news/<idOrSlug>, /page/<slug>, /contact) and missed two it
does (/site/atlas/:slug, /site/market/vendors/:serial), and records the four
drawer decisions taken before code: the /<slug> CMS catch-all with the site's
reserved segments excluded, the static section header, the hand-off icon, and
reusing LocalAssetResolver for the Custom Tab's absolute URL.

Adds "Phase 6 as landed" and ticks the phase in §8.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #118
Phase 8 was scoped as docs, coverage and the cutover. The AC-5 walk turned it
into a phase with code in it, which is the outcome the walk existed to produce -
so the record is longer than a phase-8 note would normally be.

THEMING_AND_NAV.md gains "Phase 8 as landed": what the walk confirmed, the two
defects it found and why they are phase 2's trap repeating, the role sweep that
says which Material roles are still unmapped and which of them have a reader, the
coverage-glob narrowing, and the adb-reverse rig note for the next walk. The
phase 2 note is amended where it claimed the card shadow was the milestone's only
visible change to an untouched instance - it is now one of three, and the third
(an untouched instance drawing a hero, because brand.hero carries the env
default) is a correction to the record with no code behind it.

PLAN.md's M12 entry stopped at phase 2; it now runs through 6, records phase 8
including the defects, and drops the same "one deliberate change" claim.

COVERAGE_PLAN.md gains an amendment section explaining why the ui/theme/**
directory glob became wrong the moment M12 put three pure resolvers behind it,
and the general rule it suggests: prefer file globs to directory globs once a
directory is mixed. Its own §5 already forbade excluding testable code.

Pairs with Android-app phase 8.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #119
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
wtclaude changed title from docs(android): M12 theming &amp; navigation, all phases as landed (cutover) to docs(android): M12 theming & navigation, all phases as landed (cutover) 2026-08-08 16:13:03 +00:00
whitlocktech approved these changes 2026-08-08 16:14:22 +00:00
whitlocktech merged commit f665f75bbb into main 2026-08-08 16:14:30 +00:00
whitlocktech deleted branch edge 2026-08-08 16:14:30 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/docs#120
No description provided.