diff --git a/packages/core/package-subpaths.json b/packages/core/package-subpaths.json index 784e612897..bc58a5c24a 100644 --- a/packages/core/package-subpaths.json +++ b/packages/core/package-subpaths.json @@ -86,6 +86,12 @@ "types": "./dist/studioPreviewMark.d.ts", "environments": ["browser", "bun", "node"] }, + "./keyframe-defaults": { + "source": "./src/keyframeDefaults.ts", + "runtime": "./dist/keyframeDefaults.js", + "types": "./dist/keyframeDefaults.d.ts", + "environments": ["browser", "bun", "node"] + }, "./gsap-cdn": { "source": "./src/gsapCdn.ts", "runtime": "./dist/gsapCdn.js", diff --git a/packages/core/package.json b/packages/core/package.json index 6f76f19a88..6f503e3a08 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -98,6 +98,12 @@ "import": "./src/studioPreviewMark.ts", "types": "./src/studioPreviewMark.ts" }, + "./keyframe-defaults": { + "bun": "./src/keyframeDefaults.ts", + "node": "./dist/keyframeDefaults.js", + "import": "./src/keyframeDefaults.ts", + "types": "./src/keyframeDefaults.ts" + }, "./gsap-cdn": { "bun": "./src/gsapCdn.ts", "node": "./dist/gsapCdn.js", @@ -578,6 +584,10 @@ "import": "./dist/studioPreviewMark.js", "types": "./dist/studioPreviewMark.d.ts" }, + "./keyframe-defaults": { + "import": "./dist/keyframeDefaults.js", + "types": "./dist/keyframeDefaults.d.ts" + }, "./gsap-cdn": { "import": "./dist/gsapCdn.js", "types": "./dist/gsapCdn.d.ts" diff --git a/packages/core/src/keyframeDefaults.test.ts b/packages/core/src/keyframeDefaults.test.ts new file mode 100644 index 0000000000..65c1df8ea4 --- /dev/null +++ b/packages/core/src/keyframeDefaults.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { KEYFRAME_PROPERTY_DEFAULTS } from "./keyframeDefaults"; + +describe("KEYFRAME_PROPERTY_DEFAULTS", () => { + it("holds the rest value of each numeric keyframe prop", () => { + expect(KEYFRAME_PROPERTY_DEFAULTS).toEqual({ + opacity: 1, + x: 0, + y: 0, + scale: 1, + scaleX: 1, + scaleY: 1, + rotation: 0, + width: 100, + height: 100, + }); + }); + + it.each(["color", "filter", "backgroundColor"])("has no default for %s", (prop) => { + expect(KEYFRAME_PROPERTY_DEFAULTS[prop]).toBeUndefined(); + }); +}); diff --git a/packages/core/src/keyframeDefaults.ts b/packages/core/src/keyframeDefaults.ts new file mode 100644 index 0000000000..7d96934b53 --- /dev/null +++ b/packages/core/src/keyframeDefaults.ts @@ -0,0 +1,12 @@ +// A prop absent here has no safe static default; each caller decides how to treat it. +export const KEYFRAME_PROPERTY_DEFAULTS: Readonly> = { + opacity: 1, + x: 0, + y: 0, + scale: 1, + scaleX: 1, + scaleY: 1, + rotation: 0, + width: 100, + height: 100, +}; diff --git a/packages/sdk/src/engine/keyframeBackfill.ts b/packages/sdk/src/engine/keyframeBackfill.ts index 2e050dd0d5..3692ea58be 100644 --- a/packages/sdk/src/engine/keyframeBackfill.ts +++ b/packages/sdk/src/engine/keyframeBackfill.ts @@ -12,25 +12,7 @@ * keyframes (GSAP reads the rest value from the DOM), matching recast (which skips * any prop whose default is null). */ - -// Numeric rest values for editable transform/style props. Props absent here have -// no safe static default and are intentionally omitted from the backfill set. -// -// KEEP IN SYNC WITH packages/studio/src/hooks/gsapShared.ts:PROPERTY_DEFAULTS — -// the studio (recast) and SDK (acorn) paths must derive the same defaults or -// SDK-written keyframes drift from server-written ones (the exact bug this fixes). -// TODO: lift the canonical table into @hyperframes/core and import from both. -const KEYFRAME_PROPERTY_DEFAULTS: Record = { - opacity: 1, - x: 0, - y: 0, - scale: 1, - scaleX: 1, - scaleY: 1, - rotation: 0, - width: 100, - height: 100, -}; +import { KEYFRAME_PROPERTY_DEFAULTS } from "@hyperframes/core/keyframe-defaults"; /** Derive the backfillDefaults for an add-keyframe op (numeric-default props only). */ export function deriveKeyframeBackfillDefaults( diff --git a/packages/studio/src/hooks/gsapShared.ts b/packages/studio/src/hooks/gsapShared.ts index a406c6138e..60575aef3d 100644 --- a/packages/studio/src/hooks/gsapShared.ts +++ b/packages/studio/src/hooks/gsapShared.ts @@ -25,17 +25,7 @@ export interface IframeGsap { // ── Constants ───────────────────────────────────────────────────────────────── -export const PROPERTY_DEFAULTS: Record = { - opacity: 1, - x: 0, - y: 0, - scale: 1, - scaleX: 1, - scaleY: 1, - rotation: 0, - width: 100, - height: 100, -}; +export { KEYFRAME_PROPERTY_DEFAULTS as PROPERTY_DEFAULTS } from "@hyperframes/core/keyframe-defaults"; /** A tween's eases for re-writing it as keyframes: a flat tween's ease must become easeEach, * since GSAP eases each percentage segment power1.inOut whatever the tween-level ease says. */