@hyperframes/studio 0.8.33 → 0.8.35
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-CNhkujVy.js → hyperframes-player-lPOqAC3l.js} +1 -1
- package/dist/assets/{index-DOoo96Tb.js → index-BRr1JoHX.js} +182 -172
- package/dist/assets/{index-CslAQ3JV.js → index-RPEcu_bT.js} +1 -1
- package/dist/assets/{index-DiStlnx7.js → index-ywAULfGT.js} +1 -1
- package/dist/{chunk-A7S5SGNA.js → chunk-7BP32G62.js} +294 -53
- package/dist/chunk-7BP32G62.js.map +1 -0
- package/dist/{domEditingLayers-JMMVOGTO.js → domEditingLayers-UKMVYXT6.js} +2 -2
- package/dist/index.html +1 -1
- package/dist/index.js +369 -220
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/App.tsx +6 -4
- package/src/captions/components/CaptionOverlay.sender.test.tsx +47 -0
- package/src/captions/components/CaptionOverlay.tsx +1 -0
- package/src/captions/generator.test.ts +30 -0
- package/src/captions/generator.ts +5 -3
- package/src/captions/hooks/useCaptionSync.ts +3 -2
- package/src/components/MediaPreview.tsx +2 -1
- package/src/components/editor/AnimationCard.test.tsx +3 -3
- package/src/components/editor/PropertyPanel.test.tsx +18 -14
- package/src/components/editor/SnapGuideOverlay.tsx +2 -6
- package/src/components/editor/domEditLayerWalkCache.test.ts +284 -0
- package/src/components/editor/domEditLayerWalkCache.ts +327 -0
- package/src/components/editor/domEditOverlayBasis.ts +72 -0
- package/src/components/editor/domEditOverlayGeometry.ts +78 -83
- package/src/components/editor/domEditOverlayMeasurePass.ts +60 -0
- package/src/components/editor/domEditOverlayTransform.ts +47 -9
- package/src/components/editor/domEditingDom.ts +63 -14
- package/src/components/editor/domEditingElement.ts +56 -14
- package/src/components/editor/domEditingLayers.test.ts +48 -0
- package/src/components/editor/domEditingLayers.ts +22 -35
- package/src/components/editor/offCanvasIndicatorGeometry.complexity.test.ts +373 -0
- package/src/components/editor/offCanvasIndicatorGeometry.ts +50 -11
- package/src/components/editor/offCanvasIndicatorRefresh.ts +43 -16
- package/src/components/editor/overlayFrameLoop.test.ts +246 -0
- package/src/components/editor/overlayFrameLoop.ts +204 -0
- package/src/components/editor/probeSourceElement.ts +29 -0
- package/src/components/editor/propertyPanelFill.tsx +2 -1
- package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +2 -2
- package/src/components/editor/useDomEditCompositionRect.ts +2 -4
- package/src/components/editor/useDomEditOverlayRects.ts +15 -9
- package/src/components/editor/useMotionPathData.ts +2 -4
- package/src/components/feedback/projectProvenance.ts +4 -1
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/PreviewOverlays.tsx +1 -1
- package/src/components/nle/useCompositionStack.ts +11 -4
- package/src/components/renders/RenderQueue.test.tsx +2 -2
- package/src/components/renders/RenderQueueItem.tsx +2 -1
- package/src/components/renders/useRenderQueue.ts +3 -2
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/CompositionsTab.tsx +2 -1
- package/src/components/storyboard/FramePoster.tsx +2 -1
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useCaptionDetection.sender.test.tsx +54 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useCompositionContentLoader.ts +3 -1
- package/src/hooks/useCompositionDimensions.test.tsx +47 -0
- package/src/hooks/useCompositionDimensions.ts +3 -2
- package/src/hooks/useDomEditCommits.ts +5 -4
- package/src/hooks/useElementLifecycleOps.ts +5 -1
- package/src/hooks/useFileTree.ts +5 -4
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +2 -2
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/useRenderClipContent.ts +6 -5
- package/src/hooks/useTimelineDeleteOps.ts +5 -1
- package/src/player/components/Player.tsx +6 -4
- package/src/player/components/TimelineAutomationLane.test.tsx +9 -9
- package/src/player/components/thumbnailUtils.ts +6 -2
- package/src/player/components/timelineDragDrop.test.tsx +3 -3
- package/src/player/lib/timelineIframeHelpers.test.ts +36 -0
- package/src/player/lib/timelineIframeHelpers.ts +17 -1
- package/src/test-setup.ts +8 -0
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/projectRouting.test.ts +9 -0
- package/src/utils/projectRouting.ts +18 -1
- package/src/utils/razorSplitTransaction.test.ts +3 -3
- package/src/utils/sourceScopedSelectorIndex.test.ts +148 -0
- package/src/utils/sourceScopedSelectorIndex.ts +64 -6
- package/src/utils/studioFileHistory.ts +2 -1
- package/src/utils/studioHelpers.test.ts +5 -1
- package/src/utils/studioHelpers.ts +3 -2
- package/src/utils/timelineCompositionInsert.test.ts +3 -3
- package/src/webmcp/types.ts +5 -1
- package/src/webmcp/useStudioAgentTools.polyfill.test.tsx +219 -0
- package/src/webmcp/useStudioAgentTools.test.tsx +81 -67
- package/src/webmcp/useStudioAgentTools.ts +49 -28
- package/src/webmcp/webmcpTestUtils.ts +56 -0
- package/dist/chunk-A7S5SGNA.js.map +0 -1
- /package/dist/{domEditingLayers-JMMVOGTO.js.map → domEditingLayers-UKMVYXT6.js.map} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
1
2
|
// Composition drill-down stack management for NLEContext/EditorShell
|
|
2
3
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
3
4
|
import { usePlayerStore } from "../../player";
|
|
@@ -29,7 +30,7 @@ export function useCompositionStack({
|
|
|
29
30
|
{
|
|
30
31
|
id: "master",
|
|
31
32
|
label: "Master",
|
|
32
|
-
previewUrl: `/
|
|
33
|
+
previewUrl: buildProjectApiPath(projectId, `/preview`),
|
|
33
34
|
},
|
|
34
35
|
]);
|
|
35
36
|
|
|
@@ -89,7 +90,10 @@ export function useCompositionStack({
|
|
|
89
90
|
.split("/")
|
|
90
91
|
.pop()
|
|
91
92
|
?.replace(/\.html$/, "") || resolvedPath;
|
|
92
|
-
const previewUrl =
|
|
93
|
+
const previewUrl = buildProjectApiPath(
|
|
94
|
+
projectId,
|
|
95
|
+
`/preview/comp/${encodePreviewPath(resolvedPath)}`,
|
|
96
|
+
);
|
|
93
97
|
return [...prev, { id: resolvedPath, label, previewUrl }];
|
|
94
98
|
});
|
|
95
99
|
},
|
|
@@ -103,7 +107,7 @@ export function useCompositionStack({
|
|
|
103
107
|
const master: CompositionLevel = {
|
|
104
108
|
id: "master",
|
|
105
109
|
label: "Master",
|
|
106
|
-
previewUrl: `/
|
|
110
|
+
previewUrl: buildProjectApiPath(projectId, `/preview`),
|
|
107
111
|
};
|
|
108
112
|
if (activeCompositionPath === "index.html") {
|
|
109
113
|
usePlayerStore.getState().setElements([]);
|
|
@@ -116,7 +120,10 @@ export function useCompositionStack({
|
|
|
116
120
|
// panel highlighted the row, so the canvas and timeline stayed on
|
|
117
121
|
// index.html and edits landed in the root file.
|
|
118
122
|
const label = activeCompositionPath.replace(/^compositions\//, "").replace(/\.html$/, "");
|
|
119
|
-
const previewUrl =
|
|
123
|
+
const previewUrl = buildProjectApiPath(
|
|
124
|
+
projectId,
|
|
125
|
+
`/preview/comp/${encodePreviewPath(activeCompositionPath)}`,
|
|
126
|
+
);
|
|
120
127
|
usePlayerStore.getState().setElements([]);
|
|
121
128
|
updateCompositionStack((prev) => {
|
|
122
129
|
if (prev[prev.length - 1]?.id === activeCompositionPath) return prev;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { act } from "react";
|
|
4
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
6
6
|
import { RenderQueue } from "./RenderQueue";
|
|
7
7
|
import type { FfmpegStatus } from "./useFfmpegStatus";
|
|
8
8
|
|
|
@@ -26,7 +26,7 @@ afterEach(() => {
|
|
|
26
26
|
document.body.innerHTML = "";
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
function mountRenderQueue(onStartRender:
|
|
29
|
+
function mountRenderQueue(onStartRender: Mock) {
|
|
30
30
|
const host = document.createElement("div");
|
|
31
31
|
document.body.append(host);
|
|
32
32
|
root = createRoot(host);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
1
2
|
import { memo, useCallback, useState } from "react";
|
|
2
3
|
import { VideoFrameThumbnail } from "../ui/VideoFrameThumbnail";
|
|
3
4
|
import { Button } from "../ui/Button";
|
|
@@ -36,7 +37,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
36
37
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
37
38
|
|
|
38
39
|
// Direct file URL — serves from disk, survives server restarts
|
|
39
|
-
const fileSrc = `/
|
|
40
|
+
const fileSrc = buildProjectApiPath(projectId, `/renders/file/${job.filename}`);
|
|
40
41
|
|
|
41
42
|
const handleOpen = useCallback(() => {
|
|
42
43
|
window.open(fileSrc, "_blank");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
1
2
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
2
3
|
import type { CanvasResolution } from "@hyperframes/parsers";
|
|
3
4
|
import { trackStudioRenderStart } from "../../telemetry/events";
|
|
@@ -125,7 +126,7 @@ export function useRenderQueue(
|
|
|
125
126
|
const loadRenders = useCallback(async () => {
|
|
126
127
|
if (!projectId) return;
|
|
127
128
|
try {
|
|
128
|
-
const res = await fetch(`/
|
|
129
|
+
const res = await fetch(buildProjectApiPath(projectId, `/renders`));
|
|
129
130
|
if (!res.ok) {
|
|
130
131
|
setLoadError(`Couldn't load render history (server error ${res.status}).`);
|
|
131
132
|
return;
|
|
@@ -261,7 +262,7 @@ export function useRenderQueue(
|
|
|
261
262
|
}
|
|
262
263
|
let res: Response;
|
|
263
264
|
try {
|
|
264
|
-
res = await fetch(`/
|
|
265
|
+
res = await fetch(buildProjectApiPath(projectId, `/render`), {
|
|
265
266
|
method: "POST",
|
|
266
267
|
headers: { "Content-Type": "application/json" },
|
|
267
268
|
body: JSON.stringify(body),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
1
2
|
// fallow-ignore-file code-duplication
|
|
2
3
|
import { memo, useState, useCallback, useRef, useMemo, useEffect } from "react";
|
|
3
4
|
import { SearchInput } from "../ui/SearchInput";
|
|
@@ -216,7 +217,7 @@ export const AssetsTab = memo(function AssetsTab({
|
|
|
216
217
|
useEffect(() => {
|
|
217
218
|
if (manifest404Ref.current.has(projectId)) return;
|
|
218
219
|
let cancelled = false;
|
|
219
|
-
fetch(`/
|
|
220
|
+
fetch(buildProjectApiPath(projectId, `/preview/.media/manifest.jsonl`))
|
|
220
221
|
.then((r) => {
|
|
221
222
|
if (!r.ok) {
|
|
222
223
|
manifest404Ref.current.add(projectId);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
1
2
|
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
2
3
|
import { buildCompositionThumbnailUrl } from "../../player/components/CompositionThumbnail";
|
|
3
4
|
import { setPreviewMediaMuted } from "../../player/lib/timelineIframeHelpers";
|
|
@@ -173,7 +174,7 @@ function CompCard({
|
|
|
173
174
|
setLivePreviewLoaded(false);
|
|
174
175
|
};
|
|
175
176
|
const name = comp.replace(/^compositions\//, "").replace(/\.html$/, "");
|
|
176
|
-
const previewUrl = `/
|
|
177
|
+
const previewUrl = buildProjectApiPath(projectId, `/preview/comp/${comp}`);
|
|
177
178
|
const thumbnailUrl = buildCompositionThumbnailUrl({
|
|
178
179
|
previewUrl,
|
|
179
180
|
seekTime: THUMBNAIL_SEEK_TIME_SECONDS,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
1
2
|
import { useEffect, useState } from "react";
|
|
2
3
|
import { buildCompositionThumbnailUrl } from "../../player/components/CompositionThumbnail";
|
|
3
4
|
|
|
@@ -51,7 +52,7 @@ export function FramePoster({
|
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
54
|
let url = buildCompositionThumbnailUrl({
|
|
54
|
-
previewUrl: `/
|
|
55
|
+
previewUrl: buildProjectApiPath(projectId, `/preview/comp/${src}`),
|
|
55
56
|
seekTime: seconds,
|
|
56
57
|
duration: 0,
|
|
57
58
|
origin: window.location.origin,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
1
2
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
3
|
import { copyTextToClipboard } from "../utils/clipboard";
|
|
3
4
|
import { readTagSnippetByTarget } from "../utils/sourcePatcher";
|
|
@@ -53,7 +54,7 @@ export function useAskAgentModal({
|
|
|
53
54
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
54
55
|
try {
|
|
55
56
|
const response = await fetch(
|
|
56
|
-
`/
|
|
57
|
+
buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`),
|
|
57
58
|
);
|
|
58
59
|
if (!response.ok) return;
|
|
59
60
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { expect, it, vi } from "vitest";
|
|
5
|
+
import { useCaptionDetection } from "./useCaptionDetection";
|
|
6
|
+
|
|
7
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
8
|
+
|
|
9
|
+
it("does not let a foreign window trigger caption activation", () => {
|
|
10
|
+
const host = document.createElement("div");
|
|
11
|
+
const preview = document.createElement("iframe");
|
|
12
|
+
const foreign = document.createElement("iframe");
|
|
13
|
+
document.body.append(host, preview, foreign);
|
|
14
|
+
const root = createRoot(host);
|
|
15
|
+
const fetchMock = vi.fn(() => new Promise<Response>(() => {}));
|
|
16
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
17
|
+
const params = {
|
|
18
|
+
projectId: "demo",
|
|
19
|
+
activeCompPath: "captions.html",
|
|
20
|
+
compIdToSrc: new Map<string, string>(),
|
|
21
|
+
captionEditMode: false,
|
|
22
|
+
captionHasSelection: false,
|
|
23
|
+
previewIframeRef: { current: preview },
|
|
24
|
+
captionSync: { save: vi.fn(), loadOverrides: vi.fn(async () => {}) },
|
|
25
|
+
setRightCollapsed: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
function Harness() {
|
|
28
|
+
useCaptionDetection(params);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const send = (source: MessageEventSource | null) =>
|
|
32
|
+
act(() => {
|
|
33
|
+
window.dispatchEvent(
|
|
34
|
+
new MessageEvent("message", { source, data: { source: "hf-preview", type: "state" } }),
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
act(() => root.render(<Harness />));
|
|
39
|
+
const group = preview.contentDocument!.createElement("div");
|
|
40
|
+
group.className = "caption-group";
|
|
41
|
+
preview.contentDocument!.body.append(group);
|
|
42
|
+
send(foreign.contentWindow);
|
|
43
|
+
send(null);
|
|
44
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
45
|
+
send(preview.contentWindow);
|
|
46
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
47
|
+
} finally {
|
|
48
|
+
act(() => root.unmount());
|
|
49
|
+
host.remove();
|
|
50
|
+
preview.remove();
|
|
51
|
+
foreign.remove();
|
|
52
|
+
vi.unstubAllGlobals();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
1
2
|
import { useEffect, useRef } from "react";
|
|
2
3
|
import { useCaptionStore } from "../captions/store";
|
|
3
4
|
import { acceptStudioRuntimeMessage } from "../player/lib/runtimeProtocol";
|
|
@@ -107,7 +108,7 @@ export function useCaptionDetection({
|
|
|
107
108
|
|
|
108
109
|
activating = true;
|
|
109
110
|
const srcPath = captionSrcPath;
|
|
110
|
-
fetch(`/
|
|
111
|
+
fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`))
|
|
111
112
|
.then((r) => r.json())
|
|
112
113
|
.then((data: { content?: string }) => {
|
|
113
114
|
if (!data.content || !doc || !win || useCaptionStore.getState().isEditMode) return;
|
|
@@ -130,6 +131,7 @@ export function useCaptionDetection({
|
|
|
130
131
|
};
|
|
131
132
|
|
|
132
133
|
const handleMessage = (e: MessageEvent) => {
|
|
134
|
+
if (!e.source || e.source !== previewIframeRef.current?.contentWindow) return;
|
|
133
135
|
const data = e.data;
|
|
134
136
|
if (data?.source === "hf-preview" && (data?.type === "state" || data?.type === "timeline")) {
|
|
135
137
|
if (!acceptStudioRuntimeMessage(data)) return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
1
2
|
import { useCallback } from "react";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -20,9 +21,10 @@ export function useCompositionContentLoader({
|
|
|
20
21
|
}) {
|
|
21
22
|
return useCallback(
|
|
22
23
|
(comp: string) => {
|
|
24
|
+
if (!projectId) return;
|
|
23
25
|
setActiveCompPath(comp.endsWith(".html") ? comp : null);
|
|
24
26
|
setEditingFile({ path: comp, content: null });
|
|
25
|
-
fetch(`/
|
|
27
|
+
fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`))
|
|
26
28
|
.then(async (r) => {
|
|
27
29
|
if (!r.ok) throw new Error(`Failed to load ${comp} (${r.status})`);
|
|
28
30
|
return r.json();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { expect, it } from "vitest";
|
|
5
|
+
import { useCompositionDimensions } from "./useCompositionDimensions";
|
|
6
|
+
|
|
7
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
8
|
+
|
|
9
|
+
it("accepts only the current preview frame and rejects foreign or missing senders", () => {
|
|
10
|
+
const host = document.createElement("div");
|
|
11
|
+
const preview = document.createElement("iframe");
|
|
12
|
+
const foreign = document.createElement("iframe");
|
|
13
|
+
document.body.append(host, preview, foreign);
|
|
14
|
+
const ref = { current: preview };
|
|
15
|
+
const root = createRoot(host);
|
|
16
|
+
function Harness() {
|
|
17
|
+
return JSON.stringify(useCompositionDimensions(ref));
|
|
18
|
+
}
|
|
19
|
+
const send = (source: MessageEventSource | null, width: number) =>
|
|
20
|
+
act(() => {
|
|
21
|
+
window.dispatchEvent(
|
|
22
|
+
new MessageEvent("message", {
|
|
23
|
+
source,
|
|
24
|
+
origin: window.location.origin,
|
|
25
|
+
data: { source: "hf-preview", type: "stage-size", width, height: 100 },
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
act(() => root.render(<Harness />));
|
|
31
|
+
send(foreign.contentWindow, 300);
|
|
32
|
+
send(null, 400);
|
|
33
|
+
expect(host.textContent).toBe("null");
|
|
34
|
+
send(preview.contentWindow, 200);
|
|
35
|
+
expect(host.textContent).toBe('{"width":200,"height":100}');
|
|
36
|
+
ref.current = foreign;
|
|
37
|
+
send(preview.contentWindow, 500);
|
|
38
|
+
expect(host.textContent).toBe('{"width":200,"height":100}');
|
|
39
|
+
send(foreign.contentWindow, 600);
|
|
40
|
+
expect(host.textContent).toBe('{"width":600,"height":100}');
|
|
41
|
+
} finally {
|
|
42
|
+
act(() => root.unmount());
|
|
43
|
+
host.remove();
|
|
44
|
+
preview.remove();
|
|
45
|
+
foreign.remove();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useState, type RefObject } from "react";
|
|
2
2
|
import { useMountEffect } from "./useMountEffect";
|
|
3
3
|
import type { CompositionDimensions } from "../components/renders/RenderQueue";
|
|
4
4
|
import { acceptStudioRuntimeMessage } from "../player/lib/runtimeProtocol";
|
|
@@ -29,13 +29,14 @@ function readPositiveDimensions(width: unknown, height: unknown): CompositionDim
|
|
|
29
29
|
return { width: parsedWidth, height: parsedHeight };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export function useCompositionDimensions() {
|
|
32
|
+
export function useCompositionDimensions(iframeRef: RefObject<HTMLIFrameElement | null>) {
|
|
33
33
|
const [compositionDimensions, setCompositionDimensions] = useState<CompositionDimensions | null>(
|
|
34
34
|
null,
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
useMountEffect(() => {
|
|
38
38
|
const handleMessage = (e: MessageEvent) => {
|
|
39
|
+
if (!e.source || e.source !== iframeRef.current?.contentWindow) return;
|
|
39
40
|
const dimensions = readCompositionSizeMessage(e.data);
|
|
40
41
|
if (!dimensions) return;
|
|
41
42
|
setCompositionDimensions((prev) =>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
1
2
|
import { useCallback, useRef } from "react";
|
|
2
3
|
import { findUnsafeDomPatchValues } from "@hyperframes/core/studio-api/finite-mutation";
|
|
3
4
|
import { FONT_EXT } from "../utils/mediaTypes";
|
|
@@ -127,11 +128,11 @@ export function useDomEditCommits({
|
|
|
127
128
|
FONT_EXT.test(path) &&
|
|
128
129
|
fontFamilyFromAssetPath(path).toLowerCase() === family.toLowerCase(),
|
|
129
130
|
);
|
|
130
|
-
if (!asset) return null;
|
|
131
|
+
if (!asset || !projectId) return null;
|
|
131
132
|
return {
|
|
132
133
|
family: fontFamilyFromAssetPath(asset),
|
|
133
134
|
path: asset,
|
|
134
|
-
url: `/
|
|
135
|
+
url: buildProjectApiPath(projectId, `/preview/${asset}`),
|
|
135
136
|
};
|
|
136
137
|
},
|
|
137
138
|
[fileTree, projectId, importedFontAssetsRef],
|
|
@@ -161,7 +162,7 @@ export function useDomEditCommits({
|
|
|
161
162
|
};
|
|
162
163
|
|
|
163
164
|
const readResponse = await fetch(
|
|
164
|
-
`/
|
|
165
|
+
buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`),
|
|
165
166
|
);
|
|
166
167
|
if (!readResponse.ok) {
|
|
167
168
|
throw await createStudioSaveHttpError(readResponse, `Failed to read ${targetPath}`);
|
|
@@ -219,7 +220,7 @@ export function useDomEditCommits({
|
|
|
219
220
|
domEditSaveTimestampRef.current = Date.now();
|
|
220
221
|
|
|
221
222
|
const patchResponse = await fetch(
|
|
222
|
-
`/
|
|
223
|
+
buildProjectApiPath(pid, `/file-mutations/patch-element/${encodeURIComponent(targetPath)}`),
|
|
223
224
|
{
|
|
224
225
|
method: "POST",
|
|
225
226
|
headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
1
2
|
import { useCallback } from "react";
|
|
2
3
|
import { usePlayerStore } from "../player";
|
|
3
4
|
import {
|
|
@@ -152,7 +153,10 @@ export function useElementLifecycleOps({
|
|
|
152
153
|
// selection runs to hundreds of members — the file ended up correct, but
|
|
153
154
|
// only after long enough that Delete looked like it had done nothing.
|
|
154
155
|
const removeResponse = await fetch(
|
|
155
|
-
|
|
156
|
+
buildProjectApiPath(
|
|
157
|
+
pid,
|
|
158
|
+
`/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
|
|
159
|
+
),
|
|
156
160
|
{
|
|
157
161
|
method: "POST",
|
|
158
162
|
headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
|
package/src/hooks/useFileTree.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
1
2
|
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
2
3
|
import { FONT_EXT } from "../utils/mediaTypes";
|
|
3
4
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
@@ -22,7 +23,7 @@ export function useFileTree({ projectId, projectIdRef }: UseFileTreeOptions) {
|
|
|
22
23
|
}
|
|
23
24
|
let cancelled = false;
|
|
24
25
|
setFileTreeLoaded(false);
|
|
25
|
-
fetch(
|
|
26
|
+
fetch(buildProjectApiPath(projectId))
|
|
26
27
|
.then((r) => r.json())
|
|
27
28
|
.then((data: { files?: string[]; dir?: string; compositions?: string[] }) => {
|
|
28
29
|
if (cancelled) return;
|
|
@@ -47,7 +48,7 @@ export function useFileTree({ projectId, projectIdRef }: UseFileTreeOptions) {
|
|
|
47
48
|
const refreshFileTree = useCallback(async () => {
|
|
48
49
|
const pid = projectIdRef.current;
|
|
49
50
|
if (!pid) return;
|
|
50
|
-
const res = await fetch(
|
|
51
|
+
const res = await fetch(buildProjectApiPath(pid));
|
|
51
52
|
const data = await res.json();
|
|
52
53
|
if (data.files) setFileTree(data.files);
|
|
53
54
|
}, [projectIdRef]);
|
|
@@ -62,12 +63,12 @@ export function useFileTree({ projectId, projectIdRef }: UseFileTreeOptions) {
|
|
|
62
63
|
|
|
63
64
|
const fontAssets = useMemo<ImportedFontAsset[]>(
|
|
64
65
|
() =>
|
|
65
|
-
assets
|
|
66
|
+
(projectId ? assets : [])
|
|
66
67
|
.filter((asset) => FONT_EXT.test(asset))
|
|
67
68
|
.map((asset) => ({
|
|
68
69
|
family: fontFamilyFromAssetPath(asset),
|
|
69
70
|
path: asset,
|
|
70
|
-
url: `/
|
|
71
|
+
url: projectId ? buildProjectApiPath(projectId, `/preview/${asset}`) : "",
|
|
71
72
|
})),
|
|
72
73
|
[assets, projectId],
|
|
73
74
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
1
2
|
import { useCallback } from "react";
|
|
2
3
|
import {
|
|
3
4
|
readProjectFileContent,
|
|
@@ -73,7 +74,7 @@ async function commitStructuralMutation(
|
|
|
73
74
|
|
|
74
75
|
deps.domEditSaveTimestampRef.current = Date.now();
|
|
75
76
|
const mutateResponse = await fetch(
|
|
76
|
-
`/
|
|
77
|
+
buildProjectApiPath(pid, `/file-mutations/${route}/${encodeURIComponent(targetPath)}`),
|
|
77
78
|
{
|
|
78
79
|
method: "POST",
|
|
79
80
|
headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
5
5
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
6
6
|
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
7
7
|
import { GsapEditBlockedError } from "./gsapEditOutcome";
|
|
@@ -25,7 +25,7 @@ const selection = {
|
|
|
25
25
|
element: document.createElement("div"),
|
|
26
26
|
} as unknown as DomEditSelection;
|
|
27
27
|
|
|
28
|
-
function mountFailureTelemetry(showToast:
|
|
28
|
+
function mountFailureTelemetry(showToast: Mock) {
|
|
29
29
|
let report!: ReturnType<typeof useGsapInteractionFailureTelemetry>;
|
|
30
30
|
function Harness() {
|
|
31
31
|
report = useGsapInteractionFailureTelemetry("index.html", showToast);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
1
2
|
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
|
2
3
|
import type { LintFinding } from "../components/LintModal";
|
|
3
4
|
import { usePlayerStore } from "../player";
|
|
@@ -35,7 +36,7 @@ export function useLintModal(projectId: string | null, refreshKey?: number) {
|
|
|
35
36
|
if (!projectId) return;
|
|
36
37
|
if (!opts?.background) setLinting(true);
|
|
37
38
|
try {
|
|
38
|
-
const res = await fetch(`/
|
|
39
|
+
const res = await fetch(buildProjectApiPath(projectId, `/lint`));
|
|
39
40
|
const data = await res.json();
|
|
40
41
|
const parsed = ((data.findings ?? []) as RawFinding[]).map(parseFinding);
|
|
41
42
|
if (opts?.background) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
1
2
|
import { useCallback, type ReactNode } from "react";
|
|
2
3
|
import { createElement } from "react";
|
|
3
4
|
import { CompositionThumbnail, VideoThumbnail } from "../player";
|
|
@@ -16,7 +17,7 @@ export function normalizeCompositionSrc(
|
|
|
16
17
|
): string {
|
|
17
18
|
try {
|
|
18
19
|
const parsed = new URL(compSrc, origin);
|
|
19
|
-
const previewPrefix = `/
|
|
20
|
+
const previewPrefix = buildProjectApiPath(projectId, `/preview/`);
|
|
20
21
|
if (parsed.pathname.startsWith(previewPrefix)) {
|
|
21
22
|
return parsed.pathname.slice(previewPrefix.length);
|
|
22
23
|
}
|
|
@@ -35,7 +36,7 @@ function resolvePreviewRelative(
|
|
|
35
36
|
if (!src) return null;
|
|
36
37
|
try {
|
|
37
38
|
const parsed = new URL(src, origin);
|
|
38
|
-
const base = new URL(`/
|
|
39
|
+
const base = new URL(buildProjectApiPath(pid, `/preview/`), origin).pathname;
|
|
39
40
|
return parsed.pathname.startsWith(base)
|
|
40
41
|
? decodeURIComponent(parsed.pathname.slice(base.length))
|
|
41
42
|
: null;
|
|
@@ -77,7 +78,7 @@ function renderAudioClip(
|
|
|
77
78
|
// returns the DECODED path, so it must be re-encoded here.
|
|
78
79
|
const encodedRelative = srcRelative ? encodePreviewPath(srcRelative) : null;
|
|
79
80
|
const waveformUrl = encodedRelative
|
|
80
|
-
? `/
|
|
81
|
+
? buildProjectApiPath(pid, `/waveform/${encodedRelative}`)
|
|
81
82
|
: undefined;
|
|
82
83
|
const { start, end } = trimFractions(el);
|
|
83
84
|
return createElement(AudioWaveform, {
|
|
@@ -146,7 +147,7 @@ export function useRenderClipContent({
|
|
|
146
147
|
// instead of capturing the master at a time when the comp is fading in.
|
|
147
148
|
if (compSrc) {
|
|
148
149
|
return createElement(CompositionThumbnail, {
|
|
149
|
-
previewUrl: `/
|
|
150
|
+
previewUrl: buildProjectApiPath(pid, `/preview/comp/${encodePreviewPath(compSrc)}`),
|
|
150
151
|
label: "",
|
|
151
152
|
labelColor: style.label,
|
|
152
153
|
|
|
@@ -225,7 +226,7 @@ export function useRenderClipContent({
|
|
|
225
226
|
|
|
226
227
|
if (htmlPreviewEligible) {
|
|
227
228
|
return createElement(CompositionThumbnail, {
|
|
228
|
-
previewUrl: `/
|
|
229
|
+
previewUrl: buildProjectApiPath(pid, `/preview`),
|
|
229
230
|
label: "",
|
|
230
231
|
labelColor: style.label,
|
|
231
232
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
1
2
|
// Timeline clip deletion: the marquee/multi path and the single-clip wrapper
|
|
2
3
|
// the context menu uses. Extracted verbatim from useTimelineEditing.ts to keep
|
|
3
4
|
// it under the studio 600-line cap, following useTimelineAssetDropOps.
|
|
@@ -76,7 +77,10 @@ export function useTimelineDeleteOps({
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
const removeResponse = await fetch(
|
|
79
|
-
|
|
80
|
+
buildProjectApiPath(
|
|
81
|
+
pid,
|
|
82
|
+
`/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
|
|
83
|
+
),
|
|
80
84
|
{
|
|
81
85
|
method: "POST",
|
|
82
86
|
headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
1
2
|
import { forwardRef, useEffect, useRef, useState } from "react";
|
|
2
3
|
import { isLottieAnimationLoaded } from "@hyperframes/core/runtime/lottie-readiness";
|
|
3
4
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
@@ -158,6 +159,10 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
158
159
|
const container = containerRef.current;
|
|
159
160
|
if (!container) return;
|
|
160
161
|
|
|
162
|
+
const previewSource =
|
|
163
|
+
directUrl || (projectId ? buildProjectApiPath(projectId, "/preview") : null);
|
|
164
|
+
if (!previewSource) return;
|
|
165
|
+
|
|
161
166
|
let canceled = false;
|
|
162
167
|
let cleanup: (() => void) | undefined;
|
|
163
168
|
|
|
@@ -167,10 +172,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
167
172
|
|
|
168
173
|
// Create the web component imperatively to avoid JSX custom-element typing.
|
|
169
174
|
const player = document.createElement("hyperframes-player") as HyperframesPlayerElement;
|
|
170
|
-
const srcUrl = new URL(
|
|
171
|
-
directUrl || `/api/projects/${projectId}/preview`,
|
|
172
|
-
window.location.origin,
|
|
173
|
-
);
|
|
175
|
+
const srcUrl = new URL(previewSource, window.location.origin);
|
|
174
176
|
applyPreviewVariablesToUrl(srcUrl);
|
|
175
177
|
const src = srcUrl.pathname + srcUrl.search;
|
|
176
178
|
const retryPreview = () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
import { act } from "react";
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { describe, expect, it, vi, type Mock } from "vitest";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
5
|
import { TimelineAutomationLane } from "./TimelineAutomationLane";
|
|
6
6
|
import { PAD_X } from "./automationLaneGeometry";
|
|
@@ -602,8 +602,8 @@ const mount = (automation: HfAutomation, over: Record<string, unknown> = {}) =>
|
|
|
602
602
|
// assertion below reads the calls the lane made.
|
|
603
603
|
const props = {
|
|
604
604
|
...base,
|
|
605
|
-
onPreview: base.onPreview as
|
|
606
|
-
onCommit: base.onCommit as
|
|
605
|
+
onPreview: base.onPreview as Mock,
|
|
606
|
+
onCommit: base.onCommit as Mock,
|
|
607
607
|
};
|
|
608
608
|
const { container } = render(<TimelineAutomationLane {...props} />);
|
|
609
609
|
const svg = container.querySelector("svg")!;
|
|
@@ -685,7 +685,7 @@ describe("TimelineAutomationLane segment drag", () => {
|
|
|
685
685
|
],
|
|
686
686
|
};
|
|
687
687
|
|
|
688
|
-
const previewedPoints = (props: { onPreview:
|
|
688
|
+
const previewedPoints = (props: { onPreview: Mock }) =>
|
|
689
689
|
props.onPreview.mock.calls.at(-1)?.[0].lanes[0].points as {
|
|
690
690
|
t: number;
|
|
691
691
|
v: number;
|
|
@@ -787,8 +787,8 @@ const mountRerenderable = (automation: HfAutomation, over: Record<string, unknow
|
|
|
787
787
|
const base = laneProps({ automation, ...over });
|
|
788
788
|
const props = {
|
|
789
789
|
...base,
|
|
790
|
-
onPreview: base.onPreview as
|
|
791
|
-
onCommit: base.onCommit as
|
|
790
|
+
onPreview: base.onPreview as Mock,
|
|
791
|
+
onCommit: base.onCommit as Mock,
|
|
792
792
|
};
|
|
793
793
|
const { container, rerender } = renderRerenderable(<TimelineAutomationLane {...props} />);
|
|
794
794
|
const svg = container.querySelector("svg")!;
|
|
@@ -944,7 +944,7 @@ describe("TimelineAutomationLane modifiers", () => {
|
|
|
944
944
|
},
|
|
945
945
|
],
|
|
946
946
|
};
|
|
947
|
-
const draggedTimes = (props: { onPreview:
|
|
947
|
+
const draggedTimes = (props: { onPreview: Mock }) => {
|
|
948
948
|
const points = props.onPreview.mock.calls.at(-1)?.[0].lanes[0].points as
|
|
949
949
|
| { t: number }[]
|
|
950
950
|
| undefined;
|
|
@@ -1204,7 +1204,7 @@ describe("TimelineAutomationLane group drag", () => {
|
|
|
1204
1204
|
},
|
|
1205
1205
|
],
|
|
1206
1206
|
};
|
|
1207
|
-
const pointsOf = (props: { onPreview:
|
|
1207
|
+
const pointsOf = (props: { onPreview: Mock }) =>
|
|
1208
1208
|
props.onPreview.mock.calls.at(-1)?.[0].lanes[0].points as { t: number; v: number }[];
|
|
1209
1209
|
|
|
1210
1210
|
it("moves every selected point by the same distance", () => {
|
|
@@ -1346,7 +1346,7 @@ describe("TimelineAutomationLane shift axis lock", () => {
|
|
|
1346
1346
|
},
|
|
1347
1347
|
],
|
|
1348
1348
|
};
|
|
1349
|
-
const lastPoints = (props: { onPreview:
|
|
1349
|
+
const lastPoints = (props: { onPreview: Mock }) =>
|
|
1350
1350
|
props.onPreview.mock.calls.at(-1)?.[0].lanes[0].points as { t: number; v: number }[];
|
|
1351
1351
|
|
|
1352
1352
|
it("holds the value when the pointer travels mostly sideways", () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
1
2
|
/** Rendered height of a timeline-clip thumbnail strip, in CSS px. */
|
|
2
3
|
export const THUMBNAIL_CLIP_HEIGHT = 66;
|
|
3
4
|
|
|
@@ -113,7 +114,7 @@ export function resolveMediaPreviewUrl(
|
|
|
113
114
|
return src;
|
|
114
115
|
}
|
|
115
116
|
if (!studioOrigin || parsed.origin !== studioOrigin) return src;
|
|
116
|
-
const previewPath = new URL(`/
|
|
117
|
+
const previewPath = new URL(buildProjectApiPath(projectId, `/preview/`), studioOrigin).pathname;
|
|
117
118
|
if (parsed.pathname.startsWith(previewPath)) return src;
|
|
118
119
|
if (parsed.pathname.startsWith("/api/")) return src;
|
|
119
120
|
try {
|
|
@@ -128,5 +129,8 @@ export function resolveMediaPreviewUrl(
|
|
|
128
129
|
suffix = `${parsed.search}${parsed.hash}`;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
return
|
|
132
|
+
return buildProjectApiPath(
|
|
133
|
+
projectId,
|
|
134
|
+
`/preview/${encodePreviewPath(relativePath.replace(/^\/+/, ""))}${suffix}`,
|
|
135
|
+
);
|
|
132
136
|
}
|