feat(sdk): stage 4 — canUndo/canRedo, removeElement GSAP cascade, override-set cleanup (#1431)

* feat(sdk): stage 4 — canUndo/canRedo, removeElement GSAP cascade, override-set cleanup

* docs(sdk): document cascadeRemoveAnimations bare-id v1 limitation for scoped ids

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

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

* chore(sdk): remove sdk-status-report.txt from source tree

Internal planning artifact should not be committed to the repo.

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-15 02:07:39 -07:00
committed by GitHub
co-authored by Claude Sonnet 4.6 Miguel Ángel
parent 5ecaac1fcb
commit 9627e03fa7
5 changed files with 186 additions and 3 deletions
+40 -3
View File
@@ -345,9 +345,7 @@ function handleSetTiming(
// have zero playback effect; the script's position/duration silently overrides them.
if (parsedGsap && currentScript) {
for (const { id: animId, animation } of parsedGsap.located) {
const sel = animation.targetSelector;
if (sel !== `[data-hf-id="${id}"]` && sel !== `[data-hf-id='${id}']` && sel !== `#${id}`)
continue;
if (!selectorMatchesId(animation.targetSelector, id)) continue;
const updates: Partial<GsapAnimation> = {};
if (timing.start !== undefined && newStart !== null) updates.position = newStart;
if (timing.duration !== undefined && newDuration !== null) updates.duration = newDuration;
@@ -399,6 +397,9 @@ function handleSetHold(
function handleRemoveElement(parsed: ParsedDocument, ids: HfId[]): MutationResult {
const result: MutationResult = { forward: [], inverse: [] };
const origScript = getGsapScript(parsed.document);
let currentScript = origScript;
for (const id of ids) {
const el = findById(parsed.document, id);
if (!el) continue;
@@ -412,7 +413,17 @@ function handleRemoveElement(parsed: ParsedDocument, ids: HfId[]): MutationResul
const path = elementPath(id);
result.forward.push(patchRemove(path));
result.inverse.push(patchAdd(path, { html, parentId, siblingIndex }));
if (currentScript) currentScript = cascadeRemoveAnimations(currentScript, id);
}
if (origScript && currentScript && currentScript !== origScript) {
setGsapScript(parsed.document, currentScript);
const gsapResult = gsapScriptChange(origScript, currentScript);
result.forward.push(...gsapResult.forward);
result.inverse.push(...gsapResult.inverse);
}
return result;
}
@@ -488,6 +499,32 @@ function handleSetVariableValue(
return { forward: [p.forward], inverse: [p.inverse] };
}
// ─── GSAP selector helpers ───────────────────────────────────────────────────
function selectorMatchesId(selector: string, id: HfId): boolean {
return (
selector === `[data-hf-id="${id}"]` ||
selector === `[data-hf-id='${id}']` ||
selector === `#${id}`
);
}
// v1 limitation: uses bare-id matching across the whole script, so a selector targeting
// "hf-leaf" will cascade-remove animations for both "hf-parent/hf-leaf" and any other
// element whose scoped or bare id matches "hf-leaf". Acceptable for typical single-comp
// use; sub-composition authors with leaf-id collisions should use fully-qualified selectors.
function cascadeRemoveAnimations(script: string, id: HfId): string {
const parsedGsap = parseGsapScriptAcornForWrite(script);
if (!parsedGsap) return script;
let current = script;
for (const { id: animId, animation } of parsedGsap.located) {
if (selectorMatchesId(animation.targetSelector, id)) {
current = removeAnimationFromScript(current, animId);
}
}
return current;
}
// ─── setClassStyle handler ────────────────────────────────────────────────────
function handleSetClassStyle(