@hyperframes/studio 0.8.115 → 0.8.116
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-C-9N8nk0.js → hyperframes-player-mGYvPVpw.js} +1 -1
- package/dist/assets/{index-BoUM6Xqb.js → index-BWHt0-ir.js} +1 -1
- package/dist/assets/{index-CAvZ0AA-.js → index-DYNaXPVf.js} +201 -201
- package/dist/assets/{index-CVuorIMv.js → index-Dr3hNpvS.js} +1 -1
- package/dist/{chunk-TXZMMOMA.js → chunk-3F47EFCT.js} +100 -2
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{domEditingLayers-D7VYFJJD.js → domEditingLayers-OL7GLSS4.js} +2 -2
- package/dist/index.d.ts +104 -22
- package/dist/index.html +1 -1
- package/dist/index.js +1131 -1044
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -6
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/probeSourceElement.ts +2 -1
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/hooks/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +2 -105
- package/src/hooks/gsapDragPositionCommit.ts +30 -393
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +63 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -169
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +8 -0
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +26 -58
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +2 -1
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +23 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -2
- package/src/hooks/useDomGeometryCommit.ts +1 -0
- package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +32 -1
- package/src/hooks/useEnableKeyframes.ts +2 -1
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +19 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +4 -2
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.ts +3 -2
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +17 -29
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +5 -0
- package/src/hooks/useGsapAwareGroupMove.ts +16 -16
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapScriptCommits.test.tsx +3 -1
- package/src/hooks/useGsapScriptCommits.ts +12 -10
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +277 -1
- package/src/hooks/usePersistentEditHistory.ts +47 -8
- package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.ts +2 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- package/src/player/sceneSwap.ts +2 -1
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.ts +2 -3
- package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +1 -3
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +4 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.ts +2 -1
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +2 -1
- package/dist/chunk-TXZMMOMA.js.map +0 -1
- /package/dist/{domEditingLayers-D7VYFJJD.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.116",
|
|
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/player": "0.8.
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/sdk": "0.8.
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/parsers": "0.8.116",
|
|
64
|
+
"@hyperframes/player": "0.8.116",
|
|
65
|
+
"@hyperframes/core": "0.8.116",
|
|
66
|
+
"@hyperframes/sdk": "0.8.116",
|
|
67
|
+
"@hyperframes/studio-server": "0.8.116",
|
|
68
|
+
"@hyperframes/shader-transitions": "0.8.116"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.116"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -5,6 +5,7 @@ import { useMountEffect } from "../../hooks/useMountEffect";
|
|
|
5
5
|
import { trackEvent } from "../../telemetry/client";
|
|
6
6
|
import type { CaptionStyle } from "../types";
|
|
7
7
|
import { studioWriteHeaders } from "../../utils/studioFileVersion";
|
|
8
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
8
9
|
|
|
9
10
|
interface CaptionOverrideEntry {
|
|
10
11
|
wordId?: string;
|
|
@@ -93,11 +94,14 @@ export function useCaptionSync(projectId: string | null) {
|
|
|
93
94
|
const seqAtSave = editSeqRef.current;
|
|
94
95
|
const overrides = buildOverrides(state.model);
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
studioApiFetch(
|
|
98
|
+
buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`),
|
|
99
|
+
{
|
|
100
|
+
method: "PUT",
|
|
101
|
+
headers: { "Content-Type": "text/plain", ...studioWriteHeaders() },
|
|
102
|
+
body: JSON.stringify(overrides, null, 2),
|
|
103
|
+
},
|
|
104
|
+
)
|
|
101
105
|
.then((res) => {
|
|
102
106
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
103
107
|
// A newer edit may have re-armed the debounce while this PUT was in
|
|
@@ -171,7 +175,7 @@ export function useCaptionSync(projectId: string | null) {
|
|
|
171
175
|
|
|
172
176
|
let data: { content?: string };
|
|
173
177
|
try {
|
|
174
|
-
const res = await
|
|
178
|
+
const res = await studioApiFetch(
|
|
175
179
|
buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`),
|
|
176
180
|
);
|
|
177
181
|
if (!res.ok) return; // no overrides file yet — normal
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
2
3
|
|
|
3
4
|
interface ServedProject {
|
|
4
5
|
id: string;
|
|
@@ -24,8 +25,7 @@ interface ProjectUnreachableBannerProps {
|
|
|
24
25
|
*
|
|
25
26
|
* The wording is deliberately narrower than "this project is gone". We can see
|
|
26
27
|
* which project this server serves; we cannot see why it does not serve this
|
|
27
|
-
* one, and nothing here is lost.
|
|
28
|
-
* `docs/hyperframes/plans/sdk/2026-09-20-stale-project-id-remaining-doors-plan.md` §C.
|
|
28
|
+
* one, and nothing here is lost.
|
|
29
29
|
*/
|
|
30
30
|
export function ProjectUnreachableBanner({ projectId }: ProjectUnreachableBannerProps) {
|
|
31
31
|
const [served, setServed] = useState<ServedProject[] | null>(null);
|
|
@@ -35,7 +35,7 @@ export function ProjectUnreachableBanner({ projectId }: ProjectUnreachableBanner
|
|
|
35
35
|
// A list we could not read is not evidence of anything, so a failed fetch
|
|
36
36
|
// leaves `served` empty and the banner falls back to the vague wording
|
|
37
37
|
// rather than claiming this Studio serves nothing.
|
|
38
|
-
|
|
38
|
+
studioApiFetch("/api/projects")
|
|
39
39
|
.then((res) => (res.ok ? res.json() : { projects: [] }))
|
|
40
40
|
.then((data: { projects?: ServedProject[] }) => {
|
|
41
41
|
if (!cancelled) setServed(data.projects ?? []);
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { DomEditSelection } from "./domEditingTypes";
|
|
5
|
+
import type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
|
|
6
|
+
import { STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP } from "./manualEdits";
|
|
7
|
+
import { isStudioManualEditGestureLiveIn } from "./manualEditsDom";
|
|
8
|
+
import { readTranslatePx } from "./plainTranslate";
|
|
9
|
+
import { moveDomGroupBy } from "./domGroupMoveBy";
|
|
10
|
+
import { GsapEditBlockedError } from "../../hooks/gsapEditOutcome";
|
|
11
|
+
|
|
12
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
13
|
+
|
|
14
|
+
const mocks = vi.hoisted(() => ({ drag: vi.fn() }));
|
|
15
|
+
vi.mock("../../hooks/gsapRuntimeBridge", () => ({
|
|
16
|
+
tryGsapDragIntercept: mocks.drag,
|
|
17
|
+
tryGsapRotationIntercept: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("../../hooks/useAnimatedPropertyCommit", () => ({
|
|
20
|
+
useAnimatedPropertyCommit: () => ({
|
|
21
|
+
commitAnimatedProperty: vi.fn(),
|
|
22
|
+
commitAnimatedProperties: vi.fn(),
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
vi.mock("../../hooks/useSafeGsapCommitMutation", () => ({
|
|
26
|
+
useGsapSaveFailureTelemetry: () => vi.fn(),
|
|
27
|
+
useSafeGsapCommitMutation: (commit: unknown) => commit,
|
|
28
|
+
}));
|
|
29
|
+
vi.mock("../../utils/studioSaveDiagnostics", async (importOriginal) => ({
|
|
30
|
+
...(await importOriginal<typeof import("../../utils/studioSaveDiagnostics")>()),
|
|
31
|
+
trackStudioEditBlocked: vi.fn(),
|
|
32
|
+
trackStudioSaveFailure: vi.fn(),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
import { mountGsapAwareEditing } from "../../hooks/useGsapAwareEditing.testHelpers";
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
document.body.innerHTML = "";
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** A preview iframe drawn at half size: 200x100 composition px in a 100x50 box. */
|
|
43
|
+
function preview(): Document {
|
|
44
|
+
const iframe = document.createElement("iframe");
|
|
45
|
+
document.body.append(iframe);
|
|
46
|
+
const win = iframe.contentWindow!;
|
|
47
|
+
Object.defineProperty(win, "frameElement", { configurable: true, value: iframe });
|
|
48
|
+
Object.defineProperty(win, "innerWidth", { configurable: true, value: 200 });
|
|
49
|
+
Object.defineProperty(win, "innerHeight", { configurable: true, value: 100 });
|
|
50
|
+
iframe.getBoundingClientRect = () => new DOMRect(50, 40, 100, 50);
|
|
51
|
+
return iframe.contentDocument!;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const turned = (deg: number, scale: number) => {
|
|
55
|
+
const r = (deg * Math.PI) / 180;
|
|
56
|
+
return [scale * Math.cos(r), scale * Math.sin(r), -scale * Math.sin(r), scale * Math.cos(r)];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** An element whose box centre is `at` plus its own move carried through its ancestors' linear map. */
|
|
60
|
+
function layer(doc: Document, id: string, parent = [1, 0, 0, 1], gsap?: object) {
|
|
61
|
+
const element = Object.assign(doc.createElement("div"), gsap ? { _gsap: gsap } : {});
|
|
62
|
+
doc.body.append(element);
|
|
63
|
+
const own = () => {
|
|
64
|
+
if (!gsap) return readTranslatePx(element);
|
|
65
|
+
const read = (prop: string) => Number.parseFloat(element.style.getPropertyValue(prop)) || 0;
|
|
66
|
+
return { x: read(STUDIO_OFFSET_X_PROP), y: read(STUDIO_OFFSET_Y_PROP) };
|
|
67
|
+
};
|
|
68
|
+
element.getBoundingClientRect = () => {
|
|
69
|
+
const { x, y } = own();
|
|
70
|
+
const [a, b, c, d] = parent as [number, number, number, number];
|
|
71
|
+
return new DOMRect(60 + a * x + c * y - 20, 40 + b * x + d * y - 10, 40, 20);
|
|
72
|
+
};
|
|
73
|
+
const selection = {
|
|
74
|
+
element,
|
|
75
|
+
id,
|
|
76
|
+
selector: `#${id}`,
|
|
77
|
+
label: id,
|
|
78
|
+
capabilities: { canApplyManualOffset: true },
|
|
79
|
+
} as unknown as DomEditSelection;
|
|
80
|
+
const centre = () => {
|
|
81
|
+
const r = element.getBoundingClientRect();
|
|
82
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
83
|
+
};
|
|
84
|
+
return { element, selection, centre };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mountGroupCommit() {
|
|
88
|
+
const showToast = vi.fn();
|
|
89
|
+
const save = vi.fn().mockResolvedValue(undefined);
|
|
90
|
+
const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
|
|
91
|
+
const { editing, root } = mountGsapAwareEditing({ showToast, stageElementPositionOffset });
|
|
92
|
+
const commit = (u: DomEditGroupPathOffsetCommit[], o?: { refusalToast?: boolean }) =>
|
|
93
|
+
editing().handleGsapAwareGroupPathOffsetCommit(u, o);
|
|
94
|
+
const moveBy = (moves: Parameters<typeof moveDomGroupBy>[0]) =>
|
|
95
|
+
act(() => moveDomGroupBy(moves, (u) => commit(u, { refusalToast: false })));
|
|
96
|
+
return {
|
|
97
|
+
commit: (u: DomEditGroupPathOffsetCommit[]) => commit(u),
|
|
98
|
+
moveBy,
|
|
99
|
+
showToast,
|
|
100
|
+
stageElementPositionOffset,
|
|
101
|
+
root,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const moved = (from: { x: number; y: number }, to: { x: number; y: number }) => ({
|
|
106
|
+
x: to.x - from.x,
|
|
107
|
+
y: to.y - from.y,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("handleDomGroupMoveBy", () => {
|
|
111
|
+
it("moves each box by its delta in composition px, saved under one undo entry", async () => {
|
|
112
|
+
const doc = preview();
|
|
113
|
+
const offset = layer(doc, "offset");
|
|
114
|
+
offset.element.style.setProperty("translate", "30px 0px");
|
|
115
|
+
const halved = layer(doc, "halved", [0.5, 0, 0, 0.5]);
|
|
116
|
+
const before = [offset.centre(), halved.centre()];
|
|
117
|
+
const { moveBy, stageElementPositionOffset, showToast, root } = mountGroupCommit();
|
|
118
|
+
|
|
119
|
+
await moveBy([
|
|
120
|
+
{ selection: offset.selection, delta: { x: 10, y: 0 } },
|
|
121
|
+
{ selection: halved.selection, delta: { x: 0, y: -5 } },
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
expect(moved(before[0]!, offset.centre())).toEqual({ x: 10, y: 0 });
|
|
125
|
+
expect(moved(before[1]!, halved.centre())).toEqual({ x: 0, y: -5 });
|
|
126
|
+
const calls = stageElementPositionOffset.mock.calls as unknown as unknown[][];
|
|
127
|
+
expect(calls.map((call) => call[1])).toEqual([
|
|
128
|
+
{ x: 40, y: 0 },
|
|
129
|
+
{ x: 0, y: -10 },
|
|
130
|
+
]);
|
|
131
|
+
expect(calls[0]![3]).toMatch(/^group-drag:\d+$/);
|
|
132
|
+
expect(calls[1]![3]).toBe(calls[0]![3]);
|
|
133
|
+
expect(showToast).not.toHaveBeenCalled();
|
|
134
|
+
expect(isStudioManualEditGestureLiveIn(doc)).toBe(false);
|
|
135
|
+
act(() => root.unmount());
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("lands exactly under a half-scale parent with a rotated ancestor", async () => {
|
|
139
|
+
const doc = preview();
|
|
140
|
+
const tilted = layer(doc, "tilted", turned(30, 0.5));
|
|
141
|
+
const before = tilted.centre();
|
|
142
|
+
const { moveBy, root } = mountGroupCommit();
|
|
143
|
+
|
|
144
|
+
await moveBy([{ selection: tilted.selection, delta: { x: 10, y: 0 } }]);
|
|
145
|
+
|
|
146
|
+
const delta = moved(before, tilted.centre());
|
|
147
|
+
// Exact to the 0.001 px a translate is written with.
|
|
148
|
+
expect(delta.x).toBeCloseTo(10, 3);
|
|
149
|
+
expect(delta.y).toBeCloseTo(0, 3);
|
|
150
|
+
act(() => root.unmount());
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("moves a member GSAP positions through the GSAP write, planned and written at one delta", async () => {
|
|
154
|
+
mocks.drag.mockResolvedValue({ status: "persisted" });
|
|
155
|
+
const doc = preview();
|
|
156
|
+
const animated = layer(doc, "animated", [1, 0, 0, 1], { renderTransform: () => {} });
|
|
157
|
+
const before = animated.centre();
|
|
158
|
+
const { moveBy, stageElementPositionOffset, root } = mountGroupCommit();
|
|
159
|
+
|
|
160
|
+
await moveBy([{ selection: animated.selection, delta: { x: 10, y: 0 } }]);
|
|
161
|
+
|
|
162
|
+
const write = mocks.drag.mock.calls.find((call) => !call[6]?.preflightOnly);
|
|
163
|
+
expect(write?.[0]).toBe(animated.selection);
|
|
164
|
+
expect(write?.[1]).toEqual({ x: 10, y: 0 });
|
|
165
|
+
const plan = mocks.drag.mock.calls.find((call) => call[6]?.preflightOnly);
|
|
166
|
+
expect(plan?.[1]).toEqual(write?.[1]);
|
|
167
|
+
expect(moved(before, animated.centre())).toEqual({ x: 10, y: 0 });
|
|
168
|
+
expect(stageElementPositionOffset).not.toHaveBeenCalled();
|
|
169
|
+
act(() => root.unmount());
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("skips a zero delta, and writes nothing when every delta is zero", async () => {
|
|
173
|
+
const doc = preview();
|
|
174
|
+
const still = layer(doc, "still");
|
|
175
|
+
const { moveBy, stageElementPositionOffset, root } = mountGroupCommit();
|
|
176
|
+
|
|
177
|
+
await moveBy([{ selection: still.selection, delta: { x: 0, y: 0 } }]);
|
|
178
|
+
|
|
179
|
+
expect(stageElementPositionOffset).not.toHaveBeenCalled();
|
|
180
|
+
act(() => root.unmount());
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it.each([
|
|
184
|
+
[
|
|
185
|
+
"a member that can't take a manual move",
|
|
186
|
+
(doc: Document) => {
|
|
187
|
+
const locked = layer(doc, "locked");
|
|
188
|
+
Object.assign(locked.selection.capabilities, {
|
|
189
|
+
canApplyManualOffset: false,
|
|
190
|
+
reasonIfDisabled: "Locked by a helper loop.",
|
|
191
|
+
});
|
|
192
|
+
return locked;
|
|
193
|
+
},
|
|
194
|
+
/helper loop/,
|
|
195
|
+
],
|
|
196
|
+
[
|
|
197
|
+
"a member an animation took over",
|
|
198
|
+
(doc: Document) => layer(doc, "folded", [1, 0, 0, 1], { x: "94px" }),
|
|
199
|
+
/animation took over/,
|
|
200
|
+
],
|
|
201
|
+
[
|
|
202
|
+
"a member the GSAP write refuses",
|
|
203
|
+
(doc: Document) => {
|
|
204
|
+
mocks.drag.mockResolvedValue({ status: "blocked", reason: "no-selector" });
|
|
205
|
+
return layer(doc, "blocked", [1, 0, 0, 1], { renderTransform: () => {} });
|
|
206
|
+
},
|
|
207
|
+
new GsapEditBlockedError("no-selector").message,
|
|
208
|
+
],
|
|
209
|
+
])("refuses %s before any write, without a Studio toast", async (_, make, reason) => {
|
|
210
|
+
const doc = preview();
|
|
211
|
+
const fine = layer(doc, "fine");
|
|
212
|
+
const refused = make(doc);
|
|
213
|
+
const before = [fine.centre(), refused.centre()];
|
|
214
|
+
const { moveBy, stageElementPositionOffset, showToast, root } = mountGroupCommit();
|
|
215
|
+
|
|
216
|
+
await expect(
|
|
217
|
+
moveBy([
|
|
218
|
+
{ selection: fine.selection, delta: { x: 10, y: 0 } },
|
|
219
|
+
{ selection: refused.selection, delta: { x: 10, y: 0 } },
|
|
220
|
+
]),
|
|
221
|
+
).rejects.toThrow(reason);
|
|
222
|
+
|
|
223
|
+
expect(stageElementPositionOffset).not.toHaveBeenCalled();
|
|
224
|
+
expect(mocks.drag.mock.calls.some((call) => !call[6]?.preflightOnly)).toBe(false);
|
|
225
|
+
expect(showToast).not.toHaveBeenCalled();
|
|
226
|
+
expect([fine.centre(), refused.centre()]).toEqual(before);
|
|
227
|
+
expect(isStudioManualEditGestureLiveIn(doc)).toBe(false);
|
|
228
|
+
act(() => root.unmount());
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it.each([
|
|
232
|
+
["an animation took over", () => layer(preview(), "folded", [1, 0, 0, 1], { x: "94px" }), true],
|
|
233
|
+
[
|
|
234
|
+
"the GSAP write refuses",
|
|
235
|
+
() => {
|
|
236
|
+
mocks.drag.mockResolvedValue({ status: "blocked", reason: "no-selector" });
|
|
237
|
+
return layer(preview(), "blocked", [1, 0, 0, 1], { renderTransform: () => {} });
|
|
238
|
+
},
|
|
239
|
+
false,
|
|
240
|
+
],
|
|
241
|
+
])("still toasts on a canvas group drag when %s", async (_, make, plainTranslate) => {
|
|
242
|
+
const { selection } = make();
|
|
243
|
+
const { commit, showToast, root } = mountGroupCommit();
|
|
244
|
+
|
|
245
|
+
await expect(
|
|
246
|
+
act(() => commit([{ selection, next: { x: 1, y: 2 }, plainTranslate }])),
|
|
247
|
+
).rejects.toThrow();
|
|
248
|
+
|
|
249
|
+
expect(showToast).toHaveBeenCalledWith(expect.any(String), "error");
|
|
250
|
+
act(() => root.unmount());
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { DomEditSelection } from "./domEditing";
|
|
2
|
+
import type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
|
|
3
|
+
import { canCanvasNudgeTargets } from "./domEditNudge";
|
|
4
|
+
import { isStudioManualEditGestureCurrent, restoreStudioPathOffset } from "./manualEdits";
|
|
5
|
+
import { countStudioManualEditSave } from "./manualEditsDom";
|
|
6
|
+
import {
|
|
7
|
+
applyManualOffsetDragCommit,
|
|
8
|
+
createManualOffsetDragMember,
|
|
9
|
+
endManualOffsetDragMembers,
|
|
10
|
+
restoreManualOffsetDragMembers,
|
|
11
|
+
toTopScreenPoint,
|
|
12
|
+
type ManualOffsetDragMember,
|
|
13
|
+
} from "./manualOffsetDrag";
|
|
14
|
+
|
|
15
|
+
export interface DomGroupMove {
|
|
16
|
+
selection: DomEditSelection;
|
|
17
|
+
/** Composition px: the element's box moves by exactly this in the film. */
|
|
18
|
+
delta: { x: number; y: number };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Composition px to the top page's px; the preview frames only scale and shift. */
|
|
22
|
+
function toScreen(doc: Document, delta: { x: number; y: number }) {
|
|
23
|
+
const origin = toTopScreenPoint(doc, { x: 0, y: 0 });
|
|
24
|
+
const moved = toTopScreenPoint(doc, delta);
|
|
25
|
+
if (!origin || !moved) throw new Error("The preview has no measurable size.");
|
|
26
|
+
return { x: moved.x - origin.x, y: moved.y - origin.y };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function memberFor(
|
|
30
|
+
selection: DomEditSelection,
|
|
31
|
+
scale: { x: number; y: number },
|
|
32
|
+
): ManualOffsetDragMember {
|
|
33
|
+
const { element } = selection;
|
|
34
|
+
const box = element.getBoundingClientRect();
|
|
35
|
+
const result = createManualOffsetDragMember({
|
|
36
|
+
key: selection.id ?? selection.selector ?? selection.label,
|
|
37
|
+
selection,
|
|
38
|
+
element,
|
|
39
|
+
rect: {
|
|
40
|
+
left: box.left,
|
|
41
|
+
top: box.top,
|
|
42
|
+
width: box.width,
|
|
43
|
+
height: box.height,
|
|
44
|
+
editScaleX: scale.x,
|
|
45
|
+
editScaleY: scale.y,
|
|
46
|
+
},
|
|
47
|
+
gesture: "drag",
|
|
48
|
+
});
|
|
49
|
+
if (!result.ok) throw new Error(result.reason);
|
|
50
|
+
return result.member;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Moves each element by its delta as one group drag would drop it: one save, one undo entry.
|
|
55
|
+
* Any member that can't move refuses the whole call before anything is drawn or written.
|
|
56
|
+
*/
|
|
57
|
+
export async function moveDomGroupBy(
|
|
58
|
+
moves: DomGroupMove[],
|
|
59
|
+
commitGroup: (updates: DomEditGroupPathOffsetCommit[]) => Promise<unknown>,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const moving = moves.filter(({ delta }) => delta.x !== 0 || delta.y !== 0);
|
|
62
|
+
if (moving.length === 0) return;
|
|
63
|
+
const selections = moving.map(({ selection }) => selection);
|
|
64
|
+
if (!canCanvasNudgeTargets(selections)) {
|
|
65
|
+
const blocked = selections.find((s) => !s.capabilities.canApplyManualOffset);
|
|
66
|
+
throw new Error(blocked?.capabilities.reasonIfDisabled ?? `${blocked?.label} can't be moved.`);
|
|
67
|
+
}
|
|
68
|
+
const screens = moving.map(({ selection, delta }) =>
|
|
69
|
+
toScreen(selection.element.ownerDocument, delta),
|
|
70
|
+
);
|
|
71
|
+
const members: ManualOffsetDragMember[] = [];
|
|
72
|
+
try {
|
|
73
|
+
for (const selection of selections)
|
|
74
|
+
members.push(memberFor(selection, toScreen(selection.element.ownerDocument, { x: 1, y: 1 })));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
restoreManualOffsetDragMembers(members);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
const updates = members.map((member, index) => ({
|
|
80
|
+
selection: member.selection,
|
|
81
|
+
next: applyManualOffsetDragCommit(member, screens[index]!.x, screens[index]!.y),
|
|
82
|
+
plainTranslate: member.plainTranslate,
|
|
83
|
+
}));
|
|
84
|
+
try {
|
|
85
|
+
await countStudioManualEditSave(members[0]!.element, () => commitGroup(updates));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
for (const member of members) {
|
|
88
|
+
if (isStudioManualEditGestureCurrent(member.element, member.gestureToken))
|
|
89
|
+
restoreStudioPathOffset(member.element, member.initialPathOffset);
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
} finally {
|
|
93
|
+
endManualOffsetDragMembers(members);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -5,7 +5,7 @@ import type { GestureState } from "./domEditOverlayGestures";
|
|
|
5
5
|
import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
|
|
6
6
|
import {
|
|
7
7
|
hasStudioPendingEdits,
|
|
8
|
-
|
|
8
|
+
paintBackNewestStudioPendingEdit,
|
|
9
9
|
} from "../../utils/studioPendingEdits";
|
|
10
10
|
|
|
11
11
|
afterEach(() => {
|
|
@@ -59,13 +59,13 @@ it("a drag whose save is still running can be painted back at once, and shown ag
|
|
|
59
59
|
const moved = element.style.getPropertyValue("translate");
|
|
60
60
|
expect(moved).not.toBe("40px 30px");
|
|
61
61
|
|
|
62
|
-
const
|
|
62
|
+
const shown = paintBackNewestStudioPendingEdit();
|
|
63
63
|
expect(element.style.getPropertyValue("translate")).toBe("40px 30px");
|
|
64
|
-
expect(
|
|
65
|
-
showAgain
|
|
64
|
+
expect(paintBackNewestStudioPendingEdit()).toBeNull();
|
|
65
|
+
shown!.showAgain();
|
|
66
66
|
expect(element.style.getPropertyValue("translate")).toBe(moved);
|
|
67
67
|
|
|
68
68
|
saved();
|
|
69
69
|
await vi.waitFor(() => expect(hasStudioPendingEdits()).toBe(false));
|
|
70
|
-
expect(
|
|
70
|
+
expect(paintBackNewestStudioPendingEdit()).toBeNull();
|
|
71
71
|
});
|
|
@@ -289,6 +289,39 @@ describe("a drag moves only with its own pointer pressed", () => {
|
|
|
289
289
|
);
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
+
describe("an outside change during a drag, on a preview that can swap scenes in place", () => {
|
|
293
|
+
it("leaves the dragged node in place and shows the change after the drop, loaded fresh", async () => {
|
|
294
|
+
const { live, overlay, box } = mountEditor(false);
|
|
295
|
+
const swapScenes = vi.fn((html: string) => {
|
|
296
|
+
live.contentDocument!.body.innerHTML = html.replace(/^[\s\S]*<body>|<\/body>[\s\S]*$/g, "");
|
|
297
|
+
});
|
|
298
|
+
Object.assign(live.contentWindow!, { __hfSwapScenes: swapScenes });
|
|
299
|
+
vi.stubGlobal(
|
|
300
|
+
"fetch",
|
|
301
|
+
async () => new Response(served("").contentDocument!.documentElement.outerHTML),
|
|
302
|
+
);
|
|
303
|
+
const dragged = byId(live, "title");
|
|
304
|
+
pointer(box, "pointerdown", 150, 150);
|
|
305
|
+
pointer(overlay, "pointermove", 190, 170);
|
|
306
|
+
|
|
307
|
+
act(() => api().refreshPlayer());
|
|
308
|
+
await settle();
|
|
309
|
+
expect(swapScenes, "swapped under the pointer").not.toHaveBeenCalled();
|
|
310
|
+
expect(byId(live, "title")).toBe(dragged);
|
|
311
|
+
const beforeDrop = await paintShadow(api, served("?_t=1"));
|
|
312
|
+
expect(api().iframeRef.current, "promoted under the pointer").toBe(live);
|
|
313
|
+
|
|
314
|
+
pointer(overlay, "pointerup", 190, 170);
|
|
315
|
+
await settle();
|
|
316
|
+
expect(file.title).toBe("translate: 40px 20px");
|
|
317
|
+
const fresh = served("?_t=2");
|
|
318
|
+
expect(await paintShadow(api, fresh)).toBeGreaterThan(beforeDrop);
|
|
319
|
+
expect(api().iframeRef.current).toBe(fresh);
|
|
320
|
+
expect(byId(fresh, "title").style.getPropertyValue("translate")).toBe("40px 20px");
|
|
321
|
+
vi.unstubAllGlobals();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
292
325
|
type Editor = ReturnType<typeof mountEditor>;
|
|
293
326
|
|
|
294
327
|
describe("every way a drag ends without a drop clears its mark and promotes the held reload", () => {
|
|
@@ -78,4 +78,12 @@ export const STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
78
78
|
true,
|
|
79
79
|
);
|
|
80
80
|
|
|
81
|
+
// A host whose mounted API authenticates with same-origin cookies sets this; studioApiFetch then sends them, and its
|
|
82
|
+
// requests share the preview media's sockets again.
|
|
83
|
+
export const STUDIO_API_SAME_ORIGIN_CREDENTIALS = resolveStudioBooleanEnvFlag(
|
|
84
|
+
env,
|
|
85
|
+
["VITE_STUDIO_API_SAME_ORIGIN_CREDENTIALS"],
|
|
86
|
+
false,
|
|
87
|
+
);
|
|
88
|
+
|
|
81
89
|
import { resolveEnabledSdkFamilies } from "../../utils/sdkCutoverPolicy";
|
|
@@ -146,12 +146,12 @@ function getRectCenter(element: HTMLElement): Point | null {
|
|
|
146
146
|
return null;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
};
|
|
149
|
+
const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
150
|
+
return toTopScreenPoint(element.ownerDocument, point);
|
|
151
|
+
}
|
|
153
152
|
|
|
154
|
-
|
|
153
|
+
export function toTopScreenPoint(doc: Document, point: Point): Point | null {
|
|
154
|
+
let win: Window | null = doc.defaultView;
|
|
155
155
|
while (win) {
|
|
156
156
|
const frameElement = getFrameElement(win);
|
|
157
157
|
if (!frameElement) break;
|
|
@@ -166,7 +166,6 @@ function getRectCenter(element: HTMLElement): Point | null {
|
|
|
166
166
|
};
|
|
167
167
|
win = frameElement.ownerDocument.defaultView;
|
|
168
168
|
}
|
|
169
|
-
|
|
170
169
|
return point;
|
|
171
170
|
}
|
|
172
171
|
|
|
@@ -1,41 +1,16 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
-
import
|
|
4
|
-
import { buildStableSelector, getSelectorIndex } from "./domEditingDom";
|
|
3
|
+
import { mountGroupSiblings, stableSelectionFor } from "../../hooks/domSelectionTestHarness";
|
|
5
4
|
import { selectorFor } from "./motionPathSelection";
|
|
6
5
|
|
|
7
6
|
afterEach(() => {
|
|
8
7
|
document.body.innerHTML = "";
|
|
9
8
|
});
|
|
10
9
|
|
|
11
|
-
function selectionFor(el: HTMLElement): DomEditSelection {
|
|
12
|
-
const selector = buildStableSelector(el);
|
|
13
|
-
return {
|
|
14
|
-
element: el,
|
|
15
|
-
id: el.id || undefined,
|
|
16
|
-
hfId: el.getAttribute("data-hf-id") || undefined,
|
|
17
|
-
selector,
|
|
18
|
-
selectorIndex: getSelectorIndex(document, el, selector, "index.html", null),
|
|
19
|
-
sourceFile: "index.html",
|
|
20
|
-
dataAttributes: { start: "0", duration: "2" },
|
|
21
|
-
} as unknown as DomEditSelection;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function mountGroupSiblings(): HTMLElement[] {
|
|
25
|
-
document.body.innerHTML = `
|
|
26
|
-
<div id="scene" class="clip" data-start="0" data-duration="2">
|
|
27
|
-
<div class="group"></div>
|
|
28
|
-
<div class="group"></div>
|
|
29
|
-
<div class="group"></div>
|
|
30
|
-
</div>
|
|
31
|
-
`;
|
|
32
|
-
return Array.from(document.querySelectorAll<HTMLElement>(".group"));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
10
|
describe("selectorFor", () => {
|
|
36
11
|
it("addresses one element for a class-only sibling", () => {
|
|
37
|
-
const groups = mountGroupSiblings();
|
|
38
|
-
const selector = selectorFor(
|
|
12
|
+
const groups = mountGroupSiblings(3);
|
|
13
|
+
const selector = selectorFor(stableSelectionFor(groups[2]!));
|
|
39
14
|
|
|
40
15
|
// The bare ".group" both measured home off the FIRST sibling and wrote the
|
|
41
16
|
// new motion path onto all three.
|
|
@@ -48,7 +23,7 @@ describe("selectorFor", () => {
|
|
|
48
23
|
document.body.innerHTML = `<div id="hero"></div>`;
|
|
49
24
|
const el = document.querySelector<HTMLElement>("#hero")!;
|
|
50
25
|
|
|
51
|
-
expect(selectorFor(
|
|
26
|
+
expect(selectorFor(stableSelectionFor(el))).toBe("#hero");
|
|
52
27
|
});
|
|
53
28
|
|
|
54
29
|
it("returns null with no selection", () => {
|
|
@@ -56,8 +31,8 @@ describe("selectorFor", () => {
|
|
|
56
31
|
});
|
|
57
32
|
|
|
58
33
|
it("returns null when no rung addresses one element", () => {
|
|
59
|
-
const groups = mountGroupSiblings();
|
|
60
|
-
const selection =
|
|
34
|
+
const groups = mountGroupSiblings(3);
|
|
35
|
+
const selection = stableSelectionFor(groups[1]!);
|
|
61
36
|
groups[1]!.remove();
|
|
62
37
|
|
|
63
38
|
expect(selectorFor(selection)).toBeNull();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
2
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
2
3
|
|
|
3
4
|
type ProbeTarget = { id?: string; hfId?: string; selector?: string; selectorIndex?: number };
|
|
4
5
|
|
|
@@ -12,7 +13,7 @@ const pending = new Map<string, PendingProbe[]>();
|
|
|
12
13
|
async function sendBatch(projectId: string, sourceFile: string, probes: PendingProbe[]) {
|
|
13
14
|
let exists: boolean[] = [];
|
|
14
15
|
try {
|
|
15
|
-
const response = await
|
|
16
|
+
const response = await studioApiFetch(
|
|
16
17
|
buildProjectApiPath(
|
|
17
18
|
projectId,
|
|
18
19
|
`/file-mutations/probe-elements/${encodeURIComponent(sourceFile)}`,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "./propertyPanelHelpers";
|
|
21
21
|
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
22
22
|
import { runWhenInputIdle } from "./overlayFrameLoop";
|
|
23
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
23
24
|
|
|
24
25
|
/* ------------------------------------------------------------------ */
|
|
25
26
|
/* Font helper functions */
|
|
@@ -135,7 +136,7 @@ let fontListsRequest: Promise<void> | null = null;
|
|
|
135
136
|
const fontListListeners = new Set<() => void>();
|
|
136
137
|
|
|
137
138
|
async function fetchFontList(url: string): Promise<string[]> {
|
|
138
|
-
const data = (await (await
|
|
139
|
+
const data = (await (await studioApiFetch(url)).json()) as { fonts?: unknown };
|
|
139
140
|
if (!Array.isArray(data.fonts)) throw new Error(`${url} returned no font list`);
|
|
140
141
|
return data.fonts as string[];
|
|
141
142
|
}
|
|
@@ -356,7 +357,7 @@ export function FontFamilyField({
|
|
|
356
357
|
|
|
357
358
|
const importSystemFont = async (family: string): Promise<ImportedFontAsset | null> => {
|
|
358
359
|
if (!onImportFonts) return null;
|
|
359
|
-
const response = await
|
|
360
|
+
const response = await studioApiFetch(`/api/fonts/file?family=${encodeURIComponent(family)}`);
|
|
360
361
|
if (!response.ok) return null;
|
|
361
362
|
const blob = await response.blob();
|
|
362
363
|
const ext = response.headers.get("Content-Disposition")?.match(/\.(\w+)"?$/)?.[1] ?? "ttf";
|