@hyperframes/studio 0.8.9 → 0.8.11
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-OP68oEBG.js → hyperframes-player-C6J6fsAD.js} +1 -1
- package/dist/assets/{index-D1Sa69m2.js → index-1vMlv05L.js} +1 -1
- package/dist/assets/{index-BIdEpMGS.js → index-BX0piiWc.js} +1 -1
- package/dist/assets/{index-57oMsXQN.js → index-DoW5v5eS.js} +109 -109
- package/dist/index.html +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/renders/useRenderQueue.ts +9 -2
- package/src/components/renders/useRenderQueueTransportError.test.tsx +49 -0
- package/src/player/components/TimelineFxButton.test.tsx +31 -0
- package/src/player/components/TimelineFxButton.tsx +24 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.11",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"gsap": "^3.13.0",
|
|
48
48
|
"marked": "^14.1.4",
|
|
49
49
|
"mediabunny": "^1.45.3",
|
|
50
|
-
"@hyperframes/core": "0.8.
|
|
51
|
-
"@hyperframes/parsers": "0.8.
|
|
52
|
-
"@hyperframes/
|
|
53
|
-
"@hyperframes/studio-server": "0.8.
|
|
54
|
-
"@hyperframes/
|
|
50
|
+
"@hyperframes/core": "0.8.11",
|
|
51
|
+
"@hyperframes/parsers": "0.8.11",
|
|
52
|
+
"@hyperframes/player": "0.8.11",
|
|
53
|
+
"@hyperframes/studio-server": "0.8.11",
|
|
54
|
+
"@hyperframes/sdk": "0.8.11"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/react": "19",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"vite": "^6.4.2",
|
|
69
69
|
"vitest": "^3.2.4",
|
|
70
70
|
"zustand": "^5.0.0",
|
|
71
|
-
"@hyperframes/producer": "0.8.
|
|
71
|
+
"@hyperframes/producer": "0.8.11"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"react": "19",
|
|
@@ -250,12 +250,19 @@ export function useRenderQueue(projectId: string | null) {
|
|
|
250
250
|
headers: { "Content-Type": "application/json" },
|
|
251
251
|
body: JSON.stringify(body),
|
|
252
252
|
});
|
|
253
|
-
} catch {
|
|
253
|
+
} catch (err) {
|
|
254
|
+
// The cause used to be discarded. Every failure — a dead server, an
|
|
255
|
+
// aborted request, a DNS error, a mid-render crash — surfaced as the
|
|
256
|
+
// same sentence, and this string is what travels into the feedback
|
|
257
|
+
// report too, so field reports of a render that fails *every time*
|
|
258
|
+
// still carried nothing to act on. Keep the CLI guidance, name the
|
|
259
|
+
// cause after it.
|
|
260
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
254
261
|
const failedJob: RenderJob = {
|
|
255
262
|
id: generateId(),
|
|
256
263
|
status: "failed",
|
|
257
264
|
progress: 0,
|
|
258
|
-
error:
|
|
265
|
+
error: `Could not reach render server: ${cause}. Use \`hyperframes render\` from the CLI instead.`,
|
|
259
266
|
filename: "Export failed",
|
|
260
267
|
createdAt: startTime,
|
|
261
268
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// The render POST's catch used to discard its error, so a dead server, a DNS
|
|
4
|
+
// failure and a mid-render crash all produced one sentence. That string is also
|
|
5
|
+
// what travels into the feedback report, so three field reports — one of them a
|
|
6
|
+
// render that failed EVERY time — arrived with nothing to act on.
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { mountRenderQueue } from "./renderQueueTestHarness";
|
|
10
|
+
|
|
11
|
+
vi.mock("./useFfmpegStatus", async (importOriginal) => ({
|
|
12
|
+
...(await importOriginal<typeof import("./useFfmpegStatus")>()),
|
|
13
|
+
useFfmpegStatus: () => ({ status: { ok: true }, checking: false, recheck: vi.fn() }),
|
|
14
|
+
}));
|
|
15
|
+
vi.mock("../../telemetry/events", () => ({ trackStudioRenderStart: vi.fn() }));
|
|
16
|
+
|
|
17
|
+
const { useRenderQueue } = await import("./useRenderQueue");
|
|
18
|
+
|
|
19
|
+
let queue: ReturnType<typeof mountRenderQueue> | null = null;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.stubGlobal(
|
|
23
|
+
"fetch",
|
|
24
|
+
vi.fn(() => Promise.reject(new TypeError("Failed to fetch"))),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
queue?.unmount();
|
|
30
|
+
queue = null;
|
|
31
|
+
document.body.innerHTML = "";
|
|
32
|
+
vi.unstubAllGlobals();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("useRenderQueue transport failure", () => {
|
|
36
|
+
it("names the cause instead of collapsing every failure into one sentence", async () => {
|
|
37
|
+
queue = mountRenderQueue(useRenderQueue);
|
|
38
|
+
const { act } = await import("react");
|
|
39
|
+
await act(async () => {
|
|
40
|
+
await queue?.api().startRender({});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const job = queue.api().jobs.at(-1);
|
|
44
|
+
expect(job?.status).toBe("failed");
|
|
45
|
+
expect(job?.error).toContain("Failed to fetch");
|
|
46
|
+
// The CLI guidance still earns its place; it just no longer stands alone.
|
|
47
|
+
expect(job?.error).toContain("hyperframes render");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -68,6 +68,37 @@ describe("TimelineFxButton", () => {
|
|
|
68
68
|
expect(document.querySelector('[role="dialog"]')).toBeTruthy();
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
// The reported symptom was "the grouping button did nothing". The dialog WAS
|
|
72
|
+
// opening — it positioned at `anchorRect.bottom + 4` with no flip and no
|
|
73
|
+
// clamp, and this button lives in a track header at the bottom of the studio
|
|
74
|
+
// window, so it opened past the viewport edge. The test below it passed
|
|
75
|
+
// throughout: happy-dom reports an all-zero rect for an unlaid-out button,
|
|
76
|
+
// which lands the dialog at top:4 — on screen, and nothing like the app.
|
|
77
|
+
it("flips the group dialog above the anchor when there is no room below", () => {
|
|
78
|
+
const host = mount(<TimelineFxButton variant="group-pointer" onGroupClips={vi.fn()} />);
|
|
79
|
+
const fx = byTextButton(host, "FX");
|
|
80
|
+
// A track header near the bottom edge of the (1024x768) window.
|
|
81
|
+
fx!.getBoundingClientRect = () =>
|
|
82
|
+
({ left: 300, top: 760, right: 320, bottom: 776, width: 20, height: 16 }) as DOMRect;
|
|
83
|
+
act(() => fx?.click());
|
|
84
|
+
const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
|
|
85
|
+
expect(dialog).toBeTruthy();
|
|
86
|
+
// Above the anchor, and fully inside the viewport: 644 + 112 === 756.
|
|
87
|
+
expect(dialog.style.top).toBe("644px");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("keeps the group dialog inside the right edge of the window", () => {
|
|
91
|
+
const host = mount(<TimelineFxButton variant="group-pointer" onGroupClips={vi.fn()} />);
|
|
92
|
+
const fx = byTextButton(host, "FX");
|
|
93
|
+
// Hard against the right edge: 224px wide + a 12px margin has to fit.
|
|
94
|
+
fx!.getBoundingClientRect = () =>
|
|
95
|
+
({ left: 1010, top: 100, right: 1024, bottom: 116, width: 14, height: 16 }) as DOMRect;
|
|
96
|
+
act(() => fx?.click());
|
|
97
|
+
const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
|
|
98
|
+
expect(dialog.style.left).toBe("788px");
|
|
99
|
+
expect(dialog.style.top).toBe("120px");
|
|
100
|
+
});
|
|
101
|
+
|
|
71
102
|
it("group-pointer variant offers Group instead of a popover", () => {
|
|
72
103
|
const onGroupClips = vi.fn();
|
|
73
104
|
const host = mount(<TimelineFxButton variant="group-pointer" onGroupClips={onGroupClips} />);
|
|
@@ -18,6 +18,24 @@ import {
|
|
|
18
18
|
} from "@hyperframes/core/audio-fx";
|
|
19
19
|
import type { HfAudioNameKind } from "@hyperframes/core/audio-carve";
|
|
20
20
|
import { TimelineFxPopover } from "../../components/editor/TimelineFxPopover.js";
|
|
21
|
+
import { resolveFloatingPanelPosition } from "../../components/editor/floatingPanel.js";
|
|
22
|
+
|
|
23
|
+
// Estimated, like FORMAT_PANEL_SIZE in RenderQueue: `w-56` is exact, and only
|
|
24
|
+
// the flip decision uses the height — the clamp keeps the dialog on screen
|
|
25
|
+
// either way.
|
|
26
|
+
const GROUP_DIALOG_SIZE = { width: 224, height: 112 };
|
|
27
|
+
|
|
28
|
+
/** Where the grouping dialog goes: flipped above the anchor when there is no
|
|
29
|
+
* room below, and clamped so neither edge leaves the viewport. */
|
|
30
|
+
function groupDialogPosition(anchorRect: DOMRect): { left: number; top: number } {
|
|
31
|
+
const { left, top } = resolveFloatingPanelPosition(
|
|
32
|
+
anchorRect,
|
|
33
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
34
|
+
GROUP_DIALOG_SIZE,
|
|
35
|
+
{ offset: 4 },
|
|
36
|
+
);
|
|
37
|
+
return { left, top };
|
|
38
|
+
}
|
|
21
39
|
|
|
22
40
|
function parseFxChainOrEmpty(raw: string | undefined): HfAudioFxChain {
|
|
23
41
|
if (!raw) return { version: 1, nodes: [] };
|
|
@@ -80,7 +98,12 @@ export function TimelineFxButton(props: TimelineFxButtonProps) {
|
|
|
80
98
|
role="dialog"
|
|
81
99
|
aria-label="Group these clips to add effects"
|
|
82
100
|
className="z-[200] w-56 rounded-md border border-white/10 bg-[#1b1b1f] p-2.5 text-[11px] text-white/75 shadow-xl"
|
|
83
|
-
|
|
101
|
+
// Was `top: anchorRect.bottom + 4` with no flip and no clamp. This
|
|
102
|
+
// button lives in a track header at the BOTTOM of the studio
|
|
103
|
+
// window, so the dialog opened past the viewport edge and the
|
|
104
|
+
// click read as doing nothing at all. Same helper the other body
|
|
105
|
+
// portals position with.
|
|
106
|
+
style={{ position: "fixed", ...groupDialogPosition(anchorRect) }}
|
|
84
107
|
onPointerDown={(event) => event.stopPropagation()}
|
|
85
108
|
>
|
|
86
109
|
<p>Group these clips to add effects to all of them.</p>
|