@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.
- package/dist/ButtonGroup.d.ts +1 -1
- package/dist/ButtonGroup.js +1 -1
- package/dist/Checkbox.js +1 -1
- package/dist/ChoiceCards.js +2 -2
- package/dist/CloseButton.js +1 -1
- package/dist/ColorSwatch.d.ts +1 -1
- package/dist/ColorSwatch.js +1 -1
- package/dist/Field.d.ts +1 -1
- package/dist/Field.js +4 -3
- package/dist/IconButton.d.ts +1 -1
- package/dist/IconButton.js +1 -1
- package/dist/ListRow.d.ts +1 -1
- package/dist/ListRow.js +1 -1
- package/dist/MiddleTruncate.d.ts +8 -0
- package/dist/MiddleTruncate.js +59 -0
- package/dist/PanelHeader.d.ts +1 -1
- package/dist/PanelHeader.js +1 -1
- package/dist/SectionLabel.d.ts +1 -1
- package/dist/SectionLabel.js +1 -1
- package/dist/Select.js +1 -1
- package/dist/SelectSearch.js +1 -1
- package/dist/Slider.js +1 -1
- package/dist/Surface.d.ts +1 -1
- package/dist/Surface.js +1 -1
- package/dist/Switch.js +1 -1
- package/dist/Toasts.d.ts +1 -1
- package/dist/Toasts.js +4 -5
- package/dist/Tooltip.d.ts +1 -1
- package/dist/Tooltip.js +2 -2
- package/dist/clipboard.d.ts +1 -1
- package/dist/clipboard.js +12 -2
- package/dist/dialog/parts.d.ts +3 -3
- package/dist/dialog/parts.js +2 -2
- package/dist/file-tree/helpers.d.ts +1 -0
- package/dist/file-tree/helpers.js +9 -0
- package/dist/file-tree/parts.d.ts +1 -1
- package/dist/file-tree/parts.js +1 -1
- package/dist/file-tree/use-slots.d.ts +8 -0
- package/dist/file-tree/use-slots.js +64 -22
- package/dist/format/datetime.d.ts +1 -0
- package/dist/format/datetime.js +15 -0
- package/dist/format/index.d.ts +1 -1
- package/dist/format/index.js +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -1
- package/dist/key-value-list/parts.d.ts +1 -1
- package/dist/key-value-list/parts.js +1 -1
- package/dist/locales/en.d.ts +83 -0
- package/dist/locales/en.js +82 -0
- package/dist/locales/nl.d.ts +83 -0
- package/dist/locales/nl.js +82 -0
- package/dist/locales.js +3 -2
- package/dist/menu/parts.d.ts +7 -7
- package/dist/menu/parts.js +4 -4
- package/dist/middle-truncate.d.ts +7 -0
- package/dist/middle-truncate.js +34 -0
- package/dist/popover/parts.d.ts +7 -7
- package/dist/popover/parts.js +2 -2
- package/dist/prefetch.js +6 -1
- package/dist/settings/ConfirmDialog.d.ts +4 -1
- package/dist/settings/ConfirmDialog.js +2 -2
- package/dist/settings/SettingsDialog.js +1 -1
- package/dist/tabs/parts.d.ts +8 -4
- package/dist/tabs/parts.js +84 -18
- package/dist/testing/dedupe.js +74 -23
- package/dist/theme.css +27 -10
- package/dist/tree/parts.d.ts +1 -1
- package/dist/tree/parts.js +1 -1
- package/dist/workspace/DropPreview.d.ts +8 -0
- package/dist/workspace/DropPreview.js +14 -0
- package/dist/workspace/SplitView.d.ts +1 -1
- package/dist/workspace/SplitView.js +59 -18
- package/dist/workspace/TabStrip.d.ts +1 -1
- package/dist/workspace/TabStrip.js +43 -8
- package/dist/workspace/Workspace.d.ts +2 -2
- package/dist/workspace/Workspace.js +4 -2
- package/dist/workspace/geometry.d.ts +11 -0
- package/dist/workspace/geometry.js +19 -0
- package/dist/workspace/preview.d.ts +4 -8
- package/dist/workspace/preview.js +17 -2
- package/package.json +2 -2
- package/src/ButtonGroup.tsx +1 -1
- package/src/Checkbox.tsx +1 -1
- package/src/ChoiceCards.tsx +2 -2
- package/src/CloseButton.tsx +1 -1
- package/src/ColorSwatch.tsx +1 -1
- package/src/ErrorBoundary.tsx +4 -4
- package/src/Field.tsx +4 -5
- package/src/IconButton.tsx +1 -1
- package/src/ListRow.tsx +1 -1
- package/src/MiddleTruncate.tsx +84 -0
- package/src/PanelHeader.tsx +1 -1
- package/src/SectionLabel.tsx +1 -1
- package/src/Select.tsx +1 -1
- package/src/SelectSearch.tsx +1 -1
- package/src/Slider.tsx +1 -1
- package/src/Surface.tsx +1 -1
- package/src/Switch.tsx +1 -1
- package/src/Toasts.tsx +5 -6
- package/src/Tooltip.tsx +5 -5
- package/src/clipboard.ts +11 -2
- package/src/dialog/parts.tsx +2 -2
- package/src/file-tree/helpers.ts +10 -0
- package/src/file-tree/parts.tsx +1 -1
- package/src/file-tree/use-slots.ts +68 -23
- package/src/format/datetime.ts +17 -0
- package/src/format/index.ts +1 -0
- package/src/index.ts +6 -3
- package/src/key-value-list/parts.tsx +1 -1
- package/src/locales/en.ts +82 -0
- package/src/locales/nl.ts +82 -0
- package/src/locales.ts +3 -2
- package/src/menu/parts.tsx +4 -4
- package/src/middle-truncate.ts +41 -0
- package/src/popover/parts.tsx +2 -2
- package/src/prefetch.ts +6 -1
- package/src/settings/ConfirmDialog.tsx +7 -1
- package/src/settings/SettingsDialog.tsx +1 -1
- package/src/tabs/parts.tsx +156 -54
- package/src/testing/dedupe.ts +87 -24
- package/src/theme.css +27 -10
- package/src/tree/parts.tsx +1 -1
- package/src/workspace/DropPreview.tsx +26 -0
- package/src/workspace/SplitView.tsx +58 -22
- package/src/workspace/TabStrip.tsx +44 -8
- package/src/workspace/Workspace.tsx +7 -3
- package/src/workspace/geometry.ts +26 -0
- package/src/workspace/preview.ts +22 -9
- package/dist/locales/en.json +0 -82
- package/dist/locales/nl.json +0 -82
- package/src/locales/en.json +0 -82
- 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
|
|
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 {
|
|
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
|
-
|
|
62
|
-
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
394
|
+
showPreview(null);
|
|
354
395
|
return;
|
|
355
396
|
}
|
|
356
397
|
const drop = locateDrop(event);
|
|
357
|
-
|
|
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
|
-
|
|
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}`))),
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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
|
|
2
|
-
import { useRender } from '@base-ui
|
|
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
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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",
|
package/src/ButtonGroup.tsx
CHANGED
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
|
|
4
|
+
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
|
|
5
5
|
import { Icon } from './Icon.tsx';
|
|
6
6
|
|
|
7
7
|
export interface CheckboxProps {
|
package/src/ChoiceCards.tsx
CHANGED
|
@@ -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
|
|
5
|
-
import { RadioGroup } from '@base-ui
|
|
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> {
|
package/src/CloseButton.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Ref } from 'react';
|
|
2
2
|
import { X } from 'lucide-react';
|
|
3
|
-
import { Dialog } from '@base-ui
|
|
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
|
|
package/src/ColorSwatch.tsx
CHANGED
|
@@ -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
|
|
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. */
|
package/src/ErrorBoundary.tsx
CHANGED
|
@@ -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
|
|
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={
|
|
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')}>
|
package/src/IconButton.tsx
CHANGED
|
@@ -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
|
|
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
|
@@ -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
|
+
}
|