feat(offline): implicit net-state model, two-tier version gate, unified lobby #249

Merged
developer merged 18 commits from feature/android-native into development 2026-07-13 00:59:16 +00:00
3 changed files with 23 additions and 10 deletions
Showing only changes of commit 49c53794f4 - Show all commits
+14 -8
View File
@@ -59,14 +59,20 @@ The native Android app (`ANDROID_PLAN.md`) is a Capacitor 8 wrapper of the `ui`
On Android **WebView < 140**, `env(safe-area-inset-*)` wrongly reports **0**, so chrome relying on it draws
under the bars and is untappable: the top nav under the clock, and the game's bottom action bar's **centre**
button (Hint) under the home-indicator pill (side buttons still work; `navigation_mode`=2 is gesture nav).
Fix is CSS-only: Capacitor 8's **SystemBars** plugin (built into `@capacitor/core`, `insetsHandling:'css'`
default — no dep, no config) injects correct `--safe-area-inset-*`; consume them as
`--tg-safe-*: var(--safe-area-inset-*, env(safe-area-inset-*, 0px))` (`ui/src/app.css`). **Reproduces ONLY on
WebView < 140** — an emulator whose WebView auto-updated (Play Store) to ≥140 (e.g. Chrome 149) will NOT
show it (env works there), so verify via the injected var, not the visual. **Inspect a live debug WebView**
over CDP: `adb forward tcp:9222 localabstract:$(adb shell cat /proc/net/unix | grep -o
'webview_devtools_remote_[0-9]*' | head -1)`, then Playwright `chromium.connectOverCDP('http://localhost:9222')`
→ `page.evaluate` (read `--safe-area-inset-*` vs `env(...)`).
Fix is CSS-only, in TWO parts: (1) Capacitor 8's **SystemBars** plugin (built into `@capacitor/core`,
`insetsHandling:'css'` default — no dep, no config) injects correct `--safe-area-inset-*`; consume them as
`--tg-safe-*: var(--safe-area-inset-*, env(safe-area-inset-*, 0px))` (`ui/src/app.css`) — fixes any consumer
that ALREADY applies the token (the bottom bars: `Game.svelte`/`Screen.svelte` `--tg-safe-bottom`).
(2) But a consumer that never applied the top inset on the native path is NOT fixed by the token alone — the
**header's** top inset was Telegram-fullscreen-scoped only, so the native header sat under the status bar on
EVERY WebView; it needed its own `.bar { padding-top: calc(var(--safe-area-inset-top, 0px) + 5px) }`
(`Header.svelte`, native-only via the plugin var; tg-fullscreen still overrides via specificity). **Measure
per element, don't eyeball** — a centred title at y=11 under a 54px bar reads as "fine" in a screenshot but
is overlapping; an emulator WebView auto-updated to ≥140 (Chrome 149) also hides the `env()`=0 half (so the
BOTTOM looks fine there while the user's < 140 device overlaps). **Inspect a live debug WebView** over CDP:
`adb forward tcp:9222 localabstract:$(adb shell cat /proc/net/unix | grep -o 'webview_devtools_remote_[0-9]*'
| head -1)`, then Playwright `chromium.connectOverCDP('http://localhost:9222')` → `page.evaluate` (read each
element's `getBoundingClientRect().top`, and `--safe-area-inset-*` vs `env(...)`).
- **`sharp` is whitelisted** in `ui/pnpm-workspace.yaml` (`allowBuilds: sharp: true`) — `@capacitor/assets`
uses it for `pnpm android:assets` (launcher icon/splash); else pnpm 11 raises `ERR_PNPM_IGNORED_BUILDS`.
- **Bundled offline dicts come from the `scrabble-dictionary` release** (`scrabble-dawg-<DICT_VERSION>.tar.gz`,
+4 -2
View File
@@ -441,8 +441,10 @@ airplane mode): cold-boot → offline guest lobby, offline vs_ai played end-to-e
placed "FEZ", the robot replied "NEEDFIRE"), New Game offers both modes. The smoke surfaced a native-chrome
bug on **Android 15+ edge-to-edge** (WebView < 140 reports `env(safe-area-inset-*)`=0 → the top nav draws
under the status bar and the game's centre Hint button under the gesture-nav home indicator) — **fixed** by
consuming the SystemBars core plugin's injected `--safe-area-inset-*` in `app.css` (see `.claude/CLAUDE.md`;
no dep/config — the plugin ships in `@capacitor/core` v8). Remaining for D: the reconcile-online leg
consuming the SystemBars core plugin's injected `--safe-area-inset-*`: the bottom via the `--tg-safe-*` token
(`app.css`), and the header's top inset via a native-only `.bar` rule (`Header.svelte` — its top inset was
Telegram-fullscreen-scoped only, so the token alone did not reach it). No dep/config (the plugin ships in
`@capacitor/core` v8); full story in `.claude/CLAUDE.md`. Remaining for D: the reconcile-online leg
on-device (hits prod — a guest mint) and the deferred local-game-visibility decision (§G). **Verify every
line ref below against current code.**
+5
View File
@@ -101,6 +101,11 @@
align-items: center;
gap: var(--gap);
padding: 5px var(--pad);
/* Clear the top safe area on the native build: under Android 15+ edge-to-edge the WebView draws
behind the status bar, so the header row must drop below it. --safe-area-inset-top is injected by
the Capacitor SystemBars plugin (native only — unset, so 0, on web / PWA / Telegram / VK, which do
not draw behind our header). Telegram fullscreen overrides this with its own nav-band rule below. */
padding-top: calc(var(--safe-area-inset-top, 0px) + 5px);
}
h1 {
font-size: 1.05rem;