@adecore/ui 0.38.0 → 0.40.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.
- package/dist/AccentSwatches.js +2 -3
- package/dist/Banner.js +1 -6
- package/dist/ChoiceCards.js +0 -5
- package/dist/ColorSwatch.js +1 -4
- package/dist/ColorWheelPicker.js +3 -6
- package/dist/DockShell.js +3 -5
- package/dist/EmptyState.js +0 -1
- package/dist/ErrorBoundary.js +1 -5
- package/dist/Field.js +2 -6
- package/dist/FileIcon.js +2 -4
- package/dist/Icon.js +1 -2
- package/dist/IconButton.js +1 -1
- package/dist/IconPicker.js +3 -7
- package/dist/Input.js +1 -2
- package/dist/Kbd.js +1 -3
- package/dist/LazyBoundary.js +2 -3
- package/dist/Meter.js +0 -1
- package/dist/MiddleTruncate.js +2 -6
- package/dist/PanelEmpty.js +0 -1
- package/dist/PanelHeader.js +1 -4
- package/dist/Pill.js +1 -6
- package/dist/ProjectSwitcher.js +1 -1
- package/dist/PromptDialog.js +5 -8
- package/dist/RelativeTime.js +1 -1
- package/dist/SegmentBar.js +1 -2
- package/dist/Segmented.js +2 -5
- package/dist/Select.js +1 -4
- package/dist/ShortcutHints.js +2 -3
- package/dist/Slider.js +0 -1
- package/dist/SlidingColumn.js +2 -4
- package/dist/SplitButton.js +1 -5
- package/dist/TextMenu.js +1 -5
- package/dist/Tile.js +0 -4
- package/dist/Toasts.js +1 -9
- package/dist/Tooltip.js +1 -1
- package/dist/UIProvider.js +0 -5
- package/dist/Waveform.js +4 -2
- package/dist/Wipe.js +1 -6
- package/dist/ZoomControls.js +0 -1
- package/dist/clipboard.js +2 -5
- package/dist/combobox/parts.js +3 -9
- package/dist/command-palette/parts.js +5 -14
- package/dist/dialog/parts.js +5 -10
- package/dist/dialog-layer.js +2 -3
- package/dist/error-boundary.js +4 -20
- package/dist/file-icon.js +4 -9
- package/dist/file-tree/empty-lines.js +2 -8
- package/dist/file-tree/use-empty-lines.js +1 -5
- package/dist/file-tree/use-slots.js +6 -10
- package/dist/format/datetime.js +32 -29
- package/dist/format/duration.js +2 -10
- package/dist/format/locale.js +19 -39
- package/dist/format/number.js +9 -22
- package/dist/format/regions.js +4 -7
- package/dist/icon-picker.js +5 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/key-value-list/parts.js +1 -5
- package/dist/lazy.js +4 -19
- package/dist/locales.js +2 -6
- package/dist/menu/parts.js +61 -133
- package/dist/middle-truncate.js +2 -3
- package/dist/placement.js +0 -1
- package/dist/popover/parts.js +1 -5
- package/dist/popup-layer.js +2 -4
- package/dist/prefetch.js +2 -5
- package/dist/selection.js +1 -5
- package/dist/settings/MasterDetail.js +1 -7
- package/dist/settings/SettingsDialog.js +1 -2
- package/dist/settings/SettingsRow.js +2 -5
- package/dist/settings/SettingsSection.js +1 -1
- package/dist/shortcut-hints.js +1 -5
- package/dist/shortcut.js +1 -2
- package/dist/tabs/overflow.js +1 -6
- package/dist/tabs/parts.js +69 -95
- package/dist/testing/dedupe.js +5 -7
- package/dist/tint.d.ts +2 -0
- package/dist/tint.js +11 -0
- package/dist/toast-store.js +2 -10
- package/dist/tree/use-tree-shift.js +2 -6
- package/dist/useColumnResize.js +9 -10
- package/dist/useNow.js +2 -9
- package/dist/workspace/DropPreview.js +1 -5
- package/dist/workspace/preview.js +4 -9
- package/package.json +9 -9
- package/src/AccentSwatches.tsx +9 -3
- package/src/Banner.tsx +5 -11
- package/src/Button.tsx +4 -0
- package/src/Checkbox.tsx +3 -0
- package/src/ChoiceCards.tsx +12 -11
- package/src/CloseButton.tsx +1 -0
- package/src/ColorSwatch.tsx +7 -8
- package/src/ColorWheelPicker.tsx +28 -12
- package/src/ColumnResizeHandle.tsx +1 -0
- package/src/CopyButton.tsx +6 -0
- package/src/DisabledReason.tsx +1 -0
- package/src/DockShell.tsx +11 -10
- package/src/EmptyState.tsx +10 -6
- package/src/ErrorBoundary.tsx +13 -10
- package/src/Field.tsx +11 -13
- package/src/FileIcon.tsx +4 -4
- package/src/Icon.tsx +2 -4
- package/src/IconButton.tsx +14 -5
- package/src/IconPicker.tsx +30 -15
- package/src/Input.tsx +11 -9
- package/src/Kbd.tsx +8 -7
- package/src/LazyBoundary.tsx +3 -3
- package/src/Meter.tsx +7 -2
- package/src/MiddleTruncate.tsx +15 -6
- package/src/PanelEmpty.tsx +9 -7
- package/src/PanelHeader.tsx +2 -5
- package/src/Pill.tsx +7 -8
- package/src/ProjectSwitcher.tsx +16 -6
- package/src/PromptDialog.tsx +31 -26
- package/src/RelativeTime.tsx +6 -1
- package/src/SegmentBar.tsx +22 -11
- package/src/Segmented.tsx +16 -11
- package/src/Select.tsx +7 -4
- package/src/SelectSearch.tsx +3 -0
- package/src/ShortcutHints.tsx +24 -3
- package/src/Skeleton.tsx +1 -0
- package/src/Slider.tsx +4 -1
- package/src/SlidingColumn.tsx +18 -14
- package/src/Spinner.tsx +1 -0
- package/src/SplitButton.tsx +8 -8
- package/src/Stepper.tsx +6 -0
- package/src/Switch.tsx +2 -0
- package/src/TextMenu.tsx +2 -5
- package/src/Tile.tsx +2 -6
- package/src/Toasts.tsx +8 -12
- package/src/Tooltip.tsx +10 -5
- package/src/UIProvider.tsx +7 -7
- package/src/Waveform.tsx +41 -12
- package/src/Wipe.tsx +5 -6
- package/src/ZoomControls.tsx +5 -2
- package/src/clipboard.ts +4 -5
- package/src/combobox/parts.tsx +18 -14
- package/src/command-palette/context.ts +2 -0
- package/src/command-palette/index.parts.ts +1 -0
- package/src/command-palette/parts.tsx +20 -14
- package/src/command-palette/virtual.tsx +21 -0
- package/src/dialog/parts.tsx +10 -13
- package/src/dialog-layer.ts +5 -3
- package/src/error-boundary.ts +7 -20
- package/src/file-icon.ts +8 -9
- package/src/file-tree/empty-lines.ts +24 -8
- package/src/file-tree/helpers.ts +34 -0
- package/src/file-tree/highlight.ts +19 -0
- package/src/file-tree/index.parts.ts +1 -0
- package/src/file-tree/parts.tsx +41 -5
- package/src/file-tree/use-empty-lines.ts +22 -5
- package/src/file-tree/use-file-tree-shift.tsx +10 -0
- package/src/file-tree/use-file-tree.ts +2 -0
- package/src/file-tree/use-slots.ts +77 -12
- package/src/format/datetime.ts +42 -17
- package/src/format/duration.ts +28 -10
- package/src/format/index.ts +3 -0
- package/src/format/locale.ts +33 -42
- package/src/format/number.ts +21 -27
- package/src/format/regions.ts +4 -7
- package/src/icon-picker.ts +23 -16
- package/src/index.ts +9 -0
- package/src/key-value-list/parts.tsx +1 -5
- package/src/lazy-landings.ts +2 -0
- package/src/lazy.tsx +23 -19
- package/src/locales.ts +2 -6
- package/src/match.ts +14 -0
- package/src/menu/parts.tsx +30 -55
- package/src/middle-truncate.ts +12 -3
- package/src/modality.ts +4 -0
- package/src/placement.ts +2 -1
- package/src/platform.ts +2 -0
- package/src/popover/parts.tsx +3 -5
- package/src/popup-layer.ts +6 -4
- package/src/prefetch.ts +18 -5
- package/src/project-switcher-row.tsx +2 -0
- package/src/segmented-keys.ts +6 -0
- package/src/select-shared.ts +2 -0
- package/src/selection.ts +5 -5
- package/src/settings/ConfirmDialog.tsx +3 -0
- package/src/settings/MasterDetail.tsx +3 -9
- package/src/settings/SettingsDialog.tsx +27 -9
- package/src/settings/SettingsRow.tsx +16 -10
- package/src/settings/SettingsSection.tsx +10 -5
- package/src/settings/index.ts +3 -0
- package/src/settings/target.ts +1 -0
- package/src/shortcut-hints.ts +10 -5
- package/src/shortcut.ts +26 -6
- package/src/tabs/overflow.ts +8 -6
- package/src/tabs/parts.tsx +50 -26
- package/src/testing/dedupe.ts +27 -7
- package/src/testing/fake-source.ts +1 -0
- package/src/tint.ts +13 -0
- package/src/toast-store.ts +20 -14
- package/src/tree/parts.tsx +8 -0
- package/src/tree/shift.ts +6 -0
- package/src/tree/use-tree-shift.tsx +23 -7
- package/src/useAsyncAction.ts +8 -0
- package/src/useColumnResize.ts +24 -15
- package/src/useContentSize.ts +5 -0
- package/src/useMeasuredWidth.ts +1 -0
- package/src/useNow.ts +6 -11
- package/src/workspace/DropPreview.tsx +3 -5
- package/src/workspace/SplitView.tsx +109 -0
- package/src/workspace/TabStrip.tsx +42 -0
- package/src/workspace/Workspace.tsx +5 -0
- package/src/workspace/context.ts +1 -0
- package/src/workspace/geometry.ts +49 -0
- package/src/workspace/model.ts +72 -0
- package/src/workspace/preview.ts +15 -15
|
@@ -91,6 +91,7 @@ export function TabStrip({
|
|
|
91
91
|
const { t } = useTranslation('ui');
|
|
92
92
|
const strip = useRef<HTMLDivElement>(null);
|
|
93
93
|
const closingFocus = useRef<string | null>(null);
|
|
94
|
+
|
|
94
95
|
useLayoutEffect(() => {
|
|
95
96
|
if (closingFocus.current && !items.some((item) => item.id === closingFocus.current)) {
|
|
96
97
|
closingFocus.current = null;
|
|
@@ -98,26 +99,33 @@ export function TabStrip({
|
|
|
98
99
|
target?.focus({ preventScroll: true });
|
|
99
100
|
}
|
|
100
101
|
});
|
|
102
|
+
|
|
101
103
|
const close = (id: string, action?: () => void): void => {
|
|
102
104
|
if (strip.current?.contains(strip.current.ownerDocument.activeElement)) {
|
|
103
105
|
closingFocus.current = id;
|
|
104
106
|
}
|
|
107
|
+
|
|
105
108
|
if (action) {
|
|
106
109
|
action();
|
|
107
110
|
} else {
|
|
108
111
|
onClose?.(id);
|
|
109
112
|
}
|
|
110
113
|
};
|
|
114
|
+
|
|
111
115
|
const [edges, setEdges] = useState({ start: false, end: false });
|
|
112
116
|
const [insertion, setInsertion] = useState(0);
|
|
117
|
+
|
|
113
118
|
const measure = (): void => {
|
|
114
119
|
const element = strip.current;
|
|
120
|
+
|
|
115
121
|
if (!element) {
|
|
116
122
|
return;
|
|
117
123
|
}
|
|
124
|
+
|
|
118
125
|
const start = element.scrollLeft > 0;
|
|
119
126
|
const end = element.scrollLeft + element.clientWidth < element.scrollWidth - 1;
|
|
120
127
|
setEdges((current) => (current.start === start && current.end === end ? current : { start, end }));
|
|
128
|
+
|
|
121
129
|
if (insertAt !== undefined && insertAt !== null) {
|
|
122
130
|
const tabs = Array.from(element.querySelectorAll<HTMLElement>('[data-document-tab]'));
|
|
123
131
|
const after = tabs[insertAt];
|
|
@@ -125,48 +133,63 @@ export function TabStrip({
|
|
|
125
133
|
setInsertion(after?.offsetLeft ?? (last ? last.offsetLeft + last.offsetWidth : 0));
|
|
126
134
|
}
|
|
127
135
|
};
|
|
136
|
+
|
|
128
137
|
// Stays set after the active tab changes, as its width can settle frames later; a person's own scroll ends it.
|
|
129
138
|
const revealing = useRef(false);
|
|
139
|
+
|
|
130
140
|
const reveal = (): void => {
|
|
131
141
|
const element = strip.current;
|
|
132
142
|
const selected = element?.querySelector<HTMLElement>('[aria-selected="true"]');
|
|
143
|
+
|
|
133
144
|
if (!revealing.current || !element || !selected) {
|
|
134
145
|
return;
|
|
135
146
|
}
|
|
147
|
+
|
|
136
148
|
const container = selected.closest<HTMLElement>('[data-document-tab]');
|
|
137
149
|
const tab = container && element.contains(container) ? container : selected;
|
|
138
150
|
const fade = parseFloat(getComputedStyle(element).getPropertyValue('--fade-size')) || 0;
|
|
139
151
|
const bounds = element.getBoundingClientRect();
|
|
140
152
|
const rect = tab.getBoundingClientRect();
|
|
141
153
|
const left = tabRevealScrollLeft(element, rect.left - bounds.left - element.clientLeft + element.scrollLeft, rect.width, fade);
|
|
154
|
+
|
|
142
155
|
if (left !== element.scrollLeft) {
|
|
143
156
|
element.scrollLeft = left;
|
|
144
157
|
}
|
|
145
158
|
};
|
|
159
|
+
|
|
146
160
|
useLayoutEffect(() => {
|
|
147
161
|
revealing.current = true;
|
|
148
162
|
}, [value]);
|
|
163
|
+
|
|
149
164
|
useLayoutEffect(() => {
|
|
150
165
|
reveal();
|
|
151
166
|
measure();
|
|
152
167
|
});
|
|
168
|
+
|
|
153
169
|
useEffect(() => {
|
|
154
170
|
const element = strip.current;
|
|
171
|
+
|
|
155
172
|
if (!element || typeof ResizeObserver === 'undefined') {
|
|
156
173
|
return;
|
|
157
174
|
}
|
|
175
|
+
|
|
158
176
|
const observer = new ResizeObserver(() => {
|
|
159
177
|
reveal();
|
|
160
178
|
measure();
|
|
161
179
|
});
|
|
180
|
+
|
|
162
181
|
observer.observe(element);
|
|
163
182
|
element.querySelectorAll('[data-document-tab]').forEach((tab) => observer.observe(tab));
|
|
183
|
+
|
|
164
184
|
return () => observer.disconnect();
|
|
165
185
|
});
|
|
186
|
+
|
|
166
187
|
const gap = (event: DragEvent<HTMLDivElement>): number => {
|
|
167
188
|
const rects = Array.from(event.currentTarget.querySelectorAll('[data-document-tab]'), (tab) => tab.getBoundingClientRect());
|
|
189
|
+
|
|
168
190
|
return tabGapAt(rects, event.clientX);
|
|
169
191
|
};
|
|
192
|
+
|
|
170
193
|
const context: TabContextValue = {
|
|
171
194
|
items,
|
|
172
195
|
value,
|
|
@@ -180,6 +203,7 @@ export function TabStrip({
|
|
|
180
203
|
close,
|
|
181
204
|
focus: (index) => strip.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]')[index]?.focus()
|
|
182
205
|
};
|
|
206
|
+
|
|
183
207
|
return useRender({
|
|
184
208
|
render,
|
|
185
209
|
ref: [strip, ref ?? null],
|
|
@@ -198,16 +222,20 @@ export function TabStrip({
|
|
|
198
222
|
},
|
|
199
223
|
onWheel: (event: WheelEvent<HTMLDivElement>) => {
|
|
200
224
|
props.onWheel?.(event);
|
|
225
|
+
|
|
201
226
|
if (event.defaultPrevented) {
|
|
202
227
|
return;
|
|
203
228
|
}
|
|
229
|
+
|
|
204
230
|
revealing.current = false;
|
|
231
|
+
|
|
205
232
|
if (!event.deltaX && event.deltaY) {
|
|
206
233
|
event.currentTarget.scrollLeft += event.deltaY;
|
|
207
234
|
}
|
|
208
235
|
},
|
|
209
236
|
onPointerDown: (event: PointerEvent<HTMLDivElement>) => {
|
|
210
237
|
props.onPointerDown?.(event);
|
|
238
|
+
|
|
211
239
|
// A press on the strip itself grabs its scrollbar, and a touch may pan it.
|
|
212
240
|
if (event.target === event.currentTarget || event.pointerType === 'touch') {
|
|
213
241
|
revealing.current = false;
|
|
@@ -215,12 +243,14 @@ export function TabStrip({
|
|
|
215
243
|
},
|
|
216
244
|
onDragOver: (event: DragEvent<HTMLDivElement>) => {
|
|
217
245
|
props.onDragOver?.(event);
|
|
246
|
+
|
|
218
247
|
if (!event.defaultPrevented) {
|
|
219
248
|
onTabDragOver?.(gap(event), event);
|
|
220
249
|
}
|
|
221
250
|
},
|
|
222
251
|
onDrop: (event: DragEvent<HTMLDivElement>) => {
|
|
223
252
|
props.onDrop?.(event);
|
|
253
|
+
|
|
224
254
|
if (!event.defaultPrevented) {
|
|
225
255
|
onTabDrop?.(gap(event), event);
|
|
226
256
|
}
|
|
@@ -246,6 +276,7 @@ interface TabContextValue extends Pick<
|
|
|
246
276
|
close(id: string, action?: () => void): void;
|
|
247
277
|
focus(index: number): void;
|
|
248
278
|
}
|
|
279
|
+
|
|
249
280
|
const TabContext = createContext<TabContextValue | null>(null);
|
|
250
281
|
|
|
251
282
|
export interface DocumentTabProps extends Omit<useRender.ComponentProps<'span'>, 'children' | 'onDoubleClick' | 'onDragStart' | 'onDragEnd'> {
|
|
@@ -260,20 +291,26 @@ export interface DocumentTabProps extends Omit<useRender.ComponentProps<'span'>,
|
|
|
260
291
|
/** A tab with application-owned metadata and menus, rendered inside TabStrip's renderTab slot. */
|
|
261
292
|
export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDragStart, onDragEnd, className, render, ref, ...props }: DocumentTabProps) {
|
|
262
293
|
const context = useContext(TabContext);
|
|
294
|
+
|
|
263
295
|
if (!context) {
|
|
264
296
|
throw new Error('DocumentTab must be rendered inside TabStrip.');
|
|
265
297
|
}
|
|
298
|
+
|
|
266
299
|
const { t } = useTranslation('ui');
|
|
267
300
|
const { items, value, onValueChange } = context;
|
|
268
301
|
const index = items.findIndex((candidate) => candidate.id === item.id);
|
|
302
|
+
|
|
269
303
|
const close = (): void => context.close(item.id, onClose);
|
|
304
|
+
|
|
270
305
|
const closable = item.closable !== false && (onClose ?? context.onClose) !== undefined;
|
|
306
|
+
|
|
271
307
|
// Shift with Cmd, Ctrl or Alt stays the app's to give a meaning.
|
|
272
308
|
const closesOnPress = (event: MouseEvent): boolean =>
|
|
273
309
|
!!context.pressToClose &&
|
|
274
310
|
closable &&
|
|
275
311
|
!item.pinned &&
|
|
276
312
|
(event.button === 1 || (event.button === 0 && event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey));
|
|
313
|
+
|
|
277
314
|
return useRender({
|
|
278
315
|
render,
|
|
279
316
|
ref,
|
|
@@ -290,6 +327,7 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
|
|
|
290
327
|
},
|
|
291
328
|
onMouseDown: (event: MouseEvent<HTMLSpanElement>) => {
|
|
292
329
|
props.onMouseDown?.(event);
|
|
330
|
+
|
|
293
331
|
// A press that closes starts no text selection, takes no focus and no middle-button scroll.
|
|
294
332
|
if (closesOnPress(event)) {
|
|
295
333
|
event.preventDefault();
|
|
@@ -297,6 +335,7 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
|
|
|
297
335
|
},
|
|
298
336
|
onAuxClick: (event: MouseEvent<HTMLSpanElement>) => {
|
|
299
337
|
props.onAuxClick?.(event);
|
|
338
|
+
|
|
300
339
|
if (event.button === 1 && closesOnPress(event)) {
|
|
301
340
|
event.preventDefault();
|
|
302
341
|
close();
|
|
@@ -317,8 +356,10 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
|
|
|
317
356
|
onClick={(event) => {
|
|
318
357
|
if (closesOnPress(event)) {
|
|
319
358
|
close();
|
|
359
|
+
|
|
320
360
|
return;
|
|
321
361
|
}
|
|
362
|
+
|
|
322
363
|
onValueChange(item.id);
|
|
323
364
|
}}
|
|
324
365
|
onDoubleClick={(event) => (onDoubleClick ?? context.onDoubleClick)?.(item.id, event)}
|
|
@@ -329,6 +370,7 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
|
|
|
329
370
|
onDragEnd={onDragEnd ?? context.onDragEnd}
|
|
330
371
|
onKeyDown={(event) => {
|
|
331
372
|
const next = tabIndexForKey(event.key, index, items.length);
|
|
373
|
+
|
|
332
374
|
if (next !== null) {
|
|
333
375
|
event.preventDefault();
|
|
334
376
|
onValueChange(items[next]!.id);
|
|
@@ -8,8 +8,10 @@ export interface WorkspaceProps extends useRender.ComponentProps<'div'> {
|
|
|
8
8
|
toolbar?: ReactNode;
|
|
9
9
|
sidebar?: ReactNode;
|
|
10
10
|
sidePanel?: ReactNode;
|
|
11
|
+
|
|
11
12
|
/** Auto-sized regions keep their own width, separators and opening animation. */
|
|
12
13
|
sidebarWidth?: number | 'auto';
|
|
14
|
+
|
|
13
15
|
sidePanelWidth?: number | 'auto';
|
|
14
16
|
sidebarOpen?: boolean;
|
|
15
17
|
sidePanelOpen?: boolean;
|
|
@@ -38,6 +40,7 @@ export function Workspace({
|
|
|
38
40
|
const leftOpen = hasSidebar && (sidebarOpen ?? true);
|
|
39
41
|
const rightOpen = hasPanel && (sidePanelOpen ?? true);
|
|
40
42
|
const hasToolbar = isPresent(toolbar);
|
|
43
|
+
|
|
41
44
|
const element = useRender({
|
|
42
45
|
render,
|
|
43
46
|
ref,
|
|
@@ -70,6 +73,8 @@ export function Workspace({
|
|
|
70
73
|
)
|
|
71
74
|
}
|
|
72
75
|
});
|
|
76
|
+
|
|
73
77
|
const context = { layout, edges: { top: !hasToolbar, right: !rightOpen, bottom: true, left: !leftOpen } };
|
|
78
|
+
|
|
74
79
|
return <WorkspaceContext value={context}>{element}</WorkspaceContext>;
|
|
75
80
|
}
|
package/src/workspace/context.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createContext } from 'react';
|
|
|
2
2
|
import type { SplitEdges } from './geometry.ts';
|
|
3
3
|
|
|
4
4
|
export type WorkspaceLayout = 'standard' | 'roomy';
|
|
5
|
+
|
|
5
6
|
export const WorkspaceContext = createContext<{ layout: WorkspaceLayout; edges: SplitEdges }>({
|
|
6
7
|
layout: 'standard',
|
|
7
8
|
edges: { top: true, right: true, bottom: true, left: true }
|
|
@@ -6,19 +6,23 @@ export interface SplitRect {
|
|
|
6
6
|
width: number;
|
|
7
7
|
height: number;
|
|
8
8
|
}
|
|
9
|
+
|
|
9
10
|
export interface SplitEdges {
|
|
10
11
|
top?: boolean;
|
|
11
12
|
right?: boolean;
|
|
12
13
|
bottom?: boolean;
|
|
13
14
|
left?: boolean;
|
|
14
15
|
}
|
|
16
|
+
|
|
15
17
|
export interface SplitMinimum {
|
|
16
18
|
width: number;
|
|
17
19
|
height: number;
|
|
18
20
|
}
|
|
21
|
+
|
|
19
22
|
export interface SplitPaneRect extends SplitRect {
|
|
20
23
|
pane: SplitPane;
|
|
21
24
|
}
|
|
25
|
+
|
|
22
26
|
export interface SplitDivider extends SplitRect {
|
|
23
27
|
splitId: string;
|
|
24
28
|
index: number;
|
|
@@ -26,6 +30,7 @@ export interface SplitDivider extends SplitRect {
|
|
|
26
30
|
lengths: number[];
|
|
27
31
|
minimums: number[];
|
|
28
32
|
}
|
|
33
|
+
|
|
29
34
|
export interface SplitGeometry {
|
|
30
35
|
panes: SplitPaneRect[];
|
|
31
36
|
dividers: SplitDivider[];
|
|
@@ -33,45 +38,58 @@ export interface SplitGeometry {
|
|
|
33
38
|
|
|
34
39
|
function distribute(total: number, weights: number[], minimums: number[]): number[] {
|
|
35
40
|
const floor = minimums.reduce((sum, size) => sum + size, 0);
|
|
41
|
+
|
|
36
42
|
if (floor >= total) {
|
|
37
43
|
return minimums.map((size) => (floor ? (size * total) / floor : total / minimums.length));
|
|
38
44
|
}
|
|
45
|
+
|
|
39
46
|
const result = weights.map(() => 0);
|
|
40
47
|
const pending = new Set(weights.map((_, i) => i));
|
|
41
48
|
let remaining = total;
|
|
49
|
+
|
|
42
50
|
while (pending.size) {
|
|
43
51
|
const sum = [...pending].reduce((value, i) => value + weights[i]!, 0);
|
|
44
52
|
const small = [...pending].filter((i) => (remaining * weights[i]!) / sum < minimums[i]!);
|
|
53
|
+
|
|
45
54
|
if (!small.length) {
|
|
46
55
|
pending.forEach((i) => {
|
|
47
56
|
result[i] = (remaining * weights[i]!) / sum;
|
|
48
57
|
});
|
|
58
|
+
|
|
49
59
|
break;
|
|
50
60
|
}
|
|
61
|
+
|
|
51
62
|
small.forEach((i) => {
|
|
52
63
|
result[i] = minimums[i]!;
|
|
53
64
|
remaining -= minimums[i]!;
|
|
54
65
|
pending.delete(i);
|
|
55
66
|
});
|
|
56
67
|
}
|
|
68
|
+
|
|
57
69
|
return result;
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
export function splitGeometry(layout: SplitLayout, width: number, height: number, gap: number, minimum: (pane: SplitPane) => SplitMinimum): SplitGeometry {
|
|
61
73
|
const result: SplitGeometry = { panes: [], dividers: [] };
|
|
62
74
|
const minimums = new Map<string, SplitMinimum>();
|
|
75
|
+
|
|
63
76
|
const measure = (node: SplitNode): SplitMinimum => {
|
|
64
77
|
if (node.type === 'pane') {
|
|
65
78
|
const value = minimum(node);
|
|
79
|
+
|
|
66
80
|
const safe = {
|
|
67
81
|
width: Math.max(0, Number.isFinite(value.width) ? value.width : 0),
|
|
68
82
|
height: Math.max(0, Number.isFinite(value.height) ? value.height : 0)
|
|
69
83
|
};
|
|
84
|
+
|
|
70
85
|
minimums.set(node.id, safe);
|
|
86
|
+
|
|
71
87
|
return safe;
|
|
72
88
|
}
|
|
89
|
+
|
|
73
90
|
const children = node.children.map(measure);
|
|
74
91
|
const horizontal = node.axis === 'horizontal';
|
|
92
|
+
|
|
75
93
|
const value = {
|
|
76
94
|
width: horizontal
|
|
77
95
|
? children.reduce((sum, item) => sum + item.width, 0) + gap * (children.length - 1)
|
|
@@ -80,14 +98,19 @@ export function splitGeometry(layout: SplitLayout, width: number, height: number
|
|
|
80
98
|
? Math.max(...children.map((item) => item.height))
|
|
81
99
|
: children.reduce((sum, item) => sum + item.height, 0) + gap * (children.length - 1)
|
|
82
100
|
};
|
|
101
|
+
|
|
83
102
|
minimums.set(node.id, value);
|
|
103
|
+
|
|
84
104
|
return value;
|
|
85
105
|
};
|
|
106
|
+
|
|
86
107
|
const walk = (node: SplitNode, rect: SplitRect): void => {
|
|
87
108
|
if (node.type === 'pane') {
|
|
88
109
|
result.panes.push({ ...rect, pane: node });
|
|
110
|
+
|
|
89
111
|
return;
|
|
90
112
|
}
|
|
113
|
+
|
|
91
114
|
const horizontal = node.axis === 'horizontal';
|
|
92
115
|
const axisLength = horizontal ? rect.width : rect.height;
|
|
93
116
|
const spacing = Math.min(gap, axisLength / (node.children.length - 1));
|
|
@@ -95,10 +118,12 @@ export function splitGeometry(layout: SplitLayout, width: number, height: number
|
|
|
95
118
|
const limits = node.children.map((child) => minimums.get(child.id)![horizontal ? 'width' : 'height']);
|
|
96
119
|
const lengths = distribute(available, node.sizes, limits);
|
|
97
120
|
let offset = 0;
|
|
121
|
+
|
|
98
122
|
node.children.forEach((child, i) => {
|
|
99
123
|
const end = Math.round(offset + lengths[i]!);
|
|
100
124
|
const start = Math.round(offset);
|
|
101
125
|
walk(child, horizontal ? { ...rect, x: rect.x + start, width: end - start } : { ...rect, y: rect.y + start, height: end - start });
|
|
126
|
+
|
|
102
127
|
if (i < node.children.length - 1) {
|
|
103
128
|
result.dividers.push({
|
|
104
129
|
...rect,
|
|
@@ -110,12 +135,15 @@ export function splitGeometry(layout: SplitLayout, width: number, height: number
|
|
|
110
135
|
minimums: limits
|
|
111
136
|
});
|
|
112
137
|
}
|
|
138
|
+
|
|
113
139
|
offset += lengths[i]! + spacing;
|
|
114
140
|
});
|
|
115
141
|
};
|
|
142
|
+
|
|
116
143
|
const root = layout.maximized ? (findSplitNode(layout.root, layout.maximized) ?? layout.root) : layout.root;
|
|
117
144
|
measure(root);
|
|
118
145
|
walk(root, { x: 0, y: 0, width: Math.max(0, width), height: Math.max(0, height) });
|
|
146
|
+
|
|
119
147
|
return result;
|
|
120
148
|
}
|
|
121
149
|
|
|
@@ -123,14 +151,17 @@ export function resizeSplit(divider: SplitDivider, delta: number, symmetric = fa
|
|
|
123
151
|
const lengths = [...divider.lengths];
|
|
124
152
|
const { index } = divider;
|
|
125
153
|
const total = lengths.reduce((sum, size) => sum + size, 0);
|
|
154
|
+
|
|
126
155
|
if (total <= 0 || lengths.some((size) => size <= 0)) {
|
|
127
156
|
return lengths.map(() => 1 / lengths.length);
|
|
128
157
|
}
|
|
158
|
+
|
|
129
159
|
const mirror = lengths.length - divider.index - 2;
|
|
130
160
|
const affected = new Set([index, index + 1, ...(symmetric ? [mirror, mirror + 1] : [])]);
|
|
131
161
|
const span = [...affected].reduce((sum, i) => sum + lengths[i]!, 0);
|
|
132
162
|
const share = Number.isFinite(minimumShare) ? Math.max(0, Math.min(1 / affected.size, minimumShare)) : 0;
|
|
133
163
|
const min = divider.minimums.map((size, i) => Math.min(lengths[i]!, Math.max(size, affected.has(i) ? span * share : 0)));
|
|
164
|
+
|
|
134
165
|
if (symmetric && mirror !== index) {
|
|
135
166
|
const changes = lengths.map(() => 0);
|
|
136
167
|
changes[index]!++;
|
|
@@ -139,17 +170,22 @@ export function resizeSplit(divider: SplitDivider, delta: number, symmetric = fa
|
|
|
139
170
|
changes[mirror + 1]!++;
|
|
140
171
|
let lower = -Infinity;
|
|
141
172
|
let upper = Infinity;
|
|
173
|
+
|
|
142
174
|
changes.forEach((coefficient, i) => {
|
|
143
175
|
if (coefficient > 0) {
|
|
144
176
|
lower = Math.max(lower, (min[i]! - lengths[i]!) / coefficient);
|
|
145
177
|
}
|
|
178
|
+
|
|
146
179
|
if (coefficient < 0) {
|
|
147
180
|
upper = Math.min(upper, (lengths[i]! - min[i]!) / -coefficient);
|
|
148
181
|
}
|
|
149
182
|
});
|
|
183
|
+
|
|
150
184
|
const change = Math.max(lower, Math.min(delta, upper));
|
|
185
|
+
|
|
151
186
|
return lengths.map((size, i) => Math.max(Number.EPSILON, (size + changes[i]! * change) / total));
|
|
152
187
|
}
|
|
188
|
+
|
|
153
189
|
const change = Math.max(min[index]! - lengths[index]!, Math.min(delta, lengths[index + 1]! - min[index + 1]!));
|
|
154
190
|
const pair = lengths[index]! + lengths[index + 1]!;
|
|
155
191
|
const middle = pair / 2;
|
|
@@ -157,6 +193,7 @@ export function resizeSplit(divider: SplitDivider, delta: number, symmetric = fa
|
|
|
157
193
|
// A snap must not cross either pane's minimum, including minimums inherited from nested splits.
|
|
158
194
|
lengths[index] = Math.abs(next - middle) <= snapDistance && middle >= min[index]! && middle >= min[index + 1]! ? middle : next;
|
|
159
195
|
lengths[index + 1] = pair - lengths[index]!;
|
|
196
|
+
|
|
160
197
|
return lengths.map((size) => Math.max(Number.EPSILON, size / total));
|
|
161
198
|
}
|
|
162
199
|
|
|
@@ -165,6 +202,7 @@ export function paneRadius(rect: SplitRect, width: number, height: number, edges
|
|
|
165
202
|
const top = rect.y === 0 && edges.top;
|
|
166
203
|
const right = rect.x + rect.width >= width && edges.right;
|
|
167
204
|
const bottom = rect.y + rect.height >= height && edges.bottom;
|
|
205
|
+
|
|
168
206
|
return [top || left, top || right, bottom || right, bottom || left].map((flush) => `${flush ? 0 : radius}px`).join(' ');
|
|
169
207
|
}
|
|
170
208
|
|
|
@@ -172,20 +210,25 @@ export function splitSideAt(rect: SplitRect, x: number, y: number): SplitSide |
|
|
|
172
210
|
const horizontal = (x - rect.x) / rect.width;
|
|
173
211
|
const vertical = (y - rect.y) / rect.height;
|
|
174
212
|
const nearest = Math.min(horizontal, 1 - horizontal, vertical, 1 - vertical);
|
|
213
|
+
|
|
175
214
|
if (nearest > 0.25) {
|
|
176
215
|
return undefined;
|
|
177
216
|
}
|
|
217
|
+
|
|
178
218
|
if (nearest === horizontal) {
|
|
179
219
|
return 'left';
|
|
180
220
|
}
|
|
221
|
+
|
|
181
222
|
if (nearest === 1 - horizontal) {
|
|
182
223
|
return 'right';
|
|
183
224
|
}
|
|
225
|
+
|
|
184
226
|
return nearest === vertical ? 'top' : 'bottom';
|
|
185
227
|
}
|
|
186
228
|
|
|
187
229
|
export function tabGapAt(rects: readonly { left: number; right: number }[], x: number): number {
|
|
188
230
|
const index = rects.findIndex((rect) => x < (rect.left + rect.right) / 2);
|
|
231
|
+
|
|
189
232
|
return index < 0 ? rects.length : index;
|
|
190
233
|
}
|
|
191
234
|
|
|
@@ -203,14 +246,20 @@ export interface TabScrollView {
|
|
|
203
246
|
export function tabRevealScrollLeft(view: TabScrollView, start: number, width: number, fade: number): number {
|
|
204
247
|
const max = Math.max(0, view.scrollWidth - view.clientWidth);
|
|
205
248
|
const end = start + width;
|
|
249
|
+
|
|
206
250
|
const visibleStart = (left: number): number => left + (left > 0 ? fade : 0);
|
|
251
|
+
|
|
207
252
|
const visibleEnd = (left: number): number => left + view.clientWidth - (left < max - 1 ? fade : 0);
|
|
253
|
+
|
|
208
254
|
const clamp = (left: number): number => Math.round(Math.min(max, Math.max(0, left)));
|
|
255
|
+
|
|
209
256
|
if (start >= visibleStart(view.scrollLeft) && end <= visibleEnd(view.scrollLeft)) {
|
|
210
257
|
return view.scrollLeft;
|
|
211
258
|
}
|
|
259
|
+
|
|
212
260
|
if (start < visibleStart(view.scrollLeft) || width > view.clientWidth - 2 * fade) {
|
|
213
261
|
return clamp(start - fade);
|
|
214
262
|
}
|
|
263
|
+
|
|
215
264
|
return clamp(end + fade - view.clientWidth);
|
|
216
265
|
}
|