@adecore/ui 0.23.1 → 0.24.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/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 +27 -10
- 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 +43 -8
- package/dist/workspace/Workspace.d.ts +2 -2
- package/dist/workspace/Workspace.js +4 -2
- package/dist/workspace/geometry.d.ts +11 -0
- package/dist/workspace/geometry.js +19 -0
- 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 +27 -10
- 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 +44 -8
- package/src/workspace/Workspace.tsx +7 -3
- package/src/workspace/geometry.ts +26 -0
- 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/PanelHeader.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
-
import { useRender } from '@base-ui
|
|
3
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
4
4
|
import { SectionLabel } from './SectionLabel.tsx';
|
|
5
5
|
|
|
6
6
|
export type PanelHeaderProps = useRender.ComponentProps<'header'> & {
|
package/src/SectionLabel.tsx
CHANGED
|
@@ -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
|
/* A label takes the line height of the row it sits in, which is why the size carries `/[inherit]`. */
|
|
5
5
|
const SECTION_LABEL = 'text-xs/[inherit] font-medium text-text-faint';
|
package/src/Select.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Ref } from 'react';
|
|
2
|
-
import { Select as BaseSelect } from '@base-ui
|
|
2
|
+
import { Select as BaseSelect } from '@base-ui/react/select';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { ChevronDown } from 'lucide-react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
package/src/SelectSearch.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { Combobox } from '@base-ui
|
|
2
|
+
import { Combobox } from '@base-ui/react/combobox';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { ChevronDown, Search } from 'lucide-react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
package/src/Slider.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useId, type Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
-
import { Slider as BaseSlider } from '@base-ui
|
|
3
|
+
import { Slider as BaseSlider } from '@base-ui/react/slider';
|
|
4
4
|
import { formatFixed } from './format/number.ts';
|
|
5
5
|
import { placesOf } from './step-places.ts';
|
|
6
6
|
|
package/src/Surface.tsx
CHANGED
|
@@ -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
|
const FLOAT = 'border border-border bg-[color-mix(in_srgb,var(--surface-raised)_88%,transparent)] shadow-float backdrop-blur-[14px]';
|
|
5
5
|
|
package/src/Switch.tsx
CHANGED
package/src/Toasts.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useState, type ReactNode, type Ref } from 'react';
|
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { CircleAlert, CircleCheck, Trash, X } from 'lucide-react';
|
|
5
|
+
import { Button } from './Button.tsx';
|
|
5
6
|
import { Icon } from './Icon.tsx';
|
|
6
7
|
import { IconButton } from './IconButton.tsx';
|
|
7
8
|
import { Kbd } from './Kbd.tsx';
|
|
@@ -57,9 +58,9 @@ function TimerRing({ deadline, onDismiss }: { deadline: ToastDeadline; onDismiss
|
|
|
57
58
|
function ActionButton({ action }: { action: ToastAction }) {
|
|
58
59
|
return (
|
|
59
60
|
<span className="flex items-center gap-2">
|
|
60
|
-
<
|
|
61
|
+
<Button size="xs" onClick={action.run}>
|
|
61
62
|
{action.label}
|
|
62
|
-
</
|
|
63
|
+
</Button>
|
|
63
64
|
{action.shortcut && <Kbd shortcut={action.shortcut} className="font-sans text-xs text-text-faint" />}
|
|
64
65
|
</span>
|
|
65
66
|
);
|
|
@@ -90,7 +91,7 @@ function ToastCard<T extends Toast>({ toast, footer, onDismiss }: { toast: T; fo
|
|
|
90
91
|
)}
|
|
91
92
|
{/* Two or more would squeeze the title beside them, so they get a line of their own. */}
|
|
92
93
|
{actions.length > 1 && (
|
|
93
|
-
<span className="mt-1 flex flex-wrap items-center gap-x-
|
|
94
|
+
<span className="mt-1 -ml-2 flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
94
95
|
{actions.map((action) => (
|
|
95
96
|
<ActionButton key={action.label} action={action} />
|
|
96
97
|
))}
|
|
@@ -133,9 +134,7 @@ export interface ToastsProps<T extends Toast> {
|
|
|
133
134
|
*/
|
|
134
135
|
export function Toasts<T extends Toast>({ store, footer, className, ref }: ToastsProps<T>) {
|
|
135
136
|
const toasts = store((state) => state.toasts);
|
|
136
|
-
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
137
|
+
// Stays mounted with nothing in it: a screen reader skips what a live region says the moment it is inserted.
|
|
139
138
|
return (
|
|
140
139
|
<div
|
|
141
140
|
ref={ref}
|
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';
|