@hyperframes/studio 0.7.56 → 0.7.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/hyperframes-player--Z69cEkE.js +459 -0
- package/dist/assets/{index-C4csZims.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/{index-CmVCjZjp.js → index-DfmYkU44.js} +196 -196
- package/dist/assets/{index-BWnOxAiH.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.html +2 -2
- package/dist/index.js +4345 -3812
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditOverlay.tsx +31 -62
- package/src/components/editor/LayersPanel.tsx +10 -1
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/useCanvasContextMenuState.ts +92 -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 +36 -16
- 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 +12 -1
- package/src/hooks/timelineEditingHelpers.test.ts +91 -0
- package/src/hooks/timelineEditingHelpers.ts +31 -244
- package/src/hooks/timelineMoveAdapter.test.ts +44 -40
- package/src/hooks/timelineMoveAdapter.ts +5 -3
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useDomEditCommits.test.tsx +92 -1
- package/src/hooks/useDomEditCommits.ts +63 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +165 -0
- package/src/hooks/useElementLifecycleOps.ts +20 -7
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +378 -212
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineGroupEditing.ts +153 -132
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +7 -2
- package/src/player/components/Timeline.test.ts +85 -8
- package/src/player/components/Timeline.tsx +9 -9
- package/src/player/components/TimelineCanvas.tsx +11 -2
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/timelineClipDragCommit.test.ts +91 -8
- package/src/player/components/timelineClipDragCommit.ts +106 -9
- package/src/player/components/timelineLayout.ts +99 -26
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +11 -1
- package/src/player/components/useTimelineStackingSync.ts +13 -2
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +4 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +3 -1
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/timelineDOM.ts +9 -0
- package/src/player/lib/timelineElementHelpers.ts +19 -0
- package/src/player/lib/timelineIframeHelpers.ts +2 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +37 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.57",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"gsap": "^3.13.0",
|
|
47
47
|
"marked": "^14.1.4",
|
|
48
48
|
"mediabunny": "^1.45.3",
|
|
49
|
-
"@hyperframes/
|
|
50
|
-
"@hyperframes/
|
|
51
|
-
"@hyperframes/sdk": "0.7.
|
|
52
|
-
"@hyperframes/
|
|
53
|
-
"@hyperframes/
|
|
49
|
+
"@hyperframes/core": "0.7.57",
|
|
50
|
+
"@hyperframes/parsers": "0.7.57",
|
|
51
|
+
"@hyperframes/sdk": "0.7.57",
|
|
52
|
+
"@hyperframes/player": "0.7.57",
|
|
53
|
+
"@hyperframes/studio-server": "0.7.57"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/react": "19",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"vite": "^6.4.2",
|
|
66
66
|
"vitest": "^3.2.4",
|
|
67
67
|
"zustand": "^5.0.0",
|
|
68
|
-
"@hyperframes/producer": "0.7.
|
|
68
|
+
"@hyperframes/producer": "0.7.57"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"react": "19",
|
|
@@ -3,7 +3,11 @@ import React, { act } from "react";
|
|
|
3
3
|
import { createRoot, type Root } from "react-dom/client";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { installReactActEnvironment, makeSelection } from "../../hooks/domSelectionTestHarness";
|
|
6
|
+
import { resolveZIndexEntries } from "../nle/PreviewOverlays";
|
|
7
|
+
import { useElementLifecycleOps } from "../../hooks/useElementLifecycleOps";
|
|
8
|
+
import type { DomEditPatchBatch } from "../../hooks/domEditCommitTypes";
|
|
6
9
|
import { CanvasContextMenu } from "./CanvasContextMenu";
|
|
10
|
+
import type { ZOrderAction, ZOrderPatch } from "./canvasContextMenuZOrder";
|
|
7
11
|
import type { DomEditSelection } from "./domEditing";
|
|
8
12
|
|
|
9
13
|
installReactActEnvironment();
|
|
@@ -24,7 +28,8 @@ afterEach(() => {
|
|
|
24
28
|
|
|
25
29
|
function renderMenu(props: {
|
|
26
30
|
selection: DomEditSelection;
|
|
27
|
-
onApplyZIndex?: () => void;
|
|
31
|
+
onApplyZIndex?: (patches: ZOrderPatch[], action: ZOrderAction) => void;
|
|
32
|
+
onZOrderCrossed?: (crossed: HTMLElement, action: ZOrderAction) => void;
|
|
28
33
|
onDelete?: (selection: DomEditSelection) => void;
|
|
29
34
|
}) {
|
|
30
35
|
root = createRoot(host);
|
|
@@ -36,6 +41,7 @@ function renderMenu(props: {
|
|
|
36
41
|
selection: props.selection,
|
|
37
42
|
onClose: () => {},
|
|
38
43
|
onApplyZIndex: props.onApplyZIndex,
|
|
44
|
+
onZOrderCrossed: props.onZOrderCrossed,
|
|
39
45
|
onDelete: props.onDelete,
|
|
40
46
|
}),
|
|
41
47
|
);
|
|
@@ -113,3 +119,148 @@ describe("CanvasContextMenu — handler gating", () => {
|
|
|
113
119
|
expect(document.body.querySelector(".border-t")).toBeNull();
|
|
114
120
|
});
|
|
115
121
|
});
|
|
122
|
+
|
|
123
|
+
// ── Menu z-action → commit path (wired the way PreviewOverlays wires the app) ──
|
|
124
|
+
|
|
125
|
+
function pressMenuItem(label: string) {
|
|
126
|
+
const button = zOrderButtons().find((b) => b.textContent === label);
|
|
127
|
+
expect(button).toBeDefined();
|
|
128
|
+
act(() => {
|
|
129
|
+
button!.dispatchEvent(
|
|
130
|
+
new PointerEvent("pointerdown", { bubbles: true, cancelable: true, button: 0 }),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Target (static, earlier in DOM) below an equal-z sibling — z action must renumber. */
|
|
136
|
+
function makeStaticFamily() {
|
|
137
|
+
const parent = document.createElement("div");
|
|
138
|
+
const target = document.createElement("div");
|
|
139
|
+
target.id = "target";
|
|
140
|
+
// In happy-dom an unset computed position is "" (not "static"), which would
|
|
141
|
+
// skip the commit hook's static-position injection; declare it explicitly so
|
|
142
|
+
// the test exercises the browser default.
|
|
143
|
+
target.style.position = "static";
|
|
144
|
+
const other = document.createElement("div");
|
|
145
|
+
other.id = "other";
|
|
146
|
+
parent.append(target, other);
|
|
147
|
+
document.body.append(parent);
|
|
148
|
+
return { parent, target, other };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface CapturedBatchCall {
|
|
152
|
+
batches: DomEditPatchBatch[];
|
|
153
|
+
options: { label: string; coalesceKey: string };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Mount the REAL commit hook (persist layer mocked at commitDomEditPatchBatches). */
|
|
157
|
+
function renderCommitHook(captured: CapturedBatchCall[]) {
|
|
158
|
+
type Commit = ReturnType<typeof useElementLifecycleOps>["handleDomZIndexReorderCommit"];
|
|
159
|
+
let commit: Commit | undefined;
|
|
160
|
+
function Harness() {
|
|
161
|
+
({ handleDomZIndexReorderCommit: commit } = useElementLifecycleOps({
|
|
162
|
+
activeCompPath: "index.html",
|
|
163
|
+
showToast: vi.fn(),
|
|
164
|
+
writeProjectFile: vi.fn(async () => {}),
|
|
165
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
166
|
+
editHistory: { recordEdit: vi.fn(async () => {}) },
|
|
167
|
+
projectIdRef: { current: null },
|
|
168
|
+
reloadPreview: vi.fn(),
|
|
169
|
+
clearDomSelection: vi.fn(),
|
|
170
|
+
commitDomEditPatchBatches: async (batches, options) => {
|
|
171
|
+
captured.push({ batches, options });
|
|
172
|
+
},
|
|
173
|
+
}));
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const hookHost = document.createElement("div");
|
|
177
|
+
document.body.append(hookHost);
|
|
178
|
+
const hookRoot = createRoot(hookHost);
|
|
179
|
+
act(() => hookRoot.render(<Harness />));
|
|
180
|
+
return { commit: commit!, cleanup: () => act(() => hookRoot.unmount()) };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
describe("CanvasContextMenu — z-action commit path", () => {
|
|
184
|
+
it("never mutates live styles itself and persists the position patch for a static element", async () => {
|
|
185
|
+
const { target } = makeStaticFamily();
|
|
186
|
+
const selection = makeSelection("Target", target);
|
|
187
|
+
const captured: CapturedBatchCall[] = [];
|
|
188
|
+
const { commit, cleanup } = renderCommitHook(captured);
|
|
189
|
+
|
|
190
|
+
// Wire onApplyZIndex the way the app does (PreviewOverlays → the commit
|
|
191
|
+
// hook), asserting the menu has NOT touched the DOM when it fires — the
|
|
192
|
+
// hook must capture true pre-change styles for its rollback.
|
|
193
|
+
const stylesAtApply: Array<{ zIndex: string; position: string }> = [];
|
|
194
|
+
renderMenu({
|
|
195
|
+
selection,
|
|
196
|
+
onApplyZIndex: (patches, action) => {
|
|
197
|
+
stylesAtApply.push({ zIndex: target.style.zIndex, position: target.style.position });
|
|
198
|
+
const { entries } = resolveZIndexEntries(selection, patches);
|
|
199
|
+
void commit(entries, undefined, action);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await act(async () => pressMenuItem("Bring forward"));
|
|
204
|
+
|
|
205
|
+
// The menu left the element pristine; only the commit hook wrote styles.
|
|
206
|
+
expect(stylesAtApply).toEqual([{ zIndex: "", position: "static" }]);
|
|
207
|
+
expect(target.style.zIndex).toBe("1");
|
|
208
|
+
expect(target.style.position).toBe("relative");
|
|
209
|
+
|
|
210
|
+
// The persisted payload carries BOTH the z-index and the injected position,
|
|
211
|
+
// so the reorder survives the post-commit reloadPreview().
|
|
212
|
+
expect(captured).toHaveLength(1);
|
|
213
|
+
const targetPatch = captured[0]?.batches
|
|
214
|
+
.flatMap((batch) => batch.patches)
|
|
215
|
+
.find((patch) => patch.target.id === "target");
|
|
216
|
+
expect(targetPatch?.operations).toEqual(
|
|
217
|
+
expect.arrayContaining([
|
|
218
|
+
{ type: "inline-style", property: "z-index", value: "1" },
|
|
219
|
+
{ type: "inline-style", property: "position", value: "relative" },
|
|
220
|
+
]),
|
|
221
|
+
);
|
|
222
|
+
// F7: the action kind is part of the default undo coalesce key, so two
|
|
223
|
+
// different menu actions never merge into one undo step.
|
|
224
|
+
expect(captured[0]?.options.coalesceKey).toContain("bring-forward");
|
|
225
|
+
|
|
226
|
+
cleanup();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("reports the crossed sibling to onZOrderCrossed for a forward step (resolved pre-mutation)", async () => {
|
|
230
|
+
// target (earlier in DOM) and other are tied — bring-forward steps over
|
|
231
|
+
// `other`, and the flash callback must receive exactly that element, after
|
|
232
|
+
// onApplyZIndex ran (call order lets the host measure post-commit rects).
|
|
233
|
+
const { target, other } = makeStaticFamily();
|
|
234
|
+
const selection = makeSelection("Target", target);
|
|
235
|
+
const calls: Array<{ kind: string; crossed?: HTMLElement }> = [];
|
|
236
|
+
|
|
237
|
+
renderMenu({
|
|
238
|
+
selection,
|
|
239
|
+
onApplyZIndex: () => calls.push({ kind: "apply" }),
|
|
240
|
+
onZOrderCrossed: (crossed, action) => {
|
|
241
|
+
expect(action).toBe("bring-forward");
|
|
242
|
+
calls.push({ kind: "crossed", crossed });
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await act(async () => pressMenuItem("Bring forward"));
|
|
247
|
+
|
|
248
|
+
expect(calls.map((c) => c.kind)).toEqual(["apply", "crossed"]);
|
|
249
|
+
expect(calls[1]?.crossed).toBe(other);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("does not call onZOrderCrossed for bring-to-front", async () => {
|
|
253
|
+
const { target } = makeStaticFamily();
|
|
254
|
+
const onZOrderCrossed = vi.fn();
|
|
255
|
+
|
|
256
|
+
renderMenu({
|
|
257
|
+
selection: makeSelection("Target", target),
|
|
258
|
+
onApplyZIndex: vi.fn(),
|
|
259
|
+
onZOrderCrossed,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await act(async () => pressMenuItem("Bring to front"));
|
|
263
|
+
|
|
264
|
+
expect(onZOrderCrossed).not.toHaveBeenCalled();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* useContextMenuDismiss.
|
|
8
8
|
*
|
|
9
9
|
* ── Wiring (z-order persistence) ─────────────────────────────────────────────
|
|
10
|
-
* Z-index changes are
|
|
10
|
+
* Z-index changes are resolved against the live iframe DOM via
|
|
11
11
|
* `resolveZOrderChange`, which returns a MULTI-element patch list (tie-aware:
|
|
12
12
|
* moving a target past an equal-z sibling can require renumbering the affected
|
|
13
|
-
* set). The patches are surfaced through the `onApplyZIndex` prop
|
|
13
|
+
* set). The patches are surfaced through the `onApplyZIndex` prop; the menu
|
|
14
|
+
* itself never mutates element styles — handleDomZIndexReorderCommit applies
|
|
15
|
+
* the live z-index (and injects position when needed) in the same synchronous
|
|
16
|
+
* flow, and captures the TRUE prior styles for its failure rollback.
|
|
14
17
|
*
|
|
15
18
|
* The prop MUST be wired at the call site to route through the full persist
|
|
16
19
|
* path. PreviewOverlays.tsx builds the per-patch PatchTargets (the selected
|
|
@@ -26,7 +29,9 @@ import type { DomEditSelection } from "./domEditing";
|
|
|
26
29
|
import { useContextMenuDismiss } from "../../hooks/useContextMenuDismiss";
|
|
27
30
|
import {
|
|
28
31
|
isZOrderActionEnabled,
|
|
32
|
+
resolveCrossedNeighbor,
|
|
29
33
|
resolveZOrderChange,
|
|
34
|
+
type ZOrderAction,
|
|
30
35
|
type ZOrderPatch,
|
|
31
36
|
} from "./canvasContextMenuZOrder";
|
|
32
37
|
|
|
@@ -38,12 +43,22 @@ interface CanvasContextMenuProps {
|
|
|
38
43
|
selection: DomEditSelection;
|
|
39
44
|
onClose: () => void;
|
|
40
45
|
/**
|
|
41
|
-
* Called with the resolved z-order patch list
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
46
|
+
* Called with the resolved z-order patch list and the menu action that
|
|
47
|
+
* produced it (the action feeds the undo coalesce key, so two DIFFERENT
|
|
48
|
+
* actions never merge into one undo step). Each patch is an
|
|
49
|
+
* { element, zIndex } pair (the target and, when a renumber is needed,
|
|
50
|
+
* affected siblings). The menu does NOT touch the live DOM — wire to
|
|
51
|
+
* handleDomZIndexReorderCommit, which applies the live styles itself
|
|
52
|
+
* (see module-level wiring comment).
|
|
45
53
|
*/
|
|
46
|
-
onApplyZIndex?: (patches: ZOrderPatch[]) => void;
|
|
54
|
+
onApplyZIndex?: (patches: ZOrderPatch[], action: ZOrderAction) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Called after a successful bring-forward / send-backward with the sibling
|
|
57
|
+
* the target stepped over (resolved from the SAME pre-mutation state as the
|
|
58
|
+
* patches), so the host can flash a highlight on it in the studio overlay.
|
|
59
|
+
* Never called for front/back or no-op actions.
|
|
60
|
+
*/
|
|
61
|
+
onZOrderCrossed?: (crossed: HTMLElement, action: ZOrderAction) => void;
|
|
47
62
|
/**
|
|
48
63
|
* Delete the selected element. Wire to handleDomEditElementDelete from
|
|
49
64
|
* useDomEditActionsContext — same path as the Delete/Backspace hotkey.
|
|
@@ -68,6 +83,7 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
|
|
|
68
83
|
selection,
|
|
69
84
|
onClose,
|
|
70
85
|
onApplyZIndex,
|
|
86
|
+
onZOrderCrossed,
|
|
71
87
|
onDelete,
|
|
72
88
|
}: CanvasContextMenuProps) {
|
|
73
89
|
const menuRef = useContextMenuDismiss(onClose);
|
|
@@ -93,20 +109,19 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
|
|
|
93
109
|
const el = selection.element;
|
|
94
110
|
|
|
95
111
|
function handleZAction(action: ZAction) {
|
|
96
|
-
// No persist handler → do NOT touch the live iframe DOM. An optimistic
|
|
97
|
-
// write with nothing to persist just reverts on the next reload.
|
|
98
112
|
if (!onApplyZIndex) return;
|
|
99
113
|
const patches = resolveZOrderChange(el, action);
|
|
100
114
|
if (patches === null) return;
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
onApplyZIndex(patches);
|
|
115
|
+
// Resolve the crossed neighbor BEFORE the commit path mutates live styles —
|
|
116
|
+
// both resolvers must read the same pre-change render order.
|
|
117
|
+
const crossed = onZOrderCrossed ? resolveCrossedNeighbor(el, action) : null;
|
|
118
|
+
// Do NOT pre-apply styles here: handleDomZIndexReorderCommit writes the
|
|
119
|
+
// live z-index (and injects position:relative for static elements) in the
|
|
120
|
+
// same synchronous flow, so feedback is still instant — and it must read
|
|
121
|
+
// the PRE-change styles itself, both to capture true rollback values and
|
|
122
|
+
// to detect a static position that needs persisting.
|
|
123
|
+
onApplyZIndex(patches, action);
|
|
124
|
+
if (crossed && onZOrderCrossed) onZOrderCrossed(crossed, action);
|
|
110
125
|
onClose();
|
|
111
126
|
}
|
|
112
127
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { memo,
|
|
1
|
+
import { memo, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
2
2
|
import { type DomEditSelection } from "./domEditing";
|
|
3
3
|
import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction";
|
|
4
4
|
import { useMarqueeGestures } from "./marqueeCommit";
|
|
5
5
|
import { MarqueeOverlay } from "./MarqueeOverlay";
|
|
6
6
|
import { resolveDomEditGroupOverlayRect } from "./domEditOverlayGeometry";
|
|
7
|
+
import { useZOrderCrossedFlash, ZOrderCrossedFlash } from "./useZOrderCrossedFlash";
|
|
8
|
+
import { useCanvasContextMenuState } from "./useCanvasContextMenuState";
|
|
7
9
|
import {
|
|
8
10
|
type BlockedMoveState,
|
|
9
11
|
type DomEditGroupPathOffsetCommit,
|
|
@@ -27,7 +29,7 @@ import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
|
|
|
27
29
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
28
30
|
import { startOffCanvasIndicatorRefresh } from "./offCanvasIndicatorRefresh";
|
|
29
31
|
import { CanvasContextMenu } from "./CanvasContextMenu";
|
|
30
|
-
import type { ZOrderPatch } from "./canvasContextMenuZOrder";
|
|
32
|
+
import type { ZOrderAction, ZOrderPatch } from "./canvasContextMenuZOrder";
|
|
31
33
|
import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
|
|
32
34
|
|
|
33
35
|
// Re-exports for external consumers — preserving existing import paths.
|
|
@@ -91,12 +93,17 @@ interface DomEditOverlayProps {
|
|
|
91
93
|
*/
|
|
92
94
|
onDeleteSelection?: (selection: DomEditSelection) => void;
|
|
93
95
|
/**
|
|
94
|
-
* Called with the resolved z-order patch list
|
|
95
|
-
* The patch list is tie-aware and
|
|
96
|
-
* canvasContextMenuZOrder)
|
|
96
|
+
* Called with the resolved z-order patch list and the menu action that
|
|
97
|
+
* produced it (feeds the undo coalesce key). The patch list is tie-aware and
|
|
98
|
+
* may include sibling elements (see canvasContextMenuZOrder); the live DOM is
|
|
99
|
+
* NOT yet mutated. Wire to handleDomZIndexReorderCommit from
|
|
97
100
|
* useDomEditActionsContext. See CanvasContextMenu.tsx module comment.
|
|
98
101
|
*/
|
|
99
|
-
onApplyZIndex?: (
|
|
102
|
+
onApplyZIndex?: (
|
|
103
|
+
selection: DomEditSelection,
|
|
104
|
+
patches: ZOrderPatch[],
|
|
105
|
+
action: ZOrderAction,
|
|
106
|
+
) => void;
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
// fallow-ignore-next-line complexity
|
|
@@ -139,30 +146,12 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
139
146
|
const snapGuidesRef = useRef<SnapGuidesState | null>(null);
|
|
140
147
|
const rafPausedRef = useRef(false);
|
|
141
148
|
|
|
142
|
-
// Context menu state: position of the right-click that opened it.
|
|
143
|
-
// contextMenuSelection is the element the menu targets — captured at right-click
|
|
144
|
-
// time so the menu can open even before the React selection state settles.
|
|
145
|
-
const [contextMenu, setContextMenu] = useState<{
|
|
146
|
-
x: number;
|
|
147
|
-
y: number;
|
|
148
|
-
sel: DomEditSelection;
|
|
149
|
-
} | null>(null);
|
|
150
|
-
|
|
151
149
|
const selectionRef = useRef(selection);
|
|
152
150
|
selectionRef.current = selection;
|
|
153
151
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
// over a stale target after the underlying element is gone. A right-click that
|
|
158
|
-
// OPENS the menu also selects its target, so the common open path keeps the
|
|
159
|
-
// menu (same element) rather than immediately dismissing it.
|
|
160
|
-
useEffect(() => {
|
|
161
|
-
if (!contextMenu) return;
|
|
162
|
-
if (!selection || selection.element !== contextMenu.sel.element) {
|
|
163
|
-
setContextMenu(null);
|
|
164
|
-
}
|
|
165
|
-
}, [selection, contextMenu]);
|
|
152
|
+
// Brief highlight on the sibling a forward/backward z step crossed — drawn
|
|
153
|
+
// in this studio overlay, never in the iframe DOM (see useZOrderCrossedFlash).
|
|
154
|
+
const { zOrderFlashRect, handleZOrderCrossed } = useZOrderCrossedFlash({ overlayRef, iframeRef });
|
|
166
155
|
|
|
167
156
|
const activeCompositionPathRef = useRef(activeCompositionPath);
|
|
168
157
|
activeCompositionPathRef.current = activeCompositionPath;
|
|
@@ -428,37 +417,15 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
428
417
|
e.stopPropagation();
|
|
429
418
|
};
|
|
430
419
|
|
|
431
|
-
// Right-click
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const pointerEvent = event as unknown as React.PointerEvent<HTMLDivElement>;
|
|
441
|
-
const resolved = await onCanvasPointerMoveRef.current(pointerEvent);
|
|
442
|
-
if (!resolved) return; // Nothing under the cursor — skip menu.
|
|
443
|
-
onSelectionChangeRef.current(resolved, { revealPanel: true });
|
|
444
|
-
// Use `resolved` directly: React state (and therefore selectionRef) won't
|
|
445
|
-
// update synchronously after onSelectionChange — we'd be reading stale null.
|
|
446
|
-
activeSel = resolved;
|
|
447
|
-
} else {
|
|
448
|
-
// Check if the user right-clicked on an unselected element (hover target).
|
|
449
|
-
const hover = hoverSelectionRef.current;
|
|
450
|
-
if (hover && hover.element !== currentSel.element) {
|
|
451
|
-
onSelectionChangeRef.current(hover, { revealPanel: true });
|
|
452
|
-
activeSel = hover;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (!activeSel) return;
|
|
457
|
-
setContextMenu({ x: event.clientX, y: event.clientY, sel: activeSel });
|
|
458
|
-
},
|
|
459
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
460
|
-
[],
|
|
461
|
-
);
|
|
420
|
+
// Right-click state + handler: select the element under the pointer (if
|
|
421
|
+
// needed), then open the menu; closes when the selection moves off-target.
|
|
422
|
+
const { contextMenu, closeContextMenu, handleContextMenu } = useCanvasContextMenuState({
|
|
423
|
+
selection,
|
|
424
|
+
selectionRef,
|
|
425
|
+
hoverSelectionRef,
|
|
426
|
+
onCanvasPointerMoveRef,
|
|
427
|
+
onSelectionChangeRef,
|
|
428
|
+
});
|
|
462
429
|
|
|
463
430
|
return (
|
|
464
431
|
<div
|
|
@@ -551,24 +518,26 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
551
518
|
x={contextMenu.x}
|
|
552
519
|
y={contextMenu.y}
|
|
553
520
|
selection={contextMenu.sel}
|
|
554
|
-
onClose={
|
|
521
|
+
onClose={closeContextMenu}
|
|
555
522
|
onDelete={
|
|
556
523
|
onDeleteSelection
|
|
557
524
|
? (sel) => {
|
|
558
|
-
|
|
525
|
+
closeContextMenu();
|
|
559
526
|
onDeleteSelection(sel);
|
|
560
527
|
}
|
|
561
528
|
: undefined
|
|
562
529
|
}
|
|
563
530
|
onApplyZIndex={
|
|
564
531
|
onApplyZIndex
|
|
565
|
-
? (patches) => {
|
|
566
|
-
onApplyZIndex(contextMenu.sel, patches);
|
|
532
|
+
? (patches, action) => {
|
|
533
|
+
onApplyZIndex(contextMenu.sel, patches, action);
|
|
567
534
|
}
|
|
568
535
|
: undefined
|
|
569
536
|
}
|
|
537
|
+
onZOrderCrossed={handleZOrderCrossed}
|
|
570
538
|
/>
|
|
571
539
|
)}
|
|
540
|
+
<ZOrderCrossedFlash rect={zOrderFlashRect} />
|
|
572
541
|
<GridOverlay
|
|
573
542
|
visible={gridVisible}
|
|
574
543
|
spacing={gridSpacing}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { Layers } from "../../icons/SystemIcons";
|
|
16
16
|
import { useLayerDrag, isLayerDraggable, type LayerReorderEvent } from "./useLayerDrag";
|
|
17
17
|
import { computeReorderZValues, getElementZIndex } from "../../player/lib/layerOrdering";
|
|
18
|
+
import { deriveTimelineStoreKey } from "../../player/lib/timelineElementHelpers";
|
|
18
19
|
|
|
19
20
|
const TAG_ICONS: Record<string, string> = {
|
|
20
21
|
video: "Vi",
|
|
@@ -280,9 +281,17 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
280
281
|
selector: layer.selector,
|
|
281
282
|
selectorIndex: layer.selectorIndex,
|
|
282
283
|
sourceFile: layer.sourceFile,
|
|
284
|
+
key: deriveTimelineStoreKey({
|
|
285
|
+
domId: layer.id,
|
|
286
|
+
selector: layer.selector,
|
|
287
|
+
selectorIndex: layer.selectorIndex,
|
|
288
|
+
sourceFile: layer.sourceFile,
|
|
289
|
+
}),
|
|
283
290
|
}));
|
|
284
291
|
|
|
285
|
-
|
|
292
|
+
// "layer-drag" keeps consecutive drops of the same sibling set coalescing
|
|
293
|
+
// into one undo step, without merging with a context-menu z action.
|
|
294
|
+
handleDomZIndexReorderCommit(entries, undefined, "layer-drag");
|
|
286
295
|
},
|
|
287
296
|
[handleDomZIndexReorderCommit],
|
|
288
297
|
);
|