@adecore/ui 0.23.1 → 0.24.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 (132) hide show
  1. package/dist/ButtonGroup.d.ts +1 -1
  2. package/dist/ButtonGroup.js +1 -1
  3. package/dist/Checkbox.js +1 -1
  4. package/dist/ChoiceCards.js +2 -2
  5. package/dist/CloseButton.js +1 -1
  6. package/dist/ColorSwatch.d.ts +1 -1
  7. package/dist/ColorSwatch.js +1 -1
  8. package/dist/Field.d.ts +1 -1
  9. package/dist/Field.js +4 -3
  10. package/dist/IconButton.d.ts +1 -1
  11. package/dist/IconButton.js +1 -1
  12. package/dist/ListRow.d.ts +1 -1
  13. package/dist/ListRow.js +1 -1
  14. package/dist/MiddleTruncate.d.ts +8 -0
  15. package/dist/MiddleTruncate.js +59 -0
  16. package/dist/PanelHeader.d.ts +1 -1
  17. package/dist/PanelHeader.js +1 -1
  18. package/dist/SectionLabel.d.ts +1 -1
  19. package/dist/SectionLabel.js +1 -1
  20. package/dist/Select.js +1 -1
  21. package/dist/SelectSearch.js +1 -1
  22. package/dist/Slider.js +1 -1
  23. package/dist/Surface.d.ts +1 -1
  24. package/dist/Surface.js +1 -1
  25. package/dist/Switch.js +1 -1
  26. package/dist/Toasts.d.ts +1 -1
  27. package/dist/Toasts.js +4 -5
  28. package/dist/Tooltip.d.ts +1 -1
  29. package/dist/Tooltip.js +2 -2
  30. package/dist/clipboard.d.ts +1 -1
  31. package/dist/clipboard.js +12 -2
  32. package/dist/dialog/parts.d.ts +3 -3
  33. package/dist/dialog/parts.js +2 -2
  34. package/dist/file-tree/helpers.d.ts +1 -0
  35. package/dist/file-tree/helpers.js +9 -0
  36. package/dist/file-tree/parts.d.ts +1 -1
  37. package/dist/file-tree/parts.js +1 -1
  38. package/dist/file-tree/use-slots.d.ts +8 -0
  39. package/dist/file-tree/use-slots.js +64 -22
  40. package/dist/format/datetime.d.ts +1 -0
  41. package/dist/format/datetime.js +15 -0
  42. package/dist/format/index.d.ts +1 -1
  43. package/dist/format/index.js +1 -1
  44. package/dist/index.d.ts +6 -3
  45. package/dist/index.js +5 -1
  46. package/dist/key-value-list/parts.d.ts +1 -1
  47. package/dist/key-value-list/parts.js +1 -1
  48. package/dist/locales/en.d.ts +83 -0
  49. package/dist/locales/en.js +82 -0
  50. package/dist/locales/nl.d.ts +83 -0
  51. package/dist/locales/nl.js +82 -0
  52. package/dist/locales.js +3 -2
  53. package/dist/menu/parts.d.ts +7 -7
  54. package/dist/menu/parts.js +4 -4
  55. package/dist/middle-truncate.d.ts +7 -0
  56. package/dist/middle-truncate.js +34 -0
  57. package/dist/popover/parts.d.ts +7 -7
  58. package/dist/popover/parts.js +2 -2
  59. package/dist/prefetch.js +6 -1
  60. package/dist/settings/ConfirmDialog.d.ts +4 -1
  61. package/dist/settings/ConfirmDialog.js +2 -2
  62. package/dist/settings/SettingsDialog.js +1 -1
  63. package/dist/tabs/parts.d.ts +8 -4
  64. package/dist/tabs/parts.js +84 -18
  65. package/dist/testing/dedupe.js +74 -23
  66. package/dist/theme.css +27 -10
  67. package/dist/tree/parts.d.ts +1 -1
  68. package/dist/tree/parts.js +1 -1
  69. package/dist/workspace/DropPreview.d.ts +8 -0
  70. package/dist/workspace/DropPreview.js +14 -0
  71. package/dist/workspace/SplitView.d.ts +1 -1
  72. package/dist/workspace/SplitView.js +59 -18
  73. package/dist/workspace/TabStrip.d.ts +1 -1
  74. package/dist/workspace/TabStrip.js +43 -8
  75. package/dist/workspace/Workspace.d.ts +2 -2
  76. package/dist/workspace/Workspace.js +4 -2
  77. package/dist/workspace/geometry.d.ts +11 -0
  78. package/dist/workspace/geometry.js +19 -0
  79. package/dist/workspace/preview.d.ts +4 -8
  80. package/dist/workspace/preview.js +17 -2
  81. package/package.json +2 -2
  82. package/src/ButtonGroup.tsx +1 -1
  83. package/src/Checkbox.tsx +1 -1
  84. package/src/ChoiceCards.tsx +2 -2
  85. package/src/CloseButton.tsx +1 -1
  86. package/src/ColorSwatch.tsx +1 -1
  87. package/src/ErrorBoundary.tsx +4 -4
  88. package/src/Field.tsx +4 -5
  89. package/src/IconButton.tsx +1 -1
  90. package/src/ListRow.tsx +1 -1
  91. package/src/MiddleTruncate.tsx +84 -0
  92. package/src/PanelHeader.tsx +1 -1
  93. package/src/SectionLabel.tsx +1 -1
  94. package/src/Select.tsx +1 -1
  95. package/src/SelectSearch.tsx +1 -1
  96. package/src/Slider.tsx +1 -1
  97. package/src/Surface.tsx +1 -1
  98. package/src/Switch.tsx +1 -1
  99. package/src/Toasts.tsx +5 -6
  100. package/src/Tooltip.tsx +5 -5
  101. package/src/clipboard.ts +11 -2
  102. package/src/dialog/parts.tsx +2 -2
  103. package/src/file-tree/helpers.ts +10 -0
  104. package/src/file-tree/parts.tsx +1 -1
  105. package/src/file-tree/use-slots.ts +68 -23
  106. package/src/format/datetime.ts +17 -0
  107. package/src/format/index.ts +1 -0
  108. package/src/index.ts +6 -3
  109. package/src/key-value-list/parts.tsx +1 -1
  110. package/src/locales/en.ts +82 -0
  111. package/src/locales/nl.ts +82 -0
  112. package/src/locales.ts +3 -2
  113. package/src/menu/parts.tsx +4 -4
  114. package/src/middle-truncate.ts +41 -0
  115. package/src/popover/parts.tsx +2 -2
  116. package/src/prefetch.ts +6 -1
  117. package/src/settings/ConfirmDialog.tsx +7 -1
  118. package/src/settings/SettingsDialog.tsx +1 -1
  119. package/src/tabs/parts.tsx +156 -54
  120. package/src/testing/dedupe.ts +87 -24
  121. package/src/theme.css +27 -10
  122. package/src/tree/parts.tsx +1 -1
  123. package/src/workspace/DropPreview.tsx +26 -0
  124. package/src/workspace/SplitView.tsx +58 -22
  125. package/src/workspace/TabStrip.tsx +44 -8
  126. package/src/workspace/Workspace.tsx +7 -3
  127. package/src/workspace/geometry.ts +26 -0
  128. package/src/workspace/preview.ts +22 -9
  129. package/dist/locales/en.json +0 -82
  130. package/dist/locales/nl.json +0 -82
  131. package/src/locales/en.json +0 -82
  132. package/src/locales/nl.json +0 -82
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
3
- import { useRender } from '@base-ui-components/react/use-render';
3
+ import { useRender } from '@base-ui/react/use-render';
4
4
  import { GripVertical, Maximize2, Minimize2 } from 'lucide-react';
5
5
  import clsx from 'clsx';
6
6
  import { useTranslation } from 'react-i18next';
@@ -9,7 +9,8 @@ import { WorkspaceContext } from './context.js';
9
9
  import { paneRadius, resizeSplit, splitGeometry, splitSideAt, tabGapAt } from './geometry.js';
10
10
  import { splitPanes, updateSplitLayout } from './model.js';
11
11
  import { TabStrip } from './TabStrip.js';
12
- import { dropPreviewPath, rectPreview, tabPreview } from './preview.js';
12
+ import { DropPreview } from './DropPreview.js';
13
+ import { rectDropPreview, sameDropPreview, tabDropPreview } from './preview.js';
13
14
  const ZERO = { width: 0, height: 0, left: 0, top: 0, scaleX: 1, scaleY: 1 };
14
15
  const MINIMUM = { width: 120, height: 80 };
15
16
  /* The pane's section or header under `container`, found by its data attribute rather than a selector, so no id needs escaping. */
@@ -47,6 +48,7 @@ function Divider({ divider, minimumResizeShare, onResize, onEqualize }) {
47
48
  event.preventDefault();
48
49
  cleanup.current?.();
49
50
  const element = event.currentTarget;
51
+ const view = element.ownerDocument.defaultView;
50
52
  const pointerId = event.pointerId;
51
53
  const start = horizontal ? event.clientX : event.clientY;
52
54
  const parent = element.parentElement;
@@ -54,16 +56,47 @@ function Divider({ divider, minimumResizeShare, onResize, onEqualize }) {
54
56
  const scale = horizontal ? (rect?.width ?? 1) / (parent?.clientWidth || 1) : (rect?.height ?? 1) / (parent?.clientHeight || 1);
55
57
  element.dataset.resizing = '';
56
58
  element.focus();
59
+ // A pointer reports more often than the screen draws; one resize per frame is all that shows.
60
+ let frame = 0;
61
+ let pending = null;
62
+ const apply = () => {
63
+ frame = 0;
64
+ const next = pending;
65
+ pending = null;
66
+ if (next) {
67
+ const delta = ((horizontal ? next.clientX : next.clientY) - start) / (scale || 1);
68
+ onResize(resizeSplit(divider, delta, next.altKey, 8 / (scale || 1), minimumResizeShare));
69
+ }
70
+ };
57
71
  const move = (next) => {
58
72
  if (next.pointerId !== pointerId) {
59
73
  return;
60
74
  }
61
- const delta = ((horizontal ? next.clientX : next.clientY) - start) / (scale || 1);
62
- onResize(resizeSplit(divider, delta, next.altKey, 8 / (scale || 1), minimumResizeShare));
75
+ pending = next;
76
+ if (frame !== 0) {
77
+ return;
78
+ }
79
+ if (view) {
80
+ frame = view.requestAnimationFrame(apply);
81
+ }
82
+ else {
83
+ apply();
84
+ }
85
+ };
86
+ const release = () => {
87
+ if (frame !== 0) {
88
+ view?.cancelAnimationFrame(frame);
89
+ apply();
90
+ }
91
+ end();
63
92
  };
64
93
  const end = () => {
94
+ if (frame !== 0) {
95
+ view?.cancelAnimationFrame(frame);
96
+ frame = 0;
97
+ }
65
98
  element.removeEventListener('pointermove', move);
66
- element.removeEventListener('pointerup', end);
99
+ element.removeEventListener('pointerup', release);
67
100
  element.removeEventListener('pointercancel', end);
68
101
  element.removeEventListener('lostpointercapture', end);
69
102
  element.ownerDocument.defaultView?.removeEventListener('blur', end);
@@ -76,7 +109,7 @@ function Divider({ divider, minimumResizeShare, onResize, onEqualize }) {
76
109
  cleanup.current = end;
77
110
  element.setPointerCapture(pointerId);
78
111
  element.addEventListener('pointermove', move);
79
- element.addEventListener('pointerup', end);
112
+ element.addEventListener('pointerup', release);
80
113
  element.addEventListener('pointercancel', end);
81
114
  element.addEventListener('lostpointercapture', end);
82
115
  element.ownerDocument.defaultView?.addEventListener('blur', end);
@@ -108,6 +141,14 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
108
141
  const [dragging, setDragging] = useState(false);
109
142
  const drag = useRef(null);
110
143
  const [preview, setPreview] = useState(null);
144
+ // A dragover fires many times a second; only a different shape is worth a render.
145
+ const shownPreview = useRef(null);
146
+ const showPreview = useCallback((shape) => {
147
+ if (!sameDropPreview(shownPreview.current, shape)) {
148
+ shownPreview.current = shape;
149
+ setPreview(shape);
150
+ }
151
+ }, []);
111
152
  const latest = useRef({ value, onValueChange });
112
153
  useLayoutEffect(() => {
113
154
  latest.current = { value, onValueChange };
@@ -155,8 +196,8 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
155
196
  const stopDrag = useCallback(() => {
156
197
  drag.current = null;
157
198
  setDragging(false);
158
- setPreview(null);
159
- }, []);
199
+ showPreview(null);
200
+ }, [showPreview]);
160
201
  useEffect(() => {
161
202
  if (!dragging) {
162
203
  return;
@@ -254,9 +295,9 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
254
295
  return null;
255
296
  }
256
297
  const header = Math.min(barHeight, rect.height);
257
- const strip = paneElement(root.current, 'splitHeader', rect.pane.id);
258
- const tabRects = Array.from(strip?.querySelectorAll('[data-document-tab]') ?? [], (element) => element.getBoundingClientRect());
259
298
  const inHeader = y < rect.y + header;
299
+ const strip = inHeader ? paneElement(root.current, 'splitHeader', rect.pane.id) : undefined;
300
+ const tabRects = Array.from(strip?.querySelectorAll('[data-document-tab]') ?? [], (element) => element.getBoundingClientRect());
260
301
  const side = inHeader ? undefined : (splitSideAt(rect, x, y) ?? 'center');
261
302
  const index = inHeader ? tabGapAt(tabRects, event.clientX) : undefined;
262
303
  const target = { paneId: rect.pane.id, side, index };
@@ -270,7 +311,7 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
270
311
  return null;
271
312
  }
272
313
  if (side === 'center') {
273
- return { target, shape: rectPreview(rect) };
314
+ return { target, shape: rectDropPreview(rect) };
274
315
  }
275
316
  if (side) {
276
317
  const horizontal = side === 'left' || side === 'right';
@@ -278,7 +319,7 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
278
319
  const height = horizontal ? rect.height : Math.max(0, (rect.height - gap) / 2);
279
320
  return {
280
321
  target,
281
- shape: rectPreview({
322
+ shape: rectDropPreview({
282
323
  x: side === 'right' ? rect.x + rect.width - width : rect.x,
283
324
  y: side === 'bottom' ? rect.y + rect.height - height : rect.y,
284
325
  width,
@@ -287,7 +328,7 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
287
328
  };
288
329
  }
289
330
  if (header === 0) {
290
- return { target, shape: rectPreview(rect) };
331
+ return { target, shape: rectDropPreview(rect) };
291
332
  }
292
333
  const tabWidth = Math.min(rect.width, (drag.current?.width ?? 128) / (measurement.scaleX || 1));
293
334
  const next = tabRects[index ?? tabRects.length];
@@ -295,7 +336,7 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
295
336
  const relative = Math.max(0, Math.min(rect.width - tabWidth, (left - measurement.left) / (measurement.scaleX || 1) - rect.x));
296
337
  return {
297
338
  target,
298
- shape: tabPreview({ gap: index ?? rect.pane.views.length, left: relative, tabWidth, width: rect.width, height: rect.height, barHeight: header }, { x: rect.x, y: rect.y })
339
+ shape: tabDropPreview({ gap: index ?? rect.pane.views.length, left: relative, tabWidth, width: rect.width, height: rect.height, barHeight: header }, { x: rect.x, y: rect.y })
299
340
  };
300
341
  };
301
342
  const focusDirection = (key) => {
@@ -350,11 +391,11 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
350
391
  onDragOver: (event) => {
351
392
  props.onDragOver?.(event);
352
393
  if (event.defaultPrevented) {
353
- setPreview(null);
394
+ showPreview(null);
354
395
  return;
355
396
  }
356
397
  const drop = locateDrop(event);
357
- setPreview(drop?.shape ?? null);
398
+ showPreview(drop?.shape ?? null);
358
399
  if (drop) {
359
400
  event.preventDefault();
360
401
  event.dataTransfer.dropEffect = drag.current ? 'move' : 'copy';
@@ -363,7 +404,7 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
363
404
  onDragLeave: (event) => {
364
405
  props.onDragLeave?.(event);
365
406
  if (!(event.relatedTarget instanceof Node) || !event.currentTarget.contains(event.relatedTarget)) {
366
- setPreview(null);
407
+ showPreview(null);
367
408
  }
368
409
  },
369
410
  onDrop: (event) => {
@@ -421,7 +462,7 @@ export function SplitView({ value, onValueChange, renderView, getTab, renderPane
421
462
  length: divider.axis === 'horizontal' ? measurement.width : measurement.height,
422
463
  gap
423
464
  }
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)}")` } }) }))] }))
465
+ : { type: 'equalize', splitId: divider.splitId, index: divider.index }) }, `${divider.splitId}:${divider.index}`))), _jsx(DropPreview, { shape: preview })] }))
425
466
  }
426
467
  });
427
468
  }
@@ -1,5 +1,5 @@
1
1
  import { type DragEvent, type MouseEvent, type ReactNode } from 'react';
2
- import { useRender } from '@base-ui-components/react/use-render';
2
+ import { useRender } from '@base-ui/react/use-render';
3
3
  import { type IconButtonProps } from '../IconButton.tsx';
4
4
  export interface TabStripItem {
5
5
  id: string;
@@ -1,13 +1,13 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
3
- import { useRender } from '@base-ui-components/react/use-render';
3
+ import { useRender } from '@base-ui/react/use-render';
4
4
  import { Pin, X } from 'lucide-react';
5
5
  import clsx from 'clsx';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Icon } from '../Icon.js';
8
8
  import { IconButton } from '../IconButton.js';
9
9
  import { Tooltip } from '../Tooltip.js';
10
- import { tabGapAt } from './geometry.js';
10
+ import { tabGapAt, tabRevealScrollLeft } from './geometry.js';
11
11
  /* Where an arrow, Home or End moves the selection from `index`, wrapping at either end; null for any other key. */
12
12
  function tabIndexForKey(key, index, count) {
13
13
  switch (key) {
@@ -62,20 +62,44 @@ export function TabStrip({ items, value, onValueChange, renderTab, focused = tru
62
62
  setInsertion(after?.offsetLeft ?? (last ? last.offsetLeft + last.offsetWidth : 0));
63
63
  }
64
64
  }, [insertAt]);
65
- useLayoutEffect(measure);
65
+ // Stays set after the active tab changes, as its width can settle frames later; a person's own scroll ends it.
66
+ const revealing = useRef(false);
67
+ const reveal = () => {
68
+ const element = strip.current;
69
+ const selected = element?.querySelector('[aria-selected="true"]');
70
+ if (!revealing.current || !element || !selected) {
71
+ return;
72
+ }
73
+ const container = selected.closest('[data-document-tab]');
74
+ const tab = container && element.contains(container) ? container : selected;
75
+ const fade = parseFloat(getComputedStyle(element).getPropertyValue('--fade-size')) || 0;
76
+ const bounds = element.getBoundingClientRect();
77
+ const rect = tab.getBoundingClientRect();
78
+ const left = tabRevealScrollLeft(element, rect.left - bounds.left - element.clientLeft + element.scrollLeft, rect.width, fade);
79
+ if (left !== element.scrollLeft) {
80
+ element.scrollLeft = left;
81
+ }
82
+ };
83
+ useLayoutEffect(() => {
84
+ revealing.current = true;
85
+ }, [value]);
86
+ useLayoutEffect(() => {
87
+ reveal();
88
+ measure();
89
+ });
66
90
  useEffect(() => {
67
91
  const element = strip.current;
68
92
  if (!element || typeof ResizeObserver === 'undefined') {
69
93
  return;
70
94
  }
71
- const observer = new ResizeObserver(measure);
95
+ const observer = new ResizeObserver(() => {
96
+ reveal();
97
+ measure();
98
+ });
72
99
  observer.observe(element);
73
100
  element.querySelectorAll('[data-document-tab]').forEach((tab) => observer.observe(tab));
74
101
  return () => observer.disconnect();
75
102
  });
76
- useEffect(() => {
77
- strip.current?.querySelector('[aria-selected="true"]')?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
78
- }, [value]);
79
103
  const gap = (event) => {
80
104
  const rects = Array.from(event.currentTarget.querySelectorAll('[data-document-tab]'), (tab) => tab.getBoundingClientRect());
81
105
  return tabGapAt(rects, event.clientX);
@@ -110,10 +134,21 @@ export function TabStrip({ items, value, onValueChange, renderTab, focused = tru
110
134
  },
111
135
  onWheel: (event) => {
112
136
  props.onWheel?.(event);
113
- if (!event.defaultPrevented && !event.deltaX && event.deltaY) {
137
+ if (event.defaultPrevented) {
138
+ return;
139
+ }
140
+ revealing.current = false;
141
+ if (!event.deltaX && event.deltaY) {
114
142
  event.currentTarget.scrollLeft += event.deltaY;
115
143
  }
116
144
  },
145
+ onPointerDown: (event) => {
146
+ props.onPointerDown?.(event);
147
+ // A press on the strip itself grabs its scrollbar, and a touch may pan it.
148
+ if (event.target === event.currentTarget || event.pointerType === 'touch') {
149
+ revealing.current = false;
150
+ }
151
+ },
117
152
  onDragOver: (event) => {
118
153
  props.onDragOver?.(event);
119
154
  if (!event.defaultPrevented) {
@@ -1,5 +1,5 @@
1
- import type { ReactNode } from 'react';
2
- import { useRender } from '@base-ui-components/react/use-render';
1
+ import { type ReactNode } from 'react';
2
+ import { useRender } from '@base-ui/react/use-render';
3
3
  import { type WorkspaceLayout } from './context.ts';
4
4
  export interface WorkspaceProps extends useRender.ComponentProps<'div'> {
5
5
  layout?: WorkspaceLayout;
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
2
3
  import clsx from 'clsx';
3
- import { useRender } from '@base-ui-components/react/use-render';
4
+ import { useRender } from '@base-ui/react/use-render';
4
5
  import { WorkspaceContext } from './context.js';
5
6
  const isPresent = (node) => node !== undefined && node !== null && node !== false;
6
7
  export function Workspace({ layout = 'standard', toolbar, sidebar, sidePanel, sidebarWidth = 248, sidePanelWidth = 320, sidebarOpen, sidePanelOpen, children, className, ref, render, style, ...props }) {
@@ -24,5 +25,6 @@ export function Workspace({ layout = 'standard', toolbar, sidebar, sidePanel, si
24
25
  children: (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "ade-workspace-sidebar", "data-auto": sidebarWidth === 'auto' || undefined, style: { width: sidebarWidth }, children: sidebar })), _jsxs("div", { className: "ade-workspace-center", children: [hasToolbar && _jsx("div", { className: "ade-workspace-toolbar", children: toolbar }), _jsx("div", { className: "ade-workspace-content", children: children })] }), hasPanel && (_jsx("aside", { className: "ade-workspace-side-panel", "data-auto": sidePanelWidth === 'auto' || undefined, style: { width: sidePanelWidth }, children: sidePanel }))] }))
25
26
  }
26
27
  });
27
- return _jsx(WorkspaceContext, { value: { layout, edges: { top: !hasToolbar, right: !rightOpen, bottom: true, left: !leftOpen } }, children: element });
28
+ const context = useMemo(() => ({ layout, edges: { top: !hasToolbar, right: !rightOpen, bottom: true, left: !leftOpen } }), [layout, hasToolbar, rightOpen, leftOpen]);
29
+ return _jsx(WorkspaceContext, { value: context, children: element });
28
30
  }
@@ -37,3 +37,14 @@ export declare function tabGapAt(rects: readonly {
37
37
  left: number;
38
38
  right: number;
39
39
  }[], x: number): number;
40
+ export interface TabScrollView {
41
+ scrollLeft: number;
42
+ clientWidth: number;
43
+ scrollWidth: number;
44
+ }
45
+ /**
46
+ * The scrollLeft that brings a tab fully into view and clear of the edge fades, or the current one
47
+ * when it already is. The strip only fades an edge it can still scroll toward, so a fade is counted
48
+ * only where the scroll position leaves one. A tab wider than the room between the fades shows its start.
49
+ */
50
+ export declare function tabRevealScrollLeft(view: TabScrollView, start: number, width: number, fade: number): number;
@@ -151,3 +151,22 @@ export function tabGapAt(rects, x) {
151
151
  const index = rects.findIndex((rect) => x < (rect.left + rect.right) / 2);
152
152
  return index < 0 ? rects.length : index;
153
153
  }
154
+ /**
155
+ * The scrollLeft that brings a tab fully into view and clear of the edge fades, or the current one
156
+ * when it already is. The strip only fades an edge it can still scroll toward, so a fade is counted
157
+ * only where the scroll position leaves one. A tab wider than the room between the fades shows its start.
158
+ */
159
+ export function tabRevealScrollLeft(view, start, width, fade) {
160
+ const max = Math.max(0, view.scrollWidth - view.clientWidth);
161
+ const end = start + width;
162
+ const visibleStart = (left) => left + (left > 0 ? fade : 0);
163
+ const visibleEnd = (left) => left + view.clientWidth - (left < max - 1 ? fade : 0);
164
+ const clamp = (left) => Math.round(Math.min(max, Math.max(0, left)));
165
+ if (start >= visibleStart(view.scrollLeft) && end <= visibleEnd(view.scrollLeft)) {
166
+ return view.scrollLeft;
167
+ }
168
+ if (start < visibleStart(view.scrollLeft) || width > view.clientWidth - 2 * fade) {
169
+ return clamp(start - fade);
170
+ }
171
+ return clamp(end + fade - view.clientWidth);
172
+ }
@@ -1,3 +1,4 @@
1
+ import type { SplitRect } from './geometry.ts';
1
2
  export declare const TAB_TOP_INSET = 4;
2
3
  export declare const TAB_RADIUS = 6;
3
4
  export declare const BODY_RADIUS = 4;
@@ -13,17 +14,12 @@ export interface DropPreviewShape {
13
14
  tabTop: number;
14
15
  radius: number;
15
16
  }
16
- export interface PreviewRect {
17
- x: number;
18
- y: number;
19
- width: number;
20
- height: number;
21
- }
22
- export declare function rectPreview({ x, y, width, height }: PreviewRect): DropPreviewShape;
23
- export declare function tabPreview(drop: TabDrop, origin: {
17
+ export declare function rectDropPreview({ x, y, width, height }: SplitRect): DropPreviewShape;
18
+ export declare function tabDropPreview(drop: TabDrop, origin: {
24
19
  x: number;
25
20
  y: number;
26
21
  }): DropPreviewShape;
22
+ export declare function sameDropPreview(left: DropPreviewShape | null, right: DropPreviewShape | null): boolean;
27
23
  export declare function dropPreviewPath({ x, y, width, height, barHeight, tabLeft, tabWidth, tabTop, radius }: DropPreviewShape, inset?: number): string;
28
24
  export interface TabDrop {
29
25
  gap: number;
@@ -12,11 +12,11 @@ function coordinate(value) {
12
12
  * The preview for a split or a trade: the rectangle as a tab with no height, parked just inside its
13
13
  * top left corner so the corner keeps its radius. It is where a tab target grows out of the rectangle.
14
14
  */
15
- export function rectPreview({ x, y, width, height }) {
15
+ export function rectDropPreview({ x, y, width, height }) {
16
16
  return { x, y, width, height, barHeight: 0, tabLeft: x + BODY_RADIUS, tabWidth: 0, tabTop: y, radius: TAB_RADIUS };
17
17
  }
18
18
  /* The preview for a tab: the tab and the body of the cell it lands in, `origin` being the cell's top left in the grid. */
19
- export function tabPreview(drop, origin) {
19
+ export function tabDropPreview(drop, origin) {
20
20
  return {
21
21
  x: origin.x,
22
22
  y: origin.y,
@@ -29,6 +29,21 @@ export function tabPreview(drop, origin) {
29
29
  radius: TAB_RADIUS
30
30
  };
31
31
  }
32
+ /* Two shapes that draw the same outline, so a drag that stays in one place can skip a render. */
33
+ export function sameDropPreview(left, right) {
34
+ if (left === null || right === null) {
35
+ return left === right;
36
+ }
37
+ return (left.x === right.x &&
38
+ left.y === right.y &&
39
+ left.width === right.width &&
40
+ left.height === right.height &&
41
+ left.barHeight === right.barHeight &&
42
+ left.tabLeft === right.tabLeft &&
43
+ left.tabWidth === right.tabWidth &&
44
+ left.tabTop === right.tabTop &&
45
+ left.radius === right.radius);
46
+ }
32
47
  /*
33
48
  * The outline of a tab grown into the body under it, as one closed path so no line runs between the
34
49
  * two. The commands are the same whatever the numbers are (an arc of radius 0 is a straight line), so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/ui",
3
- "version": "0.23.1",
3
+ "version": "0.24.1",
4
4
  "description": "React components, a theme, formatters and a settings dialog for desktop-grade apps on React 19, Base UI and Tailwind 4.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
@@ -65,7 +65,7 @@
65
65
  "test": "bun test"
66
66
  },
67
67
  "dependencies": {
68
- "@base-ui-components/react": "^1.0.0-rc.0",
68
+ "@base-ui/react": "^1.9.0",
69
69
  "@pierre/trees": "1.0.0-beta.6",
70
70
  "clsx": "^2.1.1",
71
71
  "lucide-react": "^1.47.0",
@@ -1,5 +1,5 @@
1
1
  import clsx from 'clsx';
2
- import { useRender } from '@base-ui-components/react/use-render';
2
+ import { useRender } from '@base-ui/react/use-render';
3
3
 
4
4
  export type ButtonGroupProps = useRender.ComponentProps<'div'>;
5
5
 
package/src/Checkbox.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { Check, Minus } from 'lucide-react';
4
- import { Checkbox as BaseCheckbox } from '@base-ui-components/react/checkbox';
4
+ import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
5
5
  import { Icon } from './Icon.tsx';
6
6
 
7
7
  export interface CheckboxProps {
@@ -1,8 +1,8 @@
1
1
  import { Fragment, useId, type ReactNode, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import type { LucideIcon } from 'lucide-react';
4
- import { Radio } from '@base-ui-components/react/radio';
5
- import { RadioGroup } from '@base-ui-components/react/radio-group';
4
+ import { Radio } from '@base-ui/react/radio';
5
+ import { RadioGroup } from '@base-ui/react/radio-group';
6
6
  import { Icon } from './Icon.tsx';
7
7
 
8
8
  export interface Choice<Value extends string | number> {
@@ -1,6 +1,6 @@
1
1
  import type { Ref } from 'react';
2
2
  import { X } from 'lucide-react';
3
- import { Dialog } from '@base-ui-components/react/dialog';
3
+ import { Dialog } from '@base-ui/react/dialog';
4
4
  import { IconButton, type IconButtonSize } from './IconButton.tsx';
5
5
  import type { Shortcut } from './shortcut.ts';
6
6
 
@@ -1,7 +1,7 @@
1
1
  import type { ComponentProps } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { Check } from 'lucide-react';
4
- import { useRender } from '@base-ui-components/react/use-render';
4
+ import { useRender } from '@base-ui/react/use-render';
5
5
  import { Icon } from './Icon.tsx';
6
6
 
7
7
  /* `focus-ring` for the swatch that is a menu item rather than a button, which the keyboard reaches all the same. */
@@ -75,18 +75,18 @@ function ErrorFallback({ label, error, componentStack, compact, reload, classNam
75
75
  * resets only while it holds an error: `resetKeys` for what the children draw from.
76
76
  */
77
77
  export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
78
- state: ErrorBoundaryState = CLEAR;
78
+ override state: ErrorBoundaryState = CLEAR;
79
79
 
80
80
  static getDerivedStateFromError(error: unknown): Partial<ErrorBoundaryState> {
81
81
  return { error, failed: true };
82
82
  }
83
83
 
84
- componentDidCatch(error: unknown, info: ErrorInfo): void {
84
+ override componentDidCatch(error: unknown, info: ErrorInfo): void {
85
85
  console.error(`${this.props.label}:`, error, info.componentStack);
86
86
  this.setState({ componentStack: info.componentStack ?? null });
87
87
  }
88
88
 
89
- componentDidUpdate(previous: ErrorBoundaryProps): void {
89
+ override componentDidUpdate(previous: ErrorBoundaryProps): void {
90
90
  if (shouldReset(this.state.failed, previous.resetKeys ?? [], this.props.resetKeys ?? [])) {
91
91
  this.reset();
92
92
  }
@@ -96,7 +96,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
96
96
  this.setState(CLEAR);
97
97
  }
98
98
 
99
- render(): ReactNode {
99
+ override render(): ReactNode {
100
100
  if (!this.state.failed) {
101
101
  return this.props.children;
102
102
  }
package/src/Field.tsx CHANGED
@@ -1,6 +1,6 @@
1
- import { useId, type ReactNode, type Ref } from 'react';
1
+ import { useId, useMemo, type ReactNode, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
- import { useRender } from '@base-ui-components/react/use-render';
3
+ import { useRender } from '@base-ui/react/use-render';
4
4
  import { FieldContext } from './field-context.ts';
5
5
  import { SectionLabel } from './SectionLabel.tsx';
6
6
 
@@ -85,6 +85,7 @@ export function Field({ label, hint, error, group = false, orientation = 'vertic
85
85
  const hasHint = isShown(hint);
86
86
  const hasError = isShown(error);
87
87
  const describedBy = [hasHint ? `${id}-hint` : null, hasError ? `${id}-error` : null].filter((part) => part !== null).join(' ') || undefined;
88
+ const control = useMemo(() => (group ? null : { id: `${id}-control`, describedBy, invalid: hasError }), [group, id, describedBy, hasError]);
88
89
  return (
89
90
  <div
90
91
  ref={ref}
@@ -99,9 +100,7 @@ export function Field({ label, hint, error, group = false, orientation = 'vertic
99
100
  </FieldLabel>
100
101
  )}
101
102
  {/* A group describes itself, so an input inside it stays unconnected rather than taking an id nothing points at. */}
102
- <FieldContext value={group ? null : { id: `${id}-control`, describedBy, invalid: hasError }}>
103
- {horizontal ? <div className="col-start-2 min-w-0">{children}</div> : children}
104
- </FieldContext>
103
+ <FieldContext value={control}>{horizontal ? <div className="col-start-2 min-w-0">{children}</div> : children}</FieldContext>
105
104
  {/* The stack already puts 6px above each line, so neither takes the margin a standalone hint has. */}
106
105
  {hasHint && (
107
106
  <p id={`${id}-hint`} className={clsx(HINT, horizontal && 'col-start-2')}>
@@ -1,7 +1,7 @@
1
1
  import type { ComponentProps, ReactElement, ReactNode } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import type { LucideIcon } from 'lucide-react';
4
- import { useRender } from '@base-ui-components/react/use-render';
4
+ import { useRender } from '@base-ui/react/use-render';
5
5
  import { Icon } from './Icon.tsx';
6
6
  import type { Shortcut } from './shortcut.ts';
7
7
  import { Spinner } from './Spinner.tsx';
package/src/ListRow.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import clsx from 'clsx';
2
- import { useRender } from '@base-ui-components/react/use-render';
2
+ import { useRender } from '@base-ui/react/use-render';
3
3
 
4
4
  const VARIANT = {
5
5
  /* A row of a list that reads as a table and fills the width of its panel: commits, processes,
@@ -0,0 +1,84 @@
1
+ import { useLayoutEffect, useMemo, useState, type Ref } from 'react';
2
+ import clsx from 'clsx';
3
+ import { fitMiddle, type MiddleCut } from './middle-truncate.ts';
4
+ import { mergeRefs } from './merge-refs.ts';
5
+ import { Tooltip } from './Tooltip.tsx';
6
+
7
+ export interface MiddleTruncateProps {
8
+ text: string;
9
+ /* The whole text on hover and focus while it is cut. Off where the caller's own tooltip already says it. */
10
+ tooltip?: boolean;
11
+ className?: string;
12
+ ref?: Ref<HTMLSpanElement>;
13
+ }
14
+
15
+ let canvas: HTMLCanvasElement | null = null;
16
+
17
+ /* How wide a text is drawn in the face of `element`, or null where the page cannot tell (no canvas). */
18
+ function textMeasure(element: HTMLElement): ((text: string) => number) | null {
19
+ const view = element.ownerDocument.defaultView;
20
+ canvas ??= element.ownerDocument.createElement('canvas');
21
+ const context = canvas.getContext('2d');
22
+ if (context === null || view === null) {
23
+ return null;
24
+ }
25
+ const style = view.getComputedStyle(element);
26
+ context.font = style.font || `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
27
+ if ('letterSpacing' in context) {
28
+ context.letterSpacing = style.letterSpacing === 'normal' ? '0px' : style.letterSpacing;
29
+ }
30
+ return (text) => context.measureText(text).width;
31
+ }
32
+
33
+ function sameCut(first: MiddleCut | null, second: MiddleCut | null): boolean {
34
+ return first?.head === second?.head && first?.tail === second?.tail;
35
+ }
36
+
37
+ /*
38
+ * A path or a branch name on one line that keeps its start and its end and cuts the middle. It asks
39
+ * for the width of the whole text and measures what it gets. The part it cuts stays in the element
40
+ * at no width, so selecting, copying and finding read the whole text.
41
+ */
42
+ export function MiddleTruncate({ text, tooltip = true, className, ref }: MiddleTruncateProps) {
43
+ const [element, setElement] = useState<HTMLSpanElement | null>(null);
44
+ const [cut, setCut] = useState<MiddleCut | null>(null);
45
+ const rootRef = useMemo(() => mergeRefs(setElement, ref), [ref]);
46
+
47
+ useLayoutEffect(() => {
48
+ if (element === null) {
49
+ return;
50
+ }
51
+ const update = (): void => {
52
+ const measure = textMeasure(element);
53
+ // Layout rounds what a canvas measures, so a text a fraction wider than its box still fits.
54
+ const next = measure === null ? null : fitMiddle(text, element.getBoundingClientRect().width + 0.5, measure);
55
+ setCut((current) => (sameCut(current, next) ? current : next));
56
+ };
57
+ update();
58
+ if (typeof ResizeObserver === 'undefined') {
59
+ return;
60
+ }
61
+ const observer = new ResizeObserver(update);
62
+ observer.observe(element);
63
+ return () => observer.disconnect();
64
+ }, [element, text]);
65
+
66
+ const content = (
67
+ <span ref={rootRef} className={clsx('middle-truncate', className)} data-text={text}>
68
+ {cut === null ? (
69
+ text
70
+ ) : (
71
+ <>
72
+ <span className="middle-truncate-head">{cut.head}</span>
73
+ <span className="middle-truncate-middle">{cut.middle}</span>
74
+ {cut.tail}
75
+ </>
76
+ )}
77
+ </span>
78
+ );
79
+ // Only a cut text has more to say, and a long list of whole ones keeps no hover handlers.
80
+ if (cut === null || !tooltip) {
81
+ return content;
82
+ }
83
+ return <Tooltip label={text}>{content}</Tooltip>;
84
+ }