@hyperframes/studio 0.7.55 → 0.7.57
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--Z69cEkE.js +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +126 -125
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +53 -2
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +260 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +56 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +23 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
redoEditHistory,
|
|
8
8
|
undoEditHistory,
|
|
9
9
|
type BuildEditHistoryEntryInput,
|
|
10
|
+
type EditHistoryEntry,
|
|
10
11
|
type EditHistoryKind,
|
|
11
12
|
type EditHistoryState,
|
|
13
|
+
type EditHistoryTransitionResult,
|
|
12
14
|
} from "../utils/editHistory";
|
|
13
15
|
import {
|
|
14
16
|
createIndexedDbEditHistoryStorage,
|
|
@@ -21,6 +23,7 @@ interface RecordEditInput {
|
|
|
21
23
|
label: string;
|
|
22
24
|
kind: EditHistoryKind;
|
|
23
25
|
coalesceKey?: string;
|
|
26
|
+
coalesceMs?: number;
|
|
24
27
|
files: BuildEditHistoryEntryInput["files"];
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -35,11 +38,24 @@ interface UsePersistentEditHistoryOptions {
|
|
|
35
38
|
now?: () => number;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Per-file content the restore just applied. `restored` is the bytes written to
|
|
43
|
+
* disk (the undo/redo target); `previous` is what was on disk immediately before
|
|
44
|
+
* (the current live preview state). The undo preview-sync diffs these to decide
|
|
45
|
+
* whether the restore is soft-reloadable (attributes/style/GSAP-script only) or
|
|
46
|
+
* needs a full iframe reload.
|
|
47
|
+
*/
|
|
48
|
+
interface ApplyRestoredFile {
|
|
49
|
+
previous: string;
|
|
50
|
+
restored: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
interface ApplyResult {
|
|
39
54
|
ok: boolean;
|
|
40
55
|
reason?: "empty" | "content-mismatch";
|
|
41
56
|
label?: string;
|
|
42
57
|
paths?: string[];
|
|
58
|
+
files?: Record<string, ApplyRestoredFile>;
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
interface PersistentEditHistoryStoreOptions {
|
|
@@ -55,6 +71,18 @@ type EditHistoryMutation<T> = (state: EditHistoryState) => Promise<{
|
|
|
55
71
|
result: T;
|
|
56
72
|
}>;
|
|
57
73
|
|
|
74
|
+
/** Pair the just-written (`restored`) bytes with the pre-write (`previous`) bytes per path. */
|
|
75
|
+
function restoredFilesMap(
|
|
76
|
+
filesToWrite: Record<string, string>,
|
|
77
|
+
currentFiles: Record<string, string>,
|
|
78
|
+
): Record<string, ApplyRestoredFile> {
|
|
79
|
+
const out: Record<string, ApplyRestoredFile> = {};
|
|
80
|
+
for (const [path, restored] of Object.entries(filesToWrite)) {
|
|
81
|
+
out[path] = { previous: currentFiles[path] ?? "", restored };
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
function createEntryId(now: number): string {
|
|
59
87
|
return `edit-${now.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
60
88
|
}
|
|
@@ -120,6 +148,52 @@ async function writeFilesWithRollback({
|
|
|
120
148
|
}
|
|
121
149
|
}
|
|
122
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Apply one undo/redo step: read current on-disk hashes, run the direction's
|
|
153
|
+
* transition, write the restored files with rollback, and shape the ApplyResult.
|
|
154
|
+
* `entry` is the stack top used to know which paths to hash before applying.
|
|
155
|
+
*/
|
|
156
|
+
async function applyHistoryStep(
|
|
157
|
+
currentState: EditHistoryState,
|
|
158
|
+
entry: EditHistoryEntry | undefined,
|
|
159
|
+
transition: (
|
|
160
|
+
state: EditHistoryState,
|
|
161
|
+
currentHashes: Record<string, string>,
|
|
162
|
+
now: number,
|
|
163
|
+
) => EditHistoryTransitionResult,
|
|
164
|
+
now: () => number,
|
|
165
|
+
callbacks: ApplyCallbacks,
|
|
166
|
+
): Promise<{ state: EditHistoryState; result: ApplyResult }> {
|
|
167
|
+
if (!entry) {
|
|
168
|
+
return { state: currentState, result: { ok: false, reason: "empty" } };
|
|
169
|
+
}
|
|
170
|
+
const { currentFiles, currentHashes } = await readCurrentFileHashes(
|
|
171
|
+
Object.keys(entry.files),
|
|
172
|
+
callbacks.readFile,
|
|
173
|
+
);
|
|
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
|
+
});
|
|
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
|
+
|
|
123
197
|
export function createPersistentEditHistoryStore({
|
|
124
198
|
projectId,
|
|
125
199
|
storage,
|
|
@@ -171,66 +245,26 @@ export function createPersistentEditHistoryStore({
|
|
|
171
245
|
});
|
|
172
246
|
},
|
|
173
247
|
async undo(callbacks: ApplyCallbacks): Promise<ApplyResult> {
|
|
174
|
-
return mutate<ApplyResult>(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
Object.keys(entry.files),
|
|
184
|
-
callbacks.readFile,
|
|
185
|
-
);
|
|
186
|
-
const result = undoEditHistory(currentState, currentHashes, now());
|
|
187
|
-
if (!result.ok) {
|
|
188
|
-
return {
|
|
189
|
-
state: currentState,
|
|
190
|
-
result: { ok: false, reason: result.reason },
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
await writeFilesWithRollback({
|
|
194
|
-
files: result.filesToWrite,
|
|
195
|
-
rollbackFiles: currentFiles,
|
|
196
|
-
writeFile: callbacks.writeFile,
|
|
197
|
-
});
|
|
198
|
-
return {
|
|
199
|
-
state: result.state,
|
|
200
|
-
result: { ok: true, label: result.entry.label, paths: Object.keys(result.entry.files) },
|
|
201
|
-
};
|
|
202
|
-
});
|
|
248
|
+
return mutate<ApplyResult>((currentState) =>
|
|
249
|
+
applyHistoryStep(
|
|
250
|
+
currentState,
|
|
251
|
+
currentState.undo[currentState.undo.length - 1],
|
|
252
|
+
undoEditHistory,
|
|
253
|
+
now,
|
|
254
|
+
callbacks,
|
|
255
|
+
),
|
|
256
|
+
);
|
|
203
257
|
},
|
|
204
258
|
async redo(callbacks: ApplyCallbacks): Promise<ApplyResult> {
|
|
205
|
-
return mutate<ApplyResult>(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
Object.keys(entry.files),
|
|
215
|
-
callbacks.readFile,
|
|
216
|
-
);
|
|
217
|
-
const result = redoEditHistory(currentState, currentHashes, now());
|
|
218
|
-
if (!result.ok) {
|
|
219
|
-
return {
|
|
220
|
-
state: currentState,
|
|
221
|
-
result: { ok: false, reason: result.reason },
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
await writeFilesWithRollback({
|
|
225
|
-
files: result.filesToWrite,
|
|
226
|
-
rollbackFiles: currentFiles,
|
|
227
|
-
writeFile: callbacks.writeFile,
|
|
228
|
-
});
|
|
229
|
-
return {
|
|
230
|
-
state: result.state,
|
|
231
|
-
result: { ok: true, label: result.entry.label, paths: Object.keys(result.entry.files) },
|
|
232
|
-
};
|
|
233
|
-
});
|
|
259
|
+
return mutate<ApplyResult>((currentState) =>
|
|
260
|
+
applyHistoryStep(
|
|
261
|
+
currentState,
|
|
262
|
+
currentState.redo[currentState.redo.length - 1],
|
|
263
|
+
redoEditHistory,
|
|
264
|
+
now,
|
|
265
|
+
callbacks,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
234
268
|
},
|
|
235
269
|
};
|
|
236
270
|
}
|
|
@@ -10,6 +10,14 @@ import type { EditHistoryKind } from "../utils/editHistory";
|
|
|
10
10
|
import { createDomEditSaveQueue } from "../utils/domEditSaveQueue";
|
|
11
11
|
import { flushStudioPendingEdits } from "../utils/studioPendingEdits";
|
|
12
12
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
13
|
+
import { applyUndoRestoreToPreview, type UndoRestoreFile } from "../utils/gsapSoftReload";
|
|
14
|
+
import { usePlayerStore } from "../player";
|
|
15
|
+
|
|
16
|
+
/** The restore payload the undo/redo preview-sync consumes (from the history store). */
|
|
17
|
+
interface HistoryPreviewRestore {
|
|
18
|
+
paths?: string[];
|
|
19
|
+
files?: Record<string, UndoRestoreFile>;
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
// ── Types ──
|
|
15
23
|
|
|
@@ -105,13 +113,12 @@ export function usePreviewPersistence({
|
|
|
105
113
|
writeProjectFile: _writeProjectFile,
|
|
106
114
|
recordEdit: _recordEdit,
|
|
107
115
|
previewIframeRef,
|
|
108
|
-
activeCompPathRef
|
|
116
|
+
activeCompPathRef,
|
|
109
117
|
domEditSaveTimestampRef,
|
|
110
118
|
reloadPreview,
|
|
111
119
|
pendingTimelineEditPathRef,
|
|
112
120
|
}: UsePreviewPersistenceParams) {
|
|
113
121
|
void _recordEdit;
|
|
114
|
-
void _activeCompPathRef;
|
|
115
122
|
|
|
116
123
|
const [domEditSaveQueuePaused, setDomEditSaveQueuePaused] = useState<string | null>(null);
|
|
117
124
|
|
|
@@ -190,12 +197,23 @@ export function usePreviewPersistence({
|
|
|
190
197
|
// ── Sync preview after undo/redo ──
|
|
191
198
|
|
|
192
199
|
const syncHistoryPreviewAfterApply = useCallback(
|
|
193
|
-
async (
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
200
|
+
async (restore: HistoryPreviewRestore) => {
|
|
201
|
+
// Prefer an in-place soft reload for a soft-reloadable restore (the change
|
|
202
|
+
// is confined to the active comp's element attributes / inline-style and/or
|
|
203
|
+
// its GSAP script) — a full iframe remount blanks the frame black and
|
|
204
|
+
// re-flashes the WebGL context. applyUndoRestoreToPreview syncs the reverted
|
|
205
|
+
// attributes onto the live DOM and re-runs the timeline at the SAME playhead,
|
|
206
|
+
// falling back to reloadPreview for anything structural (split/delete undo),
|
|
207
|
+
// multi-file, sub-comp, or a permanent soft-reload failure.
|
|
208
|
+
applyUndoRestoreToPreview(
|
|
209
|
+
previewIframeRef.current,
|
|
210
|
+
activeCompPathRef.current,
|
|
211
|
+
restore.files,
|
|
212
|
+
usePlayerStore.getState().currentTime,
|
|
213
|
+
reloadPreview,
|
|
214
|
+
);
|
|
197
215
|
},
|
|
198
|
-
[reloadPreview],
|
|
216
|
+
[previewIframeRef, activeCompPathRef, reloadPreview],
|
|
199
217
|
);
|
|
200
218
|
|
|
201
219
|
// ── Migrate legacy studio-motion.json ──
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { act } from "react";
|
|
5
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
6
|
+
import { splitElementInHtml } from "@hyperframes/studio-server/source-mutation";
|
|
6
7
|
import type { TimelineElement } from "../player";
|
|
8
|
+
import { usePlayerStore } from "../player";
|
|
7
9
|
import { useRazorSplit } from "./useRazorSplit";
|
|
8
10
|
import { createPersistentEditHistoryStore } from "./usePersistentEditHistory";
|
|
9
11
|
import { createMemoryEditHistoryStorage } from "../utils/editHistoryStorage";
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
createEmptyEditHistory,
|
|
14
|
+
hashEditHistoryContent,
|
|
15
|
+
undoEditHistory,
|
|
16
|
+
} from "../utils/editHistory";
|
|
17
|
+
import { createSplitFetchMock, mountProbe } from "./useRazorSplit.testHelpers";
|
|
11
18
|
|
|
12
19
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
20
|
|
|
14
|
-
const ORIGINAL = `<div class="clip" id="clip1" data-start="0" data-duration="4">hi</div>`;
|
|
15
|
-
const SPLIT =
|
|
16
|
-
`<div class="clip" id="clip1" data-start="0" data-duration="2">hi</div>` +
|
|
17
|
-
`<div class="clip" id="clip1-split" data-start="2" data-duration="2">hi</div>`;
|
|
21
|
+
const ORIGINAL = `<div class="clip" id="clip1" data-start="0" data-duration="4" data-hf-id="hf-clip">hi</div>`;
|
|
22
|
+
const SPLIT = splitElementInHtml(ORIGINAL, { id: "clip1" }, 2, "clip1-split").html;
|
|
18
23
|
|
|
19
24
|
const element: TimelineElement = {
|
|
20
25
|
id: "clip1",
|
|
@@ -28,13 +33,15 @@ const element: TimelineElement = {
|
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
type Split = (element: TimelineElement, splitTime: number) => Promise<void>;
|
|
36
|
+
type SplitAll = (splitTime: number) => Promise<void>;
|
|
31
37
|
|
|
32
38
|
interface Harness {
|
|
33
39
|
disk: Record<string, string>;
|
|
34
40
|
store: ReturnType<typeof createPersistentEditHistoryStore>;
|
|
35
41
|
splitRef: { current: Split | undefined };
|
|
36
|
-
root: ReturnType<typeof
|
|
42
|
+
root: ReturnType<typeof mountProbe>;
|
|
37
43
|
expected: string;
|
|
44
|
+
previewWrites: string[];
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
const SPLIT_GSAP = SPLIT.replace(
|
|
@@ -43,9 +50,10 @@ const SPLIT_GSAP = SPLIT.replace(
|
|
|
43
50
|
'tl.set("#clip1-split",{x:0},2);window.__timelines["c"]=tl;</script>',
|
|
44
51
|
);
|
|
45
52
|
|
|
46
|
-
function mountRazorSplit(opts: { gsap?: boolean } = {}): Harness {
|
|
53
|
+
function mountRazorSplit(opts: { gsap?: boolean; previewStamp?: boolean } = {}): Harness {
|
|
47
54
|
const disk: Record<string, string> = { "index.html": ORIGINAL };
|
|
48
55
|
const finalContent = opts.gsap ? SPLIT_GSAP : SPLIT;
|
|
56
|
+
const previewWrites: string[] = [];
|
|
49
57
|
|
|
50
58
|
const storage = createMemoryEditHistoryStorage();
|
|
51
59
|
const store = createPersistentEditHistoryStore({
|
|
@@ -109,23 +117,28 @@ function mountRazorSplit(opts: { gsap?: boolean } = {}): Harness {
|
|
|
109
117
|
},
|
|
110
118
|
recordEdit: store.recordEdit,
|
|
111
119
|
domEditSaveTimestampRef: { current: 0 },
|
|
112
|
-
reloadPreview: () => {
|
|
120
|
+
reloadPreview: () => {
|
|
121
|
+
if (!opts.previewStamp) return;
|
|
122
|
+
const stamped = ensureHfIds(disk["index.html"]);
|
|
123
|
+
const idsBefore = (disk["index.html"].match(/\bdata-hf-id=/g) ?? []).length;
|
|
124
|
+
const idsAfter = (stamped.match(/\bdata-hf-id=/g) ?? []).length;
|
|
125
|
+
if (idsAfter > idsBefore) {
|
|
126
|
+
disk["index.html"] = stamped;
|
|
127
|
+
previewWrites.push(stamped);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
forceReloadSdkSession: () => {},
|
|
113
131
|
});
|
|
114
132
|
splitRef.current = handleRazorSplit;
|
|
115
133
|
return null;
|
|
116
134
|
}
|
|
117
135
|
|
|
118
|
-
const
|
|
119
|
-
document.body.append(host);
|
|
120
|
-
const root = createRoot(host);
|
|
121
|
-
act(() => {
|
|
122
|
-
root.render(<Component />);
|
|
123
|
-
});
|
|
136
|
+
const root = mountProbe(Component);
|
|
124
137
|
|
|
125
|
-
return { disk, store, splitRef, root, expected: finalContent };
|
|
138
|
+
return { disk, store, splitRef, root, expected: finalContent, previewWrites };
|
|
126
139
|
}
|
|
127
140
|
|
|
128
|
-
async function undoViaDisk(harness: Harness) {
|
|
141
|
+
async function undoViaDisk(harness: Pick<Harness, "disk" | "store">) {
|
|
129
142
|
return harness.store.undo({
|
|
130
143
|
readFile: async (path) => harness.disk[path],
|
|
131
144
|
writeFile: async (path, content) => {
|
|
@@ -140,6 +153,26 @@ afterEach(() => {
|
|
|
140
153
|
});
|
|
141
154
|
|
|
142
155
|
describe("useRazorSplit — split is undoable via edit history", () => {
|
|
156
|
+
it("keeps history aligned when preview reload checks hf-id persistence", async () => {
|
|
157
|
+
const harness = mountRazorSplit({ previewStamp: true });
|
|
158
|
+
|
|
159
|
+
await act(async () => {
|
|
160
|
+
await harness.splitRef.current!(element, 2);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const snapshot = harness.store.snapshot().state;
|
|
164
|
+
const entry = snapshot.undo.at(-1)!;
|
|
165
|
+
const currentHash = hashEditHistoryContent(harness.disk["index.html"]);
|
|
166
|
+
expect(entry.files["index.html"].afterHash).toBe(currentHash);
|
|
167
|
+
|
|
168
|
+
const undo = undoEditHistory(snapshot, { "index.html": currentHash }, 2000);
|
|
169
|
+
expect(undo.ok).toBe(true);
|
|
170
|
+
expect(undo.filesToWrite).toEqual({ "index.html": ORIGINAL });
|
|
171
|
+
expect(harness.previewWrites).toHaveLength(0);
|
|
172
|
+
|
|
173
|
+
act(() => harness.root.unmount());
|
|
174
|
+
});
|
|
175
|
+
|
|
143
176
|
for (const gsap of [false, true]) {
|
|
144
177
|
describe(gsap ? "with GSAP rewrite" : "plain HTML split", () => {
|
|
145
178
|
let harness: Harness;
|
|
@@ -171,3 +204,100 @@ describe("useRazorSplit — split is undoable via edit history", () => {
|
|
|
171
204
|
});
|
|
172
205
|
}
|
|
173
206
|
});
|
|
207
|
+
|
|
208
|
+
const BATCH_ORIGINALS = {
|
|
209
|
+
"index.html": `<div class="clip" id="clip1" data-start="0" data-duration="4">one</div>`,
|
|
210
|
+
"scenes/two.html": `<div class="clip" id="clip2" data-start="0" data-duration="4">two</div>`,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const batchElements: TimelineElement[] = [
|
|
214
|
+
element,
|
|
215
|
+
{
|
|
216
|
+
...element,
|
|
217
|
+
id: "clip2",
|
|
218
|
+
domId: "clip2",
|
|
219
|
+
sourceFile: "scenes/two.html",
|
|
220
|
+
track: 1,
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
interface SplitAllHarness {
|
|
225
|
+
disk: Record<string, string>;
|
|
226
|
+
store: ReturnType<typeof createPersistentEditHistoryStore>;
|
|
227
|
+
splitAllRef: { current: SplitAll | undefined };
|
|
228
|
+
root: ReturnType<typeof mountProbe>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function mountRazorSplitAll(failOnSplit?: number): SplitAllHarness {
|
|
232
|
+
const disk: Record<string, string> = { ...BATCH_ORIGINALS };
|
|
233
|
+
const store = createPersistentEditHistoryStore({
|
|
234
|
+
projectId: "p1",
|
|
235
|
+
storage: createMemoryEditHistoryStorage(),
|
|
236
|
+
initialState: createEmptyEditHistory(),
|
|
237
|
+
now: () => 1000,
|
|
238
|
+
onChange: () => {},
|
|
239
|
+
});
|
|
240
|
+
let splitCount = 0;
|
|
241
|
+
|
|
242
|
+
vi.stubGlobal(
|
|
243
|
+
"fetch",
|
|
244
|
+
createSplitFetchMock(disk, () => {
|
|
245
|
+
splitCount++;
|
|
246
|
+
if (splitCount === failOnSplit) throw new Error("simulated split failure");
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const splitAllRef: { current: SplitAll | undefined } = { current: undefined };
|
|
251
|
+
function Component() {
|
|
252
|
+
const { handleRazorSplitAll } = useRazorSplit({
|
|
253
|
+
projectId: "p1",
|
|
254
|
+
activeCompPath: "index.html",
|
|
255
|
+
showToast: () => {},
|
|
256
|
+
writeProjectFile: async (path, content) => {
|
|
257
|
+
disk[path] = content;
|
|
258
|
+
},
|
|
259
|
+
recordEdit: store.recordEdit,
|
|
260
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
261
|
+
reloadPreview: () => {},
|
|
262
|
+
});
|
|
263
|
+
splitAllRef.current = handleRazorSplitAll;
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const root = mountProbe(Component);
|
|
268
|
+
usePlayerStore.setState({ elements: batchElements });
|
|
269
|
+
return { disk, store, splitAllRef, root };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
describe("useRazorSplit — split-all batch history", () => {
|
|
273
|
+
afterEach(() => {
|
|
274
|
+
usePlayerStore.setState({ elements: [] });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
async function runSplitAll(failOnSplit?: number) {
|
|
278
|
+
const harness = mountRazorSplitAll(failOnSplit);
|
|
279
|
+
await act(async () => {
|
|
280
|
+
await harness.splitAllRef.current!(2);
|
|
281
|
+
});
|
|
282
|
+
return harness;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
it("records one undo entry that restores every split file", async () => {
|
|
286
|
+
const harness = await runSplitAll();
|
|
287
|
+
|
|
288
|
+
expect(harness.store.snapshot().canUndo).toBe(true);
|
|
289
|
+
const result = await undoViaDisk(harness);
|
|
290
|
+
expect(result.ok).toBe(true);
|
|
291
|
+
expect(harness.disk).toEqual(BATCH_ORIGINALS);
|
|
292
|
+
expect(harness.store.snapshot().canUndo).toBe(false);
|
|
293
|
+
act(() => harness.root.unmount());
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("restores completed writes and records no undo when a later split fails", async () => {
|
|
297
|
+
const harness = await runSplitAll(2);
|
|
298
|
+
|
|
299
|
+
expect(harness.disk).toEqual(BATCH_ORIGINALS);
|
|
300
|
+
expect(harness.store.snapshot().canUndo).toBe(false);
|
|
301
|
+
act(() => harness.root.unmount());
|
|
302
|
+
});
|
|
303
|
+
});
|