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
@@ -5,16 +5,18 @@ import { useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, us
5
5
  import { flushSync } from "react-dom";
6
6
  import { cx } from "../../Helpers/ClassHelpers.js";
7
7
  import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
8
+ import { canCall, permissionsVersion, subscribePermissions } from "../../Helpers/Permissions.js";
8
9
  import { fitColumnWidth, headLeafCells, startColumnDrag, startColumnResize } from "../../Helpers/TableColumns.js";
9
10
  import { exportTableCsv, printTable } from "../../Helpers/TableExport.js";
10
11
  import { asFilterInput, filterNames, filterRequest } from "../../Helpers/TableFilters.js";
11
12
  import { joinKeys, joinSourceName, tableJoins } from "../../Helpers/TableJoins.js";
12
- import { canPinColumn, columnAlign, columnValue, cssLength, DEFAULT_MAX_SELECTION, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, formatCellText, headBands, MIN_COLUMN_WIDTH, orderColumns, TABLE_ACTIONS_COLUMN, TABLE_INDEX_COLUMN, viewColumns, } from "../../Helpers/TableHelpers.js";
13
+ import { canPinColumn, columnAlign, columnValue, cssLength, DEFAULT_MAX_SELECTION, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, formatCellText, headBands, leadColumns, namingColumn, MIN_COLUMN_WIDTH, orderColumns, releasedPins, TABLE_ACTIONS_COLUMN, TABLE_INDEX_COLUMN, viewColumns, } from "../../Helpers/TableHelpers.js";
13
14
  import { TableStore } from "../../Helpers/TableStore.js";
14
15
  import { readUserPreferences, useNumberLocale, useUserPreferences } from "../../Helpers/UserPreferences.js";
15
16
  import { Button } from "../Documents/Button.js";
16
17
  import { Menu } from "../Documents/Menu.js";
17
18
  import { SplitButton } from "../Documents/SplitButton.js";
19
+ import { useReadOnlyNotice } from "../Inputs/ReadOnlyNotice.js";
18
20
  import { TextBox } from "../Inputs/TextBox.js";
19
21
  import { toast } from "../Layouts/Toaster.js";
20
22
  import { downloadExcelTemplate, exportExcel } from "../../Services/ExcelApi.js";
@@ -41,8 +43,10 @@ export function useTable() {
41
43
  const STATE_PREFIX = "nx-table:";
42
44
  /** The key of the section `pinnedRows` makes. */
43
45
  const PINS_SECTION = "pins";
44
- /** The lookups' version for a table with no `join` column, which never listens to them. */
45
- const NO_JOINS = () => 0;
46
+ /** A version that never changes: the lookups' for a table with no `join` column, the permissions' for one with no sheet. */
47
+ const NO_VERSION = () => 0;
48
+ /** The page size of a table read whole (`pager="none"`): every row on the first page. */
49
+ const WHOLE_PAGE = Number.MAX_SAFE_INTEGER;
46
50
  function readSaved(key) {
47
51
  if (!key || typeof localStorage === "undefined")
48
52
  return null;
@@ -77,6 +81,9 @@ function usePerRow(make) {
77
81
  return value;
78
82
  }, [cache, make]);
79
83
  }
84
+ /** No pinned column scrolls: the table is wide enough for them all. */
85
+ const NONE_RELEASED = new Set();
86
+ const sameKeys = (a, b) => a.size === b.size && [...a].every(key => b.has(key));
80
87
  const sameNumbers = (a, b) => {
81
88
  const keys = Object.keys(a);
82
89
  return keys.length === Object.keys(b).length && keys.every(key => a[key] === b[key]);
@@ -99,17 +106,24 @@ const sameNumbers = (a, b) => {
99
106
  * />
100
107
  */
101
108
  export function DataTable(props) {
102
- const { columns, rows: givenRows, load, loadKey, rowKey = "id", rowLabel, table: givenTable, searchable = true, searchPlaceholder = "Search…", toolbar, actions, add, columnsMenu = true, selectable = false, maxSelection = DEFAULT_MAX_SELECTION, onSelectionChange, massActions, selectBy, rowActions, onRowOpen, openOnClick: ownOpenOnClick, sections: ownSections, rowMarks, pinnedRows, pinnedLoading = false, pinnedLabel = "Pinned", pinnedMax, onReload, reorderable = true, resizable = true, pinnable = true, moreActions, exportable = true, printable = true, exportName, onImport, excel, pageSize: ownPageSize, pageSizes = DEFAULT_PAGE_SIZES, defaultSort = null, stateKey, emptyMessage = "No records yet.", emptyAction, maxHeight, fill = false, size = "md", id, className, style, ref, } = props;
109
+ const { columns: givenColumns, rows: givenRows, load, loadKey, rowKey = "id", rowLabel, table: givenTable, searchable = true, searchPlaceholder = "Search…", caption, toolbar, actions, add, columnsMenu = true, selectable = false, maxSelection = DEFAULT_MAX_SELECTION, onSelectionChange, massActions, selectBy, rowActions, onRowOpen, openOnClick: ownOpenOnClick, onPick, sections: ownSections, rowMarks, rowStrong, pinnedRows, pinnedLoading = false, pinnedLabel = "Pinned", pinnedMax, onReload, reorderable = true, resizable = true, pinnable = true, moreActions, exportable = true, printable = true, exportName, onImport, excel, pageSize: ownPageSize, pageSizes = DEFAULT_PAGE_SIZES, defaultSort = null, stateKey, currency, emptyMessage = "No records yet.", emptyAction, maxHeight, fill = false, size = "md", id, className, style, ref, } = props;
103
110
  const paging = givenRows ? "client" : (props.paging ?? "client");
111
+ // A server's pages cannot all be shown at once: such a table keeps its paging in the footer instead.
112
+ const pager = props.pager === "none" && paging === "server" ? "footer" : (props.pager ?? "bar");
113
+ const whole = pager === "none";
104
114
  const reloadable = props.reloadable ?? Boolean(load);
105
115
  const searchDelay = props.searchDelay ?? (paging === "server" ? 300 : 0);
106
- // A table with checkboxes numbers its rows: the number becomes the checkbox as soon as the row is under the pointer.
107
- const numbered = props.numbered ?? selectable;
116
+ // A table with checkboxes numbers its rows: the number becomes the checkbox as soon as the row is under the pointer. So
117
+ // does one whose rows are picked, whose first column holds each row's radio button.
118
+ const pickable = Boolean(onPick);
119
+ const numbered = props.numbered ?? (selectable || pickable);
108
120
  // The person's choices in the quick preference panel, once made, go over what the table names itself.
109
121
  const preferences = useUserPreferences();
110
122
  const openOnClick = preferences.rowClick ?? ownOpenOnClick ?? false;
111
- /** The page size the table opens with: the person's default when this table offers it, else the table's own. */
123
+ /** The page size the table opens with: every row for a table read whole, the person's default when this table offers it, else the table's own. */
112
124
  const openingPageSize = () => {
125
+ if (whole)
126
+ return WHOLE_PAGE;
113
127
  const preferred = readUserPreferences().pageSize;
114
128
  return preferred !== null && pageSizes.includes(preferred) ? preferred : (ownPageSize ?? DEFAULT_PAGE_SIZE);
115
129
  };
@@ -117,20 +131,26 @@ export function DataTable(props) {
117
131
  const table = givenTable ?? own;
118
132
  // Prepared once for the store and the panel: the same params without what would make them filter on their own.
119
133
  const filterInputs = useMemo(() => props.filterInputs?.map(asFilterInput), [props.filterInputs]);
120
- table.configure({ columns, rowKey, rows: givenRows, load, paging, maxSelection, onSelectionChange, filterInputs });
134
+ // A picture or a logo (`lead`) first of the page's columns, as every table draws it until the user moves it.
135
+ const columns = useMemo(() => leadColumns(givenColumns), [givenColumns]);
136
+ table.configure({ columns, rowKey, rows: givenRows, load, paging, maxSelection, onSelectionChange, onPickChange: onPick, filterInputs });
121
137
  useImperativeHandle(ref, () => table, [table]);
122
138
  const state = useSyncExternalStore(table.subscribe, table.getState, table.getState);
123
139
  // The columns whose values are other records', looked up by key (`join`). Only a table with one listens to the page's
124
140
  // lookups, and it draws again - its rows too, through `cellOptions` - each time names come in.
125
141
  const joins = useMemo(() => columns.flatMap(column => (column.join ? [column.join] : [])), [columns]);
126
142
  const subscribeJoins = useCallback((listener) => (joins.length ? tableJoins.subscribe(listener) : () => { }), [joins.length]);
127
- const joinsVersion = useSyncExternalStore(subscribeJoins, joins.length ? tableJoins.getVersion : NO_JOINS, NO_JOINS);
143
+ const joinsVersion = useSyncExternalStore(subscribeJoins, joins.length ? tableJoins.getVersion : NO_VERSION, NO_VERSION);
144
+ // A spreadsheet's items ask the app's permissions, as a CRUD's actions do, so only a table with one listens for them.
145
+ const hasSheet = Boolean(excel);
146
+ const subscribeSheet = useCallback((listener) => (hasSheet ? subscribePermissions(listener) : () => { }), [hasSheet]);
147
+ useSyncExternalStore(subscribeSheet, hasSheet ? permissionsVersion : NO_VERSION, NO_VERSION);
128
148
  const dates = useDatePreferences();
129
149
  const timeZone = useBrowserTimeZone();
130
150
  const numbers = useNumberLocale();
131
151
  // `joinsVersion` is read by no cell option: a new object is what tells the memoized rows that their names came in.
132
152
  // eslint-disable-next-line react-hooks/exhaustive-deps
133
- const cellOptions = useMemo(() => ({ preferences: dates, timeZone: timeZone ?? undefined, locale: numbers }), [dates, timeZone, numbers, joinsVersion]);
153
+ const cellOptions = useMemo(() => ({ preferences: dates, timeZone: timeZone ?? undefined, locale: numbers, currency }), [dates, timeZone, numbers, currency, joinsVersion]);
134
154
  const tableId = useId();
135
155
  // The search box is uncontrolled: a new key clears it when a new list starts over.
136
156
  const [searchGeneration, setSearchGeneration] = useState(0);
@@ -147,7 +167,7 @@ export function DataTable(props) {
147
167
  started.current = true;
148
168
  const saved = readSaved(stateKey);
149
169
  table.restore({
150
- pageSize: saved?.pageSize && pageSizes.includes(saved.pageSize) ? saved.pageSize : openingPageSize(),
170
+ pageSize: !whole && saved?.pageSize && pageSizes.includes(saved.pageSize) ? saved.pageSize : openingPageSize(),
151
171
  sort: saved?.sort !== undefined ? saved.sort : defaultSort,
152
172
  hidden: saved?.hidden ?? columns.filter(column => column.hidden).map(column => column.key),
153
173
  order: saved?.order,
@@ -173,6 +193,26 @@ export function DataTable(props) {
173
193
  if (started.current && givenRows)
174
194
  table.setRows(givenRows);
175
195
  }, [table, givenRows]);
196
+ // Another table's settings behind the same table (a CRUD's scope keeps its own): read as on the first draw, before the
197
+ // rows its new list loads, and the settings on screen are not written under the new key (`restoredKey` is what says
198
+ // the store already holds that key's settings).
199
+ const restoredKey = useRef(stateKey);
200
+ useLayoutEffect(() => {
201
+ if (!started.current || restoredKey.current === stateKey)
202
+ return;
203
+ restoredKey.current = stateKey;
204
+ const saved = readSaved(stateKey);
205
+ table.restore({
206
+ pageSize: !whole && saved?.pageSize && pageSizes.includes(saved.pageSize) ? saved.pageSize : openingPageSize(),
207
+ sort: saved?.sort !== undefined ? saved.sort : (defaultSort ?? null),
208
+ hidden: saved?.hidden ?? columns.filter(column => column.hidden).map(column => column.key),
209
+ order: saved?.order ?? [],
210
+ widths: saved?.widths ?? {},
211
+ pinned: saved?.pinned ?? {},
212
+ }, { notify: true });
213
+ setOpenSections(new Set(saved?.openSections ?? (saved?.pinsOpen ? [PINS_SECTION] : [])));
214
+ // eslint-disable-next-line react-hooks/exhaustive-deps
215
+ }, [table, stateKey]);
176
216
  // Another list behind the same table: start over from the first page, with no search and no selection.
177
217
  useLayoutEffect(() => {
178
218
  if (loadedKey.current === loadKey)
@@ -183,20 +223,24 @@ export function DataTable(props) {
183
223
  void table.reload({ reset: true });
184
224
  }, [table, loadKey]);
185
225
  useEffect(() => () => clearTimeout(searchTimer.current), []);
186
- // Saved settings follow the user's choices.
226
+ // Saved settings follow the user's choices. What is written is the store's state now, never this draw's: the first
227
+ // draw, and the draw that took another key's settings, still hold the settings from before the restore, and writing
228
+ // those would put the opening defaults (or another key's settings) under the key.
187
229
  const hiddenList = useMemo(() => [...state.hidden].sort(), [state.hidden]);
188
230
  useEffect(() => {
189
231
  if (!stateKey || !started.current)
190
232
  return;
233
+ const now = table.getState();
191
234
  writeSaved(stateKey, {
192
235
  // Only a size the person picked here: one equal to the default is not kept, so a new default still reaches it.
193
- pageSize: state.query.pageSize === openingPageSize() ? undefined : state.query.pageSize,
194
- sort: state.query.sort,
195
- hidden: hiddenList,
196
- order: state.order.length ? [...state.order] : undefined,
197
- widths: Object.keys(state.widths).length ? state.widths : undefined,
198
- pinned: Object.keys(state.pinned).length ? state.pinned : undefined,
236
+ pageSize: now.query.pageSize === openingPageSize() ? undefined : now.query.pageSize,
237
+ sort: now.query.sort,
238
+ hidden: [...now.hidden].sort(),
239
+ order: now.order.length ? [...now.order] : undefined,
240
+ widths: Object.keys(now.widths).length ? now.widths : undefined,
241
+ pinned: Object.keys(now.pinned).length ? now.pinned : undefined,
199
242
  });
243
+ // eslint-disable-next-line react-hooks/exhaustive-deps
200
244
  }, [stateKey, state.query.pageSize, state.query.sort, hiddenList, state.order, state.widths, state.pinned]);
201
245
  // A new default page size from the panel reaches an open table where no size was picked. Only a change counts: on the
202
246
  // first effects the save above still holds the store's opening size and has not written the restored one yet, so a
@@ -207,7 +251,7 @@ export function DataTable(props) {
207
251
  if (preferredSize.current === preferences.pageSize)
208
252
  return;
209
253
  preferredSize.current = preferences.pageSize;
210
- if (!started.current || readSaved(stateKey)?.pageSize)
254
+ if (whole || !started.current || readSaved(stateKey)?.pageSize)
211
255
  return;
212
256
  table.setPageSize(openingPageSize());
213
257
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -232,8 +276,8 @@ export function DataTable(props) {
232
276
  const labelOf = useCallback((row) => {
233
277
  if (rowLabel)
234
278
  return rowLabel(row);
235
- const first = columns[0];
236
- const text = first ? formatCellText(first, columnValue(first, row), cellOptions) : "";
279
+ const first = namingColumn(columns);
280
+ const text = first ? formatCellText(first, columnValue(first, row), cellOptions, row) : "";
237
281
  return text || `row ${table.keyOf(row)}`;
238
282
  }, [rowLabel, columns, cellOptions, table]);
239
283
  const actionsOf = usePerRow(rowActions);
@@ -260,6 +304,10 @@ export function DataTable(props) {
260
304
  void result.finally(() => table.setBusy([key], false)).catch(() => undefined);
261
305
  }
262
306
  }, [table]);
307
+ // The picked row (`onPick`): a click on a row, or its radio button, picks it through the store, which follows it.
308
+ const pickRow = useCallback((row) => table.pick(row), [table]);
309
+ const pickName = `${tableId}pick`;
310
+ const pickedKey = state.picked ? table.keyOf(state.picked) : null;
263
311
  const onSearch = (text) => {
264
312
  clearTimeout(searchTimer.current);
265
313
  if (searchDelay > 0 && text.trim())
@@ -280,9 +328,11 @@ export function DataTable(props) {
280
328
  const headRef = useRef(null);
281
329
  const colRefs = useMemo(() => new Map(), []);
282
330
  const [offsets, setOffsets] = useState({});
331
+ // Pinned columns that scroll for now: on a narrow table the ones past `PINNED_SHARE` of its width give way.
332
+ const [released, setReleased] = useState(NONE_RELEASED);
283
333
  const sized = Object.keys(state.widths).length > 0;
284
- const views = useMemo(() => viewColumns(visible, { order: state.order, pinned: state.pinned, widths: state.widths, offsets, reorderable, resizable }), [visible, state.order, state.pinned, state.widths, offsets, reorderable, resizable]);
285
- const indexOn = selectable || numbered;
334
+ const views = useMemo(() => viewColumns(visible, { order: state.order, pinned: state.pinned, widths: state.widths, offsets, released, reorderable, resizable }), [visible, state.order, state.pinned, state.widths, offsets, released, reorderable, resizable]);
335
+ const indexOn = selectable || numbered || pickable;
286
336
  // The bands above the headings. With no group anywhere the header is the one row it always was.
287
337
  const bands = useMemo(() => headBands(views), [views]);
288
338
  const grouped = bands.some(band => band.group !== null);
@@ -327,8 +377,36 @@ export function DataTable(props) {
327
377
  // The bar of the selected rows starts after the checkbox column, whatever width it took.
328
378
  root?.style.setProperty("--nx-table-index-w", `${Math.round(left)}px`);
329
379
  setOffsets(previous => (sameNumbers(previous, next) ? previous : next));
380
+ settlePins.current();
330
381
  });
382
+ // Which pinned columns give way: measured from the headings after every render and whenever the table's width
383
+ // changes. Staying or scrolling changes no width, so it settles at once.
384
+ const settlePins = useRef(() => undefined);
385
+ settlePins.current = () => {
386
+ const scroller = scrollRef.current;
387
+ const cells = headLeafCells(headRef.current);
388
+ if (!scroller || cells.length === 0)
389
+ return;
390
+ const sides = new Map(views.map(view => [view.column.key, view.side]));
391
+ let frameWidth = 0;
392
+ const left = [];
393
+ const right = [];
394
+ for (const cell of cells) {
395
+ const key = cell.getAttribute("data-col");
396
+ const width = cell.getBoundingClientRect().width;
397
+ if (key === TABLE_INDEX_COLUMN || key === TABLE_ACTIONS_COLUMN)
398
+ frameWidth += width;
399
+ else if (key && sides.get(key) === "left")
400
+ left.push([key, width]);
401
+ else if (key && sides.get(key) === "right")
402
+ right.unshift([key, width]);
403
+ }
404
+ const next = left.length + right.length === 0 ? NONE_RELEASED : releasedPins(scroller.clientWidth, frameWidth, left, right);
405
+ setReleased(previous => (sameKeys(previous, next) ? previous : next));
406
+ };
331
407
  // Shadows at the pinned columns while the others scroll under them: set on the frame, with no render.
408
+ // The width the rows can show goes on the frame too, so a message across a wide table (no rows, a hint, a failed
409
+ // load) stays inside what is on screen instead of centring on the whole scroll width.
332
410
  useLayoutEffect(() => {
333
411
  const scroller = scrollRef.current;
334
412
  const frameElement = scroller?.parentElement;
@@ -339,9 +417,14 @@ export function DataTable(props) {
339
417
  frameElement.toggleAttribute("data-scroll-start", left > 1);
340
418
  frameElement.toggleAttribute("data-scroll-end", left < scroller.scrollWidth - scroller.clientWidth - 1);
341
419
  };
342
- update();
420
+ const measure = () => {
421
+ frameElement.style.setProperty("--nx-table-view-w", `${scroller.clientWidth}px`);
422
+ update();
423
+ settlePins.current();
424
+ };
425
+ measure();
343
426
  scroller.addEventListener("scroll", update, { passive: true });
344
- const observer = new ResizeObserver(update);
427
+ const observer = new ResizeObserver(measure);
345
428
  observer.observe(scroller);
346
429
  if (scroller.firstElementChild)
347
430
  observer.observe(scroller.firstElementChild);
@@ -467,6 +550,10 @@ export function DataTable(props) {
467
550
  const barRef = useRef(null);
468
551
  const reloadRef = useRef(null);
469
552
  const [reloadHidden, setReloadHidden] = useState(false);
553
+ // An Add that cannot add yet (`add.disabled` with a `description`) says why on a press, under it.
554
+ const addNotice = useReadOnlyNotice(Boolean(add?.disabled && add.description), barRef, add?.description, { anchor: target => target.closest(".nx-btn"), typing: false, lock: "label" });
555
+ // The spreadsheet's other ways to add sit under the Add's arrow only while it can add.
556
+ const addable = Boolean(add && !add.disabled);
470
557
  const addPart = Boolean(add || actions);
471
558
  const listsPart = Boolean(toolbar);
472
559
  useLayoutEffect(() => {
@@ -500,23 +587,35 @@ export function DataTable(props) {
500
587
  void table.reload({ quiet: true });
501
588
  };
502
589
  // A spreadsheet: the template to fill, the file to bring back, and an export the server writes from what is on
503
- // screen. With an Add, they are other ways to add, under its arrow; without one, they are in the options menu.
590
+ // screen. With an Add, they are other ways to add, under its arrow; without one, they are in the options menu. Each is
591
+ // offered only to a person the app's permissions let call every endpoint it needs, the settings included.
592
+ const sheet = excel?.endpoints;
593
+ const mayUseSheet = (...endpoints) => [sheet.settings, ...endpoints].every(endpoint => canCall(endpoint));
504
594
  const sheetItems = excel
505
595
  ? [
506
- { id: "excel-template", label: "Download template", description: `A spreadsheet to fill in`, icon: _jsx(InlineIcon, { icon: iconDownload }), separator: true, onSelect: () => void downloadExcelTemplate(excel.endpoints, { params: excel.params }) },
507
- { id: "excel-import", label: "Import from a spreadsheet", icon: _jsx(InlineIcon, { icon: iconFileImport }), onSelect: () => openExcelImport({ endpoints: excel.endpoints, subject: excel.subject, params: excel.params, onImported: onImported }) },
508
- // A bulk insert is an Add of many rows, so it is offered only beside an Add: a CRUD shows its Add only to a person
509
- // who may create, and nobody types fifty rows to be refused at the end.
510
- ...(excel.bulk === false || !add
511
- ? []
512
- : [{ id: "excel-bulk", label: "Bulk insert", description: "Type the rows here, with no file", icon: _jsx(InlineIcon, { icon: iconRowInsertBottom }), onSelect: () => openExcelImport({ endpoints: excel.endpoints, subject: excel.subject, params: excel.params, onImported: onImported, mode: "bulk" }) }]),
513
- ...(excel.endpoints.export
596
+ ...(mayUseSheet(sheet.template) ? [{ id: "excel-template", label: "Download template", description: `A spreadsheet to fill in`, icon: _jsx(InlineIcon, { icon: iconDownload }), onSelect: () => void downloadExcelTemplate(excel.endpoints, { params: excel.params }) }] : []),
597
+ ...(mayUseSheet(sheet.verify, sheet.import)
598
+ ? [
599
+ { id: "excel-import", label: "Import from a spreadsheet", icon: _jsx(InlineIcon, { icon: iconFileImport }), onSelect: () => openExcelImport({ endpoints: excel.endpoints, subject: excel.subject, params: excel.params, onImported: onImported }) },
600
+ // A bulk insert is an Add of many rows, so it is offered only beside an Add that can add: a CRUD shows its Add
601
+ // only to a person who may create, and nobody types fifty rows to be refused at the end.
602
+ ...(excel.bulk === false || !addable
603
+ ? []
604
+ : [{ id: "excel-bulk", label: "Bulk insert", description: "Type the rows here, with no file", icon: _jsx(InlineIcon, { icon: iconRowInsertBottom }), onSelect: () => openExcelImport({ endpoints: excel.endpoints, subject: excel.subject, params: excel.params, onImported: onImported, mode: "bulk" }) }]),
605
+ ]
606
+ : []),
607
+ ...(sheet.export && mayUseSheet(sheet.export)
514
608
  ? [{ id: "excel-export", label: "Export to Excel", description: "Everything the search and filters leave, from the server", icon: _jsx(InlineIcon, { icon: iconFileExport }), onSelect: () => void exportExcel(excel.endpoints, exportQuery()) }]
515
609
  : []),
516
- ]
610
+ ].map((item, index) => (index === 0 ? { ...item, separator: true } : item))
517
611
  : [];
518
612
  // In the bar it is `block`: as wide as its own label there, and the whole line once the Add section takes one.
519
- const addButton = (block = false) => add && (_jsx(Button, { variant: "primary", size: "sm", block: block, icon: _jsx(InlineIcon, { icon: iconPlus }), onClick: add.onClick, children: add.label }));
613
+ const addButton = (block = false) => add && (_jsx(Button, { variant: "primary", size: "sm", block: block, icon: _jsx(InlineIcon, { icon: iconPlus }), "aria-disabled": add.disabled || undefined, title: add.description, onKeyDown: addNotice.onKeyDown, onClick: event => {
614
+ if (!add.disabled)
615
+ add.onClick();
616
+ else if (!add.onDisabledPress?.())
617
+ addNotice.show(event.currentTarget);
618
+ }, children: add.label }));
520
619
  const optionItems = [
521
620
  ...(filterInputs?.length
522
621
  ? [{ id: "filters", label: "Filters", description: filtersOn.join(", ") || undefined, icon: _jsx(InlineIcon, { icon: iconFilter }), onSelect: () => filtersRef.current?.open() }]
@@ -531,7 +630,7 @@ export function DataTable(props) {
531
630
  : []),
532
631
  // The sheet's own Import replaces the page's `onImport` hook.
533
632
  ...(excel
534
- ? add
633
+ ? addable
535
634
  ? []
536
635
  : sheetItems
537
636
  : onImport
@@ -545,7 +644,9 @@ export function DataTable(props) {
545
644
  ...(reloadable && reloadHidden ? [{ id: "reload", label: "Reload", icon: _jsx(InlineIcon, { icon: iconRefresh }), separator: true, onSelect: reload }] : []),
546
645
  ];
547
646
  const optionEntries = menuEntries(optionItems);
548
- const pageSizePicker = _jsx(TablePageSize, { pageSize: query.pageSize, pageSizes: pageSizes, compact: searchable, onChange: value => table.setPageSize(value) });
647
+ // Rows per page: inside the search box, in the bar without one, or beside the pager (`pager`); none for a table read whole.
648
+ const pageSizePicker = whole ? null : _jsx(TablePageSize, { pageSize: query.pageSize, pageSizes: pageSizes, compact: searchable && pager === "bar", onChange: value => table.setPageSize(value) });
649
+ const barPageSize = pager === "bar" ? pageSizePicker : null;
549
650
  let body;
550
651
  if (status === "loading") {
551
652
  body = _jsx(TableSkeletonRows, { count: Math.min(query.pageSize, 8), cells: cellCount, frame: frame, sized: sized });
@@ -576,7 +677,7 @@ export function DataTable(props) {
576
677
  body = rows.map((row, index) => {
577
678
  const key = pageKeys[index];
578
679
  const selected = state.selected.has(key);
579
- return (_jsx(TableRowView, { row: row, rowKey: key, number: state.offset + index + 1, label: labelOf(row), columns: views, cellOptions: cellOptions, selectable: selectable, selected: selected, selectionFull: selectionFull && !selected, busy: state.busy.has(key), flashed: state.flashed.has(key), numbered: numbered, marks: rowMarks?.(row), frame: frame, sized: sized, actions: hasActions ? (actionsOf(row) ?? []) : null, onToggle: onToggle, onAction: onAction, onOpen: onRowOpen, openOnClick: openOnClick }, key || index));
680
+ return (_jsx(TableRowView, { row: row, rowKey: key, number: state.offset + index + 1, label: labelOf(row), columns: views, cellOptions: cellOptions, selectable: selectable, selected: selected, selectionFull: selectionFull && !selected, busy: state.busy.has(key), flashed: state.flashed.has(key), numbered: numbered, marks: rowMarks?.(row), strong: rowStrong?.(row) === true, frame: frame, sized: sized, actions: hasActions ? (actionsOf(row) ?? []) : null, onToggle: onToggle, onAction: onAction, onOpen: onRowOpen, openOnClick: openOnClick, onPick: pickable ? pickRow : undefined, picked: key === pickedKey, pickName: pickable ? pickName : undefined }, key || index));
580
681
  });
581
682
  }
582
683
  /* The sections above the page: the records pinned, the favourites, each folded open or shut, remembered on this device */
@@ -598,7 +699,9 @@ export function DataTable(props) {
598
699
  const key = table.keyOf(row);
599
700
  const selected = state.selected.has(key);
600
701
  const marks = rowMarks?.(row);
601
- return (_jsx(TableRowView, { row: row, rowKey: key, number: index + 1, label: labelOf(row), columns: views, cellOptions: cellOptions, selectable: selectable, selected: selected, selectionFull: selectionFull && !selected, busy: state.busy.has(key), flashed: state.flashed.has(key), numbered: numbered, marks: marks?.length ? marks : own, frame: frame, sized: sized, actions: hasActions ? (actionsOf(row) ?? []) : null, onToggle: onToggle, onAction: onAction, onOpen: onRowOpen, openOnClick: openOnClick }, key || index));
702
+ return (_jsx(TableRowView, { row: row, rowKey: key, number: index + 1, label: labelOf(row), columns: views, cellOptions: cellOptions, selectable: selectable, selected: selected, selectionFull: selectionFull && !selected, busy: state.busy.has(key), flashed: state.flashed.has(key), numbered: numbered, marks: marks?.length ? marks : own, strong: rowStrong?.(row) === true, frame: frame, sized: sized, actions: hasActions ? (actionsOf(row) ?? []) : null, onToggle: onToggle, onAction: onAction, onOpen: onRowOpen, openOnClick: openOnClick,
703
+ // A click picks a section's row too; its radio button is the one in the list below, where the arrow keys move.
704
+ onPick: pickable ? pickRow : undefined, picked: key === pickedKey }, key || index));
602
705
  });
603
706
  };
604
707
  const selectedRows = selecting ? [...state.selected.values()] : [];
@@ -619,8 +722,8 @@ export function DataTable(props) {
619
722
  const mass = selectedRows.length ? (typeof massActions === "function" ? massActions(selectedRows) : (massActions ?? [])) : [];
620
723
  // A column's own heading, wherever it stands: across both rows when no band is above it, in the row under its band when one is.
621
724
  const headingCell = (view, rowSpan) => (_jsx(HeaderCell, { view: view, sized: sized, rowSpan: rowSpan, sort: query.sort, disabled: status !== "ready" || rows.length < 2, covered: selectedRows.length > 0, canPin: canPinColumn(view.column, { pinnable }), canHide: view.column.hideable !== false && visible.length > 1, neighbours: moveTargets(views, view.column.key), onSort: direction => table.setSort(direction ? { key: view.column.key, direction } : null), onPin: side => table.setPinned(view.column.key, side), onMove: beforeKey => table.moveColumn(view.column.key, beforeKey), onFit: () => fitColumn(view.column.key), onHide: () => table.setHidden(view.column.key, true), onPointerDown: event => onColumnPointerDown(event, view), onResizerDown: event => onResizerPointerDown(event, view.column.key) }, view.column.key));
622
- return (_jsxs("div", { ref: rootRef, id: id, className: cx("nx-table", `nx-table--${size}`, fill && "nx-table--fill", className), style: style, "data-head-rows": headRows, "data-status": status, "data-refreshing": refreshing || undefined, "data-selecting": selecting || undefined, "data-sized": sized || undefined, children: [_jsxs("div", { ref: barRef, className: "nx-table__bar", children: [addPart && (_jsxs("div", { className: "nx-table__bar-add", children: [add &&
623
- (sheetItems.length > 0 ? (_jsx(SplitButton, { fixed: true, block: true, size: "sm", menuLabel: "More ways to add", options: [{ value: "add", label: add.label, icon: _jsx(InlineIcon, { icon: iconPlus }) }, ...sheetItems.map(item => ({ value: item.id, label: item.label, icon: item.icon, description: item.description }))], onAction: value => (value === "add" ? add.onClick() : sheetItems.find(item => item.id === value)?.onSelect()) })) : (addButton(true))), actions] })), toolbar && _jsx("div", { className: "nx-table__bar-start", children: toolbar }), _jsxs("div", { className: "nx-table__bar-end", children: [filterInputs?.length ? (_jsx(TableFiltersPanel, { ref: filtersRef, anchor: optionsRef, inputs: filterInputs, value: query.filters, delay: searchDelay, matches: state.matches, total: state.total, onChange: values => table.setFilters(values) })) : null, hasColumnsPanel && (_jsx(TableColumnsPanel, { ref: columnsRef, anchor: optionsRef, columns: ordered, hidden: state.hidden, pinned: state.pinned, reorderable: reorderable, size: "sm", changed: table.hasColumnChanges(), onToggle: (key, hide) => table.setHidden(key, hide), onOrder: keys => table.setOrder(keys), onReset: () => table.resetColumns() })), reloadable && (_jsx(Button, { ref: reloadRef, size: "sm", variant: "ghost", className: "nx-table__reload", icon: _jsx(InlineIcon, { icon: iconRefresh, className: refreshing ? "nx-spin" : undefined }), "aria-label": "Reload", title: "Reload", disabled: status === "loading", onClick: reload })), optionEntries.length > 0 && (_jsx(Menu, { items: optionEntries, size: "sm", align: "end", "aria-label": "More options", onSelect: value => optionItems.find(action => action.id === value)?.onSelect(), children: _jsx(Button, { ref: optionsRef, size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconDotsVertical }), dot: filtersOn.length > 0 && "primary", "aria-label": "More options", title: filtersOn.length ? `More options · ${filtersOn.length} ${filtersOn.length === 1 ? "filter" : "filters"} on` : "More options", "data-table-options": true }) })), !searchable && pageSizePicker] }), searchable && (_jsx(TextBox, { ref: searchRef, type: "search", size: "sm", className: "nx-table__search", actionsStart: pageSizePicker, icon: _jsx(InlineIcon, { icon: iconSearch }), placeholder: searchPlaceholder, "aria-label": searchPlaceholder.replace(/…$/, ""), "aria-controls": tableId, defaultValue: query.search, clearable: true, onValueChange: onSearch, onKeyDown: event => {
725
+ return (_jsxs("div", { ref: rootRef, id: id, className: cx("nx-table", `nx-table--${size}`, fill && "nx-table--fill", className), style: style, "data-head-rows": headRows, "data-status": status, "data-refreshing": refreshing || undefined, "data-selecting": selecting || undefined, "data-sized": sized || undefined, children: [caption && _jsx("div", { className: "nx-table__caption", children: caption }), _jsxs("div", { ref: barRef, className: "nx-table__bar", children: [addPart && (_jsxs("div", { className: "nx-table__bar-add", children: [add &&
726
+ (addable && sheetItems.length > 0 ? (_jsx(SplitButton, { fixed: true, block: true, size: "sm", menuLabel: "More ways to add", options: [{ value: "add", label: add.label, icon: _jsx(InlineIcon, { icon: iconPlus }) }, ...sheetItems.map(item => ({ value: item.id, label: item.label, icon: item.icon, description: item.description }))], onAction: value => (value === "add" ? add.onClick() : sheetItems.find(item => item.id === value)?.onSelect()) })) : (addButton(true))), actions] })), toolbar && _jsx("div", { className: "nx-table__bar-start", children: toolbar }), _jsxs("div", { className: "nx-table__bar-end", children: [filterInputs?.length ? (_jsx(TableFiltersPanel, { ref: filtersRef, anchor: optionsRef, inputs: filterInputs, value: query.filters, delay: searchDelay, matches: state.matches, total: state.total, onChange: values => table.setFilters(values) })) : null, hasColumnsPanel && (_jsx(TableColumnsPanel, { ref: columnsRef, anchor: optionsRef, columns: ordered, hidden: state.hidden, pinned: state.pinned, reorderable: reorderable, size: "sm", changed: table.hasColumnChanges(), onToggle: (key, hide) => table.setHidden(key, hide), onOrder: keys => table.setOrder(keys), onReset: () => table.resetColumns() })), reloadable && (_jsx(Button, { ref: reloadRef, size: "sm", variant: "ghost", className: "nx-table__reload", icon: _jsx(InlineIcon, { icon: iconRefresh, className: refreshing ? "nx-spin" : undefined }), "aria-label": "Reload", title: "Reload", disabled: status === "loading", onClick: reload })), optionEntries.length > 0 && (_jsx(Menu, { items: optionEntries, size: "sm", align: "end", "aria-label": "More options", onSelect: value => optionItems.find(action => action.id === value)?.onSelect(), children: _jsx(Button, { ref: optionsRef, size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconDotsVertical }), dot: filtersOn.length > 0 && "primary", "aria-label": "More options", title: filtersOn.length ? `More options · ${filtersOn.length} ${filtersOn.length === 1 ? "filter" : "filters"} on` : "More options", "data-table-options": true }) })), !searchable && barPageSize] }), searchable && (_jsx(TextBox, { ref: searchRef, type: "search", size: "sm", className: "nx-table__search", actionsStart: barPageSize, icon: _jsx(InlineIcon, { icon: iconSearch }), placeholder: searchPlaceholder, "aria-label": searchPlaceholder.replace(/…$/, ""), "aria-controls": tableId, defaultValue: query.search, clearable: true, onValueChange: onSearch, onKeyDown: event => {
624
727
  if (event.key === "Enter") {
625
728
  clearTimeout(searchTimer.current);
626
729
  table.setSearch(event.currentTarget.value);
@@ -632,13 +735,13 @@ export function DataTable(props) {
632
735
  } }) })) })), bands.map(band => band.group === null ? (headingCell(band.columns[0], headRows)) : (_jsx(GroupHeading, { band: band }, `group:${band.group}:${band.columns[0].column.key}`))), sized && _jsx("td", { className: "nx-table__heading nx-table__cell--filler", rowSpan: headRows }), frame.actions && (_jsx("th", { scope: "col", className: "nx-table__heading nx-table__cell--actions", rowSpan: headRows, "data-col": TABLE_ACTIONS_COLUMN, "data-pin": "right", "data-pin-edge": frame.actions.edge || undefined, style: { right: frame.actions.offset }, children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }), grouped && _jsx("tr", { className: "nx-table__head nx-table__head--leaves", children: bands.filter(band => band.group !== null).flatMap(band => band.columns.map(view => headingCell(view))) })] }), sections.map(section => {
633
736
  const open = openSections.has(section.key);
634
737
  return (_jsxs("tbody", { className: "nx-table__section", "data-section": section.key, "data-open": open || undefined, children: [_jsx(TableSectionHead, { span: span, label: section.label, icon: section.icon, count: section.rows.length, max: section.max, loading: Boolean(section.loading), open: open, onToggle: () => toggleSection(section.key) }), open && sectionRows(section)] }, section.key));
635
- }), _jsx("tbody", { children: body })] }) })] }), _jsx(TableFooter, { state: state, disabled: refreshing, onPage: page => table.setPage(page) })] }));
738
+ }), _jsx("tbody", { children: body })] }) })] }), _jsx(TableFooter, { state: state, disabled: refreshing, whole: whole, pageSize: pager === "footer" ? pageSizePicker : null, onPage: page => table.setPage(page) }), addNotice.notice] }));
636
739
  }
637
740
  /** Where "Move left" and "Move right" put a column: the key it goes before, or null for the end of its side. */
638
741
  function moveTargets(views, key) {
639
742
  // A column moves among the columns on its own side, past the ones that stay put.
640
- const pin = views.find(view => view.column.key === key)?.pin ?? null;
641
- const group = views.filter(view => view.pin === pin && view.movable);
743
+ const side = views.find(view => view.column.key === key)?.side ?? null;
744
+ const group = views.filter(view => view.side === side && view.movable);
642
745
  const at = group.findIndex(view => view.column.key === key);
643
746
  if (at < 0)
644
747
  return { left: undefined, right: undefined };
@@ -679,11 +782,12 @@ function HeaderCell({ view, sized, rowSpan, sort, disabled, covered, canPin, can
679
782
  if (canPin) {
680
783
  if (entries.length)
681
784
  entries.push({ separator: true });
682
- if (view.pin !== "left")
785
+ // The side chosen, also while a narrow table lets it scroll.
786
+ if (view.side !== "left")
683
787
  entries.push({ value: "pin-left", label: "Pin left", icon: _jsx(InlineIcon, { icon: iconArrowBarToLeft }) });
684
- if (view.pin !== "right")
788
+ if (view.side !== "right")
685
789
  entries.push({ value: "pin-right", label: "Pin right", icon: _jsx(InlineIcon, { icon: iconArrowBarToRight }) });
686
- if (view.pin)
790
+ if (view.side)
687
791
  entries.push({ value: "unpin", label: "Unpin", icon: _jsx(InlineIcon, { icon: iconPinnedOff }) });
688
792
  }
689
793
  if (view.movable && (neighbours.left !== undefined || neighbours.right !== undefined)) {
@@ -721,7 +825,7 @@ function HeaderCell({ view, sized, rowSpan, sort, disabled, covered, canPin, can
721
825
  onHide();
722
826
  }
723
827
  const label = _jsx("span", { className: "nx-table__heading-label", children: column.label });
724
- return (_jsxs("th", { scope: "col", className: cx("nx-table__heading", `nx-table__cell--${align}`), rowSpan: rowSpan, "data-col": column.key, "data-pin": view.pin ?? undefined, "data-pin-edge": (view.pin && view.edge) || undefined, "data-movable": view.movable || undefined, style: columnStyle(view, sized), "aria-sort": direction === "asc" ? "ascending" : direction === "desc" ? "descending" : undefined, onPointerDown: onPointerDown, children: [_jsxs("div", { className: "nx-table__heading-inner", children: [sortable ? (_jsxs("button", { type: "button", className: "nx-table__sort", "data-sorted": direction ?? undefined, disabled: disabled && !direction, tabIndex: covered ? -1 : undefined, onClick: () => onSort(direction === "asc" ? "desc" : direction === "desc" ? null : "asc"), title: `Sort by ${column.label}`, children: [label, _jsx(InlineIcon, { icon: direction === "asc" ? iconArrowUp : direction === "desc" ? iconArrowDown : iconArrowsSort, className: "nx-table__sort-icon" })] })) : (label), entries.length > 0 && (_jsx(Menu, { items: entries, size: "sm", align: "end", "aria-label": `${column.label}: column`, onSelect: choose, children: _jsx(Button, { variant: "ghost", size: "sm", className: "nx-table__col-menu", icon: _jsx(InlineIcon, { icon: iconChevronDown }), tabIndex: covered ? -1 : undefined, "aria-label": `${column.label}: column options`, title: "Column options" }) }))] }), view.resizable && (_jsx("span", { className: "nx-table__resizer", "aria-hidden": "true", "data-resize": column.key, title: `Drag to resize ${column.label}, double-click to fit`, onPointerDown: onResizerDown, onDoubleClick: onFit }))] }));
828
+ return (_jsxs("th", { scope: "col", className: cx("nx-table__heading", `nx-table__cell--${align}`), rowSpan: rowSpan, "data-col": column.key, "data-pin": view.pin ?? undefined, "data-pin-side": view.side ?? undefined, "data-pin-edge": (view.pin && view.edge) || undefined, "data-movable": view.movable || undefined, style: columnStyle(view, sized), "aria-sort": direction === "asc" ? "ascending" : direction === "desc" ? "descending" : undefined, onPointerDown: onPointerDown, children: [_jsxs("div", { className: "nx-table__heading-inner", children: [sortable ? (_jsxs("button", { type: "button", className: "nx-table__sort", "data-sorted": direction ?? undefined, disabled: disabled && !direction, tabIndex: covered ? -1 : undefined, onClick: () => onSort(direction === "asc" ? "desc" : direction === "desc" ? null : "asc"), title: `Sort by ${column.label}`, children: [label, _jsx(InlineIcon, { icon: direction === "asc" ? iconArrowUp : direction === "desc" ? iconArrowDown : iconArrowsSort, className: "nx-table__sort-icon" })] })) : (label), entries.length > 0 && (_jsx(Menu, { items: entries, size: "sm", align: "end", "aria-label": `${column.label}: column`, onSelect: choose, children: _jsx(Button, { variant: "ghost", size: "sm", className: "nx-table__col-menu", icon: _jsx(InlineIcon, { icon: iconChevronDown }), tabIndex: covered ? -1 : undefined, "aria-label": `${column.label}: column options`, title: "Column options" }) }))] }), view.resizable && (_jsx("span", { className: "nx-table__resizer", "aria-hidden": "true", "data-resize": column.key, title: `Drag to resize ${column.label}, double-click to fit`, onPointerDown: onResizerDown, onDoubleClick: onFit }))] }));
725
829
  }
726
830
  /** The steps a bar is squeezed into when its parts do not fit, each one keeping the steps before it. */
727
831
  const BAR_SQUEEZES = ["search", "search reload", "search reload add"];
@@ -1,11 +1,11 @@
1
1
  import { type Ref, type RefObject } from "react";
2
2
  import { type PopoverPanelHandle } from "../../Helpers/PopoverHooks.ts";
3
- import type { TableColumn, TablePinSide, TableRow } from "../../Interfaces/TableInterfaces.ts";
3
+ import type { TableColumn, TablePins, TableRow } from "../../Interfaces/TableInterfaces.ts";
4
4
  export interface TableColumnsPanelProps<TRow extends TableRow> {
5
5
  /** Every column the table has, in the order they are in now - hidden ones among them. */
6
6
  columns: readonly TableColumn<TRow>[];
7
7
  hidden: ReadonlySet<string>;
8
- pinned: Readonly<Record<string, TablePinSide>>;
8
+ pinned: TablePins;
9
9
  /** False while the table is not reorderable: the panel then only shows and hides. */
10
10
  reorderable?: boolean;
11
11
  /** Default `md`. */
@@ -5,6 +5,7 @@ import { useEffect, useId, useRef, useState } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
6
  import { isPopoverOpen } from "../../Helpers/PopoverHelpers.js";
7
7
  import { usePopoverPanel } from "../../Helpers/PopoverHooks.js";
8
+ import { columnPin } from "../../Helpers/TableHelpers.js";
8
9
  import { Button } from "../Documents/Button.js";
9
10
  /** The rows of the panel: a band for every run of neighbours that name the same group, as the header draws them. */
10
11
  function panelRows(columns, groups) {
@@ -69,7 +70,7 @@ export function TableColumnsPanel({ columns, hidden, pinned, reorderable = true,
69
70
  key: column.key,
70
71
  label: column.label,
71
72
  shown: !hidden.has(column.key),
72
- pin: pinned[column.key] ?? column.pinned ?? null,
73
+ pin: columnPin(column, pinned),
73
74
  movable: reorderable && column.movable !== false,
74
75
  hideable: column.hideable !== false,
75
76
  }));
@@ -1,6 +1,6 @@
1
1
  import { type Ref, type RefObject } from "react";
2
2
  import { type PopoverPanelHandle } from "../../Helpers/PopoverHooks.ts";
3
- import type { InputParams } from "../../Interfaces/FormInterfaces.ts";
3
+ import { type InputParams } from "../../Interfaces/FormInterfaces.ts";
4
4
  import type { TableFilters } from "../../Interfaces/TableInterfaces.ts";
5
5
  export interface TableFiltersPanelProps {
6
6
  /** The filter inputs, as the table prepared them (`asFilterInput`). */
@@ -4,6 +4,7 @@ import { iconFilterOff, InlineIcon } from "nexus-icons";
4
4
  import { useEffect, useId, useRef } from "react";
5
5
  import { usePopoverPanel } from "../../Helpers/PopoverHooks.js";
6
6
  import { activeFilters } from "../../Helpers/TableFilters.js";
7
+ import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
7
8
  import { Button } from "../Documents/Button.js";
8
9
  import { Form, useForm } from "../Forms/Form.js";
9
10
  import { formatCount } from "./DataTableParts.js";
@@ -14,7 +15,21 @@ import { formatCount } from "./DataTableParts.js";
14
15
  * never clears a filter on its way out.
15
16
  */
16
17
  /** Inputs typed into or dragged: their value waits until the person stops. A date box reads a date from part of one. */
17
- const TYPED = new Set(["text", "email", "search", "tel", "url", "textarea", "number", "slider", "range-slider", "date", "date-range", "time", "date-time"]);
18
+ const TYPED = new Set([
19
+ EInputTypes.Text,
20
+ EInputTypes.Email,
21
+ EInputTypes.Search,
22
+ EInputTypes.Tel,
23
+ EInputTypes.Url,
24
+ EInputTypes.TextArea,
25
+ EInputTypes.Number,
26
+ EInputTypes.Slider,
27
+ EInputTypes.RangeSlider,
28
+ EInputTypes.Date,
29
+ EInputTypes.DateRange,
30
+ EInputTypes.Time,
31
+ EInputTypes.DateTime,
32
+ ]);
18
33
  /** The Filters panel, opened through its `open()` handle: a `Form` of the filter inputs, the count, and Clear all. */
19
34
  export function TableFiltersPanel({ inputs, value, delay, matches, total, onChange, anchor, ref }) {
20
35
  const titleId = useId();
@@ -57,7 +72,7 @@ export function TableFiltersPanel({ inputs, value, delay, matches, total, onChan
57
72
  }, children: [_jsxs("div", { className: "nx-table-panel__head", children: [_jsx("span", { className: "nx-table-panel__title", id: titleId, children: "Filters" }), _jsx("span", { className: "nx-table-panel__count", children: count ? `${count} on` : "None" })] }), _jsx("div", { className: "nx-popover__scroll", children: shown && (_jsx(Form, { form: form, inputs: inputs, size: "sm", defaultValue: value, className: "nx-table-filters__form", onValueChange: (_, name) => {
58
73
  if (name === null)
59
74
  return;
60
- if (delay > 0 && TYPED.has(inputs.find(params => params.name === name)?.type ?? ""))
75
+ if (delay > 0 && TYPED.has(inputs.find(params => params.name === name)?.type ?? 0))
61
76
  wait();
62
77
  else
63
78
  apply();
@@ -41,8 +41,11 @@ export declare function TableColumnGroup<TRow extends TableRow>({ columns, width
41
41
  frame: TableFrameColumns;
42
42
  colRefs: Map<string, HTMLTableColElement>;
43
43
  }): import("react").JSX.Element;
44
- /** The number of a row, its checkbox, or both: the number until the pointer is over the row or something is selected. */
45
- export declare function TableIndexCell({ heading, rowSpan, own, number, numbered, selectable, checkbox, }: {
44
+ /**
45
+ * The number of a row, its checkbox, or both: the number until the pointer is over the row or something is selected. A
46
+ * row of a table whose rows are picked (`onPick`) also holds its radio button here, out of sight.
47
+ */
48
+ export declare function TableIndexCell({ heading, rowSpan, own, number, numbered, selectable, checkbox, pick, }: {
46
49
  heading?: boolean;
47
50
  /** 2 in the heading while the header has a band of group headings above the columns' own. */
48
51
  rowSpan?: number;
@@ -55,6 +58,8 @@ export declare function TableIndexCell({ heading, rowSpan, own, number, numbered
55
58
  numbered: boolean;
56
59
  selectable: boolean;
57
60
  checkbox?: ReactNode;
61
+ /** The row's radio button, when rows are picked. */
62
+ pick?: ReactNode;
58
63
  }): import("react").JSX.Element;
59
64
  export interface TableRowViewProps<TRow extends TableRow> {
60
65
  row: TRow;
@@ -72,6 +77,8 @@ export interface TableRowViewProps<TRow extends TableRow> {
72
77
  numbered: boolean;
73
78
  /** The row's marks (pinned, a favourite), worn one under the other where a row without any shows its number. */
74
79
  marks?: readonly TableRowMark[];
80
+ /** The row reads strong (`rowStrong`): a thread with unread lines. */
81
+ strong?: boolean;
75
82
  /** Where the table's own columns sit; `null` for the ones it does not have. */
76
83
  frame: TableFrameColumns;
77
84
  /** The user gave a column a width, so the table lays its columns out from the `<colgroup>`. */
@@ -82,9 +89,15 @@ export interface TableRowViewProps<TRow extends TableRow> {
82
89
  onOpen?: (row: TRow) => void;
83
90
  /** A single click opens the row, as a double-click does. */
84
91
  openOnClick?: boolean;
92
+ /** Rows are picked: a click picks this one (a double-click still opens it), whatever `openOnClick` says. */
93
+ onPick?: (row: TRow) => void;
94
+ /** This row is the picked one. */
95
+ picked?: boolean;
96
+ /** The name of the table's radio group, for the row's own radio button; none in a section above the page. */
97
+ pickName?: string;
85
98
  }
86
99
  /** One row. It renders again only when its row, its state, or the columns change. */
87
- declare function TableRowViewInner<TRow extends TableRow>({ row, rowKey, number, label, columns, cellOptions, selectable, selected, selectionFull, busy, flashed, numbered, marks, frame, sized, actions, onToggle, onAction, onOpen, openOnClick, }: TableRowViewProps<TRow>): import("react").JSX.Element;
100
+ declare function TableRowViewInner<TRow extends TableRow>({ row, rowKey, number, label, columns, cellOptions, selectable, selected, selectionFull, busy, flashed, numbered, marks, strong, frame, sized, actions, onToggle, onAction, onOpen, openOnClick, onPick, picked, pickName, }: TableRowViewProps<TRow>): import("react").JSX.Element;
88
101
  export declare const TableRowView: typeof TableRowViewInner;
89
102
  /**
90
103
  * The head of a section above the page: one row across the table with a button that folds the section's records open
@@ -146,11 +159,16 @@ export declare function TablePageSize({ pageSize, pageSizes, compact, onChange }
146
159
  }): import("react").JSX.Element | null;
147
160
  /**
148
161
  * Under the table: which rows show and how many the search and the filters left ("Showing 41–50 of 234 · filtered
149
- * from 1,000"), the page of how many, and the pager.
162
+ * from 1,000"), the page of how many, the rows per page when the table keeps them here (`pageSize`), and the pager.
163
+ *
164
+ * A table read whole (`whole`, every row on one page) has nothing to page: its footer shows only while a search or the
165
+ * filters leave rows out, and is otherwise kept for screen readers alone, so the count is still heard as it changes.
150
166
  */
151
- export declare function TableFooter<TRow extends TableRow>({ state, disabled, onPage }: {
167
+ export declare function TableFooter<TRow extends TableRow>({ state, disabled, whole, pageSize, onPage }: {
152
168
  state: TableState<TRow>;
153
169
  disabled: boolean;
170
+ whole?: boolean;
171
+ pageSize?: ReactNode;
154
172
  onPage: (page: number) => void;
155
173
  }): import("react").JSX.Element;
156
174
  export {};