@hyperframes/studio 0.8.101 → 0.8.103
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-CFJLrHRF.js → hyperframes-player-DG795Hwu.js} +1 -1
- package/dist/assets/{index-BBNVnnyH.js → index-BPJRf2X9.js} +119 -119
- package/dist/assets/{index-CJOzbslX.js → index-BiEtDkae.js} +1 -1
- package/dist/assets/{index-xB5O0hPw.css → index-CQN3M56N.css} +1 -1
- package/dist/assets/{index-C33EDBL2.js → index-DuCZ3pQw.js} +1 -1
- package/dist/{chunk-MTAVNAGW.js → chunk-46XP7J3B.js} +2 -2
- package/dist/{chunk-S65AQH6M.js → chunk-DI4LCJHL.js} +3 -2
- package/dist/chunk-DI4LCJHL.js.map +1 -0
- package/dist/{chunk-7X4XRP3L.js → chunk-HIGHVWYD.js} +2 -2
- package/dist/{domEditingLayers-NU6Z5QWA.js → domEditingLayers-AQBDFNUC.js} +3 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.html +2 -2
- package/dist/index.js +932 -850
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/components/editor/CanvasContextMenu.test.tsx +23 -2
- package/src/components/editor/CanvasContextMenu.tsx +7 -10
- package/src/components/editor/ConnectedDomEditOverlay.tsx +3 -128
- package/src/components/editor/DomEditOverlay.tsx +2 -1
- package/src/components/editor/PropertyPanel.tsx +2 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +13 -0
- package/src/components/editor/domEditOverlayGestures.ts +1 -1
- package/src/components/editor/domEditOverlayStartGesture.ts +2 -2
- package/src/components/editor/gsapAnimatesProperty.ts +0 -29
- package/src/components/editor/gsapLivePreview.test.ts +46 -1
- package/src/components/editor/manualOffsetDrag.test.ts +16 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -5
- package/src/components/editor/plainTranslate.test.ts +38 -22
- package/src/components/editor/plainTranslate.ts +66 -36
- package/src/components/editor/previewReadOnly.overlay.test.tsx +30 -0
- package/src/components/editor/previewReadOnlyContext.tsx +3 -1
- package/src/components/editor/propertyPanelHelpers.ts +3 -0
- package/src/components/editor/propertyPanelTransformCommit.test.ts +51 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +3 -1
- package/src/components/editor/useColorGradingController.test.ts +36 -1
- package/src/components/editor/useColorGradingController.ts +2 -2
- package/src/components/editor/useDomEditNudge.test.tsx +1 -0
- package/src/components/editor/useDomEditNudge.ts +6 -3
- package/src/components/editor/useDomEditZOrder.ts +185 -0
- package/src/components/nle/PreviewOverlaysTestMocks.ts +4 -0
- package/src/domEditExports.test.tsx +92 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +6 -0
- package/src/hooks/useDomEditCommits.test.tsx +94 -90
- package/src/hooks/usePreviewInteraction.test.ts +32 -0
- package/src/hooks/usePreviewInteraction.ts +3 -2
- package/src/index.ts +3 -0
- package/src/player/components/TimelineClipFades.test.tsx +103 -3
- package/src/player/components/TimelineClipFades.tsx +53 -50
- package/src/styles/theme.css +1 -1
- package/src/utils/projectRouting.test.ts +9 -0
- package/src/utils/projectRouting.ts +3 -1
- package/dist/chunk-S65AQH6M.js.map +0 -1
- /package/dist/{chunk-MTAVNAGW.js.map → chunk-46XP7J3B.js.map} +0 -0
- /package/dist/{chunk-7X4XRP3L.js.map → chunk-HIGHVWYD.js.map} +0 -0
- /package/dist/{domEditingLayers-NU6Z5QWA.js.map → domEditingLayers-AQBDFNUC.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.103",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"dockview-react": "8.3.1",
|
|
61
61
|
"gsap": "^3.13.0",
|
|
62
62
|
"mediabunny": "^1.45.3",
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/studio-server": "0.8.
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/player": "0.8.103",
|
|
64
|
+
"@hyperframes/shader-transitions": "0.8.103",
|
|
65
|
+
"@hyperframes/core": "0.8.103",
|
|
66
|
+
"@hyperframes/studio-server": "0.8.103",
|
|
67
|
+
"@hyperframes/parsers": "0.8.103",
|
|
68
|
+
"@hyperframes/sdk": "0.8.103"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.103"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -3,7 +3,7 @@ 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 "./
|
|
6
|
+
import { resolveZIndexEntries } from "./useDomEditZOrder";
|
|
7
7
|
import { useElementLifecycleOps } from "../../hooks/useElementLifecycleOps";
|
|
8
8
|
import { makeLifecycleOpsParams } from "../../hooks/elementLifecycleOpsTestUtils";
|
|
9
9
|
import type { DomEditPatchBatch } from "../../hooks/domEditCommitTypes";
|
|
@@ -193,6 +193,27 @@ function renderCommitHook(captured: CapturedBatchCall[]) {
|
|
|
193
193
|
return { commit: commit!, cleanup: () => act(() => hookRoot.unmount()) };
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
describe("resolveZIndexEntries", () => {
|
|
197
|
+
it("keys a same-class sibling by its index among them, not as the first of its class", () => {
|
|
198
|
+
const parent = document.body.appendChild(document.createElement("div"));
|
|
199
|
+
const [first, second, selected] = ["card", "card", "picked"].map((name) => {
|
|
200
|
+
const child = parent.appendChild(document.createElement("div"));
|
|
201
|
+
child.className = name;
|
|
202
|
+
return child;
|
|
203
|
+
});
|
|
204
|
+
const { entries } = resolveZIndexEntries(
|
|
205
|
+
makeSelection("Picked", selected!),
|
|
206
|
+
[first!, second!].map((element, zIndex) => ({ element, zIndex })),
|
|
207
|
+
null,
|
|
208
|
+
);
|
|
209
|
+
expect(entries.map((entry) => [entry.selector, entry.selectorIndex])).toEqual([
|
|
210
|
+
[".card", 0],
|
|
211
|
+
[".card", 1],
|
|
212
|
+
]);
|
|
213
|
+
parent.remove();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
196
217
|
describe("CanvasContextMenu — z-action commit path", () => {
|
|
197
218
|
it("never mutates live styles itself and persists the position patch for a static element", async () => {
|
|
198
219
|
const { target } = makeStaticFamily();
|
|
@@ -208,7 +229,7 @@ describe("CanvasContextMenu — z-action commit path", () => {
|
|
|
208
229
|
selection,
|
|
209
230
|
onApplyZIndex: (patches, action) => {
|
|
210
231
|
stylesAtApply.push({ zIndex: target.style.zIndex, position: target.style.position });
|
|
211
|
-
const { entries } = resolveZIndexEntries(selection, patches);
|
|
232
|
+
const { entries } = resolveZIndexEntries(selection, patches, null);
|
|
212
233
|
void commit(entries, undefined, action);
|
|
213
234
|
},
|
|
214
235
|
});
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* flow, and captures the TRUE prior styles for its failure rollback.
|
|
17
17
|
*
|
|
18
18
|
* The prop MUST be wired at the call site to route through the full persist
|
|
19
|
-
* path.
|
|
19
|
+
* path. useDomEditZOrder.ts builds the per-patch PatchTargets (the selected
|
|
20
20
|
* element carries its full selection identity; sibling elements are iframe DOM
|
|
21
21
|
* nodes, so their id / selector are derived from the node and they share the
|
|
22
22
|
* selection's sourceFile) and forwards them to handleDomZIndexReorderCommit.
|
|
@@ -29,8 +29,7 @@ import type { DomEditSelection } from "./domEditing";
|
|
|
29
29
|
import { useContextMenuDismiss } from "../../hooks/useContextMenuDismiss";
|
|
30
30
|
import {
|
|
31
31
|
isZOrderActionEnabled,
|
|
32
|
-
|
|
33
|
-
resolveZOrderChange,
|
|
32
|
+
resolveZOrderStep,
|
|
34
33
|
type ZOrderAction,
|
|
35
34
|
type ZOrderPatch,
|
|
36
35
|
} from "./canvasContextMenuZOrder";
|
|
@@ -170,13 +169,11 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
|
|
|
170
169
|
|
|
171
170
|
function handleZAction(action: ZAction) {
|
|
172
171
|
if (!onApplyZIndex) return;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// the z step into a timeline lane move.
|
|
179
|
-
const crossed = resolveCrossedNeighbor(el, action);
|
|
172
|
+
// Resolved BEFORE the commit path mutates live styles. `crossed` is always
|
|
173
|
+
// resolved (not only for the flash): the host mirrors it into a lane move.
|
|
174
|
+
const step = resolveZOrderStep(el, action);
|
|
175
|
+
if (step === null) return;
|
|
176
|
+
const { patches, crossed } = step;
|
|
180
177
|
// Do NOT pre-apply styles here: handleDomZIndexReorderCommit writes the
|
|
181
178
|
// live z-index (and injects position:relative for static elements) in the
|
|
182
179
|
// same synchronous flow, so feedback is still instant — and it must read
|
|
@@ -4,12 +4,7 @@ import {
|
|
|
4
4
|
useDomEditActionsContext,
|
|
5
5
|
useDomEditSelectionContext,
|
|
6
6
|
} from "../../contexts/DomEditContext";
|
|
7
|
-
import {
|
|
8
|
-
import { buildStableSelector } from "./domEditingDom";
|
|
9
|
-
import { deriveTimelineStoreKey } from "../../player/lib/timelineElementHelpers";
|
|
10
|
-
import { zReorderCoalesceKey } from "../../hooks/useElementLifecycleOps";
|
|
11
|
-
import { useCanvasZOrderTimelineMirror } from "../nle/useCanvasZOrderTimelineMirror";
|
|
12
|
-
import { runZLaneGesture } from "../nle/zLaneGesture";
|
|
7
|
+
import { useDomEditZOrder } from "./useDomEditZOrder";
|
|
13
8
|
|
|
14
9
|
type HostInputProps = Pick<
|
|
15
10
|
ComponentProps<typeof DomEditOverlay>,
|
|
@@ -25,94 +20,6 @@ export interface ConnectedDomEditOverlayProps extends HostInputProps {
|
|
|
25
20
|
onTextEditingChange?: (editing: boolean) => void;
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
type ZIndexReorderEntry = {
|
|
29
|
-
element: HTMLElement;
|
|
30
|
-
zIndex: number;
|
|
31
|
-
id?: string;
|
|
32
|
-
selector?: string;
|
|
33
|
-
selectorIndex?: number;
|
|
34
|
-
sourceFile: string;
|
|
35
|
-
/** Timeline store key — lets the commit update the store zIndex synchronously. */
|
|
36
|
-
key?: string;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/** Can this element be robustly re-targeted for a persisted z change? */
|
|
40
|
-
function canTargetZIndexElement(
|
|
41
|
-
element: HTMLElement,
|
|
42
|
-
id: string | undefined,
|
|
43
|
-
selector: string | undefined,
|
|
44
|
-
): boolean {
|
|
45
|
-
return Boolean(id || selector || readHfId(element));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** The selected element carries its full selection identity. */
|
|
49
|
-
function selectedZIndexEntry(sel: DomEditSelection, zIndex: number): ZIndexReorderEntry {
|
|
50
|
-
return {
|
|
51
|
-
element: sel.element,
|
|
52
|
-
zIndex,
|
|
53
|
-
id: sel.id ?? undefined,
|
|
54
|
-
selector: sel.selector,
|
|
55
|
-
selectorIndex: sel.selectorIndex,
|
|
56
|
-
sourceFile: sel.sourceFile,
|
|
57
|
-
key: deriveTimelineStoreKey({
|
|
58
|
-
domId: sel.id ?? undefined,
|
|
59
|
-
selector: sel.selector,
|
|
60
|
-
selectorIndex: sel.selectorIndex,
|
|
61
|
-
sourceFile: sel.sourceFile,
|
|
62
|
-
}),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** A raw iframe sibling in the selection's file; null with no id or selector (z stays live). */
|
|
67
|
-
function siblingZIndexEntry(
|
|
68
|
-
element: HTMLElement,
|
|
69
|
-
zIndex: number,
|
|
70
|
-
sourceFile: string,
|
|
71
|
-
): ZIndexReorderEntry | null {
|
|
72
|
-
const id = element.id || undefined;
|
|
73
|
-
const selector = buildStableSelector(element);
|
|
74
|
-
if (!canTargetZIndexElement(element, id, selector)) return null;
|
|
75
|
-
return {
|
|
76
|
-
element,
|
|
77
|
-
zIndex,
|
|
78
|
-
id,
|
|
79
|
-
selector,
|
|
80
|
-
selectorIndex: undefined,
|
|
81
|
-
sourceFile,
|
|
82
|
-
key: deriveTimelineStoreKey({ domId: id, selector, sourceFile }),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Short human-readable label for a dropped sibling, for the console warning below. */
|
|
87
|
-
function describeZIndexElement(element: HTMLElement): string {
|
|
88
|
-
if (element.id) return `#${element.id}`;
|
|
89
|
-
const firstClass = element.classList.item(0);
|
|
90
|
-
return firstClass
|
|
91
|
-
? `${element.tagName.toLowerCase()}.${firstClass}`
|
|
92
|
-
: element.tagName.toLowerCase();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Resolve z-index patches into commit entries; a sibling with no stable
|
|
96
|
-
// id/selector can't be written to source, so it is returned as `dropped` for
|
|
97
|
-
// the revert-on-reload warning. Exported so tests can drive the menu → commit path.
|
|
98
|
-
export function resolveZIndexEntries(
|
|
99
|
-
sel: DomEditSelection,
|
|
100
|
-
patches: ReadonlyArray<{ element: HTMLElement; zIndex: number }>,
|
|
101
|
-
): { entries: ZIndexReorderEntry[]; dropped: Array<{ element: HTMLElement; zIndex: number }> } {
|
|
102
|
-
const entries: ZIndexReorderEntry[] = [];
|
|
103
|
-
const dropped: Array<{ element: HTMLElement; zIndex: number }> = [];
|
|
104
|
-
for (const patch of patches) {
|
|
105
|
-
if (patch.element === sel.element) {
|
|
106
|
-
entries.push(selectedZIndexEntry(sel, patch.zIndex));
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
const entry = siblingZIndexEntry(patch.element, patch.zIndex, sel.sourceFile);
|
|
110
|
-
if (entry) entries.push(entry);
|
|
111
|
-
else dropped.push(patch);
|
|
112
|
-
}
|
|
113
|
-
return { entries, dropped };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
23
|
// Studio's DomEditOverlay wired to the session in DomEditProvider, so a host
|
|
117
24
|
// outside EditorShell mounts the same canvas editing without its own callbacks.
|
|
118
25
|
export function ConnectedDomEditOverlay({
|
|
@@ -142,9 +49,8 @@ export function ConnectedDomEditOverlay({
|
|
|
142
49
|
handleDomStyleCommit,
|
|
143
50
|
applyMarqueeSelection,
|
|
144
51
|
handleDomEditElementDelete,
|
|
145
|
-
handleDomZIndexReorderCommit,
|
|
146
52
|
} = useDomEditActionsContext();
|
|
147
|
-
const
|
|
53
|
+
const zOrder = useDomEditZOrder();
|
|
148
54
|
|
|
149
55
|
return (
|
|
150
56
|
<DomEditOverlay
|
|
@@ -170,38 +76,7 @@ export function ConnectedDomEditOverlay({
|
|
|
170
76
|
onRotationCommit={handleDomRotationCommit}
|
|
171
77
|
onStyleCommit={handleDomStyleCommit}
|
|
172
78
|
onDeleteSelection={handleDomEditElementDelete}
|
|
173
|
-
onApplyZIndex={
|
|
174
|
-
const { entries, dropped } = resolveZIndexEntries(sel, patches);
|
|
175
|
-
if (dropped.length > 0) {
|
|
176
|
-
// These siblings can't be written to source. Apply their live z
|
|
177
|
-
// anyway so the resolved stacking order renders coherently — it
|
|
178
|
-
// just reverts to the prior order on the next reload.
|
|
179
|
-
for (const patch of dropped) patch.element.style.zIndex = String(patch.zIndex);
|
|
180
|
-
console.warn(
|
|
181
|
-
"[studio] z-index reorder: dropping sibling(s) with no stable id/selector " +
|
|
182
|
-
"(will revert on reload):",
|
|
183
|
-
dropped.map((patch) => describeZIndexElement(patch.element)).join(", "),
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
if (entries.length === 0) return;
|
|
187
|
-
// One coalesce key for the z persist AND the lane mirror folds both into one undo entry;
|
|
188
|
-
// passed explicitly so the mirror shares it by construction, not by formula duplication.
|
|
189
|
-
const coalesceKey = zReorderCoalesceKey(entries, action);
|
|
190
|
-
// One serialized z→lane transaction: the mirror runs only AFTER a durable z commit and
|
|
191
|
-
// no second gesture interleaves (see runZLaneGesture). A failed z commit has already
|
|
192
|
-
// toasted and rolled back, so the catch only keeps its rejection from going unhandled.
|
|
193
|
-
runZLaneGesture({
|
|
194
|
-
commitZ: () => handleDomZIndexReorderCommit(entries, coalesceKey, action),
|
|
195
|
-
mirror: () =>
|
|
196
|
-
mirrorZOrderToTimeline({
|
|
197
|
-
selectionKey: entries.find((e) => e.element === sel.element)?.key,
|
|
198
|
-
action,
|
|
199
|
-
crossed,
|
|
200
|
-
sourceFile: sel.sourceFile,
|
|
201
|
-
coalesceKey,
|
|
202
|
-
}),
|
|
203
|
-
}).catch(() => undefined);
|
|
204
|
-
}}
|
|
79
|
+
onApplyZIndex={zOrder.commit}
|
|
205
80
|
onMarqueeSelect={applyMarqueeSelection}
|
|
206
81
|
/>
|
|
207
82
|
);
|
|
@@ -76,7 +76,7 @@ export interface DomEditOverlayProps {
|
|
|
76
76
|
selection: DomEditSelection,
|
|
77
77
|
options?: { revealPanel?: boolean; additive?: boolean },
|
|
78
78
|
) => void;
|
|
79
|
-
onBlockedMove: (selection: DomEditSelection) => void;
|
|
79
|
+
onBlockedMove: (selection: DomEditSelection, reason?: string) => void;
|
|
80
80
|
onManualDragStart?: () => void;
|
|
81
81
|
onPathOffsetCommit: (
|
|
82
82
|
selection: DomEditSelection,
|
|
@@ -296,6 +296,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
296
296
|
gestureRef,
|
|
297
297
|
groupGestureRef,
|
|
298
298
|
blockedMoveRef,
|
|
299
|
+
onBlockedMoveRef,
|
|
299
300
|
onManualDragStartRef,
|
|
300
301
|
onPathOffsetCommitRef,
|
|
301
302
|
onGroupPathOffsetCommitRef,
|
|
@@ -178,8 +178,9 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
|
|
178
178
|
transformPerspective: 0,
|
|
179
179
|
};
|
|
180
180
|
// Unconditional like the hooks above: must not sit behind the `!element` return below.
|
|
181
|
+
const manualOffset = element ? readMoveOffset(element.element) : { x: 0, y: 0 };
|
|
181
182
|
const { manualOffsetEditingDisabled, manualSizeEditingDisabled, manualRotationEditingDisabled } =
|
|
182
|
-
useManualEditDisabledFlags(element?.capabilities);
|
|
183
|
+
useManualEditDisabledFlags(element?.capabilities, manualOffset);
|
|
183
184
|
|
|
184
185
|
if (!element) {
|
|
185
186
|
return (
|
|
@@ -200,7 +201,6 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
|
|
200
201
|
// selection) so the Timing section shows for pure-GSAP elements with no data-start.
|
|
201
202
|
const sections = resolveEditingSections(domEditSelectionToFacts(element, gsapAnimations.length));
|
|
202
203
|
const showEditableSections = element.capabilities.canEditStyles && sections.style;
|
|
203
|
-
const manualOffset = readMoveOffset(element.element);
|
|
204
204
|
const manualSize = readStudioBoxSize(element.element);
|
|
205
205
|
const resolvedWidth =
|
|
206
206
|
manualSize.width > 0
|
|
@@ -511,3 +511,16 @@ export function isZOrderActionEnabled(
|
|
|
511
511
|
): boolean {
|
|
512
512
|
return resolveZOrderChange(target, action, options) !== null;
|
|
513
513
|
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* One z-order step: the patches plus the sibling it crosses, both read from the
|
|
517
|
+
* same pre-change render order. Null when the action is a no-op.
|
|
518
|
+
*/
|
|
519
|
+
export function resolveZOrderStep(
|
|
520
|
+
target: HTMLElement,
|
|
521
|
+
action: ZOrderAction,
|
|
522
|
+
): { patches: ZOrderPatch[]; crossed: HTMLElement | null } | null {
|
|
523
|
+
const patches = resolveZOrderChange(target, action);
|
|
524
|
+
if (patches === null) return null;
|
|
525
|
+
return { patches, crossed: resolveCrossedNeighbor(target, action) };
|
|
526
|
+
}
|
|
@@ -253,7 +253,7 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
253
253
|
suppressNextBoxClickRef: RefObject<boolean>;
|
|
254
254
|
setOverlayRect: (next: OverlayRect | null) => void;
|
|
255
255
|
setGroupOverlayItems: (next: GroupOverlayItem[]) => void;
|
|
256
|
-
onBlockedMoveRef: RefObject<(selection: DomEditSelection) => void>;
|
|
256
|
+
onBlockedMoveRef: RefObject<(selection: DomEditSelection, reason?: string) => void>;
|
|
257
257
|
onManualDragStartRef: RefObject<(() => void) | undefined>;
|
|
258
258
|
onPathOffsetCommitRef: RefObject<
|
|
259
259
|
(
|
|
@@ -76,7 +76,7 @@ export function startGroupDrag(
|
|
|
76
76
|
restoreManualOffsetDragMembers(members);
|
|
77
77
|
e.preventDefault();
|
|
78
78
|
e.stopPropagation();
|
|
79
|
-
opts.onBlockedMoveRef.current(result.selection);
|
|
79
|
+
opts.onBlockedMoveRef.current(result.selection, result.reason);
|
|
80
80
|
return false;
|
|
81
81
|
}
|
|
82
82
|
members.push(result.member);
|
|
@@ -198,7 +198,7 @@ export function startGesture(
|
|
|
198
198
|
gesture: "drag",
|
|
199
199
|
});
|
|
200
200
|
if (!result.ok) {
|
|
201
|
-
opts.onBlockedMoveRef.current(result.selection);
|
|
201
|
+
opts.onBlockedMoveRef.current(result.selection, result.reason);
|
|
202
202
|
return false;
|
|
203
203
|
}
|
|
204
204
|
pathOffsetMember = result.member;
|
|
@@ -1,32 +1,3 @@
|
|
|
1
|
-
// GSAP's CSSPlugin takes ownership of the element's entire transform stack
|
|
2
|
-
// when it tweens ANY of these — it bakes the CSS `translate` longhand into
|
|
3
|
-
// style.transform at init and writes `translate: none` every tick. Position
|
|
4
|
-
// reapply/strip logic must therefore stand down for all of them, not just x/y.
|
|
5
|
-
const GSAP_TRANSFORM_PROPS = [
|
|
6
|
-
"x",
|
|
7
|
-
"y",
|
|
8
|
-
"xPercent",
|
|
9
|
-
"yPercent",
|
|
10
|
-
"scale",
|
|
11
|
-
"scaleX",
|
|
12
|
-
"scaleY",
|
|
13
|
-
"rotation",
|
|
14
|
-
"rotate",
|
|
15
|
-
"rotationX",
|
|
16
|
-
"rotationY",
|
|
17
|
-
"skewX",
|
|
18
|
-
"skewY",
|
|
19
|
-
"transform",
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* True when GSAP animates any transform-affecting property on the element,
|
|
24
|
-
* meaning GSAP owns `style.transform` and has neutralized CSS `translate`.
|
|
25
|
-
*/
|
|
26
|
-
export function gsapAnimatesTransform(el: HTMLElement): boolean {
|
|
27
|
-
return gsapAnimatesProperty(el, ...GSAP_TRANSFORM_PROPS);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
1
|
/**
|
|
31
2
|
* Checks whether GSAP actively animates one or more CSS/GSAP properties on
|
|
32
3
|
* the given element by inspecting all registered `__timelines`.
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
|
-
import { expect, it, vi } from "vitest";
|
|
2
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
3
3
|
import { createGsapLivePreview } from "./gsapLivePreview";
|
|
4
4
|
import type { DomEditSelection } from "./domEditingTypes";
|
|
5
5
|
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
|
6
6
|
import { readGsapRuntimeValuesForPanel } from "./propertyPanelHelpers";
|
|
7
7
|
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
delete (window as { gsap?: unknown }).gsap;
|
|
10
|
+
delete (window as { __timelines?: unknown }).__timelines;
|
|
11
|
+
});
|
|
12
|
+
|
|
8
13
|
it("previews on the selected element, not an earlier same-id copy in a sub-composition", () => {
|
|
9
14
|
document.body.innerHTML =
|
|
10
15
|
'<div data-composition-id="strip"><div id="card" data-hf-id="hf-inner"></div></div>' +
|
|
@@ -61,3 +66,43 @@ it("the panel reads GSAP values off the node the live preview moves", () => {
|
|
|
61
66
|
});
|
|
62
67
|
expect(getProperty.mock.calls[0]?.[0]).toBe(root);
|
|
63
68
|
});
|
|
69
|
+
|
|
70
|
+
it.each([
|
|
71
|
+
["fades it", { opacity: 0 }, ["opacity"]],
|
|
72
|
+
["fades it about its corner", { opacity: 0, transformOrigin: "0 0" }, ["opacity"]],
|
|
73
|
+
[
|
|
74
|
+
"moves it",
|
|
75
|
+
{ x: 100 },
|
|
76
|
+
[
|
|
77
|
+
"x",
|
|
78
|
+
"y",
|
|
79
|
+
"rotation",
|
|
80
|
+
"rotationX",
|
|
81
|
+
"rotationY",
|
|
82
|
+
"rotationZ",
|
|
83
|
+
"z",
|
|
84
|
+
"scale",
|
|
85
|
+
"transformPerspective",
|
|
86
|
+
"opacity",
|
|
87
|
+
],
|
|
88
|
+
],
|
|
89
|
+
])(
|
|
90
|
+
"the panel reads GSAP's transform only off an element GSAP positions: a tween that %s",
|
|
91
|
+
(_, vars, read) => {
|
|
92
|
+
document.body.innerHTML = '<div id="card"></div>';
|
|
93
|
+
const card = document.querySelector<HTMLElement>("#card");
|
|
94
|
+
const tween = { targets: () => [card], vars, duration: () => 1 };
|
|
95
|
+
const getProperty = vi.fn(() => 1);
|
|
96
|
+
const contentWindow = Object.assign(window, {
|
|
97
|
+
gsap: { getProperty },
|
|
98
|
+
__timelines: { main: { getChildren: () => [tween] } },
|
|
99
|
+
});
|
|
100
|
+
const selection = { id: "card", sourceFile: "index.html", element: card } as DomEditSelection;
|
|
101
|
+
const animations = [{ properties: vars }] as unknown as GsapAnimation[];
|
|
102
|
+
readGsapRuntimeValuesForPanel("anim", animations, selection, {
|
|
103
|
+
current: { contentWindow, contentDocument: document } as unknown as HTMLIFrameElement,
|
|
104
|
+
});
|
|
105
|
+
const props = getProperty.mock.calls.map((call) => (call as unknown[])[1]);
|
|
106
|
+
expect(props).toEqual(read);
|
|
107
|
+
},
|
|
108
|
+
);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
resolveManualOffsetForPointerDelta,
|
|
14
14
|
type ManualOffsetDragMatrix,
|
|
15
15
|
} from "./manualOffsetDrag";
|
|
16
|
+
import { UNREADABLE_TRANSLATE } from "./plainTranslate";
|
|
16
17
|
import { STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP } from "./manualEdits";
|
|
17
18
|
import { computeDraggedGsapPosition } from "../../hooks/draggedGsapPosition";
|
|
18
19
|
|
|
@@ -596,6 +597,21 @@ describe("a move of an element GSAP does not position", () => {
|
|
|
596
597
|
return { window, element, gsapCalls, member };
|
|
597
598
|
}
|
|
598
599
|
|
|
600
|
+
it("refuses a translate it can't read instead of guessing, and leaves the element alone", () => {
|
|
601
|
+
const { element } = plainBox();
|
|
602
|
+
element.style.setProperty("translate", "abs(10% - 50px) 0px");
|
|
603
|
+
const result = createManualOffsetDragMember({
|
|
604
|
+
key: "box",
|
|
605
|
+
selection: { element } as never,
|
|
606
|
+
element,
|
|
607
|
+
rect: { left: 10, top: 20, width: 100, height: 50, editScaleX: 1, editScaleY: 1 },
|
|
608
|
+
gesture: "drag",
|
|
609
|
+
});
|
|
610
|
+
expect(result.ok).toBe(false);
|
|
611
|
+
expect(result.ok ? "" : result.reason).toBe(UNREADABLE_TRANSLATE);
|
|
612
|
+
expect(element.style.getPropertyValue("translate")).toBe("abs(10% - 50px) 0px");
|
|
613
|
+
});
|
|
614
|
+
|
|
599
615
|
it("drafts and drops the element's own translate in plain px, never touching GSAP", () => {
|
|
600
616
|
const { element, gsapCalls, member } = plainBox({ gsapLoaded: true });
|
|
601
617
|
const m = member();
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "./manualEdits";
|
|
13
13
|
import { computeDraggedGsapPosition } from "../../hooks/draggedGsapPosition";
|
|
14
14
|
import { gsapWritesPosition } from "../../hooks/gsapRuntimeKeyframes";
|
|
15
|
-
import { readTranslatePx, writeTranslatePx } from "./plainTranslate";
|
|
15
|
+
import { readTranslatePx, UNREADABLE_TRANSLATE, writeTranslatePx } from "./plainTranslate";
|
|
16
16
|
|
|
17
17
|
interface OffsetDragGsap {
|
|
18
18
|
set: (el: Element, vars: Record<string, number | string>) => void;
|
|
@@ -345,6 +345,8 @@ export function createManualOffsetDragMember(input: {
|
|
|
345
345
|
const initialOffset = plainTranslate
|
|
346
346
|
? readTranslatePx(input.element)
|
|
347
347
|
: readAppliedStudioPathOffset(input.element);
|
|
348
|
+
if (!finitePoint(initialOffset))
|
|
349
|
+
return { ok: false, reason: UNREADABLE_TRANSLATE, selection: input.selection };
|
|
348
350
|
const win = input.element.ownerDocument.defaultView as
|
|
349
351
|
| (Window & {
|
|
350
352
|
gsap?: { getProperty?: (el: Element, prop: string) => number };
|
|
@@ -382,10 +384,8 @@ export function createManualOffsetDragMember(input: {
|
|
|
382
384
|
plainTranslate,
|
|
383
385
|
});
|
|
384
386
|
if (!measured.ok) {
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
// GSAP position from the iframe runtime, so visual imprecision during
|
|
388
|
-
// drag is acceptable — the final committed position is always exact.
|
|
387
|
+
// GSAP transforms can defeat the probe: approximate with the preview scale. The commit
|
|
388
|
+
// reads GSAP's real position, so only the drag preview is imprecise.
|
|
389
389
|
const scaleX = input.rect.editScaleX || 1;
|
|
390
390
|
const scaleY = input.rect.editScaleY || 1;
|
|
391
391
|
const w = readTransformWDivisor(input.element);
|
|
@@ -28,18 +28,31 @@ describe("readTranslatePx", () => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
it.each([
|
|
31
|
-
"min(10px, 5%)
|
|
32
|
-
"
|
|
33
|
-
"calc(10px +)
|
|
34
|
-
"
|
|
35
|
-
])("
|
|
36
|
-
|
|
37
|
-
const before = element.getAttribute("style");
|
|
38
|
-
const read = readTranslatePx(element);
|
|
39
|
-
expect(Number.isFinite(read.x) && Number.isFinite(read.y)).toBe(true);
|
|
40
|
-
expect(element.getAttribute("style")).toBe(before);
|
|
31
|
+
["min(10px, 5%) max(-20%, -40px)", { x: 10, y: -32 }],
|
|
32
|
+
["clamp(0px, 10%, 30px) 5px", { x: 24, y: 5 }],
|
|
33
|
+
["calc(min(50%, 100px) * 2 - (10px + 5%)) calc(-1 * clamp(10%, 1px, 20%))", { x: 178, y: -16 }],
|
|
34
|
+
["calc(2 * 10px) calc(80px / 4)", { x: 20, y: 20 }],
|
|
35
|
+
])("works out %s by arithmetic", (translate, want) => {
|
|
36
|
+
expect(readTranslatePx(box(`translate: ${translate}`))).toEqual(want);
|
|
41
37
|
});
|
|
42
38
|
|
|
39
|
+
it.each(["calc(10px +) 0px", "abs(10% - 50px) 0px", "round(10%, 7px) 0px", "min(10px 5%) 0px"])(
|
|
40
|
+
"reads %s as NaN, never as a guess",
|
|
41
|
+
(translate) => {
|
|
42
|
+
expect(readTranslatePx(box(`translate: ${translate}`)).x).toBeNaN();
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
it.each(["content-box", "fill-box"])(
|
|
47
|
+
"resolves a percent against the content box under transform-box: %s",
|
|
48
|
+
(transformBox) => {
|
|
49
|
+
const element = box(
|
|
50
|
+
`translate: 50% 50%; padding: 10px; border: 5px solid; box-sizing: content-box; transform-box: ${transformBox}`,
|
|
51
|
+
);
|
|
52
|
+
expect(readTranslatePx(element)).toEqual({ x: 120, y: 80 });
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
43
56
|
it("counts padding and border in the box a percent resolves against", () => {
|
|
44
57
|
const element = box(
|
|
45
58
|
"translate: 50% 50%; padding: 10px; border: 5px solid; box-sizing: content-box",
|
|
@@ -47,16 +60,19 @@ describe("readTranslatePx", () => {
|
|
|
47
60
|
expect(readTranslatePx(element)).toEqual({ x: 135, y: 95 });
|
|
48
61
|
});
|
|
49
62
|
|
|
50
|
-
it("
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
it.each(["-50% -50%", "min(10px, 5%) clamp(0px, 10%, 30px)"])(
|
|
64
|
+
"writes nothing to the element while it reads %s",
|
|
65
|
+
(translate) => {
|
|
66
|
+
const element = box(`translate: ${translate}; transform: rotate(5deg)`);
|
|
67
|
+
const before = element.getAttribute("style");
|
|
68
|
+
const writes: MutationRecord[] = [];
|
|
69
|
+
const observer = new MutationObserver((records) => writes.push(...records));
|
|
70
|
+
observer.observe(element, { attributes: true });
|
|
71
|
+
readTranslatePx(element);
|
|
72
|
+
writes.push(...observer.takeRecords());
|
|
73
|
+
observer.disconnect();
|
|
74
|
+
expect(writes).toEqual([]);
|
|
75
|
+
expect(element.getAttribute("style")).toBe(before);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
62
78
|
});
|