@adecore/ui 0.38.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.
Files changed (210) hide show
  1. package/dist/AccentSwatches.js +2 -3
  2. package/dist/Banner.js +1 -6
  3. package/dist/ChoiceCards.js +0 -5
  4. package/dist/ColorSwatch.js +1 -4
  5. package/dist/ColorWheelPicker.js +3 -6
  6. package/dist/DockShell.js +3 -5
  7. package/dist/EmptyState.js +0 -1
  8. package/dist/ErrorBoundary.js +1 -5
  9. package/dist/Field.js +2 -6
  10. package/dist/FileIcon.js +2 -4
  11. package/dist/Icon.js +1 -2
  12. package/dist/IconButton.js +1 -1
  13. package/dist/IconPicker.js +3 -7
  14. package/dist/Input.js +1 -2
  15. package/dist/Kbd.js +1 -3
  16. package/dist/LazyBoundary.js +2 -3
  17. package/dist/Meter.js +0 -1
  18. package/dist/MiddleTruncate.js +2 -6
  19. package/dist/PanelEmpty.js +0 -1
  20. package/dist/PanelHeader.js +1 -4
  21. package/dist/Pill.js +1 -6
  22. package/dist/ProjectSwitcher.js +1 -1
  23. package/dist/PromptDialog.js +5 -8
  24. package/dist/RelativeTime.js +1 -1
  25. package/dist/SegmentBar.js +1 -2
  26. package/dist/Segmented.js +2 -5
  27. package/dist/Select.js +1 -4
  28. package/dist/ShortcutHints.js +2 -3
  29. package/dist/Slider.js +0 -1
  30. package/dist/SlidingColumn.js +2 -4
  31. package/dist/SplitButton.js +1 -5
  32. package/dist/TextMenu.js +1 -5
  33. package/dist/Tile.js +0 -4
  34. package/dist/Toasts.js +1 -9
  35. package/dist/Tooltip.js +1 -1
  36. package/dist/UIProvider.js +0 -5
  37. package/dist/Waveform.js +4 -2
  38. package/dist/Wipe.js +1 -6
  39. package/dist/ZoomControls.js +0 -1
  40. package/dist/clipboard.js +2 -5
  41. package/dist/combobox/parts.js +3 -9
  42. package/dist/command-palette/parts.js +5 -14
  43. package/dist/dialog/parts.js +5 -10
  44. package/dist/dialog-layer.js +2 -3
  45. package/dist/error-boundary.js +4 -20
  46. package/dist/file-icon.js +4 -9
  47. package/dist/file-tree/empty-lines.js +2 -8
  48. package/dist/file-tree/use-empty-lines.js +1 -5
  49. package/dist/file-tree/use-slots.js +6 -10
  50. package/dist/format/datetime.js +32 -29
  51. package/dist/format/duration.js +2 -10
  52. package/dist/format/locale.js +19 -39
  53. package/dist/format/number.js +9 -22
  54. package/dist/format/regions.js +4 -7
  55. package/dist/icon-picker.js +5 -14
  56. package/dist/index.d.ts +1 -0
  57. package/dist/index.js +1 -0
  58. package/dist/key-value-list/parts.js +1 -5
  59. package/dist/lazy.js +4 -19
  60. package/dist/locales.js +2 -6
  61. package/dist/menu/parts.js +61 -133
  62. package/dist/middle-truncate.js +2 -3
  63. package/dist/placement.js +0 -1
  64. package/dist/popover/parts.js +1 -5
  65. package/dist/popup-layer.js +2 -4
  66. package/dist/prefetch.js +2 -5
  67. package/dist/selection.js +1 -5
  68. package/dist/settings/MasterDetail.js +1 -7
  69. package/dist/settings/SettingsDialog.js +1 -2
  70. package/dist/settings/SettingsRow.js +2 -5
  71. package/dist/settings/SettingsSection.js +1 -1
  72. package/dist/shortcut-hints.js +1 -5
  73. package/dist/shortcut.js +1 -2
  74. package/dist/tabs/overflow.js +1 -6
  75. package/dist/tabs/parts.js +69 -95
  76. package/dist/testing/dedupe.js +5 -7
  77. package/dist/tint.d.ts +2 -0
  78. package/dist/tint.js +11 -0
  79. package/dist/toast-store.js +2 -10
  80. package/dist/tree/use-tree-shift.js +2 -6
  81. package/dist/useColumnResize.js +9 -10
  82. package/dist/useNow.js +2 -9
  83. package/dist/workspace/DropPreview.js +1 -5
  84. package/dist/workspace/preview.js +4 -9
  85. package/package.json +9 -9
  86. package/src/AccentSwatches.tsx +9 -3
  87. package/src/Banner.tsx +5 -11
  88. package/src/Button.tsx +4 -0
  89. package/src/Checkbox.tsx +3 -0
  90. package/src/ChoiceCards.tsx +12 -11
  91. package/src/CloseButton.tsx +1 -0
  92. package/src/ColorSwatch.tsx +7 -8
  93. package/src/ColorWheelPicker.tsx +28 -12
  94. package/src/ColumnResizeHandle.tsx +1 -0
  95. package/src/CopyButton.tsx +6 -0
  96. package/src/DisabledReason.tsx +1 -0
  97. package/src/DockShell.tsx +11 -10
  98. package/src/EmptyState.tsx +10 -6
  99. package/src/ErrorBoundary.tsx +13 -10
  100. package/src/Field.tsx +11 -13
  101. package/src/FileIcon.tsx +4 -4
  102. package/src/Icon.tsx +2 -4
  103. package/src/IconButton.tsx +14 -5
  104. package/src/IconPicker.tsx +30 -15
  105. package/src/Input.tsx +11 -9
  106. package/src/Kbd.tsx +8 -7
  107. package/src/LazyBoundary.tsx +3 -3
  108. package/src/Meter.tsx +7 -2
  109. package/src/MiddleTruncate.tsx +15 -6
  110. package/src/PanelEmpty.tsx +9 -7
  111. package/src/PanelHeader.tsx +2 -5
  112. package/src/Pill.tsx +7 -8
  113. package/src/ProjectSwitcher.tsx +16 -6
  114. package/src/PromptDialog.tsx +31 -26
  115. package/src/RelativeTime.tsx +6 -1
  116. package/src/SegmentBar.tsx +22 -11
  117. package/src/Segmented.tsx +16 -11
  118. package/src/Select.tsx +7 -4
  119. package/src/SelectSearch.tsx +3 -0
  120. package/src/ShortcutHints.tsx +24 -3
  121. package/src/Skeleton.tsx +1 -0
  122. package/src/Slider.tsx +4 -1
  123. package/src/SlidingColumn.tsx +18 -14
  124. package/src/Spinner.tsx +1 -0
  125. package/src/SplitButton.tsx +8 -8
  126. package/src/Stepper.tsx +6 -0
  127. package/src/Switch.tsx +2 -0
  128. package/src/TextMenu.tsx +2 -5
  129. package/src/Tile.tsx +2 -6
  130. package/src/Toasts.tsx +8 -12
  131. package/src/Tooltip.tsx +10 -5
  132. package/src/UIProvider.tsx +7 -7
  133. package/src/Waveform.tsx +41 -12
  134. package/src/Wipe.tsx +5 -6
  135. package/src/ZoomControls.tsx +5 -2
  136. package/src/clipboard.ts +4 -5
  137. package/src/combobox/parts.tsx +18 -14
  138. package/src/command-palette/context.ts +2 -0
  139. package/src/command-palette/index.parts.ts +1 -0
  140. package/src/command-palette/parts.tsx +20 -14
  141. package/src/command-palette/virtual.tsx +21 -0
  142. package/src/dialog/parts.tsx +10 -13
  143. package/src/dialog-layer.ts +5 -3
  144. package/src/error-boundary.ts +7 -20
  145. package/src/file-icon.ts +8 -9
  146. package/src/file-tree/empty-lines.ts +24 -8
  147. package/src/file-tree/helpers.ts +34 -0
  148. package/src/file-tree/highlight.ts +19 -0
  149. package/src/file-tree/index.parts.ts +1 -0
  150. package/src/file-tree/parts.tsx +41 -5
  151. package/src/file-tree/use-empty-lines.ts +22 -5
  152. package/src/file-tree/use-file-tree-shift.tsx +10 -0
  153. package/src/file-tree/use-file-tree.ts +2 -0
  154. package/src/file-tree/use-slots.ts +77 -12
  155. package/src/format/datetime.ts +42 -17
  156. package/src/format/duration.ts +28 -10
  157. package/src/format/index.ts +3 -0
  158. package/src/format/locale.ts +33 -42
  159. package/src/format/number.ts +21 -27
  160. package/src/format/regions.ts +4 -7
  161. package/src/icon-picker.ts +23 -16
  162. package/src/index.ts +9 -0
  163. package/src/key-value-list/parts.tsx +1 -5
  164. package/src/lazy-landings.ts +2 -0
  165. package/src/lazy.tsx +23 -19
  166. package/src/locales.ts +2 -6
  167. package/src/match.ts +14 -0
  168. package/src/menu/parts.tsx +30 -55
  169. package/src/middle-truncate.ts +12 -3
  170. package/src/modality.ts +4 -0
  171. package/src/placement.ts +2 -1
  172. package/src/platform.ts +2 -0
  173. package/src/popover/parts.tsx +3 -5
  174. package/src/popup-layer.ts +6 -4
  175. package/src/prefetch.ts +18 -5
  176. package/src/project-switcher-row.tsx +2 -0
  177. package/src/segmented-keys.ts +6 -0
  178. package/src/select-shared.ts +2 -0
  179. package/src/selection.ts +5 -5
  180. package/src/settings/ConfirmDialog.tsx +3 -0
  181. package/src/settings/MasterDetail.tsx +3 -9
  182. package/src/settings/SettingsDialog.tsx +27 -9
  183. package/src/settings/SettingsRow.tsx +16 -10
  184. package/src/settings/SettingsSection.tsx +10 -5
  185. package/src/settings/index.ts +3 -0
  186. package/src/settings/target.ts +1 -0
  187. package/src/shortcut-hints.ts +10 -5
  188. package/src/shortcut.ts +26 -6
  189. package/src/tabs/overflow.ts +8 -6
  190. package/src/tabs/parts.tsx +50 -26
  191. package/src/testing/dedupe.ts +27 -7
  192. package/src/testing/fake-source.ts +1 -0
  193. package/src/tint.ts +13 -0
  194. package/src/toast-store.ts +20 -14
  195. package/src/tree/parts.tsx +8 -0
  196. package/src/tree/shift.ts +6 -0
  197. package/src/tree/use-tree-shift.tsx +23 -7
  198. package/src/useAsyncAction.ts +8 -0
  199. package/src/useColumnResize.ts +24 -15
  200. package/src/useContentSize.ts +5 -0
  201. package/src/useMeasuredWidth.ts +1 -0
  202. package/src/useNow.ts +6 -11
  203. package/src/workspace/DropPreview.tsx +3 -5
  204. package/src/workspace/SplitView.tsx +109 -0
  205. package/src/workspace/TabStrip.tsx +42 -0
  206. package/src/workspace/Workspace.tsx +5 -0
  207. package/src/workspace/context.ts +1 -0
  208. package/src/workspace/geometry.ts +49 -0
  209. package/src/workspace/model.ts +72 -0
  210. package/src/workspace/preview.ts +15 -15
@@ -9,12 +9,11 @@ import { Kbd } from "../Kbd.js";
9
9
  import { MENU_LABEL } from "../menu/parts.js";
10
10
  import { splitPlacement } from '../placement.js';
11
11
  import { renderPositioner } from '../popup-layer.js';
12
- /* A line in the list instead of rows: nothing found, a search still running. It takes no room while it has nothing to say. */
12
+ /* It takes no room while it has nothing to say. */
13
13
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  const LIST_NOTE = 'text-center text-xs text-text-faint not-empty:px-3 not-empty:py-4';
15
15
  /* The field keeps free text: Base UI's autocomplete, whose suggestions only offer text, rather than its combobox, which holds a value. */
16
16
  export const ComboboxRoot = Autocomplete.Root;
17
- /* The field the suggestions open under, drawn as the library's `Input`. */
18
17
  export function ComboboxInput(t0) {
19
18
  const $ = _c(39);
20
19
  let className;
@@ -172,7 +171,6 @@ export function ComboboxInput(t0) {
172
171
  }
173
172
  return t15;
174
173
  }
175
- /* The portal, the positioner and the menu surface of the suggestions: under the field and at least as wide as it. */
176
174
  export function ComboboxPopup(t0) {
177
175
  const $ = _c(17);
178
176
  let align;
@@ -278,11 +276,7 @@ export function ComboboxList(props) {
278
276
  }
279
277
  return t1;
280
278
  }
281
- /*
282
- * One suggestion. Its children are the label. `onClick` runs for a click and for Enter on the
283
- * highlighted row; Base UI clicks the row for Enter, so a modifier held with Enter is read in the
284
- * field's `onKeyDown` instead.
285
- */
279
+ /* Base UI clicks the row for Enter, so a modifier held with Enter is read in the field's `onKeyDown` instead. */
286
280
  export function ComboboxItem(t0) {
287
281
  const $ = _c(36);
288
282
  let children;
@@ -515,7 +509,7 @@ export function ComboboxEmpty(t0) {
515
509
  }
516
510
  return t2;
517
511
  }
518
- /* A line read out as it changes, such as a search that is still running or how many it found. It stays mounted; only its children come and go. */
512
+ /* Read out as it changes. It stays mounted; only its children come and go. */
519
513
  export function ComboboxStatus(t0) {
520
514
  const $ = _c(8);
521
515
  let className;
@@ -20,11 +20,7 @@ const SIZE = {
20
20
  md: 'w-[576px]',
21
21
  lg: 'w-[760px]'
22
22
  };
23
- /*
24
- * A dialog around a list that is always open. `open` and its handlers are the dialog's; the rest
25
- * is Base UI's autocomplete, so the query, the items and their filter work as in a combobox. The
26
- * first row is highlighted and stays so while the pointer leaves the list, which is what Enter runs.
27
- */
23
+ /* `open` and its handlers are the dialog's; the rest is Base UI's autocomplete. The first row stays highlighted while the pointer leaves the list, which is what Enter runs. */
28
24
  export function CommandPaletteRoot({
29
25
  open,
30
26
  defaultOpen,
@@ -46,8 +42,7 @@ export function CommandPaletteRoot({
46
42
  const [focusHighlight, setFocusHighlight] = useState(false);
47
43
  /*
48
44
  * Base UI moves the highlight to the first row for a typed query only. For a query the app sets,
49
- * the list is empty for one commit that is never painted: Base UI drops the highlight then and
50
- * puts it on the first row of what comes back.
45
+ * the list is empty for one unpainted commit, so Base UI highlights the first row of what comes back.
51
46
  */
52
47
  const [settledValue, setSettledValue] = useState(value);
53
48
  const resetting = value !== undefined && value !== settledValue;
@@ -120,10 +115,7 @@ export function CommandPaletteRoot({
120
115
  })
121
116
  });
122
117
  }
123
- /*
124
- * The dialog's surface. It hangs from a fixed line near the top instead of the middle: the list
125
- * changes height with every key, and a centered popup would walk up and down the screen.
126
- */
118
+ /* It hangs from a fixed line near the top: the list changes height with every key, and a centered popup would walk up and down. */
127
119
  export function CommandPalettePopup(t0) {
128
120
  const $ = _c(15);
129
121
  let className;
@@ -189,7 +181,7 @@ export function CommandPalettePopup(t0) {
189
181
  }
190
182
  return t5;
191
183
  }
192
- /* The field across the top. It keeps the keys while the arrows move the highlight; a control beside it should not take the focus on a click. */
184
+ /* It keeps the keys while the arrows move the highlight; a control beside it should not take the focus on a click. */
193
185
  export function CommandPaletteInput(t0) {
194
186
  const $ = _c(29);
195
187
  let className;
@@ -312,7 +304,7 @@ export function CommandPaletteInput(t0) {
312
304
  }
313
305
  return t11;
314
306
  }
315
- /* The rows. It scrolls on its own, so the field and the footer stay put. */
307
+ /* It scrolls on its own, so the field and the footer stay put. */
316
308
  export function CommandPaletteList(t0) {
317
309
  const $ = _c(21);
318
310
  let className;
@@ -406,7 +398,6 @@ export function CommandPaletteList(t0) {
406
398
  }
407
399
  return t6;
408
400
  }
409
- /* Under the list: the keys that work here, a count, an action that leaves the palette. */
410
401
  export function CommandPaletteFooter(t0) {
411
402
  const $ = _c(14);
412
403
  let className;
@@ -5,10 +5,9 @@ import { Dialog as BaseDialog } from '@base-ui/react/dialog';
5
5
  import { useRender } from '@base-ui/react/use-render';
6
6
  import { withClass } from '../class-name.js';
7
7
  import { useDialogLayer } from '../dialog-layer.js';
8
- /* Whether the dialog is open, for its popup to decide whether it stacks over another one. */
9
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
9
  const DialogOpenContext = createContext(false);
11
- /* A question or a form of a few fields. Every one is as wide as the others, whatever it asks. */
10
+ /* Every question is as wide as the others, whatever it asks. */
12
11
  const SIZE = {
13
12
  sm: 'w-[420px] p-5'
14
13
  };
@@ -83,11 +82,7 @@ export function DialogRoot(t0) {
83
82
  }
84
83
  export const DialogTrigger = BaseDialog.Trigger;
85
84
  export const DialogClose = BaseDialog.Close;
86
- /*
87
- * The portal, the backdrop and the popup of a dialog in one part. A dialog that opens while another
88
- * one is up, such as one from the menu bar over a settings dialog, takes the nested steps and a
89
- * backdrop of its own, or the one underneath stays on top and keeps taking clicks.
90
- */
85
+ /* The portal, the backdrop and the popup of a dialog in one part; `useDialogLayer` says whether it stacks. */
91
86
  export function DialogPopup(t0) {
92
87
  const $ = _c(23);
93
88
  let backdropClassName;
@@ -225,7 +220,7 @@ const DESCRIPTION = {
225
220
  sm: 'text-sm text-text-muted',
226
221
  xs: 'text-xs text-text-muted'
227
222
  };
228
- /* The sentence under the title, which is also what a screen reader reads as the dialog's description. It carries no margin. */
223
+ /* What a screen reader reads as the dialog's description. It carries no margin. */
229
224
  export function DialogDescription(t0) {
230
225
  const $ = _c(10);
231
226
  let className;
@@ -271,7 +266,7 @@ export function DialogDescription(t0) {
271
266
  }
272
267
  return t4;
273
268
  }
274
- /* A further line in the style of the description that is not the dialog's description: a warning, a note under a choice. */
269
+ /* Styled as the description without being the dialog's description. */
275
270
  export function DialogText(t0) {
276
271
  const $ = _c(16);
277
272
  let className;
@@ -340,7 +335,7 @@ export function DialogText(t0) {
340
335
  }
341
336
  return useRender(t5);
342
337
  }
343
- /* The buttons at the foot of a dialog, the main action last and so on the right. */
338
+ /* The main action goes last, and so on the right. */
344
339
  export function DialogFooter(t0) {
345
340
  const $ = _c(14);
346
341
  let className;
@@ -3,9 +3,8 @@ import { useLayoutEffect, useRef, useState } from 'react';
3
3
  // The app-level dialogs open right now. Each one is its own root, so Base UI does not see them as nested.
4
4
  const open = new Set();
5
5
  /*
6
- * Whether a dialog opened while another app-level dialog was already up, such as one opened from the
7
- * menu bar over a settings dialog. Such a dialog takes the nested steps (`dialog-backdrop-nested`,
8
- * `dialog-popup-nested`, a forced backdrop), or the one underneath stays on top and keeps taking clicks.
6
+ * Whether a dialog opened over another app-level dialog. Such a dialog takes the nested steps
7
+ * (`dialog-backdrop-nested`, `dialog-popup-nested`, a forced backdrop), or the one underneath stays on top and keeps taking clicks.
9
8
  */
10
9
  export function useDialogLayer(isOpen) {
11
10
  const $ = _c(5);
@@ -1,12 +1,5 @@
1
- /*
2
- * The pure half of `ErrorBoundary`: when a failed subtree gets another try, and what a person copies
3
- * out of the message. Kept apart so it can be tested without a DOM.
4
- */
5
- /*
6
- * Whether a boundary that holds an error should draw its children again. Only a boundary that
7
- * failed resets. Comparing keys while the children are fine would remount a terminal or an embedded
8
- * page every time the data under it changes.
9
- */
1
+ /* The pure half of `ErrorBoundary`, kept apart so it can be tested without a DOM. */
2
+ /* Only a failed boundary resets: comparing keys while the children are fine would remount a terminal or an embedded page on every change. */
10
3
  export const shouldReset = (failed, previous, next) => {
11
4
  if (!failed) {
12
5
  return false;
@@ -17,18 +10,9 @@ export const shouldReset = (failed, previous, next) => {
17
10
  return previous.some((key, i) => !Object.is(key, next[i]));
18
11
  };
19
12
  /* A thrown value is not always an Error, and the message has to say something either way. */
20
- export const errorMessageOf = error => {
21
- if (error instanceof Error) {
22
- return error.message || error.name;
23
- }
24
- return typeof error === 'string' ? error : String(error);
25
- };
13
+ export const errorMessageOf = error => error instanceof Error ? error.message || error.name : String(error);
26
14
  /* The text behind the copy button: what a bug report needs, the component stack included. */
27
15
  export const errorReport = (label, error, componentStack) => {
28
16
  const stack = error instanceof Error && error.stack ? error.stack : errorMessageOf(error);
29
- const parts = [label, stack];
30
- if (componentStack) {
31
- parts.push(`Component stack:${componentStack}`);
32
- }
33
- return parts.join('\n\n');
17
+ return [label, stack, ...(componentStack ? [`Component stack:${componentStack}`] : [])].join('\n\n');
34
18
  };
package/dist/file-icon.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import { createFileTreeIconResolver } from '@pierre/trees';
2
2
  import { SETI_BY_FILE_EXTENSION, SETI_BY_FILE_NAME, SETI_BY_FILE_NAME_CONTAINS, SETI_DEFAULT_SYMBOL, SETI_SPRITE_SHEET } from './seti-icons.js';
3
3
  const SPRITE_ELEMENT_ID = 'adecore-ui-file-icon-sprite';
4
- /* Hand this to the tree. The glyphs are Seti's (`seti-icons.ts`), which knows far more file types than
5
- the tree's own sets. The tree colors only those, so every symbol carries its hue in its id and the
6
- sprite brings the rules that color it. Everything drawn beside a tree calls `fileIconFor`, so a tab,
7
- a picker row and a tree row never disagree about what a file is. */
4
+ /* Seti knows far more file types than the tree's own sets. The tree colors only those, so every symbol
5
+ carries its hue in its id and the sprite brings the rules that color it. */
8
6
  export const FILE_TREE_ICONS = {
9
7
  set: 'none',
10
8
  spriteSheet: SETI_SPRITE_SHEET,
@@ -22,11 +20,8 @@ const resolveIcon = (...args) => {
22
20
  return resolver.resolveIcon(...args);
23
21
  };
24
22
  /*
25
- * The id of the `<symbol>` the set gives a file, in the sprite `mountFileIconSprite` puts in the
26
- * document. Only the last segment of the path decides, first by exact name (`package.json`), then by
27
- * a part of the name (`Dockerfile.dev`), then by the longest extension that matches (`spec.ts` before
28
- * `ts`). Anything else falls back to the generic file icon, a directory included: the tree marks a
29
- * folder with its chevron, not a glyph.
23
+ * The id of a `<symbol>` in the sprite `mountFileIconSprite` puts in the document. A directory gets the
24
+ * generic file icon: the tree marks a folder with its chevron, not a glyph.
30
25
  */
31
26
  export const fileIconFor = path => resolveIcon('file-tree-icon-file', path).name;
32
27
  /* The tree carries the sprite into its own shadow root, out of reach of anything that draws an
@@ -13,10 +13,7 @@ export function isEmptyLine(path) {
13
13
  export function directoryOfEmptyLine(path) {
14
14
  return path.slice(0, -EMPTY_LINE_NAME.length);
15
15
  }
16
- /*
17
- * The lines to add and the ones to take away: an open directory whose label is not undefined and
18
- * that holds nothing besides its own line gets one, every other line the wrapper added goes.
19
- */
16
+ /* An open, labeled directory that holds nothing besides its own line gets one; every other line the wrapper added goes. */
20
17
  export function emptyLineChanges(rows, labelOf, has, added) {
21
18
  const wanted = new Set();
22
19
  rows.forEach((row, index) => {
@@ -38,10 +35,7 @@ export function emptyLineChanges(rows, labelOf, has, added) {
38
35
  remove: [...added].filter(line => !wanted.has(line) && has(line))
39
36
  };
40
37
  }
41
- /*
42
- * Where an arrow, Home or End goes instead when the row it would land on is a line: past it, or
43
- * nowhere (the focused path itself) where nothing lies past it. Null when no line is in the way.
44
- */
38
+ /* Past the line the key would land on, or the focused path itself where nothing lies past it. Null when no line is in the way. */
45
39
  export function pastEmptyLine(model, key) {
46
40
  const index = model.getFocusedIndex();
47
41
  const focused = model.getFocusedPath();
@@ -6,11 +6,7 @@ const POINTER_EVENTS = ['pointerdown', 'mousedown', 'click', 'dblclick', 'auxcli
6
6
  function onEmptyLine(event) {
7
7
  return event.composedPath().some(node => node instanceof HTMLElement && node.dataset.emptyLine === 'true');
8
8
  }
9
- /*
10
- * Keeps a line under every open directory that is known to be empty, and keeps the line out of
11
- * reach: a pointer on it does nothing and a key passes it. Returns what to call with every key the
12
- * tree hears, after anything that notes where focus was.
13
- */
9
+ /* A pointer on a line does nothing and a key passes it. Call what it returns with every key the tree hears, after anything that notes where focus was. */
14
10
  export function useEmptyLines(model, frame, emptyLabel) {
15
11
  const $ = _c(12);
16
12
  const latest = useRef(emptyLabel);
@@ -5,9 +5,8 @@ import { pathOfRow, visibleRows, visibleRowsAround } from './helpers.js';
5
5
  import { matchHighlight, rangesOfMatches } from './highlight.js';
6
6
  import { FILE_TREE_CSS } from './style.js';
7
7
  /*
8
- * The rows of the mounted window, read from the spacer beta.6 puts above it (its first index times the
9
- * row height), so a scroll frame copies those and not every visible row. The whole list stands in when
10
- * the spacer is missing or the model already moved past what is mounted.
8
+ * Read from the spacer beta.6 puts above the mounted window, so a scroll frame copies those rows and not
9
+ * every visible one. The whole list stands in when the spacer is missing or the model moved past it.
11
10
  */
12
11
  function mountedRows(model, shadow, elements) {
13
12
  const spacer = shadow.querySelector('[data-file-tree-virtualized-sticky-offset]');
@@ -22,10 +21,7 @@ function mountedRows(model, shadow, elements) {
22
21
  const paths = new Set(rows.map(row => row.path));
23
22
  return elements.every(element => element.dataset.itemPath === undefined || paths.has(element.dataset.itemPath)) ? rows : visibleRows(model);
24
23
  }
25
- /*
26
- * Lines the ports up with their rows: every size first, then the spaces they reserve, then every offset,
27
- * then the ports, so a pass forces two layouts at most however many rows are mounted.
28
- */
24
+ /* Every size first, then the spaces, then every offset, then the ports, so a pass forces two layouts at most. */
29
25
  export function positionPorts(ports, order = 'before-status') {
30
26
  const spaced = [...ports].map(port => ({
31
27
  port,
@@ -164,9 +160,9 @@ export function useFileTreeSlots(model, frame, label, {
164
160
  dropIcon(port);
165
161
  };
166
162
  const clearPort = port_0 => {
167
- port_0.rowElement.style.removeProperty('--adecore-tree-control-space');
168
- port_0.rowElement.style.removeProperty('--adecore-tree-decoration-space');
169
- port_0.rowElement.style.removeProperty('--adecore-tree-status-space');
163
+ for (const name of ['--adecore-tree-control-space', '--adecore-tree-decoration-space', '--adecore-tree-status-space']) {
164
+ port_0.rowElement.style.removeProperty(name);
165
+ }
170
166
  delete port_0.rowElement.dataset.ownIcon;
171
167
  dropPort(port_0);
172
168
  };
@@ -4,11 +4,8 @@ import { dateFormatter, wordFormatter } from './locale.js';
4
4
  const WORD_PARTS = new Set(['month', 'weekday', 'dayPeriod', 'era']);
5
5
  const toDate = at => typeof at === 'number' ? new Date(at) : at;
6
6
  /*
7
- * A date written the way the region writes one, with its words in the language the interface is in.
8
- * Both formatters get the same options and the same instant, so every part the region asks for has
9
- * one to lift the word from, whatever order the two put them in: `19 Sep` in English against a Dutch
10
- * region, `19 sep` once the interface is Dutch too, and a clock that reads `08:05` wherever the
11
- * region does not say AM.
7
+ * The region's notation with the language's words: `19 Sep` in English against a Dutch region. Both
8
+ * formatters get the same options and instant, so every word part has one to lift, whatever the order.
12
9
  */
13
10
  export const formatDateTime = (at, options) => {
14
11
  const date = toDate(at);
@@ -69,14 +66,32 @@ const NUMERIC_DATE = {
69
66
  month: 'numeric',
70
67
  day: 'numeric'
71
68
  };
69
+ const zonedSpecs = new WeakMap();
70
+ /* One options object per zone, so the formatter cached on it is built once (`dateFormatter`). */
71
+ const zoned = (options, timeZone) => {
72
+ if (timeZone === undefined) {
73
+ return options;
74
+ }
75
+ let byZone = zonedSpecs.get(options);
76
+ if (byZone === undefined) {
77
+ byZone = new Map();
78
+ zonedSpecs.set(options, byZone);
79
+ }
80
+ let spec = byZone.get(timeZone);
81
+ if (spec === undefined) {
82
+ spec = {
83
+ ...options,
84
+ timeZone
85
+ };
86
+ byZone.set(timeZone, spec);
87
+ }
88
+ return spec;
89
+ };
72
90
  /*
73
- * `08:05`, or `08:05 AM` in a region that counts to twelve. The clock functions below take the `timeZone`
74
- * to write in, such as `UTC` for a moment whose parts are given and mean the same wall clock anywhere.
91
+ * `08:05`, or `08:05 AM` in a region that counts to twelve. A `timeZone` such as `UTC` writes a moment
92
+ * whose parts are given and mean the same wall clock anywhere.
75
93
  */
76
- export const formatClock = (at, timeZone) => formatDateTime(at, timeZone === undefined ? CLOCK : {
77
- ...CLOCK,
78
- timeZone
79
- });
94
+ export const formatClock = (at, timeZone) => formatDateTime(at, zoned(CLOCK, timeZone));
80
95
  /* A time far enough off that its day is part of the answer: `Sat 08:05`. */
81
96
  export const formatWeekdayClock = at => formatDateTime(at, WEEKDAY_CLOCK);
82
97
  /* An hour of the day on its own, for the foot of a chart: `08`, or `8 AM` where that is the clock. */
@@ -84,30 +99,18 @@ export const formatHour = at => formatDateTime(at, HOUR);
84
99
  /* The day of the week on its own, for the side of a grid by weekday: `Sat`, or `Saturday` when `long`. */
85
100
  export const formatWeekday = (at, width = 'short') => formatDateTime(at, width === 'long' ? WEEKDAY_LONG : WEEKDAY);
86
101
  /* `19 Sep`, the short date a row falls back to once "days ago" stops meaning anything. */
87
- export const formatDay = (at, timeZone) => formatDateTime(at, timeZone === undefined ? DAY : {
88
- ...DAY,
89
- timeZone
90
- });
102
+ export const formatDay = (at, timeZone) => formatDateTime(at, zoned(DAY, timeZone));
91
103
  /* The same date with the year, for anything older than the one we are in. */
92
- export const formatDayWithYear = (at, timeZone) => formatDateTime(at, timeZone === undefined ? DAY_WITH_YEAR : {
93
- ...DAY_WITH_YEAR,
94
- timeZone
95
- });
104
+ export const formatDayWithYear = (at, timeZone) => formatDateTime(at, zoned(DAY_WITH_YEAR, timeZone));
96
105
  /* `Sat 19 Sep`. */
97
106
  export const formatWeekdayDay = at => formatDateTime(at, WEEKDAY_DAY);
98
107
  /* `19 Sep, 08:05`: a moment that is not today, to the minute. */
99
- export const formatDayClock = (at, timeZone) => formatDateTime(at, timeZone === undefined ? DAY_CLOCK : {
100
- ...DAY_CLOCK,
101
- timeZone
102
- });
103
- /* `19 Sep 2026, 08:05`: the same moment with the year, for one that is not in the year we are in. */
104
- export const formatDayClockWithYear = (at, timeZone) => formatDateTime(at, timeZone === undefined ? DAY_CLOCK_WITH_YEAR : {
105
- ...DAY_CLOCK_WITH_YEAR,
106
- timeZone
107
- });
108
+ export const formatDayClock = (at, timeZone) => formatDateTime(at, zoned(DAY_CLOCK, timeZone));
109
+ /* `19 Sep 2026, 08:05`: the same moment with the year. */
110
+ export const formatDayClockWithYear = (at, timeZone) => formatDateTime(at, zoned(DAY_CLOCK_WITH_YEAR, timeZone));
108
111
  /* All numbers, as a form would print it: `19-9-2026` here, `9/19/2026` in the United States. */
109
112
  export const formatNumericDate = at => formatDateTime(at, NUMERIC_DATE);
110
- /* Whether two instants fall on the same day of the same year, in the zone of whoever is reading. */
113
+ /* In the zone of whoever is reading. */
111
114
  export const isSameDay = (a, b) => {
112
115
  const left = toDate(a);
113
116
  const right = toDate(b);
@@ -4,11 +4,7 @@ const SECOND = 1000;
4
4
  const MINUTE = 60 * SECOND;
5
5
  const HOUR = 60 * MINUTE;
6
6
  const DAY = 24 * HOUR;
7
- /*
8
- * How long something took, at the coarsest unit that still says something. The units are English
9
- * abbreviations, the way the rest of the interface is written; only the number bends to the region,
10
- * which is what `1,5 h` against `1.5 h` is about.
11
- */
7
+ /* At the coarsest unit that still says something. The units stay English; only the number bends to the region (`1,5 h`). */
12
8
  export const formatDuration = ms => {
13
9
  const seconds = Math.max(0, Math.round(ms / SECOND));
14
10
  if (seconds < 60) {
@@ -54,11 +50,7 @@ export const formatCountdown = ms => {
54
50
  }
55
51
  return `${formatNumber(Math.max(1, Math.round(left / MINUTE)))}m`;
56
52
  };
57
- /*
58
- * How long something has been running, to two units and no space to spare: `12s`, `2m 5s`, `3h 20m`.
59
- * A call that took a fraction of a second still took some, so anything above zero reads as at least
60
- * `1s`; only a clock that runs ahead of the start comes out as `0s`.
61
- */
53
+ /* `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`. */
62
54
  export const formatElapsedShort = ms => {
63
55
  const seconds = ms <= 0 ? 0 : Math.max(1, Math.round(ms / SECOND));
64
56
  if (seconds < 60) {
@@ -11,19 +11,16 @@ export const setFormatSource = next => {
11
11
  source = next;
12
12
  return previous;
13
13
  };
14
- /* Where a language comes from when nothing else says: the country most of its speakers are in. */
14
+ /* The country most of a language's speakers are in. */
15
15
  const LANGUAGE_REGIONS = {
16
16
  en: 'en-US',
17
17
  nl: 'nl-NL'
18
18
  };
19
- /* The one locale to fall back on, so a format never depends on which machine ran the test. */
19
+ /* So a format never depends on which machine ran the test. */
20
20
  export const FALLBACK_LOCALE = 'en-US';
21
- /* A month and a weekday are words, so they come from the language the interface is in, while the
22
- order, the separators and the clock come from the region. English words in a Dutch notation is
23
- what a Dutch Mac running an English app should read like, and both halves are a person's to set. */
21
+ /* A month and a weekday are words, so they come from the language; the order, the separators and the clock come from the region. */
24
22
  export const wordLocale = () => source.language();
25
- /* A tag `Intl` will take, or nothing. macOS hands out locales with overrides attached, and a
26
- formatter built on one it does not know throws where a number was meant to go. */
23
+ /* macOS hands out locales with overrides attached, and a formatter built on one `Intl` does not know throws. */
27
24
  const usable = tag => {
28
25
  if (tag === undefined || tag === '') {
29
26
  return null;
@@ -35,10 +32,8 @@ const usable = tag => {
35
32
  }
36
33
  };
37
34
  /*
38
- * What the operating system was set to read numbers and dates in. In a desktop shell Chromium's own
39
- * locale is the language of the app bundle, so an English bundle on a Dutch Mac would still hand back
40
- * `en-US` and write `08:00 AM`; the shell reads the system's region instead and passes it in
41
- * (`systemLocale`). A browser has no such thing, so there the language is the best there is.
35
+ * In a desktop shell Chromium's locale is the language of the app bundle, so an English bundle on a
36
+ * Dutch Mac would write `08:00 AM`; the shell passes the system's region in instead (`systemLocale`).
42
37
  */
43
38
  export const systemLocale = () => {
44
39
  const fromShell = usable(source.systemLocale?.());
@@ -50,11 +45,7 @@ export const systemLocale = () => {
50
45
  }
51
46
  return usable(navigator.languages?.[0] ?? navigator.language) ?? FALLBACK_LOCALE;
52
47
  };
53
- /*
54
- * The region a language brings with it. The system's own when it already speaks that language, so
55
- * Dutch in Belgium keeps writing dates the Belgian way, and the country the language is most spoken
56
- * in when it does not.
57
- */
48
+ /* The system's own region when it already speaks the language, so Dutch in Belgium keeps writing dates the Belgian way. */
58
49
  const regionOfLanguage = () => {
59
50
  const language = source.language();
60
51
  const system = systemLocale();
@@ -66,12 +57,10 @@ const resolve = region => {
66
57
  }
67
58
  return region === FORMAT_SYSTEM ? systemLocale() : region;
68
59
  };
69
- /* The locale every formatter in this folder is built on. Read outside React as well, so it is a
70
- plain function over the source rather than a hook. */
60
+ /* Read outside React as well, so it is a plain function rather than a hook. */
71
61
  export const formatLocale = () => resolve(source.region());
72
62
  const subscribe = onChange => source.subscribe(onChange);
73
- /* What a component calls to draw again once the region changes. The value is the locale, which a
74
- caller may use or ignore; subscribing is the point. */
63
+ /* Subscribing is the point: a component draws again once the region changes. */
75
64
  export const useFormatLocale = () => {
76
65
  return useSyncExternalStore(subscribe, formatLocale, formatLocale);
77
66
  };
@@ -95,37 +84,28 @@ const relatives = new WeakMap();
95
84
  const names = new WeakMap();
96
85
  export const numberFormatter = options => cacheFor(numbers, options, formatLocale(), (locale, spec) => new Intl.NumberFormat(locale, spec));
97
86
  export const dateFormatter = options => cacheFor(dates, options, formatLocale(), (locale, spec) => new Intl.DateTimeFormat(locale, spec));
98
- /* The same date, written in the language the words come from. Never shown whole: only its month,
99
- weekday, era and day period are lifted out of it. */
87
+ /* Never shown whole: only its month, weekday, era and day period are lifted out of it. */
100
88
  export const wordFormatter = options => cacheFor(words, options, wordLocale(), (locale, spec) => new Intl.DateTimeFormat(locale, spec));
101
89
  /* `3 min. geleden` is words as much as a number, so the language writes it, the way it writes a month. */
102
90
  export const relativeFormatter = options => cacheFor(relatives, options, wordLocale(), (locale, spec) => new Intl.RelativeTimeFormat(locale, spec));
103
91
  /* Proper names keep the runtime locale; a shared options object reuses its collator. */
104
92
  export const nameCollator = options => cacheFor(names, options, undefined, (locale, spec) => new Intl.Collator(locale, spec));
105
93
  const collators = new Map();
106
- /*
107
- * How two labels a person reads are put in order. Labels are words, so the language writes them and
108
- * the language sorts them: Dutch puts Tekening after Terminal where English puts Drawing before it.
109
- * Keyed on the locale rather than on an options object, since there are no options to key on.
110
- */
94
+ /* Labels are words, so the language sorts them: Dutch puts Tekening after Terminal where English puts Drawing before it. */
111
95
  export const labelCollator = () => {
112
96
  const locale = wordLocale();
113
- const held = collators.get(locale);
114
- if (held !== undefined) {
115
- return held;
97
+ let collator = collators.get(locale);
98
+ if (collator === undefined) {
99
+ collator = new Intl.Collator(locale, {
100
+ sensitivity: 'base',
101
+ numeric: true
102
+ });
103
+ collators.set(locale, collator);
116
104
  }
117
- const collator = new Intl.Collator(locale, {
118
- sensitivity: 'base',
119
- numeric: true
120
- });
121
- collators.set(locale, collator);
122
105
  return collator;
123
106
  };
124
107
  let nameCollators = null;
125
- /*
126
- * The order of names that are no words, such as files: the runtime's own locale, numbers by value and
127
- * case only to break a tie. It is what `localeCompare` did, which built a collator on every call.
128
- */
108
+ /* For names that are no words, such as files: what `localeCompare` did, without a collator built on every call. */
129
109
  export const nameOrder = (left, right) => {
130
110
  nameCollators ??= {
131
111
  loose: new Intl.Collator(undefined, {
@@ -7,7 +7,6 @@ const ONE_DECIMAL = {
7
7
  };
8
8
  /* A count as a region groups one: `1,234,567` there, `1.234.567` here. */
9
9
  export const formatNumber = value => numberFormatter(WHOLE).format(Math.round(value));
10
- /* The same, to one decimal, where the fraction is the part that means something. */
11
10
  export const formatDecimal = value => numberFormatter(ONE_DECIMAL).format(value);
12
11
  /* One options object per key, so the formatter cached on it is built once (`numberFormatter`). */
13
12
  const specCache = build => {
@@ -36,9 +35,8 @@ export const formatRounded = (value, decimals) => numberFormatter(roundedSpec(de
36
35
  const MAX_FRACTION_DIGITS = 100;
37
36
  const NUMERAL = /^[+-]?\d+(?:\.(\d+))?$/;
38
37
  /*
39
- * A numeral as a database writes it (`12900`, `-0.50`, `9007199254740993`), in the region's notation
40
- * with every digit kept: a decimal or a big integer arrives as text, and a float would round it.
41
- * Text that is no plain numeral, such as `NaN` or `1e999`, comes back as it is.
38
+ * A numeral as a database writes it (`12900`, `-0.50`, `9007199254740993`) with every digit kept: a
39
+ * decimal or a big integer arrives as text, and a float would round it. Anything else comes back as it is.
42
40
  */
43
41
  export const formatNumeral = numeral => {
44
42
  const text = typeof numeral === 'number' ? String(numeral) : numeral.trim();
@@ -52,20 +50,11 @@ export const formatNumeral = numeral => {
52
50
  };
53
51
  /* Percent as Activity Monitor writes it: a decimal under ten, where the difference still shows. */
54
52
  export const formatPercent = value => `${value < 10 ? formatDecimal(value) : formatNumber(value)}%`;
55
- /*
56
- * The option objects a money formatter is cached on, one per currency and precision. The cache is
57
- * keyed on the object itself, so building a fresh one per call would build a fresh formatter too.
58
- */
59
- const moneySpecs = new Map();
60
- const moneySpec = (currency, maximumFractionDigits) => {
61
- const key = `${currency}|${maximumFractionDigits ?? ''}`;
62
- const known = moneySpecs.get(key);
63
- if (known !== undefined) {
64
- return known;
65
- }
66
- // Narrow, or a Dutch region writes a dollar amount as `US$ 5.480,96`, which is a currency lesson
67
- // nobody asked for in a table of prices.
68
- const spec = {
53
+ /* Narrow, or a Dutch region writes a dollar amount as `US$ 5.480,96`. */
54
+ const moneySpec = specCache(key => {
55
+ const [currency, digits] = key.split('|');
56
+ const maximumFractionDigits = Number(digits);
57
+ return {
69
58
  style: 'currency',
70
59
  currency,
71
60
  currencyDisplay: 'narrowSymbol',
@@ -73,11 +62,9 @@ const moneySpec = (currency, maximumFractionDigits) => {
73
62
  maximumFractionDigits
74
63
  } : {})
75
64
  };
76
- moneySpecs.set(key, spec);
77
- return spec;
78
- };
65
+ });
79
66
  /* An amount in the currency it is already in, as a bank statement would print it. */
80
- export const formatMoney = (value, currency, maximumFractionDigits) => numberFormatter(moneySpec(currency, maximumFractionDigits)).format(value);
67
+ export const formatMoney = (value, currency, maximumFractionDigits) => numberFormatter(moneySpec(`${currency}|${maximumFractionDigits ?? ''}`)).format(value);
81
68
  const SIGNIFICANT_MONEY = {
82
69
  style: 'currency',
83
70
  currency: 'USD',
@@ -1,12 +1,9 @@
1
- /*
2
- * The regions a person can read this app in, beside the one its language comes with and the one the
3
- * operating system was set to. The list is short on purpose. It is here for a machine whose region
4
- * reads nothing like the person in front of it, not as a country picker.
5
- */
6
1
  export const FORMAT_LANGUAGE = 'language';
7
2
  export const FORMAT_SYSTEM = 'system';
8
- /* Only the tags. A country names itself in the language the interface is in (`Intl.DisplayNames`),
9
- so no translation file ever carries a list of countries. */
3
+ /*
4
+ * Short on purpose: for a machine whose region reads nothing like the person in front of it, not a
5
+ * country picker. A country names itself through `Intl.DisplayNames`, so no translation file lists them.
6
+ */
10
7
  export const FORMAT_REGIONS = ['nl-NL', 'en-US', 'en-GB', 'de-DE', 'fr-FR', 'es-ES', 'sv-SE', 'ja-JP'];
11
8
  export const FORMAT_REGION_CHOICES = [FORMAT_LANGUAGE, FORMAT_SYSTEM, ...FORMAT_REGIONS];
12
9
  /* The country of a region tag, named in `language`: `nl-NL` in English is "Netherlands". */