nexus-shared 3.1.0 → 4.0.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 (234) hide show
  1. package/CHANGELOG.md +2268 -446
  2. package/LICENSE +21 -21
  3. package/README.md +1705 -1084
  4. package/dist/Client.Index.d.ts +17 -0
  5. package/dist/Client.Index.js +17 -0
  6. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  7. package/dist/Components/Documents/DocumentFrame.js +83 -0
  8. package/dist/Components/Documents/Menu.d.ts +4 -0
  9. package/dist/Components/Documents/Menu.js +40 -24
  10. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  11. package/dist/Components/Documents/PrintDocument.js +62 -0
  12. package/dist/Components/Documents/Tabs.d.ts +59 -0
  13. package/dist/Components/Documents/Tabs.js +70 -0
  14. package/dist/Components/Forms/ApiForm.js +19 -2
  15. package/dist/Components/Forms/Crud.d.ts +2 -2
  16. package/dist/Components/Forms/Crud.js +871 -377
  17. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  18. package/dist/Components/Forms/CrudActions.js +184 -0
  19. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  20. package/dist/Components/Forms/CrudHistory.js +140 -0
  21. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  22. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  23. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  24. package/dist/Components/Forms/CrudParts.js +56 -5
  25. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  26. package/dist/Components/Forms/CrudRowForms.js +114 -0
  27. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  28. package/dist/Components/Forms/CrudScopes.js +76 -0
  29. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  30. package/dist/Components/Forms/CrudTree.js +363 -0
  31. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  32. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  33. package/dist/Components/Forms/ExcelImport.js +3 -3
  34. package/dist/Components/Forms/Form.d.ts +4 -4
  35. package/dist/Components/Forms/Form.js +125 -16
  36. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  37. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  38. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  39. package/dist/Components/Forms/RecordFlows.js +282 -0
  40. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  41. package/dist/Components/Forms/SubmitForm.js +195 -10
  42. package/dist/Components/Inputs/CheckBox.js +1 -1
  43. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  44. package/dist/Components/Inputs/ChoiceField.js +28 -2
  45. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  46. package/dist/Components/Inputs/CodeBox.js +316 -0
  47. package/dist/Components/Inputs/DatePicker.js +4 -1
  48. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  49. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  50. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  51. package/dist/Components/Inputs/Dropdown.js +8 -5
  52. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  53. package/dist/Components/Inputs/DropdownParts.js +16 -4
  54. package/dist/Components/Inputs/Field.d.ts +7 -1
  55. package/dist/Components/Inputs/Field.js +2 -2
  56. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  57. package/dist/Components/Inputs/GroupForm.js +10 -11
  58. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  59. package/dist/Components/Inputs/InputRenderer.js +39 -31
  60. package/dist/Components/Inputs/NumberBox.js +4 -2
  61. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  62. package/dist/Components/Inputs/PictureInput.js +70 -0
  63. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  64. package/dist/Components/Inputs/RadioGroup.js +8 -3
  65. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  66. package/dist/Components/Inputs/RowsInput.js +18 -3
  67. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  68. package/dist/Components/Inputs/TabularForm.js +29 -29
  69. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  70. package/dist/Components/Inputs/TextArea.js +14 -2
  71. package/dist/Components/Inputs/TextBox.js +4 -1
  72. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  73. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  74. package/dist/Components/Inputs/TimePicker.js +4 -1
  75. package/dist/Components/Inputs/TreeView.d.ts +107 -6
  76. package/dist/Components/Inputs/TreeView.js +513 -30
  77. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  78. package/dist/Components/Layouts/MessageBox.js +56 -7
  79. package/dist/Components/Layouts/Popup.d.ts +13 -6
  80. package/dist/Components/Layouts/Popup.js +37 -9
  81. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  82. package/dist/Components/Layouts/PopupDock.js +2 -1
  83. package/dist/Components/Layouts/PopupHost.js +5 -4
  84. package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
  85. package/dist/Components/Layouts/ThemePicker.js +16 -4
  86. package/dist/Components/Viewers/DataTable.js +205 -61
  87. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  88. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  89. package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
  90. package/dist/Components/Viewers/DataTableParts.js +62 -21
  91. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  92. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  93. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  94. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  95. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  96. package/dist/Components/Viewers/RecordCards.js +509 -0
  97. package/dist/Helpers/ApiModules.d.ts +3 -34
  98. package/dist/Helpers/ApiModules.js +3 -3
  99. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  100. package/dist/Helpers/ApiRoutes.js +31 -27
  101. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  102. package/dist/Helpers/ArrayHelpers.js +2 -0
  103. package/dist/Helpers/BusyHooks.d.ts +6 -0
  104. package/dist/Helpers/BusyHooks.js +39 -0
  105. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  106. package/dist/Helpers/CodeHelpers.js +332 -0
  107. package/dist/Helpers/CrudBackend.d.ts +114 -6
  108. package/dist/Helpers/CrudBackend.js +214 -11
  109. package/dist/Helpers/CrudColumns.d.ts +98 -0
  110. package/dist/Helpers/CrudColumns.js +160 -0
  111. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  112. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  113. package/dist/Helpers/CrudHelpers.d.ts +130 -6
  114. package/dist/Helpers/CrudHelpers.js +304 -12
  115. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  116. package/dist/Helpers/CrudNarrowPick.js +157 -0
  117. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  118. package/dist/Helpers/CrudRowForms.js +43 -0
  119. package/dist/Helpers/CrudScopes.d.ts +32 -0
  120. package/dist/Helpers/CrudScopes.js +79 -0
  121. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  122. package/dist/Helpers/DropdownHelpers.js +6 -2
  123. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  124. package/dist/Helpers/ExcelHelpers.js +12 -11
  125. package/dist/Helpers/FileAdapter.d.ts +78 -0
  126. package/dist/Helpers/FileAdapter.js +102 -0
  127. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  128. package/dist/Helpers/FilePrimaries.js +138 -0
  129. package/dist/Helpers/FormConfirm.d.ts +32 -0
  130. package/dist/Helpers/FormConfirm.js +40 -0
  131. package/dist/Helpers/FormStore.d.ts +90 -5
  132. package/dist/Helpers/FormStore.js +318 -22
  133. package/dist/Helpers/InputConditions.d.ts +50 -0
  134. package/dist/Helpers/InputConditions.js +168 -0
  135. package/dist/Helpers/InputMemory.d.ts +106 -0
  136. package/dist/Helpers/InputMemory.js +385 -0
  137. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  138. package/dist/Helpers/InputNarrowing.js +67 -0
  139. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  140. package/dist/Helpers/InputParamsHelpers.js +162 -71
  141. package/dist/Helpers/MessageBuilder.js +2 -0
  142. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  143. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  144. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  145. package/dist/Helpers/NexusHelpers.js +1 -0
  146. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  147. package/dist/Helpers/NumberHelpers.js +126 -0
  148. package/dist/Helpers/Permissions.js +2 -1
  149. package/dist/Helpers/QrCode.d.ts +26 -0
  150. package/dist/Helpers/QrCode.js +463 -0
  151. package/dist/Helpers/RecordCards.d.ts +205 -0
  152. package/dist/Helpers/RecordCards.js +378 -0
  153. package/dist/Helpers/RequestCache.d.ts +47 -0
  154. package/dist/Helpers/RequestCache.js +132 -0
  155. package/dist/Helpers/RowsStore.d.ts +37 -2
  156. package/dist/Helpers/RowsStore.js +128 -10
  157. package/dist/Helpers/ServerPreview.d.ts +69 -0
  158. package/dist/Helpers/ServerPreview.js +99 -0
  159. package/dist/Helpers/TableColumns.js +4 -3
  160. package/dist/Helpers/TableExport.d.ts +7 -1
  161. package/dist/Helpers/TableExport.js +21 -15
  162. package/dist/Helpers/TableFilters.d.ts +1 -1
  163. package/dist/Helpers/TableFilters.js +24 -23
  164. package/dist/Helpers/TableHelpers.d.ts +24 -3
  165. package/dist/Helpers/TableHelpers.js +58 -12
  166. package/dist/Helpers/TableJoins.d.ts +83 -0
  167. package/dist/Helpers/TableJoins.js +284 -0
  168. package/dist/Helpers/TableStore.d.ts +26 -1
  169. package/dist/Helpers/TableStore.js +73 -7
  170. package/dist/Helpers/TreeStore.d.ts +306 -11
  171. package/dist/Helpers/TreeStore.js +1048 -90
  172. package/dist/Helpers/UserPreferences.d.ts +8 -0
  173. package/dist/Helpers/UserPreferences.js +17 -0
  174. package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
  175. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  176. package/dist/Interfaces/FileInterfaces.js +4 -0
  177. package/dist/Interfaces/FormInterfaces.d.ts +393 -29
  178. package/dist/Interfaces/FormInterfaces.js +79 -1
  179. package/dist/Interfaces/InputInterfaces.d.ts +39 -3
  180. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  181. package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
  182. package/dist/Interfaces/PreferenceInterfaces.js +2 -5
  183. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  184. package/dist/Interfaces/RecordInterfaces.js +1 -0
  185. package/dist/Interfaces/TableInterfaces.d.ts +160 -5
  186. package/dist/Services/ApiProxy.d.ts +36 -1
  187. package/dist/Services/ApiProxy.js +182 -52
  188. package/dist/Services/ApiProxyRules.d.ts +203 -0
  189. package/dist/Services/ApiProxyRules.js +426 -0
  190. package/dist/Services/ExcelApi.js +5 -0
  191. package/dist/Services/FileApi.d.ts +13 -0
  192. package/dist/Services/FileApi.js +29 -0
  193. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  194. package/dist/Services/ImageEditorLoader.js +51 -0
  195. package/dist/Services/JoinSources.d.ts +80 -0
  196. package/dist/Services/JoinSources.js +150 -0
  197. package/dist/Services/ModuleImport.d.ts +7 -0
  198. package/dist/Services/ModuleImport.js +22 -0
  199. package/dist/Services/ServerApi.d.ts +13 -2
  200. package/dist/Services/ServerApi.js +8 -3
  201. package/dist/Shared.Index.d.ts +20 -0
  202. package/dist/Shared.Index.js +20 -0
  203. package/package.json +92 -85
  204. package/src/Styles/Nexus.Avatar.css +80 -79
  205. package/src/Styles/Nexus.Base.css +117 -117
  206. package/src/Styles/Nexus.Button.css +186 -180
  207. package/src/Styles/Nexus.Calendar.css +273 -273
  208. package/src/Styles/Nexus.Chat.css +1443 -1443
  209. package/src/Styles/Nexus.Choice.css +359 -343
  210. package/src/Styles/Nexus.Crud.css +542 -168
  211. package/src/Styles/Nexus.DateTime.css +81 -81
  212. package/src/Styles/Nexus.Dropdown.css +733 -725
  213. package/src/Styles/Nexus.Editor.css +295 -295
  214. package/src/Styles/Nexus.Excel.css +389 -389
  215. package/src/Styles/Nexus.Files.css +410 -0
  216. package/src/Styles/Nexus.Form.css +282 -279
  217. package/src/Styles/Nexus.Index.css +41 -37
  218. package/src/Styles/Nexus.Input.css +238 -4
  219. package/src/Styles/Nexus.Markdown.css +237 -237
  220. package/src/Styles/Nexus.Menu.css +246 -237
  221. package/src/Styles/Nexus.Popup.css +800 -724
  222. package/src/Styles/Nexus.Print.css +288 -0
  223. package/src/Styles/Nexus.QrCode.css +12 -0
  224. package/src/Styles/Nexus.Record.css +430 -0
  225. package/src/Styles/Nexus.Rows.css +637 -637
  226. package/src/Styles/Nexus.Slider.css +276 -276
  227. package/src/Styles/Nexus.Tab.Buttons.css +497 -432
  228. package/src/Styles/Nexus.Table.css +98 -35
  229. package/src/Styles/Nexus.Theme.Picker.css +59 -3
  230. package/src/Styles/Nexus.Themes.css +200 -194
  231. package/src/Styles/Nexus.Time.css +144 -144
  232. package/src/Styles/Nexus.Toast.css +263 -263
  233. package/src/Styles/Nexus.Tokens.css +94 -89
  234. package/src/Styles/Nexus.Tree.css +530 -431
@@ -5,16 +5,29 @@ import type { PopupSize } from "./Popup.tsx";
5
5
  export type MessageTone = "neutral" | "info" | "success" | "warning" | "danger" | "question";
6
6
  export interface MessageBoxOptions {
7
7
  title: ReactNode;
8
- /** Text under the title. */
8
+ /** Text under the title, in the box's muted text. */
9
9
  message?: ReactNode;
10
+ /**
11
+ * What the question is about, in full, after the message and in the normal text colour: a record's details
12
+ * (`<CrudDetails row columns />`), a list. It scrolls with the box's body while the buttons stay in view; give the box a
13
+ * width that suits it (`size: "lg"` or `"xl"`).
14
+ */
15
+ details?: ReactNode;
10
16
  /** Colors the icon: `info`, `success`, `warning`, `danger`, `question`. `neutral` shows no icon. */
11
17
  tone?: MessageTone;
12
18
  /** Replaces the tone's icon; `null` shows none. */
13
19
  icon?: ReactNode | null;
14
20
  /** Default `sm`. */
15
21
  size?: PopupSize;
16
- /** The close button, Esc, and a click outside dismiss the box. Default true. */
22
+ /** The close button and Esc dismiss the box. Default true. */
17
23
  closable?: boolean;
24
+ /**
25
+ * A click outside dismisses the box. Default: as `closable`. `false` keeps a box with much to read (a question with its
26
+ * `details`) open through a stray click, while Esc and the close button still dismiss it.
27
+ */
28
+ closeOnBackdrop?: boolean;
29
+ /** Dismisses the box when it aborts, as Esc would: the form or the page that asked has gone. */
30
+ signal?: AbortSignal;
18
31
  }
19
32
  export interface MessageBoxButton<T extends string = string> {
20
33
  value: T;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { iconAlertCircle, iconAlertTriangle, iconCircleCheck, iconHelpCircle, iconInfoCircle, InlineIcon } from "nexus-icons";
4
4
  import { cx } from "../../Helpers/ClassHelpers.js";
5
5
  import { Button } from "../Documents/Button.js";
@@ -14,40 +14,85 @@ const TONE_ICONS = {
14
14
  question: iconHelpCircle,
15
15
  };
16
16
  let promptCounter = 0;
17
+ let messageCounter = 0;
18
+ /*
19
+ * A held Enter, or one pressed again at once (a double press), never answers a box. The Enter that opened a question (in
20
+ * a form's box) would otherwise press the Confirm it gave the focus to - by the key's repeat, or by the second tap of a
21
+ * double press - before anything in the box was read. Any Enter within this long of the one before it is held.
22
+ */
23
+ const HASTY_ENTER_MS = 300;
24
+ let lastEnterAt = Number.NEGATIVE_INFINITY;
25
+ function holdHastyEnter(event) {
26
+ if (event.key !== "Enter")
27
+ return;
28
+ const gap = event.timeStamp - lastEnterAt;
29
+ lastEnterAt = event.timeStamp;
30
+ if (!(event.target instanceof Element) || !event.target.closest(".nx-popup--message"))
31
+ return;
32
+ if (event.repeat || gap < HASTY_ENTER_MS)
33
+ event.preventDefault();
34
+ }
35
+ // Listening from the start: the Enter that opens a box comes before the box.
36
+ if (typeof window !== "undefined")
37
+ window.addEventListener("keydown", holdHastyEnter, true);
17
38
  function toneIcon(tone, icon) {
18
39
  if (icon !== undefined)
19
40
  return icon;
20
41
  const data = TONE_ICONS[tone];
21
42
  return data ? _jsx(InlineIcon, { icon: data }) : null;
22
43
  }
44
+ /** The muted message, then the details in the normal text colour; each with an id the box is described by. */
45
+ function messageBody(message, details, id) {
46
+ return (_jsxs(_Fragment, { children: [message && (_jsx("div", { id: `${id}-text`, className: "nx-message__text", children: message })), details && (_jsx("div", { id: `${id}-details`, className: "nx-message__details", children: details }))] }));
47
+ }
48
+ /**
49
+ * What describes a box to a screen reader: its message and its details, read with the title as the focus enters it, so
50
+ * a question's Confirm is never reached before what it confirms was heard.
51
+ */
52
+ function describedBy(message, details, id) {
53
+ return cx(message ? `${id}-text` : undefined, details ? `${id}-details` : undefined) || undefined;
54
+ }
55
+ /** Dismisses an open box when the signal aborts; nothing is left listening once it has closed. */
56
+ function closeOnAbort(popup, signal) {
57
+ if (!signal)
58
+ return;
59
+ const dismiss = () => popup.close();
60
+ signal.addEventListener("abort", dismiss, { once: true });
61
+ void popup.closed.then(() => signal.removeEventListener("abort", dismiss));
62
+ }
23
63
  /**
24
64
  * A message box with the given buttons. Resolves with the clicked button's value, or `null` when the box was
25
65
  * dismissed with the close button, Esc, or a click outside.
26
66
  */
27
67
  export function showMessageBox(options) {
28
- const { title, message, tone = "neutral", icon, size = "sm", closable = true, buttons } = options;
68
+ const { title, message, details, tone = "neutral", icon, size = "sm", closable = true, closeOnBackdrop = closable, signal, buttons } = options;
69
+ if (signal?.aborted)
70
+ return Promise.resolve(null);
29
71
  const cautious = tone === "danger" || tone === "warning";
30
72
  const explicit = buttons.findIndex(button => button.autoFocus);
31
73
  const focusIndex = explicit >= 0 ? explicit : cautious ? 0 : buttons.length - 1;
32
74
  let result = null;
75
+ const id = `nx-message-${++messageCounter}`;
33
76
  const popup = openPopup({
34
77
  title,
35
78
  icon: toneIcon(tone, icon),
36
79
  size,
37
80
  closable,
38
- closeOnBackdrop: closable,
81
+ closeOnBackdrop,
39
82
  closeOnEscape: closable,
40
83
  variant: "dialog",
41
84
  minimizable: false,
42
85
  maximizable: false,
43
86
  className: cx("nx-popup--message", `nx-popup--tone-${tone}`),
44
87
  initialFocus: "[data-nx-focus]",
45
- content: message ? _jsx("div", { className: "nx-message__text", children: message }) : null,
88
+ "aria-describedby": describedBy(message, details, id),
89
+ content: message || details ? messageBody(message, details, id) : null,
46
90
  footer: handle => buttons.map((button, index) => (_jsx(Button, { variant: button.variant ?? (index === buttons.length - 1 ? (tone === "danger" ? "danger" : "primary") : "secondary"), "data-nx-focus": index === focusIndex || undefined, onClick: () => {
47
91
  result = button.value;
48
92
  handle.close();
49
93
  }, children: button.label }, button.value))),
50
94
  });
95
+ closeOnAbort(popup, signal);
51
96
  return popup.closed.then(() => result);
52
97
  }
53
98
  function options(input) {
@@ -72,7 +117,9 @@ function confirm(input) {
72
117
  }
73
118
  /** A text box with Cancel and OK. Resolves with the text, or `null` when cancelled. */
74
119
  function prompt(input) {
75
- const { title, message, tone = "neutral", icon, size = "sm", closable = true, label, defaultValue, placeholder, required = true, inputType = "text", confirmLabel, cancelLabel } = options(input);
120
+ const { title, message, details, tone = "neutral", icon, size = "sm", closable = true, closeOnBackdrop = closable, signal, label, defaultValue, placeholder, required = true, inputType = "text", confirmLabel, cancelLabel } = options(input);
121
+ if (signal?.aborted)
122
+ return Promise.resolve(null);
76
123
  const formId = `nx-prompt-${++promptCounter}`;
77
124
  let result = null;
78
125
  const popup = openPopup({
@@ -80,20 +127,22 @@ function prompt(input) {
80
127
  icon: toneIcon(tone, icon),
81
128
  size,
82
129
  closable,
83
- closeOnBackdrop: closable,
130
+ closeOnBackdrop,
84
131
  closeOnEscape: closable,
85
132
  variant: "dialog",
86
133
  minimizable: false,
87
134
  maximizable: false,
88
135
  className: cx("nx-popup--message", `nx-popup--tone-${tone}`),
89
136
  initialFocus: `#${formId} input`,
137
+ "aria-describedby": describedBy(message, details, formId),
90
138
  content: handle => (_jsxs("form", { id: formId, className: "nx-message__form", onSubmit: (event) => {
91
139
  event.preventDefault();
92
140
  result = String(new FormData(event.currentTarget).get("value") ?? "");
93
141
  handle.close();
94
- }, children: [message && _jsx("div", { className: "nx-message__text", children: message }), _jsx(TextBox, { name: "value", label: label, type: inputType, defaultValue: defaultValue, placeholder: placeholder, required: required })] })),
142
+ }, children: [messageBody(message, details, formId), _jsx(TextBox, { name: "value", label: label, type: inputType, defaultValue: defaultValue, placeholder: placeholder, required: required })] })),
95
143
  footer: handle => (_jsxs(_Fragment, { children: [_jsx(Button, { onClick: () => handle.close(), children: cancelLabel ?? "Cancel" }), _jsx(Button, { type: "submit", form: formId, variant: "primary", children: confirmLabel ?? "OK" })] })),
96
144
  });
145
+ closeOnAbort(popup, signal);
97
146
  return popup.closed.then(() => result);
98
147
  }
99
148
  /**
@@ -50,8 +50,9 @@ export interface PopupProps {
50
50
  closeOnBackdrop?: boolean;
51
51
  /**
52
52
  * The minimize button: the popup steps aside to a bar at the bottom right, keeping everything typed in it, and the
53
- * page can be used again. Default: when `closable`, since a popup that must be answered cannot be put aside.
54
- * Needs `<PopupHost />`, which shows the bars.
53
+ * page can be used again. Default false: only a popup that asks has it. A CRUD page's Add and Edit forms and its row
54
+ * forms ask (a form put aside while the list behind it is read); message boxes, viewers, galleries, histories and
55
+ * the rest do not. Needs `<PopupHost />`, which shows the bars.
55
56
  */
56
57
  minimizable?: boolean;
57
58
  /** The maximize button, and a double-click on the title bar: the popup fills the window. Default true. */
@@ -76,17 +77,23 @@ export interface PopupProps {
76
77
  initialFocus?: string;
77
78
  /** Name for screen readers when there is no title. */
78
79
  "aria-label"?: string;
80
+ /**
81
+ * Ids of elements inside the popup that describe it to screen readers, after its `description`: a message box names
82
+ * its message and its details, so they are read with the title as the focus enters.
83
+ */
84
+ "aria-describedby"?: string;
79
85
  className?: string;
80
86
  children?: ReactNode;
81
87
  }
82
88
  /**
83
- * A modal window on the native <dialog>, with the buttons of a desktop window: minimize, maximize, and close. It sits
84
- * in the top layer above everything, dims and locks the page behind it, traps Tab, closes with Esc or a click outside,
85
- * and returns focus to where it came from. The title bar moves it; a double-click there maximizes it. Minimized, it
89
+ * A modal window on the native <dialog>, with the buttons of a desktop window: maximize and close, and minimize when
90
+ * it asks (`minimizable`: a CRUD page's Add and Edit). It sits in the top layer above everything, dims and locks the
91
+ * page behind it, traps Tab, closes with Esc or a click outside, and returns focus to where it came from. The title
92
+ * bar moves it; a double-click there maximizes it. Minimized, it
86
93
  * waits as a bar at the bottom right with its content kept, while the page can be used. The body scrolls while the
87
94
  * title and the footer stay. The content mounts while the popup is open, so a form inside starts fresh every time.
88
95
  *
89
96
  * @example
90
97
  * <Popup open={open} onClose={() => setOpen(false)} title="Edit booking" footer={<Button variant="primary">Save</Button>}>…</Popup>
91
98
  */
92
- export declare function Popup({ open, onClose, onClosed, title, description, icon, variant, size, footer, closable, closeOnEscape, closeOnBackdrop, minimizable, maximizable, movable, reopen, windowState: windowStateProp, defaultWindowState, onWindowStateChange, flush, initialFocus, "aria-label": ariaLabel, className, children, }: PopupProps): import("react").JSX.Element;
99
+ export declare function Popup({ open, onClose, onClosed, title, description, icon, variant, size, footer, closable, closeOnEscape, closeOnBackdrop, minimizable, maximizable, movable, reopen, windowState: windowStateProp, defaultWindowState, onWindowStateChange, flush, initialFocus, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, className, children, }: PopupProps): import("react").JSX.Element;
@@ -17,17 +17,20 @@ function popupPosition(dialog) {
17
17
  }
18
18
  // Each popup's opener and the popup that held it, so focus can find its way back through popups that have gone.
19
19
  const openers = new WeakMap();
20
+ // How each popup puts the focus inside itself (its `initialFocus`), for a popup closed over it that left the focus nowhere.
21
+ const homes = new WeakMap();
20
22
  /**
21
- * A modal window on the native <dialog>, with the buttons of a desktop window: minimize, maximize, and close. It sits
22
- * in the top layer above everything, dims and locks the page behind it, traps Tab, closes with Esc or a click outside,
23
- * and returns focus to where it came from. The title bar moves it; a double-click there maximizes it. Minimized, it
23
+ * A modal window on the native <dialog>, with the buttons of a desktop window: maximize and close, and minimize when
24
+ * it asks (`minimizable`: a CRUD page's Add and Edit). It sits in the top layer above everything, dims and locks the
25
+ * page behind it, traps Tab, closes with Esc or a click outside, and returns focus to where it came from. The title
26
+ * bar moves it; a double-click there maximizes it. Minimized, it
24
27
  * waits as a bar at the bottom right with its content kept, while the page can be used. The body scrolls while the
25
28
  * title and the footer stay. The content mounts while the popup is open, so a form inside starts fresh every time.
26
29
  *
27
30
  * @example
28
31
  * <Popup open={open} onClose={() => setOpen(false)} title="Edit booking" footer={<Button variant="primary">Save</Button>}>…</Popup>
29
32
  */
30
- export function Popup({ open, onClose, onClosed, title, description, icon, variant = "window", size = "md", footer, closable = true, closeOnEscape = true, closeOnBackdrop = true, minimizable, maximizable = true, movable = true, reopen = "replace", windowState: windowStateProp, defaultWindowState = "normal", onWindowStateChange, flush = false, initialFocus, "aria-label": ariaLabel, className, children, }) {
33
+ export function Popup({ open, onClose, onClosed, title, description, icon, variant = "window", size = "md", footer, closable = true, closeOnEscape = true, closeOnBackdrop = true, minimizable, maximizable = true, movable = true, reopen = "replace", windowState: windowStateProp, defaultWindowState = "normal", onWindowStateChange, flush = false, initialFocus, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, className, children, }) {
31
34
  const id = useId();
32
35
  const dialogRef = useRef(null);
33
36
  // Content stays mounted through the exit animation, and while minimized.
@@ -41,7 +44,8 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
41
44
  const maximized = windowState === "maximized";
42
45
  const dockReady = useDockReady();
43
46
  const hosted = useContext(HostedPopupContext);
44
- const canMinimize = (minimizable ?? closable) && dockReady;
47
+ // Only a popup that asks: a CRUD page's Add and Edit, and its row forms.
48
+ const canMinimize = minimizable === true && dockReady;
45
49
  const downOnBackdropRef = useRef(false);
46
50
  // The element that opened the popup, where focus goes back when the browser has nowhere better.
47
51
  const openerRef = useRef(null);
@@ -65,19 +69,22 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
65
69
  onWindowStateChange?.(next);
66
70
  }
67
71
  // The latest props and callbacks, for the dock bar and the drag, which outlive a render.
68
- const latest = useRef({ windowState, defaultWindowState, reopen, title, icon, closable, ariaLabel, onClose, changeWindowState });
72
+ const latest = useRef({ windowState, defaultWindowState, reopen, title, description, icon, closable, ariaLabel, onClose, changeWindowState });
69
73
  useLayoutEffect(() => {
70
- latest.current = { windowState, defaultWindowState, reopen, title, icon, closable, ariaLabel, onClose, changeWindowState };
74
+ latest.current = { windowState, defaultWindowState, reopen, title, description, icon, closable, ariaLabel, onClose, changeWindowState };
71
75
  if (!minimized)
72
76
  restoreToRef.current = windowState;
73
77
  });
74
78
  function dock() {
75
79
  const current = latest.current;
76
80
  dockedRef.current = true;
81
+ const label = typeof current.title === "string" ? current.title : current.ariaLabel;
77
82
  dockWindow({
78
83
  id: dockId,
79
84
  title: current.title ?? current.ariaLabel ?? "Window",
80
- label: typeof current.title === "string" ? current.title : current.ariaLabel,
85
+ // Under the title, so two bars of one kind (two Edit forms, two Adds) read apart: the record, the place.
86
+ description: current.description,
87
+ label: label && typeof current.description === "string" ? `${label} · ${current.description}` : label,
81
88
  icon: current.icon,
82
89
  restore: () => latest.current.changeWindowState(restoreToRef.current),
83
90
  close: current.closable ? () => latest.current.onClose("close") : undefined,
@@ -102,11 +109,26 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
102
109
  link = openers.get(link.within);
103
110
  if (link?.opener?.isConnected)
104
111
  link.opener.focus({ preventScroll: true });
112
+ // An opener that cannot take the focus - hidden in a menu that closed, disabled while its action runs - or none at
113
+ // all never leaves it on the page while another popup is still open: the popup now on top takes it, where it puts
114
+ // it on opening (a record's workspace: its chosen tab).
115
+ const now = document.activeElement;
116
+ if (now && now !== document.body && !dialog.contains(now))
117
+ return;
118
+ const below = Array.from(document.querySelectorAll("dialog.nx-popup")).filter(other => other !== dialog && other.open);
119
+ const top = below[below.length - 1];
120
+ if (top)
121
+ (homes.get(top) ?? (() => top.focus()))();
105
122
  }
106
123
  function focusInside(dialog) {
107
124
  const target = initialFocus ? dialog.querySelector(initialFocus) : dialog.querySelector(`:is(.nx-popup__body, .nx-popup__foot) :is(${FOCUSABLE})`);
108
125
  (target ?? dialog).focus();
109
126
  }
127
+ useLayoutEffect(() => {
128
+ const dialog = dialogRef.current;
129
+ if (dialog)
130
+ homes.set(dialog, () => focusInside(dialog));
131
+ });
110
132
  function showDialog(dialog) {
111
133
  // The first modal locks the page; the space of its scrollbar is kept as padding, so nothing shifts.
112
134
  if (!document.querySelector(".nx-popup:modal"))
@@ -317,6 +339,12 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
317
339
  function onPointerDown(event) {
318
340
  downOnBackdropRef.current = event.target === event.currentTarget;
319
341
  }
342
+ // A press outside a popup that stays open through it (`closeOnBackdrop: false`) leaves the focus where it was, as
343
+ // the press does nothing: a question's Confirm keeps it, so Enter still answers, and a field keeps its caret.
344
+ function onMouseDown(event) {
345
+ if (event.target === event.currentTarget && !closeOnBackdrop)
346
+ event.preventDefault();
347
+ }
320
348
  function onClick(event) {
321
349
  const onBackdrop = event.target === event.currentTarget && downOnBackdropRef.current;
322
350
  downOnBackdropRef.current = false;
@@ -369,5 +397,5 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
369
397
  const titleId = `${id}-title`;
370
398
  const descriptionId = `${id}-description`;
371
399
  const hasControls = canMinimize || maximizable || closable;
372
- return (_jsx("dialog", { ref: dialogRef, className: cx("nx-popup", `nx-popup--${variant}`, `nx-popup--${size}`, flush && "nx-popup--flush", maximized && "nx-popup--maximized", movable && "nx-popup--movable", closing && "nx-popup--closing", className), "data-window-state": windowState, "aria-labelledby": title ? titleId : undefined, "aria-label": title ? undefined : ariaLabel, "aria-describedby": description ? descriptionId : undefined, onCancel: onCancel, onClose: onNativeClose, onPointerDown: onPointerDown, onClick: onClick, children: shown && (_jsxs(HostedPopupContext, { value: false, children: [(title || icon || hasControls) && (_jsxs("header", { className: "nx-popup__head", onPointerDown: onHeadPointerDown, onDoubleClick: onHeadDoubleClick, children: [icon && _jsx("span", { className: "nx-popup__icon", children: icon }), _jsxs("div", { className: "nx-popup__heading", children: [title && (_jsx("h2", { id: titleId, className: "nx-popup__title", children: title })), description && (_jsx("p", { id: descriptionId, className: "nx-popup__description", children: description }))] }), hasControls && (_jsxs("div", { className: "nx-popup__controls", children: [canMinimize && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__minimize", "aria-label": "Minimize", title: "Minimize", onMouseDown: keepFocus, onClick: () => changeWindowState("minimized"), children: _jsx(InlineIcon, { icon: iconMinus }) })), maximizable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__maximize", "aria-label": maximized ? "Restore down" : "Maximize", title: maximized ? "Restore down" : "Maximize", onMouseDown: keepFocus, onClick: toggleMaximize, children: _jsx(InlineIcon, { icon: maximized ? iconSquares : iconSquare }) })), closable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__close", "aria-label": "Close", title: "Close", onClick: () => onClose("close"), children: _jsx(InlineIcon, { icon: iconX }) }))] }))] })), _jsx("div", { className: "nx-popup__body", children: children }), footer && _jsx("footer", { className: "nx-popup__foot", children: footer })] }, round)) }));
400
+ return (_jsx("dialog", { ref: dialogRef, className: cx("nx-popup", `nx-popup--${variant}`, `nx-popup--${size}`, flush && "nx-popup--flush", maximized && "nx-popup--maximized", movable && "nx-popup--movable", closing && "nx-popup--closing", className), "data-window-state": windowState, "aria-labelledby": title ? titleId : undefined, "aria-label": title ? undefined : ariaLabel, "aria-describedby": cx(description ? descriptionId : undefined, ariaDescribedBy) || undefined, onCancel: onCancel, onClose: onNativeClose, onPointerDown: onPointerDown, onMouseDown: onMouseDown, onClick: onClick, children: shown && (_jsxs(HostedPopupContext, { value: false, children: [(title || icon || hasControls) && (_jsxs("header", { className: "nx-popup__head", onPointerDown: onHeadPointerDown, onDoubleClick: onHeadDoubleClick, children: [icon && _jsx("span", { className: "nx-popup__icon", children: icon }), _jsxs("div", { className: "nx-popup__heading", children: [title && (_jsx("h2", { id: titleId, className: "nx-popup__title", children: title })), description && (_jsx("p", { id: descriptionId, className: "nx-popup__description", children: description }))] }), hasControls && (_jsxs("div", { className: "nx-popup__controls", children: [canMinimize && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__minimize", "aria-label": "Minimize", title: "Minimize", onMouseDown: keepFocus, onClick: () => changeWindowState("minimized"), children: _jsx(InlineIcon, { icon: iconMinus }) })), maximizable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__maximize", "aria-label": maximized ? "Restore down" : "Maximize", title: maximized ? "Restore down" : "Maximize", onMouseDown: keepFocus, onClick: toggleMaximize, children: _jsx(InlineIcon, { icon: maximized ? iconSquares : iconSquare }) })), closable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__close", "aria-label": "Close", title: "Close", onClick: () => onClose("close"), children: _jsx(InlineIcon, { icon: iconX }) }))] }))] })), _jsx("div", { className: "nx-popup__body", children: children }), footer && _jsx("footer", { className: "nx-popup__foot", children: footer })] }, round)) }));
373
401
  }
@@ -3,7 +3,9 @@ import { type ReactNode } from "react";
3
3
  export interface DockedWindow {
4
4
  id: string;
5
5
  title: ReactNode;
6
- /** Plain text of the title, for the tooltip. */
6
+ /** A second line under the title: the record or the place a window is about, so two of one kind read apart. */
7
+ description?: ReactNode;
8
+ /** Plain text of the title, and of the description, for the tooltip. */
7
9
  label?: string;
8
10
  /** The window's icon; a window icon when left out. */
9
11
  icon?: ReactNode;
@@ -113,9 +113,10 @@ export function PopupDock() {
113
113
  function DockBar({ item, onRestore }) {
114
114
  const id = useId();
115
115
  const titleId = `${id}-title`;
116
+ const descriptionId = `${id}-description`;
116
117
  const closeId = `${id}-close`;
117
118
  return (_jsxs("li", { className: "nx-dock__item", children: [_jsxs("button", { type: "button", className: "nx-dock__window", title: item.label ? `Restore: ${item.label}` : "Restore", onClick: () => {
118
119
  onRestore();
119
120
  item.restore();
120
- }, children: [_jsx("span", { className: "nx-dock__icon", "aria-hidden": "true", children: item.icon ?? _jsx(InlineIcon, { icon: iconAppWindow }) }), _jsx("span", { className: "nx-visually-hidden", children: "Restore " }), _jsx("span", { id: titleId, className: "nx-dock__title", children: item.title }), _jsx(InlineIcon, { className: "nx-dock__restore", icon: iconChevronUp })] }), item.close && (_jsxs("button", { type: "button", className: "nx-dock__close", "aria-labelledby": `${closeId} ${titleId}`, title: "Close", onClick: item.close, children: [_jsx("span", { id: closeId, hidden: true, children: "Close" }), _jsx(InlineIcon, { icon: iconX })] }))] }));
121
+ }, children: [_jsx("span", { className: "nx-dock__icon", "aria-hidden": "true", children: item.icon ?? _jsx(InlineIcon, { icon: iconAppWindow }) }), _jsx("span", { className: "nx-visually-hidden", children: "Restore " }), _jsxs("span", { className: "nx-dock__text", children: [_jsx("span", { id: titleId, className: "nx-dock__title", children: item.title }), item.description ? (_jsx("span", { id: descriptionId, className: "nx-dock__description", children: item.description })) : null] }), _jsx(InlineIcon, { className: "nx-dock__restore", icon: iconChevronUp })] }), item.close && (_jsxs("button", { type: "button", className: "nx-dock__close", "aria-labelledby": item.description ? `${closeId} ${titleId} ${descriptionId}` : `${closeId} ${titleId}`, title: "Close", onClick: item.close, children: [_jsx("span", { id: closeId, hidden: true, children: "Close" }), _jsx(InlineIcon, { icon: iconX })] }))] }));
121
122
  }
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useLayoutEffect, useSyncExternalStore } from "react";
4
4
  import { Popup } from "./Popup.js";
5
5
  import { HostedPopupContext } from "./PopupContext.js";
6
+ import { PageLeaveGuardContext } from "../Forms/FormLeaveGuard.js";
6
7
  import { PopupDock } from "./PopupDock.js";
7
8
  import { withPopupRule } from "./PopupRules.js";
8
9
  const EMPTY = [];
@@ -156,8 +157,8 @@ export function PopupHost() {
156
157
  for (const entry of list)
157
158
  rendered.add(entry.id);
158
159
  });
159
- return (_jsxs(HostedPopupContext, { value: true, children: [list.map(entry => {
160
- const { content, footer, onClose: _onClose, beforeClose: _beforeClose, onWindowStateChange: _onWindowStateChange, key: _key, ...rest } = entry.options;
161
- return (_jsx(Popup, { ...rest, open: entry.open, windowState: entry.windowState, onWindowStateChange: state => setWindowState(entry.id, state), footer: typeof footer === "function" ? footer(entry.handle) : footer, onClose: reason => requestClose(entry.id, reason), onClosed: () => removeEntry(entry.id), children: typeof content === "function" ? content(entry.handle) : content }, entry.id));
162
- }), _jsx(PopupDock, {})] }));
160
+ return (_jsx(PageLeaveGuardContext, { value: null, children: _jsxs(HostedPopupContext, { value: true, children: [list.map(entry => {
161
+ const { content, footer, onClose: _onClose, beforeClose: _beforeClose, onWindowStateChange: _onWindowStateChange, key: _key, ...rest } = entry.options;
162
+ return (_jsx(Popup, { ...rest, open: entry.open, windowState: entry.windowState, onWindowStateChange: state => setWindowState(entry.id, state), footer: typeof footer === "function" ? footer(entry.handle) : footer, onClose: reason => requestClose(entry.id, reason), onClosed: () => removeEntry(entry.id), children: typeof content === "function" ? content(entry.handle) : content }, entry.id));
163
+ }), _jsx(PopupDock, {})] }) }));
163
164
  }
@@ -13,8 +13,9 @@ export interface ThemePickerProps {
13
13
  className?: string;
14
14
  }
15
15
  /**
16
- * Light / Dark / System, a card per theme with a live preview of its colors, the density, the animations and, in a
17
- * development build, the scale grid. Each choice applies at once and is saved in the browser.
16
+ * Two sections: the theme (Light / Dark / System and a card per theme with a live preview of its colors) and the
17
+ * appearance (the density, the animations and, in a development build, the scale grid). Each choice applies at once
18
+ * and is saved in the browser.
18
19
  */
19
20
  export declare function ThemePicker({ themes, showMode, showDensity, showAnimations, showGrid, className }: ThemePickerProps): import("react").JSX.Element;
20
21
  export interface PreferencesPickerProps {
@@ -15,6 +15,7 @@ import { useTheme } from "../../Services/ThemeService.js";
15
15
  import { TabButtons } from "../Documents/TabButtons.js";
16
16
  import { Switch } from "../Inputs/CheckBox.js";
17
17
  import { Dropdown } from "../Inputs/Dropdown.js";
18
+ import { TextBox } from "../Inputs/TextBox.js";
18
19
  const MODES = [
19
20
  { value: "light", label: "Light", icon: _jsx(InlineIcon, { icon: iconSun }) },
20
21
  { value: "dark", label: "Dark", icon: _jsx(InlineIcon, { icon: iconMoon }) },
@@ -38,14 +39,20 @@ const GRIDS = [
38
39
  function Choice({ id, legend, options, value, onChange, children }) {
39
40
  return (_jsxs("div", { className: "nx-theme-picker__group", children: [_jsx("p", { id: id, className: "nx-theme-picker__legend", children: legend }), children ?? (options && value !== undefined && onChange && _jsx(TabButtons, { "aria-labelledby": id, name: id, options: options, value: value, onChange: onChange, block: true }))] }));
40
41
  }
42
+ /** A group of settings under its name: one section of the panel, a line above it. */
43
+ function Section({ title, wide, children }) {
44
+ const id = useId();
45
+ return (_jsxs("section", { className: cx("nx-theme-picker__block", wide && "nx-theme-picker__block--wide"), "aria-labelledby": id, children: [_jsx("h3", { id: id, className: "nx-theme-picker__section", children: title }), children] }));
46
+ }
41
47
  /**
42
- * Light / Dark / System, a card per theme with a live preview of its colors, the density, the animations and, in a
43
- * development build, the scale grid. Each choice applies at once and is saved in the browser.
48
+ * Two sections: the theme (Light / Dark / System and a card per theme with a live preview of its colors) and the
49
+ * appearance (the density, the animations and, in a development build, the scale grid). Each choice applies at once
50
+ * and is saved in the browser.
44
51
  */
45
52
  export function ThemePicker({ themes = NEXUS_THEMES, showMode = true, showDensity = true, showAnimations = true, showGrid = isDevelopmentBuild(), className }) {
46
53
  const { theme, mode, density, animations, grid, setTheme, setMode, setDensity, setAnimations, setGrid } = useTheme();
47
54
  const name = useId();
48
- return (_jsxs("div", { className: cx("nx-theme-picker", className), children: [showMode && _jsx(Choice, { id: `${name}-mode`, legend: "Mode", options: MODES, value: mode, onChange: setMode }), _jsxs("fieldset", { className: "nx-theme-picker__group", children: [_jsx("legend", { className: "nx-theme-picker__legend", children: "Theme" }), _jsx("div", { className: "nx-theme-picker__grid", children: themes.map(option => (_jsxs("label", { className: "nx-theme-option", title: option.description, children: [_jsx("input", { type: "radio", className: "nx-visually-hidden", name: `${name}-theme`, value: option.id, checked: theme === option.id, onChange: () => setTheme(option.id) }), _jsx("span", { className: "nx-theme-option__preview", "data-nx-theme": option.id, "aria-hidden": "true", children: _jsxs("span", { className: "nx-theme-option__window", children: [_jsx("span", { className: "nx-theme-option__line" }), _jsx("span", { className: "nx-theme-option__line nx-theme-option__line--muted" }), _jsxs("span", { className: "nx-theme-option__swatches", children: [_jsx("span", { className: "nx-theme-option__pill" }), _jsx("span", { className: "nx-theme-option__dot nx-theme-option__dot--success" }), _jsx("span", { className: "nx-theme-option__dot nx-theme-option__dot--warning" }), _jsx("span", { className: "nx-theme-option__dot nx-theme-option__dot--danger" })] })] }) }), _jsxs("span", { className: "nx-theme-option__name", children: [option.name, _jsx(InlineIcon, { icon: iconCheck, className: "nx-theme-option__check" })] })] }, option.id))) })] }), showDensity && _jsx(Choice, { id: `${name}-density`, legend: "Density", options: DENSITIES, value: density, onChange: setDensity }), showAnimations && _jsx(Choice, { id: `${name}-animations`, legend: "Animations", options: ANIMATIONS, value: animations, onChange: setAnimations }), showGrid && _jsx(Choice, { id: `${name}-grid`, legend: "Scale grid", options: GRIDS, value: grid, onChange: setGrid })] }));
55
+ return (_jsxs("div", { className: cx("nx-theme-picker", className), children: [_jsxs(Section, { title: "Theme", wide: true, children: [showMode && _jsx(Choice, { id: `${name}-mode`, legend: "Mode", options: MODES, value: mode, onChange: setMode }), _jsxs("fieldset", { className: "nx-theme-picker__group", children: [_jsx("legend", { className: "nx-theme-picker__legend", children: "Colors" }), _jsx("div", { className: "nx-theme-picker__grid", children: themes.map(option => (_jsxs("label", { className: "nx-theme-option", title: option.description, children: [_jsx("input", { type: "radio", className: "nx-visually-hidden", name: `${name}-theme`, value: option.id, checked: theme === option.id, onChange: () => setTheme(option.id) }), _jsx("span", { className: "nx-theme-option__preview", "data-nx-theme": option.id, "aria-hidden": "true", children: _jsxs("span", { className: "nx-theme-option__window", children: [_jsx("span", { className: "nx-theme-option__line" }), _jsx("span", { className: "nx-theme-option__line nx-theme-option__line--muted" }), _jsxs("span", { className: "nx-theme-option__swatches", children: [_jsx("span", { className: "nx-theme-option__pill" }), _jsx("span", { className: "nx-theme-option__dot nx-theme-option__dot--success" }), _jsx("span", { className: "nx-theme-option__dot nx-theme-option__dot--warning" }), _jsx("span", { className: "nx-theme-option__dot nx-theme-option__dot--danger" })] })] }) }), _jsxs("span", { className: "nx-theme-option__name", children: [option.name, _jsx(InlineIcon, { icon: iconCheck, className: "nx-theme-option__check" })] })] }, option.id))) })] })] }), (showDensity || showAnimations || showGrid) && (_jsxs(Section, { title: "Appearance", children: [showDensity && _jsx(Choice, { id: `${name}-density`, legend: "Density", options: DENSITIES, value: density, onChange: setDensity }), showAnimations && _jsx(Choice, { id: `${name}-animations`, legend: "Animations", options: ANIMATIONS, value: animations, onChange: setAnimations }), showGrid && _jsx(Choice, { id: `${name}-grid`, legend: "Scale grid", options: GRIDS, value: grid, onChange: setGrid })] }))] }));
49
56
  }
50
57
  /* The quick preferences: language, dates, numbers and tables, beside the theme. */
51
58
  const LANGUAGES = [
@@ -75,6 +82,11 @@ const SEPARATORS = [
75
82
  { value: "none", label: "1200", title: "None" },
76
83
  ];
77
84
  const PAGE_SIZES = DEFAULT_PAGE_SIZES.map(size => ({ value: size, label: `${size} rows` }));
85
+ const DESIGNS = [
86
+ { value: "standard", label: "Standard", title: "The label above the box" },
87
+ { value: "outlined", label: "Outlined", title: "The label on the box's edge" },
88
+ { value: "inbox", label: "Inbox", title: "The label inside the top of the box" },
89
+ ];
78
90
  // A fixed day, so the server and the browser write the same examples, and one past the 12th, so day and month differ.
79
91
  const SAMPLE_DATE = "2026-09-27";
80
92
  /**
@@ -89,5 +101,5 @@ export function PreferencesPicker({ className }) {
89
101
  const preferences = useUserPreferences();
90
102
  const name = useId();
91
103
  const formats = DATE_FORMATS.map(format => ({ value: format, label: formatDate(SAMPLE_DATE, { ...dates, format }), description: format }));
92
- return (_jsxs("div", { className: cx("nx-theme-picker", className), children: [_jsx("h3", { className: "nx-theme-picker__section", children: "Language and region" }), _jsx(Choice, { id: `${name}-language`, legend: "Interface language", options: LANGUAGES, value: preferences.language, onChange: language => setUserPreferences({ language }) }), _jsx(Choice, { id: `${name}-calendar`, legend: "Calendar", options: CALENDARS, value: dates.calendar, onChange: calendar => setDatePreferences({ calendar }) }), _jsx("h3", { className: "nx-theme-picker__section", children: "Dates and times" }), _jsx(Choice, { id: `${name}-script`, legend: "Date picker language", options: SCRIPTS, value: dates.script, onChange: script => setDatePreferences({ script }) }), _jsx(Choice, { id: `${name}-clock`, legend: "Clock", options: CLOCKS, value: /H/.test(dates.timeFormat) ? "24" : "12", onChange: clock => setDatePreferences({ timeFormat: clock === "24" ? "HH:mm" : DEFAULT_TIME_FORMAT }) }), _jsx(Choice, { id: `${name}-format`, legend: "Display date format", children: _jsx(Dropdown, { "aria-labelledby": `${name}-format`, options: formats, value: dates.format, clearable: false, searchable: false, onValueChange: format => format && setDatePreferences({ format }) }) }), _jsx("h3", { className: "nx-theme-picker__section", children: "Numbers" }), _jsx(Choice, { id: `${name}-scale`, legend: "Number scale", options: SCALES, value: String(preferences.numberScale), onChange: scale => setUserPreferences({ numberScale: Number(scale) === ENumberScales.SouthAsian ? ENumberScales.SouthAsian : ENumberScales.International }) }), _jsx(Choice, { id: `${name}-thousands`, legend: "Thousands separator", options: SEPARATORS, value: preferences.thousands, onChange: thousands => setUserPreferences({ thousands }) }), _jsx("h3", { className: "nx-theme-picker__section", children: "Tables" }), _jsx(Choice, { id: `${name}-page-size`, legend: "Default page size", children: _jsx(Dropdown, { "aria-labelledby": `${name}-page-size`, options: PAGE_SIZES, value: preferences.pageSize ?? DEFAULT_PAGE_SIZE, clearable: false, searchable: false, onValueChange: pageSize => pageSize && setUserPreferences({ pageSize }) }) }), _jsx(Switch, { label: "A row click opens the record", labelPosition: "start", checked: preferences.rowClick ?? false, onValueChange: rowClick => setUserPreferences({ rowClick }) })] }));
104
+ return (_jsxs("div", { className: cx("nx-theme-picker", className), children: [_jsxs(Section, { title: "Language and region", children: [_jsx(Choice, { id: `${name}-language`, legend: "Interface language", options: LANGUAGES, value: preferences.language, onChange: language => setUserPreferences({ language }) }), _jsx(Choice, { id: `${name}-calendar`, legend: "Calendar", options: CALENDARS, value: dates.calendar, onChange: calendar => setDatePreferences({ calendar }) })] }), _jsxs(Section, { title: "Dates and times", children: [_jsx(Choice, { id: `${name}-script`, legend: "Date picker language", options: SCRIPTS, value: dates.script, onChange: script => setDatePreferences({ script }) }), _jsx(Choice, { id: `${name}-clock`, legend: "Clock", options: CLOCKS, value: /H/.test(dates.timeFormat) ? "24" : "12", onChange: clock => setDatePreferences({ timeFormat: clock === "24" ? "HH:mm" : DEFAULT_TIME_FORMAT }) }), _jsx(Choice, { id: `${name}-format`, legend: "Display date format", children: _jsx(Dropdown, { "aria-labelledby": `${name}-format`, options: formats, value: dates.format, clearable: false, searchable: false, onValueChange: format => format && setDatePreferences({ format }) }) })] }), _jsxs(Section, { title: "Numbers", children: [_jsx(Choice, { id: `${name}-scale`, legend: "Number scale", options: SCALES, value: String(preferences.numberScale), onChange: scale => setUserPreferences({ numberScale: Number(scale) === ENumberScales.SouthAsian ? ENumberScales.SouthAsian : ENumberScales.International }) }), _jsx(Choice, { id: `${name}-thousands`, legend: "Thousands separator", options: SEPARATORS, value: preferences.thousands, onChange: thousands => setUserPreferences({ thousands }) })] }), _jsxs(Section, { title: "Tables", children: [_jsx(Choice, { id: `${name}-page-size`, legend: "Default page size", children: _jsx(Dropdown, { "aria-labelledby": `${name}-page-size`, options: PAGE_SIZES, value: preferences.pageSize ?? DEFAULT_PAGE_SIZE, clearable: false, searchable: false, onValueChange: pageSize => pageSize && setUserPreferences({ pageSize }) }) }), _jsx(Switch, { label: "A row click opens the record", labelPosition: "start", checked: preferences.rowClick ?? false, onValueChange: rowClick => setUserPreferences({ rowClick }) })] }), _jsxs(Section, { title: "Forms", children: [_jsx(Choice, { id: `${name}-design`, legend: "Input design", options: DESIGNS, value: preferences.inputDesign ?? "standard", onChange: inputDesign => setUserPreferences({ inputDesign }) }), _jsx(TextBox, { label: "Example", placeholder: "Type to try it" })] })] }));
93
105
  }