@hyperframes/studio 0.7.57 → 0.7.59
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--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-KsfE1bUu.js → index-B995FG46.js} +1 -1
- package/dist/assets/index-CrkAdJkb.js +426 -0
- package/dist/assets/index-Dj5p8U_A.css +1 -0
- package/dist/assets/{index-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +27 -7
- package/dist/index.html +2 -2
- package/dist/index.js +18029 -11563
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +6 -6
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/StudioRightPanel.tsx +29 -51
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/LayersPanel.tsx +110 -99
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +55 -1
- package/src/hooks/timelineTrackVisibility.ts +31 -13
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +81 -162
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +182 -84
- package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
- package/src/hooks/useFileManager.ts +82 -60
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +306 -155
- package/src/hooks/useTimelineEditing.ts +111 -101
- package/src/hooks/useTimelineEditingTypes.ts +4 -1
- package/src/hooks/useTimelineGroupEditing.ts +51 -5
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +490 -57
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +292 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +30 -27
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.ts +17 -0
- package/dist/assets/index-DfmYkU44.js +0 -423
- package/dist/assets/index-_pqzyxB1.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
PLAYHEAD_HEAD_W,
|
|
26
26
|
RULER_H,
|
|
27
27
|
TRACK_H,
|
|
28
|
+
TRACKS_LEFT_PAD,
|
|
28
29
|
getTimelineDisplayContentWidth,
|
|
29
30
|
getTimelineFitPps,
|
|
30
31
|
} from "./timelineLayout";
|
|
@@ -152,7 +153,8 @@ describe("Timeline provider boundary", () => {
|
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
const row = button.parentElement?.parentElement;
|
|
155
|
-
|
|
156
|
+
// Row children: [sticky gutter, TRACKS_LEFT_PAD spacer, time-mapped content].
|
|
157
|
+
const trackContent = row?.children.item(2);
|
|
156
158
|
expect(onToggleTrackHidden).toHaveBeenCalledWith(0, false);
|
|
157
159
|
expect(trackContent).toBeInstanceOf(HTMLElement);
|
|
158
160
|
if (!(trackContent instanceof HTMLElement)) {
|
|
@@ -454,27 +456,27 @@ describe("shouldAutoScrollTimeline", () => {
|
|
|
454
456
|
});
|
|
455
457
|
|
|
456
458
|
describe("getTimelineFitPps (min 60s extent + fit headroom)", () => {
|
|
457
|
-
const viewport = 632; // usable width = 632 - GUTTER -
|
|
459
|
+
const viewport = 632; // usable width = 632 - GUTTER - TRACKS_LEFT_PAD - 2
|
|
458
460
|
|
|
459
461
|
it("computes fit pps against the 60s floor for short compositions", () => {
|
|
460
462
|
// A 10s comp maps 60s onto the viewport → the comp takes ~1/6 of the width.
|
|
461
463
|
// (10 * 1.2 = 12s of headroom-padded content is still under the 60s floor.)
|
|
462
464
|
const pps = getTimelineFitPps(viewport, 10);
|
|
463
|
-
expect(pps).toBeCloseTo((viewport - GUTTER - 2) / MIN_TIMELINE_EXTENT_S);
|
|
464
|
-
expect(10 * pps).toBeCloseTo((viewport - GUTTER - 2) / 6);
|
|
465
|
+
expect(pps).toBeCloseTo((viewport - GUTTER - TRACKS_LEFT_PAD - 2) / MIN_TIMELINE_EXTENT_S);
|
|
466
|
+
expect(10 * pps).toBeCloseTo((viewport - GUTTER - TRACKS_LEFT_PAD - 2) / 6);
|
|
465
467
|
});
|
|
466
468
|
|
|
467
469
|
it("fits duration * FIT_ZOOM_HEADROOM (not the bare duration) for long compositions", () => {
|
|
468
470
|
expect(getTimelineFitPps(viewport, 60)).toBeCloseTo(
|
|
469
|
-
(viewport - GUTTER - 2) / (60 * FIT_ZOOM_HEADROOM),
|
|
471
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / (60 * FIT_ZOOM_HEADROOM),
|
|
470
472
|
);
|
|
471
473
|
expect(getTimelineFitPps(viewport, 120)).toBeCloseTo(
|
|
472
|
-
(viewport - GUTTER - 2) / (120 * FIT_ZOOM_HEADROOM),
|
|
474
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / (120 * FIT_ZOOM_HEADROOM),
|
|
473
475
|
);
|
|
474
476
|
});
|
|
475
477
|
|
|
476
478
|
it("leaves CapCut-style trailing headroom: the comp ends at 1/1.2 of the usable width", () => {
|
|
477
|
-
const usable = viewport - GUTTER - 2;
|
|
479
|
+
const usable = viewport - GUTTER - TRACKS_LEFT_PAD - 2;
|
|
478
480
|
const pps = getTimelineFitPps(viewport, 120);
|
|
479
481
|
// Composition content occupies usable/1.2 px; the remaining ~17% is empty
|
|
480
482
|
// droppable ruler/lane surface past the end.
|
|
@@ -484,16 +486,16 @@ describe("getTimelineFitPps (min 60s extent + fit headroom)", () => {
|
|
|
484
486
|
|
|
485
487
|
it("falls back to 100 pps before the viewport is measured", () => {
|
|
486
488
|
expect(getTimelineFitPps(0, 10)).toBe(100);
|
|
487
|
-
expect(getTimelineFitPps(GUTTER, 10)).toBe(100);
|
|
489
|
+
expect(getTimelineFitPps(GUTTER + TRACKS_LEFT_PAD, 10)).toBe(100);
|
|
488
490
|
expect(getTimelineFitPps(Number.NaN, 10)).toBe(100);
|
|
489
491
|
});
|
|
490
492
|
|
|
491
493
|
it("uses the floor for zero/invalid durations", () => {
|
|
492
494
|
expect(getTimelineFitPps(viewport, 0)).toBeCloseTo(
|
|
493
|
-
(viewport - GUTTER - 2) / MIN_TIMELINE_EXTENT_S,
|
|
495
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / MIN_TIMELINE_EXTENT_S,
|
|
494
496
|
);
|
|
495
497
|
expect(getTimelineFitPps(viewport, Number.NaN)).toBeCloseTo(
|
|
496
|
-
(viewport - GUTTER - 2) / MIN_TIMELINE_EXTENT_S,
|
|
498
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / MIN_TIMELINE_EXTENT_S,
|
|
497
499
|
);
|
|
498
500
|
});
|
|
499
501
|
});
|
|
@@ -509,7 +511,7 @@ describe("getTimelineDisplayContentWidth", () => {
|
|
|
509
511
|
it("still fills the viewport when that is larger than the 60s floor", () => {
|
|
510
512
|
expect(
|
|
511
513
|
getTimelineDisplayContentWidth({ trackContentWidth: 200, viewportWidth: 2000, pps: 5 }),
|
|
512
|
-
).toBe(2000 - GUTTER - 2);
|
|
514
|
+
).toBe(2000 - GUTTER - TRACKS_LEFT_PAD - 2);
|
|
513
515
|
});
|
|
514
516
|
|
|
515
517
|
it("tracks a drag ghost past every other bound (drag-to-extend)", () => {
|
|
@@ -599,20 +601,28 @@ describe("getTimelineScrollLeftForZoomAnchor", () => {
|
|
|
599
601
|
});
|
|
600
602
|
|
|
601
603
|
describe("getTimelinePlayheadLeft", () => {
|
|
602
|
-
it("offsets the wrapper by half the head width so the line CENTER = GUTTER + t*pps", () => {
|
|
604
|
+
it("offsets the wrapper by half the head width so the line CENTER = GUTTER + TRACKS_LEFT_PAD + t*pps", () => {
|
|
603
605
|
// Wrapper left + PLAYHEAD_HEAD_W/2 (where the 1px line is centered) must
|
|
604
|
-
// equal GUTTER + t*pps at any zoom.
|
|
605
|
-
expect(getTimelinePlayheadLeft(4, 20) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
606
|
-
|
|
606
|
+
// equal GUTTER + TRACKS_LEFT_PAD + t*pps at any zoom.
|
|
607
|
+
expect(getTimelinePlayheadLeft(4, 20) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
608
|
+
GUTTER + TRACKS_LEFT_PAD + 4 * 20,
|
|
609
|
+
);
|
|
610
|
+
expect(getTimelinePlayheadLeft(10, 7.5) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
611
|
+
GUTTER + TRACKS_LEFT_PAD + 75,
|
|
612
|
+
);
|
|
607
613
|
});
|
|
608
614
|
|
|
609
|
-
it("centers the line exactly on the
|
|
610
|
-
expect(getTimelinePlayheadLeft(0, 20) + PLAYHEAD_HEAD_W / 2).toBe(GUTTER);
|
|
615
|
+
it("centers the line exactly on the left pad's end (the 00:00 tick) at t = 0", () => {
|
|
616
|
+
expect(getTimelinePlayheadLeft(0, 20) + PLAYHEAD_HEAD_W / 2).toBe(GUTTER + TRACKS_LEFT_PAD);
|
|
611
617
|
});
|
|
612
618
|
|
|
613
619
|
it("guards invalid input", () => {
|
|
614
|
-
expect(getTimelinePlayheadLeft(Number.NaN, 20)).toBe(
|
|
615
|
-
|
|
620
|
+
expect(getTimelinePlayheadLeft(Number.NaN, 20)).toBe(
|
|
621
|
+
GUTTER + TRACKS_LEFT_PAD - PLAYHEAD_HEAD_W / 2,
|
|
622
|
+
);
|
|
623
|
+
expect(getTimelinePlayheadLeft(4, Number.NaN)).toBe(
|
|
624
|
+
GUTTER + TRACKS_LEFT_PAD - PLAYHEAD_HEAD_W / 2,
|
|
625
|
+
);
|
|
616
626
|
});
|
|
617
627
|
});
|
|
618
628
|
|
|
@@ -681,7 +691,7 @@ describe("resolveTimelineAssetDrop", () => {
|
|
|
681
691
|
trackHeight: 72,
|
|
682
692
|
trackOrder: [0, 3, 7],
|
|
683
693
|
},
|
|
684
|
-
|
|
694
|
+
480, // rectLeft(100) + GUTTER + TRACKS_LEFT_PAD + 3s*100pps
|
|
685
695
|
// clientY updated for TRACKS_TOP_PAD=72: rectTop(200) + RULER_H(24) +
|
|
686
696
|
// TRACKS_TOP_PAD(72) + TRACK_H(48) + TRACK_H/2(24) = 368 → row 1 → track 3.
|
|
687
697
|
368,
|
|
@@ -702,7 +712,7 @@ describe("resolveTimelineAssetDrop", () => {
|
|
|
702
712
|
trackHeight: 72,
|
|
703
713
|
trackOrder: [0, 3, 7],
|
|
704
714
|
},
|
|
705
|
-
250,
|
|
715
|
+
250 + TRACKS_LEFT_PAD,
|
|
706
716
|
600,
|
|
707
717
|
),
|
|
708
718
|
).toEqual({ start: 1.18, track: 8 });
|
|
@@ -9,7 +9,7 @@ import { defaultTimelineTheme } from "./timelineTheme";
|
|
|
9
9
|
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
10
10
|
import { useTimelinePlayhead } from "./useTimelinePlayhead";
|
|
11
11
|
import { useTimelineActiveClips } from "./useTimelineActiveClips";
|
|
12
|
-
import {
|
|
12
|
+
import { getTrackStyle } from "./timelineIcons";
|
|
13
13
|
import { useTimelineZoom } from "./useTimelineZoom";
|
|
14
14
|
import { useTimelineAssetDrop } from "./timelineDragDrop";
|
|
15
15
|
import { TimelineEmptyState } from "./TimelineEmptyState";
|
|
@@ -20,15 +20,14 @@ import { TimelineOverlays } from "./TimelineOverlays";
|
|
|
20
20
|
import { useTimelineEditPinning } from "./useTimelineEditPinning";
|
|
21
21
|
import { useTimelineStackingSync } from "./useTimelineStackingSync";
|
|
22
22
|
import { useTimelineGeometry } from "./useTimelineGeometry";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
getTimelineCanvasHeight,
|
|
27
|
-
shouldShowTimelineShortcutHint,
|
|
28
|
-
} from "./timelineLayout";
|
|
23
|
+
import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
|
|
24
|
+
import { GUTTER, TRACKS_LEFT_PAD, generateTicks, getTimelineCanvasHeight } from "./timelineLayout";
|
|
25
|
+
import { useTimelineScrollViewport } from "./useTimelineScrollViewport";
|
|
29
26
|
import { STUDIO_PREVIEW_FPS } from "../lib/time";
|
|
30
27
|
import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks";
|
|
31
28
|
import type { TimelineProps } from "./TimelineTypes";
|
|
29
|
+
import { useTrackGapMenu } from "./useTrackGapMenu";
|
|
30
|
+
import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
|
|
32
31
|
|
|
33
32
|
// Re-export pure utilities so existing imports from "./Timeline" still resolve.
|
|
34
33
|
export {
|
|
@@ -114,106 +113,41 @@ export const Timeline = memo(function Timeline({
|
|
|
114
113
|
const [razorGuideX, setRazorGuideX] = useState<number | null>(null);
|
|
115
114
|
|
|
116
115
|
useMountEffect(() => {
|
|
117
|
-
const
|
|
118
|
-
const up = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(false);
|
|
116
|
+
const key = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(e.type === "keydown");
|
|
119
117
|
const blur = () => setShiftHeld(false);
|
|
120
|
-
window.addEventListener("keydown",
|
|
121
|
-
window.addEventListener("keyup",
|
|
118
|
+
window.addEventListener("keydown", key);
|
|
119
|
+
window.addEventListener("keyup", key);
|
|
122
120
|
window.addEventListener("blur", blur);
|
|
123
121
|
return () => {
|
|
124
|
-
window.removeEventListener("keydown",
|
|
125
|
-
window.removeEventListener("keyup",
|
|
122
|
+
window.removeEventListener("keydown", key);
|
|
123
|
+
window.removeEventListener("keyup", key);
|
|
126
124
|
window.removeEventListener("blur", blur);
|
|
127
125
|
};
|
|
128
126
|
});
|
|
129
127
|
|
|
130
128
|
const [showPopover, setShowPopover] = useState(false);
|
|
131
|
-
const [showShortcutHint, setShowShortcutHint] = useState(true);
|
|
132
129
|
const [kfContextMenu, setKfContextMenu] = useState<KeyframeDiamondContextMenuState | null>(null);
|
|
133
130
|
const [clipContextMenu, setClipContextMenu] = useState<{
|
|
134
131
|
x: number;
|
|
135
132
|
y: number;
|
|
136
133
|
element: TimelineElement;
|
|
137
134
|
} | null>(null);
|
|
138
|
-
const [viewportWidth, setViewportWidth] = useState(0);
|
|
139
|
-
const roRef = useRef<ResizeObserver | null>(null);
|
|
140
|
-
const shortcutHintRafRef = useRef(0);
|
|
141
|
-
|
|
142
|
-
const syncShortcutHintVisibility = useCallback(() => {
|
|
143
|
-
const scroll = scrollRef.current;
|
|
144
|
-
setShowShortcutHint(
|
|
145
|
-
scroll ? shouldShowTimelineShortcutHint(scroll.scrollHeight, scroll.clientHeight) : true,
|
|
146
|
-
);
|
|
147
|
-
}, []);
|
|
148
|
-
|
|
149
|
-
const scheduleShortcutHintVisibilitySync = useCallback(() => {
|
|
150
|
-
if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
|
|
151
|
-
shortcutHintRafRef.current = requestAnimationFrame(() => {
|
|
152
|
-
shortcutHintRafRef.current = 0;
|
|
153
|
-
syncShortcutHintVisibility();
|
|
154
|
-
});
|
|
155
|
-
}, [syncShortcutHintVisibility]);
|
|
156
135
|
|
|
157
136
|
const setContainerRef = useCallback((el: HTMLDivElement | null) => {
|
|
158
137
|
containerRef.current = el;
|
|
159
138
|
}, []);
|
|
160
139
|
|
|
161
|
-
// Last horizontal scroll offset,
|
|
162
|
-
// a scroll jump); with the pinned zoom this keeps the user parked at the same spot after edits.
|
|
140
|
+
// Last horizontal scroll offset, restored across the post-edit iframe reload (pinned zoom).
|
|
163
141
|
const lastScrollLeftRef = useRef(0);
|
|
164
|
-
const setScrollRef = useCallback(
|
|
165
|
-
(el: HTMLDivElement | null) => {
|
|
166
|
-
if (roRef.current) {
|
|
167
|
-
roRef.current.disconnect();
|
|
168
|
-
roRef.current = null;
|
|
169
|
-
}
|
|
170
|
-
scrollRef.current = el;
|
|
171
|
-
if (!el) return;
|
|
172
|
-
|
|
173
|
-
const syncScrollViewport = () => {
|
|
174
|
-
setViewportWidth(el.clientWidth);
|
|
175
|
-
scheduleShortcutHintVisibilitySync();
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
syncScrollViewport();
|
|
179
|
-
roRef.current = new ResizeObserver(syncScrollViewport);
|
|
180
|
-
roRef.current.observe(el);
|
|
181
|
-
},
|
|
182
|
-
[scheduleShortcutHintVisibilitySync],
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
useMountEffect(() => () => {
|
|
186
|
-
roRef.current?.disconnect();
|
|
187
|
-
if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
|
|
188
|
-
});
|
|
189
142
|
|
|
190
143
|
const effectiveDuration = useMemo(() => {
|
|
191
144
|
const safeDur = Number.isFinite(duration) ? duration : 0;
|
|
192
145
|
if (rawElements.length === 0) return safeDur;
|
|
193
|
-
const
|
|
194
|
-
const result = Math.max(safeDur, maxEnd);
|
|
146
|
+
const result = Math.max(safeDur, ...rawElements.map((el) => el.start + el.duration));
|
|
195
147
|
return Number.isFinite(result) ? result : safeDur;
|
|
196
148
|
}, [rawElements, duration]);
|
|
197
149
|
|
|
198
|
-
const tracks =
|
|
199
|
-
const map = new Map<number, typeof expandedElements>();
|
|
200
|
-
for (const el of expandedElements) {
|
|
201
|
-
const list = map.get(el.track) ?? [];
|
|
202
|
-
list.push(el);
|
|
203
|
-
map.set(el.track, list);
|
|
204
|
-
}
|
|
205
|
-
return Array.from(map.entries()).sort(([a], [b]) => a - b);
|
|
206
|
-
}, [expandedElements]);
|
|
207
|
-
|
|
208
|
-
const trackStyles = useMemo(() => {
|
|
209
|
-
const map = new Map<number, TrackVisualStyle>();
|
|
210
|
-
for (const [trackNum, els] of tracks) {
|
|
211
|
-
map.set(trackNum, getTrackStyle(els[0]?.tag ?? ""));
|
|
212
|
-
}
|
|
213
|
-
return map;
|
|
214
|
-
}, [tracks]);
|
|
215
|
-
|
|
216
|
-
const trackOrder = useMemo(() => tracks.map(([trackNum]) => trackNum), [tracks]);
|
|
150
|
+
const { tracks, trackStyles, trackOrder } = useTimelineTrackDerivations(expandedElements);
|
|
217
151
|
const trackOrderRef = useRef(trackOrder);
|
|
218
152
|
trackOrderRef.current = trackOrder;
|
|
219
153
|
const expandedElementsRef = useRef(expandedElements);
|
|
@@ -222,8 +156,7 @@ export const Timeline = memo(function Timeline({
|
|
|
222
156
|
const ppsRef = useRef(100);
|
|
223
157
|
const durationRef = useRef(effectiveDuration);
|
|
224
158
|
durationRef.current = effectiveDuration;
|
|
225
|
-
// Declared
|
|
226
|
-
// wrappers can close over it; `fitPpsRef.current` is refreshed each render.
|
|
159
|
+
// Declared before the fitPps derivation so the edit-pin wrappers can close over it.
|
|
227
160
|
const fitPpsRef = useRef(100);
|
|
228
161
|
|
|
229
162
|
const {
|
|
@@ -252,6 +185,22 @@ export const Timeline = memo(function Timeline({
|
|
|
252
185
|
expandedElementsRef,
|
|
253
186
|
});
|
|
254
187
|
|
|
188
|
+
const {
|
|
189
|
+
gapMenuModel,
|
|
190
|
+
gapHighlight,
|
|
191
|
+
setHoveredGapAction,
|
|
192
|
+
openGapMenu,
|
|
193
|
+
dismissGapMenu,
|
|
194
|
+
closeTrackGap,
|
|
195
|
+
closeAllTrackGaps,
|
|
196
|
+
} = useTrackGapMenu({
|
|
197
|
+
tracks,
|
|
198
|
+
expandedElementsRef,
|
|
199
|
+
trackOrderRef,
|
|
200
|
+
onMoveElement: pinnedOnMoveElement,
|
|
201
|
+
onMoveElements: pinnedOnMoveElements,
|
|
202
|
+
});
|
|
203
|
+
|
|
255
204
|
const {
|
|
256
205
|
draggedClip,
|
|
257
206
|
setDraggedClip,
|
|
@@ -293,6 +242,11 @@ export const Timeline = memo(function Timeline({
|
|
|
293
242
|
}, [draggedClip, trackOrder]);
|
|
294
243
|
|
|
295
244
|
const totalH = getTimelineCanvasHeight(displayTrackOrder.length);
|
|
245
|
+
const { viewportWidth, showShortcutHint, setScrollRef } = useTimelineScrollViewport(scrollRef, [
|
|
246
|
+
timelineReady,
|
|
247
|
+
expandedElements.length,
|
|
248
|
+
totalH,
|
|
249
|
+
]);
|
|
296
250
|
const keyframeCache = usePlayerStore((s) => s.keyframeCache);
|
|
297
251
|
const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
|
|
298
252
|
const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
|
|
@@ -328,6 +282,16 @@ export const Timeline = memo(function Timeline({
|
|
|
328
282
|
lastScrollLeftRef,
|
|
329
283
|
});
|
|
330
284
|
|
|
285
|
+
const laneGapStrips = useTimelineGapHighlights({
|
|
286
|
+
gapHighlight,
|
|
287
|
+
tracks,
|
|
288
|
+
selectedElementId,
|
|
289
|
+
selectedElementIds,
|
|
290
|
+
expandedElements,
|
|
291
|
+
dragActive: draggedClip?.started === true || resizingClip != null,
|
|
292
|
+
displayDuration,
|
|
293
|
+
});
|
|
294
|
+
|
|
331
295
|
const { seekFromX, autoScrollDuringDrag, dragScrollRaf } = useTimelinePlayhead({
|
|
332
296
|
playheadRef,
|
|
333
297
|
scrollRef,
|
|
@@ -379,8 +343,7 @@ export const Timeline = memo(function Timeline({
|
|
|
379
343
|
trackOrderRef,
|
|
380
344
|
onSelectElement,
|
|
381
345
|
});
|
|
382
|
-
|
|
383
|
-
setRangeSelectionRef.current = setRangeSelection;
|
|
346
|
+
setRangeSelectionRef.current = setRangeSelection; // stable ref consumed by useTimelineClipDrag
|
|
384
347
|
|
|
385
348
|
const prevSelectedRef = useRef(selectedElementRef.current);
|
|
386
349
|
// eslint-disable-next-line no-restricted-syntax, react-hooks/exhaustive-deps
|
|
@@ -402,10 +365,6 @@ export const Timeline = memo(function Timeline({
|
|
|
402
365
|
);
|
|
403
366
|
const majorTickInterval = major.length >= 2 ? major[1] - major[0] : effectiveDuration;
|
|
404
367
|
|
|
405
|
-
useEffect(() => {
|
|
406
|
-
syncShortcutHintVisibility();
|
|
407
|
-
}, [syncShortcutHintVisibility, timelineReady, expandedElements.length, totalH]);
|
|
408
|
-
|
|
409
368
|
const getPreviewElement = useCallback(
|
|
410
369
|
(element: TimelineElement): TimelineElement => {
|
|
411
370
|
if (
|
|
@@ -459,8 +418,7 @@ export const Timeline = memo(function Timeline({
|
|
|
459
418
|
tabIndex={-1}
|
|
460
419
|
className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-none`}
|
|
461
420
|
onScroll={(e) => {
|
|
462
|
-
|
|
463
|
-
lastScrollLeftRef.current = e.currentTarget.scrollLeft;
|
|
421
|
+
lastScrollLeftRef.current = e.currentTarget.scrollLeft; // restored across post-edit reload
|
|
464
422
|
}}
|
|
465
423
|
onDragOver={handleAssetDragOver}
|
|
466
424
|
onDragLeave={() => clearDropPreview()}
|
|
@@ -468,7 +426,8 @@ export const Timeline = memo(function Timeline({
|
|
|
468
426
|
onPointerDown={(e) => {
|
|
469
427
|
if (activeTool === "razor" && e.shiftKey && e.button === 0 && scrollRef.current) {
|
|
470
428
|
const rect = scrollRef.current.getBoundingClientRect();
|
|
471
|
-
const x =
|
|
429
|
+
const x =
|
|
430
|
+
e.clientX - rect.left + scrollRef.current.scrollLeft - GUTTER - TRACKS_LEFT_PAD;
|
|
472
431
|
const splitTime = Math.max(0, x / pps);
|
|
473
432
|
onRazorSplitAll?.(splitTime);
|
|
474
433
|
return;
|
|
@@ -489,6 +448,7 @@ export const Timeline = memo(function Timeline({
|
|
|
489
448
|
majorTickInterval={majorTickInterval}
|
|
490
449
|
rangeSelection={rangeSelection}
|
|
491
450
|
marqueeRect={marqueeRect}
|
|
451
|
+
laneGapStrips={laneGapStrips}
|
|
492
452
|
theme={theme}
|
|
493
453
|
displayTrackOrder={displayTrackOrder}
|
|
494
454
|
trackOrder={trackOrder}
|
|
@@ -560,8 +520,14 @@ export const Timeline = memo(function Timeline({
|
|
|
560
520
|
e.preventDefault();
|
|
561
521
|
setSelectedElementId(el.key ?? el.id);
|
|
562
522
|
onSelectElement?.(el);
|
|
523
|
+
dismissGapMenu();
|
|
563
524
|
setClipContextMenu({ x: e.clientX, y: e.clientY, element: el });
|
|
564
525
|
}}
|
|
526
|
+
onContextMenuLane={(e, track, time) => {
|
|
527
|
+
if (draggedClip?.started || resizingClip) return;
|
|
528
|
+
setClipContextMenu(null);
|
|
529
|
+
openGapMenu({ x: e.clientX, y: e.clientY, track, time });
|
|
530
|
+
}}
|
|
565
531
|
/>
|
|
566
532
|
{activeTool === "razor" && razorGuideX !== null && (
|
|
567
533
|
<div
|
|
@@ -594,6 +560,11 @@ export const Timeline = memo(function Timeline({
|
|
|
594
560
|
onSplitElement={onSplitElement}
|
|
595
561
|
pinZoomBeforeEdit={pinZoomBeforeEdit}
|
|
596
562
|
onDeleteElement={_onDeleteElement}
|
|
563
|
+
gapContextMenu={gapMenuModel}
|
|
564
|
+
onDismissGapContextMenu={dismissGapMenu}
|
|
565
|
+
onCloseTrackGap={closeTrackGap}
|
|
566
|
+
onCloseAllTrackGaps={closeAllTrackGaps}
|
|
567
|
+
onHoverGapAction={setHoveredGapAction}
|
|
597
568
|
/>
|
|
598
569
|
</div>
|
|
599
570
|
);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
CLIP_Y,
|
|
11
11
|
TRACKS_TOP_PAD,
|
|
12
12
|
TRACKS_BOTTOM_PAD,
|
|
13
|
+
TRACKS_LEFT_PAD,
|
|
13
14
|
PLAYHEAD_HEAD_W,
|
|
14
15
|
getTimelinePlayheadLeft,
|
|
15
16
|
getTimelineRowTop,
|
|
@@ -23,6 +24,7 @@ import { TimelineClip } from "./TimelineClip";
|
|
|
23
24
|
import { TimelineLanes, type TimelineLaneBaseProps } from "./TimelineLanes";
|
|
24
25
|
import { renderClipChildren } from "./timelineClipChildren";
|
|
25
26
|
import { useTimelineRevealClip } from "./useTimelineRevealClip";
|
|
27
|
+
import type { TimelineLaneGapStrips } from "./useTimelineGapHighlights";
|
|
26
28
|
|
|
27
29
|
interface TimelineCanvasProps extends TimelineLaneBaseProps {
|
|
28
30
|
major: number[];
|
|
@@ -37,6 +39,8 @@ interface TimelineCanvasProps extends TimelineLaneBaseProps {
|
|
|
37
39
|
/** Playhead is being actively scrubbed — fills the grab-handle head. */
|
|
38
40
|
isScrubbing: boolean;
|
|
39
41
|
playheadRef: React.RefObject<HTMLDivElement | null>;
|
|
42
|
+
/** Gap strips: loud on gap-menu-row hover, quiet on the selected clip's lane. */
|
|
43
|
+
laneGapStrips: TimelineLaneGapStrips[];
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvasProps) {
|
|
@@ -97,7 +101,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
97
101
|
return (
|
|
98
102
|
<div
|
|
99
103
|
className="relative"
|
|
100
|
-
style={{ height: props.totalH, width: GUTTER + props.trackContentWidth }}
|
|
104
|
+
style={{ height: props.totalH, width: GUTTER + TRACKS_LEFT_PAD + props.trackContentWidth }}
|
|
101
105
|
>
|
|
102
106
|
<TimelineRuler
|
|
103
107
|
major={props.major}
|
|
@@ -131,6 +135,31 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
131
135
|
new bottom track comfortably (see TRACKS_BOTTOM_PAD / getTimelineCanvasHeight). */}
|
|
132
136
|
<div aria-hidden="true" style={{ height: TRACKS_BOTTOM_PAD }} />
|
|
133
137
|
|
|
138
|
+
{/* Gap strips — loud dashed fill for the gap(s) a hovered "Close gap(s)"
|
|
139
|
+
menu row would collapse; a quiet tint for every gap on the selected
|
|
140
|
+
clip's lane. Geometry mirrors the drop placeholder (row top + clip
|
|
141
|
+
inset) so strips sit exactly where a clip body would. */}
|
|
142
|
+
{props.laneGapStrips.map((strip) => {
|
|
143
|
+
const rowIndex = displayTrackOrder.indexOf(strip.track);
|
|
144
|
+
if (rowIndex < 0) return null;
|
|
145
|
+
const loud = strip.kind === "hover";
|
|
146
|
+
return strip.intervals.map((gap) => (
|
|
147
|
+
<div
|
|
148
|
+
key={`gap-${strip.kind}-${strip.track}-${gap.start}`}
|
|
149
|
+
className="pointer-events-none absolute"
|
|
150
|
+
style={{
|
|
151
|
+
top: getTimelineRowTop(rowIndex) + CLIP_Y,
|
|
152
|
+
left: GUTTER + TRACKS_LEFT_PAD + gap.start * props.pps,
|
|
153
|
+
width: Math.max((gap.end - gap.start) * props.pps, 2),
|
|
154
|
+
height: TRACK_H - CLIP_Y * 2,
|
|
155
|
+
background: loud ? "rgba(60,230,172,0.18)" : "rgba(60,230,172,0.055)",
|
|
156
|
+
borderRadius: 4,
|
|
157
|
+
zIndex: 25,
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
));
|
|
161
|
+
})}
|
|
162
|
+
|
|
134
163
|
{/* Drop placeholder — a clip-sized slot at the exact landing spot (target
|
|
135
164
|
lane + snapped start), parallel to the ghost. Hidden in insert mode. */}
|
|
136
165
|
{draggedClip?.started && draggedClip.insertRow == null && draggedRowIndex >= 0 && (
|
|
@@ -138,7 +167,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
138
167
|
className="absolute pointer-events-none"
|
|
139
168
|
style={{
|
|
140
169
|
top: getTimelineRowTop(draggedRowIndex) + CLIP_Y,
|
|
141
|
-
left: GUTTER + draggedClip.previewStart * props.pps,
|
|
170
|
+
left: GUTTER + TRACKS_LEFT_PAD + draggedClip.previewStart * props.pps,
|
|
142
171
|
width: Math.max(draggedClip.element.duration * props.pps, 4),
|
|
143
172
|
height: TRACK_H - CLIP_Y * 2,
|
|
144
173
|
border: "1px solid rgba(60,230,172,0.55)",
|
|
@@ -156,7 +185,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
156
185
|
className="absolute pointer-events-none"
|
|
157
186
|
style={{
|
|
158
187
|
top: getTimelineRowTop(draggedClip.insertRow) - 0.5,
|
|
159
|
-
left: GUTTER,
|
|
188
|
+
left: GUTTER + TRACKS_LEFT_PAD,
|
|
160
189
|
width: props.trackContentWidth,
|
|
161
190
|
height: 1,
|
|
162
191
|
background: "#3CE6AC",
|
|
@@ -171,7 +200,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
171
200
|
<div
|
|
172
201
|
className="absolute pointer-events-none"
|
|
173
202
|
style={{
|
|
174
|
-
left: GUTTER + draggedClip.snapTime * props.pps,
|
|
203
|
+
left: GUTTER + TRACKS_LEFT_PAD + draggedClip.snapTime * props.pps,
|
|
175
204
|
top: RULER_H,
|
|
176
205
|
bottom: 0,
|
|
177
206
|
width: 1,
|
|
@@ -251,7 +280,9 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
251
280
|
className="absolute pointer-events-none"
|
|
252
281
|
style={{
|
|
253
282
|
left:
|
|
254
|
-
GUTTER +
|
|
283
|
+
GUTTER +
|
|
284
|
+
TRACKS_LEFT_PAD +
|
|
285
|
+
Math.min(props.rangeSelection.start, props.rangeSelection.end) * props.pps,
|
|
255
286
|
width: Math.abs(props.rangeSelection.end - props.rangeSelection.start) * props.pps,
|
|
256
287
|
top: RULER_H,
|
|
257
288
|
bottom: 0,
|
|
@@ -6,7 +6,7 @@ import { TimelineClipDiamonds } from "./TimelineClipDiamonds";
|
|
|
6
6
|
import type { MusicBeatAnalysis } from "@hyperframes/core/beats";
|
|
7
7
|
import { getTimelineEditCapabilities, resolveBlockedTimelineEditIntent } from "./timelineEditing";
|
|
8
8
|
import type { TimelineTheme } from "./timelineTheme";
|
|
9
|
-
import { GUTTER, TRACK_H, CLIP_Y, CLIP_HANDLE_W } from "./timelineLayout";
|
|
9
|
+
import { GUTTER, TRACK_H, TRACKS_LEFT_PAD, CLIP_Y, CLIP_HANDLE_W } from "./timelineLayout";
|
|
10
10
|
import {
|
|
11
11
|
usePlayerStore,
|
|
12
12
|
type TimelineElement,
|
|
@@ -80,6 +80,12 @@ export interface TimelineLaneBaseProps {
|
|
|
80
80
|
toClipPercentage: number,
|
|
81
81
|
) => void;
|
|
82
82
|
onContextMenuClip?: (e: React.MouseEvent, element: TimelineElement) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Right-click on EMPTY lane space (not on a clip — those preventDefault
|
|
85
|
+
* before this fires — not the gutter/ruler, not below the lanes). `time` is
|
|
86
|
+
* the timeline time (seconds) under the pointer on that lane.
|
|
87
|
+
*/
|
|
88
|
+
onContextMenuLane?: (e: React.MouseEvent, track: number, time: number) => void;
|
|
83
89
|
beatAnalysis?: MusicBeatAnalysis | null;
|
|
84
90
|
}
|
|
85
91
|
|
|
@@ -133,6 +139,7 @@ export function TimelineLanes({
|
|
|
133
139
|
onContextMenuKeyframe,
|
|
134
140
|
onMoveKeyframe,
|
|
135
141
|
onContextMenuClip,
|
|
142
|
+
onContextMenuLane,
|
|
136
143
|
beatAnalysis,
|
|
137
144
|
onToggleTrackHidden,
|
|
138
145
|
onResizeElement,
|
|
@@ -167,21 +174,14 @@ export function TimelineLanes({
|
|
|
167
174
|
const isTrackHidden = els.length > 0 && els.every((element) => element.hidden === true);
|
|
168
175
|
const isAudioTrack = els.length > 0 && els.some(isAudioTimelineElement);
|
|
169
176
|
return (
|
|
170
|
-
<div
|
|
171
|
-
key={trackNum}
|
|
172
|
-
className="relative flex"
|
|
173
|
-
style={{
|
|
174
|
-
height: TRACK_H,
|
|
175
|
-
background: rowBackground,
|
|
176
|
-
borderBottom: `1px solid ${theme.rowBorder}`,
|
|
177
|
-
}}
|
|
178
|
-
>
|
|
177
|
+
<div key={trackNum} className="relative flex" style={{ height: TRACK_H }}>
|
|
179
178
|
<div
|
|
180
179
|
className="sticky left-0 z-[12] flex-shrink-0 flex flex-col items-center justify-center gap-0.5"
|
|
181
180
|
style={{
|
|
182
181
|
width: GUTTER,
|
|
183
182
|
background: theme.gutterBackground,
|
|
184
183
|
borderRight: `1px solid ${theme.gutterBorder}`,
|
|
184
|
+
borderBottom: `1px solid ${theme.rowBorder}`,
|
|
185
185
|
}}
|
|
186
186
|
>
|
|
187
187
|
{isAudioTrack && (
|
|
@@ -211,13 +211,35 @@ export function TimelineLanes({
|
|
|
211
211
|
)}
|
|
212
212
|
</button>
|
|
213
213
|
</div>
|
|
214
|
+
{/* Left breathing pad — empty lane surface before t=0, scrolling
|
|
215
|
+
with the content (the horizontal TRACKS_TOP_PAD). Sits OUTSIDE
|
|
216
|
+
the time-mapped content div so clip/beat/menu math stays
|
|
217
|
+
content-relative (clip left = t·pps). */}
|
|
218
|
+
<div
|
|
219
|
+
aria-hidden="true"
|
|
220
|
+
className="flex-shrink-0"
|
|
221
|
+
style={{ width: TRACKS_LEFT_PAD }}
|
|
222
|
+
/>
|
|
214
223
|
<div
|
|
215
224
|
style={{
|
|
216
225
|
width: trackContentWidth,
|
|
226
|
+
background: rowBackground,
|
|
227
|
+
borderBottom: `1px solid ${theme.rowBorder}`,
|
|
217
228
|
opacity: isTrackHidden ? 0.35 : 1,
|
|
218
229
|
transition: "opacity 120ms ease",
|
|
219
230
|
}}
|
|
220
231
|
className="relative"
|
|
232
|
+
onContextMenu={(e: React.MouseEvent) => {
|
|
233
|
+
// Clip / keyframe-diamond context menus preventDefault at the
|
|
234
|
+
// target before this bubble handler runs — respect them so a
|
|
235
|
+
// right-click on a clip never also opens the gap menu.
|
|
236
|
+
if (e.defaultPrevented || !onContextMenuLane) return;
|
|
237
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
238
|
+
const time = (e.clientX - rect.left) / pps;
|
|
239
|
+
if (time < 0) return;
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
onContextMenuLane(e, trackNum, time);
|
|
242
|
+
}}
|
|
221
243
|
>
|
|
222
244
|
{/* Faint beat lines in every track's background (behind the clips);
|
|
223
245
|
the active move-snap target is highlighted. */}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type KeyframeDiamondContextMenuState,
|
|
9
9
|
} from "./KeyframeDiamondContextMenu";
|
|
10
10
|
import { ClipContextMenu } from "./ClipContextMenu";
|
|
11
|
+
import { TrackGapContextMenu } from "./TrackGapContextMenu";
|
|
11
12
|
import { TimelineShortcutHint } from "./TimelineShortcutHint";
|
|
12
13
|
|
|
13
14
|
interface ClipContextMenuState {
|
|
@@ -16,6 +17,16 @@ interface ClipContextMenuState {
|
|
|
16
17
|
element: TimelineElement;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/** Resolved model for the empty-lane-space (track gap) context menu. */
|
|
21
|
+
interface TrackGapContextMenuState {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
gapWidth: number | null;
|
|
25
|
+
canCloseGap: boolean;
|
|
26
|
+
canCloseAllGaps: boolean;
|
|
27
|
+
hasAnyGaps: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
interface TimelineOverlaysProps {
|
|
20
31
|
theme: TimelineTheme;
|
|
21
32
|
showShortcutHint: boolean;
|
|
@@ -36,6 +47,11 @@ interface TimelineOverlaysProps {
|
|
|
36
47
|
onSplitElement: TimelineEditCallbacks["onSplitElement"];
|
|
37
48
|
pinZoomBeforeEdit: () => void;
|
|
38
49
|
onDeleteElement?: (element: TimelineElement) => Promise<void> | void;
|
|
50
|
+
gapContextMenu: TrackGapContextMenuState | null;
|
|
51
|
+
onDismissGapContextMenu: () => void;
|
|
52
|
+
onCloseTrackGap: () => void;
|
|
53
|
+
onCloseAllTrackGaps: () => void;
|
|
54
|
+
onHoverGapAction: (action: "close-gap" | "close-all" | null) => void;
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
// The timeline's floating overlays, rendered as siblings above the scroll area:
|
|
@@ -61,6 +77,11 @@ export function TimelineOverlays({
|
|
|
61
77
|
onSplitElement,
|
|
62
78
|
pinZoomBeforeEdit,
|
|
63
79
|
onDeleteElement,
|
|
80
|
+
gapContextMenu,
|
|
81
|
+
onDismissGapContextMenu,
|
|
82
|
+
onCloseTrackGap,
|
|
83
|
+
onCloseAllTrackGaps,
|
|
84
|
+
onHoverGapAction,
|
|
64
85
|
}: TimelineOverlaysProps) {
|
|
65
86
|
return (
|
|
66
87
|
<>
|
|
@@ -117,6 +138,21 @@ export function TimelineOverlays({
|
|
|
117
138
|
}}
|
|
118
139
|
/>
|
|
119
140
|
)}
|
|
141
|
+
|
|
142
|
+
{gapContextMenu && (
|
|
143
|
+
<TrackGapContextMenu
|
|
144
|
+
x={gapContextMenu.x}
|
|
145
|
+
y={gapContextMenu.y}
|
|
146
|
+
gapWidth={gapContextMenu.gapWidth}
|
|
147
|
+
canCloseGap={gapContextMenu.canCloseGap}
|
|
148
|
+
canCloseAllGaps={gapContextMenu.canCloseAllGaps}
|
|
149
|
+
hasAnyGaps={gapContextMenu.hasAnyGaps}
|
|
150
|
+
onClose={onDismissGapContextMenu}
|
|
151
|
+
onCloseGap={onCloseTrackGap}
|
|
152
|
+
onCloseAllGaps={onCloseAllTrackGaps}
|
|
153
|
+
onHoverAction={onHoverGapAction}
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
120
156
|
</>
|
|
121
157
|
);
|
|
122
158
|
}
|