feat(studio): route GSAP keyframe add through SDK (§3.5 PR2) (#1470)

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
This commit is contained in:
Vance Ingalls
2026-06-17 16:43:34 -07:00
committed by GitHub
co-authored by Miguel Ángel
parent 592f7c775d
commit 7ca4490328
6 changed files with 211 additions and 9 deletions
@@ -1,7 +1,9 @@
import { useCallback } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import type { Composition } from "@hyperframes/sdk";
import type { DomEditSelection } from "../components/editor/domEditingTypes";
import { executeOptimistic } from "../utils/optimisticUpdate";
import { sdkGsapKeyframePersist } from "../utils/sdkCutover";
import type { KeyframeCacheEntry } from "../player/store/playerStore";
import { commitKeyframeAtTimeImpl } from "./gsapKeyframeCommit";
import { readKeyframeSnapshot, writeKeyframeCache } from "./gsapKeyframeCacheHelpers";
@@ -10,6 +12,7 @@ import type {
SafeGsapCommitMutation,
TrackGsapSaveFailure,
} from "./gsapScriptCommitTypes";
import type { EditHistoryKind } from "../utils/editHistory";
function executeOptimisticKeyframeCacheUpdate(options: {
sourceFile: string;
@@ -30,7 +33,22 @@ function executeOptimisticKeyframeCacheUpdate(options: {
});
}
interface GsapKeyframeOpsParams {
interface SdkKeyframeDeps {
sdkSession?: Composition | null;
writeProjectFile?: (path: string, content: string) => Promise<void>;
editHistory?: {
recordEdit: (entry: {
label: string;
kind: EditHistoryKind;
coalesceKey?: string;
files: Record<string, { before: string; after: string }>;
}) => Promise<void>;
};
reloadPreview?: () => void;
domEditSaveTimestampRef?: React.MutableRefObject<number>;
}
interface GsapKeyframeOpsParams extends SdkKeyframeDeps {
activeCompPath: string | null;
commitMutation: CommitMutation;
commitMutationSafely: SafeGsapCommitMutation;
@@ -42,6 +60,11 @@ export function useGsapKeyframeOps({
commitMutation,
commitMutationSafely,
trackGsapSaveFailure,
sdkSession,
writeProjectFile,
editHistory,
reloadPreview,
domEditSaveTimestampRef,
}: GsapKeyframeOpsParams) {
const addKeyframe = useCallback(
(
@@ -67,36 +90,97 @@ export function useGsapKeyframeOps({
(a, b) => a.percentage - b.percentage,
),
}),
persist: () =>
commitMutation(selection, mutation, {
persist: async () => {
if (
sdkSession &&
writeProjectFile &&
editHistory &&
reloadPreview &&
domEditSaveTimestampRef
) {
const handled = await sdkGsapKeyframePersist(
sourceFile,
animationId,
percentage,
{ [property]: value },
sdkSession,
{ editHistory, writeProjectFile, reloadPreview, domEditSaveTimestampRef },
{
label: `Add keyframe at ${percentage}%`,
coalesceKey: `gsap:${animationId}:kf:${percentage}`,
},
);
if (handled) return;
}
await commitMutation(selection, mutation, {
label: `Add keyframe at ${percentage}%`,
softReload: true,
}),
});
},
}).catch((error) => {
trackGsapSaveFailure(error, selection, mutation, `Add keyframe at ${percentage}%`);
});
},
[activeCompPath, commitMutation, trackGsapSaveFailure],
[
activeCompPath,
commitMutation,
trackGsapSaveFailure,
sdkSession,
writeProjectFile,
editHistory,
reloadPreview,
domEditSaveTimestampRef,
],
);
const addKeyframeBatch = useCallback(
(
async (
selection: DomEditSelection,
animationId: string,
percentage: number,
properties: Record<string, number | string>,
) => {
if (
sdkSession &&
writeProjectFile &&
editHistory &&
reloadPreview &&
domEditSaveTimestampRef
) {
const sourceFile = selection.sourceFile || activeCompPath || "index.html";
const handled = await sdkGsapKeyframePersist(
sourceFile,
animationId,
percentage,
properties,
sdkSession,
{ editHistory, writeProjectFile, reloadPreview, domEditSaveTimestampRef },
{ label: `Add keyframe at ${percentage}%` },
);
if (handled) return;
}
return commitMutation(
selection,
{ type: "add-keyframe", animationId, percentage, properties },
{ label: `Add keyframe at ${percentage}%`, softReload: true },
);
},
[commitMutation],
[
commitMutation,
activeCompPath,
sdkSession,
writeProjectFile,
editHistory,
reloadPreview,
domEditSaveTimestampRef,
],
);
const removeKeyframe = useCallback(
(selection: DomEditSelection, animationId: string, percentage: number) => {
// ponytail: SDK removeGsapKeyframe uses keyframeIndex (not percentage); mismatch with
// Studio's percentage-based API. Resolving index requires parsing GSAP state at call
// time — deferred. removeKeyframe stays server-authoritative.
const sourceFile = selection.sourceFile || activeCompPath || "index.html";
const mutation = { type: "remove-keyframe", animationId, percentage };
void executeOptimisticKeyframeCacheUpdate({
@@ -126,6 +210,7 @@ export function useGsapKeyframeOps({
animationId: string,
resolvedFromValues?: Record<string, number | string>,
) => {
// ponytail: no SDK equivalent; convertToKeyframes stays server-authoritative (T6f scope)
return commitMutation(
selection,
{ type: "convert-to-keyframes", animationId, resolvedFromValues },
@@ -137,6 +222,7 @@ export function useGsapKeyframeOps({
const removeAllKeyframes = useCallback(
(selection: DomEditSelection, animationId: string) => {
// ponytail: no SDK equivalent for remove-all-keyframes; stays server-authoritative
commitMutationSafely(
selection,
{ type: "remove-all-keyframes", animationId },