@hyperframes/studio 0.8.114 → 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-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
- package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
- package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
- package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
- package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
- package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
- package/dist/chunk-5EKWJZLM.js.map +1 -0
- package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
- package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
- package/dist/index.d.ts +146 -40
- package/dist/index.html +2 -2
- package/dist/index.js +4021 -3591
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +9 -8
- package/src/App.tsx +2 -2
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/dock/Dock.tsx +5 -7
- package/src/components/editor/CanvasContextMenu.tsx +3 -2
- package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
- package/src/components/editor/DomEditCropHandles.tsx +7 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -10
- package/src/components/editor/MotionPathOverlay.tsx +7 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
- package/src/components/editor/SnapToolbar.test.tsx +100 -1
- package/src/components/editor/SnapToolbar.tsx +43 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
- package/src/components/editor/audioFxTelemetry.ts +2 -3
- package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
- package/src/components/editor/cropResize.ts +4 -3
- package/src/components/editor/domEditingDom.ts +4 -0
- package/src/components/editor/domEditingLayers.ts +1 -5
- 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/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -6
- package/src/components/editor/marqueeCommit.test.tsx +106 -37
- package/src/components/editor/marqueeCommit.ts +24 -8
- package/src/components/editor/motionPathCommit.test.ts +29 -1
- package/src/components/editor/motionPathCommit.ts +21 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +41 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
- package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
- package/src/components/editor/propertyPanelFxSection.tsx +30 -28
- package/src/components/editor/propertyPanelTypes.ts +6 -3
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditNudge.test.tsx +37 -0
- package/src/components/editor/useDomEditNudge.ts +2 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
- package/src/components/editor/useDomEditZOrder.ts +12 -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/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +5 -1
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/domEditExports.test.tsx +9 -1
- package/src/hooks/domEditCommitRunner.ts +1 -1
- 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/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +4 -125
- package/src/hooks/gsapDragPositionCommit.ts +36 -384
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- 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 +96 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -168
- 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 +13 -0
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
- 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 +8 -6
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/plainRotation.ts +3 -2
- package/src/hooks/previewPromotionSelection.test.tsx +1 -1
- package/src/hooks/refreshTimelineRowText.test.ts +65 -0
- package/src/hooks/refreshTimelineRowText.ts +29 -0
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
- package/src/hooks/useDomEditTextCommits.ts +13 -3
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +8 -4
- package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -2
- package/src/hooks/useDomSelection.test.ts +26 -0
- package/src/hooks/useDomSelection.ts +10 -3
- package/src/hooks/useDomSelectionTypes.ts +6 -1
- 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 +34 -3
- package/src/hooks/useEnableKeyframes.ts +7 -4
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
- 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/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +86 -55
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +101 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
- package/src/hooks/useGsapScriptCommits.ts +15 -10
- package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
- package/src/hooks/useGsapSelectionHandlers.ts +24 -18
- package/src/hooks/useGsapTweenCache.ts +18 -12
- 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/usePopulateKeyframeCacheForFile.test.tsx +76 -2
- package/src/hooks/usePreviewInteraction.test.ts +21 -0
- package/src/hooks/usePreviewInteraction.ts +2 -0
- 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/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- 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.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +22 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/AutomationSelectionMenu.tsx +3 -2
- package/src/player/components/ClipContextMenu.test.tsx +73 -0
- package/src/player/components/ClipContextMenu.tsx +33 -31
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
- package/src/player/components/OutOfSyncBadge.tsx +5 -5
- package/src/player/components/TextClipContent.test.ts +19 -0
- package/src/player/components/TextClipContent.tsx +27 -0
- package/src/player/components/TrackGapContextMenu.tsx +3 -6
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
- package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
- package/src/player/components/timelineClipGestureHandlers.ts +20 -2
- package/src/player/components/timelineEditing.ts +3 -1
- package/src/player/components/timelineLinkSelection.ts +35 -1
- package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
- package/src/player/components/useTimelineClipDrag.ts +6 -7
- package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
- package/src/player/hooks/timelinePlayerSync.ts +6 -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/lib/timelineElementHelpers.test.ts +87 -1
- package/src/player/lib/timelineElementHelpers.ts +6 -15
- package/src/player/lib/timelineText.ts +40 -0
- package/src/player/sceneSwap.ts +2 -1
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- 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/keyframeUsage.ts +79 -0
- package/src/utils/previewFeatureUsage.test.ts +32 -0
- package/src/utils/previewFeatureUsage.ts +46 -0
- 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.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +51 -5
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +4 -3
- package/dist/chunk-BIUXP3PG.js.map +0 -1
- package/dist/chunk-K47E2ZJA.js.map +0 -1
- /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
- /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
|
@@ -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 type { TimelineEditOutcome } from "../../hooks/timelineEditPermission";
|
|
2
|
+
import { trackKeyframeUsage } from "../../utils/keyframeUsage";
|
|
2
3
|
import { useCallback, useMemo } from "react";
|
|
3
4
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
5
|
import type { TimelineElement } from "../../player";
|
|
@@ -259,7 +260,12 @@ export function useTimelineEditCallbacks({
|
|
|
259
260
|
if (!selection) return;
|
|
260
261
|
// Serial: each removal rewrites the same source file, so dispatching
|
|
261
262
|
// them together would have the later writes read a pre-edit document.
|
|
262
|
-
|
|
263
|
+
let changed = false;
|
|
264
|
+
for (const anim of anims) {
|
|
265
|
+
const result = await handleGsapRemoveAllKeyframes(anim.id, selection, false);
|
|
266
|
+
changed = result || changed;
|
|
267
|
+
}
|
|
268
|
+
if (changed) trackKeyframeUsage("remove_all");
|
|
263
269
|
});
|
|
264
270
|
},
|
|
265
271
|
onDeleteKeyframe: (elId, keyframe) => {
|
|
@@ -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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { filename } from "./assetHelpers";
|
|
3
|
+
import { menuClasses } from "../ui/menuStyle";
|
|
3
4
|
|
|
4
5
|
/** Reject names that would escape the asset directory or break paths. */
|
|
5
6
|
function isValidAssetName(name: string): boolean {
|
|
@@ -94,8 +95,7 @@ export function ContextMenu({
|
|
|
94
95
|
onClose();
|
|
95
96
|
}, [renameDraft, asset, onRename, onClose]);
|
|
96
97
|
|
|
97
|
-
const itemCls =
|
|
98
|
-
"w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden active:bg-neutral-700/70 transition-colors";
|
|
98
|
+
const itemCls = `${menuClasses.row} ${menuClasses.rowEnabled} active:bg-neutral-700/70 transition-colors`;
|
|
99
99
|
|
|
100
100
|
return (
|
|
101
101
|
<div
|
|
@@ -110,7 +110,7 @@ export function ContextMenu({
|
|
|
110
110
|
ref={menuRef}
|
|
111
111
|
role="menu"
|
|
112
112
|
aria-label={`Actions for ${filename(asset)}`}
|
|
113
|
-
className=
|
|
113
|
+
className={`${menuClasses.panel} absolute min-w-[160px] text-xs`}
|
|
114
114
|
style={{ left: pos.x, top: pos.y }}
|
|
115
115
|
onClick={(e) => e.stopPropagation()}
|
|
116
116
|
>
|
|
@@ -147,7 +147,7 @@ export function ContextMenu({
|
|
|
147
147
|
<button
|
|
148
148
|
role="menuitem"
|
|
149
149
|
onClick={() => setMode("confirm-delete")}
|
|
150
|
-
className={`${
|
|
150
|
+
className={`${menuClasses.row} ${menuClasses.rowDanger} active:bg-neutral-700/70 transition-colors`}
|
|
151
151
|
>
|
|
152
152
|
Delete
|
|
153
153
|
</button>
|
|
@@ -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 : []);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { menuClasses } from "./menuStyle";
|
|
5
|
+
|
|
6
|
+
const spacing = /(^| )-?(p|py|pt|pb|m|my|mt|mb)-\d/;
|
|
7
|
+
const root = join(__dirname, "../..");
|
|
8
|
+
|
|
9
|
+
/** Panels that are not a column of rows: a dialog, a crop toolbar, a preset card and a bare speed list. */
|
|
10
|
+
const NOT_ROW_MENUS = new Set([
|
|
11
|
+
"player/components/AudioGainDialog.tsx",
|
|
12
|
+
"components/editor/CropPresetBar.tsx",
|
|
13
|
+
"components/editor/EaseCurveSection.tsx",
|
|
14
|
+
"player/components/SpeedMenu.tsx",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function sources(dir: string): string[] {
|
|
18
|
+
return readdirSync(dir).flatMap((name) => {
|
|
19
|
+
const path = join(dir, name);
|
|
20
|
+
if (statSync(path).isDirectory()) return sources(path);
|
|
21
|
+
return /\.tsx$/.test(name) && !/\.test\./.test(name) ? [path] : [];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const files = sources(root).map((path) => ({
|
|
26
|
+
name: relative(root, path).replaceAll("\\", "/"),
|
|
27
|
+
text: readFileSync(path, "utf8"),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
describe("menu style", () => {
|
|
31
|
+
it("adds no padding or margin around a menu's rows", () => {
|
|
32
|
+
for (const key of ["panel", "group", "divider"] as const)
|
|
33
|
+
expect(menuClasses[key], key).not.toMatch(spacing);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("clips the panel to its own rounded corners, so a highlight never pokes out of them", () => {
|
|
37
|
+
expect(menuClasses.panel).toContain("overflow-hidden");
|
|
38
|
+
expect(menuClasses.panel).toMatch(/(^| )rounded-/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("is the only place a menu panel's fill, border and shadow are written", () => {
|
|
42
|
+
const handBuilt = files
|
|
43
|
+
.filter(({ name }) => !NOT_ROW_MENUS.has(name))
|
|
44
|
+
.filter(({ text }) =>
|
|
45
|
+
[...text.matchAll(/["'`]([^"'`]*)["'`]/g)].some(
|
|
46
|
+
([, literal]) =>
|
|
47
|
+
/shadow-(lg|xl)/.test(literal!) && /bg-(neutral-900|panel-bg-2)\b/.test(literal!),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
.map(({ name }) => name);
|
|
51
|
+
expect(handBuilt).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("is the panel of every role=menu, wherever its element is written", () => {
|
|
55
|
+
const unowned = files
|
|
56
|
+
.filter(({ text }) => text.includes('role="menu"'))
|
|
57
|
+
.filter(({ name, text }) => !NOT_ROW_MENUS.has(name) && !text.includes("menuClasses.panel"))
|
|
58
|
+
.map(({ name }) => name);
|
|
59
|
+
expect(unowned).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The one owner of context menu spacing: rows touch the panel, which clips to its rounded corners. */
|
|
2
|
+
export const menuClasses = {
|
|
3
|
+
panel: "overflow-hidden rounded-md border border-neutral-700 bg-neutral-900 shadow-lg",
|
|
4
|
+
group: "empty:hidden border-b border-neutral-700/60",
|
|
5
|
+
divider: "border-t border-neutral-700/60",
|
|
6
|
+
row: "w-full px-3 py-1.5 text-left text-xs outline-hidden",
|
|
7
|
+
rowEnabled: "text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 cursor-pointer",
|
|
8
|
+
rowDanger: "text-danger-ink hover:bg-danger/25 focus-visible:bg-danger/25 cursor-pointer",
|
|
9
|
+
rowDisabled: "text-neutral-600 cursor-not-allowed",
|
|
10
|
+
} as const;
|
|
@@ -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,
|
|
@@ -251,7 +253,7 @@ export function DomEditProvider({
|
|
|
251
253
|
commitMutationRef.current = commitMutation;
|
|
252
254
|
|
|
253
255
|
const stableCommitMutation = useCallback<DomEditActionsValue["commitMutation"]>(
|
|
254
|
-
(mutation, options) => commitMutationRef.current(mutation, options),
|
|
256
|
+
(mutation, options, selection) => commitMutationRef.current(mutation, options, selection),
|
|
255
257
|
[],
|
|
256
258
|
);
|
|
257
259
|
|
|
@@ -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,
|
|
@@ -79,7 +79,7 @@ describe("DOM editing package exports", () => {
|
|
|
79
79
|
front.id = "front";
|
|
80
80
|
parent.append(back, front);
|
|
81
81
|
document.body.append(parent);
|
|
82
|
-
const commitZ = vi.fn(() =>
|
|
82
|
+
const commitZ = vi.fn(async () => ({ durable: true, changed: true }));
|
|
83
83
|
const session = {
|
|
84
84
|
handleDomZIndexReorderCommit: commitZ,
|
|
85
85
|
} as unknown as Parameters<typeof DomEditProvider>[0]["value"];
|
|
@@ -107,6 +107,11 @@ describe("DOM editing package exports", () => {
|
|
|
107
107
|
await act(async () => {
|
|
108
108
|
expect(zOrder?.apply(sel, toFront)).toBe(true);
|
|
109
109
|
});
|
|
110
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("feature_used", {
|
|
111
|
+
feature: "z_order",
|
|
112
|
+
surface: "preview",
|
|
113
|
+
method: "button",
|
|
114
|
+
});
|
|
110
115
|
expect(commitZ).toHaveBeenCalledTimes(1);
|
|
111
116
|
expect(commitZ).toHaveBeenCalledWith(
|
|
112
117
|
expect.arrayContaining([expect.objectContaining({ element: back, id: "back" })]),
|
|
@@ -159,3 +164,6 @@ describe("DOM editing package exports", () => {
|
|
|
159
164
|
await act(async () => root.unmount());
|
|
160
165
|
});
|
|
161
166
|
});
|
|
167
|
+
|
|
168
|
+
vi.mock("./utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
169
|
+
import { trackStudioEvent } from "./utils/studioTelemetry";
|
|
@@ -94,7 +94,7 @@ export type DomEditCommitDeclineReason =
|
|
|
94
94
|
| "persist-failed";
|
|
95
95
|
|
|
96
96
|
export type DomEditCommitOutcome =
|
|
97
|
-
| { ok: true; persistence?: DomEditPersistOutcome }
|
|
97
|
+
| { ok: true; persistence?: DomEditPersistOutcome; changed?: boolean }
|
|
98
98
|
| { ok: false; reason: DomEditCommitDeclineReason };
|
|
99
99
|
|
|
100
100
|
export function domEditCommitDeclined(reason: DomEditCommitDeclineReason): DomEditCommitOutcome {
|
|
@@ -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
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DomEditPersistOutcome } from "./domEditCommitTypes";
|
|
1
2
|
import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
|
|
2
3
|
import {
|
|
3
4
|
applyElementPositionOffset,
|
|
@@ -23,7 +24,7 @@ export interface ElementOffsetStagerDeps {
|
|
|
23
24
|
selection: DomEditSelection,
|
|
24
25
|
patches: PatchOperation[],
|
|
25
26
|
options: { label: string; coalesceKey: string; coalesceMs?: number },
|
|
26
|
-
) => Promise<
|
|
27
|
+
) => Promise<DomEditPersistOutcome | undefined>;
|
|
27
28
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
28
29
|
readOnlyPreview?: boolean;
|
|
29
30
|
}
|
|
@@ -59,7 +60,7 @@ function stagePlainTranslate(
|
|
|
59
60
|
selection: DomEditSelection,
|
|
60
61
|
next: { x: number; y: number },
|
|
61
62
|
coalesceKey?: string,
|
|
62
|
-
): { save: () => Promise<
|
|
63
|
+
): { save: () => Promise<DomEditPersistOutcome | undefined>; rollback: () => void } {
|
|
63
64
|
const el = selection.element;
|
|
64
65
|
const before = captureStudioPathOffset(el);
|
|
65
66
|
const patches = writePlainMove(el, next);
|
|
@@ -85,9 +86,9 @@ export function stageElementOffset(
|
|
|
85
86
|
next: { x: number; y: number },
|
|
86
87
|
plainTranslate: boolean,
|
|
87
88
|
coalesceKey?: string,
|
|
88
|
-
): { save: () => Promise<
|
|
89
|
+
): { save: () => Promise<DomEditPersistOutcome | undefined>; rollback: () => void } {
|
|
89
90
|
const el = selection.element;
|
|
90
|
-
if (readOnlyPreview) return { save: () => Promise.resolve(), rollback: () => undefined };
|
|
91
|
+
if (readOnlyPreview) return { save: () => Promise.resolve(undefined), rollback: () => undefined };
|
|
91
92
|
if (plainTranslate) {
|
|
92
93
|
refuseGsapTakeover(el, showToast);
|
|
93
94
|
return stagePlainTranslate(commitPositionPatchToHtml, selection, next, coalesceKey);
|