@adecore/ui 0.39.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AccentSwatches.js +2 -3
- package/dist/Banner.js +1 -6
- package/dist/ChoiceCards.js +0 -5
- package/dist/ColorSwatch.js +1 -4
- package/dist/ColorWheelPicker.js +3 -6
- package/dist/DockShell.js +3 -5
- package/dist/EmptyState.js +0 -1
- package/dist/ErrorBoundary.js +1 -5
- package/dist/Field.js +2 -6
- package/dist/FileIcon.js +2 -4
- package/dist/Icon.js +1 -2
- package/dist/IconButton.js +1 -1
- package/dist/IconPicker.js +3 -7
- package/dist/Input.js +1 -2
- package/dist/Kbd.js +1 -3
- package/dist/LazyBoundary.js +2 -3
- package/dist/Meter.js +0 -1
- package/dist/MiddleTruncate.js +2 -6
- package/dist/PanelEmpty.js +0 -1
- package/dist/PanelHeader.js +1 -4
- package/dist/Pill.js +1 -6
- package/dist/ProjectSwitcher.js +1 -1
- package/dist/PromptDialog.js +5 -8
- package/dist/RelativeTime.js +1 -1
- package/dist/SegmentBar.js +1 -2
- package/dist/Segmented.js +2 -5
- package/dist/Select.js +1 -4
- package/dist/ShortcutHints.js +2 -3
- package/dist/Slider.js +0 -1
- package/dist/SlidingColumn.js +2 -4
- package/dist/SplitButton.js +1 -5
- package/dist/TextMenu.js +1 -5
- package/dist/Tile.js +0 -4
- package/dist/Toasts.js +1 -9
- package/dist/Tooltip.js +1 -1
- package/dist/UIProvider.js +0 -5
- package/dist/Waveform.js +4 -2
- package/dist/Wipe.js +1 -6
- package/dist/ZoomControls.js +0 -1
- package/dist/clipboard.js +2 -5
- package/dist/combobox/parts.js +3 -9
- package/dist/command-palette/parts.js +5 -14
- package/dist/dialog/parts.js +5 -10
- package/dist/dialog-layer.js +2 -3
- package/dist/error-boundary.js +4 -20
- package/dist/file-icon.js +4 -9
- package/dist/file-tree/empty-lines.js +2 -8
- package/dist/file-tree/use-empty-lines.js +1 -5
- package/dist/file-tree/use-slots.js +6 -10
- package/dist/format/datetime.js +32 -29
- package/dist/format/duration.js +2 -10
- package/dist/format/locale.js +19 -39
- package/dist/format/number.js +9 -22
- package/dist/format/regions.js +4 -7
- package/dist/icon-picker.js +5 -14
- package/dist/key-value-list/parts.js +1 -5
- package/dist/lazy.js +4 -19
- package/dist/locales.js +2 -6
- package/dist/menu/parts.js +61 -133
- package/dist/middle-truncate.js +2 -3
- package/dist/placement.js +0 -1
- package/dist/popover/parts.js +1 -5
- package/dist/popup-layer.js +2 -4
- package/dist/prefetch.js +2 -5
- package/dist/selection.js +1 -5
- package/dist/settings/MasterDetail.js +1 -7
- package/dist/settings/SettingsDialog.js +1 -2
- package/dist/settings/SettingsRow.js +2 -5
- package/dist/settings/SettingsSection.js +1 -1
- package/dist/shortcut-hints.js +1 -5
- package/dist/shortcut.js +1 -2
- package/dist/tabs/overflow.js +1 -6
- package/dist/tabs/parts.js +69 -95
- package/dist/testing/dedupe.js +5 -7
- package/dist/tint.js +3 -5
- package/dist/toast-store.js +2 -10
- package/dist/tree/use-tree-shift.js +2 -6
- package/dist/useColumnResize.js +9 -10
- package/dist/useNow.js +2 -9
- package/dist/workspace/DropPreview.js +1 -5
- package/dist/workspace/preview.js +4 -9
- package/package.json +9 -9
- package/src/AccentSwatches.tsx +9 -3
- package/src/Banner.tsx +5 -11
- package/src/Button.tsx +4 -0
- package/src/Checkbox.tsx +3 -0
- package/src/ChoiceCards.tsx +12 -11
- package/src/CloseButton.tsx +1 -0
- package/src/ColorSwatch.tsx +7 -8
- package/src/ColorWheelPicker.tsx +28 -12
- package/src/ColumnResizeHandle.tsx +1 -0
- package/src/CopyButton.tsx +6 -0
- package/src/DisabledReason.tsx +1 -0
- package/src/DockShell.tsx +11 -10
- package/src/EmptyState.tsx +10 -6
- package/src/ErrorBoundary.tsx +13 -10
- package/src/Field.tsx +11 -13
- package/src/FileIcon.tsx +4 -4
- package/src/Icon.tsx +2 -4
- package/src/IconButton.tsx +14 -5
- package/src/IconPicker.tsx +30 -15
- package/src/Input.tsx +11 -9
- package/src/Kbd.tsx +8 -7
- package/src/LazyBoundary.tsx +3 -3
- package/src/Meter.tsx +7 -2
- package/src/MiddleTruncate.tsx +15 -6
- package/src/PanelEmpty.tsx +9 -7
- package/src/PanelHeader.tsx +2 -5
- package/src/Pill.tsx +7 -8
- package/src/ProjectSwitcher.tsx +16 -6
- package/src/PromptDialog.tsx +31 -26
- package/src/RelativeTime.tsx +6 -1
- package/src/SegmentBar.tsx +22 -11
- package/src/Segmented.tsx +16 -11
- package/src/Select.tsx +7 -4
- package/src/SelectSearch.tsx +3 -0
- package/src/ShortcutHints.tsx +24 -3
- package/src/Skeleton.tsx +1 -0
- package/src/Slider.tsx +4 -1
- package/src/SlidingColumn.tsx +18 -14
- package/src/Spinner.tsx +1 -0
- package/src/SplitButton.tsx +8 -8
- package/src/Stepper.tsx +6 -0
- package/src/Switch.tsx +2 -0
- package/src/TextMenu.tsx +2 -5
- package/src/Tile.tsx +2 -6
- package/src/Toasts.tsx +8 -12
- package/src/Tooltip.tsx +10 -5
- package/src/UIProvider.tsx +7 -7
- package/src/Waveform.tsx +41 -12
- package/src/Wipe.tsx +5 -6
- package/src/ZoomControls.tsx +5 -2
- package/src/clipboard.ts +4 -5
- package/src/combobox/parts.tsx +18 -14
- package/src/command-palette/context.ts +2 -0
- package/src/command-palette/index.parts.ts +1 -0
- package/src/command-palette/parts.tsx +20 -14
- package/src/command-palette/virtual.tsx +21 -0
- package/src/dialog/parts.tsx +10 -13
- package/src/dialog-layer.ts +5 -3
- package/src/error-boundary.ts +7 -20
- package/src/file-icon.ts +8 -9
- package/src/file-tree/empty-lines.ts +24 -8
- package/src/file-tree/helpers.ts +34 -0
- package/src/file-tree/highlight.ts +19 -0
- package/src/file-tree/index.parts.ts +1 -0
- package/src/file-tree/parts.tsx +41 -5
- package/src/file-tree/use-empty-lines.ts +22 -5
- package/src/file-tree/use-file-tree-shift.tsx +10 -0
- package/src/file-tree/use-file-tree.ts +2 -0
- package/src/file-tree/use-slots.ts +77 -12
- package/src/format/datetime.ts +42 -17
- package/src/format/duration.ts +28 -10
- package/src/format/index.ts +3 -0
- package/src/format/locale.ts +33 -42
- package/src/format/number.ts +21 -27
- package/src/format/regions.ts +4 -7
- package/src/icon-picker.ts +23 -16
- package/src/index.ts +8 -0
- package/src/key-value-list/parts.tsx +1 -5
- package/src/lazy-landings.ts +2 -0
- package/src/lazy.tsx +23 -19
- package/src/locales.ts +2 -6
- package/src/match.ts +14 -0
- package/src/menu/parts.tsx +30 -55
- package/src/middle-truncate.ts +12 -3
- package/src/modality.ts +4 -0
- package/src/placement.ts +2 -1
- package/src/platform.ts +2 -0
- package/src/popover/parts.tsx +3 -5
- package/src/popup-layer.ts +6 -4
- package/src/prefetch.ts +18 -5
- package/src/project-switcher-row.tsx +2 -0
- package/src/segmented-keys.ts +6 -0
- package/src/select-shared.ts +2 -0
- package/src/selection.ts +5 -5
- package/src/settings/ConfirmDialog.tsx +3 -0
- package/src/settings/MasterDetail.tsx +3 -9
- package/src/settings/SettingsDialog.tsx +27 -9
- package/src/settings/SettingsRow.tsx +16 -10
- package/src/settings/SettingsSection.tsx +10 -5
- package/src/settings/index.ts +3 -0
- package/src/settings/target.ts +1 -0
- package/src/shortcut-hints.ts +10 -5
- package/src/shortcut.ts +26 -6
- package/src/tabs/overflow.ts +8 -6
- package/src/tabs/parts.tsx +50 -26
- package/src/testing/dedupe.ts +27 -7
- package/src/testing/fake-source.ts +1 -0
- package/src/tint.ts +3 -5
- package/src/toast-store.ts +20 -14
- package/src/tree/parts.tsx +8 -0
- package/src/tree/shift.ts +6 -0
- package/src/tree/use-tree-shift.tsx +23 -7
- package/src/useAsyncAction.ts +8 -0
- package/src/useColumnResize.ts +24 -15
- package/src/useContentSize.ts +5 -0
- package/src/useMeasuredWidth.ts +1 -0
- package/src/useNow.ts +6 -11
- package/src/workspace/DropPreview.tsx +3 -5
- package/src/workspace/SplitView.tsx +109 -0
- package/src/workspace/TabStrip.tsx +42 -0
- package/src/workspace/Workspace.tsx +5 -0
- package/src/workspace/context.ts +1 -0
- package/src/workspace/geometry.ts +49 -0
- package/src/workspace/model.ts +72 -0
- package/src/workspace/preview.ts +15 -15
|
@@ -11,6 +11,7 @@ export interface RowSlot {
|
|
|
11
11
|
row: FileTreeVisibleRow;
|
|
12
12
|
control: HTMLElement;
|
|
13
13
|
decoration: HTMLElement;
|
|
14
|
+
|
|
14
15
|
/* Where a row's own icon goes in place of the engine's glyph; null when the row keeps that glyph. */
|
|
15
16
|
icon: HTMLElement | null;
|
|
16
17
|
}
|
|
@@ -29,31 +30,30 @@ export interface Port {
|
|
|
29
30
|
export type FileTreeRowIcon = (row: FileTreeVisibleRow) => LucideIcon | undefined;
|
|
30
31
|
|
|
31
32
|
/*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* the spacer is missing or the model already moved past what is mounted.
|
|
33
|
+
* Read from the spacer beta.6 puts above the mounted window, so a scroll frame copies those rows and not
|
|
34
|
+
* every visible one. The whole list stands in when the spacer is missing or the model moved past it.
|
|
35
35
|
*/
|
|
36
36
|
function mountedRows(model: FileTree, shadow: ShadowRoot, elements: readonly HTMLElement[]): FileTreeVisibleRow[] {
|
|
37
37
|
const spacer = shadow.querySelector<HTMLElement>('[data-file-tree-virtualized-sticky-offset]');
|
|
38
38
|
const offset = spacer === null ? Number.NaN : Number.parseFloat(spacer.style.height);
|
|
39
39
|
const itemHeight = model.getItemHeight();
|
|
40
|
+
|
|
40
41
|
if (!Number.isFinite(offset) || itemHeight <= 0) {
|
|
41
42
|
return visibleRows(model);
|
|
42
43
|
}
|
|
44
|
+
|
|
43
45
|
// A folder pinned to the top leaves the flow, but still takes its index in the window.
|
|
44
46
|
const pinned = shadow.querySelectorAll('[data-file-tree-sticky-row="true"]').length;
|
|
45
47
|
const rows = visibleRowsAround(model, Math.round(offset / itemHeight), elements.length + pinned);
|
|
46
48
|
const paths = new Set(rows.map((row) => row.path));
|
|
49
|
+
|
|
47
50
|
return elements.every((element) => element.dataset.itemPath === undefined || paths.has(element.dataset.itemPath)) ? rows : visibleRows(model);
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
/*
|
|
53
|
+
/* Relative to the engine's git status column. */
|
|
51
54
|
export type FileTreeDecorationOrder = 'before-status' | 'after-status';
|
|
52
55
|
|
|
53
|
-
/*
|
|
54
|
-
* Lines the ports up with their rows: every size first, then the spaces they reserve, then every offset,
|
|
55
|
-
* then the ports, so a pass forces two layouts at most however many rows are mounted.
|
|
56
|
-
*/
|
|
56
|
+
/* Every size first, then the spaces, then every offset, then the ports, so a pass forces two layouts at most. */
|
|
57
57
|
export function positionPorts(ports: Iterable<Port>, order: FileTreeDecorationOrder = 'before-status'): void {
|
|
58
58
|
const spaced = [...ports].map((port) => ({
|
|
59
59
|
port,
|
|
@@ -62,16 +62,19 @@ export function positionPorts(ports: Iterable<Port>, order: FileTreeDecorationOr
|
|
|
62
62
|
control: port.lightControl.childNodes.length > 0 ? Math.ceil(port.lightControl.getBoundingClientRect().width) + 6 : 0,
|
|
63
63
|
decoration: port.lightDecoration.childNodes.length > 0 ? Math.ceil(port.lightDecoration.getBoundingClientRect().width) + 6 : 0
|
|
64
64
|
}));
|
|
65
|
+
|
|
65
66
|
for (const { port, status, control, decoration } of spaced) {
|
|
66
67
|
setSpace(port.rowElement, '--adecore-tree-control-space', control);
|
|
67
68
|
setSpace(port.rowElement, '--adecore-tree-decoration-space', status === null ? decoration : 0);
|
|
68
69
|
setSpace(port.rowElement, '--adecore-tree-status-space', status === null ? 0 : decoration);
|
|
69
70
|
}
|
|
71
|
+
|
|
70
72
|
const placed = spaced.map(({ port, status, control, decoration }) => {
|
|
71
73
|
const row = port.rowElement;
|
|
72
74
|
const before = status ?? row.querySelector<HTMLElement>('[data-item-section="content"]');
|
|
73
75
|
const icon = row.querySelector<HTMLElement>('[data-item-section="icon"]');
|
|
74
76
|
const iconLeft = row.offsetLeft + (icon?.offsetLeft ?? 4);
|
|
77
|
+
|
|
75
78
|
return {
|
|
76
79
|
port,
|
|
77
80
|
top: `${row.offsetTop}px`,
|
|
@@ -80,11 +83,13 @@ export function positionPorts(ports: Iterable<Port>, order: FileTreeDecorationOr
|
|
|
80
83
|
decorationLeft: `${row.offsetLeft + (before === null ? row.offsetWidth - decoration - 2 : before.offsetLeft + before.offsetWidth + 6)}px`
|
|
81
84
|
};
|
|
82
85
|
});
|
|
86
|
+
|
|
83
87
|
for (const { port, top, iconLeft, controlLeft, decorationLeft } of placed) {
|
|
84
88
|
port.control.style.top = top;
|
|
85
89
|
port.control.style.left = controlLeft;
|
|
86
90
|
port.decoration.style.top = top;
|
|
87
91
|
port.decoration.style.left = decorationLeft;
|
|
92
|
+
|
|
88
93
|
if (port.icon !== undefined) {
|
|
89
94
|
port.icon.style.top = top;
|
|
90
95
|
port.icon.style.left = iconLeft;
|
|
@@ -95,6 +100,7 @@ export function positionPorts(ports: Iterable<Port>, order: FileTreeDecorationOr
|
|
|
95
100
|
// An unchanged space is left alone, so a pass that only scrolled measures once.
|
|
96
101
|
function setSpace(row: HTMLElement, name: string, space: number): void {
|
|
97
102
|
const value = `${space}px`;
|
|
103
|
+
|
|
98
104
|
if (row.style.getPropertyValue(name) !== value) {
|
|
99
105
|
row.style.setProperty(name, value);
|
|
100
106
|
}
|
|
@@ -103,8 +109,10 @@ function setSpace(row: HTMLElement, name: string, space: number): void {
|
|
|
103
109
|
export interface FileTreeSlotOptions {
|
|
104
110
|
order: FileTreeDecorationOrder;
|
|
105
111
|
rowIcon?: FileTreeRowIcon;
|
|
106
|
-
|
|
112
|
+
|
|
113
|
+
/* Empty marks nothing. */
|
|
107
114
|
highlight: string;
|
|
115
|
+
|
|
108
116
|
emptyLabel?: FileTreeEmptyLabel;
|
|
109
117
|
}
|
|
110
118
|
|
|
@@ -114,6 +122,7 @@ function markEmptyLine(element: HTMLElement, label: string): void {
|
|
|
114
122
|
element.dataset.interactive = 'false';
|
|
115
123
|
element.setAttribute('aria-hidden', 'true');
|
|
116
124
|
const content = element.querySelector<HTMLElement>('[data-item-section="content"]');
|
|
125
|
+
|
|
117
126
|
if (content !== null && content.dataset.emptyLabel !== label) {
|
|
118
127
|
content.dataset.emptyLabel = label;
|
|
119
128
|
}
|
|
@@ -124,9 +133,11 @@ function unmarkEmptyLine(element: HTMLElement): void {
|
|
|
124
133
|
if (element.dataset.emptyLine === undefined) {
|
|
125
134
|
return;
|
|
126
135
|
}
|
|
136
|
+
|
|
127
137
|
delete element.dataset.emptyLine;
|
|
128
138
|
element.removeAttribute('aria-hidden');
|
|
129
139
|
const content = element.querySelector<HTMLElement>('[data-item-section="content"]');
|
|
140
|
+
|
|
130
141
|
if (content !== null) {
|
|
131
142
|
delete content.dataset.emptyLabel;
|
|
132
143
|
}
|
|
@@ -152,6 +163,7 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
152
163
|
if (frame === null) {
|
|
153
164
|
return;
|
|
154
165
|
}
|
|
166
|
+
|
|
155
167
|
const ports = new Map<HTMLElement, Port>();
|
|
156
168
|
let shadow: ShadowRoot | null = null;
|
|
157
169
|
let style: HTMLStyleElement | null = null;
|
|
@@ -163,13 +175,17 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
163
175
|
|
|
164
176
|
const mark = (rows: readonly HTMLElement[], query: string): void => {
|
|
165
177
|
const matches = matchHighlight();
|
|
178
|
+
|
|
166
179
|
if (matches === null) {
|
|
167
180
|
return;
|
|
168
181
|
}
|
|
182
|
+
|
|
169
183
|
for (const range of marked) {
|
|
170
184
|
matches.delete(range);
|
|
171
185
|
}
|
|
186
|
+
|
|
172
187
|
marked = rangesOfMatches(rows, query);
|
|
188
|
+
|
|
173
189
|
for (const range of marked) {
|
|
174
190
|
matches.add(range);
|
|
175
191
|
}
|
|
@@ -178,6 +194,7 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
178
194
|
const position = (): void => positionPorts(ports.values(), order);
|
|
179
195
|
|
|
180
196
|
const resize = new ResizeObserver(position);
|
|
197
|
+
|
|
181
198
|
const dropPort = (port: Port): void => {
|
|
182
199
|
port.control.remove();
|
|
183
200
|
port.decoration.remove();
|
|
@@ -187,13 +204,16 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
187
204
|
port.lightDecoration.remove();
|
|
188
205
|
dropIcon(port);
|
|
189
206
|
};
|
|
207
|
+
|
|
190
208
|
const clearPort = (port: Port): void => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
209
|
+
for (const name of ['--adecore-tree-control-space', '--adecore-tree-decoration-space', '--adecore-tree-status-space']) {
|
|
210
|
+
port.rowElement.style.removeProperty(name);
|
|
211
|
+
}
|
|
212
|
+
|
|
194
213
|
delete port.rowElement.dataset.ownIcon;
|
|
195
214
|
dropPort(port);
|
|
196
215
|
};
|
|
216
|
+
|
|
197
217
|
const dropIcon = (port: Port): void => {
|
|
198
218
|
port.unhover?.();
|
|
199
219
|
port.icon?.remove();
|
|
@@ -202,6 +222,7 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
202
222
|
port.lightIcon = undefined;
|
|
203
223
|
port.unhover = undefined;
|
|
204
224
|
};
|
|
225
|
+
|
|
205
226
|
// The icon lets every pointer event through to the row under it, which then opens, folds or selects as it would.
|
|
206
227
|
const makeIcon = (port: Port, host: HTMLElement): void => {
|
|
207
228
|
const container = document.createElement('span');
|
|
@@ -215,24 +236,29 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
215
236
|
target.slot = slot.name;
|
|
216
237
|
target.className = 'adecore-tree-icon';
|
|
217
238
|
host.append(target);
|
|
239
|
+
|
|
218
240
|
// A folder under the pointer gives its chevron back, so the row still says it opens.
|
|
219
241
|
const show = (): void => {
|
|
220
242
|
if (port.rowElement.dataset.itemType === 'folder') {
|
|
221
243
|
container.dataset.hover = 'true';
|
|
222
244
|
}
|
|
223
245
|
};
|
|
246
|
+
|
|
224
247
|
const hide = (): void => {
|
|
225
248
|
delete container.dataset.hover;
|
|
226
249
|
};
|
|
250
|
+
|
|
227
251
|
port.rowElement.addEventListener('pointerenter', show);
|
|
228
252
|
port.rowElement.addEventListener('pointerleave', hide);
|
|
229
253
|
port.icon = container;
|
|
230
254
|
port.lightIcon = target;
|
|
255
|
+
|
|
231
256
|
port.unhover = () => {
|
|
232
257
|
port.rowElement.removeEventListener('pointerenter', show);
|
|
233
258
|
port.rowElement.removeEventListener('pointerleave', hide);
|
|
234
259
|
};
|
|
235
260
|
};
|
|
261
|
+
|
|
236
262
|
const makePort = (element: HTMLElement, host: HTMLElement, kind: 'control' | 'decoration'): { container: HTMLElement; target: HTMLElement } => {
|
|
237
263
|
const container = document.createElement('span');
|
|
238
264
|
container.dataset.treePort = kind;
|
|
@@ -244,80 +270,104 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
244
270
|
target.slot = slot.name;
|
|
245
271
|
target.dataset.treeSlot = kind;
|
|
246
272
|
target.className = kind === 'control' ? 'adecore-tree-control' : 'adecore-tree-decoration';
|
|
273
|
+
|
|
247
274
|
// React delegates to this portal target before the event reaches the engine's shadow tree.
|
|
248
275
|
for (const type of ['click', 'dblclick', 'keydown', 'keyup', 'pointerdown', 'dragstart']) {
|
|
249
276
|
target.addEventListener(type, (event) => event.stopPropagation());
|
|
250
277
|
}
|
|
278
|
+
|
|
251
279
|
host.append(target);
|
|
252
280
|
resize.observe(target);
|
|
281
|
+
|
|
253
282
|
return { container, target };
|
|
254
283
|
};
|
|
284
|
+
|
|
255
285
|
const scan = (): void => {
|
|
256
286
|
pending = false;
|
|
287
|
+
|
|
257
288
|
if (disposed) {
|
|
258
289
|
return;
|
|
259
290
|
}
|
|
291
|
+
|
|
260
292
|
const current = model.getFileTreeContainer();
|
|
293
|
+
|
|
261
294
|
if (!current?.shadowRoot) {
|
|
262
295
|
return;
|
|
263
296
|
}
|
|
297
|
+
|
|
264
298
|
if (shadow !== current.shadowRoot) {
|
|
265
299
|
shadow = current.shadowRoot;
|
|
266
300
|
style = document.createElement('style');
|
|
267
301
|
style.dataset.adecoreTreeStyle = 'true';
|
|
268
302
|
style.textContent = FILE_TREE_CSS;
|
|
269
303
|
shadow.append(style);
|
|
304
|
+
|
|
270
305
|
changes.observe(shadow, {
|
|
271
306
|
childList: true,
|
|
272
307
|
subtree: true,
|
|
273
308
|
attributes: true,
|
|
274
309
|
attributeFilter: ['data-item-path', 'aria-selected', 'aria-expanded']
|
|
275
310
|
});
|
|
311
|
+
|
|
276
312
|
changes.observe(current, { childList: true, subtree: true });
|
|
277
313
|
resize.observe(current);
|
|
278
314
|
}
|
|
315
|
+
|
|
279
316
|
shadow.querySelector('[role="tree"]')?.setAttribute('aria-label', label);
|
|
317
|
+
|
|
280
318
|
const elements = [
|
|
281
319
|
...shadow.querySelectorAll<HTMLElement>('[data-type="item"]:not([data-item-parked="true"]):not([data-file-tree-sticky-row="true"])')
|
|
282
320
|
];
|
|
321
|
+
|
|
283
322
|
const rows = mountedRows(model, shadow, elements);
|
|
284
323
|
const byPath = new Map(rows.map((row) => [pathOfRow(row), row]));
|
|
285
324
|
const byIndex = new Map(rows.map((row) => [row.index, row]));
|
|
286
325
|
const live = new Set(elements);
|
|
326
|
+
|
|
287
327
|
for (const [element, port] of ports) {
|
|
288
328
|
if (!live.has(element)) {
|
|
289
329
|
dropPort(port);
|
|
290
330
|
ports.delete(element);
|
|
291
331
|
}
|
|
292
332
|
}
|
|
333
|
+
|
|
293
334
|
const next: RowSlot[] = [];
|
|
294
335
|
const named: HTMLElement[] = [];
|
|
336
|
+
|
|
295
337
|
for (const element of elements) {
|
|
296
338
|
const path = element.dataset.itemPath;
|
|
297
339
|
const row = path === undefined ? undefined : byPath.get(path);
|
|
340
|
+
|
|
298
341
|
if (row === undefined) {
|
|
299
342
|
continue;
|
|
300
343
|
}
|
|
344
|
+
|
|
301
345
|
if (isEmptyLine(row.path)) {
|
|
302
346
|
const recycled = ports.get(element);
|
|
347
|
+
|
|
303
348
|
if (recycled !== undefined) {
|
|
304
349
|
clearPort(recycled);
|
|
305
350
|
ports.delete(element);
|
|
306
351
|
}
|
|
352
|
+
|
|
307
353
|
const directory = byPath.get(directoryOfEmptyLine(row.path));
|
|
308
354
|
markEmptyLine(element, (directory === undefined ? undefined : latestEmpty.current?.(directory)) ?? '');
|
|
355
|
+
|
|
309
356
|
continue;
|
|
310
357
|
}
|
|
358
|
+
|
|
311
359
|
unmarkEmptyLine(element);
|
|
312
360
|
named.push(element);
|
|
313
361
|
element.dataset.interactive = 'true';
|
|
314
362
|
element.dataset.joinedStart = String(row.isSelected && byIndex.get(row.index - 1)?.isSelected === true);
|
|
315
363
|
element.dataset.joinedEnd = String(row.isSelected && byIndex.get(row.index + 1)?.isSelected === true);
|
|
316
364
|
let port = ports.get(element);
|
|
365
|
+
|
|
317
366
|
if (port === undefined) {
|
|
318
367
|
serial++;
|
|
319
368
|
const control = makePort(element, current, 'control');
|
|
320
369
|
const decoration = makePort(element, current, 'decoration');
|
|
370
|
+
|
|
321
371
|
port = {
|
|
322
372
|
rowElement: element,
|
|
323
373
|
control: control.container,
|
|
@@ -325,52 +375,65 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
325
375
|
lightControl: control.target,
|
|
326
376
|
lightDecoration: decoration.target
|
|
327
377
|
};
|
|
378
|
+
|
|
328
379
|
ports.set(element, port);
|
|
329
380
|
}
|
|
381
|
+
|
|
330
382
|
if (port.control.dataset.itemPath !== row.path) {
|
|
331
383
|
port.control.dataset.itemPath = row.path;
|
|
332
384
|
}
|
|
385
|
+
|
|
333
386
|
if (port.decoration.dataset.itemPath !== row.path) {
|
|
334
387
|
port.decoration.dataset.itemPath = row.path;
|
|
335
388
|
}
|
|
389
|
+
|
|
336
390
|
port.lightControl.dataset.itemPath = row.path;
|
|
337
391
|
port.lightDecoration.dataset.itemPath = row.path;
|
|
338
392
|
const ownIcon = latestIcon.current?.(row) !== undefined;
|
|
393
|
+
|
|
339
394
|
if (ownIcon && port.icon === undefined) {
|
|
340
395
|
makeIcon(port, current);
|
|
341
396
|
} else if (!ownIcon && port.icon !== undefined) {
|
|
342
397
|
dropIcon(port);
|
|
343
398
|
}
|
|
399
|
+
|
|
344
400
|
if (port.icon !== undefined && port.icon.dataset.itemPath !== row.path) {
|
|
345
401
|
port.icon.dataset.itemPath = row.path;
|
|
346
402
|
}
|
|
403
|
+
|
|
347
404
|
if (ownIcon) {
|
|
348
405
|
element.dataset.ownIcon = 'true';
|
|
349
406
|
} else if (element.dataset.ownIcon !== undefined) {
|
|
350
407
|
delete element.dataset.ownIcon;
|
|
351
408
|
}
|
|
409
|
+
|
|
352
410
|
next.push({ row, control: port.lightControl, decoration: port.lightDecoration, icon: port.lightIcon ?? null });
|
|
353
411
|
}
|
|
412
|
+
|
|
354
413
|
const key = next
|
|
355
414
|
.map(
|
|
356
415
|
({ row, control, icon }) =>
|
|
357
416
|
`${control.slot}:${row.path}:${row.isSelected}:${row.isExpanded}:${row.isFocused}:${row.name}:${icon?.slot ?? ''}`
|
|
358
417
|
)
|
|
359
418
|
.join('|');
|
|
419
|
+
|
|
360
420
|
if (snapshot !== key) {
|
|
361
421
|
snapshot = key;
|
|
362
422
|
// Recycled rows must paint with their new portal contents in the same frame.
|
|
363
423
|
flushSync(() => setSlots(next));
|
|
364
424
|
}
|
|
425
|
+
|
|
365
426
|
mark(named, latestHighlight.current);
|
|
366
427
|
position();
|
|
367
428
|
};
|
|
429
|
+
|
|
368
430
|
const schedule = (): void => {
|
|
369
431
|
if (!pending && !disposed) {
|
|
370
432
|
pending = true;
|
|
371
433
|
queueMicrotask(scan);
|
|
372
434
|
}
|
|
373
435
|
};
|
|
436
|
+
|
|
374
437
|
rescan.current = schedule;
|
|
375
438
|
const shifts = new MutationObserver(position);
|
|
376
439
|
shifts.observe(frame, { attributes: true, attributeFilter: ['style'] });
|
|
@@ -378,6 +441,7 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
378
441
|
changes.observe(frame, { childList: true, subtree: true });
|
|
379
442
|
const unsubscribe = model.subscribe(schedule);
|
|
380
443
|
schedule();
|
|
444
|
+
|
|
381
445
|
return () => {
|
|
382
446
|
disposed = true;
|
|
383
447
|
rescan.current = undefined;
|
|
@@ -387,6 +451,7 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
387
451
|
resize.disconnect();
|
|
388
452
|
style?.remove();
|
|
389
453
|
mark([], '');
|
|
454
|
+
|
|
390
455
|
for (const port of ports.values()) {
|
|
391
456
|
clearPort(port);
|
|
392
457
|
}
|
package/src/format/datetime.ts
CHANGED
|
@@ -7,19 +7,19 @@ const WORD_PARTS = new Set<Intl.DateTimeFormatPartTypes>(['month', 'weekday', 'd
|
|
|
7
7
|
const toDate = (at: Date | number): Date => (typeof at === 'number' ? new Date(at) : at);
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* one to lift the word from, whatever order the two put them in: `19 Sep` in English against a Dutch
|
|
13
|
-
* region, `19 sep` once the interface is Dutch too, and a clock that reads `08:05` wherever the
|
|
14
|
-
* region does not say AM.
|
|
10
|
+
* The region's notation with the language's words: `19 Sep` in English against a Dutch region. Both
|
|
11
|
+
* formatters get the same options and instant, so every word part has one to lift, whatever the order.
|
|
15
12
|
*/
|
|
16
13
|
export const formatDateTime = (at: Date | number, options: Intl.DateTimeFormatOptions): string => {
|
|
17
14
|
const date = toDate(at);
|
|
18
15
|
const parts = dateFormatter(options).formatToParts(date);
|
|
16
|
+
|
|
19
17
|
if (!parts.some((part) => WORD_PARTS.has(part.type))) {
|
|
20
18
|
return parts.map((part) => part.value).join('');
|
|
21
19
|
}
|
|
20
|
+
|
|
22
21
|
const words = wordFormatter(options).formatToParts(date);
|
|
22
|
+
|
|
23
23
|
return parts.map((part) => (WORD_PARTS.has(part.type) ? (words.find((word) => word.type === part.type)?.value ?? part.value) : part.value)).join('');
|
|
24
24
|
};
|
|
25
25
|
|
|
@@ -35,11 +35,36 @@ const DAY_CLOCK: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric',
|
|
|
35
35
|
const DAY_CLOCK_WITH_YEAR: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' };
|
|
36
36
|
const NUMERIC_DATE: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'numeric', day: 'numeric' };
|
|
37
37
|
|
|
38
|
+
const zonedSpecs = new WeakMap<Intl.DateTimeFormatOptions, Map<string, Intl.DateTimeFormatOptions>>();
|
|
39
|
+
|
|
40
|
+
/* One options object per zone, so the formatter cached on it is built once (`dateFormatter`). */
|
|
41
|
+
const zoned = (options: Intl.DateTimeFormatOptions, timeZone: string | undefined): Intl.DateTimeFormatOptions => {
|
|
42
|
+
if (timeZone === undefined) {
|
|
43
|
+
return options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let byZone = zonedSpecs.get(options);
|
|
47
|
+
|
|
48
|
+
if (byZone === undefined) {
|
|
49
|
+
byZone = new Map();
|
|
50
|
+
zonedSpecs.set(options, byZone);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let spec = byZone.get(timeZone);
|
|
54
|
+
|
|
55
|
+
if (spec === undefined) {
|
|
56
|
+
spec = { ...options, timeZone };
|
|
57
|
+
byZone.set(timeZone, spec);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return spec;
|
|
61
|
+
};
|
|
62
|
+
|
|
38
63
|
/*
|
|
39
|
-
* `08:05`, or `08:05 AM` in a region that counts to twelve.
|
|
40
|
-
*
|
|
64
|
+
* `08:05`, or `08:05 AM` in a region that counts to twelve. A `timeZone` such as `UTC` writes a moment
|
|
65
|
+
* whose parts are given and mean the same wall clock anywhere.
|
|
41
66
|
*/
|
|
42
|
-
export const formatClock = (at: Date | number, timeZone?: string): string => formatDateTime(at,
|
|
67
|
+
export const formatClock = (at: Date | number, timeZone?: string): string => formatDateTime(at, zoned(CLOCK, timeZone));
|
|
43
68
|
|
|
44
69
|
/* A time far enough off that its day is part of the answer: `Sat 08:05`. */
|
|
45
70
|
export const formatWeekdayClock = (at: Date | number): string => formatDateTime(at, WEEKDAY_CLOCK);
|
|
@@ -51,30 +76,28 @@ export const formatHour = (at: Date | number): string => formatDateTime(at, HOUR
|
|
|
51
76
|
export const formatWeekday = (at: Date | number, width: 'short' | 'long' = 'short'): string => formatDateTime(at, width === 'long' ? WEEKDAY_LONG : WEEKDAY);
|
|
52
77
|
|
|
53
78
|
/* `19 Sep`, the short date a row falls back to once "days ago" stops meaning anything. */
|
|
54
|
-
export const formatDay = (at: Date | number, timeZone?: string): string => formatDateTime(at,
|
|
79
|
+
export const formatDay = (at: Date | number, timeZone?: string): string => formatDateTime(at, zoned(DAY, timeZone));
|
|
55
80
|
|
|
56
81
|
/* The same date with the year, for anything older than the one we are in. */
|
|
57
|
-
export const formatDayWithYear = (at: Date | number, timeZone?: string): string =>
|
|
58
|
-
formatDateTime(at, timeZone === undefined ? DAY_WITH_YEAR : { ...DAY_WITH_YEAR, timeZone });
|
|
82
|
+
export const formatDayWithYear = (at: Date | number, timeZone?: string): string => formatDateTime(at, zoned(DAY_WITH_YEAR, timeZone));
|
|
59
83
|
|
|
60
84
|
/* `Sat 19 Sep`. */
|
|
61
85
|
export const formatWeekdayDay = (at: Date | number): string => formatDateTime(at, WEEKDAY_DAY);
|
|
62
86
|
|
|
63
87
|
/* `19 Sep, 08:05`: a moment that is not today, to the minute. */
|
|
64
|
-
export const formatDayClock = (at: Date | number, timeZone?: string): string =>
|
|
65
|
-
formatDateTime(at, timeZone === undefined ? DAY_CLOCK : { ...DAY_CLOCK, timeZone });
|
|
88
|
+
export const formatDayClock = (at: Date | number, timeZone?: string): string => formatDateTime(at, zoned(DAY_CLOCK, timeZone));
|
|
66
89
|
|
|
67
|
-
/* `19 Sep 2026, 08:05`: the same moment with the year
|
|
68
|
-
export const formatDayClockWithYear = (at: Date | number, timeZone?: string): string =>
|
|
69
|
-
formatDateTime(at, timeZone === undefined ? DAY_CLOCK_WITH_YEAR : { ...DAY_CLOCK_WITH_YEAR, timeZone });
|
|
90
|
+
/* `19 Sep 2026, 08:05`: the same moment with the year. */
|
|
91
|
+
export const formatDayClockWithYear = (at: Date | number, timeZone?: string): string => formatDateTime(at, zoned(DAY_CLOCK_WITH_YEAR, timeZone));
|
|
70
92
|
|
|
71
93
|
/* All numbers, as a form would print it: `19-9-2026` here, `9/19/2026` in the United States. */
|
|
72
94
|
export const formatNumericDate = (at: Date | number): string => formatDateTime(at, NUMERIC_DATE);
|
|
73
95
|
|
|
74
|
-
/*
|
|
96
|
+
/* In the zone of whoever is reading. */
|
|
75
97
|
export const isSameDay = (a: Date | number, b: Date | number): boolean => {
|
|
76
98
|
const left = toDate(a);
|
|
77
99
|
const right = toDate(b);
|
|
100
|
+
|
|
78
101
|
return left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth() && left.getDate() === right.getDate();
|
|
79
102
|
};
|
|
80
103
|
|
|
@@ -90,8 +113,10 @@ export const formatAge = (at: Date | number, now: Date | number = Date.now()): s
|
|
|
90
113
|
const date = toDate(at);
|
|
91
114
|
const today = toDate(now);
|
|
92
115
|
const past = today.getTime() - date.getTime();
|
|
116
|
+
|
|
93
117
|
if (past < WEEK) {
|
|
94
118
|
return formatAgo(past);
|
|
95
119
|
}
|
|
120
|
+
|
|
96
121
|
return date.getFullYear() === today.getFullYear() ? formatDay(date) : formatDayWithYear(date);
|
|
97
122
|
};
|
package/src/format/duration.ts
CHANGED
|
@@ -6,20 +6,20 @@ const MINUTE = 60 * SECOND;
|
|
|
6
6
|
const HOUR = 60 * MINUTE;
|
|
7
7
|
const DAY = 24 * HOUR;
|
|
8
8
|
|
|
9
|
-
/*
|
|
10
|
-
* How long something took, at the coarsest unit that still says something. The units are English
|
|
11
|
-
* abbreviations, the way the rest of the interface is written; only the number bends to the region,
|
|
12
|
-
* which is what `1,5 h` against `1.5 h` is about.
|
|
13
|
-
*/
|
|
9
|
+
/* At the coarsest unit that still says something. The units stay English; only the number bends to the region (`1,5 h`). */
|
|
14
10
|
export const formatDuration = (ms: number): string => {
|
|
15
11
|
const seconds = Math.max(0, Math.round(ms / SECOND));
|
|
12
|
+
|
|
16
13
|
if (seconds < 60) {
|
|
17
14
|
return `${formatNumber(seconds)} s`;
|
|
18
15
|
}
|
|
16
|
+
|
|
19
17
|
const minutes = Math.round(seconds / 60);
|
|
18
|
+
|
|
20
19
|
if (minutes < 60) {
|
|
21
20
|
return `${formatNumber(minutes)} min`;
|
|
22
21
|
}
|
|
22
|
+
|
|
23
23
|
return `${formatDecimal(minutes / 60)} h`;
|
|
24
24
|
};
|
|
25
25
|
|
|
@@ -30,61 +30,75 @@ export const formatDuration = (ms: number): string => {
|
|
|
30
30
|
export const formatPreciseDuration = (ms: number): string => {
|
|
31
31
|
const value = Math.max(0, ms);
|
|
32
32
|
const tenths = Math.round(value * 10) / 10;
|
|
33
|
+
|
|
33
34
|
if (tenths < 10) {
|
|
34
35
|
return `${formatRounded(tenths, 1)} ms`;
|
|
35
36
|
}
|
|
37
|
+
|
|
36
38
|
if (Math.round(value) < SECOND) {
|
|
37
39
|
return `${formatNumber(Math.round(value))} ms`;
|
|
38
40
|
}
|
|
41
|
+
|
|
39
42
|
const hundredths = Math.round(value / 10) / 100;
|
|
43
|
+
|
|
40
44
|
if (hundredths < 10) {
|
|
41
45
|
return `${formatRounded(hundredths, 2)} s`;
|
|
42
46
|
}
|
|
47
|
+
|
|
43
48
|
const seconds = Math.round(value / 100) / 10;
|
|
49
|
+
|
|
44
50
|
if (seconds < 60) {
|
|
45
51
|
return `${formatRounded(seconds, 1)} s`;
|
|
46
52
|
}
|
|
53
|
+
|
|
47
54
|
return formatDuration(value);
|
|
48
55
|
};
|
|
49
56
|
|
|
50
57
|
/* How long a window still has, tight enough for the end of a bar: `4d 3h`, `12h 8m`, `9m`. */
|
|
51
58
|
export const formatCountdown = (ms: number): string => {
|
|
52
59
|
const left = Math.max(0, ms);
|
|
60
|
+
|
|
53
61
|
if (left >= DAY) {
|
|
54
62
|
return `${formatNumber(Math.floor(left / DAY))}d ${formatNumber(Math.round((left % DAY) / HOUR))}h`;
|
|
55
63
|
}
|
|
64
|
+
|
|
56
65
|
if (left >= HOUR) {
|
|
57
66
|
return `${formatNumber(Math.floor(left / HOUR))}h ${formatNumber(Math.round((left % HOUR) / MINUTE))}m`;
|
|
58
67
|
}
|
|
68
|
+
|
|
59
69
|
return `${formatNumber(Math.max(1, Math.round(left / MINUTE)))}m`;
|
|
60
70
|
};
|
|
61
71
|
|
|
62
|
-
/*
|
|
63
|
-
* How long something has been running, to two units and no space to spare: `12s`, `2m 5s`, `3h 20m`.
|
|
64
|
-
* A call that took a fraction of a second still took some, so anything above zero reads as at least
|
|
65
|
-
* `1s`; only a clock that runs ahead of the start comes out as `0s`.
|
|
66
|
-
*/
|
|
72
|
+
/* `12s`, `2m 5s`, `3h 20m`. Anything above zero reads as at least `1s`; only a clock that runs ahead of the start comes out as `0s`. */
|
|
67
73
|
export const formatElapsedShort = (ms: number): string => {
|
|
68
74
|
const seconds = ms <= 0 ? 0 : Math.max(1, Math.round(ms / SECOND));
|
|
75
|
+
|
|
69
76
|
if (seconds < 60) {
|
|
70
77
|
return `${formatNumber(seconds)}s`;
|
|
71
78
|
}
|
|
79
|
+
|
|
72
80
|
if (seconds < 3600) {
|
|
73
81
|
const rest = seconds % 60;
|
|
74
82
|
const minutes = Math.floor(seconds / 60);
|
|
83
|
+
|
|
75
84
|
return rest === 0 ? `${formatNumber(minutes)}m` : `${formatNumber(minutes)}m ${formatNumber(rest)}s`;
|
|
76
85
|
}
|
|
86
|
+
|
|
77
87
|
const hours = Math.floor(seconds / 3600);
|
|
78
88
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
89
|
+
|
|
79
90
|
return minutes === 0 ? `${formatNumber(hours)}h` : `${formatNumber(hours)}h ${formatNumber(minutes)}m`;
|
|
80
91
|
};
|
|
81
92
|
|
|
82
93
|
/* A stopwatch that is watched while it runs: `00:14`, and `1:02:03` once it passes an hour. */
|
|
83
94
|
export const formatClockDuration = (ms: number): string => {
|
|
84
95
|
const seconds = Math.max(0, Math.floor(ms / SECOND));
|
|
96
|
+
|
|
85
97
|
const pad = (value: number): string => String(value).padStart(2, '0');
|
|
98
|
+
|
|
86
99
|
const clock = `${pad(Math.floor((seconds % 3600) / 60))}:${pad(seconds % 60)}`;
|
|
87
100
|
const hours = Math.floor(seconds / 3600);
|
|
101
|
+
|
|
88
102
|
return hours > 0 ? `${formatNumber(hours)}:${clock}` : clock;
|
|
89
103
|
};
|
|
90
104
|
|
|
@@ -94,14 +108,18 @@ const NOW: Intl.RelativeTimeFormatOptions = { style: 'narrow', numeric: 'auto' }
|
|
|
94
108
|
/* How long ago something was, short enough for the right edge of a row: `3m ago`, `3 min. geleden`. */
|
|
95
109
|
export const formatAgo = (ms: number): string => {
|
|
96
110
|
const past = Math.max(0, ms);
|
|
111
|
+
|
|
97
112
|
if (past < MINUTE) {
|
|
98
113
|
return relativeFormatter(NOW).format(0, 'second');
|
|
99
114
|
}
|
|
115
|
+
|
|
100
116
|
if (past < HOUR) {
|
|
101
117
|
return relativeFormatter(AGO).format(-Math.floor(past / MINUTE), 'minute');
|
|
102
118
|
}
|
|
119
|
+
|
|
103
120
|
if (past < DAY) {
|
|
104
121
|
return relativeFormatter(AGO).format(-Math.floor(past / HOUR), 'hour');
|
|
105
122
|
}
|
|
123
|
+
|
|
106
124
|
return relativeFormatter(AGO).format(-Math.floor(past / DAY), 'day');
|
|
107
125
|
};
|
package/src/format/index.ts
CHANGED
|
@@ -16,8 +16,10 @@ export {
|
|
|
16
16
|
formatWeekdayDay,
|
|
17
17
|
isSameDay
|
|
18
18
|
} from './datetime.ts';
|
|
19
|
+
|
|
19
20
|
export { formatAgo, formatClockDuration, formatCountdown, formatDuration, formatElapsedShort, formatPreciseDuration } from './duration.ts';
|
|
20
21
|
export { formatLocale, labelCollator, nameCollator, setFormatSource, systemLocale, useFormatLocale, type FormatSource } from './locale.ts';
|
|
22
|
+
|
|
21
23
|
export {
|
|
22
24
|
formatBytes,
|
|
23
25
|
formatDecimal,
|
|
@@ -29,5 +31,6 @@ export {
|
|
|
29
31
|
formatTokens,
|
|
30
32
|
formatUsdSignificant
|
|
31
33
|
} from './number.ts';
|
|
34
|
+
|
|
32
35
|
export { FORMAT_LANGUAGE, FORMAT_REGIONS, FORMAT_REGION_CHOICES, FORMAT_SYSTEM, formatRegionFrom, regionName } from './regions.ts';
|
|
33
36
|
export { localTimeZone } from './time-zone.ts';
|