@hyperframes/studio 0.8.115 → 0.8.117
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-5fcgSstJ.js} +1 -1
- package/dist/assets/{index-CAvZ0AA-.js → index-C5qKdpgl.js} +205 -203
- package/dist/assets/{index-BoUM6Xqb.js → index-XJSawwYe.js} +1 -1
- package/dist/assets/{index-CVuorIMv.js → index-tg2LXUeu.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 +132 -41
- package/dist/index.html +1 -1
- package/dist/index.js +1184 -1084
- 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/DomEditOverlay.tsx +1 -0
- package/src/components/editor/domEditOverlayGestures.ts +1 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +2 -2
- 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 +8 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/plainTranslate.ts +5 -3
- package/src/components/editor/probeSourceElement.ts +2 -1
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelHelpers.ts +2 -2
- package/src/components/editor/propertyPanelTransformCommit.ts +2 -2
- package/src/components/editor/rotationDraft.ts +2 -1
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +2 -2
- 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/gsapRuntimeKeyframes.ts +16 -3
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +9 -1
- 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/plainRotation.ts +5 -1
- 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/useDomEditPersist.test.tsx +335 -0
- 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.test.tsx +5 -1
- package/src/hooks/useDomGeometryCommit.ts +7 -9
- 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.test.ts +39 -3
- package/src/hooks/useFreezeFrame.ts +7 -3
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +42 -29
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +17 -8
- package/src/hooks/useGsapAwareGroupMove.ts +18 -18
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapScriptCommits.test.tsx +72 -2
- package/src/hooks/useGsapScriptCommits.ts +21 -17
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +302 -3
- package/src/hooks/usePersistentEditHistory.ts +50 -17
- 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.test.ts +39 -0
- package/src/utils/gsapSoftReload.ts +9 -14
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -41
- 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 +5 -6
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +5 -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
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
import { roundTo3 } from "../../utils/rounding";
|
|
2
|
-
import {
|
|
2
|
+
import { editsPlainCss } from "../../hooks/gsapRuntimeKeyframes";
|
|
3
3
|
import { readCssRotation } from "../../hooks/draggedGsapPosition";
|
|
4
4
|
import { readStudioPathOffset, readStudioRotation, styleUsesStudioOffset } from "./manualEditsDom";
|
|
5
5
|
import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers";
|
|
@@ -142,10 +142,12 @@ export function writeTranslatePx(el: HTMLElement, p: Point): void {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/** The position the panel shows and edits: the translate a move writes, unless GSAP positions it. */
|
|
145
|
-
export function readMoveOffset(el: HTMLElement, plainTranslate =
|
|
145
|
+
export function readMoveOffset(el: HTMLElement, plainTranslate = editsPlainCss(el, "move")): Point {
|
|
146
146
|
return plainTranslate ? readTranslatePx(el) : readStudioPathOffset(el);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
export function readShownRotation(el: HTMLElement): { angle: number } {
|
|
150
|
-
return
|
|
150
|
+
return editsPlainCss(el, "rotate")
|
|
151
|
+
? { angle: roundTo3(readCssRotation(el)) }
|
|
152
|
+
: readStudioRotation(el);
|
|
151
153
|
}
|
|
@@ -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";
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
playbackStartAttributeForElement,
|
|
10
10
|
readPlaybackStartAttributes,
|
|
11
11
|
} from "../../player/lib/timelineElementHelpers";
|
|
12
|
-
import {
|
|
12
|
+
import { editsPlainCss, GSAP_TRANSFORM_KEYS } from "../../hooks/gsapRuntimeKeyframes";
|
|
13
13
|
|
|
14
14
|
export type {
|
|
15
15
|
BackgroundRemovalProgress,
|
|
@@ -496,7 +496,7 @@ export function readGsapRuntimeValuesForPanel(
|
|
|
496
496
|
const el = findPreviewNode(iframe.contentDocument, element);
|
|
497
497
|
if (!el) return null;
|
|
498
498
|
const propKeys = collectPanelPropKeys(gsapAnimations);
|
|
499
|
-
const readsTransform =
|
|
499
|
+
const readsTransform = !editsPlainCss(el, "move");
|
|
500
500
|
const result: Record<string, number> = {};
|
|
501
501
|
for (const prop of propKeys) {
|
|
502
502
|
if (!readsTransform && GSAP_TRANSFORM_KEYS.has(prop)) continue;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { editsPlainCss } from "../../hooks/gsapRuntimeKeyframes";
|
|
2
2
|
import type { DomEditSelection } from "./domEditingTypes";
|
|
3
3
|
import { readStudioBoxSize } from "./manualEdits";
|
|
4
4
|
import { readMoveOffset, UNREADABLE_TRANSLATE } from "./plainTranslate";
|
|
@@ -75,7 +75,7 @@ export function createTransformCommitHandlers({
|
|
|
75
75
|
)
|
|
76
76
|
)
|
|
77
77
|
return;
|
|
78
|
-
const plainTranslate =
|
|
78
|
+
const plainTranslate = editsPlainCss(element.element, "move");
|
|
79
79
|
const current = readMoveOffset(element.element, plainTranslate);
|
|
80
80
|
if (!Number.isFinite(current.x) || !Number.isFinite(current.y))
|
|
81
81
|
return void showToast?.(UNREADABLE_TRANSLATE);
|
|
@@ -51,7 +51,8 @@ export interface CssRotationTarget {
|
|
|
51
51
|
inline: boolean;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export type
|
|
54
|
+
export type PressChoseGsap = null;
|
|
55
|
+
export type RotationCommit = { angle: number; plain?: CssRotationTarget | PressChoseGsap };
|
|
55
56
|
|
|
56
57
|
const TRANSLATE = /^translate(?:3d|X|Y|Z)?\(/i;
|
|
57
58
|
const OWN_TURN = /^rotate\(\s*-?[\d.]+(?:e[+-]?\d+)?deg\s*\)$/i;
|
|
@@ -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();
|
|
@@ -442,7 +442,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
442
442
|
}
|
|
443
443
|
// Hold the final angle while the commit lands.
|
|
444
444
|
applyRotationDraft(sel.element, finalRotation.angle, g.plainRotation);
|
|
445
|
-
const commit =
|
|
445
|
+
const commit = { ...finalRotation, plain: g.plainRotation };
|
|
446
446
|
void Promise.resolve(opts.onRotationCommitRef.current(sel, commit))
|
|
447
447
|
.then((result) => trackPreviewEditResult("rotate", "drag", result))
|
|
448
448
|
.catch((error) => {
|
|
@@ -536,7 +536,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
536
536
|
if (finalOffset) restoreStudioPathOffset(sel.element, g.initialPathOffset);
|
|
537
537
|
};
|
|
538
538
|
void Promise.resolve(
|
|
539
|
-
opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? undefined, restore),
|
|
539
|
+
opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? undefined, restore, member),
|
|
540
540
|
)
|
|
541
541
|
.then((result) => trackPreviewEditResult("resize", "drag", result))
|
|
542
542
|
.catch((error) => {
|
|
@@ -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
|
+
}
|