diff --git a/packages/studio/src/components/editor/manualEditsDomPatches.ts b/packages/studio/src/components/editor/manualEditsDomPatches.ts index 1cfbf3e0a8..294b8406cc 100644 --- a/packages/studio/src/components/editor/manualEditsDomPatches.ts +++ b/packages/studio/src/components/editor/manualEditsDomPatches.ts @@ -118,7 +118,7 @@ export function buildClearPathOffsetPatches(element: HTMLElement): PatchOperatio /* ── Box size patches ────────────────────────────────────────────── */ -const BOX_SIZE_STYLE_PROPS = [ +export const BOX_SIZE_STYLE_PROPS = [ "width", "height", "min-width", diff --git a/packages/studio/src/components/editor/manualOffsetDrag.test.ts b/packages/studio/src/components/editor/manualOffsetDrag.test.ts index 578229354e..21150fc3d4 100644 --- a/packages/studio/src/components/editor/manualOffsetDrag.test.ts +++ b/packages/studio/src/components/editor/manualOffsetDrag.test.ts @@ -686,7 +686,10 @@ describe("a resize's anchor member", () => { expect(member.element.style.getPropertyValue("translate")).toBe("-10.25px 3.5px"); }); - it("leaves a box whose size GSAP tweens to the GSAP writer", () => { - expect(anchorMember({ width: 300 }).plainTranslate).toBe(false); - }); + it.each(["width", "maxWidth", "minHeight", "flexBasis", "scale"])( + "leaves a box whose %s GSAP tweens, which the CSS box writer also sets, to the GSAP writer", + (channel) => { + expect(anchorMember({ [channel]: 300 }).plainTranslate).toBe(false); + }, + ); }); diff --git a/packages/studio/src/hooks/gsapRuntimeKeyframes.ts b/packages/studio/src/hooks/gsapRuntimeKeyframes.ts index fff34b57d2..bc9878e70d 100644 --- a/packages/studio/src/hooks/gsapRuntimeKeyframes.ts +++ b/packages/studio/src/hooks/gsapRuntimeKeyframes.ts @@ -11,6 +11,7 @@ import { buildArcPath, type ArcPathConfig } from "@hyperframes/core/gsap-parser-acorn"; import { parsePercentageKeyframes, toAbsoluteTime } from "./gsapShared"; import { roundTo3 } from "../utils/rounding"; +import { BOX_SIZE_STYLE_PROPS } from "../components/editor/manualEditsDomPatches"; /** * A GSAP tween's `vars` object — intentionally open: it mixes channel values @@ -446,9 +447,13 @@ export function gsapWritesPosition(el: Element): boolean { return !!cache?.renderTransform || gsapWritesChannels(el, MOVE_CHANNELS); } -/** GSAP owns this element's box: its position, or its width or height. Else a resize writes CSS. */ +const BOX_CHANNELS = BOX_SIZE_STYLE_PROPS.map((prop) => + prop.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()), +); + +/** GSAP owns this element's box: its position, or any property the CSS box writer sets. Else a resize writes CSS. */ export function gsapWritesBox(el: Element): boolean { - return gsapWritesPosition(el) || gsapWritesChannels(el, ["width", "height"]); + return gsapWritesPosition(el) || gsapWritesChannels(el, BOX_CHANNELS); } /** `hasNonHoldTweenForElement` for an element in hand, read from its own window's timelines. */ diff --git a/packages/studio/src/hooks/useDomGeometryCommit.test.tsx b/packages/studio/src/hooks/useDomGeometryCommit.test.tsx index 4a7c3814b0..2469187526 100644 --- a/packages/studio/src/hooks/useDomGeometryCommit.test.tsx +++ b/packages/studio/src/hooks/useDomGeometryCommit.test.tsx @@ -176,6 +176,28 @@ describe("useDomGeometryCommit, from the package entry", () => { unmount(); }); + it("saves a GSAP-free resize as its inline size, with no GSAP script and no animation read", async () => { + const calls = stubPatchServer(); + const { element, recordEdit, hook, unmount } = renderHost(); + + await expect( + hook().commitBoxSize(makeSelection("card", element), { width: 300, height: 90 }), + ).resolves.toEqual({ ok: true }); + + expect(element.style.getPropertyValue("width")).toBe("300px"); + expect(calls.patches).toEqual([ + expect.objectContaining({ + operations: expect.arrayContaining([ + { type: "inline-style", property: "width", value: "300px" }, + { type: "inline-style", property: "height", value: "90px" }, + ]), + }), + ]); + expect(calls.urls.filter((url) => url.includes("gsap"))).toEqual([]); + expect(recordEdit).toHaveBeenCalledTimes(1); + unmount(); + }); + it("puts a GSAP-free element's translate back when its move cannot be saved", async () => { stubPatchServer(500); const { element, hook, unmount } = renderHost();