@adecore/ui 0.21.1 → 0.21.2
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/README.md +4 -4
- package/dist/DisabledReason.js +4 -0
- package/dist/Field.js +3 -2
- package/dist/Input.js +3 -8
- package/dist/SegmentBar.js +2 -1
- package/dist/Separator.js +2 -3
- package/dist/SlidingColumn.js +1 -1
- package/dist/ZoomControls.js +4 -1
- package/dist/field-context.d.ts +5 -0
- package/dist/field-context.js +6 -0
- package/dist/file-icon.js +8 -10
- package/dist/file-tree/parts.js +8 -15
- package/dist/file-tree/use-file-tree-shift.js +4 -5
- package/dist/file-tree/use-slots.js +31 -31
- package/dist/format/datetime.js +6 -5
- package/dist/format/locale.js +6 -1
- package/dist/format/number.js +15 -18
- package/dist/menu/parts.d.ts +3 -2
- package/dist/menu/parts.js +4 -2
- package/dist/modality.js +4 -5
- package/dist/placement.d.ts +4 -0
- package/dist/placement.js +23 -0
- package/dist/popover/parts.d.ts +4 -3
- package/dist/popover/parts.js +7 -4
- package/dist/shortcut.js +9 -4
- package/dist/tabs/parts.js +3 -2
- package/dist/tree/parts.js +10 -20
- package/dist/tree/use-tree-shift.js +1 -1
- package/dist/workspace/SplitView.js +20 -14
- package/dist/workspace/TabStrip.js +19 -11
- package/dist/workspace/Workspace.js +4 -3
- package/dist/workspace/model.js +123 -122
- package/package.json +1 -1
- package/src/DisabledReason.tsx +4 -5
- package/src/Field.tsx +4 -2
- package/src/IconButton.tsx +0 -1
- package/src/Input.tsx +3 -10
- package/src/Pill.tsx +8 -1
- package/src/SegmentBar.tsx +3 -10
- package/src/Separator.tsx +2 -3
- package/src/SlidingColumn.tsx +1 -1
- package/src/ZoomControls.tsx +11 -7
- package/src/field-context.ts +7 -0
- package/src/file-icon.ts +8 -10
- package/src/file-tree/parts.tsx +17 -22
- package/src/file-tree/use-file-tree-shift.tsx +4 -7
- package/src/file-tree/use-slots.ts +31 -31
- package/src/format/datetime.ts +7 -5
- package/src/format/locale.ts +7 -7
- package/src/format/number.ts +17 -20
- package/src/menu/parts.tsx +10 -29
- package/src/modality.ts +4 -5
- package/src/placement.ts +26 -0
- package/src/popover/parts.tsx +10 -63
- package/src/settings/SettingsDialog.tsx +1 -1
- package/src/shortcut.ts +9 -4
- package/src/tabs/parts.tsx +8 -8
- package/src/tree/parts.tsx +14 -21
- package/src/tree/use-tree-shift.tsx +1 -1
- package/src/workspace/SplitView.tsx +23 -19
- package/src/workspace/TabStrip.tsx +20 -12
- package/src/workspace/Workspace.tsx +5 -3
- package/src/workspace/model.ts +141 -138
- /package/dist/{file-tree → tree}/shift.d.ts +0 -0
- /package/dist/{file-tree → tree}/shift.js +0 -0
- /package/src/{file-tree → tree}/shift.ts +0 -0
package/dist/popover/parts.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Popover as BasePopover } from '@base-ui-components/react/popover';
|
|
3
3
|
import { PreviewCard as BasePreviewCard } from '@base-ui-components/react/preview-card';
|
|
4
4
|
import { withClass } from '../class-name.js';
|
|
5
|
+
import { splitPlacement } from '../placement.js';
|
|
5
6
|
import { renderPositioner } from '../popup-layer.js';
|
|
6
7
|
/*
|
|
7
8
|
* The surface a popup is drawn on. `menu` is the padded card menus use, `picker` a card whose
|
|
@@ -18,12 +19,14 @@ export const PopoverClose = BasePopover.Close;
|
|
|
18
19
|
export const PopoverTitle = BasePopover.Title;
|
|
19
20
|
export const PopoverDescription = BasePopover.Description;
|
|
20
21
|
/* The portal, the positioner and the popup of a popover: under its trigger and lined up with its start unless it says otherwise. */
|
|
21
|
-
export function PopoverPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8,
|
|
22
|
-
|
|
22
|
+
export function PopoverPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8, className, ...props }) {
|
|
23
|
+
const [placement, popupProps] = splitPlacement(props);
|
|
24
|
+
return (_jsx(BasePopover.Portal, { children: _jsx(BasePopover.Positioner, { className: "popup-positioner", render: renderPositioner, ...placement, side: side, align: align, sideOffset: sideOffset, children: _jsx(BasePopover.Popup, { className: withClass(POPUP_VARIANT[variant], className), ...popupProps }) }) }));
|
|
23
25
|
}
|
|
24
26
|
export const PreviewCardRoot = BasePreviewCard.Root;
|
|
25
27
|
export const PreviewCardTrigger = BasePreviewCard.Trigger;
|
|
26
28
|
/* A card that opens while the pointer rests on its trigger, drawn like a popover. */
|
|
27
|
-
export function PreviewCardPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8,
|
|
28
|
-
|
|
29
|
+
export function PreviewCardPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8, className, ...props }) {
|
|
30
|
+
const [placement, popupProps] = splitPlacement(props);
|
|
31
|
+
return (_jsx(BasePreviewCard.Portal, { children: _jsx(BasePreviewCard.Positioner, { className: "z-(--z-popup)", ...placement, side: side, align: align, sideOffset: sideOffset, children: _jsx(BasePreviewCard.Popup, { className: withClass(POPUP_VARIANT[variant], className), ...popupProps }) }) }));
|
|
29
32
|
}
|
package/dist/shortcut.js
CHANGED
|
@@ -14,7 +14,13 @@ const PUNCTUATION_CODES = {
|
|
|
14
14
|
const CHARACTER_KEYS = new Set(['+', '-', '=']);
|
|
15
15
|
const NAMED_KEYS = new Set(['Enter', 'Escape', 'Backspace', 'Delete', 'Tab', 'Space', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']);
|
|
16
16
|
const FUNCTION_KEY = /^F([1-9]|1[0-2])$/;
|
|
17
|
-
const
|
|
17
|
+
const MODIFIER_FIELDS = {
|
|
18
|
+
Mod: 'mod',
|
|
19
|
+
Ctrl: 'ctrl',
|
|
20
|
+
Meta: 'meta',
|
|
21
|
+
Alt: 'alt',
|
|
22
|
+
Shift: 'shift'
|
|
23
|
+
};
|
|
18
24
|
const isKey = (word) => /^[A-Za-z0-9]$/.test(word) || word in PUNCTUATION_CODES || CHARACTER_KEYS.has(word) || NAMED_KEYS.has(word) || FUNCTION_KEY.test(word);
|
|
19
25
|
/**
|
|
20
26
|
* Parses `Mod+Shift+K`. Throws on a word it does not know or on two keys, so a typo in a table fails
|
|
@@ -25,9 +31,8 @@ export const shortcut = (text) => {
|
|
|
25
31
|
const words = text === '+' ? ['+'] : text.endsWith('++') ? [...text.slice(0, -2).split('+'), '+'] : text.split('+');
|
|
26
32
|
const result = { mod: false, ctrl: false, meta: false, alt: false, shift: false, key: '' };
|
|
27
33
|
for (const word of words) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
result[modifier === 'Mod' ? 'mod' : modifier === 'Ctrl' ? 'ctrl' : modifier === 'Meta' ? 'meta' : modifier === 'Alt' ? 'alt' : 'shift'] = true;
|
|
34
|
+
if (Object.hasOwn(MODIFIER_FIELDS, word)) {
|
|
35
|
+
result[MODIFIER_FIELDS[word]] = true;
|
|
31
36
|
continue;
|
|
32
37
|
}
|
|
33
38
|
if (!isKey(word)) {
|
package/dist/tabs/parts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Children, createContext, isValidElement, useContext, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { ChevronDown, X } from 'lucide-react';
|
|
@@ -77,7 +77,8 @@ export function TabsList({ className, children, start, end, ...props }) {
|
|
|
77
77
|
});
|
|
78
78
|
const visible = shown === null ? tabs : shown.map((index) => tabs[index]);
|
|
79
79
|
const hidden = shown === null ? [] : tabs.filter((_, index) => !shown.includes(index));
|
|
80
|
-
|
|
80
|
+
const more = (_jsxs(_Fragment, { children: [t('tabs.more'), _jsx(Icon, { icon: ChevronDown, size: 14 })] }));
|
|
81
|
+
return (_jsxs("div", { ref: strip, className: clsx('relative flex items-center gap-4 border-b border-border', className), children: [start !== undefined && start !== null && (_jsx("div", { ref: head, className: "flex shrink-0 items-center gap-1", children: start })), _jsx(BaseTabs.List, { className: "flex min-w-0 items-end gap-4", ...props, children: visible }), hidden.length > 0 && (_jsxs(MenuRoot, { children: [_jsx(MenuTrigger, { className: MORE, children: more }), _jsx(MenuPopup, { align: "end", className: "min-w-48", children: _jsx(InMenuContext, { value: true, children: hidden.map((tab) => (_jsx(MenuItem, { disabled: tab.props.disabled, onClick: () => select(tab.props.value), children: tab.props.children }, tab.key))) }) })] })), end !== undefined && end !== null && (_jsx("div", { ref: tail, className: clsx('flex shrink-0 items-center gap-1', hidden.length === 0 && 'ml-auto'), children: end })), _jsxs("div", { ref: measure, "aria-hidden": true, inert: true, className: "pointer-events-none invisible absolute top-0 left-0 flex gap-4", children: [tabs.map((tab) => (_jsxs("span", { className: TAB, children: [tab.props.children, tab.props.onClose !== undefined && _jsx("span", { className: "size-5 shrink-0" })] }, tab.key))), _jsx("span", { className: MORE, children: more })] })] }));
|
|
81
82
|
}
|
|
82
83
|
/* The close button stays out of the tab order, so a closable tab is still one stop; the keys close it from the tab itself. */
|
|
83
84
|
export function TabsTab({ className, onClose, children, ...props }) {
|
package/dist/tree/parts.js
CHANGED
|
@@ -9,6 +9,11 @@ import { Icon } from '../Icon.js';
|
|
|
9
9
|
import { mergeRefs } from '../merge-refs.js';
|
|
10
10
|
import { treeRowStyle } from './style.js';
|
|
11
11
|
import { TREE_ROWS, useTreeShift } from './use-tree-shift.js';
|
|
12
|
+
/* A handler that keeps the event to the control, so the row under it never sees the press. */
|
|
13
|
+
const stopped = (handler) => (event) => {
|
|
14
|
+
event.stopPropagation();
|
|
15
|
+
handler?.(event);
|
|
16
|
+
};
|
|
12
17
|
export function TreeRoot({ overflow = 'truncate', render, className, ref, children, ...props }) {
|
|
13
18
|
const { attach, bar } = useTreeShift(TREE_ROWS);
|
|
14
19
|
const scrolls = overflow === 'scroll';
|
|
@@ -76,10 +81,7 @@ export function TreeChevron({ expanded, onExpandedChange, render, className, ref
|
|
|
76
81
|
onExpandedChange?.(!expanded);
|
|
77
82
|
}
|
|
78
83
|
},
|
|
79
|
-
onDoubleClick: (
|
|
80
|
-
event.stopPropagation();
|
|
81
|
-
onDoubleClick?.(event);
|
|
82
|
-
},
|
|
84
|
+
onDoubleClick: stopped(onDoubleClick),
|
|
83
85
|
children: _jsx(Icon, { icon: ChevronRight, size: 12, className: expanded ? 'rotate-90' : undefined })
|
|
84
86
|
}
|
|
85
87
|
});
|
|
@@ -99,22 +101,10 @@ export function TreeControl({ render, className, ref, onClick, onDoubleClick, on
|
|
|
99
101
|
...props,
|
|
100
102
|
'data-tree-control': true,
|
|
101
103
|
className: clsx('adecore-tree-control', className),
|
|
102
|
-
onClick: (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
onDoubleClick: (event) => {
|
|
107
|
-
event.stopPropagation();
|
|
108
|
-
onDoubleClick?.(event);
|
|
109
|
-
},
|
|
110
|
-
onKeyDown: (event) => {
|
|
111
|
-
event.stopPropagation();
|
|
112
|
-
onKeyDown?.(event);
|
|
113
|
-
},
|
|
114
|
-
onPointerDown: (event) => {
|
|
115
|
-
event.stopPropagation();
|
|
116
|
-
onPointerDown?.(event);
|
|
117
|
-
}
|
|
104
|
+
onClick: stopped(onClick),
|
|
105
|
+
onDoubleClick: stopped(onDoubleClick),
|
|
106
|
+
onKeyDown: stopped(onKeyDown),
|
|
107
|
+
onPointerDown: stopped(onPointerDown)
|
|
118
108
|
}
|
|
119
109
|
});
|
|
120
110
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import { clampShift, maxShift, SHIFT_PROPERTY, shiftNeed, shiftThumb, sidewaysDelta } from '
|
|
3
|
+
import { clampShift, maxShift, SHIFT_PROPERTY, shiftNeed, shiftThumb, sidewaysDelta } from './shift.js';
|
|
4
4
|
const HIDE_AFTER = 900;
|
|
5
5
|
const NEAR_BOTTOM = 16;
|
|
6
6
|
/*
|
|
@@ -12,6 +12,11 @@ import { TabStrip } from './TabStrip.js';
|
|
|
12
12
|
import { dropPreviewPath, rectPreview, tabPreview } from './preview.js';
|
|
13
13
|
const ZERO = { width: 0, height: 0, left: 0, top: 0, scaleX: 1, scaleY: 1 };
|
|
14
14
|
const MINIMUM = { width: 120, height: 80 };
|
|
15
|
+
/* The pane's section or header under `container`, found by its data attribute rather than a selector, so no id needs escaping. */
|
|
16
|
+
function paneElement(container, attribute, paneId) {
|
|
17
|
+
const selector = attribute === 'splitPane' ? '[data-split-pane]' : '[data-split-header]';
|
|
18
|
+
return Array.from(container?.querySelectorAll(selector) ?? []).find((element) => element.dataset[attribute] === paneId);
|
|
19
|
+
}
|
|
15
20
|
function Divider({ divider, minimumResizeShare, onResize, onEqualize }) {
|
|
16
21
|
const { t } = useTranslation('ui');
|
|
17
22
|
const cleanup = useRef(null);
|
|
@@ -94,7 +99,7 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
|
|
|
94
99
|
element &&
|
|
95
100
|
(!previous.isConnected || previous.closest('[hidden]')) &&
|
|
96
101
|
(!active || active === element.ownerDocument.body || active === previous)) {
|
|
97
|
-
const pane =
|
|
102
|
+
const pane = paneElement(element, 'splitPane', value.focused);
|
|
98
103
|
const target = pane?.querySelector('[role="tab"][aria-selected="true"]') ?? pane;
|
|
99
104
|
target?.focus({ preventScroll: true });
|
|
100
105
|
}
|
|
@@ -177,6 +182,14 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
|
|
|
177
182
|
.sort((left, right) => (left.viewId < right.viewId ? -1 : left.viewId > right.viewId ? 1 : 0));
|
|
178
183
|
const visible = new Map(geometry.panes.map((pane) => [pane.pane.id, pane]));
|
|
179
184
|
const radius = (rect) => paneRadius(rect, measurement.width, measurement.height, edges, layout === 'roomy' ? 8 : 0);
|
|
185
|
+
// A header squares off the top corners of the body under it.
|
|
186
|
+
const bodyRadius = (rect, header) => {
|
|
187
|
+
const corners = radius(rect).split(' ');
|
|
188
|
+
if (header > 0) {
|
|
189
|
+
corners[0] = corners[1] = '0px';
|
|
190
|
+
}
|
|
191
|
+
return corners.join(' ');
|
|
192
|
+
};
|
|
180
193
|
const bounds = panes.flatMap((pane) => {
|
|
181
194
|
const rect = visible.get(pane.id);
|
|
182
195
|
const header = Math.min(barHeight, rect?.height ?? 0);
|
|
@@ -188,11 +201,8 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
|
|
|
188
201
|
height: Math.max(0, rect.height - header) * measurement.scaleY
|
|
189
202
|
}
|
|
190
203
|
: { x: 0, y: 0, width: 0, height: 0 };
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
corners[0] = corners[1] = '0px';
|
|
194
|
-
}
|
|
195
|
-
return pane.views.map((viewId) => ({ viewId, paneId: pane.id, active: !!rect && pane.active === viewId, rect: body, borderRadius: corners.join(' ') }));
|
|
204
|
+
const borderRadius = rect ? bodyRadius(rect, header) : '0px 0px 0px 0px';
|
|
205
|
+
return pane.views.map((viewId) => ({ viewId, paneId: pane.id, active: !!rect && pane.active === viewId, rect: body, borderRadius }));
|
|
196
206
|
});
|
|
197
207
|
const boundsKey = JSON.stringify(bounds);
|
|
198
208
|
const reportedBounds = useRef('');
|
|
@@ -244,7 +254,7 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
|
|
|
244
254
|
return null;
|
|
245
255
|
}
|
|
246
256
|
const header = Math.min(barHeight, rect.height);
|
|
247
|
-
const strip =
|
|
257
|
+
const strip = paneElement(root.current, 'splitHeader', rect.pane.id);
|
|
248
258
|
const tabRects = Array.from(strip?.querySelectorAll('[data-document-tab]') ?? [], (element) => element.getBoundingClientRect());
|
|
249
259
|
const inHeader = y < rect.y + header;
|
|
250
260
|
const side = inHeader ? undefined : (splitSideAt(rect, x, y) ?? 'center');
|
|
@@ -304,7 +314,7 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
|
|
|
304
314
|
const next = options[0];
|
|
305
315
|
if (next) {
|
|
306
316
|
dispatch({ type: 'focus', paneId: next.pane.id });
|
|
307
|
-
const element =
|
|
317
|
+
const element = paneElement(root.current, 'splitPane', next.pane.id);
|
|
308
318
|
element?.querySelector('[role="tab"][aria-selected="true"]')?.focus();
|
|
309
319
|
if (!showTabs) {
|
|
310
320
|
element?.focus();
|
|
@@ -394,17 +404,13 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
|
|
|
394
404
|
const rect = visible.get(pane.id);
|
|
395
405
|
const active = !!rect && pane.active === viewId;
|
|
396
406
|
const header = Math.min(barHeight, rect?.height ?? 0);
|
|
397
|
-
const corners = rect ? radius(rect).split(' ') : [];
|
|
398
|
-
if (header > 0) {
|
|
399
|
-
corners[0] = corners[1] = '0px';
|
|
400
|
-
}
|
|
401
407
|
const position = rect
|
|
402
408
|
? {
|
|
403
409
|
left: rect.x,
|
|
404
410
|
top: rect.y + header,
|
|
405
411
|
width: rect.width,
|
|
406
412
|
height: Math.max(0, rect.height - header),
|
|
407
|
-
borderRadius:
|
|
413
|
+
borderRadius: bodyRadius(rect, header)
|
|
408
414
|
}
|
|
409
415
|
: {};
|
|
410
416
|
return (_jsx("div", { id: id('view', viewId), role: "tabpanel", "aria-labelledby": showTabs && !renderPaneHeader ? id('tab', viewId) : undefined, "aria-label": !showTabs || renderPaneHeader ? (getTab?.(viewId).label ?? viewId) : undefined, className: "ade-split-content", "data-split-view-id": viewId, hidden: !active, inert: !active, tabIndex: 0, style: position, onFocusCapture: () => dispatch({ type: 'focus', paneId: pane.id }), onPointerDown: () => dispatch({ type: 'focus', paneId: pane.id }), children: renderView(viewId, { paneId: pane.id, active }) }, viewId));
|
|
@@ -413,7 +419,7 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
|
|
|
413
419
|
type: 'equalizeAxis',
|
|
414
420
|
axis: divider.axis,
|
|
415
421
|
length: divider.axis === 'horizontal' ? measurement.width : measurement.height,
|
|
416
|
-
gap
|
|
422
|
+
gap
|
|
417
423
|
}
|
|
418
424
|
: { type: 'equalize', splitId: divider.splitId, index: divider.index }) }, `${divider.splitId}:${divider.index}`))), preview && (_jsx("svg", { className: "ade-split-preview", width: "100%", height: "100%", "aria-hidden": "true", children: _jsx("path", { style: { d: `path("${dropPreviewPath(preview)}")` } }) }))] }))
|
|
419
425
|
}
|
|
@@ -8,6 +8,21 @@ import { Icon } from '../Icon.js';
|
|
|
8
8
|
import { IconButton } from '../IconButton.js';
|
|
9
9
|
import { Tooltip } from '../Tooltip.js';
|
|
10
10
|
import { tabGapAt } from './geometry.js';
|
|
11
|
+
/* Where an arrow, Home or End moves the selection from `index`, wrapping at either end; null for any other key. */
|
|
12
|
+
function tabIndexForKey(key, index, count) {
|
|
13
|
+
switch (key) {
|
|
14
|
+
case 'ArrowRight':
|
|
15
|
+
return (index + 1) % count;
|
|
16
|
+
case 'ArrowLeft':
|
|
17
|
+
return (index + count - 1) % count;
|
|
18
|
+
case 'Home':
|
|
19
|
+
return 0;
|
|
20
|
+
case 'End':
|
|
21
|
+
return count - 1;
|
|
22
|
+
default:
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
11
26
|
export function TabStrip({ items, value, onValueChange, renderTab, focused = true, insertAt, onClose, onDoubleClick, onContextMenu, onDragStart, onDragEnd, onTabDragOver, onTabDrop, render, className, ref, ...props }) {
|
|
12
27
|
const { t } = useTranslation('ui');
|
|
13
28
|
const strip = useRef(null);
|
|
@@ -126,6 +141,7 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
|
|
|
126
141
|
const { items, value, onValueChange } = context;
|
|
127
142
|
const index = items.findIndex((candidate) => candidate.id === item.id);
|
|
128
143
|
const close = () => context.close(item.id, onClose);
|
|
144
|
+
const closable = item.closable !== false && (onClose ?? context.onClose) !== undefined;
|
|
129
145
|
return useRender({
|
|
130
146
|
render,
|
|
131
147
|
ref,
|
|
@@ -144,25 +160,17 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
|
|
|
144
160
|
event.stopPropagation();
|
|
145
161
|
(onDragStart ?? context.onDragStart)?.(item.id, event);
|
|
146
162
|
}, onDragEnd: onDragEnd ?? context.onDragEnd, onKeyDown: (event) => {
|
|
147
|
-
const next = event.key
|
|
148
|
-
? (index + 1) % items.length
|
|
149
|
-
: event.key === 'ArrowLeft'
|
|
150
|
-
? (index + items.length - 1) % items.length
|
|
151
|
-
: event.key === 'Home'
|
|
152
|
-
? 0
|
|
153
|
-
: event.key === 'End'
|
|
154
|
-
? items.length - 1
|
|
155
|
-
: null;
|
|
163
|
+
const next = tabIndexForKey(event.key, index, items.length);
|
|
156
164
|
if (next !== null) {
|
|
157
165
|
event.preventDefault();
|
|
158
166
|
onValueChange(items[next].id);
|
|
159
167
|
context.focus(next);
|
|
160
168
|
}
|
|
161
|
-
else if (event.key === 'Delete' &&
|
|
169
|
+
else if (event.key === 'Delete' && closable) {
|
|
162
170
|
event.preventDefault();
|
|
163
171
|
close();
|
|
164
172
|
}
|
|
165
|
-
}, children: [item.icon, _jsx("span", { className: "truncate", children: item.label }), item.detail, item.attention, item.unsaved && (_jsx("span", { className: "ade-tab-unsaved", children: _jsx("span", { className: "sr-only", children: typeof item.unsaved === 'string' ? item.unsaved : t('workspace.unsaved') }) }))] }) }), item.pinned && _jsx(Icon, { icon: Pin, size: 12, className: "shrink-0 text-text-muted" }),
|
|
173
|
+
}, children: [item.icon, _jsx("span", { className: "truncate", children: item.label }), item.detail, item.attention, item.unsaved && (_jsx("span", { className: "ade-tab-unsaved", children: _jsx("span", { className: "sr-only", children: typeof item.unsaved === 'string' ? item.unsaved : t('workspace.unsaved') }) }))] }) }), item.pinned && _jsx(Icon, { icon: Pin, size: 12, className: "shrink-0 text-text-muted" }), closable && (_jsx(IconButton, { icon: X, size: "2xs", label: t('workspace.closeTab', { name: item.label }), kbd: closeShortcut, className: "ade-document-tab-close", onClick: close }))] }))
|
|
166
174
|
}
|
|
167
175
|
});
|
|
168
176
|
}
|
|
@@ -2,12 +2,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useRender } from '@base-ui-components/react/use-render';
|
|
4
4
|
import { WorkspaceContext } from './context.js';
|
|
5
|
+
const isPresent = (node) => node !== undefined && node !== null && node !== false;
|
|
5
6
|
export function Workspace({ layout = 'standard', toolbar, sidebar, sidePanel, sidebarWidth = 248, sidePanelWidth = 320, sidebarOpen, sidePanelOpen, children, className, ref, render, style, ...props }) {
|
|
6
|
-
const hasSidebar = sidebar
|
|
7
|
-
const hasPanel = sidePanel
|
|
7
|
+
const hasSidebar = isPresent(sidebar);
|
|
8
|
+
const hasPanel = isPresent(sidePanel);
|
|
8
9
|
const leftOpen = hasSidebar && (sidebarOpen ?? true);
|
|
9
10
|
const rightOpen = hasPanel && (sidePanelOpen ?? true);
|
|
10
|
-
const hasToolbar = toolbar
|
|
11
|
+
const hasToolbar = isPresent(toolbar);
|
|
11
12
|
const element = useRender({
|
|
12
13
|
render,
|
|
13
14
|
ref,
|
package/dist/workspace/model.js
CHANGED
|
@@ -188,6 +188,121 @@ function equalizeAxis(root, axis, length, gap) {
|
|
|
188
188
|
};
|
|
189
189
|
return distribute(root, length);
|
|
190
190
|
}
|
|
191
|
+
function addPane(layout, panes, pane, command) {
|
|
192
|
+
if (!pane || findSplitNode(layout.root, command.newPane.id) || findSplitNode(layout.root, command.splitId) || command.newPane.id === command.splitId) {
|
|
193
|
+
return layout;
|
|
194
|
+
}
|
|
195
|
+
const allViews = new Set(panes.flatMap((item) => item.views));
|
|
196
|
+
if (command.newPane.views.some((view) => allViews.has(view)) || new Set(command.newPane.views).size !== command.newPane.views.length) {
|
|
197
|
+
return layout;
|
|
198
|
+
}
|
|
199
|
+
const added = {
|
|
200
|
+
...command.newPane,
|
|
201
|
+
active: command.newPane.views.includes(command.newPane.active ?? '') ? command.newPane.active : (command.newPane.views[0] ?? null)
|
|
202
|
+
};
|
|
203
|
+
return finish({ ...layout, maximized: null }, replace(layout.root, pane.id, () => beside(pane, added, command.side, command.splitId)), added.id);
|
|
204
|
+
}
|
|
205
|
+
function swapPanes(layout, panes, command) {
|
|
206
|
+
const source = panes.find((item) => item.id === command.sourceId);
|
|
207
|
+
const target = panes.find((item) => item.id === command.targetId);
|
|
208
|
+
if (!source || !target || source === target || source.views.length === 0 || (command.viewId !== undefined && !source.views.includes(command.viewId))) {
|
|
209
|
+
return layout;
|
|
210
|
+
}
|
|
211
|
+
if (command.viewId === undefined || source.views.length === 1) {
|
|
212
|
+
const exchange = (node) => {
|
|
213
|
+
if (node.id === source.id) {
|
|
214
|
+
return target;
|
|
215
|
+
}
|
|
216
|
+
if (node.id === target.id) {
|
|
217
|
+
return source;
|
|
218
|
+
}
|
|
219
|
+
return node.type === 'pane' ? node : { ...node, children: node.children.map(exchange) };
|
|
220
|
+
};
|
|
221
|
+
return finish({ ...layout, maximized: null }, exchange(layout.root), source.id);
|
|
222
|
+
}
|
|
223
|
+
const moving = command.viewId;
|
|
224
|
+
const displaced = target.active;
|
|
225
|
+
const sourceNext = displaced === null
|
|
226
|
+
? without(source, [moving])
|
|
227
|
+
: {
|
|
228
|
+
...source,
|
|
229
|
+
views: source.views.map((view) => (view === moving ? displaced : view)),
|
|
230
|
+
active: source.active === moving ? displaced : source.active
|
|
231
|
+
};
|
|
232
|
+
const targetNext = {
|
|
233
|
+
...target,
|
|
234
|
+
views: displaced === null ? [moving] : target.views.map((view) => (view === displaced ? moving : view)),
|
|
235
|
+
active: moving
|
|
236
|
+
};
|
|
237
|
+
const root = replace(layout.root, source.id, () => sourceNext);
|
|
238
|
+
return finish({ ...layout, maximized: null }, replace(root, target.id, () => targetNext), target.id);
|
|
239
|
+
}
|
|
240
|
+
function moveViews(layout, panes, command) {
|
|
241
|
+
const source = panes.find((item) => item.id === command.sourceId);
|
|
242
|
+
const target = panes.find((item) => item.id === command.targetId);
|
|
243
|
+
if (!source || !target || (command.viewId !== undefined && !source.views.includes(command.viewId))) {
|
|
244
|
+
return layout;
|
|
245
|
+
}
|
|
246
|
+
const moving = command.viewId === undefined ? source.views : [command.viewId];
|
|
247
|
+
if (!moving.length || (source === target && (command.viewId === undefined || (command.side !== undefined && source.views.length === 1)))) {
|
|
248
|
+
return layout;
|
|
249
|
+
}
|
|
250
|
+
let added;
|
|
251
|
+
if (command.side) {
|
|
252
|
+
const id = command.viewId === undefined ? source.id : command.newPaneId;
|
|
253
|
+
if (!id ||
|
|
254
|
+
!command.splitId ||
|
|
255
|
+
id === command.splitId ||
|
|
256
|
+
findSplitNode(layout.root, command.splitId) ||
|
|
257
|
+
(findSplitNode(layout.root, id) && !(id === source.id && command.viewId === undefined))) {
|
|
258
|
+
return layout;
|
|
259
|
+
}
|
|
260
|
+
added = { type: 'pane', id, views: moving, active: command.viewId ?? source.active };
|
|
261
|
+
}
|
|
262
|
+
const remaining = without(source, moving);
|
|
263
|
+
const root = replace(layout.root, source.id, () => (remaining.views.length || source === target ? remaining : null));
|
|
264
|
+
if (!root) {
|
|
265
|
+
return layout;
|
|
266
|
+
}
|
|
267
|
+
const next = replace(root, target.id, (node) => {
|
|
268
|
+
if (node.type !== 'pane') {
|
|
269
|
+
return node;
|
|
270
|
+
}
|
|
271
|
+
if (added && command.side && command.splitId) {
|
|
272
|
+
return beside(node, added, command.side, command.splitId);
|
|
273
|
+
}
|
|
274
|
+
const gap = Math.max(0, Math.min(target.views.length, command.index ?? target.views.length));
|
|
275
|
+
const lifted = source === target ? target.views.slice(0, gap).filter((view) => moving.includes(view)).length : 0;
|
|
276
|
+
const views = [...node.views];
|
|
277
|
+
views.splice(gap - lifted, 0, ...moving);
|
|
278
|
+
return { ...node, views, active: command.viewId ?? source.active ?? views[0] ?? null };
|
|
279
|
+
});
|
|
280
|
+
return finish({ ...layout, maximized: null }, next, added?.id ?? target.id);
|
|
281
|
+
}
|
|
282
|
+
function resizeBranch(layout, command) {
|
|
283
|
+
const node = findSplitNode(layout.root, command.splitId);
|
|
284
|
+
if (!node || node.type !== 'split') {
|
|
285
|
+
return layout;
|
|
286
|
+
}
|
|
287
|
+
let sizes = [...node.sizes];
|
|
288
|
+
if (command.type === 'resize') {
|
|
289
|
+
if (command.sizes.length !== node.children.length || command.sizes.some((size) => !Number.isFinite(size) || size <= 0)) {
|
|
290
|
+
return layout;
|
|
291
|
+
}
|
|
292
|
+
sizes = shares(command.sizes, node.children.length);
|
|
293
|
+
}
|
|
294
|
+
else if (command.index === undefined) {
|
|
295
|
+
sizes = node.children.map(() => 1 / node.children.length);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const index = command.index;
|
|
299
|
+
if (index < 0 || index >= sizes.length - 1) {
|
|
300
|
+
return layout;
|
|
301
|
+
}
|
|
302
|
+
sizes[index] = sizes[index + 1] = (sizes[index] + sizes[index + 1]) / 2;
|
|
303
|
+
}
|
|
304
|
+
return finish(layout, replace(layout.root, node.id, () => ({ ...node, sizes })));
|
|
305
|
+
}
|
|
191
306
|
/** Invalid or stale commands are no-ops. IDs for new nodes come from the caller. */
|
|
192
307
|
export function updateSplitLayout(layout, command) {
|
|
193
308
|
const panes = splitPanes(layout.root);
|
|
@@ -225,104 +340,12 @@ export function updateSplitLayout(layout, command) {
|
|
|
225
340
|
views.splice(Math.max(0, Math.min(views.length, command.index ?? views.length)), 0, command.viewId);
|
|
226
341
|
return finish(layout, replace(layout.root, pane.id, () => ({ ...pane, views, active: command.viewId })), pane.id);
|
|
227
342
|
}
|
|
228
|
-
case 'split':
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
const allViews = new Set(panes.flatMap((item) => item.views));
|
|
236
|
-
if (command.newPane.views.some((view) => allViews.has(view)) || new Set(command.newPane.views).size !== command.newPane.views.length) {
|
|
237
|
-
return layout;
|
|
238
|
-
}
|
|
239
|
-
const added = {
|
|
240
|
-
...command.newPane,
|
|
241
|
-
active: command.newPane.views.includes(command.newPane.active ?? '') ? command.newPane.active : (command.newPane.views[0] ?? null)
|
|
242
|
-
};
|
|
243
|
-
return finish({ ...layout, maximized: null }, replace(layout.root, pane.id, () => beside(pane, added, command.side, command.splitId)), added.id);
|
|
244
|
-
}
|
|
245
|
-
case 'swap': {
|
|
246
|
-
const source = panes.find((item) => item.id === command.sourceId);
|
|
247
|
-
const target = panes.find((item) => item.id === command.targetId);
|
|
248
|
-
if (!source ||
|
|
249
|
-
!target ||
|
|
250
|
-
source === target ||
|
|
251
|
-
source.views.length === 0 ||
|
|
252
|
-
(command.viewId !== undefined && !source.views.includes(command.viewId))) {
|
|
253
|
-
return layout;
|
|
254
|
-
}
|
|
255
|
-
if (command.viewId === undefined || source.views.length === 1) {
|
|
256
|
-
const exchange = (node) => {
|
|
257
|
-
if (node.id === source.id) {
|
|
258
|
-
return target;
|
|
259
|
-
}
|
|
260
|
-
if (node.id === target.id) {
|
|
261
|
-
return source;
|
|
262
|
-
}
|
|
263
|
-
return node.type === 'pane' ? node : { ...node, children: node.children.map(exchange) };
|
|
264
|
-
};
|
|
265
|
-
return finish({ ...layout, maximized: null }, exchange(layout.root), source.id);
|
|
266
|
-
}
|
|
267
|
-
const moving = command.viewId;
|
|
268
|
-
const displaced = target.active;
|
|
269
|
-
const sourceNext = displaced === null
|
|
270
|
-
? without(source, [moving])
|
|
271
|
-
: {
|
|
272
|
-
...source,
|
|
273
|
-
views: source.views.map((view) => (view === moving ? displaced : view)),
|
|
274
|
-
active: source.active === moving ? displaced : source.active
|
|
275
|
-
};
|
|
276
|
-
const targetNext = {
|
|
277
|
-
...target,
|
|
278
|
-
views: displaced === null ? [moving] : target.views.map((view) => (view === displaced ? moving : view)),
|
|
279
|
-
active: moving
|
|
280
|
-
};
|
|
281
|
-
const root = replace(layout.root, source.id, () => sourceNext);
|
|
282
|
-
return finish({ ...layout, maximized: null }, replace(root, target.id, () => targetNext), target.id);
|
|
283
|
-
}
|
|
284
|
-
case 'move': {
|
|
285
|
-
const source = panes.find((item) => item.id === command.sourceId);
|
|
286
|
-
const target = panes.find((item) => item.id === command.targetId);
|
|
287
|
-
if (!source || !target || (command.viewId !== undefined && !source.views.includes(command.viewId))) {
|
|
288
|
-
return layout;
|
|
289
|
-
}
|
|
290
|
-
const moving = command.viewId === undefined ? source.views : [command.viewId];
|
|
291
|
-
if (!moving.length || (source === target && (command.viewId === undefined || (command.side !== undefined && source.views.length === 1)))) {
|
|
292
|
-
return layout;
|
|
293
|
-
}
|
|
294
|
-
let added;
|
|
295
|
-
if (command.side) {
|
|
296
|
-
const id = command.viewId === undefined ? source.id : command.newPaneId;
|
|
297
|
-
if (!id ||
|
|
298
|
-
!command.splitId ||
|
|
299
|
-
id === command.splitId ||
|
|
300
|
-
findSplitNode(layout.root, command.splitId) ||
|
|
301
|
-
(findSplitNode(layout.root, id) && !(id === source.id && command.viewId === undefined))) {
|
|
302
|
-
return layout;
|
|
303
|
-
}
|
|
304
|
-
added = { type: 'pane', id, views: moving, active: command.viewId ?? source.active };
|
|
305
|
-
}
|
|
306
|
-
const remaining = without(source, moving);
|
|
307
|
-
const root = replace(layout.root, source.id, () => (remaining.views.length || source === target ? remaining : null));
|
|
308
|
-
if (!root) {
|
|
309
|
-
return layout;
|
|
310
|
-
}
|
|
311
|
-
const next = replace(root, target.id, (node) => {
|
|
312
|
-
if (node.type !== 'pane') {
|
|
313
|
-
return node;
|
|
314
|
-
}
|
|
315
|
-
if (added && command.side && command.splitId) {
|
|
316
|
-
return beside(node, added, command.side, command.splitId);
|
|
317
|
-
}
|
|
318
|
-
const gap = Math.max(0, Math.min(target.views.length, command.index ?? target.views.length));
|
|
319
|
-
const lifted = source === target ? target.views.slice(0, gap).filter((view) => moving.includes(view)).length : 0;
|
|
320
|
-
const views = [...node.views];
|
|
321
|
-
views.splice(gap - lifted, 0, ...moving);
|
|
322
|
-
return { ...node, views, active: command.viewId ?? source.active ?? views[0] ?? null };
|
|
323
|
-
});
|
|
324
|
-
return finish({ ...layout, maximized: null }, next, added?.id ?? target.id);
|
|
325
|
-
}
|
|
343
|
+
case 'split':
|
|
344
|
+
return addPane(layout, panes, pane, command);
|
|
345
|
+
case 'swap':
|
|
346
|
+
return swapPanes(layout, panes, command);
|
|
347
|
+
case 'move':
|
|
348
|
+
return moveViews(layout, panes, command);
|
|
326
349
|
case 'equalizeAxis': {
|
|
327
350
|
const length = command.length ?? 1;
|
|
328
351
|
const gap = command.gap ?? 0;
|
|
@@ -332,29 +355,7 @@ export function updateSplitLayout(layout, command) {
|
|
|
332
355
|
return finish(layout, equalizeAxis(layout.root, command.axis, length, gap));
|
|
333
356
|
}
|
|
334
357
|
case 'resize':
|
|
335
|
-
case 'equalize':
|
|
336
|
-
|
|
337
|
-
if (!node || node.type !== 'split') {
|
|
338
|
-
return layout;
|
|
339
|
-
}
|
|
340
|
-
let sizes = [...node.sizes];
|
|
341
|
-
if (command.type === 'resize') {
|
|
342
|
-
if (command.sizes.length !== node.children.length || command.sizes.some((size) => !Number.isFinite(size) || size <= 0)) {
|
|
343
|
-
return layout;
|
|
344
|
-
}
|
|
345
|
-
sizes = shares(command.sizes, node.children.length);
|
|
346
|
-
}
|
|
347
|
-
else if (command.index === undefined) {
|
|
348
|
-
sizes = node.children.map(() => 1 / node.children.length);
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
const index = command.index;
|
|
352
|
-
if (index < 0 || index >= sizes.length - 1) {
|
|
353
|
-
return layout;
|
|
354
|
-
}
|
|
355
|
-
sizes[index] = sizes[index + 1] = (sizes[index] + sizes[index + 1]) / 2;
|
|
356
|
-
}
|
|
357
|
-
return finish(layout, replace(layout.root, node.id, () => ({ ...node, sizes })));
|
|
358
|
-
}
|
|
358
|
+
case 'equalize':
|
|
359
|
+
return resizeBranch(layout, command);
|
|
359
360
|
}
|
|
360
361
|
}
|
package/package.json
CHANGED
package/src/DisabledReason.tsx
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { ReactElement } from 'react';
|
|
2
2
|
import { Tooltip } from './Tooltip.tsx';
|
|
3
3
|
|
|
4
|
-
/*
|
|
5
|
-
* Why a row cannot be picked, in a tooltip beside it rather than in the row itself. A reason is a
|
|
6
|
-
* sentence, and a sentence in a menu row sets the width of the whole menu. A row with nothing in its
|
|
7
|
-
* way is handed through untouched, so it carries no tooltip at all.
|
|
8
|
-
*/
|
|
9
4
|
export interface DisabledReasonProps {
|
|
10
5
|
reason: string | null;
|
|
11
6
|
children: ReactElement<Record<string, unknown>>;
|
|
12
7
|
}
|
|
13
8
|
|
|
9
|
+
/*
|
|
10
|
+
* Why a row cannot be picked, in a tooltip beside it rather than in the row itself: a sentence in a menu
|
|
11
|
+
* row sets the width of the whole menu. A row with nothing in its way is handed through untouched.
|
|
12
|
+
*/
|
|
14
13
|
export function DisabledReason({ reason, children }: DisabledReasonProps) {
|
|
15
14
|
if (reason === null) {
|
|
16
15
|
return children;
|
package/src/Field.tsx
CHANGED
|
@@ -9,6 +9,8 @@ const ERROR = 'text-xs text-status-error';
|
|
|
9
9
|
// Beside the control the label reads at the size of what is typed, on the middle of the control's first line however tall it grows.
|
|
10
10
|
const LABEL_BESIDE = 'flex h-8 items-center self-start text-sm text-text-muted';
|
|
11
11
|
|
|
12
|
+
const isShown = (line: ReactNode): boolean => line !== undefined && line !== null && line !== '';
|
|
13
|
+
|
|
12
14
|
export type FieldHintProps = useRender.ComponentProps<'p'>;
|
|
13
15
|
|
|
14
16
|
/* The line under a field that says what goes in it. */
|
|
@@ -80,8 +82,8 @@ export function Field({ label, hint, error, group = false, orientation = 'vertic
|
|
|
80
82
|
const horizontal = orientation === 'horizontal';
|
|
81
83
|
const id = useId();
|
|
82
84
|
const hasLabel = label !== undefined;
|
|
83
|
-
const hasHint = hint
|
|
84
|
-
const hasError = error
|
|
85
|
+
const hasHint = isShown(hint);
|
|
86
|
+
const hasError = isShown(error);
|
|
85
87
|
const describedBy = [hasHint ? `${id}-hint` : null, hasError ? `${id}-error` : null].filter((part) => part !== null).join(' ') || undefined;
|
|
86
88
|
return (
|
|
87
89
|
<div
|
package/src/IconButton.tsx
CHANGED
|
@@ -6,7 +6,6 @@ import { Icon } from './Icon.tsx';
|
|
|
6
6
|
import type { Shortcut } from './shortcut.ts';
|
|
7
7
|
import { Spinner } from './Spinner.tsx';
|
|
8
8
|
import { Tooltip, type TooltipSide } from './Tooltip.tsx';
|
|
9
|
-
|
|
10
9
|
import { ICON_BUTTON_CLASS, ICON_BUTTON_ICON_SIZE, type IconButtonSize } from './icon-button-size.ts';
|
|
11
10
|
|
|
12
11
|
export type { IconButtonSize };
|