@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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// Proves the package's public exports, not internal hook paths, are enough
|
|
4
|
+
// for a host to mount hand editing outside EditorShell: an edit writes
|
|
5
|
+
// through the history path, Undo restores it, and a 409 shows the banner.
|
|
6
|
+
import { act, createElement, useRef } from "react";
|
|
7
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
// Backend/storage mocking only, standing in for a host's own server and
|
|
10
|
+
// persistence — not part of the host's integration code under test.
|
|
11
|
+
import { studioFileContentVersion } from "./utils/studioFileVersion";
|
|
12
|
+
import { createMemoryEditHistoryStorage } from "./utils/editHistoryStorage";
|
|
13
|
+
import {
|
|
14
|
+
flushStudioPendingEdits,
|
|
15
|
+
useTimelineEditing,
|
|
16
|
+
useProjectFileWriter,
|
|
17
|
+
usePersistentEditHistory,
|
|
18
|
+
useEditHistoryActions,
|
|
19
|
+
ExternalFileConflictBanner,
|
|
20
|
+
StudioFileConflictError,
|
|
21
|
+
type ExternalFileChangeCoordinatorHandle,
|
|
22
|
+
type TimelineElement,
|
|
23
|
+
} from "./index";
|
|
24
|
+
|
|
25
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
26
|
+
|
|
27
|
+
const ORIGINAL_HTML = '<div id="clip" data-start="0" data-track-index="0"></div>';
|
|
28
|
+
|
|
29
|
+
let root: Root | null = null;
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
act(() => root?.unmount());
|
|
32
|
+
document.body.innerHTML = "";
|
|
33
|
+
vi.unstubAllGlobals();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** A minimal disk with etag-guarded PUT, faithful enough to exercise a real
|
|
37
|
+
* conflict: a stale If-Match gets a 409 with the current version + content. */
|
|
38
|
+
function stubHostProjectDisk(
|
|
39
|
+
initial: Record<string, string>,
|
|
40
|
+
options?: { editElsewhereAfterFirstRead?: boolean },
|
|
41
|
+
) {
|
|
42
|
+
const content = { ...initial };
|
|
43
|
+
const readsSeen = new Set<string>();
|
|
44
|
+
// The real backend's etag is a content hash (useProjectFileWriter derives
|
|
45
|
+
// If-Match the same way) — an arbitrary counter here would make every
|
|
46
|
+
// write from a host that hasn't just read the file look stale.
|
|
47
|
+
const versionOf = (path: string) => studioFileContentVersion(content[path]);
|
|
48
|
+
|
|
49
|
+
async function handlePut(path: string, init: RequestInit | undefined) {
|
|
50
|
+
const ifMatch = new Headers(init?.headers).get("If-Match");
|
|
51
|
+
const currentVersion = await versionOf(path);
|
|
52
|
+
if (ifMatch && ifMatch !== currentVersion) {
|
|
53
|
+
return new Response(JSON.stringify({ currentVersion, currentContent: content[path] }), {
|
|
54
|
+
status: 409,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
content[path] = String(init?.body ?? "");
|
|
58
|
+
return new Response(JSON.stringify({ version: await versionOf(path) }), { status: 200 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function handleGet(path: string) {
|
|
62
|
+
const served = content[path];
|
|
63
|
+
// Simulates another writer landing between this read and the caller's
|
|
64
|
+
// own write: content moves on right after being served once, so the
|
|
65
|
+
// caller's hash of what it just read no longer matches disk.
|
|
66
|
+
if (options?.editElsewhereAfterFirstRead && !readsSeen.has(path)) {
|
|
67
|
+
readsSeen.add(path);
|
|
68
|
+
content[path] = `${served} <!--edited elsewhere-->`;
|
|
69
|
+
}
|
|
70
|
+
return new Response(
|
|
71
|
+
JSON.stringify({ content: served, version: await studioFileContentVersion(served) }),
|
|
72
|
+
{ status: 200 },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
77
|
+
const match = String(input).match(/\/files\/([^?]+)/);
|
|
78
|
+
const path = match ? decodeURIComponent(match[1]) : "";
|
|
79
|
+
return (init?.method ?? "GET") === "PUT" ? handlePut(path, init) : handleGet(path);
|
|
80
|
+
});
|
|
81
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
82
|
+
return { content, fetchMock };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type TimelineEditingHandle = {
|
|
86
|
+
handleTimelineElementMove: (
|
|
87
|
+
element: TimelineElement,
|
|
88
|
+
updates: { start: number; track: number },
|
|
89
|
+
) => Promise<void>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function mountHost(
|
|
93
|
+
clip: TimelineElement,
|
|
94
|
+
iframe: HTMLIFrameElement,
|
|
95
|
+
): {
|
|
96
|
+
timelineEditing: TimelineEditingHandle;
|
|
97
|
+
undo: () => Promise<void>;
|
|
98
|
+
bannerHost: HTMLDivElement;
|
|
99
|
+
} {
|
|
100
|
+
let timelineEditing: TimelineEditingHandle | null = null;
|
|
101
|
+
let undo: (() => Promise<void>) | null = null;
|
|
102
|
+
let conflict: ExternalFileChangeCoordinatorHandle = {
|
|
103
|
+
blocked: null,
|
|
104
|
+
retry: async () => {},
|
|
105
|
+
useExternalFile: async () => {},
|
|
106
|
+
keepStudioFile: async () => {},
|
|
107
|
+
};
|
|
108
|
+
const bannerHost = document.createElement("div");
|
|
109
|
+
document.body.append(bannerHost);
|
|
110
|
+
const bannerRoot = createRoot(bannerHost);
|
|
111
|
+
// Created once: a fresh storage object on every render would change
|
|
112
|
+
// usePersistentEditHistory's effect deps each time and loop forever.
|
|
113
|
+
const storage = createMemoryEditHistoryStorage();
|
|
114
|
+
|
|
115
|
+
function Harness() {
|
|
116
|
+
const writer = useProjectFileWriter({ projectId: "p1" });
|
|
117
|
+
const editHistory = usePersistentEditHistory({ projectId: "p1", storage });
|
|
118
|
+
const historyActions = useEditHistoryActions({
|
|
119
|
+
editHistory,
|
|
120
|
+
readOptionalProjectFile: writer.readOptionalProjectFile,
|
|
121
|
+
readProjectFile: writer.readProjectFile,
|
|
122
|
+
writeProjectFile: writer.writeProjectFile,
|
|
123
|
+
showToast: () => {},
|
|
124
|
+
syncHistoryPreviewAfterApply: async () => {},
|
|
125
|
+
// The pattern a host's own dom-edit queue would also drain through —
|
|
126
|
+
// this is the one gate that closes the undo/delete race (#4194).
|
|
127
|
+
waitForPendingDomEditSaves: async () => {
|
|
128
|
+
const result = await flushStudioPendingEdits();
|
|
129
|
+
if (result.status !== "clean") throw result.error;
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
undo = historyActions.undo;
|
|
133
|
+
const commitRef = useRef(null);
|
|
134
|
+
timelineEditing = useTimelineEditing({
|
|
135
|
+
projectId: "p1",
|
|
136
|
+
activeCompPath: "index.html",
|
|
137
|
+
timelineElements: [clip],
|
|
138
|
+
showToast: () => {},
|
|
139
|
+
writeProjectFile: async (path, content, expectedContent) => {
|
|
140
|
+
try {
|
|
141
|
+
await writer.writeProjectFile(path, content, expectedContent);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error instanceof StudioFileConflictError) {
|
|
144
|
+
conflict = {
|
|
145
|
+
...conflict,
|
|
146
|
+
blocked: { status: "conflict", generation: 0, error, payload: null },
|
|
147
|
+
};
|
|
148
|
+
bannerRoot.render(createElement(ExternalFileConflictBanner, { coordinator: conflict }));
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
observeProjectFileVersion: writer.observeProjectFileVersion,
|
|
154
|
+
recordEdit: editHistory.recordEdit,
|
|
155
|
+
reloadPreview: () => {},
|
|
156
|
+
previewIframeRef: { current: iframe },
|
|
157
|
+
pendingTimelineEditPathRef: { current: new Set<string>() },
|
|
158
|
+
uploadProjectFiles: async () => [],
|
|
159
|
+
handleDomZIndexReorderCommitRef: commitRef,
|
|
160
|
+
});
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
root = createRoot(document.createElement("div"));
|
|
165
|
+
act(() => root!.render(createElement(Harness)));
|
|
166
|
+
if (!timelineEditing) throw new Error("Expected hook to mount");
|
|
167
|
+
if (!undo) throw new Error("Expected undo to be exposed");
|
|
168
|
+
return { timelineEditing, undo, bannerHost };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
describe("public export surface: a host mounting hand editing outside EditorShell", () => {
|
|
172
|
+
it("writes a move through the history path and undo restores it", async () => {
|
|
173
|
+
const disk = stubHostProjectDisk({ "index.html": ORIGINAL_HTML });
|
|
174
|
+
const iframe = document.createElement("iframe");
|
|
175
|
+
document.body.append(iframe);
|
|
176
|
+
iframe.contentDocument!.body.innerHTML = ORIGINAL_HTML;
|
|
177
|
+
const clip: TimelineElement = {
|
|
178
|
+
id: "clip",
|
|
179
|
+
domId: "clip",
|
|
180
|
+
tag: "div",
|
|
181
|
+
start: 0,
|
|
182
|
+
duration: 2,
|
|
183
|
+
track: 0,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const { timelineEditing, undo } = mountHost(clip, iframe);
|
|
187
|
+
await act(async () => {
|
|
188
|
+
await timelineEditing.handleTimelineElementMove(clip, { start: 5, track: 0 });
|
|
189
|
+
});
|
|
190
|
+
expect(disk.content["index.html"]).toContain('data-start="5"');
|
|
191
|
+
|
|
192
|
+
await act(async () => {
|
|
193
|
+
await undo();
|
|
194
|
+
});
|
|
195
|
+
expect(disk.content["index.html"]).toContain('data-start="0"');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("surfaces the conflict banner on a 409", async () => {
|
|
199
|
+
stubHostProjectDisk({ "index.html": ORIGINAL_HTML }, { editElsewhereAfterFirstRead: true });
|
|
200
|
+
const iframe = document.createElement("iframe");
|
|
201
|
+
document.body.append(iframe);
|
|
202
|
+
iframe.contentDocument!.body.innerHTML = ORIGINAL_HTML;
|
|
203
|
+
const clip: TimelineElement = {
|
|
204
|
+
id: "clip",
|
|
205
|
+
domId: "clip",
|
|
206
|
+
tag: "div",
|
|
207
|
+
start: 0,
|
|
208
|
+
duration: 2,
|
|
209
|
+
track: 0,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const { timelineEditing, bannerHost } = mountHost(clip, iframe);
|
|
213
|
+
|
|
214
|
+
await act(async () => {
|
|
215
|
+
await expect(
|
|
216
|
+
timelineEditing.handleTimelineElementMove(clip, { start: 5, track: 0 }),
|
|
217
|
+
).rejects.toBeInstanceOf(StudioFileConflictError);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(bannerHost.textContent).toContain("changed outside Studio");
|
|
221
|
+
});
|
|
222
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
|
+
// UI primitives
|
|
2
|
+
export { Button, buttonBase, buttonVariants } from "./components/ui/Button";
|
|
3
|
+
export type { ButtonSize, ButtonVariant, PreviewState } from "./components/ui/Button";
|
|
4
|
+
export { IconButton } from "./components/ui/IconButton";
|
|
5
|
+
export { Tab, TabPanel, Tabs, TabsList } from "./components/ui/Tabs";
|
|
6
|
+
export { Tooltip } from "./components/ui/Tooltip";
|
|
7
|
+
export { cn } from "./components/ui/cn";
|
|
8
|
+
export {
|
|
9
|
+
ContextMenu,
|
|
10
|
+
Menu,
|
|
11
|
+
MenuItem,
|
|
12
|
+
MenuRadioGroup,
|
|
13
|
+
MenuRadioItem,
|
|
14
|
+
MenuSeparator,
|
|
15
|
+
MenuShortcut,
|
|
16
|
+
popupSurface,
|
|
17
|
+
} from "./components/ui/Menu";
|
|
18
|
+
export type { MenuItemTone, PopupPreviewState } from "./components/ui/Menu";
|
|
19
|
+
export { Popover } from "./components/ui/Popover";
|
|
20
|
+
export { Input, fieldBase, fieldText } from "./components/ui/Input";
|
|
21
|
+
export type { InputProps } from "./components/ui/Input";
|
|
22
|
+
export { NumberField } from "./components/ui/NumberField";
|
|
23
|
+
export type { NumberFieldProps } from "./components/ui/NumberField";
|
|
24
|
+
export { Select } from "./components/ui/Select";
|
|
25
|
+
export type { SelectOption, SelectProps } from "./components/ui/Select";
|
|
26
|
+
export { Slider } from "./components/ui/Slider";
|
|
27
|
+
export type { SliderProps } from "./components/ui/Slider";
|
|
28
|
+
export { Toggle } from "./components/ui/Toggle";
|
|
29
|
+
export type { ToggleProps } from "./components/ui/Toggle";
|
|
30
|
+
|
|
1
31
|
// NLE Layout
|
|
2
32
|
export { EditorShell } from "./components/EditorShell";
|
|
33
|
+
export type { EditorShellProps } from "./components/EditorShell";
|
|
3
34
|
export { NLEPreview } from "./components/nle/NLEPreview";
|
|
4
35
|
export { CompositionBreadcrumb } from "./components/nle/CompositionBreadcrumb";
|
|
5
36
|
export type { CompositionLevel } from "./components/nle/CompositionBreadcrumb";
|
|
@@ -19,6 +50,10 @@ export {
|
|
|
19
50
|
} from "./player";
|
|
20
51
|
export type { TimelineElement } from "./player";
|
|
21
52
|
|
|
53
|
+
// Host overlays: draw over the preview in composition coordinates (see EditorShellProps.gestureOverlay)
|
|
54
|
+
export { usePreviewCompositionRect } from "./components/editor/usePreviewCompositionRect";
|
|
55
|
+
export type { PreviewCompositionRect } from "./components/editor/usePreviewCompositionRect";
|
|
56
|
+
|
|
22
57
|
// Editor
|
|
23
58
|
export { SourceEditor } from "./components/editor/SourceEditor";
|
|
24
59
|
export { PropertyPanel } from "./components/editor/PropertyPanel";
|
|
@@ -37,6 +72,22 @@ export {
|
|
|
37
72
|
} from "./components/editor/domEditingAgentPrompt";
|
|
38
73
|
export type { AgentPromptElementInfo } from "./components/editor/domEditingAgentPrompt";
|
|
39
74
|
|
|
75
|
+
// Render queue
|
|
76
|
+
export { RenderQueue } from "./components/renders/RenderQueue";
|
|
77
|
+
export type { RenderQueueProps, CompositionDimensions } from "./components/renders/RenderQueue";
|
|
78
|
+
export { useRenderQueue } from "./components/renders/useRenderQueue";
|
|
79
|
+
export type { FfmpegStatus } from "./components/renders/useFfmpegStatus";
|
|
80
|
+
export type {
|
|
81
|
+
RenderJob,
|
|
82
|
+
ResolutionPreset,
|
|
83
|
+
StartRenderOptions,
|
|
84
|
+
} from "./components/renders/useRenderQueue";
|
|
85
|
+
export {
|
|
86
|
+
getPersistedRenderSettings,
|
|
87
|
+
persistRenderSettings,
|
|
88
|
+
} from "./components/renders/renderSettings";
|
|
89
|
+
export type { PersistedRenderSettings } from "./components/renders/renderSettings";
|
|
90
|
+
|
|
40
91
|
// Hooks
|
|
41
92
|
export { useElementPicker } from "./hooks/useElementPicker";
|
|
42
93
|
export type { PickedElement } from "./hooks/useElementPicker";
|
|
@@ -45,3 +96,39 @@ export type { PickedElement } from "./hooks/useElementPicker";
|
|
|
45
96
|
export { resolveSourceFile, applyPatch } from "./utils/sourcePatcher";
|
|
46
97
|
export type { PatchOperation } from "./utils/sourcePatcher";
|
|
47
98
|
export { parseStyleString, mergeStyleIntoTag, findElementBlock } from "./utils/htmlEditor";
|
|
99
|
+
|
|
100
|
+
// Timeline editing: Studio's own hand-edit path, undo/redo, the
|
|
101
|
+
// etag-guarded writer and the conflict banner, for a host mounting the
|
|
102
|
+
// timeline outside EditorShell.
|
|
103
|
+
export { usePersistentEditHistory } from "./hooks/usePersistentEditHistory";
|
|
104
|
+
export type { UsePersistentEditHistoryOptions } from "./hooks/usePersistentEditHistory";
|
|
105
|
+
export { useTimelineEditing } from "./hooks/useTimelineEditing";
|
|
106
|
+
export type { UseTimelineEditingOptions } from "./hooks/useTimelineEditingTypes";
|
|
107
|
+
// A host's own waitForPendingDomEditSaves must also call this, or undo/redo
|
|
108
|
+
// can race a write still in flight (see useTrackPendingTimelineEdit.ts).
|
|
109
|
+
export { flushStudioPendingEdits } from "./utils/studioPendingEdits";
|
|
110
|
+
export type { StudioPendingEditsDrainResult } from "./utils/studioPendingEdits";
|
|
111
|
+
export type {
|
|
112
|
+
CanEditTimelineElement,
|
|
113
|
+
TimelineEditPermission,
|
|
114
|
+
} from "./hooks/timelineEditPermission";
|
|
115
|
+
export { useEditHistoryActions } from "./hooks/useEditHistoryActions";
|
|
116
|
+
export type {
|
|
117
|
+
EditHistoryHandle,
|
|
118
|
+
UseEditHistoryActionsOptions,
|
|
119
|
+
} from "./hooks/useEditHistoryActions";
|
|
120
|
+
export { useProjectFileWriter } from "./hooks/useProjectFileWriter";
|
|
121
|
+
export type { UseProjectFileWriterOptions } from "./hooks/useProjectFileWriter";
|
|
122
|
+
// A host's writeProjectFile throws this on a 409; catch it to know when
|
|
123
|
+
// to show ExternalFileConflictBanner.
|
|
124
|
+
export { StudioFileConflictError } from "./utils/studioSaveDiagnostics";
|
|
125
|
+
export { ExternalFileConflictBanner } from "./components/ExternalFileConflictBanner";
|
|
126
|
+
export type {
|
|
127
|
+
ExternalFileChangeCoordinatorHandle,
|
|
128
|
+
ExternalFileChangeBlockedState,
|
|
129
|
+
} from "./hooks/useExternalFileChangeCoordinator";
|
|
130
|
+
export { TimelinePane } from "./components/nle/TimelinePane";
|
|
131
|
+
export type { TimelinePaneProps } from "./components/nle/TimelinePane";
|
|
132
|
+
export { TimelineEditProvider } from "./contexts/TimelineEditContext";
|
|
133
|
+
export type { TimelineEditCallbacks } from "./player/components/timelineCallbacks";
|
|
134
|
+
export type { BlockedTimelineEditIntent } from "./player/components/timelineEditing";
|
package/src/main.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { StrictMode } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { IconContext } from "@phosphor-icons/react";
|
|
3
4
|
import { StudioApp } from "./App";
|
|
4
5
|
import { StudioErrorBoundary } from "./components/StudioErrorBoundary";
|
|
6
|
+
import { readIconTokens } from "./styles/iconTokens";
|
|
5
7
|
import { trackStudioEvent } from "./utils/studioTelemetry";
|
|
6
8
|
import "./styles/studio.css";
|
|
7
9
|
|
|
@@ -93,8 +95,12 @@ window.addEventListener("unhandledrejection", (event) => {
|
|
|
93
95
|
|
|
94
96
|
createRoot(document.getElementById("root")!).render(
|
|
95
97
|
<StrictMode>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
{/* One icon size and weight for the whole app, taken from the theme file.
|
|
99
|
+
Icons that pass their own size or weight still win. */}
|
|
100
|
+
<IconContext.Provider value={readIconTokens()}>
|
|
101
|
+
<StudioErrorBoundary>
|
|
102
|
+
<StudioApp />
|
|
103
|
+
</StudioErrorBoundary>
|
|
104
|
+
</IconContext.Provider>
|
|
99
105
|
</StrictMode>,
|
|
100
106
|
);
|
|
@@ -191,7 +191,7 @@ export const AudioWaveform = memo(function AudioWaveform({
|
|
|
191
191
|
"repeating-linear-gradient(90deg, rgba(75,163,210,0.35) 0 2px, transparent 2px 5px)",
|
|
192
192
|
}}
|
|
193
193
|
/>
|
|
194
|
-
<span className="relative rounded bg-black/50 px-1 text-[8px] text-neutral-500">
|
|
194
|
+
<span className="relative rounded-sm bg-black/50 px-1 text-[8px] text-neutral-500">
|
|
195
195
|
waveform unavailable
|
|
196
196
|
</span>
|
|
197
197
|
</div>
|
|
@@ -29,7 +29,7 @@ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
|
|
|
29
29
|
}: AutomationSelectionMenuProps) {
|
|
30
30
|
const menuRef = useContextMenuDismiss(onClose);
|
|
31
31
|
const row =
|
|
32
|
-
"block w-full px-2 py-1 text-left text-[11px] text-panel-text-1 hover:bg-panel-
|
|
32
|
+
"block w-full px-2 py-1 text-left text-[11px] text-panel-text-1 hover:bg-panel-hover disabled:opacity-40";
|
|
33
33
|
// Same edge-clamping precedent as TrackGapContextMenu: without it a
|
|
34
34
|
// right-click near the bottom/right of the timeline renders this menu
|
|
35
35
|
// partially off-screen.
|
|
@@ -41,11 +41,11 @@ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
|
|
|
41
41
|
return createPortal(
|
|
42
42
|
<div
|
|
43
43
|
ref={menuRef}
|
|
44
|
-
// z-
|
|
44
|
+
// z-200 for the same reason the timeline's FX popover uses it: this is
|
|
45
45
|
// portaled to `document.body`, but the ruler's sticky header sits at z-70
|
|
46
46
|
// in the SAME root stacking context, so a z-50 menu opened near the top of
|
|
47
47
|
// the timeline is painted through by the ruler and the playhead.
|
|
48
|
-
className="hf-automation-menu fixed z-
|
|
48
|
+
className="hf-automation-menu fixed z-200 min-w-[140px] rounded-sm border border-panel-border-input bg-panel-bg-2 py-1 shadow-lg"
|
|
49
49
|
style={{ left: adjustedX, top: adjustedY }}
|
|
50
50
|
>
|
|
51
51
|
{AUTOMATION_SHAPES.map((shape) => (
|
|
@@ -78,7 +78,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
|
|
|
78
78
|
ref={menuRef}
|
|
79
79
|
role="menu"
|
|
80
80
|
aria-label="Clip actions"
|
|
81
|
-
className="fixed z-
|
|
81
|
+
className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
|
|
82
82
|
style={{ left: adjustedX, top: adjustedY }}
|
|
83
83
|
>
|
|
84
84
|
{splitLabel && (
|
|
@@ -86,7 +86,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
|
|
|
86
86
|
<button
|
|
87
87
|
type="button"
|
|
88
88
|
role="menuitem"
|
|
89
|
-
className={`w-full flex items-center justify-between px-3 py-1.5 text-xs text-left outline-
|
|
89
|
+
className={`w-full flex items-center justify-between px-3 py-1.5 text-xs text-left outline-hidden focus-visible:bg-neutral-800 ${
|
|
90
90
|
canSplit
|
|
91
91
|
? "text-neutral-300 hover:bg-neutral-800 cursor-pointer"
|
|
92
92
|
: "text-neutral-600 cursor-not-allowed"
|
|
@@ -159,7 +159,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
|
|
|
159
159
|
<button
|
|
160
160
|
type="button"
|
|
161
161
|
role="menuitem"
|
|
162
|
-
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-
|
|
162
|
+
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left"
|
|
163
163
|
onClick={() => {
|
|
164
164
|
onDelete(element);
|
|
165
165
|
onClose();
|
|
@@ -87,9 +87,6 @@ describe("buildCompositionThumbnailUrl", () => {
|
|
|
87
87
|
|
|
88
88
|
expect(buildCompositionThumbnailUrl(base)).not.toContain("output=");
|
|
89
89
|
expect(buildCompositionThumbnailUrl({ ...base, output: "source" })).toContain("output=source");
|
|
90
|
-
expect(buildCompositionThumbnailUrl({ ...base, output: "storyboard" })).toContain(
|
|
91
|
-
"output=storyboard",
|
|
92
|
-
);
|
|
93
90
|
});
|
|
94
91
|
|
|
95
92
|
it("includes the persisted content revision in the cache identity", () => {
|
|
@@ -44,10 +44,9 @@ export function buildCompositionThumbnailUrl({
|
|
|
44
44
|
/**
|
|
45
45
|
* Capture density. Omitted, the route bounds the image to its preview cap —
|
|
46
46
|
* right for the timeline, where thumbnails are small and numerous and their
|
|
47
|
-
* decoded bytes are budgeted. `"
|
|
48
|
-
* high-density review size; `"source"` uses the composition's own dimensions.
|
|
47
|
+
* decoded bytes are budgeted. `"source"` uses the composition's own dimensions.
|
|
49
48
|
*/
|
|
50
|
-
output?: "source"
|
|
49
|
+
output?: "source";
|
|
51
50
|
contentRevision?: number;
|
|
52
51
|
}): string {
|
|
53
52
|
const thumbnailBase = previewUrl
|
|
@@ -159,7 +158,7 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
|
|
|
159
158
|
{Array.from({ length: frameCount }, (_, index) => (
|
|
160
159
|
<div
|
|
161
160
|
key={index}
|
|
162
|
-
className="relative h-full
|
|
161
|
+
className="relative h-full shrink-0 overflow-hidden"
|
|
163
162
|
style={{ width: frameW }}
|
|
164
163
|
>
|
|
165
164
|
<img
|
|
@@ -168,7 +168,7 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }:
|
|
|
168
168
|
}}
|
|
169
169
|
placeholder="What should change?"
|
|
170
170
|
rows={2}
|
|
171
|
-
className="w-full px-3 py-2 text-xs bg-neutral-800/60 border border-neutral-700/40 rounded-lg text-neutral-200 placeholder:text-neutral-600 resize-none focus:outline-
|
|
171
|
+
className="w-full px-3 py-2 text-xs bg-neutral-800/60 border border-neutral-700/40 rounded-lg text-neutral-200 placeholder:text-neutral-600 resize-none focus:outline-hidden focus:border-studio-accent/40 transition-colors"
|
|
172
172
|
/>
|
|
173
173
|
</div>
|
|
174
174
|
|
|
@@ -73,7 +73,7 @@ export const ImageThumbnail = memo(function ImageThumbnail({
|
|
|
73
73
|
{Array.from({ length: frameCount }, (_, index) => (
|
|
74
74
|
<div
|
|
75
75
|
key={index}
|
|
76
|
-
className="relative h-full
|
|
76
|
+
className="relative h-full shrink-0 overflow-hidden bg-neutral-900"
|
|
77
77
|
style={{ width: frameW }}
|
|
78
78
|
>
|
|
79
79
|
<img
|
|
@@ -40,9 +40,9 @@ interface KeyframeDiamondContextMenuProps {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const ITEM_CLS =
|
|
43
|
-
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-200 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-
|
|
43
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-200 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left";
|
|
44
44
|
const DESTRUCTIVE_ITEM_CLS =
|
|
45
|
-
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-
|
|
45
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left";
|
|
46
46
|
|
|
47
47
|
export function KeyframeDiamondContextMenu({
|
|
48
48
|
state,
|
|
@@ -96,7 +96,7 @@ export function KeyframeDiamondContextMenu({
|
|
|
96
96
|
ref={menuRef}
|
|
97
97
|
role="menu"
|
|
98
98
|
aria-label="Keyframe actions"
|
|
99
|
-
className="fixed z-
|
|
99
|
+
className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px] overflow-y-auto"
|
|
100
100
|
style={{ left: adjustedX, top: adjustedY, maxHeight: `calc(100vh - ${adjustedY + 8}px)` }}
|
|
101
101
|
>
|
|
102
102
|
{onMoveToPlayhead && (
|
|
@@ -172,7 +172,7 @@ export function KeyframeDiamondContextMenu({
|
|
|
172
172
|
<button
|
|
173
173
|
type="button"
|
|
174
174
|
role="menuitem"
|
|
175
|
-
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-red-950/40 focus-visible:bg-red-950/40 outline-
|
|
175
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-red-950/40 focus-visible:bg-red-950/40 outline-hidden cursor-pointer text-left"
|
|
176
176
|
onClick={() => {
|
|
177
177
|
onDeleteAll(state.element, state.animationId);
|
|
178
178
|
onClose();
|
|
@@ -37,7 +37,7 @@ export function LaneToggleButton({
|
|
|
37
37
|
title={`${isExpanded ? "Hide" : "Show"} lanes`}
|
|
38
38
|
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target. The glyph stays 11px;
|
|
39
39
|
// only the hit box grows.
|
|
40
|
-
className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
40
|
+
className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
41
41
|
isExpanded ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
|
|
42
42
|
}`}
|
|
43
43
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -97,7 +97,7 @@ export function LayerDisclosureRow({
|
|
|
97
97
|
</span>
|
|
98
98
|
{/* Wraps rather than truncating: a truncated name needs a hover to be
|
|
99
99
|
read, which a scanned column cannot rely on. */}
|
|
100
|
-
<span className="min-w-0 flex-1 break-
|
|
100
|
+
<span className="min-w-0 flex-1 wrap-break-word font-medium leading-tight">{name}</span>
|
|
101
101
|
<TrackClipCount clipCount={clipCount} />
|
|
102
102
|
{children}
|
|
103
103
|
{/* Anchored right, on every header that has one: the lane toggle is the
|
|
@@ -116,7 +116,7 @@ const FullscreenButton = memo(function FullscreenButton({
|
|
|
116
116
|
trackStudioEvent("playback", { action: "fullscreen_toggle", active: !isFullscreen });
|
|
117
117
|
onToggleFullscreen();
|
|
118
118
|
}}
|
|
119
|
-
className={`flex h-7 w-7
|
|
119
|
+
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${
|
|
120
120
|
isFullscreen ? "text-studio-accent" : "text-neutral-500 hover:text-neutral-200"
|
|
121
121
|
}`}
|
|
122
122
|
aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
@@ -153,7 +153,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
153
153
|
);
|
|
154
154
|
|
|
155
155
|
return (
|
|
156
|
-
<div ref={shortcutsPanelRef} className="relative
|
|
156
|
+
<div ref={shortcutsPanelRef} className="relative shrink-0">
|
|
157
157
|
<Tooltip label="Shortcuts and tools">
|
|
158
158
|
<button
|
|
159
159
|
ref={triggerRef}
|
|
@@ -192,7 +192,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
192
192
|
// Deliberately NOT aria-modal. This is a non-modal disclosure: focus is
|
|
193
193
|
// not trapped and the rest of the editor stays operable, so claiming
|
|
194
194
|
// modality would make assistive tech treat the whole app as inert.
|
|
195
|
-
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-
|
|
195
|
+
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-hidden"
|
|
196
196
|
style={{
|
|
197
197
|
background: "#161618",
|
|
198
198
|
border: "1px solid rgba(255,255,255,0.08)",
|
|
@@ -212,7 +212,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
212
212
|
pattern="[0-9]*"
|
|
213
213
|
aria-label="Jump to frame"
|
|
214
214
|
placeholder="frame number"
|
|
215
|
-
className="h-6 flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-
|
|
215
|
+
className="h-6 flex-1 rounded-sm border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-hidden transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60"
|
|
216
216
|
onKeyDown={handleJumpKeyDown}
|
|
217
217
|
onBlur={commitJumpFrame}
|
|
218
218
|
/>
|
|
@@ -220,7 +220,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
220
220
|
<button
|
|
221
221
|
type="submit"
|
|
222
222
|
disabled={disabled}
|
|
223
|
-
className="h-6 px-2 rounded border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
|
|
223
|
+
className="h-6 px-2 rounded-sm border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
|
|
224
224
|
>
|
|
225
225
|
Go
|
|
226
226
|
</button>
|
|
@@ -236,7 +236,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
236
236
|
<div className="flex items-center justify-between gap-2">
|
|
237
237
|
<div className="flex items-center gap-2">
|
|
238
238
|
<span
|
|
239
|
-
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
239
|
+
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
240
240
|
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
241
241
|
>
|
|
242
242
|
I
|
|
@@ -253,7 +253,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
253
253
|
<button
|
|
254
254
|
type="button"
|
|
255
255
|
onClick={() => setInPoint(null)}
|
|
256
|
-
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
256
|
+
className="w-4 h-4 flex items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
257
257
|
aria-label="Clear in-point"
|
|
258
258
|
>
|
|
259
259
|
<svg
|
|
@@ -277,7 +277,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
277
277
|
<div className="flex items-center justify-between gap-2">
|
|
278
278
|
<div className="flex items-center gap-2">
|
|
279
279
|
<span
|
|
280
|
-
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
280
|
+
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
281
281
|
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
282
282
|
>
|
|
283
283
|
O
|
|
@@ -294,7 +294,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
294
294
|
<button
|
|
295
295
|
type="button"
|
|
296
296
|
onClick={() => setOutPoint(null)}
|
|
297
|
-
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
297
|
+
className="w-4 h-4 flex items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
298
298
|
aria-label="Clear out-point"
|
|
299
299
|
>
|
|
300
300
|
<svg
|
|
@@ -328,7 +328,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
328
328
|
{section.hints.map((hint) => (
|
|
329
329
|
<div key={hint.key} className="flex items-center gap-3">
|
|
330
330
|
<span
|
|
331
|
-
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
|
|
331
|
+
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
|
|
332
332
|
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
333
333
|
>
|
|
334
334
|
{hint.key}
|
|
@@ -23,7 +23,7 @@ export const SpeedMenu = memo(function SpeedMenu({
|
|
|
23
23
|
const speedMenuContainerRef = useContextMenuDismiss(closeMenu);
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
|
-
<div ref={speedMenuContainerRef} className="relative
|
|
26
|
+
<div ref={speedMenuContainerRef} className="relative shrink-0">
|
|
27
27
|
<Tooltip label="Playback speed">
|
|
28
28
|
<button
|
|
29
29
|
type="button"
|
|
@@ -57,10 +57,8 @@ export const SpeedMenu = memo(function SpeedMenu({
|
|
|
57
57
|
setPlaybackRate(rate);
|
|
58
58
|
setShowSpeedMenu(false);
|
|
59
59
|
}}
|
|
60
|
-
className={`block w-full px-3 py-1.5 text-[11px] text-left font-mono tabular-nums transition-colors outline-
|
|
61
|
-
isCurrent
|
|
62
|
-
? "text-neutral-50 bg-white/[0.06]"
|
|
63
|
-
: "text-neutral-500 hover:bg-white/[0.04]"
|
|
60
|
+
className={`block w-full px-3 py-1.5 text-[11px] text-left font-mono tabular-nums transition-colors outline-hidden focus-visible:bg-white/4 ${
|
|
61
|
+
isCurrent ? "text-neutral-50 bg-white/6" : "text-neutral-500 hover:bg-white/4"
|
|
64
62
|
}`}
|
|
65
63
|
>
|
|
66
64
|
{rate}x
|