Compare commits
11 Commits
0d5743ec74
...
docs/andro
| Author | SHA1 | Date | |
|---|---|---|---|
| cbaa18ea0b | |||
| f79c2fa5a9 | |||
| 8798a0ff79 | |||
| 4eae12448a | |||
| 17320ac578 | |||
| a3a5985268 | |||
| 3a1d091a71 | |||
| cdc2bf580e | |||
| da3be61c7f | |||
| ed53990679 | |||
| 07603691bb |
130
android/PLAN.md
130
android/PLAN.md
@@ -1,6 +1,6 @@
|
|||||||
# Android App — Plan
|
# Android App — Plan
|
||||||
|
|
||||||
Status: **M0–M3 landed; M4 (player self-service & game data) next.** This document is the
|
Status: **M0–M6 landed; the functional build, design pass, and release mechanics are complete (the auto-release engine cuts a tagged, signed APK on merge to `main`; M7 push notifications next).** This document is the
|
||||||
design contract for the `RunicGateway/Android-app` repo. It was written before implementation so the
|
design contract for the `RunicGateway/Android-app` repo. It was written before implementation so the
|
||||||
API changes it depends on could be landed in `website/` and `docs/` first. The authoritative API
|
API changes it depends on could be landed in `website/` and `docs/` first. The authoritative API
|
||||||
reference is the committed OpenAPI spec at `website/server/swagger/swagger-output.json` (regenerated
|
reference is the committed OpenAPI spec at `website/server/swagger/swagger-output.json` (regenerated
|
||||||
@@ -70,12 +70,80 @@ plain credential `401`), the `SessionManager` lifecycle over a fake store, and t
|
|||||||
+ role mapping. **No backend/API change** — the app is a pure consumer of the existing mobile bearer +
|
+ role mapping. **No backend/API change** — the app is a pure consumer of the existing mobile bearer +
|
||||||
`/auth/me` surface.
|
`/auth/me` surface.
|
||||||
|
|
||||||
> **Biometric app-lock — deferred (decided at M3).** §4.3/§9 flag an *optional* biometric app-lock;
|
> **Biometric app-lock — descoped from v1 (decided at M6).** §4.3/§9 flagged an *optional* biometric
|
||||||
> it is **deferred to M6 (release hardening)**. Tokens are already encrypted at rest (Tink/AES-256-GCM),
|
> app-lock, deferred from M3 to M6. At M6 it was **descoped from v1 entirely**: tokens are already
|
||||||
> so an app-lock is a UX layer, not a security requirement, and adding it in the functional pass would
|
> encrypted at rest (Tink/AES-256-GCM), so an app-lock is a pure UX convenience, not a security
|
||||||
> widen scope without changing the data flow. Revisit as an opt-in setting during M6.
|
> requirement, and it changes no data flow. It is **not** in the first release; revisit only if it
|
||||||
|
> becomes a requested feature.
|
||||||
|
|
||||||
**M4 (functional pass) and M5 (design pass) remain.**
|
✅ **M4 — player self-service & game data** (2026-07-19, `RunicGateway/Android-app#9`, functional Kotlin
|
||||||
|
pass): the signed-in player surface, all as a pure consumer of the existing bearer-gated API.
|
||||||
|
**Account self-service** over the role-agnostic `/auth/me/account*` (§6.4) — change username (409
|
||||||
|
"taken" surfaced; a success re-validates the session so the shell reflects the new name at once),
|
||||||
|
change/set password (the SSO-account "no current password" path from `has_password`), TOTP
|
||||||
|
**setup → scan → enable** (the `data:` QR is base64-decoded to a bitmap in-app) / disable-by-code, and
|
||||||
|
list/unlink SSO identities — each mutation folding its `ApiResult` into a section-scoped, localized
|
||||||
|
banner (§7). **Game-account linking** (§6.3) — the in-game `[link` one-time code (`POST
|
||||||
|
/player/shard/link`) plus the hybrid signup (`POST /player/shard/account`, shown only when the public
|
||||||
|
`gameAccountSignup` flag is set), and the linked-accounts list. **Own game data**, text-only (§6.3):
|
||||||
|
per-account character roster → a character sheet (attributes, vitals, resistances, best-first skills,
|
||||||
|
equipment with AOS mods, and guild/governor standing chips — bare cliloc-number titles/item names are
|
||||||
|
skipped, as the app ships no cliloc table, matching the website's `CharacterSheet.jsx`); player
|
||||||
|
vendors (shops + listings) with recent sales; and the player's own houses (decay/IDOC). Each
|
||||||
|
per-account read carries its **own** load state, so a down shard degrades that one account to
|
||||||
|
offline/retry (`503`) — or not-found (`403`) — without blocking the rest. The menu gains three
|
||||||
|
**PLAYER-access** groups (My Characters / Vendors / Houses) revealed only when the session role is
|
||||||
|
`player`; a `PlayerGate` sends a signed-out or server-side-demoted user home. DTOs are hand-authored +
|
||||||
|
spec-aligned (as recorded for M1); 17 new JVM unit tests cover the account + player-shard DTO decode
|
||||||
|
(hex serials, permissive objects, equipment mods) and the character-sheet title/skill display helpers.
|
||||||
|
**No backend/API change** — the `/auth/me/*` and `/player/shard/*` surfaces the app consumes were the
|
||||||
|
§8 prerequisites, already landed.
|
||||||
|
|
||||||
|
✅ **M5 — design pass** (2026-07-20, `RunicGateway/Android-app#10`): the shard-website theme applied
|
||||||
|
across every screen, restyling the working M1–M4 UI with **no architecture, data-flow, endpoint, or DTO
|
||||||
|
change** (§2.1). The design was produced in Claude Design (`Runic Gateway Screens.dc.html`) and
|
||||||
|
implemented in Compose. Because the functional screens already draw their color/type/shape from
|
||||||
|
`MaterialTheme` tokens (§2), the restyle lives mostly in the **theme layer** and propagates: a deep
|
||||||
|
blue-black surface stack (page `#0b0f14` / screen `#0e1318` / elevated `#11161d`), a slate-blue accent
|
||||||
|
(`#7f99bd`) with a light CTA fill (`#cdd9e8`), parchment serif body copy, and the engraved **Cinzel**
|
||||||
|
serif display face (bundled weight-axis variable font, SIL OFL) for headings and the top bar. The app is
|
||||||
|
now **dark-only** — the shard-website look is a single dark theme, so the light scheme is dropped and the
|
||||||
|
system light/dark setting is ignored; **per-shard brand-accent seeding is retained** (a site's published
|
||||||
|
accent still tints the primary/secondary roles, §3). A small set of reusable components — semantic status
|
||||||
|
pills, section labels, a gradient "feature" card, and slim stat meters — carries the motifs the design
|
||||||
|
repeats (home status, shard-online banner, champ/character/house status, character vitals & skills). The
|
||||||
|
launch theme and system bars are darkened so the first frame matches (no white flash). Verified by
|
||||||
|
`:app:assembleDebug` + `:app:testDebugUnitTest` (green); an on-device visual pass against the mockup is
|
||||||
|
the one open QA item noted on the PR.
|
||||||
|
|
||||||
|
✅ **M6 — polish & release mechanics** (2026-07-20, `RunicGateway/Android-app#11`): the release
|
||||||
|
plumbing to ship v1 as a signed, sideloadable APK, with **no architecture, data-flow, or endpoint
|
||||||
|
change**. **Default brand app icons** — a gateway-medallion adaptive launcher icon (all densities +
|
||||||
|
round + Play Store icon) over the deep-indigo brand background (the Image Asset wizard's default
|
||||||
|
green grid was replaced, and the legacy square/round bitmaps + 512 Play icon recomposited to match);
|
||||||
|
plus an "RG" notification icon staged for M7. A **version-mismatch guard** (§3): the first-run connect
|
||||||
|
probe refuses a backend whose API version this build can't speak (a future `v2`) with a clear
|
||||||
|
"app out of date" message rather than mis-rendering (lenient on an older backend that omits `api`).
|
||||||
|
**Release build hardening** (§7, §12) — R8 full-mode minify + resource shrink (~31 MB debug → ~4 MB
|
||||||
|
signed release) with keep-rules for the kotlinx.serialization serializers, the wire DTOs, and the
|
||||||
|
Retrofit interfaces; a release `signingConfig` that reads keystore material from a **gitignored**
|
||||||
|
`keystore.properties` or env vars (absent → unsigned; the keystore is never committed); and
|
||||||
|
`versionName` is the committed source of truth (bumped by the release engine); `versionCode` is derived
|
||||||
|
from it (`major*10000+minor*100+patch`, monotonic); both stay `-P`-overridable for local builds (§10).
|
||||||
|
**CI `release.yml`** — mirrors `link/`'s language-agnostic release engine, adapted for Android: on every
|
||||||
|
push to `main` it derives the next version from conventional-commit subjects since the last `v*` tag
|
||||||
|
(`feat!`/BREAKING → major, `feat` → minor, `fix`/`perf` → patch; nothing releasable → no release),
|
||||||
|
generates a grouped changelog, bumps `build.gradle.kts`, builds the **signed** APK (keystore decoded from
|
||||||
|
a base64 Gitea secret), then commits the bump `[skip ci]`, tags `vX.Y.Z`, and creates the Gitea release
|
||||||
|
with notes + APK + `SHA256SUMS`. Uses `REGISTRY_USER`/`REGISTRY_TOKEN` (as `link/` does) to push the bump
|
||||||
|
and create the release, so `main` must allow that account to push.
|
||||||
|
HTTPS-only in release (M1), no token logging (logging is debug-gated, M3), and the Settings → Server
|
||||||
|
hard reset (M3) were already in place. Biometric app-lock is **descoped from v1** (see the note below).
|
||||||
|
|
||||||
|
**The functional build (M0–M4), design pass (M5), and release mechanics (M6) are complete. The first
|
||||||
|
signed release now cuts automatically on the next release-worthy merge to `main` — once the signing +
|
||||||
|
`REGISTRY_*` secrets are set, `main` allows the CI account to push, and the on-device QA pass is done.
|
||||||
|
M7 push notifications are what remain.**
|
||||||
|
|
||||||
**Prerequisite progress (§8):** all v1 prerequisites are **done** (2026-07-19) — ✅ password reset
|
**Prerequisite progress (§8):** all v1 prerequisites are **done** (2026-07-19) — ✅ password reset
|
||||||
(item 2; website#75 + docs#8), ✅ role-agnostic `/auth/me/*` self surface (item 1; website#76 + docs#10),
|
(item 2; website#75 + docs#8), ✅ role-agnostic `/auth/me/*` self surface (item 1; website#76 + docs#10),
|
||||||
@@ -245,8 +313,8 @@ completes them in a Custom Tab, then returns and signs in natively (§4.1):
|
|||||||
- **Logout:** `POST /auth/mobile/logout` `{ refreshToken?, all? }` (requires bearer) — this session or
|
- **Logout:** `POST /auth/mobile/logout` `{ refreshToken?, all? }` (requires bearer) — this session or
|
||||||
all sessions ("sign out everywhere").
|
all sessions ("sign out everywhere").
|
||||||
- **Storage:** access + refresh tokens live in EncryptedSharedPreferences, never in plain prefs/logs.
|
- **Storage:** access + refresh tokens live in EncryptedSharedPreferences, never in plain prefs/logs.
|
||||||
The base URL may live in plain DataStore; tokens must not. Optional **biometric app-lock** (available
|
The base URL may live in plain DataStore; tokens must not. An optional **biometric app-lock** was
|
||||||
cleanly at API 29) gates access to the stored session — decide at M3.
|
considered here but **descoped from v1** (tokens are already encrypted at rest; see the M3 note).
|
||||||
- **Role for the menu** comes from the login response `user.role` and is re-validated via
|
- **Role for the menu** comes from the login response `user.role` and is re-validated via
|
||||||
`GET /auth/me` on app resume (roles can change server-side; admin access is re-checked every
|
`GET /auth/me` on app resume (roles can change server-side; admin access is re-checked every
|
||||||
request on the backend, so the app treats role as *advisory for menu rendering* and lets the server
|
request on the backend, so the app treats role as *advisory for menu rendering* and lets the server
|
||||||
@@ -413,11 +481,11 @@ No `link/` or `servuo-plugins/` changes are expected — the app is downstream o
|
|||||||
|
|
||||||
## 9. Milestones
|
## 9. Milestones
|
||||||
|
|
||||||
**Two passes (§2.1).** M0–M4 are the **functional Kotlin pass** — every screen wired to its endpoints
|
**Two passes (§2.1).** M0–M4 were the **functional Kotlin pass** — every screen wired to its endpoints
|
||||||
and working end-to-end with placeholder/functional Compose UI, no design investment yet. **M5 is the
|
and working end-to-end with placeholder/functional Compose UI, no design investment yet. **M5 was the
|
||||||
design pass**: once the functional Kotlin is done, Claude Design produces the front-end design and
|
design pass**: with the functional Kotlin done, Claude Design produced the front-end design and Claude
|
||||||
Claude Code implements the final UI to it. Polish/release, push, and Play (M6–M8) follow the designed
|
Code implemented the final UI to it. **Both passes are now complete** (M0–M5 landed); polish/release,
|
||||||
app.
|
push, and Play (M6–M8) follow the designed app.
|
||||||
|
|
||||||
1. **M0 — Repo scaffold**: Gradle + Compose + Hilt skeleton, CI (build + lint + unit test), license
|
1. **M0 — Repo scaffold**: Gradle + Compose + Hilt skeleton, CI (build + lint + unit test), license
|
||||||
headers (GPL-3.0-or-later), CONTRIBUTING/AI-disclosure parity with the other repos.
|
headers (GPL-3.0-or-later), CONTRIBUTING/AI-disclosure parity with the other repos.
|
||||||
@@ -429,8 +497,8 @@ app.
|
|||||||
4. **M3 — Auth (§4)** *(functional pass)*: native password+TOTP login (429 handling), token storage,
|
4. **M3 — Auth (§4)** *(functional pass)*: native password+TOTP login (429 handling), token storage,
|
||||||
refresh interceptor, logout, `/auth/me` role re-validation, the access-level menu. Custom-Tab
|
refresh interceptor, logout, `/auth/me` role re-validation, the access-level menu. Custom-Tab
|
||||||
**hand-offs** to the website for register / invite / password-reset / SSO (no native screens for
|
**hand-offs** to the website for register / invite / password-reset / SSO (no native screens for
|
||||||
those). Optional biometric app-lock. *Prerequisite:* the website password-reset flow (§8) is
|
those). (An optional biometric app-lock was considered here, then descoped from v1 at M6.)
|
||||||
already built.
|
*Prerequisite:* the website password-reset flow (§8) is already built.
|
||||||
5. **M4 — Player self-service & game data** *(functional pass)*: account management (via
|
5. **M4 — Player self-service & game data** *(functional pass)*: account management (via
|
||||||
`/auth/me/*`), game-account linking, own roster/characters/vendors/houses/sales — **text-only**
|
`/auth/me/*`), game-account linking, own roster/characters/vendors/houses/sales — **text-only**
|
||||||
presentation (§6.3).
|
presentation (§6.3).
|
||||||
@@ -439,10 +507,15 @@ app.
|
|||||||
UI to it** — Compose screens, Material 3 theming from the per-shard branding (§3), reusable
|
UI to it** — Compose screens, Material 3 theming from the per-shard branding (§3), reusable
|
||||||
components, loading/error/empty states, the designed access-level menu. Restyles the existing
|
components, loading/error/empty states, the designed access-level menu. Restyles the existing
|
||||||
screens only; no changes to architecture, data flow, or endpoint contracts. Text-only game data
|
screens only; no changes to architecture, data flow, or endpoint contracts. Text-only game data
|
||||||
(§6.3) still holds — this is visual design of the data screens, not paperdoll art.
|
(§6.3) still holds — this is visual design of the data screens, not paperdoll art. **Landed**
|
||||||
7. **M6 — Polish & first release**: settings (server switch = hard reset), version-mismatch guard,
|
2026-07-20 (`RunicGateway/Android-app#10`): dark-only shard-website theme, Cinzel display face,
|
||||||
release build hardening (HTTPS-only, no token logging). No offline cache in v1 (§7). **Ship v1 as a
|
reusable pill/label/card/meter components; brand-accent seeding retained (see §9 build progress).
|
||||||
signed APK attached to a Gitea release** (see §10).
|
7. **M6 — Polish & release mechanics**: settings (server switch = hard reset, done M3),
|
||||||
|
version-mismatch guard, release build hardening (HTTPS-only, no token logging, R8 minify + resource
|
||||||
|
shrink, release signing). No offline cache in v1 (§7). **Ships v1 as a signed APK attached to a Gitea
|
||||||
|
release** via `release.yml`'s conventional-commit engine on merge to `main` (see §10, §12).
|
||||||
|
Biometric app-lock **descoped** (below).
|
||||||
|
**Landed** 2026-07-20 (`RunicGateway/Android-app#11`).
|
||||||
8. **M7 — Push notifications** (post-v1): add the self-hosted `ntfy` service to
|
8. **M7 — Push notifications** (post-v1): add the self-hosted `ntfy` service to
|
||||||
`website/docker-compose.yml` (declarative, zero-interaction config), UnifiedPush integration in the
|
`website/docker-compose.yml` (declarative, zero-interaction config), UnifiedPush integration in the
|
||||||
app, device registration, the subscriptions UI, and the content-free-tickle backend fan-out (see
|
app, device registration, the subscriptions UI, and the content-free-tickle backend fan-out (see
|
||||||
@@ -542,11 +615,16 @@ repos use), on a bare `ubuntu:latest` container.
|
|||||||
frequent: run the job under a prebuilt Android-SDK `container:` image so nothing installs per-run.)
|
frequent: run the job under a prebuilt Android-SDK `container:` image so nothing installs per-run.)
|
||||||
- **PR gate** (`.gitea/workflows/pr-checks.yml`, on PR → `main`): `./gradlew lint test assembleDebug`.
|
- **PR gate** (`.gitea/workflows/pr-checks.yml`, on PR → `main`): `./gradlew lint test assembleDebug`.
|
||||||
Debug builds are auto-signed, so the gate needs no secrets. Mirrors `website/`'s pre-merge gate.
|
Debug builds are auto-signed, so the gate needs no secrets. Mirrors `website/`'s pre-merge gate.
|
||||||
- **Release** (`.gitea/workflows/release.yml`, M6+): build a **signed release APK** and attach it to a
|
- **Release** (`.gitea/workflows/release.yml`, M6): mirrors `link/`'s release engine — on every push to
|
||||||
Gitea release (mirrors `link/`'s release job). The **keystore is a base64 Gitea Actions secret**
|
`main` it computes the next version from conventional commits since the last `v*` tag, generates a
|
||||||
decoded in CI; store/key passwords are secrets. The keystore never lives in the repo. Keep the
|
changelog, bumps `build.gradle.kts`, builds a **signed release APK**, commits the bump `[skip ci]`,
|
||||||
signing identity stable from the first release (Play later requires consistency).
|
tags `vX.Y.Z`, and creates the Gitea release with the notes + APK + `SHA256SUMS`. The **keystore is a
|
||||||
- Semantic `versionName` + monotonic `versionCode`; tag releases.
|
base64 Gitea Actions secret** decoded in CI (`ANDROID_KEYSTORE_BASE64`); store/key passwords + alias
|
||||||
|
are secrets too. The keystore never lives in the repo. `REGISTRY_USER`/`REGISTRY_TOKEN`
|
||||||
|
(`write:repository`) push the bump + create the release, so `main` must allow that account to push.
|
||||||
|
Keep the signing identity stable from the first release (Play later requires consistency).
|
||||||
|
- Semantic `versionName` (bumped by the engine) + derived monotonic `versionCode`
|
||||||
|
(`major*10000+minor*100+patch`); the engine tags each release.
|
||||||
|
|
||||||
## 13. Open questions (revisit as we go)
|
## 13. Open questions (revisit as we go)
|
||||||
|
|
||||||
@@ -555,8 +633,8 @@ password+TOTP only, with registration/invite/reset/SSO **handled by the website*
|
|||||||
reset built on backend + web first**, before app work (§8); minSdk 29, compile/target 35 (§2); no
|
reset built on backend + web first**, before app work (§8); minSdk 29, compile/target 35 (§2); no
|
||||||
telemetry in v1 (§2); strings externalized from day one, English-only bundled (§2); **text-only** game
|
telemetry in v1 (§2); strings externalized from day one, English-only bundled (§2); **text-only** game
|
||||||
data in v1, pretty paperdoll is future (§6.3); **no offline cache in v1** (§7); push via self-hosted
|
data in v1, pretty paperdoll is future (§6.3); **no offline cache in v1** (§7); push via self-hosted
|
||||||
ntfy / UnifiedPush (§11); **biometric app-lock deferred to M6** (tokens already encrypted at rest, so
|
ntfy / UnifiedPush (§11); **biometric app-lock descoped from v1** (tokens already encrypted at rest, so
|
||||||
it is opt-in UX, not a v1 requirement — decided at M3).
|
it is a UX convenience, not a v1 requirement — deferred at M3, descoped at M6; revisit only if requested).
|
||||||
|
|
||||||
**Still open:**
|
**Still open:**
|
||||||
- **App identity / domain.** Target application ID **`com.runicgateway.app`** — pending securing the
|
- **App identity / domain.** Target application ID **`com.runicgateway.app`** — pending securing the
|
||||||
|
|||||||
98
android/theme-plan.md
Normal file
98
android/theme-plan.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Android theme plan — mirroring the website frontend
|
||||||
|
|
||||||
|
This is a summary of the **website frontend theme** (source of truth:
|
||||||
|
`website/client/src/styles/theme.css`, applied at runtime by
|
||||||
|
`website/client/src/contexts/SiteContext.jsx`) so the native Android client can
|
||||||
|
present a visually consistent brand. Where the web uses CSS custom properties,
|
||||||
|
the Android equivalent is a Compose `MaterialTheme` `ColorScheme` + `Typography`.
|
||||||
|
|
||||||
|
## Overall character
|
||||||
|
|
||||||
|
A **dark, moody, "arcane fantasy" theme** — deep blue-black backgrounds, muted
|
||||||
|
slate-blue accent, parchment-white text, and an engraved serif display face. It
|
||||||
|
reads like a leather-and-moonlight fantasy ledger, not a bright consumer app.
|
||||||
|
There is **no light mode** on the web; the app should ship dark-only to match.
|
||||||
|
|
||||||
|
## Color tokens
|
||||||
|
|
||||||
|
The web theme is a flat set of CSS variables under `:root`. Map them to Compose
|
||||||
|
as follows (hex is authoritative):
|
||||||
|
|
||||||
|
| Web token | Hex | Role | Compose slot (suggested) |
|
||||||
|
|-------------------|------------|----------------------------------------|-------------------------------|
|
||||||
|
| `--bg` | `#0e1318` | App background | `background` |
|
||||||
|
| `--bg-deep` | `#0b0f14` | Deepest surface / on-accent text | `surfaceDim` / `onPrimary` |
|
||||||
|
| `--panel-a` | `#192231` | Card gradient top | `surface` |
|
||||||
|
| `--panel-b` | `#141a21` | Card gradient bottom | `surfaceContainer` |
|
||||||
|
| `--panel-flat` | `#11161d` | Flat panels, toolbars | `surfaceContainerLow` |
|
||||||
|
| `--line` | `#2a3544` | Borders / dividers | `outline` |
|
||||||
|
| `--line-soft` | `#1d2733` | Subtle row dividers | `outlineVariant` |
|
||||||
|
| `--accent` | `#7f99bd` | **Primary accent** (brand-overridable) | `primary` |
|
||||||
|
| `--accent-bright` | `#cdd9e8` | Primary button fill, active states | `primaryContainer` / bright |
|
||||||
|
| `--ink` | `#eef3f8` | Highest-contrast text | `onBackground` |
|
||||||
|
| `--head` | `#e6edf6` | Headings | heading color |
|
||||||
|
| `--text` | `#c4cdd8` | Body prose | `onSurface` |
|
||||||
|
| `--muted` | `#aeb8c4` | Secondary text | `onSurfaceVariant` |
|
||||||
|
| `--dim` | `#6f7d8e` | Meta / captions / placeholders | dim / disabled text |
|
||||||
|
| `--blue` | `#13243c` | Accent hover/active background | `secondaryContainer` |
|
||||||
|
| `--mode-live` | `#5fb98a` | "Shard live" status (green) | success |
|
||||||
|
| `--mode-maint` | `#e6c26a` | "Maintenance" status (amber) | warning |
|
||||||
|
|
||||||
|
### Semantic / status colors (used in badges, diffs, moderation)
|
||||||
|
|
||||||
|
- **Success / published / live:** green `#5fb98a` (fills at ~16–22% alpha, text `#7fd0a4`).
|
||||||
|
- **Warning / maintenance / moderation (kick/mute/warn):** amber `#e0b070` / `#e6c26a`.
|
||||||
|
- **Danger / ban / red-link / errors:** desaturated red `#d98b84` (borders `#6e3b38`).
|
||||||
|
- **Admin badge:** near-white `#d8e2ef` on `#3a4a5e`.
|
||||||
|
|
||||||
|
## Branding is data, not code
|
||||||
|
|
||||||
|
The `--accent` value is **overridden at runtime** per shard instance. On the web,
|
||||||
|
`SiteContext` reads `brand.accent` from the site settings API and sets the CSS
|
||||||
|
variable, so one build reskins for any shard. **The Android app should do the
|
||||||
|
same:** fetch the brand payload (name, `accent`, colors, logo/hero/favicon) from
|
||||||
|
the website API and derive the `primary` color at runtime rather than hardcoding
|
||||||
|
`#7f99bd`. Default to `#7f99bd` when the brand payload is absent/offline.
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
Three font families, by role:
|
||||||
|
|
||||||
|
- **Display** (`--display`): **Cinzel**, falling back to Georgia serif — an
|
||||||
|
engraved Roman capitals face used for the logo, `h1`/`.h1`, and prose
|
||||||
|
`h2`/`h3`. Bundle Cinzel as an app font; this face carries the brand.
|
||||||
|
- **Serif body** (`--serif`): **Georgia / Times New Roman** — default body and
|
||||||
|
prose text; `line-height ≈ 1.6`.
|
||||||
|
- **Sans** (`--sans`): **Helvetica Neue / Arial** — UI chrome: buttons, pills,
|
||||||
|
form labels, table headers, badges, meta. Labels/eyebrows/kickers are
|
||||||
|
UPPERCASE with wide letter-spacing (`0.1–0.18em`) and small (0.68–0.86rem).
|
||||||
|
|
||||||
|
Heading scale is fluid on web (`h1` clamps ~2.4–3.6rem); pick fixed Material type
|
||||||
|
scale equivalents (e.g. display for `h1`, headline for `h2`, title for `h3`).
|
||||||
|
|
||||||
|
## Shape, elevation & motion
|
||||||
|
|
||||||
|
- **Corners:** cards/panels `10–12px` radius; inputs/small elements `8px`;
|
||||||
|
pills and buttons are **fully rounded** (`999px` / capsule).
|
||||||
|
- **Cards:** vertical gradient `--panel-a → --panel-b`, 1px `--line` border, soft
|
||||||
|
drop shadow (`0 14px 34px rgba(0,0,0,0.3)`). On hover the web lifts `-3px` and
|
||||||
|
brightens the border to `--accent` — translate to a pressed/focused accent
|
||||||
|
border on Android.
|
||||||
|
- **Buttons:** primary = bright fill (`--accent-bright`) with dark text;
|
||||||
|
ghost/secondary = translucent dark fill with accent-on-hover border.
|
||||||
|
- **Motion:** short, subtle transitions (0.12–0.18s). Keep animations understated.
|
||||||
|
|
||||||
|
## Signature accents (nice-to-have)
|
||||||
|
|
||||||
|
- The **"moon"** motif: a radial-gradient sphere (`#eef3f8 → #9fb0c6 → #5d6e88`) —
|
||||||
|
a small brand flourish worth reproducing.
|
||||||
|
- Accent-tinted focus rings and left-border "note" callouts
|
||||||
|
(`border-left: 3px solid --accent` over a translucent `--blue` background).
|
||||||
|
|
||||||
|
## Implementation note for Compose
|
||||||
|
|
||||||
|
Define one `darkColorScheme(...)` from the table above, a `Typography` binding the
|
||||||
|
three families, and a `Shapes` set (`small = 8.dp`, `medium = 10.dp`, capsule for
|
||||||
|
buttons). Load `accent` from the brand API into a state holder and rebuild the
|
||||||
|
`primary` (and derived `primaryContainer`) at runtime so a shard's custom accent
|
||||||
|
flows through the whole UI — exactly as `SiteContext` does on the web.
|
||||||
Reference in New Issue
Block a user