@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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act, useRef } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
|
|
7
|
+
import {
|
|
8
|
+
usePreviewCompositionRect,
|
|
9
|
+
type PreviewCompositionRect,
|
|
10
|
+
} from "./usePreviewCompositionRect";
|
|
11
|
+
|
|
12
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
usePreviewIframeStore.getState().setIframe(null);
|
|
16
|
+
document.body.innerHTML = "";
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function boxed<T extends HTMLElement>(el: T, box: [number, number, number, number]): T {
|
|
20
|
+
const [left, top, width, height] = box;
|
|
21
|
+
el.getBoundingClientRect = () => new DOMRect(left, top, width, height);
|
|
22
|
+
return el;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mountProbe() {
|
|
26
|
+
let rect: PreviewCompositionRect | undefined;
|
|
27
|
+
function Probe() {
|
|
28
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
29
|
+
rect = usePreviewCompositionRect(ref);
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
ref={(el) => {
|
|
33
|
+
ref.current = el;
|
|
34
|
+
if (el) boxed(el, [10, 20, 1000, 600]);
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const host = document.createElement("div");
|
|
40
|
+
document.body.append(host);
|
|
41
|
+
act(() => createRoot(host).render(<Probe />));
|
|
42
|
+
return () => rect;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compositionFrame(box: [number, number, number, number], width: number, height: number) {
|
|
46
|
+
const iframe = boxed(document.createElement("iframe"), box);
|
|
47
|
+
document.body.append(iframe);
|
|
48
|
+
const root = iframe.contentDocument?.createElement("div");
|
|
49
|
+
root?.setAttribute("data-composition-id", "main");
|
|
50
|
+
root?.setAttribute("data-width", String(width));
|
|
51
|
+
root?.setAttribute("data-height", String(height));
|
|
52
|
+
if (root) iframe.contentDocument?.body.append(root);
|
|
53
|
+
return iframe;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const settle = () => new Promise((r) => setTimeout(r, 400));
|
|
57
|
+
|
|
58
|
+
describe("usePreviewCompositionRect", () => {
|
|
59
|
+
it("reports the live preview iframe's box relative to the overlay, scaled to the composition", async () => {
|
|
60
|
+
const read = mountProbe();
|
|
61
|
+
const iframe = compositionFrame([110, 70, 960, 540], 1920, 1080);
|
|
62
|
+
await act(async () => {
|
|
63
|
+
usePreviewIframeStore.getState().setIframe(iframe);
|
|
64
|
+
await settle();
|
|
65
|
+
});
|
|
66
|
+
expect(read()).toEqual({
|
|
67
|
+
left: 100,
|
|
68
|
+
top: 50,
|
|
69
|
+
width: 960,
|
|
70
|
+
height: 540,
|
|
71
|
+
scaleX: 0.5,
|
|
72
|
+
scaleY: 0.5,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("follows the iframe when a reload replaces it", async () => {
|
|
77
|
+
const read = mountProbe();
|
|
78
|
+
await act(async () => {
|
|
79
|
+
usePreviewIframeStore.getState().setIframe(compositionFrame([110, 70, 960, 540], 1920, 1080));
|
|
80
|
+
await settle();
|
|
81
|
+
});
|
|
82
|
+
await act(async () => {
|
|
83
|
+
usePreviewIframeStore.getState().setIframe(compositionFrame([60, 40, 500, 500], 1000, 1000));
|
|
84
|
+
await settle();
|
|
85
|
+
});
|
|
86
|
+
expect(read()).toEqual({
|
|
87
|
+
left: 50,
|
|
88
|
+
top: 20,
|
|
89
|
+
width: 500,
|
|
90
|
+
height: 500,
|
|
91
|
+
scaleX: 0.5,
|
|
92
|
+
scaleY: 0.5,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useMemo, type RefObject } from "react";
|
|
2
|
+
import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
|
|
3
|
+
import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
|
|
4
|
+
|
|
5
|
+
export type { DomEditCompositionRect as PreviewCompositionRect } from "./useDomEditCompositionRect";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Where the composition sits inside `overlayRef`, in overlay pixels, plus the scale from
|
|
9
|
+
* composition units to pixels. Follows the live preview iframe across reloads.
|
|
10
|
+
*/
|
|
11
|
+
export function usePreviewCompositionRect(overlayRef: RefObject<HTMLDivElement | null>) {
|
|
12
|
+
const iframeRef = useMemo<RefObject<HTMLIFrameElement | null>>(
|
|
13
|
+
() => ({
|
|
14
|
+
get current() {
|
|
15
|
+
return usePreviewIframeStore.getState().iframe;
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
[],
|
|
19
|
+
);
|
|
20
|
+
return useDomEditCompositionRect({ iframeRef, overlayRef });
|
|
21
|
+
}
|
|
@@ -4,17 +4,11 @@ import { useTimelinePlayer, usePlayerStore } from "../../player";
|
|
|
4
4
|
import type { TimelineElement } from "../../player";
|
|
5
5
|
import type { CompositionLevel } from "./CompositionBreadcrumb";
|
|
6
6
|
import { useCompositionStack } from "./useCompositionStack";
|
|
7
|
-
import { MIN_TIMELINE_H, fitTimelineHeight } from "../../utils/fitPanels";
|
|
8
7
|
import { setCompositionSourceMap } from "../editor/domEditingDom";
|
|
9
8
|
import { ensureMotionPathPluginLoaded } from "../../utils/gsapSoftReload";
|
|
10
|
-
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
11
9
|
import { useAssetPreviewStore } from "../../utils/assetPreviewStore";
|
|
12
10
|
import { createStableContext } from "../../utils/hmrStableContext";
|
|
13
11
|
|
|
14
|
-
// Timeline gets a generous default height so the preview isn't oversized and the
|
|
15
|
-
// tracks have room to breathe (CapCut-style). Users can still drag the divider.
|
|
16
|
-
const DEFAULT_TIMELINE_H = 360;
|
|
17
|
-
|
|
18
12
|
export function shouldDisableTimelineWhileCompositionLoading(compositionLoading: boolean): boolean {
|
|
19
13
|
return compositionLoading;
|
|
20
14
|
}
|
|
@@ -34,10 +28,6 @@ export interface NLEContextValue {
|
|
|
34
28
|
handleDrillDown: (element: TimelineElement) => void;
|
|
35
29
|
compIdToSrc: Map<string, string>;
|
|
36
30
|
// layout state
|
|
37
|
-
timelineH: number;
|
|
38
|
-
setTimelineH: React.Dispatch<React.SetStateAction<number>>;
|
|
39
|
-
persistTimelineH: (height: number) => void;
|
|
40
|
-
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
41
31
|
// composition loading
|
|
42
32
|
compositionLoading: boolean;
|
|
43
33
|
setCompositionLoading: (loading: boolean) => void;
|
|
@@ -256,35 +246,6 @@ export function NLEProvider({
|
|
|
256
246
|
});
|
|
257
247
|
}, [compIdToSrc]);
|
|
258
248
|
|
|
259
|
-
// Resizable timeline height — persisted alongside zoom/pan so the user's
|
|
260
|
-
// workspace layout survives reloads.
|
|
261
|
-
const [timelineH, setTimelineH] = useState(() => {
|
|
262
|
-
const stored = readStudioUiPreferences().timelineHeight;
|
|
263
|
-
return stored !== undefined && stored >= MIN_TIMELINE_H ? stored : DEFAULT_TIMELINE_H;
|
|
264
|
-
});
|
|
265
|
-
const persistTimelineH = useCallback((height: number) => {
|
|
266
|
-
writeStudioUiPreferences({ timelineHeight: Math.round(height) });
|
|
267
|
-
}, []);
|
|
268
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
269
|
-
// A height persisted on a tall window can exceed this window's container and
|
|
270
|
-
// collapse the flex-1 preview to 0px. Observing the container rather than
|
|
271
|
-
// clamping once at mount is what makes a window RESIZED after load behave the
|
|
272
|
-
// same as one loaded at that size: dragging 760 -> 520 tall used to leave the
|
|
273
|
-
// timeline at its stored 429px and the preview at 47px.
|
|
274
|
-
useEffect(() => {
|
|
275
|
-
const element = containerRef.current;
|
|
276
|
-
if (!element || typeof ResizeObserver === "undefined") return;
|
|
277
|
-
const reconcile = () => {
|
|
278
|
-
const containerH = element.getBoundingClientRect().height;
|
|
279
|
-
if (!containerH) return;
|
|
280
|
-
setTimelineH((prev) => fitTimelineHeight(containerH, prev));
|
|
281
|
-
};
|
|
282
|
-
reconcile();
|
|
283
|
-
const observer = new ResizeObserver(reconcile);
|
|
284
|
-
observer.observe(element);
|
|
285
|
-
return () => observer.disconnect();
|
|
286
|
-
}, []);
|
|
287
|
-
|
|
288
249
|
const hasLoadedOnceRef = useRef(false);
|
|
289
250
|
const [compositionLoading, setCompositionLoadingRaw] = useState(true);
|
|
290
251
|
const setCompositionLoading = useCallback((loading: boolean) => {
|
|
@@ -317,10 +278,6 @@ export function NLEProvider({
|
|
|
317
278
|
handleNavigateComposition,
|
|
318
279
|
handleDrillDown,
|
|
319
280
|
compIdToSrc,
|
|
320
|
-
timelineH,
|
|
321
|
-
setTimelineH,
|
|
322
|
-
persistTimelineH,
|
|
323
|
-
containerRef,
|
|
324
281
|
compositionLoading,
|
|
325
282
|
setCompositionLoading,
|
|
326
283
|
timelineDisabled,
|
|
@@ -442,7 +442,7 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
442
442
|
<div className="flex flex-col h-full min-h-0">
|
|
443
443
|
<div
|
|
444
444
|
ref={viewportRef}
|
|
445
|
-
className="relative flex-1 flex items-center justify-center p-2 overflow-hidden min-h-0 outline-hidden focus:ring-1 focus:ring-studio-accent/40 bg-neutral-950"
|
|
445
|
+
className="relative flex-1 flex items-center justify-center p-2 overflow-hidden min-h-0 outline-hidden focus:ring-1 focus:ring-studio-accent/40 bg-[var(--studio-preview-bg,var(--color-neutral-950))]"
|
|
446
446
|
tabIndex={0}
|
|
447
447
|
aria-label="Composition preview"
|
|
448
448
|
>
|
|
@@ -110,7 +110,7 @@ export function PreviewPane({
|
|
|
110
110
|
ref={containerRef}
|
|
111
111
|
// Panel chrome (rounded border) is dropped in fullscreen so the preview
|
|
112
112
|
// fills the screen edge-to-edge.
|
|
113
|
-
className={`flex-1 min-h-0 flex flex-col overflow-hidden bg-neutral-950 ${
|
|
113
|
+
className={`flex-1 min-h-0 flex flex-col overflow-hidden bg-[var(--studio-preview-bg,var(--color-neutral-950))] ${
|
|
114
114
|
isFullscreen ? "" : "rounded-lg border border-neutral-800/50"
|
|
115
115
|
}`}
|
|
116
116
|
data-studio-fullscreen-target=""
|
|
@@ -2,7 +2,6 @@ import { useCallback, type ReactNode } from "react";
|
|
|
2
2
|
import { Timeline } from "../../player";
|
|
3
3
|
import type { TimelineElement } from "../../player";
|
|
4
4
|
import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing";
|
|
5
|
-
import { TimelineResizeDivider } from "./TimelineResizeDivider";
|
|
6
5
|
import { useTimelineEditContext } from "../../contexts/TimelineEditContext";
|
|
7
6
|
import { trackStudioExpandedClipEdit } from "../../telemetry/events";
|
|
8
7
|
import { useNLEContext } from "./NLEContext";
|
|
@@ -78,6 +77,9 @@ export interface TimelinePaneProps {
|
|
|
78
77
|
timelineToolbar?: ReactNode;
|
|
79
78
|
/** Slot rendered below the timeline tracks */
|
|
80
79
|
timelineFooter?: ReactNode;
|
|
80
|
+
/** Slot for a host's own overlay spanning the whole timeline area (e.g. a draft/ghost
|
|
81
|
+
* clip), positioned absolutely the same way PreviewPane's previewOverlay is. */
|
|
82
|
+
timelineOverlay?: ReactNode;
|
|
81
83
|
/** Custom clip content renderer for timeline (thumbnails, waveforms, etc.) */
|
|
82
84
|
renderClipContent?: (
|
|
83
85
|
element: TimelineElement,
|
|
@@ -114,6 +116,7 @@ export interface TimelinePaneProps {
|
|
|
114
116
|
export function TimelinePane({
|
|
115
117
|
timelineToolbar,
|
|
116
118
|
timelineFooter,
|
|
119
|
+
timelineOverlay,
|
|
117
120
|
renderClipContent,
|
|
118
121
|
onFileDrop,
|
|
119
122
|
onDeleteElement,
|
|
@@ -132,10 +135,6 @@ export function TimelinePane({
|
|
|
132
135
|
handleDrillDown,
|
|
133
136
|
compositionStack,
|
|
134
137
|
updateCompositionStack,
|
|
135
|
-
timelineH,
|
|
136
|
-
setTimelineH,
|
|
137
|
-
persistTimelineH,
|
|
138
|
-
containerRef,
|
|
139
138
|
timelineDisabled,
|
|
140
139
|
timelineSessionEpoch,
|
|
141
140
|
} = useNLEContext();
|
|
@@ -254,72 +253,59 @@ export function TimelinePane({
|
|
|
254
253
|
);
|
|
255
254
|
|
|
256
255
|
return (
|
|
257
|
-
|
|
258
|
-
<TimelineResizeDivider
|
|
259
|
-
timelineH={timelineH}
|
|
260
|
-
setTimelineH={setTimelineH}
|
|
261
|
-
persistTimelineH={persistTimelineH}
|
|
262
|
-
containerRef={containerRef}
|
|
263
|
-
disabled={timelineDisabled}
|
|
264
|
-
/>
|
|
265
|
-
|
|
266
|
-
{/* Timeline section — inner padding (not margin) keeps the divider's
|
|
267
|
-
height math exact while giving the panel a gap from the shell edges. */}
|
|
256
|
+
<div className="relative flex h-full flex-col" aria-disabled={timelineDisabled || undefined}>
|
|
268
257
|
<div
|
|
269
|
-
className="
|
|
270
|
-
|
|
271
|
-
|
|
258
|
+
className="flex flex-col flex-1 min-h-0 overflow-hidden rounded-lg border border-neutral-800/50 bg-neutral-950"
|
|
259
|
+
onDoubleClick={(e) => {
|
|
260
|
+
if ((e.target as HTMLElement).closest("[data-clip]")) return;
|
|
261
|
+
if (timelineDisabled) return;
|
|
262
|
+
if (compositionStack.length > 1) {
|
|
263
|
+
updateCompositionStack((prev) => prev.slice(0, -1));
|
|
264
|
+
}
|
|
265
|
+
}}
|
|
272
266
|
>
|
|
267
|
+
<div className="shrink-0">{timelineToolbar}</div>
|
|
268
|
+
<Timeline
|
|
269
|
+
sessionEpoch={timelineSessionEpoch}
|
|
270
|
+
onSeek={seek}
|
|
271
|
+
onDrillDown={handleDrillDown}
|
|
272
|
+
renderClipContent={renderClipContent}
|
|
273
|
+
onFileDrop={onFileDrop}
|
|
274
|
+
onDeleteElement={handleDeleteElement}
|
|
275
|
+
onAssetDrop={onAssetDrop}
|
|
276
|
+
onBlockDrop={onBlockDrop}
|
|
277
|
+
onCompositionDrop={onCompositionDrop}
|
|
278
|
+
onMoveElement={handleMoveElement}
|
|
279
|
+
onMoveElements={handleMoveElements}
|
|
280
|
+
onResizeElement={handleResizeElement}
|
|
281
|
+
onResizeElements={handleResizeElements}
|
|
282
|
+
onBlockedEditAttempt={onBlockedEditAttempt}
|
|
283
|
+
onSplitElement={handleSplitElement}
|
|
284
|
+
onSelectElement={onSelectTimelineElement}
|
|
285
|
+
onCopyClip={onCopyClip}
|
|
286
|
+
onPasteClip={onPasteClip}
|
|
287
|
+
onDuplicateClip={onDuplicateClip}
|
|
288
|
+
canPasteClip={canPasteClip}
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
{timelineFooter && <div className="shrink-0">{timelineFooter}</div>}
|
|
292
|
+
{timelineOverlay && (
|
|
293
|
+
<div className="pointer-events-none absolute inset-0 z-20">{timelineOverlay}</div>
|
|
294
|
+
)}
|
|
295
|
+
{timelineDisabled && (
|
|
273
296
|
<div
|
|
274
|
-
className="
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
}}
|
|
297
|
+
className="absolute inset-0 z-30 cursor-not-allowed bg-black/18 flex items-center justify-center"
|
|
298
|
+
data-testid="timeline-loading-disabled-overlay"
|
|
299
|
+
role="status"
|
|
300
|
+
onPointerDown={(event) => event.preventDefault()}
|
|
301
|
+
onDragOver={(event) => event.preventDefault()}
|
|
302
|
+
onDrop={(event) => event.preventDefault()}
|
|
282
303
|
>
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
onSeek={seek}
|
|
287
|
-
onDrillDown={handleDrillDown}
|
|
288
|
-
renderClipContent={renderClipContent}
|
|
289
|
-
onFileDrop={onFileDrop}
|
|
290
|
-
onDeleteElement={handleDeleteElement}
|
|
291
|
-
onAssetDrop={onAssetDrop}
|
|
292
|
-
onBlockDrop={onBlockDrop}
|
|
293
|
-
onCompositionDrop={onCompositionDrop}
|
|
294
|
-
onMoveElement={handleMoveElement}
|
|
295
|
-
onMoveElements={handleMoveElements}
|
|
296
|
-
onResizeElement={handleResizeElement}
|
|
297
|
-
onResizeElements={handleResizeElements}
|
|
298
|
-
onBlockedEditAttempt={onBlockedEditAttempt}
|
|
299
|
-
onSplitElement={handleSplitElement}
|
|
300
|
-
onSelectElement={onSelectTimelineElement}
|
|
301
|
-
onCopyClip={onCopyClip}
|
|
302
|
-
onPasteClip={onPasteClip}
|
|
303
|
-
onDuplicateClip={onDuplicateClip}
|
|
304
|
-
canPasteClip={canPasteClip}
|
|
305
|
-
/>
|
|
304
|
+
<span className="rounded-md bg-neutral-900/90 px-2.5 py-1 text-[11px] text-neutral-400">
|
|
305
|
+
Loading composition…
|
|
306
|
+
</span>
|
|
306
307
|
</div>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
<div
|
|
310
|
-
className="absolute inset-0 z-30 cursor-not-allowed bg-black/18 flex items-center justify-center"
|
|
311
|
-
data-testid="timeline-loading-disabled-overlay"
|
|
312
|
-
role="status"
|
|
313
|
-
onPointerDown={(event) => event.preventDefault()}
|
|
314
|
-
onDragOver={(event) => event.preventDefault()}
|
|
315
|
-
onDrop={(event) => event.preventDefault()}
|
|
316
|
-
>
|
|
317
|
-
<span className="rounded-md bg-neutral-900/90 px-2.5 py-1 text-[11px] text-neutral-400">
|
|
318
|
-
Loading composition…
|
|
319
|
-
</span>
|
|
320
|
-
</div>
|
|
321
|
-
)}
|
|
322
|
-
</div>
|
|
323
|
-
</>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
324
310
|
);
|
|
325
311
|
}
|
|
@@ -156,7 +156,7 @@ export function makeSlideshowNotesController(): NotesController {
|
|
|
156
156
|
// ── Component ─────────────────────────────────────────────────────────────
|
|
157
157
|
|
|
158
158
|
export interface SlideshowPanelProps {
|
|
159
|
-
/** Scenes from the live clip manifest (passed from
|
|
159
|
+
/** Scenes from the live clip manifest (passed from StudioRightPanels). */
|
|
160
160
|
scenes: import("./slideshowPanelHelpers").SceneInfo[];
|
|
161
161
|
/**
|
|
162
162
|
* Called with the updated manifest after every discrete edit (toggle, add,
|
|
@@ -246,7 +246,7 @@ const EMPTY_STATE = (
|
|
|
246
246
|
</p>
|
|
247
247
|
);
|
|
248
248
|
|
|
249
|
-
// Panel orchestrator — JSX conditionals per section, same shape as
|
|
249
|
+
// Panel orchestrator — JSX conditionals per section, same shape as StudioRightPanels.
|
|
250
250
|
// fallow-ignore-next-line complexity
|
|
251
251
|
export const VariablesPanel = memo(function VariablesPanel({
|
|
252
252
|
sdkSession,
|
|
@@ -6,6 +6,14 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
6
6
|
import { CompositionsTab } from "./sidebar/CompositionsTab";
|
|
7
7
|
import { TimelineToolbar } from "./TimelineToolbar";
|
|
8
8
|
|
|
9
|
+
vi.mock("../contexts/StudioContext", () => ({
|
|
10
|
+
useStudioShellContext: () => ({
|
|
11
|
+
editHistory: { canUndo: false, canRedo: false },
|
|
12
|
+
handleUndo: vi.fn(),
|
|
13
|
+
handleRedo: vi.fn(),
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
9
17
|
// WCAG 2.2 (2.5.8) requires a 24x24 CSS pixel pointer target. happy-dom has no
|
|
10
18
|
// CSS engine, so getBoundingClientRect() is 0x0 for everything here and the size
|
|
11
19
|
// itself cannot be measured — these assert the utility classes that PRODUCE the
|
|
@@ -7,7 +7,7 @@ import { usePreviewVariablesStore } from "../../hooks/previewVariablesStore";
|
|
|
7
7
|
/**
|
|
8
8
|
* The Renders tab, wired to the shell context.
|
|
9
9
|
*
|
|
10
|
-
* Split out of
|
|
10
|
+
* Split out of StudioRightPanels because every field it needs already lives in
|
|
11
11
|
* that context, so routing them through the panel only made the panel longer
|
|
12
12
|
* without giving anything a second reader.
|
|
13
13
|
*/
|
|
@@ -4,7 +4,7 @@ import { act } from "react";
|
|
|
4
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { usePlayerStore } from "../../player/store/playerStore";
|
|
7
|
-
import {
|
|
7
|
+
import { CompositionsPanel } from "./CompositionsPanel";
|
|
8
8
|
|
|
9
9
|
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
10
10
|
(
|
|
@@ -20,23 +20,20 @@ afterEach(() => {
|
|
|
20
20
|
usePlayerStore.setState({ thumbnailContentRevision: 0 });
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
describe("
|
|
23
|
+
describe("CompositionsPanel root badge source", () => {
|
|
24
24
|
it("derives the root badge from the filtered compositions list, not the raw file tree", () => {
|
|
25
|
-
//
|
|
26
|
-
// server's filtered `compositions` list
|
|
27
|
-
// `compositions`, not the raw file tree.
|
|
25
|
+
// The project's raw file tree can hold a non-composition index.html (a vendored
|
|
26
|
+
// preset); the badge must source from the server's filtered `compositions` list.
|
|
28
27
|
const host = document.createElement("div");
|
|
29
28
|
document.body.append(host);
|
|
30
29
|
root = createRoot(host);
|
|
31
30
|
act(() => {
|
|
32
31
|
root?.render(
|
|
33
|
-
<
|
|
32
|
+
<CompositionsPanel
|
|
34
33
|
projectId="demo"
|
|
35
34
|
compositions={["hero.html"]}
|
|
36
|
-
assets={[]}
|
|
37
|
-
fileTree={["index.html", "hero.html"]}
|
|
38
35
|
activeComposition={null}
|
|
39
|
-
|
|
36
|
+
onSelect={vi.fn()}
|
|
40
37
|
/>,
|
|
41
38
|
);
|
|
42
39
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { resolveMasterCompositionPath } from "../../utils/studioUrlState";
|
|
3
|
+
import { CompositionsTab } from "./CompositionsTab";
|
|
4
|
+
|
|
5
|
+
type Props = Omit<ComponentProps<typeof CompositionsTab>, "masterCompositionPath">;
|
|
6
|
+
|
|
7
|
+
/** The Compositions dock panel body; the root badge comes from the filtered composition list. */
|
|
8
|
+
export function CompositionsPanel(props: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<CompositionsTab
|
|
11
|
+
{...props}
|
|
12
|
+
masterCompositionPath={resolveMasterCompositionPath(props.compositions)}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
package/src/components/sidebar/{LeftSidebar.lintBadge.test.tsx → SidebarLintButton.test.tsx}
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { act } from "react";
|
|
4
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import {
|
|
6
|
+
import { SidebarLintButton } from "./SidebarLintButton";
|
|
7
7
|
|
|
8
8
|
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
9
9
|
(
|
|
@@ -24,23 +24,18 @@ function renderBadge(lintHasError: boolean): HTMLElement | null {
|
|
|
24
24
|
root = createRoot(host);
|
|
25
25
|
act(() => {
|
|
26
26
|
root?.render(
|
|
27
|
-
<
|
|
28
|
-
projectId="demo"
|
|
29
|
-
compositions={["index.html"]}
|
|
30
|
-
assets={[]}
|
|
31
|
-
fileTree={["index.html"]}
|
|
32
|
-
activeComposition={null}
|
|
33
|
-
onSelectComposition={vi.fn()}
|
|
27
|
+
<SidebarLintButton
|
|
34
28
|
onLint={vi.fn()}
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
linting={false}
|
|
30
|
+
findingCount={2}
|
|
31
|
+
hasError={lintHasError}
|
|
37
32
|
/>,
|
|
38
33
|
);
|
|
39
34
|
});
|
|
40
35
|
return host.querySelector<HTMLElement>("[data-lint-badge]");
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
describe("
|
|
38
|
+
describe("SidebarLintButton badge", () => {
|
|
44
39
|
it("pulses, and stops for reduced motion, while an error finding exists", () => {
|
|
45
40
|
const badge = renderBadge(true);
|
|
46
41
|
expect(badge?.dataset.lintBadge).toBe("error");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function SidebarLintButton({
|
|
2
|
+
onLint,
|
|
3
|
+
linting,
|
|
4
|
+
findingCount,
|
|
5
|
+
hasError,
|
|
6
|
+
}: {
|
|
7
|
+
onLint: () => void;
|
|
8
|
+
linting: boolean;
|
|
9
|
+
findingCount?: number;
|
|
10
|
+
hasError?: boolean;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="border-t border-neutral-800 p-2 shrink-0">
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
onClick={onLint}
|
|
17
|
+
disabled={linting}
|
|
18
|
+
className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-md text-[11px] font-medium text-neutral-500 enabled:hover:text-amber-300 enabled:hover:bg-neutral-800 enabled:active:scale-[0.98] transition-colors disabled:opacity-40"
|
|
19
|
+
>
|
|
20
|
+
<svg
|
|
21
|
+
width="12"
|
|
22
|
+
height="12"
|
|
23
|
+
viewBox="0 0 24 24"
|
|
24
|
+
fill="none"
|
|
25
|
+
stroke="currentColor"
|
|
26
|
+
strokeWidth="2"
|
|
27
|
+
>
|
|
28
|
+
<path d="M9 11l3 3L22 4" />
|
|
29
|
+
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
|
|
30
|
+
</svg>
|
|
31
|
+
{linting ? "Linting…" : "Lint"}
|
|
32
|
+
{!linting && findingCount != null && findingCount > 0 && (
|
|
33
|
+
<span
|
|
34
|
+
data-lint-badge={hasError ? "error" : "warning"}
|
|
35
|
+
className={
|
|
36
|
+
hasError
|
|
37
|
+
? "ml-1 min-w-[16px] rounded-full bg-panel-danger/25 px-1 text-[9px] font-bold text-panel-danger animate-pulse motion-reduce:animate-none"
|
|
38
|
+
: "ml-1 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[9px] font-bold text-amber-400"
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
{findingCount}
|
|
42
|
+
<span className="sr-only">
|
|
43
|
+
{hasError ? " lint findings, including errors" : " lint findings, warnings only"}
|
|
44
|
+
</span>
|
|
45
|
+
</span>
|
|
46
|
+
)}
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// CapCut-flat icon buttons: a transparent 28px hit area with a subtle hover wash.
|
|
2
|
+
export const flatBtn = "flex h-7 w-7 items-center justify-center rounded-md transition-colors";
|
|
3
|
+
export const flatIdle = `${flatBtn} text-neutral-400 hover:bg-white/6 hover:text-neutral-200 active:scale-[0.98]`;
|
|
4
|
+
export const flatActive = `${flatBtn} bg-white/8 text-neutral-100 active:scale-[0.98]`;
|
|
5
|
+
export const flatDisabled = `${flatBtn} text-neutral-700 cursor-not-allowed`;
|