PURPOSE

The Select tab of the ship-screen UI. Top half is a hero card showing a live preview of the currently selected ship with its name, star rank, XP-to-next-star bar, headline stats (HP / SHIELD / SPEED), a SPECIALTY placeholder, and the ship’s starting weapon slots. Bottom half is a scrollable inventory grid of owned hulls; tapping a card immediately switches the active ship. Includes a multi-select filter panel (by rarity, by star) and an info popover with the full racing-game stat sheet. Rarity color identity is preserved on the hero frame, weapon-slot rings, filter pills, and title per ADR 20260513-001; all other chrome uses medical-UI tokens.

OWNS

  • The SelectTab exported component (top-level render).
  • Local UI state: filterOpen (panel toggle), rarityFilter (Set of ShipRarity), starFilter (Set of star numbers 1-5), infoOpen (ship info popover toggle).
  • Memoized ownedHulls list — filters owned hulls against active filters then sorts by rarity DESC, star DESC, XP DESC.
  • Filter toggle helpers toggleRarity and toggleStar that immutably update the corresponding Sets.
  • Hero-card layout: live-preview backdrop, info button, title, star/XP bar unit, big-stat column, specialty box, weapon-slot row.
  • Internal helper components: BigStat, FilterRow, FilterPill, WeaponSlotsSection, WeaponSlotCircle, WeaponCardPopover.
  • Internal constants: STAT_ROWS, RARITY_ORDER, RARITY_DISPLAY, WEAPON_RARITY_COLOR, headerStripStyle.
  • The weapon-card popover lifecycle: an off-screen canvas redrawn through drawRewardCard at the exact in-game card dimensions (min(140, screenW * 0.26) wide, 2.1x tall), with a portal mount and an Escape-key close listener.
  • The weapon-slot icon paint cycle: a per-slot canvas sized to device pixel ratio, repainted on mount and at 80ms / 250ms timeouts to catch late-loaded icon images.

READS FROM

  • useSessionStoreselectedShipId, setShip.
  • useInventoryStoreships, currentXp, currentStar, isUnlocked.
  • @starship-survivors/data/shipsHULL_CLASSES, getShipDef, RARITY_COLORS, displayHullName.
  • @starship-survivors/data/ships-v4-rarityShipRarity type.
  • @starship-survivors/data/ship-progressionxpToNextStar.
  • @starship-survivors/data/weaponsWEAPON_MAP, resolveWeaponRarity, WeaponCoreSpec type.
  • @starship-survivors/engine/world/levelingdescribeNewWeapon.
  • @starship-survivors/engine/rendering/weapon-iconsgetWeaponIconImg.
  • @starship-survivors/engine/rendering/huddrawRewardCard.
  • @starship-survivors/components/HullStarBar — star + XP bar visual.
  • ./ShipLivePreview — full-bleed live ship preview canvas inside the hero card.
  • ./ShipInfoPopover — modal stat-sheet popover.
  • @metagame/components/CollectibleCard — inventory grid card.
  • window.innerWidth and window.devicePixelRatio (read in the weapon-card popover and weapon-slot canvas paint).

PUSHES TO

  • useSessionStore.setShip(hull) — called on grid-card click; switches the active ship globally.
  • No other store writes. No network calls. No telemetry calls.

DOES NOT

  • Does not unlock, upgrade, fuse, or otherwise mutate ship inventory state — read-only with respect to useInventoryStore.
  • Does not drag-and-drop. Card selection is single-tap.
  • Does not implement the upgrade / fuse / craft tabs — those are sibling tabs in the parent ship screen.
  • Does not write to localStorage or any persistence layer directly.
  • Does not perform analytics / telemetry / cloud logging.
  • Does not paint the ship preview itself — delegates entirely to ShipLivePreview.
  • Does not compute weapon stats — drawRewardCard and describeNewWeapon produce all weapon-card visuals + text.
  • Does not handle ship-locking — the grid only renders owned hulls (!!ships[hull]) and passes locked={false} to CollectibleCard.
  • Does not animate route transitions or tab transitions.

Signals

  • Grid card click → setShip(hull) on session store; selected hull updates everywhere subscribed to the session store.
  • Info button (the (i) glyph top-right of the hero card) → opens ShipInfoPopover.
  • Filter button toggles the filter panel; pill clicks toggle membership in the corresponding rarity/star Set, which recomputes ownedHulls via memo.
  • Weapon-slot click on a filled slot → opens WeaponCardPopover showing the in-game reward card for that weapon. Empty slots are non-interactive.
  • WeaponCardPopover backdrop click and Escape key both close the popover via onClose.
  • ShipInfoPopover close → resets infoOpen to false.

Entry points

  • Default export: SelectTab named export. Mounted by the parent ship-screen tab router (sibling to Upgrade / Fuse / etc.).
  • No props — relies entirely on useSessionStore and useInventoryStore for state.

Pattern notes

  • Medical-UI token rollout (Tick 99 / PR 1/4): every surface that isn’t load-bearing rarity identity uses var(--med-*) tokens for color, spacing, radius, shadows, transitions. Rarity colors stay on the hero frame, title, weapon-slot rings, filter pills, and info button border per the ADR 20260513-001 exception.
  • The hero card uses a near-black #05050c canvas viewport because ShipLivePreview paints a transparent radial rarity gradient over it; this is documented in code as a deliberate exception to the medical chrome.
  • Layered hero card: layer 0 is the full-bleed ShipLivePreview (pointer-events: none); layer 1 is foreground content (also pointer-events: none on the container, with pointer-events: auto re-enabled only on the weapon-slots row so taps register).
  • ownedHulls memo dependencies include ships, both filter Sets, and both inventory-store selectors. The sort uses a rarityRank record so legendary sorts first and common sorts last.
  • Weapon icons are pre-baked into off-screen canvases by the engine; this file copies them into a slot-local canvas inside a useEffect, with two timeout-driven redraws (80ms, 250ms) to handle late image decoding.
  • WeaponCardPopover matches in-game reward-card pixel dimensions exactly: cardW = min(140, window.innerWidth * 0.26), cardH = cardW * 2.1. The card is drawn at device-pixel-ratio scale and uses the same drawRewardCard function the level-up screen calls — same pixels in both contexts.
  • The XP bar percentage is clamped to 100% at max star (star >= 5); otherwise it’s xp / (xp + toNext) rounded to an integer percent.
  • The BigStat component coerces def to Record<string, number> for stat lookup — the ship def shape is dynamic-keyed.
  • Inventory header (INVENTORY + FILTER button) lives outside the scrollable grid region so it stays pinned while the grid scrolls. Extra padding-top: 14px on the scroll region keeps dangling rarity badges on the first row from being clipped at the scroll edge.
  • The filter button shows a numeric badge (count of active filters across both Sets) using var(--med-status-good) when any filter is active.
  • The weapon-card popover mounts via createPortal to document.body so it escapes the tab’s flex layout and renders above all other UI at z-index: 1000. Backdrop is a med-modal className.
  • STAT_ROWS is declared but currently unused at the call site — the hero card hand-rolls HP / SHIELD / SPEED rows via BigStat. The constant is left in place as the canonical visible-stats list.