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
@@ -28,6 +28,43 @@ export declare function getNumberSymbols(locale?: string): NumberSymbols;
28
28
  * Returns "" for `null`, `undefined`, and `NaN`. Digits are always 0–9, and the minus sign is "-", so the text can be typed back.
29
29
  */
30
30
  export declare function formatNumber(value: number | null | undefined, options?: NumberFormatOptions): string;
31
+ /** How `formatMoney` writes an amount. */
32
+ export interface MoneyFormatOptions {
33
+ /** Written before the figures with a space: "Rs." gives "Rs. 1,250.00". None: the figures alone. */
34
+ currency?: string | null;
35
+ /** Digits after the decimal point, always shown. Default 2. */
36
+ decimals?: number;
37
+ /** Locale of the separators, as `formatNumber` takes it (`useNumberLocale()` for the person's). Default `en-US`. */
38
+ locale?: string;
39
+ /** What an empty amount (`null`, `undefined`, `NaN`) writes. Default "": a `0` amount is written as one. */
40
+ empty?: string;
41
+ }
42
+ /**
43
+ * Money as a company writes it: its currency, then the amount grouped with a fixed number of decimals, the same in a
44
+ * table's `money` column (`DataTableProps.currency`), a dashboard's figures and a printed receipt.
45
+ *
46
+ * @example formatMoney(1250, { currency: "Rs." }) // "Rs. 1,250.00"
47
+ * @example formatMoney(null, { currency: "Rs.", empty: "-" }) // "-"
48
+ */
49
+ export declare function formatMoney(value: number | null | undefined, options?: MoneyFormatOptions): string;
50
+ /** How `formatDays` writes a number of days. */
51
+ export interface DaysFormatOptions {
52
+ /** Most digits after the decimal point: half days are 0.5. Default 2. */
53
+ decimals?: number;
54
+ /** Locale of the separators, as `formatNumber` takes it. Default `en-US`. */
55
+ locale?: string;
56
+ /** What an empty count (`null`, `undefined`, `NaN`) writes. Default "". */
57
+ empty?: string;
58
+ }
59
+ /** "day" for exactly one, "days" for any other count: 0, 0.5, 1.5, 2. */
60
+ export declare function dayWord(value: number): string;
61
+ /**
62
+ * A number of days as a person reads it: a leave's length, a balance. Grouped, with no more decimals than it has.
63
+ *
64
+ * @example formatDays(1) // "1 day"
65
+ * @example formatDays(2.5) // "2.5 days"
66
+ */
67
+ export declare function formatDays(value: number | null | undefined, options?: DaysFormatOptions): string;
31
68
  /**
32
69
  * Reads a number written in the locale's format: "1,250.50" gives 1250.5, and in `de-DE` "1.250,50" does.
33
70
  * Returns `null` for empty text (also a lone minus sign or decimal separator) and `NaN` for text that is not a number.
@@ -75,3 +112,122 @@ export interface StepOptions {
75
112
  * step 1 goes up to 3, and stay within `min` and `max`. An empty field starts at 0, or at the limit nearest to 0.
76
113
  */
77
114
  export declare function stepNumber(value: number | null, direction: 1 | -1, options?: StepOptions): number;
115
+ /** The figures of a price (`priceAmounts`). Each may be missing. */
116
+ export interface PriceAmountsInput {
117
+ /** The price of one. Missing: 0. */
118
+ price?: number | null;
119
+ /** How many. Missing: 1. */
120
+ quantity?: number | null;
121
+ /** The discount off the amount, in percent: `5` for 5 %. Missing: 0. */
122
+ discountPercentage?: number | null;
123
+ /** The VAT on what is left after the discount, in percent: `13` for 13 %. Missing: 0. */
124
+ vatPercentage?: number | null;
125
+ }
126
+ /** What a price comes to (`priceAmounts`), each figure at the money's places. */
127
+ export interface PriceAmounts {
128
+ /** The price times the quantity. */
129
+ amount: number;
130
+ /** The discount off the amount. */
131
+ discountAmount: number;
132
+ /** The VAT on the amount less the discount. */
133
+ vatAmount: number;
134
+ /** The amount less the discount, plus the VAT: what is paid. */
135
+ netAmount: number;
136
+ }
137
+ /**
138
+ * What a price comes to, by the Nexus server's own rule (`SubscriberRenewals.Price`, 2026-06-20's `FN_CalculateAmount`):
139
+ * the amount is the price times the quantity; the discount comes off it; the VAT is on what is left; each rounded to
140
+ * `places` (default 2), half away from zero, so the lines add up to the net amount exactly as a receipt prints them. The
141
+ * sums are done on exact decimals, never on floating-point numbers, so a browser shows the server's figure to the paisa.
142
+ * A missing, null or NaN figure counts as 0, and a missing quantity as 1.
143
+ *
144
+ * @example priceAmounts({ price: 1000, discountPercentage: 5, vatPercentage: 13 }) // { amount: 1000, discountAmount: 50, vatAmount: 123.5, netAmount: 1073.5 }
145
+ * @example priceAmounts({ price: 500, quantity: 3, discountPercentage: 10, vatPercentage: 13 }).netAmount // 1525.5
146
+ */
147
+ export declare function priceAmounts(input: PriceAmountsInput, places?: number): PriceAmounts;
148
+ /** What `priceFromNet` is given beside the net amount it is asked for. */
149
+ export interface PriceFromNetOptions {
150
+ /** How many. Missing, null, 0 or NaN: 1, since no price of none comes to anything. */
151
+ quantity?: number | null;
152
+ /** The discount off the amount, in percent. Missing: 0. Below 100. */
153
+ discountPercentage?: number | null;
154
+ /** The VAT on what is left after the discount, in percent. Missing: 0. Above -100. */
155
+ vatPercentage?: number | null;
156
+ /** The places the price is kept at (the price input's `decimals`). Default 2. */
157
+ decimals?: number;
158
+ /** The places money is kept at, as `priceAmounts`'s `places`. Default 2. */
159
+ places?: number;
160
+ }
161
+ /** A price found for a net amount (`priceFromNet`). */
162
+ export interface PriceFromNetFound {
163
+ ok: true;
164
+ /** The price of one, at `decimals` places. */
165
+ price: number;
166
+ /** What that price comes to by the server's rule: the net amount asked for when `exact`, else the nearest one. */
167
+ netAmount: number;
168
+ /** Whether `netAmount` is exactly what was asked for. */
169
+ exact: boolean;
170
+ }
171
+ /** A net amount no price can be found for (`priceFromNet`), and why, in words for the person. */
172
+ export interface PriceFromNetRefused {
173
+ ok: false;
174
+ message: string;
175
+ }
176
+ /**
177
+ * The price that comes to a net amount by the server's own rule (`priceAmounts`): the inverse a person wants when they
178
+ * know the final figure (2,500.00) and the discount and VAT, but not the price. Plain division (2500 / 1.13 / 0.9) is only
179
+ * an estimate, because every step is rounded: it often lands 0.01 off. So the answer is searched among the prices at the
180
+ * price's own places (`decimals`), each worked forward on exact decimals exactly as the server does.
181
+ *
182
+ * The net amount never falls as the price rises (for a discount below 100 % and VAT above -100 %), so the search is
183
+ * exact: **when some price reaches the figure exactly, the smallest such price is answered** (`exact: true`); when none
184
+ * does, the price whose net amount is nearest (a tie goes to the one above, so the total is never short of the figure
185
+ * asked), with that net amount (`exact: false`). A negative figure, a negative quantity, a discount of 100 % or more
186
+ * (every price comes to 0) and VAT of -100 % or less are refused with a message.
187
+ *
188
+ * @example priceFromNet(2500, { discountPercentage: 10, vatPercentage: 13 }) // { ok: true, price: 2458.21, netAmount: 2500, exact: true }
189
+ */
190
+ export declare function priceFromNet(target: number, options?: PriceFromNetOptions): PriceFromNetFound | PriceFromNetRefused;
191
+ /** The fields a row of money keeps its figures in (`priceLadder`). Defaults: the server's `CounterAmounts`. */
192
+ export interface PriceLadderKeys {
193
+ /** The price of one. Default `unitPrice`. */
194
+ rate?: string;
195
+ /** The price times the quantity. Default `amount`. */
196
+ amount?: string;
197
+ /** Default `discountPercentage`. */
198
+ discountPercentage?: string;
199
+ /** The discount off the amount. Default `discountAmount`. */
200
+ discount?: string;
201
+ /** Default `vatPercentage`. */
202
+ vatPercentage?: string;
203
+ /** The VAT on what is left. Default `vatAmount`. */
204
+ vat?: string;
205
+ /** What is paid. Default `netAmount`. */
206
+ total?: string;
207
+ }
208
+ /** One line of a money ladder (`priceLadder`): the row's field it reads, its words, and its figure. */
209
+ export interface PriceLadderLine {
210
+ /** The row's field: `"vatAmount"`. */
211
+ key: string;
212
+ /** `"VAT (13%)"`. */
213
+ label: string;
214
+ value: number;
215
+ }
216
+ /** How `priceLadder` reads a row and words its lines. */
217
+ export interface PriceLadderOptions {
218
+ /** The row's fields, where they are not the server's `CounterAmounts` names. */
219
+ keys?: PriceLadderKeys;
220
+ /** The locale a percentage is written in: the person's (`useNumberLocale()`). Default `en-US`. */
221
+ locale?: string;
222
+ /** The total's words. Default "Calculated amount". */
223
+ totalLabel?: string;
224
+ }
225
+ /**
226
+ * A receipt's money ladder as lines any page can draw: **Rate** (left out when the row has none), **Amount**,
227
+ * **Discount (5%)** and **VAT (13%)** (each left out at 0, as a receipt leaves them out), then the total, "Calculated
228
+ * amount". A percentage is written with up to 4 places and no trailing zeros (5, 2.5, 3.3333) in `locale`, so a
229
+ * person whose decimal mark is a comma reads "Discount (2,5%)". The figures are the row's own, never worked out here.
230
+ *
231
+ * @example priceLadder(line, { locale: useNumberLocale() }).map(item => `${item.label} ${formatMoney(item.value, { locale })}`)
232
+ */
233
+ export declare function priceLadder(row: Readonly<Record<string, unknown>>, options?: PriceLadderOptions): PriceLadderLine[];
@@ -84,6 +84,38 @@ export function formatNumber(value, options = {}) {
84
84
  // The base locales write "," between groups and "." before decimals; a key's own marks take their places.
85
85
  return custom ? text.replace(/[,.]/g, mark => (mark === "," ? custom.group : custom.decimal)) : text;
86
86
  }
87
+ /**
88
+ * Money as a company writes it: its currency, then the amount grouped with a fixed number of decimals, the same in a
89
+ * table's `money` column (`DataTableProps.currency`), a dashboard's figures and a printed receipt.
90
+ *
91
+ * @example formatMoney(1250, { currency: "Rs." }) // "Rs. 1,250.00"
92
+ * @example formatMoney(null, { currency: "Rs.", empty: "-" }) // "-"
93
+ */
94
+ export function formatMoney(value, options = {}) {
95
+ const { currency, decimals = 2, locale, empty = "" } = options;
96
+ if (value === null || value === undefined || !Number.isFinite(value))
97
+ return empty;
98
+ const figures = formatNumber(value, { decimals, fixedDecimals: true, grouping: true, locale });
99
+ const before = (currency ?? "").trim();
100
+ return before ? `${before} ${figures}` : figures;
101
+ }
102
+ /** "day" for exactly one, "days" for any other count: 0, 0.5, 1.5, 2. */
103
+ export function dayWord(value) {
104
+ return value === 1 || value === -1 ? "day" : "days";
105
+ }
106
+ /**
107
+ * A number of days as a person reads it: a leave's length, a balance. Grouped, with no more decimals than it has.
108
+ *
109
+ * @example formatDays(1) // "1 day"
110
+ * @example formatDays(2.5) // "2.5 days"
111
+ */
112
+ export function formatDays(value, options = {}) {
113
+ const { decimals = 2, locale, empty = "" } = options;
114
+ if (value === null || value === undefined || !Number.isFinite(value))
115
+ return empty;
116
+ const shown = roundDecimal(value, decimals);
117
+ return `${formatNumber(shown, { decimals, grouping: true, locale })} ${dayWord(shown)}`;
118
+ }
87
119
  /**
88
120
  * Reads a number written in the locale's format: "1,250.50" gives 1250.5, and in `de-DE` "1.250,50" does.
89
121
  * Returns `null` for empty text (also a lone minus sign or decimal separator) and `NaN` for text that is not a number.
@@ -238,3 +270,213 @@ export function stepNumber(value, direction, options = {}) {
238
270
  function escapeRegExp(text) {
239
271
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
240
272
  }
273
+ const BIG_ZERO = BigInt(0);
274
+ const BIG_ONE = BigInt(1);
275
+ const BIG_TWO = BigInt(2);
276
+ const BIG_TEN = BigInt(10);
277
+ /** A number as the exact decimal its shortest text writes (1.005 is 1.005, never 1.00499…): a figure that is not finite is `missing`. */
278
+ function exactDecimal(value, missing) {
279
+ const figure = typeof value === "number" && Number.isFinite(value) ? value : missing;
280
+ const [mantissa = "0", exponent = "0"] = String(figure).toLowerCase().split("e");
281
+ const negative = mantissa.startsWith("-");
282
+ const [whole = "0", fraction = ""] = mantissa.replace("-", "").split(".");
283
+ let units = BigInt(`${whole}${fraction}`);
284
+ let scale = fraction.length - Number(exponent);
285
+ if (scale < 0) {
286
+ units *= BIG_TEN ** BigInt(-scale);
287
+ scale = 0;
288
+ }
289
+ return { units: negative ? -units : units, scale };
290
+ }
291
+ /** Rounded to `places`, half away from zero (0.005 is 0.01, -0.005 is -0.01), as .NET's `MidpointRounding.AwayFromZero`. */
292
+ function roundExact(value, places) {
293
+ if (value.scale <= places)
294
+ return { units: value.units * BIG_TEN ** BigInt(places - value.scale), scale: places };
295
+ const divisor = BIG_TEN ** BigInt(value.scale - places);
296
+ const negative = value.units < BIG_ZERO;
297
+ const size = negative ? -value.units : value.units;
298
+ let units = size / divisor;
299
+ if ((size % divisor) * BIG_TWO >= divisor)
300
+ units += BIG_ONE;
301
+ return { units: negative ? -units : units, scale: places };
302
+ }
303
+ /** The exact decimal as a number: through its text, so 1073.50 is the number 1073.5 exactly as typed. */
304
+ function exactNumber(value) {
305
+ const negative = value.units < BIG_ZERO;
306
+ const digits = (negative ? -value.units : value.units).toString().padStart(value.scale + 1, "0");
307
+ const text = value.scale ? `${digits.slice(0, -value.scale)}.${digits.slice(-value.scale)}` : digits;
308
+ return Number(`${negative ? "-" : ""}${text}`) || 0;
309
+ }
310
+ /**
311
+ * What a price comes to, by the Nexus server's own rule (`SubscriberRenewals.Price`, 2026-06-20's `FN_CalculateAmount`):
312
+ * the amount is the price times the quantity; the discount comes off it; the VAT is on what is left; each rounded to
313
+ * `places` (default 2), half away from zero, so the lines add up to the net amount exactly as a receipt prints them. The
314
+ * sums are done on exact decimals, never on floating-point numbers, so a browser shows the server's figure to the paisa.
315
+ * A missing, null or NaN figure counts as 0, and a missing quantity as 1.
316
+ *
317
+ * @example priceAmounts({ price: 1000, discountPercentage: 5, vatPercentage: 13 }) // { amount: 1000, discountAmount: 50, vatAmount: 123.5, netAmount: 1073.5 }
318
+ * @example priceAmounts({ price: 500, quantity: 3, discountPercentage: 10, vatPercentage: 13 }).netAmount // 1525.5
319
+ */
320
+ export function priceAmounts(input, places = 2) {
321
+ const digits = moneyPlaces(places);
322
+ const figures = priceExact(exactDecimal(input.price, 0), exactDecimal(input.quantity, 1), exactDecimal(input.discountPercentage, 0), exactDecimal(input.vatPercentage, 0), digits);
323
+ return {
324
+ amount: exactNumber(figures.amount),
325
+ discountAmount: exactNumber(figures.discount),
326
+ vatAmount: exactNumber(figures.vat),
327
+ netAmount: exactNumber(figures.net),
328
+ };
329
+ }
330
+ /** The places money is kept at: a whole number from 0 to `MAX_NUMBER_DIGITS`. */
331
+ function moneyPlaces(places) {
332
+ return Math.max(0, Math.min(MAX_NUMBER_DIGITS, Math.trunc(places) || 0));
333
+ }
334
+ /** A percent of an exact decimal: the product, two places further down. */
335
+ function percentOf(base, rate) {
336
+ return { units: base.units * rate.units, scale: base.scale + rate.scale + 2 };
337
+ }
338
+ /** The server's rule on exact decimals (`priceAmounts`): each figure rounded to `digits`, half away from zero. */
339
+ function priceExact(price, quantity, discountRate, vatRate, digits) {
340
+ const amount = roundExact({ units: price.units * quantity.units, scale: price.scale + quantity.scale }, digits);
341
+ const discount = roundExact(percentOf(amount, discountRate), digits);
342
+ const vat = roundExact(percentOf({ units: amount.units - discount.units, scale: digits }, vatRate), digits);
343
+ const net = { units: amount.units - discount.units + vat.units, scale: digits };
344
+ return { amount, discount, vat, net };
345
+ }
346
+ /**
347
+ * The price that comes to a net amount by the server's own rule (`priceAmounts`): the inverse a person wants when they
348
+ * know the final figure (2,500.00) and the discount and VAT, but not the price. Plain division (2500 / 1.13 / 0.9) is only
349
+ * an estimate, because every step is rounded: it often lands 0.01 off. So the answer is searched among the prices at the
350
+ * price's own places (`decimals`), each worked forward on exact decimals exactly as the server does.
351
+ *
352
+ * The net amount never falls as the price rises (for a discount below 100 % and VAT above -100 %), so the search is
353
+ * exact: **when some price reaches the figure exactly, the smallest such price is answered** (`exact: true`); when none
354
+ * does, the price whose net amount is nearest (a tie goes to the one above, so the total is never short of the figure
355
+ * asked), with that net amount (`exact: false`). A negative figure, a negative quantity, a discount of 100 % or more
356
+ * (every price comes to 0) and VAT of -100 % or less are refused with a message.
357
+ *
358
+ * @example priceFromNet(2500, { discountPercentage: 10, vatPercentage: 13 }) // { ok: true, price: 2458.21, netAmount: 2500, exact: true }
359
+ */
360
+ export function priceFromNet(target, options = {}) {
361
+ const digits = moneyPlaces(options.places ?? 2);
362
+ const priceDigits = moneyPlaces(options.decimals ?? 2);
363
+ if (typeof target !== "number" || !Number.isFinite(target))
364
+ return { ok: false, message: "Type a figure." };
365
+ const wanted = roundExact(exactDecimal(target, 0), digits).units;
366
+ if (wanted < BIG_ZERO)
367
+ return { ok: false, message: "The amount cannot be below 0." };
368
+ const rawQuantity = options.quantity;
369
+ if (typeof rawQuantity === "number" && rawQuantity < 0)
370
+ return { ok: false, message: "A quantity below 0 has no price." };
371
+ const quantity = exactDecimal(rawQuantity === 0 ? 1 : rawQuantity, 1);
372
+ const discount = options.discountPercentage;
373
+ if (typeof discount === "number" && discount >= 100)
374
+ return wanted === BIG_ZERO ? found(0, 0, true) : { ok: false, message: "With a 100% discount every price comes to 0." };
375
+ const vat = options.vatPercentage;
376
+ if (typeof vat === "number" && vat <= -100)
377
+ return wanted === BIG_ZERO ? found(0, 0, true) : { ok: false, message: "With VAT of -100% every price comes to 0." };
378
+ const discountRate = exactDecimal(discount, 0);
379
+ const vatRate = exactDecimal(vat, 0);
380
+ const netAt = (units) => priceExact({ units, scale: priceDigits }, quantity, discountRate, vatRate, digits).net.units;
381
+ // The estimate, by plain division, as the start of the search.
382
+ const q = Number(rawQuantity) || 1;
383
+ const estimate = target / q / (1 - (Number(discount) || 0) / 100) / (1 + (Number(vat) || 0) / 100);
384
+ const start = Number.isFinite(estimate) && estimate > 0 ? BigInt(Math.round(estimate * 10 ** priceDigits)) : BIG_ZERO;
385
+ // Bracket the smallest price whose net amount reaches the figure: `low` falls short of it (-1: no price below), `high`
386
+ // reaches it. Galloping from the estimate, which is a step or two away, then halving.
387
+ let low = -BIG_ONE;
388
+ let high;
389
+ if (netAt(start) >= wanted) {
390
+ high = start;
391
+ let step = BIG_ONE;
392
+ while (high > BIG_ZERO) {
393
+ const next = high - step < BIG_ZERO ? BIG_ZERO : high - step;
394
+ if (netAt(next) >= wanted)
395
+ high = next;
396
+ else {
397
+ low = next;
398
+ break;
399
+ }
400
+ step *= BIG_TWO;
401
+ }
402
+ }
403
+ else {
404
+ low = start;
405
+ let step = BIG_ONE;
406
+ for (;;) {
407
+ const next = low + step;
408
+ if (netAt(next) >= wanted) {
409
+ high = next;
410
+ break;
411
+ }
412
+ low = next;
413
+ step *= BIG_TWO;
414
+ }
415
+ }
416
+ while (high - low > BIG_ONE) {
417
+ const middle = (low + high) / BIG_TWO;
418
+ if (netAt(middle) >= wanted)
419
+ high = middle;
420
+ else
421
+ low = middle;
422
+ }
423
+ const reached = netAt(high);
424
+ if (reached === wanted || low < BIG_ZERO)
425
+ return found(high, reached, reached === wanted);
426
+ // None reaches it exactly: the nearer of the price just below and the one just above.
427
+ const below = netAt(low);
428
+ return wanted - below < reached - wanted ? found(low, below, false) : found(high, reached, false);
429
+ function found(price, net, exact) {
430
+ return { ok: true, price: exactNumber({ units: BigInt(price), scale: priceDigits }), netAmount: exactNumber({ units: BigInt(net), scale: digits }), exact };
431
+ }
432
+ }
433
+ /** Most places a percentage keeps (12.3456 %), as a price list keeps it; trailing zeros are not written. */
434
+ const PERCENT_PLACES = 4;
435
+ /** A row's figure as a number: a number or the text of one; anything else is missing. */
436
+ function figureOf(row, key) {
437
+ const value = row[key];
438
+ if (typeof value === "number")
439
+ return Number.isFinite(value) ? value : null;
440
+ if (typeof value === "string" && value.trim() !== "") {
441
+ const number = Number(value);
442
+ return Number.isFinite(number) ? number : null;
443
+ }
444
+ return null;
445
+ }
446
+ /**
447
+ * A receipt's money ladder as lines any page can draw: **Rate** (left out when the row has none), **Amount**,
448
+ * **Discount (5%)** and **VAT (13%)** (each left out at 0, as a receipt leaves them out), then the total, "Calculated
449
+ * amount". A percentage is written with up to 4 places and no trailing zeros (5, 2.5, 3.3333) in `locale`, so a
450
+ * person whose decimal mark is a comma reads "Discount (2,5%)". The figures are the row's own, never worked out here.
451
+ *
452
+ * @example priceLadder(line, { locale: useNumberLocale() }).map(item => `${item.label} ${formatMoney(item.value, { locale })}`)
453
+ */
454
+ export function priceLadder(row, options = {}) {
455
+ const { keys = {}, locale, totalLabel = "Calculated amount" } = options;
456
+ const key = {
457
+ rate: keys.rate ?? "unitPrice",
458
+ amount: keys.amount ?? "amount",
459
+ discountPercentage: keys.discountPercentage ?? "discountPercentage",
460
+ discount: keys.discount ?? "discountAmount",
461
+ vatPercentage: keys.vatPercentage ?? "vatPercentage",
462
+ vat: keys.vat ?? "vatAmount",
463
+ total: keys.total ?? "netAmount",
464
+ };
465
+ const percent = (field, words) => {
466
+ const rate = figureOf(row, field);
467
+ return rate === null ? words : `${words} (${formatNumber(rate, { decimals: PERCENT_PLACES, locale })}%)`;
468
+ };
469
+ const lines = [];
470
+ const rate = figureOf(row, key.rate);
471
+ if (rate !== null)
472
+ lines.push({ key: key.rate, label: "Rate", value: rate });
473
+ lines.push({ key: key.amount, label: "Amount", value: figureOf(row, key.amount) ?? 0 });
474
+ const discount = figureOf(row, key.discount);
475
+ if (discount)
476
+ lines.push({ key: key.discount, label: percent(key.discountPercentage, "Discount"), value: discount });
477
+ const vat = figureOf(row, key.vat);
478
+ if (vat)
479
+ lines.push({ key: key.vat, label: percent(key.vatPercentage, "VAT"), value: vat });
480
+ lines.push({ key: key.total, label: totalLabel, value: figureOf(row, key.total) ?? 0 });
481
+ return lines;
482
+ }
@@ -0,0 +1,42 @@
1
+ import type { DropdownItem, DropdownPage, DropdownPageRequest, OptionValue } from "../Interfaces/InputInterfaces.ts";
2
+ /** What `pagedPicker` needs to know of the items. */
3
+ export interface PagedPickerOptions<TItem extends DropdownItem> {
4
+ /** The field that holds the value. Default `id`. */
5
+ valueKey?: string;
6
+ /** The field shown as the option. Default `name`. */
7
+ labelKey?: string;
8
+ /** A field shown under it. */
9
+ descriptionKey?: string;
10
+ /** Items offered but not picked again: someone who has left. They show tagged when a form holds them. */
11
+ isInactive?: (item: TItem) => boolean;
12
+ /** A lookup by key, for a server that has one: asked before the pages are searched. */
13
+ find?: (keys: readonly string[], signal?: AbortSignal) => Promise<readonly TItem[]>;
14
+ /** How many of the server's first pages are searched for a value nothing named. Default 4. */
15
+ findPages?: number;
16
+ /** Items per page while searching them. Default 50. */
17
+ findPageSize?: number;
18
+ }
19
+ /** The dropdown params `pagedPicker` gives: spread them into a dropdown's input. */
20
+ export interface PagedPickerParams<TItem extends DropdownItem> {
21
+ loadOptions: (request: DropdownPageRequest) => Promise<DropdownPage<TItem>>;
22
+ resolveOptions: (values: OptionValue[]) => Promise<readonly TItem[]>;
23
+ peekOption: (key: string) => TItem | undefined;
24
+ valueKey: string;
25
+ labelKey: string;
26
+ descriptionKey?: string;
27
+ isOptionInactive: (item: TItem) => boolean;
28
+ }
29
+ /**
30
+ * A dropdown over a server's search, paged (`load` takes the dropdown's own request), that names every value it holds:
31
+ * from the pages it has shown, the items it was handed, the server's lookup, or the server's first pages.
32
+ *
33
+ * @example
34
+ * const people = pagedPicker(request => mail.people(request), { isInactive: person => person.isActive === false });
35
+ * const inputs = [{ type: EInputTypes.Dropdown, name: "to", label: "To", multiple: true, required: true, ...people }];
36
+ */
37
+ export declare function pagedPicker<TItem extends DropdownItem>(load: (request: DropdownPageRequest) => Promise<DropdownPage<TItem>>, options?: PagedPickerOptions<TItem>): PagedPickerParams<TItem>;
38
+ /**
39
+ * Hands a picker items the page already holds, so a form that opens with their values names them at once, with no call:
40
+ * the people a new message goes to by default.
41
+ */
42
+ export declare function holdPickerItems<TItem extends DropdownItem>(picker: PagedPickerParams<TItem>, items: readonly TItem[]): void;
@@ -0,0 +1,85 @@
1
+ import { isAbortError } from "./ApiResponses.js";
2
+ import { itemValue, optionKey } from "./DropdownHelpers.js";
3
+ /** The items each picker has seen, by key, kept beside its params so the params stay a dropdown's alone. */
4
+ const seenBy = new WeakMap();
5
+ /**
6
+ * A dropdown over a server's search, paged (`load` takes the dropdown's own request), that names every value it holds:
7
+ * from the pages it has shown, the items it was handed, the server's lookup, or the server's first pages.
8
+ *
9
+ * @example
10
+ * const people = pagedPicker(request => mail.people(request), { isInactive: person => person.isActive === false });
11
+ * const inputs = [{ type: EInputTypes.Dropdown, name: "to", label: "To", multiple: true, required: true, ...people }];
12
+ */
13
+ export function pagedPicker(load, options = {}) {
14
+ const { valueKey = "id", labelKey = "name", descriptionKey, isInactive, find, findPages = 4, findPageSize = 50 } = options;
15
+ const seen = new Map();
16
+ const hold = (items) => {
17
+ for (const item of items) {
18
+ const value = itemValue(item, valueKey);
19
+ if (value !== undefined)
20
+ seen.set(optionKey(value), item);
21
+ }
22
+ };
23
+ /** Searches the server's first pages for keys nothing has named, stopping at the list's end or when all are found. */
24
+ async function searchPages(missing) {
25
+ const controller = new AbortController();
26
+ for (let page = 1; page <= findPages && missing.size > 0; page++) {
27
+ const answer = await load({ search: "", page, pageSize: findPageSize, skip: (page - 1) * findPageSize, signal: controller.signal });
28
+ hold(answer.items);
29
+ for (const item of answer.items)
30
+ missing.delete(optionKey(itemValue(item, valueKey)));
31
+ const ended = answer.hasMore === false || answer.items.length === 0 || (answer.hasMore === undefined && answer.items.length < findPageSize);
32
+ if (ended)
33
+ return;
34
+ }
35
+ }
36
+ const params = {
37
+ async loadOptions(request) {
38
+ const answer = await load(request);
39
+ hold(answer.items);
40
+ return answer;
41
+ },
42
+ async resolveOptions(values) {
43
+ const keys = values.map(optionKey);
44
+ const missing = new Set(keys.filter(key => !seen.has(key)));
45
+ try {
46
+ if (missing.size > 0 && find) {
47
+ const found = await find([...missing]);
48
+ hold(found);
49
+ for (const item of found)
50
+ missing.delete(optionKey(itemValue(item, valueKey)));
51
+ }
52
+ if (missing.size > 0)
53
+ await searchPages(missing);
54
+ }
55
+ catch (error) {
56
+ // A value the dropdown cannot name shows as "Not found"; a failed lookup is not worth more than that, but the
57
+ // dropdown's own abort still ends the call.
58
+ if (isAbortError(error))
59
+ throw error;
60
+ }
61
+ return keys.map(key => seen.get(key)).filter((item) => item !== undefined);
62
+ },
63
+ peekOption: key => seen.get(key),
64
+ valueKey,
65
+ labelKey,
66
+ ...(descriptionKey && { descriptionKey }),
67
+ isOptionInactive: item => isInactive?.(item) === true,
68
+ };
69
+ seenBy.set(params, seen);
70
+ return params;
71
+ }
72
+ /**
73
+ * Hands a picker items the page already holds, so a form that opens with their values names them at once, with no call:
74
+ * the people a new message goes to by default.
75
+ */
76
+ export function holdPickerItems(picker, items) {
77
+ const seen = seenBy.get(picker);
78
+ if (!seen)
79
+ return;
80
+ for (const item of items) {
81
+ const value = itemValue(item, picker.valueKey);
82
+ if (value !== undefined)
83
+ seen.set(optionKey(value), item);
84
+ }
85
+ }
@@ -1,6 +1,7 @@
1
1
  /*
2
2
  * Permissions: may the user call this endpoint? Components ask before they show a button or a list (the CRUD's Add,
3
- * Delete, and its Trash list), so a user sees only what they may do; the server still checks every call.
3
+ * Delete, and its Trash list, a table's spreadsheet items), so a user sees only what they may do; the server still
4
+ * checks every call.
4
5
  *
5
6
  * `Permissions` is the interface an app implements, for its own backend's rules; `NexusPermissions` reads the Nexus
6
7
  * backend's access actions. Register one with `definePermissions`. Until then everything is allowed, which suits an
@@ -0,0 +1,26 @@
1
+ export type QrLevel = "L" | "M" | "Q" | "H";
2
+ /** A finished QR code. Modules are addressed by column x and row y, from the top-left corner. */
3
+ export interface QrCode {
4
+ /** 1 to 40: the code is 17 + 4 x version modules a side. */
5
+ readonly version: number;
6
+ /** Modules a side, without the quiet zone. */
7
+ readonly size: number;
8
+ readonly level: QrLevel;
9
+ /** The mask pattern, 0 to 7, that scored best. */
10
+ readonly mask: number;
11
+ /** Whether the module at column x, row y is dark. Outside the code, in its quiet zone, every module is light. */
12
+ isDark(x: number, y: number): boolean;
13
+ }
14
+ /**
15
+ * The text as a QR code at the level asked (M unless told), in the smallest version that holds it. Throws when the
16
+ * text is too long even for version 40: 2,953 bytes of UTF-8 at L, 2,331 at M, 1,663 at Q, 1,273 at H.
17
+ */
18
+ export declare function encodeQr(text: string, level?: QrLevel): QrCode;
19
+ /**
20
+ * The code as one SVG path in a square viewBox, with `border` light modules around it (4, the quiet zone the standard
21
+ * asks for). Each run of dark modules along a row is one rectangle.
22
+ */
23
+ export declare function qrPath(code: QrCode, border?: number): {
24
+ viewBox: string;
25
+ d: string;
26
+ };