@hyperframes/studio 0.7.90 → 0.7.93

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 (170) hide show
  1. package/dist/assets/{hyperframes-player-RS9ROX_S.js → hyperframes-player-NSdcoOcK.js} +1 -1
  2. package/dist/assets/{index-CquRJgm-.js → index-CCGnc_0M.js} +1 -1
  3. package/dist/assets/index-D78KEjgB.css +1 -0
  4. package/dist/assets/index-Dp1zVb9X.js +428 -0
  5. package/dist/assets/{index-Bi-CwQDX.js → index-wS6f7F5q.js} +1 -1
  6. package/dist/index.d.ts +40 -36
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +9598 -6798
  9. package/dist/index.js.map +1 -1
  10. package/package.json +8 -7
  11. package/src/App.tsx +14 -14
  12. package/src/components/DesignPanelPromoteProvider.tsx +1 -1
  13. package/src/components/ExternalFileConflictBanner.test.tsx +142 -0
  14. package/src/components/ExternalFileConflictBanner.tsx +255 -0
  15. package/src/components/StudioRightPanel.tsx +1 -1
  16. package/src/components/editor/GsapAnimationList.tsx +84 -0
  17. package/src/components/editor/GsapAnimationSection.test.tsx +100 -11
  18. package/src/components/editor/GsapAnimationSection.tsx +11 -41
  19. package/src/components/editor/PropertyPanel.tsx +1 -1
  20. package/src/components/editor/PropertyPanelFlat.tsx +29 -26
  21. package/src/components/editor/gsapAnimationCallbacks.ts +0 -10
  22. package/src/components/editor/propertyPanel3dTransform.tsx +1 -1
  23. package/src/components/editor/propertyPanelCommitField.tsx +37 -2
  24. package/src/components/editor/propertyPanelFlatDescriptors.ts +20 -0
  25. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +56 -55
  26. package/src/components/editor/propertyPanelFlatMotionSection.tsx +9 -37
  27. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +69 -0
  28. package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
  29. package/src/components/editor/propertyPanelPrimitives.tsx +2 -2
  30. package/src/components/editor/propertyPanelTransformCommit.test.ts +57 -0
  31. package/src/components/editor/propertyPanelTransformCommit.ts +14 -14
  32. package/src/components/editor/propertyPanelTypes.ts +10 -4
  33. package/src/components/editor/useInspectorGestureTransaction.test.tsx +65 -0
  34. package/src/components/editor/useInspectorGestureTransaction.ts +66 -10
  35. package/src/components/nle/useTimelineEditCallbacks.test.tsx +78 -4
  36. package/src/components/nle/useTimelineEditCallbacks.ts +12 -7
  37. package/src/components/renders/useRenderQueue.ts +16 -3
  38. package/src/components/renders/useRenderQueueTelemetry.test.tsx +104 -0
  39. package/src/components/sidebar/AssetCard.tsx +4 -3
  40. package/src/components/sidebar/AudioRow.tsx +4 -3
  41. package/src/contexts/DomEditContext.tsx +1 -1
  42. package/src/contexts/FileManagerContext.tsx +12 -0
  43. package/src/hooks/externalFileChangeOwnership.test.ts +17 -0
  44. package/src/hooks/externalFileReloadBus.test.ts +34 -0
  45. package/src/hooks/externalFileReloadBus.ts +18 -0
  46. package/src/hooks/gsapDragCommit.test.ts +39 -1
  47. package/src/hooks/gsapDragCommit.ts +12 -40
  48. package/src/hooks/gsapEditOutcome.ts +72 -0
  49. package/src/hooks/gsapResizeIntercept.test.ts +90 -2
  50. package/src/hooks/gsapResizeIntercept.ts +44 -16
  51. package/src/hooks/gsapRuntimeBridge.test.ts +195 -27
  52. package/src/hooks/gsapRuntimeBridge.ts +93 -28
  53. package/src/hooks/useAnimatedPropertyCommit.test.tsx +194 -24
  54. package/src/hooks/useAnimatedPropertyCommit.ts +101 -49
  55. package/src/hooks/useAppHotkeys.textEditing.test.tsx +215 -0
  56. package/src/hooks/useDomEditSession.test.tsx +1 -1
  57. package/src/hooks/useDomEditSession.ts +1 -1
  58. package/src/hooks/useDomGeometryCommits.test.tsx +61 -0
  59. package/src/hooks/useDomGeometryCommits.ts +20 -0
  60. package/src/hooks/useEditorSave.test.tsx +131 -0
  61. package/src/hooks/useEditorSave.ts +113 -39
  62. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +221 -0
  63. package/src/hooks/useExternalFileChangeCoordinator.ts +429 -0
  64. package/src/hooks/useFileManager.projectOwnership.test.tsx +139 -7
  65. package/src/hooks/useFileManager.ts +32 -3
  66. package/src/hooks/useGsapAnimationFetchFallback.ts +45 -24
  67. package/src/hooks/useGsapAwareEditing.test.tsx +125 -39
  68. package/src/hooks/useGsapAwareEditing.ts +88 -27
  69. package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +42 -0
  70. package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -1
  71. package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
  72. package/src/hooks/useGsapKeyframeOps.ts +20 -9
  73. package/src/hooks/usePreviewPersistence.test.ts +23 -0
  74. package/src/hooks/usePreviewPersistence.ts +22 -58
  75. package/src/hooks/useSdkSession.ts +8 -52
  76. package/src/hooks/useStudioExternalFileChanges.ts +81 -0
  77. package/src/hooks/useStudioSdkSessions.ts +2 -7
  78. package/src/hooks/useStudioSessionStart.ts +15 -0
  79. package/src/hooks/useStudioTestHooks.test.tsx +11 -7
  80. package/src/player/components/BeatStrip.test.tsx +374 -0
  81. package/src/player/components/BeatStrip.tsx +295 -37
  82. package/src/player/components/KeyframeDiamondContextMenu.test.tsx +16 -1
  83. package/src/player/components/KeyframeDiamondContextMenu.tsx +6 -5
  84. package/src/player/components/LayerDisclosureRow.tsx +2 -0
  85. package/src/player/components/Timeline.test.ts +26 -8
  86. package/src/player/components/Timeline.tsx +87 -88
  87. package/src/player/components/Timeline.virtualization.test.tsx +54 -11
  88. package/src/player/components/TimelineCanvas.tsx +14 -71
  89. package/src/player/components/TimelineClip.test.tsx +17 -2
  90. package/src/player/components/TimelineClip.tsx +20 -5
  91. package/src/player/components/TimelineClipDiamonds.test.tsx +536 -27
  92. package/src/player/components/TimelineClipDiamonds.tsx +118 -223
  93. package/src/player/components/TimelineCompactDiamonds.tsx +84 -0
  94. package/src/player/components/TimelineDiamondConnectors.tsx +31 -3
  95. package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
  96. package/src/player/components/TimelineGestureOverlay.tsx +96 -0
  97. package/src/player/components/TimelineLanes.test.tsx +64 -7
  98. package/src/player/components/TimelineLanes.tsx +84 -66
  99. package/src/player/components/TimelineOverlays.test.ts +161 -0
  100. package/src/player/components/TimelineOverlays.tsx +104 -11
  101. package/src/player/components/TimelinePropertyLanes.test.tsx +9 -5
  102. package/src/player/components/TimelinePropertyLanes.tsx +9 -2
  103. package/src/player/components/TimelineTrackHeader.test.tsx +1 -0
  104. package/src/player/components/TimelineTrackHeader.tsx +16 -0
  105. package/src/player/components/TimelineTrackRow.tsx +58 -7
  106. package/src/player/components/timelineCallbacks.ts +1 -1
  107. package/src/player/components/timelineClipDragGestureLifecycle.test.ts +18 -3
  108. package/src/player/components/timelineClipDragGestureLifecycle.ts +199 -51
  109. package/src/player/components/timelineClipDragPreview.test.ts +33 -0
  110. package/src/player/components/timelineClipDragPreview.ts +18 -12
  111. package/src/player/components/timelineClipDragTypes.ts +12 -0
  112. package/src/player/components/timelineDiamondTypes.ts +18 -21
  113. package/src/player/components/timelineDragDrop.test.tsx +206 -0
  114. package/src/player/components/timelineDragDrop.ts +152 -30
  115. package/src/player/components/timelineKeyboardNavigation.test.ts +303 -0
  116. package/src/player/components/timelineKeyboardNavigation.ts +337 -0
  117. package/src/player/components/timelineLaneProps.ts +3 -1
  118. package/src/player/components/timelineMarquee.test.ts +73 -25
  119. package/src/player/components/timelineMarquee.ts +44 -16
  120. package/src/player/components/timelineNavigationIdentity.ts +38 -0
  121. package/src/player/components/timelineTestViewport.ts +13 -0
  122. package/src/player/components/timelineViewModel.ts +4 -4
  123. package/src/player/components/useTimelineClipDrag.resize.test.tsx +219 -17
  124. package/src/player/components/useTimelineClipDrag.ts +112 -70
  125. package/src/player/components/useTimelineClipRenderWindow.ts +8 -39
  126. package/src/player/components/useTimelineFocusCoordinator.test.tsx +174 -0
  127. package/src/player/components/useTimelineFocusCoordinator.ts +221 -0
  128. package/src/player/components/useTimelineKeyboardActor.test.tsx +267 -0
  129. package/src/player/components/useTimelineKeyboardActor.ts +147 -0
  130. package/src/player/components/useTimelineKeyframeHandlers.test.tsx +43 -7
  131. package/src/player/components/useTimelineKeyframeHandlers.ts +465 -2
  132. package/src/player/components/useTimelineLaneMoveRefresh.ts +7 -0
  133. package/src/player/components/useTimelineLogicalFocus.ts +80 -0
  134. package/src/player/components/useTimelineLogicalRows.test.tsx +57 -0
  135. package/src/player/components/useTimelineLogicalRows.ts +47 -0
  136. package/src/player/components/useTimelineRangeSelection.test.tsx +204 -0
  137. package/src/player/components/useTimelineRangeSelection.ts +202 -69
  138. package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +3 -2
  139. package/src/player/components/useTimelineRowVirtualization.ts +26 -29
  140. package/src/player/components/useTrackGapMenu.test.tsx +64 -0
  141. package/src/player/components/useTrackGapMenu.ts +32 -9
  142. package/src/player/store/keyframeSlice.ts +59 -18
  143. package/src/player/store/liveTime.ts +11 -0
  144. package/src/player/store/playerStore.test.ts +106 -23
  145. package/src/player/store/playerStore.ts +43 -40
  146. package/src/player/store/timelineFocusState.ts +15 -0
  147. package/src/telemetry/canary.test.ts +325 -0
  148. package/src/telemetry/canary.ts +268 -0
  149. package/src/telemetry/client.test.ts +8 -3
  150. package/src/telemetry/client.ts +13 -43
  151. package/src/telemetry/config.ts +16 -2
  152. package/src/telemetry/distinctId.test.ts +35 -0
  153. package/src/telemetry/distinctId.ts +11 -2
  154. package/src/telemetry/policy.test.ts +81 -0
  155. package/src/telemetry/policy.ts +104 -0
  156. package/src/utils/domEditSaveQueue.test.ts +37 -1
  157. package/src/utils/domEditSaveQueue.ts +22 -4
  158. package/src/utils/externalConflictStorage.test.ts +95 -0
  159. package/src/utils/externalConflictStorage.ts +172 -0
  160. package/src/utils/studioFileVersion.test.ts +28 -1
  161. package/src/utils/studioFileVersion.ts +30 -0
  162. package/src/utils/studioPendingEdits.test.ts +108 -1
  163. package/src/utils/studioPendingEdits.ts +39 -2
  164. package/src/utils/studioSaveDiagnostics.ts +5 -0
  165. package/src/utils/studioTelemetry.test.ts +76 -0
  166. package/src/utils/studioTelemetry.ts +14 -6
  167. package/dist/assets/index-BXkzp_OU.css +0 -1
  168. package/dist/assets/index-Cql15Yur.js +0 -428
  169. package/src/player/components/useTimelineRevealClip.test.tsx +0 -139
  170. package/src/player/components/useTimelineRevealClip.ts +0 -180
@@ -5,8 +5,11 @@ import {
5
5
  isTimelineRulerPress,
6
6
  getMarqueeRect,
7
7
  getTimelineClipRect,
8
+ getMarqueeClipCandidates,
8
9
  computeMarqueeSelection,
9
10
  } from "./timelineMarquee";
11
+ import { createTimelineClipIndex } from "../lib/timelineClipIndex";
12
+ import type { TimelineElement } from "../store/playerStore";
10
13
  import {
11
14
  GUTTER,
12
15
  LANE_H,
@@ -14,6 +17,7 @@ import {
14
17
  RULER_H,
15
18
  CLIP_Y,
16
19
  TRACKS_LEFT_PAD,
20
+ createTimelineRowGeometry,
17
21
  getTimelineRowTop,
18
22
  } from "./timelineLayout";
19
23
 
@@ -95,9 +99,13 @@ describe("getMarqueeRect", () => {
95
99
 
96
100
  describe("getTimelineClipRect", () => {
97
101
  const trackOrder = [0, 2, 5];
102
+ const geometry = createTimelineRowGeometry(
103
+ trackOrder,
104
+ trackOrder.map(() => TRACK_H),
105
+ );
98
106
 
99
107
  it("maps start/duration to x via pps and the track row to y via the shared row→y helper", () => {
100
- const rect = getTimelineClipRect({ start: 2, duration: 3, track: 2 }, trackOrder, 100, GUTTER);
108
+ const rect = getTimelineClipRect({ start: 2, duration: 3, track: 2 }, geometry, 100, GUTTER);
101
109
  expect(rect).toEqual({
102
110
  left: GUTTER + 200,
103
111
  top: getTimelineRowTop(1) + CLIP_Y,
@@ -107,54 +115,48 @@ describe("getTimelineClipRect", () => {
107
115
  });
108
116
 
109
117
  it("places the first visible track below the ruler + top breathing pad", () => {
110
- const rect = getTimelineClipRect({ start: 0, duration: 1, track: 0 }, trackOrder, 50, GUTTER);
118
+ const rect = getTimelineClipRect({ start: 0, duration: 1, track: 0 }, geometry, 50, GUTTER);
111
119
  expect(rect?.top).toBe(getTimelineRowTop(0) + CLIP_Y);
112
120
  expect(rect?.left).toBe(GUTTER);
113
121
  });
114
122
 
115
123
  it("uses the row index in trackOrder, not the raw track number", () => {
116
- const rect = getTimelineClipRect({ start: 0, duration: 1, track: 5 }, trackOrder, 50, GUTTER);
124
+ const rect = getTimelineClipRect({ start: 0, duration: 1, track: 5 }, geometry, 50, GUTTER);
117
125
  expect(rect?.top).toBe(getTimelineRowTop(2) + CLIP_Y);
118
126
  });
119
127
 
120
128
  it("uses cumulative tops and the resolved height for an expanded row", () => {
121
129
  const rowHeights = [TRACK_H + 2 * LANE_H, TRACK_H, TRACK_H];
130
+ const expandedGeometry = createTimelineRowGeometry(trackOrder, rowHeights);
122
131
  const rect = getTimelineClipRect(
123
132
  { start: 0, duration: 1, track: 0 },
124
- trackOrder,
133
+ expandedGeometry,
125
134
  50,
126
135
  GUTTER,
127
- rowHeights,
128
136
  );
129
137
  expect(rect).toMatchObject({
130
138
  top: getTimelineRowTop(0, rowHeights) + CLIP_Y,
131
- height: rowHeights[0] - CLIP_Y * 2,
139
+ height: TRACK_H - CLIP_Y * 2,
132
140
  });
133
141
  expect(
134
- getTimelineClipRect({ start: 0, duration: 1, track: 2 }, trackOrder, 50, GUTTER, rowHeights)
135
- ?.top,
142
+ getTimelineClipRect({ start: 0, duration: 1, track: 2 }, expandedGeometry, 50, GUTTER)?.top,
136
143
  ).toBe(getTimelineRowTop(1, rowHeights) + CLIP_Y);
137
144
  });
138
145
 
139
146
  it("enforces the 4px minimum rendered width", () => {
140
- const rect = getTimelineClipRect(
141
- { start: 0, duration: 0.01, track: 0 },
142
- trackOrder,
143
- 10,
144
- GUTTER,
145
- );
147
+ const rect = getTimelineClipRect({ start: 0, duration: 0.01, track: 0 }, geometry, 10, GUTTER);
146
148
  expect(rect?.width).toBe(4);
147
149
  });
148
150
 
149
151
  it("returns null for a track that is not displayed or an invalid pps", () => {
150
152
  expect(
151
- getTimelineClipRect({ start: 0, duration: 1, track: 9 }, trackOrder, 100, GUTTER),
153
+ getTimelineClipRect({ start: 0, duration: 1, track: 9 }, geometry, 100, GUTTER),
152
154
  ).toBeNull();
153
155
  expect(
154
- getTimelineClipRect({ start: 0, duration: 1, track: 0 }, trackOrder, 0, GUTTER),
156
+ getTimelineClipRect({ start: 0, duration: 1, track: 0 }, geometry, 0, GUTTER),
155
157
  ).toBeNull();
156
158
  expect(
157
- getTimelineClipRect({ start: 0, duration: 1, track: 0 }, trackOrder, NaN, GUTTER),
159
+ getTimelineClipRect({ start: 0, duration: 1, track: 0 }, geometry, NaN, GUTTER),
158
160
  ).toBeNull();
159
161
  });
160
162
  });
@@ -162,6 +164,10 @@ describe("getTimelineClipRect", () => {
162
164
  describe("computeMarqueeSelection", () => {
163
165
  // Two visible tracks: row 0 = track 0, row 1 = track 1. pps 100.
164
166
  const trackOrder = [0, 1];
167
+ const rowGeometry = createTimelineRowGeometry(
168
+ trackOrder,
169
+ trackOrder.map(() => TRACK_H),
170
+ );
165
171
  const pps = 100;
166
172
  const clips = [
167
173
  { id: "a", start: 0, duration: 1, track: 0 }, // x [32,132], row 0
@@ -175,7 +181,7 @@ describe("computeMarqueeSelection", () => {
175
181
  const marquee = { left: ORIGIN, top: row0Top, width: 50, height: 10 };
176
182
  const { ids, primaryId } = computeMarqueeSelection({
177
183
  clips,
178
- trackOrder,
184
+ rowGeometry,
179
185
  pps,
180
186
  contentOrigin: ORIGIN,
181
187
  marquee,
@@ -188,7 +194,7 @@ describe("computeMarqueeSelection", () => {
188
194
  const marquee = { left: ORIGIN, top: row0Top, width: 60, height: row1Top - row0Top + 5 };
189
195
  const { ids } = computeMarqueeSelection({
190
196
  clips,
191
- trackOrder,
197
+ rowGeometry,
192
198
  pps,
193
199
  contentOrigin: ORIGIN,
194
200
  marquee,
@@ -200,7 +206,7 @@ describe("computeMarqueeSelection", () => {
200
206
  const marquee = { left: ORIGIN + 140, top: row0Top, width: 50, height: 10 };
201
207
  const { ids } = computeMarqueeSelection({
202
208
  clips,
203
- trackOrder,
209
+ rowGeometry,
204
210
  pps,
205
211
  contentOrigin: ORIGIN,
206
212
  marquee,
@@ -212,7 +218,7 @@ describe("computeMarqueeSelection", () => {
212
218
  const marquee = { left: GUTTER + 140, top: row0Top, width: 50, height: 10 };
213
219
  const { ids, primaryId } = computeMarqueeSelection({
214
220
  clips,
215
- trackOrder,
221
+ rowGeometry,
216
222
  pps,
217
223
  contentOrigin: GUTTER,
218
224
  marquee,
@@ -226,7 +232,7 @@ describe("computeMarqueeSelection", () => {
226
232
  const marquee = { left: GUTTER, top: row1Top, width: 100, height: 10 };
227
233
  const { ids, primaryId } = computeMarqueeSelection({
228
234
  clips,
229
- trackOrder,
235
+ rowGeometry,
230
236
  pps,
231
237
  contentOrigin: GUTTER,
232
238
  marquee,
@@ -240,10 +246,11 @@ describe("computeMarqueeSelection", () => {
240
246
  const wide = { left: ORIGIN, top: row0Top, width: 320, height: 10 };
241
247
  const narrow = { left: ORIGIN, top: row0Top, width: 80, height: 10 };
242
248
  expect(
243
- computeMarqueeSelection({ clips, trackOrder, pps, contentOrigin: ORIGIN, marquee: wide }).ids,
249
+ computeMarqueeSelection({ clips, rowGeometry, pps, contentOrigin: ORIGIN, marquee: wide })
250
+ .ids,
244
251
  ).toEqual(new Set(["a", "b"]));
245
252
  expect(
246
- computeMarqueeSelection({ clips, trackOrder, pps, contentOrigin: ORIGIN, marquee: narrow })
253
+ computeMarqueeSelection({ clips, rowGeometry, pps, contentOrigin: ORIGIN, marquee: narrow })
247
254
  .ids,
248
255
  ).toEqual(new Set(["a"]));
249
256
  });
@@ -252,7 +259,7 @@ describe("computeMarqueeSelection", () => {
252
259
  const marquee = { left: 0, top: 0, width: 10000, height: 10000 };
253
260
  const { ids } = computeMarqueeSelection({
254
261
  clips: [{ id: "x", start: 0, duration: 1, track: 7 }],
255
- trackOrder,
262
+ rowGeometry,
256
263
  pps,
257
264
  contentOrigin: GUTTER,
258
265
  marquee,
@@ -260,3 +267,44 @@ describe("computeMarqueeSelection", () => {
260
267
  expect(ids).toEqual(new Set());
261
268
  });
262
269
  });
270
+
271
+ describe("getMarqueeClipCandidates", () => {
272
+ it("queries only the intersecting rows and time span", () => {
273
+ const rowGeometry = createTimelineRowGeometry([0, 1, 2], [TRACK_H, TRACK_H, TRACK_H]);
274
+ const near: TimelineElement = { id: "near", tag: "div", start: 1, duration: 1, track: 1 };
275
+ const wrongTime: TimelineElement = {
276
+ id: "wrong-time",
277
+ tag: "div",
278
+ start: 20,
279
+ duration: 1,
280
+ track: 1,
281
+ };
282
+ const wrongRow: TimelineElement = {
283
+ id: "wrong-row",
284
+ tag: "div",
285
+ start: 1,
286
+ duration: 1,
287
+ track: 2,
288
+ };
289
+ const clipIndex = createTimelineClipIndex([
290
+ [0, []],
291
+ [1, [near, wrongTime]],
292
+ [2, [wrongRow]],
293
+ ]);
294
+
295
+ expect(
296
+ getMarqueeClipCandidates({
297
+ clipIndex,
298
+ rowGeometry,
299
+ marquee: {
300
+ left: ORIGIN + 100,
301
+ top: getTimelineRowTop(1),
302
+ width: 100,
303
+ height: TRACK_H - 1,
304
+ },
305
+ pps: 100,
306
+ contentOrigin: ORIGIN,
307
+ }),
308
+ ).toEqual([near]);
309
+ });
310
+ });
@@ -1,5 +1,7 @@
1
- import { RULER_H, CLIP_Y, getTimelineRowHeight, getTimelineRowTop } from "./timelineLayout";
1
+ import { RULER_H, CLIP_Y, TRACK_H, type TimelineRowGeometry } from "./timelineLayout";
2
2
  import { rectsOverlap, type Rect } from "../../utils/marqueeGeometry";
3
+ import { queryTimelineClipIndex, type TimelineClipIndex } from "../lib/timelineClipIndex";
4
+ import type { TimelineElement } from "../store/playerStore";
3
5
 
4
6
  /** Pointer must travel at least this far (either axis) before a pointerdown on
5
7
  * the empty timeline body becomes a marquee drag instead of a plain click. */
@@ -62,23 +64,22 @@ export function getMarqueeRect(
62
64
  /**
63
65
  * A clip's rendered rect in canvas/content coordinates (the same space the
64
66
  * marquee rect lives in): x from the shared content origin + start * pps, y from the clip's row
65
- * index within the visible track order (cumulative row top + CLIP_Y).
67
+ * index within the canonical row geometry (cumulative row top + CLIP_Y).
66
68
  * Returns null when the clip's track is not currently displayed.
67
69
  */
68
70
  export function getTimelineClipRect(
69
71
  clip: Pick<MarqueeClipInput, "start" | "duration" | "track">,
70
- trackOrder: number[],
72
+ rowGeometry: TimelineRowGeometry,
71
73
  pps: number,
72
74
  contentOrigin: number,
73
- rowHeights: readonly number[] = [],
74
75
  ): Rect | null {
75
- const row = trackOrder.indexOf(clip.track);
76
+ const row = rowGeometry.getRowIndex(clip.track);
76
77
  if (row < 0 || !Number.isFinite(pps) || pps <= 0) return null;
77
78
  return {
78
79
  left: contentOrigin + clip.start * pps,
79
- top: getTimelineRowTop(row, rowHeights) + CLIP_Y,
80
+ top: rowGeometry.getRowTop(row) + CLIP_Y,
80
81
  width: Math.max(clip.duration * pps, MIN_CLIP_W),
81
- height: getTimelineRowHeight(row, rowHeights) - CLIP_Y * 2,
82
+ height: TRACK_H - CLIP_Y * 2,
82
83
  };
83
84
  }
84
85
 
@@ -90,29 +91,56 @@ export interface MarqueeSelectionResult {
90
91
  primaryId: string | null;
91
92
  }
92
93
 
94
+ /** Narrow a marquee hit test to the intersecting logical rows and time span. */
95
+ export function getMarqueeClipCandidates(input: {
96
+ clipIndex: TimelineClipIndex;
97
+ rowGeometry: TimelineRowGeometry;
98
+ marquee: Rect;
99
+ pps: number;
100
+ contentOrigin: number;
101
+ }): readonly TimelineElement[] {
102
+ if (!(input.pps > 0) || input.marquee.width <= 0 || input.marquee.height <= 0) return [];
103
+ const lastRow = input.rowGeometry.rowKeys.length - 1;
104
+ const first = Math.max(0, Math.floor(input.rowGeometry.getRowFromY(input.marquee.top)));
105
+ const last = Math.min(
106
+ lastRow,
107
+ Math.floor(input.rowGeometry.getRowFromY(input.marquee.top + input.marquee.height)),
108
+ );
109
+ if (first > last) return [];
110
+ const paddingSeconds = MIN_CLIP_W / input.pps;
111
+ const start = Math.max(
112
+ 0,
113
+ (input.marquee.left - input.contentOrigin) / input.pps - paddingSeconds,
114
+ );
115
+ const end =
116
+ (input.marquee.left + input.marquee.width - input.contentOrigin) / input.pps + paddingSeconds;
117
+ if (end <= start) return [];
118
+
119
+ const candidates: TimelineElement[] = [];
120
+ for (let row = first; row <= last; row += 1) {
121
+ const rowKey = input.rowGeometry.rowKeys[row];
122
+ if (rowKey === undefined) continue;
123
+ candidates.push(...queryTimelineClipIndex(input.clipIndex, rowKey, { start, end }));
124
+ }
125
+ return candidates;
126
+ }
127
+
93
128
  /**
94
129
  * Live marquee selection: every clip whose rendered rect intersects the marquee.
95
130
  * `baseSelection` (shift/cmd-additive) is unioned in but never affects primaryId.
96
131
  */
97
132
  export function computeMarqueeSelection(input: {
98
133
  clips: MarqueeClipInput[];
99
- trackOrder: number[];
134
+ rowGeometry: TimelineRowGeometry;
100
135
  pps: number;
101
136
  contentOrigin: number;
102
137
  marquee: Rect;
103
138
  baseSelection?: Iterable<string>;
104
- rowHeights?: readonly number[];
105
139
  }): MarqueeSelectionResult {
106
140
  const ids = new Set<string>(input.baseSelection ?? []);
107
141
  let primaryId: string | null = null;
108
142
  for (const clip of input.clips) {
109
- const rect = getTimelineClipRect(
110
- clip,
111
- input.trackOrder,
112
- input.pps,
113
- input.contentOrigin,
114
- input.rowHeights,
115
- );
143
+ const rect = getTimelineClipRect(clip, input.rowGeometry, input.pps, input.contentOrigin);
116
144
  if (rect && rectsOverlap(rect, input.marquee)) {
117
145
  ids.add(clip.id);
118
146
  primaryId = clip.id;
@@ -0,0 +1,38 @@
1
+ import type { PropertyGroupName } from "@hyperframes/core/gsap-parser";
2
+ import {
3
+ timelineKeyframeSelectionKey,
4
+ type TimelineKeyframeTarget,
5
+ } from "./timelineKeyframeIdentity";
6
+
7
+ function stableId(kind: string, ...parts: Array<string | number>): string {
8
+ // Attribute-safe by construction; callers embedding it in CSS selectors must use CSS.escape.
9
+ return JSON.stringify(["timeline", kind, ...parts]);
10
+ }
11
+
12
+ export function timelineTrackRowId(track: number): string {
13
+ return stableId("track", track);
14
+ }
15
+
16
+ export function timelinePropertyRowId(elementId: string, group: PropertyGroupName): string {
17
+ return stableId("property", elementId, group);
18
+ }
19
+
20
+ export function timelineLogicalRowCellId(
21
+ lanesId: string,
22
+ rowId: string,
23
+ cell: "header" | "content",
24
+ ): string {
25
+ return `${lanesId}-${stableId("cell", rowId, cell)}`;
26
+ }
27
+
28
+ export function timelineClipFocusId(elementId: string): string {
29
+ return stableId("clip", elementId);
30
+ }
31
+
32
+ export function timelineKeyframeFocusId(elementId: string, target: TimelineKeyframeTarget): string {
33
+ return stableId("keyframe", timelineKeyframeSelectionKey(elementId, target));
34
+ }
35
+
36
+ export function timelineEaseFocusId(elementId: string, target: TimelineKeyframeTarget): string {
37
+ return stableId("ease", timelineKeyframeSelectionKey(elementId, target));
38
+ }
@@ -0,0 +1,13 @@
1
+ /** Configure the shared viewport geometry used by timeline gesture hook tests. */
2
+ export function configureTimelineTestViewport(scroll: HTMLElement, scrollHeight: number): void {
3
+ scroll.getBoundingClientRect = () =>
4
+ ({ left: 0, top: 0, right: 800, bottom: 240, width: 800, height: 240 }) as DOMRect;
5
+ Object.defineProperties(scroll, {
6
+ scrollLeft: { configurable: true, writable: true, value: 0 },
7
+ scrollTop: { configurable: true, writable: true, value: 0 },
8
+ scrollWidth: { configurable: true, value: 10_000 },
9
+ scrollHeight: { configurable: true, value: scrollHeight },
10
+ clientWidth: { configurable: true, value: 800 },
11
+ clientHeight: { configurable: true, value: 240 },
12
+ });
13
+ }
@@ -29,10 +29,10 @@ export function getTimelinePreviewElement(
29
29
  element: TimelineElement,
30
30
  resizingClip: ResizingClipState | null,
31
31
  ): TimelineElement {
32
- if (
33
- resizingClip &&
34
- getTimelineElementIdentity(resizingClip.element) === getTimelineElementIdentity(element)
35
- ) {
32
+ const elementIdentity = getTimelineElementIdentity(element);
33
+ const groupPreview = resizingClip?.groupPreview?.find((change) => change.key === elementIdentity);
34
+ if (groupPreview) return { ...element, ...groupPreview };
35
+ if (resizingClip && getTimelineElementIdentity(resizingClip.element) === elementIdentity) {
36
36
  return {
37
37
  ...element,
38
38
  start: resizingClip.previewStart,