Skip to content

feat(saves): redesign saves tab with slot-based collapsible layout#220

Merged
danielcopper merged 14 commits intomainfrom
feature/206-saves-tab-redesign
Apr 8, 2026
Merged

feat(saves): redesign saves tab with slot-based collapsible layout#220
danielcopper merged 14 commits intomainfrom
feature/206-saves-tab-redesign

Conversation

@danielcopper
Copy link
Copy Markdown
Owner

@danielcopper danielcopper commented Apr 8, 2026

Summary

Closes #206

  • Redesign saves tab from two-column layout (files left, slots right) to slot-based collapsible panels
  • Extract SavesTab.tsx from RomMGameInfoPanel.tsx (280 lines removed from parent)
  • Active slot expanded at top, inactive slots collapsed with lazy-loading via getSlotSaves
  • Replace colored status dots with colored text labels (Synced, Local changes, Server newer, Conflict)
  • Every slot switch goes through switchSlot with pre-checks (pending uploads, server reachable) and immediate sync
  • Slot switch downloads server saves or deletes local files (fresh start for empty slots) — never uploads
  • Legacy mode (no slot) fully supported: warning banner, file display, other slots visible for switching back
  • Add gear button .gpfocus styling for controller focus visibility
  • Show local/server badge on each slot panel

New backend callables

  • get_slot_saves(rom_id, slot) — fetch server save files for a specific slot (lazy-load on panel expand)
  • switch_slot(rom_id, new_slot) — guarded slot switch with immediate download sync

Frontend

  • New SavesTab.tsx component with SlotPanel sub-components
  • Collapsible panels with expand/collapse state, lazy loading, inline error feedback
  • New types: SlotSaveFile, SlotSavesResponse, SwitchSlotResponse
  • New CSS classes for slot panels in styleInjector.ts

Test plan

  • Switch between named slots — verify server saves download and replace local files
  • Switch to empty/new slot — verify local saves deleted (fresh start)
  • Switch to legacy mode (+ New Slot → empty name) — verify warning banner, no phantom saves from other slots
  • Switch back from legacy to named slot — verify download works
  • Expand inactive slot panel — verify lazy-load of server saves
  • Try switch with pending local changes — verify blocked with error message
  • Try switch with server unreachable — verify blocked with error message
  • Controller navigation — verify gear buttons show focus ring
  • Verify last_sync_check_at updates on every switch

)

Replace the two-column saves tab (files left, slots right) with a
slot-centric collapsible panel layout. Active slot is expanded at the
top, inactive slots are collapsed and lazy-load server saves on expand.

Backend:
- Add get_slot_saves callable for per-slot server save listing
- Add switch_slot callable with guards (pending uploads, server
  reachable) and immediate download sync on slot change
- 12 new tests covering happy paths, error cases, and edge cases

Frontend:
- Extract SavesTab.tsx from RomMGameInfoPanel.tsx (~280 lines removed)
- Collapsible slot panels with active/server/local badges
- Replace colored dots with colored text status labels
- Lazy-load inactive slot details via getSlotSaves
- Inline error feedback on failed slot switches

Also fix gear button focus visibility — use border-color instead of
outline so controller focus highlight matches native Steam buttons.
…focus

The useEffect slots loader used || which coerced null (legacy mode) to
"default", immediately overwriting the user's legacy slot choice.
Changed to ?? to match refreshSlotState which already handled this
correctly.

Also add explicit .gpfocus rule for gear buttons — Steam does not
provide a default focus style for DialogButton.
The slots useEffect used ?? which treats null as "use fallback",
but null is a valid value meaning legacy mode. Changed to explicit
=== undefined check, matching the pattern in refreshSlotState.

Also add !important to gear button .gpfocus border-color and
background to override Steam DialogButton default styles.
…istence

- handleNewSlot now calls switchSlot (not setGameSlot) for both legacy
  and named slots, ensuring pre-checks + immediate sync on every switch
- Legacy slot persisted as "" key in slots dict so it survives
  getSaveSlots refresh cycles
- get_save_slots maps server legacy saves to "" instead of "default"
- switch_slot filter normalizes ""/None for legacy save matching
- onSlotSwitched uses explicit === "" check instead of || for legacy
- Redundant "" panel filtered when already in legacy mode
- Error feedback shown near + New Slot button on switch failure
- Remove dead onNewSlot callback and debug logging
When switching slots, local state must match the new slot's server state:
- New slot has server saves: download them, replace local files
- New slot is empty: delete local save files (fresh start)
- Never upload — saves are not carried between slots

Also fix _check_slot_switch_readiness to not block on never-synced
files (they get deleted during the switch anyway). Update
last_sync_check_at after every switch.
match_local_to_server_saves passed all server saves unfiltered when
active_slot was None (legacy mode), causing saves from named slots
to appear as "Server newer" in the legacy view. Now only matches
saves without a slot assignment in legacy mode.
Legacy mode (active_slot=None) now only matches saves without a slot
assignment. Update tests that expected all saves to match regardless
of slot, and add a test verifying unslotted saves still match.
- Extract computeSyncSummary, renderActiveSlotBody, renderInactiveSlotBody,
  renderDeviceSyncInfo helpers to reduce SlotPanel cognitive complexity
- Extract _SYNC_DISABLED_MSG constant for duplicate literal
- Replace nested ternaries with if/else blocks
- Use Number.isNaN instead of isNaN
- Remove unnecessary non-null assertions
- Refactor negated filter condition to positive form
- Replace negated condition with positive form in pluralization
- Replace nested ternary in bodyChildren with if/else block
@danielcopper danielcopper merged commit ebb4d56 into main Apr 8, 2026
6 checks passed
@danielcopper danielcopper deleted the feature/206-saves-tab-redesign branch April 8, 2026 08:42
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 8, 2026

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.

Improve Saves Tab on GameDetailPage

1 participant