@hyperframes/studio 0.7.54 → 0.7.56
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/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.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 +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40582 -37803
- 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 +115 -0
- package/src/components/editor/CanvasContextMenu.tsx +198 -0
- 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 +158 -126
- 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 +43 -1
- 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 +208 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
- 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/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/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/nle/AssetPreviewOverlay.tsx +147 -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 +240 -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.tsx +326 -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 +39 -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 +12 -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 +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -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/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -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 +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- 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 +303 -0
- 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/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- 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/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- 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/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 +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -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 +477 -0
- package/src/player/components/timelineCollision.ts +263 -0
- 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 +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
- package/src/player/components/timelineMultiDragPreview.ts +106 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineSnapping.test.ts +134 -0
- package/src/player/components/timelineSnapping.ts +110 -0
- package/src/player/components/timelineStackingSync.test.ts +244 -0
- package/src/player/components/timelineStackingSync.ts +331 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineZones.test.ts +425 -0
- package/src/player/components/timelineZones.ts +198 -0
- package/src/player/components/timelineZoom.test.ts +67 -0
- package/src/player/components/timelineZoom.ts +51 -0
- 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/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
- package/src/player/index.ts +3 -2
- 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 +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetClickBehavior.test.ts +104 -0
- package/src/utils/assetClickBehavior.ts +75 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/canvasNudgeGate.test.ts +31 -0
- package/src/utils/canvasNudgeGate.ts +32 -0
- 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/studioUiPreferences.test.ts +38 -0
- package/src/utils/studioUiPreferences.ts +27 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/src/utils/timelineInspector.test.ts +121 -0
- package/src/utils/timelineInspector.ts +32 -1
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/assets/index-pRhCpGPz.js +0 -423
- 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
|
@@ -6,12 +6,10 @@ import {
|
|
|
6
6
|
} from "../player/components/timelineEditing";
|
|
7
7
|
import { getElementZIndex } from "../player/lib/layerOrdering";
|
|
8
8
|
import { getTimelineElementIdentity } from "../player/lib/timelineElementHelpers";
|
|
9
|
-
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
|
10
|
-
import { selectedKeyframePercentagesForElement } from "../utils/keyframeSelection";
|
|
11
|
-
import type { EditHistoryKind } from "../utils/editHistory";
|
|
9
|
+
import { saveProjectFilesWithHistory, type RecordEditInput } from "../utils/studioFileHistory";
|
|
12
10
|
import type { TimelineZIndexReorderCommit } from "./useTimelineEditingTypes";
|
|
13
11
|
import { extendRootDurationInSource } from "../utils/rootDuration";
|
|
14
|
-
|
|
12
|
+
export { deleteSelectedKeyframes } from "./deleteSelectedKeyframes";
|
|
15
13
|
function isHTMLElement(element: Element | null): element is HTMLElement {
|
|
16
14
|
if (!element) return false;
|
|
17
15
|
// Use the element's OWN realm's HTMLElement: timeline clips live in the preview
|
|
@@ -20,7 +18,6 @@ function isHTMLElement(element: Element | null): element is HTMLElement {
|
|
|
20
18
|
const Ctor = element.ownerDocument?.defaultView?.HTMLElement ?? globalThis.HTMLElement;
|
|
21
19
|
return element instanceof Ctor;
|
|
22
20
|
}
|
|
23
|
-
|
|
24
21
|
/**
|
|
25
22
|
* Resolve a timeline vertical move to a z-index stacking reorder and commit it
|
|
26
23
|
* through the shared layers-panel reorder path. Reads live sibling z-index from
|
|
@@ -38,13 +35,12 @@ export function applyTimelineStackingReorder(input: {
|
|
|
38
35
|
iframe: HTMLIFrameElement | null;
|
|
39
36
|
activeCompPath: string | null;
|
|
40
37
|
commit: TimelineZIndexReorderCommit | null | undefined;
|
|
38
|
+
coalesceKey?: string;
|
|
41
39
|
}): Promise<void> {
|
|
42
40
|
// Audio has no visual stacking; a vertical drag on it must never write z-index.
|
|
43
41
|
if (input.element.tag === "audio") return Promise.resolve();
|
|
44
|
-
|
|
45
42
|
const intent = input.stackingReorder ?? null;
|
|
46
43
|
if (intent == null || intent.zIndexChanges.length === 0) return Promise.resolve();
|
|
47
|
-
|
|
48
44
|
// Resolve each change's live element from the change's OWN locator (the intent
|
|
49
45
|
// is self-contained), falling back to the top-level element list. Sub-comp
|
|
50
46
|
// children aren't in `timelineElements`, so a list-only lookup would miss them.
|
|
@@ -67,7 +63,6 @@ export function applyTimelineStackingReorder(input: {
|
|
|
67
63
|
sourceFile: string;
|
|
68
64
|
key: string;
|
|
69
65
|
}> = [];
|
|
70
|
-
|
|
71
66
|
for (const change of intent.zIndexChanges) {
|
|
72
67
|
const sibling = siblingByKey.get(change.key);
|
|
73
68
|
const domId = change.domId ?? sibling?.domId;
|
|
@@ -86,48 +81,17 @@ export function applyTimelineStackingReorder(input: {
|
|
|
86
81
|
key: change.key,
|
|
87
82
|
});
|
|
88
83
|
}
|
|
89
|
-
|
|
90
84
|
if (commitEntries.length === 0) return Promise.resolve();
|
|
91
|
-
return input.commit?.(commitEntries) ?? Promise.resolve();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Remove the keyframes currently selected in the player store from the active
|
|
96
|
-
* element's GSAP animation. Reads selection lazily so it stays correct when
|
|
97
|
-
* invoked from a ref callback. Extracted from StudioApp to keep it under the
|
|
98
|
-
* studio 600-LOC cap.
|
|
99
|
-
*/
|
|
100
|
-
export function deleteSelectedKeyframes(session: {
|
|
101
|
-
selectedGsapAnimations: readonly { id: string; keyframes?: unknown }[];
|
|
102
|
-
handleGsapRemoveKeyframe: (animId: string, pct: number) => void;
|
|
103
|
-
}): void {
|
|
104
|
-
const { selectedKeyframes, selectedElementId } = usePlayerStore.getState();
|
|
105
|
-
const animation = session.selectedGsapAnimations.find((anim) => anim.keyframes);
|
|
106
|
-
if (!animation) return;
|
|
107
|
-
// Only the active element's keyframes; a stale cross-element selection must not delete here.
|
|
108
|
-
for (const pct of selectedKeyframePercentagesForElement(selectedKeyframes, selectedElementId)) {
|
|
109
|
-
session.handleGsapRemoveKeyframe(animation.id, pct);
|
|
110
|
-
}
|
|
85
|
+
return input.commit?.(commitEntries, input.coalesceKey) ?? Promise.resolve();
|
|
111
86
|
}
|
|
112
|
-
|
|
113
87
|
export function extendRootDurationIfNeeded(newEnd: number): boolean {
|
|
114
88
|
const store = usePlayerStore.getState();
|
|
115
89
|
if (newEnd <= store.duration) return false;
|
|
116
90
|
store.setDuration(newEnd);
|
|
117
91
|
return true;
|
|
118
92
|
}
|
|
119
|
-
|
|
120
93
|
// ── Types ──
|
|
121
|
-
|
|
122
|
-
export interface RecordEditInput {
|
|
123
|
-
label: string;
|
|
124
|
-
kind: EditHistoryKind;
|
|
125
|
-
coalesceKey?: string;
|
|
126
|
-
/** Per-entry coalesce window override (ms); lets a slow follow-up still merge. */
|
|
127
|
-
coalesceMs?: number;
|
|
128
|
-
files: Record<string, { before: string; after: string }>;
|
|
129
|
-
}
|
|
130
|
-
|
|
94
|
+
export type { RecordEditInput } from "../utils/studioFileHistory";
|
|
131
95
|
export function buildPatchTarget(element: {
|
|
132
96
|
domId?: string;
|
|
133
97
|
hfId?: string;
|
|
@@ -150,9 +114,7 @@ export function buildPatchTarget(element: {
|
|
|
150
114
|
}
|
|
151
115
|
return null;
|
|
152
116
|
}
|
|
153
|
-
|
|
154
117
|
export type PatchTarget = NonNullable<ReturnType<typeof buildPatchTarget>>;
|
|
155
|
-
|
|
156
118
|
// The runtime re-reads data-start/data-duration from the DOM on each sync tick
|
|
157
119
|
// (packages/core/src/runtime/init.ts:1324-1368), so attribute mutations here are
|
|
158
120
|
// picked up automatically on the next frame without a rebind call.
|
|
@@ -172,7 +134,6 @@ export function findTimelineElementInIframe(
|
|
|
172
134
|
return null;
|
|
173
135
|
}
|
|
174
136
|
}
|
|
175
|
-
|
|
176
137
|
export function patchIframeDomTiming(
|
|
177
138
|
iframe: HTMLIFrameElement | null,
|
|
178
139
|
element: TimelineElement,
|
|
@@ -186,7 +147,6 @@ export function patchIframeDomTiming(
|
|
|
186
147
|
// Cross-origin or mid-navigation — file save is enqueued; iframe patch is best-effort.
|
|
187
148
|
}
|
|
188
149
|
}
|
|
189
|
-
|
|
190
150
|
function postRootDurationToPreview(
|
|
191
151
|
iframe: HTMLIFrameElement | null,
|
|
192
152
|
durationSeconds: number,
|
|
@@ -203,7 +163,6 @@ function postRootDurationToPreview(
|
|
|
203
163
|
"*",
|
|
204
164
|
);
|
|
205
165
|
}
|
|
206
|
-
|
|
207
166
|
// fallow-ignore-next-line complexity
|
|
208
167
|
function resolveResizePlaybackStart(
|
|
209
168
|
original: string,
|
|
@@ -236,12 +195,26 @@ export function buildTimelineMoveTimingPatch(
|
|
|
236
195
|
target: PatchTarget,
|
|
237
196
|
start: number,
|
|
238
197
|
duration: number,
|
|
198
|
+
track?: number,
|
|
239
199
|
): string {
|
|
240
|
-
|
|
200
|
+
if (!Number.isFinite(start) || !Number.isFinite(duration)) {
|
|
201
|
+
console.warn(
|
|
202
|
+
`[Timeline] buildTimelineMoveTimingPatch: non-finite timing (start=${start}, duration=${duration}) — patch skipped`,
|
|
203
|
+
);
|
|
204
|
+
return original;
|
|
205
|
+
}
|
|
206
|
+
let patched = applyPatchByTarget(original, target, {
|
|
241
207
|
type: "attribute",
|
|
242
208
|
property: "start",
|
|
243
209
|
value: formatTimelineAttributeNumber(start),
|
|
244
210
|
});
|
|
211
|
+
if (track != null && Number.isFinite(track)) {
|
|
212
|
+
patched = applyPatchByTarget(patched, target, {
|
|
213
|
+
type: "attribute",
|
|
214
|
+
property: "track-index",
|
|
215
|
+
value: formatTimelineAttributeNumber(track),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
245
218
|
return extendRootDurationInSource(patched, start + duration);
|
|
246
219
|
}
|
|
247
220
|
|
|
@@ -595,5 +568,6 @@ export function foldedScaleGsapMutation(input: {
|
|
|
595
568
|
});
|
|
596
569
|
}
|
|
597
570
|
|
|
598
|
-
// Re-export applyPatchByTarget for use in the hook (avoids double import in callers)
|
|
599
571
|
export { applyPatchByTarget, formatTimelineAttributeNumber };
|
|
572
|
+
|
|
573
|
+
export { patchDocumentRootDuration } from "./timelineEditingGsap";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { TimelineElement } from "../player";
|
|
3
|
+
import { persistTimelineMoveEditsAtomically } from "./timelineMoveAdapter";
|
|
4
|
+
|
|
5
|
+
const element = (id: string, track: number): TimelineElement => ({
|
|
6
|
+
id,
|
|
7
|
+
key: id,
|
|
8
|
+
tag: "div",
|
|
9
|
+
start: 0,
|
|
10
|
+
duration: 2,
|
|
11
|
+
track,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("persistTimelineMoveEditsAtomically", () => {
|
|
15
|
+
it("persists two vertical edits as one group with the gesture coalesce key", async () => {
|
|
16
|
+
const handleTimelineGroupMove = vi.fn().mockResolvedValue(undefined);
|
|
17
|
+
const edits = [
|
|
18
|
+
{ element: element("a", 0), updates: { start: 1, track: 1 } },
|
|
19
|
+
{ element: element("b", 1), updates: { start: 3, track: 2 } },
|
|
20
|
+
];
|
|
21
|
+
await persistTimelineMoveEditsAtomically(edits, "clip-lane-move:7", "track-insert", {
|
|
22
|
+
handleTimelineGroupMove,
|
|
23
|
+
});
|
|
24
|
+
expect(handleTimelineGroupMove).toHaveBeenCalledTimes(1);
|
|
25
|
+
expect(handleTimelineGroupMove).toHaveBeenCalledWith(
|
|
26
|
+
[
|
|
27
|
+
{ element: edits[0].element, start: 1, track: 1 },
|
|
28
|
+
{ element: edits[1].element, start: 3, track: 2 },
|
|
29
|
+
],
|
|
30
|
+
{ coalesceKey: "clip-lane-move:7" },
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not persist track attrs for a single z-only lane reorder", async () => {
|
|
35
|
+
const handleTimelineGroupMove = vi.fn().mockResolvedValue(undefined);
|
|
36
|
+
const edit = { element: element("a", 0), updates: { start: 1, track: 1 } };
|
|
37
|
+
await persistTimelineMoveEditsAtomically([edit], "clip-lane-move:7", "lane-reorder", {
|
|
38
|
+
handleTimelineGroupMove,
|
|
39
|
+
});
|
|
40
|
+
expect(handleTimelineGroupMove).toHaveBeenCalledWith([{ element: edit.element, start: 1 }], {
|
|
41
|
+
coalesceKey: "clip-lane-move:7",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does not persist track attrs for a multi-selection lane drag", async () => {
|
|
46
|
+
const handleTimelineGroupMove = vi.fn().mockResolvedValue(undefined);
|
|
47
|
+
const edits = [
|
|
48
|
+
{ element: element("a", 0), updates: { start: 1, track: 1 } },
|
|
49
|
+
{ element: element("b", 2), updates: { start: 3, track: 2 } },
|
|
50
|
+
];
|
|
51
|
+
await persistTimelineMoveEditsAtomically(edits, "clip-lane-move:7", "lane-reorder", {
|
|
52
|
+
handleTimelineGroupMove,
|
|
53
|
+
});
|
|
54
|
+
expect(handleTimelineGroupMove).toHaveBeenCalledWith(
|
|
55
|
+
[
|
|
56
|
+
{ element: edits[0].element, start: 1 },
|
|
57
|
+
{ element: edits[1].element, start: 3 },
|
|
58
|
+
],
|
|
59
|
+
{ coalesceKey: "clip-lane-move:7" },
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rejects without retrying individual members when the atomic batch fails", async () => {
|
|
64
|
+
const failure = new Error("batch failed");
|
|
65
|
+
const handleTimelineGroupMove = vi.fn().mockRejectedValue(failure);
|
|
66
|
+
const edits = [
|
|
67
|
+
{ element: element("a", 0), updates: { start: 1, track: 1 } },
|
|
68
|
+
{ element: element("b", 1), updates: { start: 3, track: 2 } },
|
|
69
|
+
];
|
|
70
|
+
await expect(
|
|
71
|
+
persistTimelineMoveEditsAtomically(edits, "clip-lane-move:7", "track-insert", {
|
|
72
|
+
handleTimelineGroupMove,
|
|
73
|
+
}),
|
|
74
|
+
).rejects.toBe(failure);
|
|
75
|
+
expect(handleTimelineGroupMove).toHaveBeenCalledTimes(1);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { TimelineElement } from "../player";
|
|
2
|
+
import type {
|
|
3
|
+
TimelineGroupCommitOptions,
|
|
4
|
+
TimelineGroupMoveChange,
|
|
5
|
+
} from "./useTimelineGroupEditing";
|
|
6
|
+
|
|
7
|
+
interface MoveEdit {
|
|
8
|
+
element: TimelineElement;
|
|
9
|
+
updates: Pick<TimelineElement, "start" | "track">;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AtomicMoveDeps {
|
|
13
|
+
handleTimelineGroupMove: (
|
|
14
|
+
changes: TimelineGroupMoveChange[],
|
|
15
|
+
options?: TimelineGroupCommitOptions,
|
|
16
|
+
) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type TimelineMoveOperation = "timing" | "lane-reorder" | "track-insert";
|
|
20
|
+
|
|
21
|
+
export function persistTimelineMoveEditsAtomically(
|
|
22
|
+
edits: MoveEdit[],
|
|
23
|
+
coalesceKey: string | undefined,
|
|
24
|
+
operation: TimelineMoveOperation,
|
|
25
|
+
deps: AtomicMoveDeps,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
return deps.handleTimelineGroupMove(
|
|
28
|
+
edits.map(({ element, updates }) => ({
|
|
29
|
+
element,
|
|
30
|
+
start: updates.start,
|
|
31
|
+
// A single vertical edit is the z-only reorder path. Multi-edit gestures
|
|
32
|
+
// are track inserts/ripples and must persist every resulting lane index.
|
|
33
|
+
track: operation === "track-insert" ? updates.track : undefined,
|
|
34
|
+
})),
|
|
35
|
+
{ coalesceKey },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
|
-
import { createRoot } from "react-dom/client";
|
|
5
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
7
6
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
8
7
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
9
8
|
import { useAnimatedPropertyCommit } from "./useAnimatedPropertyCommit";
|
|
9
|
+
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
10
10
|
|
|
11
11
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
12
|
|
|
@@ -58,13 +58,7 @@ function renderHookWith(
|
|
|
58
58
|
onReady(commitAnimatedProperties);
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
document.body.append(host);
|
|
63
|
-
const root = createRoot(host);
|
|
64
|
-
act(() => {
|
|
65
|
-
root.render(<Harness />);
|
|
66
|
-
});
|
|
67
|
-
return root;
|
|
61
|
+
return mountReactHarness(<Harness />);
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
function renderCommitHook(
|
|
@@ -163,4 +157,38 @@ describe("commitStaticSet group routing", () => {
|
|
|
163
157
|
expect(update!.mutation.animationId).toBe("#box-set-0-position");
|
|
164
158
|
expect(update!.label).toBe("Move layer");
|
|
165
159
|
});
|
|
160
|
+
|
|
161
|
+
it("width edit updates its duration-zero size hold instead of appending a set", async () => {
|
|
162
|
+
const instantSizeHold = {
|
|
163
|
+
id: "#box-to-0-size",
|
|
164
|
+
targetSelector: "#box",
|
|
165
|
+
propertyGroup: "size",
|
|
166
|
+
method: "to",
|
|
167
|
+
properties: { width: 150, height: 150 },
|
|
168
|
+
resolvedStart: 0,
|
|
169
|
+
duration: 0,
|
|
170
|
+
extras: { immediateRender: "__raw:true" },
|
|
171
|
+
} as unknown as GsapAnimation;
|
|
172
|
+
const committed: Array<{ mutation: Record<string, unknown>; label: string }> = [];
|
|
173
|
+
let commit!: Commit;
|
|
174
|
+
renderHookWith(
|
|
175
|
+
[positionSet, instantSizeHold],
|
|
176
|
+
(mutation, label) => committed.push({ mutation, label }),
|
|
177
|
+
(c) => (commit = c),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await act(async () => {
|
|
181
|
+
await commit(selection, { width: 344 });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(committed).toHaveLength(1);
|
|
185
|
+
expect(committed[0]!.mutation).toEqual({
|
|
186
|
+
type: "update-properties",
|
|
187
|
+
animationId: instantSizeHold.id,
|
|
188
|
+
properties: { width: 344 },
|
|
189
|
+
});
|
|
190
|
+
expect(committed[0]!.label).toBe("Resize layer");
|
|
191
|
+
expect(committed.some(({ mutation }) => mutation.type === "add")).toBe(false);
|
|
192
|
+
expect(committed[0]!.mutation.animationId).not.toBe(positionSet.id);
|
|
193
|
+
});
|
|
166
194
|
});
|
|
@@ -14,7 +14,7 @@ import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
|
14
14
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
15
15
|
import { readAllAnimatedProperties, readGsapProperty } from "./gsapRuntimeBridge";
|
|
16
16
|
import type { SetPatchProps } from "./gsapRuntimePatch";
|
|
17
|
-
import { selectorFromSelection, computeElementPercentage } from "./gsapShared";
|
|
17
|
+
import { selectorFromSelection, computeElementPercentage, isInstantHold } from "./gsapShared";
|
|
18
18
|
import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
|
|
19
19
|
import { roundTo3 } from "../utils/rounding";
|
|
20
20
|
import { commitWholePropertyOffset } from "./gsapWholePropertyOffsetCommit";
|
|
@@ -161,8 +161,8 @@ async function commitSetProps(
|
|
|
161
161
|
/**
|
|
162
162
|
* Static element (no keyframes on ANY of its tweens): persist the 3D props as a
|
|
163
163
|
* `tl.set` — NEVER keyframes. Mirrors manual drag / resize / rotate, which `tl.set`
|
|
164
|
-
* a static element instead of animating it. Updates an existing
|
|
165
|
-
* creates a dedicated `set` at position 0 when the element has none.
|
|
164
|
+
* a static element instead of animating it. Updates an existing same-group static
|
|
165
|
+
* hold in place, or creates a dedicated `set` at position 0 when the element has none.
|
|
166
166
|
*/
|
|
167
167
|
async function commitStaticSet(
|
|
168
168
|
selection: DomEditSelection,
|
|
@@ -172,12 +172,12 @@ async function commitStaticSet(
|
|
|
172
172
|
commit: Commit,
|
|
173
173
|
): Promise<void> {
|
|
174
174
|
if (!selector) return;
|
|
175
|
-
// One commit per PROPERTY GROUP, each into a
|
|
176
|
-
//
|
|
177
|
-
// into the element's position set, producing a mixed
|
|
178
|
-
// exists to prevent). Within a group everything batches into ONE
|
|
179
|
-
//
|
|
180
|
-
// 404 the next update.
|
|
175
|
+
// One commit per PROPERTY GROUP, each into a static write that owns that group —
|
|
176
|
+
// never a live tween, and never a foreign-group write (a width edit used to
|
|
177
|
+
// merge into the element's position set, producing a mixed write the split
|
|
178
|
+
// machinery exists to prevent). Within a group everything batches into ONE
|
|
179
|
+
// commit: a write's id is group-derived, so a per-prop loop would shift the id
|
|
180
|
+
// mid-way and 404 the next update.
|
|
181
181
|
const byGroup = new Map<string, [string, number | string][]>();
|
|
182
182
|
for (const entry of propEntries) {
|
|
183
183
|
const group = classifyPropertyGroup(entry[0]);
|
|
@@ -185,24 +185,24 @@ async function commitStaticSet(
|
|
|
185
185
|
batch.push(entry);
|
|
186
186
|
byGroup.set(group, batch);
|
|
187
187
|
}
|
|
188
|
-
const
|
|
188
|
+
const staticWrites = animations.filter((a) => isInstantHold(a) && a.targetSelector === selector);
|
|
189
189
|
// Resolve every group's target BEFORE committing anything, and coalesce
|
|
190
|
-
// groups that land on the SAME
|
|
191
|
-
//
|
|
190
|
+
// groups that land on the SAME write into one commit: the snapshot is captured
|
|
191
|
+
// once, so if two groups resolved to one legacy mixed write, a first
|
|
192
192
|
// commit could re-shape it server-side and leave the second chasing a stale
|
|
193
193
|
// id (404 on legacy pre-split files).
|
|
194
|
-
const
|
|
194
|
+
const byTargetWrite = new Map<GsapAnimation, [string, number | string][]>();
|
|
195
195
|
const newSetBatches: [string, number | string][][] = [];
|
|
196
196
|
for (const [group, batch] of byGroup) {
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
|
|
197
|
+
const existingWrite = findGroupOwningStaticWrite(staticWrites, group);
|
|
198
|
+
if (existingWrite) {
|
|
199
|
+
byTargetWrite.set(existingWrite, [...(byTargetWrite.get(existingWrite) ?? []), ...batch]);
|
|
200
200
|
} else {
|
|
201
201
|
newSetBatches.push(batch);
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
|
-
for (const [
|
|
205
|
-
await commitSetProps(selection,
|
|
204
|
+
for (const [targetWrite, batch] of byTargetWrite) {
|
|
205
|
+
await commitSetProps(selection, targetWrite, batch, selector, animations, commit);
|
|
206
206
|
}
|
|
207
207
|
// Fresh adds don't reshape existing sets, so their ids can't go stale.
|
|
208
208
|
for (const batch of newSetBatches) {
|
|
@@ -211,14 +211,19 @@ async function commitStaticSet(
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
|
-
* The
|
|
215
|
-
* else a mixed
|
|
216
|
-
* same-group values there beats spawning a second writer for the channel).
|
|
214
|
+
* The static write that owns a property group: one already dedicated to the
|
|
215
|
+
* group wins; else a mixed write that already carries a property of the group
|
|
216
|
+
* (merging same-group values there beats spawning a second writer for the channel).
|
|
217
217
|
*/
|
|
218
|
-
function
|
|
218
|
+
function findGroupOwningStaticWrite(
|
|
219
|
+
staticWrites: GsapAnimation[],
|
|
220
|
+
group: string,
|
|
221
|
+
): GsapAnimation | undefined {
|
|
219
222
|
return (
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
staticWrites.find((a) => a.propertyGroup === group) ??
|
|
224
|
+
staticWrites.find((a) =>
|
|
225
|
+
Object.keys(a.properties).some((k) => classifyPropertyGroup(k) === group),
|
|
226
|
+
)
|
|
222
227
|
);
|
|
223
228
|
}
|
|
224
229
|
|
|
@@ -445,8 +450,8 @@ export function useAnimatedPropertyCommit(deps: CommitAnimatedPropertyDeps) {
|
|
|
445
450
|
}
|
|
446
451
|
|
|
447
452
|
// Existing static hold on a NON-animated element — merge the props into the
|
|
448
|
-
//
|
|
449
|
-
if (anim
|
|
453
|
+
// same write (maybeAutoKeyframeSet no-ops when nothing else is keyframed).
|
|
454
|
+
if (anim && isInstantHold(anim)) {
|
|
450
455
|
await commitSetProps(
|
|
451
456
|
selection,
|
|
452
457
|
anim,
|
|
@@ -4,7 +4,7 @@ import type { TimelineElement } from "../player";
|
|
|
4
4
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
5
5
|
import type { LeftSidebarHandle } from "../components/sidebar/LeftSidebar";
|
|
6
6
|
import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
|
|
7
|
-
import {
|
|
7
|
+
import { isEditableTarget } from "../utils/timelineDiscovery";
|
|
8
8
|
import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
|
|
9
9
|
import { canSplitElement } from "../utils/timelineElementSplit";
|
|
10
10
|
import { STUDIO_RAZOR_TOOL_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
@@ -81,6 +81,8 @@ interface HistoryResult {
|
|
|
81
81
|
reason?: string;
|
|
82
82
|
label?: string;
|
|
83
83
|
paths?: string[];
|
|
84
|
+
/** Per-file restored/previous content, used to soft-apply the preview. */
|
|
85
|
+
files?: Record<string, { previous: string; restored: string }>;
|
|
84
86
|
}
|
|
85
87
|
interface HistoryFileCallbacks {
|
|
86
88
|
readFile: (path: string) => Promise<string>;
|
|
@@ -96,7 +98,6 @@ interface EditHistoryHandle {
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
interface UseAppHotkeysParams {
|
|
99
|
-
toggleTimelineVisibility: () => void;
|
|
100
101
|
handleTimelineElementDelete: (element: TimelineElement) => Promise<void>;
|
|
101
102
|
handleTimelineElementSplit: (element: TimelineElement, splitTime: number) => Promise<void>;
|
|
102
103
|
handleDomEditElementDelete: (selection: DomEditSelection) => Promise<void>;
|
|
@@ -108,7 +109,10 @@ interface UseAppHotkeysParams {
|
|
|
108
109
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
109
110
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
110
111
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
111
|
-
syncHistoryPreviewAfterApply: (
|
|
112
|
+
syncHistoryPreviewAfterApply: (restore: {
|
|
113
|
+
paths?: string[];
|
|
114
|
+
files?: Record<string, { previous: string; restored: string }>;
|
|
115
|
+
}) => Promise<void>;
|
|
112
116
|
waitForPendingDomEditSaves: () => Promise<void>;
|
|
113
117
|
leftSidebarRef: React.RefObject<LeftSidebarHandle | null>;
|
|
114
118
|
handleCopy: () => boolean;
|
|
@@ -135,7 +139,6 @@ interface UseAppHotkeysParams {
|
|
|
135
139
|
// ── Extracted keydown dispatch (pure function, no hooks) ──
|
|
136
140
|
|
|
137
141
|
interface HotkeyCallbacks {
|
|
138
|
-
toggleTimelineVisibility: () => void;
|
|
139
142
|
handleTimelineElementDelete: (element: TimelineElement) => Promise<void>;
|
|
140
143
|
handleTimelineElementSplit: (element: TimelineElement, splitTime: number) => Promise<void>;
|
|
141
144
|
handleDomEditElementDelete: (selection: DomEditSelection) => Promise<void>;
|
|
@@ -291,9 +294,14 @@ function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks
|
|
|
291
294
|
return;
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
+
// Delete acts on the primary selection OR the marquee multi-selection —
|
|
298
|
+
// the delete handler expands a clip that is part of the multi-selection
|
|
299
|
+
// into an atomic delete of the whole selection (single undo).
|
|
300
|
+
const { selectedElementId, selectedElementIds, elements } = usePlayerStore.getState();
|
|
301
|
+
const selectionKeys = new Set(selectedElementIds);
|
|
302
|
+
if (selectedElementId) selectionKeys.add(selectedElementId);
|
|
303
|
+
if (selectionKeys.size > 0) {
|
|
304
|
+
const el = elements.find((e) => selectionKeys.has(e.key ?? e.id));
|
|
297
305
|
if (el) {
|
|
298
306
|
event.preventDefault();
|
|
299
307
|
void cb.handleTimelineElementDelete(el);
|
|
@@ -317,7 +325,6 @@ function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks
|
|
|
317
325
|
// ── Hook ──
|
|
318
326
|
|
|
319
327
|
export function useAppHotkeys({
|
|
320
|
-
toggleTimelineVisibility,
|
|
321
328
|
handleTimelineElementDelete,
|
|
322
329
|
handleTimelineElementSplit,
|
|
323
330
|
handleDomEditElementDelete,
|
|
@@ -346,15 +353,6 @@ export function useAppHotkeys({
|
|
|
346
353
|
const previewHotkeyWindowRef = useRef<Window | null>(null);
|
|
347
354
|
const previewHistoryCleanupRef = useRef<(() => void) | null>(null);
|
|
348
355
|
|
|
349
|
-
const handleTimelineToggleHotkey = useCallback(
|
|
350
|
-
(event: KeyboardEvent) => {
|
|
351
|
-
if (!shouldHandleTimelineToggleHotkey(event)) return;
|
|
352
|
-
event.preventDefault();
|
|
353
|
-
toggleTimelineVisibility();
|
|
354
|
-
},
|
|
355
|
-
[toggleTimelineVisibility],
|
|
356
|
-
);
|
|
357
|
-
|
|
358
356
|
// ── Undo / Redo ──
|
|
359
357
|
|
|
360
358
|
const readHistoryFile = useCallback(
|
|
@@ -397,7 +395,7 @@ export function useAppHotkeys({
|
|
|
397
395
|
if (activeCompPath && result.paths?.includes(activeCompPath)) {
|
|
398
396
|
forceReloadSdkSession?.();
|
|
399
397
|
}
|
|
400
|
-
await syncHistoryPreviewAfterApply(result.paths);
|
|
398
|
+
await syncHistoryPreviewAfterApply({ paths: result.paths, files: result.files });
|
|
401
399
|
showToast(`${direction === "undo" ? "Undid" : "Redid"} ${result.label}`, "info");
|
|
402
400
|
}
|
|
403
401
|
},
|
|
@@ -421,7 +419,6 @@ export function useAppHotkeys({
|
|
|
421
419
|
|
|
422
420
|
const cbRef = useRef<HotkeyCallbacks>(null!);
|
|
423
421
|
cbRef.current = {
|
|
424
|
-
toggleTimelineVisibility,
|
|
425
422
|
handleTimelineElementDelete,
|
|
426
423
|
handleTimelineElementSplit,
|
|
427
424
|
handleDomEditElementDelete,
|
|
@@ -444,11 +441,6 @@ export function useAppHotkeys({
|
|
|
444
441
|
|
|
445
442
|
const handleAppKeyDown = useCallback((event: KeyboardEvent) => {
|
|
446
443
|
const cb = cbRef.current;
|
|
447
|
-
if (shouldHandleTimelineToggleHotkey(event)) {
|
|
448
|
-
event.preventDefault();
|
|
449
|
-
cb.toggleTimelineVisibility();
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
444
|
const key = event.key.toLowerCase();
|
|
453
445
|
if (event.metaKey || event.ctrlKey) {
|
|
454
446
|
dispatchModifierKey(event, key, cb);
|
|
@@ -537,6 +529,5 @@ export function useAppHotkeys({
|
|
|
537
529
|
handleRedo,
|
|
538
530
|
syncPreviewTimelineHotkey,
|
|
539
531
|
syncPreviewHistoryHotkey,
|
|
540
|
-
handleTimelineToggleHotkey,
|
|
541
532
|
};
|
|
542
533
|
}
|