PURPOSE

Feedback modal triggered by the in-canvas ”?” button. Captures a screenshot of the current view, collects free-text feedback from the player, and writes the record to Supabase. Pauses the active mission while open so gameplay doesn’t continue underneath the modal.

OWNS

  • Module-level _activeMission and _gameCanvas refs holding the current mission handle and game canvas.
  • Exports setActiveMission(mission, canvas) and clearActiveMission() used by GameScreen to register/clear the active mission.
  • Local React state: open, text, screenshot, sending, sent, error.
  • didPauseRef tracking whether this component is the one that paused the game (so it only resumes what it paused).
  • Two side-effect flows: handleOpen (pause + capture + open) and handleClose (resume + close).
  • handleSubmit building the feedback payload and inserting it into the player_feedback table.

READS FROM

  • usePlayerStore — pulls profile for player_id and displayName; renders nothing when profile is null.
  • _activeMission / _gameCanvas module refs.
  • _gameCanvas.toDataURL('image/png') for the in-game screenshot.
  • html2canvas(document.body, ...) fallback for menu screens.
  • window.location.pathname, navigator.userAgent, window.screen.width, window.screen.height for context fields.
  • MissionHandle type from @starship-survivors/engine/bridge.

PUSHES TO

  • supabase.from('player_feedback').insert(payload) with player_id, display_name, feedback_text, screenshot_data, screen, user_agent, screen_w, screen_h, created_at.
  • _activeMission.pause() on open, _activeMission.resume() on close (gated by didPauseRef).
  • Dispatches ss:mission-changed on window from both setActiveMission and clearActiveMission.
  • Renders a MedPanelModal containing a MedPanel variant="doors" with textarea, screenshot well, send/cancel buttons, and a success state.

DOES NOT

  • Does not render a FAB button. The on-screen FAB has been removed from every screen; the canvas ”?” hit-region in hud.ts is the only opener.
  • Does not auto-open on errors or unhandled rejections; only opens on the ss:open-feedback event.
  • Does not retry failed inserts or queue offline submissions; failures surface as an inline error string.
  • Does not upload the screenshot as a binary blob — it inlines the PNG data URL into screenshot_data.
  • Does not resume a mission it did not pause (the didPauseRef flag prevents stomping on an unrelated pause state).
  • Does not gate by network status or rate-limit submissions.

Signals

  • Listens for window event ss:open-feedback.
  • Emits window event ss:mission-changed whenever setActiveMission or clearActiveMission is called.

Entry points

  • setActiveMission(mission, canvas) — called by GameScreen when a mission is created.
  • clearActiveMission() — called by GameScreen when a mission is destroyed.
  • <FeedbackFAB /> — mounted once in the metagame shell; renders null until ss:open-feedback arrives.
  • ss:open-feedback event — dispatched from the canvas HUD ”?” hit-region (and any other UI surface that wants to invoke the modal).

Pattern notes

  • Third member of the modal-form trio refactored to medical-UI styling: WelcomeModal (tick 74), ClaimAccountModal (tick 75), FeedbackFAB (tick 76). Uses MedPanelModal + MedPanel variant="doors" plus .med-textarea and .med-image-well CSS classes.
  • Module-level mutable singletons (_activeMission, _gameCanvas) are intentional: the feedback modal lives outside the GameScreen subtree but needs direct access to the mission handle and canvas. The ss:mission-changed event lets other listeners observe transitions without subscribing through React.
  • Screenshot strategy branches on _gameCanvas presence — direct toDataURL during gameplay (cheap, mission already paused), html2canvas of the DOM on menu screens (heavier path, used only when there is no canvas).
  • The pause/resume contract is one-sided: only resumes if this component performed the pause. Other pause sources (e.g., system pause overlay) are not disturbed.
  • Returns null until open is true, so the component contributes nothing to the DOM in steady state.