PURPOSE

Primitives for the medical-UI design language introduced at tick 74. Exports MedPanel (a white card / flat / pneumatic-doors panel) and MedPanelModal (a fullscreen scrim wrapping a centered panel). Provides the clean-white-panel + hairline-border + snap-mechanical-motion aesthetic used across the metagame shell. Also installs a single global haptic listener for .med-btn-primary presses.

OWNS

  • MedPanel functional component (variants card, flat, doors).
  • MedPanelModal functional component (scrim + click-out dismiss).
  • MedPanelVariant exported type.
  • Module-level haptic listener installation (installMedPrimaryHapticListener).
  • Module-level singleton flag _hapticListenerInstalled.
  • Local helper fireHaptic(ms) — safe navigator.vibrate wrapper.
  • Size threshold constants PANEL_SIZE_SM_MAX (240) and PANEL_SIZE_LG_MIN (560).
  • Haptic duration constant HAPTIC_PRIMARY_MS (15).

READS FROM

  • @starship-survivors/engine/vfx/juiceJuice.fire(cue) for panel_open / panel_open_sm / panel_open_lg audio cues.
  • navigator.vibrate (browser API, optional, behind feature detection).
  • document (browser API, optional, behind typeof document check).
  • rootRef.current.offsetWidth — measured once on mount to pick size-aware audio cue.
  • React (useEffect, useRef, ReactNode, CSSProperties).
  • CSS classes from src/metagame/styles/medical.css: med-panel, med-panel-doors, med-panel-flat, med-panel-doors-seam, med-modal, med-btn-primary.

PUSHES TO

  • Juice.fire('panel_open' | 'panel_open_sm' | 'panel_open_lg') on doors-variant mount.
  • navigator.vibrate(15) on pointerdown over any .med-btn-primary element.
  • DOM via JSX (renders a <div> for the panel and a <div> for the modal scrim).
  • Caller’s onScrimClick callback when the scrim itself (not children) is clicked.

DOES NOT

  • Does not manage open/closed state — the parent owns whether the modal is mounted.
  • Does not animate doors in JS — animation lives in CSS (medical.css).
  • Does not own routing, navigation, or screen lifecycle.
  • Does not read or write game state, Zustand stores, Supabase, or telemetry.
  • Does not read offsetWidth per frame — measured once on mount only.
  • Does not throw on missing audio or missing vibrate support — silently no-ops.
  • Does not stop event propagation on the haptic pointerdown listener (passive, capture).
  • Does not re-fire audio on re-render — guarded by audioFiredRef.
  • Does not register more than one global haptic listener — guarded by _hapticListenerInstalled.

Signals

  • Audio cue panel_open (default), panel_open_sm (panel width < 240px), panel_open_lg (panel width > 560px).
  • Haptic vibrate pulse of 15ms on .med-btn-primary pointerdown.
  • Visible cyan seam element (med-panel-doors-seam) rendered only for the doors variant.

Entry points

  • MedPanel({ children, variant, style, className, audio }) — named export.
  • MedPanelModal({ children, onScrimClick, style, className }) — named export.
  • MedPanelVariant — exported type union of 'card' | 'flat' | 'doors'.
  • Side-effect on module load: installMedPrimaryHapticListener() runs once.

Pattern notes

  • Doors-variant audio is fired inside useEffect with an audioFiredRef guard so it plays exactly once per mount, never on re-render.
  • Size-aware cue selection runs at mount-time only via a single offsetWidth read; no per-frame reads or ResizeObserver.
  • The global haptic listener attaches at module load with { capture: true, passive: true } so it cannot be stopped by downstream handlers and never blocks the input thread.
  • Haptic firing is wrapped in try/catch and typeof guards to silently no-op on desktop, iOS PWAs without permission, and SSR environments.
  • Scrim click is filtered with e.target === e.currentTarget so clicks inside the panel do not dismiss the modal.
  • If onScrimClick is undefined, the modal is intentionally non-dismissible (used for forced flows like onboarding or revive prompts).
  • Variant maps to a single CSS class (med-panel-doors, med-panel-flat, or none for card); the visual differences are CSS-only.
  • The audio prop defaults to true and is intended to be set to false only in tests or when nesting a doors panel inside another doors panel.