@adecore/ui 0.23.0 → 0.24.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/ButtonGroup.d.ts +1 -1
- package/dist/ButtonGroup.js +1 -1
- package/dist/Checkbox.js +1 -1
- package/dist/ChoiceCards.js +2 -2
- package/dist/CloseButton.js +1 -1
- package/dist/ColorSwatch.d.ts +1 -1
- package/dist/ColorSwatch.js +1 -1
- package/dist/Field.d.ts +1 -1
- package/dist/Field.js +4 -3
- package/dist/IconButton.d.ts +1 -1
- package/dist/IconButton.js +1 -1
- package/dist/ListRow.d.ts +1 -1
- package/dist/ListRow.js +1 -1
- package/dist/MiddleTruncate.d.ts +8 -0
- package/dist/MiddleTruncate.js +59 -0
- package/dist/PanelHeader.d.ts +1 -1
- package/dist/PanelHeader.js +1 -1
- package/dist/SectionLabel.d.ts +1 -1
- package/dist/SectionLabel.js +1 -1
- package/dist/Select.js +1 -1
- package/dist/SelectSearch.js +1 -1
- package/dist/Slider.js +1 -1
- package/dist/Surface.d.ts +1 -1
- package/dist/Surface.js +1 -1
- package/dist/Switch.js +1 -1
- package/dist/Toasts.d.ts +1 -1
- package/dist/Toasts.js +4 -5
- package/dist/Tooltip.d.ts +1 -1
- package/dist/Tooltip.js +2 -2
- package/dist/clipboard.d.ts +1 -1
- package/dist/clipboard.js +12 -2
- package/dist/dialog/parts.d.ts +3 -3
- package/dist/dialog/parts.js +2 -2
- package/dist/file-tree/helpers.d.ts +1 -0
- package/dist/file-tree/helpers.js +9 -0
- package/dist/file-tree/parts.d.ts +1 -1
- package/dist/file-tree/parts.js +1 -1
- package/dist/file-tree/use-slots.d.ts +8 -0
- package/dist/file-tree/use-slots.js +64 -22
- package/dist/format/datetime.d.ts +1 -0
- package/dist/format/datetime.js +15 -0
- package/dist/format/index.d.ts +1 -1
- package/dist/format/index.js +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -1
- package/dist/key-value-list/parts.d.ts +1 -1
- package/dist/key-value-list/parts.js +1 -1
- package/dist/locales/en.d.ts +83 -0
- package/dist/locales/en.js +82 -0
- package/dist/locales/nl.d.ts +83 -0
- package/dist/locales/nl.js +82 -0
- package/dist/locales.js +3 -2
- package/dist/menu/parts.d.ts +7 -7
- package/dist/menu/parts.js +4 -4
- package/dist/middle-truncate.d.ts +7 -0
- package/dist/middle-truncate.js +34 -0
- package/dist/popover/parts.d.ts +7 -7
- package/dist/popover/parts.js +2 -2
- package/dist/prefetch.js +6 -1
- package/dist/settings/ConfirmDialog.d.ts +4 -1
- package/dist/settings/ConfirmDialog.js +2 -2
- package/dist/settings/SettingsDialog.js +1 -1
- package/dist/tabs/parts.d.ts +8 -4
- package/dist/tabs/parts.js +84 -18
- package/dist/testing/dedupe.js +74 -23
- package/dist/theme.css +24 -8
- package/dist/tree/parts.d.ts +1 -1
- package/dist/tree/parts.js +1 -1
- package/dist/workspace/DropPreview.d.ts +8 -0
- package/dist/workspace/DropPreview.js +14 -0
- package/dist/workspace/SplitView.d.ts +1 -1
- package/dist/workspace/SplitView.js +59 -18
- package/dist/workspace/TabStrip.d.ts +1 -1
- package/dist/workspace/TabStrip.js +1 -1
- package/dist/workspace/Workspace.d.ts +2 -2
- package/dist/workspace/Workspace.js +4 -2
- package/dist/workspace/preview.d.ts +4 -8
- package/dist/workspace/preview.js +17 -2
- package/package.json +2 -2
- package/src/ButtonGroup.tsx +1 -1
- package/src/Checkbox.tsx +1 -1
- package/src/ChoiceCards.tsx +2 -2
- package/src/CloseButton.tsx +1 -1
- package/src/ColorSwatch.tsx +1 -1
- package/src/ErrorBoundary.tsx +4 -4
- package/src/Field.tsx +4 -5
- package/src/IconButton.tsx +1 -1
- package/src/ListRow.tsx +1 -1
- package/src/MiddleTruncate.tsx +84 -0
- package/src/PanelHeader.tsx +1 -1
- package/src/SectionLabel.tsx +1 -1
- package/src/Select.tsx +1 -1
- package/src/SelectSearch.tsx +1 -1
- package/src/Slider.tsx +1 -1
- package/src/Surface.tsx +1 -1
- package/src/Switch.tsx +1 -1
- package/src/Toasts.tsx +5 -6
- package/src/Tooltip.tsx +5 -5
- package/src/clipboard.ts +11 -2
- package/src/dialog/parts.tsx +2 -2
- package/src/file-tree/helpers.ts +10 -0
- package/src/file-tree/parts.tsx +1 -1
- package/src/file-tree/use-slots.ts +68 -23
- package/src/format/datetime.ts +17 -0
- package/src/format/index.ts +1 -0
- package/src/index.ts +6 -3
- package/src/key-value-list/parts.tsx +1 -1
- package/src/locales/en.ts +82 -0
- package/src/locales/nl.ts +82 -0
- package/src/locales.ts +3 -2
- package/src/menu/parts.tsx +4 -4
- package/src/middle-truncate.ts +41 -0
- package/src/popover/parts.tsx +2 -2
- package/src/prefetch.ts +6 -1
- package/src/settings/ConfirmDialog.tsx +7 -1
- package/src/settings/SettingsDialog.tsx +1 -1
- package/src/tabs/parts.tsx +156 -54
- package/src/testing/dedupe.ts +87 -24
- package/src/theme.css +24 -8
- package/src/tree/parts.tsx +1 -1
- package/src/workspace/DropPreview.tsx +26 -0
- package/src/workspace/SplitView.tsx +58 -22
- package/src/workspace/TabStrip.tsx +1 -1
- package/src/workspace/Workspace.tsx +7 -3
- package/src/workspace/preview.ts +22 -9
- package/dist/locales/en.json +0 -82
- package/dist/locales/nl.json +0 -82
- package/src/locales/en.json +0 -82
- package/src/locales/nl.json +0 -82
package/src/Tooltip.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, type ComponentProps, type ReactElement, type ReactNode } from 'react';
|
|
2
|
-
import { Tooltip as BaseTooltip } from '@base-ui
|
|
2
|
+
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
|
3
3
|
import { Kbd } from './Kbd.tsx';
|
|
4
4
|
import type { Shortcut } from './shortcut.ts';
|
|
5
5
|
import { registerShortcutHint } from './shortcut-hints.ts';
|
|
@@ -53,10 +53,10 @@ export function Tooltip({ label, open, onOpenChange, anchor, kbd, side = 'top',
|
|
|
53
53
|
<BaseTooltip.Portal>
|
|
54
54
|
<BaseTooltip.Positioner anchor={anchor} side={side} sideOffset={sideOffset} className="tooltip-positioner">
|
|
55
55
|
<BaseTooltip.Popup role="tooltip" className="tooltip-popup">
|
|
56
|
-
|
|
56
|
+
{/* Not Base UI's Viewport: that one morphs between the triggers of one popup and
|
|
57
|
+
measures and clones its content to do so, while every hint here has a root of its own. */}
|
|
58
|
+
<div className="max-w-72 px-[9px] py-[5px] text-pretty">
|
|
57
59
|
<span className="wrap-anywhere">{label}</span>
|
|
58
|
-
{/* The viewport wraps its children in a div of its own, so the 8px
|
|
59
|
-
between the label and the shortcut has to sit on the shortcut itself. */}
|
|
60
60
|
{kbd !== undefined &&
|
|
61
61
|
(typeof kbd === 'string' ? (
|
|
62
62
|
<Kbd variant="inline" className="ml-2">
|
|
@@ -65,7 +65,7 @@ export function Tooltip({ label, open, onOpenChange, anchor, kbd, side = 'top',
|
|
|
65
65
|
) : (
|
|
66
66
|
<Kbd shortcut={kbd} variant="inline" className="ml-2" />
|
|
67
67
|
))}
|
|
68
|
-
</
|
|
68
|
+
</div>
|
|
69
69
|
</BaseTooltip.Popup>
|
|
70
70
|
</BaseTooltip.Positioner>
|
|
71
71
|
</BaseTooltip.Portal>
|
package/src/clipboard.ts
CHANGED
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
* and allowed, and there is nothing a person can do about a refusal, so both sides stay quiet.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/* Resolves to whether the text got there, for a caller that tells the person; it never rejects. */
|
|
7
|
+
export const copyText = async (text: string): Promise<boolean> => {
|
|
8
|
+
try {
|
|
9
|
+
if (!navigator.clipboard) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
await navigator.clipboard.writeText(text);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
8
17
|
};
|
|
9
18
|
|
|
10
19
|
/* What is on the clipboard, or nothing at all when the browser will not say. */
|
package/src/dialog/parts.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createContext, useContext, useState, type ComponentProps } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
-
import { Dialog as BaseDialog } from '@base-ui
|
|
4
|
-
import { useRender } from '@base-ui
|
|
3
|
+
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
|
4
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
5
5
|
import { withClass } from '../class-name.ts';
|
|
6
6
|
import { useDialogLayer } from '../dialog-layer.ts';
|
|
7
7
|
|
package/src/file-tree/helpers.ts
CHANGED
|
@@ -21,6 +21,16 @@ export function visibleRows(model: FileTree): FileTreeVisibleRow[] {
|
|
|
21
21
|
return model.getVisibleRows(0, model.getVisibleCount()).map((row) => ({ ...row, path: pathOfRow(row) }));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/* The `count` rows from `start` and one on each side, for a neighbor's selection, without copying the whole list. */
|
|
25
|
+
export function visibleRowsAround(model: FileTree, start: number, count: number): FileTreeVisibleRow[] {
|
|
26
|
+
const first = Math.max(0, start - 1);
|
|
27
|
+
const last = Math.min(model.getVisibleCount() - 1, start + count);
|
|
28
|
+
if (last < first) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return model.getVisibleRows(first, last).map((row) => ({ ...row, path: pathOfRow(row) }));
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
export function applyExpansion(model: FileTree, collapsed: ReadonlySet<string>, keyOf?: FoldKeyOf): void {
|
|
25
35
|
for (let pass = 0; pass < EXPANSION_PASSES; pass++) {
|
|
26
36
|
const { collapse, expand } = expansionChanges(visibleRows(model), collapsed, keyOf);
|
package/src/file-tree/parts.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
-
import { useRender } from '@base-ui
|
|
3
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
4
4
|
import { FileTree as PierreFileTree, type FileTreeProps as PierreProps } from '@pierre/trees/react';
|
|
5
5
|
import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useId, useState } from 'react';
|
|
2
2
|
import { flushSync } from 'react-dom';
|
|
3
3
|
import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
|
|
4
|
-
import { pathOfRow, visibleRows } from './helpers.ts';
|
|
4
|
+
import { pathOfRow, visibleRows, visibleRowsAround } from './helpers.ts';
|
|
5
5
|
import { FILE_TREE_CSS } from './style.ts';
|
|
6
6
|
|
|
7
7
|
export interface RowSlot {
|
|
@@ -10,7 +10,7 @@ export interface RowSlot {
|
|
|
10
10
|
decoration: HTMLElement;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
interface Port {
|
|
13
|
+
export interface Port {
|
|
14
14
|
rowElement: HTMLElement;
|
|
15
15
|
control: HTMLElement;
|
|
16
16
|
decoration: HTMLElement;
|
|
@@ -18,6 +18,66 @@ interface Port {
|
|
|
18
18
|
lightDecoration: HTMLElement;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/*
|
|
22
|
+
* The rows of the mounted window, read from the spacer beta.6 puts above it (its first index times the
|
|
23
|
+
* row height), so a scroll frame copies those and not every visible row. The whole list stands in when
|
|
24
|
+
* the spacer is missing or the model already moved past what is mounted.
|
|
25
|
+
*/
|
|
26
|
+
function mountedRows(model: FileTree, shadow: ShadowRoot, elements: readonly HTMLElement[]): FileTreeVisibleRow[] {
|
|
27
|
+
const spacer = shadow.querySelector<HTMLElement>('[data-file-tree-virtualized-sticky-offset]');
|
|
28
|
+
const offset = spacer === null ? Number.NaN : Number.parseFloat(spacer.style.height);
|
|
29
|
+
const itemHeight = model.getItemHeight();
|
|
30
|
+
if (!Number.isFinite(offset) || itemHeight <= 0) {
|
|
31
|
+
return visibleRows(model);
|
|
32
|
+
}
|
|
33
|
+
// A folder pinned to the top leaves the flow, but still takes its index in the window.
|
|
34
|
+
const pinned = shadow.querySelectorAll('[data-file-tree-sticky-row="true"]').length;
|
|
35
|
+
const rows = visibleRowsAround(model, Math.round(offset / itemHeight), elements.length + pinned);
|
|
36
|
+
const paths = new Set(rows.map((row) => row.path));
|
|
37
|
+
return elements.every((element) => element.dataset.itemPath === undefined || paths.has(element.dataset.itemPath)) ? rows : visibleRows(model);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* Lines the ports up with their rows: every size first, then the spaces they reserve, then every offset,
|
|
42
|
+
* then the ports, so a pass forces two layouts at most however many rows are mounted.
|
|
43
|
+
*/
|
|
44
|
+
export function positionPorts(ports: Iterable<Port>): void {
|
|
45
|
+
const spaced = [...ports].map((port) => ({
|
|
46
|
+
port,
|
|
47
|
+
control: port.lightControl.childNodes.length > 0 ? Math.ceil(port.lightControl.getBoundingClientRect().width) + 6 : 0,
|
|
48
|
+
decoration: port.lightDecoration.childNodes.length > 0 ? Math.ceil(port.lightDecoration.getBoundingClientRect().width) + 6 : 0
|
|
49
|
+
}));
|
|
50
|
+
for (const { port, control, decoration } of spaced) {
|
|
51
|
+
setSpace(port.rowElement, '--adecore-tree-control-space', control);
|
|
52
|
+
setSpace(port.rowElement, '--adecore-tree-decoration-space', decoration);
|
|
53
|
+
}
|
|
54
|
+
const placed = spaced.map(({ port, control, decoration }) => {
|
|
55
|
+
const row = port.rowElement;
|
|
56
|
+
const content = row.querySelector<HTMLElement>('[data-item-section="content"]');
|
|
57
|
+
const icon = row.querySelector<HTMLElement>('[data-item-section="icon"]');
|
|
58
|
+
return {
|
|
59
|
+
port,
|
|
60
|
+
top: `${row.offsetTop}px`,
|
|
61
|
+
controlLeft: `${row.offsetLeft + (icon?.offsetLeft ?? 4) - control}px`,
|
|
62
|
+
decorationLeft: `${row.offsetLeft + (content === null ? row.offsetWidth - decoration - 2 : content.offsetLeft + content.offsetWidth + 6)}px`
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
for (const { port, top, controlLeft, decorationLeft } of placed) {
|
|
66
|
+
port.control.style.top = top;
|
|
67
|
+
port.control.style.left = controlLeft;
|
|
68
|
+
port.decoration.style.top = top;
|
|
69
|
+
port.decoration.style.left = decorationLeft;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// An unchanged space is left alone, so a pass that only scrolled measures once.
|
|
74
|
+
function setSpace(row: HTMLElement, name: string, space: number): void {
|
|
75
|
+
const value = `${space}px`;
|
|
76
|
+
if (row.style.getPropertyValue(name) !== value) {
|
|
77
|
+
row.style.setProperty(name, value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
21
81
|
// beta.6 owns button rows. Sibling slots keep real controls outside those buttons and in the app's stylesheet.
|
|
22
82
|
export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, label: string) {
|
|
23
83
|
const prefix = useId().replaceAll(':', '');
|
|
@@ -35,23 +95,7 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
35
95
|
let pending = false;
|
|
36
96
|
let snapshot: string | null = null;
|
|
37
97
|
|
|
38
|
-
const position = (): void =>
|
|
39
|
-
for (const port of ports.values()) {
|
|
40
|
-
const row = port.rowElement;
|
|
41
|
-
const content = row.querySelector<HTMLElement>('[data-item-section="content"]');
|
|
42
|
-
const icon = row.querySelector<HTMLElement>('[data-item-section="icon"]');
|
|
43
|
-
const control = port.lightControl;
|
|
44
|
-
const decoration = port.lightDecoration;
|
|
45
|
-
const controlSpace = control.childNodes.length > 0 ? Math.ceil(control.getBoundingClientRect().width) + 6 : 0;
|
|
46
|
-
const decorationSpace = decoration.childNodes.length > 0 ? Math.ceil(decoration.getBoundingClientRect().width) + 6 : 0;
|
|
47
|
-
row.style.setProperty('--adecore-tree-control-space', `${controlSpace}px`);
|
|
48
|
-
row.style.setProperty('--adecore-tree-decoration-space', `${decorationSpace}px`);
|
|
49
|
-
port.control.style.top = `${row.offsetTop}px`;
|
|
50
|
-
port.control.style.left = `${row.offsetLeft + (icon?.offsetLeft ?? 4) - controlSpace}px`;
|
|
51
|
-
port.decoration.style.top = `${row.offsetTop}px`;
|
|
52
|
-
port.decoration.style.left = `${row.offsetLeft + (content === null ? row.offsetWidth - decorationSpace - 2 : content.offsetLeft + content.offsetWidth + 6)}px`;
|
|
53
|
-
}
|
|
54
|
-
};
|
|
98
|
+
const position = (): void => positionPorts(ports.values());
|
|
55
99
|
|
|
56
100
|
const resize = new ResizeObserver(position);
|
|
57
101
|
const dropPort = (port: Port): void => {
|
|
@@ -106,11 +150,12 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
106
150
|
resize.observe(current);
|
|
107
151
|
}
|
|
108
152
|
shadow.querySelector('[role="tree"]')?.setAttribute('aria-label', label);
|
|
109
|
-
const rows = visibleRows(model);
|
|
110
|
-
const byPath = new Map(rows.map((row) => [pathOfRow(row), row]));
|
|
111
153
|
const elements = [
|
|
112
154
|
...shadow.querySelectorAll<HTMLElement>('[data-type="item"]:not([data-item-parked="true"]):not([data-file-tree-sticky-row="true"])')
|
|
113
155
|
];
|
|
156
|
+
const rows = mountedRows(model, shadow, elements);
|
|
157
|
+
const byPath = new Map(rows.map((row) => [pathOfRow(row), row]));
|
|
158
|
+
const byIndex = new Map(rows.map((row) => [row.index, row]));
|
|
114
159
|
const live = new Set(elements);
|
|
115
160
|
for (const [element, port] of ports) {
|
|
116
161
|
if (!live.has(element)) {
|
|
@@ -126,8 +171,8 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
126
171
|
continue;
|
|
127
172
|
}
|
|
128
173
|
element.dataset.interactive = 'true';
|
|
129
|
-
element.dataset.joinedStart = String(row.isSelected &&
|
|
130
|
-
element.dataset.joinedEnd = String(row.isSelected &&
|
|
174
|
+
element.dataset.joinedStart = String(row.isSelected && byIndex.get(row.index - 1)?.isSelected === true);
|
|
175
|
+
element.dataset.joinedEnd = String(row.isSelected && byIndex.get(row.index + 1)?.isSelected === true);
|
|
131
176
|
let port = ports.get(element);
|
|
132
177
|
if (port === undefined) {
|
|
133
178
|
serial++;
|
package/src/format/datetime.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatAgo } from './duration.ts';
|
|
1
2
|
import { dateFormatter, wordFormatter } from './locale.ts';
|
|
2
3
|
|
|
3
4
|
/* The parts that are a word rather than a number, and so come from the language and not the region. */
|
|
@@ -63,3 +64,19 @@ export const isSameDay = (a: Date | number, b: Date | number): boolean => {
|
|
|
63
64
|
};
|
|
64
65
|
|
|
65
66
|
export const formatMoment = (at: Date | number, now: Date | number = Date.now()): string => (isSameDay(at, now) ? formatClock(at) : formatDayClock(at));
|
|
67
|
+
|
|
68
|
+
const WEEK = 7 * 24 * 60 * 60 * 1000;
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
* How old a row is, for its right edge: `3d ago` for the first week, then the day, with the year once
|
|
72
|
+
* it is not this one. An `at` ahead of `now`, from a clock that runs ahead, reads as now.
|
|
73
|
+
*/
|
|
74
|
+
export const formatAge = (at: Date | number, now: Date | number = Date.now()): string => {
|
|
75
|
+
const date = toDate(at);
|
|
76
|
+
const today = toDate(now);
|
|
77
|
+
const past = today.getTime() - date.getTime();
|
|
78
|
+
if (past < WEEK) {
|
|
79
|
+
return formatAgo(past);
|
|
80
|
+
}
|
|
81
|
+
return date.getFullYear() === today.getFullYear() ? formatDay(date) : formatDayWithYear(date);
|
|
82
|
+
};
|
package/src/format/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type {
|
|
|
25
25
|
} from './menu/parts.tsx';
|
|
26
26
|
export type { KeyValueListItemProps, KeyValueListNameProps, KeyValueListRootProps, KeyValueListValueProps } from './key-value-list/parts.tsx';
|
|
27
27
|
export type { PopoverPopupProps, PopupVariant, PreviewCardPopupProps } from './popover/parts.tsx';
|
|
28
|
-
export type { TabsCountProps, TabsListProps, TabsRootProps, TabsTabProps, TabsValue } from './tabs/parts.tsx';
|
|
28
|
+
export type { TabsCountProps, TabsListProps, TabsRootProps, TabsTabProps, TabsValue, TabsVariant } from './tabs/parts.tsx';
|
|
29
29
|
|
|
30
30
|
export { AccentSwatches, type AccentSwatchesProps } from './AccentSwatches.tsx';
|
|
31
31
|
export { Banner, type BannerProps, type BannerTone } from './Banner.tsx';
|
|
@@ -49,11 +49,12 @@ export { Input, TextArea, type InputProps, type TextAreaProps } from './Input.ts
|
|
|
49
49
|
export { Kbd, KeyCap, Keys, type KbdProps, type KeyCapProps, type KeysProps } from './Kbd.tsx';
|
|
50
50
|
export { ListRow, type ListRowProps } from './ListRow.tsx';
|
|
51
51
|
export { Meter, type MeterProps } from './Meter.tsx';
|
|
52
|
+
export { MiddleTruncate, type MiddleTruncateProps } from './MiddleTruncate.tsx';
|
|
52
53
|
export { PanelEmpty, type PanelEmptyProps } from './PanelEmpty.tsx';
|
|
53
54
|
export { PanelHeader, type PanelHeaderProps } from './PanelHeader.tsx';
|
|
54
55
|
export { Pill, type PillProps } from './Pill.tsx';
|
|
55
56
|
export { PromptDialog, type PromptDialogProps } from './PromptDialog.tsx';
|
|
56
|
-
export { ProjectSwitcher, type ProjectSwitcherItem, type ProjectSwitcherProps } from './ProjectSwitcher.tsx';
|
|
57
|
+
export { ProjectSwitcher, ProjectSwitcherGroups, type ProjectSwitcherItem, type ProjectSwitcherProps } from './ProjectSwitcher.tsx';
|
|
57
58
|
export { SectionLabel, type SectionLabelProps } from './SectionLabel.tsx';
|
|
58
59
|
export { SegmentBar, type SegmentBarPart, type SegmentBarProps } from './SegmentBar.tsx';
|
|
59
60
|
export { Segmented, type SegmentedOption, type SegmentedProps } from './Segmented.tsx';
|
|
@@ -128,6 +129,8 @@ export { Workspace, type WorkspaceProps } from './workspace/Workspace.tsx';
|
|
|
128
129
|
export type { WorkspaceLayout } from './workspace/context.ts';
|
|
129
130
|
export { DocumentTab, TabStrip, type DocumentTabProps, type TabStripProps, type TabStripItem } from './workspace/TabStrip.tsx';
|
|
130
131
|
export { SplitView, type SplitViewProps, type SplitViewInfo, type SplitViewBounds, type SplitDropTarget, type SplitDrag } from './workspace/SplitView.tsx';
|
|
132
|
+
export { DropPreview, type DropPreviewProps } from './workspace/DropPreview.tsx';
|
|
133
|
+
export { dropPreviewPath, rectDropPreview, sameDropPreview, tabDropPreview, type DropPreviewShape, type TabDrop } from './workspace/preview.ts';
|
|
131
134
|
export {
|
|
132
135
|
createSplitLayout,
|
|
133
136
|
normalizeSplitLayout,
|
|
@@ -141,4 +144,4 @@ export {
|
|
|
141
144
|
type SplitLayout,
|
|
142
145
|
type SplitCommand
|
|
143
146
|
} from './workspace/model.ts';
|
|
144
|
-
export type
|
|
147
|
+
export { tabGapAt, type SplitRect, type SplitEdges, type SplitMinimum } from './workspace/geometry.ts';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import { useRender } from '@base-ui
|
|
2
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
3
3
|
|
|
4
4
|
export type KeyValueListRootProps = useRender.ComponentProps<'dl'> & {
|
|
5
5
|
/* A hairline between rows, for a long list such as the headers of a message. Without it the rows sit close, for a few facts in a card. */
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
action: {
|
|
3
|
+
cancel: 'Cancel',
|
|
4
|
+
copy: 'Copy',
|
|
5
|
+
copyError: 'Copy error',
|
|
6
|
+
dismiss: 'Dismiss',
|
|
7
|
+
reload: 'Reload',
|
|
8
|
+
retry: 'Try again',
|
|
9
|
+
select: 'Select',
|
|
10
|
+
selectAll: 'Select all'
|
|
11
|
+
},
|
|
12
|
+
accent: {
|
|
13
|
+
more: 'More colors'
|
|
14
|
+
},
|
|
15
|
+
settings: {
|
|
16
|
+
title: 'Settings',
|
|
17
|
+
section: 'Settings section',
|
|
18
|
+
sections: 'Settings sections',
|
|
19
|
+
close: 'Close settings',
|
|
20
|
+
failed: 'This settings page failed to render',
|
|
21
|
+
search: {
|
|
22
|
+
placeholder: 'Search settings',
|
|
23
|
+
clear: 'Clear search',
|
|
24
|
+
none: 'Nothing matches "{{query}}".',
|
|
25
|
+
results: 'Search results',
|
|
26
|
+
pane: 'Section'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
controls: {
|
|
30
|
+
stepper: {
|
|
31
|
+
smaller: 'Smaller',
|
|
32
|
+
larger: 'Larger',
|
|
33
|
+
smallerFor: '{{label}}: smaller',
|
|
34
|
+
largerFor: '{{label}}: larger'
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
select: {
|
|
38
|
+
search: 'Search',
|
|
39
|
+
noMatch: 'No option matches “{{query}}”'
|
|
40
|
+
},
|
|
41
|
+
iconPicker: {
|
|
42
|
+
label: 'Icon',
|
|
43
|
+
none: 'No icon',
|
|
44
|
+
search: 'Find an icon',
|
|
45
|
+
clear: 'Clear search',
|
|
46
|
+
noMatch: 'No icon matches “{{query}}”'
|
|
47
|
+
},
|
|
48
|
+
zoom: {
|
|
49
|
+
out: 'Zoom out',
|
|
50
|
+
in: 'Zoom in',
|
|
51
|
+
presets: 'Zoom presets',
|
|
52
|
+
fit: 'Zoom to fit',
|
|
53
|
+
fitEverything: 'Fit everything'
|
|
54
|
+
},
|
|
55
|
+
waveform: {
|
|
56
|
+
position: '{{time}} of {{duration}}'
|
|
57
|
+
},
|
|
58
|
+
projectSwitcher: {
|
|
59
|
+
label: 'Projects',
|
|
60
|
+
recent: 'Recent projects',
|
|
61
|
+
actionsFor: 'Actions for {{name}}'
|
|
62
|
+
},
|
|
63
|
+
tabs: {
|
|
64
|
+
more: 'More',
|
|
65
|
+
close: 'Close tab'
|
|
66
|
+
},
|
|
67
|
+
tree: {
|
|
68
|
+
label: 'Files',
|
|
69
|
+
expand: 'Expand',
|
|
70
|
+
collapse: 'Collapse'
|
|
71
|
+
},
|
|
72
|
+
workspace: {
|
|
73
|
+
tabs: 'Open views',
|
|
74
|
+
unsaved: 'Unsaved changes',
|
|
75
|
+
closeTab: 'Close {{name}}',
|
|
76
|
+
resize: 'Resize panels',
|
|
77
|
+
pane: 'Panel',
|
|
78
|
+
movePane: 'Move panel',
|
|
79
|
+
maximize: 'Maximize panel',
|
|
80
|
+
restore: 'Restore panels'
|
|
81
|
+
}
|
|
82
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
action: {
|
|
3
|
+
cancel: 'Annuleren',
|
|
4
|
+
copy: 'Kopiëren',
|
|
5
|
+
copyError: 'Fout kopiëren',
|
|
6
|
+
dismiss: 'Sluiten',
|
|
7
|
+
reload: 'Herladen',
|
|
8
|
+
retry: 'Opnieuw proberen',
|
|
9
|
+
select: 'Selecteren',
|
|
10
|
+
selectAll: 'Alles selecteren'
|
|
11
|
+
},
|
|
12
|
+
accent: {
|
|
13
|
+
more: 'Meer kleuren'
|
|
14
|
+
},
|
|
15
|
+
settings: {
|
|
16
|
+
title: 'Instellingen',
|
|
17
|
+
section: 'Onderdeel van de instellingen',
|
|
18
|
+
sections: 'Onderdelen van de instellingen',
|
|
19
|
+
close: 'Instellingen sluiten',
|
|
20
|
+
failed: 'Deze instellingenpagina kon niet worden getekend',
|
|
21
|
+
search: {
|
|
22
|
+
placeholder: 'Instellingen zoeken',
|
|
23
|
+
clear: 'Zoekopdracht wissen',
|
|
24
|
+
none: 'Niets komt overeen met "{{query}}".',
|
|
25
|
+
results: 'Zoekresultaten',
|
|
26
|
+
pane: 'Onderdeel'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
controls: {
|
|
30
|
+
stepper: {
|
|
31
|
+
smaller: 'Kleiner',
|
|
32
|
+
larger: 'Groter',
|
|
33
|
+
smallerFor: '{{label}}: kleiner',
|
|
34
|
+
largerFor: '{{label}}: groter'
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
select: {
|
|
38
|
+
search: 'Zoeken',
|
|
39
|
+
noMatch: 'Geen optie past bij “{{query}}”'
|
|
40
|
+
},
|
|
41
|
+
iconPicker: {
|
|
42
|
+
label: 'Icoon',
|
|
43
|
+
none: 'Geen icoon',
|
|
44
|
+
search: 'Zoek een icoon',
|
|
45
|
+
clear: 'Zoekopdracht wissen',
|
|
46
|
+
noMatch: 'Geen icoon past bij “{{query}}”'
|
|
47
|
+
},
|
|
48
|
+
zoom: {
|
|
49
|
+
out: 'Uitzoomen',
|
|
50
|
+
in: 'Inzoomen',
|
|
51
|
+
presets: 'Zoomstanden',
|
|
52
|
+
fit: 'Zoomen tot alles past',
|
|
53
|
+
fitEverything: 'Alles laten passen'
|
|
54
|
+
},
|
|
55
|
+
waveform: {
|
|
56
|
+
position: '{{time}} van {{duration}}'
|
|
57
|
+
},
|
|
58
|
+
projectSwitcher: {
|
|
59
|
+
label: 'Projecten',
|
|
60
|
+
recent: 'Recente projecten',
|
|
61
|
+
actionsFor: 'Acties voor {{name}}'
|
|
62
|
+
},
|
|
63
|
+
tabs: {
|
|
64
|
+
more: 'Meer',
|
|
65
|
+
close: 'Tabblad sluiten'
|
|
66
|
+
},
|
|
67
|
+
tree: {
|
|
68
|
+
label: 'Bestanden',
|
|
69
|
+
expand: 'Uitvouwen',
|
|
70
|
+
collapse: 'Invouwen'
|
|
71
|
+
},
|
|
72
|
+
workspace: {
|
|
73
|
+
tabs: 'Open views',
|
|
74
|
+
unsaved: 'Niet-opgeslagen wijzigingen',
|
|
75
|
+
closeTab: 'Sluit {{name}}',
|
|
76
|
+
resize: 'Panelen vergroten of verkleinen',
|
|
77
|
+
pane: 'Paneel',
|
|
78
|
+
movePane: 'Paneel verplaatsen',
|
|
79
|
+
maximize: 'Paneel maximaliseren',
|
|
80
|
+
restore: 'Panelen herstellen'
|
|
81
|
+
}
|
|
82
|
+
};
|
package/src/locales.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { i18n as I18n } from 'i18next';
|
|
2
|
-
|
|
3
|
-
import
|
|
2
|
+
/* The words are modules, not JSON: Node needs `with { type: 'json' }` on a JSON import, and a browser refuses it on the JavaScript Vite serves. */
|
|
3
|
+
import en from './locales/en.ts';
|
|
4
|
+
import nl from './locales/nl.ts';
|
|
4
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. */
|
|
6
7
|
export const UI_NAMESPACE = 'ui';
|
package/src/menu/parts.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createContext, useContext, type ComponentProps, type ReactNode, type Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { Check, ChevronRight, type LucideIcon } from 'lucide-react';
|
|
4
|
-
import { ContextMenu as BaseContextMenu } from '@base-ui
|
|
5
|
-
import { Menu as BaseMenu } from '@base-ui
|
|
6
|
-
import { Separator as BaseSeparator } from '@base-ui
|
|
7
|
-
import { useRender } from '@base-ui
|
|
4
|
+
import { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu';
|
|
5
|
+
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
|
6
|
+
import { Separator as BaseSeparator } from '@base-ui/react/separator';
|
|
7
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
8
8
|
import { withClass } from '../class-name.ts';
|
|
9
9
|
import { Icon } from '../Icon.tsx';
|
|
10
10
|
import { Tooltip } from '../Tooltip.tsx';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface MiddleCut {
|
|
2
|
+
head: string;
|
|
3
|
+
/* What the ellipsis stands for; still in the text, so a copy or a find reads it. */
|
|
4
|
+
middle: string;
|
|
5
|
+
tail: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const MIDDLE_ELLIPSIS = '…';
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* The head and tail of `text` that keep the most of it around an ellipsis within `width`, or null
|
|
12
|
+
* when the whole text fits. The tail gets the odd character, since the end of a path or a branch is
|
|
13
|
+
* what tells two of them apart. Code points, never UTF-16 units, so a cut never halves a character.
|
|
14
|
+
*/
|
|
15
|
+
export function fitMiddle(text: string, width: number, measure: (text: string) => number): MiddleCut | null {
|
|
16
|
+
if (measure(text) <= width) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const characters = Array.from(text);
|
|
20
|
+
const cutAt = (kept: number): MiddleCut => {
|
|
21
|
+
const headLength = Math.floor(kept / 2);
|
|
22
|
+
const tailStart = characters.length - (kept - headLength);
|
|
23
|
+
return {
|
|
24
|
+
head: characters.slice(0, headLength).join(''),
|
|
25
|
+
middle: characters.slice(headLength, tailStart).join(''),
|
|
26
|
+
tail: characters.slice(tailStart).join('')
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const fits = (cut: MiddleCut): boolean => measure(cut.head + MIDDLE_ELLIPSIS + cut.tail) <= width;
|
|
30
|
+
let low = 0;
|
|
31
|
+
let high = characters.length - 1;
|
|
32
|
+
while (low < high) {
|
|
33
|
+
const kept = Math.ceil((low + high) / 2);
|
|
34
|
+
if (fits(cutAt(kept))) {
|
|
35
|
+
low = kept;
|
|
36
|
+
} else {
|
|
37
|
+
high = kept - 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return cutAt(low);
|
|
41
|
+
}
|
package/src/popover/parts.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentProps } from 'react';
|
|
2
|
-
import { Popover as BasePopover } from '@base-ui
|
|
3
|
-
import { PreviewCard as BasePreviewCard } from '@base-ui
|
|
2
|
+
import { Popover as BasePopover } from '@base-ui/react/popover';
|
|
3
|
+
import { PreviewCard as BasePreviewCard } from '@base-ui/react/preview-card';
|
|
4
4
|
import { withClass } from '../class-name.ts';
|
|
5
5
|
import { splitPlacement, type PlacementKey } from '../placement.ts';
|
|
6
6
|
import { renderPositioner } from '../popup-layer.ts';
|
package/src/prefetch.ts
CHANGED
|
@@ -16,7 +16,7 @@ export class Prefetcher {
|
|
|
16
16
|
private running: Promise<void> | null = null;
|
|
17
17
|
private loading = false;
|
|
18
18
|
private readonly whenIdle: WhenIdle;
|
|
19
|
-
/* Asked on every
|
|
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
|
|
|
22
22
|
constructor(whenIdle: WhenIdle, allowed: () => boolean = () => true) {
|
|
@@ -78,6 +78,11 @@ export class Prefetcher {
|
|
|
78
78
|
try {
|
|
79
79
|
for (let load = this.queue.shift(); load; load = this.queue.shift()) {
|
|
80
80
|
await new Promise<void>((resolve) => this.whenIdle(resolve));
|
|
81
|
+
if (!this.allowed()) {
|
|
82
|
+
// Data saving came on while it waited; a later ask may queue it again.
|
|
83
|
+
this.queued.delete(load);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
81
86
|
this.loading = true;
|
|
82
87
|
try {
|
|
83
88
|
await load();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Ref } from 'react';
|
|
1
2
|
import { PromptDialog } from '../PromptDialog.tsx';
|
|
2
3
|
|
|
3
4
|
export interface ConfirmDialogProps {
|
|
@@ -8,12 +9,17 @@ export interface ConfirmDialogProps {
|
|
|
8
9
|
confirmLabel: string;
|
|
9
10
|
/* Closes the dialog once it resolves; a rejection stays on screen as the reason. */
|
|
10
11
|
onConfirm(): Promise<void>;
|
|
12
|
+
/* Extra on the popup. */
|
|
13
|
+
className?: string;
|
|
14
|
+
ref?: Ref<HTMLDivElement>;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
/* One destructive step, asked before it happens, over whichever dialog it was opened from. */
|
|
14
|
-
export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, onConfirm }: ConfirmDialogProps) {
|
|
18
|
+
export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, onConfirm, className, ref }: ConfirmDialogProps) {
|
|
15
19
|
return (
|
|
16
20
|
<PromptDialog
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={className}
|
|
17
23
|
open={open}
|
|
18
24
|
nested
|
|
19
25
|
danger
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Suspense, useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode, type Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { Tabs } from '@base-ui
|
|
4
|
+
import { Tabs } from '@base-ui/react/tabs';
|
|
5
5
|
import { Search, X, type LucideIcon } from 'lucide-react';
|
|
6
6
|
import { CloseButton } from '../CloseButton.tsx';
|
|
7
7
|
import { DialogDescription, DialogPopup, DialogRoot, DialogTitle } from '../dialog/parts.tsx';
|