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
MedPanelfunctional component (variantscard,flat,doors).MedPanelModalfunctional component (scrim + click-out dismiss).MedPanelVariantexported type.- Module-level haptic listener installation (
installMedPrimaryHapticListener). - Module-level singleton flag
_hapticListenerInstalled. - Local helper
fireHaptic(ms)— safenavigator.vibratewrapper. - Size threshold constants
PANEL_SIZE_SM_MAX(240) andPANEL_SIZE_LG_MIN(560). - Haptic duration constant
HAPTIC_PRIMARY_MS(15).
READS FROM
@starship-survivors/engine/vfx/juice—Juice.fire(cue)forpanel_open/panel_open_sm/panel_open_lgaudio cues.navigator.vibrate(browser API, optional, behind feature detection).document(browser API, optional, behindtypeof documentcheck).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-primaryelement.- DOM via JSX (renders a
<div>for the panel and a<div>for the modal scrim). - Caller’s
onScrimClickcallback 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
offsetWidthper 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-primarypointerdown. - 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
useEffectwith anaudioFiredRefguard so it plays exactly once per mount, never on re-render. - Size-aware cue selection runs at mount-time only via a single
offsetWidthread; no per-frame reads orResizeObserver. - 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/catchandtypeofguards to silently no-op on desktop, iOS PWAs without permission, and SSR environments. - Scrim click is filtered with
e.target === e.currentTargetso clicks inside the panel do not dismiss the modal. - If
onScrimClickis 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 forcard); the visual differences are CSS-only. - The
audioprop defaults to true and is intended to be set to false only in tests or when nesting a doors panel inside another doors panel.