@hyperframes/studio 0.8.49 → 0.8.51
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-CqON2Rzo.js → hyperframes-player-Cn_PpHqP.js} +3 -3
- package/dist/assets/index-C05SNrvY.js +360 -0
- package/dist/assets/{index-mIgSsKqs.js → index-CPS3Cvj8.js} +1 -1
- package/dist/assets/{index-BGovDksE.js → index-Clrb11lj.js} +1 -1
- package/dist/assets/index-DUR9qzQM.css +1 -0
- package/dist/index.d.ts +29 -16
- package/dist/index.html +2 -2
- package/dist/index.js +7283 -8606
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +11 -12
- package/src/App.tsx +132 -143
- package/src/captions/components/CaptionAnimationPanel.tsx +4 -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.selectionSync.test.tsx +4 -0
- package/src/components/EditorShell.tsx +28 -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/SaveQueuePausedBanner.tsx +2 -2
- package/src/components/StudioHeader.tsx +2 -62
- package/src/components/StudioLeftSidebar.tsx +7 -4
- package/src/components/StudioOverlays.tsx +2 -6
- package/src/components/StudioRightPanel.tsx +4 -4
- package/src/components/StudioToast.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +7 -7
- package/src/components/dock/Dock.test.tsx +192 -0
- package/src/components/dock/Dock.tsx +264 -0
- package/src/components/dock/dock.css +23 -0
- package/src/components/dock/dockLayout.ts +58 -0
- package/src/components/dock/dockLayoutSchema.ts +66 -0
- package/src/components/dock/dockLayoutStore.ts +91 -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.tsx +3 -3
- package/src/components/editor/MarqueeOverlay.tsx +1 -1
- package/src/components/editor/OffCanvasIndicators.tsx +1 -1
- package/src/components/editor/PreviewGuides.test.tsx +88 -0
- package/src/components/editor/PreviewGuides.tsx +125 -0
- 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.test.tsx +21 -0
- package/src/components/editor/SnapToolbar.tsx +27 -3
- package/src/components/editor/TimelineFxPopover.tsx +1 -1
- package/src/components/editor/TopologyLens.tsx +4 -4
- 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 +9 -3
- package/src/components/editor/previewGuidesStore.ts +26 -0
- 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/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.test.ts +13 -0
- package/src/components/nle/NLEPreview.tsx +12 -7
- package/src/components/nle/PreviewOverlays.tsx +6 -6
- package/src/components/nle/PreviewPane.test.tsx +52 -0
- package/src/components/nle/PreviewPane.tsx +4 -2
- package/src/components/nle/TimelinePane.tsx +17 -3
- package/src/components/nle/TimelineResizeDivider.tsx +2 -2
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -3
- 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/renders/FfmpegRequiredNotice.tsx +5 -5
- package/src/components/renders/RenderQueue.test.tsx +7 -0
- package/src/components/renders/RenderQueue.tsx +6 -6
- 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/ui/Button.test.tsx +60 -0
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/SearchInput.tsx +2 -2
- package/src/components/ui/Tooltip.tsx +1 -1
- package/src/hooks/appHotkeysDispatch.ts +11 -0
- package/src/hooks/timelineAudioGroupCreate.ts +2 -2
- package/src/hooks/timelineTrackVisibility.ts +8 -11
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
- package/src/hooks/useAppHotkeys.ts +4 -1
- package/src/hooks/useAskAgentModal.ts +2 -0
- package/src/hooks/useClipboard.test.ts +234 -0
- package/src/hooks/useClipboard.ts +264 -70
- package/src/hooks/useGroupCommits.test.ts +0 -1
- package/src/hooks/useLintModal.test.tsx +65 -0
- package/src/hooks/useLintModal.ts +3 -0
- package/src/hooks/useRazorSplit.history.test.tsx +0 -1
- package/src/hooks/useRazorSplit.test.ts +0 -2
- package/src/hooks/useRazorSplit.test.tsx +0 -1
- package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
- package/src/hooks/useSdkSession.ts +43 -10
- package/src/hooks/useStudioContextValue.ts +5 -19
- package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
- package/src/hooks/useTimelineAssetDropOps.ts +52 -11
- package/src/hooks/useTimelineEditing.test.tsx +0 -1
- package/src/main.tsx +9 -3
- package/src/player/components/AudioWaveform.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +2 -2
- package/src/player/components/ClipContextMenu.tsx +90 -10
- 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 +16 -15
- package/src/player/components/TimelineCanvas.tsx +45 -8
- 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/TimelineGestureOverlay.test.tsx +35 -1
- package/src/player/components/TimelineGestureOverlay.tsx +1 -1
- 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 +54 -1
- package/src/player/components/TimelineLanes.tsx +2 -5
- package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
- package/src/player/components/TimelineOverlays.tsx +12 -0
- 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 +6 -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/timelineAuthoredMoveTarget.ts +1 -2
- package/src/player/components/timelineCallbacks.ts +8 -8
- package/src/player/components/timelineClipChildren.test.ts +2 -2
- package/src/player/components/timelineClipChildren.tsx +2 -1
- package/src/player/components/timelineClipDragCommit.ts +2 -2
- package/src/player/components/timelineClipDragPreview.ts +51 -27
- package/src/player/components/timelineClipDragTypes.ts +3 -0
- package/src/player/components/timelineDragDrop.test.tsx +92 -1
- package/src/player/components/timelineDragDrop.ts +82 -33
- package/src/player/components/timelineEditCapabilities.ts +1 -2
- package/src/player/components/timelineEditing.test.ts +0 -16
- package/src/player/components/timelineGroupEditing.test.ts +1 -3
- package/src/player/components/timelineGroupEditing.ts +0 -1
- 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/timelineTrackDisplay.ts +1 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +0 -53
- package/src/player/components/trackHeaderLabelRows.tsx +4 -4
- package/src/player/components/useTimelineClipContextMenu.ts +32 -0
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
- package/src/player/components/useTimelineClipDrag.ts +30 -3
- package/src/player/components/useTimelineTrackDerivations.ts +1 -1
- package/src/player/hooks/timelineDomBinding.test.ts +0 -3
- package/src/player/hooks/timelineSyncHydration.test.ts +18 -0
- package/src/player/hooks/timelineSyncHydration.ts +37 -18
- package/src/player/hooks/useTimelinePlayer.test.ts +3 -20
- package/src/player/hooks/useTimelineRowElements.test.ts +28 -0
- package/src/player/hooks/useTimelineRowElements.ts +17 -0
- package/src/player/hooks/useTimelineSyncCallbacks.emptyManifest.test.tsx +41 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -10
- package/src/player/lib/describeClips.test.ts +95 -0
- package/src/player/lib/describeClips.ts +52 -0
- package/src/player/lib/timelineDOM.test.ts +0 -80
- package/src/player/lib/timelineDOM.ts +1 -61
- package/src/player/lib/timelineElementHelpers.ts +2 -53
- package/src/player/store/playerStore.ts +5 -0
- package/src/player/store/timelineElement.ts +1 -3
- package/src/shims/canvasBrowserStub.js +50 -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/tailwind-preset.shared.js +3 -0
- package/src/styles/theme.css +213 -0
- package/src/styles/theme.test.ts +196 -0
- package/src/utils/clipboardPayload.test.ts +42 -2
- package/src/utils/clipboardPayload.ts +57 -32
- package/src/utils/previewSafeMargins.test.ts +34 -0
- package/src/utils/previewSafeMargins.ts +10 -0
- package/src/utils/razorSplitTransaction.test.ts +0 -1
- package/src/utils/sdkCanvasComposition.test.ts +77 -0
- package/src/utils/studioUiPreferences.test.ts +43 -0
- package/src/utils/studioUiPreferences.ts +34 -6
- package/src/utils/timelineDropTrackInsert.test.ts +134 -0
- package/src/utils/timelineDropTrackInsert.ts +83 -0
- package/src/utils/timelineElementSplit.test.ts +1 -2
- package/src/utils/timelineElementSplit.ts +0 -1
- package/src/webmcp/tools/lookTools.test.ts +40 -0
- package/src/webmcp/tools/lookTools.ts +32 -1
- package/dist/assets/index-B9LfWNyM.css +0 -1
- package/dist/assets/index-Bgmx2h7W.js +0 -441
- package/src/components/StudioGlobalDragOverlay.tsx +0 -24
- 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
- package/src/player/hooks/useExpandedTimelineElements.test.ts +0 -774
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -460
|
@@ -37,7 +37,7 @@ export function LaneToggleButton({
|
|
|
37
37
|
title={`${isExpanded ? "Hide" : "Show"} lanes`}
|
|
38
38
|
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target. The glyph stays 11px;
|
|
39
39
|
// only the hit box grows.
|
|
40
|
-
className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
40
|
+
className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
41
41
|
isExpanded ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
|
|
42
42
|
}`}
|
|
43
43
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -97,7 +97,7 @@ export function LayerDisclosureRow({
|
|
|
97
97
|
</span>
|
|
98
98
|
{/* Wraps rather than truncating: a truncated name needs a hover to be
|
|
99
99
|
read, which a scanned column cannot rely on. */}
|
|
100
|
-
<span className="min-w-0 flex-1 break-
|
|
100
|
+
<span className="min-w-0 flex-1 wrap-break-word font-medium leading-tight">{name}</span>
|
|
101
101
|
<TrackClipCount clipCount={clipCount} />
|
|
102
102
|
{children}
|
|
103
103
|
{/* Anchored right, on every header that has one: the lane toggle is the
|
|
@@ -116,7 +116,7 @@ const FullscreenButton = memo(function FullscreenButton({
|
|
|
116
116
|
trackStudioEvent("playback", { action: "fullscreen_toggle", active: !isFullscreen });
|
|
117
117
|
onToggleFullscreen();
|
|
118
118
|
}}
|
|
119
|
-
className={`flex h-7 w-7
|
|
119
|
+
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${
|
|
120
120
|
isFullscreen ? "text-studio-accent" : "text-neutral-500 hover:text-neutral-200"
|
|
121
121
|
}`}
|
|
122
122
|
aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
@@ -153,7 +153,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
153
153
|
);
|
|
154
154
|
|
|
155
155
|
return (
|
|
156
|
-
<div ref={shortcutsPanelRef} className="relative
|
|
156
|
+
<div ref={shortcutsPanelRef} className="relative shrink-0">
|
|
157
157
|
<Tooltip label="Shortcuts and tools">
|
|
158
158
|
<button
|
|
159
159
|
ref={triggerRef}
|
|
@@ -192,7 +192,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
192
192
|
// Deliberately NOT aria-modal. This is a non-modal disclosure: focus is
|
|
193
193
|
// not trapped and the rest of the editor stays operable, so claiming
|
|
194
194
|
// modality would make assistive tech treat the whole app as inert.
|
|
195
|
-
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-
|
|
195
|
+
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-hidden"
|
|
196
196
|
style={{
|
|
197
197
|
background: "#161618",
|
|
198
198
|
border: "1px solid rgba(255,255,255,0.08)",
|
|
@@ -212,7 +212,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
212
212
|
pattern="[0-9]*"
|
|
213
213
|
aria-label="Jump to frame"
|
|
214
214
|
placeholder="frame number"
|
|
215
|
-
className="h-6 flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-
|
|
215
|
+
className="h-6 flex-1 rounded-sm border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-hidden transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60"
|
|
216
216
|
onKeyDown={handleJumpKeyDown}
|
|
217
217
|
onBlur={commitJumpFrame}
|
|
218
218
|
/>
|
|
@@ -220,7 +220,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
220
220
|
<button
|
|
221
221
|
type="submit"
|
|
222
222
|
disabled={disabled}
|
|
223
|
-
className="h-6 px-2 rounded border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
|
|
223
|
+
className="h-6 px-2 rounded-sm border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
|
|
224
224
|
>
|
|
225
225
|
Go
|
|
226
226
|
</button>
|
|
@@ -236,7 +236,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
236
236
|
<div className="flex items-center justify-between gap-2">
|
|
237
237
|
<div className="flex items-center gap-2">
|
|
238
238
|
<span
|
|
239
|
-
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
239
|
+
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
240
240
|
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
241
241
|
>
|
|
242
242
|
I
|
|
@@ -253,7 +253,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
253
253
|
<button
|
|
254
254
|
type="button"
|
|
255
255
|
onClick={() => setInPoint(null)}
|
|
256
|
-
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
256
|
+
className="w-4 h-4 flex items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
257
257
|
aria-label="Clear in-point"
|
|
258
258
|
>
|
|
259
259
|
<svg
|
|
@@ -277,7 +277,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
277
277
|
<div className="flex items-center justify-between gap-2">
|
|
278
278
|
<div className="flex items-center gap-2">
|
|
279
279
|
<span
|
|
280
|
-
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
280
|
+
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
281
281
|
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
282
282
|
>
|
|
283
283
|
O
|
|
@@ -294,7 +294,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
294
294
|
<button
|
|
295
295
|
type="button"
|
|
296
296
|
onClick={() => setOutPoint(null)}
|
|
297
|
-
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
297
|
+
className="w-4 h-4 flex items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
298
298
|
aria-label="Clear out-point"
|
|
299
299
|
>
|
|
300
300
|
<svg
|
|
@@ -328,7 +328,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
328
328
|
{section.hints.map((hint) => (
|
|
329
329
|
<div key={hint.key} className="flex items-center gap-3">
|
|
330
330
|
<span
|
|
331
|
-
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
|
|
331
|
+
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
|
|
332
332
|
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
333
333
|
>
|
|
334
334
|
{hint.key}
|
|
@@ -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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useRef, useMemo, useCallback, useState, memo } from "react";
|
|
2
2
|
import { useAdjustedBeatAnalysis, useMusicBeatAnalysis } from "../../hooks/useMusicBeatAnalysis";
|
|
3
3
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
4
|
-
import {
|
|
4
|
+
import { useTimelineRowElements } from "../hooks/useTimelineRowElements";
|
|
5
5
|
import { defaultTimelineTheme } from "./timelineTheme";
|
|
6
6
|
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
7
7
|
import { useTimelinePlayhead } from "./useTimelinePlayhead";
|
|
@@ -43,6 +43,7 @@ import { useTimelineClipRenderWindow } from "./useTimelineClipRenderWindow";
|
|
|
43
43
|
import { useTimelineActiveClips } from "./useTimelineActiveClips";
|
|
44
44
|
import { useTimelineLaneMoveRefresh } from "./useTimelineLaneMoveRefresh";
|
|
45
45
|
import { useTimelineLogicalFocus } from "./useTimelineLogicalFocus";
|
|
46
|
+
import { useClipContextMenu } from "./useTimelineClipContextMenu";
|
|
46
47
|
|
|
47
48
|
export {
|
|
48
49
|
shouldAutoScrollTimeline,
|
|
@@ -79,6 +80,10 @@ export const Timeline = memo(function Timeline({
|
|
|
79
80
|
onBlockedEditAttempt: onBlockedEditAttemptOverride,
|
|
80
81
|
onSplitElement: onSplitElementOverride,
|
|
81
82
|
onSelectElement,
|
|
83
|
+
onCopyClip,
|
|
84
|
+
onPasteClip,
|
|
85
|
+
onDuplicateClip,
|
|
86
|
+
canPasteClip,
|
|
82
87
|
theme: themeOverrides,
|
|
83
88
|
sessionEpoch = 0,
|
|
84
89
|
}: TimelineProps = {}) {
|
|
@@ -106,7 +111,7 @@ export const Timeline = memo(function Timeline({
|
|
|
106
111
|
const refreshAfterLaneMove = useTimelineLaneMoveRefresh();
|
|
107
112
|
useMusicBeatAnalysis();
|
|
108
113
|
const rawElements = usePlayerStore((s) => s.elements);
|
|
109
|
-
const expandedElements =
|
|
114
|
+
const expandedElements = useTimelineRowElements();
|
|
110
115
|
const adjustedBeatAnalysis = useAdjustedBeatAnalysis();
|
|
111
116
|
const duration = usePlayerStore((s) => s.duration);
|
|
112
117
|
const timeDisplayMode = usePlayerStore((s) => s.timeDisplayMode);
|
|
@@ -208,6 +213,7 @@ export const Timeline = memo(function Timeline({
|
|
|
208
213
|
onMoveElement: pinnedOnMoveElement,
|
|
209
214
|
onMoveElements: pinnedOnMoveElements,
|
|
210
215
|
});
|
|
216
|
+
const onContextMenuClip = useClipContextMenu(onSelectElement, dismissGapMenu, setClipContextMenu);
|
|
211
217
|
|
|
212
218
|
const {
|
|
213
219
|
draggedClip,
|
|
@@ -227,6 +233,7 @@ export const Timeline = memo(function Timeline({
|
|
|
227
233
|
onResizeElement: pinnedOnResizeElement,
|
|
228
234
|
onResizeElements: pinnedOnResizeElements,
|
|
229
235
|
onBlockedEditAttempt,
|
|
236
|
+
onSeek,
|
|
230
237
|
setShowPopover,
|
|
231
238
|
setRangeSelectionRef,
|
|
232
239
|
readZIndex: zSyncEnabled ? readClipZIndex : undefined,
|
|
@@ -458,7 +465,7 @@ export const Timeline = memo(function Timeline({
|
|
|
458
465
|
data-timeline-scroll-viewport
|
|
459
466
|
data-timeline-auto-scroll-left-inset={labelMode ? LABEL_COL_W : 0}
|
|
460
467
|
tabIndex={-1}
|
|
461
|
-
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`}
|
|
462
469
|
onScroll={(e) => {
|
|
463
470
|
lastScrollLeftRef.current = e.currentTarget.scrollLeft; // restored across post-edit reload
|
|
464
471
|
recordTimelineScroll(e.currentTarget);
|
|
@@ -492,6 +499,7 @@ export const Timeline = memo(function Timeline({
|
|
|
492
499
|
rangeSelection={rangeSelection}
|
|
493
500
|
marqueeRect={marqueeRect}
|
|
494
501
|
laneGapStrips={laneGapStrips}
|
|
502
|
+
dropPreview={assetDrop.dropPreview}
|
|
495
503
|
theme={theme}
|
|
496
504
|
displayTrackOrder={displayLayout.displayTrackOrder}
|
|
497
505
|
rowHeights={displayLayout.displayRowHeights}
|
|
@@ -548,18 +556,7 @@ export const Timeline = memo(function Timeline({
|
|
|
548
556
|
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
549
557
|
onMoveKeyframe={onMoveKeyframe}
|
|
550
558
|
onContextMenuKeyframe={onContextMenuKeyframe}
|
|
551
|
-
onContextMenuClip={
|
|
552
|
-
e.preventDefault();
|
|
553
|
-
setSelectedElementId(el.key ?? el.id);
|
|
554
|
-
onSelectElement?.(el);
|
|
555
|
-
dismissGapMenu();
|
|
556
|
-
setClipContextMenu({
|
|
557
|
-
x: e.clientX,
|
|
558
|
-
y: e.clientY,
|
|
559
|
-
element: el,
|
|
560
|
-
sessionEpoch: usePlayerStore.getState().timelineSessionEpoch,
|
|
561
|
-
});
|
|
562
|
-
}}
|
|
559
|
+
onContextMenuClip={onContextMenuClip}
|
|
563
560
|
onContextMenuLane={(e, track, time) => {
|
|
564
561
|
if (draggedClip?.started || resizingClip) return;
|
|
565
562
|
setClipContextMenu(null);
|
|
@@ -588,6 +585,10 @@ export const Timeline = memo(function Timeline({
|
|
|
588
585
|
onSplitElement={onSplitElement}
|
|
589
586
|
pinZoomBeforeEdit={pinZoomBeforeEdit}
|
|
590
587
|
onDeleteElement={_onDeleteElement}
|
|
588
|
+
onCopyClip={onCopyClip}
|
|
589
|
+
onPasteClip={onPasteClip}
|
|
590
|
+
onDuplicateClip={onDuplicateClip}
|
|
591
|
+
canPasteClip={canPasteClip}
|
|
591
592
|
gapContextMenu={gapMenuModel}
|
|
592
593
|
onDismissGapContextMenu={dismissGapMenu}
|
|
593
594
|
onCloseTrackGap={closeTrackGap}
|
|
@@ -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[];
|
|
@@ -39,12 +41,25 @@ interface TimelineCanvasProps extends TimelineLaneBaseProps {
|
|
|
39
41
|
playheadRef: React.RefObject<HTMLDivElement | null>;
|
|
40
42
|
/** Gap strips: loud on gap-menu-row hover, quiet on the selected clip's lane. */
|
|
41
43
|
laneGapStrips: TimelineLaneGapStrips[];
|
|
44
|
+
/** Landing spot of an outside drag in progress, or null. */
|
|
45
|
+
dropPreview: TimelineDropPlacement | null;
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
// A dropped clip's length is unknown until it lands; the preview shows a default.
|
|
49
|
+
const DROP_PREVIEW_SECONDS = 3;
|
|
50
|
+
|
|
44
51
|
export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvasProps) {
|
|
45
|
-
const { draggedClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
|
|
52
|
+
const { draggedClip, resizingClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
|
|
53
|
+
const snapGuide = resolveSnapGuide(draggedClip, resizingClip);
|
|
46
54
|
const draggedRowIndex =
|
|
47
55
|
draggedClip?.started === true ? displayTrackOrder.indexOf(draggedClip.previewTrack) : -1;
|
|
56
|
+
const dropTrackIndex = props.dropPreview
|
|
57
|
+
? displayTrackOrder.indexOf(props.dropPreview.track)
|
|
58
|
+
: -1;
|
|
59
|
+
// A track not in the order is a new track, drawn one row past the last lane.
|
|
60
|
+
const dropRowIndex = dropTrackIndex < 0 ? displayTrackOrder.length : dropTrackIndex;
|
|
61
|
+
const insertLineRow =
|
|
62
|
+
(draggedClip?.started ? draggedClip.insertRow : null) ?? props.dropPreview?.insertRow ?? null;
|
|
48
63
|
const draggedRowHeight = getTimelineRowHeight(draggedRowIndex, props.rowHeights);
|
|
49
64
|
// A clip bar in an EXPANDED row still renders at TRACK_H (the property lanes
|
|
50
65
|
// occupy the rest of the row — see TimelineLanes' clipHeight), so the drag
|
|
@@ -107,6 +122,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
107
122
|
|
|
108
123
|
<TimelineLanes
|
|
109
124
|
{...props}
|
|
125
|
+
snapGuide={snapGuide}
|
|
110
126
|
draggedElement={draggedElement}
|
|
111
127
|
multiDragPreview={multiDragPreview}
|
|
112
128
|
onToggleTrackHidden={onToggleTrackHidden}
|
|
@@ -171,13 +187,34 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
171
187
|
/>
|
|
172
188
|
)}
|
|
173
189
|
|
|
190
|
+
{/* Drop preview: where an asset or file dragged in from outside will land
|
|
191
|
+
(a row past the last lane means a new track). */}
|
|
192
|
+
{props.dropPreview && props.dropPreview.insertRow == null && (
|
|
193
|
+
<div
|
|
194
|
+
aria-hidden="true"
|
|
195
|
+
data-testid="timeline-drop-preview"
|
|
196
|
+
className="absolute pointer-events-none"
|
|
197
|
+
style={{
|
|
198
|
+
top: getTimelineRowTop(dropRowIndex, props.rowHeights) + CLIP_Y,
|
|
199
|
+
left: props.contentOrigin + props.dropPreview.start * props.pps,
|
|
200
|
+
width: DROP_PREVIEW_SECONDS * props.pps,
|
|
201
|
+
height: TRACK_H - CLIP_Y * 2,
|
|
202
|
+
border: "1px solid rgba(60,230,172,0.55)",
|
|
203
|
+
background: "rgba(60,230,172,0.12)",
|
|
204
|
+
borderRadius: 4,
|
|
205
|
+
zIndex: 30,
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
|
|
174
210
|
{/* Insertion line — a new track will be inserted at this boundary on drop.
|
|
175
211
|
Shown while the pointer is near a lane boundary (insert mode). */}
|
|
176
|
-
{
|
|
212
|
+
{insertLineRow != null && (
|
|
177
213
|
<div
|
|
214
|
+
data-testid="timeline-insert-line"
|
|
178
215
|
className="absolute pointer-events-none"
|
|
179
216
|
style={{
|
|
180
|
-
top: getTimelineRowTop(
|
|
217
|
+
top: getTimelineRowTop(insertLineRow, props.rowHeights) - 0.5,
|
|
181
218
|
left: props.contentOrigin,
|
|
182
219
|
width: props.trackContentWidth,
|
|
183
220
|
height: 1,
|
|
@@ -188,18 +225,18 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
188
225
|
/>
|
|
189
226
|
)}
|
|
190
227
|
|
|
191
|
-
{/* Snap guide for non-beat targets during clip
|
|
192
|
-
{
|
|
228
|
+
{/* Snap guide for non-beat targets during a clip move or trim */}
|
|
229
|
+
{snapGuide && snapGuide.type !== "beat" && (
|
|
193
230
|
<div
|
|
194
231
|
className="absolute pointer-events-none"
|
|
195
232
|
style={{
|
|
196
|
-
left: props.contentOrigin +
|
|
233
|
+
left: props.contentOrigin + snapGuide.time * props.pps,
|
|
197
234
|
top: RULER_H,
|
|
198
235
|
bottom: 0,
|
|
199
236
|
width: 1,
|
|
200
|
-
background:
|
|
237
|
+
background: snapGuide.type === "playhead" ? "#3CE6AC" : "rgba(255,255,255,0.6)",
|
|
201
238
|
boxShadow:
|
|
202
|
-
|
|
239
|
+
snapGuide.type === "playhead"
|
|
203
240
|
? "0 0 6px rgba(60,230,172,0.5)"
|
|
204
241
|
: "0 0 6px rgba(255,255,255,0.4)",
|
|
205
242
|
zIndex: 60,
|
|
@@ -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();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { defaultTimelineTheme } from "./timelineTheme";
|
|
7
7
|
import { TimelineGestureOverlay } from "./TimelineGestureOverlay";
|
|
8
8
|
import type { DraggedClipState } from "./timelineClipDragTypes";
|
|
@@ -65,6 +65,40 @@ describe("TimelineGestureOverlay", () => {
|
|
|
65
65
|
act(() => root.unmount());
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
it("asks for the same thumbnail frames as the clip at rest", () => {
|
|
69
|
+
const host = document.createElement("div");
|
|
70
|
+
document.body.append(host);
|
|
71
|
+
const root = createRoot(host);
|
|
72
|
+
const renderClipContent = vi.fn(() => null);
|
|
73
|
+
act(() => {
|
|
74
|
+
root.render(
|
|
75
|
+
<TimelineGestureOverlay
|
|
76
|
+
drag={drag}
|
|
77
|
+
scrollRef={{
|
|
78
|
+
current: {
|
|
79
|
+
scrollLeft: 0,
|
|
80
|
+
scrollTop: 0,
|
|
81
|
+
getBoundingClientRect: () => ({ left: 0, top: 0 }),
|
|
82
|
+
} as HTMLDivElement,
|
|
83
|
+
}}
|
|
84
|
+
pixelsPerSecond={100}
|
|
85
|
+
rowHeight={42}
|
|
86
|
+
selectedElementId="hero"
|
|
87
|
+
currentTime={4}
|
|
88
|
+
theme={defaultTimelineTheme}
|
|
89
|
+
getTrackStyle={getTrackStyle}
|
|
90
|
+
renderClipContent={renderClipContent}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
expect(renderClipContent).toHaveBeenCalledWith(
|
|
95
|
+
expect.anything(),
|
|
96
|
+
expect.anything(),
|
|
97
|
+
expect.objectContaining({ rich: false }),
|
|
98
|
+
);
|
|
99
|
+
act(() => root.unmount());
|
|
100
|
+
});
|
|
101
|
+
|
|
68
102
|
it("keeps the stable overlay host after terminal cleanup", () => {
|
|
69
103
|
const host = document.createElement("div");
|
|
70
104
|
document.body.append(host);
|
|
@@ -89,7 +89,7 @@ export const TimelineGestureOverlay = memo(function TimelineGestureOverlay({
|
|
|
89
89
|
getTrackStyle(element.tag),
|
|
90
90
|
renderClipContent,
|
|
91
91
|
renderClipOverlay,
|
|
92
|
-
{ priority: "interaction", rich:
|
|
92
|
+
{ priority: "interaction", rich: false },
|
|
93
93
|
)}
|
|
94
94
|
</TimelineClip>
|
|
95
95
|
</div>
|
|
@@ -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
|