@hyperframes/studio 0.8.53 → 0.8.55
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-DKWM-Ons.js → hyperframes-player-BjAGc2ID.js} +1 -1
- package/dist/assets/{index-Y4OnpTEM.js → index-BEIRytvV.js} +1 -1
- package/dist/assets/{index-UvW6BEwf.js → index-DaNpNFJC.js} +1 -1
- package/dist/assets/{index-HfP3sap2.js → index-TcBZyzNA.js} +191 -191
- package/dist/index.d.ts +440 -10
- package/dist/index.html +1 -1
- package/dist/index.js +1891 -1544
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/hooks/gsapDragCommit.ts +1 -1
- package/src/hooks/gsapEditOutcome.test.ts +115 -0
- package/src/hooks/gsapEditOutcome.ts +39 -10
- package/src/hooks/gsapResizeIntercept.test.ts +42 -3
- package/src/hooks/gsapResizeIntercept.ts +12 -2
- package/src/hooks/gsapRuntimeBridge.test.ts +10 -2
- package/src/hooks/gsapRuntimeBridge.ts +14 -3
- package/src/hooks/timelineAudioGroupCreate.ts +17 -1
- package/src/hooks/timelineAudioGroupVolume.ts +35 -2
- package/src/hooks/timelineEditPermission.ts +31 -0
- package/src/hooks/timelineElementFxAttribute.ts +34 -2
- package/src/hooks/useFileManager.ts +21 -123
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/useProjectFileWriter.ts +150 -0
- package/src/hooks/useTimelineAssetDropOps.test.tsx +62 -4
- package/src/hooks/useTimelineAssetDropOps.ts +40 -3
- package/src/hooks/useTimelineDeleteOps.test.tsx +111 -0
- package/src/hooks/useTimelineEditing.test.tsx +481 -4
- package/src/hooks/useTimelineEditing.ts +151 -27
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTrackPendingTimelineEdit.test.tsx +62 -0
- package/src/hooks/useTrackPendingTimelineEdit.ts +22 -0
- package/src/hostMount.test.tsx +222 -0
- package/src/index.ts +40 -2
- package/src/player/components/Timeline.tsx +6 -6
- package/src/player/components/TimelineTypes.ts +3 -0
- package/src/player/components/usePublishRangeSelection.test.tsx +60 -0
- package/src/player/components/usePublishRangeSelection.ts +42 -0
- package/src/player/index.ts +1 -0
- package/src/player/store/playerStore.ts +8 -51
- package/src/player/store/rangeSelectionSlice.ts +13 -0
- package/src/player/store/timelineResetState.ts +51 -0
- package/src/utils/studioSaveDiagnostics.ts +12 -0
|
@@ -4,6 +4,7 @@ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
|
4
4
|
import { useTimelineRowElements } from "../hooks/useTimelineRowElements";
|
|
5
5
|
import { defaultTimelineTheme } from "./timelineTheme";
|
|
6
6
|
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
7
|
+
import { usePublishRangeSelection } from "./usePublishRangeSelection";
|
|
7
8
|
import { useTimelinePlayhead } from "./useTimelinePlayhead";
|
|
8
9
|
import { useTimelineZoom } from "./useTimelineZoom";
|
|
9
10
|
import { useTimelineAssetDrop } from "./timelineDragDrop";
|
|
@@ -80,6 +81,7 @@ export const Timeline = memo(function Timeline({
|
|
|
80
81
|
onBlockedEditAttempt: onBlockedEditAttemptOverride,
|
|
81
82
|
onSplitElement: onSplitElementOverride,
|
|
82
83
|
onSelectElement,
|
|
84
|
+
onRangeSelect,
|
|
83
85
|
onCopyClip,
|
|
84
86
|
onPasteClip,
|
|
85
87
|
onDuplicateClip,
|
|
@@ -167,10 +169,10 @@ export const Timeline = memo(function Timeline({
|
|
|
167
169
|
selectedElementIds,
|
|
168
170
|
);
|
|
169
171
|
const expandedElementsRef = useRef(expandedElements);
|
|
170
|
-
expandedElementsRef.current = expandedElements;
|
|
172
|
+
expandedElementsRef.current = expandedElements; // oxlint-disable-line react/refs -- event handlers read the latest elements
|
|
171
173
|
const ppsRef = useRef(100);
|
|
172
174
|
const durationRef = useRef(effectiveDuration);
|
|
173
|
-
durationRef.current = effectiveDuration;
|
|
175
|
+
durationRef.current = effectiveDuration; // oxlint-disable-line react/refs -- event handlers read the latest duration
|
|
174
176
|
const fitPpsRef = useRef(100);
|
|
175
177
|
const {
|
|
176
178
|
pinZoomBeforeEdit,
|
|
@@ -214,7 +216,6 @@ export const Timeline = memo(function Timeline({
|
|
|
214
216
|
onMoveElements: pinnedOnMoveElements,
|
|
215
217
|
});
|
|
216
218
|
const onContextMenuClip = useClipContextMenu(onSelectElement, dismissGapMenu, setClipContextMenu);
|
|
217
|
-
|
|
218
219
|
const {
|
|
219
220
|
draggedClip,
|
|
220
221
|
setDraggedClip,
|
|
@@ -241,7 +242,6 @@ export const Timeline = memo(function Timeline({
|
|
|
241
242
|
refreshAfterLaneMove,
|
|
242
243
|
sessionEpoch,
|
|
243
244
|
});
|
|
244
|
-
|
|
245
245
|
const assetDrop = useTimelineAssetDrop({
|
|
246
246
|
scrollRef,
|
|
247
247
|
ppsRef,
|
|
@@ -320,7 +320,6 @@ export const Timeline = memo(function Timeline({
|
|
|
320
320
|
setKfContextMenu,
|
|
321
321
|
toggleSelectedKeyframe,
|
|
322
322
|
});
|
|
323
|
-
|
|
324
323
|
const { clipIndex, renderTimeRange, visibleTimeRange, pinnedClipIdentities } =
|
|
325
324
|
useTimelineClipRenderWindow({
|
|
326
325
|
tracks,
|
|
@@ -415,7 +414,8 @@ export const Timeline = memo(function Timeline({
|
|
|
415
414
|
contentOrigin,
|
|
416
415
|
sessionEpoch,
|
|
417
416
|
});
|
|
418
|
-
|
|
417
|
+
usePublishRangeSelection(rangeSelection, onRangeSelect);
|
|
418
|
+
setRangeSelectionRef.current = setRangeSelection; // oxlint-disable-line react/refs -- stable ref consumed by useTimelineClipDrag
|
|
419
419
|
|
|
420
420
|
useTimelineSelectionLifecycle(expandedElements, selectedElementId, setShowPopover, () =>
|
|
421
421
|
setRangeSelection(null),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { TimelineElement } from "../store/playerStore";
|
|
3
|
+
import type { TimelineTimeRange } from "../store/rangeSelectionSlice";
|
|
3
4
|
import type { TimelineDropCallbacks } from "./timelineCallbacks";
|
|
4
5
|
import type { TimelineTheme } from "./timelineTheme";
|
|
5
6
|
import type { TimelineEditOverrides } from "./useResolvedTimelineEditCallbacks";
|
|
@@ -23,6 +24,8 @@ export interface TimelineProps extends TimelineDropCallbacks, TimelineEditOverri
|
|
|
23
24
|
renderClipOverlay?: (element: TimelineElement) => ReactNode;
|
|
24
25
|
onDeleteElement?: (element: TimelineElement) => Promise<void> | void;
|
|
25
26
|
onSelectElement?: (element: TimelineElement | null) => void;
|
|
27
|
+
/** Notification only; null when cleared. The value lives in usePlayerStore.rangeSelection. */
|
|
28
|
+
onRangeSelect?: (range: TimelineTimeRange | null) => void;
|
|
26
29
|
onCopyClip?: () => boolean;
|
|
27
30
|
onPasteClip?: () => Promise<void>;
|
|
28
31
|
onDuplicateClip?: () => Promise<boolean>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { usePlayerStore } from "../store/playerStore";
|
|
6
|
+
import type { TimelineTimeRange } from "../store/rangeSelectionSlice";
|
|
7
|
+
import type { TimelineRangeSelection } from "./timelineEditing";
|
|
8
|
+
import { usePublishRangeSelection } from "./usePublishRangeSelection";
|
|
9
|
+
|
|
10
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
|
|
12
|
+
function mount(onRangeSelect: (range: TimelineTimeRange | null) => void) {
|
|
13
|
+
const host = document.createElement("div");
|
|
14
|
+
const root = createRoot(host);
|
|
15
|
+
const Harness = ({ selection }: { selection: TimelineRangeSelection | null }) => {
|
|
16
|
+
usePublishRangeSelection(selection, onRangeSelect);
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
const render = (selection: TimelineRangeSelection | null) =>
|
|
20
|
+
act(() => root.render(<Harness selection={selection} />));
|
|
21
|
+
return { render, unmount: () => act(() => root.unmount()) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const selection = (start: number, end: number): TimelineRangeSelection => ({
|
|
25
|
+
start,
|
|
26
|
+
end,
|
|
27
|
+
anchorX: 0,
|
|
28
|
+
anchorY: 0,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("usePublishRangeSelection", () => {
|
|
32
|
+
afterEach(() => usePlayerStore.setState({ rangeSelection: null }));
|
|
33
|
+
|
|
34
|
+
it("publishes an ordered range to the store and the host, and clears with null", () => {
|
|
35
|
+
const onRangeSelect = vi.fn();
|
|
36
|
+
const view = mount(onRangeSelect);
|
|
37
|
+
|
|
38
|
+
view.render(selection(3, 1));
|
|
39
|
+
expect(usePlayerStore.getState().rangeSelection).toEqual({ t0: 1, t1: 3 });
|
|
40
|
+
expect(onRangeSelect).toHaveBeenLastCalledWith({ t0: 1, t1: 3 });
|
|
41
|
+
|
|
42
|
+
view.render(null);
|
|
43
|
+
expect(usePlayerStore.getState().rangeSelection).toBeNull();
|
|
44
|
+
expect(onRangeSelect).toHaveBeenLastCalledWith(null);
|
|
45
|
+
view.unmount();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("does not re-notify for an unchanged range and preserves a newer store value on unmount", () => {
|
|
49
|
+
const onRangeSelect = vi.fn();
|
|
50
|
+
const view = mount(onRangeSelect);
|
|
51
|
+
|
|
52
|
+
view.render(selection(1, 3));
|
|
53
|
+
view.render(selection(3, 1));
|
|
54
|
+
expect(onRangeSelect).toHaveBeenCalledTimes(1);
|
|
55
|
+
|
|
56
|
+
usePlayerStore.setState({ rangeSelection: { t0: 9, t1: 10 } });
|
|
57
|
+
view.unmount();
|
|
58
|
+
expect(usePlayerStore.getState().rangeSelection).toEqual({ t0: 9, t1: 10 });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { usePlayerStore } from "../store/playerStore";
|
|
3
|
+
import type { TimelineTimeRange } from "../store/rangeSelectionSlice";
|
|
4
|
+
import type { TimelineRangeSelection } from "./timelineEditing";
|
|
5
|
+
|
|
6
|
+
/** Mirrors the gesture's range into the store and notifies the host. */
|
|
7
|
+
export function usePublishRangeSelection(
|
|
8
|
+
rangeSelection: TimelineRangeSelection | null,
|
|
9
|
+
onRangeSelect?: (range: TimelineTimeRange | null) => void,
|
|
10
|
+
) {
|
|
11
|
+
const onRangeSelectRef = useRef(onRangeSelect);
|
|
12
|
+
const publishedRangeRef = useRef<TimelineTimeRange | null>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
onRangeSelectRef.current = onRangeSelect;
|
|
16
|
+
}, [onRangeSelect]);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const range = rangeSelection
|
|
20
|
+
? {
|
|
21
|
+
t0: Math.min(rangeSelection.start, rangeSelection.end),
|
|
22
|
+
t1: Math.max(rangeSelection.start, rangeSelection.end),
|
|
23
|
+
}
|
|
24
|
+
: null;
|
|
25
|
+
const store = usePlayerStore.getState();
|
|
26
|
+
const current = store.rangeSelection;
|
|
27
|
+
if (current?.t0 === range?.t0 && current?.t1 === range?.t1) return;
|
|
28
|
+
publishedRangeRef.current = range;
|
|
29
|
+
usePlayerStore.setState({ rangeSelection: range });
|
|
30
|
+
onRangeSelectRef.current?.(range);
|
|
31
|
+
}, [rangeSelection]);
|
|
32
|
+
|
|
33
|
+
useEffect(
|
|
34
|
+
() => () => {
|
|
35
|
+
const current = usePlayerStore.getState().rangeSelection;
|
|
36
|
+
const published = publishedRangeRef.current;
|
|
37
|
+
if (current?.t0 !== published?.t0 || current?.t1 !== published?.t1) return;
|
|
38
|
+
usePlayerStore.setState({ rangeSelection: null });
|
|
39
|
+
},
|
|
40
|
+
[],
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/player/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { resolveIframe } from "./lib/timelineDOM";
|
|
|
12
12
|
// Store
|
|
13
13
|
export { usePlayerStore, liveTime } from "./store/playerStore";
|
|
14
14
|
export type { SelectElementOptions, TimelineElement, ZoomMode } from "./store/playerStore";
|
|
15
|
+
export type { TimelineTimeRange } from "./store/rangeSelectionSlice";
|
|
15
16
|
|
|
16
17
|
// Utils
|
|
17
18
|
export { formatTime } from "./lib/time";
|
|
@@ -2,7 +2,6 @@ import { create } from "zustand";
|
|
|
2
2
|
import { attachPlayerStoreDevHandle } from "./playerStoreDevHandle";
|
|
3
3
|
import { nextSelectionSet, revealTargetsSelection } from "./playerStoreSelection";
|
|
4
4
|
import type { MusicBeatAnalysis } from "@hyperframes/core/beats";
|
|
5
|
-
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
5
|
import type { BeatEditState } from "../../utils/beatEditing";
|
|
7
6
|
import type { ClipManifestClip } from "../lib/playbackTypes";
|
|
8
7
|
import {
|
|
@@ -11,7 +10,7 @@ import {
|
|
|
11
10
|
type TimelineTimeDisplayMode,
|
|
12
11
|
} from "../../utils/studioUiPreferences";
|
|
13
12
|
import { clampTimelineZoomPercent, computePinnedZoomPercent } from "../components/timelineZoom";
|
|
14
|
-
import { createKeyframeSlice, type
|
|
13
|
+
import { createKeyframeSlice, type KeyframeSlice } from "./keyframeSlice";
|
|
15
14
|
import {
|
|
16
15
|
createAutomationSelectionSlice,
|
|
17
16
|
type AutomationSelectionSlice,
|
|
@@ -19,10 +18,12 @@ import {
|
|
|
19
18
|
import { createEditingModeSlice, type EditingModeSlice } from "./editingModeSlice";
|
|
20
19
|
import { createTimelineFocusRequest, type TimelineFocusRequest } from "./timelineFocusState";
|
|
21
20
|
import { createThumbnailSlice, type ThumbnailSlice } from "./thumbnailSlice";
|
|
22
|
-
import { createPlaybackReadinessSlice
|
|
23
|
-
|
|
21
|
+
import { createPlaybackReadinessSlice } from "./readinessSlice";
|
|
22
|
+
import { createRangeSelectionSlice, type RangeSelectionSlice } from "./rangeSelectionSlice";
|
|
23
|
+
import { createTimelineResetState } from "./timelineResetState";
|
|
24
24
|
export type { KeyframeCacheEntry } from "./keyframeSlice";
|
|
25
25
|
export { liveTime } from "./liveTime";
|
|
26
|
+
export { createTimelineResetState };
|
|
26
27
|
|
|
27
28
|
import type {
|
|
28
29
|
TimelineElement,
|
|
@@ -59,7 +60,8 @@ type PlayerStoreSlices = KeyframeSlice &
|
|
|
59
60
|
AutomationSelectionSlice &
|
|
60
61
|
ThumbnailSlice &
|
|
61
62
|
EditingModeSlice &
|
|
62
|
-
ReturnType<typeof createPlaybackReadinessSlice
|
|
63
|
+
ReturnType<typeof createPlaybackReadinessSlice> &
|
|
64
|
+
RangeSelectionSlice;
|
|
63
65
|
interface PlayerState extends PlayerStoreSlices {
|
|
64
66
|
isPlaying: boolean;
|
|
65
67
|
currentTime: number;
|
|
@@ -250,52 +252,6 @@ interface BeatHistoryEntry {
|
|
|
250
252
|
label: string;
|
|
251
253
|
}
|
|
252
254
|
|
|
253
|
-
export function createTimelineResetState() {
|
|
254
|
-
return {
|
|
255
|
-
isPlaying: false,
|
|
256
|
-
currentTime: 0,
|
|
257
|
-
duration: 0,
|
|
258
|
-
...resetPlaybackReadinessState(),
|
|
259
|
-
beatDragging: false,
|
|
260
|
-
elements: [],
|
|
261
|
-
selectedElementId: null,
|
|
262
|
-
zEditVersion: 0,
|
|
263
|
-
inPoint: null,
|
|
264
|
-
outPoint: null,
|
|
265
|
-
activeTool: "select" as const,
|
|
266
|
-
activeKeyframePct: null,
|
|
267
|
-
motionPathArmed: false,
|
|
268
|
-
motionPathCreateAvailable: false,
|
|
269
|
-
selectedKeyframes: new Set<string>(),
|
|
270
|
-
// Ephemeral like every other selection here. A range surviving a project
|
|
271
|
-
// switch can match a same-keyed clip in the new project and redirect a
|
|
272
|
-
// paste through `sel.elementKey === paste.elementKey` to a stale t0.
|
|
273
|
-
automationSelection: null,
|
|
274
|
-
expandedClipIds: new Set<string>(),
|
|
275
|
-
// Per-composition: ids from comp A match nothing in B, silencing all of it.
|
|
276
|
-
collapsedGroupIds: new Set<string>(),
|
|
277
|
-
expandedLaneOwnerIds: new Set<string>(),
|
|
278
|
-
focusedEaseSegment: null,
|
|
279
|
-
revealedAudioFxTarget: null,
|
|
280
|
-
selectedElementIds: new Set<string>(),
|
|
281
|
-
requestedSeekTime: null,
|
|
282
|
-
lintFindingsByElement: new Map<string, { count: number; messages: string[] }>(),
|
|
283
|
-
timelineFocus: null,
|
|
284
|
-
keyframeCache: new Map<string, KeyframeCacheEntry>(),
|
|
285
|
-
gsapAnimations: new Map<string, GsapAnimation[]>(),
|
|
286
|
-
beatAnalysis: null,
|
|
287
|
-
beatEdits: null,
|
|
288
|
-
beatUndo: [],
|
|
289
|
-
beatRedo: [],
|
|
290
|
-
beatPersist: null,
|
|
291
|
-
clipManifest: null,
|
|
292
|
-
clipParentMap: new Map<string, string>(),
|
|
293
|
-
topLevelIds: null,
|
|
294
|
-
domClipChildren: [],
|
|
295
|
-
subCompositionHostState: new Map<string, SubCompositionHostState>(),
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
255
|
export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
300
256
|
isPlaying: false,
|
|
301
257
|
currentTime: 0,
|
|
@@ -328,6 +284,7 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
328
284
|
|
|
329
285
|
...createAutomationSelectionSlice(set),
|
|
330
286
|
...createEditingModeSlice(set),
|
|
287
|
+
...createRangeSelectionSlice(),
|
|
331
288
|
...createPlaybackReadinessSlice(set),
|
|
332
289
|
|
|
333
290
|
activeKeyframePct: null,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface TimelineTimeRange {
|
|
2
|
+
t0: number;
|
|
3
|
+
t1: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface RangeSelectionSlice {
|
|
7
|
+
/** Shift-drag ruler range in seconds, normalized so t0 <= t1. */
|
|
8
|
+
rangeSelection: TimelineTimeRange | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createRangeSelectionSlice(): RangeSelectionSlice {
|
|
12
|
+
return { rangeSelection: null };
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
|
+
import type { KeyframeCacheEntry } from "./keyframeSlice";
|
|
3
|
+
import { resetPlaybackReadinessState } from "./readinessSlice";
|
|
4
|
+
import type { SubCompositionHostState } from "./timelineElement";
|
|
5
|
+
|
|
6
|
+
export function createTimelineResetState() {
|
|
7
|
+
return {
|
|
8
|
+
isPlaying: false,
|
|
9
|
+
currentTime: 0,
|
|
10
|
+
duration: 0,
|
|
11
|
+
...resetPlaybackReadinessState(),
|
|
12
|
+
beatDragging: false,
|
|
13
|
+
elements: [],
|
|
14
|
+
selectedElementId: null,
|
|
15
|
+
zEditVersion: 0,
|
|
16
|
+
inPoint: null,
|
|
17
|
+
outPoint: null,
|
|
18
|
+
rangeSelection: null,
|
|
19
|
+
activeTool: "select" as const,
|
|
20
|
+
activeKeyframePct: null,
|
|
21
|
+
motionPathArmed: false,
|
|
22
|
+
motionPathCreateAvailable: false,
|
|
23
|
+
selectedKeyframes: new Set<string>(),
|
|
24
|
+
// Ephemeral like every other selection here. A range surviving a project
|
|
25
|
+
// switch can match a same-keyed clip in the new project and redirect a
|
|
26
|
+
// paste through `sel.elementKey === paste.elementKey` to a stale t0.
|
|
27
|
+
automationSelection: null,
|
|
28
|
+
expandedClipIds: new Set<string>(),
|
|
29
|
+
// Per-composition: ids from comp A match nothing in B, silencing all of it.
|
|
30
|
+
collapsedGroupIds: new Set<string>(),
|
|
31
|
+
expandedLaneOwnerIds: new Set<string>(),
|
|
32
|
+
focusedEaseSegment: null,
|
|
33
|
+
revealedAudioFxTarget: null,
|
|
34
|
+
selectedElementIds: new Set<string>(),
|
|
35
|
+
requestedSeekTime: null,
|
|
36
|
+
lintFindingsByElement: new Map<string, { count: number; messages: string[] }>(),
|
|
37
|
+
timelineFocus: null,
|
|
38
|
+
keyframeCache: new Map<string, KeyframeCacheEntry>(),
|
|
39
|
+
gsapAnimations: new Map<string, GsapAnimation[]>(),
|
|
40
|
+
beatAnalysis: null,
|
|
41
|
+
beatEdits: null,
|
|
42
|
+
beatUndo: [],
|
|
43
|
+
beatRedo: [],
|
|
44
|
+
beatPersist: null,
|
|
45
|
+
clipManifest: null,
|
|
46
|
+
clipParentMap: new Map<string, string>(),
|
|
47
|
+
topLevelIds: null,
|
|
48
|
+
domClipChildren: [],
|
|
49
|
+
subCompositionHostState: new Map<string, SubCompositionHostState>(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -165,9 +165,21 @@ export function buildStudioSaveFailureProperties(
|
|
|
165
165
|
target_id: input.targetId ?? undefined,
|
|
166
166
|
target_selector: input.targetSelector ?? undefined,
|
|
167
167
|
target_source_file: input.targetSourceFile ?? undefined,
|
|
168
|
+
block_detail: gsapBlockDetail(input.error),
|
|
168
169
|
};
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
/**
|
|
173
|
+
* The specific cause behind a blocked GSAP edit. `error_message` only carries
|
|
174
|
+
* the coarse copy, and one of those strings covers six different situations,
|
|
175
|
+
* so the message alone cannot say which one a user hit.
|
|
176
|
+
*/
|
|
177
|
+
function gsapBlockDetail(error: unknown): string | undefined {
|
|
178
|
+
if (!(error instanceof Error) || error.name !== "GsapEditBlockedError") return undefined;
|
|
179
|
+
const detail = (error as { detail?: unknown }).detail;
|
|
180
|
+
return typeof detail === "string" ? detail : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
171
183
|
export function trackStudioSaveFailure(input: StudioSaveFailureInput): void {
|
|
172
184
|
trackStudioEvent("save_failure", buildStudioSaveFailureProperties(input));
|
|
173
185
|
}
|