PURPOSE

Unified Ships / Artifacts screen. Hosts two sub-tabs that share a common layout (config surface on top, inventory picker on the bottom, BACK + sub-tabs in the bottom bar). The SHIPS sub-tab handles ship selection; the ARTIFACTS sub-tab lets the player pick the alien artifact carried into the next run. Mounted at route /games/starship-survivors/ships?tab=select|artifacts, with tab optional and defaulting to select.

OWNS

  • The SubTab type ('select' | 'artifacts') and the parseTab helper that coerces a URL query value into a valid sub-tab.
  • Local tab state via useState, synchronized with the ?tab= URL search param.
  • The screen’s outer chrome: medical-UI background (--med-panel-recessed), the top currency strip (credits + gems), the flex body that renders the active sub-tab, and the OverlayBottomBar at the bottom.
  • The tabs: OverlayTab[] array describing the SHIPS and ARTIFACTS entries (label, emoji icon, click handler, active flag).
  • Default export ShipsScreen (React function component).

READS FROM

  • react-router-dom’s useNavigate (for BACK) and useSearchParams (for the tab query param).
  • useWalletStore from @starship-survivors/stores/walletStore — selectors for credits and gems rendered in the currency strip.
  • OverlayBottomBar and the OverlayTab type from @metagame/components/BottomNav.
  • SelectTab from ./ships/SelectTab and ArtifactsTab from ./ships/ArtifactsTab — the two sub-tab bodies.
  • Medical-UI CSS custom properties: --med-panel-recessed, --med-panel-raised, --med-text, --med-border, --med-border-strong, --med-amber, --med-magenta.

PUSHES TO

  • The URL: when local tab state changes, replaces the ?tab= search param via setParams(next, { replace: true }) to keep deep-links and browser back/forward in sync.
  • Router navigation: BACK in the bottom bar navigates to /.
  • The two sub-tab components (SelectTab, ArtifactsTab) mount/unmount based on the active tab; no props are passed from ShipsScreen itself.

DOES NOT

  • Persist tab state across mounts beyond the URL — there is no localStorage, store write, or server call from this file.
  • Own ship-selection logic, artifact-selection logic, inventory queries, or purchase flows. Those live inside SelectTab and ArtifactsTab respectively.
  • Render its own back button (delegated to OverlayBottomBar’s onBack).
  • Mutate the wallet — credits and gems are read-only here.
  • Handle the case where the URL has a tab value other than select or artifacts; parseTab coerces any unknown string to select.

Signals

  • Tab change is the single state signal — flips between 'select' and 'artifacts' via setTab from the bottom-bar click handlers.
  • The useEffect keyed on [tab] is the URL-sync signal: if the URL’s tab query value differs from local state, it writes local state back into the URL (replace, not push). The eslint hooks-exhaustive-deps rule is intentionally suppressed.
  • BACK → navigate('/') is the only outbound route signal.

Entry points

  • Mounted by the metagame router at /games/starship-survivors/ships. Accepts an optional ?tab=select or ?tab=artifacts query string for deep-linking.
  • Default-exported, so consumers import ShipsScreen from '.../ShipsScreen'.
  • The component takes no props.

Pattern notes

  • Sub-tab pattern: parent owns chrome (currency strip, bottom bar, layout), children own content. Both children share the “config surface on top, inventory picker on the bottom” convention, but each owns its own internal split.
  • URL-state sync uses useState initialized from the URL plus a one-way useEffect that writes state back to the URL on change. Initial URL → state happens once at mount via useState(parseTab(...)); the effect is the state → URL leg.
  • Currency strip is hand-rolled inline (flex row, two SVG-iconed pills) and mirrors the strip on V32Shell per the inline comment. Styling is inline style={{...}} with medical-UI CSS variables — not Tailwind, not a shared component.
  • Tick 77 medical-UI rollout (PR 3/5) is called out in two comments: the outer container uses the recessed panel token, and the currency strip uses raised panel + medical tokens.
  • Sub-tab body is wrapped in flex: 1; minHeight: 0; overflow: hidden so the inner scroll surface (owned by each sub-tab) sizes correctly inside the flex column.
  • parseTab is a defensive narrowing — keeps the tab type tight without throwing on unknown query values.