@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
|
@@ -1,18 +1,64 @@
|
|
|
1
1
|
import type { Direction, DockviewApi } from "dockview-react";
|
|
2
2
|
import { DOCK_PANEL_COMPONENT } from "./dockLayoutSchema";
|
|
3
|
-
import { PANEL_DEFINITIONS, type PanelId } from "./panelRegistry";
|
|
3
|
+
import { PANEL_DEFINITIONS, isPanelId, type PanelId } from "./panelRegistry";
|
|
4
4
|
|
|
5
5
|
const MIN_PREVIEW_W = 360;
|
|
6
6
|
const MIN_PREVIEW_H = 200;
|
|
7
7
|
const MIN_TIMELINE_H = 100;
|
|
8
8
|
const MIN_SIDE_W = 200;
|
|
9
|
+
const MIN_SIDE_W_FLOOR = 120;
|
|
9
10
|
const DEFAULT_TIMELINE_H = 360;
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
/** Preferred side widths; when they overflow the preview's floor the right yields first, then the left. */
|
|
13
|
+
export function defaultSideWidths(viewportWidth: number) {
|
|
14
|
+
const floor = sideMinimumWidth(viewportWidth);
|
|
15
|
+
const left = Math.max(floor, Math.min(384, Math.round(viewportWidth * 0.257)));
|
|
16
|
+
const right = Math.max(Math.min(280, floor), Math.min(424, Math.round(viewportWidth * 0.284)));
|
|
17
|
+
const excess = left + right - Math.max(0, viewportWidth - MIN_PREVIEW_W);
|
|
18
|
+
if (excess <= 0) return { left, right };
|
|
19
|
+
const shrunkRight = Math.max(floor, right - excess);
|
|
20
|
+
return { left: Math.max(floor, left - (excess - (right - shrunkRight))), right: shrunkRight };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Side groups keep MIN_SIDE_W while both fit beside the preview's floor, then shrink down to
|
|
25
|
+
* MIN_SIDE_W_FLOOR; below that the dock overflows and clips rather than squeezing the preview.
|
|
26
|
+
*/
|
|
27
|
+
export function sideMinimumWidth(dockWidth: number): number {
|
|
28
|
+
const fair = Math.floor((dockWidth - MIN_PREVIEW_W) / 2);
|
|
29
|
+
return Math.min(MIN_SIDE_W, Math.max(MIN_SIDE_W_FLOOR, fair));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function holdsPreview(group: DockviewApi["groups"][number]) {
|
|
33
|
+
return group.panels.some((panel) => panel.id === "preview");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The zone of a group whose every tab is a side panel, else null. */
|
|
37
|
+
function sideZone(group: DockviewApi["groups"][number]): "left" | "right" | null {
|
|
38
|
+
const zones = group.panels.map((panel) =>
|
|
39
|
+
isPanelId(panel.id) ? PANEL_DEFINITIONS[panel.id].zone : "center",
|
|
40
|
+
);
|
|
41
|
+
const first = zones[0];
|
|
42
|
+
const allSide = zones.every((zone) => zone !== "center");
|
|
43
|
+
return allSide && first && first !== "center" ? first : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Idempotent; rewrites every group's minimum, since dockview keeps a constraint once set. */
|
|
47
|
+
export function applySideMinimums(api: DockviewApi, dockWidth = window.innerWidth) {
|
|
48
|
+
const minimumWidth = sideMinimumWidth(dockWidth);
|
|
49
|
+
const cap = defaultSideWidths(dockWidth);
|
|
50
|
+
for (const group of api.groups) {
|
|
51
|
+
if (holdsPreview(group)) {
|
|
52
|
+
group.api.setConstraints({ minimumWidth: MIN_PREVIEW_W });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
group.api.setConstraints({ minimumWidth });
|
|
56
|
+
const zone = sideZone(group);
|
|
57
|
+
const limit = zone === "right" ? cap.right : cap.left;
|
|
58
|
+
if (zone && minimumWidth < MIN_SIDE_W && group.width > limit) {
|
|
59
|
+
group.api.setSize({ width: limit });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
16
62
|
}
|
|
17
63
|
|
|
18
64
|
function minimumSize(id: PanelId) {
|
|
@@ -52,6 +98,7 @@ export function buildEditLayout(api: DockviewApi, viewportWidth: number) {
|
|
|
52
98
|
}
|
|
53
99
|
api.getPanel("compositions")?.api.setActive();
|
|
54
100
|
api.getPanel("design")?.api.setActive();
|
|
101
|
+
applySideMinimums(api, viewportWidth);
|
|
55
102
|
api.getPanel("compositions")?.group.api.setSize({ width: widths.left });
|
|
56
103
|
api.getPanel("design")?.group.api.setSize({ width: widths.right });
|
|
57
104
|
api.getPanel("timeline")?.group.api.setSize({ height: DEFAULT_TIMELINE_H });
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { useDockLayoutStore } from "./dockLayoutStore";
|
|
3
|
+
import type { PanelId } from "./panelRegistry";
|
|
4
|
+
|
|
5
|
+
const snapshot = (groupActivePanels: PanelId[], activePanel: PanelId | null = null) => ({
|
|
6
|
+
openPanels: new Set<PanelId>(["design", "renders", "compositions"]),
|
|
7
|
+
visiblePanels: new Set<PanelId>(groupActivePanels),
|
|
8
|
+
activePanel,
|
|
9
|
+
groupActivePanels,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("dock store sync", () => {
|
|
13
|
+
beforeEach(() => useDockLayoutStore.setState({ lastActive: {} }));
|
|
14
|
+
|
|
15
|
+
it("records what every side group shows, not only the globally active panel", () => {
|
|
16
|
+
useDockLayoutStore.getState().sync(snapshot(["compositions", "renders"], "compositions"));
|
|
17
|
+
expect(useDockLayoutStore.getState().lastActive).toEqual({
|
|
18
|
+
left: "compositions",
|
|
19
|
+
right: "renders",
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("forgets the previous dock's memory on detach", () => {
|
|
24
|
+
useDockLayoutStore.getState().sync(snapshot(["renders"], "renders"));
|
|
25
|
+
useDockLayoutStore.getState().detach();
|
|
26
|
+
expect(useDockLayoutStore.getState().lastActive).toEqual({});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -25,6 +25,8 @@ export interface DockSnapshot {
|
|
|
25
25
|
/** Open panels whose tab is showing and whose group is not hidden. */
|
|
26
26
|
visiblePanels: ReadonlySet<PanelId>;
|
|
27
27
|
activePanel: PanelId | null;
|
|
28
|
+
/** The tab each group shows, so a restored layout still records what every side column last showed. */
|
|
29
|
+
groupActivePanels?: readonly PanelId[];
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
type LastActive = Partial<Record<PanelZone, PanelId>>;
|
|
@@ -53,13 +55,17 @@ export const useDockLayoutStore = create<DockLayoutState>((set, get) => ({
|
|
|
53
55
|
lastActive: {},
|
|
54
56
|
pendingActivation: null,
|
|
55
57
|
attach: (controller) => set({ controller }),
|
|
56
|
-
detach: () => set({ controller: null }),
|
|
58
|
+
detach: () => set({ controller: null, lastActive: {} }),
|
|
57
59
|
sync: (snapshot) =>
|
|
58
60
|
set((state) => {
|
|
61
|
+
const lastActive = { ...state.lastActive };
|
|
62
|
+
for (const id of snapshot.groupActivePanels ?? []) {
|
|
63
|
+
const zone = PANEL_DEFINITIONS[id].zone;
|
|
64
|
+
if (zone !== "center") lastActive[zone] = id;
|
|
65
|
+
}
|
|
59
66
|
const { activePanel } = snapshot;
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
return { ...snapshot, lastActive: { ...state.lastActive, [zone]: activePanel } };
|
|
67
|
+
if (activePanel) lastActive[PANEL_DEFINITIONS[activePanel].zone] = activePanel;
|
|
68
|
+
return { ...snapshot, lastActive };
|
|
63
69
|
}),
|
|
64
70
|
takePendingActivation: () => {
|
|
65
71
|
const { pendingActivation } = get();
|
|
@@ -89,3 +95,14 @@ export const useDockLayoutStore = create<DockLayoutState>((set, get) => ({
|
|
|
89
95
|
},
|
|
90
96
|
resetLayout: () => get().controller?.reset(),
|
|
91
97
|
}));
|
|
98
|
+
|
|
99
|
+
/** The panel of `zone` the user is looking at: the last one focused if still showing, else any showing. */
|
|
100
|
+
export function visiblePanelInZone(
|
|
101
|
+
zone: PanelZone,
|
|
102
|
+
lastActive: LastActive,
|
|
103
|
+
visiblePanels: ReadonlySet<PanelId>,
|
|
104
|
+
): PanelId | null {
|
|
105
|
+
const last = lastActive[zone];
|
|
106
|
+
if (last && visiblePanels.has(last)) return last;
|
|
107
|
+
return panelsInZone(zone).find((id) => visiblePanels.has(id)) ?? null;
|
|
108
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createDockview, type DockviewApi } from "dockview-react";
|
|
2
|
+
|
|
3
|
+
/** A real dockview mounted on a fresh host element, with empty panel bodies. */
|
|
4
|
+
export function mountBareDockview(): { host: HTMLElement; api: DockviewApi } {
|
|
5
|
+
const host = document.createElement("div");
|
|
6
|
+
document.body.append(host);
|
|
7
|
+
const api = createDockview(host, {
|
|
8
|
+
createComponent: () => ({ element: document.createElement("div"), init() {}, dispose() {} }),
|
|
9
|
+
});
|
|
10
|
+
return { host, api };
|
|
11
|
+
}
|
|
@@ -1,12 +1,55 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
|
+
import { createElement, act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
5
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
4
6
|
import { Window } from "happy-dom";
|
|
7
|
+
import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
|
|
5
8
|
import type { DomEditLayerItem } from "./domEditingTypes";
|
|
6
|
-
import { createRafThrottle, sortLayersByZIndex } from "./LayersPanel";
|
|
9
|
+
import { createRafThrottle, LayersPanel, sortLayersByZIndex } from "./LayersPanel";
|
|
7
10
|
import { isLayerDraggable } from "./useLayerDrag";
|
|
8
11
|
import { liveTime } from "../../player";
|
|
9
12
|
|
|
13
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
|
+
|
|
15
|
+
const mocks = vi.hoisted(() => ({
|
|
16
|
+
previewIframeRef: { current: null } as { current: unknown },
|
|
17
|
+
collect: vi.fn(),
|
|
18
|
+
setActiveGroupElement: vi.fn(),
|
|
19
|
+
showToast: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("../../contexts/StudioContext", () => ({
|
|
23
|
+
useStudioShellContext: () => ({
|
|
24
|
+
previewIframeRef: mocks.previewIframeRef,
|
|
25
|
+
activeCompPath: "index.html",
|
|
26
|
+
showToast: mocks.showToast,
|
|
27
|
+
}),
|
|
28
|
+
useStudioPlaybackContext: () => ({
|
|
29
|
+
refreshKey: 0,
|
|
30
|
+
compositionLoading: true,
|
|
31
|
+
timelineElements: [],
|
|
32
|
+
isPlaying: false,
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
vi.mock("../../contexts/DomEditContext", () => ({
|
|
36
|
+
useDomEditContext: () => ({
|
|
37
|
+
domEditSelection: null,
|
|
38
|
+
activeGroupElement: null,
|
|
39
|
+
applyDomSelection: vi.fn(),
|
|
40
|
+
updateDomEditHoverSelection: vi.fn(),
|
|
41
|
+
handleDomZIndexReorderCommit: vi.fn(),
|
|
42
|
+
setActiveGroupElement: mocks.setActiveGroupElement,
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
vi.mock("../nle/useCanvasZOrderTimelineMirror", () => ({
|
|
46
|
+
useLayerReorderTimelineMirror: () => vi.fn(),
|
|
47
|
+
}));
|
|
48
|
+
vi.mock("./domEditing", async (importOriginal) => ({
|
|
49
|
+
...(await importOriginal<typeof import("./domEditing")>()),
|
|
50
|
+
collectDomEditLayerItems: mocks.collect,
|
|
51
|
+
}));
|
|
52
|
+
|
|
10
53
|
function makeLayer(
|
|
11
54
|
overrides: Partial<DomEditLayerItem> & { zIndex?: string; locked?: boolean },
|
|
12
55
|
): DomEditLayerItem {
|
|
@@ -197,3 +240,56 @@ describe("liveTime subscribe / unsubscribe (LayersPanel scrub contract)", () =>
|
|
|
197
240
|
unsubscribe();
|
|
198
241
|
});
|
|
199
242
|
});
|
|
243
|
+
|
|
244
|
+
describe("LayersPanel preview promotion", () => {
|
|
245
|
+
let root: Root;
|
|
246
|
+
let host: HTMLDivElement;
|
|
247
|
+
|
|
248
|
+
function makeIframe(compId: string): HTMLIFrameElement {
|
|
249
|
+
const iframe = document.createElement("iframe");
|
|
250
|
+
const doc = new Window().document;
|
|
251
|
+
doc.body.innerHTML = `<div data-composition-id="${compId}"></div>`;
|
|
252
|
+
Object.defineProperty(iframe, "contentDocument", { value: doc });
|
|
253
|
+
return iframe;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const collectedRootIds = () =>
|
|
257
|
+
mocks.collect.mock.calls.map((call) => (call[0] as HTMLElement).dataset.compositionId);
|
|
258
|
+
|
|
259
|
+
beforeEach(() => {
|
|
260
|
+
mocks.collect.mockReset().mockReturnValue([]);
|
|
261
|
+
host = document.createElement("div");
|
|
262
|
+
document.body.append(host);
|
|
263
|
+
root = createRoot(host);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
afterEach(() => {
|
|
267
|
+
act(() => root.unmount());
|
|
268
|
+
host.remove();
|
|
269
|
+
usePreviewIframeStore.setState({ iframe: null });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("recollects layers from the promoted document's load, not the replaced one", () => {
|
|
273
|
+
const a = makeIframe("a");
|
|
274
|
+
const b = makeIframe("b");
|
|
275
|
+
mocks.previewIframeRef.current = a;
|
|
276
|
+
usePreviewIframeStore.setState({ iframe: a });
|
|
277
|
+
act(() => root.render(createElement(LayersPanel)));
|
|
278
|
+
|
|
279
|
+
act(() => {
|
|
280
|
+
mocks.previewIframeRef.current = b;
|
|
281
|
+
usePreviewIframeStore.getState().setIframe(b);
|
|
282
|
+
});
|
|
283
|
+
mocks.collect.mockClear();
|
|
284
|
+
|
|
285
|
+
act(() => {
|
|
286
|
+
a.dispatchEvent(new Event("load"));
|
|
287
|
+
});
|
|
288
|
+
expect(mocks.collect).not.toHaveBeenCalled();
|
|
289
|
+
|
|
290
|
+
act(() => {
|
|
291
|
+
b.dispatchEvent(new Event("load"));
|
|
292
|
+
});
|
|
293
|
+
expect(collectedRootIds()).toEqual(["b"]);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
|
|
1
2
|
import { memo, useState, useCallback, useEffect, useRef } from "react";
|
|
2
3
|
import {
|
|
3
4
|
collectDomEditLayerItems,
|
|
@@ -122,6 +123,7 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
122
123
|
|
|
123
124
|
const isMasterView = !activeCompPath || activeCompPath === "index.html";
|
|
124
125
|
|
|
126
|
+
const livePreviewIframe = useLivePreviewIframe();
|
|
125
127
|
const collectLayers = useCallback(() => {
|
|
126
128
|
const iframe = previewIframeRef.current;
|
|
127
129
|
if (!iframe) return;
|
|
@@ -152,7 +154,7 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
152
154
|
|
|
153
155
|
useEffect(() => {
|
|
154
156
|
collectLayers();
|
|
155
|
-
}, [collectLayers, refreshKey, zEditVersion]);
|
|
157
|
+
}, [collectLayers, refreshKey, zEditVersion, livePreviewIframe]);
|
|
156
158
|
|
|
157
159
|
useEffect(() => {
|
|
158
160
|
const iframe = previewIframeRef.current;
|
|
@@ -163,7 +165,7 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
163
165
|
};
|
|
164
166
|
iframe.addEventListener("load", handleLoad);
|
|
165
167
|
return () => iframe.removeEventListener("load", handleLoad);
|
|
166
|
-
}, [previewIframeRef, collectLayers]);
|
|
168
|
+
}, [previewIframeRef, livePreviewIframe, collectLayers]);
|
|
167
169
|
|
|
168
170
|
useEffect(() => {
|
|
169
171
|
if (!compositionLoading) {
|
|
@@ -8,7 +8,7 @@ interface PreviewGuidesProps {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const TICKS = Array.from({ length: 11 }, (_, i) => i * 10);
|
|
11
|
-
const INK = "
|
|
11
|
+
const INK = "color-mix(in srgb, white 70%, transparent)";
|
|
12
12
|
|
|
13
13
|
/** Ruler and safe-margin boxes drawn over the preview pane, never inside the composition. */
|
|
14
14
|
export const PreviewGuides = memo(function PreviewGuides({ iframeRef }: PreviewGuidesProps) {
|
|
@@ -5,7 +5,6 @@ import { createRoot } from "react-dom/client";
|
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { useAppHotkeys } from "../../hooks/useAppHotkeys";
|
|
7
7
|
import { usePlayerStore } from "../../player/store/playerStore";
|
|
8
|
-
import type { LeftSidebarHandle } from "../sidebar/LeftSidebar";
|
|
9
8
|
import type { DomEditSelection } from "./domEditing";
|
|
10
9
|
import { SnapToolbar } from "./SnapToolbar";
|
|
11
10
|
import { usePreviewGuidesStore } from "./previewGuidesStore";
|
|
@@ -31,7 +30,6 @@ function renderToolbar(onSnapChange = vi.fn()) {
|
|
|
31
30
|
function AppHotkeyHarness() {
|
|
32
31
|
const domEditSelectionRef = useRef<DomEditSelection | null>(null);
|
|
33
32
|
const clearDomSelectionRef = useRef<() => void>(() => undefined);
|
|
34
|
-
const leftSidebarRef = useRef<LeftSidebarHandle | null>(null);
|
|
35
33
|
|
|
36
34
|
useAppHotkeys({
|
|
37
35
|
handleTimelineElementsDelete: vi.fn(async () => {}),
|
|
@@ -50,7 +48,6 @@ function AppHotkeyHarness() {
|
|
|
50
48
|
showToast: vi.fn(),
|
|
51
49
|
syncHistoryPreviewAfterApply: vi.fn(async () => undefined),
|
|
52
50
|
waitForPendingDomEditSaves: vi.fn(async () => undefined),
|
|
53
|
-
leftSidebarRef,
|
|
54
51
|
handleCopy: vi.fn(() => false),
|
|
55
52
|
handlePaste: vi.fn(async () => undefined),
|
|
56
53
|
handleCut: vi.fn(async () => false),
|
|
@@ -4,6 +4,7 @@ import React, { act } from "react";
|
|
|
4
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { studioEditLifecycle, type StudioWriteResult } from "../../webmcp/writeCoordinator";
|
|
7
|
+
import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
|
|
7
8
|
import { TopologyLens } from "./TopologyLens";
|
|
8
9
|
|
|
9
10
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -291,4 +292,31 @@ describe("TopologyLens", () => {
|
|
|
291
292
|
expect(vi.getTimerCount()).toBe(0);
|
|
292
293
|
expect(studioEditLifecycle.getSnapshot()).toEqual({ phase: "idle" });
|
|
293
294
|
});
|
|
295
|
+
|
|
296
|
+
it("dismisses on the promoted iframe's load, not the replaced one", () => {
|
|
297
|
+
const a = document.createElement("iframe");
|
|
298
|
+
const b = document.createElement("iframe");
|
|
299
|
+
document.body.append(a, b);
|
|
300
|
+
const ref = { current: a as HTMLIFrameElement | null };
|
|
301
|
+
usePreviewIframeStore.setState({ iframe: a });
|
|
302
|
+
window.matchMedia = matchMedia(false);
|
|
303
|
+
host = document.createElement("div");
|
|
304
|
+
document.body.append(host);
|
|
305
|
+
root = createRoot(host);
|
|
306
|
+
act(() => {
|
|
307
|
+
root?.render(<TopologyLens iframeRef={ref} activeCompositionPath="index.html" />);
|
|
308
|
+
});
|
|
309
|
+
begin();
|
|
310
|
+
|
|
311
|
+
act(() => {
|
|
312
|
+
ref.current = b;
|
|
313
|
+
usePreviewIframeStore.getState().setIframe(b);
|
|
314
|
+
});
|
|
315
|
+
act(() => a.dispatchEvent(new Event("load")));
|
|
316
|
+
expect(host.querySelector('[data-topology-lens="hidden"]')).toBeNull();
|
|
317
|
+
|
|
318
|
+
act(() => b.dispatchEvent(new Event("load")));
|
|
319
|
+
expect(host.querySelector('[data-topology-lens="hidden"]')).not.toBeNull();
|
|
320
|
+
usePreviewIframeStore.setState({ iframe: null });
|
|
321
|
+
});
|
|
294
322
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
|
|
1
2
|
import {
|
|
2
3
|
useEffect,
|
|
3
4
|
useLayoutEffect,
|
|
@@ -113,13 +114,14 @@ export function TopologyLens({ iframeRef, activeCompositionPath }: TopologyLensP
|
|
|
113
114
|
setMeasured(geometry ? { callId, geometry } : null);
|
|
114
115
|
}, [activeCompositionPath, callId, handle, iframeRef, phase]);
|
|
115
116
|
|
|
117
|
+
const livePreviewIframe = useLivePreviewIframe();
|
|
116
118
|
useEffect(() => {
|
|
117
119
|
const iframe = iframeRef.current;
|
|
118
120
|
if (!iframe || !callId) return;
|
|
119
121
|
const dismiss = () => studioEditLifecycle.dismiss(callId);
|
|
120
122
|
iframe.addEventListener("load", dismiss);
|
|
121
123
|
return () => iframe.removeEventListener("load", dismiss);
|
|
122
|
-
}, [callId, iframeRef]);
|
|
124
|
+
}, [callId, iframeRef, livePreviewIframe]);
|
|
123
125
|
|
|
124
126
|
useEffect(() => {
|
|
125
127
|
if (pendingUnmountDismissRef.current !== null) {
|
|
@@ -4,6 +4,8 @@ import React, { act } from "react";
|
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { normalizeHfColorGrading } from "@hyperframes/core/color-grading";
|
|
7
|
+
import { runtimeProtocolMetadata } from "@hyperframes/core/runtime/protocol";
|
|
8
|
+
import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
|
|
7
9
|
import { useColorGradingController } from "./useColorGradingController";
|
|
8
10
|
import type { DomEditSelection } from "./domEditing";
|
|
9
11
|
|
|
@@ -22,6 +24,7 @@ function cleanStudioGrading() {
|
|
|
22
24
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
23
25
|
|
|
24
26
|
afterEach(() => {
|
|
27
|
+
usePreviewIframeStore.setState({ iframe: null });
|
|
25
28
|
document.body.innerHTML = "";
|
|
26
29
|
});
|
|
27
30
|
|
|
@@ -737,3 +740,38 @@ describe("useColorGradingController", () => {
|
|
|
737
740
|
vi.unstubAllGlobals();
|
|
738
741
|
});
|
|
739
742
|
});
|
|
743
|
+
|
|
744
|
+
describe("useColorGradingController after the preview iframe is replaced", () => {
|
|
745
|
+
it("replays the grade to the new iframe on its ready message and ignores the old one", () => {
|
|
746
|
+
vi.useFakeTimers();
|
|
747
|
+
const a = createPreviewFrame();
|
|
748
|
+
const b = createPreviewFrame();
|
|
749
|
+
const ref = { current: a.iframe as HTMLIFrameElement | null };
|
|
750
|
+
usePreviewIframeStore.getState().setIframe(a.iframe);
|
|
751
|
+
const initialElement = makeElement({
|
|
752
|
+
dataAttributes: { "color-grading": JSON.stringify({ preset: "bright-pop", intensity: 1 }) },
|
|
753
|
+
});
|
|
754
|
+
const { root } = renderHook(vi.fn(), initialElement, ref);
|
|
755
|
+
act(() => vi.advanceTimersByTime(100));
|
|
756
|
+
|
|
757
|
+
const postToA = vi.spyOn(a.contentWindow, "postMessage");
|
|
758
|
+
const postToB = vi.spyOn(b.contentWindow, "postMessage");
|
|
759
|
+
act(() => {
|
|
760
|
+
ref.current = b.iframe;
|
|
761
|
+
usePreviewIframeStore.getState().setIframe(b.iframe);
|
|
762
|
+
});
|
|
763
|
+
const ready = { source: "hf-preview", type: "ready", ...runtimeProtocolMetadata(30) };
|
|
764
|
+
act(() => {
|
|
765
|
+
window.dispatchEvent(new MessageEvent("message", { data: ready, source: a.contentWindow }));
|
|
766
|
+
});
|
|
767
|
+
expect(colorGradingMessages(postToB.mock.calls)).toHaveLength(0);
|
|
768
|
+
act(() => {
|
|
769
|
+
window.dispatchEvent(new MessageEvent("message", { data: ready, source: b.contentWindow }));
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
expect(colorGradingMessages(postToB.mock.calls)).toHaveLength(1);
|
|
773
|
+
expect(postToA).not.toHaveBeenCalled();
|
|
774
|
+
act(() => root.unmount());
|
|
775
|
+
vi.useRealTimers();
|
|
776
|
+
});
|
|
777
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
|
|
1
2
|
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
2
3
|
import {
|
|
3
4
|
HF_COLOR_GRADING_ATTR,
|
|
@@ -430,6 +431,7 @@ export function useColorGradingController({
|
|
|
430
431
|
[previewIframeRef, target],
|
|
431
432
|
);
|
|
432
433
|
|
|
434
|
+
const livePreviewIframe = useLivePreviewIframe();
|
|
433
435
|
useEffect(() => {
|
|
434
436
|
const iframe = previewIframeRef?.current;
|
|
435
437
|
if (!iframe) return;
|
|
@@ -455,7 +457,13 @@ export function useColorGradingController({
|
|
|
455
457
|
window.removeEventListener("message", onMessage);
|
|
456
458
|
window.clearTimeout(timer);
|
|
457
459
|
};
|
|
458
|
-
}, [
|
|
460
|
+
}, [
|
|
461
|
+
postColorGrading,
|
|
462
|
+
postCompare,
|
|
463
|
+
previewIframeRef,
|
|
464
|
+
scheduleRuntimeStatusRefresh,
|
|
465
|
+
livePreviewIframe,
|
|
466
|
+
]);
|
|
459
467
|
|
|
460
468
|
useEffect(
|
|
461
469
|
() => () => {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { normalizeHfColorGrading } from "@hyperframes/core/color-grading";
|
|
7
|
+
import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
|
|
8
|
+
import { useColorGradingPreviews } from "./useColorGradingPreviews";
|
|
9
|
+
|
|
10
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
usePreviewIframeStore.setState({ iframe: null });
|
|
14
|
+
document.body.innerHTML = "";
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function createFrame() {
|
|
18
|
+
return document.body.appendChild(document.createElement("iframe"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function installRenderer(iframe: HTMLIFrameElement) {
|
|
22
|
+
const renderPreviews = vi.fn().mockResolvedValue({
|
|
23
|
+
width: 160,
|
|
24
|
+
height: 90,
|
|
25
|
+
images: [{ id: "bright-pop", dataUrl: "data:image/png;base64,x" }],
|
|
26
|
+
});
|
|
27
|
+
(iframe.contentWindow as unknown as { __hf: unknown }).__hf = {
|
|
28
|
+
colorGrading: { renderPreviews },
|
|
29
|
+
};
|
|
30
|
+
return renderPreviews;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("useColorGradingPreviews after the preview iframe is replaced", () => {
|
|
34
|
+
it("requests preset previews from the new iframe's runtime", async () => {
|
|
35
|
+
vi.useFakeTimers();
|
|
36
|
+
const a = createFrame();
|
|
37
|
+
const b = createFrame();
|
|
38
|
+
const renderOnB = installRenderer(b);
|
|
39
|
+
const ref = { current: a as HTMLIFrameElement | null };
|
|
40
|
+
usePreviewIframeStore.getState().setIframe(a);
|
|
41
|
+
const grading = normalizeHfColorGrading({ preset: "neutral" });
|
|
42
|
+
if (!grading) throw new Error("expected neutral grading");
|
|
43
|
+
const gradingRef = { current: grading };
|
|
44
|
+
const target = { selector: "#v" } as never;
|
|
45
|
+
const postColorGrading = vi.fn();
|
|
46
|
+
let requestPresetPreviews: () => void = () => {};
|
|
47
|
+
function Host() {
|
|
48
|
+
requestPresetPreviews = useColorGradingPreviews({
|
|
49
|
+
grading,
|
|
50
|
+
gradingRef,
|
|
51
|
+
identityKey: "k",
|
|
52
|
+
target,
|
|
53
|
+
previewIframeRef: ref,
|
|
54
|
+
postColorGrading,
|
|
55
|
+
}).requestPresetPreviews;
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const root = createRoot(document.body.appendChild(document.createElement("div")));
|
|
59
|
+
act(() => root.render(React.createElement(Host)));
|
|
60
|
+
act(() => requestPresetPreviews());
|
|
61
|
+
act(() => vi.advanceTimersByTime(0));
|
|
62
|
+
|
|
63
|
+
act(() => {
|
|
64
|
+
ref.current = b;
|
|
65
|
+
usePreviewIframeStore.getState().setIframe(b);
|
|
66
|
+
});
|
|
67
|
+
act(() => vi.advanceTimersByTime(0));
|
|
68
|
+
await act(async () => {
|
|
69
|
+
await Promise.resolve();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(renderOnB).toHaveBeenCalled();
|
|
73
|
+
act(() => root.unmount());
|
|
74
|
+
vi.useRealTimers();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
|
|
1
2
|
import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
|
2
3
|
import {
|
|
3
4
|
HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS,
|
|
@@ -155,6 +156,7 @@ export function useColorGradingPreviews({
|
|
|
155
156
|
}
|
|
156
157
|
const previews = state.identityKey === identityKey ? state.previews : emptyPreviews();
|
|
157
158
|
|
|
159
|
+
const livePreviewIframe = useLivePreviewIframe();
|
|
158
160
|
useEffect(() => {
|
|
159
161
|
if (!request) return;
|
|
160
162
|
const { kind } = request;
|
|
@@ -245,7 +247,15 @@ export function useColorGradingPreviews({
|
|
|
245
247
|
iframe.removeEventListener("load", attempt);
|
|
246
248
|
window.removeEventListener("message", onMessage);
|
|
247
249
|
};
|
|
248
|
-
}, [
|
|
250
|
+
}, [
|
|
251
|
+
grading.effects,
|
|
252
|
+
grading.lut,
|
|
253
|
+
grading.palette,
|
|
254
|
+
previewIframeRef,
|
|
255
|
+
livePreviewIframe,
|
|
256
|
+
request,
|
|
257
|
+
target,
|
|
258
|
+
]);
|
|
249
259
|
|
|
250
260
|
const stopAnimatedPreview = useCallback(() => {
|
|
251
261
|
const session = animatedRef.current;
|