@adecore/ui 0.14.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 (318) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/AccentSwatches.d.ts +15 -0
  4. package/dist/AccentSwatches.js +21 -0
  5. package/dist/Banner.d.ts +12 -0
  6. package/dist/Banner.js +18 -0
  7. package/dist/Button.d.ts +9 -0
  8. package/dist/Button.js +28 -0
  9. package/dist/ButtonGroup.d.ts +3 -0
  10. package/dist/ButtonGroup.js +11 -0
  11. package/dist/Checkbox.d.ts +11 -0
  12. package/dist/Checkbox.js +9 -0
  13. package/dist/ChoiceCards.d.ts +23 -0
  14. package/dist/ChoiceCards.js +21 -0
  15. package/dist/CloseButton.d.ts +15 -0
  16. package/dist/CloseButton.js +11 -0
  17. package/dist/ColorSwatch.d.ts +14 -0
  18. package/dist/ColorSwatch.js +28 -0
  19. package/dist/ColumnResizeHandle.d.ts +9 -0
  20. package/dist/ColumnResizeHandle.js +12 -0
  21. package/dist/DisabledReason.d.ts +6 -0
  22. package/dist/DisabledReason.js +8 -0
  23. package/dist/DockShell.d.ts +9 -0
  24. package/dist/DockShell.js +49 -0
  25. package/dist/EmptyState.d.ts +16 -0
  26. package/dist/EmptyState.js +11 -0
  27. package/dist/ErrorBoundary.d.ts +24 -0
  28. package/dist/ErrorBoundary.js +47 -0
  29. package/dist/Field.d.ts +17 -0
  30. package/dist/Field.js +48 -0
  31. package/dist/FileIcon.d.ts +8 -0
  32. package/dist/FileIcon.js +15 -0
  33. package/dist/Icon.d.ts +9 -0
  34. package/dist/Icon.js +12 -0
  35. package/dist/IconButton.d.ts +23 -0
  36. package/dist/IconButton.js +25 -0
  37. package/dist/IconPicker.d.ts +17 -0
  38. package/dist/IconPicker.js +93 -0
  39. package/dist/Input.d.ts +14 -0
  40. package/dist/Input.js +25 -0
  41. package/dist/Kbd.d.ts +23 -0
  42. package/dist/Kbd.js +20 -0
  43. package/dist/ListRow.d.ts +10 -0
  44. package/dist/ListRow.js +20 -0
  45. package/dist/Meter.d.ts +12 -0
  46. package/dist/Meter.js +7 -0
  47. package/dist/PanelEmpty.d.ts +16 -0
  48. package/dist/PanelEmpty.js +7 -0
  49. package/dist/PanelHeader.d.ts +6 -0
  50. package/dist/PanelHeader.js +20 -0
  51. package/dist/Pill.d.ts +27 -0
  52. package/dist/Pill.js +25 -0
  53. package/dist/ProjectSwitcher.d.ts +30 -0
  54. package/dist/ProjectSwitcher.js +18 -0
  55. package/dist/PromptDialog.d.ts +40 -0
  56. package/dist/PromptDialog.js +47 -0
  57. package/dist/SectionLabel.d.ts +3 -0
  58. package/dist/SectionLabel.js +16 -0
  59. package/dist/SegmentBar.d.ts +18 -0
  60. package/dist/SegmentBar.js +44 -0
  61. package/dist/Segmented.d.ts +17 -0
  62. package/dist/Segmented.js +7 -0
  63. package/dist/Select.d.ts +27 -0
  64. package/dist/Select.js +34 -0
  65. package/dist/Separator.d.ts +7 -0
  66. package/dist/Separator.js +8 -0
  67. package/dist/ShortcutHints.d.ts +1 -0
  68. package/dist/ShortcutHints.js +94 -0
  69. package/dist/Skeleton.d.ts +6 -0
  70. package/dist/Skeleton.js +6 -0
  71. package/dist/SlidingColumn.d.ts +19 -0
  72. package/dist/SlidingColumn.js +41 -0
  73. package/dist/Spinner.d.ts +6 -0
  74. package/dist/Spinner.js +7 -0
  75. package/dist/Stepper.d.ts +14 -0
  76. package/dist/Stepper.js +18 -0
  77. package/dist/Surface.d.ts +3 -0
  78. package/dist/Surface.js +16 -0
  79. package/dist/Switch.d.ts +10 -0
  80. package/dist/Switch.js +7 -0
  81. package/dist/TextMenu.d.ts +5 -0
  82. package/dist/TextMenu.js +31 -0
  83. package/dist/Tile.d.ts +11 -0
  84. package/dist/Tile.js +10 -0
  85. package/dist/Toasts.d.ts +9 -0
  86. package/dist/Toasts.js +58 -0
  87. package/dist/Tooltip.d.ts +16 -0
  88. package/dist/Tooltip.js +21 -0
  89. package/dist/UIProvider.d.ts +9 -0
  90. package/dist/UIProvider.js +26 -0
  91. package/dist/Waveform.d.ts +16 -0
  92. package/dist/Waveform.js +123 -0
  93. package/dist/Wipe.d.ts +12 -0
  94. package/dist/Wipe.js +47 -0
  95. package/dist/ZoomControls.d.ts +30 -0
  96. package/dist/ZoomControls.js +16 -0
  97. package/dist/class-name.d.ts +2 -0
  98. package/dist/class-name.js +3 -0
  99. package/dist/clipboard.d.ts +2 -0
  100. package/dist/clipboard.js +16 -0
  101. package/dist/context-menu/index.parts.d.ts +1 -0
  102. package/dist/context-menu/index.parts.js +2 -0
  103. package/dist/dialog/index.parts.d.ts +1 -0
  104. package/dist/dialog/index.parts.js +1 -0
  105. package/dist/dialog/parts.d.ts +36 -0
  106. package/dist/dialog/parts.js +55 -0
  107. package/dist/dialog-layer.d.ts +1 -0
  108. package/dist/dialog-layer.js +27 -0
  109. package/dist/error-boundary.d.ts +4 -0
  110. package/dist/error-boundary.js +34 -0
  111. package/dist/error-message.d.ts +1 -0
  112. package/dist/error-message.js +4 -0
  113. package/dist/field-context.d.ts +7 -0
  114. package/dist/field-context.js +4 -0
  115. package/dist/file-icon.d.ts +4 -0
  116. package/dist/file-icon.js +46 -0
  117. package/dist/floating.d.ts +5 -0
  118. package/dist/floating.js +6 -0
  119. package/dist/format/datetime.d.ts +11 -0
  120. package/dist/format/datetime.js +50 -0
  121. package/dist/format/duration.d.ts +5 -0
  122. package/dist/format/duration.js +76 -0
  123. package/dist/format/index.d.ts +6 -0
  124. package/dist/format/index.js +7 -0
  125. package/dist/format/locale.d.ts +17 -0
  126. package/dist/format/locale.js +105 -0
  127. package/dist/format/number.d.ts +8 -0
  128. package/dist/format/number.js +72 -0
  129. package/dist/format/regions.d.ts +6 -0
  130. package/dist/format/regions.js +15 -0
  131. package/dist/format/time-zone.d.ts +1 -0
  132. package/dist/format/time-zone.js +2 -0
  133. package/dist/icon-button-size.d.ts +3 -0
  134. package/dist/icon-button-size.js +3 -0
  135. package/dist/icon-picker.d.ts +23 -0
  136. package/dist/icon-picker.js +80 -0
  137. package/dist/index.d.ts +74 -0
  138. package/dist/index.js +74 -0
  139. package/dist/lazy.d.ts +12 -0
  140. package/dist/lazy.js +104 -0
  141. package/dist/locales/en.json +59 -0
  142. package/dist/locales/nl.json +59 -0
  143. package/dist/locales.d.ts +4 -0
  144. package/dist/locales.js +17 -0
  145. package/dist/menu/index.parts.d.ts +1 -0
  146. package/dist/menu/index.parts.js +1 -0
  147. package/dist/menu/parts.d.ts +67 -0
  148. package/dist/menu/parts.js +113 -0
  149. package/dist/merge-refs.d.ts +2 -0
  150. package/dist/merge-refs.js +11 -0
  151. package/dist/modality.d.ts +1 -0
  152. package/dist/modality.js +29 -0
  153. package/dist/platform.d.ts +2 -0
  154. package/dist/platform.js +9 -0
  155. package/dist/popover/index.parts.d.ts +1 -0
  156. package/dist/popover/index.parts.js +1 -0
  157. package/dist/popover/parts.d.ts +26 -0
  158. package/dist/popover/parts.js +28 -0
  159. package/dist/prefetch.d.ts +21 -0
  160. package/dist/prefetch.js +99 -0
  161. package/dist/preview-card/index.parts.d.ts +1 -0
  162. package/dist/preview-card/index.parts.js +1 -0
  163. package/dist/project-switcher-row.d.ts +6 -0
  164. package/dist/project-switcher-row.js +16 -0
  165. package/dist/selection.d.ts +2 -0
  166. package/dist/selection.js +22 -0
  167. package/dist/seti-icons.d.ts +5 -0
  168. package/dist/seti-icons.js +445 -0
  169. package/dist/settings/ConfirmDialog.d.ts +9 -0
  170. package/dist/settings/ConfirmDialog.js +9 -0
  171. package/dist/settings/DetailHeader.d.ts +10 -0
  172. package/dist/settings/DetailHeader.js +6 -0
  173. package/dist/settings/MasterDetail.d.ts +24 -0
  174. package/dist/settings/MasterDetail.js +16 -0
  175. package/dist/settings/SettingsDialog.d.ts +46 -0
  176. package/dist/settings/SettingsDialog.js +96 -0
  177. package/dist/settings/SettingsRow.d.ts +22 -0
  178. package/dist/settings/SettingsRow.js +30 -0
  179. package/dist/settings/SettingsSection.d.ts +14 -0
  180. package/dist/settings/SettingsSection.js +8 -0
  181. package/dist/settings/index.d.ts +7 -0
  182. package/dist/settings/index.js +8 -0
  183. package/dist/settings/target.d.ts +7 -0
  184. package/dist/settings/target.js +3 -0
  185. package/dist/shortcut-hints.d.ts +38 -0
  186. package/dist/shortcut-hints.js +81 -0
  187. package/dist/shortcut.d.ts +32 -0
  188. package/dist/shortcut.js +137 -0
  189. package/dist/testing/dedupe.d.ts +1 -0
  190. package/dist/testing/dedupe.js +73 -0
  191. package/dist/testing/fake-source.d.ts +8 -0
  192. package/dist/testing/fake-source.js +15 -0
  193. package/dist/testing/index.d.ts +1 -0
  194. package/dist/testing/index.js +2 -0
  195. package/dist/theme.css +562 -0
  196. package/dist/toast-store.d.ts +38 -0
  197. package/dist/toast-store.js +61 -0
  198. package/dist/useAsyncAction.d.ts +8 -0
  199. package/dist/useAsyncAction.js +23 -0
  200. package/dist/useColumnResize.d.ts +14 -0
  201. package/dist/useColumnResize.js +49 -0
  202. package/dist/useContentSize.d.ts +5 -0
  203. package/dist/useContentSize.js +21 -0
  204. package/dist/useMeasuredWidth.d.ts +1 -0
  205. package/dist/useMeasuredWidth.js +6 -0
  206. package/dist/useNow.d.ts +3 -0
  207. package/dist/useNow.js +41 -0
  208. package/dist/wipe-split.d.ts +7 -0
  209. package/dist/wipe-split.js +20 -0
  210. package/dist/zoom.d.ts +3 -0
  211. package/dist/zoom.js +6 -0
  212. package/package.json +92 -0
  213. package/src/AccentSwatches.tsx +73 -0
  214. package/src/Banner.tsx +43 -0
  215. package/src/Button.tsx +55 -0
  216. package/src/ButtonGroup.tsx +14 -0
  217. package/src/Checkbox.tsx +44 -0
  218. package/src/ChoiceCards.tsx +108 -0
  219. package/src/CloseButton.tsx +21 -0
  220. package/src/ColorSwatch.tsx +41 -0
  221. package/src/ColumnResizeHandle.tsx +23 -0
  222. package/src/DisabledReason.tsx +23 -0
  223. package/src/DockShell.tsx +76 -0
  224. package/src/EmptyState.tsx +48 -0
  225. package/src/ErrorBoundary.tsx +116 -0
  226. package/src/Field.tsx +116 -0
  227. package/src/FileIcon.tsx +27 -0
  228. package/src/Icon.tsx +22 -0
  229. package/src/IconButton.tsx +84 -0
  230. package/src/IconPicker.tsx +257 -0
  231. package/src/Input.tsx +78 -0
  232. package/src/Kbd.tsx +67 -0
  233. package/src/ListRow.tsx +26 -0
  234. package/src/Meter.tsx +41 -0
  235. package/src/PanelEmpty.tsx +37 -0
  236. package/src/PanelHeader.tsx +31 -0
  237. package/src/Pill.tsx +61 -0
  238. package/src/ProjectSwitcher.tsx +127 -0
  239. package/src/PromptDialog.tsx +168 -0
  240. package/src/SectionLabel.tsx +20 -0
  241. package/src/SegmentBar.tsx +128 -0
  242. package/src/Segmented.tsx +51 -0
  243. package/src/Select.tsx +135 -0
  244. package/src/Separator.tsx +15 -0
  245. package/src/ShortcutHints.tsx +118 -0
  246. package/src/Skeleton.tsx +13 -0
  247. package/src/SlidingColumn.tsx +91 -0
  248. package/src/Spinner.tsx +32 -0
  249. package/src/Stepper.tsx +67 -0
  250. package/src/Surface.tsx +20 -0
  251. package/src/Switch.tsx +32 -0
  252. package/src/TextMenu.tsx +59 -0
  253. package/src/Tile.tsx +49 -0
  254. package/src/Toasts.tsx +151 -0
  255. package/src/Tooltip.tsx +70 -0
  256. package/src/UIProvider.tsx +41 -0
  257. package/src/Waveform.tsx +199 -0
  258. package/src/Wipe.tsx +99 -0
  259. package/src/ZoomControls.tsx +99 -0
  260. package/src/class-name.ts +8 -0
  261. package/src/clipboard.ts +17 -0
  262. package/src/context-menu/index.parts.ts +21 -0
  263. package/src/dialog/index.parts.ts +10 -0
  264. package/src/dialog/parts.tsx +112 -0
  265. package/src/dialog-layer.ts +31 -0
  266. package/src/error-boundary.ts +40 -0
  267. package/src/error-message.ts +5 -0
  268. package/src/field-context.ts +12 -0
  269. package/src/file-icon.ts +52 -0
  270. package/src/floating.ts +9 -0
  271. package/src/format/datetime.ts +63 -0
  272. package/src/format/duration.ts +83 -0
  273. package/src/format/index.ts +20 -0
  274. package/src/format/locale.ts +153 -0
  275. package/src/format/number.ts +87 -0
  276. package/src/format/regions.ts +20 -0
  277. package/src/format/time-zone.ts +2 -0
  278. package/src/icon-button-size.ts +6 -0
  279. package/src/icon-picker.ts +112 -0
  280. package/src/index.ts +103 -0
  281. package/src/lazy.tsx +124 -0
  282. package/src/locales/en.json +59 -0
  283. package/src/locales/nl.json +59 -0
  284. package/src/locales.ts +21 -0
  285. package/src/menu/index.parts.ts +20 -0
  286. package/src/menu/parts.tsx +296 -0
  287. package/src/merge-refs.ts +14 -0
  288. package/src/modality.ts +37 -0
  289. package/src/platform.ts +10 -0
  290. package/src/popover/index.parts.ts +8 -0
  291. package/src/popover/parts.tsx +119 -0
  292. package/src/prefetch.ts +113 -0
  293. package/src/preview-card/index.parts.ts +1 -0
  294. package/src/project-switcher-row.tsx +51 -0
  295. package/src/selection.ts +24 -0
  296. package/src/seti-icons.ts +451 -0
  297. package/src/settings/ConfirmDialog.tsx +30 -0
  298. package/src/settings/DetailHeader.tsx +25 -0
  299. package/src/settings/MasterDetail.tsx +67 -0
  300. package/src/settings/SettingsDialog.tsx +342 -0
  301. package/src/settings/SettingsRow.tsx +89 -0
  302. package/src/settings/SettingsSection.tsx +46 -0
  303. package/src/settings/index.ts +16 -0
  304. package/src/settings/target.ts +12 -0
  305. package/src/shortcut-hints.ts +117 -0
  306. package/src/shortcut.ts +170 -0
  307. package/src/testing/dedupe.ts +93 -0
  308. package/src/testing/fake-source.ts +21 -0
  309. package/src/testing/index.ts +3 -0
  310. package/src/theme.css +562 -0
  311. package/src/toast-store.ts +120 -0
  312. package/src/useAsyncAction.ts +32 -0
  313. package/src/useColumnResize.ts +75 -0
  314. package/src/useContentSize.ts +27 -0
  315. package/src/useMeasuredWidth.ts +7 -0
  316. package/src/useNow.ts +49 -0
  317. package/src/wipe-split.ts +24 -0
  318. package/src/zoom.ts +9 -0
@@ -0,0 +1,17 @@
1
+ /*
2
+ * The clipboard, as the app uses it. A browser hands text over only to a document that is focused
3
+ * and allowed, and there is nothing a person can do about a refusal, so both sides stay quiet.
4
+ */
5
+
6
+ export const copyText = (text: string): void => {
7
+ void navigator.clipboard?.writeText(text).catch(() => undefined);
8
+ };
9
+
10
+ /* What is on the clipboard, or nothing at all when the browser will not say. */
11
+ export const readClipboardText = async (): Promise<string> => {
12
+ try {
13
+ return (await navigator.clipboard?.readText()) ?? '';
14
+ } catch {
15
+ return '';
16
+ }
17
+ };
@@ -0,0 +1,21 @@
1
+ /* A context menu is a menu that opens at the pointer; every part inside it is the menu's own. */
2
+ export {
3
+ MenuCheck as Check,
4
+ MenuCheckboxItem as CheckboxItem,
5
+ MenuGroup as Group,
6
+ MenuGroupLabel as GroupLabel,
7
+ MenuHint as Hint,
8
+ MenuItem as Item,
9
+ MenuLabel as Label,
10
+ MenuPopup as Popup,
11
+ MenuRadioGroup as RadioGroup,
12
+ MenuRadioItem as RadioItem,
13
+ ContextMenuRoot as Root,
14
+ MenuRow as Row,
15
+ MenuRowAction as RowAction,
16
+ MenuRowSubmenuTrigger as RowSubmenuTrigger,
17
+ MenuSeparator as Separator,
18
+ MenuSubmenuRoot as SubmenuRoot,
19
+ MenuSubmenuTrigger as SubmenuTrigger,
20
+ ContextMenuTrigger as Trigger
21
+ } from '../menu/parts.tsx';
@@ -0,0 +1,10 @@
1
+ export {
2
+ DialogClose as Close,
3
+ DialogDescription as Description,
4
+ DialogFooter as Footer,
5
+ DialogPopup as Popup,
6
+ DialogRoot as Root,
7
+ DialogText as Text,
8
+ DialogTitle as Title,
9
+ DialogTrigger as Trigger
10
+ } from './parts.tsx';
@@ -0,0 +1,112 @@
1
+ import { createContext, useContext, useState, type ComponentProps } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
4
+ import { useRender } from '@base-ui-components/react/use-render';
5
+ import { withClass } from '../class-name.ts';
6
+ import { useDialogLayer } from '../dialog-layer.ts';
7
+
8
+ /* Whether the dialog is open, for its popup to decide whether it stacks over another one. */
9
+ const DialogOpenContext = createContext(false);
10
+
11
+ /* A question or a form of a few fields. Every one is as wide as the others, whatever it asks. */
12
+ const SIZE = {
13
+ sm: 'w-[420px] p-5'
14
+ } as const;
15
+
16
+ export function DialogRoot({ open, defaultOpen, onOpenChange, ...props }: ComponentProps<typeof BaseDialog.Root>) {
17
+ // Followed rather than taken over, so a dialog that keeps its own state still does.
18
+ const [seen, setSeen] = useState(defaultOpen ?? false);
19
+ return (
20
+ <DialogOpenContext value={open ?? seen}>
21
+ <BaseDialog.Root
22
+ open={open}
23
+ defaultOpen={defaultOpen}
24
+ onOpenChange={(next, details) => {
25
+ onOpenChange?.(next, details);
26
+ if (!details.isCanceled) {
27
+ setSeen(next);
28
+ }
29
+ }}
30
+ {...props}
31
+ />
32
+ </DialogOpenContext>
33
+ );
34
+ }
35
+
36
+ export const DialogTrigger = BaseDialog.Trigger;
37
+
38
+ export const DialogClose = BaseDialog.Close;
39
+
40
+ export type DialogPopupProps = ComponentProps<typeof BaseDialog.Popup> & {
41
+ /* `sm` is the dialog of a question or a few fields. Without a size the popup is as wide as its `className` says. */
42
+ size?: keyof typeof SIZE;
43
+ /* Always over another dialog. A dialog that opens while another one is up stacks by itself. */
44
+ nested?: boolean;
45
+ /* The dim behind the dialog. Off for a dialog that leaves what is under it as it is. */
46
+ backdrop?: boolean;
47
+ /* Extra on the backdrop, such as the heavier dim a picture needs. */
48
+ backdropClassName?: string;
49
+ /* Keeps the popup in the document while it is closed. */
50
+ keepMounted?: boolean;
51
+ };
52
+
53
+ /*
54
+ * The portal, the backdrop and the popup of a dialog in one part. A dialog that opens while another
55
+ * one is up, such as one from the menu bar over a settings dialog, takes the nested steps and a
56
+ * backdrop of its own, or the one underneath stays on top and keeps taking clicks.
57
+ */
58
+ export function DialogPopup({ size, nested = false, backdrop = true, backdropClassName, keepMounted, className, ...props }: DialogPopupProps) {
59
+ const stacked = useDialogLayer(useContext(DialogOpenContext)) || nested;
60
+ return (
61
+ <BaseDialog.Portal keepMounted={keepMounted}>
62
+ {backdrop && (
63
+ <BaseDialog.Backdrop className={clsx('dialog-backdrop', stacked && 'dialog-backdrop-nested', backdropClassName)} forceRender={stacked} />
64
+ )}
65
+ <BaseDialog.Popup
66
+ className={withClass(clsx('dialog-popup', size !== undefined && SIZE[size], stacked && 'dialog-popup-nested'), className)}
67
+ {...props}
68
+ />
69
+ </BaseDialog.Portal>
70
+ );
71
+ }
72
+
73
+ export type DialogTitleProps = ComponentProps<typeof BaseDialog.Title> & {
74
+ /* `lg` for a dialog with a header of its own rather than a question. */
75
+ size?: 'base' | 'lg';
76
+ };
77
+
78
+ export function DialogTitle({ size = 'base', className, ...props }: DialogTitleProps) {
79
+ return <BaseDialog.Title className={withClass(clsx(size === 'lg' ? 'text-lg' : 'text-base', 'font-semibold text-text'), className)} {...props} />;
80
+ }
81
+
82
+ const DESCRIPTION = { sm: 'text-sm text-text-muted', xs: 'text-xs text-text-muted' } as const;
83
+
84
+ export type DialogDescriptionProps = ComponentProps<typeof BaseDialog.Description> & {
85
+ size?: keyof typeof DESCRIPTION;
86
+ };
87
+
88
+ /* The sentence under the title, which is also what a screen reader reads as the dialog's description. It carries no margin. */
89
+ export function DialogDescription({ size = 'sm', className, ...props }: DialogDescriptionProps) {
90
+ return <BaseDialog.Description className={withClass(DESCRIPTION[size], className)} {...props} />;
91
+ }
92
+
93
+ export type DialogTextProps = useRender.ComponentProps<'p'> & {
94
+ size?: keyof typeof DESCRIPTION;
95
+ };
96
+
97
+ /* A further line in the style of the description that is not the dialog's description: a warning, a note under a choice. */
98
+ export function DialogText({ size = 'sm', render, className, ref, ...props }: DialogTextProps) {
99
+ return useRender({ render, ref, defaultTagName: 'p', props: { ...props, className: clsx(DESCRIPTION[size], className) } });
100
+ }
101
+
102
+ export type DialogFooterProps = useRender.ComponentProps<'div'>;
103
+
104
+ /* The buttons at the foot of a dialog, the main action last and so on the right. */
105
+ export function DialogFooter({ render, className, ref, ...props }: DialogFooterProps) {
106
+ return useRender({
107
+ render,
108
+ ref,
109
+ defaultTagName: 'div',
110
+ props: { ...props, className: clsx('mt-4 flex flex-wrap items-center justify-end gap-2', className) }
111
+ });
112
+ }
@@ -0,0 +1,31 @@
1
+ import { useLayoutEffect, useRef, useState } from 'react';
2
+
3
+ // The app-level dialogs open right now. Each one is its own root, so Base UI does not see them as nested.
4
+ const open = new Set<symbol>();
5
+
6
+ /*
7
+ * Whether a dialog opened while another app-level dialog was already up, such as one opened from the
8
+ * menu bar over a settings dialog. Such a dialog takes the nested steps (`dialog-backdrop-nested`,
9
+ * `dialog-popup-nested`, a forced backdrop), or the one underneath stays on top and keeps taking clicks.
10
+ */
11
+ export function useDialogLayer(isOpen: boolean): boolean {
12
+ const id = useRef(Symbol('dialog'));
13
+ // Decided in render at the moment the dialog opens, so its first frame is already on the right step.
14
+ const [seen, setSeen] = useState({ open: false, stacked: false });
15
+ if (seen.open !== isOpen) {
16
+ setSeen({ open: isOpen, stacked: isOpen && open.size > 0 });
17
+ }
18
+
19
+ useLayoutEffect(() => {
20
+ if (!isOpen) {
21
+ return;
22
+ }
23
+ const self = id.current;
24
+ open.add(self);
25
+ return () => {
26
+ open.delete(self);
27
+ };
28
+ }, [isOpen]);
29
+
30
+ return isOpen && seen.stacked;
31
+ }
@@ -0,0 +1,40 @@
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
+ /* Anything the subtree draws from. A change in any of them means the data may be whole again. */
7
+ export type ResetKeys = readonly unknown[];
8
+
9
+ /*
10
+ * Whether a boundary that holds an error should draw its children again. Only a boundary that
11
+ * failed resets. Comparing keys while the children are fine would remount a terminal or an embedded
12
+ * page every time the data under it changes.
13
+ */
14
+ export const shouldReset = (failed: boolean, previous: ResetKeys, next: ResetKeys): boolean => {
15
+ if (!failed) {
16
+ return false;
17
+ }
18
+ if (previous.length !== next.length) {
19
+ return true;
20
+ }
21
+ return previous.some((key, i) => !Object.is(key, next[i]));
22
+ };
23
+
24
+ /* A thrown value is not always an Error, and the message has to say something either way. */
25
+ export const errorMessageOf = (error: unknown): string => {
26
+ if (error instanceof Error) {
27
+ return error.message || error.name;
28
+ }
29
+ return typeof error === 'string' ? error : String(error);
30
+ };
31
+
32
+ /* The text behind the copy button: what a bug report needs, the component stack included. */
33
+ export const errorReport = (label: string, error: unknown, componentStack: string | null): string => {
34
+ const stack = error instanceof Error && error.stack ? error.stack : errorMessageOf(error);
35
+ const parts = [label, stack];
36
+ if (componentStack) {
37
+ parts.push(`Component stack:${componentStack}`);
38
+ }
39
+ return parts.join('\n\n');
40
+ };
@@ -0,0 +1,5 @@
1
+ /* The sentence of an error, for a person to read. One that crossed an Electron bridge (`ipcRenderer.invoke`)
2
+ carries Electron's own prefix, which says nothing to a person. A throw that is not an Error has no
3
+ sentence in it, so a caller that knows the step names it. */
4
+ export const messageOf = (e: unknown, fallback?: string): string =>
5
+ (e instanceof Error ? e.message : (fallback ?? String(e))).replace(/^Error invoking remote method '[^']+': (?:[A-Za-z]*Error: )?/, '');
@@ -0,0 +1,12 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export interface FieldControl {
4
+ id: string;
5
+ describedBy: string | undefined;
6
+ invalid: boolean;
7
+ }
8
+
9
+ export const FieldContext = createContext<FieldControl | null>(null);
10
+
11
+ /* What an `Input` or a `TextArea` inside a `Field` takes over: the id its label points at, the hint and the error it is described by. */
12
+ export const useFieldControl = (): FieldControl | null => useContext(FieldContext);
@@ -0,0 +1,52 @@
1
+ import { createFileTreeIconResolver, type FileTreeIconConfig } from '@pierre/trees';
2
+ import { SETI_BY_FILE_EXTENSION, SETI_BY_FILE_NAME, SETI_BY_FILE_NAME_CONTAINS, SETI_DEFAULT_SYMBOL, SETI_SPRITE_SHEET } from './seti-icons.ts';
3
+
4
+ const SPRITE_ELEMENT_ID = 'adecore-ui-file-icon-sprite';
5
+
6
+ /* The glyphs are Seti's (`seti-icons.ts`), which knows far more file types than the sets the tree
7
+ ships with. The tree only colors its own sets, so every symbol carries its hue in its id and the
8
+ sprite brings the rules that color it. A tree takes all of it as configuration where everything
9
+ drawn beside it calls `fileIconFor`, so a tab, a picker row and a tree row never disagree about
10
+ what a file is. Hand this to the tree. */
11
+ export const FILE_TREE_ICONS: FileTreeIconConfig = {
12
+ set: 'none',
13
+ spriteSheet: SETI_SPRITE_SHEET,
14
+ remap: { 'file-tree-icon-file': SETI_DEFAULT_SYMBOL },
15
+ byFileName: SETI_BY_FILE_NAME,
16
+ byFileExtension: SETI_BY_FILE_EXTENSION,
17
+ byFileNameContains: SETI_BY_FILE_NAME_CONTAINS
18
+ };
19
+
20
+ let resolver: ReturnType<typeof createFileTreeIconResolver> | null = null;
21
+
22
+ /* Built on the first icon rather than on import, so a bundle that never draws one never builds it. */
23
+ const resolveIcon: ReturnType<typeof createFileTreeIconResolver>['resolveIcon'] = (...args) => {
24
+ resolver ??= createFileTreeIconResolver(FILE_TREE_ICONS);
25
+ return resolver.resolveIcon(...args);
26
+ };
27
+
28
+ /*
29
+ * The id of the `<symbol>` the set gives a file, in the sprite `mountFileIconSprite` puts in the
30
+ * document. The path may be absolute or relative. Only the last segment decides, first by exact
31
+ * name (`package.json`, `.gitignore`), then by a part of the name (`Dockerfile.dev`), then by the
32
+ * longest extension that matches (`spec.ts` before `ts`). Anything the set does not know falls back
33
+ * to its generic file icon, and so does a directory. The tree marks a folder with the chevron that
34
+ * turns as it opens, not with a glyph of its own.
35
+ */
36
+ export const fileIconFor = (path: string): string => resolveIcon('file-tree-icon-file', path).name;
37
+
38
+ /* The tree carries the sprite into its own shadow root, out of reach of anything that draws an
39
+ icon next to it, so the document gets a copy of the same sheet to point `<use>` at. */
40
+ export const mountFileIconSprite = (): void => {
41
+ if (document.getElementById(SPRITE_ELEMENT_ID) !== null) {
42
+ return;
43
+ }
44
+ const holder = document.createElement('div');
45
+ holder.innerHTML = SETI_SPRITE_SHEET;
46
+ const sprite = holder.firstElementChild;
47
+ if (sprite === null) {
48
+ return;
49
+ }
50
+ sprite.id = SPRITE_ELEMENT_ID;
51
+ (document.body ?? document.documentElement).append(sprite);
52
+ };
@@ -0,0 +1,9 @@
1
+ // Every popup, menu, tooltip and dialog of the library is portaled; these are the layers it draws them on.
2
+ const FLOATING_LAYERS = '[data-base-ui-portal], .tooltip-popup, .dialog-popup, .dialog-backdrop';
3
+
4
+ /* Whether an event landed in a popup rather than on the surface under it, which a canvas-like surface must not take as a click of its own. */
5
+ export const isInFloatingLayer = (target: EventTarget | null): boolean => target instanceof Element && target.closest(FLOATING_LAYERS) !== null;
6
+
7
+ /* A popup is portaled out of its trigger's element but not out of React's tree, so its events still bubble to that trigger. */
8
+ export const cameThroughPortal = (event: { currentTarget: Element; target: EventTarget }): boolean =>
9
+ !(event.target instanceof Node && event.currentTarget.contains(event.target));
@@ -0,0 +1,63 @@
1
+ import { dateFormatter, wordFormatter } from './locale.ts';
2
+
3
+ /* The parts that are a word rather than a number, and so come from the language and not the region. */
4
+ const WORD_PARTS = new Set<Intl.DateTimeFormatPartTypes>(['month', 'weekday', 'dayPeriod', 'era']);
5
+
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.
12
+ */
13
+ export const formatDateTime = (at: Date | number, options: Intl.DateTimeFormatOptions): string => {
14
+ const date = typeof at === 'number' ? new Date(at) : at;
15
+ const parts = dateFormatter(options).formatToParts(date);
16
+ if (!parts.some((part) => WORD_PARTS.has(part.type))) {
17
+ return parts.map((part) => part.value).join('');
18
+ }
19
+ const english = wordFormatter(options).formatToParts(date);
20
+ return parts.map((part) => (WORD_PARTS.has(part.type) ? (english.find((word) => word.type === part.type)?.value ?? part.value) : part.value)).join('');
21
+ };
22
+
23
+ const CLOCK: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' };
24
+ const WEEKDAY_CLOCK: Intl.DateTimeFormatOptions = { weekday: 'short', hour: '2-digit', minute: '2-digit' };
25
+ const HOUR: Intl.DateTimeFormatOptions = { hour: 'numeric' };
26
+ const DAY: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
27
+ const DAY_WITH_YEAR: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' };
28
+ const WEEKDAY_DAY: Intl.DateTimeFormatOptions = { weekday: 'short', month: 'short', day: 'numeric' };
29
+ const DAY_CLOCK: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
30
+ const NUMERIC_DATE: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'numeric', day: 'numeric' };
31
+
32
+ /* `08:05`, or `08:05 AM` in a region that counts to twelve. */
33
+ export const formatClock = (at: Date | number): string => formatDateTime(at, CLOCK);
34
+
35
+ /* A time far enough off that its day is part of the answer: `Sat 08:05`. */
36
+ export const formatWeekdayClock = (at: Date | number): string => formatDateTime(at, WEEKDAY_CLOCK);
37
+
38
+ /* An hour of the day on its own, for the foot of a chart: `08`, or `8 AM` where that is the clock. */
39
+ export const formatHour = (at: Date | number): string => formatDateTime(at, HOUR);
40
+
41
+ /* `19 Sep`, the short date a row falls back to once "days ago" stops meaning anything. */
42
+ export const formatDay = (at: Date | number): string => formatDateTime(at, DAY);
43
+
44
+ /* The same date with the year, for anything older than the one we are in. */
45
+ export const formatDayWithYear = (at: Date | number): string => formatDateTime(at, DAY_WITH_YEAR);
46
+
47
+ /* `Sat 19 Sep`. */
48
+ export const formatWeekdayDay = (at: Date | number): string => formatDateTime(at, WEEKDAY_DAY);
49
+
50
+ /* `19 Sep, 08:05`: a moment that is not today, to the minute. */
51
+ export const formatDayClock = (at: Date | number): string => formatDateTime(at, DAY_CLOCK);
52
+
53
+ /* All numbers, as a form would print it: `19-9-2026` here, `9/19/2026` in the United States. */
54
+ export const formatNumericDate = (at: Date | number): string => formatDateTime(at, NUMERIC_DATE);
55
+
56
+ /* Whether two instants fall on the same day of the same year, in the zone of whoever is reading. */
57
+ export const isSameDay = (a: Date | number, b: Date | number): boolean => {
58
+ const left = typeof a === 'number' ? new Date(a) : a;
59
+ const right = typeof b === 'number' ? new Date(b) : b;
60
+ return left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth() && left.getDate() === right.getDate();
61
+ };
62
+
63
+ export const formatMoment = (at: Date | number, now: Date | number = Date.now()): string => (isSameDay(at, now) ? formatClock(at) : formatDayClock(at));
@@ -0,0 +1,83 @@
1
+ import { relativeFormatter } from './locale.ts';
2
+ import { formatDecimal, formatNumber } from './number.ts';
3
+
4
+ const SECOND = 1000;
5
+ const MINUTE = 60 * SECOND;
6
+ const HOUR = 60 * MINUTE;
7
+ const DAY = 24 * HOUR;
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
+ */
14
+ export const formatDuration = (ms: number): string => {
15
+ const seconds = Math.max(0, Math.round(ms / SECOND));
16
+ if (seconds < 60) {
17
+ return `${formatNumber(seconds)} s`;
18
+ }
19
+ const minutes = Math.round(seconds / 60);
20
+ if (minutes < 60) {
21
+ return `${formatNumber(minutes)} min`;
22
+ }
23
+ return `${formatDecimal(minutes / 60)} h`;
24
+ };
25
+
26
+ /* How long a window still has, tight enough for the end of a bar: `4d 3h`, `12h 8m`, `9m`. */
27
+ export const formatCountdown = (ms: number): string => {
28
+ const left = Math.max(0, ms);
29
+ if (left >= DAY) {
30
+ return `${formatNumber(Math.floor(left / DAY))}d ${formatNumber(Math.round((left % DAY) / HOUR))}h`;
31
+ }
32
+ if (left >= HOUR) {
33
+ return `${formatNumber(Math.floor(left / HOUR))}h ${formatNumber(Math.round((left % HOUR) / MINUTE))}m`;
34
+ }
35
+ return `${formatNumber(Math.max(1, Math.round(left / MINUTE)))}m`;
36
+ };
37
+
38
+ /*
39
+ * How long something has been running, to two units and no space to spare: `12s`, `2m 5s`, `3h 20m`.
40
+ * A call that took a fraction of a second still took some, so anything above zero reads as at least
41
+ * `1s`; only a clock that runs ahead of the start comes out as `0s`.
42
+ */
43
+ export const formatElapsedShort = (ms: number): string => {
44
+ const seconds = ms <= 0 ? 0 : Math.max(1, Math.round(ms / SECOND));
45
+ if (seconds < 60) {
46
+ return `${formatNumber(seconds)}s`;
47
+ }
48
+ if (seconds < 3600) {
49
+ const rest = seconds % 60;
50
+ const minutes = Math.floor(seconds / 60);
51
+ return rest === 0 ? `${formatNumber(minutes)}m` : `${formatNumber(minutes)}m ${formatNumber(rest)}s`;
52
+ }
53
+ const hours = Math.floor(seconds / 3600);
54
+ const minutes = Math.floor((seconds % 3600) / 60);
55
+ return minutes === 0 ? `${formatNumber(hours)}h` : `${formatNumber(hours)}h ${formatNumber(minutes)}m`;
56
+ };
57
+
58
+ /* A stopwatch that is watched while it runs: `00:14`, and `1:02:03` once it passes an hour. */
59
+ export const formatClockDuration = (ms: number): string => {
60
+ const seconds = Math.max(0, Math.floor(ms / SECOND));
61
+ const pad = (value: number): string => String(value).padStart(2, '0');
62
+ const clock = `${pad(Math.floor((seconds % 3600) / 60))}:${pad(seconds % 60)}`;
63
+ const hours = Math.floor(seconds / 3600);
64
+ return hours > 0 ? `${formatNumber(hours)}:${clock}` : clock;
65
+ };
66
+
67
+ const AGO: Intl.RelativeTimeFormatOptions = { style: 'narrow', numeric: 'always' };
68
+ const NOW: Intl.RelativeTimeFormatOptions = { style: 'narrow', numeric: 'auto' };
69
+
70
+ /* How long ago something was, short enough for the right edge of a row: `3m ago`, `3 min. geleden`. */
71
+ export const formatAgo = (ms: number): string => {
72
+ const past = Math.max(0, ms);
73
+ if (past < MINUTE) {
74
+ return relativeFormatter(NOW).format(0, 'second');
75
+ }
76
+ if (past < HOUR) {
77
+ return relativeFormatter(AGO).format(-Math.floor(past / MINUTE), 'minute');
78
+ }
79
+ if (past < DAY) {
80
+ return relativeFormatter(AGO).format(-Math.floor(past / HOUR), 'hour');
81
+ }
82
+ return relativeFormatter(AGO).format(-Math.floor(past / DAY), 'day');
83
+ };
@@ -0,0 +1,20 @@
1
+ /* Every number, date, time and duration a person reads, written the way the language and the region they set ask for. */
2
+
3
+ export {
4
+ formatClock,
5
+ formatDateTime,
6
+ formatDay,
7
+ formatDayClock,
8
+ formatDayWithYear,
9
+ formatHour,
10
+ formatMoment,
11
+ formatNumericDate,
12
+ formatWeekdayClock,
13
+ formatWeekdayDay,
14
+ isSameDay
15
+ } from './datetime.ts';
16
+ export { formatAgo, formatClockDuration, formatCountdown, formatDuration, formatElapsedShort } from './duration.ts';
17
+ export { FALLBACK_LOCALE, formatLocale, labelCollator, setFormatSource, systemLocale, useFormatLocale, type FormatSource } from './locale.ts';
18
+ export { formatBytes, formatDecimal, formatMoney, formatNumber, formatPercent, formatTokens, formatUsdSignificant } from './number.ts';
19
+ export { FORMAT_LANGUAGE, FORMAT_REGIONS, FORMAT_REGION_CHOICES, FORMAT_SYSTEM, formatRegionFrom, regionName } from './regions.ts';
20
+ export { localTimeZone } from './time-zone.ts';
@@ -0,0 +1,153 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import { FORMAT_LANGUAGE, FORMAT_SYSTEM } from './regions.ts';
3
+
4
+ /*
5
+ * Where the formatters read what a person set. The app owns both settings and hands them over once,
6
+ * before the first render; the language writes the words and the region writes the notation.
7
+ */
8
+ export interface FormatSource {
9
+ /* The language the interface is written in right now, such as `en` or `nl`. */
10
+ language(): string;
11
+ /* A region tag such as `nl-NL`, or `FORMAT_LANGUAGE` or `FORMAT_SYSTEM`. */
12
+ region(): string;
13
+ /* The system's own locale when a shell can read it; a browser tab falls back on `navigator`. */
14
+ systemLocale?(): string | undefined;
15
+ /* Calls back whenever the language or the region may have changed. */
16
+ subscribe(onChange: () => void): () => void;
17
+ }
18
+
19
+ let source: FormatSource = { language: () => 'en', region: () => FORMAT_LANGUAGE, subscribe: () => () => {} };
20
+
21
+ /* Answers the source it replaced, so a test can put that one back. */
22
+ export const setFormatSource = (next: FormatSource): FormatSource => {
23
+ const previous = source;
24
+ source = next;
25
+ return previous;
26
+ };
27
+
28
+ /* Where a language comes from when nothing else says: the country most of its speakers are in. */
29
+ const LANGUAGE_REGIONS: Record<string, string> = { en: 'en-US', nl: 'nl-NL' };
30
+
31
+ /* The one locale to fall back on, so a format never depends on which machine ran the test. */
32
+ export const FALLBACK_LOCALE = 'en-US';
33
+
34
+ /* A month and a weekday are words, so they come from the language the interface is in, while the
35
+ order, the separators and the clock come from the region. English words in a Dutch notation is
36
+ what a Dutch Mac running an English app should read like, and both halves are a person's to set. */
37
+ export const wordLocale = (): string => source.language();
38
+
39
+ /* A tag `Intl` will take, or nothing. macOS hands out locales with overrides attached, and a
40
+ formatter built on one it does not know throws where a number was meant to go. */
41
+ const usable = (tag: string | undefined): string | null => {
42
+ if (tag === undefined || tag === '') {
43
+ return null;
44
+ }
45
+ try {
46
+ return Intl.DateTimeFormat.supportedLocalesOf([tag]).length > 0 ? tag : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ };
51
+
52
+ /*
53
+ * What the operating system was set to read numbers and dates in. In a desktop shell Chromium's own
54
+ * locale is the language of the app bundle, so an English bundle on a Dutch Mac would still hand back
55
+ * `en-US` and write `08:00 AM`; the shell reads the system's region instead and passes it in
56
+ * (`systemLocale`). A browser has no such thing, so there the language is the best there is.
57
+ */
58
+ export const systemLocale = (): string => {
59
+ const fromShell = usable(source.systemLocale?.());
60
+ if (fromShell !== null) {
61
+ return fromShell;
62
+ }
63
+ if (typeof navigator === 'undefined') {
64
+ return FALLBACK_LOCALE;
65
+ }
66
+ return usable(navigator.languages?.[0] ?? navigator.language) ?? FALLBACK_LOCALE;
67
+ };
68
+
69
+ /*
70
+ * The region a language brings with it. The system's own when it already speaks that language, so
71
+ * Dutch in Belgium keeps writing dates the Belgian way, and the country the language is most spoken
72
+ * in when it does not.
73
+ */
74
+ const regionOfLanguage = (): string => {
75
+ const language = source.language();
76
+ const system = systemLocale();
77
+ return system.toLowerCase().startsWith(language) ? system : (LANGUAGE_REGIONS[language] ?? language);
78
+ };
79
+
80
+ const resolve = (region: string): string => {
81
+ if (region === FORMAT_LANGUAGE) {
82
+ return regionOfLanguage();
83
+ }
84
+ return region === FORMAT_SYSTEM ? systemLocale() : region;
85
+ };
86
+
87
+ /* The locale every formatter in this folder is built on. Read outside React as well, so it is a
88
+ plain function over the source rather than a hook. */
89
+ export const formatLocale = (): string => resolve(source.region());
90
+
91
+ /* What a component calls to draw again once the region changes. The value is the locale, which a
92
+ caller may use or ignore; subscribing is the point. */
93
+ const subscribe = (onChange: () => void): (() => void) => source.subscribe(onChange);
94
+
95
+ export const useFormatLocale = (): string => useSyncExternalStore(subscribe, formatLocale, formatLocale);
96
+
97
+ /*
98
+ * A formatter per options object, keyed on the object itself, so a module-level spec builds its
99
+ * formatter once and a list of a thousand rows constructs nothing. A region change throws the entry
100
+ * away, since the locale is part of what was cached.
101
+ */
102
+ interface Cached<T> {
103
+ locale: string;
104
+ formatter: T;
105
+ }
106
+
107
+ const cacheFor = <O extends object, T>(store: WeakMap<O, Cached<T>>, options: O, locale: string, build: (locale: string, options: O) => T): T => {
108
+ const hit = store.get(options);
109
+ if (hit !== undefined && hit.locale === locale) {
110
+ return hit.formatter;
111
+ }
112
+ const formatter = build(locale, options);
113
+ store.set(options, { locale, formatter });
114
+ return formatter;
115
+ };
116
+
117
+ const numbers = new WeakMap<Intl.NumberFormatOptions, Cached<Intl.NumberFormat>>();
118
+ const dates = new WeakMap<Intl.DateTimeFormatOptions, Cached<Intl.DateTimeFormat>>();
119
+ const words = new WeakMap<Intl.DateTimeFormatOptions, Cached<Intl.DateTimeFormat>>();
120
+ const relatives = new WeakMap<Intl.RelativeTimeFormatOptions, Cached<Intl.RelativeTimeFormat>>();
121
+
122
+ export const numberFormatter = (options: Intl.NumberFormatOptions): Intl.NumberFormat =>
123
+ cacheFor(numbers, options, formatLocale(), (locale, spec) => new Intl.NumberFormat(locale, spec));
124
+
125
+ export const dateFormatter = (options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat =>
126
+ cacheFor(dates, options, formatLocale(), (locale, spec) => new Intl.DateTimeFormat(locale, spec));
127
+
128
+ /* The same date, written in the language the words come from. Never shown whole: only its month,
129
+ weekday, era and day period are lifted out of it. */
130
+ export const wordFormatter = (options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat =>
131
+ cacheFor(words, options, wordLocale(), (locale, spec) => new Intl.DateTimeFormat(locale, spec));
132
+
133
+ /* `3 min. geleden` is words as much as a number, so the language writes it, the way it writes a month. */
134
+ export const relativeFormatter = (options: Intl.RelativeTimeFormatOptions): Intl.RelativeTimeFormat =>
135
+ cacheFor(relatives, options, wordLocale(), (locale, spec) => new Intl.RelativeTimeFormat(locale, spec));
136
+
137
+ const collators = new Map<string, Intl.Collator>();
138
+
139
+ /*
140
+ * How two labels a person reads are put in order. Labels are words, so the language writes them and
141
+ * the language sorts them: Dutch puts Tekening after Terminal where English puts Drawing before it.
142
+ * Keyed on the locale rather than on an options object, since there are no options to key on.
143
+ */
144
+ export const labelCollator = (): Intl.Collator => {
145
+ const locale = wordLocale();
146
+ const held = collators.get(locale);
147
+ if (held !== undefined) {
148
+ return held;
149
+ }
150
+ const collator = new Intl.Collator(locale, { sensitivity: 'base', numeric: true });
151
+ collators.set(locale, collator);
152
+ return collator;
153
+ };