@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
|
@@ -31,22 +31,28 @@ function virtualRowsOf<ItemValue, Group extends { items: readonly ItemValue[] }>
|
|
|
31
31
|
const rows: VirtualPaletteRow<ItemValue>[] = [];
|
|
32
32
|
const itemRows: number[] = [];
|
|
33
33
|
let count = 0;
|
|
34
|
+
|
|
34
35
|
for (const value of values) {
|
|
35
36
|
const grouped = value !== null && typeof value === 'object' && 'items' in value && Array.isArray(value.items);
|
|
36
37
|
const group = grouped ? (value as Group) : null;
|
|
38
|
+
|
|
37
39
|
if (group !== null && group.items.length === 0) {
|
|
38
40
|
continue;
|
|
39
41
|
}
|
|
42
|
+
|
|
40
43
|
const groupIndex = groups.length;
|
|
41
44
|
groups.push(group);
|
|
45
|
+
|
|
42
46
|
if (group !== null && labelled) {
|
|
43
47
|
rows.push({ group: groupIndex, index: -1 });
|
|
44
48
|
}
|
|
49
|
+
|
|
45
50
|
for (const item of group === null ? [value as ItemValue] : group.items) {
|
|
46
51
|
itemRows.push(rows.length);
|
|
47
52
|
rows.push({ group: groupIndex, index: count++, item });
|
|
48
53
|
}
|
|
49
54
|
}
|
|
55
|
+
|
|
50
56
|
return { groups, rows, itemRows, count };
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -64,6 +70,7 @@ export function CommandPaletteVirtualList<ItemValue, Group extends { items: read
|
|
|
64
70
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
65
71
|
const labelled = groupLabel !== undefined;
|
|
66
72
|
const { groups, rows, itemRows, count } = useMemo(() => virtualRowsOf<ItemValue, Group>(filtered, labelled), [filtered, labelled]);
|
|
73
|
+
|
|
67
74
|
// oxlint-disable-next-line react/incompatible-library
|
|
68
75
|
const virtualizer = useVirtualizer({
|
|
69
76
|
count: rows.length,
|
|
@@ -74,38 +81,48 @@ export function CommandPaletteVirtualList<ItemValue, Group extends { items: read
|
|
|
74
81
|
scrollPaddingStart: 6,
|
|
75
82
|
scrollPaddingEnd: 6
|
|
76
83
|
});
|
|
84
|
+
|
|
77
85
|
useLayoutEffect(() => {
|
|
78
86
|
if (scroll === null) {
|
|
79
87
|
return;
|
|
80
88
|
}
|
|
89
|
+
|
|
81
90
|
const highlight = (index: number, reason: string): void => {
|
|
82
91
|
const row = itemRows[index];
|
|
92
|
+
|
|
83
93
|
if (row !== undefined && reason !== 'pointer') {
|
|
84
94
|
virtualizer.scrollToIndex(row, { align: reason === 'none' ? 'start' : 'auto' });
|
|
85
95
|
}
|
|
86
96
|
};
|
|
97
|
+
|
|
87
98
|
scroll.current = highlight;
|
|
99
|
+
|
|
88
100
|
return () => {
|
|
89
101
|
if (scroll.current === highlight) {
|
|
90
102
|
scroll.current = null;
|
|
91
103
|
}
|
|
92
104
|
};
|
|
93
105
|
}, [scroll, virtualizer, itemRows]);
|
|
106
|
+
|
|
94
107
|
const visible = virtualizer.getVirtualItems();
|
|
95
108
|
const visibleGroups = new Map<number, typeof visible>();
|
|
109
|
+
|
|
96
110
|
for (const item of visible) {
|
|
97
111
|
const group = rows[item.index]!.group;
|
|
98
112
|
const current = visibleGroups.get(group);
|
|
113
|
+
|
|
99
114
|
if (current === undefined) {
|
|
100
115
|
visibleGroups.set(group, [item]);
|
|
101
116
|
} else {
|
|
102
117
|
current.push(item);
|
|
103
118
|
}
|
|
104
119
|
}
|
|
120
|
+
|
|
105
121
|
const renderRows = (items: typeof visible): ReactNode =>
|
|
106
122
|
items.map((item) => {
|
|
107
123
|
const row = rows[item.index]!;
|
|
108
124
|
const group = groups[row.group];
|
|
125
|
+
|
|
109
126
|
return (
|
|
110
127
|
<div
|
|
111
128
|
key={item.key}
|
|
@@ -123,15 +140,19 @@ export function CommandPaletteVirtualList<ItemValue, Group extends { items: read
|
|
|
123
140
|
</div>
|
|
124
141
|
);
|
|
125
142
|
});
|
|
143
|
+
|
|
126
144
|
return (
|
|
127
145
|
<CommandPaletteList {...props} ref={mergeRefs(ref, scrollRef)}>
|
|
128
146
|
<div role="presentation" className="relative" style={{ height: virtualizer.getTotalSize() }}>
|
|
129
147
|
{[...visibleGroups].map(([index, items]) => {
|
|
130
148
|
const group = groups[index];
|
|
149
|
+
|
|
131
150
|
if (group === null || group === undefined) {
|
|
132
151
|
return <Fragment key={index}>{renderRows(items)}</Fragment>;
|
|
133
152
|
}
|
|
153
|
+
|
|
134
154
|
const labelVisible = items.some((item) => rows[item.index]!.index === -1);
|
|
155
|
+
|
|
135
156
|
return (
|
|
136
157
|
<Autocomplete.Group key={index} items={group.items}>
|
|
137
158
|
{!labelVisible && groupLabel !== undefined && (
|
package/src/dialog/parts.tsx
CHANGED
|
@@ -5,10 +5,9 @@ import { useRender } from '@base-ui/react/use-render';
|
|
|
5
5
|
import { withClass } from '../class-name.ts';
|
|
6
6
|
import { useDialogLayer } from '../dialog-layer.ts';
|
|
7
7
|
|
|
8
|
-
/* Whether the dialog is open, for its popup to decide whether it stacks over another one. */
|
|
9
8
|
const DialogOpenContext = createContext(false);
|
|
10
9
|
|
|
11
|
-
/*
|
|
10
|
+
/* Every question is as wide as the others, whatever it asks. */
|
|
12
11
|
const SIZE = {
|
|
13
12
|
sm: 'w-[420px] p-5'
|
|
14
13
|
} as const;
|
|
@@ -16,6 +15,7 @@ const SIZE = {
|
|
|
16
15
|
export function DialogRoot({ open, defaultOpen, onOpenChange, ...props }: ComponentProps<typeof BaseDialog.Root>) {
|
|
17
16
|
// Followed rather than taken over, so a dialog that keeps its own state still does.
|
|
18
17
|
const [seen, setSeen] = useState(defaultOpen ?? false);
|
|
18
|
+
|
|
19
19
|
return (
|
|
20
20
|
<DialogOpenContext value={open ?? seen}>
|
|
21
21
|
<BaseDialog.Root
|
|
@@ -23,6 +23,7 @@ export function DialogRoot({ open, defaultOpen, onOpenChange, ...props }: Compon
|
|
|
23
23
|
defaultOpen={defaultOpen}
|
|
24
24
|
onOpenChange={(next, details) => {
|
|
25
25
|
onOpenChange?.(next, details);
|
|
26
|
+
|
|
26
27
|
if (!details.isCanceled) {
|
|
27
28
|
setSeen(next);
|
|
28
29
|
}
|
|
@@ -40,23 +41,19 @@ export const DialogClose = BaseDialog.Close;
|
|
|
40
41
|
export type DialogPopupProps = ComponentProps<typeof BaseDialog.Popup> & {
|
|
41
42
|
/* `sm` is the dialog of a question or a few fields. Without a size the popup is as wide as its `className` says. */
|
|
42
43
|
size?: keyof typeof SIZE;
|
|
44
|
+
|
|
43
45
|
/* Always over another dialog. A dialog that opens while another one is up stacks by itself. */
|
|
44
46
|
nested?: boolean;
|
|
45
|
-
|
|
47
|
+
|
|
46
48
|
backdrop?: boolean;
|
|
47
|
-
/* Extra on the backdrop, such as the heavier dim a picture needs. */
|
|
48
49
|
backdropClassName?: string;
|
|
49
|
-
/* Keeps the popup in the document while it is closed. */
|
|
50
50
|
keepMounted?: boolean;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
/*
|
|
54
|
-
* The portal, the backdrop and the popup of a dialog in one part. A dialog that opens while another
|
|
55
|
-
* one is up, such as one from the menu bar over a settings dialog, takes the nested steps and a
|
|
56
|
-
* backdrop of its own, or the one underneath stays on top and keeps taking clicks.
|
|
57
|
-
*/
|
|
53
|
+
/* The portal, the backdrop and the popup of a dialog in one part; `useDialogLayer` says whether it stacks. */
|
|
58
54
|
export function DialogPopup({ size, nested = false, backdrop = true, backdropClassName, keepMounted, className, ...props }: DialogPopupProps) {
|
|
59
55
|
const stacked = useDialogLayer(useContext(DialogOpenContext)) || nested;
|
|
56
|
+
|
|
60
57
|
return (
|
|
61
58
|
<BaseDialog.Portal keepMounted={keepMounted}>
|
|
62
59
|
{backdrop && (
|
|
@@ -85,7 +82,7 @@ export type DialogDescriptionProps = ComponentProps<typeof BaseDialog.Descriptio
|
|
|
85
82
|
size?: keyof typeof DESCRIPTION;
|
|
86
83
|
};
|
|
87
84
|
|
|
88
|
-
/*
|
|
85
|
+
/* What a screen reader reads as the dialog's description. It carries no margin. */
|
|
89
86
|
export function DialogDescription({ size = 'sm', className, ...props }: DialogDescriptionProps) {
|
|
90
87
|
return <BaseDialog.Description className={withClass(DESCRIPTION[size], className)} {...props} />;
|
|
91
88
|
}
|
|
@@ -94,14 +91,14 @@ export type DialogTextProps = useRender.ComponentProps<'p'> & {
|
|
|
94
91
|
size?: keyof typeof DESCRIPTION;
|
|
95
92
|
};
|
|
96
93
|
|
|
97
|
-
/*
|
|
94
|
+
/* Styled as the description without being the dialog's description. */
|
|
98
95
|
export function DialogText({ size = 'sm', render, className, ref, ...props }: DialogTextProps) {
|
|
99
96
|
return useRender({ render, ref, defaultTagName: 'p', props: { ...props, className: clsx(DESCRIPTION[size], className) } });
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
export type DialogFooterProps = useRender.ComponentProps<'div'>;
|
|
103
100
|
|
|
104
|
-
/* The
|
|
101
|
+
/* The main action goes last, and so on the right. */
|
|
105
102
|
export function DialogFooter({ render, className, ref, ...props }: DialogFooterProps) {
|
|
106
103
|
return useRender({
|
|
107
104
|
render,
|
package/src/dialog-layer.ts
CHANGED
|
@@ -4,14 +4,14 @@ import { useLayoutEffect, useRef, useState } from 'react';
|
|
|
4
4
|
const open = new Set<symbol>();
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
|
-
* Whether a dialog opened
|
|
8
|
-
*
|
|
9
|
-
* `dialog-popup-nested`, a forced backdrop), or the one underneath stays on top and keeps taking clicks.
|
|
7
|
+
* Whether a dialog opened over another app-level dialog. Such a dialog takes the nested steps
|
|
8
|
+
* (`dialog-backdrop-nested`, `dialog-popup-nested`, a forced backdrop), or the one underneath stays on top and keeps taking clicks.
|
|
10
9
|
*/
|
|
11
10
|
export function useDialogLayer(isOpen: boolean): boolean {
|
|
12
11
|
const id = useRef(Symbol('dialog'));
|
|
13
12
|
// Decided in render at the moment the dialog opens, so its first frame is already on the right step.
|
|
14
13
|
const [seen, setSeen] = useState({ open: false, stacked: false });
|
|
14
|
+
|
|
15
15
|
if (seen.open !== isOpen) {
|
|
16
16
|
setSeen({ open: isOpen, stacked: isOpen && open.size > 0 });
|
|
17
17
|
}
|
|
@@ -20,8 +20,10 @@ export function useDialogLayer(isOpen: boolean): boolean {
|
|
|
20
20
|
if (!isOpen) {
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
+
|
|
23
24
|
const self = id.current;
|
|
24
25
|
open.add(self);
|
|
26
|
+
|
|
25
27
|
return () => {
|
|
26
28
|
open.delete(self);
|
|
27
29
|
};
|
package/src/error-boundary.ts
CHANGED
|
@@ -1,40 +1,27 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The pure half of `ErrorBoundary`: when a failed subtree gets another try, and what a person copies
|
|
3
|
-
* out of the message. Kept apart so it can be tested without a DOM.
|
|
4
|
-
*/
|
|
1
|
+
/* The pure half of `ErrorBoundary`, kept apart so it can be tested without a DOM. */
|
|
5
2
|
|
|
6
3
|
/* Anything the subtree draws from. A change in any of them means the data may be whole again. */
|
|
7
4
|
export type ResetKeys = readonly unknown[];
|
|
8
5
|
|
|
9
|
-
/*
|
|
10
|
-
* Whether a boundary that holds an error should draw its children again. Only a boundary that
|
|
11
|
-
* failed resets. Comparing keys while the children are fine would remount a terminal or an embedded
|
|
12
|
-
* page every time the data under it changes.
|
|
13
|
-
*/
|
|
6
|
+
/* Only a failed boundary resets: comparing keys while the children are fine would remount a terminal or an embedded page on every change. */
|
|
14
7
|
export const shouldReset = (failed: boolean, previous: ResetKeys, next: ResetKeys): boolean => {
|
|
15
8
|
if (!failed) {
|
|
16
9
|
return false;
|
|
17
10
|
}
|
|
11
|
+
|
|
18
12
|
if (previous.length !== next.length) {
|
|
19
13
|
return true;
|
|
20
14
|
}
|
|
15
|
+
|
|
21
16
|
return previous.some((key, i) => !Object.is(key, next[i]));
|
|
22
17
|
};
|
|
23
18
|
|
|
24
19
|
/* A thrown value is not always an Error, and the message has to say something either way. */
|
|
25
|
-
export const errorMessageOf = (error: unknown): string =>
|
|
26
|
-
if (error instanceof Error) {
|
|
27
|
-
return error.message || error.name;
|
|
28
|
-
}
|
|
29
|
-
return typeof error === 'string' ? error : String(error);
|
|
30
|
-
};
|
|
20
|
+
export const errorMessageOf = (error: unknown): string => (error instanceof Error ? error.message || error.name : String(error));
|
|
31
21
|
|
|
32
22
|
/* The text behind the copy button: what a bug report needs, the component stack included. */
|
|
33
23
|
export const errorReport = (label: string, error: unknown, componentStack: string | null): string => {
|
|
34
24
|
const stack = error instanceof Error && error.stack ? error.stack : errorMessageOf(error);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
parts.push(`Component stack:${componentStack}`);
|
|
38
|
-
}
|
|
39
|
-
return parts.join('\n\n');
|
|
25
|
+
|
|
26
|
+
return [label, stack, ...(componentStack ? [`Component stack:${componentStack}`] : [])].join('\n\n');
|
|
40
27
|
};
|
package/src/file-icon.ts
CHANGED
|
@@ -3,10 +3,8 @@ import { SETI_BY_FILE_EXTENSION, SETI_BY_FILE_NAME, SETI_BY_FILE_NAME_CONTAINS,
|
|
|
3
3
|
|
|
4
4
|
const SPRITE_ELEMENT_ID = 'adecore-ui-file-icon-sprite';
|
|
5
5
|
|
|
6
|
-
/*
|
|
7
|
-
|
|
8
|
-
sprite brings the rules that color it. Everything drawn beside a tree calls `fileIconFor`, so a tab,
|
|
9
|
-
a picker row and a tree row never disagree about what a file is. */
|
|
6
|
+
/* Seti knows far more file types than the tree's own sets. The tree colors only those, so every symbol
|
|
7
|
+
carries its hue in its id and the sprite brings the rules that color it. */
|
|
10
8
|
export const FILE_TREE_ICONS: FileTreeIconConfig = {
|
|
11
9
|
set: 'none',
|
|
12
10
|
spriteSheet: SETI_SPRITE_SHEET,
|
|
@@ -21,15 +19,13 @@ let resolver: ReturnType<typeof createFileTreeIconResolver> | null = null;
|
|
|
21
19
|
/* Built on the first icon rather than on import, so a bundle that never draws one never builds it. */
|
|
22
20
|
const resolveIcon: ReturnType<typeof createFileTreeIconResolver>['resolveIcon'] = (...args) => {
|
|
23
21
|
resolver ??= createFileTreeIconResolver(FILE_TREE_ICONS);
|
|
22
|
+
|
|
24
23
|
return resolver.resolveIcon(...args);
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
/*
|
|
28
|
-
* The id of
|
|
29
|
-
*
|
|
30
|
-
* a part of the name (`Dockerfile.dev`), then by the longest extension that matches (`spec.ts` before
|
|
31
|
-
* `ts`). Anything else falls back to the generic file icon, a directory included: the tree marks a
|
|
32
|
-
* folder with its chevron, not a glyph.
|
|
27
|
+
* The id of a `<symbol>` in the sprite `mountFileIconSprite` puts in the document. A directory gets the
|
|
28
|
+
* generic file icon: the tree marks a folder with its chevron, not a glyph.
|
|
33
29
|
*/
|
|
34
30
|
export const fileIconFor = (path: string): string => resolveIcon('file-tree-icon-file', path).name;
|
|
35
31
|
|
|
@@ -39,12 +35,15 @@ export const mountFileIconSprite = (): void => {
|
|
|
39
35
|
if (document.getElementById(SPRITE_ELEMENT_ID) !== null) {
|
|
40
36
|
return;
|
|
41
37
|
}
|
|
38
|
+
|
|
42
39
|
const holder = document.createElement('div');
|
|
43
40
|
holder.innerHTML = SETI_SPRITE_SHEET;
|
|
44
41
|
const sprite = holder.firstElementChild;
|
|
42
|
+
|
|
45
43
|
if (sprite === null) {
|
|
46
44
|
return;
|
|
47
45
|
}
|
|
46
|
+
|
|
48
47
|
sprite.id = SPRITE_ELEMENT_ID;
|
|
49
48
|
(document.body ?? document.documentElement).append(sprite);
|
|
50
49
|
};
|
|
@@ -21,10 +21,7 @@ export function directoryOfEmptyLine(path: string): string {
|
|
|
21
21
|
return path.slice(0, -EMPTY_LINE_NAME.length);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/*
|
|
25
|
-
* The lines to add and the ones to take away: an open directory whose label is not undefined and
|
|
26
|
-
* that holds nothing besides its own line gets one, every other line the wrapper added goes.
|
|
27
|
-
*/
|
|
24
|
+
/* An open, labeled directory that holds nothing besides its own line gets one; every other line the wrapper added goes. */
|
|
28
25
|
export function emptyLineChanges(
|
|
29
26
|
rows: readonly FileTreeVisibleRow[],
|
|
30
27
|
labelOf: FileTreeEmptyLabel,
|
|
@@ -32,74 +29,93 @@ export function emptyLineChanges(
|
|
|
32
29
|
added: Iterable<string>
|
|
33
30
|
): { add: string[]; remove: string[] } {
|
|
34
31
|
const wanted = new Set<string>();
|
|
32
|
+
|
|
35
33
|
rows.forEach((row, index) => {
|
|
36
34
|
if (row.kind !== 'directory' || !row.isExpanded) {
|
|
37
35
|
return;
|
|
38
36
|
}
|
|
37
|
+
|
|
39
38
|
const line = emptyLineOf(row.path);
|
|
39
|
+
|
|
40
40
|
for (let next = index + 1; next < rows.length && rows[next]!.depth > row.depth; next++) {
|
|
41
41
|
if (rows[next]!.path !== line) {
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
|
|
45
46
|
if (labelOf(row) !== undefined) {
|
|
46
47
|
wanted.add(line);
|
|
47
48
|
}
|
|
48
49
|
});
|
|
50
|
+
|
|
49
51
|
return {
|
|
50
52
|
add: [...wanted].filter((line) => !has(line)),
|
|
51
53
|
remove: [...added].filter((line) => !wanted.has(line) && has(line))
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
/*
|
|
56
|
-
* Where an arrow, Home or End goes instead when the row it would land on is a line: past it, or
|
|
57
|
-
* nowhere (the focused path itself) where nothing lies past it. Null when no line is in the way.
|
|
58
|
-
*/
|
|
57
|
+
/* Past the line the key would land on, or the focused path itself where nothing lies past it. Null when no line is in the way. */
|
|
59
58
|
export function pastEmptyLine(model: FileTree, key: string): string | null {
|
|
60
59
|
const index = model.getFocusedIndex();
|
|
61
60
|
const focused = model.getFocusedPath();
|
|
61
|
+
|
|
62
62
|
if (index < 0 || focused === null) {
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
|
+
|
|
65
66
|
const count = model.getVisibleCount();
|
|
67
|
+
|
|
66
68
|
const at = (position: number): FileTreeVisibleRow | null =>
|
|
67
69
|
position >= 0 && position < count ? (model.getVisibleRows(position, position)[0] ?? null) : null;
|
|
70
|
+
|
|
68
71
|
const lineAt = (position: number): boolean => {
|
|
69
72
|
const row = at(position);
|
|
73
|
+
|
|
70
74
|
return row !== null && isEmptyLine(row.path);
|
|
71
75
|
};
|
|
76
|
+
|
|
72
77
|
const pathAt = (position: number): string => {
|
|
73
78
|
const row = at(position);
|
|
79
|
+
|
|
74
80
|
return row === null ? focused : pathOfRow(row);
|
|
75
81
|
};
|
|
82
|
+
|
|
76
83
|
if ((key === 'ArrowDown' || key === 'ArrowRight') && lineAt(index + 1)) {
|
|
77
84
|
return key === 'ArrowDown' ? pathAt(index + 2) : focused;
|
|
78
85
|
}
|
|
86
|
+
|
|
79
87
|
if (key === 'ArrowUp' && lineAt(index - 1)) {
|
|
80
88
|
return pathAt(index - 2);
|
|
81
89
|
}
|
|
90
|
+
|
|
82
91
|
if (key === 'End' && lineAt(count - 1)) {
|
|
83
92
|
return pathAt(count - 2);
|
|
84
93
|
}
|
|
94
|
+
|
|
85
95
|
return null;
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
/* Focus that still lands on a line, through a key with a modifier or the engine giving focus back to the line's element, goes on the way the last key went. */
|
|
89
99
|
export function skipEmptyLine(model: FileTree, key: string): boolean {
|
|
90
100
|
const path = model.getFocusedPath();
|
|
101
|
+
|
|
91
102
|
if (path === null || !isEmptyLine(path)) {
|
|
92
103
|
return false;
|
|
93
104
|
}
|
|
105
|
+
|
|
94
106
|
if (key === 'ArrowUp' || key === 'PageUp') {
|
|
95
107
|
model.focusPreviousItem();
|
|
108
|
+
|
|
96
109
|
return true;
|
|
97
110
|
}
|
|
111
|
+
|
|
98
112
|
if (key === 'ArrowDown' || key === 'PageDown' || key === 'End') {
|
|
99
113
|
model.focusNextItem();
|
|
100
114
|
}
|
|
115
|
+
|
|
101
116
|
if (model.getFocusedPath() === path) {
|
|
102
117
|
model.focusPath(directoryOfEmptyLine(path));
|
|
103
118
|
}
|
|
119
|
+
|
|
104
120
|
return true;
|
|
105
121
|
}
|
package/src/file-tree/helpers.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface SortRow {
|
|
|
11
11
|
|
|
12
12
|
export function directoryHandle(model: FileTree, path: string): FileTreeDirectoryHandle | null {
|
|
13
13
|
const item = model.getItem(path);
|
|
14
|
+
|
|
14
15
|
return item?.isDirectory() ? (item as FileTreeDirectoryHandle) : null;
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -26,21 +27,26 @@ export function visibleRows(model: FileTree): FileTreeVisibleRow[] {
|
|
|
26
27
|
export function visibleRowsAround(model: FileTree, start: number, count: number): FileTreeVisibleRow[] {
|
|
27
28
|
const first = Math.max(0, start - 1);
|
|
28
29
|
const last = Math.min(model.getVisibleCount() - 1, start + count);
|
|
30
|
+
|
|
29
31
|
if (last < first) {
|
|
30
32
|
return [];
|
|
31
33
|
}
|
|
34
|
+
|
|
32
35
|
return model.getVisibleRows(first, last).map((row) => ({ ...row, path: pathOfRow(row) }));
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export function applyExpansion(model: FileTree, collapsed: ReadonlySet<string>, keyOf?: FoldKeyOf): void {
|
|
36
39
|
for (let pass = 0; pass < EXPANSION_PASSES; pass++) {
|
|
37
40
|
const { collapse, expand } = expansionChanges(visibleRows(model), collapsed, keyOf);
|
|
41
|
+
|
|
38
42
|
if (collapse.length === 0 && expand.length === 0) {
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
45
|
+
|
|
41
46
|
for (const path of collapse) {
|
|
42
47
|
directoryHandle(model, path)?.collapse();
|
|
43
48
|
}
|
|
49
|
+
|
|
44
50
|
for (const path of expand) {
|
|
45
51
|
directoryHandle(model, path)?.expand();
|
|
46
52
|
}
|
|
@@ -49,6 +55,7 @@ export function applyExpansion(model: FileTree, collapsed: ReadonlySet<string>,
|
|
|
49
55
|
|
|
50
56
|
export function resetExpandedPaths(model: FileTree, paths: readonly string[], expanded: ReadonlySet<string>): void {
|
|
51
57
|
model.resetPaths(paths, { initialExpandedPaths: [...expanded] });
|
|
58
|
+
|
|
52
59
|
// @pierre/trees beta.6 restores expansion with its default sort, so custom sorting needs a second pass.
|
|
53
60
|
for (const path of expanded) {
|
|
54
61
|
directoryHandle(model, path)?.expand();
|
|
@@ -58,10 +65,12 @@ export function resetExpandedPaths(model: FileTree, paths: readonly string[], ex
|
|
|
58
65
|
export function rowPathOf(event: { nativeEvent: Event }): string | null {
|
|
59
66
|
for (const node of event.nativeEvent.composedPath()) {
|
|
60
67
|
const path = node instanceof HTMLElement ? node.dataset.itemPath : undefined;
|
|
68
|
+
|
|
61
69
|
if (path) {
|
|
62
70
|
return path;
|
|
63
71
|
}
|
|
64
72
|
}
|
|
73
|
+
|
|
65
74
|
return null;
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -85,7 +94,9 @@ export function selectOnly(model: FileTree, path: string | null): void {
|
|
|
85
94
|
model.getItem(selected)?.deselect();
|
|
86
95
|
}
|
|
87
96
|
}
|
|
97
|
+
|
|
88
98
|
const item = path === null ? null : model.getItem(path);
|
|
99
|
+
|
|
89
100
|
if (item && !item.isSelected()) {
|
|
90
101
|
item.select();
|
|
91
102
|
}
|
|
@@ -93,13 +104,16 @@ export function selectOnly(model: FileTree, path: string | null): void {
|
|
|
93
104
|
|
|
94
105
|
export function followFocus(model: FileTree, onMoved?: (path: string) => void): () => void {
|
|
95
106
|
const from = model.getFocusedPath();
|
|
107
|
+
|
|
96
108
|
const timer = window.setTimeout(() => {
|
|
97
109
|
const path = model.getFocusedPath();
|
|
98
110
|
selectOnly(model, path);
|
|
111
|
+
|
|
99
112
|
if (path !== null && path !== from) {
|
|
100
113
|
onMoved?.(path);
|
|
101
114
|
}
|
|
102
115
|
}, 0);
|
|
116
|
+
|
|
103
117
|
return () => window.clearTimeout(timer);
|
|
104
118
|
}
|
|
105
119
|
|
|
@@ -107,6 +121,7 @@ export function focusRow(model: FileTree, path: string): boolean {
|
|
|
107
121
|
const selector = `[data-type="item"][data-item-path="${CSS.escape(path)}"]:not([data-item-parked="true"])`;
|
|
108
122
|
const row = model.getFileTreeContainer()?.shadowRoot?.querySelector<HTMLElement>(selector) ?? null;
|
|
109
123
|
row?.focus();
|
|
124
|
+
|
|
110
125
|
return row !== null;
|
|
111
126
|
}
|
|
112
127
|
|
|
@@ -115,10 +130,12 @@ export function ancestorDirsOf(treePath: string): string[] {
|
|
|
115
130
|
segments.pop();
|
|
116
131
|
const dirs: string[] = [];
|
|
117
132
|
let prefix = '';
|
|
133
|
+
|
|
118
134
|
for (const segment of segments) {
|
|
119
135
|
prefix = `${prefix}${segment}/`;
|
|
120
136
|
dirs.push(prefix);
|
|
121
137
|
}
|
|
138
|
+
|
|
122
139
|
return dirs;
|
|
123
140
|
}
|
|
124
141
|
|
|
@@ -132,29 +149,37 @@ export function newlyExpanded(before: ReadonlySet<string>, after: ReadonlySet<st
|
|
|
132
149
|
|
|
133
150
|
export function mergeExpanded(remembered: ReadonlySet<string>, reported: ReadonlySet<string>, known: ReadonlySet<string>): Set<string> {
|
|
134
151
|
const merged = new Set(reported);
|
|
152
|
+
|
|
135
153
|
for (const path of remembered) {
|
|
136
154
|
if (!known.has(path)) {
|
|
137
155
|
merged.add(path);
|
|
138
156
|
}
|
|
139
157
|
}
|
|
158
|
+
|
|
140
159
|
return merged;
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
export function compareRows(left: SortRow, right: SortRow): number {
|
|
144
163
|
const shared = Math.min(left.segments.length, right.segments.length);
|
|
164
|
+
|
|
145
165
|
for (let i = 0; i < shared; i++) {
|
|
146
166
|
const leftSegment = left.segments[i]!;
|
|
147
167
|
const rightSegment = right.segments[i]!;
|
|
168
|
+
|
|
148
169
|
if (leftSegment === rightSegment) {
|
|
149
170
|
continue;
|
|
150
171
|
}
|
|
172
|
+
|
|
151
173
|
const leftIsDirectory = left.isDirectory || i < left.segments.length - 1;
|
|
152
174
|
const rightIsDirectory = right.isDirectory || i < right.segments.length - 1;
|
|
175
|
+
|
|
153
176
|
if (leftIsDirectory !== rightIsDirectory) {
|
|
154
177
|
return leftIsDirectory ? -1 : 1;
|
|
155
178
|
}
|
|
179
|
+
|
|
156
180
|
return nameOrder(leftSegment, rightSegment);
|
|
157
181
|
}
|
|
182
|
+
|
|
158
183
|
// One path is the head of the other, so the shorter one is the directory the longer one sits in.
|
|
159
184
|
return left.segments.length - right.segments.length;
|
|
160
185
|
}
|
|
@@ -172,6 +197,7 @@ function plainKey(rowPath: string): string | null {
|
|
|
172
197
|
export function collapsedPathsOf(rows: readonly FileTreeVisibleRow[], keyOf: FoldKeyOf = plainKey): string[] {
|
|
173
198
|
return rows.flatMap((row) => {
|
|
174
199
|
const key = row.kind === 'directory' && !row.isExpanded ? keyOf(row.path) : null;
|
|
200
|
+
|
|
175
201
|
return key === null ? [] : [key];
|
|
176
202
|
});
|
|
177
203
|
}
|
|
@@ -181,11 +207,13 @@ export function mergeCollapsedPaths(current: string[], rows: readonly FileTreeVi
|
|
|
181
207
|
const folded = new Set(collapsedPathsOf(rows, keyOf));
|
|
182
208
|
const next = current.filter((dir) => !known.has(dir) || folded.has(dir));
|
|
183
209
|
const existing = new Set(current);
|
|
210
|
+
|
|
184
211
|
for (const dir of folded) {
|
|
185
212
|
if (!existing.has(dir)) {
|
|
186
213
|
next.push(dir);
|
|
187
214
|
}
|
|
188
215
|
}
|
|
216
|
+
|
|
189
217
|
// Selection also notifies subscribers; unchanged folds must not restart the shared-state cycle.
|
|
190
218
|
return next.length === current.length && next.every((dir, index) => dir === current[index]) ? current : next;
|
|
191
219
|
}
|
|
@@ -196,18 +224,24 @@ export function expansionChanges(
|
|
|
196
224
|
keyOf: FoldKeyOf = plainKey
|
|
197
225
|
): { collapse: string[]; expand: string[] } {
|
|
198
226
|
const changes: { collapse: string[]; expand: string[] } = { collapse: [], expand: [] };
|
|
227
|
+
|
|
199
228
|
for (const row of rows) {
|
|
200
229
|
const key = row.kind === 'directory' ? keyOf(row.path) : null;
|
|
230
|
+
|
|
201
231
|
if (key === null) {
|
|
202
232
|
continue;
|
|
203
233
|
}
|
|
234
|
+
|
|
204
235
|
const folded = collapsed.has(key);
|
|
236
|
+
|
|
205
237
|
if (folded && row.isExpanded) {
|
|
206
238
|
changes.collapse.push(row.path);
|
|
207
239
|
}
|
|
240
|
+
|
|
208
241
|
if (!folded && !row.isExpanded) {
|
|
209
242
|
changes.expand.push(row.path);
|
|
210
243
|
}
|
|
211
244
|
}
|
|
245
|
+
|
|
212
246
|
return changes;
|
|
213
247
|
}
|