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
+102 -21
View File
@@ -47,6 +47,7 @@ import {
addAnimationToScript,
updateAnimationInScript,
removeAnimationFromScript,
removePropertyFromAnimation,
addKeyframeToScript,
removeKeyframeFromScript,
updateKeyframeInScript,
@@ -136,7 +137,48 @@ function targets(target: HfId | HfId[]): HfId[] {
// ─── Op dispatch ────────────────────────────────────────────────────────────
function dispatchRemoveGsapKeyframe(
parsed: ParsedDocument,
op: Extract<EditOp, { type: "removeGsapKeyframe" }>,
): MutationResult {
return "percentage" in op
? handleRemoveGsapKeyframeByPercentage(parsed, op.animationId, op.percentage)
: handleRemoveGsapKeyframe(parsed, op.animationId, op.keyframeIndex);
}
function applyGsapOp(parsed: ParsedDocument, op: EditOp): MutationResult | undefined {
switch (op.type) {
case "addGsapTween":
return handleAddGsapTween(parsed, op.target, op.tween);
case "setGsapTween":
return handleSetGsapTween(parsed, op.animationId, op.properties);
case "removeGsapProperty":
return handleRemoveGsapProperty(parsed, op.animationId, op.property, op.from);
case "removeGsapTween":
return handleRemoveGsapTween(parsed, op.animationId);
case "deleteAllForSelector":
return handleDeleteAllForSelector(parsed, op.selector);
case "setGsapKeyframe":
return handleSetGsapKeyframe(
parsed,
op.animationId,
op.keyframeIndex,
op.position,
op.value,
op.ease,
);
case "addGsapKeyframe":
return handleAddGsapKeyframe(parsed, op.animationId, op.position, op.value);
case "removeGsapKeyframe":
return dispatchRemoveGsapKeyframe(parsed, op);
default:
return undefined;
}
}
export function applyOp(parsed: ParsedDocument, op: EditOp): MutationResult {
const gsap = applyGsapOp(parsed, op);
if (gsap !== undefined) return gsap;
switch (op.type) {
case "setStyle":
return handleSetStyle(parsed, targets(op.target), op.styles);
@@ -160,31 +202,14 @@ export function applyOp(parsed: ParsedDocument, op: EditOp): MutationResult {
return handleSetCompositionMetadata(parsed, op);
case "setVariableValue":
return handleSetVariableValue(parsed, op.id, op.value);
case "addGsapTween":
return handleAddGsapTween(parsed, op.target, op.tween);
case "setGsapTween":
return handleSetGsapTween(parsed, op.animationId, op.properties);
case "removeGsapTween":
return handleRemoveGsapTween(parsed, op.animationId);
case "setGsapKeyframe":
return handleSetGsapKeyframe(
parsed,
op.animationId,
op.keyframeIndex,
op.position,
op.value,
op.ease,
);
case "addGsapKeyframe":
return handleAddGsapKeyframe(parsed, op.animationId, op.position, op.value);
case "removeGsapKeyframe":
return handleRemoveGsapKeyframe(parsed, op.animationId, op.keyframeIndex);
case "setClassStyle":
return handleSetClassStyle(parsed, op.selector, op.styles);
case "addLabel":
return handleAddLabel(parsed, op.name, op.position);
case "removeLabel":
return handleRemoveLabel(parsed, op.name);
case "setClassStyle":
return handleSetClassStyle(parsed, op.selector, op.styles);
default:
throw new UnsupportedOpError((op as EditOp).type);
}
}
@@ -689,6 +714,20 @@ function handleSetGsapTween(
return gsapScriptChange(script, newScript);
}
function handleRemoveGsapProperty(
parsed: ParsedDocument,
animationId: string,
property: string,
from: boolean | undefined,
): MutationResult {
const script = getGsapScript(parsed.document);
if (!script) return EMPTY;
const newScript = removePropertyFromAnimation(script, animationId, property, from ?? false);
if (newScript === script) return EMPTY;
setGsapScript(parsed.document, newScript);
return gsapScriptChange(script, newScript);
}
function handleRemoveGsapTween(parsed: ParsedDocument, animationId: string): MutationResult {
const script = getGsapScript(parsed.document);
if (!script) return EMPTY;
@@ -698,6 +737,24 @@ function handleRemoveGsapTween(parsed: ParsedDocument, animationId: string): Mut
return gsapScriptChange(script, newScript);
}
function handleDeleteAllForSelector(parsed: ParsedDocument, selector: string): MutationResult {
const script = getGsapScript(parsed.document);
if (!script) return EMPTY;
const parsedForWrite = parseGsapScriptAcornForWrite(script);
if (!parsedForWrite) return EMPTY;
const matching = parsedForWrite.located.filter((l) => l.animation.targetSelector === selector);
if (matching.length === 0) return EMPTY;
let newScript = script;
for (const m of [...matching].reverse()) {
newScript = removeAnimationFromScript(newScript, m.id);
}
if (newScript === script) return EMPTY;
setGsapScript(parsed.document, newScript);
// ponytail: skips stripStudioEditsFromTarget (data-hf-studio-path-offset cleanup) —
// studio path offset is cosmetic once all animations are gone; session reloads after write
return gsapScriptChange(script, newScript);
}
function resolveKeyframe(parsed: ParsedDocument, animationId: string, keyframeIndex: number) {
const script = getGsapScript(parsed.document);
if (!script) return null;
@@ -772,6 +829,28 @@ function handleAddGsapKeyframe(
return gsapScriptChange(script, newScript);
}
function handleRemoveGsapKeyframeByPercentage(
parsed: ParsedDocument,
animationId: string,
percentage: number,
): MutationResult {
const script = getGsapScript(parsed.document);
if (!script) return EMPTY;
const parsedForWrite = parseGsapScriptAcornForWrite(script);
const located = parsedForWrite?.located.find((l) => l.id === animationId);
const kfs = located?.animation.keyframes?.keyframes;
if (!kfs) return EMPTY;
// No-op on ambiguity: duplicate-percentage keyframes can't be disambiguated.
const TOLERANCE = 0.001;
const matches = kfs.filter((k) => Math.abs(k.percentage - percentage) <= TOLERANCE);
if (matches.length !== 1) return EMPTY;
const pct = matches[0]!.percentage;
const newScript = removeKeyframeFromScript(script, animationId, pct);
if (newScript === script) return EMPTY;
setGsapScript(parsed.document, newScript);
return gsapScriptChange(script, newScript);
}
function handleRemoveGsapKeyframe(
parsed: ParsedDocument,
animationId: string,
@@ -873,7 +952,9 @@ export function validateOp(parsed: ParsedDocument, op: EditOp): CanResult {
case "setGsapKeyframe":
case "addGsapKeyframe":
case "removeGsapKeyframe":
case "removeGsapProperty":
case "removeGsapTween":
case "deleteAllForSelector":
case "removeLabel":
if (getGsapScript(parsed.document) === null)
return canErr(
+3
View File
@@ -102,7 +102,10 @@ export type EditOp =
value: Record<string, unknown>;
}
| { type: "removeGsapKeyframe"; animationId: string; keyframeIndex: number }
| { type: "removeGsapKeyframe"; animationId: string; percentage: number }
| { type: "removeGsapProperty"; animationId: string; property: string; from?: boolean }
| { type: "removeGsapTween"; animationId: string }
| { type: "deleteAllForSelector"; selector: string }
| { type: "addLabel"; name: string; position: number }
| { type: "removeLabel"; name: string };