@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
|
@@ -18,6 +18,26 @@ interface ServerConnectionState {
|
|
|
18
18
|
* Polls every 2 s until the server responds, then transitions automatically.
|
|
19
19
|
* Cleans up pending timers on unmount so it is safe under React StrictMode.
|
|
20
20
|
*/
|
|
21
|
+
/**
|
|
22
|
+
* Whether a hash-supplied project id still names something the server can
|
|
23
|
+
* resolve. Three answers, not two: a failed request must NOT be read as
|
|
24
|
+
* "missing", or one network blip would discard a perfectly good deep link.
|
|
25
|
+
*
|
|
26
|
+
* Resolved through `/api/projects/:id`, which calls the same
|
|
27
|
+
* `adapter.resolveProject` the file routes use. A match against the
|
|
28
|
+
* `/api/projects` list would be wrong twice over: that list omits session ids
|
|
29
|
+
* (which resolve fine) and skips project dirs without an `index.html`.
|
|
30
|
+
*/
|
|
31
|
+
async function resolveHashProject(id: string): Promise<"ok" | "missing" | "unknown"> {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`/api/projects/${encodeURIComponent(id)}`);
|
|
34
|
+
if (res.ok) return "ok";
|
|
35
|
+
return res.status === 404 ? "missing" : "unknown";
|
|
36
|
+
} catch {
|
|
37
|
+
return "unknown";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
export function useServerConnection(): ServerConnectionState {
|
|
22
42
|
const [projectId, setProjectId] = useState<string | null>(null);
|
|
23
43
|
const [resolving, setResolving] = useState(true);
|
|
@@ -39,21 +59,34 @@ export function useServerConnection(): ServerConnectionState {
|
|
|
39
59
|
function tryConnect() {
|
|
40
60
|
fetch("/api/projects")
|
|
41
61
|
.then((r) => r.json())
|
|
42
|
-
.then((data) => {
|
|
62
|
+
.then(async (data) => {
|
|
43
63
|
if (cancelled) return;
|
|
64
|
+
// A hash project id outlives the project it names — a renamed folder,
|
|
65
|
+
// or a bookmark from a project that is gone. Trusting it blindly made
|
|
66
|
+
// every later /api/projects/<id>/... request 404 for the life of the
|
|
67
|
+
// tab, including the composition read that opens the SDK session, so
|
|
68
|
+
// every edit fell back to the server path with nothing to show why.
|
|
69
|
+
// Telemetry after the read-reason split: 120 http_error/404 reads
|
|
70
|
+
// across 5 users in 24h, ~24 each, never recovering.
|
|
44
71
|
if (hashProjectId) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
setProjectId(
|
|
72
|
+
const state = await resolveHashProject(hashProjectId);
|
|
73
|
+
if (cancelled) return;
|
|
74
|
+
// "unknown" keeps the old behaviour: a transient failure must not
|
|
75
|
+
// rewrite the user's hash out from under a valid project.
|
|
76
|
+
if (state !== "missing") {
|
|
77
|
+
setProjectId(hashProjectId);
|
|
51
78
|
setWaitingForServer(false);
|
|
52
|
-
|
|
53
|
-
} else {
|
|
54
|
-
scheduleRetry();
|
|
79
|
+
return;
|
|
55
80
|
}
|
|
56
81
|
}
|
|
82
|
+
const first = (data.projects ?? [])[0];
|
|
83
|
+
if (first) {
|
|
84
|
+
setProjectId(first.id);
|
|
85
|
+
setWaitingForServer(false);
|
|
86
|
+
window.location.hash = buildProjectHash(first.id);
|
|
87
|
+
} else {
|
|
88
|
+
scheduleRetry();
|
|
89
|
+
}
|
|
57
90
|
})
|
|
58
91
|
.catch(() => {
|
|
59
92
|
if (!cancelled) scheduleRetry();
|
|
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
5
5
|
import { useTimelineAssetDropOps } from "./useTimelineAssetDropOps";
|
|
6
6
|
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
7
7
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
8
|
+
import type { TimelineElement } from "../player";
|
|
8
9
|
|
|
9
10
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
11
|
|
|
@@ -19,6 +20,7 @@ type DropFn = ReturnType<typeof useTimelineAssetDropOps>["handleTimelineAssetDro
|
|
|
19
20
|
function renderDropHook(
|
|
20
21
|
sourceContent: string,
|
|
21
22
|
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>,
|
|
23
|
+
timelineElements: TimelineElement[] = [],
|
|
22
24
|
) {
|
|
23
25
|
vi.stubGlobal(
|
|
24
26
|
"fetch",
|
|
@@ -29,7 +31,7 @@ function renderDropHook(
|
|
|
29
31
|
const { handleTimelineAssetDrop } = useTimelineAssetDropOps({
|
|
30
32
|
projectIdRef: { current: "project" },
|
|
31
33
|
activeCompPath: "index.html",
|
|
32
|
-
timelineElements
|
|
34
|
+
timelineElements,
|
|
33
35
|
showToast: vi.fn(),
|
|
34
36
|
writeProjectFile,
|
|
35
37
|
recordEdit: vi.fn().mockResolvedValue(undefined),
|
|
@@ -84,4 +86,42 @@ describe("useTimelineAssetDropOps handleTimelineAssetDrop", () => {
|
|
|
84
86
|
|
|
85
87
|
expect(usePlayerStore.getState().selectedElementId).toBe("index.html#clip");
|
|
86
88
|
});
|
|
89
|
+
|
|
90
|
+
it("measures the insert row against the visible rows, not clips the timeline hides", async () => {
|
|
91
|
+
const clip = (id: string, track: number): TimelineElement => ({
|
|
92
|
+
id,
|
|
93
|
+
key: id,
|
|
94
|
+
tag: "div",
|
|
95
|
+
start: 0,
|
|
96
|
+
duration: 2,
|
|
97
|
+
track,
|
|
98
|
+
authoredTrack: track,
|
|
99
|
+
hfId: `hf-${id}`,
|
|
100
|
+
domId: id,
|
|
101
|
+
});
|
|
102
|
+
const source = [
|
|
103
|
+
'<main data-composition-id="scene" data-duration="10" data-width="1920" data-height="1080">',
|
|
104
|
+
...["a:0", "hidden:1", "b:2"].map((t) => {
|
|
105
|
+
const [id, track] = t.split(":");
|
|
106
|
+
return `<div data-hf-id="hf-${id}" id="${id}" data-start="0" data-track-index="${track}"></div>`;
|
|
107
|
+
}),
|
|
108
|
+
"</main>",
|
|
109
|
+
].join("\n");
|
|
110
|
+
usePlayerStore.getState().setTopLevelIds(new Set(["a", "b"]));
|
|
111
|
+
const writeProjectFile = vi.fn().mockResolvedValue(undefined);
|
|
112
|
+
const getDrop = renderDropHook(source, writeProjectFile, [
|
|
113
|
+
clip("a", 0),
|
|
114
|
+
clip("hidden", 1),
|
|
115
|
+
clip("b", 2),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
await act(async () => {
|
|
119
|
+
await getDrop()("clip.mp4", { start: 1, track: 1, insertRow: 1, trackOrder: [0, 2] }, 2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const [, written] = writeProjectFile.mock.calls[0] as [string, string];
|
|
123
|
+
expect(written).toContain('id="b" data-start="0" data-track-index="2"');
|
|
124
|
+
expect(written).toContain('id="hidden" data-start="0" data-track-index="1"');
|
|
125
|
+
expect(written).toMatch(/<video id="clip"[^>]*data-track-index="1"/);
|
|
126
|
+
});
|
|
87
127
|
});
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// 600-line cap.
|
|
5
5
|
import { useCallback, type MutableRefObject, type RefObject } from "react";
|
|
6
6
|
import type { TimelineElement } from "../player";
|
|
7
|
+
import type { TimelineDropPlacement } from "../player/components/timelineCallbacks";
|
|
8
|
+
import { resolveDropTrack } from "../utils/timelineDropTrackInsert";
|
|
7
9
|
import {
|
|
8
10
|
buildTimelineAssetId,
|
|
9
11
|
buildTimelineAssetInsertHtml,
|
|
@@ -23,6 +25,18 @@ import { commitTimelineCompositionInsertion } from "../utils/timelineComposition
|
|
|
23
25
|
import { extendRootDurationInSource } from "../utils/rootDuration";
|
|
24
26
|
import { deriveTimelineStoreKeyForDomId } from "../player/lib/timelineElementHelpers";
|
|
25
27
|
import { selectAndRevealTimelineElement } from "../player/components/timelineDropReveal";
|
|
28
|
+
import { selectTimelineRowElements } from "../player/hooks/useTimelineRowElements";
|
|
29
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
30
|
+
|
|
31
|
+
/** The first uploaded file opens the new track (if asked); the rest land on the lane it landed on. */
|
|
32
|
+
function fileDropPlacement(
|
|
33
|
+
index: number,
|
|
34
|
+
next: { start: number; track: number },
|
|
35
|
+
dropped: TimelineDropPlacement | undefined,
|
|
36
|
+
landedTrack: number | undefined,
|
|
37
|
+
): TimelineDropPlacement {
|
|
38
|
+
return index === 0 ? { ...dropped, ...next } : { ...next, track: landedTrack ?? next.track };
|
|
39
|
+
}
|
|
26
40
|
|
|
27
41
|
interface UseTimelineAssetDropOpsOptions {
|
|
28
42
|
projectIdRef: MutableRefObject<string | null>;
|
|
@@ -52,16 +66,16 @@ export function useTimelineAssetDropOps({
|
|
|
52
66
|
observeProjectFileVersion,
|
|
53
67
|
}: UseTimelineAssetDropOpsOptions) {
|
|
54
68
|
// fallow-ignore-next-line complexity
|
|
55
|
-
const
|
|
69
|
+
const dropAssetAt = useCallback(
|
|
56
70
|
// fallow-ignore-next-line complexity
|
|
57
71
|
async (
|
|
58
72
|
assetPath: string,
|
|
59
|
-
placement:
|
|
73
|
+
placement: TimelineDropPlacement,
|
|
60
74
|
durationOverride?: number,
|
|
61
|
-
) => {
|
|
75
|
+
): Promise<number | undefined> => {
|
|
62
76
|
if (isRecordingRef?.current) {
|
|
63
77
|
showToast("Cannot edit timeline while recording", "error");
|
|
64
|
-
return;
|
|
78
|
+
return undefined;
|
|
65
79
|
}
|
|
66
80
|
const pid = projectIdRef.current;
|
|
67
81
|
if (!pid) throw new Error("No active project");
|
|
@@ -69,7 +83,7 @@ export function useTimelineAssetDropOps({
|
|
|
69
83
|
const kind = getTimelineAssetKind(assetPath);
|
|
70
84
|
if (!kind) {
|
|
71
85
|
showToast("Only image, video, and audio assets can be dropped onto the timeline.");
|
|
72
|
-
return;
|
|
86
|
+
return undefined;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
const targetPath = activeCompPath || "index.html";
|
|
@@ -91,9 +105,24 @@ export function useTimelineAssetDropOps({
|
|
|
91
105
|
);
|
|
92
106
|
const newElementZIndex = Math.max(1, relevantElements.length + 1);
|
|
93
107
|
|
|
108
|
+
const { source: sourceWithRoom, track } = resolveDropTrack({
|
|
109
|
+
source: originalContent,
|
|
110
|
+
// insertRow counts the rows the timeline shows, so plan against those.
|
|
111
|
+
elements: selectTimelineRowElements(
|
|
112
|
+
relevantElements,
|
|
113
|
+
usePlayerStore.getState().topLevelIds,
|
|
114
|
+
),
|
|
115
|
+
placement,
|
|
116
|
+
dropped: {
|
|
117
|
+
id: newId,
|
|
118
|
+
tag: kind === "image" ? "img" : kind,
|
|
119
|
+
start: normalizedStart,
|
|
120
|
+
duration: normalizedDuration,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
94
123
|
const patchedContent = extendRootDurationInSource(
|
|
95
124
|
insertTimelineAssetIntoSource(
|
|
96
|
-
|
|
125
|
+
sourceWithRoom,
|
|
97
126
|
buildTimelineAssetInsertHtml({
|
|
98
127
|
id: newId,
|
|
99
128
|
hfId: `hf-${generateId()}`,
|
|
@@ -101,7 +130,7 @@ export function useTimelineAssetDropOps({
|
|
|
101
130
|
kind,
|
|
102
131
|
start: normalizedStart,
|
|
103
132
|
duration: normalizedDuration,
|
|
104
|
-
track
|
|
133
|
+
track,
|
|
105
134
|
zIndex: newElementZIndex,
|
|
106
135
|
geometry: fitTimelineAssetGeometry(
|
|
107
136
|
null,
|
|
@@ -125,10 +154,12 @@ export function useTimelineAssetDropOps({
|
|
|
125
154
|
selectAndRevealTimelineElement(deriveTimelineStoreKeyForDomId(newId, targetPath));
|
|
126
155
|
forceReloadSdkSession?.();
|
|
127
156
|
reloadPreview();
|
|
157
|
+
return track;
|
|
128
158
|
} catch (error) {
|
|
129
159
|
const message =
|
|
130
160
|
error instanceof Error ? error.message : "Failed to drop asset onto timeline";
|
|
131
161
|
showToast(message);
|
|
162
|
+
return undefined;
|
|
132
163
|
}
|
|
133
164
|
},
|
|
134
165
|
[
|
|
@@ -144,10 +175,17 @@ export function useTimelineAssetDropOps({
|
|
|
144
175
|
],
|
|
145
176
|
);
|
|
146
177
|
|
|
178
|
+
const handleTimelineAssetDrop = useCallback(
|
|
179
|
+
async (assetPath: string, placement: TimelineDropPlacement, durationOverride?: number) => {
|
|
180
|
+
await dropAssetAt(assetPath, placement, durationOverride);
|
|
181
|
+
},
|
|
182
|
+
[dropAssetAt],
|
|
183
|
+
);
|
|
184
|
+
|
|
147
185
|
// fallow-ignore-next-line complexity
|
|
148
186
|
const handleTimelineFileDrop = useCallback(
|
|
149
187
|
// fallow-ignore-next-line complexity
|
|
150
|
-
async (files: File[], placement?:
|
|
188
|
+
async (files: File[], placement?: TimelineDropPlacement) => {
|
|
151
189
|
if (isRecordingRef?.current) {
|
|
152
190
|
showToast("Cannot edit timeline while recording", "error");
|
|
153
191
|
return;
|
|
@@ -166,15 +204,18 @@ export function useTimelineAssetDropOps({
|
|
|
166
204
|
placement ?? { start: 0, track: 0 },
|
|
167
205
|
durations,
|
|
168
206
|
);
|
|
207
|
+
let landedTrack: number | undefined;
|
|
169
208
|
for (const [index, assetPath] of uploaded.entries()) {
|
|
170
|
-
|
|
209
|
+
const next = placements[index] ?? placements[0];
|
|
210
|
+
const track = await dropAssetAt(
|
|
171
211
|
assetPath,
|
|
172
|
-
|
|
212
|
+
fileDropPlacement(index, next, placement, landedTrack),
|
|
173
213
|
durations[index],
|
|
174
214
|
);
|
|
215
|
+
if (index === 0) landedTrack = track;
|
|
175
216
|
}
|
|
176
217
|
},
|
|
177
|
-
[
|
|
218
|
+
[dropAssetAt, projectIdRef, uploadProjectFiles, isRecordingRef, showToast],
|
|
178
219
|
);
|
|
179
220
|
|
|
180
221
|
const handleTimelineCompositionDrop = useCallback(
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
|
+
// UI primitives
|
|
2
|
+
export { Button, buttonBase, buttonVariants } from "./components/ui/Button";
|
|
3
|
+
export type { ButtonSize, ButtonVariant, PreviewState } from "./components/ui/Button";
|
|
4
|
+
export { IconButton } from "./components/ui/IconButton";
|
|
5
|
+
export { Tab, TabPanel, Tabs, TabsList } from "./components/ui/Tabs";
|
|
6
|
+
export { Tooltip } from "./components/ui/Tooltip";
|
|
7
|
+
export { cn } from "./components/ui/cn";
|
|
8
|
+
export {
|
|
9
|
+
ContextMenu,
|
|
10
|
+
Menu,
|
|
11
|
+
MenuItem,
|
|
12
|
+
MenuRadioGroup,
|
|
13
|
+
MenuRadioItem,
|
|
14
|
+
MenuSeparator,
|
|
15
|
+
MenuShortcut,
|
|
16
|
+
popupSurface,
|
|
17
|
+
} from "./components/ui/Menu";
|
|
18
|
+
export type { MenuItemTone, PopupPreviewState } from "./components/ui/Menu";
|
|
19
|
+
export { Popover } from "./components/ui/Popover";
|
|
20
|
+
export { Input, fieldBase, fieldText } from "./components/ui/Input";
|
|
21
|
+
export type { InputProps } from "./components/ui/Input";
|
|
22
|
+
export { NumberField } from "./components/ui/NumberField";
|
|
23
|
+
export type { NumberFieldProps } from "./components/ui/NumberField";
|
|
24
|
+
export { Select } from "./components/ui/Select";
|
|
25
|
+
export type { SelectOption, SelectProps } from "./components/ui/Select";
|
|
26
|
+
export { Slider } from "./components/ui/Slider";
|
|
27
|
+
export type { SliderProps } from "./components/ui/Slider";
|
|
28
|
+
export { Toggle } from "./components/ui/Toggle";
|
|
29
|
+
export type { ToggleProps } from "./components/ui/Toggle";
|
|
30
|
+
|
|
1
31
|
// NLE Layout
|
|
2
32
|
export { EditorShell } from "./components/EditorShell";
|
|
33
|
+
export type { EditorShellProps } from "./components/EditorShell";
|
|
3
34
|
export { NLEPreview } from "./components/nle/NLEPreview";
|
|
4
35
|
export { CompositionBreadcrumb } from "./components/nle/CompositionBreadcrumb";
|
|
5
36
|
export type { CompositionLevel } from "./components/nle/CompositionBreadcrumb";
|
|
@@ -19,6 +50,10 @@ export {
|
|
|
19
50
|
} from "./player";
|
|
20
51
|
export type { TimelineElement } from "./player";
|
|
21
52
|
|
|
53
|
+
// Host overlays: draw over the preview in composition coordinates (see EditorShellProps.gestureOverlay)
|
|
54
|
+
export { usePreviewCompositionRect } from "./components/editor/usePreviewCompositionRect";
|
|
55
|
+
export type { PreviewCompositionRect } from "./components/editor/usePreviewCompositionRect";
|
|
56
|
+
|
|
22
57
|
// Editor
|
|
23
58
|
export { SourceEditor } from "./components/editor/SourceEditor";
|
|
24
59
|
export { PropertyPanel } from "./components/editor/PropertyPanel";
|
|
@@ -37,6 +72,22 @@ export {
|
|
|
37
72
|
} from "./components/editor/domEditingAgentPrompt";
|
|
38
73
|
export type { AgentPromptElementInfo } from "./components/editor/domEditingAgentPrompt";
|
|
39
74
|
|
|
75
|
+
// Render queue
|
|
76
|
+
export { RenderQueue } from "./components/renders/RenderQueue";
|
|
77
|
+
export type { RenderQueueProps, CompositionDimensions } from "./components/renders/RenderQueue";
|
|
78
|
+
export { useRenderQueue } from "./components/renders/useRenderQueue";
|
|
79
|
+
export type { FfmpegStatus } from "./components/renders/useFfmpegStatus";
|
|
80
|
+
export type {
|
|
81
|
+
RenderJob,
|
|
82
|
+
ResolutionPreset,
|
|
83
|
+
StartRenderOptions,
|
|
84
|
+
} from "./components/renders/useRenderQueue";
|
|
85
|
+
export {
|
|
86
|
+
getPersistedRenderSettings,
|
|
87
|
+
persistRenderSettings,
|
|
88
|
+
} from "./components/renders/renderSettings";
|
|
89
|
+
export type { PersistedRenderSettings } from "./components/renders/renderSettings";
|
|
90
|
+
|
|
40
91
|
// Hooks
|
|
41
92
|
export { useElementPicker } from "./hooks/useElementPicker";
|
|
42
93
|
export type { PickedElement } from "./hooks/useElementPicker";
|
package/src/main.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { StrictMode } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { IconContext } from "@phosphor-icons/react";
|
|
3
4
|
import { StudioApp } from "./App";
|
|
4
5
|
import { StudioErrorBoundary } from "./components/StudioErrorBoundary";
|
|
6
|
+
import { readIconTokens } from "./styles/iconTokens";
|
|
5
7
|
import { trackStudioEvent } from "./utils/studioTelemetry";
|
|
6
8
|
import "./styles/studio.css";
|
|
7
9
|
|
|
@@ -93,8 +95,12 @@ window.addEventListener("unhandledrejection", (event) => {
|
|
|
93
95
|
|
|
94
96
|
createRoot(document.getElementById("root")!).render(
|
|
95
97
|
<StrictMode>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
{/* One icon size and weight for the whole app, taken from the theme file.
|
|
99
|
+
Icons that pass their own size or weight still win. */}
|
|
100
|
+
<IconContext.Provider value={readIconTokens()}>
|
|
101
|
+
<StudioErrorBoundary>
|
|
102
|
+
<StudioApp />
|
|
103
|
+
</StudioErrorBoundary>
|
|
104
|
+
</IconContext.Provider>
|
|
99
105
|
</StrictMode>,
|
|
100
106
|
);
|
|
@@ -191,7 +191,7 @@ export const AudioWaveform = memo(function AudioWaveform({
|
|
|
191
191
|
"repeating-linear-gradient(90deg, rgba(75,163,210,0.35) 0 2px, transparent 2px 5px)",
|
|
192
192
|
}}
|
|
193
193
|
/>
|
|
194
|
-
<span className="relative rounded bg-black/50 px-1 text-[8px] text-neutral-500">
|
|
194
|
+
<span className="relative rounded-sm bg-black/50 px-1 text-[8px] text-neutral-500">
|
|
195
195
|
waveform unavailable
|
|
196
196
|
</span>
|
|
197
197
|
</div>
|
|
@@ -29,7 +29,7 @@ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
|
|
|
29
29
|
}: AutomationSelectionMenuProps) {
|
|
30
30
|
const menuRef = useContextMenuDismiss(onClose);
|
|
31
31
|
const row =
|
|
32
|
-
"block w-full px-2 py-1 text-left text-[11px] text-panel-text-1 hover:bg-panel-
|
|
32
|
+
"block w-full px-2 py-1 text-left text-[11px] text-panel-text-1 hover:bg-panel-hover disabled:opacity-40";
|
|
33
33
|
// Same edge-clamping precedent as TrackGapContextMenu: without it a
|
|
34
34
|
// right-click near the bottom/right of the timeline renders this menu
|
|
35
35
|
// partially off-screen.
|
|
@@ -41,11 +41,11 @@ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
|
|
|
41
41
|
return createPortal(
|
|
42
42
|
<div
|
|
43
43
|
ref={menuRef}
|
|
44
|
-
// z-
|
|
44
|
+
// z-200 for the same reason the timeline's FX popover uses it: this is
|
|
45
45
|
// portaled to `document.body`, but the ruler's sticky header sits at z-70
|
|
46
46
|
// in the SAME root stacking context, so a z-50 menu opened near the top of
|
|
47
47
|
// the timeline is painted through by the ruler and the playhead.
|
|
48
|
-
className="hf-automation-menu fixed z-
|
|
48
|
+
className="hf-automation-menu fixed z-200 min-w-[140px] rounded-sm border border-panel-border-input bg-panel-bg-2 py-1 shadow-lg"
|
|
49
49
|
style={{ left: adjustedX, top: adjustedY }}
|
|
50
50
|
>
|
|
51
51
|
{AUTOMATION_SHAPES.map((shape) => (
|
|
@@ -78,7 +78,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
|
|
|
78
78
|
ref={menuRef}
|
|
79
79
|
role="menu"
|
|
80
80
|
aria-label="Clip actions"
|
|
81
|
-
className="fixed z-
|
|
81
|
+
className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
|
|
82
82
|
style={{ left: adjustedX, top: adjustedY }}
|
|
83
83
|
>
|
|
84
84
|
{splitLabel && (
|
|
@@ -86,7 +86,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
|
|
|
86
86
|
<button
|
|
87
87
|
type="button"
|
|
88
88
|
role="menuitem"
|
|
89
|
-
className={`w-full flex items-center justify-between px-3 py-1.5 text-xs text-left outline-
|
|
89
|
+
className={`w-full flex items-center justify-between px-3 py-1.5 text-xs text-left outline-hidden focus-visible:bg-neutral-800 ${
|
|
90
90
|
canSplit
|
|
91
91
|
? "text-neutral-300 hover:bg-neutral-800 cursor-pointer"
|
|
92
92
|
: "text-neutral-600 cursor-not-allowed"
|
|
@@ -159,7 +159,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
|
|
|
159
159
|
<button
|
|
160
160
|
type="button"
|
|
161
161
|
role="menuitem"
|
|
162
|
-
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-
|
|
162
|
+
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left"
|
|
163
163
|
onClick={() => {
|
|
164
164
|
onDelete(element);
|
|
165
165
|
onClose();
|
|
@@ -87,9 +87,6 @@ describe("buildCompositionThumbnailUrl", () => {
|
|
|
87
87
|
|
|
88
88
|
expect(buildCompositionThumbnailUrl(base)).not.toContain("output=");
|
|
89
89
|
expect(buildCompositionThumbnailUrl({ ...base, output: "source" })).toContain("output=source");
|
|
90
|
-
expect(buildCompositionThumbnailUrl({ ...base, output: "storyboard" })).toContain(
|
|
91
|
-
"output=storyboard",
|
|
92
|
-
);
|
|
93
90
|
});
|
|
94
91
|
|
|
95
92
|
it("includes the persisted content revision in the cache identity", () => {
|
|
@@ -44,10 +44,9 @@ export function buildCompositionThumbnailUrl({
|
|
|
44
44
|
/**
|
|
45
45
|
* Capture density. Omitted, the route bounds the image to its preview cap —
|
|
46
46
|
* right for the timeline, where thumbnails are small and numerous and their
|
|
47
|
-
* decoded bytes are budgeted. `"
|
|
48
|
-
* high-density review size; `"source"` uses the composition's own dimensions.
|
|
47
|
+
* decoded bytes are budgeted. `"source"` uses the composition's own dimensions.
|
|
49
48
|
*/
|
|
50
|
-
output?: "source"
|
|
49
|
+
output?: "source";
|
|
51
50
|
contentRevision?: number;
|
|
52
51
|
}): string {
|
|
53
52
|
const thumbnailBase = previewUrl
|
|
@@ -159,7 +158,7 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
|
|
|
159
158
|
{Array.from({ length: frameCount }, (_, index) => (
|
|
160
159
|
<div
|
|
161
160
|
key={index}
|
|
162
|
-
className="relative h-full
|
|
161
|
+
className="relative h-full shrink-0 overflow-hidden"
|
|
163
162
|
style={{ width: frameW }}
|
|
164
163
|
>
|
|
165
164
|
<img
|
|
@@ -168,7 +168,7 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }:
|
|
|
168
168
|
}}
|
|
169
169
|
placeholder="What should change?"
|
|
170
170
|
rows={2}
|
|
171
|
-
className="w-full px-3 py-2 text-xs bg-neutral-800/60 border border-neutral-700/40 rounded-lg text-neutral-200 placeholder:text-neutral-600 resize-none focus:outline-
|
|
171
|
+
className="w-full px-3 py-2 text-xs bg-neutral-800/60 border border-neutral-700/40 rounded-lg text-neutral-200 placeholder:text-neutral-600 resize-none focus:outline-hidden focus:border-studio-accent/40 transition-colors"
|
|
172
172
|
/>
|
|
173
173
|
</div>
|
|
174
174
|
|
|
@@ -73,7 +73,7 @@ export const ImageThumbnail = memo(function ImageThumbnail({
|
|
|
73
73
|
{Array.from({ length: frameCount }, (_, index) => (
|
|
74
74
|
<div
|
|
75
75
|
key={index}
|
|
76
|
-
className="relative h-full
|
|
76
|
+
className="relative h-full shrink-0 overflow-hidden bg-neutral-900"
|
|
77
77
|
style={{ width: frameW }}
|
|
78
78
|
>
|
|
79
79
|
<img
|
|
@@ -40,9 +40,9 @@ interface KeyframeDiamondContextMenuProps {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const ITEM_CLS =
|
|
43
|
-
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-200 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-
|
|
43
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-200 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left";
|
|
44
44
|
const DESTRUCTIVE_ITEM_CLS =
|
|
45
|
-
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-
|
|
45
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left";
|
|
46
46
|
|
|
47
47
|
export function KeyframeDiamondContextMenu({
|
|
48
48
|
state,
|
|
@@ -96,7 +96,7 @@ export function KeyframeDiamondContextMenu({
|
|
|
96
96
|
ref={menuRef}
|
|
97
97
|
role="menu"
|
|
98
98
|
aria-label="Keyframe actions"
|
|
99
|
-
className="fixed z-
|
|
99
|
+
className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px] overflow-y-auto"
|
|
100
100
|
style={{ left: adjustedX, top: adjustedY, maxHeight: `calc(100vh - ${adjustedY + 8}px)` }}
|
|
101
101
|
>
|
|
102
102
|
{onMoveToPlayhead && (
|
|
@@ -172,7 +172,7 @@ export function KeyframeDiamondContextMenu({
|
|
|
172
172
|
<button
|
|
173
173
|
type="button"
|
|
174
174
|
role="menuitem"
|
|
175
|
-
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-red-950/40 focus-visible:bg-red-950/40 outline-
|
|
175
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-red-950/40 focus-visible:bg-red-950/40 outline-hidden cursor-pointer text-left"
|
|
176
176
|
onClick={() => {
|
|
177
177
|
onDeleteAll(state.element, state.animationId);
|
|
178
178
|
onClose();
|
|
@@ -37,7 +37,7 @@ export function LaneToggleButton({
|
|
|
37
37
|
title={`${isExpanded ? "Hide" : "Show"} lanes`}
|
|
38
38
|
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target. The glyph stays 11px;
|
|
39
39
|
// only the hit box grows.
|
|
40
|
-
className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
40
|
+
className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
41
41
|
isExpanded ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
|
|
42
42
|
}`}
|
|
43
43
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -97,7 +97,7 @@ export function LayerDisclosureRow({
|
|
|
97
97
|
</span>
|
|
98
98
|
{/* Wraps rather than truncating: a truncated name needs a hover to be
|
|
99
99
|
read, which a scanned column cannot rely on. */}
|
|
100
|
-
<span className="min-w-0 flex-1 break-
|
|
100
|
+
<span className="min-w-0 flex-1 wrap-break-word font-medium leading-tight">{name}</span>
|
|
101
101
|
<TrackClipCount clipCount={clipCount} />
|
|
102
102
|
{children}
|
|
103
103
|
{/* Anchored right, on every header that has one: the lane toggle is the
|
|
@@ -116,7 +116,7 @@ const FullscreenButton = memo(function FullscreenButton({
|
|
|
116
116
|
trackStudioEvent("playback", { action: "fullscreen_toggle", active: !isFullscreen });
|
|
117
117
|
onToggleFullscreen();
|
|
118
118
|
}}
|
|
119
|
-
className={`flex h-7 w-7
|
|
119
|
+
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${
|
|
120
120
|
isFullscreen ? "text-studio-accent" : "text-neutral-500 hover:text-neutral-200"
|
|
121
121
|
}`}
|
|
122
122
|
aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
@@ -153,7 +153,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
153
153
|
);
|
|
154
154
|
|
|
155
155
|
return (
|
|
156
|
-
<div ref={shortcutsPanelRef} className="relative
|
|
156
|
+
<div ref={shortcutsPanelRef} className="relative shrink-0">
|
|
157
157
|
<Tooltip label="Shortcuts and tools">
|
|
158
158
|
<button
|
|
159
159
|
ref={triggerRef}
|
|
@@ -192,7 +192,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
192
192
|
// Deliberately NOT aria-modal. This is a non-modal disclosure: focus is
|
|
193
193
|
// not trapped and the rest of the editor stays operable, so claiming
|
|
194
194
|
// modality would make assistive tech treat the whole app as inert.
|
|
195
|
-
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-
|
|
195
|
+
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-hidden"
|
|
196
196
|
style={{
|
|
197
197
|
background: "#161618",
|
|
198
198
|
border: "1px solid rgba(255,255,255,0.08)",
|
|
@@ -212,7 +212,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
212
212
|
pattern="[0-9]*"
|
|
213
213
|
aria-label="Jump to frame"
|
|
214
214
|
placeholder="frame number"
|
|
215
|
-
className="h-6 flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-
|
|
215
|
+
className="h-6 flex-1 rounded-sm border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-hidden transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60"
|
|
216
216
|
onKeyDown={handleJumpKeyDown}
|
|
217
217
|
onBlur={commitJumpFrame}
|
|
218
218
|
/>
|
|
@@ -220,7 +220,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
220
220
|
<button
|
|
221
221
|
type="submit"
|
|
222
222
|
disabled={disabled}
|
|
223
|
-
className="h-6 px-2 rounded border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
|
|
223
|
+
className="h-6 px-2 rounded-sm border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
|
|
224
224
|
>
|
|
225
225
|
Go
|
|
226
226
|
</button>
|
|
@@ -236,7 +236,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
236
236
|
<div className="flex items-center justify-between gap-2">
|
|
237
237
|
<div className="flex items-center gap-2">
|
|
238
238
|
<span
|
|
239
|
-
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
239
|
+
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
240
240
|
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
241
241
|
>
|
|
242
242
|
I
|
|
@@ -253,7 +253,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
253
253
|
<button
|
|
254
254
|
type="button"
|
|
255
255
|
onClick={() => setInPoint(null)}
|
|
256
|
-
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
256
|
+
className="w-4 h-4 flex items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
257
257
|
aria-label="Clear in-point"
|
|
258
258
|
>
|
|
259
259
|
<svg
|
|
@@ -277,7 +277,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
277
277
|
<div className="flex items-center justify-between gap-2">
|
|
278
278
|
<div className="flex items-center gap-2">
|
|
279
279
|
<span
|
|
280
|
-
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
280
|
+
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
281
281
|
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
282
282
|
>
|
|
283
283
|
O
|
|
@@ -294,7 +294,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
294
294
|
<button
|
|
295
295
|
type="button"
|
|
296
296
|
onClick={() => setOutPoint(null)}
|
|
297
|
-
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
297
|
+
className="w-4 h-4 flex items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
298
298
|
aria-label="Clear out-point"
|
|
299
299
|
>
|
|
300
300
|
<svg
|
|
@@ -328,7 +328,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
328
328
|
{section.hints.map((hint) => (
|
|
329
329
|
<div key={hint.key} className="flex items-center gap-3">
|
|
330
330
|
<span
|
|
331
|
-
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
|
|
331
|
+
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
|
|
332
332
|
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
333
333
|
>
|
|
334
334
|
{hint.key}
|