import { createContext, type ReactNode, useContext, useEffect, useMemo, useState, } from 'react'; import * as GuidedTourOverlayModule from './GuidedTourOverlay'; import { createGuidedTourPlayerAdapter, createIdleGuidedTourSnapshot, type GuidedTourBridgeSnapshot, type GuidedTourPlayerAdapter, type GuidedTourPlayerAdapterOptions, } from '../../animations/guidedTourAdapter'; type AnyRecord = Record; export interface GuidedTourRuntimeControls { start: () => void; pause: () => void; resume: () => void; restart: () => void; stop: () => void; next: () => void; previous: () => void; goToStep: (index: number) => void; } export interface GuidedTourRuntimeBridgeContextValue { player: GuidedTourPlayerAdapter | null; snapshot: GuidedTourBridgeSnapshot; controls: GuidedTourRuntimeControls; } export interface GuidedTourRuntimeBridgeProps { machineId: string; tour: unknown; tourId?: string; autoStart?: boolean; autoLoop?: boolean; showOverlay?: boolean; className?: string; overlayClassName?: string; onSnapshotChange?: (snapshot: GuidedTourBridgeSnapshot, player: GuidedTourPlayerAdapter) => void; onHighlightChange?: (partIds: string[], snapshot: GuidedTourBridgeSnapshot) => void; onCameraPresetChange?: (preset: string, snapshot: GuidedTourBridgeSnapshot) => void; renderOverlay?: (context: GuidedTourRuntimeBridgeContextValue) => ReactNode; children?: ReactNode | ((context: GuidedTourRuntimeBridgeContextValue) => ReactNode); } export interface GuidedTourOverlaySurfaceProps { context: GuidedTourRuntimeBridgeContextValue; className?: string; } const GuidedTourRuntimeContext = createContext(null); function resolveOverlayComponent() { const moduleRecord = GuidedTourOverlayModule as AnyRecord; const candidate = moduleRecord.GuidedTourOverlay ?? moduleRecord.TourOverlay ?? moduleRecord.default; return typeof candidate === 'function' ? candidate : null; } function createNoopControls(player: GuidedTourPlayerAdapter | null): GuidedTourRuntimeControls { return { start: () => player?.start(), pause: () => player?.pause(), resume: () => player?.resume(), restart: () => player?.restart(), stop: () => player?.stop(), next: () => player?.next(), previous: () => player?.previous(), goToStep: (index: number) => player?.goToStep(index), }; } export function useGuidedTourRuntime(): GuidedTourRuntimeBridgeContextValue { const context = useContext(GuidedTourRuntimeContext); if (!context) { throw new Error('useGuidedTourRuntime must be used within GuidedTourRuntimeBridge.'); } return context; } export function GuidedTourOverlaySurface({ context, className, }: GuidedTourOverlaySurfaceProps) { const OverlayComponent = resolveOverlayComponent(); const { snapshot, controls, player } = context; const activeStep = snapshot.activeStep; const overlayProps: AnyRecord = { snapshot, state: snapshot, tourState: snapshot, tour: player?.tour, player, rawPlayer: player?.rawPlayer, controls, activeStep, step: activeStep, activeStepIndex: snapshot.activeStepIndex, stepCount: snapshot.stepCount, progress: snapshot.progress, status: snapshot.status, isPlaying: snapshot.isPlaying, isPaused: snapshot.isPaused, highlightedPartIds: snapshot.highlightedPartIds, cameraPreset: snapshot.cameraPreset, onStart: controls.start, onPlay: controls.start, onPause: controls.pause, onResume: controls.resume, onRestart: controls.restart, onStop: controls.stop, onClose: controls.stop, onNext: controls.next, onPrevious: controls.previous, onPrev: controls.previous, onStepChange: controls.goToStep, goToStep: controls.goToStep, className, }; if (OverlayComponent) { const Component = OverlayComponent as React.ComponentType; return ; } return ; } function DefaultGuidedTourOverlaySurface({ context, className = '', }: GuidedTourOverlaySurfaceProps) { const { snapshot, controls } = context; const activeStep = snapshot.activeStep; if (snapshot.stepCount === 0) { return null; } return (

Guided tour ยท {snapshot.activeStepIndex + 1}/{snapshot.stepCount}

{activeStep?.title ?? 'Guided tour'}

{Math.round(snapshot.progress * 100)}%

{activeStep?.caption ?? 'Step through the highlighted components to learn how this machine works.'}

); } export function GuidedTourRuntimeBridge({ machineId, tour, tourId, autoStart = false, autoLoop = false, showOverlay = true, className = '', overlayClassName = '', onSnapshotChange, onHighlightChange, onCameraPresetChange, renderOverlay, children, }: GuidedTourRuntimeBridgeProps) { const [player, setPlayer] = useState(null); const [snapshot, setSnapshot] = useState(() => createIdleGuidedTourSnapshot(tour, { machineId, tourId }), ); useEffect(() => { const options: GuidedTourPlayerAdapterOptions = { machineId, tourId, autoLoop, }; const nextPlayer = createGuidedTourPlayerAdapter(tour, options); setPlayer(nextPlayer); setSnapshot(nextPlayer.getSnapshot()); const unsubscribe = nextPlayer.subscribe((nextSnapshot) => { setSnapshot(nextSnapshot); }); if (autoStart) { nextPlayer.start(); } return () => { unsubscribe(); nextPlayer.dispose(); }; }, [autoLoop, autoStart, machineId, tour, tourId]); useEffect(() => { if (!player) { return; } onSnapshotChange?.(snapshot, player); onHighlightChange?.(snapshot.highlightedPartIds, snapshot); if (snapshot.cameraPreset) { onCameraPresetChange?.(snapshot.cameraPreset, snapshot); } }, [onCameraPresetChange, onHighlightChange, onSnapshotChange, player, snapshot]); const controls = useMemo(() => createNoopControls(player), [player]); const context = useMemo( () => ({ player, snapshot, controls, }), [controls, player, snapshot], ); const renderedChildren = typeof children === 'function' ? (children as (context: GuidedTourRuntimeBridgeContextValue) => ReactNode)(context) : children; return (
{renderedChildren} {showOverlay ? renderOverlay ? renderOverlay(context) : : null}
); }