@hyperframes/studio 0.8.114 → 0.8.115
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-C-9N8nk0.js} +1 -1
- package/dist/assets/{index-Bn2RJNTJ.js → index-BoUM6Xqb.js} +1 -1
- package/dist/assets/{index-Dz7doz9l.js → index-CAvZ0AA-.js} +201 -201
- package/dist/assets/{index-C9N-DGjC.js → index-CVuorIMv.js} +1 -1
- package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
- 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/{chunk-BIUXP3PG.js → chunk-TXZMMOMA.js} +43 -276
- package/dist/chunk-TXZMMOMA.js.map +1 -0
- package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-D7VYFJJD.js} +3 -5
- package/dist/index.d.ts +43 -19
- package/dist/index.html +2 -2
- package/dist/index.js +2939 -2596
- 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/components/LinkedSelectionToggle.tsx +3 -2
- 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/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +7 -0
- 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/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +39 -15
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- 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/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/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +1 -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/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.ts +2 -20
- package/src/hooks/gsapDragPositionCommit.ts +18 -3
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- package/src/hooks/gsapResizeIntercept.test.ts +33 -0
- package/src/hooks/gsapResizeIntercept.ts +1 -0
- package/src/hooks/gsapScriptCommitTypes.ts +5 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +20 -0
- package/src/hooks/keyframeCacheAstLoad.ts +6 -5
- 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/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditTextCommits.test.tsx +18 -0
- package/src/hooks/useDomEditTextCommits.ts +5 -1
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +7 -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/useEnableKeyframes.test.ts +2 -2
- package/src/hooks/useEnableKeyframes.ts +5 -3
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +102 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +31 -6
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +86 -55
- package/src/hooks/useGsapAwareEditing.test.tsx +11 -6
- package/src/hooks/useGsapAwareEditing.ts +96 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +57 -0
- package/src/hooks/useGsapScriptCommits.ts +3 -0
- 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/usePopulateKeyframeCacheForFile.test.tsx +76 -2
- package/src/hooks/usePreviewInteraction.test.ts +21 -0
- package/src/hooks/usePreviewInteraction.ts +2 -0
- package/src/hooks/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +21 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- 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/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/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- 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/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/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- 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/studioTelemetry.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +49 -4
- package/src/webmcp/StudioAgentTools.tsx +2 -2
- 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-D7VYFJJD.js.map} +0 -0
|
@@ -81,9 +81,9 @@ async function requestParsedAnimations(
|
|
|
81
81
|
try {
|
|
82
82
|
const res = await fetch(
|
|
83
83
|
`/api/projects/${encodeURIComponent(projectId)}/gsap-animations/${encodeURIComponent(sourceFile)}`,
|
|
84
|
-
// Always
|
|
85
|
-
//
|
|
86
|
-
{ cache: "no-
|
|
84
|
+
// Always revalidate; an unchanged file answers 304. No per-call timestamp
|
|
85
|
+
// (a deterministic-render no-no).
|
|
86
|
+
{ cache: "no-cache" },
|
|
87
87
|
);
|
|
88
88
|
if (!res.ok) return null;
|
|
89
89
|
const parsed: unknown = await res.json();
|
|
@@ -106,9 +106,9 @@ export async function populateKeyframeCacheFromAst(
|
|
|
106
106
|
projectId: string,
|
|
107
107
|
sf: string,
|
|
108
108
|
doc: Document | null | undefined,
|
|
109
|
-
): Promise<
|
|
109
|
+
): Promise<boolean> {
|
|
110
110
|
const parsed = await fetchParsedAnimations(projectId, sf);
|
|
111
|
-
if (!parsed) return;
|
|
111
|
+
if (!parsed) return false;
|
|
112
112
|
const { elements, domClipChildren } = usePlayerStore.getState();
|
|
113
113
|
const mergedByElement = new Map<string, GsapKeyframesData<MergeableKeyframe>>();
|
|
114
114
|
const sourceByElement = new Map<string, GsapAnimation[]>();
|
|
@@ -136,4 +136,5 @@ export async function populateKeyframeCacheFromAst(
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
replaceKeyframeCacheForFile(sf, mergedByElement, sourceByElement);
|
|
139
|
+
return true;
|
|
139
140
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DomEditPersistOutcome } from "./domEditCommitTypes";
|
|
1
2
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
2
3
|
import { captureStudioRotation, clearStudioRotation } from "../components/editor/manualEdits";
|
|
3
4
|
import { buildClearRotationPatches } from "../components/editor/manualEditsDomPatches";
|
|
@@ -17,8 +18,8 @@ export function savePlainRotation(
|
|
|
17
18
|
{ commitPositionPatchToHtml, readOnlyPreview }: Omit<ElementOffsetStagerDeps, "showToast">,
|
|
18
19
|
selection: DomEditSelection,
|
|
19
20
|
next: RotationCommit,
|
|
20
|
-
): Promise<
|
|
21
|
-
if (readOnlyPreview) return Promise.resolve();
|
|
21
|
+
): Promise<DomEditPersistOutcome | undefined> {
|
|
22
|
+
if (readOnlyPreview) return Promise.resolve(undefined);
|
|
22
23
|
const { element } = selection;
|
|
23
24
|
const before = captureStudioRotation(element);
|
|
24
25
|
// Legacy marks go in the same write, or a seek puts their angle back; clearing them voids the press read.
|
|
@@ -101,7 +101,7 @@ describe("a shadow reload promoted without a host re-render", () => {
|
|
|
101
101
|
});
|
|
102
102
|
expect(overlayNode).toBe(selection.element);
|
|
103
103
|
|
|
104
|
-
const commitPositionPatchToHtml = vi.fn(async () =>
|
|
104
|
+
const commitPositionPatchToHtml = vi.fn(async () => undefined);
|
|
105
105
|
await savePlainRotation({ commitPositionPatchToHtml }, selection, { angle: 30 });
|
|
106
106
|
await stageElementOffset(
|
|
107
107
|
{ commitPositionPatchToHtml, showToast: vi.fn() },
|
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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
|
);
|
|
@@ -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
|
);
|
|
@@ -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 }));
|
|
@@ -464,3 +479,6 @@ describe("useDomEditTextCommits", () => {
|
|
|
464
479
|
expect(agentElement.textContent).toBe("Edited");
|
|
465
480
|
});
|
|
466
481
|
});
|
|
482
|
+
|
|
483
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
484
|
+
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 });
|
|
@@ -272,11 +275,12 @@ export function useDomEditTextCommits({
|
|
|
272
275
|
appliedHtml = element.innerHTML;
|
|
273
276
|
},
|
|
274
277
|
persist: async () => {
|
|
275
|
-
await persistDomEditOperations(selection, operations, {
|
|
278
|
+
const result = await persistDomEditOperations(selection, operations, {
|
|
276
279
|
label: "Edit text",
|
|
277
280
|
skipRefresh: true,
|
|
278
281
|
shouldSave: isLatestTextCommit,
|
|
279
282
|
});
|
|
283
|
+
if (result?.changed) trackPreviewFeatureUsed("text_edit", "field");
|
|
280
284
|
},
|
|
281
285
|
shouldRevert: () => isLatestTextCommit(),
|
|
282
286
|
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
|
|
@@ -127,7 +127,7 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
127
127
|
y: 20,
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
expect(outcome).toEqual({ ok: true });
|
|
130
|
+
expect(outcome).toEqual({ ok: true, changed: true });
|
|
131
131
|
expect(mutations).toEqual([
|
|
132
132
|
expect.objectContaining({ type: "add", targetSelector: "#card", method: "set" }),
|
|
133
133
|
]);
|
|
@@ -146,7 +146,14 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
146
146
|
const url = String(input instanceof Request ? input.url : input);
|
|
147
147
|
calls.urls.push(url);
|
|
148
148
|
if (url.includes("/patch-element/")) calls.patches.push(JSON.parse(String(init?.body)));
|
|
149
|
-
const saved = {
|
|
149
|
+
const saved = {
|
|
150
|
+
ok: true,
|
|
151
|
+
changed: true,
|
|
152
|
+
matched: true,
|
|
153
|
+
path: "index.html",
|
|
154
|
+
content: "AFTER",
|
|
155
|
+
version: "v2",
|
|
156
|
+
};
|
|
150
157
|
const body = url.includes("/files/") ? { content: SOURCE } : saved;
|
|
151
158
|
const status = url.includes("/patch-element/") ? patchStatus : 200;
|
|
152
159
|
return new Response(JSON.stringify(body), {
|
|
@@ -163,7 +170,10 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
163
170
|
const { element, recordEdit, hook, unmount } = renderHost();
|
|
164
171
|
|
|
165
172
|
const card = makeSelection("card", element);
|
|
166
|
-
await expect(hook().commitPathOffset(card, { x: 130.5, y: 90 })).resolves.toEqual({
|
|
173
|
+
await expect(hook().commitPathOffset(card, { x: 130.5, y: 90 })).resolves.toEqual({
|
|
174
|
+
ok: true,
|
|
175
|
+
changed: true,
|
|
176
|
+
});
|
|
167
177
|
|
|
168
178
|
expect(element.style.getPropertyValue("translate")).toBe("130.5px 90px");
|
|
169
179
|
expect(calls.patches).toEqual([
|
|
@@ -185,7 +195,7 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
185
195
|
|
|
186
196
|
await expect(
|
|
187
197
|
hook().commitBoxSize(makeSelection("card", element), { width: 300, height: 90 }),
|
|
188
|
-
).resolves.toEqual({ ok: true });
|
|
198
|
+
).resolves.toEqual({ ok: true, changed: true });
|
|
189
199
|
|
|
190
200
|
expect(element.style.getPropertyValue("width")).toBe("300px");
|
|
191
201
|
expect(calls.patches).toEqual([
|
|
@@ -247,7 +257,7 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
247
257
|
{ x: 1, y: 2 },
|
|
248
258
|
{ plainTranslate: true },
|
|
249
259
|
),
|
|
250
|
-
).resolves.toEqual({ ok: true });
|
|
260
|
+
).resolves.toEqual({ ok: true, changed: true });
|
|
251
261
|
expect(calls.patches).toHaveLength(1);
|
|
252
262
|
unmount();
|
|
253
263
|
});
|
|
@@ -396,7 +406,14 @@ describe("useDomGeometryCommit, one word of a staggered phrase", () => {
|
|
|
396
406
|
headers: { "content-type": "application/json" },
|
|
397
407
|
});
|
|
398
408
|
}
|
|
399
|
-
return json({
|
|
409
|
+
return json({
|
|
410
|
+
ok: true,
|
|
411
|
+
changed: true,
|
|
412
|
+
matched: true,
|
|
413
|
+
path: "index.html",
|
|
414
|
+
content: "AFTER",
|
|
415
|
+
version: "v2",
|
|
416
|
+
});
|
|
400
417
|
}
|
|
401
418
|
if (url.includes("/api/projects/p1/gsap-mutations/")) {
|
|
402
419
|
calls.gsapMutations.push(JSON.parse(String(init?.body)));
|
|
@@ -426,6 +443,7 @@ describe("useDomGeometryCommit, one word of a staggered phrase", () => {
|
|
|
426
443
|
|
|
427
444
|
await expect(hook().commitPathOffset(selection, { x: 40, y: 20 })).resolves.toEqual({
|
|
428
445
|
ok: true,
|
|
446
|
+
changed: true,
|
|
429
447
|
});
|
|
430
448
|
|
|
431
449
|
expect(calls.gsapMutations).toHaveLength(0);
|
|
@@ -455,6 +473,7 @@ describe("useDomGeometryCommit, one word of a staggered phrase", () => {
|
|
|
455
473
|
]);
|
|
456
474
|
await expect(hook().commitPathOffset(selection, { x: 40, y: 20 })).resolves.toEqual({
|
|
457
475
|
ok: true,
|
|
476
|
+
changed: true,
|
|
458
477
|
});
|
|
459
478
|
|
|
460
479
|
expect(calls.patches).toHaveLength(2);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GeometryCommitResult } from "../utils/previewFeatureUsage";
|
|
1
2
|
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
2
3
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
4
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
@@ -67,7 +68,7 @@ export function useDomGeometryCommit({
|
|
|
67
68
|
}: UseDomGeometryCommitOptions): DomGeometryCommits {
|
|
68
69
|
const projectIdRef = useRef(projectId);
|
|
69
70
|
projectIdRef.current = projectId;
|
|
70
|
-
const pending = useRef(new Set<Promise<
|
|
71
|
+
const pending = useRef(new Set<Promise<GeometryCommitResult | undefined>>()).current;
|
|
71
72
|
const editHistory = useMemo(() => ({ recordEdit }), [recordEdit]);
|
|
72
73
|
const reload = useCallback(
|
|
73
74
|
() => (reloadPreview ? reloadPreview() : iframeRef.current?.contentWindow?.location.reload()),
|
|
@@ -142,7 +143,10 @@ export function useDomGeometryCommit({
|
|
|
142
143
|
updateArcSegment: gsap.updateArcSegment,
|
|
143
144
|
});
|
|
144
145
|
const saved = useCallback(
|
|
145
|
-
async (
|
|
146
|
+
async (
|
|
147
|
+
commit: () => Promise<GeometryCommitResult | undefined>,
|
|
148
|
+
restore = noop,
|
|
149
|
+
): Promise<DomEditCommitOutcome> => {
|
|
146
150
|
const refusal = !projectIdRef.current
|
|
147
151
|
? "No project is open"
|
|
148
152
|
: !isPreviewBooted(projectIdRef.current)
|
|
@@ -156,11 +160,10 @@ export function useDomGeometryCommit({
|
|
|
156
160
|
const run = commit();
|
|
157
161
|
pending.add(run);
|
|
158
162
|
try {
|
|
159
|
-
await run;
|
|
163
|
+
return { ok: true, changed: (await run)?.changed === true };
|
|
160
164
|
} finally {
|
|
161
165
|
pending.delete(run);
|
|
162
166
|
}
|
|
163
|
-
return { ok: true };
|
|
164
167
|
},
|
|
165
168
|
[pending, showToast],
|
|
166
169
|
);
|
|
@@ -305,6 +305,7 @@ describe("Reset after a plain move puts the author's translate back", () => {
|
|
|
305
305
|
async (_selection, patches) => {
|
|
306
306
|
project.history.push(project.file);
|
|
307
307
|
project.file = patches.reduce((html, op) => applyPatch(html, "layer", op), project.file);
|
|
308
|
+
return undefined;
|
|
308
309
|
},
|
|
309
310
|
);
|
|
310
311
|
const selection = { id: "layer", selector: "#layer", element } as unknown as DomEditSelection;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DomEditPersistOutcome } from "./domEditCommitTypes";
|
|
1
2
|
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
2
3
|
import { useCallback } from "react";
|
|
3
4
|
import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
|
|
@@ -39,7 +40,7 @@ export interface UseDomGeometryCommitsParams {
|
|
|
39
40
|
skipRefresh?: boolean;
|
|
40
41
|
deferRender?: boolean;
|
|
41
42
|
},
|
|
42
|
-
) => Promise<
|
|
43
|
+
) => Promise<DomEditPersistOutcome | undefined>;
|
|
43
44
|
readOnlyPreview: boolean;
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -73,7 +74,7 @@ export function useDomGeometryCommits({
|
|
|
73
74
|
restore?: () => void,
|
|
74
75
|
undoKey?: string,
|
|
75
76
|
) => {
|
|
76
|
-
if (readOnlyPreview) return Promise.resolve();
|
|
77
|
+
if (readOnlyPreview) return Promise.resolve(undefined);
|
|
77
78
|
const element = selection.element;
|
|
78
79
|
const beforeSize = captureStudioBoxSize(element);
|
|
79
80
|
const beforeOffset = captureStudioPathOffset(element);
|
|
@@ -334,3 +334,29 @@ describe("useDomSelection", () => {
|
|
|
334
334
|
harness.cleanup();
|
|
335
335
|
});
|
|
336
336
|
});
|
|
337
|
+
|
|
338
|
+
it("reports unchanged marquee membership and one new additive member", () => {
|
|
339
|
+
const { harness, card, chip } = renderCardAndChip();
|
|
340
|
+
try {
|
|
341
|
+
act(() => harness.current().applyDomSelection(card));
|
|
342
|
+
let receipt!: ReturnType<ReturnType<typeof useDomSelection>["applyMarqueeSelection"]>;
|
|
343
|
+
act(() => {
|
|
344
|
+
receipt = harness.current().applyMarqueeSelection([chip], true);
|
|
345
|
+
});
|
|
346
|
+
expect(receipt).toEqual({ changed: true, count: 2 });
|
|
347
|
+
act(() => {
|
|
348
|
+
receipt = harness.current().applyMarqueeSelection([card, chip], true);
|
|
349
|
+
});
|
|
350
|
+
expect(receipt).toEqual({ changed: false, count: 2 });
|
|
351
|
+
act(() => {
|
|
352
|
+
receipt = harness.current().applyMarqueeSelection([], true);
|
|
353
|
+
});
|
|
354
|
+
expect(receipt).toEqual({ changed: false, count: 2 });
|
|
355
|
+
act(() => {
|
|
356
|
+
receipt = harness.current().applyMarqueeSelection([], false);
|
|
357
|
+
});
|
|
358
|
+
expect(receipt).toEqual({ changed: true, count: 0 });
|
|
359
|
+
} finally {
|
|
360
|
+
harness.cleanup();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
@@ -487,12 +487,13 @@ export function useDomSelection({
|
|
|
487
487
|
// fallow-ignore-next-line complexity
|
|
488
488
|
(selections: DomEditSelection[], additive: boolean) => {
|
|
489
489
|
logSelect("marquee", { hits: selections.length, additive });
|
|
490
|
+
const current = domEditSelectionRef.current;
|
|
491
|
+
const currentGroup = domEditGroupSelectionsRef.current;
|
|
490
492
|
if (selections.length === 0) {
|
|
493
|
+
const count = seedDomEditGroupWithSelection(currentGroup, current).length;
|
|
491
494
|
if (!additive) applyDomSelection(null, { revealPanel: false });
|
|
492
|
-
return;
|
|
495
|
+
return { changed: !additive && count > 0, count: additive ? count : 0 };
|
|
493
496
|
}
|
|
494
|
-
const current = domEditSelectionRef.current;
|
|
495
|
-
const currentGroup = domEditGroupSelectionsRef.current;
|
|
496
497
|
let nextGroup: DomEditSelection[];
|
|
497
498
|
if (additive) {
|
|
498
499
|
nextGroup = seedDomEditGroupWithSelection(currentGroup, current);
|
|
@@ -507,11 +508,17 @@ export function useDomSelection({
|
|
|
507
508
|
}
|
|
508
509
|
}
|
|
509
510
|
const nextSelection = additive && current ? current : selections[0];
|
|
511
|
+
const previous = seedDomEditGroupWithSelection(currentGroup, current);
|
|
512
|
+
const changed =
|
|
513
|
+
previous.length !== nextGroup.length ||
|
|
514
|
+
nextGroup.some((selection) => !domEditSelectionInGroup(previous, selection)) ||
|
|
515
|
+
!domEditSelectionsTargetSame(current, nextSelection);
|
|
510
516
|
domEditSelectionRef.current = nextSelection;
|
|
511
517
|
domEditGroupSelectionsRef.current = nextGroup;
|
|
512
518
|
setDomEditSelection(nextSelection);
|
|
513
519
|
setDomEditGroupSelections(nextGroup);
|
|
514
520
|
announceTimelineSelection(nextGroup, nextSelection, true);
|
|
521
|
+
return { changed, count: nextGroup.length };
|
|
515
522
|
},
|
|
516
523
|
[applyDomSelection, announceTimelineSelection],
|
|
517
524
|
);
|
|
@@ -3,6 +3,11 @@ import type { DomEditSelection } from "../components/editor/domEditing";
|
|
|
3
3
|
import type { SelectElementOptions, TimelineElement } from "../player";
|
|
4
4
|
import type { RightPanelTab } from "../utils/studioHelpers";
|
|
5
5
|
|
|
6
|
+
export interface DomSelectionResult {
|
|
7
|
+
changed: boolean;
|
|
8
|
+
count: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export interface ApplyDomSelectionOptions {
|
|
7
12
|
revealPanel?: boolean;
|
|
8
13
|
additive?: boolean;
|
|
@@ -85,5 +90,5 @@ export interface UseDomSelectionReturn {
|
|
|
85
90
|
handleTimelineElementSelect: (element: TimelineElement | null) => Promise<void>;
|
|
86
91
|
refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => Promise<void>;
|
|
87
92
|
refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
|
|
88
|
-
applyMarqueeSelection: (selections: DomEditSelection[], additive: boolean) =>
|
|
93
|
+
applyMarqueeSelection: (selections: DomEditSelection[], additive: boolean) => DomSelectionResult;
|
|
89
94
|
}
|