@hyperframes/studio 0.7.46 → 0.7.47
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/index-Dq7FEg0K.css +1 -0
- package/dist/assets/{index-BGVoOXdg.js → index-DsckwbdW.js} +1 -1
- package/dist/assets/index-VsAbY3rl.js +423 -0
- package/dist/assets/{index-DO2MAFSG.js → index-uvB30_yI.js} +1 -1
- package/dist/index.d.ts +96 -29
- package/dist/index.html +2 -2
- package/dist/index.js +8443 -4305
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -32
- package/src/components/DesignPanelPromoteProvider.tsx +45 -0
- package/src/components/PanelTabButton.tsx +31 -0
- package/src/components/StudioPreviewArea.tsx +75 -3
- package/src/components/StudioRightPanel.tsx +111 -113
- package/src/components/editor/LayersPanel.tsx +2 -22
- package/src/components/editor/PromotableControl.tsx +105 -0
- package/src/components/editor/propertyPanelSections.tsx +42 -21
- package/src/components/panels/VariablesBindElement.tsx +266 -0
- package/src/components/panels/VariablesDeclarationForm.test.ts +135 -0
- package/src/components/panels/VariablesDeclarationForm.tsx +321 -0
- package/src/components/panels/VariablesOtherCompositions.tsx +159 -0
- package/src/components/panels/VariablesPanel.tsx +554 -0
- package/src/components/panels/VariablesRowAction.tsx +25 -0
- package/src/components/panels/VariablesValueControls.tsx +215 -0
- package/src/components/renders/useRenderQueue.ts +12 -0
- package/src/contexts/TimelineEditContext.tsx +7 -2
- package/src/contexts/VariablePromoteContext.tsx +138 -0
- package/src/contexts/variablePromoteHelpers.test.ts +124 -0
- package/src/contexts/variablePromoteHelpers.ts +53 -0
- package/src/contexts/variablePromoteIntegration.test.ts +80 -0
- package/src/hooks/previewVariablesStore.ts +34 -0
- package/src/hooks/timelineEditingHelpers.test.ts +104 -0
- package/src/hooks/timelineEditingHelpers.ts +386 -10
- package/src/hooks/useDomEditWiring.ts +9 -7
- package/src/hooks/useDomSelection.test.ts +36 -2
- package/src/hooks/useDomSelection.ts +56 -75
- package/src/hooks/useElementLifecycleOps.ts +57 -17
- package/src/hooks/usePreviewDocumentVersion.ts +27 -0
- package/src/hooks/useProjectCompositionVariables.ts +131 -0
- package/src/hooks/useStudioContextValue.ts +6 -1
- package/src/hooks/useStudioSdkSessions.ts +37 -0
- package/src/hooks/useTimelineEditing.test.tsx +963 -0
- package/src/hooks/useTimelineEditing.ts +122 -100
- package/src/hooks/useTimelineEditingTypes.ts +15 -0
- package/src/hooks/useTimelineGroupEditing.ts +370 -0
- package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +129 -0
- package/src/hooks/useTimelineSelectionPreviewSync.ts +130 -0
- package/src/hooks/useVariablesPersist.ts +61 -0
- package/src/player/components/BeatStrip.tsx +19 -3
- package/src/player/components/Player.tsx +7 -1
- package/src/player/components/Timeline.test.ts +38 -0
- package/src/player/components/Timeline.tsx +102 -80
- package/src/player/components/TimelineCanvas.tsx +340 -296
- package/src/player/components/TimelineClip.test.tsx +11 -0
- package/src/player/components/TimelineClipDiamonds.tsx +40 -9
- package/src/player/components/TimelineDragGhost.tsx +59 -0
- package/src/player/components/TimelineDropInsertionLine.tsx +32 -0
- package/src/player/components/TimelineLayerGroupHeader.tsx +97 -0
- package/src/player/components/TimelineLayerGutter.tsx +61 -0
- package/src/player/components/TimelineRuler.tsx +4 -2
- package/src/player/components/TimelineSelectionOverlays.tsx +55 -0
- package/src/player/components/timelineCallbacks.ts +20 -2
- package/src/player/components/timelineClipDragPreview.ts +52 -0
- package/src/player/components/timelineDragDrop.ts +19 -3
- package/src/player/components/timelineDropIndicator.test.ts +48 -0
- package/src/player/components/timelineDropIndicator.ts +33 -0
- package/src/player/components/timelineEditing.test.ts +251 -1
- package/src/player/components/timelineEditing.ts +178 -19
- package/src/player/components/timelineGroupEditing.ts +159 -0
- package/src/player/components/timelineLayerDrag.test.ts +200 -0
- package/src/player/components/timelineLayerDrag.ts +383 -0
- package/src/player/components/timelineLayout.test.ts +34 -0
- package/src/player/components/timelineLayout.ts +47 -0
- package/src/player/components/timelineMarqueeSelection.test.ts +87 -0
- package/src/player/components/timelineSnapTargets.test.ts +144 -0
- package/src/player/components/timelineSnapTargets.ts +164 -0
- package/src/player/components/timelineStacking.ts +62 -0
- package/src/player/components/timelineTrackOrder.test.ts +144 -0
- package/src/player/components/timelineTrackOrder.ts +173 -0
- package/src/player/components/useTimelineActiveClips.test.ts +20 -0
- package/src/player/components/useTimelineActiveClips.ts +15 -4
- package/src/player/components/useTimelineClipDrag.test.tsx +506 -0
- package/src/player/components/useTimelineClipDrag.ts +172 -126
- package/src/player/components/useTimelineClipGroupDrag.ts +417 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +73 -0
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +231 -0
- package/src/player/components/useTimelineMarqueeSelection.ts +276 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +4 -1
- package/src/player/hooks/useTimelinePlayer.ts +7 -0
- package/src/player/lib/layerOrdering.test.ts +77 -0
- package/src/player/lib/layerOrdering.ts +98 -0
- package/src/player/lib/playbackTypes.ts +3 -0
- package/src/player/lib/timelineDOM.test.ts +103 -0
- package/src/player/lib/timelineDOM.ts +80 -0
- package/src/player/lib/timelineElementHelpers.ts +1 -1
- package/src/player/store/playerStore.test.ts +92 -0
- package/src/player/store/playerStore.ts +69 -9
- package/src/utils/blockInstaller.ts +2 -14
- package/src/utils/editHistory.test.ts +61 -0
- package/src/utils/editHistory.ts +7 -1
- package/src/utils/rootDuration.test.ts +34 -0
- package/src/utils/rootDuration.ts +17 -0
- package/src/utils/sdkCutover.ts +45 -0
- package/src/utils/studioHelpers.test.ts +31 -0
- package/src/utils/studioHelpers.ts +29 -1
- package/src/utils/studioUrlState.test.ts +29 -0
- package/src/utils/studioUrlState.ts +16 -1
- package/dist/assets/index-BsLQ2qGn.js +0 -420
- package/dist/assets/index-DoVLXke0.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.47",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"gsap": "^3.13.0",
|
|
47
47
|
"marked": "^14.1.4",
|
|
48
48
|
"mediabunny": "^1.45.3",
|
|
49
|
-
"@hyperframes/
|
|
50
|
-
"@hyperframes/
|
|
51
|
-
"@hyperframes/
|
|
52
|
-
"@hyperframes/sdk": "0.7.
|
|
53
|
-
"@hyperframes/
|
|
49
|
+
"@hyperframes/player": "0.7.47",
|
|
50
|
+
"@hyperframes/core": "0.7.47",
|
|
51
|
+
"@hyperframes/studio-server": "0.7.47",
|
|
52
|
+
"@hyperframes/sdk": "0.7.47",
|
|
53
|
+
"@hyperframes/parsers": "0.7.47"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/react": "19",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"vite": "^6.4.2",
|
|
66
66
|
"vitest": "^3.2.4",
|
|
67
67
|
"zustand": "^5.0.0",
|
|
68
|
-
"@hyperframes/producer": "0.7.
|
|
68
|
+
"@hyperframes/producer": "0.7.47"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -11,15 +11,17 @@ import { usePanelLayout } from "./hooks/usePanelLayout";
|
|
|
11
11
|
import { useFileManager } from "./hooks/useFileManager";
|
|
12
12
|
import { usePreviewPersistence } from "./hooks/usePreviewPersistence";
|
|
13
13
|
import { useTimelineEditing } from "./hooks/useTimelineEditing";
|
|
14
|
+
import type { TimelineZIndexReorderCommit } from "./hooks/useTimelineEditingTypes";
|
|
14
15
|
import type { BlockPreviewInfo } from "./components/sidebar/BlocksTab";
|
|
15
16
|
import { useDomEditSession } from "./hooks/useDomEditSession";
|
|
16
|
-
import { useSdkSession } from "./hooks/useSdkSession";
|
|
17
17
|
import { useSdkSelectionSync } from "./hooks/useSdkSelectionSync";
|
|
18
|
+
import { useStudioSdkSessions } from "./hooks/useStudioSdkSessions";
|
|
19
|
+
import { usePreviewDocumentVersion } from "./hooks/usePreviewDocumentVersion";
|
|
18
20
|
import { useBlockHandlers } from "./hooks/useBlockHandlers";
|
|
19
21
|
import { useAppHotkeys } from "./hooks/useAppHotkeys";
|
|
20
22
|
import { useClipboard } from "./hooks/useClipboard";
|
|
21
23
|
import { readStudioUiPreferences, writeStudioUiPreferences } from "./utils/studioUiPreferences";
|
|
22
|
-
import {
|
|
24
|
+
import { deleteSelectedKeyframes } from "./hooks/timelineEditingHelpers";
|
|
23
25
|
import { useCaptionDetection } from "./hooks/useCaptionDetection";
|
|
24
26
|
import { useRenderClipContent } from "./hooks/useRenderClipContent";
|
|
25
27
|
import { useConsoleErrorCapture } from "./hooks/useConsoleErrorCapture";
|
|
@@ -54,6 +56,7 @@ import { useServerConnection } from "./hooks/useServerConnection";
|
|
|
54
56
|
import {
|
|
55
57
|
normalizeStudioCompositionPath,
|
|
56
58
|
readStudioUrlStateFromWindow,
|
|
59
|
+
resolveMasterCompositionPath,
|
|
57
60
|
} from "./utils/studioUrlState";
|
|
58
61
|
import { trackStudioSessionStart } from "./telemetry/events";
|
|
59
62
|
import { hasFiredSessionStart, markSessionStartFired } from "./telemetry/config";
|
|
@@ -80,7 +83,7 @@ export function StudioApp() {
|
|
|
80
83
|
const [previewIframe, setPreviewIframe] = useState<HTMLIFrameElement | null>(null);
|
|
81
84
|
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
82
85
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
83
|
-
const [previewDocumentVersion,
|
|
86
|
+
const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
|
|
84
87
|
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
|
|
85
88
|
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
86
89
|
const activeCompPathRef = useRef(activeCompPath);
|
|
@@ -105,22 +108,6 @@ export function StudioApp() {
|
|
|
105
108
|
: 0;
|
|
106
109
|
return Math.max(timelineDuration, maxEnd);
|
|
107
110
|
}, [timelineDuration, timelineElements]);
|
|
108
|
-
const refreshTimersRef = useRef<number[]>([]);
|
|
109
|
-
const refreshPreviewDocumentVersion = useCallback(() => {
|
|
110
|
-
for (const id of refreshTimersRef.current) clearTimeout(id);
|
|
111
|
-
refreshTimersRef.current = [];
|
|
112
|
-
setPreviewDocumentVersion((v) => v + 1);
|
|
113
|
-
refreshTimersRef.current.push(
|
|
114
|
-
window.setTimeout(() => setPreviewDocumentVersion((v) => v + 1), 80),
|
|
115
|
-
window.setTimeout(() => setPreviewDocumentVersion((v) => v + 1), 300),
|
|
116
|
-
);
|
|
117
|
-
}, []);
|
|
118
|
-
useEffect(
|
|
119
|
-
() => () => {
|
|
120
|
-
for (const id of refreshTimersRef.current) clearTimeout(id);
|
|
121
|
-
},
|
|
122
|
-
[],
|
|
123
|
-
);
|
|
124
111
|
const [timelineVisible, setTimelineVisible] = useState(
|
|
125
112
|
() =>
|
|
126
113
|
initialUrlStateRef.current.timelineVisible ??
|
|
@@ -140,6 +127,7 @@ export function StudioApp() {
|
|
|
140
127
|
});
|
|
141
128
|
const editHistory = usePersistentEditHistory({ projectId });
|
|
142
129
|
const domEditSaveTimestampRef = useRef(0);
|
|
130
|
+
const handleDomZIndexReorderCommitRef = useRef<TimelineZIndexReorderCommit | null>(null);
|
|
143
131
|
const pendingTimelineEditPathRef = useRef(new Set<string>());
|
|
144
132
|
const isGestureRecordingRef = useRef(false);
|
|
145
133
|
const reloadPreview = useCallback(() => setRefreshKey((k) => k + 1), []);
|
|
@@ -150,7 +138,16 @@ export function StudioApp() {
|
|
|
150
138
|
domEditSaveTimestampRef,
|
|
151
139
|
setRefreshKey,
|
|
152
140
|
});
|
|
153
|
-
const
|
|
141
|
+
const masterCompPath = useMemo(
|
|
142
|
+
() => resolveMasterCompositionPath(fileManager.fileTree),
|
|
143
|
+
[fileManager.fileTree],
|
|
144
|
+
);
|
|
145
|
+
const { sdkHandle, editFlowSdkSession } = useStudioSdkSessions(
|
|
146
|
+
projectId,
|
|
147
|
+
activeCompPath,
|
|
148
|
+
domEditSaveTimestampRef,
|
|
149
|
+
masterCompPath,
|
|
150
|
+
);
|
|
154
151
|
useEffect(() => {
|
|
155
152
|
if (activeCompPathHydrated) return;
|
|
156
153
|
if (!fileManager.fileTreeLoaded) return;
|
|
@@ -186,8 +183,9 @@ export function StudioApp() {
|
|
|
186
183
|
pendingTimelineEditPathRef,
|
|
187
184
|
uploadProjectFiles: fileManager.uploadProjectFiles,
|
|
188
185
|
isRecordingRef: isGestureRecordingRef,
|
|
189
|
-
sdkSession:
|
|
186
|
+
sdkSession: editFlowSdkSession,
|
|
190
187
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
188
|
+
handleDomZIndexReorderCommitRef,
|
|
191
189
|
});
|
|
192
190
|
const {
|
|
193
191
|
activeBlockParams,
|
|
@@ -302,25 +300,18 @@ export function StudioApp() {
|
|
|
302
300
|
openSourceForSelection: fileManager.openSourceForSelection,
|
|
303
301
|
selectSidebarTab: sidebarTabRef.current.select,
|
|
304
302
|
getSidebarTab: sidebarTabRef.current.get,
|
|
305
|
-
sdkSession:
|
|
303
|
+
sdkSession: editFlowSdkSession,
|
|
306
304
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
307
305
|
});
|
|
308
306
|
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
|
|
307
|
+
handleDomZIndexReorderCommitRef.current = domEditSession.handleDomZIndexReorderCommit;
|
|
309
308
|
clearDomSelectionRef.current = domEditSession.clearDomSelection;
|
|
310
309
|
handleDomEditElementDeleteRef.current = domEditSession.handleDomEditElementDelete;
|
|
311
310
|
resetKeyframesRef.current = domEditSession.handleResetSelectedElementKeyframes;
|
|
312
311
|
invalidateGsapCacheRef.current = domEditSession.invalidateGsapCache;
|
|
313
|
-
deleteSelectedKeyframesRef.current = () =>
|
|
314
|
-
const { selectedKeyframes, selectedElementId } = usePlayerStore.getState();
|
|
315
|
-
const a = domEditSession.selectedGsapAnimations.find((x) => x.keyframes);
|
|
316
|
-
if (!a) return;
|
|
317
|
-
// Only the active element's keyframes; a stale cross-element selection must not delete here.
|
|
318
|
-
for (const p of selectedKeyframePercentagesForElement(selectedKeyframes, selectedElementId)) {
|
|
319
|
-
domEditSession.handleGsapRemoveKeyframe(a.id, p);
|
|
320
|
-
}
|
|
321
|
-
};
|
|
312
|
+
deleteSelectedKeyframesRef.current = () => deleteSelectedKeyframes(domEditSession);
|
|
322
313
|
useSdkSelectionSync(
|
|
323
|
-
|
|
314
|
+
editFlowSdkSession,
|
|
324
315
|
domEditSession.domEditSelection,
|
|
325
316
|
domEditSession.domEditGroupSelections,
|
|
326
317
|
);
|
|
@@ -528,7 +519,10 @@ export function StudioApp() {
|
|
|
528
519
|
handleTimelineFileDrop={timelineEditing.handleTimelineFileDrop}
|
|
529
520
|
handleTimelineElementMove={timelineEditing.handleTimelineElementMove}
|
|
530
521
|
handleTimelineElementResize={timelineEditing.handleTimelineElementResize}
|
|
522
|
+
handleTimelineGroupMove={timelineEditing.handleTimelineGroupMove}
|
|
523
|
+
handleTimelineGroupResize={timelineEditing.handleTimelineGroupResize}
|
|
531
524
|
handleToggleTrackHidden={timelineEditing.handleToggleTrackHidden}
|
|
525
|
+
handleToggleElementHidden={timelineEditing.handleToggleElementHidden}
|
|
532
526
|
handleBlockedTimelineEdit={timelineEditing.handleBlockedTimelineEdit}
|
|
533
527
|
handleTimelineElementSplit={timelineEditing.handleTimelineElementSplit}
|
|
534
528
|
handleRazorSplit={timelineEditing.handleRazorSplit}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wires the Design panel's promote-to-variable context. Promote/bind operates
|
|
3
|
+
* on the file the selected element actually lives in — a sub-composition file
|
|
4
|
+
* when you select an element inside an inlined sub-comp, not the host. So we
|
|
5
|
+
* open (and persist to) an SDK session keyed on `selection.sourceFile`, not the
|
|
6
|
+
* host `activeCompPath`. Declaring a variable therefore lands in the sub-comp's
|
|
7
|
+
* own file, making it a knob on that reusable frame everywhere it is used. When
|
|
8
|
+
* nothing is selected (or the element is top-level) the target is the active
|
|
9
|
+
* composition, so behavior there is unchanged.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ReactNode } from "react";
|
|
13
|
+
import type { DomEditSelection } from "./editor/domEditingTypes";
|
|
14
|
+
import { useSdkSession } from "../hooks/useSdkSession";
|
|
15
|
+
import { useVariablesPersist, type UseVariablesPersistParams } from "../hooks/useVariablesPersist";
|
|
16
|
+
import { VariablePromoteProvider } from "../contexts/VariablePromoteContext";
|
|
17
|
+
|
|
18
|
+
/** Persist wiring minus the target — this provider derives the target from the selection. */
|
|
19
|
+
type PersistDeps = Omit<UseVariablesPersistParams, "sdkSession" | "activeCompPath">;
|
|
20
|
+
|
|
21
|
+
export function DesignPanelPromoteProvider({
|
|
22
|
+
selection,
|
|
23
|
+
projectId,
|
|
24
|
+
activeCompPath,
|
|
25
|
+
children,
|
|
26
|
+
...persistDeps
|
|
27
|
+
}: PersistDeps & {
|
|
28
|
+
selection: DomEditSelection | null;
|
|
29
|
+
projectId: string | null;
|
|
30
|
+
activeCompPath: string | null;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}) {
|
|
33
|
+
const targetPath = selection?.sourceFile || activeCompPath || "index.html";
|
|
34
|
+
const handle = useSdkSession(projectId, targetPath, persistDeps.domEditSaveTimestampRef);
|
|
35
|
+
const persist = useVariablesPersist({
|
|
36
|
+
...persistDeps,
|
|
37
|
+
sdkSession: handle.session,
|
|
38
|
+
activeCompPath: targetPath,
|
|
39
|
+
});
|
|
40
|
+
return (
|
|
41
|
+
<VariablePromoteProvider session={handle.session} selection={selection} persist={persist}>
|
|
42
|
+
{children}
|
|
43
|
+
</VariablePromoteProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Tooltip } from "./ui";
|
|
2
|
+
|
|
3
|
+
/** Tab-bar button for the right inspector panel header. */
|
|
4
|
+
export function PanelTabButton({
|
|
5
|
+
label,
|
|
6
|
+
tooltip,
|
|
7
|
+
active,
|
|
8
|
+
onClick,
|
|
9
|
+
}: {
|
|
10
|
+
label: string;
|
|
11
|
+
tooltip: string;
|
|
12
|
+
active: boolean;
|
|
13
|
+
onClick: () => void;
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<Tooltip label={tooltip} side="bottom">
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
onClick={onClick}
|
|
20
|
+
aria-pressed={active}
|
|
21
|
+
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors active:scale-[0.98] ${
|
|
22
|
+
active
|
|
23
|
+
? "bg-neutral-800 text-white"
|
|
24
|
+
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
|
25
|
+
}`}
|
|
26
|
+
>
|
|
27
|
+
{label}
|
|
28
|
+
</button>
|
|
29
|
+
</Tooltip>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -25,6 +25,15 @@ import { TimelineEditProvider } from "../contexts/TimelineEditContext";
|
|
|
25
25
|
import type { BlockPreviewInfo } from "./sidebar/BlocksTab";
|
|
26
26
|
import { readStudioUiPreferences } from "../utils/studioUiPreferences";
|
|
27
27
|
import type { GestureRecordingState } from "./editor/GestureRecordControl";
|
|
28
|
+
import { useTimelineSelectionPreviewSync } from "../hooks/useTimelineSelectionPreviewSync";
|
|
29
|
+
import type {
|
|
30
|
+
TimelineGroupMoveChange,
|
|
31
|
+
TimelineGroupResizeChange,
|
|
32
|
+
} from "../hooks/useTimelineGroupEditing";
|
|
33
|
+
import {
|
|
34
|
+
formatTimelineAttributeNumber,
|
|
35
|
+
patchIframeDomTiming,
|
|
36
|
+
} from "../hooks/timelineEditingHelpers";
|
|
28
37
|
|
|
29
38
|
export interface StudioPreviewAreaProps {
|
|
30
39
|
timelineToolbar: ReactNode;
|
|
@@ -58,7 +67,10 @@ export interface StudioPreviewAreaProps {
|
|
|
58
67
|
element: TimelineElement,
|
|
59
68
|
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
|
|
60
69
|
) => Promise<void> | void;
|
|
70
|
+
handleTimelineGroupMove: (changes: TimelineGroupMoveChange[]) => Promise<void> | void;
|
|
71
|
+
handleTimelineGroupResize: (changes: TimelineGroupResizeChange[]) => Promise<void> | void;
|
|
61
72
|
handleToggleTrackHidden: (track: number, hidden: boolean) => Promise<void> | void;
|
|
73
|
+
handleToggleElementHidden: (elementKey: string, hidden: boolean) => Promise<void> | void;
|
|
62
74
|
handleBlockedTimelineEdit: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
|
|
63
75
|
handleTimelineElementSplit: (element: TimelineElement, splitTime: number) => Promise<void> | void;
|
|
64
76
|
handleRazorSplit: (element: TimelineElement, splitTime: number) => Promise<void> | void;
|
|
@@ -84,7 +96,10 @@ export function StudioPreviewArea({
|
|
|
84
96
|
handleTimelineFileDrop,
|
|
85
97
|
handleTimelineElementMove,
|
|
86
98
|
handleTimelineElementResize,
|
|
99
|
+
handleTimelineGroupMove,
|
|
100
|
+
handleTimelineGroupResize,
|
|
87
101
|
handleToggleTrackHidden,
|
|
102
|
+
handleToggleElementHidden,
|
|
88
103
|
handleBlockedTimelineEdit,
|
|
89
104
|
handleTimelineElementSplit,
|
|
90
105
|
handleRazorSplit,
|
|
@@ -146,6 +161,9 @@ export function StudioPreviewArea({
|
|
|
146
161
|
buildDomSelectionForTimelineElement,
|
|
147
162
|
applyMarqueeSelection,
|
|
148
163
|
} = useDomEditActionsContext();
|
|
164
|
+
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
165
|
+
const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
|
|
166
|
+
const timelineElements = usePlayerStore((s) => s.elements);
|
|
149
167
|
|
|
150
168
|
// fallow-ignore-next-line complexity
|
|
151
169
|
const [snapPrefs, setSnapPrefs] = useState(() => {
|
|
@@ -158,6 +176,18 @@ export function StudioPreviewArea({
|
|
|
158
176
|
};
|
|
159
177
|
});
|
|
160
178
|
|
|
179
|
+
useTimelineSelectionPreviewSync({
|
|
180
|
+
selectedElementId,
|
|
181
|
+
selectedElementIds,
|
|
182
|
+
timelineElements,
|
|
183
|
+
domEditSelection,
|
|
184
|
+
domEditGroupSelections,
|
|
185
|
+
activeCompPath,
|
|
186
|
+
buildDomSelectionForTimelineElement,
|
|
187
|
+
applyDomSelection,
|
|
188
|
+
applyMarqueeSelection,
|
|
189
|
+
});
|
|
190
|
+
|
|
161
191
|
// Resolve a timeline-diamond callback's clip-% to the keyframe's anim id + its
|
|
162
192
|
// tween-relative percentage (shared by the delete/move keyframe callbacks): the
|
|
163
193
|
// diamond reports a clip-% but the script ops key on the tween-%. Prefers the
|
|
@@ -176,19 +206,56 @@ export function StudioPreviewArea({
|
|
|
176
206
|
[domEditSelection?.id, selectedGsapAnimations],
|
|
177
207
|
);
|
|
178
208
|
|
|
209
|
+
const handleTimelineGroupMovePreview = useCallback(
|
|
210
|
+
(changes: TimelineGroupMoveChange[]) => {
|
|
211
|
+
for (const change of changes) {
|
|
212
|
+
patchIframeDomTiming(previewIframeRef.current, change.element, [
|
|
213
|
+
["data-start", formatTimelineAttributeNumber(change.start)],
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
[previewIframeRef],
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const handleTimelineGroupResizePreview = useCallback(
|
|
221
|
+
(changes: TimelineGroupResizeChange[]) => {
|
|
222
|
+
for (const change of changes) {
|
|
223
|
+
const attrs: Array<[string, string]> = [
|
|
224
|
+
["data-start", formatTimelineAttributeNumber(change.start)],
|
|
225
|
+
["data-duration", formatTimelineAttributeNumber(change.duration)],
|
|
226
|
+
];
|
|
227
|
+
if (change.playbackStart != null) {
|
|
228
|
+
attrs.push([
|
|
229
|
+
change.element.playbackStartAttr === "playback-start"
|
|
230
|
+
? "data-playback-start"
|
|
231
|
+
: "data-media-start",
|
|
232
|
+
formatTimelineAttributeNumber(change.playbackStart),
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
patchIframeDomTiming(previewIframeRef.current, change.element, attrs);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
[previewIframeRef],
|
|
239
|
+
);
|
|
240
|
+
|
|
179
241
|
// fallow-ignore-next-line complexity
|
|
180
242
|
const timelineEditCallbacks = useMemo(
|
|
181
243
|
() => ({
|
|
182
244
|
onMoveElement: handleTimelineElementMove,
|
|
183
245
|
onResizeElement: handleTimelineElementResize,
|
|
246
|
+
onMoveElements: handleTimelineGroupMove,
|
|
247
|
+
onResizeElements: handleTimelineGroupResize,
|
|
248
|
+
onPreviewMoveElements: handleTimelineGroupMovePreview,
|
|
249
|
+
onPreviewResizeElements: handleTimelineGroupResizePreview,
|
|
184
250
|
onToggleTrackHidden: handleToggleTrackHidden,
|
|
251
|
+
onToggleElementHidden: handleToggleElementHidden,
|
|
185
252
|
onBlockedEditAttempt: handleBlockedTimelineEdit,
|
|
186
253
|
onSplitElement: handleTimelineElementSplit,
|
|
187
254
|
onRazorSplit: handleRazorSplit,
|
|
188
255
|
onRazorSplitAll: handleRazorSplitAll,
|
|
189
256
|
onDeleteAllKeyframes: () => {
|
|
190
257
|
// Hold the element where it is (collapse keyframes to a static set) rather
|
|
191
|
-
// than deleting the whole animation
|
|
258
|
+
// than deleting the whole animation, deleting strands a stale GSAP base
|
|
192
259
|
// that the next drag adds to, flinging the element off-screen.
|
|
193
260
|
const anim = selectedGsapAnimations.find((a) => a.keyframes);
|
|
194
261
|
if (!anim) return;
|
|
@@ -208,7 +275,7 @@ export function StudioPreviewArea({
|
|
|
208
275
|
// absolute time (via the clip's timing basis) and let resolveKeyframeRetime
|
|
209
276
|
// decide: a drop inside the tween window is a plain move (re-key tween-%); a
|
|
210
277
|
// drop past the boundary (last keyframe past the end, first before the start)
|
|
211
|
-
// resizes the tween
|
|
278
|
+
// resizes the tween, position/duration grow so the dragged keyframe lands at
|
|
212
279
|
// the drop while every other keyframe keeps its absolute time (value+ease too).
|
|
213
280
|
// fallow-ignore-next-line complexity
|
|
214
281
|
onMoveKeyframe: (_elId: string, fromClipPct: number, toClipPct: number) => {
|
|
@@ -281,7 +348,12 @@ export function StudioPreviewArea({
|
|
|
281
348
|
[
|
|
282
349
|
handleTimelineElementMove,
|
|
283
350
|
handleTimelineElementResize,
|
|
351
|
+
handleTimelineGroupMove,
|
|
352
|
+
handleTimelineGroupMovePreview,
|
|
353
|
+
handleTimelineGroupResize,
|
|
354
|
+
handleTimelineGroupResizePreview,
|
|
284
355
|
handleToggleTrackHidden,
|
|
356
|
+
handleToggleElementHidden,
|
|
285
357
|
handleBlockedTimelineEdit,
|
|
286
358
|
handleTimelineElementSplit,
|
|
287
359
|
handleRazorSplit,
|
|
@@ -323,7 +395,7 @@ export function StudioPreviewArea({
|
|
|
323
395
|
onCompositionLoadingChange={setCompositionLoading}
|
|
324
396
|
onCompositionChange={(compPath) => {
|
|
325
397
|
// Sync activeCompPath when user drills down via timeline double-click
|
|
326
|
-
// or navigates back via breadcrumb
|
|
398
|
+
// or navigates back via breadcrumb, keeps sidebar + thumbnails in sync.
|
|
327
399
|
// Guard against no-op updates to prevent circular refresh cascades
|
|
328
400
|
// between activeCompPath → compositionStack → onCompositionChange.
|
|
329
401
|
if (compPath !== activeCompPath) {
|