@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.
- package/dist/assets/{hyperframes-player-RS9ROX_S.js → hyperframes-player-NSdcoOcK.js} +1 -1
- package/dist/assets/{index-CquRJgm-.js → index-CCGnc_0M.js} +1 -1
- package/dist/assets/index-D78KEjgB.css +1 -0
- package/dist/assets/index-Dp1zVb9X.js +428 -0
- package/dist/assets/{index-Bi-CwQDX.js → index-wS6f7F5q.js} +1 -1
- package/dist/index.d.ts +40 -36
- package/dist/index.html +2 -2
- package/dist/index.js +9598 -6798
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/App.tsx +14 -14
- package/src/components/DesignPanelPromoteProvider.tsx +1 -1
- package/src/components/ExternalFileConflictBanner.test.tsx +142 -0
- package/src/components/ExternalFileConflictBanner.tsx +255 -0
- package/src/components/StudioRightPanel.tsx +1 -1
- package/src/components/editor/GsapAnimationList.tsx +84 -0
- package/src/components/editor/GsapAnimationSection.test.tsx +100 -11
- package/src/components/editor/GsapAnimationSection.tsx +11 -41
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/PropertyPanelFlat.tsx +29 -26
- package/src/components/editor/gsapAnimationCallbacks.ts +0 -10
- package/src/components/editor/propertyPanel3dTransform.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +37 -2
- package/src/components/editor/propertyPanelFlatDescriptors.ts +20 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +56 -55
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +9 -37
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +69 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelTransformCommit.test.ts +57 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +14 -14
- package/src/components/editor/propertyPanelTypes.ts +10 -4
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +65 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +66 -10
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +78 -4
- package/src/components/nle/useTimelineEditCallbacks.ts +12 -7
- package/src/components/renders/useRenderQueue.ts +16 -3
- package/src/components/renders/useRenderQueueTelemetry.test.tsx +104 -0
- package/src/components/sidebar/AssetCard.tsx +4 -3
- package/src/components/sidebar/AudioRow.tsx +4 -3
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/contexts/FileManagerContext.tsx +12 -0
- package/src/hooks/externalFileChangeOwnership.test.ts +17 -0
- package/src/hooks/externalFileReloadBus.test.ts +34 -0
- package/src/hooks/externalFileReloadBus.ts +18 -0
- package/src/hooks/gsapDragCommit.test.ts +39 -1
- package/src/hooks/gsapDragCommit.ts +12 -40
- package/src/hooks/gsapEditOutcome.ts +72 -0
- package/src/hooks/gsapResizeIntercept.test.ts +90 -2
- package/src/hooks/gsapResizeIntercept.ts +44 -16
- package/src/hooks/gsapRuntimeBridge.test.ts +195 -27
- package/src/hooks/gsapRuntimeBridge.ts +93 -28
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +194 -24
- package/src/hooks/useAnimatedPropertyCommit.ts +101 -49
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +215 -0
- package/src/hooks/useDomEditSession.test.tsx +1 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +61 -0
- package/src/hooks/useDomGeometryCommits.ts +20 -0
- package/src/hooks/useEditorSave.test.tsx +131 -0
- package/src/hooks/useEditorSave.ts +113 -39
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +221 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +429 -0
- package/src/hooks/useFileManager.projectOwnership.test.tsx +139 -7
- package/src/hooks/useFileManager.ts +32 -3
- package/src/hooks/useGsapAnimationFetchFallback.ts +45 -24
- package/src/hooks/useGsapAwareEditing.test.tsx +125 -39
- package/src/hooks/useGsapAwareEditing.ts +88 -27
- package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +42 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
- package/src/hooks/useGsapKeyframeOps.ts +20 -9
- package/src/hooks/usePreviewPersistence.test.ts +23 -0
- package/src/hooks/usePreviewPersistence.ts +22 -58
- package/src/hooks/useSdkSession.ts +8 -52
- package/src/hooks/useStudioExternalFileChanges.ts +81 -0
- package/src/hooks/useStudioSdkSessions.ts +2 -7
- package/src/hooks/useStudioSessionStart.ts +15 -0
- package/src/hooks/useStudioTestHooks.test.tsx +11 -7
- package/src/player/components/BeatStrip.test.tsx +374 -0
- package/src/player/components/BeatStrip.tsx +295 -37
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +16 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +6 -5
- package/src/player/components/LayerDisclosureRow.tsx +2 -0
- package/src/player/components/Timeline.test.ts +26 -8
- package/src/player/components/Timeline.tsx +87 -88
- package/src/player/components/Timeline.virtualization.test.tsx +54 -11
- package/src/player/components/TimelineCanvas.tsx +14 -71
- package/src/player/components/TimelineClip.test.tsx +17 -2
- package/src/player/components/TimelineClip.tsx +20 -5
- package/src/player/components/TimelineClipDiamonds.test.tsx +536 -27
- package/src/player/components/TimelineClipDiamonds.tsx +118 -223
- package/src/player/components/TimelineCompactDiamonds.tsx +84 -0
- package/src/player/components/TimelineDiamondConnectors.tsx +31 -3
- package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
- package/src/player/components/TimelineGestureOverlay.tsx +96 -0
- package/src/player/components/TimelineLanes.test.tsx +64 -7
- package/src/player/components/TimelineLanes.tsx +84 -66
- package/src/player/components/TimelineOverlays.test.ts +161 -0
- package/src/player/components/TimelineOverlays.tsx +104 -11
- package/src/player/components/TimelinePropertyLanes.test.tsx +9 -5
- package/src/player/components/TimelinePropertyLanes.tsx +9 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -0
- package/src/player/components/TimelineTrackHeader.tsx +16 -0
- package/src/player/components/TimelineTrackRow.tsx +58 -7
- package/src/player/components/timelineCallbacks.ts +1 -1
- package/src/player/components/timelineClipDragGestureLifecycle.test.ts +18 -3
- package/src/player/components/timelineClipDragGestureLifecycle.ts +199 -51
- package/src/player/components/timelineClipDragPreview.test.ts +33 -0
- package/src/player/components/timelineClipDragPreview.ts +18 -12
- package/src/player/components/timelineClipDragTypes.ts +12 -0
- package/src/player/components/timelineDiamondTypes.ts +18 -21
- package/src/player/components/timelineDragDrop.test.tsx +206 -0
- package/src/player/components/timelineDragDrop.ts +152 -30
- package/src/player/components/timelineKeyboardNavigation.test.ts +303 -0
- package/src/player/components/timelineKeyboardNavigation.ts +337 -0
- package/src/player/components/timelineLaneProps.ts +3 -1
- package/src/player/components/timelineMarquee.test.ts +73 -25
- package/src/player/components/timelineMarquee.ts +44 -16
- package/src/player/components/timelineNavigationIdentity.ts +38 -0
- package/src/player/components/timelineTestViewport.ts +13 -0
- package/src/player/components/timelineViewModel.ts +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +219 -17
- package/src/player/components/useTimelineClipDrag.ts +112 -70
- package/src/player/components/useTimelineClipRenderWindow.ts +8 -39
- package/src/player/components/useTimelineFocusCoordinator.test.tsx +174 -0
- package/src/player/components/useTimelineFocusCoordinator.ts +221 -0
- package/src/player/components/useTimelineKeyboardActor.test.tsx +267 -0
- package/src/player/components/useTimelineKeyboardActor.ts +147 -0
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +43 -7
- package/src/player/components/useTimelineKeyframeHandlers.ts +465 -2
- package/src/player/components/useTimelineLaneMoveRefresh.ts +7 -0
- package/src/player/components/useTimelineLogicalFocus.ts +80 -0
- package/src/player/components/useTimelineLogicalRows.test.tsx +57 -0
- package/src/player/components/useTimelineLogicalRows.ts +47 -0
- package/src/player/components/useTimelineRangeSelection.test.tsx +204 -0
- package/src/player/components/useTimelineRangeSelection.ts +202 -69
- package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +3 -2
- package/src/player/components/useTimelineRowVirtualization.ts +26 -29
- package/src/player/components/useTrackGapMenu.test.tsx +64 -0
- package/src/player/components/useTrackGapMenu.ts +32 -9
- package/src/player/store/keyframeSlice.ts +59 -18
- package/src/player/store/liveTime.ts +11 -0
- package/src/player/store/playerStore.test.ts +106 -23
- package/src/player/store/playerStore.ts +43 -40
- package/src/player/store/timelineFocusState.ts +15 -0
- package/src/telemetry/canary.test.ts +325 -0
- package/src/telemetry/canary.ts +268 -0
- package/src/telemetry/client.test.ts +8 -3
- package/src/telemetry/client.ts +13 -43
- package/src/telemetry/config.ts +16 -2
- package/src/telemetry/distinctId.test.ts +35 -0
- package/src/telemetry/distinctId.ts +11 -2
- package/src/telemetry/policy.test.ts +81 -0
- package/src/telemetry/policy.ts +104 -0
- package/src/utils/domEditSaveQueue.test.ts +37 -1
- package/src/utils/domEditSaveQueue.ts +22 -4
- package/src/utils/externalConflictStorage.test.ts +95 -0
- package/src/utils/externalConflictStorage.ts +172 -0
- package/src/utils/studioFileVersion.test.ts +28 -1
- package/src/utils/studioFileVersion.ts +30 -0
- package/src/utils/studioPendingEdits.test.ts +108 -1
- package/src/utils/studioPendingEdits.ts +39 -2
- package/src/utils/studioSaveDiagnostics.ts +5 -0
- package/src/utils/studioTelemetry.test.ts +76 -0
- package/src/utils/studioTelemetry.ts +14 -6
- package/dist/assets/index-BXkzp_OU.css +0 -1
- package/dist/assets/index-Cql15Yur.js +0 -428
- package/src/player/components/useTimelineRevealClip.test.tsx +0 -139
- package/src/player/components/useTimelineRevealClip.ts +0 -180
|
@@ -12,7 +12,7 @@ import { TimelineEmptyState } from "./TimelineEmptyState";
|
|
|
12
12
|
import { TimelineCanvas } from "./TimelineCanvas";
|
|
13
13
|
import { type KeyframeDiamondContextMenuState } from "./KeyframeDiamondContextMenu";
|
|
14
14
|
import { useTimelineClipDrag } from "./useTimelineClipDrag";
|
|
15
|
-
import { TimelineOverlays } from "./TimelineOverlays";
|
|
15
|
+
import { TimelineOverlays, type ClipContextMenuState } from "./TimelineOverlays";
|
|
16
16
|
import { useTimelineEditPinning } from "./useTimelineEditPinning";
|
|
17
17
|
import { useTimelineStackingSync } from "./useTimelineStackingSync";
|
|
18
18
|
import { useTimelineGeometry } from "./useTimelineGeometry";
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
|
|
30
30
|
import { useTrackGapMenu } from "./useTrackGapMenu";
|
|
31
31
|
import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
|
|
32
|
-
import { useStudioPlaybackContextOptional } from "../../contexts/StudioContext";
|
|
33
32
|
import { TimelineRazorGuide, useTimelineRazorInteraction } from "./TimelineRazorInteraction";
|
|
34
33
|
import { useTimelinePerformanceTelemetry } from "./useTimelinePerformanceTelemetry";
|
|
35
34
|
import {
|
|
@@ -42,9 +41,10 @@ import { useTimelineShiftModifier } from "./useTimelineShiftModifier";
|
|
|
42
41
|
import { useTimelineTicks } from "./useTimelineTicks";
|
|
43
42
|
import { getTimelineElementIndexes } from "../lib/timelineElementIndexes";
|
|
44
43
|
import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
|
|
45
|
-
import { useTimelineRowVirtualization } from "./useTimelineRowVirtualization";
|
|
46
44
|
import { useTimelineClipRenderWindow } from "./useTimelineClipRenderWindow";
|
|
47
45
|
import { useTimelineActiveClips } from "./useTimelineActiveClips";
|
|
46
|
+
import { useTimelineLaneMoveRefresh } from "./useTimelineLaneMoveRefresh";
|
|
47
|
+
import { useTimelineLogicalFocus } from "./useTimelineLogicalFocus";
|
|
48
48
|
|
|
49
49
|
export {
|
|
50
50
|
shouldAutoScrollTimeline,
|
|
@@ -106,11 +106,7 @@ export const Timeline = memo(function Timeline({
|
|
|
106
106
|
onSplitElement: onSplitElementOverride,
|
|
107
107
|
});
|
|
108
108
|
const theme = useMemo(() => ({ ...defaultTimelineTheme, ...themeOverrides }), [themeOverrides]);
|
|
109
|
-
const
|
|
110
|
-
const setRefreshKey = playbackContext?.setRefreshKey;
|
|
111
|
-
const refreshAfterLaneMove = useCallback(() => {
|
|
112
|
-
setRefreshKey?.((key) => key + 1);
|
|
113
|
-
}, [setRefreshKey]);
|
|
109
|
+
const refreshAfterLaneMove = useTimelineLaneMoveRefresh();
|
|
114
110
|
useMusicBeatAnalysis();
|
|
115
111
|
const rawElements = usePlayerStore((s) => s.elements);
|
|
116
112
|
const expandedElements = useExpandedTimelineElements();
|
|
@@ -126,17 +122,10 @@ export const Timeline = memo(function Timeline({
|
|
|
126
122
|
const timelineReady = usePlayerStore((s) => s.timelineReady);
|
|
127
123
|
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
128
124
|
const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
|
|
129
|
-
const clipRevealRequest = usePlayerStore((s) => s.clipRevealRequest);
|
|
130
125
|
const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
|
|
131
126
|
const gsapAnimations = usePlayerStore((s) => s.gsapAnimations);
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
[gsapAnimations],
|
|
135
|
-
);
|
|
136
|
-
const labelMode = hasKeyframedClips;
|
|
137
|
-
// Without the label column the pre-t=0 breathing room is still TRACKS_LEFT_PAD
|
|
138
|
-
// (dropping it would jam clip 0 against the gutter on every non-keyframed
|
|
139
|
-
// composition); in label mode the 232px label column already provides it.
|
|
127
|
+
const labelMode = useMemo(() => hasKeyframedTimelineClips(gsapAnimations), [gsapAnimations]);
|
|
128
|
+
// The label column provides pre-t=0 space; otherwise keep TRACKS_LEFT_PAD after the gutter.
|
|
140
129
|
const contentOrigin = labelMode ? LABEL_COL_W + GUTTER : GUTTER + TRACKS_LEFT_PAD;
|
|
141
130
|
const contentGutter = labelMode ? GUTTER : 0;
|
|
142
131
|
const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
|
|
@@ -151,11 +140,7 @@ export const Timeline = memo(function Timeline({
|
|
|
151
140
|
const shiftHeld = useTimelineShiftModifier();
|
|
152
141
|
const [showPopover, setShowPopover] = useState(false);
|
|
153
142
|
const [kfContextMenu, setKfContextMenu] = useState<KeyframeDiamondContextMenuState | null>(null);
|
|
154
|
-
const [clipContextMenu, setClipContextMenu] = useState<
|
|
155
|
-
x: number;
|
|
156
|
-
y: number;
|
|
157
|
-
element: TimelineElement;
|
|
158
|
-
} | null>(null);
|
|
143
|
+
const [clipContextMenu, setClipContextMenu] = useState<ClipContextMenuState | null>(null);
|
|
159
144
|
const setContainerRef = useCallback((el: HTMLDivElement | null) => {
|
|
160
145
|
containerRef.current = el;
|
|
161
146
|
}, []);
|
|
@@ -185,7 +170,6 @@ export const Timeline = memo(function Timeline({
|
|
|
185
170
|
const ppsRef = useRef(100);
|
|
186
171
|
const durationRef = useRef(effectiveDuration);
|
|
187
172
|
durationRef.current = effectiveDuration;
|
|
188
|
-
// Declared before the fitPps derivation so the edit-pin wrappers can close over it.
|
|
189
173
|
const fitPpsRef = useRef(100);
|
|
190
174
|
const {
|
|
191
175
|
pinZoomBeforeEdit,
|
|
@@ -236,7 +220,6 @@ export const Timeline = memo(function Timeline({
|
|
|
236
220
|
setResizingClip,
|
|
237
221
|
blockedClipRef,
|
|
238
222
|
suppressClickRef,
|
|
239
|
-
syncClipDragAutoScroll,
|
|
240
223
|
} = useTimelineClipDrag({
|
|
241
224
|
scrollRef,
|
|
242
225
|
ppsRef,
|
|
@@ -253,22 +236,26 @@ export const Timeline = memo(function Timeline({
|
|
|
253
236
|
readZIndex: zSyncEnabled ? readClipZIndex : undefined,
|
|
254
237
|
onStackingPatches: zSyncEnabled ? applyStackingPatches : undefined,
|
|
255
238
|
refreshAfterLaneMove,
|
|
239
|
+
sessionEpoch,
|
|
256
240
|
});
|
|
257
241
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
242
|
+
const assetDrop = useTimelineAssetDrop({
|
|
243
|
+
scrollRef,
|
|
244
|
+
ppsRef,
|
|
245
|
+
durationRef,
|
|
246
|
+
trackOrderRef,
|
|
247
|
+
rowGeometryRef,
|
|
248
|
+
contentOrigin,
|
|
249
|
+
onFileDrop: pinnedOnFileDrop,
|
|
250
|
+
onAssetDrop: pinnedOnAssetDrop,
|
|
251
|
+
onBlockDrop: pinnedOnBlockDrop,
|
|
252
|
+
onCompositionDrop: pinnedOnCompositionDrop,
|
|
253
|
+
sessionEpoch,
|
|
254
|
+
});
|
|
271
255
|
const displayLayout = useTimelineDisplayLayout(draggedClip, trackOrder, rowGeometry);
|
|
256
|
+
const resizingElementIds =
|
|
257
|
+
resizingClip?.groupPreview?.map((change) => change.key) ??
|
|
258
|
+
(resizingClip ? [getTimelineElementIdentity(resizingClip.element)] : undefined);
|
|
272
259
|
const { recordTimelineScroll } = useTimelinePerformanceTelemetry({
|
|
273
260
|
totalClipCount: expandedElements.length,
|
|
274
261
|
totalRowCount: displayLayout.displayTrackOrder.length,
|
|
@@ -280,22 +267,43 @@ export const Timeline = memo(function Timeline({
|
|
|
280
267
|
expandedElements.length,
|
|
281
268
|
displayLayout.totalH,
|
|
282
269
|
]);
|
|
283
|
-
const
|
|
270
|
+
const { pps, fitPps, displayContentWidth, displayDuration, zoomModeRef, manualZoomPercentRef } =
|
|
271
|
+
useTimelineGeometry({
|
|
272
|
+
viewportWidth: viewport.clientWidth,
|
|
273
|
+
effectiveDuration,
|
|
274
|
+
zoomMode,
|
|
275
|
+
manualZoomPercent,
|
|
276
|
+
ppsRef,
|
|
277
|
+
fitPpsRef,
|
|
278
|
+
draggedClip,
|
|
279
|
+
resizingClip,
|
|
280
|
+
expandedElements,
|
|
281
|
+
isDragging,
|
|
282
|
+
scrollRef,
|
|
283
|
+
lastScrollLeftRef,
|
|
284
|
+
contentOrigin,
|
|
285
|
+
});
|
|
286
|
+
const timelineFocus = useTimelineLogicalFocus({
|
|
284
287
|
scrollRef,
|
|
288
|
+
tracks,
|
|
289
|
+
layout: displayLayout,
|
|
290
|
+
laneCounts,
|
|
291
|
+
selectedElementId,
|
|
292
|
+
selectedElementIds,
|
|
293
|
+
gsapAnimations,
|
|
294
|
+
elements: expandedElements,
|
|
295
|
+
pixelsPerSecond: pps,
|
|
296
|
+
contentOrigin,
|
|
297
|
+
allowHorizontal: zoomMode === "manual",
|
|
285
298
|
viewport,
|
|
286
|
-
rowGeometry: displayLayout.rowGeometry,
|
|
287
299
|
sessionEpoch,
|
|
288
|
-
elements: expandedElements,
|
|
289
|
-
selectedElementId,
|
|
290
|
-
revealElementId: clipRevealRequest?.elementId ?? null,
|
|
291
300
|
draggedRowKey: draggedClip?.started ? draggedClip.previewTrack : undefined,
|
|
292
|
-
|
|
301
|
+
resizingElementIds,
|
|
293
302
|
clipContextMenuRowKey: clipContextMenu?.element.track,
|
|
294
303
|
keyframeContextMenuRowKey: kfContextMenu?.element.track,
|
|
295
304
|
lastScrollLeftRef,
|
|
296
305
|
syncScrollViewport,
|
|
297
306
|
});
|
|
298
|
-
const { enabled: rowVirtualizationActive, virtualRows, focusedElementId } = rowWindow;
|
|
299
307
|
const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
|
|
300
308
|
const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
|
|
301
309
|
const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
|
|
@@ -308,22 +316,7 @@ export const Timeline = memo(function Timeline({
|
|
|
308
316
|
setKfContextMenu,
|
|
309
317
|
toggleSelectedKeyframe,
|
|
310
318
|
});
|
|
311
|
-
|
|
312
|
-
useTimelineGeometry({
|
|
313
|
-
viewportWidth: viewport.clientWidth,
|
|
314
|
-
effectiveDuration,
|
|
315
|
-
zoomMode,
|
|
316
|
-
manualZoomPercent,
|
|
317
|
-
ppsRef,
|
|
318
|
-
fitPpsRef,
|
|
319
|
-
draggedClip,
|
|
320
|
-
resizingClip,
|
|
321
|
-
expandedElements,
|
|
322
|
-
isDragging,
|
|
323
|
-
scrollRef,
|
|
324
|
-
lastScrollLeftRef,
|
|
325
|
-
contentOrigin,
|
|
326
|
-
});
|
|
319
|
+
|
|
327
320
|
const { clipIndex, renderTimeRange, pinnedClipIdentities } = useTimelineClipRenderWindow({
|
|
328
321
|
tracks,
|
|
329
322
|
viewport,
|
|
@@ -332,8 +325,8 @@ export const Timeline = memo(function Timeline({
|
|
|
332
325
|
duration: displayDuration,
|
|
333
326
|
selectedElementId: selectedElementId ?? undefined,
|
|
334
327
|
draggedElementId: draggedClip ? getTimelineElementIdentity(draggedClip.element) : undefined,
|
|
335
|
-
|
|
336
|
-
|
|
328
|
+
resizingElementIds,
|
|
329
|
+
focusedElementId: timelineFocus.pinnedElementId,
|
|
337
330
|
focusedEaseElementId: focusedEaseSegment?.elementId,
|
|
338
331
|
clipContextMenuElementId: clipContextMenu
|
|
339
332
|
? getTimelineElementIdentity(clipContextMenu.element)
|
|
@@ -341,13 +334,6 @@ export const Timeline = memo(function Timeline({
|
|
|
341
334
|
keyframeContextMenuElementId: kfContextMenu
|
|
342
335
|
? getTimelineElementIdentity(kfContextMenu.element)
|
|
343
336
|
: undefined,
|
|
344
|
-
focusedElementId,
|
|
345
|
-
scrollRef,
|
|
346
|
-
elements: expandedElements,
|
|
347
|
-
rowGeometry: displayLayout.rowGeometry,
|
|
348
|
-
allowHorizontalReveal: zoomMode === "manual",
|
|
349
|
-
rowVirtualizationActive,
|
|
350
|
-
sessionEpoch,
|
|
351
337
|
});
|
|
352
338
|
useTimelineActiveClips({
|
|
353
339
|
scrollRef,
|
|
@@ -405,6 +391,7 @@ export const Timeline = memo(function Timeline({
|
|
|
405
391
|
handlePointerDown,
|
|
406
392
|
handlePointerMove,
|
|
407
393
|
handlePointerUp,
|
|
394
|
+
handlePointerCancel,
|
|
408
395
|
} = useTimelineRangeSelection({
|
|
409
396
|
scrollRef,
|
|
410
397
|
ppsRef,
|
|
@@ -417,10 +404,11 @@ export const Timeline = memo(function Timeline({
|
|
|
417
404
|
isDragging,
|
|
418
405
|
setShowPopover,
|
|
419
406
|
elementsRef: expandedElementsRef,
|
|
420
|
-
|
|
407
|
+
clipIndex,
|
|
421
408
|
rowGeometryRef,
|
|
422
409
|
onSelectElement,
|
|
423
410
|
contentOrigin,
|
|
411
|
+
sessionEpoch,
|
|
424
412
|
});
|
|
425
413
|
setRangeSelectionRef.current = setRangeSelection; // stable ref consumed by useTimelineClipDrag
|
|
426
414
|
|
|
@@ -432,7 +420,7 @@ export const Timeline = memo(function Timeline({
|
|
|
432
420
|
displayDuration,
|
|
433
421
|
pps,
|
|
434
422
|
timeDisplayMode,
|
|
435
|
-
rowVirtualizationActive ? renderTimeRange : undefined,
|
|
423
|
+
timelineFocus.rowVirtualizationActive ? renderTimeRange : undefined,
|
|
436
424
|
);
|
|
437
425
|
|
|
438
426
|
const getPreviewElement = useCallback(
|
|
@@ -443,11 +431,11 @@ export const Timeline = memo(function Timeline({
|
|
|
443
431
|
if (!timelineReady || expandedElements.length === 0) {
|
|
444
432
|
return (
|
|
445
433
|
<TimelineEmptyState
|
|
446
|
-
isDragOver={isDragOver}
|
|
434
|
+
isDragOver={assetDrop.isDragOver}
|
|
447
435
|
onFileDrop={!!onFileDrop}
|
|
448
|
-
onDragOver={handleAssetDragOver}
|
|
449
|
-
onDragLeave={
|
|
450
|
-
onDrop={handleAssetDrop}
|
|
436
|
+
onDragOver={assetDrop.handleAssetDragOver}
|
|
437
|
+
onDragLeave={assetDrop.handleAssetDragLeave}
|
|
438
|
+
onDrop={assetDrop.handleAssetDrop}
|
|
451
439
|
/>
|
|
452
440
|
);
|
|
453
441
|
}
|
|
@@ -457,7 +445,7 @@ export const Timeline = memo(function Timeline({
|
|
|
457
445
|
ref={setContainerRef}
|
|
458
446
|
aria-label="Timeline"
|
|
459
447
|
data-timeline-element-count={expandedElements.length}
|
|
460
|
-
className={`relative border-t select-none h-full overflow-hidden ${isDragOver ? "ring-1 ring-inset ring-studio-accent/60" : ""} ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
|
|
448
|
+
className={`relative border-t select-none h-full overflow-hidden ${assetDrop.isDragOver ? "ring-1 ring-inset ring-studio-accent/60" : ""} ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
|
|
461
449
|
onMouseMove={updateRazorGuide}
|
|
462
450
|
onMouseLeave={clearRazorGuide}
|
|
463
451
|
style={{
|
|
@@ -468,6 +456,7 @@ export const Timeline = memo(function Timeline({
|
|
|
468
456
|
>
|
|
469
457
|
<div
|
|
470
458
|
ref={setScrollRef}
|
|
459
|
+
// Stable owner for gestures that must survive virtual row/clip unmounts.
|
|
471
460
|
data-timeline-scroll-viewport
|
|
472
461
|
data-timeline-auto-scroll-left-inset={labelMode ? LABEL_COL_W : 0}
|
|
473
462
|
tabIndex={-1}
|
|
@@ -477,20 +466,20 @@ export const Timeline = memo(function Timeline({
|
|
|
477
466
|
recordTimelineScroll(e.currentTarget);
|
|
478
467
|
syncScrollViewport(e.currentTarget, true);
|
|
479
468
|
}}
|
|
480
|
-
{...
|
|
481
|
-
onDragOver={handleAssetDragOver}
|
|
482
|
-
onDragLeave={
|
|
483
|
-
onDrop={handleAssetDrop}
|
|
469
|
+
{...timelineFocus.timelineFocusProps}
|
|
470
|
+
onDragOver={assetDrop.handleAssetDragOver}
|
|
471
|
+
onDragLeave={assetDrop.handleAssetDragLeave}
|
|
472
|
+
onDrop={assetDrop.handleAssetDrop}
|
|
484
473
|
onPointerDown={(e) => {
|
|
485
|
-
//
|
|
486
|
-
// their own clicks — scrubbing here would preventDefault and eat them.
|
|
474
|
+
// Interactive controls own their clicks; scrubbing would preventDefault and eat them.
|
|
487
475
|
if (e.target instanceof Element && e.target.closest("button, input, select, a")) return;
|
|
488
476
|
if (splitAllAtPointer(e)) return;
|
|
489
477
|
handlePointerDown(e);
|
|
490
478
|
}}
|
|
491
479
|
onPointerMove={handlePointerMove}
|
|
492
480
|
onPointerUp={handlePointerUp}
|
|
493
|
-
|
|
481
|
+
onPointerCancel={handlePointerCancel}
|
|
482
|
+
onLostPointerCapture={handlePointerCancel}
|
|
494
483
|
>
|
|
495
484
|
<TimelineCanvas
|
|
496
485
|
major={major}
|
|
@@ -509,8 +498,10 @@ export const Timeline = memo(function Timeline({
|
|
|
509
498
|
displayTrackOrder={displayLayout.displayTrackOrder}
|
|
510
499
|
rowHeights={displayLayout.displayRowHeights}
|
|
511
500
|
rowGeometry={displayLayout.rowGeometry}
|
|
512
|
-
virtualRows={virtualRows}
|
|
513
|
-
|
|
501
|
+
virtualRows={timelineFocus.virtualRows}
|
|
502
|
+
logicalRows={timelineFocus.logicalRows}
|
|
503
|
+
focusedTargetId={timelineFocus.focusedTargetId}
|
|
504
|
+
rowsVirtualized={timelineFocus.rowVirtualizationActive}
|
|
514
505
|
clipIndex={clipIndex}
|
|
515
506
|
renderTimeRange={renderTimeRange}
|
|
516
507
|
pinnedClipIdentities={pinnedClipIdentities}
|
|
@@ -529,7 +520,9 @@ export const Timeline = memo(function Timeline({
|
|
|
529
520
|
scrollRef={scrollRef}
|
|
530
521
|
// Windowing drops content to mount a row cheaply; unvirtualized it is pure cost.
|
|
531
522
|
renderClipContent={
|
|
532
|
-
rowVirtualizationActive && viewport.isScrolling
|
|
523
|
+
timelineFocus.rowVirtualizationActive && viewport.isScrolling
|
|
524
|
+
? undefined
|
|
525
|
+
: renderClipContent
|
|
533
526
|
}
|
|
534
527
|
renderClipOverlay={renderClipOverlay}
|
|
535
528
|
playheadRef={playheadRef}
|
|
@@ -541,7 +534,6 @@ export const Timeline = memo(function Timeline({
|
|
|
541
534
|
setResizingClip={setResizingClip}
|
|
542
535
|
setDraggedClip={setDraggedClip}
|
|
543
536
|
setSelectedElementId={setSelectedElementId}
|
|
544
|
-
syncClipDragAutoScroll={syncClipDragAutoScroll}
|
|
545
537
|
shiftClickClipRef={shiftClickClipRef}
|
|
546
538
|
getPreviewElement={getPreviewElement}
|
|
547
539
|
getTrackStyle={getTrackStyle}
|
|
@@ -561,7 +553,12 @@ export const Timeline = memo(function Timeline({
|
|
|
561
553
|
setSelectedElementId(el.key ?? el.id);
|
|
562
554
|
onSelectElement?.(el);
|
|
563
555
|
dismissGapMenu();
|
|
564
|
-
setClipContextMenu({
|
|
556
|
+
setClipContextMenu({
|
|
557
|
+
x: e.clientX,
|
|
558
|
+
y: e.clientY,
|
|
559
|
+
element: el,
|
|
560
|
+
sessionEpoch: usePlayerStore.getState().timelineSessionEpoch,
|
|
561
|
+
});
|
|
565
562
|
}}
|
|
566
563
|
onContextMenuLane={(e, track, time) => {
|
|
567
564
|
if (draggedClip?.started || resizingClip) return;
|
|
@@ -572,6 +569,8 @@ export const Timeline = memo(function Timeline({
|
|
|
572
569
|
{activeTool === "razor" && razorGuideX !== null && <TimelineRazorGuide x={razorGuideX} />}
|
|
573
570
|
</div>
|
|
574
571
|
<TimelineOverlays
|
|
572
|
+
elements={expandedElements}
|
|
573
|
+
elementsRef={expandedElementsRef}
|
|
575
574
|
theme={theme}
|
|
576
575
|
showShortcutHint={showShortcutHint}
|
|
577
576
|
showPopover={showPopover}
|
|
@@ -132,7 +132,7 @@ describe("Timeline row virtualization", { timeout: 30_000 }, () => {
|
|
|
132
132
|
|
|
133
133
|
const { host, root } = await mountTimeline(React.createElement(Timeline, { sessionEpoch: 2 }));
|
|
134
134
|
|
|
135
|
-
const rows = host.querySelectorAll(
|
|
135
|
+
const rows = host.querySelectorAll("[data-timeline-row]");
|
|
136
136
|
expect(rows.length).toBeGreaterThan(0);
|
|
137
137
|
expect(rows.length).toBeLessThanOrEqual(16);
|
|
138
138
|
|
|
@@ -201,16 +201,17 @@ describe("Timeline row virtualization", { timeout: 30_000 }, () => {
|
|
|
201
201
|
|
|
202
202
|
await settleUntil(
|
|
203
203
|
() =>
|
|
204
|
-
(host.querySelector('[role="
|
|
205
|
-
0,
|
|
204
|
+
(host.querySelector('[role="treegrid"]')?.querySelectorAll('[role="row"]').length ?? 0) > 0,
|
|
206
205
|
);
|
|
207
|
-
const
|
|
208
|
-
const rows =
|
|
206
|
+
const treegrid = host.querySelector<HTMLElement>('[role="treegrid"]');
|
|
207
|
+
const rows = treegrid?.querySelectorAll('[role="row"]') ?? [];
|
|
209
208
|
expect(rows.length).toBeGreaterThan(0);
|
|
210
209
|
expect(rows.length).toBeLessThanOrEqual(16);
|
|
211
|
-
expect(rows[0]?.getAttribute("aria-
|
|
212
|
-
expect(
|
|
213
|
-
expect(
|
|
210
|
+
expect(rows[0]?.getAttribute("aria-rowindex")).toBe("1");
|
|
211
|
+
expect(treegrid?.getAttribute("aria-rowcount")).toBe("1000");
|
|
212
|
+
expect(treegrid?.hasAttribute("aria-multiselectable")).toBe(false);
|
|
213
|
+
expect(treegrid?.querySelectorAll('[data-timeline-focus-id][tabindex="0"]')).toHaveLength(1);
|
|
214
|
+
expect(treegrid?.parentElement?.style.height).toBe(
|
|
214
215
|
`${getTimelineCanvasHeight(Array.from({ length: 1_000 }, () => TRACK_H))}px`,
|
|
215
216
|
);
|
|
216
217
|
|
|
@@ -226,7 +227,44 @@ describe("Timeline row virtualization", { timeout: 30_000 }, () => {
|
|
|
226
227
|
scroller.dispatchEvent(new Event("scroll"));
|
|
227
228
|
});
|
|
228
229
|
}
|
|
229
|
-
expect(
|
|
230
|
+
expect(treegrid?.querySelector('[data-timeline-row-key="0"]')).not.toBeNull();
|
|
231
|
+
expect(document.activeElement).toBe(focusedControl);
|
|
232
|
+
|
|
233
|
+
act(() => root.unmount());
|
|
234
|
+
usePlayerStore.getState().reset();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("keeps focus pinning active after the scroll viewport remounts", async () => {
|
|
238
|
+
const [{ Timeline }, { usePlayerStore }] = await Promise.all([
|
|
239
|
+
import("./Timeline"),
|
|
240
|
+
import("../store/playerStore"),
|
|
241
|
+
]);
|
|
242
|
+
usePlayerStore.setState({
|
|
243
|
+
duration: 60,
|
|
244
|
+
timelineReady: true,
|
|
245
|
+
elements: clipsByTrack(1_000),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const { host, root } = await mountTimeline(React.createElement(Timeline, { sessionEpoch: 9 }));
|
|
249
|
+
await settleUntil(() => host.querySelectorAll("[data-timeline-row]").length > 0);
|
|
250
|
+
const firstScroller = host.querySelector<HTMLElement>("[data-timeline-scroll-viewport]");
|
|
251
|
+
|
|
252
|
+
await act(async () => usePlayerStore.setState({ timelineReady: false }));
|
|
253
|
+
expect(host.querySelector("[data-timeline-scroll-viewport]")).toBeNull();
|
|
254
|
+
await act(async () => usePlayerStore.setState({ timelineReady: true }));
|
|
255
|
+
await settleUntil(() => host.querySelectorAll("[data-timeline-row]").length > 0);
|
|
256
|
+
|
|
257
|
+
const scroller = host.querySelector<HTMLElement>("[data-timeline-scroll-viewport]");
|
|
258
|
+
const firstRow = host.querySelector<HTMLElement>('[data-timeline-row-key="0"]');
|
|
259
|
+
const focusedControl = firstRow?.querySelector<HTMLButtonElement>("button");
|
|
260
|
+
expect(scroller).not.toBe(firstScroller);
|
|
261
|
+
expect(focusedControl).not.toBeNull();
|
|
262
|
+
act(() => focusedControl?.focus());
|
|
263
|
+
if (scroller) {
|
|
264
|
+
scroller.scrollTop = 500 * 48;
|
|
265
|
+
await dispatchScroll(scroller);
|
|
266
|
+
}
|
|
267
|
+
expect(host.querySelector('[data-timeline-row-key="0"]')).not.toBeNull();
|
|
230
268
|
expect(document.activeElement).toBe(focusedControl);
|
|
231
269
|
|
|
232
270
|
act(() => root.unmount());
|
|
@@ -242,6 +280,8 @@ describe("Timeline row virtualization", { timeout: 30_000 }, () => {
|
|
|
242
280
|
usePlayerStore.setState({
|
|
243
281
|
duration: 1_000,
|
|
244
282
|
timelineReady: true,
|
|
283
|
+
timelineProjectId: "project-a",
|
|
284
|
+
timelineSessionEpoch: 4,
|
|
245
285
|
zoomMode: "manual",
|
|
246
286
|
manualZoomPercent: 2_000,
|
|
247
287
|
selectedElementId: "clip-490",
|
|
@@ -286,10 +326,13 @@ describe("Timeline row virtualization", { timeout: 30_000 }, () => {
|
|
|
286
326
|
expect(host.querySelector('[data-el-id="clip-490"]')).not.toBeNull();
|
|
287
327
|
expect(host.querySelectorAll("[data-timeline-grid-cell]").length).toBeLessThan(100);
|
|
288
328
|
|
|
289
|
-
|
|
329
|
+
const { timelineClipFocusId } = await import("./timelineNavigationIdentity");
|
|
330
|
+
await act(async () =>
|
|
331
|
+
usePlayerStore.getState().requestTimelineFocus(timelineClipFocusId("clip-300")),
|
|
332
|
+
);
|
|
290
333
|
await advanceFrame();
|
|
291
334
|
await act(async () => {});
|
|
292
|
-
expect(usePlayerStore.getState().
|
|
335
|
+
expect(usePlayerStore.getState().timelineFocus?.id).toBe(timelineClipFocusId("clip-300"));
|
|
293
336
|
const focusedClip = host.querySelector('[data-el-id="clip-300"]');
|
|
294
337
|
expect(document.activeElement).toBe(focusedClip);
|
|
295
338
|
await advanceFrame();
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { memo } from "react";
|
|
2
2
|
import { TimelineRuler } from "./TimelineRuler";
|
|
3
3
|
import { PlayheadIndicator } from "./PlayheadIndicator";
|
|
4
|
-
import {
|
|
5
|
-
import { getRenderedTimelineElement } from "./timelineTheme";
|
|
4
|
+
import type { TimelineRangeSelection } from "./timelineEditing";
|
|
6
5
|
import {
|
|
7
6
|
RULER_H,
|
|
8
7
|
CLIP_Y,
|
|
@@ -19,13 +18,11 @@ import type { ResizingClipState } from "./useTimelineClipDrag";
|
|
|
19
18
|
import { type MultiDragPreviewInput } from "./timelineMultiDragPreview";
|
|
20
19
|
import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
|
|
21
20
|
import type { Rect } from "../../utils/marqueeGeometry";
|
|
22
|
-
import { TimelineClip } from "./TimelineClip";
|
|
23
21
|
import { TimelineLanes } from "./TimelineLanes";
|
|
24
22
|
import type { TimelineLaneBaseProps } from "./timelineLaneProps";
|
|
25
|
-
import { renderClipChildren } from "./timelineClipChildren";
|
|
26
23
|
import type { TimelineLaneGapStrips } from "./useTimelineGapHighlights";
|
|
27
|
-
import { isTimelineClipActive } from "./useTimelineActiveClips";
|
|
28
24
|
import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
|
|
25
|
+
import { TimelineGestureOverlay } from "./TimelineGestureOverlay";
|
|
29
26
|
|
|
30
27
|
interface TimelineCanvasProps extends TimelineLaneBaseProps {
|
|
31
28
|
major: number[];
|
|
@@ -65,15 +62,6 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
65
62
|
const beatDragging = usePlayerStore((s) => s.beatDragging);
|
|
66
63
|
const draggedElement = draggedClip?.element ?? null;
|
|
67
64
|
const draggedElementIdentity = draggedElement ? getTimelineElementIdentity(draggedElement) : null;
|
|
68
|
-
const activeDraggedElement =
|
|
69
|
-
draggedClip?.started === true && draggedElement && draggedElementIdentity
|
|
70
|
-
? getRenderedTimelineElement({
|
|
71
|
-
element: draggedElement,
|
|
72
|
-
draggedElementId: draggedElementIdentity,
|
|
73
|
-
previewStart: draggedClip.previewStart,
|
|
74
|
-
previewTrack: draggedClip.previewTrack,
|
|
75
|
-
})
|
|
76
|
-
: null;
|
|
77
65
|
// The drag ghost follows the cursor freely (both axes) — CapCut-style. The
|
|
78
66
|
// "magnetic" affordance is a highlight on the destination lane (draggedRowIndex),
|
|
79
67
|
// which flips at the MAGNETIC_TRACK_THRESHOLD point; the clip drops into it.
|
|
@@ -94,22 +82,6 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
94
82
|
selectedKeys: selectedElementIds,
|
|
95
83
|
}
|
|
96
84
|
: null;
|
|
97
|
-
const activeDraggedPosition =
|
|
98
|
-
draggedClip?.started === true && activeDraggedElement && scrollRef.current
|
|
99
|
-
? {
|
|
100
|
-
left:
|
|
101
|
-
draggedClip.pointerClientX -
|
|
102
|
-
scrollRef.current.getBoundingClientRect().left +
|
|
103
|
-
scrollRef.current.scrollLeft -
|
|
104
|
-
draggedClip.pointerOffsetX,
|
|
105
|
-
top:
|
|
106
|
-
draggedClip.pointerClientY -
|
|
107
|
-
scrollRef.current.getBoundingClientRect().top +
|
|
108
|
-
scrollRef.current.scrollTop -
|
|
109
|
-
draggedClip.pointerOffsetY,
|
|
110
|
-
}
|
|
111
|
-
: null;
|
|
112
|
-
|
|
113
85
|
return (
|
|
114
86
|
<div
|
|
115
87
|
className="relative"
|
|
@@ -235,47 +207,18 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
235
207
|
/>
|
|
236
208
|
)}
|
|
237
209
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
<TimelineClip
|
|
251
|
-
el={{ ...activeDraggedElement, start: 0 }}
|
|
252
|
-
pps={props.pps}
|
|
253
|
-
clipY={0}
|
|
254
|
-
isSelected={
|
|
255
|
-
props.selectedElementId === (activeDraggedElement.key ?? activeDraggedElement.id)
|
|
256
|
-
}
|
|
257
|
-
isHovered={false}
|
|
258
|
-
isDragging={true}
|
|
259
|
-
isActive={isTimelineClipActive(activeDraggedElement, props.currentTime)}
|
|
260
|
-
hasCustomContent={!!props.renderClipContent}
|
|
261
|
-
capabilities={getTimelineEditCapabilities(activeDraggedElement)}
|
|
262
|
-
theme={props.theme}
|
|
263
|
-
isComposition={!!activeDraggedElement.compositionSrc}
|
|
264
|
-
onHoverStart={() => {}}
|
|
265
|
-
onHoverEnd={() => {}}
|
|
266
|
-
onResizeStart={() => {}}
|
|
267
|
-
onClick={() => {}}
|
|
268
|
-
onDoubleClick={() => {}}
|
|
269
|
-
>
|
|
270
|
-
{renderClipChildren(
|
|
271
|
-
activeDraggedElement,
|
|
272
|
-
props.getTrackStyle(activeDraggedElement.tag),
|
|
273
|
-
props.renderClipContent,
|
|
274
|
-
props.renderClipOverlay,
|
|
275
|
-
)}
|
|
276
|
-
</TimelineClip>
|
|
277
|
-
</div>
|
|
278
|
-
)}
|
|
210
|
+
<TimelineGestureOverlay
|
|
211
|
+
drag={draggedClip}
|
|
212
|
+
scrollRef={scrollRef}
|
|
213
|
+
pixelsPerSecond={props.pps}
|
|
214
|
+
rowHeight={draggedClipHeight}
|
|
215
|
+
selectedElementId={props.selectedElementId}
|
|
216
|
+
currentTime={props.currentTime}
|
|
217
|
+
theme={props.theme}
|
|
218
|
+
getTrackStyle={props.getTrackStyle}
|
|
219
|
+
renderClipContent={props.renderClipContent}
|
|
220
|
+
renderClipOverlay={props.renderClipOverlay}
|
|
221
|
+
/>
|
|
279
222
|
|
|
280
223
|
{/* Marquee (rubber-band) multi-select rectangle — mirrors the canvas
|
|
281
224
|
MarqueeOverlay look: semi-transparent accent fill + dashed border. */}
|
|
@@ -36,6 +36,7 @@ function renderClip({
|
|
|
36
36
|
const host = document.createElement("div");
|
|
37
37
|
document.body.append(host);
|
|
38
38
|
const root = createRoot(host);
|
|
39
|
+
const onClick = vi.fn();
|
|
39
40
|
|
|
40
41
|
act(() => {
|
|
41
42
|
root.render(
|
|
@@ -50,7 +51,7 @@ function renderClip({
|
|
|
50
51
|
isComposition={false}
|
|
51
52
|
onHoverStart={vi.fn()}
|
|
52
53
|
onHoverEnd={vi.fn()}
|
|
53
|
-
onClick={
|
|
54
|
+
onClick={onClick}
|
|
54
55
|
onDoubleClick={vi.fn()}
|
|
55
56
|
>
|
|
56
57
|
<div data-custom-content="true" />
|
|
@@ -58,7 +59,7 @@ function renderClip({
|
|
|
58
59
|
);
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
return { host, root };
|
|
62
|
+
return { host, onClick, root };
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
describe("TimelineClip", () => {
|
|
@@ -113,4 +114,18 @@ describe("TimelineClip", () => {
|
|
|
113
114
|
|
|
114
115
|
act(() => root.unmount());
|
|
115
116
|
});
|
|
117
|
+
|
|
118
|
+
it("is a roving native button with explicit selection semantics", () => {
|
|
119
|
+
const { host, onClick, root } = renderClip({
|
|
120
|
+
element: { id: "hero", label: "Hero", tag: "div", start: 1, duration: 2, track: 0 },
|
|
121
|
+
isSelected: true,
|
|
122
|
+
});
|
|
123
|
+
const clip = host.querySelector<HTMLButtonElement>(".timeline-clip")!;
|
|
124
|
+
expect(clip.type).toBe("button");
|
|
125
|
+
expect(clip.tabIndex).toBe(-1);
|
|
126
|
+
expect(clip.getAttribute("aria-pressed")).toBe("true");
|
|
127
|
+
act(() => clip.click());
|
|
128
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
129
|
+
act(() => root.unmount());
|
|
130
|
+
});
|
|
116
131
|
});
|