@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
|
@@ -38,7 +38,7 @@ describe("PropertyPanelFlatHeader", () => {
|
|
|
38
38
|
expect(host.textContent).toContain("Mono Label");
|
|
39
39
|
expect(host.textContent).toContain(".mono-label · div");
|
|
40
40
|
const icon = host.querySelector('[data-flat-header-icon="true"]');
|
|
41
|
-
expect(icon?.className).toContain("text-
|
|
41
|
+
expect(icon?.className).toContain("text-accent-ink");
|
|
42
42
|
act(() => root.unmount());
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -4,7 +4,7 @@ import { ClipboardList, Film, Square, Type, X } from "../../icons/SystemIcons";
|
|
|
4
4
|
|
|
5
5
|
const ICON_BY_KIND = { text: Type, media: Film, other: Square } as const;
|
|
6
6
|
const ICON_COLOR_BY_KIND = {
|
|
7
|
-
text: "text-
|
|
7
|
+
text: "text-accent-ink",
|
|
8
8
|
media: "text-panel-media",
|
|
9
9
|
other: "text-panel-container",
|
|
10
10
|
} as const;
|
|
@@ -92,7 +92,7 @@ export function PropertyPanelFlatHeader({
|
|
|
92
92
|
track("button", "Copy element info");
|
|
93
93
|
onCopy();
|
|
94
94
|
}}
|
|
95
|
-
className={copied ? "text-
|
|
95
|
+
className={copied ? "text-accent-ink" : undefined}
|
|
96
96
|
>
|
|
97
97
|
<ClipboardList size={13} />
|
|
98
98
|
</button>
|
|
@@ -73,7 +73,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
|
|
|
73
73
|
type="button"
|
|
74
74
|
className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
|
|
75
75
|
motionPathArmed
|
|
76
|
-
? "bg-studio-accent/20 text-
|
|
76
|
+
? "bg-studio-accent/20 text-accent-ink"
|
|
77
77
|
: "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
|
|
78
78
|
}`}
|
|
79
79
|
onClick={() => setMotionPathArmed(!motionPathArmed)}
|
|
@@ -99,7 +99,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
|
|
|
99
99
|
type="button"
|
|
100
100
|
className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
|
|
101
101
|
visible
|
|
102
|
-
? "bg-studio-accent/20 text-
|
|
102
|
+
? "bg-studio-accent/20 text-accent-ink"
|
|
103
103
|
: "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
|
|
104
104
|
}`}
|
|
105
105
|
onClick={toggle}
|
|
@@ -115,7 +115,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
|
|
|
115
115
|
type="button"
|
|
116
116
|
className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
|
|
117
117
|
prefs.snapEnabled
|
|
118
|
-
? "bg-studio-accent/20 text-
|
|
118
|
+
? "bg-studio-accent/20 text-accent-ink"
|
|
119
119
|
: "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
|
|
120
120
|
}`}
|
|
121
121
|
onClick={toggleSnap}
|
|
@@ -131,7 +131,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
|
|
|
131
131
|
type="button"
|
|
132
132
|
className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
|
|
133
133
|
prefs.gridVisible
|
|
134
|
-
? "bg-studio-accent/20 text-
|
|
134
|
+
? "bg-studio-accent/20 text-accent-ink"
|
|
135
135
|
: "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
|
|
136
136
|
}`}
|
|
137
137
|
onClick={toggleGrid}
|
|
@@ -164,7 +164,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
|
|
|
164
164
|
{gridPopoverOpen && (
|
|
165
165
|
<div
|
|
166
166
|
ref={popoverRef}
|
|
167
|
-
className="absolute right-0 top-full mt-1 rounded-lg bg-
|
|
167
|
+
className="absolute right-0 top-full mt-1 rounded-lg bg-raised border border-border p-3 shadow-xl min-w-[180px]"
|
|
168
168
|
>
|
|
169
169
|
<label className="flex items-center justify-between text-xs text-white/80 mb-2">
|
|
170
170
|
<span>Grid spacing</span>
|
|
@@ -139,7 +139,7 @@ export function TimelineFxPopover({
|
|
|
139
139
|
ref={rootRef}
|
|
140
140
|
role="dialog"
|
|
141
141
|
aria-label="Effects"
|
|
142
|
-
className="z-200 flex flex-col overflow-hidden rounded-md border border-
|
|
142
|
+
className="z-200 flex flex-col overflow-hidden rounded-md border border-border bg-raised p-2 shadow-xl"
|
|
143
143
|
style={clampedStyle(anchorRect)}
|
|
144
144
|
onKeyDown={onKeyDown}
|
|
145
145
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -165,10 +165,10 @@ export function TimelineFxPopover({
|
|
|
165
165
|
onAuditionTracked={(id) => trackPresetAuditioned(id, { trackKind })}
|
|
166
166
|
/>
|
|
167
167
|
</div>
|
|
168
|
-
<div className="mt-2 flex shrink-0 items-center justify-between border-t border-
|
|
168
|
+
<div className="mt-2 flex shrink-0 items-center justify-between border-t border-border pt-2 text-[10px] text-text-muted">
|
|
169
169
|
<button
|
|
170
170
|
type="button"
|
|
171
|
-
className="hover:text-
|
|
171
|
+
className="hover:text-text-0"
|
|
172
172
|
onClick={() => {
|
|
173
173
|
onClose();
|
|
174
174
|
onOpenRack();
|
|
@@ -178,7 +178,7 @@ export function TimelineFxPopover({
|
|
|
178
178
|
</button>
|
|
179
179
|
<button
|
|
180
180
|
type="button"
|
|
181
|
-
className="hover:text-
|
|
181
|
+
className="hover:text-text-0"
|
|
182
182
|
onClick={() => {
|
|
183
183
|
onClose();
|
|
184
184
|
onOpenRack();
|
|
@@ -314,7 +314,7 @@ export function Transform3DCube({
|
|
|
314
314
|
aria-label="Keyframe 3D transform"
|
|
315
315
|
aria-pressed={keyframed}
|
|
316
316
|
className={`absolute left-1.5 top-1.5 rounded p-0.5 hover:bg-neutral-800 ${
|
|
317
|
-
keyframed ? "text-
|
|
317
|
+
keyframed ? "text-accent-ink" : "text-neutral-500 hover:text-neutral-200"
|
|
318
318
|
}`}
|
|
319
319
|
>
|
|
320
320
|
<svg
|
|
@@ -16,23 +16,49 @@ const ORIGIN_CENTER = {
|
|
|
16
16
|
|
|
17
17
|
// Consistent geometry stub: model the physical truth the real DOM would report.
|
|
18
18
|
// A CSS width/height change grows the box from its top-left, so the rendered
|
|
19
|
-
// center drifts by half the size delta; the
|
|
20
|
-
//
|
|
19
|
+
// center drifts by half the size delta; the element's own translate, which the gesture
|
|
20
|
+
// writes in plain px, pulls it back. `elementCornerOverlayPoints`
|
|
21
21
|
// returns the four corners of that drifted box; `overlayCornersCentroid` (kept
|
|
22
22
|
// real) averages them so the anchor loop can measure the true center each frame.
|
|
23
|
+
// An authored `translate: 25% 25%` follows the box size until the gesture writes px over it.
|
|
24
|
+
const authored = vi.hoisted(() => ({ percent: 0 }));
|
|
25
|
+
function inlineTranslate(element: HTMLElement): { x: number; y: number } | null {
|
|
26
|
+
const value = element.style.getPropertyValue("translate");
|
|
27
|
+
if (!value) return null;
|
|
28
|
+
const [x = 0, y = 0] = value.split(" ").map((v) => Number.parseFloat(v) || 0);
|
|
29
|
+
return { x, y };
|
|
30
|
+
}
|
|
31
|
+
function renderedTranslate(element: HTMLElement, width: number, height: number) {
|
|
32
|
+
return inlineTranslate(element) ?? { x: authored.percent * width, y: authored.percent * height };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
vi.mock("./plainTranslate", async () => {
|
|
36
|
+
const actual = await vi.importActual<typeof import("./plainTranslate")>("./plainTranslate");
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
readTranslatePx: (element: HTMLElement) =>
|
|
40
|
+
renderedTranslate(
|
|
41
|
+
element,
|
|
42
|
+
element.offsetWidth || ORIGIN.width,
|
|
43
|
+
element.offsetHeight || ORIGIN.height,
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
23
48
|
vi.mock("./domEditOverlayGeometry", async () => {
|
|
24
49
|
const actual = await vi.importActual<typeof import("./domEditOverlayGeometry")>(
|
|
25
50
|
"./domEditOverlayGeometry",
|
|
26
51
|
);
|
|
27
|
-
const { readStudioBoxSize
|
|
52
|
+
const { readStudioBoxSize } = await import("./manualEditsDom");
|
|
28
53
|
const physicalCenter = (element: HTMLElement) => {
|
|
29
54
|
const size = readStudioBoxSize(element);
|
|
30
55
|
const width = size.width > 0 ? size.width : ORIGIN.width;
|
|
31
56
|
const height = size.height > 0 ? size.height : ORIGIN.height;
|
|
32
|
-
const offset =
|
|
57
|
+
const offset = renderedTranslate(element, width, height);
|
|
58
|
+
const start = { x: authored.percent * ORIGIN.width, y: authored.percent * ORIGIN.height };
|
|
33
59
|
return {
|
|
34
|
-
x: ORIGIN_CENTER.x + (width - ORIGIN.width) / 2 + offset.x,
|
|
35
|
-
y: ORIGIN_CENTER.y + (height - ORIGIN.height) / 2 + offset.y,
|
|
60
|
+
x: ORIGIN_CENTER.x + (width - ORIGIN.width) / 2 + offset.x - start.x,
|
|
61
|
+
y: ORIGIN_CENTER.y + (height - ORIGIN.height) / 2 + offset.y - start.y,
|
|
36
62
|
width,
|
|
37
63
|
height,
|
|
38
64
|
};
|
|
@@ -189,6 +215,7 @@ async function finishResize(handlers: ReturnType<typeof createDomEditOverlayGest
|
|
|
189
215
|
|
|
190
216
|
afterEach(() => {
|
|
191
217
|
document.body.innerHTML = "";
|
|
218
|
+
authored.percent = 0;
|
|
192
219
|
});
|
|
193
220
|
|
|
194
221
|
describe("anchored corner resize — the release commit feeds the center-pin offset", () => {
|
|
@@ -227,6 +254,19 @@ describe("anchored corner resize — the release commit feeds the center-pin off
|
|
|
227
254
|
expect(offset.y).toBeCloseTo(-(size.height - ORIGIN.height) / 2, 0);
|
|
228
255
|
});
|
|
229
256
|
|
|
257
|
+
it("keeps the centre on the first frame when the authored translate is a percent", async () => {
|
|
258
|
+
const { orientedOverlayRect } = await import("./domEditOverlayGeometry");
|
|
259
|
+
authored.percent = 0.25;
|
|
260
|
+
const { handlers, selection } = buildHarness();
|
|
261
|
+
handlers.startGesture("resize", evt(ORIGIN_CENTER.x + 100, ORIGIN_CENTER.y), {
|
|
262
|
+
resizeHandle: "se",
|
|
263
|
+
});
|
|
264
|
+
handlers.onPointerMove(evt(ORIGIN_CENTER.x + 150, ORIGIN_CENTER.y));
|
|
265
|
+
const rect = orientedOverlayRect(null as never, null as never, selection.element)!;
|
|
266
|
+
expect(rect.left + rect.width / 2).toBeCloseTo(ORIGIN_CENTER.x, 1);
|
|
267
|
+
expect(rect.top + rect.height / 2).toBeCloseTo(ORIGIN_CENTER.y, 1);
|
|
268
|
+
});
|
|
269
|
+
|
|
230
270
|
it("does not log a paused save queue as an ordinary resize failure", async () => {
|
|
231
271
|
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
232
272
|
const { handlers } = buildHarness(() => Promise.reject(new DomEditSaveQueueOpenError()));
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { applyStudioBoxSize, applyStudioPathOffset, readStudioBoxSize } from "./manualEditsDom";
|
|
4
|
-
import { reapplyPositionEditsAfterSeek } from "./manualEditsSeekReapply";
|
|
5
|
-
import { buildBoxSizePatches, buildPathOffsetPatches } from "./manualEditsDomPatches";
|
|
6
|
-
import { createManualOffsetDragMember, applyManualOffsetDragCommit } from "./manualOffsetDrag";
|
|
7
3
|
import { computeNextResizeAnchor } from "./domEditResizeLocal";
|
|
8
|
-
import type { PatchOperation } from "../../utils/sourcePatcher";
|
|
9
|
-
import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers";
|
|
10
4
|
|
|
11
5
|
/**
|
|
12
6
|
* Center-anchored corner resize (CapCut model): the element scales about its
|
|
@@ -38,40 +32,6 @@ afterEach(() => {
|
|
|
38
32
|
document.body.innerHTML = "";
|
|
39
33
|
});
|
|
40
34
|
|
|
41
|
-
/** Apply a built PatchOperation[] to a live element, mirroring sourcePatcher's
|
|
42
|
-
* inline-style / attribute application — i.e. what the persisted source carries
|
|
43
|
-
* when it is re-parsed into the DOM on the next preview load. */
|
|
44
|
-
function applyPatchesToElement(el: HTMLElement, ops: PatchOperation[]): void {
|
|
45
|
-
for (const op of ops) {
|
|
46
|
-
if (op.type === "inline-style") {
|
|
47
|
-
if (op.value === null) el.style.removeProperty(op.property);
|
|
48
|
-
else el.style.setProperty(op.property, op.value);
|
|
49
|
-
} else if (op.type === "attribute") {
|
|
50
|
-
if (op.value === null) el.removeAttribute(op.property);
|
|
51
|
-
else el.setAttribute(op.property, op.value);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Net translate applied to an element, resolving the studio offset var()
|
|
57
|
-
* expression to its px value so we compare the actually-rendered translation. */
|
|
58
|
-
function resolvedTranslatePx(el: HTMLElement): { x: number; y: number } {
|
|
59
|
-
const raw = el.style.getPropertyValue("translate").trim();
|
|
60
|
-
if (!raw || raw === "none") return { x: 0, y: 0 };
|
|
61
|
-
const vx = Number.parseFloat(el.style.getPropertyValue("--hf-studio-offset-x")) || 0;
|
|
62
|
-
const vy = Number.parseFloat(el.style.getPropertyValue("--hf-studio-offset-y")) || 0;
|
|
63
|
-
const parts = splitTopLevelWhitespace(raw);
|
|
64
|
-
const parseAxis = (part: string, varVal: number): number => {
|
|
65
|
-
if (part && part.includes("--hf-studio-offset")) return varVal;
|
|
66
|
-
const n = Number.parseFloat(part);
|
|
67
|
-
return Number.isFinite(n) ? n : 0;
|
|
68
|
-
};
|
|
69
|
-
return {
|
|
70
|
-
x: parseAxis(parts[0] ?? "", vx),
|
|
71
|
-
y: parseAxis(parts[1] ?? "", vy),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
35
|
describe("center-anchored corner resize — no shift after release", () => {
|
|
76
36
|
it("the per-frame center anchor converges (does NOT oscillate) — the release-shift root cause", () => {
|
|
77
37
|
// Model the pointermove anchor loop that pins the element's CENTER. The physical
|
|
@@ -157,48 +117,4 @@ describe("center-anchored corner resize — no shift after release", () => {
|
|
|
157
117
|
}
|
|
158
118
|
}
|
|
159
119
|
});
|
|
160
|
-
|
|
161
|
-
it("net translate after persist+reload equals the committed anchor offset (non-GSAP)", () => {
|
|
162
|
-
// The committed offset flows through the real apply → persist → reload chain
|
|
163
|
-
// unchanged (this hop was proved clean; the shift is upstream in the anchor
|
|
164
|
-
// loop above, not in persistence).
|
|
165
|
-
const el = document.createElement("div");
|
|
166
|
-
el.style.setProperty("width", "200px");
|
|
167
|
-
el.style.setProperty("height", "100px");
|
|
168
|
-
document.body.appendChild(el);
|
|
169
|
-
|
|
170
|
-
const anchorDx = -30;
|
|
171
|
-
const anchorDy = -18;
|
|
172
|
-
const finalSize = { width: 240, height: 130 };
|
|
173
|
-
|
|
174
|
-
applyStudioBoxSize(el, finalSize);
|
|
175
|
-
const memberResult = createManualOffsetDragMember({
|
|
176
|
-
key: "k",
|
|
177
|
-
selection: { element: el } as never,
|
|
178
|
-
element: el,
|
|
179
|
-
rect: { left: 0, top: 0, width: 240, height: 130, editScaleX: 1, editScaleY: 1 },
|
|
180
|
-
});
|
|
181
|
-
expect(memberResult.ok).toBe(true);
|
|
182
|
-
if (!memberResult.ok) return;
|
|
183
|
-
|
|
184
|
-
const finalOffset = applyManualOffsetDragCommit(memberResult.member, anchorDx, anchorDy);
|
|
185
|
-
|
|
186
|
-
applyStudioBoxSize(el, finalSize);
|
|
187
|
-
const patches = buildBoxSizePatches(el);
|
|
188
|
-
applyStudioPathOffset(el, finalOffset);
|
|
189
|
-
patches.push(...buildPathOffsetPatches(el));
|
|
190
|
-
|
|
191
|
-
expect(resolvedTranslatePx(el)).toEqual({ x: anchorDx, y: anchorDy });
|
|
192
|
-
|
|
193
|
-
// Persist → fresh element re-parsed from source → reload re-stamp.
|
|
194
|
-
const reloaded = document.createElement("div");
|
|
195
|
-
reloaded.style.setProperty("width", "200px");
|
|
196
|
-
reloaded.style.setProperty("height", "100px");
|
|
197
|
-
document.body.appendChild(reloaded);
|
|
198
|
-
applyPatchesToElement(reloaded, patches);
|
|
199
|
-
reapplyPositionEditsAfterSeek(reloaded.ownerDocument);
|
|
200
|
-
|
|
201
|
-
expect(resolvedTranslatePx(reloaded)).toEqual({ x: anchorDx, y: anchorDy });
|
|
202
|
-
expect(readStudioBoxSize(reloaded)).toEqual(finalSize);
|
|
203
|
-
});
|
|
204
120
|
});
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
StudioRotationSnapshot,
|
|
7
7
|
} from "./manualEdits";
|
|
8
8
|
import type { ManualOffsetDragMember } from "./manualOffsetDrag";
|
|
9
|
+
import type { CssRotationTarget, RotationCommit } from "./rotationDraft";
|
|
9
10
|
import type { GroupOverlayItem, OverlayRect } from "./domEditOverlayGeometry";
|
|
10
11
|
import type { SnapContext } from "./snapTargetCollection";
|
|
11
12
|
import type { SnapGuidesState } from "./SnapGuideOverlay";
|
|
@@ -63,27 +64,14 @@ export interface GestureState {
|
|
|
63
64
|
actualWidth: number;
|
|
64
65
|
actualHeight: number;
|
|
65
66
|
actualRotation: number;
|
|
67
|
+
/** Null when GSAP owns the rotate; else where its CSS turn is drawn and saved, read at press. */
|
|
68
|
+
plainRotation: CssRotationTarget | null;
|
|
66
69
|
editScaleX: number;
|
|
67
70
|
editScaleY: number;
|
|
68
|
-
// Rendered
|
|
69
|
-
//
|
|
70
|
-
// follows the cursor instead of overshooting by the live scale.
|
|
71
|
+
// Rendered px per CSS px of the element at gesture start (> 1 under a GSAP scale()); the resize
|
|
72
|
+
// draft divides by it so the box follows the cursor instead of overshooting by the live scale.
|
|
71
73
|
contentScaleX: number;
|
|
72
74
|
contentScaleY: number;
|
|
73
|
-
// Resize anchor pinning: with a live scale transform, growing the CSS box
|
|
74
|
-
// shifts the rendered box (scaling happens around the element center), so the
|
|
75
|
-
// un-dragged corner creeps during the draft. The move handler measures the
|
|
76
|
-
// gesture-start top-left drift each frame and counters it through the GSAP
|
|
77
|
-
// position channel; the pin accumulates so the correction converges.
|
|
78
|
-
// Present only on resize gestures.
|
|
79
|
-
resizeAnchor?: {
|
|
80
|
-
anchorX: number;
|
|
81
|
-
anchorY: number;
|
|
82
|
-
baseGsapX: number;
|
|
83
|
-
baseGsapY: number;
|
|
84
|
-
pinX: number;
|
|
85
|
-
pinY: number;
|
|
86
|
-
};
|
|
87
75
|
manualEditDragToken?: string;
|
|
88
76
|
snapContext?: SnapContext;
|
|
89
77
|
lastSnappedDx?: number;
|
|
@@ -232,9 +220,15 @@ export function hasDomEditRotationChanged(initialAngle: number, nextAngle: numbe
|
|
|
232
220
|
// These live here (rather than in DomEditOverlay.tsx or useDomEditOverlayGestures.ts)
|
|
233
221
|
// to break circular imports between those files.
|
|
234
222
|
|
|
223
|
+
export interface MoveCommitOptions {
|
|
224
|
+
altKey?: boolean;
|
|
225
|
+
plainTranslate?: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
235
228
|
export interface DomEditGroupPathOffsetCommit {
|
|
236
229
|
selection: DomEditSelection;
|
|
237
230
|
next: { x: number; y: number };
|
|
231
|
+
plainTranslate?: boolean;
|
|
238
232
|
}
|
|
239
233
|
|
|
240
234
|
// Refs are stable across renders; values are read via .current.
|
|
@@ -259,7 +253,7 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
259
253
|
(
|
|
260
254
|
s: DomEditSelection,
|
|
261
255
|
n: { x: number; y: number },
|
|
262
|
-
m?:
|
|
256
|
+
m?: MoveCommitOptions,
|
|
263
257
|
) => Promise<unknown> | void
|
|
264
258
|
>;
|
|
265
259
|
onGroupPathOffsetCommitRef: RefObject<
|
|
@@ -274,7 +268,7 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
274
268
|
) => Promise<unknown> | void
|
|
275
269
|
>;
|
|
276
270
|
onRotationCommitRef: RefObject<
|
|
277
|
-
(s: DomEditSelection, n:
|
|
271
|
+
(s: DomEditSelection, n: RotationCommit) => Promise<unknown> | void
|
|
278
272
|
>;
|
|
279
273
|
onCanvasPointerMoveRef: RefObject<
|
|
280
274
|
(
|
|
@@ -20,38 +20,48 @@ const PRESS = {
|
|
|
20
20
|
currentTarget: { setPointerCapture() {} },
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
/** Presses a drag on the element and returns the gesture it started. */
|
|
24
|
-
function
|
|
23
|
+
/** Presses a drag or resize on the element and returns the gesture it started. */
|
|
24
|
+
function pressGesture(element: HTMLElement, kind: "drag" | "resize" = "drag"): GestureState | null {
|
|
25
25
|
const opts = pressOptions(element);
|
|
26
26
|
expect(
|
|
27
|
-
createDomEditOverlayGestureHandlers(opts as never).startGesture(
|
|
27
|
+
createDomEditOverlayGestureHandlers(opts as never).startGesture(kind, PRESS as never),
|
|
28
28
|
).toBe(true);
|
|
29
29
|
return opts.gestureRef.current;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function pressOptions(element: HTMLElement) {
|
|
33
33
|
const ref = <T>(current: T) => ({ current });
|
|
34
|
-
const
|
|
34
|
+
const capabilities = { canApplyManualOffset: true, canApplyManualSize: true };
|
|
35
|
+
const selection = { element, capabilities };
|
|
35
36
|
return {
|
|
36
37
|
selectionRef: ref(selection as unknown as DomEditSelection),
|
|
37
38
|
overlayRectRef: ref({ left: 0, top: 0, width: 240, height: 160, editScaleX: 1, editScaleY: 1 }),
|
|
38
|
-
boxRef: ref(
|
|
39
|
+
boxRef: ref(document.createElement("div")),
|
|
39
40
|
overlayRef: ref(null),
|
|
40
41
|
iframeRef: ref(null),
|
|
41
42
|
gestureRef: ref<GestureState | null>(null),
|
|
42
43
|
rafPausedRef: ref(false),
|
|
43
44
|
onManualDragStartRef: ref(vi.fn()),
|
|
44
45
|
onBlockedMoveRef: ref(vi.fn()),
|
|
46
|
+
onPathOffsetCommitRef: ref(vi.fn()),
|
|
47
|
+
snapGuidesRef: ref(null),
|
|
48
|
+
groupGestureRef: ref(null),
|
|
49
|
+
blockedMoveRef: ref(null),
|
|
50
|
+
setOverlayRect: vi.fn(),
|
|
51
|
+
suppressNextBoxClickRef: ref(false),
|
|
52
|
+
hoverSelectionRef: ref(null),
|
|
53
|
+
onCanvasMouseDown: vi.fn(),
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
describe("a drag press on a page that loads GSAP", () => {
|
|
57
|
+
describe("a drag or resize press on a page that loads GSAP", () => {
|
|
49
58
|
it.each([
|
|
50
|
-
["GSAP animates nothing", false],
|
|
51
|
-
["GSAP animates only its parent", true],
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
["drag", "GSAP animates nothing", false],
|
|
60
|
+
["drag", "GSAP animates only its parent", true],
|
|
61
|
+
["resize", "GSAP animates nothing", false],
|
|
62
|
+
] as const)(
|
|
63
|
+
"%s, %s: the press never asks GSAP about the element, so its translate stays CSS",
|
|
64
|
+
(kind, _, parentTween) => {
|
|
55
65
|
const getProperty = vi.fn(() => 0);
|
|
56
66
|
const set = vi.fn();
|
|
57
67
|
const parent = document.createElement("div");
|
|
@@ -62,7 +72,7 @@ describe("a drag press on a page that loads GSAP", () => {
|
|
|
62
72
|
const tween = { targets: () => [parent], vars: { x: 100 }, duration: () => 2 };
|
|
63
73
|
const timelines = { main: { getChildren: () => (parentTween ? [tween] : []) } };
|
|
64
74
|
Object.assign(window, { gsap: { getProperty, set }, __timelines: timelines });
|
|
65
|
-
expect(
|
|
75
|
+
expect(pressGesture(element, kind)?.pathOffsetMember?.plainTranslate).toBe(true);
|
|
66
76
|
expect(getProperty).not.toHaveBeenCalled();
|
|
67
77
|
expect(set).not.toHaveBeenCalled();
|
|
68
78
|
expect(element.style.getPropertyValue("translate")).toBe("40px 30px");
|
|
@@ -77,7 +87,154 @@ describe("a drag press on a centred element without GSAP", () => {
|
|
|
77
87
|
"position: absolute; left: 50%; top: 50%; width: 240px; height: 160px; translate: -50% -50%";
|
|
78
88
|
document.body.append(element);
|
|
79
89
|
const style = element.getAttribute("style");
|
|
80
|
-
expect(
|
|
90
|
+
expect(pressGesture(element)?.pathOffsetMember?.initialOffset).toEqual({ x: -120, y: -80 });
|
|
81
91
|
expect(element.getAttribute("style")).toBe(style);
|
|
82
92
|
});
|
|
83
93
|
});
|
|
94
|
+
|
|
95
|
+
describe("a drag on an element without GSAP", () => {
|
|
96
|
+
it("drops on the route it chose at press, even if GSAP takes the element over mid-drag", () => {
|
|
97
|
+
const element = document.createElement("div");
|
|
98
|
+
element.style.setProperty("translate", "40px 30px");
|
|
99
|
+
document.body.append(element);
|
|
100
|
+
const opts = pressOptions(element);
|
|
101
|
+
const handlers = createDomEditOverlayGestureHandlers(opts as never);
|
|
102
|
+
expect(handlers.startGesture("drag", PRESS as never)).toBe(true);
|
|
103
|
+
Object.assign(element, { _gsap: { renderTransform: () => {} } });
|
|
104
|
+
const release = {
|
|
105
|
+
...PRESS,
|
|
106
|
+
clientX: 110,
|
|
107
|
+
clientY: 70,
|
|
108
|
+
currentTarget: { releasePointerCapture() {} },
|
|
109
|
+
};
|
|
110
|
+
handlers.onPointerUp(release as never);
|
|
111
|
+
expect(opts.onPathOffsetCommitRef.current).toHaveBeenCalledWith(
|
|
112
|
+
expect.anything(),
|
|
113
|
+
expect.anything(),
|
|
114
|
+
expect.objectContaining({ plainTranslate: true }),
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("a group drag of elements without GSAP", () => {
|
|
120
|
+
it("drops every member on the route it chose at press", () => {
|
|
121
|
+
const items = [0, 1].map((i) => {
|
|
122
|
+
const element = document.createElement("div");
|
|
123
|
+
element.style.setProperty("translate", `${i * 10}px 0px`);
|
|
124
|
+
document.body.append(element);
|
|
125
|
+
const selection = { element, capabilities: { canApplyManualOffset: true } };
|
|
126
|
+
const rect = { left: 0, top: 0, width: 240, height: 160, editScaleX: 1, editScaleY: 1 };
|
|
127
|
+
return { key: `m${i}`, selection, element, rect };
|
|
128
|
+
});
|
|
129
|
+
const onGroupPathOffsetCommit = vi.fn();
|
|
130
|
+
const opts = {
|
|
131
|
+
...pressOptions(items[0]!.element),
|
|
132
|
+
groupOverlayItemsRef: { current: items },
|
|
133
|
+
onGroupPathOffsetCommitRef: { current: onGroupPathOffsetCommit },
|
|
134
|
+
setGroupOverlayItems: vi.fn(),
|
|
135
|
+
};
|
|
136
|
+
const handlers = createDomEditOverlayGestureHandlers(opts as never);
|
|
137
|
+
expect(handlers.startGroupDrag(PRESS as never)).toBe(true);
|
|
138
|
+
const release = {
|
|
139
|
+
...PRESS,
|
|
140
|
+
clientX: 110,
|
|
141
|
+
clientY: 70,
|
|
142
|
+
currentTarget: { releasePointerCapture() {} },
|
|
143
|
+
};
|
|
144
|
+
handlers.onPointerUp(release as never);
|
|
145
|
+
const updates = onGroupPathOffsetCommit.mock.calls[0]?.[0] as { plainTranslate?: boolean }[];
|
|
146
|
+
expect(updates.map((update) => update.plainTranslate)).toEqual([true, true]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("a rotate on a page that loads GSAP", () => {
|
|
151
|
+
it("never asks GSAP about an element it does not turn, and draws the turn as its CSS rotate", () => {
|
|
152
|
+
const getProperty = vi.fn(() => 0);
|
|
153
|
+
const set = vi.fn();
|
|
154
|
+
const element = document.createElement("div");
|
|
155
|
+
element.style.setProperty("rotate", "30deg");
|
|
156
|
+
document.body.append(element);
|
|
157
|
+
Object.assign(window, {
|
|
158
|
+
gsap: { getProperty, set },
|
|
159
|
+
__timelines: { main: { getChildren: () => [] } },
|
|
160
|
+
});
|
|
161
|
+
const selection = { element, capabilities: { canApplyManualRotation: true } };
|
|
162
|
+
const ref = <T>(current: T) => ({ current });
|
|
163
|
+
const opts = {
|
|
164
|
+
selectionRef: ref(selection as unknown as DomEditSelection),
|
|
165
|
+
overlayRectRef: ref({ left: 0, top: 0, width: 50, height: 40, editScaleX: 1, editScaleY: 1 }),
|
|
166
|
+
boxRef: ref(document.createElement("div")),
|
|
167
|
+
overlayRef: ref(null),
|
|
168
|
+
iframeRef: ref(null),
|
|
169
|
+
gestureRef: ref<GestureState | null>(null),
|
|
170
|
+
groupGestureRef: ref(null),
|
|
171
|
+
blockedMoveRef: ref(null),
|
|
172
|
+
rafPausedRef: ref(false),
|
|
173
|
+
onCanvasPointerMoveRef: ref(vi.fn()),
|
|
174
|
+
};
|
|
175
|
+
const pointer = (clientX: number, clientY: number) => ({
|
|
176
|
+
clientX,
|
|
177
|
+
clientY,
|
|
178
|
+
pointerId: 1,
|
|
179
|
+
button: 0,
|
|
180
|
+
shiftKey: false,
|
|
181
|
+
preventDefault() {},
|
|
182
|
+
stopPropagation() {},
|
|
183
|
+
currentTarget: { setPointerCapture() {} },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const handlers = createDomEditOverlayGestureHandlers(opts as never);
|
|
187
|
+
expect(handlers.startGesture("rotate", pointer(25, -20) as never)).toBe(true);
|
|
188
|
+
expect(opts.gestureRef.current?.plainRotation).toEqual({
|
|
189
|
+
property: "rotate",
|
|
190
|
+
before: "",
|
|
191
|
+
after: "",
|
|
192
|
+
share: 0,
|
|
193
|
+
sign: 1,
|
|
194
|
+
inline: false,
|
|
195
|
+
});
|
|
196
|
+
expect(opts.gestureRef.current?.actualRotation).toBeCloseTo(30);
|
|
197
|
+
handlers.onPointerMove(pointer(60, 20) as never);
|
|
198
|
+
|
|
199
|
+
expect(element.style.getPropertyValue("rotate")).toMatch(/deg$/);
|
|
200
|
+
expect(element.style.getPropertyValue("rotate")).not.toBe("30deg");
|
|
201
|
+
expect(getProperty).not.toHaveBeenCalled();
|
|
202
|
+
expect(set).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("a rotate press on an element a GSAP tween turns", () => {
|
|
207
|
+
it.each(["rotation", "rotate", "rotateZ"])("reads its base from GSAP (%s)", (channel) => {
|
|
208
|
+
const getProperty = vi.fn(() => 40);
|
|
209
|
+
const element = document.createElement("div");
|
|
210
|
+
document.body.append(element);
|
|
211
|
+
const tween = { targets: () => [element], vars: { [channel]: 40 }, duration: () => 2 };
|
|
212
|
+
const timelines = { main: { getChildren: () => [tween] } };
|
|
213
|
+
Object.assign(window, { gsap: { getProperty, set: vi.fn() }, __timelines: timelines });
|
|
214
|
+
const ref = <T>(current: T) => ({ current });
|
|
215
|
+
const selection = { element, capabilities: { canApplyManualRotation: true } };
|
|
216
|
+
const opts = {
|
|
217
|
+
selectionRef: ref(selection as unknown as DomEditSelection),
|
|
218
|
+
overlayRectRef: ref({ left: 0, top: 0, width: 50, height: 40, editScaleX: 1, editScaleY: 1 }),
|
|
219
|
+
boxRef: ref(document.createElement("div")),
|
|
220
|
+
overlayRef: ref(null),
|
|
221
|
+
iframeRef: ref(null),
|
|
222
|
+
gestureRef: ref<GestureState | null>(null),
|
|
223
|
+
rafPausedRef: ref(false),
|
|
224
|
+
};
|
|
225
|
+
const press = {
|
|
226
|
+
clientX: 25,
|
|
227
|
+
clientY: -20,
|
|
228
|
+
pointerId: 1,
|
|
229
|
+
button: 0,
|
|
230
|
+
preventDefault() {},
|
|
231
|
+
stopPropagation() {},
|
|
232
|
+
currentTarget: { setPointerCapture() {} },
|
|
233
|
+
};
|
|
234
|
+
const handlers = createDomEditOverlayGestureHandlers(opts as never);
|
|
235
|
+
expect(handlers.startGesture("rotate", press as never)).toBe(true);
|
|
236
|
+
expect(opts.gestureRef.current?.plainRotation).toBeNull();
|
|
237
|
+
expect(opts.gestureRef.current?.actualRotation).toBe(40);
|
|
238
|
+
expect(getProperty).toHaveBeenCalledWith(element, "rotation");
|
|
239
|
+
});
|
|
240
|
+
});
|