mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +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:
@@ -0,0 +1,112 @@
|
||||
import { memo, useState } from "react";
|
||||
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||
import { Film } from "../../icons/SystemIcons";
|
||||
import { Section } from "./propertyPanelPrimitives";
|
||||
import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimationConstants";
|
||||
import { AnimationCard } from "./AnimationCard";
|
||||
|
||||
interface GsapAnimationSectionProps {
|
||||
animations: GsapAnimation[];
|
||||
multipleTimelines?: boolean;
|
||||
unsupportedTimelinePattern?: boolean;
|
||||
onUpdateProperty: (animationId: string, property: string, value: number | string) => void;
|
||||
onUpdateMeta: (
|
||||
animationId: string,
|
||||
updates: { duration?: number; ease?: string; position?: number },
|
||||
) => void;
|
||||
onDeleteAnimation: (animationId: string) => void;
|
||||
onAddProperty: (animationId: string, property: string) => void;
|
||||
onRemoveProperty: (animationId: string, property: string) => void;
|
||||
onAddAnimation: (method: "to" | "from" | "set") => void;
|
||||
onLivePreview?: (property: string, value: number | string) => void;
|
||||
onLivePreviewEnd?: () => void;
|
||||
}
|
||||
|
||||
export const GsapAnimationSection = memo(function GsapAnimationSection({
|
||||
animations,
|
||||
multipleTimelines,
|
||||
unsupportedTimelinePattern,
|
||||
onUpdateProperty,
|
||||
onUpdateMeta,
|
||||
onDeleteAnimation,
|
||||
onAddProperty,
|
||||
onRemoveProperty,
|
||||
onAddAnimation,
|
||||
onLivePreview,
|
||||
onLivePreviewEnd,
|
||||
}: GsapAnimationSectionProps) {
|
||||
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Section title="Animation" icon={<Film size={15} />}>
|
||||
{multipleTimelines && (
|
||||
<p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400">
|
||||
This file has multiple GSAP timelines. Animation editing is disabled to prevent data loss
|
||||
— consolidate into a single timeline to enable editing.
|
||||
</p>
|
||||
)}
|
||||
{unsupportedTimelinePattern && (
|
||||
<p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400">
|
||||
This composition uses a timeline assignment pattern (window.__timelines[...]) that the
|
||||
editor doesn't support. Use a variable declaration (const tl = gsap.timeline()) to
|
||||
enable editing.
|
||||
</p>
|
||||
)}
|
||||
{multipleTimelines || unsupportedTimelinePattern ? null : (
|
||||
<div className="space-y-2">
|
||||
{animations.map((anim, index) => (
|
||||
<AnimationCard
|
||||
key={anim.id}
|
||||
animation={anim}
|
||||
defaultExpanded={index === 0}
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
onUpdateMeta={onUpdateMeta}
|
||||
onDeleteAnimation={onDeleteAnimation}
|
||||
onAddProperty={onAddProperty}
|
||||
onRemoveProperty={onRemoveProperty}
|
||||
onLivePreview={onLivePreview}
|
||||
onLivePreviewEnd={onLivePreviewEnd}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="relative pt-1">
|
||||
{addMenuOpen ? (
|
||||
<div className="flex gap-1.5">
|
||||
{ADD_METHODS.map((method) => (
|
||||
<button
|
||||
key={method}
|
||||
type="button"
|
||||
title={METHOD_TOOLTIPS[method]}
|
||||
onClick={() => {
|
||||
onAddAnimation(method);
|
||||
setAddMenuOpen(false);
|
||||
}}
|
||||
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 py-1.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
|
||||
>
|
||||
{ADD_METHOD_LABELS[method] ?? method}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAddMenuOpen(false)}
|
||||
className="px-1.5 text-[11px] text-neutral-500 hover:text-neutral-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAddMenuOpen(true)}
|
||||
className="text-[11px] font-medium text-neutral-400 transition-colors hover:text-neutral-200"
|
||||
title="Add a new animation effect to this element"
|
||||
>
|
||||
+ Add effect
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user