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
SelectTabexported component (top-level render). - Local UI state:
filterOpen(panel toggle),rarityFilter(Set ofShipRarity),starFilter(Set of star numbers 1-5),infoOpen(ship info popover toggle). - Memoized
ownedHullslist — filters owned hulls against active filters then sorts by rarity DESC, star DESC, XP DESC. - Filter toggle helpers
toggleRarityandtoggleStarthat 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
drawRewardCardat 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
useSessionStore—selectedShipId,setShip.useInventoryStore—ships,currentXp,currentStar,isUnlocked.@starship-survivors/data/ships—HULL_CLASSES,getShipDef,RARITY_COLORS,displayHullName.@starship-survivors/data/ships-v4-rarity—ShipRaritytype.@starship-survivors/data/ship-progression—xpToNextStar.@starship-survivors/data/weapons—WEAPON_MAP,resolveWeaponRarity,WeaponCoreSpectype.@starship-survivors/engine/world/leveling—describeNewWeapon.@starship-survivors/engine/rendering/weapon-icons—getWeaponIconImg.@starship-survivors/engine/rendering/hud—drawRewardCard.@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.innerWidthandwindow.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 —
drawRewardCardanddescribeNewWeaponproduce all weapon-card visuals + text. - Does not handle ship-locking — the grid only renders owned hulls (
!!ships[hull]) and passeslocked={false}toCollectibleCard. - 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) → opensShipInfoPopover. - Filter button toggles the filter panel; pill clicks toggle membership in the corresponding rarity/star Set, which recomputes
ownedHullsvia memo. - Weapon-slot click on a filled slot → opens
WeaponCardPopovershowing the in-game reward card for that weapon. Empty slots are non-interactive. WeaponCardPopoverbackdrop click and Escape key both close the popover viaonClose.ShipInfoPopoverclose → resetsinfoOpento false.
Entry points
- Default export:
SelectTabnamed export. Mounted by the parent ship-screen tab router (sibling to Upgrade / Fuse / etc.). - No props — relies entirely on
useSessionStoreanduseInventoryStorefor 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 ADR20260513-001exception. - The hero card uses a near-black
#05050ccanvas viewport becauseShipLivePreviewpaints 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, withpointer-events: autore-enabled only on the weapon-slots row so taps register). ownedHullsmemo dependencies includeships, both filter Sets, and both inventory-store selectors. The sort uses ararityRankrecord 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. WeaponCardPopovermatches 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 samedrawRewardCardfunction 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’sxp / (xp + toNext)rounded to an integer percent. - The
BigStatcomponent coercesdeftoRecord<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. Extrapadding-top: 14pxon 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
createPortaltodocument.bodyso it escapes the tab’s flex layout and renders above all other UI atz-index: 1000. Backdrop is amed-modalclassName. STAT_ROWSis declared but currently unused at the call site — the hero card hand-rolls HP / SHIELD / SPEED rows viaBigStat. The constant is left in place as the canonical visible-stats list.