@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
|
@@ -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
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Block drop/add handlers for the Studio.
|
|
3
3
|
* Extracted from App.tsx to keep file sizes under the 600-line limit.
|
|
4
4
|
*/
|
|
5
|
-
import { useCallback, useMemo, useState } from "react";
|
|
5
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
6
6
|
import type { TimelineElement } from "../player";
|
|
7
7
|
import { usePlayerStore } from "../player";
|
|
8
8
|
import { addBlockToProject } from "../utils/blockInstaller";
|
|
@@ -83,17 +83,41 @@ export function useBlockHandlers({
|
|
|
83
83
|
],
|
|
84
84
|
);
|
|
85
85
|
|
|
86
|
+
// Block installs hit the server and end in a full preview reload; without a
|
|
87
|
+
// guard, repeat drops while one is in flight stack duplicate installs.
|
|
88
|
+
const installingBlockRef = useRef(false);
|
|
89
|
+
const runBlockInstall = useCallback(
|
|
90
|
+
async <T>(blockName: string, install: () => Promise<T>): Promise<T | null> => {
|
|
91
|
+
if (installingBlockRef.current) {
|
|
92
|
+
blockCtx.showToast("A block is already installing — one moment…", "info");
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
installingBlockRef.current = true;
|
|
96
|
+
blockCtx.showToast(`Adding ${blockName}…`, "info");
|
|
97
|
+
try {
|
|
98
|
+
return await install();
|
|
99
|
+
} finally {
|
|
100
|
+
installingBlockRef.current = false;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[blockCtx],
|
|
104
|
+
);
|
|
105
|
+
|
|
86
106
|
const handleAddBlock = useCallback(
|
|
87
107
|
(blockName: string) => {
|
|
88
108
|
if (!projectId) return;
|
|
109
|
+
// fallow-ignore-next-line complexity
|
|
89
110
|
void (async () => {
|
|
90
|
-
const result = await
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
const result = await runBlockInstall(blockName, () =>
|
|
112
|
+
addBlockToProject({
|
|
113
|
+
projectId,
|
|
114
|
+
blockName,
|
|
115
|
+
...blockCtx,
|
|
116
|
+
previewIframe: previewIframeRef.current,
|
|
117
|
+
currentTime: usePlayerStore.getState().currentTime,
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
if (result === null) return;
|
|
97
121
|
const params = result?.block.type === "hyperframes:block" ? result.block.params : undefined;
|
|
98
122
|
if (params?.length) {
|
|
99
123
|
setActiveBlockParams({
|
|
@@ -107,37 +131,41 @@ export function useBlockHandlers({
|
|
|
107
131
|
}
|
|
108
132
|
})();
|
|
109
133
|
},
|
|
110
|
-
[projectId, blockCtx, previewIframeRef, setRightCollapsed, setRightPanelTab],
|
|
134
|
+
[projectId, blockCtx, previewIframeRef, runBlockInstall, setRightCollapsed, setRightPanelTab],
|
|
111
135
|
);
|
|
112
136
|
|
|
113
137
|
const handleTimelineBlockDrop = useCallback(
|
|
114
138
|
(blockName: string, placement: { start: number; track: number }) => {
|
|
115
139
|
if (!projectId) return;
|
|
116
|
-
void
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
void runBlockInstall(blockName, () =>
|
|
141
|
+
addBlockToProject({
|
|
142
|
+
projectId,
|
|
143
|
+
blockName,
|
|
144
|
+
placement,
|
|
145
|
+
...blockCtx,
|
|
146
|
+
previewIframe: previewIframeRef.current,
|
|
147
|
+
currentTime: usePlayerStore.getState().currentTime,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
124
150
|
},
|
|
125
|
-
[projectId, blockCtx, previewIframeRef],
|
|
151
|
+
[projectId, blockCtx, previewIframeRef, runBlockInstall],
|
|
126
152
|
);
|
|
127
153
|
|
|
128
154
|
const handlePreviewBlockDrop = useCallback(
|
|
129
155
|
(blockName: string, position: { left: number; top: number }) => {
|
|
130
156
|
if (!projectId) return;
|
|
131
|
-
void
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
void runBlockInstall(blockName, () =>
|
|
158
|
+
addBlockToProject({
|
|
159
|
+
projectId,
|
|
160
|
+
blockName,
|
|
161
|
+
visualPosition: position,
|
|
162
|
+
...blockCtx,
|
|
163
|
+
previewIframe: previewIframeRef.current,
|
|
164
|
+
currentTime: usePlayerStore.getState().currentTime,
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
139
167
|
},
|
|
140
|
-
[projectId, blockCtx, previewIframeRef],
|
|
168
|
+
[projectId, blockCtx, previewIframeRef, runBlockInstall],
|
|
141
169
|
);
|
|
142
170
|
|
|
143
171
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import { useCaptionStore } from "../captions/store";
|
|
3
|
+
import { acceptStudioRuntimeMessage } from "../player/lib/runtimeProtocol";
|
|
3
4
|
import { useCaptionSync } from "../captions/hooks/useCaptionSync";
|
|
4
5
|
import { parseCaptionComposition } from "../captions/parser";
|
|
5
6
|
|
|
@@ -111,6 +112,7 @@ export function useCaptionDetection({
|
|
|
111
112
|
const handleMessage = (e: MessageEvent) => {
|
|
112
113
|
const data = e.data;
|
|
113
114
|
if (data?.source === "hf-preview" && (data?.type === "state" || data?.type === "timeline")) {
|
|
115
|
+
if (!acceptStudioRuntimeMessage(data)) return;
|
|
114
116
|
tryActivateCaptions();
|
|
115
117
|
}
|
|
116
118
|
};
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { useMountEffect } from "./useMountEffect";
|
|
3
3
|
import type { CompositionDimensions } from "../components/renders/RenderQueue";
|
|
4
|
+
import { acceptStudioRuntimeMessage } from "../player/lib/runtimeProtocol";
|
|
5
|
+
|
|
6
|
+
function readCompositionSizeMessage(data: unknown): CompositionDimensions | null {
|
|
7
|
+
if (!isStageSizeMessage(data)) return null;
|
|
8
|
+
const message = data;
|
|
9
|
+
if (!acceptStudioRuntimeMessage(message)) return null;
|
|
10
|
+
return readPositiveDimensions(message.width, message.height);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isStageSizeMessage(value: unknown): value is Record<string, unknown> {
|
|
14
|
+
if (typeof value !== "object") return false;
|
|
15
|
+
if (value === null) return false;
|
|
16
|
+
const message = value as Record<string, unknown>;
|
|
17
|
+
return message.source === "hf-preview" && message.type === "stage-size";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readPositiveNumber(value: unknown): number | null {
|
|
21
|
+
if (typeof value !== "number") return null;
|
|
22
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readPositiveDimensions(width: unknown, height: unknown): CompositionDimensions | null {
|
|
26
|
+
const parsedWidth = readPositiveNumber(width);
|
|
27
|
+
const parsedHeight = readPositiveNumber(height);
|
|
28
|
+
if (parsedWidth === null || parsedHeight === null) return null;
|
|
29
|
+
return { width: parsedWidth, height: parsedHeight };
|
|
30
|
+
}
|
|
4
31
|
|
|
5
32
|
export function useCompositionDimensions() {
|
|
6
33
|
const [compositionDimensions, setCompositionDimensions] = useState<CompositionDimensions | null>(
|
|
@@ -9,12 +36,12 @@ export function useCompositionDimensions() {
|
|
|
9
36
|
|
|
10
37
|
useMountEffect(() => {
|
|
11
38
|
const handleMessage = (e: MessageEvent) => {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
14
|
-
const { width, height } = data as { width: number; height: number };
|
|
15
|
-
if (!(width > 0) || !(height > 0)) return;
|
|
39
|
+
const dimensions = readCompositionSizeMessage(e.data);
|
|
40
|
+
if (!dimensions) return;
|
|
16
41
|
setCompositionDimensions((prev) =>
|
|
17
|
-
prev && prev.width === width && prev.height === height
|
|
42
|
+
prev && prev.width === dimensions.width && prev.height === dimensions.height
|
|
43
|
+
? prev
|
|
44
|
+
: dimensions,
|
|
18
45
|
);
|
|
19
46
|
};
|
|
20
47
|
window.addEventListener("message", handleMessage);
|
|
@@ -1,26 +1,50 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, type RefObject } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Shared dismiss logic for context menus: closes on
|
|
5
|
-
*
|
|
4
|
+
* Shared dismiss logic for context menus: closes on ANY pointerdown outside the
|
|
5
|
+
* menu (mouse, pen, or touch), or Escape.
|
|
6
|
+
*
|
|
7
|
+
* Two failure modes this guards against, both seen in the canvas editor:
|
|
8
|
+
*
|
|
9
|
+
* 1. The menu lives inside DomEditOverlay, whose own pointer handlers call
|
|
10
|
+
* `event.stopPropagation()` on several branches (marquee start, shift-select).
|
|
11
|
+
* A bubble-phase `mousedown`/`pointerdown` listener on `document` never fires
|
|
12
|
+
* for those events — the overlay eats them first — so the menu stayed open.
|
|
13
|
+
* Listening in the CAPTURE phase runs this dismiss BEFORE any bubble-phase
|
|
14
|
+
* stopPropagation, so an outside press always closes the menu.
|
|
15
|
+
*
|
|
16
|
+
* 2. `mousedown` alone misses pointer/touch-only gestures. `pointerdown` is the
|
|
17
|
+
* superset (fires for mouse, pen, and touch) and is exactly the event the
|
|
18
|
+
* overlay itself acts on, so hooking it here dismisses on the same press that
|
|
19
|
+
* starts a canvas gesture — no second click required.
|
|
6
20
|
*/
|
|
7
21
|
export function useContextMenuDismiss(onClose: () => void): RefObject<HTMLDivElement | null> {
|
|
8
22
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
9
23
|
|
|
10
24
|
const dismiss = useCallback(
|
|
11
|
-
(e: MouseEvent | KeyboardEvent) => {
|
|
12
|
-
if (e instanceof KeyboardEvent
|
|
13
|
-
|
|
25
|
+
(e: PointerEvent | MouseEvent | KeyboardEvent) => {
|
|
26
|
+
if (e instanceof KeyboardEvent) {
|
|
27
|
+
if (e.key === "Escape") onClose();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Any press inside the menu is a menu interaction — leave it open (the
|
|
31
|
+
// item's own handler will close it after acting).
|
|
32
|
+
if (menuRef.current?.contains(e.target as Node)) return;
|
|
14
33
|
onClose();
|
|
15
34
|
},
|
|
16
35
|
[onClose],
|
|
17
36
|
);
|
|
18
37
|
|
|
19
38
|
useEffect(() => {
|
|
20
|
-
|
|
39
|
+
// Capture phase so overlay/iframe-side handlers that stopPropagation on the
|
|
40
|
+
// bubble phase can't swallow the dismiss. `pointerdown` covers mouse + touch
|
|
41
|
+
// + pen; keep `mousedown` too for any synthetic-mouse path that skips it.
|
|
42
|
+
document.addEventListener("pointerdown", dismiss, true);
|
|
43
|
+
document.addEventListener("mousedown", dismiss, true);
|
|
21
44
|
document.addEventListener("keydown", dismiss);
|
|
22
45
|
return () => {
|
|
23
|
-
document.removeEventListener("
|
|
46
|
+
document.removeEventListener("pointerdown", dismiss, true);
|
|
47
|
+
document.removeEventListener("mousedown", dismiss, true);
|
|
24
48
|
document.removeEventListener("keydown", dismiss);
|
|
25
49
|
};
|
|
26
50
|
}, [dismiss]);
|