fix(core): split-into-property-groups and replace-with-keyframes mutations (#1355)

* fix(core): per-property-group keyframe foundations

Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.

* fix(core): add split-into-property-groups and replace-with-keyframes mutations

Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
This commit is contained in:
Miguel Ángel
2026-06-12 00:12:26 -04:00
committed by GitHub
parent 889e9f09ad
commit b6bf1b1190
2 changed files with 49 additions and 4 deletions
+39 -2
View File
@@ -409,6 +409,20 @@ type GsapMutationRequest =
}>;
ease?: string;
}
| {
type: "replace-with-keyframes";
animationId: string;
targetSelector: string;
position: number;
duration: number;
keyframes: Array<{
percentage: number;
properties: Record<string, number | string>;
ease?: string;
auto?: boolean;
}>;
ease?: string;
}
| {
type: "split-animations";
originalId: string;
@@ -416,6 +430,10 @@ type GsapMutationRequest =
splitTime: number;
elementStart: number;
elementDuration: number;
}
| {
type: "split-into-property-groups";
animationId: string;
};
// ── GSAP mutation executor ──────────────────────────────────────────────────
@@ -445,6 +463,7 @@ async function executeGsapMutation(
removeArcPathFromScript,
addAnimationWithKeyframesToScript,
splitAnimationsInScript,
splitIntoPropertyGroups,
} = parser;
function requireAnimation(
@@ -617,6 +636,18 @@ async function executeGsapMutation(
);
return result.script;
}
case "replace-with-keyframes": {
const script = removeAnimationFromScript(block.scriptText, body.animationId);
const added = addAnimationWithKeyframesToScript(
script,
body.targetSelector,
body.position,
body.duration,
body.keyframes,
body.ease,
);
return added.script;
}
case "split-animations": {
if (
typeof body.originalId !== "string" ||
@@ -647,6 +678,10 @@ async function executeGsapMutation(
elementDuration: body.elementDuration,
});
}
case "split-into-property-groups": {
const result = splitIntoPropertyGroups(block.scriptText, body.animationId);
return result.script;
}
default:
return respond({ error: `unknown mutation type: ${(body as { type: string }).type}` }, 400);
}
@@ -1061,8 +1096,9 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void {
if (result instanceof Response) return result;
const newScript = typeof result === "string" ? result : result.script;
const newHtml = block.replaceScript(newScript);
if (newHtml !== html) {
const changed = newScript !== block.scriptText;
const newHtml = changed ? block.replaceScript(newScript) : html;
if (changed) {
writeFileSync(res.absPath, newHtml, "utf-8");
}
@@ -1070,6 +1106,7 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void {
const freshParsed = parseGsapScript(newScript);
const responsePayload: Record<string, unknown> = {
ok: true,
changed,
parsed: freshParsed,
before: html,
after: newHtml,