@hyperframes/studio 0.8.115 → 0.8.117
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-C-9N8nk0.js → hyperframes-player-5fcgSstJ.js} +1 -1
- package/dist/assets/{index-CAvZ0AA-.js → index-C5qKdpgl.js} +205 -203
- package/dist/assets/{index-BoUM6Xqb.js → index-XJSawwYe.js} +1 -1
- package/dist/assets/{index-CVuorIMv.js → index-tg2LXUeu.js} +1 -1
- package/dist/{chunk-TXZMMOMA.js → chunk-3F47EFCT.js} +100 -2
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{domEditingLayers-D7VYFJJD.js → domEditingLayers-OL7GLSS4.js} +2 -2
- package/dist/index.d.ts +132 -41
- package/dist/index.html +1 -1
- package/dist/index.js +1184 -1084
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/editor/DomEditOverlay.tsx +1 -0
- package/src/components/editor/domEditOverlayGestures.ts +1 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +2 -2
- package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualOffsetDrag.ts +8 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/plainTranslate.ts +5 -3
- package/src/components/editor/probeSourceElement.ts +2 -1
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelHelpers.ts +2 -2
- package/src/components/editor/propertyPanelTransformCommit.ts +2 -2
- package/src/components/editor/rotationDraft.ts +2 -1
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +2 -2
- package/src/components/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/hooks/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +2 -105
- package/src/hooks/gsapDragPositionCommit.ts +30 -393
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +63 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -169
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapRuntimeKeyframes.ts +16 -3
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +9 -1
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +26 -58
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +2 -1
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/plainRotation.ts +5 -1
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditPersist.test.tsx +335 -0
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +23 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +5 -1
- package/src/hooks/useDomGeometryCommit.ts +7 -9
- package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +32 -1
- package/src/hooks/useEnableKeyframes.ts +2 -1
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +19 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +4 -2
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.test.ts +39 -3
- package/src/hooks/useFreezeFrame.ts +7 -3
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +42 -29
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +17 -8
- package/src/hooks/useGsapAwareGroupMove.ts +18 -18
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapScriptCommits.test.tsx +72 -2
- package/src/hooks/useGsapScriptCommits.ts +21 -17
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +302 -3
- package/src/hooks/usePersistentEditHistory.ts +50 -17
- package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.ts +2 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- package/src/player/sceneSwap.ts +2 -1
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.test.ts +39 -0
- package/src/utils/gsapSoftReload.ts +9 -14
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -41
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +5 -6
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +5 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.ts +2 -1
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +2 -1
- package/dist/chunk-TXZMMOMA.js.map +0 -1
- /package/dist/{domEditingLayers-D7VYFJJD.js.map → domEditingLayers-OL7GLSS4.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.117",
|
|
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.117",
|
|
64
|
+
"@hyperframes/parsers": "0.8.117",
|
|
65
|
+
"@hyperframes/sdk": "0.8.117",
|
|
66
|
+
"@hyperframes/shader-transitions": "0.8.117",
|
|
67
|
+
"@hyperframes/studio-server": "0.8.117",
|
|
68
|
+
"@hyperframes/player": "0.8.117"
|
|
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.117"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -5,6 +5,7 @@ import { useMountEffect } from "../../hooks/useMountEffect";
|
|
|
5
5
|
import { trackEvent } from "../../telemetry/client";
|
|
6
6
|
import type { CaptionStyle } from "../types";
|
|
7
7
|
import { studioWriteHeaders } from "../../utils/studioFileVersion";
|
|
8
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
8
9
|
|
|
9
10
|
interface CaptionOverrideEntry {
|
|
10
11
|
wordId?: string;
|
|
@@ -93,11 +94,14 @@ export function useCaptionSync(projectId: string | null) {
|
|
|
93
94
|
const seqAtSave = editSeqRef.current;
|
|
94
95
|
const overrides = buildOverrides(state.model);
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
studioApiFetch(
|
|
98
|
+
buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`),
|
|
99
|
+
{
|
|
100
|
+
method: "PUT",
|
|
101
|
+
headers: { "Content-Type": "text/plain", ...studioWriteHeaders() },
|
|
102
|
+
body: JSON.stringify(overrides, null, 2),
|
|
103
|
+
},
|
|
104
|
+
)
|
|
101
105
|
.then((res) => {
|
|
102
106
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
103
107
|
// A newer edit may have re-armed the debounce while this PUT was in
|
|
@@ -171,7 +175,7 @@ export function useCaptionSync(projectId: string | null) {
|
|
|
171
175
|
|
|
172
176
|
let data: { content?: string };
|
|
173
177
|
try {
|
|
174
|
-
const res = await
|
|
178
|
+
const res = await studioApiFetch(
|
|
175
179
|
buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`),
|
|
176
180
|
);
|
|
177
181
|
if (!res.ok) return; // no overrides file yet — normal
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
2
3
|
|
|
3
4
|
interface ServedProject {
|
|
4
5
|
id: string;
|
|
@@ -24,8 +25,7 @@ interface ProjectUnreachableBannerProps {
|
|
|
24
25
|
*
|
|
25
26
|
* The wording is deliberately narrower than "this project is gone". We can see
|
|
26
27
|
* which project this server serves; we cannot see why it does not serve this
|
|
27
|
-
* one, and nothing here is lost.
|
|
28
|
-
* `docs/hyperframes/plans/sdk/2026-09-20-stale-project-id-remaining-doors-plan.md` §C.
|
|
28
|
+
* one, and nothing here is lost.
|
|
29
29
|
*/
|
|
30
30
|
export function ProjectUnreachableBanner({ projectId }: ProjectUnreachableBannerProps) {
|
|
31
31
|
const [served, setServed] = useState<ServedProject[] | null>(null);
|
|
@@ -35,7 +35,7 @@ export function ProjectUnreachableBanner({ projectId }: ProjectUnreachableBanner
|
|
|
35
35
|
// A list we could not read is not evidence of anything, so a failed fetch
|
|
36
36
|
// leaves `served` empty and the banner falls back to the vague wording
|
|
37
37
|
// rather than claiming this Studio serves nothing.
|
|
38
|
-
|
|
38
|
+
studioApiFetch("/api/projects")
|
|
39
39
|
.then((res) => (res.ok ? res.json() : { projects: [] }))
|
|
40
40
|
.then((data: { projects?: ServedProject[] }) => {
|
|
41
41
|
if (!cancelled) setServed(data.projects ?? []);
|
|
@@ -94,6 +94,7 @@ export interface DomEditOverlayProps {
|
|
|
94
94
|
next: { width: number; height: number },
|
|
95
95
|
offset?: { x: number; y: number },
|
|
96
96
|
restore?: () => void,
|
|
97
|
+
route?: { plainTranslate: boolean },
|
|
97
98
|
) => Promise<unknown> | void;
|
|
98
99
|
onRotationCommit: (selection: DomEditSelection, next: RotationCommit) => Promise<unknown> | void;
|
|
99
100
|
onStyleCommit?: (property: string, value: string) => Promise<unknown> | void;
|
|
@@ -275,6 +275,7 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
275
275
|
n: { width: number; height: number },
|
|
276
276
|
offset?: { x: number; y: number },
|
|
277
277
|
restore?: () => void,
|
|
278
|
+
route?: { plainTranslate: boolean },
|
|
278
279
|
) => Promise<unknown> | void
|
|
279
280
|
>;
|
|
280
281
|
onRotationCommitRef: RefObject<
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type UseDomEditOverlayGesturesOptions,
|
|
33
33
|
} from "./domEditOverlayGestures";
|
|
34
34
|
import { collectSnapContext, buildExcludeElements } from "./snapTargetCollection";
|
|
35
|
-
import {
|
|
35
|
+
import { editsPlainCss } from "../../hooks/gsapRuntimeKeyframes";
|
|
36
36
|
import { logResize, resetResizeMoveLog } from "../../utils/resizeDebug";
|
|
37
37
|
import { logDrag, readDragPositions, resetDragMoveLog } from "../../utils/dragDebug";
|
|
38
38
|
|
|
@@ -234,7 +234,7 @@ export function startGesture(
|
|
|
234
234
|
try {
|
|
235
235
|
// Rotation base: the angle the element shows. An element GSAP does not turn, or a plain-translate
|
|
236
236
|
// move, never asks GSAP: reading a property makes it bake the CSS into its transform.
|
|
237
|
-
const plain = !!pathOffsetMember?.plainTranslate ||
|
|
237
|
+
const plain = !!pathOffsetMember?.plainTranslate || editsPlainCss(sel.element, "rotate");
|
|
238
238
|
const plainRotation = plain && kind === "rotate" ? readCssRotationTarget(sel.element) : null;
|
|
239
239
|
const rotation = { angle: readRotationBase(sel.element, plain) };
|
|
240
240
|
const overlayBounds = overlayEl?.getBoundingClientRect();
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { DomEditSelection } from "./domEditingTypes";
|
|
5
|
+
import type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
|
|
6
|
+
import { STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP } from "./manualEdits";
|
|
7
|
+
import { isStudioManualEditGestureLiveIn } from "./manualEditsDom";
|
|
8
|
+
import { readTranslatePx } from "./plainTranslate";
|
|
9
|
+
import { moveDomGroupBy } from "./domGroupMoveBy";
|
|
10
|
+
import { GsapEditBlockedError } from "../../hooks/gsapEditOutcome";
|
|
11
|
+
|
|
12
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
13
|
+
|
|
14
|
+
const mocks = vi.hoisted(() => ({ drag: vi.fn() }));
|
|
15
|
+
vi.mock("../../hooks/gsapRuntimeBridge", () => ({
|
|
16
|
+
tryGsapDragIntercept: mocks.drag,
|
|
17
|
+
tryGsapRotationIntercept: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("../../hooks/useAnimatedPropertyCommit", () => ({
|
|
20
|
+
useAnimatedPropertyCommit: () => ({
|
|
21
|
+
commitAnimatedProperty: vi.fn(),
|
|
22
|
+
commitAnimatedProperties: vi.fn(),
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
vi.mock("../../hooks/useSafeGsapCommitMutation", () => ({
|
|
26
|
+
useGsapSaveFailureTelemetry: () => vi.fn(),
|
|
27
|
+
useSafeGsapCommitMutation: (commit: unknown) => commit,
|
|
28
|
+
}));
|
|
29
|
+
vi.mock("../../utils/studioSaveDiagnostics", async (importOriginal) => ({
|
|
30
|
+
...(await importOriginal<typeof import("../../utils/studioSaveDiagnostics")>()),
|
|
31
|
+
trackStudioEditBlocked: vi.fn(),
|
|
32
|
+
trackStudioSaveFailure: vi.fn(),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
import { mountGsapAwareEditing } from "../../hooks/useGsapAwareEditing.testHelpers";
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
document.body.innerHTML = "";
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** A preview iframe drawn at half size: 200x100 composition px in a 100x50 box. */
|
|
43
|
+
function preview(): Document {
|
|
44
|
+
const iframe = document.createElement("iframe");
|
|
45
|
+
document.body.append(iframe);
|
|
46
|
+
const win = iframe.contentWindow!;
|
|
47
|
+
Object.defineProperty(win, "frameElement", { configurable: true, value: iframe });
|
|
48
|
+
Object.defineProperty(win, "innerWidth", { configurable: true, value: 200 });
|
|
49
|
+
Object.defineProperty(win, "innerHeight", { configurable: true, value: 100 });
|
|
50
|
+
iframe.getBoundingClientRect = () => new DOMRect(50, 40, 100, 50);
|
|
51
|
+
return iframe.contentDocument!;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const turned = (deg: number, scale: number) => {
|
|
55
|
+
const r = (deg * Math.PI) / 180;
|
|
56
|
+
return [scale * Math.cos(r), scale * Math.sin(r), -scale * Math.sin(r), scale * Math.cos(r)];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** An element whose box centre is `at` plus its own move carried through its ancestors' linear map. */
|
|
60
|
+
function layer(doc: Document, id: string, parent = [1, 0, 0, 1], gsap?: object) {
|
|
61
|
+
const element = Object.assign(doc.createElement("div"), gsap ? { _gsap: gsap } : {});
|
|
62
|
+
doc.body.append(element);
|
|
63
|
+
const own = () => {
|
|
64
|
+
if (!gsap) return readTranslatePx(element);
|
|
65
|
+
const read = (prop: string) => Number.parseFloat(element.style.getPropertyValue(prop)) || 0;
|
|
66
|
+
return { x: read(STUDIO_OFFSET_X_PROP), y: read(STUDIO_OFFSET_Y_PROP) };
|
|
67
|
+
};
|
|
68
|
+
element.getBoundingClientRect = () => {
|
|
69
|
+
const { x, y } = own();
|
|
70
|
+
const [a, b, c, d] = parent as [number, number, number, number];
|
|
71
|
+
return new DOMRect(60 + a * x + c * y - 20, 40 + b * x + d * y - 10, 40, 20);
|
|
72
|
+
};
|
|
73
|
+
const selection = {
|
|
74
|
+
element,
|
|
75
|
+
id,
|
|
76
|
+
selector: `#${id}`,
|
|
77
|
+
label: id,
|
|
78
|
+
capabilities: { canApplyManualOffset: true },
|
|
79
|
+
} as unknown as DomEditSelection;
|
|
80
|
+
const centre = () => {
|
|
81
|
+
const r = element.getBoundingClientRect();
|
|
82
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
83
|
+
};
|
|
84
|
+
return { element, selection, centre };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mountGroupCommit() {
|
|
88
|
+
const showToast = vi.fn();
|
|
89
|
+
const save = vi.fn().mockResolvedValue(undefined);
|
|
90
|
+
const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
|
|
91
|
+
const { editing, root } = mountGsapAwareEditing({ showToast, stageElementPositionOffset });
|
|
92
|
+
const commit = (u: DomEditGroupPathOffsetCommit[], o?: { refusalToast?: boolean }) =>
|
|
93
|
+
editing().handleGsapAwareGroupPathOffsetCommit(u, o);
|
|
94
|
+
const moveBy = (moves: Parameters<typeof moveDomGroupBy>[0]) =>
|
|
95
|
+
act(() => moveDomGroupBy(moves, (u) => commit(u, { refusalToast: false })));
|
|
96
|
+
return {
|
|
97
|
+
commit: (u: DomEditGroupPathOffsetCommit[]) => commit(u),
|
|
98
|
+
moveBy,
|
|
99
|
+
showToast,
|
|
100
|
+
stageElementPositionOffset,
|
|
101
|
+
root,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const moved = (from: { x: number; y: number }, to: { x: number; y: number }) => ({
|
|
106
|
+
x: to.x - from.x,
|
|
107
|
+
y: to.y - from.y,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("handleDomGroupMoveBy", () => {
|
|
111
|
+
it("moves each box by its delta in composition px, saved under one undo entry", async () => {
|
|
112
|
+
const doc = preview();
|
|
113
|
+
const offset = layer(doc, "offset");
|
|
114
|
+
offset.element.style.setProperty("translate", "30px 0px");
|
|
115
|
+
const halved = layer(doc, "halved", [0.5, 0, 0, 0.5]);
|
|
116
|
+
const before = [offset.centre(), halved.centre()];
|
|
117
|
+
const { moveBy, stageElementPositionOffset, showToast, root } = mountGroupCommit();
|
|
118
|
+
|
|
119
|
+
await moveBy([
|
|
120
|
+
{ selection: offset.selection, delta: { x: 10, y: 0 } },
|
|
121
|
+
{ selection: halved.selection, delta: { x: 0, y: -5 } },
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
expect(moved(before[0]!, offset.centre())).toEqual({ x: 10, y: 0 });
|
|
125
|
+
expect(moved(before[1]!, halved.centre())).toEqual({ x: 0, y: -5 });
|
|
126
|
+
const calls = stageElementPositionOffset.mock.calls as unknown as unknown[][];
|
|
127
|
+
expect(calls.map((call) => call[1])).toEqual([
|
|
128
|
+
{ x: 40, y: 0 },
|
|
129
|
+
{ x: 0, y: -10 },
|
|
130
|
+
]);
|
|
131
|
+
expect(calls[0]![3]).toMatch(/^group-drag:\d+$/);
|
|
132
|
+
expect(calls[1]![3]).toBe(calls[0]![3]);
|
|
133
|
+
expect(showToast).not.toHaveBeenCalled();
|
|
134
|
+
expect(isStudioManualEditGestureLiveIn(doc)).toBe(false);
|
|
135
|
+
act(() => root.unmount());
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("lands exactly under a half-scale parent with a rotated ancestor", async () => {
|
|
139
|
+
const doc = preview();
|
|
140
|
+
const tilted = layer(doc, "tilted", turned(30, 0.5));
|
|
141
|
+
const before = tilted.centre();
|
|
142
|
+
const { moveBy, root } = mountGroupCommit();
|
|
143
|
+
|
|
144
|
+
await moveBy([{ selection: tilted.selection, delta: { x: 10, y: 0 } }]);
|
|
145
|
+
|
|
146
|
+
const delta = moved(before, tilted.centre());
|
|
147
|
+
// Exact to the 0.001 px a translate is written with.
|
|
148
|
+
expect(delta.x).toBeCloseTo(10, 3);
|
|
149
|
+
expect(delta.y).toBeCloseTo(0, 3);
|
|
150
|
+
act(() => root.unmount());
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("moves a member GSAP positions through the GSAP write, planned and written at one delta", async () => {
|
|
154
|
+
mocks.drag.mockResolvedValue({ status: "persisted" });
|
|
155
|
+
const doc = preview();
|
|
156
|
+
const animated = layer(doc, "animated", [1, 0, 0, 1], { renderTransform: () => {} });
|
|
157
|
+
const before = animated.centre();
|
|
158
|
+
const { moveBy, stageElementPositionOffset, root } = mountGroupCommit();
|
|
159
|
+
|
|
160
|
+
await moveBy([{ selection: animated.selection, delta: { x: 10, y: 0 } }]);
|
|
161
|
+
|
|
162
|
+
const write = mocks.drag.mock.calls.find((call) => !call[6]?.preflightOnly);
|
|
163
|
+
expect(write?.[0]).toBe(animated.selection);
|
|
164
|
+
expect(write?.[1]).toEqual({ x: 10, y: 0 });
|
|
165
|
+
const plan = mocks.drag.mock.calls.find((call) => call[6]?.preflightOnly);
|
|
166
|
+
expect(plan?.[1]).toEqual(write?.[1]);
|
|
167
|
+
expect(moved(before, animated.centre())).toEqual({ x: 10, y: 0 });
|
|
168
|
+
expect(stageElementPositionOffset).not.toHaveBeenCalled();
|
|
169
|
+
act(() => root.unmount());
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("skips a zero delta, and writes nothing when every delta is zero", async () => {
|
|
173
|
+
const doc = preview();
|
|
174
|
+
const still = layer(doc, "still");
|
|
175
|
+
const { moveBy, stageElementPositionOffset, root } = mountGroupCommit();
|
|
176
|
+
|
|
177
|
+
await moveBy([{ selection: still.selection, delta: { x: 0, y: 0 } }]);
|
|
178
|
+
|
|
179
|
+
expect(stageElementPositionOffset).not.toHaveBeenCalled();
|
|
180
|
+
act(() => root.unmount());
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it.each([
|
|
184
|
+
[
|
|
185
|
+
"a member that can't take a manual move",
|
|
186
|
+
(doc: Document) => {
|
|
187
|
+
const locked = layer(doc, "locked");
|
|
188
|
+
Object.assign(locked.selection.capabilities, {
|
|
189
|
+
canApplyManualOffset: false,
|
|
190
|
+
reasonIfDisabled: "Locked by a helper loop.",
|
|
191
|
+
});
|
|
192
|
+
return locked;
|
|
193
|
+
},
|
|
194
|
+
/helper loop/,
|
|
195
|
+
],
|
|
196
|
+
[
|
|
197
|
+
"a member an animation took over",
|
|
198
|
+
(doc: Document) => layer(doc, "folded", [1, 0, 0, 1], { x: "94px" }),
|
|
199
|
+
/animation took over/,
|
|
200
|
+
],
|
|
201
|
+
[
|
|
202
|
+
"a member the GSAP write refuses",
|
|
203
|
+
(doc: Document) => {
|
|
204
|
+
mocks.drag.mockResolvedValue({ status: "blocked", reason: "no-selector" });
|
|
205
|
+
return layer(doc, "blocked", [1, 0, 0, 1], { renderTransform: () => {} });
|
|
206
|
+
},
|
|
207
|
+
new GsapEditBlockedError("no-selector").message,
|
|
208
|
+
],
|
|
209
|
+
])("refuses %s before any write, without a Studio toast", async (_, make, reason) => {
|
|
210
|
+
const doc = preview();
|
|
211
|
+
const fine = layer(doc, "fine");
|
|
212
|
+
const refused = make(doc);
|
|
213
|
+
const before = [fine.centre(), refused.centre()];
|
|
214
|
+
const { moveBy, stageElementPositionOffset, showToast, root } = mountGroupCommit();
|
|
215
|
+
|
|
216
|
+
await expect(
|
|
217
|
+
moveBy([
|
|
218
|
+
{ selection: fine.selection, delta: { x: 10, y: 0 } },
|
|
219
|
+
{ selection: refused.selection, delta: { x: 10, y: 0 } },
|
|
220
|
+
]),
|
|
221
|
+
).rejects.toThrow(reason);
|
|
222
|
+
|
|
223
|
+
expect(stageElementPositionOffset).not.toHaveBeenCalled();
|
|
224
|
+
expect(mocks.drag.mock.calls.some((call) => !call[6]?.preflightOnly)).toBe(false);
|
|
225
|
+
expect(showToast).not.toHaveBeenCalled();
|
|
226
|
+
expect([fine.centre(), refused.centre()]).toEqual(before);
|
|
227
|
+
expect(isStudioManualEditGestureLiveIn(doc)).toBe(false);
|
|
228
|
+
act(() => root.unmount());
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it.each([
|
|
232
|
+
["an animation took over", () => layer(preview(), "folded", [1, 0, 0, 1], { x: "94px" }), true],
|
|
233
|
+
[
|
|
234
|
+
"the GSAP write refuses",
|
|
235
|
+
() => {
|
|
236
|
+
mocks.drag.mockResolvedValue({ status: "blocked", reason: "no-selector" });
|
|
237
|
+
return layer(preview(), "blocked", [1, 0, 0, 1], { renderTransform: () => {} });
|
|
238
|
+
},
|
|
239
|
+
false,
|
|
240
|
+
],
|
|
241
|
+
])("still toasts on a canvas group drag when %s", async (_, make, plainTranslate) => {
|
|
242
|
+
const { selection } = make();
|
|
243
|
+
const { commit, showToast, root } = mountGroupCommit();
|
|
244
|
+
|
|
245
|
+
await expect(
|
|
246
|
+
act(() => commit([{ selection, next: { x: 1, y: 2 }, plainTranslate }])),
|
|
247
|
+
).rejects.toThrow();
|
|
248
|
+
|
|
249
|
+
expect(showToast).toHaveBeenCalledWith(expect.any(String), "error");
|
|
250
|
+
act(() => root.unmount());
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { DomEditSelection } from "./domEditing";
|
|
2
|
+
import type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
|
|
3
|
+
import { canCanvasNudgeTargets } from "./domEditNudge";
|
|
4
|
+
import { isStudioManualEditGestureCurrent, restoreStudioPathOffset } from "./manualEdits";
|
|
5
|
+
import { countStudioManualEditSave } from "./manualEditsDom";
|
|
6
|
+
import {
|
|
7
|
+
applyManualOffsetDragCommit,
|
|
8
|
+
createManualOffsetDragMember,
|
|
9
|
+
endManualOffsetDragMembers,
|
|
10
|
+
restoreManualOffsetDragMembers,
|
|
11
|
+
toTopScreenPoint,
|
|
12
|
+
type ManualOffsetDragMember,
|
|
13
|
+
} from "./manualOffsetDrag";
|
|
14
|
+
|
|
15
|
+
export interface DomGroupMove {
|
|
16
|
+
selection: DomEditSelection;
|
|
17
|
+
/** Composition px: the element's box moves by exactly this in the film. */
|
|
18
|
+
delta: { x: number; y: number };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Composition px to the top page's px; the preview frames only scale and shift. */
|
|
22
|
+
function toScreen(doc: Document, delta: { x: number; y: number }) {
|
|
23
|
+
const origin = toTopScreenPoint(doc, { x: 0, y: 0 });
|
|
24
|
+
const moved = toTopScreenPoint(doc, delta);
|
|
25
|
+
if (!origin || !moved) throw new Error("The preview has no measurable size.");
|
|
26
|
+
return { x: moved.x - origin.x, y: moved.y - origin.y };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function memberFor(
|
|
30
|
+
selection: DomEditSelection,
|
|
31
|
+
scale: { x: number; y: number },
|
|
32
|
+
): ManualOffsetDragMember {
|
|
33
|
+
const { element } = selection;
|
|
34
|
+
const box = element.getBoundingClientRect();
|
|
35
|
+
const result = createManualOffsetDragMember({
|
|
36
|
+
key: selection.id ?? selection.selector ?? selection.label,
|
|
37
|
+
selection,
|
|
38
|
+
element,
|
|
39
|
+
rect: {
|
|
40
|
+
left: box.left,
|
|
41
|
+
top: box.top,
|
|
42
|
+
width: box.width,
|
|
43
|
+
height: box.height,
|
|
44
|
+
editScaleX: scale.x,
|
|
45
|
+
editScaleY: scale.y,
|
|
46
|
+
},
|
|
47
|
+
gesture: "drag",
|
|
48
|
+
});
|
|
49
|
+
if (!result.ok) throw new Error(result.reason);
|
|
50
|
+
return result.member;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Moves each element by its delta as one group drag would drop it: one save, one undo entry.
|
|
55
|
+
* Any member that can't move refuses the whole call before anything is drawn or written.
|
|
56
|
+
*/
|
|
57
|
+
export async function moveDomGroupBy(
|
|
58
|
+
moves: DomGroupMove[],
|
|
59
|
+
commitGroup: (updates: DomEditGroupPathOffsetCommit[]) => Promise<unknown>,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const moving = moves.filter(({ delta }) => delta.x !== 0 || delta.y !== 0);
|
|
62
|
+
if (moving.length === 0) return;
|
|
63
|
+
const selections = moving.map(({ selection }) => selection);
|
|
64
|
+
if (!canCanvasNudgeTargets(selections)) {
|
|
65
|
+
const blocked = selections.find((s) => !s.capabilities.canApplyManualOffset);
|
|
66
|
+
throw new Error(blocked?.capabilities.reasonIfDisabled ?? `${blocked?.label} can't be moved.`);
|
|
67
|
+
}
|
|
68
|
+
const screens = moving.map(({ selection, delta }) =>
|
|
69
|
+
toScreen(selection.element.ownerDocument, delta),
|
|
70
|
+
);
|
|
71
|
+
const members: ManualOffsetDragMember[] = [];
|
|
72
|
+
try {
|
|
73
|
+
for (const selection of selections)
|
|
74
|
+
members.push(memberFor(selection, toScreen(selection.element.ownerDocument, { x: 1, y: 1 })));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
restoreManualOffsetDragMembers(members);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
const updates = members.map((member, index) => ({
|
|
80
|
+
selection: member.selection,
|
|
81
|
+
next: applyManualOffsetDragCommit(member, screens[index]!.x, screens[index]!.y),
|
|
82
|
+
plainTranslate: member.plainTranslate,
|
|
83
|
+
}));
|
|
84
|
+
try {
|
|
85
|
+
await countStudioManualEditSave(members[0]!.element, () => commitGroup(updates));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
for (const member of members) {
|
|
88
|
+
if (isStudioManualEditGestureCurrent(member.element, member.gestureToken))
|
|
89
|
+
restoreStudioPathOffset(member.element, member.initialPathOffset);
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
} finally {
|
|
93
|
+
endManualOffsetDragMembers(members);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -5,7 +5,7 @@ import type { GestureState } from "./domEditOverlayGestures";
|
|
|
5
5
|
import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
|
|
6
6
|
import {
|
|
7
7
|
hasStudioPendingEdits,
|
|
8
|
-
|
|
8
|
+
paintBackNewestStudioPendingEdit,
|
|
9
9
|
} from "../../utils/studioPendingEdits";
|
|
10
10
|
|
|
11
11
|
afterEach(() => {
|
|
@@ -59,13 +59,13 @@ it("a drag whose save is still running can be painted back at once, and shown ag
|
|
|
59
59
|
const moved = element.style.getPropertyValue("translate");
|
|
60
60
|
expect(moved).not.toBe("40px 30px");
|
|
61
61
|
|
|
62
|
-
const
|
|
62
|
+
const shown = paintBackNewestStudioPendingEdit();
|
|
63
63
|
expect(element.style.getPropertyValue("translate")).toBe("40px 30px");
|
|
64
|
-
expect(
|
|
65
|
-
showAgain
|
|
64
|
+
expect(paintBackNewestStudioPendingEdit()).toBeNull();
|
|
65
|
+
shown!.showAgain();
|
|
66
66
|
expect(element.style.getPropertyValue("translate")).toBe(moved);
|
|
67
67
|
|
|
68
68
|
saved();
|
|
69
69
|
await vi.waitFor(() => expect(hasStudioPendingEdits()).toBe(false));
|
|
70
|
-
expect(
|
|
70
|
+
expect(paintBackNewestStudioPendingEdit()).toBeNull();
|
|
71
71
|
});
|
|
@@ -289,6 +289,39 @@ describe("a drag moves only with its own pointer pressed", () => {
|
|
|
289
289
|
);
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
+
describe("an outside change during a drag, on a preview that can swap scenes in place", () => {
|
|
293
|
+
it("leaves the dragged node in place and shows the change after the drop, loaded fresh", async () => {
|
|
294
|
+
const { live, overlay, box } = mountEditor(false);
|
|
295
|
+
const swapScenes = vi.fn((html: string) => {
|
|
296
|
+
live.contentDocument!.body.innerHTML = html.replace(/^[\s\S]*<body>|<\/body>[\s\S]*$/g, "");
|
|
297
|
+
});
|
|
298
|
+
Object.assign(live.contentWindow!, { __hfSwapScenes: swapScenes });
|
|
299
|
+
vi.stubGlobal(
|
|
300
|
+
"fetch",
|
|
301
|
+
async () => new Response(served("").contentDocument!.documentElement.outerHTML),
|
|
302
|
+
);
|
|
303
|
+
const dragged = byId(live, "title");
|
|
304
|
+
pointer(box, "pointerdown", 150, 150);
|
|
305
|
+
pointer(overlay, "pointermove", 190, 170);
|
|
306
|
+
|
|
307
|
+
act(() => api().refreshPlayer());
|
|
308
|
+
await settle();
|
|
309
|
+
expect(swapScenes, "swapped under the pointer").not.toHaveBeenCalled();
|
|
310
|
+
expect(byId(live, "title")).toBe(dragged);
|
|
311
|
+
const beforeDrop = await paintShadow(api, served("?_t=1"));
|
|
312
|
+
expect(api().iframeRef.current, "promoted under the pointer").toBe(live);
|
|
313
|
+
|
|
314
|
+
pointer(overlay, "pointerup", 190, 170);
|
|
315
|
+
await settle();
|
|
316
|
+
expect(file.title).toBe("translate: 40px 20px");
|
|
317
|
+
const fresh = served("?_t=2");
|
|
318
|
+
expect(await paintShadow(api, fresh)).toBeGreaterThan(beforeDrop);
|
|
319
|
+
expect(api().iframeRef.current).toBe(fresh);
|
|
320
|
+
expect(byId(fresh, "title").style.getPropertyValue("translate")).toBe("40px 20px");
|
|
321
|
+
vi.unstubAllGlobals();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
292
325
|
type Editor = ReturnType<typeof mountEditor>;
|
|
293
326
|
|
|
294
327
|
describe("every way a drag ends without a drop clears its mark and promotes the held reload", () => {
|
|
@@ -78,4 +78,12 @@ export const STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
78
78
|
true,
|
|
79
79
|
);
|
|
80
80
|
|
|
81
|
+
// A host whose mounted API authenticates with same-origin cookies sets this; studioApiFetch then sends them, and its
|
|
82
|
+
// requests share the preview media's sockets again.
|
|
83
|
+
export const STUDIO_API_SAME_ORIGIN_CREDENTIALS = resolveStudioBooleanEnvFlag(
|
|
84
|
+
env,
|
|
85
|
+
["VITE_STUDIO_API_SAME_ORIGIN_CREDENTIALS"],
|
|
86
|
+
false,
|
|
87
|
+
);
|
|
88
|
+
|
|
81
89
|
import { resolveEnabledSdkFamilies } from "../../utils/sdkCutoverPolicy";
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type StudioPathOffsetSnapshot,
|
|
12
12
|
} from "./manualEdits";
|
|
13
13
|
import { computeDraggedGsapPosition } from "../../hooks/draggedGsapPosition";
|
|
14
|
-
import {
|
|
14
|
+
import { editsPlainCss } from "../../hooks/gsapRuntimeKeyframes";
|
|
15
15
|
import { readTranslatePx, UNREADABLE_TRANSLATE, writeTranslatePx } from "./plainTranslate";
|
|
16
16
|
import type { StudioEditRevert } from "../../utils/studioPendingEdits";
|
|
17
17
|
|
|
@@ -146,12 +146,12 @@ function getRectCenter(element: HTMLElement): Point | null {
|
|
|
146
146
|
return null;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
};
|
|
149
|
+
const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
150
|
+
return toTopScreenPoint(element.ownerDocument, point);
|
|
151
|
+
}
|
|
153
152
|
|
|
154
|
-
|
|
153
|
+
export function toTopScreenPoint(doc: Document, point: Point): Point | null {
|
|
154
|
+
let win: Window | null = doc.defaultView;
|
|
155
155
|
while (win) {
|
|
156
156
|
const frameElement = getFrameElement(win);
|
|
157
157
|
if (!frameElement) break;
|
|
@@ -166,7 +166,6 @@ function getRectCenter(element: HTMLElement): Point | null {
|
|
|
166
166
|
};
|
|
167
167
|
win = frameElement.ownerDocument.defaultView;
|
|
168
168
|
}
|
|
169
|
-
|
|
170
169
|
return point;
|
|
171
170
|
}
|
|
172
171
|
|
|
@@ -319,8 +318,8 @@ export function createManualOffsetDragMember(input: {
|
|
|
319
318
|
rect: ManualOffsetDragRect;
|
|
320
319
|
gesture?: "drag" | "nudge" | "resize"; // resize: the anchor that keeps its centre planted
|
|
321
320
|
}): ManualOffsetDragMemberResult {
|
|
322
|
-
const
|
|
323
|
-
|
|
321
|
+
const plainTranslate =
|
|
322
|
+
!!input.gesture && editsPlainCss(input.element, input.gesture === "resize" ? "resize" : "move");
|
|
324
323
|
// The APPLIED offset, never a dormant var, so a stale one can't fling the element off-screen.
|
|
325
324
|
const initialOffset = plainTranslate
|
|
326
325
|
? readTranslatePx(input.element)
|