feat(ui): app-shell core — single-route dispatcher, route collapse, nav→state

Collapse the game UI to one route (`/`): a screen dispatcher renders
login/lobby/lobby-create/game from `appScreen`/`activeView` state instead of
URL routes. Move screen components to lib/screens & lib/game; the game shell
reads the game id from `appScreen.gameId` and re-inits per-game stores via an
$effect; in-game views render from `activeView`. Flip ~23 goto/href nav sites
to store mutations; drop the `?sidebar=` URL coupling. Auth gate is now
state-based. WIP: browser-history (Back→lobby), restore-validation, the
return-to-lobby button, push deep-links, and the test migration are follow-ups
on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-23 20:04:04 +02:00
parent 182beebcd6
commit b6770d394c
30 changed files with 294 additions and 394 deletions
+2 -3
View File
@@ -27,11 +27,10 @@ absent until Phase 24 wires push-event state.
import TurnNavigator from "./turn-navigator.svelte";
type Props = {
gameId: string;
sidebarOpen: boolean;
onToggleSidebar: () => void;
};
let { gameId, sidebarOpen, onToggleSidebar }: Props = $props();
let { sidebarOpen, onToggleSidebar }: Props = $props();
const gameState = getContext<GameStateStore | undefined>(
GAME_STATE_CONTEXT_KEY,
@@ -69,7 +68,7 @@ absent until Phase 24 wires push-event state.
>
</button>
<ViewMenu {gameId} />
<ViewMenu />
<AccountMenu />
</div>
</header>
+16 -16
View File
@@ -7,21 +7,18 @@ itself is identical. The same component is reused for the mobile
Lists the seven IA destinations: map, tables (sub-list of six
entities), report, battle, mail, ship-class designer, science
designer. Closes on Escape, on outside click, and after a
navigation. Phase 26 introduces the history-mode entry; Phase 35
polishes microcopy.
designer. Each entry mutates `activeView` (the single-URL app-shell
has no per-view routes) and closes the menu. Closes on Escape, on
outside click, and after a selection. Phase 26 introduces the
history-mode entry; Phase 35 polishes microcopy.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { activeView, type GameView } from "$lib/app-nav.svelte";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
import { mailStore } from "$lib/mail-store.svelte";
import { restoreFocus } from "$lib/a11y/restore-focus";
type Props = { gameId: string };
let { gameId }: Props = $props();
const mailUnread = $derived(mailStore.unreadCount);
let open = $state(false);
@@ -40,9 +37,12 @@ polishes microcopy.
open = !open;
}
function go(path: string): void {
function select(
view: GameView,
params: { tableEntity?: string } = {},
): void {
open = false;
void goto(withBase(path));
activeView.select(view, params);
}
function onKeyDown(event: KeyboardEvent): void {
@@ -93,7 +93,7 @@ polishes microcopy.
type="button"
role="menuitem"
data-testid="view-menu-item-map"
onclick={() => go(`/games/${gameId}/map`)}
onclick={() => select("map")}
>
{i18n.t("game.view.map")}
</button>
@@ -105,7 +105,7 @@ polishes microcopy.
type="button"
role="menuitem"
data-testid="view-menu-item-table-{entry.slug}"
onclick={() => go(`/games/${gameId}/table/${entry.slug}`)}
onclick={() => select("table", { tableEntity: entry.slug })}
>
{i18n.t(entry.key)}
</button>
@@ -116,7 +116,7 @@ polishes microcopy.
type="button"
role="menuitem"
data-testid="view-menu-item-report"
onclick={() => go(`/games/${gameId}/report`)}
onclick={() => select("report")}
>
{i18n.t("game.view.report")}
</button>
@@ -124,7 +124,7 @@ polishes microcopy.
type="button"
role="menuitem"
data-testid="view-menu-item-battle"
onclick={() => go(`/games/${gameId}/battle`)}
onclick={() => select("battle")}
>
{i18n.t("game.view.battle")}
</button>
@@ -133,7 +133,7 @@ polishes microcopy.
role="menuitem"
data-testid="view-menu-item-mail"
class="with-badge"
onclick={() => go(`/games/${gameId}/mail`)}
onclick={() => select("mail")}
>
<span>{i18n.t("game.view.mail")}</span>
{#if mailUnread > 0}
@@ -146,7 +146,7 @@ polishes microcopy.
type="button"
role="menuitem"
data-testid="view-menu-item-designer-science"
onclick={() => go(`/games/${gameId}/designer/science`)}
onclick={() => select("designer-science")}
>
{i18n.t("game.view.designer.science")}
</button>