feat(ui): screen-level history for the app-shell (Back → lobby)

Mirror the screen into browser history via SvelteKit shallow routing
(pushState/replaceState with page.state) so Back/Forward move between
screens while the URL stays at /game/. Overlays (game, lobby-create) push;
lobby/login replace. A popstate→page.state effect syncs the store back
without re-pushing (no loop); the boot stamp puts a restored overlay above
the load entry so Back falls through to lobby. In-game view switches never
touch history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-23 20:07:03 +02:00
parent b6770d394c
commit be7f06e163
3 changed files with 62 additions and 1 deletions
+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);
}
}
}