PURPOSE
Modal stat-sheet popover that displays the main combat, drive, recovery, and meta stats for a single ship hull at a given star level. Styled like a racing-game stat card. Deliberately omits feel-knobs and physics-tuning fields (sprite color, ramSpeedBleed, terrainFriction, etc.) — those belong in the playground, not the player-facing collection screen.
OWNS
- Local
useEffectthat wires an Escape-key listener and tears it down on unmount. - A
STAT_GROUPStable (module-level constant) that drives all rendered rows. Four groups:COMBAT,DRIVE,RECOVERY,META. - Per-row formatter functions:
fmt0(rounded integer),fmt2(2 decimals),fmtPct(rounded percent),fmtSec(1-decimal seconds). - Two internal types:
StatRowandStatGroup. - A portaled overlay rendered to
document.bodyso it escapes the hub’s portrait clipping. - Inline styling for the backdrop, card frame, close button, header, group headings, and stat rows. Frame color is driven by ship rarity.
READS FROM
@starship-survivors/data/ships—getShipDef(hull, star),displayHullName(hull),RARITY_COLORS.- Resolved
ShipDeffromgetShipDeffor the stat fields listed inSTAT_GROUPS:hp,shield,armor,weaponDamagePct,fireRatePct,weaponSlots,upgradeSlots,speed,acceleration,turnRate,drag,heatBuildup,heatCooldown,shieldRegenRate,shieldRegenDelay,hpRegen,luck,magnetRange,currencyBonus, plusrarity(used to pick the frame color). - Props:
hull: string,star: number,onClose: () => void. document.body(portal target).window(Escape-key listener).
PUSHES TO
- Invokes the
onCloseprop on backdrop click, X-button click, and Escape key. - Renders a React portal subtree into
document.body. No store writes, no network, no telemetry.
DOES NOT
- Mutate the ship def or any store.
- Persist any state — purely presentational and stateless beyond the keydown effect.
- Render feel-knobs, tuning fields, sprite color, ram bleed, or terrain friction.
- Display rows for stats not declared in
STAT_GROUPS. - Animate. Open/close is instant; no transitions.
- Trap focus or manage
aria-modalsemantics beyond the close button’saria-label. - Lock body scroll when open.
- Guard against
getShipDefthrowing; assumes hull is valid.
Signals
- Backdrop click (
onClick={onClose}on the outer fixed-position div) closes the modal. - Card click is stopped via
e.stopPropagation()so it does not bubble to the backdrop. - Close button (
<button aria-label="Close">) callsonClose. - Escape key, captured globally on
window, callsonClose.
Entry points
- Default export: none. Named export
ShipInfoPopover. - Props contract:
{ hull: string; star: number; onClose: () => void }. The popover normalizesstartoMath.max(1, star || 1)before resolving stats and rendering the star indicator.
Pattern notes
- Driven by a data-table-then-loop pattern. Adding a stat row is a one-line edit to
STAT_GROUPS; no JSX changes required as long as a suitable formatter exists. - Type-erases the def lookup with
(def as unknown as Record<string, number>)[row.key as string]to keepStatRow.keystrongly typed againstReturnType<typeof getShipDef>while letting the renderer treat values uniformly. Non-number values fall through to0. - The default formatter when
row.fmtis omitted isfmt0. - Rarity-driven theming: border, glow, and title color all key off
RARITY_COLORS[def.rarity].color. - Uses
createPortalto escape ancestor clipping/stacking. Fixed atzIndex: 1000. - Backdrop is
rgba(0,0,0,0.82)withpadding: 20so the card never butts against the viewport edge on small phones. - Card is capped at
maxWidth: 420andmaxHeight: 90vhwithoverflowY: 'auto'to stay scrollable on short screens. - Typography uses
'Cal Sans'for labels/eyebrows and'Space Grotesk'for headings and stat values. - Effect cleanup correctly removes the keydown listener on unmount and on
onCloseidentity change.