docs(ui): sync docs to the app-shell; fix stale nav comments
Tests · UI / test (push) Failing after 9m28s

Rewrite ui/docs (navigation, order-composer, auth-flow, pwa-strategy,
game-state + secondary topic docs) and ui/README for the single-URL
app-shell (in-memory screens/views, Back→lobby via shallow routing,
sessionStorage restore + validation, return-to-lobby). ui/PLAN.md gets a
Phase-10 supersede note (implemented; standalone-compatible). Fix stale
code comments (session-store auth gate, report-sections spec contract).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-23 21:04:11 +02:00
parent 4e0058d46c
commit e31fb2c17a
17 changed files with 453 additions and 262 deletions
+135 -67
View File
@@ -18,6 +18,22 @@ module is a pure compute boundary on every platform.
> realistic multi-turn projection, and the cross-platform acceptance
> pass in [ROADMAP.md](ROADMAP.md). This file is retained as the staged
> record of how the MVP was built.
>
> **Routing — superseded by the app-shell.** After the MVP, the
> URL-based routing the per-phase artifacts below describe was refactored
> into a single-URL **app-shell**: the game UI is one SvelteKit route at
> `/game/`, the screen and the in-game view are in-memory rune state
> (`lib/app-nav.svelte.ts`), the `routes/games/[id]/` subtree and the
> per-view `+page.svelte` wrappers were removed, the in-game layout
> became `lib/game/game-shell.svelte`, and the login / lobby /
> lobby-create screens moved under `lib/screens/`. Browser Back/Forward
> move between screens via shallow routing without changing the URL — a
> model that also suits the bundled standalone targets (Wails /
> Capacitor / gomobile) that have no URLs. The current navigation model
> is described in [docs/navigation.md](docs/navigation.md) and in the
> reframed `Information Architecture and Navigation` section and Phase 10
> decisions below; the per-phase `routes/games/[id]/…` artifact paths are
> left as the historical record of what each phase delivered at the time.
The existing Fyne client in `client/` is deprecated and is not modified
or imported by the new code. The architectural overview is mirrored into
@@ -130,38 +146,55 @@ The intended v1 architecture is:
## Information Architecture and Navigation
The client is a single-page application with **one active view at a
time**. Navigation is mobile-first: floating panels never overlap the
map, the main area never splits into multiple visible panels on small
screens. Desktop and mobile share the same model; on desktop, the
sidebar sits beside the active view, on mobile it lives behind a
bottom-tab bar.
The client is a single-page **app-shell** with **one active view at a
time**. It is served at a single URL (`/game/`) that never changes:
the visible screen and view are in-memory state, not routes. Navigation
is mobile-first: floating panels never overlap the map, the main area
never splits into multiple visible panels on small screens. Desktop
and mobile share the same model; on desktop, the sidebar sits beside
the active view, on mobile it lives behind a bottom-tab bar.
### View model
### Screen and view model
Two pieces of in-memory state (rune singletons in
`lib/app-nav.svelte.ts`) replace what URLs used to encode — `appScreen`
(the top-level screen plus the active game id) and `activeView` (the
in-game view plus its sub-parameters):
```text
ActiveView ∈ {
/login, (anonymous only)
/lobby, (auth required)
/games/:id/map, (default in-game view)
/games/:id/table/:entity, (entity ∈
planets | ship-classes |
ship-groups | fleets |
sciences | races)
/games/:id/report,
/games/:id/battle/:battleId,
/games/:id/mail,
/games/:id/designer/ship-class/:id?,
/games/:id/designer/science/:id?,
appScreen.screen ∈ {
login, (anonymous only)
lobby, (auth required)
lobby-create, (auth required)
game, (auth required; carries appScreen.gameId)
}
activeView.view ∈ { (meaningful only while screen === game)
map, (default in-game view)
table, (+ tableEntity ∈ planets | ship-classes |
ship-groups | fleets | sciences | races)
report,
battle, (+ battleId, turn)
mail,
designer-science, (+ scienceId; absent = new-science form)
}
```
The top-level screen is chosen by the single-route dispatcher
(`routes/+page.svelte`) from `session.status` + `appScreen.screen`;
the in-game shell (`lib/game/game-shell.svelte`) renders the active
view from `activeView`. Browser Back/Forward move between screens
(Back from a game → lobby) via SvelteKit shallow routing, without
changing the URL; in-game view switches do not create history entries.
Switching between views happens through the header dropdown (desktop)
or hamburger menu (mobile). Double-tapping a row in a `table:` view
returns to `/map` with `focus=<objectId>`. Some views can push a
transient map overlay with a back affordance (for example, ship-class
designer pushes a range-preview overlay onto the map). The transient
overlay clears when the user navigates to any other view.
or hamburger menu (mobile), driven by `activeView.select(...)`.
Double-tapping a row in a table view returns to the map focused on the
object. Some views can push a transient map overlay with a back
affordance (for example, ship-class designer pushes a range-preview
overlay onto the map). The transient overlay clears when the user
selects any other view. The implementation is documented in
[docs/navigation.md](docs/navigation.md).
### Layout per breakpoint
@@ -257,12 +290,20 @@ turn current` action.
- The account menu (top-right on desktop, last hamburger entry on
mobile) holds Settings, Sessions, Theme, Language, Logout.
### Authenticated route transitions
### Authenticated screen transitions
- `/login``/lobby` after successful confirm-email-code.
- `/lobby``/games/:id/map` when a game card is selected.
- Any view → `/login` immediately on session revocation push event.
- Designer views can push a transient overlay onto `/map`; the back
All transitions are in-memory screen/view changes; the URL stays
`/game/` throughout.
- login → lobby after successful confirm-email-code (`session.status`
settles to `authenticated`).
- lobby → game (view `map`) when a game card is selected
(`appScreen.go("game", { gameId })`).
- any screen → login immediately on session revocation push event
(`session.status` settles back to `anonymous`).
- the in-game header carries a "return to lobby" control
(`appScreen.go("lobby")`); browser Back from a game does the same.
- Designer views can push a transient overlay onto the map; the back
affordance returns to the originating designer.
Per-screen behaviour (validations, exact field names, error mappings)
@@ -1062,37 +1103,58 @@ end-to-end before any data is wired.
Decisions taken with the project owner during implementation:
1. **Routing — file-system based, no extra dispatcher.** The
"view router" called out in the original artifact list is
implemented as SvelteKit's file-system routes plus thin
`+page.svelte` wrappers that mount the matching
`lib/active-view/<name>.svelte` stub. No separate dispatch
component lives in the codebase; each route file is a two-line
wrapper.
2. **Optional designer ID segments.** Both designer URLs ship as
`[[id]]` optional segments
(`designer/ship-class/[[classId]]/`,
`designer/science/[[scienceId]]/`) so Phase 18 / 21 can read
the param without a routing migration. Phase 10 stubs ignore
the param.
3. **Battle URL — optional id.** `battle/[[battleId]]/` accepts
both the list URL (`/battle`) and a specific battle URL
(`/battle/<id>`). Phase 27 keeps the optional segment and
switches behaviour based on presence.
1. **Routing — single-URL app-shell, in-memory dispatch.** The game
UI is one SvelteKit route served at `/game/`; the address bar never
changes. The "view router" called out in the original artifact list
is the in-memory dispatch in `lib/game/game-shell.svelte` — an
`{#if}` ladder over `activeView.view` that mounts the matching
`lib/active-view/<name>.svelte` stub. The top-level screen
(login / lobby / lobby-create / game) is chosen by the single-route
dispatcher `routes/+page.svelte` from `session.status` +
`appScreen.screen`. Both `appScreen` and `activeView` are rune
singletons in `lib/app-nav.svelte.ts`; there are no per-screen or
per-view file routes (only the dev/test `/__debug/*` ones remain).
Screen-level browser history (Back → lobby) is layered on top via
SvelteKit shallow routing (`pushState`/`replaceState` + `page.state`)
so the URL stays `/game/`. This single-URL model is also the natural
fit for the deferred standalone wrappers (Wails desktop, Capacitor /
gomobile mobile in [ROADMAP.md](ROADMAP.md)), which load a single
bundled `index.html` with no URLs or history. See
[docs/navigation.md](docs/navigation.md).
> This decision supersedes the original "file-system routes plus
> thin `+page.svelte` wrappers" plan. The app-shell transition was
> implemented after the MVP phases: the `routes/games/[id]/`
> subtree and the per-view route wrappers were removed, the layout
> became `lib/game/game-shell.svelte`, and the login / lobby /
> lobby-create screens moved under `lib/screens/`. The
> `lib/active-view/*` components are unchanged — only how they are
> mounted changed.
2. **In-game view sub-parameters — `activeView` state, not URL
segments.** What were optional URL segments are now optional fields
on `activeView` state: the science designer reads `scienceId`
(absent = new-science form), the battle view reads `battleId`
(empty = list) and `turn`, and the table view reads `tableEntity`.
Later phases set these through `activeView.select(view, params)`
instead of navigating a URL.
3. **Battle view — optional id.** The battle view accepts both the
list state (no `battleId`) and a specific battle (`battleId` set).
Phase 27 keeps the optional sub-param and switches behaviour based
on presence.
4. **Tablet sidebar — click toggle, not swipe.** The 7681024 px
tablet sidebar slides in from a header-button click rather
than the IA section's swipe-from-right gesture. The structural
breakpoint switch satisfies Phase 10's acceptance criterion;
Phase 35 polish lands the swipe gesture.
5. **Mobile tool overlay — `mobileTool` state, gated by URL.**
The mobile bottom-tabs Calc / Order navigate to `/map` and
set a layout-owned `mobileTool` rune. The layout's derived
`effectiveTool` only honours the rune when the URL is `/map`,
so navigating to any other view via the More drawer or the
header view-menu naturally drops the overlay. The desktop
sidebar separately accepts a `?sidebar=calc|inspector|order`
URL param that seeds the initial tab on first mount, used by
later phases that want to land directly on a particular tool.
5. **Mobile tool overlay — `mobileTool` state, gated by active view.**
The mobile bottom-tabs Calc / Order select the map view and
set a shell-owned `mobileTool` rune. The shell's derived
`effectiveTool` only honours the rune while `activeView.view ===
"map"`, so selecting any other view via the More drawer or the
header view-menu naturally drops the overlay. The sidebar tool
state is pure in-memory rune state — there is no `?sidebar=` URL
param (the app-shell carries no per-screen URL); the sidebar opens
on its `inspector` default and external events flip the tab.
6. **Sidebar tool filenames — `*-tab.svelte`.** Phase 12 / 13 / 30
each name their final implementation
(`order-tab.svelte`, `inspector-tab.svelte`,
@@ -1103,11 +1165,16 @@ Decisions taken with the project owner during implementation:
name is the static `race ?` string from i18n, mirroring the
spec's static `turn ?` placeholder. Phase 11 wires both from
`user.games.report` data through `lib/header/turn-counter.svelte`.
8. **Auth gate inherited.** The root `+layout.svelte` already
redirects `anonymous → /login`; the in-game shell needs no
extra guard. Phase 10 verified this by booting the e2e shell
spec via `__galaxyDebug.setDeviceSessionId` and observing the
post-`session.init` `authenticated` status.
8. **Auth gate — state-based in the dispatcher.** The single-route
dispatcher (`routes/+page.svelte`) renders the login screen for an
`anonymous` session and the authenticated screens for an
`authenticated` one; there is no `goto` redirect (the app-shell
stays at `/game/`). The in-game shell needs no extra guard. Phase 10
verified the gate by booting the e2e shell spec via
`__galaxyDebug.setDeviceSessionId` and observing the
post-`session.init` `authenticated` status. (Originally the gate was
a `goto("/login")` redirect in the root layout; the app-shell
transition replaced it with state-based rendering.)
9. **More drawer mirrors the view-menu.** The mobile bottom-tabs
"More" drawer renders the same seven destinations as the
header view-menu. The IA section's narrower More list (Mail,
@@ -1136,9 +1203,11 @@ Artifacts (delivered):
`i18n.setLocale`; Logout calls `session.signOut("user")`)
- `ui/frontend/src/lib/sidebar/{sidebar, tab-bar, calculator-tab,
inspector-tab, order-tab, bottom-tabs}.svelte` — three-tab
sidebar with `inspector` default and `?sidebar=` URL seed;
mobile-only bottom-tabs with `[Map, Calc, Order, More]` plus a
More drawer duplicating the view-menu destinations
sidebar with `inspector` default (the app-shell transition later
dropped the original `?sidebar=` URL seed — there is no per-screen
URL to carry it); mobile-only bottom-tabs with
`[Map, Calc, Order, More]` plus a More drawer duplicating the
view-menu destinations
- `ui/frontend/src/lib/sidebar/types.ts` — shared `SidebarTab`
and `MobileTool` types
- `ui/frontend/src/lib/active-view/{map, table, report, battle,
@@ -1173,7 +1242,7 @@ Targeted tests (delivered):
view-menu navigation to every IA destination, account-menu
Logout / Language wiring);
- Vitest component tests for the sidebar (default tab, switching,
empty-state copy, `?sidebar=` URL seed, close button);
empty-state copy, close button);
- Vitest component tests for every active-view stub (title,
`coming soon` copy, table-entity prop, battle-id prop);
- Playwright e2e: visit every view stub via header dropdown and
@@ -1430,8 +1499,7 @@ Artifacts (delivered):
`tab-bar.svelte`, `bottom-tabs.svelte` — `historyMode` prop on
the sidebar forwards to `hideOrder` on tab-bar / bottom-tabs;
active-tab `order` is reset to `inspector` if the flag flips
on, and the `?sidebar=order` URL seed falls back to
`inspector` while the flag is true.
on while it is selected.
- `ui/frontend/src/routes/games/[id]/+layout.svelte` —
instantiates `OrderDraftStore`, sets context, runs
`init({ cache, gameId })` next to `gameState.init` through