@hyperframes/studio 0.8.50 → 0.8.52
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-I5YRzB02.js} +3 -3
- package/dist/assets/{index-Dt3qBI-9.js → index-BTAfgesM.js} +1 -1
- package/dist/assets/index-CxAu_Okg.css +1 -0
- package/dist/assets/index-DocJlW2v.js +376 -0
- package/dist/assets/{index-DUAIWxkI.js → index-Du5x7KUR.js} +1 -1
- package/dist/index.d.ts +419 -20
- package/dist/index.html +2 -2
- package/dist/index.js +7496 -8288
- 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 +131 -143
- 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.tsx +8 -7
- package/src/components/ExternalFileConflictBanner.tsx +16 -16
- package/src/components/LintModal.tsx +6 -16
- package/src/components/MediaPreview.tsx +2 -2
- package/src/components/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.tsx +110 -210
- package/src/components/StudioLeftSidebar.tsx +7 -4
- package/src/components/StudioOverlays.tsx +1 -1
- package/src/components/StudioRightPanel.tsx +51 -39
- 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 +260 -0
- package/src/components/dock/Dock.tsx +281 -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.ts +91 -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.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/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 +3 -3
- package/src/components/nle/TimelineResizeDivider.tsx +2 -2
- package/src/components/panels/SlideshowPanel.tsx +2 -2
- package/src/components/panels/SlideshowSubPanels.tsx +20 -17
- package/src/components/panels/VariablesBindElement.tsx +2 -2
- package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
- package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +3 -3
- package/src/components/panels/VariablesValueControls.tsx +2 -2
- package/src/components/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/sidebar/AssetCard.tsx +8 -8
- package/src/components/sidebar/AssetContextMenu.tsx +8 -8
- package/src/components/sidebar/AssetsTab.tsx +2 -2
- package/src/components/sidebar/AudioRow.tsx +6 -6
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.tsx +5 -5
- package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
- package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
- package/src/components/sidebar/LeftSidebar.tsx +19 -5
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/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/hooks/useAppHotkeys.ts +21 -80
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- package/src/hooks/useLintModal.test.tsx +65 -0
- package/src/hooks/useLintModal.ts +3 -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/useTimelineAssetDropOps.test.tsx +41 -1
- package/src/hooks/useTimelineAssetDropOps.ts +52 -11
- package/src/index.ts +51 -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 +2 -1
- 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.tsx +1 -1
- 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/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/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 +243 -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 -1
- package/src/utils/studioUiPreferences.ts +7 -0
- 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/storyboard/AgentChatMessageButton.test.tsx +0 -47
- package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
- package/src/components/storyboard/FramePoster.test.tsx +0 -61
- package/src/components/storyboard/FramePoster.tsx +0 -86
- package/src/components/storyboard/StoryboardDirection.tsx +0 -45
- package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
- package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
- package/src/components/storyboard/StoryboardGrid.tsx +0 -55
- package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
- package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
- package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
- package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
- package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
- package/src/components/storyboard/StoryboardView.tsx +0 -169
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
- package/src/components/storyboard/frameComments.test.ts +0 -95
- package/src/components/storyboard/frameComments.ts +0 -115
- package/src/components/storyboard/frameStatus.ts +0 -36
- package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
- package/src/components/storyboard/storyboardReviewStage.ts +0 -45
- package/src/components/storyboard/useFrameComments.ts +0 -98
- package/src/contexts/ViewModeContext.tsx +0 -142
- package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
- package/src/hooks/useProjectSignaturePoll.ts +0 -68
- package/src/hooks/useStoryboard.ts +0 -99
|
@@ -9,7 +9,7 @@ import { BlockParamsPanel } from "./editor/BlockParamsPanel";
|
|
|
9
9
|
import { RenderQueuePanel } from "./renders/RenderQueuePanel";
|
|
10
10
|
import { SlideshowPanel } from "./panels/SlideshowPanel";
|
|
11
11
|
import { VariablesPanel } from "./panels/VariablesPanel";
|
|
12
|
-
import {
|
|
12
|
+
import { RightPanelTabs, type RightPanelTabDescriptor } from "./RightPanelTabs";
|
|
13
13
|
import type { RenderJob } from "./renders/useRenderQueue";
|
|
14
14
|
import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./editor/manualEditingAvailability";
|
|
15
15
|
import { useSlideshowPersist } from "../hooks/useSlideshowPersist";
|
|
@@ -339,6 +339,51 @@ export function StudioRightPanel({
|
|
|
339
339
|
|
|
340
340
|
const renderQueuePanel = <RenderQueuePanel />;
|
|
341
341
|
|
|
342
|
+
// Slideshow appears only for a slideshow composition, so the strip is built
|
|
343
|
+
// rather than written out: a tab that is not in this list is not reachable by
|
|
344
|
+
// an arrow key either.
|
|
345
|
+
const inspectorTabs: RightPanelTabDescriptor[] = [
|
|
346
|
+
{
|
|
347
|
+
id: "design",
|
|
348
|
+
label: "Design",
|
|
349
|
+
tooltip: "Element styles and properties",
|
|
350
|
+
active: designPaneOpen,
|
|
351
|
+
onSelect: () => handleInspectorPaneButtonClick("design"),
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "layers",
|
|
355
|
+
label: "Layers",
|
|
356
|
+
tooltip: "Composition layer stack",
|
|
357
|
+
active: layersPaneOpen,
|
|
358
|
+
onSelect: () => handleInspectorPaneButtonClick("layers"),
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "renders",
|
|
362
|
+
label: renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders",
|
|
363
|
+
tooltip: "Render queue and exports",
|
|
364
|
+
active: rightPanelTab === "renders",
|
|
365
|
+
onSelect: () => setRightPanelTab("renders"),
|
|
366
|
+
},
|
|
367
|
+
...(isSlideshowComposition
|
|
368
|
+
? [
|
|
369
|
+
{
|
|
370
|
+
id: "slideshow",
|
|
371
|
+
label: "Slideshow",
|
|
372
|
+
tooltip: "Slideshow branching editor",
|
|
373
|
+
active: rightPanelTab === "slideshow",
|
|
374
|
+
onSelect: () => setRightPanelTab("slideshow"),
|
|
375
|
+
},
|
|
376
|
+
]
|
|
377
|
+
: []),
|
|
378
|
+
{
|
|
379
|
+
id: "variables",
|
|
380
|
+
label: "Variables",
|
|
381
|
+
tooltip: "Template variables — declare, preview with values",
|
|
382
|
+
active: rightPanelTab === "variables",
|
|
383
|
+
onSelect: () => setRightPanelTab("variables"),
|
|
384
|
+
},
|
|
385
|
+
];
|
|
386
|
+
|
|
342
387
|
return (
|
|
343
388
|
<>
|
|
344
389
|
{/* Vertical resize divider: 3px visible seam, 13px hit zone via the inner div. */}
|
|
@@ -347,7 +392,7 @@ export function StudioRightPanel({
|
|
|
347
392
|
aria-label="Resize inspector panel"
|
|
348
393
|
aria-orientation="vertical"
|
|
349
394
|
tabIndex={0}
|
|
350
|
-
className="group relative w-[3px]
|
|
395
|
+
className="group relative w-[3px] shrink-0 cursor-col-resize outline-hidden focus-visible:bg-studio-accent/20"
|
|
351
396
|
style={{ touchAction: "none" }}
|
|
352
397
|
onPointerDown={(e) => handlePanelResizeStart("right", e)}
|
|
353
398
|
onPointerMove={handlePanelResizeMove}
|
|
@@ -364,52 +409,19 @@ export function StudioRightPanel({
|
|
|
364
409
|
{/* Asymmetric hit zone: 8px into the preview's p-2 gutter (the only dead
|
|
365
410
|
space), the 3px seam, 2px into the card. Stops short of the 24px WCAG
|
|
366
411
|
2.5.8 target because the next pixel each way is live. */}
|
|
367
|
-
<div className="absolute inset-y-0
|
|
412
|
+
<div className="absolute inset-y-0 left-[-8px] w-[13px]" />
|
|
368
413
|
{/* Visible hairline */}
|
|
369
414
|
<div className="absolute top-1/2 left-0 h-[52px] w-[3px] -translate-y-1/2 bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
|
|
370
415
|
</div>
|
|
371
416
|
<div
|
|
372
|
-
className="flex min-w-0
|
|
417
|
+
className="flex min-w-0 shrink-0 flex-col overflow-hidden rounded-lg border border-neutral-800 bg-neutral-950"
|
|
373
418
|
style={{ width: rightWidth }}
|
|
374
419
|
>
|
|
375
420
|
{captionEditMode ? (
|
|
376
421
|
<CaptionPropertyPanel iframeRef={previewIframeRef} />
|
|
377
422
|
) : (
|
|
378
423
|
<>
|
|
379
|
-
<
|
|
380
|
-
<PanelTabButton
|
|
381
|
-
label="Design"
|
|
382
|
-
tooltip="Element styles and properties"
|
|
383
|
-
active={designPaneOpen}
|
|
384
|
-
onClick={() => handleInspectorPaneButtonClick("design")}
|
|
385
|
-
/>
|
|
386
|
-
<PanelTabButton
|
|
387
|
-
label="Layers"
|
|
388
|
-
tooltip="Composition layer stack"
|
|
389
|
-
active={layersPaneOpen}
|
|
390
|
-
onClick={() => handleInspectorPaneButtonClick("layers")}
|
|
391
|
-
/>
|
|
392
|
-
<PanelTabButton
|
|
393
|
-
label={renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
|
|
394
|
-
tooltip="Render queue and exports"
|
|
395
|
-
active={rightPanelTab === "renders"}
|
|
396
|
-
onClick={() => setRightPanelTab("renders")}
|
|
397
|
-
/>
|
|
398
|
-
{isSlideshowComposition && (
|
|
399
|
-
<PanelTabButton
|
|
400
|
-
label="Slideshow"
|
|
401
|
-
tooltip="Slideshow branching editor"
|
|
402
|
-
active={rightPanelTab === "slideshow"}
|
|
403
|
-
onClick={() => setRightPanelTab("slideshow")}
|
|
404
|
-
/>
|
|
405
|
-
)}
|
|
406
|
-
<PanelTabButton
|
|
407
|
-
label="Variables"
|
|
408
|
-
tooltip="Template variables — declare, preview with values"
|
|
409
|
-
active={rightPanelTab === "variables"}
|
|
410
|
-
onClick={() => setRightPanelTab("variables")}
|
|
411
|
-
/>
|
|
412
|
-
</div>
|
|
424
|
+
<RightPanelTabs tabs={inspectorTabs} activateOnFocus={STUDIO_FLAT_INSPECTOR_ENABLED} />
|
|
413
425
|
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
414
426
|
{rightPanelTab === "block-params" && activeBlockParams ? (
|
|
415
427
|
<BlockParamsPanel
|
|
@@ -444,7 +456,7 @@ export function StudioRightPanel({
|
|
|
444
456
|
role="separator"
|
|
445
457
|
aria-label="Resize Layers and Design panes"
|
|
446
458
|
aria-orientation="horizontal"
|
|
447
|
-
className="group flex h-2
|
|
459
|
+
className="group flex h-2 shrink-0 cursor-row-resize items-center justify-center border-y border-neutral-800 bg-neutral-900"
|
|
448
460
|
style={{ touchAction: "none" }}
|
|
449
461
|
onPointerDown={handleInspectorSplitResizeStart}
|
|
450
462
|
onPointerMove={handleInspectorSplitResizeMove}
|
|
@@ -30,7 +30,7 @@ export function StudioToast({ message, tone, leaving, onDismiss }: StudioToastPr
|
|
|
30
30
|
}}
|
|
31
31
|
>
|
|
32
32
|
<span
|
|
33
|
-
className={`min-w-0 break-
|
|
33
|
+
className={`min-w-0 wrap-break-word leading-5 ${isError ? "text-red-200" : "text-neutral-200"}`}
|
|
34
34
|
>
|
|
35
35
|
{message}
|
|
36
36
|
</span>
|
|
@@ -38,7 +38,7 @@ export function StudioToast({ message, tone, leaving, onDismiss }: StudioToastPr
|
|
|
38
38
|
<button
|
|
39
39
|
type="button"
|
|
40
40
|
onClick={onDismiss}
|
|
41
|
-
className="flex h-5 w-5
|
|
41
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-neutral-300"
|
|
42
42
|
aria-label="Dismiss"
|
|
43
43
|
>
|
|
44
44
|
<svg
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
5
|
+
import { cleanupMounted, mountHost } from "./ui/mountHost.testHelpers";
|
|
6
|
+
import { isTypingTarget } from "../utils/typingTarget";
|
|
7
|
+
import { shouldIgnorePlaybackShortcutTarget } from "../player/lib/playbackShortcuts";
|
|
8
|
+
|
|
9
|
+
const editHistory = {
|
|
10
|
+
canUndo: false,
|
|
11
|
+
canRedo: false,
|
|
12
|
+
undoLabel: undefined as string | undefined,
|
|
13
|
+
redoLabel: undefined as string | undefined,
|
|
14
|
+
};
|
|
15
|
+
const handleUndo = vi.fn();
|
|
16
|
+
const handleRedo = vi.fn();
|
|
17
|
+
const trackStudioEvent = vi.fn();
|
|
18
|
+
|
|
19
|
+
vi.mock("../contexts/StudioContext", () => ({
|
|
20
|
+
useStudioShellContext: () => ({ editHistory, handleUndo, handleRedo }),
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent }));
|
|
23
|
+
|
|
24
|
+
const { TimelineHistoryButtons } = await import("./TimelineHistoryButtons");
|
|
25
|
+
const { TimelineToolbar } = await import("./TimelineToolbar");
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
Object.assign(editHistory, {
|
|
29
|
+
canUndo: false,
|
|
30
|
+
canRedo: false,
|
|
31
|
+
undoLabel: undefined,
|
|
32
|
+
redoLabel: undefined,
|
|
33
|
+
});
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(cleanupMounted);
|
|
38
|
+
|
|
39
|
+
const mount = mountHost;
|
|
40
|
+
|
|
41
|
+
function button(host: HTMLElement, label: string): HTMLButtonElement {
|
|
42
|
+
const el = host.querySelector<HTMLButtonElement>(`button[aria-label="${label}"]`);
|
|
43
|
+
if (!el) throw new Error(`not rendered: ${label}`);
|
|
44
|
+
return el;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function click(el: HTMLElement): void {
|
|
48
|
+
act(() => {
|
|
49
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it("calls handleUndo once and tracks the event when Undo is enabled", () => {
|
|
54
|
+
editHistory.canUndo = true;
|
|
55
|
+
const host = mount(<TimelineHistoryButtons />);
|
|
56
|
+
|
|
57
|
+
click(button(host, "Undo"));
|
|
58
|
+
|
|
59
|
+
expect(handleUndo).toHaveBeenCalledTimes(1);
|
|
60
|
+
expect(handleRedo).not.toHaveBeenCalled();
|
|
61
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("toolbar_action", { action: "undo" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("calls handleRedo once and tracks the event when Redo is enabled", () => {
|
|
65
|
+
editHistory.canRedo = true;
|
|
66
|
+
const host = mount(<TimelineHistoryButtons />);
|
|
67
|
+
|
|
68
|
+
click(button(host, "Redo"));
|
|
69
|
+
|
|
70
|
+
expect(handleRedo).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("toolbar_action", { action: "redo" });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("disables both buttons and ignores clicks on an empty history", () => {
|
|
75
|
+
const host = mount(<TimelineHistoryButtons />);
|
|
76
|
+
|
|
77
|
+
for (const label of ["Undo", "Redo"]) {
|
|
78
|
+
const el = button(host, label);
|
|
79
|
+
expect(el.disabled).toBe(true);
|
|
80
|
+
expect(el.className).toContain("cursor-not-allowed");
|
|
81
|
+
click(el);
|
|
82
|
+
}
|
|
83
|
+
expect(handleUndo).not.toHaveBeenCalled();
|
|
84
|
+
expect(handleRedo).not.toHaveBeenCalled();
|
|
85
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("leaves enabled buttons without the disabled attribute", () => {
|
|
89
|
+
editHistory.canUndo = true;
|
|
90
|
+
const el = button(mount(<TimelineHistoryButtons />), "Undo");
|
|
91
|
+
|
|
92
|
+
expect(el.disabled).toBe(false);
|
|
93
|
+
expect(el.className).not.toContain("cursor-not-allowed");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("orders the toolbar Undo, Redo, Select, Razor", () => {
|
|
97
|
+
const host = mount(<TimelineToolbar />);
|
|
98
|
+
const labels = Array.from(host.querySelectorAll("button"))
|
|
99
|
+
.map((b) => b.getAttribute("aria-label"))
|
|
100
|
+
.filter((l) => l !== null)
|
|
101
|
+
.slice(0, 4);
|
|
102
|
+
|
|
103
|
+
expect(labels).toEqual(["Undo", "Redo", "Selection tool", "Razor tool"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("classifies Undo and Redo for the hotkey filters at their new location (KTD13)", () => {
|
|
107
|
+
// The header's own version of this check dropped Undo/Redo when they moved
|
|
108
|
+
// here; a <button> is never a typing target and is always claimed by the
|
|
109
|
+
// playback filter, same as every other toolbar tool.
|
|
110
|
+
const host = mount(<TimelineHistoryButtons />);
|
|
111
|
+
|
|
112
|
+
for (const label of ["Undo", "Redo"]) {
|
|
113
|
+
const el = button(host, label);
|
|
114
|
+
expect(isTypingTarget(el), label).toBe(false);
|
|
115
|
+
expect(shouldIgnorePlaybackShortcutTarget(el), label).toBe(true);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
2
|
+
import { RotateCcw, RotateCw } from "../icons/SystemIcons";
|
|
3
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
4
|
+
import { historyTooltipLabel } from "../utils/studioHelpers";
|
|
5
|
+
import { flatDisabled, flatIdle } from "./timelineToolbarStyles";
|
|
6
|
+
import { Tooltip } from "./ui";
|
|
7
|
+
|
|
8
|
+
interface HistoryButtonProps {
|
|
9
|
+
action: "undo" | "redo";
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
label: string | undefined;
|
|
12
|
+
onClick: () => Promise<void> | void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function HistoryButton({ action, enabled, label, onClick }: HistoryButtonProps) {
|
|
16
|
+
const Icon = action === "undo" ? RotateCcw : RotateCw;
|
|
17
|
+
return (
|
|
18
|
+
<Tooltip label={historyTooltipLabel(action, label)}>
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
aria-label={action === "undo" ? "Undo" : "Redo"}
|
|
22
|
+
disabled={!enabled}
|
|
23
|
+
className={enabled ? flatIdle : flatDisabled}
|
|
24
|
+
onClick={() => {
|
|
25
|
+
trackStudioEvent("toolbar_action", { action });
|
|
26
|
+
void onClick();
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<Icon size={16} />
|
|
30
|
+
</button>
|
|
31
|
+
</Tooltip>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Undo and Redo, on the one edit-history path the shell already owns. */
|
|
36
|
+
export function TimelineHistoryButtons() {
|
|
37
|
+
const { editHistory, handleUndo, handleRedo } = useStudioShellContext();
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<HistoryButton
|
|
41
|
+
action="undo"
|
|
42
|
+
enabled={editHistory.canUndo}
|
|
43
|
+
label={editHistory.undoLabel}
|
|
44
|
+
onClick={handleUndo}
|
|
45
|
+
/>
|
|
46
|
+
<HistoryButton
|
|
47
|
+
action="redo"
|
|
48
|
+
enabled={editHistory.canRedo}
|
|
49
|
+
label={editHistory.redoLabel}
|
|
50
|
+
onClick={handleRedo}
|
|
51
|
+
/>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -8,6 +8,14 @@ import { usePlayerStore } from "../player/store/playerStore";
|
|
|
8
8
|
import { makeSelection } from "../hooks/domSelectionTestHarness";
|
|
9
9
|
import { TimelineToolbar } from "./TimelineToolbar";
|
|
10
10
|
|
|
11
|
+
vi.mock("../contexts/StudioContext", () => ({
|
|
12
|
+
useStudioShellContext: () => ({
|
|
13
|
+
editHistory: { canUndo: false, canRedo: false },
|
|
14
|
+
handleUndo: vi.fn(),
|
|
15
|
+
handleRedo: vi.fn(),
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
11
19
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
20
|
|
|
13
21
|
afterEach(() => {
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
import { useTimelineZoom } from "../player/components/useTimelineZoom";
|
|
23
23
|
import { usePlayerStore, type TimelineElement } from "../player";
|
|
24
24
|
import { Tooltip } from "./ui";
|
|
25
|
+
import { flatActive, flatBtn, flatDisabled, flatIdle } from "./timelineToolbarStyles";
|
|
26
|
+
import { TimelineHistoryButtons } from "./TimelineHistoryButtons";
|
|
25
27
|
import { Scissors } from "../icons/SystemIcons";
|
|
26
28
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
27
29
|
import type { DomEditSelection } from "./editor/domEditingTypes";
|
|
@@ -186,19 +188,13 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
|
|
186
188
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
187
189
|
}, []);
|
|
188
190
|
|
|
189
|
-
// CapCut-flat icon buttons: no per-button border/box chrome — a transparent
|
|
190
|
-
// 28px hit area with a subtle rounded hover wash, consistent 16px glyphs.
|
|
191
|
-
const flatBtn = "flex h-7 w-7 items-center justify-center rounded-md transition-colors";
|
|
192
|
-
const flatIdle = `${flatBtn} text-neutral-400 hover:bg-white/[0.06] hover:text-neutral-200 active:scale-[0.98]`;
|
|
193
|
-
const flatActive = `${flatBtn} bg-white/[0.08] text-neutral-100 active:scale-[0.98]`;
|
|
194
|
-
const flatDisabled = `${flatBtn} text-neutral-700 cursor-not-allowed`;
|
|
195
|
-
|
|
196
191
|
return (
|
|
197
192
|
// The "TIMELINE" label is dropped for CapCut-like density — the pane's
|
|
198
193
|
// position (tracks right below) makes it self-evident.
|
|
199
194
|
<div className="border-b border-neutral-800/60">
|
|
200
195
|
<div className="flex items-center justify-between px-2 py-0.5">
|
|
201
196
|
<div className="flex items-center gap-0.5">
|
|
197
|
+
<TimelineHistoryButtons />
|
|
202
198
|
<Tooltip label="Selection tool (V)">
|
|
203
199
|
<button
|
|
204
200
|
type="button"
|
|
@@ -297,7 +293,7 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
|
|
297
293
|
className={
|
|
298
294
|
!onToggleKeyframe
|
|
299
295
|
? flatDisabled
|
|
300
|
-
: `${flatBtn} active:scale-[0.98] hover:bg-white/
|
|
296
|
+
: `${flatBtn} active:scale-[0.98] hover:bg-white/6 ${
|
|
301
297
|
keyframeState === "active"
|
|
302
298
|
? "text-studio-accent"
|
|
303
299
|
: keyframeState === "inactive"
|
|
@@ -332,7 +328,7 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
|
|
332
328
|
onClick={() => setAutoKeyframeEnabled(!autoKeyframeEnabled)}
|
|
333
329
|
aria-label="Auto-record manual edits as keyframes"
|
|
334
330
|
aria-pressed={autoKeyframeEnabled}
|
|
335
|
-
className={`${flatBtn} active:scale-[0.98] hover:bg-white/
|
|
331
|
+
className={`${flatBtn} active:scale-[0.98] hover:bg-white/6 ${
|
|
336
332
|
autoKeyframeEnabled
|
|
337
333
|
? "text-red-400 hover:text-red-300"
|
|
338
334
|
: "text-neutral-600 hover:text-neutral-400"
|
|
@@ -429,7 +425,7 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
|
|
429
425
|
}}
|
|
430
426
|
className={
|
|
431
427
|
canAdd
|
|
432
|
-
? `${flatBtn} text-neutral-400 hover:bg-white/
|
|
428
|
+
? `${flatBtn} text-neutral-400 hover:bg-white/6 hover:text-[#22c55e] active:scale-[0.98]`
|
|
433
429
|
: flatDisabled
|
|
434
430
|
}
|
|
435
431
|
>
|
|
@@ -467,7 +463,7 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
|
|
467
463
|
className={`h-7 px-2 rounded-md text-[11px] font-medium transition-colors ${
|
|
468
464
|
thumbnailsVisible
|
|
469
465
|
? "bg-studio-accent/10 text-studio-accent"
|
|
470
|
-
: "text-neutral-400 hover:bg-white/
|
|
466
|
+
: "text-neutral-400 hover:bg-white/6 hover:text-neutral-200"
|
|
471
467
|
}`}
|
|
472
468
|
>
|
|
473
469
|
<Image size={16} aria-hidden="true" />
|
|
@@ -480,7 +476,7 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
|
|
480
476
|
className={`h-7 px-2 rounded-md text-[11px] font-medium transition-colors ${
|
|
481
477
|
zoomMode === "fit"
|
|
482
478
|
? "bg-studio-accent/10 text-studio-accent"
|
|
483
|
-
: "text-neutral-400 hover:bg-white/
|
|
479
|
+
: "text-neutral-400 hover:bg-white/6 hover:text-neutral-200"
|
|
484
480
|
}`}
|
|
485
481
|
>
|
|
486
482
|
Fit
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act, type ComponentProps } from "react";
|
|
4
|
+
import type { DockviewApi } from "dockview-react";
|
|
5
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import * as dockLayout from "./dockLayout";
|
|
8
|
+
import { Dock } from "./Dock";
|
|
9
|
+
import { parseDockLayout } from "./dockLayoutSchema";
|
|
10
|
+
import { useDockLayoutStore } from "./dockLayoutStore";
|
|
11
|
+
import { PANEL_IDS } from "./panelRegistry";
|
|
12
|
+
import { readStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
13
|
+
|
|
14
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
|
|
16
|
+
let dockApi: DockviewApi | null = null;
|
|
17
|
+
vi.mock("dockview-react", async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal<typeof import("dockview-react")>();
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
DockviewReact: (props: ComponentProps<typeof actual.DockviewReact>) =>
|
|
22
|
+
React.createElement(actual.DockviewReact, {
|
|
23
|
+
...props,
|
|
24
|
+
onReady: (event) => {
|
|
25
|
+
dockApi = event.api;
|
|
26
|
+
props.onReady(event);
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const liveObservers = new Set<{ callback: () => void; target?: Element }>();
|
|
33
|
+
class ResizeObserverStub {
|
|
34
|
+
private readonly entry: { callback: () => void; target?: Element };
|
|
35
|
+
constructor(callback: () => void) {
|
|
36
|
+
this.entry = { callback };
|
|
37
|
+
}
|
|
38
|
+
observe(target: Element) {
|
|
39
|
+
this.entry.target = target;
|
|
40
|
+
liveObservers.add(this.entry);
|
|
41
|
+
}
|
|
42
|
+
unobserve() {}
|
|
43
|
+
disconnect() {
|
|
44
|
+
liveObservers.delete(this.entry);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = ResizeObserverStub;
|
|
48
|
+
|
|
49
|
+
vi.mock("./dockLayout", async (importOriginal) => {
|
|
50
|
+
const actual = await importOriginal<typeof import("./dockLayout")>();
|
|
51
|
+
return { ...actual, applySideMinimums: vi.fn(actual.applySideMinimums) };
|
|
52
|
+
});
|
|
53
|
+
const applySideMinimums = vi.mocked(dockLayout.applySideMinimums);
|
|
54
|
+
|
|
55
|
+
let root: Root | null = null;
|
|
56
|
+
|
|
57
|
+
function mount(projectId: string | null) {
|
|
58
|
+
const host = document.createElement("div");
|
|
59
|
+
document.body.append(host);
|
|
60
|
+
root = createRoot(host);
|
|
61
|
+
act(() => {
|
|
62
|
+
root?.render(
|
|
63
|
+
<Dock.Root projectId={projectId}>
|
|
64
|
+
{PANEL_IDS.map((id) => (
|
|
65
|
+
<Dock.Panel key={id} id={id}>
|
|
66
|
+
<div data-testid={`content-${id}`}>{id}</div>
|
|
67
|
+
</Dock.Panel>
|
|
68
|
+
))}
|
|
69
|
+
</Dock.Root>,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
return host;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The persisted views of the group holding `id`, after the debounced write lands. */
|
|
76
|
+
function persistedGroupOf(id: string): string | undefined {
|
|
77
|
+
act(() => {
|
|
78
|
+
vi.advanceTimersByTime(1000);
|
|
79
|
+
});
|
|
80
|
+
const grid = JSON.stringify(readStudioUiPreferences(undefined, "p1").dockLayout?.grid);
|
|
81
|
+
const groups = [...grid.matchAll(/"views":\[([^\]]*)\]/g)].map((m) => m[1]);
|
|
82
|
+
return groups.find((views) => views.includes(`"${id}"`));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
localStorage.clear();
|
|
87
|
+
vi.useFakeTimers();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
act(() => root?.unmount());
|
|
92
|
+
root = null;
|
|
93
|
+
document.body.innerHTML = "";
|
|
94
|
+
vi.useRealTimers();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// The default Edit layout tabs [compositions|assets|code|catalog] into one
|
|
98
|
+
// group and [design|layers|renders|variables] into another; dockview shows
|
|
99
|
+
// only the active tab's content per group. `slideshow` is never part of the
|
|
100
|
+
// default build — StudioRightPanels opens it itself when the file is one.
|
|
101
|
+
const DEFAULT_OPEN = PANEL_IDS.filter((id) => id !== "slideshow");
|
|
102
|
+
const DEFAULT_VISIBLE = ["preview", "timeline", "compositions", "design"];
|
|
103
|
+
|
|
104
|
+
describe("Dock on React 19", () => {
|
|
105
|
+
it("mounts the default layout's ten panels, showing only each group's active tab", () => {
|
|
106
|
+
const host = mount("p1");
|
|
107
|
+
for (const id of DEFAULT_VISIBLE) {
|
|
108
|
+
expect(host.querySelector(`[data-testid="content-${id}"]`)).not.toBeNull();
|
|
109
|
+
}
|
|
110
|
+
for (const id of DEFAULT_OPEN.filter((id) => !DEFAULT_VISIBLE.includes(id))) {
|
|
111
|
+
expect(host.querySelector(`[data-testid="content-${id}"]`)).toBeNull();
|
|
112
|
+
}
|
|
113
|
+
expect(host.querySelector('[data-testid="content-slideshow"]')).toBeNull();
|
|
114
|
+
expect(useDockLayoutStore.getState().openPanels).toEqual(new Set(DEFAULT_OPEN));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("persists a layout change per project and reads it back through the schema", () => {
|
|
118
|
+
mount("p1");
|
|
119
|
+
act(() => useDockLayoutStore.getState().closePanel("renders"));
|
|
120
|
+
act(() => {
|
|
121
|
+
vi.advanceTimersByTime(400);
|
|
122
|
+
});
|
|
123
|
+
const stored = readStudioUiPreferences(undefined, "p1").dockLayout;
|
|
124
|
+
const expected = DEFAULT_OPEN.filter((id) => id !== "renders");
|
|
125
|
+
expect(Object.keys(stored?.panels ?? {}).sort()).toEqual([...expected].sort());
|
|
126
|
+
expect(readStudioUiPreferences(undefined, "p2").dockLayout).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("closes a panel and reopens it from the store, becoming its group's visible tab", async () => {
|
|
130
|
+
const host = mount("p1");
|
|
131
|
+
act(() => useDockLayoutStore.getState().closePanel("renders"));
|
|
132
|
+
expect(useDockLayoutStore.getState().openPanels.has("renders")).toBe(false);
|
|
133
|
+
expect(host.querySelector('[data-testid="content-renders"]')).toBeNull();
|
|
134
|
+
// dockview's own panel-active event dispatch resolves on a microtask, one
|
|
135
|
+
// tick after the synchronous store call returns; `act(async ...)` is what
|
|
136
|
+
// actually waits for it instead of asserting against pre-flush DOM.
|
|
137
|
+
await act(async () => {
|
|
138
|
+
useDockLayoutStore.getState().togglePanel("renders");
|
|
139
|
+
await Promise.resolve();
|
|
140
|
+
});
|
|
141
|
+
expect(useDockLayoutStore.getState().openPanels.has("renders")).toBe(true);
|
|
142
|
+
expect(host.querySelector('[data-testid="content-renders"]')).not.toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("reopens a closed panel as a tab of its zone's group, not a new group", () => {
|
|
146
|
+
mount("p1");
|
|
147
|
+
act(() => useDockLayoutStore.getState().closePanel("compositions"));
|
|
148
|
+
act(() => useDockLayoutStore.getState().togglePanel("compositions"));
|
|
149
|
+
expect(persistedGroupOf("compositions")).toContain('"assets"');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("reopens a side panel next to the preview when its whole column was closed", () => {
|
|
153
|
+
mount("p1");
|
|
154
|
+
const { closePanel, togglePanel } = useDockLayoutStore.getState();
|
|
155
|
+
for (const id of ["compositions", "assets", "code", "catalog"] as const)
|
|
156
|
+
act(() => closePanel(id));
|
|
157
|
+
act(() => togglePanel("compositions"));
|
|
158
|
+
expect(useDockLayoutStore.getState().openPanels.has("compositions")).toBe(true);
|
|
159
|
+
expect(useDockLayoutStore.getState().visiblePanels.has("compositions")).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("reopens the timeline as its own group, never as a tab of the preview", () => {
|
|
163
|
+
mount("p1");
|
|
164
|
+
act(() => useDockLayoutStore.getState().closePanel("timeline"));
|
|
165
|
+
act(() => useDockLayoutStore.getState().togglePanel("timeline"));
|
|
166
|
+
expect(persistedGroupOf("timeline")).not.toContain('"preview"');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("restores the stored layout on the next mount instead of rebuilding the default", () => {
|
|
170
|
+
mount("p1");
|
|
171
|
+
act(() => useDockLayoutStore.getState().closePanel("renders"));
|
|
172
|
+
act(() => {
|
|
173
|
+
vi.advanceTimersByTime(1000);
|
|
174
|
+
});
|
|
175
|
+
act(() => root?.unmount());
|
|
176
|
+
root = null;
|
|
177
|
+
document.body.innerHTML = "";
|
|
178
|
+
|
|
179
|
+
mount("p1");
|
|
180
|
+
expect(useDockLayoutStore.getState().openPanels.has("renders")).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("falls back to the default layout when the stored one names an unknown panel", () => {
|
|
184
|
+
localStorage.setItem(
|
|
185
|
+
"hf-studio-ui-preferences:p1",
|
|
186
|
+
JSON.stringify({ dockLayout: { grid: {}, panels: { nope: {} } } }),
|
|
187
|
+
);
|
|
188
|
+
const host = mount("p1");
|
|
189
|
+
expect(host.querySelector('[data-testid="content-preview"]')).not.toBeNull();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("Dock wiring", () => {
|
|
194
|
+
it("re-applies the side minimums when a panel is dragged to another group", () => {
|
|
195
|
+
mount(null);
|
|
196
|
+
applySideMinimums.mockClear();
|
|
197
|
+
const preview = dockApi?.getPanel("preview");
|
|
198
|
+
const design = dockApi?.getPanel("design");
|
|
199
|
+
if (!preview || !design) throw new Error("default layout is missing panels");
|
|
200
|
+
act(() => design.api.moveTo({ group: preview.group }));
|
|
201
|
+
expect(applySideMinimums).toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("makes the sashes keyboard-focusable separators", () => {
|
|
205
|
+
const host = mount(null);
|
|
206
|
+
const sashes = host.querySelectorAll<HTMLElement>('.dv-sash[role="separator"]');
|
|
207
|
+
expect(sashes.length).toBeGreaterThan(0);
|
|
208
|
+
for (const sash of sashes) expect(sash.tabIndex).toBe(0);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("re-fits the side columns to the window width when it resizes", () => {
|
|
212
|
+
mount(null);
|
|
213
|
+
const innerWidth = window.innerWidth;
|
|
214
|
+
Object.defineProperty(window, "innerWidth", { value: 560, configurable: true });
|
|
215
|
+
try {
|
|
216
|
+
applySideMinimums.mockClear();
|
|
217
|
+
const dockObservers = [...liveObservers].filter(({ target }) =>
|
|
218
|
+
target?.classList.contains("hf-dock"),
|
|
219
|
+
);
|
|
220
|
+
act(() => dockObservers.forEach(({ callback }) => callback()));
|
|
221
|
+
expect(applySideMinimums).toHaveBeenCalledWith(expect.anything(), 560);
|
|
222
|
+
} finally {
|
|
223
|
+
Object.defineProperty(window, "innerWidth", { value: innerWidth, configurable: true });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("parseDockLayout", () => {
|
|
229
|
+
it("rejects shapes that are not a dock layout", () => {
|
|
230
|
+
expect(parseDockLayout(null)).toBeNull();
|
|
231
|
+
expect(parseDockLayout({ grid: {}, panels: {} })).toBeNull();
|
|
232
|
+
expect(
|
|
233
|
+
parseDockLayout({
|
|
234
|
+
grid: {
|
|
235
|
+
width: 1,
|
|
236
|
+
height: 1,
|
|
237
|
+
orientation: "HORIZONTAL",
|
|
238
|
+
root: { type: "leaf", data: { views: ["ghost"] } },
|
|
239
|
+
},
|
|
240
|
+
panels: { ghost: { id: "ghost", contentComponent: "panel" } },
|
|
241
|
+
}),
|
|
242
|
+
).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const placed = (views: string[], panelIds: string[]) => ({
|
|
246
|
+
grid: {
|
|
247
|
+
width: 1,
|
|
248
|
+
height: 1,
|
|
249
|
+
orientation: "HORIZONTAL",
|
|
250
|
+
root: { type: "leaf", data: { views } },
|
|
251
|
+
},
|
|
252
|
+
panels: Object.fromEntries(panelIds.map((id) => [id, { id, contentComponent: "panel" }])),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("rejects a view that has no panel entry and a panel that no view places", () => {
|
|
256
|
+
expect(parseDockLayout(placed(["preview", "design"], ["preview"]))).toBeNull();
|
|
257
|
+
expect(parseDockLayout(placed(["preview"], ["preview", "design"]))).toBeNull();
|
|
258
|
+
expect(parseDockLayout(placed(["preview", "design"], ["preview", "design"]))).not.toBeNull();
|
|
259
|
+
});
|
|
260
|
+
});
|