PURPOSE

Modal popover that renders the full five-tier ladder for a single artifact. Each row shows the tier color band, tier name, the artifact’s ability text at that runtime tier, and the per-tier flat stat bonus. Used to let the player inspect what an artifact does at every tier from a single place.

OWNS

  • The modal/portal DOM tree (backdrop, framed panel, close button, header, tier row list).
  • The Escape-to-close keyboard handler (registered on window for the lifetime of the popover).
  • The construction of the five tier-row view models (runtime tier 0 to ARTIFACT_TIER_MAX).

READS FROM

  • @starship-survivors/data/artifacts:
    • ARTIFACT_MAP — to look up the artifact definition by artifactId.
    • ARTIFACT_TIER_COLOR_BY_IDX — per-tier color band.
    • ARTIFACT_TIER_NAMES — tier display names.
    • ARTIFACT_TIER_MAX — count of runtime tiers minus one.
    • FLAT_BONUS_PCT_BY_TIER — per-tier flat bonus percentage (10/20/30/40/50).
    • getArtifactFlatBonus(artifactId) — returns the flat-bonus descriptor (with label) for this artifact.
    • getTierLabelAt(def, runtimeTier) — returns the artifact’s ability text at the given runtime tier.
  • Props: artifactId (string), onClose (callback).

PUSHES TO

  • Invokes the onClose prop on backdrop click, X button click, and Escape key.
  • Mounts via createPortal to document.body (escapes any parent clipping such as the hub’s portrait container).

DOES NOT

  • Does not mutate any store or persistent state.
  • Does not read from Zustand stores, the run loop, or telemetry.
  • Does not perform network or Supabase calls.
  • Does not handle equipping, upgrading, or any artifact gameplay logic.
  • Does not animate or use timers beyond the keydown listener.
  • Renders nothing (return null) if the artifact definition or flat-bonus descriptor is missing.

Signals

  • Click on the outer backdrop onClose().
  • Click on the inner panel e.stopPropagation() (suppresses backdrop close).
  • Click on the X button onClose().
  • keydown Escape on window onClose().

Entry points

  • Default export: named export ArtifactInfoPopover({ artifactId, onClose }).
  • Rendered by the ships screen when the player taps an artifact for details. Parent passes the artifact id and an onClose callback that clears its own selection state.

Pattern notes

  • Portal-to-body pattern (createPortal(..., document.body)) is used specifically to escape the hub’s portrait clipping; replicate this when another modal must overlay clipped containers.
  • Tier rows are built by mapping over Array.from({ length: ARTIFACT_TIER_MAX + 1 }), ordered common at top to legendary at bottom; the count is data-driven via ARTIFACT_TIER_MAX.
  • Per-tier color is sourced from a single ARTIFACT_TIER_COLOR_BY_IDX table and applied to both the left border band and the tier-name/bonus text — keeps tier coloring consistent across the row.
  • Bonus label uses flat.label.toUpperCase() from getArtifactFlatBonus, so the displayed stat name follows the artifact data table rather than a hardcoded string.
  • Inline styles only (no CSS module). Fonts are 'Cal Sans' for labels/eyebrows and 'Space Grotesk' for headings and body text. Accent color #4fd6c7 and dark background #05050c match the hub modal style.
  • Escape handler is registered and torn down in a single useEffect keyed on onClose, avoiding listener leaks.
  • Early return after the useEffect hook keeps hook order stable across renders.