@hyperframes/studio 0.7.58 → 0.7.60
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-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
- package/dist/assets/index-DXbu6IPT.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
- package/dist/assets/index-cH6NfVV_.js +426 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13755 -8627
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +42 -53
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.ts +2 -2
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +41 -1
- package/src/hooks/useDomEditCommits.ts +31 -177
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
- package/src/hooks/useFileManager.ts +151 -65
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePanelLayout.test.ts +118 -0
- package/src/hooks/usePanelLayout.ts +24 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +4 -1
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useRazorSplit.history.test.tsx +12 -4
- package/src/hooks/useRazorSplit.test.tsx +88 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
- package/src/hooks/useRazorSplit.ts +50 -16
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +4 -1
- package/src/hooks/useTimelineEditing.ts +18 -10
- package/src/hooks/useTimelineEditingTypes.ts +5 -1
- package/src/hooks/useTimelineGroupEditing.ts +17 -3
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.test.ts +19 -0
- package/src/utils/domEditSaveQueue.ts +2 -1
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +511 -62
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +293 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +34 -29
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioFileVersion.test.ts +36 -0
- package/src/utils/studioFileVersion.ts +23 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.test.ts +18 -0
- package/src/utils/studioSaveDiagnostics.ts +38 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -3,7 +3,11 @@ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
|
3
3
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
4
4
|
import { usePlayerStore } from "../player";
|
|
5
5
|
import { computeCurrentPercentage } from "./gsapDragCommit";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getStudioSaveErrorMessage,
|
|
8
|
+
isStudioSaveErrorAlreadyToasted,
|
|
9
|
+
trackStudioSaveFailure,
|
|
10
|
+
} from "../utils/studioSaveDiagnostics";
|
|
7
11
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
8
12
|
import type { CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
9
13
|
|
|
@@ -34,6 +38,7 @@ export function useGsapSelectionHandlers({
|
|
|
34
38
|
removeAllKeyframes,
|
|
35
39
|
handleDomManualEditsReset,
|
|
36
40
|
selectedGsapAnimations,
|
|
41
|
+
showToast,
|
|
37
42
|
}: {
|
|
38
43
|
domEditSelection: DomEditSelection | null;
|
|
39
44
|
updateGsapProperty: (
|
|
@@ -46,24 +51,24 @@ export function useGsapSelectionHandlers({
|
|
|
46
51
|
sel: DomEditSelection,
|
|
47
52
|
animId: string,
|
|
48
53
|
updates: { duration?: number; ease?: string; position?: number },
|
|
49
|
-
) => void
|
|
50
|
-
deleteGsapAnimation: (sel: DomEditSelection, animId: string) => void
|
|
51
|
-
deleteAllForSelector: (sel: DomEditSelection, targetSelector: string) => void
|
|
54
|
+
) => Promise<void>;
|
|
55
|
+
deleteGsapAnimation: (sel: DomEditSelection, animId: string) => Promise<void>;
|
|
56
|
+
deleteAllForSelector: (sel: DomEditSelection, targetSelector: string) => Promise<void>;
|
|
52
57
|
addGsapAnimation: (
|
|
53
58
|
sel: DomEditSelection,
|
|
54
59
|
method: "to" | "from" | "set" | "fromTo",
|
|
55
60
|
time: number,
|
|
56
61
|
) => Promise<void>;
|
|
57
|
-
addGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => void
|
|
58
|
-
removeGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => void
|
|
62
|
+
addGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
63
|
+
removeGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
59
64
|
updateGsapFromProperty: (
|
|
60
65
|
sel: DomEditSelection,
|
|
61
66
|
animId: string,
|
|
62
67
|
prop: string,
|
|
63
68
|
value: number | string,
|
|
64
|
-
) => void
|
|
65
|
-
addGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => void
|
|
66
|
-
removeGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => void
|
|
69
|
+
) => Promise<void>;
|
|
70
|
+
addGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
71
|
+
removeGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
67
72
|
addKeyframe: (
|
|
68
73
|
sel: DomEditSelection,
|
|
69
74
|
animId: string,
|
|
@@ -104,10 +109,11 @@ export function useGsapSelectionHandlers({
|
|
|
104
109
|
duration?: number,
|
|
105
110
|
commitOverrides?: Partial<CommitMutationOptions>,
|
|
106
111
|
) => Promise<void>;
|
|
107
|
-
removeAllKeyframes: (sel: DomEditSelection, animId: string) => void
|
|
112
|
+
removeAllKeyframes: (sel: DomEditSelection, animId: string) => Promise<void>;
|
|
108
113
|
|
|
109
114
|
handleDomManualEditsReset: (sel: DomEditSelection) => void;
|
|
110
115
|
selectedGsapAnimations: GsapAnimation[];
|
|
116
|
+
showToast: (message: string, tone?: "error" | "info") => void;
|
|
111
117
|
}) {
|
|
112
118
|
const lastSelectionRef = useRef<DomEditSelection | null>(null);
|
|
113
119
|
if (domEditSelection) lastSelectionRef.current = domEditSelection;
|
|
@@ -124,8 +130,20 @@ export function useGsapSelectionHandlers({
|
|
|
124
130
|
targetSelector: selection.selector,
|
|
125
131
|
targetSourceFile: selection.sourceFile,
|
|
126
132
|
});
|
|
133
|
+
if (!isStudioSaveErrorAlreadyToasted(error)) {
|
|
134
|
+
showToast(`Couldn't save animation: ${getStudioSaveErrorMessage(error)}`, "error");
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
[showToast],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const observeGsapMutation = useCallback(
|
|
141
|
+
(mutation: Promise<void>, selection: DomEditSelection, mutationType: string, label: string) => {
|
|
142
|
+
void mutation.catch((error) => {
|
|
143
|
+
trackGsapHandlerFailure(error, selection, mutationType, label);
|
|
144
|
+
});
|
|
127
145
|
},
|
|
128
|
-
[],
|
|
146
|
+
[trackGsapHandlerFailure],
|
|
129
147
|
);
|
|
130
148
|
|
|
131
149
|
const handleGsapUpdateProperty = useCallback(
|
|
@@ -144,18 +162,23 @@ export function useGsapSelectionHandlers({
|
|
|
144
162
|
) => {
|
|
145
163
|
const sel = selectionOverride ?? domEditSelection ?? lastSelectionRef.current;
|
|
146
164
|
if (!sel) return;
|
|
147
|
-
|
|
165
|
+
observeGsapMutation(
|
|
166
|
+
updateGsapMeta(sel, animId, updates),
|
|
167
|
+
sel,
|
|
168
|
+
"update-meta",
|
|
169
|
+
"Edit GSAP animation",
|
|
170
|
+
);
|
|
148
171
|
},
|
|
149
|
-
[domEditSelection, updateGsapMeta],
|
|
172
|
+
[domEditSelection, observeGsapMutation, updateGsapMeta],
|
|
150
173
|
);
|
|
151
174
|
|
|
152
175
|
const handleGsapDeleteAnimation = useCallback(
|
|
153
176
|
(animId: string) => {
|
|
154
177
|
const sel = domEditSelection ?? lastSelectionRef.current;
|
|
155
178
|
if (!sel) return;
|
|
156
|
-
deleteGsapAnimation(sel, animId);
|
|
179
|
+
observeGsapMutation(deleteGsapAnimation(sel, animId), sel, "delete", "Delete GSAP animation");
|
|
157
180
|
},
|
|
158
|
-
[domEditSelection, deleteGsapAnimation],
|
|
181
|
+
[domEditSelection, deleteGsapAnimation, observeGsapMutation],
|
|
159
182
|
);
|
|
160
183
|
|
|
161
184
|
const handleGsapDeleteAllForElement = useCallback(
|
|
@@ -163,9 +186,14 @@ export function useGsapSelectionHandlers({
|
|
|
163
186
|
const sel = domEditSelection ?? lastSelectionRef.current;
|
|
164
187
|
if (!sel) return;
|
|
165
188
|
trackStudioEvent("keyframe", { action: "delete_all" });
|
|
166
|
-
|
|
189
|
+
observeGsapMutation(
|
|
190
|
+
deleteAllForSelector(sel, targetSelector),
|
|
191
|
+
sel,
|
|
192
|
+
"delete-all-for-selector",
|
|
193
|
+
"Delete all animations for element",
|
|
194
|
+
);
|
|
167
195
|
},
|
|
168
|
-
[domEditSelection, deleteAllForSelector],
|
|
196
|
+
[domEditSelection, deleteAllForSelector, observeGsapMutation],
|
|
169
197
|
);
|
|
170
198
|
|
|
171
199
|
const handleGsapAddAnimation = useCallback(
|
|
@@ -186,41 +214,66 @@ export function useGsapSelectionHandlers({
|
|
|
186
214
|
const handleGsapAddProperty = useCallback(
|
|
187
215
|
(animId: string, prop: string) => {
|
|
188
216
|
if (!domEditSelection) return;
|
|
189
|
-
|
|
217
|
+
observeGsapMutation(
|
|
218
|
+
addGsapProperty(domEditSelection, animId, prop),
|
|
219
|
+
domEditSelection,
|
|
220
|
+
"add-property",
|
|
221
|
+
`Add GSAP ${prop}`,
|
|
222
|
+
);
|
|
190
223
|
},
|
|
191
|
-
[domEditSelection, addGsapProperty],
|
|
224
|
+
[domEditSelection, addGsapProperty, observeGsapMutation],
|
|
192
225
|
);
|
|
193
226
|
|
|
194
227
|
const handleGsapRemoveProperty = useCallback(
|
|
195
228
|
(animId: string, prop: string) => {
|
|
196
229
|
if (!domEditSelection) return;
|
|
197
|
-
|
|
230
|
+
observeGsapMutation(
|
|
231
|
+
removeGsapProperty(domEditSelection, animId, prop),
|
|
232
|
+
domEditSelection,
|
|
233
|
+
"remove-property",
|
|
234
|
+
`Remove GSAP ${prop}`,
|
|
235
|
+
);
|
|
198
236
|
},
|
|
199
|
-
[domEditSelection, removeGsapProperty],
|
|
237
|
+
[domEditSelection, observeGsapMutation, removeGsapProperty],
|
|
200
238
|
);
|
|
201
239
|
|
|
202
240
|
const handleGsapUpdateFromProperty = useCallback(
|
|
203
241
|
(animId: string, prop: string, value: number | string) => {
|
|
204
242
|
if (!domEditSelection) return;
|
|
205
|
-
|
|
243
|
+
observeGsapMutation(
|
|
244
|
+
updateGsapFromProperty(domEditSelection, animId, prop, value),
|
|
245
|
+
domEditSelection,
|
|
246
|
+
"update-from-property",
|
|
247
|
+
`Edit GSAP from-${prop}`,
|
|
248
|
+
);
|
|
206
249
|
},
|
|
207
|
-
[domEditSelection, updateGsapFromProperty],
|
|
250
|
+
[domEditSelection, observeGsapMutation, updateGsapFromProperty],
|
|
208
251
|
);
|
|
209
252
|
|
|
210
253
|
const handleGsapAddFromProperty = useCallback(
|
|
211
254
|
(animId: string, prop: string) => {
|
|
212
255
|
if (!domEditSelection) return;
|
|
213
|
-
|
|
256
|
+
observeGsapMutation(
|
|
257
|
+
addGsapFromProperty(domEditSelection, animId, prop),
|
|
258
|
+
domEditSelection,
|
|
259
|
+
"add-from-property",
|
|
260
|
+
`Add GSAP from-${prop}`,
|
|
261
|
+
);
|
|
214
262
|
},
|
|
215
|
-
[domEditSelection, addGsapFromProperty],
|
|
263
|
+
[domEditSelection, addGsapFromProperty, observeGsapMutation],
|
|
216
264
|
);
|
|
217
265
|
|
|
218
266
|
const handleGsapRemoveFromProperty = useCallback(
|
|
219
267
|
(animId: string, prop: string) => {
|
|
220
268
|
if (!domEditSelection) return;
|
|
221
|
-
|
|
269
|
+
observeGsapMutation(
|
|
270
|
+
removeGsapFromProperty(domEditSelection, animId, prop),
|
|
271
|
+
domEditSelection,
|
|
272
|
+
"remove-from-property",
|
|
273
|
+
`Remove GSAP from-${prop}`,
|
|
274
|
+
);
|
|
222
275
|
},
|
|
223
|
-
[domEditSelection, removeGsapFromProperty],
|
|
276
|
+
[domEditSelection, observeGsapMutation, removeGsapFromProperty],
|
|
224
277
|
);
|
|
225
278
|
|
|
226
279
|
const handleGsapAddKeyframe = useCallback(
|
|
@@ -354,18 +407,28 @@ export function useGsapSelectionHandlers({
|
|
|
354
407
|
const handleGsapRemoveAllKeyframes = useCallback(
|
|
355
408
|
(animId: string) => {
|
|
356
409
|
if (!domEditSelection) return;
|
|
357
|
-
|
|
410
|
+
observeGsapMutation(
|
|
411
|
+
removeAllKeyframes(domEditSelection, animId),
|
|
412
|
+
domEditSelection,
|
|
413
|
+
"remove-all-keyframes",
|
|
414
|
+
"Remove all keyframes",
|
|
415
|
+
);
|
|
358
416
|
},
|
|
359
|
-
[domEditSelection, removeAllKeyframes],
|
|
417
|
+
[domEditSelection, observeGsapMutation, removeAllKeyframes],
|
|
360
418
|
);
|
|
361
419
|
|
|
362
420
|
const handleResetSelectedElementKeyframes = useCallback((): boolean => {
|
|
363
421
|
if (!domEditSelection) return false;
|
|
364
422
|
const withKeyframes = selectedGsapAnimations.find((a) => a.keyframes);
|
|
365
423
|
if (!withKeyframes) return false;
|
|
366
|
-
|
|
424
|
+
observeGsapMutation(
|
|
425
|
+
removeAllKeyframes(domEditSelection, withKeyframes.id),
|
|
426
|
+
domEditSelection,
|
|
427
|
+
"remove-all-keyframes",
|
|
428
|
+
"Remove all keyframes",
|
|
429
|
+
);
|
|
367
430
|
return true;
|
|
368
|
-
}, [domEditSelection,
|
|
431
|
+
}, [domEditSelection, observeGsapMutation, removeAllKeyframes, selectedGsapAnimations]);
|
|
369
432
|
|
|
370
433
|
return {
|
|
371
434
|
handleGsapUpdateProperty,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
|
2
|
+
|
|
3
|
+
const MIN_INSPECTOR_SPLIT_PERCENT = 20;
|
|
4
|
+
const MAX_INSPECTOR_SPLIT_PERCENT = 75;
|
|
5
|
+
|
|
6
|
+
export function useInspectorSplitResize() {
|
|
7
|
+
const [layersPanePercent, setLayersPanePercent] = useState(40);
|
|
8
|
+
const splitContainerRef = useRef<HTMLDivElement>(null);
|
|
9
|
+
const splitDragRef = useRef<{
|
|
10
|
+
startY: number;
|
|
11
|
+
startPercent: number;
|
|
12
|
+
height: number;
|
|
13
|
+
} | null>(null);
|
|
14
|
+
|
|
15
|
+
const handleInspectorSplitResizeStart = useCallback(
|
|
16
|
+
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
17
|
+
event.preventDefault();
|
|
18
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
19
|
+
const height = splitContainerRef.current?.getBoundingClientRect().height ?? 0;
|
|
20
|
+
splitDragRef.current = {
|
|
21
|
+
startY: event.clientY,
|
|
22
|
+
startPercent: layersPanePercent,
|
|
23
|
+
height,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
[layersPanePercent],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const handleInspectorSplitResizeMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
|
30
|
+
const drag = splitDragRef.current;
|
|
31
|
+
if (!drag || drag.height <= 0) return;
|
|
32
|
+
const deltaPercent = ((event.clientY - drag.startY) / drag.height) * 100;
|
|
33
|
+
const next = Math.min(
|
|
34
|
+
MAX_INSPECTOR_SPLIT_PERCENT,
|
|
35
|
+
Math.max(MIN_INSPECTOR_SPLIT_PERCENT, drag.startPercent + deltaPercent),
|
|
36
|
+
);
|
|
37
|
+
setLayersPanePercent(next);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const handleInspectorSplitResizeEnd = useCallback(() => {
|
|
41
|
+
splitDragRef.current = null;
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
layersPanePercent,
|
|
46
|
+
splitContainerRef,
|
|
47
|
+
handleInspectorSplitResizeStart,
|
|
48
|
+
handleInspectorSplitResizeMove,
|
|
49
|
+
handleInspectorSplitResizeEnd,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { usePanelLayout } from "./usePanelLayout";
|
|
7
|
+
|
|
8
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
document.body.innerHTML = "";
|
|
12
|
+
vi.doUnmock("../components/editor/manualEditingAvailability");
|
|
13
|
+
vi.resetModules();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function renderPanelLayoutWith(hook: typeof usePanelLayout) {
|
|
17
|
+
const host = document.createElement("div");
|
|
18
|
+
document.body.append(host);
|
|
19
|
+
const root = createRoot(host);
|
|
20
|
+
let current: ReturnType<typeof usePanelLayout> | null = null;
|
|
21
|
+
|
|
22
|
+
function Harness() {
|
|
23
|
+
current = hook();
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
act(() => {
|
|
28
|
+
root.render(React.createElement(Harness));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
getState: (): ReturnType<typeof usePanelLayout> => {
|
|
33
|
+
if (!current) throw new Error("usePanelLayout did not render");
|
|
34
|
+
return current;
|
|
35
|
+
},
|
|
36
|
+
unmount: () => act(() => root.unmount()),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderPanelLayout() {
|
|
41
|
+
return renderPanelLayoutWith(usePanelLayout);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("usePanelLayout — right inspector panes", () => {
|
|
45
|
+
it("toggleRightInspectorPane independently flips one pane, allowing both open at once", () => {
|
|
46
|
+
const harness = renderPanelLayout();
|
|
47
|
+
expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
|
|
48
|
+
|
|
49
|
+
act(() => harness.getState().toggleRightInspectorPane("layers"));
|
|
50
|
+
expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: true });
|
|
51
|
+
|
|
52
|
+
harness.unmount();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("toggleRightInspectorPane refuses to turn off the last remaining pane", () => {
|
|
56
|
+
const harness = renderPanelLayout();
|
|
57
|
+
act(() => harness.getState().toggleRightInspectorPane("design"));
|
|
58
|
+
// Only "design" was on; toggling it off would leave both false — guarded.
|
|
59
|
+
expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
|
|
60
|
+
harness.unmount();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("setExclusiveRightInspectorPane is radio-style — selecting one turns the other off", () => {
|
|
64
|
+
const harness = renderPanelLayout();
|
|
65
|
+
act(() => harness.getState().toggleRightInspectorPane("layers"));
|
|
66
|
+
expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: true });
|
|
67
|
+
|
|
68
|
+
act(() => harness.getState().setExclusiveRightInspectorPane("layers"));
|
|
69
|
+
expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: false });
|
|
70
|
+
|
|
71
|
+
act(() => harness.getState().setExclusiveRightInspectorPane("design"));
|
|
72
|
+
expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
|
|
73
|
+
|
|
74
|
+
harness.unmount();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("setRightPanelTab additively opens a pane when the flat inspector is off (legacy behavior)", async () => {
|
|
78
|
+
vi.resetModules();
|
|
79
|
+
vi.doMock("../components/editor/manualEditingAvailability", async () => {
|
|
80
|
+
const actual = await vi.importActual<
|
|
81
|
+
typeof import("../components/editor/manualEditingAvailability")
|
|
82
|
+
>("../components/editor/manualEditingAvailability");
|
|
83
|
+
return { ...actual, STUDIO_FLAT_INSPECTOR_ENABLED: false };
|
|
84
|
+
});
|
|
85
|
+
const { usePanelLayout: usePanelLayoutFlatOff } = await import("./usePanelLayout");
|
|
86
|
+
const harness = renderPanelLayoutWith(usePanelLayoutFlatOff);
|
|
87
|
+
expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
|
|
88
|
+
|
|
89
|
+
act(() => harness.getState().setRightPanelTab("layers"));
|
|
90
|
+
// Legacy (split-view) behavior: additive, both panes end up open.
|
|
91
|
+
expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: true });
|
|
92
|
+
|
|
93
|
+
harness.unmount();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("setRightPanelTab is flat-aware: exclusivity holds for callers other than a direct in-panel tab click", async () => {
|
|
97
|
+
vi.resetModules();
|
|
98
|
+
vi.doMock("../components/editor/manualEditingAvailability", async () => {
|
|
99
|
+
const actual = await vi.importActual<
|
|
100
|
+
typeof import("../components/editor/manualEditingAvailability")
|
|
101
|
+
>("../components/editor/manualEditingAvailability");
|
|
102
|
+
return { ...actual, STUDIO_FLAT_INSPECTOR_ENABLED: true };
|
|
103
|
+
});
|
|
104
|
+
const { usePanelLayout: usePanelLayoutFlatOn } = await import("./usePanelLayout");
|
|
105
|
+
const harness = renderPanelLayoutWith(usePanelLayoutFlatOn);
|
|
106
|
+
expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
|
|
107
|
+
|
|
108
|
+
// Element-select / block-params-close / header Inspector-button callers
|
|
109
|
+
// all reach setRightPanelTab directly, not through the in-panel tab
|
|
110
|
+
// click's own setExclusiveRightInspectorPane call — this must still
|
|
111
|
+
// enforce exclusivity under the flat flag, or both tabs end up
|
|
112
|
+
// highlighted while only one pane actually renders.
|
|
113
|
+
act(() => harness.getState().setRightPanelTab("layers"));
|
|
114
|
+
expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: false });
|
|
115
|
+
|
|
116
|
+
harness.unmount();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from "../utils/studioHelpers";
|
|
7
7
|
import { readStudioUiPreferences, writeStudioUiPreferences } from "../utils/studioUiPreferences";
|
|
8
8
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
9
|
+
import { STUDIO_FLAT_INSPECTOR_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
9
10
|
|
|
10
11
|
export interface InitialPanelLayoutState {
|
|
11
12
|
rightCollapsed?: boolean | null;
|
|
@@ -80,7 +81,20 @@ export function usePanelLayout(initialState?: InitialPanelLayoutState) {
|
|
|
80
81
|
const trackedSetRightPanelTab = useCallback(
|
|
81
82
|
(tab: RightPanelTab) => {
|
|
82
83
|
if (tab === "design" || tab === "layers") {
|
|
83
|
-
|
|
84
|
+
// Flat inspector: Layers always renders full-height by itself (see
|
|
85
|
+
// StudioRightPanel's render gate), so this MUST land on the same
|
|
86
|
+
// radio-style exclusivity setExclusiveRightInspectorPane enforces for
|
|
87
|
+
// the direct in-panel tab click — every OTHER path that reaches here
|
|
88
|
+
// (element select, closing block-params, the header Inspector
|
|
89
|
+
// button, and this function's own callers outside an active
|
|
90
|
+
// inspector tab) would otherwise additively leave both panes `true`
|
|
91
|
+
// and reproduce the "both tabs highlight, only one renders" bug this
|
|
92
|
+
// still-additive branch used to cause under the flat flag.
|
|
93
|
+
setRightInspectorPanes(
|
|
94
|
+
STUDIO_FLAT_INSPECTOR_ENABLED
|
|
95
|
+
? { design: tab === "design", layers: tab === "layers" }
|
|
96
|
+
: (panes) => ({ ...panes, [tab]: true }),
|
|
97
|
+
);
|
|
84
98
|
}
|
|
85
99
|
setRightPanelTab(tab);
|
|
86
100
|
trackStudioEvent("tab_switch", { panel: "right_panel", tab });
|
|
@@ -96,6 +110,14 @@ export function usePanelLayout(initialState?: InitialPanelLayoutState) {
|
|
|
96
110
|
});
|
|
97
111
|
}, []);
|
|
98
112
|
|
|
113
|
+
// Radio-style variant for the flat inspector: Layers always renders full-
|
|
114
|
+
// height by itself there (never split-shared with Design), so leaving both
|
|
115
|
+
// panes independently toggleable would highlight both tabs as "active"
|
|
116
|
+
// while only one actually shows. Selecting one turns the other off.
|
|
117
|
+
const setExclusiveRightInspectorPane = useCallback((pane: RightInspectorPane) => {
|
|
118
|
+
setRightInspectorPanes({ design: pane === "design", layers: pane === "layers" });
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
99
121
|
return {
|
|
100
122
|
leftWidth,
|
|
101
123
|
setLeftWidth,
|
|
@@ -109,6 +131,7 @@ export function usePanelLayout(initialState?: InitialPanelLayoutState) {
|
|
|
109
131
|
setRightPanelTab: trackedSetRightPanelTab,
|
|
110
132
|
rightInspectorPanes,
|
|
111
133
|
toggleRightInspectorPane,
|
|
134
|
+
setExclusiveRightInspectorPane,
|
|
112
135
|
toggleLeftSidebar,
|
|
113
136
|
handlePanelResizeStart,
|
|
114
137
|
handlePanelResizeMove,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { createMemoryEditHistoryStorage } from "../utils/editHistoryStorage";
|
|
6
|
+
import { usePersistentEditHistory } from "./usePersistentEditHistory";
|
|
7
|
+
|
|
8
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
async function flushAsyncEffects(): Promise<void> {
|
|
11
|
+
await act(async () => {
|
|
12
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("usePersistentEditHistory project ownership", () => {
|
|
17
|
+
it("rejects a delayed project A recorder after project B becomes active", async () => {
|
|
18
|
+
const storage = createMemoryEditHistoryStorage();
|
|
19
|
+
const now = () => 100;
|
|
20
|
+
const captured: { history: ReturnType<typeof usePersistentEditHistory> | null } = {
|
|
21
|
+
history: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function Probe({ projectId }: { projectId: string }) {
|
|
25
|
+
captured.history = usePersistentEditHistory({ projectId, storage, now });
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const root = createRoot(document.createElement("div"));
|
|
30
|
+
await act(async () => root.render(<Probe projectId="project-a" />));
|
|
31
|
+
await flushAsyncEffects();
|
|
32
|
+
const recordProjectA = captured.history?.recordEdit;
|
|
33
|
+
if (!recordProjectA) throw new Error("project A history did not load");
|
|
34
|
+
|
|
35
|
+
await act(async () => root.render(<Probe projectId="project-b" />));
|
|
36
|
+
await expect(
|
|
37
|
+
recordProjectA({
|
|
38
|
+
label: "Delayed A edit",
|
|
39
|
+
kind: "manual",
|
|
40
|
+
files: { "index.html": { before: "A", after: "A2" } },
|
|
41
|
+
}),
|
|
42
|
+
).rejects.toThrow("inactive project project-a");
|
|
43
|
+
await flushAsyncEffects();
|
|
44
|
+
|
|
45
|
+
const recordProjectB = captured.history?.recordEdit;
|
|
46
|
+
if (!recordProjectB) throw new Error("project B history did not load");
|
|
47
|
+
await act(async () => {
|
|
48
|
+
await recordProjectB({
|
|
49
|
+
label: "B edit",
|
|
50
|
+
kind: "manual",
|
|
51
|
+
files: { "index.html": { before: "B", after: "B2" } },
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(await storage.get("project-a")).toBeNull();
|
|
56
|
+
expect((await storage.get("project-b"))?.undo.map((entry) => entry.label)).toEqual(["B edit"]);
|
|
57
|
+
|
|
58
|
+
await act(async () => root.unmount());
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { createEmptyEditHistory } from "../utils/editHistory";
|
|
3
3
|
import type { EditHistoryStorageAdapter } from "../utils/editHistoryStorage";
|
|
4
4
|
import { createMemoryEditHistoryStorage } from "../utils/editHistoryStorage";
|
|
5
|
+
import {
|
|
6
|
+
serializeStudioFileMutation,
|
|
7
|
+
serializeStudioFileMutations,
|
|
8
|
+
} from "../utils/studioFileMutationCoordinator";
|
|
5
9
|
import {
|
|
6
10
|
createPersistentEditHistoryController,
|
|
7
11
|
createPersistentEditHistoryStore,
|
|
@@ -213,6 +217,50 @@ describe("createPersistentEditHistoryController", () => {
|
|
|
213
217
|
expect(store.snapshot().canRedo).toBe(true);
|
|
214
218
|
});
|
|
215
219
|
|
|
220
|
+
it("waits for same-file mutations before checking an undo hash", async () => {
|
|
221
|
+
const storage = createMemoryEditHistoryStorage();
|
|
222
|
+
const store = createPersistentEditHistoryStore({
|
|
223
|
+
projectId: "project-1",
|
|
224
|
+
storage,
|
|
225
|
+
initialState: createEmptyEditHistory(),
|
|
226
|
+
now: () => 100,
|
|
227
|
+
onChange: () => {},
|
|
228
|
+
});
|
|
229
|
+
await store.recordEdit({
|
|
230
|
+
label: "Edit source",
|
|
231
|
+
kind: "source",
|
|
232
|
+
files: { "index.html": { before: "before", after: "after" } },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
let disk = "after";
|
|
236
|
+
let release!: () => void;
|
|
237
|
+
const blocked = new Promise<void>((resolve) => {
|
|
238
|
+
release = resolve;
|
|
239
|
+
});
|
|
240
|
+
const writeFile = vi.fn(async (_path: string, content: string) => {
|
|
241
|
+
disk = content;
|
|
242
|
+
});
|
|
243
|
+
const priorMutation = serializeStudioFileMutation(writeFile, "index.html", async () => {
|
|
244
|
+
await blocked;
|
|
245
|
+
disk = "newer-edit";
|
|
246
|
+
});
|
|
247
|
+
const readFile = vi.fn(async () => disk);
|
|
248
|
+
const undo = store.undo({
|
|
249
|
+
readFile,
|
|
250
|
+
writeFile,
|
|
251
|
+
serialize: (paths, task) => serializeStudioFileMutations(writeFile, paths, task),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await Promise.resolve();
|
|
255
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
256
|
+
release();
|
|
257
|
+
await priorMutation;
|
|
258
|
+
await expect(undo).resolves.toMatchObject({ ok: false, reason: "content-mismatch" });
|
|
259
|
+
expect(disk).toBe("newer-edit");
|
|
260
|
+
expect(writeFile).not.toHaveBeenCalled();
|
|
261
|
+
expect(store.snapshot().canUndo).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
216
264
|
it("returns per-file restored/previous content so the preview can soft-apply", async () => {
|
|
217
265
|
const storage = createMemoryEditHistoryStorage();
|
|
218
266
|
const store = createPersistentEditHistoryStore({
|