Migrate UI from vanilla DOM to Svelte components - #9
Merged
Conversation
Replace the hand-written innerHTML UI (src/ui/ui.ts, src/replay/replayui.ts) with a Svelte app mounted on #ui and driven by stores. The game engine (Three.js render loop, netcode, replay player) is untouched: the UI / ReplayLibraryUI / ReplayViewerUI classes keep their exact method signatures and now write stores instead of building HTML, so GameClient and ReplayPlayer call them unchanged. No Threlte — Svelte is only for the DOM overlay. - Scaffold Svelte via @sveltejs/vite-plugin-svelte + svelte-check (gates typecheck/build); every component is <script lang="ts">. - v2 look: Lilita One + Rubik self-hosted as woff2 (no CDN), Xelu CC0 key prompts under public/assets/keys, refined pink/gold/navy palette in theme.css. - Screens/modals: menu, lobby, HUD, match-end, settings, input-mode prompt, replay library, replay viewer bar, and a richer export modal (draggable trim in/out, KO marks, preset ranges, filename, live preview, progress). - Preserve the full test/DOM contract (ids, classes, data-*, window.__* hooks, localStorage keys). Update two lobby assertions for deliberate v2 changes (host crown vs star, "share it") and scope the bot-tag count to the lobby roster now that the HUD scoreboard also tags bots. Credit Xelu (CC0 keys) and the OFL fonts in CREDITS.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THjy3Zddqoam7SJaSAcJVw
The "consecutive hits build the combo counter" spec drives two light attacks in-page and waits for gc.combo >= 2. Under the full parallel suite (16 files sharing the CPU) the client render/predict loop slows enough that the old 8 s window occasionally isn't long enough for both hits to land; the same test passes reliably in isolation. Widen the in-page window to 20 s (still far under the 120 s per-test timeout). Pure test robustness — no product change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THjy3Zddqoam7SJaSAcJVw
svelte-check (0 → 0 errors, 13 → 0 warnings): - Settings/ExportModal seeded local $state from props at the top level, which Svelte flagged as "only captures the initial value". These modals are remounted per open, so a one-time snapshot is intended — make that explicit by reading the props inside untrack(). - ExportModal's `strip` bind:this ref is now $state (was flagged non-reactive). Vite bundler (3 → 0 warnings), which also improves the output: - Extract downloadBlob/replayFilename into replay/download.ts so gameclient no longer imports replayui; replayui re-exports them (main.ts unchanged). Kills the "replayui dynamically + statically imported" split warning. - replayui now imports export.ts type-only and pulls ExportCancelled lazily in the one catch that needs it (webCodecsAvailable inlined), so player.ts's dynamic import actually code-splits export.ts (own 3.4 kB chunk). - Give three.js its own vendor chunk and raise chunkSizeWarningLimit above it. Main app chunk drops 793 kB → 184 kB; three is a separate long-cached 604 kB. Behavior is unchanged; typecheck and vite build are clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THjy3Zddqoam7SJaSAcJVw
The previous commit code-split export.ts, turning player.startExport's
`await import("./export")` into a real chunk fetch. The replay keeps
auto-playing during that fetch, so exportVideo captured a resume-tick that had
already drifted from where the caller read playheadTick — the
13-replay-export "restores the viewer" check failed (playheadRestored=false).
Import export.ts statically from player.ts (and ExportCancelled statically in
replayui) so it stays in the main chunk with no async gap, and make startExport
synchronous. mp4-muxer is still lazy-loaded inside export.ts, so the heavy
encoder stays split. `bun run build` stays 0 warnings; replay specs 12 + 13 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01THjy3Zddqoam7SJaSAcJVw
Replays no longer age out: saveReplay drops the evictOverCap call, and both evictOverCap and MAX_REPLAYS are gone, so the "newest 10 unpinned" eviction is removed. Pinning is now purely a favorite marker. Updated the library storage line, the README, and the store e2e spec (14) to assert all 14 saved replays are retained with pinning preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THjy3Zddqoam7SJaSAcJVw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refactored the entire UI layer from imperative DOM manipulation to a reactive Svelte-based architecture. The game engine and replay system remain untouched; UI controllers now write to Svelte stores instead of building innerHTML, and components render the reactive state.
Key Changes
Removed monolithic stylesheet: Deleted
client/src/ui/style.css(1365 lines) and split into:client/src/ui/theme.css— global design tokens, resets, shared "arcade" primitives (buttons, inputs, panels), and animation keyframes<style>blocks in each Svelte fileIntroduced Svelte component architecture:
App.svelte— root layout that switches between screens (menu, connecting, hud, replayLib, replayViewer)Menu.svelte,Hud.svelte,Lobby.svelte,MatchEnd.svelte,Connecting.svelte,ReplayLibrary.svelte,ReplayViewerBar.svelteSettings.svelte,InputModePrompt.svelte,ExportModal.svelteIcon.svelte,Keycap.svelte,BotTag.svelte,ControlsHint.svelte,HudFx.svelteCreated reactive state boundary (
client/src/ui/app/stores.ts):UI,ReplayLibraryUI,ReplayViewerUI) maintain their original method signatures but now write to stores instead of manipulating DOMRefactored UI controllers:
client/src/ui/ui.ts— translatesGameClientcalls into store writes; keepsshowMenu(),buildHud(),setDamage(), etc.client/src/replay/replayui.ts— translatesReplayPlayercalls into store writes; keepsshow(),mount(),tick(), etc.Extracted utilities:
client/src/ui/util.ts—colorOf()andesc()helpers (moved fromui.ts)client/src/ui/app/actions.ts— Svelte actions for animation retriggering and focus managementclient/src/ui/icons.ts; SVG icons now live inIcon.svelteAdded self-hosted fonts:
client/public/assets/fonts/@font-faceintheme.cssfor offline supportAdded keycap assets:
client/public/assets/keys/Keycap.sveltecomponent for control hintsUpdated build configuration:
svelte.config.jswith vitePreprocessvite.config.tsto include@sveltejs/vite-plugin-sveltetsconfig.jsonto include.sveltefilesindex.htmlto remove old stylesheet link and mount Svelte apppackage.jsonwith Svelte dependenciesImplementation Details
GameClient,ReplayPlayer) never touches the DOM; they call the same public methods on UI controllers, which now translate to store writeshttps://claude.ai/code/session_01THjy3Zddqoam7SJaSAcJVw