@hyperframes/studio 0.8.66 → 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-BRlwG-p9.js → hyperframes-player-BF2Y9Rtn.js} +1 -1
- package/dist/assets/index-BBOktYKC.css +1 -0
- package/dist/assets/{index-DJl2JKQj.js → index-BXCEB5uw.js} +1 -1
- package/dist/assets/{index-BzGcmMBS.js → index-CGQ66fle.js} +200 -200
- package/dist/assets/{index-D6Jy-2Zs.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 +4436 -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/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
|
@@ -1,28 +1,197 @@
|
|
|
1
1
|
@import "dockview-react/dist/styles/dockview.css";
|
|
2
2
|
|
|
3
3
|
.hf-dock {
|
|
4
|
-
--dv-group-view-background-color: var(--
|
|
5
|
-
--dv-tabs-and-actions-container-background-color: var(--
|
|
6
|
-
--dv-
|
|
7
|
-
|
|
8
|
-
--dv-
|
|
9
|
-
--dv-
|
|
10
|
-
--dv-
|
|
11
|
-
--dv-
|
|
12
|
-
--dv-
|
|
13
|
-
--dv-
|
|
14
|
-
--dv-tab-
|
|
15
|
-
--dv-
|
|
16
|
-
--dv-
|
|
17
|
-
--dv-
|
|
4
|
+
--dv-group-view-background-color: var(--panel-card-bg);
|
|
5
|
+
--dv-tabs-and-actions-container-background-color: var(--panel-strip-bg);
|
|
6
|
+
--dv-tabs-and-actions-container-height: 36px;
|
|
7
|
+
/* The fill paints the shown tab, so every tab state is transparent. */
|
|
8
|
+
--dv-activegroup-visiblepanel-tab-background-color: transparent;
|
|
9
|
+
--dv-activegroup-hiddenpanel-tab-background-color: transparent;
|
|
10
|
+
--dv-inactivegroup-visiblepanel-tab-background-color: transparent;
|
|
11
|
+
--dv-inactivegroup-hiddenpanel-tab-background-color: transparent;
|
|
12
|
+
--dv-activegroup-visiblepanel-tab-color: var(--color-text-1);
|
|
13
|
+
--dv-activegroup-hiddenpanel-tab-color: var(--color-text-2);
|
|
14
|
+
--dv-inactivegroup-visiblepanel-tab-color: var(--color-text-1);
|
|
15
|
+
--dv-inactivegroup-hiddenpanel-tab-color: var(--color-text-2);
|
|
16
|
+
--dv-tab-divider-color: transparent;
|
|
17
|
+
--dv-tab-margin: 0;
|
|
18
|
+
--dv-separator-border: transparent;
|
|
19
|
+
--dv-paneview-header-border-color: transparent;
|
|
20
|
+
--dv-sash-color: transparent;
|
|
18
21
|
--dv-active-sash-color: var(--color-panel-accent);
|
|
19
22
|
--dv-drag-over-background-color: color-mix(in srgb, var(--color-panel-accent) 12%, transparent);
|
|
20
23
|
--dv-drag-over-border-color: var(--color-panel-accent);
|
|
21
|
-
--dv-tab-font-size:
|
|
22
|
-
--dv-tabs-and-actions-container-font-size:
|
|
24
|
+
--dv-tab-font-size: 12.5px;
|
|
25
|
+
--dv-tabs-and-actions-container-font-size: 12.5px;
|
|
26
|
+
background-color: var(--panel-shell-bg);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
.hf-dock .dv-sash:focus-visible {
|
|
26
30
|
background-color: var(--dv-active-sash-color);
|
|
27
31
|
outline: none;
|
|
28
32
|
}
|
|
33
|
+
|
|
34
|
+
.hf-dock .dv-dockview {
|
|
35
|
+
background-color: var(--panel-shell-bg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Cards on the shell: 3px around each group's view makes a 6px gutter. The card owns the only border. */
|
|
39
|
+
.hf-dock .dv-view:has(> .dv-groupview) {
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
padding: 3px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.hf-dock .dv-groupview {
|
|
45
|
+
border: 1px solid var(--panel-card-border);
|
|
46
|
+
border-radius: 10px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.hf-dock .dv-tabs-and-actions-container {
|
|
50
|
+
align-items: flex-end;
|
|
51
|
+
padding: 6px 0 0 4px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.hf-dock .dv-scrollable {
|
|
55
|
+
height: 30px;
|
|
56
|
+
min-width: 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* dockview's own scrollbar lane; the strip scrolls by wheel and keeps the shown tab in view. */
|
|
60
|
+
.hf-dock .dv-scrollbar {
|
|
61
|
+
display: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.hf-dock .dv-tabs-container {
|
|
65
|
+
--hf-fade-start: 0px;
|
|
66
|
+
--hf-fade-end: 0px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* A mask adds a compositor render pass; only a strip that is actually clipped needs one. */
|
|
70
|
+
.hf-dock .dv-tabs-container[data-clipped] {
|
|
71
|
+
mask-image: linear-gradient(
|
|
72
|
+
to right,
|
|
73
|
+
transparent,
|
|
74
|
+
black var(--hf-fade-start),
|
|
75
|
+
black calc(100% - var(--hf-fade-end)),
|
|
76
|
+
transparent
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.hf-dock .dv-tabs-container[data-clipped~="start"] {
|
|
81
|
+
--hf-fade-start: 12px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.hf-dock .dv-tabs-container[data-clipped~="end"] {
|
|
85
|
+
--hf-fade-end: 12px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.hf-dock .dv-tab {
|
|
89
|
+
z-index: 1;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
height: 30px;
|
|
93
|
+
padding: 0 6px;
|
|
94
|
+
border-radius: 6px 6px 0 0;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* dockview's own state rule paints tabs from these variables, so hover sets them. */
|
|
99
|
+
.hf-dock .dv-tab.dv-inactive-tab:hover {
|
|
100
|
+
--dv-activegroup-hiddenpanel-tab-background-color: var(--panel-tab-bg-hover);
|
|
101
|
+
--dv-inactivegroup-hiddenpanel-tab-background-color: var(--panel-tab-bg-hover);
|
|
102
|
+
--dv-activegroup-hiddenpanel-tab-color: var(--color-text-1);
|
|
103
|
+
--dv-inactivegroup-hiddenpanel-tab-color: var(--color-text-1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.hf-dock .dv-tab:focus-visible {
|
|
107
|
+
outline: 1.5px solid var(--color-accent);
|
|
108
|
+
outline-offset: -1.5px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.hf-dock .dv-tab:focus-visible::after,
|
|
112
|
+
.hf-dock .dv-tab:has(:focus-visible)::after {
|
|
113
|
+
outline: none !important;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.hf-dock-tab-fill {
|
|
117
|
+
position: absolute;
|
|
118
|
+
bottom: 0;
|
|
119
|
+
left: 0;
|
|
120
|
+
height: 30px;
|
|
121
|
+
border-radius: 6px 6px 0 0;
|
|
122
|
+
background-color: var(--panel-card-bg);
|
|
123
|
+
pointer-events: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.hf-dock-tab-fill[data-ready] {
|
|
127
|
+
transition:
|
|
128
|
+
transform 140ms ease-out,
|
|
129
|
+
width 140ms ease-out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Fills dockview's tab part, so the label and icon centre in the tab's full height. */
|
|
133
|
+
.hf-dock-tab {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 4px;
|
|
137
|
+
height: 100%;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.hf-dock .dv-inactive-tab .hf-dock-tab-icon,
|
|
141
|
+
.hf-dock .dv-inactive-tab .hf-dock-tab-close {
|
|
142
|
+
display: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.hf-dock-tab-icon {
|
|
146
|
+
flex: none;
|
|
147
|
+
animation: hf-dock-tab-icon-in 80ms ease-out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@keyframes hf-dock-tab-icon-in {
|
|
151
|
+
from {
|
|
152
|
+
opacity: 0;
|
|
153
|
+
transform: translateX(-2px);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Takes its space whenever the tab is shown, so hover never moves the label. */
|
|
158
|
+
.hf-dock-tab-close {
|
|
159
|
+
display: flex;
|
|
160
|
+
opacity: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.dv-tab:hover .hf-dock-tab-close,
|
|
164
|
+
.dv-tab:focus-visible .hf-dock-tab-close {
|
|
165
|
+
opacity: 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Sized by the actions it draws, so a strip without them gives its tabs the whole width. */
|
|
169
|
+
.hf-dock .dv-right-actions-container {
|
|
170
|
+
flex: none;
|
|
171
|
+
align-self: flex-start;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.hf-dock-strip-actions {
|
|
175
|
+
display: flex;
|
|
176
|
+
margin-right: 4px;
|
|
177
|
+
opacity: 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.dv-groupview:hover .hf-dock-strip-actions {
|
|
181
|
+
opacity: 0.5;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.hf-dock-strip-actions:hover,
|
|
185
|
+
.hf-dock-strip-actions:focus-within {
|
|
186
|
+
opacity: 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@media (prefers-reduced-motion: reduce) {
|
|
190
|
+
.hf-dock-tab-fill[data-ready] {
|
|
191
|
+
transition: none;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.hf-dock-tab-icon {
|
|
195
|
+
animation: none;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { DockviewApi } from "dockview-react";
|
|
2
|
+
|
|
3
|
+
const TAB_FILL_CLASS = "hf-dock-tab-fill";
|
|
4
|
+
|
|
5
|
+
/** The strip's one fill, created on first use. Dockview inserts tabs before tabs, so it stays first. */
|
|
6
|
+
function fillOf(list: HTMLElement): HTMLElement {
|
|
7
|
+
const existing = list.querySelector<HTMLElement>(`:scope > .${TAB_FILL_CLASS}`);
|
|
8
|
+
if (existing) return existing;
|
|
9
|
+
const fill = document.createElement("span");
|
|
10
|
+
fill.className = TAB_FILL_CLASS;
|
|
11
|
+
fill.setAttribute("aria-hidden", "true");
|
|
12
|
+
list.prepend(fill);
|
|
13
|
+
// Placed once without motion; only later moves slide.
|
|
14
|
+
requestAnimationFrame(() => fill.setAttribute("data-ready", ""));
|
|
15
|
+
return fill;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A strip's width and where its shown tab sits, as of the last pass. */
|
|
19
|
+
type StripShape = { width: number; tab: HTMLElement | null; left: number; size: number };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* True when the strip narrowed or its shown tab moved or resized since it was last visible. dockview
|
|
23
|
+
* reveals a tab only when it is activated, and the user's own scrolling changes none of these.
|
|
24
|
+
*/
|
|
25
|
+
function reshaped(before: StripShape | undefined, now: StripShape) {
|
|
26
|
+
if (!before || now.width === 0) return false;
|
|
27
|
+
if (now.width < before.width) return true;
|
|
28
|
+
return now.tab !== before.tab || now.left !== before.left || now.size !== before.size;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The scroll that shows the whole shown tab, e.g. after the strip narrows because its actions appeared. */
|
|
32
|
+
function revealedScroll(shape: StripShape, scrollLeft: number) {
|
|
33
|
+
if (shape.left < scrollLeft) return shape.left;
|
|
34
|
+
// Offset sizes are rounded while the tab's edge is fractional; a pixel over is clamped at the end.
|
|
35
|
+
return Math.max(scrollLeft, shape.left + shape.size + 1 - shape.width);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reads where the shown tab sits; the write happens after every strip is read, so layout runs once. */
|
|
39
|
+
function measureFill(list: HTMLElement, shapes: WeakMap<HTMLElement, StripShape>): () => void {
|
|
40
|
+
const fill = fillOf(list);
|
|
41
|
+
const tab = list.querySelector<HTMLElement>(":scope > .dv-active-tab");
|
|
42
|
+
const shape = {
|
|
43
|
+
width: list.clientWidth,
|
|
44
|
+
tab,
|
|
45
|
+
left: tab?.offsetLeft ?? 0,
|
|
46
|
+
size: tab?.offsetWidth ?? 0,
|
|
47
|
+
};
|
|
48
|
+
const reveal = tab !== null && reshaped(shapes.get(list), shape);
|
|
49
|
+
// A hidden strip (another group maximised) keeps its scroll, so it is compared with its last visible shape.
|
|
50
|
+
if (shape.width > 0) shapes.set(list, shape);
|
|
51
|
+
const scrollLeft = list.scrollLeft;
|
|
52
|
+
const target = reveal ? revealedScroll(shape, scrollLeft) : scrollLeft;
|
|
53
|
+
return () => {
|
|
54
|
+
fill.hidden = !tab;
|
|
55
|
+
if (!tab) return;
|
|
56
|
+
fill.style.width = `${shape.size}px`;
|
|
57
|
+
fill.style.transform = `translateX(${shape.left}px)`;
|
|
58
|
+
if (target !== scrollLeft) list.scrollLeft = target;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Marks which edges of a scrolled strip are clipped, for the CSS edge fade. */
|
|
63
|
+
function markClippedEdges(list: HTMLElement) {
|
|
64
|
+
const edges = [];
|
|
65
|
+
if (list.scrollLeft > 0) edges.push("start");
|
|
66
|
+
if (list.scrollLeft + list.clientWidth < list.scrollWidth - 1) edges.push("end");
|
|
67
|
+
const value = edges.join(" ");
|
|
68
|
+
if (value) list.setAttribute("data-clipped", value);
|
|
69
|
+
else list.removeAttribute("data-clipped");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Slides one fill per tab strip under its shown tab, keeps that tab in view, and keeps the
|
|
74
|
+
* clipped-edge marks current.
|
|
75
|
+
* Returns a disposer.
|
|
76
|
+
*/
|
|
77
|
+
export function installTabFill(api: DockviewApi, root: HTMLElement): () => void {
|
|
78
|
+
const lists = () => root.querySelectorAll<HTMLElement>(".dv-tabs-container");
|
|
79
|
+
// Tab widths also change with no dockview event: a renamed title, a late web font.
|
|
80
|
+
const resizeObserver = new ResizeObserver(() => placeAll());
|
|
81
|
+
const observed = new Set<Element>();
|
|
82
|
+
function observe(elements: Iterable<Element>) {
|
|
83
|
+
for (const element of observed) {
|
|
84
|
+
if (element.isConnected) continue;
|
|
85
|
+
resizeObserver.unobserve(element);
|
|
86
|
+
observed.delete(element);
|
|
87
|
+
}
|
|
88
|
+
for (const element of elements) {
|
|
89
|
+
if (observed.has(element)) continue;
|
|
90
|
+
resizeObserver.observe(element);
|
|
91
|
+
observed.add(element);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const shapes = new WeakMap<HTMLElement, StripShape>();
|
|
95
|
+
function placeAll() {
|
|
96
|
+
const strips = [...lists()];
|
|
97
|
+
const writes = strips.map((list) => measureFill(list, shapes));
|
|
98
|
+
for (const write of writes) write();
|
|
99
|
+
for (const list of strips) markClippedEdges(list);
|
|
100
|
+
observe(strips.flatMap((list) => [list, ...list.querySelectorAll(":scope > .dv-tab")]));
|
|
101
|
+
}
|
|
102
|
+
const onScroll = (event: Event) => {
|
|
103
|
+
const list = event.target;
|
|
104
|
+
if (list instanceof HTMLElement && list.classList.contains("dv-tabs-container")) {
|
|
105
|
+
markClippedEdges(list);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const subscriptions = [
|
|
109
|
+
api.onDidActivePanelChange(placeAll),
|
|
110
|
+
api.onDidAddPanel(placeAll),
|
|
111
|
+
api.onDidRemovePanel(placeAll),
|
|
112
|
+
api.onDidMovePanel(placeAll),
|
|
113
|
+
api.onDidLayoutChange(placeAll),
|
|
114
|
+
api.onDidLayoutFromJSON(placeAll),
|
|
115
|
+
];
|
|
116
|
+
// Scroll does not bubble; a capturing listener still sees every strip's.
|
|
117
|
+
root.addEventListener("scroll", onScroll, true);
|
|
118
|
+
placeAll();
|
|
119
|
+
return () => {
|
|
120
|
+
root.removeEventListener("scroll", onScroll, true);
|
|
121
|
+
for (const subscription of subscriptions) subscription.dispose();
|
|
122
|
+
resizeObserver.disconnect();
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
const dockCss = readFileSync(path.join(import.meta.dirname, "dock.css"), "utf8").replace(
|
|
6
|
+
/@import[^;]*;/,
|
|
7
|
+
"",
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
/** Each top-level rule's selector and body, with the reduced-motion block kept whole. */
|
|
11
|
+
function rules(css: string): { selector: string; body: string }[] {
|
|
12
|
+
const found: { selector: string; body: string }[] = [];
|
|
13
|
+
let depth = 0;
|
|
14
|
+
let start = 0;
|
|
15
|
+
let selector = "";
|
|
16
|
+
for (let index = 0; index < css.length; index++) {
|
|
17
|
+
if (css[index] === "{") {
|
|
18
|
+
if (depth === 0) {
|
|
19
|
+
selector = css
|
|
20
|
+
.slice(start, index)
|
|
21
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
22
|
+
.trim();
|
|
23
|
+
start = index + 1;
|
|
24
|
+
}
|
|
25
|
+
depth++;
|
|
26
|
+
} else if (css[index] === "}") {
|
|
27
|
+
depth--;
|
|
28
|
+
if (depth === 0) {
|
|
29
|
+
found.push({ selector, body: css.slice(start, index) });
|
|
30
|
+
start = index + 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const bodyOf = (selector: string) =>
|
|
38
|
+
rules(dockCss).find((rule) => rule.selector === selector)?.body;
|
|
39
|
+
|
|
40
|
+
describe("dock panel strip contract", () => {
|
|
41
|
+
it("draws one border per surface: the card's, and no divider, separator or tab outline", () => {
|
|
42
|
+
const bordered = rules(dockCss)
|
|
43
|
+
.filter((rule) => /(^|[\s;{])border(-(top|right|bottom|left))?\s*:/.test(rule.body))
|
|
44
|
+
.map((rule) => rule.selector);
|
|
45
|
+
expect(bordered).toEqual([".hf-dock .dv-groupview"]);
|
|
46
|
+
const dock = bodyOf(".hf-dock");
|
|
47
|
+
expect(dock).toContain("--dv-tab-divider-color: transparent");
|
|
48
|
+
expect(dock).toContain("--dv-separator-border: transparent");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("slides only transform and width, 140ms ease-out, and reveals the icon in 80ms", () => {
|
|
52
|
+
expect(bodyOf(".hf-dock-tab-fill[data-ready]")?.replace(/\s+/g, " ").trim()).toBe(
|
|
53
|
+
"transition: transform 140ms ease-out, width 140ms ease-out;",
|
|
54
|
+
);
|
|
55
|
+
expect(bodyOf(".hf-dock-tab-icon")).toContain("animation: hf-dock-tab-icon-in 80ms ease-out");
|
|
56
|
+
const animated = rules(dockCss).filter(
|
|
57
|
+
(rule) => !rule.selector.startsWith("@") && /transition|animation/.test(rule.body),
|
|
58
|
+
);
|
|
59
|
+
expect(animated.map((rule) => rule.selector)).toEqual([
|
|
60
|
+
".hf-dock-tab-fill[data-ready]",
|
|
61
|
+
".hf-dock-tab-icon",
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("turns the slide and the icon reveal off under reduced motion", () => {
|
|
66
|
+
const reduced = bodyOf("@media (prefers-reduced-motion: reduce)")?.replace(/\s+/g, " ");
|
|
67
|
+
expect(reduced).toContain(".hf-dock-tab-fill[data-ready] { transition: none; }");
|
|
68
|
+
expect(reduced).toContain(".hf-dock-tab-icon { animation: none; }");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("centres the tab's icon and label in the tab's full height", () => {
|
|
72
|
+
const tab = bodyOf(".hf-dock-tab");
|
|
73
|
+
expect(tab).toContain("height: 100%");
|
|
74
|
+
expect(tab).toContain("align-items: center");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("takes every colour from a token", () => {
|
|
78
|
+
expect(dockCss).not.toMatch(/#[0-9a-f]{3,8}\b|rgba?\(|hsla?\(|oklch\(/i);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -115,10 +115,10 @@ export function PreviewPane({
|
|
|
115
115
|
return (
|
|
116
116
|
<div
|
|
117
117
|
ref={containerRef}
|
|
118
|
-
//
|
|
119
|
-
// fills the screen edge-to-edge.
|
|
118
|
+
// A recessed well inset in its dock card; dropped in fullscreen so the
|
|
119
|
+
// preview fills the screen edge-to-edge.
|
|
120
120
|
className={`flex-1 min-h-0 flex flex-col overflow-hidden bg-[var(--studio-preview-bg,var(--color-neutral-950))] ${
|
|
121
|
-
isFullscreen ? "" : "rounded-
|
|
121
|
+
isFullscreen ? "" : "m-3 rounded-md border border-neutral-800/50"
|
|
122
122
|
}`}
|
|
123
123
|
data-studio-fullscreen-target=""
|
|
124
124
|
>
|
|
@@ -85,7 +85,7 @@ export function TimelinePane({
|
|
|
85
85
|
aria-disabled={timelineDisabled || undefined}
|
|
86
86
|
>
|
|
87
87
|
<div
|
|
88
|
-
className="flex flex-col flex-1 min-h-0 overflow-hidden
|
|
88
|
+
className="flex flex-col flex-1 min-h-0 overflow-hidden"
|
|
89
89
|
onDoubleClick={(e) => {
|
|
90
90
|
if ((e.target as HTMLElement).closest("[data-clip]")) return;
|
|
91
91
|
if (timelineDisabled) return;
|
|
@@ -220,6 +220,22 @@ export function MenuRadioItem({ className, children, ...props }: MenuRadioItemPr
|
|
|
220
220
|
);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/** An on/off row; the tick renders only while `checked`. */
|
|
224
|
+
export function MenuCheckboxItem({
|
|
225
|
+
className,
|
|
226
|
+
children,
|
|
227
|
+
...props
|
|
228
|
+
}: StyledProps<typeof BaseMenu.CheckboxItem>) {
|
|
229
|
+
return (
|
|
230
|
+
<BaseMenu.CheckboxItem className={cn(itemBase, className)} {...props}>
|
|
231
|
+
<span className="truncate">{children}</span>
|
|
232
|
+
<span className="flex size-3 shrink-0 items-center justify-center text-accent">
|
|
233
|
+
<BaseMenu.CheckboxItemIndicator>✓</BaseMenu.CheckboxItemIndicator>
|
|
234
|
+
</span>
|
|
235
|
+
</BaseMenu.CheckboxItem>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
223
239
|
/** A hairline between two groups of items. */
|
|
224
240
|
export function MenuSeparator({ className, ...props }: StyledProps<typeof BaseMenu.Separator>) {
|
|
225
241
|
return <BaseMenu.Separator className={cn("my-1 h-px bg-hairline", className)} {...props} />;
|
package/src/styles/theme.css
CHANGED
|
@@ -273,6 +273,14 @@
|
|
|
273
273
|
--color-panel-media: var(--color-media);
|
|
274
274
|
--color-panel-container: var(--color-container);
|
|
275
275
|
|
|
276
|
+
/* Dock panels are cards on the shell. The card owns the only border; its tab
|
|
277
|
+
* strip sits below the card body and the shown tab is a fill in the body colour. */
|
|
278
|
+
--panel-shell-bg: var(--color-bg-1);
|
|
279
|
+
--panel-card-bg: var(--color-surface);
|
|
280
|
+
--panel-card-border: #303034;
|
|
281
|
+
--panel-strip-bg: var(--color-bg-0);
|
|
282
|
+
--panel-tab-bg-hover: #202023;
|
|
283
|
+
|
|
276
284
|
/* Names Studio's markup invented that no config ever defined, so the classes
|
|
277
285
|
* compiled to nothing. Aliased to the token that was meant, rather than
|
|
278
286
|
* given values of their own, so there is still one source per decision.
|