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
SubTabtype ('select' | 'artifacts') and theparseTabhelper that coerces a URL query value into a valid sub-tab. - Local
tabstate viauseState, 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 theOverlayBottomBarat 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’suseNavigate(for BACK) anduseSearchParams(for thetabquery param).useWalletStorefrom@starship-survivors/stores/walletStore— selectors forcreditsandgemsrendered in the currency strip.OverlayBottomBarand theOverlayTabtype from@metagame/components/BottomNav.SelectTabfrom./ships/SelectTabandArtifactsTabfrom./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
tabstate changes, replaces the?tab=search param viasetParams(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 fromShipsScreenitself.
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
SelectTabandArtifactsTabrespectively. - Render its own back button (delegated to
OverlayBottomBar’sonBack). - Mutate the wallet —
creditsandgemsare read-only here. - Handle the case where the URL has a
tabvalue other thanselectorartifacts;parseTabcoerces any unknown string toselect.
Signals
- Tab change is the single state signal — flips between
'select'and'artifacts'viasetTabfrom the bottom-bar click handlers. - The
useEffectkeyed on[tab]is the URL-sync signal: if the URL’stabquery 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=selector?tab=artifactsquery 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
useStateinitialized from the URL plus a one-wayuseEffectthat writes state back to the URL on change. Initial URL → state happens once at mount viauseState(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
V32Shellper the inline comment. Styling is inlinestyle={{...}}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: hiddenso the inner scroll surface (owned by each sub-tab) sizes correctly inside the flex column. parseTabis a defensive narrowing — keeps thetabtype tight without throwing on unknown query values.