@hyperframes/studio 0.8.88 → 0.8.89
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-w3Bbv0kd.js → hyperframes-player-CVoS7kv8.js} +1 -1
- package/dist/assets/{index-DE6cZYR5.js → index-BM8PwYby.js} +111 -111
- package/dist/assets/{index-DldknZPp.js → index-D35-L_yn.js} +1 -1
- package/dist/assets/{index-B6PGJTzp.js → index-DBub9qQY.js} +1 -1
- package/dist/index.d.ts +39 -5
- package/dist/index.html +1 -1
- package/dist/index.js +105 -2
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/components/editor/DomEditOverlay.tsx +7 -4
- package/src/components/editor/domEditOverlayGestures.ts +4 -4
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +226 -0
- package/src/hooks/useDomGeometryCommit.ts +147 -0
- package/src/hooks/useGsapAwareEditing.ts +9 -4
- package/src/index.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.89",
|
|
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/parsers": "0.8.
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/parsers": "0.8.89",
|
|
64
|
+
"@hyperframes/sdk": "0.8.89",
|
|
65
|
+
"@hyperframes/shader-transitions": "0.8.89",
|
|
66
|
+
"@hyperframes/core": "0.8.89",
|
|
67
|
+
"@hyperframes/player": "0.8.89",
|
|
68
|
+
"@hyperframes/studio-server": "0.8.89"
|
|
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.89"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -81,15 +81,18 @@ export interface DomEditOverlayProps {
|
|
|
81
81
|
selection: DomEditSelection,
|
|
82
82
|
next: { x: number; y: number },
|
|
83
83
|
modifiers?: { altKey?: boolean },
|
|
84
|
-
) => Promise<
|
|
85
|
-
onGroupPathOffsetCommit: (updates: DomEditGroupPathOffsetCommit[]) => Promise<
|
|
84
|
+
) => Promise<unknown> | void;
|
|
85
|
+
onGroupPathOffsetCommit: (updates: DomEditGroupPathOffsetCommit[]) => Promise<unknown> | void;
|
|
86
86
|
onBoxSizeCommit: (
|
|
87
87
|
selection: DomEditSelection,
|
|
88
88
|
next: { width: number; height: number },
|
|
89
89
|
offset?: { x: number; y: number },
|
|
90
90
|
restore?: () => void,
|
|
91
|
-
) => Promise<
|
|
92
|
-
onRotationCommit: (
|
|
91
|
+
) => Promise<unknown> | void;
|
|
92
|
+
onRotationCommit: (
|
|
93
|
+
selection: DomEditSelection,
|
|
94
|
+
next: { angle: number },
|
|
95
|
+
) => Promise<unknown> | void;
|
|
93
96
|
onStyleCommit?: (property: string, value: string) => Promise<unknown> | void;
|
|
94
97
|
recordingState?: GestureRecordingState;
|
|
95
98
|
onToggleRecording?: () => void;
|
|
@@ -247,10 +247,10 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
247
247
|
s: DomEditSelection,
|
|
248
248
|
n: { x: number; y: number },
|
|
249
249
|
m?: { altKey?: boolean },
|
|
250
|
-
) => Promise<
|
|
250
|
+
) => Promise<unknown> | void
|
|
251
251
|
>;
|
|
252
252
|
onGroupPathOffsetCommitRef: RefObject<
|
|
253
|
-
(updates: DomEditGroupPathOffsetCommit[]) => Promise<
|
|
253
|
+
(updates: DomEditGroupPathOffsetCommit[]) => Promise<unknown> | void
|
|
254
254
|
>;
|
|
255
255
|
onBoxSizeCommitRef: RefObject<
|
|
256
256
|
(
|
|
@@ -258,10 +258,10 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
258
258
|
n: { width: number; height: number },
|
|
259
259
|
offset?: { x: number; y: number },
|
|
260
260
|
restore?: () => void,
|
|
261
|
-
) => Promise<
|
|
261
|
+
) => Promise<unknown> | void
|
|
262
262
|
>;
|
|
263
263
|
onRotationCommitRef: RefObject<
|
|
264
|
-
(s: DomEditSelection, n: { angle: number }) => Promise<
|
|
264
|
+
(s: DomEditSelection, n: { angle: number }) => Promise<unknown> | void
|
|
265
265
|
>;
|
|
266
266
|
onCanvasPointerMoveRef: RefObject<
|
|
267
267
|
(
|
|
@@ -60,10 +60,10 @@ export interface UseDomEditNudgeParams {
|
|
|
60
60
|
s: DomEditSelection,
|
|
61
61
|
n: { x: number; y: number },
|
|
62
62
|
m?: { altKey?: boolean },
|
|
63
|
-
) => Promise<
|
|
63
|
+
) => Promise<unknown> | void
|
|
64
64
|
>;
|
|
65
65
|
onGroupPathOffsetCommitRef: RefObject<
|
|
66
|
-
(updates: DomEditGroupPathOffsetCommit[]) => Promise<
|
|
66
|
+
(updates: DomEditGroupPathOffsetCommit[]) => Promise<unknown> | void
|
|
67
67
|
>;
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act, createElement, useRef } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
useDomGeometryCommit,
|
|
7
|
+
usePlayerStore,
|
|
8
|
+
type DomEditOverlayProps,
|
|
9
|
+
type UseDomGeometryCommitOptions,
|
|
10
|
+
} from "../index";
|
|
11
|
+
import { makeSelection } from "./domSelectionTestHarness";
|
|
12
|
+
|
|
13
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
14
|
+
|
|
15
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
16
|
+
|
|
17
|
+
const SOURCE = '<div id="card">Card</div><div id="other">Other</div>';
|
|
18
|
+
|
|
19
|
+
/** A warm parse where only another element animates, and a GSAP writer answering `status`. */
|
|
20
|
+
function stubServer(status = 200, parseStatus = 200) {
|
|
21
|
+
const mutations: unknown[] = [];
|
|
22
|
+
vi.stubGlobal(
|
|
23
|
+
"fetch",
|
|
24
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
25
|
+
const url = String(input instanceof Request ? input.url : input);
|
|
26
|
+
const json = (body: unknown, code = 200) =>
|
|
27
|
+
new Response(JSON.stringify(body), {
|
|
28
|
+
status: code,
|
|
29
|
+
headers: { "content-type": "application/json" },
|
|
30
|
+
});
|
|
31
|
+
if (url.includes("/api/projects/p1/gsap-animations/")) {
|
|
32
|
+
if (parseStatus !== 200) return json({ error: "down" }, parseStatus);
|
|
33
|
+
return json({
|
|
34
|
+
animations: [
|
|
35
|
+
{
|
|
36
|
+
id: "other-fade",
|
|
37
|
+
targetSelector: "#other",
|
|
38
|
+
method: "to",
|
|
39
|
+
position: 0,
|
|
40
|
+
duration: 1,
|
|
41
|
+
properties: { opacity: 1 },
|
|
42
|
+
propertyGroup: "opacity",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (url.includes("/api/projects/p1/gsap-mutations/")) {
|
|
48
|
+
mutations.push(JSON.parse(String(init?.body)));
|
|
49
|
+
if (status !== 200) return json({ error: "refused" }, status);
|
|
50
|
+
return json({ ok: true, changed: true, before: "BEFORE", after: "AFTER" });
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
return mutations;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderHost(options: Partial<UseDomGeometryCommitOptions> = {}) {
|
|
59
|
+
const iframe = document.createElement("iframe");
|
|
60
|
+
document.body.append(iframe);
|
|
61
|
+
const doc = iframe.contentDocument;
|
|
62
|
+
if (!doc) throw new Error("Expected iframe document");
|
|
63
|
+
doc.body.innerHTML = SOURCE;
|
|
64
|
+
const element = doc.getElementById("card") as HTMLElement;
|
|
65
|
+
const recordEdit = vi.fn(async () => {});
|
|
66
|
+
const writeProjectFile = vi.fn(async () => {});
|
|
67
|
+
const reloadPreview = vi.fn();
|
|
68
|
+
const api: { current: ReturnType<typeof useDomGeometryCommit> | null } = { current: null };
|
|
69
|
+
function Host() {
|
|
70
|
+
const iframeRef = useRef<HTMLIFrameElement | null>(iframe);
|
|
71
|
+
api.current = useDomGeometryCommit({
|
|
72
|
+
projectId: "p1",
|
|
73
|
+
iframeRef,
|
|
74
|
+
writeProjectFile,
|
|
75
|
+
recordEdit,
|
|
76
|
+
reloadPreview,
|
|
77
|
+
...options,
|
|
78
|
+
});
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const root = createRoot(document.createElement("div"));
|
|
82
|
+
act(() => root.render(createElement(Host)));
|
|
83
|
+
const hook = () => {
|
|
84
|
+
if (!api.current) throw new Error("Expected the hook to render");
|
|
85
|
+
return api.current;
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
element,
|
|
89
|
+
recordEdit,
|
|
90
|
+
hook,
|
|
91
|
+
rerender: () => act(() => root.render(createElement(Host))),
|
|
92
|
+
unmount: () => act(() => root.unmount()),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
usePlayerStore.setState({ previewBooted: true, timelineProjectId: "p1" });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
vi.unstubAllGlobals();
|
|
102
|
+
document.body.innerHTML = "";
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("useDomGeometryCommit, from the package entry", () => {
|
|
106
|
+
it("plugs into DomEditOverlay and saves a move as one GSAP write and one undo step", async () => {
|
|
107
|
+
const mutations = stubServer();
|
|
108
|
+
const { element, recordEdit, hook, unmount } = renderHost();
|
|
109
|
+
const overlayCommits: Pick<
|
|
110
|
+
DomEditOverlayProps,
|
|
111
|
+
"onPathOffsetCommit" | "onGroupPathOffsetCommit" | "onBoxSizeCommit" | "onRotationCommit"
|
|
112
|
+
> = {
|
|
113
|
+
onPathOffsetCommit: hook().commitPathOffset,
|
|
114
|
+
onGroupPathOffsetCommit: hook().commitGroupPathOffset,
|
|
115
|
+
onBoxSizeCommit: hook().commitBoxSize,
|
|
116
|
+
onRotationCommit: hook().commitRotation,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const outcome = await overlayCommits.onPathOffsetCommit(makeSelection("card", element), {
|
|
120
|
+
x: 40,
|
|
121
|
+
y: 20,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(outcome).toEqual({ ok: true });
|
|
125
|
+
expect(mutations).toEqual([
|
|
126
|
+
expect.objectContaining({ type: "add", targetSelector: "#card", method: "set" }),
|
|
127
|
+
]);
|
|
128
|
+
expect(recordEdit).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(recordEdit).toHaveBeenCalledWith(
|
|
130
|
+
expect.objectContaining({ files: { "index.html": { before: "BEFORE", after: "AFTER" } } }),
|
|
131
|
+
);
|
|
132
|
+
unmount();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("keeps the same commits across renders, so the overlay's handlers stay put", () => {
|
|
136
|
+
stubServer();
|
|
137
|
+
const { hook, rerender, unmount } = renderHost();
|
|
138
|
+
const first = hook();
|
|
139
|
+
rerender();
|
|
140
|
+
expect(hook().commitPathOffset).toBe(first.commitPathOffset);
|
|
141
|
+
expect(hook().commitRotation).toBe(first.commitRotation);
|
|
142
|
+
unmount();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("rejects a move the server refuses, so the overlay undoes it", async () => {
|
|
146
|
+
stubServer(500);
|
|
147
|
+
const showToast = vi.fn();
|
|
148
|
+
const { element, recordEdit, hook, unmount } = renderHost({ showToast });
|
|
149
|
+
|
|
150
|
+
await expect(
|
|
151
|
+
hook().commitPathOffset(makeSelection("card", element), { x: 40, y: 20 }),
|
|
152
|
+
).rejects.toThrow();
|
|
153
|
+
expect(recordEdit).not.toHaveBeenCalled();
|
|
154
|
+
expect(showToast).toHaveBeenCalledWith(expect.any(String), "error");
|
|
155
|
+
unmount();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("undoes a resize whose animations cannot be read, and saves nothing", async () => {
|
|
159
|
+
const mutations = stubServer(200, 500);
|
|
160
|
+
const showToast = vi.fn();
|
|
161
|
+
const restore = vi.fn();
|
|
162
|
+
const { element, recordEdit, hook, unmount } = renderHost({ showToast });
|
|
163
|
+
|
|
164
|
+
await expect(
|
|
165
|
+
hook().commitBoxSize(
|
|
166
|
+
makeSelection("card", element),
|
|
167
|
+
{ width: 300, height: 90 },
|
|
168
|
+
undefined,
|
|
169
|
+
restore,
|
|
170
|
+
),
|
|
171
|
+
).rejects.toThrow();
|
|
172
|
+
expect(restore).toHaveBeenCalledTimes(1);
|
|
173
|
+
expect(showToast).toHaveBeenCalledWith(expect.any(String), "error");
|
|
174
|
+
expect(mutations).toHaveLength(0);
|
|
175
|
+
expect(recordEdit).not.toHaveBeenCalled();
|
|
176
|
+
unmount();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it.each([
|
|
180
|
+
["the player shows another project", { timelineProjectId: "another-project" }],
|
|
181
|
+
["the preview has not booted", { previewBooted: false }],
|
|
182
|
+
])("refuses at once when %s", async (_state, store) => {
|
|
183
|
+
const mutations = stubServer();
|
|
184
|
+
usePlayerStore.setState(store);
|
|
185
|
+
const restore = vi.fn();
|
|
186
|
+
const showToast = vi.fn();
|
|
187
|
+
const { element, hook, unmount } = renderHost({ showToast });
|
|
188
|
+
|
|
189
|
+
await expect(
|
|
190
|
+
hook().commitBoxSize(
|
|
191
|
+
makeSelection("card", element),
|
|
192
|
+
{ width: 300, height: 90 },
|
|
193
|
+
undefined,
|
|
194
|
+
restore,
|
|
195
|
+
),
|
|
196
|
+
).rejects.toThrow("has not loaded yet");
|
|
197
|
+
expect(restore).toHaveBeenCalledTimes(1);
|
|
198
|
+
expect(showToast).toHaveBeenCalledWith(expect.stringContaining("not loaded"), "error");
|
|
199
|
+
expect(mutations).toHaveLength(0);
|
|
200
|
+
unmount();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("waits for a move still being saved", async () => {
|
|
204
|
+
const mutations = stubServer();
|
|
205
|
+
const { element, recordEdit, hook, unmount } = renderHost();
|
|
206
|
+
|
|
207
|
+
const move = hook().commitPathOffset(makeSelection("card", element), { x: 40, y: 20 });
|
|
208
|
+
await hook().waitForPendingSaves();
|
|
209
|
+
|
|
210
|
+
expect(mutations).toHaveLength(1);
|
|
211
|
+
expect(recordEdit).toHaveBeenCalledTimes(1);
|
|
212
|
+
await move;
|
|
213
|
+
unmount();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("rejects without a project and writes nothing", async () => {
|
|
217
|
+
const mutations = stubServer();
|
|
218
|
+
const { element, hook, unmount } = renderHost({ projectId: null });
|
|
219
|
+
|
|
220
|
+
await expect(
|
|
221
|
+
hook().commitRotation(makeSelection("card", element), { angle: 15 }),
|
|
222
|
+
).rejects.toThrow("No project is open");
|
|
223
|
+
expect(mutations).toHaveLength(0);
|
|
224
|
+
unmount();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
2
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
4
|
+
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
5
|
+
import { isPreviewBooted } from "../player/store/playerStore";
|
|
6
|
+
import type { DomEditCommitOutcome } from "./domEditCommitRunner";
|
|
7
|
+
import type { UseDomStyleCommitOptions } from "./useDomStyleCommit";
|
|
8
|
+
import { useGsapAnimationFetchFallback } from "./useGsapAnimationFetchFallback";
|
|
9
|
+
import { useGsapAwareEditing } from "./useGsapAwareEditing";
|
|
10
|
+
import { useGsapInteractionFailureTelemetry } from "./useGsapInteractionFailureTelemetry";
|
|
11
|
+
import { useGsapScriptCommits } from "./useGsapScriptCommits";
|
|
12
|
+
import { useGsapCacheVersion } from "./useGsapTweenCache";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Studio's `Player` must show the project, with `beginTimelineSession(projectId)` run before it
|
|
16
|
+
* mounts; drain `waitForPendingSaves` before switching projects.
|
|
17
|
+
*/
|
|
18
|
+
export interface UseDomGeometryCommitOptions extends UseDomStyleCommitOptions {
|
|
19
|
+
/** Called when a save cannot patch the preview in place; defaults to reloading the iframe. */
|
|
20
|
+
reloadPreview?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DomGeometryCommits {
|
|
24
|
+
commitPathOffset: (
|
|
25
|
+
selection: DomEditSelection,
|
|
26
|
+
next: { x: number; y: number },
|
|
27
|
+
modifiers?: { altKey?: boolean },
|
|
28
|
+
) => Promise<DomEditCommitOutcome>;
|
|
29
|
+
commitGroupPathOffset: (updates: DomEditGroupPathOffsetCommit[]) => Promise<DomEditCommitOutcome>;
|
|
30
|
+
commitBoxSize: (
|
|
31
|
+
selection: DomEditSelection,
|
|
32
|
+
next: { width: number; height: number },
|
|
33
|
+
offset?: { x: number; y: number },
|
|
34
|
+
restore?: () => void,
|
|
35
|
+
) => Promise<DomEditCommitOutcome>;
|
|
36
|
+
commitRotation: (
|
|
37
|
+
selection: DomEditSelection,
|
|
38
|
+
next: { angle: number },
|
|
39
|
+
) => Promise<DomEditCommitOutcome>;
|
|
40
|
+
waitForPendingSaves: () => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const noop = () => {};
|
|
44
|
+
const NO_SELECTED_ANIMATIONS: GsapAnimation[] = [];
|
|
45
|
+
// ponytail: unreachable, the GSAP writer always exists so a resize never takes the DOM route.
|
|
46
|
+
const noDomBoxSizeRoute = () => Promise.reject(new Error("Resize has no DOM route here"));
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Saves canvas moves, resizes and rotations through Studio's own GSAP-aware commits, for a host
|
|
50
|
+
* without the editor. A failed save rejects, so DomEditOverlay undoes the live gesture.
|
|
51
|
+
*/
|
|
52
|
+
export function useDomGeometryCommit({
|
|
53
|
+
projectId,
|
|
54
|
+
iframeRef,
|
|
55
|
+
writeProjectFile,
|
|
56
|
+
recordEdit,
|
|
57
|
+
activeCompPath = null,
|
|
58
|
+
showToast = noop,
|
|
59
|
+
reloadPreview,
|
|
60
|
+
}: UseDomGeometryCommitOptions): DomGeometryCommits {
|
|
61
|
+
const projectIdRef = useRef(projectId);
|
|
62
|
+
projectIdRef.current = projectId;
|
|
63
|
+
const pending = useRef(new Set<Promise<void>>()).current;
|
|
64
|
+
const editHistory = useMemo(() => ({ recordEdit }), [recordEdit]);
|
|
65
|
+
const reload = useCallback(
|
|
66
|
+
() => (reloadPreview ? reloadPreview() : iframeRef.current?.contentWindow?.location.reload()),
|
|
67
|
+
[reloadPreview, iframeRef],
|
|
68
|
+
);
|
|
69
|
+
const { bump: bumpGsapCache } = useGsapCacheVersion();
|
|
70
|
+
const gsap = useGsapScriptCommits({
|
|
71
|
+
projectIdRef,
|
|
72
|
+
activeCompPath,
|
|
73
|
+
previewIframeRef: iframeRef,
|
|
74
|
+
editHistory,
|
|
75
|
+
reloadPreview: reload,
|
|
76
|
+
onCacheInvalidate: bumpGsapCache,
|
|
77
|
+
showToast,
|
|
78
|
+
writeProjectFile,
|
|
79
|
+
});
|
|
80
|
+
const makeFetchFallback = useGsapAnimationFetchFallback(projectId);
|
|
81
|
+
const trackGsapInteractionFailure = useGsapInteractionFailureTelemetry(activeCompPath, showToast);
|
|
82
|
+
const {
|
|
83
|
+
handleGsapAwarePathOffsetCommit,
|
|
84
|
+
handleGsapAwareGroupPathOffsetCommit,
|
|
85
|
+
handleGsapAwareBoxSizeCommit,
|
|
86
|
+
handleGsapAwareRotationCommit,
|
|
87
|
+
} = useGsapAwareEditing({
|
|
88
|
+
// The host owns selection, so every gesture reads its element's animations from the server.
|
|
89
|
+
domEditSelection: null,
|
|
90
|
+
selectedGsapAnimations: NO_SELECTED_ANIMATIONS,
|
|
91
|
+
gsapCommitMutation: gsap.commitMutation,
|
|
92
|
+
previewIframeRef: iframeRef,
|
|
93
|
+
showToast,
|
|
94
|
+
bumpGsapCache,
|
|
95
|
+
makeFetchFallback,
|
|
96
|
+
trackGsapInteractionFailure,
|
|
97
|
+
handleDomBoxSizeCommit: noDomBoxSizeRoute,
|
|
98
|
+
addGsapAnimation: gsap.addGsapAnimation,
|
|
99
|
+
convertToKeyframes: gsap.convertToKeyframes,
|
|
100
|
+
setArcPath: gsap.setArcPath,
|
|
101
|
+
updateArcSegment: gsap.updateArcSegment,
|
|
102
|
+
});
|
|
103
|
+
const saved = useCallback(
|
|
104
|
+
async (commit: () => Promise<void>, restore = noop): Promise<DomEditCommitOutcome> => {
|
|
105
|
+
const refusal = !projectIdRef.current
|
|
106
|
+
? "No project is open"
|
|
107
|
+
: !isPreviewBooted(projectIdRef.current)
|
|
108
|
+
? "The preview for this project has not loaded yet"
|
|
109
|
+
: null;
|
|
110
|
+
if (refusal) {
|
|
111
|
+
restore();
|
|
112
|
+
showToast(refusal, "error");
|
|
113
|
+
throw new Error(refusal);
|
|
114
|
+
}
|
|
115
|
+
const run = commit();
|
|
116
|
+
pending.add(run);
|
|
117
|
+
try {
|
|
118
|
+
await run;
|
|
119
|
+
} finally {
|
|
120
|
+
pending.delete(run);
|
|
121
|
+
}
|
|
122
|
+
return { ok: true };
|
|
123
|
+
},
|
|
124
|
+
[pending, showToast],
|
|
125
|
+
);
|
|
126
|
+
return useMemo(
|
|
127
|
+
() => ({
|
|
128
|
+
commitPathOffset: (selection, next, modifiers) =>
|
|
129
|
+
saved(() => handleGsapAwarePathOffsetCommit(selection, next, modifiers)),
|
|
130
|
+
commitGroupPathOffset: (updates) =>
|
|
131
|
+
saved(() => handleGsapAwareGroupPathOffsetCommit(updates)),
|
|
132
|
+
commitBoxSize: (selection, next, offset, restore) =>
|
|
133
|
+
saved(() => handleGsapAwareBoxSizeCommit(selection, next, offset, restore), restore),
|
|
134
|
+
commitRotation: (selection, next) =>
|
|
135
|
+
saved(() => handleGsapAwareRotationCommit(selection, next)),
|
|
136
|
+
waitForPendingSaves: () => Promise.allSettled([...pending]).then(noop),
|
|
137
|
+
}),
|
|
138
|
+
[
|
|
139
|
+
saved,
|
|
140
|
+
pending,
|
|
141
|
+
handleGsapAwarePathOffsetCommit,
|
|
142
|
+
handleGsapAwareGroupPathOffsetCommit,
|
|
143
|
+
handleGsapAwareBoxSizeCommit,
|
|
144
|
+
handleGsapAwareRotationCommit,
|
|
145
|
+
],
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -298,10 +298,15 @@ export function useGsapAwareEditing({
|
|
|
298
298
|
offset?: { x: number; y: number },
|
|
299
299
|
restore: () => void = () => undefined,
|
|
300
300
|
) => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
: await ownedAnimations;
|
|
301
|
+
let targetAnimations: GsapAnimation[];
|
|
302
|
+
try {
|
|
303
|
+
const ownedAnimations = getGsapAnimationsForSelection(selection);
|
|
304
|
+
targetAnimations = Array.isArray(ownedAnimations) ? ownedAnimations : await ownedAnimations;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
restore();
|
|
307
|
+
trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
305
310
|
const scaleRoute = targetAnimations.some((anim) => anim.propertyGroup === "scale");
|
|
306
311
|
const selector = selectorFromSelection(selection);
|
|
307
312
|
const hasLivePositionTween = selector
|
package/src/index.ts
CHANGED
|
@@ -167,7 +167,7 @@ export type { AudioMeterStripProps } from "./components/nle/AudioMeterStrip";
|
|
|
167
167
|
export { useClipboard } from "./hooks/useClipboard";
|
|
168
168
|
export type { UseClipboardOptions } from "./hooks/useClipboard";
|
|
169
169
|
|
|
170
|
-
// DOM editing for a host outside EditorShell;
|
|
170
|
+
// DOM editing for a host outside EditorShell; the Commit hooks save without useDomEditSession.
|
|
171
171
|
export { useDomEditSession } from "./hooks/useDomEditSession";
|
|
172
172
|
export type { UseDomEditSessionParams } from "./hooks/useDomEditSession";
|
|
173
173
|
export { usePreviewPersistence } from "./hooks/usePreviewPersistence";
|
|
@@ -181,6 +181,8 @@ export { useDomStyleCommit } from "./hooks/useDomStyleCommit";
|
|
|
181
181
|
export type { UseDomStyleCommitOptions } from "./hooks/useDomStyleCommit";
|
|
182
182
|
export type { DomEditCommitDeclineReason, DomEditCommitOutcome } from "./hooks/domEditCommitRunner";
|
|
183
183
|
export { resolveDomEditSelection } from "./components/editor/domEditingLayers";
|
|
184
|
+
export { useDomGeometryCommit } from "./hooks/useDomGeometryCommit";
|
|
185
|
+
export type { DomGeometryCommits, UseDomGeometryCommitOptions } from "./hooks/useDomGeometryCommit";
|
|
184
186
|
export { DomEditOverlay } from "./components/editor/DomEditOverlay";
|
|
185
187
|
export type {
|
|
186
188
|
DomEditGroupPathOffsetCommit,
|