@hyperframes/studio 0.8.103 → 0.8.105
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-DG795Hwu.js → hyperframes-player-tnfCiVdR.js} +1 -1
- package/dist/assets/{index-BiEtDkae.js → index-Ca6LZlVb.js} +1 -1
- package/dist/assets/index-Cre9X4RS.css +1 -0
- package/dist/assets/{index-BPJRf2X9.js → index-DEDhPmPD.js} +215 -215
- package/dist/assets/{index-DuCZ3pQw.js → index-zpe332hb.js} +1 -1
- package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
- package/dist/chunk-EEXSYY5F.js.map +1 -0
- package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
- package/dist/chunk-NCUBAVGK.js.map +1 -0
- package/dist/{chunk-HIGHVWYD.js → chunk-QADHJFDF.js} +2 -2
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +181 -168
- package/dist/index.html +2 -2
- package/dist/index.js +1018 -919
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui.js +2 -2
- package/package.json +8 -8
- package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
- package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
- package/src/captions/components/CaptionTimeline.tsx +3 -3
- package/src/components/AskAgentModal.tsx +1 -1
- package/src/components/LintModal.tsx +35 -35
- package/src/components/StudioErrorBoundary.tsx +1 -1
- package/src/components/StudioHeader.dom.test.tsx +4 -4
- package/src/components/StudioHeader.tsx +2 -1
- package/src/components/StudioToast.tsx +7 -16
- package/src/components/TimelineToolPicker.tsx +1 -1
- package/src/components/TimelineToolbar.tsx +11 -11
- package/src/components/dock/Dock.tsx +1 -1
- package/src/components/dock/dock.css +9 -1
- package/src/components/editor/AnimationCard.tsx +5 -5
- package/src/components/editor/AnimationCardParts.tsx +1 -1
- package/src/components/editor/BlockParamsPanel.tsx +3 -3
- package/src/components/editor/BorderRadiusEditor.tsx +8 -15
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +5 -6
- package/src/components/editor/DomEditRotateHandle.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +14 -19
- package/src/components/editor/EaseModeControls.tsx +2 -2
- package/src/components/editor/EaseParamFields.tsx +8 -8
- package/src/components/editor/FileTree.tsx +1 -1
- package/src/components/editor/FileTreeIcons.tsx +47 -26
- package/src/components/editor/FileTreeNodes.tsx +11 -8
- package/src/components/editor/GestureRecordControl.tsx +3 -3
- package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
- package/src/components/editor/GsapAnimationSection.tsx +2 -2
- package/src/components/editor/InlineTextToolbar.tsx +4 -4
- package/src/components/editor/InspectorHeaderActions.tsx +1 -1
- package/src/components/editor/KeyframeDiamond.tsx +6 -4
- package/src/components/editor/KeyframeNavigation.tsx +4 -4
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanel.tsx +3 -3
- package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
- package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +5 -5
- package/src/components/editor/TimelineFxPopover.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +46 -6
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
- package/src/components/editor/domEditOverlayGestures.ts +13 -19
- package/src/components/editor/domEditOverlayStartGesture.test.ts +170 -13
- package/src/components/editor/domEditOverlayStartGesture.ts +15 -25
- package/src/components/editor/easeCurveSvg.tsx +1 -1
- package/src/components/editor/manualEdits.test.ts +3 -9
- package/src/components/editor/manualEdits.ts +0 -1
- package/src/components/editor/manualEditsDom.ts +3 -13
- package/src/components/editor/manualEditsDomPatches.test.ts +3 -131
- package/src/components/editor/manualEditsDomPatches.ts +1 -53
- package/src/components/editor/manualEditsSnapshot.ts +3 -0
- package/src/components/editor/manualEditsTypes.ts +2 -0
- package/src/components/editor/manualOffsetDrag.test.ts +41 -0
- package/src/components/editor/manualOffsetDrag.ts +6 -27
- package/src/components/editor/persistSeam.integration.test.ts +0 -18
- package/src/components/editor/plainTranslate.ts +42 -6
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
- package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
- package/src/components/editor/propertyPanelFill.tsx +6 -6
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +2 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +2 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
- package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelTransformCommit.test.ts +29 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +8 -5
- package/src/components/editor/propertyPanelTypes.ts +1 -0
- package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
- package/src/components/editor/propertyPanelValueTier.ts +1 -1
- package/src/components/editor/rotateInlineSave.test.ts +111 -0
- package/src/components/editor/rotationDraft.test.ts +111 -0
- package/src/components/editor/rotationDraft.ts +145 -0
- package/src/components/editor/useDomEditNudge.test.tsx +33 -0
- package/src/components/editor/useDomEditNudge.ts +6 -2
- package/src/components/editor/useDomEditOverlayGestures.ts +22 -24
- package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
- package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
- package/src/components/nle/AudioMeterStrip.tsx +2 -2
- package/src/components/nle/NLEPreview.test.ts +8 -4
- package/src/components/nle/PreviewOverlays.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/PreviewPoster.tsx +29 -1
- package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
- package/src/components/panels/SlideshowPanel.tsx +4 -4
- package/src/components/panels/SlideshowSubPanels.tsx +19 -21
- package/src/components/panels/VariablesBindElement.tsx +1 -1
- package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +4 -6
- package/src/components/panels/VariablesRowAction.tsx +1 -1
- package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
- package/src/components/renders/RenderQueue.tsx +2 -2
- package/src/components/renders/RenderQueueItem.tsx +3 -3
- package/src/components/sidebar/AssetCard.tsx +3 -3
- package/src/components/sidebar/AssetContextMenu.tsx +7 -7
- package/src/components/sidebar/AssetsTab.tsx +6 -6
- package/src/components/sidebar/AudioRow.tsx +5 -3
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
- package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
- package/src/components/sidebar/CompositionsTab.tsx +65 -12
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.tsx +3 -3
- package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
- package/src/components/timelineToolbarStyles.ts +3 -3
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/HyperframesLogo.tsx +19 -18
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Menu.tsx +4 -2
- package/src/hooks/draggedGsapPosition.ts +28 -0
- package/src/hooks/elementOffsetStager.ts +34 -13
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +36 -2
- package/src/hooks/plainRotation.test.ts +122 -0
- package/src/hooks/plainRotation.ts +42 -0
- package/src/hooks/useClipboard.source.test.ts +42 -4
- package/src/hooks/useClipboard.ts +13 -3
- package/src/hooks/useDomEditCommits.ts +0 -3
- package/src/hooks/useDomEditSession.ts +5 -5
- package/src/hooks/useDomGeometryCommit.test.tsx +79 -1
- package/src/hooks/useDomGeometryCommit.ts +16 -17
- package/src/hooks/useDomGeometryCommits.test.tsx +233 -33
- package/src/hooks/useDomGeometryCommits.ts +27 -80
- package/src/hooks/useGsapAwareEditing.test.tsx +194 -11
- package/src/hooks/useGsapAwareEditing.ts +43 -42
- package/src/hooks/useInlineTextEdit.test.tsx +33 -3
- package/src/hooks/useInlineTextEdit.ts +2 -2
- package/src/player/components/ClipContextMenu.tsx +1 -1
- package/src/player/components/CompositionThumbnail.tsx +23 -11
- package/src/player/components/EditModal.tsx +6 -6
- package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +3 -3
- package/src/player/components/ShortcutsPanel.tsx +8 -20
- package/src/player/components/TimelineCanvas.tsx +1 -1
- package/src/player/components/TimelineClip.tsx +2 -2
- package/src/player/components/TimelineEmptyState.tsx +2 -2
- package/src/player/components/TimelineLanes.tsx +0 -1
- package/src/player/components/TrackClipCount.tsx +1 -1
- package/src/player/components/VolumeControl.tsx +1 -1
- package/src/player/components/timelineClipChildren.tsx +1 -1
- package/src/player/components/timelineMotionStyles.test.ts +6 -22
- package/src/player/components/trackHeaderLabelRows.tsx +16 -16
- package/src/player/lib/thumbnailScheduler.ts +4 -5
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/styles/hex-baseline.json +7 -19
- package/src/styles/studio.css +0 -9
- package/src/styles/tailwind-preset.shared.js +2 -1
- package/src/styles/theme.css +204 -147
- package/src/styles/theme.test.ts +13 -9
- package/src/utils/blockCategories.ts +41 -9
- package/src/utils/clipboardPayload.ts +2 -0
- package/dist/assets/index-CQN3M56N.css +0 -1
- package/dist/chunk-62DDWOSH.js.map +0 -1
- package/dist/chunk-6GAO75IU.js.map +0 -1
- package/src/hooks/gsapTargetCache.ts +0 -62
- /package/dist/{chunk-HIGHVWYD.js.map → chunk-QADHJFDF.js.map} +0 -0
|
@@ -4,6 +4,7 @@ import React, { act } from "react";
|
|
|
4
4
|
import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
5
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
6
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
|
+
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
7
8
|
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
8
9
|
import { mountReactHarness, withInlineLayoutBox } from "./domSelectionTestHarness";
|
|
9
10
|
import { DomEditCropHandles } from "../components/editor/DomEditCropHandles";
|
|
@@ -41,6 +42,7 @@ vi.mock("./useSafeGsapCommitMutation", () => ({
|
|
|
41
42
|
}));
|
|
42
43
|
|
|
43
44
|
import { useGsapAwareEditing } from "./useGsapAwareEditing";
|
|
45
|
+
import { tryGsapRotationIntercept } from "./gsapRuntimeBridge";
|
|
44
46
|
|
|
45
47
|
afterEach(() => {
|
|
46
48
|
vi.clearAllMocks();
|
|
@@ -54,8 +56,10 @@ function gsapPositioned(tag: string): HTMLElement {
|
|
|
54
56
|
function mountResizeHandler(
|
|
55
57
|
animations: GsapAnimation[],
|
|
56
58
|
targetAnimations: GsapAnimation[] = animations,
|
|
59
|
+
gsapOwnsBox = true,
|
|
60
|
+
hasGsapWriter = true,
|
|
57
61
|
) {
|
|
58
|
-
const element = document.createElement("div");
|
|
62
|
+
const element = gsapOwnsBox ? gsapPositioned("div") : document.createElement("div");
|
|
59
63
|
const selection = { element, id: "clip", selector: "#clip" } as unknown as DomEditSelection;
|
|
60
64
|
const fallback = vi.fn().mockResolvedValue(undefined);
|
|
61
65
|
const anchorSave = vi.fn().mockResolvedValue(undefined);
|
|
@@ -63,6 +67,7 @@ function mountResizeHandler(
|
|
|
63
67
|
const elementOffset = vi.fn(() => ({ save: anchorSave, rollback: anchorRollback }));
|
|
64
68
|
const commitMutation = vi.fn().mockResolvedValue(undefined);
|
|
65
69
|
const commitPatch = vi.fn().mockResolvedValue(undefined);
|
|
70
|
+
const fetchAnimations = vi.fn(() => vi.fn().mockResolvedValue(targetAnimations));
|
|
66
71
|
let resize:
|
|
67
72
|
| ((
|
|
68
73
|
selection: DomEditSelection,
|
|
@@ -76,14 +81,15 @@ function mountResizeHandler(
|
|
|
76
81
|
const editing = useGsapAwareEditing({
|
|
77
82
|
domEditSelection: selection,
|
|
78
83
|
selectedGsapAnimations: animations,
|
|
79
|
-
gsapCommitMutation: commitMutation,
|
|
84
|
+
gsapCommitMutation: hasGsapWriter ? commitMutation : null,
|
|
80
85
|
previewIframeRef: { current: null },
|
|
81
86
|
showToast: vi.fn(),
|
|
82
87
|
bumpGsapCache: vi.fn(),
|
|
83
|
-
makeFetchFallback:
|
|
88
|
+
makeFetchFallback: fetchAnimations,
|
|
84
89
|
trackGsapInteractionFailure: vi.fn(),
|
|
85
90
|
stageElementPositionOffset: elementOffset,
|
|
86
91
|
handleDomBoxSizeCommit: fallback,
|
|
92
|
+
handleDomRotationCommit: vi.fn(),
|
|
87
93
|
commitPositionPatchToHtml: commitPatch,
|
|
88
94
|
addGsapAnimation: vi.fn(),
|
|
89
95
|
convertToKeyframes: vi.fn(),
|
|
@@ -103,6 +109,7 @@ function mountResizeHandler(
|
|
|
103
109
|
anchorRollback,
|
|
104
110
|
commitMutation,
|
|
105
111
|
commitPatch,
|
|
112
|
+
fetchAnimations,
|
|
106
113
|
resize: resize!,
|
|
107
114
|
property: property!,
|
|
108
115
|
root,
|
|
@@ -116,13 +123,21 @@ function mountGroupHandler({
|
|
|
116
123
|
makeFetchFallback,
|
|
117
124
|
trackGsapInteractionFailure = vi.fn(),
|
|
118
125
|
stageElementPositionOffset = vi.fn(),
|
|
126
|
+
handleDomRotationCommit = vi.fn(),
|
|
119
127
|
}: Pick<AwareEditingParams, "gsapCommitMutation" | "makeFetchFallback"> &
|
|
120
|
-
Partial<
|
|
128
|
+
Partial<
|
|
129
|
+
Pick<
|
|
130
|
+
AwareEditingParams,
|
|
131
|
+
"trackGsapInteractionFailure" | "stageElementPositionOffset" | "handleDomRotationCommit"
|
|
132
|
+
>
|
|
133
|
+
>) {
|
|
121
134
|
let groupCommit!: (updates: DomEditGroupPathOffsetCommit[]) => Promise<void>;
|
|
122
135
|
let pathOffsetCommit!: (
|
|
123
136
|
selection: DomEditSelection,
|
|
124
137
|
next: { x: number; y: number },
|
|
138
|
+
route?: { plainTranslate?: boolean },
|
|
125
139
|
) => Promise<void>;
|
|
140
|
+
let rotationCommit!: (selection: DomEditSelection, next: RotationCommit) => Promise<void>;
|
|
126
141
|
function Harness() {
|
|
127
142
|
const editing = useGsapAwareEditing({
|
|
128
143
|
domEditSelection: null,
|
|
@@ -135,6 +150,7 @@ function mountGroupHandler({
|
|
|
135
150
|
trackGsapInteractionFailure,
|
|
136
151
|
stageElementPositionOffset,
|
|
137
152
|
handleDomBoxSizeCommit: vi.fn(),
|
|
153
|
+
handleDomRotationCommit,
|
|
138
154
|
commitPositionPatchToHtml: vi.fn(),
|
|
139
155
|
addGsapAnimation: vi.fn(),
|
|
140
156
|
convertToKeyframes: vi.fn(),
|
|
@@ -143,13 +159,19 @@ function mountGroupHandler({
|
|
|
143
159
|
});
|
|
144
160
|
groupCommit = editing.handleGsapAwareGroupPathOffsetCommit;
|
|
145
161
|
pathOffsetCommit = editing.handleGsapAwarePathOffsetCommit;
|
|
162
|
+
rotationCommit = editing.handleGsapAwareRotationCommit;
|
|
146
163
|
return null;
|
|
147
164
|
}
|
|
148
165
|
const root = mountReactHarness(<Harness />);
|
|
149
166
|
return {
|
|
150
167
|
groupCommit: (updates: DomEditGroupPathOffsetCommit[]) => groupCommit(updates),
|
|
151
|
-
pathOffsetCommit: (
|
|
152
|
-
|
|
168
|
+
pathOffsetCommit: (
|
|
169
|
+
selection: DomEditSelection,
|
|
170
|
+
next: { x: number; y: number },
|
|
171
|
+
route?: { plainTranslate?: boolean },
|
|
172
|
+
) => pathOffsetCommit(selection, next, route),
|
|
173
|
+
rotationCommit: (selection: DomEditSelection, next: RotationCommit) =>
|
|
174
|
+
rotationCommit(selection, next),
|
|
153
175
|
root,
|
|
154
176
|
};
|
|
155
177
|
}
|
|
@@ -167,7 +189,7 @@ describe("useGsapAwareEditing moves of an element GSAP does not position", () =>
|
|
|
167
189
|
});
|
|
168
190
|
const box = { element: document.createElement("div"), id: "box", selector: "#box" };
|
|
169
191
|
await act(() => pathOffsetCommit(box as unknown as DomEditSelection, { x: 130.25, y: 90 }));
|
|
170
|
-
expect(stageElementPositionOffset).toHaveBeenCalledWith(box, { x: 130.25, y: 90 });
|
|
192
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(box, { x: 130.25, y: 90 }, true);
|
|
171
193
|
expect(save).toHaveBeenCalledTimes(1);
|
|
172
194
|
expect(gsapCommitMutation).not.toHaveBeenCalled();
|
|
173
195
|
expect(makeFetchFallback).not.toHaveBeenCalled();
|
|
@@ -176,6 +198,127 @@ describe("useGsapAwareEditing moves of an element GSAP does not position", () =>
|
|
|
176
198
|
});
|
|
177
199
|
});
|
|
178
200
|
|
|
201
|
+
describe("useGsapAwareEditing keeps the route a gesture chose at press", () => {
|
|
202
|
+
it.each([
|
|
203
|
+
["the CSS route, on an element GSAP has since taken over", true, () => gsapPositioned("div")],
|
|
204
|
+
[
|
|
205
|
+
"the GSAP route, on an element that now looks GSAP-free",
|
|
206
|
+
false,
|
|
207
|
+
() => document.createElement("div"),
|
|
208
|
+
],
|
|
209
|
+
])("%s", async (_, plainTranslate, makeElement) => {
|
|
210
|
+
mocks.drag.mockResolvedValue({ status: "persisted" });
|
|
211
|
+
const save = vi.fn().mockResolvedValue(undefined);
|
|
212
|
+
const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
|
|
213
|
+
const { pathOffsetCommit, groupCommit, root } = mountGroupHandler({
|
|
214
|
+
gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
|
|
215
|
+
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
216
|
+
stageElementPositionOffset,
|
|
217
|
+
});
|
|
218
|
+
const box = {
|
|
219
|
+
element: makeElement(),
|
|
220
|
+
id: "box",
|
|
221
|
+
selector: "#box",
|
|
222
|
+
} as unknown as DomEditSelection;
|
|
223
|
+
await act(() => pathOffsetCommit(box, { x: 40, y: 20 }, { plainTranslate }));
|
|
224
|
+
await act(() => groupCommit([{ selection: box, next: { x: 40, y: 20 }, plainTranslate }]));
|
|
225
|
+
expect(stageElementPositionOffset.mock.calls.length).toBe(plainTranslate ? 2 : 0);
|
|
226
|
+
expect(mocks.drag.mock.calls.some((call) => call[0] === box)).toBe(!plainTranslate);
|
|
227
|
+
act(() => root.unmount());
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("useGsapAwareEditing refuses a group GSAP took over before writing any member", () => {
|
|
232
|
+
it("writes no member when one CSS-route member has been folded since the press", async () => {
|
|
233
|
+
const stageElementPositionOffset = vi.fn(() => ({ save: vi.fn(), rollback: vi.fn() }));
|
|
234
|
+
const { groupCommit, root } = mountGroupHandler({
|
|
235
|
+
gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
|
|
236
|
+
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
237
|
+
stageElementPositionOffset,
|
|
238
|
+
});
|
|
239
|
+
const card = {
|
|
240
|
+
element: document.createElement("div"),
|
|
241
|
+
id: "card",
|
|
242
|
+
} as unknown as DomEditSelection;
|
|
243
|
+
const folded = Object.assign(document.createElement("div"), {
|
|
244
|
+
_gsap: { renderTransform: () => {}, x: "94px", y: "66px" },
|
|
245
|
+
});
|
|
246
|
+
const box = { element: folded, id: "box" } as unknown as DomEditSelection;
|
|
247
|
+
await expect(
|
|
248
|
+
act(() =>
|
|
249
|
+
groupCommit([
|
|
250
|
+
{ selection: card, next: { x: 1, y: 2 }, plainTranslate: true },
|
|
251
|
+
{ selection: box, next: { x: 1, y: 2 }, plainTranslate: true },
|
|
252
|
+
]),
|
|
253
|
+
),
|
|
254
|
+
).rejects.toThrow(/animation took over/);
|
|
255
|
+
expect(stageElementPositionOffset).not.toHaveBeenCalled();
|
|
256
|
+
act(() => root.unmount());
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("useGsapAwareEditing rotation routing", () => {
|
|
261
|
+
function rotate(element: HTMLElement) {
|
|
262
|
+
const handleDomRotationCommit = vi.fn().mockResolvedValue(undefined);
|
|
263
|
+
const gsapCommitMutation = vi.fn();
|
|
264
|
+
const h = mountGroupHandler({
|
|
265
|
+
gsapCommitMutation,
|
|
266
|
+
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
267
|
+
handleDomRotationCommit,
|
|
268
|
+
});
|
|
269
|
+
const box = { element, id: "box", selector: "#box" } as unknown as DomEditSelection;
|
|
270
|
+
return { ...h, box, handleDomRotationCommit, gsapCommitMutation };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
it("turns an element GSAP does not turn by the CSS writer, with no GSAP write", async () => {
|
|
274
|
+
const h = rotate(document.createElement("div"));
|
|
275
|
+
await act(() => h.rotationCommit(h.box, { angle: 55 }));
|
|
276
|
+
expect(h.handleDomRotationCommit).toHaveBeenCalledWith(h.box, { angle: 55 });
|
|
277
|
+
expect(tryGsapRotationIntercept).not.toHaveBeenCalled();
|
|
278
|
+
expect(h.gsapCommitMutation).not.toHaveBeenCalled();
|
|
279
|
+
act(() => h.root.unmount());
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("leaves an element a GSAP tween turns to the GSAP rotation route", async () => {
|
|
283
|
+
vi.mocked(tryGsapRotationIntercept).mockResolvedValue({ status: "persisted" });
|
|
284
|
+
const element = document.createElement("div");
|
|
285
|
+
const h = rotate(element);
|
|
286
|
+
const tween = { vars: { rotation: 30 }, targets: () => [element] };
|
|
287
|
+
Object.assign(window, { __timelines: { main: { getChildren: () => [tween] } } });
|
|
288
|
+
try {
|
|
289
|
+
await act(() => h.rotationCommit(h.box, { angle: 55 }));
|
|
290
|
+
} finally {
|
|
291
|
+
delete (window as { __timelines?: unknown }).__timelines;
|
|
292
|
+
}
|
|
293
|
+
expect(tryGsapRotationIntercept).toHaveBeenCalledTimes(1);
|
|
294
|
+
expect(h.handleDomRotationCommit).not.toHaveBeenCalled();
|
|
295
|
+
act(() => h.root.unmount());
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("saves a turn the press drew as CSS by the CSS writer, even once a GSAP tween turns the element", async () => {
|
|
299
|
+
const element = document.createElement("div");
|
|
300
|
+
const h = rotate(element);
|
|
301
|
+
const tween = { vars: { rotation: 30 }, targets: () => [element] };
|
|
302
|
+
Object.assign(window, { __timelines: { main: { getChildren: () => [tween] } } });
|
|
303
|
+
const plain = {
|
|
304
|
+
property: "rotate",
|
|
305
|
+
before: "",
|
|
306
|
+
after: "",
|
|
307
|
+
share: 0,
|
|
308
|
+
sign: 1,
|
|
309
|
+
inline: false,
|
|
310
|
+
} as const;
|
|
311
|
+
try {
|
|
312
|
+
await act(() => h.rotationCommit(h.box, { angle: 55, plain }));
|
|
313
|
+
} finally {
|
|
314
|
+
delete (window as { __timelines?: unknown }).__timelines;
|
|
315
|
+
}
|
|
316
|
+
expect(h.handleDomRotationCommit).toHaveBeenCalledWith(h.box, { angle: 55, plain });
|
|
317
|
+
expect(tryGsapRotationIntercept).not.toHaveBeenCalled();
|
|
318
|
+
act(() => h.root.unmount());
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
179
322
|
describe("useGsapAwareEditing shared-tween moves", () => {
|
|
180
323
|
it("saves a single drag through the element's own offset", async () => {
|
|
181
324
|
mocks.drag.mockResolvedValue({ status: "element-offset" });
|
|
@@ -189,7 +332,7 @@ describe("useGsapAwareEditing shared-tween moves", () => {
|
|
|
189
332
|
});
|
|
190
333
|
const word = { element: gsapPositioned("span"), hfId: "w0", selector: ".w" };
|
|
191
334
|
await act(() => pathOffsetCommit(word as unknown as DomEditSelection, { x: 40, y: 20 }));
|
|
192
|
-
expect(stageElementPositionOffset).toHaveBeenCalledWith(word, { x: 40, y: 20 });
|
|
335
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(word, { x: 40, y: 20 }, false);
|
|
193
336
|
expect(save).toHaveBeenCalledTimes(1);
|
|
194
337
|
expect(commitMutation).not.toHaveBeenCalled();
|
|
195
338
|
act(() => root.unmount());
|
|
@@ -219,8 +362,18 @@ describe("useGsapAwareEditing shared-tween moves", () => {
|
|
|
219
362
|
] as unknown as DomEditGroupPathOffsetCommit[]),
|
|
220
363
|
);
|
|
221
364
|
const groupKey = expect.stringMatching(/^group-drag:\d+$/);
|
|
222
|
-
expect(stageElementPositionOffset).toHaveBeenCalledWith(
|
|
223
|
-
|
|
365
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(
|
|
366
|
+
word,
|
|
367
|
+
{ x: 40, y: 20 },
|
|
368
|
+
false,
|
|
369
|
+
groupKey,
|
|
370
|
+
);
|
|
371
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(
|
|
372
|
+
plain,
|
|
373
|
+
{ x: 40, y: 20 },
|
|
374
|
+
true,
|
|
375
|
+
groupKey,
|
|
376
|
+
);
|
|
224
377
|
expect(mocks.drag.mock.calls.some((call) => call[0] === plain)).toBe(false);
|
|
225
378
|
act(() => root.unmount());
|
|
226
379
|
});
|
|
@@ -252,6 +405,36 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
252
405
|
act(() => h.root.unmount());
|
|
253
406
|
});
|
|
254
407
|
|
|
408
|
+
it("resizes a box GSAP does not own through the CSS writer, with no GSAP write or fetch", async () => {
|
|
409
|
+
const h = mountResizeHandler([], [], false);
|
|
410
|
+
const restore = vi.fn();
|
|
411
|
+
await act(() =>
|
|
412
|
+
h.resize(h.selection, { width: 300, height: 200 }, { x: -50.5, y: -25 }, restore),
|
|
413
|
+
);
|
|
414
|
+
expect(h.fallback).toHaveBeenCalledWith(
|
|
415
|
+
h.selection,
|
|
416
|
+
{ width: 300, height: 200 },
|
|
417
|
+
{ x: -50.5, y: -25 },
|
|
418
|
+
restore,
|
|
419
|
+
);
|
|
420
|
+
expect(mocks.resize).not.toHaveBeenCalled();
|
|
421
|
+
expect(mocks.drag).not.toHaveBeenCalled();
|
|
422
|
+
expect(h.commitMutation).not.toHaveBeenCalled();
|
|
423
|
+
expect(h.fetchAnimations).not.toHaveBeenCalled();
|
|
424
|
+
act(() => h.root.unmount());
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("fails a GSAP-owned resize loudly when there is no GSAP writer, never writing CSS", async () => {
|
|
428
|
+
const h = mountResizeHandler([], [], true, false);
|
|
429
|
+
const restore = vi.fn();
|
|
430
|
+
await expect(
|
|
431
|
+
act(() => h.resize(h.selection, { width: 300, height: 200 }, undefined, restore)),
|
|
432
|
+
).rejects.toThrow("no GSAP writer");
|
|
433
|
+
expect(h.fallback).not.toHaveBeenCalled();
|
|
434
|
+
expect(restore).toHaveBeenCalledTimes(1);
|
|
435
|
+
act(() => h.root.unmount());
|
|
436
|
+
});
|
|
437
|
+
|
|
255
438
|
it("rejects a blocked resize instead of falling through to a competing DOM write", async () => {
|
|
256
439
|
mocks.resize.mockResolvedValue({ status: "blocked", reason: "source-uneditable" });
|
|
257
440
|
const h = mountResizeHandler([]);
|
|
@@ -287,7 +470,7 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
287
470
|
await act(() => h.resize(h.selection, { width: 300, height: 200 }, { x: -50, y: -25 }));
|
|
288
471
|
const key = h.commitMutation.mock.calls[0]![2].coalesceKey;
|
|
289
472
|
expect(key).toMatch(/^tx:/);
|
|
290
|
-
expect(h.elementOffset).toHaveBeenCalledWith(h.selection, { x: -50, y: -25 }, key);
|
|
473
|
+
expect(h.elementOffset).toHaveBeenCalledWith(h.selection, { x: -50, y: -25 }, false, key);
|
|
291
474
|
expect(h.anchorSave.mock.invocationCallOrder[0]).toBeGreaterThan(
|
|
292
475
|
h.commitMutation.mock.invocationCallOrder[0]!,
|
|
293
476
|
);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Extracted from useDomEditSession to isolate the GSAP intercept routing
|
|
8
8
|
* from the rest of the editing orchestration.
|
|
9
9
|
*/
|
|
10
|
+
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
10
11
|
import { useCallback } from "react";
|
|
11
12
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
12
13
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
@@ -28,15 +29,18 @@ import type {
|
|
|
28
29
|
import { setElementGsapPosition } from "../utils/elementGsap";
|
|
29
30
|
import { logResize, logResizeSettle } from "../utils/resizeDebug";
|
|
30
31
|
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
32
|
+
import type { MoveCommitOptions } from "../components/editor/domEditOverlayGestures";
|
|
31
33
|
import { runGestureTransaction } from "./gestureTransaction";
|
|
32
34
|
import {
|
|
35
|
+
gsapWritesBox,
|
|
33
36
|
gsapWritesPosition,
|
|
37
|
+
gsapWritesRotation,
|
|
34
38
|
hasNonHoldTweenForElement,
|
|
35
39
|
POSITION_CHANNELS,
|
|
36
40
|
} from "./gsapRuntimeKeyframes";
|
|
37
41
|
import { assertGsapEditPersisted, saveMove } from "./gsapEditOutcome";
|
|
38
42
|
import type { GsapAnimationFetchOptions } from "./useGsapAnimationFetchFallback";
|
|
39
|
-
import type
|
|
43
|
+
import { refuseGsapTakeover, type ElementOffsetStagerDeps } from "./elementOffsetStager";
|
|
40
44
|
import {
|
|
41
45
|
prepareCropResize,
|
|
42
46
|
saveCropResize,
|
|
@@ -80,12 +84,15 @@ export interface UseGsapAwareEditingParams {
|
|
|
80
84
|
stageElementPositionOffset: (
|
|
81
85
|
selection: DomEditSelection,
|
|
82
86
|
next: { x: number; y: number },
|
|
87
|
+
plainTranslate: boolean,
|
|
83
88
|
coalesceKey?: string,
|
|
84
89
|
) => { save: () => Promise<void>; rollback: () => void };
|
|
90
|
+
handleDomRotationCommit: (selection: DomEditSelection, next: RotationCommit) => Promise<void>;
|
|
85
91
|
handleDomBoxSizeCommit: (
|
|
86
92
|
selection: DomEditSelection,
|
|
87
93
|
next: { width: number; height: number },
|
|
88
94
|
offset?: { x: number; y: number },
|
|
95
|
+
restore?: () => void,
|
|
89
96
|
) => Promise<void>;
|
|
90
97
|
commitPositionPatchToHtml: ElementOffsetStagerDeps["commitPositionPatchToHtml"];
|
|
91
98
|
// GSAP script commit ops (from useGsapScriptCommits)
|
|
@@ -131,6 +138,7 @@ export function useGsapAwareEditing({
|
|
|
131
138
|
trackGsapInteractionFailure,
|
|
132
139
|
stageElementPositionOffset,
|
|
133
140
|
handleDomBoxSizeCommit,
|
|
141
|
+
handleDomRotationCommit,
|
|
134
142
|
commitPositionPatchToHtml,
|
|
135
143
|
addGsapAnimation,
|
|
136
144
|
convertToKeyframes,
|
|
@@ -151,10 +159,11 @@ export function useGsapAwareEditing({
|
|
|
151
159
|
async (
|
|
152
160
|
selection: DomEditSelection,
|
|
153
161
|
next: { x: number; y: number },
|
|
154
|
-
modifiers?:
|
|
162
|
+
modifiers?: MoveCommitOptions,
|
|
155
163
|
) => {
|
|
156
|
-
|
|
157
|
-
|
|
164
|
+
// A gesture and the panel carry their route; webmcp's moveTo decides here.
|
|
165
|
+
if (modifiers?.plainTranslate ?? !gsapWritesPosition(selection.element))
|
|
166
|
+
return stageElementPositionOffset(selection, next, true).save();
|
|
158
167
|
if (gsapCommitMutation) {
|
|
159
168
|
try {
|
|
160
169
|
const ownedAnimations = getGsapAnimationsForSelection(selection);
|
|
@@ -170,7 +179,7 @@ export function useGsapAwareEditing({
|
|
|
170
179
|
makeFetchFallback(selection),
|
|
171
180
|
modifiers,
|
|
172
181
|
);
|
|
173
|
-
await saveMove(outcome, () => stageElementPositionOffset(selection, next).save());
|
|
182
|
+
await saveMove(outcome, () => stageElementPositionOffset(selection, next, false).save());
|
|
174
183
|
} catch (error) {
|
|
175
184
|
trackGsapInteractionFailure(error, selection, "drag", "Move animated layer");
|
|
176
185
|
throw error;
|
|
@@ -197,13 +206,9 @@ export function useGsapAwareEditing({
|
|
|
197
206
|
// it survives the N sequential server round-trips) onto each commit —
|
|
198
207
|
// otherwise each member records its own entry and it takes N presses to undo.
|
|
199
208
|
const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
|
|
200
|
-
// Members are written one at a time, and a
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
// stay there until their own write lands, which is the single element seen
|
|
204
|
-
// jumping mid-commit while the rest of the group sat still. The drafted
|
|
205
|
-
// positions are already on screen, so holding the render until the last
|
|
206
|
-
// member has been written costs nothing and never shows a half-moved group.
|
|
209
|
+
// Members are written one at a time, and a re-render re-runs the script with the OLD
|
|
210
|
+
// position of every member not yet written, so they snap back until their own write
|
|
211
|
+
// lands. The drafts are already on screen: hold the render until the last member.
|
|
207
212
|
let renderOnCommit = false;
|
|
208
213
|
const previewFallbackLatch = { pending: false };
|
|
209
214
|
const withGroupOptions = (options: CommitMutationOptions): CommitMutationOptions => ({
|
|
@@ -236,17 +241,17 @@ export function useGsapAwareEditing({
|
|
|
236
241
|
return Promise.resolve();
|
|
237
242
|
};
|
|
238
243
|
const preflightAnimations = new Map<DomEditSelection, GsapAnimation[]>();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// Every member reads the same file, and a preflight writes nothing — so run
|
|
245
|
-
// them together. The parse layer shares one in-flight request per file, which
|
|
246
|
-
// turns N sequential round trips into one.
|
|
244
|
+
// Members saved on themselves, each with its route: true for its CSS translate.
|
|
245
|
+
const offsetMembers = new Map<DomEditSelection, boolean>();
|
|
246
|
+
// Editability is user-atomic: prove every member can be written before the first source
|
|
247
|
+
// mutation, so a blocked member never leaves earlier siblings partially moved. Preflights
|
|
248
|
+
// write nothing and share one in-flight parse per file, so they run together.
|
|
247
249
|
const preflightResults = await Promise.allSettled(
|
|
248
|
-
updates.map(async ({ selection }) => {
|
|
249
|
-
if (!gsapWritesPosition(selection.element))
|
|
250
|
+
updates.map(async ({ selection, plainTranslate }) => {
|
|
251
|
+
if (plainTranslate ?? !gsapWritesPosition(selection.element)) {
|
|
252
|
+
refuseGsapTakeover(selection.element, showToast);
|
|
253
|
+
return void offsetMembers.set(selection, true);
|
|
254
|
+
}
|
|
250
255
|
const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
|
|
251
256
|
preflightAnimations.set(selection, animations);
|
|
252
257
|
const outcome = await tryGsapDragIntercept(
|
|
@@ -258,7 +263,7 @@ export function useGsapAwareEditing({
|
|
|
258
263
|
undefined,
|
|
259
264
|
{ preflightOnly: true, group: true },
|
|
260
265
|
);
|
|
261
|
-
if (outcome.status === "element-offset") offsetMembers.
|
|
266
|
+
if (outcome.status === "element-offset") offsetMembers.set(selection, false);
|
|
262
267
|
assertGsapEditPersisted(outcome);
|
|
263
268
|
}),
|
|
264
269
|
);
|
|
@@ -277,8 +282,9 @@ export function useGsapAwareEditing({
|
|
|
277
282
|
);
|
|
278
283
|
for (const [index, { selection, next }] of updates.entries()) {
|
|
279
284
|
renderOnCommit = index === lastScriptWrite;
|
|
280
|
-
|
|
281
|
-
|
|
285
|
+
const plain = offsetMembers.get(selection);
|
|
286
|
+
if (plain !== undefined) {
|
|
287
|
+
await stageElementPositionOffset(selection, next, plain, coalesceKey).save();
|
|
282
288
|
continue;
|
|
283
289
|
}
|
|
284
290
|
try {
|
|
@@ -318,6 +324,7 @@ export function useGsapAwareEditing({
|
|
|
318
324
|
makeFetchFallback,
|
|
319
325
|
trackGsapInteractionFailure,
|
|
320
326
|
stageElementPositionOffset,
|
|
327
|
+
showToast,
|
|
321
328
|
],
|
|
322
329
|
);
|
|
323
330
|
|
|
@@ -328,6 +335,8 @@ export function useGsapAwareEditing({
|
|
|
328
335
|
offset?: { x: number; y: number },
|
|
329
336
|
restore: () => void = () => undefined,
|
|
330
337
|
) => {
|
|
338
|
+
if (!gsapWritesBox(selection.element))
|
|
339
|
+
return handleDomBoxSizeCommit(selection, next, offset, restore);
|
|
331
340
|
let targetAnimations: GsapAnimation[];
|
|
332
341
|
try {
|
|
333
342
|
const ownedAnimations = getGsapAnimationsForSelection(selection);
|
|
@@ -411,7 +420,8 @@ export function useGsapAwareEditing({
|
|
|
411
420
|
);
|
|
412
421
|
// Saved after the size, under its undo key, so the two are one step.
|
|
413
422
|
await saveMove(dragOutcome, async () => {
|
|
414
|
-
|
|
423
|
+
const plain = !gsapWritesPosition(selection.element);
|
|
424
|
+
anchorMove = stageElementPositionOffset(selection, offset, plain, coalesceKey);
|
|
415
425
|
});
|
|
416
426
|
}
|
|
417
427
|
logResizeSettle(selection.element, ownsDragOffset ? "gsap-scale" : "gsap-size");
|
|
@@ -421,13 +431,7 @@ export function useGsapAwareEditing({
|
|
|
421
431
|
throw error;
|
|
422
432
|
}
|
|
423
433
|
}
|
|
424
|
-
|
|
425
|
-
next,
|
|
426
|
-
offset: offset ?? null,
|
|
427
|
-
hadGsapMutation: !!gsapCommitMutation,
|
|
428
|
-
});
|
|
429
|
-
logResizeSettle(selection.element, "dom-route");
|
|
430
|
-
await handleDomBoxSizeCommit(selection, next, offset);
|
|
434
|
+
throw new Error("Resize of a GSAP-owned box has no GSAP writer");
|
|
431
435
|
},
|
|
432
436
|
afterBufferedCommitsSaved: async () => {
|
|
433
437
|
await anchorMove?.save();
|
|
@@ -456,17 +460,13 @@ export function useGsapAwareEditing({
|
|
|
456
460
|
);
|
|
457
461
|
|
|
458
462
|
const handleGsapAwareRotationCommit = useCallback(
|
|
459
|
-
async (selection: DomEditSelection, next:
|
|
463
|
+
async (selection: DomEditSelection, next: RotationCommit) => {
|
|
464
|
+
if (next.plain || !gsapWritesRotation(selection.element))
|
|
465
|
+
return handleDomRotationCommit(selection, next);
|
|
460
466
|
if (gsapCommitMutation) {
|
|
461
467
|
try {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
? ownedAnimations
|
|
465
|
-
: await ownedAnimations;
|
|
466
|
-
// Single source of truth for rotation too: tryGsapRotationIntercept handles
|
|
467
|
-
// tweened elements (keyframes) and static ones (a tl.set), so there's no
|
|
468
|
-
// CSS-var fallback. Selectorless/computed source rejects so the gesture
|
|
469
|
-
// transaction can restore its draft instead of reporting a false success.
|
|
468
|
+
const targetAnimations = await getGsapAnimationsForSelection(selection);
|
|
469
|
+
// A keyframe or a tl.set; a computed source rejects, so the gesture restores its draft.
|
|
470
470
|
const outcome = await tryGsapRotationIntercept(
|
|
471
471
|
selection,
|
|
472
472
|
next.angle,
|
|
@@ -488,6 +488,7 @@ export function useGsapAwareEditing({
|
|
|
488
488
|
makeFetchFallback,
|
|
489
489
|
trackGsapInteractionFailure,
|
|
490
490
|
getGsapAnimationsForSelection,
|
|
491
|
+
handleDomRotationCommit,
|
|
491
492
|
],
|
|
492
493
|
);
|
|
493
494
|
|
|
@@ -345,7 +345,7 @@ describe("useInlineTextEdit", () => {
|
|
|
345
345
|
act(() => root.unmount());
|
|
346
346
|
});
|
|
347
347
|
|
|
348
|
-
it("
|
|
348
|
+
it("commits on Escape", () => {
|
|
349
349
|
const element = heading("Original");
|
|
350
350
|
const { controls, root, onCommit } = mount();
|
|
351
351
|
act(() => {
|
|
@@ -355,8 +355,38 @@ describe("useInlineTextEdit", () => {
|
|
|
355
355
|
element.textContent = "half-typed";
|
|
356
356
|
press(element, "Escape");
|
|
357
357
|
|
|
358
|
-
expect(element.textContent).toBe("
|
|
359
|
-
expect(onCommit).
|
|
358
|
+
expect(element.textContent).toBe("half-typed");
|
|
359
|
+
expect(onCommit).toHaveBeenCalledTimes(1);
|
|
360
|
+
expect(controls().session).toBeNull();
|
|
361
|
+
act(() => root.unmount());
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// The save carries the styled markup it replaced, which is what undo puts back.
|
|
365
|
+
it("saves typing ended by Escape once, with the markup it replaced", async () => {
|
|
366
|
+
const element = heading();
|
|
367
|
+
element.innerHTML = 'Hello <span style="color: red">world</span>';
|
|
368
|
+
const { controls, root, onCommit } = mount();
|
|
369
|
+
act(() => {
|
|
370
|
+
controls().start(element);
|
|
371
|
+
});
|
|
372
|
+
await act(async () => {
|
|
373
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve(null)));
|
|
374
|
+
});
|
|
375
|
+
expect(document.activeElement).toBe(element);
|
|
376
|
+
|
|
377
|
+
element.innerHTML = 'Hello <span style="color: red">world</span>!';
|
|
378
|
+
press(element, "Escape");
|
|
379
|
+
|
|
380
|
+
expect(onCommit.mock.calls).toEqual([
|
|
381
|
+
[
|
|
382
|
+
{
|
|
383
|
+
element,
|
|
384
|
+
html: 'Hello <span style="color: red">world</span>!',
|
|
385
|
+
previousHtml: 'Hello <span style="color: red">world</span>',
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
]);
|
|
389
|
+
expect(element.innerHTML).toBe('Hello <span style="color: red">world</span>!');
|
|
360
390
|
act(() => root.unmount());
|
|
361
391
|
});
|
|
362
392
|
|
|
@@ -203,7 +203,7 @@ export function useInlineTextEdit({
|
|
|
203
203
|
}
|
|
204
204
|
if (event.key === "Escape") {
|
|
205
205
|
event.preventDefault();
|
|
206
|
-
|
|
206
|
+
commit();
|
|
207
207
|
}
|
|
208
208
|
};
|
|
209
209
|
// Clicking away keeps the work, which is what every other field in Studio
|
|
@@ -243,7 +243,7 @@ export function useInlineTextEdit({
|
|
|
243
243
|
element.removeEventListener("dragover", onDragOver);
|
|
244
244
|
element.removeEventListener("drop", onDrop);
|
|
245
245
|
};
|
|
246
|
-
}, [session, commit
|
|
246
|
+
}, [session, commit]);
|
|
247
247
|
|
|
248
248
|
// Unmount keeps the typed words, as blur does. Read through a ref so a new
|
|
249
249
|
// onCommit never closes the session mid-edit.
|
|
@@ -201,7 +201,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
|
|
|
201
201
|
<button
|
|
202
202
|
type="button"
|
|
203
203
|
role="menuitem"
|
|
204
|
-
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-
|
|
204
|
+
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-danger-ink hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left"
|
|
205
205
|
onClick={() => {
|
|
206
206
|
onDelete(element);
|
|
207
207
|
onClose();
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { memo, useMemo, type CSSProperties } from "react";
|
|
2
2
|
import { useThumbnailLease } from "../../hooks/useThumbnailLease";
|
|
3
3
|
import { useThumbnailStripSize } from "../../hooks/useThumbnailStripSize";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createThumbnailKey,
|
|
6
|
+
type ThumbnailPriority,
|
|
7
|
+
type ThumbnailRequest,
|
|
8
|
+
} from "../lib/thumbnailScheduler";
|
|
5
9
|
import { TIMELINE_VIEWPORT_BUDGETS } from "../lib/timelineViewportBudgets";
|
|
6
10
|
import { computeThumbnailStrip, probeImageAspect } from "./thumbnailUtils";
|
|
7
11
|
|
|
@@ -85,6 +89,22 @@ export function compositionPathOfPreviewUrl(previewUrl: string): string {
|
|
|
85
89
|
return match?.[1] ? decodeURIComponent(match[1]) : "index.html";
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
export function compositionThumbnailRequest(
|
|
93
|
+
url: string,
|
|
94
|
+
projectId: string,
|
|
95
|
+
{ sessionEpoch = 0, priority = "visible", rich = false }: Partial<ThumbnailRequest> = {},
|
|
96
|
+
): ThumbnailRequest {
|
|
97
|
+
return {
|
|
98
|
+
key: createThumbnailKey({ kind: "composition", url }),
|
|
99
|
+
projectId,
|
|
100
|
+
sessionEpoch,
|
|
101
|
+
kind: "composition",
|
|
102
|
+
priority,
|
|
103
|
+
rich,
|
|
104
|
+
load: (signal: AbortSignal) => loadCompositionImage(url, signal),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
async function loadCompositionImage(url: string, signal: AbortSignal) {
|
|
89
109
|
const response = await fetch(url, { signal });
|
|
90
110
|
if (!response.ok) throw new Error(`Composition thumbnail failed (${response.status})`);
|
|
@@ -132,15 +152,7 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
|
|
|
132
152
|
contentRevision,
|
|
133
153
|
});
|
|
134
154
|
const request = useMemo(
|
|
135
|
-
() => ({
|
|
136
|
-
key: createThumbnailKey({ kind: "composition", url }),
|
|
137
|
-
projectId,
|
|
138
|
-
sessionEpoch,
|
|
139
|
-
kind: "composition" as const,
|
|
140
|
-
priority,
|
|
141
|
-
rich: true,
|
|
142
|
-
load: (signal: AbortSignal) => loadCompositionImage(url, signal),
|
|
143
|
-
}),
|
|
155
|
+
() => compositionThumbnailRequest(url, projectId, { sessionEpoch, priority, rich: true }),
|
|
144
156
|
[priority, projectId, sessionEpoch, url],
|
|
145
157
|
);
|
|
146
158
|
const snapshot = useThumbnailLease(request);
|
|
@@ -182,7 +194,7 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
|
|
|
182
194
|
</div>
|
|
183
195
|
)}
|
|
184
196
|
{snapshot.status === "loading" && (
|
|
185
|
-
<div className="absolute inset-0 animate-pulse bg-
|
|
197
|
+
<div className="absolute inset-0 animate-pulse bg-text-0/[0.035]" />
|
|
186
198
|
)}
|
|
187
199
|
{label && (
|
|
188
200
|
<div className="absolute inset-y-0 left-3 z-10 flex items-center">
|