feat(ui): single-URL game app-shell (in-memory screens/views) #35

Merged
developer merged 8 commits from feature/ui-app-shell into development 2026-05-23 20:18:09 +00:00
3 changed files with 62 additions and 1 deletions
Showing only changes of commit be7f06e163 - Show all commits
+7 -1
View File
@@ -3,7 +3,13 @@ declare global {
const __APP_VERSION__: string;
namespace App {
// future-phase types added later
// Shallow-routing state for the single-URL app-shell: the screen
// (and active game) live in `page.state` so browser Back/Forward
// move between screens while the address bar stays at /game/.
interface PageState {
screen?: "login" | "lobby" | "lobby-create" | "game";
gameId?: string | null;
}
}
}
+30
View File
@@ -20,6 +20,8 @@
// in the shell via SvelteKit shallow routing; this module is the source of
// truth, history only mirrors it.
import { pushState, replaceState } from "$app/navigation";
export type AppScreen = "login" | "lobby" | "lobby-create" | "game";
export type GameView =
@@ -158,6 +160,34 @@ class AppScreenStore {
this.#gameId = null;
}
persist();
this.#syncHistory();
}
/**
* syncFromHistory applies a screen restored from browser history (a
* Back/Forward popstate) WITHOUT pushing a new entry. An absent/unknown
* screen (the load entry beneath an overlay) falls back to lobby.
*/
syncFromHistory(screen: AppScreen | undefined, gameId: string | null): void {
const next =
screen !== undefined && APP_SCREENS.includes(screen) ? screen : "lobby";
this.#screen = next;
this.#gameId = next === "game" ? gameId : null;
persist();
}
// Mirror the screen into browser history via shallow routing (the URL is
// unchanged — the address bar stays at /game/). Overlays (game,
// lobby-create) push a new entry so browser Back returns to the lobby
// beneath; lobby/login replace in place.
#syncHistory(): void {
if (typeof window === "undefined") return;
const state: App.PageState = { screen: this.#screen, gameId: this.#gameId };
if (this.#screen === "game" || this.#screen === "lobby-create") {
pushState("", state);
} else {
replaceState("", state);
}
}
}
+25
View File
@@ -12,6 +12,31 @@
import LobbyScreen from "$lib/screens/lobby-screen.svelte";
import LobbyCreateScreen from "$lib/screens/lobby-create-screen.svelte";
import GameShell from "$lib/game/game-shell.svelte";
import { pushState } from "$app/navigation";
import { page } from "$app/state";
// Screen-level browser history (Back → lobby) without changing the URL.
// On the first authenticated render, stamp a restored overlay (game /
// lobby-create) on top of the load entry so Back falls through to lobby.
let historyStamped = $state(false);
$effect(() => {
if (session.status === "authenticated" && !historyStamped) {
historyStamped = true;
if (appScreen.screen === "game" && appScreen.gameId !== null) {
pushState("", { screen: "game", gameId: appScreen.gameId });
} else if (appScreen.screen === "lobby-create") {
pushState("", { screen: "lobby-create" });
}
}
});
// Sync the store from history on Back/Forward (popstate updates
// `page.state`). Skipped until the baseline is stamped so it never
// clobbers the restored screen on first render.
$effect(() => {
if (!historyStamped) return;
appScreen.syncFromHistory(page.state.screen, page.state.gameId ?? null);
});
</script>
{#if session.status === "authenticated"}