mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 07:09:59 +00:00
feat(studio): GSAP tween editing in Design panel (#1102)
* feat(studio): GSAP tween editing in Design panel
Add a GSAP animation editor to the studio Design panel: select an element,
view and edit its tweens (properties, easing, timing), add/delete animations,
and drag custom bezier speed curves — all persisted back to the composition
HTML. Gated behind VITE_STUDIO_ENABLE_GSAP_PANEL.
Parsing of existing GSAP source now uses a recast + Babel AST parser instead of
regex, giving scope resolution, stable tween IDs, and round-trip preservation of
extras and unresolved raw values.
recast compiles to CommonJS that calls require("fs"), which breaks browser and
Vite SSR bundles. To contain it, @hyperframes/core is split into an isomorphic
layer and a Node-only AST layer:
- gsapSerialize.ts holds the recast-free helpers (serialization, keyframe
conversion, validation, shared types). htmlParser.ts is now fully isomorphic.
- parseGsapScript and the script-mutation helpers live in gsapParser.ts,
reachable only via the @hyperframes/core/gsap-parser subpath, loaded
server-side by the studio-api mutation routes and the linter via dynamic
import (recast stays external under SSR).
- The barrel and the gsap-constants subpath are recast-free, so studio browser
bundles never trace recast.
Adds AST parser unit + stress coverage and e2e helpers for the panel.
* fix(lint): await async lintHyperframeHtml in all callers
lintHyperframeHtml became async (gsap rules use dynamic import)
but lintProject and check-hyperframe-static weren't awaiting it,
causing typecheck failures and runtime crashes in CI.
Also wire LintRule type in gsap rules to fix fallow unused-type
finding, and suppress render.ts exported-for-tests symbols.
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import type { TimelineElement } from "../player";
|
||||
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
|
||||
import {
|
||||
STUDIO_INSPECTOR_PANELS_ENABLED,
|
||||
STUDIO_GSAP_PANEL_ENABLED,
|
||||
} from "../components/editor/manualEditingAvailability";
|
||||
import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
|
||||
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
|
||||
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
||||
import type { EditHistoryKind } from "../utils/editHistory";
|
||||
import type { RightPanelTab } from "../utils/studioHelpers";
|
||||
@@ -11,6 +15,8 @@ import { useAskAgentModal } from "./useAskAgentModal";
|
||||
import { useDomSelection } from "./useDomSelection";
|
||||
import { usePreviewInteraction } from "./usePreviewInteraction";
|
||||
import { useDomEditCommits } from "./useDomEditCommits";
|
||||
import { useGsapScriptCommits } from "./useGsapScriptCommits";
|
||||
import { useGsapAnimationsForElement, useGsapCacheVersion } from "./useGsapTweenCache";
|
||||
|
||||
// ── Types ──
|
||||
|
||||
@@ -185,6 +191,37 @@ export function useDomEditSession({
|
||||
onClickToSource,
|
||||
});
|
||||
|
||||
// ── GSAP script editing ──
|
||||
|
||||
const { version: gsapCacheVersion, bump: bumpGsapCache } = useGsapCacheVersion();
|
||||
|
||||
const {
|
||||
animations: selectedGsapAnimations,
|
||||
multipleTimelines: gsapMultipleTimelines,
|
||||
unsupportedTimelinePattern: gsapUnsupportedTimelinePattern,
|
||||
} = useGsapAnimationsForElement(
|
||||
STUDIO_GSAP_PANEL_ENABLED ? (projectId ?? null) : null,
|
||||
domEditSelection?.sourceFile || activeCompPath || "index.html",
|
||||
domEditSelection?.id ?? null,
|
||||
gsapCacheVersion,
|
||||
);
|
||||
|
||||
const {
|
||||
updateGsapProperty,
|
||||
updateGsapMeta,
|
||||
deleteGsapAnimation,
|
||||
addGsapAnimation,
|
||||
addGsapProperty,
|
||||
removeGsapProperty,
|
||||
} = useGsapScriptCommits({
|
||||
projectIdRef,
|
||||
activeCompPath,
|
||||
editHistory,
|
||||
domEditSaveTimestampRef,
|
||||
reloadPreview,
|
||||
onCacheInvalidate: bumpGsapCache,
|
||||
});
|
||||
|
||||
// ── Commit handlers (delegated to useDomEditCommits) ──
|
||||
|
||||
const {
|
||||
@@ -224,7 +261,53 @@ export function useDomEditSession({
|
||||
buildDomSelectionFromTarget,
|
||||
});
|
||||
|
||||
// ── Effects ──
|
||||
const handleGsapUpdateProperty = useCallback(
|
||||
(animId: string, prop: string, value: number | string) => {
|
||||
if (!domEditSelection) return;
|
||||
updateGsapProperty(domEditSelection, animId, prop, value);
|
||||
},
|
||||
[domEditSelection, updateGsapProperty],
|
||||
);
|
||||
|
||||
const handleGsapUpdateMeta = useCallback(
|
||||
(animId: string, updates: { duration?: number; ease?: string; position?: number }) => {
|
||||
if (!domEditSelection) return;
|
||||
updateGsapMeta(domEditSelection, animId, updates);
|
||||
},
|
||||
[domEditSelection, updateGsapMeta],
|
||||
);
|
||||
|
||||
const handleGsapDeleteAnimation = useCallback(
|
||||
(animId: string) => {
|
||||
if (!domEditSelection) return;
|
||||
deleteGsapAnimation(domEditSelection, animId);
|
||||
},
|
||||
[domEditSelection, deleteGsapAnimation],
|
||||
);
|
||||
|
||||
const handleGsapAddAnimation = useCallback(
|
||||
(method: "to" | "from" | "set") => {
|
||||
if (!domEditSelection) return;
|
||||
addGsapAnimation(domEditSelection, method, currentTime);
|
||||
},
|
||||
[domEditSelection, addGsapAnimation, currentTime],
|
||||
);
|
||||
|
||||
const handleGsapAddProperty = useCallback(
|
||||
(animId: string, prop: string) => {
|
||||
if (!domEditSelection) return;
|
||||
addGsapProperty(domEditSelection, animId, prop);
|
||||
},
|
||||
[domEditSelection, addGsapProperty],
|
||||
);
|
||||
|
||||
const handleGsapRemoveProperty = useCallback(
|
||||
(animId: string, prop: string) => {
|
||||
if (!domEditSelection) return;
|
||||
removeGsapProperty(domEditSelection, animId, prop);
|
||||
},
|
||||
[domEditSelection, removeGsapProperty],
|
||||
);
|
||||
|
||||
// Sync selection from preview document on load / refresh
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
@@ -243,6 +326,8 @@ export function useDomEditSession({
|
||||
}
|
||||
if (!doc) return;
|
||||
|
||||
reapplyPositionEditsAfterSeek(doc);
|
||||
|
||||
const nextElement = findElementForSelection(doc, currentSelection, activeCompPath);
|
||||
if (!nextElement) {
|
||||
applyDomSelection(null, { revealPanel: false });
|
||||
@@ -345,5 +430,16 @@ export function useDomEditSession({
|
||||
setAgentModalOpen,
|
||||
setAgentPromptSelectionContext,
|
||||
setAgentModalAnchorPoint,
|
||||
|
||||
// GSAP script editing
|
||||
selectedGsapAnimations,
|
||||
gsapMultipleTimelines,
|
||||
gsapUnsupportedTimelinePattern,
|
||||
handleGsapUpdateProperty,
|
||||
handleGsapUpdateMeta,
|
||||
handleGsapDeleteAnimation,
|
||||
handleGsapAddAnimation,
|
||||
handleGsapAddProperty,
|
||||
handleGsapRemoveProperty,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user