@adecore/ui 0.49.7 → 0.50.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.
@@ -0,0 +1,81 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { useContext } from 'react';
3
+ import clsx from 'clsx';
4
+ import { TopRowSlotContext } from './context.js';
5
+ /**
6
+ * The 47 pixel band at the top of a `Workspace`'s sheet, as its `toolbar`: the project, and beside it the
7
+ * slot where the bar of a lone pane goes (`useTopRowSlot`, `PaneBar`'s `container`). The window drags by
8
+ * every part of it that is not a control.
9
+ */
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ export function WorkspaceTopRow(t0) {
12
+ const $ = _c(17);
13
+ let children;
14
+ let className;
15
+ let leading;
16
+ let props;
17
+ let t1;
18
+ if ($[0] !== t0) {
19
+ ({
20
+ leading,
21
+ children,
22
+ slotless: t1,
23
+ className,
24
+ ...props
25
+ } = t0);
26
+ $[0] = t0;
27
+ $[1] = children;
28
+ $[2] = className;
29
+ $[3] = leading;
30
+ $[4] = props;
31
+ $[5] = t1;
32
+ } else {
33
+ children = $[1];
34
+ className = $[2];
35
+ leading = $[3];
36
+ props = $[4];
37
+ t1 = $[5];
38
+ }
39
+ const slotless = t1 === undefined ? false : t1;
40
+ const {
41
+ register
42
+ } = useContext(TopRowSlotContext);
43
+ let t2;
44
+ if ($[6] !== className) {
45
+ t2 = clsx("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", className);
46
+ $[6] = className;
47
+ $[7] = t2;
48
+ } else {
49
+ t2 = $[7];
50
+ }
51
+ let t3;
52
+ if ($[8] !== register || $[9] !== slotless) {
53
+ t3 = !slotless && /*#__PURE__*/_jsx("div", {
54
+ ref: register,
55
+ "data-top-row-slot": "",
56
+ className: "flex h-full min-w-0 grow items-center gap-2"
57
+ });
58
+ $[8] = register;
59
+ $[9] = slotless;
60
+ $[10] = t3;
61
+ } else {
62
+ t3 = $[10];
63
+ }
64
+ let t4;
65
+ if ($[11] !== children || $[12] !== leading || $[13] !== props || $[14] !== t2 || $[15] !== t3) {
66
+ t4 = /*#__PURE__*/_jsxs("header", {
67
+ ...props,
68
+ className: t2,
69
+ children: [leading, children, t3]
70
+ });
71
+ $[11] = children;
72
+ $[12] = leading;
73
+ $[13] = props;
74
+ $[14] = t2;
75
+ $[15] = t3;
76
+ $[16] = t4;
77
+ } else {
78
+ t4 = $[16];
79
+ }
80
+ return t4;
81
+ }
@@ -4,3 +4,7 @@ export declare const WorkspaceContext: import("react").Context<{
4
4
  layout: WorkspaceLayout;
5
5
  edges: SplitEdges;
6
6
  }>;
7
+ export declare const TopRowSlotContext: import("react").Context<{
8
+ slot: HTMLElement | null;
9
+ register(element: HTMLElement | null): void;
10
+ }>;
@@ -7,4 +7,9 @@ export const WorkspaceContext = createContext({
7
7
  bottom: true,
8
8
  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({
13
+ slot: null,
14
+ register: () => undefined
10
15
  });
@@ -0,0 +1,2 @@
1
+ /** The slot of the `Workspace`'s top row, for `PaneBar`'s `container`, or null while there is none. */
2
+ export declare function useTopRowSlot(): HTMLElement | null;
@@ -0,0 +1,6 @@
1
+ import { useContext } from 'react';
2
+ import { TopRowSlotContext } from './context.js';
3
+ /** The slot of the `Workspace`'s top row, for `PaneBar`'s `container`, or null while there is none. */
4
+ export function useTopRowSlot() {
5
+ return useContext(TopRowSlotContext).slot;
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/ui",
3
- "version": "0.49.7",
3
+ "version": "0.50.0",
4
4
  "description": "React components, a theme, formatters and a settings dialog for desktop-grade apps on React 19, Base UI and Tailwind 4.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
@@ -16,6 +16,9 @@ export interface SlidingColumnProps {
16
16
  /* Space before the surface, in pixels. Replaces the border and carries the resize handle. */
17
17
  gap?: number;
18
18
 
19
+ /* The line before the surface without a gap; off beside something that draws an edge of its own over that pixel, such as a workspace's sheet. */
20
+ edge?: boolean;
21
+
19
22
  bounds: { min: number; max(): number };
20
23
  onWidthChange(width: number): void;
21
24
 
@@ -40,7 +43,19 @@ export interface SlidingColumnProps {
40
43
  * The contents sit in an inner column of the stored width, so they do not reflow while the outer one
41
44
  * moves, and stay mounted until the slide is over, so a close plays out.
42
45
  */
43
- export function SlidingColumn({ open, width, gap = 0, bounds, onWidthChange, instant = false, body, className, ref, children }: SlidingColumnProps) {
46
+ export function SlidingColumn({
47
+ open,
48
+ width,
49
+ gap = 0,
50
+ edge = true,
51
+ bounds,
52
+ onWidthChange,
53
+ instant = false,
54
+ body,
55
+ className,
56
+ ref,
57
+ children
58
+ }: SlidingColumnProps) {
44
59
  /* Closed and done animating. */
45
60
  const [settled, setSettled] = useState(!open);
46
61
 
@@ -92,7 +107,11 @@ export function SlidingColumn({ open, width, gap = 0, bounds, onWidthChange, ins
92
107
  <div
93
108
  ref={body?.ref}
94
109
  tabIndex={body ? -1 : undefined}
95
- className={clsx('relative flex h-full shrink-0 flex-col bg-surface outline-none', spacing === 0 && 'border-l border-border')}
110
+ className={clsx(
111
+ 'relative flex h-full shrink-0 flex-col bg-surface outline-none',
112
+ // Without its line the pixel stays, under the edge beside it, so the contents keep their place.
113
+ spacing === 0 && clsx('border-l', edge ? 'border-border' : 'border-transparent')
114
+ )}
96
115
  style={{ width, marginLeft: spacing }}
97
116
  onKeyDown={body?.onKeyDown}
98
117
  >
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export type {
33
33
  export type { KeyValueListItemProps, KeyValueListNameProps, KeyValueListRootProps, KeyValueListValueProps } from './key-value-list/parts.tsx';
34
34
  export type { PopoverPopupProps, PopupVariant, PreviewCardPopupProps } from './popover/parts.tsx';
35
35
  export type { TabsCountProps, TabsListProps, TabsRootProps, TabsTabProps, TabsValue, TabsVariant } from './tabs/parts.tsx';
36
+ export { tabDropIndex, useTabDrag, type TabDrag, type TabDragOptions } from './tabs/use-tab-drag.ts';
36
37
 
37
38
  export { AccentSwatches, type AccentSwatchesProps } from './AccentSwatches.tsx';
38
39
  export { Banner, type BannerProps, type BannerTone } from './Banner.tsx';
@@ -141,6 +142,10 @@ export type {
141
142
  } from './tree/parts.tsx';
142
143
 
143
144
  export { Workspace, type WorkspaceProps } from './workspace/Workspace.tsx';
145
+ export { WorkspaceRail, WorkspaceRailSeparator, WorkspaceRailSpacer, type WorkspaceRailProps } from './workspace/WorkspaceRail.tsx';
146
+ export { WorkspaceTopRow, type WorkspaceTopRowProps } from './workspace/WorkspaceTopRow.tsx';
147
+ export { useTopRowSlot } from './workspace/top-row-slot.ts';
148
+ export { PaneBar, type PaneBarProps } from './workspace/PaneBar.tsx';
144
149
  export type { WorkspaceLayout } from './workspace/context.ts';
145
150
  export { DocumentTab, TabStrip, type DocumentTabProps, type TabStripProps, type TabStripItem } from './workspace/TabStrip.tsx';
146
151
  export { SplitView, type SplitViewProps, type SplitViewInfo, type SplitViewBounds, type SplitDropTarget, type SplitDrag } from './workspace/SplitView.tsx';
package/src/locales/en.ts CHANGED
@@ -86,6 +86,9 @@ export default {
86
86
  pane: 'Panel',
87
87
  movePane: 'Move panel',
88
88
  maximize: 'Maximize panel',
89
- restore: 'Restore panels'
89
+ restore: 'Restore panels',
90
+ moreActions: 'More actions',
91
+ closePane: 'Close panel',
92
+ tools: 'Tools'
90
93
  }
91
94
  };
package/src/locales/nl.ts CHANGED
@@ -86,6 +86,9 @@ export default {
86
86
  pane: 'Paneel',
87
87
  movePane: 'Paneel verplaatsen',
88
88
  maximize: 'Paneel maximaliseren',
89
- restore: 'Panelen herstellen'
89
+ restore: 'Panelen herstellen',
90
+ moreActions: 'Meer acties',
91
+ closePane: 'Paneel sluiten',
92
+ tools: 'Gereedschappen'
90
93
  }
91
94
  };
@@ -0,0 +1,256 @@
1
+ import { useRef, useState, type PointerEvent, type RefObject } from 'react';
2
+
3
+ /* How far a pressed tab moves before it is a drag rather than a click. */
4
+ const DRAG_THRESHOLD_PX = 4;
5
+
6
+ /* How far above or below the strip a dragged tab goes before it tears off. */
7
+ const TEAR_DISTANCE_PX = 24;
8
+
9
+ /* The width of `.ade-tab-insertion`, and how far outside the first or last tab it stands. */
10
+ const INSERTION_WIDTH_PX = 2;
11
+ const INSERTION_INSET_PX = 2;
12
+
13
+ /* A pressed tab: still a click, moving along the strip, out of the strip, or moving the window itself. */
14
+ interface Press {
15
+ key: string;
16
+ pointerId: number;
17
+ /* Where in the window the press began, which is where the pointer holds a window the tab goes into. */
18
+ grab: { x: number; y: number };
19
+ mode: 'press' | 'reorder' | 'out' | 'window';
20
+ /* Where among the other tabs a reorder lets the tab go, or null while it would stay where it is. */
21
+ landing: number | null;
22
+ }
23
+
24
+ export interface TabDragOptions {
25
+ /** The keys of the strip in their order, read when a drag needs them. */
26
+ keys(): readonly string[];
27
+
28
+ /** Where among the other tabs the tab lands for a drop before the `index`th of them, or null where it stays; pinned tabs keep their own run. */
29
+ landingOf(key: string, index: number): number | null;
30
+
31
+ onReorder(key: string, landing: number): void;
32
+
33
+ /**
34
+ * The tab leaves the strip while held: pulled `TEAR_DISTANCE` above or below it, or out of the window.
35
+ * `center` is where it is held, at the start of the strip; `width` is its width. Without it a tab stays in the strip.
36
+ */
37
+ onTearOut?(key: string, center: { x: number; y: number }, width: number): void;
38
+
39
+ /** A release outside the window, for a platform that cannot follow a tab out while it is held. `at` is where the window would stand on screen. */
40
+ onTearOff?(key: string, at: { x: number; y: number }): void;
41
+
42
+ /** Whether dragging this tab moves its window rather than the tab, as for the only tab of a window. */
43
+ dragsWindow?(key: string): boolean;
44
+
45
+ onWindowDrag?(key: string, grab: { x: number; y: number }): void;
46
+
47
+ /** The pointer let go after a tear-out or a window drag, wherever it was. `window` says it moved the window. */
48
+ onRelease?(key: string, window: boolean): void;
49
+ }
50
+
51
+ export interface TabDrag {
52
+ /** For the element around the tabs, whose box decides when a tab leaves the strip. */
53
+ strip: RefObject<HTMLDivElement | null>;
54
+
55
+ /** Where the line that says where the tab lands stands, from the strip's left, or null while there is none. */
56
+ insertion: number | null;
57
+
58
+ /** For each tab, which must also carry `data-tab-key`. */
59
+ tabProps(key: string): {
60
+ 'data-tab-key': string;
61
+ onPointerDown(event: PointerEvent<HTMLElement>): void;
62
+ onPointerMove(event: PointerEvent<HTMLElement>): void;
63
+ onPointerUp(event: PointerEvent<HTMLElement>): void;
64
+ onPointerCancel(event: PointerEvent<HTMLElement>): void;
65
+ onLostPointerCapture(event: PointerEvent<HTMLElement>): void;
66
+ };
67
+ }
68
+
69
+ /** The place among the other tabs of a strip where a dragged tab lands: before the first one whose middle is right of `x`. */
70
+ export function tabDropIndex(strip: Element, key: string, x: number): number {
71
+ const others = [...strip.querySelectorAll<HTMLElement>('[data-tab-key]')].filter((tab) => tab.dataset.tabKey !== key);
72
+
73
+ return others.filter((tab) => {
74
+ const box = tab.getBoundingClientRect();
75
+
76
+ return box.left + box.width / 2 < x;
77
+ }).length;
78
+ }
79
+
80
+ /* Where in the strip the insertion line stands for a tab landing at `landing` among the others: halfway the gap it goes in. */
81
+ function insertionLeft(strip: Element, others: readonly string[], landing: number): number {
82
+ const boxOf = (key: string | undefined) =>
83
+ key === undefined ? null : (strip.querySelector<HTMLElement>(`[data-tab-key="${CSS.escape(key)}"]`)?.getBoundingClientRect() ?? null);
84
+ const before = boxOf(others[landing - 1]);
85
+ const after = boxOf(others[landing]);
86
+ const origin = strip.getBoundingClientRect().left;
87
+ const x =
88
+ before !== null && after !== null
89
+ ? (before.right + after.left) / 2
90
+ : after !== null
91
+ ? after.left - INSERTION_INSET_PX
92
+ : (before?.right ?? origin) + INSERTION_INSET_PX;
93
+
94
+ return Math.max(0, Math.round(x - origin - INSERTION_WIDTH_PX / 2));
95
+ }
96
+
97
+ function outsideViewport(x: number, y: number): boolean {
98
+ return x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight;
99
+ }
100
+
101
+ /**
102
+ * Pointer handling for a strip of tabs that a person reorders by dragging, and can drag out of the strip into
103
+ * a window of its own. The strip stays put while a tab is held, so the insertion line says where it lands and
104
+ * the drop moves it once. A press on a button inside a tab stays that button's.
105
+ */
106
+ export function useTabDrag(options: TabDragOptions): TabDrag {
107
+ const [insertion, setInsertion] = useState<number | null>(null);
108
+ const press = useRef<Press | null>(null);
109
+ const strip = useRef<HTMLDivElement>(null);
110
+ const follows = options.onTearOut !== undefined;
111
+
112
+ /*
113
+ * Out of the strip the tab's element may go with it, so the release is heard on the window itself,
114
+ * which keeps the pointer for as long as it is down.
115
+ */
116
+ const awaitRelease = (held: Press): void => {
117
+ const types = ['pointerup', 'mouseup', 'pointercancel'] as const;
118
+ const release = (): void => {
119
+ for (const type of types) {
120
+ removeEventListener(type, release);
121
+ }
122
+
123
+ if (press.current === held) {
124
+ press.current = null;
125
+ }
126
+
127
+ options.onRelease?.(held.key, held.mode === 'window');
128
+ };
129
+
130
+ for (const type of types) {
131
+ addEventListener(type, release);
132
+ }
133
+ };
134
+
135
+ const onPointerDown = (event: PointerEvent<HTMLElement>, key: string): void => {
136
+ // A close button inside the tab is a press of its own.
137
+ if (event.button !== 0 || (event.target as Element).closest('button') !== null) {
138
+ return;
139
+ }
140
+
141
+ // Held, the pointer keeps reporting to this tab outside the window too, which is where a tab tears off.
142
+ event.currentTarget.setPointerCapture(event.pointerId);
143
+ press.current = { key, pointerId: event.pointerId, grab: { x: event.clientX, y: event.clientY }, mode: 'press', landing: null };
144
+ };
145
+
146
+ const onPointerMove = (event: PointerEvent<HTMLElement>): void => {
147
+ const held = press.current;
148
+
149
+ if (held === null || held.pointerId !== event.pointerId) {
150
+ return;
151
+ }
152
+
153
+ if (held.mode === 'press') {
154
+ if (Math.hypot(event.clientX - held.grab.x, event.clientY - held.grab.y) < DRAG_THRESHOLD_PX) {
155
+ return;
156
+ }
157
+
158
+ if (options.dragsWindow?.(held.key) === true && options.onWindowDrag !== undefined) {
159
+ held.mode = 'window';
160
+ awaitRelease(held);
161
+ options.onWindowDrag(held.key, held.grab);
162
+
163
+ return;
164
+ }
165
+
166
+ held.mode = 'reorder';
167
+ }
168
+
169
+ if (held.mode !== 'reorder') {
170
+ return;
171
+ }
172
+
173
+ const box = strip.current?.getBoundingClientRect();
174
+ const offStrip = box !== undefined && (event.clientY < box.top - TEAR_DISTANCE_PX || event.clientY > box.bottom + TEAR_DISTANCE_PX);
175
+
176
+ if (follows && (offStrip || outsideViewport(event.clientX, event.clientY))) {
177
+ held.mode = 'out';
178
+ held.landing = null;
179
+ setInsertion(null);
180
+ awaitRelease(held);
181
+
182
+ // Held by the middle of its tab, at the start of a strip like this one, until the window it goes into says where its strip is.
183
+ const tab = event.currentTarget.getBoundingClientRect();
184
+ const left = box?.left ?? tab.left;
185
+
186
+ options.onTearOut?.(held.key, { x: left + tab.width / 2, y: tab.top + tab.height / 2 }, tab.width);
187
+
188
+ return;
189
+ }
190
+
191
+ const row = event.currentTarget.parentElement;
192
+
193
+ if (row === null || strip.current === null) {
194
+ return;
195
+ }
196
+
197
+ held.landing = options.landingOf(held.key, tabDropIndex(row, held.key, event.clientX));
198
+
199
+ const others = options.keys().filter((one) => one !== held.key);
200
+
201
+ setInsertion(held.landing === null ? null : insertionLeft(strip.current, others, held.landing));
202
+ };
203
+
204
+ const onPointerEnd = (event: PointerEvent<HTMLElement>, cancelled: boolean): void => {
205
+ const held = press.current;
206
+
207
+ if (held === null || held.pointerId !== event.pointerId || held.mode === 'out' || held.mode === 'window') {
208
+ return;
209
+ }
210
+
211
+ press.current = null;
212
+ setInsertion(null);
213
+
214
+ if (held.mode !== 'reorder' || cancelled) {
215
+ return;
216
+ }
217
+
218
+ if (!follows && options.onTearOff !== undefined && outsideViewport(event.clientX, event.clientY)) {
219
+ options.onTearOff(held.key, { x: event.screenX - held.grab.x, y: event.screenY - held.grab.y });
220
+
221
+ return;
222
+ }
223
+
224
+ if (held.landing !== null) {
225
+ options.onReorder(held.key, held.landing);
226
+ }
227
+ };
228
+
229
+ // A held tab whose element moves, as when a tab from elsewhere drops into the strip, lets go of the capture; it is taken back.
230
+ const onLostPointerCapture = (event: PointerEvent<HTMLElement>): void => {
231
+ const held = press.current;
232
+
233
+ if (held === null || held.pointerId !== event.pointerId || (held.mode !== 'press' && held.mode !== 'reorder')) {
234
+ return;
235
+ }
236
+
237
+ try {
238
+ event.currentTarget.setPointerCapture(event.pointerId);
239
+ } catch {
240
+ press.current = null;
241
+ }
242
+ };
243
+
244
+ return {
245
+ strip,
246
+ insertion,
247
+ tabProps: (key) => ({
248
+ 'data-tab-key': key,
249
+ onPointerDown: (event) => onPointerDown(event, key),
250
+ onPointerMove,
251
+ onPointerUp: (event) => onPointerEnd(event, false),
252
+ onPointerCancel: (event) => onPointerEnd(event, true),
253
+ onLostPointerCapture
254
+ })
255
+ };
256
+ }
package/src/theme.css CHANGED
@@ -91,6 +91,8 @@
91
91
  --media-handle-text: #18181b;
92
92
  --media-handle-edge: 0 0 0 1px rgb(0 0 0 / 0.3);
93
93
  --media-handle-shadow: 0 2px 12px rgb(0 0 0 / 0.35);
94
+ /* Around a workspace's sheet; light carries half the blur of dark. */
95
+ --sheet-shadow: 0 0 7px rgb(30 30 50 / 0.06);
94
96
  /* The file type palette. A file icon is the one thing in the interface that is not tinted by the
95
97
  theme: TypeScript is blue and Vue is green wherever the file shows up, which is what makes a
96
98
  row readable before the name is. The sprite of the file icons reads these, inside a tree's
@@ -151,6 +153,7 @@
151
153
  --thumb-shadow:
152
154
  0 1px 2px rgb(0 0 0 / 0.5),
153
155
  0 2px 4px -1px rgb(0 0 0 / 0.4);
156
+ --sheet-shadow: 0 0 14px rgb(0 0 0 / 0.3);
154
157
  --status-running: #60a5fa;
155
158
  --status-needs-you: #fbbf24;
156
159
  --status-idle: #4ade80;
@@ -818,16 +821,32 @@
818
821
  }
819
822
 
820
823
  @layer components {
824
+ /* The sidebar and the content lie on one sheet, which stands over the side panel and the rail with its
825
+ own edge and shadow; those two sit a step darker, in the well behind it. */
821
826
  .ade-workspace {
827
+ --workspace-well: color-mix(in srgb, var(--surface) 70%, var(--surface-sunken));
822
828
  display: flex;
823
829
  width: 100%;
824
830
  height: 100%;
825
831
  min-width: 0;
826
832
  min-height: 0;
827
833
  overflow: hidden;
828
- background: var(--separator);
834
+ background: var(--workspace-well);
829
835
  --border: var(--separator);
830
836
  }
837
+ .ade-workspace-sheet {
838
+ position: relative;
839
+ z-index: 1;
840
+ display: flex;
841
+ flex: 1;
842
+ min-width: 0;
843
+ min-height: 0;
844
+ overflow: hidden;
845
+ border-radius: 0 12px 12px 0;
846
+ /* The surface its regions draw, so no lighter ground shows where the rounded edge cuts them. Roomy shows its gaps in it. */
847
+ background: var(--surface);
848
+ box-shadow: 0 0 0 1px var(--border), var(--sheet-shadow);
849
+ }
831
850
  .ade-workspace-center {
832
851
  display: flex;
833
852
  flex: 1;
@@ -843,7 +862,8 @@
843
862
  background: var(--surface);
844
863
  }
845
864
  .ade-workspace-sidebar { border-right: 1px solid var(--separator); }
846
- .ade-workspace-side-panel { border-left: 1px solid var(--separator); }
865
+ /* What draws a surface inside the panel draws the well instead. Its left edge is the sheet's. */
866
+ .ade-workspace-side-panel { --surface: var(--workspace-well); }
847
867
  .ade-workspace-toolbar {
848
868
  flex: none;
849
869
  overflow: hidden;
@@ -858,15 +878,46 @@
858
878
  overflow: hidden;
859
879
  background: var(--surface);
860
880
  }
861
- .ade-workspace[data-layout="roomy"] > .ade-workspace-center { gap: 8px; }
862
- .ade-workspace[data-layout="roomy"] > .ade-workspace-sidebar:not([data-auto]) { margin-right: 8px; }
863
- .ade-workspace[data-layout="roomy"] > .ade-workspace-side-panel:not([data-auto]) { margin-left: 8px; }
864
881
  .ade-workspace-sidebar[data-auto], .ade-workspace-side-panel[data-auto] { display: contents; }
865
- .ade-workspace[data-layout="roomy"] > .ade-workspace-sidebar,
866
- .ade-workspace[data-layout="roomy"] > .ade-workspace-side-panel,
867
- .ade-workspace[data-layout="roomy"] > .ade-workspace-center > .ade-workspace-toolbar { border: 0; }
868
- .ade-workspace[data-layout="roomy"][data-sidebar] > .ade-workspace-center > .ade-workspace-toolbar { border-bottom-left-radius: 8px; }
869
- .ade-workspace[data-layout="roomy"][data-side-panel] > .ade-workspace-center > .ade-workspace-toolbar { border-bottom-right-radius: 8px; }
882
+ .ade-workspace[data-layout="roomy"],
883
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-sheet { background: var(--separator); }
884
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-sheet > .ade-workspace-center { gap: 8px; }
885
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-sheet > .ade-workspace-sidebar:not([data-auto]) { margin-right: 8px; }
886
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-side-panel:not([data-auto]) { margin-left: 8px; }
887
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-sheet > .ade-workspace-sidebar,
888
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-sheet > .ade-workspace-center > .ade-workspace-toolbar { border: 0; }
889
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-sheet > .ade-workspace-center > .ade-workspace-toolbar { border-radius: 0 0 8px 8px; }
890
+ .ade-workspace[data-layout="roomy"][data-toolbar] > .ade-workspace-sheet > .ade-workspace-center > .ade-workspace-content { border-radius: 8px 8px 0 0; }
891
+
892
+ .ade-workspace-rail {
893
+ display: flex;
894
+ flex: none;
895
+ flex-direction: column;
896
+ align-items: center;
897
+ gap: 4px;
898
+ width: 48px;
899
+ min-height: 0;
900
+ /* The 7 pixels and the first separator's 3 put that separator on the bottom line of a 47 pixel toolbar. */
901
+ padding: 7px 0 8px;
902
+ background: var(--workspace-well, var(--surface));
903
+ /* As long as a sliding column takes to open, so the line comes and goes with the panel. */
904
+ transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
905
+ }
906
+ /* No line beside the sheet, whose rounded edge shows while the panel is closed; one beside an open panel. */
907
+ .ade-workspace[data-side-panel] > .ade-workspace-rail { box-shadow: inset 1px 0 var(--separator); }
908
+ .ade-workspace[data-layout="roomy"] > .ade-workspace-rail { margin-left: 8px; box-shadow: none; }
909
+ .ade-workspace-rail-separator {
910
+ flex: none;
911
+ width: 20px;
912
+ height: 1px;
913
+ margin: 4px 0;
914
+ background: var(--separator);
915
+ }
916
+ .ade-workspace-rail > :nth-child(1 of .ade-workspace-rail-separator) { margin-top: 3px; }
917
+ .ade-workspace-rail-spacer { flex: 1; }
918
+ /* The window drags by the top row, except where a control or a pane's title answers the pointer. */
919
+ .ade-workspace-top-row { -webkit-app-region: drag; }
920
+ .ade-workspace-top-row :is(button, input, a, [role="tab"], .ade-pane-title) { -webkit-app-region: no-drag; }
870
921
  .ade-split-view {
871
922
  position: relative;
872
923
  isolation: isolate;
@@ -915,8 +966,6 @@
915
966
  .ade-split-divider[aria-orientation="horizontal"]::after { inset-inline: 0; top: -1px; height: 2px; }
916
967
  .ade-split-view[data-layout="roomy"] > .ade-split-divider[aria-orientation="vertical"]::after { left: 3px; }
917
968
  .ade-split-view[data-layout="roomy"] > .ade-split-divider[aria-orientation="horizontal"]::after { top: 3px; }
918
- .ade-workspace[data-layout="roomy"][data-toolbar][data-sidebar] > .ade-workspace-center > .ade-workspace-content { border-top-left-radius: 8px; }
919
- .ade-workspace[data-layout="roomy"][data-toolbar][data-side-panel] > .ade-workspace-center > .ade-workspace-content { border-top-right-radius: 8px; }
920
969
  .ade-split-divider:is(:hover, :focus-visible, [data-resizing])::after { background: var(--accent); }
921
970
  .ade-drop-preview {
922
971
  position: absolute;
@@ -959,6 +1008,8 @@
959
1008
  gap: 4px;
960
1009
  padding: 0 8px 0 12px;
961
1010
  color: var(--text-muted);
1011
+ /* The weight of a pane's title, so a view reads the same as a tab and as the only view of its pane. */
1012
+ font-weight: 500;
962
1013
  }
963
1014
  .ade-document-tab:hover { color: var(--text); background: var(--surface-raised); }
964
1015
  .ade-document-tab[data-active] { color: var(--text); }
@@ -977,7 +1028,8 @@
977
1028
  min-width: 0;
978
1029
  max-width: 160px;
979
1030
  align-items: center;
980
- gap: 6px;
1031
+ /* As far from its icon as a pane's title stands from its glyph. */
1032
+ gap: 8px;
981
1033
  font-size: var(--text-sm);
982
1034
  color: inherit;
983
1035
  outline-offset: -2px;