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
@@ -1,15 +1,14 @@
<!--
Phase 27 Report View — battles section. Each row is a link into the
Battle Viewer at `/games/<id>/battle/<uuid>?turn=<turn>` where
`turn` follows the current report's turn so history-mode views land
on the right battle. Phase 23 rendered the same rows as inactive
Phase 27 Report View — battles section. Each row opens the Battle
Viewer through `activeView.select("battle", { battleId, turn })`
where `turn` follows the current report's turn so history-mode views
land on the right battle. Phase 23 rendered the same rows as inactive
monospace `<span>`; the rewire here is the one-liner the Phase 23
decision log called out.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { getContext } from "svelte";
import { page } from "$app/state";
import { activeView } from "$lib/app-nav.svelte";
import { i18n } from "$lib/i18n/index.svelte";
import {
@@ -22,8 +21,11 @@ decision log called out.
);
const report = $derived(rendered?.report ?? null);
const battles = $derived(report?.battles ?? []);
const gameId = $derived(page.params.id ?? "");
const turn = $derived(report?.turn ?? 0);
function openBattle(battleId: string): void {
activeView.select("battle", { battleId, turn });
}
</script>
<section
@@ -46,12 +48,13 @@ decision log called out.
<span class="label">
{i18n.t("game.report.section.battles.id_label")}
</span>
<a
<button
type="button"
class="uuid"
href={withBase(`/games/${gameId}/battle/${b.id}?turn=${turn}`)}
onclick={() => openBattle(b.id)}
data-testid="report-battle-row"
data-id={b.id}
>{b.id}</a>
>{b.id}</button>
</li>
{/each}
</ul>
@@ -90,10 +93,15 @@ decision log called out.
font-size: 0.7rem;
}
.uuid {
padding: 0;
border: 0;
background: transparent;
font: inherit;
color: var(--color-accent);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
}
.uuid:hover {
color: var(--color-text);