@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/tabs/parts.tsx
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
useContext,
|
|
6
6
|
useEffect,
|
|
7
7
|
useLayoutEffect,
|
|
8
|
-
useMemo,
|
|
9
8
|
useRef,
|
|
10
9
|
useState,
|
|
11
10
|
type ComponentProps,
|
|
@@ -41,7 +40,6 @@ const InMenuContext = createContext(false);
|
|
|
41
40
|
const TAB =
|
|
42
41
|
'relative flex h-9 shrink-0 items-center gap-1.5 whitespace-nowrap text-xs font-medium text-text-muted hover:text-text data-[active]:text-text data-[disabled]:opacity-50 after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 data-[active]:after:bg-text';
|
|
43
42
|
|
|
44
|
-
/* The button that opens the tabs that do not fit reads as one more tab, at the very end of the strip. */
|
|
45
43
|
const MORE = clsx(TAB, 'ml-auto data-[popup-open]:text-text');
|
|
46
44
|
|
|
47
45
|
/* A tab as a key in a bar, for tabs that share a toolbar with buttons and must not read as the tabs of a pane under it. */
|
|
@@ -70,18 +68,21 @@ export function TabsRoot({ value, defaultValue = 0, onValueChange, ...props }: T
|
|
|
70
68
|
const current = controlled ? value : own;
|
|
71
69
|
// The latest handler without a new context for every render of a parent that writes it inline.
|
|
72
70
|
const latest = useRef(onValueChange);
|
|
71
|
+
|
|
73
72
|
useLayoutEffect(() => {
|
|
74
73
|
latest.current = onValueChange;
|
|
75
74
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
75
|
+
|
|
76
|
+
const select = (next: TabsValue): void => {
|
|
77
|
+
if (!controlled) {
|
|
78
|
+
setOwn(next);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
latest.current?.(next);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const selection = { value: current, select };
|
|
85
|
+
|
|
85
86
|
return (
|
|
86
87
|
<SelectionContext value={selection}>
|
|
87
88
|
<BaseTabs.Root value={current} onValueChange={(next) => selection.select(next)} {...props} />
|
|
@@ -92,14 +93,19 @@ export function TabsRoot({ value, defaultValue = 0, onValueChange, ...props }: T
|
|
|
92
93
|
export type TabsListProps = Omit<ComponentProps<typeof BaseTabs.List>, 'className'> & {
|
|
93
94
|
/* On the strip, which runs the width of the pane; padding here insets the tabs from its edges. */
|
|
94
95
|
className?: string;
|
|
96
|
+
|
|
95
97
|
/* Drawn before the tabs, such as what they are the views of. It keeps its own width, like `end`. */
|
|
96
98
|
start?: ReactNode;
|
|
99
|
+
|
|
97
100
|
/* Drawn after the tabs, such as a button that adds one. It keeps its own width: the tabs fit in what is left. */
|
|
98
101
|
end?: ReactNode;
|
|
102
|
+
|
|
99
103
|
/* Like `end`, but right beside the last tab instead of at the end of the strip, so it moves with the tabs. */
|
|
100
104
|
after?: ReactNode;
|
|
105
|
+
|
|
101
106
|
/* `line` underlines the picked tab over the strip's border; `pill` fills it, for tabs in a toolbar. */
|
|
102
107
|
variant?: TabsVariant;
|
|
108
|
+
|
|
103
109
|
/* What happens to tabs that do not fit: a "More" menu at the end of the strip, or a row that scrolls sideways. */
|
|
104
110
|
overflow?: 'menu' | 'scroll';
|
|
105
111
|
};
|
|
@@ -108,9 +114,8 @@ const sameIndices = (left: readonly number[] | null, right: readonly number[]):
|
|
|
108
114
|
left !== null && left.length === right.length && left.every((index, at) => index === right[at]);
|
|
109
115
|
|
|
110
116
|
/*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* since the menu repeats what a tab holds.
|
|
117
|
+
* Every tab is measured in a row nobody sees, so the strip knows what fits before it draws. The tabs
|
|
118
|
+
* are the direct children, since the menu repeats what a tab holds.
|
|
114
119
|
*/
|
|
115
120
|
export function TabsList({ className, children, start, end, after, variant = 'line', overflow = 'menu', ...props }: TabsListProps) {
|
|
116
121
|
const style = STYLES[variant];
|
|
@@ -123,43 +128,51 @@ export function TabsList({ className, children, start, end, after, variant = 'li
|
|
|
123
128
|
const beside = useRef<HTMLDivElement>(null);
|
|
124
129
|
const list = useRef<HTMLDivElement>(null);
|
|
125
130
|
const scrolls = overflow === 'scroll';
|
|
126
|
-
// Which edges of a scrolling row still hide a tab, where it fades out.
|
|
127
131
|
const [edges, setEdges] = useState({ start: false, end: false });
|
|
128
132
|
// Null until the first measure, which runs before the first paint.
|
|
129
133
|
const [shown, setShown] = useState<number[] | null>(null);
|
|
134
|
+
|
|
130
135
|
const tabs = Children.toArray(children).filter(
|
|
131
136
|
(child): child is ReactElement<ComponentProps<typeof TabsTab>> => isValidElement(child) && child.type === TabsTab
|
|
132
137
|
);
|
|
138
|
+
|
|
133
139
|
const kept = tabs.flatMap((tab, index) => (tab.props.value === value || tab.props.pinned === true ? [index] : []));
|
|
134
140
|
|
|
135
141
|
useLayoutEffect(() => {
|
|
136
142
|
const host = strip.current;
|
|
137
143
|
const row = measure.current;
|
|
144
|
+
|
|
138
145
|
if (scrolls || !host || !row) {
|
|
139
146
|
return;
|
|
140
147
|
}
|
|
148
|
+
|
|
141
149
|
const fit = (): void => {
|
|
142
150
|
const style = getComputedStyle(host);
|
|
143
151
|
const gap = parseFloat(style.columnGap) || 0;
|
|
152
|
+
|
|
144
153
|
const taken = [head.current, beside.current, tail.current].reduce(
|
|
145
154
|
(sum, part) => (part === null ? sum : sum + part.getBoundingClientRect().width + gap),
|
|
146
155
|
0
|
|
147
156
|
);
|
|
157
|
+
|
|
148
158
|
const room = host.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) - taken;
|
|
149
159
|
const cells = [...row.children].map((cell) => cell.getBoundingClientRect().width);
|
|
150
160
|
const more = cells.pop() ?? 0;
|
|
151
161
|
const next = shownTabs(cells, room, parseFloat(getComputedStyle(row).columnGap) || 0, more, kept);
|
|
152
162
|
setShown((current) => (sameIndices(current, next) ? current : next));
|
|
153
163
|
};
|
|
164
|
+
|
|
154
165
|
fit();
|
|
155
166
|
const observer = new ResizeObserver(fit);
|
|
156
167
|
observer.observe(host);
|
|
157
168
|
observer.observe(row);
|
|
169
|
+
|
|
158
170
|
for (const part of [head.current, beside.current, tail.current]) {
|
|
159
171
|
if (part !== null) {
|
|
160
172
|
observer.observe(part);
|
|
161
173
|
}
|
|
162
174
|
}
|
|
175
|
+
|
|
163
176
|
return () => observer.disconnect();
|
|
164
177
|
});
|
|
165
178
|
|
|
@@ -168,11 +181,14 @@ export function TabsList({ className, children, start, end, after, variant = 'li
|
|
|
168
181
|
useEffect(() => {
|
|
169
182
|
const row = list.current;
|
|
170
183
|
const tab = row?.querySelector<HTMLElement>('[aria-selected="true"]');
|
|
184
|
+
|
|
171
185
|
if (!scrolls || !row || !tab) {
|
|
172
186
|
return;
|
|
173
187
|
}
|
|
188
|
+
|
|
174
189
|
const strip = row.getBoundingClientRect();
|
|
175
190
|
const picked = tab.getBoundingClientRect();
|
|
191
|
+
|
|
176
192
|
if (picked.left < strip.left) {
|
|
177
193
|
row.scrollLeft -= strip.left - picked.left;
|
|
178
194
|
} else if (picked.right > strip.right) {
|
|
@@ -182,21 +198,26 @@ export function TabsList({ className, children, start, end, after, variant = 'li
|
|
|
182
198
|
|
|
183
199
|
useEffect(() => {
|
|
184
200
|
const row = list.current;
|
|
201
|
+
|
|
185
202
|
if (!scrolls || !row) {
|
|
186
203
|
return;
|
|
187
204
|
}
|
|
205
|
+
|
|
188
206
|
const measure = (): void => {
|
|
189
207
|
const start = row.scrollLeft > 0;
|
|
190
208
|
const end = row.scrollLeft + row.clientWidth < row.scrollWidth - 1;
|
|
191
209
|
setEdges((current) => (current.start === start && current.end === end ? current : { start, end }));
|
|
192
210
|
};
|
|
211
|
+
|
|
193
212
|
measure();
|
|
194
213
|
row.addEventListener('scroll', measure, { passive: true });
|
|
195
214
|
const observer = new ResizeObserver(measure);
|
|
196
215
|
observer.observe(row);
|
|
216
|
+
|
|
197
217
|
for (const tab of row.children) {
|
|
198
218
|
observer.observe(tab);
|
|
199
219
|
}
|
|
220
|
+
|
|
200
221
|
return () => {
|
|
201
222
|
row.removeEventListener('scroll', measure);
|
|
202
223
|
observer.disconnect();
|
|
@@ -206,20 +227,25 @@ export function TabsList({ className, children, start, end, after, variant = 'li
|
|
|
206
227
|
// A mouse wheel only turns vertically, and on a row that scrolls sideways that is the way it should turn.
|
|
207
228
|
useEffect(() => {
|
|
208
229
|
const row = list.current;
|
|
230
|
+
|
|
209
231
|
if (!scrolls || !row) {
|
|
210
232
|
return;
|
|
211
233
|
}
|
|
234
|
+
|
|
212
235
|
const onWheel = (event: WheelEvent): void => {
|
|
213
236
|
if (!event.deltaX && event.deltaY) {
|
|
214
237
|
row.scrollLeft += event.deltaY;
|
|
215
238
|
}
|
|
216
239
|
};
|
|
240
|
+
|
|
217
241
|
row.addEventListener('wheel', onWheel, { passive: true });
|
|
242
|
+
|
|
218
243
|
return () => row.removeEventListener('wheel', onWheel);
|
|
219
244
|
}, [scrolls]);
|
|
220
245
|
|
|
221
246
|
const visible = shown === null ? tabs : shown.map((index) => tabs[index]!);
|
|
222
247
|
const hidden = shown === null ? [] : tabs.filter((_, index) => !shown.includes(index));
|
|
248
|
+
|
|
223
249
|
const more = (
|
|
224
250
|
<>
|
|
225
251
|
{t('tabs.more')}
|
|
@@ -289,6 +315,7 @@ type TabProps = ComponentProps<typeof BaseTabs.Tab>;
|
|
|
289
315
|
export type TabsTabProps = TabProps & {
|
|
290
316
|
/* Makes the tab closable: a close button in it, a middle click, and Delete or Backspace while the tab has focus. */
|
|
291
317
|
onClose?(): void;
|
|
318
|
+
|
|
292
319
|
/* Keeps the tab in the strip, never in the menu, and leaves it unclosable even with `onClose`. */
|
|
293
320
|
pinned?: boolean;
|
|
294
321
|
};
|
|
@@ -298,17 +325,9 @@ export function TabsTab({ className, onClose, pinned = false, children, ...props
|
|
|
298
325
|
const { t } = useTranslation('ui');
|
|
299
326
|
const tab = STYLES[useContext(VariantContext)].tab;
|
|
300
327
|
|
|
301
|
-
if (pinned) {
|
|
302
|
-
return (
|
|
303
|
-
<BaseTabs.Tab className={withClass(tab, className)} data-pinned="" {...props}>
|
|
304
|
-
{children}
|
|
305
|
-
</BaseTabs.Tab>
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (onClose === undefined) {
|
|
328
|
+
if (pinned || onClose === undefined) {
|
|
310
329
|
return (
|
|
311
|
-
<BaseTabs.Tab className={withClass(tab, className)} {...props}>
|
|
330
|
+
<BaseTabs.Tab className={withClass(tab, className)} data-pinned={pinned ? '' : undefined} {...props}>
|
|
312
331
|
{children}
|
|
313
332
|
</BaseTabs.Tab>
|
|
314
333
|
);
|
|
@@ -318,6 +337,7 @@ export function TabsTab({ className, onClose, pinned = false, children, ...props
|
|
|
318
337
|
|
|
319
338
|
const handleKeyDown = (e: Parameters<NonNullable<TabProps['onKeyDown']>>[0]): void => {
|
|
320
339
|
onKeyDown?.(e);
|
|
340
|
+
|
|
321
341
|
if (!e.defaultPrevented && e.target === e.currentTarget && (e.key === 'Delete' || e.key === 'Backspace')) {
|
|
322
342
|
e.preventDefault();
|
|
323
343
|
onClose();
|
|
@@ -327,6 +347,7 @@ export function TabsTab({ className, onClose, pinned = false, children, ...props
|
|
|
327
347
|
// A middle press starts autoscroll on some platforms; the release is what closes.
|
|
328
348
|
const handleMouseDown = (e: Parameters<NonNullable<TabProps['onAuxClick']>>[0]): void => {
|
|
329
349
|
onMouseDown?.(e);
|
|
350
|
+
|
|
330
351
|
if (e.button === 1) {
|
|
331
352
|
e.preventDefault();
|
|
332
353
|
}
|
|
@@ -334,6 +355,7 @@ export function TabsTab({ className, onClose, pinned = false, children, ...props
|
|
|
334
355
|
|
|
335
356
|
const handleAuxClick = (e: Parameters<NonNullable<TabProps['onAuxClick']>>[0]): void => {
|
|
336
357
|
onAuxClick?.(e);
|
|
358
|
+
|
|
337
359
|
if (!e.defaultPrevented && e.button === 1) {
|
|
338
360
|
e.preventDefault();
|
|
339
361
|
onClose();
|
|
@@ -371,9 +393,10 @@ export type TabsCountProps = Omit<useRender.ComponentProps<'span'>, 'children'>
|
|
|
371
393
|
value: number;
|
|
372
394
|
};
|
|
373
395
|
|
|
374
|
-
/*
|
|
396
|
+
/* Nothing at zero: an empty view needs no number to say so. */
|
|
375
397
|
export function TabsCount({ value, render, className, ref, ...props }: TabsCountProps) {
|
|
376
398
|
const inMenu = useContext(InMenuContext);
|
|
399
|
+
|
|
377
400
|
const element = useRender({
|
|
378
401
|
render,
|
|
379
402
|
ref,
|
|
@@ -384,5 +407,6 @@ export function TabsCount({ value, render, className, ref, ...props }: TabsCount
|
|
|
384
407
|
className: clsx('inline-flex shrink-0', inMenu && 'ml-auto', className)
|
|
385
408
|
}
|
|
386
409
|
});
|
|
410
|
+
|
|
387
411
|
return value === 0 ? null : element;
|
|
388
412
|
}
|
package/src/testing/dedupe.ts
CHANGED
|
@@ -3,11 +3,10 @@ import { dirname, join, sep } from 'node:path';
|
|
|
3
3
|
import { Glob, plugin } from 'bun';
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* same file in the app's copy. Does nothing once the library is installed from the registry.
|
|
6
|
+
* What `resolve.dedupe` does for Vite, for `bun test` in an app that links a checkout: a second React,
|
|
7
|
+
* i18next or Base UI breaks every hook, word and compound part. Bun has no dedupe and its resolve hook
|
|
8
|
+
* never sees a package import, so each file of these packages in the checkout loads as a stand-in for
|
|
9
|
+
* the app's copy.
|
|
11
10
|
*
|
|
12
11
|
* CodeMirror and Lezer reject an extension, a facet or a node prop made by another copy, and the chat
|
|
13
12
|
* views take both from the app; shiki is in the list for the highlighter an app hands them.
|
|
@@ -34,6 +33,7 @@ interface Copy {
|
|
|
34
33
|
|
|
35
34
|
const manifestOf = (folder: string): Manifest | null => {
|
|
36
35
|
const path = join(folder, 'package.json');
|
|
36
|
+
|
|
37
37
|
return existsSync(path) ? (JSON.parse(readFileSync(path, 'utf8')) as Manifest) : null;
|
|
38
38
|
};
|
|
39
39
|
|
|
@@ -43,6 +43,7 @@ const dependsOnLibrary = (manifest: Manifest | null): boolean =>
|
|
|
43
43
|
const membersOf = (root: string, manifest: Manifest | null): string[] => {
|
|
44
44
|
const workspaces = manifest?.workspaces;
|
|
45
45
|
const patterns = (Array.isArray(workspaces) ? workspaces : workspaces?.packages) ?? [];
|
|
46
|
+
|
|
46
47
|
return patterns
|
|
47
48
|
.flatMap((pattern) => [...new Glob(`${pattern}/package.json`).scanSync({ cwd: root, onlyFiles: true })])
|
|
48
49
|
.map((path) => join(root, dirname(path)));
|
|
@@ -54,6 +55,7 @@ const membersOf = (root: string, manifest: Manifest | null): string[] => {
|
|
|
54
55
|
*/
|
|
55
56
|
const appFolders = (): string[] => {
|
|
56
57
|
const root = process.cwd();
|
|
58
|
+
|
|
57
59
|
return [root, ...membersOf(root, manifestOf(root)).filter((folder) => dependsOnLibrary(manifestOf(folder)))];
|
|
58
60
|
};
|
|
59
61
|
|
|
@@ -61,21 +63,25 @@ const appFolders = (): string[] => {
|
|
|
61
63
|
const checkoutFolders = (): string[] => {
|
|
62
64
|
for (let folder = dirname(library); dirname(folder) !== folder; folder = dirname(folder)) {
|
|
63
65
|
const manifest = manifestOf(folder);
|
|
66
|
+
|
|
64
67
|
if (manifest?.workspaces !== undefined) {
|
|
65
68
|
const members = membersOf(folder, manifest);
|
|
69
|
+
|
|
66
70
|
return members.includes(library) ? [folder, ...members] : [library];
|
|
67
71
|
}
|
|
68
72
|
}
|
|
73
|
+
|
|
69
74
|
return [library];
|
|
70
75
|
};
|
|
71
76
|
|
|
72
|
-
/* The packages a name of the list stands for in a folder: itself, or every package of the scope for `@scope/*`. */
|
|
73
77
|
const namesIn = (folder: string, pattern: string): string[] => {
|
|
74
78
|
if (!pattern.endsWith('/*')) {
|
|
75
79
|
return [pattern];
|
|
76
80
|
}
|
|
81
|
+
|
|
77
82
|
const scope = pattern.slice(0, -2);
|
|
78
83
|
const path = join(folder, 'node_modules', scope);
|
|
84
|
+
|
|
79
85
|
return existsSync(path)
|
|
80
86
|
? readdirSync(path)
|
|
81
87
|
.filter((entry) => !entry.startsWith('.'))
|
|
@@ -83,13 +89,15 @@ const namesIn = (folder: string, pattern: string): string[] => {
|
|
|
83
89
|
: [];
|
|
84
90
|
};
|
|
85
91
|
|
|
86
|
-
/*
|
|
92
|
+
/* Walks up the node_modules folders as Node does. */
|
|
87
93
|
const copyFrom = (from: string, name: string): string | null => {
|
|
88
94
|
for (let folder = from; ; folder = dirname(folder)) {
|
|
89
95
|
const candidate = join(folder, 'node_modules', name);
|
|
96
|
+
|
|
90
97
|
if (existsSync(candidate)) {
|
|
91
98
|
return realpathSync(candidate);
|
|
92
99
|
}
|
|
100
|
+
|
|
93
101
|
if (dirname(folder) === folder) {
|
|
94
102
|
return null;
|
|
95
103
|
}
|
|
@@ -98,28 +106,38 @@ const copyFrom = (from: string, name: string): string | null => {
|
|
|
98
106
|
|
|
99
107
|
const findCopies = (): Copy[] => {
|
|
100
108
|
const checkout = checkoutFolders();
|
|
109
|
+
|
|
101
110
|
const inside = (path: string): boolean => path === checkout[0] || path.startsWith(`${checkout[0]}${sep}`);
|
|
111
|
+
|
|
102
112
|
// A run inside the checkout is the library's own; a node_modules above it belongs to no app.
|
|
103
113
|
if (inside(realpathSync(process.cwd()))) {
|
|
104
114
|
return [];
|
|
105
115
|
}
|
|
116
|
+
|
|
106
117
|
const folders = appFolders();
|
|
118
|
+
|
|
107
119
|
const added = folders.flatMap((folder) => {
|
|
108
120
|
const names = manifestOf(folder)?.adecore?.dedupe;
|
|
121
|
+
|
|
109
122
|
return Array.isArray(names) ? names.filter((name): name is string => typeof name === 'string') : [];
|
|
110
123
|
});
|
|
124
|
+
|
|
111
125
|
const names = new Set([...SHARED, ...added].flatMap((pattern) => checkout.flatMap((folder) => namesIn(folder, pattern))));
|
|
126
|
+
|
|
112
127
|
return [...names].flatMap((name) => {
|
|
113
128
|
const ours = folders.map((folder) => copyFrom(folder, name)).find((copy): copy is string => copy !== null && !inside(copy));
|
|
129
|
+
|
|
114
130
|
if (ours === undefined) {
|
|
115
131
|
return [];
|
|
116
132
|
}
|
|
133
|
+
|
|
117
134
|
const theirs = new Set(
|
|
118
135
|
checkout
|
|
119
136
|
.map((folder) => join(folder, 'node_modules', name))
|
|
120
137
|
.filter((path) => existsSync(path))
|
|
121
138
|
.map((path) => realpathSync(path))
|
|
122
139
|
);
|
|
140
|
+
|
|
123
141
|
return [...theirs].filter((real) => real !== ours).map((real) => ({ theirs: `${real}${sep}`, ours: `${ours}${sep}` }));
|
|
124
142
|
});
|
|
125
143
|
};
|
|
@@ -140,6 +158,7 @@ const standIn = (path: string): string => {
|
|
|
140
158
|
const { exports, imports } = transpiler.scan(readFileSync(path, 'utf8'));
|
|
141
159
|
const esm = exports.length > 0 || imports.some((entry) => entry.kind === 'import-statement');
|
|
142
160
|
const target = JSON.stringify(path);
|
|
161
|
+
|
|
143
162
|
return [`export * from ${target};`, ...(!esm || exports.includes('default') ? [`export { default } from ${target};`] : [])].join('\n');
|
|
144
163
|
};
|
|
145
164
|
|
|
@@ -149,6 +168,7 @@ if (copies.length > 0) {
|
|
|
149
168
|
setup(build) {
|
|
150
169
|
build.onLoad({ filter: new RegExp(`^(${copies.map((copy) => escape(copy.theirs)).join('|')})`) }, (args) => {
|
|
151
170
|
const copy = copies.find((candidate) => args.path.startsWith(candidate.theirs))!;
|
|
171
|
+
|
|
152
172
|
return { contents: standIn(copy.ours + args.path.slice(copy.theirs.length)), loader: 'js' };
|
|
153
173
|
});
|
|
154
174
|
}
|
package/src/tint.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { CSSProperties } from 'react';
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* take it. `--border` is mixed from `--border-strong`, since a property that reads itself is no
|
|
8
|
-
* value at all.
|
|
4
|
+
* The theme's utilities read `--surface-hover` and `--border` themselves (`@theme inline`), so
|
|
5
|
+
* buttons and separators under the element take its color. `--border` mixes from `--border-strong`,
|
|
6
|
+
* since a property that reads itself is no value at all.
|
|
9
7
|
*/
|
|
10
8
|
export function tintStyle(color: string, ground = 'var(--surface-raised)'): CSSProperties {
|
|
11
9
|
return {
|
package/src/toast-store.ts
CHANGED
|
@@ -4,7 +4,6 @@ import type { Shortcut } from './shortcut.ts';
|
|
|
4
4
|
// How long a toast that went well stays up; a failure waits for the person instead.
|
|
5
5
|
export const SUCCESS_MS = 4000;
|
|
6
6
|
|
|
7
|
-
// How long a deletion can still be taken back.
|
|
8
7
|
export const UNDO_MS = 8000;
|
|
9
8
|
|
|
10
9
|
// `setTimeout` fires at once past this, so a longer lifetime, `Infinity` too, is a toast that stays.
|
|
@@ -16,35 +15,39 @@ export type ToastKind = 'progress' | 'success' | 'error' | 'deleted';
|
|
|
16
15
|
export interface ToastAction {
|
|
17
16
|
label: string;
|
|
18
17
|
run(): void;
|
|
18
|
+
|
|
19
19
|
/* The key that does the same, printed beside the label. The key itself is bound elsewhere. */
|
|
20
20
|
shortcut?: Shortcut;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/*
|
|
23
|
+
/* Epoch milliseconds. */
|
|
24
24
|
export interface ToastDeadline {
|
|
25
25
|
start: number;
|
|
26
26
|
end: number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/* How far into its lifetime a toast is at `now`, held inside that lifetime. */
|
|
30
29
|
export const elapsedOf = (deadline: ToastDeadline, now: number): number => Math.min(Math.max(now - deadline.start, 0), deadline.end - deadline.start);
|
|
31
30
|
|
|
32
31
|
/* An app that carries more on a toast extends this and hands its own type to `createToastStore`. */
|
|
33
32
|
export interface Toast {
|
|
34
33
|
id: string;
|
|
35
34
|
title: string;
|
|
36
|
-
/* The line under the title, often the one that says why something stopped. */
|
|
37
35
|
description?: string;
|
|
38
36
|
kind: ToastKind;
|
|
39
37
|
action?: ToastAction;
|
|
40
|
-
|
|
38
|
+
|
|
39
|
+
/* Drawn after `action`, in order. */
|
|
41
40
|
actions?: ToastAction[];
|
|
41
|
+
|
|
42
42
|
/* A success that waits to be dismissed, for news that lands while nobody is looking yet. */
|
|
43
43
|
persist?: boolean;
|
|
44
|
+
|
|
44
45
|
/* Milliseconds until the toast takes itself away, whatever its kind; over the kind's own lifetime and `persist`. Zero or less is ignored. */
|
|
45
46
|
lifetime?: number;
|
|
46
|
-
|
|
47
|
+
|
|
48
|
+
/* Whether it ran out or was dismissed. */
|
|
47
49
|
onClose?: () => void;
|
|
50
|
+
|
|
48
51
|
/* Set by the store from the timer that takes the toast away, so what counts down on screen is that timer. */
|
|
49
52
|
deadline?: ToastDeadline;
|
|
50
53
|
}
|
|
@@ -55,18 +58,17 @@ export type ToastPatch<T extends Toast = Toast> = Partial<Omit<T, 'id' | 'deadli
|
|
|
55
58
|
|
|
56
59
|
export interface ToastStore<T extends Toast = Toast> {
|
|
57
60
|
toasts: T[];
|
|
61
|
+
|
|
58
62
|
/* Puts one up, or moves the one with this id to what it says now; answers its id. */
|
|
59
63
|
show(toast: ToastInput<T>): string;
|
|
64
|
+
|
|
60
65
|
update(id: string, patch: ToastPatch<T>): void;
|
|
61
66
|
dismiss(id: string): void;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
export type ToastStoreHook<T extends Toast = Toast> = UseBoundStore<StoreApi<ToastStore<T>>>;
|
|
65
70
|
|
|
66
|
-
/*
|
|
67
|
-
* The one place an app says how something went that no surface on screen shows. A long action keeps
|
|
68
|
-
* one id from progress to its outcome, so a person never watches two cards for one thing.
|
|
69
|
-
*/
|
|
71
|
+
/* A long action keeps one id from progress to its outcome, so a person never watches two cards for one thing. */
|
|
70
72
|
export const createToastStore = <T extends Toast = Toast>(): ToastStoreHook<T> => {
|
|
71
73
|
const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
72
74
|
let counter = 0;
|
|
@@ -77,18 +79,17 @@ export const createToastStore = <T extends Toast = Toast>(): ToastStoreHook<T> =
|
|
|
77
79
|
timers.delete(id);
|
|
78
80
|
};
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
* A toast that went well takes itself away, and so does the offer to undo; a failure and a running
|
|
82
|
-
* action stay, unless the toast names a lifetime of its own.
|
|
83
|
-
*/
|
|
82
|
+
// A failure and a running action stay, unless the toast names a lifetime of its own.
|
|
84
83
|
const schedule = (id: string, toast: Pick<Toast, 'kind' | 'persist' | 'lifetime'>): ToastDeadline | undefined => {
|
|
85
84
|
stop(id);
|
|
86
85
|
const { kind, persist } = toast;
|
|
87
86
|
const own = toast.lifetime !== undefined && toast.lifetime > 0 ? toast.lifetime : undefined;
|
|
88
87
|
const lifetime = own ?? (kind === 'deleted' ? UNDO_MS : kind === 'success' && persist !== true ? SUCCESS_MS : null);
|
|
88
|
+
|
|
89
89
|
if (lifetime === null || lifetime > LONGEST_TIMER_MS) {
|
|
90
90
|
return undefined;
|
|
91
91
|
}
|
|
92
|
+
|
|
92
93
|
timers.set(
|
|
93
94
|
id,
|
|
94
95
|
setTimeout(() => {
|
|
@@ -96,7 +97,9 @@ export const createToastStore = <T extends Toast = Toast>(): ToastStoreHook<T> =
|
|
|
96
97
|
get().dismiss(id);
|
|
97
98
|
}, lifetime)
|
|
98
99
|
);
|
|
100
|
+
|
|
99
101
|
const start = Date.now();
|
|
102
|
+
|
|
100
103
|
return { start, end: start + lifetime };
|
|
101
104
|
};
|
|
102
105
|
|
|
@@ -108,13 +111,16 @@ export const createToastStore = <T extends Toast = Toast>(): ToastStoreHook<T> =
|
|
|
108
111
|
const next = { ...toast, id, deadline: schedule(id, toast) } as T;
|
|
109
112
|
const toasts = get().toasts;
|
|
110
113
|
set({ toasts: toasts.some((entry) => entry.id === id) ? toasts.map((entry) => (entry.id === id ? next : entry)) : [...toasts, next] });
|
|
114
|
+
|
|
111
115
|
return id;
|
|
112
116
|
},
|
|
113
117
|
update(id, patch) {
|
|
114
118
|
const current = get().toasts.find((entry) => entry.id === id);
|
|
119
|
+
|
|
115
120
|
if (current === undefined) {
|
|
116
121
|
return;
|
|
117
122
|
}
|
|
123
|
+
|
|
118
124
|
const merged: T = { ...current, ...patch };
|
|
119
125
|
const next: T = { ...merged, deadline: schedule(id, merged) };
|
|
120
126
|
set({ toasts: get().toasts.map((entry) => (entry.id === id ? next : entry)) });
|
package/src/tree/parts.tsx
CHANGED
|
@@ -25,10 +25,12 @@ export type TreeRootProps = useRender.ComponentProps<'div'> & {
|
|
|
25
25
|
export function TreeRoot({ overflow = 'truncate', render, className, ref, children, ...props }: TreeRootProps) {
|
|
26
26
|
const { attach, bar } = useTreeShift(TREE_ROWS);
|
|
27
27
|
const scrolls = overflow === 'scroll';
|
|
28
|
+
|
|
28
29
|
const rootRef = (node: HTMLDivElement | null) => {
|
|
29
30
|
attach(scrolls ? node : null);
|
|
30
31
|
mergeRefs<HTMLDivElement>(ref)(node);
|
|
31
32
|
};
|
|
33
|
+
|
|
32
34
|
return useRender({
|
|
33
35
|
render,
|
|
34
36
|
ref: rootRef,
|
|
@@ -115,6 +117,7 @@ export type TreeChevronProps = useRender.ComponentProps<'button'> & {
|
|
|
115
117
|
|
|
116
118
|
export function TreeChevron({ expanded, onExpandedChange, render, className, ref, onClick, onDoubleClick, ...props }: TreeChevronProps) {
|
|
117
119
|
const { t } = useTranslation('ui');
|
|
120
|
+
|
|
118
121
|
return useRender({
|
|
119
122
|
render,
|
|
120
123
|
ref,
|
|
@@ -128,6 +131,7 @@ export function TreeChevron({ expanded, onExpandedChange, render, className, ref
|
|
|
128
131
|
onClick: (event: MouseEvent<HTMLButtonElement>) => {
|
|
129
132
|
event.stopPropagation();
|
|
130
133
|
onClick?.(event);
|
|
134
|
+
|
|
131
135
|
if (!event.defaultPrevented) {
|
|
132
136
|
onExpandedChange?.(!expanded);
|
|
133
137
|
}
|
|
@@ -139,16 +143,19 @@ export function TreeChevron({ expanded, onExpandedChange, render, className, ref
|
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
export type TreeLabelProps = useRender.ComponentProps<'span'>;
|
|
146
|
+
|
|
142
147
|
export function TreeLabel({ render, className, ref, ...props }: TreeLabelProps) {
|
|
143
148
|
return useRender({ render, ref, defaultTagName: 'span', props: { ...props, className: clsx('adecore-tree-label', className) } });
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
export type TreeDecorationProps = useRender.ComponentProps<'span'>;
|
|
152
|
+
|
|
147
153
|
export function TreeDecoration({ render, className, ref, ...props }: TreeDecorationProps) {
|
|
148
154
|
return useRender({ render, ref, defaultTagName: 'span', props: { ...props, className: clsx('adecore-tree-decoration', className) } });
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
export type TreeControlProps = useRender.ComponentProps<'span'>;
|
|
158
|
+
|
|
152
159
|
export function TreeControl({ render, className, ref, onClick, onDoubleClick, onKeyDown, onPointerDown, ...props }: TreeControlProps) {
|
|
153
160
|
return useRender({
|
|
154
161
|
render,
|
|
@@ -167,6 +174,7 @@ export function TreeControl({ render, className, ref, onClick, onDoubleClick, on
|
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
export type TreeCheckboxProps = CheckboxProps;
|
|
177
|
+
|
|
170
178
|
export function TreeCheckbox(props: TreeCheckboxProps) {
|
|
171
179
|
return (
|
|
172
180
|
<TreeControl>
|
package/src/tree/shift.ts
CHANGED
|
@@ -27,15 +27,19 @@ export function sidewaysDelta(step: WheelStep, pageWidth: number): number {
|
|
|
27
27
|
const swapped = step.shiftKey && step.deltaX === 0;
|
|
28
28
|
const sideways = swapped ? step.deltaY : step.deltaX;
|
|
29
29
|
const across = swapped ? 0 : step.deltaY;
|
|
30
|
+
|
|
30
31
|
if (sideways === 0 || Math.abs(sideways) <= Math.abs(across)) {
|
|
31
32
|
return 0;
|
|
32
33
|
}
|
|
34
|
+
|
|
33
35
|
if (step.deltaMode === 1) {
|
|
34
36
|
return sideways * LINE_WIDTH;
|
|
35
37
|
}
|
|
38
|
+
|
|
36
39
|
if (step.deltaMode === 2) {
|
|
37
40
|
return sideways * pageWidth;
|
|
38
41
|
}
|
|
42
|
+
|
|
39
43
|
return sideways;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -43,6 +47,8 @@ export function shiftThumb(shift: number, max: number, track: number, view: numb
|
|
|
43
47
|
if (max <= 0 || track <= 0) {
|
|
44
48
|
return { left: 0, width: Math.max(track, 0) };
|
|
45
49
|
}
|
|
50
|
+
|
|
46
51
|
const width = Math.min(track, Math.max(MIN_THUMB, Math.round((track * view) / (view + max))));
|
|
52
|
+
|
|
47
53
|
return { left: Math.round(((track - width) * clampShift(shift, max)) / max), width };
|
|
48
54
|
}
|