@adecore/ui 0.24.1 → 0.25.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/Button.d.ts +3 -1
- package/dist/Button.js +7 -2
- package/dist/EmptyState.d.ts +1 -3
- package/dist/EmptyState.js +2 -2
- package/dist/IconButton.d.ts +1 -3
- package/dist/IconButton.js +2 -2
- package/dist/LazyBoundary.d.ts +6 -0
- package/dist/LazyBoundary.js +12 -0
- package/dist/PanelEmpty.d.ts +1 -3
- package/dist/PanelEmpty.js +2 -2
- package/dist/file-tree/helpers.js +2 -2
- package/dist/format/datetime.d.ts +5 -4
- package/dist/format/datetime.js +11 -5
- package/dist/format/index.d.ts +3 -3
- package/dist/format/index.js +3 -3
- package/dist/format/locale.d.ts +1 -0
- package/dist/format/locale.js +9 -0
- package/dist/index.d.ts +10 -9
- package/dist/index.js +7 -8
- package/dist/lazy-landings.d.ts +3 -0
- package/dist/lazy-landings.js +18 -0
- package/dist/lazy.d.ts +4 -1
- package/dist/lazy.js +7 -1
- package/dist/settings/SettingsDialog.js +3 -2
- package/dist/workspace/model.d.ts +0 -2
- package/dist/workspace/model.js +0 -69
- package/package.json +1 -1
- package/src/Button.tsx +13 -3
- package/src/EmptyState.tsx +2 -4
- package/src/IconButton.tsx +1 -4
- package/src/LazyBoundary.tsx +17 -0
- package/src/PanelEmpty.tsx +2 -4
- package/src/file-tree/helpers.ts +2 -2
- package/src/format/datetime.ts +15 -5
- package/src/format/index.ts +2 -2
- package/src/format/locale.ts +11 -0
- package/src/index.ts +8 -10
- package/src/lazy-landings.ts +21 -0
- package/src/lazy.tsx +11 -2
- package/src/settings/SettingsDialog.tsx +5 -4
- package/src/workspace/model.ts +0 -70
package/dist/Button.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ComponentProps } from 'react';
|
|
2
2
|
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'danger-outline' | 'positive' | 'inverse';
|
|
3
3
|
export type ButtonSize = 'xs' | 'sm' | 'md';
|
|
4
|
+
export type ButtonShape = 'default' | 'pill';
|
|
4
5
|
export interface ButtonProps extends ComponentProps<'button'> {
|
|
5
6
|
variant?: ButtonVariant;
|
|
6
7
|
size?: ButtonSize;
|
|
8
|
+
shape?: ButtonShape;
|
|
7
9
|
href?: string;
|
|
8
10
|
}
|
|
9
|
-
export declare function Button({ variant, size, className, href, type, ref, ...rest }: ButtonProps): import("react").JSX.Element;
|
|
11
|
+
export declare function Button({ variant, size, shape, className, href, type, ref, ...rest }: ButtonProps): import("react").JSX.Element;
|
package/dist/Button.js
CHANGED
|
@@ -18,9 +18,14 @@ const SIZE = {
|
|
|
18
18
|
sm: 'h-7 px-2.5',
|
|
19
19
|
md: 'h-8 px-3'
|
|
20
20
|
};
|
|
21
|
+
/* A pill is one size of its own, for a control that sits in a round row, so it takes the place of `size`. */
|
|
22
|
+
const SHAPE = {
|
|
23
|
+
default: 'rounded-md text-xs',
|
|
24
|
+
pill: 'h-9 rounded-full px-3.5 text-sm'
|
|
25
|
+
};
|
|
21
26
|
/* Every button with a word in it. A button that is only an icon is an `IconButton`, which is a square, not a label. */
|
|
22
|
-
export function Button({ variant = 'ghost', size = 'md', className, href, type = 'button', ref, ...rest }) {
|
|
23
|
-
const classes = clsx('inline-flex shrink-0 items-center justify-center gap-1.5
|
|
27
|
+
export function Button({ variant = 'ghost', size = 'md', shape = 'default', className, href, type = 'button', ref, ...rest }) {
|
|
28
|
+
const classes = clsx('inline-flex shrink-0 items-center justify-center gap-1.5 font-medium disabled:opacity-50', SHAPE[shape], VARIANT[variant], shape === 'default' && SIZE[size], className);
|
|
24
29
|
if (href) {
|
|
25
30
|
return (_jsx("a", { ref: ref, className: classes, href: href, target: "_blank", rel: "noreferrer", ...rest }));
|
|
26
31
|
}
|
package/dist/EmptyState.d.ts
CHANGED
|
@@ -5,12 +5,10 @@ export interface EmptyStateProps {
|
|
|
5
5
|
size?: number;
|
|
6
6
|
}>;
|
|
7
7
|
busy?: boolean;
|
|
8
|
-
/** @deprecated Turns a Lucide icon; use `busy`, which draws a `Spinner` in its place. */
|
|
9
|
-
spin?: boolean;
|
|
10
8
|
title?: ReactNode;
|
|
11
9
|
children: ReactNode;
|
|
12
10
|
action?: ReactNode;
|
|
13
11
|
className?: string;
|
|
14
12
|
ref?: Ref<HTMLDivElement>;
|
|
15
13
|
}
|
|
16
|
-
export declare function EmptyState({ icon, busy,
|
|
14
|
+
export declare function EmptyState({ icon, busy, title, children, action, className, ref }: EmptyStateProps): import("react").JSX.Element;
|
package/dist/EmptyState.js
CHANGED
|
@@ -6,6 +6,6 @@ import { Spinner } from './Spinner.js';
|
|
|
6
6
|
// One size in a panel, a card and a dialog alike, so no caller picks its own.
|
|
7
7
|
const ICON_SIZE = 20;
|
|
8
8
|
/* What a list, a canvas or a thread shows before it holds anything. */
|
|
9
|
-
export function EmptyState({ icon, busy = false,
|
|
10
|
-
return (_jsxs("div", { ref: ref, className: clsx('flex flex-col items-center justify-center gap-2 px-6 py-8 text-center', className), children: [(busy || icon) && (_jsx("span", { className: "text-text-faint", children: busy ? (_jsx(Spinner, { size: ICON_SIZE })) : isValidElement(icon) ? (cloneElement(icon, { size: ICON_SIZE })) : (icon && _jsx(Icon, { icon: icon, size: ICON_SIZE
|
|
9
|
+
export function EmptyState({ icon, busy = false, title, children, action, className, ref }) {
|
|
10
|
+
return (_jsxs("div", { ref: ref, className: clsx('flex flex-col items-center justify-center gap-2 px-6 py-8 text-center', className), children: [(busy || icon) && (_jsx("span", { className: "text-text-faint", children: busy ? (_jsx(Spinner, { size: ICON_SIZE })) : isValidElement(icon) ? (cloneElement(icon, { size: ICON_SIZE })) : (icon && _jsx(Icon, { icon: icon, size: ICON_SIZE })) })), title && _jsx("p", { className: "text-sm font-medium text-text", children: title }), _jsx("p", { className: "max-w-[280px] text-xs leading-snug text-text-muted", children: children }), action && _jsx("div", { className: "mt-2", children: action })] }));
|
|
11
11
|
}
|
package/dist/IconButton.d.ts
CHANGED
|
@@ -14,10 +14,8 @@ export interface IconButtonProps extends Omit<ComponentProps<'button'>, 'childre
|
|
|
14
14
|
size?: IconButtonSize;
|
|
15
15
|
active?: boolean;
|
|
16
16
|
busy?: boolean;
|
|
17
|
-
/** @deprecated Turns the icon; use `busy`, which draws a `Spinner` in its place. */
|
|
18
|
-
spin?: boolean;
|
|
19
17
|
iconClassName?: string;
|
|
20
18
|
children?: ReactNode;
|
|
21
19
|
render?: useRender.RenderProp;
|
|
22
20
|
}
|
|
23
|
-
export declare function IconButton({ icon, label, tooltip, kbd, tooltipSide, size, active, busy,
|
|
21
|
+
export declare function IconButton({ icon, label, tooltip, kbd, tooltipSide, size, active, busy, iconClassName, render, className, ref, children, ...props }: IconButtonProps): import("react").JSX.Element;
|
package/dist/IconButton.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Spinner } from './Spinner.js';
|
|
|
6
6
|
import { Tooltip } from './Tooltip.js';
|
|
7
7
|
import { ICON_BUTTON_CLASS, ICON_BUTTON_ICON_SIZE } from './icon-button-size.js';
|
|
8
8
|
/* Every button that is an icon and no word. A square, and the one place the icon inside it is sized. */
|
|
9
|
-
export function IconButton({ icon, label, tooltip, kbd, tooltipSide, size = 'md', active = false, busy = false,
|
|
9
|
+
export function IconButton({ icon, label, tooltip, kbd, tooltipSide, size = 'md', active = false, busy = false, iconClassName, render, className, ref, children, ...props }) {
|
|
10
10
|
const button = useRender({
|
|
11
11
|
render: render ?? _jsx("button", { type: "button" }),
|
|
12
12
|
ref,
|
|
@@ -15,7 +15,7 @@ export function IconButton({ icon, label, tooltip, kbd, tooltipSide, size = 'md'
|
|
|
15
15
|
'data-active': active ? 'true' : undefined,
|
|
16
16
|
...props,
|
|
17
17
|
className: clsx('icon-btn', ICON_BUTTON_CLASS[size], className),
|
|
18
|
-
children: (_jsxs(_Fragment, { children: [busy ? (_jsx(Spinner, { size: ICON_BUTTON_ICON_SIZE[size], className: iconClassName })) : (icon !== undefined && _jsx(Icon, { icon: icon, size: ICON_BUTTON_ICON_SIZE[size], className:
|
|
18
|
+
children: (_jsxs(_Fragment, { children: [busy ? (_jsx(Spinner, { size: ICON_BUTTON_ICON_SIZE[size], className: iconClassName })) : (icon !== undefined && _jsx(Icon, { icon: icon, size: ICON_BUTTON_ICON_SIZE[size], className: iconClassName })), children] }))
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
if (tooltip === false) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, useSyncExternalStore } from 'react';
|
|
3
|
+
import { readLandings, subscribeLandings } from './lazy-landings.js';
|
|
4
|
+
/*
|
|
5
|
+
* `Suspense` for what `lazyNamed` loads. React holds content that comes in as a retry back until
|
|
6
|
+
* 300 ms after its fallback showed; this boundary draws again as an ordinary update once a module
|
|
7
|
+
* lands, so a chunk that takes 20 ms shows after 20 ms.
|
|
8
|
+
*/
|
|
9
|
+
export function LazyBoundary({ fallback = null, children }) {
|
|
10
|
+
useSyncExternalStore(subscribeLandings, readLandings, readLandings);
|
|
11
|
+
return _jsx(Suspense, { fallback: fallback, children: children });
|
|
12
|
+
}
|
package/dist/PanelEmpty.d.ts
CHANGED
|
@@ -4,8 +4,6 @@ export interface PanelEmptyProps {
|
|
|
4
4
|
header?: ReactNode;
|
|
5
5
|
icon?: LucideIcon;
|
|
6
6
|
busy?: boolean;
|
|
7
|
-
/** @deprecated Turns the icon; use `busy`, which draws a `Spinner` in its place. */
|
|
8
|
-
spin?: boolean;
|
|
9
7
|
action?: ReactNode;
|
|
10
8
|
sunken?: boolean;
|
|
11
9
|
fill?: 'grow' | 'full';
|
|
@@ -13,4 +11,4 @@ export interface PanelEmptyProps {
|
|
|
13
11
|
className?: string;
|
|
14
12
|
ref?: Ref<HTMLDivElement>;
|
|
15
13
|
}
|
|
16
|
-
export declare function PanelEmpty({ header, icon, busy,
|
|
14
|
+
export declare function PanelEmpty({ header, icon, busy, action, sunken, fill, children, className, ref }: PanelEmptyProps): import("react").JSX.Element;
|
package/dist/PanelEmpty.js
CHANGED
|
@@ -2,6 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { EmptyState } from './EmptyState.js';
|
|
4
4
|
/* What a panel or a card shows while it holds nothing: a sentence, one icon, at most one button. */
|
|
5
|
-
export function PanelEmpty({ header, icon, busy = false,
|
|
6
|
-
return (_jsxs("div", { ref: ref, className: clsx('grid min-h-0 place-items-center', fill === 'full' ? 'h-full' : 'grow', sunken && 'bg-surface-sunken', className), children: [header, _jsx(EmptyState, { icon: icon, busy: busy,
|
|
5
|
+
export function PanelEmpty({ header, icon, busy = false, action, sunken = false, fill = 'grow', children, className, ref }) {
|
|
6
|
+
return (_jsxs("div", { ref: ref, className: clsx('grid min-h-0 place-items-center', fill === 'full' ? 'h-full' : 'grow', sunken && 'bg-surface-sunken', className), children: [header, _jsx(EmptyState, { icon: icon, busy: busy, action: action, children: children })] }));
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nameOrder } from '../format/locale.js';
|
|
1
2
|
// Expanded folders can reveal more flattened rows; cap custom-key cycles.
|
|
2
3
|
const EXPANSION_PASSES = 32;
|
|
3
4
|
export function directoryHandle(model, path) {
|
|
@@ -126,8 +127,7 @@ export function compareRows(left, right) {
|
|
|
126
127
|
if (leftIsDirectory !== rightIsDirectory) {
|
|
127
128
|
return leftIsDirectory ? -1 : 1;
|
|
128
129
|
}
|
|
129
|
-
|
|
130
|
-
return leftSegment.localeCompare(rightSegment, undefined, { numeric: true, sensitivity: 'base' }) || leftSegment.localeCompare(rightSegment);
|
|
130
|
+
return nameOrder(leftSegment, rightSegment);
|
|
131
131
|
}
|
|
132
132
|
// One path is the head of the other, so the shorter one is the directory the longer one sits in.
|
|
133
133
|
return left.segments.length - right.segments.length;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export declare const formatDateTime: (at: Date | number, options: Intl.DateTimeFormatOptions) => string;
|
|
2
|
-
export declare const formatClock: (at: Date | number) => string;
|
|
2
|
+
export declare const formatClock: (at: Date | number, timeZone?: string) => string;
|
|
3
3
|
export declare const formatWeekdayClock: (at: Date | number) => string;
|
|
4
4
|
export declare const formatHour: (at: Date | number) => string;
|
|
5
|
-
export declare const formatDay: (at: Date | number) => string;
|
|
6
|
-
export declare const formatDayWithYear: (at: Date | number) => string;
|
|
5
|
+
export declare const formatDay: (at: Date | number, timeZone?: string) => string;
|
|
6
|
+
export declare const formatDayWithYear: (at: Date | number, timeZone?: string) => string;
|
|
7
7
|
export declare const formatWeekdayDay: (at: Date | number) => string;
|
|
8
|
-
export declare const formatDayClock: (at: Date | number) => string;
|
|
8
|
+
export declare const formatDayClock: (at: Date | number, timeZone?: string) => string;
|
|
9
|
+
export declare const formatDayClockWithYear: (at: Date | number, timeZone?: string) => string;
|
|
9
10
|
export declare const formatNumericDate: (at: Date | number) => string;
|
|
10
11
|
export declare const isSameDay: (a: Date | number, b: Date | number) => boolean;
|
|
11
12
|
export declare const formatMoment: (at: Date | number, now?: Date | number) => string;
|
package/dist/format/datetime.js
CHANGED
|
@@ -26,21 +26,27 @@ const DAY = { month: 'short', day: 'numeric' };
|
|
|
26
26
|
const DAY_WITH_YEAR = { month: 'short', day: 'numeric', year: 'numeric' };
|
|
27
27
|
const WEEKDAY_DAY = { weekday: 'short', month: 'short', day: 'numeric' };
|
|
28
28
|
const DAY_CLOCK = { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
|
|
29
|
+
const DAY_CLOCK_WITH_YEAR = { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' };
|
|
29
30
|
const NUMERIC_DATE = { year: 'numeric', month: 'numeric', day: 'numeric' };
|
|
30
|
-
/*
|
|
31
|
-
|
|
31
|
+
/*
|
|
32
|
+
* `08:05`, or `08:05 AM` in a region that counts to twelve. The clock functions below take the `timeZone`
|
|
33
|
+
* to write in, such as `UTC` for a moment whose parts are given and mean the same wall clock anywhere.
|
|
34
|
+
*/
|
|
35
|
+
export const formatClock = (at, timeZone) => formatDateTime(at, timeZone === undefined ? CLOCK : { ...CLOCK, timeZone });
|
|
32
36
|
/* A time far enough off that its day is part of the answer: `Sat 08:05`. */
|
|
33
37
|
export const formatWeekdayClock = (at) => formatDateTime(at, WEEKDAY_CLOCK);
|
|
34
38
|
/* An hour of the day on its own, for the foot of a chart: `08`, or `8 AM` where that is the clock. */
|
|
35
39
|
export const formatHour = (at) => formatDateTime(at, HOUR);
|
|
36
40
|
/* `19 Sep`, the short date a row falls back to once "days ago" stops meaning anything. */
|
|
37
|
-
export const formatDay = (at) => formatDateTime(at, DAY);
|
|
41
|
+
export const formatDay = (at, timeZone) => formatDateTime(at, timeZone === undefined ? DAY : { ...DAY, timeZone });
|
|
38
42
|
/* The same date with the year, for anything older than the one we are in. */
|
|
39
|
-
export const formatDayWithYear = (at) => formatDateTime(at, DAY_WITH_YEAR);
|
|
43
|
+
export const formatDayWithYear = (at, timeZone) => formatDateTime(at, timeZone === undefined ? DAY_WITH_YEAR : { ...DAY_WITH_YEAR, timeZone });
|
|
40
44
|
/* `Sat 19 Sep`. */
|
|
41
45
|
export const formatWeekdayDay = (at) => formatDateTime(at, WEEKDAY_DAY);
|
|
42
46
|
/* `19 Sep, 08:05`: a moment that is not today, to the minute. */
|
|
43
|
-
export const formatDayClock = (at) => formatDateTime(at, DAY_CLOCK);
|
|
47
|
+
export const formatDayClock = (at, timeZone) => formatDateTime(at, timeZone === undefined ? DAY_CLOCK : { ...DAY_CLOCK, timeZone });
|
|
48
|
+
/* `19 Sep 2026, 08:05`: the same moment with the year, for one that is not in the year we are in. */
|
|
49
|
+
export const formatDayClockWithYear = (at, timeZone) => formatDateTime(at, timeZone === undefined ? DAY_CLOCK_WITH_YEAR : { ...DAY_CLOCK_WITH_YEAR, timeZone });
|
|
44
50
|
/* All numbers, as a form would print it: `19-9-2026` here, `9/19/2026` in the United States. */
|
|
45
51
|
export const formatNumericDate = (at) => formatDateTime(at, NUMERIC_DATE);
|
|
46
52
|
/* Whether two instants fall on the same day of the same year, in the zone of whoever is reading. */
|
package/dist/format/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { formatAge, formatClock, formatDateTime, formatDay, formatDayClock, formatDayWithYear, formatHour, formatMoment, formatNumericDate, formatWeekdayClock, formatWeekdayDay, isSameDay } from './datetime.ts';
|
|
1
|
+
export { formatAge, formatClock, formatDateTime, formatDay, formatDayClock, formatDayClockWithYear, formatDayWithYear, formatHour, formatMoment, formatNumericDate, formatWeekdayClock, formatWeekdayDay, isSameDay } from './datetime.ts';
|
|
2
2
|
export { formatAgo, formatClockDuration, formatCountdown, formatDuration, formatElapsedShort } from './duration.ts';
|
|
3
|
-
export {
|
|
4
|
-
export { formatBytes, formatDecimal,
|
|
3
|
+
export { formatLocale, labelCollator, setFormatSource, systemLocale, useFormatLocale, type FormatSource } from './locale.ts';
|
|
4
|
+
export { formatBytes, formatDecimal, formatMoney, formatNumber, formatNumeral, formatPercent, formatRounded, formatTokens, formatUsdSignificant } from './number.ts';
|
|
5
5
|
export { FORMAT_LANGUAGE, FORMAT_REGIONS, FORMAT_REGION_CHOICES, FORMAT_SYSTEM, formatRegionFrom, regionName } from './regions.ts';
|
|
6
6
|
export { localTimeZone } from './time-zone.ts';
|
package/dist/format/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Every number, date, time and duration a person reads, written the way the language and the region they set ask for. */
|
|
2
|
-
export { formatAge, formatClock, formatDateTime, formatDay, formatDayClock, formatDayWithYear, formatHour, formatMoment, formatNumericDate, formatWeekdayClock, formatWeekdayDay, isSameDay } from './datetime.js';
|
|
2
|
+
export { formatAge, formatClock, formatDateTime, formatDay, formatDayClock, formatDayClockWithYear, formatDayWithYear, formatHour, formatMoment, formatNumericDate, formatWeekdayClock, formatWeekdayDay, isSameDay } from './datetime.js';
|
|
3
3
|
export { formatAgo, formatClockDuration, formatCountdown, formatDuration, formatElapsedShort } from './duration.js';
|
|
4
|
-
export {
|
|
5
|
-
export { formatBytes, formatDecimal,
|
|
4
|
+
export { formatLocale, labelCollator, setFormatSource, systemLocale, useFormatLocale } from './locale.js';
|
|
5
|
+
export { formatBytes, formatDecimal, formatMoney, formatNumber, formatNumeral, formatPercent, formatRounded, formatTokens, formatUsdSignificant } from './number.js';
|
|
6
6
|
export { FORMAT_LANGUAGE, FORMAT_REGIONS, FORMAT_REGION_CHOICES, FORMAT_SYSTEM, formatRegionFrom, regionName } from './regions.js';
|
|
7
7
|
export { localTimeZone } from './time-zone.js';
|
package/dist/format/locale.d.ts
CHANGED
|
@@ -15,3 +15,4 @@ export declare const dateFormatter: (options: Intl.DateTimeFormatOptions) => Int
|
|
|
15
15
|
export declare const wordFormatter: (options: Intl.DateTimeFormatOptions) => Intl.DateTimeFormat;
|
|
16
16
|
export declare const relativeFormatter: (options: Intl.RelativeTimeFormatOptions) => Intl.RelativeTimeFormat;
|
|
17
17
|
export declare const labelCollator: () => Intl.Collator;
|
|
18
|
+
export declare const nameOrder: (left: string, right: string) => number;
|
package/dist/format/locale.js
CHANGED
|
@@ -108,3 +108,12 @@ export const labelCollator = () => {
|
|
|
108
108
|
collators.set(locale, collator);
|
|
109
109
|
return collator;
|
|
110
110
|
};
|
|
111
|
+
let nameCollators = null;
|
|
112
|
+
/*
|
|
113
|
+
* The order of names that are no words, such as files: the runtime's own locale, numbers by value and
|
|
114
|
+
* case only to break a tie. It is what `localeCompare` did, which built a collator on every call.
|
|
115
|
+
*/
|
|
116
|
+
export const nameOrder = (left, right) => {
|
|
117
|
+
nameCollators ??= { loose: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }), exact: new Intl.Collator() };
|
|
118
|
+
return nameCollators.loose.compare(left, right) || nameCollators.exact.compare(left, right);
|
|
119
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type { PopoverPopupProps, PopupVariant, PreviewCardPopupProps } from './p
|
|
|
14
14
|
export type { TabsCountProps, TabsListProps, TabsRootProps, TabsTabProps, TabsValue, TabsVariant } from './tabs/parts.tsx';
|
|
15
15
|
export { AccentSwatches, type AccentSwatchesProps } from './AccentSwatches.tsx';
|
|
16
16
|
export { Banner, type BannerProps, type BannerTone } from './Banner.tsx';
|
|
17
|
-
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from './Button.tsx';
|
|
17
|
+
export { Button, type ButtonProps, type ButtonShape, type ButtonSize, type ButtonVariant } from './Button.tsx';
|
|
18
18
|
export { ButtonGroup, type ButtonGroupProps } from './ButtonGroup.tsx';
|
|
19
19
|
export { Checkbox, type CheckboxProps } from './Checkbox.tsx';
|
|
20
20
|
export { ChoiceCards, type Choice, type ChoiceCardsProps } from './ChoiceCards.tsx';
|
|
@@ -56,7 +56,7 @@ export { Switch, type SwitchProps } from './Switch.tsx';
|
|
|
56
56
|
export { TextMenu, type TextMenuProps } from './TextMenu.tsx';
|
|
57
57
|
export { Tile, type TileProps } from './Tile.tsx';
|
|
58
58
|
export { Toasts, type ToastsProps } from './Toasts.tsx';
|
|
59
|
-
export { Tooltip,
|
|
59
|
+
export { Tooltip, type TooltipProps, type TooltipSide } from './Tooltip.tsx';
|
|
60
60
|
export { UIProvider, type UIProviderProps } from './UIProvider.tsx';
|
|
61
61
|
export { Waveform, type WaveformProps } from './Waveform.tsx';
|
|
62
62
|
export { Wipe, type WipeProps } from './Wipe.tsx';
|
|
@@ -67,19 +67,20 @@ export type { ResetKeys } from './error-boundary.ts';
|
|
|
67
67
|
export { FILE_TREE_ICONS } from './file-icon.ts';
|
|
68
68
|
export { cameThroughPortal, isInFloatingLayer } from './floating.ts';
|
|
69
69
|
export { LoadedComponent, lazyDialog, lazyNamed, onLazyOpenError } from './lazy.tsx';
|
|
70
|
-
export {
|
|
71
|
-
export {
|
|
70
|
+
export type { LazyComponent } from './lazy.tsx';
|
|
71
|
+
export { LazyBoundary } from './LazyBoundary.tsx';
|
|
72
|
+
export type { LazyBoundaryProps } from './LazyBoundary.tsx';
|
|
73
|
+
export { UI_NAMESPACE, UI_RESOURCES } from './locales.ts';
|
|
72
74
|
export { isApplePlatform } from './platform.ts';
|
|
73
|
-
export {
|
|
75
|
+
export { prefetcher, type Loader, type WhenIdle } from './prefetch.ts';
|
|
74
76
|
export { selectAllWithin, selectionWithin } from './selection.ts';
|
|
75
77
|
export { EDIT_SHORTCUTS, KEY_SHORTCUTS, formatShortcut, isModHeld, matchesShortcut, shortcut, shortcutParts, type KeyLike, type Shortcut } from './shortcut.ts';
|
|
76
|
-
export {
|
|
78
|
+
export { UNDO_MS, createToastStore, type Toast, type ToastAction, type ToastDeadline, type ToastInput, type ToastKind, type ToastPatch, type ToastStore, type ToastStoreHook } from './toast-store.ts';
|
|
77
79
|
export { useAsyncAction, type AsyncAction } from './useAsyncAction.ts';
|
|
78
80
|
export { clampColumnSize, useColumnResize, type ColumnEdge, type ColumnResize, type ColumnResizeOptions } from './useColumnResize.ts';
|
|
79
81
|
export { useContentSize, type ContentSize } from './useContentSize.ts';
|
|
80
82
|
export { useMeasuredWidth } from './useMeasuredWidth.ts';
|
|
81
83
|
export { useNow, useTickingText } from './useNow.ts';
|
|
82
|
-
export { ZOOM_PRESETS } from './zoom.ts';
|
|
83
84
|
export { useFileTree, type FileTreeOptions } from './file-tree/use-file-tree.ts';
|
|
84
85
|
export type { FileTreeRootProps } from './file-tree/parts.tsx';
|
|
85
86
|
export type { FileTree as FileTreeModel, FileTreeVisibleRow, FileTreeDragAndDropConfig, FileTreeDropContext, FileTreeDropResult } from '@pierre/trees';
|
|
@@ -90,6 +91,6 @@ export type { WorkspaceLayout } from './workspace/context.ts';
|
|
|
90
91
|
export { DocumentTab, TabStrip, type DocumentTabProps, type TabStripProps, type TabStripItem } from './workspace/TabStrip.tsx';
|
|
91
92
|
export { SplitView, type SplitViewProps, type SplitViewInfo, type SplitViewBounds, type SplitDropTarget, type SplitDrag } from './workspace/SplitView.tsx';
|
|
92
93
|
export { DropPreview, type DropPreviewProps } from './workspace/DropPreview.tsx';
|
|
93
|
-
export {
|
|
94
|
-
export { createSplitLayout,
|
|
94
|
+
export { rectDropPreview, sameDropPreview, tabDropPreview, type DropPreviewShape, type TabDrop } from './workspace/preview.ts';
|
|
95
|
+
export { createSplitLayout, updateSplitLayout, splitPanes, type SplitAxis, type SplitSide, type SplitPane, type SplitBranch, type SplitNode, type SplitLayout, type SplitCommand } from './workspace/model.ts';
|
|
95
96
|
export { tabGapAt, type SplitRect, type SplitEdges, type SplitMinimum } from './workspace/geometry.ts';
|
package/dist/index.js
CHANGED
|
@@ -55,7 +55,7 @@ export { Switch } from './Switch.js';
|
|
|
55
55
|
export { TextMenu } from './TextMenu.js';
|
|
56
56
|
export { Tile } from './Tile.js';
|
|
57
57
|
export { Toasts } from './Toasts.js';
|
|
58
|
-
export { Tooltip
|
|
58
|
+
export { Tooltip } from './Tooltip.js';
|
|
59
59
|
export { UIProvider } from './UIProvider.js';
|
|
60
60
|
export { Waveform } from './Waveform.js';
|
|
61
61
|
export { Wipe } from './Wipe.js';
|
|
@@ -65,24 +65,23 @@ export { messageOf } from './error-message.js';
|
|
|
65
65
|
export { FILE_TREE_ICONS } from './file-icon.js';
|
|
66
66
|
export { cameThroughPortal, isInFloatingLayer } from './floating.js';
|
|
67
67
|
export { LoadedComponent, lazyDialog, lazyNamed, onLazyOpenError } from './lazy.js';
|
|
68
|
-
export {
|
|
69
|
-
export {
|
|
68
|
+
export { LazyBoundary } from './LazyBoundary.js';
|
|
69
|
+
export { UI_NAMESPACE, UI_RESOURCES } from './locales.js';
|
|
70
70
|
export { isApplePlatform } from './platform.js';
|
|
71
|
-
export {
|
|
71
|
+
export { prefetcher } from './prefetch.js';
|
|
72
72
|
export { selectAllWithin, selectionWithin } from './selection.js';
|
|
73
73
|
export { EDIT_SHORTCUTS, KEY_SHORTCUTS, formatShortcut, isModHeld, matchesShortcut, shortcut, shortcutParts } from './shortcut.js';
|
|
74
|
-
export {
|
|
74
|
+
export { UNDO_MS, createToastStore } from './toast-store.js';
|
|
75
75
|
export { useAsyncAction } from './useAsyncAction.js';
|
|
76
76
|
export { clampColumnSize, useColumnResize } from './useColumnResize.js';
|
|
77
77
|
export { useContentSize } from './useContentSize.js';
|
|
78
78
|
export { useMeasuredWidth } from './useMeasuredWidth.js';
|
|
79
79
|
export { useNow, useTickingText } from './useNow.js';
|
|
80
|
-
export { ZOOM_PRESETS } from './zoom.js';
|
|
81
80
|
export { useFileTree } from './file-tree/use-file-tree.js';
|
|
82
81
|
export { Workspace } from './workspace/Workspace.js';
|
|
83
82
|
export { DocumentTab, TabStrip } from './workspace/TabStrip.js';
|
|
84
83
|
export { SplitView } from './workspace/SplitView.js';
|
|
85
84
|
export { DropPreview } from './workspace/DropPreview.js';
|
|
86
|
-
export {
|
|
87
|
-
export { createSplitLayout,
|
|
85
|
+
export { rectDropPreview, sameDropPreview, tabDropPreview } from './workspace/preview.js';
|
|
86
|
+
export { createSplitLayout, updateSplitLayout, splitPanes } from './workspace/model.js';
|
|
88
87
|
export { tabGapAt } from './workspace/geometry.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
let landings = 0;
|
|
2
|
+
const listeners = new Set();
|
|
3
|
+
/* Called once per module, when a `LoadedComponent` first has it. */
|
|
4
|
+
export function noteLanding() {
|
|
5
|
+
landings += 1;
|
|
6
|
+
for (const listener of listeners) {
|
|
7
|
+
listener();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function subscribeLandings(listener) {
|
|
11
|
+
listeners.add(listener);
|
|
12
|
+
return () => {
|
|
13
|
+
listeners.delete(listener);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function readLandings() {
|
|
17
|
+
return landings;
|
|
18
|
+
}
|
package/dist/lazy.d.ts
CHANGED
|
@@ -8,5 +8,8 @@ export declare class LoadedComponent<Component> {
|
|
|
8
8
|
subscribe(listener: () => void): () => void;
|
|
9
9
|
}
|
|
10
10
|
export declare function onLazyOpenError(listener: (error: unknown) => void): () => void;
|
|
11
|
-
export
|
|
11
|
+
export type LazyComponent<Props> = ComponentType<Props> & {
|
|
12
|
+
readonly preload: () => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export declare function lazyNamed<Module extends Record<Name, ComponentType<any>>, Name extends keyof Module>(load: () => Promise<Module>, name: Name): LazyComponent<ComponentProps<Module[Name]>>;
|
|
12
15
|
export declare function lazyDialog<Module extends Record<Name, ComponentType>, Name extends keyof Module, State>(load: () => Promise<Module>, name: Name, useStore: (select: (state: State) => boolean) => boolean, isOpen: (state: State) => boolean): ComponentType;
|
package/dist/lazy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Suspense, lazy, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { noteLanding } from './lazy-landings.js';
|
|
3
4
|
import { prefetcher } from './prefetch.js';
|
|
4
5
|
/*
|
|
5
6
|
* A module's component, loaded once and kept. `React.lazy` suspends for a tick even on a module the
|
|
@@ -20,6 +21,7 @@ export class LoadedComponent {
|
|
|
20
21
|
for (const listener of this.listeners) {
|
|
21
22
|
listener();
|
|
22
23
|
}
|
|
24
|
+
noteLanding();
|
|
23
25
|
}
|
|
24
26
|
return component;
|
|
25
27
|
}
|
|
@@ -68,7 +70,11 @@ export function lazyNamed(load, name) {
|
|
|
68
70
|
const Component = loaded.current ?? Lazy;
|
|
69
71
|
return _jsx(Component, { ...props });
|
|
70
72
|
};
|
|
71
|
-
return LazyNamed
|
|
73
|
+
return Object.assign(LazyNamed, {
|
|
74
|
+
preload: async () => {
|
|
75
|
+
await loaded.load();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
72
78
|
}
|
|
73
79
|
/* Stays true after the first opening, so every close after it still has something mounted to animate. */
|
|
74
80
|
function useOpenedOnce(open) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { Tabs } from '@base-ui/react/tabs';
|
|
@@ -9,6 +9,7 @@ import { DialogDescription, DialogPopup, DialogRoot, DialogTitle } from '../dial
|
|
|
9
9
|
import { ErrorBoundary } from '../ErrorBoundary.js';
|
|
10
10
|
import { Icon } from '../Icon.js';
|
|
11
11
|
import { IconButton } from '../IconButton.js';
|
|
12
|
+
import { LazyBoundary } from '../LazyBoundary.js';
|
|
12
13
|
import { Input } from '../Input.js';
|
|
13
14
|
import { Select } from '../Select.js';
|
|
14
15
|
import { SettingsTargetContext } from './target.js';
|
|
@@ -91,6 +92,6 @@ export function SettingsDialog({ open, onOpenChange, section, onNavigate, groups
|
|
|
91
92
|
const top = event.currentTarget.scrollTop;
|
|
92
93
|
event.currentTarget.toggleAttribute('data-fade-start', top > 0);
|
|
93
94
|
barFill.current?.style.setProperty('opacity', String(Math.min(top / BAR_FILL_DISTANCE, 1)));
|
|
94
|
-
}, children: _jsx(ErrorBoundary, { label: t('settings.failed'), className: "min-h-0 grow", children: _jsxs(
|
|
95
|
+
}, children: _jsx(ErrorBoundary, { label: t('settings.failed'), className: "min-h-0 grow", children: _jsxs(LazyBoundary, { children: [Hero && (_jsx("div", { className: "-mx-8 shrink-0 max-[960px]:-mx-4", children: _jsx(Hero, {}) })), _jsx(Pane, {})] }) }) }, entry.id));
|
|
95
96
|
}) })] })] }), _jsx(DialogDescription, { className: "sr-only", children: meta.description })] }) }));
|
|
96
97
|
}
|
|
@@ -77,7 +77,5 @@ export type SplitCommand = {
|
|
|
77
77
|
export declare function createSplitLayout(views?: string[], paneId?: string): SplitLayout;
|
|
78
78
|
export declare function splitPanes(node: SplitNode): SplitPane[];
|
|
79
79
|
export declare function findSplitNode(node: SplitNode, id: string): SplitNode | undefined;
|
|
80
|
-
/** Repairs persisted data at the application boundary; view payloads never enter the layout. */
|
|
81
|
-
export declare function normalizeSplitLayout(value: unknown): SplitLayout;
|
|
82
80
|
/** Invalid or stale commands are no-ops. IDs for new nodes come from the caller. */
|
|
83
81
|
export declare function updateSplitLayout(layout: SplitLayout, command: SplitCommand): SplitLayout;
|
package/dist/workspace/model.js
CHANGED
|
@@ -40,75 +40,6 @@ function flattenSplits(node) {
|
|
|
40
40
|
}
|
|
41
41
|
return { ...node, children, sizes: shares(sizes, children.length) };
|
|
42
42
|
}
|
|
43
|
-
/** Repairs persisted data at the application boundary; view payloads never enter the layout. */
|
|
44
|
-
export function normalizeSplitLayout(value) {
|
|
45
|
-
const ids = new Set();
|
|
46
|
-
const views = new Set();
|
|
47
|
-
const seen = new Set();
|
|
48
|
-
let serial = 0;
|
|
49
|
-
const nextId = (candidate) => {
|
|
50
|
-
let id = typeof candidate === 'string' && candidate.length > 0 ? candidate : `pane-${serial++}`;
|
|
51
|
-
while (ids.has(id)) {
|
|
52
|
-
id = `pane-${serial++}`;
|
|
53
|
-
}
|
|
54
|
-
ids.add(id);
|
|
55
|
-
return id;
|
|
56
|
-
};
|
|
57
|
-
const read = (input, depth) => {
|
|
58
|
-
if (input === null || typeof input !== 'object' || seen.has(input) || depth > 64) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
seen.add(input);
|
|
62
|
-
const row = input;
|
|
63
|
-
if (row.type === 'pane') {
|
|
64
|
-
const items = Array.isArray(row.views)
|
|
65
|
-
? row.views.filter((view) => {
|
|
66
|
-
if (typeof view !== 'string' || view.length === 0 || views.has(view)) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
views.add(view);
|
|
70
|
-
return true;
|
|
71
|
-
})
|
|
72
|
-
: [];
|
|
73
|
-
return {
|
|
74
|
-
type: 'pane',
|
|
75
|
-
id: nextId(row.id),
|
|
76
|
-
views: items,
|
|
77
|
-
active: typeof row.active === 'string' && items.includes(row.active) ? row.active : (items[0] ?? null)
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
if (row.type !== 'split' || !Array.isArray(row.children)) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
const children = [];
|
|
84
|
-
const sizes = [];
|
|
85
|
-
row.children.forEach((child, i) => {
|
|
86
|
-
const result = read(child, depth + 1);
|
|
87
|
-
if (result) {
|
|
88
|
-
children.push(result);
|
|
89
|
-
sizes.push(Array.isArray(row.sizes) ? Number(row.sizes[i]) : 1);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
if (children.length < 2) {
|
|
93
|
-
return children[0] ?? null;
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
type: 'split',
|
|
97
|
-
id: nextId(row.id),
|
|
98
|
-
axis: row.axis === 'vertical' ? 'vertical' : 'horizontal',
|
|
99
|
-
children,
|
|
100
|
-
sizes: shares(sizes, children.length)
|
|
101
|
-
};
|
|
102
|
-
};
|
|
103
|
-
const input = value !== null && typeof value === 'object' ? value : {};
|
|
104
|
-
const root = flattenSplits(read(input.root, 0) ?? createSplitLayout().root);
|
|
105
|
-
const panes = splitPanes(root);
|
|
106
|
-
return {
|
|
107
|
-
root,
|
|
108
|
-
focused: panes.find((pane) => pane.id === input.focused)?.id ?? panes[0].id,
|
|
109
|
-
maximized: panes.find((pane) => pane.id === input.maximized)?.id ?? null
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
43
|
function replace(node, id, transform) {
|
|
113
44
|
if (node.id === id) {
|
|
114
45
|
return transform(node);
|
package/package.json
CHANGED
package/src/Button.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|
|
3
3
|
|
|
4
4
|
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'danger-outline' | 'positive' | 'inverse';
|
|
5
5
|
export type ButtonSize = 'xs' | 'sm' | 'md';
|
|
6
|
+
export type ButtonShape = 'default' | 'pill';
|
|
6
7
|
|
|
7
8
|
const VARIANT: Record<ButtonVariant, string> = {
|
|
8
9
|
// A filled button darkens under the pointer. Its color is the meaning, so no token of its own for hover.
|
|
@@ -24,19 +25,28 @@ const SIZE: Record<ButtonSize, string> = {
|
|
|
24
25
|
md: 'h-8 px-3'
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
/* A pill is one size of its own, for a control that sits in a round row, so it takes the place of `size`. */
|
|
29
|
+
const SHAPE: Record<ButtonShape, string> = {
|
|
30
|
+
default: 'rounded-md text-xs',
|
|
31
|
+
pill: 'h-9 rounded-full px-3.5 text-sm'
|
|
32
|
+
};
|
|
33
|
+
|
|
27
34
|
export interface ButtonProps extends ComponentProps<'button'> {
|
|
28
35
|
variant?: ButtonVariant;
|
|
29
36
|
size?: ButtonSize;
|
|
37
|
+
/* `pill` is 36 pixels tall and fully rounded, and ignores `size`. */
|
|
38
|
+
shape?: ButtonShape;
|
|
30
39
|
/* Makes it a link that looks like a button, a real anchor, so it opens the way links open. */
|
|
31
40
|
href?: string;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
/* Every button with a word in it. A button that is only an icon is an `IconButton`, which is a square, not a label. */
|
|
35
|
-
export function Button({ variant = 'ghost', size = 'md', className, href, type = 'button', ref, ...rest }: ButtonProps) {
|
|
44
|
+
export function Button({ variant = 'ghost', size = 'md', shape = 'default', className, href, type = 'button', ref, ...rest }: ButtonProps) {
|
|
36
45
|
const classes = clsx(
|
|
37
|
-
'inline-flex shrink-0 items-center justify-center gap-1.5
|
|
46
|
+
'inline-flex shrink-0 items-center justify-center gap-1.5 font-medium disabled:opacity-50',
|
|
47
|
+
SHAPE[shape],
|
|
38
48
|
VARIANT[variant],
|
|
39
|
-
SIZE[size],
|
|
49
|
+
shape === 'default' && SIZE[size],
|
|
40
50
|
className
|
|
41
51
|
);
|
|
42
52
|
if (href) {
|
package/src/EmptyState.tsx
CHANGED
|
@@ -12,8 +12,6 @@ export interface EmptyStateProps {
|
|
|
12
12
|
icon?: LucideIcon | ReactElement<{ size?: number }>;
|
|
13
13
|
/* A `Spinner` in place of the icon, for a state that is still loading. */
|
|
14
14
|
busy?: boolean;
|
|
15
|
-
/** @deprecated Turns a Lucide icon; use `busy`, which draws a `Spinner` in its place. */
|
|
16
|
-
spin?: boolean;
|
|
17
15
|
/* A heading above the sentence, for a state that is an outcome rather than a list with nothing in it. */
|
|
18
16
|
title?: ReactNode;
|
|
19
17
|
/* One sentence. What is missing, and what puts something there. */
|
|
@@ -25,7 +23,7 @@ export interface EmptyStateProps {
|
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
/* What a list, a canvas or a thread shows before it holds anything. */
|
|
28
|
-
export function EmptyState({ icon, busy = false,
|
|
26
|
+
export function EmptyState({ icon, busy = false, title, children, action, className, ref }: EmptyStateProps) {
|
|
29
27
|
return (
|
|
30
28
|
<div ref={ref} className={clsx('flex flex-col items-center justify-center gap-2 px-6 py-8 text-center', className)}>
|
|
31
29
|
{(busy || icon) && (
|
|
@@ -35,7 +33,7 @@ export function EmptyState({ icon, busy = false, spin = false, title, children,
|
|
|
35
33
|
) : isValidElement(icon) ? (
|
|
36
34
|
cloneElement(icon, { size: ICON_SIZE })
|
|
37
35
|
) : (
|
|
38
|
-
icon && <Icon icon={icon} size={ICON_SIZE}
|
|
36
|
+
icon && <Icon icon={icon} size={ICON_SIZE} />
|
|
39
37
|
)}
|
|
40
38
|
</span>
|
|
41
39
|
)}
|
package/src/IconButton.tsx
CHANGED
|
@@ -24,8 +24,6 @@ export interface IconButtonProps extends Omit<ComponentProps<'button'>, 'childre
|
|
|
24
24
|
active?: boolean;
|
|
25
25
|
/* A `Spinner` in place of the icon, for a step that is running. */
|
|
26
26
|
busy?: boolean;
|
|
27
|
-
/** @deprecated Turns the icon; use `busy`, which draws a `Spinner` in its place. */
|
|
28
|
-
spin?: boolean;
|
|
29
27
|
/* Classes for the icon rather than the button: a tone of its own, a turn, a fill. The size stays the button's. */
|
|
30
28
|
iconClassName?: string;
|
|
31
29
|
/* Drawn after the icon: a count, or a word beside it together with `w-auto`. */
|
|
@@ -44,7 +42,6 @@ export function IconButton({
|
|
|
44
42
|
size = 'md',
|
|
45
43
|
active = false,
|
|
46
44
|
busy = false,
|
|
47
|
-
spin = false,
|
|
48
45
|
iconClassName,
|
|
49
46
|
render,
|
|
50
47
|
className,
|
|
@@ -65,7 +62,7 @@ export function IconButton({
|
|
|
65
62
|
{busy ? (
|
|
66
63
|
<Spinner size={ICON_BUTTON_ICON_SIZE[size]} className={iconClassName} />
|
|
67
64
|
) : (
|
|
68
|
-
icon !== undefined && <Icon icon={icon} size={ICON_BUTTON_ICON_SIZE[size]} className={
|
|
65
|
+
icon !== undefined && <Icon icon={icon} size={ICON_BUTTON_ICON_SIZE[size]} className={iconClassName} />
|
|
69
66
|
)}
|
|
70
67
|
{children}
|
|
71
68
|
</>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Suspense, useSyncExternalStore, type ReactNode } from 'react';
|
|
2
|
+
import { readLandings, subscribeLandings } from './lazy-landings.ts';
|
|
3
|
+
|
|
4
|
+
export interface LazyBoundaryProps {
|
|
5
|
+
readonly fallback?: ReactNode;
|
|
6
|
+
readonly children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* `Suspense` for what `lazyNamed` loads. React holds content that comes in as a retry back until
|
|
11
|
+
* 300 ms after its fallback showed; this boundary draws again as an ordinary update once a module
|
|
12
|
+
* lands, so a chunk that takes 20 ms shows after 20 ms.
|
|
13
|
+
*/
|
|
14
|
+
export function LazyBoundary({ fallback = null, children }: LazyBoundaryProps) {
|
|
15
|
+
useSyncExternalStore(subscribeLandings, readLandings, readLandings);
|
|
16
|
+
return <Suspense fallback={fallback}>{children}</Suspense>;
|
|
17
|
+
}
|
package/src/PanelEmpty.tsx
CHANGED
|
@@ -9,8 +9,6 @@ export interface PanelEmptyProps {
|
|
|
9
9
|
icon?: LucideIcon;
|
|
10
10
|
/* A `Spinner` in place of the icon, for a panel that is still loading. */
|
|
11
11
|
busy?: boolean;
|
|
12
|
-
/** @deprecated Turns the icon; use `busy`, which draws a `Spinner` in its place. */
|
|
13
|
-
spin?: boolean;
|
|
14
12
|
action?: ReactNode;
|
|
15
13
|
/* The sunken ground, for a box that has none behind it yet (a card on a canvas) rather than a panel that does. */
|
|
16
14
|
sunken?: boolean;
|
|
@@ -25,11 +23,11 @@ export interface PanelEmptyProps {
|
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
/* What a panel or a card shows while it holds nothing: a sentence, one icon, at most one button. */
|
|
28
|
-
export function PanelEmpty({ header, icon, busy = false,
|
|
26
|
+
export function PanelEmpty({ header, icon, busy = false, action, sunken = false, fill = 'grow', children, className, ref }: PanelEmptyProps) {
|
|
29
27
|
return (
|
|
30
28
|
<div ref={ref} className={clsx('grid min-h-0 place-items-center', fill === 'full' ? 'h-full' : 'grow', sunken && 'bg-surface-sunken', className)}>
|
|
31
29
|
{header}
|
|
32
|
-
<EmptyState icon={icon} busy={busy}
|
|
30
|
+
<EmptyState icon={icon} busy={busy} action={action}>
|
|
33
31
|
{children}
|
|
34
32
|
</EmptyState>
|
|
35
33
|
</div>
|
package/src/file-tree/helpers.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FileTree, FileTreeDirectoryHandle, FileTreeVisibleRow } from '@pierre/trees';
|
|
2
|
+
import { nameOrder } from '../format/locale.ts';
|
|
2
3
|
|
|
3
4
|
// Expanded folders can reveal more flattened rows; cap custom-key cycles.
|
|
4
5
|
const EXPANSION_PASSES = 32;
|
|
@@ -152,8 +153,7 @@ export function compareRows(left: SortRow, right: SortRow): number {
|
|
|
152
153
|
if (leftIsDirectory !== rightIsDirectory) {
|
|
153
154
|
return leftIsDirectory ? -1 : 1;
|
|
154
155
|
}
|
|
155
|
-
|
|
156
|
-
return leftSegment.localeCompare(rightSegment, undefined, { numeric: true, sensitivity: 'base' }) || leftSegment.localeCompare(rightSegment);
|
|
156
|
+
return nameOrder(leftSegment, rightSegment);
|
|
157
157
|
}
|
|
158
158
|
// One path is the head of the other, so the shorter one is the directory the longer one sits in.
|
|
159
159
|
return left.segments.length - right.segments.length;
|
package/src/format/datetime.ts
CHANGED
|
@@ -30,10 +30,14 @@ const DAY: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
|
|
|
30
30
|
const DAY_WITH_YEAR: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' };
|
|
31
31
|
const WEEKDAY_DAY: Intl.DateTimeFormatOptions = { weekday: 'short', month: 'short', day: 'numeric' };
|
|
32
32
|
const DAY_CLOCK: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
|
|
33
|
+
const DAY_CLOCK_WITH_YEAR: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' };
|
|
33
34
|
const NUMERIC_DATE: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'numeric', day: 'numeric' };
|
|
34
35
|
|
|
35
|
-
/*
|
|
36
|
-
|
|
36
|
+
/*
|
|
37
|
+
* `08:05`, or `08:05 AM` in a region that counts to twelve. The clock functions below take the `timeZone`
|
|
38
|
+
* to write in, such as `UTC` for a moment whose parts are given and mean the same wall clock anywhere.
|
|
39
|
+
*/
|
|
40
|
+
export const formatClock = (at: Date | number, timeZone?: string): string => formatDateTime(at, timeZone === undefined ? CLOCK : { ...CLOCK, timeZone });
|
|
37
41
|
|
|
38
42
|
/* A time far enough off that its day is part of the answer: `Sat 08:05`. */
|
|
39
43
|
export const formatWeekdayClock = (at: Date | number): string => formatDateTime(at, WEEKDAY_CLOCK);
|
|
@@ -42,16 +46,22 @@ export const formatWeekdayClock = (at: Date | number): string => formatDateTime(
|
|
|
42
46
|
export const formatHour = (at: Date | number): string => formatDateTime(at, HOUR);
|
|
43
47
|
|
|
44
48
|
/* `19 Sep`, the short date a row falls back to once "days ago" stops meaning anything. */
|
|
45
|
-
export const formatDay = (at: Date | number): string => formatDateTime(at, DAY);
|
|
49
|
+
export const formatDay = (at: Date | number, timeZone?: string): string => formatDateTime(at, timeZone === undefined ? DAY : { ...DAY, timeZone });
|
|
46
50
|
|
|
47
51
|
/* The same date with the year, for anything older than the one we are in. */
|
|
48
|
-
export const formatDayWithYear = (at: Date | number): string =>
|
|
52
|
+
export const formatDayWithYear = (at: Date | number, timeZone?: string): string =>
|
|
53
|
+
formatDateTime(at, timeZone === undefined ? DAY_WITH_YEAR : { ...DAY_WITH_YEAR, timeZone });
|
|
49
54
|
|
|
50
55
|
/* `Sat 19 Sep`. */
|
|
51
56
|
export const formatWeekdayDay = (at: Date | number): string => formatDateTime(at, WEEKDAY_DAY);
|
|
52
57
|
|
|
53
58
|
/* `19 Sep, 08:05`: a moment that is not today, to the minute. */
|
|
54
|
-
export const formatDayClock = (at: Date | number): string =>
|
|
59
|
+
export const formatDayClock = (at: Date | number, timeZone?: string): string =>
|
|
60
|
+
formatDateTime(at, timeZone === undefined ? DAY_CLOCK : { ...DAY_CLOCK, timeZone });
|
|
61
|
+
|
|
62
|
+
/* `19 Sep 2026, 08:05`: the same moment with the year, for one that is not in the year we are in. */
|
|
63
|
+
export const formatDayClockWithYear = (at: Date | number, timeZone?: string): string =>
|
|
64
|
+
formatDateTime(at, timeZone === undefined ? DAY_CLOCK_WITH_YEAR : { ...DAY_CLOCK_WITH_YEAR, timeZone });
|
|
55
65
|
|
|
56
66
|
/* All numbers, as a form would print it: `19-9-2026` here, `9/19/2026` in the United States. */
|
|
57
67
|
export const formatNumericDate = (at: Date | number): string => formatDateTime(at, NUMERIC_DATE);
|
package/src/format/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export {
|
|
|
6
6
|
formatDateTime,
|
|
7
7
|
formatDay,
|
|
8
8
|
formatDayClock,
|
|
9
|
+
formatDayClockWithYear,
|
|
9
10
|
formatDayWithYear,
|
|
10
11
|
formatHour,
|
|
11
12
|
formatMoment,
|
|
@@ -15,11 +16,10 @@ export {
|
|
|
15
16
|
isSameDay
|
|
16
17
|
} from './datetime.ts';
|
|
17
18
|
export { formatAgo, formatClockDuration, formatCountdown, formatDuration, formatElapsedShort } from './duration.ts';
|
|
18
|
-
export {
|
|
19
|
+
export { formatLocale, labelCollator, setFormatSource, systemLocale, useFormatLocale, type FormatSource } from './locale.ts';
|
|
19
20
|
export {
|
|
20
21
|
formatBytes,
|
|
21
22
|
formatDecimal,
|
|
22
|
-
formatFixed,
|
|
23
23
|
formatMoney,
|
|
24
24
|
formatNumber,
|
|
25
25
|
formatNumeral,
|
package/src/format/locale.ts
CHANGED
|
@@ -151,3 +151,14 @@ export const labelCollator = (): Intl.Collator => {
|
|
|
151
151
|
collators.set(locale, collator);
|
|
152
152
|
return collator;
|
|
153
153
|
};
|
|
154
|
+
|
|
155
|
+
let nameCollators: { loose: Intl.Collator; exact: Intl.Collator } | null = null;
|
|
156
|
+
|
|
157
|
+
/*
|
|
158
|
+
* The order of names that are no words, such as files: the runtime's own locale, numbers by value and
|
|
159
|
+
* case only to break a tie. It is what `localeCompare` did, which built a collator on every call.
|
|
160
|
+
*/
|
|
161
|
+
export const nameOrder = (left: string, right: string): number => {
|
|
162
|
+
nameCollators ??= { loose: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }), exact: new Intl.Collator() };
|
|
163
|
+
return nameCollators.loose.compare(left, right) || nameCollators.exact.compare(left, right);
|
|
164
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type { TabsCountProps, TabsListProps, TabsRootProps, TabsTabProps, TabsVa
|
|
|
29
29
|
|
|
30
30
|
export { AccentSwatches, type AccentSwatchesProps } from './AccentSwatches.tsx';
|
|
31
31
|
export { Banner, type BannerProps, type BannerTone } from './Banner.tsx';
|
|
32
|
-
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from './Button.tsx';
|
|
32
|
+
export { Button, type ButtonProps, type ButtonShape, type ButtonSize, type ButtonVariant } from './Button.tsx';
|
|
33
33
|
export { ButtonGroup, type ButtonGroupProps } from './ButtonGroup.tsx';
|
|
34
34
|
export { Checkbox, type CheckboxProps } from './Checkbox.tsx';
|
|
35
35
|
export { ChoiceCards, type Choice, type ChoiceCardsProps } from './ChoiceCards.tsx';
|
|
@@ -71,7 +71,7 @@ export { Switch, type SwitchProps } from './Switch.tsx';
|
|
|
71
71
|
export { TextMenu, type TextMenuProps } from './TextMenu.tsx';
|
|
72
72
|
export { Tile, type TileProps } from './Tile.tsx';
|
|
73
73
|
export { Toasts, type ToastsProps } from './Toasts.tsx';
|
|
74
|
-
export { Tooltip,
|
|
74
|
+
export { Tooltip, type TooltipProps, type TooltipSide } from './Tooltip.tsx';
|
|
75
75
|
export { UIProvider, type UIProviderProps } from './UIProvider.tsx';
|
|
76
76
|
export { Waveform, type WaveformProps } from './Waveform.tsx';
|
|
77
77
|
export { Wipe, type WipeProps } from './Wipe.tsx';
|
|
@@ -83,17 +83,17 @@ export type { ResetKeys } from './error-boundary.ts';
|
|
|
83
83
|
export { FILE_TREE_ICONS } from './file-icon.ts';
|
|
84
84
|
export { cameThroughPortal, isInFloatingLayer } from './floating.ts';
|
|
85
85
|
export { LoadedComponent, lazyDialog, lazyNamed, onLazyOpenError } from './lazy.tsx';
|
|
86
|
-
export {
|
|
87
|
-
export {
|
|
86
|
+
export type { LazyComponent } from './lazy.tsx';
|
|
87
|
+
export { LazyBoundary } from './LazyBoundary.tsx';
|
|
88
|
+
export type { LazyBoundaryProps } from './LazyBoundary.tsx';
|
|
89
|
+
export { UI_NAMESPACE, UI_RESOURCES } from './locales.ts';
|
|
88
90
|
export { isApplePlatform } from './platform.ts';
|
|
89
|
-
export {
|
|
91
|
+
export { prefetcher, type Loader, type WhenIdle } from './prefetch.ts';
|
|
90
92
|
export { selectAllWithin, selectionWithin } from './selection.ts';
|
|
91
93
|
export { EDIT_SHORTCUTS, KEY_SHORTCUTS, formatShortcut, isModHeld, matchesShortcut, shortcut, shortcutParts, type KeyLike, type Shortcut } from './shortcut.ts';
|
|
92
94
|
export {
|
|
93
|
-
SUCCESS_MS,
|
|
94
95
|
UNDO_MS,
|
|
95
96
|
createToastStore,
|
|
96
|
-
elapsedOf,
|
|
97
97
|
type Toast,
|
|
98
98
|
type ToastAction,
|
|
99
99
|
type ToastDeadline,
|
|
@@ -108,7 +108,6 @@ export { clampColumnSize, useColumnResize, type ColumnEdge, type ColumnResize, t
|
|
|
108
108
|
export { useContentSize, type ContentSize } from './useContentSize.ts';
|
|
109
109
|
export { useMeasuredWidth } from './useMeasuredWidth.ts';
|
|
110
110
|
export { useNow, useTickingText } from './useNow.ts';
|
|
111
|
-
export { ZOOM_PRESETS } from './zoom.ts';
|
|
112
111
|
|
|
113
112
|
export { useFileTree, type FileTreeOptions } from './file-tree/use-file-tree.ts';
|
|
114
113
|
export type { FileTreeRootProps } from './file-tree/parts.tsx';
|
|
@@ -130,10 +129,9 @@ export type { WorkspaceLayout } from './workspace/context.ts';
|
|
|
130
129
|
export { DocumentTab, TabStrip, type DocumentTabProps, type TabStripProps, type TabStripItem } from './workspace/TabStrip.tsx';
|
|
131
130
|
export { SplitView, type SplitViewProps, type SplitViewInfo, type SplitViewBounds, type SplitDropTarget, type SplitDrag } from './workspace/SplitView.tsx';
|
|
132
131
|
export { DropPreview, type DropPreviewProps } from './workspace/DropPreview.tsx';
|
|
133
|
-
export {
|
|
132
|
+
export { rectDropPreview, sameDropPreview, tabDropPreview, type DropPreviewShape, type TabDrop } from './workspace/preview.ts';
|
|
134
133
|
export {
|
|
135
134
|
createSplitLayout,
|
|
136
|
-
normalizeSplitLayout,
|
|
137
135
|
updateSplitLayout,
|
|
138
136
|
splitPanes,
|
|
139
137
|
type SplitAxis,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
let landings = 0;
|
|
2
|
+
const listeners = new Set<() => void>();
|
|
3
|
+
|
|
4
|
+
/* Called once per module, when a `LoadedComponent` first has it. */
|
|
5
|
+
export function noteLanding(): void {
|
|
6
|
+
landings += 1;
|
|
7
|
+
for (const listener of listeners) {
|
|
8
|
+
listener();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function subscribeLandings(listener: () => void): () => void {
|
|
13
|
+
listeners.add(listener);
|
|
14
|
+
return () => {
|
|
15
|
+
listeners.delete(listener);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readLandings(): number {
|
|
20
|
+
return landings;
|
|
21
|
+
}
|
package/src/lazy.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Suspense, lazy, useState, useSyncExternalStore, type ComponentProps, type ComponentType } from 'react';
|
|
2
|
+
import { noteLanding } from './lazy-landings.ts';
|
|
2
3
|
import { prefetcher } from './prefetch.ts';
|
|
3
4
|
|
|
4
5
|
/*
|
|
@@ -22,6 +23,7 @@ export class LoadedComponent<Component> {
|
|
|
22
23
|
for (const listener of this.listeners) {
|
|
23
24
|
listener();
|
|
24
25
|
}
|
|
26
|
+
noteLanding();
|
|
25
27
|
}
|
|
26
28
|
return component;
|
|
27
29
|
}
|
|
@@ -60,6 +62,9 @@ async function open<Component>(loaded: LoadedComponent<Component>): Promise<{ de
|
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/* A lazy component, with `preload` to have its module before the first render, so that render never suspends. */
|
|
66
|
+
export type LazyComponent<Props> = ComponentType<Props> & { readonly preload: () => Promise<void> };
|
|
67
|
+
|
|
63
68
|
/*
|
|
64
69
|
* `React.lazy` for a module that exports its component by name, `default` included. Every module
|
|
65
70
|
* loaded through here, or through `lazyDialog`, registers with `prefetcher`, which loads it ahead of
|
|
@@ -68,7 +73,7 @@ async function open<Component>(loaded: LoadedComponent<Component>): Promise<{ de
|
|
|
68
73
|
export function lazyNamed<Module extends Record<Name, ComponentType<any>>, Name extends keyof Module>(
|
|
69
74
|
load: () => Promise<Module>,
|
|
70
75
|
name: Name
|
|
71
|
-
):
|
|
76
|
+
): LazyComponent<ComponentProps<Module[Name]>> {
|
|
72
77
|
const loaded = new LoadedComponent(async () => (await load())[name]);
|
|
73
78
|
prefetcher.register(() => loaded.load());
|
|
74
79
|
const Lazy = lazy(() => open(loaded));
|
|
@@ -77,7 +82,11 @@ export function lazyNamed<Module extends Record<Name, ComponentType<any>>, Name
|
|
|
77
82
|
const Component: ComponentType<any> = loaded.current ?? Lazy;
|
|
78
83
|
return <Component {...props} />;
|
|
79
84
|
};
|
|
80
|
-
return LazyNamed
|
|
85
|
+
return Object.assign(LazyNamed, {
|
|
86
|
+
preload: async () => {
|
|
87
|
+
await loaded.load();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
/* Stays true after the first opening, so every close after it still has something mounted to animate. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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
4
|
import { Tabs } from '@base-ui/react/tabs';
|
|
@@ -8,6 +8,7 @@ import { DialogDescription, DialogPopup, DialogRoot, DialogTitle } from '../dial
|
|
|
8
8
|
import { ErrorBoundary } from '../ErrorBoundary.tsx';
|
|
9
9
|
import { Icon } from '../Icon.tsx';
|
|
10
10
|
import { IconButton } from '../IconButton.tsx';
|
|
11
|
+
import { LazyBoundary } from '../LazyBoundary.tsx';
|
|
11
12
|
import { Input } from '../Input.tsx';
|
|
12
13
|
import { Select } from '../Select.tsx';
|
|
13
14
|
import { SettingsTargetContext } from './target.ts';
|
|
@@ -18,7 +19,7 @@ export interface SettingsSectionEntry {
|
|
|
18
19
|
label: string;
|
|
19
20
|
/* One line under the pane title that says what the pane is about. */
|
|
20
21
|
description: string;
|
|
21
|
-
/* Rendered in a
|
|
22
|
+
/* Rendered in a `LazyBoundary`, so a lazy component loads when its section is opened. */
|
|
22
23
|
pane: ComponentType;
|
|
23
24
|
/* A list beside a detail that scrolls on its own (`MasterDetail`), instead of one padded column. */
|
|
24
25
|
split?: boolean;
|
|
@@ -320,14 +321,14 @@ export function SettingsDialog({
|
|
|
320
321
|
}}
|
|
321
322
|
>
|
|
322
323
|
<ErrorBoundary label={t('settings.failed')} className="min-h-0 grow">
|
|
323
|
-
<
|
|
324
|
+
<LazyBoundary>
|
|
324
325
|
{Hero && (
|
|
325
326
|
<div className="-mx-8 shrink-0 max-[960px]:-mx-4">
|
|
326
327
|
<Hero />
|
|
327
328
|
</div>
|
|
328
329
|
)}
|
|
329
330
|
<Pane />
|
|
330
|
-
</
|
|
331
|
+
</LazyBoundary>
|
|
331
332
|
</ErrorBoundary>
|
|
332
333
|
</Tabs.Panel>
|
|
333
334
|
);
|
package/src/workspace/model.ts
CHANGED
|
@@ -84,76 +84,6 @@ function flattenSplits(node: SplitNode): SplitNode {
|
|
|
84
84
|
return { ...node, children, sizes: shares(sizes, children.length) };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/** Repairs persisted data at the application boundary; view payloads never enter the layout. */
|
|
88
|
-
export function normalizeSplitLayout(value: unknown): SplitLayout {
|
|
89
|
-
const ids = new Set<string>();
|
|
90
|
-
const views = new Set<string>();
|
|
91
|
-
const seen = new Set<object>();
|
|
92
|
-
let serial = 0;
|
|
93
|
-
const nextId = (candidate: unknown): string => {
|
|
94
|
-
let id = typeof candidate === 'string' && candidate.length > 0 ? candidate : `pane-${serial++}`;
|
|
95
|
-
while (ids.has(id)) {
|
|
96
|
-
id = `pane-${serial++}`;
|
|
97
|
-
}
|
|
98
|
-
ids.add(id);
|
|
99
|
-
return id;
|
|
100
|
-
};
|
|
101
|
-
const read = (input: unknown, depth: number): SplitNode | null => {
|
|
102
|
-
if (input === null || typeof input !== 'object' || seen.has(input) || depth > 64) {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
seen.add(input);
|
|
106
|
-
const row = input as Record<string, unknown>;
|
|
107
|
-
if (row.type === 'pane') {
|
|
108
|
-
const items = Array.isArray(row.views)
|
|
109
|
-
? row.views.filter((view): view is string => {
|
|
110
|
-
if (typeof view !== 'string' || view.length === 0 || views.has(view)) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
views.add(view);
|
|
114
|
-
return true;
|
|
115
|
-
})
|
|
116
|
-
: [];
|
|
117
|
-
return {
|
|
118
|
-
type: 'pane',
|
|
119
|
-
id: nextId(row.id),
|
|
120
|
-
views: items,
|
|
121
|
-
active: typeof row.active === 'string' && items.includes(row.active) ? row.active : (items[0] ?? null)
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
if (row.type !== 'split' || !Array.isArray(row.children)) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
const children: SplitNode[] = [];
|
|
128
|
-
const sizes: number[] = [];
|
|
129
|
-
row.children.forEach((child, i) => {
|
|
130
|
-
const result = read(child, depth + 1);
|
|
131
|
-
if (result) {
|
|
132
|
-
children.push(result);
|
|
133
|
-
sizes.push(Array.isArray(row.sizes) ? Number(row.sizes[i]) : 1);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
if (children.length < 2) {
|
|
137
|
-
return children[0] ?? null;
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
type: 'split',
|
|
141
|
-
id: nextId(row.id),
|
|
142
|
-
axis: row.axis === 'vertical' ? 'vertical' : 'horizontal',
|
|
143
|
-
children,
|
|
144
|
-
sizes: shares(sizes, children.length)
|
|
145
|
-
};
|
|
146
|
-
};
|
|
147
|
-
const input = value !== null && typeof value === 'object' ? (value as Record<string, unknown>) : {};
|
|
148
|
-
const root = flattenSplits(read(input.root, 0) ?? createSplitLayout().root);
|
|
149
|
-
const panes = splitPanes(root);
|
|
150
|
-
return {
|
|
151
|
-
root,
|
|
152
|
-
focused: panes.find((pane) => pane.id === input.focused)?.id ?? panes[0]!.id,
|
|
153
|
-
maximized: panes.find((pane) => pane.id === input.maximized)?.id ?? null
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
87
|
function replace(node: SplitNode, id: string, transform: (node: SplitNode) => SplitNode | null): SplitNode | null {
|
|
158
88
|
if (node.id === id) {
|
|
159
89
|
return transform(node);
|