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
@@ -4,13 +4,12 @@ import { iconAlertCircle, iconArrowDown, iconArrowUp, iconChevronDown, iconCopy,
4
4
  import { memo, useCallback, 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 { formatInputValue } from "../../Helpers/InputParamsHelpers.js";
7
+ import { formatInputValue, isBooleanInput } from "../../Helpers/InputParamsHelpers.js";
8
+ import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
8
9
  import { Button } from "../Documents/Button.js";
9
10
  import { firstFocusable, labelText, RowCell, RowsFooter, RowsLegend, useRowsFrame, useRowsInput, useRowVersion } from "./RowsInput.js";
10
11
  /** Inputs that read as a line of text in a collapsed section's summary. */
11
- const SUMMARY_SKIP = new Set(["checkbox", "switch", "radio", "tree", "tabular", "group", "markdown"]);
12
- /** Inputs without a box: they line up with the boxes beside them. */
13
- const CHOICE_TYPES = new Set(["checkbox", "switch", "radio"]);
12
+ const SUMMARY_SKIP = new Set([EInputTypes.CheckBox, EInputTypes.Switch, EInputTypes.Radio, EInputTypes.Tree, EInputTypes.Tabular, EInputTypes.Group, EInputTypes.Markdown, EInputTypes.Picture]);
14
13
  function defaultSummary(row, inputs, preferences) {
15
14
  const parts = [];
16
15
  for (const params of inputs) {
@@ -46,7 +45,7 @@ const GroupSection = memo(function GroupSection({ store, rowKey, index, count, i
46
45
  setCollapsed(false);
47
46
  }, [problems, collapsed]);
48
47
  // Radio columns this row has on show as tags: "Primary".
49
- const tags = inputs.filter(params => params.type === "radio" && !params.hidden && row[params.name] === true).map(labelText);
48
+ const tags = inputs.filter(params => params.type === EInputTypes.Radio && !params.hidden && row[params.name] === true).map(labelText);
50
49
  let summary = null;
51
50
  if (!open) {
52
51
  const { summary: pick } = context;
@@ -64,7 +63,7 @@ const GroupSection = memo(function GroupSection({ store, rowKey, index, count, i
64
63
  store.touch(rowKey, cell.dataset.nxCell ?? "");
65
64
  }
66
65
  const heading = (_jsxs(_Fragment, { children: [_jsx("span", { id: titleId, className: "nx-group-form__title", children: title }), tags.map(tag => (_jsx("span", { className: "nx-group-form__tag", children: tag }, tag))), summary && _jsx("span", { className: "nx-group-form__summary", children: summary }), problems > 0 && (_jsxs("span", { className: "nx-group-form__problems", children: [_jsx(InlineIcon, { icon: iconAlertCircle }), problems, " to fix"] }))] }));
67
- return (_jsxs("li", { className: "nx-group-form__section", "data-nx-row": rowKey, "data-open": open || undefined, "data-problem": problems > 0 || undefined, children: [_jsxs("div", { className: "nx-group-form__head", children: [context.collapsible ? (_jsxs("button", { type: "button", className: "nx-group-form__toggle", "aria-expanded": open, "aria-controls": open ? bodyId : undefined, "data-nx-row-actions": "", onClick: () => setCollapsed(open), children: [_jsx(InlineIcon, { icon: iconChevronDown, className: "nx-group-form__chevron" }), heading] })) : (_jsx("div", { className: "nx-group-form__heading", children: heading })), (context.sortable || context.canAdd || context.removable) && (_jsxs("div", { className: "nx-group-form__actions", children: [context.sortable && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowUp }), "aria-label": `Move ${name} up`, title: "Move up", disabled: index === 0, onClick: () => onAction("move-up", rowKey) }), _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowDown }), "aria-label": `Move ${name} down`, title: "Move down", disabled: index === count - 1, onClick: () => onAction("move-down", rowKey) })] })), context.canAdd && (_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconCopy }), "aria-label": `Duplicate ${name}`, title: "Duplicate", onClick: () => onAction("duplicate", rowKey) })), context.removable && (_jsx(Button, { variant: "ghost", size: "sm", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), "aria-label": `Remove ${name}`, title: "Remove", onClick: () => onAction("remove", rowKey) }))] }))] }), open && (_jsx("div", { id: bodyId, role: "group", "aria-labelledby": titleId, className: "nx-group-form__body", onBlur: onBlur, children: inputs.map(params => params.hidden ? null : (_jsx("div", { className: cx("nx-group-form__cell", CHOICE_TYPES.has(params.type) && "nx-group-form__cell--choice"), style: { "--nx-span": Math.max(1, Math.min(12, params.span ?? 12)) }, "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: "group", idPrefix: context.idPrefix, size: context.size, design: context.design, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index) }, store.revision(rowKey, params.name)) }, params.name))) }))] }));
66
+ return (_jsxs("li", { className: "nx-group-form__section", "data-nx-row": rowKey, "data-open": open || undefined, "data-problem": problems > 0 || undefined, children: [_jsxs("div", { className: "nx-group-form__head", children: [context.collapsible ? (_jsxs("button", { type: "button", className: "nx-group-form__toggle", "aria-expanded": open, "aria-controls": open ? bodyId : undefined, "data-nx-row-actions": "", onClick: () => setCollapsed(open), children: [_jsx(InlineIcon, { icon: iconChevronDown, className: "nx-group-form__chevron" }), heading] })) : (_jsx("div", { className: "nx-group-form__heading", children: heading })), (context.sortable || context.canAdd || context.removable) && (_jsxs("div", { className: "nx-group-form__actions", children: [context.sortable && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowUp }), "aria-label": `Move ${name} up`, title: "Move up", disabled: index === 0, onClick: () => onAction("move-up", rowKey) }), _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowDown }), "aria-label": `Move ${name} down`, title: "Move down", disabled: index === count - 1, onClick: () => onAction("move-down", rowKey) })] })), context.canAdd && (_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconCopy }), "aria-label": `Duplicate ${name}`, title: "Duplicate", onClick: () => onAction("duplicate", rowKey) })), context.removable && (_jsx(Button, { variant: "ghost", size: "sm", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), "aria-label": `Remove ${name}`, title: "Remove", onClick: () => onAction("remove", rowKey) }))] }))] }), open && (_jsx("div", { id: bodyId, role: "group", "aria-labelledby": titleId, className: "nx-group-form__body", onBlur: onBlur, children: inputs.map(params => params.hidden ? null : (_jsx("div", { className: cx("nx-group-form__cell", isBooleanInput(params) && "nx-group-form__cell--choice"), style: { "--nx-span": Math.max(1, Math.min(12, params.span ?? 12)) }, "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: "group", idPrefix: context.idPrefix, size: context.size, design: context.design, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index), cells: context }, store.revision(rowKey, params.name)) }, params.name))) }))] }));
68
67
  });
69
68
  /**
70
69
  * The rows of a `TabularForm` as sections: each row's inputs with their own labels, on a 12-column grid (`span` in
@@ -77,11 +76,11 @@ const GroupSection = memo(function GroupSection({ store, rowKey, index, count, i
77
76
  *
78
77
  * @example
79
78
  * <GroupForm name="addresses" label="Addresses" itemLabel="Address" max={5} inputs={[
80
- * { type: "dropdown", name: "country", label: "Country", required: true, options: countries, span: 6 },
81
- * { type: "text", name: "city", label: "City", required: true, span: 6 },
82
- * { type: "text", name: "zipCode", label: "Zip code", span: 4 },
83
- * { type: "dropdown", name: "type", label: "Type", options: ADDRESS_TYPES, span: 4 },
84
- * { type: "radio", name: "isPrimary", label: "Primary address", span: 4, required: true },
79
+ * { type: EInputTypes.Dropdown, name: "country", label: "Country", required: true, options: countries, span: 6 },
80
+ * { type: EInputTypes.Text, name: "city", label: "City", required: true, span: 6 },
81
+ * { type: EInputTypes.Text, name: "zipCode", label: "Zip code", span: 4 },
82
+ * { type: EInputTypes.Dropdown, name: "type", label: "Type", options: ADDRESS_TYPES, span: 4 },
83
+ * { type: EInputTypes.Radio, name: "isPrimary", label: "Primary address", span: 4, required: true },
85
84
  * ]} />
86
85
  */
87
86
  export function GroupForm(props) {
@@ -155,6 +154,7 @@ export function GroupForm(props) {
155
154
  design,
156
155
  cellLock: rows.cellLock,
157
156
  cellParams: rows.cellParams,
157
+ computeWith: rows.computeWith,
158
158
  readOnly,
159
159
  disabled,
160
160
  removable,
@@ -166,7 +166,7 @@ export function GroupForm(props) {
166
166
  preferences,
167
167
  sectionTitle,
168
168
  summary,
169
- }), [frame.id, size, design, rows.cellLock, rows.cellParams, readOnly, disabled, removable, sortable, rows.canAdd, collapsible, defaultCollapsed, snapshot.showAll, preferences, sectionTitle, summary]);
169
+ }), [frame.id, size, design, rows.cellLock, rows.cellParams, rows.computeWith, readOnly, disabled, removable, sortable, rows.canAdd, collapsible, defaultCollapsed, snapshot.showAll, preferences, sectionTitle, summary]);
170
170
  const invalid = Boolean(error || snapshot.message || snapshot.invalidRows);
171
171
  return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-group-form", `nx-group-form--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || 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 }), count === 0 ? (_jsx("p", { className: "nx-group-form__empty", children: frame.emptyMessage })) : (_jsx("ol", { ref: listRef, className: "nx-group-form__list", children: snapshot.keys.map((key, index) => (_jsx(GroupSection, { store: store, rowKey: key, index: index, count: count, inputs: inputs, context: context, isNew: !loadedKeys.has(key), onAction: onAction }, `${snapshot.generation}:${key}`))) })), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
172
172
  }
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { InputParams } from "../../Interfaces/FormInterfaces.ts";
2
+ import { type InputParams, type RowsInputProps } from "../../Interfaces/FormInterfaces.ts";
3
3
  import type { InputDesign } from "../../Interfaces/InputInterfaces.ts";
4
4
  import type { Size } from "../../Interfaces/TypeInterfaces.ts";
5
5
  /** What a form sets on an input it renders from params. Keys given here replace the params' own, even when `undefined`. */
@@ -10,8 +10,8 @@ export interface InputOverrides {
10
10
  form?: string;
11
11
  /** The value the input starts with: `defaultChecked` for a checkbox, switch, or radio, `defaultValue` for the rest. */
12
12
  defaultValue?: unknown;
13
- /** The new value after each change, before the params' own `onValueChange`. */
14
- onValueChange?: (value: unknown) => void;
13
+ /** The new value after each change, before the params' own `onValueChange`; a dropdown's item (items) with it. */
14
+ onValueChange?: (value: unknown, item?: unknown) => void;
15
15
  error?: string | null;
16
16
  /** Replaces the params' own, e.g. `false` for a radio in rows, where "required" means one row has it. */
17
17
  required?: boolean;
@@ -25,13 +25,20 @@ export interface InputOverrides {
25
25
  readOnlyMessage?: string;
26
26
  /** On top of the params, like `readOnly`. */
27
27
  disabled?: boolean;
28
+ /** A dropdown's value is on its way: its spinner shows, as a form's does while a picker is asked about a remembered value. */
29
+ loading?: boolean;
28
30
  fieldName?: string;
29
31
  className?: string;
30
32
  "aria-label"?: string;
33
+ /** A tabular or group form's values of the form around it that its rows follow (`RowsInputProps.formValues`). */
34
+ formValues?: RowsInputProps["formValues"];
35
+ /** A tabular or group form's `computeWith`: the form's own, for row inputs worked out from it (`RowsInputProps.computeWith`). */
36
+ computeWith?: unknown;
31
37
  }
32
38
  /**
33
- * Renders an input from its params: `{ type: "email", name: "email", label: "Email", required: true }` gives a
34
- * `TextBox` of type email. Forms render their `inputs` with it, and tabular and group forms render each row's inputs.
35
- * Layout keys (`span`, `width`, `grow`) stay out of the component's props; `hidden` renders nothing.
39
+ * Renders an input from its params: `{ type: EInputTypes.Email, name: "email", label: "Email", required: true }` gives a
40
+ * `TextBox` of HTML type email. Forms render their `inputs` with it, and tabular and group forms render each row's inputs.
41
+ * Layout keys (`span`, `width`, `grow`, and the Form-only ones such as `showWhen` and `compute`) stay out of the
42
+ * component's props; `hidden` renders nothing.
36
43
  */
37
44
  export declare function renderInput(params: InputParams, overrides?: InputOverrides): ReactNode;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { isBooleanInput } from "../../Helpers/InputParamsHelpers.js";
3
+ import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
3
4
  import { CheckBox, RadioButton, Switch } from "./CheckBox.js";
4
5
  import { CheckBoxGroup } from "./CheckBoxGroup.js";
5
6
  import { CodeBox } from "./CodeBox.js";
@@ -10,23 +11,26 @@ import { Dropdown } from "./Dropdown.js";
10
11
  import { GroupForm } from "./GroupForm.js";
11
12
  import { MarkdownEditor } from "./MarkdownEditor.js";
12
13
  import { NumberBox } from "./NumberBox.js";
14
+ import { PictureInput } from "./PictureInput.js";
13
15
  import { RadioGroup } from "./RadioGroup.js";
14
16
  import { RichTextEditor } from "./RichTextEditor.js";
15
17
  import { RangeSlider, Slider } from "./Slider.js";
16
18
  import { TabularForm } from "./TabularForm.js";
17
19
  import { TextArea } from "./TextArea.js";
18
20
  import { TextBox } from "./TextBox.js";
21
+ import { isTextInputType, TEXT_BOX_TYPES } from "./TextBoxTypes.js";
19
22
  import { TimePicker } from "./TimePicker.js";
20
23
  import { TreeView } from "./TreeView.js";
21
24
  /**
22
- * Renders an input from its params: `{ type: "email", name: "email", label: "Email", required: true }` gives a
23
- * `TextBox` of type email. Forms render their `inputs` with it, and tabular and group forms render each row's inputs.
24
- * Layout keys (`span`, `width`, `grow`) stay out of the component's props; `hidden` renders nothing.
25
+ * Renders an input from its params: `{ type: EInputTypes.Email, name: "email", label: "Email", required: true }` gives a
26
+ * `TextBox` of HTML type email. Forms render their `inputs` with it, and tabular and group forms render each row's inputs.
27
+ * Layout keys (`span`, `width`, `grow`, and the Form-only ones such as `showWhen` and `compute`) stay out of the
28
+ * component's props; `hidden` renders nothing.
25
29
  */
26
30
  export function renderInput(params, overrides = {}) {
27
31
  if (params.hidden)
28
32
  return null;
29
- const { type, section: _section, span: _span, width: _width, grow: _grow, hidden: _hidden, dependsOn: _dependsOn, onValueChange: ownChange, ...rest } = params;
33
+ const { type, section: _section, span: _span, width: _width, grow: _grow, hidden: _hidden, dependsOn: _dependsOn, narrowedBy: _narrowedBy, showWhen: _showWhen, readOnlyWhen: _readOnlyWhen, remember: _remember, labelWhen: _labelWhen, spanWhen: _spanWhen, compute: _compute, onValueChange: ownChange, ...rest } = params;
30
34
  const { defaultValue, onValueChange, readOnly, disabled, ...set } = overrides;
31
35
  const props = { ...rest, ...set };
32
36
  if (readOnly)
@@ -43,59 +47,60 @@ export function renderInput(params, overrides = {}) {
43
47
  }
44
48
  if (onValueChange || ownChange) {
45
49
  props.onValueChange = (value, ...more) => {
46
- onValueChange?.(value);
50
+ // A dropdown says which item it picked; the other inputs' second argument is their event.
51
+ onValueChange?.(value, type === EInputTypes.Dropdown ? more[0] : undefined);
47
52
  ownChange?.(value, ...more);
48
53
  };
49
54
  }
55
+ if (isTextInputType(type))
56
+ return _jsx(TextBox, { ...props, type: TEXT_BOX_TYPES[type] });
50
57
  switch (type) {
51
- case "text":
52
- case "email":
53
- case "password":
54
- case "search":
55
- case "tel":
56
- case "url":
57
- return _jsx(TextBox, { ...props, type: type });
58
- case "textarea":
58
+ case EInputTypes.TextArea:
59
59
  return _jsx(TextArea, { ...props });
60
- case "number":
60
+ case EInputTypes.Number:
61
61
  return _jsx(NumberBox, { ...props });
62
- case "code":
62
+ case EInputTypes.Code:
63
63
  return _jsx(CodeBox, { ...props });
64
- case "checkbox":
64
+ case EInputTypes.CheckBox:
65
65
  return _jsx(CheckBox, { ...props });
66
- case "switch":
66
+ case EInputTypes.Switch:
67
67
  return _jsx(Switch, { ...props });
68
- case "radio":
68
+ case EInputTypes.Radio:
69
69
  return _jsx(RadioButton, { ...props });
70
- case "checkbox-group":
70
+ case EInputTypes.CheckBoxGroup:
71
71
  return _jsx(CheckBoxGroup, { ...props });
72
- case "radio-group":
72
+ case EInputTypes.RadioGroup:
73
73
  return _jsx(RadioGroup, { ...props });
74
- case "dropdown":
74
+ case EInputTypes.Dropdown:
75
75
  // Either overload: `multiple` travels in the props.
76
76
  return _jsx(Dropdown, { ...props });
77
- case "date":
77
+ case EInputTypes.Date:
78
78
  return _jsx(DatePicker, { ...props });
79
- case "date-range":
79
+ case EInputTypes.DateRange:
80
80
  return _jsx(DateRangePicker, { ...props });
81
- case "time":
81
+ case EInputTypes.Time:
82
82
  return _jsx(TimePicker, { ...props });
83
- case "date-time":
83
+ case EInputTypes.DateTime:
84
84
  return _jsx(DateTimePicker, { ...props });
85
- case "slider":
85
+ case EInputTypes.Slider:
86
86
  return _jsx(Slider, { ...props });
87
- case "range-slider":
87
+ case EInputTypes.RangeSlider:
88
88
  return _jsx(RangeSlider, { ...props });
89
- case "markdown":
89
+ case EInputTypes.Markdown:
90
90
  return _jsx(MarkdownEditor, { ...props });
91
- case "rich-text":
91
+ case EInputTypes.RichText:
92
92
  return _jsx(RichTextEditor, { ...props });
93
- case "tree":
93
+ case EInputTypes.Tree:
94
94
  return _jsx(TreeView, { ...props });
95
- case "tabular":
95
+ case EInputTypes.Tabular:
96
96
  return _jsx(TabularForm, { ...props });
97
- case "group":
97
+ case EInputTypes.Group:
98
98
  return _jsx(GroupForm, { ...props });
99
+ case EInputTypes.Picture: {
100
+ // A picture is the owner's file, not a value: the form's value and field name stay out of it.
101
+ const { defaultValue: _value, name: _name, form: _form, ...picture } = props;
102
+ return _jsx(PictureInput, { ...picture });
103
+ }
99
104
  default:
100
105
  return null;
101
106
  }
@@ -23,6 +23,12 @@ export interface MarkdownEditorProps extends NativeTextAreaProps, TextRules {
23
23
  modes?: readonly MarkdownEditorMode[];
24
24
  /** Toolbar buttons in order, `"|"` for a divider. Default `MARKDOWN_TOOLBAR`; `[]` hides them. Shortcuts work for the tools listed. */
25
25
  toolbar?: readonly MarkdownToolbarItem[];
26
+ /**
27
+ * What the buttons do when the bar is too narrow for them. `wrap` (the default): they take a row of their own and wrap.
28
+ * `menu`: the bar keeps one row and the buttons that do not fit go into a "More formatting" menu at its end, so a
29
+ * small editor (a chat's reply box on a phone) keeps its room for the text. Their shortcuts work either way.
30
+ */
31
+ toolbarOverflow?: "wrap" | "menu";
26
32
  /** Height of the text and preview, in pixels. Default 280. A double-click on the handle goes back to it. */
27
33
  height?: number;
28
34
  /** Smallest height the handle allows. Default 120. */
@@ -1,9 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { iconBlockquote, iconBold, iconCode, iconEye, iconHeading, iconItalic, iconLayoutColumns, iconLink, iconList, iconListCheck, iconListNumbers, iconMarkdown, iconPencil, iconPhoto, iconSeparatorHorizontal, iconSourceCode, iconStrikethrough, iconTable, InlineIcon, } from "nexus-icons";
3
+ import { iconBlockquote, iconBold, iconCode, iconDots, iconEye, iconHeading, iconItalic, iconLayoutColumns, iconLink, iconList, iconListCheck, iconListNumbers, iconMarkdown, iconPencil, iconPhoto, iconSeparatorHorizontal, iconSourceCode, iconStrikethrough, iconTable, InlineIcon, } from "nexus-icons";
4
4
  import { startTransition, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
6
  import { applyMarkdownTool, continueMarkdownList, linkPastedUrl, toggleMarkdownTask } from "../../Helpers/MarkdownEditing.js";
7
+ import { fitToolbar } from "../../Helpers/ToolbarFit.js";
8
+ import { Menu } from "../Documents/Menu.js";
7
9
  import { TabButtons } from "../Documents/TabButtons.js";
8
10
  import { MarkdownView } from "../Viewers/MarkdownView.js";
9
11
  import { EditorCount, useEditorShell, writeEditorCount } from "./EditorShell.js";
@@ -61,7 +63,7 @@ const ALL_MODES = ["write", "split", "preview"];
61
63
  * @example <MarkdownEditor name="description" label="Description" defaultMode="split" heightKey="room-description" maxLength={5000} />
62
64
  */
63
65
  export function MarkdownEditor(props) {
64
- const { label, hint, error, defaultMode = "write", mode: modeProp, onModeChange, modes = ALL_MODES, toolbar = MARKDOWN_TOOLBAR, height = 280, minHeight = 120, maxHeight = 960, resizable = true, heightKey, fullscreen: canFullscreen = true, breaks = true, monospace = false, showCount = true, emptyPreview = "Nothing to preview", readOnlyMessage = "Read-only: this text cannot be changed.", fieldName, validateOn, onValueChange, className, textAreaClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, onPaste, onScroll, ...textAreaProps } = props;
66
+ const { label, hint, error, defaultMode = "write", mode: modeProp, onModeChange, modes = ALL_MODES, toolbar = MARKDOWN_TOOLBAR, toolbarOverflow = "wrap", height = 280, minHeight = 120, maxHeight = 960, resizable = true, heightKey, fullscreen: canFullscreen = true, breaks = true, monospace = false, showCount = true, emptyPreview = "Nothing to preview", readOnlyMessage = "Read-only: this text cannot be changed.", fieldName, validateOn, onValueChange, className, textAreaClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, onPaste, onScroll, ...textAreaProps } = props;
65
67
  const [modeState, setModeState] = useState(() => (modes.includes(defaultMode) ? defaultMode : (modes[0] ?? "write")));
66
68
  const mode = modeProp ?? modeState;
67
69
  const previewRef = useRef(null);
@@ -87,6 +89,30 @@ export function MarkdownEditor(props) {
87
89
  const tools = new Set(toolbar.filter((item) => item !== "|"));
88
90
  const firstTool = toolbar.findIndex(item => item !== "|");
89
91
  const editable = !readOnly && !disabled;
92
+ // One row: the buttons that fit the room the bar leaves them, measured with the row's own sizes (a cozy page's are
93
+ // larger), the rest in the "More formatting" menu. The row's width comes from the bar, never from its buttons, so
94
+ // drawing fewer of them does not change what was measured.
95
+ const toolsRef = useRef(null);
96
+ const [fit, setFit] = useState(null);
97
+ const showsTools = editable && mode !== "preview" && firstTool >= 0;
98
+ useLayoutEffect(() => {
99
+ const row = toolsRef.current;
100
+ if (toolbarOverflow !== "menu" || !showsTools || !row) {
101
+ setFit(null);
102
+ return;
103
+ }
104
+ const measure = () => {
105
+ const style = getComputedStyle(row);
106
+ const tool = parseFloat(style.getPropertyValue("--nx-control-sm")) || 28;
107
+ const next = fitToolbar(toolbar, row.clientWidth, { tool, divider: 11, gap: parseFloat(style.columnGap) || 2 });
108
+ setFit(held => (held && held.shown.length === next.shown.length && held.hidden.length === next.hidden.length ? held : next));
109
+ };
110
+ measure();
111
+ const observer = new ResizeObserver(measure);
112
+ observer.observe(row);
113
+ return () => observer.disconnect();
114
+ }, [toolbarOverflow, showsTools, toolbar]);
115
+ const shownTools = fit ? fit.shown : toolbar;
90
116
  const shell = useEditorShell({
91
117
  height,
92
118
  minHeight,
@@ -210,11 +236,11 @@ export function MarkdownEditor(props) {
210
236
  onScroll?.(event);
211
237
  follow(event.currentTarget, previewRef.current?.element);
212
238
  }
213
- return (_jsxs(_Fragment, { children: [_jsx(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, hint: hint, error: field.error, disabled: disabled, readOnly: readOnly, multiline: true, className: cx("nx-editor", "nx-md", monospace && "nx-md--mono", className), style: style, children: _jsxs("div", { ref: shell.frameRef, className: "nx-editor__frame", "data-fullscreen": shell.fullscreen || undefined, onKeyDown: shell.onFrameKeyDown, children: [_jsxs("div", { className: "nx-editor__bar", children: [modes.length > 1 && (_jsx(TabButtons, { "aria-label": "View", size: "sm", labels: "selected", options: modes.map(option => ({ value: option, label: MODES[option].label, icon: _jsx(InlineIcon, { icon: MODES[option].icon }), disabled })), value: mode, onChange: changeMode,
239
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, hint: hint, error: field.error, disabled: disabled, readOnly: readOnly, multiline: true, className: cx("nx-editor", "nx-md", monospace && "nx-md--mono", className), style: style, children: _jsxs("div", { ref: shell.frameRef, className: "nx-editor__frame", "data-fullscreen": shell.fullscreen || undefined, onKeyDown: shell.onFrameKeyDown, children: [_jsxs("div", { className: cx("nx-editor__bar", toolbarOverflow === "menu" && "nx-editor__bar--one-row"), children: [modes.length > 1 && (_jsx(TabButtons, { "aria-label": "View", size: "sm", labels: "selected", options: modes.map(option => ({ value: option, label: MODES[option].label, icon: _jsx(InlineIcon, { icon: MODES[option].icon }), disabled })), value: mode, onChange: changeMode,
214
240
  // An id that matches no form: the view is never sent with the form around the editor.
215
- form: `${field.controlId}-view`, className: "nx-editor__modes" })), editable && mode !== "preview" && firstTool >= 0 && (_jsx("div", { role: "toolbar", "aria-label": "Formatting", "aria-controls": field.controlId, className: "nx-editor__tools", onKeyDown: shell.moveTool, children: toolbar.map((item, index) => item === "|" ? (_jsx("span", { className: "nx-editor__divider", "aria-hidden": "true" }, index)) : (_jsx("button", { type: "button", className: "nx-editor__tool", tabIndex: index === firstTool ? 0 : -1, "aria-label": TOOLS[item].label, "aria-keyshortcuts": TOOLS[item].keys, title: TOOLS[item].keys ? `${TOOLS[item].label} (${TOOLS[item].keys.replace("Control", "Ctrl")})` : TOOLS[item].label,
216
- // The text area keeps focus and its selection.
217
- onMouseDown: event => event.preventDefault(), onClick: () => runTool(item), children: _jsx(InlineIcon, { icon: TOOLS[item].icon }) }, index))) })), _jsxs("span", { className: "nx-editor__end", children: [readOnlyNotice.lock, shell.fullscreenButton] })] }), _jsxs("div", { ref: shell.bodyRef, className: "nx-editor__body", "data-mode": mode, style: shell.bodyStyle, children: [_jsx("textarea", { ...textAreaProps, ...field.controlProps, className: cx("nx-field__control", "nx-md__input", textAreaClassName), disabled: disabled, readOnly: readOnly, onClick: handleClick, onBlur: handleBlur, onKeyDown: handleKeyDown, onPaste: handlePaste, onInvalid: handleInvalid, onScroll: handleScroll }), mode !== "write" && (_jsx(MarkdownPreview, { ref: previewRef, initialText: initialText(), breaks: breaks, empty: emptyPreview, onTaskToggle: editable ? toggleTask : undefined, onScroll: event => follow(event.currentTarget, field.elementRef.current), onRender: () => {
241
+ form: `${field.controlId}-view`, className: "nx-editor__modes" })), showsTools && (_jsxs("div", { ref: toolsRef, role: "toolbar", "aria-label": "Formatting", "aria-controls": field.controlId, className: "nx-editor__tools", onKeyDown: shell.moveTool, children: [shownTools.map((item, index) => item === "|" ? (_jsx("span", { className: "nx-editor__divider", "aria-hidden": "true" }, index)) : (_jsx("button", { type: "button", className: "nx-editor__tool", tabIndex: index === firstTool ? 0 : -1, "aria-label": TOOLS[item].label, "aria-keyshortcuts": TOOLS[item].keys, title: TOOLS[item].keys ? `${TOOLS[item].label} (${TOOLS[item].keys.replace("Control", "Ctrl")})` : TOOLS[item].label,
242
+ // The text area keeps focus and its selection.
243
+ onMouseDown: event => event.preventDefault(), onClick: () => runTool(item), children: _jsx(InlineIcon, { icon: TOOLS[item].icon }) }, index))), fit && fit.hidden.length > 0 && (_jsx(Menu, { items: fit.hidden.map(tool => ({ value: tool, label: TOOLS[tool].label, icon: _jsx(InlineIcon, { icon: TOOLS[tool].icon }), shortcut: TOOLS[tool].keys?.replace("Control", "Ctrl") })), align: "end", size: "sm", "aria-label": "More formatting", onSelect: tool => runTool(tool), children: _jsx("button", { type: "button", className: "nx-editor__tool", tabIndex: -1, "aria-label": "More formatting", title: "More formatting", children: _jsx(InlineIcon, { icon: iconDots }) }) }))] })), _jsxs("span", { className: "nx-editor__end", children: [readOnlyNotice.lock, shell.fullscreenButton] })] }), _jsxs("div", { ref: shell.bodyRef, className: "nx-editor__body", "data-mode": mode, style: shell.bodyStyle, children: [_jsx("textarea", { ...textAreaProps, ...field.controlProps, className: cx("nx-field__control", "nx-md__input", textAreaClassName), disabled: disabled, readOnly: readOnly, onClick: handleClick, onBlur: handleBlur, onKeyDown: handleKeyDown, onPaste: handlePaste, onInvalid: handleInvalid, onScroll: handleScroll }), mode !== "write" && (_jsx(MarkdownPreview, { ref: previewRef, initialText: initialText(), breaks: breaks, empty: emptyPreview, onTaskToggle: editable ? toggleTask : undefined, onScroll: event => follow(event.currentTarget, field.elementRef.current), onRender: () => {
218
244
  if (field.elementRef.current)
219
245
  follow(field.elementRef.current, previewRef.current?.element);
220
246
  } }))] }), shell.footer(_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconMarkdown }), _jsx("span", { children: "Markdown" })] }), showCount && _jsx(EditorCount, { ref: countRef, text: initialText(), maxLength: maxLength }))] }) }), readOnlyNotice.notice] }));
@@ -0,0 +1,41 @@
1
+ import { type CSSProperties, type ReactNode } from "react";
2
+ import type { ApiModuleValue } from "../../Interfaces/ApiInterfaces.ts";
3
+ import type { Size } from "../../Interfaces/TypeInterfaces.ts";
4
+ import { type OwnerFilesParams } from "../Viewers/OwnerFiles.tsx";
5
+ export interface PictureInputProps extends Omit<OwnerFilesParams<ApiModuleValue>, "ownerId"> {
6
+ /**
7
+ * The owner's id: the record the form edits (`row?.id` in a CRUD's inputs). Null or missing for a record not saved
8
+ * yet, which has no pictures until it is: the input says so.
9
+ */
10
+ ownerId?: string | null;
11
+ label?: ReactNode;
12
+ /**
13
+ * Help text under the picture. Default: what the category takes, once it is known. There is no `required`: the
14
+ * picture is saved at once, apart from the form, so a form could not refuse a save without one.
15
+ */
16
+ hint?: ReactNode;
17
+ /** The preview's height: `sm` 72px, `md` 112px, `lg` 152px. Default `md`. */
18
+ size?: Size;
19
+ disabled?: boolean;
20
+ /** What the record is, for the note a new one shows: "branch". Default "record". */
21
+ subject?: string;
22
+ id?: string;
23
+ className?: string;
24
+ style?: CSSProperties;
25
+ "aria-label"?: string;
26
+ }
27
+ /**
28
+ * A record's picture in a form: the owner's main picture, with Upload (a new one, edited first, made the main picture),
29
+ * Edit (the main one, saved as a new picture), Remove (no main picture; the pictures stay) and, when the category keeps
30
+ * more than one, All pictures (the gallery). In a category that keeps one file, Upload is Replace and Remove deletes it.
31
+ * Clicking or pressing the picture opens the pictures window too, read only where the input is. The change is saved at
32
+ * once, not with the form: a picture is the owner's file, not one of the record's values, so the form keeps no field for
33
+ * it and sends nothing under its name.
34
+ *
35
+ * @example
36
+ * inputs: (action, row) => [
37
+ * { type: EInputTypes.Picture, name: "logo", label: "Logo", module: NexusModule.Enterprise, category: "company-logo", ownerId: row?.id, aspect: 1 },
38
+ * ...
39
+ * ]
40
+ */
41
+ export declare function PictureInput({ label, hint, size, disabled, readOnly, subject, id, className, style, "aria-label": ariaLabel, ...params }: PictureInputProps): import("react").JSX.Element;
@@ -0,0 +1,75 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { iconPencil, iconPhoto, iconTrash, iconUpload, InlineIcon } from "nexus-icons";
4
+ import { useId, useRef, useState } from "react";
5
+ import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { allowsFileChange } from "../../Helpers/FileAdapter.js";
7
+ import { getFileAdapter } from "../../Services/FileApi.js";
8
+ import { preloadImageEditor } from "../../Services/ImageEditorLoader.js";
9
+ import { Button } from "../Documents/Button.js";
10
+ import { busyProps, openPictureGallery, useFocusThroughChange, useOwnerFiles } from "../Viewers/OwnerFiles.js";
11
+ import { ChoiceLegend, GroupFooter } from "./ChoiceField.js";
12
+ const HEIGHTS = { sm: 72, md: 112, lg: 152 };
13
+ /**
14
+ * A record's picture in a form: the owner's main picture, with Upload (a new one, edited first, made the main picture),
15
+ * Edit (the main one, saved as a new picture), Remove (no main picture; the pictures stay) and, when the category keeps
16
+ * more than one, All pictures (the gallery). In a category that keeps one file, Upload is Replace and Remove deletes it.
17
+ * Clicking or pressing the picture opens the pictures window too, read only where the input is. The change is saved at
18
+ * once, not with the form: a picture is the owner's file, not one of the record's values, so the form keeps no field for
19
+ * it and sends nothing under its name.
20
+ *
21
+ * @example
22
+ * inputs: (action, row) => [
23
+ * { type: EInputTypes.Picture, name: "logo", label: "Logo", module: NexusModule.Enterprise, category: "company-logo", ownerId: row?.id, aspect: 1 },
24
+ * ...
25
+ * ]
26
+ */
27
+ export function PictureInput({ label, hint, size = "md", disabled = false, readOnly = false, subject = "record", id, className, style, "aria-label": ariaLabel, ...params }) {
28
+ const generated = useId();
29
+ const fieldId = id ?? generated;
30
+ const [failed, setFailed] = useState(null);
31
+ const files = useOwnerFiles({ ...params, readOnly: readOnly || disabled });
32
+ const { status, data, busy, canWrite, readOnlyReason, changes, changesNote, offer, editorProblem, actions } = files;
33
+ // A keyboard's focus stays in the input through a change: on the button pressed, or the first left when it went.
34
+ const rootRef = useRef(null);
35
+ useFocusThroughChange(rootRef, busy);
36
+ const primary = data?.files.find(file => file.isPrimary) ?? null;
37
+ const url = primary ? getFileAdapter().fileUrl(primary, "thumbnail") : null;
38
+ const shown = url && failed !== url;
39
+ const ratio = typeof params.aspect === "number" && params.aspect > 0 ? params.aspect : 1;
40
+ const preview = { "--nx-picture-h": `${HEIGHTS[size]}px`, "--nx-picture-ratio": ratio };
41
+ const unsaved = !params.ownerId;
42
+ // A category that keeps one file: a new picture takes its place, and Remove deletes it (there is nothing else to show).
43
+ const single = data?.category.maxCount === 1;
44
+ const kept = data?.files ?? [];
45
+ // What this input offers, within the changes allowed: its Upload makes the new picture the main one, so it needs both.
46
+ const offered = offer(primary);
47
+ const canUpload = offered.upload && (single || allowsFileChange(changes, "primary"));
48
+ const canRemove = Boolean(primary) && (single ? offered.remove : offered.clearPrimary);
49
+ const note = unsaved
50
+ ? `Save the ${subject} first, then add its picture.`
51
+ : status.kind === "failed"
52
+ ? status.message
53
+ : busy
54
+ ? `${busy}…`
55
+ : editorProblem
56
+ ? // Why Upload or Edit did not open the editor, until a try opens it: never a button that does nothing.
57
+ editorProblem
58
+ : // Why it cannot be changed, when the page or the backend says (`lockReason`, a refused key): never a silent input.
59
+ (readOnlyReason ?? changesNote ?? hint ?? (data && canWrite ? `${data.files.length} of ${data.category.maxCount} ${single ? "picture" : "pictures"} kept.` : undefined));
60
+ const problem = status.kind === "failed" || (!unsaved && !busy && Boolean(editorProblem));
61
+ const named = typeof label === "string" && label ? label : "Picture";
62
+ /** The pictures window, as All pictures opens it: what changes there is read again here. */
63
+ const openAll = () => openPictureGallery({
64
+ ...params,
65
+ ownerId: params.ownerId,
66
+ readOnly: readOnly || disabled,
67
+ onChange: primary => {
68
+ void actions.reload();
69
+ params.onChange?.(primary);
70
+ },
71
+ });
72
+ const pictureClass = cx("nx-picture", params.shape === "circle" && "nx-picture--round", !shown && "nx-picture--empty", status.kind === "loading" && "nx-picture--loading");
73
+ const face = shown ? _jsx("img", { src: url, alt: "", decoding: "async", onError: () => setFailed(url) }) : _jsx(InlineIcon, { icon: iconPhoto });
74
+ return (_jsxs("fieldset", { ref: rootRef, id: fieldId, className: cx("nx-choices", size !== "md" && `nx-choices--${size}`, "nx-picture-input", className), style: style, disabled: disabled, "aria-label": ariaLabel, "aria-busy": status.kind === "loading" || Boolean(busy) || undefined, "aria-describedby": note ? `${fieldId}-message` : undefined, "data-readonly": readOnly || undefined, children: [_jsx(ChoiceLegend, { label: label }), _jsxs("div", { className: "nx-picture-input__body", children: [!unsaved && data ? (_jsx("button", { type: "button", className: pictureClass, style: preview, "aria-label": primary ? `${named}: open the pictures` : `${named}: none yet, open the pictures`, "aria-haspopup": "dialog", title: "All pictures", ...busyProps(busy, openAll), onPointerEnter: preloadImageEditor, children: face })) : (_jsx("span", { className: pictureClass, style: preview, role: "img", "aria-label": primary ? `${named}: ${primary.name}` : "No picture", children: face })), !unsaved && data && (_jsxs("div", { className: "nx-picture-input__actions", role: "group", children: [canUpload && (_jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconUpload }), ...busyProps(busy, () => void actions.replace()), onPointerEnter: preloadImageEditor, onFocus: preloadImageEditor, children: single && kept.length > 0 ? "Replace" : "Upload" })), primary && offered.edit && (_jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconPencil }), ...busyProps(busy, () => void actions.edit(primary)), onPointerEnter: preloadImageEditor, onFocus: preloadImageEditor, children: "Edit" })), canRemove && primary && (_jsx(Button, { size: "sm", variant: "ghost", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), ...busyProps(busy, () => void (single ? actions.remove(primary) : actions.makePrimary(null))), children: single ? "Delete" : "Remove" })), (data.category.maxCount > 1 || kept.length > 1 || (kept.length > 0 && !primary)) && (_jsxs(Button, { size: "sm", variant: "ghost", ...busyProps(busy, openAll), children: ["All pictures (", data.files.length, ")"] }))] }))] }), _jsx(GroupFooter, { id: `${fieldId}-message`, hint: problem ? undefined : note, error: problem ? note : undefined })] }));
75
+ }
@@ -1,7 +1,19 @@
1
1
  import type { ChangeEvent } from "react";
2
- import type { OptionValue } from "../../Interfaces/InputInterfaces.ts";
2
+ import type { ChoiceDesign, OptionValue } from "../../Interfaces/InputInterfaces.ts";
3
3
  import { type ChoiceGroupProps } from "./ChoiceField.tsx";
4
- export interface RadioGroupProps<T extends OptionValue = string> extends ChoiceGroupProps<T> {
4
+ /**
5
+ * How a `RadioGroup` draws its options: `default` (a circle beside each label), `cards` (a card each, with its
6
+ * description), or `tabs` (nav-style tabs on one track, as `TabButtons` draws them).
7
+ */
8
+ export type RadioGroupDesign = ChoiceDesign | "tabs";
9
+ export interface RadioGroupProps<T extends OptionValue = string> extends Omit<ChoiceGroupProps<T>, "design"> {
10
+ /**
11
+ * `default`: a circle beside each label. `cards`: each option is a card. `tabs`: the options as nav-style tabs on one
12
+ * track (`TabButtons`' look), wrapping onto more rows where they do not fit; each option's `description` is its
13
+ * tooltip and what a screen reader hears with it, and the chosen option's shows under the track. `orientation` and
14
+ * `columns` do not apply to tabs.
15
+ */
16
+ design?: RadioGroupDesign;
5
17
  /** The chosen value, or `null` for none. With `value`, the group is controlled; pair it with `onValueChange`. */
6
18
  value?: T | null;
7
19
  defaultValue?: T | null;
@@ -12,7 +24,8 @@ export interface RadioGroupProps<T extends OptionValue = string> extends ChoiceG
12
24
  }
13
25
  /**
14
26
  * One choice from a list of options, such as a room type or a payment method. Native radio buttons, so the arrow keys
15
- * move the choice. `design="cards"` shows each option as a card with its description. Read-only (default note
27
+ * move the choice. `design="cards"` shows each option as a card with its description, `design="tabs"` as nav-style tabs
28
+ * whose descriptions are tooltips (the chosen one's under the track). Read-only (default note
16
29
  * "Read-only: this choice cannot be changed.") shows a lock after the label and a note under a clicked option.
17
30
  *
18
31
  * @example <RadioGroup name="roomType" label="Room type" required design="cards" options={[{ value: "deluxe", label: "Deluxe" }]} />
@@ -1,11 +1,13 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { validateSelection } from "../../Helpers/ValidationHelpers.js";
4
- import { ChoiceItem, ChoiceLegend, GroupFooter, groupLayout, leavesField, OPTION_ATTRIBUTE, optionAnchor, useChoiceField } from "./ChoiceField.js";
4
+ import { cx } from "../../Helpers/ClassHelpers.js";
5
+ import { ChoiceItem, ChoiceLegend, ChoiceTab, ChoiceTabNotes, GroupFooter, groupLayout, leavesField, OPTION_ATTRIBUTE, optionAnchor, showChosenNote, useChoiceField } from "./ChoiceField.js";
5
6
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
6
7
  /**
7
8
  * One choice from a list of options, such as a room type or a payment method. Native radio buttons, so the arrow keys
8
- * move the choice. `design="cards"` shows each option as a card with its description. Read-only (default note
9
+ * move the choice. `design="cards"` shows each option as a card with its description, `design="tabs"` as nav-style tabs
10
+ * whose descriptions are tooltips (the chosen one's under the track). Read-only (default note
9
11
  * "Read-only: this choice cannot be changed.") shows a lock after the label and a note under a clicked option.
10
12
  *
11
13
  * @example <RadioGroup name="roomType" label="Room type" required design="cards" options={[{ value: "deluxe", label: "Deluxe" }]} />
@@ -14,6 +16,7 @@ export function RadioGroup(props) {
14
16
  const { options, name, label, hint, required, validate, value, defaultValue, readOnly = false, disabled, onValueChange } = props;
15
17
  const { readOnlyMessage = "Read-only: this choice cannot be changed." } = props;
16
18
  const { size = "md" } = props;
19
+ const tabs = props.design === "tabs";
17
20
  const chosen = (inputs) => {
18
21
  const input = inputs.find(item => item.checked);
19
22
  return input ? [options[Number(input.getAttribute(OPTION_ATTRIBUTE))].value] : [];
@@ -21,6 +24,8 @@ export function RadioGroup(props) {
21
24
  const field = useChoiceField(props, {
22
25
  check: (inputs, fieldName) => validateSelection(chosen(inputs), { required, validate: validate && (values => validate(values[0] ?? null)) }, fieldName),
23
26
  checkKeys: [value, required, validate, options],
27
+ // Tabs show the chosen option's description under the track: it follows every change and a form's reset.
28
+ sync: tabs ? showChosenNote : undefined,
24
29
  });
25
30
  function handleChange(event) {
26
31
  if (readOnly)
@@ -39,5 +44,5 @@ export function RadioGroup(props) {
39
44
  }
40
45
  const invalid = Boolean(field.error);
41
46
  const layout = groupLayout(props);
42
- return (_jsxs("fieldset", { ref: field.containerRef, id: field.fieldId, role: "radiogroup", className: layout.className, style: props.style, disabled: disabled, "data-invalid": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"], "aria-describedby": field.error || hint ? field.messageId : undefined, "aria-required": required || undefined, "aria-invalid": invalid || undefined, "aria-readonly": readOnly || undefined, onChange: handleChange, onClick: readOnlyNotice.onClick, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [_jsx(ChoiceLegend, { label: label, required: required, lock: readOnlyNotice.lock }), _jsx("div", { className: "nx-choices__options", style: layout.optionsStyle, children: options.map((option, index) => (_jsx(ChoiceItem, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, size: size, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) }), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
47
+ return (_jsxs("fieldset", { ref: field.containerRef, id: field.fieldId, role: "radiogroup", className: layout.className, style: props.style, disabled: disabled, "data-invalid": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"], "aria-describedby": field.error || hint ? field.messageId : undefined, "aria-required": required || undefined, "aria-invalid": invalid || undefined, "aria-readonly": readOnly || undefined, onChange: handleChange, onClick: readOnlyNotice.onClick, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [_jsx(ChoiceLegend, { label: label, required: required, lock: readOnlyNotice.lock }), tabs ? (_jsx("div", { className: cx("nx-tabs", `nx-tabs--${size}`, "nx-tabs--wrap"), children: options.map((option, index) => (_jsx(ChoiceTab, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) })) : (_jsx("div", { className: "nx-choices__options", style: layout.optionsStyle, children: options.map((option, index) => (_jsx(ChoiceItem, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, size: size, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) })), tabs && _jsx(ChoiceTabNotes, { groupId: field.fieldId, options: options, chosen: options.findIndex(option => option.value === (value !== undefined ? value : defaultValue)) }), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
43
48
  }
@@ -1,8 +1,9 @@
1
1
  import { type FormEvent, type ReactNode } from "react";
2
2
  import { RowsStore, type RowsSnapshot } from "../../Helpers/RowsStore.ts";
3
- import type { InputParams, RowsInputProps, RowValue } from "../../Interfaces/FormInterfaces.ts";
3
+ import { type InputParams, type RowsInputProps, type RowValue } from "../../Interfaces/FormInterfaces.ts";
4
4
  import type { InputDesign } from "../../Interfaces/InputInterfaces.ts";
5
5
  import type { Size } from "../../Interfaces/TypeInterfaces.ts";
6
+ import { type ComputedNote } from "./ComputedInput.tsx";
6
7
  /** Form names of inputs inside rows that need one (radio and checkbox groups). They are taken out of the form's data. */
7
8
  export declare const ROW_INPUT_NAME = "nx-rows:";
8
9
  /** Where focus goes in a row once it is on the page: an input by name, the row's first input, or its actions button. */
@@ -37,6 +38,7 @@ export declare function useRowsInput<TRow extends RowValue>(props: RowsInputProp
37
38
  cellLock: (key: string, name: string, index: number) => string | boolean | null | undefined;
38
39
  rowLock: (key: string | null, index: number) => string | boolean | null | undefined;
39
40
  cellParams: (key: string, params: InputParams, index: number) => InputParams;
41
+ computeWith: () => unknown;
40
42
  };
41
43
  /** What a layout asks for each cell: the reason it is generated, `true` for no reason, or nothing. */
42
44
  export type CellLock = (key: string, name: string, index: number) => string | boolean | null | undefined;
@@ -44,6 +46,15 @@ export type CellLock = (key: string, name: string, index: number) => string | bo
44
46
  export type CellParams = (key: string, params: InputParams, index: number) => InputParams;
45
47
  /** What a layout asks for each row: the reason nobody may change it, `true` for no reason, or nothing. From `rowReadOnly`. */
46
48
  export type RowLock = (key: string | null, index: number) => string | boolean | null | undefined;
49
+ /** What a row input worked out from others (`compute`) asks its layout: the other cells' params and locks, `computeWith`. */
50
+ export interface RowCellContext {
51
+ cellParams: CellParams;
52
+ cellLock: CellLock;
53
+ /** The rows' `computeWith` as it is now. */
54
+ computeWith?: () => unknown;
55
+ /** A typed-in figure's note, for a table to say under its rows (`null` once it no longer applies). */
56
+ onNote?: (key: string, name: string, note: ComputedNote | null) => void;
57
+ }
47
58
  /** The first control of an element that takes focus. */
48
59
  export declare function firstFocusable(element: Element | null): HTMLElement | null;
49
60
  /** Subscribes a row's component to its row: it renders again only when the row's messages or code-set values change. */
@@ -66,13 +77,15 @@ export interface RowCellProps {
66
77
  disabled: boolean;
67
78
  /** This one cell holds a generated value: the reason, `true` for no reason, or nothing. From `cellReadOnly`. */
68
79
  lock?: string | boolean | null;
80
+ /** The layout's own, for a cell worked out from the row's others (`compute`). */
81
+ cells?: RowCellContext;
69
82
  }
70
83
  /**
71
84
  * One input of one row. It starts from the store's value when it mounts and writes every change back, so typing renders
72
85
  * nothing; a value set from code mounts it again (its key holds the value's revision). In a table the column header is
73
86
  * the label, so the input is named "City, row 2" for screen readers and its messages still say "City".
74
87
  */
75
- export declare const RowCell: import("react").MemoExoticComponent<({ store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled, lock }: RowCellProps) => ReactNode>;
88
+ export declare const RowCell: import("react").MemoExoticComponent<(props: RowCellProps) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined>;
76
89
  /** The legend of the rows, with the required mark and, when read-only, a lock. */
77
90
  export declare function RowsLegend({ id, label, required, lock }: {
78
91
  id: string;