Skip to content

Migrate UI from vanilla DOM to Svelte components - #9

Merged
Advik-B merged 5 commits into
masterfrom
claude/peaceful-brahmagupta-9anwg6
Jul 16, 2026
Merged

Migrate UI from vanilla DOM to Svelte components#9
Advik-B merged 5 commits into
masterfrom
claude/peaceful-brahmagupta-9anwg6

Conversation

@Advik-B

@Advik-B Advik-B commented Jul 15, 2026

Copy link
Copy Markdown
Owner

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
    • Component-scoped <style> blocks in each Svelte file
  • Introduced Svelte component architecture:

    • App.svelte — root layout that switches between screens (menu, connecting, hud, replayLib, replayViewer)
    • Screen components: Menu.svelte, Hud.svelte, Lobby.svelte, MatchEnd.svelte, Connecting.svelte, ReplayLibrary.svelte, ReplayViewerBar.svelte
    • Modal components: Settings.svelte, InputModePrompt.svelte, ExportModal.svelte
    • Reusable UI components: Icon.svelte, Keycap.svelte, BotTag.svelte, ControlsHint.svelte, HudFx.svelte
  • Created reactive state boundary (client/src/ui/app/stores.ts):

    • Writable stores for screen state, menu, overlay (lobby/matchend), HUD values, replay viewer/library state
    • Controllers (UI, ReplayLibraryUI, ReplayViewerUI) maintain their original method signatures but now write to stores instead of manipulating DOM
    • Hot per-frame values (damage, combo, scores) are individual stores that only update on change to minimize reactivity churn
  • Refactored UI controllers:

    • client/src/ui/ui.ts — translates GameClient calls into store writes; keeps showMenu(), buildHud(), setDamage(), etc.
    • client/src/replay/replayui.ts — translates ReplayPlayer calls into store writes; keeps show(), mount(), tick(), etc.
  • Extracted utilities:

    • client/src/ui/util.tscolorOf() and esc() helpers (moved from ui.ts)
    • client/src/ui/app/actions.ts — Svelte actions for animation retriggering and focus management
    • Removed client/src/ui/icons.ts; SVG icons now live in Icon.svelte
  • Added self-hosted fonts:

    • Lilita One (display) and Rubik (body) as WOFF2 files in client/public/assets/fonts/
    • Defined via @font-face in theme.css for offline support
  • Added keycap assets:

    • PNG images for keyboard keys and mouse buttons in client/public/assets/keys/
    • Used by Keycap.svelte component for control hints
  • Updated build configuration:

    • Added svelte.config.js with vitePreprocess
    • Updated vite.config.ts to include @sveltejs/vite-plugin-svelte
    • Updated tsconfig.json to include .svelte files
    • Updated index.html to remove old stylesheet link and mount Svelte app
    • Updated package.json with Svelte dependencies

Implementation Details

  • Engine isolation: Game logic (GameClient, ReplayPlayer) never touches the DOM; they call the same public methods on UI controllers, which now translate to store writes
  • Animation retriggering: CSS keyframe animations (combo-pop, dmg-pop, flash-out, etc.) are retriggered by Svelte actions that watch store changes
  • Design tokens: All colors, fonts, and spacing are CSS custom properties in

https://claude.ai/code/session_01THjy3Zddqoam7SJaSAcJVw

claude added 5 commits July 15, 2026 20:07
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
@Advik-B
Advik-B merged commit c49d23c into master Jul 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants