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
@@ -26,7 +26,9 @@ export declare const API_ACTIONS: {
26
26
  readonly trash: "TrashPagination";
27
27
  readonly deleted: "DeletePagination";
28
28
  };
29
- /** The records the user pinned, which a CRUD shows above its list. */
29
+ /** The records this user pinned or made favourites, each with its marks: what a CRUD shows above its list. */
30
+ readonly bookmarkListing: "BookmarkListing";
31
+ /** The records the user pinned, from a backend that keeps no favourites. */
30
32
  readonly pinListing: "PinListing";
31
33
  readonly create: "Create";
32
34
  readonly update: "Update";
@@ -35,13 +37,21 @@ export declare const API_ACTIONS: {
35
37
  readonly trashFromDelete: "TrashFromDelete";
36
38
  readonly recover: "Recover";
37
39
  readonly flag: "Flag";
40
+ /** Flags a record or unflags it, the call saying which. */
41
+ readonly switchFlag: "SwitchFlag";
42
+ /** Sets this user's marks on a record, the whole set at once. */
43
+ readonly switchBookmarks: "SwitchBookmarks";
38
44
  readonly pin: "Pin";
39
45
  readonly audit: "AuditListing";
40
46
  readonly accessActions: "AccessActions";
41
47
  readonly massDescription: "MassDescription";
42
48
  readonly massTrash: "MassTrash";
43
49
  readonly massFlag: "MassFlag";
50
+ /** Flags many records or unflags them, the call saying which. */
51
+ readonly massSwitchFlag: "MassSwitchFlag";
44
52
  readonly massPin: "MassPin";
53
+ /** Puts one mark on many records, or takes it off. */
54
+ readonly massSwitchBookmarks: "MassSwitchBookmarks";
45
55
  readonly massDelete: "MassDelete";
46
56
  readonly massRecover: "MassRecover";
47
57
  readonly massTrashFromDelete: "MassTrashFromDelete";
@@ -61,16 +71,26 @@ export declare const apiEndpoint: {
61
71
  delete: <TModule extends ApiModuleValue>(module: TModule, path: string, extras?: EndpointExtras) => ApiEndpoint<TModule>;
62
72
  };
63
73
  /**
64
- * The endpoints of a Nexus controller, named as the backend names them. `subject` is what the controller keeps, for
65
- * messages ("room" → "Room saved.", "3 rooms deleted."); a call can name the record instead (`subject: "Room 204"`).
66
- *
67
- * @example
68
- * export const ROOMS = apiController(NexusModule.Reservation, "Room", "room");
69
- * api.get(ROOMS.details, { params: { id: 5 } }); // GET /api/app/reservation/Room/Detail/5
70
- * api.put(ROOMS.update, values); // PUT /api/app/reservation/Room/Update → "Room saved."
71
- * api.delete(ROOMS.delete, { params: { id: 5 } }); // DELETE .../Room/Delete/5 → "Room deleted."
74
+ * The parts of a controller `without` leaves out, for a backend that does not have them: `pages` (every
75
+ * `...Pagination`), `audit`, `flag` (one record and many), `marks` (the pins and favourites, one record and many),
76
+ * and a single action on many records: `mass.description`, `mass.trashFromDelete`, ...
72
77
  */
73
- export declare function apiController<TModule extends ApiModuleValue>(module: TModule, controller: string, subject?: string): {
78
+ export type ApiControllerPart = "pages" | "audit" | "flag" | "marks" | `mass.${"description" | "trash" | "trashFromDelete" | "delete" | "recover"}`;
79
+ /** What a controller's backend speaks, and what it does not have. */
80
+ export interface ApiControllerOptions {
81
+ /**
82
+ * How it sets a flag and this user's marks. `"switch"`, the default, is the Nexus backend since 2026-10-07: the call
83
+ * carries the state wanted (`SwitchFlag/{id}?isFlagged=`, `SwitchBookmarks/{id}?bookmarkTypes=`, `MassSwitchFlag`,
84
+ * `MassSwitchBookmarks`), and one `BookmarkListing` holds pins and favourites. `"toggle"` is a controller before it:
85
+ * `Flag/{id}`, `Pin/{id}`, `PinListing`, `MassFlag`, `MassPin`, pins and no favourites. A CRUD sends the direction to
86
+ * either, and only the records a change is for, so a toggle lands where it was asked to.
87
+ */
88
+ commands?: "switch" | "toggle";
89
+ /** The parts it does not have, so no list or action offers them: `["pages", "mass.description"]`. */
90
+ without?: readonly ApiControllerPart[];
91
+ }
92
+ /** The endpoints `apiController` gives. Those a controller may not have - its generation decides, or `without` - are optional. */
93
+ export interface ApiControllerEndpoints<TModule extends ApiModuleValue = ApiModuleValue> {
74
94
  module: TModule;
75
95
  controller: string;
76
96
  /** Every active row: GET `Room/NormalListing`. */
@@ -80,13 +100,15 @@ export declare function apiController<TModule extends ApiModuleValue>(module: TM
80
100
  /** Rows by id, for saved values a list no longer shows: POST `Room/FindListing` with the ids. */
81
101
  findListing: ApiEndpoint<TModule>;
82
102
  /** One page of active rows: POST `Room/NormalPagination` with `{ searchKeyword, skipCount, maxCount, sorting, sortType }`. */
83
- pagination: ApiEndpoint<TModule>;
103
+ pagination?: ApiEndpoint<TModule>;
84
104
  /** Every row of each list, for a CRUD that pages in the browser: GET `Room/NormalListing`, `Room/TrashListing`, … */
85
105
  lists: Record<CrudMode, ApiEndpoint<TModule>>;
86
106
  /** One page of each list, for a CRUD that pages on the server: POST `Room/NormalPagination`, `Room/TrashPagination`, … */
87
- pages: Record<CrudMode, ApiEndpoint<TModule>>;
88
- /** The records the user pinned, shown above the list: GET `Room/PinListing`. */
89
- pins: ApiEndpoint<TModule>;
107
+ pages?: Record<CrudMode, ApiEndpoint<TModule>>;
108
+ /** This user's pins and favourites in one list, each with its marks: GET `Room/BookmarkListing` (`"switch"`). */
109
+ bookmarks?: ApiEndpoint<TModule>;
110
+ /** The records the user pinned: GET `Room/PinListing` (`"toggle"`). */
111
+ pins?: ApiEndpoint<TModule>;
90
112
  /** One record: GET `Room/Detail/{id}`. */
91
113
  details: ApiEndpoint<TModule>;
92
114
  create: ApiEndpoint<TModule>;
@@ -95,26 +117,46 @@ export declare function apiController<TModule extends ApiModuleValue>(module: TM
95
117
  trash: ApiEndpoint<TModule>;
96
118
  trashFromDelete: ApiEndpoint<TModule>;
97
119
  recover: ApiEndpoint<TModule>;
98
- flag: ApiEndpoint<TModule>;
99
- pin: ApiEndpoint<TModule>;
120
+ /** Flags a record or unflags it: PUT `Room/SwitchFlag/{id}?isFlagged=` (`"switch"`), or `Room/Flag/{id}` (`"toggle"`). */
121
+ flag?: ApiEndpoint<TModule>;
122
+ /** This user's whole set of marks on a record: PUT `Room/SwitchBookmarks/{id}?bookmarkTypes=3` (`"switch"`). */
123
+ bookmark?: ApiEndpoint<TModule>;
124
+ /** A pin toggled: PUT `Room/Pin/{id}` (`"toggle"`). */
125
+ pin?: ApiEndpoint<TModule>;
100
126
  /** A record's history: GET `Room/AuditListing/{ownerId}/{skipCount}`. */
101
- audit: ApiEndpoint<TModule>;
127
+ audit?: ApiEndpoint<TModule>;
102
128
  /** What the user may do here: GET `Room/AccessActions`. */
103
129
  accessActions: ApiEndpoint<TModule>;
104
130
  /** Actions on many rows at once: POST `Room/MassDelete` and the rest, with the ids. */
105
131
  mass: {
106
- description: ApiEndpoint<TModule>;
107
- trash: ApiEndpoint<TModule>;
108
- trashFromDelete: ApiEndpoint<TModule>;
109
- flag: ApiEndpoint<TModule>;
110
- pin: ApiEndpoint<TModule>;
111
- delete: ApiEndpoint<TModule>;
112
- recover: ApiEndpoint<TModule>;
132
+ description?: ApiEndpoint<TModule>;
133
+ trash?: ApiEndpoint<TModule>;
134
+ trashFromDelete?: ApiEndpoint<TModule>;
135
+ /** POST `Room/MassSwitchFlag?isFlagged=` (`"switch"`), or `Room/MassFlag` (`"toggle"`). */
136
+ flag?: ApiEndpoint<TModule>;
137
+ /** POST `Room/MassPin` (`"toggle"`). */
138
+ pin?: ApiEndpoint<TModule>;
139
+ /** One mark on, or off, every record sent: POST `Room/MassSwitchBookmarks?bookmarkTypes=1&isAdding=true` (`"switch"`). */
140
+ bookmark?: ApiEndpoint<TModule>;
141
+ delete?: ApiEndpoint<TModule>;
142
+ recover?: ApiEndpoint<TModule>;
113
143
  };
114
144
  /** Any other action of the controller: `ROOMS.action("PUT", "Block/{id}", { action: "update" })`. */
115
145
  action: (method: HttpMethod, action: string, extras?: EndpointExtras) => ApiEndpoint<TModule>;
116
- };
117
- export type ApiControllerEndpoints = ReturnType<typeof apiController>;
146
+ }
147
+ /**
148
+ * The endpoints of a Nexus controller, named as the backend names them. `subject` is what the controller keeps, for
149
+ * messages ("room" → "Room saved.", "3 rooms deleted."); a call can name the record instead (`subject: "Room 204"`).
150
+ * `options` says which generation of the backend it is and what it lacks (`ApiControllerOptions`).
151
+ *
152
+ * @example
153
+ * export const ROOMS = apiController(NexusModule.Reservation, "Room", "room");
154
+ * export const REGIONS = apiController(NexusModule.Configuration, "Region", "region", { without: ["pages", "mass.description"] });
155
+ * api.get(ROOMS.details, { params: { id: 5 } }); // GET /api/app/reservation/Room/Detail/5
156
+ * api.put(ROOMS.update, values); // PUT /api/app/reservation/Room/Update → "Room saved."
157
+ * api.delete(ROOMS.delete, { params: { id: 5 } }); // DELETE .../Room/Delete/5 → "Room deleted."
158
+ */
159
+ export declare function apiController<TModule extends ApiModuleValue>(module: TModule, controller: string, subject?: string, options?: ApiControllerOptions): ApiControllerEndpoints<TModule>;
118
160
  /** Whether a value is an endpoint (not a URL or a request of another shape). */
119
161
  export declare function isApiEndpoint(value: unknown): value is ApiEndpoint;
120
162
  /** Thrown when a path names a value the call did not give, so a request never goes to ".../Detail/undefined". */
@@ -19,7 +19,9 @@ export const API_ACTIONS = {
19
19
  listings: { normal: "NormalListing", flag: "FlagListing", trash: "TrashListing", deleted: "DeleteListing" },
20
20
  /** One page of each list a CRUD shows. */
21
21
  paginations: { normal: "NormalPagination", flag: "FlagPagination", trash: "TrashPagination", deleted: "DeletePagination" },
22
- /** The records the user pinned, which a CRUD shows above its list. */
22
+ /** The records this user pinned or made favourites, each with its marks: what a CRUD shows above its list. */
23
+ bookmarkListing: "BookmarkListing",
24
+ /** The records the user pinned, from a backend that keeps no favourites. */
23
25
  pinListing: "PinListing",
24
26
  create: "Create",
25
27
  update: "Update",
@@ -28,13 +30,21 @@ export const API_ACTIONS = {
28
30
  trashFromDelete: "TrashFromDelete",
29
31
  recover: "Recover",
30
32
  flag: "Flag",
33
+ /** Flags a record or unflags it, the call saying which. */
34
+ switchFlag: "SwitchFlag",
35
+ /** Sets this user's marks on a record, the whole set at once. */
36
+ switchBookmarks: "SwitchBookmarks",
31
37
  pin: "Pin",
32
38
  audit: "AuditListing",
33
39
  accessActions: "AccessActions",
34
40
  massDescription: "MassDescription",
35
41
  massTrash: "MassTrash",
36
42
  massFlag: "MassFlag",
43
+ /** Flags many records or unflags them, the call saying which. */
44
+ massSwitchFlag: "MassSwitchFlag",
37
45
  massPin: "MassPin",
46
+ /** Puts one mark on many records, or takes it off. */
47
+ massSwitchBookmarks: "MassSwitchBookmarks",
38
48
  massDelete: "MassDelete",
39
49
  massRecover: "MassRecover",
40
50
  massTrashFromDelete: "MassTrashFromDelete",
@@ -54,37 +64,36 @@ export const apiEndpoint = {
54
64
  /**
55
65
  * The endpoints of a Nexus controller, named as the backend names them. `subject` is what the controller keeps, for
56
66
  * messages ("room" → "Room saved.", "3 rooms deleted."); a call can name the record instead (`subject: "Room 204"`).
67
+ * `options` says which generation of the backend it is and what it lacks (`ApiControllerOptions`).
57
68
  *
58
69
  * @example
59
70
  * export const ROOMS = apiController(NexusModule.Reservation, "Room", "room");
71
+ * export const REGIONS = apiController(NexusModule.Configuration, "Region", "region", { without: ["pages", "mass.description"] });
60
72
  * api.get(ROOMS.details, { params: { id: 5 } }); // GET /api/app/reservation/Room/Detail/5
61
73
  * api.put(ROOMS.update, values); // PUT /api/app/reservation/Room/Update → "Room saved."
62
74
  * api.delete(ROOMS.delete, { params: { id: 5 } }); // DELETE .../Room/Delete/5 → "Room deleted."
63
75
  */
64
- export function apiController(module, controller, subject) {
76
+ export function apiController(module, controller, subject, options = {}) {
65
77
  const at = (action) => `${controller}/${action}`;
66
78
  const endpoint = (method, path, extras) => ({ module, path, method, ...(subject && { subject }), ...extras });
67
79
  const A = API_ACTIONS;
68
80
  const modes = Object.keys(A.listings);
69
81
  const byMode = (method, actions) => Object.fromEntries(modes.map(mode => [mode, endpoint(method, at(actions[mode]), { action: "load" })]));
82
+ const toggle = options.commands === "toggle";
83
+ const without = new Set(options.without ?? []);
84
+ const has = (part) => !without.has(part);
70
85
  return {
71
86
  module,
72
87
  controller,
73
- /** Every active row: GET `Room/NormalListing`. */
74
88
  listing: endpoint("GET", at(A.listing), { action: "load" }),
75
- /** Active rows for dropdowns: GET `Room/ActiveListing`. */
76
89
  activeListing: endpoint("GET", at(A.activeListing), { action: "load" }),
77
- /** Rows by id, for saved values a list no longer shows: POST `Room/FindListing` with the ids. */
78
90
  findListing: endpoint("POST", at(A.findListing), { action: "load" }),
79
- /** One page of active rows: POST `Room/NormalPagination` with `{ searchKeyword, skipCount, maxCount, sorting, sortType }`. */
80
- pagination: endpoint("POST", at(A.pagination), { action: "load" }),
81
- /** Every row of each list, for a CRUD that pages in the browser: GET `Room/NormalListing`, `Room/TrashListing`, … */
91
+ ...(has("pages") && { pagination: endpoint("POST", at(A.pagination), { action: "load" }), pages: byMode("POST", A.paginations) }),
82
92
  lists: byMode("GET", A.listings),
83
- /** One page of each list, for a CRUD that pages on the server: POST `Room/NormalPagination`, `Room/TrashPagination`, … */
84
- pages: byMode("POST", A.paginations),
85
- /** The records the user pinned, shown above the list: GET `Room/PinListing`. */
86
- pins: endpoint("GET", at(A.pinListing), { action: "load" }),
87
- /** One record: GET `Room/Detail/{id}`. */
93
+ ...(has("marks") &&
94
+ (toggle
95
+ ? { pins: endpoint("GET", at(A.pinListing), { action: "load" }), pin: endpoint("PUT", at(`${A.pin}/{id}`), { action: "pin" }) }
96
+ : { bookmarks: endpoint("GET", at(A.bookmarkListing), { action: "load" }), bookmark: endpoint("PUT", at(`${A.switchBookmarks}/{id}`), { action: "update" }) })),
88
97
  details: endpoint("GET", at(`${A.details}/{id}`), { action: "load" }),
89
98
  create: endpoint("POST", at(A.create), { action: "create" }),
90
99
  update: endpoint("PUT", at(A.update), { action: "save" }),
@@ -92,23 +101,18 @@ export function apiController(module, controller, subject) {
92
101
  trash: endpoint("DELETE", at(`${A.trash}/{id}`), { action: "trash" }),
93
102
  trashFromDelete: endpoint("PUT", at(`${A.trashFromDelete}/{id}`), { action: "trash" }),
94
103
  recover: endpoint("PUT", at(`${A.recover}/{id}`), { action: "recover" }),
95
- flag: endpoint("PUT", at(`${A.flag}/{id}`), { action: "flag" }),
96
- pin: endpoint("PUT", at(`${A.pin}/{id}`), { action: "pin" }),
97
- /** A record's history: GET `Room/AuditListing/{ownerId}/{skipCount}`. */
98
- audit: endpoint("GET", at(`${A.audit}/{ownerId}/{skipCount}`), { action: "load" }),
99
- /** What the user may do here: GET `Room/AccessActions`. */
104
+ ...(has("flag") && { flag: endpoint("PUT", at(`${toggle ? A.flag : A.switchFlag}/{id}`), { action: "flag" }) }),
105
+ ...(has("audit") && { audit: endpoint("GET", at(`${A.audit}/{ownerId}/{skipCount}`), { action: "load" }) }),
100
106
  accessActions: endpoint("GET", at(A.accessActions), { action: "load" }),
101
- /** Actions on many rows at once: POST `Room/MassDelete` and the rest, with the ids. */
102
107
  mass: {
103
- description: endpoint("POST", at(A.massDescription), { action: "update" }),
104
- trash: endpoint("POST", at(A.massTrash), { action: "trash" }),
105
- trashFromDelete: endpoint("POST", at(A.massTrashFromDelete), { action: "trash" }),
106
- flag: endpoint("POST", at(A.massFlag), { action: "flag" }),
107
- pin: endpoint("POST", at(A.massPin), { action: "pin" }),
108
- delete: endpoint("POST", at(A.massDelete), { action: "delete" }),
109
- recover: endpoint("POST", at(A.massRecover), { action: "recover" }),
108
+ ...(has("mass.description") && { description: endpoint("POST", at(A.massDescription), { action: "update" }) }),
109
+ ...(has("mass.trash") && { trash: endpoint("POST", at(A.massTrash), { action: "trash" }) }),
110
+ ...(has("mass.trashFromDelete") && { trashFromDelete: endpoint("POST", at(A.massTrashFromDelete), { action: "trash" }) }),
111
+ ...(has("flag") && { flag: endpoint("POST", at(toggle ? A.massFlag : A.massSwitchFlag), { action: "flag" }) }),
112
+ ...(has("marks") && (toggle ? { pin: endpoint("POST", at(A.massPin), { action: "pin" }) } : { bookmark: endpoint("POST", at(A.massSwitchBookmarks), { action: "update" }) })),
113
+ ...(has("mass.delete") && { delete: endpoint("POST", at(A.massDelete), { action: "delete" }) }),
114
+ ...(has("mass.recover") && { recover: endpoint("POST", at(A.massRecover), { action: "recover" }) }),
110
115
  },
111
- /** Any other action of the controller: `ROOMS.action("PUT", "Block/{id}", { action: "update" })`. */
112
116
  action: (method, action, extras) => endpoint(method, at(action), extras),
113
117
  };
114
118
  }
File without changes
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ // speed proof ts
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Whether to say that something is still under way: `true` only once `busy` has lasted 400ms, and then for at least
3
+ * 300ms, however soon `busy` ends. A status line that reads it says "Loading…" for a slow read and nothing for a quick
4
+ * one. A Crud's details footer and `DocumentFrame` use it.
5
+ */
6
+ export declare function useSlowBusy(busy: boolean): boolean;
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ import { useEffect, useRef, useState } from "react";
3
+ /**
4
+ * A read that ends sooner than this says nothing: most backends answer sooner, and a word shown for a frame or two is
5
+ * the flicker this avoids. Longer than a popup's 160ms fade, so the word never lands on a half-faded popup.
6
+ */
7
+ const SLOW_DELAY = 400;
8
+ /** Once the word shows, it stays at least this long, so a read that ends just after the delay does not blink it. */
9
+ const SLOW_HOLD = 300;
10
+ /**
11
+ * Whether to say that something is still under way: `true` only once `busy` has lasted 400ms, and then for at least
12
+ * 300ms, however soon `busy` ends. A status line that reads it says "Loading…" for a slow read and nothing for a quick
13
+ * one. A Crud's details footer and `DocumentFrame` use it.
14
+ */
15
+ export function useSlowBusy(busy) {
16
+ const [said, setSaid] = useState(false);
17
+ const since = useRef(0);
18
+ useEffect(() => {
19
+ if (busy) {
20
+ if (said)
21
+ return;
22
+ const timer = window.setTimeout(() => {
23
+ since.current = performance.now();
24
+ setSaid(true);
25
+ }, SLOW_DELAY);
26
+ return () => window.clearTimeout(timer);
27
+ }
28
+ if (!said)
29
+ return;
30
+ const left = since.current + SLOW_HOLD - performance.now();
31
+ if (left <= 0) {
32
+ setSaid(false);
33
+ return;
34
+ }
35
+ const timer = window.setTimeout(() => setSaid(false), left);
36
+ return () => window.clearTimeout(timer);
37
+ }, [busy, said]);
38
+ return said;
39
+ }
@@ -0,0 +1,97 @@
1
+ import type { CodeCharacters, CodeRules } from "../Interfaces/InputInterfaces.ts";
2
+ /** The boxes a code has when it names neither `length` nor `groups`. */
3
+ export declare const DEFAULT_CODE_LENGTH = 6;
4
+ /** One character of each kind, as a regular expression. */
5
+ export declare const CODE_CHARACTER_PATTERNS: Record<CodeCharacters, string>;
6
+ /** The boxes in their groups: `groups` when given (`[3, 2, 4]`), else one group of `length`, 6 by default. */
7
+ export declare function codeGroups(rules: Pick<CodeRules, "length" | "groups">): number[];
8
+ /** How many boxes a code has: 9 for `[3, 2, 4]`. */
9
+ export declare const codeLength: (rules: Pick<CodeRules, "length" | "groups">) => number;
10
+ /**
11
+ * Text as the code holds it: letters in the code's case, every character it does not take dropped, and cut to its
12
+ * length: "123-45-6789" is "123456789" for an SSN. The box never does this to a value on its own: text that arrives in
13
+ * it at once is read by `readCodeText`, which finds the code in a message, and a saved value by `loadCode`, which keeps
14
+ * what does not fit so its check can say so.
15
+ */
16
+ export declare function normalizeCode(text: string | null | undefined, rules?: CodeRules): string;
17
+ /** A code's shape and how it is written out: its rules, the separator between groups, and the hint in each box. */
18
+ export type CodeShape = CodeRules & {
19
+ separator?: string;
20
+ slotPlaceholder?: string;
21
+ };
22
+ /**
23
+ * A code as it was saved or given (a form's loaded record, `value`, `defaultValue`), as the box holds it. It loses only
24
+ * what writing a code out adds: letters take the code's case, and separators the code does not take - spaces, dashes,
25
+ * slashes, dots, the field's own `separator`, and a hex code's leading "#" - come out: "123-45-6789" is "123456789".
26
+ * Nothing else changes. A character the code does not take, or one past its boxes, stays, so its check says what is
27
+ * wrong ("SSN takes digits only.", "The code is 6 digits long, not 7.") and a form never saves it back changed.
28
+ */
29
+ export declare function loadCode(value: unknown, shape?: CodeShape): string;
30
+ /** Whether the boxes can hold a code as it is: every character one the code takes, and no more than the boxes. */
31
+ export declare function codeFits(value: string | null | undefined, rules?: CodeRules): boolean;
32
+ /**
33
+ * What a screen reader is told about the boxes it cannot see, after the label: how many characters of which kind, in
34
+ * which groups, and the hint in each box - "6 digits", "9 digits, in groups of 3, 2 and 4", "8 digits: DD/MM/YYYY".
35
+ */
36
+ export declare function describeCode(shape?: CodeShape): string;
37
+ /** A code in its input while it is edited: the characters, and the selection from `start` to `end` (equal for a caret). */
38
+ export interface CodeEdit {
39
+ value: string;
40
+ start: number;
41
+ end: number;
42
+ }
43
+ /** What text that arrives at once holds for a code: the characters the code takes, in its case, and whether they are a whole code. */
44
+ export interface CodeText {
45
+ code: string;
46
+ whole: boolean;
47
+ }
48
+ /** The keys that move a code's caret from box to box. */
49
+ export type CodeMove = "ArrowLeft" | "ArrowRight" | "Home" | "End";
50
+ /**
51
+ * The code in text that arrives at once - a paste, a drop, a keyboard's suggestion, autofill, an input method's word.
52
+ * It is whole when the text holds one code of exactly as many characters as the boxes: one run of them ("482913" in
53
+ * "Your 2-step code is 482913"), or one run split by single spaces, dashes, slashes or dots ("482 913", "123-45-6789"),
54
+ * or every character the text holds when they number exactly that ("(482) 913"). Otherwise its characters are a part of
55
+ * a code, or `null` when they are more than the boxes ("Codes 4829135 and 1234567"): a message is never guessed at.
56
+ */
57
+ export declare function readCodeText(text: string | null | undefined, rules?: CodeRules): CodeText | null;
58
+ /**
59
+ * A full code with its caret at the end selects its last box, so the next key replaces it and Backspace or Delete
60
+ * remove it: the box with the ring is the one they act on. Any other edit is returned as it is.
61
+ */
62
+ export declare function settleCode(edit: CodeEdit, rules?: CodeRules): CodeEdit;
63
+ /**
64
+ * 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
65
+ * code (see `readCodeText`) replaces everything. Otherwise each character goes in as if typed: over the selection, or at
66
+ * a caret into the box after it - inserted while the code has an empty box, so a gap Backspace or Delete left takes back
67
+ * 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
68
+ * code takes the last box; the rest of a paste that finds no box left is not typed. `null` when the text changes nothing.
69
+ */
70
+ export declare function typeCode(edit: CodeEdit, text: string, rules?: CodeRules): CodeEdit | null;
71
+ /**
72
+ * 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
73
+ * it and Backspace or Delete remove it. It stops at the first empty box, where the next character goes, or on a full
74
+ * code at the last box. With several boxes selected, ← goes to the first of them and → to the last. End on a saved code
75
+ * longer than its boxes goes to its very end, so Backspace takes what the boxes cannot show.
76
+ */
77
+ export declare function moveCode(edit: CodeEdit, key: CodeMove, rules?: CodeRules): CodeEdit;
78
+ /**
79
+ * A code after the browser changed its input itself - a phone's keyboard, an input method's word, autofill, a drop,
80
+ * Backspace or Delete - worked out from the edit before the change and the input as the browser left it. Text that
81
+ * arrived goes in by `typeCode`, so every way of typing follows one model, and text it does not take leaves the code as
82
+ * it was; a removal stands as the browser made it, the boxes after it closed up.
83
+ */
84
+ export declare function codeAfterInput(before: CodeEdit, after: CodeEdit, rules?: CodeRules): CodeEdit;
85
+ /**
86
+ * Checks a code against its rules and returns the first message, or `null` when it is valid: required, every box
87
+ * filled, and only the characters it takes. Pure and DOM-free, like `validateText`.
88
+ */
89
+ export declare function validateCode(value: string | null | undefined, rules: CodeRules, fieldName?: string): string | null;
90
+ /**
91
+ * A code as text to read, in its groups: "123-45-6789" with `separator: "-"`, "482 913" for `[3, 3]` with none, and a dot
92
+ * for each character when masked. "" for no code.
93
+ */
94
+ export declare function formatCode(value: string | null | undefined, rules?: Pick<CodeRules, "length" | "groups"> & {
95
+ separator?: string;
96
+ masked?: boolean;
97
+ }): string;