feat(studio): route GSAP tween add/update/delete through SDK (§3.5 PR1) (#1469)

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
This commit is contained in:
Vance Ingalls
2026-06-17 16:42:02 -07:00
committed by GitHub
co-authored by Miguel Ángel
parent e65c3c7918
commit 592f7c775d
7 changed files with 460 additions and 39 deletions
@@ -1,11 +1,33 @@
import { useCallback, useEffect, useRef } from "react";
import type { Composition } from "@hyperframes/sdk";
import type { DomEditSelection } from "../components/editor/domEditingTypes";
import { sdkGsapTweenPersist } from "../utils/sdkCutover";
import { PROPERTY_DEFAULTS } from "./gsapScriptCommitHelpers";
import type { SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
import type { EditHistoryKind } from "../utils/editHistory";
const DEBOUNCE_MS = 150;
export function useGsapPropertyDebounce(commitMutationSafely: SafeGsapCommitMutation) {
interface SdkPropertyDeps {
sdkSession?: Composition | null;
writeProjectFile?: (path: string, content: string) => Promise<void>;
editHistory?: {
recordEdit: (entry: {
label: string;
kind: EditHistoryKind;
coalesceKey?: string;
files: Record<string, { before: string; after: string }>;
}) => Promise<void>;
};
reloadPreview?: () => void;
domEditSaveTimestampRef?: React.MutableRefObject<number>;
activeCompPath?: string | null;
}
export function useGsapPropertyDebounce(
commitMutationSafely: SafeGsapCommitMutation,
sdkDeps?: SdkPropertyDeps,
) {
const pendingPropertyEditRef = useRef<{
selection: DomEditSelection;
animationId: string;
@@ -14,21 +36,51 @@ export function useGsapPropertyDebounce(commitMutationSafely: SafeGsapCommitMuta
} | null>(null);
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const flushPendingPropertyEdit = useCallback(() => {
const pending = pendingPropertyEditRef.current;
if (!pending) return;
pendingPropertyEditRef.current = null;
const { selection, animationId, property, value } = pending;
commitMutationSafely(
selection,
{ type: "update-property", animationId, property, value },
{
label: `Edit GSAP ${property}`,
coalesceKey: `gsap:${animationId}:${property}`,
softReload: true,
},
);
}, [commitMutationSafely]);
// fallow-ignore-next-line complexity
const flushPendingPropertyEdit = useCallback(
// fallow-ignore-next-line complexity
async () => {
const pending = pendingPropertyEditRef.current;
if (!pending) return;
pendingPropertyEditRef.current = null;
const { selection, animationId, property, value } = pending;
const {
sdkSession,
writeProjectFile,
editHistory,
reloadPreview,
domEditSaveTimestampRef,
activeCompPath,
} = sdkDeps ?? {};
if (
sdkSession &&
writeProjectFile &&
editHistory &&
reloadPreview &&
domEditSaveTimestampRef
) {
const targetPath = selection.sourceFile || activeCompPath || "index.html";
const handled = await sdkGsapTweenPersist(
targetPath,
{ kind: "set", animationId, properties: { properties: { [property]: value } } },
sdkSession,
{ editHistory, writeProjectFile, reloadPreview, domEditSaveTimestampRef },
{ label: `Edit GSAP ${property}`, coalesceKey: `gsap:${animationId}:${property}` },
);
if (handled) return;
}
commitMutationSafely(
selection,
{ type: "update-property", animationId, property, value },
{
label: `Edit GSAP ${property}`,
coalesceKey: `gsap:${animationId}:${property}`,
softReload: true,
},
);
},
[commitMutationSafely, sdkDeps],
);
const updateGsapProperty = useCallback(
(
@@ -39,7 +91,9 @@ export function useGsapPropertyDebounce(commitMutationSafely: SafeGsapCommitMuta
) => {
pendingPropertyEditRef.current = { selection, animationId, property, value };
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = setTimeout(flushPendingPropertyEdit, DEBOUNCE_MS);
debounceTimerRef.current = setTimeout(() => {
void flushPendingPropertyEdit();
}, DEBOUNCE_MS);
},
[flushPendingPropertyEdit],
);
@@ -47,12 +101,14 @@ export function useGsapPropertyDebounce(commitMutationSafely: SafeGsapCommitMuta
useEffect(() => {
return () => {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
flushPendingPropertyEdit();
void flushPendingPropertyEdit();
};
}, [flushPendingPropertyEdit]);
// fallow-ignore-next-line complexity
const addGsapProperty = useCallback(
(selection: DomEditSelection, animationId: string, property: string) => {
// fallow-ignore-next-line complexity
async (selection: DomEditSelection, animationId: string, property: string) => {
let defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
const el = selection.element;
if (property === "width" || property === "height") {
@@ -62,17 +118,43 @@ export function useGsapPropertyDebounce(commitMutationSafely: SafeGsapCommitMuta
const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
defaultValue = cs ? Number.parseFloat(cs.opacity) || 1 : 1;
}
const {
sdkSession,
writeProjectFile,
editHistory,
reloadPreview,
domEditSaveTimestampRef,
activeCompPath,
} = sdkDeps ?? {};
if (
sdkSession &&
writeProjectFile &&
editHistory &&
reloadPreview &&
domEditSaveTimestampRef
) {
const targetPath = selection.sourceFile || activeCompPath || "index.html";
const handled = await sdkGsapTweenPersist(
targetPath,
{ kind: "set", animationId, properties: { properties: { [property]: defaultValue } } },
sdkSession,
{ editHistory, writeProjectFile, reloadPreview, domEditSaveTimestampRef },
{ label: `Add GSAP ${property}` },
);
if (handled) return;
}
commitMutationSafely(
selection,
{ type: "add-property", animationId, property, defaultValue },
{ label: `Add GSAP ${property}` },
);
},
[commitMutationSafely],
[commitMutationSafely, sdkDeps],
);
const removeGsapProperty = useCallback(
(selection: DomEditSelection, animationId: string, property: string) => {
// ponytail: null ≠ removal in upsertProp; remove-property stays server-authoritative
commitMutationSafely(
selection,
{ type: "remove-property", animationId, property },
@@ -82,13 +164,43 @@ export function useGsapPropertyDebounce(commitMutationSafely: SafeGsapCommitMuta
[commitMutationSafely],
);
// fallow-ignore-next-line complexity
const updateGsapFromProperty = useCallback(
(
// fallow-ignore-next-line complexity
async (
selection: DomEditSelection,
animationId: string,
property: string,
value: number | string,
) => {
const {
sdkSession,
writeProjectFile,
editHistory,
reloadPreview,
domEditSaveTimestampRef,
activeCompPath,
} = sdkDeps ?? {};
if (
sdkSession &&
writeProjectFile &&
editHistory &&
reloadPreview &&
domEditSaveTimestampRef
) {
const targetPath = selection.sourceFile || activeCompPath || "index.html";
const handled = await sdkGsapTweenPersist(
targetPath,
{ kind: "set", animationId, properties: { fromProperties: { [property]: value } } },
sdkSession,
{ editHistory, writeProjectFile, reloadPreview, domEditSaveTimestampRef },
{
label: `Edit GSAP from-${property}`,
coalesceKey: `gsap:${animationId}:from:${property}`,
},
);
if (handled) return;
}
commitMutationSafely(
selection,
{ type: "update-from-property", animationId, property, value },
@@ -98,23 +210,55 @@ export function useGsapPropertyDebounce(commitMutationSafely: SafeGsapCommitMuta
},
);
},
[commitMutationSafely],
[commitMutationSafely, sdkDeps],
);
// fallow-ignore-next-line complexity
const addGsapFromProperty = useCallback(
(selection: DomEditSelection, animationId: string, property: string) => {
// fallow-ignore-next-line complexity
async (selection: DomEditSelection, animationId: string, property: string) => {
const defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
const {
sdkSession,
writeProjectFile,
editHistory,
reloadPreview,
domEditSaveTimestampRef,
activeCompPath,
} = sdkDeps ?? {};
if (
sdkSession &&
writeProjectFile &&
editHistory &&
reloadPreview &&
domEditSaveTimestampRef
) {
const targetPath = selection.sourceFile || activeCompPath || "index.html";
const handled = await sdkGsapTweenPersist(
targetPath,
{
kind: "set",
animationId,
properties: { fromProperties: { [property]: defaultValue } },
},
sdkSession,
{ editHistory, writeProjectFile, reloadPreview, domEditSaveTimestampRef },
{ label: `Add GSAP from-${property}` },
);
if (handled) return;
}
commitMutationSafely(
selection,
{ type: "add-from-property", animationId, property, defaultValue },
{ label: `Add GSAP from-${property}` },
);
},
[commitMutationSafely],
[commitMutationSafely, sdkDeps],
);
const removeGsapFromProperty = useCallback(
(selection: DomEditSelection, animationId: string, property: string) => {
// ponytail: null ≠ removal in upsertProp; remove-from-property stays server-authoritative
commitMutationSafely(
selection,
{ type: "remove-from-property", animationId, property },