@hyperframes/studio 0.7.90 → 0.7.93
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-RS9ROX_S.js → hyperframes-player-NSdcoOcK.js} +1 -1
- package/dist/assets/{index-CquRJgm-.js → index-CCGnc_0M.js} +1 -1
- package/dist/assets/index-D78KEjgB.css +1 -0
- package/dist/assets/index-Dp1zVb9X.js +428 -0
- package/dist/assets/{index-Bi-CwQDX.js → index-wS6f7F5q.js} +1 -1
- package/dist/index.d.ts +40 -36
- package/dist/index.html +2 -2
- package/dist/index.js +9598 -6798
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/App.tsx +14 -14
- package/src/components/DesignPanelPromoteProvider.tsx +1 -1
- package/src/components/ExternalFileConflictBanner.test.tsx +142 -0
- package/src/components/ExternalFileConflictBanner.tsx +255 -0
- package/src/components/StudioRightPanel.tsx +1 -1
- package/src/components/editor/GsapAnimationList.tsx +84 -0
- package/src/components/editor/GsapAnimationSection.test.tsx +100 -11
- package/src/components/editor/GsapAnimationSection.tsx +11 -41
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/PropertyPanelFlat.tsx +29 -26
- package/src/components/editor/gsapAnimationCallbacks.ts +0 -10
- package/src/components/editor/propertyPanel3dTransform.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +37 -2
- package/src/components/editor/propertyPanelFlatDescriptors.ts +20 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +56 -55
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +9 -37
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +69 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelTransformCommit.test.ts +57 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +14 -14
- package/src/components/editor/propertyPanelTypes.ts +10 -4
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +65 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +66 -10
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +78 -4
- package/src/components/nle/useTimelineEditCallbacks.ts +12 -7
- package/src/components/renders/useRenderQueue.ts +16 -3
- package/src/components/renders/useRenderQueueTelemetry.test.tsx +104 -0
- package/src/components/sidebar/AssetCard.tsx +4 -3
- package/src/components/sidebar/AudioRow.tsx +4 -3
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/contexts/FileManagerContext.tsx +12 -0
- package/src/hooks/externalFileChangeOwnership.test.ts +17 -0
- package/src/hooks/externalFileReloadBus.test.ts +34 -0
- package/src/hooks/externalFileReloadBus.ts +18 -0
- package/src/hooks/gsapDragCommit.test.ts +39 -1
- package/src/hooks/gsapDragCommit.ts +12 -40
- package/src/hooks/gsapEditOutcome.ts +72 -0
- package/src/hooks/gsapResizeIntercept.test.ts +90 -2
- package/src/hooks/gsapResizeIntercept.ts +44 -16
- package/src/hooks/gsapRuntimeBridge.test.ts +195 -27
- package/src/hooks/gsapRuntimeBridge.ts +93 -28
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +194 -24
- package/src/hooks/useAnimatedPropertyCommit.ts +101 -49
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +215 -0
- package/src/hooks/useDomEditSession.test.tsx +1 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +61 -0
- package/src/hooks/useDomGeometryCommits.ts +20 -0
- package/src/hooks/useEditorSave.test.tsx +131 -0
- package/src/hooks/useEditorSave.ts +113 -39
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +221 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +429 -0
- package/src/hooks/useFileManager.projectOwnership.test.tsx +139 -7
- package/src/hooks/useFileManager.ts +32 -3
- package/src/hooks/useGsapAnimationFetchFallback.ts +45 -24
- package/src/hooks/useGsapAwareEditing.test.tsx +125 -39
- package/src/hooks/useGsapAwareEditing.ts +88 -27
- package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +42 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
- package/src/hooks/useGsapKeyframeOps.ts +20 -9
- package/src/hooks/usePreviewPersistence.test.ts +23 -0
- package/src/hooks/usePreviewPersistence.ts +22 -58
- package/src/hooks/useSdkSession.ts +8 -52
- package/src/hooks/useStudioExternalFileChanges.ts +81 -0
- package/src/hooks/useStudioSdkSessions.ts +2 -7
- package/src/hooks/useStudioSessionStart.ts +15 -0
- package/src/hooks/useStudioTestHooks.test.tsx +11 -7
- package/src/player/components/BeatStrip.test.tsx +374 -0
- package/src/player/components/BeatStrip.tsx +295 -37
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +16 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +6 -5
- package/src/player/components/LayerDisclosureRow.tsx +2 -0
- package/src/player/components/Timeline.test.ts +26 -8
- package/src/player/components/Timeline.tsx +87 -88
- package/src/player/components/Timeline.virtualization.test.tsx +54 -11
- package/src/player/components/TimelineCanvas.tsx +14 -71
- package/src/player/components/TimelineClip.test.tsx +17 -2
- package/src/player/components/TimelineClip.tsx +20 -5
- package/src/player/components/TimelineClipDiamonds.test.tsx +536 -27
- package/src/player/components/TimelineClipDiamonds.tsx +118 -223
- package/src/player/components/TimelineCompactDiamonds.tsx +84 -0
- package/src/player/components/TimelineDiamondConnectors.tsx +31 -3
- package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
- package/src/player/components/TimelineGestureOverlay.tsx +96 -0
- package/src/player/components/TimelineLanes.test.tsx +64 -7
- package/src/player/components/TimelineLanes.tsx +84 -66
- package/src/player/components/TimelineOverlays.test.ts +161 -0
- package/src/player/components/TimelineOverlays.tsx +104 -11
- package/src/player/components/TimelinePropertyLanes.test.tsx +9 -5
- package/src/player/components/TimelinePropertyLanes.tsx +9 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -0
- package/src/player/components/TimelineTrackHeader.tsx +16 -0
- package/src/player/components/TimelineTrackRow.tsx +58 -7
- package/src/player/components/timelineCallbacks.ts +1 -1
- package/src/player/components/timelineClipDragGestureLifecycle.test.ts +18 -3
- package/src/player/components/timelineClipDragGestureLifecycle.ts +199 -51
- package/src/player/components/timelineClipDragPreview.test.ts +33 -0
- package/src/player/components/timelineClipDragPreview.ts +18 -12
- package/src/player/components/timelineClipDragTypes.ts +12 -0
- package/src/player/components/timelineDiamondTypes.ts +18 -21
- package/src/player/components/timelineDragDrop.test.tsx +206 -0
- package/src/player/components/timelineDragDrop.ts +152 -30
- package/src/player/components/timelineKeyboardNavigation.test.ts +303 -0
- package/src/player/components/timelineKeyboardNavigation.ts +337 -0
- package/src/player/components/timelineLaneProps.ts +3 -1
- package/src/player/components/timelineMarquee.test.ts +73 -25
- package/src/player/components/timelineMarquee.ts +44 -16
- package/src/player/components/timelineNavigationIdentity.ts +38 -0
- package/src/player/components/timelineTestViewport.ts +13 -0
- package/src/player/components/timelineViewModel.ts +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +219 -17
- package/src/player/components/useTimelineClipDrag.ts +112 -70
- package/src/player/components/useTimelineClipRenderWindow.ts +8 -39
- package/src/player/components/useTimelineFocusCoordinator.test.tsx +174 -0
- package/src/player/components/useTimelineFocusCoordinator.ts +221 -0
- package/src/player/components/useTimelineKeyboardActor.test.tsx +267 -0
- package/src/player/components/useTimelineKeyboardActor.ts +147 -0
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +43 -7
- package/src/player/components/useTimelineKeyframeHandlers.ts +465 -2
- package/src/player/components/useTimelineLaneMoveRefresh.ts +7 -0
- package/src/player/components/useTimelineLogicalFocus.ts +80 -0
- package/src/player/components/useTimelineLogicalRows.test.tsx +57 -0
- package/src/player/components/useTimelineLogicalRows.ts +47 -0
- package/src/player/components/useTimelineRangeSelection.test.tsx +204 -0
- package/src/player/components/useTimelineRangeSelection.ts +202 -69
- package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +3 -2
- package/src/player/components/useTimelineRowVirtualization.ts +26 -29
- package/src/player/components/useTrackGapMenu.test.tsx +64 -0
- package/src/player/components/useTrackGapMenu.ts +32 -9
- package/src/player/store/keyframeSlice.ts +59 -18
- package/src/player/store/liveTime.ts +11 -0
- package/src/player/store/playerStore.test.ts +106 -23
- package/src/player/store/playerStore.ts +43 -40
- package/src/player/store/timelineFocusState.ts +15 -0
- package/src/telemetry/canary.test.ts +325 -0
- package/src/telemetry/canary.ts +268 -0
- package/src/telemetry/client.test.ts +8 -3
- package/src/telemetry/client.ts +13 -43
- package/src/telemetry/config.ts +16 -2
- package/src/telemetry/distinctId.test.ts +35 -0
- package/src/telemetry/distinctId.ts +11 -2
- package/src/telemetry/policy.test.ts +81 -0
- package/src/telemetry/policy.ts +104 -0
- package/src/utils/domEditSaveQueue.test.ts +37 -1
- package/src/utils/domEditSaveQueue.ts +22 -4
- package/src/utils/externalConflictStorage.test.ts +95 -0
- package/src/utils/externalConflictStorage.ts +172 -0
- package/src/utils/studioFileVersion.test.ts +28 -1
- package/src/utils/studioFileVersion.ts +30 -0
- package/src/utils/studioPendingEdits.test.ts +108 -1
- package/src/utils/studioPendingEdits.ts +39 -2
- package/src/utils/studioSaveDiagnostics.ts +5 -0
- package/src/utils/studioTelemetry.test.ts +76 -0
- package/src/utils/studioTelemetry.ts +14 -6
- package/dist/assets/index-BXkzp_OU.css +0 -1
- package/dist/assets/index-Cql15Yur.js +0 -428
- package/src/player/components/useTimelineRevealClip.test.tsx +0 -139
- package/src/player/components/useTimelineRevealClip.ts +0 -180
|
@@ -5,11 +5,15 @@ import { createRoot } from "react-dom/client";
|
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
7
7
|
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
|
|
8
|
-
import { usePlayerStore } from "../../player";
|
|
8
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
9
9
|
import { GsapAnimationSection } from "./GsapAnimationSection";
|
|
10
10
|
|
|
11
11
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
12
|
|
|
13
|
+
const animationCardMock = vi.hoisted(() => ({
|
|
14
|
+
consumers: [] as Array<{ tweenPercentage: number | null; consume: () => void }>,
|
|
15
|
+
}));
|
|
16
|
+
|
|
13
17
|
vi.mock("./AnimationCard", () => ({
|
|
14
18
|
AnimationCard: ({
|
|
15
19
|
animation,
|
|
@@ -19,22 +23,27 @@ vi.mock("./AnimationCard", () => ({
|
|
|
19
23
|
animation: GsapAnimation;
|
|
20
24
|
focusedSegment: { tweenPercentage: number } | null;
|
|
21
25
|
onFocusSegmentConsumed: () => void;
|
|
22
|
-
}) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
}) => {
|
|
27
|
+
animationCardMock.consumers.push({
|
|
28
|
+
tweenPercentage: focusedSegment?.tweenPercentage ?? null,
|
|
29
|
+
consume: onFocusSegmentConsumed,
|
|
30
|
+
});
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
data-testid={`animation-${animation.id}`}
|
|
35
|
+
data-focused={focusedSegment ? String(focusedSegment.tweenPercentage) : ""}
|
|
36
|
+
onClick={onFocusSegmentConsumed}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
},
|
|
32
40
|
}));
|
|
33
41
|
|
|
34
42
|
vi.mock("./GsapAddAnimationControl", () => ({ GsapAddAnimationControl: () => null }));
|
|
35
43
|
|
|
36
44
|
afterEach(() => {
|
|
37
45
|
document.body.innerHTML = "";
|
|
46
|
+
animationCardMock.consumers = [];
|
|
38
47
|
usePlayerStore.getState().reset();
|
|
39
48
|
});
|
|
40
49
|
|
|
@@ -76,8 +85,22 @@ function renderSection(elementId: string) {
|
|
|
76
85
|
return { host, root, render };
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
function focusSharedAnimation(elementId: string, tweenPercentage: number) {
|
|
89
|
+
const store = usePlayerStore.getState();
|
|
90
|
+
store.beginTimelineSession("project-a");
|
|
91
|
+
store.setSelectedElementId(elementId);
|
|
92
|
+
store.setFocusedEaseSegment({
|
|
93
|
+
elementId,
|
|
94
|
+
animationId: sharedAnimation.id,
|
|
95
|
+
tweenPercentage,
|
|
96
|
+
});
|
|
97
|
+
return store;
|
|
98
|
+
}
|
|
99
|
+
|
|
79
100
|
describe("GsapAnimationSection", () => {
|
|
80
101
|
it("consumes a shared animation id only for the focused element", () => {
|
|
102
|
+
usePlayerStore.getState().beginTimelineSession("project-a");
|
|
103
|
+
usePlayerStore.getState().setSelectedElementId("index.html#second");
|
|
81
104
|
usePlayerStore.getState().setFocusedEaseSegment({
|
|
82
105
|
elementId: "index.html#second",
|
|
83
106
|
animationId: sharedAnimation.id,
|
|
@@ -99,4 +122,70 @@ describe("GsapAnimationSection", () => {
|
|
|
99
122
|
expect(usePlayerStore.getState().focusedEaseSegment).toBeNull();
|
|
100
123
|
act(() => view.root.unmount());
|
|
101
124
|
});
|
|
125
|
+
|
|
126
|
+
it("does not let an older consumer clear a newer request", () => {
|
|
127
|
+
const store = focusSharedAnimation("index.html#first", 25);
|
|
128
|
+
const view = renderSection("index.html#first");
|
|
129
|
+
const staleConsumer = animationCardMock.consumers.at(-1)?.consume;
|
|
130
|
+
if (!staleConsumer) throw new Error("expected first focus consumer");
|
|
131
|
+
|
|
132
|
+
act(() => {
|
|
133
|
+
store.setFocusedEaseSegment({
|
|
134
|
+
elementId: "index.html#first",
|
|
135
|
+
animationId: sharedAnimation.id,
|
|
136
|
+
tweenPercentage: 75,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
const current = usePlayerStore.getState().focusedEaseSegment;
|
|
140
|
+
if (!current) throw new Error("expected replacement request");
|
|
141
|
+
|
|
142
|
+
act(() => staleConsumer());
|
|
143
|
+
expect(usePlayerStore.getState().focusedEaseSegment).toBe(current);
|
|
144
|
+
|
|
145
|
+
const currentConsumer = animationCardMock.consumers.at(-1)?.consume;
|
|
146
|
+
if (!currentConsumer) throw new Error("expected replacement focus consumer");
|
|
147
|
+
act(() => currentConsumer());
|
|
148
|
+
expect(usePlayerStore.getState().focusedEaseSegment).toBeNull();
|
|
149
|
+
act(() => view.root.unmount());
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("keeps the focus consumer stable across unrelated parent renders", () => {
|
|
153
|
+
focusSharedAnimation("index.html#first", 25);
|
|
154
|
+
const view = renderSection("index.html#first");
|
|
155
|
+
const firstConsumer = animationCardMock.consumers.at(-1)?.consume;
|
|
156
|
+
if (!firstConsumer) throw new Error("expected focus consumer");
|
|
157
|
+
|
|
158
|
+
view.render("index.html#first");
|
|
159
|
+
|
|
160
|
+
expect(animationCardMock.consumers.at(-1)?.consume).toBe(firstConsumer);
|
|
161
|
+
act(() => view.root.unmount());
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("rejects a request from an earlier project session", () => {
|
|
165
|
+
const store = usePlayerStore.getState();
|
|
166
|
+
store.beginTimelineSession("project-a");
|
|
167
|
+
store.setSelectedElementId("index.html#first");
|
|
168
|
+
store.setFocusedEaseSegment({
|
|
169
|
+
elementId: "index.html#first",
|
|
170
|
+
animationId: sharedAnimation.id,
|
|
171
|
+
tweenPercentage: 50,
|
|
172
|
+
});
|
|
173
|
+
const staleRequest = usePlayerStore.getState().focusedEaseSegment;
|
|
174
|
+
if (!staleRequest) throw new Error("expected request");
|
|
175
|
+
|
|
176
|
+
const view = renderSection("index.html#first");
|
|
177
|
+
|
|
178
|
+
act(() => {
|
|
179
|
+
store.beginTimelineSession("project-b");
|
|
180
|
+
store.setSelectedElementId("index.html#first");
|
|
181
|
+
usePlayerStore.setState({ focusedEaseSegment: staleRequest });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const card = view.host.querySelector<HTMLButtonElement>(
|
|
185
|
+
"[data-testid='animation-shared-animation']",
|
|
186
|
+
);
|
|
187
|
+
expect(card?.dataset.focused).toBe("");
|
|
188
|
+
expect(usePlayerStore.getState().focusedEaseSegment).toBe(staleRequest);
|
|
189
|
+
act(() => view.root.unmount());
|
|
190
|
+
});
|
|
102
191
|
});
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import { memo
|
|
1
|
+
import { memo } from "react";
|
|
2
2
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
3
|
import { Film } from "../../icons/SystemIcons";
|
|
4
4
|
import { Section } from "./propertyPanelPrimitives";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
type GsapAnimationEditCallbacks,
|
|
8
|
-
withTrackedGsapAnimationCallbacks,
|
|
9
|
-
clearFocusedEaseSegment,
|
|
10
|
-
} from "./gsapAnimationCallbacks";
|
|
11
|
-
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
12
|
-
import { usePlayerStore } from "../../player";
|
|
13
|
-
import { GsapAddAnimationControl } from "./GsapAddAnimationControl";
|
|
5
|
+
import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks";
|
|
6
|
+
import { GsapAnimationList } from "./GsapAnimationList";
|
|
14
7
|
|
|
15
8
|
interface GsapAnimationSectionProps extends GsapAnimationEditCallbacks {
|
|
16
9
|
elementId: string;
|
|
@@ -21,18 +14,13 @@ interface GsapAnimationSectionProps extends GsapAnimationEditCallbacks {
|
|
|
21
14
|
}
|
|
22
15
|
|
|
23
16
|
export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
24
|
-
animations,
|
|
25
17
|
elementId,
|
|
18
|
+
animations,
|
|
26
19
|
multipleTimelines,
|
|
27
20
|
unsupportedTimelinePattern,
|
|
28
21
|
onAddAnimation,
|
|
29
22
|
...callbacks
|
|
30
23
|
}: GsapAnimationSectionProps) {
|
|
31
|
-
const track = useTrackDesignInput();
|
|
32
|
-
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
|
33
|
-
const trackedCallbacks = withTrackedGsapAnimationCallbacks(callbacks, track);
|
|
34
|
-
const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
|
|
35
|
-
|
|
36
24
|
return (
|
|
37
25
|
<Section title="Animation" icon={<Film size={15} />}>
|
|
38
26
|
{multipleTimelines && (
|
|
@@ -49,31 +37,13 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
|
49
37
|
</p>
|
|
50
38
|
)}
|
|
51
39
|
{multipleTimelines || unsupportedTimelinePattern ? null : (
|
|
52
|
-
<
|
|
53
|
-
{
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
focusedSegment={
|
|
60
|
-
focusedEaseSegment?.elementId === elementId &&
|
|
61
|
-
focusedEaseSegment.animationId === anim.id
|
|
62
|
-
? focusedEaseSegment
|
|
63
|
-
: null
|
|
64
|
-
}
|
|
65
|
-
onFocusSegmentConsumed={clearFocusedEaseSegment}
|
|
66
|
-
/>
|
|
67
|
-
))}
|
|
68
|
-
|
|
69
|
-
<GsapAddAnimationControl
|
|
70
|
-
open={addMenuOpen}
|
|
71
|
-
setOpen={setAddMenuOpen}
|
|
72
|
-
onAddAnimation={onAddAnimation}
|
|
73
|
-
track={track}
|
|
74
|
-
variant="classic"
|
|
75
|
-
/>
|
|
76
|
-
</div>
|
|
40
|
+
<GsapAnimationList
|
|
41
|
+
{...callbacks}
|
|
42
|
+
elementId={elementId}
|
|
43
|
+
animations={animations}
|
|
44
|
+
onAddAnimation={onAddAnimation}
|
|
45
|
+
variant="classic"
|
|
46
|
+
/>
|
|
77
47
|
)}
|
|
78
48
|
</Section>
|
|
79
49
|
);
|
|
@@ -570,8 +570,8 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
|
|
570
570
|
onUpdateArcSegment={onUpdateArcSegment}
|
|
571
571
|
onUnroll={onUnroll}
|
|
572
572
|
onUpdateKeyframeEase={onUpdateKeyframeEase}
|
|
573
|
-
onSetAllKeyframeEases={onSetAllKeyframeEases}
|
|
574
573
|
onUpdateSegmentEase={onUpdateSegmentEase}
|
|
574
|
+
onSetAllKeyframeEases={onSetAllKeyframeEases}
|
|
575
575
|
/>
|
|
576
576
|
)}
|
|
577
577
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { scopedElementKey } from "../../hooks/gsapKeyframeCacheHelpers";
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useShallow } from "zustand/react/shallow";
|
|
3
4
|
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
|
|
4
5
|
import { slugifyDesignInput } from "../../utils/designInputTracking";
|
|
5
6
|
import { isTextEditableSelection } from "./domEditing";
|
|
@@ -18,6 +19,7 @@ import { createGsapLivePreview } from "./gsapLivePreview";
|
|
|
18
19
|
import { formatTextFieldPreview } from "./propertyPanelSections";
|
|
19
20
|
import { useColorGradingController } from "./useColorGradingController";
|
|
20
21
|
import { usePlayerStore } from "../../player";
|
|
22
|
+
import { isFocusedEaseRequestCurrent } from "../../player/store/keyframeSlice";
|
|
21
23
|
import {
|
|
22
24
|
FlatColorGradingAccessory,
|
|
23
25
|
FlatColorGradingSection,
|
|
@@ -31,24 +33,10 @@ import {
|
|
|
31
33
|
deriveMediaOverlayPlacement,
|
|
32
34
|
FlatOverlaysSection,
|
|
33
35
|
} from "./propertyPanelFlatOverlaysSection";
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
summary?: string;
|
|
39
|
-
accessory?: ReactNode;
|
|
40
|
-
content: ReactNode;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Required callback shape for the gated-off Motion effect list.
|
|
44
|
-
const EMPTY_GSAP_EFFECT_HANDLERS = {
|
|
45
|
-
onAddAnimation: () => {},
|
|
46
|
-
onUpdateProperty: () => {},
|
|
47
|
-
onUpdateMeta: () => {},
|
|
48
|
-
onDeleteAnimation: () => {},
|
|
49
|
-
onAddProperty: () => {},
|
|
50
|
-
onRemoveProperty: () => {},
|
|
51
|
-
};
|
|
36
|
+
import {
|
|
37
|
+
EMPTY_GSAP_EFFECT_HANDLERS,
|
|
38
|
+
type FlatGroupDescriptor,
|
|
39
|
+
} from "./propertyPanelFlatDescriptors";
|
|
52
40
|
|
|
53
41
|
/** The flat inspector shell with one shared open-group state. */
|
|
54
42
|
// fallow-ignore-next-line complexity
|
|
@@ -161,11 +149,18 @@ export function PropertyPanelFlat({
|
|
|
161
149
|
// When the inline timeline ease button focuses a segment on this element,
|
|
162
150
|
// force the Motion group open so its AnimationCard (which only mounts while
|
|
163
151
|
// the group is expanded) can consume the focus and reveal the ease editor.
|
|
164
|
-
const focusedEaseSegment = usePlayerStore(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
152
|
+
const { focusedEaseSegment, timelineProjectId, timelineSessionEpoch } = usePlayerStore(
|
|
153
|
+
useShallow((state) => ({
|
|
154
|
+
focusedEaseSegment: state.focusedEaseSegment,
|
|
155
|
+
timelineProjectId: state.timelineProjectId,
|
|
156
|
+
timelineSessionEpoch: state.timelineSessionEpoch,
|
|
157
|
+
})),
|
|
158
|
+
);
|
|
159
|
+
// Identity of the element THIS panel actually renders (not the store's
|
|
160
|
+
// selectedElementId, which flips synchronously on selection while the panel
|
|
161
|
+
// still renders the previous element during async DOM-selection resolution):
|
|
162
|
+
// a stale panel would otherwise consume a focus request meant for its
|
|
163
|
+
// successor when both share a class-selector animation id.
|
|
169
164
|
const renderedElementId = scopedElementKey(element);
|
|
170
165
|
// Adjusted during render (not an effect) so the card mounts on the same
|
|
171
166
|
// commit the request lands on. Keyed on request identity: a group the user
|
|
@@ -174,8 +169,16 @@ export function PropertyPanelFlat({
|
|
|
174
169
|
if (focusedEaseSegment !== consumedFocus) {
|
|
175
170
|
setConsumedFocus(focusedEaseSegment);
|
|
176
171
|
const focusesThisPanel =
|
|
177
|
-
focusedEaseSegment
|
|
178
|
-
|
|
172
|
+
focusedEaseSegment !== null &&
|
|
173
|
+
// A request from a previous project/session/selection is stale: it must
|
|
174
|
+
// not reopen Motion on whichever panel happens to be mounted now.
|
|
175
|
+
isFocusedEaseRequestCurrent(focusedEaseSegment, {
|
|
176
|
+
timelineProjectId,
|
|
177
|
+
timelineSessionEpoch,
|
|
178
|
+
selectedElementId,
|
|
179
|
+
}) &&
|
|
180
|
+
focusedEaseSegment.elementId === renderedElementId &&
|
|
181
|
+
gsapAnimations.some((animation) => animation.id === focusedEaseSegment.animationId);
|
|
179
182
|
if (focusesThisPanel) setOpenGroupId("motion");
|
|
180
183
|
}
|
|
181
184
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ArcPathSegment } from "@hyperframes/parsers/gsap-parser";
|
|
2
|
-
import { usePlayerStore } from "../../player";
|
|
3
2
|
import type { AnimationKeyframeTarget } from "../../hooks/gsapTweenSynth";
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -164,12 +163,3 @@ export function withTrackedGsapAnimationCallbacks(
|
|
|
164
163
|
: undefined,
|
|
165
164
|
};
|
|
166
165
|
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Stable consumer for the store's one-shot ease-focus request. Module-level on
|
|
170
|
-
* purpose: an inline arrow in the section components is a dep of AnimationCard's
|
|
171
|
-
* focus effect, so a fresh identity each render re-runs that effect every render.
|
|
172
|
-
*/
|
|
173
|
-
export function clearFocusedEaseSegment(): void {
|
|
174
|
-
usePlayerStore.getState().setFocusedEaseSegment(null);
|
|
175
|
-
}
|
|
@@ -253,7 +253,7 @@ function Transform3dField({
|
|
|
253
253
|
onCommit={(next) => {
|
|
254
254
|
const v = parse(next);
|
|
255
255
|
if (v != null && onCommitAnimatedProperty) {
|
|
256
|
-
|
|
256
|
+
return onCommitAnimatedProperty(ctx.element, prop, v);
|
|
257
257
|
}
|
|
258
258
|
}}
|
|
259
259
|
/>
|
|
@@ -21,7 +21,7 @@ export function CommitField({
|
|
|
21
21
|
liveCommit?: boolean;
|
|
22
22
|
align?: "left" | "right";
|
|
23
23
|
onPreview?: (nextValue: string) => void;
|
|
24
|
-
onCommit: (nextValue: string) => void
|
|
24
|
+
onCommit: (nextValue: string) => void | Promise<void>;
|
|
25
25
|
}) {
|
|
26
26
|
const [draft, setDraft] = useState(value);
|
|
27
27
|
const valueRef = useRef(value);
|
|
@@ -29,6 +29,19 @@ export function CommitField({
|
|
|
29
29
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
30
30
|
const focusedRef = useRef(false);
|
|
31
31
|
const dirtyRef = useRef(false);
|
|
32
|
+
const commitGenerationRef = useRef(0);
|
|
33
|
+
const pendingCommitRef = useRef<{
|
|
34
|
+
baseline: string;
|
|
35
|
+
optimistic: string;
|
|
36
|
+
} | null>(null);
|
|
37
|
+
const lastValueRef = useRef(value);
|
|
38
|
+
if (!Object.is(lastValueRef.current, value)) {
|
|
39
|
+
lastValueRef.current = value;
|
|
40
|
+
if (!Object.is(pendingCommitRef.current?.optimistic, value)) {
|
|
41
|
+
commitGenerationRef.current += 1;
|
|
42
|
+
pendingCommitRef.current = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
32
45
|
valueRef.current = value;
|
|
33
46
|
draftRef.current = draft;
|
|
34
47
|
|
|
@@ -67,14 +80,34 @@ export function CommitField({
|
|
|
67
80
|
}, 250);
|
|
68
81
|
};
|
|
69
82
|
const cancelGesture = () => {
|
|
83
|
+
commitGenerationRef.current += 1;
|
|
70
84
|
clearGestureSettleTimer();
|
|
71
85
|
gestureActiveRef.current = false;
|
|
72
86
|
gestureTransaction.cancel();
|
|
73
87
|
};
|
|
74
88
|
const commitDraft = (nextValue: string) => {
|
|
89
|
+
const generation = ++commitGenerationRef.current;
|
|
75
90
|
setDraft(nextValue);
|
|
76
91
|
onPreview?.(nextValue);
|
|
77
|
-
if (nextValue !== valueRef.current)
|
|
92
|
+
if (nextValue !== valueRef.current) {
|
|
93
|
+
const baseline = valueRef.current;
|
|
94
|
+
pendingCommitRef.current = { baseline, optimistic: nextValue };
|
|
95
|
+
const rollback = () => {
|
|
96
|
+
if (generation !== commitGenerationRef.current) return;
|
|
97
|
+
pendingCommitRef.current = null;
|
|
98
|
+
// The source write is authoritative. A rejected mutation must not leave
|
|
99
|
+
// the field showing an optimistic value that will disappear on seek.
|
|
100
|
+
setDraft(baseline);
|
|
101
|
+
onPreview?.(baseline);
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
void Promise.resolve(onCommit(nextValue)).then(() => {
|
|
105
|
+
if (generation === commitGenerationRef.current) pendingCommitRef.current = null;
|
|
106
|
+
}, rollback);
|
|
107
|
+
} catch {
|
|
108
|
+
rollback();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
78
111
|
};
|
|
79
112
|
const cancelGestureFromKeyEvent = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
80
113
|
if (!gestureActiveRef.current) return false;
|
|
@@ -89,6 +122,7 @@ export function CommitField({
|
|
|
89
122
|
const nextDraft = adjustNumericToken(draftRef.current, direction, event);
|
|
90
123
|
if (!nextDraft) return;
|
|
91
124
|
event.preventDefault();
|
|
125
|
+
commitGenerationRef.current += 1;
|
|
92
126
|
dirtyRef.current = false;
|
|
93
127
|
gestureActiveRef.current = true;
|
|
94
128
|
gestureTransaction.preview(nextDraft);
|
|
@@ -148,6 +182,7 @@ export function CommitField({
|
|
|
148
182
|
focusedRef.current = true;
|
|
149
183
|
}}
|
|
150
184
|
onChange={(event) => {
|
|
185
|
+
commitGenerationRef.current += 1;
|
|
151
186
|
settleGesture();
|
|
152
187
|
dirtyRef.current = true;
|
|
153
188
|
setDraft(event.target.value);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type FlatGroupDescriptor = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
summary?: string;
|
|
7
|
+
accessory?: ReactNode;
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// FlatMotionSection never calls these while effect cards are hidden; they only
|
|
12
|
+
// provide its required callback shape on the gated-off path.
|
|
13
|
+
export const EMPTY_GSAP_EFFECT_HANDLERS = {
|
|
14
|
+
onAddAnimation: () => {},
|
|
15
|
+
onUpdateProperty: () => {},
|
|
16
|
+
onUpdateMeta: () => {},
|
|
17
|
+
onDeleteAnimation: () => {},
|
|
18
|
+
onAddProperty: () => {},
|
|
19
|
+
onRemoveProperty: () => {},
|
|
20
|
+
};
|
|
@@ -273,13 +273,14 @@ describe("FlatMotionSection", () => {
|
|
|
273
273
|
expect(buttons().some((b) => b.textContent === "+ Add effect")).toBe(true);
|
|
274
274
|
act(() => root.unmount());
|
|
275
275
|
});
|
|
276
|
-
});
|
|
277
276
|
|
|
278
|
-
it("forwards focused bulk segment easing through the flat animation card", () => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
it("forwards focused bulk segment easing through the flat animation card", () => {
|
|
278
|
+
const onUpdateKeyframeEase = vi.fn();
|
|
279
|
+
const onUpdateSegmentEase = vi.fn();
|
|
280
|
+
const store = usePlayerStore.getState();
|
|
281
|
+
store.beginTimelineSession("project-a");
|
|
282
|
+
store.setSelectedElementId("index.html#hero");
|
|
283
|
+
store.setFocusedEaseSegment({
|
|
283
284
|
elementId: "index.html#hero",
|
|
284
285
|
animationId: "a1",
|
|
285
286
|
tweenPercentage: 50,
|
|
@@ -287,55 +288,55 @@ it("forwards focused bulk segment easing through the flat animation card", () =>
|
|
|
287
288
|
{ animationId: "a1", tweenPercentage: 50 },
|
|
288
289
|
{ animationId: "a2", tweenPercentage: 75 },
|
|
289
290
|
],
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
},
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
);
|
|
291
|
+
});
|
|
292
|
+
const { host, root } = renderInto(
|
|
293
|
+
<FlatMotionSection
|
|
294
|
+
element={baseElement()}
|
|
295
|
+
animations={[
|
|
296
|
+
{
|
|
297
|
+
id: "a1",
|
|
298
|
+
method: "to",
|
|
299
|
+
position: 0,
|
|
300
|
+
duration: 1,
|
|
301
|
+
properties: { x: 100 },
|
|
302
|
+
keyframes: {
|
|
303
|
+
format: "percentage",
|
|
304
|
+
keyframes: [
|
|
305
|
+
{ percentage: 0, properties: { x: 0 } },
|
|
306
|
+
{ percentage: 50, properties: { x: 100 } },
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
} as never,
|
|
310
|
+
]}
|
|
311
|
+
showTiming={false}
|
|
312
|
+
showEffects
|
|
313
|
+
onSetAttribute={vi.fn()}
|
|
314
|
+
onAddAnimation={vi.fn()}
|
|
315
|
+
onUpdateProperty={vi.fn()}
|
|
316
|
+
onUpdateMeta={vi.fn()}
|
|
317
|
+
onDeleteAnimation={vi.fn()}
|
|
318
|
+
onAddProperty={vi.fn()}
|
|
319
|
+
onRemoveProperty={vi.fn()}
|
|
320
|
+
onUpdateKeyframeEase={onUpdateKeyframeEase}
|
|
321
|
+
onUpdateSegmentEase={onUpdateSegmentEase}
|
|
322
|
+
/>,
|
|
323
|
+
);
|
|
324
324
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
325
|
+
const dropdown = host.querySelector<HTMLButtonElement>("[data-ease-type-dropdown]");
|
|
326
|
+
expect(dropdown).not.toBeNull();
|
|
327
|
+
act(() => dropdown?.click());
|
|
328
|
+
const preset = host.querySelector<HTMLButtonElement>('[data-ease-preset-id="quad-out"]');
|
|
329
|
+
expect(preset).not.toBeNull();
|
|
330
|
+
act(() => preset?.click());
|
|
331
331
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
332
|
+
expect(onUpdateSegmentEase).toHaveBeenCalledExactlyOnceWith(
|
|
333
|
+
[
|
|
334
|
+
{ animationId: "a1", tweenPercentage: 50 },
|
|
335
|
+
{ animationId: "a2", tweenPercentage: 75 },
|
|
336
|
+
],
|
|
337
|
+
"power2.out",
|
|
338
|
+
);
|
|
339
|
+
expect(onUpdateKeyframeEase).not.toHaveBeenCalled();
|
|
340
|
+
act(() => root.unmount());
|
|
341
|
+
});
|
|
341
342
|
});
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { scopedElementKey } from "../../hooks/gsapKeyframeCacheHelpers";
|
|
2
|
-
import { useState } from "react";
|
|
3
2
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
3
|
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
5
4
|
import type { DomEditSelection } from "./domEditing";
|
|
6
5
|
import { formatTimingValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
|
|
7
6
|
import { parseTimingValue } from "./propertyPanelTimingSection";
|
|
8
7
|
import { CommitField } from "./propertyPanelPrimitives";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
type GsapAnimationEditCallbacks,
|
|
12
|
-
withTrackedGsapAnimationCallbacks,
|
|
13
|
-
clearFocusedEaseSegment,
|
|
14
|
-
} from "./gsapAnimationCallbacks";
|
|
8
|
+
import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks";
|
|
15
9
|
import { deriveElementTiming } from "./propertyPanelFlatTimingDerivation";
|
|
16
|
-
import {
|
|
17
|
-
import { GsapAddAnimationControl } from "./GsapAddAnimationControl";
|
|
10
|
+
import { GsapAnimationList } from "./GsapAnimationList";
|
|
18
11
|
|
|
19
12
|
export function FlatTimingRow({
|
|
20
13
|
element,
|
|
@@ -136,19 +129,11 @@ export function FlatMotionSection({
|
|
|
136
129
|
onSetAttributes?: (selection: DomEditSelection, attrs: Record<string, string>) => Promise<void>;
|
|
137
130
|
onAddAnimation: (method: "to" | "from" | "set" | "fromTo") => void;
|
|
138
131
|
} & GsapAnimationEditCallbacks) {
|
|
139
|
-
const track = useTrackDesignInput();
|
|
140
|
-
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
|
141
|
-
const trackedCallbacks = withTrackedGsapAnimationCallbacks(callbacks, track);
|
|
142
|
-
const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
|
|
143
132
|
// Only consume a focus request aimed at the element THIS panel renders (not
|
|
144
133
|
// the store's selectedElementId, which flips synchronously during async
|
|
145
134
|
// selection resolution), so a shared class-selector animation id can't open
|
|
146
135
|
// the wrong element's editor.
|
|
147
136
|
const renderedElementId = scopedElementKey(element);
|
|
148
|
-
const focusedHere =
|
|
149
|
-
focusedEaseSegment && focusedEaseSegment.elementId === renderedElementId
|
|
150
|
-
? focusedEaseSegment
|
|
151
|
-
: null;
|
|
152
137
|
|
|
153
138
|
return (
|
|
154
139
|
<div className="space-y-3">
|
|
@@ -174,26 +159,13 @@ export function FlatMotionSection({
|
|
|
174
159
|
</p>
|
|
175
160
|
)}
|
|
176
161
|
{!multipleTimelines && !unsupportedTimelinePattern && (
|
|
177
|
-
<
|
|
178
|
-
{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
flat
|
|
185
|
-
focusedSegment={focusedHere?.animationId === anim.id ? focusedHere : null}
|
|
186
|
-
onFocusSegmentConsumed={clearFocusedEaseSegment}
|
|
187
|
-
/>
|
|
188
|
-
))}
|
|
189
|
-
<GsapAddAnimationControl
|
|
190
|
-
open={addMenuOpen}
|
|
191
|
-
setOpen={setAddMenuOpen}
|
|
192
|
-
onAddAnimation={onAddAnimation}
|
|
193
|
-
track={track}
|
|
194
|
-
variant="flat"
|
|
195
|
-
/>
|
|
196
|
-
</div>
|
|
162
|
+
<GsapAnimationList
|
|
163
|
+
{...callbacks}
|
|
164
|
+
elementId={renderedElementId}
|
|
165
|
+
animations={animations}
|
|
166
|
+
onAddAnimation={onAddAnimation}
|
|
167
|
+
variant="flat"
|
|
168
|
+
/>
|
|
197
169
|
)}
|
|
198
170
|
</>
|
|
199
171
|
)}
|