@hyperframes/studio 0.8.49 → 0.8.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-CqON2Rzo.js → hyperframes-player-Cn_PpHqP.js} +3 -3
- package/dist/assets/index-C05SNrvY.js +360 -0
- package/dist/assets/{index-mIgSsKqs.js → index-CPS3Cvj8.js} +1 -1
- package/dist/assets/{index-BGovDksE.js → index-Clrb11lj.js} +1 -1
- package/dist/assets/index-DUR9qzQM.css +1 -0
- package/dist/index.d.ts +29 -16
- package/dist/index.html +2 -2
- package/dist/index.js +7283 -8606
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +11 -12
- package/src/App.tsx +132 -143
- package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
- package/src/captions/components/CaptionOverlayUtils.ts +2 -3
- package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
- package/src/captions/components/CaptionTimeline.tsx +1 -1
- package/src/captions/components/shared.tsx +2 -2
- package/src/components/AskAgentModal.tsx +5 -5
- package/src/components/EditorShell.selectionSync.test.tsx +4 -0
- package/src/components/EditorShell.tsx +28 -7
- package/src/components/ExternalFileConflictBanner.tsx +16 -16
- package/src/components/LintModal.tsx +6 -16
- package/src/components/MediaPreview.tsx +2 -2
- package/src/components/SaveQueuePausedBanner.tsx +2 -2
- package/src/components/StudioHeader.tsx +2 -62
- package/src/components/StudioLeftSidebar.tsx +7 -4
- package/src/components/StudioOverlays.tsx +2 -6
- package/src/components/StudioRightPanel.tsx +4 -4
- package/src/components/StudioToast.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +7 -7
- package/src/components/dock/Dock.test.tsx +192 -0
- package/src/components/dock/Dock.tsx +264 -0
- package/src/components/dock/dock.css +23 -0
- package/src/components/dock/dockLayout.ts +58 -0
- package/src/components/dock/dockLayoutSchema.ts +66 -0
- package/src/components/dock/dockLayoutStore.ts +91 -0
- package/src/components/dock/panelRegistry.ts +67 -0
- package/src/components/editor/AnimationCard.tsx +4 -4
- package/src/components/editor/AnimationCardParts.tsx +6 -6
- package/src/components/editor/BlockParamsPanel.tsx +4 -4
- package/src/components/editor/BorderRadiusEditor.tsx +1 -1
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +1 -1
- package/src/components/editor/EaseParamFields.tsx +5 -5
- package/src/components/editor/FileTree.tsx +4 -4
- package/src/components/editor/FileTreeIcons.tsx +1 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -13
- package/src/components/editor/InlineTextToolbar.tsx +1 -1
- package/src/components/editor/InspectorHeaderActions.tsx +3 -3
- package/src/components/editor/KeyframeDiamond.tsx +1 -1
- package/src/components/editor/KeyframeEaseList.tsx +1 -1
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/MarqueeOverlay.tsx +1 -1
- package/src/components/editor/OffCanvasIndicators.tsx +1 -1
- package/src/components/editor/PreviewGuides.test.tsx +88 -0
- package/src/components/editor/PreviewGuides.tsx +125 -0
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.test.tsx +21 -0
- package/src/components/editor/SnapToolbar.tsx +27 -3
- package/src/components/editor/TimelineFxPopover.tsx +1 -1
- package/src/components/editor/TopologyLens.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +2 -2
- package/src/components/editor/audioFxRevealTarget.ts +1 -0
- package/src/components/editor/domEditing.test.ts +1 -1
- package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
- package/src/components/editor/domEditingAgentPrompt.ts +9 -3
- package/src/components/editor/previewGuidesStore.ts +26 -0
- package/src/components/editor/propertyPanelColor.tsx +11 -11
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
- package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
- package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +1 -1
- package/src/components/editor/propertyPanelFill.tsx +4 -4
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
- package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
- package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +5 -5
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +6 -6
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
- package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
- package/src/components/editor/propertyPanelSections.tsx +8 -8
- package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
- package/src/components/feedback/feedbackTrigger.ts +0 -1
- package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
- package/src/components/nle/NLEPreview.test.ts +13 -0
- package/src/components/nle/NLEPreview.tsx +12 -7
- package/src/components/nle/PreviewOverlays.tsx +6 -6
- package/src/components/nle/PreviewPane.test.tsx +52 -0
- package/src/components/nle/PreviewPane.tsx +4 -2
- package/src/components/nle/TimelinePane.tsx +17 -3
- package/src/components/nle/TimelineResizeDivider.tsx +2 -2
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -3
- package/src/components/panels/SlideshowPanel.tsx +2 -2
- package/src/components/panels/SlideshowSubPanels.tsx +20 -17
- package/src/components/panels/VariablesBindElement.tsx +2 -2
- package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
- package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +3 -3
- package/src/components/panels/VariablesValueControls.tsx +2 -2
- package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
- package/src/components/renders/RenderQueue.test.tsx +7 -0
- package/src/components/renders/RenderQueue.tsx +6 -6
- package/src/components/renders/RenderQueueItem.tsx +6 -6
- package/src/components/sidebar/AssetCard.tsx +8 -8
- package/src/components/sidebar/AssetContextMenu.tsx +8 -8
- package/src/components/sidebar/AssetsTab.tsx +2 -2
- package/src/components/sidebar/AudioRow.tsx +6 -6
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.tsx +5 -5
- package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
- package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
- package/src/components/sidebar/LeftSidebar.tsx +19 -5
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/ui/Button.test.tsx +60 -0
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/SearchInput.tsx +2 -2
- package/src/components/ui/Tooltip.tsx +1 -1
- package/src/hooks/appHotkeysDispatch.ts +11 -0
- package/src/hooks/timelineAudioGroupCreate.ts +2 -2
- package/src/hooks/timelineTrackVisibility.ts +8 -11
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
- package/src/hooks/useAppHotkeys.ts +4 -1
- package/src/hooks/useAskAgentModal.ts +2 -0
- package/src/hooks/useClipboard.test.ts +234 -0
- package/src/hooks/useClipboard.ts +264 -70
- package/src/hooks/useGroupCommits.test.ts +0 -1
- package/src/hooks/useLintModal.test.tsx +65 -0
- package/src/hooks/useLintModal.ts +3 -0
- package/src/hooks/useRazorSplit.history.test.tsx +0 -1
- package/src/hooks/useRazorSplit.test.ts +0 -2
- package/src/hooks/useRazorSplit.test.tsx +0 -1
- package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
- package/src/hooks/useSdkSession.ts +43 -10
- package/src/hooks/useStudioContextValue.ts +5 -19
- package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
- package/src/hooks/useTimelineAssetDropOps.ts +52 -11
- package/src/hooks/useTimelineEditing.test.tsx +0 -1
- package/src/main.tsx +9 -3
- package/src/player/components/AudioWaveform.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +2 -2
- package/src/player/components/ClipContextMenu.tsx +90 -10
- package/src/player/components/CompositionThumbnail.test.ts +0 -3
- package/src/player/components/CompositionThumbnail.tsx +3 -4
- package/src/player/components/EditModal.tsx +1 -1
- package/src/player/components/ImageThumbnail.tsx +1 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
- package/src/player/components/LayerDisclosureRow.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +1 -1
- package/src/player/components/ShortcutsPanel.tsx +9 -9
- package/src/player/components/SpeedMenu.tsx +3 -5
- package/src/player/components/Timeline.tsx +16 -15
- package/src/player/components/TimelineCanvas.tsx +45 -8
- package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
- package/src/player/components/TimelineEmptyState.tsx +5 -5
- package/src/player/components/TimelineFxButton.tsx +6 -6
- package/src/player/components/TimelineGestureOverlay.test.tsx +35 -1
- package/src/player/components/TimelineGestureOverlay.tsx +1 -1
- package/src/player/components/TimelineGroupHeader.tsx +5 -5
- package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
- package/src/player/components/TimelineGroupRow.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +54 -1
- package/src/player/components/TimelineLanes.tsx +2 -5
- package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
- package/src/player/components/TimelineOverlays.tsx +12 -0
- package/src/player/components/TimelineRuler.tsx +1 -1
- package/src/player/components/TimelineShortcutHint.tsx +1 -1
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
- package/src/player/components/TimelineTypes.ts +6 -1
- package/src/player/components/TrackGapContextMenu.tsx +1 -1
- package/src/player/components/VideoThumbnail.tsx +4 -2
- package/src/player/components/VolumeControl.tsx +2 -2
- package/src/player/components/timelineAuthoredMoveTarget.ts +1 -2
- package/src/player/components/timelineCallbacks.ts +8 -8
- package/src/player/components/timelineClipChildren.test.ts +2 -2
- package/src/player/components/timelineClipChildren.tsx +2 -1
- package/src/player/components/timelineClipDragCommit.ts +2 -2
- package/src/player/components/timelineClipDragPreview.ts +51 -27
- package/src/player/components/timelineClipDragTypes.ts +3 -0
- package/src/player/components/timelineDragDrop.test.tsx +92 -1
- package/src/player/components/timelineDragDrop.ts +82 -33
- package/src/player/components/timelineEditCapabilities.ts +1 -2
- package/src/player/components/timelineEditing.test.ts +0 -16
- package/src/player/components/timelineGroupEditing.test.ts +1 -3
- package/src/player/components/timelineGroupEditing.ts +0 -1
- package/src/player/components/timelineLaneProps.ts +3 -0
- package/src/player/components/timelineSnapping.test.ts +27 -0
- package/src/player/components/timelineSnapping.ts +14 -1
- package/src/player/components/timelineTrackDisplay.ts +1 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +0 -53
- package/src/player/components/trackHeaderLabelRows.tsx +4 -4
- package/src/player/components/useTimelineClipContextMenu.ts +32 -0
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
- package/src/player/components/useTimelineClipDrag.ts +30 -3
- package/src/player/components/useTimelineTrackDerivations.ts +1 -1
- package/src/player/hooks/timelineDomBinding.test.ts +0 -3
- package/src/player/hooks/timelineSyncHydration.test.ts +18 -0
- package/src/player/hooks/timelineSyncHydration.ts +37 -18
- package/src/player/hooks/useTimelinePlayer.test.ts +3 -20
- package/src/player/hooks/useTimelineRowElements.test.ts +28 -0
- package/src/player/hooks/useTimelineRowElements.ts +17 -0
- package/src/player/hooks/useTimelineSyncCallbacks.emptyManifest.test.tsx +41 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -10
- package/src/player/lib/describeClips.test.ts +95 -0
- package/src/player/lib/describeClips.ts +52 -0
- package/src/player/lib/timelineDOM.test.ts +0 -80
- package/src/player/lib/timelineDOM.ts +1 -61
- package/src/player/lib/timelineElementHelpers.ts +2 -53
- package/src/player/store/playerStore.ts +5 -0
- package/src/player/store/timelineElement.ts +1 -3
- package/src/shims/canvasBrowserStub.js +50 -0
- package/src/styles/iconTokens.ts +20 -0
- package/src/styles/palette-reset.css +6 -0
- package/src/styles/studio.css +24 -21
- package/src/styles/tailwind-preset.shared.js +3 -0
- package/src/styles/theme.css +213 -0
- package/src/styles/theme.test.ts +196 -0
- package/src/utils/clipboardPayload.test.ts +42 -2
- package/src/utils/clipboardPayload.ts +57 -32
- package/src/utils/previewSafeMargins.test.ts +34 -0
- package/src/utils/previewSafeMargins.ts +10 -0
- package/src/utils/razorSplitTransaction.test.ts +0 -1
- package/src/utils/sdkCanvasComposition.test.ts +77 -0
- package/src/utils/studioUiPreferences.test.ts +43 -0
- package/src/utils/studioUiPreferences.ts +34 -6
- package/src/utils/timelineDropTrackInsert.test.ts +134 -0
- package/src/utils/timelineDropTrackInsert.ts +83 -0
- package/src/utils/timelineElementSplit.test.ts +1 -2
- package/src/utils/timelineElementSplit.ts +0 -1
- package/src/webmcp/tools/lookTools.test.ts +40 -0
- package/src/webmcp/tools/lookTools.ts +32 -1
- package/dist/assets/index-B9LfWNyM.css +0 -1
- package/dist/assets/index-Bgmx2h7W.js +0 -441
- package/src/components/StudioGlobalDragOverlay.tsx +0 -24
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
- package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
- package/src/components/storyboard/FramePoster.test.tsx +0 -61
- package/src/components/storyboard/FramePoster.tsx +0 -86
- package/src/components/storyboard/StoryboardDirection.tsx +0 -45
- package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
- package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
- package/src/components/storyboard/StoryboardGrid.tsx +0 -55
- package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
- package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
- package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
- package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
- package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
- package/src/components/storyboard/StoryboardView.tsx +0 -169
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
- package/src/components/storyboard/frameComments.test.ts +0 -95
- package/src/components/storyboard/frameComments.ts +0 -115
- package/src/components/storyboard/frameStatus.ts +0 -36
- package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
- package/src/components/storyboard/storyboardReviewStage.ts +0 -45
- package/src/components/storyboard/useFrameComments.ts +0 -98
- package/src/contexts/ViewModeContext.tsx +0 -142
- package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
- package/src/hooks/useProjectSignaturePoll.ts +0 -68
- package/src/hooks/useStoryboard.ts +0 -99
- package/src/player/hooks/useExpandedTimelineElements.test.ts +0 -774
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -460
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { Dock } from "./Dock";
|
|
7
|
+
import { parseDockLayout } from "./dockLayoutSchema";
|
|
8
|
+
import { useDockLayoutStore } from "./dockLayoutStore";
|
|
9
|
+
import { PANEL_IDS } from "./panelRegistry";
|
|
10
|
+
import { readStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
11
|
+
|
|
12
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
|
+
|
|
14
|
+
class ResizeObserverStub {
|
|
15
|
+
observe() {}
|
|
16
|
+
unobserve() {}
|
|
17
|
+
disconnect() {}
|
|
18
|
+
}
|
|
19
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = ResizeObserverStub;
|
|
20
|
+
|
|
21
|
+
let root: Root | null = null;
|
|
22
|
+
|
|
23
|
+
function mount(projectId: string | null) {
|
|
24
|
+
const host = document.createElement("div");
|
|
25
|
+
document.body.append(host);
|
|
26
|
+
root = createRoot(host);
|
|
27
|
+
act(() => {
|
|
28
|
+
root?.render(
|
|
29
|
+
<Dock.Root projectId={projectId}>
|
|
30
|
+
{PANEL_IDS.map((id) => (
|
|
31
|
+
<Dock.Panel key={id} id={id}>
|
|
32
|
+
<div data-testid={`content-${id}`}>{id}</div>
|
|
33
|
+
</Dock.Panel>
|
|
34
|
+
))}
|
|
35
|
+
</Dock.Root>,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
return host;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
localStorage.clear();
|
|
43
|
+
vi.useFakeTimers();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
act(() => root?.unmount());
|
|
48
|
+
root = null;
|
|
49
|
+
document.body.innerHTML = "";
|
|
50
|
+
vi.useRealTimers();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// The default Edit layout tabs [compositions|assets|code|catalog] into one
|
|
54
|
+
// group and [design|layers|renders|variables] into another; dockview shows
|
|
55
|
+
// only the active tab's content per group. `slideshow` is never part of the
|
|
56
|
+
// default build — StudioRightPanels opens it itself when the file is one.
|
|
57
|
+
const DEFAULT_OPEN = PANEL_IDS.filter((id) => id !== "slideshow");
|
|
58
|
+
const DEFAULT_VISIBLE = ["preview", "timeline", "compositions", "design"];
|
|
59
|
+
|
|
60
|
+
describe("Dock on React 19", () => {
|
|
61
|
+
it("mounts the default layout's ten panels, showing only each group's active tab", () => {
|
|
62
|
+
const host = mount("p1");
|
|
63
|
+
for (const id of DEFAULT_VISIBLE) {
|
|
64
|
+
expect(host.querySelector(`[data-testid="content-${id}"]`)).not.toBeNull();
|
|
65
|
+
}
|
|
66
|
+
for (const id of DEFAULT_OPEN.filter((id) => !DEFAULT_VISIBLE.includes(id))) {
|
|
67
|
+
expect(host.querySelector(`[data-testid="content-${id}"]`)).toBeNull();
|
|
68
|
+
}
|
|
69
|
+
expect(host.querySelector('[data-testid="content-slideshow"]')).toBeNull();
|
|
70
|
+
expect(useDockLayoutStore.getState().openPanels).toEqual(new Set(DEFAULT_OPEN));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("persists a layout change per project and reads it back through the schema", () => {
|
|
74
|
+
mount("p1");
|
|
75
|
+
act(() => useDockLayoutStore.getState().closePanel("renders"));
|
|
76
|
+
act(() => {
|
|
77
|
+
vi.advanceTimersByTime(400);
|
|
78
|
+
});
|
|
79
|
+
const stored = readStudioUiPreferences(undefined, "p1").dockLayout;
|
|
80
|
+
const expected = DEFAULT_OPEN.filter((id) => id !== "renders");
|
|
81
|
+
expect(Object.keys(stored?.panels ?? {}).sort()).toEqual([...expected].sort());
|
|
82
|
+
expect(readStudioUiPreferences(undefined, "p2").dockLayout).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("closes a panel and reopens it from the store, becoming its group's visible tab", async () => {
|
|
86
|
+
const host = mount("p1");
|
|
87
|
+
act(() => useDockLayoutStore.getState().closePanel("renders"));
|
|
88
|
+
expect(useDockLayoutStore.getState().openPanels.has("renders")).toBe(false);
|
|
89
|
+
expect(host.querySelector('[data-testid="content-renders"]')).toBeNull();
|
|
90
|
+
// dockview's own panel-active event dispatch resolves on a microtask, one
|
|
91
|
+
// tick after the synchronous store call returns; `act(async ...)` is what
|
|
92
|
+
// actually waits for it instead of asserting against pre-flush DOM.
|
|
93
|
+
await act(async () => {
|
|
94
|
+
useDockLayoutStore.getState().togglePanel("renders");
|
|
95
|
+
await Promise.resolve();
|
|
96
|
+
});
|
|
97
|
+
expect(useDockLayoutStore.getState().openPanels.has("renders")).toBe(true);
|
|
98
|
+
expect(host.querySelector('[data-testid="content-renders"]')).not.toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("reopens a closed panel as a tab of its zone's group, not a new group", () => {
|
|
102
|
+
mount("p1");
|
|
103
|
+
act(() => useDockLayoutStore.getState().closePanel("compositions"));
|
|
104
|
+
act(() => useDockLayoutStore.getState().togglePanel("compositions"));
|
|
105
|
+
act(() => {
|
|
106
|
+
vi.advanceTimersByTime(1000);
|
|
107
|
+
});
|
|
108
|
+
const grid = JSON.stringify(readStudioUiPreferences(undefined, "p1").dockLayout?.grid);
|
|
109
|
+
const groups = [...grid.matchAll(/"views":\[([^\]]*)\]/g)].map((m) => m[1]);
|
|
110
|
+
const home = groups.find((views) => views.includes('"compositions"'));
|
|
111
|
+
expect(home).toContain('"assets"');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("reopens a side panel next to the preview when its whole column was closed", () => {
|
|
115
|
+
mount("p1");
|
|
116
|
+
const { closePanel, togglePanel } = useDockLayoutStore.getState();
|
|
117
|
+
for (const id of ["compositions", "assets", "code", "catalog"] as const)
|
|
118
|
+
act(() => closePanel(id));
|
|
119
|
+
act(() => togglePanel("compositions"));
|
|
120
|
+
expect(useDockLayoutStore.getState().openPanels.has("compositions")).toBe(true);
|
|
121
|
+
expect(useDockLayoutStore.getState().visiblePanels.has("compositions")).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("reopens the timeline as its own group, never as a tab of the preview", () => {
|
|
125
|
+
mount("p1");
|
|
126
|
+
act(() => useDockLayoutStore.getState().closePanel("timeline"));
|
|
127
|
+
act(() => useDockLayoutStore.getState().togglePanel("timeline"));
|
|
128
|
+
act(() => {
|
|
129
|
+
vi.advanceTimersByTime(1000);
|
|
130
|
+
});
|
|
131
|
+
const grid = JSON.stringify(readStudioUiPreferences(undefined, "p1").dockLayout?.grid);
|
|
132
|
+
const groups = [...grid.matchAll(/"views":\[([^\]]*)\]/g)].map((m) => m[1]);
|
|
133
|
+
expect(groups.find((views) => views.includes('"timeline"'))).not.toContain('"preview"');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("restores the stored layout on the next mount instead of rebuilding the default", () => {
|
|
137
|
+
mount("p1");
|
|
138
|
+
act(() => useDockLayoutStore.getState().closePanel("renders"));
|
|
139
|
+
act(() => {
|
|
140
|
+
vi.advanceTimersByTime(1000);
|
|
141
|
+
});
|
|
142
|
+
act(() => root?.unmount());
|
|
143
|
+
root = null;
|
|
144
|
+
document.body.innerHTML = "";
|
|
145
|
+
|
|
146
|
+
mount("p1");
|
|
147
|
+
expect(useDockLayoutStore.getState().openPanels.has("renders")).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("falls back to the default layout when the stored one names an unknown panel", () => {
|
|
151
|
+
localStorage.setItem(
|
|
152
|
+
"hf-studio-ui-preferences:p1",
|
|
153
|
+
JSON.stringify({ dockLayout: { grid: {}, panels: { nope: {} } } }),
|
|
154
|
+
);
|
|
155
|
+
const host = mount("p1");
|
|
156
|
+
expect(host.querySelector('[data-testid="content-preview"]')).not.toBeNull();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("parseDockLayout", () => {
|
|
161
|
+
it("rejects shapes that are not a dock layout", () => {
|
|
162
|
+
expect(parseDockLayout(null)).toBeNull();
|
|
163
|
+
expect(parseDockLayout({ grid: {}, panels: {} })).toBeNull();
|
|
164
|
+
expect(
|
|
165
|
+
parseDockLayout({
|
|
166
|
+
grid: {
|
|
167
|
+
width: 1,
|
|
168
|
+
height: 1,
|
|
169
|
+
orientation: "HORIZONTAL",
|
|
170
|
+
root: { type: "leaf", data: { views: ["ghost"] } },
|
|
171
|
+
},
|
|
172
|
+
panels: { ghost: { id: "ghost", contentComponent: "panel" } },
|
|
173
|
+
}),
|
|
174
|
+
).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const placed = (views: string[], panelIds: string[]) => ({
|
|
178
|
+
grid: {
|
|
179
|
+
width: 1,
|
|
180
|
+
height: 1,
|
|
181
|
+
orientation: "HORIZONTAL",
|
|
182
|
+
root: { type: "leaf", data: { views } },
|
|
183
|
+
},
|
|
184
|
+
panels: Object.fromEntries(panelIds.map((id) => [id, { id, contentComponent: "panel" }])),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("rejects a view that has no panel entry and a panel that no view places", () => {
|
|
188
|
+
expect(parseDockLayout(placed(["preview", "design"], ["preview"]))).toBeNull();
|
|
189
|
+
expect(parseDockLayout(placed(["preview"], ["preview", "design"]))).toBeNull();
|
|
190
|
+
expect(parseDockLayout(placed(["preview", "design"], ["preview", "design"]))).not.toBeNull();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { createPortal } from "react-dom";
|
|
11
|
+
import {
|
|
12
|
+
DockviewReact,
|
|
13
|
+
type DockviewApi,
|
|
14
|
+
type DockviewReadyEvent,
|
|
15
|
+
type IDockviewPanelProps,
|
|
16
|
+
} from "dockview-react";
|
|
17
|
+
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
18
|
+
import { addRegisteredPanel, buildEditLayout } from "./dockLayout";
|
|
19
|
+
import { DOCK_PANEL_COMPONENT } from "./dockLayoutSchema";
|
|
20
|
+
import { useDockLayoutStore, type DockController, type DockSnapshot } from "./dockLayoutStore";
|
|
21
|
+
import {
|
|
22
|
+
PANEL_DEFINITIONS,
|
|
23
|
+
PANEL_IDS,
|
|
24
|
+
isPanelId,
|
|
25
|
+
panelsInZone,
|
|
26
|
+
type PanelDefinition,
|
|
27
|
+
type PanelId,
|
|
28
|
+
} from "./panelRegistry";
|
|
29
|
+
import "./dock.css";
|
|
30
|
+
|
|
31
|
+
const PERSIST_DEBOUNCE_MS = 250;
|
|
32
|
+
|
|
33
|
+
type Slots = Partial<Record<PanelId, HTMLElement>>;
|
|
34
|
+
|
|
35
|
+
interface SlotsContextValue {
|
|
36
|
+
slots: Slots;
|
|
37
|
+
registerSlot: (id: PanelId, element: HTMLElement | null) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SlotsContext = createContext<SlotsContextValue | null>(null);
|
|
41
|
+
|
|
42
|
+
function useSlots(): SlotsContextValue {
|
|
43
|
+
const value = useContext(SlotsContext);
|
|
44
|
+
if (!value) throw new Error("Dock.Panel must be rendered inside Dock.Root");
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The element dockview mounts for a panel; Dock.Panel portals the real content into it. */
|
|
49
|
+
function PanelSlot({ api }: IDockviewPanelProps) {
|
|
50
|
+
const { registerSlot } = useSlots();
|
|
51
|
+
const id = api.id;
|
|
52
|
+
const ref = useCallback(
|
|
53
|
+
(element: HTMLDivElement | null) => {
|
|
54
|
+
if (isPanelId(id)) registerSlot(id, element);
|
|
55
|
+
},
|
|
56
|
+
[id, registerSlot],
|
|
57
|
+
);
|
|
58
|
+
return <div ref={ref} className="h-full w-full min-h-0 min-w-0 overflow-hidden" />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const COMPONENTS = { [DOCK_PANEL_COMPONENT]: PanelSlot };
|
|
62
|
+
|
|
63
|
+
function snapshot(api: DockviewApi): DockSnapshot {
|
|
64
|
+
const openPanels = new Set<PanelId>();
|
|
65
|
+
const visiblePanels = new Set<PanelId>();
|
|
66
|
+
for (const panel of api.panels) {
|
|
67
|
+
if (!isPanelId(panel.id)) continue;
|
|
68
|
+
openPanels.add(panel.id);
|
|
69
|
+
if (panel.api.isVisible && panel.group.api.isVisible) visiblePanels.add(panel.id);
|
|
70
|
+
}
|
|
71
|
+
const active = api.activePanel?.id;
|
|
72
|
+
return { openPanels, visiblePanels, activePanel: isPanelId(active) ? active : null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createController(api: DockviewApi): DockController {
|
|
76
|
+
const open = (id: PanelId) => {
|
|
77
|
+
if (api.getPanel(id)) return;
|
|
78
|
+
const { zone, reopen } = PANEL_DEFINITIONS[id];
|
|
79
|
+
// Side columns are tab groups; preview and timeline are separate groups in the centre.
|
|
80
|
+
const sibling =
|
|
81
|
+
zone === "center"
|
|
82
|
+
? undefined
|
|
83
|
+
: panelsInZone(zone).find((other) => other !== id && api.getPanel(other));
|
|
84
|
+
if (sibling) {
|
|
85
|
+
addRegisteredPanel(api, id, { referencePanel: sibling, direction: "within" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const hasAnchor = api.getPanel(reopen.near) !== undefined;
|
|
89
|
+
const position = { referencePanel: reopen.near, direction: reopen.direction };
|
|
90
|
+
addRegisteredPanel(api, id, hasAnchor ? position : undefined);
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
open,
|
|
94
|
+
activate: (id) => {
|
|
95
|
+
open(id);
|
|
96
|
+
api.getPanel(id)?.api.setActive();
|
|
97
|
+
},
|
|
98
|
+
close: (id) => {
|
|
99
|
+
const panel = api.getPanel(id);
|
|
100
|
+
if (panel) api.removePanel(panel);
|
|
101
|
+
},
|
|
102
|
+
setTitle: (id, title) => api.getPanel(id)?.api.setTitle(title),
|
|
103
|
+
setGroupVisible: (id, visible) => api.getPanel(id)?.group.api.setVisible(visible),
|
|
104
|
+
reset: () => buildEditLayout(api, window.innerWidth),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function restoreOrBuild(api: DockviewApi, projectId: string | null) {
|
|
109
|
+
const stored = readStudioUiPreferences(undefined, projectId).dockLayout;
|
|
110
|
+
if (stored) {
|
|
111
|
+
try {
|
|
112
|
+
api.fromJSON(stored);
|
|
113
|
+
return;
|
|
114
|
+
} catch {
|
|
115
|
+
/* a layout the schema accepted but dockview cannot load: start over */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
buildEditLayout(api, window.innerWidth);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function Root({ projectId, children }: { projectId: string | null; children: ReactNode }) {
|
|
122
|
+
const [slots, setSlots] = useState<Slots>({});
|
|
123
|
+
const registerSlot = useCallback((id: PanelId, element: HTMLElement | null) => {
|
|
124
|
+
setSlots((prev) => {
|
|
125
|
+
if (prev[id] === (element ?? undefined)) return prev;
|
|
126
|
+
const next = { ...prev };
|
|
127
|
+
if (element) next[id] = element;
|
|
128
|
+
else delete next[id];
|
|
129
|
+
return next;
|
|
130
|
+
});
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
const disposeRef = useRef<() => void>(() => {});
|
|
134
|
+
useEffect(() => () => disposeRef.current(), []);
|
|
135
|
+
|
|
136
|
+
const onReady = useCallback(
|
|
137
|
+
({ api }: DockviewReadyEvent) => {
|
|
138
|
+
restoreOrBuild(api, projectId);
|
|
139
|
+
const store = useDockLayoutStore.getState();
|
|
140
|
+
store.attach(createController(api));
|
|
141
|
+
store.sync(snapshot(api));
|
|
142
|
+
const pending = store.takePendingActivation();
|
|
143
|
+
if (pending) api.getPanel(pending)?.api.setActive();
|
|
144
|
+
|
|
145
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
146
|
+
const persist = () => {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
timer = setTimeout(() => {
|
|
149
|
+
writeStudioUiPreferences({ dockLayout: api.toJSON() }, undefined, projectId);
|
|
150
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
151
|
+
};
|
|
152
|
+
// dockview does not fire onDidLayoutChange for add/remove/activate,
|
|
153
|
+
// so every event class is wired to the same sync+persist pair.
|
|
154
|
+
const onDockChange = () => {
|
|
155
|
+
useDockLayoutStore.getState().sync(snapshot(api));
|
|
156
|
+
persist();
|
|
157
|
+
};
|
|
158
|
+
const subscriptions = [
|
|
159
|
+
api.onDidAddPanel(onDockChange),
|
|
160
|
+
api.onDidRemovePanel(onDockChange),
|
|
161
|
+
api.onDidActivePanelChange(onDockChange),
|
|
162
|
+
api.onDidLayoutChange(onDockChange),
|
|
163
|
+
];
|
|
164
|
+
disposeRef.current = () => {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
for (const subscription of subscriptions) subscription.dispose();
|
|
167
|
+
useDockLayoutStore.getState().detach();
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
[projectId],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<SlotsContext.Provider value={{ slots, registerSlot }}>
|
|
175
|
+
<DockviewReact
|
|
176
|
+
key={projectId ?? ""}
|
|
177
|
+
className="hf-dock flex-1 min-h-0"
|
|
178
|
+
components={COMPONENTS}
|
|
179
|
+
defaultRenderer="always"
|
|
180
|
+
disableFloatingGroups
|
|
181
|
+
onReady={onReady}
|
|
182
|
+
/>
|
|
183
|
+
{children}
|
|
184
|
+
</SlotsContext.Provider>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function Panel({ id, title, children }: { id: PanelId; title?: string; children: ReactNode }) {
|
|
189
|
+
const element = useSlots().slots[id];
|
|
190
|
+
const visible = useDockLayoutStore((state) => state.visiblePanels.has(id));
|
|
191
|
+
const controller = useDockLayoutStore((state) => state.controller);
|
|
192
|
+
const label = title ?? PANEL_DEFINITIONS[id].title;
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
controller?.setTitle(id, label);
|
|
195
|
+
}, [controller, id, label]);
|
|
196
|
+
const definition: PanelDefinition = PANEL_DEFINITIONS[id];
|
|
197
|
+
const shown = visible || definition.keepMounted;
|
|
198
|
+
return element && shown ? createPortal(children, element) : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function WindowMenu() {
|
|
202
|
+
const openPanels = useDockLayoutStore((state) => state.openPanels);
|
|
203
|
+
const togglePanel = useDockLayoutStore((state) => state.togglePanel);
|
|
204
|
+
const resetLayout = useDockLayoutStore((state) => state.resetLayout);
|
|
205
|
+
const [open, setOpen] = useState(false);
|
|
206
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!open) return;
|
|
210
|
+
const close = (event: PointerEvent) => {
|
|
211
|
+
if (!rootRef.current?.contains(event.target as Node)) setOpen(false);
|
|
212
|
+
};
|
|
213
|
+
document.addEventListener("pointerdown", close);
|
|
214
|
+
return () => document.removeEventListener("pointerdown", close);
|
|
215
|
+
}, [open]);
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div ref={rootRef} className="relative">
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
aria-haspopup="menu"
|
|
222
|
+
aria-expanded={open}
|
|
223
|
+
onClick={() => setOpen((value) => !value)}
|
|
224
|
+
className="h-7 px-2.5 rounded-md text-[11px] font-medium text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 transition-colors"
|
|
225
|
+
>
|
|
226
|
+
Window
|
|
227
|
+
</button>
|
|
228
|
+
{open && (
|
|
229
|
+
<div
|
|
230
|
+
role="menu"
|
|
231
|
+
className="absolute right-0 top-8 z-50 w-44 rounded-md border border-neutral-800 bg-neutral-900 py-1 shadow-lg"
|
|
232
|
+
>
|
|
233
|
+
{PANEL_IDS.map((id) => (
|
|
234
|
+
<button
|
|
235
|
+
key={id}
|
|
236
|
+
type="button"
|
|
237
|
+
role="menuitemcheckbox"
|
|
238
|
+
aria-checked={openPanels.has(id)}
|
|
239
|
+
onClick={() => togglePanel(id)}
|
|
240
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] text-neutral-300 hover:bg-neutral-800"
|
|
241
|
+
>
|
|
242
|
+
<span className="w-3 text-panel-accent">{openPanels.has(id) ? "✓" : ""}</span>
|
|
243
|
+
{PANEL_DEFINITIONS[id].title}
|
|
244
|
+
</button>
|
|
245
|
+
))}
|
|
246
|
+
<div className="my-1 border-t border-neutral-800" />
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
role="menuitem"
|
|
250
|
+
onClick={() => {
|
|
251
|
+
resetLayout();
|
|
252
|
+
setOpen(false);
|
|
253
|
+
}}
|
|
254
|
+
className="w-full px-3 py-1.5 text-left text-[11px] text-neutral-300 hover:bg-neutral-800"
|
|
255
|
+
>
|
|
256
|
+
Reset layout
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const Dock = { Root, Panel, WindowMenu };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@import "dockview-react/dist/styles/dockview.css";
|
|
2
|
+
|
|
3
|
+
.hf-dock {
|
|
4
|
+
--dv-group-view-background-color: var(--color-panel-bg);
|
|
5
|
+
--dv-tabs-and-actions-container-background-color: var(--color-panel-surface);
|
|
6
|
+
--dv-activegroup-visiblepanel-tab-background-color: var(--color-panel-bg);
|
|
7
|
+
--dv-activegroup-hiddenpanel-tab-background-color: var(--color-panel-surface);
|
|
8
|
+
--dv-inactivegroup-visiblepanel-tab-background-color: var(--color-panel-bg);
|
|
9
|
+
--dv-inactivegroup-hiddenpanel-tab-background-color: var(--color-panel-surface);
|
|
10
|
+
--dv-activegroup-visiblepanel-tab-color: var(--color-panel-text-1);
|
|
11
|
+
--dv-activegroup-hiddenpanel-tab-color: var(--color-panel-text-3);
|
|
12
|
+
--dv-inactivegroup-visiblepanel-tab-color: var(--color-panel-text-2);
|
|
13
|
+
--dv-inactivegroup-hiddenpanel-tab-color: var(--color-panel-text-4);
|
|
14
|
+
--dv-tab-divider-color: var(--color-panel-border);
|
|
15
|
+
--dv-separator-border: var(--color-panel-border);
|
|
16
|
+
--dv-paneview-header-border-color: var(--color-panel-border);
|
|
17
|
+
--dv-sash-color: var(--color-panel-hover);
|
|
18
|
+
--dv-active-sash-color: var(--color-panel-accent);
|
|
19
|
+
--dv-drag-over-background-color: color-mix(in srgb, var(--color-panel-accent) 12%, transparent);
|
|
20
|
+
--dv-drag-over-border-color: var(--color-panel-accent);
|
|
21
|
+
--dv-tab-font-size: 11px;
|
|
22
|
+
--dv-tabs-and-actions-container-font-size: 11px;
|
|
23
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Direction, DockviewApi } from "dockview-react";
|
|
2
|
+
import { DOCK_PANEL_COMPONENT } from "./dockLayoutSchema";
|
|
3
|
+
import { PANEL_DEFINITIONS, type PanelId } from "./panelRegistry";
|
|
4
|
+
|
|
5
|
+
const MIN_PREVIEW_W = 360;
|
|
6
|
+
const MIN_PREVIEW_H = 200;
|
|
7
|
+
const MIN_TIMELINE_H = 100;
|
|
8
|
+
const MIN_SIDE_W = 200;
|
|
9
|
+
const DEFAULT_TIMELINE_H = 360;
|
|
10
|
+
|
|
11
|
+
function defaultSideWidths(viewportWidth: number) {
|
|
12
|
+
return {
|
|
13
|
+
left: Math.max(MIN_SIDE_W, Math.min(384, Math.round(viewportWidth * 0.257))),
|
|
14
|
+
right: Math.max(280, Math.min(424, Math.round(viewportWidth * 0.284))),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function minimumSize(id: PanelId) {
|
|
19
|
+
if (id === "preview") return { minimumWidth: MIN_PREVIEW_W, minimumHeight: MIN_PREVIEW_H };
|
|
20
|
+
if (id === "timeline") return { minimumHeight: MIN_TIMELINE_H };
|
|
21
|
+
return { minimumWidth: MIN_SIDE_W };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function addRegisteredPanel(
|
|
25
|
+
api: DockviewApi,
|
|
26
|
+
id: PanelId,
|
|
27
|
+
position?: { referencePanel: PanelId; direction: Direction },
|
|
28
|
+
) {
|
|
29
|
+
return api.addPanel({
|
|
30
|
+
id,
|
|
31
|
+
component: DOCK_PANEL_COMPONENT,
|
|
32
|
+
title: PANEL_DEFINITIONS[id].title,
|
|
33
|
+
renderer: "always",
|
|
34
|
+
...minimumSize(id),
|
|
35
|
+
...(position ? { position } : {}),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The default Edit layout: [library | preview | inspector] over a full-width timeline. */
|
|
40
|
+
export function buildEditLayout(api: DockviewApi, viewportWidth: number) {
|
|
41
|
+
api.clear();
|
|
42
|
+
const widths = defaultSideWidths(viewportWidth);
|
|
43
|
+
addRegisteredPanel(api, "preview");
|
|
44
|
+
addRegisteredPanel(api, "timeline", { referencePanel: "preview", direction: "below" });
|
|
45
|
+
addRegisteredPanel(api, "compositions", { referencePanel: "preview", direction: "left" });
|
|
46
|
+
for (const id of ["assets", "code", "catalog"] as const) {
|
|
47
|
+
addRegisteredPanel(api, id, { referencePanel: "compositions", direction: "within" });
|
|
48
|
+
}
|
|
49
|
+
addRegisteredPanel(api, "design", { referencePanel: "preview", direction: "right" });
|
|
50
|
+
for (const id of ["layers", "renders", "variables"] as const) {
|
|
51
|
+
addRegisteredPanel(api, id, { referencePanel: "design", direction: "within" });
|
|
52
|
+
}
|
|
53
|
+
api.getPanel("compositions")?.api.setActive();
|
|
54
|
+
api.getPanel("design")?.api.setActive();
|
|
55
|
+
api.getPanel("compositions")?.group.api.setSize({ width: widths.left });
|
|
56
|
+
api.getPanel("design")?.group.api.setSize({ width: widths.right });
|
|
57
|
+
api.getPanel("timeline")?.group.api.setSize({ height: DEFAULT_TIMELINE_H });
|
|
58
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { SerializedDockview } from "dockview-react";
|
|
2
|
+
import { isPanelId } from "./panelRegistry";
|
|
3
|
+
|
|
4
|
+
export const DOCK_PANEL_COMPONENT = "panel";
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function collectLeafViews(node: unknown, into: unknown[]): boolean {
|
|
11
|
+
if (!isRecord(node)) return false;
|
|
12
|
+
if (node.type === "leaf") {
|
|
13
|
+
if (!isRecord(node.data) || !Array.isArray(node.data.views)) return false;
|
|
14
|
+
into.push(...node.data.views);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (node.type === "branch") {
|
|
18
|
+
return Array.isArray(node.data) && node.data.every((child) => collectLeafViews(child, into));
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasGridShape(grid: Record<string, unknown>): boolean {
|
|
24
|
+
const oriented = grid.orientation === "HORIZONTAL" || grid.orientation === "VERTICAL";
|
|
25
|
+
return oriented && typeof grid.width === "number" && typeof grid.height === "number";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function panelsAreRegistered(panels: Record<string, unknown>): boolean {
|
|
29
|
+
const ids = Object.keys(panels);
|
|
30
|
+
return (
|
|
31
|
+
ids.length > 0 &&
|
|
32
|
+
ids.every((id) => {
|
|
33
|
+
const state = panels[id];
|
|
34
|
+
return (
|
|
35
|
+
isPanelId(id) &&
|
|
36
|
+
isRecord(state) &&
|
|
37
|
+
state.id === id &&
|
|
38
|
+
state.contentComponent === DOCK_PANEL_COMPONENT
|
|
39
|
+
);
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectFloatingViews(floating: unknown, into: unknown[]): boolean {
|
|
45
|
+
if (floating === undefined) return true;
|
|
46
|
+
if (!Array.isArray(floating)) return false;
|
|
47
|
+
return floating.every((group) => {
|
|
48
|
+
if (!isRecord(group) || !isRecord(group.data) || !Array.isArray(group.data.views)) return false;
|
|
49
|
+
into.push(...group.data.views);
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Parses a stored layout at the trust boundary; null (stale or foreign shape) means use the default preset. */
|
|
55
|
+
export function parseDockLayout(value: unknown): SerializedDockview | null {
|
|
56
|
+
if (!isRecord(value) || !isRecord(value.grid) || !isRecord(value.panels)) return null;
|
|
57
|
+
const { grid, panels } = value;
|
|
58
|
+
if (!hasGridShape(grid) || !panelsAreRegistered(panels)) return null;
|
|
59
|
+
|
|
60
|
+
const views: unknown[] = [];
|
|
61
|
+
if (!collectLeafViews(grid.root, views)) return null;
|
|
62
|
+
if (!collectFloatingViews(value.floatingGroups, views)) return null;
|
|
63
|
+
const everyViewIsAPanel = views.every((id) => typeof id === "string" && id in panels);
|
|
64
|
+
const everyPanelIsPlaced = Object.keys(panels).every((id) => views.includes(id));
|
|
65
|
+
return everyViewIsAPanel && everyPanelIsPlaced ? (value as unknown as SerializedDockview) : null;
|
|
66
|
+
}
|