@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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
7
|
+
import { useAssetPreviewStore } from "../../utils/assetPreviewStore";
|
|
8
|
+
import { AssetPreviewOverlay } from "./AssetPreviewOverlay";
|
|
9
|
+
|
|
10
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
|
|
12
|
+
let root: Root | null = null;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (root) {
|
|
16
|
+
act(() => root?.unmount());
|
|
17
|
+
root = null;
|
|
18
|
+
}
|
|
19
|
+
document.body.innerHTML = "";
|
|
20
|
+
usePlayerStore.getState().reset();
|
|
21
|
+
// reset() does not cover the out-of-loop seek request; clear it explicitly
|
|
22
|
+
// so a pending-seek test can't leak into the next one.
|
|
23
|
+
usePlayerStore.getState().clearSeekRequest();
|
|
24
|
+
useAssetPreviewStore.getState().clearPreviewAsset();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function mountOverlay(): void {
|
|
28
|
+
const host = document.createElement("div");
|
|
29
|
+
document.body.append(host);
|
|
30
|
+
root = createRoot(host);
|
|
31
|
+
act(() => {
|
|
32
|
+
root?.render(<AssetPreviewOverlay />);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("AssetPreviewOverlay playback dismissal", () => {
|
|
37
|
+
it("dismisses immediately when opened while playback is ALREADY running", () => {
|
|
38
|
+
// The RAF playback loop bypasses the store, so no post-open store change
|
|
39
|
+
// will arrive — the dismiss check must be level-triggered, not edge-triggered.
|
|
40
|
+
usePlayerStore.setState({ isPlaying: true });
|
|
41
|
+
mountOverlay();
|
|
42
|
+
|
|
43
|
+
act(() => {
|
|
44
|
+
useAssetPreviewStore.getState().setPreviewAsset("assets/clip.mp3", "p1");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(useAssetPreviewStore.getState().previewAsset).toBeNull();
|
|
48
|
+
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("dismisses immediately when opened while a seek is ALREADY in flight", () => {
|
|
52
|
+
// A pending out-of-loop seek (requestedSeekTime set, not yet consumed) may
|
|
53
|
+
// produce no further store change before currentTime settles — the on-open
|
|
54
|
+
// check must evaluate the full shared dismiss predicate, not just isPlaying.
|
|
55
|
+
usePlayerStore.setState({ isPlaying: false, requestedSeekTime: 3.2 });
|
|
56
|
+
mountOverlay();
|
|
57
|
+
|
|
58
|
+
act(() => {
|
|
59
|
+
useAssetPreviewStore.getState().setPreviewAsset("assets/clip.mp3", "p1");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(useAssetPreviewStore.getState().previewAsset).toBeNull();
|
|
63
|
+
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("stays open when the playhead is idle", () => {
|
|
67
|
+
mountOverlay();
|
|
68
|
+
|
|
69
|
+
act(() => {
|
|
70
|
+
useAssetPreviewStore.getState().setPreviewAsset("assets/clip.mp3", "p1");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(useAssetPreviewStore.getState().previewAsset).toBe("assets/clip.mp3");
|
|
74
|
+
expect(document.querySelector('[role="dialog"]')).not.toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("still dismisses when playback starts AFTER the preview opened", () => {
|
|
78
|
+
mountOverlay();
|
|
79
|
+
|
|
80
|
+
act(() => {
|
|
81
|
+
useAssetPreviewStore.getState().setPreviewAsset("assets/clip.mp3", "p1");
|
|
82
|
+
});
|
|
83
|
+
expect(useAssetPreviewStore.getState().previewAsset).toBe("assets/clip.mp3");
|
|
84
|
+
|
|
85
|
+
act(() => {
|
|
86
|
+
usePlayerStore.setState({ isPlaying: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(useAssetPreviewStore.getState().previewAsset).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("still dismisses when the playhead is scrubbed after opening", () => {
|
|
93
|
+
usePlayerStore.setState({ currentTime: 2 });
|
|
94
|
+
mountOverlay();
|
|
95
|
+
|
|
96
|
+
act(() => {
|
|
97
|
+
useAssetPreviewStore.getState().setPreviewAsset("assets/clip.mp3", "p1");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
act(() => {
|
|
101
|
+
usePlayerStore.setState({ currentTime: 4.5 });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(useAssetPreviewStore.getState().previewAsset).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* CapCut-style asset preview overlay rendered inside PreviewPane.
|
|
3
3
|
*
|
|
4
4
|
* Shown when the user clicks an asset card that has NOT yet been added to the
|
|
5
|
-
* timeline. Displays the media (image / video / audio)
|
|
6
|
-
*
|
|
5
|
+
* timeline. Displays the media (image / video / audio) as a compact floating
|
|
6
|
+
* card over the canvas — the canvas stays visible behind a barely-tinted
|
|
7
|
+
* click-catcher — without modifying the composition (no undo entry, no file
|
|
8
|
+
* mutation).
|
|
7
9
|
*
|
|
8
|
-
* Dismiss: X button, Escape key,
|
|
10
|
+
* Dismiss: X button, Escape key, click outside the card, or any playhead
|
|
11
|
+
* activity (starting playback / seeking) — the canvas refocuses.
|
|
9
12
|
* Switching to another not-added asset replaces the current preview.
|
|
10
13
|
*/
|
|
11
14
|
import { useEffect, useCallback } from "react";
|
|
12
15
|
import { VIDEO_EXT, IMAGE_EXT } from "../../utils/mediaTypes";
|
|
13
16
|
import { useAssetPreviewStore } from "../../utils/assetPreviewStore";
|
|
17
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
18
|
+
import { shouldDismissAssetPreview } from "../../utils/assetPreviewDismiss";
|
|
14
19
|
import { resolveMediaPreviewUrl } from "../../player/components/thumbnailUtils";
|
|
15
20
|
|
|
16
21
|
function basename(path: string): string {
|
|
@@ -37,11 +42,7 @@ function AssetPreviewMedia({
|
|
|
37
42
|
}) {
|
|
38
43
|
if (kind === "image") {
|
|
39
44
|
return (
|
|
40
|
-
<img
|
|
41
|
-
src={serveUrl}
|
|
42
|
-
alt={name}
|
|
43
|
-
className="max-w-full max-h-[70vh] rounded-md object-contain shadow-2xl"
|
|
44
|
-
/>
|
|
45
|
+
<img src={serveUrl} alt={name} className="max-w-full max-h-[40vh] rounded object-contain" />
|
|
45
46
|
);
|
|
46
47
|
}
|
|
47
48
|
if (kind === "video") {
|
|
@@ -52,12 +53,12 @@ function AssetPreviewMedia({
|
|
|
52
53
|
autoPlay
|
|
53
54
|
muted
|
|
54
55
|
playsInline
|
|
55
|
-
className="max-w-full max-h-[
|
|
56
|
+
className="max-w-full max-h-[40vh] rounded"
|
|
56
57
|
/>
|
|
57
58
|
);
|
|
58
59
|
}
|
|
59
60
|
return (
|
|
60
|
-
<div className="flex flex-col items-center gap-
|
|
61
|
+
<div className="flex flex-col items-center gap-3 px-6 py-4">
|
|
61
62
|
<svg
|
|
62
63
|
width="40"
|
|
63
64
|
height="40"
|
|
@@ -94,6 +95,30 @@ export function AssetPreviewOverlay() {
|
|
|
94
95
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
95
96
|
}, [previewAsset, handleKeyDown]);
|
|
96
97
|
|
|
98
|
+
// The canvas refocuses on any playhead activity: starting playback or a
|
|
99
|
+
// seek/scrub away from where the playhead sat when the preview opened
|
|
100
|
+
// dismisses it. openedTime is captured per preview open (previewAsset dep),
|
|
101
|
+
// so a stale render can never dismiss against the wrong reference time.
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!previewAsset) return;
|
|
104
|
+
const opened = usePlayerStore.getState();
|
|
105
|
+
const openedTime = opened.currentTime;
|
|
106
|
+
// Level-triggered, not edge-triggered: a preview opened while playback is
|
|
107
|
+
// ALREADY running (the RAF loop bypasses the store) or while a seek is
|
|
108
|
+
// already in flight gets no store change to react to, so evaluate the
|
|
109
|
+
// current state once, through the same shared predicate the subscription
|
|
110
|
+
// uses. openedTime is this snapshot's own currentTime, so the
|
|
111
|
+
// time-diverged branch can't false-positive at open — only the
|
|
112
|
+
// isPlaying / requestedSeekTime branches can fire here.
|
|
113
|
+
if (shouldDismissAssetPreview(openedTime, opened)) {
|
|
114
|
+
clearPreviewAsset();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
return usePlayerStore.subscribe((state) => {
|
|
118
|
+
if (shouldDismissAssetPreview(openedTime, state)) clearPreviewAsset();
|
|
119
|
+
});
|
|
120
|
+
}, [previewAsset, clearPreviewAsset]);
|
|
121
|
+
|
|
97
122
|
if (!previewAsset || !previewProjectId) return null;
|
|
98
123
|
|
|
99
124
|
const serveUrl = resolveMediaPreviewUrl(previewAsset, previewProjectId);
|
|
@@ -101,40 +126,39 @@ export function AssetPreviewOverlay() {
|
|
|
101
126
|
|
|
102
127
|
return (
|
|
103
128
|
<div
|
|
104
|
-
className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/
|
|
129
|
+
className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/20"
|
|
105
130
|
onClick={clearPreviewAsset}
|
|
106
131
|
role="dialog"
|
|
107
132
|
aria-label={`Preview: ${name}`}
|
|
108
|
-
aria-modal="true"
|
|
109
133
|
>
|
|
110
|
-
{/*
|
|
111
|
-
<button
|
|
112
|
-
className="absolute top-3 right-3 w-7 h-7 rounded-full bg-neutral-800 hover:bg-neutral-700 text-neutral-300 hover:text-white flex items-center justify-center transition-colors z-10"
|
|
113
|
-
onClick={(e) => {
|
|
114
|
-
e.stopPropagation();
|
|
115
|
-
clearPreviewAsset();
|
|
116
|
-
}}
|
|
117
|
-
aria-label="Close preview"
|
|
118
|
-
>
|
|
119
|
-
<svg
|
|
120
|
-
width="12"
|
|
121
|
-
height="12"
|
|
122
|
-
viewBox="0 0 24 24"
|
|
123
|
-
stroke="currentColor"
|
|
124
|
-
strokeWidth="2.5"
|
|
125
|
-
fill="none"
|
|
126
|
-
strokeLinecap="round"
|
|
127
|
-
>
|
|
128
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
129
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
130
|
-
</svg>
|
|
131
|
-
</button>
|
|
132
|
-
|
|
133
|
-
{/* Media */}
|
|
134
|
+
{/* Floating preview card — compact, canvas stays visible around it */}
|
|
134
135
|
<div
|
|
135
|
-
className="flex flex-col items-center gap-
|
|
136
|
+
className="relative flex flex-col items-center gap-2 max-w-[58%] rounded-lg border border-neutral-800 bg-neutral-950/95 p-2 pt-8 shadow-2xl"
|
|
136
137
|
onClick={(e) => e.stopPropagation()}
|
|
137
138
|
>
|
|
139
|
+
{/* Close button */}
|
|
140
|
+
<button
|
|
141
|
+
className="absolute top-2 right-2 w-6 h-6 rounded-full bg-neutral-800 hover:bg-neutral-700 text-neutral-300 hover:text-white flex items-center justify-center transition-colors z-10"
|
|
142
|
+
onClick={(e) => {
|
|
143
|
+
e.stopPropagation();
|
|
144
|
+
clearPreviewAsset();
|
|
145
|
+
}}
|
|
146
|
+
aria-label="Close preview"
|
|
147
|
+
>
|
|
148
|
+
<svg
|
|
149
|
+
width="12"
|
|
150
|
+
height="12"
|
|
151
|
+
viewBox="0 0 24 24"
|
|
152
|
+
stroke="currentColor"
|
|
153
|
+
strokeWidth="2.5"
|
|
154
|
+
fill="none"
|
|
155
|
+
strokeLinecap="round"
|
|
156
|
+
>
|
|
157
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
158
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
159
|
+
</svg>
|
|
160
|
+
</button>
|
|
161
|
+
|
|
138
162
|
<AssetPreviewMedia kind={resolveAssetKind(previewAsset)} serveUrl={serveUrl} name={name} />
|
|
139
163
|
|
|
140
164
|
{/* Filename label */}
|
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
import { readStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
19
19
|
import { readHfId, type DomEditSelection } from "../editor/domEditing";
|
|
20
20
|
import { buildStableSelector } from "../editor/domEditingDom";
|
|
21
|
+
import { deriveTimelineStoreKey } from "../../player/lib/timelineElementHelpers";
|
|
22
|
+
import { zReorderCoalesceKey } from "../../hooks/useElementLifecycleOps";
|
|
23
|
+
import { useCanvasZOrderTimelineMirror } from "./useCanvasZOrderTimelineMirror";
|
|
24
|
+
import { runZLaneGesture } from "./zLaneGesture";
|
|
21
25
|
import type { BlockPreviewInfo } from "../sidebar/BlocksTab";
|
|
22
26
|
import type { GestureRecordingState } from "../editor/GestureRecordControl";
|
|
23
27
|
import type { ReactNode } from "react";
|
|
@@ -38,6 +42,8 @@ type ZIndexReorderEntry = {
|
|
|
38
42
|
selector?: string;
|
|
39
43
|
selectorIndex?: number;
|
|
40
44
|
sourceFile: string;
|
|
45
|
+
/** Timeline store key — lets the commit update the store zIndex synchronously. */
|
|
46
|
+
key?: string;
|
|
41
47
|
};
|
|
42
48
|
|
|
43
49
|
/** Can this element be robustly re-targeted for a persisted z change? */
|
|
@@ -58,6 +64,12 @@ function selectedZIndexEntry(sel: DomEditSelection, zIndex: number): ZIndexReord
|
|
|
58
64
|
selector: sel.selector,
|
|
59
65
|
selectorIndex: sel.selectorIndex,
|
|
60
66
|
sourceFile: sel.sourceFile,
|
|
67
|
+
key: deriveTimelineStoreKey({
|
|
68
|
+
domId: sel.id ?? undefined,
|
|
69
|
+
selector: sel.selector,
|
|
70
|
+
selectorIndex: sel.selectorIndex,
|
|
71
|
+
sourceFile: sel.sourceFile,
|
|
72
|
+
}),
|
|
61
73
|
};
|
|
62
74
|
}
|
|
63
75
|
|
|
@@ -75,7 +87,15 @@ function siblingZIndexEntry(
|
|
|
75
87
|
const id = element.id || undefined;
|
|
76
88
|
const selector = buildStableSelector(element);
|
|
77
89
|
if (!canTargetZIndexElement(element, id, selector)) return null;
|
|
78
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
element,
|
|
92
|
+
zIndex,
|
|
93
|
+
id,
|
|
94
|
+
selector,
|
|
95
|
+
selectorIndex: undefined,
|
|
96
|
+
sourceFile,
|
|
97
|
+
key: deriveTimelineStoreKey({ domId: id, selector, sourceFile }),
|
|
98
|
+
};
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
/** Short human-readable label for a dropped sibling, for the console warning below. */
|
|
@@ -88,14 +108,16 @@ function describeZIndexElement(element: HTMLElement): string {
|
|
|
88
108
|
}
|
|
89
109
|
|
|
90
110
|
// Resolve z-index patches into commit entries; a sibling with no stable
|
|
91
|
-
// id/selector can't be written to source, so it is
|
|
92
|
-
// revert-on-reload warning
|
|
93
|
-
|
|
111
|
+
// id/selector can't be written to source, so it is returned as `dropped` for
|
|
112
|
+
// the revert-on-reload warning (and a live-only style write, so the resolved
|
|
113
|
+
// stacking order still renders coherently). Exported so tests can drive the
|
|
114
|
+
// menu → commit path through the same wiring the app uses.
|
|
115
|
+
export function resolveZIndexEntries(
|
|
94
116
|
sel: DomEditSelection,
|
|
95
117
|
patches: ReadonlyArray<{ element: HTMLElement; zIndex: number }>,
|
|
96
|
-
): { entries: ZIndexReorderEntry[];
|
|
118
|
+
): { entries: ZIndexReorderEntry[]; dropped: Array<{ element: HTMLElement; zIndex: number }> } {
|
|
97
119
|
const entries: ZIndexReorderEntry[] = [];
|
|
98
|
-
const
|
|
120
|
+
const dropped: Array<{ element: HTMLElement; zIndex: number }> = [];
|
|
99
121
|
for (const patch of patches) {
|
|
100
122
|
if (patch.element === sel.element) {
|
|
101
123
|
entries.push(selectedZIndexEntry(sel, patch.zIndex));
|
|
@@ -103,9 +125,9 @@ function resolveZIndexEntries(
|
|
|
103
125
|
}
|
|
104
126
|
const entry = siblingZIndexEntry(patch.element, patch.zIndex, sel.sourceFile);
|
|
105
127
|
if (entry) entries.push(entry);
|
|
106
|
-
else
|
|
128
|
+
else dropped.push(patch);
|
|
107
129
|
}
|
|
108
|
-
return { entries,
|
|
130
|
+
return { entries, dropped };
|
|
109
131
|
}
|
|
110
132
|
|
|
111
133
|
// fallow-ignore-next-line complexity
|
|
@@ -139,6 +161,7 @@ export function PreviewOverlays({
|
|
|
139
161
|
handleDomEditElementDelete,
|
|
140
162
|
handleDomZIndexReorderCommit,
|
|
141
163
|
} = useDomEditActionsContext();
|
|
164
|
+
const mirrorZOrderToTimeline = useCanvasZOrderTimelineMirror();
|
|
142
165
|
|
|
143
166
|
// fallow-ignore-next-line complexity
|
|
144
167
|
const [snapPrefs, setSnapPrefs] = useState(() => {
|
|
@@ -205,19 +228,41 @@ export function PreviewOverlays({
|
|
|
205
228
|
onRotationCommit={handleDomRotationCommit}
|
|
206
229
|
onStyleCommit={handleDomStyleCommit}
|
|
207
230
|
onDeleteSelection={handleDomEditElementDelete}
|
|
208
|
-
onApplyZIndex={(sel, patches) => {
|
|
209
|
-
const { entries,
|
|
210
|
-
if (
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
231
|
+
onApplyZIndex={(sel, patches, action, crossed) => {
|
|
232
|
+
const { entries, dropped } = resolveZIndexEntries(sel, patches);
|
|
233
|
+
if (dropped.length > 0) {
|
|
234
|
+
// These siblings can't be written to source. Apply their live z
|
|
235
|
+
// anyway so the resolved stacking order renders coherently — it
|
|
236
|
+
// just reverts to the prior order on the next reload.
|
|
237
|
+
for (const patch of dropped) patch.element.style.zIndex = String(patch.zIndex);
|
|
214
238
|
console.warn(
|
|
215
239
|
"[studio] z-index reorder: dropping sibling(s) with no stable id/selector " +
|
|
216
240
|
"(will revert on reload):",
|
|
217
|
-
|
|
241
|
+
dropped.map((patch) => describeZIndexElement(patch.element)).join(", "),
|
|
218
242
|
);
|
|
219
243
|
}
|
|
220
|
-
if (entries.length
|
|
244
|
+
if (entries.length === 0) return;
|
|
245
|
+
// Shared undo coalesce key: passed to BOTH the z persist and the
|
|
246
|
+
// timeline lane mirror below so editHistory folds the two records
|
|
247
|
+
// into one undo entry (same value handleDomZIndexReorderCommit would
|
|
248
|
+
// default to — passed explicitly so the mirror shares it by
|
|
249
|
+
// construction, not by formula duplication).
|
|
250
|
+
const coalesceKey = zReorderCoalesceKey(entries, action);
|
|
251
|
+
// One serialized z→lane transaction: the mirror runs only AFTER the
|
|
252
|
+
// z commit resolved AND reported durable targets, and a second rapid
|
|
253
|
+
// gesture cannot interleave between the two phases — see
|
|
254
|
+
// runZLaneGesture. A failed z commit already toasted + rolled back.
|
|
255
|
+
runZLaneGesture({
|
|
256
|
+
commitZ: () => handleDomZIndexReorderCommit(entries, coalesceKey, action),
|
|
257
|
+
mirror: () =>
|
|
258
|
+
mirrorZOrderToTimeline({
|
|
259
|
+
selectionKey: entries.find((e) => e.element === sel.element)?.key,
|
|
260
|
+
action,
|
|
261
|
+
crossed,
|
|
262
|
+
sourceFile: sel.sourceFile,
|
|
263
|
+
coalesceKey,
|
|
264
|
+
}),
|
|
265
|
+
}).catch(() => undefined);
|
|
221
266
|
}}
|
|
222
267
|
gridVisible={snapPrefs.gridVisible}
|
|
223
268
|
gridSpacing={snapPrefs.gridSpacing}
|
|
@@ -22,6 +22,7 @@ describe("TimelinePane move wrapper", () => {
|
|
|
22
22
|
"clip-lane-move:7",
|
|
23
23
|
"track-insert",
|
|
24
24
|
onMoveElements,
|
|
25
|
+
Number.POSITIVE_INFINITY,
|
|
25
26
|
);
|
|
26
27
|
expect(onMoveElements).toHaveBeenCalledWith(
|
|
27
28
|
[
|
|
@@ -32,6 +33,8 @@ describe("TimelinePane move wrapper", () => {
|
|
|
32
33
|
],
|
|
33
34
|
"clip-lane-move:7",
|
|
34
35
|
"track-insert",
|
|
36
|
+
// The per-gesture coalesce window rides along with the shared key.
|
|
37
|
+
Number.POSITIVE_INFINITY,
|
|
35
38
|
);
|
|
36
39
|
});
|
|
37
40
|
|
|
@@ -21,7 +21,9 @@ export function forwardRebasedTimelineMoveElements(
|
|
|
21
21
|
edits: TimelineMoveEdit[],
|
|
22
22
|
coalesceKey?: string,
|
|
23
23
|
operation?: TimelineMoveOperation,
|
|
24
|
+
coalesceMs?: number,
|
|
24
25
|
) => Promise<void> | void,
|
|
26
|
+
coalesceMs?: number,
|
|
25
27
|
) {
|
|
26
28
|
return onMoveElements(
|
|
27
29
|
edits.map(({ element, updates }) => {
|
|
@@ -34,6 +36,7 @@ export function forwardRebasedTimelineMoveElements(
|
|
|
34
36
|
}),
|
|
35
37
|
coalesceKey,
|
|
36
38
|
operation,
|
|
39
|
+
coalesceMs,
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -160,6 +163,7 @@ export function TimelinePane({
|
|
|
160
163
|
edits: Array<{ element: TimelineElement; updates: Pick<TimelineElement, "start" | "track"> }>,
|
|
161
164
|
coalesceKey?: string,
|
|
162
165
|
operation?: TimelineMoveOperation,
|
|
166
|
+
coalesceMs?: number,
|
|
163
167
|
) => {
|
|
164
168
|
// Match the sibling handlers: report the telemetry when the batch touches at
|
|
165
169
|
// least one expanded sub-comp child (the clips being rebased to local coords).
|
|
@@ -167,7 +171,13 @@ export function TimelinePane({
|
|
|
167
171
|
trackStudioExpandedClipEdit({ action: "move" });
|
|
168
172
|
}
|
|
169
173
|
if (!onMoveElements) return;
|
|
170
|
-
return forwardRebasedTimelineMoveElements(
|
|
174
|
+
return forwardRebasedTimelineMoveElements(
|
|
175
|
+
edits,
|
|
176
|
+
coalesceKey,
|
|
177
|
+
operation,
|
|
178
|
+
onMoveElements,
|
|
179
|
+
coalesceMs,
|
|
180
|
+
);
|
|
171
181
|
},
|
|
172
182
|
[onMoveElements],
|
|
173
183
|
);
|