@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
|
@@ -4,8 +4,14 @@ import {
|
|
|
4
4
|
parseTimelineCompositionPayload,
|
|
5
5
|
TIMELINE_COMPOSITION_MIME,
|
|
6
6
|
} from "../../utils/timelineCompositionDrop";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
getTimelineInsertBoundaryBand,
|
|
9
|
+
getTimelineRowPositionFromY,
|
|
10
|
+
resolveTimelineAssetDrop,
|
|
11
|
+
type TimelineRowGeometry,
|
|
12
|
+
} from "./timelineLayout";
|
|
13
|
+
import { resolveInsertRow } from "./timelineCollision";
|
|
14
|
+
import type { TimelineDropCallbacks, TimelineDropPlacement } from "./timelineCallbacks";
|
|
9
15
|
import {
|
|
10
16
|
applyTimelineAutoScrollStep,
|
|
11
17
|
resolveTimelineAutoScrollLoopAction,
|
|
@@ -20,8 +26,6 @@ interface UseTimelineAssetDropOptions extends TimelineDropCallbacks {
|
|
|
20
26
|
sessionEpoch: number;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
type TimelinePlacement = { start: number; track: number };
|
|
24
|
-
|
|
25
29
|
/**
|
|
26
30
|
* Parse a JSON drag payload and, if it yields a value, forward it to the drop
|
|
27
31
|
* callback. Malformed payloads are ignored. Shared by the asset + block paths so
|
|
@@ -30,8 +34,8 @@ type TimelinePlacement = { start: number; track: number };
|
|
|
30
34
|
function applyJsonDropPayload(
|
|
31
35
|
raw: string,
|
|
32
36
|
pick: (parsed: Record<string, string | undefined>) => string | undefined,
|
|
33
|
-
apply: (value: string, placement:
|
|
34
|
-
placement:
|
|
37
|
+
apply: (value: string, placement: TimelineDropPlacement) => void,
|
|
38
|
+
placement: TimelineDropPlacement,
|
|
35
39
|
): boolean {
|
|
36
40
|
try {
|
|
37
41
|
const value = pick(JSON.parse(raw) as Record<string, string | undefined>);
|
|
@@ -43,6 +47,40 @@ function applyJsonDropPayload(
|
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
/** Only file and asset drops are written by the asset op that can open a track. */
|
|
51
|
+
function canInsertTrackFor(types: readonly string[]): boolean {
|
|
52
|
+
return types.includes("Files") || types.includes(TIMELINE_ASSET_MIME);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The row boundary a pointer at `contentY` arms for a new track, or null over a lane's middle. */
|
|
56
|
+
export function resolveDropInsertRow(
|
|
57
|
+
contentY: number,
|
|
58
|
+
rowHeights: readonly number[] | undefined,
|
|
59
|
+
trackCount: number,
|
|
60
|
+
): number | null {
|
|
61
|
+
if (trackCount === 0) return null;
|
|
62
|
+
const { rowFloat, rowHeight } = getTimelineRowPositionFromY(contentY, rowHeights);
|
|
63
|
+
// Past the last lane the drop already appends a track (getDefaultDroppedTrack).
|
|
64
|
+
if (rowFloat >= trackCount) return null;
|
|
65
|
+
return resolveInsertRow(rowFloat, trackCount, getTimelineInsertBoundaryBand(rowHeight));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function placeDrop(
|
|
69
|
+
geometry: Parameters<typeof resolveTimelineAssetDrop>[0],
|
|
70
|
+
clientX: number,
|
|
71
|
+
clientY: number,
|
|
72
|
+
// Blocks and compositions are written by their own installers, which only take a track.
|
|
73
|
+
canInsertTrack: boolean,
|
|
74
|
+
): TimelineDropPlacement {
|
|
75
|
+
const placement = resolveTimelineAssetDrop(geometry, clientX, clientY);
|
|
76
|
+
if (!canInsertTrack) return placement;
|
|
77
|
+
const contentY = clientY - geometry.rectTop + geometry.scrollTop;
|
|
78
|
+
const insertRow = resolveDropInsertRow(contentY, geometry.rowHeights, geometry.trackOrder.length);
|
|
79
|
+
return insertRow == null
|
|
80
|
+
? placement
|
|
81
|
+
: { ...placement, insertRow, trackOrder: geometry.trackOrder };
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
function invokeDropCallback(callback: () => Promise<void> | void): void {
|
|
47
85
|
try {
|
|
48
86
|
void Promise.resolve(callback()).catch(() => undefined);
|
|
@@ -54,7 +92,7 @@ function invokeDropCallback(callback: () => Promise<void> | void): void {
|
|
|
54
92
|
function applyFileDrop(
|
|
55
93
|
transfer: DataTransfer,
|
|
56
94
|
onFileDrop: TimelineDropCallbacks["onFileDrop"],
|
|
57
|
-
placement:
|
|
95
|
+
placement: TimelineDropPlacement,
|
|
58
96
|
): boolean {
|
|
59
97
|
if (!onFileDrop || transfer.files.length === 0) return false;
|
|
60
98
|
invokeDropCallback(() => onFileDrop(Array.from(transfer.files), placement));
|
|
@@ -65,8 +103,8 @@ function applyTypedJsonDrop(
|
|
|
65
103
|
transfer: DataTransfer,
|
|
66
104
|
mime: string,
|
|
67
105
|
field: "name" | "path",
|
|
68
|
-
apply: ((value: string, placement:
|
|
69
|
-
placement:
|
|
106
|
+
apply: ((value: string, placement: TimelineDropPlacement) => Promise<void> | void) | undefined,
|
|
107
|
+
placement: TimelineDropPlacement,
|
|
70
108
|
): boolean {
|
|
71
109
|
if (!apply || !Array.from(transfer.types).includes(mime)) return false;
|
|
72
110
|
const payload = transfer.getData(mime);
|
|
@@ -98,6 +136,7 @@ export function useTimelineAssetDrop({
|
|
|
98
136
|
sessionEpoch,
|
|
99
137
|
}: UseTimelineAssetDropOptions) {
|
|
100
138
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
139
|
+
const [dropPreview, setDropPreview] = useState<TimelineDropPlacement | null>(null);
|
|
101
140
|
const dragPointerRef = useRef<{ clientX: number; clientY: number; sessionEpoch: number } | null>(
|
|
102
141
|
null,
|
|
103
142
|
);
|
|
@@ -142,6 +181,29 @@ export function useTimelineAssetDrop({
|
|
|
142
181
|
[scrollRef, sessionEpoch, stepAutoScroll],
|
|
143
182
|
);
|
|
144
183
|
|
|
184
|
+
const resolveDropPlacement = useCallback(
|
|
185
|
+
(clientX: number, clientY: number, canInsertTrack: boolean): TimelineDropPlacement => {
|
|
186
|
+
const scroll = scrollRef.current;
|
|
187
|
+
const rect = scroll?.getBoundingClientRect();
|
|
188
|
+
return placeDrop(
|
|
189
|
+
{
|
|
190
|
+
rectLeft: rect?.left ?? 0,
|
|
191
|
+
rectTop: rect?.top ?? 0,
|
|
192
|
+
scrollLeft: scroll?.scrollLeft ?? 0,
|
|
193
|
+
scrollTop: scroll?.scrollTop ?? 0,
|
|
194
|
+
contentOrigin,
|
|
195
|
+
pixelsPerSecond: ppsRef.current,
|
|
196
|
+
rowHeights: rowGeometryRef.current.rowHeights,
|
|
197
|
+
trackOrder: trackOrderRef.current,
|
|
198
|
+
},
|
|
199
|
+
clientX,
|
|
200
|
+
clientY,
|
|
201
|
+
canInsertTrack,
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
[scrollRef, ppsRef, trackOrderRef, rowGeometryRef, contentOrigin],
|
|
205
|
+
);
|
|
206
|
+
|
|
145
207
|
const handleAssetDragOver = useCallback(
|
|
146
208
|
(e: React.DragEvent) => {
|
|
147
209
|
const types = Array.from(e.dataTransfer.types);
|
|
@@ -154,15 +216,22 @@ export function useTimelineAssetDrop({
|
|
|
154
216
|
e.dataTransfer.dropEffect = "copy";
|
|
155
217
|
activeDropEpochRef.current = sessionEpoch;
|
|
156
218
|
setIsDragOver(true);
|
|
219
|
+
const next = resolveDropPlacement(e.clientX, e.clientY, canInsertTrackFor(types));
|
|
220
|
+
setDropPreview((prev) =>
|
|
221
|
+
prev?.start === next.start && prev.track === next.track && prev.insertRow === next.insertRow
|
|
222
|
+
? prev
|
|
223
|
+
: next,
|
|
224
|
+
);
|
|
157
225
|
syncAutoScroll(e.clientX, e.clientY);
|
|
158
226
|
},
|
|
159
|
-
[sessionEpoch, syncAutoScroll],
|
|
227
|
+
[resolveDropPlacement, sessionEpoch, syncAutoScroll],
|
|
160
228
|
);
|
|
161
229
|
|
|
162
230
|
const clearDropPreview = useCallback(() => {
|
|
163
231
|
activeDropEpochRef.current = null;
|
|
164
232
|
stopAutoScroll();
|
|
165
233
|
setIsDragOver(false);
|
|
234
|
+
setDropPreview(null);
|
|
166
235
|
}, [stopAutoScroll]);
|
|
167
236
|
|
|
168
237
|
const handleAssetDragLeave = useCallback(
|
|
@@ -174,35 +243,14 @@ export function useTimelineAssetDrop({
|
|
|
174
243
|
[clearDropPreview],
|
|
175
244
|
);
|
|
176
245
|
|
|
177
|
-
const resolveDropPlacement = useCallback(
|
|
178
|
-
(clientX: number, clientY: number): TimelinePlacement => {
|
|
179
|
-
const scroll = scrollRef.current;
|
|
180
|
-
const rect = scroll?.getBoundingClientRect();
|
|
181
|
-
return resolveTimelineAssetDrop(
|
|
182
|
-
{
|
|
183
|
-
rectLeft: rect?.left ?? 0,
|
|
184
|
-
rectTop: rect?.top ?? 0,
|
|
185
|
-
scrollLeft: scroll?.scrollLeft ?? 0,
|
|
186
|
-
scrollTop: scroll?.scrollTop ?? 0,
|
|
187
|
-
contentOrigin,
|
|
188
|
-
pixelsPerSecond: ppsRef.current,
|
|
189
|
-
rowHeights: rowGeometryRef.current.rowHeights,
|
|
190
|
-
trackOrder: trackOrderRef.current,
|
|
191
|
-
},
|
|
192
|
-
clientX,
|
|
193
|
-
clientY,
|
|
194
|
-
);
|
|
195
|
-
},
|
|
196
|
-
[scrollRef, ppsRef, trackOrderRef, rowGeometryRef, contentOrigin],
|
|
197
|
-
);
|
|
198
|
-
|
|
199
246
|
const handleAssetDrop = useCallback(
|
|
200
247
|
(e: React.DragEvent) => {
|
|
201
248
|
e.preventDefault();
|
|
202
249
|
const canCommit = activeDropEpochRef.current === sessionEpoch;
|
|
203
250
|
clearDropPreview();
|
|
204
251
|
if (!canCommit) return;
|
|
205
|
-
const
|
|
252
|
+
const types = Array.from(e.dataTransfer.types);
|
|
253
|
+
const placement = resolveDropPlacement(e.clientX, e.clientY, canInsertTrackFor(types));
|
|
206
254
|
|
|
207
255
|
const compositionPayload = parseTimelineCompositionPayload(
|
|
208
256
|
e.dataTransfer.getData(TIMELINE_COMPOSITION_MIME),
|
|
@@ -240,6 +288,7 @@ export function useTimelineAssetDrop({
|
|
|
240
288
|
|
|
241
289
|
return {
|
|
242
290
|
isDragOver,
|
|
291
|
+
dropPreview,
|
|
243
292
|
handleAssetDragOver,
|
|
244
293
|
handleAssetDragLeave,
|
|
245
294
|
handleAssetDrop,
|
|
@@ -42,10 +42,9 @@ export function getTimelineEditCapabilities(input: {
|
|
|
42
42
|
playbackStart?: number;
|
|
43
43
|
playbackStartAttr?: "media-start" | "playback-start";
|
|
44
44
|
sourceDuration?: number;
|
|
45
|
-
timingSource?: "authored" | "implicit";
|
|
46
45
|
timelineLocked?: boolean;
|
|
47
46
|
}): TimelineEditCapabilities {
|
|
48
|
-
if (input.
|
|
47
|
+
if (input.timelineLocked) {
|
|
49
48
|
return { canMove: false, canTrimStart: false, canTrimEnd: false };
|
|
50
49
|
}
|
|
51
50
|
|
|
@@ -412,21 +412,6 @@ describe("getTimelineEditCapabilities", () => {
|
|
|
412
412
|
});
|
|
413
413
|
});
|
|
414
414
|
|
|
415
|
-
it("keeps implicit layout layers selectable but not timeline-editable", () => {
|
|
416
|
-
expect(
|
|
417
|
-
getTimelineEditCapabilities({
|
|
418
|
-
duration: 8,
|
|
419
|
-
selector: ".scene-shell",
|
|
420
|
-
tag: "div",
|
|
421
|
-
timingSource: "implicit",
|
|
422
|
-
}),
|
|
423
|
-
).toEqual({
|
|
424
|
-
canMove: false,
|
|
425
|
-
canTrimStart: false,
|
|
426
|
-
canTrimEnd: false,
|
|
427
|
-
});
|
|
428
|
-
});
|
|
429
|
-
|
|
430
415
|
it("allows move and both trims for patchable media clips with offset support", () => {
|
|
431
416
|
expect(
|
|
432
417
|
getTimelineEditCapabilities({
|
|
@@ -496,7 +481,6 @@ describe("getTimelineEditCapabilities", () => {
|
|
|
496
481
|
tag: "div",
|
|
497
482
|
duration: 4,
|
|
498
483
|
selector: "#hero-card",
|
|
499
|
-
timingSource: "authored",
|
|
500
484
|
}),
|
|
501
485
|
).toEqual({
|
|
502
486
|
canMove: true,
|
|
@@ -34,11 +34,9 @@ describe("buildTimelineGroupResizeMembers (legacy 36413da7f semantics)", () => {
|
|
|
34
34
|
expect(buildTimelineGroupResizeMembers([a, locked], keys("a", "b"), "a", "end")).toBeNull();
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
it("degrades when a member
|
|
37
|
+
it("degrades when a member has no patch target", () => {
|
|
38
38
|
const a = el("a");
|
|
39
|
-
const implicit = el("b", { timingSource: "implicit" });
|
|
40
39
|
const noTarget = el("c", { domId: undefined, selector: undefined });
|
|
41
|
-
expect(buildTimelineGroupResizeMembers([a, implicit], keys("a", "b"), "a", "end")).toBeNull();
|
|
42
40
|
expect(buildTimelineGroupResizeMembers([a, noTarget], keys("a", "c"), "a", "end")).toBeNull();
|
|
43
41
|
});
|
|
44
42
|
|
|
@@ -198,7 +198,6 @@ function canTrimEdge(element: TimelineElement, edge: TimelineGroupResizeEdge): b
|
|
|
198
198
|
playbackStart: element.playbackStart,
|
|
199
199
|
playbackStartAttr: element.playbackStartAttr,
|
|
200
200
|
sourceDuration: element.sourceDuration,
|
|
201
|
-
timingSource: element.timingSource,
|
|
202
201
|
timelineLocked: element.timelineLocked,
|
|
203
202
|
});
|
|
204
203
|
return edge === "start" ? caps.canTrimStart : caps.canTrimEnd;
|
|
@@ -8,6 +8,7 @@ import type { TrackVisualStyle } from "./timelineIcons";
|
|
|
8
8
|
import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./useTimelineClipDrag";
|
|
9
9
|
import type { TimelineClipIndex, TimelineTimeRange } from "../lib/timelineClipIndex";
|
|
10
10
|
import type { TimelineRowGeometry } from "./timelineLayout";
|
|
11
|
+
import type { TimelineSnapTarget } from "./timelineSnapping";
|
|
11
12
|
import type { TimelineVirtualRow } from "./useTimelineVirtualRows";
|
|
12
13
|
import type { MultiDragPreviewInput } from "./timelineMultiDragPreview";
|
|
13
14
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
@@ -112,6 +113,8 @@ export interface TimelineLaneBaseProps {
|
|
|
112
113
|
export interface TimelineLanesProps extends TimelineLaneBaseProps {
|
|
113
114
|
/** Live-derived by TimelineCanvas from {@link TimelineLaneBaseProps.draggedClip}. */
|
|
114
115
|
draggedElement: TimelineElement | null;
|
|
116
|
+
/** Live move or trim snap target, resolved once by TimelineCanvas. */
|
|
117
|
+
snapGuide: TimelineSnapTarget | null;
|
|
115
118
|
multiDragPreview: MultiDragPreviewInput | null;
|
|
116
119
|
onToggleTrackHidden: TimelineEditCallbacks["onToggleTrackHidden"];
|
|
117
120
|
onTogglePropertyGroupKeyframe: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"];
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
collectTimelineSnapTargets,
|
|
5
5
|
snapMoveToTargets,
|
|
6
6
|
snapTimelineTime,
|
|
7
|
+
resolveSnapGuide,
|
|
7
8
|
} from "./timelineSnapping";
|
|
8
9
|
|
|
9
10
|
describe("collectTimelineSnapTargets", () => {
|
|
@@ -26,6 +27,18 @@ describe("collectTimelineSnapTargets", () => {
|
|
|
26
27
|
expect(targets).toContainEqual({ time: 0.5, type: "beat" });
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
it("omits the playhead when includePlayhead is false, for a trim", () => {
|
|
31
|
+
const targets = collectTimelineSnapTargets({
|
|
32
|
+
elements,
|
|
33
|
+
playheadTime: 7.25,
|
|
34
|
+
beatTimes: [0.5],
|
|
35
|
+
includePlayhead: false,
|
|
36
|
+
});
|
|
37
|
+
expect(targets.some((t) => t.type === "playhead")).toBe(false);
|
|
38
|
+
expect(targets).toContainEqual({ time: 2, type: "clip-edge" });
|
|
39
|
+
expect(targets).toContainEqual({ time: 0.5, type: "beat" });
|
|
40
|
+
});
|
|
41
|
+
|
|
29
42
|
it("excludes the dragged element's own edges", () => {
|
|
30
43
|
const targets = collectTimelineSnapTargets({
|
|
31
44
|
elements,
|
|
@@ -132,3 +145,17 @@ describe("snapMoveToTargets", () => {
|
|
|
132
145
|
expect(r.snapTime).toBeNull();
|
|
133
146
|
});
|
|
134
147
|
});
|
|
148
|
+
|
|
149
|
+
describe("resolveSnapGuide", () => {
|
|
150
|
+
it("prefers a started move, falls back to a trim, and is null when neither snapped", () => {
|
|
151
|
+
const move = { started: true, snapTime: 2, snapType: "playhead" as const };
|
|
152
|
+
const trim = { snapTime: 5, snapType: "clip-edge" as const };
|
|
153
|
+
expect(resolveSnapGuide(move, trim)).toEqual({ time: 2, type: "playhead" });
|
|
154
|
+
expect(resolveSnapGuide({ ...move, started: false }, trim)).toEqual({
|
|
155
|
+
time: 5,
|
|
156
|
+
type: "clip-edge",
|
|
157
|
+
});
|
|
158
|
+
expect(resolveSnapGuide(null, { snapTime: null, snapType: null })).toBeNull();
|
|
159
|
+
expect(resolveSnapGuide(null, null)).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -7,6 +7,16 @@ export interface TimelineSnapTarget {
|
|
|
7
7
|
type: TimelineSnapType;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** The guide the canvas draws: the live move's snap target, else the live trim's. */
|
|
11
|
+
export function resolveSnapGuide(
|
|
12
|
+
moving: { started: boolean; snapTime: number | null; snapType: TimelineSnapType | null } | null,
|
|
13
|
+
trimming: { snapTime?: number | null; snapType?: TimelineSnapType | null } | null,
|
|
14
|
+
): TimelineSnapTarget | null {
|
|
15
|
+
const source = moving?.started ? moving : trimming;
|
|
16
|
+
if (source?.snapTime == null || source.snapType == null) return null;
|
|
17
|
+
return { time: source.snapTime, type: source.snapType };
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
/** Pixel radius within which a time snaps to a target (matches historical beat snap). */
|
|
11
21
|
export const TIMELINE_SNAP_PX = 8;
|
|
12
22
|
|
|
@@ -21,6 +31,8 @@ export function collectTimelineSnapTargets(input: {
|
|
|
21
31
|
playheadTime: number | null;
|
|
22
32
|
beatTimes: readonly number[];
|
|
23
33
|
excludeElementKey?: string | null;
|
|
34
|
+
/** A trim excludes the playhead: the dragged edge drives it, so snapping to it is circular. */
|
|
35
|
+
includePlayhead?: boolean;
|
|
24
36
|
}): TimelineSnapTarget[] {
|
|
25
37
|
const byTime = new Map<number, TimelineSnapTarget>();
|
|
26
38
|
const add = (time: number, type: TimelineSnapType) => {
|
|
@@ -38,7 +50,8 @@ export function collectTimelineSnapTargets(input: {
|
|
|
38
50
|
add(el.start, "clip-edge");
|
|
39
51
|
add(el.start + el.duration, "clip-edge");
|
|
40
52
|
}
|
|
41
|
-
if (input.playheadTime != null
|
|
53
|
+
if (input.playheadTime != null && input.includePlayhead !== false)
|
|
54
|
+
add(input.playheadTime, "playhead");
|
|
42
55
|
|
|
43
56
|
return Array.from(byTime.values()).sort((a, b) => a.time - b.time);
|
|
44
57
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `TimelineElement.track` is a z-order SORT key, not a row number: an expanded
|
|
5
5
|
* sub-composition child gets a synthesized fractional key (`host.track + n /
|
|
6
|
-
* (siblings + 2)`, see `
|
|
6
|
+
* (siblings + 2)`, see `useTimelineRowElements`), so putting it in a string
|
|
7
7
|
* announces "Hide track 0.16666666666666666". Every user-visible track number,
|
|
8
8
|
* whether it is rendered by a component or baked into an undo-history label,
|
|
9
9
|
* routes through here; the raw key stays in callbacks and lookups only.
|
|
@@ -2,7 +2,6 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
import type { TimelineElement } from "../store/playerStore";
|
|
3
3
|
import type { ClipManifestClip } from "../lib/playbackTypes";
|
|
4
4
|
import { createTimelineElementFromManifestClip } from "../lib/timelineDOM";
|
|
5
|
-
import { buildExpandedElements } from "../hooks/useExpandedTimelineElements";
|
|
6
5
|
import { normalizeToZones } from "./timelineZones";
|
|
7
6
|
import { commitDraggedClipMove, type TimelineMoveEdit } from "./timelineClipDragCommit";
|
|
8
7
|
import type { DraggedClipState } from "./useTimelineClipDrag";
|
|
@@ -113,56 +112,4 @@ describe("track persist pipeline (manifest → factory → lanes → drag commit
|
|
|
113
112
|
const up = commitLaneChange(byId.get("g")!, 0, elements, [0, 1, 2]);
|
|
114
113
|
expect(up[0].updates.track).toBe(3); // authored, NOT display lane 0
|
|
115
114
|
});
|
|
116
|
-
|
|
117
|
-
it("an expanded sub-comp child's lane change persists the sibling's authored track from ITS file", () => {
|
|
118
|
-
// Host timeline: the sub-comp host plus a root clip, discovered through the
|
|
119
|
-
// factory and lane-normalized like the store does.
|
|
120
|
-
const hostManifest = [
|
|
121
|
-
manifestClip({
|
|
122
|
-
id: "scene",
|
|
123
|
-
kind: "composition",
|
|
124
|
-
tagName: "div",
|
|
125
|
-
track: 0,
|
|
126
|
-
start: 0,
|
|
127
|
-
duration: 10,
|
|
128
|
-
compositionId: "scene",
|
|
129
|
-
compositionSrc: "scene.html",
|
|
130
|
-
}),
|
|
131
|
-
manifestClip({ id: "root-clip", kind: "video", tagName: "video", track: 1, start: 0 }),
|
|
132
|
-
];
|
|
133
|
-
// scene.html has SPARSE authored tracks 3 and 7.
|
|
134
|
-
const childClips = [
|
|
135
|
-
manifestClip({ id: "c3", track: 3, start: 1, duration: 2, parentCompositionId: "scene" }),
|
|
136
|
-
manifestClip({ id: "c7", track: 7, start: 4, duration: 2, parentCompositionId: "scene" }),
|
|
137
|
-
];
|
|
138
|
-
const storeElements = normalizeToZones(fromManifest(hostManifest));
|
|
139
|
-
const parentMap = new Map([
|
|
140
|
-
["c3", "scene"],
|
|
141
|
-
["c7", "scene"],
|
|
142
|
-
]);
|
|
143
|
-
const expanded = buildExpandedElements(
|
|
144
|
-
storeElements,
|
|
145
|
-
[...hostManifest, ...childClips],
|
|
146
|
-
parentMap,
|
|
147
|
-
"scene",
|
|
148
|
-
"scene",
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
// The children replaced the host row: synthetic display lanes, but the
|
|
152
|
-
// authored track (in scene.html's coordinate space) survived the expansion.
|
|
153
|
-
const c3 = expanded.find((e) => e.domId === "c3")!;
|
|
154
|
-
const c7 = expanded.find((e) => e.domId === "c7")!;
|
|
155
|
-
expect(c3).toMatchObject({ authoredTrack: 3, sourceFile: "scene.html" });
|
|
156
|
-
expect(c7).toMatchObject({ authoredTrack: 7, sourceFile: "scene.html" });
|
|
157
|
-
expect(c3.stackingContextId).toBe("root");
|
|
158
|
-
expect(c3.track).not.toBe(3); // display row is synthetic
|
|
159
|
-
|
|
160
|
-
// Drag c3 onto c7's display lane: the persist target is c3's OWN file, so
|
|
161
|
-
// the written track must be c7's authored 7 — not the display-lane integer.
|
|
162
|
-
const trackOrder = [...new Set(expanded.map((e) => e.track))].sort((a, b) => a - b);
|
|
163
|
-
const edits = commitLaneChange(c3, c7.track, expanded, trackOrder);
|
|
164
|
-
expect(edits).toHaveLength(1);
|
|
165
|
-
expect(edits[0].updates.track).toBe(7);
|
|
166
|
-
expect(edits[0].updates.track).not.toBe(c7.track);
|
|
167
|
-
});
|
|
168
115
|
});
|
|
@@ -39,7 +39,7 @@ function PropertyGroupNavigation({
|
|
|
39
39
|
// bigger box; focus-visible matches every other control in this header.
|
|
40
40
|
const CHEVRON_BUTTON_CLASS =
|
|
41
41
|
"relative h-5 w-3 border-0 bg-transparent p-0 text-white/55 hover:text-white disabled:text-white/15 " +
|
|
42
|
-
"focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] " +
|
|
42
|
+
"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] " +
|
|
43
43
|
"before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 " +
|
|
44
44
|
"before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']";
|
|
45
45
|
const seekTo = (keyframe: { percentage: number } | null) => {
|
|
@@ -154,7 +154,7 @@ export function PropertyGroupHeaderRow({
|
|
|
154
154
|
aria-label={`${navigation.currentKeyframe ? "Remove" : "Add"} ${label} keyframe`}
|
|
155
155
|
title={`${navigation.currentKeyframe ? "Remove" : "Add"} ${label} keyframe`}
|
|
156
156
|
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target; the ◆ glyph stays 11px.
|
|
157
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-[#3CE6AC] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
157
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-[#3CE6AC] focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
158
158
|
onClick={(event) => {
|
|
159
159
|
// Same as the disclosure caret and the eye: a control in the label
|
|
160
160
|
// column owns its click, it does not also hit the track row behind it.
|
|
@@ -262,7 +262,7 @@ export function AutomationLaneHeaderRow({
|
|
|
262
262
|
aria-label={onReveal ? `Show ${label} in the effect rack` : undefined}
|
|
263
263
|
title={onReveal ? `Show ${label} in the effect rack` : label}
|
|
264
264
|
disabled={!onReveal}
|
|
265
|
-
className="flex min-w-0 flex-1 flex-col justify-center rounded border-0 bg-transparent p-0 text-left leading-tight enabled:hover:text-white focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
265
|
+
className="flex min-w-0 flex-1 flex-col justify-center rounded-sm border-0 bg-transparent p-0 text-left leading-tight enabled:hover:text-white focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
266
266
|
onPointerDown={(event) => event.stopPropagation()}
|
|
267
267
|
onClick={(event) => {
|
|
268
268
|
// The label column owns its click; it does not also fall through to
|
|
@@ -311,7 +311,7 @@ export function AutomationLaneHeaderRow({
|
|
|
311
311
|
aria-label={`Remove ${label} automation`}
|
|
312
312
|
title={`Remove ${label} automation`}
|
|
313
313
|
// h-6 w-6 is the 24x24 WCAG 2.2 target; the glyph stays small.
|
|
314
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-white/35 hover:text-white focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
314
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-white/35 hover:text-white focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
315
315
|
onPointerDown={(event) => event.stopPropagation()}
|
|
316
316
|
onClick={(event) => {
|
|
317
317
|
// A control in the label column owns its click; it does not also hit
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
3
|
+
import type { ClipContextMenuState } from "./TimelineOverlays";
|
|
4
|
+
|
|
5
|
+
/** A clicked member of an active multi-selection keeps the whole group
|
|
6
|
+
* selected, so the context menu it opens acts on all of it. */
|
|
7
|
+
export function useClipContextMenu(
|
|
8
|
+
onSelectElement: ((element: TimelineElement | null) => void) | undefined,
|
|
9
|
+
dismissGapMenu: () => void,
|
|
10
|
+
setClipContextMenu: (state: ClipContextMenuState | null) => void,
|
|
11
|
+
) {
|
|
12
|
+
const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
|
|
13
|
+
const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
|
|
14
|
+
return useCallback(
|
|
15
|
+
(e: React.MouseEvent, el: TimelineElement) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
const id = el.key ?? el.id;
|
|
18
|
+
if (!(selectedElementIds.size > 1 && selectedElementIds.has(id))) {
|
|
19
|
+
setSelectedElementId(id);
|
|
20
|
+
onSelectElement?.(el);
|
|
21
|
+
}
|
|
22
|
+
dismissGapMenu();
|
|
23
|
+
setClipContextMenu({
|
|
24
|
+
x: e.clientX,
|
|
25
|
+
y: e.clientY,
|
|
26
|
+
element: el,
|
|
27
|
+
sessionEpoch: usePlayerStore.getState().timelineSessionEpoch,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
[selectedElementIds, setSelectedElementId, onSelectElement, dismissGapMenu, setClipContextMenu],
|
|
31
|
+
);
|
|
32
|
+
}
|