diff --git a/src/App.tsx b/src/App.tsx index b3be210..d866847 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -362,7 +362,7 @@ function App() { }, [clearSessionState, setProjects, setProjectsLoaded, setUsage, clearTasks, setRuntimeNotifications, setServerNotifications, setCanvasWorkflow, setCurrentCanvasProjectId, setWorkspaceExpanded, handleSetView]); const showSessionReplacedModal = useCallback((message?: string) => { - clearAuthenticatedState(); + clearAuthenticatedState({ resetView: true }); showSessionReplaced(message); }, [clearAuthenticatedState, showSessionReplaced]); diff --git a/src/components/PageTransition.tsx b/src/components/PageTransition.tsx index 514b017..0e429c8 100644 --- a/src/components/PageTransition.tsx +++ b/src/components/PageTransition.tsx @@ -6,6 +6,7 @@ interface PageTransitionProps { } const EXIT_DURATION_MS = 180; +const ENTER_DURATION_MS = 220; const NAV_ORDER: string[] = [ "home", @@ -39,8 +40,8 @@ function getNavIndex(key: string): number { export default function PageTransition({ viewKey, children }: PageTransitionProps) { const [displayedChildren, setDisplayedChildren] = useState(children); - const [phase, setPhase] = useState<"idle" | "exit">("idle"); - const [direction, setDirection] = useState<"forward" | "backward" | "neutral">("neutral"); + const [phase, setPhase] = useState<"idle" | "exit" | "enter">("idle"); + const [exitDirection, setExitDirection] = useState<"forward" | "backward" | "neutral">("neutral"); const prevKeyRef = useRef(viewKey); const timerRef = useRef>(); @@ -49,29 +50,62 @@ export default function PageTransition({ viewKey, children }: PageTransitionProp setDisplayedChildren(children); return; } + + const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + if (prefersReducedMotion) { + prevKeyRef.current = viewKey; + setDisplayedChildren(children); + setPhase("idle"); + return; + } + const prevIndex = getNavIndex(prevKeyRef.current); const nextIndex = getNavIndex(viewKey); if (prevIndex < nextIndex) { - setDirection("forward"); + setExitDirection("forward"); } else if (prevIndex > nextIndex) { - setDirection("backward"); + setExitDirection("backward"); } else { - setDirection("neutral"); + setExitDirection("neutral"); } prevKeyRef.current = viewKey; + setPhase("exit"); timerRef.current = setTimeout(() => { setDisplayedChildren(children); - setPhase("idle"); + setPhase("enter"); }, EXIT_DURATION_MS); return () => clearTimeout(timerRef.current); }, [viewKey, children]); - const dirClass = direction === "forward" ? " is-forward" : direction === "backward" ? " is-backward" : ""; + // After enter animation completes, go back to idle + useEffect(() => { + if (phase !== "enter") return; + const timer = setTimeout(() => setPhase("idle"), ENTER_DURATION_MS); + return () => clearTimeout(timer); + }, [phase]); + + const dirClass = exitDirection === "forward" ? " is-forward" : exitDirection === "backward" ? " is-backward" : ""; + + if (phase === "exit") { + return ( +
+ {displayedChildren} +
+ ); + } + + if (phase === "enter") { + return ( +
+ {displayedChildren} +
+ ); + } return ( -
+
{displayedChildren}
); -} +} \ No newline at end of file diff --git a/src/features/ecommerce/EcommerceVideoWorkspace.tsx b/src/features/ecommerce/EcommerceVideoWorkspace.tsx index 8a3e584..e498002 100644 --- a/src/features/ecommerce/EcommerceVideoWorkspace.tsx +++ b/src/features/ecommerce/EcommerceVideoWorkspace.tsx @@ -170,7 +170,7 @@ export default function EcommerceVideoWorkspace({ const handleRender = async () => { if (!planResult || !scenes.length) return; - const imageUrl = productImageDataUrls[0] || ""; + const imageUrl = planResult.imageUrls[0] || ""; setStage("rendering"); setError(null); renderAbortRef.current = { current: false }; @@ -213,7 +213,7 @@ export default function EcommerceVideoWorkspace({ const completedScenes = scenes.filter((s) => s.status === "completed" && s.resultUrl); const primaryVideo = completedScenes[0]?.resultUrl; const canRender = planResult?.compliance.allow_video_generation && stage === "planned"; - const sourceImage = productImageDataUrls[0] || ""; + const sourceImage = planResult?.imageUrls[0] || productImageDataUrls[0] || ""; const flowHasStarted = stage !== "idle" || completedSteps.length > 0 || scenes.length > 0; const flowMeta = `${platform} / ${aspectRatio} / ${durationSeconds}s / ${resolution}`; const planActionLabel = stage === "planning" diff --git a/src/features/ecommerce/ecommerceVideoService.ts b/src/features/ecommerce/ecommerceVideoService.ts index 5f92e86..e3e7265 100644 --- a/src/features/ecommerce/ecommerceVideoService.ts +++ b/src/features/ecommerce/ecommerceVideoService.ts @@ -77,7 +77,7 @@ export async function runVideoPlan( const compliance = await checkCompliance(summary, selling, storyboard, signal); onStepDone("compliance"); - return { summary, selling, creatives, storyboard, videoPrompts, compliance }; + return { imageUrls, summary, selling, creatives, storyboard, videoPrompts, compliance }; } export interface RenderSceneInput { diff --git a/src/features/ecommerce/ecommerceVideoTypes.ts b/src/features/ecommerce/ecommerceVideoTypes.ts index 29ca564..4443c7f 100644 --- a/src/features/ecommerce/ecommerceVideoTypes.ts +++ b/src/features/ecommerce/ecommerceVideoTypes.ts @@ -31,6 +31,7 @@ export interface EcommerceVideoSceneTask { } export interface EcommerceVideoPlanResult { + imageUrls: string[]; summary: ProductSummary; selling: SellingPointResult; creatives: CreativeOption[]; diff --git a/src/styles/components/page-transition.css b/src/styles/components/page-transition.css index 0a51b50..a722b12 100644 --- a/src/styles/components/page-transition.css +++ b/src/styles/components/page-transition.css @@ -16,43 +16,29 @@ } } -/* Directional page transitions */ -.page-motion--enter.is-forward { - animation: page-slide-in-forward 200ms var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1)) both; -} - -.page-motion--enter.is-backward { - animation: page-slide-in-backward 200ms var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1)) both; +/* Exit: fade + directional slide */ +.page-motion--exit { + animation: page-out 180ms ease forwards; + pointer-events: none; } .page-motion--exit.is-forward { - animation: page-slide-out-forward 180ms ease both; + animation: page-slide-out-forward 180ms ease forwards; + pointer-events: none; } .page-motion--exit.is-backward { - animation: page-slide-out-backward 180ms ease both; + animation: page-slide-out-backward 180ms ease forwards; + pointer-events: none; } -@keyframes page-slide-in-forward { - from { - opacity: 0; - transform: translateX(20px); - } - to { - opacity: 1; - transform: translateX(0); - } +/* Cancel child's own entrance animation during exit */ +.page-motion--exit .page-motion { + animation: none !important; } -@keyframes page-slide-in-backward { - from { - opacity: 0; - transform: translateX(-20px); - } - to { - opacity: 1; - transform: translateX(0); - } +@keyframes page-out { + to { opacity: 0; transform: translateY(-6px); } } @keyframes page-slide-out-forward { @@ -68,3 +54,23 @@ transform: translateX(16px); } } + +/* Enter: explicit wrapper entrance animation overrides child page-motion */ +.page-motion--enter { + animation: page-enter-fade 220ms ease both; +} + +.page-motion--enter .page-motion { + animation: none !important; +} + +@keyframes page-enter-fade { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/src/styles/components/primitives.css b/src/styles/components/primitives.css index a864fef..a6b99a4 100644 --- a/src/styles/components/primitives.css +++ b/src/styles/components/primitives.css @@ -65,18 +65,7 @@ min-height: 0; } -.page-motion--exit { - animation: page-out 180ms ease both; - pointer-events: none; -} - -.page-motion--exit .page-motion { - animation: none; -} - -@keyframes page-out { - to { opacity: 0; transform: translateY(-6px); } -} +/* page-motion--exit moved to page-transition.css */ .page-loading-center { display: flex;