@hyperframes/studio 0.8.137 → 0.8.138
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-D_Mh6hbx.js → hyperframes-player-B30apksz.js} +1 -1
- package/dist/assets/{index-B3lJsA-e.js → index-BdjA0CLD.js} +137 -137
- package/dist/assets/{index-CC_E-Cbl.css → index-CVd4O6eH.css} +1 -1
- package/dist/assets/{index-CHEctGzc.js → index-DIJuIBwI.js} +1 -1
- package/dist/assets/{index-rtccfg9b.js → index-DXVvWfQG.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +21 -30
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +9 -8
- package/src/App.tsx +2 -7
- package/src/components/editor/LayersPanel.test.ts +25 -0
- package/src/components/editor/LayersPanel.tsx +1 -1
- package/src/hooks/useConsoleErrorCapture.test.tsx +87 -0
- package/src/hooks/useConsoleErrorCapture.ts +18 -22
- package/src/styles/studio.css +9 -0
- package/src/utils/gsapUndoRestore.browser.test.ts +21 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.138",
|
|
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/
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/player": "0.8.
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/parsers": "0.8.138",
|
|
64
|
+
"@hyperframes/core": "0.8.138",
|
|
65
|
+
"@hyperframes/player": "0.8.138",
|
|
66
|
+
"@hyperframes/sdk": "0.8.138",
|
|
67
|
+
"@hyperframes/shader-transitions": "0.8.138",
|
|
68
|
+
"@hyperframes/studio-server": "0.8.138"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"@types/react-dom": "19",
|
|
74
74
|
"@vitejs/plugin-react": "^4.0.0",
|
|
75
75
|
"chokidar": "^4.0.3",
|
|
76
|
+
"esbuild": "^0.25.12",
|
|
76
77
|
"fake-indexeddb": "^6.2.5",
|
|
77
78
|
"puppeteer-core": "^25.2.1",
|
|
78
79
|
"tailwindcss": "^4.3.3",
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
"vite": "^6.4.2",
|
|
82
83
|
"vitest": "^4.1.11",
|
|
83
84
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
85
|
+
"@hyperframes/producer": "0.8.138"
|
|
85
86
|
},
|
|
86
87
|
"peerDependencies": {
|
|
87
88
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -357,11 +357,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
357
357
|
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
|
|
358
358
|
});
|
|
359
359
|
const frameCapture = useStableHandlers(frameCaptureResult, projectId);
|
|
360
|
-
const {
|
|
361
|
-
consoleErrors,
|
|
362
|
-
setConsoleErrors,
|
|
363
|
-
resetErrors: resetConsoleErrors,
|
|
364
|
-
} = useConsoleErrorCapture(previewIframe);
|
|
360
|
+
const { consoleErrors, setConsoleErrors } = useConsoleErrorCapture(previewIframe);
|
|
365
361
|
const fileDrop = useGlobalFileDrop(timelineEditing.handleTimelineFileDrop);
|
|
366
362
|
const handleToggleRecordingRef = useRef<(method?: "button" | "keyboard") => void>(() => {});
|
|
367
363
|
const domEditSessionRef = useRef(domEditSession);
|
|
@@ -385,10 +381,9 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
385
381
|
previewIframeRef.current = iframe;
|
|
386
382
|
usePreviewIframeStore.getState().setIframe(iframe);
|
|
387
383
|
appHotkeys.syncPreviewHotkeys(iframe);
|
|
388
|
-
resetConsoleErrors();
|
|
389
384
|
refreshPreviewDocumentVersion();
|
|
390
385
|
},
|
|
391
|
-
[appHotkeys,
|
|
386
|
+
[appHotkeys, refreshPreviewDocumentVersion],
|
|
392
387
|
);
|
|
393
388
|
const rightPanel = useRightPanelIntent();
|
|
394
389
|
const { inspectorPanelActive, shouldShowMotionPath, shouldShowSelectedDomBounds } =
|
|
@@ -306,4 +306,29 @@ describe("LayersPanel preview promotion", () => {
|
|
|
306
306
|
expect(collectedRootIds()).toEqual(["a"]);
|
|
307
307
|
fetchSpy.mockRestore();
|
|
308
308
|
});
|
|
309
|
+
|
|
310
|
+
it("a row press starts no text selection, so dragging a row leaves no letters highlighted", () => {
|
|
311
|
+
const doc = new Window().document;
|
|
312
|
+
doc.body.innerHTML = `<div data-composition-id="a"><div id="back"></div><div id="front"></div></div>`;
|
|
313
|
+
const iframe = document.createElement("iframe");
|
|
314
|
+
Object.defineProperty(iframe, "contentDocument", { value: doc });
|
|
315
|
+
const item = (id: string) =>
|
|
316
|
+
({
|
|
317
|
+
key: id,
|
|
318
|
+
element: doc.getElementById(id),
|
|
319
|
+
label: id,
|
|
320
|
+
depth: 0,
|
|
321
|
+
childCount: 0,
|
|
322
|
+
id,
|
|
323
|
+
}) as unknown as DomEditLayerItem;
|
|
324
|
+
mocks.collect.mockReset().mockReturnValue([item("back"), item("front")]);
|
|
325
|
+
mocks.previewIframeRef.current = iframe;
|
|
326
|
+
usePreviewIframeStore.setState({ iframe });
|
|
327
|
+
act(() => root.render(createElement(LayersPanel)));
|
|
328
|
+
act(() => void iframe.dispatchEvent(new Event("load")));
|
|
329
|
+
|
|
330
|
+
const rows = [...host.querySelectorAll("[data-layer-index]")];
|
|
331
|
+
expect(rows).toHaveLength(2);
|
|
332
|
+
expect(rows.every((row) => row.closest(".select-none"))).toBe(true);
|
|
333
|
+
});
|
|
309
334
|
});
|
|
@@ -433,7 +433,7 @@ export const LayersPanel = memo(function LayersPanel({ host }: { host?: LayersPa
|
|
|
433
433
|
</div>
|
|
434
434
|
<div
|
|
435
435
|
ref={scrollContainerRef}
|
|
436
|
-
className="relative min-h-0 flex-1 overflow-y-auto py-1"
|
|
436
|
+
className="relative min-h-0 flex-1 select-none overflow-y-auto py-1"
|
|
437
437
|
onPointerMove={handleContainerPointerMove}
|
|
438
438
|
onPointerUp={handleContainerPointerUp}
|
|
439
439
|
onPointerCancel={handleContainerPointerUp}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, expect, it } from "vitest";
|
|
6
|
+
import { STUDIO_PREVIEW_ERRORS } from "@hyperframes/core/studio-preview-mark";
|
|
7
|
+
import { useConsoleErrorCapture } from "./useConsoleErrorCapture";
|
|
8
|
+
|
|
9
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
10
|
+
|
|
11
|
+
let capture: ReturnType<typeof useConsoleErrorCapture>;
|
|
12
|
+
function Harness({ iframe }: { iframe: HTMLIFrameElement }) {
|
|
13
|
+
capture = useConsoleErrorCapture(iframe);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function previewFrame() {
|
|
18
|
+
const previewWindow = Object.assign(new EventTarget(), { console: { error: () => undefined } });
|
|
19
|
+
const iframe = document.createElement("iframe");
|
|
20
|
+
Object.defineProperty(iframe, "contentWindow", { get: () => previewWindow });
|
|
21
|
+
return { iframe, previewWindow };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const shown = () => capture.consoleErrors?.map((finding) => finding.message);
|
|
25
|
+
|
|
26
|
+
let root: Root | null = null;
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await act(async () => root?.unmount());
|
|
29
|
+
root = null;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("shows the errors a preview raised before its load, once each", async () => {
|
|
33
|
+
const raisedBeforeLoad = "Uncaught Error: GSAP could not load from a or b";
|
|
34
|
+
const { iframe, previewWindow } = previewFrame();
|
|
35
|
+
|
|
36
|
+
root = createRoot(document.createElement("div"));
|
|
37
|
+
await act(async () => root?.render(React.createElement(Harness, { iframe })));
|
|
38
|
+
Object.assign(previewWindow, { [STUDIO_PREVIEW_ERRORS]: [raisedBeforeLoad] });
|
|
39
|
+
await act(async () => iframe.dispatchEvent(new Event("load")));
|
|
40
|
+
|
|
41
|
+
expect(shown()).toEqual([raisedBeforeLoad]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("shows a loaded preview's errors once when the capture attaches to it twice", async () => {
|
|
45
|
+
const raised = "Uncaught Error: GSAP could not load from a or b";
|
|
46
|
+
const { iframe, previewWindow } = previewFrame();
|
|
47
|
+
Object.assign(previewWindow, { [STUDIO_PREVIEW_ERRORS]: [raised] });
|
|
48
|
+
|
|
49
|
+
root = createRoot(document.createElement("div"));
|
|
50
|
+
const harness = React.createElement(Harness, { iframe });
|
|
51
|
+
await act(async () => root?.render(React.createElement(React.StrictMode, null, harness)));
|
|
52
|
+
|
|
53
|
+
expect(shown()).toEqual([raised]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("keeps listening after reading a preview's very long error list", async () => {
|
|
57
|
+
const { iframe, previewWindow } = previewFrame();
|
|
58
|
+
const raised = Array.from({ length: 200_000 }, (_, i) => `Uncaught Error: ${i}`);
|
|
59
|
+
Object.assign(previewWindow, { [STUDIO_PREVIEW_ERRORS]: raised });
|
|
60
|
+
|
|
61
|
+
root = createRoot(document.createElement("div"));
|
|
62
|
+
await act(async () => root?.render(React.createElement(Harness, { iframe })));
|
|
63
|
+
const live = new ErrorEvent("error", { message: "Uncaught Error: raised after attach" });
|
|
64
|
+
await act(async () => previewWindow.dispatchEvent(live));
|
|
65
|
+
|
|
66
|
+
expect(shown()?.length).toBe(raised.length + 1);
|
|
67
|
+
expect(shown()?.at(-1)).toBe("Uncaught Error: raised after attach");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("clears a document's errors when its load replaces it, and when the preview goes away", async () => {
|
|
71
|
+
const { iframe, previewWindow } = previewFrame();
|
|
72
|
+
const NullableHarness = ({ frame }: { frame: HTMLIFrameElement | null }) => {
|
|
73
|
+
capture = useConsoleErrorCapture(frame);
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
root = createRoot(document.createElement("div"));
|
|
77
|
+
await act(async () => root?.render(React.createElement(NullableHarness, { frame: iframe })));
|
|
78
|
+
await act(async () =>
|
|
79
|
+
previewWindow.dispatchEvent(new ErrorEvent("error", { message: "Uncaught Error: old" })),
|
|
80
|
+
);
|
|
81
|
+
Object.assign(previewWindow, { [STUDIO_PREVIEW_ERRORS]: ["Uncaught Error: new"] });
|
|
82
|
+
await act(async () => iframe.dispatchEvent(new Event("load")));
|
|
83
|
+
expect(shown()).toEqual(["Uncaught Error: new"]);
|
|
84
|
+
|
|
85
|
+
await act(async () => root?.render(React.createElement(NullableHarness, { frame: null })));
|
|
86
|
+
expect(capture.consoleErrors).toBeNull();
|
|
87
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { STUDIO_PREVIEW_ERRORS } from "@hyperframes/core/studio-preview-mark";
|
|
2
3
|
import type { LintFinding } from "../components/LintModal";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -16,6 +17,7 @@ export function useConsoleErrorCapture(previewIframe: HTMLIFrameElement | null)
|
|
|
16
17
|
|
|
17
18
|
// eslint-disable-next-line no-restricted-syntax
|
|
18
19
|
useEffect(() => {
|
|
20
|
+
resetErrors();
|
|
19
21
|
if (!previewIframe) return;
|
|
20
22
|
|
|
21
23
|
let patchedWin: (Window & typeof globalThis) | null = null;
|
|
@@ -40,49 +42,43 @@ export function useConsoleErrorCapture(previewIframe: HTMLIFrameElement | null)
|
|
|
40
42
|
|
|
41
43
|
const attachErrorCapture = () => {
|
|
42
44
|
detachErrorCapture();
|
|
45
|
+
resetErrors();
|
|
43
46
|
try {
|
|
44
47
|
const win = previewIframe.contentWindow as (Window & typeof globalThis) | null;
|
|
45
48
|
if (!win) return;
|
|
46
49
|
if ((win as unknown as Record<string, unknown>).__hfErrorCapture) return;
|
|
47
50
|
(win as unknown as Record<string, unknown>).__hfErrorCapture = true;
|
|
48
51
|
patchedWin = win;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
origConsoleError!(...args);
|
|
52
|
-
const text = args.map((a) => (a instanceof Error ? a.message : String(a))).join(" ");
|
|
53
|
-
if (text.includes("favicon")) return;
|
|
52
|
+
const record = (texts: readonly string[]) => {
|
|
53
|
+
if (texts.length === 0) return;
|
|
54
54
|
consoleErrorsRef.current = [
|
|
55
55
|
...consoleErrorsRef.current,
|
|
56
|
-
{ severity: "error", message
|
|
56
|
+
...texts.map((message) => ({ severity: "error" as const, message })),
|
|
57
57
|
];
|
|
58
58
|
setConsoleErrors([...consoleErrorsRef.current]);
|
|
59
59
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
];
|
|
66
|
-
setConsoleErrors([...consoleErrorsRef.current]);
|
|
60
|
+
origConsoleError = win.console.error.bind(win.console);
|
|
61
|
+
win.console.error = function (...args: unknown[]) {
|
|
62
|
+
origConsoleError!(...args);
|
|
63
|
+
const text = args.map((a) => (a instanceof Error ? a.message : String(a))).join(" ");
|
|
64
|
+
if (!text.includes("favicon")) record([text]);
|
|
67
65
|
};
|
|
66
|
+
errorHandler = (e: ErrorEvent) => record([e.message || String(e)]);
|
|
68
67
|
win.addEventListener("error", errorHandler);
|
|
68
|
+
const raised: unknown = Reflect.get(win, STUDIO_PREVIEW_ERRORS);
|
|
69
|
+
if (Array.isArray(raised)) record(raised.map(String));
|
|
69
70
|
} catch {
|
|
70
71
|
/* same-origin only */
|
|
71
72
|
}
|
|
72
73
|
};
|
|
73
74
|
|
|
74
75
|
attachErrorCapture();
|
|
75
|
-
|
|
76
|
-
consoleErrorsRef.current = [];
|
|
77
|
-
setConsoleErrors(null);
|
|
78
|
-
attachErrorCapture();
|
|
79
|
-
};
|
|
80
|
-
previewIframe.addEventListener("load", handleLoad);
|
|
76
|
+
previewIframe.addEventListener("load", attachErrorCapture);
|
|
81
77
|
return () => {
|
|
82
|
-
previewIframe.removeEventListener("load",
|
|
78
|
+
previewIframe.removeEventListener("load", attachErrorCapture);
|
|
83
79
|
detachErrorCapture();
|
|
84
80
|
};
|
|
85
|
-
}, [previewIframe]);
|
|
81
|
+
}, [previewIframe, resetErrors]);
|
|
86
82
|
|
|
87
|
-
return { consoleErrors, setConsoleErrors
|
|
83
|
+
return { consoleErrors, setConsoleErrors };
|
|
88
84
|
}
|
package/src/styles/studio.css
CHANGED
|
@@ -45,4 +45,13 @@ body {
|
|
|
45
45
|
*/
|
|
46
46
|
height: 100vh;
|
|
47
47
|
height: 100dvh;
|
|
48
|
+
/*
|
|
49
|
+
* Nothing may overflow #root. Dockview parks hidden panel overlays below
|
|
50
|
+
* the dock, which made the document ~2x the viewport tall; body's
|
|
51
|
+
* overflow:hidden only blocks user scrolling, so scrollIntoView, focus()
|
|
52
|
+
* and find-in-page still scrolled the viewport and cut off the header
|
|
53
|
+
* (repro: select a clip, open the Grade group). `clip`, not `hidden`,
|
|
54
|
+
* which is itself programmatically scrollable.
|
|
55
|
+
*/
|
|
56
|
+
overflow: clip;
|
|
48
57
|
}
|
|
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import puppeteer, { type Browser } from "puppeteer-core";
|
|
8
|
-
import { build } from "
|
|
8
|
+
import { build, type Plugin } from "esbuild";
|
|
9
9
|
import { afterAll, beforeAll, expect, it, vi } from "vitest";
|
|
10
10
|
import { findSystemChrome } from "../../vite.browser";
|
|
11
11
|
import { writeFixture } from "../../tests/e2e/edit-accuracy/grid.mjs";
|
|
@@ -18,20 +18,29 @@ let browser: Browser;
|
|
|
18
18
|
let undoBundle: string;
|
|
19
19
|
let softReloadBundle: string;
|
|
20
20
|
|
|
21
|
+
// recast imports these, and its code that uses them is dropped from the bundle. A new
|
|
22
|
+
// built-in fails the build instead of turning into an empty module the way Vite makes it.
|
|
23
|
+
const emptyNodeBuiltins: Plugin = {
|
|
24
|
+
name: "empty-node-builtins",
|
|
25
|
+
setup(build) {
|
|
26
|
+
build.onResolve({ filter: /^(node:)?(fs|os)$/ }, ({ path }) => ({ path, namespace: "empty" }));
|
|
27
|
+
build.onLoad({ filter: /.*/, namespace: "empty" }, () => ({ contents: "module.exports = {}" }));
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
21
31
|
async function bundle(file: string, name: string): Promise<string> {
|
|
22
32
|
const out = await build({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
},
|
|
33
|
+
entryPoints: [fileURLToPath(new URL(file, import.meta.url))],
|
|
34
|
+
bundle: true,
|
|
35
|
+
write: false,
|
|
36
|
+
platform: "browser",
|
|
37
|
+
format: "iife",
|
|
38
|
+
globalName: name,
|
|
39
|
+
alias: { canvas: fileURLToPath(new URL("../shims/canvasBrowserStub.js", import.meta.url)) },
|
|
40
|
+
plugins: [emptyNodeBuiltins],
|
|
41
|
+
define: { "import.meta.env": "{}" },
|
|
33
42
|
});
|
|
34
|
-
return
|
|
43
|
+
return out.outputFiles[0]!.text;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
beforeAll(async () => {
|