fix(studio): route panel property commits to a group-owning set

commitStaticSet merged every property into the FIRST set found for the
selector: a panel W edit on an element whose only set was positional
produced tl.set("#el",{x,y,width}) — a mixed-group set the split
machinery exists to prevent — labeled "Set 3D transform" in undo.

Commits now batch per property group into a set that owns that group
(exact group match, then a mixed set already carrying the group, then a
fresh off-timeline gsap.set), with undo labels derived from the group
(Move layer / Resize layer / Rotate layer / Set 3D transform).
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-11 04:08:00 -04:00
parent cbfb6ba943
commit 066c3dae37
2 changed files with 130 additions and 22 deletions
@@ -39,15 +39,16 @@ type Commit = (
) => Promise<void>;
/** Renders the hook and hands its commit function to the caller via a ref callback. */
function renderCommitHook(
mutations: Array<Record<string, unknown>>,
function renderHookWith(
animations: GsapAnimation[],
onMutation: (mutation: Record<string, unknown>, label: string) => void,
onReady: (commit: Commit) => void,
) {
function Harness() {
const { commitAnimatedProperties } = useAnimatedPropertyCommit({
selectedGsapAnimations: [keyframedAnim],
gsapCommitMutation: async (_sel, mutation) => {
mutations.push(mutation);
selectedGsapAnimations: animations,
gsapCommitMutation: async (_sel, mutation, options) => {
onMutation(mutation, options.label);
},
addGsapAnimation: vi.fn(),
convertToKeyframes: vi.fn(),
@@ -66,6 +67,13 @@ function renderCommitHook(
return root;
}
function renderCommitHook(
mutations: Array<Record<string, unknown>>,
onReady: (commit: Commit) => void,
) {
return renderHookWith([keyframedAnim], (mutation) => mutations.push(mutation), onReady);
}
// Regression (#1808): a "3D transform" / design-panel property edit on an
// element that already has a keyframed tween is the ACTUAL path a manual
// canvas nudge exercises (not the raw drag intercept) — with auto-keyframe
@@ -103,3 +111,56 @@ describe("useAnimatedPropertyCommit — autoKeyframeEnabled toggle (#1808)", ()
act(() => root.unmount());
});
});
// Regression: commitStaticSet picked the FIRST `set` for the selector with no
// group check — a panel W edit on a static element merged `width` into the
// POSITION set (`tl.set("#el",{x,y,width})`), a mixed-group set the split
// machinery exists to prevent, labeled "Set 3D transform" in undo history.
describe("commitStaticSet group routing", () => {
const positionSet = {
id: "#box-set-0-position",
targetSelector: "#box",
propertyGroup: "position",
method: "set",
properties: { x: 10, y: 20 },
} as unknown as GsapAnimation;
function renderStaticHook(
committed: Array<{ mutation: Record<string, unknown>; label: string }>,
onReady: (commit: Commit) => void,
) {
return renderHookWith(
[positionSet],
(mutation, label) => committed.push({ mutation, label }),
onReady,
);
}
it("width edit creates a size set instead of contaminating the position set", async () => {
const committed: Array<{ mutation: Record<string, unknown>; label: string }> = [];
let commit!: Commit;
renderStaticHook(committed, (c) => (commit = c));
await act(async () => {
await commit(selection, { width: 500 });
});
const updates = committed.filter((c) => c.mutation.type === "update-properties");
expect(updates).toHaveLength(0);
const adds = committed.filter((c) => c.mutation.type === "add");
expect(adds).toHaveLength(1);
expect(adds[0]!.mutation.properties).toEqual({ width: 500 });
expect(adds[0]!.label).toBe("Resize layer");
});
it("x edit updates the position set with a Move label", async () => {
const committed: Array<{ mutation: Record<string, unknown>; label: string }> = [];
let commit!: Commit;
renderStaticHook(committed, (c) => (commit = c));
await act(async () => {
await commit(selection, { x: 400 });
});
const update = committed.find((c) => c.mutation.type === "update-properties");
expect(update).toBeDefined();
expect(update!.mutation.animationId).toBe("#box-set-0-position");
expect(update!.label).toBe("Move layer");
});
});