feat(sdk,studio): ws-1.2 — percentage-based removeGsapKeyframe (#1498)

* feat(sdk,studio): ws-1.2 — percentage-based removeGsapKeyframe

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(sdk,studio): ws-1.3 — removeGsapProperty SDK op + Studio hook cutover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(sdk,studio): ws-1.4 — deleteAllForSelector SDK op + Studio hook cutover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* fix(core): cascade-remove GSAP tweens in removeElementFromHtml (WS-2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

---------

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
This commit is contained in:
Vance Ingalls
2026-06-17 16:49:47 -07:00
committed by GitHub
co-authored by Miguel Ángel Claude Sonnet 4.6
parent a5016ed416
commit ceb815c318
9 changed files with 317 additions and 96 deletions
+70 -32
View File
@@ -192,32 +192,44 @@ type SdkGsapTweenOp =
| { kind: "set"; animationId: string; properties: Partial<GsapTweenSpec> }
| { kind: "remove"; animationId: string };
export async function sdkGsapTweenPersist(
export function sdkGsapTweenPersist(
targetPath: string,
op: SdkGsapTweenOp,
sdkSession: Composition | null | undefined,
deps: CutoverDeps,
options?: CutoverOptions,
): Promise<boolean> {
if (op.kind === "add" && sdkSession && !sdkSession.getElement(op.target))
return Promise.resolve(false);
// dispatchGsapOpAndPersist returns false on before===after — that catches stale
// animationIds and unsupported shapes (e.g. from-prop on a plain tween), falling
// back to the server path. This subsumes explicit existence guards for set/remove.
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) => {
s.batch(() => {
if (op.kind === "add") {
s.addGsapTween(op.target, op.spec);
} else if (op.kind === "set") {
s.setGsapTween(op.animationId, op.properties);
} else {
s.removeGsapTween(op.animationId);
}
});
});
}
async function dispatchGsapOpAndPersist(
targetPath: string,
sdkSession: Composition | null | undefined,
deps: CutoverDeps,
options: CutoverOptions | undefined,
dispatch: (s: Composition) => void,
): Promise<boolean> {
if (!sdkSession) return false;
if (wrongCompositionFile(deps, targetPath)) return false;
try {
if (op.kind === "add" && !sdkSession.getElement(op.target)) return false;
const before = sdkSession.serialize();
sdkSession.batch(() => {
if (op.kind === "add") {
sdkSession.addGsapTween(op.target, op.spec);
} else if (op.kind === "set") {
sdkSession.setGsapTween(op.animationId, op.properties);
} else {
sdkSession.removeGsapTween(op.animationId);
}
});
dispatch(sdkSession);
const after = sdkSession.serialize();
// No-op (stale animationId, unsupported shape e.g. from-prop on a plain
// tween): fall back to the server path so it surfaces the proper error
// instead of writing a phantom before==after undo step. Subsumes a
// per-op existence guard for the set/remove branches.
if (after === before) return false;
await persistSdkSerialize(after, targetPath, before, deps, options);
trackStudioEvent("sdk_cutover_success", { opCount: 1 });
@@ -228,7 +240,7 @@ export async function sdkGsapTweenPersist(
}
}
export async function sdkGsapKeyframePersist(
export function sdkGsapKeyframePersist(
targetPath: string,
animationId: string,
position: number,
@@ -237,22 +249,48 @@ export async function sdkGsapKeyframePersist(
deps: CutoverDeps,
options?: CutoverOptions,
): Promise<boolean> {
if (!sdkSession) return false;
if (wrongCompositionFile(deps, targetPath)) return false;
try {
const before = sdkSession.serialize();
sdkSession.batch(() =>
sdkSession.dispatch({ type: "addGsapKeyframe", animationId, position, value }),
);
const after = sdkSession.serialize();
if (after === before) return false;
await persistSdkSerialize(after, targetPath, before, deps, options);
trackStudioEvent("sdk_cutover_success", { opCount: 1 });
return true;
} catch (err) {
trackStudioEvent("sdk_cutover_fallback", { error: String(err) });
return false;
}
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) =>
s.batch(() => s.dispatch({ type: "addGsapKeyframe", animationId, position, value })),
);
}
export function sdkGsapRemoveKeyframePersist(
targetPath: string,
animationId: string,
percentage: number,
sdkSession: Composition | null | undefined,
deps: CutoverDeps,
options?: CutoverOptions,
): Promise<boolean> {
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) =>
s.dispatch({ type: "removeGsapKeyframe", animationId, percentage }),
);
}
export function sdkGsapRemovePropertyPersist(
targetPath: string,
animationId: string,
property: string,
from: boolean,
sdkSession: Composition | null | undefined,
deps: CutoverDeps,
options?: CutoverOptions,
): Promise<boolean> {
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) =>
s.dispatch({ type: "removeGsapProperty", animationId, property, from }),
);
}
export function sdkGsapDeleteAllForSelectorPersist(
targetPath: string,
selector: string,
sdkSession: Composition | null | undefined,
deps: CutoverDeps,
options?: CutoverOptions,
): Promise<boolean> {
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) =>
s.dispatch({ type: "deleteAllForSelector", selector }),
);
}
export async function sdkDeletePersist(