@hyperframes/studio 0.8.94 → 0.8.96
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-Bkm-3G56.js → hyperframes-player-ClfxdXeC.js} +1 -1
- package/dist/assets/{index-n2ZRiO44.js → index-BNQLP5_v.js} +1 -1
- package/dist/assets/{index-BupvpoiM.js → index-CSHEHeOo.js} +176 -176
- package/dist/assets/{index-BUn5H_np.js → index-D8ztHPd3.js} +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.html +1 -1
- package/dist/index.js +109 -43
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/components/editor/DomEditCropHandles.test.tsx +63 -0
- package/src/components/editor/DomEditCropHandles.tsx +7 -10
- package/src/components/editor/DomEditSelectionChrome.tsx +3 -3
- package/src/components/editor/domEditOverlayGestures.ts +16 -0
- package/src/components/editor/groupDragMove.ts +13 -6
- package/src/components/editor/manualEditsSeekReapply.ts +5 -1
- package/src/components/editor/manualOffsetDrag.test.ts +56 -0
- package/src/components/editor/manualOffsetDrag.ts +16 -9
- package/src/components/editor/shiftDragAxisLock.test.tsx +358 -0
- package/src/components/editor/snapEngine.ts +3 -2
- package/src/components/editor/useDomEditOverlayGestures.ts +24 -9
- package/src/player/components/Timeline.ghostLane.test.tsx +126 -0
- package/src/player/components/Timeline.rulerSeek.test.tsx +7 -0
- package/src/player/components/Timeline.test.ts +3 -3
- package/src/player/components/TimelineCanvas.tsx +30 -2
- package/src/player/components/TimelineProvider.tsx +1 -0
- package/src/player/components/timelineDragDrop.test.tsx +14 -12
- package/src/player/components/timelineLayout.test.ts +40 -11
- package/src/player/components/timelineLayout.ts +6 -6
- package/src/player/components/useTimelineProviderState.tsx +3 -1
- package/src/utils/gsapSoftReload.test.ts +51 -2
- package/src/utils/gsapSoftReload.ts +24 -10
- package/src/utils/gsapUndoRestore.test.ts +49 -0
- package/src/utils/gsapUndoRestore.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.96",
|
|
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/core": "0.8.
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/core": "0.8.96",
|
|
64
|
+
"@hyperframes/player": "0.8.96",
|
|
65
|
+
"@hyperframes/parsers": "0.8.96",
|
|
66
|
+
"@hyperframes/sdk": "0.8.96",
|
|
67
|
+
"@hyperframes/studio-server": "0.8.96",
|
|
68
|
+
"@hyperframes/shader-transitions": "0.8.96"
|
|
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.96"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -126,6 +126,69 @@ describe("DomEditCropHandles clip lift/restore", () => {
|
|
|
126
126
|
expect(a.style.getPropertyValue("clip-path")).toBe("inset(10px 30px 10px 10px)");
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
it.each([
|
|
130
|
+
{ name: "a crop edge", clip: "inset(10px)", handle: "Crop right", dx: -20, dy: 0 },
|
|
131
|
+
{
|
|
132
|
+
name: "the reposition handle on fractional insets",
|
|
133
|
+
clip: "inset(20px 0.42px 40px 0.03px)",
|
|
134
|
+
handle: "Reposition crop",
|
|
135
|
+
dx: 0,
|
|
136
|
+
dy: 10,
|
|
137
|
+
},
|
|
138
|
+
])("commits nothing when $name is dragged back to where it started", (c) => {
|
|
139
|
+
const onStyleCommit = vi.fn();
|
|
140
|
+
render(makeEl("a", c.clip), onStyleCommit);
|
|
141
|
+
const handle = document.querySelector<HTMLButtonElement>(`[aria-label="${c.handle}"]`)!;
|
|
142
|
+
for (const [type, d] of [
|
|
143
|
+
["pointerdown", 0],
|
|
144
|
+
["pointermove", 1],
|
|
145
|
+
["pointermove", 0],
|
|
146
|
+
["pointerup", 0],
|
|
147
|
+
] as const) {
|
|
148
|
+
const at = { clientX: 100 + d * c.dx, clientY: 50 + d * c.dy };
|
|
149
|
+
act(() =>
|
|
150
|
+
handle.dispatchEvent(new PointerEvent(type, { bubbles: true, pointerId: 3, ...at })),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
expect(onStyleCommit).not.toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("commits where the pointer was released, even before its last move renders", () => {
|
|
157
|
+
const onStyleCommit = vi.fn();
|
|
158
|
+
render(makeEl("a", "inset(10px)"), onStyleCommit);
|
|
159
|
+
const handle = document.querySelector<HTMLButtonElement>('[aria-label="Crop right"]')!;
|
|
160
|
+
act(() => {
|
|
161
|
+
for (const [type, clientX] of [
|
|
162
|
+
["pointerdown", 100],
|
|
163
|
+
["pointermove", 90],
|
|
164
|
+
["pointermove", 80],
|
|
165
|
+
["pointerup", 80],
|
|
166
|
+
] as const) {
|
|
167
|
+
handle.dispatchEvent(new PointerEvent(type, { bubbles: true, pointerId: 4, clientX }));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
expect(onStyleCommit).toHaveBeenCalledWith("clip-path", "inset(10px 30px 10px 10px)");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("keeps a first crop on deselect when the release beats the last render", async () => {
|
|
174
|
+
const a = makeEl("a", "");
|
|
175
|
+
const { root } = render(a, (property, value) => void a.style.setProperty(property, value));
|
|
176
|
+
const handle = document.querySelector<HTMLButtonElement>('[aria-label="Crop right"]')!;
|
|
177
|
+
act(() => {
|
|
178
|
+
for (const [type, clientX] of [
|
|
179
|
+
["pointerdown", 100],
|
|
180
|
+
["pointermove", 90],
|
|
181
|
+
["pointermove", 80],
|
|
182
|
+
["pointerup", 80],
|
|
183
|
+
] as const) {
|
|
184
|
+
handle.dispatchEvent(new PointerEvent(type, { bubbles: true, pointerId: 5, clientX }));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
await act(async () => undefined);
|
|
188
|
+
act(() => root.unmount());
|
|
189
|
+
expect(a.style.getPropertyValue("clip-path")).toBe("inset(0px 20px 0px 0px)");
|
|
190
|
+
});
|
|
191
|
+
|
|
129
192
|
it("re-lifts when the crop commit rejects", async () => {
|
|
130
193
|
const a = makeEl("a", "inset(10px)");
|
|
131
194
|
const onStyleCommit = vi.fn((property: string, value: string) => {
|
|
@@ -18,7 +18,7 @@ interface CropGestureState {
|
|
|
18
18
|
startX: number;
|
|
19
19
|
startY: number;
|
|
20
20
|
startInsets: ClipPathInsetSides;
|
|
21
|
-
|
|
21
|
+
insets: ClipPathInsetSides;
|
|
22
22
|
/** Element frame captured at gesture start: pointer deltas rotate into it. */
|
|
23
23
|
angleDeg: number;
|
|
24
24
|
scaleX: number;
|
|
@@ -186,7 +186,7 @@ export function DomEditCropHandles({
|
|
|
186
186
|
startX: event.clientX,
|
|
187
187
|
startY: event.clientY,
|
|
188
188
|
startInsets: state.insets,
|
|
189
|
-
|
|
189
|
+
insets: state.insets,
|
|
190
190
|
angleDeg: frame.angleDeg,
|
|
191
191
|
scaleX: frame.scaleX,
|
|
192
192
|
scaleY: frame.scaleY,
|
|
@@ -217,7 +217,7 @@ export function DomEditCropHandles({
|
|
|
217
217
|
gesture.edge === "move"
|
|
218
218
|
? resolveCropInsetFromMoveDrag(drag)
|
|
219
219
|
: resolveCropInsetFromEdgeDrag({ ...drag, edge: gesture.edge, width, height });
|
|
220
|
-
gesture.
|
|
220
|
+
gesture.insets = nextInsets;
|
|
221
221
|
setState((prev) => ({ ...prev, insets: nextInsets }));
|
|
222
222
|
};
|
|
223
223
|
|
|
@@ -234,7 +234,6 @@ export function DomEditCropHandles({
|
|
|
234
234
|
const finishCropGesture = (event: ReactPointerEvent<HTMLElement>) => {
|
|
235
235
|
const gesture = endCropGesture(event);
|
|
236
236
|
if (!gesture) return;
|
|
237
|
-
if (!gesture.didMove) return;
|
|
238
237
|
// Commit to the file. The commit path re-applies the value to the live
|
|
239
238
|
// element synchronously, so re-lift in the same turn to keep showing the full
|
|
240
239
|
// content + dim while selected. Re-lift again on rejection so a failed commit
|
|
@@ -243,12 +242,10 @@ export function DomEditCropHandles({
|
|
|
243
242
|
const reLift = () => {
|
|
244
243
|
if (liftedRef.current) el.style.setProperty("clip-path", "none");
|
|
245
244
|
};
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
state.insets.bottom > 0 ||
|
|
251
|
-
state.insets.left > 0;
|
|
245
|
+
const { insets } = gesture;
|
|
246
|
+
const committedValue = buildInsetClipPathSides(insets, state.radius);
|
|
247
|
+
if (committedValue === buildInsetClipPathSides(gesture.startInsets, state.radius)) return;
|
|
248
|
+
const cropped = insets.top > 0 || insets.right > 0 || insets.bottom > 0 || insets.left > 0;
|
|
252
249
|
const commit = onStyleCommit?.("clip-path", committedValue);
|
|
253
250
|
// handleDomStyleCommit applies the persisted value to the live element
|
|
254
251
|
// synchronously before its first await. Restore the crop-mode lift in this
|
|
@@ -105,7 +105,7 @@ export function DomEditGroupChrome({
|
|
|
105
105
|
cursor: !allowBodyDrag ? undefined : canManipulate && groupCanMove ? "move" : "default",
|
|
106
106
|
}}
|
|
107
107
|
onPointerDown={(e) => {
|
|
108
|
-
if (!canManipulate || e.shiftKey
|
|
108
|
+
if (!canManipulate || !allowBodyDrag || (e.shiftKey && !groupCanMove)) return;
|
|
109
109
|
gestures.startGroupDrag(e);
|
|
110
110
|
}}
|
|
111
111
|
onMouseDown={onBoxMouseDown}
|
|
@@ -218,12 +218,12 @@ export function DomEditSelectionChrome({
|
|
|
218
218
|
e.stopPropagation();
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
221
|
-
if (!allowBodyDrag || !canManipulate
|
|
221
|
+
if (!allowBodyDrag || !canManipulate) return;
|
|
222
222
|
if (selection.capabilities.canApplyManualOffset) {
|
|
223
223
|
gestures.startGesture("drag", e);
|
|
224
224
|
return;
|
|
225
225
|
}
|
|
226
|
-
gestures.startBlockedMove(e, selection);
|
|
226
|
+
if (!e.shiftKey) gestures.startBlockedMove(e, selection);
|
|
227
227
|
}}
|
|
228
228
|
onMouseDown={onBoxMouseDown}
|
|
229
229
|
onClick={onBoxClick}
|
|
@@ -18,6 +18,20 @@ export type GestureKind = "drag" | "resize" | "rotate";
|
|
|
18
18
|
export type ResizeHandle = "nw" | "ne" | "sw" | "se";
|
|
19
19
|
|
|
20
20
|
export const BLOCKED_MOVE_THRESHOLD_PX = 4;
|
|
21
|
+
|
|
22
|
+
export interface AxisLockedDelta {
|
|
23
|
+
dx: number;
|
|
24
|
+
dy: number;
|
|
25
|
+
lockedAxis?: "x" | "y";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function lockDragToDominantAxis(dx: number, dy: number, shiftKey: boolean): AxisLockedDelta {
|
|
29
|
+
if (!shiftKey) return { dx, dy };
|
|
30
|
+
return Math.abs(dx) >= Math.abs(dy)
|
|
31
|
+
? { dx, dy: 0, lockedAxis: "y" }
|
|
32
|
+
: { dx: 0, dy, lockedAxis: "x" };
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
const ROTATION_COMMIT_EPSILON_DEGREES = 0.05;
|
|
22
36
|
const ROTATION_SNAP_DEGREES = 15;
|
|
23
37
|
/**
|
|
@@ -73,6 +87,7 @@ export interface GestureState {
|
|
|
73
87
|
snapContext?: SnapContext;
|
|
74
88
|
lastSnappedDx?: number;
|
|
75
89
|
lastSnappedDy?: number;
|
|
90
|
+
travelled?: boolean;
|
|
76
91
|
/** Corner the resize gesture grabbed (resize gestures only). */
|
|
77
92
|
resizeHandle?: ResizeHandle;
|
|
78
93
|
/** Last anchoring translation applied during a corner resize (overlay px). */
|
|
@@ -96,6 +111,7 @@ export interface GroupGestureState {
|
|
|
96
111
|
snapContext?: SnapContext;
|
|
97
112
|
lastSnappedDx?: number;
|
|
98
113
|
lastSnappedDy?: number;
|
|
114
|
+
travelled?: boolean;
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
export interface BlockedMoveState {
|
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
SNAP_THRESHOLD_PX,
|
|
7
7
|
} from "./snapEngine";
|
|
8
8
|
import { applyManualOffsetDragDraft } from "./manualOffsetDrag";
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
lockDragToDominantAxis,
|
|
11
|
+
type AxisLockedDelta,
|
|
12
|
+
type GroupGestureState,
|
|
13
|
+
type UseDomEditOverlayGesturesOptions,
|
|
14
|
+
} from "./domEditOverlayGestures";
|
|
10
15
|
import type { GroupOverlayItem } from "./domEditOverlayGeometry";
|
|
11
16
|
import {
|
|
12
17
|
findNonRigidMembers,
|
|
@@ -33,7 +38,7 @@ export function createGroupDragMover(
|
|
|
33
38
|
const snapGroupDelta = (
|
|
34
39
|
groupG: GroupGestureState,
|
|
35
40
|
e: React.PointerEvent<HTMLDivElement>,
|
|
36
|
-
proposed:
|
|
41
|
+
proposed: AxisLockedDelta,
|
|
37
42
|
) => {
|
|
38
43
|
const sc = groupG.snapContext;
|
|
39
44
|
if (!sc?.snapEnabled || sc.targets.length === 0) return proposed;
|
|
@@ -52,6 +57,7 @@ export function createGroupDragMover(
|
|
|
52
57
|
gridEdges: sc.gridEdges ?? undefined,
|
|
53
58
|
threshold: SNAP_THRESHOLD_PX,
|
|
54
59
|
disabled: e.altKey,
|
|
60
|
+
lockedAxis: proposed.lockedAxis,
|
|
55
61
|
});
|
|
56
62
|
const movingRect = {
|
|
57
63
|
...groupBounds,
|
|
@@ -75,10 +81,11 @@ export function createGroupDragMover(
|
|
|
75
81
|
lastGesture = groupG;
|
|
76
82
|
lastGroupPositions = {};
|
|
77
83
|
}
|
|
78
|
-
const { dx, dy } = snapGroupDelta(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
const { dx, dy } = snapGroupDelta(
|
|
85
|
+
groupG,
|
|
86
|
+
e,
|
|
87
|
+
lockDragToDominantAxis(e.clientX - groupG.startX, e.clientY - groupG.startY, e.shiftKey),
|
|
88
|
+
);
|
|
82
89
|
groupG.lastSnappedDx = dx;
|
|
83
90
|
groupG.lastSnappedDy = dy;
|
|
84
91
|
|
|
@@ -67,7 +67,11 @@ function reapplyRotations(doc: Document): void {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Every mark a reapply below acts on, the legacy double-prefixed ones included.
|
|
70
|
-
const STUDIO_EDIT_ATTRS = [
|
|
70
|
+
export const STUDIO_EDIT_ATTRS = [
|
|
71
|
+
STUDIO_PATH_OFFSET_ATTR,
|
|
72
|
+
STUDIO_BOX_SIZE_ATTR,
|
|
73
|
+
STUDIO_ROTATION_ATTR,
|
|
74
|
+
]
|
|
71
75
|
.flatMap((attr) => [attr, `data-${attr}`])
|
|
72
76
|
.concat(STUDIO_MOTION_ATTR);
|
|
73
77
|
// One selector each: Chrome answers a lone attribute selector without walking the DOM, a comma list it walks.
|
|
@@ -449,6 +449,62 @@ describe("GSAP-element drag — dot-a flies regressions", () => {
|
|
|
449
449
|
});
|
|
450
450
|
});
|
|
451
451
|
|
|
452
|
+
describe("a stylesheet translate after a committed GSAP drag", () => {
|
|
453
|
+
function draggedTitle(css: string, oldOffsetChannel = false, withGsap = true) {
|
|
454
|
+
const window = new Window();
|
|
455
|
+
const style = window.document.createElement("style");
|
|
456
|
+
style.textContent = css;
|
|
457
|
+
window.document.head.append(style);
|
|
458
|
+
const element = window.document.createElement("h1");
|
|
459
|
+
element.id = "title";
|
|
460
|
+
if (oldOffsetChannel) {
|
|
461
|
+
element.setAttribute("data-hf-studio-path-offset", "true");
|
|
462
|
+
element.style.setProperty(STUDIO_OFFSET_X_PROP, "30px");
|
|
463
|
+
element.style.setProperty(STUDIO_OFFSET_Y_PROP, "0px");
|
|
464
|
+
element.style.translate = `var(${STUDIO_OFFSET_X_PROP}, 0px) var(${STUDIO_OFFSET_Y_PROP}, 0px)`;
|
|
465
|
+
}
|
|
466
|
+
window.document.body.append(element);
|
|
467
|
+
element.getBoundingClientRect = () => new window.DOMRect(10, 20, 100, 50);
|
|
468
|
+
(window as unknown as { gsap: unknown }).gsap = withGsap && {
|
|
469
|
+
set: (el: HTMLElement, vars: { x?: number; y?: number }) =>
|
|
470
|
+
el.style.setProperty("transform", `translate(${vars.x ?? 0}px, ${vars.y ?? 0}px)`),
|
|
471
|
+
getProperty: () => 0,
|
|
472
|
+
};
|
|
473
|
+
const result = createManualOffsetDragMember({
|
|
474
|
+
key: "title",
|
|
475
|
+
selection: { element } as never,
|
|
476
|
+
element,
|
|
477
|
+
rect: { left: 10, top: 20, width: 100, height: 50, editScaleX: 1, editScaleY: 1 },
|
|
478
|
+
});
|
|
479
|
+
if (!result.ok) throw new Error("member not created");
|
|
480
|
+
applyManualOffsetDragCommit(result.member, 40, 30);
|
|
481
|
+
if (withGsap) expect(element.style.getPropertyValue("translate")).toBe("none");
|
|
482
|
+
endManualOffsetDragMembers([result.member]);
|
|
483
|
+
return element;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
it("stays masked, so it is not applied on top of the committed transform", () => {
|
|
487
|
+
expect(
|
|
488
|
+
draggedTitle("#title { translate: 0 -200px; }").style.getPropertyValue("translate"),
|
|
489
|
+
).toBe("none");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("stays masked for an element still on the old offset channel", () => {
|
|
493
|
+
const element = draggedTitle("#title { translate: 0 -200px; }", true);
|
|
494
|
+
expect(element.hasAttribute("data-hf-studio-path-offset")).toBe(false);
|
|
495
|
+
expect(element.style.getPropertyValue("translate")).toBe("none");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("is not masked when GSAP does not own the position", () => {
|
|
499
|
+
const element = draggedTitle("#title { translate: 0 -200px; }", true, false);
|
|
500
|
+
expect(element.style.getPropertyValue("translate")).toContain(STUDIO_OFFSET_X_PROP);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("is not left behind when nothing sits under the draft's mask", () => {
|
|
504
|
+
expect(draggedTitle("").style.getPropertyValue("translate")).toBe("");
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
452
508
|
describe("resumeGsapTimelines", () => {
|
|
453
509
|
it("unpauses exactly the timelines the drag start paused, then re-seeks the player", () => {
|
|
454
510
|
const window = new Window();
|
|
@@ -514,6 +514,7 @@ export function applyManualOffsetNudgeCommit(
|
|
|
514
514
|
|
|
515
515
|
function restoreManualOffsetDragMember(member: ManualOffsetDragMember): void {
|
|
516
516
|
restoreStudioPathOffset(member.element, member.initialPathOffset);
|
|
517
|
+
getOffsetDragGsap(member.element)?.set(member.element, { ...member.baseGsap });
|
|
517
518
|
endStudioManualEditGesture(member.element, member.gestureToken);
|
|
518
519
|
}
|
|
519
520
|
|
|
@@ -525,6 +526,18 @@ export function restoreManualOffsetDragMembers(members: ManualOffsetDragMember[]
|
|
|
525
526
|
}
|
|
526
527
|
}
|
|
527
528
|
|
|
529
|
+
// Drop the draft's `translate: none` so the soft reload starts clean (#1673). When GSAP owns
|
|
530
|
+
// the position it folded any translate into its transform, so one still showing applies twice.
|
|
531
|
+
function settleDraftTranslate(element: HTMLElement, gsapOwnsPosition: boolean): void {
|
|
532
|
+
if (element.style.getPropertyValue("translate") === "none") {
|
|
533
|
+
element.style.removeProperty("translate");
|
|
534
|
+
}
|
|
535
|
+
if (!gsapOwnsPosition) return;
|
|
536
|
+
const view = element.ownerDocument.defaultView;
|
|
537
|
+
const underneath = view?.getComputedStyle(element).getPropertyValue("translate");
|
|
538
|
+
if (underneath && underneath !== "none") element.style.setProperty("translate", "none");
|
|
539
|
+
}
|
|
540
|
+
|
|
528
541
|
/** Teardown after a COMMITTED drag. */
|
|
529
542
|
export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): void {
|
|
530
543
|
for (const member of members) {
|
|
@@ -533,23 +546,17 @@ export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): v
|
|
|
533
546
|
member.element.removeAttribute("data-hf-drag-initial-offset-y");
|
|
534
547
|
member.element.removeAttribute("data-hf-drag-gsap-base-x");
|
|
535
548
|
member.element.removeAttribute("data-hf-drag-gsap-base-y");
|
|
536
|
-
// Clear the draft's `translate: none` so the soft reload starts clean —
|
|
537
|
-
// otherwise button-less pointermoves after the reload compute deltas
|
|
538
|
-
// from a stale base and fling the element off-screen (#1673).
|
|
539
549
|
// Do NOT clearProps:"transform" — that nukes the committed GSAP position
|
|
540
550
|
// and causes a visual snap-back before the soft reload re-applies it.
|
|
541
|
-
|
|
542
|
-
member.element.style.removeProperty("translate");
|
|
543
|
-
}
|
|
551
|
+
const gsapOwnsPosition = Boolean(getOffsetDragGsap(member.element));
|
|
544
552
|
// Migration: when GSAP owns the position (the committed value lives in the
|
|
545
553
|
// GSAP transform), the legacy `--hf-studio-offset` CSS channel is obsolete.
|
|
546
554
|
// Clear it on the LIVE element — otherwise the leftover `translate:
|
|
547
555
|
// var(--hf-studio-offset)` composes with the GSAP transform and the element
|
|
548
556
|
// renders offset by the stale value until a full page reload (the source is
|
|
549
557
|
// already stripped). clearStudioPathOffset leaves `transform` untouched.
|
|
550
|
-
if (
|
|
551
|
-
|
|
552
|
-
}
|
|
558
|
+
if (gsapOwnsPosition) clearStudioPathOffset(member.element);
|
|
559
|
+
settleDraftTranslate(member.element, gsapOwnsPosition);
|
|
553
560
|
resumeGsapTimelines(member.element);
|
|
554
561
|
}
|
|
555
562
|
}
|