@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.
- 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 +24 -8
- 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 +1 -1
- package/dist/workspace/Workspace.d.ts +2 -2
- package/dist/workspace/Workspace.js +4 -2
- 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 +24 -8
- 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 +1 -1
- package/src/workspace/Workspace.tsx +7 -3
- 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,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
|
|
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
|
|
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
|
}
|
|
@@ -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.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
|
|
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
|
+
}
|
package/src/PanelHeader.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } 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 { SectionLabel } from './SectionLabel.tsx';
|
|
5
5
|
|
|
6
6
|
export type PanelHeaderProps = useRender.ComponentProps<'header'> & {
|
package/src/SectionLabel.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import { useRender } from '@base-ui
|
|
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
|
|
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';
|
package/src/SelectSearch.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { Combobox } from '@base-ui
|
|
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
|
|
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
|
|
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
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
|
-
<
|
|
61
|
+
<Button size="xs" onClick={action.run}>
|
|
61
62
|
{action.label}
|
|
62
|
-
</
|
|
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-
|
|
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
|
-
|
|
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}
|