11import { useCallback , useEffect , useRef } from "react" ;
22import type { GsapAnimation , ParsedGsap } from "@hyperframes/core/gsap-parser" ;
3+ import { findNonFiniteNumericFields } from "@hyperframes/core/studio-api/finite-mutation" ;
34import type { DomEditSelection } from "../components/editor/domEditingTypes" ;
45import type { EditHistoryKind } from "../utils/editHistory" ;
56import { applySoftReload } from "../utils/gsapSoftReload" ;
@@ -11,43 +12,13 @@ import {
1112 readKeyframeSnapshot ,
1213 writeKeyframeCache ,
1314} from "./gsapKeyframeCacheHelpers" ;
14-
15- const PROPERTY_DEFAULTS : Record < string , number > = {
16- opacity : 1 ,
17- x : 0 ,
18- y : 0 ,
19- scale : 1 ,
20- scaleX : 1 ,
21- scaleY : 1 ,
22- rotation : 0 ,
23- width : 100 ,
24- height : 100 ,
25- } ;
26-
27- /**
28- * Ensures the element has an id so it can be targeted by a GSAP selector.
29- * If the element already has an id or a CSS selector, returns those.
30- * Otherwise mints a unique id and sets it on the live element.
31- */
32- function ensureElementAddressable ( selection : DomEditSelection ) : {
33- selector : string ;
34- autoId ?: string ;
35- } {
36- if ( selection . id ) return { selector : `#${ selection . id } ` } ;
37- if ( selection . selector ) return { selector : selection . selector } ;
38-
39- const el = selection . element ;
40- const doc = el . ownerDocument ;
41- const tag = el . tagName . toLowerCase ( ) ;
42- let id = tag ;
43- let n = 1 ;
44- while ( doc . getElementById ( id ) ) {
45- n += 1 ;
46- id = `${ tag } -${ n } ` ;
47- }
48- el . setAttribute ( "id" , id ) ;
49- return { selector : `#${ id } ` , autoId : id } ;
50- }
15+ import {
16+ GsapMutationHttpError ,
17+ ensureElementAddressable ,
18+ formatGsapMutationRejectionToast ,
19+ PROPERTY_DEFAULTS ,
20+ readJsonResponseBody ,
21+ } from "./gsapScriptCommitHelpers" ;
5122
5223interface MutationResult {
5324 ok : boolean ;
@@ -62,21 +33,19 @@ async function mutateGsapScript(
6233 projectId : string ,
6334 sourceFile : string ,
6435 mutation : Record < string , unknown > ,
65- ) : Promise < MutationResult | null > {
66- try {
67- const res = await fetch (
68- `/api/projects/${ encodeURIComponent ( projectId ) } /gsap-mutations/${ encodeURIComponent ( sourceFile ) } ` ,
69- {
70- method : "POST" ,
71- headers : { "Content-Type" : "application/json" } ,
72- body : JSON . stringify ( mutation ) ,
73- } ,
74- ) ;
75- if ( ! res . ok ) return null ;
76- return ( await res . json ( ) ) as MutationResult ;
77- } catch {
78- return null ;
36+ ) : Promise < MutationResult > {
37+ const res = await fetch (
38+ `/api/projects/${ encodeURIComponent ( projectId ) } /gsap-mutations/${ encodeURIComponent ( sourceFile ) } ` ,
39+ {
40+ method : "POST" ,
41+ headers : { "Content-Type" : "application/json" } ,
42+ body : JSON . stringify ( mutation ) ,
43+ } ,
44+ ) ;
45+ if ( ! res . ok ) {
46+ throw new GsapMutationHttpError ( res . status , await readJsonResponseBody ( res ) ) ;
7947 }
48+ return ( await res . json ( ) ) as MutationResult ;
8049}
8150interface GsapScriptCommitsParams {
8251 projectIdRef : React . MutableRefObject < string | null > ;
@@ -94,6 +63,7 @@ interface GsapScriptCommitsParams {
9463 reloadPreview : ( ) => void ;
9564 onCacheInvalidate : ( ) => void ;
9665 onFileContentChanged ?: ( path : string , content : string ) => void ;
66+ showToast ?: ( message : string , tone ?: "error" | "info" ) => void ;
9767}
9868const DEBOUNCE_MS = 150 ;
9969
@@ -107,6 +77,7 @@ export function useGsapScriptCommits({
10777 reloadPreview,
10878 onCacheInvalidate,
10979 onFileContentChanged,
80+ showToast,
11081} : GsapScriptCommitsParams ) {
11182 const pendingPropertyEditRef = useRef < {
11283 selection : DomEditSelection ;
@@ -131,11 +102,27 @@ export function useGsapScriptCommits({
131102 ) => {
132103 const pid = projectIdRef . current ;
133104 if ( ! pid ) return ;
105+ const nonFiniteFields = findNonFiniteNumericFields ( mutation ) ;
106+ if ( nonFiniteFields . length > 0 ) {
107+ showToast ?.(
108+ "Couldn't read element layout — try again at a different playhead time" ,
109+ "error" ,
110+ ) ;
111+ if ( options . skipReload ) return ;
112+ throw new Error (
113+ `Mutation contains non-finite numeric values: ${ nonFiniteFields . map ( ( field ) => field . path ) . join ( ", " ) } ` ,
114+ ) ;
115+ }
134116 const targetPath = selection . sourceFile || activeCompPath || "index.html" ;
135- const result = await mutateGsapScript ( pid , targetPath , mutation ) ;
136- if ( ! result ) {
117+ let result : MutationResult ;
118+ try {
119+ result = await mutateGsapScript ( pid , targetPath , mutation ) ;
120+ } catch ( error ) {
121+ if ( error instanceof GsapMutationHttpError ) {
122+ showToast ?.( formatGsapMutationRejectionToast ( error ) , "error" ) ;
123+ }
137124 if ( options . skipReload ) return ;
138- throw new Error ( `Mutation failed: ${ mutation . type } ` ) ;
125+ throw error ;
139126 }
140127
141128 if ( result . changed === false ) {
@@ -195,6 +182,7 @@ export function useGsapScriptCommits({
195182 reloadPreview ,
196183 onCacheInvalidate ,
197184 onFileContentChanged ,
185+ showToast ,
198186 ] ,
199187 ) ;
200188 const flushPendingPropertyEdit = useCallback ( ( ) => {
@@ -488,7 +476,7 @@ export function useGsapScriptCommits({
488476 return commitMutation (
489477 selection ,
490478 { type : "convert-to-keyframes" , animationId, resolvedFromValues } ,
491- { label : "Convert to keyframes" } ,
479+ { label : "Convert to keyframes" , softReload : true } ,
492480 ) ;
493481 } ,
494482 [ commitMutation ] ,
0 commit comments