@adecore/ui 0.38.0 → 0.40.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/AccentSwatches.js +2 -3
- package/dist/Banner.js +1 -6
- package/dist/ChoiceCards.js +0 -5
- package/dist/ColorSwatch.js +1 -4
- package/dist/ColorWheelPicker.js +3 -6
- package/dist/DockShell.js +3 -5
- package/dist/EmptyState.js +0 -1
- package/dist/ErrorBoundary.js +1 -5
- package/dist/Field.js +2 -6
- package/dist/FileIcon.js +2 -4
- package/dist/Icon.js +1 -2
- package/dist/IconButton.js +1 -1
- package/dist/IconPicker.js +3 -7
- package/dist/Input.js +1 -2
- package/dist/Kbd.js +1 -3
- package/dist/LazyBoundary.js +2 -3
- package/dist/Meter.js +0 -1
- package/dist/MiddleTruncate.js +2 -6
- package/dist/PanelEmpty.js +0 -1
- package/dist/PanelHeader.js +1 -4
- package/dist/Pill.js +1 -6
- package/dist/ProjectSwitcher.js +1 -1
- package/dist/PromptDialog.js +5 -8
- package/dist/RelativeTime.js +1 -1
- package/dist/SegmentBar.js +1 -2
- package/dist/Segmented.js +2 -5
- package/dist/Select.js +1 -4
- package/dist/ShortcutHints.js +2 -3
- package/dist/Slider.js +0 -1
- package/dist/SlidingColumn.js +2 -4
- package/dist/SplitButton.js +1 -5
- package/dist/TextMenu.js +1 -5
- package/dist/Tile.js +0 -4
- package/dist/Toasts.js +1 -9
- package/dist/Tooltip.js +1 -1
- package/dist/UIProvider.js +0 -5
- package/dist/Waveform.js +4 -2
- package/dist/Wipe.js +1 -6
- package/dist/ZoomControls.js +0 -1
- package/dist/clipboard.js +2 -5
- package/dist/combobox/parts.js +3 -9
- package/dist/command-palette/parts.js +5 -14
- package/dist/dialog/parts.js +5 -10
- package/dist/dialog-layer.js +2 -3
- package/dist/error-boundary.js +4 -20
- package/dist/file-icon.js +4 -9
- package/dist/file-tree/empty-lines.js +2 -8
- package/dist/file-tree/use-empty-lines.js +1 -5
- package/dist/file-tree/use-slots.js +6 -10
- package/dist/format/datetime.js +32 -29
- package/dist/format/duration.js +2 -10
- package/dist/format/locale.js +19 -39
- package/dist/format/number.js +9 -22
- package/dist/format/regions.js +4 -7
- package/dist/icon-picker.js +5 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/key-value-list/parts.js +1 -5
- package/dist/lazy.js +4 -19
- package/dist/locales.js +2 -6
- package/dist/menu/parts.js +61 -133
- package/dist/middle-truncate.js +2 -3
- package/dist/placement.js +0 -1
- package/dist/popover/parts.js +1 -5
- package/dist/popup-layer.js +2 -4
- package/dist/prefetch.js +2 -5
- package/dist/selection.js +1 -5
- package/dist/settings/MasterDetail.js +1 -7
- package/dist/settings/SettingsDialog.js +1 -2
- package/dist/settings/SettingsRow.js +2 -5
- package/dist/settings/SettingsSection.js +1 -1
- package/dist/shortcut-hints.js +1 -5
- package/dist/shortcut.js +1 -2
- package/dist/tabs/overflow.js +1 -6
- package/dist/tabs/parts.js +69 -95
- package/dist/testing/dedupe.js +5 -7
- package/dist/tint.d.ts +2 -0
- package/dist/tint.js +11 -0
- package/dist/toast-store.js +2 -10
- package/dist/tree/use-tree-shift.js +2 -6
- package/dist/useColumnResize.js +9 -10
- package/dist/useNow.js +2 -9
- package/dist/workspace/DropPreview.js +1 -5
- package/dist/workspace/preview.js +4 -9
- package/package.json +9 -9
- package/src/AccentSwatches.tsx +9 -3
- package/src/Banner.tsx +5 -11
- package/src/Button.tsx +4 -0
- package/src/Checkbox.tsx +3 -0
- package/src/ChoiceCards.tsx +12 -11
- package/src/CloseButton.tsx +1 -0
- package/src/ColorSwatch.tsx +7 -8
- package/src/ColorWheelPicker.tsx +28 -12
- package/src/ColumnResizeHandle.tsx +1 -0
- package/src/CopyButton.tsx +6 -0
- package/src/DisabledReason.tsx +1 -0
- package/src/DockShell.tsx +11 -10
- package/src/EmptyState.tsx +10 -6
- package/src/ErrorBoundary.tsx +13 -10
- package/src/Field.tsx +11 -13
- package/src/FileIcon.tsx +4 -4
- package/src/Icon.tsx +2 -4
- package/src/IconButton.tsx +14 -5
- package/src/IconPicker.tsx +30 -15
- package/src/Input.tsx +11 -9
- package/src/Kbd.tsx +8 -7
- package/src/LazyBoundary.tsx +3 -3
- package/src/Meter.tsx +7 -2
- package/src/MiddleTruncate.tsx +15 -6
- package/src/PanelEmpty.tsx +9 -7
- package/src/PanelHeader.tsx +2 -5
- package/src/Pill.tsx +7 -8
- package/src/ProjectSwitcher.tsx +16 -6
- package/src/PromptDialog.tsx +31 -26
- package/src/RelativeTime.tsx +6 -1
- package/src/SegmentBar.tsx +22 -11
- package/src/Segmented.tsx +16 -11
- package/src/Select.tsx +7 -4
- package/src/SelectSearch.tsx +3 -0
- package/src/ShortcutHints.tsx +24 -3
- package/src/Skeleton.tsx +1 -0
- package/src/Slider.tsx +4 -1
- package/src/SlidingColumn.tsx +18 -14
- package/src/Spinner.tsx +1 -0
- package/src/SplitButton.tsx +8 -8
- package/src/Stepper.tsx +6 -0
- package/src/Switch.tsx +2 -0
- package/src/TextMenu.tsx +2 -5
- package/src/Tile.tsx +2 -6
- package/src/Toasts.tsx +8 -12
- package/src/Tooltip.tsx +10 -5
- package/src/UIProvider.tsx +7 -7
- package/src/Waveform.tsx +41 -12
- package/src/Wipe.tsx +5 -6
- package/src/ZoomControls.tsx +5 -2
- package/src/clipboard.ts +4 -5
- package/src/combobox/parts.tsx +18 -14
- package/src/command-palette/context.ts +2 -0
- package/src/command-palette/index.parts.ts +1 -0
- package/src/command-palette/parts.tsx +20 -14
- package/src/command-palette/virtual.tsx +21 -0
- package/src/dialog/parts.tsx +10 -13
- package/src/dialog-layer.ts +5 -3
- package/src/error-boundary.ts +7 -20
- package/src/file-icon.ts +8 -9
- package/src/file-tree/empty-lines.ts +24 -8
- package/src/file-tree/helpers.ts +34 -0
- package/src/file-tree/highlight.ts +19 -0
- package/src/file-tree/index.parts.ts +1 -0
- package/src/file-tree/parts.tsx +41 -5
- package/src/file-tree/use-empty-lines.ts +22 -5
- package/src/file-tree/use-file-tree-shift.tsx +10 -0
- package/src/file-tree/use-file-tree.ts +2 -0
- package/src/file-tree/use-slots.ts +77 -12
- package/src/format/datetime.ts +42 -17
- package/src/format/duration.ts +28 -10
- package/src/format/index.ts +3 -0
- package/src/format/locale.ts +33 -42
- package/src/format/number.ts +21 -27
- package/src/format/regions.ts +4 -7
- package/src/icon-picker.ts +23 -16
- package/src/index.ts +9 -0
- package/src/key-value-list/parts.tsx +1 -5
- package/src/lazy-landings.ts +2 -0
- package/src/lazy.tsx +23 -19
- package/src/locales.ts +2 -6
- package/src/match.ts +14 -0
- package/src/menu/parts.tsx +30 -55
- package/src/middle-truncate.ts +12 -3
- package/src/modality.ts +4 -0
- package/src/placement.ts +2 -1
- package/src/platform.ts +2 -0
- package/src/popover/parts.tsx +3 -5
- package/src/popup-layer.ts +6 -4
- package/src/prefetch.ts +18 -5
- package/src/project-switcher-row.tsx +2 -0
- package/src/segmented-keys.ts +6 -0
- package/src/select-shared.ts +2 -0
- package/src/selection.ts +5 -5
- package/src/settings/ConfirmDialog.tsx +3 -0
- package/src/settings/MasterDetail.tsx +3 -9
- package/src/settings/SettingsDialog.tsx +27 -9
- package/src/settings/SettingsRow.tsx +16 -10
- package/src/settings/SettingsSection.tsx +10 -5
- package/src/settings/index.ts +3 -0
- package/src/settings/target.ts +1 -0
- package/src/shortcut-hints.ts +10 -5
- package/src/shortcut.ts +26 -6
- package/src/tabs/overflow.ts +8 -6
- package/src/tabs/parts.tsx +50 -26
- package/src/testing/dedupe.ts +27 -7
- package/src/testing/fake-source.ts +1 -0
- package/src/tint.ts +13 -0
- package/src/toast-store.ts +20 -14
- package/src/tree/parts.tsx +8 -0
- package/src/tree/shift.ts +6 -0
- package/src/tree/use-tree-shift.tsx +23 -7
- package/src/useAsyncAction.ts +8 -0
- package/src/useColumnResize.ts +24 -15
- package/src/useContentSize.ts +5 -0
- package/src/useMeasuredWidth.ts +1 -0
- package/src/useNow.ts +6 -11
- package/src/workspace/DropPreview.tsx +3 -5
- package/src/workspace/SplitView.tsx +109 -0
- package/src/workspace/TabStrip.tsx +42 -0
- package/src/workspace/Workspace.tsx +5 -0
- package/src/workspace/context.ts +1 -0
- package/src/workspace/geometry.ts +49 -0
- package/src/workspace/model.ts +72 -0
- package/src/workspace/preview.ts +15 -15
package/src/RelativeTime.tsx
CHANGED
|
@@ -10,19 +10,22 @@ const TICK_MS = 30_000;
|
|
|
10
10
|
export interface RelativeTimeProps {
|
|
11
11
|
/* `null`, `undefined` or an invalid date for a moment that is not known, drawn as a dash. */
|
|
12
12
|
at: Date | number | null | undefined;
|
|
13
|
+
|
|
13
14
|
/* Off inside a row that is the focus target itself: no tab stop of its own, and the row shows the exact time. */
|
|
14
15
|
tooltip?: boolean;
|
|
16
|
+
|
|
15
17
|
tooltipSide?: TooltipSide;
|
|
16
18
|
className?: string;
|
|
17
19
|
ref?: Ref<HTMLElement>;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
/*
|
|
22
|
+
/* Counts on while it is on screen, with the exact time on hover and on keyboard focus. */
|
|
21
23
|
export function RelativeTime({ at, tooltip = true, tooltipSide, className, ref }: RelativeTimeProps) {
|
|
22
24
|
// The formatters read the locale from a module, not from an input, so a compiled render would keep its first words.
|
|
23
25
|
'use no memo';
|
|
24
26
|
useFormatLocale();
|
|
25
27
|
const now = useNow(TICK_MS);
|
|
28
|
+
|
|
26
29
|
if (at === null || at === undefined || Number.isNaN(new Date(at).getTime())) {
|
|
27
30
|
return (
|
|
28
31
|
<span ref={ref} className={className}>
|
|
@@ -30,6 +33,7 @@ export function RelativeTime({ at, tooltip = true, tooltipSide, className, ref }
|
|
|
30
33
|
</span>
|
|
31
34
|
);
|
|
32
35
|
}
|
|
36
|
+
|
|
33
37
|
if (!tooltip) {
|
|
34
38
|
return (
|
|
35
39
|
<time ref={ref as Ref<HTMLTimeElement>} dateTime={new Date(at).toISOString()} className={clsx('whitespace-nowrap', className)}>
|
|
@@ -37,6 +41,7 @@ export function RelativeTime({ at, tooltip = true, tooltipSide, className, ref }
|
|
|
37
41
|
</time>
|
|
38
42
|
);
|
|
39
43
|
}
|
|
44
|
+
|
|
40
45
|
return (
|
|
41
46
|
<Tooltip label={formatDayClockWithYear(at)} side={tooltipSide}>
|
|
42
47
|
{/* Text is no tab stop of its own, and a hint that only the pointer can open is lost to the keyboard. */}
|
package/src/SegmentBar.tsx
CHANGED
|
@@ -7,34 +7,37 @@ import { useMeasuredWidth } from './useMeasuredWidth.ts';
|
|
|
7
7
|
export interface SegmentBarPart {
|
|
8
8
|
/* Its length, in the unit of the bar's `range`. */
|
|
9
9
|
value: number;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
/* Defaults to the end of the part before, or 0 for the first; a later start leaves the stretch in between empty. Parts stay in order. */
|
|
12
12
|
start?: number;
|
|
13
|
+
|
|
13
14
|
/* Drawn in the part where there is room for it, and always read by a screen reader. */
|
|
14
15
|
label?: string;
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
/* Any CSS color. It wins over the accent of `current`. */
|
|
16
18
|
color?: string;
|
|
17
|
-
|
|
19
|
+
|
|
18
20
|
current?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export interface SegmentBarProps {
|
|
22
24
|
parts: readonly SegmentBarPart[];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
/* Defaults to 0 up to the end of the last part. */
|
|
25
27
|
range?: readonly [from: number, to: number];
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
/* `sm` is a thin bar that shows its labels only as a tooltip. */
|
|
27
30
|
size?: 'sm' | 'md';
|
|
28
|
-
|
|
31
|
+
|
|
32
|
+
/* Makes every part a button. */
|
|
29
33
|
onSelect?(index: number): void;
|
|
30
|
-
|
|
34
|
+
|
|
31
35
|
label?: string;
|
|
32
36
|
className?: string;
|
|
33
37
|
ref?: Ref<HTMLOListElement>;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
/* A
|
|
37
|
-
place; the gap between two parts that touch comes out of the one before. */
|
|
40
|
+
/* A part starts exactly at its place; the gap between two parts that touch comes out of the one before. */
|
|
38
41
|
export function SegmentBar({ parts, range, size = 'md', onSelect, label, className, ref }: SegmentBarProps) {
|
|
39
42
|
const [measure, width] = useMeasuredWidth();
|
|
40
43
|
const rootRef = mergeRefs(ref, measure);
|
|
@@ -42,11 +45,14 @@ export function SegmentBar({ parts, range, size = 'md', onSelect, label, classNa
|
|
|
42
45
|
const [cut, setCut] = useState<readonly boolean[]>([]);
|
|
43
46
|
|
|
44
47
|
let end = 0;
|
|
48
|
+
|
|
45
49
|
const placed = parts.map((part, index) => {
|
|
46
50
|
const from = part.start ?? end;
|
|
47
51
|
end = from + Math.max(0, part.value);
|
|
52
|
+
|
|
48
53
|
return { part, index, from, to: end };
|
|
49
54
|
});
|
|
55
|
+
|
|
50
56
|
const [from, to] = range ?? [0, end];
|
|
51
57
|
const span = to - from;
|
|
52
58
|
const visible = span > 0 ? placed.filter((placement) => placement.to > from && placement.from < to) : [];
|
|
@@ -73,6 +79,7 @@ export function SegmentBar({ parts, range, size = 'md', onSelect, label, classNa
|
|
|
73
79
|
const next = visible[i + 1];
|
|
74
80
|
// Less than a pixel apart they touch, whatever rounding the caller's starts carry.
|
|
75
81
|
const touches = next !== undefined && ((next.from - partTo) / span) * width < 1;
|
|
82
|
+
|
|
76
83
|
const fill = clsx(
|
|
77
84
|
'flex size-full min-w-0 items-center',
|
|
78
85
|
size === 'sm' ? 'rounded-full' : 'rounded-sm px-1.5 text-left text-xs',
|
|
@@ -81,6 +88,7 @@ export function SegmentBar({ parts, range, size = 'md', onSelect, label, classNa
|
|
|
81
88
|
!part.current && size === 'md' && 'text-text-muted',
|
|
82
89
|
onSelect !== undefined && 'cursor-pointer hover:opacity-80'
|
|
83
90
|
);
|
|
91
|
+
|
|
84
92
|
const text = (
|
|
85
93
|
<span
|
|
86
94
|
ref={(element) => {
|
|
@@ -91,7 +99,9 @@ export function SegmentBar({ parts, range, size = 'md', onSelect, label, classNa
|
|
|
91
99
|
{part.label}
|
|
92
100
|
</span>
|
|
93
101
|
);
|
|
102
|
+
|
|
94
103
|
const look = { className: fill, style: { backgroundColor: part.color }, 'aria-current': part.current ? true : undefined };
|
|
104
|
+
|
|
95
105
|
const body =
|
|
96
106
|
onSelect === undefined ? (
|
|
97
107
|
<span {...look}>{text}</span>
|
|
@@ -100,6 +110,7 @@ export function SegmentBar({ parts, range, size = 'md', onSelect, label, classNa
|
|
|
100
110
|
{text}
|
|
101
111
|
</button>
|
|
102
112
|
);
|
|
113
|
+
|
|
103
114
|
return (
|
|
104
115
|
<li
|
|
105
116
|
key={index}
|
package/src/Segmented.tsx
CHANGED
|
@@ -9,7 +9,8 @@ export interface SegmentedOption<T extends string> {
|
|
|
9
9
|
id: T;
|
|
10
10
|
label: string;
|
|
11
11
|
icon?: LucideIcon;
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
/* Takes the option out of the tab order and the arrow keys; the rest of the group stays usable. */
|
|
13
14
|
disabled?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -17,15 +18,18 @@ export interface SegmentedProps<T extends string> {
|
|
|
17
18
|
value: T;
|
|
18
19
|
onValueChange(id: T): void;
|
|
19
20
|
options: readonly SegmentedOption<T>[];
|
|
20
|
-
/* The accessible name of the group. */
|
|
21
21
|
label: string;
|
|
22
22
|
disabled?: boolean;
|
|
23
|
-
|
|
23
|
+
|
|
24
|
+
/* `vertical` is as wide as its container, for labels too long to stand side by side. */
|
|
24
25
|
orientation?: 'horizontal' | 'vertical';
|
|
25
|
-
|
|
26
|
+
|
|
27
|
+
/* The heights of `Input`, `Select` and `Button`. */
|
|
26
28
|
size?: 'md' | 'sm';
|
|
27
|
-
|
|
29
|
+
|
|
30
|
+
/* An option with an icon draws as a square, its label as its accessible name and tooltip. One without an icon keeps its label. */
|
|
28
31
|
iconOnly?: boolean;
|
|
32
|
+
|
|
29
33
|
className?: string;
|
|
30
34
|
ref?: Ref<HTMLDivElement>;
|
|
31
35
|
}
|
|
@@ -35,10 +39,7 @@ const SIZE = {
|
|
|
35
39
|
sm: { track: 'rounded-md', row: 'min-h-7', option: 'min-h-6 rounded-sm px-2.5 py-0.5', square: 'size-6 rounded-sm', icon: 12 }
|
|
36
40
|
} as const;
|
|
37
41
|
|
|
38
|
-
/*
|
|
39
|
-
* One of a few options side by side in one sunken track, the picked one lifted out of it. A radio
|
|
40
|
-
* group: one tab stop, and the arrows pick and focus the next option, around the ends.
|
|
41
|
-
*/
|
|
42
|
+
/* A radio group: one tab stop, and the arrows pick and focus the next option, around the ends. */
|
|
42
43
|
export function Segmented<T extends string>({
|
|
43
44
|
value,
|
|
44
45
|
onValueChange,
|
|
@@ -55,15 +56,17 @@ export function Segmented<T extends string>({
|
|
|
55
56
|
const sizing = SIZE[size];
|
|
56
57
|
const stop = segmentedStop(options, value);
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
const move = (event: KeyboardEvent<HTMLButtonElement>, index: number): void => {
|
|
59
60
|
const next = segmentedTarget(options, index, event.key);
|
|
61
|
+
|
|
60
62
|
if (next === null) {
|
|
61
63
|
return;
|
|
62
64
|
}
|
|
65
|
+
|
|
63
66
|
event.preventDefault();
|
|
64
67
|
onValueChange(options[next]!.id);
|
|
65
68
|
event.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>('[role="radio"]')[next]?.focus();
|
|
66
|
-
}
|
|
69
|
+
};
|
|
67
70
|
|
|
68
71
|
return (
|
|
69
72
|
<div
|
|
@@ -80,6 +83,7 @@ export function Segmented<T extends string>({
|
|
|
80
83
|
>
|
|
81
84
|
{options.map((option, index) => {
|
|
82
85
|
const square = iconOnly && option.icon !== undefined;
|
|
86
|
+
|
|
83
87
|
const button = (
|
|
84
88
|
<button
|
|
85
89
|
key={option.id}
|
|
@@ -100,6 +104,7 @@ export function Segmented<T extends string>({
|
|
|
100
104
|
{!square && option.label}
|
|
101
105
|
</button>
|
|
102
106
|
);
|
|
107
|
+
|
|
103
108
|
return square ? (
|
|
104
109
|
<Tooltip key={option.id} label={option.label} name>
|
|
105
110
|
{button}
|
package/src/Select.tsx
CHANGED
|
@@ -16,19 +16,25 @@ export interface SelectProps<T extends string> {
|
|
|
16
16
|
value: T | null;
|
|
17
17
|
onValueChange(value: T): void;
|
|
18
18
|
items: SelectItem<T>[] | SelectGroup<T>[];
|
|
19
|
+
|
|
19
20
|
/* The accessible name of the trigger; the row above it usually carries the visible one. */
|
|
20
21
|
label: string;
|
|
22
|
+
|
|
21
23
|
/* What the trigger shows while nothing is picked. */
|
|
22
24
|
placeholder?: string;
|
|
25
|
+
|
|
23
26
|
size?: 'sm' | 'md';
|
|
24
27
|
variant?: 'outlined' | 'ghost';
|
|
25
28
|
disabled?: boolean;
|
|
26
29
|
align?: 'start' | 'end';
|
|
27
30
|
truncateValue?: boolean;
|
|
31
|
+
|
|
28
32
|
/* A search field above the options: `true` always, a number once the list has at least that many options. */
|
|
29
33
|
searchable?: boolean | number;
|
|
34
|
+
|
|
30
35
|
/* What the search field says while it is empty, and its accessible name. */
|
|
31
36
|
searchPlaceholder?: string;
|
|
37
|
+
|
|
32
38
|
className?: string;
|
|
33
39
|
ref?: Ref<HTMLButtonElement>;
|
|
34
40
|
}
|
|
@@ -41,10 +47,7 @@ function Row<T extends string>({ item, picked }: { item: SelectItem<T>; picked:
|
|
|
41
47
|
);
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
/*
|
|
45
|
-
picker in a toolbar read the same. Base UI brings the keyboard along (arrows,
|
|
46
|
-
Home and End, typeahead, Enter, Escape), which a hand-rolled listbox would have to repeat.
|
|
47
|
-
A long list can ask for a search field, which swaps in a combobox with the same trigger and rows. */
|
|
50
|
+
/* A long list can ask for a search field, which swaps in a combobox with the same trigger and rows. */
|
|
48
51
|
export function Select<T extends string>(props: SelectProps<T>) {
|
|
49
52
|
const { searchable, items } = props;
|
|
50
53
|
const count = items.length > 0 && isGroup(items[0]!) ? (items as SelectGroup<T>[]).reduce((sum, group) => sum + group.items.length, 0) : items.length;
|
package/src/SelectSearch.tsx
CHANGED
|
@@ -32,13 +32,16 @@ export function SelectSearch<T extends string>({
|
|
|
32
32
|
const flat = groups ? groups.flatMap((group) => group.items) : (items as SelectItem<T>[]);
|
|
33
33
|
const byValue = new Map(flat.map((item) => [item.value, item]));
|
|
34
34
|
const selected = value === null ? null : (byValue.get(value) ?? null);
|
|
35
|
+
|
|
35
36
|
const comboboxItems = groups
|
|
36
37
|
? groups.map((group) => ({ value: group.label, items: group.items.map((item) => item.value) }))
|
|
37
38
|
: flat.map((item) => item.value);
|
|
39
|
+
|
|
38
40
|
const searchLabel = searchPlaceholder ?? t('select.search');
|
|
39
41
|
|
|
40
42
|
const renderRow = (itemValue: T) => {
|
|
41
43
|
const item = byValue.get(itemValue)!;
|
|
44
|
+
|
|
42
45
|
return (
|
|
43
46
|
<Combobox.Item key={item.value} className={rowClassName(item)} value={item.value} disabled={item.disabled}>
|
|
44
47
|
<RowContent item={item} picked={item.value === value} text={<span className="truncate">{item.label}</span>} />
|
package/src/ShortcutHints.tsx
CHANGED
|
@@ -15,35 +15,42 @@ const isSeen = (element: Element, rect: DOMRect): boolean => {
|
|
|
15
15
|
if (rect.width === 0 || rect.height === 0 || rect.bottom <= 0 || rect.right <= 0 || rect.top >= window.innerHeight || rect.left >= window.innerWidth) {
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
|
+
|
|
18
19
|
if (!element.checkVisibility({ opacityProperty: true, visibilityProperty: true })) {
|
|
19
20
|
return false;
|
|
20
21
|
}
|
|
22
|
+
|
|
21
23
|
const hit = document.elementFromPoint((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2);
|
|
24
|
+
|
|
22
25
|
return hit !== null && element.contains(hit);
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
const measureHints = (apple: boolean): PlacedHint[] => {
|
|
26
29
|
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
27
30
|
const hints: PlacedHint[] = [];
|
|
31
|
+
|
|
28
32
|
for (const [element, target] of shortcutHintTargets()) {
|
|
29
33
|
const rect = element.getBoundingClientRect();
|
|
34
|
+
|
|
30
35
|
if (!isSeen(element, rect)) {
|
|
31
36
|
continue;
|
|
32
37
|
}
|
|
38
|
+
|
|
33
39
|
const placement = placeHint(rect, viewport);
|
|
40
|
+
|
|
34
41
|
hints.push({
|
|
35
42
|
id: target.id,
|
|
36
43
|
text: formatShortcut(target.shortcut, apple),
|
|
37
44
|
style: { left: placement.left, right: placement.right, top: placement.top, translate: `${placement.translateX} ${placement.translateY}` }
|
|
38
45
|
});
|
|
39
46
|
}
|
|
47
|
+
|
|
40
48
|
return hints;
|
|
41
49
|
};
|
|
42
50
|
|
|
43
51
|
/*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* itself still takes the hints down. An embedded page (`<webview>`) has the keyboard to itself and shows none.
|
|
52
|
+
* The listener sits on the window in the capture phase so a terminal that keeps a key to itself still
|
|
53
|
+
* takes the hints down. An embedded page (`<webview>`) has the keyboard to itself and shows none.
|
|
47
54
|
*/
|
|
48
55
|
export function ShortcutHints() {
|
|
49
56
|
const [shown, setShown] = useState(false);
|
|
@@ -53,15 +60,20 @@ export function ShortcutHints() {
|
|
|
53
60
|
useEffect(() => {
|
|
54
61
|
const hold = new ModifierHold(isApplePlatform(), setShown);
|
|
55
62
|
holdRef.current = hold;
|
|
63
|
+
|
|
56
64
|
const onKeyDown = (e: KeyboardEvent): void => hold.keyDown(e);
|
|
65
|
+
|
|
57
66
|
const onKeyUp = (e: KeyboardEvent): void => hold.keyUp(e);
|
|
67
|
+
|
|
58
68
|
// A click or a zoom with the modifier down is a gesture, not someone looking for a shortcut.
|
|
59
69
|
const cancel = (): void => hold.cancel();
|
|
70
|
+
|
|
60
71
|
window.addEventListener('keydown', onKeyDown, true);
|
|
61
72
|
window.addEventListener('keyup', onKeyUp, true);
|
|
62
73
|
window.addEventListener('pointerdown', cancel, true);
|
|
63
74
|
window.addEventListener('wheel', cancel, { capture: true, passive: true });
|
|
64
75
|
window.addEventListener('blur', cancel);
|
|
76
|
+
|
|
65
77
|
return () => {
|
|
66
78
|
window.removeEventListener('keydown', onKeyDown, true);
|
|
67
79
|
window.removeEventListener('keyup', onKeyUp, true);
|
|
@@ -77,30 +89,39 @@ export function ShortcutHints() {
|
|
|
77
89
|
if (!shown) {
|
|
78
90
|
return;
|
|
79
91
|
}
|
|
92
|
+
|
|
80
93
|
const apple = isApplePlatform();
|
|
81
94
|
let frame = 0;
|
|
82
95
|
let last = '';
|
|
96
|
+
|
|
83
97
|
const measure = (): void => {
|
|
84
98
|
// Focus that moved into a page takes the key-up with it, so the embedder never hears the modifier let go.
|
|
85
99
|
if (document.activeElement?.tagName === 'WEBVIEW') {
|
|
86
100
|
holdRef.current?.cancel();
|
|
101
|
+
|
|
87
102
|
return;
|
|
88
103
|
}
|
|
104
|
+
|
|
89
105
|
const next = measureHints(apple);
|
|
90
106
|
const signature = JSON.stringify(next);
|
|
107
|
+
|
|
91
108
|
if (signature !== last) {
|
|
92
109
|
last = signature;
|
|
93
110
|
setHints(next);
|
|
94
111
|
}
|
|
112
|
+
|
|
95
113
|
frame = requestAnimationFrame(measure);
|
|
96
114
|
};
|
|
115
|
+
|
|
97
116
|
measure();
|
|
117
|
+
|
|
98
118
|
return () => cancelAnimationFrame(frame);
|
|
99
119
|
}, [shown]);
|
|
100
120
|
|
|
101
121
|
if (!shown) {
|
|
102
122
|
return null;
|
|
103
123
|
}
|
|
124
|
+
|
|
104
125
|
return createPortal(
|
|
105
126
|
<div aria-hidden className="pointer-events-none fixed inset-0 z-(--z-shortcut-hints)">
|
|
106
127
|
{hints.map((hint) => (
|
package/src/Skeleton.tsx
CHANGED
package/src/Slider.tsx
CHANGED
|
@@ -10,18 +10,21 @@ export interface SliderProps {
|
|
|
10
10
|
min: number;
|
|
11
11
|
max: number;
|
|
12
12
|
step?: number;
|
|
13
|
+
|
|
13
14
|
/* Places after the separator, always shown. Defaults to the places `step` is written with. */
|
|
14
15
|
decimals?: number;
|
|
16
|
+
|
|
15
17
|
/* Printed right after the number, such as `px` or `%`. */
|
|
16
18
|
unit?: string;
|
|
19
|
+
|
|
17
20
|
/* Shown above the track and read as the slider's name. */
|
|
18
21
|
label: string;
|
|
22
|
+
|
|
19
23
|
disabled?: boolean;
|
|
20
24
|
className?: string;
|
|
21
25
|
ref?: Ref<HTMLDivElement>;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
/* A number picked along a range: the label and the value on one line, the track under them. */
|
|
25
28
|
export function Slider({ value, onValueChange, min, max, step = 1, decimals = placesOf(step), unit, label, disabled, className, ref }: SliderProps) {
|
|
26
29
|
const labelId = useId();
|
|
27
30
|
const reading = formatFixed(value, decimals);
|
package/src/SlidingColumn.tsx
CHANGED
|
@@ -9,48 +9,50 @@ const TRANSITION_MS = 200;
|
|
|
9
9
|
|
|
10
10
|
export interface SlidingColumnProps {
|
|
11
11
|
open: boolean;
|
|
12
|
+
|
|
12
13
|
/* Already clamped by the caller, which knows what has to stay beside it (`clampColumnSize`). */
|
|
13
14
|
width: number;
|
|
15
|
+
|
|
14
16
|
/* Space before the surface, in pixels. Replaces the border and carries the resize handle. */
|
|
15
17
|
gap?: number;
|
|
18
|
+
|
|
16
19
|
bounds: { min: number; max(): number };
|
|
17
20
|
onWidthChange(width: number): void;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* loads; only what a person does afterwards should animate.
|
|
21
|
-
*/
|
|
21
|
+
|
|
22
|
+
/* For a column whose place is restored as the page loads; only what a person does afterwards should animate. */
|
|
22
23
|
instant?: boolean;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* by script alone, so opening something in it hands it the keys without a click first.
|
|
26
|
-
*/
|
|
24
|
+
|
|
25
|
+
/* For a panel with shortcuts of its own. Focusable by script alone, so opening something in it hands it the keys without a click. */
|
|
27
26
|
body?: {
|
|
28
27
|
ref: RefObject<HTMLDivElement | null>;
|
|
29
28
|
onKeyDown(event: ReactKeyboardEvent<HTMLDivElement>): void;
|
|
30
29
|
};
|
|
30
|
+
|
|
31
31
|
className?: string;
|
|
32
|
+
|
|
32
33
|
/* The outer column, for bounds that measure what is beside it rather than the whole window. */
|
|
33
34
|
ref?: Ref<HTMLElement>;
|
|
35
|
+
|
|
34
36
|
children?: ReactNode;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* moves, and they stay mounted until the slide is over, so a close plays out. The left edge is the
|
|
41
|
-
* drag handle.
|
|
40
|
+
* The contents sit in an inner column of the stored width, so they do not reflow while the outer one
|
|
41
|
+
* moves, and stay mounted until the slide is over, so a close plays out.
|
|
42
42
|
*/
|
|
43
43
|
export function SlidingColumn({ open, width, gap = 0, bounds, onWidthChange, instant = false, body, className, ref, children }: SlidingColumnProps) {
|
|
44
44
|
/* Closed and done animating. */
|
|
45
45
|
const [settled, setSettled] = useState(!open);
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
|
|
47
|
+
/* A width that lands without a transition fires no `transitionend`. */
|
|
48
48
|
if (instant && settled !== !open) {
|
|
49
49
|
setSettled(!open);
|
|
50
50
|
}
|
|
51
|
+
|
|
51
52
|
const present = open || !settled;
|
|
52
53
|
const column = useRef<HTMLElement>(null);
|
|
53
54
|
const spacing = Math.max(0, Math.round(gap));
|
|
55
|
+
|
|
54
56
|
const { startResize } = useColumnResize(column, {
|
|
55
57
|
min: bounds.min + spacing,
|
|
56
58
|
max: () => bounds.max() + spacing,
|
|
@@ -63,8 +65,10 @@ export function SlidingColumn({ open, width, gap = 0, bounds, onWidthChange, ins
|
|
|
63
65
|
if (open || settled) {
|
|
64
66
|
return;
|
|
65
67
|
}
|
|
68
|
+
|
|
66
69
|
// Reduced motion and a hidden tab paint no width change, so no `transitionend` arrives.
|
|
67
70
|
const timer = window.setTimeout(() => setSettled(true), TRANSITION_MS + 50);
|
|
71
|
+
|
|
68
72
|
return () => {
|
|
69
73
|
window.clearTimeout(timer);
|
|
70
74
|
};
|
package/src/Spinner.tsx
CHANGED
|
@@ -6,6 +6,7 @@ const DOTS = [0, 1, 2];
|
|
|
6
6
|
export interface SpinnerProps extends Omit<ComponentProps<'span'>, 'children'> {
|
|
7
7
|
/* In pixels, on the steps of an icon: 12, 14, 16 or 20. */
|
|
8
8
|
size?: number;
|
|
9
|
+
|
|
9
10
|
/* What runs, read by a screen reader. Left out where the words beside it already say so, which hides the spinner as an icon is hidden. */
|
|
10
11
|
label?: string;
|
|
11
12
|
}
|
package/src/SplitButton.tsx
CHANGED
|
@@ -10,28 +10,28 @@ import { Tooltip } from './Tooltip.tsx';
|
|
|
10
10
|
export interface SplitButtonProps extends Omit<ComponentProps<'button'>, 'ref'> {
|
|
11
11
|
variant?: ButtonVariant;
|
|
12
12
|
size?: ButtonSize;
|
|
13
|
+
|
|
13
14
|
/* The hint on the action half. */
|
|
14
15
|
tooltip?: ReactNode;
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
/* The rows of the menu the chevron opens. */
|
|
16
18
|
menu: ReactNode;
|
|
19
|
+
|
|
17
20
|
/* The accessible name of the chevron, and its hint unless `menuTooltip` says something else. */
|
|
18
21
|
menuLabel: string;
|
|
22
|
+
|
|
19
23
|
menuTooltip?: ReactNode;
|
|
20
24
|
menuDisabled?: boolean;
|
|
21
|
-
/* Where the menu lines up under the whole control. */
|
|
22
25
|
menuAlign?: MenuPopupProps['align'];
|
|
23
|
-
/* A width or a layout of the menu's own, such as `w-72`. */
|
|
24
26
|
menuClassName?: string;
|
|
27
|
+
|
|
25
28
|
/* On the box around both halves; every other prop goes to the action half. */
|
|
26
29
|
className?: string;
|
|
30
|
+
|
|
27
31
|
ref?: ComponentProps<'span'>['ref'];
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
/*
|
|
31
|
-
* A primary action and a chevron that opens a menu of related ones, drawn as one button with a short
|
|
32
|
-
* line between the halves. Each half is a button of its own, so each takes the focus, can be disabled
|
|
33
|
-
* and carries its own hint; the menu hangs under the whole control.
|
|
34
|
-
*/
|
|
34
|
+
/* Each half is a button of its own, so each takes the focus, can be disabled and carries its own hint; the menu hangs under the whole control. */
|
|
35
35
|
export function SplitButton({
|
|
36
36
|
variant = 'secondary',
|
|
37
37
|
size = 'md',
|
package/src/Stepper.tsx
CHANGED
|
@@ -13,10 +13,13 @@ export interface StepperProps {
|
|
|
13
13
|
min: number;
|
|
14
14
|
max: number;
|
|
15
15
|
step: number;
|
|
16
|
+
|
|
16
17
|
/* Places after the separator, always shown: `1,0`, `1,5`. Defaults to the places `step` is written with. */
|
|
17
18
|
decimals?: number;
|
|
19
|
+
|
|
18
20
|
/* Printed right after the number, such as `px` or `%`. */
|
|
19
21
|
unit?: string;
|
|
22
|
+
|
|
20
23
|
label: string;
|
|
21
24
|
className?: string;
|
|
22
25
|
ref?: Ref<HTMLDivElement>;
|
|
@@ -25,16 +28,19 @@ export interface StepperProps {
|
|
|
25
28
|
/* A number between minus and plus, in one sunken group so the three read as a single control. */
|
|
26
29
|
export function Stepper({ value, onValueChange, min, max, step, decimals = placesOf(step), unit, label, className, ref }: StepperProps) {
|
|
27
30
|
const { t } = useTranslation('ui');
|
|
31
|
+
|
|
28
32
|
const reading = (amount: number) => (
|
|
29
33
|
<>
|
|
30
34
|
{formatFixed(amount, decimals)}
|
|
31
35
|
{unit && <span className="text-text-muted">{unit}</span>}
|
|
32
36
|
</>
|
|
33
37
|
);
|
|
38
|
+
|
|
34
39
|
const nudge = (direction: -1 | 1): void => {
|
|
35
40
|
const next = Math.round((value + direction * step) * 100) / 100;
|
|
36
41
|
onValueChange(Math.min(max, Math.max(min, next)));
|
|
37
42
|
};
|
|
43
|
+
|
|
38
44
|
return (
|
|
39
45
|
<ButtonGroup
|
|
40
46
|
ref={ref}
|
package/src/Switch.tsx
CHANGED
|
@@ -5,8 +5,10 @@ import { Switch as BaseSwitch } from '@base-ui/react/switch';
|
|
|
5
5
|
export interface SwitchProps {
|
|
6
6
|
checked: boolean;
|
|
7
7
|
onCheckedChange(checked: boolean): void;
|
|
8
|
+
|
|
8
9
|
/* The accessible name; the row it sits in usually carries the visible one. */
|
|
9
10
|
label: string;
|
|
11
|
+
|
|
10
12
|
disabled?: boolean;
|
|
11
13
|
className?: string;
|
|
12
14
|
ref?: Ref<HTMLButtonElement>;
|
package/src/TextMenu.tsx
CHANGED
|
@@ -15,11 +15,7 @@ export type TextMenuProps = ComponentProps<'div'> & {
|
|
|
15
15
|
items?: ReactNode;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
/*
|
|
19
|
-
* A block of text and the menu that belongs to it: Copy for what is selected inside it, Select all
|
|
20
|
-
* for the whole of it, on Cmd+A as well. Anywhere text can be selected a right-click has to offer
|
|
21
|
-
* to copy it, and an app that draws every menu itself offers nothing at all without one of these.
|
|
22
|
-
*/
|
|
18
|
+
/* An app that draws every menu itself offers no Copy on a right-click without one of these. */
|
|
23
19
|
export function TextMenu({ children, items, ref, ...rest }: TextMenuProps) {
|
|
24
20
|
const { t } = useTranslation('ui');
|
|
25
21
|
const host = useRef<HTMLDivElement>(null);
|
|
@@ -30,6 +26,7 @@ export function TextMenu({ children, items, ref, ...rest }: TextMenuProps) {
|
|
|
30
26
|
if (!matchesShortcut(EDIT_SHORTCUTS.selectAll, event, isApplePlatform())) {
|
|
31
27
|
return;
|
|
32
28
|
}
|
|
29
|
+
|
|
33
30
|
event.preventDefault();
|
|
34
31
|
selectAllWithin(host.current);
|
|
35
32
|
};
|
package/src/Tile.tsx
CHANGED
|
@@ -6,19 +6,15 @@ import type { Shortcut } from './shortcut.ts';
|
|
|
6
6
|
export interface TileProps extends Omit<ComponentProps<'button'>, 'title'> {
|
|
7
7
|
icon: ReactNode;
|
|
8
8
|
title: string;
|
|
9
|
-
/* One short line under the title: where it acts, or why it waits. */
|
|
10
9
|
description?: ReactNode;
|
|
11
10
|
shortcut?: Shortcut;
|
|
11
|
+
|
|
12
12
|
/* The one tile a screen leads with. */
|
|
13
13
|
primary?: boolean;
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
size?: 'md' | 'sm';
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/*
|
|
19
|
-
* One thing to start with, as a card that is a button: a start screen and the empty places of an app
|
|
20
|
-
* use it, so a place to begin looks the same wherever it is offered.
|
|
21
|
-
*/
|
|
22
18
|
export function Tile({ icon, title, description, shortcut, primary = false, size = 'md', className, type = 'button', ...rest }: TileProps) {
|
|
23
19
|
return (
|
|
24
20
|
<button
|