@hyperframes/studio 0.7.56 → 0.7.58
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-CtTDO63S.js +459 -0
- package/dist/assets/{index-CmVCjZjp.js → index-B_UvTX3E.js} +205 -205
- package/dist/assets/{index-BWnOxAiH.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-C4csZims.js → index-DeQPzqwH.js} +1 -1
- package/dist/assets/index-uahwWkgw.css +1 -0
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +41 -4
- package/dist/index.html +2 -2
- package/dist/index.js +7436 -5423
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/editor/CanvasContextMenu.test.tsx +166 -2
- package/src/components/editor/CanvasContextMenu.tsx +100 -20
- package/src/components/editor/DomEditOverlay.tsx +34 -62
- package/src/components/editor/LayersPanel.tsx +111 -91
- package/src/components/editor/canvasContextMenuZOrder.test.ts +243 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +184 -23
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +61 -37
- package/src/components/nle/PreviewOverlays.tsx +61 -16
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +19 -2
- package/src/components/sidebar/AudioRow.tsx +19 -2
- package/src/hooks/domEditCommitTypes.ts +29 -2
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +183 -1
- package/src/hooks/timelineEditingHelpers.ts +63 -259
- package/src/hooks/timelineMoveAdapter.test.ts +44 -40
- package/src/hooks/timelineMoveAdapter.ts +7 -4
- package/src/hooks/timelineTimingSync.test.ts +820 -0
- package/src/hooks/timelineTimingSync.ts +597 -0
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useDomEditCommits.test.tsx +392 -23
- package/src/hooks/useDomEditCommits.ts +180 -53
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +559 -28
- package/src/hooks/useElementLifecycleOps.ts +175 -69
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +591 -277
- package/src/hooks/useTimelineEditing.ts +202 -282
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +187 -134
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +7 -2
- package/src/player/components/Timeline.test.ts +105 -18
- package/src/player/components/Timeline.tsx +69 -98
- package/src/player/components/TimelineCanvas.tsx +47 -7
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +70 -47
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +370 -11
- package/src/player/components/timelineClipDragCommit.ts +267 -68
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +119 -30
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +102 -1
- package/src/player/components/timelineStackingSync.ts +45 -5
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +11 -2
- package/src/player/components/useTimelineStackingSync.ts +17 -4
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +19 -1
- 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 +4 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +11 -1
- package/src/player/index.ts +3 -1
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/lib/timelineElementHelpers.ts +54 -11
- package/src/player/lib/timelineIframeHelpers.ts +2 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +49 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +62 -86
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-D-GyYi2d.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -11,6 +11,14 @@ import {
|
|
|
11
11
|
readHfId,
|
|
12
12
|
type DomEditSelection,
|
|
13
13
|
} from "../components/editor/domEditing";
|
|
14
|
+
import { LAYER_REVEAL_PRIOR_POSITION_ATTR } from "../player/lib/timelineElementHelpers";
|
|
15
|
+
import {
|
|
16
|
+
beginLayerRevealCommit,
|
|
17
|
+
beginLayerZPersist,
|
|
18
|
+
completeLayerRevealCommit,
|
|
19
|
+
rollbackLayerRevealCommit,
|
|
20
|
+
type LayerRevealCommitOwnership,
|
|
21
|
+
} from "../components/editor/useLayerRevealOverride";
|
|
14
22
|
import type { CommitDomEditPatchBatches, DomEditPatchBatch } from "./domEditCommitTypes";
|
|
15
23
|
|
|
16
24
|
interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
|
|
@@ -25,6 +33,36 @@ interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
|
|
|
25
33
|
onElementDeleted?: (selection: DomEditSelection) => void;
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
// One coalesce key per z-reorder gesture. A monotonic counter — NOT Date.now()
|
|
37
|
+
// / Math.random(), which the determinism rules forbid — matches the
|
|
38
|
+
// laneChangeGestureSeq precedent in timelineClipDragCommit.ts: the key only has
|
|
39
|
+
// to be unique per gesture and identical across the gesture's records.
|
|
40
|
+
let zReorderGestureSeq = 0;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Undo coalesce key for ONE z-reorder gesture — unique per call. The key
|
|
44
|
+
* carries the action kind + element ids for debuggability, plus a gesture
|
|
45
|
+
* sequence so two SEPARATE user actions (even the same action on the same
|
|
46
|
+
* selection) never share a key. That uniqueness is what makes the unbounded
|
|
47
|
+
* per-gesture coalesce window (see handleDomZIndexReorderCommit) safe: the
|
|
48
|
+
* fold can only ever merge records of the SAME gesture.
|
|
49
|
+
*
|
|
50
|
+
* Exported as THE single implementation of the key: the canvas z-order wiring
|
|
51
|
+
* (PreviewOverlays) mints it once per gesture and passes the same instance to
|
|
52
|
+
* both the z persist and the timeline lane mirror (useCanvasZOrderTimelineMirror)
|
|
53
|
+
* so editHistory folds the z write and the track write into one undo entry —
|
|
54
|
+
* recomputing the key per record would silently split the undo.
|
|
55
|
+
*/
|
|
56
|
+
export function zReorderCoalesceKey(
|
|
57
|
+
entries: ReadonlyArray<{ element: HTMLElement; id?: string; selector?: string }>,
|
|
58
|
+
actionKind?: string,
|
|
59
|
+
): string {
|
|
60
|
+
const ids = entries
|
|
61
|
+
.map((e) => e.id ?? e.selector ?? e.element.getAttribute("data-hf-id") ?? "el")
|
|
62
|
+
.join(":");
|
|
63
|
+
return `z-reorder:${actionKind ?? "reorder"}:${ids}:g${zReorderGestureSeq++}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
28
66
|
export function useElementLifecycleOps({
|
|
29
67
|
activeCompPath,
|
|
30
68
|
showToast,
|
|
@@ -130,11 +168,11 @@ export function useElementLifecycleOps({
|
|
|
130
168
|
],
|
|
131
169
|
);
|
|
132
170
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
171
|
+
// Z-index reorder folds patches by source file, then sends one aggregate cross-file
|
|
172
|
+
// patch-element-batches request. The server refuses the whole gesture on any unmatched
|
|
173
|
+
// target and rolls back earlier file writes if a later write fails.
|
|
135
174
|
// No SDK reorder/reparent op exists; DOM sibling order stays server-authoritative if ever needed.
|
|
136
175
|
const handleDomZIndexReorderCommit = useCallback(
|
|
137
|
-
// fallow-ignore-next-line complexity
|
|
138
176
|
(
|
|
139
177
|
entries: Array<{
|
|
140
178
|
element: HTMLElement;
|
|
@@ -146,78 +184,146 @@ export function useElementLifecycleOps({
|
|
|
146
184
|
key?: string;
|
|
147
185
|
}>,
|
|
148
186
|
gestureCoalesceKey?: string,
|
|
187
|
+
actionKind?: string,
|
|
149
188
|
) => {
|
|
150
189
|
if (entries.length === 0) return Promise.resolve();
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
gestureCoalesceKey ??
|
|
158
|
-
`z-reorder:${entries.map((e) => e.id ?? e.selector ?? e.element.getAttribute("data-hf-id") ?? "el").join(":")}`;
|
|
159
|
-
const patchesBySourceFile = new Map<string, DomEditPatchBatch["patches"]>();
|
|
160
|
-
const rollbacks: Array<() => void> = [];
|
|
161
|
-
for (const entry of entries) {
|
|
162
|
-
const priorZIndex = entry.element.style.zIndex;
|
|
163
|
-
const priorPosition = entry.element.style.position;
|
|
164
|
-
const priorStoreEntry = entry.key
|
|
165
|
-
? usePlayerStore.getState().elements.find((el) => (el.key ?? el.id) === entry.key)
|
|
166
|
-
: undefined;
|
|
167
|
-
let positionChanged = false;
|
|
168
|
-
entry.element.style.zIndex = String(entry.zIndex);
|
|
169
|
-
const patches: Array<{ type: "inline-style"; property: string; value: string }> = [
|
|
170
|
-
{ type: "inline-style", property: "z-index", value: String(entry.zIndex) },
|
|
171
|
-
];
|
|
190
|
+
// One async owner must bracket reveal tokens, optimistic DOM/store state,
|
|
191
|
+
// atomic persistence, rollback, and the final persistence-count release.
|
|
192
|
+
// Splitting those phases would make transaction ownership less explicit.
|
|
193
|
+
// fallow-ignore-next-line complexity
|
|
194
|
+
return (async () => {
|
|
195
|
+
const releaseZPersists = entries.map((entry) => beginLayerZPersist(entry.element));
|
|
172
196
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
197
|
+
// Resolver shadow (telemetry-only, decoupled from cutover): record whether
|
|
198
|
+
// the SDK resolves each reordered element — the reorderElements op's targets.
|
|
199
|
+
onReorderShadow?.(
|
|
200
|
+
entries.map((e) => readHfId(e.element)).filter((id): id is string => id != null),
|
|
201
|
+
);
|
|
202
|
+
// The default key carries the action kind so two DIFFERENT actions on the
|
|
203
|
+
// same element set (e.g. "bring-forward" then "send-backward" within the
|
|
204
|
+
// coalesce window) never merge into one undo step. Callers that share a
|
|
205
|
+
// gesture (lane moves) pass an explicit gestureCoalesceKey instead.
|
|
206
|
+
const coalesceKey = gestureCoalesceKey ?? zReorderCoalesceKey(entries, actionKind);
|
|
207
|
+
const patchesBySourceFile = new Map<string, DomEditPatchBatch["patches"]>();
|
|
208
|
+
const rollbacks: Array<() => void> = [];
|
|
209
|
+
const revealCommits: Array<{
|
|
210
|
+
element: HTMLElement;
|
|
211
|
+
ownership: LayerRevealCommitOwnership;
|
|
212
|
+
}> = [];
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
const priorZIndex = entry.element.style.zIndex;
|
|
215
|
+
const priorPosition = entry.element.style.position;
|
|
216
|
+
const priorStoreEntry = entry.key
|
|
217
|
+
? usePlayerStore.getState().elements.find((el) => (el.key ?? el.id) === entry.key)
|
|
218
|
+
: undefined;
|
|
219
|
+
let positionChanged = false;
|
|
220
|
+
// An active Layers-panel reveal lift on this element is consumed by
|
|
221
|
+
// this commit: the new z is the truth. Read the parked TRUE position
|
|
222
|
+
// for the static check below (the lift set a temporary
|
|
223
|
+
// position:relative that would otherwise mask the need to persist
|
|
224
|
+
// one), then drop the lift attributes so z readers stop reporting the
|
|
225
|
+
// stale prior (see useLayerRevealOverride / readLayerRevealPriorZ).
|
|
226
|
+
const liftPriorPosition = entry.element.getAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR);
|
|
227
|
+
const revealOwnership = beginLayerRevealCommit(entry.element);
|
|
228
|
+
if (revealOwnership) {
|
|
229
|
+
revealCommits.push({ element: entry.element, ownership: revealOwnership });
|
|
230
|
+
}
|
|
231
|
+
entry.element.style.zIndex = String(entry.zIndex);
|
|
232
|
+
const patches: Array<{ type: "inline-style"; property: string; value: string }> = [
|
|
233
|
+
{ type: "inline-style", property: "z-index", value: String(entry.zIndex) },
|
|
234
|
+
];
|
|
235
|
+
try {
|
|
236
|
+
const win = entry.element.ownerDocument?.defaultView;
|
|
237
|
+
const effectivePosition =
|
|
238
|
+
liftPriorPosition ??
|
|
239
|
+
(win ? win.getComputedStyle(entry.element).position : undefined);
|
|
240
|
+
if (effectivePosition === "static") {
|
|
241
|
+
entry.element.style.position = "relative";
|
|
242
|
+
positionChanged = true;
|
|
243
|
+
patches.push({ type: "inline-style", property: "position", value: "relative" });
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
/* cross-origin or detached — skip */
|
|
247
|
+
}
|
|
248
|
+
if (entry.key) {
|
|
249
|
+
usePlayerStore
|
|
250
|
+
.getState()
|
|
251
|
+
.updateElement(entry.key, { zIndex: entry.zIndex, hasExplicitZIndex: true });
|
|
252
|
+
}
|
|
253
|
+
rollbacks.push(() => {
|
|
254
|
+
if (revealOwnership) {
|
|
255
|
+
rollbackLayerRevealCommit(entry.element, revealOwnership);
|
|
256
|
+
} else {
|
|
257
|
+
entry.element.style.zIndex = priorZIndex;
|
|
258
|
+
if (positionChanged) entry.element.style.position = priorPosition;
|
|
259
|
+
}
|
|
260
|
+
if (entry.key && priorStoreEntry) {
|
|
261
|
+
usePlayerStore.getState().updateElement(entry.key, {
|
|
262
|
+
zIndex: priorStoreEntry.zIndex,
|
|
263
|
+
hasExplicitZIndex: priorStoreEntry.hasExplicitZIndex,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
const filePatches = patchesBySourceFile.get(entry.sourceFile) ?? [];
|
|
268
|
+
filePatches.push({
|
|
269
|
+
target: buildDomEditPatchTarget({
|
|
270
|
+
id: entry.id,
|
|
271
|
+
hfId: readHfId(entry.element),
|
|
272
|
+
selector: entry.selector,
|
|
273
|
+
selectorIndex: entry.selectorIndex,
|
|
274
|
+
}),
|
|
275
|
+
operations: patches,
|
|
276
|
+
});
|
|
277
|
+
patchesBySourceFile.set(entry.sourceFile, filePatches);
|
|
178
278
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
279
|
+
const batches = [...patchesBySourceFile].map(([sourceFile, patches]) => ({
|
|
280
|
+
sourceFile,
|
|
281
|
+
patches,
|
|
282
|
+
}));
|
|
283
|
+
// Live z state changed with NO reload coming (skipReload below) — nudge
|
|
284
|
+
// DOM-derived views (Layers panel z-sort) to re-read the iframe.
|
|
285
|
+
usePlayerStore.getState().bumpZEditVersion();
|
|
286
|
+
const rollbackOptimisticState = () => {
|
|
287
|
+
for (const rollback of rollbacks) rollback();
|
|
288
|
+
usePlayerStore.getState().bumpZEditVersion();
|
|
289
|
+
};
|
|
290
|
+
// Resolves once every source-file batch is persisted so a same-file timing write
|
|
291
|
+
// can be ordered after it (see applyTimelineStackingReorder callers).
|
|
292
|
+
//
|
|
293
|
+
// skipReload: the live iframe DOM and the player store already hold the
|
|
294
|
+
// final z state (applied synchronously above), and the persisted patch is
|
|
295
|
+
// inline-style-only — a full iframe remount would only blink the preview.
|
|
296
|
+
// commitDomEditPatchBatches still falls back to reloading whenever the
|
|
297
|
+
// server reports an unmatched patch target (live DOM ≠ disk).
|
|
298
|
+
try {
|
|
299
|
+
const result = await commitDomEditPatchBatches(batches, {
|
|
300
|
+
label: "Reorder layers",
|
|
301
|
+
coalesceKey,
|
|
302
|
+
// Unbounded window: every key this commit records under is unique per
|
|
303
|
+
// gesture (zReorderCoalesceKey's gesture seq, or the lane drag's
|
|
304
|
+
// clip-lane-move:<seq>), so the fold can only merge records of the SAME
|
|
305
|
+
// gesture — and those records are separated by a server round-trip
|
|
306
|
+
// (move→z on a lane drag, z→lane-mirror on a canvas action), which
|
|
307
|
+
// under real network latency exceeds the 300ms default window.
|
|
308
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
309
|
+
skipReload: true,
|
|
194
310
|
});
|
|
311
|
+
if (!result.durable) {
|
|
312
|
+
rollbackOptimisticState();
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
for (const { element, ownership } of revealCommits) {
|
|
316
|
+
completeLayerRevealCommit(element, ownership);
|
|
317
|
+
}
|
|
318
|
+
return result;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
rollbackOptimisticState();
|
|
321
|
+
throw error;
|
|
195
322
|
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
id: entry.id,
|
|
201
|
-
hfId: readHfId(entry.element),
|
|
202
|
-
selector: entry.selector,
|
|
203
|
-
selectorIndex: entry.selectorIndex,
|
|
204
|
-
}),
|
|
205
|
-
operations: patches,
|
|
206
|
-
});
|
|
207
|
-
patchesBySourceFile.set(entry.sourceFile, filePatches);
|
|
208
|
-
}
|
|
209
|
-
const batches = [...patchesBySourceFile].map(([sourceFile, patches]) => ({
|
|
210
|
-
sourceFile,
|
|
211
|
-
patches,
|
|
212
|
-
}));
|
|
213
|
-
// Resolves once every source-file batch is persisted so a same-file timing write
|
|
214
|
-
// can be ordered after it (see applyTimelineStackingReorder callers).
|
|
215
|
-
return commitDomEditPatchBatches(batches, { label: "Reorder layers", coalesceKey }).catch(
|
|
216
|
-
(error) => {
|
|
217
|
-
for (const rollback of rollbacks) rollback();
|
|
218
|
-
throw error;
|
|
219
|
-
},
|
|
220
|
-
);
|
|
323
|
+
} finally {
|
|
324
|
+
for (const release of releaseZPersists) release();
|
|
325
|
+
}
|
|
326
|
+
})();
|
|
221
327
|
},
|
|
222
328
|
[commitDomEditPatchBatches, onReorderShadow],
|
|
223
329
|
);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { useState, useCallback, useRef } from "react";
|
|
2
2
|
import { useMountEffect } from "./useMountEffect";
|
|
3
3
|
import { resolveSourceFile, applyPatch } from "../utils/sourcePatcher";
|
|
4
|
+
import {
|
|
5
|
+
acceptStudioRuntimeMessage,
|
|
6
|
+
postRuntimeControlMessage,
|
|
7
|
+
} from "../player/lib/runtimeProtocol";
|
|
4
8
|
|
|
5
9
|
export interface PickedElement {
|
|
6
10
|
id: string | null;
|
|
@@ -65,10 +69,7 @@ export function useElementPicker(
|
|
|
65
69
|
|
|
66
70
|
const enablePick = useCallback(() => {
|
|
67
71
|
try {
|
|
68
|
-
getActiveIframe()?.contentWindow
|
|
69
|
-
{ source: "hf-parent", type: "control", action: "enable-pick-mode" },
|
|
70
|
-
"*",
|
|
71
|
-
);
|
|
72
|
+
postRuntimeControlMessage(getActiveIframe()?.contentWindow, "enable-pick-mode");
|
|
72
73
|
setIsPickMode(true);
|
|
73
74
|
} catch {
|
|
74
75
|
/* cross-origin */
|
|
@@ -77,10 +78,7 @@ export function useElementPicker(
|
|
|
77
78
|
|
|
78
79
|
const disablePick = useCallback(() => {
|
|
79
80
|
try {
|
|
80
|
-
getActiveIframe()?.contentWindow
|
|
81
|
-
{ source: "hf-parent", type: "control", action: "disable-pick-mode" },
|
|
82
|
-
"*",
|
|
83
|
-
);
|
|
81
|
+
postRuntimeControlMessage(getActiveIframe()?.contentWindow, "disable-pick-mode");
|
|
84
82
|
} catch {
|
|
85
83
|
/* cross-origin */
|
|
86
84
|
}
|
|
@@ -96,6 +94,7 @@ export function useElementPicker(
|
|
|
96
94
|
const handleMessage = (e: MessageEvent) => {
|
|
97
95
|
const data = e.data;
|
|
98
96
|
if (data?.source !== "hf-preview") return;
|
|
97
|
+
if (!acceptStudioRuntimeMessage(data)) return;
|
|
99
98
|
// Accept events from either the primary iframe or the active override
|
|
100
99
|
const activeIframe = getActiveIframe();
|
|
101
100
|
if (!activeIframe) return;
|
|
@@ -10,7 +10,7 @@ import type { EditHistoryKind } from "../utils/editHistory";
|
|
|
10
10
|
import { createDomEditSaveQueue } from "../utils/domEditSaveQueue";
|
|
11
11
|
import { flushStudioPendingEdits } from "../utils/studioPendingEdits";
|
|
12
12
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
13
|
-
import { applyUndoRestoreToPreview, type UndoRestoreFile } from "../utils/
|
|
13
|
+
import { applyUndoRestoreToPreview, type UndoRestoreFile } from "../utils/gsapUndoRestore";
|
|
14
14
|
import { usePlayerStore } from "../player";
|
|
15
15
|
|
|
16
16
|
/** The restore payload the undo/redo preview-sync consumes (from the history store). */
|
|
@@ -156,7 +156,7 @@ export function usePreviewPersistence({
|
|
|
156
156
|
|
|
157
157
|
// ── Queue / drain helpers ──
|
|
158
158
|
|
|
159
|
-
const queueDomEditSave = useCallback((save: () => Promise<
|
|
159
|
+
const queueDomEditSave = useCallback(<T>(save: () => Promise<T>): Promise<T> => {
|
|
160
160
|
return domEditSaveQueueRef.current?.enqueue(save) ?? save();
|
|
161
161
|
}, []);
|
|
162
162
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Asset-drop handlers for the timeline: drop an existing project asset at a
|
|
2
|
+
// placement, or upload dragged-in OS files and place them sequentially.
|
|
3
|
+
// Extracted verbatim from useTimelineEditing.ts to keep it under the studio
|
|
4
|
+
// 600-line cap.
|
|
5
|
+
import { useCallback, type MutableRefObject, type RefObject } from "react";
|
|
6
|
+
import type { TimelineElement } from "../player";
|
|
7
|
+
import {
|
|
8
|
+
buildTimelineAssetId,
|
|
9
|
+
buildTimelineAssetInsertHtml,
|
|
10
|
+
buildTimelineFileDropPlacements,
|
|
11
|
+
fitTimelineAssetGeometry,
|
|
12
|
+
getTimelineAssetKind,
|
|
13
|
+
insertTimelineAssetIntoSource,
|
|
14
|
+
resolveTimelineAssetCompositionSize,
|
|
15
|
+
resolveTimelineAssetSrc,
|
|
16
|
+
} from "../utils/timelineAssetDrop";
|
|
17
|
+
import { generateId } from "../utils/generateId";
|
|
18
|
+
import { saveProjectFilesWithHistory, type RecordEditInput } from "../utils/studioFileHistory";
|
|
19
|
+
import { collectHtmlIds, resolveDroppedAssetDuration } from "../utils/studioHelpers";
|
|
20
|
+
import { formatTimelineAttributeNumber } from "./timelineEditingHelpers";
|
|
21
|
+
import { readFileContent } from "./timelineTimingSync";
|
|
22
|
+
|
|
23
|
+
interface UseTimelineAssetDropOpsOptions {
|
|
24
|
+
projectIdRef: MutableRefObject<string | null>;
|
|
25
|
+
activeCompPath: string | null;
|
|
26
|
+
timelineElements: TimelineElement[];
|
|
27
|
+
showToast: (message: string, tone?: "error" | "info") => void;
|
|
28
|
+
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
29
|
+
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
30
|
+
domEditSaveTimestampRef: MutableRefObject<number>;
|
|
31
|
+
reloadPreview: () => void;
|
|
32
|
+
uploadProjectFiles: (files: Iterable<File>, dir?: string) => Promise<string[]>;
|
|
33
|
+
isRecordingRef?: RefObject<boolean>;
|
|
34
|
+
forceReloadSdkSession?: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useTimelineAssetDropOps({
|
|
38
|
+
projectIdRef,
|
|
39
|
+
activeCompPath,
|
|
40
|
+
timelineElements,
|
|
41
|
+
showToast,
|
|
42
|
+
writeProjectFile,
|
|
43
|
+
recordEdit,
|
|
44
|
+
domEditSaveTimestampRef,
|
|
45
|
+
reloadPreview,
|
|
46
|
+
uploadProjectFiles,
|
|
47
|
+
isRecordingRef,
|
|
48
|
+
forceReloadSdkSession,
|
|
49
|
+
}: UseTimelineAssetDropOpsOptions) {
|
|
50
|
+
// fallow-ignore-next-line complexity
|
|
51
|
+
const handleTimelineAssetDrop = useCallback(
|
|
52
|
+
// fallow-ignore-next-line complexity
|
|
53
|
+
async (
|
|
54
|
+
assetPath: string,
|
|
55
|
+
placement: Pick<TimelineElement, "start" | "track">,
|
|
56
|
+
durationOverride?: number,
|
|
57
|
+
) => {
|
|
58
|
+
if (isRecordingRef?.current) {
|
|
59
|
+
showToast("Cannot edit timeline while recording", "error");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const pid = projectIdRef.current;
|
|
63
|
+
if (!pid) throw new Error("No active project");
|
|
64
|
+
|
|
65
|
+
const kind = getTimelineAssetKind(assetPath);
|
|
66
|
+
if (!kind) {
|
|
67
|
+
showToast("Only image, video, and audio assets can be dropped onto the timeline.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const targetPath = activeCompPath || "index.html";
|
|
72
|
+
try {
|
|
73
|
+
const originalContent = await readFileContent(pid, targetPath);
|
|
74
|
+
|
|
75
|
+
const normalizedStart = Number(formatTimelineAttributeNumber(placement.start));
|
|
76
|
+
const duration =
|
|
77
|
+
Number.isFinite(durationOverride) && durationOverride != null && durationOverride > 0
|
|
78
|
+
? durationOverride
|
|
79
|
+
: await resolveDroppedAssetDuration(pid, assetPath, kind);
|
|
80
|
+
const normalizedDuration = Number(formatTimelineAttributeNumber(duration));
|
|
81
|
+
const newId = buildTimelineAssetId(assetPath, collectHtmlIds(originalContent));
|
|
82
|
+
const resolvedAssetSrc = resolveTimelineAssetSrc(targetPath, assetPath);
|
|
83
|
+
|
|
84
|
+
const resolvedTargetPath = targetPath || "index.html";
|
|
85
|
+
const relevantElements = timelineElements.filter(
|
|
86
|
+
(te) => (te.sourceFile || activeCompPath || "index.html") === resolvedTargetPath,
|
|
87
|
+
);
|
|
88
|
+
const newElementZIndex = Math.max(1, relevantElements.length + 1);
|
|
89
|
+
|
|
90
|
+
const patchedContent = insertTimelineAssetIntoSource(
|
|
91
|
+
originalContent,
|
|
92
|
+
buildTimelineAssetInsertHtml({
|
|
93
|
+
id: newId,
|
|
94
|
+
hfId: `hf-${generateId()}`,
|
|
95
|
+
assetPath: resolvedAssetSrc,
|
|
96
|
+
kind,
|
|
97
|
+
start: normalizedStart,
|
|
98
|
+
duration: normalizedDuration,
|
|
99
|
+
track: placement.track,
|
|
100
|
+
zIndex: newElementZIndex,
|
|
101
|
+
geometry: fitTimelineAssetGeometry(
|
|
102
|
+
null,
|
|
103
|
+
resolveTimelineAssetCompositionSize(originalContent),
|
|
104
|
+
),
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
109
|
+
await saveProjectFilesWithHistory({
|
|
110
|
+
projectId: pid,
|
|
111
|
+
label: "Add timeline asset",
|
|
112
|
+
kind: "timeline",
|
|
113
|
+
files: { [targetPath]: patchedContent },
|
|
114
|
+
readFile: async () => originalContent,
|
|
115
|
+
writeFile: writeProjectFile,
|
|
116
|
+
recordEdit,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
forceReloadSdkSession?.();
|
|
120
|
+
reloadPreview();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const message =
|
|
123
|
+
error instanceof Error ? error.message : "Failed to drop asset onto timeline";
|
|
124
|
+
showToast(message);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[
|
|
128
|
+
projectIdRef,
|
|
129
|
+
activeCompPath,
|
|
130
|
+
recordEdit,
|
|
131
|
+
showToast,
|
|
132
|
+
timelineElements,
|
|
133
|
+
writeProjectFile,
|
|
134
|
+
domEditSaveTimestampRef,
|
|
135
|
+
reloadPreview,
|
|
136
|
+
isRecordingRef,
|
|
137
|
+
forceReloadSdkSession,
|
|
138
|
+
],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// fallow-ignore-next-line complexity
|
|
142
|
+
const handleTimelineFileDrop = useCallback(
|
|
143
|
+
// fallow-ignore-next-line complexity
|
|
144
|
+
async (files: File[], placement?: Pick<TimelineElement, "start" | "track">) => {
|
|
145
|
+
if (isRecordingRef?.current) {
|
|
146
|
+
showToast("Cannot edit timeline while recording", "error");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const pid = projectIdRef.current;
|
|
150
|
+
if (!pid) return;
|
|
151
|
+
const uploaded = await uploadProjectFiles(files);
|
|
152
|
+
if (uploaded.length === 0) return;
|
|
153
|
+
const durations: number[] = [];
|
|
154
|
+
for (const assetPath of uploaded) {
|
|
155
|
+
const kind = getTimelineAssetKind(assetPath);
|
|
156
|
+
const duration = kind ? await resolveDroppedAssetDuration(pid, assetPath, kind) : 0;
|
|
157
|
+
durations.push(Number(formatTimelineAttributeNumber(duration)));
|
|
158
|
+
}
|
|
159
|
+
const placements = buildTimelineFileDropPlacements(
|
|
160
|
+
placement ?? { start: 0, track: 0 },
|
|
161
|
+
durations,
|
|
162
|
+
);
|
|
163
|
+
for (const [index, assetPath] of uploaded.entries()) {
|
|
164
|
+
await handleTimelineAssetDrop(
|
|
165
|
+
assetPath,
|
|
166
|
+
placements[index] ?? placements[0],
|
|
167
|
+
durations[index],
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
[handleTimelineAssetDrop, projectIdRef, uploadProjectFiles, isRecordingRef, showToast],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return { handleTimelineAssetDrop, handleTimelineFileDrop };
|
|
175
|
+
}
|