@hyperframes/studio 0.8.65 → 0.8.67
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-CwFrSySS.js → hyperframes-player-BF2Y9Rtn.js} +1 -1
- package/dist/assets/index-BBOktYKC.css +1 -0
- package/dist/assets/{index-a5nEhdAU.js → index-BXCEB5uw.js} +1 -1
- package/dist/assets/{index-Cs1lQrQ6.js → index-CGQ66fle.js} +200 -200
- package/dist/assets/{index-wQFEeTL3.js → index-Nnd43iYY.js} +1 -1
- package/dist/index.css +147 -16
- package/dist/index.css.map +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +4451 -4193
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/components/EditorShell.tsx +1 -3
- package/src/components/StudioLeftPanels.tsx +1 -1
- package/src/components/dock/Dock.tsx +8 -0
- package/src/components/dock/DockStripActions.tsx +63 -0
- package/src/components/dock/DockTab.tsx +68 -0
- package/src/components/dock/DockTabs.test.tsx +255 -0
- package/src/components/dock/dock.css +185 -16
- package/src/components/dock/dockTabFill.ts +124 -0
- package/src/components/dock/dockTabs.contract.test.ts +80 -0
- package/src/components/editor/manualEdits.test.ts +100 -0
- package/src/components/editor/manualEdits.ts +17 -0
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/TimelinePane.tsx +1 -1
- package/src/components/ui/Menu.tsx +16 -0
- package/src/components/ui/index.ts +1 -0
- package/src/styles/theme.css +8 -0
- package/dist/assets/index-BnsYeuxA.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.67",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
"dockview-react": "8.3.1",
|
|
52
52
|
"gsap": "^3.13.0",
|
|
53
53
|
"mediabunny": "^1.45.3",
|
|
54
|
-
"@hyperframes/player": "0.8.
|
|
55
|
-
"@hyperframes/
|
|
56
|
-
"@hyperframes/
|
|
57
|
-
"@hyperframes/
|
|
58
|
-
"@hyperframes/
|
|
54
|
+
"@hyperframes/player": "0.8.67",
|
|
55
|
+
"@hyperframes/core": "0.8.67",
|
|
56
|
+
"@hyperframes/sdk": "0.8.67",
|
|
57
|
+
"@hyperframes/studio-server": "0.8.67",
|
|
58
|
+
"@hyperframes/parsers": "0.8.67"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"vite": "^6.4.2",
|
|
72
72
|
"vitest": "^4.1.11",
|
|
73
73
|
"zustand": "^5.0.0",
|
|
74
|
-
"@hyperframes/producer": "0.8.
|
|
74
|
+
"@hyperframes/producer": "0.8.67"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"react": "19",
|
|
@@ -286,9 +286,7 @@ function EditorShellBody({
|
|
|
286
286
|
return (
|
|
287
287
|
<div
|
|
288
288
|
ref={containerRef}
|
|
289
|
-
|
|
290
|
-
// gaps between panels read as visible seams (CapCut-style).
|
|
291
|
-
className="flex flex-col flex-1 min-h-0 bg-panel-surface"
|
|
289
|
+
className="flex flex-col flex-1 min-h-0"
|
|
292
290
|
onKeyDown={handleKeyDown}
|
|
293
291
|
tabIndex={-1}
|
|
294
292
|
>
|
|
@@ -28,7 +28,7 @@ interface StudioLeftPanelsProps {
|
|
|
28
28
|
|
|
29
29
|
function PanelColumn({ footer, children }: { footer: ReactNode; children: ReactNode }) {
|
|
30
30
|
return (
|
|
31
|
-
<div className="flex h-full min-h-0 flex-col overflow-hidden
|
|
31
|
+
<div className="flex h-full min-h-0 flex-col overflow-hidden">
|
|
32
32
|
<div className="flex min-h-0 flex-1 flex-col">{children}</div>
|
|
33
33
|
{footer}
|
|
34
34
|
</div>
|
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
} from "dockview-react";
|
|
17
17
|
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
18
18
|
import { installDockAccessibility } from "./dockAccessibility";
|
|
19
|
+
import { DockStripActions } from "./DockStripActions";
|
|
20
|
+
import { DockTab } from "./DockTab";
|
|
21
|
+
import { installTabFill } from "./dockTabFill";
|
|
19
22
|
import { addRegisteredPanel, applySideMinimums, buildEditLayout } from "./dockLayout";
|
|
20
23
|
import { DOCK_PANEL_COMPONENT } from "./dockLayoutSchema";
|
|
21
24
|
import { useDockLayoutStore, type DockController, type DockSnapshot } from "./dockLayoutStore";
|
|
@@ -150,6 +153,7 @@ function Root({ projectId, children }: { projectId: string | null; children: Rea
|
|
|
150
153
|
applySideMinimums(api);
|
|
151
154
|
const root = api.groups[0]?.element.closest<HTMLElement>(".hf-dock");
|
|
152
155
|
const disposeAccessibility = root ? installDockAccessibility(api, root) : () => {};
|
|
156
|
+
const disposeTabFill = root ? installTabFill(api, root) : () => {};
|
|
153
157
|
// The dock spans the window (buildEditLayout sizes against it too); its own box lags a resize.
|
|
154
158
|
const resizeObserver = new ResizeObserver(() => applySideMinimums(api, window.innerWidth));
|
|
155
159
|
if (root) resizeObserver.observe(root);
|
|
@@ -189,6 +193,7 @@ function Root({ projectId, children }: { projectId: string | null; children: Rea
|
|
|
189
193
|
clearTimeout(timer);
|
|
190
194
|
for (const subscription of subscriptions) subscription.dispose();
|
|
191
195
|
disposeAccessibility();
|
|
196
|
+
disposeTabFill();
|
|
192
197
|
resizeObserver.disconnect();
|
|
193
198
|
useDockLayoutStore.getState().detach();
|
|
194
199
|
};
|
|
@@ -202,8 +207,11 @@ function Root({ projectId, children }: { projectId: string | null; children: Rea
|
|
|
202
207
|
key={projectId ?? ""}
|
|
203
208
|
className="hf-dock flex-1 min-h-0"
|
|
204
209
|
components={COMPONENTS}
|
|
210
|
+
defaultTabComponent={DockTab}
|
|
211
|
+
rightHeaderActionsComponent={DockStripActions}
|
|
205
212
|
defaultRenderer="always"
|
|
206
213
|
disableFloatingGroups
|
|
214
|
+
disableTabsOverflowList
|
|
207
215
|
onReady={onReady}
|
|
208
216
|
/>
|
|
209
217
|
{children}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
2
|
+
import { CornersIn, CornersOut, DotsThree, X } from "@phosphor-icons/react";
|
|
3
|
+
import type { IDockviewHeaderActionsProps } from "dockview-react";
|
|
4
|
+
import { IconButton, Menu, MenuCheckboxItem, MenuItem, MenuSeparator } from "../ui";
|
|
5
|
+
import { useDockLayoutStore } from "./dockLayoutStore";
|
|
6
|
+
import { PANEL_DEFINITIONS, isPanelId, panelsInZone } from "./panelRegistry";
|
|
7
|
+
|
|
8
|
+
/** Panel menu, maximise and close group, drawn on the active group's strip only. */
|
|
9
|
+
export function DockStripActions({
|
|
10
|
+
api,
|
|
11
|
+
containerApi,
|
|
12
|
+
panels,
|
|
13
|
+
isGroupActive,
|
|
14
|
+
}: IDockviewHeaderActionsProps) {
|
|
15
|
+
const subscribe = useCallback(
|
|
16
|
+
(onChange: () => void) => {
|
|
17
|
+
const subscription = containerApi.onDidMaximizedGroupChange(onChange);
|
|
18
|
+
return () => subscription.dispose();
|
|
19
|
+
},
|
|
20
|
+
[containerApi],
|
|
21
|
+
);
|
|
22
|
+
const maximized = useSyncExternalStore(subscribe, () => api.isMaximized());
|
|
23
|
+
const openPanels = useDockLayoutStore((state) => state.openPanels);
|
|
24
|
+
const togglePanel = useDockLayoutStore((state) => state.togglePanel);
|
|
25
|
+
const resetLayout = useDockLayoutStore((state) => state.resetLayout);
|
|
26
|
+
if (!isGroupActive) return null;
|
|
27
|
+
|
|
28
|
+
const first = panels.find((panel) => isPanelId(panel.id))?.id;
|
|
29
|
+
const menuPanels = isPanelId(first) ? panelsInZone(PANEL_DEFINITIONS[first].zone) : [];
|
|
30
|
+
return (
|
|
31
|
+
<div className="hf-dock-strip-actions">
|
|
32
|
+
<Menu
|
|
33
|
+
align="end"
|
|
34
|
+
aria-label="Panel menu"
|
|
35
|
+
trigger={<IconButton size="sm" aria-label="Panel menu" icon={<DotsThree size={16} />} />}
|
|
36
|
+
>
|
|
37
|
+
{menuPanels.map((id) => (
|
|
38
|
+
<MenuCheckboxItem
|
|
39
|
+
key={id}
|
|
40
|
+
checked={openPanels.has(id)}
|
|
41
|
+
onCheckedChange={() => togglePanel(id)}
|
|
42
|
+
>
|
|
43
|
+
{PANEL_DEFINITIONS[id].title}
|
|
44
|
+
</MenuCheckboxItem>
|
|
45
|
+
))}
|
|
46
|
+
<MenuSeparator />
|
|
47
|
+
<MenuItem onClick={resetLayout}>Reset layout</MenuItem>
|
|
48
|
+
</Menu>
|
|
49
|
+
<IconButton
|
|
50
|
+
size="sm"
|
|
51
|
+
aria-label={maximized ? "Restore panel" : "Maximize panel"}
|
|
52
|
+
icon={maximized ? <CornersIn size={16} /> : <CornersOut size={16} />}
|
|
53
|
+
onClick={() => (maximized ? api.exitMaximized() : api.maximize())}
|
|
54
|
+
/>
|
|
55
|
+
<IconButton
|
|
56
|
+
size="sm"
|
|
57
|
+
aria-label="Close group"
|
|
58
|
+
icon={<X size={16} />}
|
|
59
|
+
onClick={() => api.close()}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
2
|
+
import {
|
|
3
|
+
BracketsCurly,
|
|
4
|
+
ChartBarHorizontal,
|
|
5
|
+
Code,
|
|
6
|
+
FilmSlate,
|
|
7
|
+
Image,
|
|
8
|
+
Layout,
|
|
9
|
+
Monitor,
|
|
10
|
+
Presentation,
|
|
11
|
+
SlidersHorizontal,
|
|
12
|
+
SquaresFour,
|
|
13
|
+
Stack,
|
|
14
|
+
X,
|
|
15
|
+
type Icon,
|
|
16
|
+
} from "@phosphor-icons/react";
|
|
17
|
+
import type { IDockviewPanelHeaderProps } from "dockview-react";
|
|
18
|
+
import { isPanelId, type PanelId } from "./panelRegistry";
|
|
19
|
+
|
|
20
|
+
const TAB_ICONS: Record<PanelId, Icon> = {
|
|
21
|
+
preview: Monitor,
|
|
22
|
+
timeline: ChartBarHorizontal,
|
|
23
|
+
compositions: Layout,
|
|
24
|
+
assets: Image,
|
|
25
|
+
code: Code,
|
|
26
|
+
catalog: SquaresFour,
|
|
27
|
+
design: SlidersHorizontal,
|
|
28
|
+
layers: Stack,
|
|
29
|
+
renders: FilmSlate,
|
|
30
|
+
variables: BracketsCurly,
|
|
31
|
+
slideshow: Presentation,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A dock tab. dock.css shows the type icon and close glyph on the shown tab only, keyed on
|
|
36
|
+
* dockview's own tab class, so they swap in the frame the tab changes rather than a render later.
|
|
37
|
+
*/
|
|
38
|
+
export function DockTab({ api }: IDockviewPanelHeaderProps) {
|
|
39
|
+
const subscribe = useCallback(
|
|
40
|
+
(onChange: () => void) => {
|
|
41
|
+
const subscription = api.onDidTitleChange(onChange);
|
|
42
|
+
return () => subscription.dispose();
|
|
43
|
+
},
|
|
44
|
+
[api],
|
|
45
|
+
);
|
|
46
|
+
const title = useSyncExternalStore(subscribe, () => api.title ?? "");
|
|
47
|
+
const TypeIcon = isPanelId(api.id) ? TAB_ICONS[api.id] : null;
|
|
48
|
+
return (
|
|
49
|
+
<div className="hf-dock-tab">
|
|
50
|
+
{TypeIcon ? <TypeIcon className="hf-dock-tab-icon" size={14} aria-hidden /> : null}
|
|
51
|
+
<span className="hf-dock-tab-label">{title}</span>
|
|
52
|
+
{/* Same shape as dockview's own close control: a tab cannot hold a focusable button. */}
|
|
53
|
+
<div
|
|
54
|
+
role="button"
|
|
55
|
+
tabIndex={-1}
|
|
56
|
+
aria-label={`Close ${title}`}
|
|
57
|
+
className="hf-dock-tab-close"
|
|
58
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
59
|
+
onClick={(event) => {
|
|
60
|
+
event.stopPropagation();
|
|
61
|
+
api.close();
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<X size={12} aria-hidden />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { act } from "react";
|
|
6
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
7
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { Dock } from "./Dock";
|
|
9
|
+
import { useDockLayoutStore } from "./dockLayoutStore";
|
|
10
|
+
import { PANEL_IDS, type PanelId } from "./panelRegistry";
|
|
11
|
+
|
|
12
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
|
+
|
|
14
|
+
// happy-dom never resizes anything; a test calls `resizeAll` to stand in for a real resize.
|
|
15
|
+
const liveObservers = new Set<ResizeObserverStub>();
|
|
16
|
+
class ResizeObserverStub {
|
|
17
|
+
readonly callback: () => void;
|
|
18
|
+
constructor(callback: () => void) {
|
|
19
|
+
this.callback = callback;
|
|
20
|
+
liveObservers.add(this);
|
|
21
|
+
}
|
|
22
|
+
observe() {}
|
|
23
|
+
unobserve() {}
|
|
24
|
+
disconnect() {
|
|
25
|
+
liveObservers.delete(this);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const resizeAll = () => {
|
|
29
|
+
for (const observer of liveObservers) observer.callback();
|
|
30
|
+
};
|
|
31
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = ResizeObserverStub;
|
|
32
|
+
|
|
33
|
+
// happy-dom has no layout: a tab sits at 100px per position and is 90px wide.
|
|
34
|
+
const TAB_STEP = 100;
|
|
35
|
+
const TAB_WIDTH = 90;
|
|
36
|
+
const tabIndex = (element: HTMLElement) =>
|
|
37
|
+
element.classList.contains("dv-tab")
|
|
38
|
+
? [...(element.parentElement?.querySelectorAll(":scope > .dv-tab") ?? [])].indexOf(element)
|
|
39
|
+
: -1;
|
|
40
|
+
const layout = {
|
|
41
|
+
offsetLeft: {
|
|
42
|
+
get(this: HTMLElement) {
|
|
43
|
+
return Math.max(tabIndex(this), 0) * TAB_STEP;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
offsetWidth: {
|
|
47
|
+
get(this: HTMLElement) {
|
|
48
|
+
return tabIndex(this) >= 0 ? TAB_WIDTH : 0;
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const original = {
|
|
53
|
+
offsetLeft: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetLeft"),
|
|
54
|
+
offsetWidth: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth"),
|
|
55
|
+
};
|
|
56
|
+
beforeAll(() => {
|
|
57
|
+
// The shown-tab-only rule lives in dock.css; apply it so computed display is what a user sees.
|
|
58
|
+
const style = document.createElement("style");
|
|
59
|
+
style.textContent = readFileSync(path.join(import.meta.dirname, "dock.css"), "utf8").replace(
|
|
60
|
+
/@import[^;]*;/,
|
|
61
|
+
"",
|
|
62
|
+
);
|
|
63
|
+
document.head.append(style);
|
|
64
|
+
Object.defineProperties(HTMLElement.prototype, {
|
|
65
|
+
offsetLeft: { ...layout.offsetLeft, configurable: true },
|
|
66
|
+
offsetWidth: { ...layout.offsetWidth, configurable: true },
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
afterAll(() => {
|
|
70
|
+
for (const [key, descriptor] of Object.entries(original)) {
|
|
71
|
+
if (descriptor) Object.defineProperty(HTMLElement.prototype, key, descriptor);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
let root: Root | null = null;
|
|
76
|
+
let host: HTMLElement;
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
localStorage.clear();
|
|
80
|
+
vi.useFakeTimers();
|
|
81
|
+
host = document.createElement("div");
|
|
82
|
+
document.body.append(host);
|
|
83
|
+
root = createRoot(host);
|
|
84
|
+
act(() => {
|
|
85
|
+
root?.render(
|
|
86
|
+
<Dock.Root projectId="p1">
|
|
87
|
+
{PANEL_IDS.map((id) => (
|
|
88
|
+
<Dock.Panel key={id} id={id}>
|
|
89
|
+
<div>{id}</div>
|
|
90
|
+
</Dock.Panel>
|
|
91
|
+
))}
|
|
92
|
+
</Dock.Root>,
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
act(() => root?.unmount());
|
|
99
|
+
root = null;
|
|
100
|
+
document.body.innerHTML = "";
|
|
101
|
+
vi.useRealTimers();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const tab = (id: PanelId) => host.querySelector<HTMLElement>(`[data-tab-panel-id="${id}"]`);
|
|
105
|
+
const stripOf = (id: PanelId) => tab(id)?.closest<HTMLElement>(".dv-tabs-container");
|
|
106
|
+
const groupOf = (id: PanelId) => tab(id)?.closest<HTMLElement>(".dv-groupview");
|
|
107
|
+
const shows = (id: PanelId, part: "icon" | "close") => {
|
|
108
|
+
const element = tab(id)?.querySelector(`.hf-dock-tab-${part}`);
|
|
109
|
+
return element ? getComputedStyle(element).display !== "none" : false;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
async function activate(id: PanelId) {
|
|
113
|
+
// dockview settles the active panel on a microtask; `act(async)` waits for it.
|
|
114
|
+
await act(async () => useDockLayoutStore.getState().activatePanel(id));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe("dock tabs", () => {
|
|
118
|
+
it("draw the type icon and close glyph on the shown tab only", () => {
|
|
119
|
+
for (const id of ["design", "compositions"] as const) {
|
|
120
|
+
expect(shows(id, "icon")).toBe(true);
|
|
121
|
+
expect(shows(id, "close")).toBe(true);
|
|
122
|
+
}
|
|
123
|
+
for (const id of ["layers", "renders", "variables", "assets", "code", "catalog"] as const) {
|
|
124
|
+
expect(shows(id, "icon")).toBe(false);
|
|
125
|
+
expect(shows(id, "close")).toBe(false);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("move the icon to the newly shown tab and drop it from the old one", async () => {
|
|
130
|
+
await activate("layers");
|
|
131
|
+
expect(shows("layers", "icon")).toBe(true);
|
|
132
|
+
expect(shows("design", "icon")).toBe(false);
|
|
133
|
+
expect(shows("design", "close")).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("name the close control after the panel, and close only that panel", async () => {
|
|
137
|
+
const close = tab("design")?.querySelector<HTMLElement>(".hf-dock-tab-close");
|
|
138
|
+
expect(close?.getAttribute("aria-label")).toBe("Close Design");
|
|
139
|
+
await act(async () => close?.click());
|
|
140
|
+
expect(useDockLayoutStore.getState().openPanels.has("design")).toBe(false);
|
|
141
|
+
expect(useDockLayoutStore.getState().openPanels.has("layers")).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("dock strip actions", () => {
|
|
146
|
+
const groupsWithActions = () =>
|
|
147
|
+
[...host.querySelectorAll(".hf-dock-strip-actions")].map((actions) =>
|
|
148
|
+
actions.closest(".dv-groupview"),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
it("sit on the active group's strip only, and follow the active group", async () => {
|
|
152
|
+
await activate("design");
|
|
153
|
+
expect(groupsWithActions()).toEqual([groupOf("design")]);
|
|
154
|
+
const labels = [...host.querySelectorAll(".hf-dock-strip-actions button")].map((button) =>
|
|
155
|
+
button.getAttribute("aria-label"),
|
|
156
|
+
);
|
|
157
|
+
expect(labels).toEqual(["Panel menu", "Maximize panel", "Close group"]);
|
|
158
|
+
await activate("assets");
|
|
159
|
+
expect(groupsWithActions()).toEqual([groupOf("assets")]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("dock tab fill", () => {
|
|
164
|
+
it("is one element per strip, under the shown tab, and follows a switch", async () => {
|
|
165
|
+
for (const strip of host.querySelectorAll(".dv-tabs-container")) {
|
|
166
|
+
expect(strip.querySelectorAll(".hf-dock-tab-fill")).toHaveLength(1);
|
|
167
|
+
}
|
|
168
|
+
const fill = () => stripOf("design")?.querySelector<HTMLElement>(".hf-dock-tab-fill");
|
|
169
|
+
expect(fill()?.style.transform).toBe("translateX(0px)");
|
|
170
|
+
expect(fill()?.style.width).toBe(`${TAB_WIDTH}px`);
|
|
171
|
+
await activate("renders");
|
|
172
|
+
expect(stripOf("design")?.querySelectorAll(".hf-dock-tab-fill")).toHaveLength(1);
|
|
173
|
+
expect(fill()?.style.transform).toBe(`translateX(${2 * TAB_STEP}px)`);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("keeps the shown tab whole when its strip narrows, as when the strip's actions appear", async () => {
|
|
177
|
+
const strip = stripOf("design");
|
|
178
|
+
if (!strip) throw new Error("no design strip");
|
|
179
|
+
await activate("variables");
|
|
180
|
+
// The four tabs fit, so the strip is unscrolled; then it narrows to two and a half tabs.
|
|
181
|
+
strip.scrollLeft = 0;
|
|
182
|
+
Object.defineProperty(strip, "clientWidth", { value: 1000, configurable: true });
|
|
183
|
+
act(() => resizeAll());
|
|
184
|
+
Object.defineProperty(strip, "clientWidth", { value: 250, configurable: true });
|
|
185
|
+
act(() => resizeAll());
|
|
186
|
+
expect(strip.scrollLeft).toBe(3 * TAB_STEP + TAB_WIDTH + 1 - 250);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("leaves a strip the user scrolled alone when something else in the dock changes", async () => {
|
|
190
|
+
const strip = stripOf("design");
|
|
191
|
+
if (!strip) throw new Error("no design strip");
|
|
192
|
+
await activate("variables");
|
|
193
|
+
Object.defineProperty(strip, "clientWidth", { value: 250, configurable: true });
|
|
194
|
+
act(() => resizeAll());
|
|
195
|
+
// Scrolled back by hand to look at the first tabs, then another group takes focus.
|
|
196
|
+
strip.scrollLeft = 0;
|
|
197
|
+
await activate("assets");
|
|
198
|
+
act(() => resizeAll());
|
|
199
|
+
expect(strip.scrollLeft).toBe(0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("keeps a hand scroll through another group being maximised and restored", async () => {
|
|
203
|
+
const strip = stripOf("design");
|
|
204
|
+
if (!strip) throw new Error("no design strip");
|
|
205
|
+
await activate("variables");
|
|
206
|
+
Object.defineProperty(strip, "clientWidth", { value: 250, configurable: true });
|
|
207
|
+
act(() => resizeAll());
|
|
208
|
+
strip.scrollLeft = 0;
|
|
209
|
+
// Hidden while another group is maximised; the browser keeps a hidden strip's scroll.
|
|
210
|
+
Object.defineProperty(strip, "clientWidth", { value: 0, configurable: true });
|
|
211
|
+
act(() => resizeAll());
|
|
212
|
+
Object.defineProperty(strip, "clientWidth", { value: 250, configurable: true });
|
|
213
|
+
act(() => resizeAll());
|
|
214
|
+
expect(strip.scrollLeft).toBe(0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("leaves a hand scroll alone when its strip widens", async () => {
|
|
218
|
+
const strip = stripOf("design");
|
|
219
|
+
if (!strip) throw new Error("no design strip");
|
|
220
|
+
await activate("variables");
|
|
221
|
+
Object.defineProperty(strip, "clientWidth", { value: 150, configurable: true });
|
|
222
|
+
act(() => resizeAll());
|
|
223
|
+
strip.scrollLeft = 0;
|
|
224
|
+
// Wider, but the shown tab is still out of view where the user left the strip.
|
|
225
|
+
Object.defineProperty(strip, "clientWidth", { value: 200, configurable: true });
|
|
226
|
+
act(() => resizeAll());
|
|
227
|
+
expect(strip.scrollLeft).toBe(0);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("reveals the shown tab when its strip comes back narrower than it left", async () => {
|
|
231
|
+
const strip = stripOf("design");
|
|
232
|
+
if (!strip) throw new Error("no design strip");
|
|
233
|
+
await activate("variables");
|
|
234
|
+
strip.scrollLeft = 0;
|
|
235
|
+
Object.defineProperty(strip, "clientWidth", { value: 1000, configurable: true });
|
|
236
|
+
act(() => resizeAll());
|
|
237
|
+
Object.defineProperty(strip, "clientWidth", { value: 0, configurable: true });
|
|
238
|
+
act(() => resizeAll());
|
|
239
|
+
Object.defineProperty(strip, "clientWidth", { value: 250, configurable: true });
|
|
240
|
+
act(() => resizeAll());
|
|
241
|
+
expect(strip.scrollLeft).toBe(3 * TAB_STEP + TAB_WIDTH + 1 - 250);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("reveals the shown tab when a panel beside it closes and it moves", async () => {
|
|
245
|
+
const strip = stripOf("design");
|
|
246
|
+
if (!strip) throw new Error("no design strip");
|
|
247
|
+
await activate("variables");
|
|
248
|
+
Object.defineProperty(strip, "clientWidth", { value: 150, configurable: true });
|
|
249
|
+
act(() => resizeAll());
|
|
250
|
+
strip.scrollLeft = 0;
|
|
251
|
+
await act(async () => useDockLayoutStore.getState().togglePanel("design"));
|
|
252
|
+
// Variables moved from the fourth place to the third; its right edge is at 290.
|
|
253
|
+
expect(strip.scrollLeft).toBe(2 * TAB_STEP + TAB_WIDTH + 1 - 150);
|
|
254
|
+
});
|
|
255
|
+
});
|