PURPOSE

Modal overlay that renders the detail card for a tapped ship hull or mod instance. Wraps a MedPanel inside a med-modal scrim portaled to document.body. Separates “inspect” (tap → open Inspector) from “commit” (drag → apply), so the player can read full stats before acting. Built on the medical UI design system; preserves rarity / status / mod colors as semantic identity (ADR 20260513-001 exception).

OWNS

  • InspectTarget discriminated union ({ kind: 'ship'; hull } | { kind: 'mod'; mod }).
  • Inspector exported component (props: target: InspectTarget | null, onClose: () => void).
  • Internal ShipBody and ModBody renderers, dispatched on target.kind.
  • Internal SectionHeader helper component.
  • Layout constants: SHIP_STAT_ROWS (label/key pairs for ship stats), MOD_STAT_LABEL (template key → display label map), mod piece cell size (CELL = 28, GAP = 2).
  • Inline styles: statRowStyle, closeButtonStyle, and the inspectorPopIn keyframe animation.

READS FROM

  • @starship-survivors/data/shipsgetShipDef, RARITY_COLORS (aliased SHIP_RARITY_COLORS), RARITY_NAMES, displayHullName.
  • @starship-survivors/engine/rendering/ships-v4-loadergetShipV4SpritePath.
  • @starship-survivors/data/modsMOD_TEMPLATES_BY_ID, RARITY_COLORS (aliased MOD_RARITY_COLORS), RARITY_MULTIPLIER, filledCellCount, ModInstance type.
  • @starship-survivors/stores/inventoryStoreuseInventoryStore selectors: isUnlocked, currentXp, currentStar.
  • @starship-survivors/data/ship-progressionxpToNextStar.
  • @metagame/components/MedPanelMedPanel wrapper.
  • react / react-domReactNode type, createPortal.

PUSHES TO

  • document.body via createPortal (DOM-level escape from local stacking contexts).
  • The supplied onClose callback, fired on backdrop click and the close button click.

DOES NOT

  • Does not mutate inventory, equip mods, or apply changes — read-only inspection.
  • Does not own its open/close state; the parent screen passes target and onClose.
  • Does not fetch or compute stats — getShipDef and the mod template / RARITY_MULTIPLIER provide all values.
  • Does not animate the close transition — only the open inspectorPopIn keyframe is defined.
  • Does not handle keyboard dismissal (no Escape listener).
  • Does not localize labels; stat labels are hard-coded uppercase strings.

Signals

  • Returns null early when target is null (closed state).
  • Backdrop onClick={onClose} and inner card onClick={e => e.stopPropagation()} form the click-outside-to-dismiss contract.
  • Close button (×) labelled aria-label="Close".
  • Portal uses zIndex: 90000 to sit above all screen chrome.
  • Ship body: locked hulls render in grayscale with a “Locked — pull from the Shipyard to unlock.” message; unlocked hulls render with a rarity-colored drop-shadow, a five-star strip, and an XP bar (rarity-gradient fill, gold-to-warn gradient and MAX ★5 label when starLvl >= 5).
  • Ship star value is clamped to Math.max(1, starLvl || 1) when unlocked, falls back to 1 when locked, before being passed to getShipDef.
  • Mod body: rejects unknown template IDs with a plain “Unknown mod.” message. Piece preview paints filled cells from tpl.cells at CELL × CELL px each; stat values are rendered as +{v * mult} in --med-status-good color (upgrade semantics).
  • Mod legendary tier appends “this is the max.” to the merge-rules footnote; lower tiers end with a period.

Entry points

  • Exported: Inspector({ target, onClose }) and the InspectTarget type. Consumed by Ships sub-tab screens that pass a tapped card’s identity in and clear target on dismiss.

Pattern notes

  • Component is a single-file dispatcher: one outer modal frame, two body renderers selected by discriminated-union kind.
  • Rendering bypasses parent stacking via createPortal(..., document.body).
  • Styling mixes class hooks from the medical design system (med-modal, med-h1, med-text, med-text-muted) with inline styles and CSS variables (--med-panel-recessed, --med-border, --med-radius-md, --med-radius-sm, --med-shadow-elevated, --med-shadow-hairline, --med-amber, --med-status-good, --med-status-warn, --med-text, --med-text-muted, --med-text-inverse, --med-panel-raised).
  • Rarity color (frame.color / frame.accent for ships, color for mods) drives the panel border, box-shadow glow, name text, and (for ships) the XP-bar gradient and sprite drop-shadow — kept per ADR 20260513-001 even though the rest of the modal is the neutral medical palette.
  • Ship stats are read off def with an as unknown as Record<string, number> cast, then filtered by v === undefined so missing keys are skipped silently.
  • Mod stats iterate Object.entries(tpl.stats) and skip non-numeric values (typeof v !== 'number').
  • inspectorPopIn keyframe is inlined via a <style> tag inside the portal, scoping the animation to this component without touching global CSS.
  • Font stack mixes 'Space Grotesk' (stat readouts) and 'Cal Sans' (section headers).
  • Sprites are pixel-art (imageRendering: 'pixelated') and non-draggable / non-selectable.