@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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { usePlayerStore, type TimelineElement } from "../player/store/playerStore";
|
|
4
|
+
import { refreshTimelineRowText } from "./refreshTimelineRowText";
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
usePlayerStore.getState().reset();
|
|
8
|
+
document.body.innerHTML = "";
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const row = (key: string, selector: string, sourceFile?: string): TimelineElement => ({
|
|
12
|
+
id: key.split("#").pop()!,
|
|
13
|
+
key,
|
|
14
|
+
tag: "p",
|
|
15
|
+
start: 0,
|
|
16
|
+
duration: 2,
|
|
17
|
+
track: 0,
|
|
18
|
+
selector,
|
|
19
|
+
selectorIndex: 0,
|
|
20
|
+
sourceFile,
|
|
21
|
+
text: { value: "Old" },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("refreshTimelineRowText", () => {
|
|
25
|
+
it("gives the edited layer's row, addressed by its store key, its new words", () => {
|
|
26
|
+
document.body.innerHTML = `<h1 id="title">New words</h1>`;
|
|
27
|
+
usePlayerStore.getState().setElements([row("index.html#title", "#title")]);
|
|
28
|
+
|
|
29
|
+
refreshTimelineRowText(document.getElementById("title")!);
|
|
30
|
+
|
|
31
|
+
expect(usePlayerStore.getState().elements[0]?.text?.value).toBe("New words");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("finds a row known only by its data-hf-id", () => {
|
|
35
|
+
document.body.innerHTML = `<p data-hf-id="caption-a">New</p>`;
|
|
36
|
+
usePlayerStore
|
|
37
|
+
.getState()
|
|
38
|
+
.setElements([
|
|
39
|
+
{ ...row("index.html#caption-a", ""), selector: undefined, hfId: "caption-a" },
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
refreshTimelineRowText(document.querySelector("p")!);
|
|
43
|
+
|
|
44
|
+
expect(usePlayerStore.getState().elements[0]?.text?.value).toBe("New");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("finds the row in the edited layer's own composition when two share a selector", () => {
|
|
48
|
+
document.body.innerHTML = `
|
|
49
|
+
<div data-composition-id="a" data-composition-file="a.html"><p class="caption">A</p></div>
|
|
50
|
+
<div data-composition-id="b" data-composition-file="b.html"><p class="caption">B new</p></div>`;
|
|
51
|
+
usePlayerStore
|
|
52
|
+
.getState()
|
|
53
|
+
.setElements([
|
|
54
|
+
row("a.html#a-caption", ".caption", "a.html"),
|
|
55
|
+
row("b.html#b-caption", ".caption", "b.html"),
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
refreshTimelineRowText(document.querySelectorAll<HTMLElement>(".caption")[1]!);
|
|
59
|
+
|
|
60
|
+
expect(usePlayerStore.getState().elements.map((element) => element.text?.value)).toEqual([
|
|
61
|
+
"Old",
|
|
62
|
+
"B new",
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTimelineElementIdentity,
|
|
3
|
+
getTimelineElementSelector,
|
|
4
|
+
getTimelineElementSelectorIndex,
|
|
5
|
+
getTimelineElementSourceFile,
|
|
6
|
+
} from "../player/lib/timelineElementHelpers";
|
|
7
|
+
import { readTimelineText } from "../player/lib/timelineText";
|
|
8
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
9
|
+
|
|
10
|
+
/** A text commit skips the preview reload, so the edited layer's row re-reads its words here. */
|
|
11
|
+
export function refreshTimelineRowText(el: HTMLElement): void {
|
|
12
|
+
const sourceFile = getTimelineElementSourceFile(el);
|
|
13
|
+
const hfId = el.getAttribute("data-hf-id");
|
|
14
|
+
const selector = getTimelineElementSelector(el);
|
|
15
|
+
const selectorIndex = selector
|
|
16
|
+
? (getTimelineElementSelectorIndex(el.ownerDocument, el, selector) ?? 0)
|
|
17
|
+
: 0;
|
|
18
|
+
const { elements, updateElement } = usePlayerStore.getState();
|
|
19
|
+
const row = elements.find(
|
|
20
|
+
(candidate) =>
|
|
21
|
+
candidate.sourceFile === sourceFile &&
|
|
22
|
+
(hfId
|
|
23
|
+
? candidate.hfId === hfId
|
|
24
|
+
: !!selector &&
|
|
25
|
+
candidate.selector === selector &&
|
|
26
|
+
(candidate.selectorIndex ?? 0) === selectorIndex),
|
|
27
|
+
);
|
|
28
|
+
if (row) updateElement(getTimelineElementIdentity(row), { text: readTimelineText(el) });
|
|
29
|
+
}
|
|
@@ -593,6 +593,7 @@ describe("a timing edit's soft reload restores what GSAP wrote from the files",
|
|
|
593
593
|
|
|
594
594
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
595
595
|
expect.stringContaining("/files/compositions%2Fsub.html"),
|
|
596
|
+
{ credentials: "omit" },
|
|
596
597
|
);
|
|
597
598
|
expect(reloadPreview).not.toHaveBeenCalled();
|
|
598
599
|
expect(wt.getAttribute("style")).toBe("left: 700px;");
|
|
@@ -28,12 +28,13 @@ import {
|
|
|
28
28
|
serializeStudioFileMutations,
|
|
29
29
|
type StudioProjectFileWriter,
|
|
30
30
|
} from "../utils/studioFileMutationCoordinator";
|
|
31
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
31
32
|
|
|
32
33
|
export async function readFileContent(projectId: string, targetPath: string): Promise<string> {
|
|
33
34
|
if (targetPath.includes("\0") || targetPath.includes("..")) {
|
|
34
35
|
throw new Error(`Unsafe path: ${targetPath}`);
|
|
35
36
|
}
|
|
36
|
-
const response = await
|
|
37
|
+
const response = await studioApiFetch(
|
|
37
38
|
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(targetPath)}`,
|
|
38
39
|
);
|
|
39
40
|
if (!response.ok) {
|
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import type
|
|
5
|
+
import { parseGsapScript, type GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
6
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
7
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
8
8
|
import { useAnimatedPropertyCommit } from "./useAnimatedPropertyCommit";
|
|
9
9
|
import { mountReactHarness, withInlineLayoutBox } from "./domSelectionTestHarness";
|
|
10
10
|
import { writeSizeWithCrop } from "../components/editor/cropResize";
|
|
11
|
-
import
|
|
11
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
12
|
+
import { trackKeyframeCommit } from "../utils/keyframeUsage";
|
|
13
|
+
import type { CommitMutationOptions, CommitMutation } from "./gsapScriptCommitTypes";
|
|
14
|
+
|
|
15
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
12
16
|
|
|
13
17
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
18
|
|
|
15
19
|
afterEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
16
21
|
document.body.innerHTML = "";
|
|
17
22
|
usePlayerStore.setState({ autoKeyframeEnabled: true, currentTime: 0 });
|
|
18
23
|
});
|
|
@@ -43,7 +48,11 @@ type Commit = (
|
|
|
43
48
|
/** Renders the hook and hands its commit function to the caller via a ref callback. */
|
|
44
49
|
function renderHookWith(
|
|
45
50
|
animations: GsapAnimation[],
|
|
46
|
-
onMutation: (
|
|
51
|
+
onMutation: (
|
|
52
|
+
mutation: Record<string, unknown>,
|
|
53
|
+
label: string,
|
|
54
|
+
options: CommitMutationOptions,
|
|
55
|
+
) => unknown | Promise<unknown>,
|
|
47
56
|
onReady: (commit: Commit) => void,
|
|
48
57
|
bumpGsapCache = vi.fn(),
|
|
49
58
|
onBatch?: (
|
|
@@ -56,9 +65,9 @@ function renderHookWith(
|
|
|
56
65
|
async (
|
|
57
66
|
_sel: DomEditSelection,
|
|
58
67
|
mutation: Record<string, unknown>,
|
|
59
|
-
options:
|
|
68
|
+
options: CommitMutationOptions,
|
|
60
69
|
) => {
|
|
61
|
-
await onMutation(mutation, options.label);
|
|
70
|
+
await onMutation(mutation, options.label, options);
|
|
62
71
|
},
|
|
63
72
|
onBatch
|
|
64
73
|
? {
|
|
@@ -401,3 +410,35 @@ describe("useAnimatedPropertyCommit — a size write and its crop are one undo s
|
|
|
401
410
|
act(() => root.unmount());
|
|
402
411
|
});
|
|
403
412
|
});
|
|
413
|
+
|
|
414
|
+
it("counts one add when an inspector edit first converts a flat animation", async () => {
|
|
415
|
+
usePlayerStore.setState({ autoKeyframeEnabled: true, currentTime: 1 });
|
|
416
|
+
const flat = parseGsapScript(
|
|
417
|
+
'const tl = gsap.timeline(); tl.to("#box", { rotationX: 0, duration: 2 }, 0);',
|
|
418
|
+
).animations[0]!;
|
|
419
|
+
const mutations: Record<string, unknown>[] = [];
|
|
420
|
+
let commit!: Commit;
|
|
421
|
+
const root = renderHookWith(
|
|
422
|
+
[flat, keyframedAnim],
|
|
423
|
+
(mutation, _label, options) => {
|
|
424
|
+
mutations.push(mutation);
|
|
425
|
+
trackKeyframeCommit([mutation], { ok: true, changed: true }, options);
|
|
426
|
+
},
|
|
427
|
+
(value) => {
|
|
428
|
+
commit = value;
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
try {
|
|
432
|
+
await act(async () => {
|
|
433
|
+
await commit(selection, { rotationX: 20 });
|
|
434
|
+
});
|
|
435
|
+
expect(mutations.map((mutation) => mutation.type)).toEqual([
|
|
436
|
+
"convert-to-keyframes",
|
|
437
|
+
"add-keyframe",
|
|
438
|
+
]);
|
|
439
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(1);
|
|
440
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("keyframe", { action: "add" });
|
|
441
|
+
} finally {
|
|
442
|
+
act(() => root.unmount());
|
|
443
|
+
}
|
|
444
|
+
});
|
|
@@ -333,7 +333,7 @@ async function commitKeyframeProps(
|
|
|
333
333
|
await commit(
|
|
334
334
|
selection,
|
|
335
335
|
{ type: "convert-to-keyframes", animationId: anim.id },
|
|
336
|
-
{ label: "Convert to keyframes", skipReload: true },
|
|
336
|
+
{ label: "Convert to keyframes", keyframeTelemetry: false, skipReload: true },
|
|
337
337
|
);
|
|
338
338
|
}
|
|
339
339
|
const ct = usePlayerStore.getState().currentTime;
|
|
@@ -389,7 +389,7 @@ async function commitKeyframeProps(
|
|
|
389
389
|
duration: roundTo3(newDuration),
|
|
390
390
|
keyframes: remapped,
|
|
391
391
|
},
|
|
392
|
-
{ label: `Edit ${primaryProp} (extended keyframe)`, softReload: true },
|
|
392
|
+
{ label: `Edit ${primaryProp} (extended keyframe)`, keyframeAction: "add", softReload: true },
|
|
393
393
|
);
|
|
394
394
|
return;
|
|
395
395
|
}
|
|
@@ -7,6 +7,7 @@ import { toProjectAbsolutePath, type AgentModalAnchorPoint } from "../utils/stud
|
|
|
7
7
|
import { liveMarkupWithoutPreviewMarks } from "../utils/authoredSource";
|
|
8
8
|
import { buildElementAgentPrompt, type DomEditSelection } from "../components/editor/domEditing";
|
|
9
9
|
import { usePlayerStore } from "../player";
|
|
10
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
10
11
|
|
|
11
12
|
// ── Types ──
|
|
12
13
|
|
|
@@ -55,7 +56,7 @@ export function useAskAgentModal({
|
|
|
55
56
|
|
|
56
57
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
57
58
|
try {
|
|
58
|
-
const response = await
|
|
59
|
+
const response = await studioApiFetch(
|
|
59
60
|
buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`),
|
|
60
61
|
);
|
|
61
62
|
if (!response.ok) return;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type BlockCategory,
|
|
6
6
|
resolveBlockCategory,
|
|
7
7
|
} from "../utils/blockCategories";
|
|
8
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
8
9
|
|
|
9
10
|
type CatalogItem = RegistryItem & {
|
|
10
11
|
category: BlockCategory;
|
|
@@ -14,7 +15,7 @@ let catalogCache: CatalogItem[] | null = null;
|
|
|
14
15
|
let catalogRequest: Promise<CatalogItem[]> | null = null;
|
|
15
16
|
|
|
16
17
|
function loadCatalog(): Promise<CatalogItem[]> {
|
|
17
|
-
catalogRequest ??=
|
|
18
|
+
catalogRequest ??= studioApiFetch("/api/registry/blocks")
|
|
18
19
|
.then((response) => {
|
|
19
20
|
if (!response.ok) throw new Error("Failed to load catalog");
|
|
20
21
|
return response.json() as Promise<RegistryItem[]>;
|
|
@@ -8,19 +8,28 @@
|
|
|
8
8
|
|
|
9
9
|
import { useCallback, useRef } from "react";
|
|
10
10
|
import type { TimelineElement } from "../player";
|
|
11
|
+
import {
|
|
12
|
+
MAX_HAND_EDIT_CLIPS,
|
|
13
|
+
type BlockedTimelineEditIntent,
|
|
14
|
+
} from "../player/components/timelineEditing";
|
|
11
15
|
|
|
12
16
|
const BLOCKED_TOAST_INTERVAL_MS = 1500;
|
|
13
17
|
|
|
14
18
|
export function useBlockedTimelineEditToast(
|
|
15
19
|
showToast: (message: string, tone?: "info" | "error") => void,
|
|
16
|
-
): (element: TimelineElement) => void {
|
|
20
|
+
): (element: TimelineElement, intent: BlockedTimelineEditIntent) => void {
|
|
17
21
|
const lastAtRef = useRef(0);
|
|
18
22
|
return useCallback(
|
|
19
|
-
(_element: TimelineElement) => {
|
|
23
|
+
(_element: TimelineElement, intent: BlockedTimelineEditIntent) => {
|
|
20
24
|
const now = Date.now();
|
|
21
25
|
if (now - lastAtRef.current < BLOCKED_TOAST_INTERVAL_MS) return;
|
|
22
26
|
lastAtRef.current = now;
|
|
23
|
-
showToast(
|
|
27
|
+
showToast(
|
|
28
|
+
intent === "edit-many"
|
|
29
|
+
? `Select ${MAX_HAND_EDIT_CLIPS} or fewer clips to move or resize by hand, or describe the change.`
|
|
30
|
+
: "This clip can't be moved or resized from the timeline yet.",
|
|
31
|
+
"info",
|
|
32
|
+
);
|
|
24
33
|
},
|
|
25
34
|
[showToast],
|
|
26
35
|
);
|
|
@@ -5,6 +5,7 @@ import { useCaptionStore } from "../captions/store";
|
|
|
5
5
|
import { acceptStudioRuntimeMessage } from "../player/lib/runtimeProtocol";
|
|
6
6
|
import { useCaptionSync } from "../captions/hooks/useCaptionSync";
|
|
7
7
|
import { parseCaptionComposition } from "../captions/parser";
|
|
8
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
8
9
|
|
|
9
10
|
interface UseCaptionDetectionParams {
|
|
10
11
|
projectId: string | null;
|
|
@@ -110,7 +111,7 @@ export function useCaptionDetection({
|
|
|
110
111
|
|
|
111
112
|
activating = true;
|
|
112
113
|
const srcPath = captionSrcPath;
|
|
113
|
-
|
|
114
|
+
studioApiFetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`))
|
|
114
115
|
.then((r) => r.json())
|
|
115
116
|
.then((data: { content?: string }) => {
|
|
116
117
|
if (!data.content || !doc || !win || useCaptionStore.getState().isEditMode) return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
2
2
|
import { useCallback } from "react";
|
|
3
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Loads a composition file's content for the source editor when a composition
|
|
@@ -24,7 +25,7 @@ export function useCompositionContentLoader({
|
|
|
24
25
|
if (!projectId) return;
|
|
25
26
|
setActiveCompPath(comp.endsWith(".html") ? comp : null);
|
|
26
27
|
setEditingFile({ path: comp, content: null });
|
|
27
|
-
|
|
28
|
+
studioApiFetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`))
|
|
28
29
|
.then(async (r) => {
|
|
29
30
|
if (!r.ok) throw new Error(`Failed to load ${comp} (${r.status})`);
|
|
30
31
|
return r.json();
|
|
@@ -7,6 +7,7 @@ import { buildProjectApiPath } from "../utils/projectRouting";
|
|
|
7
7
|
import type { DomEditPatchBatch } from "./domEditCommitTypes";
|
|
8
8
|
import { formatFieldsSuffix } from "./gsapScriptCommitHelpers";
|
|
9
9
|
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
|
10
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
10
11
|
|
|
11
12
|
export function formatUnsafeFieldList(fields: Array<{ path: string }>): string {
|
|
12
13
|
return fields.map((field) => field.path).join(", ");
|
|
@@ -99,7 +100,7 @@ function isAtomicElementPatchFile(value: unknown): value is AtomicElementPatchFi
|
|
|
99
100
|
export async function patchElementBatches(projectId: string, batches: DomEditPatchBatch[]) {
|
|
100
101
|
const body = JSON.stringify({ batches });
|
|
101
102
|
try {
|
|
102
|
-
const response = await
|
|
103
|
+
const response = await studioApiFetch(
|
|
103
104
|
`/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
|
|
104
105
|
{
|
|
105
106
|
method: "POST",
|
|
@@ -178,7 +179,7 @@ export async function postPatchElement(
|
|
|
178
179
|
body: unknown,
|
|
179
180
|
showToast: ShowToast,
|
|
180
181
|
): Promise<PatchElementResponse> {
|
|
181
|
-
const response = await
|
|
182
|
+
const response = await studioApiFetch(
|
|
182
183
|
buildProjectApiPath(
|
|
183
184
|
projectId,
|
|
184
185
|
`/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
|
|
@@ -33,30 +33,25 @@ export function useDomEditPositionPatchCommit({
|
|
|
33
33
|
coalesceMs: options.coalesceMs,
|
|
34
34
|
skipRefresh: options.skipRefresh ?? true,
|
|
35
35
|
deferRender: options.deferRender,
|
|
36
|
-
})
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
mutationType: "position",
|
|
53
|
-
label: options.label,
|
|
54
|
-
targetId: selection.id,
|
|
55
|
-
targetSelector: selection.selector,
|
|
56
|
-
targetSourceFile: selection.sourceFile,
|
|
57
|
-
});
|
|
58
|
-
throw error;
|
|
36
|
+
}).catch((error) => {
|
|
37
|
+
// The paused-save banner already explains this refusal. Rethrow so the
|
|
38
|
+
// caller reverts the preview instead of treating an unsaved edit as committed.
|
|
39
|
+
if (error instanceof DomEditSaveQueueOpenError) throw error;
|
|
40
|
+
if (!wasAlreadyToasted(error)) {
|
|
41
|
+
showToast(error instanceof Error ? error.message : "Failed to save position");
|
|
42
|
+
}
|
|
43
|
+
trackStudioSaveFailure({
|
|
44
|
+
source: "dom_edit",
|
|
45
|
+
error,
|
|
46
|
+
filePath: selection.sourceFile ?? activeCompPath ?? "index.html",
|
|
47
|
+
mutationType: "position",
|
|
48
|
+
label: options.label,
|
|
49
|
+
targetId: selection.id,
|
|
50
|
+
targetSelector: selection.selector,
|
|
51
|
+
targetSourceFile: selection.sourceFile,
|
|
59
52
|
});
|
|
53
|
+
throw error;
|
|
54
|
+
});
|
|
60
55
|
},
|
|
61
56
|
[activeCompPath, persistDomEditOperations, showToast],
|
|
62
57
|
);
|
|
@@ -10,6 +10,7 @@ import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
|
10
10
|
import type { TimelineElement } from "../player";
|
|
11
11
|
import type { UseDomEditSessionParams } from "./useDomEditSession";
|
|
12
12
|
import { makeSelection } from "./domSelectionTestHarness";
|
|
13
|
+
import { readTranslatePx } from "../components/editor/plainTranslate";
|
|
13
14
|
|
|
14
15
|
const styleOp = (property: string, value: string): PatchOperation => ({
|
|
15
16
|
type: "inline-style",
|
|
@@ -66,6 +67,7 @@ const groupSelectionSpy = vi.fn();
|
|
|
66
67
|
const gsapCommitMutation = Object.assign(vi.fn(), { batch: vi.fn() });
|
|
67
68
|
const neverParsed = new Promise<{ animations: GsapAnimation[] }>(() => undefined);
|
|
68
69
|
const parsedFile = { current: neverParsed };
|
|
70
|
+
const groupCommitSpy = vi.fn(async () => {});
|
|
69
71
|
|
|
70
72
|
function createSessionParams(
|
|
71
73
|
overrides: Partial<UseDomEditSessionParams> = {},
|
|
@@ -256,7 +258,7 @@ vi.mock("./usePreviewInteraction", () => ({
|
|
|
256
258
|
vi.mock("./useGsapAwareEditing", () => ({
|
|
257
259
|
useGsapAwareEditing: () => ({
|
|
258
260
|
handleGsapAwarePathOffsetCommit: vi.fn(),
|
|
259
|
-
handleGsapAwareGroupPathOffsetCommit:
|
|
261
|
+
handleGsapAwareGroupPathOffsetCommit: groupCommitSpy,
|
|
260
262
|
handleGsapAwareBoxSizeCommit: vi.fn(),
|
|
261
263
|
handleGsapAwareRotationCommit: vi.fn(),
|
|
262
264
|
commitAnimatedProperty: vi.fn(),
|
|
@@ -605,3 +607,30 @@ describe("a shadow reload promoted without a host re-render", () => {
|
|
|
605
607
|
act(() => root.unmount());
|
|
606
608
|
});
|
|
607
609
|
});
|
|
610
|
+
|
|
611
|
+
describe("handleDomGroupMoveBy", () => {
|
|
612
|
+
it("saves through the group drag's commit, leaving a refusal for the host to explain", async () => {
|
|
613
|
+
const { useDomEditSession } = await import("./useDomEditSession");
|
|
614
|
+
const element = document.createElement("div");
|
|
615
|
+
document.body.append(element);
|
|
616
|
+
element.getBoundingClientRect = () => {
|
|
617
|
+
const { x, y } = readTranslatePx(element);
|
|
618
|
+
return new DOMRect(x, y, 40, 20);
|
|
619
|
+
};
|
|
620
|
+
const selection = makeSelection("card", element);
|
|
621
|
+
let moveBy!: ReturnType<typeof useDomEditSession>["handleDomGroupMoveBy"];
|
|
622
|
+
function Probe() {
|
|
623
|
+
moveBy = useDomEditSession(createSessionParams()).handleDomGroupMoveBy;
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
const root = createRoot(document.createElement("div"));
|
|
627
|
+
act(() => root.render(<Probe />));
|
|
628
|
+
await act(() => moveBy([{ selection, delta: { x: 10, y: 0 } }]));
|
|
629
|
+
expect(groupCommitSpy).toHaveBeenCalledWith(
|
|
630
|
+
[{ selection, next: { x: 10, y: 0 }, plainTranslate: true }],
|
|
631
|
+
{ refusalToast: false },
|
|
632
|
+
);
|
|
633
|
+
act(() => root.unmount());
|
|
634
|
+
element.remove();
|
|
635
|
+
});
|
|
636
|
+
});
|
|
@@ -25,6 +25,7 @@ import { membersForDelete, timelineElementsForDelete } from "./domEditDeleteMemb
|
|
|
25
25
|
import type { RecordEditInput } from "./domEditDeleteMembers";
|
|
26
26
|
import type { DomEditTimelineParams } from "./useDomSelectionTypes";
|
|
27
27
|
import { useLivePreviewIframe } from "./useLivePreviewIframe";
|
|
28
|
+
import { moveDomGroupBy, type DomGroupMove } from "../components/editor/domGroupMoveBy";
|
|
28
29
|
// Re-exported: the delete rule lives in its own module; callers and its test import it from here.
|
|
29
30
|
export { membersForDelete };
|
|
30
31
|
|
|
@@ -445,6 +446,7 @@ export function useDomEditSession({
|
|
|
445
446
|
domEditSelection,
|
|
446
447
|
selectedGsapAnimations,
|
|
447
448
|
gsapCommitMutation,
|
|
449
|
+
activeCompPath,
|
|
448
450
|
previewIframeRef,
|
|
449
451
|
showToast,
|
|
450
452
|
bumpGsapCache,
|
|
@@ -459,6 +461,13 @@ export function useDomEditSession({
|
|
|
459
461
|
setArcPath,
|
|
460
462
|
updateArcSegment,
|
|
461
463
|
});
|
|
464
|
+
const handleDomGroupMoveBy = useCallback(
|
|
465
|
+
(moves: DomGroupMove[]) =>
|
|
466
|
+
moveDomGroupBy(moves, (updates) =>
|
|
467
|
+
handleGsapAwareGroupPathOffsetCommit(updates, { refusalToast: false }),
|
|
468
|
+
),
|
|
469
|
+
[handleGsapAwareGroupPathOffsetCommit],
|
|
470
|
+
);
|
|
462
471
|
const { handleUpdateSegmentEase, handleUpdateKeyframeEase, handleSetAllKeyframeEases } =
|
|
463
472
|
useKeyframeEaseCommits({ gsapCommitMutation, domEditSelectionRef });
|
|
464
473
|
const committable = useCommitPreflightCapabilities({
|
|
@@ -497,6 +506,7 @@ export function useDomEditSession({
|
|
|
497
506
|
handleDomAttributeBatchCommit,
|
|
498
507
|
handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
|
|
499
508
|
handleDomGroupPathOffsetCommit: handleGsapAwareGroupPathOffsetCommit,
|
|
509
|
+
handleDomGroupMoveBy,
|
|
500
510
|
handleDomZIndexReorderCommit,
|
|
501
511
|
handleDomBoxSizeCommit: handleGsapAwareBoxSizeCommit,
|
|
502
512
|
handleDomRotationCommit: handleGsapAwareRotationCommit,
|
|
@@ -160,6 +160,21 @@ describe("useDomEditTextCommits", () => {
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
it.each([true, false])(
|
|
164
|
+
"counts in-place text only after a changed write (%s)",
|
|
165
|
+
async (changed) => {
|
|
166
|
+
vi.mocked(trackStudioEvent).mockClear();
|
|
167
|
+
const { persist, save } = richTextProbe();
|
|
168
|
+
persist.mockResolvedValue({ changed, sourceFile: "private.html", version: "v2" });
|
|
169
|
+
await save();
|
|
170
|
+
expect(vi.mocked(trackStudioEvent).mock.calls).toEqual(
|
|
171
|
+
changed
|
|
172
|
+
? [["feature_used", { feature: "text_edit", surface: "preview", method: "field" }]]
|
|
173
|
+
: [],
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
163
178
|
it("saves in-place text while the preview is editable, and refreshes the selection it edited", async () => {
|
|
164
179
|
const applyDomSelection = vi.fn();
|
|
165
180
|
const { persist, element, save } = richTextProbe(undefined, () => ({ applyDomSelection }));
|
|
@@ -335,6 +350,29 @@ describe("useDomEditTextCommits", () => {
|
|
|
335
350
|
expect(outcome).toEqual({ ok: true, persistence });
|
|
336
351
|
});
|
|
337
352
|
|
|
353
|
+
it("saves a style map on a selection as one patch, so it is one undo step", async () => {
|
|
354
|
+
const { iframe, element } = previewElement("<div id='card'>Original</div>", "card");
|
|
355
|
+
const persist = vi.fn().mockResolvedValue({ sourceFile: "index.html", changed: true });
|
|
356
|
+
const hook = renderTextCommitHook(
|
|
357
|
+
commitParams({
|
|
358
|
+
previewIframeRef: { current: iframe },
|
|
359
|
+
domEditSelection: null,
|
|
360
|
+
persistDomEditOperations: persist,
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await act(async () => {
|
|
365
|
+
await hook.handleDomStyleCommitForSelection(selectionFor(element), {
|
|
366
|
+
"border-width": "4px",
|
|
367
|
+
"border-style": "solid",
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(persist).toHaveBeenCalledTimes(1);
|
|
372
|
+
expect(element.style.borderWidth).toBe("4px");
|
|
373
|
+
expect(element.style.borderStyle).toBe("solid");
|
|
374
|
+
});
|
|
375
|
+
|
|
338
376
|
it("declines a style commit with no selection, without reaching the writer", async () => {
|
|
339
377
|
const persistDomEditOperations = vi.fn().mockResolvedValue(undefined);
|
|
340
378
|
const hook = renderTextCommitHook(
|
|
@@ -464,3 +502,6 @@ describe("useDomEditTextCommits", () => {
|
|
|
464
502
|
expect(agentElement.textContent).toBe("Edited");
|
|
465
503
|
});
|
|
466
504
|
});
|
|
505
|
+
|
|
506
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
507
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { trackPreviewFeatureUsed } from "../utils/previewFeatureUsage";
|
|
1
2
|
import { useCallback, useRef } from "react";
|
|
3
|
+
import { refreshTimelineRowText } from "./refreshTimelineRowText";
|
|
2
4
|
import { normalizeDomEditStyleValue } from "../utils/studioHelpers";
|
|
3
5
|
import { injectPreviewGoogleFont, injectPreviewImportedFont } from "../utils/studioFontHelpers";
|
|
4
6
|
import {
|
|
@@ -63,6 +65,7 @@ async function resyncDomTextSelectionFromPreview(
|
|
|
63
65
|
if (!doc) return;
|
|
64
66
|
const refreshed = findElementForSelection(doc, selection, activeCompPath);
|
|
65
67
|
if (!refreshed) return;
|
|
68
|
+
refreshTimelineRowText(refreshed);
|
|
66
69
|
const nextSelection = await buildDomSelectionFromTarget(refreshed);
|
|
67
70
|
if (!nextSelection) return;
|
|
68
71
|
applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
|
|
@@ -106,7 +109,11 @@ export function useDomEditTextCommits({
|
|
|
106
109
|
});
|
|
107
110
|
|
|
108
111
|
const handleDomStyleCommitForSelection = useCallback(
|
|
109
|
-
(
|
|
112
|
+
(
|
|
113
|
+
selection: DomEditSelection,
|
|
114
|
+
propertyOrStylesAsOnePatch: string | Record<string, string>,
|
|
115
|
+
value = "",
|
|
116
|
+
): Promise<DomEditCommitOutcome> =>
|
|
110
117
|
commitDomStyles(
|
|
111
118
|
{
|
|
112
119
|
activeCompPath,
|
|
@@ -118,7 +125,9 @@ export function useDomEditTextCommits({
|
|
|
118
125
|
resync: refreshDomEditSelectionFromPreview,
|
|
119
126
|
},
|
|
120
127
|
selection,
|
|
121
|
-
|
|
128
|
+
typeof propertyOrStylesAsOnePatch === "string"
|
|
129
|
+
? { [propertyOrStylesAsOnePatch]: value }
|
|
130
|
+
: propertyOrStylesAsOnePatch,
|
|
122
131
|
),
|
|
123
132
|
[
|
|
124
133
|
activeCompPath,
|
|
@@ -272,11 +281,12 @@ export function useDomEditTextCommits({
|
|
|
272
281
|
appliedHtml = element.innerHTML;
|
|
273
282
|
},
|
|
274
283
|
persist: async () => {
|
|
275
|
-
await persistDomEditOperations(selection, operations, {
|
|
284
|
+
const result = await persistDomEditOperations(selection, operations, {
|
|
276
285
|
label: "Edit text",
|
|
277
286
|
skipRefresh: true,
|
|
278
287
|
shouldSave: isLatestTextCommit,
|
|
279
288
|
});
|
|
289
|
+
if (result?.changed) trackPreviewFeatureUsed("text_edit", "field");
|
|
280
290
|
},
|
|
281
291
|
shouldRevert: () => isLatestTextCommit(),
|
|
282
292
|
revert: () => {
|
|
@@ -104,8 +104,10 @@ export interface UseDomEditWiringParams {
|
|
|
104
104
|
animId: string,
|
|
105
105
|
resolvedFromValues?: Record<string, number | string>,
|
|
106
106
|
) => Promise<void>;
|
|
107
|
-
removeAllKeyframes:
|
|
108
|
-
handleDomManualEditsReset: (
|
|
107
|
+
removeAllKeyframes: Parameters<typeof useGsapSelectionHandlers>[0]["removeAllKeyframes"];
|
|
108
|
+
handleDomManualEditsReset: (
|
|
109
|
+
sel: DomEditSelection,
|
|
110
|
+
) => Promise<void | import("./domEditCommitTypes").DomEditPersistOutcome>;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
// fallow-ignore-next-line complexity
|