@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
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type ColorGradingPresetPreviews,
|
|
30
30
|
type ColorGradingPreviewOptions,
|
|
31
31
|
} from "./useColorGradingPreviews";
|
|
32
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
32
33
|
|
|
33
34
|
export type { ColorGradingPresetPreviews, ColorGradingPreviewOptions };
|
|
34
35
|
|
|
@@ -290,7 +291,7 @@ export function useColorGradingController({
|
|
|
290
291
|
return;
|
|
291
292
|
}
|
|
292
293
|
const controller = new AbortController();
|
|
293
|
-
|
|
294
|
+
studioApiFetch(cacheKey, { signal: controller.signal })
|
|
294
295
|
.then(async (response) => {
|
|
295
296
|
if (!response.ok) return { ok: false as const };
|
|
296
297
|
const data: MediaMetadataResponse | null = await response.json();
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { fxAutomationTarget, type HfAutomationLane } from "@hyperframes/core/audio-automation";
|
|
25
25
|
import { clipStart } from "./propertyPanelAudioFxGroupUtils.js";
|
|
26
26
|
import { clipAudioOnItsClock, type ClipClock } from "./clipAudioClock.js";
|
|
27
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
27
28
|
|
|
28
29
|
/** Decode rate every carve measurement shares — see `measureCarve`. */
|
|
29
30
|
const DECODE_SAMPLE_RATE = 48000;
|
|
@@ -95,7 +96,7 @@ export async function measureCarve(
|
|
|
95
96
|
if (!Ctor) return null;
|
|
96
97
|
// Each clip as it plays: trimmed to its in-point and duration, and at its own speed.
|
|
97
98
|
const decode = async (relative: string, clock: ClipClock): Promise<Float32Array> => {
|
|
98
|
-
const res = await
|
|
99
|
+
const res = await studioApiFetch(new URL(relative, doc.baseURI).href);
|
|
99
100
|
const buffer = await new Ctor(1, 1, DECODE_SAMPLE_RATE).decodeAudioData(
|
|
100
101
|
await res.arrayBuffer(),
|
|
101
102
|
);
|
|
@@ -26,6 +26,7 @@ import { trackLeveller } from "./audioFxTelemetry.js";
|
|
|
26
26
|
import type { DomEditSelection } from "./domEditingTypes";
|
|
27
27
|
import { clipSourceWindow, readClipClock } from "./clipAudioClock.js";
|
|
28
28
|
import { useAuditionTransport } from "./useAuditionTransport.js";
|
|
29
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Rate the track is decoded at. Analysis is self-consistent because it reads
|
|
@@ -63,7 +64,7 @@ export function useFxLevelling(
|
|
|
63
64
|
(window as unknown as { webkitOfflineAudioContext?: typeof OfflineAudioContext })
|
|
64
65
|
.webkitOfflineAudioContext;
|
|
65
66
|
if (!Ctor) return null;
|
|
66
|
-
const res = await
|
|
67
|
+
const res = await studioApiFetch(new URL(src, doc.baseURI).href);
|
|
67
68
|
const buffer = await new Ctor(1, 1, DECODE_SAMPLE_RATE).decodeAudioData(
|
|
68
69
|
await res.arrayBuffer(),
|
|
69
70
|
);
|
|
@@ -17,6 +17,7 @@ import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
|
17
17
|
|
|
18
18
|
import type { FeedbackContext } from "./feedbackTrigger";
|
|
19
19
|
import { isMediaFile } from "../../utils/mediaTypes";
|
|
20
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
20
21
|
|
|
21
22
|
/** Written by `hyperframes init`; absent in a hand-made or copied project. */
|
|
22
23
|
const CONFIG_FILE = "hyperframes.json";
|
|
@@ -53,7 +54,7 @@ export async function captureProjectProvenance(
|
|
|
53
54
|
if (!scaffolded) return;
|
|
54
55
|
|
|
55
56
|
try {
|
|
56
|
-
const res = await
|
|
57
|
+
const res = await studioApiFetch(
|
|
57
58
|
buildProjectApiPath(projectId, `/files/${encodeURIComponent(CONFIG_FILE)}`),
|
|
58
59
|
);
|
|
59
60
|
if (!res.ok) return;
|
|
@@ -126,6 +126,7 @@ describe("NLEProvider — composition source-map scoping", () => {
|
|
|
126
126
|
expect(observed.filter((map) => map.size === 0)).toHaveLength(2);
|
|
127
127
|
expect(fetchMock).toHaveBeenCalledWith("/api/projects/project-b/files/index.html", {
|
|
128
128
|
signal: expect.any(AbortSignal),
|
|
129
|
+
credentials: "omit",
|
|
129
130
|
});
|
|
130
131
|
expect(observed.at(-1)).toEqual(new Map([["b-comp", "compositions/b.html"]]));
|
|
131
132
|
|
|
@@ -9,6 +9,7 @@ import { setCompositionSourceMap } from "../editor/domEditingDom";
|
|
|
9
9
|
import { ensureMotionPathPluginLoaded } from "../../utils/gsapSoftReload";
|
|
10
10
|
import { useAssetPreviewStore } from "../../utils/assetPreviewStore";
|
|
11
11
|
import { createStableContext } from "../../utils/hmrStableContext";
|
|
12
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
12
13
|
|
|
13
14
|
export function shouldDisableTimelineWhileCompositionLoading(compositionLoading: boolean): boolean {
|
|
14
15
|
return compositionLoading;
|
|
@@ -209,7 +210,7 @@ export function NLEProvider({
|
|
|
209
210
|
setCompositionSourceMap(emptyMap);
|
|
210
211
|
onCompIdToSrcChangeRef.current?.(emptyMap);
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
studioApiFetch(buildProjectApiPath(projectId, `/files/index.html`), {
|
|
213
214
|
signal: controller.signal,
|
|
214
215
|
})
|
|
215
216
|
.then((r) => {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type RefObject,
|
|
13
13
|
} from "react";
|
|
14
14
|
import { Player } from "../../player";
|
|
15
|
-
import type {
|
|
15
|
+
import type { NLEContextValue } from "./NLEContext";
|
|
16
16
|
import {
|
|
17
17
|
DEFAULT_PREVIEW_ZOOM,
|
|
18
18
|
canStartPreviewPan,
|
|
@@ -29,16 +29,18 @@ import { RULER_GUTTER_PX, usePreviewGuidesStore } from "../editor/previewGuidesS
|
|
|
29
29
|
import { PreviewZoomOverlay, usePreviewNavigator } from "./PreviewZoomOverlay";
|
|
30
30
|
import { usePreviewFirstFrameTelemetry } from "../../player/hooks/usePreviewFirstFrameTelemetry";
|
|
31
31
|
import { PreviewPoster, usePreviewPoster } from "./PreviewPoster";
|
|
32
|
-
interface NLEPreviewProps
|
|
33
|
-
|
|
32
|
+
interface NLEPreviewProps extends Pick<
|
|
33
|
+
NLEContextValue,
|
|
34
|
+
| "projectId"
|
|
35
|
+
| "onIframeLoad"
|
|
36
|
+
| "previewSlots"
|
|
37
|
+
| "onShadowIframeLoad"
|
|
38
|
+
| "onShadowReadyChange"
|
|
39
|
+
| "onShadowError"
|
|
40
|
+
| "setShadowIframeNode"
|
|
41
|
+
| "resetPreviewSlots"
|
|
42
|
+
> {
|
|
34
43
|
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
35
|
-
onIframeLoad: () => void;
|
|
36
|
-
previewSlots: PreviewIframeSlot[];
|
|
37
|
-
onShadowIframeLoad: (gen: number) => void;
|
|
38
|
-
onShadowReadyChange: (gen: number, ready: boolean) => void;
|
|
39
|
-
onShadowError: (gen: number, message: string) => void;
|
|
40
|
-
setShadowIframeNode: (node: HTMLIFrameElement | null) => void;
|
|
41
|
-
resetPreviewSlots: () => void;
|
|
42
44
|
onCompositionLoadingChange?: (loading: boolean) => void;
|
|
43
45
|
portrait?: boolean;
|
|
44
46
|
directUrl?: string;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ThumbnailRequest,
|
|
7
7
|
} from "../../player/lib/thumbnailScheduler";
|
|
8
8
|
import type { PreviewCompositionSize } from "../../utils/previewCompositionSize";
|
|
9
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
9
10
|
|
|
10
11
|
/** Frame 0 as the thumbnail route last rendered it; `cachedOnly` never starts a render. */
|
|
11
12
|
function previewPosterUrl(projectId: string, cachedOnly: boolean): string {
|
|
@@ -107,7 +108,7 @@ export function renderPosterForNextOpen(projectId: string): void {
|
|
|
107
108
|
priority: "overscan",
|
|
108
109
|
discardWhenReleased: true,
|
|
109
110
|
load: async (signal) => {
|
|
110
|
-
await (await
|
|
111
|
+
await (await studioApiFetch(url, { signal })).arrayBuffer();
|
|
111
112
|
return { value: { kind: "image", url, aspect: 16 / 9 }, weight: 0 };
|
|
112
113
|
},
|
|
113
114
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* What the dev server knows about this machine's FFmpeg, as reported by
|
|
@@ -59,7 +60,7 @@ function parseStatus(body: unknown): ProbeResult {
|
|
|
59
60
|
|
|
60
61
|
async function probe(): Promise<ProbeResult> {
|
|
61
62
|
try {
|
|
62
|
-
const res = await
|
|
63
|
+
const res = await studioApiFetch("/api/environment/ffmpeg");
|
|
63
64
|
return res.ok ? parseStatus(await res.json()) : null;
|
|
64
65
|
} catch {
|
|
65
66
|
return null;
|
|
@@ -8,6 +8,7 @@ import { generateId } from "../../utils/generateId";
|
|
|
8
8
|
import { readServerError } from "./serverError";
|
|
9
9
|
import { ffmpegInstallMessage, useFfmpegStatus } from "./useFfmpegStatus";
|
|
10
10
|
import { requestStudioFeedback, type FeedbackContext } from "../feedback/feedbackTrigger";
|
|
11
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
11
12
|
|
|
12
13
|
export interface RenderJob {
|
|
13
14
|
id: string;
|
|
@@ -126,7 +127,7 @@ export function useRenderQueue(
|
|
|
126
127
|
const loadRenders = useCallback(async () => {
|
|
127
128
|
if (!projectId) return;
|
|
128
129
|
try {
|
|
129
|
-
const res = await
|
|
130
|
+
const res = await studioApiFetch(buildProjectApiPath(projectId, `/renders`));
|
|
130
131
|
if (!res.ok) {
|
|
131
132
|
setLoadError(`Couldn't load render history (server error ${res.status}).`);
|
|
132
133
|
return;
|
|
@@ -262,7 +263,7 @@ export function useRenderQueue(
|
|
|
262
263
|
}
|
|
263
264
|
let res: Response;
|
|
264
265
|
try {
|
|
265
|
-
res = await
|
|
266
|
+
res = await studioApiFetch(buildProjectApiPath(projectId, `/render`), {
|
|
266
267
|
method: "POST",
|
|
267
268
|
headers: { "Content-Type": "application/json" },
|
|
268
269
|
body: JSON.stringify(body),
|
|
@@ -376,7 +377,7 @@ export function useRenderQueue(
|
|
|
376
377
|
),
|
|
377
378
|
);
|
|
378
379
|
try {
|
|
379
|
-
const res = await
|
|
380
|
+
const res = await studioApiFetch(`/api/render/${jobId}/cancel`, { method: "POST" });
|
|
380
381
|
if (!res.ok && res.status !== 404) {
|
|
381
382
|
setActionError("Couldn't cancel on the server — the render may still be running.");
|
|
382
383
|
return;
|
|
@@ -403,7 +404,7 @@ export function useRenderQueue(
|
|
|
403
404
|
setActionError(null);
|
|
404
405
|
closeActiveEventSource(jobId);
|
|
405
406
|
try {
|
|
406
|
-
const res = await
|
|
407
|
+
const res = await studioApiFetch(`/api/render/${jobId}`, { method: "DELETE" });
|
|
407
408
|
if (!res.ok) {
|
|
408
409
|
setActionError("Couldn't delete the render — it's still on disk.");
|
|
409
410
|
return;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { AudioRow } from "./AudioRow";
|
|
17
17
|
import { GlobalAssetsView } from "./GlobalAssetsView";
|
|
18
18
|
import { AssetCard, FontRow } from "./AssetCard";
|
|
19
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
19
20
|
|
|
20
21
|
interface AssetsTabProps {
|
|
21
22
|
projectId: string;
|
|
@@ -217,7 +218,7 @@ export const AssetsTab = memo(function AssetsTab({
|
|
|
217
218
|
useEffect(() => {
|
|
218
219
|
if (manifest404Ref.current.has(projectId)) return;
|
|
219
220
|
let cancelled = false;
|
|
220
|
-
|
|
221
|
+
studioApiFetch(buildProjectApiPath(projectId, `/preview/.media/manifest.jsonl`))
|
|
221
222
|
.then((r) => {
|
|
222
223
|
if (!r.ok) {
|
|
223
224
|
manifest404Ref.current.add(projectId);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
2
3
|
|
|
3
4
|
// Cross-project asset view — the global media-use cache (~/.media), fetched from
|
|
4
5
|
// /api/assets/global. Self-contained (owns its fetch + state) so AssetsTab stays
|
|
@@ -43,7 +44,7 @@ export function GlobalAssetsView({ searchQuery }: { searchQuery: string }) {
|
|
|
43
44
|
const [records, setRecords] = useState<GlobalAssetRecord[] | null>(null);
|
|
44
45
|
useEffect(() => {
|
|
45
46
|
let cancelled = false;
|
|
46
|
-
|
|
47
|
+
studioApiFetch("/api/assets/global")
|
|
47
48
|
.then((r) => (r.ok ? r.json() : { assets: [] }))
|
|
48
49
|
.then((d) => {
|
|
49
50
|
if (!cancelled) setRecords(Array.isArray(d.assets) ? d.assets : []);
|
|
@@ -24,6 +24,7 @@ const SAVES_AN_EDIT = {
|
|
|
24
24
|
handleDomAttributeBatchCommit: true,
|
|
25
25
|
handleDomPathOffsetCommit: true,
|
|
26
26
|
handleDomGroupPathOffsetCommit: true,
|
|
27
|
+
handleDomGroupMoveBy: true,
|
|
27
28
|
handleDomZIndexReorderCommit: true,
|
|
28
29
|
handleDomBoxSizeCommit: true,
|
|
29
30
|
handleDomRotationCommit: true,
|
|
@@ -179,6 +180,7 @@ export function DomEditProvider({
|
|
|
179
180
|
handleDomAttributeBatchCommit,
|
|
180
181
|
handleDomPathOffsetCommit,
|
|
181
182
|
handleDomGroupPathOffsetCommit,
|
|
183
|
+
handleDomGroupMoveBy,
|
|
182
184
|
handleDomZIndexReorderCommit,
|
|
183
185
|
handleDomBoxSizeCommit,
|
|
184
186
|
handleDomRotationCommit,
|
|
@@ -274,6 +276,7 @@ export function DomEditProvider({
|
|
|
274
276
|
handleDomAttributeBatchCommit,
|
|
275
277
|
handleDomPathOffsetCommit,
|
|
276
278
|
handleDomGroupPathOffsetCommit,
|
|
279
|
+
handleDomGroupMoveBy,
|
|
277
280
|
handleDomZIndexReorderCommit,
|
|
278
281
|
handleDomBoxSizeCommit,
|
|
279
282
|
handleDomRotationCommit,
|
|
@@ -350,6 +353,7 @@ export function DomEditProvider({
|
|
|
350
353
|
handleDomAttributeBatchCommit,
|
|
351
354
|
handleDomPathOffsetCommit,
|
|
352
355
|
handleDomGroupPathOffsetCommit,
|
|
356
|
+
handleDomGroupMoveBy,
|
|
353
357
|
handleDomZIndexReorderCommit,
|
|
354
358
|
handleDomBoxSizeCommit,
|
|
355
359
|
handleDomRotationCommit,
|
|
@@ -4,6 +4,7 @@ import type React from "react";
|
|
|
4
4
|
import { act } from "react";
|
|
5
5
|
import { createRoot, type Root } from "react-dom/client";
|
|
6
6
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
7
|
+
import { buildStableSelector, getSelectorIndex } from "../components/editor/domEditingDom";
|
|
7
8
|
|
|
8
9
|
export function installReactActEnvironment(): void {
|
|
9
10
|
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", {
|
|
@@ -61,3 +62,25 @@ export function makeSelection(label: string, element: HTMLElement): DomEditSelec
|
|
|
61
62
|
},
|
|
62
63
|
};
|
|
63
64
|
}
|
|
65
|
+
|
|
66
|
+
/** Built as getDomLayerPatchTarget builds it: a bare class for an element with no id or hf-id. */
|
|
67
|
+
export function stableSelectionFor(el: HTMLElement): DomEditSelection {
|
|
68
|
+
const selector = buildStableSelector(el);
|
|
69
|
+
return {
|
|
70
|
+
element: el,
|
|
71
|
+
id: el.id || undefined,
|
|
72
|
+
hfId: el.getAttribute("data-hf-id") || undefined,
|
|
73
|
+
selector,
|
|
74
|
+
selectorIndex: getSelectorIndex(document, el, selector, "index.html", null),
|
|
75
|
+
sourceFile: "index.html",
|
|
76
|
+
dataAttributes: { start: "0", duration: "2" },
|
|
77
|
+
} as unknown as DomEditSelection;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function mountGroupSiblings(count: number, withIds = false): HTMLElement[] {
|
|
81
|
+
const groups = Array.from({ length: count }, (_, i) =>
|
|
82
|
+
withIds ? `<div class="group" id="group-${i}"></div>` : `<div class="group"></div>`,
|
|
83
|
+
);
|
|
84
|
+
document.body.innerHTML = `<div id="scene" class="clip" data-start="0" data-duration="2">${groups.join("")}</div>`;
|
|
85
|
+
return Array.from(document.querySelectorAll<HTMLElement>(".group"));
|
|
86
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
4
|
+
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
5
|
+
import { commitDomStyles, type DomStyleCommitContext } from "./domStyleCommit";
|
|
6
|
+
|
|
7
|
+
const selection = {
|
|
8
|
+
id: "target",
|
|
9
|
+
selector: "#target",
|
|
10
|
+
label: "Target",
|
|
11
|
+
tagName: "div",
|
|
12
|
+
sourceFile: "index.html",
|
|
13
|
+
compositionPath: "index.html",
|
|
14
|
+
textFields: [],
|
|
15
|
+
inlineStyles: {},
|
|
16
|
+
computedStyles: {},
|
|
17
|
+
dataAttributes: {},
|
|
18
|
+
capabilities: { canSelect: true, canEditStyles: true },
|
|
19
|
+
} as unknown as DomEditSelection;
|
|
20
|
+
|
|
21
|
+
async function savedPatches(styles: Record<string, string>) {
|
|
22
|
+
const saves: Array<{ operations: PatchOperation[]; label?: string }> = [];
|
|
23
|
+
const context: DomStyleCommitContext = {
|
|
24
|
+
activeCompPath: "index.html",
|
|
25
|
+
previewIframeRef: { current: null },
|
|
26
|
+
showToast: () => undefined,
|
|
27
|
+
versions: new Map(),
|
|
28
|
+
persistDomEditOperations: (async (
|
|
29
|
+
_selection: DomEditSelection,
|
|
30
|
+
operations: PatchOperation[],
|
|
31
|
+
options?: { label?: string },
|
|
32
|
+
) => {
|
|
33
|
+
saves.push({ operations, label: options?.label });
|
|
34
|
+
}) as unknown as DomStyleCommitContext["persistDomEditOperations"],
|
|
35
|
+
};
|
|
36
|
+
await commitDomStyles(context, selection, styles);
|
|
37
|
+
return saves;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const written = (operations: PatchOperation[]) =>
|
|
41
|
+
operations.map((op) => [(op as { property: string }).property, (op as { value: string }).value]);
|
|
42
|
+
|
|
43
|
+
it("writes a style named the JavaScript way under its CSS name", async () => {
|
|
44
|
+
const saves = await savedPatches({ backgroundColor: "#ff0000" });
|
|
45
|
+
expect(saves.map((save) => written(save.operations))).toEqual([
|
|
46
|
+
[["background-color", "#ff0000"]],
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("saves a map mixing both forms as one patch and one undo step, all under CSS names", async () => {
|
|
51
|
+
const saves = await savedPatches({
|
|
52
|
+
borderRadius: "4",
|
|
53
|
+
"border-color": "red",
|
|
54
|
+
WebkitTextStroke: "1px black",
|
|
55
|
+
msTransform: "none",
|
|
56
|
+
});
|
|
57
|
+
expect(saves).toHaveLength(1);
|
|
58
|
+
expect(saves[0]!.label).toBe("Edit layer style");
|
|
59
|
+
expect(written(saves[0]!.operations)).toEqual([
|
|
60
|
+
["border-radius", "4px"],
|
|
61
|
+
["border-color", "red"],
|
|
62
|
+
["-webkit-text-stroke", "1px black"],
|
|
63
|
+
["-ms-transform", "none"],
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("keeps a custom property's name exactly as given", async () => {
|
|
68
|
+
const saves = await savedPatches({ "--brand": "#123456", "--brandAccent": "#654321" });
|
|
69
|
+
expect(written(saves[0]!.operations)).toEqual([
|
|
70
|
+
["--brand", "#123456"],
|
|
71
|
+
["--brandAccent", "#654321"],
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
2
2
|
import {
|
|
3
|
+
cssPropertyName,
|
|
3
4
|
isImageBackgroundValue,
|
|
4
5
|
isManualGeometryStyleProperty,
|
|
5
6
|
normalizeDomEditStyleValue,
|
|
@@ -43,8 +44,14 @@ export interface DomStyleCommitContext {
|
|
|
43
44
|
export async function commitDomStyles(
|
|
44
45
|
context: DomStyleCommitContext,
|
|
45
46
|
selection: DomEditSelection,
|
|
46
|
-
|
|
47
|
+
stylesInEitherCase: Record<string, string>,
|
|
47
48
|
): Promise<DomEditCommitOutcome> {
|
|
49
|
+
const styles = Object.fromEntries(
|
|
50
|
+
Object.entries(stylesInEitherCase).map(([property, value]) => [
|
|
51
|
+
cssPropertyName(property),
|
|
52
|
+
value,
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
48
55
|
const entries = Object.entries(styles);
|
|
49
56
|
if (entries.length === 0) return domEditCommitDeclined("no-selection");
|
|
50
57
|
if (entries.some(([property]) => isManualGeometryStyleProperty(property)))
|
|
@@ -75,3 +75,13 @@ export function computeDraggedGsapPosition(
|
|
|
75
75
|
baseGsapY,
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
/** Puts the drag's preview offset back once the written position renders instead. */
|
|
80
|
+
export function restoreDragOffset(element: HTMLElement): void {
|
|
81
|
+
const origin = (axis: "x" | "y") =>
|
|
82
|
+
Number.parseFloat(element.getAttribute(`data-hf-drag-initial-offset-${axis}`) ?? "") || 0;
|
|
83
|
+
element.style.setProperty("--hf-studio-offset-x", `${origin("x")}px`);
|
|
84
|
+
element.style.setProperty("--hf-studio-offset-y", `${origin("y")}px`);
|
|
85
|
+
element.removeAttribute("data-hf-drag-initial-offset-x");
|
|
86
|
+
element.removeAttribute("data-hf-drag-initial-offset-y");
|
|
87
|
+
}
|