@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
|
@@ -1108,6 +1108,7 @@ describe("useTimelineEditing duration rollback on failed persist", () => {
|
|
|
1108
1108
|
stubProjectFetch(ROLLBACK_SOURCE);
|
|
1109
1109
|
usePlayerStore.getState().setDuration(4);
|
|
1110
1110
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1111
|
+
const showToast = vi.fn();
|
|
1111
1112
|
const hook = renderTimelineEditingHook({
|
|
1112
1113
|
timelineElements: [clip],
|
|
1113
1114
|
iframe,
|
|
@@ -1116,8 +1117,9 @@ describe("useTimelineEditing duration rollback on failed persist", () => {
|
|
|
1116
1117
|
writeProjectFile,
|
|
1117
1118
|
recordEdit: vi.fn(async () => {}),
|
|
1118
1119
|
reloadPreview: vi.fn(),
|
|
1120
|
+
showToast,
|
|
1119
1121
|
});
|
|
1120
|
-
return { iframe, clip, hook, writeError };
|
|
1122
|
+
return { iframe, clip, hook, showToast, writeError };
|
|
1121
1123
|
}
|
|
1122
1124
|
|
|
1123
1125
|
/**
|
|
@@ -1137,6 +1139,7 @@ describe("useTimelineEditing duration rollback on failed persist", () => {
|
|
|
1137
1139
|
await flushAsyncWork();
|
|
1138
1140
|
});
|
|
1139
1141
|
expect(rejection).toBe(ctx.writeError);
|
|
1142
|
+
expect(ctx.showToast).toHaveBeenCalledWith("write failed", "error");
|
|
1140
1143
|
expect(usePlayerStore.getState().duration).toBe(4);
|
|
1141
1144
|
expect(rootDurationAttr(ctx.iframe)).toBe("4");
|
|
1142
1145
|
}
|
|
@@ -32,8 +32,9 @@ import {
|
|
|
32
32
|
} from "./timelineTrackVisibility";
|
|
33
33
|
import { useTimelineGroupEditing } from "./useTimelineGroupEditing";
|
|
34
34
|
import { serializeZLaneGesture } from "../components/nle/zLaneGesture";
|
|
35
|
-
import { sdkTimingPersist } from "../utils/sdkCutover";
|
|
35
|
+
import { cutoverCommittedOrThrow, sdkTimingPersist } from "../utils/sdkCutover";
|
|
36
36
|
import type { UseTimelineEditingOptions } from "./useTimelineEditingTypes";
|
|
37
|
+
import { getStudioSaveErrorMessage } from "../utils/studioSaveDiagnostics";
|
|
37
38
|
|
|
38
39
|
type TimelineMoveUpdates = Pick<TimelineElement, "start" | "track"> & {
|
|
39
40
|
stackingReorder?: TimelineStackingReorderIntent | null;
|
|
@@ -53,6 +54,7 @@ export function useTimelineEditing({
|
|
|
53
54
|
uploadProjectFiles,
|
|
54
55
|
isRecordingRef,
|
|
55
56
|
sdkSession,
|
|
57
|
+
publishSdkSession,
|
|
56
58
|
forceReloadSdkSession,
|
|
57
59
|
handleDomZIndexReorderCommitRef,
|
|
58
60
|
}: UseTimelineEditingOptions) {
|
|
@@ -121,10 +123,10 @@ export function useTimelineEditing({
|
|
|
121
123
|
recordEdit,
|
|
122
124
|
reloadPreview,
|
|
123
125
|
sdkSession,
|
|
126
|
+
publishSdkSession,
|
|
124
127
|
showToast,
|
|
125
128
|
writeProjectFile,
|
|
126
129
|
});
|
|
127
|
-
|
|
128
130
|
const handleTimelineElementMove = useCallback(
|
|
129
131
|
// fallow-ignore-next-line complexity
|
|
130
132
|
(element: TimelineElement, updates: TimelineMoveUpdates) => {
|
|
@@ -215,10 +217,11 @@ export function useTimelineEditing({
|
|
|
215
217
|
// Capture on-disk bytes as the undo `before` so undoing a timing move
|
|
216
218
|
// restores the file verbatim, not a normalized full-DOM re-emit.
|
|
217
219
|
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
220
|
+
publishSession: publishSdkSession,
|
|
218
221
|
},
|
|
219
222
|
{ label: "Move timeline clip", coalesceKey },
|
|
220
|
-
).then((
|
|
221
|
-
if (!
|
|
223
|
+
).then((result) => {
|
|
224
|
+
if (!cutoverCommittedOrThrow(result)) return moveFallback();
|
|
222
225
|
});
|
|
223
226
|
}
|
|
224
227
|
return moveFallback();
|
|
@@ -226,6 +229,7 @@ export function useTimelineEditing({
|
|
|
226
229
|
.catch((error) => {
|
|
227
230
|
// Failed persist: revert the optimistic duration readout + live root.
|
|
228
231
|
rollbackDuration();
|
|
232
|
+
showToast(getStudioSaveErrorMessage(error), "error");
|
|
229
233
|
throw error;
|
|
230
234
|
});
|
|
231
235
|
};
|
|
@@ -236,12 +240,14 @@ export function useTimelineEditing({
|
|
|
236
240
|
enqueueEdit,
|
|
237
241
|
activeCompPath,
|
|
238
242
|
sdkSession,
|
|
243
|
+
publishSdkSession,
|
|
239
244
|
recordEdit,
|
|
240
245
|
writeProjectFile,
|
|
241
246
|
reloadPreview,
|
|
242
247
|
domEditSaveTimestampRef,
|
|
243
248
|
timelineElements,
|
|
244
249
|
handleDomZIndexReorderCommitRef,
|
|
250
|
+
showToast,
|
|
245
251
|
],
|
|
246
252
|
);
|
|
247
253
|
|
|
@@ -255,9 +261,6 @@ export function useTimelineEditing({
|
|
|
255
261
|
["data-start", formatTimelineAttributeNumber(updates.start)],
|
|
256
262
|
["data-duration", formatTimelineAttributeNumber(updates.duration)],
|
|
257
263
|
];
|
|
258
|
-
// Patch the live playback-start/media-start attr too, or a resize that
|
|
259
|
-
// trims the playback start leaves the preview showing the old in-point
|
|
260
|
-
// until the next reload (the persisted patch handles it via pbs below).
|
|
261
264
|
if (updates.playbackStart != null) {
|
|
262
265
|
const liveAttr =
|
|
263
266
|
element.playbackStartAttr === "playback-start"
|
|
@@ -319,15 +322,17 @@ export function useTimelineEditing({
|
|
|
319
322
|
// Capture on-disk bytes as the undo `before` so undoing a timing
|
|
320
323
|
// resize restores the file verbatim, not a normalized full-DOM re-emit.
|
|
321
324
|
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
325
|
+
publishSession: publishSdkSession,
|
|
322
326
|
},
|
|
323
327
|
{ label: "Resize timeline clip", coalesceKey },
|
|
324
|
-
).then((
|
|
325
|
-
if (!
|
|
328
|
+
).then((result) => {
|
|
329
|
+
if (!cutoverCommittedOrThrow(result)) return resizeFallback();
|
|
326
330
|
})
|
|
327
331
|
: resizeFallback();
|
|
328
332
|
return persistDone.catch((error) => {
|
|
329
333
|
// Failed persist: revert the optimistic duration readout + live root.
|
|
330
334
|
rollbackDuration();
|
|
335
|
+
showToast(getStudioSaveErrorMessage(error), "error");
|
|
331
336
|
throw error;
|
|
332
337
|
});
|
|
333
338
|
},
|
|
@@ -336,10 +341,12 @@ export function useTimelineEditing({
|
|
|
336
341
|
enqueueEdit,
|
|
337
342
|
activeCompPath,
|
|
338
343
|
sdkSession,
|
|
344
|
+
publishSdkSession,
|
|
339
345
|
recordEdit,
|
|
340
346
|
writeProjectFile,
|
|
341
347
|
reloadPreview,
|
|
342
348
|
domEditSaveTimestampRef,
|
|
349
|
+
showToast,
|
|
343
350
|
],
|
|
344
351
|
);
|
|
345
352
|
|
|
@@ -360,7 +367,6 @@ export function useTimelineEditing({
|
|
|
360
367
|
const handleToggleElementHidden = useTimelineElementVisibilityEditing({
|
|
361
368
|
projectIdRef,
|
|
362
369
|
activeCompPath,
|
|
363
|
-
timelineElements,
|
|
364
370
|
showToast,
|
|
365
371
|
writeProjectFile,
|
|
366
372
|
recordEdit,
|
|
@@ -2,6 +2,7 @@ import type { MutableRefObject, RefObject } from "react";
|
|
|
2
2
|
import type { Composition } from "@hyperframes/sdk";
|
|
3
3
|
import type { TimelineElement } from "../player";
|
|
4
4
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
5
|
+
import type { PublishSdkSession } from "../utils/sdkCutover";
|
|
5
6
|
|
|
6
7
|
interface RecordEditInput {
|
|
7
8
|
label: string;
|
|
@@ -40,6 +41,8 @@ export interface UseTimelineEditingOptions {
|
|
|
40
41
|
isRecordingRef?: RefObject<boolean>;
|
|
41
42
|
/** Stage 7 §3.2: SDK session for routing timing ops through setTiming. */
|
|
42
43
|
sdkSession?: Composition | null;
|
|
44
|
+
/** Publish a fully persisted candidate SDK session. */
|
|
45
|
+
publishSdkSession?: PublishSdkSession;
|
|
43
46
|
/** Resync the SDK session after a server-authoritative timeline write. */
|
|
44
47
|
forceReloadSdkSession?: () => void;
|
|
45
48
|
handleDomZIndexReorderCommitRef?: MutableRefObject<TimelineZIndexReorderCommit | null>;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { useCallback, type MutableRefObject, type RefObject } from "react";
|
|
2
2
|
import type { Composition } from "@hyperframes/sdk";
|
|
3
3
|
import type { TimelineElement } from "../player";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
cutoverCommittedOrThrow,
|
|
6
|
+
sdkTimingBatchPersist,
|
|
7
|
+
type PublishSdkSession,
|
|
8
|
+
} from "../utils/sdkCutover";
|
|
5
9
|
import {
|
|
6
10
|
buildTimelineMoveTimingPatch,
|
|
7
11
|
buildTimelineResizeTimingPatch,
|
|
@@ -20,6 +24,7 @@ import {
|
|
|
20
24
|
shiftGsapPositions,
|
|
21
25
|
syncPreviewContentDuration,
|
|
22
26
|
} from "./timelineTimingSync";
|
|
27
|
+
import { getStudioSaveErrorMessage } from "../utils/studioSaveDiagnostics";
|
|
23
28
|
|
|
24
29
|
export interface TimelineGroupMoveChange {
|
|
25
30
|
element: TimelineElement;
|
|
@@ -53,6 +58,7 @@ interface UseTimelineGroupEditingOptions {
|
|
|
53
58
|
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
54
59
|
reloadPreview: () => void;
|
|
55
60
|
sdkSession?: Composition | null;
|
|
61
|
+
publishSdkSession?: PublishSdkSession;
|
|
56
62
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
57
63
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
58
64
|
}
|
|
@@ -108,6 +114,7 @@ export function useTimelineGroupEditing({
|
|
|
108
114
|
recordEdit,
|
|
109
115
|
reloadPreview,
|
|
110
116
|
sdkSession,
|
|
117
|
+
publishSdkSession,
|
|
111
118
|
showToast,
|
|
112
119
|
writeProjectFile,
|
|
113
120
|
}: UseTimelineGroupEditingOptions) {
|
|
@@ -189,7 +196,7 @@ export function useTimelineGroupEditing({
|
|
|
189
196
|
input.eligible &&
|
|
190
197
|
input.sdkChanges.every((change) => change !== null);
|
|
191
198
|
if (!canUseSdk) return false;
|
|
192
|
-
|
|
199
|
+
const result = await sdkTimingBatchPersist(
|
|
193
200
|
input.sdkChanges.filter((change): change is NonNullable<typeof change> => change !== null),
|
|
194
201
|
sharedPath,
|
|
195
202
|
sdkSession,
|
|
@@ -200,14 +207,17 @@ export function useTimelineGroupEditing({
|
|
|
200
207
|
domEditSaveTimestampRef,
|
|
201
208
|
compositionPath: activeCompPath,
|
|
202
209
|
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
210
|
+
publishSession: publishSdkSession,
|
|
203
211
|
},
|
|
204
212
|
{ label: input.label, coalesceKey: input.coalesceKey, coalesceMs: input.coalesceMs },
|
|
205
213
|
);
|
|
214
|
+
return cutoverCommittedOrThrow(result);
|
|
206
215
|
},
|
|
207
216
|
[
|
|
208
217
|
activeCompPath,
|
|
209
218
|
domEditSaveTimestampRef,
|
|
210
219
|
projectIdRef,
|
|
220
|
+
publishSdkSession,
|
|
211
221
|
recordEdit,
|
|
212
222
|
reloadPreview,
|
|
213
223
|
sdkSession,
|
|
@@ -313,6 +323,7 @@ export function useTimelineGroupEditing({
|
|
|
313
323
|
// Failed persist: revert the optimistic duration readout + live root
|
|
314
324
|
// alongside the gesture owner's store rollback.
|
|
315
325
|
rollbackDuration();
|
|
326
|
+
showToast(getStudioSaveErrorMessage(error), "error");
|
|
316
327
|
throw error;
|
|
317
328
|
});
|
|
318
329
|
},
|
|
@@ -324,6 +335,7 @@ export function useTimelineGroupEditing({
|
|
|
324
335
|
recordEdit,
|
|
325
336
|
reloadPreview,
|
|
326
337
|
trySdkBatchPersist,
|
|
338
|
+
showToast,
|
|
327
339
|
],
|
|
328
340
|
);
|
|
329
341
|
|
|
@@ -419,6 +431,7 @@ export function useTimelineGroupEditing({
|
|
|
419
431
|
// Failed persist: revert the optimistic duration readout + live root
|
|
420
432
|
// alongside the gesture owner's store rollback.
|
|
421
433
|
rollbackDuration();
|
|
434
|
+
showToast(getStudioSaveErrorMessage(error), "error");
|
|
422
435
|
throw error;
|
|
423
436
|
});
|
|
424
437
|
},
|
|
@@ -430,6 +443,7 @@ export function useTimelineGroupEditing({
|
|
|
430
443
|
recordEdit,
|
|
431
444
|
reloadPreview,
|
|
432
445
|
trySdkBatchPersist,
|
|
446
|
+
showToast,
|
|
433
447
|
],
|
|
434
448
|
);
|
|
435
449
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import type { Composition } from "@hyperframes/sdk";
|
|
3
|
-
import {
|
|
3
|
+
import { cutoverCommittedOrThrow, persistSdkCandidateMutation } from "../utils/sdkCutover";
|
|
4
4
|
import type { UseSlideshowPersistParams } from "./useSlideshowPersist";
|
|
5
5
|
|
|
6
6
|
/** Same single-writer dependency set the slideshow persist path uses. */
|
|
@@ -21,6 +21,7 @@ export function useVariablesPersist({
|
|
|
21
21
|
recordEdit,
|
|
22
22
|
reloadPreview,
|
|
23
23
|
domEditSaveTimestampRef,
|
|
24
|
+
publishSdkSession,
|
|
24
25
|
}: UseVariablesPersistParams): (
|
|
25
26
|
label: string,
|
|
26
27
|
mutate: (session: Composition) => void,
|
|
@@ -30,11 +31,8 @@ export function useVariablesPersist({
|
|
|
30
31
|
if (!sdkSession) return false;
|
|
31
32
|
const path = activeCompPath ?? "index.html";
|
|
32
33
|
const originalContent = await readProjectFile(path);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (after === originalContent) return false;
|
|
36
|
-
await persistSdkSerialize(
|
|
37
|
-
after,
|
|
34
|
+
const result = await persistSdkCandidateMutation(
|
|
35
|
+
sdkSession,
|
|
38
36
|
path,
|
|
39
37
|
originalContent,
|
|
40
38
|
{
|
|
@@ -43,10 +41,13 @@ export function useVariablesPersist({
|
|
|
43
41
|
reloadPreview,
|
|
44
42
|
domEditSaveTimestampRef,
|
|
45
43
|
compositionPath: activeCompPath,
|
|
44
|
+
readProjectFile,
|
|
45
|
+
publishSession: publishSdkSession,
|
|
46
46
|
},
|
|
47
|
+
mutate,
|
|
47
48
|
{ label },
|
|
48
49
|
);
|
|
49
|
-
return
|
|
50
|
+
return cutoverCommittedOrThrow(result);
|
|
50
51
|
},
|
|
51
52
|
[
|
|
52
53
|
sdkSession,
|
|
@@ -56,6 +57,7 @@ export function useVariablesPersist({
|
|
|
56
57
|
recordEdit,
|
|
57
58
|
reloadPreview,
|
|
58
59
|
domEditSaveTimestampRef,
|
|
60
|
+
publishSdkSession,
|
|
59
61
|
],
|
|
60
62
|
);
|
|
61
63
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
ArrowClockwise,
|
|
22
22
|
Gear,
|
|
23
23
|
Scissors as PhScissors,
|
|
24
|
+
Link as PhLink,
|
|
24
25
|
} from "@phosphor-icons/react";
|
|
25
26
|
import type { Icon as PhosphorIcon, IconProps as PhosphorIconProps } from "@phosphor-icons/react";
|
|
26
27
|
|
|
@@ -69,3 +70,4 @@ export const Camera = makeIcon(PhCamera);
|
|
|
69
70
|
export const RotateCw = makeIcon(ArrowClockwise);
|
|
70
71
|
export const Settings = makeIcon(Gear);
|
|
71
72
|
export const Scissors = makeIcon(PhScissors);
|
|
73
|
+
export const Link = makeIcon(PhLink);
|
package/src/styles/studio.css
CHANGED
|
@@ -447,3 +447,25 @@ body {
|
|
|
447
447
|
animation: none;
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
|
+
|
|
451
|
+
/* Flat inspector group header/body entrance (FlatGroupHeader) */
|
|
452
|
+
@keyframes hf-flat-group-in {
|
|
453
|
+
from {
|
|
454
|
+
opacity: 0;
|
|
455
|
+
transform: translateY(-4px);
|
|
456
|
+
}
|
|
457
|
+
to {
|
|
458
|
+
opacity: 1;
|
|
459
|
+
transform: translateY(0);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.hf-flat-group-enter {
|
|
464
|
+
animation: hf-flat-group-in 120ms ease-out;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@media (prefers-reduced-motion: reduce) {
|
|
468
|
+
.hf-flat-group-enter {
|
|
469
|
+
animation: none;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -12,11 +12,16 @@ const studioPreset = {
|
|
|
12
12
|
},
|
|
13
13
|
panel: {
|
|
14
14
|
bg: "#0C0C0E",
|
|
15
|
+
// Open inspector-section body — slightly lighter than headers (bg)
|
|
16
|
+
// so the recessed scrollable region reads distinct.
|
|
17
|
+
"bg-inset": "#121214",
|
|
15
18
|
input: "#161618",
|
|
16
19
|
surface: "#18181B",
|
|
17
20
|
hover: "#27272A",
|
|
18
21
|
border: "#1E1E1E",
|
|
19
22
|
"border-input": "#27272A",
|
|
23
|
+
hairline: "#1A1A1C",
|
|
24
|
+
"text-0": "#FAFAFA",
|
|
20
25
|
"text-1": "#E4E4E7",
|
|
21
26
|
"text-2": "#A1A1AA",
|
|
22
27
|
"text-3": "#71717A",
|
|
@@ -24,6 +29,8 @@ const studioPreset = {
|
|
|
24
29
|
"text-5": "#3F3F46",
|
|
25
30
|
accent: "#3CE6AC",
|
|
26
31
|
danger: "#EF4444",
|
|
32
|
+
media: "#00E3FF",
|
|
33
|
+
container: "#F5A623",
|
|
27
34
|
},
|
|
28
35
|
},
|
|
29
36
|
},
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const trackStudioEvent = vi.fn();
|
|
4
|
+
vi.mock("./studioTelemetry", () => ({
|
|
5
|
+
trackStudioEvent: (...args: unknown[]) => trackStudioEvent(...args),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
__resetDesignInputThrottle,
|
|
10
|
+
slugifyDesignInput,
|
|
11
|
+
trackDesignInput,
|
|
12
|
+
} from "./designInputTracking";
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
trackStudioEvent.mockReset();
|
|
16
|
+
__resetDesignInputThrottle();
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("trackDesignInput", () => {
|
|
25
|
+
it("emits one design_input event with ui/section/control/name", () => {
|
|
26
|
+
trackDesignInput({ ui: "flat", section: "Style", control: "metric", name: "Opacity" });
|
|
27
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(1);
|
|
28
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("design_input", {
|
|
29
|
+
ui: "flat",
|
|
30
|
+
section: "style",
|
|
31
|
+
control: "metric",
|
|
32
|
+
name: "opacity",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("slugifies compound names and sections", () => {
|
|
37
|
+
trackDesignInput({
|
|
38
|
+
ui: "classic",
|
|
39
|
+
section: "Color Grading",
|
|
40
|
+
control: "slider",
|
|
41
|
+
name: "Font Size",
|
|
42
|
+
});
|
|
43
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("design_input", {
|
|
44
|
+
ui: "classic",
|
|
45
|
+
section: "color-grading",
|
|
46
|
+
control: "slider",
|
|
47
|
+
name: "font-size",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("marks an unresolved name as 'unnamed' (R3 coverage signal)", () => {
|
|
52
|
+
trackDesignInput({ ui: "classic", section: "style", control: "button", name: "" });
|
|
53
|
+
expect(trackStudioEvent).toHaveBeenCalledWith(
|
|
54
|
+
"design_input",
|
|
55
|
+
expect.objectContaining({ name: "unnamed" }),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("coalesces repeated fires of the same input within the window (R4)", () => {
|
|
60
|
+
const nowSpy = vi.spyOn(performance, "now");
|
|
61
|
+
// Same key, three quick fires -> 1 event.
|
|
62
|
+
nowSpy.mockReturnValue(1000);
|
|
63
|
+
trackDesignInput({ ui: "flat", section: "style", control: "slider", name: "opacity" });
|
|
64
|
+
nowSpy.mockReturnValue(1100);
|
|
65
|
+
trackDesignInput({ ui: "flat", section: "style", control: "slider", name: "opacity" });
|
|
66
|
+
nowSpy.mockReturnValue(1500);
|
|
67
|
+
trackDesignInput({ ui: "flat", section: "style", control: "slider", name: "opacity" });
|
|
68
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(1);
|
|
69
|
+
|
|
70
|
+
// A different input within the window is NOT collapsed.
|
|
71
|
+
nowSpy.mockReturnValue(1550);
|
|
72
|
+
trackDesignInput({ ui: "flat", section: "style", control: "slider", name: "scale" });
|
|
73
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(2);
|
|
74
|
+
|
|
75
|
+
// Same input after the window fires again.
|
|
76
|
+
nowSpy.mockReturnValue(2200);
|
|
77
|
+
trackDesignInput({ ui: "flat", section: "style", control: "slider", name: "opacity" });
|
|
78
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("never throws even if the underlying tracker throws", () => {
|
|
82
|
+
trackStudioEvent.mockImplementation(() => {
|
|
83
|
+
throw new Error("ingest down");
|
|
84
|
+
});
|
|
85
|
+
expect(() =>
|
|
86
|
+
trackDesignInput({ ui: "flat", section: "style", control: "metric", name: "opacity" }),
|
|
87
|
+
).not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("slugifyDesignInput", () => {
|
|
92
|
+
it("lowercases, collapses non-alphanumerics, and trims dashes", () => {
|
|
93
|
+
expect(slugifyDesignInput(" Border Radius (px) ")).toBe("border-radius-px");
|
|
94
|
+
expect(slugifyDesignInput("X")).toBe("x");
|
|
95
|
+
expect(slugifyDesignInput("---")).toBe("");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { trackStudioEvent } from "./studioTelemetry";
|
|
2
|
+
|
|
3
|
+
// Per-input usage telemetry for the design (inspector) panel. Both inspector UIs
|
|
4
|
+
// (classic PropertyPanel, flat PropertyPanelFlat) funnel their inputs through this
|
|
5
|
+
// one helper so usage can be ranked by input to find removal candidates. Emits the
|
|
6
|
+
// batched `studio:design_input` event via trackStudioEvent (opt-out-aware, never-throw).
|
|
7
|
+
//
|
|
8
|
+
// Fire convention (kept consistent across both UIs):
|
|
9
|
+
// - Discrete controls (metric/text/select/segmented/toggle/color/button) fire only
|
|
10
|
+
// when a real committed value change happens (the commit site guards on
|
|
11
|
+
// next !== current).
|
|
12
|
+
// - Continuous controls (sliders, scrub) fire once per user interaction that
|
|
13
|
+
// produced commits, even if the net value ends unchanged — net-change-at-settle
|
|
14
|
+
// is unreliable for them because mid-drag commits have already advanced the value,
|
|
15
|
+
// so "did the user work this control" is the honest signal. The coalescing window
|
|
16
|
+
// below collapses the many mid-drag commits into that single event.
|
|
17
|
+
|
|
18
|
+
export type DesignInputUi = "flat" | "classic";
|
|
19
|
+
|
|
20
|
+
export interface DesignInputDescriptor {
|
|
21
|
+
ui: DesignInputUi;
|
|
22
|
+
/** Section slug the input lives under (e.g. "style", "color-grading"). */
|
|
23
|
+
section: string;
|
|
24
|
+
/** Control kind: "metric" | "slider" | "select" | "segmented" | "toggle" | "color" | "text" | "button" | … */
|
|
25
|
+
control: string;
|
|
26
|
+
/** Input identity — the field label or CSS/GSAP property. Slugified for stable ranking. */
|
|
27
|
+
name: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Continuous controls (sliders, scrub, wheel-nudge, live-commit text) fire many
|
|
31
|
+
// commits per interaction. Collapse repeated fires of the same input within this
|
|
32
|
+
// window into one event so a single drag counts once (R4).
|
|
33
|
+
const COALESCE_WINDOW_MS = 600;
|
|
34
|
+
|
|
35
|
+
const lastFiredByKey = new Map<string, number>();
|
|
36
|
+
|
|
37
|
+
function now(): number {
|
|
38
|
+
// performance.now() is monotonic and available in the studio runtime and jsdom.
|
|
39
|
+
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
40
|
+
? performance.now()
|
|
41
|
+
: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function slugifyDesignInput(value: string): string {
|
|
45
|
+
return value
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
49
|
+
.replace(/(^-|-$)/g, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Test seam: clear the coalescing state between cases. */
|
|
53
|
+
export function __resetDesignInputThrottle(): void {
|
|
54
|
+
lastFiredByKey.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function trackDesignInput(descriptor: DesignInputDescriptor): void {
|
|
58
|
+
try {
|
|
59
|
+
const section = slugifyDesignInput(descriptor.section) || "unknown";
|
|
60
|
+
const name = slugifyDesignInput(descriptor.name);
|
|
61
|
+
// An input with no resolvable name is useless for the removal analysis (R3).
|
|
62
|
+
// Emit it anyway (so a coverage test can catch it) but under an explicit marker.
|
|
63
|
+
const control = descriptor.control || "unknown";
|
|
64
|
+
const key = `${descriptor.ui}:${section}:${control}:${name || "unnamed"}`;
|
|
65
|
+
|
|
66
|
+
const t = now();
|
|
67
|
+
const last = lastFiredByKey.get(key);
|
|
68
|
+
if (last !== undefined && t - last < COALESCE_WINDOW_MS) return;
|
|
69
|
+
lastFiredByKey.set(key, t);
|
|
70
|
+
|
|
71
|
+
trackStudioEvent("design_input", {
|
|
72
|
+
ui: descriptor.ui,
|
|
73
|
+
section,
|
|
74
|
+
control,
|
|
75
|
+
name: name || "unnamed",
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
// Telemetry must never break the edit path.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
// Dark-launch contract: with STUDIO_SDK_CUTOVER_ENABLED=false, EVERY cutover
|
|
4
|
-
// persist chokepoint must
|
|
4
|
+
// persist chokepoint must explicitly decline so the caller takes the legacy server
|
|
5
5
|
// path — even when a valid SDK session exists (one always does, for
|
|
6
6
|
// shadow/selection). This is the contract the prod flag-flip rests on; a future
|
|
7
7
|
// refactor of the gate guards that silently re-enables cutover on flag-off
|
|
@@ -31,12 +31,12 @@ const makeDeps = () =>
|
|
|
31
31
|
domEditSaveTimestampRef: { current: 0 },
|
|
32
32
|
}) as never;
|
|
33
33
|
|
|
34
|
-
describe("dark-launch gate — STUDIO_SDK_CUTOVER_ENABLED=false ⇒ persist
|
|
34
|
+
describe("dark-launch gate — STUDIO_SDK_CUTOVER_ENABLED=false ⇒ persist declines", () => {
|
|
35
35
|
it("sdkTimingPersist falls back without writing", async () => {
|
|
36
36
|
const deps = makeDeps();
|
|
37
|
-
expect(
|
|
38
|
-
|
|
39
|
-
);
|
|
37
|
+
expect(
|
|
38
|
+
await sdkTimingPersist("hf-a", "/c.html", { start: 1 }, makeSession(), deps),
|
|
39
|
+
).toMatchObject({ status: "declined", reason: "feature_disabled" });
|
|
40
40
|
expect(
|
|
41
41
|
(deps as unknown as { writeProjectFile: ReturnType<typeof vi.fn> }).writeProjectFile,
|
|
42
42
|
).not.toHaveBeenCalled();
|
|
@@ -50,12 +50,12 @@ describe("dark-launch gate — STUDIO_SDK_CUTOVER_ENABLED=false ⇒ persist retu
|
|
|
50
50
|
makeSession(),
|
|
51
51
|
makeDeps(),
|
|
52
52
|
),
|
|
53
|
-
).
|
|
53
|
+
).toMatchObject({ status: "declined", reason: "feature_disabled" });
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it("sdkDeletePersist falls back", async () => {
|
|
57
57
|
expect(
|
|
58
58
|
await sdkDeletePersist("hf-a", "<html></html>", "/c.html", makeSession(), makeDeps()),
|
|
59
|
-
).
|
|
59
|
+
).toMatchObject({ status: "declined", reason: "feature_disabled" });
|
|
60
60
|
});
|
|
61
61
|
});
|