@hyperframes/studio 0.8.104 → 0.8.106
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-DAoaajuM.js → hyperframes-player-dl6K6ABp.js} +1 -1
- package/dist/assets/index-CMiYVGv5.js +399 -0
- package/dist/assets/{index-WV9LroAD.js → index-DKT5NF1Y.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-BDK4rTMb.js → index-ooaZZhKf.js} +1 -1
- package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
- package/dist/chunk-EEXSYY5F.js.map +1 -0
- package/dist/{chunk-HIGHVWYD.js → chunk-ICTZJPQQ.js} +3 -2
- package/dist/chunk-ICTZJPQQ.js.map +1 -0
- package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
- package/dist/chunk-NCUBAVGK.js.map +1 -0
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.html +11 -2
- package/dist/index.js +10391 -9836
- 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 +9 -9
- 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/captions/keyboard.test.ts +6 -0
- package/src/captions/keyboard.ts +2 -1
- 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 +35 -13
- package/src/components/StudioHeader.tsx +6 -2
- package/src/components/StudioToast.tsx +7 -16
- package/src/components/ThemeToggle.tsx +99 -0
- 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 +37 -5
- 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/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/SourceEditor.tsx +2 -6
- package/src/components/editor/TimelineFxPopover.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
- package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
- package/src/components/editor/easeCurveSvg.tsx +1 -1
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
- package/src/components/editor/manualEditsDom.ts +36 -3
- package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
- package/src/components/editor/manualEditsDomPatches.ts +0 -30
- package/src/components/editor/manualEditsSnapshot.ts +1 -0
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/persistSeam.integration.test.ts +0 -18
- package/src/components/editor/plainTranslate.ts +34 -3
- 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.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +71 -48
- 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/propertyPanelHelpers.ts +1 -2
- 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/propertyPanelValueTier.test.ts +1 -1
- package/src/components/editor/propertyPanelValueTier.ts +1 -1
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +31 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +1 -1
- package/src/components/editor/useMotionPathData.test.tsx +64 -0
- package/src/components/editor/useMotionPathData.ts +5 -1
- 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/appHotkeysDispatch.ts +2 -1
- package/src/hooks/elementOffsetStager.ts +8 -7
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.test.ts +32 -0
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useClipboard.source.test.ts +141 -23
- package/src/hooks/useClipboard.ts +154 -123
- package/src/hooks/useDomEditCommits.ts +0 -3
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +7 -43
- package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
- package/src/hooks/useDomGeometryCommit.ts +0 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
- package/src/hooks/useDomGeometryCommits.ts +4 -49
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.ts +16 -2
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +2 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/index.ts +2 -0
- package/src/livePreviewIframeExport.test.tsx +33 -0
- package/src/main.tsx +4 -1
- package/src/player/components/AudioWaveform.test.tsx +36 -15
- package/src/player/components/AudioWaveform.tsx +24 -7
- 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.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +16 -8
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- 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/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/useShadowPreviewReload.ts +72 -5
- package/src/player/lib/automationStoreSync.test.ts +18 -1
- package/src/player/lib/automationStoreSync.ts +29 -17
- 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/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/hex-baseline.json +7 -19
- package/src/styles/studio.css +4 -7
- package/src/styles/tailwind-preset.shared.js +2 -1
- package/src/styles/theme.css +214 -147
- package/src/styles/theme.test.ts +13 -9
- package/src/utils/blockCategories.ts +41 -9
- package/src/utils/clipboardPayload.ts +12 -1
- package/src/utils/studioPendingEdits.ts +21 -0
- package/src/utils/studioTheme.test.tsx +141 -0
- package/src/utils/studioTheme.ts +34 -0
- package/src/utils/studioUiPreferences.ts +3 -0
- package/src/utils/typingTarget.test.ts +8 -1
- package/src/utils/typingTarget.ts +5 -0
- package/dist/assets/index-CQN3M56N.css +0 -1
- package/dist/assets/index-De2aO4XM.js +0 -399
- package/dist/chunk-62DDWOSH.js.map +0 -1
- package/dist/chunk-6GAO75IU.js.map +0 -1
- package/dist/chunk-HIGHVWYD.js.map +0 -1
- package/src/hooks/gsapTargetCache.ts +0 -62
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { readCssRotationTarget, readRotationBase } from "./rotationDraft";
|
|
13
13
|
import {
|
|
14
14
|
beginStudioManualEditGesture,
|
|
15
|
+
endStudioManualEditGesture,
|
|
15
16
|
captureStudioBoxSize,
|
|
16
17
|
captureStudioPathOffset,
|
|
17
18
|
captureStudioRotation,
|
|
@@ -81,48 +82,54 @@ export function startGroupDrag(
|
|
|
81
82
|
}
|
|
82
83
|
members.push(result.member);
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
members.
|
|
90
|
-
member
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
try {
|
|
86
|
+
resetDragMoveLog();
|
|
87
|
+
logDrag("group-start", {
|
|
88
|
+
// A member whose mapping differs from its neighbours travels a different
|
|
89
|
+
// distance for the same pointer delta, which is the group coming apart.
|
|
90
|
+
members: Object.fromEntries(
|
|
91
|
+
members.map((member) => [
|
|
92
|
+
member.key,
|
|
93
|
+
{
|
|
94
|
+
map: `${member.screenToOffset.a.toFixed(3)},${member.screenToOffset.d.toFixed(3)}`,
|
|
95
|
+
base: `${Math.round(member.baseGsap.x)},${Math.round(member.baseGsap.y)}`,
|
|
96
|
+
offset: `${Math.round(member.initialOffset.x)},${Math.round(member.initialOffset.y)}`,
|
|
97
|
+
},
|
|
98
|
+
]),
|
|
99
|
+
),
|
|
100
|
+
at: readDragPositions(members),
|
|
101
|
+
});
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
iframe,
|
|
108
|
-
excludeElements: buildExcludeElements({
|
|
103
|
+
const overlayEl = opts.overlayRef.current;
|
|
104
|
+
const iframe = opts.iframeRef.current;
|
|
105
|
+
const snapContext =
|
|
106
|
+
overlayEl && iframe
|
|
107
|
+
? collectSnapContext({
|
|
108
|
+
overlayEl,
|
|
109
109
|
iframe,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
excludeElements: buildExcludeElements({
|
|
111
|
+
iframe,
|
|
112
|
+
groupSelections: items.map((i) => i.selection),
|
|
113
|
+
}),
|
|
114
|
+
})
|
|
115
|
+
: undefined;
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
120
|
+
opts.rafPausedRef.current = true;
|
|
121
|
+
opts.groupGestureRef.current = {
|
|
122
|
+
startX: e.clientX,
|
|
123
|
+
startY: e.clientY,
|
|
124
|
+
originItems: items,
|
|
125
|
+
members,
|
|
126
|
+
snapContext,
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
opts.groupGestureRef.current = null;
|
|
130
|
+
restoreManualOffsetDragMembers(members);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
126
133
|
return true;
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -219,84 +226,89 @@ export function startGesture(
|
|
|
219
226
|
}
|
|
220
227
|
}
|
|
221
228
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
// A throw before the gesture is armed would leave its mark, and the mark holds every reload.
|
|
230
|
+
try {
|
|
231
|
+
// Rotation base: the angle the element shows. An element GSAP does not turn, or a plain-translate
|
|
232
|
+
// move, never asks GSAP: reading a property makes it bake the CSS into its transform.
|
|
233
|
+
const plain = !!pathOffsetMember?.plainTranslate || !gsapWritesRotation(sel.element);
|
|
234
|
+
const plainRotation = plain && kind === "rotate" ? readCssRotationTarget(sel.element) : null;
|
|
235
|
+
const rotation = { angle: readRotationBase(sel.element, plain) };
|
|
236
|
+
const overlayBounds = overlayEl?.getBoundingClientRect();
|
|
237
|
+
const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
|
|
238
|
+
const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
|
|
230
239
|
|
|
231
|
-
|
|
240
|
+
const iframe = opts.iframeRef.current;
|
|
232
241
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
242
|
+
// A corner resize pins the centroid of the four rotation-aware corners: an axis-aligned
|
|
243
|
+
// size delta only holds the centre still for a symmetric, unrotated growth.
|
|
244
|
+
let resizeFixedCenterStart: { x: number; y: number } | undefined;
|
|
245
|
+
if (kind === "resize" && pathOffsetMember && overlayEl && iframe) {
|
|
246
|
+
const corners = elementCornerOverlayPoints(overlayEl, iframe, sel.element);
|
|
247
|
+
if (corners) resizeFixedCenterStart = overlayCornersCentroid(corners);
|
|
248
|
+
}
|
|
249
|
+
const snapContext =
|
|
250
|
+
(kind === "drag" || kind === "resize") && overlayEl && iframe
|
|
251
|
+
? collectSnapContext({
|
|
252
|
+
overlayEl,
|
|
253
|
+
iframe,
|
|
254
|
+
excludeElements: buildExcludeElements({ iframe, selection: sel }),
|
|
255
|
+
})
|
|
256
|
+
: undefined;
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
e.stopPropagation();
|
|
259
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
260
|
+
opts.rafPausedRef.current = true;
|
|
261
|
+
opts.gestureRef.current = {
|
|
262
|
+
kind,
|
|
263
|
+
mode,
|
|
264
|
+
selection: sel,
|
|
265
|
+
startX: e.clientX,
|
|
266
|
+
startY: e.clientY,
|
|
267
|
+
centerX,
|
|
268
|
+
centerY,
|
|
269
|
+
initialPathOffset,
|
|
270
|
+
initialRotation: captureStudioRotation(sel.element),
|
|
271
|
+
initialBoxSize: captureStudioBoxSize(sel.element),
|
|
272
|
+
pathOffsetMember,
|
|
273
|
+
originLeft: rect.left,
|
|
274
|
+
originTop: rect.top,
|
|
275
|
+
originWidth: rect.width,
|
|
276
|
+
originHeight: rect.height,
|
|
277
|
+
actualWidth,
|
|
278
|
+
actualHeight,
|
|
279
|
+
actualRotation: rotation.angle,
|
|
280
|
+
plainRotation,
|
|
281
|
+
editScaleX: rect.editScaleX,
|
|
282
|
+
editScaleY: rect.editScaleY,
|
|
283
|
+
contentScaleX,
|
|
284
|
+
contentScaleY,
|
|
285
|
+
manualEditDragToken,
|
|
286
|
+
snapContext,
|
|
287
|
+
resizeHandle: kind === "resize" ? (options?.resizeHandle ?? "se") : undefined,
|
|
288
|
+
resizeFixedCenterStart,
|
|
289
|
+
};
|
|
290
|
+
if (kind === "resize") {
|
|
291
|
+
resetResizeMoveLog();
|
|
292
|
+
logResize("start", {
|
|
293
|
+
handle: options?.resizeHandle ?? "se",
|
|
294
|
+
pointer: { x: e.clientX, y: e.clientY },
|
|
295
|
+
center: { x: centerX, y: centerY },
|
|
296
|
+
origin: { left: rect.left, top: rect.top, w: rect.width, h: rect.height },
|
|
297
|
+
actual: { w: actualWidth, h: actualHeight },
|
|
298
|
+
editScale: { x: rect.editScaleX, y: rect.editScaleY },
|
|
299
|
+
contentScale: { x: contentScaleX, y: contentScaleY },
|
|
300
|
+
rotation: rotation.angle,
|
|
301
|
+
hasOffsetMember: !!pathOffsetMember,
|
|
302
|
+
fixedCenterStart: resizeFixedCenterStart ?? null,
|
|
303
|
+
initialBoxSize: opts.gestureRef.current?.initialBoxSize ?? null,
|
|
304
|
+
initialInlineStyle: sel.element.getAttribute("style"),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
opts.gestureRef.current = null;
|
|
309
|
+
if (pathOffsetMember) restoreManualOffsetDragMembers([pathOffsetMember]);
|
|
310
|
+
else if (manualEditDragToken) endStudioManualEditGesture(sel.element, manualEditDragToken);
|
|
311
|
+
throw error;
|
|
300
312
|
}
|
|
301
313
|
return true;
|
|
302
314
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { makeSelection } from "../../hooks/domSelectionTestHarness";
|
|
7
|
+
import { useInlineTextEdit } from "../../hooks/useInlineTextEdit";
|
|
8
|
+
import {
|
|
9
|
+
makePreview,
|
|
10
|
+
mountPlayerWithPreview,
|
|
11
|
+
paintShadow,
|
|
12
|
+
resetPlayerStore,
|
|
13
|
+
type TimelinePlayerApi,
|
|
14
|
+
} from "../../player/hooks/timelinePlayerTestHarness";
|
|
15
|
+
import type { DomEditSelection } from "./domEditing";
|
|
16
|
+
import "./domEditOverlayTestMocks";
|
|
17
|
+
import { DomEditOverlay } from "./DomEditOverlay";
|
|
18
|
+
import { PreviewReadOnlyProvider } from "./previewReadOnlyContext";
|
|
19
|
+
import { STUDIO_MANUAL_EDIT_GESTURE_ATTR } from "./manualEditsTypes";
|
|
20
|
+
|
|
21
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
22
|
+
|
|
23
|
+
const layout = vi.hoisted(() => ({
|
|
24
|
+
group: [] as unknown[],
|
|
25
|
+
rect: { left: 100, top: 100, width: 200, height: 100, editScaleX: 1, editScaleY: 1 },
|
|
26
|
+
}));
|
|
27
|
+
vi.mock("./useDomEditOverlayRects", () => ({
|
|
28
|
+
useDomEditOverlayRects: () => {
|
|
29
|
+
const { rect, group } = layout;
|
|
30
|
+
const groupOverlayItemsRef = { current: group };
|
|
31
|
+
const noop = () => undefined;
|
|
32
|
+
return {
|
|
33
|
+
overlayRect: rect,
|
|
34
|
+
overlayRectRef: { current: rect },
|
|
35
|
+
groupOverlayItems: group,
|
|
36
|
+
groupOverlayItemsRef,
|
|
37
|
+
hoverRect: null,
|
|
38
|
+
childRects: [],
|
|
39
|
+
setOverlayRect: noop,
|
|
40
|
+
setGroupOverlayItems: noop,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
vi.mock("../../utils/gsapSoftReload", async (importOriginal) => ({
|
|
45
|
+
...(await importOriginal<typeof import("../../utils/gsapSoftReload")>()),
|
|
46
|
+
ensureMotionPathPluginLoaded: vi.fn(),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const BOX = '[data-dom-edit-selection-box="true"]';
|
|
50
|
+
const RECT = layout.rect;
|
|
51
|
+
const TWO_LAYERS = '<h1 id="title">Title</h1><p id="sub">Sub</p>';
|
|
52
|
+
|
|
53
|
+
/** What the server serves: the file as the last commit left it. */
|
|
54
|
+
let file = { title: "", sub: "" };
|
|
55
|
+
const served = (query: string) =>
|
|
56
|
+
makePreview(
|
|
57
|
+
`<h1 id="title" style="${file.title}">Title</h1><p id="sub" style="${file.sub}">Sub</p>`,
|
|
58
|
+
query,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
let overlayRoot: Root;
|
|
62
|
+
let player: ReturnType<typeof mountPlayerWithPreview>;
|
|
63
|
+
|
|
64
|
+
const marked = (doc: Document) => doc.querySelectorAll(`[${STUDIO_MANUAL_EDIT_GESTURE_ATTR}]`);
|
|
65
|
+
const byId = (iframe: HTMLIFrameElement | null, id: string) =>
|
|
66
|
+
iframe!.contentDocument!.getElementById(id) as HTMLElement;
|
|
67
|
+
const api = (): TimelinePlayerApi => player.getApi();
|
|
68
|
+
|
|
69
|
+
function pointer(target: Element, type: string, x: number, y: number) {
|
|
70
|
+
const init = { bubbles: true, cancelable: true, button: 0, pointerId: 1, clientX: x, clientY: y };
|
|
71
|
+
act(() => void target.dispatchEvent(new PointerEvent(type, init)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The real player with a two-layer preview, and the real overlay editing it. */
|
|
75
|
+
function mountEditor(group: boolean) {
|
|
76
|
+
file = { title: "", sub: "" };
|
|
77
|
+
const live = makePreview(TWO_LAYERS);
|
|
78
|
+
player = mountPlayerWithPreview(live);
|
|
79
|
+
const selections = ["title", "sub"].map((id) => makeSelection(id, byId(live, id)));
|
|
80
|
+
layout.group = group
|
|
81
|
+
? selections.map((s) => ({ key: s.id, selection: s, element: s.element, rect: RECT }))
|
|
82
|
+
: [];
|
|
83
|
+
const write = (sel: DomEditSelection, next: { x: number; y: number }) => {
|
|
84
|
+
file = { ...file, [sel.id!]: `translate: ${next.x}px ${next.y}px` };
|
|
85
|
+
return Promise.resolve();
|
|
86
|
+
};
|
|
87
|
+
const onPathOffsetCommit = vi.fn(write);
|
|
88
|
+
const onGroupPathOffsetCommit = vi.fn(
|
|
89
|
+
async (updates: { selection: DomEditSelection; next: { x: number; y: number } }[]) => {
|
|
90
|
+
for (const { selection, next } of updates) await write(selection, next);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
const host = document.body.appendChild(document.createElement("div"));
|
|
94
|
+
overlayRoot = createRoot(host);
|
|
95
|
+
const render = (readOnly: boolean) =>
|
|
96
|
+
act(() =>
|
|
97
|
+
overlayRoot.render(
|
|
98
|
+
<PreviewReadOnlyProvider readOnly={readOnly}>
|
|
99
|
+
<DomEditOverlay
|
|
100
|
+
iframeRef={api().iframeRef}
|
|
101
|
+
activeCompositionPath={null}
|
|
102
|
+
selection={group ? null : selections[0]!}
|
|
103
|
+
groupSelections={group ? selections : []}
|
|
104
|
+
hoverSelection={null}
|
|
105
|
+
onCanvasMouseDown={() => undefined}
|
|
106
|
+
onCanvasPointerMove={() => Promise.resolve(null)}
|
|
107
|
+
onCanvasPointerLeave={() => undefined}
|
|
108
|
+
onSelectionChange={() => undefined}
|
|
109
|
+
onBlockedMove={() => undefined}
|
|
110
|
+
onPathOffsetCommit={onPathOffsetCommit}
|
|
111
|
+
onGroupPathOffsetCommit={onGroupPathOffsetCommit}
|
|
112
|
+
onBoxSizeCommit={() => undefined}
|
|
113
|
+
onRotationCommit={() => undefined}
|
|
114
|
+
/>
|
|
115
|
+
</PreviewReadOnlyProvider>,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
render(false);
|
|
119
|
+
const overlay = host.querySelector('[aria-label="Composition canvas"]') as HTMLElement;
|
|
120
|
+
return { live, overlay, box: overlay.querySelector(BOX)!, onPathOffsetCommit, render };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A reload whose shadow paints while the gesture is still live. */
|
|
124
|
+
async function reloadMidGesture() {
|
|
125
|
+
act(() => api().refreshPlayer());
|
|
126
|
+
const shadow = served("?_t=1");
|
|
127
|
+
const gen = await paintShadow(api, shadow);
|
|
128
|
+
return { shadow, gen };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function settle() {
|
|
132
|
+
await act(async () => {
|
|
133
|
+
for (let tick = 0; tick < 10; tick++) await Promise.resolve();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
HTMLElement.prototype.setPointerCapture = () => undefined;
|
|
139
|
+
HTMLElement.prototype.releasePointerCapture = () => undefined;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
act(() => overlayRoot?.unmount());
|
|
144
|
+
act(() => player?.root.unmount());
|
|
145
|
+
layout.group = [];
|
|
146
|
+
document.body.innerHTML = "";
|
|
147
|
+
resetPlayerStore();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("a reload during a drag", () => {
|
|
151
|
+
it.each([
|
|
152
|
+
["one layer", false],
|
|
153
|
+
["a group", true],
|
|
154
|
+
])(
|
|
155
|
+
"that paints before the drop of %s waits, then shows the drop from a fresh load",
|
|
156
|
+
async (_, group) => {
|
|
157
|
+
const { live, overlay, box } = mountEditor(group);
|
|
158
|
+
pointer(box, "pointerdown", 150, 150);
|
|
159
|
+
pointer(overlay, "pointermove", 170, 160);
|
|
160
|
+
pointer(overlay, "pointermove", 190, 170);
|
|
161
|
+
const held = await reloadMidGesture();
|
|
162
|
+
expect(api().iframeRef.current, "promoted under the pointer").toBe(live);
|
|
163
|
+
|
|
164
|
+
pointer(overlay, "pointerup", 190, 170);
|
|
165
|
+
await settle();
|
|
166
|
+
expect(byId(live, "title").style.getPropertyValue("translate")).toBe("40px 20px");
|
|
167
|
+
expect(file.title).toBe("translate: 40px 20px");
|
|
168
|
+
if (group) expect(file.sub).toBe("translate: 40px 20px");
|
|
169
|
+
expect(marked(live.contentDocument!)).toHaveLength(0);
|
|
170
|
+
expect(api().iframeRef.current, "the shadow loaded before the drop").toBe(live);
|
|
171
|
+
|
|
172
|
+
const fresh = served("?_t=2");
|
|
173
|
+
expect(await paintShadow(api, fresh)).toBeGreaterThan(held.gen);
|
|
174
|
+
expect(api().iframeRef.current).toBe(fresh);
|
|
175
|
+
expect(byId(fresh, "title").style.getPropertyValue("translate")).toBe("40px 20px");
|
|
176
|
+
if (group) expect(byId(fresh, "sub").style.getPropertyValue("translate")).toBe("40px 20px");
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
it("that paints after the drop shows the drop from a fresh load, not the file before it", async () => {
|
|
181
|
+
const { live, overlay, box } = mountEditor(false);
|
|
182
|
+
pointer(box, "pointerdown", 150, 150);
|
|
183
|
+
pointer(overlay, "pointermove", 190, 170);
|
|
184
|
+
act(() => api().refreshPlayer());
|
|
185
|
+
const beforeDrop = served("?_t=1");
|
|
186
|
+
pointer(overlay, "pointerup", 190, 170);
|
|
187
|
+
await settle();
|
|
188
|
+
expect(file.title).toBe("translate: 40px 20px");
|
|
189
|
+
|
|
190
|
+
const requested = await paintShadow(api, beforeDrop);
|
|
191
|
+
expect(api().iframeRef.current, "the file before the drop").toBe(live);
|
|
192
|
+
const fresh = served("?_t=2");
|
|
193
|
+
expect(await paintShadow(api, fresh)).toBeGreaterThan(requested);
|
|
194
|
+
expect(byId(fresh, "title").style.getPropertyValue("translate")).toBe("40px 20px");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
type Editor = ReturnType<typeof mountEditor>;
|
|
199
|
+
|
|
200
|
+
describe("every way a drag ends without a drop clears its mark and promotes the held reload", () => {
|
|
201
|
+
it.each([
|
|
202
|
+
["pointercancel", (e: Editor) => pointer(e.overlay, "pointercancel", 0, 0)],
|
|
203
|
+
["lostpointercapture", (e: Editor) => pointer(e.box, "lostpointercapture", 0, 0)],
|
|
204
|
+
["window blur", () => act(() => void window.dispatchEvent(new Event("blur")))],
|
|
205
|
+
["a switch to read-only", (e: Editor) => e.render(true)],
|
|
206
|
+
["overlay unmount", () => act(() => overlayRoot.unmount())],
|
|
207
|
+
])("%s", async (_, end) => {
|
|
208
|
+
const editor = mountEditor(false);
|
|
209
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
210
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
211
|
+
expect(marked(editor.live.contentDocument!)).toHaveLength(1);
|
|
212
|
+
const held = await reloadMidGesture();
|
|
213
|
+
expect(api().iframeRef.current).toBe(editor.live);
|
|
214
|
+
|
|
215
|
+
end(editor);
|
|
216
|
+
await settle();
|
|
217
|
+
expect(marked(editor.live.contentDocument!)).toHaveLength(0);
|
|
218
|
+
expect(byId(editor.live, "title").style.getPropertyValue("translate")).not.toBe("40px 20px");
|
|
219
|
+
expect(editor.onPathOffsetCommit).not.toHaveBeenCalled();
|
|
220
|
+
expect(api().iframeRef.current, "nothing was saved, so the held copy is current").toBe(
|
|
221
|
+
held.shadow,
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("a press that throws before the drag is armed leaves no mark behind", async () => {
|
|
226
|
+
const editor = mountEditor(false);
|
|
227
|
+
HTMLElement.prototype.setPointerCapture = () => {
|
|
228
|
+
throw new Error("capture refused");
|
|
229
|
+
};
|
|
230
|
+
const swallow = (event: ErrorEvent) => event.preventDefault();
|
|
231
|
+
window.addEventListener("error", swallow);
|
|
232
|
+
try {
|
|
233
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
234
|
+
} catch {
|
|
235
|
+
// React may rethrow the handler's error; either way the mark must be gone.
|
|
236
|
+
} finally {
|
|
237
|
+
window.removeEventListener("error", swallow);
|
|
238
|
+
}
|
|
239
|
+
expect(marked(editor.live.contentDocument!)).toHaveLength(0);
|
|
240
|
+
const { shadow } = await reloadMidGesture();
|
|
241
|
+
expect(api().iframeRef.current).toBe(shadow);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("an inline text edit", () => {
|
|
246
|
+
it("holds a reload while open, and the reload after it shows the saved text", async () => {
|
|
247
|
+
const live = makePreview(TWO_LAYERS);
|
|
248
|
+
player = mountPlayerWithPreview(live);
|
|
249
|
+
let land: () => void = () => {};
|
|
250
|
+
const onCommit = vi.fn(() => new Promise<void>((resolve) => (land = resolve)));
|
|
251
|
+
let controls: ReturnType<typeof useInlineTextEdit> | null = null;
|
|
252
|
+
function Editor() {
|
|
253
|
+
controls = useInlineTextEdit({ onCommit });
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
overlayRoot = createRoot(document.body.appendChild(document.createElement("div")));
|
|
257
|
+
act(() => overlayRoot.render(<Editor />));
|
|
258
|
+
act(() => void controls!.start(byId(live, "title")));
|
|
259
|
+
const held = await reloadMidGesture();
|
|
260
|
+
expect(api().iframeRef.current).toBe(live);
|
|
261
|
+
|
|
262
|
+
act(() => controls!.commit());
|
|
263
|
+
await settle();
|
|
264
|
+
expect(onCommit).toHaveBeenCalledTimes(1);
|
|
265
|
+
expect(marked(live.contentDocument!), "closed, though the save is still out").toHaveLength(0);
|
|
266
|
+
expect(api().iframeRef.current, "the held copy predates the text").toBe(live);
|
|
267
|
+
expect(api().previewSlots.find((slot) => slot.role === "shadow")?.gen).toBe(held.gen);
|
|
268
|
+
|
|
269
|
+
await act(async () => land());
|
|
270
|
+
await settle();
|
|
271
|
+
const afterSave = served("?_t=2");
|
|
272
|
+
expect(await paintShadow(api, afterSave)).toBeGreaterThan(held.gen);
|
|
273
|
+
expect(api().iframeRef.current).toBe(afterSave);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -46,9 +46,42 @@ export function beginStudioManualEditGesture(element: HTMLElement): string {
|
|
|
46
46
|
return token;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
const GESTURE_ENDED = "hf-manual-edit-gesture-ended";
|
|
50
|
+
|
|
49
51
|
export function endStudioManualEditGesture(element: HTMLElement, token?: string): void {
|
|
50
52
|
if (token && element.getAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR) !== token) return;
|
|
53
|
+
if (!element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR)) return;
|
|
51
54
|
element.removeAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
|
|
55
|
+
const doc = element.ownerDocument;
|
|
56
|
+
doc.dispatchEvent(new (doc.defaultView?.Event ?? Event)(GESTURE_ENDED));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isStudioManualEditGestureLiveIn(doc: Document): boolean {
|
|
60
|
+
return doc.querySelector(`[${STUDIO_MANUAL_EDIT_GESTURE_ATTR}]`) !== null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Runs `run` once the last gesture in `doc` ends; the returned function stops waiting. */
|
|
64
|
+
export function afterStudioManualEditGestures(doc: Document, run: () => void): () => void {
|
|
65
|
+
const onEnded = () => {
|
|
66
|
+
if (isStudioManualEditGestureLiveIn(doc)) return;
|
|
67
|
+
stop();
|
|
68
|
+
run();
|
|
69
|
+
};
|
|
70
|
+
const stop = () => doc.removeEventListener(GESTURE_ENDED, onEnded);
|
|
71
|
+
doc.addEventListener(GESTURE_ENDED, onEnded);
|
|
72
|
+
return stop;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const gestureSaves = new WeakMap<Document, number>();
|
|
76
|
+
|
|
77
|
+
export function studioManualEditSavesIn(doc: Document): number {
|
|
78
|
+
return gestureSaves.get(doc) ?? 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A gesture's save in `element`'s document: a reload requested before it shows the file before it. */
|
|
82
|
+
export function noteStudioManualEditSave(element: HTMLElement): void {
|
|
83
|
+
const doc = element.ownerDocument;
|
|
84
|
+
gestureSaves.set(doc, studioManualEditSavesIn(doc) + 1);
|
|
52
85
|
}
|
|
53
86
|
|
|
54
87
|
function isStudioManualEditGestureActive(element: HTMLElement): boolean {
|
|
@@ -253,9 +286,9 @@ function stripGsapTranslateFromTransform(element: HTMLElement): void {
|
|
|
253
286
|
// — as the non-GSAP path does — composes ON TOP of GSAP's transform, and the
|
|
254
287
|
// subsequent strip/reapply math compounds into a runaway matrix that flings the
|
|
255
288
|
// element off-canvas. So for GSAP-animated elements we keep `translate: none`
|
|
256
|
-
// and push the offset straight into GSAP's x/y via gsap.set; the var() offset
|
|
257
|
-
//
|
|
258
|
-
//
|
|
289
|
+
// and push the offset straight into GSAP's x/y via gsap.set; the var() offset an
|
|
290
|
+
// older Studio saved stays in the file, and GSAP re-reads it at init on reload.
|
|
291
|
+
// Returns true when handled as GSAP (caller must skip the CSS path).
|
|
259
292
|
// fallow-ignore-next-line complexity
|
|
260
293
|
function applyStudioPathOffsetViaGsap(
|
|
261
294
|
element: HTMLElement,
|