fix(ui): F8-04b lobby — auto-expand first available games sub + hide empty invitations
Two follow-up nits on the F8-04b sidebar: 1. The bare-`lobby` resolver (lobby-screen.svelte) redirected to `games-recruitment` unconditionally on mount. With games already in the player's roster the sidebar then highlighted the wrong sub-page. The resolver now awaits the lobby fan-out + account fetch, then hands off to the same `firstVisibleGamesScreen` helper the sidebar uses — so a fresh entry with games lands on `active-past`, the canonical-order fallback stays `recruitment`. 2. `games-invitations` was unconditionally visible in the sidebar. Now it follows the `active-past` rule: hidden until the pending-invites list reports >=1. The lobby shell's auto-kick effect treats it symmetrically — accepting / declining the last invite moves the player to the next visible sub-page once the fan-out has resolved. Acceptance order in games-invitations-screen.acceptInvite was also swapped to setMyGames-before-removeInvitation: both mutations land in the same microtask, so the new auto-kick sees the freshly added game in `myGames` when invitations drop to zero and routes the player to `active-past` instead of bouncing through `recruitment`. The visibility predicates and canonical order live in the new `src/lib/lobby-nav.ts` pure helper, shared between the sidebar and the resolver so they cannot disagree. Unit tests cover every combination of (hasMyGames, hasInvitations, isPaidOrDev). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// Pure helpers for the lobby `games` submenu.
|
||||
//
|
||||
// Both the sidebar (`lib/screens/lobby-shell.svelte`) and the bare-`lobby`
|
||||
// resolver (`lib/screens/lobby-screen.svelte`) need to agree on which
|
||||
// sub-screen is the "first available" — the one auto-expanded in the
|
||||
// sidebar and the redirect target when the user lands on the bare lobby
|
||||
// alias. Centralising the predicates here keeps the two surfaces from
|
||||
// drifting apart.
|
||||
//
|
||||
// The functions are intentionally pure: they take a resolved snapshot of
|
||||
// lobby / account state, return the visible sub-screen list (in canonical
|
||||
// order) and the first entry, and never touch stores or globals. Callers
|
||||
// derive the state from `lobbyData` / `account` and the DEV-affordances
|
||||
// flag.
|
||||
|
||||
export type GamesSubScreen =
|
||||
| "games-active-past"
|
||||
| "games-recruitment"
|
||||
| "games-invitations"
|
||||
| "games-private-games";
|
||||
|
||||
/**
|
||||
* LobbyNavState is the resolved snapshot of the lobby state the
|
||||
* games-submenu visibility rules depend on. Build it from the live
|
||||
* stores at the call site; the helpers below stay pure on top.
|
||||
*/
|
||||
export interface LobbyNavState {
|
||||
/** Player has at least one own / past game in `lobby.my-games.list`. */
|
||||
readonly hasMyGames: boolean;
|
||||
/** Player has at least one pending invite in `lobby.invites.list`. */
|
||||
readonly hasInvitations: boolean;
|
||||
/** Player is on a paid tier, or the build exposes DEV affordances. */
|
||||
readonly isPaidOrDev: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* visibleGamesSubScreens returns the games-submenu sub-screens that
|
||||
* are currently visible, in canonical display order:
|
||||
*
|
||||
* 1. `games-active-past` — only when the player has games.
|
||||
* 2. `games-recruitment` — always visible.
|
||||
* 3. `games-invitations` — only when the player has pending invites.
|
||||
* 4. `games-private-games` — only when paid-tier or DEV.
|
||||
*
|
||||
* `games-recruitment` is unconditional, so the returned array is
|
||||
* always non-empty.
|
||||
*/
|
||||
export function visibleGamesSubScreens(
|
||||
state: LobbyNavState,
|
||||
): readonly GamesSubScreen[] {
|
||||
const out: GamesSubScreen[] = [];
|
||||
if (state.hasMyGames) out.push("games-active-past");
|
||||
out.push("games-recruitment");
|
||||
if (state.hasInvitations) out.push("games-invitations");
|
||||
if (state.isPaidOrDev) out.push("games-private-games");
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* firstVisibleGamesScreen returns the first entry from
|
||||
* `visibleGamesSubScreens(state)`. Since `games-recruitment` is
|
||||
* unconditional, the result is always defined.
|
||||
*/
|
||||
export function firstVisibleGamesScreen(state: LobbyNavState): GamesSubScreen {
|
||||
return visibleGamesSubScreens(state)[0]!;
|
||||
}
|
||||
Reference in New Issue
Block a user