@hyperframes/studio 0.8.108 → 0.8.110
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-BakGew45.js → hyperframes-player-c6WR3RsF.js} +1 -1
- package/dist/assets/{index-C5dyUHwk.js → index-BJpQzV6h.js} +1 -1
- package/dist/assets/{index-CPe7d_wO.js → index-C0KfbPAo.js} +1 -1
- package/dist/assets/{index-ncVJ5IVu.css → index-C7apTvpr.css} +1 -1
- package/dist/assets/{index-C842tcVa.js → index-Co6W8IVk.js} +211 -208
- package/dist/{chunk-NCUBAVGK.js → chunk-5VSXRGIX.js} +4 -4
- package/dist/chunk-5VSXRGIX.js.map +1 -0
- package/dist/{chunk-AOWVEVEU.js → chunk-QUXZVHOJ.js} +8 -2
- package/dist/chunk-QUXZVHOJ.js.map +1 -0
- package/dist/index.d.ts +64 -9
- package/dist/index.html +2 -2
- package/dist/index.js +8216 -6268
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/dist/ui.js +1 -1
- package/package.json +8 -8
- package/src/App.tsx +7 -11
- package/src/components/EditorShell.tsx +4 -0
- package/src/components/LinkedSelectionToggle.tsx +73 -0
- package/src/components/TimelineToolbar.test.tsx +31 -0
- package/src/components/TimelineToolbar.tsx +2 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +51 -22
- package/src/components/editor/DomEditCropHandles.tsx +2 -1
- package/src/components/editor/InlineTextCaret.test.tsx +150 -0
- package/src/components/editor/InlineTextCaret.tsx +211 -0
- package/src/components/editor/InlineTextToolbar.test.tsx +34 -31
- package/src/components/editor/InlineTextToolbar.tsx +7 -5
- package/src/components/editor/PropertyPanelFlat.tsx +4 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +1 -0
- package/src/components/editor/commitPreflightHandles.test.tsx +4 -1
- package/src/components/editor/domEditInlineText.ts +6 -4
- package/src/components/editor/domEditInlineTextElement.test.ts +7 -0
- package/src/components/editor/domEditOverlayGestures.ts +10 -0
- package/src/components/editor/domEditOverlayStartGesture.test.ts +1 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +2 -0
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +39 -2
- package/src/components/editor/linkedSpeedEdits.test.tsx +164 -0
- package/src/components/editor/linkedSpeedEdits.ts +94 -0
- package/src/components/editor/marqueeCommit.test.tsx +31 -6
- package/src/components/editor/marqueeCommit.ts +16 -8
- package/src/components/editor/mediaAudioEdits.test.ts +20 -4
- package/src/components/editor/mediaAudioEdits.ts +27 -14
- package/src/components/editor/mediaLinkEdits.test.ts +334 -0
- package/src/components/editor/mediaLinkEdits.ts +303 -0
- package/src/components/editor/previewReadOnly.overlay.test.tsx +8 -1
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +4 -3
- package/src/components/editor/propertyPanelMediaSection.tsx +6 -1
- package/src/components/editor/shiftDragAxisLock.test.tsx +17 -3
- package/src/components/editor/useDomEditOverlayGestures.ts +3 -0
- package/src/components/editor/useInlineTextEditing.tsx +11 -4
- package/src/components/editor/useVolumeAutomation.ts +29 -7
- package/src/components/nle/useTimelineEditCallbacks.ts +10 -0
- package/src/components/ui/Menu.test.tsx +3 -22
- package/src/components/ui/Menu.tsx +2 -2
- package/src/components/ui/Popover.tsx +2 -2
- package/src/components/ui/Select.tsx +2 -2
- package/src/components/ui/Tooltip.tsx +2 -2
- package/src/components/ui/mountHost.testHelpers.ts +8 -0
- package/src/components/ui/valueControls.test.tsx +2 -21
- package/src/contexts/TimelineEditContext.tsx +4 -0
- package/src/editorGesturesExports.test.tsx +1 -0
- package/src/hooks/appHotkeysDispatch.ts +11 -1
- package/src/hooks/linkShortcuts.test.ts +70 -0
- package/src/hooks/linkShortcuts.ts +41 -0
- package/src/hooks/timelineElementsAttribute.test.ts +95 -0
- package/src/hooks/timelineElementsAttribute.ts +113 -0
- package/src/hooks/timelineLinkEditPlan.test.ts +141 -0
- package/src/hooks/timelineLinkEditPlan.ts +128 -0
- package/src/hooks/timelineMoveAdapter.ts +13 -0
- package/src/hooks/timelineTimingSync.test.ts +59 -0
- package/src/hooks/timelineTimingSync.ts +22 -3
- package/src/hooks/useAppHotkeys.test.ts +34 -0
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomGeometryCommits.test.tsx +8 -1
- package/src/hooks/useGsapScriptCommits.test.tsx +84 -2
- package/src/hooks/useGsapScriptCommits.ts +23 -3
- package/src/hooks/usePreviewPersistence.history.test.tsx +76 -0
- package/src/hooks/usePreviewPersistence.ts +15 -0
- package/src/hooks/useRazorSplit.test.tsx +48 -0
- package/src/hooks/useRazorSplit.ts +12 -1
- package/src/hooks/useRenderClipContent.test.ts +6 -4
- package/src/hooks/useRenderClipContent.ts +9 -8
- package/src/hooks/useTimelineDeleteOps.ts +10 -4
- package/src/hooks/useTimelineEditGuard.ts +54 -0
- package/src/hooks/useTimelineEditing.ts +49 -55
- package/src/hooks/useTimelineLinkEditing.test.tsx +124 -0
- package/src/hooks/useTimelineLinkEditing.ts +158 -0
- package/src/index.ts +5 -0
- package/src/player/components/AudioGainDialog.test.tsx +133 -0
- package/src/player/components/AudioGainDialog.tsx +226 -0
- package/src/player/components/AudioWaveform.test.tsx +30 -3
- package/src/player/components/AudioWaveform.tsx +6 -7
- package/src/player/components/ClipBadges.test.tsx +32 -8
- package/src/player/components/ClipBadges.tsx +37 -31
- package/src/player/components/ClipContextMenu.groups.test.tsx +102 -0
- package/src/player/components/ClipContextMenu.test.tsx +10 -4
- package/src/player/components/ClipContextMenu.tsx +115 -90
- package/src/player/components/ClipPeakMarks.tsx +2 -14
- package/src/player/components/OutOfSyncBadge.test.tsx +91 -0
- package/src/player/components/OutOfSyncBadge.tsx +119 -0
- package/src/player/components/Timeline.test.ts +15 -0
- package/src/player/components/TimelineClip.test.tsx +58 -0
- package/src/player/components/TimelineClip.tsx +9 -2
- package/src/player/components/TimelineLanes.test.tsx +45 -2
- package/src/player/components/TimelineLanes.tsx +0 -2
- package/src/player/components/TimelineOverlays.tsx +2 -0
- package/src/player/components/audioClipLink.test.ts +288 -31
- package/src/player/components/audioClipLink.ts +151 -27
- package/src/player/components/audioGainApply.test.ts +87 -0
- package/src/player/components/audioGainApply.ts +57 -0
- package/src/player/components/audioGainDialogStore.ts +36 -0
- package/src/player/components/audioGainPlan.test.ts +82 -0
- package/src/player/components/audioGainPlan.ts +53 -0
- package/src/player/components/clipMenuAudioItems.noShell.test.tsx +86 -0
- package/src/player/components/clipMenuAudioItems.test.tsx +59 -50
- package/src/player/components/clipMenuAudioItems.tsx +44 -57
- package/src/player/components/clipMenuDuck.ts +5 -0
- package/src/player/components/clipMenuLinkItems.test.ts +174 -0
- package/src/player/components/clipMenuLinkItems.tsx +166 -0
- package/src/player/components/clipMenuToolItems.test.tsx +130 -4
- package/src/player/components/clipMenuToolItems.tsx +80 -9
- package/src/player/components/clipPeakMap.ts +46 -0
- package/src/player/components/clipPeakRuns.test.ts +14 -1
- package/src/player/components/clipPeakRuns.ts +11 -0
- package/src/player/components/clipSync.test.ts +67 -0
- package/src/player/components/clipSync.ts +80 -0
- package/src/player/components/clipToolAttrs.test.ts +57 -41
- package/src/player/components/clipToolAttrs.ts +48 -45
- package/src/player/components/linkLabelColor.test.ts +20 -0
- package/src/player/components/linkLabelColor.ts +24 -0
- package/src/player/components/studioShortcuts.ts +2 -0
- package/src/player/components/timelineCallbacks.ts +17 -0
- package/src/player/components/timelineClipDragGestureLifecycle.ts +9 -0
- package/src/player/components/timelineClipDragPreview.test.ts +96 -0
- package/src/player/components/timelineClipDragPreview.ts +32 -7
- package/src/player/components/timelineClipDragTypes.ts +2 -0
- package/src/player/components/timelineClipGestureHandlers.ts +29 -31
- package/src/player/components/timelineEditing.test.ts +0 -13
- package/src/player/components/timelineEditing.ts +0 -11
- package/src/player/components/timelineLaneProps.ts +0 -5
- package/src/player/components/timelineLinkSelection.ts +41 -0
- package/src/player/components/timelineViewportGeometry.ts +3 -0
- package/src/player/components/useClipToolState.ts +0 -5
- package/src/player/components/useTimelineClipContextMenu.ts +2 -1
- package/src/player/components/useTimelineClipDrag.ts +28 -0
- package/src/player/components/useTimelineOverlaysState.ts +0 -3
- package/src/player/components/useTimelineProviderState.tsx +3 -2
- package/src/player/components/useTimelineRangeSelection.ts +0 -14
- package/src/player/components/useTimelineRowVirtualization.ts +2 -1
- package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
- package/src/player/hooks/timelinePlayerSync.ts +1 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +4 -1
- package/src/player/lib/timelineDOM.test.ts +31 -0
- package/src/player/lib/timelineDOM.ts +13 -0
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/components.css +27 -11
- package/src/styles/theme.css +0 -1
- package/src/utils/gsapSoftReload.test.ts +238 -2
- package/src/utils/gsapSoftReload.ts +89 -74
- package/src/utils/gsapUndoRestore.test.ts +4 -3
- package/src/utils/gsapUndoRestore.ts +15 -0
- package/src/utils/linkedClipPreferences.ts +34 -0
- package/src/utils/sdkCutover.linkedTiming.test.ts +52 -0
- package/src/utils/softReloadTargets.ts +128 -0
- package/src/utils/studioUiPreferences.ts +8 -0
- package/src/utils/typingTarget.test.ts +8 -0
- package/src/utils/typingTarget.ts +1 -0
- package/dist/chunk-AOWVEVEU.js.map +0 -1
- package/dist/chunk-NCUBAVGK.js.map +0 -1
|
@@ -27,6 +27,8 @@ import type {
|
|
|
27
27
|
BlockedClipState,
|
|
28
28
|
} from "./timelineClipDragTypes";
|
|
29
29
|
import { getTimelineElementIndexes } from "../lib/timelineElementIndexes";
|
|
30
|
+
import { dropMisalignedTrimPartners, linkedGestureKeys } from "./audioClipLink";
|
|
31
|
+
import { isLinkedSelectionOn } from "../../utils/linkedClipPreferences";
|
|
30
32
|
import type { TimelineRowGeometry } from "./timelineLayout";
|
|
31
33
|
import {
|
|
32
34
|
mountTimelineClipDragGestureLifecycle,
|
|
@@ -65,6 +67,7 @@ interface UseTimelineClipDragInput {
|
|
|
65
67
|
) => Promise<void> | void;
|
|
66
68
|
onResizeElements?: NonNullable<TimelineEditCallbacks["onResizeElements"]>;
|
|
67
69
|
onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedClipState["intent"]) => void;
|
|
70
|
+
onLinkEdit?: TimelineEditCallbacks["onLinkEdit"];
|
|
68
71
|
/** Seeks the preview; a trim shows the frame at its dragged edge. */
|
|
69
72
|
onSeek?: (time: number, options?: { keepPlaying?: boolean; follow?: boolean }) => void;
|
|
70
73
|
setShowPopover: (show: boolean) => void;
|
|
@@ -95,6 +98,7 @@ export function useTimelineClipDrag({
|
|
|
95
98
|
onResizeElement,
|
|
96
99
|
onResizeElements,
|
|
97
100
|
onBlockedEditAttempt,
|
|
101
|
+
onLinkEdit,
|
|
98
102
|
onSeek,
|
|
99
103
|
setShowPopover,
|
|
100
104
|
setRangeSelectionRef,
|
|
@@ -220,6 +224,13 @@ export function useTimelineClipDrag({
|
|
|
220
224
|
return;
|
|
221
225
|
}
|
|
222
226
|
beginGesture("drag", next.pointerId);
|
|
227
|
+
gestureSelectedKeysRef.current = linkedGestureKeys(
|
|
228
|
+
gestureSelectedKeysRef.current,
|
|
229
|
+
next.element,
|
|
230
|
+
elementsRef.current,
|
|
231
|
+
next.altKey === true,
|
|
232
|
+
isLinkedSelectionOn(),
|
|
233
|
+
);
|
|
223
234
|
publishDraggedClip(next);
|
|
224
235
|
},
|
|
225
236
|
[beginGesture, publishDraggedClip],
|
|
@@ -231,6 +242,18 @@ export function useTimelineClipDrag({
|
|
|
231
242
|
return;
|
|
232
243
|
}
|
|
233
244
|
beginGesture("resize", next.pointerId);
|
|
245
|
+
gestureSelectedKeysRef.current = dropMisalignedTrimPartners(
|
|
246
|
+
linkedGestureKeys(
|
|
247
|
+
gestureSelectedKeysRef.current,
|
|
248
|
+
next.element,
|
|
249
|
+
elementsRef.current,
|
|
250
|
+
next.altKey === true,
|
|
251
|
+
isLinkedSelectionOn(),
|
|
252
|
+
),
|
|
253
|
+
next.element,
|
|
254
|
+
elementsRef.current,
|
|
255
|
+
next.edge,
|
|
256
|
+
);
|
|
234
257
|
publishResizingClip(next);
|
|
235
258
|
},
|
|
236
259
|
[beginGesture, publishResizingClip],
|
|
@@ -249,6 +272,8 @@ export function useTimelineClipDrag({
|
|
|
249
272
|
onMoveElementsRef.current = onMoveElements;
|
|
250
273
|
const onBlockedEditAttemptRef = useRef(onBlockedEditAttempt);
|
|
251
274
|
onBlockedEditAttemptRef.current = onBlockedEditAttempt;
|
|
275
|
+
const onLinkEditRef = useRef(onLinkEdit);
|
|
276
|
+
onLinkEditRef.current = onLinkEdit;
|
|
252
277
|
const onResizeElementRef = useRef(onResizeElement);
|
|
253
278
|
onResizeElementRef.current = onResizeElement;
|
|
254
279
|
const onResizeElementsRef = useRef(onResizeElements);
|
|
@@ -304,6 +329,8 @@ export function useTimelineClipDrag({
|
|
|
304
329
|
scroll: scrollRef.current,
|
|
305
330
|
pps: ppsRef.current,
|
|
306
331
|
buildSnapTargets,
|
|
332
|
+
elements: elementsRef.current,
|
|
333
|
+
gestureKeys: gestureSelectedKeysRef.current,
|
|
307
334
|
});
|
|
308
335
|
trimSeekOriginRef.current ??= usePlayerStore.getState().currentTime;
|
|
309
336
|
const setResizeState = (v: ResizePreviewResult) => {
|
|
@@ -422,6 +449,7 @@ export function useTimelineClipDrag({
|
|
|
422
449
|
refreshAfterLaneMoveRef,
|
|
423
450
|
readZIndexRef,
|
|
424
451
|
onBlockedEditAttemptRef,
|
|
452
|
+
onLinkEditRef,
|
|
425
453
|
onResizeElementsRef,
|
|
426
454
|
onResizeElementRef,
|
|
427
455
|
onMoveElementsRef,
|
|
@@ -32,7 +32,6 @@ type OverlayStateInputs = Omit<
|
|
|
32
32
|
|
|
33
33
|
export type TimelineOverlaysStateResult = {
|
|
34
34
|
overlays: TimelineOverlaysState;
|
|
35
|
-
shiftClickClipRef: ReturnType<typeof useTimelineRangeSelection>["shiftClickClipRef"];
|
|
36
35
|
marqueeRect: ReturnType<typeof useTimelineRangeSelection>["marqueeRect"];
|
|
37
36
|
isScrubbing: ReturnType<typeof useTimelineRangeSelection>["isScrubbing"];
|
|
38
37
|
handlePointerDown: ReturnType<typeof useTimelineRangeSelection>["handlePointerDown"];
|
|
@@ -55,7 +54,6 @@ export function useTimelineOverlaysState({
|
|
|
55
54
|
const {
|
|
56
55
|
rangeSelection,
|
|
57
56
|
setRangeSelection,
|
|
58
|
-
shiftClickClipRef,
|
|
59
57
|
marqueeRect,
|
|
60
58
|
isScrubbing,
|
|
61
59
|
handlePointerDown,
|
|
@@ -86,7 +84,6 @@ export function useTimelineOverlaysState({
|
|
|
86
84
|
onCloseAllTrackGaps: gap.closeAllTrackGaps,
|
|
87
85
|
onHoverGapAction: gap.setHoveredGapAction,
|
|
88
86
|
},
|
|
89
|
-
shiftClickClipRef,
|
|
90
87
|
marqueeRect,
|
|
91
88
|
isScrubbing,
|
|
92
89
|
handlePointerDown,
|
|
@@ -86,6 +86,7 @@ export function useTimelineProviderState({
|
|
|
86
86
|
onResizeElement,
|
|
87
87
|
onResizeElements,
|
|
88
88
|
onBlockedEditAttempt,
|
|
89
|
+
onLinkEdit,
|
|
89
90
|
onSplitElement,
|
|
90
91
|
onRazorSplitAll,
|
|
91
92
|
onDeleteKeyframe,
|
|
@@ -105,6 +106,7 @@ export function useTimelineProviderState({
|
|
|
105
106
|
const readOnlyPress = useTimelineReadOnlyPress();
|
|
106
107
|
const oneShotEdits = refuseWhenReadOnly(readOnlyPress, {
|
|
107
108
|
onBlockedEditAttempt,
|
|
109
|
+
onLinkEdit,
|
|
108
110
|
onRazorSplitAll,
|
|
109
111
|
onToggleTrackHidden: editContext.onToggleTrackHidden,
|
|
110
112
|
onTogglePropertyGroupKeyframe: editContext.onTogglePropertyGroupKeyframe,
|
|
@@ -217,6 +219,7 @@ export function useTimelineProviderState({
|
|
|
217
219
|
onResizeElement: pinnedOnResizeElement,
|
|
218
220
|
onResizeElements: pinnedOnResizeElements,
|
|
219
221
|
onBlockedEditAttempt: oneShotEdits.onBlockedEditAttempt,
|
|
222
|
+
onLinkEdit: oneShotEdits.onLinkEdit,
|
|
220
223
|
onSeek,
|
|
221
224
|
setShowPopover,
|
|
222
225
|
setRangeSelectionRef,
|
|
@@ -417,7 +420,6 @@ export function useTimelineProviderState({
|
|
|
417
420
|
gapHighlight,
|
|
418
421
|
openGapMenu,
|
|
419
422
|
onContextMenuClip,
|
|
420
|
-
shiftClickClipRef,
|
|
421
423
|
marqueeRect,
|
|
422
424
|
isScrubbing,
|
|
423
425
|
handlePointerDown,
|
|
@@ -500,7 +502,6 @@ export function useTimelineProviderState({
|
|
|
500
502
|
setResizingClip,
|
|
501
503
|
setDraggedClip,
|
|
502
504
|
setSelectedElementId,
|
|
503
|
-
shiftClickClipRef,
|
|
504
505
|
getPreviewElement,
|
|
505
506
|
getTrackStyle,
|
|
506
507
|
keyframeCache,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useRef, useState, useCallback, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
|
-
buildClipRangeSelection,
|
|
4
3
|
applyTimelineAutoScrollStep,
|
|
5
4
|
resolveTimelineAutoScrollLoopAction,
|
|
6
5
|
type TimelineRangeSelection,
|
|
@@ -149,11 +148,6 @@ export function useTimelineRangeSelection({
|
|
|
149
148
|
// rendering). Drives the playhead head's filled-vs-hollow state.
|
|
150
149
|
const [isScrubbing, setIsScrubbing] = useState(false);
|
|
151
150
|
const [rangeSelection, setRangeSelection] = useState<TimelineRangeSelection | null>(null);
|
|
152
|
-
const shiftClickClipRef = useRef<{
|
|
153
|
-
element: TimelineElement;
|
|
154
|
-
anchorX: number;
|
|
155
|
-
anchorY: number;
|
|
156
|
-
} | null>(null);
|
|
157
151
|
|
|
158
152
|
const seekRafRef = useRef(0);
|
|
159
153
|
const pendingClientXRef = useRef(0);
|
|
@@ -322,7 +316,6 @@ export function useTimelineRangeSelection({
|
|
|
322
316
|
beginRangeSelection(e);
|
|
323
317
|
return;
|
|
324
318
|
}
|
|
325
|
-
shiftClickClipRef.current = null;
|
|
326
319
|
if ((e.target as HTMLElement).closest("[data-clip]")) return;
|
|
327
320
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
328
321
|
activePointerIdRef.current = e.pointerId;
|
|
@@ -427,13 +420,7 @@ export function useTimelineRangeSelection({
|
|
|
427
420
|
// clip range), otherwise clear it.
|
|
428
421
|
const finishRangeSelection = useCallback(() => {
|
|
429
422
|
isRangeSelecting.current = false;
|
|
430
|
-
const pendingShiftClick = shiftClickClipRef.current;
|
|
431
|
-
shiftClickClipRef.current = null;
|
|
432
423
|
setRangeSelection((prev) => {
|
|
433
|
-
if (prev && pendingShiftClick && Math.abs(prev.end - prev.start) <= 0.2) {
|
|
434
|
-
setShowPopover(true);
|
|
435
|
-
return buildClipRangeSelection(pendingShiftClick.element, pendingShiftClick);
|
|
436
|
-
}
|
|
437
424
|
if (prev && Math.abs(prev.end - prev.start) > 0.2) {
|
|
438
425
|
setShowPopover(true);
|
|
439
426
|
return prev;
|
|
@@ -589,7 +576,6 @@ export function useTimelineRangeSelection({
|
|
|
589
576
|
return {
|
|
590
577
|
rangeSelection,
|
|
591
578
|
setRangeSelection,
|
|
592
|
-
shiftClickClipRef,
|
|
593
579
|
marqueeRect,
|
|
594
580
|
isScrubbing,
|
|
595
581
|
handlePointerDown,
|
|
@@ -120,6 +120,7 @@ export function useTimelineRowVirtualization({
|
|
|
120
120
|
previousGeometry,
|
|
121
121
|
rowGeometry,
|
|
122
122
|
scroll.scrollTop,
|
|
123
|
+
draggedRowKey !== undefined,
|
|
123
124
|
);
|
|
124
125
|
if (nextScrollTop !== scroll.scrollTop) {
|
|
125
126
|
scroll.scrollTop = nextScrollTop;
|
|
@@ -127,7 +128,7 @@ export function useTimelineRowVirtualization({
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
previousLayoutRef.current = rowGeometry;
|
|
130
|
-
}, [lastScrollLeftRef, rowGeometry, scrollRef, sessionEpoch, syncScrollViewport]);
|
|
131
|
+
}, [draggedRowKey, lastScrollLeftRef, rowGeometry, scrollRef, sessionEpoch, syncScrollViewport]);
|
|
131
132
|
|
|
132
133
|
return {
|
|
133
134
|
enabled,
|
|
@@ -13,9 +13,15 @@ describe("timelineElementsChanged", () => {
|
|
|
13
13
|
["audio group hidden", { audioGroupHidden: true }],
|
|
14
14
|
["fade in", { fadeIn: 1 }],
|
|
15
15
|
["fade out", { fadeOut: 1 }],
|
|
16
|
+
["link", { link: "lk-1" }],
|
|
16
17
|
["source (re-pointed at its preview copy)", { src: "clip1.mp4?hf-proxy=h264" }],
|
|
17
18
|
])("sees a clip whose %s changed with no timing change", (_name, change) => {
|
|
18
19
|
expect(timelineElementsChanged([clip], [{ ...clip, ...change }])).toBe(true);
|
|
19
20
|
expect(timelineElementsChanged([{ ...clip, ...change }], [{ ...clip, ...change }])).toBe(false);
|
|
20
21
|
});
|
|
22
|
+
|
|
23
|
+
it("sees an unlink: the same clips re-derived without their link", () => {
|
|
24
|
+
const linked = { ...clip, link: "lk-1" };
|
|
25
|
+
expect(timelineElementsChanged([linked], [clip])).toBe(true);
|
|
26
|
+
});
|
|
21
27
|
});
|
|
@@ -15,6 +15,7 @@ import type { PlaybackAdapter, IframeWindow } from "../lib/playbackTypes";
|
|
|
15
15
|
import { readTimelineDurationFromDocument } from "../lib/timelineDOM";
|
|
16
16
|
import { buildMissingCompositionElements } from "../lib/timelineIframeHelpers";
|
|
17
17
|
import { acceptedRuntimeMessageFps } from "../lib/runtimeProtocol";
|
|
18
|
+
import { useLinkedClipPreferences } from "../../utils/linkedClipPreferences";
|
|
18
19
|
import {
|
|
19
20
|
buildTimelineElementsFromClips,
|
|
20
21
|
syncManifestTimeline,
|
|
@@ -196,8 +197,10 @@ export function useTimelineSyncCallbacks({
|
|
|
196
197
|
// at the authored root `data-duration` so a runtime that measures only the
|
|
197
198
|
// furthest clip end (shorter than the authored window) can't leave a stale,
|
|
198
199
|
// too-short total in the transport (the "0:44/0:40" bug).
|
|
200
|
+
const fps = acceptedRuntimeMessageFps(data);
|
|
201
|
+
useLinkedClipPreferences.getState().setCompositionFps(fps);
|
|
199
202
|
const newDuration = resolveTimelineTotalDuration({
|
|
200
|
-
manifestDurationSeconds: data.durationInFrames /
|
|
203
|
+
manifestDurationSeconds: data.durationInFrames / fps,
|
|
201
204
|
authoredRootDurationSeconds: readTimelineDurationFromDocument(iframeDoc),
|
|
202
205
|
});
|
|
203
206
|
syncManifestTimeline(
|
|
@@ -281,6 +281,37 @@ describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
|
|
|
281
281
|
|
|
282
282
|
expect(element.hidden).toBe(true);
|
|
283
283
|
});
|
|
284
|
+
|
|
285
|
+
it("reads data-link and data-sync-origin on both the DOM and manifest paths", () => {
|
|
286
|
+
const doc = makeDoc(`
|
|
287
|
+
<div data-composition-id="root">
|
|
288
|
+
<video id="talk" class="clip" src="t.mp4" muted data-link="lk-1" data-sync-origin="lk-1" data-start="0" data-duration="5"></video>
|
|
289
|
+
</div>
|
|
290
|
+
`);
|
|
291
|
+
const fromDom = parseTimelineFromDOM(doc, 10).find((el) => el.domId === "talk");
|
|
292
|
+
expect(fromDom?.link).toBe("lk-1");
|
|
293
|
+
expect(fromDom?.syncOrigin).toBe("lk-1");
|
|
294
|
+
const element = createTimelineElementFromManifestClip({
|
|
295
|
+
clip: {
|
|
296
|
+
id: "talk",
|
|
297
|
+
label: "Talk",
|
|
298
|
+
kind: "element",
|
|
299
|
+
tagName: "video",
|
|
300
|
+
start: 0,
|
|
301
|
+
duration: 5,
|
|
302
|
+
track: 0,
|
|
303
|
+
compositionId: null,
|
|
304
|
+
parentCompositionId: null,
|
|
305
|
+
compositionSrc: null,
|
|
306
|
+
assetUrl: null,
|
|
307
|
+
},
|
|
308
|
+
fallbackIndex: 0,
|
|
309
|
+
doc,
|
|
310
|
+
hostEl: doc.getElementById("talk"),
|
|
311
|
+
});
|
|
312
|
+
expect(element.link).toBe("lk-1");
|
|
313
|
+
expect(element.syncOrigin).toBe("lk-1");
|
|
314
|
+
});
|
|
284
315
|
});
|
|
285
316
|
|
|
286
317
|
describe("group info cache", () => {
|
|
@@ -12,6 +12,7 @@ import type { TimelineElement } from "../store/playerStore";
|
|
|
12
12
|
import type { ClipManifestClip, IframeWindow, TimelineLike } from "./playbackTypes";
|
|
13
13
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
14
14
|
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
|
15
|
+
import { linkScopeOf } from "@hyperframes/core/media-link";
|
|
15
16
|
import { createRuntimeStartTimeResolver } from "@hyperframes/core/runtime/start-resolver";
|
|
16
17
|
import { groupInfoFor } from "./timelineGroupInfo";
|
|
17
18
|
import { transitionLabelsForDocument } from "./timelineTransitionMetadata";
|
|
@@ -150,6 +151,12 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
150
151
|
if (hostEl.hasAttribute("data-hidden")) entry.hidden = true;
|
|
151
152
|
const timelineRole = hostEl.getAttribute("data-timeline-role");
|
|
152
153
|
if (timelineRole) entry.timelineRole = timelineRole;
|
|
154
|
+
const link = hostEl.getAttribute("data-link");
|
|
155
|
+
if (link) entry.link = link;
|
|
156
|
+
const compositionScope = linkScopeOf(hostEl)?.getAttribute("data-composition-id");
|
|
157
|
+
if (compositionScope) entry.compositionScope = compositionScope;
|
|
158
|
+
const syncOrigin = hostEl.getAttribute("data-sync-origin");
|
|
159
|
+
if (syncOrigin) entry.syncOrigin = syncOrigin;
|
|
153
160
|
const audioGroup = hostEl.getAttribute("data-audio-group");
|
|
154
161
|
if (audioGroup) {
|
|
155
162
|
entry.audioGroup = audioGroup;
|
|
@@ -337,6 +344,12 @@ export function parseTimelineFromDOM(
|
|
|
337
344
|
|
|
338
345
|
const timelineRole = el.getAttribute("data-timeline-role");
|
|
339
346
|
if (timelineRole) entry.timelineRole = timelineRole;
|
|
347
|
+
const domLink = el.getAttribute("data-link");
|
|
348
|
+
if (domLink) entry.link = domLink;
|
|
349
|
+
const domCompositionScope = linkScopeOf(el)?.getAttribute("data-composition-id");
|
|
350
|
+
if (domCompositionScope) entry.compositionScope = domCompositionScope;
|
|
351
|
+
const domSyncOrigin = el.getAttribute("data-sync-origin");
|
|
352
|
+
if (domSyncOrigin) entry.syncOrigin = domSyncOrigin;
|
|
340
353
|
|
|
341
354
|
const domAudioGroup = el.getAttribute("data-audio-group");
|
|
342
355
|
if (domAudioGroup) {
|
|
@@ -83,6 +83,9 @@ export interface TimelineElement {
|
|
|
83
83
|
/** The owning group's serialized `data-fx-chain`, when set — resolved once per parse. */
|
|
84
84
|
audioGroupFxChain?: string;
|
|
85
85
|
audioGroupAutomation?: string;
|
|
86
|
+
link?: string;
|
|
87
|
+
compositionScope?: string;
|
|
88
|
+
syncOrigin?: string;
|
|
86
89
|
/**
|
|
87
90
|
* Master start of the composition this row runs in, which its tweens and its
|
|
88
91
|
* `data-start` are local to; 0 at the root. Writes go through toAuthoredStart.
|
|
@@ -116,6 +119,15 @@ export function toCompositionTime(element: RowClock, masterTime: number): number
|
|
|
116
119
|
return masterTime - (element.parentCompositionStart ?? 0);
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
type CompositionScoped = Pick<TimelineElement, "sourceFile" | "compositionScope">;
|
|
123
|
+
|
|
124
|
+
export function sameCompositionScope(a: CompositionScoped, b: CompositionScoped): boolean {
|
|
125
|
+
return (
|
|
126
|
+
(a.sourceFile ?? "") === (b.sourceFile ?? "") &&
|
|
127
|
+
(a.compositionScope ?? "") === (b.compositionScope ?? "")
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
/**
|
|
120
132
|
* The fields `updateElement` may write.
|
|
121
133
|
*
|
|
@@ -296,17 +296,6 @@
|
|
|
296
296
|
transition: none;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
.timeline-audio-link {
|
|
300
|
-
position: absolute;
|
|
301
|
-
z-index: 4;
|
|
302
|
-
top: -9px;
|
|
303
|
-
left: 50%;
|
|
304
|
-
width: 1px;
|
|
305
|
-
height: 10px;
|
|
306
|
-
background: var(--timeline-audio-link);
|
|
307
|
-
pointer-events: none;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
299
|
.timeline-clip.is-hovered {
|
|
311
300
|
background-color: var(--clip-bg-hover);
|
|
312
301
|
}
|
|
@@ -362,6 +351,16 @@
|
|
|
362
351
|
white-space: nowrap;
|
|
363
352
|
}
|
|
364
353
|
|
|
354
|
+
/* Link groups share a label colour on the border and the name chip. */
|
|
355
|
+
.timeline-clip[data-link-color] {
|
|
356
|
+
border-color: var(--clip-link-color);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.timeline-clip[data-link-color] .timeline-clip__label {
|
|
360
|
+
box-shadow: inset 3px 0 0 var(--clip-link-color);
|
|
361
|
+
padding-left: 8px;
|
|
362
|
+
}
|
|
363
|
+
|
|
365
364
|
.timeline-clip.is-audio .timeline-clip__label {
|
|
366
365
|
top: 11px;
|
|
367
366
|
left: 9px;
|
|
@@ -517,6 +516,23 @@
|
|
|
517
516
|
transition: transform 160ms ease;
|
|
518
517
|
}
|
|
519
518
|
|
|
519
|
+
/* The caret of a text edited in place (InlineTextCaret): lit half the time, like the system caret */
|
|
520
|
+
.hf-inline-text-caret {
|
|
521
|
+
animation: hf-inline-text-caret-blink 1.06s step-end infinite;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@keyframes hf-inline-text-caret-blink {
|
|
525
|
+
50% {
|
|
526
|
+
opacity: 0;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
@media (prefers-reduced-motion: reduce) {
|
|
531
|
+
.hf-inline-text-caret {
|
|
532
|
+
animation: none;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
520
536
|
/* Toast enter/exit (StudioToast) */
|
|
521
537
|
@keyframes hf-toast-in {
|
|
522
538
|
from {
|
package/src/styles/theme.css
CHANGED
|
@@ -117,7 +117,6 @@
|
|
|
117
117
|
--timeline-clip-audio-bg-hover: oklch(0.49 0.12 300);
|
|
118
118
|
--timeline-clip-audio-bg-dragging: oklch(0.42 0.12 300 / 0.96);
|
|
119
119
|
--timeline-clip-audio-muted-bg: rgb(72, 74, 78);
|
|
120
|
-
--timeline-audio-link: light-dark(#626d66, #a0aaa4);
|
|
121
120
|
--timeline-clip-selection: light-dark(#0f766e, rgba(255, 255, 255, 0.85));
|
|
122
121
|
--timeline-clip-selection-highlight: rgb(255 255 255 / 0.55);
|
|
123
122
|
--timeline-clip-radius: 8px;
|