@hyperframes/studio 0.7.76 → 0.7.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-DsRdxz7A.js → hyperframes-player-CEggaaxR.js} +1 -1
- package/dist/assets/{index-BLICKger.js → index-B1INQNmj.js} +1 -1
- package/dist/assets/{index-Du1RoLiB.js → index-B1Tjjdse.js} +1 -1
- package/dist/assets/index-Bp4jAYZG.js +428 -0
- package/dist/assets/index-CBAfprvx.css +1 -0
- package/dist/index.d.ts +64 -17
- package/dist/index.html +2 -2
- package/dist/index.js +18040 -16293
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +16 -17
- package/src/components/EditorShell.tsx +3 -0
- package/src/components/TimelineToolbar.test.tsx +48 -3
- package/src/components/TimelineToolbar.tsx +89 -36
- package/src/components/editor/AnimationCard.test.tsx +18 -75
- package/src/components/editor/AnimationCard.tsx +26 -2
- package/src/components/editor/GsapAddAnimationControl.tsx +68 -0
- package/src/components/editor/GsapAnimationSection.tsx +18 -157
- package/src/components/editor/KeyframeEaseList.tsx +5 -1
- package/src/components/editor/KeyframeNavigation.tsx +46 -14
- package/src/components/editor/KeyframeNavigationDiamond.test.tsx +59 -0
- package/src/components/editor/MotionPathOverlay.tsx +32 -10
- package/src/components/editor/PropertyPanel.tsx +15 -5
- package/src/components/editor/PropertyPanelFlat.tsx +48 -23
- package/src/components/editor/gsapAnimationCallbacks.test.ts +122 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +108 -4
- package/src/components/editor/keyframeRetime.test.ts +25 -22
- package/src/components/editor/keyframeRetime.ts +1 -2
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +25 -139
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +32 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +21 -4
- package/src/components/nle/PreviewOverlays.tsx +3 -1
- package/src/components/nle/useTimelineEditCallbacks.test.ts +30 -0
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +693 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +197 -57
- package/src/contexts/TimelineEditContext.tsx +1 -1
- package/src/hooks/deleteSelectedKeyframes.ts +24 -8
- package/src/hooks/gsapDragCommit.ts +2 -2
- package/src/hooks/gsapDragPositionCommit.ts +84 -5
- package/src/hooks/gsapKeyframeCacheHelpers.test.ts +176 -1
- package/src/hooks/gsapKeyframeCacheHelpers.ts +93 -52
- package/src/hooks/gsapRuntimeBridge.test.ts +100 -0
- package/src/hooks/gsapScriptCommitHelpers.ts +3 -2
- package/src/hooks/gsapScriptCommitTypes.ts +2 -0
- package/src/hooks/gsapShared.test.ts +109 -1
- package/src/hooks/gsapShared.ts +142 -17
- package/src/hooks/gsapTweenSynth.test.ts +30 -1
- package/src/hooks/gsapTweenSynth.ts +61 -14
- package/src/hooks/keyframeCacheAstLoad.ts +181 -0
- package/src/hooks/timelineEditingHelpers.test.ts +80 -0
- package/src/hooks/timelineMoveAdapter.ts +9 -2
- package/src/hooks/useDomEditWiring.ts +2 -2
- package/src/hooks/useDomSelection.ts +4 -0
- package/src/hooks/useEnableKeyframes.test.ts +122 -0
- package/src/hooks/useEnableKeyframes.ts +116 -56
- package/src/hooks/useGestureCommit.ts +2 -2
- package/src/hooks/useGsapAnimationFetchFallback.ts +1 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +92 -13
- package/src/hooks/useGsapKeyframeOps.ts +30 -13
- package/src/hooks/useGsapScriptCommits.test.tsx +21 -0
- package/src/hooks/useGsapScriptCommits.ts +2 -0
- package/src/hooks/useGsapSelectionHandlers.test.tsx +69 -3
- package/src/hooks/useGsapSelectionHandlers.ts +90 -65
- package/src/hooks/useGsapTweenCache.test.ts +16 -0
- package/src/hooks/useGsapTweenCache.ts +64 -178
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +68 -0
- package/src/hooks/useStudioContextValue.test.ts +93 -0
- package/src/hooks/useStudioContextValue.ts +17 -3
- package/src/hooks/useStudioTestHooks.ts +63 -0
- package/src/hooks/useTimelineEditing.test.tsx +187 -22
- package/src/hooks/useTimelineEditing.ts +43 -32
- package/src/hooks/useTimelineEditingTypes.ts +2 -0
- package/src/hooks/useTimelineGroupEditing.ts +106 -82
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +69 -0
- package/src/player/components/KeyframeDiamondContextMenu.tsx +19 -8
- package/src/player/components/LayerDisclosureRow.tsx +76 -0
- package/src/player/components/Timeline.test.ts +420 -90
- package/src/player/components/Timeline.tsx +80 -78
- package/src/player/components/TimelineCanvas.tsx +33 -21
- package/src/player/components/TimelineClip.tsx +3 -1
- package/src/player/components/TimelineClipDiamonds.test.tsx +540 -16
- package/src/player/components/TimelineClipDiamonds.tsx +320 -117
- package/src/player/components/TimelineDiamondConnectors.tsx +145 -0
- package/src/player/components/TimelineLanes.tsx +153 -79
- package/src/player/components/TimelineOverlays.tsx +4 -18
- package/src/player/components/TimelinePropertyLanes.test.tsx +450 -0
- package/src/player/components/TimelinePropertyLanes.tsx +191 -0
- package/src/player/components/TimelineRazorInteraction.tsx +60 -0
- package/src/player/components/TimelineRuler.tsx +11 -21
- package/src/player/components/TimelineTrackHeader.test.tsx +346 -0
- package/src/player/components/TimelineTrackHeader.tsx +363 -0
- package/src/player/components/TrackClipCount.tsx +17 -0
- package/src/player/components/timelineCallbacks.ts +21 -6
- package/src/player/components/timelineClipDragPreview.test.ts +50 -3
- package/src/player/components/timelineClipDragPreview.ts +33 -15
- package/src/player/components/timelineCollision.ts +4 -13
- package/src/player/components/timelineDiamondTypes.ts +120 -0
- package/src/player/components/timelineDragDrop.ts +8 -3
- package/src/player/components/timelineEditing.test.ts +19 -27
- package/src/player/components/timelineEditing.ts +7 -8
- package/src/player/components/timelineKeyframeIdentity.test.ts +49 -0
- package/src/player/components/timelineKeyframeIdentity.ts +66 -0
- package/src/player/components/timelineLayout.test.ts +162 -9
- package/src/player/components/timelineLayout.ts +198 -38
- package/src/player/components/timelineMarquee.test.ts +76 -21
- package/src/player/components/timelineMarquee.ts +17 -14
- package/src/player/components/trackHeaderLaneState.ts +121 -0
- package/src/player/components/trackHeaderLaneValues.ts +139 -0
- package/src/player/components/useAutoExpandKeyframedClips.test.tsx +75 -0
- package/src/player/components/useAutoExpandKeyframedClips.ts +50 -0
- package/src/player/components/useTimelineClipDrag.ts +4 -1
- package/src/player/components/useTimelineGeometry.ts +4 -1
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +108 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +50 -15
- package/src/player/components/useTimelinePlayhead.ts +21 -12
- package/src/player/components/useTimelineRangeSelection.ts +35 -14
- package/src/player/components/useTimelineTrackLayout.test.ts +53 -0
- package/src/player/components/useTimelineTrackLayout.ts +162 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +21 -12
- package/src/player/lib/timelineElementHelpers.ts +26 -11
- package/src/player/store/keyframeSlice.ts +122 -0
- package/src/player/store/playerStore.test.ts +25 -0
- package/src/player/store/playerStore.ts +7 -43
- package/src/telemetry/events.test.ts +9 -3
- package/src/telemetry/events.ts +16 -7
- package/src/utils/globalTimeCompiler.test.ts +10 -0
- package/src/utils/globalTimeCompiler.ts +3 -3
- package/dist/assets/index-B37rWXo4.css +0 -1
- package/dist/assets/index-BdOfFsR8.js +0 -428
- package/src/player/components/TimelineDragGhost.tsx +0 -59
- package/src/utils/keyframeSelection.test.ts +0 -45
- package/src/utils/keyframeSelection.ts +0 -29
|
@@ -19,6 +19,7 @@ import { createGsapLivePreview } from "./gsapLivePreview";
|
|
|
19
19
|
import { formatTextFieldPreview } from "./propertyPanelSections";
|
|
20
20
|
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
|
|
21
21
|
import { useColorGradingController } from "./useColorGradingController";
|
|
22
|
+
import { usePlayerStore } from "../../player";
|
|
22
23
|
import {
|
|
23
24
|
FlatColorGradingAccessory,
|
|
24
25
|
FlatColorGradingSection,
|
|
@@ -231,7 +232,40 @@ export function PropertyPanelFlat({
|
|
|
231
232
|
: "layout",
|
|
232
233
|
);
|
|
233
234
|
|
|
234
|
-
//
|
|
235
|
+
// Tracks which group(s) are actively transitioning this toggle cycle, so
|
|
236
|
+
// their header/body gets the fast entrance animation (hf-flat-group-enter)
|
|
237
|
+
// and no one else's does. Deliberately NOT derived from remounting alone:
|
|
238
|
+
// FlatGroupHeader instances are keyed by group id and React normally
|
|
239
|
+
// preserves them across re-renders, but toggling a non-adjacent group still
|
|
240
|
+
// shifts the untouched collapsed siblings between the before/after-open
|
|
241
|
+
// slices below, and Chromium restarts a CSS animation on that kind of
|
|
242
|
+
// position shift even though nothing about the sibling actually changed.
|
|
243
|
+
// Gating on these ids (cleared shortly after the 120ms CSS animation
|
|
244
|
+
// finishes) keeps the animation scoped to only the groups that actually
|
|
245
|
+
// just toggled. Two ids, not one: the clicked (newly-opening/closing) group
|
|
246
|
+
// AND whichever group was open immediately before the click and got
|
|
247
|
+
// implicitly closed by it — both freshly-mounted headers need to animate.
|
|
248
|
+
// When the inline timeline ease button focuses a segment on this element,
|
|
249
|
+
// force the Motion group open so its AnimationCard (which only mounts while
|
|
250
|
+
// the group is expanded) can consume the focus and reveal the ease editor.
|
|
251
|
+
const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
|
|
252
|
+
// The element THIS panel renders, not the store's selectedElementId: that
|
|
253
|
+
// flips synchronously while the panel still renders its predecessor, so a
|
|
254
|
+
// stale panel would consume a request meant for its successor whenever the
|
|
255
|
+
// two share a class-selector animation id.
|
|
256
|
+
const renderedElementId = `${element.sourceFile}#${element.id}`;
|
|
257
|
+
// Adjusted during render (not an effect) so the card mounts on the same
|
|
258
|
+
// commit the request lands on. Keyed on request identity: a group the user
|
|
259
|
+
// closes afterwards stays closed.
|
|
260
|
+
const [consumedFocus, setConsumedFocus] = useState(focusedEaseSegment);
|
|
261
|
+
if (focusedEaseSegment !== consumedFocus) {
|
|
262
|
+
setConsumedFocus(focusedEaseSegment);
|
|
263
|
+
const focusesThisPanel =
|
|
264
|
+
focusedEaseSegment?.elementId === renderedElementId &&
|
|
265
|
+
gsapAnimations.some((a) => a.id === focusedEaseSegment.animationId);
|
|
266
|
+
if (focusesThisPanel) setOpenGroupId("motion");
|
|
267
|
+
}
|
|
268
|
+
|
|
235
269
|
const [justToggledIds, setJustToggledIds] = useState<string[]>([]);
|
|
236
270
|
const justToggledTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
237
271
|
const panelBodyRef = useRef<HTMLDivElement>(null);
|
|
@@ -493,6 +527,17 @@ export function PropertyPanelFlat({
|
|
|
493
527
|
const beforeOpen = openIndex === -1 ? groups : groups.slice(0, openIndex);
|
|
494
528
|
const openGroup = openIndex === -1 ? null : groups[openIndex];
|
|
495
529
|
const afterOpen = openIndex === -1 ? [] : groups.slice(openIndex + 1);
|
|
530
|
+
const renderClosedGroup = (group: FlatGroupDescriptor) => (
|
|
531
|
+
<DesignPanelInputProvider key={group.id} section={slugifyDesignInput(group.title)}>
|
|
532
|
+
<FlatGroupHeader
|
|
533
|
+
title={group.title}
|
|
534
|
+
isOpen={false}
|
|
535
|
+
onToggleOpen={() => toggleOpen(group.id)}
|
|
536
|
+
summary={group.summary}
|
|
537
|
+
animateEntrance={justToggledIds.includes(group.id)}
|
|
538
|
+
/>
|
|
539
|
+
</DesignPanelInputProvider>
|
|
540
|
+
);
|
|
496
541
|
|
|
497
542
|
return (
|
|
498
543
|
<DesignPanelInputProvider ui="flat">
|
|
@@ -520,17 +565,7 @@ export function PropertyPanelFlat({
|
|
|
520
565
|
data-flat-panel-body="true"
|
|
521
566
|
className="flex min-h-0 flex-1 flex-col overflow-y-auto"
|
|
522
567
|
>
|
|
523
|
-
{beforeOpen.map(
|
|
524
|
-
<DesignPanelInputProvider key={g.id} section={slugifyDesignInput(g.title)}>
|
|
525
|
-
<FlatGroupHeader
|
|
526
|
-
title={g.title}
|
|
527
|
-
isOpen={false}
|
|
528
|
-
onToggleOpen={() => toggleOpen(g.id)}
|
|
529
|
-
summary={g.summary}
|
|
530
|
-
animateEntrance={justToggledIds.includes(g.id)}
|
|
531
|
-
/>
|
|
532
|
-
</DesignPanelInputProvider>
|
|
533
|
-
))}
|
|
568
|
+
{beforeOpen.map(renderClosedGroup)}
|
|
534
569
|
{openGroup && (
|
|
535
570
|
<DesignPanelInputProvider section={slugifyDesignInput(openGroup.title)}>
|
|
536
571
|
<div data-flat-group-open="true" className="flex min-h-[180px] flex-none flex-col">
|
|
@@ -549,17 +584,7 @@ export function PropertyPanelFlat({
|
|
|
549
584
|
</div>
|
|
550
585
|
</DesignPanelInputProvider>
|
|
551
586
|
)}
|
|
552
|
-
{afterOpen.map(
|
|
553
|
-
<DesignPanelInputProvider key={g.id} section={slugifyDesignInput(g.title)}>
|
|
554
|
-
<FlatGroupHeader
|
|
555
|
-
title={g.title}
|
|
556
|
-
isOpen={false}
|
|
557
|
-
onToggleOpen={() => toggleOpen(g.id)}
|
|
558
|
-
summary={g.summary}
|
|
559
|
-
animateEntrance={justToggledIds.includes(g.id)}
|
|
560
|
-
/>
|
|
561
|
-
</DesignPanelInputProvider>
|
|
562
|
-
))}
|
|
587
|
+
{afterOpen.map(renderClosedGroup)}
|
|
563
588
|
</div>
|
|
564
589
|
<DesignPanelInputProvider section="footer">
|
|
565
590
|
<PropertyPanelFlatFooter
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
type GsapAnimationEditCallbacks,
|
|
4
|
+
withTrackedGsapAnimationCallbacks,
|
|
5
|
+
} from "./gsapAnimationCallbacks";
|
|
6
|
+
|
|
7
|
+
function requiredCallbacks(): GsapAnimationEditCallbacks {
|
|
8
|
+
return {
|
|
9
|
+
onUpdateProperty: vi.fn(),
|
|
10
|
+
onUpdateMeta: vi.fn(),
|
|
11
|
+
onDeleteAnimation: vi.fn(),
|
|
12
|
+
onAddProperty: vi.fn(),
|
|
13
|
+
onRemoveProperty: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function requireCallback<T>(callback: T | undefined): T {
|
|
18
|
+
if (callback === undefined) throw new Error("expected callback to be present");
|
|
19
|
+
return callback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("withTrackedGsapAnimationCallbacks", () => {
|
|
23
|
+
it("keeps absent optional callbacks absent and passes preview callbacks through unchanged", () => {
|
|
24
|
+
const callbacks = requiredCallbacks();
|
|
25
|
+
const onLivePreview = vi.fn();
|
|
26
|
+
const onLivePreviewEnd = vi.fn();
|
|
27
|
+
callbacks.onLivePreview = onLivePreview;
|
|
28
|
+
callbacks.onLivePreviewEnd = onLivePreviewEnd;
|
|
29
|
+
|
|
30
|
+
const tracked = withTrackedGsapAnimationCallbacks(callbacks, vi.fn());
|
|
31
|
+
|
|
32
|
+
expect(tracked.onUpdateFromProperty).toBeUndefined();
|
|
33
|
+
expect(tracked.onAddFromProperty).toBeUndefined();
|
|
34
|
+
expect(tracked.onRemoveFromProperty).toBeUndefined();
|
|
35
|
+
expect(tracked.onSetArcPath).toBeUndefined();
|
|
36
|
+
expect(tracked.onUpdateArcSegment).toBeUndefined();
|
|
37
|
+
expect(tracked.onUpdateKeyframeEase).toBeUndefined();
|
|
38
|
+
expect(tracked.onSetAllKeyframeEases).toBeUndefined();
|
|
39
|
+
expect(tracked.onUnroll).toBeUndefined();
|
|
40
|
+
expect(tracked.onLivePreview).toBe(onLivePreview);
|
|
41
|
+
expect(tracked.onLivePreviewEnd).toBe(onLivePreviewEnd);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("tracks each edit once before invoking its mutation callback", () => {
|
|
45
|
+
const events: string[] = [];
|
|
46
|
+
const mutation = (name: string) => () => events.push(`mutate:${name}`);
|
|
47
|
+
const callbacks: GsapAnimationEditCallbacks = {
|
|
48
|
+
onUpdateProperty: mutation("update-property"),
|
|
49
|
+
onUpdateMeta: mutation("update-meta"),
|
|
50
|
+
onDeleteAnimation: mutation("delete"),
|
|
51
|
+
onAddProperty: mutation("add-property"),
|
|
52
|
+
onRemoveProperty: mutation("remove-property"),
|
|
53
|
+
onUpdateFromProperty: mutation("update-from"),
|
|
54
|
+
onAddFromProperty: mutation("add-from"),
|
|
55
|
+
onRemoveFromProperty: mutation("remove-from"),
|
|
56
|
+
onSetArcPath: mutation("arc-path"),
|
|
57
|
+
onUpdateArcSegment: mutation("arc-segment"),
|
|
58
|
+
onUpdateKeyframeEase: mutation("keyframe-ease"),
|
|
59
|
+
onSetAllKeyframeEases: mutation("all-eases"),
|
|
60
|
+
onUnroll: mutation("unroll"),
|
|
61
|
+
};
|
|
62
|
+
const tracked = withTrackedGsapAnimationCallbacks(callbacks, (control, name) => {
|
|
63
|
+
events.push(`track:${control}:${name}`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
tracked.onUpdateProperty("a1", "visibility", 1);
|
|
67
|
+
tracked.onUpdateProperty("a1", "filter", "blur(2px)");
|
|
68
|
+
tracked.onUpdateProperty("a1", "opacity", 0.5);
|
|
69
|
+
tracked.onUpdateMeta("a1", { duration: 2, ease: "none", position: 1 });
|
|
70
|
+
tracked.onDeleteAnimation("a1");
|
|
71
|
+
tracked.onAddProperty("a1", "scale");
|
|
72
|
+
tracked.onRemoveProperty("a1", "scale");
|
|
73
|
+
requireCallback(tracked.onUpdateFromProperty)("a1", "clipPath", "none");
|
|
74
|
+
requireCallback(tracked.onAddFromProperty)("a1", "x");
|
|
75
|
+
requireCallback(tracked.onRemoveFromProperty)("a1", "x");
|
|
76
|
+
requireCallback(tracked.onSetArcPath)("a1", { enabled: true });
|
|
77
|
+
requireCallback(tracked.onSetArcPath)("a1", { enabled: true, autoRotate: true });
|
|
78
|
+
requireCallback(tracked.onUpdateArcSegment)("a1", 1, {});
|
|
79
|
+
requireCallback(tracked.onUpdateArcSegment)("a1", 1, { curviness: 0.5 });
|
|
80
|
+
requireCallback(tracked.onUpdateKeyframeEase)("a1", 50, "power2.out");
|
|
81
|
+
requireCallback(tracked.onSetAllKeyframeEases)("a1", "none");
|
|
82
|
+
requireCallback(tracked.onUnroll)("a1");
|
|
83
|
+
|
|
84
|
+
expect(events).toEqual([
|
|
85
|
+
"track:toggle:visibility",
|
|
86
|
+
"mutate:update-property",
|
|
87
|
+
"track:text:filter",
|
|
88
|
+
"mutate:update-property",
|
|
89
|
+
"track:metric:opacity",
|
|
90
|
+
"mutate:update-property",
|
|
91
|
+
"track:metric:Length",
|
|
92
|
+
"track:select:Speed",
|
|
93
|
+
"track:metric:Starts at",
|
|
94
|
+
"mutate:update-meta",
|
|
95
|
+
"track:button:Remove animation",
|
|
96
|
+
"mutate:delete",
|
|
97
|
+
"track:select:Add effect property",
|
|
98
|
+
"mutate:add-property",
|
|
99
|
+
"track:button:Remove scale",
|
|
100
|
+
"mutate:remove-property",
|
|
101
|
+
"track:text:clipPath",
|
|
102
|
+
"mutate:update-from",
|
|
103
|
+
"track:select:Add from property",
|
|
104
|
+
"mutate:add-from",
|
|
105
|
+
"track:button:Remove from x",
|
|
106
|
+
"mutate:remove-from",
|
|
107
|
+
"track:toggle:Arc motion",
|
|
108
|
+
"mutate:arc-path",
|
|
109
|
+
"track:toggle:Auto rotate",
|
|
110
|
+
"mutate:arc-path",
|
|
111
|
+
"track:button:Reset arc segment 2",
|
|
112
|
+
"mutate:arc-segment",
|
|
113
|
+
"mutate:arc-segment",
|
|
114
|
+
"track:select:Keyframe ease",
|
|
115
|
+
"mutate:keyframe-ease",
|
|
116
|
+
"track:select:All keyframe eases",
|
|
117
|
+
"mutate:all-eases",
|
|
118
|
+
"track:button:Unroll animation",
|
|
119
|
+
"mutate:unroll",
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ArcPathSegment } from "@hyperframes/parsers/gsap-parser";
|
|
2
|
+
import { usePlayerStore } from "../../player";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Edit callbacks shared by GsapAnimationSection and each AnimationCard it
|
|
@@ -35,6 +36,18 @@ export interface GsapAnimationEditCallbacks {
|
|
|
35
36
|
onUnroll?: (animationId: string) => void;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
type TrackDesignInput = (control: string, name: string) => void;
|
|
40
|
+
|
|
41
|
+
function trackAnimationProperty(track: TrackDesignInput, property: string): void {
|
|
42
|
+
const control =
|
|
43
|
+
property === "visibility"
|
|
44
|
+
? "toggle"
|
|
45
|
+
: property === "filter" || property === "clipPath"
|
|
46
|
+
? "text"
|
|
47
|
+
: "metric";
|
|
48
|
+
track(control, property);
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
// User-facing control label for each animation-meta field. The ease control is
|
|
39
52
|
// labelled "Speed" in the card UI, so ease/easeEach map there.
|
|
40
53
|
const ANIMATION_META_LABELS: Record<string, { control: string; name: string }> = {
|
|
@@ -51,13 +64,104 @@ const ANIMATION_META_LABELS: Record<string, { control: string; name: string }> =
|
|
|
51
64
|
* added later is attributed honestly by its own key instead of poisoning another
|
|
52
65
|
* control's usage count.
|
|
53
66
|
*/
|
|
54
|
-
|
|
55
|
-
track: (control: string, name: string) => void,
|
|
56
|
-
updates: Record<string, unknown>,
|
|
57
|
-
): void {
|
|
67
|
+
function trackAnimationMetaUpdate(track: TrackDesignInput, updates: Record<string, unknown>): void {
|
|
58
68
|
for (const key of Object.keys(updates)) {
|
|
59
69
|
const mapped = ANIMATION_META_LABELS[key];
|
|
60
70
|
if (mapped) track(mapped.control, mapped.name);
|
|
61
71
|
else track("select", key);
|
|
62
72
|
}
|
|
63
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Add design-input telemetry to the shared animation-edit callback surface.
|
|
77
|
+
* Optional callbacks remain absent, pass-through preview callbacks keep their
|
|
78
|
+
* original identity, and every tracked event fires once before its mutation.
|
|
79
|
+
*/
|
|
80
|
+
export function withTrackedGsapAnimationCallbacks(
|
|
81
|
+
callbacks: GsapAnimationEditCallbacks,
|
|
82
|
+
track: TrackDesignInput,
|
|
83
|
+
): GsapAnimationEditCallbacks {
|
|
84
|
+
return {
|
|
85
|
+
onUpdateProperty: (animationId, property, value) => {
|
|
86
|
+
trackAnimationProperty(track, property);
|
|
87
|
+
callbacks.onUpdateProperty(animationId, property, value);
|
|
88
|
+
},
|
|
89
|
+
onUpdateMeta: (animationId, updates) => {
|
|
90
|
+
trackAnimationMetaUpdate(track, updates);
|
|
91
|
+
callbacks.onUpdateMeta(animationId, updates);
|
|
92
|
+
},
|
|
93
|
+
onDeleteAnimation: (animationId) => {
|
|
94
|
+
track("button", "Remove animation");
|
|
95
|
+
callbacks.onDeleteAnimation(animationId);
|
|
96
|
+
},
|
|
97
|
+
onAddProperty: (animationId, property) => {
|
|
98
|
+
track("select", "Add effect property");
|
|
99
|
+
callbacks.onAddProperty(animationId, property);
|
|
100
|
+
},
|
|
101
|
+
onRemoveProperty: (animationId, property) => {
|
|
102
|
+
track("button", `Remove ${property}`);
|
|
103
|
+
callbacks.onRemoveProperty(animationId, property);
|
|
104
|
+
},
|
|
105
|
+
onUpdateFromProperty: callbacks.onUpdateFromProperty
|
|
106
|
+
? (animationId, property, value) => {
|
|
107
|
+
trackAnimationProperty(track, property);
|
|
108
|
+
callbacks.onUpdateFromProperty?.(animationId, property, value);
|
|
109
|
+
}
|
|
110
|
+
: undefined,
|
|
111
|
+
onAddFromProperty: callbacks.onAddFromProperty
|
|
112
|
+
? (animationId, property) => {
|
|
113
|
+
track("select", "Add from property");
|
|
114
|
+
callbacks.onAddFromProperty?.(animationId, property);
|
|
115
|
+
}
|
|
116
|
+
: undefined,
|
|
117
|
+
onRemoveFromProperty: callbacks.onRemoveFromProperty
|
|
118
|
+
? (animationId, property) => {
|
|
119
|
+
track("button", `Remove from ${property}`);
|
|
120
|
+
callbacks.onRemoveFromProperty?.(animationId, property);
|
|
121
|
+
}
|
|
122
|
+
: undefined,
|
|
123
|
+
onLivePreview: callbacks.onLivePreview,
|
|
124
|
+
onLivePreviewEnd: callbacks.onLivePreviewEnd,
|
|
125
|
+
onSetArcPath: callbacks.onSetArcPath
|
|
126
|
+
? (animationId, config) => {
|
|
127
|
+
track("toggle", config.autoRotate !== undefined ? "Auto rotate" : "Arc motion");
|
|
128
|
+
callbacks.onSetArcPath?.(animationId, config);
|
|
129
|
+
}
|
|
130
|
+
: undefined,
|
|
131
|
+
onUpdateArcSegment: callbacks.onUpdateArcSegment
|
|
132
|
+
? (animationId, segmentIndex, update) => {
|
|
133
|
+
if (update.curviness === undefined) {
|
|
134
|
+
track("button", `Reset arc segment ${segmentIndex + 1}`);
|
|
135
|
+
}
|
|
136
|
+
callbacks.onUpdateArcSegment?.(animationId, segmentIndex, update);
|
|
137
|
+
}
|
|
138
|
+
: undefined,
|
|
139
|
+
onUpdateKeyframeEase: callbacks.onUpdateKeyframeEase
|
|
140
|
+
? (animationId, percentage, ease) => {
|
|
141
|
+
track("select", "Keyframe ease");
|
|
142
|
+
callbacks.onUpdateKeyframeEase?.(animationId, percentage, ease);
|
|
143
|
+
}
|
|
144
|
+
: undefined,
|
|
145
|
+
onSetAllKeyframeEases: callbacks.onSetAllKeyframeEases
|
|
146
|
+
? (animationId, ease) => {
|
|
147
|
+
track("select", "All keyframe eases");
|
|
148
|
+
callbacks.onSetAllKeyframeEases?.(animationId, ease);
|
|
149
|
+
}
|
|
150
|
+
: undefined,
|
|
151
|
+
onUnroll: callbacks.onUnroll
|
|
152
|
+
? (animationId) => {
|
|
153
|
+
track("button", "Unroll animation");
|
|
154
|
+
callbacks.onUnroll?.(animationId);
|
|
155
|
+
}
|
|
156
|
+
: undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Stable consumer for the store's one-shot ease-focus request. Module-level on
|
|
162
|
+
* purpose: an inline arrow in the section components is a dep of AnimationCard's
|
|
163
|
+
* focus effect, so a fresh identity each render re-runs that effect every render.
|
|
164
|
+
*/
|
|
165
|
+
export function clearFocusedEaseSegment(): void {
|
|
166
|
+
usePlayerStore.getState().setFocusedEaseSegment(null);
|
|
167
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Boundary cases share an arrange/assert shape on purpose: each case states its
|
|
2
|
+
// own window, drag, and expected remap so a failure reads without cross-referencing.
|
|
3
|
+
// fallow-ignore-file code-duplication
|
|
1
4
|
import { describe, expect, it } from "vitest";
|
|
2
5
|
import { resolveKeyframeRetime, type RetimeKeyframe } from "./keyframeRetime";
|
|
3
6
|
|
|
@@ -11,6 +14,22 @@ const KEYFRAMES: RetimeKeyframe[] = [
|
|
|
11
14
|
const WINDOW = { tweenStart: 2, tweenDuration: 4 };
|
|
12
15
|
const LEFT_BOUNDARY_DROP = { ...WINDOW, dropAbsTime: 0.5 };
|
|
13
16
|
|
|
17
|
+
function expectLeftResize(
|
|
18
|
+
keyframes: RetimeKeyframe[],
|
|
19
|
+
draggedTweenPct: number,
|
|
20
|
+
pctRemap: Array<{ from: number; to: number }>,
|
|
21
|
+
): void {
|
|
22
|
+
const result = resolveKeyframeRetime({
|
|
23
|
+
...LEFT_BOUNDARY_DROP,
|
|
24
|
+
keyframes,
|
|
25
|
+
draggedTweenPct,
|
|
26
|
+
});
|
|
27
|
+
expect(result.kind).toBe("resize");
|
|
28
|
+
expect(result.position).toBeCloseTo(0.5, 5);
|
|
29
|
+
expect(result.duration).toBeCloseTo(5.5, 5);
|
|
30
|
+
expect(result.pctRemap).toEqual(pctRemap);
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
describe("resolveKeyframeRetime — move (within the tween window)", () => {
|
|
15
34
|
it("re-keys an interior keyframe to the tween-% of the drop", () => {
|
|
16
35
|
const r = resolveKeyframeRetime({
|
|
@@ -94,29 +113,21 @@ describe("resolveKeyframeRetime — resize (past the tween boundary)", () => {
|
|
|
94
113
|
expect(r.kind).toBe("resize");
|
|
95
114
|
expect(r.position).toBeCloseTo(2, 5); // start unchanged
|
|
96
115
|
expect(r.duration).toBeCloseTo(6, 5); // 8 - 2
|
|
97
|
-
// abs 2/4/8 over the new [2,8] window → 0 / 33.
|
|
116
|
+
// abs 2/4/8 over the new [2,8] window → 0 / 33.333 / 100. pctRemap carries each
|
|
98
117
|
// existing keyframe's old→new tween-%; the commit re-keys in place (value +
|
|
99
118
|
// ease + _auto preserved by round-tripping the source node, not re-emitted here).
|
|
100
119
|
expect(r.pctRemap).toEqual([
|
|
101
120
|
{ from: 0, to: 0 },
|
|
102
|
-
{ from: 50, to: 33.
|
|
121
|
+
{ from: 50, to: 33.333 },
|
|
103
122
|
{ from: 100, to: 100 },
|
|
104
123
|
]);
|
|
105
124
|
});
|
|
106
125
|
|
|
107
126
|
it("extends the FIRST keyframe before the start, shifting position earlier", () => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
keyframes: KEYFRAMES,
|
|
111
|
-
draggedTweenPct: 0,
|
|
112
|
-
});
|
|
113
|
-
expect(r.kind).toBe("resize");
|
|
114
|
-
expect(r.position).toBeCloseTo(0.5, 5);
|
|
115
|
-
expect(r.duration).toBeCloseTo(5.5, 5); // 6 - 0.5
|
|
116
|
-
// abs 0.5/4/6 over [0.5,6] → 0 / 63.6 / 100.
|
|
117
|
-
expect(r.pctRemap).toEqual([
|
|
127
|
+
// abs 0.5/4/6 over [0.5,6] → 0 / 63.636 / 100.
|
|
128
|
+
expectLeftResize(KEYFRAMES, 0, [
|
|
118
129
|
{ from: 0, to: 0 },
|
|
119
|
-
{ from: 50, to: 63.
|
|
130
|
+
{ from: 50, to: 63.636 },
|
|
120
131
|
{ from: 100, to: 100 },
|
|
121
132
|
]);
|
|
122
133
|
});
|
|
@@ -139,15 +150,7 @@ describe("resolveKeyframeRetime — single keyframe (both first and last)", () =
|
|
|
139
150
|
});
|
|
140
151
|
|
|
141
152
|
it("resizes left before the start", () => {
|
|
142
|
-
|
|
143
|
-
...LEFT_BOUNDARY_DROP,
|
|
144
|
-
keyframes: lone,
|
|
145
|
-
draggedTweenPct: 100,
|
|
146
|
-
});
|
|
147
|
-
expect(r.kind).toBe("resize");
|
|
148
|
-
expect(r.position).toBeCloseTo(0.5, 5);
|
|
149
|
-
expect(r.duration).toBeCloseTo(5.5, 5);
|
|
150
|
-
expect(r.pctRemap).toEqual([{ from: 100, to: 0 }]);
|
|
153
|
+
expectLeftResize(lone, 100, [{ from: 100, to: 0 }]);
|
|
151
154
|
});
|
|
152
155
|
});
|
|
153
156
|
|
|
@@ -55,7 +55,6 @@ const EPSILON_TIME = 1e-4;
|
|
|
55
55
|
const MIN_TWEEN_DURATION = 0.01;
|
|
56
56
|
|
|
57
57
|
const round3 = (n: number) => Math.round(n * 1000) / 1000;
|
|
58
|
-
const round1 = (n: number) => Math.round(n * 10) / 10; // 0.1% precision
|
|
59
58
|
const clamp = (n: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, n));
|
|
60
59
|
|
|
61
60
|
/** Resolve timing for a flat tween's synthesized start/end diamond. */
|
|
@@ -153,7 +152,7 @@ export function resolveKeyframeRetime(opts: {
|
|
|
153
152
|
const pctRemap: KeyframePctRemap[] = keyframes.map((kf, i) => {
|
|
154
153
|
const absTime =
|
|
155
154
|
i === draggedIdx ? dropAbsTime : tweenStart + (kf.percentage / 100) * tweenDuration;
|
|
156
|
-
return { from: kf.percentage, to:
|
|
155
|
+
return { from: kf.percentage, to: round3(((absTime - newStart) / newDuration) * 100) };
|
|
157
156
|
});
|
|
158
157
|
|
|
159
158
|
return {
|
|
@@ -83,10 +83,10 @@ function KeyframeGutter({
|
|
|
83
83
|
track("button", `Add ${property} keyframe`);
|
|
84
84
|
void onCommitAnimatedProperty(element, property, displayValue);
|
|
85
85
|
}}
|
|
86
|
-
onRemoveKeyframe={(pct) => {
|
|
86
|
+
onRemoveKeyframe={(pct, animationId) => {
|
|
87
87
|
if (!onRemoveKeyframe) return;
|
|
88
88
|
track("button", `Remove ${property} keyframe`);
|
|
89
|
-
onRemoveKeyframe(animIdForProp(property), pct);
|
|
89
|
+
onRemoveKeyframe(animationId ?? animIdForProp(property), pct);
|
|
90
90
|
}}
|
|
91
91
|
onConvertToKeyframes={() => {
|
|
92
92
|
if (!onConvertToKeyframes) return;
|
|
@@ -6,12 +6,14 @@ import { formatTimingValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
|
|
|
6
6
|
import { parseTimingValue } from "./propertyPanelTimingSection";
|
|
7
7
|
import { CommitField } from "./propertyPanelPrimitives";
|
|
8
8
|
import { AnimationCard } from "./AnimationCard";
|
|
9
|
-
import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimationConstants";
|
|
10
9
|
import {
|
|
11
|
-
trackAnimationMetaUpdate,
|
|
12
10
|
type GsapAnimationEditCallbacks,
|
|
11
|
+
withTrackedGsapAnimationCallbacks,
|
|
12
|
+
clearFocusedEaseSegment,
|
|
13
13
|
} from "./gsapAnimationCallbacks";
|
|
14
14
|
import { deriveElementTiming } from "./propertyPanelFlatTimingDerivation";
|
|
15
|
+
import { usePlayerStore } from "../../player";
|
|
16
|
+
import { GsapAddAnimationControl } from "./GsapAddAnimationControl";
|
|
15
17
|
|
|
16
18
|
export function FlatTimingRow({
|
|
17
19
|
element,
|
|
@@ -135,15 +137,17 @@ export function FlatMotionSection({
|
|
|
135
137
|
} & GsapAnimationEditCallbacks) {
|
|
136
138
|
const track = useTrackDesignInput();
|
|
137
139
|
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
const trackedCallbacks = withTrackedGsapAnimationCallbacks(callbacks, track);
|
|
141
|
+
const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
|
|
142
|
+
// Only consume a focus request aimed at the element THIS panel renders (not
|
|
143
|
+
// the store's selectedElementId, which flips synchronously during async
|
|
144
|
+
// selection resolution), so a shared class-selector animation id can't open
|
|
145
|
+
// the wrong element's editor.
|
|
146
|
+
const renderedElementId = `${element.sourceFile}#${element.id}`;
|
|
147
|
+
const focusedHere =
|
|
148
|
+
focusedEaseSegment && focusedEaseSegment.elementId === renderedElementId
|
|
149
|
+
? focusedEaseSegment
|
|
150
|
+
: null;
|
|
147
151
|
|
|
148
152
|
return (
|
|
149
153
|
<div className="space-y-3">
|
|
@@ -172,140 +176,22 @@ export function FlatMotionSection({
|
|
|
172
176
|
<div className="space-y-2">
|
|
173
177
|
{animations.map((anim, index) => (
|
|
174
178
|
<AnimationCard
|
|
179
|
+
{...trackedCallbacks}
|
|
175
180
|
key={anim.id}
|
|
176
181
|
animation={anim}
|
|
177
182
|
defaultExpanded={index === 0}
|
|
178
183
|
flat
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
callbacks.onUpdateProperty(animationId, property, value);
|
|
182
|
-
}}
|
|
183
|
-
onUpdateMeta={(animationId, updates) => {
|
|
184
|
-
trackAnimationMetaUpdate(track, updates);
|
|
185
|
-
callbacks.onUpdateMeta(animationId, updates);
|
|
186
|
-
}}
|
|
187
|
-
onDeleteAnimation={(animationId) => {
|
|
188
|
-
track("button", "Remove animation");
|
|
189
|
-
callbacks.onDeleteAnimation(animationId);
|
|
190
|
-
}}
|
|
191
|
-
onAddProperty={(animationId, property) => {
|
|
192
|
-
track("select", "Add effect property");
|
|
193
|
-
callbacks.onAddProperty(animationId, property);
|
|
194
|
-
}}
|
|
195
|
-
onRemoveProperty={(animationId, property) => {
|
|
196
|
-
track("button", `Remove ${property}`);
|
|
197
|
-
callbacks.onRemoveProperty(animationId, property);
|
|
198
|
-
}}
|
|
199
|
-
onUpdateFromProperty={
|
|
200
|
-
callbacks.onUpdateFromProperty
|
|
201
|
-
? (animationId, property, value) => {
|
|
202
|
-
trackProperty(property);
|
|
203
|
-
callbacks.onUpdateFromProperty?.(animationId, property, value);
|
|
204
|
-
}
|
|
205
|
-
: undefined
|
|
206
|
-
}
|
|
207
|
-
onAddFromProperty={
|
|
208
|
-
callbacks.onAddFromProperty
|
|
209
|
-
? (animationId, property) => {
|
|
210
|
-
track("select", "Add from property");
|
|
211
|
-
callbacks.onAddFromProperty?.(animationId, property);
|
|
212
|
-
}
|
|
213
|
-
: undefined
|
|
214
|
-
}
|
|
215
|
-
onRemoveFromProperty={
|
|
216
|
-
callbacks.onRemoveFromProperty
|
|
217
|
-
? (animationId, property) => {
|
|
218
|
-
track("button", `Remove from ${property}`);
|
|
219
|
-
callbacks.onRemoveFromProperty?.(animationId, property);
|
|
220
|
-
}
|
|
221
|
-
: undefined
|
|
222
|
-
}
|
|
223
|
-
onLivePreview={callbacks.onLivePreview}
|
|
224
|
-
onLivePreviewEnd={callbacks.onLivePreviewEnd}
|
|
225
|
-
onSetArcPath={
|
|
226
|
-
callbacks.onSetArcPath
|
|
227
|
-
? (animationId, config) => {
|
|
228
|
-
track(
|
|
229
|
-
"toggle",
|
|
230
|
-
config.autoRotate !== undefined ? "Auto rotate" : "Arc motion",
|
|
231
|
-
);
|
|
232
|
-
callbacks.onSetArcPath?.(animationId, config);
|
|
233
|
-
}
|
|
234
|
-
: undefined
|
|
235
|
-
}
|
|
236
|
-
onUpdateArcSegment={
|
|
237
|
-
callbacks.onUpdateArcSegment
|
|
238
|
-
? (animationId, segmentIndex, update) => {
|
|
239
|
-
if (update.curviness === undefined) {
|
|
240
|
-
track("button", `Reset arc segment ${segmentIndex + 1}`);
|
|
241
|
-
}
|
|
242
|
-
callbacks.onUpdateArcSegment?.(animationId, segmentIndex, update);
|
|
243
|
-
}
|
|
244
|
-
: undefined
|
|
245
|
-
}
|
|
246
|
-
onUpdateKeyframeEase={
|
|
247
|
-
callbacks.onUpdateKeyframeEase
|
|
248
|
-
? (animationId, percentage, ease) => {
|
|
249
|
-
track("select", "Keyframe ease");
|
|
250
|
-
callbacks.onUpdateKeyframeEase?.(animationId, percentage, ease);
|
|
251
|
-
}
|
|
252
|
-
: undefined
|
|
253
|
-
}
|
|
254
|
-
onSetAllKeyframeEases={
|
|
255
|
-
callbacks.onSetAllKeyframeEases
|
|
256
|
-
? (animationId, ease) => {
|
|
257
|
-
track("select", "All keyframe eases");
|
|
258
|
-
callbacks.onSetAllKeyframeEases?.(animationId, ease);
|
|
259
|
-
}
|
|
260
|
-
: undefined
|
|
261
|
-
}
|
|
262
|
-
onUnroll={
|
|
263
|
-
callbacks.onUnroll
|
|
264
|
-
? (animationId) => {
|
|
265
|
-
track("button", "Unroll animation");
|
|
266
|
-
callbacks.onUnroll?.(animationId);
|
|
267
|
-
}
|
|
268
|
-
: undefined
|
|
269
|
-
}
|
|
184
|
+
focusedSegment={focusedHere?.animationId === anim.id ? focusedHere : null}
|
|
185
|
+
onFocusSegmentConsumed={clearFocusedEaseSegment}
|
|
270
186
|
/>
|
|
271
187
|
))}
|
|
272
|
-
<
|
|
273
|
-
{addMenuOpen
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
title={METHOD_TOOLTIPS[method]}
|
|
280
|
-
onClick={() => {
|
|
281
|
-
track("button", `Add ${method} animation`);
|
|
282
|
-
onAddAnimation(method);
|
|
283
|
-
setAddMenuOpen(false);
|
|
284
|
-
}}
|
|
285
|
-
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"
|
|
286
|
-
>
|
|
287
|
-
{ADD_METHOD_LABELS[method] ?? method}
|
|
288
|
-
</button>
|
|
289
|
-
))}
|
|
290
|
-
<button
|
|
291
|
-
type="button"
|
|
292
|
-
onClick={() => setAddMenuOpen(false)}
|
|
293
|
-
className="px-1.5 text-[11px] text-panel-text-3 hover:text-panel-text-1"
|
|
294
|
-
>
|
|
295
|
-
Cancel
|
|
296
|
-
</button>
|
|
297
|
-
</div>
|
|
298
|
-
) : (
|
|
299
|
-
<button
|
|
300
|
-
type="button"
|
|
301
|
-
onClick={() => setAddMenuOpen(true)}
|
|
302
|
-
className="text-[11px] font-medium text-panel-text-3 transition-colors hover:text-panel-text-1"
|
|
303
|
-
title="Add a new animation effect to this element"
|
|
304
|
-
>
|
|
305
|
-
+ Add effect
|
|
306
|
-
</button>
|
|
307
|
-
)}
|
|
308
|
-
</div>
|
|
188
|
+
<GsapAddAnimationControl
|
|
189
|
+
open={addMenuOpen}
|
|
190
|
+
setOpen={setAddMenuOpen}
|
|
191
|
+
onAddAnimation={onAddAnimation}
|
|
192
|
+
track={track}
|
|
193
|
+
variant="flat"
|
|
194
|
+
/>
|
|
309
195
|
</div>
|
|
310
196
|
)}
|
|
311
197
|
</>
|