@hyperframes/studio 0.7.54 → 0.7.56
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/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40582 -37803
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +115 -0
- package/src/components/editor/CanvasContextMenu.tsx +198 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +158 -126
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +43 -1
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +208 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +240 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.tsx +326 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +39 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +12 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +303 -0
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +477 -0
- package/src/player/components/timelineCollision.ts +263 -0
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
- package/src/player/components/timelineMultiDragPreview.ts +106 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineSnapping.test.ts +134 -0
- package/src/player/components/timelineSnapping.ts +110 -0
- package/src/player/components/timelineStackingSync.test.ts +244 -0
- package/src/player/components/timelineStackingSync.ts +331 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineZones.test.ts +425 -0
- package/src/player/components/timelineZones.ts +198 -0
- package/src/player/components/timelineZoom.test.ts +67 -0
- package/src/player/components/timelineZoom.ts +51 -0
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
- package/src/player/index.ts +3 -2
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetClickBehavior.test.ts +104 -0
- package/src/utils/assetClickBehavior.ts +75 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/canvasNudgeGate.test.ts +31 -0
- package/src/utils/canvasNudgeGate.ts +32 -0
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUiPreferences.test.ts +38 -0
- package/src/utils/studioUiPreferences.ts +27 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/src/utils/timelineInspector.test.ts +121 -0
- package/src/utils/timelineInspector.ts +32 -1
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/assets/index-pRhCpGPz.js +0 -423
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import type { TimelineElement } from "../../player";
|
|
3
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
4
|
+
import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing";
|
|
5
|
+
import type { TimelineEditCallbacks } from "../../player/components/timelineCallbacks";
|
|
6
|
+
import { useStudioShellContext } from "../../contexts/StudioContext";
|
|
7
|
+
import {
|
|
8
|
+
useDomEditActionsContext,
|
|
9
|
+
useDomEditSelectionContext,
|
|
10
|
+
} from "../../contexts/DomEditContext";
|
|
11
|
+
import { resolveTweenStart, resolveTweenDuration } from "../../utils/globalTimeCompiler";
|
|
12
|
+
import { resolveClipTimingBasis } from "../../hooks/useGsapTweenCache";
|
|
13
|
+
import { resolveKeyframeRetime } from "../editor/keyframeRetime";
|
|
14
|
+
import type { TimelineMoveOperation } from "../../hooks/timelineMoveAdapter";
|
|
15
|
+
|
|
16
|
+
export interface TimelineEditCallbackDeps {
|
|
17
|
+
handleTimelineElementMove: (
|
|
18
|
+
element: TimelineElement,
|
|
19
|
+
updates: Pick<TimelineElement, "start" | "track">,
|
|
20
|
+
) => Promise<void> | void;
|
|
21
|
+
handleTimelineElementsMove: (
|
|
22
|
+
edits: Array<{ element: TimelineElement; updates: Pick<TimelineElement, "start" | "track"> }>,
|
|
23
|
+
coalesceKey?: string,
|
|
24
|
+
operation?: TimelineMoveOperation,
|
|
25
|
+
) => Promise<void> | void;
|
|
26
|
+
handleTimelineElementResize: (
|
|
27
|
+
element: TimelineElement,
|
|
28
|
+
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
|
|
29
|
+
) => Promise<void> | void;
|
|
30
|
+
handleTimelineGroupResize: NonNullable<TimelineEditCallbacks["onResizeElements"]>;
|
|
31
|
+
handleToggleTrackHidden: (track: number, hidden: boolean) => Promise<void> | void;
|
|
32
|
+
handleBlockedTimelineEdit: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
|
|
33
|
+
handleTimelineElementSplit: (element: TimelineElement, splitTime: number) => Promise<void> | void;
|
|
34
|
+
handleRazorSplit: (element: TimelineElement, splitTime: number) => Promise<void> | void;
|
|
35
|
+
handleRazorSplitAll: (splitTime: number) => Promise<void> | void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds the timeline edit callback bag (move/resize/split/razor plus the
|
|
40
|
+
* keyframe-diamond callbacks) provided to `<Timeline>` via TimelineEditProvider.
|
|
41
|
+
* The keyframe callbacks resolve the dragged diamond back to its GSAP anim id +
|
|
42
|
+
* tween-relative percentage, reading DOM-edit selection state from context.
|
|
43
|
+
*/
|
|
44
|
+
// fallow-ignore-next-line complexity
|
|
45
|
+
export function useTimelineEditCallbacks({
|
|
46
|
+
handleTimelineElementMove,
|
|
47
|
+
handleTimelineElementsMove,
|
|
48
|
+
handleTimelineElementResize,
|
|
49
|
+
handleTimelineGroupResize,
|
|
50
|
+
handleToggleTrackHidden,
|
|
51
|
+
handleBlockedTimelineEdit,
|
|
52
|
+
handleTimelineElementSplit,
|
|
53
|
+
handleRazorSplit,
|
|
54
|
+
handleRazorSplitAll,
|
|
55
|
+
}: TimelineEditCallbackDeps): TimelineEditCallbacks {
|
|
56
|
+
const { projectId, activeCompPath } = useStudioShellContext();
|
|
57
|
+
const { domEditSelection, selectedGsapAnimations } = useDomEditSelectionContext();
|
|
58
|
+
const {
|
|
59
|
+
handleGsapRemoveKeyframe,
|
|
60
|
+
handleGsapMoveKeyframeToPlayhead,
|
|
61
|
+
handleGsapMoveKeyframe,
|
|
62
|
+
handleGsapResizeKeyframedTween,
|
|
63
|
+
handleGsapUpdateMeta,
|
|
64
|
+
handleGsapAddKeyframe,
|
|
65
|
+
handleGsapConvertToKeyframes,
|
|
66
|
+
handleGsapRemoveAllKeyframes,
|
|
67
|
+
buildDomSelectionForTimelineElement,
|
|
68
|
+
} = useDomEditActionsContext();
|
|
69
|
+
|
|
70
|
+
// Resolve a timeline-diamond callback's clip-% to the keyframe's anim id + its
|
|
71
|
+
// tween-relative percentage (shared by the delete/move keyframe callbacks): the
|
|
72
|
+
// diamond reports a clip-% but the script ops key on the tween-%. Prefers the
|
|
73
|
+
// anim in the keyframe's property group, falling back to the first keyframed one.
|
|
74
|
+
const resolveKeyframeTarget = useCallback(
|
|
75
|
+
// fallow-ignore-next-line complexity
|
|
76
|
+
(pct: number): { animId: string; tweenPct: number } | null => {
|
|
77
|
+
const cached = usePlayerStore.getState().keyframeCache.get(domEditSelection?.id ?? "");
|
|
78
|
+
const kf = cached?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
|
|
79
|
+
const group = kf?.propertyGroup;
|
|
80
|
+
const anim =
|
|
81
|
+
(group ? selectedGsapAnimations.find((a) => a.propertyGroup === group) : undefined) ??
|
|
82
|
+
selectedGsapAnimations.find((a) => a.keyframes);
|
|
83
|
+
return anim ? { animId: anim.id, tweenPct: kf?.tweenPercentage ?? pct } : null;
|
|
84
|
+
},
|
|
85
|
+
[domEditSelection?.id, selectedGsapAnimations],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return useMemo(
|
|
89
|
+
() => ({
|
|
90
|
+
onMoveElement: handleTimelineElementMove,
|
|
91
|
+
onMoveElements: handleTimelineElementsMove,
|
|
92
|
+
onResizeElement: handleTimelineElementResize,
|
|
93
|
+
onResizeElements: handleTimelineGroupResize,
|
|
94
|
+
onToggleTrackHidden: handleToggleTrackHidden,
|
|
95
|
+
onBlockedEditAttempt: handleBlockedTimelineEdit,
|
|
96
|
+
onSplitElement: handleTimelineElementSplit,
|
|
97
|
+
onRazorSplit: handleRazorSplit,
|
|
98
|
+
onRazorSplitAll: handleRazorSplitAll,
|
|
99
|
+
onDeleteAllKeyframes: () => {
|
|
100
|
+
// Hold the element where it is (collapse keyframes to a static set) rather
|
|
101
|
+
// than deleting the whole animation — deleting strands a stale GSAP base
|
|
102
|
+
// that the next drag adds to, flinging the element off-screen.
|
|
103
|
+
const anim = selectedGsapAnimations.find((a) => a.keyframes);
|
|
104
|
+
if (!anim) return;
|
|
105
|
+
handleGsapRemoveAllKeyframes(anim.id);
|
|
106
|
+
},
|
|
107
|
+
onDeleteKeyframe: (_elId: string, pct: number) => {
|
|
108
|
+
const target = resolveKeyframeTarget(pct);
|
|
109
|
+
if (target) handleGsapRemoveKeyframe(target.animId, target.tweenPct);
|
|
110
|
+
},
|
|
111
|
+
// Retime the keyframe to the playhead, preserving its value + ease.
|
|
112
|
+
onMoveKeyframeToPlayhead: (_elId: string, pct: number) => {
|
|
113
|
+
const target = resolveKeyframeTarget(pct);
|
|
114
|
+
if (target) handleGsapMoveKeyframeToPlayhead(target.animId, target.tweenPct);
|
|
115
|
+
},
|
|
116
|
+
// Drag-to-retime. The diamond reports clip-%s; resolveKeyframeTarget gives
|
|
117
|
+
// the dragged keyframe's anim + tween-%. We convert the clip-% drop to an
|
|
118
|
+
// absolute time (via the clip's timing basis) and let resolveKeyframeRetime
|
|
119
|
+
// decide: a drop inside the tween window is a plain move (re-key tween-%); a
|
|
120
|
+
// drop past the boundary (last keyframe past the end, first before the start)
|
|
121
|
+
// resizes the tween — position/duration grow so the dragged keyframe lands at
|
|
122
|
+
// the drop while every other keyframe keeps its absolute time (value+ease too).
|
|
123
|
+
// fallow-ignore-next-line complexity
|
|
124
|
+
onMoveKeyframe: (_elId: string, fromClipPct: number, toClipPct: number) => {
|
|
125
|
+
const target = resolveKeyframeTarget(fromClipPct);
|
|
126
|
+
const sel = domEditSelection;
|
|
127
|
+
if (!target || !sel) return;
|
|
128
|
+
const anim = selectedGsapAnimations.find((a) => a.id === target.animId);
|
|
129
|
+
const tweenStart = anim ? resolveTweenStart(anim) : null;
|
|
130
|
+
if (!anim || tweenStart === null) return;
|
|
131
|
+
const tweenDuration = anim.duration ?? resolveTweenDuration(anim);
|
|
132
|
+
const sourceFile = sel.sourceFile || activeCompPath || "index.html";
|
|
133
|
+
const { elements, domClipChildren } = usePlayerStore.getState();
|
|
134
|
+
const { elStart, elDuration } = resolveClipTimingBasis(
|
|
135
|
+
sel.id ?? "",
|
|
136
|
+
sourceFile,
|
|
137
|
+
elements,
|
|
138
|
+
domClipChildren,
|
|
139
|
+
);
|
|
140
|
+
const dropAbsTime = elStart + (toClipPct / 100) * elDuration;
|
|
141
|
+
const decision = resolveKeyframeRetime({
|
|
142
|
+
keyframes: anim.keyframes?.keyframes ?? [],
|
|
143
|
+
draggedTweenPct: target.tweenPct,
|
|
144
|
+
tweenStart,
|
|
145
|
+
tweenDuration,
|
|
146
|
+
dropAbsTime,
|
|
147
|
+
});
|
|
148
|
+
if (decision.kind === "move" && decision.toTweenPct != null) {
|
|
149
|
+
handleGsapMoveKeyframe(target.animId, target.tweenPct, decision.toTweenPct);
|
|
150
|
+
} else if (
|
|
151
|
+
decision.kind === "resize" &&
|
|
152
|
+
decision.pctRemap &&
|
|
153
|
+
decision.position != null &&
|
|
154
|
+
decision.duration != null
|
|
155
|
+
) {
|
|
156
|
+
handleGsapResizeKeyframedTween(
|
|
157
|
+
target.animId,
|
|
158
|
+
decision.position,
|
|
159
|
+
decision.duration,
|
|
160
|
+
decision.pctRemap,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
onChangeKeyframeEase: (_elId: string, _pct: number, ease: string) => {
|
|
165
|
+
for (const anim of selectedGsapAnimations) {
|
|
166
|
+
if (anim.keyframes) handleGsapUpdateMeta(anim.id, { ease });
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
// fallow-ignore-next-line complexity
|
|
170
|
+
onToggleKeyframeAtPlayhead: (el: TimelineElement) => {
|
|
171
|
+
const currentTime = usePlayerStore.getState().currentTime;
|
|
172
|
+
const pct =
|
|
173
|
+
el.duration > 0
|
|
174
|
+
? Math.max(0, Math.min(100, Math.round(((currentTime - el.start) / el.duration) * 100)))
|
|
175
|
+
: 0;
|
|
176
|
+
const anim = selectedGsapAnimations.find((a) => a.keyframes);
|
|
177
|
+
if (anim?.keyframes) {
|
|
178
|
+
const existing = anim.keyframes.keyframes.find((k) => Math.abs(k.percentage - pct) <= 1);
|
|
179
|
+
if (existing) {
|
|
180
|
+
handleGsapRemoveKeyframe(anim.id, existing.percentage);
|
|
181
|
+
} else {
|
|
182
|
+
handleGsapAddKeyframe(anim.id, pct, "x", 0);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
const flatAnim = selectedGsapAnimations.find((a) => !a.keyframes);
|
|
186
|
+
if (flatAnim) handleGsapConvertToKeyframes(flatAnim.id);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
191
|
+
[
|
|
192
|
+
handleTimelineElementMove,
|
|
193
|
+
handleTimelineElementsMove,
|
|
194
|
+
handleTimelineElementResize,
|
|
195
|
+
handleTimelineGroupResize,
|
|
196
|
+
handleToggleTrackHidden,
|
|
197
|
+
handleBlockedTimelineEdit,
|
|
198
|
+
handleTimelineElementSplit,
|
|
199
|
+
handleRazorSplit,
|
|
200
|
+
handleRazorSplitAll,
|
|
201
|
+
handleGsapRemoveAllKeyframes,
|
|
202
|
+
resolveKeyframeTarget,
|
|
203
|
+
selectedGsapAnimations,
|
|
204
|
+
handleGsapRemoveKeyframe,
|
|
205
|
+
handleGsapMoveKeyframeToPlayhead,
|
|
206
|
+
handleGsapMoveKeyframe,
|
|
207
|
+
handleGsapResizeKeyframedTween,
|
|
208
|
+
handleGsapUpdateMeta,
|
|
209
|
+
handleGsapAddKeyframe,
|
|
210
|
+
handleGsapConvertToKeyframes,
|
|
211
|
+
buildDomSelectionForTimelineElement,
|
|
212
|
+
projectId,
|
|
213
|
+
activeCompPath,
|
|
214
|
+
domEditSelection,
|
|
215
|
+
],
|
|
216
|
+
);
|
|
217
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssetCard and FontRow — visual asset tile / row components for the Assets panel.
|
|
3
|
+
* Extracted from AssetsTab.tsx to keep that file under the 600-line CI gate.
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
6
|
+
import { VideoFrameThumbnail } from "../ui/VideoFrameThumbnail";
|
|
7
|
+
import { VIDEO_EXT, IMAGE_EXT } from "../../utils/mediaTypes";
|
|
8
|
+
import { TIMELINE_ASSET_MIME } from "../../utils/timelineAssetDrop";
|
|
9
|
+
import { ContextMenu } from "./AssetContextMenu";
|
|
10
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
11
|
+
import { useAssetPreviewStore } from "../../utils/assetPreviewStore";
|
|
12
|
+
import { findClipForAsset, isPointerClick } from "../../utils/assetClickBehavior";
|
|
13
|
+
import { basename, ext, truncateMiddle, formatDuration } from "./assetHelpers";
|
|
14
|
+
import { resolveMediaPreviewUrl } from "../../player/components/thumbnailUtils";
|
|
15
|
+
|
|
16
|
+
/** Drag payload writer shared by the asset tile and the font row: copy effect
|
|
17
|
+
* plus the timeline-asset MIME and a plain-text path fallback. */
|
|
18
|
+
function writeAssetDragData(e: React.DragEvent, asset: string): void {
|
|
19
|
+
e.dataTransfer.effectAllowed = "copy";
|
|
20
|
+
e.dataTransfer.setData(TIMELINE_ASSET_MIME, JSON.stringify({ path: asset }));
|
|
21
|
+
e.dataTransfer.setData("text/plain", asset);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Open the row/tile context menu at the pointer, shared by asset tile + font row. */
|
|
25
|
+
function openAssetContextMenu(
|
|
26
|
+
e: React.MouseEvent,
|
|
27
|
+
setContextMenu: (menu: { x: number; y: number }) => void,
|
|
28
|
+
): void {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
setContextMenu({ x: e.clientX, y: e.clientY });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Lazily probe a video/audio URL for its duration via a hidden HTMLVideoElement
|
|
35
|
+
* (`preload="metadata"`). The manifest only covers ~/.media assets, so project
|
|
36
|
+
* assets in assets/ have no manifest entry — this fills the gap.
|
|
37
|
+
* Returns `undefined` until the probe completes; `null` if it failed.
|
|
38
|
+
*/
|
|
39
|
+
function useProbedDuration(src: string, skip: boolean): number | null | undefined {
|
|
40
|
+
const [duration, setDuration] = useState<number | null | undefined>(undefined);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (skip) return;
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
45
|
+
// The in-flight probe element, so unmount cleanup can abort its network
|
|
46
|
+
// fetch (clearing `src`) instead of leaving it to finish in the background.
|
|
47
|
+
let liveVid: HTMLVideoElement | null = null;
|
|
48
|
+
|
|
49
|
+
function teardown(vid: HTMLVideoElement) {
|
|
50
|
+
vid.onloadedmetadata = null;
|
|
51
|
+
vid.onerror = null;
|
|
52
|
+
vid.src = "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function probe(attempt: number) {
|
|
56
|
+
if (cancelled) return;
|
|
57
|
+
const vid = document.createElement("video");
|
|
58
|
+
liveVid = vid;
|
|
59
|
+
vid.preload = "metadata";
|
|
60
|
+
vid.muted = true;
|
|
61
|
+
vid.onloadedmetadata = () => {
|
|
62
|
+
const d = Number.isFinite(vid.duration) && vid.duration > 0 ? vid.duration : null;
|
|
63
|
+
teardown(vid);
|
|
64
|
+
if (!cancelled) setDuration(d);
|
|
65
|
+
};
|
|
66
|
+
vid.onerror = () => {
|
|
67
|
+
teardown(vid);
|
|
68
|
+
if (!cancelled) {
|
|
69
|
+
if (attempt < 1) retryTimer = setTimeout(() => probe(attempt + 1), 50);
|
|
70
|
+
else setDuration(null);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
vid.src = src;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
probe(0);
|
|
77
|
+
return () => {
|
|
78
|
+
cancelled = true;
|
|
79
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
80
|
+
if (liveVid) teardown(liveVid);
|
|
81
|
+
};
|
|
82
|
+
}, [src, skip]);
|
|
83
|
+
return duration;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface AssetCardProps {
|
|
87
|
+
projectId: string;
|
|
88
|
+
asset: string;
|
|
89
|
+
used: boolean;
|
|
90
|
+
duration?: number;
|
|
91
|
+
onCopy: (path: string) => void;
|
|
92
|
+
isCopied: boolean;
|
|
93
|
+
onDelete?: (path: string) => void;
|
|
94
|
+
onRename?: (oldPath: string, newPath: string) => void;
|
|
95
|
+
onAddAssetToTimeline?: (path: string) => void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Thumbnail card for images and video assets. Renders in a 2-col grid.
|
|
100
|
+
*
|
|
101
|
+
* Click behaviour (CapCut-style):
|
|
102
|
+
* - Already added → selects the clip on the timeline (setSelectedElementId).
|
|
103
|
+
* - Not yet added → opens the asset preview overlay over the canvas.
|
|
104
|
+
* Drag behaviour is preserved: a pointer movement exceeding DRAG_THRESHOLD_PX
|
|
105
|
+
* before pointerup is treated as drag-start, not a click.
|
|
106
|
+
*/
|
|
107
|
+
// fallow-ignore-next-line complexity
|
|
108
|
+
export function AssetCard({
|
|
109
|
+
projectId,
|
|
110
|
+
asset,
|
|
111
|
+
used,
|
|
112
|
+
duration,
|
|
113
|
+
onCopy,
|
|
114
|
+
isCopied,
|
|
115
|
+
onDelete,
|
|
116
|
+
onRename,
|
|
117
|
+
onAddAssetToTimeline,
|
|
118
|
+
}: AssetCardProps) {
|
|
119
|
+
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
120
|
+
const [hovered, setHovered] = useState(false);
|
|
121
|
+
const fullName = asset.split("/").pop() ?? asset;
|
|
122
|
+
const name = basename(asset);
|
|
123
|
+
const extension = ext(asset);
|
|
124
|
+
const serveUrl = resolveMediaPreviewUrl(asset, projectId);
|
|
125
|
+
const isVideo = VIDEO_EXT.test(asset);
|
|
126
|
+
const isImage = IMAGE_EXT.test(asset);
|
|
127
|
+
const probedDuration = useProbedDuration(serveUrl, !isVideo || duration != null);
|
|
128
|
+
const resolvedDuration = duration ?? probedDuration ?? undefined;
|
|
129
|
+
const durationLabel = formatDuration(resolvedDuration ?? 0);
|
|
130
|
+
|
|
131
|
+
// Drag-threshold click gate: track pointer-down position so we can ignore
|
|
132
|
+
// pointer-up events that followed a real drag gesture.
|
|
133
|
+
const pointerDownRef = useRef<{ x: number; y: number } | null>(null);
|
|
134
|
+
|
|
135
|
+
const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
|
|
136
|
+
const elements = usePlayerStore((s) => s.elements);
|
|
137
|
+
const setPreviewAsset = useAssetPreviewStore((s) => s.setPreviewAsset);
|
|
138
|
+
|
|
139
|
+
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
|
140
|
+
pointerDownRef.current = { x: e.clientX, y: e.clientY };
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
const handlePointerUp = useCallback(
|
|
144
|
+
(e: React.PointerEvent) => {
|
|
145
|
+
const origin = pointerDownRef.current;
|
|
146
|
+
pointerDownRef.current = null;
|
|
147
|
+
if (!origin) return;
|
|
148
|
+
if (!isPointerClick(e.clientX - origin.x, e.clientY - origin.y)) return;
|
|
149
|
+
// Treat as click
|
|
150
|
+
if (used) {
|
|
151
|
+
const clip = findClipForAsset(elements, asset);
|
|
152
|
+
if (clip) {
|
|
153
|
+
setSelectedElementId(clip.key ?? clip.id);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Not added (or no matching clip found) → preview overlay
|
|
158
|
+
setPreviewAsset(asset, projectId);
|
|
159
|
+
},
|
|
160
|
+
[used, elements, asset, projectId, setSelectedElementId, setPreviewAsset],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<>
|
|
165
|
+
<div
|
|
166
|
+
draggable
|
|
167
|
+
onPointerDown={handlePointerDown}
|
|
168
|
+
onPointerUp={handlePointerUp}
|
|
169
|
+
onDragStart={(e) => writeAssetDragData(e, asset)}
|
|
170
|
+
onContextMenu={(e) => openAssetContextMenu(e, setContextMenu)}
|
|
171
|
+
onPointerEnter={() => setHovered(true)}
|
|
172
|
+
onPointerLeave={() => setHovered(false)}
|
|
173
|
+
className={`flex flex-col gap-1 cursor-pointer rounded-md p-1 transition-colors ${
|
|
174
|
+
isCopied ? "bg-studio-accent/10" : "hover:bg-neutral-800/40"
|
|
175
|
+
}`}
|
|
176
|
+
>
|
|
177
|
+
{/* Thumbnail */}
|
|
178
|
+
<div className="w-full aspect-video rounded overflow-hidden bg-neutral-900 relative">
|
|
179
|
+
{isImage && (
|
|
180
|
+
<img
|
|
181
|
+
src={serveUrl}
|
|
182
|
+
alt={name}
|
|
183
|
+
loading="lazy"
|
|
184
|
+
className="w-full h-full object-cover"
|
|
185
|
+
onError={(e) => {
|
|
186
|
+
(e.target as HTMLImageElement).style.display = "none";
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
{isVideo && (
|
|
191
|
+
<>
|
|
192
|
+
<VideoFrameThumbnail src={serveUrl} />
|
|
193
|
+
{hovered && (
|
|
194
|
+
<video
|
|
195
|
+
src={serveUrl}
|
|
196
|
+
autoPlay
|
|
197
|
+
muted
|
|
198
|
+
loop
|
|
199
|
+
playsInline
|
|
200
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
</>
|
|
204
|
+
)}
|
|
205
|
+
{!isImage && !isVideo && (
|
|
206
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
207
|
+
<span className="text-[10px] font-medium text-neutral-600">{extension}</span>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
{/* "Added" badge — top-left */}
|
|
212
|
+
{used && (
|
|
213
|
+
<span className="absolute top-1 left-1 text-[9px] font-semibold leading-none px-1.5 py-[3px] rounded bg-neutral-950/80 text-panel-text-1">
|
|
214
|
+
Added
|
|
215
|
+
</span>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{/* Duration badge — top-right, media only */}
|
|
219
|
+
{durationLabel && (
|
|
220
|
+
<span className="absolute top-1 right-1 text-[9px] font-medium leading-none px-1.5 py-[3px] rounded bg-neutral-950/80 text-panel-text-2 tabular-nums">
|
|
221
|
+
{durationLabel}
|
|
222
|
+
</span>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* Filename caption */}
|
|
227
|
+
<span
|
|
228
|
+
className={`text-[10px] leading-tight text-center block w-full ${
|
|
229
|
+
used ? "text-panel-text-2" : "text-panel-text-4"
|
|
230
|
+
}`}
|
|
231
|
+
title={fullName}
|
|
232
|
+
>
|
|
233
|
+
{truncateMiddle(fullName, 22)}
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{contextMenu && (
|
|
238
|
+
<ContextMenu
|
|
239
|
+
x={contextMenu.x}
|
|
240
|
+
y={contextMenu.y}
|
|
241
|
+
asset={asset}
|
|
242
|
+
onClose={() => setContextMenu(null)}
|
|
243
|
+
onCopy={onCopy}
|
|
244
|
+
onDelete={onDelete}
|
|
245
|
+
onRename={onRename}
|
|
246
|
+
onAddAtPlayhead={onAddAssetToTimeline}
|
|
247
|
+
/>
|
|
248
|
+
)}
|
|
249
|
+
</>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface FontRowProps {
|
|
254
|
+
asset: string;
|
|
255
|
+
used: boolean;
|
|
256
|
+
onCopy: (path: string) => void;
|
|
257
|
+
isCopied: boolean;
|
|
258
|
+
onDelete?: (path: string) => void;
|
|
259
|
+
onRename?: (oldPath: string, newPath: string) => void;
|
|
260
|
+
onAddAssetToTimeline?: (path: string) => void;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Compact row for font assets (no meaningful thumbnail; show ext badge + name).
|
|
265
|
+
*/
|
|
266
|
+
export function FontRow({
|
|
267
|
+
asset,
|
|
268
|
+
used,
|
|
269
|
+
onCopy,
|
|
270
|
+
isCopied,
|
|
271
|
+
onDelete,
|
|
272
|
+
onRename,
|
|
273
|
+
onAddAssetToTimeline,
|
|
274
|
+
}: FontRowProps) {
|
|
275
|
+
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
276
|
+
const name = basename(asset);
|
|
277
|
+
const extension = ext(asset);
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<>
|
|
281
|
+
<div
|
|
282
|
+
draggable
|
|
283
|
+
onClick={() => onCopy(asset)}
|
|
284
|
+
onDragStart={(e) => writeAssetDragData(e, asset)}
|
|
285
|
+
onContextMenu={(e) => openAssetContextMenu(e, setContextMenu)}
|
|
286
|
+
className={`px-2.5 py-1.5 flex items-center gap-2.5 cursor-pointer transition-colors ${
|
|
287
|
+
isCopied
|
|
288
|
+
? "bg-studio-accent/10 border-l-2 border-studio-accent"
|
|
289
|
+
: "border-l-2 border-transparent hover:bg-neutral-800/50"
|
|
290
|
+
}`}
|
|
291
|
+
>
|
|
292
|
+
<div className="w-[50px] h-[32px] rounded overflow-hidden bg-neutral-900 flex-shrink-0 flex items-center justify-center">
|
|
293
|
+
<span className="text-[9px] font-medium text-neutral-700">{extension}</span>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="min-w-0 flex-1">
|
|
296
|
+
<span
|
|
297
|
+
className={`text-xs font-medium truncate block ${used ? "text-panel-text-1" : "text-panel-text-3"}`}
|
|
298
|
+
>
|
|
299
|
+
{name}
|
|
300
|
+
</span>
|
|
301
|
+
<div className="flex items-center gap-1.5">
|
|
302
|
+
<span className="text-[10px] text-neutral-600 truncate">{extension}</span>
|
|
303
|
+
{used && (
|
|
304
|
+
<span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded">
|
|
305
|
+
in use
|
|
306
|
+
</span>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
{contextMenu && (
|
|
313
|
+
<ContextMenu
|
|
314
|
+
x={contextMenu.x}
|
|
315
|
+
y={contextMenu.y}
|
|
316
|
+
asset={asset}
|
|
317
|
+
onClose={() => setContextMenu(null)}
|
|
318
|
+
onCopy={onCopy}
|
|
319
|
+
onDelete={onDelete}
|
|
320
|
+
onRename={onRename}
|
|
321
|
+
onAddAtPlayhead={onAddAssetToTimeline}
|
|
322
|
+
/>
|
|
323
|
+
)}
|
|
324
|
+
</>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
@@ -6,6 +6,7 @@ export function ContextMenu({
|
|
|
6
6
|
onCopy,
|
|
7
7
|
onDelete,
|
|
8
8
|
onRename,
|
|
9
|
+
onAddAtPlayhead,
|
|
9
10
|
}: {
|
|
10
11
|
x: number;
|
|
11
12
|
y: number;
|
|
@@ -14,6 +15,7 @@ export function ContextMenu({
|
|
|
14
15
|
onCopy: (path: string) => void;
|
|
15
16
|
onDelete?: (path: string) => void;
|
|
16
17
|
onRename?: (oldPath: string, newPath: string) => void;
|
|
18
|
+
onAddAtPlayhead?: (path: string) => void;
|
|
17
19
|
}) {
|
|
18
20
|
return (
|
|
19
21
|
<div
|
|
@@ -28,6 +30,19 @@ export function ContextMenu({
|
|
|
28
30
|
className="absolute bg-neutral-900 border border-neutral-700 rounded-lg shadow-xl py-1 min-w-[140px] text-xs"
|
|
29
31
|
style={{ left: x, top: y }}
|
|
30
32
|
>
|
|
33
|
+
{onAddAtPlayhead && (
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={(e) => {
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
onAddAtPlayhead(asset);
|
|
39
|
+
onClose();
|
|
40
|
+
}}
|
|
41
|
+
className="w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 transition-colors"
|
|
42
|
+
>
|
|
43
|
+
Add at playhead
|
|
44
|
+
</button>
|
|
45
|
+
)}
|
|
31
46
|
<button
|
|
32
47
|
onClick={(e) => {
|
|
33
48
|
e.stopPropagation();
|
|
@@ -65,33 +80,3 @@ export function ContextMenu({
|
|
|
65
80
|
</div>
|
|
66
81
|
);
|
|
67
82
|
}
|
|
68
|
-
|
|
69
|
-
export function DeleteConfirm({
|
|
70
|
-
name,
|
|
71
|
-
onConfirm,
|
|
72
|
-
onCancel,
|
|
73
|
-
}: {
|
|
74
|
-
name: string;
|
|
75
|
-
onConfirm: () => void;
|
|
76
|
-
onCancel: () => void;
|
|
77
|
-
}) {
|
|
78
|
-
return (
|
|
79
|
-
<div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex items-center justify-between gap-2">
|
|
80
|
-
<span className="text-[10px] text-red-400 truncate">Delete {name}?</span>
|
|
81
|
-
<div className="flex items-center gap-1 flex-shrink-0">
|
|
82
|
-
<button
|
|
83
|
-
onClick={onConfirm}
|
|
84
|
-
className="px-2 py-0.5 text-[10px] rounded bg-red-600 text-white hover:bg-red-500 transition-colors"
|
|
85
|
-
>
|
|
86
|
-
Delete
|
|
87
|
-
</button>
|
|
88
|
-
<button
|
|
89
|
-
onClick={onCancel}
|
|
90
|
-
className="px-2 py-0.5 text-[10px] rounded text-neutral-400 hover:text-neutral-200 transition-colors"
|
|
91
|
-
>
|
|
92
|
-
Cancel
|
|
93
|
-
</button>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
}
|