@hyperframes/studio 0.8.50 → 0.8.52
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-CxxDxHTW.js → hyperframes-player-I5YRzB02.js} +3 -3
- package/dist/assets/{index-Dt3qBI-9.js → index-BTAfgesM.js} +1 -1
- package/dist/assets/index-CxAu_Okg.css +1 -0
- package/dist/assets/index-DocJlW2v.js +376 -0
- package/dist/assets/{index-DUAIWxkI.js → index-Du5x7KUR.js} +1 -1
- package/dist/index.d.ts +419 -20
- package/dist/index.html +2 -2
- package/dist/index.js +7496 -8288
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +13 -12
- package/src/App.tsx +131 -143
- package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
- package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
- package/src/captions/components/CaptionOverlay.tsx +8 -4
- package/src/captions/components/CaptionOverlayUtils.ts +2 -3
- package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
- package/src/captions/components/CaptionTimeline.tsx +1 -1
- package/src/captions/components/shared.tsx +2 -2
- package/src/components/AskAgentModal.tsx +5 -5
- package/src/components/EditorShell.tsx +8 -7
- package/src/components/ExternalFileConflictBanner.tsx +16 -16
- package/src/components/LintModal.tsx +6 -16
- package/src/components/MediaPreview.tsx +2 -2
- package/src/components/RightPanelTabs.test.tsx +151 -0
- package/src/components/RightPanelTabs.tsx +59 -0
- package/src/components/SaveQueuePausedBanner.tsx +2 -2
- package/src/components/StudioHeader.dom.test.tsx +181 -0
- package/src/components/StudioHeader.tsx +110 -210
- package/src/components/StudioLeftSidebar.tsx +7 -4
- package/src/components/StudioOverlays.tsx +1 -1
- package/src/components/StudioRightPanel.tsx +51 -39
- package/src/components/StudioToast.tsx +2 -2
- package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
- package/src/components/TimelineHistoryButtons.tsx +54 -0
- package/src/components/TimelineToolbar.test.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +8 -12
- package/src/components/dock/Dock.test.tsx +260 -0
- package/src/components/dock/Dock.tsx +281 -0
- package/src/components/dock/dock.css +28 -0
- package/src/components/dock/dockAccessibility.test.ts +166 -0
- package/src/components/dock/dockAccessibility.ts +98 -0
- package/src/components/dock/dockLayout.test.ts +134 -0
- package/src/components/dock/dockLayout.ts +105 -0
- package/src/components/dock/dockLayoutSchema.ts +66 -0
- package/src/components/dock/dockLayoutStore.ts +91 -0
- package/src/components/dock/dockTestHarness.ts +11 -0
- package/src/components/dock/panelRegistry.ts +67 -0
- package/src/components/editor/AnimationCard.tsx +4 -4
- package/src/components/editor/AnimationCardParts.tsx +6 -6
- package/src/components/editor/BlockParamsPanel.tsx +4 -4
- package/src/components/editor/BorderRadiusEditor.tsx +1 -1
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +1 -1
- package/src/components/editor/EaseParamFields.tsx +5 -5
- package/src/components/editor/FileTree.tsx +4 -4
- package/src/components/editor/FileTreeIcons.tsx +1 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -13
- package/src/components/editor/InlineTextToolbar.tsx +1 -1
- package/src/components/editor/InspectorHeaderActions.tsx +3 -3
- package/src/components/editor/KeyframeDiamond.tsx +1 -1
- package/src/components/editor/KeyframeEaseList.tsx +1 -1
- package/src/components/editor/LayersPanel.test.ts +97 -1
- package/src/components/editor/LayersPanel.tsx +7 -5
- package/src/components/editor/MarqueeOverlay.tsx +1 -1
- package/src/components/editor/OffCanvasIndicators.tsx +1 -1
- package/src/components/editor/PreviewGuides.tsx +1 -1
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +2 -2
- package/src/components/editor/TimelineFxPopover.tsx +1 -1
- package/src/components/editor/TopologyLens.test.tsx +28 -0
- package/src/components/editor/TopologyLens.tsx +7 -5
- package/src/components/editor/Transform3DCube.tsx +2 -2
- package/src/components/editor/audioFxRevealTarget.ts +1 -0
- package/src/components/editor/domEditing.test.ts +1 -1
- package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
- package/src/components/editor/domEditingAgentPrompt.ts +4 -3
- package/src/components/editor/propertyPanelColor.tsx +11 -11
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
- package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
- package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +1 -1
- package/src/components/editor/propertyPanelFill.tsx +4 -4
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
- package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
- package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +5 -5
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +6 -6
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
- package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
- package/src/components/editor/propertyPanelSections.tsx +8 -8
- package/src/components/editor/useColorGradingController.test.ts +38 -0
- package/src/components/editor/useColorGradingController.ts +9 -1
- package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
- package/src/components/editor/useColorGradingPreviews.ts +11 -1
- package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
- package/src/components/editor/usePreviewCompositionRect.ts +21 -0
- package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
- package/src/components/feedback/feedbackTrigger.ts +0 -1
- package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
- package/src/components/nle/NLEPreview.tsx +3 -3
- package/src/components/nle/PreviewOverlays.tsx +6 -6
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/TimelinePane.tsx +3 -3
- package/src/components/nle/TimelineResizeDivider.tsx +2 -2
- package/src/components/panels/SlideshowPanel.tsx +2 -2
- package/src/components/panels/SlideshowSubPanels.tsx +20 -17
- package/src/components/panels/VariablesBindElement.tsx +2 -2
- package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
- package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +3 -3
- package/src/components/panels/VariablesValueControls.tsx +2 -2
- package/src/components/pointerTargetSize.test.tsx +8 -0
- package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
- package/src/components/renders/RenderQueue.test.tsx +7 -0
- package/src/components/renders/RenderQueue.tsx +7 -7
- package/src/components/renders/RenderQueueItem.tsx +6 -6
- package/src/components/sidebar/AssetCard.tsx +8 -8
- package/src/components/sidebar/AssetContextMenu.tsx +8 -8
- package/src/components/sidebar/AssetsTab.tsx +2 -2
- package/src/components/sidebar/AudioRow.tsx +6 -6
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.tsx +5 -5
- package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
- package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
- package/src/components/sidebar/LeftSidebar.tsx +19 -5
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/timelineToolbarStyles.ts +5 -0
- package/src/components/ui/Button.test.tsx +252 -0
- package/src/components/ui/Button.tsx +89 -126
- package/src/components/ui/IconButton.tsx +38 -0
- package/src/components/ui/Input.tsx +115 -0
- package/src/components/ui/Menu.test.tsx +374 -0
- package/src/components/ui/Menu.tsx +213 -0
- package/src/components/ui/NumberField.tsx +122 -0
- package/src/components/ui/Popover.tsx +74 -0
- package/src/components/ui/SearchInput.tsx +2 -2
- package/src/components/ui/Select.tsx +100 -0
- package/src/components/ui/Slider.tsx +153 -0
- package/src/components/ui/Tabs.test.tsx +98 -0
- package/src/components/ui/Tabs.tsx +71 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/Tooltip.test.tsx +110 -0
- package/src/components/ui/Tooltip.tsx +38 -112
- package/src/components/ui/cn.test.ts +30 -0
- package/src/components/ui/cn.ts +25 -0
- package/src/components/ui/index.ts +29 -2
- package/src/components/ui/mountHost.testHelpers.ts +24 -0
- package/src/components/ui/valueControls.test.tsx +524 -0
- package/src/hooks/useAppHotkeys.ts +21 -80
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- package/src/hooks/useLintModal.test.tsx +65 -0
- package/src/hooks/useLintModal.ts +3 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
- package/src/hooks/useSdkSession.ts +43 -10
- package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
- package/src/hooks/useServerConnection.ts +43 -10
- package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
- package/src/hooks/useTimelineAssetDropOps.ts +52 -11
- package/src/index.ts +51 -0
- package/src/main.tsx +9 -3
- package/src/player/components/AudioWaveform.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +3 -3
- package/src/player/components/ClipContextMenu.tsx +3 -3
- package/src/player/components/CompositionThumbnail.test.ts +0 -3
- package/src/player/components/CompositionThumbnail.tsx +3 -4
- package/src/player/components/EditModal.tsx +1 -1
- package/src/player/components/ImageThumbnail.tsx +1 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
- package/src/player/components/LayerDisclosureRow.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +1 -1
- package/src/player/components/ShortcutsPanel.tsx +9 -9
- package/src/player/components/SpeedMenu.tsx +3 -5
- package/src/player/components/Timeline.tsx +2 -1
- package/src/player/components/TimelineCanvas.tsx +21 -14
- package/src/player/components/TimelineClip.tsx +0 -1
- package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
- package/src/player/components/TimelineEmptyState.tsx +5 -5
- package/src/player/components/TimelineFxButton.tsx +6 -6
- package/src/player/components/TimelineGroupHeader.tsx +5 -5
- package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
- package/src/player/components/TimelineGroupRow.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +32 -0
- package/src/player/components/TimelineLanes.tsx +2 -5
- package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
- package/src/player/components/TimelineRuler.tsx +1 -1
- package/src/player/components/TimelineShortcutHint.tsx +1 -1
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
- package/src/player/components/TimelineTypes.ts +2 -1
- package/src/player/components/TrackGapContextMenu.tsx +1 -1
- package/src/player/components/VideoThumbnail.tsx +4 -2
- package/src/player/components/VolumeControl.tsx +2 -2
- package/src/player/components/timelineCallbacks.ts +8 -8
- package/src/player/components/timelineClipDragPreview.ts +51 -27
- package/src/player/components/timelineClipDragTypes.ts +3 -0
- package/src/player/components/timelineDragDrop.test.tsx +61 -1
- package/src/player/components/timelineDragDrop.ts +57 -15
- package/src/player/components/timelineLaneProps.ts +3 -0
- package/src/player/components/timelineSnapping.test.ts +27 -0
- package/src/player/components/timelineSnapping.ts +14 -1
- package/src/player/components/trackHeaderLabelRows.tsx +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
- package/src/player/components/useTimelineClipDrag.ts +30 -3
- package/src/player/lib/describeClips.ts +3 -54
- package/src/player/lib/playbackShortcuts.ts +2 -0
- package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
- package/src/player/store/previewIframeStore.test.tsx +29 -0
- package/src/player/store/previewIframeStore.ts +25 -0
- package/src/renderQueueExports.test.tsx +58 -0
- package/src/styles/classCandidates.test.ts +105 -0
- package/src/styles/classCandidates.ts +281 -0
- package/src/styles/hex-baseline.json +84 -0
- package/src/styles/hexRatchet.test.ts +201 -0
- package/src/styles/iconTokens.ts +20 -0
- package/src/styles/palette-reset.css +6 -0
- package/src/styles/studio.css +24 -21
- package/src/styles/styleSources.test.ts +20 -0
- package/src/styles/styleSources.ts +47 -0
- package/src/styles/tailwind-preset.shared.js +3 -0
- package/src/styles/theme.css +243 -0
- package/src/styles/theme.test.ts +183 -0
- package/src/styles/tokenGate.test.ts +109 -0
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioHelpers.test.ts +18 -0
- package/src/utils/studioHelpers.ts +11 -1
- package/src/utils/studioUiPreferences.ts +7 -0
- package/src/utils/timelineDropTrackInsert.test.ts +134 -0
- package/src/utils/timelineDropTrackInsert.ts +83 -0
- package/src/utils/typingTarget.ts +4 -0
- package/dist/assets/index-n2ml4tUn.js +0 -442
- package/dist/assets/index-pO6p0H27.css +0 -1
- package/src/components/PanelTabButton.tsx +0 -31
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
- package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
- package/src/components/storyboard/FramePoster.test.tsx +0 -61
- package/src/components/storyboard/FramePoster.tsx +0 -86
- package/src/components/storyboard/StoryboardDirection.tsx +0 -45
- package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
- package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
- package/src/components/storyboard/StoryboardGrid.tsx +0 -55
- package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
- package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
- package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
- package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
- package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
- package/src/components/storyboard/StoryboardView.tsx +0 -169
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
- package/src/components/storyboard/frameComments.test.ts +0 -95
- package/src/components/storyboard/frameComments.ts +0 -115
- package/src/components/storyboard/frameStatus.ts +0 -36
- package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
- package/src/components/storyboard/storyboardReviewStage.ts +0 -45
- package/src/components/storyboard/useFrameComments.ts +0 -98
- package/src/contexts/ViewModeContext.tsx +0 -142
- package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
- package/src/hooks/useProjectSignaturePoll.ts +0 -68
- package/src/hooks/useStoryboard.ts +0 -99
|
@@ -23,7 +23,7 @@ export const SpeedMenu = memo(function SpeedMenu({
|
|
|
23
23
|
const speedMenuContainerRef = useContextMenuDismiss(closeMenu);
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
|
-
<div ref={speedMenuContainerRef} className="relative
|
|
26
|
+
<div ref={speedMenuContainerRef} className="relative shrink-0">
|
|
27
27
|
<Tooltip label="Playback speed">
|
|
28
28
|
<button
|
|
29
29
|
type="button"
|
|
@@ -57,10 +57,8 @@ export const SpeedMenu = memo(function SpeedMenu({
|
|
|
57
57
|
setPlaybackRate(rate);
|
|
58
58
|
setShowSpeedMenu(false);
|
|
59
59
|
}}
|
|
60
|
-
className={`block w-full px-3 py-1.5 text-[11px] text-left font-mono tabular-nums transition-colors outline-
|
|
61
|
-
isCurrent
|
|
62
|
-
? "text-neutral-50 bg-white/[0.06]"
|
|
63
|
-
: "text-neutral-500 hover:bg-white/[0.04]"
|
|
60
|
+
className={`block w-full px-3 py-1.5 text-[11px] text-left font-mono tabular-nums transition-colors outline-hidden focus-visible:bg-white/4 ${
|
|
61
|
+
isCurrent ? "text-neutral-50 bg-white/6" : "text-neutral-500 hover:bg-white/4"
|
|
64
62
|
}`}
|
|
65
63
|
>
|
|
66
64
|
{rate}x
|
|
@@ -233,6 +233,7 @@ export const Timeline = memo(function Timeline({
|
|
|
233
233
|
onResizeElement: pinnedOnResizeElement,
|
|
234
234
|
onResizeElements: pinnedOnResizeElements,
|
|
235
235
|
onBlockedEditAttempt,
|
|
236
|
+
onSeek,
|
|
236
237
|
setShowPopover,
|
|
237
238
|
setRangeSelectionRef,
|
|
238
239
|
readZIndex: zSyncEnabled ? readClipZIndex : undefined,
|
|
@@ -464,7 +465,7 @@ export const Timeline = memo(function Timeline({
|
|
|
464
465
|
data-timeline-scroll-viewport
|
|
465
466
|
data-timeline-auto-scroll-left-inset={labelMode ? LABEL_COL_W : 0}
|
|
466
467
|
tabIndex={-1}
|
|
467
|
-
className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-
|
|
468
|
+
className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-hidden`}
|
|
468
469
|
onScroll={(e) => {
|
|
469
470
|
lastScrollLeftRef.current = e.currentTarget.scrollLeft; // restored across post-edit reload
|
|
470
471
|
recordTimelineScroll(e.currentTarget);
|
|
@@ -2,6 +2,7 @@ import { memo } from "react";
|
|
|
2
2
|
import { TimelineRuler } from "./TimelineRuler";
|
|
3
3
|
import { PlayheadIndicator } from "./PlayheadIndicator";
|
|
4
4
|
import type { TimelineRangeSelection } from "./timelineEditing";
|
|
5
|
+
import type { TimelineDropPlacement } from "./timelineCallbacks";
|
|
5
6
|
import {
|
|
6
7
|
RULER_H,
|
|
7
8
|
CLIP_Y,
|
|
@@ -23,6 +24,7 @@ import type { TimelineLaneBaseProps } from "./timelineLaneProps";
|
|
|
23
24
|
import type { TimelineLaneGapStrips } from "./useTimelineGapHighlights";
|
|
24
25
|
import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
|
|
25
26
|
import { TimelineGestureOverlay } from "./TimelineGestureOverlay";
|
|
27
|
+
import { resolveSnapGuide } from "./timelineSnapping";
|
|
26
28
|
|
|
27
29
|
interface TimelineCanvasProps extends TimelineLaneBaseProps {
|
|
28
30
|
major: number[];
|
|
@@ -40,14 +42,15 @@ interface TimelineCanvasProps extends TimelineLaneBaseProps {
|
|
|
40
42
|
/** Gap strips: loud on gap-menu-row hover, quiet on the selected clip's lane. */
|
|
41
43
|
laneGapStrips: TimelineLaneGapStrips[];
|
|
42
44
|
/** Landing spot of an outside drag in progress, or null. */
|
|
43
|
-
dropPreview:
|
|
45
|
+
dropPreview: TimelineDropPlacement | null;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// A dropped clip's length is unknown until it lands; the preview shows a default.
|
|
47
49
|
const DROP_PREVIEW_SECONDS = 3;
|
|
48
50
|
|
|
49
51
|
export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvasProps) {
|
|
50
|
-
const { draggedClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
|
|
52
|
+
const { draggedClip, resizingClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
|
|
53
|
+
const snapGuide = resolveSnapGuide(draggedClip, resizingClip);
|
|
51
54
|
const draggedRowIndex =
|
|
52
55
|
draggedClip?.started === true ? displayTrackOrder.indexOf(draggedClip.previewTrack) : -1;
|
|
53
56
|
const dropTrackIndex = props.dropPreview
|
|
@@ -55,6 +58,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
55
58
|
: -1;
|
|
56
59
|
// A track not in the order is a new track, drawn one row past the last lane.
|
|
57
60
|
const dropRowIndex = dropTrackIndex < 0 ? displayTrackOrder.length : dropTrackIndex;
|
|
61
|
+
const insertLineRow =
|
|
62
|
+
(draggedClip?.started ? draggedClip.insertRow : null) ?? props.dropPreview?.insertRow ?? null;
|
|
58
63
|
const draggedRowHeight = getTimelineRowHeight(draggedRowIndex, props.rowHeights);
|
|
59
64
|
// A clip bar in an EXPANDED row still renders at TRACK_H (the property lanes
|
|
60
65
|
// occupy the rest of the row — see TimelineLanes' clipHeight), so the drag
|
|
@@ -117,6 +122,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
117
122
|
|
|
118
123
|
<TimelineLanes
|
|
119
124
|
{...props}
|
|
125
|
+
snapGuide={snapGuide}
|
|
120
126
|
draggedElement={draggedElement}
|
|
121
127
|
multiDragPreview={multiDragPreview}
|
|
122
128
|
onToggleTrackHidden={onToggleTrackHidden}
|
|
@@ -173,8 +179,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
173
179
|
left: props.contentOrigin + draggedClip.previewStart * props.pps,
|
|
174
180
|
width: Math.max(draggedClip.element.duration * props.pps, 4),
|
|
175
181
|
height: draggedClipHeight,
|
|
176
|
-
border: "1px solid
|
|
177
|
-
background: "
|
|
182
|
+
border: "1px solid color-mix(in srgb, var(--color-accent) 55%, transparent)",
|
|
183
|
+
background: "color-mix(in srgb, var(--color-accent) 12%, transparent)",
|
|
178
184
|
borderRadius: 4,
|
|
179
185
|
zIndex: 30,
|
|
180
186
|
}}
|
|
@@ -183,7 +189,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
183
189
|
|
|
184
190
|
{/* Drop preview: where an asset or file dragged in from outside will land
|
|
185
191
|
(a row past the last lane means a new track). */}
|
|
186
|
-
{props.dropPreview && (
|
|
192
|
+
{props.dropPreview && props.dropPreview.insertRow == null && (
|
|
187
193
|
<div
|
|
188
194
|
aria-hidden="true"
|
|
189
195
|
data-testid="timeline-drop-preview"
|
|
@@ -193,8 +199,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
193
199
|
left: props.contentOrigin + props.dropPreview.start * props.pps,
|
|
194
200
|
width: DROP_PREVIEW_SECONDS * props.pps,
|
|
195
201
|
height: TRACK_H - CLIP_Y * 2,
|
|
196
|
-
border: "1px solid
|
|
197
|
-
background: "
|
|
202
|
+
border: "1px solid color-mix(in srgb, var(--color-accent) 55%, transparent)",
|
|
203
|
+
background: "color-mix(in srgb, var(--color-accent) 12%, transparent)",
|
|
198
204
|
borderRadius: 4,
|
|
199
205
|
zIndex: 30,
|
|
200
206
|
}}
|
|
@@ -203,11 +209,12 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
203
209
|
|
|
204
210
|
{/* Insertion line — a new track will be inserted at this boundary on drop.
|
|
205
211
|
Shown while the pointer is near a lane boundary (insert mode). */}
|
|
206
|
-
{
|
|
212
|
+
{insertLineRow != null && (
|
|
207
213
|
<div
|
|
214
|
+
data-testid="timeline-insert-line"
|
|
208
215
|
className="absolute pointer-events-none"
|
|
209
216
|
style={{
|
|
210
|
-
top: getTimelineRowTop(
|
|
217
|
+
top: getTimelineRowTop(insertLineRow, props.rowHeights) - 0.5,
|
|
211
218
|
left: props.contentOrigin,
|
|
212
219
|
width: props.trackContentWidth,
|
|
213
220
|
height: 1,
|
|
@@ -218,18 +225,18 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
218
225
|
/>
|
|
219
226
|
)}
|
|
220
227
|
|
|
221
|
-
{/* Snap guide for non-beat targets during clip
|
|
222
|
-
{
|
|
228
|
+
{/* Snap guide for non-beat targets during a clip move or trim */}
|
|
229
|
+
{snapGuide && snapGuide.type !== "beat" && (
|
|
223
230
|
<div
|
|
224
231
|
className="absolute pointer-events-none"
|
|
225
232
|
style={{
|
|
226
|
-
left: props.contentOrigin +
|
|
233
|
+
left: props.contentOrigin + snapGuide.time * props.pps,
|
|
227
234
|
top: RULER_H,
|
|
228
235
|
bottom: 0,
|
|
229
236
|
width: 1,
|
|
230
|
-
background:
|
|
237
|
+
background: snapGuide.type === "playhead" ? "#3CE6AC" : "rgba(255,255,255,0.6)",
|
|
231
238
|
boxShadow:
|
|
232
|
-
|
|
239
|
+
snapGuide.type === "playhead"
|
|
233
240
|
? "0 0 6px rgba(60,230,172,0.5)"
|
|
234
241
|
: "0 0 6px rgba(255,255,255,0.4)",
|
|
235
242
|
zIndex: 60,
|
|
@@ -71,7 +71,6 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
71
71
|
isSelected ? "is-selected" : "",
|
|
72
72
|
isHovered ? "is-hovered" : "",
|
|
73
73
|
isDragging ? "is-dragging" : "",
|
|
74
|
-
showDefaultText ? "" : "is-micro",
|
|
75
74
|
isAudioTimelineElement(el) ? "is-audio" : "",
|
|
76
75
|
]
|
|
77
76
|
.filter((className) => className.length > 0)
|
|
@@ -203,7 +203,7 @@ function SegmentEaseControl({
|
|
|
203
203
|
// for that overlay the button keeps its 16x16 hit area, which is WCAG's
|
|
204
204
|
// target-spacing exception: the neighbouring diamonds are themselves the
|
|
205
205
|
// reason it cannot grow, and stealing their clicks is the worse failure.
|
|
206
|
-
className={`absolute flex items-center justify-center rounded ${restingVisible ? "opacity-40" : "opacity-0"} transition-opacity hover:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 ${roomForFullTarget ? "before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']" : ""}`}
|
|
206
|
+
className={`absolute flex items-center justify-center rounded-sm ${restingVisible ? "opacity-40" : "opacity-0"} transition-opacity hover:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 ${roomForFullTarget ? "before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']" : ""}`}
|
|
207
207
|
style={{
|
|
208
208
|
left: "50%",
|
|
209
209
|
top: "50%",
|
|
@@ -19,7 +19,7 @@ export function TimelineEmptyState({
|
|
|
19
19
|
return (
|
|
20
20
|
<div
|
|
21
21
|
className={`h-full border-t bg-[#0a0a0b] flex flex-col select-none transition-colors duration-150 ${
|
|
22
|
-
isDragOver ? "border-studio-accent/50 bg-studio-accent/
|
|
22
|
+
isDragOver ? "border-studio-accent/50 bg-studio-accent/3" : "border-neutral-800/50"
|
|
23
23
|
}`}
|
|
24
24
|
onDragOver={onDragOver}
|
|
25
25
|
onDragLeave={onDragLeave}
|
|
@@ -27,7 +27,7 @@ export function TimelineEmptyState({
|
|
|
27
27
|
>
|
|
28
28
|
{/* Ruler */}
|
|
29
29
|
<div
|
|
30
|
-
className="
|
|
30
|
+
className="shrink-0 border-b border-neutral-800/40 flex items-end relative"
|
|
31
31
|
style={{ height: RULER_H, paddingLeft: GUTTER }}
|
|
32
32
|
>
|
|
33
33
|
{[0, 10, 20, 30, 40, 50].map((s) => (
|
|
@@ -49,7 +49,7 @@ export function TimelineEmptyState({
|
|
|
49
49
|
<div className="flex-1 flex items-center justify-center">
|
|
50
50
|
<div
|
|
51
51
|
className={`flex items-center gap-3 px-6 py-3 border border-dashed rounded-lg transition-colors duration-150 ${
|
|
52
|
-
isDragOver ? "border-studio-accent/60 bg-studio-accent/
|
|
52
|
+
isDragOver ? "border-studio-accent/60 bg-studio-accent/6" : "border-neutral-700/50"
|
|
53
53
|
}`}
|
|
54
54
|
>
|
|
55
55
|
{isDragOver ? (
|
|
@@ -63,7 +63,7 @@ export function TimelineEmptyState({
|
|
|
63
63
|
strokeWidth="1.5"
|
|
64
64
|
strokeLinecap="round"
|
|
65
65
|
strokeLinejoin="round"
|
|
66
|
-
className="text-studio-accent
|
|
66
|
+
className="text-studio-accent shrink-0"
|
|
67
67
|
>
|
|
68
68
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
69
69
|
<polyline points="7 10 12 15 17 10" />
|
|
@@ -82,7 +82,7 @@ export function TimelineEmptyState({
|
|
|
82
82
|
strokeWidth="1.5"
|
|
83
83
|
strokeLinecap="round"
|
|
84
84
|
strokeLinejoin="round"
|
|
85
|
-
className="text-neutral-600
|
|
85
|
+
className="text-neutral-600 shrink-0"
|
|
86
86
|
>
|
|
87
87
|
<rect x="2" y="2" width="20" height="20" rx="2" />
|
|
88
88
|
<path d="M7 2v20" />
|
|
@@ -79,7 +79,7 @@ function GroupNameDialog({
|
|
|
79
79
|
<div
|
|
80
80
|
role="dialog"
|
|
81
81
|
aria-label="This track cannot be grouped"
|
|
82
|
-
className="z-
|
|
82
|
+
className="z-200 w-64 rounded-md border border-white/10 bg-[#1b1b1f] p-3 text-[11px] leading-snug text-white/75 shadow-xl"
|
|
83
83
|
style={{ position: "fixed", ...groupDialogPosition(anchorRect) }}
|
|
84
84
|
onPointerDown={(event) => event.stopPropagation()}
|
|
85
85
|
onKeyDown={(event) => {
|
|
@@ -97,7 +97,7 @@ function GroupNameDialog({
|
|
|
97
97
|
<div
|
|
98
98
|
role="dialog"
|
|
99
99
|
aria-label="Name this group"
|
|
100
|
-
className="z-
|
|
100
|
+
className="z-200 w-64 rounded-md border border-white/10 bg-[#1b1b1f] p-3 text-[11px] text-white/75 shadow-xl"
|
|
101
101
|
style={{ position: "fixed", ...groupDialogPosition(anchorRect) }}
|
|
102
102
|
onPointerDown={(event) => event.stopPropagation()}
|
|
103
103
|
onKeyDown={(event) => {
|
|
@@ -115,7 +115,7 @@ function GroupNameDialog({
|
|
|
115
115
|
aria-label="Group name"
|
|
116
116
|
value={label}
|
|
117
117
|
onChange={(event) => setLabel(event.currentTarget.value)}
|
|
118
|
-
className="w-full rounded border border-white/20 bg-black/30 px-1.5 py-1 text-[11px] text-white outline-
|
|
118
|
+
className="w-full rounded-sm border border-white/20 bg-black/30 px-1.5 py-1 text-[11px] text-white outline-hidden focus:border-[#3CE6AC]"
|
|
119
119
|
/>
|
|
120
120
|
{/* The sentence. No jargon, and it names both things a bus does. */}
|
|
121
121
|
<p className="mt-2 leading-snug">
|
|
@@ -125,14 +125,14 @@ function GroupNameDialog({
|
|
|
125
125
|
<div className="mt-2.5 flex justify-end gap-1.5">
|
|
126
126
|
<button
|
|
127
127
|
type="button"
|
|
128
|
-
className="rounded border border-white/20 px-2 py-1 text-[10px] text-white/75 hover:bg-white/10"
|
|
128
|
+
className="rounded-sm border border-white/20 px-2 py-1 text-[10px] text-white/75 hover:bg-white/10"
|
|
129
129
|
onClick={onCancel}
|
|
130
130
|
>
|
|
131
131
|
Cancel
|
|
132
132
|
</button>
|
|
133
133
|
<button
|
|
134
134
|
type="button"
|
|
135
|
-
className="rounded border border-[#3CE6AC] bg-[#3CE6AC]/15 px-2 py-1 text-[10px] font-semibold text-[#3CE6AC] hover:bg-[#3CE6AC]/25"
|
|
135
|
+
className="rounded-sm border border-[#3CE6AC] bg-[#3CE6AC]/15 px-2 py-1 text-[10px] font-semibold text-[#3CE6AC] hover:bg-[#3CE6AC]/25"
|
|
136
136
|
onClick={confirm}
|
|
137
137
|
>
|
|
138
138
|
Group
|
|
@@ -212,7 +212,7 @@ export function TimelineFxButton(props: TimelineFxButtonProps) {
|
|
|
212
212
|
ref={buttonRef}
|
|
213
213
|
aria-label="Effects — group these clips first"
|
|
214
214
|
title="Group these clips to add effects to all of them"
|
|
215
|
-
className="flex h-6 items-center justify-center rounded border-0 bg-transparent px-1 text-[10px] font-semibold text-white/35 hover:text-white/75"
|
|
215
|
+
className="flex h-6 items-center justify-center rounded-sm border-0 bg-transparent px-1 text-[10px] font-semibold text-white/35 hover:text-white/75"
|
|
216
216
|
onPointerDown={(event) => event.stopPropagation()}
|
|
217
217
|
onClick={(event) => {
|
|
218
218
|
event.stopPropagation();
|
|
@@ -55,7 +55,7 @@ function GroupNameButton({
|
|
|
55
55
|
// No `flex-1`: the row's control group owns the slack now (`ml-auto`), so
|
|
56
56
|
// claiming it here would push the controls off the right edge — and the
|
|
57
57
|
// count with them, since it rides inside this button.
|
|
58
|
-
className="flex h-6 min-w-0 items-center gap-1.5 rounded border-0 bg-transparent p-0 text-left text-[11px] text-white hover:text-[#3CE6AC] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
58
|
+
className="flex h-6 min-w-0 items-center gap-1.5 rounded-sm border-0 bg-transparent p-0 text-left text-[11px] text-white hover:text-[#3CE6AC] focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
59
59
|
onPointerDown={(event) => event.stopPropagation()}
|
|
60
60
|
onClick={(event) => {
|
|
61
61
|
event.stopPropagation();
|
|
@@ -67,7 +67,7 @@ function GroupNameButton({
|
|
|
67
67
|
</span>
|
|
68
68
|
{/* Wraps rather than truncating — a name that needs a hover to be read
|
|
69
69
|
is no use in a column you scan. */}
|
|
70
|
-
<span className="min-w-0 break-
|
|
70
|
+
<span className="min-w-0 wrap-break-word text-left font-medium leading-tight">{label}</span>
|
|
71
71
|
<span
|
|
72
72
|
className="shrink-0 rounded-full bg-white/10 px-1 text-[9px] leading-[14px] tabular-nums text-white/55"
|
|
73
73
|
aria-hidden="true"
|
|
@@ -99,7 +99,7 @@ export function TimelineGroupHeader({
|
|
|
99
99
|
<div
|
|
100
100
|
role="rowheader"
|
|
101
101
|
aria-colindex={1}
|
|
102
|
-
className="sticky left-0 z-
|
|
102
|
+
className="sticky left-0 z-12 flex shrink-0 items-center gap-1.5 overflow-hidden px-1.5 text-[11px]"
|
|
103
103
|
style={{
|
|
104
104
|
width: columnWidth,
|
|
105
105
|
height: TRACK_H,
|
|
@@ -121,7 +121,7 @@ export function TimelineGroupHeader({
|
|
|
121
121
|
// 13px mono, matching the property panel's preset-run caret
|
|
122
122
|
// (`hf-fx-preset-run-caret`) — the same disclosure, so the same glyph
|
|
123
123
|
// at the same size rather than a smaller one unique to this row.
|
|
124
|
-
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 font-mono text-[13px] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
124
|
+
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 font-mono text-[13px] focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
125
125
|
isExpanded ? "text-white" : "text-white/55 hover:text-white"
|
|
126
126
|
}`}
|
|
127
127
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -159,7 +159,7 @@ export function TimelineGroupHeader({
|
|
|
159
159
|
aria-label={`${isLaneOpen ? "Hide" : "Show"} ${label} lanes`}
|
|
160
160
|
title={`${isLaneOpen ? "Hide" : "Show"} lanes`}
|
|
161
161
|
// Anchored right, matching every other header's lane toggle.
|
|
162
|
-
className={`ml-auto flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
162
|
+
className={`ml-auto flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[11px] leading-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
163
163
|
isLaneOpen ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
|
|
164
164
|
}`}
|
|
165
165
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -64,7 +64,7 @@ export function TimelineGroupLaneLabels({
|
|
|
64
64
|
key={lane.target}
|
|
65
65
|
data-group-lane-label={lane.target}
|
|
66
66
|
aria-label={`Show ${groupLabel} ${parts.name}${parts.param ? ` ${parts.param}` : ""} in the effect rack`}
|
|
67
|
-
className="absolute left-0 flex items-center gap-1.5 overflow-hidden border-0 px-1.5 text-left text-[10px] text-white/65 hover:text-white focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
67
|
+
className="absolute left-0 flex items-center gap-1.5 overflow-hidden border-0 px-1.5 text-left text-[10px] text-white/65 hover:text-white focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
68
68
|
style={{
|
|
69
69
|
top: top + index * AUTOMATION_LANE_H,
|
|
70
70
|
width: columnWidth,
|
|
@@ -140,7 +140,7 @@ export function TimelineGroupRow({
|
|
|
140
140
|
around the labels alone does not work either, because as a flex item
|
|
141
141
|
after the header it starts at x = columnWidth, i.e. inside the lanes. */}
|
|
142
142
|
<div
|
|
143
|
-
className="sticky left-0 z-
|
|
143
|
+
className="sticky left-0 z-12 shrink-0"
|
|
144
144
|
style={{ width: contentOrigin >= LABEL_COL_W ? LABEL_COL_W : contentOrigin }}
|
|
145
145
|
>
|
|
146
146
|
<TimelineGroupHeader
|
|
@@ -72,6 +72,7 @@ interface RenderLanesOptions {
|
|
|
72
72
|
onContextMenuLane?: (e: React.MouseEvent, track: number, time: number) => void;
|
|
73
73
|
hoveredClip?: string | null;
|
|
74
74
|
renderClipContent?: React.ComponentProps<typeof TimelineLanes>["renderClipContent"];
|
|
75
|
+
snapGuide?: { time: number; type: "beat" | "clip-edge" | "playhead" } | null;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
function renderLanes(options: RenderLanesOptions = {}): {
|
|
@@ -141,6 +142,7 @@ function renderLanes(options: RenderLanesOptions = {}): {
|
|
|
141
142
|
renderClipContent={next.renderClipContent}
|
|
142
143
|
draggedClip={next.draggedClip ?? null}
|
|
143
144
|
draggedElement={null}
|
|
145
|
+
snapGuide={next.snapGuide ?? null}
|
|
144
146
|
multiDragPreview={next.multiDragPreview ?? null}
|
|
145
147
|
blockedClipRef={createRef<BlockedClipState | null>()}
|
|
146
148
|
suppressClickRef={{ current: false }}
|
|
@@ -179,6 +181,36 @@ function visibilityLabels(host: HTMLElement): (string | null)[] {
|
|
|
179
181
|
);
|
|
180
182
|
}
|
|
181
183
|
|
|
184
|
+
/** The beat guide's own highlight div, keyed by the green glow every other beat lacks. */
|
|
185
|
+
function beatHighlight(host: HTMLElement): HTMLElement | undefined {
|
|
186
|
+
return Array.from(host.querySelectorAll("div")).find((div) =>
|
|
187
|
+
(div.style.boxShadow ?? "").includes("34,197,94"),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
describe("TimelineLanes beat guide", () => {
|
|
192
|
+
it("draws the beat highlight from snapGuide, not from the stale draggedClip prop", () => {
|
|
193
|
+
const view = renderLanes({
|
|
194
|
+
elements: [element("clip-a", TRACK_A)],
|
|
195
|
+
snapGuide: { time: 1.5, type: "beat" },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(beatHighlight(view.host)?.style.left).toBe("150px");
|
|
199
|
+
act(() => view.root.unmount());
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("clears the highlight once the trim it belonged to ends", () => {
|
|
203
|
+
const view = renderLanes({
|
|
204
|
+
elements: [element("clip-a", TRACK_A)],
|
|
205
|
+
snapGuide: { time: 1.5, type: "beat" },
|
|
206
|
+
});
|
|
207
|
+
view.rerender({ elements: [element("clip-a", TRACK_A)], snapGuide: null });
|
|
208
|
+
|
|
209
|
+
expect(beatHighlight(view.host)).toBeUndefined();
|
|
210
|
+
act(() => view.root.unmount());
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
182
214
|
describe("TimelineLanes track numbering", () => {
|
|
183
215
|
// Screen readers literally announced "Hide track 0.16666666666666666".
|
|
184
216
|
it("numbers tracks contiguously from 1 regardless of the fractional sort keys", () => {
|
|
@@ -59,6 +59,7 @@ export function TimelineLanes({
|
|
|
59
59
|
hoveredClip,
|
|
60
60
|
draggedClip,
|
|
61
61
|
draggedElement,
|
|
62
|
+
snapGuide,
|
|
62
63
|
multiDragPreview,
|
|
63
64
|
blockedClipRef,
|
|
64
65
|
suppressClickRef,
|
|
@@ -331,11 +332,7 @@ export function TimelineLanes({
|
|
|
331
332
|
beatTimes={beatAnalysis?.beatTimes}
|
|
332
333
|
beatStrengths={beatAnalysis?.beatStrengths}
|
|
333
334
|
pps={pps}
|
|
334
|
-
highlightTime={
|
|
335
|
-
draggedClip?.started && draggedClip.snapType === "beat"
|
|
336
|
-
? draggedClip.snapTime
|
|
337
|
-
: null
|
|
338
|
-
}
|
|
335
|
+
highlightTime={snapGuide?.type === "beat" ? snapGuide.time : null}
|
|
339
336
|
renderTimeRange={rowsVirtualized ? renderTimeRange : undefined}
|
|
340
337
|
/>
|
|
341
338
|
{/* Beat dots on the active track (the one holding the selection),
|
|
@@ -56,14 +56,14 @@ export function TimelineLayerGroupHeader({
|
|
|
56
56
|
}}
|
|
57
57
|
>
|
|
58
58
|
<div
|
|
59
|
-
className="sticky left-0 z-
|
|
59
|
+
className="sticky left-0 z-13 flex h-full items-center"
|
|
60
60
|
style={{
|
|
61
61
|
width: Math.min(GUTTER + 220, GUTTER + trackContentWidth),
|
|
62
62
|
background: theme.gutterBackground,
|
|
63
63
|
}}
|
|
64
64
|
>
|
|
65
65
|
<div
|
|
66
|
-
className="relative h-full
|
|
66
|
+
className="relative h-full shrink-0"
|
|
67
67
|
style={{
|
|
68
68
|
width: GUTTER,
|
|
69
69
|
borderRight: `1px solid ${theme.gutterBorder}`,
|
|
@@ -85,7 +85,7 @@ export const TimelineRuler = memo(function TimelineRuler({
|
|
|
85
85
|
style={{ height: RULER_H, width: contentOrigin + trackContentWidth, zIndex: 70 }}
|
|
86
86
|
>
|
|
87
87
|
<div
|
|
88
|
-
className="sticky left-0 z-
|
|
88
|
+
className="sticky left-0 z-12 shrink-0"
|
|
89
89
|
style={{
|
|
90
90
|
width: contentOrigin,
|
|
91
91
|
// Ruler corner uses the panel surface — same as the ruler strip itself.
|
|
@@ -12,7 +12,7 @@ export function TimelineShortcutHint({ theme }: TimelineShortcutHintProps) {
|
|
|
12
12
|
style={{ background: "rgba(17,23,35,0.84)", borderColor: theme.gutterBorder }}
|
|
13
13
|
>
|
|
14
14
|
<kbd
|
|
15
|
-
className="text-[9px] font-mono px-1 py-0.5 rounded"
|
|
15
|
+
className="text-[9px] font-mono px-1 py-0.5 rounded-sm"
|
|
16
16
|
style={{ color: theme.textSecondary, background: "rgba(255,255,255,0.06)" }}
|
|
17
17
|
>
|
|
18
18
|
Shift
|
|
@@ -259,7 +259,7 @@ export function TimelineTrackHeader({
|
|
|
259
259
|
<div
|
|
260
260
|
role="rowheader"
|
|
261
261
|
aria-colindex={1}
|
|
262
|
-
className="sticky left-0 z-
|
|
262
|
+
className="sticky left-0 z-12 shrink-0"
|
|
263
263
|
style={{
|
|
264
264
|
width: showTrackLabel ? LABEL_COL_W : contentOrigin,
|
|
265
265
|
background: gutterFill(theme.gutterBackground, isGroupMember),
|
|
@@ -39,7 +39,7 @@ export function VisibilityButton({
|
|
|
39
39
|
type="button"
|
|
40
40
|
aria-label={label}
|
|
41
41
|
title={label}
|
|
42
|
-
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 transition-colors focus-visible:outline focus-visible:outline-1 focus-visible
|
|
42
|
+
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 transition-colors focus-visible:outline-solid focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-[#3CE6AC] ${
|
|
43
43
|
hidden ? "text-[#3CE6AC] hover:text-white" : "text-white/35 hover:text-white/75"
|
|
44
44
|
}`}
|
|
45
45
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -97,10 +97,10 @@ export function PlainTrackHeader({
|
|
|
97
97
|
|
|
98
98
|
Wraps rather than truncating: a truncated name needs a hover to be
|
|
99
99
|
read at all, and a tooltip is no use to a name you are scanning a
|
|
100
|
-
column of. `break-
|
|
100
|
+
column of. `wrap-break-word` so a long single token breaks instead of
|
|
101
101
|
forcing the column wider. */}
|
|
102
102
|
{showTrackLabel && (
|
|
103
|
-
<span className="min-w-0 break-
|
|
103
|
+
<span className="min-w-0 wrap-break-word text-[11px] leading-tight">{trackLabel}</span>
|
|
104
104
|
)}
|
|
105
105
|
{showTrackLabel && <TrackClipCount clipCount={clipCount} />}
|
|
106
106
|
{/* `ml-auto` is what anchors the group right: it absorbs the slack the
|
|
@@ -12,7 +12,8 @@ export interface TimelineClipRenderContext {
|
|
|
12
12
|
export interface TimelineProps extends TimelineDropCallbacks, TimelineEditOverrides {
|
|
13
13
|
/** Project-scoped reset boundary; soft source refreshes retain the same epoch. */
|
|
14
14
|
sessionEpoch?: number;
|
|
15
|
-
|
|
15
|
+
/** keepPlaying: true preserves the current play state across the seek. */
|
|
16
|
+
onSeek?: (time: number, options?: { keepPlaying?: boolean }) => void;
|
|
16
17
|
onDrillDown?: (element: TimelineElement) => void;
|
|
17
18
|
renderClipContent?: (
|
|
18
19
|
element: TimelineElement,
|
|
@@ -75,7 +75,7 @@ export const TrackGapContextMenu = memo(function TrackGapContextMenu({
|
|
|
75
75
|
return createPortal(
|
|
76
76
|
<div
|
|
77
77
|
ref={menuRef}
|
|
78
|
-
className="fixed z-
|
|
78
|
+
className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
|
|
79
79
|
style={{ left: adjustedX, top: adjustedY }}
|
|
80
80
|
onPointerLeave={() => onHoverAction(null)}
|
|
81
81
|
>
|
|
@@ -94,7 +94,7 @@ export const VideoThumbnail = memo(function VideoThumbnail({
|
|
|
94
94
|
return (
|
|
95
95
|
<div
|
|
96
96
|
key={index}
|
|
97
|
-
className="relative h-full
|
|
97
|
+
className="relative h-full shrink-0 overflow-hidden bg-neutral-900"
|
|
98
98
|
style={{ width: frameW }}
|
|
99
99
|
>
|
|
100
100
|
<img
|
|
@@ -119,7 +119,9 @@ export const VideoThumbnail = memo(function VideoThumbnail({
|
|
|
119
119
|
)}
|
|
120
120
|
{snapshot.status === "error" && (
|
|
121
121
|
<div className="absolute inset-0 flex items-center justify-center bg-neutral-900/60">
|
|
122
|
-
<span className="rounded bg-black/50 px-1 text-[8px] text-neutral-500">
|
|
122
|
+
<span className="rounded-sm bg-black/50 px-1 text-[8px] text-neutral-500">
|
|
123
|
+
no preview
|
|
124
|
+
</span>
|
|
123
125
|
</div>
|
|
124
126
|
)}
|
|
125
127
|
{label && (
|
|
@@ -52,7 +52,7 @@ export const VolumeControl = memo(function VolumeControl({
|
|
|
52
52
|
const muteLabel = silent ? "Unmute audio" : "Mute audio";
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
<div className="group flex
|
|
55
|
+
<div className="group flex shrink-0 items-center">
|
|
56
56
|
<div className="w-0 overflow-hidden opacity-0 transition-[width,opacity] duration-150 ease-out group-hover:w-14 group-hover:opacity-100 group-focus-within:w-14 group-focus-within:opacity-100">
|
|
57
57
|
<div className="relative mx-1 flex h-6 w-12 items-center">
|
|
58
58
|
<div className="absolute inset-x-0 h-0.5 overflow-hidden rounded-full bg-neutral-700">
|
|
@@ -92,7 +92,7 @@ export const VolumeControl = memo(function VolumeControl({
|
|
|
92
92
|
disabled={disabled}
|
|
93
93
|
aria-label={muteLabel}
|
|
94
94
|
aria-pressed={silent}
|
|
95
|
-
className={`flex h-7 w-7
|
|
95
|
+
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-30 ${
|
|
96
96
|
silent ? "text-studio-accent" : "text-neutral-500 hover:text-neutral-200"
|
|
97
97
|
}`}
|
|
98
98
|
>
|
|
@@ -14,20 +14,20 @@ export interface TimelinePropertyGroupKeyframeToggle {
|
|
|
14
14
|
remove: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Where an outside drop lands; `insertRow` opens a new track at that row boundary of `trackOrder`. */
|
|
18
|
+
export type TimelineDropPlacement = { start: number; track: number } & (
|
|
19
|
+
| { insertRow?: null; trackOrder?: undefined }
|
|
20
|
+
| { insertRow: number; trackOrder: readonly number[] }
|
|
21
|
+
);
|
|
22
|
+
|
|
17
23
|
/**
|
|
18
24
|
* Shared callback signatures for timeline editing operations.
|
|
19
25
|
* Used by NLELayout, Timeline, and any component that passes through
|
|
20
26
|
* the standard set of timeline mutation handlers.
|
|
21
27
|
*/
|
|
22
28
|
export interface TimelineDropCallbacks {
|
|
23
|
-
onFileDrop?: (
|
|
24
|
-
|
|
25
|
-
placement?: { start: number; track: number },
|
|
26
|
-
) => Promise<void> | void;
|
|
27
|
-
onAssetDrop?: (
|
|
28
|
-
assetPath: string,
|
|
29
|
-
placement: { start: number; track: number },
|
|
30
|
-
) => Promise<void> | void;
|
|
29
|
+
onFileDrop?: (files: File[], placement?: TimelineDropPlacement) => Promise<void> | void;
|
|
30
|
+
onAssetDrop?: (assetPath: string, placement: TimelineDropPlacement) => Promise<void> | void;
|
|
31
31
|
onBlockDrop?: (
|
|
32
32
|
blockName: string,
|
|
33
33
|
placement: { start: number; track: number },
|