@adecore/ui 0.39.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/dist/AccentSwatches.js +2 -3
  2. package/dist/Banner.js +1 -6
  3. package/dist/ChoiceCards.js +0 -5
  4. package/dist/ColorSwatch.js +1 -4
  5. package/dist/ColorWheelPicker.js +3 -6
  6. package/dist/DockShell.js +3 -5
  7. package/dist/EmptyState.js +0 -1
  8. package/dist/ErrorBoundary.js +1 -5
  9. package/dist/Field.js +2 -6
  10. package/dist/FileIcon.js +2 -4
  11. package/dist/Icon.js +1 -2
  12. package/dist/IconButton.js +1 -1
  13. package/dist/IconPicker.js +3 -7
  14. package/dist/Input.js +1 -2
  15. package/dist/Kbd.js +1 -3
  16. package/dist/LazyBoundary.js +2 -3
  17. package/dist/Meter.js +0 -1
  18. package/dist/MiddleTruncate.js +2 -6
  19. package/dist/PanelEmpty.js +0 -1
  20. package/dist/PanelHeader.js +1 -4
  21. package/dist/Pill.js +1 -6
  22. package/dist/ProjectSwitcher.js +1 -1
  23. package/dist/PromptDialog.js +5 -8
  24. package/dist/RelativeTime.js +1 -1
  25. package/dist/SegmentBar.js +1 -2
  26. package/dist/Segmented.js +2 -5
  27. package/dist/Select.js +1 -4
  28. package/dist/ShortcutHints.js +2 -3
  29. package/dist/Slider.js +0 -1
  30. package/dist/SlidingColumn.js +2 -4
  31. package/dist/SplitButton.js +1 -5
  32. package/dist/TextMenu.js +1 -5
  33. package/dist/Tile.js +0 -4
  34. package/dist/Toasts.js +1 -9
  35. package/dist/Tooltip.js +1 -1
  36. package/dist/UIProvider.js +0 -5
  37. package/dist/Waveform.js +4 -2
  38. package/dist/Wipe.js +1 -6
  39. package/dist/ZoomControls.js +0 -1
  40. package/dist/clipboard.js +2 -5
  41. package/dist/combobox/parts.js +3 -9
  42. package/dist/command-palette/parts.js +5 -14
  43. package/dist/dialog/parts.js +5 -10
  44. package/dist/dialog-layer.js +2 -3
  45. package/dist/error-boundary.js +4 -20
  46. package/dist/file-icon.js +4 -9
  47. package/dist/file-tree/empty-lines.js +2 -8
  48. package/dist/file-tree/use-empty-lines.js +1 -5
  49. package/dist/file-tree/use-slots.js +6 -10
  50. package/dist/format/datetime.js +32 -29
  51. package/dist/format/duration.js +2 -10
  52. package/dist/format/locale.js +19 -39
  53. package/dist/format/number.js +9 -22
  54. package/dist/format/regions.js +4 -7
  55. package/dist/icon-picker.js +5 -14
  56. package/dist/key-value-list/parts.js +1 -5
  57. package/dist/lazy.js +4 -19
  58. package/dist/locales.js +2 -6
  59. package/dist/menu/parts.js +61 -133
  60. package/dist/middle-truncate.js +2 -3
  61. package/dist/placement.js +0 -1
  62. package/dist/popover/parts.js +1 -5
  63. package/dist/popup-layer.js +2 -4
  64. package/dist/prefetch.js +2 -5
  65. package/dist/selection.js +1 -5
  66. package/dist/settings/MasterDetail.js +1 -7
  67. package/dist/settings/SettingsDialog.js +1 -2
  68. package/dist/settings/SettingsRow.js +2 -5
  69. package/dist/settings/SettingsSection.js +1 -1
  70. package/dist/shortcut-hints.js +1 -5
  71. package/dist/shortcut.js +1 -2
  72. package/dist/tabs/overflow.js +1 -6
  73. package/dist/tabs/parts.js +69 -95
  74. package/dist/testing/dedupe.js +5 -7
  75. package/dist/tint.js +3 -5
  76. package/dist/toast-store.js +2 -10
  77. package/dist/tree/use-tree-shift.js +2 -6
  78. package/dist/useColumnResize.js +9 -10
  79. package/dist/useNow.js +2 -9
  80. package/dist/workspace/DropPreview.js +1 -5
  81. package/dist/workspace/preview.js +4 -9
  82. package/package.json +10 -10
  83. package/src/AccentSwatches.tsx +9 -3
  84. package/src/Banner.tsx +5 -11
  85. package/src/Button.tsx +4 -0
  86. package/src/Checkbox.tsx +3 -0
  87. package/src/ChoiceCards.tsx +12 -11
  88. package/src/CloseButton.tsx +1 -0
  89. package/src/ColorSwatch.tsx +7 -8
  90. package/src/ColorWheelPicker.tsx +28 -12
  91. package/src/ColumnResizeHandle.tsx +1 -0
  92. package/src/CopyButton.tsx +6 -0
  93. package/src/DisabledReason.tsx +1 -0
  94. package/src/DockShell.tsx +11 -10
  95. package/src/EmptyState.tsx +10 -6
  96. package/src/ErrorBoundary.tsx +13 -10
  97. package/src/Field.tsx +11 -13
  98. package/src/FileIcon.tsx +4 -4
  99. package/src/Icon.tsx +2 -4
  100. package/src/IconButton.tsx +14 -5
  101. package/src/IconPicker.tsx +30 -15
  102. package/src/Input.tsx +11 -9
  103. package/src/Kbd.tsx +8 -7
  104. package/src/LazyBoundary.tsx +3 -3
  105. package/src/Meter.tsx +7 -2
  106. package/src/MiddleTruncate.tsx +15 -6
  107. package/src/PanelEmpty.tsx +9 -7
  108. package/src/PanelHeader.tsx +2 -5
  109. package/src/Pill.tsx +7 -8
  110. package/src/ProjectSwitcher.tsx +16 -6
  111. package/src/PromptDialog.tsx +31 -26
  112. package/src/RelativeTime.tsx +6 -1
  113. package/src/SegmentBar.tsx +22 -11
  114. package/src/Segmented.tsx +16 -11
  115. package/src/Select.tsx +7 -4
  116. package/src/SelectSearch.tsx +3 -0
  117. package/src/ShortcutHints.tsx +24 -3
  118. package/src/Skeleton.tsx +1 -0
  119. package/src/Slider.tsx +4 -1
  120. package/src/SlidingColumn.tsx +18 -14
  121. package/src/Spinner.tsx +1 -0
  122. package/src/SplitButton.tsx +8 -8
  123. package/src/Stepper.tsx +6 -0
  124. package/src/Switch.tsx +2 -0
  125. package/src/TextMenu.tsx +2 -5
  126. package/src/Tile.tsx +2 -6
  127. package/src/Toasts.tsx +8 -12
  128. package/src/Tooltip.tsx +10 -5
  129. package/src/UIProvider.tsx +7 -7
  130. package/src/Waveform.tsx +41 -12
  131. package/src/Wipe.tsx +5 -6
  132. package/src/ZoomControls.tsx +5 -2
  133. package/src/clipboard.ts +4 -5
  134. package/src/combobox/parts.tsx +18 -14
  135. package/src/command-palette/context.ts +2 -0
  136. package/src/command-palette/index.parts.ts +1 -0
  137. package/src/command-palette/parts.tsx +20 -14
  138. package/src/command-palette/virtual.tsx +21 -0
  139. package/src/dialog/parts.tsx +10 -13
  140. package/src/dialog-layer.ts +5 -3
  141. package/src/error-boundary.ts +7 -20
  142. package/src/file-icon.ts +8 -9
  143. package/src/file-tree/empty-lines.ts +24 -8
  144. package/src/file-tree/helpers.ts +34 -0
  145. package/src/file-tree/highlight.ts +19 -0
  146. package/src/file-tree/index.parts.ts +1 -0
  147. package/src/file-tree/parts.tsx +41 -5
  148. package/src/file-tree/use-empty-lines.ts +22 -5
  149. package/src/file-tree/use-file-tree-shift.tsx +10 -0
  150. package/src/file-tree/use-file-tree.ts +2 -0
  151. package/src/file-tree/use-slots.ts +77 -12
  152. package/src/format/datetime.ts +42 -17
  153. package/src/format/duration.ts +28 -10
  154. package/src/format/index.ts +3 -0
  155. package/src/format/locale.ts +33 -42
  156. package/src/format/number.ts +21 -27
  157. package/src/format/regions.ts +4 -7
  158. package/src/icon-picker.ts +23 -16
  159. package/src/index.ts +8 -0
  160. package/src/key-value-list/parts.tsx +1 -5
  161. package/src/lazy-landings.ts +2 -0
  162. package/src/lazy.tsx +23 -19
  163. package/src/locales.ts +2 -6
  164. package/src/match.ts +14 -0
  165. package/src/menu/parts.tsx +30 -55
  166. package/src/middle-truncate.ts +12 -3
  167. package/src/modality.ts +4 -0
  168. package/src/placement.ts +2 -1
  169. package/src/platform.ts +2 -0
  170. package/src/popover/parts.tsx +3 -5
  171. package/src/popup-layer.ts +6 -4
  172. package/src/prefetch.ts +18 -5
  173. package/src/project-switcher-row.tsx +2 -0
  174. package/src/segmented-keys.ts +6 -0
  175. package/src/select-shared.ts +2 -0
  176. package/src/selection.ts +5 -5
  177. package/src/settings/ConfirmDialog.tsx +3 -0
  178. package/src/settings/MasterDetail.tsx +3 -9
  179. package/src/settings/SettingsDialog.tsx +27 -9
  180. package/src/settings/SettingsRow.tsx +16 -10
  181. package/src/settings/SettingsSection.tsx +10 -5
  182. package/src/settings/index.ts +3 -0
  183. package/src/settings/target.ts +1 -0
  184. package/src/shortcut-hints.ts +10 -5
  185. package/src/shortcut.ts +26 -6
  186. package/src/tabs/overflow.ts +8 -6
  187. package/src/tabs/parts.tsx +50 -26
  188. package/src/testing/dedupe.ts +27 -7
  189. package/src/testing/fake-source.ts +1 -0
  190. package/src/tint.ts +3 -5
  191. package/src/toast-store.ts +20 -14
  192. package/src/tree/parts.tsx +8 -0
  193. package/src/tree/shift.ts +6 -0
  194. package/src/tree/use-tree-shift.tsx +23 -7
  195. package/src/useAsyncAction.ts +8 -0
  196. package/src/useColumnResize.ts +24 -15
  197. package/src/useContentSize.ts +5 -0
  198. package/src/useMeasuredWidth.ts +1 -0
  199. package/src/useNow.ts +6 -11
  200. package/src/workspace/DropPreview.tsx +3 -5
  201. package/src/workspace/SplitView.tsx +109 -0
  202. package/src/workspace/TabStrip.tsx +42 -0
  203. package/src/workspace/Workspace.tsx +5 -0
  204. package/src/workspace/context.ts +1 -0
  205. package/src/workspace/geometry.ts +49 -0
  206. package/src/workspace/model.ts +72 -0
  207. 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
  }
@@ -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
  }