@hyperframes/studio 0.8.52 → 0.8.53
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-I5YRzB02.js → hyperframes-player-DKWM-Ons.js} +1 -1
- package/dist/assets/index-DrHQAuGO.css +1 -0
- package/dist/assets/index-HfP3sap2.js +390 -0
- package/dist/assets/{index-BTAfgesM.js → index-UvW6BEwf.js} +1 -1
- package/dist/assets/{index-Du5x7KUR.js → index-Y4OnpTEM.js} +1 -1
- package/dist/index.css +29 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.html +2 -2
- package/dist/index.js +7664 -7953
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -0
- package/package.json +8 -7
- package/src/App.tsx +42 -48
- package/src/components/EditorShell.selectionSync.test.tsx +9 -4
- package/src/components/EditorShell.tsx +52 -56
- package/src/components/StudioHeader.dom.test.tsx +1 -1
- package/src/components/StudioHeader.test.ts +0 -12
- package/src/components/StudioHeader.tsx +10 -13
- package/src/components/StudioLeftPanels.tsx +202 -0
- package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -206
- package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
- package/src/components/dock/Dock.test.tsx +16 -2
- package/src/components/dock/Dock.tsx +13 -3
- package/src/components/dock/dockLayoutStore.test.ts +28 -0
- package/src/components/dock/dockLayoutStore.ts +21 -4
- package/src/components/editor/SnapToolbar.test.tsx +0 -3
- package/src/components/nle/NLEContext.tsx +0 -43
- package/src/components/nle/TimelinePane.tsx +46 -67
- package/src/components/panels/SlideshowPanel.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +1 -1
- package/src/components/renders/RenderQueue.test.tsx +133 -25
- package/src/components/renders/RenderQueue.tsx +99 -92
- package/src/components/renders/RenderQueueItem.tsx +69 -64
- 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/ui/Select.tsx +4 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -57
- package/src/headerExports.test.tsx +23 -0
- package/src/hooks/appHotkeysDispatch.ts +3 -4
- 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 +0 -4
- package/src/hooks/useDomEditPreviewSync.ts +4 -6
- package/src/hooks/useDomEditSession.ts +0 -7
- package/src/hooks/useDomEditWiring.ts +4 -9
- package/src/hooks/usePanelLayout.test.ts +108 -296
- package/src/hooks/usePanelLayout.ts +50 -255
- package/src/hooks/useRightPanelIntents.test.ts +181 -0
- package/src/hooks/useRightPanelIntents.ts +64 -0
- 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/index.ts +2 -1
- package/src/main.tsx +26 -0
- package/src/player/components/Timeline.tsx +1 -1
- package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
- package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
- package/src/utils/studioHelpers.ts +0 -7
- package/src/utils/studioUiPreferences.test.ts +10 -16
- package/src/utils/studioUiPreferences.ts +0 -16
- package/dist/assets/index-CxAu_Okg.css +0 -1
- package/dist/assets/index-DocJlW2v.js +0 -376
- 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,29 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, type MutableRefObject } from "react";
|
|
2
2
|
import { SLIDESHOW_ISLAND_TYPE, slideshowIslandRegex } from "@hyperframes/core/slideshow";
|
|
3
3
|
import type { SceneInfo } from "../components/panels/SlideshowPanel";
|
|
4
4
|
import type { IframeWindow } from "../player/lib/playbackTypes";
|
|
5
|
-
import type { RightPanelTab } from "../utils/studioHelpers";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Derives whether the currently-edited composition is a slideshow (carries
|
|
9
8
|
* the slideshow JSON island — the same definitive marker the CLI's `present`
|
|
10
9
|
* command requires; it refuses to run without one) and the live scene list
|
|
11
|
-
* for the Slideshow panel
|
|
12
|
-
*
|
|
13
|
-
* while that tab was open) so the panel never shows a dangling active tab
|
|
14
|
-
* whose button is no longer even rendered.
|
|
15
|
-
*
|
|
16
|
-
* Extracted from StudioRightPanel to keep that file under the 600-LOC gate.
|
|
10
|
+
* for the Slideshow panel. Extracted from StudioRightPanels to keep that file
|
|
11
|
+
* under the 600-LOC gate.
|
|
17
12
|
*/
|
|
18
13
|
export function useSlideshowTabState(params: {
|
|
19
14
|
editingFileContent: string | null | undefined;
|
|
20
15
|
previewIframeRef: MutableRefObject<HTMLIFrameElement | null>;
|
|
21
16
|
refreshKey: number;
|
|
22
|
-
|
|
23
|
-
setRightPanelTab: (tab: RightPanelTab) => void;
|
|
17
|
+
slideshowVisible: boolean;
|
|
24
18
|
}): { isSlideshowComposition: boolean; slideshowScenes: SceneInfo[] } {
|
|
25
|
-
const { editingFileContent, previewIframeRef, refreshKey,
|
|
26
|
-
params;
|
|
19
|
+
const { editingFileContent, previewIframeRef, refreshKey, slideshowVisible } = params;
|
|
27
20
|
|
|
28
21
|
// Presence-only (not full manifest validation): a malformed island should
|
|
29
22
|
// still surface the Slideshow tab so the user can see/fix it, rather than
|
|
@@ -49,13 +42,7 @@ export function useSlideshowTabState(params: {
|
|
|
49
42
|
return [];
|
|
50
43
|
}
|
|
51
44
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
-
}, [previewIframeRef,
|
|
53
|
-
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
if (rightPanelTab === "slideshow" && !isSlideshowComposition) {
|
|
56
|
-
setRightPanelTab("renders");
|
|
57
|
-
}
|
|
58
|
-
}, [rightPanelTab, isSlideshowComposition, setRightPanelTab]);
|
|
45
|
+
}, [previewIframeRef, slideshowVisible, refreshKey]);
|
|
59
46
|
|
|
60
47
|
return { isSlideshowComposition, slideshowScenes };
|
|
61
48
|
}
|
|
@@ -4,14 +4,12 @@ import React from "react";
|
|
|
4
4
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
7
|
-
import type {
|
|
7
|
+
import type { PanelId } from "../components/dock/panelRegistry";
|
|
8
8
|
import { makeSelection } from "./domSelectionTestHarness";
|
|
9
9
|
import { useInspectorState, type InspectorState } from "./useStudioContextValue";
|
|
10
10
|
|
|
11
11
|
interface HarnessProps {
|
|
12
|
-
|
|
13
|
-
rightInspectorPanes: RightInspectorPanes;
|
|
14
|
-
rightCollapsed: boolean;
|
|
12
|
+
rightPanel: PanelId | null;
|
|
15
13
|
isPlaying: boolean;
|
|
16
14
|
isGestureRecording: boolean;
|
|
17
15
|
domEditSelection: DomEditSelection | null;
|
|
@@ -22,9 +20,7 @@ function renderInspectorState(props: HarnessProps): InspectorState {
|
|
|
22
20
|
|
|
23
21
|
function Harness() {
|
|
24
22
|
state = useInspectorState(
|
|
25
|
-
props.
|
|
26
|
-
props.rightInspectorPanes,
|
|
27
|
-
props.rightCollapsed,
|
|
23
|
+
props.rightPanel,
|
|
28
24
|
props.isPlaying,
|
|
29
25
|
props.domEditSelection,
|
|
30
26
|
props.isGestureRecording,
|
|
@@ -42,9 +38,7 @@ function selectedProps(
|
|
|
42
38
|
): HarnessProps & { domEditSelection: DomEditSelection } {
|
|
43
39
|
const element = document.createElement("div");
|
|
44
40
|
return {
|
|
45
|
-
|
|
46
|
-
rightInspectorPanes: { layers: false, design: false },
|
|
47
|
-
rightCollapsed: true,
|
|
41
|
+
rightPanel: "renders",
|
|
48
42
|
isPlaying: false,
|
|
49
43
|
isGestureRecording: false,
|
|
50
44
|
domEditSelection: makeSelection("Selected", element),
|
|
@@ -78,16 +72,10 @@ describe("useInspectorState", () => {
|
|
|
78
72
|
it("keeps selected DOM bounds coupled to the inspector or variables panel", () => {
|
|
79
73
|
expect(renderInspectorState(selectedProps()).shouldShowSelectedDomBounds).toBe(false);
|
|
80
74
|
expect(
|
|
81
|
-
renderInspectorState(
|
|
82
|
-
selectedProps({
|
|
83
|
-
rightPanelTab: "design",
|
|
84
|
-
rightInspectorPanes: { layers: false, design: true },
|
|
85
|
-
}),
|
|
86
|
-
).shouldShowSelectedDomBounds,
|
|
75
|
+
renderInspectorState(selectedProps({ rightPanel: "design" })).shouldShowSelectedDomBounds,
|
|
87
76
|
).toBe(true);
|
|
88
77
|
expect(
|
|
89
|
-
renderInspectorState(selectedProps({
|
|
90
|
-
.shouldShowSelectedDomBounds,
|
|
78
|
+
renderInspectorState(selectedProps({ rightPanel: "variables" })).shouldShowSelectedDomBounds,
|
|
91
79
|
).toBe(true);
|
|
92
80
|
});
|
|
93
81
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo, type DragEvent } from "react";
|
|
2
2
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
3
3
|
import type { StudioContextValue } from "../contexts/StudioContext";
|
|
4
|
-
import type {
|
|
4
|
+
import type { PanelId } from "../components/dock/panelRegistry";
|
|
5
5
|
import type { TimelineFileDropHandler } from "./useTimelineEditingTypes";
|
|
6
6
|
import { usePlayerStore } from "../player";
|
|
7
7
|
|
|
@@ -66,33 +66,21 @@ export function buildStudioContextValue(input: StudioContextInput): StudioContex
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export interface InspectorState {
|
|
69
|
-
layersPanelActive: boolean;
|
|
70
|
-
designPanelActive: boolean;
|
|
71
69
|
inspectorPanelActive: boolean;
|
|
72
|
-
inspectorButtonActive: boolean;
|
|
73
70
|
shouldShowMotionPath: boolean;
|
|
74
71
|
shouldShowSelectedDomBounds: boolean;
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
export function useInspectorState(
|
|
78
|
-
|
|
79
|
-
rightInspectorPanes: RightInspectorPanes,
|
|
80
|
-
rightCollapsed: boolean,
|
|
75
|
+
rightPanel: PanelId | null,
|
|
81
76
|
isPlaying: boolean,
|
|
82
77
|
domEditSelection: DomEditSelection | null,
|
|
83
78
|
isGestureRecording?: boolean,
|
|
84
79
|
): InspectorState {
|
|
85
|
-
// fallow-ignore-next-line complexity
|
|
86
80
|
return useMemo(() => {
|
|
87
|
-
const
|
|
88
|
-
const layersPanelActive = inspectorTabActive && rightInspectorPanes.layers;
|
|
89
|
-
const designPanelActive = inspectorTabActive && rightInspectorPanes.design;
|
|
90
|
-
const inspectorPanelActive = layersPanelActive || designPanelActive;
|
|
81
|
+
const inspectorPanelActive = rightPanel === "layers" || rightPanel === "design";
|
|
91
82
|
return {
|
|
92
|
-
layersPanelActive,
|
|
93
|
-
designPanelActive,
|
|
94
83
|
inspectorPanelActive,
|
|
95
|
-
inspectorButtonActive: !rightCollapsed && inspectorPanelActive,
|
|
96
84
|
// Deliberately wider than shouldShowSelectedDomBounds: the on-canvas path
|
|
97
85
|
// handles ARE the arc-drag affordance, so gating them on an open Inspector
|
|
98
86
|
// would make keyframe path editing reachable only from a side panel.
|
|
@@ -102,18 +90,9 @@ export function useInspectorState(
|
|
|
102
90
|
// The Variables tab also works against the canvas selection (bind card),
|
|
103
91
|
// so the selection outline stays visible there too.
|
|
104
92
|
shouldShowSelectedDomBounds:
|
|
105
|
-
(inspectorPanelActive ||
|
|
106
|
-
!isPlaying &&
|
|
107
|
-
!isGestureRecording,
|
|
93
|
+
(inspectorPanelActive || rightPanel === "variables") && !isPlaying && !isGestureRecording,
|
|
108
94
|
};
|
|
109
|
-
}, [
|
|
110
|
-
rightPanelTab,
|
|
111
|
-
rightInspectorPanes,
|
|
112
|
-
rightCollapsed,
|
|
113
|
-
isPlaying,
|
|
114
|
-
isGestureRecording,
|
|
115
|
-
domEditSelection,
|
|
116
|
-
]);
|
|
95
|
+
}, [rightPanel, isPlaying, isGestureRecording, domEditSelection]);
|
|
117
96
|
}
|
|
118
97
|
|
|
119
98
|
/** Lets an OS file drop reach `onDrop` anywhere in the shell; the timeline shows its own landing preview. */
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// UI primitives
|
|
2
|
-
export { Button, buttonBase, buttonVariants } from "./components/ui/Button";
|
|
2
|
+
export { Button, buttonBase, buttonSizes, buttonVariants } from "./components/ui/Button";
|
|
3
3
|
export type { ButtonSize, ButtonVariant, PreviewState } from "./components/ui/Button";
|
|
4
4
|
export { IconButton } from "./components/ui/IconButton";
|
|
5
5
|
export { Tab, TabPanel, Tabs, TabsList } from "./components/ui/Tabs";
|
|
6
6
|
export { Tooltip } from "./components/ui/Tooltip";
|
|
7
|
+
export { HyperframesLogo } from "./components/StudioHeader";
|
|
7
8
|
export { cn } from "./components/ui/cn";
|
|
8
9
|
export {
|
|
9
10
|
ContextMenu,
|
package/src/main.tsx
CHANGED
|
@@ -65,6 +65,10 @@ window.addEventListener("error", (event) => {
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
let filteredAssetErrorCount = 0;
|
|
68
|
+
let filteredAbortCount = 0;
|
|
69
|
+
// Bounded so a hostile extension cannot grow it; a novel abort message still surfaces once.
|
|
70
|
+
const seenAbortMessages = new Set<string>();
|
|
71
|
+
const MAX_SEEN_ABORT_MESSAGES = 20;
|
|
68
72
|
|
|
69
73
|
// fallow-ignore-next-line complexity
|
|
70
74
|
window.addEventListener("unhandledrejection", (event) => {
|
|
@@ -80,6 +84,28 @@ window.addEventListener("unhandledrejection", (event) => {
|
|
|
80
84
|
}
|
|
81
85
|
return;
|
|
82
86
|
}
|
|
87
|
+
// An AbortError rejection is a cancellation we asked for, not a failure. Our own
|
|
88
|
+
// fetch chains handle it; the unhandled copies come from browser extensions that
|
|
89
|
+
// wrap window.fetch and derive a promise from each request without a rejection
|
|
90
|
+
// handler, so they reject with our abort reason whenever a thumbnail lease is
|
|
91
|
+
// released mid-fetch. Filtered before the cap so a scroll burst cannot exhaust
|
|
92
|
+
// ERROR_CAP and silence the session's real rejections. Sampled per distinct
|
|
93
|
+
// message, so an abort we have not seen before (e.g. our own "Aborted") is
|
|
94
|
+
// recorded once instead of drowning in the extension flood.
|
|
95
|
+
if (props.error_name === "AbortError") {
|
|
96
|
+
filteredAbortCount++;
|
|
97
|
+
const message = props.error_message.slice(0, 200);
|
|
98
|
+
const novel =
|
|
99
|
+
!seenAbortMessages.has(message) && seenAbortMessages.size < MAX_SEEN_ABORT_MESSAGES;
|
|
100
|
+
if (novel) seenAbortMessages.add(message);
|
|
101
|
+
if (novel || filteredAbortCount % 100 === 0) {
|
|
102
|
+
trackStudioEvent("abort_rejection_filtered", {
|
|
103
|
+
error_message: message,
|
|
104
|
+
total_filtered: filteredAbortCount,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
83
109
|
|
|
84
110
|
rejectionCount++;
|
|
85
111
|
if (rejectionCount > ERROR_CAP) {
|
|
@@ -448,7 +448,7 @@ export const Timeline = memo(function Timeline({
|
|
|
448
448
|
return (
|
|
449
449
|
<div
|
|
450
450
|
ref={setContainerRef}
|
|
451
|
-
aria-label="Timeline"
|
|
451
|
+
aria-label="Timeline track view"
|
|
452
452
|
data-timeline-element-count={expandedElements.length}
|
|
453
453
|
className={`relative border-t select-none h-full overflow-hidden ${assetDrop.isDragOver ? "ring-1 ring-inset ring-studio-accent/60" : ""} ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
|
|
454
454
|
onMouseMove={updateRazorGuide}
|
|
@@ -14,7 +14,7 @@ describe("timeline performance diagnostics", () => {
|
|
|
14
14
|
|
|
15
15
|
it("reads mounted resources without mutating the timeline", () => {
|
|
16
16
|
document.body.innerHTML = `
|
|
17
|
-
<div aria-label="Timeline" data-timeline-scheduler-queued="3"
|
|
17
|
+
<div aria-label="Timeline track view" data-timeline-scheduler-queued="3"
|
|
18
18
|
data-timeline-scheduler-active="2" data-timeline-cache-bytes="4096">
|
|
19
19
|
<div data-timeline-row><div data-clip="true"></div><div data-clip="true"></div></div>
|
|
20
20
|
<div data-timeline-row><div data-clip="true"></div></div>
|
|
@@ -41,7 +41,8 @@ describe("timeline performance diagnostics", () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it("returns the zero baseline after unmount or reset removes the DOM", () => {
|
|
44
|
-
document.body.innerHTML =
|
|
44
|
+
document.body.innerHTML =
|
|
45
|
+
'<div aria-label="Timeline track view"><div data-clip="true"></div></div>';
|
|
45
46
|
expect(readTimelinePerformanceDiagnostics().mountedClipRoots).toBe(1);
|
|
46
47
|
|
|
47
48
|
document.body.replaceChildren();
|
|
@@ -69,7 +69,7 @@ function sumDataAttribute(root: ParentNode, selector: string, dataKey: string):
|
|
|
69
69
|
export function readTimelinePerformanceDiagnostics(
|
|
70
70
|
root: ParentNode = document,
|
|
71
71
|
): Readonly<TimelinePerformanceDiagnostics> {
|
|
72
|
-
const timelineRoots = root.querySelectorAll<HTMLElement>('[aria-label="Timeline"]');
|
|
72
|
+
const timelineRoots = root.querySelectorAll<HTMLElement>('[aria-label="Timeline track view"]');
|
|
73
73
|
let mountedTimelineDescendants = 0;
|
|
74
74
|
for (const timelineRoot of timelineRoots) {
|
|
75
75
|
mountedTimelineDescendants += timelineRoot.querySelectorAll("*").length;
|
|
@@ -22,13 +22,6 @@ export type RightPanelTab =
|
|
|
22
22
|
| "block-params"
|
|
23
23
|
| "slideshow"
|
|
24
24
|
| "variables";
|
|
25
|
-
export type RightInspectorPane = "layers" | "design";
|
|
26
|
-
|
|
27
|
-
export interface RightInspectorPanes {
|
|
28
|
-
layers: boolean;
|
|
29
|
-
design: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
export interface AgentModalAnchorPoint {
|
|
33
26
|
x: number;
|
|
34
27
|
y: number;
|
|
@@ -20,7 +20,6 @@ describe("studio UI preferences", () => {
|
|
|
20
20
|
const storage = createStorage();
|
|
21
21
|
|
|
22
22
|
writeStudioUiPreferences({ timelineVisible: false }, storage);
|
|
23
|
-
writeStudioUiPreferences({ leftWidth: 384, rightWidth: 424 }, storage);
|
|
24
23
|
writeStudioUiPreferences({ playbackRate: 1.5 }, storage);
|
|
25
24
|
writeStudioUiPreferences({ audioMuted: true }, storage);
|
|
26
25
|
writeStudioUiPreferences({ audioVolume: 0.4 }, storage);
|
|
@@ -28,8 +27,6 @@ describe("studio UI preferences", () => {
|
|
|
28
27
|
|
|
29
28
|
expect(readStudioUiPreferences(storage)).toEqual({
|
|
30
29
|
timelineVisible: false,
|
|
31
|
-
leftWidth: 384,
|
|
32
|
-
rightWidth: 424,
|
|
33
30
|
playbackRate: 1.5,
|
|
34
31
|
audioMuted: true,
|
|
35
32
|
audioVolume: 0.4,
|
|
@@ -57,9 +54,6 @@ describe("studio UI preferences", () => {
|
|
|
57
54
|
storage.setItem(
|
|
58
55
|
"hf-studio-ui-preferences",
|
|
59
56
|
JSON.stringify({
|
|
60
|
-
leftCollapsed: "yes",
|
|
61
|
-
leftWidth: "wide",
|
|
62
|
-
rightWidth: Number.NaN,
|
|
63
57
|
timelineVisible: true,
|
|
64
58
|
playbackRate: Number.NaN,
|
|
65
59
|
audioMuted: "false",
|
|
@@ -119,28 +113,28 @@ describe("thumbnailMode preference", () => {
|
|
|
119
113
|
describe("per-project scoping", () => {
|
|
120
114
|
it("keeps two projects' preferences independent", () => {
|
|
121
115
|
const storage = createStorage();
|
|
122
|
-
writeStudioUiPreferences({
|
|
123
|
-
writeStudioUiPreferences({
|
|
116
|
+
writeStudioUiPreferences({ playbackRate: 1.25 }, storage, "alpha");
|
|
117
|
+
writeStudioUiPreferences({ playbackRate: 2 }, storage, "beta");
|
|
124
118
|
|
|
125
|
-
expect(readStudioUiPreferences(storage, "alpha").
|
|
126
|
-
expect(readStudioUiPreferences(storage, "beta").
|
|
119
|
+
expect(readStudioUiPreferences(storage, "alpha").playbackRate).toBe(1.25);
|
|
120
|
+
expect(readStudioUiPreferences(storage, "beta").playbackRate).toBe(2);
|
|
127
121
|
});
|
|
128
122
|
|
|
129
123
|
it("a project with nothing saved yet inherits the pre-scoping global entry once", () => {
|
|
130
124
|
const storage = createStorage();
|
|
131
|
-
storage.setItem("hf-studio-ui-preferences", JSON.stringify({
|
|
125
|
+
storage.setItem("hf-studio-ui-preferences", JSON.stringify({ playbackRate: 0.5 }));
|
|
132
126
|
|
|
133
|
-
expect(readStudioUiPreferences(storage, "gamma").
|
|
127
|
+
expect(readStudioUiPreferences(storage, "gamma").playbackRate).toBe(0.5);
|
|
134
128
|
});
|
|
135
129
|
|
|
136
130
|
it("stops inheriting the global entry once the project has its own write", () => {
|
|
137
131
|
const storage = createStorage();
|
|
138
|
-
storage.setItem("hf-studio-ui-preferences", JSON.stringify({
|
|
132
|
+
storage.setItem("hf-studio-ui-preferences", JSON.stringify({ playbackRate: 0.5 }));
|
|
139
133
|
|
|
140
|
-
writeStudioUiPreferences({
|
|
134
|
+
writeStudioUiPreferences({ playbackRate: 1.75 }, storage, "gamma");
|
|
141
135
|
|
|
142
|
-
expect(readStudioUiPreferences(storage, "gamma").
|
|
143
|
-
expect(readStudioUiPreferences(storage, null).
|
|
136
|
+
expect(readStudioUiPreferences(storage, "gamma").playbackRate).toBe(1.75);
|
|
137
|
+
expect(readStudioUiPreferences(storage, null).playbackRate).toBe(0.5);
|
|
144
138
|
});
|
|
145
139
|
});
|
|
146
140
|
|
|
@@ -10,11 +10,7 @@ export interface StoredPreviewZoomState {
|
|
|
10
10
|
export type TimelineTimeDisplayMode = "time" | "frame";
|
|
11
11
|
|
|
12
12
|
export interface StudioUiPreferences {
|
|
13
|
-
leftCollapsed?: boolean;
|
|
14
|
-
leftWidth?: number;
|
|
15
|
-
rightWidth?: number;
|
|
16
13
|
timelineVisible?: boolean;
|
|
17
|
-
timelineHeight?: number;
|
|
18
14
|
playbackRate?: number;
|
|
19
15
|
audioMuted?: boolean;
|
|
20
16
|
audioVolume?: number;
|
|
@@ -83,21 +79,9 @@ function readStorage(storage: Storage | null, key: string): StudioUiPreferences
|
|
|
83
79
|
if (!isRecord(parsed)) return {};
|
|
84
80
|
|
|
85
81
|
const preferences: StudioUiPreferences = {};
|
|
86
|
-
if (typeof parsed.leftCollapsed === "boolean") {
|
|
87
|
-
preferences.leftCollapsed = parsed.leftCollapsed;
|
|
88
|
-
}
|
|
89
|
-
if (typeof parsed.leftWidth === "number" && Number.isFinite(parsed.leftWidth)) {
|
|
90
|
-
preferences.leftWidth = parsed.leftWidth;
|
|
91
|
-
}
|
|
92
|
-
if (typeof parsed.rightWidth === "number" && Number.isFinite(parsed.rightWidth)) {
|
|
93
|
-
preferences.rightWidth = parsed.rightWidth;
|
|
94
|
-
}
|
|
95
82
|
if (typeof parsed.timelineVisible === "boolean") {
|
|
96
83
|
preferences.timelineVisible = parsed.timelineVisible;
|
|
97
84
|
}
|
|
98
|
-
if (typeof parsed.timelineHeight === "number" && Number.isFinite(parsed.timelineHeight)) {
|
|
99
|
-
preferences.timelineHeight = parsed.timelineHeight;
|
|
100
|
-
}
|
|
101
85
|
if (typeof parsed.playbackRate === "number" && Number.isFinite(parsed.playbackRate)) {
|
|
102
86
|
preferences.playbackRate = parsed.playbackRate;
|
|
103
87
|
}
|