@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,52 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { PreviewPane } from "./PreviewPane";
|
|
7
|
+
|
|
8
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
vi.mock("../editor/PreviewGuides", () => ({ PreviewGuides: () => <i data-testid="guides" /> }));
|
|
11
|
+
vi.mock("../../player", () => ({ PlayerControls: () => null }));
|
|
12
|
+
vi.mock("./NLEPreview", () => ({ NLEPreview: () => null }));
|
|
13
|
+
vi.mock("./CompositionBreadcrumb", () => ({ CompositionBreadcrumb: () => null }));
|
|
14
|
+
vi.mock("./AssetPreviewOverlay", () => ({ AssetPreviewOverlay: () => null }));
|
|
15
|
+
vi.mock("./usePreviewBlockDrop", () => ({
|
|
16
|
+
usePreviewBlockDrop: () => ({
|
|
17
|
+
isDragOver: false,
|
|
18
|
+
handleDragEnter: vi.fn(),
|
|
19
|
+
handleDragOver: vi.fn(),
|
|
20
|
+
handleDragLeave: vi.fn(),
|
|
21
|
+
handleDrop: vi.fn(),
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
vi.mock("./NLEContext", () => ({
|
|
25
|
+
useNLEContext: () => ({
|
|
26
|
+
projectId: "p",
|
|
27
|
+
iframeRef: { current: null },
|
|
28
|
+
togglePlay: vi.fn(),
|
|
29
|
+
seek: vi.fn(),
|
|
30
|
+
onIframeLoad: vi.fn(),
|
|
31
|
+
compositionStack: [{ previewUrl: undefined }],
|
|
32
|
+
handleNavigateComposition: vi.fn(),
|
|
33
|
+
setCompositionLoading: vi.fn(),
|
|
34
|
+
timelineDisabled: false,
|
|
35
|
+
hasLoadedOnceRef: { current: false },
|
|
36
|
+
previewCompositionSize: null,
|
|
37
|
+
setPreviewCompositionSize: vi.fn(),
|
|
38
|
+
}),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
document.body.innerHTML = "";
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("PreviewPane", () => {
|
|
46
|
+
it("draws the guides in the pane itself, not through the previewOverlay slot", () => {
|
|
47
|
+
const host = document.createElement("div");
|
|
48
|
+
document.body.append(host);
|
|
49
|
+
act(() => createRoot(host).render(<PreviewPane />));
|
|
50
|
+
expect(host.querySelector('[data-testid="guides"]')).not.toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -6,6 +6,7 @@ import { CompositionBreadcrumb } from "./CompositionBreadcrumb";
|
|
|
6
6
|
import { usePreviewBlockDrop } from "./usePreviewBlockDrop";
|
|
7
7
|
import { useNLEContext } from "./NLEContext";
|
|
8
8
|
import { AssetPreviewOverlay } from "./AssetPreviewOverlay";
|
|
9
|
+
import { PreviewGuides } from "../editor/PreviewGuides";
|
|
9
10
|
|
|
10
11
|
function subscribeFullscreen(cb: () => void) {
|
|
11
12
|
document.addEventListener("fullscreenchange", cb);
|
|
@@ -138,15 +139,16 @@ export function PreviewPane({
|
|
|
138
139
|
onCompositionSizeChange={setPreviewCompositionSize}
|
|
139
140
|
/>
|
|
140
141
|
{previewDragOver && (
|
|
141
|
-
<div className="absolute inset-2 z-40 rounded-lg border-2 border-dashed border-studio-accent/50 bg-studio-accent/
|
|
142
|
+
<div className="absolute inset-2 z-40 rounded-lg border-2 border-dashed border-studio-accent/50 bg-studio-accent/4 pointer-events-none" />
|
|
142
143
|
)}
|
|
143
144
|
<AssetPreviewOverlay />
|
|
144
145
|
</div>
|
|
146
|
+
<PreviewGuides iframeRef={iframeRef} />
|
|
145
147
|
{!isFullscreen && previewOverlay}
|
|
146
148
|
</div>
|
|
147
149
|
{/* Transport row: no own background or border — the controls sit flat on
|
|
148
150
|
the preview panel's surface (CapCut-style). */}
|
|
149
|
-
<div className="
|
|
151
|
+
<div className="shrink-0">
|
|
150
152
|
{!isFullscreen && compositionStack.length > 1 && (
|
|
151
153
|
<CompositionBreadcrumb stack={compositionStack} onNavigate={handleNavigateComposition} />
|
|
152
154
|
)}
|
|
@@ -102,6 +102,12 @@ export interface TimelinePaneProps {
|
|
|
102
102
|
) => Promise<void> | void;
|
|
103
103
|
onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
|
|
104
104
|
onSelectTimelineElement?: (element: TimelineElement | null) => void;
|
|
105
|
+
/** Copy/paste/duplicate act on the store's own selection, not a passed
|
|
106
|
+
* element, so unlike onDeleteElement they need no composition-basis wrapper. */
|
|
107
|
+
onCopyClip?: () => boolean;
|
|
108
|
+
onPasteClip?: () => Promise<void>;
|
|
109
|
+
onDuplicateClip?: () => Promise<boolean>;
|
|
110
|
+
canPasteClip?: () => boolean;
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
// fallow-ignore-next-line complexity
|
|
@@ -116,6 +122,10 @@ export function TimelinePane({
|
|
|
116
122
|
onCompositionDrop,
|
|
117
123
|
onBlockedEditAttempt,
|
|
118
124
|
onSelectTimelineElement,
|
|
125
|
+
onCopyClip,
|
|
126
|
+
onPasteClip,
|
|
127
|
+
onDuplicateClip,
|
|
128
|
+
canPasteClip,
|
|
119
129
|
}: TimelinePaneProps) {
|
|
120
130
|
const {
|
|
121
131
|
seek,
|
|
@@ -256,7 +266,7 @@ export function TimelinePane({
|
|
|
256
266
|
{/* Timeline section — inner padding (not margin) keeps the divider's
|
|
257
267
|
height math exact while giving the panel a gap from the shell edges. */}
|
|
258
268
|
<div
|
|
259
|
-
className="relative flex flex-col
|
|
269
|
+
className="relative flex flex-col shrink-0 px-px pb-px"
|
|
260
270
|
style={{ height: timelineH }}
|
|
261
271
|
aria-disabled={timelineDisabled || undefined}
|
|
262
272
|
>
|
|
@@ -270,7 +280,7 @@ export function TimelinePane({
|
|
|
270
280
|
}
|
|
271
281
|
}}
|
|
272
282
|
>
|
|
273
|
-
<div className="
|
|
283
|
+
<div className="shrink-0">{timelineToolbar}</div>
|
|
274
284
|
<Timeline
|
|
275
285
|
sessionEpoch={timelineSessionEpoch}
|
|
276
286
|
onSeek={seek}
|
|
@@ -288,9 +298,13 @@ export function TimelinePane({
|
|
|
288
298
|
onBlockedEditAttempt={onBlockedEditAttempt}
|
|
289
299
|
onSplitElement={handleSplitElement}
|
|
290
300
|
onSelectElement={onSelectTimelineElement}
|
|
301
|
+
onCopyClip={onCopyClip}
|
|
302
|
+
onPasteClip={onPasteClip}
|
|
303
|
+
onDuplicateClip={onDuplicateClip}
|
|
304
|
+
canPasteClip={canPasteClip}
|
|
291
305
|
/>
|
|
292
306
|
</div>
|
|
293
|
-
{timelineFooter && <div className="
|
|
307
|
+
{timelineFooter && <div className="shrink-0">{timelineFooter}</div>}
|
|
294
308
|
{timelineDisabled && (
|
|
295
309
|
<div
|
|
296
310
|
className="absolute inset-0 z-30 cursor-not-allowed bg-black/18 flex items-center justify-center"
|
|
@@ -79,7 +79,7 @@ export function TimelineResizeDivider({
|
|
|
79
79
|
(containerRef.current?.getBoundingClientRect().height ?? 600) - MIN_PREVIEW_H,
|
|
80
80
|
)}
|
|
81
81
|
tabIndex={0}
|
|
82
|
-
className="group relative h-[3px]
|
|
82
|
+
className="group relative h-[3px] shrink-0 cursor-row-resize z-10 outline-hidden focus-visible:bg-studio-accent/20"
|
|
83
83
|
style={{ touchAction: "none" }}
|
|
84
84
|
onPointerDown={handlePointerDown}
|
|
85
85
|
onPointerMove={handlePointerMove}
|
|
@@ -93,7 +93,7 @@ export function TimelineResizeDivider({
|
|
|
93
93
|
the 24px WCAG 2.2 (2.5.8) target because that is all the dead space
|
|
94
94
|
there is: 28px transport buttons sit above and 28px toolbar buttons
|
|
95
95
|
below, and silently stealing their clicks is the worse bug. */}
|
|
96
|
-
<div className="absolute inset-x-0
|
|
96
|
+
<div className="absolute inset-x-0 top-[-4px] h-[10px]" />
|
|
97
97
|
{/* Visible hairline — invisible at rest, subtle wash on hover/drag/focus */}
|
|
98
98
|
<div className="h-[3px] w-full bg-transparent transition-colors group-hover:bg-white/12 group-active:bg-white/18 group-focus-visible:bg-studio-accent/60" />
|
|
99
99
|
</div>
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
afterEach(() => {
|
|
25
25
|
document.body.innerHTML = "";
|
|
26
26
|
// Wrapped in act: mounted harnesses subscribe to the store via
|
|
27
|
-
//
|
|
27
|
+
// useTimelineRowElements, so the reset re-renders them.
|
|
28
28
|
act(() => usePlayerStore.getState().setElements([]));
|
|
29
29
|
});
|
|
30
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
2
|
import { usePlayerStore } from "../../player";
|
|
3
|
-
import {
|
|
3
|
+
import { useTimelineRowElements } from "../../player/hooks/useTimelineRowElements";
|
|
4
4
|
import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
|
|
5
5
|
import {
|
|
6
6
|
displayTrackOrder,
|
|
@@ -41,7 +41,7 @@ export interface MirrorZOrderInput {
|
|
|
41
41
|
* commitZMirrorLaneMove) — the shared key is unique per gesture
|
|
42
42
|
* (zReorderCoalesceKey's gesture seq), so the fold stays gesture-scoped.
|
|
43
43
|
*
|
|
44
|
-
* Element source: `
|
|
44
|
+
* Element source: `useTimelineRowElements()` — the same expanded display
|
|
45
45
|
* set the Timeline renders and the resolver expects (post-normalizeToZones
|
|
46
46
|
* lanes, expanded sub-comp children on their synthetic rows). No new expansion
|
|
47
47
|
* is built here.
|
|
@@ -141,7 +141,7 @@ function useMirrorLaneMoveCommit(): (
|
|
|
141
141
|
coalesceKey: string,
|
|
142
142
|
resolveMove: (element: TimelineElement, elements: TimelineElement[]) => ZMirrorLaneMove,
|
|
143
143
|
) => Promise<boolean> {
|
|
144
|
-
const elements =
|
|
144
|
+
const elements = useTimelineRowElements();
|
|
145
145
|
const elementsRef = useRef(elements);
|
|
146
146
|
elementsRef.current = elements;
|
|
147
147
|
const { onMoveElements } = useTimelineEditContextOptional();
|
|
@@ -435,7 +435,7 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
|
|
|
435
435
|
type="button"
|
|
436
436
|
disabled={retrying}
|
|
437
437
|
onClick={handleRetryPersist}
|
|
438
|
-
className="px-2 py-0.5 text-[10px] rounded bg-red-600 text-white enabled:hover:bg-red-500 enabled:active:scale-[0.97] disabled:opacity-50 transition-colors"
|
|
438
|
+
className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white enabled:hover:bg-red-500 enabled:active:scale-[0.97] disabled:opacity-50 transition-colors"
|
|
439
439
|
>
|
|
440
440
|
{retrying ? "Retrying…" : "Retry"}
|
|
441
441
|
</button>
|
|
@@ -447,7 +447,7 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
|
|
|
447
447
|
type="button"
|
|
448
448
|
onClick={handleUndo}
|
|
449
449
|
title="Undo last slideshow edit (⌘Z)"
|
|
450
|
-
className="px-2 py-0.5 text-[10px] rounded text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 active:scale-[0.97] transition-colors"
|
|
450
|
+
className="px-2 py-0.5 text-[10px] rounded-sm text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 active:scale-[0.97] transition-colors"
|
|
451
451
|
>
|
|
452
452
|
Undo ({undoDepth})
|
|
453
453
|
</button>
|
|
@@ -90,11 +90,11 @@ export function SlideList({
|
|
|
90
90
|
checked={isSlide}
|
|
91
91
|
onChange={() => onToggle(scene.id)}
|
|
92
92
|
onClick={(e) => e.stopPropagation()}
|
|
93
|
-
className="accent-studio-accent
|
|
93
|
+
className="accent-studio-accent shrink-0"
|
|
94
94
|
/>
|
|
95
95
|
<span className="flex-1 truncate">{scene.label || scene.id}</span>
|
|
96
96
|
{isSlide && (
|
|
97
|
-
<span className="flex gap-0.5
|
|
97
|
+
<span className="flex gap-0.5 shrink-0">
|
|
98
98
|
<button
|
|
99
99
|
type="button"
|
|
100
100
|
aria-label="Move slide up"
|
|
@@ -162,7 +162,7 @@ export function SlideInspector({
|
|
|
162
162
|
<div className="flex flex-col gap-1">
|
|
163
163
|
<label className="text-[11px] text-neutral-400">Notes</label>
|
|
164
164
|
<textarea
|
|
165
|
-
className="bg-neutral-800 border border-neutral-700 rounded px-2 py-1.5 text-[11px] text-white resize-none placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-
|
|
165
|
+
className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1.5 text-[11px] text-white resize-none placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
|
|
166
166
|
rows={3}
|
|
167
167
|
placeholder="Speaker notes or script..."
|
|
168
168
|
value={slide?.notes ?? ""}
|
|
@@ -174,7 +174,7 @@ export function SlideInspector({
|
|
|
174
174
|
<span className="text-[11px] text-neutral-400">Fragment hold-points</span>
|
|
175
175
|
<button
|
|
176
176
|
type="button"
|
|
177
|
-
className="text-[10px] px-2 py-0.5 rounded bg-neutral-700 hover:bg-neutral-600 text-neutral-200 transition-colors"
|
|
177
|
+
className="text-[10px] px-2 py-0.5 rounded-sm bg-neutral-700 hover:bg-neutral-600 text-neutral-200 transition-colors"
|
|
178
178
|
onClick={onMarkFragment}
|
|
179
179
|
title={`Mark ${currentTime.toFixed(2)}s as hold-point`}
|
|
180
180
|
>
|
|
@@ -186,7 +186,7 @@ export function SlideInspector({
|
|
|
186
186
|
{fragments.map((t, i) => (
|
|
187
187
|
<span
|
|
188
188
|
key={`frag-${i}`}
|
|
189
|
-
className="inline-flex items-center gap-1 bg-neutral-700 rounded px-1.5 py-0.5 text-[10px] text-neutral-200"
|
|
189
|
+
className="inline-flex items-center gap-1 bg-neutral-700 rounded-sm px-1.5 py-0.5 text-[10px] text-neutral-200"
|
|
190
190
|
>
|
|
191
191
|
{t.toFixed(2)}s
|
|
192
192
|
<button
|
|
@@ -249,7 +249,7 @@ export function BranchTree({
|
|
|
249
249
|
<input
|
|
250
250
|
id={inputId}
|
|
251
251
|
type="text"
|
|
252
|
-
className="flex-1 bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-[11px] text-white placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-
|
|
252
|
+
className="flex-1 bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
|
|
253
253
|
placeholder="New branch name..."
|
|
254
254
|
value={newLabel}
|
|
255
255
|
onChange={(e) => setNewLabel(e.target.value)}
|
|
@@ -260,7 +260,7 @@ export function BranchTree({
|
|
|
260
260
|
/>
|
|
261
261
|
<button
|
|
262
262
|
type="button"
|
|
263
|
-
className="px-2 py-1 rounded bg-neutral-700 hover:bg-neutral-600 text-[11px] text-neutral-200 transition-colors
|
|
263
|
+
className="px-2 py-1 rounded-sm bg-neutral-700 hover:bg-neutral-600 text-[11px] text-neutral-200 transition-colors shrink-0"
|
|
264
264
|
onClick={handleCreate}
|
|
265
265
|
>
|
|
266
266
|
Add
|
|
@@ -322,11 +322,11 @@ function BranchItem({
|
|
|
322
322
|
}, [draft, onRename, seq.id, seq.label]);
|
|
323
323
|
|
|
324
324
|
return (
|
|
325
|
-
<div className="border border-neutral-700/60 rounded p-2 flex flex-col gap-2">
|
|
325
|
+
<div className="border border-neutral-700/60 rounded-sm p-2 flex flex-col gap-2">
|
|
326
326
|
<div className="flex items-center gap-1">
|
|
327
327
|
{editing ? (
|
|
328
328
|
<input
|
|
329
|
-
className="flex-1 bg-neutral-800 border border-neutral-600 rounded px-1.5 py-0.5 text-[11px] text-white focus:border-studio-accent/60 focus:outline-
|
|
329
|
+
className="flex-1 bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-0.5 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
|
|
330
330
|
value={draft}
|
|
331
331
|
autoFocus
|
|
332
332
|
onChange={(e) => setDraft(e.target.value)}
|
|
@@ -364,7 +364,7 @@ function BranchItem({
|
|
|
364
364
|
</button>
|
|
365
365
|
</div>
|
|
366
366
|
{confirmingDelete && (
|
|
367
|
-
<div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex flex-col gap-1 rounded-
|
|
367
|
+
<div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex flex-col gap-1 rounded-xs">
|
|
368
368
|
<span className="text-[10px] text-red-400">
|
|
369
369
|
Delete branch “{seq.label}”
|
|
370
370
|
{seq.slides.length > 0
|
|
@@ -379,14 +379,14 @@ function BranchItem({
|
|
|
379
379
|
setConfirmingDelete(false);
|
|
380
380
|
onDelete(seq.id);
|
|
381
381
|
}}
|
|
382
|
-
className="px-2 py-0.5 text-[10px] rounded bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
|
|
382
|
+
className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
|
|
383
383
|
>
|
|
384
384
|
Delete
|
|
385
385
|
</button>
|
|
386
386
|
<button
|
|
387
387
|
type="button"
|
|
388
388
|
onClick={() => setConfirmingDelete(false)}
|
|
389
|
-
className="px-2 py-0.5 text-[10px] rounded text-neutral-400 hover:text-neutral-200 transition-colors"
|
|
389
|
+
className="px-2 py-0.5 text-[10px] rounded-sm text-neutral-400 hover:text-neutral-200 transition-colors"
|
|
390
390
|
>
|
|
391
391
|
Cancel
|
|
392
392
|
</button>
|
|
@@ -407,7 +407,7 @@ function BranchItem({
|
|
|
407
407
|
aria-label={`Assign ${scene.label || scene.id} to branch ${seq.label}`}
|
|
408
408
|
checked={assigned}
|
|
409
409
|
onChange={(e) => onAssign(seq.id, scene.id, e.target.checked)}
|
|
410
|
-
className="accent-studio-accent
|
|
410
|
+
className="accent-studio-accent shrink-0"
|
|
411
411
|
/>
|
|
412
412
|
{assigned ? (
|
|
413
413
|
<button
|
|
@@ -497,7 +497,7 @@ export function HotspotTool({
|
|
|
497
497
|
<label className="text-[11px] text-neutral-400">Hotspot label</label>
|
|
498
498
|
<input
|
|
499
499
|
type="text"
|
|
500
|
-
className="bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-[11px] text-white placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-
|
|
500
|
+
className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
|
|
501
501
|
placeholder="Button label..."
|
|
502
502
|
value={hotspotLabel}
|
|
503
503
|
onChange={(e) => setHotspotLabel(e.target.value)}
|
|
@@ -505,7 +505,7 @@ export function HotspotTool({
|
|
|
505
505
|
/>
|
|
506
506
|
<label className="text-[11px] text-neutral-400">Target branch</label>
|
|
507
507
|
<select
|
|
508
|
-
className="bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-
|
|
508
|
+
className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
|
|
509
509
|
value={targetSequenceId}
|
|
510
510
|
onChange={(e) => setTargetSequenceId(e.target.value)}
|
|
511
511
|
aria-label="Target branch sequence"
|
|
@@ -527,7 +527,7 @@ export function HotspotTool({
|
|
|
527
527
|
? "Choose a target branch first"
|
|
528
528
|
: undefined
|
|
529
529
|
}
|
|
530
|
-
className="px-3 py-1.5 rounded bg-studio-accent/80 enabled:hover:bg-studio-accent enabled:active:scale-[0.98] text-white text-[11px] font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
530
|
+
className="px-3 py-1.5 rounded-sm bg-studio-accent/80 enabled:hover:bg-studio-accent enabled:active:scale-[0.98] text-white text-[11px] font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
531
531
|
onClick={handleMakeHotspot}
|
|
532
532
|
>
|
|
533
533
|
Make hotspot
|
|
@@ -540,7 +540,10 @@ export function HotspotTool({
|
|
|
540
540
|
{hotspots.map((h) => {
|
|
541
541
|
const seqLabel = sequences.find((s) => s.id === h.target)?.label ?? h.target;
|
|
542
542
|
return (
|
|
543
|
-
<div
|
|
543
|
+
<div
|
|
544
|
+
key={h.id}
|
|
545
|
+
className="flex items-center gap-2 bg-neutral-800 rounded-sm px-2 py-1"
|
|
546
|
+
>
|
|
544
547
|
<span className="flex-1 text-[11px] text-neutral-200 truncate">
|
|
545
548
|
{h.label} → <span className="text-neutral-400">{seqLabel}</span>
|
|
546
549
|
</span>
|
|
@@ -227,7 +227,7 @@ export function VariablesBindElement({
|
|
|
227
227
|
<button
|
|
228
228
|
type="button"
|
|
229
229
|
onClick={() => setActiveKey(null)}
|
|
230
|
-
className="h-6 rounded px-2 text-[10px] text-neutral-500 hover:text-neutral-300"
|
|
230
|
+
className="h-6 rounded-sm px-2 text-[10px] text-neutral-500 hover:text-neutral-300"
|
|
231
231
|
>
|
|
232
232
|
Cancel
|
|
233
233
|
</button>
|
|
@@ -238,7 +238,7 @@ export function VariablesBindElement({
|
|
|
238
238
|
setActiveKey(null);
|
|
239
239
|
onBind(active, trimmedId);
|
|
240
240
|
}}
|
|
241
|
-
className="h-6 rounded bg-neutral-800 px-2 text-[10px] font-medium text-neutral-200 hover:bg-neutral-700 disabled:cursor-not-allowed disabled:opacity-40"
|
|
241
|
+
className="h-6 rounded-sm bg-neutral-800 px-2 text-[10px] font-medium text-neutral-200 hover:bg-neutral-700 disabled:cursor-not-allowed disabled:opacity-40"
|
|
242
242
|
>
|
|
243
243
|
{existingDecl ? "Bind anyway" : "Bind"}
|
|
244
244
|
</button>
|
|
@@ -304,14 +304,14 @@ export function DeclarationForm({
|
|
|
304
304
|
<button
|
|
305
305
|
type="button"
|
|
306
306
|
onClick={onCancel}
|
|
307
|
-
className="h-6 rounded px-2 text-[10px] text-neutral-500 hover:text-neutral-300"
|
|
307
|
+
className="h-6 rounded-sm px-2 text-[10px] text-neutral-500 hover:text-neutral-300"
|
|
308
308
|
>
|
|
309
309
|
Cancel
|
|
310
310
|
</button>
|
|
311
311
|
<button
|
|
312
312
|
type="button"
|
|
313
313
|
onClick={submit}
|
|
314
|
-
className="h-6 rounded bg-neutral-800 px-2 text-[10px] font-medium text-neutral-200 hover:bg-neutral-700"
|
|
314
|
+
className="h-6 rounded-sm bg-neutral-800 px-2 text-[10px] font-medium text-neutral-200 hover:bg-neutral-700"
|
|
315
315
|
>
|
|
316
316
|
{submitLabel}
|
|
317
317
|
</button>
|
|
@@ -51,7 +51,7 @@ function CompositionSection({
|
|
|
51
51
|
<span className="truncate text-[10px] font-medium text-neutral-300">
|
|
52
52
|
{decl.label}
|
|
53
53
|
</span>
|
|
54
|
-
<span className="rounded bg-neutral-800 px-1 py-px font-mono text-[8px] text-neutral-500">
|
|
54
|
+
<span className="rounded-sm bg-neutral-800 px-1 py-px font-mono text-[8px] text-neutral-500">
|
|
55
55
|
{decl.type}
|
|
56
56
|
</span>
|
|
57
57
|
<span className="ml-auto flex items-center gap-1">
|
|
@@ -96,12 +96,12 @@ function VariableRow({
|
|
|
96
96
|
<div className="space-y-1.5 rounded-lg border border-neutral-800/70 p-2">
|
|
97
97
|
<div className="flex items-center gap-1.5">
|
|
98
98
|
<span className="truncate text-[10px] font-medium text-neutral-300">{decl.label}</span>
|
|
99
|
-
<span className="rounded bg-neutral-800 px-1 py-px font-mono text-[8px] text-neutral-500">
|
|
99
|
+
<span className="rounded-sm bg-neutral-800 px-1 py-px font-mono text-[8px] text-neutral-500">
|
|
100
100
|
{decl.type}
|
|
101
101
|
</span>
|
|
102
102
|
{unused && (
|
|
103
103
|
<span
|
|
104
|
-
className="rounded bg-amber-900/40 px-1 py-px text-[8px] text-amber-400"
|
|
104
|
+
className="rounded-sm bg-amber-900/40 px-1 py-px text-[8px] text-amber-400"
|
|
105
105
|
title="No script reads this variable"
|
|
106
106
|
>
|
|
107
107
|
unused
|
|
@@ -189,7 +189,7 @@ function PreviewModeHeader({
|
|
|
189
189
|
<button
|
|
190
190
|
type="button"
|
|
191
191
|
onClick={onReset}
|
|
192
|
-
className="h-6 rounded px-2 text-[10px] text-neutral-400 hover:text-neutral-200"
|
|
192
|
+
className="h-6 rounded-sm px-2 text-[10px] text-neutral-400 hover:text-neutral-200"
|
|
193
193
|
>
|
|
194
194
|
Reset
|
|
195
195
|
</button>
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
} from "@hyperframes/sdk";
|
|
15
15
|
|
|
16
16
|
export const VARIABLES_INPUT_CLASS =
|
|
17
|
-
"w-full bg-neutral-900 border border-neutral-800 rounded px-2 py-1 text-[10px] text-neutral-200 focus:outline-
|
|
17
|
+
"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";
|
|
18
18
|
|
|
19
19
|
/** Text input that drafts locally and commits on blur/Enter. */
|
|
20
20
|
function DraftTextInput({
|
|
@@ -106,7 +106,7 @@ function ColorControl({
|
|
|
106
106
|
if (draft !== null && draft !== colorValue) onCommit(draft);
|
|
107
107
|
setDraft(null);
|
|
108
108
|
}}
|
|
109
|
-
className="h-6 w-6 cursor-pointer rounded border border-neutral-700 bg-transparent"
|
|
109
|
+
className="h-6 w-6 cursor-pointer rounded-sm border border-neutral-700 bg-transparent"
|
|
110
110
|
/>
|
|
111
111
|
<DraftTextInput
|
|
112
112
|
value={colorValue}
|
|
@@ -8,7 +8,7 @@ const CUE_MS = 1600;
|
|
|
8
8
|
// Matches the focus treatment every other button in this panel uses. A control
|
|
9
9
|
// the keyboard can reach but not see focus on is unusable without a mouse.
|
|
10
10
|
const FOCUS_RING =
|
|
11
|
-
"outline-
|
|
11
|
+
"outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Shown above Export when the dev server reports no usable FFmpeg.
|
|
@@ -81,7 +81,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
|
|
|
81
81
|
|
|
82
82
|
{status.command ? (
|
|
83
83
|
<div className="flex items-center gap-1.5">
|
|
84
|
-
<code className="flex-1 overflow-x-auto whitespace-nowrap rounded-
|
|
84
|
+
<code className="flex-1 overflow-x-auto whitespace-nowrap rounded-xs bg-black/40 px-2 py-1 text-[10px] text-panel-text-2">
|
|
85
85
|
{status.command}
|
|
86
86
|
</code>
|
|
87
87
|
{/* Fixed width so swapping the label to "Copied" cannot shift the
|
|
@@ -89,7 +89,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
|
|
|
89
89
|
<button
|
|
90
90
|
type="button"
|
|
91
91
|
onClick={() => void copy(status.command ?? "")}
|
|
92
|
-
className={`h-6 w-14
|
|
92
|
+
className={`h-6 w-14 shrink-0 rounded-xs border border-amber-500/30 text-[10px] font-medium text-amber-200 transition-colors hover:bg-amber-500/20 active:scale-[0.98] ${FOCUS_RING}`}
|
|
93
93
|
>
|
|
94
94
|
{copied ? "Copied" : "Copy"}
|
|
95
95
|
</button>
|
|
@@ -107,7 +107,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
|
|
|
107
107
|
type="button"
|
|
108
108
|
onClick={onRecheck}
|
|
109
109
|
disabled={checking}
|
|
110
|
-
className={`rounded-
|
|
110
|
+
className={`rounded-xs py-0.5 text-[10px] font-medium text-amber-200 underline-offset-2 transition-colors hover:underline disabled:opacity-50 ${FOCUS_RING}`}
|
|
111
111
|
>
|
|
112
112
|
{checking ? "Checking…" : "Recheck"}
|
|
113
113
|
</button>
|
|
@@ -115,7 +115,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
|
|
|
115
115
|
href={DOWNLOAD_URL}
|
|
116
116
|
target="_blank"
|
|
117
117
|
rel="noreferrer"
|
|
118
|
-
className={`rounded-
|
|
118
|
+
className={`rounded-xs py-0.5 text-[10px] text-panel-text-2 underline-offset-2 transition-colors hover:text-panel-text-0 hover:underline ${FOCUS_RING}`}
|
|
119
119
|
>
|
|
120
120
|
Other install options
|
|
121
121
|
</a>
|
|
@@ -81,6 +81,13 @@ function exportButtonIn(host: HTMLElement): HTMLButtonElement {
|
|
|
81
81
|
return button;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
describe("RenderQueue Export button", () => {
|
|
85
|
+
it("leaves the font size to the shared Button", () => {
|
|
86
|
+
const host = mountRenderQueue(vi.fn());
|
|
87
|
+
expect(exportButtonIn(host).className).not.toMatch(/text-\[/);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
84
91
|
describe("RenderQueue FFmpeg gate", () => {
|
|
85
92
|
it("refuses Export and shows the install command when the server reports no FFmpeg", () => {
|
|
86
93
|
ffmpegStatus = {
|
|
@@ -207,7 +207,7 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
|
|
|
207
207
|
onFocus={show}
|
|
208
208
|
onBlur={hide}
|
|
209
209
|
onClick={() => setOpen((prev) => !prev)}
|
|
210
|
-
className="flex items-center justify-center p-0.5 -m-0.5 rounded text-panel-text-5 hover:text-panel-text-3 transition-colors cursor-help outline-
|
|
210
|
+
className="flex items-center justify-center p-0.5 -m-0.5 rounded-sm text-panel-text-5 hover:text-panel-text-3 transition-colors cursor-help outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent"
|
|
211
211
|
>
|
|
212
212
|
<svg
|
|
213
213
|
width="12"
|
|
@@ -232,7 +232,7 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
|
|
|
232
232
|
role="tooltip"
|
|
233
233
|
onPointerEnter={show}
|
|
234
234
|
onPointerLeave={hide}
|
|
235
|
-
className="fixed w-52 p-2 rounded bg-panel-input border border-neutral-700 shadow-lg z-
|
|
235
|
+
className="fixed w-52 p-2 rounded-sm bg-panel-input border border-neutral-700 shadow-lg z-200"
|
|
236
236
|
style={{ left: position?.left ?? -9999, top: position?.top ?? -9999 }}
|
|
237
237
|
>
|
|
238
238
|
<p className="text-[10px] font-semibold text-panel-text-1 mb-0.5">{info.label}</p>
|
|
@@ -303,7 +303,7 @@ function FormatExportButton({
|
|
|
303
303
|
const showQuality = format !== "mov";
|
|
304
304
|
|
|
305
305
|
const selectCls =
|
|
306
|
-
"h-7 w-full px-2 text-[11px] bg-panel-input rounded-md text-panel-text-1 outline-
|
|
306
|
+
"h-7 w-full px-2 text-[11px] bg-panel-input rounded-md text-panel-text-1 outline-hidden cursor-pointer disabled:opacity-50 hover:bg-panel-hover transition-colors";
|
|
307
307
|
|
|
308
308
|
return (
|
|
309
309
|
<div className="flex flex-col gap-3">
|
|
@@ -407,7 +407,7 @@ function FormatExportButton({
|
|
|
407
407
|
trackStudioEvent("render_start", { format, quality, resolution: outputResolution, fps });
|
|
408
408
|
void onStartRender(format, quality, outputResolution, fps);
|
|
409
409
|
}}
|
|
410
|
-
className="w-full
|
|
410
|
+
className="w-full font-semibold"
|
|
411
411
|
>
|
|
412
412
|
{isRendering ? "Rendering…" : "Export"}
|
|
413
413
|
</Button>
|
|
@@ -454,7 +454,7 @@ export const RenderQueue = memo(function RenderQueue({
|
|
|
454
454
|
|
|
455
455
|
return (
|
|
456
456
|
<div className="flex flex-col h-full">
|
|
457
|
-
<div className="px-3 py-3 border-b border-panel-border
|
|
457
|
+
<div className="px-3 py-3 border-b border-panel-border shrink-0">
|
|
458
458
|
<FormatExportButton
|
|
459
459
|
onStartRender={onStartRender}
|
|
460
460
|
isRendering={isRendering}
|
|
@@ -476,7 +476,7 @@ export const RenderQueue = memo(function RenderQueue({
|
|
|
476
476
|
<button
|
|
477
477
|
onClick={onDismissActionError}
|
|
478
478
|
aria-label="Dismiss error"
|
|
479
|
-
className="text-[10px] text-panel-text-4 hover:text-panel-text-2
|
|
479
|
+
className="text-[10px] text-panel-text-4 hover:text-panel-text-2 shrink-0"
|
|
480
480
|
>
|
|
481
481
|
✕
|
|
482
482
|
</button>
|
|
@@ -77,8 +77,8 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
77
77
|
disabled={!isComplete}
|
|
78
78
|
aria-label={isComplete ? `Open ${job.filename} in a new tab` : undefined}
|
|
79
79
|
className={[
|
|
80
|
-
"w-20 h-[45px] rounded-md overflow-hidden bg-panel-input
|
|
81
|
-
"outline-
|
|
80
|
+
"w-20 h-[45px] rounded-md overflow-hidden bg-panel-input shrink-0 relative",
|
|
81
|
+
"outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent",
|
|
82
82
|
isComplete ? "cursor-pointer" : "cursor-default",
|
|
83
83
|
].join(" ")}
|
|
84
84
|
>
|
|
@@ -130,7 +130,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
130
130
|
{job.filename}
|
|
131
131
|
</span>
|
|
132
132
|
{job.durationMs && (
|
|
133
|
-
<span className="text-[9px] text-panel-text-5
|
|
133
|
+
<span className="text-[9px] text-panel-text-5 shrink-0">
|
|
134
134
|
{formatDuration(job.durationMs)}
|
|
135
135
|
</span>
|
|
136
136
|
)}
|
|
@@ -171,7 +171,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
171
171
|
</div>
|
|
172
172
|
|
|
173
173
|
{/* Actions — always visible to prevent layout shifts */}
|
|
174
|
-
<div className="flex items-center gap-1.5
|
|
174
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
175
175
|
{isRendering ? (
|
|
176
176
|
<Button
|
|
177
177
|
size="sm"
|
|
@@ -211,7 +211,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
211
211
|
<>
|
|
212
212
|
<button
|
|
213
213
|
onClick={isComplete ? handleDownload : undefined}
|
|
214
|
-
className={`p-1.5 min-w-6 min-h-6 rounded transition-colors outline-
|
|
214
|
+
className={`p-1.5 min-w-6 min-h-6 rounded transition-colors outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent ${
|
|
215
215
|
isComplete
|
|
216
216
|
? "text-panel-text-5 hover:text-panel-accent"
|
|
217
217
|
: "text-panel-text-5/30 cursor-default"
|
|
@@ -240,7 +240,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
240
240
|
e.stopPropagation();
|
|
241
241
|
setConfirmingDelete(true);
|
|
242
242
|
}}
|
|
243
|
-
className="p-1.5 min-w-6 min-h-6 rounded text-panel-text-5 hover:text-red-400 transition-colors outline-
|
|
243
|
+
className="p-1.5 min-w-6 min-h-6 rounded-sm text-panel-text-5 hover:text-red-400 transition-colors outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent"
|
|
244
244
|
title="Delete render file"
|
|
245
245
|
aria-label={`Delete ${job.filename}`}
|
|
246
246
|
>
|