@adecore/ui 0.23.0 → 0.24.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.
Files changed (129) 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 +24 -8
  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 +1 -1
  75. package/dist/workspace/Workspace.d.ts +2 -2
  76. package/dist/workspace/Workspace.js +4 -2
  77. package/dist/workspace/preview.d.ts +4 -8
  78. package/dist/workspace/preview.js +17 -2
  79. package/package.json +2 -2
  80. package/src/ButtonGroup.tsx +1 -1
  81. package/src/Checkbox.tsx +1 -1
  82. package/src/ChoiceCards.tsx +2 -2
  83. package/src/CloseButton.tsx +1 -1
  84. package/src/ColorSwatch.tsx +1 -1
  85. package/src/ErrorBoundary.tsx +4 -4
  86. package/src/Field.tsx +4 -5
  87. package/src/IconButton.tsx +1 -1
  88. package/src/ListRow.tsx +1 -1
  89. package/src/MiddleTruncate.tsx +84 -0
  90. package/src/PanelHeader.tsx +1 -1
  91. package/src/SectionLabel.tsx +1 -1
  92. package/src/Select.tsx +1 -1
  93. package/src/SelectSearch.tsx +1 -1
  94. package/src/Slider.tsx +1 -1
  95. package/src/Surface.tsx +1 -1
  96. package/src/Switch.tsx +1 -1
  97. package/src/Toasts.tsx +5 -6
  98. package/src/Tooltip.tsx +5 -5
  99. package/src/clipboard.ts +11 -2
  100. package/src/dialog/parts.tsx +2 -2
  101. package/src/file-tree/helpers.ts +10 -0
  102. package/src/file-tree/parts.tsx +1 -1
  103. package/src/file-tree/use-slots.ts +68 -23
  104. package/src/format/datetime.ts +17 -0
  105. package/src/format/index.ts +1 -0
  106. package/src/index.ts +6 -3
  107. package/src/key-value-list/parts.tsx +1 -1
  108. package/src/locales/en.ts +82 -0
  109. package/src/locales/nl.ts +82 -0
  110. package/src/locales.ts +3 -2
  111. package/src/menu/parts.tsx +4 -4
  112. package/src/middle-truncate.ts +41 -0
  113. package/src/popover/parts.tsx +2 -2
  114. package/src/prefetch.ts +6 -1
  115. package/src/settings/ConfirmDialog.tsx +7 -1
  116. package/src/settings/SettingsDialog.tsx +1 -1
  117. package/src/tabs/parts.tsx +156 -54
  118. package/src/testing/dedupe.ts +87 -24
  119. package/src/theme.css +24 -8
  120. package/src/tree/parts.tsx +1 -1
  121. package/src/workspace/DropPreview.tsx +26 -0
  122. package/src/workspace/SplitView.tsx +58 -22
  123. package/src/workspace/TabStrip.tsx +1 -1
  124. package/src/workspace/Workspace.tsx +7 -3
  125. package/src/workspace/preview.ts +22 -9
  126. package/dist/locales/en.json +0 -82
  127. package/dist/locales/nl.json +0 -82
  128. package/src/locales/en.json +0 -82
  129. 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,6 +1,6 @@
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';
@@ -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
  }
@@ -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.0",
3
+ "version": "0.24.0",
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
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } 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 { SectionLabel } from './SectionLabel.tsx';
5
5
 
6
6
  export type PanelHeaderProps = useRender.ComponentProps<'header'> & {
@@ -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
  /* A label takes the line height of the row it sits in, which is why the size carries `/[inherit]`. */
5
5
  const SECTION_LABEL = 'text-xs/[inherit] font-medium text-text-faint';
package/src/Select.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Ref } from 'react';
2
- import { Select as BaseSelect } from '@base-ui-components/react/select';
2
+ import { Select as BaseSelect } from '@base-ui/react/select';
3
3
  import clsx from 'clsx';
4
4
  import { ChevronDown } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react';
2
- import { Combobox } from '@base-ui-components/react/combobox';
2
+ import { Combobox } from '@base-ui/react/combobox';
3
3
  import clsx from 'clsx';
4
4
  import { ChevronDown, Search } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
package/src/Slider.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useId, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
- import { Slider as BaseSlider } from '@base-ui-components/react/slider';
3
+ import { Slider as BaseSlider } from '@base-ui/react/slider';
4
4
  import { formatFixed } from './format/number.ts';
5
5
  import { placesOf } from './step-places.ts';
6
6
 
package/src/Surface.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 FLOAT = 'border border-border bg-[color-mix(in_srgb,var(--surface-raised)_88%,transparent)] shadow-float backdrop-blur-[14px]';
5
5
 
package/src/Switch.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Ref } from 'react';
2
2
  import clsx from 'clsx';
3
- import { Switch as BaseSwitch } from '@base-ui-components/react/switch';
3
+ import { Switch as BaseSwitch } from '@base-ui/react/switch';
4
4
 
5
5
  export interface SwitchProps {
6
6
  checked: boolean;
package/src/Toasts.tsx CHANGED
@@ -2,6 +2,7 @@ import { useState, type ReactNode, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { CircleAlert, CircleCheck, Trash, X } from 'lucide-react';
5
+ import { Button } from './Button.tsx';
5
6
  import { Icon } from './Icon.tsx';
6
7
  import { IconButton } from './IconButton.tsx';
7
8
  import { Kbd } from './Kbd.tsx';
@@ -57,9 +58,9 @@ function TimerRing({ deadline, onDismiss }: { deadline: ToastDeadline; onDismiss
57
58
  function ActionButton({ action }: { action: ToastAction }) {
58
59
  return (
59
60
  <span className="flex items-center gap-2">
60
- <button className="rounded-sm text-sm font-semibold text-text hover:text-text-muted" onClick={action.run}>
61
+ <Button size="xs" onClick={action.run}>
61
62
  {action.label}
62
- </button>
63
+ </Button>
63
64
  {action.shortcut && <Kbd shortcut={action.shortcut} className="font-sans text-xs text-text-faint" />}
64
65
  </span>
65
66
  );
@@ -90,7 +91,7 @@ function ToastCard<T extends Toast>({ toast, footer, onDismiss }: { toast: T; fo
90
91
  )}
91
92
  {/* Two or more would squeeze the title beside them, so they get a line of their own. */}
92
93
  {actions.length > 1 && (
93
- <span className="mt-1 flex flex-wrap items-center gap-x-4 gap-y-1">
94
+ <span className="mt-1 -ml-2 flex flex-wrap items-center gap-x-2 gap-y-1">
94
95
  {actions.map((action) => (
95
96
  <ActionButton key={action.label} action={action} />
96
97
  ))}
@@ -133,9 +134,7 @@ export interface ToastsProps<T extends Toast> {
133
134
  */
134
135
  export function Toasts<T extends Toast>({ store, footer, className, ref }: ToastsProps<T>) {
135
136
  const toasts = store((state) => state.toasts);
136
- if (toasts.length === 0) {
137
- return null;
138
- }
137
+ // Stays mounted with nothing in it: a screen reader skips what a live region says the moment it is inserted.
139
138
  return (
140
139
  <div
141
140
  ref={ref}