PURPOSE

Fullscreen scrollable reward-track overlay for a single planet. Renders the planet’s name, overall progress bar, sprite, and a vertical scrollable list of reward segments (one per track level). Each segment shows a per-card vertical progress bar with a numbered circle badge and a reward card (header bar + body + collectible square that extends off the top). Locked rewards render as grey question-mark cards. Clicking an unclaimed but unlocked collectible fires the claim RPC, optimistically marks the reward claimed locally, and triggers a fullscreen reveal popup. Auto-dismisses the reveal after 2.5 s.

OWNS

  • The PlanetTrackViewer component (default export-style named export) — overlay shell, scroll container, top/title/sprite area, divider, scroll-hint triangle, and reveal-popup mount point.
  • The internal TrackSegment component — per-row layout for one reward level (vertical bar + circle badge + card or locked card).
  • The internal RewardRevealPopup component — fullscreen celebration on claim with auto-dismiss timer.
  • Local UI state: claimingLevel (which level is mid-claim), revealReward (which reward is being shown in the reveal popup), showScrollHint (whether the bottom triangle is visible).
  • The scroll-position ref scrollRef and the scroll listener that toggles showScrollHint based on distance from the bottom.
  • Optimistic claim semantics: on RPC failure the local store is still updated and the reveal still fires; the server reconciles on next bootstrap.
  • Per-card bar fill computation (barFillPct) derived from currentLevel and levelPct.
  • Inline rarity styling on cards (header bg.dark, body bg.light, collectible --col-border / --col-dark CSS custom properties) and the reveal name’s stroked-text inline style.

READS FROM

  • @starship-survivors/data/planet-configPLANETS map and PlanetId type; reads planet.name and planet.image.
  • @starship-survivors/data/planet-progressiongetPlanetTrack (reward list), getXpProgress (level + intra-level pct), TRACK_REWARD_RARITY (per-level rarity array), TRACK_RARITY_COLORS (rarity-to-color), TRACK_RARITY_BG (rarity-to-background pair { dark, light }), and the types PlanetTrackReward and TrackRewardRarity.
  • @starship-survivors/stores/planetProgressStoregetXp(planetId), isRewardClaimed, canClaimReward, addClaim.
  • ./PlanetProgressBar — composed at the top of the panel.
  • ./BottomNavOverlayBottomBar used as the bottom bar with a BACK button.
  • ./planet-track.css — all visual styling for the overlay, segments, cards, badge, scroll hint, and reveal.
  • Props: planetId, onClose.

PUSHES TO

  • @metagame/services/supabase — calls invokeRpc('claim_planet_reward', { p_planet_id, p_level }) when the user clicks an unclaimed unlocked collectible.
  • usePlanetProgressStore.addClaim(planetId, reward.level) — local store mutation (optimistic, runs even if the RPC throws).
  • onClose callback prop — invoked when the OverlayBottomBar BACK button is pressed.
  • setRevealReward(reward) and setRevealReward(null) — opens and closes the fullscreen reveal popup.
  • console.error('Failed to claim planet reward:', err) on RPC failure.

DOES NOT

  • Does not own or mutate XP totals; XP comes from the store via getXp.
  • Does not compute rarity tables or color palettes; consumes them from planet-progression.
  • Does not render the persistent main-screen progress bar — that is PlanetProgressBar, composed here.
  • Does not gate clicks on locked levels; locked cards render as a separate locked branch with no click handler.
  • Does not retry the RPC on failure — failure path is silent (console only) and relies on server reconciliation at next bootstrap.
  • Does not animate the reveal explicitly; styling and any animation live in planet-track.css.
  • Does not persist any UI state across mounts; claimingLevel, revealReward, and showScrollHint reset each time the overlay opens.
  • Does not handle the onOpenTrack callback from PlanetProgressBar (passes a no-op since the user is already in the track view).
  • Does not currently use the rarityColor value inside TrackSegment (it is read but not applied — only bg.dark and bg.light are used for card styling).

Signals

  • onClose: () => void (prop) — wired to OverlayBottomBar’s onBack. Closes the overlay.
  • onClaim(reward) — passed from PlanetTrackViewer into each TrackSegment; bound to handleClaim, which performs the RPC + optimistic update + reveal trigger.
  • Scroll event on the inner scroll container — listened on mount, removed on unmount; recomputes showScrollHint.
  • setTimeout(onDismiss, 2500) in RewardRevealPopup — auto-dismisses the reveal after 2.5 s; cleared on unmount.
  • Click on the reveal background — also dismisses via the same onDismiss.
  • Click on a collectible square — stopPropagation then conditional onClaim(reward) if canClaim && !isClaimed && !isClaiming.

Entry points

  • Named export PlanetTrackViewer({ planetId, onClose }) — the public component.
  • Mounted by the parent metagame screen whenever the user opens a planet’s reward track. The overlay sits above the planet hub via .planet-track-overlay / .planet-track-panel CSS.
  • Internal helpers TrackSegment and RewardRevealPopup are not exported.

Pattern notes

  • React 19 + hooks: useState, useCallback, useRef, useEffect. Store reads use individual selectors (usePlanetProgressStore(s => s.getXp(planetId))) per Zustand 5 conventions.
  • Optimistic UI: local store updates and reveal popup fire regardless of RPC outcome; server is the eventual source of truth via bootstrap reconciliation.
  • Inline styles are limited to dynamic rarity values (bg.dark, bg.light, CSS custom properties on the collectible, rarity color + stroked text on the reveal name). Everything else lives in planet-track.css.
  • Per-card bar fill is computed as a ternary chain: 100 for past levels, Math.round(levelPct * 100) for the in-progress level, 0 for future levels.
  • The collectible square uses CSS custom properties --col-border and --col-dark so the stylesheet can drive border + dark-fill from rarity without per-element class explosion.
  • The has-divider class is appended to every segment except the last (isLast flag) to draw a separator under each row.
  • The OverlayBottomBar is reused here in “overlay” mode with a BACK button, matching other fullscreen overlays in the metagame.
  • The PlanetProgressBar is reused with a no-op onOpenTrack since the user is already inside the track view.
  • The scroll-hint triangle () is a literal Unicode character in JSX, gated on showScrollHint.
  • The reveal popup mounts as a sibling of .planet-track-panel (still inside .planet-track-overlay) so its z-index can sit above the panel without re-parenting.
  • Lint note: rarityColor is destructured in TrackSegment but never read; only bg.dark and bg.light reach the DOM. Safe to remove if a lint cleanup runs.
  • File organization uses section banners (═══ rules) to separate the main component, TrackSegment, and RewardRevealPopup.