@hyperframes/studio 0.8.101 → 0.8.102
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-BuRiFtrF.js} +1 -1
- package/dist/assets/{index-C33EDBL2.js → index-BM35fjFf.js} +1 -1
- package/dist/assets/{index-CJOzbslX.js → index-BT3sauee.js} +1 -1
- package/dist/assets/{index-BBNVnnyH.js → index-C2utDZt-.js} +113 -113
- package/dist/index.d.ts +11 -1
- package/dist/index.html +1 -1
- package/dist/index.js +810 -766
- package/dist/index.js.map +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/canvasContextMenuZOrder.ts +13 -0
- package/src/components/editor/gsapAnimatesProperty.ts +0 -29
- package/src/components/editor/gsapLivePreview.test.ts +46 -1
- package/src/components/editor/propertyPanelHelpers.ts +3 -0
- 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/index.ts +3 -0
|
@@ -9,13 +9,17 @@ import {
|
|
|
9
9
|
PreviewReadOnlyProvider,
|
|
10
10
|
useDomEditSelectionContext,
|
|
11
11
|
useDomEditSession,
|
|
12
|
+
useDomEditZOrder,
|
|
12
13
|
usePreviewPersistence,
|
|
13
14
|
type ConnectedDomEditOverlayProps,
|
|
14
15
|
type DomEditCapabilities,
|
|
15
16
|
type DomEditSelection,
|
|
17
|
+
type DomEditZOrder,
|
|
16
18
|
type UseDomEditSessionParams,
|
|
17
19
|
type UsePreviewPersistenceParams,
|
|
20
|
+
type ZOrderAction,
|
|
18
21
|
} from "@hyperframes/studio";
|
|
22
|
+
import { makeSelection } from "./hooks/domSelectionTestHarness";
|
|
19
23
|
|
|
20
24
|
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
21
25
|
|
|
@@ -66,4 +70,92 @@ describe("DOM editing package exports", () => {
|
|
|
66
70
|
expect(canvas?.className).toContain("pointer-events-none");
|
|
67
71
|
await act(async () => root.unmount());
|
|
68
72
|
});
|
|
73
|
+
|
|
74
|
+
it("gives a host the canvas menu's z-order: enabled, then one step through the session's commit", async () => {
|
|
75
|
+
const parent = document.createElement("div");
|
|
76
|
+
const back = document.createElement("div");
|
|
77
|
+
back.id = "back";
|
|
78
|
+
const front = document.createElement("div");
|
|
79
|
+
front.id = "front";
|
|
80
|
+
parent.append(back, front);
|
|
81
|
+
document.body.append(parent);
|
|
82
|
+
const commitZ = vi.fn(() => Promise.resolve());
|
|
83
|
+
const session = {
|
|
84
|
+
handleDomZIndexReorderCommit: commitZ,
|
|
85
|
+
} as unknown as Parameters<typeof DomEditProvider>[0]["value"];
|
|
86
|
+
let zOrder: DomEditZOrder | undefined;
|
|
87
|
+
function Probe() {
|
|
88
|
+
zOrder = useDomEditZOrder();
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const el = document.createElement("div");
|
|
92
|
+
document.body.append(el);
|
|
93
|
+
const root = createRoot(el);
|
|
94
|
+
await act(async () =>
|
|
95
|
+
root.render(
|
|
96
|
+
<DomEditProvider value={session}>
|
|
97
|
+
<Probe />
|
|
98
|
+
</DomEditProvider>,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
const sel = makeSelection("Back", back);
|
|
102
|
+
const toFront: ZOrderAction = "bring-to-front";
|
|
103
|
+
expect(zOrder?.enabled(sel, "send-to-back")).toBe(false);
|
|
104
|
+
expect(zOrder?.enabled(sel, toFront)).toBe(true);
|
|
105
|
+
expect(zOrder?.apply(sel, "send-to-back")).toBe(false);
|
|
106
|
+
expect(commitZ).not.toHaveBeenCalled();
|
|
107
|
+
await act(async () => {
|
|
108
|
+
expect(zOrder?.apply(sel, toFront)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
expect(commitZ).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(commitZ).toHaveBeenCalledWith(
|
|
112
|
+
expect.arrayContaining([expect.objectContaining({ element: back, id: "back" })]),
|
|
113
|
+
expect.stringContaining(toFront),
|
|
114
|
+
toFront,
|
|
115
|
+
);
|
|
116
|
+
await act(async () => root.unmount());
|
|
117
|
+
parent.remove();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("gives a read-only preview or a detached element no z-order, and never asks the session", async () => {
|
|
121
|
+
const parent = document.createElement("div");
|
|
122
|
+
const back = document.createElement("div");
|
|
123
|
+
back.id = "back";
|
|
124
|
+
parent.append(back, document.createElement("div"));
|
|
125
|
+
document.body.append(parent);
|
|
126
|
+
const commitZ = vi.fn(() => Promise.resolve());
|
|
127
|
+
const session = {
|
|
128
|
+
handleDomZIndexReorderCommit: commitZ,
|
|
129
|
+
} as unknown as Parameters<typeof DomEditProvider>[0]["value"];
|
|
130
|
+
let zOrder: DomEditZOrder | undefined;
|
|
131
|
+
function Probe() {
|
|
132
|
+
zOrder = useDomEditZOrder();
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const el = document.body.appendChild(document.createElement("div"));
|
|
136
|
+
const root = createRoot(el);
|
|
137
|
+
const mount = (readOnly: boolean) =>
|
|
138
|
+
act(async () =>
|
|
139
|
+
root.render(
|
|
140
|
+
<PreviewReadOnlyProvider readOnly={readOnly}>
|
|
141
|
+
<DomEditProvider value={session}>
|
|
142
|
+
<Probe />
|
|
143
|
+
</DomEditProvider>
|
|
144
|
+
</PreviewReadOnlyProvider>,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
const sel = makeSelection("Back", back);
|
|
148
|
+
await mount(true);
|
|
149
|
+
expect(zOrder?.enabled(sel, "bring-to-front")).toBe(false);
|
|
150
|
+
expect(zOrder?.apply(sel, "bring-to-front")).toBe(false);
|
|
151
|
+
expect(zOrder?.commit(sel, [{ element: back, zIndex: 1 }], "bring-to-front", null)).toBe(false);
|
|
152
|
+
await mount(false);
|
|
153
|
+
// A reload leaves the old nodes connected to their old document, one no window shows.
|
|
154
|
+
document.implementation.createHTMLDocument().body.append(parent);
|
|
155
|
+
expect(back.isConnected).toBe(true);
|
|
156
|
+
expect(zOrder?.enabled(sel, "bring-to-front"), "a selection from before a reload").toBe(false);
|
|
157
|
+
expect(zOrder?.apply(sel, "bring-to-front")).toBe(false);
|
|
158
|
+
expect(commitZ).not.toHaveBeenCalled();
|
|
159
|
+
await act(async () => root.unmount());
|
|
160
|
+
});
|
|
69
161
|
});
|
|
@@ -419,6 +419,12 @@ export const POSITION_CHANNELS: string[] = [
|
|
|
419
419
|
];
|
|
420
420
|
const MOVE_CHANNELS = [...POSITION_CHANNELS, "motionPath"];
|
|
421
421
|
|
|
422
|
+
export const GSAP_TRANSFORM_KEYS = new Set(
|
|
423
|
+
"x,y,z,scale,scaleX,scaleY,xPercent,yPercent,rotation,rotationX,rotationY,skewX,skewY,transformOrigin,svgOrigin,force3D,smoothOrigin,transformPerspective,translateX,translateY,translateZ,rotate,rotationZ,rotateZ,rotateX,rotateY".split(
|
|
424
|
+
",",
|
|
425
|
+
),
|
|
426
|
+
);
|
|
427
|
+
|
|
422
428
|
/** Whether a live timeline tween or hold writes any of `channels` on `el`. Sync, no fetch. */
|
|
423
429
|
function gsapWritesChannels(el: Element, channels: string[]): boolean {
|
|
424
430
|
const win = el.ownerDocument.defaultView as { __timelines?: Record<string, RuntimeTimeline> };
|
|
@@ -42,6 +42,7 @@ interface RenderDomEditCommitsOptions {
|
|
|
42
42
|
onTrySdkPersist?: () => Promise<CutoverResult>;
|
|
43
43
|
queueDomEditSave?: <T>(save: () => Promise<T>) => Promise<T>;
|
|
44
44
|
projectIdRef?: MutableRefObject<string | null>;
|
|
45
|
+
readOnlyPreview?: boolean;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
type FetchHandler = (
|
|
@@ -232,7 +233,7 @@ function renderDomEditCommits(
|
|
|
232
233
|
target === selection.element ? selection : null,
|
|
233
234
|
),
|
|
234
235
|
onTrySdkPersist: options.onTrySdkPersist,
|
|
235
|
-
readOnlyPreview: false,
|
|
236
|
+
readOnlyPreview: options.readOnlyPreview ?? false,
|
|
236
237
|
});
|
|
237
238
|
return null;
|
|
238
239
|
}
|
|
@@ -266,97 +267,100 @@ describe("useDomEditCommits z-index reorder persistence", () => {
|
|
|
266
267
|
document.body.replaceChildren();
|
|
267
268
|
});
|
|
268
269
|
|
|
269
|
-
it(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const { iframe, element } = createPreviewElement(
|
|
299
|
-
'<div data-hf-id="hf-card"></div><div id="b"></div><div id="c"></div>',
|
|
300
|
-
);
|
|
301
|
-
element.id = "a";
|
|
302
|
-
const elements = [
|
|
303
|
-
element,
|
|
304
|
-
iframe.contentDocument!.getElementById("b")!,
|
|
305
|
-
iframe.contentDocument!.getElementById("c")!,
|
|
306
|
-
];
|
|
307
|
-
const rendered = renderDomEditCommits(createSelection(element), iframe);
|
|
308
|
-
|
|
309
|
-
try {
|
|
310
|
-
await act(async () => {
|
|
311
|
-
await rendered.hook.handleDomZIndexReorderCommit(
|
|
312
|
-
elements.map((item, index) => ({
|
|
313
|
-
element: item,
|
|
314
|
-
zIndex: 3 - index,
|
|
315
|
-
id: item.id,
|
|
316
|
-
sourceFile: "index.html",
|
|
317
|
-
})),
|
|
318
|
-
"z-reorder:test",
|
|
319
|
-
);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const batchPosts = fetchMock.mock.calls.filter(([input]) =>
|
|
323
|
-
requestUrl(input).endsWith("/file-mutations/patch-element-batches"),
|
|
270
|
+
it.each([false, true])(
|
|
271
|
+
"persists an N-element reorder with one batch POST, one undo entry, and NO iframe reload (read-only preview %s: the timeline's z sync stays editable)",
|
|
272
|
+
async (readOnlyPreview) => {
|
|
273
|
+
const original =
|
|
274
|
+
'<div id="a" style="z-index: 1"></div><div id="b" style="z-index: 2"></div><div id="c" style="z-index: 3"></div>';
|
|
275
|
+
const after =
|
|
276
|
+
'<div id="a" style="z-index: 3"></div><div id="b" style="z-index: 2"></div><div id="c" style="z-index: 1"></div>';
|
|
277
|
+
const fetchMock = vi.fn(
|
|
278
|
+
async (
|
|
279
|
+
input: Parameters<typeof fetch>[0],
|
|
280
|
+
_init?: Parameters<typeof fetch>[1],
|
|
281
|
+
): Promise<Response> => {
|
|
282
|
+
const url = requestUrl(input);
|
|
283
|
+
if (url.endsWith("/file-mutations/patch-element-batches")) {
|
|
284
|
+
return jsonResponse({
|
|
285
|
+
durable: true,
|
|
286
|
+
files: [
|
|
287
|
+
{
|
|
288
|
+
sourceFile: "index.html",
|
|
289
|
+
changed: true,
|
|
290
|
+
matched: [true, true, true],
|
|
291
|
+
before: original,
|
|
292
|
+
after,
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
298
|
+
},
|
|
324
299
|
);
|
|
325
|
-
|
|
326
|
-
|
|
300
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
301
|
+
const { iframe, element } = createPreviewElement(
|
|
302
|
+
'<div data-hf-id="hf-card"></div><div id="b"></div><div id="c"></div>',
|
|
327
303
|
);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
304
|
+
element.id = "a";
|
|
305
|
+
const elements = [
|
|
306
|
+
element,
|
|
307
|
+
iframe.contentDocument!.getElementById("b")!,
|
|
308
|
+
iframe.contentDocument!.getElementById("c")!,
|
|
309
|
+
];
|
|
310
|
+
const rendered = renderDomEditCommits(createSelection(element), iframe, { readOnlyPreview });
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
await act(async () => {
|
|
314
|
+
await rendered.hook.handleDomZIndexReorderCommit(
|
|
315
|
+
elements.map((item, index) => ({
|
|
316
|
+
element: item,
|
|
317
|
+
zIndex: 3 - index,
|
|
318
|
+
id: item.id,
|
|
319
|
+
sourceFile: "index.html",
|
|
320
|
+
})),
|
|
321
|
+
"z-reorder:test",
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const batchPosts = fetchMock.mock.calls.filter(([input]) =>
|
|
326
|
+
requestUrl(input).endsWith("/file-mutations/patch-element-batches"),
|
|
327
|
+
);
|
|
328
|
+
const singlePosts = fetchMock.mock.calls.filter(([input]) =>
|
|
329
|
+
requestUrl(input).includes("/file-mutations/patch-elements-batch/"),
|
|
330
|
+
);
|
|
331
|
+
expect(batchPosts).toHaveLength(1);
|
|
332
|
+
expect(singlePosts).toHaveLength(0);
|
|
333
|
+
expect(JSON.parse(String(batchPosts[0]?.[1]?.body))).toEqual({
|
|
334
|
+
batches: [
|
|
335
|
+
{
|
|
336
|
+
sourceFile: "index.html",
|
|
337
|
+
patches: expect.arrayContaining([
|
|
338
|
+
expect.objectContaining({ target: expect.objectContaining({ id: "a" }) }),
|
|
339
|
+
expect.objectContaining({ target: expect.objectContaining({ id: "b" }) }),
|
|
340
|
+
expect.objectContaining({ target: expect.objectContaining({ id: "c" }) }),
|
|
341
|
+
]),
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
});
|
|
345
|
+
expect(rendered.recordEdit).toHaveBeenCalledTimes(1);
|
|
346
|
+
expect(rendered.recordEdit).toHaveBeenCalledWith({
|
|
347
|
+
label: "Reorder layers",
|
|
348
|
+
coalesceKey: "z-reorder:test",
|
|
349
|
+
// Unbounded per-gesture fold window (keys are unique per gesture):
|
|
350
|
+
// the z entry and its mirror/lane counterpart fold across the server
|
|
351
|
+
// round-trip that separates them.
|
|
352
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
353
|
+
files: { "index.html": { before: original, after } },
|
|
354
|
+
});
|
|
355
|
+
// FIX: a z-only reorder must NOT remount the preview iframe ("the blink").
|
|
356
|
+
// The live DOM + store already hold the final state and the server matched
|
|
357
|
+
// every style-only patch, so the reload is provably redundant.
|
|
358
|
+
expect(rendered.reloadPreview).not.toHaveBeenCalled();
|
|
359
|
+
} finally {
|
|
360
|
+
rendered.cleanup();
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
);
|
|
360
364
|
|
|
361
365
|
it("falls back to reloading when the server response omits matched[]", async () => {
|
|
362
366
|
// Without a matched[] confirmation the persist can't be proven in sync with
|
package/src/index.ts
CHANGED
|
@@ -187,6 +187,9 @@ export { DomEditProvider, useDomEditSelectionContext } from "./contexts/DomEditC
|
|
|
187
187
|
export { PreviewReadOnlyProvider } from "./components/editor/previewReadOnlyContext";
|
|
188
188
|
export { ConnectedDomEditOverlay } from "./components/editor/ConnectedDomEditOverlay";
|
|
189
189
|
export type { ConnectedDomEditOverlayProps } from "./components/editor/ConnectedDomEditOverlay";
|
|
190
|
+
export { useDomEditZOrder } from "./components/editor/useDomEditZOrder";
|
|
191
|
+
export type { DomEditZOrder } from "./components/editor/useDomEditZOrder";
|
|
192
|
+
export type { ZOrderAction } from "./components/editor/canvasContextMenuZOrder";
|
|
190
193
|
export type { DomEditCapabilities, DomEditSelection } from "./components/editor/domEditingTypes";
|
|
191
194
|
export { useDomStyleCommit } from "./hooks/useDomStyleCommit";
|
|
192
195
|
export type { UseDomStyleCommitOptions } from "./hooks/useDomStyleCommit";
|