@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,7 +5,6 @@ import { createRoot, type Root } from "react-dom/client";
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { TextAreaField } from "../components/editor/propertyPanelSections";
|
|
7
7
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
8
|
-
import type { LeftSidebarHandle } from "../components/sidebar/LeftSidebar";
|
|
9
8
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
10
9
|
import { useAppHotkeys } from "./useAppHotkeys";
|
|
11
10
|
|
|
@@ -70,7 +69,6 @@ function Harness() {
|
|
|
70
69
|
const [selectionRefreshed, setSelectionRefreshed] = useState(false);
|
|
71
70
|
const selectionRef = useRef<DomEditSelection | null>(parentWithTextChild());
|
|
72
71
|
const clearSelectionRef = useRef<() => void>(() => undefined);
|
|
73
|
-
const leftSidebarRef = useRef<LeftSidebarHandle | null>(null);
|
|
74
72
|
|
|
75
73
|
useAppHotkeys({
|
|
76
74
|
handleTimelineElementsDelete: timelineDeleteMany,
|
|
@@ -89,7 +87,6 @@ function Harness() {
|
|
|
89
87
|
showToast: vi.fn(),
|
|
90
88
|
syncHistoryPreviewAfterApply: vi.fn(async () => undefined),
|
|
91
89
|
waitForPendingDomEditSaves: vi.fn(async () => undefined),
|
|
92
|
-
leftSidebarRef,
|
|
93
90
|
handleCopy: vi.fn(() => false),
|
|
94
91
|
handlePaste: vi.fn(async () => undefined),
|
|
95
92
|
handleCut: vi.fn(async () => false),
|
|
@@ -2,8 +2,6 @@ import { useCallback, useEffect, useRef } from "react";
|
|
|
2
2
|
import { usePlayerStore } from "../player";
|
|
3
3
|
import type { TimelineElement } from "../player";
|
|
4
4
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
5
|
-
import type { LeftSidebarHandle } from "../components/sidebar/LeftSidebar";
|
|
6
|
-
import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
|
|
7
5
|
import { isTypingTarget } from "../utils/typingTarget";
|
|
8
6
|
import { useCaptionStore } from "../captions/store";
|
|
9
7
|
import {
|
|
@@ -11,13 +9,17 @@ import {
|
|
|
11
9
|
isCaptionPreviewVisible,
|
|
12
10
|
} from "../captions/components/CaptionOverlayUtils";
|
|
13
11
|
import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
|
|
14
|
-
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
|
|
15
12
|
import {
|
|
16
13
|
type HotkeyCallbacks,
|
|
17
14
|
dispatchModifierKey,
|
|
18
15
|
dispatchPlainKey,
|
|
19
16
|
handleUndoRedoKey,
|
|
20
17
|
} from "./appHotkeysDispatch";
|
|
18
|
+
import {
|
|
19
|
+
useEditHistoryActions,
|
|
20
|
+
type EditHistoryHandle,
|
|
21
|
+
type UseEditHistoryActionsOptions,
|
|
22
|
+
} from "./useEditHistoryActions";
|
|
21
23
|
|
|
22
24
|
function iframeContentWindow(iframe: HTMLIFrameElement | null): Window | null {
|
|
23
25
|
try {
|
|
@@ -69,28 +71,6 @@ function tryApplyBeatHistory(
|
|
|
69
71
|
|
|
70
72
|
// ── Types ──
|
|
71
73
|
|
|
72
|
-
interface HistoryResult {
|
|
73
|
-
ok: boolean;
|
|
74
|
-
reason?: string;
|
|
75
|
-
label?: string;
|
|
76
|
-
paths?: string[];
|
|
77
|
-
/** Per-file restored/previous content, used to soft-apply the preview. */
|
|
78
|
-
files?: Record<string, { previous: string; restored: string }>;
|
|
79
|
-
}
|
|
80
|
-
interface HistoryFileCallbacks {
|
|
81
|
-
readFile: (path: string) => Promise<string>;
|
|
82
|
-
writeFile: (path: string, content: string) => Promise<void>;
|
|
83
|
-
serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
|
|
84
|
-
}
|
|
85
|
-
interface EditHistoryHandle {
|
|
86
|
-
undo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
|
|
87
|
-
redo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
|
|
88
|
-
state: {
|
|
89
|
-
undo: ReadonlyArray<{ createdAt: number }>;
|
|
90
|
-
redo: ReadonlyArray<{ createdAt: number }>;
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
74
|
interface UseAppHotkeysParams {
|
|
95
75
|
handleTimelineElementsDelete: (elements: TimelineElement[]) => Promise<void>;
|
|
96
76
|
handleTimelineElementSplit: (element: TimelineElement, splitTime: number) => Promise<void>;
|
|
@@ -105,12 +85,8 @@ interface UseAppHotkeysParams {
|
|
|
105
85
|
readProjectFile: (path: string) => Promise<string>;
|
|
106
86
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
107
87
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
108
|
-
syncHistoryPreviewAfterApply:
|
|
109
|
-
paths?: string[];
|
|
110
|
-
files?: Record<string, { previous: string; restored: string }>;
|
|
111
|
-
}) => Promise<void>;
|
|
88
|
+
syncHistoryPreviewAfterApply: UseEditHistoryActionsOptions["syncHistoryPreviewAfterApply"];
|
|
112
89
|
waitForPendingDomEditSaves: () => Promise<void>;
|
|
113
|
-
leftSidebarRef: React.RefObject<LeftSidebarHandle | null>;
|
|
114
90
|
handleCopy: () => boolean;
|
|
115
91
|
handlePaste: () => Promise<void>;
|
|
116
92
|
handleCut: () => Promise<boolean>;
|
|
@@ -147,7 +123,6 @@ export function useAppHotkeys({
|
|
|
147
123
|
showToast,
|
|
148
124
|
syncHistoryPreviewAfterApply,
|
|
149
125
|
waitForPendingDomEditSaves,
|
|
150
|
-
leftSidebarRef,
|
|
151
126
|
handleCopy,
|
|
152
127
|
handlePaste,
|
|
153
128
|
handleCut,
|
|
@@ -165,22 +140,18 @@ export function useAppHotkeys({
|
|
|
165
140
|
|
|
166
141
|
// ── Undo / Redo ──
|
|
167
142
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<T>(paths: readonly string[], task: () => Promise<T>) =>
|
|
181
|
-
serializeStudioFileMutations(writeProjectFile, paths, task),
|
|
182
|
-
[writeProjectFile],
|
|
183
|
-
);
|
|
143
|
+
const fileHistory = useEditHistoryActions({
|
|
144
|
+
editHistory,
|
|
145
|
+
readOptionalProjectFile,
|
|
146
|
+
readProjectFile,
|
|
147
|
+
writeProjectFile,
|
|
148
|
+
showToast,
|
|
149
|
+
syncHistoryPreviewAfterApply,
|
|
150
|
+
waitForPendingDomEditSaves,
|
|
151
|
+
onAfterUndoRedo,
|
|
152
|
+
activeCompPath,
|
|
153
|
+
forceReloadSdkSession,
|
|
154
|
+
});
|
|
184
155
|
|
|
185
156
|
const applyHistory = useCallback(
|
|
186
157
|
async (direction: "undo" | "redo") => {
|
|
@@ -189,7 +160,7 @@ export function useAppHotkeys({
|
|
|
189
160
|
// earlier file edit (which would ALSO leave the caption change intact).
|
|
190
161
|
const captionState = useCaptionStore.getState();
|
|
191
162
|
// Only when the caption preview is actually visible: isEditMode stays
|
|
192
|
-
// true while the preview is hidden
|
|
163
|
+
// true while the preview is hidden, and eating ⌘Z
|
|
193
164
|
// there would pop invisible caption edits instead of file history.
|
|
194
165
|
if (captionState.isEditMode && isCaptionPreviewVisible()) {
|
|
195
166
|
const restored = direction === "undo" ? captionState.undo() : captionState.redo();
|
|
@@ -204,42 +175,9 @@ export function useAppHotkeys({
|
|
|
204
175
|
// Beat edits interleave with file history by timestamp; handle them first.
|
|
205
176
|
if (tryApplyBeatHistory(direction, editHistory.state, showToast)) return;
|
|
206
177
|
|
|
207
|
-
await
|
|
208
|
-
const result = await editHistory[direction]({
|
|
209
|
-
readFile: readHistoryFile,
|
|
210
|
-
writeFile: writeHistoryFile,
|
|
211
|
-
serialize: serializeHistoryFiles,
|
|
212
|
-
});
|
|
213
|
-
if (!result.ok && result.reason === "content-mismatch") {
|
|
214
|
-
showToast(
|
|
215
|
-
`File changed outside Studio. ${direction === "undo" ? "Undo" : "Redo"} history was not applied.`,
|
|
216
|
-
"info",
|
|
217
|
-
);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
if (result.ok && result.label) {
|
|
221
|
-
onAfterUndoRedo?.();
|
|
222
|
-
// If the active composition was among the written files, force-reload
|
|
223
|
-
// the SDK session so its in-memory doc matches the reverted content.
|
|
224
|
-
if (activeCompPath && result.paths?.includes(activeCompPath)) {
|
|
225
|
-
forceReloadSdkSession?.();
|
|
226
|
-
}
|
|
227
|
-
await syncHistoryPreviewAfterApply({ paths: result.paths, files: result.files });
|
|
228
|
-
showToast(`${direction === "undo" ? "Undid" : "Redid"} ${result.label}`, "info");
|
|
229
|
-
}
|
|
178
|
+
await fileHistory[direction]();
|
|
230
179
|
},
|
|
231
|
-
[
|
|
232
|
-
editHistory,
|
|
233
|
-
readHistoryFile,
|
|
234
|
-
showToast,
|
|
235
|
-
syncHistoryPreviewAfterApply,
|
|
236
|
-
waitForPendingDomEditSaves,
|
|
237
|
-
writeHistoryFile,
|
|
238
|
-
serializeHistoryFiles,
|
|
239
|
-
onAfterUndoRedo,
|
|
240
|
-
activeCompPath,
|
|
241
|
-
forceReloadSdkSession,
|
|
242
|
-
],
|
|
180
|
+
[editHistory.state, fileHistory, showToast],
|
|
243
181
|
);
|
|
244
182
|
|
|
245
183
|
const handleUndo = useCallback(() => applyHistory("undo"), [applyHistory]);
|
|
@@ -263,7 +201,6 @@ export function useAppHotkeys({
|
|
|
263
201
|
onToggleRecording,
|
|
264
202
|
onGroupSelection,
|
|
265
203
|
onUngroupSelection,
|
|
266
|
-
leftSidebarRef,
|
|
267
204
|
domEditSelectionRef,
|
|
268
205
|
showToast,
|
|
269
206
|
};
|
|
@@ -3,6 +3,7 @@ import { act } from "react";
|
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
4
|
import { expect, it, vi } from "vitest";
|
|
5
5
|
import { useCaptionDetection } from "./useCaptionDetection";
|
|
6
|
+
import { usePreviewIframeStore } from "../player/store/previewIframeStore";
|
|
6
7
|
|
|
7
8
|
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
8
9
|
|
|
@@ -52,3 +53,48 @@ it("does not let a foreign window trigger caption activation", () => {
|
|
|
52
53
|
vi.unstubAllGlobals();
|
|
53
54
|
}
|
|
54
55
|
});
|
|
56
|
+
|
|
57
|
+
it("re-runs caption activation on the promoted iframe's document", () => {
|
|
58
|
+
const host = document.createElement("div");
|
|
59
|
+
const a = document.createElement("iframe");
|
|
60
|
+
const b = document.createElement("iframe");
|
|
61
|
+
document.body.append(host, a, b);
|
|
62
|
+
const ref = { current: a };
|
|
63
|
+
const root = createRoot(host);
|
|
64
|
+
const fetchMock = vi.fn(() => new Promise<Response>(() => {}));
|
|
65
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
66
|
+
const params = {
|
|
67
|
+
projectId: "demo",
|
|
68
|
+
activeCompPath: "captions.html",
|
|
69
|
+
compIdToSrc: new Map<string, string>(),
|
|
70
|
+
captionEditMode: false,
|
|
71
|
+
captionHasSelection: false,
|
|
72
|
+
previewIframeRef: ref,
|
|
73
|
+
captionSync: { save: vi.fn(), loadOverrides: vi.fn(async () => {}) },
|
|
74
|
+
setRightCollapsed: vi.fn(),
|
|
75
|
+
};
|
|
76
|
+
function Harness() {
|
|
77
|
+
useCaptionDetection(params);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
act(() => usePreviewIframeStore.getState().setIframe(a));
|
|
82
|
+
act(() => root.render(<Harness />));
|
|
83
|
+
const group = b.contentDocument!.createElement("div");
|
|
84
|
+
group.className = "caption-group";
|
|
85
|
+
b.contentDocument!.body.append(group);
|
|
86
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
87
|
+
act(() => {
|
|
88
|
+
ref.current = b;
|
|
89
|
+
usePreviewIframeStore.getState().setIframe(b);
|
|
90
|
+
});
|
|
91
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
92
|
+
} finally {
|
|
93
|
+
act(() => root.unmount());
|
|
94
|
+
usePreviewIframeStore.setState({ iframe: null });
|
|
95
|
+
host.remove();
|
|
96
|
+
a.remove();
|
|
97
|
+
b.remove();
|
|
98
|
+
vi.unstubAllGlobals();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLivePreviewIframe } from "../player/store/previewIframeStore";
|
|
1
2
|
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
2
3
|
import { useEffect, useRef } from "react";
|
|
3
4
|
import { useCaptionStore } from "../captions/store";
|
|
@@ -26,6 +27,7 @@ export function useCaptionDetection({
|
|
|
26
27
|
captionSync,
|
|
27
28
|
setRightCollapsed,
|
|
28
29
|
}: UseCaptionDetectionParams) {
|
|
30
|
+
const livePreviewIframe = useLivePreviewIframe();
|
|
29
31
|
// Switching compositions must drop the previous comp's caption state — a
|
|
30
32
|
// stale model + full-canvas overlay otherwise blocks normal element editing
|
|
31
33
|
// on the new composition (and edit mode could never be exited).
|
|
@@ -145,7 +147,7 @@ export function useCaptionDetection({
|
|
|
145
147
|
return () => {
|
|
146
148
|
window.removeEventListener("message", handleMessage);
|
|
147
149
|
};
|
|
148
|
-
}, [activeCompPath, projectId, compIdToSrc, captionSync, previewIframeRef]);
|
|
150
|
+
}, [activeCompPath, projectId, compIdToSrc, captionSync, previewIframeRef, livePreviewIframe]);
|
|
149
151
|
|
|
150
152
|
// eslint-disable-next-line no-restricted-syntax
|
|
151
153
|
useEffect(() => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { useEffect, useRef } from "react";
|
|
7
7
|
import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
|
|
8
8
|
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
|
|
9
|
-
import
|
|
9
|
+
import { useDockLayoutStore } from "../components/dock/dockLayoutStore";
|
|
10
10
|
import type { PatchTarget } from "../utils/sourcePatcher";
|
|
11
11
|
import { logSelect } from "../utils/selectDebug";
|
|
12
12
|
|
|
@@ -30,7 +30,6 @@ interface UseDomEditPreviewSyncParams {
|
|
|
30
30
|
(iframe: HTMLIFrameElement) => Promise<void>
|
|
31
31
|
>;
|
|
32
32
|
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
|
|
33
|
-
getSidebarTab?: () => SidebarTab;
|
|
34
33
|
gsapCacheVersion?: number;
|
|
35
34
|
}
|
|
36
35
|
|
|
@@ -48,7 +47,6 @@ export function useDomEditPreviewSync({
|
|
|
48
47
|
syncPreviewHotkeys,
|
|
49
48
|
applyStudioManualEditsToPreviewRef,
|
|
50
49
|
openSourceForSelection,
|
|
51
|
-
getSidebarTab,
|
|
52
50
|
gsapCacheVersion,
|
|
53
51
|
}: UseDomEditPreviewSyncParams): void {
|
|
54
52
|
// Sync selection from preview document on load / refresh
|
|
@@ -142,15 +140,15 @@ export function useDomEditPreviewSync({
|
|
|
142
140
|
useEffect(
|
|
143
141
|
// fallow-ignore-next-line complexity
|
|
144
142
|
() => {
|
|
145
|
-
if (!domEditSelection || !openSourceRef.current
|
|
143
|
+
if (!domEditSelection || !openSourceRef.current) return;
|
|
146
144
|
if (!domEditSelection.sourceFile) return;
|
|
147
|
-
if (
|
|
145
|
+
if (!useDockLayoutStore.getState().visiblePanels.has("code")) return;
|
|
148
146
|
openSourceRef.current(domEditSelection.sourceFile, {
|
|
149
147
|
id: domEditSelection.id,
|
|
150
148
|
selector: domEditSelection.selector,
|
|
151
149
|
selectorIndex: domEditSelection.selectorIndex,
|
|
152
150
|
});
|
|
153
151
|
},
|
|
154
|
-
[domEditSelection
|
|
152
|
+
[domEditSelection],
|
|
155
153
|
);
|
|
156
154
|
}
|
|
@@ -5,7 +5,6 @@ import type { TimelineElement } from "../player";
|
|
|
5
5
|
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
|
6
6
|
import type { RightPanelTab } from "../utils/studioHelpers";
|
|
7
7
|
import type { PatchTarget } from "../utils/sourcePatcher";
|
|
8
|
-
import type { SidebarTab } from "../components/sidebar/LeftSidebar";
|
|
9
8
|
import type { Composition } from "@hyperframes/sdk";
|
|
10
9
|
import { sdkCutoverPersist, sdkDeletePersist, type PublishSdkSession } from "../utils/sdkCutover";
|
|
11
10
|
import { runResolverShadow, recordResolverParity } from "../utils/sdkResolverShadow";
|
|
@@ -53,8 +52,6 @@ export interface UseDomEditSessionParams extends DomEditTimelineParams {
|
|
|
53
52
|
reloadPreview: () => void;
|
|
54
53
|
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
|
|
55
54
|
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
|
|
56
|
-
selectSidebarTab?: (tab: SidebarTab) => void;
|
|
57
|
-
getSidebarTab?: () => SidebarTab;
|
|
58
55
|
sdkSession?: Composition | null;
|
|
59
56
|
publishSdkSession?: PublishSdkSession;
|
|
60
57
|
forceReloadSdkSession?: () => void;
|
|
@@ -97,8 +94,6 @@ export function useDomEditSession({
|
|
|
97
94
|
reloadPreview,
|
|
98
95
|
setRefreshKey: _setRefreshKey,
|
|
99
96
|
openSourceForSelection,
|
|
100
|
-
selectSidebarTab,
|
|
101
|
-
getSidebarTab,
|
|
102
97
|
sdkSession,
|
|
103
98
|
publishSdkSession,
|
|
104
99
|
forceReloadSdkSession,
|
|
@@ -430,8 +425,6 @@ export function useDomEditSession({
|
|
|
430
425
|
applyDomSelection,
|
|
431
426
|
buildDomSelectionFromTarget,
|
|
432
427
|
openSourceForSelection,
|
|
433
|
-
selectSidebarTab,
|
|
434
|
-
getSidebarTab,
|
|
435
428
|
updateGsapProperty,
|
|
436
429
|
updateGsapMeta,
|
|
437
430
|
deleteGsapAnimation,
|
|
@@ -16,7 +16,7 @@ import { useGsapAnimationFetchFallback } from "./useGsapAnimationFetchFallback";
|
|
|
16
16
|
import { useGsapInteractionFailureTelemetry } from "./useGsapInteractionFailureTelemetry";
|
|
17
17
|
import { useGsapSelectionHandlers } from "./useGsapSelectionHandlers";
|
|
18
18
|
import type { PatchTarget } from "../utils/sourcePatcher";
|
|
19
|
-
import
|
|
19
|
+
import { useDockLayoutStore } from "../components/dock/dockLayoutStore";
|
|
20
20
|
|
|
21
21
|
export interface UseDomEditWiringParams {
|
|
22
22
|
projectId: string | null;
|
|
@@ -43,8 +43,6 @@ export interface UseDomEditWiringParams {
|
|
|
43
43
|
) => void;
|
|
44
44
|
buildDomSelectionFromTarget: (element: HTMLElement) => Promise<DomEditSelection | null>;
|
|
45
45
|
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
|
|
46
|
-
selectSidebarTab?: (tab: SidebarTab) => void;
|
|
47
|
-
getSidebarTab?: () => SidebarTab;
|
|
48
46
|
// GSAP script commit ops (from useGsapScriptCommits)
|
|
49
47
|
updateGsapProperty: (
|
|
50
48
|
sel: DomEditSelection,
|
|
@@ -132,8 +130,6 @@ export function useDomEditWiring({
|
|
|
132
130
|
applyDomSelection,
|
|
133
131
|
buildDomSelectionFromTarget,
|
|
134
132
|
openSourceForSelection,
|
|
135
|
-
selectSidebarTab,
|
|
136
|
-
getSidebarTab,
|
|
137
133
|
updateGsapProperty,
|
|
138
134
|
updateGsapMeta,
|
|
139
135
|
deleteGsapAnimation,
|
|
@@ -157,16 +153,16 @@ export function useDomEditWiring({
|
|
|
157
153
|
|
|
158
154
|
const onClickToSource = useCallback(
|
|
159
155
|
(selection: DomEditSelection) => {
|
|
160
|
-
if (!openSourceForSelection
|
|
156
|
+
if (!openSourceForSelection) return;
|
|
161
157
|
if (!selection.sourceFile) return;
|
|
162
|
-
|
|
158
|
+
useDockLayoutStore.getState().activatePanel("code");
|
|
163
159
|
openSourceForSelection(selection.sourceFile, {
|
|
164
160
|
id: selection.id,
|
|
165
161
|
selector: selection.selector,
|
|
166
162
|
selectorIndex: selection.selectorIndex,
|
|
167
163
|
});
|
|
168
164
|
},
|
|
169
|
-
[openSourceForSelection
|
|
165
|
+
[openSourceForSelection],
|
|
170
166
|
);
|
|
171
167
|
|
|
172
168
|
// ── DOM selection -> timeline element sync ──
|
|
@@ -267,7 +263,6 @@ export function useDomEditWiring({
|
|
|
267
263
|
syncPreviewHotkeys,
|
|
268
264
|
applyStudioManualEditsToPreviewRef,
|
|
269
265
|
openSourceForSelection,
|
|
270
|
-
getSidebarTab,
|
|
271
266
|
gsapCacheVersion,
|
|
272
267
|
});
|
|
273
268
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act, createElement } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
|
|
7
|
+
import { useEditHistoryActions, type EditHistoryHandle } from "./useEditHistoryActions";
|
|
8
|
+
|
|
9
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
let root: Root | null = null;
|
|
12
|
+
afterEach(() => act(() => root?.unmount()));
|
|
13
|
+
|
|
14
|
+
function mount(result: { ok: boolean; reason?: string; label?: string; paths?: string[] }) {
|
|
15
|
+
const editHistory = {
|
|
16
|
+
undo: vi.fn<EditHistoryHandle["undo"]>(async (cb) => {
|
|
17
|
+
if (result.ok) await cb.writeFile("index.html", "before");
|
|
18
|
+
return result;
|
|
19
|
+
}),
|
|
20
|
+
redo: vi.fn<EditHistoryHandle["redo"]>(async () => result),
|
|
21
|
+
};
|
|
22
|
+
const deps = {
|
|
23
|
+
editHistory,
|
|
24
|
+
readOptionalProjectFile: vi.fn(async () => ""),
|
|
25
|
+
readProjectFile: vi.fn(async () => ""),
|
|
26
|
+
writeProjectFile: vi.fn(async () => undefined),
|
|
27
|
+
showToast: vi.fn(),
|
|
28
|
+
syncHistoryPreviewAfterApply: vi.fn(async () => undefined),
|
|
29
|
+
waitForPendingDomEditSaves: vi.fn(async () => undefined),
|
|
30
|
+
onAfterUndoRedo: vi.fn(),
|
|
31
|
+
activeCompPath: "index.html",
|
|
32
|
+
forceReloadSdkSession: vi.fn(),
|
|
33
|
+
};
|
|
34
|
+
let actions!: ReturnType<typeof useEditHistoryActions>;
|
|
35
|
+
function Probe() {
|
|
36
|
+
actions = useEditHistoryActions(deps);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
root = createRoot(document.createElement("div"));
|
|
40
|
+
act(() => root!.render(createElement(Probe)));
|
|
41
|
+
return { deps, actions };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("useEditHistoryActions", () => {
|
|
45
|
+
it("undo writes through the host writer, resyncs the preview and toasts the label", async () => {
|
|
46
|
+
const { deps, actions } = mount({ ok: true, label: "Move clip", paths: ["index.html"] });
|
|
47
|
+
await act(() => actions.undo());
|
|
48
|
+
expect(deps.waitForPendingDomEditSaves).toHaveBeenCalled();
|
|
49
|
+
expect(deps.writeProjectFile).toHaveBeenCalledWith("index.html", "before");
|
|
50
|
+
expect(deps.onAfterUndoRedo).toHaveBeenCalled();
|
|
51
|
+
expect(deps.forceReloadSdkSession).toHaveBeenCalled();
|
|
52
|
+
expect(deps.syncHistoryPreviewAfterApply).toHaveBeenCalled();
|
|
53
|
+
expect(deps.showToast).toHaveBeenCalledWith("Undid Move clip", "info");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("redo reports the redone label and skips the SDK reload for other files", async () => {
|
|
57
|
+
const { deps, actions } = mount({ ok: true, label: "Split clip", paths: ["other.html"] });
|
|
58
|
+
await act(() => actions.redo());
|
|
59
|
+
expect(deps.forceReloadSdkSession).not.toHaveBeenCalled();
|
|
60
|
+
expect(deps.showToast).toHaveBeenCalledWith("Redid Split clip", "info");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("explains a refused undo when the file changed on disk", async () => {
|
|
64
|
+
const { deps, actions } = mount({ ok: false, reason: "content-mismatch" });
|
|
65
|
+
await act(() => actions.undo());
|
|
66
|
+
expect(deps.showToast).toHaveBeenCalledWith(
|
|
67
|
+
"File changed outside Studio. Undo history was not applied.",
|
|
68
|
+
"info",
|
|
69
|
+
);
|
|
70
|
+
expect(deps.syncHistoryPreviewAfterApply).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("waits for pending saves first and reads the motion file through the optional reader", async () => {
|
|
74
|
+
const { deps, actions } = mount({ ok: true, label: "Move clip", paths: ["index.html"] });
|
|
75
|
+
const order: string[] = [];
|
|
76
|
+
deps.waitForPendingDomEditSaves.mockImplementation(async () => void order.push("wait"));
|
|
77
|
+
deps.editHistory.undo.mockImplementation(async (cb) => {
|
|
78
|
+
order.push("undo");
|
|
79
|
+
await cb.readFile(STUDIO_MOTION_PATH);
|
|
80
|
+
await cb.readFile("index.html");
|
|
81
|
+
await cb.serialize?.(["index.html"], async () => order.push("serialized"));
|
|
82
|
+
return { ok: true };
|
|
83
|
+
});
|
|
84
|
+
await act(() => actions.undo());
|
|
85
|
+
expect(order).toEqual(["wait", "undo", "serialized"]);
|
|
86
|
+
expect(deps.readOptionalProjectFile).toHaveBeenCalledWith(STUDIO_MOTION_PATH);
|
|
87
|
+
expect(deps.readProjectFile).toHaveBeenCalledWith("index.html");
|
|
88
|
+
expect(deps.readProjectFile).not.toHaveBeenCalledWith(STUDIO_MOTION_PATH);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// fallow-ignore-file complexity
|
|
2
|
+
import { useCallback, useMemo } from "react";
|
|
3
|
+
import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
|
|
4
|
+
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
|
|
5
|
+
|
|
6
|
+
interface HistoryResult {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
reason?: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
paths?: string[];
|
|
11
|
+
/** Per-file restored/previous content, used to soft-apply the preview. */
|
|
12
|
+
files?: Record<string, { previous: string; restored: string }>;
|
|
13
|
+
}
|
|
14
|
+
interface HistoryFileCallbacks {
|
|
15
|
+
readFile: (path: string) => Promise<string>;
|
|
16
|
+
writeFile: (path: string, content: string) => Promise<void>;
|
|
17
|
+
serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
|
|
18
|
+
}
|
|
19
|
+
export interface EditHistoryHandle {
|
|
20
|
+
undo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
|
|
21
|
+
redo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
|
|
22
|
+
state: {
|
|
23
|
+
undo: ReadonlyArray<{ createdAt: number }>;
|
|
24
|
+
redo: ReadonlyArray<{ createdAt: number }>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UseEditHistoryActionsOptions {
|
|
29
|
+
editHistory: Pick<EditHistoryHandle, "undo" | "redo">;
|
|
30
|
+
readOptionalProjectFile: (path: string) => Promise<string>;
|
|
31
|
+
readProjectFile: (path: string) => Promise<string>;
|
|
32
|
+
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
33
|
+
showToast: (message: string, tone?: "error" | "info") => void;
|
|
34
|
+
syncHistoryPreviewAfterApply: (restore: Pick<HistoryResult, "paths" | "files">) => Promise<void>;
|
|
35
|
+
waitForPendingDomEditSaves: () => Promise<void>;
|
|
36
|
+
onAfterUndoRedo?: () => void;
|
|
37
|
+
/** Active composition path — decides whether undo/redo must resync the SDK session. */
|
|
38
|
+
activeCompPath?: string | null;
|
|
39
|
+
/** Reloads the SDK session after a revert of the active comp, past the self-write suppress window. */
|
|
40
|
+
forceReloadSdkSession?: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Applies one persisted file-history step: the single owner of undo/redo over project files. */
|
|
44
|
+
export function useEditHistoryActions({
|
|
45
|
+
editHistory,
|
|
46
|
+
readOptionalProjectFile,
|
|
47
|
+
readProjectFile,
|
|
48
|
+
writeProjectFile,
|
|
49
|
+
showToast,
|
|
50
|
+
syncHistoryPreviewAfterApply,
|
|
51
|
+
waitForPendingDomEditSaves,
|
|
52
|
+
onAfterUndoRedo,
|
|
53
|
+
activeCompPath,
|
|
54
|
+
forceReloadSdkSession,
|
|
55
|
+
}: UseEditHistoryActionsOptions) {
|
|
56
|
+
const readHistoryFile = useCallback(
|
|
57
|
+
(path: string): Promise<string> =>
|
|
58
|
+
path === STUDIO_MOTION_PATH ? readOptionalProjectFile(path) : readProjectFile(path),
|
|
59
|
+
[readOptionalProjectFile, readProjectFile],
|
|
60
|
+
);
|
|
61
|
+
const serializeHistoryFiles = useCallback(
|
|
62
|
+
<T>(paths: readonly string[], task: () => Promise<T>) =>
|
|
63
|
+
serializeStudioFileMutations(writeProjectFile, paths, task),
|
|
64
|
+
[writeProjectFile],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const apply = useCallback(
|
|
68
|
+
async (direction: "undo" | "redo") => {
|
|
69
|
+
const [noun, verb] = direction === "undo" ? ["Undo", "Undid"] : ["Redo", "Redid"];
|
|
70
|
+
await waitForPendingDomEditSaves();
|
|
71
|
+
const result = await editHistory[direction]({
|
|
72
|
+
readFile: readHistoryFile,
|
|
73
|
+
writeFile: writeProjectFile,
|
|
74
|
+
serialize: serializeHistoryFiles,
|
|
75
|
+
});
|
|
76
|
+
if (!result.ok && result.reason === "content-mismatch") {
|
|
77
|
+
showToast(`File changed outside Studio. ${noun} history was not applied.`, "info");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (result.ok && result.label) {
|
|
81
|
+
onAfterUndoRedo?.();
|
|
82
|
+
if (activeCompPath && result.paths?.includes(activeCompPath)) {
|
|
83
|
+
forceReloadSdkSession?.();
|
|
84
|
+
}
|
|
85
|
+
await syncHistoryPreviewAfterApply({ paths: result.paths, files: result.files });
|
|
86
|
+
showToast(`${verb} ${result.label}`, "info");
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
[
|
|
90
|
+
editHistory,
|
|
91
|
+
readHistoryFile,
|
|
92
|
+
showToast,
|
|
93
|
+
syncHistoryPreviewAfterApply,
|
|
94
|
+
waitForPendingDomEditSaves,
|
|
95
|
+
writeProjectFile,
|
|
96
|
+
serializeHistoryFiles,
|
|
97
|
+
onAfterUndoRedo,
|
|
98
|
+
activeCompPath,
|
|
99
|
+
forceReloadSdkSession,
|
|
100
|
+
],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const undo = useCallback(() => apply("undo"), [apply]);
|
|
104
|
+
const redo = useCallback(() => apply("redo"), [apply]);
|
|
105
|
+
return useMemo(() => ({ undo, redo }), [undo, redo]);
|
|
106
|
+
}
|