@hyperframes/studio 0.7.55 → 0.7.56
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-BXaqaVKt.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CPetwHFV.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.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 +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40414 -37680
- 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/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 +158 -126
- 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 +43 -1
- 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/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/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/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/nle/AssetPreviewOverlay.tsx +147 -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 +240 -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.tsx +326 -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 +39 -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 +12 -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 +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -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/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -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 +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- 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/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- 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/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- 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/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 +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -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 +120 -61
- 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/timelineTheme.ts +6 -1
- 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/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/index.ts +3 -2
- 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 +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -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.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- 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
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
import type { TimelineElement } from "../store/playerStore";
|
|
12
12
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
13
|
-
import { getElementZIndex, hasExplicitZIndex } from "./layerOrdering";
|
|
14
13
|
import {
|
|
15
14
|
resolveMediaElement,
|
|
16
15
|
applyMediaMetadataFromElement,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
buildTimelineElementIdentity,
|
|
25
24
|
getTimelineElementIdentity,
|
|
26
25
|
isTimelineIgnoredElement,
|
|
26
|
+
readTimelineElementZIndex,
|
|
27
27
|
} from "./timelineElementHelpers";
|
|
28
28
|
|
|
29
29
|
// Re-export helpers that were previously public from this module so that
|
|
@@ -66,49 +66,6 @@ function resolveClipTag(clip: ClipManifestClip): string {
|
|
|
66
66
|
return clip.tagName || clip.kind || "div";
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function resolveDomCompositionContext(
|
|
70
|
-
element: Element,
|
|
71
|
-
root: Element | null,
|
|
72
|
-
): {
|
|
73
|
-
parentCompositionId: string | null;
|
|
74
|
-
compositionAncestors: string[];
|
|
75
|
-
stackingContextId: string | null;
|
|
76
|
-
} {
|
|
77
|
-
const ancestors: string[] = [];
|
|
78
|
-
let parentCompositionId: string | null = null;
|
|
79
|
-
let cursor = element.parentElement;
|
|
80
|
-
while (cursor) {
|
|
81
|
-
const compositionId = cursor.getAttribute("data-composition-id");
|
|
82
|
-
if (compositionId) {
|
|
83
|
-
ancestors.push(compositionId);
|
|
84
|
-
if (!parentCompositionId && cursor !== root) {
|
|
85
|
-
parentCompositionId = compositionId;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
cursor = cursor.parentElement;
|
|
89
|
-
}
|
|
90
|
-
const compositionAncestors = ancestors.reverse();
|
|
91
|
-
return {
|
|
92
|
-
parentCompositionId,
|
|
93
|
-
compositionAncestors,
|
|
94
|
-
stackingContextId: parentCompositionId ?? compositionAncestors[0] ?? null,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function isHTMLElement(element: Element | null): element is HTMLElement {
|
|
99
|
-
if (!element) return false;
|
|
100
|
-
const HtmlElementCtor = element.ownerDocument.defaultView?.HTMLElement ?? globalThis.HTMLElement;
|
|
101
|
-
return typeof HtmlElementCtor !== "undefined" && element instanceof HtmlElementCtor;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function getTimelineElementZIndex(element: Element | null): number | undefined {
|
|
105
|
-
return isHTMLElement(element) ? getElementZIndex(element) : undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function getTimelineElementHasExplicitZIndex(element: Element | null): boolean {
|
|
109
|
-
return isHTMLElement(element) ? hasExplicitZIndex(element) : false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
69
|
// fallow-ignore-next-line complexity
|
|
113
70
|
export function createTimelineElementFromManifestClip(params: {
|
|
114
71
|
clip: ClipManifestClip;
|
|
@@ -130,14 +87,6 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
130
87
|
let sourceFile: string | undefined;
|
|
131
88
|
|
|
132
89
|
let hfId: string | undefined;
|
|
133
|
-
const domContext = hostEl
|
|
134
|
-
? resolveDomCompositionContext(hostEl, doc?.querySelector("[data-composition-id]") ?? null)
|
|
135
|
-
: null;
|
|
136
|
-
const compositionAncestors = clip.compositionAncestors ?? domContext?.compositionAncestors;
|
|
137
|
-
const parentCompositionId = clip.parentCompositionId ?? domContext?.parentCompositionId;
|
|
138
|
-
const stackingContextId =
|
|
139
|
-
clip.stackingContextId ?? parentCompositionId ?? compositionAncestors?.[0] ?? null;
|
|
140
|
-
|
|
141
90
|
if (hostEl) {
|
|
142
91
|
domId = hostEl.id || undefined;
|
|
143
92
|
hfId = hostEl.getAttribute("data-hf-id") || undefined;
|
|
@@ -164,15 +113,6 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
164
113
|
start: clip.start,
|
|
165
114
|
duration: clip.duration,
|
|
166
115
|
track: clip.track,
|
|
167
|
-
// Prefer the effective (computed) z-index read from the live element — the
|
|
168
|
-
// same read the reorder commit uses — so CSS-rule z-index (not just inline)
|
|
169
|
-
// is captured. clip.zIndex from the runtime is inline-only (0 for CSS rules),
|
|
170
|
-
// so it can only serve as a fallback when the element isn't live.
|
|
171
|
-
zIndex: getTimelineElementZIndex(hostEl) ?? clip.zIndex ?? 0,
|
|
172
|
-
hasExplicitZIndex: getTimelineElementHasExplicitZIndex(hostEl),
|
|
173
|
-
stackingContextId,
|
|
174
|
-
parentCompositionId,
|
|
175
|
-
compositionAncestors,
|
|
176
116
|
domId,
|
|
177
117
|
hfId,
|
|
178
118
|
selector,
|
|
@@ -185,6 +125,7 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
185
125
|
if (hostEl.hasAttribute("data-hidden")) entry.hidden = true;
|
|
186
126
|
const timelineRole = hostEl.getAttribute("data-timeline-role");
|
|
187
127
|
if (timelineRole) entry.timelineRole = timelineRole;
|
|
128
|
+
entry.zIndex = readTimelineElementZIndex(hostEl);
|
|
188
129
|
}
|
|
189
130
|
if (clip.assetUrl) entry.src = clip.assetUrl;
|
|
190
131
|
if (clip.kind === "composition" && clip.compositionId) {
|
|
@@ -207,8 +148,6 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
207
148
|
}
|
|
208
149
|
}
|
|
209
150
|
if (hostEl) {
|
|
210
|
-
entry.zIndex = getTimelineElementZIndex(hostEl) ?? entry.zIndex;
|
|
211
|
-
entry.hasExplicitZIndex = getTimelineElementHasExplicitZIndex(hostEl);
|
|
212
151
|
entry.domId = hostEl.id || undefined;
|
|
213
152
|
entry.hfId = hostEl.getAttribute("data-hf-id") || undefined;
|
|
214
153
|
entry.selector = getTimelineElementSelector(hostEl);
|
|
@@ -269,10 +208,10 @@ export function createImplicitTimelineLayersFromDOM(
|
|
|
269
208
|
});
|
|
270
209
|
if (existingKeys.has(identity.key) || existingKeys.has(identity.id)) continue;
|
|
271
210
|
|
|
272
|
-
const compositionContext = resolveDomCompositionContext(child, rootComp);
|
|
273
211
|
layers.push({
|
|
274
212
|
domId: child.id || undefined,
|
|
275
213
|
hfId: child.getAttribute("data-hf-id") || undefined,
|
|
214
|
+
zIndex: readTimelineElementZIndex(child),
|
|
276
215
|
duration: rootDuration,
|
|
277
216
|
id: identity.id,
|
|
278
217
|
key: identity.key,
|
|
@@ -281,11 +220,6 @@ export function createImplicitTimelineLayersFromDOM(
|
|
|
281
220
|
selectorIndex,
|
|
282
221
|
sourceFile,
|
|
283
222
|
start: 0,
|
|
284
|
-
zIndex: getTimelineElementZIndex(child),
|
|
285
|
-
hasExplicitZIndex: getTimelineElementHasExplicitZIndex(child),
|
|
286
|
-
stackingContextId: compositionContext.stackingContextId,
|
|
287
|
-
parentCompositionId: compositionContext.parentCompositionId,
|
|
288
|
-
compositionAncestors: compositionContext.compositionAncestors,
|
|
289
223
|
tag: child.tagName.toLowerCase(),
|
|
290
224
|
timingSource: "implicit",
|
|
291
225
|
track: maxTrack + 1 + layers.length,
|
|
@@ -347,7 +281,6 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
347
281
|
selectorIndex,
|
|
348
282
|
sourceFile,
|
|
349
283
|
});
|
|
350
|
-
const compositionContext = resolveDomCompositionContext(el, rootComp);
|
|
351
284
|
const entry: TimelineElement = {
|
|
352
285
|
id: identity.id,
|
|
353
286
|
label,
|
|
@@ -356,17 +289,13 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
356
289
|
start,
|
|
357
290
|
duration: dur,
|
|
358
291
|
track: isNaN(track) ? 0 : track,
|
|
359
|
-
zIndex: getTimelineElementZIndex(el),
|
|
360
|
-
hasExplicitZIndex: getTimelineElementHasExplicitZIndex(el),
|
|
361
|
-
stackingContextId: compositionContext.stackingContextId,
|
|
362
|
-
parentCompositionId: compositionContext.parentCompositionId,
|
|
363
|
-
compositionAncestors: compositionContext.compositionAncestors,
|
|
364
292
|
domId: el.id || undefined,
|
|
365
293
|
hfId: el.getAttribute("data-hf-id") || undefined,
|
|
366
294
|
selector,
|
|
367
295
|
selectorIndex,
|
|
368
296
|
sourceFile,
|
|
369
297
|
timingSource: "authored",
|
|
298
|
+
zIndex: readTimelineElementZIndex(el),
|
|
370
299
|
};
|
|
371
300
|
|
|
372
301
|
const mediaEl = resolveMediaElement(el);
|
|
@@ -437,7 +366,14 @@ export function mergeTimelineElementsPreservingDowngrades(
|
|
|
437
366
|
|
|
438
367
|
const nextIdentities = new Set(nextElements.map(getTimelineElementIdentity));
|
|
439
368
|
const preserved = currentElements.filter(
|
|
440
|
-
(element) =>
|
|
369
|
+
(element) =>
|
|
370
|
+
!nextIdentities.has(getTimelineElementIdentity(element)) &&
|
|
371
|
+
// Only preserve enriched sub-composition children (compositionSrc set),
|
|
372
|
+
// which a bare DOM re-scan legitimately drops and enrichMissingCompositions
|
|
373
|
+
// re-adds. A TOP-LEVEL element missing from the fresh scan was genuinely
|
|
374
|
+
// removed (undo of a split, a delete), so let it go — otherwise undoing a
|
|
375
|
+
// split leaves a ghost clip in the timeline even though the file is reverted.
|
|
376
|
+
element.compositionSrc != null,
|
|
441
377
|
);
|
|
442
378
|
if (preserved.length === 0) return nextElements;
|
|
443
379
|
return [...nextElements, ...preserved];
|
|
@@ -481,11 +417,6 @@ export function buildStandaloneRootTimelineElement(params: {
|
|
|
481
417
|
start: 0,
|
|
482
418
|
duration: params.rootDuration,
|
|
483
419
|
track: 0,
|
|
484
|
-
zIndex: 0,
|
|
485
|
-
hasExplicitZIndex: false,
|
|
486
|
-
stackingContextId: params.compositionId,
|
|
487
|
-
parentCompositionId: null,
|
|
488
|
-
compositionAncestors: [params.compositionId],
|
|
489
420
|
compositionSrc,
|
|
490
421
|
selector: params.selector,
|
|
491
422
|
selectorIndex: params.selectorIndex,
|
|
@@ -15,6 +15,31 @@ import { isFinitePositive } from "./playbackAdapter";
|
|
|
15
15
|
// Duration attribute helpers
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Read a host element's effective CSS stacking order for the timeline's reverse
|
|
20
|
+
* z→lane mapping. Prefers the inline `style.zIndex` (what the canvas context
|
|
21
|
+
* menu and LayersPanel z-edits write via handleDomZIndexReorderCommit), falls
|
|
22
|
+
* back to computed style; "auto" / empty / unparseable ⇒ 0. Works with a
|
|
23
|
+
* detached parse Document (no defaultView) as well as a live iframe. Mirrors
|
|
24
|
+
* canvasContextMenuZOrder.parseZIndex semantics so the two directions agree.
|
|
25
|
+
*/
|
|
26
|
+
export function readTimelineElementZIndex(el: Element): number {
|
|
27
|
+
const html = el as HTMLElement;
|
|
28
|
+
const parseZ = (value: string | null | undefined): number | null => {
|
|
29
|
+
if (value == null || value === "" || value === "auto") return null;
|
|
30
|
+
const n = Number.parseInt(value, 10);
|
|
31
|
+
return Number.isFinite(n) ? n : null;
|
|
32
|
+
};
|
|
33
|
+
const fromInline = parseZ(html.style?.zIndex);
|
|
34
|
+
if (fromInline != null) return fromInline;
|
|
35
|
+
const view = el.ownerDocument?.defaultView;
|
|
36
|
+
if (view?.getComputedStyle) {
|
|
37
|
+
const fromComputed = parseZ(view.getComputedStyle(html).zIndex);
|
|
38
|
+
if (fromComputed != null) return fromComputed;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
function readDurationAttribute(el: Element | null | undefined): number {
|
|
19
44
|
if (!el) return 0;
|
|
20
45
|
const duration =
|
|
@@ -36,14 +61,20 @@ export function isTimelineIgnoredElement(el: Element): boolean {
|
|
|
36
61
|
);
|
|
37
62
|
}
|
|
38
63
|
|
|
39
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Furthest clip end (start + RAW `data-duration`) over every non-root clip in the
|
|
66
|
+
* document. Reads the authored attribute, NOT any runtime-computed value — so it
|
|
67
|
+
* is immune to the runtime's clamp that truncates a clip's live duration to the
|
|
68
|
+
* composition length. This is the source of truth for content-driven duration:
|
|
69
|
+
* computing it from the store instead would feed the truncated value back in and
|
|
70
|
+
* make the composition length ratchet down (research HANDOFF-3 §6.1 feedback loop).
|
|
71
|
+
*/
|
|
72
|
+
export function furthestClipEndFromDocument(doc: Document | null | undefined): number {
|
|
40
73
|
if (!doc) return 0;
|
|
41
|
-
const
|
|
42
|
-
if (rootDuration > 0) return rootDuration;
|
|
43
|
-
|
|
74
|
+
const root = doc.querySelector("[data-composition-id]");
|
|
44
75
|
let maxEnd = 0;
|
|
45
76
|
for (const node of Array.from(doc.querySelectorAll("[data-start]"))) {
|
|
46
|
-
if (isTimelineIgnoredElement(node)) continue;
|
|
77
|
+
if (node === root || isTimelineIgnoredElement(node)) continue;
|
|
47
78
|
const start = Number.parseFloat(node.getAttribute("data-start") ?? "");
|
|
48
79
|
const duration = readDurationAttribute(node);
|
|
49
80
|
if (!Number.isFinite(start) || start < 0 || duration <= 0) continue;
|
|
@@ -52,6 +83,24 @@ export function readTimelineDurationFromDocument(doc: Document | null | undefine
|
|
|
52
83
|
return maxEnd;
|
|
53
84
|
}
|
|
54
85
|
|
|
86
|
+
export function readTimelineDurationFromDocument(doc: Document | null | undefined): number {
|
|
87
|
+
if (!doc) return 0;
|
|
88
|
+
const rootDuration = readDurationAttribute(doc.querySelector("[data-composition-id]"));
|
|
89
|
+
if (rootDuration > 0) return rootDuration;
|
|
90
|
+
return furthestClipEndFromDocument(doc);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Furthest clip end parsed straight from a composition SOURCE STRING (the HTML
|
|
95
|
+
* being saved). Uses raw `data-duration`, so it is the correct input for syncing
|
|
96
|
+
* the root duration after an edit — reading the store instead would use the
|
|
97
|
+
* runtime-truncated durations and shrink the composition (the feedback loop).
|
|
98
|
+
*/
|
|
99
|
+
export function furthestClipEndFromSource(source: string): number {
|
|
100
|
+
if (!source) return 0;
|
|
101
|
+
return furthestClipEndFromDocument(new DOMParser().parseFromString(source, "text/html"));
|
|
102
|
+
}
|
|
103
|
+
|
|
55
104
|
// ---------------------------------------------------------------------------
|
|
56
105
|
// DOM element type guards
|
|
57
106
|
// ---------------------------------------------------------------------------
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getTimelineElementSelectorIndex,
|
|
19
19
|
getTimelineElementDisplayLabel,
|
|
20
20
|
buildTimelineElementIdentity,
|
|
21
|
+
readTimelineElementZIndex,
|
|
21
22
|
} from "./timelineElementHelpers";
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
@@ -377,6 +378,7 @@ export function buildMissingCompositionElements(
|
|
|
377
378
|
selector,
|
|
378
379
|
selectorIndex,
|
|
379
380
|
sourceFile,
|
|
381
|
+
zIndex: readTimelineElementZIndex(el),
|
|
380
382
|
};
|
|
381
383
|
if (compSrc) {
|
|
382
384
|
entry.compositionSrc = compSrc;
|
|
@@ -3,6 +3,7 @@ import type { MusicBeatAnalysis } from "@hyperframes/core/beats";
|
|
|
3
3
|
import type { BeatEditState } from "../../utils/beatEditing";
|
|
4
4
|
import type { ClipManifestClip } from "../lib/playbackTypes";
|
|
5
5
|
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
6
|
+
import { computePinnedZoomPercent } from "../components/timelineZoom";
|
|
6
7
|
|
|
7
8
|
/** Minimal keyframe cache types — mirrors GsapKeyframesData without pulling in Node-only gsap-parser. */
|
|
8
9
|
export interface KeyframeCacheEntry {
|
|
@@ -75,6 +76,10 @@ export interface TimelineElement {
|
|
|
75
76
|
export type ZoomMode = "fit" | "manual";
|
|
76
77
|
type TimelineTool = "select" | "razor";
|
|
77
78
|
|
|
79
|
+
export interface SelectElementOptions {
|
|
80
|
+
preserveSet?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
function resolveElementSelection(
|
|
79
84
|
ids: Iterable<string>,
|
|
80
85
|
anchor?: string | null,
|
|
@@ -142,6 +147,25 @@ interface PlayerState {
|
|
|
142
147
|
|
|
143
148
|
/** Multi-select: additional selected elements beyond selectedElementId. */
|
|
144
149
|
selectedElementIds: Set<string>;
|
|
150
|
+
clearSelectedElementIds: () => void;
|
|
151
|
+
/** Replace the whole multi-selection at once (marquee live updates). */
|
|
152
|
+
setSelectedElementIds: (ids: Set<string>) => void;
|
|
153
|
+
/** Timeline magnet toggle — when false, clip drags/trims/drops never snap. */
|
|
154
|
+
timelineSnapEnabled: boolean;
|
|
155
|
+
setTimelineSnapEnabled: (enabled: boolean) => void;
|
|
156
|
+
/**
|
|
157
|
+
* Pin the timeline zoom to its current visual scale before a duration-changing
|
|
158
|
+
* edit, so a subsequent duration change (which recomputes fit-pps) stops
|
|
159
|
+
* rescaling every clip. No-op once already pinned (mode is "manual").
|
|
160
|
+
*/
|
|
161
|
+
pinTimelineZoom: (currentPixelsPerSecond: number, fitPixelsPerSecond: number) => void;
|
|
162
|
+
/**
|
|
163
|
+
* The timeline's live pixels-per-second + fit basis, published by <Timeline> on
|
|
164
|
+
* every render. Non-reactive scratch state (never read as a render input).
|
|
165
|
+
*/
|
|
166
|
+
timelinePps: number;
|
|
167
|
+
timelineFitPps: number;
|
|
168
|
+
setTimelineScale: (pps: number, fitPps: number) => void;
|
|
145
169
|
setSelection: (ids: Iterable<string>, anchor?: string | null) => void;
|
|
146
170
|
addSelectedElementId: (id: string) => void;
|
|
147
171
|
toggleSelectedElementId: (id: string) => void;
|
|
@@ -160,7 +184,7 @@ interface PlayerState {
|
|
|
160
184
|
setTimelineReady: (ready: boolean) => void;
|
|
161
185
|
setBeatDragging: (dragging: boolean) => void;
|
|
162
186
|
setElements: (elements: TimelineElement[]) => void;
|
|
163
|
-
setSelectedElementId: (id: string | null) => void;
|
|
187
|
+
setSelectedElementId: (id: string | null, options?: SelectElementOptions) => void;
|
|
164
188
|
/** Move the selection anchor within an active multi-selection without collapsing it. */
|
|
165
189
|
setSelectionAnchor: (id: string | null) => void;
|
|
166
190
|
updateElement: (
|
|
@@ -260,6 +284,8 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
260
284
|
loopEnabled: false,
|
|
261
285
|
zoomMode: "fit",
|
|
262
286
|
manualZoomPercent: 100,
|
|
287
|
+
timelinePps: 100,
|
|
288
|
+
timelineFitPps: 100,
|
|
263
289
|
inPoint: null,
|
|
264
290
|
outPoint: null,
|
|
265
291
|
|
|
@@ -383,6 +409,32 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
383
409
|
},
|
|
384
410
|
setLoopEnabled: (enabled) => set({ loopEnabled: enabled }),
|
|
385
411
|
setZoomMode: (mode) => set({ zoomMode: mode }),
|
|
412
|
+
clearSelectedElementIds: () => set({ selectedElementIds: new Set() }),
|
|
413
|
+
setSelectedElementIds: (ids: Set<string>) => set({ selectedElementIds: new Set(ids) }),
|
|
414
|
+
timelineSnapEnabled: readStudioUiPreferences().timelineSnapEnabled ?? true,
|
|
415
|
+
setTimelineSnapEnabled: (enabled) => {
|
|
416
|
+
writeStudioUiPreferences({ timelineSnapEnabled: enabled });
|
|
417
|
+
set({ timelineSnapEnabled: enabled });
|
|
418
|
+
},
|
|
419
|
+
pinTimelineZoom: (currentPixelsPerSecond, fitPixelsPerSecond) =>
|
|
420
|
+
set((s) => {
|
|
421
|
+
// Already pinned (or the user manually zoomed) — never clobber that.
|
|
422
|
+
if (s.zoomMode === "manual") return {};
|
|
423
|
+
const percent = computePinnedZoomPercent(currentPixelsPerSecond, fitPixelsPerSecond);
|
|
424
|
+
writeStudioUiPreferences({
|
|
425
|
+
timelineZoomMode: "manual",
|
|
426
|
+
timelineManualZoomPercent: percent,
|
|
427
|
+
});
|
|
428
|
+
return { zoomMode: "manual", manualZoomPercent: percent };
|
|
429
|
+
}),
|
|
430
|
+
setTimelineScale: (pps, fitPps) => {
|
|
431
|
+
// Non-reactive publish: mutate in place + reuse the same object identity so no
|
|
432
|
+
// subscriber re-renders (these fields are never a render input, only read
|
|
433
|
+
// imperatively before pinning).
|
|
434
|
+
const state = get();
|
|
435
|
+
state.timelinePps = pps;
|
|
436
|
+
state.timelineFitPps = fitPps;
|
|
437
|
+
},
|
|
386
438
|
setInPoint: (time) =>
|
|
387
439
|
set((state) => {
|
|
388
440
|
const t = time !== null && Number.isFinite(time) ? time : null;
|
|
@@ -414,9 +466,16 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
414
466
|
// A genuine single selection: always collapse the set to just this element. User
|
|
415
467
|
// intent (timeline click, preview click via applyDomSelection) flows here; DOM sync
|
|
416
468
|
// echoes that must preserve a group go through setSelectionAnchor instead.
|
|
417
|
-
setSelectedElementId: (id) =>
|
|
469
|
+
setSelectedElementId: (id, options) =>
|
|
418
470
|
set((s) => {
|
|
419
|
-
const
|
|
471
|
+
const preserveSet = Boolean(options?.preserveSet && id && s.selectedElementIds.has(id));
|
|
472
|
+
const selectedElementIds = preserveSet
|
|
473
|
+
? new Set(s.selectedElementIds)
|
|
474
|
+
: options?.preserveSet
|
|
475
|
+
? new Set<string>()
|
|
476
|
+
: id
|
|
477
|
+
? new Set([id])
|
|
478
|
+
: new Set<string>();
|
|
420
479
|
// Selecting a different element drops any active keyframe selection — otherwise
|
|
421
480
|
// a stale activeKeyframePct from a prior diamond click would force the next drag
|
|
422
481
|
// to "modify" a keyframe on the new element. A diamond click sets the pct AFTER
|
package/src/styles/studio.css
CHANGED
|
@@ -141,6 +141,22 @@ body {
|
|
|
141
141
|
opacity: 1;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/* Audio clips read as a distinct kind — a persistent violet tint vs the teal-
|
|
145
|
+
accented visual clips — so the audio zone at the bottom is visually separate
|
|
146
|
+
at a glance (the timeline's kinds: visual on top, audio below). */
|
|
147
|
+
.timeline-clip.is-audio {
|
|
148
|
+
background-color: rgba(167, 139, 250, 0.16);
|
|
149
|
+
border-color: rgba(167, 139, 250, 0.4);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.timeline-clip.is-audio.is-hovered {
|
|
153
|
+
background-color: rgba(167, 139, 250, 0.24);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.timeline-clip.is-audio.is-dragging {
|
|
157
|
+
background-color: rgba(60, 52, 84, 0.96);
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
.timeline-clip.is-hovered {
|
|
145
161
|
background-color: rgba(255, 255, 255, 0.09);
|
|
146
162
|
}
|
|
@@ -158,9 +174,20 @@ body {
|
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
.timeline-clip.is-dragging {
|
|
177
|
+
/* Solid background so the picked-up clip reads clearly while dragging — audio
|
|
178
|
+
clips (waveform drawn on a separate layer) would otherwise look transparent. */
|
|
179
|
+
background-color: rgba(38, 42, 52, 0.96);
|
|
161
180
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
162
181
|
}
|
|
163
182
|
|
|
183
|
+
/* Keep the white selection outline while dragging (a plain box-shadow alone gets
|
|
184
|
+
overridden by .is-dragging's drop shadow, so re-state both together). */
|
|
185
|
+
.timeline-clip.is-selected.is-dragging {
|
|
186
|
+
box-shadow:
|
|
187
|
+
0 0 0 1.5px rgba(255, 255, 255, 0.85),
|
|
188
|
+
0 8px 24px rgba(0, 0, 0, 0.4);
|
|
189
|
+
}
|
|
190
|
+
|
|
164
191
|
.timeline-clip[data-active].is-selected.is-dragging {
|
|
165
192
|
box-shadow:
|
|
166
193
|
0 0 0 1.5px rgba(255, 255, 255, 0.85),
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny Zustand slice that carries the "asset preview overlay" state.
|
|
3
|
+
*
|
|
4
|
+
* When a user clicks an asset card that has NOT yet been added to the
|
|
5
|
+
* timeline the overlay fires up: a dark scrim + centered media element
|
|
6
|
+
* (img / video / audio) + filename label rendered inside PreviewPane.
|
|
7
|
+
*
|
|
8
|
+
* State lives here so AssetsTab (sidebar) and PreviewPane (preview column)
|
|
9
|
+
* can communicate without prop-drilling through the multi-layer EditorShell
|
|
10
|
+
* tree. The store is project-scoped: NLEProvider (NLEContext.tsx) clears it
|
|
11
|
+
* whenever `projectId` changes, so a preview opened in one project can't
|
|
12
|
+
* bleed into another (the overlay itself stays mounted across project
|
|
13
|
+
* switches — EditorShell isn't keyed by projectId).
|
|
14
|
+
*/
|
|
15
|
+
import { create } from "zustand";
|
|
16
|
+
|
|
17
|
+
interface AssetPreviewState {
|
|
18
|
+
/** Project-relative asset path currently being previewed, or null. */
|
|
19
|
+
previewAsset: string | null;
|
|
20
|
+
/** projectId for which the preview was opened (used to build the serve URL). */
|
|
21
|
+
previewProjectId: string | null;
|
|
22
|
+
/** Open a media preview for the given asset. */
|
|
23
|
+
setPreviewAsset: (asset: string, projectId: string) => void;
|
|
24
|
+
/** Close the preview overlay. */
|
|
25
|
+
clearPreviewAsset: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const useAssetPreviewStore = create<AssetPreviewState>((set) => ({
|
|
29
|
+
previewAsset: null,
|
|
30
|
+
previewProjectId: null,
|
|
31
|
+
setPreviewAsset: (asset, projectId) => set({ previewAsset: asset, previewProjectId: projectId }),
|
|
32
|
+
clearPreviewAsset: () => set({ previewAsset: null, previewProjectId: null }),
|
|
33
|
+
}));
|
|
@@ -2,9 +2,10 @@ import type { RegistryItem } from "@hyperframes/core/registry";
|
|
|
2
2
|
import type { TimelineElement } from "../player";
|
|
3
3
|
import {
|
|
4
4
|
insertTimelineAssetIntoSource,
|
|
5
|
-
|
|
5
|
+
resolveTimelineAssetCompositionSize,
|
|
6
6
|
} from "./timelineAssetDrop";
|
|
7
7
|
import { collectHtmlIds } from "./studioHelpers";
|
|
8
|
+
import { generateId } from "./generateId";
|
|
8
9
|
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
|
9
10
|
import { saveProjectFilesWithHistory } from "./studioFileHistory";
|
|
10
11
|
import type { EditHistoryKind } from "./editHistory";
|
|
@@ -120,7 +121,9 @@ export async function addBlockToProject(
|
|
|
120
121
|
);
|
|
121
122
|
|
|
122
123
|
const isBlock = block.type === "hyperframes:block";
|
|
123
|
-
const
|
|
124
|
+
const { width: hostWidth, height: hostHeight } =
|
|
125
|
+
resolveTimelineAssetCompositionSize(originalContent);
|
|
126
|
+
const hostDims = { left: 0, top: 0, width: hostWidth, height: hostHeight };
|
|
124
127
|
|
|
125
128
|
const currentTime = opts.currentTime ?? 0;
|
|
126
129
|
const start = placement
|
|
@@ -152,6 +155,11 @@ export async function addBlockToProject(
|
|
|
152
155
|
|
|
153
156
|
const subCompHtml = [
|
|
154
157
|
`<div`,
|
|
158
|
+
// A stable id (+ hf-id) is what authored sub-comps carry; without it the
|
|
159
|
+
// timeline can't dedup the host and renders duplicate clips that multiply
|
|
160
|
+
// on every interaction. Matches the authored-comp shape.
|
|
161
|
+
` id="${compId}"`,
|
|
162
|
+
` data-hf-id="hf-${generateId()}"`,
|
|
155
163
|
` data-composition-id="${compId}"`,
|
|
156
164
|
` data-composition-src="${compositionFile}"`,
|
|
157
165
|
` data-start="${formatTimelineAttributeNumber(start)}"`,
|
|
@@ -209,6 +209,41 @@ describe("edit history", () => {
|
|
|
209
209
|
expect(state.undo[0].files["index.html"].after).toBe("c");
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
+
it("merges a lane-change move with its z-reorder past the default window via entry coalesceMs", () => {
|
|
213
|
+
// The z entry records only after the move persist's round-trip — often >300ms.
|
|
214
|
+
// Both sides pass coalesceMs: 5000 with the shared gesture key so the pair
|
|
215
|
+
// still folds into ONE undo step.
|
|
216
|
+
const move = buildEditHistoryEntry({
|
|
217
|
+
projectId: "project-1",
|
|
218
|
+
label: "Move timeline clips",
|
|
219
|
+
kind: "timeline",
|
|
220
|
+
coalesceKey: "clip-lane-move:1",
|
|
221
|
+
coalesceMs: 5000,
|
|
222
|
+
files: { "index.html": { before: "a", after: "b" } },
|
|
223
|
+
now: 100,
|
|
224
|
+
id: "move-entry",
|
|
225
|
+
});
|
|
226
|
+
const zReorder = buildEditHistoryEntry({
|
|
227
|
+
projectId: "project-1",
|
|
228
|
+
label: "Reorder layers",
|
|
229
|
+
kind: "manual",
|
|
230
|
+
coalesceKey: "clip-lane-move:1",
|
|
231
|
+
coalesceMs: 5000,
|
|
232
|
+
files: { "index.html": { before: "b", after: "c" } },
|
|
233
|
+
now: 500,
|
|
234
|
+
id: "z-entry",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const state = pushEditHistoryEntry(
|
|
238
|
+
pushEditHistoryEntry(createEmptyEditHistory(), move),
|
|
239
|
+
zReorder,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(state.undo).toHaveLength(1);
|
|
243
|
+
expect(state.undo[0].files["index.html"].before).toBe("a");
|
|
244
|
+
expect(state.undo[0].files["index.html"].after).toBe("c");
|
|
245
|
+
});
|
|
246
|
+
|
|
212
247
|
it("folds a slow GSAP follow-up into the timing edit via a per-entry coalesceMs override", () => {
|
|
213
248
|
const timing = buildEditHistoryEntry({
|
|
214
249
|
projectId: "project-1",
|