feat(ui): lobby site-style sidebar + profile screen (#47)
- Wrap lobby and profile in a shared `lobby-shell.svelte` chrome: page-list sidebar (Overview/Profile) and a top "Player-xxxx" identity strip mirroring the project site's monospace look. - Strip the legacy `lobby.title`, device-session-id `<code>`, and `lobby.greeting` paragraph; the identity strip both names the user and opens the profile editor. - Add a top-level `profile` AppScreen with a three-field form (`display_name`, `preferred_language`, `time_zone`) backed by a new `src/api/account.ts` wrapper around `user.account.get`, `user.profile.update`, and `user.settings.update`. Saving switches the active i18n locale in-place when the new preferred language is one the UI ships translations for. - Update e2e fixture + auth-flow / lobby-flow specs to use the new `lobby-account-name` testid and wait for the loaded identity before releasing pending `SubscribeEvents` (webkit revocation race). New `profile-screen.spec.ts` covers navigation, edit-save, and cancel. - Sync `ui/docs/lobby.md` and `ui/docs/navigation.md` to the new layout. Closes #47
This commit is contained in:
+52
-11
@@ -2,16 +2,36 @@
|
||||
|
||||
The lobby is the first authenticated view; the user lands here after
|
||||
the email-code login completes (see
|
||||
[`docs/auth-flow.md`](auth-flow.md)). This doc captures the
|
||||
sections, the application / invite lifecycle the user sees, and
|
||||
the defaults baked into the create-game form.
|
||||
[`docs/auth-flow.md`](auth-flow.md)). This doc captures the shared
|
||||
shell, the Overview sections, the profile sub-screen, and the
|
||||
defaults baked into the create-game form.
|
||||
|
||||
## Sections
|
||||
## Shell
|
||||
|
||||
The lobby renders one column of sections, top to bottom, with the
|
||||
common content max-width capped at `32rem` (same convention as the
|
||||
login page). Cards inside each section take the full available
|
||||
width.
|
||||
Lobby and profile share a single chrome implemented in
|
||||
`lib/screens/lobby-shell.svelte`. The chrome mirrors the project
|
||||
site's VitePress layout: a left page-list sidebar (Overview /
|
||||
Profile), a top identity strip on the right, and the page content in
|
||||
the right-hand column. The shell uses `var(--font-mono)` so the
|
||||
post-login pages adopt the "nerdy" type stack that the public site
|
||||
already uses.
|
||||
|
||||
The identity strip renders the caller's `display_name` (falling back
|
||||
to the immutable `user_name` handle, then to a loading placeholder
|
||||
while `user.account.get` resolves) as a `data-testid="lobby-account-name"`
|
||||
button. Clicking it switches the top-level screen to `profile`
|
||||
(`appScreen.go("profile")`); the e2e suites use that testid as their
|
||||
lobby-loaded signal. The logout button sits next to it
|
||||
(`session.signOut("user")`).
|
||||
|
||||
The sidebar always renders both pages; clicking the active page is a
|
||||
no-op. The shell collapses to a horizontal scrolling strip below
|
||||
640px.
|
||||
|
||||
## Overview sections
|
||||
|
||||
The Overview page renders one column of sections, top to bottom.
|
||||
Cards inside each section take the full available width.
|
||||
|
||||
| Section | Empty state | Source | Action |
|
||||
| -------------------- | --------------------- | -------------------------- | --------------------------------------------------------- |
|
||||
@@ -21,9 +41,30 @@ width.
|
||||
| `my applications` | `no applications` | `lobby.my.applications.list` | Status badge (`pending` / `approved` / `rejected`) |
|
||||
| `public games` | `no public games` | `lobby.public.games.list` | Submit application via inline race-name form (`lobby.application.submit`) |
|
||||
|
||||
The header preserves the device-session-id `<code>` block (kept as
|
||||
a debug affordance) plus a greeting if the gateway returns a
|
||||
`display_name` for the caller.
|
||||
## Profile sub-screen
|
||||
|
||||
`lib/screens/profile-screen.svelte` is a top-level `AppScreen` (peer of
|
||||
`lobby` and `lobby-create`). The browser Back stack treats it the
|
||||
same as the create screen — pushing a fresh history entry on entry,
|
||||
falling back to lobby on Back/Forward (see
|
||||
[`navigation.md`](navigation.md)).
|
||||
|
||||
On mount it issues `user.account.get` through `src/api/account.ts`
|
||||
and renders an identity read-out (immutable `user_name`, `email`)
|
||||
plus a three-field form:
|
||||
|
||||
| Field | Endpoint | Notes |
|
||||
| --------------------- | --------------------- | -------------------------------------------------------------- |
|
||||
| `display_name` | `user.profile.update` | Trimmed; empty value clears the stored name (backend PATCH semantics). |
|
||||
| `preferred_language` | `user.settings.update`| `<select>` over `SUPPORTED_LOCALES`; if the stored value is unsupported the option is preserved verbatim so a round-trip save does not silently switch it. |
|
||||
| `time_zone` | `user.settings.update`| Free-text IANA name. Placeholder shows the browser's current zone; backend validates with `time.LoadLocation`. |
|
||||
|
||||
Save fires `user.profile.update` and/or `user.settings.update`
|
||||
conditionally on which fields actually changed, then returns to the
|
||||
lobby (`appScreen.go("lobby")`). When the saved
|
||||
`preferred_language` is one the UI also ships translations for, the
|
||||
active i18n locale switches in-place so the rest of the session
|
||||
matches the new preference.
|
||||
|
||||
`GameSummary` carries a `current_turn` field that the lobby UI does
|
||||
not display directly — the in-game shell reads it from the same
|
||||
|
||||
+11
-7
@@ -18,9 +18,9 @@ for the whole session. The only other routes are the dev/test-only
|
||||
rune singletons in `src/lib/app-nav.svelte.ts`:
|
||||
|
||||
- **`appScreen`** — the top-level screen
|
||||
(`login` / `lobby` / `lobby-create` / `game`) plus the active
|
||||
`gameId`. It replaces the old `goto`-based redirects and the `[id]`
|
||||
route param.
|
||||
(`login` / `lobby` / `lobby-create` / `profile` / `game`) plus the
|
||||
active `gameId`. It replaces the old `goto`-based redirects and the
|
||||
`[id]` route param.
|
||||
- **`activeView`** — the in-game view (`map` / `table` / `report` /
|
||||
`battle` / `mail` / `designer-science`) plus the sub-parameters the
|
||||
old route segments carried (`tableEntity`, `battleId`, `turn`,
|
||||
@@ -31,8 +31,11 @@ render: it gates on `session.status` (anonymous → login, authenticated
|
||||
→ the `appScreen.screen`), and for the authenticated tree mounts the
|
||||
matching screen component from `src/lib/screens/`
|
||||
(`login-screen.svelte`, `lobby-screen.svelte`,
|
||||
`lobby-create-screen.svelte`) or, for `screen === "game"`, the in-game
|
||||
shell `src/lib/game/game-shell.svelte`. The game shell in turn renders
|
||||
`lobby-create-screen.svelte`, `profile-screen.svelte`) or, for
|
||||
`screen === "game"`, the in-game shell
|
||||
`src/lib/game/game-shell.svelte`. Lobby and profile share a
|
||||
post-login chrome (sidebar + identity strip) implemented in
|
||||
`lib/screens/lobby-shell.svelte`; see [`lobby.md`](lobby.md). The game shell in turn renders
|
||||
the active view from `activeView` (see below). Navigation is
|
||||
`appScreen.go(screen, { gameId })` and `activeView.select(view,
|
||||
params)` — never `goto`.
|
||||
@@ -73,8 +76,9 @@ Browser **Back/Forward move between screens**, not views, and they do
|
||||
so without ever changing the URL. The shell layers screen history on
|
||||
top of `appScreen` via SvelteKit shallow routing: `appScreen.go(...)`
|
||||
calls `pushState("", { screen, gameId })` for the overlay screens
|
||||
(`game`, `lobby-create`) and `replaceState(...)` for `lobby` / `login`,
|
||||
so browser **Back from a game returns to the lobby** beneath it. On
|
||||
(`game`, `lobby-create`, `profile`) and `replaceState(...)` for
|
||||
`lobby` / `login`, so browser **Back from a game (or profile) returns
|
||||
to the lobby** beneath it. On
|
||||
the first authenticated render the dispatcher stamps the restored
|
||||
overlay on top of the load entry, then mirrors `page.state` back into
|
||||
the store on every popstate through `appScreen.syncFromHistory(...)`.
|
||||
|
||||
Reference in New Issue
Block a user