@hyperframes/studio 0.8.50 → 0.8.53-desktop.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-PTEnAHhg.js} +3 -3
- package/dist/assets/{index-DUAIWxkI.js → index--myXp8ui.js} +1 -1
- package/dist/assets/{index-Dt3qBI-9.js → index-BjslOS9g.js} +1 -1
- package/dist/assets/index-BnXZuIJD.css +1 -0
- package/dist/assets/index-CNyviK_l.js +390 -0
- package/dist/index.css +29 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +825 -24
- package/dist/index.html +2 -2
- package/dist/index.js +8911 -9853
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +13 -12
- package/src/App.tsx +145 -163
- package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
- package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
- package/src/captions/components/CaptionOverlay.tsx +8 -4
- package/src/captions/components/CaptionOverlayUtils.ts +2 -3
- package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
- package/src/captions/components/CaptionTimeline.tsx +1 -1
- package/src/captions/components/shared.tsx +2 -2
- package/src/components/AskAgentModal.tsx +5 -5
- package/src/components/EditorShell.selectionSync.test.tsx +9 -4
- package/src/components/EditorShell.tsx +59 -62
- package/src/components/ExternalFileConflictBanner.tsx +16 -16
- package/src/components/LintModal.tsx +6 -16
- package/src/components/MediaPreview.tsx +2 -2
- package/src/components/RightPanelTabs.test.tsx +151 -0
- package/src/components/RightPanelTabs.tsx +59 -0
- package/src/components/SaveQueuePausedBanner.tsx +2 -2
- package/src/components/StudioHeader.dom.test.tsx +181 -0
- package/src/components/StudioHeader.test.ts +0 -12
- package/src/components/StudioHeader.tsx +118 -221
- package/src/components/StudioLeftPanels.tsx +202 -0
- package/src/components/StudioOverlays.tsx +1 -1
- package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -194
- package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
- package/src/components/StudioToast.tsx +2 -2
- package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
- package/src/components/TimelineHistoryButtons.tsx +54 -0
- package/src/components/TimelineToolbar.test.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +8 -12
- package/src/components/dock/Dock.test.tsx +274 -0
- package/src/components/dock/Dock.tsx +291 -0
- package/src/components/dock/dock.css +28 -0
- package/src/components/dock/dockAccessibility.test.ts +166 -0
- package/src/components/dock/dockAccessibility.ts +98 -0
- package/src/components/dock/dockLayout.test.ts +134 -0
- package/src/components/dock/dockLayout.ts +105 -0
- package/src/components/dock/dockLayoutSchema.ts +66 -0
- package/src/components/dock/dockLayoutStore.test.ts +28 -0
- package/src/components/dock/dockLayoutStore.ts +108 -0
- package/src/components/dock/dockTestHarness.ts +11 -0
- package/src/components/dock/panelRegistry.ts +67 -0
- package/src/components/editor/AnimationCard.tsx +4 -4
- package/src/components/editor/AnimationCardParts.tsx +6 -6
- package/src/components/editor/BlockParamsPanel.tsx +4 -4
- package/src/components/editor/BorderRadiusEditor.tsx +1 -1
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +1 -1
- package/src/components/editor/EaseParamFields.tsx +5 -5
- package/src/components/editor/FileTree.tsx +4 -4
- package/src/components/editor/FileTreeIcons.tsx +1 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -13
- package/src/components/editor/InlineTextToolbar.tsx +1 -1
- package/src/components/editor/InspectorHeaderActions.tsx +3 -3
- package/src/components/editor/KeyframeDiamond.tsx +1 -1
- package/src/components/editor/KeyframeEaseList.tsx +1 -1
- package/src/components/editor/LayersPanel.test.ts +97 -1
- package/src/components/editor/LayersPanel.tsx +7 -5
- package/src/components/editor/MarqueeOverlay.tsx +1 -1
- package/src/components/editor/OffCanvasIndicators.tsx +1 -1
- package/src/components/editor/PreviewGuides.tsx +1 -1
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.test.tsx +0 -3
- package/src/components/editor/SnapToolbar.tsx +2 -2
- package/src/components/editor/TimelineFxPopover.tsx +1 -1
- package/src/components/editor/TopologyLens.test.tsx +28 -0
- package/src/components/editor/TopologyLens.tsx +7 -5
- package/src/components/editor/Transform3DCube.tsx +2 -2
- package/src/components/editor/audioFxRevealTarget.ts +1 -0
- package/src/components/editor/domEditing.test.ts +1 -1
- package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
- package/src/components/editor/domEditingAgentPrompt.ts +4 -3
- package/src/components/editor/propertyPanelColor.tsx +11 -11
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
- package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
- package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +1 -1
- package/src/components/editor/propertyPanelFill.tsx +4 -4
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
- package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
- package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +5 -5
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +6 -6
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
- package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
- package/src/components/editor/propertyPanelSections.tsx +8 -8
- package/src/components/editor/useColorGradingController.test.ts +38 -0
- package/src/components/editor/useColorGradingController.ts +9 -1
- package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
- package/src/components/editor/useColorGradingPreviews.ts +11 -1
- package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
- package/src/components/editor/usePreviewCompositionRect.ts +21 -0
- package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
- package/src/components/feedback/feedbackTrigger.ts +0 -1
- package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
- package/src/components/nle/NLEContext.tsx +0 -43
- package/src/components/nle/NLEPreview.tsx +3 -3
- package/src/components/nle/PreviewOverlays.tsx +6 -6
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/TimelinePane.tsx +53 -67
- package/src/components/panels/SlideshowPanel.tsx +3 -3
- 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 +4 -4
- package/src/components/panels/VariablesValueControls.tsx +2 -2
- package/src/components/pointerTargetSize.test.tsx +8 -0
- package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
- package/src/components/renders/RenderQueue.test.tsx +7 -0
- package/src/components/renders/RenderQueue.tsx +7 -7
- package/src/components/renders/RenderQueueItem.tsx +6 -6
- package/src/components/renders/RenderQueuePanel.tsx +1 -1
- 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/{LeftSidebar.masterComposition.test.tsx → CompositionsPanel.masterComposition.test.tsx} +6 -9
- package/src/components/sidebar/CompositionsPanel.tsx +15 -0
- package/src/components/sidebar/CompositionsTab.tsx +5 -5
- package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.test.tsx +53 -0
- package/src/components/sidebar/SidebarLintButton.tsx +50 -0
- package/src/components/timelineToolbarStyles.ts +5 -0
- package/src/components/ui/Button.test.tsx +252 -0
- package/src/components/ui/Button.tsx +89 -126
- package/src/components/ui/IconButton.tsx +38 -0
- package/src/components/ui/Input.tsx +115 -0
- package/src/components/ui/Menu.test.tsx +374 -0
- package/src/components/ui/Menu.tsx +213 -0
- package/src/components/ui/NumberField.tsx +122 -0
- package/src/components/ui/Popover.tsx +74 -0
- package/src/components/ui/SearchInput.tsx +2 -2
- package/src/components/ui/Select.tsx +100 -0
- package/src/components/ui/Slider.tsx +153 -0
- package/src/components/ui/Tabs.test.tsx +98 -0
- package/src/components/ui/Tabs.tsx +71 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/Tooltip.test.tsx +110 -0
- package/src/components/ui/Tooltip.tsx +38 -112
- package/src/components/ui/cn.test.ts +30 -0
- package/src/components/ui/cn.ts +25 -0
- package/src/components/ui/index.ts +29 -2
- package/src/components/ui/mountHost.testHelpers.ts +24 -0
- package/src/components/ui/valueControls.test.tsx +524 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -57
- package/src/hooks/appHotkeysDispatch.ts +3 -4
- package/src/hooks/timelineEditPermission.ts +31 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -2
- package/src/hooks/useAppHotkeys.test.ts +0 -1
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -3
- package/src/hooks/useAppHotkeys.ts +21 -84
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useDomEditPreviewSync.ts +4 -6
- package/src/hooks/useDomEditSession.ts +0 -7
- package/src/hooks/useDomEditWiring.ts +4 -9
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- package/src/hooks/useFileManager.ts +21 -123
- package/src/hooks/useLintModal.test.tsx +65 -0
- package/src/hooks/useLintModal.ts +3 -0
- package/src/hooks/usePanelLayout.test.ts +108 -296
- package/src/hooks/usePanelLayout.ts +50 -255
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/useProjectFileWriter.ts +150 -0
- package/src/hooks/useRightPanelIntents.test.ts +181 -0
- package/src/hooks/useRightPanelIntents.ts +63 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
- package/src/hooks/useSdkSession.ts +43 -10
- package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
- package/src/hooks/useServerConnection.ts +43 -10
- package/src/hooks/useSlideshowTabState.test.ts +9 -31
- package/src/hooks/useSlideshowTabState.ts +6 -19
- package/src/hooks/useStudioContextValue.test.ts +6 -18
- package/src/hooks/useStudioContextValue.ts +5 -26
- package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
- package/src/hooks/useTimelineAssetDropOps.ts +52 -11
- package/src/hooks/useTimelineDeleteOps.test.tsx +111 -0
- package/src/hooks/useTimelineEditing.test.tsx +322 -3
- package/src/hooks/useTimelineEditing.ts +103 -16
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTrackPendingTimelineEdit.test.tsx +62 -0
- package/src/hooks/useTrackPendingTimelineEdit.ts +22 -0
- package/src/hostMount.test.tsx +222 -0
- package/src/index.ts +87 -0
- package/src/main.tsx +9 -3
- package/src/player/components/AudioWaveform.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +3 -3
- package/src/player/components/ClipContextMenu.tsx +3 -3
- package/src/player/components/CompositionThumbnail.test.ts +0 -3
- package/src/player/components/CompositionThumbnail.tsx +3 -4
- package/src/player/components/EditModal.tsx +1 -1
- package/src/player/components/ImageThumbnail.tsx +1 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
- package/src/player/components/LayerDisclosureRow.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +1 -1
- package/src/player/components/ShortcutsPanel.tsx +9 -9
- package/src/player/components/SpeedMenu.tsx +3 -5
- package/src/player/components/Timeline.tsx +3 -2
- package/src/player/components/TimelineCanvas.tsx +21 -14
- package/src/player/components/TimelineClip.tsx +0 -1
- package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
- package/src/player/components/TimelineEmptyState.tsx +5 -5
- package/src/player/components/TimelineFxButton.tsx +6 -6
- package/src/player/components/TimelineGroupHeader.tsx +5 -5
- package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
- package/src/player/components/TimelineGroupRow.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +32 -0
- package/src/player/components/TimelineLanes.tsx +2 -5
- package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
- package/src/player/components/TimelineRuler.tsx +1 -1
- package/src/player/components/TimelineShortcutHint.tsx +1 -1
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +2 -2
- package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
- package/src/player/components/TimelineTypes.ts +2 -1
- package/src/player/components/TrackGapContextMenu.tsx +1 -1
- package/src/player/components/VideoThumbnail.tsx +4 -2
- package/src/player/components/VolumeControl.tsx +2 -2
- package/src/player/components/timelineCallbacks.ts +8 -8
- package/src/player/components/timelineClipDragPreview.ts +51 -27
- package/src/player/components/timelineClipDragTypes.ts +3 -0
- package/src/player/components/timelineDragDrop.test.tsx +61 -1
- package/src/player/components/timelineDragDrop.ts +57 -15
- package/src/player/components/timelineLaneProps.ts +3 -0
- package/src/player/components/timelineSnapping.test.ts +27 -0
- package/src/player/components/timelineSnapping.ts +14 -1
- package/src/player/components/timelineTheme.test.ts +4 -4
- package/src/player/components/timelineTheme.ts +30 -30
- package/src/player/components/trackHeaderLabelRows.tsx +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
- package/src/player/components/useTimelineClipDrag.ts +30 -3
- package/src/player/lib/describeClips.ts +3 -54
- package/src/player/lib/playbackShortcuts.ts +2 -0
- package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
- package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
- package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
- package/src/player/store/previewIframeStore.test.tsx +29 -0
- package/src/player/store/previewIframeStore.ts +25 -0
- package/src/renderQueueExports.test.tsx +58 -0
- package/src/styles/classCandidates.test.ts +105 -0
- package/src/styles/classCandidates.ts +281 -0
- package/src/styles/hex-baseline.json +84 -0
- package/src/styles/hexRatchet.test.ts +201 -0
- package/src/styles/iconTokens.ts +20 -0
- package/src/styles/palette-reset.css +6 -0
- package/src/styles/studio.css +24 -21
- package/src/styles/styleSources.test.ts +20 -0
- package/src/styles/styleSources.ts +47 -0
- package/src/styles/tailwind-preset.shared.js +3 -0
- package/src/styles/theme.css +270 -0
- package/src/styles/theme.test.ts +183 -0
- package/src/styles/tokenGate.test.ts +109 -0
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioHelpers.test.ts +18 -0
- package/src/utils/studioHelpers.ts +11 -8
- package/src/utils/studioUiPreferences.test.ts +10 -16
- package/src/utils/studioUiPreferences.ts +7 -16
- package/src/utils/timelineDropTrackInsert.test.ts +134 -0
- package/src/utils/timelineDropTrackInsert.ts +83 -0
- package/src/utils/typingTarget.ts +4 -0
- package/dist/assets/index-n2ml4tUn.js +0 -442
- package/dist/assets/index-pO6p0H27.css +0 -1
- package/src/components/PanelTabButton.tsx +0 -31
- package/src/components/StudioLeftSidebar.tsx +0 -204
- package/src/components/nle/TimelineResizeDivider.tsx +0 -101
- package/src/components/sidebar/LeftSidebar.storage.test.ts +0 -28
- package/src/components/sidebar/LeftSidebar.tsx +0 -334
- 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/useInspectorSplitResize.ts +0 -51
- package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
- package/src/hooks/useProjectSignaturePoll.ts +0 -68
- package/src/hooks/useStoryboard.ts +0 -99
- package/src/utils/fitPanels.test.ts +0 -164
- package/src/utils/fitPanels.ts +0 -151
|
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
5
5
|
import { useTimelineAssetDropOps } from "./useTimelineAssetDropOps";
|
|
6
6
|
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
7
7
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
8
|
+
import type { TimelineElement } from "../player";
|
|
8
9
|
|
|
9
10
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
11
|
|
|
@@ -19,6 +20,7 @@ type DropFn = ReturnType<typeof useTimelineAssetDropOps>["handleTimelineAssetDro
|
|
|
19
20
|
function renderDropHook(
|
|
20
21
|
sourceContent: string,
|
|
21
22
|
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>,
|
|
23
|
+
timelineElements: TimelineElement[] = [],
|
|
22
24
|
) {
|
|
23
25
|
vi.stubGlobal(
|
|
24
26
|
"fetch",
|
|
@@ -29,7 +31,7 @@ function renderDropHook(
|
|
|
29
31
|
const { handleTimelineAssetDrop } = useTimelineAssetDropOps({
|
|
30
32
|
projectIdRef: { current: "project" },
|
|
31
33
|
activeCompPath: "index.html",
|
|
32
|
-
timelineElements
|
|
34
|
+
timelineElements,
|
|
33
35
|
showToast: vi.fn(),
|
|
34
36
|
writeProjectFile,
|
|
35
37
|
recordEdit: vi.fn().mockResolvedValue(undefined),
|
|
@@ -84,4 +86,42 @@ describe("useTimelineAssetDropOps handleTimelineAssetDrop", () => {
|
|
|
84
86
|
|
|
85
87
|
expect(usePlayerStore.getState().selectedElementId).toBe("index.html#clip");
|
|
86
88
|
});
|
|
89
|
+
|
|
90
|
+
it("measures the insert row against the visible rows, not clips the timeline hides", async () => {
|
|
91
|
+
const clip = (id: string, track: number): TimelineElement => ({
|
|
92
|
+
id,
|
|
93
|
+
key: id,
|
|
94
|
+
tag: "div",
|
|
95
|
+
start: 0,
|
|
96
|
+
duration: 2,
|
|
97
|
+
track,
|
|
98
|
+
authoredTrack: track,
|
|
99
|
+
hfId: `hf-${id}`,
|
|
100
|
+
domId: id,
|
|
101
|
+
});
|
|
102
|
+
const source = [
|
|
103
|
+
'<main data-composition-id="scene" data-duration="10" data-width="1920" data-height="1080">',
|
|
104
|
+
...["a:0", "hidden:1", "b:2"].map((t) => {
|
|
105
|
+
const [id, track] = t.split(":");
|
|
106
|
+
return `<div data-hf-id="hf-${id}" id="${id}" data-start="0" data-track-index="${track}"></div>`;
|
|
107
|
+
}),
|
|
108
|
+
"</main>",
|
|
109
|
+
].join("\n");
|
|
110
|
+
usePlayerStore.getState().setTopLevelIds(new Set(["a", "b"]));
|
|
111
|
+
const writeProjectFile = vi.fn().mockResolvedValue(undefined);
|
|
112
|
+
const getDrop = renderDropHook(source, writeProjectFile, [
|
|
113
|
+
clip("a", 0),
|
|
114
|
+
clip("hidden", 1),
|
|
115
|
+
clip("b", 2),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
await act(async () => {
|
|
119
|
+
await getDrop()("clip.mp4", { start: 1, track: 1, insertRow: 1, trackOrder: [0, 2] }, 2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const [, written] = writeProjectFile.mock.calls[0] as [string, string];
|
|
123
|
+
expect(written).toContain('id="b" data-start="0" data-track-index="2"');
|
|
124
|
+
expect(written).toContain('id="hidden" data-start="0" data-track-index="1"');
|
|
125
|
+
expect(written).toMatch(/<video id="clip"[^>]*data-track-index="1"/);
|
|
126
|
+
});
|
|
87
127
|
});
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// 600-line cap.
|
|
5
5
|
import { useCallback, type MutableRefObject, type RefObject } from "react";
|
|
6
6
|
import type { TimelineElement } from "../player";
|
|
7
|
+
import type { TimelineDropPlacement } from "../player/components/timelineCallbacks";
|
|
8
|
+
import { resolveDropTrack } from "../utils/timelineDropTrackInsert";
|
|
7
9
|
import {
|
|
8
10
|
buildTimelineAssetId,
|
|
9
11
|
buildTimelineAssetInsertHtml,
|
|
@@ -23,6 +25,18 @@ import { commitTimelineCompositionInsertion } from "../utils/timelineComposition
|
|
|
23
25
|
import { extendRootDurationInSource } from "../utils/rootDuration";
|
|
24
26
|
import { deriveTimelineStoreKeyForDomId } from "../player/lib/timelineElementHelpers";
|
|
25
27
|
import { selectAndRevealTimelineElement } from "../player/components/timelineDropReveal";
|
|
28
|
+
import { selectTimelineRowElements } from "../player/hooks/useTimelineRowElements";
|
|
29
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
30
|
+
|
|
31
|
+
/** The first uploaded file opens the new track (if asked); the rest land on the lane it landed on. */
|
|
32
|
+
function fileDropPlacement(
|
|
33
|
+
index: number,
|
|
34
|
+
next: { start: number; track: number },
|
|
35
|
+
dropped: TimelineDropPlacement | undefined,
|
|
36
|
+
landedTrack: number | undefined,
|
|
37
|
+
): TimelineDropPlacement {
|
|
38
|
+
return index === 0 ? { ...dropped, ...next } : { ...next, track: landedTrack ?? next.track };
|
|
39
|
+
}
|
|
26
40
|
|
|
27
41
|
interface UseTimelineAssetDropOpsOptions {
|
|
28
42
|
projectIdRef: MutableRefObject<string | null>;
|
|
@@ -52,16 +66,16 @@ export function useTimelineAssetDropOps({
|
|
|
52
66
|
observeProjectFileVersion,
|
|
53
67
|
}: UseTimelineAssetDropOpsOptions) {
|
|
54
68
|
// fallow-ignore-next-line complexity
|
|
55
|
-
const
|
|
69
|
+
const dropAssetAt = useCallback(
|
|
56
70
|
// fallow-ignore-next-line complexity
|
|
57
71
|
async (
|
|
58
72
|
assetPath: string,
|
|
59
|
-
placement:
|
|
73
|
+
placement: TimelineDropPlacement,
|
|
60
74
|
durationOverride?: number,
|
|
61
|
-
) => {
|
|
75
|
+
): Promise<number | undefined> => {
|
|
62
76
|
if (isRecordingRef?.current) {
|
|
63
77
|
showToast("Cannot edit timeline while recording", "error");
|
|
64
|
-
return;
|
|
78
|
+
return undefined;
|
|
65
79
|
}
|
|
66
80
|
const pid = projectIdRef.current;
|
|
67
81
|
if (!pid) throw new Error("No active project");
|
|
@@ -69,7 +83,7 @@ export function useTimelineAssetDropOps({
|
|
|
69
83
|
const kind = getTimelineAssetKind(assetPath);
|
|
70
84
|
if (!kind) {
|
|
71
85
|
showToast("Only image, video, and audio assets can be dropped onto the timeline.");
|
|
72
|
-
return;
|
|
86
|
+
return undefined;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
const targetPath = activeCompPath || "index.html";
|
|
@@ -91,9 +105,24 @@ export function useTimelineAssetDropOps({
|
|
|
91
105
|
);
|
|
92
106
|
const newElementZIndex = Math.max(1, relevantElements.length + 1);
|
|
93
107
|
|
|
108
|
+
const { source: sourceWithRoom, track } = resolveDropTrack({
|
|
109
|
+
source: originalContent,
|
|
110
|
+
// insertRow counts the rows the timeline shows, so plan against those.
|
|
111
|
+
elements: selectTimelineRowElements(
|
|
112
|
+
relevantElements,
|
|
113
|
+
usePlayerStore.getState().topLevelIds,
|
|
114
|
+
),
|
|
115
|
+
placement,
|
|
116
|
+
dropped: {
|
|
117
|
+
id: newId,
|
|
118
|
+
tag: kind === "image" ? "img" : kind,
|
|
119
|
+
start: normalizedStart,
|
|
120
|
+
duration: normalizedDuration,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
94
123
|
const patchedContent = extendRootDurationInSource(
|
|
95
124
|
insertTimelineAssetIntoSource(
|
|
96
|
-
|
|
125
|
+
sourceWithRoom,
|
|
97
126
|
buildTimelineAssetInsertHtml({
|
|
98
127
|
id: newId,
|
|
99
128
|
hfId: `hf-${generateId()}`,
|
|
@@ -101,7 +130,7 @@ export function useTimelineAssetDropOps({
|
|
|
101
130
|
kind,
|
|
102
131
|
start: normalizedStart,
|
|
103
132
|
duration: normalizedDuration,
|
|
104
|
-
track
|
|
133
|
+
track,
|
|
105
134
|
zIndex: newElementZIndex,
|
|
106
135
|
geometry: fitTimelineAssetGeometry(
|
|
107
136
|
null,
|
|
@@ -125,10 +154,12 @@ export function useTimelineAssetDropOps({
|
|
|
125
154
|
selectAndRevealTimelineElement(deriveTimelineStoreKeyForDomId(newId, targetPath));
|
|
126
155
|
forceReloadSdkSession?.();
|
|
127
156
|
reloadPreview();
|
|
157
|
+
return track;
|
|
128
158
|
} catch (error) {
|
|
129
159
|
const message =
|
|
130
160
|
error instanceof Error ? error.message : "Failed to drop asset onto timeline";
|
|
131
161
|
showToast(message);
|
|
162
|
+
return undefined;
|
|
132
163
|
}
|
|
133
164
|
},
|
|
134
165
|
[
|
|
@@ -144,10 +175,17 @@ export function useTimelineAssetDropOps({
|
|
|
144
175
|
],
|
|
145
176
|
);
|
|
146
177
|
|
|
178
|
+
const handleTimelineAssetDrop = useCallback(
|
|
179
|
+
async (assetPath: string, placement: TimelineDropPlacement, durationOverride?: number) => {
|
|
180
|
+
await dropAssetAt(assetPath, placement, durationOverride);
|
|
181
|
+
},
|
|
182
|
+
[dropAssetAt],
|
|
183
|
+
);
|
|
184
|
+
|
|
147
185
|
// fallow-ignore-next-line complexity
|
|
148
186
|
const handleTimelineFileDrop = useCallback(
|
|
149
187
|
// fallow-ignore-next-line complexity
|
|
150
|
-
async (files: File[], placement?:
|
|
188
|
+
async (files: File[], placement?: TimelineDropPlacement) => {
|
|
151
189
|
if (isRecordingRef?.current) {
|
|
152
190
|
showToast("Cannot edit timeline while recording", "error");
|
|
153
191
|
return;
|
|
@@ -166,15 +204,18 @@ export function useTimelineAssetDropOps({
|
|
|
166
204
|
placement ?? { start: 0, track: 0 },
|
|
167
205
|
durations,
|
|
168
206
|
);
|
|
207
|
+
let landedTrack: number | undefined;
|
|
169
208
|
for (const [index, assetPath] of uploaded.entries()) {
|
|
170
|
-
|
|
209
|
+
const next = placements[index] ?? placements[0];
|
|
210
|
+
const track = await dropAssetAt(
|
|
171
211
|
assetPath,
|
|
172
|
-
|
|
212
|
+
fileDropPlacement(index, next, placement, landedTrack),
|
|
173
213
|
durations[index],
|
|
174
214
|
);
|
|
215
|
+
if (index === 0) landedTrack = track;
|
|
175
216
|
}
|
|
176
217
|
},
|
|
177
|
-
[
|
|
218
|
+
[dropAssetAt, projectIdRef, uploadProjectFiles, isRecordingRef, showToast],
|
|
178
219
|
);
|
|
179
220
|
|
|
180
221
|
const handleTimelineCompositionDrop = useCallback(
|
|
@@ -5,6 +5,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
5
5
|
import type { TimelineElement } from "../player";
|
|
6
6
|
import { applyRippleShifts, useTimelineDeleteOps } from "./useTimelineDeleteOps";
|
|
7
7
|
import { installReactActEnvironment, mountReactHarness } from "./domSelectionTestHarness";
|
|
8
|
+
import { useTrackPendingTimelineEdit } from "./useTrackPendingTimelineEdit";
|
|
9
|
+
import { flushStudioPendingEdits } from "../utils/studioPendingEdits";
|
|
8
10
|
|
|
9
11
|
installReactActEnvironment();
|
|
10
12
|
|
|
@@ -117,3 +119,112 @@ describe("useTimelineDeleteOps: ripple undo label", () => {
|
|
|
117
119
|
);
|
|
118
120
|
});
|
|
119
121
|
});
|
|
122
|
+
|
|
123
|
+
// Regression: a delete's write can reach disk before its history entry is
|
|
124
|
+
// recorded, so an Undo fired in that window found nothing to restore.
|
|
125
|
+
// Fails without useTrackPendingTimelineEdit's wrap (see that file).
|
|
126
|
+
describe("useTimelineDeleteOps: undo race", () => {
|
|
127
|
+
// data-composition-id (readRootCompositionDuration's key) plus a
|
|
128
|
+
// remove-element mock that actually drops hf-c: needed for a real diff.
|
|
129
|
+
const html = `<!DOCTYPE html><html data-composition-variables='[]'><body>
|
|
130
|
+
<div data-hf-id="hf-stage" data-composition-id="main" data-duration="6">
|
|
131
|
+
<div data-hf-id="hf-a" data-start="0" data-duration="2"></div>
|
|
132
|
+
<div data-hf-id="hf-b" data-start="2" data-duration="2"></div>
|
|
133
|
+
<div data-hf-id="hf-c" data-start="4" data-duration="2"></div>
|
|
134
|
+
</div>
|
|
135
|
+
</body></html>`;
|
|
136
|
+
const htmlAfterDeletingC = `<!DOCTYPE html><html data-composition-variables='[]'><body>
|
|
137
|
+
<div data-hf-id="hf-stage" data-composition-id="main" data-duration="6">
|
|
138
|
+
<div data-hf-id="hf-a" data-start="0" data-duration="2"></div>
|
|
139
|
+
<div data-hf-id="hf-b" data-start="2" data-duration="2"></div>
|
|
140
|
+
</div>
|
|
141
|
+
</body></html>`;
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
vi.stubGlobal(
|
|
145
|
+
"fetch",
|
|
146
|
+
vi.fn(async (input: RequestInfo | URL) => {
|
|
147
|
+
const url = String(input);
|
|
148
|
+
if (url.includes("/file-mutations/remove-element/")) {
|
|
149
|
+
return new Response(JSON.stringify({ changed: true, content: htmlAfterDeletingC }), {
|
|
150
|
+
status: 200,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return new Response(JSON.stringify({ content: html }), { status: 200 });
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
afterEach(() => {
|
|
159
|
+
document.body.innerHTML = "";
|
|
160
|
+
vi.unstubAllGlobals();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// A manually-resolved gate stands in for the delete's write landing — a
|
|
164
|
+
// real timer raced the fix's own microtasks unpredictably (verified: it
|
|
165
|
+
// kept passing even with the fix's tracking call deleted).
|
|
166
|
+
function deferred<T>() {
|
|
167
|
+
let resolve!: (value: T) => void;
|
|
168
|
+
const promise = new Promise<T>((r) => (resolve = r));
|
|
169
|
+
return { promise, resolve };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function mountRaceHarness() {
|
|
173
|
+
const elements = [el("hf-a", 0, 2), el("hf-b", 2, 2), el("hf-c", 4, 2)];
|
|
174
|
+
const gate = deferred<void>();
|
|
175
|
+
const recordEdit = vi.fn(() => gate.promise);
|
|
176
|
+
let tracked: ((elements: TimelineElement[]) => Promise<void>) | null = null;
|
|
177
|
+
function Harness() {
|
|
178
|
+
const track = useTrackPendingTimelineEdit();
|
|
179
|
+
const hook = useTimelineDeleteOps({
|
|
180
|
+
projectIdRef: { current: "test-project" },
|
|
181
|
+
activeCompPath: "index.html",
|
|
182
|
+
timelineElements: elements,
|
|
183
|
+
showToast: vi.fn(),
|
|
184
|
+
writeProjectFile: vi.fn().mockResolvedValue(undefined),
|
|
185
|
+
recordEdit,
|
|
186
|
+
reloadPreview: vi.fn(),
|
|
187
|
+
previewIframeRef: { current: null },
|
|
188
|
+
handleTimelineGroupMove: vi.fn().mockResolvedValue(undefined),
|
|
189
|
+
});
|
|
190
|
+
tracked = track(hook.handleTimelineElementsDelete);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
mountReactHarness(<Harness />);
|
|
194
|
+
// Deletes the LAST clip (not the middle one): only this shrinks the
|
|
195
|
+
// composition's furthest-clip-end duration, so the write actually
|
|
196
|
+
// differs from the original content and recordEdit is reached — the
|
|
197
|
+
// middle clip's delete leaves duration unchanged and is a no-op write.
|
|
198
|
+
return { gate, recordEdit, deleteSelection: () => tracked!([elements[2]]) };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
it("the pending-edit flush waits while the delete's history write is still landing", async () => {
|
|
202
|
+
const { gate, recordEdit, deleteSelection } = mountRaceHarness();
|
|
203
|
+
const order: string[] = [];
|
|
204
|
+
const deletePromise = deleteSelection().then(() => order.push("deleted"));
|
|
205
|
+
// Let the delete run up to (and call) recordEdit, which is now gated —
|
|
206
|
+
// this is the exact window a fast Undo used to race into.
|
|
207
|
+
await vi.waitFor(() => expect(recordEdit).toHaveBeenCalled());
|
|
208
|
+
// No React state changes happen inside the flush itself here (no
|
|
209
|
+
// pending-edit-flush listeners are registered in this harness), so it
|
|
210
|
+
// does not need act()'s wrapping — only awaiting the delete's own
|
|
211
|
+
// resolution below does.
|
|
212
|
+
const flushPromise = flushStudioPendingEdits().then(() => order.push("flushed"));
|
|
213
|
+
await Promise.resolve();
|
|
214
|
+
await Promise.resolve();
|
|
215
|
+
expect(order).toEqual([]);
|
|
216
|
+
gate.resolve();
|
|
217
|
+
await deletePromise;
|
|
218
|
+
await flushPromise;
|
|
219
|
+
expect(order).toEqual(["deleted", "flushed"]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("the pending-edit flush resolves immediately once the delete has already finished", async () => {
|
|
223
|
+
const { gate, deleteSelection } = mountRaceHarness();
|
|
224
|
+
gate.resolve();
|
|
225
|
+
await deleteSelection();
|
|
226
|
+
await act(async () => {
|
|
227
|
+
await flushStudioPendingEdits();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|