@hyperframes/studio 0.8.51 → 0.8.53-desktop.0
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-Cn_PpHqP.js → hyperframes-player-PTEnAHhg.js} +1 -1
- package/dist/assets/{index-CPS3Cvj8.js → index--myXp8ui.js} +1 -1
- package/dist/assets/{index-Clrb11lj.js → index-BjslOS9g.js} +1 -1
- package/dist/assets/index-BnXZuIJD.css +1 -0
- package/dist/assets/index-CNyviK_l.js +390 -0
- package/dist/index.css +29 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +808 -12
- package/dist/index.html +2 -2
- package/dist/index.js +8109 -7583
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
- package/src/App.tsx +45 -50
- package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
- package/src/captions/components/CaptionOverlay.tsx +8 -4
- package/src/components/EditorShell.selectionSync.test.tsx +9 -4
- package/src/components/EditorShell.tsx +56 -56
- package/src/components/RightPanelTabs.test.tsx +151 -0
- package/src/components/RightPanelTabs.tsx +59 -0
- package/src/components/StudioHeader.dom.test.tsx +181 -0
- package/src/components/StudioHeader.test.ts +0 -12
- package/src/components/StudioHeader.tsx +117 -160
- package/src/components/StudioLeftPanels.tsx +202 -0
- package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -194
- package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
- 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 +3 -7
- package/src/components/dock/Dock.test.tsx +100 -18
- package/src/components/dock/Dock.tsx +32 -5
- package/src/components/dock/dock.css +5 -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 +53 -6
- package/src/components/dock/dockLayoutStore.test.ts +28 -0
- package/src/components/dock/dockLayoutStore.ts +21 -4
- package/src/components/dock/dockTestHarness.ts +11 -0
- package/src/components/editor/LayersPanel.test.ts +97 -1
- package/src/components/editor/LayersPanel.tsx +4 -2
- package/src/components/editor/PreviewGuides.tsx +1 -1
- package/src/components/editor/SnapToolbar.test.tsx +0 -3
- package/src/components/editor/TopologyLens.test.tsx +28 -0
- package/src/components/editor/TopologyLens.tsx +3 -1
- 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/nle/NLEContext.tsx +0 -43
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +1 -1
- package/src/components/nle/TimelinePane.tsx +53 -67
- package/src/components/panels/SlideshowPanel.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +1 -1
- package/src/components/pointerTargetSize.test.tsx +8 -0
- package/src/components/renders/RenderQueue.tsx +1 -1
- package/src/components/renders/RenderQueuePanel.tsx +1 -1
- package/src/components/sidebar/{LeftSidebar.masterComposition.test.tsx → CompositionsPanel.masterComposition.test.tsx} +6 -9
- package/src/components/sidebar/CompositionsPanel.tsx +15 -0
- package/src/components/sidebar/{LeftSidebar.lintBadge.test.tsx → SidebarLintButton.test.tsx} +6 -11
- package/src/components/sidebar/SidebarLintButton.tsx +50 -0
- package/src/components/timelineToolbarStyles.ts +5 -0
- package/src/components/ui/Button.test.tsx +222 -30
- package/src/components/ui/Button.tsx +88 -125
- 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/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/contexts/PanelLayoutContext.tsx +3 -57
- package/src/hooks/appHotkeysDispatch.ts +3 -4
- package/src/hooks/timelineEditPermission.ts +31 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -2
- package/src/hooks/useAppHotkeys.test.ts +0 -1
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -3
- package/src/hooks/useAppHotkeys.ts +20 -83
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useDomEditPreviewSync.ts +4 -6
- package/src/hooks/useDomEditSession.ts +0 -7
- package/src/hooks/useDomEditWiring.ts +4 -9
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- package/src/hooks/useFileManager.ts +21 -123
- package/src/hooks/usePanelLayout.test.ts +108 -296
- package/src/hooks/usePanelLayout.ts +50 -255
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/useProjectFileWriter.ts +150 -0
- package/src/hooks/useRightPanelIntents.test.ts +181 -0
- package/src/hooks/useRightPanelIntents.ts +63 -0
- package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
- package/src/hooks/useServerConnection.ts +43 -10
- package/src/hooks/useSlideshowTabState.test.ts +9 -31
- package/src/hooks/useSlideshowTabState.ts +6 -19
- package/src/hooks/useStudioContextValue.test.ts +6 -18
- package/src/hooks/useStudioContextValue.ts +5 -26
- package/src/hooks/useTimelineDeleteOps.test.tsx +111 -0
- package/src/hooks/useTimelineEditing.test.tsx +322 -3
- package/src/hooks/useTimelineEditing.ts +103 -16
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTrackPendingTimelineEdit.test.tsx +62 -0
- package/src/hooks/useTrackPendingTimelineEdit.ts +22 -0
- package/src/hostMount.test.tsx +222 -0
- package/src/index.ts +87 -0
- package/src/player/components/AutomationSelectionMenu.tsx +1 -1
- package/src/player/components/Timeline.tsx +1 -1
- package/src/player/components/TimelineCanvas.tsx +4 -4
- package/src/player/components/TimelineClip.tsx +0 -1
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/timelineTheme.test.ts +4 -4
- package/src/player/components/timelineTheme.ts +30 -30
- package/src/player/lib/playbackShortcuts.ts +2 -0
- package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
- package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
- 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/styleSources.test.ts +20 -0
- package/src/styles/styleSources.ts +47 -0
- package/src/styles/theme.css +57 -0
- package/src/styles/theme.test.ts +2 -15
- 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 -8
- package/src/utils/studioUiPreferences.test.ts +10 -16
- package/src/utils/studioUiPreferences.ts +0 -16
- package/src/utils/typingTarget.ts +4 -0
- package/dist/assets/index-C05SNrvY.js +0 -360
- package/dist/assets/index-DUR9qzQM.css +0 -1
- package/src/components/PanelTabButton.tsx +0 -31
- package/src/components/StudioLeftSidebar.tsx +0 -207
- package/src/components/nle/TimelineResizeDivider.tsx +0 -101
- package/src/components/sidebar/LeftSidebar.storage.test.ts +0 -28
- package/src/components/sidebar/LeftSidebar.tsx +0 -348
- package/src/hooks/useInspectorSplitResize.ts +0 -51
- package/src/utils/fitPanels.test.ts +0 -164
- package/src/utils/fitPanels.ts +0 -151
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.53-desktop.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@base-ui/react": "1.7.0",
|
|
33
34
|
"@codemirror/autocomplete": "^6.20.1",
|
|
34
35
|
"@codemirror/commands": "^6.10.3",
|
|
35
36
|
"@codemirror/lang-css": "^6.3.1",
|
|
@@ -45,14 +46,15 @@
|
|
|
45
46
|
"@phosphor-icons/react": "^2.1.10",
|
|
46
47
|
"@tanstack/react-virtual": "^3.14.6",
|
|
47
48
|
"bpm-detective": "^2.0.5",
|
|
49
|
+
"cn": "0.2.4",
|
|
48
50
|
"dockview-react": "8.3.1",
|
|
49
51
|
"gsap": "^3.13.0",
|
|
50
52
|
"mediabunny": "^1.45.3",
|
|
51
|
-
"@hyperframes/
|
|
52
|
-
"@hyperframes/
|
|
53
|
-
"@hyperframes/studio-server": "0.8.
|
|
54
|
-
"@hyperframes/sdk": "0.8.
|
|
55
|
-
"@hyperframes/
|
|
53
|
+
"@hyperframes/parsers": "0.8.53-desktop.0",
|
|
54
|
+
"@hyperframes/core": "0.8.53-desktop.0",
|
|
55
|
+
"@hyperframes/studio-server": "0.8.53-desktop.0",
|
|
56
|
+
"@hyperframes/sdk": "0.8.53-desktop.0",
|
|
57
|
+
"@hyperframes/player": "0.8.53-desktop.0"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
"vite": "^6.4.2",
|
|
69
71
|
"vitest": "^4.1.11",
|
|
70
72
|
"zustand": "^5.0.0",
|
|
71
|
-
"@hyperframes/producer": "0.8.
|
|
73
|
+
"@hyperframes/producer": "0.8.53-desktop.0"
|
|
72
74
|
},
|
|
73
75
|
"peerDependencies": {
|
|
74
76
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { useOwnPreviewIframe, usePreviewIframeStore } from "./player/store/previewIframeStore";
|
|
1
2
|
import { buildProjectApiPath } from "./utils/projectRouting";
|
|
2
3
|
import { useState, useCallback, useRef, useMemo, useLayoutEffect } from "react";
|
|
3
|
-
import
|
|
4
|
+
import { useDismissingTabSetter, useRightPanelIntent } from "./hooks/useRightPanelIntents";
|
|
4
5
|
import { useRenderQueue } from "./components/renders/useRenderQueue";
|
|
5
6
|
import { usePlayerStore } from "./player";
|
|
6
7
|
import { StudioOverlays } from "./components/StudioOverlays";
|
|
@@ -47,9 +48,9 @@ import type { DomEditSelection } from "./components/editor/domEditing";
|
|
|
47
48
|
import { StudioHeader } from "./components/StudioHeader";
|
|
48
49
|
import { useGestureCommit } from "./hooks/useGestureCommit";
|
|
49
50
|
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
|
|
50
|
-
import {
|
|
51
|
+
import { StudioLeftPanels } from "./components/StudioLeftPanels";
|
|
51
52
|
import { EditorShell } from "./components/EditorShell";
|
|
52
|
-
import {
|
|
53
|
+
import { StudioRightPanels } from "./components/StudioRightPanels";
|
|
53
54
|
import { TimelineToolbar } from "./components/TimelineToolbar";
|
|
54
55
|
import { StudioPlaybackProvider, StudioShellProvider } from "./contexts/StudioContext";
|
|
55
56
|
import { PanelLayoutProvider } from "./contexts/PanelLayoutContext";
|
|
@@ -68,13 +69,12 @@ export function StudioApp() {
|
|
|
68
69
|
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
|
|
69
70
|
useStudioSessionStart(projectId, resolving, waitingForServer);
|
|
70
71
|
const [compIdToSrc, setCompIdToSrc] = useState<Map<string, string>>(new Map());
|
|
71
|
-
const
|
|
72
|
+
const previewIframe = useOwnPreviewIframe();
|
|
72
73
|
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
73
74
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
74
75
|
const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
|
|
75
76
|
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
|
|
76
77
|
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
77
|
-
const leftSidebarRef = useRef<LeftSidebarHandle>(null);
|
|
78
78
|
const captionEditMode = useCaptionStore((s) => s.isEditMode);
|
|
79
79
|
const captionHasSelection = useCaptionStore((s) => s.selectedSegmentIds.size > 0);
|
|
80
80
|
const captionSync = useCaptionSync(projectId);
|
|
@@ -201,6 +201,13 @@ export function StudioApp() {
|
|
|
201
201
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
202
202
|
setRightPanelTab: panelLayout.setRightPanelTab,
|
|
203
203
|
});
|
|
204
|
+
const dismissBlockParams = useCallback(() => setActiveBlockParams(null), [setActiveBlockParams]);
|
|
205
|
+
const setRightPanelTab = useDismissingTabSetter(panelLayout.setRightPanelTab, dismissBlockParams);
|
|
206
|
+
const layout = useMemo(
|
|
207
|
+
() => ({ ...panelLayout, setRightPanelTab }),
|
|
208
|
+
[panelLayout, setRightPanelTab],
|
|
209
|
+
);
|
|
210
|
+
|
|
204
211
|
const clearDomSelectionRef = useRef<() => void>(() => {});
|
|
205
212
|
const domEditSelectionBridgeRef = useRef<DomEditSelection | null>(null);
|
|
206
213
|
type DomEditDelete = (s: DomEditSelection, o?: { expandGroup?: boolean }) => Promise<void>;
|
|
@@ -233,7 +240,6 @@ export function StudioApp() {
|
|
|
233
240
|
showToast,
|
|
234
241
|
syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
|
|
235
242
|
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
|
|
236
|
-
leftSidebarRef,
|
|
237
243
|
handleCopy,
|
|
238
244
|
handlePaste,
|
|
239
245
|
handleCut,
|
|
@@ -247,10 +253,6 @@ export function StudioApp() {
|
|
|
247
253
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
248
254
|
onToggleRecording: () => handleToggleRecordingRef.current(),
|
|
249
255
|
});
|
|
250
|
-
const sidebarTabRef = useRef({
|
|
251
|
-
select: (t: SidebarTab) => leftSidebarRef.current?.selectTab(t),
|
|
252
|
-
get: () => leftSidebarRef.current?.getTab() ?? "compositions",
|
|
253
|
-
});
|
|
254
256
|
const domEditSession = useDomEditSession({
|
|
255
257
|
projectId,
|
|
256
258
|
activeCompPath,
|
|
@@ -263,7 +265,7 @@ export function StudioApp() {
|
|
|
263
265
|
setSelectedTimelineElementId,
|
|
264
266
|
setTimelineSelectionSet,
|
|
265
267
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
266
|
-
setRightPanelTab
|
|
268
|
+
setRightPanelTab,
|
|
267
269
|
showToast,
|
|
268
270
|
isRecordingRef: isGestureRecordingRef,
|
|
269
271
|
refreshPreviewDocumentVersion,
|
|
@@ -285,8 +287,6 @@ export function StudioApp() {
|
|
|
285
287
|
reloadPreview,
|
|
286
288
|
setRefreshKey,
|
|
287
289
|
openSourceForSelection: fileManager.openSourceForSelection,
|
|
288
|
-
selectSidebarTab: sidebarTabRef.current.select,
|
|
289
|
-
getSidebarTab: sidebarTabRef.current.get,
|
|
290
290
|
sdkSession: editFlowSdkSession,
|
|
291
291
|
publishSdkSession: sdkHandle.publish,
|
|
292
292
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
@@ -359,27 +359,25 @@ export function StudioApp() {
|
|
|
359
359
|
const handlePreviewIframeRef = useCallback(
|
|
360
360
|
(iframe: HTMLIFrameElement | null) => {
|
|
361
361
|
previewIframeRef.current = iframe;
|
|
362
|
-
|
|
362
|
+
usePreviewIframeStore.getState().setIframe(iframe);
|
|
363
363
|
appHotkeys.syncPreviewHotkeys(iframe);
|
|
364
364
|
resetConsoleErrors();
|
|
365
365
|
refreshPreviewDocumentVersion();
|
|
366
366
|
},
|
|
367
367
|
[appHotkeys, resetConsoleErrors, refreshPreviewDocumentVersion],
|
|
368
368
|
);
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
gestureState === "recording",
|
|
382
|
-
);
|
|
369
|
+
const rightPanel = useRightPanelIntent();
|
|
370
|
+
const { inspectorPanelActive, shouldShowMotionPath, shouldShowSelectedDomBounds } =
|
|
371
|
+
useInspectorState(
|
|
372
|
+
rightPanel,
|
|
373
|
+
isPlaying,
|
|
374
|
+
domEditSession.domEditSelection,
|
|
375
|
+
gestureState === "recording",
|
|
376
|
+
);
|
|
377
|
+
// The dock has no separate "railed by window width" state (it shrinks
|
|
378
|
+
// panels, never auto-hides the group), so rightCollapsed is already the
|
|
379
|
+
// value that decides whether the panel is actually showing.
|
|
380
|
+
const inspectorButtonActive = !panelLayout.rightCollapsed && inspectorPanelActive;
|
|
383
381
|
useStudioUrlState({
|
|
384
382
|
projectId,
|
|
385
383
|
activeCompPath,
|
|
@@ -396,7 +394,7 @@ export function StudioApp() {
|
|
|
396
394
|
applyMarqueeSelection: domEditSession.applyMarqueeSelection,
|
|
397
395
|
buildDomSelectionFromTarget: domEditSession.buildDomSelectionFromTarget,
|
|
398
396
|
applyDomSelection: domEditSession.applyDomSelection,
|
|
399
|
-
setRightPanelTab
|
|
397
|
+
setRightPanelTab,
|
|
400
398
|
initialState: initialUrlStateRef.current,
|
|
401
399
|
});
|
|
402
400
|
const studioCtxValue = buildStudioContextValue({
|
|
@@ -436,7 +434,7 @@ export function StudioApp() {
|
|
|
436
434
|
return (
|
|
437
435
|
<StudioShellProvider value={studioCtxValue}>
|
|
438
436
|
<StudioPlaybackProvider value={studioCtxValue}>
|
|
439
|
-
<PanelLayoutProvider value={
|
|
437
|
+
<PanelLayoutProvider value={layout}>
|
|
440
438
|
<FileManagerProvider value={fileManager}>
|
|
441
439
|
<DomEditProvider value={domEditSession}>
|
|
442
440
|
<div
|
|
@@ -467,26 +465,23 @@ export function StudioApp() {
|
|
|
467
465
|
)}
|
|
468
466
|
<ExternalFileConflictBanner coordinator={externalFileChanges} />
|
|
469
467
|
<EditorShell
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
right={
|
|
486
|
-
panelLayout.effectiveRightCollapsed ? null : (
|
|
487
|
-
<StudioRightPanel
|
|
488
|
-
designPanelActive={designPanelActive}
|
|
468
|
+
panels={
|
|
469
|
+
<>
|
|
470
|
+
<StudioLeftPanels
|
|
471
|
+
onSelectComposition={handleSelectComposition}
|
|
472
|
+
onAddBlock={handleAddBlock}
|
|
473
|
+
onPreviewBlock={setBlockPreview}
|
|
474
|
+
onLint={handleLint}
|
|
475
|
+
linting={linting}
|
|
476
|
+
lintFindingCount={lintModal?.length ?? findingsByFile.size}
|
|
477
|
+
lintFindingsByFile={findingsByFile}
|
|
478
|
+
lintHasError={hasLintError}
|
|
479
|
+
onAddAssetToTimeline={handleAddAssetAtPlayhead}
|
|
480
|
+
onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
|
|
481
|
+
/>
|
|
482
|
+
<StudioRightPanels
|
|
489
483
|
activeBlockParams={activeBlockParams}
|
|
484
|
+
onDismissBlockParams={dismissBlockParams}
|
|
490
485
|
onCloseBlockParams={() => {
|
|
491
486
|
setActiveBlockParams(null);
|
|
492
487
|
panelLayout.setRightPanelTab("design");
|
|
@@ -503,7 +498,7 @@ export function StudioApp() {
|
|
|
503
498
|
onAutoGroupCarveSources={timelineEditing.handleAutoGroupCarveSources}
|
|
504
499
|
onAddMediaOverlay={handleAddMediaOverlay}
|
|
505
500
|
/>
|
|
506
|
-
|
|
501
|
+
</>
|
|
507
502
|
}
|
|
508
503
|
timelineToolbar={timelineToolbar}
|
|
509
504
|
renderClipContent={renderClipContent}
|
|
@@ -4,6 +4,7 @@ import { createRoot } from "react-dom/client";
|
|
|
4
4
|
import { expect, it, vi } from "vitest";
|
|
5
5
|
import { CaptionOverlay } from "./CaptionOverlay";
|
|
6
6
|
import { useCaptionStore } from "../store";
|
|
7
|
+
import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
|
|
7
8
|
|
|
8
9
|
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
9
10
|
|
|
@@ -45,3 +46,54 @@ it("only schedules caption layout for messages from its preview", () => {
|
|
|
45
46
|
vi.unstubAllGlobals();
|
|
46
47
|
}
|
|
47
48
|
});
|
|
49
|
+
|
|
50
|
+
it("observes and accepts messages from the promoted iframe, not the old one", () => {
|
|
51
|
+
const host = document.createElement("div");
|
|
52
|
+
const a = document.createElement("iframe");
|
|
53
|
+
const b = document.createElement("iframe");
|
|
54
|
+
document.body.append(host, a, b);
|
|
55
|
+
const ref = { current: a };
|
|
56
|
+
const root = createRoot(host);
|
|
57
|
+
const schedule = vi.fn(() => 1);
|
|
58
|
+
const observed: Element[] = [];
|
|
59
|
+
vi.stubGlobal("requestAnimationFrame", schedule);
|
|
60
|
+
vi.stubGlobal("cancelAnimationFrame", vi.fn());
|
|
61
|
+
vi.stubGlobal(
|
|
62
|
+
"ResizeObserver",
|
|
63
|
+
class {
|
|
64
|
+
observe(el: Element) {
|
|
65
|
+
observed.push(el);
|
|
66
|
+
}
|
|
67
|
+
disconnect() {}
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
useCaptionStore.getState().setEditMode(true);
|
|
71
|
+
const send = (source: MessageEventSource | null) =>
|
|
72
|
+
act(() => {
|
|
73
|
+
window.dispatchEvent(new MessageEvent("message", { source, data: { source: "hf-preview" } }));
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
act(() => usePreviewIframeStore.getState().setIframe(a));
|
|
77
|
+
act(() => root.render(<CaptionOverlay iframeRef={ref} />));
|
|
78
|
+
expect(observed).toContain(a);
|
|
79
|
+
expect(observed).not.toContain(b);
|
|
80
|
+
act(() => {
|
|
81
|
+
ref.current = b;
|
|
82
|
+
usePreviewIframeStore.getState().setIframe(b);
|
|
83
|
+
});
|
|
84
|
+
expect(observed).toContain(b);
|
|
85
|
+
schedule.mockClear();
|
|
86
|
+
send(a.contentWindow);
|
|
87
|
+
expect(schedule).not.toHaveBeenCalled();
|
|
88
|
+
send(b.contentWindow);
|
|
89
|
+
expect(schedule).toHaveBeenCalledOnce();
|
|
90
|
+
} finally {
|
|
91
|
+
act(() => root.unmount());
|
|
92
|
+
usePreviewIframeStore.setState({ iframe: null });
|
|
93
|
+
useCaptionStore.getState().reset();
|
|
94
|
+
host.remove();
|
|
95
|
+
a.remove();
|
|
96
|
+
b.remove();
|
|
97
|
+
vi.unstubAllGlobals();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { memo, useState, useCallback, useRef } from "react";
|
|
1
|
+
import { memo, useEffect, useState, useCallback, useRef } from "react";
|
|
2
|
+
import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
|
|
2
3
|
import { useCaptionStore } from "../store";
|
|
3
4
|
import { usePlayerStore } from "../../player";
|
|
4
|
-
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
5
5
|
import { shouldHandleCaptionNudgeKey, isEditableEventTarget } from "../keyboard";
|
|
6
6
|
import {
|
|
7
7
|
readWordBoxes,
|
|
@@ -32,6 +32,7 @@ function syncToStore(segmentId: string, el: HTMLElement, iframeWin: Window) {
|
|
|
32
32
|
|
|
33
33
|
export const CaptionOverlay = memo(function CaptionOverlay({ iframeRef }: CaptionOverlayProps) {
|
|
34
34
|
const isEditMode = useCaptionStore((s) => s.isEditMode);
|
|
35
|
+
const livePreviewIframe = useLivePreviewIframe();
|
|
35
36
|
const model = useCaptionStore((s) => s.model);
|
|
36
37
|
const selectedSegmentIds = useCaptionStore((s) => s.selectedSegmentIds);
|
|
37
38
|
const selectSegment = useCaptionStore((s) => s.selectSegment);
|
|
@@ -81,7 +82,9 @@ export const CaptionOverlay = memo(function CaptionOverlay({ iframeRef }: Captio
|
|
|
81
82
|
| null
|
|
82
83
|
>(null);
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
// The observer and message filter bind to the live iframe element, which a promoted reload replaces.
|
|
86
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
87
|
+
useEffect(() => {
|
|
85
88
|
if (!isEditMode) return;
|
|
86
89
|
|
|
87
90
|
// Let undo/redo (useAppHotkeys) reapply restored models to this iframe.
|
|
@@ -240,7 +243,8 @@ export const CaptionOverlay = memo(function CaptionOverlay({ iframeRef }: Captio
|
|
|
240
243
|
window.removeEventListener("keydown", handleKeyDown);
|
|
241
244
|
cancelDragRef.current = null;
|
|
242
245
|
};
|
|
243
|
-
|
|
246
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
247
|
+
}, [livePreviewIframe]);
|
|
244
248
|
|
|
245
249
|
const getCssScale = useCallback(() => {
|
|
246
250
|
const iframe = iframeRef.current;
|
|
@@ -42,7 +42,6 @@ vi.mock("./nle/NLEContext", () => ({
|
|
|
42
42
|
useNLEContext: () => ({
|
|
43
43
|
compositionStack: [],
|
|
44
44
|
updateCompositionStack: vi.fn(),
|
|
45
|
-
containerRef: { current: null },
|
|
46
45
|
}),
|
|
47
46
|
}));
|
|
48
47
|
vi.mock("./nle/useTimelineEditCallbacks", () => ({
|
|
@@ -53,7 +52,14 @@ vi.mock("./nle/PreviewOverlays", () => ({ PreviewOverlays: () => null }));
|
|
|
53
52
|
vi.mock("./nle/TimelinePane", () => ({ TimelinePane: () => null }));
|
|
54
53
|
vi.mock("../captions/components/CaptionTimeline", () => ({ CaptionTimeline: () => null }));
|
|
55
54
|
|
|
56
|
-
Object.assign(globalThis, {
|
|
55
|
+
Object.assign(globalThis, {
|
|
56
|
+
IS_REACT_ACT_ENVIRONMENT: true,
|
|
57
|
+
ResizeObserver: class {
|
|
58
|
+
observe() {}
|
|
59
|
+
unobserve() {}
|
|
60
|
+
disconnect() {}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
57
63
|
|
|
58
64
|
afterEach(() => {
|
|
59
65
|
document.body.innerHTML = "";
|
|
@@ -69,8 +75,7 @@ describe("EditorShell timeline selection sync", () => {
|
|
|
69
75
|
act(() => {
|
|
70
76
|
root.render(
|
|
71
77
|
<EditorShell
|
|
72
|
-
|
|
73
|
-
right={null}
|
|
78
|
+
panels={null}
|
|
74
79
|
timelineToolbar={null}
|
|
75
80
|
renderClipContent={() => null}
|
|
76
81
|
handleTimelineElementDelete={vi.fn()}
|
|
@@ -17,6 +17,7 @@ import type { GestureRecordingState } from "./editor/GestureRecordControl";
|
|
|
17
17
|
import { useTimelineSelectionPreviewSync } from "../hooks/useTimelineSelectionPreviewSync";
|
|
18
18
|
import { StudioAgentTools } from "../webmcp/StudioAgentTools";
|
|
19
19
|
import type { TimelineDropPlacement } from "../player/components/timelineCallbacks";
|
|
20
|
+
import { Dock } from "./dock/Dock";
|
|
20
21
|
|
|
21
22
|
type RenderClipContent = (
|
|
22
23
|
element: TimelineElement,
|
|
@@ -26,10 +27,8 @@ type RenderClipContent = (
|
|
|
26
27
|
// The seven move/resize/split/razor handlers come from TimelineEditCallbackDeps
|
|
27
28
|
// (shared with useTimelineEditCallbacks); the rest are drop + wiring props.
|
|
28
29
|
export interface EditorShellProps extends TimelineEditCallbackDeps {
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
/** Right panel (inspector/design) or null when collapsed, in the top row. */
|
|
32
|
-
right: ReactNode;
|
|
30
|
+
/** Dock.Panel elements for every panel except the built-in preview and timeline. */
|
|
31
|
+
panels: ReactNode;
|
|
33
32
|
timelineToolbar: ReactNode;
|
|
34
33
|
renderClipContent: RenderClipContent;
|
|
35
34
|
handleTimelineElementDelete: (element: TimelineElement) => Promise<void> | void;
|
|
@@ -65,15 +64,18 @@ export interface EditorShellProps extends TimelineEditCallbackDeps {
|
|
|
65
64
|
isGestureRecording?: boolean;
|
|
66
65
|
recordingState?: GestureRecordingState;
|
|
67
66
|
onToggleRecording?: () => void;
|
|
67
|
+
/**
|
|
68
|
+
* Host layer over the preview, positioned with `usePreviewCompositionRect`. Hidden in
|
|
69
|
+
* fullscreen and during a block preview; below the selection overlay past z-index 10.
|
|
70
|
+
*/
|
|
68
71
|
gestureOverlay?: ReactNode;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
// The
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
+
// The dockable shell: every panel lives in one Dock, arranged by the user's
|
|
75
|
+
// saved layout. Owns the shared player + composition-stack state via
|
|
76
|
+
// NLEProvider so every panel shares one player.
|
|
74
77
|
export function EditorShell({
|
|
75
|
-
|
|
76
|
-
right,
|
|
78
|
+
panels,
|
|
77
79
|
timelineToolbar,
|
|
78
80
|
renderClipContent,
|
|
79
81
|
handleTimelineElementDelete,
|
|
@@ -174,8 +176,8 @@ export function EditorShell({
|
|
|
174
176
|
}}
|
|
175
177
|
>
|
|
176
178
|
<EditorShellBody
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
projectId={projectId}
|
|
180
|
+
panels={panels}
|
|
179
181
|
captionEditMode={captionEditMode}
|
|
180
182
|
onSelectTimelineElement={handleTimelineElementSelect}
|
|
181
183
|
onPreviewBlockDrop={handlePreviewBlockDrop}
|
|
@@ -209,8 +211,8 @@ export function EditorShell({
|
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
interface EditorShellBodyProps {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
panels: ReactNode;
|
|
215
|
+
projectId: string;
|
|
214
216
|
captionEditMode: boolean;
|
|
215
217
|
previewOverlay: ReactNode;
|
|
216
218
|
onSelectTimelineElement: (element: TimelineElement | null) => void;
|
|
@@ -235,8 +237,8 @@ interface EditorShellBodyProps {
|
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
function EditorShellBody({
|
|
238
|
-
|
|
239
|
-
|
|
240
|
+
panels,
|
|
241
|
+
projectId,
|
|
240
242
|
captionEditMode,
|
|
241
243
|
previewOverlay,
|
|
242
244
|
onSelectTimelineElement,
|
|
@@ -253,7 +255,7 @@ function EditorShellBody({
|
|
|
253
255
|
onDuplicateClip,
|
|
254
256
|
canPasteClip,
|
|
255
257
|
}: EditorShellBodyProps) {
|
|
256
|
-
const { compositionStack, updateCompositionStack
|
|
258
|
+
const { compositionStack, updateCompositionStack } = useNLEContext();
|
|
257
259
|
|
|
258
260
|
// The caption track's blocks are seek targets; CaptionTimeline took an onSeek
|
|
259
261
|
// prop that nothing ever passed, so clicking a block did nothing.
|
|
@@ -273,7 +275,6 @@ function EditorShellBody({
|
|
|
273
275
|
|
|
274
276
|
return (
|
|
275
277
|
<div
|
|
276
|
-
ref={containerRef}
|
|
277
278
|
// Shell canvas is a step LIGHTER than the near-black panel cards so the
|
|
278
279
|
// gaps between panels read as visible seams (CapCut-style).
|
|
279
280
|
className="flex flex-col flex-1 min-h-0 bg-panel-surface"
|
|
@@ -283,47 +284,46 @@ function EditorShellBody({
|
|
|
283
284
|
{/* Renders nothing; exposes Studio's state to an agentic browser. Mounted
|
|
284
285
|
here rather than in App because it needs the DomEdit contexts. */}
|
|
285
286
|
<StudioAgentTools />
|
|
286
|
-
{
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
287
|
+
<Dock.Root projectId={projectId}>
|
|
288
|
+
<Dock.Panel id="preview">
|
|
289
|
+
<div className="relative flex h-full w-full min-w-0 flex-col">
|
|
290
|
+
<PreviewPane
|
|
291
|
+
previewOverlay={previewOverlay}
|
|
292
|
+
onSelectTimelineElement={onSelectTimelineElement}
|
|
293
|
+
onPreviewBlockDrop={onPreviewBlockDrop}
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
</Dock.Panel>
|
|
297
|
+
<Dock.Panel id="timeline">
|
|
298
|
+
<TimelinePane
|
|
299
|
+
timelineToolbar={timelineToolbar}
|
|
300
|
+
renderClipContent={renderClipContent}
|
|
301
|
+
onFileDrop={onFileDrop}
|
|
302
|
+
onAssetDrop={onAssetDrop}
|
|
303
|
+
onBlockDrop={onBlockDrop}
|
|
304
|
+
onCompositionDrop={onCompositionDrop}
|
|
305
|
+
onDeleteElement={onDeleteElement}
|
|
306
|
+
onCopyClip={onCopyClip}
|
|
307
|
+
onPasteClip={onPasteClip}
|
|
308
|
+
onDuplicateClip={onDuplicateClip}
|
|
309
|
+
canPasteClip={canPasteClip}
|
|
293
310
|
onSelectTimelineElement={onSelectTimelineElement}
|
|
294
|
-
|
|
311
|
+
timelineFooter={
|
|
312
|
+
captionEditMode ? (
|
|
313
|
+
<div className="border-t border-neutral-800/30 shrink-0" style={{ height: 60 }}>
|
|
314
|
+
<div className="flex items-center gap-1.5 px-2 py-0.5">
|
|
315
|
+
<span className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider">
|
|
316
|
+
Captions
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
<CaptionTimeline pixelsPerSecond={100} onSeek={seekCaptionTime} />
|
|
320
|
+
</div>
|
|
321
|
+
) : undefined
|
|
322
|
+
}
|
|
295
323
|
/>
|
|
296
|
-
</
|
|
297
|
-
{
|
|
298
|
-
</
|
|
299
|
-
|
|
300
|
-
{/* Full-width timeline row */}
|
|
301
|
-
<TimelinePane
|
|
302
|
-
timelineToolbar={timelineToolbar}
|
|
303
|
-
renderClipContent={renderClipContent}
|
|
304
|
-
onFileDrop={onFileDrop}
|
|
305
|
-
onAssetDrop={onAssetDrop}
|
|
306
|
-
onBlockDrop={onBlockDrop}
|
|
307
|
-
onCompositionDrop={onCompositionDrop}
|
|
308
|
-
onDeleteElement={onDeleteElement}
|
|
309
|
-
onCopyClip={onCopyClip}
|
|
310
|
-
onPasteClip={onPasteClip}
|
|
311
|
-
onDuplicateClip={onDuplicateClip}
|
|
312
|
-
canPasteClip={canPasteClip}
|
|
313
|
-
onSelectTimelineElement={onSelectTimelineElement}
|
|
314
|
-
timelineFooter={
|
|
315
|
-
captionEditMode ? (
|
|
316
|
-
<div className="border-t border-neutral-800/30 shrink-0" style={{ height: 60 }}>
|
|
317
|
-
<div className="flex items-center gap-1.5 px-2 py-0.5">
|
|
318
|
-
<span className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider">
|
|
319
|
-
Captions
|
|
320
|
-
</span>
|
|
321
|
-
</div>
|
|
322
|
-
<CaptionTimeline pixelsPerSecond={100} onSeek={seekCaptionTime} />
|
|
323
|
-
</div>
|
|
324
|
-
) : undefined
|
|
325
|
-
}
|
|
326
|
-
/>
|
|
324
|
+
</Dock.Panel>
|
|
325
|
+
{panels}
|
|
326
|
+
</Dock.Root>
|
|
327
327
|
</div>
|
|
328
328
|
);
|
|
329
329
|
}
|