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
InspectTargetdiscriminated union ({ kind: 'ship'; hull }|{ kind: 'mod'; mod }).Inspectorexported component (props:target: InspectTarget | null,onClose: () => void).- Internal
ShipBodyandModBodyrenderers, dispatched ontarget.kind. - Internal
SectionHeaderhelper 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 theinspectorPopInkeyframe animation.
READS FROM
@starship-survivors/data/ships—getShipDef,RARITY_COLORS(aliasedSHIP_RARITY_COLORS),RARITY_NAMES,displayHullName.@starship-survivors/engine/rendering/ships-v4-loader—getShipV4SpritePath.@starship-survivors/data/mods—MOD_TEMPLATES_BY_ID,RARITY_COLORS(aliasedMOD_RARITY_COLORS),RARITY_MULTIPLIER,filledCellCount,ModInstancetype.@starship-survivors/stores/inventoryStore—useInventoryStoreselectors:isUnlocked,currentXp,currentStar.@starship-survivors/data/ship-progression—xpToNextStar.@metagame/components/MedPanel—MedPanelwrapper.react/react-dom—ReactNodetype,createPortal.
PUSHES TO
document.bodyviacreatePortal(DOM-level escape from local stacking contexts).- The supplied
onClosecallback, 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
targetandonClose. - Does not fetch or compute stats —
getShipDefand the mod template /RARITY_MULTIPLIERprovide all values. - Does not animate the close transition — only the open
inspectorPopInkeyframe is defined. - Does not handle keyboard dismissal (no Escape listener).
- Does not localize labels; stat labels are hard-coded uppercase strings.
Signals
- Returns
nullearly whentargetisnull(closed state). - Backdrop
onClick={onClose}and inner cardonClick={e => e.stopPropagation()}form the click-outside-to-dismiss contract. - Close button (
×) labelledaria-label="Close". - Portal uses
zIndex: 90000to 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 ★5label whenstarLvl >= 5). - Ship
starvalue is clamped toMath.max(1, starLvl || 1)when unlocked, falls back to1when locked, before being passed togetShipDef. - Mod body: rejects unknown template IDs with a plain “Unknown mod.” message. Piece preview paints filled cells from
tpl.cellsatCELL × CELLpx each; stat values are rendered as+{v * mult}in--med-status-goodcolor (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 theInspectTargettype. Consumed by Ships sub-tab screens that pass a tapped card’s identity in and cleartargeton 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.accentfor ships,colorfor 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
defwith anas unknown as Record<string, number>cast, then filtered byv === undefinedso missing keys are skipped silently. - Mod stats iterate
Object.entries(tpl.stats)and skip non-numeric values (typeof v !== 'number'). inspectorPopInkeyframe 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.