nexus-shared 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +2268 -446
  2. package/LICENSE +21 -21
  3. package/README.md +1705 -1084
  4. package/dist/Client.Index.d.ts +17 -0
  5. package/dist/Client.Index.js +17 -0
  6. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  7. package/dist/Components/Documents/DocumentFrame.js +83 -0
  8. package/dist/Components/Documents/Menu.d.ts +4 -0
  9. package/dist/Components/Documents/Menu.js +40 -24
  10. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  11. package/dist/Components/Documents/PrintDocument.js +62 -0
  12. package/dist/Components/Documents/Tabs.d.ts +59 -0
  13. package/dist/Components/Documents/Tabs.js +70 -0
  14. package/dist/Components/Forms/ApiForm.js +19 -2
  15. package/dist/Components/Forms/Crud.d.ts +2 -2
  16. package/dist/Components/Forms/Crud.js +871 -377
  17. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  18. package/dist/Components/Forms/CrudActions.js +184 -0
  19. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  20. package/dist/Components/Forms/CrudHistory.js +140 -0
  21. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  22. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  23. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  24. package/dist/Components/Forms/CrudParts.js +56 -5
  25. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  26. package/dist/Components/Forms/CrudRowForms.js +114 -0
  27. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  28. package/dist/Components/Forms/CrudScopes.js +76 -0
  29. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  30. package/dist/Components/Forms/CrudTree.js +363 -0
  31. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  32. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  33. package/dist/Components/Forms/ExcelImport.js +3 -3
  34. package/dist/Components/Forms/Form.d.ts +4 -4
  35. package/dist/Components/Forms/Form.js +125 -16
  36. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  37. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  38. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  39. package/dist/Components/Forms/RecordFlows.js +282 -0
  40. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  41. package/dist/Components/Forms/SubmitForm.js +195 -10
  42. package/dist/Components/Inputs/CheckBox.js +1 -1
  43. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  44. package/dist/Components/Inputs/ChoiceField.js +28 -2
  45. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  46. package/dist/Components/Inputs/CodeBox.js +316 -0
  47. package/dist/Components/Inputs/DatePicker.js +4 -1
  48. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  49. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  50. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  51. package/dist/Components/Inputs/Dropdown.js +8 -5
  52. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  53. package/dist/Components/Inputs/DropdownParts.js +16 -4
  54. package/dist/Components/Inputs/Field.d.ts +7 -1
  55. package/dist/Components/Inputs/Field.js +2 -2
  56. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  57. package/dist/Components/Inputs/GroupForm.js +10 -11
  58. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  59. package/dist/Components/Inputs/InputRenderer.js +39 -31
  60. package/dist/Components/Inputs/NumberBox.js +4 -2
  61. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  62. package/dist/Components/Inputs/PictureInput.js +70 -0
  63. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  64. package/dist/Components/Inputs/RadioGroup.js +8 -3
  65. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  66. package/dist/Components/Inputs/RowsInput.js +18 -3
  67. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  68. package/dist/Components/Inputs/TabularForm.js +29 -29
  69. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  70. package/dist/Components/Inputs/TextArea.js +14 -2
  71. package/dist/Components/Inputs/TextBox.js +4 -1
  72. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  73. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  74. package/dist/Components/Inputs/TimePicker.js +4 -1
  75. package/dist/Components/Inputs/TreeView.d.ts +107 -6
  76. package/dist/Components/Inputs/TreeView.js +513 -30
  77. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  78. package/dist/Components/Layouts/MessageBox.js +56 -7
  79. package/dist/Components/Layouts/Popup.d.ts +13 -6
  80. package/dist/Components/Layouts/Popup.js +37 -9
  81. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  82. package/dist/Components/Layouts/PopupDock.js +2 -1
  83. package/dist/Components/Layouts/PopupHost.js +5 -4
  84. package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
  85. package/dist/Components/Layouts/ThemePicker.js +16 -4
  86. package/dist/Components/Viewers/DataTable.js +205 -61
  87. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  88. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  89. package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
  90. package/dist/Components/Viewers/DataTableParts.js +62 -21
  91. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  92. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  93. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  94. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  95. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  96. package/dist/Components/Viewers/RecordCards.js +509 -0
  97. package/dist/Helpers/ApiModules.d.ts +3 -34
  98. package/dist/Helpers/ApiModules.js +3 -3
  99. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  100. package/dist/Helpers/ApiRoutes.js +31 -27
  101. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  102. package/dist/Helpers/ArrayHelpers.js +2 -0
  103. package/dist/Helpers/BusyHooks.d.ts +6 -0
  104. package/dist/Helpers/BusyHooks.js +39 -0
  105. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  106. package/dist/Helpers/CodeHelpers.js +332 -0
  107. package/dist/Helpers/CrudBackend.d.ts +114 -6
  108. package/dist/Helpers/CrudBackend.js +214 -11
  109. package/dist/Helpers/CrudColumns.d.ts +98 -0
  110. package/dist/Helpers/CrudColumns.js +160 -0
  111. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  112. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  113. package/dist/Helpers/CrudHelpers.d.ts +130 -6
  114. package/dist/Helpers/CrudHelpers.js +304 -12
  115. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  116. package/dist/Helpers/CrudNarrowPick.js +157 -0
  117. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  118. package/dist/Helpers/CrudRowForms.js +43 -0
  119. package/dist/Helpers/CrudScopes.d.ts +32 -0
  120. package/dist/Helpers/CrudScopes.js +79 -0
  121. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  122. package/dist/Helpers/DropdownHelpers.js +6 -2
  123. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  124. package/dist/Helpers/ExcelHelpers.js +12 -11
  125. package/dist/Helpers/FileAdapter.d.ts +78 -0
  126. package/dist/Helpers/FileAdapter.js +102 -0
  127. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  128. package/dist/Helpers/FilePrimaries.js +138 -0
  129. package/dist/Helpers/FormConfirm.d.ts +32 -0
  130. package/dist/Helpers/FormConfirm.js +40 -0
  131. package/dist/Helpers/FormStore.d.ts +90 -5
  132. package/dist/Helpers/FormStore.js +318 -22
  133. package/dist/Helpers/InputConditions.d.ts +50 -0
  134. package/dist/Helpers/InputConditions.js +168 -0
  135. package/dist/Helpers/InputMemory.d.ts +106 -0
  136. package/dist/Helpers/InputMemory.js +385 -0
  137. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  138. package/dist/Helpers/InputNarrowing.js +67 -0
  139. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  140. package/dist/Helpers/InputParamsHelpers.js +162 -71
  141. package/dist/Helpers/MessageBuilder.js +2 -0
  142. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  143. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  144. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  145. package/dist/Helpers/NexusHelpers.js +1 -0
  146. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  147. package/dist/Helpers/NumberHelpers.js +126 -0
  148. package/dist/Helpers/Permissions.js +2 -1
  149. package/dist/Helpers/QrCode.d.ts +26 -0
  150. package/dist/Helpers/QrCode.js +463 -0
  151. package/dist/Helpers/RecordCards.d.ts +205 -0
  152. package/dist/Helpers/RecordCards.js +378 -0
  153. package/dist/Helpers/RequestCache.d.ts +47 -0
  154. package/dist/Helpers/RequestCache.js +132 -0
  155. package/dist/Helpers/RowsStore.d.ts +37 -2
  156. package/dist/Helpers/RowsStore.js +128 -10
  157. package/dist/Helpers/ServerPreview.d.ts +69 -0
  158. package/dist/Helpers/ServerPreview.js +99 -0
  159. package/dist/Helpers/TableColumns.js +4 -3
  160. package/dist/Helpers/TableExport.d.ts +7 -1
  161. package/dist/Helpers/TableExport.js +21 -15
  162. package/dist/Helpers/TableFilters.d.ts +1 -1
  163. package/dist/Helpers/TableFilters.js +24 -23
  164. package/dist/Helpers/TableHelpers.d.ts +24 -3
  165. package/dist/Helpers/TableHelpers.js +58 -12
  166. package/dist/Helpers/TableJoins.d.ts +83 -0
  167. package/dist/Helpers/TableJoins.js +284 -0
  168. package/dist/Helpers/TableStore.d.ts +26 -1
  169. package/dist/Helpers/TableStore.js +73 -7
  170. package/dist/Helpers/TreeStore.d.ts +306 -11
  171. package/dist/Helpers/TreeStore.js +1048 -90
  172. package/dist/Helpers/UserPreferences.d.ts +8 -0
  173. package/dist/Helpers/UserPreferences.js +17 -0
  174. package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
  175. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  176. package/dist/Interfaces/FileInterfaces.js +4 -0
  177. package/dist/Interfaces/FormInterfaces.d.ts +393 -29
  178. package/dist/Interfaces/FormInterfaces.js +79 -1
  179. package/dist/Interfaces/InputInterfaces.d.ts +39 -3
  180. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  181. package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
  182. package/dist/Interfaces/PreferenceInterfaces.js +2 -5
  183. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  184. package/dist/Interfaces/RecordInterfaces.js +1 -0
  185. package/dist/Interfaces/TableInterfaces.d.ts +160 -5
  186. package/dist/Services/ApiProxy.d.ts +36 -1
  187. package/dist/Services/ApiProxy.js +182 -52
  188. package/dist/Services/ApiProxyRules.d.ts +203 -0
  189. package/dist/Services/ApiProxyRules.js +426 -0
  190. package/dist/Services/ExcelApi.js +5 -0
  191. package/dist/Services/FileApi.d.ts +13 -0
  192. package/dist/Services/FileApi.js +29 -0
  193. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  194. package/dist/Services/ImageEditorLoader.js +51 -0
  195. package/dist/Services/JoinSources.d.ts +80 -0
  196. package/dist/Services/JoinSources.js +150 -0
  197. package/dist/Services/ModuleImport.d.ts +7 -0
  198. package/dist/Services/ModuleImport.js +22 -0
  199. package/dist/Services/ServerApi.d.ts +13 -2
  200. package/dist/Services/ServerApi.js +8 -3
  201. package/dist/Shared.Index.d.ts +20 -0
  202. package/dist/Shared.Index.js +20 -0
  203. package/package.json +92 -85
  204. package/src/Styles/Nexus.Avatar.css +80 -79
  205. package/src/Styles/Nexus.Base.css +117 -117
  206. package/src/Styles/Nexus.Button.css +186 -180
  207. package/src/Styles/Nexus.Calendar.css +273 -273
  208. package/src/Styles/Nexus.Chat.css +1443 -1443
  209. package/src/Styles/Nexus.Choice.css +359 -343
  210. package/src/Styles/Nexus.Crud.css +542 -168
  211. package/src/Styles/Nexus.DateTime.css +81 -81
  212. package/src/Styles/Nexus.Dropdown.css +733 -725
  213. package/src/Styles/Nexus.Editor.css +295 -295
  214. package/src/Styles/Nexus.Excel.css +389 -389
  215. package/src/Styles/Nexus.Files.css +410 -0
  216. package/src/Styles/Nexus.Form.css +282 -279
  217. package/src/Styles/Nexus.Index.css +41 -37
  218. package/src/Styles/Nexus.Input.css +238 -4
  219. package/src/Styles/Nexus.Markdown.css +237 -237
  220. package/src/Styles/Nexus.Menu.css +246 -237
  221. package/src/Styles/Nexus.Popup.css +800 -724
  222. package/src/Styles/Nexus.Print.css +288 -0
  223. package/src/Styles/Nexus.QrCode.css +12 -0
  224. package/src/Styles/Nexus.Record.css +430 -0
  225. package/src/Styles/Nexus.Rows.css +637 -637
  226. package/src/Styles/Nexus.Slider.css +276 -276
  227. package/src/Styles/Nexus.Tab.Buttons.css +497 -432
  228. package/src/Styles/Nexus.Table.css +98 -35
  229. package/src/Styles/Nexus.Theme.Picker.css +59 -3
  230. package/src/Styles/Nexus.Themes.css +200 -194
  231. package/src/Styles/Nexus.Time.css +144 -144
  232. package/src/Styles/Nexus.Toast.css +263 -263
  233. package/src/Styles/Nexus.Tokens.css +94 -89
  234. package/src/Styles/Nexus.Tree.css +530 -431
@@ -0,0 +1,332 @@
1
+ import { DEFAULT_FIELD_NAME, requiredMessage } from "./ValidationHelpers.js";
2
+ /*
3
+ * Codes typed one character to a box: a one-time code, a PIN, an SSN, a licence key, a hex colour. Pure functions, so a
4
+ * form checks a code it loaded the way the box checks one typed, without a browser.
5
+ */
6
+ /** The boxes a code has when it names neither `length` nor `groups`. */
7
+ export const DEFAULT_CODE_LENGTH = 6;
8
+ /** One character of each kind, as a regular expression. */
9
+ export const CODE_CHARACTER_PATTERNS = {
10
+ digits: "[0-9]",
11
+ letters: "[A-Za-z]",
12
+ alphanumeric: "[0-9A-Za-z]",
13
+ hex: "[0-9A-Fa-f]",
14
+ };
15
+ /** What messages call the characters: "The code needs all 6 digits." */
16
+ const CHARACTER_NAMES = { digits: "digits", letters: "letters", alphanumeric: "characters", hex: "hex digits" };
17
+ /** What a code takes, for "SSN takes digits only." */
18
+ const ONLY_NAMES = { digits: "digits", letters: "letters", alphanumeric: "letters and digits", hex: "hex digits, 0–9 and A–F" };
19
+ /** The boxes in their groups: `groups` when given (`[3, 2, 4]`), else one group of `length`, 6 by default. */
20
+ export function codeGroups(rules) {
21
+ const groups = (rules.groups ?? []).filter(size => Number.isInteger(size) && size > 0);
22
+ if (groups.length)
23
+ return groups;
24
+ const { length = DEFAULT_CODE_LENGTH } = rules;
25
+ return [Number.isInteger(length) && length > 0 ? length : DEFAULT_CODE_LENGTH];
26
+ }
27
+ /** How many boxes a code has: 9 for `[3, 2, 4]`. */
28
+ export const codeLength = (rules) => codeGroups(rules).reduce((sum, size) => sum + size, 0);
29
+ const characterTests = new Map();
30
+ /** Whether one character is one the code takes. Built once per pattern, since a form may check thousands of codes. */
31
+ function characterTest(rules) {
32
+ const source = rules.allowed || CODE_CHARACTER_PATTERNS[rules.characters ?? "digits"];
33
+ let test = characterTests.get(source);
34
+ if (!test) {
35
+ test = new RegExp(`^(?:${source})$`, "u");
36
+ characterTests.set(source, test);
37
+ }
38
+ return test;
39
+ }
40
+ function withCase(text, letterCase = "upper") {
41
+ return letterCase === "upper" ? text.toUpperCase() : letterCase === "lower" ? text.toLowerCase() : text;
42
+ }
43
+ /**
44
+ * Text as the code holds it: letters in the code's case, every character it does not take dropped, and cut to its
45
+ * length: "123-45-6789" is "123456789" for an SSN. The box never does this to a value on its own: text that arrives in
46
+ * it at once is read by `readCodeText`, which finds the code in a message, and a saved value by `loadCode`, which keeps
47
+ * what does not fit so its check can say so.
48
+ */
49
+ export function normalizeCode(text, rules = {}) {
50
+ if (!text)
51
+ return "";
52
+ const test = characterTest(rules);
53
+ const length = codeLength(rules);
54
+ const kept = [];
55
+ for (const character of withCase(text, rules.letterCase)) {
56
+ if (kept.length === length)
57
+ break;
58
+ if (test.test(character))
59
+ kept.push(character);
60
+ }
61
+ return kept.join("");
62
+ }
63
+ /** One space, dash, slash or dot between the groups of a code written out: "482 913", "123-45-6789", "03/04/1990". */
64
+ const GROUP_JOINER = /^[\s./–-]$/u;
65
+ /**
66
+ * A code as it was saved or given (a form's loaded record, `value`, `defaultValue`), as the box holds it. It loses only
67
+ * what writing a code out adds: letters take the code's case, and separators the code does not take - spaces, dashes,
68
+ * slashes, dots, the field's own `separator`, and a hex code's leading "#" - come out: "123-45-6789" is "123456789".
69
+ * Nothing else changes. A character the code does not take, or one past its boxes, stays, so its check says what is
70
+ * wrong ("SSN takes digits only.", "The code is 6 digits long, not 7.") and a form never saves it back changed.
71
+ */
72
+ export function loadCode(value, shape = {}) {
73
+ if (value === null || value === undefined)
74
+ return "";
75
+ const test = characterTest(shape);
76
+ const separator = shape.separator ?? "";
77
+ const hex = shape.characters === "hex" && !shape.allowed;
78
+ let kept = "";
79
+ for (const character of withCase(String(value), shape.letterCase)) {
80
+ const writtenOut = GROUP_JOINER.test(character) || (separator !== "" && separator.includes(character)) || (hex && character === "#" && kept === "");
81
+ if (writtenOut && !test.test(character))
82
+ continue;
83
+ kept += character;
84
+ }
85
+ return kept;
86
+ }
87
+ /** Whether the boxes can hold a code as it is: every character one the code takes, and no more than the boxes. */
88
+ export function codeFits(value, rules = {}) {
89
+ const characters = [...(value ?? "")];
90
+ if (characters.length > codeLength(rules))
91
+ return false;
92
+ const test = characterTest(rules);
93
+ return characters.every(character => test.test(withCase(character, rules.letterCase)));
94
+ }
95
+ /** What a description calls one character of a code, and several: "6 digits", "1 letter". */
96
+ const DESCRIBED_NAMES = {
97
+ digits: ["digit", "digits"],
98
+ letters: ["letter", "letters"],
99
+ alphanumeric: ["letter or digit", "letters and digits"],
100
+ hex: ["hex digit", "hex digits"],
101
+ };
102
+ /**
103
+ * What a screen reader is told about the boxes it cannot see, after the label: how many characters of which kind, in
104
+ * which groups, and the hint in each box - "6 digits", "9 digits, in groups of 3, 2 and 4", "8 digits: DD/MM/YYYY".
105
+ */
106
+ export function describeCode(shape = {}) {
107
+ const groups = codeGroups(shape);
108
+ const total = codeLength(shape);
109
+ const [one, many] = shape.allowed ? ["character", "characters"] : DESCRIBED_NAMES[shape.characters ?? "digits"];
110
+ const count = `${total} ${total === 1 ? one : many}`;
111
+ const hints = [...(shape.slotPlaceholder ?? "")];
112
+ if (hints.length === total && total > 1)
113
+ return `${count}: ${formatCode(hints.join(""), { groups, separator: shape.separator })}`;
114
+ if (groups.length < 2)
115
+ return count;
116
+ const sizes = groups.map(String);
117
+ return `${count}, in groups of ${sizes.slice(0, -1).join(", ")} and ${sizes[sizes.length - 1]}`;
118
+ }
119
+ /**
120
+ * The code in text that arrives at once - a paste, a drop, a keyboard's suggestion, autofill, an input method's word.
121
+ * It is whole when the text holds one code of exactly as many characters as the boxes: one run of them ("482913" in
122
+ * "Your 2-step code is 482913"), or one run split by single spaces, dashes, slashes or dots ("482 913", "123-45-6789"),
123
+ * or every character the text holds when they number exactly that ("(482) 913"). Otherwise its characters are a part of
124
+ * a code, or `null` when they are more than the boxes ("Codes 4829135 and 1234567"): a message is never guessed at.
125
+ */
126
+ export function readCodeText(text, rules = {}) {
127
+ const test = characterTest(rules);
128
+ const total = codeLength(rules);
129
+ // The runs of characters the code takes, each marked when a single joiner links it to the run before.
130
+ const runs = [];
131
+ let run = null;
132
+ let gap = [];
133
+ for (const character of withCase(text ?? "", rules.letterCase)) {
134
+ if (!test.test(character)) {
135
+ run = null;
136
+ gap.push(character);
137
+ continue;
138
+ }
139
+ if (!run) {
140
+ const joiner = gap.length === 1 ? gap[0] : undefined;
141
+ run = { characters: [], joined: runs.length > 0 && joiner !== undefined && GROUP_JOINER.test(joiner) };
142
+ runs.push(run);
143
+ }
144
+ run.characters.push(character);
145
+ gap = [];
146
+ }
147
+ // Each way of writing a whole code; two different ones are no answer.
148
+ const candidates = new Set();
149
+ for (const run of runs) {
150
+ if (run.characters.length === total)
151
+ candidates.add(run.characters.join(""));
152
+ }
153
+ let group = [];
154
+ let parts = 0;
155
+ for (const [index, run] of runs.entries()) {
156
+ if (index > 0 && !run.joined) {
157
+ if (parts > 1 && group.length === total)
158
+ candidates.add(group.join(""));
159
+ group = [];
160
+ parts = 0;
161
+ }
162
+ group.push(...run.characters);
163
+ parts++;
164
+ }
165
+ if (parts > 1 && group.length === total)
166
+ candidates.add(group.join(""));
167
+ if (candidates.size === 1) {
168
+ const [code = ""] = candidates;
169
+ return { code, whole: true };
170
+ }
171
+ const all = runs.flatMap(run => run.characters);
172
+ if (all.length > total)
173
+ return null;
174
+ return { code: all.join(""), whole: all.length === total };
175
+ }
176
+ /**
177
+ * A full code with its caret at the end selects its last box, so the next key replaces it and Backspace or Delete
178
+ * remove it: the box with the ring is the one they act on. Any other edit is returned as it is.
179
+ */
180
+ export function settleCode(edit, rules = {}) {
181
+ const total = codeLength(rules);
182
+ if (edit.start === edit.end && edit.start >= total && [...edit.value].length === total)
183
+ return { value: edit.value, start: total - 1, end: total };
184
+ return edit;
185
+ }
186
+ /**
187
+ * A code after text arrives at its caret - the same for a key, a phone's keyboard, an input method and a paste. A whole
188
+ * code (see `readCodeText`) replaces everything. Otherwise each character goes in as if typed: over the selection, or at
189
+ * a caret into the box after it - inserted while the code has an empty box, so a gap Backspace or Delete left takes back
190
+ * what was there and the boxes after it move along, and over that box once the code is full. A key at the end of a full
191
+ * code takes the last box; the rest of a paste that finds no box left is not typed. `null` when the text changes nothing.
192
+ */
193
+ export function typeCode(edit, text, rules = {}) {
194
+ const read = readCodeText(text, rules);
195
+ if (!read || !read.code)
196
+ return null;
197
+ const total = codeLength(rules);
198
+ if (read.whole)
199
+ return settleCode({ value: read.code, start: total, end: total }, rules);
200
+ const characters = [...edit.value];
201
+ let start = Math.max(0, Math.min(edit.start, characters.length));
202
+ let end = Math.max(start, Math.min(edit.end, characters.length));
203
+ let typed = 0;
204
+ for (const character of read.code) {
205
+ if (end > start) {
206
+ characters.splice(start, end - start, character);
207
+ }
208
+ else if (characters.length < total) {
209
+ characters.splice(start, 0, character);
210
+ }
211
+ else if (start < total) {
212
+ characters[start] = character;
213
+ }
214
+ else if (typed === 0) {
215
+ start = total - 1;
216
+ characters[start] = character;
217
+ }
218
+ else {
219
+ break;
220
+ }
221
+ start += 1;
222
+ end = start;
223
+ typed += 1;
224
+ }
225
+ return settleCode({ value: characters.join(""), start, end }, rules);
226
+ }
227
+ /**
228
+ * Where ← → Home End take a code's caret: a box at a time, and a filled box it lands on is selected, so a key replaces
229
+ * it and Backspace or Delete remove it. It stops at the first empty box, where the next character goes, or on a full
230
+ * code at the last box. With several boxes selected, ← goes to the first of them and → to the last. End on a saved code
231
+ * longer than its boxes goes to its very end, so Backspace takes what the boxes cannot show.
232
+ */
233
+ export function moveCode(edit, key, rules = {}) {
234
+ const total = codeLength(rules);
235
+ const length = [...edit.value].length;
236
+ if (key === "End" && length > total)
237
+ return { value: edit.value, start: edit.value.length, end: edit.value.length };
238
+ const furthest = Math.min(length, total - 1);
239
+ let box;
240
+ if (key === "Home")
241
+ box = 0;
242
+ else if (key === "End")
243
+ box = furthest;
244
+ else if (edit.end - edit.start > 1)
245
+ box = key === "ArrowLeft" ? edit.start : edit.end - 1;
246
+ else
247
+ box = Math.min(edit.start, total - 1) + (key === "ArrowLeft" ? -1 : 1);
248
+ box = Math.max(0, Math.min(box, furthest));
249
+ return { value: edit.value, start: box, end: box < length ? box + 1 : box };
250
+ }
251
+ /**
252
+ * A code after the browser changed its input itself - a phone's keyboard, an input method's word, autofill, a drop,
253
+ * Backspace or Delete - worked out from the edit before the change and the input as the browser left it. Text that
254
+ * arrived goes in by `typeCode`, so every way of typing follows one model, and text it does not take leaves the code as
255
+ * it was; a removal stands as the browser made it, the boxes after it closed up.
256
+ */
257
+ export function codeAfterInput(before, after, rules = {}) {
258
+ if (after.value === before.value)
259
+ return after;
260
+ const head = before.value.slice(0, before.start);
261
+ const tail = before.value.slice(before.end);
262
+ let start = before.start;
263
+ let end = before.end;
264
+ let arrived;
265
+ if (after.value.length >= head.length + tail.length && after.value.startsWith(head) && after.value.endsWith(tail)) {
266
+ // Text in the selection's place or at the caret: a key, a keyboard's word, an input method's commit.
267
+ arrived = after.value.slice(head.length, after.value.length - tail.length);
268
+ }
269
+ else {
270
+ // Anything else, such as Backspace at a caret or autofill: the part that differs.
271
+ start = 0;
272
+ while (start < before.value.length && start < after.value.length && before.value[start] === after.value[start])
273
+ start++;
274
+ let same = 0;
275
+ while (same < before.value.length - start &&
276
+ same < after.value.length - start &&
277
+ before.value[before.value.length - 1 - same] === after.value[after.value.length - 1 - same]) {
278
+ same++;
279
+ }
280
+ end = before.value.length - same;
281
+ arrived = after.value.slice(start, after.value.length - same);
282
+ }
283
+ if (!arrived)
284
+ return after;
285
+ return typeCode({ value: before.value, start, end }, arrived, rules) ?? before;
286
+ }
287
+ /**
288
+ * Checks a code against its rules and returns the first message, or `null` when it is valid: required, every box
289
+ * filled, and only the characters it takes. Pure and DOM-free, like `validateText`.
290
+ */
291
+ export function validateCode(value, rules, fieldName = DEFAULT_FIELD_NAME) {
292
+ const code = (value ?? "").trim();
293
+ if (code) {
294
+ const kind = rules.characters ?? "digits";
295
+ const test = characterTest(rules);
296
+ const characters = [...code];
297
+ if (characters.some(character => !test.test(withCase(character, rules.letterCase)))) {
298
+ return rules.allowed ? `${fieldName} has a character it does not take.` : `${fieldName} takes ${ONLY_NAMES[kind]} only.`;
299
+ }
300
+ const length = codeLength(rules);
301
+ const names = rules.allowed ? "characters" : CHARACTER_NAMES[kind];
302
+ if (characters.length < length)
303
+ return `${fieldName} needs all ${length} ${names}.`;
304
+ if (characters.length > length)
305
+ return `${fieldName} is ${length} ${names} long, not ${characters.length}.`;
306
+ }
307
+ else if (rules.required) {
308
+ return requiredMessage(fieldName);
309
+ }
310
+ return rules.validate?.(code) || null;
311
+ }
312
+ /**
313
+ * A code as text to read, in its groups: "123-45-6789" with `separator: "-"`, "482 913" for `[3, 3]` with none, and a dot
314
+ * for each character when masked. "" for no code.
315
+ */
316
+ export function formatCode(value, rules = {}) {
317
+ const characters = [...(value ?? "")].map(character => (rules.masked ? "•" : character));
318
+ if (!characters.length)
319
+ return "";
320
+ const parts = [];
321
+ let start = 0;
322
+ for (const size of codeGroups(rules)) {
323
+ if (start >= characters.length)
324
+ break;
325
+ parts.push(characters.slice(start, start + size).join(""));
326
+ start += size;
327
+ }
328
+ // A code longer than its boxes (a value from elsewhere) keeps the rest, so nothing is hidden.
329
+ if (start < characters.length)
330
+ parts.push(characters.slice(start).join(""));
331
+ return parts.join(rules.separator || " ");
332
+ }
@@ -1,6 +1,23 @@
1
- import type { CrudMassActionName, CrudMode } from "../Interfaces/CrudInterfaces.ts";
2
- import type { TableKey, TablePage, TableQuery, TableRow } from "../Interfaces/TableInterfaces.ts";
1
+ import type { ApiQuery } from "../Interfaces/ApiInterfaces.ts";
2
+ import type { CrudMark, CrudMassActionName, CrudMode } from "../Interfaces/CrudInterfaces.ts";
3
+ import type { TableKey, TablePage, TableQuery, TableRow, TableTag } from "../Interfaces/TableInterfaces.ts";
3
4
  type Row = Record<string, unknown>;
5
+ /** What a write of marks sends beside the endpoint's own path: query values, a body, or both. */
6
+ export interface CrudMarksRequest {
7
+ query?: ApiQuery;
8
+ body?: unknown;
9
+ }
10
+ /** The words of a record's history, by the numbers the backend keeps (`CrudBackend.auditWords`). */
11
+ export interface CrudAuditWords {
12
+ /** What a request did, by its command number: "Added", "Moved to the trash". */
13
+ commands: Readonly<Record<number, string>>;
14
+ /** Audit keys that are the whole story, by number: "Flagged", "Made a favourite". */
15
+ directions: Readonly<Record<number, string>>;
16
+ /** The part of the record an update changed, by audit key: "basic information". */
17
+ parts: Readonly<Record<number, string>>;
18
+ /** The command number of a plain update: the one whose audit key names a part. */
19
+ update?: number;
20
+ }
4
21
  export declare abstract class CrudBackend {
5
22
  /**
6
23
  * What a server table sends for one page, from the query. `sortKey` is the sorted column's server name. `filters` are
@@ -16,9 +33,57 @@ export declare abstract class CrudBackend {
16
33
  abstract isFlagged(row: TableRow): boolean;
17
34
  /** The row with its flag set or cleared. */
18
35
  abstract withFlag<TRow extends TableRow>(row: TRow, on: boolean): TRow;
36
+ /** Whether a record can be flagged or unflagged at all. Default yes. */
37
+ canFlag(row: TableRow): boolean;
38
+ /**
39
+ * What flagging one record or many sends beside the key(s): the state wanted, never a toggle. Default the Nexus
40
+ * backend's `?isFlagged=true`. A backend that toggles ignores it, and a CRUD sends only the records a change is for.
41
+ */
42
+ flagRequest(on: boolean): CrudMarksRequest;
19
43
  abstract isPinned(row: TableRow): boolean;
20
44
  /** The row with its pin set or cleared. */
21
45
  abstract withPin<TRow extends TableRow>(row: TRow, on: boolean): TRow;
46
+ /**
47
+ * This user's marks on a record as a set of bits (`CRUD_MARK_BITS`: pinned 1, favourite 2), as a bookmark listing
48
+ * answers them on each record. Bits the package does not know are kept as they are. Default: the pin alone, through
49
+ * `isPinned`.
50
+ */
51
+ marksOf(row: TableRow): number;
52
+ /** The row carrying this set of marks. Default: the pin alone, through `withPin`. */
53
+ withMarks<TRow extends TableRow>(row: TRow, marks: number): TRow;
54
+ /**
55
+ * What setting one record's marks sends beside its key: the **whole set** it is to have, never a toggle. Default the
56
+ * Nexus backend's: `?bookmarkTypes=3`.
57
+ */
58
+ marksRequest(marks: number): CrudMarksRequest;
59
+ /**
60
+ * What putting one mark on many records, or taking it off, sends: the keys, the mark and which way. Each record keeps
61
+ * its other marks. Default the Nexus backend's: the keys as the body, `?bookmarkTypes=2&isAdding=true`.
62
+ */
63
+ massMarksRequest(keys: TableKey[], mark: CrudMark, on: boolean): CrudMarksRequest;
64
+ /** The marks a write left on the record, read from its answer. `null` when it does not say, and the set sent stands. */
65
+ answeredMarks(result: unknown): number | null;
66
+ /** What an export sends to name the list on screen, beside the table's own filters. Default nothing. */
67
+ exportFilters(mode: CrudMode): Record<string, unknown>;
68
+ /** A record status in words, for a status column and the history: "Active", "Flagged", "Trashed". Default the number. */
69
+ statusText(status: number): string;
70
+ /**
71
+ * The short tag a reference to a record carries when that record is no longer available - "Trashed" beside a parent in
72
+ * the trash, "Deleted" beside one deleted - read from its status. `null` for a live record, and for no status at all.
73
+ * A joined column (`crudColumns.dbJoin`, `crudColumns.clientJoin`) draws it after the name. Default: none.
74
+ */
75
+ unavailableTag(status: unknown): TableTag | null;
76
+ /**
77
+ * Whether a record may be picked in a form: what a picker offers, and what it shows tagged and will not offer again when
78
+ * a form already holds it. Read from the record. Default: every record.
79
+ */
80
+ isPickable(record: TableRow): boolean;
81
+ /**
82
+ * The words of a record's history: what each command did (`commands`), the audit keys that are the whole story of a
83
+ * switch (`directions`), and the parts of the record an update names (`parts`, with `update` the command that names
84
+ * one). Numbers an audit row keeps for good, so each backend's own. Default none: "Action 3".
85
+ */
86
+ get auditWords(): CrudAuditWords;
22
87
  /**
23
88
  * Changes an action's answer makes to rows, by key: a mass flag that answers each record's new status. Rows without
24
89
  * an entry take the change the action stands for (a toggled flag). Default: none.
@@ -34,16 +99,25 @@ export declare abstract class CrudBackend {
34
99
  * `null` when it says nothing about it - a bare key, an empty body - and then the caller keeps what it has.
35
100
  *
36
101
  * **This is what spares a page its reload.** A save or a mark whose answer carries the record is written into the
37
- * table, the pinned band and every list kept in the cache, and nothing is loaded again. Only answers of actions on
102
+ * table, the sections above it and every list kept in the cache, and nothing is loaded again. Only answers of actions on
38
103
  * **one** record are read here; a mass answer goes through `answeredChanges`, record by record.
39
104
  */
40
105
  answeredRow<TRow extends TableRow>(result: unknown): Partial<TRow> | null;
41
106
  /**
42
107
  * The key an answer gives in place of the record: `Create` answering the new record's id. `null` when the answer
43
- * is the record itself, or says nothing. With a key the new record goes straight into the list, and where there is
44
- * a `details` endpoint it is read back on its own - one small request, never the whole list again.
108
+ * is the record itself, or says nothing. With a key the new record goes straight into the list, finished by `savedRow`;
109
+ * only a record whose list still cannot be told is read back on its own through `details` - never the whole list again.
45
110
  */
46
111
  answeredKey(result: unknown): TableKey | null;
112
+ /**
113
+ * A record a save wrote - what the form sent, under what the answer said - with the fields only the server keeps
114
+ * filled in as this backend writes them, so nothing has to be read back: a status, the times, the marks. `before` is
115
+ * the record as the page holds it now (an edit; `undefined` for a new one): an edit's form carries its own copy of those
116
+ * fields from when it opened, which a flag or a mark made since has left behind. Default: the record as it is - and a
117
+ * record whose list still cannot be told from it (`listMembership`) is read back once through `details`, where there is
118
+ * one.
119
+ */
120
+ savedRow<TRow extends TableRow>(action: "create" | "edit", record: TRow, before?: TRow): TRow;
47
121
  /**
48
122
  * Whether a record belongs in a list, read from the record itself. It is what lets one answer be written into the
49
123
  * lists that are **not** on screen: a flagged record joins the flagged list, a trashed one leaves the active list
@@ -65,16 +139,41 @@ export declare const NEXUS_DELETED_STATUS = 60;
65
139
  /**
66
140
  * The Nexus backend: pages as `{ ...filters, searchKeyword, skipCount, maxCount, sorting, sortType }` answered with
67
141
  * `{ results, totalCount, filterCount }`, newest first unless a column is sorted; mass actions send the ids
68
- * (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 25, a pinned one `isPinned`.
142
+ * (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 25, and this user's marks are
143
+ * its `bookmarkTypes` (`[Flags] EBookmarkTypes`: 1 pinned, 2 favourite), or `isPinned` from a backend before them.
69
144
  */
70
145
  export declare class NexusCrudBackend extends CrudBackend {
71
146
  pageRequest(query: TableQuery, sortKey: string | undefined, filters?: Readonly<Record<string, unknown>>): unknown;
72
147
  readPage<TRow extends TableRow>(result: unknown): TablePage<TRow>;
73
148
  massRequest(action: CrudMassActionName, keys: TableKey[], values?: Row): unknown;
149
+ /** The whole flag band, 25-29: every status comparison is a range test. */
74
150
  isFlagged(row: TableRow): boolean;
151
+ /**
152
+ * A live record that is not pending (0-29): the backend flags 0-24 and unflags 25-29, and never touches a pending
153
+ * record (30-39), which would lose that status for good. A record with no status yet is not refused here.
154
+ */
155
+ canFlag(row: TableRow): boolean;
75
156
  withFlag<TRow extends TableRow>(row: TRow, on: boolean): TRow;
76
157
  isPinned(row: TableRow): boolean;
77
158
  withPin<TRow extends TableRow>(row: TRow, on: boolean): TRow;
159
+ marksOf(row: TableRow): number;
160
+ /** `bookmarkTypes`, and `isPinned` too on a record that has one, so neither says something the other does not. */
161
+ withMarks<TRow extends TableRow>(row: TRow, marks: number): TRow;
162
+ /** `SwitchBookmarks` answers the marks it stored, as a number. */
163
+ answeredMarks(result: unknown): number | null;
164
+ /** An export names the list on screen as `flag`, by the number `ECommandTypes` gives its listing. */
165
+ exportFilters(mode: CrudMode): Record<string, unknown>;
166
+ /** The members `ERecordStatus` names, then the band of any other number in it. */
167
+ statusText(status: number): string;
168
+ /**
169
+ * Out of the live band, 40 and up (the owner, 2026-10-08: "if status is not active range, like trash or deleted, show a
170
+ * very short tag"): "Trashed" in the warning color, "Deleted" in the danger color. A flagged or pending record is still
171
+ * live, and carries none.
172
+ */
173
+ unavailableTag(status: unknown): TableTag | null;
174
+ /** Usable, under 30, as a Nexus `ActiveListing` answers: a pending record is live but not one to pick. No status reads as usable. */
175
+ isPickable(record: TableRow): boolean;
176
+ get auditWords(): CrudAuditWords;
78
177
  /** Mass answers list the records they changed, with their new `recordStatus` and `isPinned`. */
79
178
  answeredChanges(result: unknown, rowKey: string): Map<string, Row>;
80
179
  /**
@@ -86,6 +185,15 @@ export declare class NexusCrudBackend extends CrudBackend {
86
185
  answeredRow<TRow extends TableRow>(result: unknown): Partial<TRow> | null;
87
186
  /** `Create` and `Update` answer the record's id: a token when ids are encrypted, the number as text when not. */
88
187
  answeredKey(result: unknown): TableKey | null;
188
+ /**
189
+ * What a Nexus save leaves to the server, written as the server writes it, so nothing is read back (the owner,
190
+ * 2026-10-10: "the creationTime, recordStatus we will provide fake or just right now"). A new record lacking them is
191
+ * Active (every Create writes `NEXUS_ACTIVE_STATUS`), has no marks, was added now and never changed; what its answer
192
+ * said stays. An Update writes none of the status, the marks and the time added, so an edited record has the ones the
193
+ * page holds (`before`) - over its form's older copies - and was changed now. Times are UTC ISO strings, as a date-time
194
+ * column reads them.
195
+ */
196
+ savedRow<TRow extends TableRow>(action: "create" | "edit", record: TRow, before?: TRow): TRow;
89
197
  /**
90
198
  * The bands the owner settled on: under 40 is live, 25–29 of that is flagged, 40–59 is the trash, and 60 and up is
91
199
  * deleted. A row without a `recordStatus` says nothing, and the lists it cannot answer for load again.