@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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import type { DockviewApi } from "dockview-react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
applySideMinimums,
|
|
7
|
+
buildEditLayout,
|
|
8
|
+
defaultSideWidths,
|
|
9
|
+
sideMinimumWidth,
|
|
10
|
+
} from "./dockLayout";
|
|
11
|
+
import { mountBareDockview } from "./dockTestHarness";
|
|
12
|
+
|
|
13
|
+
class ResizeObserverStub {
|
|
14
|
+
observe() {}
|
|
15
|
+
unobserve() {}
|
|
16
|
+
disconnect() {}
|
|
17
|
+
}
|
|
18
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = ResizeObserverStub;
|
|
19
|
+
|
|
20
|
+
// Expected values are hand-derived from the old fitPanels rule: the preview keeps 360 first,
|
|
21
|
+
// then each side column takes half of what remains, never above 200 or below 120.
|
|
22
|
+
describe("sideMinimumWidth", () => {
|
|
23
|
+
it.each([
|
|
24
|
+
[1680, 200],
|
|
25
|
+
[1200, 200],
|
|
26
|
+
[860, 200],
|
|
27
|
+
[760, 200],
|
|
28
|
+
[700, 170],
|
|
29
|
+
[600, 120],
|
|
30
|
+
[560, 120],
|
|
31
|
+
[400, 120],
|
|
32
|
+
])("is %ipx wide -> sides shrink to %ipx at most", (viewport, expected) => {
|
|
33
|
+
expect(sideMinimumWidth(viewport)).toBe(expected);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("defaultSideWidths", () => {
|
|
38
|
+
it.each([
|
|
39
|
+
[1680, 384, 424],
|
|
40
|
+
[1280, 329, 364],
|
|
41
|
+
[860, 221, 244],
|
|
42
|
+
[560, 120, 120],
|
|
43
|
+
])("at %ipx gives %i / %i", (viewport, left, right) => {
|
|
44
|
+
expect(defaultSideWidths(viewport)).toEqual({ left, right });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it.each([1680, 1280, 1000, 860, 760, 700, 600, 560, 480])(
|
|
48
|
+
"leaves the preview its 360px floor at %ipx whenever the sides can shrink enough",
|
|
49
|
+
(viewport) => {
|
|
50
|
+
const { left, right } = defaultSideWidths(viewport);
|
|
51
|
+
const floor = sideMinimumWidth(viewport);
|
|
52
|
+
const overflow = left + right + 360 - viewport;
|
|
53
|
+
expect(overflow <= 0 || (left === floor && right === floor)).toBe(true);
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("applySideMinimums", () => {
|
|
59
|
+
let api: DockviewApi;
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
({ api } = mountBareDockview());
|
|
62
|
+
});
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
api.dispose();
|
|
65
|
+
document.body.innerHTML = "";
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("lowers side groups' minimum on a narrow window and shrinks them, never touching the preview", () => {
|
|
69
|
+
api.layout(1200, 700);
|
|
70
|
+
buildEditLayout(api, 1200);
|
|
71
|
+
applySideMinimums(api, 560);
|
|
72
|
+
expect(api.getPanel("compositions")?.group.minimumWidth).toBe(120);
|
|
73
|
+
expect(api.getPanel("design")?.group.minimumWidth).toBe(120);
|
|
74
|
+
expect(api.getPanel("compositions")?.group.width).toBe(120);
|
|
75
|
+
expect(api.getPanel("design")?.group.width).toBe(120);
|
|
76
|
+
expect(api.getPanel("preview")?.group.minimumWidth).toBe(360);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("leaves a wide window's user-chosen widths alone", () => {
|
|
80
|
+
api.layout(1200, 700);
|
|
81
|
+
buildEditLayout(api, 1200);
|
|
82
|
+
api.getPanel("compositions")?.group.api.setSize({ width: 300 });
|
|
83
|
+
applySideMinimums(api, 1200);
|
|
84
|
+
expect(api.getPanel("compositions")?.group.width).toBe(300);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("keeps the preview's floor when a side panel is tabbed into the preview group", () => {
|
|
88
|
+
api.layout(1200, 700);
|
|
89
|
+
buildEditLayout(api, 1200);
|
|
90
|
+
const preview = api.getPanel("preview");
|
|
91
|
+
const design = api.getPanel("design");
|
|
92
|
+
if (!preview || !design) throw new Error("default layout is missing panels");
|
|
93
|
+
design.api.moveTo({ group: preview.group, position: "center", index: 0 });
|
|
94
|
+
preview.api.setActive();
|
|
95
|
+
applySideMinimums(api, 560);
|
|
96
|
+
expect(preview.group.minimumWidth).toBe(360);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("pins the preview floor when a side panel that was showing gets the preview dragged in", () => {
|
|
100
|
+
api.layout(1200, 700);
|
|
101
|
+
buildEditLayout(api, 1200);
|
|
102
|
+
applySideMinimums(api, 560);
|
|
103
|
+
const preview = api.getPanel("preview");
|
|
104
|
+
const design = api.getPanel("design");
|
|
105
|
+
if (!preview || !design) throw new Error("default layout is missing panels");
|
|
106
|
+
preview.api.moveTo({ group: design.group });
|
|
107
|
+
applySideMinimums(api, 560);
|
|
108
|
+
expect(preview.group.minimumWidth).toBe(360);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("rewrites the minimum of a group that is neither the preview nor side-only", () => {
|
|
112
|
+
api.layout(1200, 700);
|
|
113
|
+
buildEditLayout(api, 1200);
|
|
114
|
+
const timeline = api.getPanel("timeline");
|
|
115
|
+
const compositions = api.getPanel("compositions");
|
|
116
|
+
if (!timeline || !compositions) throw new Error("default layout is missing panels");
|
|
117
|
+
timeline.api.moveTo({ group: compositions.group });
|
|
118
|
+
applySideMinimums(api, 560);
|
|
119
|
+
expect(compositions.group.minimumWidth).toBe(120);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("drops the preview floor from a group the preview has left", () => {
|
|
123
|
+
api.layout(1200, 700);
|
|
124
|
+
buildEditLayout(api, 1200);
|
|
125
|
+
const preview = api.getPanel("preview");
|
|
126
|
+
const timeline = api.getPanel("timeline");
|
|
127
|
+
if (!preview || !timeline) throw new Error("default layout is missing panels");
|
|
128
|
+
preview.api.moveTo({ group: timeline.group });
|
|
129
|
+
applySideMinimums(api, 560);
|
|
130
|
+
preview.api.moveTo({ position: "right", group: api.getPanel("design")?.group });
|
|
131
|
+
applySideMinimums(api, 560);
|
|
132
|
+
expect(timeline.group.minimumWidth).toBe(120);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Direction, DockviewApi } from "dockview-react";
|
|
2
|
+
import { DOCK_PANEL_COMPONENT } from "./dockLayoutSchema";
|
|
3
|
+
import { PANEL_DEFINITIONS, isPanelId, 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 MIN_SIDE_W_FLOOR = 120;
|
|
10
|
+
const DEFAULT_TIMELINE_H = 360;
|
|
11
|
+
|
|
12
|
+
/** Preferred side widths; when they overflow the preview's floor the right yields first, then the left. */
|
|
13
|
+
export function defaultSideWidths(viewportWidth: number) {
|
|
14
|
+
const floor = sideMinimumWidth(viewportWidth);
|
|
15
|
+
const left = Math.max(floor, Math.min(384, Math.round(viewportWidth * 0.257)));
|
|
16
|
+
const right = Math.max(Math.min(280, floor), Math.min(424, Math.round(viewportWidth * 0.284)));
|
|
17
|
+
const excess = left + right - Math.max(0, viewportWidth - MIN_PREVIEW_W);
|
|
18
|
+
if (excess <= 0) return { left, right };
|
|
19
|
+
const shrunkRight = Math.max(floor, right - excess);
|
|
20
|
+
return { left: Math.max(floor, left - (excess - (right - shrunkRight))), right: shrunkRight };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Side groups keep MIN_SIDE_W while both fit beside the preview's floor, then shrink down to
|
|
25
|
+
* MIN_SIDE_W_FLOOR; below that the dock overflows and clips rather than squeezing the preview.
|
|
26
|
+
*/
|
|
27
|
+
export function sideMinimumWidth(dockWidth: number): number {
|
|
28
|
+
const fair = Math.floor((dockWidth - MIN_PREVIEW_W) / 2);
|
|
29
|
+
return Math.min(MIN_SIDE_W, Math.max(MIN_SIDE_W_FLOOR, fair));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function holdsPreview(group: DockviewApi["groups"][number]) {
|
|
33
|
+
return group.panels.some((panel) => panel.id === "preview");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The zone of a group whose every tab is a side panel, else null. */
|
|
37
|
+
function sideZone(group: DockviewApi["groups"][number]): "left" | "right" | null {
|
|
38
|
+
const zones = group.panels.map((panel) =>
|
|
39
|
+
isPanelId(panel.id) ? PANEL_DEFINITIONS[panel.id].zone : "center",
|
|
40
|
+
);
|
|
41
|
+
const first = zones[0];
|
|
42
|
+
const allSide = zones.every((zone) => zone !== "center");
|
|
43
|
+
return allSide && first && first !== "center" ? first : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Idempotent; rewrites every group's minimum, since dockview keeps a constraint once set. */
|
|
47
|
+
export function applySideMinimums(api: DockviewApi, dockWidth = window.innerWidth) {
|
|
48
|
+
const minimumWidth = sideMinimumWidth(dockWidth);
|
|
49
|
+
const cap = defaultSideWidths(dockWidth);
|
|
50
|
+
for (const group of api.groups) {
|
|
51
|
+
if (holdsPreview(group)) {
|
|
52
|
+
group.api.setConstraints({ minimumWidth: MIN_PREVIEW_W });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
group.api.setConstraints({ minimumWidth });
|
|
56
|
+
const zone = sideZone(group);
|
|
57
|
+
const limit = zone === "right" ? cap.right : cap.left;
|
|
58
|
+
if (zone && minimumWidth < MIN_SIDE_W && group.width > limit) {
|
|
59
|
+
group.api.setSize({ width: limit });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function minimumSize(id: PanelId) {
|
|
65
|
+
if (id === "preview") return { minimumWidth: MIN_PREVIEW_W, minimumHeight: MIN_PREVIEW_H };
|
|
66
|
+
if (id === "timeline") return { minimumHeight: MIN_TIMELINE_H };
|
|
67
|
+
return { minimumWidth: MIN_SIDE_W };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function addRegisteredPanel(
|
|
71
|
+
api: DockviewApi,
|
|
72
|
+
id: PanelId,
|
|
73
|
+
position?: { referencePanel: PanelId; direction: Direction },
|
|
74
|
+
) {
|
|
75
|
+
return api.addPanel({
|
|
76
|
+
id,
|
|
77
|
+
component: DOCK_PANEL_COMPONENT,
|
|
78
|
+
title: PANEL_DEFINITIONS[id].title,
|
|
79
|
+
renderer: "always",
|
|
80
|
+
...minimumSize(id),
|
|
81
|
+
...(position ? { position } : {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The default Edit layout: [library | preview | inspector] over a full-width timeline. */
|
|
86
|
+
export function buildEditLayout(api: DockviewApi, viewportWidth: number) {
|
|
87
|
+
api.clear();
|
|
88
|
+
const widths = defaultSideWidths(viewportWidth);
|
|
89
|
+
addRegisteredPanel(api, "preview");
|
|
90
|
+
addRegisteredPanel(api, "timeline", { referencePanel: "preview", direction: "below" });
|
|
91
|
+
addRegisteredPanel(api, "compositions", { referencePanel: "preview", direction: "left" });
|
|
92
|
+
for (const id of ["assets", "code", "catalog"] as const) {
|
|
93
|
+
addRegisteredPanel(api, id, { referencePanel: "compositions", direction: "within" });
|
|
94
|
+
}
|
|
95
|
+
addRegisteredPanel(api, "design", { referencePanel: "preview", direction: "right" });
|
|
96
|
+
for (const id of ["layers", "renders", "variables"] as const) {
|
|
97
|
+
addRegisteredPanel(api, id, { referencePanel: "design", direction: "within" });
|
|
98
|
+
}
|
|
99
|
+
api.getPanel("compositions")?.api.setActive();
|
|
100
|
+
api.getPanel("design")?.api.setActive();
|
|
101
|
+
applySideMinimums(api, viewportWidth);
|
|
102
|
+
api.getPanel("compositions")?.group.api.setSize({ width: widths.left });
|
|
103
|
+
api.getPanel("design")?.group.api.setSize({ width: widths.right });
|
|
104
|
+
api.getPanel("timeline")?.group.api.setSize({ height: DEFAULT_TIMELINE_H });
|
|
105
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import {
|
|
3
|
+
PANEL_DEFINITIONS,
|
|
4
|
+
PANEL_IDS,
|
|
5
|
+
panelsInZone,
|
|
6
|
+
type PanelId,
|
|
7
|
+
type PanelZone,
|
|
8
|
+
} from "./panelRegistry";
|
|
9
|
+
|
|
10
|
+
/** The narrow surface Dock.Root exposes; dockview types never leave the dock folder. */
|
|
11
|
+
export interface DockController {
|
|
12
|
+
/** Adds the panel at its default place if it is closed; never moves focus. */
|
|
13
|
+
open: (id: PanelId) => void;
|
|
14
|
+
/** Focuses the panel, reopening it at its default place if it was closed. */
|
|
15
|
+
activate: (id: PanelId) => void;
|
|
16
|
+
setTitle: (id: PanelId, title: string) => void;
|
|
17
|
+
close: (id: PanelId) => void;
|
|
18
|
+
/** Hides or shows the panel's whole tab group without closing it. */
|
|
19
|
+
setGroupVisible: (id: PanelId, visible: boolean) => void;
|
|
20
|
+
reset: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DockSnapshot {
|
|
24
|
+
openPanels: ReadonlySet<PanelId>;
|
|
25
|
+
/** Open panels whose tab is showing and whose group is not hidden. */
|
|
26
|
+
visiblePanels: ReadonlySet<PanelId>;
|
|
27
|
+
activePanel: PanelId | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type LastActive = Partial<Record<PanelZone, PanelId>>;
|
|
31
|
+
|
|
32
|
+
interface DockLayoutState extends DockSnapshot {
|
|
33
|
+
controller: DockController | null;
|
|
34
|
+
lastActive: LastActive;
|
|
35
|
+
/** An activation requested before the dock mounted; Dock.Root applies it on ready. */
|
|
36
|
+
pendingActivation: PanelId | null;
|
|
37
|
+
attach: (controller: DockController) => void;
|
|
38
|
+
detach: () => void;
|
|
39
|
+
sync: (snapshot: DockSnapshot) => void;
|
|
40
|
+
takePendingActivation: () => PanelId | null;
|
|
41
|
+
activatePanel: (id: PanelId) => void;
|
|
42
|
+
closePanel: (id: PanelId) => void;
|
|
43
|
+
togglePanel: (id: PanelId) => void;
|
|
44
|
+
setZoneVisible: (zone: PanelZone, visible: boolean) => void;
|
|
45
|
+
resetLayout: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const useDockLayoutStore = create<DockLayoutState>((set, get) => ({
|
|
49
|
+
controller: null,
|
|
50
|
+
openPanels: new Set(PANEL_IDS),
|
|
51
|
+
visiblePanels: new Set(PANEL_IDS),
|
|
52
|
+
activePanel: null,
|
|
53
|
+
lastActive: {},
|
|
54
|
+
pendingActivation: null,
|
|
55
|
+
attach: (controller) => set({ controller }),
|
|
56
|
+
detach: () => set({ controller: null }),
|
|
57
|
+
sync: (snapshot) =>
|
|
58
|
+
set((state) => {
|
|
59
|
+
const { activePanel } = snapshot;
|
|
60
|
+
if (!activePanel) return snapshot;
|
|
61
|
+
const zone = PANEL_DEFINITIONS[activePanel].zone;
|
|
62
|
+
return { ...snapshot, lastActive: { ...state.lastActive, [zone]: activePanel } };
|
|
63
|
+
}),
|
|
64
|
+
takePendingActivation: () => {
|
|
65
|
+
const { pendingActivation } = get();
|
|
66
|
+
set({ pendingActivation: null });
|
|
67
|
+
return pendingActivation;
|
|
68
|
+
},
|
|
69
|
+
activatePanel: (id) => {
|
|
70
|
+
const { controller } = get();
|
|
71
|
+
if (!controller) {
|
|
72
|
+
set({ pendingActivation: id });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
controller.setGroupVisible(id, true);
|
|
76
|
+
controller.activate(id);
|
|
77
|
+
},
|
|
78
|
+
closePanel: (id) => get().controller?.close(id),
|
|
79
|
+
togglePanel: (id) => {
|
|
80
|
+
const { openPanels, activatePanel, closePanel } = get();
|
|
81
|
+
if (openPanels.has(id)) closePanel(id);
|
|
82
|
+
else activatePanel(id);
|
|
83
|
+
},
|
|
84
|
+
setZoneVisible: (zone, visible) => {
|
|
85
|
+
const { controller, openPanels } = get();
|
|
86
|
+
for (const id of panelsInZone(zone)) {
|
|
87
|
+
if (openPanels.has(id)) controller?.setGroupVisible(id, visible);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
resetLayout: () => get().controller?.reset(),
|
|
91
|
+
}));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createDockview, type DockviewApi } from "dockview-react";
|
|
2
|
+
|
|
3
|
+
/** A real dockview mounted on a fresh host element, with empty panel bodies. */
|
|
4
|
+
export function mountBareDockview(): { host: HTMLElement; api: DockviewApi } {
|
|
5
|
+
const host = document.createElement("div");
|
|
6
|
+
document.body.append(host);
|
|
7
|
+
const api = createDockview(host, {
|
|
8
|
+
createComponent: () => ({ element: document.createElement("div"), init() {}, dispose() {} }),
|
|
9
|
+
});
|
|
10
|
+
return { host, api };
|
|
11
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Direction } from "dockview-react";
|
|
2
|
+
|
|
3
|
+
export const PANEL_IDS = [
|
|
4
|
+
"preview",
|
|
5
|
+
"timeline",
|
|
6
|
+
"compositions",
|
|
7
|
+
"assets",
|
|
8
|
+
"code",
|
|
9
|
+
"catalog",
|
|
10
|
+
"design",
|
|
11
|
+
"layers",
|
|
12
|
+
"renders",
|
|
13
|
+
"variables",
|
|
14
|
+
"slideshow",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export type PanelId = (typeof PANEL_IDS)[number];
|
|
18
|
+
export type PanelZone = "left" | "center" | "right";
|
|
19
|
+
|
|
20
|
+
export interface PanelDefinition {
|
|
21
|
+
title: string;
|
|
22
|
+
zone: PanelZone;
|
|
23
|
+
/** Where Window > <panel> puts it when it has no saved place: next to `near`. */
|
|
24
|
+
reopen: { near: PanelId; direction: Direction };
|
|
25
|
+
/** Content stays mounted while its tab is hidden (the preview iframe must not reload). */
|
|
26
|
+
keepMounted?: true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const PANEL_DEFINITIONS = {
|
|
30
|
+
preview: {
|
|
31
|
+
title: "Preview",
|
|
32
|
+
zone: "center",
|
|
33
|
+
reopen: { near: "timeline", direction: "above" },
|
|
34
|
+
keepMounted: true,
|
|
35
|
+
},
|
|
36
|
+
timeline: {
|
|
37
|
+
title: "Timeline",
|
|
38
|
+
zone: "center",
|
|
39
|
+
reopen: { near: "preview", direction: "below" },
|
|
40
|
+
keepMounted: true,
|
|
41
|
+
},
|
|
42
|
+
compositions: {
|
|
43
|
+
title: "Compositions",
|
|
44
|
+
zone: "left",
|
|
45
|
+
reopen: { near: "preview", direction: "left" },
|
|
46
|
+
},
|
|
47
|
+
assets: { title: "Assets", zone: "left", reopen: { near: "compositions", direction: "within" } },
|
|
48
|
+
code: { title: "Code", zone: "left", reopen: { near: "compositions", direction: "within" } },
|
|
49
|
+
catalog: {
|
|
50
|
+
title: "Catalog",
|
|
51
|
+
zone: "left",
|
|
52
|
+
reopen: { near: "compositions", direction: "within" },
|
|
53
|
+
},
|
|
54
|
+
design: { title: "Design", zone: "right", reopen: { near: "preview", direction: "right" } },
|
|
55
|
+
layers: { title: "Layers", zone: "right", reopen: { near: "design", direction: "within" } },
|
|
56
|
+
renders: { title: "Renders", zone: "right", reopen: { near: "design", direction: "within" } },
|
|
57
|
+
variables: { title: "Variables", zone: "right", reopen: { near: "design", direction: "within" } },
|
|
58
|
+
slideshow: { title: "Slideshow", zone: "right", reopen: { near: "design", direction: "within" } },
|
|
59
|
+
} as const satisfies Record<PanelId, PanelDefinition>;
|
|
60
|
+
|
|
61
|
+
export function isPanelId(value: unknown): value is PanelId {
|
|
62
|
+
return typeof value === "string" && (PANEL_IDS as readonly string[]).includes(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function panelsInZone(zone: PanelZone): PanelId[] {
|
|
66
|
+
return PANEL_IDS.filter((id) => PANEL_DEFINITIONS[id].zone === zone);
|
|
67
|
+
}
|
|
@@ -172,7 +172,7 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
172
172
|
return (
|
|
173
173
|
<div className="border-b border-neutral-800 pb-2">
|
|
174
174
|
<div className="flex items-center gap-2 py-1.5">
|
|
175
|
-
<span className="rounded bg-neutral-800 px-1.5 py-0.5 text-[10px] font-medium text-neutral-400">
|
|
175
|
+
<span className="rounded-sm bg-neutral-800 px-1.5 py-0.5 text-[10px] font-medium text-neutral-400">
|
|
176
176
|
Position
|
|
177
177
|
</span>
|
|
178
178
|
<span className="text-[11px] text-neutral-500">
|
|
@@ -201,7 +201,7 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
201
201
|
className="flex w-full items-center gap-2 py-1.5 active:scale-[0.99]"
|
|
202
202
|
>
|
|
203
203
|
<span
|
|
204
|
-
className="rounded bg-panel-accent/10 px-1.5 py-0.5 text-[10px] font-semibold text-panel-accent"
|
|
204
|
+
className="rounded-sm bg-panel-accent/10 px-1.5 py-0.5 text-[10px] font-semibold text-panel-accent"
|
|
205
205
|
title={METHOD_TOOLTIPS[animation.method]}
|
|
206
206
|
>
|
|
207
207
|
{methodLabel}
|
|
@@ -226,7 +226,7 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
226
226
|
height="10"
|
|
227
227
|
viewBox="0 0 10 10"
|
|
228
228
|
fill="currentColor"
|
|
229
|
-
className={`
|
|
229
|
+
className={`shrink-0 transition-transform ${flat ? "text-panel-text-5" : "text-neutral-500"} ${expanded ? "" : "-rotate-90"}`}
|
|
230
230
|
>
|
|
231
231
|
<path d="M2 3l3 4 3-4z" />
|
|
232
232
|
</svg>
|
|
@@ -262,7 +262,7 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
262
262
|
setCopied(true);
|
|
263
263
|
setTimeout(() => setCopied(false), 1500);
|
|
264
264
|
}}
|
|
265
|
-
className="
|
|
265
|
+
className="shrink-0 rounded-sm px-1.5 py-0.5 text-[9px] font-medium text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
|
|
266
266
|
title="Copy description to clipboard — paste into agent prompts"
|
|
267
267
|
>
|
|
268
268
|
{copied ? "Copied" : "Copy"}
|
|
@@ -46,7 +46,7 @@ function RemoveButton({ onClick, title }: { onClick: () => void; title: string }
|
|
|
46
46
|
<button
|
|
47
47
|
type="button"
|
|
48
48
|
onClick={onClick}
|
|
49
|
-
className="relative
|
|
49
|
+
className="relative shrink-0 rounded-sm p-1.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-red-400 active:scale-[0.95]"
|
|
50
50
|
title={title}
|
|
51
51
|
aria-label={title}
|
|
52
52
|
>
|
|
@@ -91,7 +91,7 @@ export function PropertyRow({
|
|
|
91
91
|
role="switch"
|
|
92
92
|
aria-checked={isVisible}
|
|
93
93
|
onClick={() => onCommit(isVisible ? "hidden" : "visible")}
|
|
94
|
-
className="
|
|
94
|
+
className="shrink-0 rounded-full transition-colors duration-200 relative"
|
|
95
95
|
style={{ width: 28, height: 16, background: isVisible ? P.accent : P.borderInput }}
|
|
96
96
|
title={isVisible ? "Visible — click to hide" : "Hidden — click to show"}
|
|
97
97
|
>
|
|
@@ -117,13 +117,13 @@ export function PropertyRow({
|
|
|
117
117
|
<div className="flex flex-col gap-1">
|
|
118
118
|
<div className="flex items-center gap-1">
|
|
119
119
|
<div className="min-w-0 flex-1 flex items-center gap-2 px-2 py-1 rounded-lg bg-neutral-900 border border-neutral-800">
|
|
120
|
-
<span className="
|
|
120
|
+
<span className="shrink-0 text-[11px] font-medium text-neutral-500">
|
|
121
121
|
{PROP_LABELS[prop] ?? prop}
|
|
122
122
|
</span>
|
|
123
123
|
<input
|
|
124
124
|
type="text"
|
|
125
125
|
defaultValue={String(val)}
|
|
126
|
-
className="flex-1 bg-transparent text-[11px] text-neutral-200 outline-
|
|
126
|
+
className="flex-1 bg-transparent text-[11px] text-neutral-200 outline-hidden"
|
|
127
127
|
onBlur={(e) => onCommit(e.currentTarget.value)}
|
|
128
128
|
onKeyDown={(e) => {
|
|
129
129
|
if (e.key === "Enter") e.currentTarget.blur();
|
|
@@ -139,7 +139,7 @@ export function PropertyRow({
|
|
|
139
139
|
key={p.value}
|
|
140
140
|
type="button"
|
|
141
141
|
onClick={() => onCommit(p.value)}
|
|
142
|
-
className="px-1.5 py-0.5 rounded text-[9px] font-medium text-neutral-500 bg-neutral-800/50 hover:bg-neutral-800 hover:text-neutral-300 transition-colors"
|
|
142
|
+
className="px-1.5 py-0.5 rounded-sm text-[9px] font-medium text-neutral-500 bg-neutral-800/50 hover:bg-neutral-800 hover:text-neutral-300 transition-colors"
|
|
143
143
|
>
|
|
144
144
|
{p.label}
|
|
145
145
|
</button>
|
|
@@ -190,7 +190,7 @@ export function AddPropertyTrigger({
|
|
|
190
190
|
return (
|
|
191
191
|
<select
|
|
192
192
|
autoFocus
|
|
193
|
-
className="min-w-0 rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-[11px] text-neutral-100 outline-
|
|
193
|
+
className="min-w-0 rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-[11px] text-neutral-100 outline-hidden"
|
|
194
194
|
defaultValue=""
|
|
195
195
|
onChange={(e) => {
|
|
196
196
|
if (e.target.value) onAdd(e.target.value);
|
|
@@ -222,7 +222,7 @@ function ParamControl({
|
|
|
222
222
|
disabled={disabled}
|
|
223
223
|
aria-label={`${param.label} color`}
|
|
224
224
|
onChange={(e) => onChange(e.target.value)}
|
|
225
|
-
className="w-7 h-7 rounded border border-neutral-700 bg-transparent cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
|
|
225
|
+
className="w-7 h-7 rounded-sm border border-neutral-700 bg-transparent cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
|
|
226
226
|
/>
|
|
227
227
|
<input
|
|
228
228
|
type="text"
|
|
@@ -230,7 +230,7 @@ function ParamControl({
|
|
|
230
230
|
disabled={disabled}
|
|
231
231
|
aria-label={`${param.label} value`}
|
|
232
232
|
onChange={(e) => onChange(e.target.value)}
|
|
233
|
-
className="flex-1 bg-neutral-900 border border-neutral-800 rounded px-2 py-1 text-[10px] text-neutral-200 font-mono focus:outline-
|
|
233
|
+
className="flex-1 bg-neutral-900 border border-neutral-800 rounded-sm px-2 py-1 text-[10px] text-neutral-200 font-mono focus:outline-hidden focus:border-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
234
234
|
/>
|
|
235
235
|
</div>
|
|
236
236
|
)}
|
|
@@ -259,7 +259,7 @@ function ParamControl({
|
|
|
259
259
|
disabled={disabled}
|
|
260
260
|
aria-label={param.label}
|
|
261
261
|
onChange={(e) => onChange(e.target.value)}
|
|
262
|
-
className="w-full bg-neutral-900 border border-neutral-800 rounded px-2 py-1 text-[10px] text-neutral-200 focus:outline-
|
|
262
|
+
className="w-full bg-neutral-900 border border-neutral-800 rounded-sm px-2 py-1 text-[10px] text-neutral-200 focus:outline-hidden focus:border-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
263
263
|
/>
|
|
264
264
|
)}
|
|
265
265
|
|
|
@@ -269,7 +269,7 @@ function ParamControl({
|
|
|
269
269
|
disabled={disabled}
|
|
270
270
|
aria-label={param.label}
|
|
271
271
|
onChange={(e) => onChange(e.target.value)}
|
|
272
|
-
className="w-full bg-neutral-900 border border-neutral-800 rounded px-2 py-1 text-[10px] text-neutral-200 focus:outline-
|
|
272
|
+
className="w-full bg-neutral-900 border border-neutral-800 rounded-sm px-2 py-1 text-[10px] text-neutral-200 focus:outline-hidden focus:border-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
273
273
|
>
|
|
274
274
|
{param.options.map((opt) => (
|
|
275
275
|
<option key={opt.value} value={opt.value}>
|
|
@@ -212,7 +212,7 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
|
|
|
212
212
|
return createPortal(
|
|
213
213
|
<div
|
|
214
214
|
ref={menuRef}
|
|
215
|
-
className="fixed z-
|
|
215
|
+
className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
|
|
216
216
|
style={{ left: adjustedX, top: adjustedY }}
|
|
217
217
|
onPointerDown={stopBubble}
|
|
218
218
|
onMouseDown={stopBubble}
|
|
@@ -29,7 +29,7 @@ export function ComputedTweenNotice({
|
|
|
29
29
|
<button
|
|
30
30
|
type="button"
|
|
31
31
|
onClick={onUnroll}
|
|
32
|
-
className="
|
|
32
|
+
className="shrink-0 rounded-sm px-1.5 py-0.5 text-[9px] font-medium text-panel-accent hover:bg-neutral-800"
|
|
33
33
|
title="Rewrite the helper/loop into explicit tweens so this keyframe edits directly"
|
|
34
34
|
>
|
|
35
35
|
Unroll to edit
|
|
@@ -465,7 +465,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
465
465
|
ref={overlayRef}
|
|
466
466
|
// Standing aside is the only way the caret below can be reached, and is
|
|
467
467
|
// what keeps selection, drag and marquee from firing mid-edit.
|
|
468
|
-
className={`absolute inset-0 z-10 outline-
|
|
468
|
+
className={`absolute inset-0 z-10 outline-hidden ${
|
|
469
469
|
inlineText.editing ? "pointer-events-none" : "pointer-events-auto"
|
|
470
470
|
}`}
|
|
471
471
|
data-editing-text={inlineText.editing ? "true" : undefined}
|
|
@@ -518,7 +518,7 @@ export function EaseCurveSection({
|
|
|
518
518
|
aria-valuemax={1}
|
|
519
519
|
aria-valuenow={key === "p1" ? x1 : x2}
|
|
520
520
|
aria-valuetext={`x ${key === "p1" ? x1 : x2}, y ${key === "p1" ? y1 : y2}`}
|
|
521
|
-
className="cursor-grab stroke-transparent outline-
|
|
521
|
+
className="cursor-grab stroke-transparent outline-hidden active:cursor-grabbing focus-visible:stroke-white focus-visible:stroke-[2px]"
|
|
522
522
|
onPointerDown={(e) => handlePointerDown(key, e)}
|
|
523
523
|
onKeyDown={(event) => handleKeyDown(key, event)}
|
|
524
524
|
onPointerEnter={() => setHover(key)}
|