import React, { useRef, useEffect, useState } from 'react'; import { useUIStore } from '../../stores/uiStore'; interface MobileScreenStackProps { rootScreen: React.ReactNode; screenMap: Record) => React.ReactNode>; } export function MobileScreenStack({ rootScreen, screenMap }: MobileScreenStackProps) { const mobileStack = useUIStore((s) => s.mobileStack); const [transitioning, setTransitioning] = useState<'push' | 'pop' | null>(null); const [renderStack, setRenderStack] = useState(mobileStack); const prevStackRef = useRef(mobileStack); const animatingRef = useRef(false); useEffect(() => { const prevStack = prevStackRef.current; const prevLen = prevStack.length; const newLen = mobileStack.length; prevStackRef.current = mobileStack; if (newLen > prevLen) { // Push — render new screen off-screen, then animate in on next frame setRenderStack(mobileStack); setTransitioning('push'); animatingRef.current = true; // Use rAF to ensure the off-screen position is painted before animating requestAnimationFrame(() => { requestAnimationFrame(() => { setTransitioning(null); animatingRef.current = false; }); }); } else if (newLen < prevLen) { // Pop — keep old screen visible during exit animation, then remove setTransitioning('pop'); animatingRef.current = true; const timer = setTimeout(() => { setRenderStack(mobileStack); setTransitioning(null); animatingRef.current = false; }, 200); return () => clearTimeout(timer); } else { setRenderStack(mobileStack); } }, [mobileStack]); return (
{/* Root screen — always rendered underneath, invisible when covered by stack */}
0 ? 'hidden' : 'visible' }} > {rootScreen}
{/* Stacked screens */} {renderStack.map((entry, index) => { const isTop = index === renderStack.length - 1; const renderer = screenMap[entry.screen]; if (!renderer) return null; let style: React.CSSProperties = {}; let className = 'absolute inset-0 bg-surface-base z-10 flex flex-col'; if (isTop && transitioning === 'push') { // Initial position: off-screen right style.transform = 'translateX(100%)'; } else if (isTop && transitioning === 'pop') { // Animate out to the right className += ' transition-transform duration-200 ease-out'; style.transform = 'translateX(100%)'; } else if (isTop && !transitioning && !animatingRef.current) { // Settled position className += ' transition-transform duration-200 ease-out'; } return (
{renderer(entry.params)}
); })}
); }