@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { usePlayerStore, type TimelineElement } from "../player";
|
|
3
3
|
import { reseekPreviewAtTime } from "../player/hooks/timelineSyncHydration";
|
|
4
|
-
import {
|
|
4
|
+
import { useTimelineRowElements } from "../player/hooks/useTimelineRowElements";
|
|
5
5
|
import { applySoftReloadFinalization } from "../utils/gsapSoftReload";
|
|
6
6
|
import {
|
|
7
7
|
timelineTrackOrder,
|
|
@@ -295,7 +295,7 @@ export function useTimelineTrackVisibilityEditing({
|
|
|
295
295
|
// virtual sub-comp children carry their own (display.track + idx) track numbers,
|
|
296
296
|
// so filtering the raw store list by a virtual track number would hide the wrong
|
|
297
297
|
// outer-scene sibling sharing that index.
|
|
298
|
-
const expandedElements =
|
|
298
|
+
const expandedElements = useTimelineRowElements();
|
|
299
299
|
return useCallback(
|
|
300
300
|
async (track: number, hidden: boolean, displayNumber?: number | null) => {
|
|
301
301
|
if (isRecordingRef?.current) {
|
|
@@ -354,15 +354,7 @@ export function useTimelineElementVisibilityEditing({
|
|
|
354
354
|
elementKey: string | readonly string[],
|
|
355
355
|
hidden: boolean,
|
|
356
356
|
) => Promise<void> {
|
|
357
|
-
|
|
358
|
-
// sub-composition child has no entry of its own in the raw list (only its
|
|
359
|
-
// host does), so an elementKey for such a child (the
|
|
360
|
-
// `sourceFile#domId`-shaped virtual key `resolveTimelineIdForSelection`
|
|
361
|
-
// falls back to) would never match anything there and Hide All would
|
|
362
|
-
// silently no-op for it. The expanded list synthesizes a real, patchable
|
|
363
|
-
// TimelineElement (with matching key/domId/sourceFile) for each visible
|
|
364
|
-
// child whenever its host is currently expanded.
|
|
365
|
-
const expandedElements = useExpandedTimelineElements();
|
|
357
|
+
const expandedElements = useTimelineRowElements();
|
|
366
358
|
return useCallback(
|
|
367
359
|
async (elementKey: string | readonly string[], hidden: boolean) => {
|
|
368
360
|
if (isRecordingRef?.current) {
|
|
@@ -371,6 +363,11 @@ export function useTimelineElementVisibilityEditing({
|
|
|
371
363
|
}
|
|
372
364
|
const pid = projectIdRef.current;
|
|
373
365
|
if (!pid) return;
|
|
366
|
+
const keys = typeof elementKey === "string" ? [elementKey] : elementKey;
|
|
367
|
+
if (!expandedElements.some((item) => keys.includes(item.key ?? item.id))) {
|
|
368
|
+
showToast("This element is inside a sub-composition and has no timeline row to hide.");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
374
371
|
try {
|
|
375
372
|
await toggleTimelineElementHidden({
|
|
376
373
|
projectId: pid,
|
|
@@ -93,6 +93,7 @@ function Harness() {
|
|
|
93
93
|
handleCopy: vi.fn(() => false),
|
|
94
94
|
handlePaste: vi.fn(async () => undefined),
|
|
95
95
|
handleCut: vi.fn(async () => false),
|
|
96
|
+
handleDuplicate: vi.fn(async () => false),
|
|
96
97
|
onResetKeyframes: vi.fn(() => false),
|
|
97
98
|
onDeleteSelectedKeyframes: keyframeDelete,
|
|
98
99
|
});
|
|
@@ -114,6 +114,7 @@ interface UseAppHotkeysParams {
|
|
|
114
114
|
handleCopy: () => boolean;
|
|
115
115
|
handlePaste: () => Promise<void>;
|
|
116
116
|
handleCut: () => Promise<boolean>;
|
|
117
|
+
handleDuplicate: () => Promise<boolean>;
|
|
117
118
|
onResetKeyframes: () => boolean;
|
|
118
119
|
onDeleteSelectedKeyframes: () => void;
|
|
119
120
|
onAfterUndoRedo?: () => void;
|
|
@@ -150,6 +151,7 @@ export function useAppHotkeys({
|
|
|
150
151
|
handleCopy,
|
|
151
152
|
handlePaste,
|
|
152
153
|
handleCut,
|
|
154
|
+
handleDuplicate,
|
|
153
155
|
onResetKeyframes,
|
|
154
156
|
onDeleteSelectedKeyframes,
|
|
155
157
|
onAfterUndoRedo,
|
|
@@ -187,7 +189,7 @@ export function useAppHotkeys({
|
|
|
187
189
|
// earlier file edit (which would ALSO leave the caption change intact).
|
|
188
190
|
const captionState = useCaptionStore.getState();
|
|
189
191
|
// Only when the caption preview is actually visible: isEditMode stays
|
|
190
|
-
// true while the preview is hidden
|
|
192
|
+
// true while the preview is hidden, and eating ⌘Z
|
|
191
193
|
// there would pop invisible caption edits instead of file history.
|
|
192
194
|
if (captionState.isEditMode && isCaptionPreviewVisible()) {
|
|
193
195
|
const restored = direction === "undo" ? captionState.undo() : captionState.redo();
|
|
@@ -255,6 +257,7 @@ export function useAppHotkeys({
|
|
|
255
257
|
handleCopy,
|
|
256
258
|
handlePaste,
|
|
257
259
|
handleCut,
|
|
260
|
+
handleDuplicate,
|
|
258
261
|
onResetKeyframes,
|
|
259
262
|
onDeleteSelectedKeyframes,
|
|
260
263
|
onToggleRecording,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatTimelineBlock } from "../player/lib/describeClips";
|
|
1
2
|
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
2
3
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
3
4
|
import { copyTextToClipboard } from "../utils/clipboard";
|
|
@@ -92,6 +93,7 @@ export function useAskAgentModal({
|
|
|
92
93
|
const prompt = buildElementAgentPrompt({
|
|
93
94
|
selection: domEditSelection,
|
|
94
95
|
currentTime: usePlayerStore.getState().currentTime,
|
|
96
|
+
timeline: formatTimelineBlock(usePlayerStore.getState().elements),
|
|
95
97
|
tagSnippet,
|
|
96
98
|
selectionContext: agentPromptSelectionContext,
|
|
97
99
|
userInstruction,
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { pasteTimelineClips, resolveFreeTrack, type PlacedClip } from "./useClipboard";
|
|
4
|
+
import { extendRootDurationInSource } from "../utils/rootDuration";
|
|
5
|
+
import type { TimelineClipboardClip } from "../utils/clipboardPayload";
|
|
6
|
+
import type { TimelineElement } from "../player";
|
|
7
|
+
|
|
8
|
+
const ROOT =
|
|
9
|
+
'<div id="claude-paper" data-composition-id="claude-paper" data-start="0" data-duration="10"></div>';
|
|
10
|
+
|
|
11
|
+
function clip(id: string, start: number, duration: number, track: number): TimelineClipboardClip {
|
|
12
|
+
return {
|
|
13
|
+
html: `<audio id="${id}" src="typenew.mp3" data-start="${start}" data-duration="${duration}" data-track-index="${track}"></audio>`,
|
|
14
|
+
start,
|
|
15
|
+
duration,
|
|
16
|
+
track,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function liveElement(track: number, start: number, duration: number): TimelineElement {
|
|
21
|
+
return { id: `x-${track}`, tag: "audio", start, duration, track, authoredTrack: track };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("resolveFreeTrack", () => {
|
|
25
|
+
it("keeps the preferred track when nothing occupies it", () => {
|
|
26
|
+
const track = resolveFreeTrack({ track: 5, start: 10, duration: 1 }, []);
|
|
27
|
+
expect(track).toBe(5);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("bumps to a new track when the preferred one overlaps in time", () => {
|
|
31
|
+
const taken: PlacedClip[] = [{ track: 5, start: 10.2, duration: 0.5 }];
|
|
32
|
+
const track = resolveFreeTrack({ track: 5, start: 10, duration: 1 }, taken);
|
|
33
|
+
expect(track).toBe(6);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("keeps the preferred track when an occupant on it does not overlap in time", () => {
|
|
37
|
+
const taken: PlacedClip[] = [{ track: 5, start: 50, duration: 1 }];
|
|
38
|
+
const track = resolveFreeTrack({ track: 5, start: 10, duration: 1 }, taken);
|
|
39
|
+
expect(track).toBe(5);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("picks the next track above the highest taken one, not just +1 from preferred", () => {
|
|
43
|
+
const taken: PlacedClip[] = [
|
|
44
|
+
{ track: 5, start: 10, duration: 1 },
|
|
45
|
+
{ track: 9, start: 30, duration: 1 },
|
|
46
|
+
];
|
|
47
|
+
const track = resolveFreeTrack({ track: 5, start: 10.1, duration: 1 }, taken);
|
|
48
|
+
expect(track).toBe(10);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("pasteTimelineClips", () => {
|
|
53
|
+
it("anchors a single clip's data-start at the anchor time and keeps its track", () => {
|
|
54
|
+
const { content, ids } = pasteTimelineClips(ROOT, [clip("sfx-23", 21.61, 0.57, 132)], 40, []);
|
|
55
|
+
expect(content).toContain('data-start="40"');
|
|
56
|
+
expect(content).toContain('data-track-index="132"');
|
|
57
|
+
expect(ids).toEqual(["sfx-23"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("preserves each clip's relative offset from the group's earliest clip", () => {
|
|
61
|
+
const clips = [
|
|
62
|
+
clip("sfx-23", 21.61, 0.57, 132),
|
|
63
|
+
clip("sfx-24", 21.73, 0.57, 133),
|
|
64
|
+
clip("sfx-25", 21.84, 0.57, 134),
|
|
65
|
+
];
|
|
66
|
+
const { content } = pasteTimelineClips(ROOT, clips, 40, []);
|
|
67
|
+
// Offsets from the earliest clip (21.61): 0, 0.12, 0.23 -> 40, 40.12, 40.23
|
|
68
|
+
expect(content).toContain('id="sfx-23" src="typenew.mp3" data-start="40"');
|
|
69
|
+
expect(content).toContain('id="sfx-24" src="typenew.mp3" data-start="40.12"');
|
|
70
|
+
expect(content).toContain('id="sfx-25" src="typenew.mp3" data-start="40.23"');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("moves a clip to a new track when its original track is occupied at the target time", () => {
|
|
74
|
+
const live = [liveElement(132, 40, 0.57)];
|
|
75
|
+
const { content } = pasteTimelineClips(ROOT, [clip("sfx-23", 21.61, 0.57, 132)], 40, live);
|
|
76
|
+
expect(content).not.toContain('data-track-index="132"');
|
|
77
|
+
expect(content).toMatch(/data-track-index="133"/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("strips the copied data-hf-id so a fresh one mints on next save", () => {
|
|
81
|
+
const clipWithHfId: TimelineClipboardClip = {
|
|
82
|
+
html: '<audio data-hf-id="hf-75o7" id="sfx-type-0" src="typenew.mp3" data-start="7.8" data-duration="0.57" data-track-index="109"></audio>',
|
|
83
|
+
start: 7.8,
|
|
84
|
+
duration: 0.57,
|
|
85
|
+
track: 109,
|
|
86
|
+
};
|
|
87
|
+
const { content } = pasteTimelineClips(ROOT, [clipWithHfId], 40, []);
|
|
88
|
+
expect(content).not.toContain("data-hf-id");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("strips a nested descendant's data-hf-id too, not just the root's", () => {
|
|
92
|
+
// A composition-instance clip's descendants carry their own hf-ids; a
|
|
93
|
+
// clone must remint all of them, not just the root, or the descendants
|
|
94
|
+
// collide with their originals the same way the root would.
|
|
95
|
+
const nested: TimelineClipboardClip = {
|
|
96
|
+
html: '<div data-hf-id="hf-root" id="comp-1" data-start="7.8" data-duration="0.57" data-track-index="109"><span data-hf-id="hf-child"></span></div>',
|
|
97
|
+
start: 7.8,
|
|
98
|
+
duration: 0.57,
|
|
99
|
+
track: 109,
|
|
100
|
+
};
|
|
101
|
+
const { content } = pasteTimelineClips(ROOT, [nested], 40, []);
|
|
102
|
+
expect(content).not.toContain("hf-root");
|
|
103
|
+
expect(content).not.toContain("hf-child");
|
|
104
|
+
expect(content).not.toContain("data-hf-id");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("does not strip text content that happens to contain the data-hf-id string", () => {
|
|
108
|
+
const withText: TimelineClipboardClip = {
|
|
109
|
+
html: '<div id="cap-1" data-start="7.8" data-duration="0.57" data-track-index="109"><span>talking about data-hf-id="hf-xyz" in my video</span></div>',
|
|
110
|
+
start: 7.8,
|
|
111
|
+
duration: 0.57,
|
|
112
|
+
track: 109,
|
|
113
|
+
};
|
|
114
|
+
const { content } = pasteTimelineClips(ROOT, [withText], 40, []);
|
|
115
|
+
expect(content).toContain('talking about data-hf-id="hf-xyz" in my video');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("bumps the second clip in a batch off the first clip's own new track when their offsets collide", () => {
|
|
119
|
+
// Both clips start life a track apart but paste to overlapping times on
|
|
120
|
+
// the SAME track, so the second must dodge the first's placement, not
|
|
121
|
+
// just the live elements it was checked against.
|
|
122
|
+
const clips = [clip("sfx-23", 21.61, 0.57, 132), clip("sfx-24", 21.7, 0.57, 132)];
|
|
123
|
+
const { content } = pasteTimelineClips(ROOT, clips, 40, []);
|
|
124
|
+
expect(content).toContain(
|
|
125
|
+
'id="sfx-23" src="typenew.mp3" data-start="40" data-duration="0.57" data-track-index="132"',
|
|
126
|
+
);
|
|
127
|
+
expect(content).toContain(
|
|
128
|
+
'id="sfx-24" src="typenew.mp3" data-start="40.09" data-duration="0.57" data-track-index="133"',
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("dedupes an id that already exists in the target file", () => {
|
|
133
|
+
const withExisting = ROOT.replace(
|
|
134
|
+
"</div>",
|
|
135
|
+
'<audio id="sfx-23" data-start="1" data-duration="1" data-track-index="0"></audio></div>',
|
|
136
|
+
);
|
|
137
|
+
const { content, ids } = pasteTimelineClips(
|
|
138
|
+
withExisting,
|
|
139
|
+
[clip("sfx-23", 21.61, 0.57, 132)],
|
|
140
|
+
40,
|
|
141
|
+
[],
|
|
142
|
+
);
|
|
143
|
+
expect(ids[0]).not.toBe("sfx-23");
|
|
144
|
+
expect(ids[0]).toMatch(/^sfx-23-\d+$/);
|
|
145
|
+
expect(content).toContain(`id="${ids[0]}"`);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("strips a single-quoted data-hf-id, not just a double-quoted one", () => {
|
|
149
|
+
const singleQuoted: TimelineClipboardClip = {
|
|
150
|
+
html: '<audio data-hf-id=\'hf-75o7\' id="sfx-type-0" src="typenew.mp3" data-start="7.8" data-duration="0.57" data-track-index="109"></audio>',
|
|
151
|
+
start: 7.8,
|
|
152
|
+
duration: 0.57,
|
|
153
|
+
track: 109,
|
|
154
|
+
};
|
|
155
|
+
const { content } = pasteTimelineClips(ROOT, [singleQuoted], 40, []);
|
|
156
|
+
expect(content).not.toContain("data-hf-id");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("strips data-hf-id even when an earlier attribute value contains a literal >", () => {
|
|
160
|
+
// A `>` inside title's value would close a bracket-scoped tag regex
|
|
161
|
+
// early, leaving data-hf-id (which comes after it) outside the matched
|
|
162
|
+
// span and unstripped. A real parser doesn't have this failure mode.
|
|
163
|
+
const gtInAttr: TimelineClipboardClip = {
|
|
164
|
+
html: '<div title="a > b" id="cap-1" data-hf-id="hf-xyz" data-start="7.8" data-duration="0.57" data-track-index="109"></div>',
|
|
165
|
+
start: 7.8,
|
|
166
|
+
duration: 0.57,
|
|
167
|
+
track: 109,
|
|
168
|
+
};
|
|
169
|
+
const { content } = pasteTimelineClips(ROOT, [gtInAttr], 40, []);
|
|
170
|
+
expect(content).not.toContain("data-hf-id");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("still strips data-hf-id from bare text with no element root", () => {
|
|
174
|
+
const noRoot: TimelineClipboardClip = {
|
|
175
|
+
html: 'text mentioning data-hf-id="hf-leak" with no tag at all',
|
|
176
|
+
start: 0,
|
|
177
|
+
duration: 1,
|
|
178
|
+
track: 0,
|
|
179
|
+
};
|
|
180
|
+
const { content } = pasteTimelineClips(ROOT, [noRoot], 0, []);
|
|
181
|
+
expect(content).not.toContain("data-hf-id");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("still strips data-hf-id from a tag the HTML parser hoists out of body (e.g. title)", () => {
|
|
185
|
+
// A <title> never reaches DOMParser's body, so stripHfIds's DOM walk sees
|
|
186
|
+
// no root element here -- this exercises the regex fallback, not the walk.
|
|
187
|
+
const hoisted: TimelineClipboardClip = {
|
|
188
|
+
html: '<title data-hf-id="hf-leak">x</title>',
|
|
189
|
+
start: 0,
|
|
190
|
+
duration: 1,
|
|
191
|
+
track: 0,
|
|
192
|
+
};
|
|
193
|
+
const { content } = pasteTimelineClips(ROOT, [hoisted], 0, []);
|
|
194
|
+
expect(content).not.toContain("data-hf-id");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("captures the root's own id, not a data-id that happens to precede it", () => {
|
|
198
|
+
const withDataId: TimelineClipboardClip = {
|
|
199
|
+
html: '<audio data-id="not-the-id" id="sfx-23" src="typenew.mp3" data-start="21.61" data-duration="0.57" data-track-index="132"></audio>',
|
|
200
|
+
start: 21.61,
|
|
201
|
+
duration: 0.57,
|
|
202
|
+
track: 132,
|
|
203
|
+
};
|
|
204
|
+
const { ids } = pasteTimelineClips(ROOT, [withDataId], 40, []);
|
|
205
|
+
expect(ids).toEqual(["sfx-23"]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("reports the furthest clip end across the whole batch as requiredEnd, not just the last clip's", () => {
|
|
209
|
+
// The batch is placed in order sfx-23, sfx-24, sfx-25, but sfx-24 ends up
|
|
210
|
+
// the furthest forward once offsets are applied; requiredEnd must track
|
|
211
|
+
// the max across all placed clips, not whichever one the loop saw last.
|
|
212
|
+
const clips = [
|
|
213
|
+
clip("sfx-23", 21.61, 0.57, 132),
|
|
214
|
+
clip("sfx-24", 21.9, 5, 133),
|
|
215
|
+
clip("sfx-25", 21.73, 0.1, 134),
|
|
216
|
+
];
|
|
217
|
+
const { requiredEnd } = pasteTimelineClips(ROOT, clips, 40, []);
|
|
218
|
+
expect(requiredEnd).toBeCloseTo(40.29 + 5, 2);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("pasteTimelineClips + extendRootDurationInSource", () => {
|
|
223
|
+
it("grows the root composition's duration when a paste lands past its current end", () => {
|
|
224
|
+
const pasted = pasteTimelineClips(ROOT, [clip("sfx-23", 21.61, 0.57, 132)], 40, []);
|
|
225
|
+
const extended = extendRootDurationInSource(pasted.content, pasted.requiredEnd);
|
|
226
|
+
expect(extended).toContain('data-duration="40.57"');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("leaves the root composition's duration unchanged when the paste fits inside it", () => {
|
|
230
|
+
const pasted = pasteTimelineClips(ROOT, [clip("sfx-23", 1, 0.5, 132)], 2, []);
|
|
231
|
+
const extended = extendRootDurationInSource(pasted.content, pasted.requiredEnd);
|
|
232
|
+
expect(extended).toContain('data-duration="10"');
|
|
233
|
+
});
|
|
234
|
+
});
|