@hyperframes/studio 0.7.58 → 0.7.59
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-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-B995FG46.js} +1 -1
- package/dist/assets/index-CrkAdJkb.js +426 -0
- package/dist/assets/index-Dj5p8U_A.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-FvzmPhfG.js} +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13514 -8528
- 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 +3 -3
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +29 -51
- 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 +101 -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/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/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.ts +29 -175
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +17 -5
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
- package/src/hooks/useFileManager.ts +82 -60
- 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/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- 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/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 +16 -10
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTimelineGroupEditing.ts +16 -2
- 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/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +490 -57
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +292 -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 +30 -27
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.ts +17 -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,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({
|
|
@@ -30,6 +30,7 @@ interface RecordEditInput {
|
|
|
30
30
|
interface ApplyCallbacks {
|
|
31
31
|
readFile: (path: string) => Promise<string>;
|
|
32
32
|
writeFile: (path: string, content: string) => Promise<void>;
|
|
33
|
+
serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
interface UsePersistentEditHistoryOptions {
|
|
@@ -167,31 +168,32 @@ async function applyHistoryStep(
|
|
|
167
168
|
if (!entry) {
|
|
168
169
|
return { state: currentState, result: { ok: false, reason: "empty" } };
|
|
169
170
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
callbacks.readFile
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
const paths = Object.keys(entry.files);
|
|
172
|
+
const apply = async (): Promise<{ state: EditHistoryState; result: ApplyResult }> => {
|
|
173
|
+
const { currentFiles, currentHashes } = await readCurrentFileHashes(paths, callbacks.readFile);
|
|
174
|
+
const result = transition(currentState, currentHashes, now());
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
return {
|
|
177
|
+
state: currentState,
|
|
178
|
+
result: { ok: false, reason: result.reason },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
await writeFilesWithRollback({
|
|
182
|
+
files: result.filesToWrite,
|
|
183
|
+
rollbackFiles: currentFiles,
|
|
184
|
+
writeFile: callbacks.writeFile,
|
|
185
|
+
});
|
|
176
186
|
return {
|
|
177
|
-
state:
|
|
178
|
-
result: {
|
|
187
|
+
state: result.state,
|
|
188
|
+
result: {
|
|
189
|
+
ok: true,
|
|
190
|
+
label: result.entry.label,
|
|
191
|
+
paths: Object.keys(result.entry.files),
|
|
192
|
+
files: restoredFilesMap(result.filesToWrite, currentFiles),
|
|
193
|
+
},
|
|
179
194
|
};
|
|
180
|
-
}
|
|
181
|
-
await writeFilesWithRollback({
|
|
182
|
-
files: result.filesToWrite,
|
|
183
|
-
rollbackFiles: currentFiles,
|
|
184
|
-
writeFile: callbacks.writeFile,
|
|
185
|
-
});
|
|
186
|
-
return {
|
|
187
|
-
state: result.state,
|
|
188
|
-
result: {
|
|
189
|
-
ok: true,
|
|
190
|
-
label: result.entry.label,
|
|
191
|
-
paths: Object.keys(result.entry.files),
|
|
192
|
-
files: restoredFilesMap(result.filesToWrite, currentFiles),
|
|
193
|
-
},
|
|
194
195
|
};
|
|
196
|
+
return callbacks.serialize ? callbacks.serialize(paths, apply) : apply();
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
export function createPersistentEditHistoryStore({
|
|
@@ -305,11 +307,15 @@ export function usePersistentEditHistory(options: UsePersistentEditHistoryOption
|
|
|
305
307
|
const [loaded, setLoaded] = useState(false);
|
|
306
308
|
const projectId = options.projectId;
|
|
307
309
|
const storeRef = useRef<ReturnType<typeof createPersistentEditHistoryStore> | null>(null);
|
|
310
|
+
const storeProjectIdRef = useRef<string | null>(null);
|
|
311
|
+
const activeProjectIdRef = useRef(projectId);
|
|
312
|
+
activeProjectIdRef.current = projectId;
|
|
308
313
|
|
|
309
314
|
useEffect(() => {
|
|
310
315
|
let cancelled = false;
|
|
311
316
|
const emptyState = createEmptyEditHistory();
|
|
312
317
|
storeRef.current = null;
|
|
318
|
+
storeProjectIdRef.current = null;
|
|
313
319
|
setState(emptyState);
|
|
314
320
|
setLoaded(false);
|
|
315
321
|
if (!projectId) {
|
|
@@ -327,6 +333,7 @@ export function usePersistentEditHistory(options: UsePersistentEditHistoryOption
|
|
|
327
333
|
now,
|
|
328
334
|
onChange: setState,
|
|
329
335
|
});
|
|
336
|
+
storeProjectIdRef.current = projectId;
|
|
330
337
|
setState(loadedState);
|
|
331
338
|
})
|
|
332
339
|
.catch(() => {
|
|
@@ -338,6 +345,7 @@ export function usePersistentEditHistory(options: UsePersistentEditHistoryOption
|
|
|
338
345
|
now,
|
|
339
346
|
onChange: setState,
|
|
340
347
|
});
|
|
348
|
+
storeProjectIdRef.current = projectId;
|
|
341
349
|
setState(emptyState);
|
|
342
350
|
})
|
|
343
351
|
.finally(() => {
|
|
@@ -349,17 +357,49 @@ export function usePersistentEditHistory(options: UsePersistentEditHistoryOption
|
|
|
349
357
|
};
|
|
350
358
|
}, [now, projectId, storage]);
|
|
351
359
|
|
|
352
|
-
const recordEdit = useCallback(
|
|
353
|
-
|
|
354
|
-
|
|
360
|
+
const recordEdit = useCallback(
|
|
361
|
+
async (input: RecordEditInput) => {
|
|
362
|
+
if (!projectId) return;
|
|
363
|
+
if (activeProjectIdRef.current !== projectId) {
|
|
364
|
+
throw new Error(`Cannot record an edit for inactive project ${projectId}`);
|
|
365
|
+
}
|
|
366
|
+
const store = storeRef.current;
|
|
367
|
+
if (!store) return;
|
|
368
|
+
if (storeProjectIdRef.current !== projectId) {
|
|
369
|
+
throw new Error(`Edit history store does not belong to project ${projectId}`);
|
|
370
|
+
}
|
|
371
|
+
await store.recordEdit(input);
|
|
372
|
+
},
|
|
373
|
+
[projectId],
|
|
374
|
+
);
|
|
355
375
|
|
|
356
|
-
const undo = useCallback(
|
|
357
|
-
|
|
358
|
-
|
|
376
|
+
const undo = useCallback(
|
|
377
|
+
async (callbacks: ApplyCallbacks): Promise<ApplyResult> => {
|
|
378
|
+
if (
|
|
379
|
+
!projectId ||
|
|
380
|
+
activeProjectIdRef.current !== projectId ||
|
|
381
|
+
storeProjectIdRef.current !== projectId
|
|
382
|
+
) {
|
|
383
|
+
return { ok: false, reason: "empty" };
|
|
384
|
+
}
|
|
385
|
+
return storeRef.current?.undo(callbacks) ?? { ok: false, reason: "empty" };
|
|
386
|
+
},
|
|
387
|
+
[projectId],
|
|
388
|
+
);
|
|
359
389
|
|
|
360
|
-
const redo = useCallback(
|
|
361
|
-
|
|
362
|
-
|
|
390
|
+
const redo = useCallback(
|
|
391
|
+
async (callbacks: ApplyCallbacks): Promise<ApplyResult> => {
|
|
392
|
+
if (
|
|
393
|
+
!projectId ||
|
|
394
|
+
activeProjectIdRef.current !== projectId ||
|
|
395
|
+
storeProjectIdRef.current !== projectId
|
|
396
|
+
) {
|
|
397
|
+
return { ok: false, reason: "empty" };
|
|
398
|
+
}
|
|
399
|
+
return storeRef.current?.redo(callbacks) ?? { ok: false, reason: "empty" };
|
|
400
|
+
},
|
|
401
|
+
[projectId],
|
|
402
|
+
);
|
|
363
403
|
|
|
364
404
|
return {
|
|
365
405
|
loaded,
|
|
@@ -103,17 +103,16 @@ export function useEditVariablesInFile(deps: EditVariablesDeps) {
|
|
|
103
103
|
return useCallback(
|
|
104
104
|
async (path: string, label: string, mutate: (session: Composition) => void): Promise<void> => {
|
|
105
105
|
const originalContent = await readProjectFile(path);
|
|
106
|
-
const comp = await openComposition(originalContent, { history: false });
|
|
107
|
-
let after: string;
|
|
108
|
-
try {
|
|
109
|
-
mutate(comp);
|
|
110
|
-
after = comp.serialize();
|
|
111
|
-
} finally {
|
|
112
|
-
comp.dispose();
|
|
113
|
-
}
|
|
114
|
-
if (after === originalContent) return;
|
|
115
106
|
await persistSdkSerialize(
|
|
116
|
-
|
|
107
|
+
async (onDiskBefore) => {
|
|
108
|
+
const comp = await openComposition(onDiskBefore, { history: false });
|
|
109
|
+
try {
|
|
110
|
+
mutate(comp);
|
|
111
|
+
return comp.serialize();
|
|
112
|
+
} finally {
|
|
113
|
+
comp.dispose();
|
|
114
|
+
}
|
|
115
|
+
},
|
|
117
116
|
path,
|
|
118
117
|
originalContent,
|
|
119
118
|
{
|
|
@@ -122,6 +121,7 @@ export function useEditVariablesInFile(deps: EditVariablesDeps) {
|
|
|
122
121
|
reloadPreview,
|
|
123
122
|
domEditSaveTimestampRef,
|
|
124
123
|
compositionPath: path,
|
|
124
|
+
readProjectFile,
|
|
125
125
|
},
|
|
126
126
|
{ label },
|
|
127
127
|
);
|