@adecore/ui 0.39.0 → 0.40.1
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 +10 -10
- package/src/AccentSwatches.tsx +9 -3
- package/src/Banner.tsx +5 -11
- package/src/Button.tsx +4 -0
- package/src/Checkbox.tsx +3 -0
- package/src/ChoiceCards.tsx +12 -11
- package/src/CloseButton.tsx +1 -0
- package/src/ColorSwatch.tsx +7 -8
- package/src/ColorWheelPicker.tsx +28 -12
- package/src/ColumnResizeHandle.tsx +1 -0
- package/src/CopyButton.tsx +6 -0
- package/src/DisabledReason.tsx +1 -0
- package/src/DockShell.tsx +11 -10
- package/src/EmptyState.tsx +10 -6
- package/src/ErrorBoundary.tsx +13 -10
- package/src/Field.tsx +11 -13
- package/src/FileIcon.tsx +4 -4
- package/src/Icon.tsx +2 -4
- package/src/IconButton.tsx +14 -5
- package/src/IconPicker.tsx +30 -15
- package/src/Input.tsx +11 -9
- package/src/Kbd.tsx +8 -7
- package/src/LazyBoundary.tsx +3 -3
- package/src/Meter.tsx +7 -2
- package/src/MiddleTruncate.tsx +15 -6
- package/src/PanelEmpty.tsx +9 -7
- package/src/PanelHeader.tsx +2 -5
- package/src/Pill.tsx +7 -8
- package/src/ProjectSwitcher.tsx +16 -6
- package/src/PromptDialog.tsx +31 -26
- package/src/RelativeTime.tsx +6 -1
- package/src/SegmentBar.tsx +22 -11
- package/src/Segmented.tsx +16 -11
- package/src/Select.tsx +7 -4
- package/src/SelectSearch.tsx +3 -0
- package/src/ShortcutHints.tsx +24 -3
- package/src/Skeleton.tsx +1 -0
- package/src/Slider.tsx +4 -1
- package/src/SlidingColumn.tsx +18 -14
- package/src/Spinner.tsx +1 -0
- package/src/SplitButton.tsx +8 -8
- package/src/Stepper.tsx +6 -0
- package/src/Switch.tsx +2 -0
- package/src/TextMenu.tsx +2 -5
- package/src/Tile.tsx +2 -6
- package/src/Toasts.tsx +8 -12
- package/src/Tooltip.tsx +10 -5
- package/src/UIProvider.tsx +7 -7
- package/src/Waveform.tsx +41 -12
- package/src/Wipe.tsx +5 -6
- package/src/ZoomControls.tsx +5 -2
- package/src/clipboard.ts +4 -5
- package/src/combobox/parts.tsx +18 -14
- package/src/command-palette/context.ts +2 -0
- package/src/command-palette/index.parts.ts +1 -0
- package/src/command-palette/parts.tsx +20 -14
- package/src/command-palette/virtual.tsx +21 -0
- package/src/dialog/parts.tsx +10 -13
- package/src/dialog-layer.ts +5 -3
- package/src/error-boundary.ts +7 -20
- package/src/file-icon.ts +8 -9
- package/src/file-tree/empty-lines.ts +24 -8
- package/src/file-tree/helpers.ts +34 -0
- package/src/file-tree/highlight.ts +19 -0
- package/src/file-tree/index.parts.ts +1 -0
- package/src/file-tree/parts.tsx +41 -5
- package/src/file-tree/use-empty-lines.ts +22 -5
- package/src/file-tree/use-file-tree-shift.tsx +10 -0
- package/src/file-tree/use-file-tree.ts +2 -0
- package/src/file-tree/use-slots.ts +77 -12
- package/src/format/datetime.ts +42 -17
- package/src/format/duration.ts +28 -10
- package/src/format/index.ts +3 -0
- package/src/format/locale.ts +33 -42
- package/src/format/number.ts +21 -27
- package/src/format/regions.ts +4 -7
- package/src/icon-picker.ts +23 -16
- package/src/index.ts +8 -0
- package/src/key-value-list/parts.tsx +1 -5
- package/src/lazy-landings.ts +2 -0
- package/src/lazy.tsx +23 -19
- package/src/locales.ts +2 -6
- package/src/match.ts +14 -0
- package/src/menu/parts.tsx +30 -55
- package/src/middle-truncate.ts +12 -3
- package/src/modality.ts +4 -0
- package/src/placement.ts +2 -1
- package/src/platform.ts +2 -0
- package/src/popover/parts.tsx +3 -5
- package/src/popup-layer.ts +6 -4
- package/src/prefetch.ts +18 -5
- package/src/project-switcher-row.tsx +2 -0
- package/src/segmented-keys.ts +6 -0
- package/src/select-shared.ts +2 -0
- package/src/selection.ts +5 -5
- package/src/settings/ConfirmDialog.tsx +3 -0
- package/src/settings/MasterDetail.tsx +3 -9
- package/src/settings/SettingsDialog.tsx +27 -9
- package/src/settings/SettingsRow.tsx +16 -10
- package/src/settings/SettingsSection.tsx +10 -5
- package/src/settings/index.ts +3 -0
- package/src/settings/target.ts +1 -0
- package/src/shortcut-hints.ts +10 -5
- package/src/shortcut.ts +26 -6
- package/src/tabs/overflow.ts +8 -6
- package/src/tabs/parts.tsx +50 -26
- package/src/testing/dedupe.ts +27 -7
- package/src/testing/fake-source.ts +1 -0
- package/src/tint.ts +3 -5
- package/src/toast-store.ts +20 -14
- package/src/tree/parts.tsx +8 -0
- package/src/tree/shift.ts +6 -0
- package/src/tree/use-tree-shift.tsx +23 -7
- package/src/useAsyncAction.ts +8 -0
- package/src/useColumnResize.ts +24 -15
- package/src/useContentSize.ts +5 -0
- package/src/useMeasuredWidth.ts +1 -0
- package/src/useNow.ts +6 -11
- package/src/workspace/DropPreview.tsx +3 -5
- package/src/workspace/SplitView.tsx +109 -0
- package/src/workspace/TabStrip.tsx +42 -0
- package/src/workspace/Workspace.tsx +5 -0
- package/src/workspace/context.ts +1 -0
- package/src/workspace/geometry.ts +49 -0
- package/src/workspace/model.ts +72 -0
- package/src/workspace/preview.ts +15 -15
package/src/Toasts.tsx
CHANGED
|
@@ -27,14 +27,12 @@ const TONE = {
|
|
|
27
27
|
/* The box is as tall as the title's own line, so what sits beside the title centers on that line. */
|
|
28
28
|
const TITLE_LINE = 'flex h-(--text-sm--line-height) shrink-0 items-center';
|
|
29
29
|
|
|
30
|
-
/*
|
|
31
|
-
* How long the offer in a toast still stands, drawn from the timer that takes the toast away: the
|
|
32
|
-
* ring starts as far along as that timer already is, so both end together. With motion off it stays full.
|
|
33
|
-
*/
|
|
30
|
+
/* Starts as far along as the timer that takes the toast away, so both end together. With motion off it stays full. */
|
|
34
31
|
function TimerRing({ deadline, onDismiss }: { deadline: ToastDeadline; onDismiss(): void }) {
|
|
35
32
|
const { t } = useTranslation('ui');
|
|
36
33
|
const lifetime = deadline.end - deadline.start;
|
|
37
34
|
const [elapsed] = useState(() => elapsedOf(deadline, Date.now()));
|
|
35
|
+
|
|
38
36
|
return (
|
|
39
37
|
<IconButton size="xs" label={t('action.dismiss')} className="group/ring relative -my-px" onClick={onDismiss}>
|
|
40
38
|
<svg width={16} height={16} viewBox="0 0 16 16" className="-rotate-90" aria-hidden>
|
|
@@ -74,6 +72,7 @@ function ToastCard<T extends Toast>({ toast, footer, onDismiss }: { toast: T; fo
|
|
|
74
72
|
const actions = [...(toast.action === undefined ? [] : [toast.action]), ...(toast.actions ?? [])];
|
|
75
73
|
// An offer that runs out shows how long it has left instead.
|
|
76
74
|
const countdown = actions.length > 0 ? toast.deadline : undefined;
|
|
75
|
+
|
|
77
76
|
return (
|
|
78
77
|
<Surface className="group flex items-start gap-3 rounded-[10px] py-2.5 pr-2 pl-3">
|
|
79
78
|
<span className={clsx(TITLE_LINE, 'w-4 justify-center')}>
|
|
@@ -85,8 +84,7 @@ function ToastCard<T extends Toast>({ toast, footer, onDismiss }: { toast: T; fo
|
|
|
85
84
|
</span>
|
|
86
85
|
<div className="flex min-w-0 grow flex-col gap-1">
|
|
87
86
|
<span className="text-sm break-words text-text">{toast.title}</span>
|
|
88
|
-
{/* Wraps rather than clips:
|
|
89
|
-
wrong, and a reason cut off at the card's edge is no reason at all. */}
|
|
87
|
+
{/* Wraps rather than clips: a reason cut off at the card's edge is no reason at all. */}
|
|
90
88
|
{toast.description !== undefined && toast.description !== '' && (
|
|
91
89
|
<span className="text-xs break-words text-pretty text-text-muted">{toast.description}</span>
|
|
92
90
|
)}
|
|
@@ -122,20 +120,18 @@ function ToastCard<T extends Toast>({ toast, footer, onDismiss }: { toast: T; fo
|
|
|
122
120
|
|
|
123
121
|
export interface ToastsProps<T extends Toast> {
|
|
124
122
|
store: ToastStoreHook<T>;
|
|
125
|
-
|
|
123
|
+
|
|
124
|
+
/* Drawn under the description. */
|
|
126
125
|
footer?: (toast: T) => ReactNode;
|
|
126
|
+
|
|
127
127
|
className?: string;
|
|
128
128
|
ref?: Ref<HTMLDivElement>;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/*
|
|
132
|
-
* Bottom right, over everything: what an action is doing while it runs and how it went when it is
|
|
133
|
-
* over, and a deletion that can still be taken back. A success takes itself away after four seconds,
|
|
134
|
-
* a deletion after eight, and a failure waits to be read.
|
|
135
|
-
*/
|
|
136
131
|
export function Toasts<T extends Toast>({ store, footer, className, ref }: ToastsProps<T>) {
|
|
137
132
|
// Through `useStore`: the compiler takes a call of the prop itself for a plain function and would skip it on a later draw.
|
|
138
133
|
const toasts = useStore(store, (state) => state.toasts);
|
|
134
|
+
|
|
139
135
|
// Stays mounted with nothing in it: a screen reader skips what a live region says the moment it is inserted.
|
|
140
136
|
return (
|
|
141
137
|
<div
|
package/src/Tooltip.tsx
CHANGED
|
@@ -19,28 +19,33 @@ export interface TooltipProps {
|
|
|
19
19
|
label: ReactNode;
|
|
20
20
|
open?: boolean;
|
|
21
21
|
onOpenChange?(open: boolean): void;
|
|
22
|
+
|
|
22
23
|
/* A virtual anchor positions a controlled hint over canvas content without adding an input target. */
|
|
23
24
|
anchor?: ComponentProps<typeof BaseTooltip.Positioner>['anchor'];
|
|
25
|
+
|
|
24
26
|
/* A shortcut, or a short phrase about a key that is not one ("Shift skips the cache"). */
|
|
25
27
|
kbd?: Shortcut | string;
|
|
28
|
+
|
|
26
29
|
side?: TooltipSide;
|
|
27
30
|
sideOffset?: number;
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
|
|
32
|
+
/* Makes the label the accessible name of the trigger as well, so the name and the tooltip never say two different things. */
|
|
30
33
|
name?: boolean;
|
|
31
|
-
|
|
34
|
+
|
|
32
35
|
disabled?: boolean;
|
|
33
|
-
|
|
36
|
+
|
|
37
|
+
/* Its own children and handlers are kept; Base UI merges the tooltip props in. */
|
|
34
38
|
children?: ReactElement<Record<string, unknown>>;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
/*
|
|
41
|
+
/* Every hint in an app is one of these, never a `title`. */
|
|
38
42
|
export function Tooltip({ label, open, onOpenChange, anchor, kbd, side = 'top', sideOffset = 6, name = false, disabled = false, children }: TooltipProps) {
|
|
39
43
|
// Held Cmd (Ctrl off macOS) prints this shortcut under the trigger (`ShortcutHints`).
|
|
40
44
|
const hintRef = (element: HTMLElement | null) => {
|
|
41
45
|
if (element === null || kbd === undefined || typeof kbd === 'string') {
|
|
42
46
|
return undefined;
|
|
43
47
|
}
|
|
48
|
+
|
|
44
49
|
return registerShortcutHint(element, kbd);
|
|
45
50
|
};
|
|
46
51
|
|
package/src/UIProvider.tsx
CHANGED
|
@@ -7,27 +7,27 @@ import { startInputModality } from './modality.ts';
|
|
|
7
7
|
import { TooltipProvider } from './Tooltip.tsx';
|
|
8
8
|
|
|
9
9
|
export interface UIProviderProps {
|
|
10
|
-
/* The
|
|
10
|
+
/* The library's `ui` namespace is added to it, in every language it ships. */
|
|
11
11
|
i18n: I18n;
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
/* Without one the formatters write English in the region of that language. */
|
|
13
14
|
formatSource?: FormatSource;
|
|
15
|
+
|
|
14
16
|
children: ReactNode;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
/*
|
|
18
|
-
* Everything the library needs from an app, once, around the whole tree: its words in the app's
|
|
19
|
-
* i18next, the source of the formatters, the shared tooltip delay and the note of which input device
|
|
20
|
-
* is in use. Mount it above the first component of the library.
|
|
21
|
-
*/
|
|
22
19
|
export function UIProvider({ i18n, formatSource, children }: UIProviderProps) {
|
|
23
20
|
const [handed, setHanded] = useState<{ i18n: I18n | null; source: FormatSource | undefined }>({ i18n: null, source: undefined });
|
|
21
|
+
|
|
24
22
|
// In render and not in an effect, so the first child already reads the words and the region.
|
|
25
23
|
// Both calls only store what they are handed, so a render React throws away leaves nothing wrong behind.
|
|
26
24
|
if (handed.i18n !== i18n || handed.source !== formatSource) {
|
|
27
25
|
addUiResources(i18n);
|
|
26
|
+
|
|
28
27
|
if (formatSource !== undefined) {
|
|
29
28
|
setFormatSource(formatSource);
|
|
30
29
|
}
|
|
30
|
+
|
|
31
31
|
setHanded({ i18n, source: formatSource });
|
|
32
32
|
}
|
|
33
33
|
|
package/src/Waveform.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { useMeasuredWidth } from './useMeasuredWidth.ts';
|
|
|
7
7
|
|
|
8
8
|
/* About one bar per 3px, each 2px wide; a bar sits at its own time, so it lines up with the marks and the playhead. */
|
|
9
9
|
const BAR_PITCH = 3;
|
|
10
|
+
|
|
10
11
|
const BAR_WIDTH = 2;
|
|
11
12
|
|
|
12
13
|
/* The height of a silent bar, so silence still reads as a line. */
|
|
@@ -20,50 +21,62 @@ const barsOf = (levels: readonly number[], duration: number, from: number, to: n
|
|
|
20
21
|
if (count <= 0 || levels.length === 0 || duration <= 0 || to <= from) {
|
|
21
22
|
return [];
|
|
22
23
|
}
|
|
24
|
+
|
|
23
25
|
const perSecond = levels.length / duration;
|
|
26
|
+
|
|
24
27
|
return Array.from({ length: count }, (_, i) => {
|
|
25
28
|
const start = from + ((to - from) * i) / count;
|
|
26
29
|
const end = from + ((to - from) * (i + 1)) / count;
|
|
30
|
+
|
|
27
31
|
if (end <= 0 || start >= duration) {
|
|
28
32
|
return null;
|
|
29
33
|
}
|
|
34
|
+
|
|
30
35
|
const first = Math.max(0, Math.floor(start * perSecond));
|
|
31
36
|
const last = Math.min(levels.length - 1, Math.max(first, Math.ceil(end * perSecond) - 1));
|
|
32
37
|
let level = 0;
|
|
38
|
+
|
|
33
39
|
for (let index = first; index <= last; index++) {
|
|
34
40
|
level = Math.max(level, levels[index] ?? 0);
|
|
35
41
|
}
|
|
42
|
+
|
|
36
43
|
return Math.min(1, Math.max(SILENCE, level));
|
|
37
44
|
});
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
export interface WaveformProps {
|
|
41
|
-
/*
|
|
48
|
+
/* Samples spread evenly over the duration, from 0 for silence to 1 for the loudest, at any rate. */
|
|
42
49
|
levels: readonly number[];
|
|
43
|
-
|
|
50
|
+
|
|
51
|
+
/* In seconds, as are all times here. */
|
|
44
52
|
duration: number;
|
|
45
|
-
|
|
53
|
+
|
|
54
|
+
/* Defaults to the whole. */
|
|
46
55
|
range?: readonly [from: number, to: number];
|
|
47
|
-
|
|
56
|
+
|
|
57
|
+
/* Left out, the waveform draws no playhead. */
|
|
48
58
|
value?: number;
|
|
49
|
-
|
|
59
|
+
|
|
60
|
+
/* Without it the waveform only shows. */
|
|
50
61
|
onValueChange?(value: number): void;
|
|
51
|
-
|
|
62
|
+
|
|
63
|
+
/* Where a drag lets go and where a key moved to. */
|
|
52
64
|
onValueCommitted?(value: number): void;
|
|
53
|
-
|
|
65
|
+
|
|
54
66
|
step?: number;
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
|
|
68
|
+
/* Runs first; a key it calls `preventDefault` on, the waveform leaves alone and lets bubble. */
|
|
57
69
|
onKeyDown?(e: KeyboardEvent<HTMLDivElement>): void;
|
|
58
|
-
|
|
70
|
+
|
|
59
71
|
marks?: readonly number[];
|
|
60
72
|
label: string;
|
|
73
|
+
|
|
61
74
|
/* Its height, which the waveform fills. */
|
|
62
75
|
className?: string;
|
|
76
|
+
|
|
63
77
|
ref?: Ref<HTMLDivElement>;
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
/* The loudness of a piece of audio over time, played up to the playhead, where a press or a drag seeks. */
|
|
67
80
|
export function Waveform({
|
|
68
81
|
levels,
|
|
69
82
|
duration,
|
|
@@ -88,11 +101,18 @@ export function Waveform({
|
|
|
88
101
|
const bars = barsOf(levels, duration, from, to, count);
|
|
89
102
|
|
|
90
103
|
const positionOf = (time: number): number => (span > 0 ? (time - from) / span : 0);
|
|
91
|
-
|
|
104
|
+
|
|
105
|
+
const inView = (time: number): boolean => {
|
|
106
|
+
const position = positionOf(time);
|
|
107
|
+
|
|
108
|
+
return position >= 0 && position <= 1;
|
|
109
|
+
};
|
|
110
|
+
|
|
92
111
|
const clamp = (time: number): number => Math.min(duration, Math.max(0, time));
|
|
93
112
|
|
|
94
113
|
const seekAt = (e: PointerEvent<HTMLDivElement>): void => {
|
|
95
114
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
115
|
+
|
|
96
116
|
if (rect.width > 0) {
|
|
97
117
|
dragged.current = clamp(from + ((e.clientX - rect.left) / rect.width) * span);
|
|
98
118
|
onValueChange?.(dragged.current);
|
|
@@ -108,10 +128,13 @@ export function Waveform({
|
|
|
108
128
|
|
|
109
129
|
const moveByKey = (e: KeyboardEvent<HTMLDivElement>): void => {
|
|
110
130
|
onKeyDown?.(e);
|
|
131
|
+
|
|
111
132
|
if (e.defaultPrevented) {
|
|
112
133
|
return;
|
|
113
134
|
}
|
|
135
|
+
|
|
114
136
|
const at = value ?? 0;
|
|
137
|
+
|
|
115
138
|
const moves: Record<string, number> = {
|
|
116
139
|
ArrowLeft: at - step,
|
|
117
140
|
ArrowDown: at - step,
|
|
@@ -122,10 +145,13 @@ export function Waveform({
|
|
|
122
145
|
Home: 0,
|
|
123
146
|
End: duration
|
|
124
147
|
};
|
|
148
|
+
|
|
125
149
|
const next = moves[e.key];
|
|
150
|
+
|
|
126
151
|
if (next === undefined) {
|
|
127
152
|
return;
|
|
128
153
|
}
|
|
154
|
+
|
|
129
155
|
// The slider owns these keys; a transport listening higher up would move the playhead a second time.
|
|
130
156
|
e.preventDefault();
|
|
131
157
|
e.stopPropagation();
|
|
@@ -172,7 +198,9 @@ export function Waveform({
|
|
|
172
198
|
if (level === null) {
|
|
173
199
|
return null;
|
|
174
200
|
}
|
|
201
|
+
|
|
175
202
|
const center = from + (span * (i + 0.5)) / count;
|
|
203
|
+
|
|
176
204
|
return (
|
|
177
205
|
<rect
|
|
178
206
|
key={i}
|
|
@@ -186,6 +214,7 @@ export function Waveform({
|
|
|
186
214
|
})}
|
|
187
215
|
{marks.filter(inView).map((mark) => {
|
|
188
216
|
const x = Math.round(positionOf(mark) * width) + 0.5;
|
|
217
|
+
|
|
189
218
|
return (
|
|
190
219
|
<line key={mark} x1={x} x2={x} y1={0} y2={100} strokeWidth={1} vectorEffect="non-scaling-stroke" className="stroke-accent opacity-60" />
|
|
191
220
|
);
|
package/src/Wipe.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { splitAt, splitForKey } from './wipe-split.ts';
|
|
|
9
9
|
export interface WipeProps {
|
|
10
10
|
/* The share of the width left of the handle, from 0 to 1. */
|
|
11
11
|
value: number;
|
|
12
|
+
|
|
12
13
|
onValueChange(value: number): void;
|
|
13
14
|
before: ReactNode;
|
|
14
15
|
after: ReactNode;
|
|
@@ -18,12 +19,7 @@ export interface WipeProps {
|
|
|
18
19
|
ref?: Ref<HTMLDivElement>;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
/*
|
|
22
|
-
* Two versions of one picture in one frame: `before` left of the split and `after` right of it, with a
|
|
23
|
-
* handle between them a person drags or moves with the arrow keys. It fills the positioned element it
|
|
24
|
-
* sits in. The handle is a line with a round knob on it, or the plain line, which is dragged anywhere
|
|
25
|
-
* along its height.
|
|
26
|
-
*/
|
|
22
|
+
/* It fills the positioned element it sits in. A plain line handle is dragged anywhere along its height. */
|
|
27
23
|
export function Wipe({ value: split, onValueChange: onSplit, before, after, label, handle = 'knob', className, ref }: WipeProps) {
|
|
28
24
|
const frame = useRef<HTMLDivElement>(null);
|
|
29
25
|
const percent = `${split * 100}%`;
|
|
@@ -31,6 +27,7 @@ export function Wipe({ value: split, onValueChange: onSplit, before, after, labe
|
|
|
31
27
|
function moveTo(clientX: number): void {
|
|
32
28
|
const rect = frame.current?.getBoundingClientRect();
|
|
33
29
|
const next = rect ? splitAt(clientX, rect) : null;
|
|
30
|
+
|
|
34
31
|
if (next !== null) {
|
|
35
32
|
onSplit(next);
|
|
36
33
|
}
|
|
@@ -51,9 +48,11 @@ export function Wipe({ value: split, onValueChange: onSplit, before, after, labe
|
|
|
51
48
|
|
|
52
49
|
function onKeyDown(e: KeyboardEvent<HTMLDivElement>): void {
|
|
53
50
|
const next = splitForKey(split, e.key);
|
|
51
|
+
|
|
54
52
|
if (next === null) {
|
|
55
53
|
return;
|
|
56
54
|
}
|
|
55
|
+
|
|
57
56
|
// Only while the handle has the focus, so a canvas or a timeline around it never hears these keys.
|
|
58
57
|
e.preventDefault();
|
|
59
58
|
e.stopPropagation();
|
package/src/ZoomControls.tsx
CHANGED
|
@@ -13,8 +13,10 @@ import { activeZoomPreset, steppedZoom, ZOOM_PRESETS } from './zoom.ts';
|
|
|
13
13
|
export interface ZoomSelection {
|
|
14
14
|
label: string;
|
|
15
15
|
shortcut?: Shortcut;
|
|
16
|
+
|
|
16
17
|
/* Nothing is selected, so the row is there but says so rather than doing nothing. */
|
|
17
18
|
enabled: boolean;
|
|
19
|
+
|
|
18
20
|
onZoom(): void;
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -30,14 +32,16 @@ export interface ZoomLabels {
|
|
|
30
32
|
export interface ZoomControlsProps {
|
|
31
33
|
/* 1 is 100%. */
|
|
32
34
|
zoom: number;
|
|
35
|
+
|
|
33
36
|
onZoomChange(zoom: number): void;
|
|
34
37
|
onFitAll(): void;
|
|
35
|
-
/* The percentages in the menu under the readout. */
|
|
36
38
|
presets?: readonly number[];
|
|
37
39
|
labels?: ZoomLabels;
|
|
38
40
|
shortcuts?: { zoomReset?: Shortcut; fitAll?: Shortcut };
|
|
41
|
+
|
|
39
42
|
/* Left out by a surface with nothing to select. */
|
|
40
43
|
selection?: ZoomSelection;
|
|
44
|
+
|
|
41
45
|
className?: string;
|
|
42
46
|
ref?: Ref<HTMLDivElement>;
|
|
43
47
|
}
|
|
@@ -50,7 +54,6 @@ function MenuIcon({ icon }: { icon: LucideIcon }) {
|
|
|
50
54
|
);
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
/* The zoom group of a dock: out, the readout with its presets, in, and fit everything. */
|
|
54
57
|
export function ZoomControls({
|
|
55
58
|
zoom,
|
|
56
59
|
onZoomChange,
|
package/src/clipboard.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The clipboard, as the app uses it. A browser hands text over only to a document that is focused
|
|
3
|
-
* and allowed, and there is nothing a person can do about a refusal, so both sides stay quiet.
|
|
4
|
-
*/
|
|
1
|
+
/* A browser hands text over only to a focused, allowed document, and a person can do nothing about a refusal, so both sides stay quiet. */
|
|
5
2
|
|
|
6
|
-
/*
|
|
3
|
+
/* Never rejects. */
|
|
7
4
|
export const copyText = async (text: string): Promise<boolean> => {
|
|
8
5
|
try {
|
|
9
6
|
if (!navigator.clipboard) {
|
|
10
7
|
return false;
|
|
11
8
|
}
|
|
9
|
+
|
|
12
10
|
await navigator.clipboard.writeText(text);
|
|
11
|
+
|
|
13
12
|
return true;
|
|
14
13
|
} catch {
|
|
15
14
|
return false;
|
package/src/combobox/parts.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import { splitPlacement, type PlacementKey } from '../placement.ts';
|
|
|
12
12
|
import { renderPositioner } from '../popup-layer.ts';
|
|
13
13
|
import type { Shortcut } from '../shortcut.ts';
|
|
14
14
|
|
|
15
|
-
/*
|
|
15
|
+
/* It takes no room while it has nothing to say. */
|
|
16
16
|
const LIST_NOTE = 'text-center text-xs text-text-faint not-empty:px-3 not-empty:py-4';
|
|
17
17
|
|
|
18
18
|
/* The field keeps free text: Base UI's autocomplete, whose suggestions only offer text, rather than its combobox, which holds a value. */
|
|
@@ -20,14 +20,16 @@ export const ComboboxRoot = Autocomplete.Root;
|
|
|
20
20
|
|
|
21
21
|
export type ComboboxInputProps = Omit<ComponentProps<typeof Autocomplete.Input>, 'size' | 'className'> & {
|
|
22
22
|
className?: string;
|
|
23
|
+
|
|
23
24
|
/* The heights of `Input`. */
|
|
24
25
|
size?: 'md' | 'sm';
|
|
26
|
+
|
|
25
27
|
mono?: boolean;
|
|
28
|
+
|
|
26
29
|
/* Drawn in front of the text. `className` then goes to the box around the icon and the input; `ref` stays on the input. */
|
|
27
30
|
icon?: LucideIcon;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
|
-
/* The field the suggestions open under, drawn as the library's `Input`. */
|
|
31
33
|
export function ComboboxInput({ size = 'md', mono = false, icon, className, id, ...props }: ComboboxInputProps) {
|
|
32
34
|
const field = useFieldControl();
|
|
33
35
|
const small = size === 'sm';
|
|
@@ -61,9 +63,9 @@ type Placement = Pick<ComponentProps<typeof Autocomplete.Positioner>, PlacementK
|
|
|
61
63
|
|
|
62
64
|
export type ComboboxPopupProps = ComponentProps<typeof Autocomplete.Popup> & Placement;
|
|
63
65
|
|
|
64
|
-
/* The portal, the positioner and the menu surface of the suggestions: under the field and at least as wide as it. */
|
|
65
66
|
export function ComboboxPopup({ side = 'bottom', align = 'start', sideOffset = 6, className, ...props }: ComboboxPopupProps) {
|
|
66
67
|
const [placement, popupProps] = splitPlacement(props);
|
|
68
|
+
|
|
67
69
|
return (
|
|
68
70
|
<Autocomplete.Portal>
|
|
69
71
|
<Autocomplete.Positioner className="popup-positioner" render={renderPositioner} {...placement} side={side} align={align} sideOffset={sideOffset}>
|
|
@@ -75,28 +77,28 @@ export function ComboboxPopup({ side = 'bottom', align = 'start', sideOffset = 6
|
|
|
75
77
|
|
|
76
78
|
export function ComboboxList(props: ComponentProps<typeof Autocomplete.List>) {
|
|
77
79
|
const { t } = useTranslation('ui');
|
|
80
|
+
|
|
78
81
|
return <Autocomplete.List aria-label={t('combobox.suggestions')} {...props} />;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
export type ComboboxItemProps = ComponentProps<typeof Autocomplete.Item> & {
|
|
82
|
-
/* Before the label, such as a 14px icon or a file's mark. */
|
|
83
85
|
icon?: ReactNode;
|
|
84
|
-
|
|
86
|
+
|
|
87
|
+
/* The first to give way when the row is narrow. */
|
|
85
88
|
hint?: ReactNode;
|
|
86
|
-
|
|
89
|
+
|
|
87
90
|
description?: ReactNode;
|
|
88
|
-
|
|
91
|
+
|
|
92
|
+
/* Before the shortcut. */
|
|
89
93
|
trailing?: ReactNode;
|
|
94
|
+
|
|
90
95
|
shortcut?: Shortcut;
|
|
91
|
-
|
|
96
|
+
|
|
97
|
+
/* Keeps only what makes it an option, for a row that lays out itself. */
|
|
92
98
|
unstyled?: boolean;
|
|
93
99
|
};
|
|
94
100
|
|
|
95
|
-
/*
|
|
96
|
-
* One suggestion. Its children are the label. `onClick` runs for a click and for Enter on the
|
|
97
|
-
* highlighted row; Base UI clicks the row for Enter, so a modifier held with Enter is read in the
|
|
98
|
-
* field's `onKeyDown` instead.
|
|
99
|
-
*/
|
|
101
|
+
/* Base UI clicks the row for Enter, so a modifier held with Enter is read in the field's `onKeyDown` instead. */
|
|
100
102
|
export function ComboboxItem({ icon, hint, description, trailing, shortcut, unstyled = false, className, children, ...props }: ComboboxItemProps) {
|
|
101
103
|
if (unstyled) {
|
|
102
104
|
return (
|
|
@@ -105,7 +107,9 @@ export function ComboboxItem({ icon, hint, description, trailing, shortcut, unst
|
|
|
105
107
|
</Autocomplete.Item>
|
|
106
108
|
);
|
|
107
109
|
}
|
|
110
|
+
|
|
108
111
|
const label = <span className="min-w-0 truncate">{children}</span>;
|
|
112
|
+
|
|
109
113
|
return (
|
|
110
114
|
<Autocomplete.Item className={withClass(clsx('menu-item', description !== undefined && 'items-start'), className)} {...props}>
|
|
111
115
|
{icon !== undefined && <span className="flex h-lh shrink-0 items-center text-text-faint">{icon}</span>}
|
|
@@ -141,7 +145,7 @@ export function ComboboxEmpty({ className, ...props }: ComponentProps<typeof Aut
|
|
|
141
145
|
return <Autocomplete.Empty className={withClass(LIST_NOTE, className)} {...props} />;
|
|
142
146
|
}
|
|
143
147
|
|
|
144
|
-
/*
|
|
148
|
+
/* Read out as it changes. It stays mounted; only its children come and go. */
|
|
145
149
|
export function ComboboxStatus({ className, ...props }: ComponentProps<typeof Autocomplete.Status>) {
|
|
146
150
|
return <Autocomplete.Status className={withClass(LIST_NOTE, className)} {...props} />;
|
|
147
151
|
}
|
|
@@ -3,8 +3,10 @@ import { createContext, useContext, type RefObject } from 'react';
|
|
|
3
3
|
interface PaletteRefs {
|
|
4
4
|
/* For the popup to put the focus in when it opens: a control before it would take it otherwise. */
|
|
5
5
|
input: RefObject<HTMLInputElement | null>;
|
|
6
|
+
|
|
6
7
|
/* For the root to scroll back to the top when the app sets the query. */
|
|
7
8
|
list: RefObject<HTMLDivElement | null>;
|
|
9
|
+
|
|
8
10
|
highlight(row: Element): void;
|
|
9
11
|
virtualScroll: RefObject<((index: number, reason: string) => void) | null>;
|
|
10
12
|
}
|
|
@@ -33,11 +33,7 @@ type DialogProps = Pick<ComponentProps<typeof DialogRoot>, 'open' | 'defaultOpen
|
|
|
33
33
|
|
|
34
34
|
export type CommandPaletteRootProps<ItemValue> = AutocompleteProps<ItemValue> & DialogProps;
|
|
35
35
|
|
|
36
|
-
/*
|
|
37
|
-
* A dialog around a list that is always open. `open` and its handlers are the dialog's; the rest
|
|
38
|
-
* is Base UI's autocomplete, so the query, the items and their filter work as in a combobox. The
|
|
39
|
-
* first row is highlighted and stays so while the pointer leaves the list, which is what Enter runs.
|
|
40
|
-
*/
|
|
36
|
+
/* `open` and its handlers are the dialog's; the rest is Base UI's autocomplete. The first row stays highlighted while the pointer leaves the list, which is what Enter runs. */
|
|
41
37
|
export function CommandPaletteRoot<ItemValue>({
|
|
42
38
|
open,
|
|
43
39
|
defaultOpen,
|
|
@@ -59,8 +55,7 @@ export function CommandPaletteRoot<ItemValue>({
|
|
|
59
55
|
const [focusHighlight, setFocusHighlight] = useState(false);
|
|
60
56
|
/*
|
|
61
57
|
* Base UI moves the highlight to the first row for a typed query only. For a query the app sets,
|
|
62
|
-
* the list is empty for one commit
|
|
63
|
-
* puts it on the first row of what comes back.
|
|
58
|
+
* the list is empty for one unpainted commit, so Base UI highlights the first row of what comes back.
|
|
64
59
|
*/
|
|
65
60
|
const [settledValue, setSettledValue] = useState(value);
|
|
66
61
|
const resetting = value !== undefined && value !== settledValue;
|
|
@@ -69,9 +64,11 @@ export function CommandPaletteRoot<ItemValue>({
|
|
|
69
64
|
if (!resetting) {
|
|
70
65
|
return;
|
|
71
66
|
}
|
|
67
|
+
|
|
72
68
|
if (listRef.current !== null) {
|
|
73
69
|
listRef.current.scrollTop = 0;
|
|
74
70
|
}
|
|
71
|
+
|
|
75
72
|
// The rows may come back only after Base UI has seen the empty list in a commit of its own.
|
|
76
73
|
// oxlint-disable-next-line react/set-state-in-effect
|
|
77
74
|
setSettledValue(value);
|
|
@@ -81,11 +78,13 @@ export function CommandPaletteRoot<ItemValue>({
|
|
|
81
78
|
if (row.hasAttribute('data-highlighted')) {
|
|
82
79
|
return;
|
|
83
80
|
}
|
|
81
|
+
|
|
84
82
|
// Base UI has no imperative highlight action and disables highlighting on item focus.
|
|
85
83
|
// Enable pointer navigation just for this event when the app has disabled hover highlighting.
|
|
86
84
|
if (!highlightItemOnHover) {
|
|
87
85
|
flushSync(() => setFocusHighlight(true));
|
|
88
86
|
}
|
|
87
|
+
|
|
89
88
|
try {
|
|
90
89
|
row.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, movementX: 1 }));
|
|
91
90
|
} finally {
|
|
@@ -106,6 +105,7 @@ export function CommandPaletteRoot<ItemValue>({
|
|
|
106
105
|
if (details.reason === 'input-change') {
|
|
107
106
|
setSettledValue(next);
|
|
108
107
|
}
|
|
108
|
+
|
|
109
109
|
onValueChange?.(next, details);
|
|
110
110
|
}}
|
|
111
111
|
items={(resetting ? NO_ITEMS : items) as readonly ItemValue[] | undefined}
|
|
@@ -113,6 +113,7 @@ export function CommandPaletteRoot<ItemValue>({
|
|
|
113
113
|
if (item !== undefined && virtualScroll.current !== null) {
|
|
114
114
|
queueMicrotask(() => virtualScroll.current?.(details.index, details.reason));
|
|
115
115
|
}
|
|
116
|
+
|
|
116
117
|
onItemHighlighted?.(item, details);
|
|
117
118
|
}}
|
|
118
119
|
inline
|
|
@@ -132,13 +133,11 @@ export type CommandPalettePopupProps = Omit<DialogPopupProps, 'size'> & {
|
|
|
132
133
|
size?: keyof typeof SIZE;
|
|
133
134
|
};
|
|
134
135
|
|
|
135
|
-
/*
|
|
136
|
-
* The dialog's surface. It hangs from a fixed line near the top instead of the middle: the list
|
|
137
|
-
* changes height with every key, and a centered popup would walk up and down the screen.
|
|
138
|
-
*/
|
|
136
|
+
/* It hangs from a fixed line near the top: the list changes height with every key, and a centered popup would walk up and down. */
|
|
139
137
|
export function CommandPalettePopup({ size = 'md', initialFocus, className, ...props }: CommandPalettePopupProps) {
|
|
140
138
|
const { t } = useTranslation('ui');
|
|
141
139
|
const refs = useContext(PaletteRefsContext);
|
|
140
|
+
|
|
142
141
|
return (
|
|
143
142
|
<DialogPopup
|
|
144
143
|
aria-label={t('palette.label')}
|
|
@@ -152,19 +151,24 @@ export function CommandPalettePopup({ size = 'md', initialFocus, className, ...p
|
|
|
152
151
|
export type CommandPaletteInputProps = Omit<ComponentProps<typeof Autocomplete.Input>, 'className'> & {
|
|
153
152
|
/* In front of the text instead of the magnifier: the mark of a mode, a button back. */
|
|
154
153
|
leading?: ReactNode;
|
|
154
|
+
|
|
155
155
|
/* After the text, before the Escape hint: toggles of a search, a button that uses the text. */
|
|
156
156
|
trailing?: ReactNode;
|
|
157
|
+
|
|
157
158
|
/* The Escape key at the end of the field. Off where Escape does something else first. */
|
|
158
159
|
escapeHint?: boolean;
|
|
160
|
+
|
|
159
161
|
/* A pattern or a path read back letter by letter. */
|
|
160
162
|
mono?: boolean;
|
|
163
|
+
|
|
161
164
|
/* Goes to the row around the field; `ref` stays on the input. */
|
|
162
165
|
className?: string;
|
|
163
166
|
};
|
|
164
167
|
|
|
165
|
-
/*
|
|
168
|
+
/* It keeps the keys while the arrows move the highlight; a control beside it should not take the focus on a click. */
|
|
166
169
|
export function CommandPaletteInput({ leading, trailing, escapeHint = true, mono = false, className, ref, ...props }: CommandPaletteInputProps) {
|
|
167
170
|
const refs = useContext(PaletteRefsContext);
|
|
171
|
+
|
|
168
172
|
return (
|
|
169
173
|
<div className={clsx('relative flex shrink-0 items-center gap-2 border-b border-border px-3', className)}>
|
|
170
174
|
{leading ?? <Icon icon={Search} size={14} className="shrink-0 text-text-faint" />}
|
|
@@ -180,10 +184,11 @@ export function CommandPaletteInput({ leading, trailing, escapeHint = true, mono
|
|
|
180
184
|
);
|
|
181
185
|
}
|
|
182
186
|
|
|
183
|
-
/*
|
|
187
|
+
/* It scrolls on its own, so the field and the footer stay put. */
|
|
184
188
|
export function CommandPaletteList({ className, ref, onFocus, ...props }: ComponentProps<typeof Autocomplete.List>) {
|
|
185
189
|
const { t } = useTranslation('ui');
|
|
186
190
|
const refs = useContext(PaletteRefsContext);
|
|
191
|
+
|
|
187
192
|
return (
|
|
188
193
|
<Autocomplete.List
|
|
189
194
|
ref={mergeRefs(ref, refs?.list)}
|
|
@@ -193,9 +198,11 @@ export function CommandPaletteList({ className, ref, onFocus, ...props }: Compon
|
|
|
193
198
|
onFocus={(event) => {
|
|
194
199
|
onFocus?.(event);
|
|
195
200
|
const row = event.target.closest('[role="option"]');
|
|
201
|
+
|
|
196
202
|
if (row === null || !event.currentTarget.contains(row) || refs?.input.current == null) {
|
|
197
203
|
return;
|
|
198
204
|
}
|
|
205
|
+
|
|
199
206
|
refs.highlight(row);
|
|
200
207
|
refs.input.current.focus({ preventScroll: true });
|
|
201
208
|
}}
|
|
@@ -205,7 +212,6 @@ export function CommandPaletteList({ className, ref, onFocus, ...props }: Compon
|
|
|
205
212
|
|
|
206
213
|
export type CommandPaletteFooterProps = useRender.ComponentProps<'div'>;
|
|
207
214
|
|
|
208
|
-
/* Under the list: the keys that work here, a count, an action that leaves the palette. */
|
|
209
215
|
export function CommandPaletteFooter({ render, className, ref, ...props }: CommandPaletteFooterProps) {
|
|
210
216
|
return useRender({
|
|
211
217
|
render,
|