@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.
Files changed (262) hide show
  1. package/dist/assets/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
  2. package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
  3. package/dist/assets/index-CmVCjZjp.js +423 -0
  4. package/dist/assets/index-D-GyYi2d.css +1 -0
  5. package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
  6. package/dist/chunk-5QSIMBEJ.js.map +1 -0
  7. package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
  8. package/dist/index.d.ts +146 -144
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +40582 -37803
  11. package/dist/index.js.map +1 -1
  12. package/package.json +7 -7
  13. package/src/App.tsx +106 -96
  14. package/src/components/EditorShell.tsx +253 -0
  15. package/src/components/StudioGlobalDragOverlay.tsx +1 -3
  16. package/src/components/StudioLeftSidebar.tsx +13 -3
  17. package/src/components/StudioRightPanel.tsx +8 -3
  18. package/src/components/TimelineToolbar.test.tsx +2 -2
  19. package/src/components/TimelineToolbar.tsx +179 -121
  20. package/src/components/editor/CanvasContextMenu.test.tsx +115 -0
  21. package/src/components/editor/CanvasContextMenu.tsx +198 -0
  22. package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
  23. package/src/components/editor/DomEditCropHandles.tsx +64 -29
  24. package/src/components/editor/DomEditOverlay.test.ts +245 -279
  25. package/src/components/editor/DomEditOverlay.tsx +158 -126
  26. package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
  27. package/src/components/editor/DomEditRotateHandle.tsx +38 -18
  28. package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
  29. package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
  30. package/src/components/editor/LayersPanel.test.ts +66 -2
  31. package/src/components/editor/LayersPanel.tsx +43 -1
  32. package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
  33. package/src/components/editor/OffCanvasIndicators.tsx +49 -10
  34. package/src/components/editor/SnapGuideOverlay.tsx +28 -23
  35. package/src/components/editor/SnapToolbar.test.tsx +0 -1
  36. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
  37. package/src/components/editor/anchoredResizeReleaseShift.test.ts +208 -0
  38. package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
  39. package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
  40. package/src/components/editor/domEditNudge.test.ts +80 -0
  41. package/src/components/editor/domEditNudge.ts +44 -0
  42. package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
  43. package/src/components/editor/domEditOverlayGeometry.ts +280 -20
  44. package/src/components/editor/domEditOverlayGestures.ts +44 -41
  45. package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
  46. package/src/components/editor/domEditResizeLocal.test.ts +131 -0
  47. package/src/components/editor/domEditResizeLocal.ts +125 -0
  48. package/src/components/editor/domEditingDom.ts +1 -1
  49. package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
  50. package/src/components/editor/manualOffsetDrag.ts +55 -12
  51. package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
  52. package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
  53. package/src/components/editor/resizeDraft.ts +108 -0
  54. package/src/components/editor/snapEngine.test.ts +12 -78
  55. package/src/components/editor/snapEngine.ts +13 -53
  56. package/src/components/editor/useDomEditNudge.test.tsx +228 -0
  57. package/src/components/editor/useDomEditNudge.ts +254 -0
  58. package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
  59. package/src/components/editor/useDomEditOverlayRects.ts +9 -2
  60. package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
  61. package/src/components/nle/NLEContext.test.ts +144 -0
  62. package/src/components/nle/NLEContext.tsx +328 -0
  63. package/src/components/nle/NLEPreview.tsx +1 -1
  64. package/src/components/nle/PreviewOverlays.tsx +240 -0
  65. package/src/components/nle/PreviewPane.tsx +163 -0
  66. package/src/components/nle/TimelinePane.test.ts +60 -0
  67. package/src/components/nle/TimelinePane.tsx +293 -0
  68. package/src/components/nle/TimelineResizeDivider.tsx +8 -2
  69. package/src/components/nle/useCompositionStack.test.tsx +44 -0
  70. package/src/components/nle/useCompositionStack.ts +17 -9
  71. package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
  72. package/src/components/sidebar/AssetCard.tsx +326 -0
  73. package/src/components/sidebar/AssetContextMenu.tsx +15 -30
  74. package/src/components/sidebar/AssetsTab.test.ts +96 -0
  75. package/src/components/sidebar/AssetsTab.tsx +71 -206
  76. package/src/components/sidebar/AudioRow.tsx +39 -2
  77. package/src/components/sidebar/BlocksTab.tsx +8 -1
  78. package/src/components/sidebar/LeftSidebar.tsx +4 -1
  79. package/src/components/sidebar/assetHelpers.ts +29 -0
  80. package/src/contexts/DomEditContext.tsx +9 -0
  81. package/src/contexts/StudioContext.tsx +9 -8
  82. package/src/contexts/TimelineEditContext.tsx +3 -7
  83. package/src/hooks/deleteSelectedKeyframes.ts +35 -0
  84. package/src/hooks/domEditCommitTypes.ts +12 -1
  85. package/src/hooks/domSelectionTestHarness.ts +14 -0
  86. package/src/hooks/gestureTransaction.test.ts +311 -0
  87. package/src/hooks/gestureTransaction.ts +199 -0
  88. package/src/hooks/gsapDragCommit.test.ts +141 -41
  89. package/src/hooks/gsapDragCommit.ts +64 -74
  90. package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
  91. package/src/hooks/gsapResizeIntercept.test.ts +69 -0
  92. package/src/hooks/gsapResizeIntercept.ts +38 -10
  93. package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
  94. package/src/hooks/gsapRuntimeBridge.ts +4 -4
  95. package/src/hooks/gsapScriptCommitTypes.ts +16 -5
  96. package/src/hooks/gsapShared.test.ts +19 -1
  97. package/src/hooks/gsapShared.ts +14 -0
  98. package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
  99. package/src/hooks/timelineEditingGsap.ts +14 -0
  100. package/src/hooks/timelineEditingHelpers.test.ts +34 -1
  101. package/src/hooks/timelineEditingHelpers.ts +22 -48
  102. package/src/hooks/timelineMoveAdapter.test.ts +77 -0
  103. package/src/hooks/timelineMoveAdapter.ts +37 -0
  104. package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
  105. package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
  106. package/src/hooks/useAppHotkeys.ts +16 -25
  107. package/src/hooks/useBlockHandlers.ts +55 -27
  108. package/src/hooks/useContextMenuDismiss.ts +31 -7
  109. package/src/hooks/useDomEditCommits.test.tsx +154 -1
  110. package/src/hooks/useDomEditCommits.ts +100 -3
  111. package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
  112. package/src/hooks/useDomEditSession.ts +2 -2
  113. package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
  114. package/src/hooks/useDomEditTextCommits.ts +8 -3
  115. package/src/hooks/useDomEditWiring.ts +7 -9
  116. package/src/hooks/useDomGeometryCommits.ts +17 -2
  117. package/src/hooks/useDomSelection.test.ts +2 -36
  118. package/src/hooks/useDomSelection.ts +81 -51
  119. package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
  120. package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
  121. package/src/hooks/useElementLifecycleOps.ts +40 -71
  122. package/src/hooks/useEnableKeyframes.test.ts +142 -1
  123. package/src/hooks/useEnableKeyframes.ts +73 -29
  124. package/src/hooks/useGestureCommit.test.tsx +130 -0
  125. package/src/hooks/useGestureCommit.ts +32 -8
  126. package/src/hooks/useGroupCommits.ts +7 -28
  127. package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
  128. package/src/hooks/useGsapAwareEditing.ts +115 -20
  129. package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
  130. package/src/hooks/useGsapKeyframeOps.ts +38 -10
  131. package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
  132. package/src/hooks/useGsapScriptCommits.ts +134 -50
  133. package/src/hooks/useGsapSelectionHandlers.ts +49 -16
  134. package/src/hooks/useMusicBeatAnalysis.ts +72 -36
  135. package/src/hooks/usePersistentEditHistory.test.ts +33 -0
  136. package/src/hooks/usePersistentEditHistory.ts +92 -58
  137. package/src/hooks/usePreviewPersistence.ts +25 -7
  138. package/src/hooks/useRazorSplit.history.test.tsx +303 -0
  139. package/src/hooks/useRazorSplit.test.ts +269 -0
  140. package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
  141. package/src/hooks/useRazorSplit.ts +107 -36
  142. package/src/hooks/useRenderClipContent.ts +24 -6
  143. package/src/hooks/useStudioContextValue.ts +15 -5
  144. package/src/hooks/useStudioUrlState.ts +2 -4
  145. package/src/hooks/useTimelineEditing.test.tsx +20 -29
  146. package/src/hooks/useTimelineEditingTypes.ts +6 -0
  147. package/src/hooks/useTimelineGroupEditing.ts +18 -4
  148. package/src/index.ts +1 -1
  149. package/src/player/components/ImageThumbnail.test.tsx +173 -0
  150. package/src/player/components/ImageThumbnail.tsx +160 -0
  151. package/src/player/components/PlayheadIndicator.tsx +50 -5
  152. package/src/player/components/Timeline.test.ts +102 -19
  153. package/src/player/components/Timeline.tsx +194 -194
  154. package/src/player/components/TimelineCanvas.tsx +207 -521
  155. package/src/player/components/TimelineClip.tsx +4 -1
  156. package/src/player/components/TimelineClipDiamonds.tsx +9 -40
  157. package/src/player/components/TimelineEmptyState.tsx +3 -1
  158. package/src/player/components/TimelineLanes.tsx +487 -0
  159. package/src/player/components/TimelineOverlays.tsx +122 -0
  160. package/src/player/components/VideoThumbnail.test.tsx +152 -0
  161. package/src/player/components/VideoThumbnail.tsx +28 -7
  162. package/src/player/components/thumbnailUtils.test.ts +127 -0
  163. package/src/player/components/thumbnailUtils.ts +54 -0
  164. package/src/player/components/timelineCallbacks.ts +18 -18
  165. package/src/player/components/timelineClipChildren.tsx +39 -0
  166. package/src/player/components/timelineClipDragCommit.test.ts +943 -0
  167. package/src/player/components/timelineClipDragCommit.ts +401 -0
  168. package/src/player/components/timelineClipDragPreview.test.ts +144 -0
  169. package/src/player/components/timelineClipDragPreview.ts +323 -38
  170. package/src/player/components/timelineClipDragTypes.ts +65 -0
  171. package/src/player/components/timelineCollision.test.ts +477 -0
  172. package/src/player/components/timelineCollision.ts +263 -0
  173. package/src/player/components/timelineDragDrop.ts +69 -60
  174. package/src/player/components/timelineEditCapabilities.ts +52 -0
  175. package/src/player/components/timelineEditing.test.ts +1 -1
  176. package/src/player/components/timelineEditing.ts +76 -59
  177. package/src/player/components/timelineGroupEditing.test.ts +126 -0
  178. package/src/player/components/timelineGroupEditing.ts +133 -0
  179. package/src/player/components/timelineGroupResizeCommit.ts +54 -0
  180. package/src/player/components/timelineLayout.test.ts +95 -23
  181. package/src/player/components/timelineLayout.ts +120 -61
  182. package/src/player/components/timelineMarquee.test.ts +197 -0
  183. package/src/player/components/timelineMarquee.ts +112 -0
  184. package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
  185. package/src/player/components/timelineMultiDragPreview.ts +106 -0
  186. package/src/player/components/timelineOptimisticRevision.ts +42 -0
  187. package/src/player/components/timelineSnapping.test.ts +134 -0
  188. package/src/player/components/timelineSnapping.ts +110 -0
  189. package/src/player/components/timelineStackingSync.test.ts +244 -0
  190. package/src/player/components/timelineStackingSync.ts +331 -0
  191. package/src/player/components/timelineTheme.ts +6 -1
  192. package/src/player/components/timelineZones.test.ts +425 -0
  193. package/src/player/components/timelineZones.ts +198 -0
  194. package/src/player/components/timelineZoom.test.ts +67 -0
  195. package/src/player/components/timelineZoom.ts +51 -0
  196. package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
  197. package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
  198. package/src/player/components/useTimelineClipDrag.ts +380 -386
  199. package/src/player/components/useTimelineEditPinning.ts +121 -0
  200. package/src/player/components/useTimelineGeometry.ts +129 -0
  201. package/src/player/components/useTimelinePlayhead.ts +4 -16
  202. package/src/player/components/useTimelineRangeSelection.ts +341 -50
  203. package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
  204. package/src/player/components/useTimelineStackingSync.ts +83 -0
  205. package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
  206. package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
  207. package/src/player/hooks/useTimelinePlayer.ts +48 -53
  208. package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
  209. package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
  210. package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
  211. package/src/player/index.ts +3 -2
  212. package/src/player/lib/time.test.ts +19 -19
  213. package/src/player/lib/time.ts +6 -2
  214. package/src/player/lib/timelineDOM.test.ts +25 -98
  215. package/src/player/lib/timelineDOM.ts +12 -81
  216. package/src/player/lib/timelineElementHelpers.ts +54 -5
  217. package/src/player/lib/timelineIframeHelpers.ts +2 -0
  218. package/src/player/store/playerStore.ts +62 -3
  219. package/src/styles/studio.css +27 -0
  220. package/src/utils/assetClickBehavior.test.ts +104 -0
  221. package/src/utils/assetClickBehavior.ts +75 -0
  222. package/src/utils/assetPreviewStore.ts +33 -0
  223. package/src/utils/blockInstaller.ts +10 -2
  224. package/src/utils/canvasNudgeGate.test.ts +31 -0
  225. package/src/utils/canvasNudgeGate.ts +32 -0
  226. package/src/utils/editHistory.test.ts +35 -0
  227. package/src/utils/gsapSoftReload.test.ts +119 -1
  228. package/src/utils/gsapSoftReload.ts +153 -0
  229. package/src/utils/mediaTypes.ts +3 -2
  230. package/src/utils/resizeDebug.ts +55 -0
  231. package/src/utils/rootDuration.test.ts +90 -1
  232. package/src/utils/rootDuration.ts +76 -13
  233. package/src/utils/sdkCutover.ts +4 -0
  234. package/src/utils/studioFileHistory.ts +37 -7
  235. package/src/utils/studioHelpers.ts +62 -0
  236. package/src/utils/studioPreviewHelpers.test.ts +65 -8
  237. package/src/utils/studioPreviewHelpers.ts +10 -0
  238. package/src/utils/studioTelemetry.ts +4 -1
  239. package/src/utils/studioUiPreferences.test.ts +38 -0
  240. package/src/utils/studioUiPreferences.ts +27 -0
  241. package/src/utils/studioUrlState.test.ts +0 -1
  242. package/src/utils/timelineAssetDrop.ts +62 -0
  243. package/src/utils/timelineDiscovery.ts +0 -17
  244. package/src/utils/timelineInspector.test.ts +121 -0
  245. package/src/utils/timelineInspector.ts +32 -1
  246. package/dist/assets/index-Dq7FEg0K.css +0 -1
  247. package/dist/assets/index-pRhCpGPz.js +0 -423
  248. package/dist/chunk-SOTCF4DF.js.map +0 -1
  249. package/src/components/StudioPreviewArea.tsx +0 -500
  250. package/src/components/nle/NLELayout.test.ts +0 -12
  251. package/src/components/nle/NLELayout.tsx +0 -591
  252. package/src/player/components/TimelineLayerGutter.tsx +0 -61
  253. package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
  254. package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
  255. package/src/player/components/timelineSnapTargets.test.ts +0 -144
  256. package/src/player/components/timelineSnapTargets.ts +0 -164
  257. package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
  258. package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
  259. package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
  260. package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
  261. package/src/utils/timelineDiscovery.test.ts +0 -90
  262. /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
@@ -1,417 +0,0 @@
1
- import { useCallback, useRef, type RefObject } from "react";
2
- import type {
3
- TimelineGroupMoveChange,
4
- TimelineGroupResizeChange,
5
- } from "../../hooks/useTimelineGroupEditing";
6
- import type { TimelineElement } from "../store/playerStore";
7
- import {
8
- getTimelineEditCapabilities,
9
- resolveTimelineGroupMove,
10
- resolveTimelineGroupResize,
11
- type TimelineGroupResizeEdge,
12
- type TimelineGroupTimingMember,
13
- } from "./timelineEditing";
14
-
15
- type TimelineResizeUpdates = Pick<TimelineElement, "start" | "duration" | "playbackStart">;
16
-
17
- type UpdateTimelineElement = (
18
- elementId: string,
19
- updates: Partial<Pick<TimelineElement, "start" | "duration" | "playbackStart">>,
20
- ) => void;
21
-
22
- interface GroupTimingMember extends TimelineGroupTimingMember {
23
- element: TimelineElement;
24
- key: string;
25
- }
26
-
27
- interface MoveSession {
28
- grabbedKey: string;
29
- members: GroupTimingMember[];
30
- changes: TimelineGroupMoveChange[];
31
- hasChanged: boolean;
32
- }
33
-
34
- interface ResizeSession {
35
- grabbedKey: string;
36
- edge: TimelineGroupResizeEdge;
37
- members: GroupTimingMember[];
38
- changes: TimelineGroupResizeChange[];
39
- hasChanged: boolean;
40
- }
41
-
42
- interface UseTimelineClipGroupDragInput {
43
- timelineElementsRef: RefObject<TimelineElement[]>;
44
- updateElement: UpdateTimelineElement;
45
- onMoveElementsRef: RefObject<
46
- ((changes: TimelineGroupMoveChange[]) => Promise<void> | void) | undefined
47
- >;
48
- onResizeElementsRef: RefObject<
49
- ((changes: TimelineGroupResizeChange[]) => Promise<void> | void) | undefined
50
- >;
51
- onPreviewMoveElementsRef: RefObject<((changes: TimelineGroupMoveChange[]) => void) | undefined>;
52
- onPreviewResizeElementsRef: RefObject<
53
- ((changes: TimelineGroupResizeChange[]) => void) | undefined
54
- >;
55
- }
56
-
57
- interface PreviewGroupMoveResult {
58
- active: boolean;
59
- previewStart: number;
60
- }
61
-
62
- interface PreviewGroupResizeResult {
63
- active: boolean;
64
- updates: TimelineResizeUpdates;
65
- }
66
-
67
- function elementKey(element: TimelineElement): string {
68
- return element.key ?? element.id;
69
- }
70
-
71
- function isMediaElement(element: TimelineElement): boolean {
72
- const normalizedTag = element.tag.toLowerCase();
73
- return normalizedTag === "audio" || normalizedTag === "video";
74
- }
75
-
76
- function selectedElementSet(selectedElementIdsInput: Set<string>): Set<string> {
77
- return selectedElementIdsInput instanceof Set ? selectedElementIdsInput : new Set<string>();
78
- }
79
-
80
- function selectedMembers(
81
- grabbedElement: TimelineElement,
82
- selectedElementIdsInput: Set<string>,
83
- timelineElements: readonly TimelineElement[],
84
- mapMember: (element: TimelineElement) => GroupTimingMember,
85
- canEdit: (element: TimelineElement) => boolean,
86
- ): GroupTimingMember[] | null {
87
- const selectedElementIds = selectedElementSet(selectedElementIdsInput);
88
- const grabbedKey = elementKey(grabbedElement);
89
- if (selectedElementIds.size <= 1 || !selectedElementIds.has(grabbedKey)) return null;
90
-
91
- const elements = timelineElements.filter((element) =>
92
- selectedElementIds.has(elementKey(element)),
93
- );
94
- // A group edit must not touch a member that individually forbids this operation
95
- // (e.g. a locked or implicitly-timed clip). If any member can't take it, don't form
96
- // a group; the gesture degrades to a normal single-clip edit of the grabbed clip.
97
- if (!elements.every(canEdit)) return null;
98
- const members = elements.map(mapMember);
99
- return members.length > 1 ? members : null;
100
- }
101
-
102
- function moveMember(element: TimelineElement): GroupTimingMember {
103
- return {
104
- element,
105
- key: elementKey(element),
106
- start: element.start,
107
- duration: element.duration,
108
- };
109
- }
110
-
111
- function resizeMember(edge: TimelineGroupResizeEdge, element: TimelineElement): GroupTimingMember {
112
- const shouldSeedPlaybackStart = edge === "start" && isMediaElement(element);
113
- return {
114
- element,
115
- key: elementKey(element),
116
- start: element.start,
117
- duration: element.duration,
118
- playbackStart: shouldSeedPlaybackStart ? (element.playbackStart ?? 0) : element.playbackStart,
119
- playbackRate: element.playbackRate,
120
- };
121
- }
122
-
123
- function sameGesture(sessionKey: string, element: TimelineElement): boolean {
124
- return sessionKey === elementKey(element);
125
- }
126
-
127
- function createMoveSession(
128
- element: TimelineElement,
129
- selectedElementIds: Set<string>,
130
- timelineElements: readonly TimelineElement[],
131
- ): MoveSession | null {
132
- const members = selectedMembers(
133
- element,
134
- selectedElementIds,
135
- timelineElements,
136
- moveMember,
137
- (candidate) => getTimelineEditCapabilities(candidate).canMove,
138
- );
139
- if (!members) return null;
140
- return {
141
- grabbedKey: elementKey(element),
142
- members,
143
- changes: [],
144
- hasChanged: false,
145
- };
146
- }
147
-
148
- function createResizeSession(
149
- element: TimelineElement,
150
- selectedElementIds: Set<string>,
151
- timelineElements: readonly TimelineElement[],
152
- edge: TimelineGroupResizeEdge,
153
- ): ResizeSession | null {
154
- const members = selectedMembers(
155
- element,
156
- selectedElementIds,
157
- timelineElements,
158
- (member) => resizeMember(edge, member),
159
- (candidate) => {
160
- const caps = getTimelineEditCapabilities(candidate);
161
- return edge === "start" ? caps.canTrimStart : caps.canTrimEnd;
162
- },
163
- );
164
- if (!members) return null;
165
- return {
166
- grabbedKey: elementKey(element),
167
- edge,
168
- members,
169
- changes: [],
170
- hasChanged: false,
171
- };
172
- }
173
-
174
- function resolveMoveChanges(
175
- session: MoveSession,
176
- previewStart: number,
177
- ): TimelineGroupMoveChange[] | null {
178
- const grabbed = session.members.find((member) => member.key === session.grabbedKey);
179
- if (!grabbed) return null;
180
- const result = resolveTimelineGroupMove(session.members, previewStart - grabbed.start);
181
- return result.members.map((member, index) => ({
182
- element: session.members[index]!.element,
183
- start: member.start,
184
- }));
185
- }
186
-
187
- function resizeRawDelta(session: ResizeSession, updates: TimelineResizeUpdates): number | null {
188
- const grabbed = session.members.find((member) => member.key === session.grabbedKey);
189
- if (!grabbed) return null;
190
- return session.edge === "start"
191
- ? updates.start - grabbed.start
192
- : updates.duration - grabbed.duration;
193
- }
194
-
195
- function resolveResizeChanges(
196
- session: ResizeSession,
197
- updates: TimelineResizeUpdates,
198
- ): TimelineGroupResizeChange[] | null {
199
- const rawDelta = resizeRawDelta(session, updates);
200
- if (rawDelta == null) return null;
201
- const result = resolveTimelineGroupResize(session.members, session.edge, rawDelta);
202
- return result.members.map((member, index) => ({
203
- element: session.members[index]!.element,
204
- start: member.start,
205
- duration: member.duration,
206
- playbackStart: member.playbackStart,
207
- }));
208
- }
209
-
210
- function moveSessionHasChanged(
211
- session: MoveSession,
212
- changes: readonly TimelineGroupMoveChange[],
213
- ): boolean {
214
- return changes.some((change, index) => change.start !== session.members[index]!.start);
215
- }
216
-
217
- function resizeSessionHasChanged(
218
- session: ResizeSession,
219
- changes: readonly TimelineGroupResizeChange[],
220
- ): boolean {
221
- return changes.some((change, index) => {
222
- const member = session.members[index]!;
223
- return (
224
- change.start !== member.start ||
225
- change.duration !== member.duration ||
226
- change.playbackStart !== member.playbackStart
227
- );
228
- });
229
- }
230
-
231
- function previewStartForGrabbed(
232
- session: MoveSession,
233
- changes: readonly TimelineGroupMoveChange[],
234
- fallback: number,
235
- ): number {
236
- const change = changes.find((candidate) => elementKey(candidate.element) === session.grabbedKey);
237
- return change?.start ?? fallback;
238
- }
239
-
240
- function resizeUpdatesForGrabbed(
241
- session: ResizeSession,
242
- changes: readonly TimelineGroupResizeChange[],
243
- fallback: TimelineResizeUpdates,
244
- ): TimelineResizeUpdates {
245
- const change = changes.find((candidate) => elementKey(candidate.element) === session.grabbedKey);
246
- if (!change) return fallback;
247
- return {
248
- start: change.start,
249
- duration: change.duration,
250
- playbackStart: change.playbackStart,
251
- };
252
- }
253
-
254
- export function useTimelineClipGroupDrag({
255
- timelineElementsRef,
256
- updateElement,
257
- onMoveElementsRef,
258
- onResizeElementsRef,
259
- onPreviewMoveElementsRef,
260
- onPreviewResizeElementsRef,
261
- }: UseTimelineClipGroupDragInput) {
262
- const moveSessionRef = useRef<MoveSession | null>(null);
263
- const resizeSessionRef = useRef<ResizeSession | null>(null);
264
-
265
- const rollbackMove = useCallback(
266
- (session: MoveSession) => {
267
- const changes = session.members.map((member) => ({
268
- element: member.element,
269
- start: member.start,
270
- }));
271
- for (const change of changes) {
272
- updateElement(elementKey(change.element), { start: change.start });
273
- }
274
- onPreviewMoveElementsRef.current?.(changes);
275
- },
276
- [onPreviewMoveElementsRef, updateElement],
277
- );
278
-
279
- const rollbackResize = useCallback(
280
- (session: ResizeSession) => {
281
- const changes = session.members.map((member) => ({
282
- element: member.element,
283
- start: member.start,
284
- duration: member.duration,
285
- playbackStart: member.playbackStart,
286
- }));
287
- for (const change of changes) {
288
- updateElement(elementKey(change.element), {
289
- start: change.start,
290
- duration: change.duration,
291
- playbackStart: change.playbackStart,
292
- });
293
- }
294
- onPreviewResizeElementsRef.current?.(changes);
295
- },
296
- [onPreviewResizeElementsRef, updateElement],
297
- );
298
-
299
- const previewGroupMove = useCallback(
300
- (
301
- element: TimelineElement,
302
- selectedElementIds: Set<string>,
303
- previewStart: number,
304
- ): PreviewGroupMoveResult => {
305
- let session = moveSessionRef.current;
306
- if (!session || !sameGesture(session.grabbedKey, element)) {
307
- if (!onMoveElementsRef.current) return { active: false, previewStart };
308
- session = createMoveSession(element, selectedElementIds, timelineElementsRef.current);
309
- if (!session) return { active: false, previewStart };
310
- moveSessionRef.current = session;
311
- }
312
-
313
- const changes = resolveMoveChanges(session, previewStart);
314
- if (!changes) return { active: false, previewStart };
315
- session.changes = changes;
316
- session.hasChanged = moveSessionHasChanged(session, changes);
317
-
318
- for (const change of changes) {
319
- updateElement(elementKey(change.element), { start: change.start });
320
- }
321
- onPreviewMoveElementsRef.current?.(changes);
322
-
323
- return {
324
- active: true,
325
- previewStart: previewStartForGrabbed(session, changes, previewStart),
326
- };
327
- },
328
- [onMoveElementsRef, onPreviewMoveElementsRef, timelineElementsRef, updateElement],
329
- );
330
-
331
- const previewGroupResize = useCallback(
332
- (
333
- element: TimelineElement,
334
- selectedElementIds: Set<string>,
335
- edge: TimelineGroupResizeEdge,
336
- updates: TimelineResizeUpdates,
337
- ): PreviewGroupResizeResult => {
338
- let session = resizeSessionRef.current;
339
- if (!session || !sameGesture(session.grabbedKey, element) || session.edge !== edge) {
340
- if (!onResizeElementsRef.current) return { active: false, updates };
341
- session = createResizeSession(
342
- element,
343
- selectedElementIds,
344
- timelineElementsRef.current,
345
- edge,
346
- );
347
- if (!session) return { active: false, updates };
348
- resizeSessionRef.current = session;
349
- }
350
-
351
- const changes = resolveResizeChanges(session, updates);
352
- if (!changes) return { active: false, updates };
353
- session.changes = changes;
354
- session.hasChanged = resizeSessionHasChanged(session, changes);
355
-
356
- for (const change of changes) {
357
- updateElement(elementKey(change.element), {
358
- start: change.start,
359
- duration: change.duration,
360
- playbackStart: change.playbackStart,
361
- });
362
- }
363
- onPreviewResizeElementsRef.current?.(changes);
364
-
365
- return {
366
- active: true,
367
- updates: resizeUpdatesForGrabbed(session, changes, updates),
368
- };
369
- },
370
- [onPreviewResizeElementsRef, onResizeElementsRef, timelineElementsRef, updateElement],
371
- );
372
-
373
- const commitGroupMove = useCallback(
374
- (element: TimelineElement): boolean => {
375
- const session = moveSessionRef.current;
376
- if (!session || !sameGesture(session.grabbedKey, element)) return false;
377
- moveSessionRef.current = null;
378
- if (!session.hasChanged) return true;
379
-
380
- Promise.resolve(onMoveElementsRef.current?.(session.changes)).catch((error) => {
381
- rollbackMove(session);
382
- console.error("[Timeline] Failed to persist group clip move", error);
383
- });
384
- return true;
385
- },
386
- [onMoveElementsRef, rollbackMove],
387
- );
388
-
389
- const commitGroupResize = useCallback(
390
- (element: TimelineElement): boolean => {
391
- const session = resizeSessionRef.current;
392
- if (!session || !sameGesture(session.grabbedKey, element)) return false;
393
- resizeSessionRef.current = null;
394
- if (!session.hasChanged) return true;
395
-
396
- Promise.resolve(onResizeElementsRef.current?.(session.changes)).catch((error) => {
397
- rollbackResize(session);
398
- console.error("[Timeline] Failed to persist group clip resize", error);
399
- });
400
- return true;
401
- },
402
- [onResizeElementsRef, rollbackResize],
403
- );
404
-
405
- const clearGroupDragSessions = useCallback(() => {
406
- moveSessionRef.current = null;
407
- resizeSessionRef.current = null;
408
- }, []);
409
-
410
- return {
411
- previewGroupMove,
412
- previewGroupResize,
413
- commitGroupMove,
414
- commitGroupResize,
415
- clearGroupDragSessions,
416
- };
417
- }
@@ -1,231 +0,0 @@
1
- // @vitest-environment happy-dom
2
-
3
- import React, { act } from "react";
4
- import { createRoot } from "react-dom/client";
5
- import { afterEach, describe, expect, it } from "vitest";
6
- import type { TimelineElement } from "../store/playerStore";
7
- import { usePlayerStore } from "../store/playerStore";
8
- import { GUTTER, RULER_H, TRACK_H } from "./timelineLayout";
9
- import type { StackingTimelineLayer } from "./timelineTrackOrder";
10
- import { useTimelineMarqueeSelection } from "./useTimelineMarqueeSelection";
11
-
12
- (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
13
-
14
- function element(id: string, start: number, duration: number, track: number): TimelineElement {
15
- return { id, tag: "div", start, duration, track };
16
- }
17
-
18
- function layer(id: string, elements: TimelineElement[]): StackingTimelineLayer {
19
- return {
20
- id,
21
- kind: "visual",
22
- contextKey: "",
23
- zIndex: 0,
24
- placementTrack: elements[0]?.track ?? 0,
25
- elements,
26
- };
27
- }
28
-
29
- function pointerEvent(type: string, init: MouseEventInit & { pointerId?: number }) {
30
- const event = new MouseEvent(type, { bubbles: true, cancelable: true, ...init });
31
- Object.defineProperty(event, "pointerId", { value: init.pointerId ?? 1 });
32
- return event;
33
- }
34
-
35
- function dispatchPointer(
36
- target: HTMLElement,
37
- type: "pointerdown" | "pointermove" | "pointerup",
38
- point: { x: number; y: number },
39
- ) {
40
- target.dispatchEvent(pointerEvent(type, { button: 0, clientX: point.x, clientY: point.y }));
41
- }
42
-
43
- function dragMarquee(
44
- harness: ReturnType<typeof renderMarqueeHarness>,
45
- start: { x: number; y: number },
46
- end: { x: number; y: number },
47
- downTarget: HTMLElement = harness.scroll,
48
- ) {
49
- dispatchPointer(downTarget, "pointerdown", start);
50
- dispatchPointer(harness.scroll, "pointermove", end);
51
- dispatchPointer(harness.scroll, "pointerup", end);
52
- }
53
-
54
- function renderMarqueeHarness(layers: StackingTimelineLayer[]) {
55
- const host = document.createElement("div");
56
- document.body.append(host);
57
- const layerOrder = layers.map((item) => item.id);
58
- const setShowPopover = () => {};
59
- const setRangeSelection = () => {};
60
- const seekedX: number[] = [];
61
-
62
- function Harness() {
63
- const scrollRef = React.useRef<HTMLDivElement | null>(null);
64
- const hook = useTimelineMarqueeSelection({
65
- scrollRef,
66
- ppsRef: { current: 100 },
67
- trackOrderRef: { current: layerOrder },
68
- timelineLayersRef: { current: layers },
69
- setShowPopover,
70
- setRangeSelectionRef: { current: setRangeSelection },
71
- seekFromX: (clientX: number) => seekedX.push(clientX),
72
- });
73
- return (
74
- <div
75
- ref={scrollRef}
76
- data-scroll="true"
77
- onPointerDown={(event) => {
78
- hook.handlePointerDown(event);
79
- }}
80
- onPointerMove={(event) => {
81
- hook.handlePointerMove(event);
82
- }}
83
- onPointerUp={(event) => {
84
- hook.handlePointerUp(event);
85
- }}
86
- >
87
- <button data-clip="true">clip</button>
88
- {hook.marqueeRect && <span data-marquee="true" />}
89
- </div>
90
- );
91
- }
92
-
93
- const root = createRoot(host);
94
- act(() => {
95
- root.render(<Harness />);
96
- });
97
- const scroll = host.querySelector<HTMLElement>("[data-scroll]");
98
- if (!scroll) throw new Error("Expected scroll host");
99
- Object.defineProperty(scroll, "clientWidth", { configurable: true, value: 160 });
100
- Object.defineProperty(scroll, "clientHeight", { configurable: true, value: 160 });
101
- Object.defineProperty(scroll, "scrollWidth", { configurable: true, value: 600 });
102
- Object.defineProperty(scroll, "scrollHeight", { configurable: true, value: 260 });
103
- scroll.getBoundingClientRect = () =>
104
- ({
105
- left: 0,
106
- top: 0,
107
- right: 160,
108
- bottom: 160,
109
- width: 160,
110
- height: 160,
111
- x: 0,
112
- y: 0,
113
- toJSON: () => ({}),
114
- }) as DOMRect;
115
-
116
- return {
117
- host,
118
- scroll,
119
- root,
120
- seekedX,
121
- clip: host.querySelector<HTMLElement>("[data-clip]")!,
122
- unmount() {
123
- act(() => root.unmount());
124
- },
125
- };
126
- }
127
-
128
- afterEach(() => {
129
- document.body.innerHTML = "";
130
- usePlayerStore.getState().reset();
131
- });
132
-
133
- describe("useTimelineMarqueeSelection", () => {
134
- it("selects clips intersecting an empty-lane drag rectangle", () => {
135
- const layers = [
136
- layer("lane-0", [element("first", 0.5, 0.5, 0)]),
137
- layer("lane-1", [element("second", 2, 0.5, 1)]),
138
- layer("lane-2", [element("third", 0.5, 0.5, 2)]),
139
- ];
140
- const harness = renderMarqueeHarness(layers);
141
- const start = { x: GUTTER + 10, y: RULER_H + 4 };
142
- const end = { x: GUTTER + 260, y: RULER_H + TRACK_H * 2 - 2 };
143
-
144
- act(() => {
145
- dispatchPointer(harness.scroll, "pointerdown", start);
146
- dispatchPointer(harness.scroll, "pointermove", end);
147
- });
148
- expect(harness.host.querySelector("[data-marquee]")).not.toBeNull();
149
-
150
- act(() => {
151
- dispatchPointer(harness.scroll, "pointerup", end);
152
- });
153
-
154
- expect([...usePlayerStore.getState().selectedElementIds]).toEqual(["first", "second"]);
155
- harness.unmount();
156
- });
157
-
158
- it("treats a sub-threshold empty-lane drag as a clear click that also seeks", () => {
159
- usePlayerStore.getState().setSelection(["selected"]);
160
- const harness = renderMarqueeHarness([layer("lane-0", [element("selected", 0, 1, 0)])]);
161
-
162
- act(() => {
163
- dragMarquee(harness, { x: GUTTER + 20, y: RULER_H + 4 }, { x: GUTTER + 21, y: RULER_H + 5 });
164
- });
165
-
166
- expect(usePlayerStore.getState().selectedElementIds.size).toBe(0);
167
- expect(harness.host.querySelector("[data-marquee]")).toBeNull();
168
- // A sub-threshold press still scrubs the playhead to the click, like a plain lane click.
169
- expect(harness.seekedX).toEqual([GUTTER + 20]);
170
- harness.unmount();
171
- });
172
-
173
- it("does not start from clips or the ruler", () => {
174
- usePlayerStore.getState().setSelection(["kept"]);
175
- const harness = renderMarqueeHarness([layer("lane-0", [element("kept", 0, 1, 0)])]);
176
-
177
- act(() => {
178
- dragMarquee(
179
- harness,
180
- { x: GUTTER + 20, y: RULER_H + 4 },
181
- { x: GUTTER + 80, y: RULER_H + 40 },
182
- harness.clip,
183
- );
184
- dragMarquee(harness, { x: GUTTER + 20, y: RULER_H - 2 }, { x: GUTTER + 80, y: RULER_H + 40 });
185
- });
186
-
187
- expect([...usePlayerStore.getState().selectedElementIds]).toEqual(["kept"]);
188
- expect(harness.host.querySelector("[data-marquee]")).toBeNull();
189
- harness.unmount();
190
- });
191
-
192
- it("clears selection when the released marquee hits no clips", () => {
193
- usePlayerStore.getState().setSelection(["selected"]);
194
- const harness = renderMarqueeHarness([layer("lane-0", [element("selected", 4, 1, 0)])]);
195
-
196
- act(() => {
197
- dragMarquee(harness, { x: GUTTER + 10, y: RULER_H + 4 }, { x: GUTTER + 80, y: RULER_H + 40 });
198
- });
199
-
200
- expect(usePlayerStore.getState().selectedElementIds.size).toBe(0);
201
- harness.unmount();
202
- });
203
-
204
- it("uses autoscroll position when resolving the released marquee", () => {
205
- const originalRaf = window.requestAnimationFrame;
206
- const originalCancel = window.cancelAnimationFrame;
207
- const callbacks: FrameRequestCallback[] = [];
208
- window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
209
- callbacks.push(callback);
210
- return callbacks.length;
211
- }) as typeof window.requestAnimationFrame;
212
- window.cancelAnimationFrame = (() => {}) as typeof window.cancelAnimationFrame;
213
- const harness = renderMarqueeHarness([layer("lane-0", [element("reachable", 1.28, 0.2, 0)])]);
214
-
215
- try {
216
- act(() => {
217
- dispatchPointer(harness.scroll, "pointerdown", { x: GUTTER + 10, y: RULER_H + 4 });
218
- dispatchPointer(harness.scroll, "pointermove", { x: 155, y: RULER_H + 40 });
219
- callbacks.shift()?.(0);
220
- dispatchPointer(harness.scroll, "pointerup", { x: 155, y: RULER_H + 40 });
221
- });
222
-
223
- expect(harness.scroll.scrollLeft).toBeGreaterThan(0);
224
- expect([...usePlayerStore.getState().selectedElementIds]).toEqual(["reachable"]);
225
- harness.unmount();
226
- } finally {
227
- window.requestAnimationFrame = originalRaf;
228
- window.cancelAnimationFrame = originalCancel;
229
- }
230
- });
231
- });