@adecore/ui 0.49.7 → 0.50.1
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/SlidingColumn.d.ts +2 -1
- package/dist/SlidingColumn.js +90 -87
- package/dist/file-tree/style.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/locales/en.d.ts +3 -0
- package/dist/locales/en.js +4 -1
- package/dist/locales/nl.d.ts +3 -0
- package/dist/locales/nl.js +4 -1
- package/dist/tabs/use-tab-drag.d.ts +52 -0
- package/dist/tabs/use-tab-drag.js +218 -0
- package/dist/theme.css +65 -13
- package/dist/workspace/PaneBar.d.ts +39 -0
- package/dist/workspace/PaneBar.js +399 -0
- package/dist/workspace/Workspace.d.ts +4 -1
- package/dist/workspace/Workspace.js +207 -155
- package/dist/workspace/WorkspaceRail.d.ts +13 -0
- package/dist/workspace/WorkspaceRail.js +125 -0
- package/dist/workspace/WorkspaceTopRow.d.ts +15 -0
- package/dist/workspace/WorkspaceTopRow.js +81 -0
- package/dist/workspace/context.d.ts +4 -0
- package/dist/workspace/context.js +5 -0
- package/dist/workspace/top-row-slot.d.ts +2 -0
- package/dist/workspace/top-row-slot.js +6 -0
- package/package.json +1 -1
- package/src/SlidingColumn.tsx +21 -2
- package/src/file-tree/style.ts +2 -0
- package/src/index.ts +5 -0
- package/src/locales/en.ts +4 -1
- package/src/locales/nl.ts +4 -1
- package/src/tabs/use-tab-drag.ts +256 -0
- package/src/theme.css +65 -13
- package/src/workspace/PaneBar.tsx +276 -0
- package/src/workspace/Workspace.tsx +28 -12
- package/src/workspace/WorkspaceRail.tsx +30 -0
- package/src/workspace/WorkspaceTopRow.tsx +37 -0
- package/src/workspace/context.ts +6 -0
- package/src/workspace/top-row-slot.ts +7 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ComponentProps, type DragEvent, type ReactNode, type RefObject } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { MoreHorizontal } from 'lucide-react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { CloseButton } from '../CloseButton.tsx';
|
|
7
|
+
import * as ContextMenu from '../context-menu/index.parts.ts';
|
|
8
|
+
import { IconButton } from '../IconButton.tsx';
|
|
9
|
+
import * as Popover from '../popover/index.parts.ts';
|
|
10
|
+
import { Separator } from '../Separator.tsx';
|
|
11
|
+
import type { Shortcut } from '../shortcut.ts';
|
|
12
|
+
|
|
13
|
+
/* What the bar holds that is not the bar: a press on one of these is not the start of a drag. */
|
|
14
|
+
const CONTROLS = 'input, textarea, select, button, a, [contenteditable=""], [contenteditable="true"], [role="button"]';
|
|
15
|
+
|
|
16
|
+
/* Without this margin, a bar right at the fold point would fold and unfold on every pixel of a splitter drag. */
|
|
17
|
+
const UNFOLD_MARGIN = 24;
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* Whether the actions are folded into the overflow menu: only once the title has shrunk to its glyph and
|
|
21
|
+
* the actions still do not fit. The width where that happened is remembered, since a folded bar fits by
|
|
22
|
+
* definition and can no longer tell by overflowing when there is room again.
|
|
23
|
+
*/
|
|
24
|
+
function useFolded(bar: RefObject<HTMLElement | null>, actions: RefObject<HTMLElement | null>, enabled: boolean): boolean {
|
|
25
|
+
const [foldedAt, setFoldedAt] = useState<number | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const element = bar.current;
|
|
29
|
+
|
|
30
|
+
if (!enabled || element === null || typeof ResizeObserver === 'undefined') {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const measure = (): void => {
|
|
35
|
+
const width = element.clientWidth;
|
|
36
|
+
|
|
37
|
+
setFoldedAt((current) => {
|
|
38
|
+
if (current !== null) {
|
|
39
|
+
return width > current + UNFOLD_MARGIN ? null : current;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return element.scrollWidth > width + 1 ? width : null;
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const observer = new ResizeObserver(measure);
|
|
47
|
+
|
|
48
|
+
observer.observe(element);
|
|
49
|
+
|
|
50
|
+
// The actions change size on their own as well, as when a terminal's agent adds its mode.
|
|
51
|
+
if (actions.current) {
|
|
52
|
+
observer.observe(actions.current);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
measure();
|
|
56
|
+
|
|
57
|
+
return () => observer.disconnect();
|
|
58
|
+
// Again on every fold: the actions are a new element each time they come back.
|
|
59
|
+
}, [bar, actions, enabled, foldedAt]);
|
|
60
|
+
|
|
61
|
+
// A pane that brings no actions has nothing to fold, whatever the bar measured before it changed.
|
|
62
|
+
return enabled && foldedAt !== null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface PaneBarProps extends Omit<ComponentProps<'header'>, 'title' | 'children' | 'onDragStart' | 'onDragEnd'> {
|
|
66
|
+
/** What the pane is: a glyph and a name, or a `TabStrip` when `tabs` is set. */
|
|
67
|
+
title: ReactNode;
|
|
68
|
+
|
|
69
|
+
/** The title is a strip of tabs, which takes the bar's room and keeps the bar whatever tab is in front. */
|
|
70
|
+
tabs?: boolean;
|
|
71
|
+
|
|
72
|
+
/** The pane's own controls. They fold into a "More actions" popover once the title has given way and they still do not fit. */
|
|
73
|
+
actions?: ReactNode;
|
|
74
|
+
|
|
75
|
+
/** Keeps the actions in the bar of a pane with tabs, rather than in a bar of their own under it. */
|
|
76
|
+
compactActions?: boolean;
|
|
77
|
+
|
|
78
|
+
/** A separator between the title and the actions of a pane without tabs. */
|
|
79
|
+
separated?: boolean;
|
|
80
|
+
|
|
81
|
+
/** Between the title and the actions, such as a mark that lives on the line over the body. */
|
|
82
|
+
accessory?: ReactNode;
|
|
83
|
+
|
|
84
|
+
/** Before the close button, such as a sign that the pane fills the view. */
|
|
85
|
+
trailing?: ReactNode;
|
|
86
|
+
|
|
87
|
+
focused?: boolean;
|
|
88
|
+
|
|
89
|
+
onClose?(): void;
|
|
90
|
+
closeLabel?: string;
|
|
91
|
+
closeShortcut?: Shortcut | string;
|
|
92
|
+
moreActionsLabel?: string;
|
|
93
|
+
|
|
94
|
+
/** The items of the bar's context menu. Leave it out for a bar without one. */
|
|
95
|
+
menu?: ReactNode;
|
|
96
|
+
|
|
97
|
+
/** With `onDragStart`, the bar is a handle that drags the pane; a press on a control in it never starts one. */
|
|
98
|
+
onDragStart?(event: DragEvent<HTMLElement>): void;
|
|
99
|
+
|
|
100
|
+
onDragEnd?(event: DragEvent<HTMLElement>): void;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* An element to draw the bar in instead of over the pane, such as the slot of a `WorkspaceTopRow`. The bar
|
|
104
|
+
* stays the pane's in the tree, so its events still reach the pane's handlers. A separator leads it there.
|
|
105
|
+
*/
|
|
106
|
+
container?: HTMLElement | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The bar over a pane: its title or its tabs, its actions and its close button. Drawn in a `container` it
|
|
111
|
+
* takes that element's height and background, and only its title answers the pointer, so a window can be
|
|
112
|
+
* dragged by the rest.
|
|
113
|
+
*/
|
|
114
|
+
export function PaneBar({
|
|
115
|
+
title,
|
|
116
|
+
tabs = false,
|
|
117
|
+
actions,
|
|
118
|
+
compactActions = false,
|
|
119
|
+
separated = false,
|
|
120
|
+
accessory,
|
|
121
|
+
trailing,
|
|
122
|
+
focused = false,
|
|
123
|
+
onClose,
|
|
124
|
+
closeLabel,
|
|
125
|
+
closeShortcut,
|
|
126
|
+
moreActionsLabel,
|
|
127
|
+
menu,
|
|
128
|
+
onDragStart,
|
|
129
|
+
onDragEnd,
|
|
130
|
+
container = null,
|
|
131
|
+
className,
|
|
132
|
+
...props
|
|
133
|
+
}: PaneBarProps) {
|
|
134
|
+
const { t } = useTranslation('ui');
|
|
135
|
+
/* Whether the pointer came down on the bar itself. A drag starts on the nearest draggable ancestor, so
|
|
136
|
+
`draggable` on a child does not hold it back: the attribute itself has to go while the pointer is in a
|
|
137
|
+
control, or selecting text in an address field would drag the pane away instead. */
|
|
138
|
+
const [grabbable, setGrabbable] = useState(true);
|
|
139
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
140
|
+
const bar = useRef<HTMLElement>(null);
|
|
141
|
+
const actionsBar = useRef<HTMLDivElement>(null);
|
|
142
|
+
const actionsRef = useRef<HTMLSpanElement>(null);
|
|
143
|
+
const hasActions = actions !== undefined && actions !== null && actions !== false;
|
|
144
|
+
const separate = tabs && !compactActions;
|
|
145
|
+
const folded = useFolded(separate ? actionsBar : bar, actionsRef, hasActions);
|
|
146
|
+
const inContainer = container !== null;
|
|
147
|
+
const draggable = onDragStart !== undefined;
|
|
148
|
+
|
|
149
|
+
const actionsPart = hasActions && (
|
|
150
|
+
<>
|
|
151
|
+
{!folded && (
|
|
152
|
+
<>
|
|
153
|
+
{!tabs && separated && <Separator />}
|
|
154
|
+
{/* The controls keep their minimum width so overflow can trigger folding. */}
|
|
155
|
+
<span ref={actionsRef} className={clsx('flex min-w-min items-center', tabs && compactActions ? 'shrink-0' : 'grow')}>
|
|
156
|
+
{actions}
|
|
157
|
+
</span>
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
{folded && (
|
|
161
|
+
<Popover.Root open={menuOpen} onOpenChange={setMenuOpen}>
|
|
162
|
+
<IconButton
|
|
163
|
+
icon={MoreHorizontal}
|
|
164
|
+
size="sm"
|
|
165
|
+
label={moreActionsLabel ?? t('workspace.moreActions')}
|
|
166
|
+
className="cursor-default"
|
|
167
|
+
render={<Popover.Trigger />}
|
|
168
|
+
/>
|
|
169
|
+
{/* A popover keeps the keys of controls such as an address field. */}
|
|
170
|
+
<Popover.Popup sideOffset={6} align="end" className="flex min-w-72 items-center gap-2 p-2 text-xs">
|
|
171
|
+
{actions}
|
|
172
|
+
</Popover.Popup>
|
|
173
|
+
</Popover.Root>
|
|
174
|
+
)}
|
|
175
|
+
{/* Still mounted while folded, so what portals into the actions keeps its place to go. */}
|
|
176
|
+
{folded && !menuOpen && (
|
|
177
|
+
<span hidden className="hidden">
|
|
178
|
+
{actions}
|
|
179
|
+
</span>
|
|
180
|
+
)}
|
|
181
|
+
</>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const header = (
|
|
185
|
+
<header
|
|
186
|
+
{...props}
|
|
187
|
+
ref={bar}
|
|
188
|
+
data-pane-bar=""
|
|
189
|
+
data-placement={inContainer ? 'container' : undefined}
|
|
190
|
+
draggable={draggable && grabbable}
|
|
191
|
+
className={clsx(
|
|
192
|
+
'ade-pane-bar flex items-center gap-2 overflow-hidden text-xs',
|
|
193
|
+
draggable && 'cursor-grab active:cursor-grabbing',
|
|
194
|
+
inContainer
|
|
195
|
+
? 'h-full min-w-0 grow text-text'
|
|
196
|
+
: clsx(
|
|
197
|
+
'h-10 shrink-0 border-b border-border pr-1.5',
|
|
198
|
+
!tabs && 'pl-2',
|
|
199
|
+
focused ? 'bg-surface text-text' : 'bg-surface-idle text-text-muted'
|
|
200
|
+
),
|
|
201
|
+
className
|
|
202
|
+
)}
|
|
203
|
+
onPointerDown={(event) => {
|
|
204
|
+
setGrabbable(!(event.target as HTMLElement | null)?.closest(CONTROLS));
|
|
205
|
+
props.onPointerDown?.(event);
|
|
206
|
+
}}
|
|
207
|
+
onPointerUp={(event) => {
|
|
208
|
+
setGrabbable(true);
|
|
209
|
+
props.onPointerUp?.(event);
|
|
210
|
+
}}
|
|
211
|
+
onDragStart={onDragStart}
|
|
212
|
+
onDragEnd={onDragEnd}
|
|
213
|
+
>
|
|
214
|
+
{/* The title gives way first, down to its glyph. With tabs, the strip takes its place. */}
|
|
215
|
+
<span
|
|
216
|
+
data-pane-title=""
|
|
217
|
+
className={clsx(
|
|
218
|
+
'ade-pane-title flex items-center gap-2',
|
|
219
|
+
tabs ? 'min-w-24 self-stretch' : 'min-w-5',
|
|
220
|
+
/* In a container the first text stands as far after the separator as the text before it
|
|
221
|
+
stands before it: 16 pixels, of which the gap brings 8 and a tab 12 of its own. */
|
|
222
|
+
inContainer ? (tabs ? '-ml-1' : 'pl-2') : !tabs && 'pl-1',
|
|
223
|
+
// A weight rather than `shrink`, since controls whose labels truncate shrink as well.
|
|
224
|
+
tabs || ((folded || !hasActions) && !inContainer) ? 'grow' : 'shrink-100'
|
|
225
|
+
)}
|
|
226
|
+
>
|
|
227
|
+
{title}
|
|
228
|
+
</span>
|
|
229
|
+
{accessory}
|
|
230
|
+
{!separate && actionsPart}
|
|
231
|
+
{/* The room the title leaves in a container, which the window drags by. */}
|
|
232
|
+
{inContainer && !tabs && (folded || !hasActions) && <span aria-hidden="true" className="grow self-stretch" />}
|
|
233
|
+
{trailing}
|
|
234
|
+
{onClose && (
|
|
235
|
+
<CloseButton label={closeLabel ?? t('workspace.closePane')} kbd={closeShortcut} size="sm" className="cursor-default" onClick={onClose} />
|
|
236
|
+
)}
|
|
237
|
+
</header>
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const placed =
|
|
241
|
+
menu === undefined ? (
|
|
242
|
+
header
|
|
243
|
+
) : (
|
|
244
|
+
<ContextMenu.Root>
|
|
245
|
+
<ContextMenu.Trigger render={header} />
|
|
246
|
+
<ContextMenu.Popup>{menu}</ContextMenu.Popup>
|
|
247
|
+
</ContextMenu.Root>
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<>
|
|
252
|
+
{inContainer
|
|
253
|
+
? createPortal(
|
|
254
|
+
<>
|
|
255
|
+
<Separator />
|
|
256
|
+
{placed}
|
|
257
|
+
</>,
|
|
258
|
+
container
|
|
259
|
+
)
|
|
260
|
+
: placed}
|
|
261
|
+
{separate && hasActions && (
|
|
262
|
+
<div
|
|
263
|
+
ref={actionsBar}
|
|
264
|
+
data-pane-actions=""
|
|
265
|
+
className={clsx(
|
|
266
|
+
'flex h-10 shrink-0 items-center gap-2 overflow-hidden border-b border-border px-2 text-xs',
|
|
267
|
+
folded && 'justify-end',
|
|
268
|
+
focused ? 'bg-surface text-text' : 'bg-surface-idle text-text-muted'
|
|
269
|
+
)}
|
|
270
|
+
>
|
|
271
|
+
{actionsPart}
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
</>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
1
|
+
import { useState, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useRender } from '@base-ui/react/use-render';
|
|
4
|
-
import { WorkspaceContext, type WorkspaceLayout } from './context.ts';
|
|
4
|
+
import { TopRowSlotContext, WorkspaceContext, type WorkspaceLayout } from './context.ts';
|
|
5
5
|
|
|
6
6
|
export interface WorkspaceProps extends useRender.ComponentProps<'div'> {
|
|
7
7
|
layout?: WorkspaceLayout;
|
|
@@ -9,6 +9,9 @@ export interface WorkspaceProps extends useRender.ComponentProps<'div'> {
|
|
|
9
9
|
sidebar?: ReactNode;
|
|
10
10
|
sidePanel?: ReactNode;
|
|
11
11
|
|
|
12
|
+
/** A column at the window's right edge, after the side panel, usually a `WorkspaceRail`. */
|
|
13
|
+
rail?: ReactNode;
|
|
14
|
+
|
|
12
15
|
/** Auto-sized regions keep their own width, separators and opening animation. */
|
|
13
16
|
sidebarWidth?: number | 'auto';
|
|
14
17
|
|
|
@@ -19,11 +22,13 @@ export interface WorkspaceProps extends useRender.ComponentProps<'div'> {
|
|
|
19
22
|
|
|
20
23
|
const isPresent = (node: ReactNode): boolean => node !== undefined && node !== null && node !== false;
|
|
21
24
|
|
|
25
|
+
/** The sidebar, the toolbar and the content on one raised sheet, with the side panel and the rail behind it. */
|
|
22
26
|
export function Workspace({
|
|
23
27
|
layout = 'standard',
|
|
24
28
|
toolbar,
|
|
25
29
|
sidebar,
|
|
26
30
|
sidePanel,
|
|
31
|
+
rail,
|
|
27
32
|
sidebarWidth = 248,
|
|
28
33
|
sidePanelWidth = 320,
|
|
29
34
|
sidebarOpen,
|
|
@@ -37,9 +42,11 @@ export function Workspace({
|
|
|
37
42
|
}: WorkspaceProps) {
|
|
38
43
|
const hasSidebar = isPresent(sidebar);
|
|
39
44
|
const hasPanel = isPresent(sidePanel);
|
|
45
|
+
const hasRail = isPresent(rail);
|
|
40
46
|
const leftOpen = hasSidebar && (sidebarOpen ?? true);
|
|
41
47
|
const rightOpen = hasPanel && (sidePanelOpen ?? true);
|
|
42
48
|
const hasToolbar = isPresent(toolbar);
|
|
49
|
+
const [topRowSlot, setTopRowSlot] = useState<HTMLElement | null>(null);
|
|
43
50
|
|
|
44
51
|
const element = useRender({
|
|
45
52
|
render,
|
|
@@ -53,28 +60,37 @@ export function Workspace({
|
|
|
53
60
|
'data-toolbar': hasToolbar || undefined,
|
|
54
61
|
'data-sidebar': leftOpen || undefined,
|
|
55
62
|
'data-side-panel': rightOpen || undefined,
|
|
63
|
+
'data-rail': hasRail || undefined,
|
|
56
64
|
children: (
|
|
57
65
|
<>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
<div className="ade-workspace-sheet">
|
|
67
|
+
{hasSidebar && (
|
|
68
|
+
<aside className="ade-workspace-sidebar" data-auto={sidebarWidth === 'auto' || undefined} style={{ width: sidebarWidth }}>
|
|
69
|
+
{sidebar}
|
|
70
|
+
</aside>
|
|
71
|
+
)}
|
|
72
|
+
<div className="ade-workspace-center">
|
|
73
|
+
{hasToolbar && <div className="ade-workspace-toolbar">{toolbar}</div>}
|
|
74
|
+
<div className="ade-workspace-content">{children}</div>
|
|
75
|
+
</div>
|
|
66
76
|
</div>
|
|
67
77
|
{hasPanel && (
|
|
68
78
|
<aside className="ade-workspace-side-panel" data-auto={sidePanelWidth === 'auto' || undefined} style={{ width: sidePanelWidth }}>
|
|
69
79
|
{sidePanel}
|
|
70
80
|
</aside>
|
|
71
81
|
)}
|
|
82
|
+
{hasRail && rail}
|
|
72
83
|
</>
|
|
73
84
|
)
|
|
74
85
|
}
|
|
75
86
|
});
|
|
76
87
|
|
|
77
|
-
|
|
88
|
+
// The content ends at the sheet's own edge, whatever stands beside it.
|
|
89
|
+
const context = { layout, edges: { top: !hasToolbar, right: true, bottom: true, left: !leftOpen } };
|
|
78
90
|
|
|
79
|
-
return
|
|
91
|
+
return (
|
|
92
|
+
<WorkspaceContext value={context}>
|
|
93
|
+
<TopRowSlotContext value={{ slot: topRowSlot, register: setTopRowSlot }}>{element}</TopRowSlotContext>
|
|
94
|
+
</WorkspaceContext>
|
|
95
|
+
);
|
|
80
96
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
4
|
+
|
|
5
|
+
export type WorkspaceRailProps = useRender.ComponentProps<'nav'>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The column of buttons at the right edge of a `Workspace`, 48 pixels wide. Its first separator stands on
|
|
9
|
+
* the top row's bottom line when one 32 pixel button comes before it. It makes no stacking context, so a
|
|
10
|
+
* hover never covers the sheet's edge; a tooltip still rises above the sheet.
|
|
11
|
+
*/
|
|
12
|
+
export function WorkspaceRail({ className, render, ref, ...props }: WorkspaceRailProps) {
|
|
13
|
+
const { t } = useTranslation('ui');
|
|
14
|
+
|
|
15
|
+
return useRender({
|
|
16
|
+
render,
|
|
17
|
+
ref,
|
|
18
|
+
defaultTagName: 'nav',
|
|
19
|
+
props: { 'aria-label': t('workspace.tools'), ...props, className: clsx('ade-workspace-rail', className) }
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function WorkspaceRailSeparator({ className }: { className?: string }) {
|
|
24
|
+
return <span aria-hidden className={clsx('ade-workspace-rail-separator', className)} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Pushes what follows to the bottom of the rail. */
|
|
28
|
+
export function WorkspaceRailSpacer() {
|
|
29
|
+
return <span aria-hidden className="ade-workspace-rail-spacer" />;
|
|
30
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useContext, type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { TopRowSlotContext } from './context.ts';
|
|
4
|
+
|
|
5
|
+
export interface WorkspaceTopRowProps extends Omit<ComponentProps<'header'>, 'children'> {
|
|
6
|
+
/** Before the project: room for a window's own controls, and a button that brings the sidebar back. */
|
|
7
|
+
leading?: ReactNode;
|
|
8
|
+
|
|
9
|
+
/** The project: a switcher, or a strip of project tabs. */
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
|
|
12
|
+
/** Leaves out the slot a lone pane's bar goes into, as beside project tabs, which take the row. */
|
|
13
|
+
slotless?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The 47 pixel band at the top of a `Workspace`'s sheet, as its `toolbar`: the project, and beside it the
|
|
18
|
+
* slot where the bar of a lone pane goes (`useTopRowSlot`, `PaneBar`'s `container`). The window drags by
|
|
19
|
+
* every part of it that is not a control.
|
|
20
|
+
*/
|
|
21
|
+
export function WorkspaceTopRow({ leading, children, slotless = false, className, ...props }: WorkspaceTopRowProps) {
|
|
22
|
+
const { register } = useContext(TopRowSlotContext);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<header
|
|
26
|
+
{...props}
|
|
27
|
+
className={clsx(
|
|
28
|
+
'ade-workspace-top-row relative flex h-[47px] shrink-0 items-center gap-2 bg-surface pr-1.5 pl-2 text-xs text-text-muted',
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
{leading}
|
|
33
|
+
{children}
|
|
34
|
+
{!slotless && <div ref={register} data-top-row-slot="" className="flex h-full min-w-0 grow items-center gap-2" />}
|
|
35
|
+
</header>
|
|
36
|
+
);
|
|
37
|
+
}
|
package/src/workspace/context.ts
CHANGED
|
@@ -7,3 +7,9 @@ export const WorkspaceContext = createContext<{ layout: WorkspaceLayout; edges:
|
|
|
7
7
|
layout: 'standard',
|
|
8
8
|
edges: { top: true, right: true, bottom: true, left: true }
|
|
9
9
|
});
|
|
10
|
+
|
|
11
|
+
/* The element of the top row a lone pane's bar goes into, which the row registers and every pane of the workspace reads. */
|
|
12
|
+
export const TopRowSlotContext = createContext<{ slot: HTMLElement | null; register(element: HTMLElement | null): void }>({
|
|
13
|
+
slot: null,
|
|
14
|
+
register: () => undefined
|
|
15
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { TopRowSlotContext } from './context.ts';
|
|
3
|
+
|
|
4
|
+
/** The slot of the `Workspace`'s top row, for `PaneBar`'s `container`, or null while there is none. */
|
|
5
|
+
export function useTopRowSlot(): HTMLElement | null {
|
|
6
|
+
return useContext(TopRowSlotContext).slot;
|
|
7
|
+
}
|