@adecore/ui 0.39.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/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.js +3 -5
- 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 +8 -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 +3 -5
- 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/Checkbox.tsx
CHANGED
|
@@ -7,10 +7,13 @@ import { Icon } from './Icon.tsx';
|
|
|
7
7
|
export interface CheckboxProps {
|
|
8
8
|
checked: boolean;
|
|
9
9
|
onCheckedChange(checked: boolean): void;
|
|
10
|
+
|
|
10
11
|
/* The accessible name; a `<label>` around the box and its text usually carries the visible one. */
|
|
11
12
|
label: string;
|
|
13
|
+
|
|
12
14
|
/* Some of the items it stands for are picked and some are not, as on a box that picks every row of a list. */
|
|
13
15
|
indeterminate?: boolean;
|
|
16
|
+
|
|
14
17
|
disabled?: boolean;
|
|
15
18
|
className?: string;
|
|
16
19
|
ref?: Ref<HTMLButtonElement>;
|
package/src/ChoiceCards.tsx
CHANGED
|
@@ -8,7 +8,6 @@ import { Icon } from './Icon.tsx';
|
|
|
8
8
|
export interface Choice<Value extends string | number> {
|
|
9
9
|
value: Value;
|
|
10
10
|
title: string;
|
|
11
|
-
/* One line under the title that says what picking it means. */
|
|
12
11
|
description: string;
|
|
13
12
|
icon?: LucideIcon;
|
|
14
13
|
disabled?: boolean;
|
|
@@ -18,16 +17,20 @@ export interface ChoiceCardsProps<Value extends string | number> {
|
|
|
18
17
|
value: Value;
|
|
19
18
|
onValueChange(value: Value): void;
|
|
20
19
|
choices: readonly Choice<Value>[];
|
|
21
|
-
|
|
20
|
+
|
|
21
|
+
/* Read by a screen reader, since the row has no visible heading of its own. */
|
|
22
22
|
label: string;
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
orientation?: 'horizontal' | 'vertical';
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
/* Filled row by row; takes the place of `orientation`. */
|
|
26
27
|
columns?: number;
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
radio?: 'start' | 'end';
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
/* What the checked choice still asks for, under its card; only a vertical group without columns has room for it. */
|
|
30
32
|
detail?: ReactNode;
|
|
33
|
+
|
|
31
34
|
disabled?: boolean;
|
|
32
35
|
className?: string;
|
|
33
36
|
ref?: Ref<HTMLDivElement>;
|
|
@@ -35,12 +38,14 @@ export interface ChoiceCardsProps<Value extends string | number> {
|
|
|
35
38
|
|
|
36
39
|
function ChoiceCard<Value extends string | number>({ choice, radio }: { choice: Choice<Value>; radio: 'start' | 'end' }) {
|
|
37
40
|
const id = useId();
|
|
41
|
+
|
|
38
42
|
// As tall as the title's line, so the dot centers on it.
|
|
39
43
|
const dot = (
|
|
40
44
|
<span className="flex h-(--text-sm--line-height) shrink-0 items-center" aria-hidden>
|
|
41
45
|
<span className="size-4 rounded-full border border-border-strong group-data-checked:border-5 group-data-checked:border-accent group-data-checked:bg-accent-text" />
|
|
42
46
|
</span>
|
|
43
47
|
);
|
|
48
|
+
|
|
44
49
|
return (
|
|
45
50
|
<Radio.Root
|
|
46
51
|
value={choice.value}
|
|
@@ -68,11 +73,6 @@ function ChoiceCard<Value extends string | number>({ choice, radio }: { choice:
|
|
|
68
73
|
);
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
/*
|
|
72
|
-
* One of a few options, each a card that says what it means, for a choice that deserves more than a
|
|
73
|
-
* select: the cards stand side by side at the same width, one under the other or in a set number of
|
|
74
|
-
* columns, and the arrow keys move between them.
|
|
75
|
-
*/
|
|
76
76
|
export function ChoiceCards<Value extends string | number>({
|
|
77
77
|
value,
|
|
78
78
|
onValueChange,
|
|
@@ -87,6 +87,7 @@ export function ChoiceCards<Value extends string | number>({
|
|
|
87
87
|
ref
|
|
88
88
|
}: ChoiceCardsProps<Value>) {
|
|
89
89
|
const vertical = columns === undefined && orientation === 'vertical';
|
|
90
|
+
|
|
90
91
|
return (
|
|
91
92
|
<RadioGroup
|
|
92
93
|
ref={ref}
|
package/src/CloseButton.tsx
CHANGED
|
@@ -17,5 +17,6 @@ export function CloseButton({ label, kbd, size, className, ref, ...closes }: Clo
|
|
|
17
17
|
if ('dialog' in closes) {
|
|
18
18
|
return <IconButton ref={ref} icon={X} label={label} kbd={kbd} size={size} className={className} render={<Dialog.Close />} />;
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
return <IconButton ref={ref} icon={X} label={label} kbd={kbd} size={size} className={className} onClick={closes.onClick} />;
|
|
21
22
|
}
|
package/src/ColorSwatch.tsx
CHANGED
|
@@ -14,21 +14,20 @@ const PICKED_RING = {
|
|
|
14
14
|
} as const;
|
|
15
15
|
|
|
16
16
|
export interface ColorSwatchProps extends ComponentProps<'button'> {
|
|
17
|
-
/*
|
|
17
|
+
/* Without one the swatch is an outlined circle: "no color", or the button that opens the rest of a palette. */
|
|
18
18
|
color?: string;
|
|
19
|
+
|
|
19
20
|
picked?: boolean;
|
|
20
|
-
/* On the surface behind a popup, or inside the popup itself. */
|
|
21
21
|
on?: keyof typeof PICKED_RING;
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
/* Off where the ring alone marks a pick, such as on a swatch of two tones, where a tick reads as a glitch. */
|
|
23
24
|
tick?: boolean;
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
/* Such as `<Menu.Item unstyled />`, so the row's padding and highlight stay off it. */
|
|
25
27
|
render?: useRender.RenderProp;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
/*
|
|
29
|
-
* One color, drawn as the color itself. Its name belongs in a tooltip, so the circle carries a tick
|
|
30
|
-
* alone when it is picked. Children replace the tick, such as the ellipsis of a "more colors" swatch.
|
|
31
|
-
*/
|
|
30
|
+
/* Its name belongs in a tooltip. Children replace the tick, such as the ellipsis of a "more colors" swatch. */
|
|
32
31
|
export function ColorSwatch({ color, picked = false, on = 'surface', tick = true, render, className, style, ref, children, ...props }: ColorSwatchProps) {
|
|
33
32
|
return useRender({
|
|
34
33
|
render: render ?? <button type="button" />,
|
package/src/ColorWheelPicker.tsx
CHANGED
|
@@ -6,25 +6,33 @@ import { PopoverPopup, PopoverRoot, PopoverTrigger } from './popover/parts.tsx';
|
|
|
6
6
|
import { triggerClassName } from './select-shared.ts';
|
|
7
7
|
|
|
8
8
|
const SWATCH_SIZE = 24;
|
|
9
|
+
|
|
9
10
|
/* The outer ring stands this far from the middle, further when its swatches would not keep a gap between them otherwise. */
|
|
10
11
|
const MIN_RADIUS = 97;
|
|
12
|
+
|
|
11
13
|
const MIN_GAP = 8;
|
|
14
|
+
|
|
12
15
|
/* Room past the outer ring for the ring a picked swatch wears. */
|
|
13
16
|
const EDGE = 7;
|
|
14
17
|
|
|
15
18
|
export interface ColorWheelPickerProps<Id extends string> {
|
|
16
19
|
value: Id;
|
|
17
20
|
onValueChange(id: Id): void;
|
|
18
|
-
|
|
21
|
+
|
|
22
|
+
/* The one under the pointer or the focus while the wheel is open; null once none is looked at. */
|
|
19
23
|
onPreviewChange?(id: Id | null): void;
|
|
20
|
-
|
|
24
|
+
|
|
25
|
+
/* The accessible name; the row the picker stands in usually carries the visible one. */
|
|
21
26
|
label: string;
|
|
22
|
-
|
|
27
|
+
|
|
28
|
+
/* From the outside in, each ring in its own order from the top. */
|
|
23
29
|
rings: readonly (readonly Id[])[];
|
|
24
|
-
|
|
30
|
+
|
|
25
31
|
center?: Id;
|
|
32
|
+
|
|
26
33
|
/* Any CSS background, a gradient too. */
|
|
27
34
|
colorOf(id: Id): string;
|
|
35
|
+
|
|
28
36
|
labelOf(id: Id): string;
|
|
29
37
|
disabled?: boolean;
|
|
30
38
|
align?: 'start' | 'end';
|
|
@@ -38,17 +46,21 @@ interface Spot<Id> {
|
|
|
38
46
|
top: number;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
/*
|
|
49
|
+
/* Top left corners in whole pixels, the rings evenly apart from the outer one to the middle. */
|
|
42
50
|
function layout<Id>(rings: readonly (readonly Id[])[], center: Id | undefined): { size: number; spots: Spot<Id>[] } {
|
|
43
51
|
const outer = rings[0]?.length ?? 0;
|
|
44
52
|
const radius = Math.max(MIN_RADIUS, outer > 1 ? Math.ceil((SWATCH_SIZE + MIN_GAP) / (2 * Math.sin(Math.PI / outer))) : 0);
|
|
45
53
|
const size = 2 * (radius + SWATCH_SIZE / 2 + EDGE);
|
|
46
54
|
const middle = (size - SWATCH_SIZE) / 2;
|
|
55
|
+
|
|
47
56
|
const spot = (id: Id, turn: number, distance: number): Spot<Id> => {
|
|
48
57
|
const angle = turn * 2 * Math.PI - Math.PI / 2;
|
|
58
|
+
|
|
49
59
|
return { id, left: Math.round(middle + distance * Math.cos(angle)), top: Math.round(middle + distance * Math.sin(angle)) };
|
|
50
60
|
};
|
|
61
|
+
|
|
51
62
|
const spots = rings.flatMap((ring, depth) => ring.map((id, i) => spot(id, i / ring.length, (radius * (rings.length - depth)) / rings.length)));
|
|
63
|
+
|
|
52
64
|
return { size, spots: center === undefined ? spots : [...spots, spot(center, 0, 0)] };
|
|
53
65
|
}
|
|
54
66
|
|
|
@@ -62,11 +74,8 @@ const STEPS: Record<string, (index: number, count: number) => number> = {
|
|
|
62
74
|
};
|
|
63
75
|
|
|
64
76
|
/*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* caller can show it in place; its name stands under the wheel, which is why a swatch carries no
|
|
68
|
-
* tooltip. The wheel is one tab stop: the arrows walk it in its order and only Enter, Space or a click
|
|
69
|
-
* picks. Every preview ends with null, after a pick as well as on closing without one or on going away while open.
|
|
77
|
+
* The looked-at swatch's name stands under the wheel, so a swatch carries no tooltip. Only Enter, Space
|
|
78
|
+
* or a click picks. Every preview ends with null: after a pick, on closing, or on going away while open.
|
|
70
79
|
*/
|
|
71
80
|
export function ColorWheelPicker<Id extends string>({
|
|
72
81
|
value,
|
|
@@ -92,16 +101,18 @@ export function ColorWheelPicker<Id extends string>({
|
|
|
92
101
|
const tabStop = spots.find((spot) => spot.id === (focused ?? value))?.id ?? spots[0]?.id;
|
|
93
102
|
// Read by the cleanup below, which runs after the state and the props of its render are gone.
|
|
94
103
|
const preview = useRef({ looking: false, onPreviewChange });
|
|
104
|
+
|
|
95
105
|
useEffect(() => {
|
|
96
106
|
preview.current.onPreviewChange = onPreviewChange;
|
|
97
107
|
});
|
|
108
|
+
|
|
98
109
|
const look = (id: Id | null): void => {
|
|
99
110
|
setLooking(id);
|
|
100
111
|
preview.current.looking = id !== null;
|
|
101
112
|
onPreviewChange?.(id);
|
|
102
113
|
};
|
|
103
|
-
|
|
104
|
-
//
|
|
114
|
+
|
|
115
|
+
// A picker taken away while a color is previewed gets no close of its popover, so it ends the preview itself.
|
|
105
116
|
useEffect(
|
|
106
117
|
() => () => {
|
|
107
118
|
if (preview.current.looking) {
|
|
@@ -110,21 +121,26 @@ export function ColorWheelPicker<Id extends string>({
|
|
|
110
121
|
},
|
|
111
122
|
[]
|
|
112
123
|
);
|
|
124
|
+
|
|
113
125
|
const close = (): void => {
|
|
114
126
|
setOpen(false);
|
|
115
127
|
setFocused(null);
|
|
116
128
|
look(null);
|
|
117
129
|
};
|
|
130
|
+
|
|
118
131
|
const step = (event: KeyboardEvent<HTMLDivElement>): void => {
|
|
119
132
|
const next = STEPS[event.key];
|
|
120
133
|
const swatches = [...(wheel.current?.querySelectorAll<HTMLElement>('[role="radio"]') ?? [])];
|
|
121
134
|
const index = swatches.indexOf(event.target as HTMLElement);
|
|
135
|
+
|
|
122
136
|
if (next === undefined || index < 0) {
|
|
123
137
|
return;
|
|
124
138
|
}
|
|
139
|
+
|
|
125
140
|
event.preventDefault();
|
|
126
141
|
swatches[next(index, swatches.length)]?.focus();
|
|
127
142
|
};
|
|
143
|
+
|
|
128
144
|
return (
|
|
129
145
|
<PopoverRoot
|
|
130
146
|
open={open}
|
|
@@ -5,6 +5,7 @@ import type { ColumnEdge } from './useColumnResize.ts';
|
|
|
5
5
|
export interface ColumnResizeHandleProps {
|
|
6
6
|
/* The edge the column hangs from, as handed to `useColumnResize`; the handle sits on the other one. */
|
|
7
7
|
from: ColumnEdge;
|
|
8
|
+
|
|
8
9
|
onPointerDown(event: ReactPointerEvent<HTMLElement>): void;
|
|
9
10
|
className?: string;
|
|
10
11
|
style?: CSSProperties;
|
package/src/CopyButton.tsx
CHANGED
|
@@ -11,8 +11,10 @@ const COPIED_MS = 1500;
|
|
|
11
11
|
|
|
12
12
|
interface CopyButtonBase {
|
|
13
13
|
text: string;
|
|
14
|
+
|
|
14
15
|
/* The accessible name and the tooltip of the icon, or the word on the button with `showLabel`. */
|
|
15
16
|
label?: string;
|
|
17
|
+
|
|
16
18
|
disabled?: boolean;
|
|
17
19
|
className?: string;
|
|
18
20
|
ref?: Ref<HTMLButtonElement>;
|
|
@@ -42,12 +44,15 @@ export function CopyButton(props: CopyButtonProps) {
|
|
|
42
44
|
if (copiedAt === null) {
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
47
|
+
|
|
45
48
|
const timer = window.setTimeout(() => setCopiedAt(null), COPIED_MS);
|
|
49
|
+
|
|
46
50
|
return () => window.clearTimeout(timer);
|
|
47
51
|
}, [copiedAt]);
|
|
48
52
|
|
|
49
53
|
const copied = copiedAt !== null;
|
|
50
54
|
const name = label ?? t('action.copy');
|
|
55
|
+
|
|
51
56
|
const copy = async (): Promise<void> => {
|
|
52
57
|
// A refusal goes unconfirmed and unreported: a person can do nothing about it (`clipboard.ts`).
|
|
53
58
|
if (await copyText(text)) {
|
|
@@ -73,6 +78,7 @@ export function CopyButton(props: CopyButtonProps) {
|
|
|
73
78
|
onClick={() => void copy()}
|
|
74
79
|
/>
|
|
75
80
|
);
|
|
81
|
+
|
|
76
82
|
return (
|
|
77
83
|
<>
|
|
78
84
|
{button}
|
package/src/DisabledReason.tsx
CHANGED
package/src/DockShell.tsx
CHANGED
|
@@ -7,21 +7,16 @@ const REVEAL_ZONE = 120;
|
|
|
7
7
|
|
|
8
8
|
export interface DockShellProps extends HTMLAttributes<HTMLDivElement> {
|
|
9
9
|
children: ReactNode;
|
|
10
|
-
/* The dock waits below the window's edge until the pointer comes down to it. */
|
|
11
10
|
autoHide?: boolean;
|
|
12
|
-
/* Classes for the bar itself, such as letting a dock with many buttons wrap. */
|
|
13
11
|
barClassName?: string;
|
|
14
|
-
/* Told whenever the bar slides away or comes back, for what stands on top of it. */
|
|
15
12
|
onHiddenChange?: (hidden: boolean) => void;
|
|
16
13
|
ref?: Ref<HTMLDivElement>;
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
/*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* buttons stay in the tab order while it is out of sight, so focus brings it back. Something that
|
|
24
|
-
* moves with the dock marks itself `data-holds-dock`, so reaching for it does not hide the dock.
|
|
17
|
+
* An open menu keeps a hiding dock up, or it would take its own popup with it. The buttons stay in the
|
|
18
|
+
* tab order while out of sight, so focus brings it back. Something that moves with the dock marks itself
|
|
19
|
+
* `data-holds-dock`, so reaching for it does not hide the dock.
|
|
25
20
|
*/
|
|
26
21
|
export function DockShell({ children, autoHide = false, className, barClassName, onHiddenChange, ref, ...rest }: DockShellProps) {
|
|
27
22
|
const barRef = useRef<HTMLDivElement>(null);
|
|
@@ -31,23 +26,27 @@ export function DockShell({ children, autoHide = false, className, barClassName,
|
|
|
31
26
|
if (!autoHide) {
|
|
32
27
|
return;
|
|
33
28
|
}
|
|
29
|
+
|
|
34
30
|
let pointerNear = false;
|
|
31
|
+
|
|
35
32
|
const evaluate = (): void => {
|
|
36
33
|
const bar = barRef.current;
|
|
37
34
|
const holds = bar !== null && (bar.querySelector('[data-popup-open]') !== null || bar.querySelector(':focus-visible') !== null);
|
|
38
35
|
setRevealed(pointerNear || holds);
|
|
39
36
|
};
|
|
37
|
+
|
|
40
38
|
const onMove = (event: PointerEvent): void => {
|
|
41
|
-
/* Something that moves with the dock would slide away under the pointer reaching for it,
|
|
42
|
-
so over one the dock stays as it is. */
|
|
43
39
|
if (event.target instanceof Element && event.target.closest('[data-holds-dock]') !== null) {
|
|
44
40
|
return;
|
|
45
41
|
}
|
|
42
|
+
|
|
46
43
|
pointerNear = window.innerHeight - event.clientY <= REVEAL_ZONE;
|
|
47
44
|
evaluate();
|
|
48
45
|
};
|
|
46
|
+
|
|
49
47
|
window.addEventListener('pointermove', onMove);
|
|
50
48
|
window.addEventListener('focusin', evaluate);
|
|
49
|
+
|
|
51
50
|
return () => {
|
|
52
51
|
window.removeEventListener('pointermove', onMove);
|
|
53
52
|
window.removeEventListener('focusin', evaluate);
|
|
@@ -56,9 +55,11 @@ export function DockShell({ children, autoHide = false, className, barClassName,
|
|
|
56
55
|
}, [autoHide]);
|
|
57
56
|
|
|
58
57
|
const hidden = autoHide && !revealed;
|
|
58
|
+
|
|
59
59
|
useEffect(() => {
|
|
60
60
|
onHiddenChange?.(hidden);
|
|
61
61
|
}, [hidden, onHiddenChange]);
|
|
62
|
+
|
|
62
63
|
return (
|
|
63
64
|
<div ref={ref} {...rest} className={clsx('pointer-events-none absolute inset-x-0 bottom-4 flex justify-center', className)}>
|
|
64
65
|
<Surface
|
package/src/EmptyState.tsx
CHANGED
|
@@ -8,21 +8,25 @@ import { Spinner } from './Spinner.tsx';
|
|
|
8
8
|
const ICON_SIZE = 20;
|
|
9
9
|
|
|
10
10
|
export interface EmptyStateProps {
|
|
11
|
-
/* A Lucide icon, or a mark of another kind
|
|
11
|
+
/* A Lucide icon, or a mark of another kind that takes a `size`. */
|
|
12
12
|
icon?: LucideIcon | ReactElement<{ size?: number }>;
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
/* A `Spinner` in place of the icon. */
|
|
14
15
|
busy?: boolean;
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
/* For a state that is an outcome rather than a list with nothing in it. */
|
|
16
18
|
title?: ReactNode;
|
|
17
|
-
|
|
19
|
+
|
|
20
|
+
/* One sentence: what is missing, and what puts something there. */
|
|
18
21
|
children: ReactNode;
|
|
19
|
-
|
|
22
|
+
|
|
23
|
+
/* Kept to one. */
|
|
20
24
|
action?: ReactNode;
|
|
25
|
+
|
|
21
26
|
className?: string;
|
|
22
27
|
ref?: Ref<HTMLDivElement>;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
/* What a list, a canvas or a thread shows before it holds anything. */
|
|
26
30
|
export function EmptyState({ icon, busy = false, title, children, action, className, ref }: EmptyStateProps) {
|
|
27
31
|
return (
|
|
28
32
|
<div ref={ref} className={clsx('flex flex-col items-center justify-center gap-2 px-6 py-8 text-center', className)}>
|
package/src/ErrorBoundary.tsx
CHANGED
|
@@ -9,16 +9,20 @@ import { errorMessageOf, errorReport, shouldReset, type ResetKeys } from './erro
|
|
|
9
9
|
import { Icon } from './Icon.tsx';
|
|
10
10
|
|
|
11
11
|
export interface ErrorBoundaryProps {
|
|
12
|
-
/*
|
|
12
|
+
/* The first line of the message: "Could not show this view". */
|
|
13
13
|
label: string;
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
resetKeys?: ResetKeys;
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
/* Drops the icon and the spacing, for a small surface. */
|
|
17
18
|
compact?: boolean;
|
|
18
|
-
|
|
19
|
+
|
|
20
|
+
/* For the last resort, which has nothing around it to fall back to. */
|
|
19
21
|
reload?: boolean;
|
|
20
|
-
|
|
22
|
+
|
|
23
|
+
/* By default the message covers the positioned parent. */
|
|
21
24
|
className?: string;
|
|
25
|
+
|
|
22
26
|
children: ReactNode;
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -43,6 +47,7 @@ interface FallbackProps {
|
|
|
43
47
|
/* A function component, so the words come from the i18next instance the app renders with. */
|
|
44
48
|
function ErrorFallback({ label, error, componentStack, compact, reload, className, onRetry }: FallbackProps) {
|
|
45
49
|
const { t } = useTranslation('ui');
|
|
50
|
+
|
|
46
51
|
return (
|
|
47
52
|
<div role="alert" className={clsx('grid place-items-center overflow-auto bg-surface', compact ? 'p-3' : 'p-6', className)}>
|
|
48
53
|
<div className={clsx('flex max-w-[360px] flex-col items-center text-center', compact ? 'gap-1' : 'gap-2')}>
|
|
@@ -68,11 +73,7 @@ function ErrorFallback({ label, error, componentStack, compact, reload, classNam
|
|
|
68
73
|
);
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
/*
|
|
72
|
-
* Keeps a render failure to the subtree it happened in. Without one React unmounts the whole tree,
|
|
73
|
-
* and whatever lives in it (an embedded page, a running terminal) goes down with it. Key it so it
|
|
74
|
-
* resets only while it holds an error: `resetKeys` for what the children draw from.
|
|
75
|
-
*/
|
|
76
|
+
/* Without one React unmounts the whole tree, and whatever lives in it (an embedded page, a running terminal) goes down with it. */
|
|
76
77
|
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
77
78
|
override state: ErrorBoundaryState = CLEAR;
|
|
78
79
|
|
|
@@ -99,7 +100,9 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
99
100
|
if (!this.state.failed) {
|
|
100
101
|
return this.props.children;
|
|
101
102
|
}
|
|
103
|
+
|
|
102
104
|
const { label, compact = false, reload = false, className = 'absolute inset-0' } = this.props;
|
|
105
|
+
|
|
103
106
|
return (
|
|
104
107
|
<ErrorFallback
|
|
105
108
|
label={label}
|
package/src/Field.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { SectionLabel } from './SectionLabel.tsx';
|
|
|
6
6
|
|
|
7
7
|
const HINT = 'text-xs text-text-muted';
|
|
8
8
|
const ERROR = 'text-xs text-status-error';
|
|
9
|
+
|
|
9
10
|
// Beside the control the label reads at the size of what is typed, on the middle of the control's first line however tall it grows.
|
|
10
11
|
const LABEL_BESIDE = 'flex h-8 items-center self-start text-sm text-text-muted';
|
|
11
12
|
|
|
@@ -13,7 +14,6 @@ const isShown = (line: ReactNode): boolean => line !== undefined && line !== nul
|
|
|
13
14
|
|
|
14
15
|
export type FieldHintProps = useRender.ComponentProps<'p'>;
|
|
15
16
|
|
|
16
|
-
/* The line under a field that says what goes in it. */
|
|
17
17
|
export function FieldHint({ render, className, ref, ...props }: FieldHintProps) {
|
|
18
18
|
return useRender({
|
|
19
19
|
render,
|
|
@@ -25,7 +25,7 @@ export function FieldHint({ render, className, ref, ...props }: FieldHintProps)
|
|
|
25
25
|
|
|
26
26
|
export type FormErrorProps = useRender.ComponentProps<'p'>;
|
|
27
27
|
|
|
28
|
-
/*
|
|
28
|
+
/* An alert, so a screen reader hears it arrive. */
|
|
29
29
|
export function FormError({ render, className, ref, ...props }: FormErrorProps) {
|
|
30
30
|
return useRender({
|
|
31
31
|
render,
|
|
@@ -48,6 +48,7 @@ function FieldLabel({ id, group, horizontal, children }: { id: string; group: bo
|
|
|
48
48
|
</label>
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
|
+
|
|
51
52
|
return group ? (
|
|
52
53
|
<SectionLabel id={`${id}-label`}>{children}</SectionLabel>
|
|
53
54
|
) : (
|
|
@@ -58,26 +59,22 @@ function FieldLabel({ id, group, horizontal, children }: { id: string; group: bo
|
|
|
58
59
|
export interface FieldProps {
|
|
59
60
|
label?: ReactNode;
|
|
60
61
|
hint?: ReactNode;
|
|
62
|
+
|
|
61
63
|
/* Shown under the control and marks it invalid; null or empty shows nothing. */
|
|
62
64
|
error?: ReactNode;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
* in a box with a button beside it. The label then names a group around it, which the hint and
|
|
66
|
-
* the error describe.
|
|
67
|
-
*/
|
|
65
|
+
|
|
66
|
+
/* For what a `<label>` cannot point at, such as a `Segmented`: the label then names a group around it. */
|
|
68
67
|
group?: boolean;
|
|
69
|
-
|
|
68
|
+
|
|
70
69
|
orientation?: 'vertical' | 'horizontal';
|
|
71
70
|
className?: string;
|
|
72
71
|
ref?: Ref<HTMLDivElement>;
|
|
73
|
-
|
|
72
|
+
|
|
73
|
+
/* An `Input` or a `TextArea` connects to the label and the lines under it by itself, unless the field is a `group`. */
|
|
74
74
|
children: ReactNode;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/*
|
|
78
|
-
* A labelled control, or a labelled group, with an optional hint and error under it, stacked 6px apart.
|
|
79
|
-
* Beside each other the label column is 112px, so the controls of every field in a form line up.
|
|
80
|
-
*/
|
|
77
|
+
/* Horizontal, the label column is 112px, so the controls of every field in a form line up. */
|
|
81
78
|
export function Field({ label, hint, error, group = false, orientation = 'vertical', className, ref, children }: FieldProps) {
|
|
82
79
|
const horizontal = orientation === 'horizontal';
|
|
83
80
|
const id = useId();
|
|
@@ -87,6 +84,7 @@ export function Field({ label, hint, error, group = false, orientation = 'vertic
|
|
|
87
84
|
const lines = [hasHint ? `${id}-hint` : null, hasError ? `${id}-error` : null].filter((part) => part !== null);
|
|
88
85
|
const describedBy = lines.length > 0 ? lines.join(' ') : undefined;
|
|
89
86
|
const control = group ? null : { id: `${id}-control`, describedBy, invalid: hasError };
|
|
87
|
+
|
|
90
88
|
return (
|
|
91
89
|
<div
|
|
92
90
|
ref={ref}
|
package/src/FileIcon.tsx
CHANGED
|
@@ -5,20 +5,20 @@ import { fileIconFor, mountFileIconSprite } from './file-icon.ts';
|
|
|
5
5
|
export interface FileIconProps {
|
|
6
6
|
/* Absolute or relative; only the last segment decides which icon this is. */
|
|
7
7
|
path: string;
|
|
8
|
+
|
|
8
9
|
size?: number;
|
|
9
10
|
className?: string;
|
|
10
11
|
ref?: Ref<SVGSVGElement>;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/*
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
16
|
-
* a TypeScript blue or a Vue green is the mark of the file type, not a theme choice. Decorative like
|
|
17
|
-
* every other icon here, so the name beside it does the reading.
|
|
15
|
+
* The one place the library steps outside Lucide. The glyphs are Seti's, and so are their colors: a
|
|
16
|
+
* TypeScript blue is the mark of the file type, not a theme choice. Decorative like every other icon here.
|
|
18
17
|
*/
|
|
19
18
|
export function FileIcon({ path, size = 16, className, ref }: FileIconProps) {
|
|
20
19
|
// Before the browser paints, so a `<use>` never points at a symbol that is not in the document yet.
|
|
21
20
|
useInsertionEffect(mountFileIconSprite, []);
|
|
21
|
+
|
|
22
22
|
return (
|
|
23
23
|
<svg ref={ref} className={clsx('file-icon', className)} width={size} height={size} viewBox="0 0 16 16" aria-hidden>
|
|
24
24
|
<use href={`#${fileIconFor(path)}`} />
|
package/src/Icon.tsx
CHANGED
|
@@ -13,10 +13,8 @@ export interface IconProps {
|
|
|
13
13
|
ref?: Ref<SVGSVGElement>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/*
|
|
17
|
-
sits on the button or the label next to it, so the glyph stays hidden. */
|
|
16
|
+
/* Decorative: the name a screen reader reads sits on the button or the label next to it. */
|
|
18
17
|
export function Icon({ icon: Glyph, size = 16, className, ref }: IconProps) {
|
|
19
|
-
// An inline svg sits on the text baseline, which drops a square glyph below the words next to
|
|
20
|
-
// it; the middle of the box against the middle of the text is what reads as aligned.
|
|
18
|
+
// An inline svg sits on the text baseline, which drops a square glyph below the words next to it.
|
|
21
19
|
return <Glyph ref={ref} size={size} strokeWidth={STROKE_WIDTH} className={clsx('align-middle', className)} aria-hidden />;
|
|
22
20
|
}
|
package/src/IconButton.tsx
CHANGED
|
@@ -13,26 +13,33 @@ export type { IconButtonSize };
|
|
|
13
13
|
export interface IconButtonProps extends Omit<ComponentProps<'button'>, 'children'> {
|
|
14
14
|
/* Left out for a button that draws something other than one Lucide glyph, handed in as children. */
|
|
15
15
|
icon?: LucideIcon;
|
|
16
|
+
|
|
16
17
|
/* The accessible name, and the tooltip unless `tooltip` says otherwise. */
|
|
17
18
|
label: string;
|
|
19
|
+
|
|
18
20
|
/* A tooltip that says more than the name (a reason it is disabled), or `false` for none at all. */
|
|
19
21
|
tooltip?: ReactNode | false;
|
|
22
|
+
|
|
20
23
|
kbd?: Shortcut | string;
|
|
21
24
|
tooltipSide?: TooltipSide;
|
|
22
25
|
size?: IconButtonSize;
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
/* Drawn as a pressed key. Use `aria-pressed` instead where the button is a real toggle. */
|
|
24
28
|
active?: boolean;
|
|
25
|
-
|
|
29
|
+
|
|
30
|
+
/* A `Spinner` in place of the icon. */
|
|
26
31
|
busy?: boolean;
|
|
27
|
-
|
|
32
|
+
|
|
33
|
+
/* For the icon rather than the button. The size stays the button's. */
|
|
28
34
|
iconClassName?: string;
|
|
35
|
+
|
|
29
36
|
/* Drawn after the icon: a count, or a word beside it together with `w-auto`. */
|
|
30
37
|
children?: ReactNode;
|
|
31
|
-
|
|
38
|
+
|
|
32
39
|
render?: useRender.RenderProp;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
/*
|
|
42
|
+
/* The one place the icon inside an icon button is sized. */
|
|
36
43
|
export function IconButton({
|
|
37
44
|
icon,
|
|
38
45
|
label,
|
|
@@ -69,9 +76,11 @@ export function IconButton({
|
|
|
69
76
|
)
|
|
70
77
|
}
|
|
71
78
|
});
|
|
79
|
+
|
|
72
80
|
if (tooltip === false) {
|
|
73
81
|
return button;
|
|
74
82
|
}
|
|
83
|
+
|
|
75
84
|
return (
|
|
76
85
|
<Tooltip label={tooltip ?? label} kbd={kbd} side={tooltipSide}>
|
|
77
86
|
{button as ReactElement<Record<string, unknown>>}
|