nexus-shared 3.2.0 → 4.1.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 (271) hide show
  1. package/CHANGELOG.md +2359 -0
  2. package/README.md +1911 -1123
  3. package/dist/Client.Index.d.ts +27 -0
  4. package/dist/Client.Index.js +27 -0
  5. package/dist/Components/Chats/ChatComposer.d.ts +7 -1
  6. package/dist/Components/Chats/ChatComposer.js +9 -6
  7. package/dist/Components/Chats/ChatParts.d.ts +15 -2
  8. package/dist/Components/Chats/ChatParts.js +15 -4
  9. package/dist/Components/Chats/ChatPopup.d.ts +46 -0
  10. package/dist/Components/Chats/ChatPopup.js +34 -0
  11. package/dist/Components/Chats/ChatThread.d.ts +9 -1
  12. package/dist/Components/Chats/ChatThread.js +54 -6
  13. package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
  14. package/dist/Components/Dashboards/DashboardBoard.js +594 -0
  15. package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
  16. package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
  17. package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
  18. package/dist/Components/Dashboards/WidgetFrame.js +45 -0
  19. package/dist/Components/Documents/Button.d.ts +6 -1
  20. package/dist/Components/Documents/Button.js +2 -2
  21. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  22. package/dist/Components/Documents/DocumentFrame.js +83 -0
  23. package/dist/Components/Documents/Menu.d.ts +4 -0
  24. package/dist/Components/Documents/Menu.js +40 -24
  25. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  26. package/dist/Components/Documents/PrintDocument.js +62 -0
  27. package/dist/Components/Documents/TabButtons.d.ts +6 -0
  28. package/dist/Components/Documents/TabButtons.js +5 -4
  29. package/dist/Components/Documents/Tabs.d.ts +59 -0
  30. package/dist/Components/Documents/Tabs.js +70 -0
  31. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  32. package/dist/Components/Domains/DomainCard.js +182 -0
  33. package/dist/Components/Forms/ApiForm.js +24 -3
  34. package/dist/Components/Forms/Crud.d.ts +2 -2
  35. package/dist/Components/Forms/Crud.js +628 -247
  36. package/dist/Components/Forms/CrudActions.d.ts +137 -0
  37. package/dist/Components/Forms/CrudActions.js +214 -0
  38. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  39. package/dist/Components/Forms/CrudHistory.js +14 -6
  40. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  41. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  42. package/dist/Components/Forms/CrudParts.d.ts +37 -2
  43. package/dist/Components/Forms/CrudParts.js +123 -7
  44. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  45. package/dist/Components/Forms/CrudRowForms.js +174 -0
  46. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  47. package/dist/Components/Forms/CrudScopes.js +76 -0
  48. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  49. package/dist/Components/Forms/CrudTree.js +363 -0
  50. package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
  51. package/dist/Components/Forms/CrudWorkspace.js +177 -0
  52. package/dist/Components/Forms/ExcelImport.js +3 -3
  53. package/dist/Components/Forms/Form.d.ts +6 -5
  54. package/dist/Components/Forms/Form.js +134 -21
  55. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  56. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  57. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  58. package/dist/Components/Forms/FormQuestion.js +28 -0
  59. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  60. package/dist/Components/Forms/RecordFlows.js +282 -0
  61. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  62. package/dist/Components/Forms/SubmitForm.js +182 -10
  63. package/dist/Components/Inputs/CheckBox.js +1 -1
  64. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  65. package/dist/Components/Inputs/ChoiceField.js +28 -2
  66. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  67. package/dist/Components/Inputs/ComputedInput.js +179 -0
  68. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  69. package/dist/Components/Inputs/Dropdown.js +8 -6
  70. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  71. package/dist/Components/Inputs/DropdownParts.js +16 -4
  72. package/dist/Components/Inputs/Field.d.ts +7 -1
  73. package/dist/Components/Inputs/Field.js +2 -2
  74. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  75. package/dist/Components/Inputs/GroupForm.js +12 -12
  76. package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
  77. package/dist/Components/Inputs/InputRenderer.js +37 -32
  78. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  79. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  80. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  81. package/dist/Components/Inputs/PictureInput.js +75 -0
  82. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  83. package/dist/Components/Inputs/RadioGroup.js +8 -3
  84. package/dist/Components/Inputs/RowsInput.d.ts +15 -2
  85. package/dist/Components/Inputs/RowsInput.js +96 -8
  86. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  87. package/dist/Components/Inputs/TabularForm.js +45 -32
  88. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  89. package/dist/Components/Inputs/TextArea.js +11 -2
  90. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  91. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  92. package/dist/Components/Inputs/TreeView.d.ts +76 -2
  93. package/dist/Components/Inputs/TreeView.js +227 -16
  94. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  95. package/dist/Components/Layouts/MessageBox.js +56 -7
  96. package/dist/Components/Layouts/Popup.d.ts +34 -7
  97. package/dist/Components/Layouts/Popup.js +143 -13
  98. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  99. package/dist/Components/Layouts/PopupDock.js +2 -1
  100. package/dist/Components/Layouts/PopupHost.js +5 -4
  101. package/dist/Components/Messages/MailClient.d.ts +50 -0
  102. package/dist/Components/Messages/MailClient.js +410 -0
  103. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  104. package/dist/Components/Messages/MailColumns.js +92 -0
  105. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  106. package/dist/Components/Messages/MailCompose.js +154 -0
  107. package/dist/Components/Messages/MailReader.d.ts +43 -0
  108. package/dist/Components/Messages/MailReader.js +207 -0
  109. package/dist/Components/Messages/MailThread.d.ts +71 -0
  110. package/dist/Components/Messages/MailThread.js +185 -0
  111. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  112. package/dist/Components/Messages/MessageBell.js +76 -0
  113. package/dist/Components/Viewers/DataTable.js +153 -49
  114. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  115. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  116. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  117. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  118. package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
  119. package/dist/Components/Viewers/DataTableParts.js +26 -14
  120. package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
  121. package/dist/Components/Viewers/OwnerFiles.js +700 -0
  122. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  123. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  124. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  125. package/dist/Components/Viewers/RecordCards.js +509 -0
  126. package/dist/Helpers/ApiModules.js +4 -0
  127. package/dist/Helpers/AuditEntries.d.ts +50 -0
  128. package/dist/Helpers/AuditEntries.js +96 -0
  129. package/dist/Helpers/BusyHooks.d.ts +6 -0
  130. package/dist/Helpers/BusyHooks.js +39 -0
  131. package/dist/Helpers/ChatBackend.d.ts +8 -1
  132. package/dist/Helpers/ChatBackend.js +18 -0
  133. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  134. package/dist/Helpers/ChatHelpers.js +4 -1
  135. package/dist/Helpers/CrudBackend.d.ts +20 -2
  136. package/dist/Helpers/CrudBackend.js +46 -2
  137. package/dist/Helpers/CrudColumns.d.ts +91 -6
  138. package/dist/Helpers/CrudColumns.js +148 -7
  139. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  140. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  141. package/dist/Helpers/CrudHelpers.d.ts +116 -2
  142. package/dist/Helpers/CrudHelpers.js +256 -0
  143. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  144. package/dist/Helpers/CrudNarrowPick.js +157 -0
  145. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  146. package/dist/Helpers/CrudPickerPlan.js +46 -0
  147. package/dist/Helpers/CrudRowForms.d.ts +32 -0
  148. package/dist/Helpers/CrudRowForms.js +76 -0
  149. package/dist/Helpers/CrudScopes.d.ts +32 -0
  150. package/dist/Helpers/CrudScopes.js +79 -0
  151. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  152. package/dist/Helpers/DashboardHelpers.js +305 -0
  153. package/dist/Helpers/DomainCards.d.ts +140 -0
  154. package/dist/Helpers/DomainCards.js +370 -0
  155. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  156. package/dist/Helpers/DropdownHelpers.js +6 -2
  157. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  158. package/dist/Helpers/ExcelHelpers.js +12 -11
  159. package/dist/Helpers/FileAdapter.d.ts +158 -0
  160. package/dist/Helpers/FileAdapter.js +173 -0
  161. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  162. package/dist/Helpers/FilePrimaries.js +138 -0
  163. package/dist/Helpers/FormConfirm.d.ts +32 -0
  164. package/dist/Helpers/FormConfirm.js +40 -0
  165. package/dist/Helpers/FormStore.d.ts +100 -5
  166. package/dist/Helpers/FormStore.js +368 -25
  167. package/dist/Helpers/InputConditions.d.ts +56 -0
  168. package/dist/Helpers/InputConditions.js +192 -0
  169. package/dist/Helpers/InputMemory.d.ts +106 -0
  170. package/dist/Helpers/InputMemory.js +395 -0
  171. package/dist/Helpers/InputNarrowing.d.ts +50 -0
  172. package/dist/Helpers/InputNarrowing.js +148 -0
  173. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  174. package/dist/Helpers/InputParamsHelpers.js +157 -74
  175. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  176. package/dist/Helpers/MailClientHelpers.js +211 -0
  177. package/dist/Helpers/MessageBackend.d.ts +152 -0
  178. package/dist/Helpers/MessageBackend.js +329 -0
  179. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  180. package/dist/Helpers/MessageHelpers.js +573 -0
  181. package/dist/Helpers/MessageUnread.d.ts +95 -0
  182. package/dist/Helpers/MessageUnread.js +245 -0
  183. package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
  184. package/dist/Helpers/NexusDriveAdapter.js +236 -0
  185. package/dist/Helpers/NumberHelpers.d.ts +156 -0
  186. package/dist/Helpers/NumberHelpers.js +242 -0
  187. package/dist/Helpers/PagedPicker.d.ts +42 -0
  188. package/dist/Helpers/PagedPicker.js +85 -0
  189. package/dist/Helpers/Permissions.js +2 -1
  190. package/dist/Helpers/QrCode.d.ts +26 -0
  191. package/dist/Helpers/QrCode.js +463 -0
  192. package/dist/Helpers/RecordCards.d.ts +205 -0
  193. package/dist/Helpers/RecordCards.js +378 -0
  194. package/dist/Helpers/RequestCache.d.ts +14 -2
  195. package/dist/Helpers/RequestCache.js +22 -3
  196. package/dist/Helpers/RowsStore.d.ts +61 -2
  197. package/dist/Helpers/RowsStore.js +203 -16
  198. package/dist/Helpers/ServerPreview.d.ts +69 -0
  199. package/dist/Helpers/ServerPreview.js +99 -0
  200. package/dist/Helpers/TableColumns.js +4 -3
  201. package/dist/Helpers/TableExport.d.ts +7 -1
  202. package/dist/Helpers/TableExport.js +9 -3
  203. package/dist/Helpers/TableFilters.d.ts +1 -1
  204. package/dist/Helpers/TableFilters.js +24 -23
  205. package/dist/Helpers/TableHelpers.d.ts +38 -4
  206. package/dist/Helpers/TableHelpers.js +78 -10
  207. package/dist/Helpers/TableJoins.d.ts +13 -0
  208. package/dist/Helpers/TableJoins.js +34 -0
  209. package/dist/Helpers/TableStore.d.ts +28 -4
  210. package/dist/Helpers/TableStore.js +73 -11
  211. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  212. package/dist/Helpers/ToolbarFit.js +30 -0
  213. package/dist/Helpers/TreeStore.d.ts +156 -6
  214. package/dist/Helpers/TreeStore.js +547 -18
  215. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  216. package/dist/Interfaces/ApiInterfaces.js +10 -1
  217. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  218. package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
  219. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  220. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  221. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  222. package/dist/Interfaces/DomainInterfaces.js +1 -0
  223. package/dist/Interfaces/FileInterfaces.d.ts +183 -0
  224. package/dist/Interfaces/FileInterfaces.js +6 -0
  225. package/dist/Interfaces/FormInterfaces.d.ts +460 -32
  226. package/dist/Interfaces/FormInterfaces.js +79 -1
  227. package/dist/Interfaces/InputInterfaces.d.ts +22 -3
  228. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  229. package/dist/Interfaces/MailInterfaces.js +1 -0
  230. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  231. package/dist/Interfaces/RecordInterfaces.js +1 -0
  232. package/dist/Interfaces/TableInterfaces.d.ts +102 -3
  233. package/dist/Services/ExcelApi.js +5 -0
  234. package/dist/Services/FileApi.d.ts +13 -0
  235. package/dist/Services/FileApi.js +29 -0
  236. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  237. package/dist/Services/ImageEditorLoader.js +51 -0
  238. package/dist/Services/JoinSources.d.ts +79 -6
  239. package/dist/Services/JoinSources.js +132 -21
  240. package/dist/Services/MessageApi.d.ts +18 -0
  241. package/dist/Services/MessageApi.js +64 -0
  242. package/dist/Services/ModuleImport.d.ts +7 -0
  243. package/dist/Services/ModuleImport.js +22 -0
  244. package/dist/Shared.Index.d.ts +28 -0
  245. package/dist/Shared.Index.js +28 -0
  246. package/package.json +13 -2
  247. package/src/Styles/Nexus.Avatar.css +6 -5
  248. package/src/Styles/Nexus.Button.css +39 -4
  249. package/src/Styles/Nexus.Chat.css +114 -22
  250. package/src/Styles/Nexus.Choice.css +16 -0
  251. package/src/Styles/Nexus.Crud.css +486 -21
  252. package/src/Styles/Nexus.Dashboard.css +763 -0
  253. package/src/Styles/Nexus.Domain.css +384 -0
  254. package/src/Styles/Nexus.Dropdown.css +8 -0
  255. package/src/Styles/Nexus.Editor.css +20 -0
  256. package/src/Styles/Nexus.Files.css +437 -0
  257. package/src/Styles/Nexus.Form.css +5 -2
  258. package/src/Styles/Nexus.Index.css +7 -0
  259. package/src/Styles/Nexus.Input.css +73 -0
  260. package/src/Styles/Nexus.Mail.css +1062 -0
  261. package/src/Styles/Nexus.Menu.css +10 -1
  262. package/src/Styles/Nexus.Popup.css +135 -1
  263. package/src/Styles/Nexus.Print.css +288 -0
  264. package/src/Styles/Nexus.QrCode.css +12 -0
  265. package/src/Styles/Nexus.Record.css +430 -0
  266. package/src/Styles/Nexus.Tab.Buttons.css +87 -22
  267. package/src/Styles/Nexus.Table.css +1458 -1387
  268. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  269. package/src/Styles/Nexus.Themes.css +12 -6
  270. package/src/Styles/Nexus.Tokens.css +8 -3
  271. package/src/Styles/Nexus.Tree.css +41 -1
@@ -1,12 +1,16 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { iconAlertCircle, iconArrowBackUp, iconPlus, InlineIcon } from "nexus-icons";
4
- import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
4
+ import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
6
  import { useDatePreferences } from "../../Helpers/DatePreferences.js";
7
+ import { browserInputMemory, FORM_SAVED_EVENT } from "../../Helpers/InputMemory.js";
8
+ import { narrowingValue, narrowInputParamsBy } from "../../Helpers/InputNarrowing.js";
7
9
  import { inputFieldName } from "../../Helpers/InputParamsHelpers.js";
8
10
  import { RowsStore } from "../../Helpers/RowsStore.js";
11
+ import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
9
12
  import { Button } from "../Documents/Button.js";
13
+ import { ComputedInput } from "./ComputedInput.js";
10
14
  import { hasContent } from "./Field.js";
11
15
  import { isFirstInvalid } from "./InputField.js";
12
16
  import { renderInput } from "./InputRenderer.js";
@@ -64,7 +68,14 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
64
68
  });
65
69
  }, [store, syncValidity]);
66
70
  useEffect(syncValidity);
67
- // Inside a form: the rows go into its data as JSON under `name`, and a reset brings back the rows loaded last.
71
+ // Remembered row inputs (`remember`) read and keep their values in this browser: rows added from now on open on them.
72
+ // Given once drawn, so the server's draw and the first one in the browser are the same.
73
+ useLayoutEffect(() => {
74
+ if (inputs.some(params => params.remember))
75
+ store.attachMemory(browserInputMemory());
76
+ }, [store, inputs]);
77
+ // Inside a form: the rows go into its data as JSON under `name`, a reset brings back the rows loaded last, and a save
78
+ // (a SubmitForm's) keeps the remembered values of the row added last.
68
79
  useEffect(() => {
69
80
  const form = proxyRef.current?.form;
70
81
  if (!form)
@@ -79,11 +90,17 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
79
90
  };
80
91
  // The reset event comes before the browser restores the fields.
81
92
  const onReset = () => setTimeout(() => store.reset());
93
+ const onSaved = () => {
94
+ if (!latest.current.disabled)
95
+ store.rememberAdded();
96
+ };
82
97
  form.addEventListener("formdata", onFormData);
83
98
  form.addEventListener("reset", onReset);
99
+ form.addEventListener(FORM_SAVED_EVENT, onSaved);
84
100
  return () => {
85
101
  form.removeEventListener("formdata", onFormData);
86
102
  form.removeEventListener("reset", onReset);
103
+ form.removeEventListener(FORM_SAVED_EVENT, onSaved);
87
104
  };
88
105
  }, [store, props.form, name]);
89
106
  function focusProblem(place) {
@@ -179,9 +196,43 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
179
196
  const row = store.row(key);
180
197
  return row ? ask(row, name, index) : undefined;
181
198
  }, [store, rowLock]);
199
+ // The form's values the rows follow (`narrowedBy` `from: "form"`): when one changes, every row draws again, narrowed afresh.
200
+ const formKey = formValuesKey(props.formValues);
201
+ const shownFormKey = useRef(formKey);
202
+ useLayoutEffect(() => {
203
+ if (shownFormKey.current === formKey)
204
+ return;
205
+ shownFormKey.current = formKey;
206
+ store.redrawRows();
207
+ }, [store, formKey]);
208
+ // Row inputs worked out from others (`compute`) read the rows' `computeWith` as they draw: a new one draws every row.
209
+ const computeWith = useCallback(() => latest.current.props.computeWith, []);
210
+ const shownComputeWith = useRef(props.computeWith);
211
+ useLayoutEffect(() => {
212
+ if (Object.is(shownComputeWith.current, props.computeWith))
213
+ return;
214
+ shownComputeWith.current = props.computeWith;
215
+ if (inputs.some(params => params.compute))
216
+ store.redrawRows();
217
+ }, [store, inputs, props.computeWith]);
182
218
  // A cell's own params, merged once per answer: the same answer gives the same object, so the cell does not draw again.
183
219
  const merged = useRef(new WeakMap());
184
220
  const cellParams = useCallback((key, params, index) => {
221
+ const own = ownCellParams(key, params, index);
222
+ // Options narrowed by the row's own values and the form's (`narrowedBy`): read as the row draws, which it does when
223
+ // one of them changes.
224
+ if (!params.narrowedBy)
225
+ return own;
226
+ const row = store.row(key);
227
+ const outer = latest.current.props.formValues;
228
+ return narrowInputParamsBy(own, narrowing => {
229
+ if (narrowing.from === "form")
230
+ return outer?.[narrowing.name] ?? { value: undefined };
231
+ return { value: row?.[narrowing.name], label: latest.current.props.inputs.find(input => input.name === narrowing.name)?.label };
232
+ });
233
+ }, [store]);
234
+ /** The column's params, or the row's own over them (`cellParams`). */
235
+ function ownCellParams(key, params, index) {
185
236
  const ask = latest.current.props.cellParams;
186
237
  if (!ask)
187
238
  return params;
@@ -201,8 +252,14 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
201
252
  byColumn.set(params, result);
202
253
  }
203
254
  return result;
204
- }, [store]);
205
- return { store, snapshot, proxyRef, onInvalid, canAdd, actions, cellLock, rowLock, cellParams };
255
+ }
256
+ return { store, snapshot, proxyRef, onInvalid, canAdd, actions, cellLock, rowLock, cellParams, computeWith };
257
+ }
258
+ /** The form values rows follow, as text that changes only when one of them does (a new object with the same values does not). */
259
+ function formValuesKey(values) {
260
+ if (!values)
261
+ return "";
262
+ return JSON.stringify(Object.keys(values).sort().map(name => [name, narrowingValue(values[name]?.value)]));
206
263
  }
207
264
  function addButtonOf(proxy) {
208
265
  return proxy?.closest(".nx-rows")?.querySelector(":scope > .nx-rows__footer .nx-rows__add:not(:disabled)") ?? null;
@@ -228,17 +285,23 @@ export function rowName(store, index) {
228
285
  export function labelText(params) {
229
286
  return typeof params.label === "string" ? params.label : (inputFieldName(params) ?? params.name);
230
287
  }
288
+ /** A cell's control id: the rows' id, the row's key, and the input's name. */
289
+ const cellId = (idPrefix, rowKey, name) => `${idPrefix}-${rowKey.replace(/[^A-Za-z0-9_-]/g, "_")}-${name}`;
231
290
  /**
232
291
  * One input of one row. It starts from the store's value when it mounts and writes every change back, so typing renders
233
292
  * nothing; a value set from code mounts it again (its key holds the value's revision). In a table the column header is
234
293
  * the label, so the input is named "City, row 2" for screen readers and its messages still say "City".
235
294
  */
236
- export const RowCell = memo(function RowCell({ store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled, lock }) {
295
+ export const RowCell = memo(function RowCell(props) {
296
+ const { store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled, lock } = props;
237
297
  const [initial] = useState(() => store.get(rowKey, params.name));
238
298
  const onValueChange = useCallback((next) => store.set(rowKey, params.name, next, "input"), [store, rowKey, params.name]);
239
- const needsName = params.type === "radio-group" || params.type === "checkbox-group";
299
+ // Worked out from the row's other inputs: no value of its own, drawn from the row as it is now.
300
+ if (params.compute)
301
+ return _jsx(RowComputedCell, { ...props });
302
+ const needsName = params.type === EInputTypes.RadioGroup || params.type === EInputTypes.CheckBoxGroup;
240
303
  const overrides = {
241
- id: `${idPrefix}-${rowKey.replace(/[^A-Za-z0-9_-]/g, "_")}-${params.name}`,
304
+ id: cellId(idPrefix, rowKey, params.name),
242
305
  // Nothing goes into a parent form by itself: the rows go in whole, under the rows input's name.
243
306
  name: needsName ? `${ROW_INPUT_NAME}${idPrefix}:${rowKey}:${params.name}` : undefined,
244
307
  defaultValue: initial,
@@ -257,7 +320,7 @@ export const RowCell = memo(function RowCell({ store, rowKey, params, index, err
257
320
  if (error !== undefined)
258
321
  overrides.error = error;
259
322
  // A required radio means one row has it on, which the rows check; each radio alone is not required.
260
- if (params.type === "radio")
323
+ if (params.type === EInputTypes.Radio)
261
324
  overrides.required = false;
262
325
  if (layout === "tabular") {
263
326
  overrides.label = undefined;
@@ -270,6 +333,31 @@ export const RowCell = memo(function RowCell({ store, rowKey, params, index, err
270
333
  }
271
334
  return renderInput(params, overrides);
272
335
  });
336
+ /**
337
+ * A row input worked out from the row's others (`compute`): `ComputedInput` reading the row and the rows' `computeWith`,
338
+ * drawn again whenever the row changes, and writing a typed-in figure's answer as the person's change of the row
339
+ * (`RowsStore.setFromPerson`). In a table it has no label and no line under it: the note goes under the rows.
340
+ */
341
+ function RowComputedCell({ store, rowKey, params, index, layout, idPrefix, size, design, readOnly, disabled, lock, cells }) {
342
+ useRowVersion(store, rowKey);
343
+ const at = useRef(index);
344
+ at.current = index;
345
+ const read = useCallback(() => (store.row(rowKey) ?? {}), [store, rowKey]);
346
+ const write = useCallback((values) => void store.setFromPerson(rowKey, values), [store, rowKey]);
347
+ // The row's other inputs as this row has them: its own params (`cellParams`), and read-only where the cell is locked.
348
+ const inputParams = useCallback((name) => {
349
+ const column = store.inputs.find(params => params.name === name);
350
+ if (!column || !cells)
351
+ return column;
352
+ const own = cells.cellParams(rowKey, column, at.current);
353
+ return cells.cellLock(rowKey, name, at.current) ? { ...own, readOnly: true } : own;
354
+ }, [store, rowKey, cells]);
355
+ const sourceId = useCallback((name) => cellId(idPrefix, rowKey, name), [idPrefix, rowKey]);
356
+ const tabular = layout === "tabular";
357
+ const say = cells?.onNote;
358
+ const onNote = useMemo(() => (tabular && say ? (note) => say(rowKey, params.name, note) : undefined), [tabular, say, rowKey, params.name]);
359
+ return (_jsx(ComputedInput, { params: params, id: cellId(idPrefix, rowKey, params.name), read: read, withValue: cells?.computeWith?.(), write: write, inputParams: inputParams, sourceId: sourceId, size: params.size ?? size, design: design, readOnly: readOnly || Boolean(lock), readOnlyMessage: typeof lock === "string" ? lock : undefined, disabled: disabled, cell: tabular ? { "aria-label": `${labelText(params)}, ${rowName(store, index)}` } : undefined, onNote: onNote }));
360
+ }
273
361
  /** The legend of the rows, with the required mark and, when read-only, a lock. */
274
362
  export function RowsLegend({ id, label, required, lock }) {
275
363
  if (!hasContent(label))
@@ -1,4 +1,4 @@
1
- import type { RowValue, TabularFormProps } from "../../Interfaces/FormInterfaces.ts";
1
+ import { type RowValue, type TabularFormProps } from "../../Interfaces/FormInterfaces.ts";
2
2
  /**
3
3
  * Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
4
4
  * value is an array of row objects, one value per input, with any other fields the rows came with (such as ids). Inputs
@@ -18,10 +18,10 @@ import type { RowValue, TabularFormProps } from "../../Interfaces/FormInterfaces
18
18
  *
19
19
  * @example
20
20
  * <TabularForm name="emails" label="Emails" itemLabel="Email" max={5} unique={["email"]} inputs={[
21
- * { type: "email", name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
22
- * { type: "dropdown", name: "type", label: "Type", options: EMAIL_TYPES },
23
- * { type: "checkbox", name: "isVerified", label: "Verified" },
24
- * { type: "radio", name: "isPrimary", label: "Primary", required: true },
21
+ * { type: EInputTypes.Email, name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
22
+ * { type: EInputTypes.Dropdown, name: "type", label: "Type", options: EMAIL_TYPES },
23
+ * { type: EInputTypes.CheckBox, name: "isVerified", label: "Verified" },
24
+ * { type: EInputTypes.Radio, name: "isPrimary", label: "Primary", required: true },
25
25
  * ]} />
26
26
  */
27
27
  export declare function TabularForm<TRow extends RowValue = RowValue>(props: TabularFormProps<TRow>): import("react").JSX.Element;
@@ -3,6 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { iconArrowDown, iconArrowUp, iconCopy, iconDotsVertical, iconLock, iconLockOpen, iconRowInsertBottom, iconRowInsertTop, iconTrash, InlineIcon } from "nexus-icons";
4
4
  import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState, } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { isBooleanInput } from "../../Helpers/InputParamsHelpers.js";
7
+ import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
6
8
  import { Menu } from "../Documents/Menu.js";
7
9
  import { firstFocusable, labelText, RowCell, RowsFooter, RowsLegend, useRowsFrame, useRowsInput, useRowVersion } from "./RowsInput.js";
8
10
  /** Height of a row: the input's box and the line under it. Measured from the first row once it renders. */
@@ -12,38 +14,36 @@ const OVERSCAN = 8;
12
14
  const DEFAULT_MAX_HEIGHT = 440;
13
15
  /** A column's width by input type, when its params give none. */
14
16
  const COLUMN_WIDTH = {
15
- text: "10rem",
16
- email: "13rem",
17
- password: "10rem",
18
- search: "10rem",
19
- tel: "9rem",
20
- url: "13rem",
21
- textarea: "14rem",
22
- number: "7.5rem",
23
- checkbox: "6rem",
24
- switch: "6rem",
25
- radio: "6rem",
26
- "checkbox-group": "16rem",
27
- "radio-group": "16rem",
28
- dropdown: "10rem",
29
- date: "10rem",
30
- "date-range": "15rem",
31
- time: "8rem",
32
- "date-time": "14rem",
33
- slider: "10rem",
34
- "range-slider": "12rem",
17
+ [EInputTypes.Text]: "10rem",
18
+ [EInputTypes.Email]: "13rem",
19
+ [EInputTypes.Password]: "10rem",
20
+ [EInputTypes.Search]: "10rem",
21
+ [EInputTypes.Tel]: "9rem",
22
+ [EInputTypes.Url]: "13rem",
23
+ [EInputTypes.TextArea]: "14rem",
24
+ [EInputTypes.Number]: "7.5rem",
25
+ [EInputTypes.CheckBox]: "6rem",
26
+ [EInputTypes.Switch]: "6rem",
27
+ [EInputTypes.Radio]: "6rem",
28
+ [EInputTypes.CheckBoxGroup]: "16rem",
29
+ [EInputTypes.RadioGroup]: "16rem",
30
+ [EInputTypes.Dropdown]: "10rem",
31
+ [EInputTypes.Date]: "10rem",
32
+ [EInputTypes.DateRange]: "15rem",
33
+ [EInputTypes.Time]: "8rem",
34
+ [EInputTypes.DateTime]: "14rem",
35
+ [EInputTypes.Slider]: "10rem",
36
+ [EInputTypes.RangeSlider]: "12rem",
35
37
  };
36
- /** Columns that keep their width instead of sharing the spare room. */
37
- const FIXED_TYPES = new Set(["checkbox", "switch", "radio"]);
38
38
  /** What a locked row's cells say when one is used. */
39
39
  const LOCKED_ROW = "Locked: unlock the row to change it.";
40
40
  const ENTER_MOVES = new Set(["text", "email", "tel", "url", "search", "password", "number"]);
41
41
  const cssLength = (value) => (typeof value === "number" ? `${value}px` : value);
42
42
  /** How a cell lines up: numbers at the end, checkboxes and radios in the middle, the rest at the start. */
43
43
  function cellKind(params) {
44
- if (params.type === "number")
44
+ if (params.type === EInputTypes.Number)
45
45
  return "number";
46
- if (FIXED_TYPES.has(params.type))
46
+ if (isBooleanInput(params))
47
47
  return "check";
48
48
  return "text";
49
49
  }
@@ -84,7 +84,7 @@ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, colum
84
84
  return;
85
85
  store.touch(rowKey, cell.dataset.nxCell ?? "");
86
86
  }
87
- return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsxs("div", { role: "cell", className: "nx-tabular__num", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), !atEnd && menu] })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: context.cellParams(rowKey, params, index), index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index) }, store.revision(rowKey, params.name)) }, params.name))), atEnd && (_jsxs("div", { role: "cell", className: "nx-tabular__actions", children: [removable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `Remove ${title}`, title: locked ? "Unlock the row to remove it" : "Remove", "data-tone": "danger", "data-nx-row-actions": "", disabled: locked || context.disabled, onClick: () => onAction("remove", rowKey), children: _jsx(InlineIcon, { icon: iconTrash }) })), lockable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${locked ? "Unlock" : "Lock"} ${title}`, "aria-pressed": locked, title: locked ? "Locked: unlock to change it" : "Lock, so it cannot be changed", "data-nx-row-actions": "", disabled: context.disabled, onClick: () => onAction(locked ? "unlock" : "lock", rowKey), children: _jsx(InlineIcon, { icon: locked ? iconLock : iconLockOpen }) })), menu] }))] }));
87
+ return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsxs("div", { role: "cell", className: "nx-tabular__num", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), !atEnd && menu] })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: context.cellParams(rowKey, params, index), index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index), cells: context }, store.revision(rowKey, params.name)) }, params.name))), atEnd && (_jsxs("div", { role: "cell", className: "nx-tabular__actions", children: [removable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `Remove ${title}`, title: locked ? "Unlock the row to remove it" : "Remove", "data-tone": "danger", "data-nx-row-actions": "", disabled: locked || context.disabled, onClick: () => onAction("remove", rowKey), children: _jsx(InlineIcon, { icon: iconTrash }) })), lockable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${locked ? "Unlock" : "Lock"} ${title}`, "aria-pressed": locked, title: locked ? "Locked: unlock to change it" : "Lock, so it cannot be changed", "data-nx-row-actions": "", disabled: context.disabled, onClick: () => onAction(locked ? "unlock" : "lock", rowKey), children: _jsx(InlineIcon, { icon: locked ? iconLock : iconLockOpen }) })), menu] }))] }));
88
88
  });
89
89
  /**
90
90
  * Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
@@ -105,10 +105,10 @@ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, colum
105
105
  *
106
106
  * @example
107
107
  * <TabularForm name="emails" label="Emails" itemLabel="Email" max={5} unique={["email"]} inputs={[
108
- * { type: "email", name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
109
- * { type: "dropdown", name: "type", label: "Type", options: EMAIL_TYPES },
110
- * { type: "checkbox", name: "isVerified", label: "Verified" },
111
- * { type: "radio", name: "isPrimary", label: "Primary", required: true },
108
+ * { type: EInputTypes.Email, name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
109
+ * { type: EInputTypes.Dropdown, name: "type", label: "Type", options: EMAIL_TYPES },
110
+ * { type: EInputTypes.CheckBox, name: "isVerified", label: "Verified" },
111
+ * { type: EInputTypes.Radio, name: "isPrimary", label: "Primary", required: true },
112
112
  * ]} />
113
113
  */
114
114
  export function TabularForm(props) {
@@ -135,6 +135,15 @@ export function TabularForm(props) {
135
135
  const pageRows = Math.ceil((typeof maxHeight === "number" ? maxHeight : 600) / rowHeight);
136
136
  const [range, setRange] = useState({ start: 0, end: pageRows + OVERSCAN });
137
137
  const [status, setStatus] = useState(null);
138
+ /** The note of the figure typed into a worked-out cell last (`compute.solve`), until it no longer applies. */
139
+ const [note, setNote] = useState(null);
140
+ const onNote = useCallback((key, name, next) => {
141
+ setNote(current => {
142
+ if (next)
143
+ return current && current.key === key && current.name === name && current.text === next.text && current.refused === next.refused ? current : { ...next, key, name };
144
+ return current && current.key === key && current.name === name ? null : current;
145
+ });
146
+ }, []);
138
147
  const pendingRef = useRef(null);
139
148
  const start = Math.min(range.start, count);
140
149
  const end = Math.min(count, Math.max(range.end, start + 1));
@@ -311,7 +320,7 @@ export function TabularForm(props) {
311
320
  layoutRef.current?.focusRow(key, name);
312
321
  }
313
322
  }
314
- const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll, cellLock, cellParams: rows.cellParams, rowLock: rows.rowLock, actionsAt, lockable, isLocked }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll, cellLock, rows.cellParams, rows.rowLock, actionsAt, lockable, isLocked]);
323
+ const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll, cellLock, cellParams: rows.cellParams, rowLock: rows.rowLock, actionsAt, lockable, isLocked, computeWith: rows.computeWith, onNote }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll, cellLock, rows.cellParams, rows.rowLock, actionsAt, lockable, isLocked, rows.computeWith, onNote]);
315
324
  /**
316
325
  * Which columns are generated for **every** row on the page. The lock is then said once, on the column header,
317
326
  * instead of on each of its cells: eighteen locks down one column is eighteen times the ink for one fact. The cells
@@ -359,7 +368,7 @@ export function TabularForm(props) {
359
368
  const template = [
360
369
  numbered ? numberWidth : null,
361
370
  ...columns.map((params, index) => {
362
- const grow = params.grow ?? (FIXED_TYPES.has(params.type) ? 0 : 1);
371
+ const grow = params.grow ?? (isBooleanInput(params) ? 0 : 1);
363
372
  return grow > 0 ? `minmax(${widths[index]}, ${grow}fr)` : widths[index];
364
373
  }),
365
374
  actionsWidth,
@@ -369,6 +378,10 @@ export function TabularForm(props) {
369
378
  const minWidth = `calc(${[numbered ? numberWidth : "0px", ...widths, actionsWidth ?? "0px"].join(" + ")})`;
370
379
  const tableStyle = { "--nx-tabular-columns": template, "--nx-tabular-num": numbered ? numberWidth : "0px", minWidth };
371
380
  const invalid = Boolean(error || snapshot.message || snapshot.invalidRows);
381
+ // A typed-in figure's note, named with its row, says more than a hint; a message of the cell in focus says more still.
382
+ const noteRow = note ? store.indexOf(note.key) : -1;
383
+ const said = note && noteRow >= 0 ? { text: `${store.names.title} ${noteRow + 1}: ${note.text}`, error: note.refused } : null;
384
+ const footerStatus = status?.error ? status : (said ?? status);
372
385
  return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-tabular", `nx-tabular--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || undefined, "data-actions-at": atEnd ? "end" : undefined, "aria-label": props["aria-label"], "aria-describedby": frame.messageId, children: [_jsx(RowsLegend, { id: frame.legendId, label: label, required: required || (props.min ?? 0) > 0, lock: frame.lock }), _jsx("input", { ref: rows.proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", form: props.form, onInvalid: rows.onInvalid }), _jsx("div", { ref: viewportRef, className: "nx-tabular__viewport", tabIndex: -1,
373
386
  // A focused cell scrolls clear of the actions fixed at the end, as it does of the number at the start.
374
387
  style: { maxHeight: viewportMax ?? cssLength(maxHeight), scrollPaddingRight: actionsWidth ?? undefined }, onScroll: updateRange, children: _jsxs("div", { role: "table", className: "nx-tabular__table", style: tableStyle, "aria-labelledby": label ? frame.legendId : undefined, "aria-label": label ? undefined : props["aria-label"], "aria-rowcount": count + 1, children: [_jsx("div", { ref: headRef, role: "rowgroup", className: "nx-tabular__head", children: _jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": 1, children: [numbered && (_jsxs("div", { role: "columnheader", className: "nx-tabular__num", children: [_jsx("span", { "aria-hidden": "true", children: "#" }), _jsx("span", { className: "nx-visually-hidden", children: store.names.title })] })), columns.map(params => {
@@ -376,5 +389,5 @@ export function TabularForm(props) {
376
389
  const locked = lockedColumns.get(params.name);
377
390
  const reason = typeof locked === "string" ? locked : locked ? "Read-only" : undefined;
378
391
  return (_jsxs("div", { role: "columnheader", className: cx("nx-tabular__head-cell", `nx-tabular__cell--${cellKind(params)}`), title: reason ?? (typeof columnHint === "string" ? columnHint : undefined), "data-nx-locked": locked ? "" : undefined, children: [_jsx("span", { className: "nx-tabular__head-label", children: params.label ?? params.name }), params.required && !locked && (_jsx("span", { className: "nx-field__required", "aria-hidden": "true", children: "*" })), locked && _jsx(InlineIcon, { icon: iconLock, className: "nx-tabular__head-lock", "aria-hidden": "true" })] }, params.name));
379
- }), actionsWidth && (_jsx("div", { role: "columnheader", className: "nx-tabular__actions", children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status: status, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
392
+ }), actionsWidth && (_jsx("div", { role: "columnheader", className: "nx-tabular__actions", children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status: footerStatus, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
380
393
  }
@@ -22,6 +22,12 @@ export interface TextAreaProps extends NativeTextAreaProps, TextRules {
22
22
  maxRows?: number;
23
23
  /** Shows the character count, and the limit when `maxLength` is set. */
24
24
  showCount?: boolean;
25
+ /**
26
+ * Text that is code (HTML, JSON): the monospace font, lines kept as typed (no wrapping, a long line scrolls sideways
27
+ * inside the box), a tab two spaces wide, and no spelling, capitals, corrections, suggestions or translation. Tab still
28
+ * leaves the box, so a keyboard is never trapped in it.
29
+ */
30
+ code?: boolean;
25
31
  /** What a read-only field says when someone tries to change it. Default "Read-only: this field cannot be changed." */
26
32
  readOnlyMessage?: string;
27
33
  /** Name used in messages. Default: the label, when it is text. */
@@ -5,13 +5,22 @@ import { Field } from "./Field.js";
5
5
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
6
6
  import { useTextField } from "./TextField.js";
7
7
  import { useInputDesign } from "../../Helpers/UserPreferences.js";
8
+ /** A `code` text area's attributes: lines kept as typed, and nothing that would change what was typed. A page's own props win. */
9
+ const CODE_PROPS = {
10
+ wrap: "off",
11
+ spellCheck: false,
12
+ autoCapitalize: "off",
13
+ autoCorrect: "off",
14
+ autoComplete: "off",
15
+ translate: "no",
16
+ };
8
17
  /**
9
18
  * A multi-line text input with the same label, hint, validation, and designs as `TextBox`.
10
19
  *
11
20
  * @example <TextArea label="Description" maxLength={500} showCount autoResize />
12
21
  */
13
22
  export function TextArea(props) {
14
- const { label, hint, error, design: ownDesign, size = "md", icon, rows = 3, autoResize = false, maxRows = 12, showCount = false, readOnlyMessage = "Read-only: this field cannot be changed.", fieldName, validateOn, onValueChange, className, textAreaClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, placeholder, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, ...textAreaProps } = props;
23
+ const { label, hint, error, design: ownDesign, size = "md", icon, rows = 3, autoResize = false, maxRows = 12, showCount = false, code = false, readOnlyMessage = "Read-only: this field cannot be changed.", fieldName, validateOn, onValueChange, className, textAreaClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, placeholder, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, ...textAreaProps } = props;
15
24
  // The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
16
25
  const design = useInputDesign(ownDesign, Boolean(label));
17
26
  const field = useTextField(props);
@@ -30,5 +39,5 @@ export function TextArea(props) {
30
39
  readOnlyNotice.hide();
31
40
  field.controlProps.onBlur(event);
32
41
  }
33
- return (_jsxs(_Fragment, { children: [_jsx(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, design: design, size: size, hint: hint, error: field.error, icon: icon, actions: readOnlyNotice.lock, aside: field.count, disabled: disabled, readOnly: readOnly, raiseLabel: Boolean(placeholder), multiline: true, className: className, style: { ...rowsStyle, ...style }, children: _jsx("textarea", { ...textAreaProps, ...field.controlProps, className: cx("nx-field__control", autoResize && "nx-field__control--auto", textAreaClassName), rows: rows, placeholder: placeholder || " ", disabled: disabled, readOnly: readOnly, onClick: handleClick, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), readOnlyNotice.notice] }));
42
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, design: design, size: size, hint: hint, error: field.error, icon: icon, actions: readOnlyNotice.lock, aside: field.count, disabled: disabled, readOnly: readOnly, raiseLabel: Boolean(placeholder), multiline: true, className: className, style: { ...rowsStyle, ...style }, children: _jsx("textarea", { ...(code ? CODE_PROPS : null), ...textAreaProps, ...field.controlProps, className: cx("nx-field__control", autoResize && "nx-field__control--auto", code && "nx-field__control--code", textAreaClassName), rows: rows, placeholder: placeholder || " ", disabled: disabled, readOnly: readOnly, onClick: handleClick, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), readOnlyNotice.notice] }));
34
43
  }
@@ -0,0 +1,6 @@
1
+ import { EInputTypes, type TextInputType } from "../../Interfaces/FormInterfaces.ts";
2
+ import type { TextBoxType } from "./TextBox.tsx";
3
+ /** The HTML type of each text box kind. */
4
+ export declare const TEXT_BOX_TYPES: Readonly<Record<TextInputType, TextBoxType>>;
5
+ /** Whether a `TextBox` renders this input type, e.g. `true` for `EInputTypes.Email`, `false` for `EInputTypes.TextArea`. */
6
+ export declare const isTextInputType: (type: EInputTypes) => type is TextInputType;
@@ -0,0 +1,17 @@
1
+ import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
2
+ /*
3
+ * Which input types a `TextBox` renders, and the HTML type each one gets: the one place an input type becomes a string.
4
+ * `renderInput` reads it; it is its own file, not exported from the package, so the unit suites can check it without
5
+ * a browser (scripts/test-form-store.mts).
6
+ */
7
+ /** The HTML type of each text box kind. */
8
+ export const TEXT_BOX_TYPES = {
9
+ [EInputTypes.Text]: "text",
10
+ [EInputTypes.Email]: "email",
11
+ [EInputTypes.Password]: "password",
12
+ [EInputTypes.Search]: "search",
13
+ [EInputTypes.Tel]: "tel",
14
+ [EInputTypes.Url]: "url",
15
+ };
16
+ /** Whether a `TextBox` renders this input type, e.g. `true` for `EInputTypes.Email`, `false` for `EInputTypes.TextArea`. */
17
+ export const isTextInputType = (type) => Object.hasOwn(TEXT_BOX_TYPES, type);
@@ -1,7 +1,17 @@
1
1
  import { type CSSProperties, type ReactNode, type Ref } from "react";
2
- import { type TreeChildrenPage, type TreeChildrenRequest, type TreeItem, type TreeValue, type TreeValueMode } from "../../Helpers/TreeStore.ts";
2
+ import { type TreeChangeRules, type TreeChildrenPage, type TreeChildrenRequest, type TreeFilter, type TreeItem, type TreeItemChange, type TreeValue, type TreeValueMode } from "../../Helpers/TreeStore.ts";
3
+ import type { TableRowAction } from "../../Interfaces/TableInterfaces.ts";
3
4
  import type { Size } from "../../Interfaces/TypeInterfaces.ts";
4
5
  export type TreeAction = "create" | "update" | "delete";
6
+ /**
7
+ * An action of the page's own on one item: an icon button on its row when `inline` (shown under the pointer or with
8
+ * focus, always on a touch screen), and an entry of the row's context menu either way. The row is busy while a
9
+ * promise `onSelect` returns runs.
10
+ */
11
+ export interface TreeRowAction<TItem extends TreeItem = TreeItem> extends TableRowAction<TItem> {
12
+ /** The key that runs it on the focused row, as Insert adds, F2 renames and Delete deletes in a tree that edits its own items. */
13
+ shortcut?: "Insert" | "F2" | "Delete";
14
+ }
5
15
  export interface TreeCreateRequest<TItem extends TreeItem = TreeItem> {
6
16
  /** The item the new one goes under, or `null` at the top level. */
7
17
  parent: TItem | null;
@@ -31,7 +41,10 @@ export interface TreeViewHandle<TItem extends TreeItem = TreeItem> {
31
41
  /** Expands an item and its parents. */
32
42
  expand: (value: TreeValue) => void;
33
43
  collapse: (value: TreeValue) => void;
34
- /** Expands the parents of an item, scrolls to it, and focuses it unless `focus` is false. */
44
+ /**
45
+ * Expands the parents of an item, scrolls to it, and focuses it unless `focus` is false. An item not loaded yet is
46
+ * revealed when the page that brings it comes (opened by `expand`, or Load more), unless the user moves on first.
47
+ */
35
48
  reveal: (value: TreeValue, focus?: boolean) => void;
36
49
  getValue: () => TreeValue[];
37
50
  /** Checks exactly these values, locked items included. For a tree without `value`. */
@@ -40,6 +53,8 @@ export interface TreeViewHandle<TItem extends TreeItem = TreeItem> {
40
53
  checkAll: (checked: boolean) => void;
41
54
  /** Every item in tree order, with the changes made in the tree. */
42
55
  getItems: () => TItem[];
56
+ /** The item of a value, as the tree holds it now; `undefined` when it is not loaded. */
57
+ getItem: (value: TreeValue) => TItem | undefined;
43
58
  /** Opens the editor for a new item under `parentValue`, or at the top level for `null`. */
44
59
  startCreate: (parentValue: TreeValue | null) => void;
45
60
  startRename: (value: TreeValue) => void;
@@ -53,6 +68,12 @@ export interface TreeViewHandle<TItem extends TreeItem = TreeItem> {
53
68
  * when it scrolls in, a closed one when it is next expanded. What is drawn stays until the fresh pages replace it.
54
69
  */
55
70
  refresh: (value?: TreeValue) => void;
71
+ /**
72
+ * Writes changes made elsewhere into this tree, as `writeTreeChanges` does into every tree with a `cacheKey` (which
73
+ * calls this): items placed, moved and taken out, the parents' counts and chevrons, and focus kept on its row, or on the
74
+ * row in its place when the item left. `rules` gives `compare`, the order of siblings.
75
+ */
76
+ apply: (changes: readonly TreeItemChange<TItem>[], rules?: TreeChangeRules<TItem>) => void;
56
77
  }
57
78
  export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
58
79
  /**
@@ -159,6 +180,53 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
159
180
  onItemsChange?: (items: TItem[]) => void;
160
181
  /** Asks before deleting. Default true; needs `<PopupHost />` on the page. */
161
182
  confirmDelete?: boolean;
183
+ /**
184
+ * Actions of the page's own on each item - Add under it, Edit, Move to trash - for a tree whose items are records
185
+ * changed through forms rather than renamed in the row: icon buttons on the row for the `inline` ones, all of them in
186
+ * the row's context menu, and the key each names. Not offered on locked items, nor in a read-only or disabled tree.
187
+ */
188
+ rowActions?: (item: TItem) => readonly TreeRowAction<TItem>[];
189
+ /**
190
+ * A button of the page's own at the end of the toolbar, such as Add New for a form. Its `description` is its tooltip
191
+ * and what a screen reader hears after its name. `disabled` with a `description` says why it cannot be pressed yet: it
192
+ * stays in the Tab order, looks unavailable, and a press - a click, a tap, Enter - shows the description in a note under
193
+ * it rather than doing nothing.
194
+ */
195
+ add?: {
196
+ label: ReactNode;
197
+ onClick: () => void;
198
+ disabled?: boolean;
199
+ description?: string;
200
+ };
201
+ /**
202
+ * The row shown as selected: the item the page picked - through `onItemClick`, say - marked `aria-selected` with a tint
203
+ * that stays when focus moves on. The page decides it: moving focus, a chevron, and opening or closing a row leave it
204
+ * where it is. Not on a `checkable` tree, whose rows are checked instead.
205
+ */
206
+ selectedValue?: TreeValue | null;
207
+ /**
208
+ * Small tabs above the rows that narrow them, such as All actions · View only · Add and edit · Trash and delete on a
209
+ * permission tree. A filter's `match` is asked of every item without children (one without `match` shows all); the
210
+ * tree shows the leaves it takes and the parents that hold one. While it is on, checking a parent - or Select all -
211
+ * checks or unchecks only the leaves shown, a parent's box and count read only them, and a leaf the filter hides keeps
212
+ * its check exactly as it was. The value is the same whatever tab is on: switching tabs never changes it, and never
213
+ * makes a form dirty. The first filter is on at the start, unless `defaultFilter` names another.
214
+ */
215
+ filters?: readonly TreeFilter<TItem>[];
216
+ /** With `filters`: the id of the filter on at the start. Default the first. */
217
+ defaultFilter?: string;
218
+ /**
219
+ * With `filters`: keeps the tab chosen in this browser, per `key` and `viewer` (the signed-in person), and opens it
220
+ * next time; nothing is kept without a viewer. Read after the first draw, as every kept choice is.
221
+ */
222
+ rememberFilter?: {
223
+ key: string;
224
+ viewer?: string | null;
225
+ };
226
+ /** With `filters`: after a tab is chosen, its id. */
227
+ onFilterChange?: (id: string) => void;
228
+ /** With `filters`: what a screen reader calls the tabs. Default "Show". */
229
+ filtersLabel?: string;
162
230
  /** The longest name the editor takes. Default 100. */
163
231
  maxLabelLength?: number;
164
232
  /** A click on a row without a checkbox, or Enter on it. Without it, a click expands or collapses a parent. */
@@ -173,6 +241,12 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
173
241
  height?: number | string;
174
242
  /** The tallest the rows' area grows before it scrolls. Default 420. */
175
243
  maxHeight?: number | string;
244
+ /**
245
+ * Takes the height it is given, as a data table's `fill` does: the toolbar stays on top and the rows scroll inside
246
+ * under it, so the page around it never has to. `height` and `maxHeight` are ignored while it is on. Give it a cell
247
+ * that has a height: a side of an `AdminSplit` on an `AdminPage` with `fill` (nexus-admin), or a parent of your own.
248
+ */
249
+ fill?: boolean;
176
250
  /** Can be browsed, expanded, and focused, but not changed. A lock follows the label, and a try shows a note. */
177
251
  readOnly?: boolean;
178
252
  /** What a read-only tree says when someone tries to change it. Default "Read-only: this tree cannot be changed." */