@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/src/popover/parts.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import type { ComponentProps } from 'react';
|
|
|
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.ts';
|
|
5
|
+
import { splitPlacement, type PlacementKey } from '../placement.ts';
|
|
5
6
|
import { renderPositioner } from '../popup-layer.ts';
|
|
6
7
|
|
|
7
8
|
/*
|
|
@@ -16,10 +17,7 @@ const POPUP_VARIANT = {
|
|
|
16
17
|
|
|
17
18
|
export type PopupVariant = keyof typeof POPUP_VARIANT;
|
|
18
19
|
|
|
19
|
-
type Placement = Pick<
|
|
20
|
-
ComponentProps<typeof BasePopover.Positioner>,
|
|
21
|
-
'side' | 'align' | 'sideOffset' | 'alignOffset' | 'collisionPadding' | 'collisionBoundary' | 'collisionAvoidance' | 'anchor' | 'sticky' | 'positionMethod'
|
|
22
|
-
>;
|
|
20
|
+
type Placement = Pick<ComponentProps<typeof BasePopover.Positioner>, PlacementKey>;
|
|
23
21
|
|
|
24
22
|
export const PopoverRoot = BasePopover.Root;
|
|
25
23
|
|
|
@@ -37,38 +35,12 @@ export type PopoverPopupProps = ComponentProps<typeof BasePopover.Popup> &
|
|
|
37
35
|
};
|
|
38
36
|
|
|
39
37
|
/* The portal, the positioner and the popup of a popover: under its trigger and lined up with its start unless it says otherwise. */
|
|
40
|
-
export function PopoverPopup({
|
|
41
|
-
|
|
42
|
-
side = 'bottom',
|
|
43
|
-
align = 'start',
|
|
44
|
-
sideOffset = 8,
|
|
45
|
-
alignOffset,
|
|
46
|
-
collisionPadding,
|
|
47
|
-
collisionBoundary,
|
|
48
|
-
collisionAvoidance,
|
|
49
|
-
anchor,
|
|
50
|
-
sticky,
|
|
51
|
-
positionMethod,
|
|
52
|
-
className,
|
|
53
|
-
...props
|
|
54
|
-
}: PopoverPopupProps) {
|
|
38
|
+
export function PopoverPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8, className, ...props }: PopoverPopupProps) {
|
|
39
|
+
const [placement, popupProps] = splitPlacement(props);
|
|
55
40
|
return (
|
|
56
41
|
<BasePopover.Portal>
|
|
57
|
-
<BasePopover.Positioner
|
|
58
|
-
className=
|
|
59
|
-
render={renderPositioner}
|
|
60
|
-
side={side}
|
|
61
|
-
align={align}
|
|
62
|
-
sideOffset={sideOffset}
|
|
63
|
-
alignOffset={alignOffset}
|
|
64
|
-
collisionPadding={collisionPadding}
|
|
65
|
-
collisionBoundary={collisionBoundary}
|
|
66
|
-
collisionAvoidance={collisionAvoidance}
|
|
67
|
-
anchor={anchor}
|
|
68
|
-
sticky={sticky}
|
|
69
|
-
positionMethod={positionMethod}
|
|
70
|
-
>
|
|
71
|
-
<BasePopover.Popup className={withClass(POPUP_VARIANT[variant], className)} {...props} />
|
|
42
|
+
<BasePopover.Positioner className="popup-positioner" render={renderPositioner} {...placement} side={side} align={align} sideOffset={sideOffset}>
|
|
43
|
+
<BasePopover.Popup className={withClass(POPUP_VARIANT[variant], className)} {...popupProps} />
|
|
72
44
|
</BasePopover.Positioner>
|
|
73
45
|
</BasePopover.Portal>
|
|
74
46
|
);
|
|
@@ -84,37 +56,12 @@ export type PreviewCardPopupProps = ComponentProps<typeof BasePreviewCard.Popup>
|
|
|
84
56
|
};
|
|
85
57
|
|
|
86
58
|
/* A card that opens while the pointer rests on its trigger, drawn like a popover. */
|
|
87
|
-
export function PreviewCardPopup({
|
|
88
|
-
|
|
89
|
-
side = 'bottom',
|
|
90
|
-
align = 'start',
|
|
91
|
-
sideOffset = 8,
|
|
92
|
-
alignOffset,
|
|
93
|
-
collisionPadding,
|
|
94
|
-
collisionBoundary,
|
|
95
|
-
collisionAvoidance,
|
|
96
|
-
anchor,
|
|
97
|
-
sticky,
|
|
98
|
-
positionMethod,
|
|
99
|
-
className,
|
|
100
|
-
...props
|
|
101
|
-
}: PreviewCardPopupProps) {
|
|
59
|
+
export function PreviewCardPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8, className, ...props }: PreviewCardPopupProps) {
|
|
60
|
+
const [placement, popupProps] = splitPlacement(props);
|
|
102
61
|
return (
|
|
103
62
|
<BasePreviewCard.Portal>
|
|
104
|
-
<BasePreviewCard.Positioner
|
|
105
|
-
className=
|
|
106
|
-
side={side}
|
|
107
|
-
align={align}
|
|
108
|
-
sideOffset={sideOffset}
|
|
109
|
-
alignOffset={alignOffset}
|
|
110
|
-
collisionPadding={collisionPadding}
|
|
111
|
-
collisionBoundary={collisionBoundary}
|
|
112
|
-
collisionAvoidance={collisionAvoidance}
|
|
113
|
-
anchor={anchor}
|
|
114
|
-
sticky={sticky}
|
|
115
|
-
positionMethod={positionMethod}
|
|
116
|
-
>
|
|
117
|
-
<BasePreviewCard.Popup className={withClass(POPUP_VARIANT[variant], className)} {...props} />
|
|
63
|
+
<BasePreviewCard.Positioner className="z-(--z-popup)" {...placement} side={side} align={align} sideOffset={sideOffset}>
|
|
64
|
+
<BasePreviewCard.Popup className={withClass(POPUP_VARIANT[variant], className)} {...popupProps} />
|
|
118
65
|
</BasePreviewCard.Positioner>
|
|
119
66
|
</BasePreviewCard.Portal>
|
|
120
67
|
);
|
|
@@ -277,7 +277,7 @@ export function SettingsDialog({
|
|
|
277
277
|
</div>
|
|
278
278
|
</div>
|
|
279
279
|
{/* The header sits outside the panels: inside one it remounts on every section
|
|
280
|
-
|
|
280
|
+
change, which throws the keyboard's focus away mid-arrow-key. */}
|
|
281
281
|
<div className="relative flex min-h-0 min-w-0 grow flex-col">
|
|
282
282
|
<div
|
|
283
283
|
className={clsx('flex min-w-0 items-start gap-4 px-8 pt-5.5 pb-4.5 max-[960px]:px-4', floating && 'absolute inset-x-0 top-0 z-10')}
|
package/src/shortcut.ts
CHANGED
|
@@ -37,7 +37,13 @@ const NAMED_KEYS = new Set(['Enter', 'Escape', 'Backspace', 'Delete', 'Tab', 'Sp
|
|
|
37
37
|
|
|
38
38
|
const FUNCTION_KEY = /^F([1-9]|1[0-2])$/;
|
|
39
39
|
|
|
40
|
-
const
|
|
40
|
+
const MODIFIER_FIELDS: Readonly<Record<string, 'mod' | 'ctrl' | 'meta' | 'alt' | 'shift'>> = {
|
|
41
|
+
Mod: 'mod',
|
|
42
|
+
Ctrl: 'ctrl',
|
|
43
|
+
Meta: 'meta',
|
|
44
|
+
Alt: 'alt',
|
|
45
|
+
Shift: 'shift'
|
|
46
|
+
};
|
|
41
47
|
|
|
42
48
|
const isKey = (word: string): boolean =>
|
|
43
49
|
/^[A-Za-z0-9]$/.test(word) || word in PUNCTUATION_CODES || CHARACTER_KEYS.has(word) || NAMED_KEYS.has(word) || FUNCTION_KEY.test(word);
|
|
@@ -51,9 +57,8 @@ export const shortcut = (text: string): Shortcut => {
|
|
|
51
57
|
const words = text === '+' ? ['+'] : text.endsWith('++') ? [...text.slice(0, -2).split('+'), '+'] : text.split('+');
|
|
52
58
|
const result: Shortcut = { mod: false, ctrl: false, meta: false, alt: false, shift: false, key: '' };
|
|
53
59
|
for (const word of words) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
result[modifier === 'Mod' ? 'mod' : modifier === 'Ctrl' ? 'ctrl' : modifier === 'Meta' ? 'meta' : modifier === 'Alt' ? 'alt' : 'shift'] = true;
|
|
60
|
+
if (Object.hasOwn(MODIFIER_FIELDS, word)) {
|
|
61
|
+
result[MODIFIER_FIELDS[word]!] = true;
|
|
57
62
|
continue;
|
|
58
63
|
}
|
|
59
64
|
if (!isKey(word)) {
|
package/src/tabs/parts.tsx
CHANGED
|
@@ -124,6 +124,12 @@ export function TabsList({ className, children, start, end, ...props }: TabsList
|
|
|
124
124
|
|
|
125
125
|
const visible = shown === null ? tabs : shown.map((index) => tabs[index]!);
|
|
126
126
|
const hidden = shown === null ? [] : tabs.filter((_, index) => !shown.includes(index));
|
|
127
|
+
const more = (
|
|
128
|
+
<>
|
|
129
|
+
{t('tabs.more')}
|
|
130
|
+
<Icon icon={ChevronDown} size={14} />
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
127
133
|
|
|
128
134
|
return (
|
|
129
135
|
<div ref={strip} className={clsx('relative flex items-center gap-4 border-b border-border', className)}>
|
|
@@ -137,10 +143,7 @@ export function TabsList({ className, children, start, end, ...props }: TabsList
|
|
|
137
143
|
</BaseTabs.List>
|
|
138
144
|
{hidden.length > 0 && (
|
|
139
145
|
<MenuRoot>
|
|
140
|
-
<MenuTrigger className={MORE}>
|
|
141
|
-
{t('tabs.more')}
|
|
142
|
-
<Icon icon={ChevronDown} size={14} />
|
|
143
|
-
</MenuTrigger>
|
|
146
|
+
<MenuTrigger className={MORE}>{more}</MenuTrigger>
|
|
144
147
|
<MenuPopup align="end" className="min-w-48">
|
|
145
148
|
<InMenuContext value>
|
|
146
149
|
{hidden.map((tab) => (
|
|
@@ -164,10 +167,7 @@ export function TabsList({ className, children, start, end, ...props }: TabsList
|
|
|
164
167
|
{tab.props.onClose !== undefined && <span className="size-5 shrink-0" />}
|
|
165
168
|
</span>
|
|
166
169
|
))}
|
|
167
|
-
<span className={MORE}>
|
|
168
|
-
{t('tabs.more')}
|
|
169
|
-
<Icon icon={ChevronDown} size={14} />
|
|
170
|
-
</span>
|
|
170
|
+
<span className={MORE}>{more}</span>
|
|
171
171
|
</div>
|
|
172
172
|
</div>
|
|
173
173
|
);
|
package/src/tree/parts.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, type MouseEvent
|
|
1
|
+
import { useCallback, type MouseEvent } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useRender } from '@base-ui-components/react/use-render';
|
|
4
4
|
import { ChevronRight } from 'lucide-react';
|
|
@@ -9,6 +9,14 @@ import { mergeRefs } from '../merge-refs.ts';
|
|
|
9
9
|
import { treeRowStyle } from './style.ts';
|
|
10
10
|
import { TREE_ROWS, useTreeShift } from './use-tree-shift.tsx';
|
|
11
11
|
|
|
12
|
+
/* A handler that keeps the event to the control, so the row under it never sees the press. */
|
|
13
|
+
const stopped =
|
|
14
|
+
<Event extends { stopPropagation(): void }>(handler?: (event: Event) => void) =>
|
|
15
|
+
(event: Event): void => {
|
|
16
|
+
event.stopPropagation();
|
|
17
|
+
handler?.(event);
|
|
18
|
+
};
|
|
19
|
+
|
|
12
20
|
export type TreeRootProps = useRender.ComponentProps<'div'> & {
|
|
13
21
|
/* What a row wider than the tree does: cut its name off at the end, or keep the row whole and let every row slide sideways together, as a file tree does. */
|
|
14
22
|
overflow?: 'truncate' | 'scroll';
|
|
@@ -127,10 +135,7 @@ export function TreeChevron({ expanded, onExpandedChange, render, className, ref
|
|
|
127
135
|
onExpandedChange?.(!expanded);
|
|
128
136
|
}
|
|
129
137
|
},
|
|
130
|
-
onDoubleClick: (
|
|
131
|
-
event.stopPropagation();
|
|
132
|
-
onDoubleClick?.(event);
|
|
133
|
-
},
|
|
138
|
+
onDoubleClick: stopped(onDoubleClick),
|
|
134
139
|
children: <Icon icon={ChevronRight} size={12} className={expanded ? 'rotate-90' : undefined} />
|
|
135
140
|
}
|
|
136
141
|
});
|
|
@@ -156,22 +161,10 @@ export function TreeControl({ render, className, ref, onClick, onDoubleClick, on
|
|
|
156
161
|
...props,
|
|
157
162
|
'data-tree-control': true,
|
|
158
163
|
className: clsx('adecore-tree-control', className),
|
|
159
|
-
onClick: (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
onDoubleClick: (event: MouseEvent<HTMLSpanElement>) => {
|
|
164
|
-
event.stopPropagation();
|
|
165
|
-
onDoubleClick?.(event);
|
|
166
|
-
},
|
|
167
|
-
onKeyDown: (event: KeyboardEvent<HTMLSpanElement>) => {
|
|
168
|
-
event.stopPropagation();
|
|
169
|
-
onKeyDown?.(event);
|
|
170
|
-
},
|
|
171
|
-
onPointerDown: (event: PointerEvent<HTMLSpanElement>) => {
|
|
172
|
-
event.stopPropagation();
|
|
173
|
-
onPointerDown?.(event);
|
|
174
|
-
}
|
|
164
|
+
onClick: stopped(onClick),
|
|
165
|
+
onDoubleClick: stopped(onDoubleClick),
|
|
166
|
+
onKeyDown: stopped(onKeyDown),
|
|
167
|
+
onPointerDown: stopped(onPointerDown)
|
|
175
168
|
}
|
|
176
169
|
});
|
|
177
170
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState, type ReactElement } from 'react';
|
|
2
|
-
import { clampShift, maxShift, SHIFT_PROPERTY, shiftNeed, shiftThumb, sidewaysDelta } from '
|
|
2
|
+
import { clampShift, maxShift, SHIFT_PROPERTY, shiftNeed, shiftThumb, sidewaysDelta } from './shift.ts';
|
|
3
3
|
|
|
4
4
|
const HIDE_AFTER = 900;
|
|
5
5
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useState,
|
|
9
9
|
type CSSProperties,
|
|
10
10
|
type DragEvent,
|
|
11
|
+
type FocusEvent,
|
|
11
12
|
type KeyboardEvent,
|
|
12
13
|
type ReactNode
|
|
13
14
|
} from 'react';
|
|
@@ -90,6 +91,12 @@ interface Measurement {
|
|
|
90
91
|
const ZERO: Measurement = { width: 0, height: 0, left: 0, top: 0, scaleX: 1, scaleY: 1 };
|
|
91
92
|
const MINIMUM: SplitMinimum = { width: 120, height: 80 };
|
|
92
93
|
|
|
94
|
+
/* The pane's section or header under `container`, found by its data attribute rather than a selector, so no id needs escaping. */
|
|
95
|
+
function paneElement(container: ParentNode | null | undefined, attribute: 'splitPane' | 'splitHeader', paneId: string): HTMLElement | undefined {
|
|
96
|
+
const selector = attribute === 'splitPane' ? '[data-split-pane]' : '[data-split-header]';
|
|
97
|
+
return Array.from(container?.querySelectorAll<HTMLElement>(selector) ?? []).find((element) => element.dataset[attribute] === paneId);
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
function Divider({
|
|
94
101
|
divider,
|
|
95
102
|
minimumResizeShare,
|
|
@@ -227,7 +234,7 @@ export function SplitView({
|
|
|
227
234
|
(!previous.isConnected || previous.closest('[hidden]')) &&
|
|
228
235
|
(!active || active === element.ownerDocument.body || active === previous)
|
|
229
236
|
) {
|
|
230
|
-
const pane =
|
|
237
|
+
const pane = paneElement(element, 'splitPane', value.focused);
|
|
231
238
|
const target = pane?.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]') ?? pane;
|
|
232
239
|
target?.focus({ preventScroll: true });
|
|
233
240
|
}
|
|
@@ -312,6 +319,14 @@ export function SplitView({
|
|
|
312
319
|
.sort((left, right) => (left.viewId < right.viewId ? -1 : left.viewId > right.viewId ? 1 : 0));
|
|
313
320
|
const visible = new Map(geometry.panes.map((pane) => [pane.pane.id, pane]));
|
|
314
321
|
const radius = (rect: SplitRect): string => paneRadius(rect, measurement.width, measurement.height, edges, layout === 'roomy' ? 8 : 0);
|
|
322
|
+
// A header squares off the top corners of the body under it.
|
|
323
|
+
const bodyRadius = (rect: SplitRect, header: number): string => {
|
|
324
|
+
const corners = radius(rect).split(' ');
|
|
325
|
+
if (header > 0) {
|
|
326
|
+
corners[0] = corners[1] = '0px';
|
|
327
|
+
}
|
|
328
|
+
return corners.join(' ');
|
|
329
|
+
};
|
|
315
330
|
const bounds: SplitViewBounds[] = panes.flatMap((pane) => {
|
|
316
331
|
const rect = visible.get(pane.id);
|
|
317
332
|
const header = Math.min(barHeight, rect?.height ?? 0);
|
|
@@ -323,11 +338,8 @@ export function SplitView({
|
|
|
323
338
|
height: Math.max(0, rect.height - header) * measurement.scaleY
|
|
324
339
|
}
|
|
325
340
|
: { x: 0, y: 0, width: 0, height: 0 };
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
corners[0] = corners[1] = '0px';
|
|
329
|
-
}
|
|
330
|
-
return pane.views.map((viewId) => ({ viewId, paneId: pane.id, active: !!rect && pane.active === viewId, rect: body, borderRadius: corners.join(' ') }));
|
|
341
|
+
const borderRadius = rect ? bodyRadius(rect, header) : '0px 0px 0px 0px';
|
|
342
|
+
return pane.views.map((viewId) => ({ viewId, paneId: pane.id, active: !!rect && pane.active === viewId, rect: body, borderRadius }));
|
|
331
343
|
});
|
|
332
344
|
const boundsKey = JSON.stringify(bounds);
|
|
333
345
|
const reportedBounds = useRef('');
|
|
@@ -379,9 +391,7 @@ export function SplitView({
|
|
|
379
391
|
return null;
|
|
380
392
|
}
|
|
381
393
|
const header = Math.min(barHeight, rect.height);
|
|
382
|
-
const strip =
|
|
383
|
-
(element) => element.dataset.splitHeader === rect.pane.id
|
|
384
|
-
);
|
|
394
|
+
const strip = paneElement(root.current, 'splitHeader', rect.pane.id);
|
|
385
395
|
const tabRects = Array.from(strip?.querySelectorAll('[data-document-tab]') ?? [], (element) => element.getBoundingClientRect());
|
|
386
396
|
const inHeader = y < rect.y + header;
|
|
387
397
|
const side = inHeader ? undefined : (splitSideAt(rect, x, y) ?? 'center');
|
|
@@ -446,9 +456,7 @@ export function SplitView({
|
|
|
446
456
|
const next = options[0];
|
|
447
457
|
if (next) {
|
|
448
458
|
dispatch({ type: 'focus', paneId: next.pane.id });
|
|
449
|
-
const element =
|
|
450
|
-
(pane) => pane.dataset.splitPane === next.pane.id
|
|
451
|
-
);
|
|
459
|
+
const element = paneElement(root.current, 'splitPane', next.pane.id);
|
|
452
460
|
element?.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]')?.focus();
|
|
453
461
|
if (!showTabs) {
|
|
454
462
|
element?.focus();
|
|
@@ -465,7 +473,7 @@ export function SplitView({
|
|
|
465
473
|
style,
|
|
466
474
|
'data-layout': layout,
|
|
467
475
|
'data-dragging': dragging || undefined,
|
|
468
|
-
onFocusCapture: (event:
|
|
476
|
+
onFocusCapture: (event: FocusEvent<HTMLDivElement>) => {
|
|
469
477
|
props.onFocusCapture?.(event);
|
|
470
478
|
lastFocus.current = event.target;
|
|
471
479
|
},
|
|
@@ -591,17 +599,13 @@ export function SplitView({
|
|
|
591
599
|
const rect = visible.get(pane.id);
|
|
592
600
|
const active = !!rect && pane.active === viewId;
|
|
593
601
|
const header = Math.min(barHeight, rect?.height ?? 0);
|
|
594
|
-
const corners = rect ? radius(rect).split(' ') : [];
|
|
595
|
-
if (header > 0) {
|
|
596
|
-
corners[0] = corners[1] = '0px';
|
|
597
|
-
}
|
|
598
602
|
const position: CSSProperties = rect
|
|
599
603
|
? {
|
|
600
604
|
left: rect.x,
|
|
601
605
|
top: rect.y + header,
|
|
602
606
|
width: rect.width,
|
|
603
607
|
height: Math.max(0, rect.height - header),
|
|
604
|
-
borderRadius:
|
|
608
|
+
borderRadius: bodyRadius(rect, header)
|
|
605
609
|
}
|
|
606
610
|
: {};
|
|
607
611
|
return (
|
|
@@ -637,7 +641,7 @@ export function SplitView({
|
|
|
637
641
|
type: 'equalizeAxis',
|
|
638
642
|
axis: divider.axis,
|
|
639
643
|
length: divider.axis === 'horizontal' ? measurement.width : measurement.height,
|
|
640
|
-
gap
|
|
644
|
+
gap
|
|
641
645
|
}
|
|
642
646
|
: { type: 'equalize', splitId: divider.splitId, index: divider.index }
|
|
643
647
|
)
|
|
@@ -21,6 +21,22 @@ import { IconButton, type IconButtonProps } from '../IconButton.tsx';
|
|
|
21
21
|
import { Tooltip } from '../Tooltip.tsx';
|
|
22
22
|
import { tabGapAt } from './geometry.ts';
|
|
23
23
|
|
|
24
|
+
/* Where an arrow, Home or End moves the selection from `index`, wrapping at either end; null for any other key. */
|
|
25
|
+
function tabIndexForKey(key: string, index: number, count: number): number | null {
|
|
26
|
+
switch (key) {
|
|
27
|
+
case 'ArrowRight':
|
|
28
|
+
return (index + 1) % count;
|
|
29
|
+
case 'ArrowLeft':
|
|
30
|
+
return (index + count - 1) % count;
|
|
31
|
+
case 'Home':
|
|
32
|
+
return 0;
|
|
33
|
+
case 'End':
|
|
34
|
+
return count - 1;
|
|
35
|
+
default:
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
export interface TabStripItem {
|
|
25
41
|
id: string;
|
|
26
42
|
label: string;
|
|
@@ -213,6 +229,7 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
|
|
|
213
229
|
const { items, value, onValueChange } = context;
|
|
214
230
|
const index = items.findIndex((candidate) => candidate.id === item.id);
|
|
215
231
|
const close = (): void => context.close(item.id, onClose);
|
|
232
|
+
const closable = item.closable !== false && (onClose ?? context.onClose) !== undefined;
|
|
216
233
|
return useRender({
|
|
217
234
|
render,
|
|
218
235
|
ref,
|
|
@@ -247,21 +264,12 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
|
|
|
247
264
|
}}
|
|
248
265
|
onDragEnd={onDragEnd ?? context.onDragEnd}
|
|
249
266
|
onKeyDown={(event) => {
|
|
250
|
-
const next =
|
|
251
|
-
event.key === 'ArrowRight'
|
|
252
|
-
? (index + 1) % items.length
|
|
253
|
-
: event.key === 'ArrowLeft'
|
|
254
|
-
? (index + items.length - 1) % items.length
|
|
255
|
-
: event.key === 'Home'
|
|
256
|
-
? 0
|
|
257
|
-
: event.key === 'End'
|
|
258
|
-
? items.length - 1
|
|
259
|
-
: null;
|
|
267
|
+
const next = tabIndexForKey(event.key, index, items.length);
|
|
260
268
|
if (next !== null) {
|
|
261
269
|
event.preventDefault();
|
|
262
270
|
onValueChange(items[next]!.id);
|
|
263
271
|
context.focus(next);
|
|
264
|
-
} else if (event.key === 'Delete' &&
|
|
272
|
+
} else if (event.key === 'Delete' && closable) {
|
|
265
273
|
event.preventDefault();
|
|
266
274
|
close();
|
|
267
275
|
}
|
|
@@ -279,7 +287,7 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
|
|
|
279
287
|
</button>
|
|
280
288
|
</Tooltip>
|
|
281
289
|
{item.pinned && <Icon icon={Pin} size={12} className="shrink-0 text-text-muted" />}
|
|
282
|
-
{
|
|
290
|
+
{closable && (
|
|
283
291
|
<IconButton
|
|
284
292
|
icon={X}
|
|
285
293
|
size="2xs"
|
|
@@ -15,6 +15,8 @@ export interface WorkspaceProps extends useRender.ComponentProps<'div'> {
|
|
|
15
15
|
sidePanelOpen?: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const isPresent = (node: ReactNode): boolean => node !== undefined && node !== null && node !== false;
|
|
19
|
+
|
|
18
20
|
export function Workspace({
|
|
19
21
|
layout = 'standard',
|
|
20
22
|
toolbar,
|
|
@@ -31,11 +33,11 @@ export function Workspace({
|
|
|
31
33
|
style,
|
|
32
34
|
...props
|
|
33
35
|
}: WorkspaceProps) {
|
|
34
|
-
const hasSidebar = sidebar
|
|
35
|
-
const hasPanel = sidePanel
|
|
36
|
+
const hasSidebar = isPresent(sidebar);
|
|
37
|
+
const hasPanel = isPresent(sidePanel);
|
|
36
38
|
const leftOpen = hasSidebar && (sidebarOpen ?? true);
|
|
37
39
|
const rightOpen = hasPanel && (sidePanelOpen ?? true);
|
|
38
|
-
const hasToolbar = toolbar
|
|
40
|
+
const hasToolbar = isPresent(toolbar);
|
|
39
41
|
const element = useRender({
|
|
40
42
|
render,
|
|
41
43
|
ref,
|