docs(android): M12 phase 4 as landed
§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>
This commit is contained in:
@@ -348,12 +348,16 @@ a logo. Nothing new is needed to fetch them — they already arrive resolved, an
|
||||
|
||||
Two surfaces, chosen to mirror the website's without inventing new layout:
|
||||
|
||||
- **the drawer header**, above the instance name that sits there today;
|
||||
- **the top bar**, replacing the uppercased name when a logo exists.
|
||||
- **the drawer header**, above the instance name that sits there today, at 32dp;
|
||||
- **the top bar**, replacing the uppercased name when a logo exists, at 24dp.
|
||||
|
||||
And the hero on **Home**, above the title block, which is the one screen with a
|
||||
hero-shaped space.
|
||||
|
||||
Both logo slots cap their width at **six times their height**, mirroring the
|
||||
website's `maxWidth: height * 6`, so a long wordmark scales down rather than
|
||||
pushing the drawer header or the top bar's title out of shape.
|
||||
|
||||
The M5 `BrandLogo` rule carries over: **render nothing when the slot is empty.**
|
||||
Not a placeholder, not a reserved gap — an instance with no uploaded logo must
|
||||
lay out exactly as it does today, which is §2 applied to assets. On the centered
|
||||
@@ -363,6 +367,35 @@ reason: a row would change the block's height on instances that have no logo.
|
||||
An asset that fails to load is the same as no asset. No broken-image icon, no
|
||||
retry.
|
||||
|
||||
**The top bar is the one place where "empty" is not "nothing".** Everywhere else
|
||||
the empty slot draws literally nothing, because something else on the surface
|
||||
already says the instance's name. In the top bar the logo *is* the title, so a
|
||||
404 — or a logo that can't be fetched because the shard is down — would leave
|
||||
the app in an unnamed shell until the next resume refresh. There "the same as no
|
||||
asset" resolves to the text, since the text is what an instance with no logo
|
||||
shows. There is deliberately **no fallback while the load is still in flight**:
|
||||
drawing the text first would flash text → logo on every navigation for the sake
|
||||
of one frame, as Coil serves the second and later reads from its memory cache.
|
||||
|
||||
**The hero is a fixed 180dp band, cropped**, rather than the intrinsic aspect
|
||||
ratio the app's other images (`PostScreen`, `BlockRenderer`) draw at. The
|
||||
website's hero is a CSS background driven by `hero_layout`, which the app does
|
||||
not port, so the app needs a rule of its own — and the website's *default* hero
|
||||
is a square emblem (`/assets/img/runic-emblem.png`), so an uploaded square is a
|
||||
case to expect rather than an edge one. At the intrinsic aspect that square
|
||||
would be a ~360dp block that pushes the status card off the first screenful;
|
||||
cropped to a band, a wide banner and a square give the same frame above the
|
||||
title. It clips to `shapes.medium`, so the hero follows `--radius-card` like
|
||||
every other surface the admin can round off (§5.2). Note that the app takes **no
|
||||
fallback image**: where the website substitutes its own emblem for an unset
|
||||
hero, the app draws nothing, because §2 outranks the mirror.
|
||||
|
||||
Accessibility follows the website's split: the logo is **decorative wherever the
|
||||
name is also on screen in text** (the drawer header, where the name is the very
|
||||
next line) and **named only where it stands alone** (the top bar). Describing
|
||||
the drawer's would have a screen reader say the instance's name twice — the same
|
||||
call the website's `alt=''` makes. The hero is always decorative.
|
||||
|
||||
## 6. Navigation
|
||||
|
||||
### 6.1 The hard constraint carries over
|
||||
@@ -551,7 +584,7 @@ PR against `main`.
|
||||
| **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 |
|
||||
| **4** | **Brand assets** | Logo in the drawer header and top bar, hero on Home (§5.6). Coil + `LocalAssetResolver` already exist; renders nothing when unset |
|
||||
| **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) |
|
||||
| **7** | **Authenticated navs** | ❌ **cancelled** — was: `GET /api/v1/settings/nav` behind a session-keyed repository; label/hidden for the four mapped rows (§6.4). See "Phase 7, cancelled" below |
|
||||
@@ -739,6 +772,47 @@ proven not to have moved anything.
|
||||
device — that is AC-5, in phase 8, where IM Fell English's synthesised bold is
|
||||
the thing to look at.
|
||||
|
||||
### Phase 4 as landed
|
||||
|
||||
- **The empty slot is enforced by layout, not by a conditional at each site.**
|
||||
Every size and spacing modifier hangs off the image itself, so when the image
|
||||
is not composed neither is its padding. A caller that wants space below the
|
||||
hero passes `Modifier.padding(bottom = 16.dp)` rather than a sibling `Spacer`,
|
||||
and both cases — asset and no asset — come out right without the caller
|
||||
knowing which it got. This is the detail that makes "renders nothing when
|
||||
unset" hold without a `brand?.hero != null` check leaking into `HomeScreen`.
|
||||
- **Three §5.6 questions the spec left open, settled by the org lead before
|
||||
code**: the top bar falls back to the text rather than going blank; the hero
|
||||
is a fixed cropped band rather than its intrinsic aspect; the hero sits inside
|
||||
Home's existing 20dp padding with themed corners rather than going full-bleed.
|
||||
All three are written into §5.6 above with their reasoning, so they are not
|
||||
re-litigated from scratch.
|
||||
- **A failed load is keyed on the URL.** `remember(url)` resets the failure flag
|
||||
when a resume refresh (§5.5) swaps the logo, so an instance that fixes a broken
|
||||
upload recovers on the next refresh instead of inheriting the old failure for
|
||||
the life of the process.
|
||||
- **The blank check runs on both sides of the resolver.** `BrandDto` defaults
|
||||
every asset field to `""` rather than null — the server publishes the empty
|
||||
string for "not set" — and a resolver with no base URL configured may hand a
|
||||
path straight back. Null out of `brandAssetUrl` is the "draw nothing" signal,
|
||||
so a blank slipping through either side would put a zero-size image request in
|
||||
the layout instead of no image at all.
|
||||
- **No new dependency, no new asset, no APK cost.** Unlike phase 3 this phase
|
||||
adds nothing to the package: `LocalAssetResolver` (M1) and Coil (M5) were both
|
||||
already there, and `BrandDto` has carried `logo` and `hero` since M1 without a
|
||||
reader. The whole phase is one new file plus three call sites.
|
||||
- Tests: `BrandAssetsTest` (9 — every shape "not set" arrives in, the default
|
||||
`BrandDto`, `SiteAppearance.NONE`, site-relative and absolute paths, and a
|
||||
resolver that returns null or blank). **410 unit tests green** (401 + 9),
|
||||
`lintDebug` and `assembleDebug` clean. **The drawing itself is untested and
|
||||
cannot be tested here** — the app carries no Robolectric and has no
|
||||
`androidTest` source set, so a composable body cannot run in a JVM test. Only
|
||||
the decision of *whether* to draw is pure, which is why `brandAssetUrl` is
|
||||
pulled out of the composables at all. Everything else about this phase is
|
||||
AC-5's to catch, and it is the phase with the most riding on that walk: a
|
||||
cropped hero, a synthesised bold, and a logo's contrast against the top bar
|
||||
are all things only a screen shows.
|
||||
|
||||
### Phase 7, cancelled
|
||||
|
||||
Reading `nav_admin` / `nav_player` is **not scheduled**. The measurement in §6.4
|
||||
|
||||
Reference in New Issue
Block a user