@hyperframes/studio 0.7.55 → 0.7.57
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--Z69cEkE.js +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +126 -125
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +53 -2
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +260 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +56 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +23 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -6,21 +6,25 @@ import {
|
|
|
6
6
|
buildTimelineMoveTimingPatch,
|
|
7
7
|
buildTimelineResizeTimingPatch,
|
|
8
8
|
extendRootDurationIfNeeded,
|
|
9
|
-
finishTimelineTimingFallback,
|
|
10
|
-
foldGsapMutationIntoHistory,
|
|
11
9
|
formatTimelineAttributeNumber,
|
|
12
10
|
patchIframeDomTiming,
|
|
13
11
|
persistTimelineBatchEdit,
|
|
14
|
-
readFileContent,
|
|
15
|
-
scaleGsapPositions,
|
|
16
|
-
shiftGsapPositions,
|
|
17
12
|
type PersistTimelineBatchChange,
|
|
18
13
|
type RecordEditInput,
|
|
19
14
|
} from "./timelineEditingHelpers";
|
|
15
|
+
import {
|
|
16
|
+
captureDurationRollback,
|
|
17
|
+
finishGroupTimingGsapFallback,
|
|
18
|
+
readFileContent,
|
|
19
|
+
scaleGsapPositions,
|
|
20
|
+
shiftGsapPositions,
|
|
21
|
+
syncPreviewContentDuration,
|
|
22
|
+
} from "./timelineTimingSync";
|
|
20
23
|
|
|
21
24
|
export interface TimelineGroupMoveChange {
|
|
22
25
|
element: TimelineElement;
|
|
23
26
|
start: number;
|
|
27
|
+
track?: number;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export interface TimelineGroupResizeChange {
|
|
@@ -74,6 +78,15 @@ function resizeCoalesceKey(changes: readonly TimelineGroupResizeChange[]): strin
|
|
|
74
78
|
return `timeline-group-resize:${changes.map((change) => change.element.hfId ?? change.element.id).join(",")}`;
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
function toSdkTimingChanges<T extends { element: TimelineElement }>(
|
|
82
|
+
changes: readonly T[],
|
|
83
|
+
timingUpdate: (change: T) => { start: number; duration?: number },
|
|
84
|
+
): Array<{ hfId: string; timingUpdate: { start: number; duration?: number } } | null> {
|
|
85
|
+
return changes.map((change) =>
|
|
86
|
+
change.element.hfId ? { hfId: change.element.hfId, timingUpdate: timingUpdate(change) } : null,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
function resizeHasPlaybackStartAdjustment(change: TimelineGroupResizeChange): boolean {
|
|
78
91
|
return (
|
|
79
92
|
change.playbackStart != null ||
|
|
@@ -148,45 +161,90 @@ export function useTimelineGroupEditing({
|
|
|
148
161
|
],
|
|
149
162
|
);
|
|
150
163
|
|
|
164
|
+
// Shared SDK fast path for group move/resize: eligible when nothing needs the
|
|
165
|
+
// server (no root-duration growth, one shared file, every change SDK-addressable
|
|
166
|
+
// and `eligible` per the caller's own gate). Returns whether the SDK handled it;
|
|
167
|
+
// false → caller falls through to the server batch persist.
|
|
168
|
+
const trySdkBatchPersist = useCallback(
|
|
169
|
+
async (input: {
|
|
170
|
+
changes: readonly { element: TimelineElement }[];
|
|
171
|
+
sdkChanges: Array<{
|
|
172
|
+
hfId: string;
|
|
173
|
+
timingUpdate: { start: number; duration?: number };
|
|
174
|
+
} | null>;
|
|
175
|
+
eligible: boolean;
|
|
176
|
+
needsExtension: boolean;
|
|
177
|
+
label: string;
|
|
178
|
+
coalesceKey: string;
|
|
179
|
+
}): Promise<boolean> => {
|
|
180
|
+
const sharedPath = allChangesSharePath(input.changes, activeCompPath);
|
|
181
|
+
const canUseSdk =
|
|
182
|
+
!input.needsExtension &&
|
|
183
|
+
sharedPath !== null &&
|
|
184
|
+
input.eligible &&
|
|
185
|
+
input.sdkChanges.every((change) => change !== null);
|
|
186
|
+
if (!canUseSdk) return false;
|
|
187
|
+
return sdkTimingBatchPersist(
|
|
188
|
+
input.sdkChanges.filter((change): change is NonNullable<typeof change> => change !== null),
|
|
189
|
+
sharedPath,
|
|
190
|
+
sdkSession,
|
|
191
|
+
{
|
|
192
|
+
editHistory: { recordEdit },
|
|
193
|
+
writeProjectFile,
|
|
194
|
+
reloadPreview,
|
|
195
|
+
domEditSaveTimestampRef,
|
|
196
|
+
compositionPath: activeCompPath,
|
|
197
|
+
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
198
|
+
},
|
|
199
|
+
{ label: input.label, coalesceKey: input.coalesceKey },
|
|
200
|
+
);
|
|
201
|
+
},
|
|
202
|
+
[
|
|
203
|
+
activeCompPath,
|
|
204
|
+
domEditSaveTimestampRef,
|
|
205
|
+
projectIdRef,
|
|
206
|
+
recordEdit,
|
|
207
|
+
reloadPreview,
|
|
208
|
+
sdkSession,
|
|
209
|
+
writeProjectFile,
|
|
210
|
+
],
|
|
211
|
+
);
|
|
212
|
+
|
|
151
213
|
const handleTimelineGroupMove = useCallback(
|
|
152
214
|
(changes: TimelineGroupMoveChange[], options?: TimelineGroupCommitOptions) => {
|
|
153
215
|
if (changes.length === 0) return Promise.resolve();
|
|
154
216
|
for (const change of changes) {
|
|
155
|
-
|
|
217
|
+
const attrs: Array<[string, string]> = [
|
|
156
218
|
["data-start", formatTimelineAttributeNumber(change.start)],
|
|
157
|
-
]
|
|
219
|
+
];
|
|
220
|
+
if (change.track != null) {
|
|
221
|
+
attrs.push(["data-track-index", formatTimelineAttributeNumber(change.track)]);
|
|
222
|
+
}
|
|
223
|
+
patchIframeDomTiming(previewIframeRef.current, change.element, attrs);
|
|
158
224
|
}
|
|
159
225
|
|
|
160
226
|
const maxEnd = Math.max(...changes.map((change) => change.start + change.element.duration));
|
|
227
|
+
// Snapshot the duration BEFORE the optimistic updates below so a failed
|
|
228
|
+
// persist can roll the readout + live root back (see captureDurationRollback).
|
|
229
|
+
const rollbackDuration = captureDurationRollback(previewIframeRef.current);
|
|
230
|
+
// needsExtension gates the SDK path (setTiming can't grow the root duration),
|
|
231
|
+
// so read the store BEFORE the readout sync below optimistically updates it.
|
|
161
232
|
const needsExtension = extendRootDurationIfNeeded(maxEnd);
|
|
233
|
+
// Optimistic duration readout: content-driven (grow AND shrink), read from
|
|
234
|
+
// the just-patched live DOM. See syncPreviewContentDuration.
|
|
235
|
+
syncPreviewContentDuration(previewIframeRef.current);
|
|
162
236
|
const coalesceKey = options?.coalesceKey ?? moveCoalesceKey(changes);
|
|
163
237
|
return enqueueGroupOperation("Move timeline clips", async (projectId) => {
|
|
164
238
|
await options?.beforeTiming;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
change.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
const handled = await sdkTimingBatchPersist(
|
|
175
|
-
sdkChanges.filter((change): change is NonNullable<typeof change> => change !== null),
|
|
176
|
-
sharedPath,
|
|
177
|
-
sdkSession,
|
|
178
|
-
{
|
|
179
|
-
editHistory: { recordEdit },
|
|
180
|
-
writeProjectFile,
|
|
181
|
-
reloadPreview,
|
|
182
|
-
domEditSaveTimestampRef,
|
|
183
|
-
compositionPath: activeCompPath,
|
|
184
|
-
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
185
|
-
},
|
|
186
|
-
{ label: "Move timeline clips", coalesceKey },
|
|
187
|
-
);
|
|
188
|
-
if (handled) return;
|
|
189
|
-
}
|
|
239
|
+
const handledBySdk = await trySdkBatchPersist({
|
|
240
|
+
changes,
|
|
241
|
+
sdkChanges: toSdkTimingChanges(changes, (change) => ({ start: change.start })),
|
|
242
|
+
eligible: changes.every((change) => change.track == null),
|
|
243
|
+
needsExtension,
|
|
244
|
+
label: "Move timeline clips",
|
|
245
|
+
coalesceKey,
|
|
246
|
+
});
|
|
247
|
+
if (handledBySdk) return;
|
|
190
248
|
|
|
191
249
|
await persistServerBatch(
|
|
192
250
|
projectId,
|
|
@@ -194,54 +252,49 @@ export function useTimelineGroupEditing({
|
|
|
194
252
|
changes.map((change) => ({
|
|
195
253
|
element: change.element,
|
|
196
254
|
buildPatches: (original, target) =>
|
|
197
|
-
buildTimelineMoveTimingPatch(
|
|
255
|
+
buildTimelineMoveTimingPatch(
|
|
256
|
+
original,
|
|
257
|
+
target,
|
|
258
|
+
change.start,
|
|
259
|
+
change.element.duration,
|
|
260
|
+
change.track,
|
|
261
|
+
),
|
|
198
262
|
})),
|
|
199
263
|
coalesceKey,
|
|
200
264
|
);
|
|
201
|
-
await
|
|
265
|
+
await finishGroupTimingGsapFallback({
|
|
266
|
+
projectId,
|
|
202
267
|
iframe: previewIframeRef.current,
|
|
203
|
-
needsExtension,
|
|
204
|
-
rootDurationSeconds: maxEnd,
|
|
205
268
|
reloadPreview,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const status = await shiftGsapPositions(
|
|
220
|
-
projectId,
|
|
221
|
-
targetPathFor(change.element, activeCompPath),
|
|
222
|
-
domId,
|
|
223
|
-
delta,
|
|
224
|
-
);
|
|
225
|
-
mutated = mutated || status.mutated;
|
|
226
|
-
}
|
|
227
|
-
return { mutated };
|
|
228
|
-
},
|
|
229
|
-
}),
|
|
230
|
-
onGsapError: (err) => console.error("[Timeline] Failed to shift GSAP positions", err),
|
|
269
|
+
label: "Move timeline clips",
|
|
270
|
+
errorLabel: "Failed to shift GSAP positions",
|
|
271
|
+
coalesceKey,
|
|
272
|
+
recordEdit,
|
|
273
|
+
activeCompPath,
|
|
274
|
+
changes,
|
|
275
|
+
resolveChangePath: (element) => targetPathFor(element, activeCompPath),
|
|
276
|
+
mutateChange: (change, changePath) => {
|
|
277
|
+
const delta = change.start - change.element.start;
|
|
278
|
+
const domId = change.element.domId;
|
|
279
|
+
if (delta === 0 || !domId) return null;
|
|
280
|
+
return shiftGsapPositions(projectId, changePath, domId, delta);
|
|
281
|
+
},
|
|
231
282
|
});
|
|
283
|
+
}).catch((error) => {
|
|
284
|
+
// Failed persist: revert the optimistic duration readout + live root
|
|
285
|
+
// alongside the gesture owner's store rollback.
|
|
286
|
+
rollbackDuration();
|
|
287
|
+
throw error;
|
|
232
288
|
});
|
|
233
289
|
},
|
|
234
290
|
[
|
|
235
291
|
activeCompPath,
|
|
236
|
-
domEditSaveTimestampRef,
|
|
237
292
|
enqueueGroupOperation,
|
|
238
293
|
persistServerBatch,
|
|
239
294
|
previewIframeRef,
|
|
240
|
-
projectIdRef,
|
|
241
295
|
recordEdit,
|
|
242
296
|
reloadPreview,
|
|
243
|
-
|
|
244
|
-
writeProjectFile,
|
|
297
|
+
trySdkBatchPersist,
|
|
245
298
|
],
|
|
246
299
|
);
|
|
247
300
|
|
|
@@ -264,41 +317,30 @@ export function useTimelineGroupEditing({
|
|
|
264
317
|
}
|
|
265
318
|
|
|
266
319
|
const maxEnd = Math.max(...changes.map((change) => change.start + change.duration));
|
|
320
|
+
// Snapshot the duration BEFORE the optimistic updates below so a failed
|
|
321
|
+
// persist can roll the readout + live root back (see captureDurationRollback).
|
|
322
|
+
const rollbackDuration = captureDurationRollback(previewIframeRef.current);
|
|
323
|
+
// needsExtension gates the SDK path (setTiming can't grow the root duration),
|
|
324
|
+
// so read the store BEFORE the readout sync below optimistically updates it.
|
|
267
325
|
const needsExtension = extendRootDurationIfNeeded(maxEnd);
|
|
326
|
+
// Optimistic duration readout: content-driven (grow AND shrink), read from
|
|
327
|
+
// the just-patched live DOM. See syncPreviewContentDuration.
|
|
328
|
+
syncPreviewContentDuration(previewIframeRef.current);
|
|
268
329
|
const coalesceKey = options?.coalesceKey ?? resizeCoalesceKey(changes);
|
|
269
330
|
return enqueueGroupOperation("Resize timeline clips", async (projectId) => {
|
|
270
331
|
await options?.beforeTiming;
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
change
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
changes.every((change) => !resizeHasPlaybackStartAdjustment(change)) &&
|
|
284
|
-
sdkChanges.every((change) => change !== null);
|
|
285
|
-
if (canUseSdk) {
|
|
286
|
-
const handled = await sdkTimingBatchPersist(
|
|
287
|
-
sdkChanges.filter((change): change is NonNullable<typeof change> => change !== null),
|
|
288
|
-
sharedPath,
|
|
289
|
-
sdkSession,
|
|
290
|
-
{
|
|
291
|
-
editHistory: { recordEdit },
|
|
292
|
-
writeProjectFile,
|
|
293
|
-
reloadPreview,
|
|
294
|
-
domEditSaveTimestampRef,
|
|
295
|
-
compositionPath: activeCompPath,
|
|
296
|
-
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
297
|
-
},
|
|
298
|
-
{ label: "Resize timeline clips", coalesceKey },
|
|
299
|
-
);
|
|
300
|
-
if (handled) return;
|
|
301
|
-
}
|
|
332
|
+
const handledBySdk = await trySdkBatchPersist({
|
|
333
|
+
changes,
|
|
334
|
+
sdkChanges: toSdkTimingChanges(changes, (change) => ({
|
|
335
|
+
start: change.start,
|
|
336
|
+
duration: change.duration,
|
|
337
|
+
})),
|
|
338
|
+
eligible: changes.every((change) => !resizeHasPlaybackStartAdjustment(change)),
|
|
339
|
+
needsExtension,
|
|
340
|
+
label: "Resize timeline clips",
|
|
341
|
+
coalesceKey,
|
|
342
|
+
});
|
|
343
|
+
if (handledBySdk) return;
|
|
302
344
|
|
|
303
345
|
await persistServerBatch(
|
|
304
346
|
projectId,
|
|
@@ -314,55 +356,48 @@ export function useTimelineGroupEditing({
|
|
|
314
356
|
})),
|
|
315
357
|
coalesceKey,
|
|
316
358
|
);
|
|
317
|
-
await
|
|
359
|
+
await finishGroupTimingGsapFallback({
|
|
360
|
+
projectId,
|
|
318
361
|
iframe: previewIframeRef.current,
|
|
319
|
-
needsExtension,
|
|
320
|
-
rootDurationSeconds: maxEnd,
|
|
321
362
|
reloadPreview,
|
|
322
|
-
|
|
323
|
-
|
|
363
|
+
label: "Resize timeline clips",
|
|
364
|
+
errorLabel: "Failed to scale GSAP positions",
|
|
365
|
+
coalesceKey,
|
|
366
|
+
recordEdit,
|
|
367
|
+
activeCompPath,
|
|
368
|
+
changes,
|
|
369
|
+
resolveChangePath: (element) => targetPathFor(element, activeCompPath),
|
|
370
|
+
mutateChange: (change, changePath) => {
|
|
371
|
+
const domId = change.element.domId;
|
|
372
|
+
const timingChanged =
|
|
373
|
+
change.start !== change.element.start || change.duration !== change.element.duration;
|
|
374
|
+
if (!timingChanged || !domId) return null;
|
|
375
|
+
return scaleGsapPositions(
|
|
324
376
|
projectId,
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const timingChanged =
|
|
334
|
-
change.start !== change.element.start ||
|
|
335
|
-
change.duration !== change.element.duration;
|
|
336
|
-
if (!timingChanged || !domId) continue;
|
|
337
|
-
const status = await scaleGsapPositions(
|
|
338
|
-
projectId,
|
|
339
|
-
targetPathFor(change.element, activeCompPath),
|
|
340
|
-
domId,
|
|
341
|
-
change.element.start,
|
|
342
|
-
change.element.duration,
|
|
343
|
-
change.start,
|
|
344
|
-
change.duration,
|
|
345
|
-
);
|
|
346
|
-
mutated = mutated || status.mutated;
|
|
347
|
-
}
|
|
348
|
-
return { mutated };
|
|
349
|
-
},
|
|
350
|
-
}),
|
|
351
|
-
onGsapError: (err) => console.error("[Timeline] Failed to scale GSAP positions", err),
|
|
377
|
+
changePath,
|
|
378
|
+
domId,
|
|
379
|
+
change.element.start,
|
|
380
|
+
change.element.duration,
|
|
381
|
+
change.start,
|
|
382
|
+
change.duration,
|
|
383
|
+
);
|
|
384
|
+
},
|
|
352
385
|
});
|
|
386
|
+
}).catch((error) => {
|
|
387
|
+
// Failed persist: revert the optimistic duration readout + live root
|
|
388
|
+
// alongside the gesture owner's store rollback.
|
|
389
|
+
rollbackDuration();
|
|
390
|
+
throw error;
|
|
353
391
|
});
|
|
354
392
|
},
|
|
355
393
|
[
|
|
356
394
|
activeCompPath,
|
|
357
|
-
domEditSaveTimestampRef,
|
|
358
395
|
enqueueGroupOperation,
|
|
359
396
|
persistServerBatch,
|
|
360
397
|
previewIframeRef,
|
|
361
|
-
projectIdRef,
|
|
362
398
|
recordEdit,
|
|
363
399
|
reloadPreview,
|
|
364
|
-
|
|
365
|
-
writeProjectFile,
|
|
400
|
+
trySdkBatchPersist,
|
|
366
401
|
],
|
|
367
402
|
);
|
|
368
403
|
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// NLE Layout
|
|
2
|
-
export {
|
|
2
|
+
export { EditorShell } from "./components/EditorShell";
|
|
3
3
|
export { NLEPreview } from "./components/nle/NLEPreview";
|
|
4
4
|
export { CompositionBreadcrumb } from "./components/nle/CompositionBreadcrumb";
|
|
5
5
|
export type { CompositionLevel } from "./components/nle/CompositionBreadcrumb";
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { ImageThumbnail } from "./ImageThumbnail";
|
|
7
|
+
|
|
8
|
+
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", {
|
|
9
|
+
configurable: true,
|
|
10
|
+
value: true,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// --- Observer stubs: fire "intersecting" immediately on observe ---
|
|
14
|
+
class MockIntersectionObserver {
|
|
15
|
+
private cb: IntersectionObserverCallback;
|
|
16
|
+
constructor(cb: IntersectionObserverCallback) {
|
|
17
|
+
this.cb = cb;
|
|
18
|
+
}
|
|
19
|
+
observe() {
|
|
20
|
+
this.cb(
|
|
21
|
+
[{ isIntersecting: true } as IntersectionObserverEntry],
|
|
22
|
+
this as unknown as IntersectionObserver,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
disconnect() {}
|
|
26
|
+
unobserve() {}
|
|
27
|
+
takeRecords(): IntersectionObserverEntry[] {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class MockResizeObserver {
|
|
33
|
+
observe() {}
|
|
34
|
+
disconnect() {}
|
|
35
|
+
unobserve() {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Image stub: captures instances so tests fire load/error deterministically ---
|
|
39
|
+
class MockImage {
|
|
40
|
+
static instances: MockImage[] = [];
|
|
41
|
+
onload: (() => void) | null = null;
|
|
42
|
+
onerror: (() => void) | null = null;
|
|
43
|
+
naturalWidth = 0;
|
|
44
|
+
naturalHeight = 0;
|
|
45
|
+
src = "";
|
|
46
|
+
constructor() {
|
|
47
|
+
MockImage.instances.push(this);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const originalIO = globalThis.IntersectionObserver;
|
|
52
|
+
const originalRO = globalThis.ResizeObserver;
|
|
53
|
+
const originalImage = globalThis.Image;
|
|
54
|
+
|
|
55
|
+
let host: HTMLDivElement;
|
|
56
|
+
let root: Root | null = null;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
globalThis.IntersectionObserver =
|
|
60
|
+
MockIntersectionObserver as unknown as typeof IntersectionObserver;
|
|
61
|
+
globalThis.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
|
|
62
|
+
globalThis.Image = MockImage as unknown as typeof Image;
|
|
63
|
+
MockImage.instances = [];
|
|
64
|
+
host = document.createElement("div");
|
|
65
|
+
document.body.append(host);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
act(() => root?.unmount());
|
|
70
|
+
root = null;
|
|
71
|
+
globalThis.IntersectionObserver = originalIO;
|
|
72
|
+
globalThis.ResizeObserver = originalRO;
|
|
73
|
+
globalThis.Image = originalImage;
|
|
74
|
+
document.body.innerHTML = "";
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function render(props: { imageSrc: string; label?: string; labelColor?: string }) {
|
|
78
|
+
root = createRoot(host);
|
|
79
|
+
act(() => {
|
|
80
|
+
root!.render(
|
|
81
|
+
<ImageThumbnail
|
|
82
|
+
imageSrc={props.imageSrc}
|
|
83
|
+
label={props.label ?? ""}
|
|
84
|
+
labelColor={props.labelColor ?? "#fff"}
|
|
85
|
+
/>,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function lastProbe(): MockImage {
|
|
91
|
+
const probe = MockImage.instances.at(-1);
|
|
92
|
+
expect(probe).toBeDefined();
|
|
93
|
+
return probe!;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Assert at least one tile rendered and the first tile serves `expectedSrc`. */
|
|
97
|
+
function expectFirstTileSrc(expectedSrc: string): void {
|
|
98
|
+
const imgs = [...host.querySelectorAll("img")];
|
|
99
|
+
expect(imgs.length).toBeGreaterThanOrEqual(1);
|
|
100
|
+
expect(imgs[0].getAttribute("src")).toBe(expectedSrc);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe("ImageThumbnail", () => {
|
|
104
|
+
it("shows the loading shimmer before the image resolves", () => {
|
|
105
|
+
render({ imageSrc: "/api/projects/p/preview/assets/pic.png" });
|
|
106
|
+
expect(host.querySelector(".animate-pulse")).not.toBeNull();
|
|
107
|
+
expect(host.querySelectorAll("img").length).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("probes the resolved src and renders repeated object-cover tiles on load", () => {
|
|
111
|
+
render({ imageSrc: "/api/projects/p/preview/assets/pic.png" });
|
|
112
|
+
const probe = lastProbe();
|
|
113
|
+
expect(probe.src).toBe("/api/projects/p/preview/assets/pic.png");
|
|
114
|
+
|
|
115
|
+
act(() => {
|
|
116
|
+
probe.naturalWidth = 1920;
|
|
117
|
+
probe.naturalHeight = 1080;
|
|
118
|
+
probe.onload?.();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const imgs = [...host.querySelectorAll("img")];
|
|
122
|
+
expect(imgs.length).toBeGreaterThanOrEqual(1);
|
|
123
|
+
for (const img of imgs) {
|
|
124
|
+
expect(img.getAttribute("src")).toBe("/api/projects/p/preview/assets/pic.png");
|
|
125
|
+
expect(img.className).toContain("object-cover");
|
|
126
|
+
}
|
|
127
|
+
expect(host.querySelector(".animate-pulse")).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("drops the shimmer and renders no tiles when a raster image fails to load", () => {
|
|
131
|
+
render({ imageSrc: "/api/projects/p/preview/assets/missing.png" });
|
|
132
|
+
act(() => lastProbe().onerror?.());
|
|
133
|
+
expect(host.querySelectorAll("img").length).toBe(0);
|
|
134
|
+
expect(host.querySelector(".animate-pulse")).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("renders tiles at 16:9 when an SVG has no intrinsic dimensions (naturalWidth=0)", () => {
|
|
138
|
+
render({ imageSrc: "/api/projects/p/preview/assets/logo.svg" });
|
|
139
|
+
const probe = lastProbe();
|
|
140
|
+
|
|
141
|
+
act(() => {
|
|
142
|
+
// naturalWidth stays 0 — SVG with no width/height attribute
|
|
143
|
+
probe.onload?.();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expectFirstTileSrc("/api/projects/p/preview/assets/logo.svg");
|
|
147
|
+
expect(host.querySelector(".animate-pulse")).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("renders SVG tiles at 16:9 fallback even when the probe fires onerror", () => {
|
|
151
|
+
// Some browser/sandbox environments fire onerror for SVGs even though the
|
|
152
|
+
// <img> element itself can render the file — we must not blank the strip.
|
|
153
|
+
render({ imageSrc: "/api/projects/p/preview/assets/icon.svg" });
|
|
154
|
+
|
|
155
|
+
act(() => lastProbe().onerror?.());
|
|
156
|
+
|
|
157
|
+
expectFirstTileSrc("/api/projects/p/preview/assets/icon.svg");
|
|
158
|
+
expect(host.querySelector(".animate-pulse")).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("renders the label above the strip when provided", () => {
|
|
162
|
+
render({ imageSrc: "/x.png", label: "hero", labelColor: "#abc" });
|
|
163
|
+
act(() => {
|
|
164
|
+
const probe = lastProbe();
|
|
165
|
+
probe.naturalWidth = 100;
|
|
166
|
+
probe.naturalHeight = 100;
|
|
167
|
+
probe.onload?.();
|
|
168
|
+
});
|
|
169
|
+
const label = [...host.querySelectorAll("span")].find((s) => s.textContent === "hero");
|
|
170
|
+
expect(label).toBeDefined();
|
|
171
|
+
expect(label!.closest(".z-10")).not.toBeNull();
|
|
172
|
+
});
|
|
173
|
+
});
|