@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/locales.ts
CHANGED
|
@@ -3,16 +3,12 @@ import type { i18n as I18n } from 'i18next';
|
|
|
3
3
|
import en from './locales/en.ts';
|
|
4
4
|
import nl from './locales/nl.ts';
|
|
5
5
|
|
|
6
|
-
/* The words of this library live in a namespace of their own, which the app adds to its i18next beside its own namespaces. */
|
|
7
6
|
export const UI_NAMESPACE = 'ui';
|
|
8
7
|
|
|
9
|
-
/*
|
|
10
|
-
* Every language the library speaks, by language code. Small enough to ship whole, which is what lets
|
|
11
|
-
* `addUiResources` run synchronously before the first render, so no word ever shows as its key.
|
|
12
|
-
*/
|
|
8
|
+
/* Small enough to ship whole, so `addUiResources` runs before the first render and no word shows as its key. */
|
|
13
9
|
export const UI_RESOURCES: Readonly<Record<string, Record<string, unknown>>> = { en, nl };
|
|
14
10
|
|
|
15
|
-
/*
|
|
11
|
+
/* A language the app already filled keeps its words. */
|
|
16
12
|
export const addUiResources = (i18n: I18n): void => {
|
|
17
13
|
for (const [language, words] of Object.entries(UI_RESOURCES)) {
|
|
18
14
|
if (!i18n.hasResourceBundle(language, UI_NAMESPACE)) {
|
package/src/match.ts
CHANGED
|
@@ -3,24 +3,31 @@ const WORD_START = /[\s\-_./:]/;
|
|
|
3
3
|
/* Whether `query` starts the text or one of its words. */
|
|
4
4
|
const startsWord = (text: string, query: string): boolean => {
|
|
5
5
|
let at = text.indexOf(query);
|
|
6
|
+
|
|
6
7
|
while (at !== -1) {
|
|
7
8
|
if (at === 0 || WORD_START.test(text[at - 1]!)) {
|
|
8
9
|
return true;
|
|
9
10
|
}
|
|
11
|
+
|
|
10
12
|
at = text.indexOf(query, at + 1);
|
|
11
13
|
}
|
|
14
|
+
|
|
12
15
|
return false;
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
const lettersInOrder = (text: string, query: string): boolean => {
|
|
16
19
|
let at = 0;
|
|
20
|
+
|
|
17
21
|
for (const letter of query.replace(/\s+/g, '')) {
|
|
18
22
|
at = text.indexOf(letter, at);
|
|
23
|
+
|
|
19
24
|
if (at === -1) {
|
|
20
25
|
return false;
|
|
21
26
|
}
|
|
27
|
+
|
|
22
28
|
at += 1;
|
|
23
29
|
}
|
|
30
|
+
|
|
24
31
|
return true;
|
|
25
32
|
};
|
|
26
33
|
|
|
@@ -32,19 +39,26 @@ const lettersInOrder = (text: string, query: string): boolean => {
|
|
|
32
39
|
*/
|
|
33
40
|
export function matchScore(text: string, query: string): number | null {
|
|
34
41
|
const wanted = query.trim().toLowerCase();
|
|
42
|
+
|
|
35
43
|
if (wanted === '') {
|
|
36
44
|
return 0;
|
|
37
45
|
}
|
|
46
|
+
|
|
38
47
|
const haystack = text.toLowerCase();
|
|
48
|
+
|
|
39
49
|
if (haystack.startsWith(wanted)) {
|
|
40
50
|
return 5;
|
|
41
51
|
}
|
|
52
|
+
|
|
42
53
|
if (startsWord(haystack, wanted)) {
|
|
43
54
|
return 4;
|
|
44
55
|
}
|
|
56
|
+
|
|
45
57
|
const words = wanted.split(/\s+/);
|
|
58
|
+
|
|
46
59
|
if (words.every((word) => haystack.includes(word))) {
|
|
47
60
|
return haystack.startsWith(words[0]!) ? 3 : 2;
|
|
48
61
|
}
|
|
62
|
+
|
|
49
63
|
return lettersInOrder(haystack, wanted) ? 1 : null;
|
|
50
64
|
}
|
package/src/menu/parts.tsx
CHANGED
|
@@ -20,25 +20,20 @@ type PositionerProps = ComponentProps<typeof BaseMenu.Positioner>;
|
|
|
20
20
|
|
|
21
21
|
type Placement = Pick<PositionerProps, PlacementKey>;
|
|
22
22
|
|
|
23
|
-
/*
|
|
24
|
-
* A menu hangs under its trigger, lined up with its start, unless there is no room. A context menu
|
|
25
|
-
* opens at the pointer, which its positioner already knows. A submenu sits beside its row, pulled
|
|
26
|
-
* up by the popup's own padding so its first row lines up with the row that opened it.
|
|
27
|
-
*/
|
|
23
|
+
/* A context menu opens at the pointer, which its positioner already knows. A submenu is pulled up by the popup's padding so its first row lines up with the row that opened it. */
|
|
28
24
|
const PLACEMENT: Record<MenuKind, Placement> = {
|
|
29
25
|
menu: { side: 'bottom', align: 'start', sideOffset: 6 },
|
|
30
26
|
context: {},
|
|
31
27
|
submenu: { sideOffset: 4, alignOffset: -4 }
|
|
32
28
|
};
|
|
33
29
|
|
|
34
|
-
/*
|
|
30
|
+
/* It takes the line height of the row it sits in. */
|
|
35
31
|
export const MENU_LABEL = 'px-2.5 pt-1.5 pb-0.5 text-xs/[inherit] text-text-faint';
|
|
36
32
|
|
|
37
33
|
/* A trailing hint in a row: what the item does to something else, never a shortcut. A shortcut stays a `Kbd`, which `.menu-item` already pushes to the right. */
|
|
38
34
|
const MENU_HINT = 'ml-auto pl-3 text-xs/[inherit] text-text-faint';
|
|
39
35
|
|
|
40
|
-
/* The
|
|
41
|
-
negative margin buys back from its 4px of padding, and it is softer than a border a surface ends with. */
|
|
36
|
+
/* The negative margin buys back the popup's 4px of padding, so the line runs its whole width. */
|
|
42
37
|
const MENU_SEPARATOR = '-mx-1 my-1 h-px bg-border-soft';
|
|
43
38
|
|
|
44
39
|
export function MenuRoot(props: ComponentProps<typeof BaseMenu.Root>) {
|
|
@@ -71,14 +66,10 @@ export const ContextMenuTrigger = BaseContextMenu.Trigger;
|
|
|
71
66
|
|
|
72
67
|
export type MenuPopupProps = ComponentProps<typeof BaseMenu.Popup> & Placement;
|
|
73
68
|
|
|
74
|
-
/*
|
|
75
|
-
* The shell every menu is drawn in: the portal, the layer it sits on and the gap to its trigger are
|
|
76
|
-
* decided once. The popup scrolls inside the room the window leaves it; `className` adds a minimum
|
|
77
|
-
* width or a layout of its own, such as a grid of swatches.
|
|
78
|
-
*/
|
|
79
69
|
export function MenuPopup({ className, ...props }: MenuPopupProps) {
|
|
80
70
|
const defaults = PLACEMENT[useContext(MenuKindContext)];
|
|
81
71
|
const [placement, popupProps] = splitPlacement(props);
|
|
72
|
+
|
|
82
73
|
return (
|
|
83
74
|
<BaseMenu.Portal>
|
|
84
75
|
<BaseMenu.Positioner
|
|
@@ -97,11 +88,7 @@ export function MenuPopup({ className, ...props }: MenuPopupProps) {
|
|
|
97
88
|
}
|
|
98
89
|
|
|
99
90
|
type ItemLook = {
|
|
100
|
-
/*
|
|
101
|
-
* Leaves the row out: no padding, highlight or indicator, only what makes it an item (the arrow
|
|
102
|
-
* keys, typeahead, closing the menu). For an icon button, a swatch, a link or a segment inside a
|
|
103
|
-
* menu, which draws itself and its own highlight.
|
|
104
|
-
*/
|
|
91
|
+
/* Keeps only the item's behavior, for a swatch, a link or a segment that draws itself and its own highlight. */
|
|
105
92
|
unstyled?: boolean;
|
|
106
93
|
};
|
|
107
94
|
|
|
@@ -114,19 +101,18 @@ export function MenuItem({ unstyled = false, className, ...props }: MenuItemProp
|
|
|
114
101
|
export interface MenuCheckProps {
|
|
115
102
|
/* One of several is a bare tick; on or off is a tick in an outlined box, which says the row can be off. */
|
|
116
103
|
kind: 'radio' | 'checkbox';
|
|
104
|
+
|
|
117
105
|
/* Only for a row that is not a radio or checkbox item, and so has no indicator of its own. */
|
|
118
106
|
checked?: boolean;
|
|
107
|
+
|
|
119
108
|
className?: string;
|
|
120
109
|
ref?: Ref<HTMLSpanElement>;
|
|
121
110
|
}
|
|
122
111
|
|
|
123
|
-
/*
|
|
124
|
-
* The slot before the label of a row that can be checked, in a menu or a list that reads as one. It
|
|
125
|
-
* is one line of the row tall, so a row with a second line keeps the tick beside its first. Radio and
|
|
126
|
-
* checkbox items draw it themselves; a plain row that shows a state hands it `checked`.
|
|
127
|
-
*/
|
|
112
|
+
/* One line of the row tall, so a row with a second line keeps the tick beside its first. */
|
|
128
113
|
export function MenuCheck({ kind, checked, className, ref }: MenuCheckProps) {
|
|
129
114
|
const tick = <Icon icon={Check} size={kind === 'radio' ? 14 : 12} />;
|
|
115
|
+
|
|
130
116
|
const indicator =
|
|
131
117
|
checked !== undefined ? (
|
|
132
118
|
checked && tick
|
|
@@ -135,6 +121,7 @@ export function MenuCheck({ kind, checked, className, ref }: MenuCheckProps) {
|
|
|
135
121
|
) : (
|
|
136
122
|
<BaseMenu.CheckboxItemIndicator className="flex">{tick}</BaseMenu.CheckboxItemIndicator>
|
|
137
123
|
);
|
|
124
|
+
|
|
138
125
|
return (
|
|
139
126
|
<span ref={ref} className={clsx('grid h-lh w-4 shrink-0 place-items-center', className)}>
|
|
140
127
|
{kind === 'checkbox' ? <span className="grid h-4 w-4 place-items-center rounded border border-border-strong">{indicator}</span> : indicator}
|
|
@@ -149,20 +136,21 @@ type Indicator = {
|
|
|
149
136
|
|
|
150
137
|
type MenuCheckboxItemProps = ComponentProps<typeof BaseMenu.CheckboxItem> & ItemLook & Indicator & { children?: ReactNode };
|
|
151
138
|
|
|
152
|
-
|
|
139
|
+
const checkRow = (kind: MenuCheckProps['kind'], unstyled: boolean, indicator: Indicator['indicator'], children: ReactNode) =>
|
|
140
|
+
unstyled ? (
|
|
141
|
+
children
|
|
142
|
+
) : (
|
|
143
|
+
<>
|
|
144
|
+
{indicator === 'start' && <MenuCheck kind={kind} />}
|
|
145
|
+
{children}
|
|
146
|
+
{indicator === 'end' && <MenuCheck kind={kind} className="ml-auto" />}
|
|
147
|
+
</>
|
|
148
|
+
);
|
|
149
|
+
|
|
153
150
|
export function MenuCheckboxItem({ unstyled = false, indicator = 'start', className, children, ...props }: MenuCheckboxItemProps) {
|
|
154
|
-
if (unstyled) {
|
|
155
|
-
return (
|
|
156
|
-
<BaseMenu.CheckboxItem className={className} {...props}>
|
|
157
|
-
{children}
|
|
158
|
-
</BaseMenu.CheckboxItem>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
151
|
return (
|
|
162
|
-
<BaseMenu.CheckboxItem className={withClass('menu-item', className)} {...props}>
|
|
163
|
-
{
|
|
164
|
-
{children}
|
|
165
|
-
{indicator === 'end' && <MenuCheck kind="checkbox" className="ml-auto" />}
|
|
152
|
+
<BaseMenu.CheckboxItem className={unstyled ? className : withClass('menu-item', className)} {...props}>
|
|
153
|
+
{checkRow('checkbox', unstyled, indicator, children)}
|
|
166
154
|
</BaseMenu.CheckboxItem>
|
|
167
155
|
);
|
|
168
156
|
}
|
|
@@ -171,26 +159,17 @@ export const MenuRadioGroup = BaseMenu.RadioGroup;
|
|
|
171
159
|
|
|
172
160
|
type MenuRadioItemProps = ComponentProps<typeof BaseMenu.RadioItem> & ItemLook & Indicator & { children?: ReactNode };
|
|
173
161
|
|
|
174
|
-
/* One of several. The tick before its label is part of it. */
|
|
175
162
|
export function MenuRadioItem({ unstyled = false, indicator = 'start', className, children, ...props }: MenuRadioItemProps) {
|
|
176
|
-
if (unstyled) {
|
|
177
|
-
return (
|
|
178
|
-
<BaseMenu.RadioItem className={className} {...props}>
|
|
179
|
-
{children}
|
|
180
|
-
</BaseMenu.RadioItem>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
163
|
return (
|
|
184
|
-
<BaseMenu.RadioItem className={withClass('menu-item', className)} {...props}>
|
|
185
|
-
{
|
|
186
|
-
{children}
|
|
187
|
-
{indicator === 'end' && <MenuCheck kind="radio" className="ml-auto" />}
|
|
164
|
+
<BaseMenu.RadioItem className={unstyled ? className : withClass('menu-item', className)} {...props}>
|
|
165
|
+
{checkRow('radio', unstyled, indicator, children)}
|
|
188
166
|
</BaseMenu.RadioItem>
|
|
189
167
|
);
|
|
190
168
|
}
|
|
191
169
|
|
|
192
170
|
type SubmenuTriggerProps = ComponentProps<typeof BaseMenu.SubmenuTrigger> & {
|
|
193
171
|
children?: ReactNode;
|
|
172
|
+
|
|
194
173
|
/* The chevron at the end of the row that says it opens more. Off for a row that carries a hint there instead. */
|
|
195
174
|
chevron?: boolean;
|
|
196
175
|
};
|
|
@@ -206,18 +185,17 @@ export function MenuSubmenuTrigger({ className, children, chevron = true, ...pro
|
|
|
206
185
|
|
|
207
186
|
export type MenuRowProps = useRender.ComponentProps<'div'>;
|
|
208
187
|
|
|
209
|
-
/*
|
|
210
|
-
* A row with buttons at its end that reads as one row. Its first item is the row, every `RowAction`
|
|
211
|
-
* and `RowSubmenuTrigger` after it an item of its own, so the arrow keys reach each.
|
|
212
|
-
*/
|
|
188
|
+
/* Its first item is the row, every `RowAction` and `RowSubmenuTrigger` after it an item of its own, so the arrow keys reach each. */
|
|
213
189
|
export function MenuRow({ render, className, ref, ...props }: MenuRowProps) {
|
|
214
190
|
return useRender({ render, ref, defaultTagName: 'div', props: { role: 'group', ...props, className: clsx('menu-row', className) } });
|
|
215
191
|
}
|
|
216
192
|
|
|
217
193
|
type RowButton = {
|
|
218
194
|
icon: LucideIcon;
|
|
195
|
+
|
|
219
196
|
/* The button's accessible name. Unlike the `label` of an item, typing never lands on the button. */
|
|
220
197
|
label: string;
|
|
198
|
+
|
|
221
199
|
/* Classes for the icon rather than the button, such as a fill. The size stays the row's. */
|
|
222
200
|
iconClassName?: string;
|
|
223
201
|
};
|
|
@@ -237,10 +215,7 @@ export function MenuRowAction({ icon, label, iconClassName, className, ...props
|
|
|
237
215
|
|
|
238
216
|
export type MenuRowSubmenuTriggerProps = Omit<ComponentProps<typeof BaseMenu.SubmenuTrigger>, 'children' | 'label'> & RowButton;
|
|
239
217
|
|
|
240
|
-
/*
|
|
241
|
-
* A button in a `Menu.Row` that opens a submenu, inside a `Menu.SubmenuRoot`. It has no tooltip: the
|
|
242
|
-
* submenu opens on the same hover and says what the button holds.
|
|
243
|
-
*/
|
|
218
|
+
/* No tooltip: the submenu opens on the same hover and says what the button holds. */
|
|
244
219
|
export function MenuRowSubmenuTrigger({ icon, label, iconClassName, className, ...props }: MenuRowSubmenuTriggerProps) {
|
|
245
220
|
return (
|
|
246
221
|
<BaseMenu.SubmenuTrigger className={withClass('menu-item menu-row-action', className)} aria-label={label} {...props}>
|
package/src/middle-truncate.ts
CHANGED
|
@@ -1,41 +1,50 @@
|
|
|
1
1
|
export interface MiddleCut {
|
|
2
2
|
head: string;
|
|
3
|
+
|
|
3
4
|
/* What the ellipsis stands for; still in the text, so a copy or a find reads it. */
|
|
4
5
|
middle: string;
|
|
6
|
+
|
|
5
7
|
tail: string;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export const MIDDLE_ELLIPSIS = '…';
|
|
9
11
|
|
|
10
12
|
/*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* what tells two of them apart. Code points, never UTF-16 units, so a cut never halves a character.
|
|
13
|
+
* Null when the whole text fits. The tail gets the odd character, since the end of a path is what tells
|
|
14
|
+
* two of them apart. Code points, never UTF-16 units, so a cut never halves a character.
|
|
14
15
|
*/
|
|
15
16
|
export function fitMiddle(text: string, width: number, measure: (text: string) => number): MiddleCut | null {
|
|
16
17
|
if (measure(text) <= width) {
|
|
17
18
|
return null;
|
|
18
19
|
}
|
|
20
|
+
|
|
19
21
|
const characters = Array.from(text);
|
|
22
|
+
|
|
20
23
|
const cutAt = (kept: number): MiddleCut => {
|
|
21
24
|
const headLength = Math.floor(kept / 2);
|
|
22
25
|
const tailStart = characters.length - (kept - headLength);
|
|
26
|
+
|
|
23
27
|
return {
|
|
24
28
|
head: characters.slice(0, headLength).join(''),
|
|
25
29
|
middle: characters.slice(headLength, tailStart).join(''),
|
|
26
30
|
tail: characters.slice(tailStart).join('')
|
|
27
31
|
};
|
|
28
32
|
};
|
|
33
|
+
|
|
29
34
|
const fits = (cut: MiddleCut): boolean => measure(cut.head + MIDDLE_ELLIPSIS + cut.tail) <= width;
|
|
35
|
+
|
|
30
36
|
let low = 0;
|
|
31
37
|
let high = characters.length - 1;
|
|
38
|
+
|
|
32
39
|
while (low < high) {
|
|
33
40
|
const kept = Math.ceil((low + high) / 2);
|
|
41
|
+
|
|
34
42
|
if (fits(cutAt(kept))) {
|
|
35
43
|
low = kept;
|
|
36
44
|
} else {
|
|
37
45
|
high = kept - 1;
|
|
38
46
|
}
|
|
39
47
|
}
|
|
48
|
+
|
|
40
49
|
return cutAt(low);
|
|
41
50
|
}
|
package/src/modality.ts
CHANGED
|
@@ -7,6 +7,7 @@ const set = (modality: Modality): void => {
|
|
|
7
7
|
if (current === modality) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
|
+
|
|
10
11
|
current = modality;
|
|
11
12
|
document.documentElement.dataset.modality = modality;
|
|
12
13
|
};
|
|
@@ -19,8 +20,10 @@ export function startInputModality(): void {
|
|
|
19
20
|
if (started) {
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
23
|
+
|
|
22
24
|
started = true;
|
|
23
25
|
set('pointer');
|
|
26
|
+
|
|
24
27
|
window.addEventListener(
|
|
25
28
|
'keydown',
|
|
26
29
|
(event) => {
|
|
@@ -31,6 +34,7 @@ export function startInputModality(): void {
|
|
|
31
34
|
},
|
|
32
35
|
{ capture: true }
|
|
33
36
|
);
|
|
37
|
+
|
|
34
38
|
window.addEventListener('pointerdown', () => set('pointer'), { capture: true, passive: true });
|
|
35
39
|
window.addEventListener('pointermove', () => set('pointer'), { capture: true, passive: true });
|
|
36
40
|
}
|
package/src/placement.ts
CHANGED
|
@@ -14,13 +14,14 @@ const PLACEMENT_KEYS = [
|
|
|
14
14
|
|
|
15
15
|
export type PlacementKey = (typeof PLACEMENT_KEYS)[number];
|
|
16
16
|
|
|
17
|
-
/* Splits a popup part's props into what goes to the positioner and what goes to the popup. */
|
|
18
17
|
export function splitPlacement<Props extends Partial<Record<PlacementKey, unknown>>>(props: Props): [Pick<Props, PlacementKey>, Omit<Props, PlacementKey>] {
|
|
19
18
|
const placement: Record<string, unknown> = {};
|
|
20
19
|
const rest: Record<string, unknown> = { ...props };
|
|
20
|
+
|
|
21
21
|
for (const key of PLACEMENT_KEYS) {
|
|
22
22
|
placement[key] = props[key];
|
|
23
23
|
delete rest[key];
|
|
24
24
|
}
|
|
25
|
+
|
|
25
26
|
return [placement as Pick<Props, PlacementKey>, rest as Omit<Props, PlacementKey>];
|
|
26
27
|
}
|
package/src/platform.ts
CHANGED
|
@@ -3,7 +3,9 @@ export const isApplePlatform = (): boolean => {
|
|
|
3
3
|
if (typeof navigator === 'undefined') {
|
|
4
4
|
return false;
|
|
5
5
|
}
|
|
6
|
+
|
|
6
7
|
const hints = (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData;
|
|
8
|
+
|
|
7
9
|
return applePlatformFrom(hints?.platform || navigator.platform);
|
|
8
10
|
};
|
|
9
11
|
|
package/src/popover/parts.tsx
CHANGED
|
@@ -5,10 +5,7 @@ import { withClass } from '../class-name.ts';
|
|
|
5
5
|
import { splitPlacement, type PlacementKey } from '../placement.ts';
|
|
6
6
|
import { renderPositioner } from '../popup-layer.ts';
|
|
7
7
|
|
|
8
|
-
/*
|
|
9
|
-
* The surface a popup is drawn on. `menu` is the padded card menus use, `picker` a card whose
|
|
10
|
-
* content runs to its edges (a search field on top of a list), and `plain` leaves the surface to the caller.
|
|
11
|
-
*/
|
|
8
|
+
/* `picker` is a card whose content runs to its edges, `plain` leaves the surface to the caller. */
|
|
12
9
|
const POPUP_VARIANT = {
|
|
13
10
|
menu: 'menu-popup',
|
|
14
11
|
picker: 'picker-popup',
|
|
@@ -34,9 +31,9 @@ export type PopoverPopupProps = ComponentProps<typeof BasePopover.Popup> &
|
|
|
34
31
|
variant?: PopupVariant;
|
|
35
32
|
};
|
|
36
33
|
|
|
37
|
-
/* The portal, the positioner and the popup of a popover: under its trigger and lined up with its start unless it says otherwise. */
|
|
38
34
|
export function PopoverPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8, className, ...props }: PopoverPopupProps) {
|
|
39
35
|
const [placement, popupProps] = splitPlacement(props);
|
|
36
|
+
|
|
40
37
|
return (
|
|
41
38
|
<BasePopover.Portal>
|
|
42
39
|
<BasePopover.Positioner className="popup-positioner" render={renderPositioner} {...placement} side={side} align={align} sideOffset={sideOffset}>
|
|
@@ -58,6 +55,7 @@ export type PreviewCardPopupProps = ComponentProps<typeof BasePreviewCard.Popup>
|
|
|
58
55
|
/* A card that opens while the pointer rests on its trigger, drawn like a popover. */
|
|
59
56
|
export function PreviewCardPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8, className, ...props }: PreviewCardPopupProps) {
|
|
60
57
|
const [placement, popupProps] = splitPlacement(props);
|
|
58
|
+
|
|
61
59
|
return (
|
|
62
60
|
<BasePreviewCard.Portal>
|
|
63
61
|
<BasePreviewCard.Positioner className="z-(--z-popup)" {...placement} side={side} align={align} sideOffset={sideOffset}>
|
package/src/popup-layer.ts
CHANGED
|
@@ -4,20 +4,22 @@ import { createElement, useLayoutEffect, type ComponentProps } from 'react';
|
|
|
4
4
|
const open = new Set<symbol>();
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* browser restyle the whole page on every DOM insertion.
|
|
7
|
+
* The theme reads `data-popup-open` to turn the window's drag regions off. An attribute instead of
|
|
8
|
+
* `:root:has(...)`, which made the browser restyle the whole page on every DOM insertion.
|
|
10
9
|
*/
|
|
11
10
|
function usePopupLayer(isOpen: boolean): void {
|
|
12
11
|
useLayoutEffect(() => {
|
|
13
12
|
if (!isOpen) {
|
|
14
13
|
return;
|
|
15
14
|
}
|
|
15
|
+
|
|
16
16
|
const self = Symbol('popup');
|
|
17
17
|
open.add(self);
|
|
18
18
|
document.documentElement.setAttribute('data-popup-open', '');
|
|
19
|
+
|
|
19
20
|
return () => {
|
|
20
21
|
open.delete(self);
|
|
22
|
+
|
|
21
23
|
if (open.size === 0) {
|
|
22
24
|
document.documentElement.removeAttribute('data-popup-open');
|
|
23
25
|
}
|
|
@@ -27,8 +29,8 @@ function usePopupLayer(isOpen: boolean): void {
|
|
|
27
29
|
|
|
28
30
|
function LayerPositioner({ open: isOpen, ...props }: ComponentProps<'div'> & { readonly open: boolean }) {
|
|
29
31
|
usePopupLayer(isOpen);
|
|
32
|
+
|
|
30
33
|
return createElement('div', props);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
/* The `render` of a popup's positioner, which knows whether the popup is open and so marks the root for as long as it is. */
|
|
34
36
|
export const renderPositioner = (props: ComponentProps<'div'>, state: { open: boolean }) => createElement(LayerPositioner, { ...props, open: state.open });
|
package/src/prefetch.ts
CHANGED
|
@@ -4,9 +4,8 @@ export type Loader = () => Promise<unknown>;
|
|
|
4
4
|
export type WhenIdle = (run: () => void) => void;
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
9
|
-
* joins the end, which is how a lazy surface inside another lazy one gets its turn.
|
|
7
|
+
* One module per idle moment and never two at once. A loader registered while everything is on joins
|
|
8
|
+
* the end, which is how a lazy surface inside another lazy one gets its turn.
|
|
10
9
|
*/
|
|
11
10
|
export class Prefetcher {
|
|
12
11
|
private readonly registered: Loader[] = [];
|
|
@@ -16,6 +15,7 @@ export class Prefetcher {
|
|
|
16
15
|
private running: Promise<void> | null = null;
|
|
17
16
|
private loading = false;
|
|
18
17
|
private readonly whenIdle: WhenIdle;
|
|
18
|
+
|
|
19
19
|
/* Asked on every ask and again before every load, since a person can turn data saving on while the page runs. */
|
|
20
20
|
private readonly allowed: () => boolean;
|
|
21
21
|
|
|
@@ -31,17 +31,19 @@ export class Prefetcher {
|
|
|
31
31
|
|
|
32
32
|
register(load: Loader): void {
|
|
33
33
|
this.registered.push(load);
|
|
34
|
+
|
|
34
35
|
if (this.everythingOn) {
|
|
35
36
|
this.enqueue(load);
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/* Loads this one module, ahead of whatever is registered. */
|
|
40
40
|
prefetch(load: Loader): Promise<void> {
|
|
41
41
|
if (!this.allowed()) {
|
|
42
42
|
return Promise.resolve();
|
|
43
43
|
}
|
|
44
|
+
|
|
44
45
|
this.enqueue(load);
|
|
46
|
+
|
|
45
47
|
return this.drain();
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -50,10 +52,13 @@ export class Prefetcher {
|
|
|
50
52
|
if (!this.allowed()) {
|
|
51
53
|
return Promise.resolve();
|
|
52
54
|
}
|
|
55
|
+
|
|
53
56
|
this.everythingOn = true;
|
|
57
|
+
|
|
54
58
|
for (const load of this.registered) {
|
|
55
59
|
this.enqueue(load);
|
|
56
60
|
}
|
|
61
|
+
|
|
57
62
|
return this.drain();
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -61,6 +66,7 @@ export class Prefetcher {
|
|
|
61
66
|
if (this.queued.has(load)) {
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
69
|
+
|
|
64
70
|
this.queued.add(load);
|
|
65
71
|
this.queue.push(load);
|
|
66
72
|
void this.drain();
|
|
@@ -71,6 +77,7 @@ export class Prefetcher {
|
|
|
71
77
|
if (this.running === null && this.queue.length > 0) {
|
|
72
78
|
this.running = this.loadQueue();
|
|
73
79
|
}
|
|
80
|
+
|
|
74
81
|
return this.running ?? Promise.resolve();
|
|
75
82
|
}
|
|
76
83
|
|
|
@@ -78,12 +85,16 @@ export class Prefetcher {
|
|
|
78
85
|
try {
|
|
79
86
|
for (let load = this.queue.shift(); load; load = this.queue.shift()) {
|
|
80
87
|
await new Promise<void>((resolve) => this.whenIdle(resolve));
|
|
88
|
+
|
|
81
89
|
if (!this.allowed()) {
|
|
82
90
|
// Data saving came on while it waited; a later ask may queue it again.
|
|
83
91
|
this.queued.delete(load);
|
|
92
|
+
|
|
84
93
|
continue;
|
|
85
94
|
}
|
|
95
|
+
|
|
86
96
|
this.loading = true;
|
|
97
|
+
|
|
87
98
|
try {
|
|
88
99
|
await load();
|
|
89
100
|
} catch {
|
|
@@ -100,19 +111,21 @@ export class Prefetcher {
|
|
|
100
111
|
|
|
101
112
|
/* Safari has no idle callback; the timeout at least lets the frame that asked finish first. */
|
|
102
113
|
const FALLBACK_DELAY_MS = 50;
|
|
114
|
+
|
|
103
115
|
/* A canvas that keeps drawing (a blinking cursor, a stream) may never leave an idle moment, and a prefetch that never runs helps nobody. */
|
|
104
116
|
const IDLE_TIMEOUT_MS = 2000;
|
|
105
117
|
|
|
106
118
|
const whenIdle: WhenIdle = (run) => {
|
|
107
119
|
if (typeof requestIdleCallback === 'function') {
|
|
108
120
|
requestIdleCallback(() => run(), { timeout: IDLE_TIMEOUT_MS });
|
|
121
|
+
|
|
109
122
|
return;
|
|
110
123
|
}
|
|
124
|
+
|
|
111
125
|
setTimeout(run, FALLBACK_DELAY_MS);
|
|
112
126
|
};
|
|
113
127
|
|
|
114
128
|
/* Not in every DOM typing yet, and only Chromium reports it. */
|
|
115
129
|
const savesData = (): boolean => (navigator as { connection?: { saveData?: boolean } }).connection?.saveData === true;
|
|
116
130
|
|
|
117
|
-
/* The one every `lazyNamed` and `lazyDialog` registers with. */
|
|
118
131
|
export const prefetcher = /* @__PURE__ */ new Prefetcher(whenIdle, () => !savesData());
|
|
@@ -15,6 +15,7 @@ export function ProjectSwitcherRow<Item extends ProjectSwitcherItem>({
|
|
|
15
15
|
}) {
|
|
16
16
|
const { t } = useTranslation('ui');
|
|
17
17
|
const hasActions = Boolean(project.actions);
|
|
18
|
+
|
|
18
19
|
const item = (
|
|
19
20
|
<Menu.Item
|
|
20
21
|
className={clsx('min-w-0', project.muted && 'opacity-50')}
|
|
@@ -27,6 +28,7 @@ export function ProjectSwitcherRow<Item extends ProjectSwitcherItem>({
|
|
|
27
28
|
{project.hint && <span className="ml-auto truncate pl-3 text-xs text-text-faint">{project.hint}</span>}
|
|
28
29
|
</Menu.Item>
|
|
29
30
|
);
|
|
31
|
+
|
|
30
32
|
const row = project.description ? (
|
|
31
33
|
<Tooltip label={project.description} side="right" sideOffset={hasActions ? 41 : undefined}>
|
|
32
34
|
{item}
|
package/src/segmented-keys.ts
CHANGED
|
@@ -5,6 +5,7 @@ const STEPS: Readonly<Record<string, number>> = { ArrowRight: 1, ArrowDown: 1, A
|
|
|
5
5
|
// The index of the one tab stop: the picked option, or the first that can be picked when that one is missing or disabled.
|
|
6
6
|
export function segmentedStop<T extends string>(options: readonly SegmentedOption<T>[], value: T): number {
|
|
7
7
|
const picked = options.findIndex((option) => option.id === value);
|
|
8
|
+
|
|
8
9
|
return picked !== -1 && !options[picked]!.disabled ? picked : options.findIndex((option) => !option.disabled);
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -13,21 +14,26 @@ export function segmentedTarget(options: readonly SegmentedOption<string>[], ind
|
|
|
13
14
|
const nearest = (from: number, step: number): number | null => {
|
|
14
15
|
for (let offset = 0; offset < options.length; offset++) {
|
|
15
16
|
const candidate = (((from + step * offset) % options.length) + options.length) % options.length;
|
|
17
|
+
|
|
16
18
|
if (!options[candidate]!.disabled) {
|
|
17
19
|
return candidate;
|
|
18
20
|
}
|
|
19
21
|
}
|
|
22
|
+
|
|
20
23
|
return null;
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
if (key in STEPS) {
|
|
24
27
|
return nearest(index + STEPS[key]!, STEPS[key]!);
|
|
25
28
|
}
|
|
29
|
+
|
|
26
30
|
if (key === 'Home') {
|
|
27
31
|
return nearest(0, 1);
|
|
28
32
|
}
|
|
33
|
+
|
|
29
34
|
if (key === 'End') {
|
|
30
35
|
return nearest(options.length - 1, -1);
|
|
31
36
|
}
|
|
37
|
+
|
|
32
38
|
return null;
|
|
33
39
|
}
|
package/src/select-shared.ts
CHANGED
package/src/selection.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The text selection, as a context menu has to read it. The app has one selection at a time and a
|
|
3
|
-
* menu belongs to the surface the click landed on, so both of these are scoped to an element.
|
|
4
|
-
* A selection somewhere else is no selection this menu can copy.
|
|
5
|
-
*/
|
|
1
|
+
/* A menu belongs to the surface the click landed on, so both of these are scoped to an element: a selection elsewhere is none this menu can copy. */
|
|
6
2
|
|
|
7
3
|
export const selectionWithin = (element: HTMLElement | null): string => {
|
|
8
4
|
const selection = window.getSelection();
|
|
5
|
+
|
|
9
6
|
if (!element || !selection || selection.isCollapsed || selection.rangeCount === 0) {
|
|
10
7
|
return '';
|
|
11
8
|
}
|
|
9
|
+
|
|
12
10
|
return element.contains(selection.getRangeAt(0).commonAncestorContainer) ? selection.toString() : '';
|
|
13
11
|
};
|
|
14
12
|
|
|
15
13
|
export const selectAllWithin = (element: HTMLElement | null): void => {
|
|
16
14
|
const selection = window.getSelection();
|
|
15
|
+
|
|
17
16
|
if (!element || !selection) {
|
|
18
17
|
return;
|
|
19
18
|
}
|
|
19
|
+
|
|
20
20
|
const range = document.createRange();
|
|
21
21
|
range.selectNodeContents(element);
|
|
22
22
|
selection.removeAllRanges();
|
|
@@ -7,10 +7,13 @@ export interface ConfirmDialogProps {
|
|
|
7
7
|
title: string;
|
|
8
8
|
description: string;
|
|
9
9
|
confirmLabel: string;
|
|
10
|
+
|
|
10
11
|
/* Closes the dialog once it resolves; a rejection stays on screen as the reason. */
|
|
11
12
|
onConfirm(): Promise<void>;
|
|
13
|
+
|
|
12
14
|
/* Extra on the popup. */
|
|
13
15
|
className?: string;
|
|
16
|
+
|
|
14
17
|
ref?: Ref<HTMLDivElement>;
|
|
15
18
|
}
|
|
16
19
|
|