docs(ui): sync docs to the app-shell; fix stale nav comments
Tests · UI / test (push) Failing after 9m28s
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:
+135
-67
@@ -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 768–1024 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
|
||||
|
||||
Reference in New Issue
Block a user