@adecore/ui 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AccentSwatches.js +2 -3
- package/dist/Banner.js +1 -6
- package/dist/ChoiceCards.js +0 -5
- package/dist/ColorSwatch.js +1 -4
- package/dist/ColorWheelPicker.js +3 -6
- package/dist/DockShell.js +3 -5
- package/dist/EmptyState.js +0 -1
- package/dist/ErrorBoundary.js +1 -5
- package/dist/Field.js +2 -6
- package/dist/FileIcon.js +2 -4
- package/dist/Icon.js +1 -2
- package/dist/IconButton.js +1 -1
- package/dist/IconPicker.js +3 -7
- package/dist/Input.js +1 -2
- package/dist/Kbd.js +1 -3
- package/dist/LazyBoundary.js +2 -3
- package/dist/Meter.js +0 -1
- package/dist/MiddleTruncate.js +2 -6
- package/dist/PanelEmpty.js +0 -1
- package/dist/PanelHeader.js +1 -4
- package/dist/Pill.js +1 -6
- package/dist/ProjectSwitcher.js +1 -1
- package/dist/PromptDialog.js +5 -8
- package/dist/RelativeTime.js +1 -1
- package/dist/SegmentBar.js +1 -2
- package/dist/Segmented.js +2 -5
- package/dist/Select.js +1 -4
- package/dist/ShortcutHints.js +2 -3
- package/dist/Slider.js +0 -1
- package/dist/SlidingColumn.js +2 -4
- package/dist/SplitButton.js +1 -5
- package/dist/TextMenu.js +1 -5
- package/dist/Tile.js +0 -4
- package/dist/Toasts.js +1 -9
- package/dist/Tooltip.js +1 -1
- package/dist/UIProvider.js +0 -5
- package/dist/Waveform.js +4 -2
- package/dist/Wipe.js +1 -6
- package/dist/ZoomControls.js +0 -1
- package/dist/clipboard.js +2 -5
- package/dist/combobox/parts.js +3 -9
- package/dist/command-palette/parts.js +5 -14
- package/dist/dialog/parts.js +5 -10
- package/dist/dialog-layer.js +2 -3
- package/dist/error-boundary.js +4 -20
- package/dist/file-icon.js +4 -9
- package/dist/file-tree/empty-lines.js +2 -8
- package/dist/file-tree/use-empty-lines.js +1 -5
- package/dist/file-tree/use-slots.js +6 -10
- package/dist/format/datetime.js +32 -29
- package/dist/format/duration.js +2 -10
- package/dist/format/locale.js +19 -39
- package/dist/format/number.js +9 -22
- package/dist/format/regions.js +4 -7
- package/dist/icon-picker.js +5 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/key-value-list/parts.js +1 -5
- package/dist/lazy.js +4 -19
- package/dist/locales.js +2 -6
- package/dist/menu/parts.js +61 -133
- package/dist/middle-truncate.js +2 -3
- package/dist/placement.js +0 -1
- package/dist/popover/parts.js +1 -5
- package/dist/popup-layer.js +2 -4
- package/dist/prefetch.js +2 -5
- package/dist/selection.js +1 -5
- package/dist/settings/MasterDetail.js +1 -7
- package/dist/settings/SettingsDialog.js +1 -2
- package/dist/settings/SettingsRow.js +2 -5
- package/dist/settings/SettingsSection.js +1 -1
- package/dist/shortcut-hints.js +1 -5
- package/dist/shortcut.js +1 -2
- package/dist/tabs/overflow.js +1 -6
- package/dist/tabs/parts.js +69 -95
- package/dist/testing/dedupe.js +5 -7
- package/dist/tint.d.ts +2 -0
- package/dist/tint.js +11 -0
- package/dist/toast-store.js +2 -10
- package/dist/tree/use-tree-shift.js +2 -6
- package/dist/useColumnResize.js +9 -10
- package/dist/useNow.js +2 -9
- package/dist/workspace/DropPreview.js +1 -5
- package/dist/workspace/preview.js +4 -9
- package/package.json +9 -9
- package/src/AccentSwatches.tsx +9 -3
- package/src/Banner.tsx +5 -11
- package/src/Button.tsx +4 -0
- package/src/Checkbox.tsx +3 -0
- package/src/ChoiceCards.tsx +12 -11
- package/src/CloseButton.tsx +1 -0
- package/src/ColorSwatch.tsx +7 -8
- package/src/ColorWheelPicker.tsx +28 -12
- package/src/ColumnResizeHandle.tsx +1 -0
- package/src/CopyButton.tsx +6 -0
- package/src/DisabledReason.tsx +1 -0
- package/src/DockShell.tsx +11 -10
- package/src/EmptyState.tsx +10 -6
- package/src/ErrorBoundary.tsx +13 -10
- package/src/Field.tsx +11 -13
- package/src/FileIcon.tsx +4 -4
- package/src/Icon.tsx +2 -4
- package/src/IconButton.tsx +14 -5
- package/src/IconPicker.tsx +30 -15
- package/src/Input.tsx +11 -9
- package/src/Kbd.tsx +8 -7
- package/src/LazyBoundary.tsx +3 -3
- package/src/Meter.tsx +7 -2
- package/src/MiddleTruncate.tsx +15 -6
- package/src/PanelEmpty.tsx +9 -7
- package/src/PanelHeader.tsx +2 -5
- package/src/Pill.tsx +7 -8
- package/src/ProjectSwitcher.tsx +16 -6
- package/src/PromptDialog.tsx +31 -26
- package/src/RelativeTime.tsx +6 -1
- package/src/SegmentBar.tsx +22 -11
- package/src/Segmented.tsx +16 -11
- package/src/Select.tsx +7 -4
- package/src/SelectSearch.tsx +3 -0
- package/src/ShortcutHints.tsx +24 -3
- package/src/Skeleton.tsx +1 -0
- package/src/Slider.tsx +4 -1
- package/src/SlidingColumn.tsx +18 -14
- package/src/Spinner.tsx +1 -0
- package/src/SplitButton.tsx +8 -8
- package/src/Stepper.tsx +6 -0
- package/src/Switch.tsx +2 -0
- package/src/TextMenu.tsx +2 -5
- package/src/Tile.tsx +2 -6
- package/src/Toasts.tsx +8 -12
- package/src/Tooltip.tsx +10 -5
- package/src/UIProvider.tsx +7 -7
- package/src/Waveform.tsx +41 -12
- package/src/Wipe.tsx +5 -6
- package/src/ZoomControls.tsx +5 -2
- package/src/clipboard.ts +4 -5
- package/src/combobox/parts.tsx +18 -14
- package/src/command-palette/context.ts +2 -0
- package/src/command-palette/index.parts.ts +1 -0
- package/src/command-palette/parts.tsx +20 -14
- package/src/command-palette/virtual.tsx +21 -0
- package/src/dialog/parts.tsx +10 -13
- package/src/dialog-layer.ts +5 -3
- package/src/error-boundary.ts +7 -20
- package/src/file-icon.ts +8 -9
- package/src/file-tree/empty-lines.ts +24 -8
- package/src/file-tree/helpers.ts +34 -0
- package/src/file-tree/highlight.ts +19 -0
- package/src/file-tree/index.parts.ts +1 -0
- package/src/file-tree/parts.tsx +41 -5
- package/src/file-tree/use-empty-lines.ts +22 -5
- package/src/file-tree/use-file-tree-shift.tsx +10 -0
- package/src/file-tree/use-file-tree.ts +2 -0
- package/src/file-tree/use-slots.ts +77 -12
- package/src/format/datetime.ts +42 -17
- package/src/format/duration.ts +28 -10
- package/src/format/index.ts +3 -0
- package/src/format/locale.ts +33 -42
- package/src/format/number.ts +21 -27
- package/src/format/regions.ts +4 -7
- package/src/icon-picker.ts +23 -16
- package/src/index.ts +9 -0
- package/src/key-value-list/parts.tsx +1 -5
- package/src/lazy-landings.ts +2 -0
- package/src/lazy.tsx +23 -19
- package/src/locales.ts +2 -6
- package/src/match.ts +14 -0
- package/src/menu/parts.tsx +30 -55
- package/src/middle-truncate.ts +12 -3
- package/src/modality.ts +4 -0
- package/src/placement.ts +2 -1
- package/src/platform.ts +2 -0
- package/src/popover/parts.tsx +3 -5
- package/src/popup-layer.ts +6 -4
- package/src/prefetch.ts +18 -5
- package/src/project-switcher-row.tsx +2 -0
- package/src/segmented-keys.ts +6 -0
- package/src/select-shared.ts +2 -0
- package/src/selection.ts +5 -5
- package/src/settings/ConfirmDialog.tsx +3 -0
- package/src/settings/MasterDetail.tsx +3 -9
- package/src/settings/SettingsDialog.tsx +27 -9
- package/src/settings/SettingsRow.tsx +16 -10
- package/src/settings/SettingsSection.tsx +10 -5
- package/src/settings/index.ts +3 -0
- package/src/settings/target.ts +1 -0
- package/src/shortcut-hints.ts +10 -5
- package/src/shortcut.ts +26 -6
- package/src/tabs/overflow.ts +8 -6
- package/src/tabs/parts.tsx +50 -26
- package/src/testing/dedupe.ts +27 -7
- package/src/testing/fake-source.ts +1 -0
- package/src/tint.ts +13 -0
- package/src/toast-store.ts +20 -14
- package/src/tree/parts.tsx +8 -0
- package/src/tree/shift.ts +6 -0
- package/src/tree/use-tree-shift.tsx +23 -7
- package/src/useAsyncAction.ts +8 -0
- package/src/useColumnResize.ts +24 -15
- package/src/useContentSize.ts +5 -0
- package/src/useMeasuredWidth.ts +1 -0
- package/src/useNow.ts +6 -11
- package/src/workspace/DropPreview.tsx +3 -5
- package/src/workspace/SplitView.tsx +109 -0
- package/src/workspace/TabStrip.tsx +42 -0
- package/src/workspace/Workspace.tsx +5 -0
- package/src/workspace/context.ts +1 -0
- package/src/workspace/geometry.ts +49 -0
- package/src/workspace/model.ts +72 -0
- package/src/workspace/preview.ts +15 -15
package/src/IconPicker.tsx
CHANGED
|
@@ -16,35 +16,41 @@ export type { IconPickerGroup } from './icon-picker.ts';
|
|
|
16
16
|
|
|
17
17
|
/* A row of cells is 28 high with a gap of 4. Under a heading of 28 the last row keeps 8 below it; without headings the grid has 6 above as well. */
|
|
18
18
|
const ROW_HEIGHT = 32;
|
|
19
|
+
|
|
19
20
|
const HEADED_EXTRA = 32;
|
|
20
21
|
const BARE_EXTRA = 10;
|
|
21
22
|
|
|
22
23
|
export interface IconPickerProps {
|
|
23
|
-
/*
|
|
24
|
+
/* By name, in the order of the grid, or in groups under a heading each. The name is also its tooltip. */
|
|
24
25
|
icons: IconPickerIcons;
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
value: string | null;
|
|
27
28
|
onValueChange(name: string): void;
|
|
28
|
-
|
|
29
|
+
|
|
30
|
+
/* By icon name: more words a search finds it by, beside its name and its group's label. */
|
|
29
31
|
keywords?: Readonly<Record<string, readonly string[]>>;
|
|
30
|
-
|
|
32
|
+
|
|
33
|
+
/* On by default for groups, off for a flat set. */
|
|
31
34
|
searchable?: boolean;
|
|
32
|
-
|
|
35
|
+
|
|
36
|
+
/* The grid as a scroll area this many rows high. Without it the grid is as tall as its icons. */
|
|
33
37
|
rows?: number;
|
|
38
|
+
|
|
34
39
|
disabled?: boolean;
|
|
35
|
-
/* Clearing the icon. A surface that falls back to a default of its own offers that instead. */
|
|
36
40
|
onClear?: () => void;
|
|
37
|
-
/* The label above the trigger. A surface that also takes an image calls this something else, or the two read as one list. */
|
|
38
41
|
label?: string;
|
|
39
|
-
|
|
42
|
+
|
|
43
|
+
/* It still names the trigger. */
|
|
40
44
|
hideLabel?: boolean;
|
|
45
|
+
|
|
41
46
|
className?: string;
|
|
42
47
|
ref?: Ref<HTMLDivElement>;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
/*
|
|
50
|
+
/* Only a stuck heading draws a line under it. */
|
|
46
51
|
const markStuckHeadings = (area: HTMLElement): void => {
|
|
47
52
|
const top = area.scrollTop;
|
|
53
|
+
|
|
48
54
|
for (const heading of area.querySelectorAll<HTMLElement>('[data-icon-heading]')) {
|
|
49
55
|
const group = heading.parentElement;
|
|
50
56
|
const stuck = group !== null && top > 0 && group.offsetTop <= top && group.offsetTop + group.offsetHeight > top;
|
|
@@ -52,11 +58,7 @@ const markStuckHeadings = (area: HTMLElement): void => {
|
|
|
52
58
|
}
|
|
53
59
|
};
|
|
54
60
|
|
|
55
|
-
/*
|
|
56
|
-
* The mark a thing wears, one of a set of Lucide icons, behind a trigger drawn like a field. The
|
|
57
|
-
* popover holds a search over the grid. The grid is one tab stop: arrows move the focus and only
|
|
58
|
-
* Enter, Space or a click chooses, since a caller may save every choice at once.
|
|
59
|
-
*/
|
|
61
|
+
/* The grid is one tab stop: arrows move the focus and only Enter, Space or a click chooses, since a caller may save every choice at once. */
|
|
60
62
|
export function IconPicker({
|
|
61
63
|
icons,
|
|
62
64
|
value,
|
|
@@ -168,7 +170,7 @@ interface IconPickerPanelProps {
|
|
|
168
170
|
onClear: (() => void) | undefined;
|
|
169
171
|
}
|
|
170
172
|
|
|
171
|
-
/*
|
|
173
|
+
/* It mounts on every open, so a search and the focus start over each time. */
|
|
172
174
|
function IconPickerPanel({ id, name, sections, headed, value, keywords, searchable, rows, disabled, onChoose, onClear }: IconPickerPanelProps) {
|
|
173
175
|
const { t } = useTranslation('ui');
|
|
174
176
|
const input = useRef<HTMLInputElement>(null);
|
|
@@ -176,6 +178,7 @@ function IconPickerPanel({ id, name, sections, headed, value, keywords, searchab
|
|
|
176
178
|
const [query, setQuery] = useState('');
|
|
177
179
|
const [focused, setFocused] = useState<string | null>(null);
|
|
178
180
|
const shown = filterSections(sections, query, keywords);
|
|
181
|
+
|
|
179
182
|
const tabStop = tabStopOf(
|
|
180
183
|
shown.flatMap((section) => section.icons.map(([icon]) => icon)),
|
|
181
184
|
focused,
|
|
@@ -186,15 +189,18 @@ function IconPickerPanel({ id, name, sections, headed, value, keywords, searchab
|
|
|
186
189
|
useLayoutEffect(() => {
|
|
187
190
|
const element = area.current;
|
|
188
191
|
const chosen = element?.querySelector<HTMLElement>('[aria-checked="true"]');
|
|
192
|
+
|
|
189
193
|
if (!element || !chosen || element.scrollHeight <= element.clientHeight) {
|
|
190
194
|
return;
|
|
191
195
|
}
|
|
196
|
+
|
|
192
197
|
const heading = element.querySelector<HTMLElement>('[data-icon-heading]')?.offsetHeight ?? 0;
|
|
193
198
|
element.scrollTop = Math.round(chosen.offsetTop - heading - (element.clientHeight - heading - chosen.offsetHeight) / 2);
|
|
194
199
|
}, []);
|
|
195
200
|
|
|
196
201
|
const search = (next: string): void => {
|
|
197
202
|
setQuery(next);
|
|
203
|
+
|
|
198
204
|
if (area.current) {
|
|
199
205
|
area.current.scrollTop = 0;
|
|
200
206
|
}
|
|
@@ -205,6 +211,7 @@ function IconPickerPanel({ id, name, sections, headed, value, keywords, searchab
|
|
|
205
211
|
if (!escapeClearsSearch(event.key, query)) {
|
|
206
212
|
return;
|
|
207
213
|
}
|
|
214
|
+
|
|
208
215
|
event.stopPropagation();
|
|
209
216
|
event.preventDefault();
|
|
210
217
|
search('');
|
|
@@ -215,19 +222,24 @@ function IconPickerPanel({ id, name, sections, headed, value, keywords, searchab
|
|
|
215
222
|
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
216
223
|
return;
|
|
217
224
|
}
|
|
225
|
+
|
|
218
226
|
const cells = [...event.currentTarget.querySelectorAll<HTMLElement>('[role="radio"]')];
|
|
219
227
|
const from = cells.indexOf(event.target as HTMLElement);
|
|
228
|
+
|
|
220
229
|
if (from === -1) {
|
|
221
230
|
return;
|
|
222
231
|
}
|
|
232
|
+
|
|
223
233
|
const to = cellForKey(
|
|
224
234
|
event.key,
|
|
225
235
|
cells.map((cell) => cell.getBoundingClientRect()),
|
|
226
236
|
from
|
|
227
237
|
);
|
|
238
|
+
|
|
228
239
|
if (to === null) {
|
|
229
240
|
return;
|
|
230
241
|
}
|
|
242
|
+
|
|
231
243
|
event.preventDefault();
|
|
232
244
|
cells[to]?.focus();
|
|
233
245
|
};
|
|
@@ -305,10 +317,13 @@ function IconPickerPanel({ id, name, sections, headed, value, keywords, searchab
|
|
|
305
317
|
))}
|
|
306
318
|
</div>
|
|
307
319
|
);
|
|
320
|
+
|
|
308
321
|
if (section.label === null) {
|
|
309
322
|
return cells;
|
|
310
323
|
}
|
|
324
|
+
|
|
311
325
|
const headingId = `${id}-group-${index}`;
|
|
326
|
+
|
|
312
327
|
return (
|
|
313
328
|
<div key={section.id} role="group" aria-labelledby={headingId}>
|
|
314
329
|
{/* A step lighter than the popover in the dark theme, where a darker band would read as a hole in it. */}
|
package/src/Input.tsx
CHANGED
|
@@ -5,17 +5,18 @@ import { fieldControlProps, useFieldControl } from './field-context.ts';
|
|
|
5
5
|
import { Icon } from './Icon.tsx';
|
|
6
6
|
|
|
7
7
|
export interface InputProps extends Omit<ComponentProps<'input'>, 'size'> {
|
|
8
|
-
/* `sm` is the height of a small button
|
|
8
|
+
/* `sm` is the height of a small button. */
|
|
9
9
|
size?: 'md' | 'sm';
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
mono?: boolean;
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
/* `className` then goes to the box around the icon and the input; `ref` stays on the input. */
|
|
13
14
|
icon?: LucideIcon;
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
/* Inside the field, after the text. `className` then goes to the box, as with `icon`. */
|
|
15
17
|
end?: ReactNode;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
/* The one text input: one border and one focus ring at the one height or the compact one. */
|
|
19
20
|
export function Input({ size = 'md', mono = false, icon, end, className, id, ...props }: InputProps) {
|
|
20
21
|
const field = useFieldControl();
|
|
21
22
|
const small = size === 'sm';
|
|
@@ -50,19 +51,20 @@ export function Input({ size = 'md', mono = false, icon, end, className, id, ...
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export interface TextAreaProps extends ComponentProps<'textarea'> {
|
|
53
|
-
/* `sm` is the type and the padding of a small input
|
|
54
|
+
/* `sm` is the type and the padding of a small input. */
|
|
54
55
|
size?: 'md' | 'sm';
|
|
55
|
-
|
|
56
|
+
|
|
56
57
|
resize?: 'none' | 'vertical';
|
|
57
|
-
/* A command or anything else read back letter by letter. */
|
|
58
58
|
mono?: boolean;
|
|
59
|
+
|
|
59
60
|
/* `bare` drops the border, the ground and the focus outline, for a field inside a card that draws all three around it. */
|
|
60
61
|
variant?: 'field' | 'bare';
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
/*
|
|
64
|
+
/* At least 64px tall; `rows` makes it taller. */
|
|
64
65
|
export function TextArea({ size = 'md', resize = 'none', mono = false, variant = 'field', className, id, ...props }: TextAreaProps) {
|
|
65
66
|
const field = useFieldControl();
|
|
67
|
+
|
|
66
68
|
return (
|
|
67
69
|
<textarea
|
|
68
70
|
{...fieldControlProps(field, id)}
|
package/src/Kbd.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { formatShortcut, shortcutParts, type Shortcut } from './shortcut.ts';
|
|
|
6
6
|
/* A shortcut chip on the surface it sits on: in a tooltip, in a palette's footer, in a line of help. */
|
|
7
7
|
const INLINE_KBD = 'rounded-sm bg-surface-sunken px-[5px] py-px font-sans text-xs/[inherit] text-text-muted';
|
|
8
8
|
|
|
9
|
-
/*
|
|
9
|
+
/* A settings page prints a shortcut key by key, so a gesture can stand beside it. */
|
|
10
10
|
const KEY_CAP = 'inline-grid place-items-center rounded-md border border-b-2 border-border-strong bg-surface-hover bg-clip-padding font-sans text-text';
|
|
11
11
|
|
|
12
12
|
/* `sm` fits inside a small field, as the hint of the key that focuses it. */
|
|
@@ -15,17 +15,16 @@ const KEY_CAP_SIZES = { md: 'h-6 min-w-6 px-1.5 text-xs', sm: 'h-5 min-w-5 px-1
|
|
|
15
15
|
export interface KbdProps {
|
|
16
16
|
/* Printed the way this platform writes it. Without one, the children are printed as they are (`↑`, `Esc`). */
|
|
17
17
|
shortcut?: Shortcut;
|
|
18
|
+
|
|
18
19
|
children?: ReactNode;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* quiet hint at the end of the row; anywhere else it is a chip on the surface it sits on.
|
|
22
|
-
*/
|
|
20
|
+
|
|
21
|
+
/* In a menu row the `.menu-item kbd` rule dresses it; anywhere else it is a chip on the surface it sits on. */
|
|
23
22
|
variant?: 'menu' | 'inline';
|
|
23
|
+
|
|
24
24
|
className?: string;
|
|
25
25
|
ref?: Ref<HTMLElement>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/* A shortcut as this platform prints it: `⌘K` on macOS, `Ctrl+K` elsewhere. */
|
|
29
28
|
export function Kbd({ shortcut, children, variant = 'menu', className, ref }: KbdProps) {
|
|
30
29
|
return (
|
|
31
30
|
<kbd ref={ref} className={clsx(variant === 'inline' && INLINE_KBD, className)}>
|
|
@@ -51,16 +50,18 @@ export function KeyCap({ children, size = 'md', className, ref }: KeyCapProps) {
|
|
|
51
50
|
|
|
52
51
|
export interface KeysProps {
|
|
53
52
|
shortcut: Shortcut;
|
|
53
|
+
|
|
54
54
|
/* The pointer gesture the keys go with, such as "drag", as one more cap. */
|
|
55
55
|
then?: string;
|
|
56
|
+
|
|
56
57
|
size?: 'md' | 'sm';
|
|
57
58
|
className?: string;
|
|
58
59
|
ref?: Ref<HTMLSpanElement>;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
/* A shortcut as one cap per key, `⌘` and `K`, plus the pointer gesture it goes with. */
|
|
62
62
|
export function Keys({ shortcut, then, size = 'md', className, ref }: KeysProps) {
|
|
63
63
|
const parts = [...shortcutParts(shortcut, isApplePlatform()), ...(then ? [then] : [])];
|
|
64
|
+
|
|
64
65
|
return (
|
|
65
66
|
<span ref={ref} className={clsx('flex items-center', size === 'sm' ? 'gap-0.5' : 'gap-1', className)}>
|
|
66
67
|
{parts.map((part, index) => (
|
package/src/LazyBoundary.tsx
CHANGED
|
@@ -7,13 +7,13 @@ export interface LazyBoundaryProps {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* lands, so a chunk that takes 20 ms shows after 20 ms.
|
|
10
|
+
* React holds content that comes in as a retry back until 300 ms after its fallback showed; this
|
|
11
|
+
* boundary draws again as an ordinary update once a module lands, so a 20 ms chunk shows after 20 ms.
|
|
13
12
|
*/
|
|
14
13
|
export function LazyBoundary({ fallback = null, children }: LazyBoundaryProps) {
|
|
15
14
|
// Compiled, the same boundary would come back and the landing would wait for React's own retry after all.
|
|
16
15
|
'use no memo';
|
|
17
16
|
useSyncExternalStore(subscribeLandings, readLandings, readLandings);
|
|
17
|
+
|
|
18
18
|
return <Suspense fallback={fallback}>{children}</Suspense>;
|
|
19
19
|
}
|
package/src/Meter.tsx
CHANGED
|
@@ -4,19 +4,24 @@ import clsx from 'clsx';
|
|
|
4
4
|
export interface MeterProps {
|
|
5
5
|
/* In the unit of `min` and `max`, held to them. Null draws an empty track, for a level not measured yet. */
|
|
6
6
|
value: number | null;
|
|
7
|
+
|
|
7
8
|
min?: number;
|
|
8
9
|
max?: number;
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
/* Drawn as lines across the track, such as a target or a limit. */
|
|
10
12
|
marks?: readonly number[];
|
|
13
|
+
|
|
11
14
|
label: string;
|
|
15
|
+
|
|
12
16
|
/* The value in words for a screen reader, such as "-18.2 LUFS". Left out, it reads the number. */
|
|
13
17
|
valueText?: string;
|
|
18
|
+
|
|
14
19
|
/* Its width, which it fills. */
|
|
15
20
|
className?: string;
|
|
21
|
+
|
|
16
22
|
ref?: Ref<HTMLDivElement>;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
/* A level against a scale: how full it is, and where a target or a limit sits on the same scale. */
|
|
20
25
|
export function Meter({ value, min = 0, max = 100, marks = [], label, valueText, className, ref }: MeterProps) {
|
|
21
26
|
const fractionOf = (level: number): number => (max > min ? Math.min(1, Math.max(0, (level - min) / (max - min))) : 0);
|
|
22
27
|
|
package/src/MiddleTruncate.tsx
CHANGED
|
@@ -6,27 +6,33 @@ import { Tooltip } from './Tooltip.tsx';
|
|
|
6
6
|
|
|
7
7
|
export interface MiddleTruncateProps {
|
|
8
8
|
text: string;
|
|
9
|
+
|
|
9
10
|
/* The whole text on hover and focus while it is cut. Off where the caller's own tooltip already says it. */
|
|
10
11
|
tooltip?: boolean;
|
|
12
|
+
|
|
11
13
|
className?: string;
|
|
12
14
|
ref?: Ref<HTMLSpanElement>;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
let canvas: HTMLCanvasElement | null = null;
|
|
16
18
|
|
|
17
|
-
/*
|
|
19
|
+
/* Null where the page has no canvas. */
|
|
18
20
|
function textMeasure(element: HTMLElement): ((text: string) => number) | null {
|
|
19
21
|
const view = element.ownerDocument.defaultView;
|
|
20
22
|
canvas ??= element.ownerDocument.createElement('canvas');
|
|
21
23
|
const context = canvas.getContext('2d');
|
|
24
|
+
|
|
22
25
|
if (context === null || view === null) {
|
|
23
26
|
return null;
|
|
24
27
|
}
|
|
28
|
+
|
|
25
29
|
const style = view.getComputedStyle(element);
|
|
26
30
|
context.font = style.font || `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
|
|
31
|
+
|
|
27
32
|
if ('letterSpacing' in context) {
|
|
28
33
|
context.letterSpacing = style.letterSpacing === 'normal' ? '0px' : style.letterSpacing;
|
|
29
34
|
}
|
|
35
|
+
|
|
30
36
|
return (text) => context.measureText(text).width;
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -34,11 +40,7 @@ function sameCut(first: MiddleCut | null, second: MiddleCut | null): boolean {
|
|
|
34
40
|
return first?.head === second?.head && first?.tail === second?.tail;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
/*
|
|
38
|
-
* A path or a branch name on one line that keeps its start and its end and cuts the middle. It asks
|
|
39
|
-
* for the width of the whole text and measures what it gets. The part it cuts stays in the element
|
|
40
|
-
* at no width, so selecting, copying and finding read the whole text.
|
|
41
|
-
*/
|
|
43
|
+
/* The part it cuts stays in the element at no width, so selecting, copying and finding read the whole text. */
|
|
42
44
|
export function MiddleTruncate({ text, tooltip = true, className, ref }: MiddleTruncateProps) {
|
|
43
45
|
const [element, setElement] = useState<HTMLSpanElement | null>(null);
|
|
44
46
|
const [cut, setCut] = useState<MiddleCut | null>(null);
|
|
@@ -48,18 +50,23 @@ export function MiddleTruncate({ text, tooltip = true, className, ref }: MiddleT
|
|
|
48
50
|
if (element === null) {
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
53
|
+
|
|
51
54
|
const update = (): void => {
|
|
52
55
|
const measure = textMeasure(element);
|
|
53
56
|
// Layout rounds what a canvas measures, so a text a fraction wider than its box still fits.
|
|
54
57
|
const next = measure === null ? null : fitMiddle(text, element.getBoundingClientRect().width + 0.5, measure);
|
|
55
58
|
setCut((current) => (sameCut(current, next) ? current : next));
|
|
56
59
|
};
|
|
60
|
+
|
|
57
61
|
update();
|
|
62
|
+
|
|
58
63
|
if (typeof ResizeObserver === 'undefined') {
|
|
59
64
|
return;
|
|
60
65
|
}
|
|
66
|
+
|
|
61
67
|
const observer = new ResizeObserver(update);
|
|
62
68
|
observer.observe(element);
|
|
69
|
+
|
|
63
70
|
return () => observer.disconnect();
|
|
64
71
|
}, [element, text]);
|
|
65
72
|
|
|
@@ -76,9 +83,11 @@ export function MiddleTruncate({ text, tooltip = true, className, ref }: MiddleT
|
|
|
76
83
|
)}
|
|
77
84
|
</span>
|
|
78
85
|
);
|
|
86
|
+
|
|
79
87
|
// Only a cut text has more to say, and a long list of whole ones keeps no hover handlers.
|
|
80
88
|
if (cut === null || !tooltip) {
|
|
81
89
|
return content;
|
|
82
90
|
}
|
|
91
|
+
|
|
83
92
|
return <Tooltip label={text}>{content}</Tooltip>;
|
|
84
93
|
}
|
package/src/PanelEmpty.tsx
CHANGED
|
@@ -6,23 +6,25 @@ import { EmptyState } from './EmptyState.tsx';
|
|
|
6
6
|
export interface PanelEmptyProps {
|
|
7
7
|
/* The panel's own header, kept above the sentence so a person can still act while nothing is listed. */
|
|
8
8
|
header?: ReactNode;
|
|
9
|
+
|
|
9
10
|
icon?: LucideIcon;
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
/* A `Spinner` in place of the icon. */
|
|
11
13
|
busy?: boolean;
|
|
14
|
+
|
|
12
15
|
action?: ReactNode;
|
|
13
|
-
|
|
16
|
+
|
|
17
|
+
/* For a box that has no ground behind it yet, such as a card on a canvas. */
|
|
14
18
|
sunken?: boolean;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* height of its own, and `grow` means nothing to the block that lays it out.
|
|
18
|
-
*/
|
|
19
|
+
|
|
20
|
+
/* A panel is one track of a flex column; a card is a box with a height of its own, where `grow` means nothing. */
|
|
19
21
|
fill?: 'grow' | 'full';
|
|
22
|
+
|
|
20
23
|
children: ReactNode;
|
|
21
24
|
className?: string;
|
|
22
25
|
ref?: Ref<HTMLDivElement>;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
/* What a panel or a card shows while it holds nothing: a sentence, one icon, at most one button. */
|
|
26
28
|
export function PanelEmpty({ header, icon, busy = false, action, sunken = false, fill = 'grow', children, className, ref }: PanelEmptyProps) {
|
|
27
29
|
return (
|
|
28
30
|
<div ref={ref} className={clsx('grid min-h-0 place-items-center', fill === 'full' ? 'h-full' : 'grow', sunken && 'bg-surface-sunken', className)}>
|
package/src/PanelHeader.tsx
CHANGED
|
@@ -4,14 +4,11 @@ import { useRender } from '@base-ui/react/use-render';
|
|
|
4
4
|
import { SectionLabel } from './SectionLabel.tsx';
|
|
5
5
|
|
|
6
6
|
export type PanelHeaderProps = useRender.ComponentProps<'header'> & {
|
|
7
|
-
/*
|
|
7
|
+
/* Left out when the caller lays out the name itself. */
|
|
8
8
|
title?: ReactNode;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
/*
|
|
12
|
-
* The bar across the top of a side panel, as tall as a 48px toolbar beside it: the panel's name,
|
|
13
|
-
* its own controls, and a close button last.
|
|
14
|
-
*/
|
|
11
|
+
/* As tall as a 48px toolbar beside it. */
|
|
15
12
|
export function PanelHeader({ title, render, className, ref, children, ...props }: PanelHeaderProps) {
|
|
16
13
|
return useRender({
|
|
17
14
|
render,
|
package/src/Pill.tsx
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { ReactNode, Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
|
|
4
|
-
/*
|
|
5
|
-
* What the label says about the thing it names, drawn as its ground. Every ground is an alpha over
|
|
6
|
-
* whatever the pill stands on, the way a border is: in the dark theme it lifts a shade off the row
|
|
7
|
-
* under it, in the light theme it sinks one. A solid sunken ground read as a hole on a dark row.
|
|
8
|
-
*/
|
|
4
|
+
/* Every ground is an alpha over whatever the pill stands on, the way a border is: a solid sunken ground read as a hole on a dark row. */
|
|
9
5
|
const TONES = {
|
|
10
6
|
muted: 'bg-text/7 text-text-muted',
|
|
11
7
|
/* A step further off the ground, for a label that should stand out as a control. */
|
|
@@ -34,18 +30,19 @@ export interface PillProps {
|
|
|
34
30
|
tone?: keyof typeof TONES;
|
|
35
31
|
shape?: keyof typeof SHAPES;
|
|
36
32
|
size?: 'md' | 'sm';
|
|
37
|
-
/* A branch name or a count reads better in the monospace face. */
|
|
38
33
|
mono?: boolean;
|
|
34
|
+
|
|
39
35
|
/* With a handler the pill is a button; without one it is a label. */
|
|
40
36
|
onClick?: () => void;
|
|
41
|
-
|
|
37
|
+
|
|
38
|
+
/* Makes the button a toggle. */
|
|
42
39
|
pressed?: boolean;
|
|
40
|
+
|
|
43
41
|
disabled?: boolean;
|
|
44
42
|
className?: string;
|
|
45
43
|
ref?: Ref<HTMLElement>;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
/* The small rounded label in a header, a sidebar row or a settings line: a count, a branch, a status. */
|
|
49
46
|
export function Pill({ icon, children, tone = 'muted', shape = 'pill', size = 'md', mono = false, onClick, pressed, disabled, className, ref }: PillProps) {
|
|
50
47
|
// Never broken inside itself: in a row that wraps, a pill that does not fit moves to the next line whole.
|
|
51
48
|
const shared = clsx(
|
|
@@ -56,6 +53,7 @@ export function Pill({ icon, children, tone = 'muted', shape = 'pill', size = 'm
|
|
|
56
53
|
mono && 'font-mono',
|
|
57
54
|
className
|
|
58
55
|
);
|
|
56
|
+
|
|
59
57
|
if (!onClick) {
|
|
60
58
|
return (
|
|
61
59
|
<span ref={ref} className={shared}>
|
|
@@ -64,6 +62,7 @@ export function Pill({ icon, children, tone = 'muted', shape = 'pill', size = 'm
|
|
|
64
62
|
</span>
|
|
65
63
|
);
|
|
66
64
|
}
|
|
65
|
+
|
|
67
66
|
return (
|
|
68
67
|
<button
|
|
69
68
|
ref={ref as Ref<HTMLButtonElement>}
|
package/src/ProjectSwitcher.tsx
CHANGED
|
@@ -10,14 +10,19 @@ export interface ProjectSwitcherItem {
|
|
|
10
10
|
id: string;
|
|
11
11
|
name: string;
|
|
12
12
|
icon?: ReactNode;
|
|
13
|
+
|
|
13
14
|
/* The folder or other details, shown in a tooltip so the row stays on one line. */
|
|
14
15
|
description?: ReactNode;
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
/* At the end of the row. */
|
|
16
18
|
hint?: ReactNode;
|
|
19
|
+
|
|
17
20
|
disabled?: boolean;
|
|
21
|
+
|
|
18
22
|
/* A stale or disconnected project can still be selected to try opening it. */
|
|
19
23
|
muted?: boolean;
|
|
20
|
-
|
|
24
|
+
|
|
25
|
+
/* For the submenu beside this project's row. */
|
|
21
26
|
actions?: ReactNode;
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -26,14 +31,19 @@ export interface ProjectSwitcherProps<Item extends ProjectSwitcherItem = Project
|
|
|
26
31
|
projects: readonly Item[];
|
|
27
32
|
recentProjects?: readonly Item[];
|
|
28
33
|
onSelect(project: Item, event: MouseEvent<HTMLElement>): void;
|
|
29
|
-
|
|
34
|
+
|
|
35
|
+
/* Before the current project's icon. */
|
|
30
36
|
leading?: ReactNode;
|
|
31
|
-
|
|
37
|
+
|
|
38
|
+
/* A group of their own between the open projects and Recent projects. */
|
|
32
39
|
before?: ReactNode;
|
|
33
|
-
|
|
40
|
+
|
|
41
|
+
/* In one group with Recent projects. */
|
|
34
42
|
children?: ReactNode;
|
|
43
|
+
|
|
35
44
|
/* What the trigger reads while `current` is null; next to a current project, it comes before that name in the accessible name. */
|
|
36
45
|
label?: string;
|
|
46
|
+
|
|
37
47
|
disabled?: boolean;
|
|
38
48
|
open?: boolean;
|
|
39
49
|
defaultOpen?: boolean;
|
|
@@ -83,7 +93,7 @@ export function ProjectSwitcher<Item extends ProjectSwitcherItem>({
|
|
|
83
93
|
);
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
/*
|
|
96
|
+
/* Apart from the popup, so the rows render without an open menu. */
|
|
87
97
|
export function ProjectSwitcherGroups<Item extends ProjectSwitcherItem>({
|
|
88
98
|
projects,
|
|
89
99
|
recentProjects = [],
|
package/src/PromptDialog.tsx
CHANGED
|
@@ -11,56 +11,59 @@ import { useAsyncAction } from './useAsyncAction.ts';
|
|
|
11
11
|
export interface PromptDialogProps {
|
|
12
12
|
open: boolean;
|
|
13
13
|
title: string;
|
|
14
|
-
/* The mark beside the title, for a dialog that names the thing it is about. */
|
|
15
14
|
titleIcon?: LucideIcon;
|
|
16
15
|
description?: ReactNode;
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
/* Without it the dialog is a confirm and nothing else. */
|
|
18
18
|
field?: {
|
|
19
|
-
/*
|
|
19
|
+
/* Without one only a screen reader hears the name, from `ariaLabel`. */
|
|
20
20
|
label?: string;
|
|
21
|
+
|
|
21
22
|
ariaLabel?: string;
|
|
22
23
|
initial?: string;
|
|
23
24
|
placeholder?: string;
|
|
24
|
-
/* A branch, a path or anything else read back letter by letter. */
|
|
25
25
|
mono?: boolean;
|
|
26
26
|
maxLength?: number;
|
|
27
27
|
};
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
/* A second, taller field under it. */
|
|
29
30
|
area?: { label: string; initial?: string; placeholder?: string };
|
|
31
|
+
|
|
30
32
|
confirmLabel: string;
|
|
31
|
-
/* What the button says while the step runs, for one slow enough that a quiet button is not enough. */
|
|
32
33
|
confirmBusyLabel?: string;
|
|
33
34
|
confirmIcon?: LucideIcon;
|
|
34
35
|
danger?: boolean;
|
|
35
|
-
|
|
36
|
+
|
|
37
|
+
/* For a step the caller runs elsewhere; a step this dialog awaits says so itself. */
|
|
36
38
|
busy?: boolean;
|
|
37
|
-
|
|
39
|
+
|
|
38
40
|
allowEmpty?: boolean;
|
|
41
|
+
|
|
39
42
|
/* A reason this question cannot be answered at all, beside the one an empty field is. */
|
|
40
43
|
confirmDisabled?: boolean;
|
|
41
|
-
|
|
44
|
+
|
|
42
45
|
secondary?: { label: string; onClick(): void };
|
|
43
|
-
/* Over another dialog, so what is under it is dimmed a second time. */
|
|
44
46
|
nested?: boolean;
|
|
45
|
-
|
|
47
|
+
|
|
48
|
+
/* Between the description and the buttons. */
|
|
46
49
|
children?: ReactNode;
|
|
47
|
-
|
|
50
|
+
|
|
51
|
+
/* A rejection stays on screen as the reason and leaves the dialog open. */
|
|
48
52
|
onConfirm(value: string, body: string): void | Promise<unknown>;
|
|
49
|
-
|
|
53
|
+
|
|
54
|
+
/* For a rejection that carries no sentence of its own. */
|
|
50
55
|
fallbackMessage?: string;
|
|
51
|
-
|
|
56
|
+
|
|
57
|
+
/* Only ever told `false`: the dialog never closes itself. */
|
|
52
58
|
onOpenChange(open: boolean): void;
|
|
53
|
-
|
|
59
|
+
|
|
60
|
+
/* On the popup. */
|
|
54
61
|
className?: string;
|
|
62
|
+
|
|
55
63
|
ref?: Ref<HTMLDivElement>;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
/*
|
|
59
|
-
* The one dialog a question is asked in: a name to type, or a warning to agree with. Every confirm
|
|
60
|
-
* and every rename shares it, so none of them grows a layout of its own. It never closes itself,
|
|
61
|
-
* because what a confirm leads to is the caller's business: an action that asks a second question
|
|
62
|
-
* would be shut out by a close of its own.
|
|
63
|
-
*/
|
|
66
|
+
/* It never closes itself: an action that asks a second question would be shut out by a close of its own. */
|
|
64
67
|
export function PromptDialog({
|
|
65
68
|
open,
|
|
66
69
|
title,
|
|
@@ -85,20 +88,21 @@ export function PromptDialog({
|
|
|
85
88
|
ref
|
|
86
89
|
}: PromptDialogProps) {
|
|
87
90
|
const { t } = useTranslation('ui');
|
|
88
|
-
/* The dialog stays mounted between questions
|
|
89
|
-
The token is what it was handed, so a new question resets the fields in the same render that
|
|
90
|
-
shows it and not in a second one after. */
|
|
91
|
+
/* The dialog stays mounted between questions; a new question resets the fields in the render that shows it. */
|
|
91
92
|
const token = open ? `${field?.initial ?? ''}\u0000${area?.initial ?? ''}` : '';
|
|
92
93
|
const [draft, setDraft] = useState({ token, value: field?.initial ?? '', body: area?.initial ?? '' });
|
|
93
94
|
const step = useAsyncAction(fallbackMessage);
|
|
95
|
+
|
|
94
96
|
if (draft.token !== token) {
|
|
95
97
|
setDraft({ token, value: field?.initial ?? '', body: area?.initial ?? '' });
|
|
96
98
|
step.clear();
|
|
97
99
|
}
|
|
98
|
-
|
|
99
|
-
const body = draft
|
|
100
|
+
|
|
101
|
+
const { value, body } = draft;
|
|
100
102
|
const working = busy || step.busy;
|
|
103
|
+
|
|
101
104
|
const setValue = (next: string): void => setDraft({ ...draft, value: next });
|
|
105
|
+
|
|
102
106
|
const setBody = (next: string): void => setDraft({ ...draft, body: next });
|
|
103
107
|
|
|
104
108
|
const submit = (): void => {
|
|
@@ -131,6 +135,7 @@ export function PromptDialog({
|
|
|
131
135
|
onKeyDown={(event) => {
|
|
132
136
|
// Whatever listens on the window has keys of its own, and a name may hold any of them.
|
|
133
137
|
event.stopPropagation();
|
|
138
|
+
|
|
134
139
|
if (event.key === 'Enter') {
|
|
135
140
|
event.preventDefault();
|
|
136
141
|
submit();
|