@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
|
@@ -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;
|
|
@@ -45,6 +46,7 @@ export function useTimelineEditing({
|
|
|
45
46
|
timelineElements,
|
|
46
47
|
showToast,
|
|
47
48
|
writeProjectFile,
|
|
49
|
+
observeProjectFileVersion,
|
|
48
50
|
recordEdit,
|
|
49
51
|
domEditSaveTimestampRef,
|
|
50
52
|
reloadPreview,
|
|
@@ -53,6 +55,7 @@ export function useTimelineEditing({
|
|
|
53
55
|
uploadProjectFiles,
|
|
54
56
|
isRecordingRef,
|
|
55
57
|
sdkSession,
|
|
58
|
+
publishSdkSession,
|
|
56
59
|
forceReloadSdkSession,
|
|
57
60
|
handleDomZIndexReorderCommitRef,
|
|
58
61
|
}: UseTimelineEditingOptions) {
|
|
@@ -121,10 +124,10 @@ export function useTimelineEditing({
|
|
|
121
124
|
recordEdit,
|
|
122
125
|
reloadPreview,
|
|
123
126
|
sdkSession,
|
|
127
|
+
publishSdkSession,
|
|
124
128
|
showToast,
|
|
125
129
|
writeProjectFile,
|
|
126
130
|
});
|
|
127
|
-
|
|
128
131
|
const handleTimelineElementMove = useCallback(
|
|
129
132
|
// fallow-ignore-next-line complexity
|
|
130
133
|
(element: TimelineElement, updates: TimelineMoveUpdates) => {
|
|
@@ -215,10 +218,11 @@ export function useTimelineEditing({
|
|
|
215
218
|
// Capture on-disk bytes as the undo `before` so undoing a timing move
|
|
216
219
|
// restores the file verbatim, not a normalized full-DOM re-emit.
|
|
217
220
|
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
221
|
+
publishSession: publishSdkSession,
|
|
218
222
|
},
|
|
219
223
|
{ label: "Move timeline clip", coalesceKey },
|
|
220
|
-
).then((
|
|
221
|
-
if (!
|
|
224
|
+
).then((result) => {
|
|
225
|
+
if (!cutoverCommittedOrThrow(result)) return moveFallback();
|
|
222
226
|
});
|
|
223
227
|
}
|
|
224
228
|
return moveFallback();
|
|
@@ -226,6 +230,7 @@ export function useTimelineEditing({
|
|
|
226
230
|
.catch((error) => {
|
|
227
231
|
// Failed persist: revert the optimistic duration readout + live root.
|
|
228
232
|
rollbackDuration();
|
|
233
|
+
showToast(getStudioSaveErrorMessage(error), "error");
|
|
229
234
|
throw error;
|
|
230
235
|
});
|
|
231
236
|
};
|
|
@@ -236,12 +241,14 @@ export function useTimelineEditing({
|
|
|
236
241
|
enqueueEdit,
|
|
237
242
|
activeCompPath,
|
|
238
243
|
sdkSession,
|
|
244
|
+
publishSdkSession,
|
|
239
245
|
recordEdit,
|
|
240
246
|
writeProjectFile,
|
|
241
247
|
reloadPreview,
|
|
242
248
|
domEditSaveTimestampRef,
|
|
243
249
|
timelineElements,
|
|
244
250
|
handleDomZIndexReorderCommitRef,
|
|
251
|
+
showToast,
|
|
245
252
|
],
|
|
246
253
|
);
|
|
247
254
|
|
|
@@ -255,9 +262,6 @@ export function useTimelineEditing({
|
|
|
255
262
|
["data-start", formatTimelineAttributeNumber(updates.start)],
|
|
256
263
|
["data-duration", formatTimelineAttributeNumber(updates.duration)],
|
|
257
264
|
];
|
|
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
265
|
if (updates.playbackStart != null) {
|
|
262
266
|
const liveAttr =
|
|
263
267
|
element.playbackStartAttr === "playback-start"
|
|
@@ -319,15 +323,17 @@ export function useTimelineEditing({
|
|
|
319
323
|
// Capture on-disk bytes as the undo `before` so undoing a timing
|
|
320
324
|
// resize restores the file verbatim, not a normalized full-DOM re-emit.
|
|
321
325
|
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
326
|
+
publishSession: publishSdkSession,
|
|
322
327
|
},
|
|
323
328
|
{ label: "Resize timeline clip", coalesceKey },
|
|
324
|
-
).then((
|
|
325
|
-
if (!
|
|
329
|
+
).then((result) => {
|
|
330
|
+
if (!cutoverCommittedOrThrow(result)) return resizeFallback();
|
|
326
331
|
})
|
|
327
332
|
: resizeFallback();
|
|
328
333
|
return persistDone.catch((error) => {
|
|
329
334
|
// Failed persist: revert the optimistic duration readout + live root.
|
|
330
335
|
rollbackDuration();
|
|
336
|
+
showToast(getStudioSaveErrorMessage(error), "error");
|
|
331
337
|
throw error;
|
|
332
338
|
});
|
|
333
339
|
},
|
|
@@ -336,10 +342,12 @@ export function useTimelineEditing({
|
|
|
336
342
|
enqueueEdit,
|
|
337
343
|
activeCompPath,
|
|
338
344
|
sdkSession,
|
|
345
|
+
publishSdkSession,
|
|
339
346
|
recordEdit,
|
|
340
347
|
writeProjectFile,
|
|
341
348
|
reloadPreview,
|
|
342
349
|
domEditSaveTimestampRef,
|
|
350
|
+
showToast,
|
|
343
351
|
],
|
|
344
352
|
);
|
|
345
353
|
|
|
@@ -360,7 +368,6 @@ export function useTimelineEditing({
|
|
|
360
368
|
const handleToggleElementHidden = useTimelineElementVisibilityEditing({
|
|
361
369
|
projectIdRef,
|
|
362
370
|
activeCompPath,
|
|
363
|
-
timelineElements,
|
|
364
371
|
showToast,
|
|
365
372
|
writeProjectFile,
|
|
366
373
|
recordEdit,
|
|
@@ -497,6 +504,7 @@ export function useTimelineEditing({
|
|
|
497
504
|
activeCompPath,
|
|
498
505
|
showToast,
|
|
499
506
|
writeProjectFile,
|
|
507
|
+
observeProjectFileVersion,
|
|
500
508
|
recordEdit,
|
|
501
509
|
domEditSaveTimestampRef,
|
|
502
510
|
reloadPreview,
|
|
@@ -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;
|
|
@@ -30,7 +31,8 @@ export interface UseTimelineEditingOptions {
|
|
|
30
31
|
activeCompPath: string | null;
|
|
31
32
|
timelineElements: TimelineElement[];
|
|
32
33
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
33
|
-
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
34
|
+
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
35
|
+
observeProjectFileVersion?: (path: string, version: string | null) => void;
|
|
34
36
|
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
35
37
|
domEditSaveTimestampRef: MutableRefObject<number>;
|
|
36
38
|
reloadPreview: () => void;
|
|
@@ -40,6 +42,8 @@ export interface UseTimelineEditingOptions {
|
|
|
40
42
|
isRecordingRef?: RefObject<boolean>;
|
|
41
43
|
/** Stage 7 §3.2: SDK session for routing timing ops through setTiming. */
|
|
42
44
|
sdkSession?: Composition | null;
|
|
45
|
+
/** Publish a fully persisted candidate SDK session. */
|
|
46
|
+
publishSdkSession?: PublishSdkSession;
|
|
43
47
|
/** Resync the SDK session after a server-authoritative timeline write. */
|
|
44
48
|
forceReloadSdkSession?: () => void;
|
|
45
49
|
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,8 +58,9 @@ 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
|
-
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
63
|
+
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
function targetPathFor(element: TimelineElement, activeCompPath: string | null): string {
|
|
@@ -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
|
+
}
|
|
@@ -114,4 +114,23 @@ describe("dom edit save queue", () => {
|
|
|
114
114
|
expect(onOpen).not.toHaveBeenCalled();
|
|
115
115
|
queue.destroy();
|
|
116
116
|
});
|
|
117
|
+
|
|
118
|
+
it("pauses immediately on a file conflict instead of retrying stale work", async () => {
|
|
119
|
+
const onOpen = vi.fn();
|
|
120
|
+
const queue = createDomEditSaveQueue({ failureThreshold: 5, onOpen });
|
|
121
|
+
|
|
122
|
+
await expect(
|
|
123
|
+
queue.enqueue(async () => {
|
|
124
|
+
throw new StudioSaveHttpError("File changed elsewhere", 409);
|
|
125
|
+
}),
|
|
126
|
+
).rejects.toThrow("File changed elsewhere");
|
|
127
|
+
|
|
128
|
+
expect(onOpen).toHaveBeenCalledWith({
|
|
129
|
+
consecutiveFailures: 1,
|
|
130
|
+
errorMessage: "File changed elsewhere",
|
|
131
|
+
statusCode: 409,
|
|
132
|
+
});
|
|
133
|
+
await expect(queue.enqueue(async () => {})).rejects.toThrow("Auto-save is paused");
|
|
134
|
+
queue.destroy();
|
|
135
|
+
});
|
|
117
136
|
});
|
|
@@ -59,7 +59,8 @@ export function createDomEditSaveQueue(options: DomEditSaveQueueOptions = {}): D
|
|
|
59
59
|
return result;
|
|
60
60
|
} catch (error) {
|
|
61
61
|
consecutiveFailures += 1;
|
|
62
|
-
if (consecutiveFailures >= failureThreshold)
|
|
62
|
+
if (getStudioSaveStatusCode(error) === 409 || consecutiveFailures >= failureThreshold)
|
|
63
|
+
open(error);
|
|
63
64
|
throw error;
|
|
64
65
|
}
|
|
65
66
|
};
|
|
@@ -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
|
});
|