feat(ui): single-URL game app-shell (in-memory screens/views) #35
@@ -3,7 +3,13 @@ declare global {
|
|||||||
const __APP_VERSION__: string;
|
const __APP_VERSION__: string;
|
||||||
|
|
||||||
namespace App {
|
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
|
// in the shell via SvelteKit shallow routing; this module is the source of
|
||||||
// truth, history only mirrors it.
|
// truth, history only mirrors it.
|
||||||
|
|
||||||
|
import { pushState, replaceState } from "$app/navigation";
|
||||||
|
|
||||||
export type AppScreen = "login" | "lobby" | "lobby-create" | "game";
|
export type AppScreen = "login" | "lobby" | "lobby-create" | "game";
|
||||||
|
|
||||||
export type GameView =
|
export type GameView =
|
||||||
@@ -158,6 +160,34 @@ class AppScreenStore {
|
|||||||
this.#gameId = null;
|
this.#gameId = null;
|
||||||
}
|
}
|
||||||
persist();
|
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 LobbyScreen from "$lib/screens/lobby-screen.svelte";
|
||||||
import LobbyCreateScreen from "$lib/screens/lobby-create-screen.svelte";
|
import LobbyCreateScreen from "$lib/screens/lobby-create-screen.svelte";
|
||||||
import GameShell from "$lib/game/game-shell.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>
|
</script>
|
||||||
|
|
||||||
{#if session.status === "authenticated"}
|
{#if session.status === "authenticated"}
|
||||||
|
|||||||
Reference in New Issue
Block a user