mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): compose FlatMotionSection from Timing and the flat effect-card list
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { FlatTimingRow } from "./propertyPanelFlatMotionSection";
|
||||
import { FlatMotionSection, FlatTimingRow } from "./propertyPanelFlatMotionSection";
|
||||
import type { DomEditSelection } from "./domEditing";
|
||||
|
||||
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
@@ -102,3 +102,118 @@ describe("FlatTimingRow", () => {
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
describe("FlatMotionSection", () => {
|
||||
it("renders Timing when showTiming is true and the effect list when showEffects is true", () => {
|
||||
const { host, root } = renderInto(
|
||||
<FlatMotionSection
|
||||
element={baseElement()}
|
||||
animations={[
|
||||
{
|
||||
id: "a1",
|
||||
method: "to",
|
||||
position: 0.8,
|
||||
duration: 1.2,
|
||||
ease: "power2.out",
|
||||
properties: { opacity: 1 },
|
||||
} as never,
|
||||
]}
|
||||
showTiming
|
||||
showEffects
|
||||
onSetAttribute={vi.fn()}
|
||||
onAddAnimation={vi.fn()}
|
||||
onUpdateProperty={vi.fn()}
|
||||
onUpdateMeta={vi.fn()}
|
||||
onDeleteAnimation={vi.fn()}
|
||||
onAddProperty={vi.fn()}
|
||||
onRemoveProperty={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(host.textContent).toContain("Start");
|
||||
expect(host.textContent).toContain("power2.out");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("omits Timing entirely when showTiming is false", () => {
|
||||
const { host, root } = renderInto(
|
||||
<FlatMotionSection
|
||||
element={baseElement()}
|
||||
animations={[]}
|
||||
showTiming={false}
|
||||
showEffects
|
||||
onSetAttribute={vi.fn()}
|
||||
onAddAnimation={vi.fn()}
|
||||
onUpdateProperty={vi.fn()}
|
||||
onUpdateMeta={vi.fn()}
|
||||
onDeleteAnimation={vi.fn()}
|
||||
onAddProperty={vi.fn()}
|
||||
onRemoveProperty={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(host.textContent).not.toContain("Start");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("omits the effect list entirely when showEffects is false", () => {
|
||||
const { host, root } = renderInto(
|
||||
<FlatMotionSection
|
||||
element={baseElement()}
|
||||
animations={[
|
||||
{
|
||||
id: "a1",
|
||||
method: "to",
|
||||
position: 0.8,
|
||||
duration: 1.2,
|
||||
ease: "power2.out",
|
||||
properties: { opacity: 1 },
|
||||
} as never,
|
||||
]}
|
||||
showTiming
|
||||
showEffects={false}
|
||||
onSetAttribute={vi.fn()}
|
||||
onAddAnimation={vi.fn()}
|
||||
onUpdateProperty={vi.fn()}
|
||||
onUpdateMeta={vi.fn()}
|
||||
onDeleteAnimation={vi.fn()}
|
||||
onAddProperty={vi.fn()}
|
||||
onRemoveProperty={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(host.textContent).not.toContain("power2.out");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("opens the add-method menu on '+ Add effect' and calls onAddAnimation with the chosen method", () => {
|
||||
const onAddAnimation = vi.fn();
|
||||
const { host, root } = renderInto(
|
||||
<FlatMotionSection
|
||||
element={baseElement()}
|
||||
animations={[]}
|
||||
showTiming
|
||||
showEffects
|
||||
onSetAttribute={vi.fn()}
|
||||
onAddAnimation={onAddAnimation}
|
||||
onUpdateProperty={vi.fn()}
|
||||
onUpdateMeta={vi.fn()}
|
||||
onDeleteAnimation={vi.fn()}
|
||||
onAddProperty={vi.fn()}
|
||||
onRemoveProperty={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const buttons = () => Array.from(host.querySelectorAll("button"));
|
||||
const addTrigger = buttons().find((b) => b.textContent === "+ Add effect");
|
||||
if (!addTrigger) throw new Error("expected an '+ Add effect' trigger button");
|
||||
act(() => {
|
||||
addTrigger.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
const animateButton = buttons().find((b) => b.textContent === "Animate");
|
||||
if (!animateButton) throw new Error("expected an 'Animate' method button");
|
||||
act(() => {
|
||||
animateButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
expect(onAddAnimation).toHaveBeenCalledWith("to");
|
||||
// The menu closes back to the trigger after a selection.
|
||||
expect(buttons().some((b) => b.textContent === "+ Add effect")).toBe(true);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||
import type { DomEditSelection } from "./domEditing";
|
||||
import { formatTimingValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
|
||||
import { parseTimingValue } from "./propertyPanelTimingSection";
|
||||
import { CommitField } from "./propertyPanelPrimitives";
|
||||
import { AnimationCard } from "./AnimationCard";
|
||||
import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimationConstants";
|
||||
import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks";
|
||||
|
||||
function deriveTimingFromAnimations(
|
||||
animations: GsapAnimation[],
|
||||
@@ -79,3 +83,98 @@ export function FlatTimingRow({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FlatMotionSection({
|
||||
element,
|
||||
animations,
|
||||
showTiming,
|
||||
showEffects,
|
||||
multipleTimelines,
|
||||
unsupportedTimelinePattern,
|
||||
onSetAttribute,
|
||||
onAddAnimation,
|
||||
...callbacks
|
||||
}: {
|
||||
element: DomEditSelection;
|
||||
animations: GsapAnimation[];
|
||||
showTiming: boolean;
|
||||
showEffects: boolean;
|
||||
multipleTimelines?: boolean;
|
||||
unsupportedTimelinePattern?: boolean;
|
||||
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
||||
onAddAnimation: (method: "to" | "from" | "set" | "fromTo") => void;
|
||||
} & GsapAnimationEditCallbacks) {
|
||||
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{showTiming && (
|
||||
<FlatTimingRow element={element} animations={animations} onSetAttribute={onSetAttribute} />
|
||||
)}
|
||||
{showEffects && (
|
||||
<>
|
||||
{multipleTimelines && (
|
||||
<p className="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="rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400">
|
||||
This timeline uses a computed key the editor can't resolve statically.
|
||||
</p>
|
||||
)}
|
||||
{!multipleTimelines && !unsupportedTimelinePattern && (
|
||||
<div className="space-y-2">
|
||||
{animations.map((anim, index) => (
|
||||
<AnimationCard
|
||||
key={anim.id}
|
||||
animation={anim}
|
||||
defaultExpanded={index === 0}
|
||||
flat
|
||||
{...callbacks}
|
||||
/>
|
||||
))}
|
||||
<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-panel-border-input bg-panel-input px-2.5 py-1.5 text-[11px] font-medium text-panel-text-2 transition-colors hover:border-panel-text-4 hover:text-panel-text-0"
|
||||
>
|
||||
{ADD_METHOD_LABELS[method] ?? method}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAddMenuOpen(false)}
|
||||
className="px-1.5 text-[11px] text-panel-text-3 hover:text-panel-text-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAddMenuOpen(true)}
|
||||
className="text-[11px] font-medium text-panel-text-3 transition-colors hover:text-panel-text-1"
|
||||
title="Add a new animation effect to this element"
|
||||
>
|
||||
+ Add effect
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user