@hyperframes/studio 0.8.53-desktop.0 → 0.8.53
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-PTEnAHhg.js → hyperframes-player-DKWM-Ons.js} +1 -1
- package/dist/assets/{index-BnXZuIJD.css → index-DrHQAuGO.css} +1 -1
- package/dist/assets/{index-CNyviK_l.js → index-HfP3sap2.js} +200 -200
- package/dist/assets/{index-BjslOS9g.js → index-UvW6BEwf.js} +1 -1
- package/dist/assets/{index--myXp8ui.js → index-Y4OnpTEM.js} +1 -1
- package/dist/index.d.ts +11 -406
- package/dist/index.html +2 -2
- package/dist/index.js +7570 -7709
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -0
- package/package.json +8 -7
- package/src/components/StudioHeader.tsx +1 -1
- package/src/components/nle/TimelinePane.tsx +0 -7
- package/src/components/renders/RenderQueue.test.tsx +133 -25
- package/src/components/renders/RenderQueue.tsx +99 -92
- package/src/components/renders/RenderQueueItem.tsx +69 -64
- package/src/components/ui/Select.tsx +4 -0
- package/src/headerExports.test.tsx +23 -0
- package/src/hooks/useFileManager.ts +123 -21
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/useRightPanelIntents.ts +2 -1
- package/src/hooks/useTimelineDeleteOps.test.tsx +0 -111
- package/src/hooks/useTimelineEditing.test.tsx +3 -322
- package/src/hooks/useTimelineEditing.ts +16 -103
- package/src/hooks/useTimelineEditingTypes.ts +0 -3
- package/src/index.ts +2 -37
- package/src/main.tsx +26 -0
- package/src/player/components/TimelineTrackHeader.test.tsx +5 -26
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/timelineTheme.test.ts +4 -4
- package/src/player/components/timelineTheme.ts +30 -30
- package/src/styles/hex-baseline.json +2 -2
- package/src/styles/theme.css +0 -27
- package/src/hooks/timelineEditPermission.ts +0 -31
- package/src/hooks/useProjectFileWriter.ts +0 -150
- package/src/hooks/useTrackPendingTimelineEdit.test.tsx +0 -62
- package/src/hooks/useTrackPendingTimelineEdit.ts +0 -22
- package/src/hostMount.test.tsx +0 -222
package/src/hostMount.test.tsx
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
|
|
3
|
-
// Proves the package's public exports, not internal hook paths, are enough
|
|
4
|
-
// for a host to mount hand editing outside EditorShell: an edit writes
|
|
5
|
-
// through the history path, Undo restores it, and a 409 shows the banner.
|
|
6
|
-
import { act, createElement, useRef } from "react";
|
|
7
|
-
import { createRoot, type Root } from "react-dom/client";
|
|
8
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
-
// Backend/storage mocking only, standing in for a host's own server and
|
|
10
|
-
// persistence — not part of the host's integration code under test.
|
|
11
|
-
import { studioFileContentVersion } from "./utils/studioFileVersion";
|
|
12
|
-
import { createMemoryEditHistoryStorage } from "./utils/editHistoryStorage";
|
|
13
|
-
import {
|
|
14
|
-
flushStudioPendingEdits,
|
|
15
|
-
useTimelineEditing,
|
|
16
|
-
useProjectFileWriter,
|
|
17
|
-
usePersistentEditHistory,
|
|
18
|
-
useEditHistoryActions,
|
|
19
|
-
ExternalFileConflictBanner,
|
|
20
|
-
StudioFileConflictError,
|
|
21
|
-
type ExternalFileChangeCoordinatorHandle,
|
|
22
|
-
type TimelineElement,
|
|
23
|
-
} from "./index";
|
|
24
|
-
|
|
25
|
-
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
26
|
-
|
|
27
|
-
const ORIGINAL_HTML = '<div id="clip" data-start="0" data-track-index="0"></div>';
|
|
28
|
-
|
|
29
|
-
let root: Root | null = null;
|
|
30
|
-
afterEach(() => {
|
|
31
|
-
act(() => root?.unmount());
|
|
32
|
-
document.body.innerHTML = "";
|
|
33
|
-
vi.unstubAllGlobals();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
/** A minimal disk with etag-guarded PUT, faithful enough to exercise a real
|
|
37
|
-
* conflict: a stale If-Match gets a 409 with the current version + content. */
|
|
38
|
-
function stubHostProjectDisk(
|
|
39
|
-
initial: Record<string, string>,
|
|
40
|
-
options?: { editElsewhereAfterFirstRead?: boolean },
|
|
41
|
-
) {
|
|
42
|
-
const content = { ...initial };
|
|
43
|
-
const readsSeen = new Set<string>();
|
|
44
|
-
// The real backend's etag is a content hash (useProjectFileWriter derives
|
|
45
|
-
// If-Match the same way) — an arbitrary counter here would make every
|
|
46
|
-
// write from a host that hasn't just read the file look stale.
|
|
47
|
-
const versionOf = (path: string) => studioFileContentVersion(content[path]);
|
|
48
|
-
|
|
49
|
-
async function handlePut(path: string, init: RequestInit | undefined) {
|
|
50
|
-
const ifMatch = new Headers(init?.headers).get("If-Match");
|
|
51
|
-
const currentVersion = await versionOf(path);
|
|
52
|
-
if (ifMatch && ifMatch !== currentVersion) {
|
|
53
|
-
return new Response(JSON.stringify({ currentVersion, currentContent: content[path] }), {
|
|
54
|
-
status: 409,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
content[path] = String(init?.body ?? "");
|
|
58
|
-
return new Response(JSON.stringify({ version: await versionOf(path) }), { status: 200 });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function handleGet(path: string) {
|
|
62
|
-
const served = content[path];
|
|
63
|
-
// Simulates another writer landing between this read and the caller's
|
|
64
|
-
// own write: content moves on right after being served once, so the
|
|
65
|
-
// caller's hash of what it just read no longer matches disk.
|
|
66
|
-
if (options?.editElsewhereAfterFirstRead && !readsSeen.has(path)) {
|
|
67
|
-
readsSeen.add(path);
|
|
68
|
-
content[path] = `${served} <!--edited elsewhere-->`;
|
|
69
|
-
}
|
|
70
|
-
return new Response(
|
|
71
|
-
JSON.stringify({ content: served, version: await studioFileContentVersion(served) }),
|
|
72
|
-
{ status: 200 },
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
77
|
-
const match = String(input).match(/\/files\/([^?]+)/);
|
|
78
|
-
const path = match ? decodeURIComponent(match[1]) : "";
|
|
79
|
-
return (init?.method ?? "GET") === "PUT" ? handlePut(path, init) : handleGet(path);
|
|
80
|
-
});
|
|
81
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
82
|
-
return { content, fetchMock };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
type TimelineEditingHandle = {
|
|
86
|
-
handleTimelineElementMove: (
|
|
87
|
-
element: TimelineElement,
|
|
88
|
-
updates: { start: number; track: number },
|
|
89
|
-
) => Promise<void>;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
function mountHost(
|
|
93
|
-
clip: TimelineElement,
|
|
94
|
-
iframe: HTMLIFrameElement,
|
|
95
|
-
): {
|
|
96
|
-
timelineEditing: TimelineEditingHandle;
|
|
97
|
-
undo: () => Promise<void>;
|
|
98
|
-
bannerHost: HTMLDivElement;
|
|
99
|
-
} {
|
|
100
|
-
let timelineEditing: TimelineEditingHandle | null = null;
|
|
101
|
-
let undo: (() => Promise<void>) | null = null;
|
|
102
|
-
let conflict: ExternalFileChangeCoordinatorHandle = {
|
|
103
|
-
blocked: null,
|
|
104
|
-
retry: async () => {},
|
|
105
|
-
useExternalFile: async () => {},
|
|
106
|
-
keepStudioFile: async () => {},
|
|
107
|
-
};
|
|
108
|
-
const bannerHost = document.createElement("div");
|
|
109
|
-
document.body.append(bannerHost);
|
|
110
|
-
const bannerRoot = createRoot(bannerHost);
|
|
111
|
-
// Created once: a fresh storage object on every render would change
|
|
112
|
-
// usePersistentEditHistory's effect deps each time and loop forever.
|
|
113
|
-
const storage = createMemoryEditHistoryStorage();
|
|
114
|
-
|
|
115
|
-
function Harness() {
|
|
116
|
-
const writer = useProjectFileWriter({ projectId: "p1" });
|
|
117
|
-
const editHistory = usePersistentEditHistory({ projectId: "p1", storage });
|
|
118
|
-
const historyActions = useEditHistoryActions({
|
|
119
|
-
editHistory,
|
|
120
|
-
readOptionalProjectFile: writer.readOptionalProjectFile,
|
|
121
|
-
readProjectFile: writer.readProjectFile,
|
|
122
|
-
writeProjectFile: writer.writeProjectFile,
|
|
123
|
-
showToast: () => {},
|
|
124
|
-
syncHistoryPreviewAfterApply: async () => {},
|
|
125
|
-
// The pattern a host's own dom-edit queue would also drain through —
|
|
126
|
-
// this is the one gate that closes the undo/delete race (#4194).
|
|
127
|
-
waitForPendingDomEditSaves: async () => {
|
|
128
|
-
const result = await flushStudioPendingEdits();
|
|
129
|
-
if (result.status !== "clean") throw result.error;
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
undo = historyActions.undo;
|
|
133
|
-
const commitRef = useRef(null);
|
|
134
|
-
timelineEditing = useTimelineEditing({
|
|
135
|
-
projectId: "p1",
|
|
136
|
-
activeCompPath: "index.html",
|
|
137
|
-
timelineElements: [clip],
|
|
138
|
-
showToast: () => {},
|
|
139
|
-
writeProjectFile: async (path, content, expectedContent) => {
|
|
140
|
-
try {
|
|
141
|
-
await writer.writeProjectFile(path, content, expectedContent);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
if (error instanceof StudioFileConflictError) {
|
|
144
|
-
conflict = {
|
|
145
|
-
...conflict,
|
|
146
|
-
blocked: { status: "conflict", generation: 0, error, payload: null },
|
|
147
|
-
};
|
|
148
|
-
bannerRoot.render(createElement(ExternalFileConflictBanner, { coordinator: conflict }));
|
|
149
|
-
}
|
|
150
|
-
throw error;
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
observeProjectFileVersion: writer.observeProjectFileVersion,
|
|
154
|
-
recordEdit: editHistory.recordEdit,
|
|
155
|
-
reloadPreview: () => {},
|
|
156
|
-
previewIframeRef: { current: iframe },
|
|
157
|
-
pendingTimelineEditPathRef: { current: new Set<string>() },
|
|
158
|
-
uploadProjectFiles: async () => [],
|
|
159
|
-
handleDomZIndexReorderCommitRef: commitRef,
|
|
160
|
-
});
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
root = createRoot(document.createElement("div"));
|
|
165
|
-
act(() => root!.render(createElement(Harness)));
|
|
166
|
-
if (!timelineEditing) throw new Error("Expected hook to mount");
|
|
167
|
-
if (!undo) throw new Error("Expected undo to be exposed");
|
|
168
|
-
return { timelineEditing, undo, bannerHost };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
describe("public export surface: a host mounting hand editing outside EditorShell", () => {
|
|
172
|
-
it("writes a move through the history path and undo restores it", async () => {
|
|
173
|
-
const disk = stubHostProjectDisk({ "index.html": ORIGINAL_HTML });
|
|
174
|
-
const iframe = document.createElement("iframe");
|
|
175
|
-
document.body.append(iframe);
|
|
176
|
-
iframe.contentDocument!.body.innerHTML = ORIGINAL_HTML;
|
|
177
|
-
const clip: TimelineElement = {
|
|
178
|
-
id: "clip",
|
|
179
|
-
domId: "clip",
|
|
180
|
-
tag: "div",
|
|
181
|
-
start: 0,
|
|
182
|
-
duration: 2,
|
|
183
|
-
track: 0,
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const { timelineEditing, undo } = mountHost(clip, iframe);
|
|
187
|
-
await act(async () => {
|
|
188
|
-
await timelineEditing.handleTimelineElementMove(clip, { start: 5, track: 0 });
|
|
189
|
-
});
|
|
190
|
-
expect(disk.content["index.html"]).toContain('data-start="5"');
|
|
191
|
-
|
|
192
|
-
await act(async () => {
|
|
193
|
-
await undo();
|
|
194
|
-
});
|
|
195
|
-
expect(disk.content["index.html"]).toContain('data-start="0"');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("surfaces the conflict banner on a 409", async () => {
|
|
199
|
-
stubHostProjectDisk({ "index.html": ORIGINAL_HTML }, { editElsewhereAfterFirstRead: true });
|
|
200
|
-
const iframe = document.createElement("iframe");
|
|
201
|
-
document.body.append(iframe);
|
|
202
|
-
iframe.contentDocument!.body.innerHTML = ORIGINAL_HTML;
|
|
203
|
-
const clip: TimelineElement = {
|
|
204
|
-
id: "clip",
|
|
205
|
-
domId: "clip",
|
|
206
|
-
tag: "div",
|
|
207
|
-
start: 0,
|
|
208
|
-
duration: 2,
|
|
209
|
-
track: 0,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const { timelineEditing, bannerHost } = mountHost(clip, iframe);
|
|
213
|
-
|
|
214
|
-
await act(async () => {
|
|
215
|
-
await expect(
|
|
216
|
-
timelineEditing.handleTimelineElementMove(clip, { start: 5, track: 0 }),
|
|
217
|
-
).rejects.toBeInstanceOf(StudioFileConflictError);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
expect(bannerHost.textContent).toContain("changed outside Studio");
|
|
221
|
-
});
|
|
222
|
-
});
|