@adecore/ui 0.35.4 → 0.36.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/Button.d.ts +2 -3
- package/dist/Button.js +21 -46
- package/dist/Checkbox.js +1 -1
- package/dist/ChoiceCards.js +2 -2
- package/dist/IconPicker.d.ts +2 -1
- package/dist/IconPicker.js +504 -217
- package/dist/Input.d.ts +5 -3
- package/dist/Input.js +127 -112
- package/dist/Segmented.js +1 -1
- package/dist/SplitButton.d.ts +17 -0
- package/dist/SplitButton.js +237 -0
- package/dist/Stepper.js +1 -1
- package/dist/Switch.js +1 -1
- package/dist/Tile.js +2 -2
- package/dist/button-class.d.ts +6 -0
- package/dist/button-class.js +92 -0
- package/dist/command-palette/parts.d.ts +2 -2
- package/dist/command-palette/parts.js +125 -164
- package/dist/file-tree/empty-lines.d.ts +11 -0
- package/dist/file-tree/empty-lines.js +89 -0
- package/dist/file-tree/highlight.d.ts +4 -0
- package/dist/file-tree/highlight.js +75 -0
- package/dist/file-tree/parts.d.ts +9 -1
- package/dist/file-tree/parts.js +297 -205
- package/dist/file-tree/style.js +13 -0
- package/dist/file-tree/use-empty-lines.d.ts +3 -0
- package/dist/file-tree/use-empty-lines.js +176 -0
- package/dist/file-tree/use-file-tree.js +5 -1
- package/dist/file-tree/use-slots.d.ts +16 -2
- package/dist/file-tree/use-slots.js +186 -43
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/locales/en.d.ts +1 -0
- package/dist/locales/en.js +1 -0
- package/dist/locales/nl.d.ts +1 -0
- package/dist/locales/nl.js +1 -0
- package/dist/select-shared.d.ts +1 -1
- package/dist/select-shared.js +1 -1
- package/dist/settings/SettingsDialog.js +1 -1
- package/dist/theme.css +33 -3
- package/package.json +1 -1
- package/src/Button.tsx +3 -36
- package/src/Checkbox.tsx +1 -1
- package/src/ChoiceCards.tsx +2 -2
- package/src/IconPicker.tsx +220 -136
- package/src/Input.tsx +19 -10
- package/src/Segmented.tsx +1 -1
- package/src/SplitButton.tsx +72 -0
- package/src/Stepper.tsx +6 -1
- package/src/Switch.tsx +1 -1
- package/src/Tile.tsx +2 -2
- package/src/button-class.ts +111 -0
- package/src/command-palette/parts.tsx +34 -2
- package/src/file-tree/empty-lines.ts +105 -0
- package/src/file-tree/highlight.ts +80 -0
- package/src/file-tree/parts.tsx +42 -11
- package/src/file-tree/style.ts +13 -0
- package/src/file-tree/use-empty-lines.ts +115 -0
- package/src/file-tree/use-file-tree.ts +2 -1
- package/src/file-tree/use-slots.ts +176 -17
- package/src/index.ts +1 -0
- package/src/locales/en.ts +1 -0
- package/src/locales/nl.ts +1 -0
- package/src/select-shared.ts +2 -1
- package/src/settings/SettingsDialog.tsx +1 -1
- package/src/theme.css +33 -3
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
const BASE = 'inline-flex shrink-0 items-center justify-center gap-1.5 font-medium disabled:opacity-50';
|
|
3
|
+
const VARIANT = {
|
|
4
|
+
// A filled button darkens under the pointer. Its color is the meaning, so no token of its own for hover.
|
|
5
|
+
primary: 'bg-accent text-accent-text hover:brightness-90 disabled:hover:brightness-100',
|
|
6
|
+
// Drawn like a field, so it stands a step off any ground, a dialog's raised one included.
|
|
7
|
+
secondary: 'border border-field-border bg-field text-text hover:bg-surface-hover disabled:hover:bg-field',
|
|
8
|
+
ghost: 'text-text-muted hover:bg-surface-hover hover:text-text',
|
|
9
|
+
danger: 'bg-status-error text-accent-text hover:brightness-90 disabled:hover:brightness-100',
|
|
10
|
+
// A step that forgets something rather than destroys it: softer than the filled button of a deletion that cannot come back.
|
|
11
|
+
'danger-outline': 'border border-status-error/35 text-status-error hover:bg-status-error/10 disabled:hover:bg-transparent',
|
|
12
|
+
positive: 'bg-positive text-positive-text hover:brightness-90 disabled:hover:brightness-100',
|
|
13
|
+
// Dark on a light theme, light on a dark one, matching what Apple's and GitHub's sign-in buttons ask for.
|
|
14
|
+
inverse: 'bg-text text-bg hover:opacity-90 disabled:hover:opacity-100'
|
|
15
|
+
};
|
|
16
|
+
/* 28 and 32 pixels are the compact and the normal control; 24 fits a header of 32. */
|
|
17
|
+
const SIZE = {
|
|
18
|
+
xs: 'h-6 px-2',
|
|
19
|
+
sm: 'h-7 px-2.5',
|
|
20
|
+
md: 'h-8 px-3'
|
|
21
|
+
};
|
|
22
|
+
/* A pill is one size of its own, for a control that sits in a round row, so it takes the place of `size`. */
|
|
23
|
+
const SHAPE = {
|
|
24
|
+
default: 'rounded-md text-xs',
|
|
25
|
+
pill: 'h-9 rounded-full px-3.5 text-sm'
|
|
26
|
+
};
|
|
27
|
+
export function buttonClass(variant, size, shape) {
|
|
28
|
+
return clsx(BASE, SHAPE[shape], VARIANT[variant], shape === 'default' && SIZE[size]);
|
|
29
|
+
}
|
|
30
|
+
/*
|
|
31
|
+
* A split button is one shape: the frame draws the radius, the outline of an outlined variant and the
|
|
32
|
+
* height, and clips both halves, which carry the ground and its hover each on their own.
|
|
33
|
+
*/
|
|
34
|
+
const SPLIT_FRAME = {
|
|
35
|
+
primary: '',
|
|
36
|
+
secondary: 'border border-field-border',
|
|
37
|
+
ghost: '',
|
|
38
|
+
danger: '',
|
|
39
|
+
'danger-outline': 'border border-status-error/35',
|
|
40
|
+
positive: '',
|
|
41
|
+
inverse: ''
|
|
42
|
+
};
|
|
43
|
+
const SPLIT_HALF = {
|
|
44
|
+
primary: VARIANT.primary,
|
|
45
|
+
secondary: 'bg-field text-text hover:bg-surface-hover disabled:hover:bg-field',
|
|
46
|
+
ghost: VARIANT.ghost,
|
|
47
|
+
danger: VARIANT.danger,
|
|
48
|
+
'danger-outline': 'text-status-error hover:bg-status-error/10 disabled:hover:bg-transparent',
|
|
49
|
+
positive: VARIANT.positive,
|
|
50
|
+
inverse: VARIANT.inverse
|
|
51
|
+
};
|
|
52
|
+
/* The short line between the halves: a light alpha of the text over a filled ground, the border's alpha elsewhere. */
|
|
53
|
+
const SPLIT_SEPARATOR = {
|
|
54
|
+
primary: 'before:bg-current/30',
|
|
55
|
+
secondary: 'before:bg-border-strong',
|
|
56
|
+
ghost: 'before:bg-border-strong',
|
|
57
|
+
danger: 'before:bg-current/30',
|
|
58
|
+
'danger-outline': 'before:bg-status-error/35',
|
|
59
|
+
positive: 'before:bg-current/30',
|
|
60
|
+
inverse: 'before:bg-current/30'
|
|
61
|
+
};
|
|
62
|
+
const SPLIT_HEIGHT = {
|
|
63
|
+
xs: 'h-6',
|
|
64
|
+
sm: 'h-7',
|
|
65
|
+
md: 'h-8'
|
|
66
|
+
};
|
|
67
|
+
/* Half the height of the button, centered by auto margins, which land on whole pixels in the bordered frames too. */
|
|
68
|
+
const SPLIT_SEPARATOR_HEIGHT = {
|
|
69
|
+
xs: 'before:h-3',
|
|
70
|
+
sm: 'before:h-3.5',
|
|
71
|
+
md: 'before:h-4'
|
|
72
|
+
};
|
|
73
|
+
const SPLIT_PADDING = {
|
|
74
|
+
action: {
|
|
75
|
+
xs: 'px-2',
|
|
76
|
+
sm: 'px-2.5',
|
|
77
|
+
md: 'px-3'
|
|
78
|
+
},
|
|
79
|
+
// The chevron half is narrower than a half with a word in it.
|
|
80
|
+
menu: {
|
|
81
|
+
xs: 'px-1',
|
|
82
|
+
sm: 'px-1.5',
|
|
83
|
+
md: 'px-2'
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
export function splitFrameClass(variant, size) {
|
|
87
|
+
return clsx('inline-flex shrink-0 items-stretch overflow-clip rounded-md', SPLIT_HEIGHT[size], SPLIT_FRAME[variant]);
|
|
88
|
+
}
|
|
89
|
+
/* The frame clips the focus outline, so a half draws it inside its own edge. */
|
|
90
|
+
export function splitHalfClass(variant, size, half) {
|
|
91
|
+
return clsx('inline-flex items-center justify-center gap-1.5 text-xs font-medium focus-visible:-outline-offset-2 disabled:opacity-50', SPLIT_HALF[variant], SPLIT_PADDING[half][size], half === 'menu' && clsx('relative before:absolute before:inset-y-0 before:left-0 before:my-auto before:w-px before:content-[""]', SPLIT_SEPARATOR_HEIGHT[size], SPLIT_SEPARATOR[variant]));
|
|
92
|
+
}
|
|
@@ -13,7 +13,7 @@ type AutocompleteProps<ItemValue> = Omit<Autocomplete.Root.Props<ItemValue>, 'op
|
|
|
13
13
|
};
|
|
14
14
|
type DialogProps = Pick<ComponentProps<typeof DialogRoot>, 'open' | 'defaultOpen' | 'onOpenChange' | 'onOpenChangeComplete'>;
|
|
15
15
|
export type CommandPaletteRootProps<ItemValue> = AutocompleteProps<ItemValue> & DialogProps;
|
|
16
|
-
export declare function CommandPaletteRoot<ItemValue>({ open, defaultOpen, onOpenChange, onOpenChangeComplete, autoHighlight, keepHighlight, value, onValueChange, items, children, ...props }: CommandPaletteRootProps<ItemValue>): import("react").JSX.Element;
|
|
16
|
+
export declare function CommandPaletteRoot<ItemValue>({ open, defaultOpen, onOpenChange, onOpenChangeComplete, autoHighlight, keepHighlight, highlightItemOnHover, value, onValueChange, items, children, ...props }: CommandPaletteRootProps<ItemValue>): import("react").JSX.Element;
|
|
17
17
|
export type CommandPalettePopupProps = Omit<DialogPopupProps, 'size'> & {
|
|
18
18
|
size?: keyof typeof SIZE;
|
|
19
19
|
};
|
|
@@ -26,7 +26,7 @@ export type CommandPaletteInputProps = Omit<ComponentProps<typeof Autocomplete.I
|
|
|
26
26
|
className?: string;
|
|
27
27
|
};
|
|
28
28
|
export declare function CommandPaletteInput({ leading, trailing, escapeHint, mono, className, ref, ...props }: CommandPaletteInputProps): import("react").JSX.Element;
|
|
29
|
-
export declare function CommandPaletteList({ className, ref, ...props }: ComponentProps<typeof Autocomplete.List>): import("react").JSX.Element;
|
|
29
|
+
export declare function CommandPaletteList({ className, ref, onFocus, ...props }: ComponentProps<typeof Autocomplete.List>): import("react").JSX.Element;
|
|
30
30
|
export type CommandPaletteFooterProps = useRender.ComponentProps<'div'>;
|
|
31
31
|
export declare function CommandPaletteFooter({ render, className, ref, ...props }: CommandPaletteFooterProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
32
32
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { c as _c } from "react/compiler-runtime";
|
|
2
2
|
import { createContext, useContext, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { flushSync } from 'react-dom';
|
|
3
4
|
import clsx from 'clsx';
|
|
4
5
|
import { Search } from 'lucide-react';
|
|
5
6
|
import { useTranslation } from 'react-i18next';
|
|
@@ -24,156 +25,91 @@ const SIZE = {
|
|
|
24
25
|
* is Base UI's autocomplete, so the query, the items and their filter work as in a combobox. The
|
|
25
26
|
* first row is highlighted and stays so while the pointer leaves the list, which is what Enter runs.
|
|
26
27
|
*/
|
|
27
|
-
export function CommandPaletteRoot(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
({
|
|
42
|
-
open,
|
|
43
|
-
defaultOpen,
|
|
44
|
-
onOpenChange,
|
|
45
|
-
onOpenChangeComplete,
|
|
46
|
-
autoHighlight: t1,
|
|
47
|
-
keepHighlight: t2,
|
|
48
|
-
value,
|
|
49
|
-
onValueChange,
|
|
50
|
-
items,
|
|
51
|
-
children,
|
|
52
|
-
...props
|
|
53
|
-
} = t0);
|
|
54
|
-
$[0] = t0;
|
|
55
|
-
$[1] = children;
|
|
56
|
-
$[2] = defaultOpen;
|
|
57
|
-
$[3] = items;
|
|
58
|
-
$[4] = onOpenChange;
|
|
59
|
-
$[5] = onOpenChangeComplete;
|
|
60
|
-
$[6] = onValueChange;
|
|
61
|
-
$[7] = open;
|
|
62
|
-
$[8] = props;
|
|
63
|
-
$[9] = t1;
|
|
64
|
-
$[10] = t2;
|
|
65
|
-
$[11] = value;
|
|
66
|
-
} else {
|
|
67
|
-
children = $[1];
|
|
68
|
-
defaultOpen = $[2];
|
|
69
|
-
items = $[3];
|
|
70
|
-
onOpenChange = $[4];
|
|
71
|
-
onOpenChangeComplete = $[5];
|
|
72
|
-
onValueChange = $[6];
|
|
73
|
-
open = $[7];
|
|
74
|
-
props = $[8];
|
|
75
|
-
t1 = $[9];
|
|
76
|
-
t2 = $[10];
|
|
77
|
-
value = $[11];
|
|
78
|
-
}
|
|
79
|
-
const autoHighlight = t1 === undefined ? "always" : t1;
|
|
80
|
-
const keepHighlight = t2 === undefined ? true : t2;
|
|
28
|
+
export function CommandPaletteRoot({
|
|
29
|
+
open,
|
|
30
|
+
defaultOpen,
|
|
31
|
+
onOpenChange,
|
|
32
|
+
onOpenChangeComplete,
|
|
33
|
+
autoHighlight = 'always',
|
|
34
|
+
keepHighlight = true,
|
|
35
|
+
highlightItemOnHover = true,
|
|
36
|
+
value,
|
|
37
|
+
onValueChange,
|
|
38
|
+
items,
|
|
39
|
+
children,
|
|
40
|
+
...props
|
|
41
|
+
}) {
|
|
81
42
|
const inputRef = useRef(null);
|
|
82
43
|
const listRef = useRef(null);
|
|
44
|
+
const [focusHighlight, setFocusHighlight] = useState(false);
|
|
45
|
+
/*
|
|
46
|
+
* Base UI moves the highlight to the first row for a typed query only. For a query the app sets,
|
|
47
|
+
* the list is empty for one commit that is never painted: Base UI drops the highlight then and
|
|
48
|
+
* puts it on the first row of what comes back.
|
|
49
|
+
*/
|
|
83
50
|
const [settledValue, setSettledValue] = useState(value);
|
|
84
51
|
const resetting = value !== undefined && value !== settledValue;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
52
|
+
useLayoutEffect(() => {
|
|
53
|
+
if (!resetting) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (listRef.current !== null) {
|
|
57
|
+
listRef.current.scrollTop = 0;
|
|
58
|
+
}
|
|
59
|
+
// The rows may come back only after Base UI has seen the empty list in a commit of its own.
|
|
60
|
+
// oxlint-disable-next-line react/set-state-in-effect
|
|
61
|
+
setSettledValue(value);
|
|
62
|
+
}, [resetting, value]);
|
|
63
|
+
const highlight = row => {
|
|
64
|
+
if (row.hasAttribute('data-highlighted')) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Base UI has no imperative highlight action and disables highlighting on item focus.
|
|
68
|
+
// Enable pointer navigation just for this event when the app has disabled hover highlighting.
|
|
69
|
+
if (!highlightItemOnHover) {
|
|
70
|
+
flushSync(() => setFocusHighlight(true));
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
row.dispatchEvent(new MouseEvent('mousemove', {
|
|
74
|
+
bubbles: true,
|
|
75
|
+
movementX: 1
|
|
76
|
+
}));
|
|
77
|
+
} finally {
|
|
78
|
+
if (!highlightItemOnHover) {
|
|
79
|
+
flushSync(() => setFocusHighlight(false));
|
|
91
80
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
useLayoutEffect(t3, t4);
|
|
107
|
-
let t5;
|
|
108
|
-
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
|
109
|
-
t5 = {
|
|
110
|
-
input: inputRef,
|
|
111
|
-
list: listRef
|
|
112
|
-
};
|
|
113
|
-
$[16] = t5;
|
|
114
|
-
} else {
|
|
115
|
-
t5 = $[16];
|
|
116
|
-
}
|
|
117
|
-
let t6;
|
|
118
|
-
if ($[17] !== onValueChange) {
|
|
119
|
-
t6 = (next, details) => {
|
|
120
|
-
if (details.reason === "input-change") {
|
|
121
|
-
setSettledValue(next);
|
|
122
|
-
}
|
|
123
|
-
onValueChange?.(next, details);
|
|
124
|
-
};
|
|
125
|
-
$[17] = onValueChange;
|
|
126
|
-
$[18] = t6;
|
|
127
|
-
} else {
|
|
128
|
-
t6 = $[18];
|
|
129
|
-
}
|
|
130
|
-
const t7 = resetting ? NO_ITEMS : items;
|
|
131
|
-
let t8;
|
|
132
|
-
if ($[19] !== autoHighlight || $[20] !== children || $[21] !== keepHighlight || $[22] !== props || $[23] !== t6 || $[24] !== t7 || $[25] !== value) {
|
|
133
|
-
t8 = /*#__PURE__*/_jsx(PaletteRefsContext, {
|
|
134
|
-
value: t5,
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return /*#__PURE__*/_jsx(DialogRoot, {
|
|
84
|
+
open: open,
|
|
85
|
+
defaultOpen: defaultOpen,
|
|
86
|
+
onOpenChange: onOpenChange,
|
|
87
|
+
onOpenChangeComplete: onOpenChangeComplete,
|
|
88
|
+
children: /*#__PURE__*/_jsx(PaletteRefsContext, {
|
|
89
|
+
value: {
|
|
90
|
+
input: inputRef,
|
|
91
|
+
list: listRef,
|
|
92
|
+
highlight
|
|
93
|
+
},
|
|
135
94
|
children: /*#__PURE__*/_jsx(Autocomplete.Root, {
|
|
136
95
|
...props,
|
|
137
96
|
value: value,
|
|
138
|
-
onValueChange:
|
|
139
|
-
|
|
97
|
+
onValueChange: (next, details) => {
|
|
98
|
+
if (details.reason === 'input-change') {
|
|
99
|
+
setSettledValue(next);
|
|
100
|
+
}
|
|
101
|
+
onValueChange?.(next, details);
|
|
102
|
+
},
|
|
103
|
+
items: resetting ? NO_ITEMS : items,
|
|
140
104
|
inline: true,
|
|
141
105
|
open: true,
|
|
142
106
|
autoHighlight: autoHighlight,
|
|
143
107
|
keepHighlight: keepHighlight,
|
|
108
|
+
highlightItemOnHover: highlightItemOnHover || focusHighlight,
|
|
144
109
|
children: children
|
|
145
110
|
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
$[20] = children;
|
|
149
|
-
$[21] = keepHighlight;
|
|
150
|
-
$[22] = props;
|
|
151
|
-
$[23] = t6;
|
|
152
|
-
$[24] = t7;
|
|
153
|
-
$[25] = value;
|
|
154
|
-
$[26] = t8;
|
|
155
|
-
} else {
|
|
156
|
-
t8 = $[26];
|
|
157
|
-
}
|
|
158
|
-
let t9;
|
|
159
|
-
if ($[27] !== defaultOpen || $[28] !== onOpenChange || $[29] !== onOpenChangeComplete || $[30] !== open || $[31] !== t8) {
|
|
160
|
-
t9 = /*#__PURE__*/_jsx(DialogRoot, {
|
|
161
|
-
open: open,
|
|
162
|
-
defaultOpen: defaultOpen,
|
|
163
|
-
onOpenChange: onOpenChange,
|
|
164
|
-
onOpenChangeComplete: onOpenChangeComplete,
|
|
165
|
-
children: t8
|
|
166
|
-
});
|
|
167
|
-
$[27] = defaultOpen;
|
|
168
|
-
$[28] = onOpenChange;
|
|
169
|
-
$[29] = onOpenChangeComplete;
|
|
170
|
-
$[30] = open;
|
|
171
|
-
$[31] = t8;
|
|
172
|
-
$[32] = t9;
|
|
173
|
-
} else {
|
|
174
|
-
t9 = $[32];
|
|
175
|
-
}
|
|
176
|
-
return t9;
|
|
111
|
+
})
|
|
112
|
+
});
|
|
177
113
|
}
|
|
178
114
|
/*
|
|
179
115
|
* The dialog's surface. It hangs from a fixed line near the top instead of the middle: the list
|
|
@@ -369,24 +305,28 @@ export function CommandPaletteInput(t0) {
|
|
|
369
305
|
}
|
|
370
306
|
/* The rows. It scrolls on its own, so the field and the footer stay put. */
|
|
371
307
|
export function CommandPaletteList(t0) {
|
|
372
|
-
const $ = _c(
|
|
308
|
+
const $ = _c(21);
|
|
373
309
|
let className;
|
|
310
|
+
let onFocus;
|
|
374
311
|
let props;
|
|
375
312
|
let ref;
|
|
376
313
|
if ($[0] !== t0) {
|
|
377
314
|
({
|
|
378
315
|
className,
|
|
379
316
|
ref,
|
|
317
|
+
onFocus,
|
|
380
318
|
...props
|
|
381
319
|
} = t0);
|
|
382
320
|
$[0] = t0;
|
|
383
321
|
$[1] = className;
|
|
384
|
-
$[2] =
|
|
385
|
-
$[3] =
|
|
322
|
+
$[2] = onFocus;
|
|
323
|
+
$[3] = props;
|
|
324
|
+
$[4] = ref;
|
|
386
325
|
} else {
|
|
387
326
|
className = $[1];
|
|
388
|
-
|
|
389
|
-
|
|
327
|
+
onFocus = $[2];
|
|
328
|
+
props = $[3];
|
|
329
|
+
ref = $[4];
|
|
390
330
|
}
|
|
391
331
|
const {
|
|
392
332
|
t
|
|
@@ -394,47 +334,68 @@ export function CommandPaletteList(t0) {
|
|
|
394
334
|
const refs = useContext(PaletteRefsContext);
|
|
395
335
|
const t1 = refs?.list;
|
|
396
336
|
let t2;
|
|
397
|
-
if ($[
|
|
337
|
+
if ($[5] !== ref || $[6] !== t1) {
|
|
398
338
|
t2 = mergeRefs(ref, t1);
|
|
399
|
-
$[
|
|
400
|
-
$[
|
|
401
|
-
$[
|
|
339
|
+
$[5] = ref;
|
|
340
|
+
$[6] = t1;
|
|
341
|
+
$[7] = t2;
|
|
402
342
|
} else {
|
|
403
|
-
t2 = $[
|
|
343
|
+
t2 = $[7];
|
|
404
344
|
}
|
|
405
345
|
let t3;
|
|
406
|
-
if ($[
|
|
346
|
+
if ($[8] !== t) {
|
|
407
347
|
t3 = t("palette.results");
|
|
408
|
-
$[
|
|
409
|
-
$[
|
|
348
|
+
$[8] = t;
|
|
349
|
+
$[9] = t3;
|
|
410
350
|
} else {
|
|
411
|
-
t3 = $[
|
|
351
|
+
t3 = $[9];
|
|
412
352
|
}
|
|
413
353
|
let t4;
|
|
414
|
-
if ($[
|
|
354
|
+
if ($[10] !== className) {
|
|
415
355
|
t4 = withClass("max-h-[50vh] min-h-0 overflow-y-auto overscroll-contain scroll-py-1.5 not-empty:p-1.5", className);
|
|
416
|
-
$[
|
|
417
|
-
$[
|
|
356
|
+
$[10] = className;
|
|
357
|
+
$[11] = t4;
|
|
418
358
|
} else {
|
|
419
|
-
t4 = $[
|
|
359
|
+
t4 = $[11];
|
|
420
360
|
}
|
|
421
361
|
let t5;
|
|
422
|
-
if ($[
|
|
423
|
-
t5 =
|
|
362
|
+
if ($[12] !== onFocus || $[13] !== refs) {
|
|
363
|
+
t5 = event => {
|
|
364
|
+
onFocus?.(event);
|
|
365
|
+
const row = event.target.closest("[role=\"option\"]");
|
|
366
|
+
if (row === null || !event.currentTarget.contains(row) || refs?.input.current == null) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
refs.highlight(row);
|
|
370
|
+
refs.input.current.focus({
|
|
371
|
+
preventScroll: true
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
$[12] = onFocus;
|
|
375
|
+
$[13] = refs;
|
|
376
|
+
$[14] = t5;
|
|
377
|
+
} else {
|
|
378
|
+
t5 = $[14];
|
|
379
|
+
}
|
|
380
|
+
let t6;
|
|
381
|
+
if ($[15] !== props || $[16] !== t2 || $[17] !== t3 || $[18] !== t4 || $[19] !== t5) {
|
|
382
|
+
t6 = /*#__PURE__*/_jsx(Autocomplete.List, {
|
|
424
383
|
ref: t2,
|
|
425
384
|
"aria-label": t3,
|
|
426
385
|
className: t4,
|
|
427
|
-
...props
|
|
386
|
+
...props,
|
|
387
|
+
onFocus: t5
|
|
428
388
|
});
|
|
429
|
-
$[
|
|
430
|
-
$[
|
|
431
|
-
$[
|
|
432
|
-
$[
|
|
433
|
-
$[
|
|
389
|
+
$[15] = props;
|
|
390
|
+
$[16] = t2;
|
|
391
|
+
$[17] = t3;
|
|
392
|
+
$[18] = t4;
|
|
393
|
+
$[19] = t5;
|
|
394
|
+
$[20] = t6;
|
|
434
395
|
} else {
|
|
435
|
-
|
|
396
|
+
t6 = $[20];
|
|
436
397
|
}
|
|
437
|
-
return
|
|
398
|
+
return t6;
|
|
438
399
|
}
|
|
439
400
|
/* Under the list: the keys that work here, a count, an action that leaves the palette. */
|
|
440
401
|
export function CommandPaletteFooter(t0) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
|
|
2
|
+
export type FileTreeEmptyLabel = (row: FileTreeVisibleRow) => string | undefined;
|
|
3
|
+
export declare function emptyLineOf(directory: string): string;
|
|
4
|
+
export declare function isEmptyLine(path: string): boolean;
|
|
5
|
+
export declare function directoryOfEmptyLine(path: string): string;
|
|
6
|
+
export declare function emptyLineChanges(rows: readonly FileTreeVisibleRow[], labelOf: FileTreeEmptyLabel, has: (path: string) => boolean, added: Iterable<string>): {
|
|
7
|
+
add: string[];
|
|
8
|
+
remove: string[];
|
|
9
|
+
};
|
|
10
|
+
export declare function pastEmptyLine(model: FileTree, key: string): string | null;
|
|
11
|
+
export declare function skipEmptyLine(model: FileTree, key: string): boolean;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { pathOfRow } from './helpers.js';
|
|
2
|
+
/*
|
|
3
|
+
* The engine draws only rows it holds, so the line under an open, empty directory is a child the
|
|
4
|
+
* wrapper adds and removes itself. U+FDD0 is a noncharacter, which no real file name carries.
|
|
5
|
+
*/
|
|
6
|
+
const EMPTY_LINE_NAME = 'empty';
|
|
7
|
+
export function emptyLineOf(directory) {
|
|
8
|
+
return `${directory}${EMPTY_LINE_NAME}`;
|
|
9
|
+
}
|
|
10
|
+
export function isEmptyLine(path) {
|
|
11
|
+
return path.endsWith(`/${EMPTY_LINE_NAME}`);
|
|
12
|
+
}
|
|
13
|
+
export function directoryOfEmptyLine(path) {
|
|
14
|
+
return path.slice(0, -EMPTY_LINE_NAME.length);
|
|
15
|
+
}
|
|
16
|
+
/*
|
|
17
|
+
* The lines to add and the ones to take away: an open directory whose label is not undefined and
|
|
18
|
+
* that holds nothing besides its own line gets one, every other line the wrapper added goes.
|
|
19
|
+
*/
|
|
20
|
+
export function emptyLineChanges(rows, labelOf, has, added) {
|
|
21
|
+
const wanted = new Set();
|
|
22
|
+
rows.forEach((row, index) => {
|
|
23
|
+
if (row.kind !== 'directory' || !row.isExpanded) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const line = emptyLineOf(row.path);
|
|
27
|
+
for (let next = index + 1; next < rows.length && rows[next].depth > row.depth; next++) {
|
|
28
|
+
if (rows[next].path !== line) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (labelOf(row) !== undefined) {
|
|
33
|
+
wanted.add(line);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
add: [...wanted].filter(line => !has(line)),
|
|
38
|
+
remove: [...added].filter(line => !wanted.has(line) && has(line))
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/*
|
|
42
|
+
* Where an arrow, Home or End goes instead when the row it would land on is a line: past it, or
|
|
43
|
+
* nowhere (the focused path itself) where nothing lies past it. Null when no line is in the way.
|
|
44
|
+
*/
|
|
45
|
+
export function pastEmptyLine(model, key) {
|
|
46
|
+
const index = model.getFocusedIndex();
|
|
47
|
+
const focused = model.getFocusedPath();
|
|
48
|
+
if (index < 0 || focused === null) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const count = model.getVisibleCount();
|
|
52
|
+
const at = position => position >= 0 && position < count ? model.getVisibleRows(position, position)[0] ?? null : null;
|
|
53
|
+
const lineAt = position => {
|
|
54
|
+
const row = at(position);
|
|
55
|
+
return row !== null && isEmptyLine(row.path);
|
|
56
|
+
};
|
|
57
|
+
const pathAt = position => {
|
|
58
|
+
const row = at(position);
|
|
59
|
+
return row === null ? focused : pathOfRow(row);
|
|
60
|
+
};
|
|
61
|
+
if ((key === 'ArrowDown' || key === 'ArrowRight') && lineAt(index + 1)) {
|
|
62
|
+
return key === 'ArrowDown' ? pathAt(index + 2) : focused;
|
|
63
|
+
}
|
|
64
|
+
if (key === 'ArrowUp' && lineAt(index - 1)) {
|
|
65
|
+
return pathAt(index - 2);
|
|
66
|
+
}
|
|
67
|
+
if (key === 'End' && lineAt(count - 1)) {
|
|
68
|
+
return pathAt(count - 2);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/* 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. */
|
|
73
|
+
export function skipEmptyLine(model, key) {
|
|
74
|
+
const path = model.getFocusedPath();
|
|
75
|
+
if (path === null || !isEmptyLine(path)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (key === 'ArrowUp' || key === 'PageUp') {
|
|
79
|
+
model.focusPreviousItem();
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (key === 'ArrowDown' || key === 'PageDown' || key === 'End') {
|
|
83
|
+
model.focusNextItem();
|
|
84
|
+
}
|
|
85
|
+
if (model.getFocusedPath() === path) {
|
|
86
|
+
model.focusPath(directoryOfEmptyLine(path));
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const MATCH_HIGHLIGHT = "adecore-tree-match";
|
|
2
|
+
export declare function matchHighlight(): Highlight | null;
|
|
3
|
+
export declare function matchRanges(text: string, query: string): [number, number][];
|
|
4
|
+
export declare function rangesOfMatches(rows: readonly HTMLElement[], query: string): Range[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* The name a tree's shadow stylesheet colors with `::highlight()`. One registry entry is shared by every tree on the page. */
|
|
2
|
+
export const MATCH_HIGHLIGHT = 'adecore-tree-match';
|
|
3
|
+
// The copies of a name the engine keeps for truncation, which the tree's stylesheet hides.
|
|
4
|
+
const HIDDEN_PARTS = '[data-truncate-content="overflow"], [data-truncate-marker-cell], [data-truncate-fill]';
|
|
5
|
+
let shared = null;
|
|
6
|
+
/* Null where the browser has no CSS Custom Highlight API; the names are then drawn without a mark. */
|
|
7
|
+
export function matchHighlight() {
|
|
8
|
+
if (typeof CSS === 'undefined' || !('highlights' in CSS) || typeof Highlight === 'undefined') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
if (shared === null) {
|
|
12
|
+
shared = new Highlight();
|
|
13
|
+
}
|
|
14
|
+
if (CSS.highlights.get(MATCH_HIGHLIGHT) !== shared) {
|
|
15
|
+
CSS.highlights.set(MATCH_HIGHLIGHT, shared);
|
|
16
|
+
}
|
|
17
|
+
return shared;
|
|
18
|
+
}
|
|
19
|
+
/* Every place `query` occurs in `text`, ignoring case, as `[start, end)` pairs that do not overlap. */
|
|
20
|
+
export function matchRanges(text, query) {
|
|
21
|
+
const wanted = query.toLowerCase();
|
|
22
|
+
const haystack = text.toLowerCase();
|
|
23
|
+
// A letter that lowercases to more than one would shift every offset after it.
|
|
24
|
+
if (wanted === '' || haystack.length !== text.length) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const found = [];
|
|
28
|
+
let at = haystack.indexOf(wanted);
|
|
29
|
+
while (at !== -1) {
|
|
30
|
+
found.push([at, at + wanted.length]);
|
|
31
|
+
at = haystack.indexOf(wanted, at + wanted.length);
|
|
32
|
+
}
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
/* The ranges over the visible text of each row's name where `query` occurs. */
|
|
36
|
+
export function rangesOfMatches(rows, query) {
|
|
37
|
+
const ranges = [];
|
|
38
|
+
if (query === '') {
|
|
39
|
+
return ranges;
|
|
40
|
+
}
|
|
41
|
+
for (const row of rows) {
|
|
42
|
+
const content = row.querySelector('[data-item-section="content"]');
|
|
43
|
+
if (content === null) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const nodes = [];
|
|
47
|
+
const walker = row.ownerDocument.createTreeWalker(content, NodeFilter.SHOW_TEXT);
|
|
48
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
49
|
+
if (node.parentElement?.closest(HIDDEN_PARTS) === null) {
|
|
50
|
+
nodes.push(node);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const starts = [];
|
|
54
|
+
let text = '';
|
|
55
|
+
for (const node of nodes) {
|
|
56
|
+
starts.push(text.length);
|
|
57
|
+
text += node.data;
|
|
58
|
+
}
|
|
59
|
+
// The node an offset falls in, and the offset inside it; an end may sit at the end of a node.
|
|
60
|
+
const locate = (offset, end) => {
|
|
61
|
+
let index = nodes.length - 1;
|
|
62
|
+
while (index > 0 && (end ? starts[index] >= offset : starts[index] > offset)) {
|
|
63
|
+
index--;
|
|
64
|
+
}
|
|
65
|
+
return [nodes[index], offset - starts[index]];
|
|
66
|
+
};
|
|
67
|
+
for (const [start, end] of matchRanges(text, query)) {
|
|
68
|
+
const range = row.ownerDocument.createRange();
|
|
69
|
+
range.setStart(...locate(start, false));
|
|
70
|
+
range.setEnd(...locate(end, true));
|
|
71
|
+
ranges.push(range);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return ranges;
|
|
75
|
+
}
|
|
@@ -2,6 +2,9 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import { useRender } from '@base-ui/react/use-render';
|
|
3
3
|
import { type FileTreeProps as PierreProps } from '@pierre/trees/react';
|
|
4
4
|
import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
|
|
5
|
+
import type { LucideIcon } from 'lucide-react';
|
|
6
|
+
import type { FileTreeEmptyLabel } from './empty-lines.ts';
|
|
7
|
+
import { type FileTreeDecorationOrder } from './use-slots.ts';
|
|
5
8
|
export type FileTreeRootProps = useRender.ComponentProps<'div'> & {
|
|
6
9
|
model: FileTree;
|
|
7
10
|
label?: string;
|
|
@@ -15,6 +18,11 @@ export type FileTreeRootProps = useRender.ComponentProps<'div'> & {
|
|
|
15
18
|
onExpandedPathsChange?(paths: readonly string[]): void;
|
|
16
19
|
renderControl?(row: FileTreeVisibleRow): ReactNode;
|
|
17
20
|
renderDecoration?(row: FileTreeVisibleRow): ReactNode;
|
|
21
|
+
decorationOrder?: FileTreeDecorationOrder;
|
|
22
|
+
rowIcon?(row: FileTreeVisibleRow): LucideIcon | undefined;
|
|
23
|
+
fitContent?: boolean;
|
|
24
|
+
highlight?: string;
|
|
25
|
+
emptyLabel?: FileTreeEmptyLabel;
|
|
18
26
|
treeProps?: Omit<PierreProps, 'model'>;
|
|
19
27
|
};
|
|
20
|
-
export declare function FileTreeRoot({ model, label, resetKey, selectionFollowsFocus, onActivate, onFocusMove, onLoadChildren, onExpandedPathsChange, onRowContextMenu, onRowDragStart, renderControl, renderDecoration, treeProps, render, className, ref, children, onClick, onKeyDownCapture, ...props }: FileTreeRootProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
28
|
+
export declare function FileTreeRoot({ model, label, resetKey, selectionFollowsFocus, onActivate, onFocusMove, onLoadChildren, onExpandedPathsChange, onRowContextMenu, onRowDragStart, renderControl, renderDecoration, decorationOrder, rowIcon, fitContent, highlight, emptyLabel, treeProps, render, className, ref, children, onClick, onKeyDownCapture, style, ...props }: FileTreeRootProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|