feat(ui): single-URL game app-shell (in-memory screens/views) #35
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user