@hyperframes/studio 0.8.93 → 0.8.94

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 (39) hide show
  1. package/dist/assets/{hyperframes-player-CkV9KK6A.js → hyperframes-player-Bkm-3G56.js} +1 -1
  2. package/dist/assets/{index-BlLFipOk.js → index-BUn5H_np.js} +1 -1
  3. package/dist/assets/{index-CYk0NwSv.js → index-BupvpoiM.js} +175 -175
  4. package/dist/assets/{index-V3fzEfOD.js → index-n2ZRiO44.js} +1 -1
  5. package/dist/index.d.ts +62 -46
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +142 -98
  8. package/dist/index.js.map +1 -1
  9. package/package.json +8 -8
  10. package/src/components/TimelineToolbar.test.tsx +17 -0
  11. package/src/components/TimelineToolbar.tsx +3 -1
  12. package/src/components/editor/ConnectedDomEditOverlay.test.tsx +2 -0
  13. package/src/components/editor/ConnectedDomEditOverlay.tsx +3 -1
  14. package/src/components/editor/DomEditCropHandles.test.tsx +87 -1
  15. package/src/components/editor/DomEditCropHandles.tsx +64 -62
  16. package/src/components/editor/DomEditOverlay.test.ts +39 -0
  17. package/src/components/editor/DomEditOverlay.tsx +6 -1
  18. package/src/components/editor/DomEditSelectionChrome.test.tsx +114 -1
  19. package/src/components/editor/DomEditSelectionChrome.tsx +17 -10
  20. package/src/components/editor/PreviewOverlayProvider.test.tsx +19 -0
  21. package/src/components/editor/PreviewOverlayProvider.tsx +1 -1
  22. package/src/components/editor/SnapToolbar.tsx +1 -1
  23. package/src/components/editor/domEditOverlayGeometry.ts +2 -0
  24. package/src/components/nle/PreviewOverlays.tsx +3 -3
  25. package/src/hooks/gsapResizeIntercept.test.ts +40 -0
  26. package/src/hooks/gsapResizeIntercept.ts +8 -4
  27. package/src/index.ts +4 -0
  28. package/src/player/components/PlayerControls.tsx +2 -15
  29. package/src/player/components/ShortcutsPanel.test.tsx +33 -1
  30. package/src/player/components/ShortcutsPanel.tsx +26 -0
  31. package/src/player/components/Timeline.trackPadding.test.tsx +157 -0
  32. package/src/player/components/TimelineCanvas.tsx +16 -19
  33. package/src/player/components/TimelineTypes.ts +2 -0
  34. package/src/player/components/timelineLayout.test.ts +47 -0
  35. package/src/player/components/timelineLayout.ts +22 -15
  36. package/src/player/components/useTimelineProviderState.tsx +2 -0
  37. package/src/player/components/useTimelineTrackLayout.ts +11 -2
  38. package/src/player/components/useTimelineVirtualRows.test.tsx +32 -0
  39. package/src/player/components/useTimelineVirtualRows.ts +2 -2
@@ -247,6 +247,53 @@ describe("track-area breathing pad y-math", () => {
247
247
  });
248
248
  });
249
249
 
250
+ describe("host track padding", () => {
251
+ const geometry = createTimelineRowGeometry([0, 1, 2], baseRows(3), { top: 0, bottom: TRACK_H });
252
+ // Row 1 with no top pad, but still row 0 if any reader keeps the default pad.
253
+ const yInRow1 = RULER_H + TRACK_H + 10;
254
+
255
+ it("moves the first row up and sizes the canvas from the host pads", () => {
256
+ expect(geometry.getRowTop(0)).toBe(RULER_H);
257
+ expect(getTimelineRowTop(1, geometry.rowHeights)).toBe(RULER_H + TRACK_H);
258
+ expect(getTimelineCanvasHeight(geometry.rowHeights)).toBe(RULER_H + 4 * TRACK_H);
259
+ });
260
+
261
+ it("maps a drag pointer y to the row under it", () => {
262
+ expect(Math.floor(geometry.getRowFromY(yInRow1))).toBe(1);
263
+ expect(Math.floor(getTimelineRowFromY(yInRow1, geometry.rowHeights))).toBe(1);
264
+ });
265
+
266
+ it("drops onto the row under the pointer, and a new track in the bottom pad", () => {
267
+ const drop = {
268
+ rectLeft: 0,
269
+ rectTop: 0,
270
+ scrollLeft: 0,
271
+ scrollTop: 0,
272
+ contentOrigin: GUTTER,
273
+ pixelsPerSecond: 100,
274
+ rowHeights: geometry.rowHeights,
275
+ trackOrder: [0, 1, 2],
276
+ };
277
+ expect(resolveTimelineAssetDrop(drop, GUTTER, yInRow1).track).toBe(1);
278
+ const inBottomPad = RULER_H + 3 * TRACK_H + TRACK_H / 2;
279
+ expect(resolveTimelineAssetDrop(drop, GUTTER, inBottomPad).track).toBe(3);
280
+ });
281
+
282
+ it("clamps a negative pad to 0 and replaces a non-finite one with the default", () => {
283
+ const odd = createTimelineRowGeometry([0], baseRows(1), { top: -20, bottom: Number.NaN });
284
+ expect(odd.padding).toEqual({ top: 0, bottom: TRACKS_BOTTOM_PAD });
285
+ expect(odd.getRowTop(0)).toBe(RULER_H);
286
+ expect(odd.canvasHeight).toBe(RULER_H + TRACK_H + TRACKS_BOTTOM_PAD);
287
+ const infinite = createTimelineRowGeometry([0], baseRows(1), { top: Infinity });
288
+ expect(infinite.padding.top).toBe(TRACKS_TOP_PAD);
289
+ });
290
+
291
+ it("keeps the default pads when a host passes none", () => {
292
+ const defaults = createTimelineRowGeometry([0], baseRows(1));
293
+ expect(defaults.padding).toEqual({ top: TRACKS_TOP_PAD, bottom: TRACKS_BOTTOM_PAD });
294
+ });
295
+ });
296
+
250
297
  describe("getTimelineScrubTime", () => {
251
298
  const at = (clientX: number, duration = 10) =>
252
299
  getTimelineScrubTime({
@@ -55,9 +55,8 @@ export function getTimelineLaneTop(laneIndex: number): number {
55
55
  return TRACK_H + Math.max(0, Math.trunc(laneIndex)) * LANE_H;
56
56
  }
57
57
  /**
58
- * Breathing room INSIDE the scroll area (CapCut-style), threaded through every
59
- * track-row y computation via {@link getTimelineRowTop} — never inline a magic
60
- * offset; a track row's top is always ruler + top pad + cumulative row heights.
58
+ * Default breathing room INSIDE the scroll area (CapCut-style). A host overrides
59
+ * it per Timeline; rows always read TimelineRowGeometry.padding, never these.
61
60
  *
62
61
  * - TRACKS_TOP_PAD: empty space between the (sticky) ruler and the first track
63
62
  * (~half a track height) so the first clip isn't jammed under the ruler.
@@ -66,6 +65,10 @@ export function getTimelineLaneTop(laneIndex: number): number {
66
65
  */
67
66
  export const TRACKS_TOP_PAD = 50;
68
67
  export const TRACKS_BOTTOM_PAD = Math.round(TRACK_H * 1.5);
68
+ export interface TimelineTrackPadding {
69
+ top?: number;
70
+ bottom?: number;
71
+ }
69
72
  /**
70
73
  * Breathing room LEFT of t=0 (CapCut-style), inside the scroll content — the
71
74
  * horizontal sibling of TRACKS_TOP_PAD: empty lane surface between the sticky
@@ -115,12 +118,17 @@ function validRowHeight(height: number | undefined): number {
115
118
  return height;
116
119
  }
117
120
 
121
+ function validPad(pad: number | undefined, fallback: number): number {
122
+ return pad !== undefined && Number.isFinite(pad) ? Math.max(0, pad) : fallback;
123
+ }
124
+
118
125
  export interface TimelineRowGeometry {
119
126
  readonly rowKeys: readonly number[];
120
127
  readonly rowHeights: readonly number[];
121
128
  /** Cumulative row boundaries, including the final bottom boundary. */
122
129
  readonly rowOffsets: readonly number[];
123
130
  readonly rowsHeight: number;
131
+ readonly padding: Readonly<Required<TimelineTrackPadding>>;
124
132
  readonly canvasHeight: number;
125
133
  getRowIndex(rowKey: number): number;
126
134
  getRowHeight(row: number): number;
@@ -141,7 +149,10 @@ const EMPTY_ROW_HEIGHTS: readonly number[] = Object.freeze([]);
141
149
  export function createTimelineRowGeometry(
142
150
  rowKeys: readonly number[],
143
151
  rowHeights: readonly number[],
152
+ padding: TimelineTrackPadding = {},
144
153
  ): TimelineRowGeometry {
154
+ const topPad = validPad(padding.top, TRACKS_TOP_PAD);
155
+ const bottomPad = validPad(padding.bottom, TRACKS_BOTTOM_PAD);
145
156
  const heights = Object.freeze(rowHeights.map(validRowHeight));
146
157
  const keys = Object.freeze(
147
158
  heights.map((_, row) => {
@@ -165,7 +176,7 @@ export function createTimelineRowGeometry(
165
176
  return (offsets[wholeRow] ?? 0) + (row - wholeRow) * getRowHeight(wholeRow);
166
177
  };
167
178
  const getRowFromY = (contentY: number) => {
168
- const y = contentY - RULER_H - TRACKS_TOP_PAD;
179
+ const y = contentY - RULER_H - topPad;
169
180
  if (heights.length === 0) return y / TRACK_H;
170
181
  if (y < 0) return y / getRowHeight(0);
171
182
  const rowsHeight = offsets[heights.length] ?? 0;
@@ -188,10 +199,11 @@ export function createTimelineRowGeometry(
188
199
  rowHeights: heights,
189
200
  rowOffsets: offsets,
190
201
  rowsHeight: offsets.at(-1) ?? 0,
191
- canvasHeight: RULER_H + TRACKS_TOP_PAD + (offsets.at(-1) ?? 0) + TRACKS_BOTTOM_PAD,
202
+ padding: Object.freeze({ top: topPad, bottom: bottomPad }),
203
+ canvasHeight: RULER_H + topPad + (offsets.at(-1) ?? 0) + bottomPad,
192
204
  getRowIndex: (rowKey) => rowIndexByKey.get(rowKey) ?? -1,
193
205
  getRowHeight,
194
- getRowTop: (row) => RULER_H + TRACKS_TOP_PAD + getRowOffset(row),
206
+ getRowTop: (row) => RULER_H + topPad + getRowOffset(row),
195
207
  getRowFromY,
196
208
  getRowPositionFromY: (contentY) => {
197
209
  const rowFloat = getRowFromY(contentY);
@@ -204,7 +216,7 @@ export function createTimelineRowGeometry(
204
216
  return frozenGeometry;
205
217
  }
206
218
 
207
- /** Compatibility accessor; repeated calls for one height-array reuse one snapshot. */
219
+ /** Compatibility accessor: a geometry's own rowHeights resolves back to it, pads included. */
208
220
  export function getTimelineRowGeometry(rowHeights: readonly number[]): TimelineRowGeometry {
209
221
  const cached = rowGeometryCache.get(rowHeights);
210
222
  if (cached) return cached;
@@ -228,10 +240,6 @@ export function getTimelineRowHeight(
228
240
  return validRowHeight(rowHeights[row]);
229
241
  }
230
242
 
231
- function getTimelineRowOffset(row: number, rowHeights: readonly number[]): number {
232
- return getTimelineRowGeometry(rowHeights).getRowTop(row) - RULER_H - TRACKS_TOP_PAD;
233
- }
234
-
235
243
  /**
236
244
  * The y (content-space) of the top edge of track ROW index `row` (0 = first
237
245
  * displayed lane). The single source of truth for row->y: the ruler height plus
@@ -243,7 +251,7 @@ export function getTimelineRowTop(
243
251
  row: number,
244
252
  rowHeights: readonly number[] = EMPTY_ROW_HEIGHTS,
245
253
  ): number {
246
- return RULER_H + TRACKS_TOP_PAD + getTimelineRowOffset(row, rowHeights);
254
+ return getTimelineRowGeometry(rowHeights).getRowTop(row);
247
255
  }
248
256
 
249
257
  /**
@@ -484,9 +492,8 @@ export function getTimelinePlaybackFollowScrollLeft(input: {
484
492
  }
485
493
 
486
494
  export function getTimelineCanvasHeight(rowHeights: readonly number[]): number {
487
- // RULER_H + top pad + lanes + bottom pad. The old TIMELINE_SCROLL_BUFFER is
488
- // subsumed by TRACKS_BOTTOM_PAD (which is larger), so the drag-into-void space
489
- // below the last lane is real scrollable surface, not a hidden buffer.
495
+ // RULER_H + top pad + lanes + bottom pad, read from the geometry's padding.
496
+ // The drag-into-void space below the last lane is real scrollable surface.
490
497
  return getTimelineRowGeometry(rowHeights).canvasHeight;
491
498
  }
492
499
 
@@ -65,6 +65,7 @@ export function useTimelineProviderState({
65
65
  theme: themeOverrides,
66
66
  showAudioEffects = true,
67
67
  showKeyframes = true,
68
+ trackPadding,
68
69
  sessionEpoch = 0,
69
70
  previewIframeRef,
70
71
  onZIndexReorder,
@@ -143,6 +144,7 @@ export function useTimelineProviderState({
143
144
  gsapAnimations,
144
145
  selectedElementId,
145
146
  selectedElementIds,
147
+ trackPadding,
146
148
  );
147
149
  const timelineElementsRef = useRef(timelineElements);
148
150
  timelineElementsRef.current = timelineElements; // oxlint-disable-line react/refs -- event handlers read the latest elements
@@ -10,6 +10,7 @@ import {
10
10
  TRACK_H,
11
11
  createTimelineRowGeometry,
12
12
  type TimelineRowGeometry,
13
+ type TimelineTrackPadding,
13
14
  trackHeights,
14
15
  type TimelineTrackHeightClip,
15
16
  } from "./timelineLayout";
@@ -164,7 +165,10 @@ function useTimelineRowHeights(
164
165
  selectedElementId: string | null,
165
166
  selectedElementIds: ReadonlySet<string>,
166
167
  groups: readonly TimelineTrackGroupInfo[],
168
+ trackPadding: TimelineTrackPadding | undefined,
167
169
  ) {
170
+ const padTop = trackPadding?.top;
171
+ const padBottom = trackPadding?.bottom;
168
172
  const expandedClipIds = usePlayerStore((s) => s.expandedClipIds);
169
173
  const expandedLaneOwnerIds = usePlayerStore((s) => s.expandedLaneOwnerIds);
170
174
  const { laneCounts, rowGeometry } = useMemo(() => {
@@ -207,9 +211,12 @@ function useTimelineRowHeights(
207
211
  rowGeometry: createTimelineRowGeometry(
208
212
  tracks.map(([track]) => track),
209
213
  rowHeights,
214
+ { top: padTop, bottom: padBottom },
210
215
  ),
211
216
  };
212
217
  }, [
218
+ padTop,
219
+ padBottom,
213
220
  expandedClipIds,
214
221
  expandedLaneOwnerIds,
215
222
  gsapAnimations,
@@ -233,6 +240,7 @@ export function useTimelineTrackLayout(
233
240
  gsapAnimations: Map<string, GsapAnimation[]>,
234
241
  selectedElementId: string | null,
235
242
  selectedElementIds: ReadonlySet<string>,
243
+ trackPadding?: TimelineTrackPadding,
236
244
  ) {
237
245
  const { tracks, trackStyles, trackOrder, groups, trackGroupOf } =
238
246
  useTimelineTrackDerivations(expandedElements);
@@ -244,6 +252,7 @@ export function useTimelineTrackLayout(
244
252
  selectedElementId,
245
253
  selectedElementIds,
246
254
  groups,
255
+ trackPadding,
247
256
  );
248
257
 
249
258
  return {
@@ -305,8 +314,8 @@ export function useTimelineDisplayLayout(
305
314
  const displayTrackOrder = useDisplayTrackOrder(draggedClip, trackOrder);
306
315
  const displayRowHeights = useDisplayRowHeights(displayTrackOrder, rowGeometry);
307
316
  const displayRowGeometry = useMemo(
308
- () => createTimelineRowGeometry(displayTrackOrder, displayRowHeights),
309
- [displayTrackOrder, displayRowHeights],
317
+ () => createTimelineRowGeometry(displayTrackOrder, displayRowHeights, rowGeometry.padding),
318
+ [displayTrackOrder, displayRowHeights, rowGeometry.padding],
310
319
  );
311
320
  return {
312
321
  displayTrackOrder,
@@ -171,4 +171,36 @@ describe("useTimelineVirtualRows", () => {
171
171
 
172
172
  act(() => root.unmount());
173
173
  });
174
+
175
+ it("offsets the mounted range by the geometry's own top pad", () => {
176
+ const keys = Array.from({ length: 1_000 }, (_, index) => index);
177
+ const topPad = 200 * 48;
178
+ const geometry = createTimelineRowGeometry(
179
+ keys,
180
+ keys.map(() => 48),
181
+ { top: topPad },
182
+ );
183
+ const scroll = createScrollElement(RULER_H + topPad + 300 * 48);
184
+ const scrollRef = { current: scroll };
185
+ let rows: ReturnType<typeof useTimelineVirtualRows> = [];
186
+
187
+ function Probe() {
188
+ rows = useTimelineVirtualRows({
189
+ enabled: true,
190
+ scrollRef,
191
+ viewport: viewport(scroll),
192
+ rowGeometry: geometry,
193
+ sessionEpoch: 1,
194
+ pinnedRowKeys: [],
195
+ });
196
+ return null;
197
+ }
198
+
199
+ const root = createRoot(document.createElement("div"));
200
+ act(() => root.render(React.createElement(Probe)));
201
+ act(() => {});
202
+ expect(rows.some((row) => row.index === 300)).toBe(true);
203
+ expect(rows.some((row) => row.index === 500)).toBe(false);
204
+ act(() => root.unmount());
205
+ });
174
206
  });
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, type RefObject } from "react";
2
2
  import { defaultRangeExtractor, useVirtualizer, type Range } from "@tanstack/react-virtual";
3
3
  import { TIMELINE_VIEWPORT_BUDGETS } from "../lib/timelineViewportBudgets";
4
4
  import type { TimelineScrollViewportSnapshot } from "./useTimelineScrollViewport";
5
- import { RULER_H, TRACKS_TOP_PAD, type TimelineRowGeometry } from "./timelineLayout";
5
+ import { RULER_H, type TimelineRowGeometry } from "./timelineLayout";
6
6
 
7
7
  export interface TimelineVirtualRow {
8
8
  readonly index: number;
@@ -84,7 +84,7 @@ export function useTimelineVirtualRows({
84
84
  getItemKey,
85
85
  overscan: TIMELINE_VIEWPORT_BUDGETS.rowOverscanPerSide,
86
86
  rangeExtractor,
87
- scrollMargin: RULER_H + TRACKS_TOP_PAD,
87
+ scrollMargin: RULER_H + rowGeometry.padding.top,
88
88
  initialRect: { width: viewport.clientWidth, height: viewport.clientHeight },
89
89
  initialOffset,
90
90
  });