From 0e95b7e543f482df4b22041fe0bea92a102a8936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sat, 28 Feb 2026 17:17:20 +0100 Subject: [PATCH] fix: track animation ended state and wire preview controls correctly - Replace boolean paused with AnimState ('playing'|'paused'|'ended') - Use onAnimationEnd to detect when finite animations finish - Play re-enables after end and restarts the animation (replay) - Pause only active while playing; Restart always available - Config changes auto-restart preview so edits are instantly visible Co-Authored-By: Claude Sonnet 4.6 --- components/animate/AnimationPreview.tsx | 38 ++++++++++++++++--------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/components/animate/AnimationPreview.tsx b/components/animate/AnimationPreview.tsx index 788e605..8cb0b63 100644 --- a/components/animate/AnimationPreview.tsx +++ b/components/animate/AnimationPreview.tsx @@ -14,6 +14,8 @@ interface Props { onElementChange: (e: PreviewElement) => void; } +type AnimState = 'playing' | 'paused' | 'ended'; + const SPEEDS: { label: string; value: string }[] = [ { label: '0.25×', value: '0.25' }, { label: '0.5×', value: '0.5' }, @@ -23,9 +25,8 @@ const SPEEDS: { label: string; value: string }[] = [ export function AnimationPreview({ config, element, onElementChange }: Props) { const styleRef = useRef(null); - const elementRef = useRef(null); const [restartKey, setRestartKey] = useState(0); - const [paused, setPaused] = useState(false); + const [animState, setAnimState] = useState('playing'); const [speed, setSpeed] = useState('1'); // Inject @keyframes CSS into document head @@ -36,21 +37,32 @@ export function AnimationPreview({ config, element, onElementChange }: Props) { document.head.appendChild(styleRef.current); } styleRef.current.textContent = buildCSS(config); + // Restart preview whenever config changes so changes are immediately visible + setAnimState('playing'); + setRestartKey((k) => k + 1); }, [config]); // Cleanup on unmount useEffect(() => { - return () => { - styleRef.current?.remove(); - }; + return () => { styleRef.current?.remove(); }; }, []); const restart = () => { - setPaused(false); + setAnimState('playing'); setRestartKey((k) => k + 1); }; + const handlePlay = () => { + if (animState === 'ended') { + // Animation finished — restart it + restart(); + } else { + setAnimState('playing'); + } + }; + const scaledDuration = Math.round(config.duration / Number(speed)); + const isInfinite = config.iterationCount === 'infinite'; return ( @@ -79,12 +91,12 @@ export function AnimationPreview({ config, element, onElementChange }: Props) { {/* Animated element */}
!isInfinite && setAnimState('ended')} > {element === 'box' && (
@@ -118,17 +130,17 @@ export function AnimationPreview({ config, element, onElementChange }: Props) {