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.
- package/CHANGELOG.md +2268 -446
- package/LICENSE +21 -21
- package/README.md +1705 -1084
- package/dist/Client.Index.d.ts +17 -0
- package/dist/Client.Index.js +17 -0
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Forms/ApiForm.js +19 -2
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +871 -377
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudHistory.d.ts +66 -0
- package/dist/Components/Forms/CrudHistory.js +140 -0
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +30 -2
- package/dist/Components/Forms/CrudParts.js +56 -5
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +114 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
- package/dist/Components/Forms/CrudWorkspace.js +169 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +4 -4
- package/dist/Components/Forms/Form.js +125 -16
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +195 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/CodeBox.d.ts +69 -0
- package/dist/Components/Inputs/CodeBox.js +316 -0
- package/dist/Components/Inputs/DatePicker.js +4 -1
- package/dist/Components/Inputs/DateRangePicker.js +4 -1
- package/dist/Components/Inputs/DateTimePicker.js +4 -1
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -5
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +10 -11
- package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
- package/dist/Components/Inputs/InputRenderer.js +39 -31
- package/dist/Components/Inputs/NumberBox.js +4 -2
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +70 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +1 -1
- package/dist/Components/Inputs/RowsInput.js +18 -3
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +29 -29
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +14 -2
- package/dist/Components/Inputs/TextBox.js +4 -1
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TimePicker.js +4 -1
- package/dist/Components/Inputs/TreeView.d.ts +107 -6
- package/dist/Components/Inputs/TreeView.js +513 -30
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +13 -6
- package/dist/Components/Layouts/Popup.js +37 -9
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
- package/dist/Components/Layouts/ThemePicker.js +16 -4
- package/dist/Components/Viewers/DataTable.js +205 -61
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
- package/dist/Components/Viewers/DataTableParts.js +62 -21
- package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
- package/dist/Components/Viewers/OwnerFiles.js +589 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- package/dist/Helpers/ApiModules.d.ts +3 -34
- package/dist/Helpers/ApiModules.js +3 -3
- package/dist/Helpers/ApiRoutes.d.ts +68 -26
- package/dist/Helpers/ApiRoutes.js +31 -27
- package/dist/Helpers/ArrayHelpers.d.ts +0 -0
- package/dist/Helpers/ArrayHelpers.js +2 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CodeHelpers.d.ts +97 -0
- package/dist/Helpers/CodeHelpers.js +332 -0
- package/dist/Helpers/CrudBackend.d.ts +114 -6
- package/dist/Helpers/CrudBackend.js +214 -11
- package/dist/Helpers/CrudColumns.d.ts +98 -0
- package/dist/Helpers/CrudColumns.js +160 -0
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +130 -6
- package/dist/Helpers/CrudHelpers.js +304 -12
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudRowForms.d.ts +15 -0
- package/dist/Helpers/CrudRowForms.js +43 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +78 -0
- package/dist/Helpers/FileAdapter.js +102 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +90 -5
- package/dist/Helpers/FormStore.js +318 -22
- package/dist/Helpers/InputConditions.d.ts +50 -0
- package/dist/Helpers/InputConditions.js +168 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +385 -0
- package/dist/Helpers/InputNarrowing.d.ts +18 -0
- package/dist/Helpers/InputNarrowing.js +67 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +162 -71
- package/dist/Helpers/MessageBuilder.js +2 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NexusHelpers.d.ts +0 -0
- package/dist/Helpers/NexusHelpers.js +1 -0
- package/dist/Helpers/NumberHelpers.d.ts +95 -0
- package/dist/Helpers/NumberHelpers.js +126 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +47 -0
- package/dist/Helpers/RequestCache.js +132 -0
- package/dist/Helpers/RowsStore.d.ts +37 -2
- package/dist/Helpers/RowsStore.js +128 -10
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +21 -15
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +24 -3
- package/dist/Helpers/TableHelpers.js +58 -12
- package/dist/Helpers/TableJoins.d.ts +83 -0
- package/dist/Helpers/TableJoins.js +284 -0
- package/dist/Helpers/TableStore.d.ts +26 -1
- package/dist/Helpers/TableStore.js +73 -7
- package/dist/Helpers/TreeStore.d.ts +306 -11
- package/dist/Helpers/TreeStore.js +1048 -90
- package/dist/Helpers/UserPreferences.d.ts +8 -0
- package/dist/Helpers/UserPreferences.js +17 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +393 -29
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +39 -3
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
- package/dist/Interfaces/PreferenceInterfaces.js +2 -5
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +160 -5
- package/dist/Services/ApiProxy.d.ts +36 -1
- package/dist/Services/ApiProxy.js +182 -52
- package/dist/Services/ApiProxyRules.d.ts +203 -0
- package/dist/Services/ApiProxyRules.js +426 -0
- package/dist/Services/ExcelApi.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +80 -0
- package/dist/Services/JoinSources.js +150 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Services/ServerApi.d.ts +13 -2
- package/dist/Services/ServerApi.js +8 -3
- package/dist/Shared.Index.d.ts +20 -0
- package/dist/Shared.Index.js +20 -0
- package/package.json +92 -85
- package/src/Styles/Nexus.Avatar.css +80 -79
- package/src/Styles/Nexus.Base.css +117 -117
- package/src/Styles/Nexus.Button.css +186 -180
- package/src/Styles/Nexus.Calendar.css +273 -273
- package/src/Styles/Nexus.Chat.css +1443 -1443
- package/src/Styles/Nexus.Choice.css +359 -343
- package/src/Styles/Nexus.Crud.css +542 -168
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +733 -725
- package/src/Styles/Nexus.Editor.css +295 -295
- package/src/Styles/Nexus.Excel.css +389 -389
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +282 -279
- package/src/Styles/Nexus.Index.css +41 -37
- package/src/Styles/Nexus.Input.css +238 -4
- package/src/Styles/Nexus.Markdown.css +237 -237
- package/src/Styles/Nexus.Menu.css +246 -237
- package/src/Styles/Nexus.Popup.css +800 -724
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Rows.css +637 -637
- package/src/Styles/Nexus.Slider.css +276 -276
- package/src/Styles/Nexus.Tab.Buttons.css +497 -432
- package/src/Styles/Nexus.Table.css +98 -35
- package/src/Styles/Nexus.Theme.Picker.css +59 -3
- package/src/Styles/Nexus.Themes.css +200 -194
- package/src/Styles/Nexus.Time.css +144 -144
- package/src/Styles/Nexus.Toast.css +263 -263
- package/src/Styles/Nexus.Tokens.css +94 -89
- package/src/Styles/Nexus.Tree.css +530 -431
package/CHANGELOG.md
CHANGED
|
@@ -1,446 +1,2268 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
Notable changes to `nexus-shared`. The **values** of exported constants count as API here: several of them are the
|
|
4
|
-
Nexus backend's own numbers, and a value that differs from the backend by one does not fail loudly — it silently
|
|
5
|
-
means the wrong thing on every record and every permission check.
|
|
6
|
-
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
(
|
|
135
|
-
|
|
136
|
-
`
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
`
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
A
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
`
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
`
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
`
|
|
286
|
-
|
|
287
|
-
###
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
`
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
- **`
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
the
|
|
361
|
-
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
- **
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
-
|
|
429
|
-
|
|
430
|
-
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to `nexus-shared`. The **values** of exported constants count as API here: several of them are the
|
|
4
|
+
Nexus backend's own numbers, and a value that differs from the backend by one does not fail loudly — it silently
|
|
5
|
+
means the wrong thing on every record and every permission check.
|
|
6
|
+
|
|
7
|
+
## 4.0.0
|
|
8
|
+
|
|
9
|
+
**Breaking: an input's `type` is a number, a member of `EInputTypes`, and a string is refused.** Everything else in
|
|
10
|
+
this version was prepared as `3.3.0`, which was never published; it follows under
|
|
11
|
+
[the changes prepared as 3.3.0](#also-in-400-the-changes-prepared-as-330).
|
|
12
|
+
|
|
13
|
+
Every export of the published `3.2.0` is still exported (287 are new: 226 from `nexus-shared`, 61 from
|
|
14
|
+
`nexus-shared/client`, none from `nexus-shared/server`; 55 of them are [pictures and documents](#pictures-and-documents-declared-by-params), 19 are [cards C1's and C1-fix's](#the-editor-loads-in-every-bundler-a-reason-initials-one-action-locked-options-narrowed), 24 are [card RF-comp's](#a-printable-document-money-in-a-currency-inputs-shown-by-another-focus-kept-through-a-picture-change), 20 are [card TABS-comp's](#a-records-workspace-a-profile-and-tabs-in-the-details-popup), 3 are [card R2-comp's](#a-servers-page-in-a-sandboxed-frame-code-text-in-a-text-area), 1 is [card C-table-whole's](#a-short-list-read-whole-paging-in-the-footer-only), 1 is [card C-choice-tabs'](#a-choice-drawn-as-tabs), 1 is [card C-input-memory's](#an-input-that-remembers-its-last-value-or-opens-on-the-companys-default), 1 is [card C-label-follows'](#a-label-that-follows-another-input), 6 are [card C-server-preview's](#a-preview-the-server-works-out-as-the-person-types), 1 is [card C-print-logo's](#a-printable-documents-logo), 12 are [card C-crud-scope's](#a-list-that-can-be-widened-to-other-endpoints), 5 are [card C-row-form's](#a-row-action-that-opens-a-form-of-its-own-for-the-record), 3 are [cards C-leave-guard's and C-leave-fix's](#a-form-with-changes-asks-before-a-navigation-inside-the-app), 4 are [cards IMG-comp's and IMG-comp-fix's](#every-picture-through-the-editor-and-within-drives-limits), 2 are [card C-pin-narrow's](#pinned-columns-give-way-on-a-narrow-table), 56 are [card C-record-cards'](#a-records-page-of-cards-every-change-in-a-popup), 2 are [card C-tab-footer's](#a-record-tabs-footer-that-stays-in-view), 13 are [card C-form-figures'](#form-figures-an-input-worked-out-from-others-the-servers-money-rule-and-a-money-column), 7 are [card C-form-confirm's](#a-form-asks-before-it-saves-the-full-details-in-sections), 2 are [card C-profile-fields'](#a-few-fields-in-a-records-profile-left-out-of-its-details-tab), 2 are [card C-span-when's](#an-inputs-width-that-follows-a-value), 6 are [card C-record-cards-fit's](#record-cards-that-fit-what-was-built-on-them); the rest are named in the sections below or
|
|
15
|
+
[in the list at the end of these notes](#new-exports-no-section-names)), and no exported constant changed a value (86
|
|
16
|
+
compared), both checked against the published `3.2.0` with `Frontend/scripts/release-compare` (again on 2026-11-06,
|
|
17
|
+
which corrected the hand count of 195; card C-record-cards' 56 are counted from its three files). What breaks is a type: `InputParams` and
|
|
18
|
+
every params type in it take `type` as an `EInputTypes` member, so every list of inputs an app writes changes.
|
|
19
|
+
`nexus-icons` stays `^1.2.0`; its published copy exports every name this package imports (125). The packages built on
|
|
20
|
+
this one move with it: `nexus-admin` `4.0.0` asks `^4.0.0` (a major, because its range crosses this one), and
|
|
21
|
+
`nexus-public` `3.0.1` asks `^3.0.0 || ^4.0.0` (it imports only `cx`).
|
|
22
|
+
|
|
23
|
+
### Breaking: an input's type is the numbered `EInputTypes`
|
|
24
|
+
|
|
25
|
+
The owner, 2026-10-30, pointing at `type: "switch"` and `type: "dropdown"` in an app's inputs: the old project held an
|
|
26
|
+
input's type as a numbered enum, and so does this package now. A number is what a database, a generated page and a C#
|
|
27
|
+
enum can hold, and a member name that does not exist fails to compile where a mistyped string compiled.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// 3.x
|
|
31
|
+
import type { InputParams } from "nexus-shared";
|
|
32
|
+
const INPUTS: InputParams[] = [
|
|
33
|
+
{ type: "text", name: "fullName", label: "Full name", required: true },
|
|
34
|
+
{ type: "dropdown", name: "tags", label: "Tags", multiple: true, options: TAGS },
|
|
35
|
+
{ type: "switch", name: "isActive", label: "Active" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// 4.0.0
|
|
39
|
+
import { EInputTypes, type InputParams } from "nexus-shared";
|
|
40
|
+
const INPUTS: InputParams[] = [
|
|
41
|
+
{ type: EInputTypes.Text, name: "fullName", label: "Full name", required: true },
|
|
42
|
+
{ type: EInputTypes.Dropdown, name: "tags", label: "Tags", multiple: true, options: TAGS },
|
|
43
|
+
{ type: EInputTypes.Switch, name: "isActive", label: "Active" },
|
|
44
|
+
];
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`EInputTypes` is written as `ENumberScales` is: an `as const` object and a type of the same name, so it is a value and
|
|
48
|
+
a type, works in a server component, and `InputParams` still narrows on it (a dropdown's `multiple: true` still makes
|
|
49
|
+
its value a list). Its numbers are this package's own, in blocks by kind with room to grow; **0 is never a kind**, so a
|
|
50
|
+
value left out is never a valid one, and a released number is never reused or given another meaning.
|
|
51
|
+
|
|
52
|
+
| Block | Kind | Member | Number | Was |
|
|
53
|
+
|---|---|---|---|---|
|
|
54
|
+
| 1–19 | Text boxes | `Text` | 1 | `"text"` |
|
|
55
|
+
| | | `Email` | 2 | `"email"` |
|
|
56
|
+
| | | `Password` | 3 | `"password"` |
|
|
57
|
+
| | | `Search` | 4 | `"search"` |
|
|
58
|
+
| | | `Tel` | 5 | `"tel"` |
|
|
59
|
+
| | | `Url` | 6 | `"url"` |
|
|
60
|
+
| | | `TextArea` | 7 | `"textarea"` |
|
|
61
|
+
| | | `Code` | 8 | `"code"` |
|
|
62
|
+
| 20–39 | Choices | `CheckBox` | 20 | `"checkbox"` |
|
|
63
|
+
| | | `Switch` | 21 | `"switch"` |
|
|
64
|
+
| | | `Radio` | 22 | `"radio"` |
|
|
65
|
+
| | | `CheckBoxGroup` | 23 | `"checkbox-group"` |
|
|
66
|
+
| | | `RadioGroup` | 24 | `"radio-group"` |
|
|
67
|
+
| | | `Dropdown` | 25 | `"dropdown"`, one item or many by `multiple` |
|
|
68
|
+
| 40–59 | Dates and times | `Date` | 40 | `"date"` |
|
|
69
|
+
| | | `DateRange` | 41 | `"date-range"` |
|
|
70
|
+
| | | `Time` | 42 | `"time"` |
|
|
71
|
+
| | | `DateTime` | 43 | `"date-time"` |
|
|
72
|
+
| 60–79 | Numbers and sliders | `Number` | 60 | `"number"` |
|
|
73
|
+
| | | `Slider` | 61 | `"slider"` |
|
|
74
|
+
| | | `RangeSlider` | 62 | `"range-slider"` |
|
|
75
|
+
| 80–99 | Editors | `Markdown` | 80 | `"markdown"` |
|
|
76
|
+
| | | `RichText` | 81 | `"rich-text"` |
|
|
77
|
+
| 100–119 | Structures | `Tree` | 100 | `"tree"` |
|
|
78
|
+
| | | `Tabular` | 101 | `"tabular"` |
|
|
79
|
+
| | | `Group` | 102 | `"group"` |
|
|
80
|
+
| | | `Picture` | 103 | new: [a record's picture](#pictures-and-documents-declared-by-params) |
|
|
81
|
+
|
|
82
|
+
Every number fits a byte. To move an app: import `EInputTypes` from `nexus-shared` and write the member in place of
|
|
83
|
+
each string in the table; the type checker lists every place it is still a string. A page that compared an input's type
|
|
84
|
+
(`params.type === "radio"`) compares the member. Unchanged: a `TextBox`'s own `type` prop is still its HTML type
|
|
85
|
+
(`<TextBox type="email" />`), which is not an input's params; a dropdown column's `type` (`"code"`, `"muted"`), a table
|
|
86
|
+
column's `format` and the chat's conversation `type` are not inputs either and keep their strings.
|
|
87
|
+
|
|
88
|
+
New beside it, and counted in the 30 above: `TextInputType` (the six text box members, whose `TextBox` gets its HTML
|
|
89
|
+
type from one map in the renderer) and `inputTypeName(type)`, the member's name for a message or a log
|
|
90
|
+
(`inputTypeName(EInputTypes.RichText)` is `"RichText"`; a number that is no member comes back as its digits). A table's
|
|
91
|
+
filter that cannot filter now warns with the name: *a RichText input cannot filter rows*.
|
|
92
|
+
|
|
93
|
+
### Pictures and documents, declared by params
|
|
94
|
+
|
|
95
|
+
Card D2 of the subscription demo round (2026-11-02). A record's pictures and documents - a company's logo, a branch's
|
|
96
|
+
pictures, a person's own picture, a subscriber's documents - are the files of an **owner** (a row of a module) in a
|
|
97
|
+
**category** (what they are for, with its limits), and a page adds them by declaration:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { EInputTypes, NexusModule } from "nexus-shared";
|
|
101
|
+
import { DocumentList, openPictureGallery, pictureColumn } from "nexus-shared/client";
|
|
102
|
+
|
|
103
|
+
const BRANCH_PICTURES = { module: NexusModule.Enterprise, category: "branch-picture", aspect: 4 / 3 };
|
|
104
|
+
|
|
105
|
+
// A form: the owner's main picture, changed at once (Upload, Edit, Remove, All pictures).
|
|
106
|
+
inputs: (action, row) => [{ type: EInputTypes.Picture, name: "picture", label: "Picture", ...BRANCH_PICTURES, ownerId: row?.id }];
|
|
107
|
+
// A table: each row's picture, the page's rows read in one call; a click opens the row's gallery.
|
|
108
|
+
columns: [pictureColumn({ ...BRANCH_PICTURES, rowTitle: row => row.branchName }), ...];
|
|
109
|
+
// A window, and a list for a category that takes PDF.
|
|
110
|
+
openPictureGallery({ ...BRANCH_PICTURES, ownerId: branch.id, title: branch.branchName });
|
|
111
|
+
<DocumentList module={NexusModule.Subscription} category="subscriber-document" ownerId={subscriber.id} />;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- **`EInputTypes.Picture` (103)**, the next number of the structures block: `PictureInput`, the owner's main picture
|
|
115
|
+
with Upload (choose, edit with the shape locked, store, make it the main one), Edit (a new picture from the main one),
|
|
116
|
+
Remove (no main picture; the pictures stay) and All pictures. In a category that keeps one file, Upload is Replace
|
|
117
|
+
(the new picture takes the kept one's place once the editor has saved) and Remove is Delete, so the input is never
|
|
118
|
+
left with a file it cannot delete. It holds no value of the form: the form store keeps no field for it
|
|
119
|
+
(**`holdsValue`**), so a save sends nothing under its name, and it takes no `required`, since the form could not
|
|
120
|
+
refuse a save without a picture. A record not saved yet (`ownerId` null) says "Save the {subject} first".
|
|
121
|
+
- **`PictureGallery` / `openPictureGallery`**: upload several at once up to the category's count (each clamped to
|
|
122
|
+
2048 px and given its thumbnail by the editor's `renderImage`, without opening it; a picture's size is checked once
|
|
123
|
+
it is made, so a phone photo over the category's size is stored), make one the main picture, view
|
|
124
|
+
them in the editor module's viewer, edit one (saved as a new picture), delete. **`DocumentList`**: the same for a
|
|
125
|
+
category that takes PDF, as a list (kind, size, date, open, rename, delete); the gallery picks it by itself, and
|
|
126
|
+
`openPictureGallery` without `view` names its window after it ("Documents") once the category is read.
|
|
127
|
+
- **`pictureColumn`**: each row's picture; every row a table draws asks `primaryPictures` (`PrimaryPictures`), which
|
|
128
|
+
gathers the asks of one moment into **one call per module and category**, at most the adapter's `maxOwners` (200),
|
|
129
|
+
and keeps the answers until a change on the page writes one in, so a row redraws without a reload. A refused or
|
|
130
|
+
failed read is kept as no picture for `retryAfter` (30 s), so rows that draw again do not each ask again; `clear()`
|
|
131
|
+
also drops the reads under way, so an ask after a sign-out never gets the answer meant for the person before.
|
|
132
|
+
- **`loadImageEditor()`** (about 1 KB): the image editor (`nexus-image-editor`) is **never in an app's bundle**. The
|
|
133
|
+
adapter names its versioned address (with Nexus, Drive's `Assets/File/nexus-image-editor.txt`), and the loader
|
|
134
|
+
imports it with the bundler told to leave the import alone (`webpackIgnore` / `turbopackIgnore`; proved in a
|
|
135
|
+
Next.js 16 Turbopack build: the chunk holds a plain `import(url)`), once per page; a failed load is not kept.
|
|
136
|
+
`preloadImageEditor()` starts it on the hover of an Edit or Upload button. Its types are in `ImageEditorModule`.
|
|
137
|
+
- **`FileAdapter`** is the abstract class an app implements for its own file backend (`list`, `primaries`, `upload`,
|
|
138
|
+
`setPrimary`, `rename`, `remove`, `fileUrl`, `editorModuleUrl`), registered once with `defineFileAdapter`;
|
|
139
|
+
**`NexusDriveAdapter`** is the default: a write ticket from the owner's module (`Files/Ticket/{category}/{ownerId}`,
|
|
140
|
+
kept until a minute before it runs out, asked again once when Drive answers `DRV_X011`), a read ticket
|
|
141
|
+
(`Files/Owners/{category}`) for a page's rows and for a person who may see but not change an owner's files, then
|
|
142
|
+
Drive's `File/List`, `Primaries`, `Upload`, `SetPrimary`, `Rename`, `Delete` and `View/{id}/{Original|Thumbnail}`.
|
|
143
|
+
Nothing generic names a backend's numbers: a category's kinds are MIME types.
|
|
144
|
+
- New shared helpers: `fileRefusal` (a kind or size the category refuses, said before the upload), `fileKindsSentence`,
|
|
145
|
+
`fileSizeText`, `roomLeft`, `takesDocuments`, `pictureName`; one stylesheet, `Nexus.Files.css` (`nx-picture`,
|
|
146
|
+
`nx-picture-input`, `nx-files`), whose hovered pictures get a ring and never a new size.
|
|
147
|
+
|
|
148
|
+
Proved by `npm run test:files` (the adapter against a pretend backend: the tickets, the fresh one after `DRV_X011`, the
|
|
149
|
+
read-only fallback, the page's order, the multipart fields, the pointer file) and nexus.template's `/demo/pictures`
|
|
150
|
+
with its browser check `pictures.cjs`, on a pretend Drive that answers as the C# one.
|
|
151
|
+
|
|
152
|
+
### The editor loads in every bundler; a reason, initials, one action locked, options narrowed
|
|
153
|
+
|
|
154
|
+
Card C1 of the subscription demo's second round (2026-11-02): fixes to the pictures above, and two features the
|
|
155
|
+
subscription pages needed and kept as code of their own.
|
|
156
|
+
|
|
157
|
+
- **The image editor loads in an app's production build.** The package build rewrites "./x.ts" imports to "./x.js"
|
|
158
|
+
(`rewriteRelativeImportExtensions`), and for an `import()` of a variable it wrapped the address in a run-time helper,
|
|
159
|
+
`import(__rewriteRelativeImportExtension(url))`. The bundler-ignore comments were then inside a call, and Next 16's
|
|
160
|
+
Turbopack compiled the import to "Cannot find module as expression is too dynamic": in every app built on `dist`,
|
|
161
|
+
Upload and Edit on a picture never opened the editor (an app reading the source, like nexus.template, never saw it).
|
|
162
|
+
The import now lives alone in **`importModule(url)`** (`Services/ModuleImport.ts`, which imports nothing), built a
|
|
163
|
+
second time without the rewrite (`tsconfig.build.module-import.json`, the second step of `npm run build` and of
|
|
164
|
+
`copy-packages`' watch), so `dist` keeps `import(/* webpackIgnore: true */ /* turbopackIgnore: true */ /* @vite-ignore */ url)`.
|
|
165
|
+
The setting stays for every other file. `scripts/test-dist-loader.mts` (in `npm test`) runs the build's steps into a
|
|
166
|
+
scratch folder and fails if any built file holds the helper.
|
|
167
|
+
- **An editor that cannot load says so.** `loadImageEditor()` rejects in plain words ("The image editor could not be
|
|
168
|
+
downloaded. Check the connection and try again; ...") and logs the cause (a network error, a 404, a bundler that
|
|
169
|
+
rewrote the import) with `console.error`; the picture input, the gallery and the viewer show it as a toast.
|
|
170
|
+
- **A picture input says why it is read only.** `OwnerFilesParams.lockReason` (on `PictureInput`, `PictureGallery`,
|
|
171
|
+
`openPictureGallery`, and `pictureColumn` as a sentence or `row => sentence`) makes the files read only with that
|
|
172
|
+
sentence under them: a record that is closed, a year that is locked. Without it, a person the backend lets only look
|
|
173
|
+
is told the backend's own words: `NexusDriveAdapter` keeps the refused write ticket's message as
|
|
174
|
+
`OwnerFiles.readOnlyReason`, and **`READ_ONLY_REASON`** stands in when there is none.
|
|
175
|
+
- **Initials for an owner with no picture.** `OwnerPicture` `name` and `pictureColumn` `nameKey` (the row's field with
|
|
176
|
+
a name): without a picture, the name's initials, on the tone the shared `Avatar` gives that name
|
|
177
|
+
(`.nx-picture--initials`; the avatar tones now also apply to `.nx-picture[data-tone]`).
|
|
178
|
+
- **One action locked on a row: `Crud` `rowActionLock`**: `row => ({ trash: "The head office stays open..." })` switches
|
|
179
|
+
off single actions of a record, each with its sentence in the row's menu, in a selection that holds it, and for
|
|
180
|
+
`edit` in its details too; the rest still run. `rowLock` still locks every change at once. New types
|
|
181
|
+
`CrudLockableAction`, `CrudActionLocks`.
|
|
182
|
+
- **Options narrowed by another input: `narrowedBy`** on any input (Form only), `{ name, key?, placeholder? }`: a
|
|
183
|
+
Package list only of the Group picked. The form hands the input its params narrowed by the other value as it draws
|
|
184
|
+
(`narrowInputParams`, `narrowingValue`, `narrowingPlaceholder`, pure): static `options` filtered, `loadOptions` asked
|
|
185
|
+
with the new **`DropdownPageRequest.narrow`**, a load key per value, nothing offered and "Pick a group first" while the
|
|
186
|
+
other is empty; the form store follows it as `dependsOn` would and clears the narrowed value when a person changes the
|
|
187
|
+
other (never when code sets it). `crudPicker` answers `narrow` from its kept list, or, with the new **`narrowQuery`**,
|
|
188
|
+
asks the server (`activeListing?{narrowQuery}={value}`, one kept list per value, dropped with the others on a save).
|
|
189
|
+
`FormStore.inputParams(name)` is new.
|
|
190
|
+
- **A list narrowed by a box of its own, remembered per viewer: `Crud` `narrowPicker`** (`CrudNarrowPicker`): the
|
|
191
|
+
picker's params, the box's label, `remember: { key, viewer }` and `onPick`. The CRUD draws the box in its toolbar, keeps
|
|
192
|
+
the pick as `narrow` would, and opens the same pick on the next visit in this browser for the same viewer while the
|
|
193
|
+
box still offers it; only the record's key is stored. `useCrudNarrowPicker` is exported (`nexus-shared/client`).
|
|
194
|
+
|
|
195
|
+
Proved by `npm test` (`test-dist-loader`, `test-input-narrowing`, `test-files`' reason), nexus.template's `/demo/pictures`
|
|
196
|
+
(`pictures.cjs`: initials, the lock reason, an editor whose module is refused) and `/demo/crud`'s "One group at a time"
|
|
197
|
+
(`crud-narrowing.cjs`), and nexus.web's production build against a Drive API.
|
|
198
|
+
|
|
199
|
+
Card C1-fix (the review of C1, 2026-11-02):
|
|
200
|
+
|
|
201
|
+
- **`narrowedBy` clears down a chain.** A person changing the Country clears the Region, and now the District picked
|
|
202
|
+
among that Region's too, whatever order the form lists them in (before, the District kept a value of the old Region,
|
|
203
|
+
and could be saved).
|
|
204
|
+
- **A narrowed input's getters answer on every draw.** `Form` copied a `narrowedBy` input's params once and kept the
|
|
205
|
+
copy, so a `readOnly`, `hint`, `label` or `options` getter that `dependsOn` follows stayed as it first read. The
|
|
206
|
+
copy is now made afresh each time the input draws.
|
|
207
|
+
- **A `narrowPicker`'s words name its box**: "Pick a group in the Group box to see its amenities.", "... to add a new
|
|
208
|
+
amenity for it." on Add, and "Added for Bathroom: pick another group in the Group box to add elsewhere." on the Add
|
|
209
|
+
form's fixed field (before, all three said "the other list"). New on `CrudNarrow` (and so on `narrowPicker`):
|
|
210
|
+
**`addMessage`** and **`fixedMessage: name => sentence`**, beside `emptyMessage`; the words are
|
|
211
|
+
**`narrowPickWords(label, subject, plural)`**.
|
|
212
|
+
- **The remembered pick keeps the key alone, and only for a viewer.** No name is written to local storage (a pick kept
|
|
213
|
+
with one by C1's build still opens, and is written again without it), and without `remember.viewer` nothing is
|
|
214
|
+
remembered: **`narrowPickerStorageKey`** (now from `nexus-shared`) answers `null`. A kept pick is found again by its
|
|
215
|
+
key alone, **`findNarrowPick`**: `resolveOptions` when the picker has it, else the box's pages at 50 a page (never one
|
|
216
|
+
page of everything, which a server-paged source refuses). `readNarrowPick`, `writeNarrowPick`,
|
|
217
|
+
`narrowPickerPlaceholder` are the rest of `Helpers/CrudNarrowPick.ts`, pure.
|
|
218
|
+
- **An owner not in use is not "not found".** When a module refuses the write ticket with a 404 (the owner pending,
|
|
219
|
+
ended, in the trash or deleted) and a read ticket still finds it, the files say **`OWNER_NOT_IN_USE_REASON`** ("This
|
|
220
|
+
record is not in use now, so its files can be seen but not changed.") rather than "The record was not found.".
|
|
221
|
+
- **`test-dist-loader` checks this folder's whole `dist`** with the scratch build's rules (ModuleImport.js there, its
|
|
222
|
+
import plain, no helper in any file), so a dist built with only the first step fails.
|
|
223
|
+
|
|
224
|
+
Proved by `npm test` (23 suites; `test-input-narrowing` 49, `test-files` 73, `test-dist-loader` 17) and nexus.template's
|
|
225
|
+
`crud-narrowing.cjs` (55, both motions: the three sentences, a narrowed Icon whose hint follows "Free for guests"),
|
|
226
|
+
`staff-posts.cjs` (86, the other-list words unchanged), `crud.cjs`, `forms.cjs` (154) and `pictures.cjs` (83).
|
|
227
|
+
|
|
228
|
+
### A record's workspace: a profile and tabs in the details popup
|
|
229
|
+
|
|
230
|
+
Card TABS-comp (2026-11-03), from the owner's request for the tab buttons of the 2026-06-20 project's record popups:
|
|
231
|
+
View, and a click on a row under `table.openOnClick`, open the record as a wide popup with who it is and its tabs on the
|
|
232
|
+
left and the chosen tab on the right. Additions only.
|
|
233
|
+
|
|
234
|
+
- **`CrudDetailsOptions.tabs`**: `{ id, label, icon?, endpoint?, hidden?(row), badge?, render(context) }[]`. A tab that
|
|
235
|
+
names an `endpoint` shows only to a person who may call it (the rule of `CrudDetailsAction`, again whenever the rights
|
|
236
|
+
change). `render` is handed a **`CrudDetailsTabContext`**: `row` (as the list has it now), `form` (the record in the
|
|
237
|
+
`FormStore` the `aside` is handed: the row, then the `details` endpoint's record, then each refresh's), `popup`, `tab`,
|
|
238
|
+
`select(tabId)` and **`refresh()`**, which reads the record again from the `details` endpoint and writes it into the
|
|
239
|
+
table, the pinned band, every list kept and every tree (`trees`), as an edit's answer is - no list is read again - and
|
|
240
|
+
answers the record (or `null`). The built-in tab `details` (the fields, with `aside` beside them) comes first; list
|
|
241
|
+
`{ id: "details" }` to move or rename it, give it `hidden: () => true` to leave it out, or a `render` to replace it.
|
|
242
|
+
Only the chosen tab is drawn. With tabs the popup's default size is `full`.
|
|
243
|
+
- **`CrudDetailsOptions.profile`**: `row => ({ picture?, title, subtitle?, tags? })` or anything to draw. `picture` is
|
|
244
|
+
the owner's main picture by the params the picture column takes (`{ ...GUEST_PICTURE, ownerId }`, a
|
|
245
|
+
**`CrudDetailsPicture`**: a click opens the pictures, initials stand in without one) or a node; `tags` are
|
|
246
|
+
`{ label, tone? }` in the look of a table's tags. It follows the record as it is loaded and refreshed.
|
|
247
|
+
- **`CrudDetailsOptions.remember`**: `{ key, viewer }` keeps the last tab chosen in this browser
|
|
248
|
+
(`nx-crud-tab:{key}:{viewer}`) and opens it next time while it is still offered; nothing is kept without a viewer.
|
|
249
|
+
- The layout: a 20rem column (the picture 10rem, the name, the line, the tags, then the tabs as `design="list"` rows)
|
|
250
|
+
and the tab beside it, which scrolls inside itself; the popup's body never scrolls and a switch never changes its
|
|
251
|
+
size. The workspace is a size container: below 720px of popup width (a phone) the profile is one row (a 3rem
|
|
252
|
+
picture) and the tabs one strip that scrolls sideways, above the tab. The focus opens on the chosen tab, a switch
|
|
253
|
+
that takes away the panel holding the focus puts it on the chosen tab, and it never drops to the page.
|
|
254
|
+
- **`Tabs`** and **`TabPanel`** (`nexus-shared/client`): tabs that switch panels, as a WAI-ARIA `tablist` - the one
|
|
255
|
+
piece `TabButtons`' own doc pointed to and the package did not have. The look is `TabButtons`', from the same
|
|
256
|
+
stylesheet (`design` `list`, `nav`, or `auto`: the list, and a strip in a container under 720px); the chosen tab is
|
|
257
|
+
`aria-selected` and the only one in the Tab order, the arrows (either way), Home and End move and open, the panel is
|
|
258
|
+
joined by `tabIds(id, value)` and takes the focus itself when it holds nothing that does. `TabOption`, `TabsDesign`,
|
|
259
|
+
`TabsProps`, `TabPanelProps`.
|
|
260
|
+
- The tabs' rules without a browser, in `nexus-shared`: `offeredCrudTabs`, `openingCrudTab`, `crudTabStorageKey`,
|
|
261
|
+
`readCrudTab`, `writeCrudTab`, `isCrudProfile`, `isCrudPicture`, `CRUD_DETAILS_TAB`, `OfferedCrudTab`.
|
|
262
|
+
- `OwnerPicture` `size` also takes a CSS length (`"var(--nx-crud-profile-picture)"`), so a picture can follow a
|
|
263
|
+
container query.
|
|
264
|
+
- Styles: `Nexus.Crud.css` (`nx-crud-workspace`, `__frame`, `__frame--alone`, `__side`, `__profile`, `__nav`,
|
|
265
|
+
`__panel`; `nx-crud-profile`, `__picture`, `__text`, `__title`, `__subtitle`, `__tags`) and `Nexus.Tab.Buttons.css`
|
|
266
|
+
(the chosen option is `:checked` or `[aria-selected="true"]`, a button's own look taken away, `nx-tabs--auto`,
|
|
267
|
+
`nx-tab-panel`).
|
|
268
|
+
- Proved by `scripts/test-crud-details-tabs.mts` (33) and nexus.template's `/demo/crud` "A guest's workspace" with
|
|
269
|
+
`scripts/browser-checks/crud-workspace.cjs` (60 in both motion modes: the click on a row, the keyboard, one Update
|
|
270
|
+
and one Detail for a check-in with the row's visits written in, the remembered tab, a role without the endpoint, every
|
|
271
|
+
theme light and dark at 1280x720 and 390x844).
|
|
272
|
+
- Card TABS-fix1-comp (2026-11-03), from the review of the first page built on it:
|
|
273
|
+
- **A form in a tab is never thrown away without asking.** A `SubmitForm` (and so an `ApiForm`) joins the place it
|
|
274
|
+
is drawn in by itself (`FormLeaveGuard`, by the rule of its "leave the page?" question: not while locked, not when
|
|
275
|
+
a draft keeps it); while one has changes, choosing another tab, Close, Edit, the popup's X, Esc and its bar on the
|
|
276
|
+
dock ask "Discard your changes?". Keep editing stays where it was. No new export.
|
|
277
|
+
- **`refresh()` lands after the popup closed.** Only the first read of the record stops when the popup closes; a
|
|
278
|
+
save in a tab followed at once by Close still writes the row's new status and dates into the lists.
|
|
279
|
+
- **A record off the page shown is patched in place.** `refresh()` hands the record as the popup held it, so one
|
|
280
|
+
opened from the pinned band, or from a page turned since, is written into every page kept that has it: no page is
|
|
281
|
+
dropped and the page shown is not read again.
|
|
282
|
+
- **A popup closed over another never leaves the focus on the page.** When its opener cannot take the focus back (an
|
|
283
|
+
item of a menu that closed, a button disabled while its action runs), the popup now on top takes it where it puts
|
|
284
|
+
it on opening: in a workspace, the chosen tab.
|
|
285
|
+
- The examples here, in the README and in `CrudDetailsOptions.tabs`' doc are a guest's, as the template's demo is.
|
|
286
|
+
|
|
287
|
+
### A printable document, money in a currency, inputs shown by another, focus kept through a picture change
|
|
288
|
+
|
|
289
|
+
Card RF-comp (2026-11-03), from the whole-round review of the subscription demo's second round: three pieces the
|
|
290
|
+
subscription pages had written as code of their own (findings rules-3, rules-4, rules-5), and the picture input and
|
|
291
|
+
gallery keeping a keyboard's focus (packages-3).
|
|
292
|
+
|
|
293
|
+
- **`PrintDocument`** (`nexus-shared`, no state, so server components too): a receipt, a bill or a statement by params:
|
|
294
|
+
`paper` (`"a5"`, the default, or `"thermal"`, an 80 mm roll), `title`, `subtitles`, `kind`, `stamp` ("Voided: ..."),
|
|
295
|
+
`blocks` in order - `{ facts: [{ label, value, hidden? }] }` and `{ columns: [{ label, align? }], lines: [{ cells,
|
|
296
|
+
note?, hidden? }], totals: [{ label, value }] }`, falsy entries left out - and `footer` (line breaks kept). On screen it
|
|
297
|
+
reads in the theme's colours at its paper's width (128 mm, or 72 mm); printed with `window.print()` it is the only thing
|
|
298
|
+
on the paper, black on white, at the paper's left edge, and the component writes the page's `@page` rule (A5 portrait
|
|
299
|
+
with 10 mm margins, or 80 x 297 mm with 4 mm) while it is on the page. The print rule applies only while a document is
|
|
300
|
+
on the page (`body:has(.nx-print)`), so any other page prints as it is. **`PRINT_PAPERS`** are the papers as
|
|
301
|
+
`TabButtons` options. Styles: `Styles/Nexus.Print.css` (`nx-print`, `--thermal`, `__head`, `__title`, `__kind`,
|
|
302
|
+
`__stamp`, `__facts`, `__lines`, `__end`, `__note`, `__foot`). Types `PrintPaper`, `PrintFact`, `PrintColumn`,
|
|
303
|
+
`PrintLine`, `PrintFactsBlock`, `PrintLinesBlock`, `PrintBlock`, `PrintDocumentProps`.
|
|
304
|
+
- **Money in the company's currency, set once.** `DataTableProps.currency` (and `Crud` `table.currency`) writes every
|
|
305
|
+
`money` column as "Rs. 1,250.00" in the cells, the details, the CSV and the print; **`TableColumn.currency`** gives a
|
|
306
|
+
column its own, a text or `row => row.currencyText`. `formatCellText(column, value, options, row?)` takes the row for
|
|
307
|
+
it and `CellTextOptions.currency` the table's; **`columnCurrency`** answers which. **`formatMoney(value, { currency,
|
|
308
|
+
decimals = 2, locale, empty })`** writes the same anywhere else (a dashboard's figures, a receipt): "Rs. 1,250.00".
|
|
309
|
+
`prefix` still works as before.
|
|
310
|
+
- **An input shown by another input's value: `showWhen`** on any input (Form only): `{ name, in?, notIn?, filled? }`
|
|
311
|
+
or a list of them, every one holding (`InputCondition`). Values compare as text (1 matches "1"), a list value matches
|
|
312
|
+
`in` by any item. The form gives such an input a live `hidden` that reads its store and follows the inputs named as
|
|
313
|
+
`dependsOn` would; a hidden input keeps its value and is not checked; its own `hidden` still hides it. Pure helpers
|
|
314
|
+
`withInputConditions`, `inputShown`, `inputReadOnly`, `conditionsHold`, `conditionHolds`, `conditionsOf`, `isEmptyValue`.
|
|
315
|
+
- **An input read-only by another input's value: `readOnlyWhen`** (Form only), the same `InputCondition`s: a live
|
|
316
|
+
`readOnly` that follows the inputs named, the input's own `readOnlyMessage` as the reason, its value kept in the data
|
|
317
|
+
and not checked while read-only; its own `readOnly` still locks it. A CRUD's Edit form holds the record's key and its
|
|
318
|
+
Add form does not, so `readOnlyWhen: { name: "id", filled: true }` is a field picked on Add and fixed on Edit (a
|
|
319
|
+
price's group).
|
|
320
|
+
- **Options narrowed by a fixed value: `narrowedBy: { key, value, orUnset?, placeholder? }`** beside the
|
|
321
|
+
`{ name }` form (`InputNarrowing` is now `InputNarrowingByInput | InputNarrowingByValue`): the subscriber's own group,
|
|
322
|
+
known when the form opens; nothing followed, nothing cleared, a `null` value offers nothing. **`orUnset`** (both forms)
|
|
323
|
+
also offers the options whose field is empty, a fee of every group: `DropdownPageRequest.narrow` carries it (and a
|
|
324
|
+
`null` value with it), and `crudPicker` honours it from its kept list. `isFixedNarrowing` tells the forms apart.
|
|
325
|
+
- **The picture input and the gallery keep a keyboard's focus.** Their buttons no longer switch themselves off while a
|
|
326
|
+
change runs (`disabled` sent the focus to the page in every browser): they stay focusable, `aria-disabled`, their
|
|
327
|
+
press ignored (**`busyProps(busy, run)`**). When a change ends with the focused button gone - Remove and Edit with
|
|
328
|
+
the main picture, a star once its picture is the main one, a deleted file's buttons - the focus goes to the first
|
|
329
|
+
control left in its tile, else in the input (**`useFocusThroughChange(root, busy)`**, `nexus-shared/client`).
|
|
330
|
+
|
|
331
|
+
Proved by `npm test` (24 suites; `test-input-conditions` new, 37; `test-input-narrowing` 61, `test-table` 439,
|
|
332
|
+
`test-number-helpers` 125; the CSS contract), and nexus.template's `/demo/print-document` and `/demo/form`'s "Inputs
|
|
333
|
+
shown by another input" (`print-document.cjs`, 52: the money cells, the receipt, its widths and `@page`, printed alone
|
|
334
|
+
and black on white, a page without one printed whole, `showWhen` and the fixed narrowing, every theme light and dark at
|
|
335
|
+
1280x720 and 390x844 at AA) and `pictures.cjs` (87: Remove, the star and Delete from the keyboard keep the focus).
|
|
336
|
+
|
|
337
|
+
### A server's page in a sandboxed frame; code text in a text area
|
|
338
|
+
|
|
339
|
+
Card R2-comp (2026-11-05), for the Report module's Print layouts page: its preview and print door answer a whole HTML
|
|
340
|
+
page (`{ html }`, with its own `@page` sheet, screen style and a CSP that runs no script), and its editor holds a
|
|
341
|
+
layout's HTML. Additions only.
|
|
342
|
+
|
|
343
|
+
- **`DocumentFrame`** (`nexus-shared/client`): the page in an `<iframe srcdoc>`, sandboxed `""` (an opaque origin) or,
|
|
344
|
+
with `printable`, `"allow-same-origin allow-modals"`, so the handle's **`print()`** (`DocumentFrameHandle`) opens the
|
|
345
|
+
print dialog for the page alone; never `allow-scripts`. Props (`DocumentFrameProps`): `html`, `title` (required; the
|
|
346
|
+
frame's name to a screen reader), `loading` (the page area `aria-busy` at once; a read that lasts 0.4s dims the page
|
|
347
|
+
and says `loadingMessage`, default "Loading…"), `error` (a `role="alert"` line over the last page, which stays),
|
|
348
|
+
`emptyMessage` (default "Nothing to show yet."), `actions` (buttons at the end of the frame's bar - the form's bar and
|
|
349
|
+
status line, shown only with actions or a status - which wrap under the line on a phone), `printable`, `className`,
|
|
350
|
+
`style`, `ref`. It fills the height its container gives (a flex column, a grid row) and is never under 22rem; the page
|
|
351
|
+
scrolls inside it, an A4 sheet sideways too. While loading or failed, an `html` of null keeps the last page; a
|
|
352
|
+
printable page read again keeps its scroll while the title stays. A frame that becomes printable is a new frame (a
|
|
353
|
+
sandbox counts from the next page). Styles in `Nexus.Print.css` (`nx-document-frame`, `__bar`, `__view`,
|
|
354
|
+
`__view--busy`, `__page`, `__empty`).
|
|
355
|
+
- **`TextArea` `code`** (and `code: true` in a TextArea's `InputParams`): the monospace token, lines kept as typed
|
|
356
|
+
(`wrap="off"`, a long line scrolled sideways in the box), `tab-size: 2`, and `spellCheck`, `autoCapitalize`,
|
|
357
|
+
`autoCorrect`, `autoComplete` off and `translate="no"`; Tab still leaves the box. `nx-field__control--code` in
|
|
358
|
+
`Nexus.Input.css`; read-only and disabled code keep the look.
|
|
359
|
+
- `.nx-form-status` takes its room beside a bar's buttons from `--nx-form-status-basis` (default 14rem, as before), so
|
|
360
|
+
the frame's bar, whose words are few, keeps two buttons beside its line in a half-width frame (10rem).
|
|
361
|
+
- A CRUD's details footer and the frame share one rule for a slow read (said after 0.4s, held 0.3s): `useSlowBusy`,
|
|
362
|
+
internal, moved out of `Crud.tsx`.
|
|
363
|
+
|
|
364
|
+
Proved by `npm test` (25 suites; the CSS contract) and nexus.template's `/demo/document-frame`
|
|
365
|
+
(`document-frame.cjs`, 68 and 69 with reduced motion: the sandboxes, srcdoc, the empty, slow and failed states, the
|
|
366
|
+
bar's buttons by keyboard, Print firing the frame's `beforeprint` and `false` without a page or when not printable, the
|
|
367
|
+
scroll kept, a script that never runs printable or sealed, the code text area, every theme light and dark at 1280x720
|
|
368
|
+
and 375x812 with no overflow and the status lines at AA), with `crud.cjs` and `crud-workspace.cjs` for the footer.
|
|
369
|
+
|
|
370
|
+
### A field at the top of a CRUD popup's column keeps its label and focus ring whole
|
|
371
|
+
|
|
372
|
+
Card R2-fix-label (2026-11-05), the owner's "overflow hidden bug": in a subscriber's window, the outlined "New note" box
|
|
373
|
+
first in the notes beside the details showed only the lower half of its raised label, and its focus outline was cut at
|
|
374
|
+
the top and at the panel's right edge. No export changed.
|
|
375
|
+
|
|
376
|
+
- **`.nx-crud-aside__main` and `.nx-crud-aside__side`** (side by side, from 1000px) each scroll on their own, and a
|
|
377
|
+
scroll box clips at its padding edge, so a field first in either lost what it draws past its box. Both now keep room
|
|
378
|
+
inside their edges, paid back by a negative margin so nothing moves: above, an outlined field's raised label (0.6em
|
|
379
|
+
of the xs text) and its 2px outline; on every side, the 3px focus ring. The form's column had only 2px (rings), now
|
|
380
|
+
the same room; the panel's column had none.
|
|
381
|
+
- **`scroll-padding`** on those columns, `.nx-crud-workspace__panel` and **`.nx-popup__body`**: focus scrolls a field's
|
|
382
|
+
control to the box's edge, and the raised label above it, or the box's border and ring below it, were cut there by 4
|
|
383
|
+
to 7.2px. The room past the control is now kept the same way in every popup.
|
|
384
|
+
- Proved by nexus.template's `/demo/crud` "A guest's workspace", which now has the guest's notes beside the details (a
|
|
385
|
+
form first) and the guest's card beside the Add and Edit forms, and `scripts/browser-checks/crud-field-room.cjs`.
|
|
386
|
+
It measures the label and the outline's and shadows' reach against every box that clips them, focused with text,
|
|
387
|
+
in the standard, outlined and inbox designs and the sm, md and lg sizes, light and dark, at 1280x720, 1040x720 and
|
|
388
|
+
390x844, and at each scroll edge. 306 pass. On the old CSS 108 failed: every case in a column side by side. With
|
|
389
|
+
`scroll-padding` off, 20 failed: the edge cases.
|
|
390
|
+
|
|
391
|
+
### A short list read whole; paging in the footer only
|
|
392
|
+
|
|
393
|
+
Card C-table-whole (2026-11-05), the owner on a subscriber's notes: *"footer have gap top side have gap ... used only
|
|
394
|
+
footer for this types of datatable"*, *"or do not put limit here"* (a subscriber keeps at most 50 notes). Additions only:
|
|
395
|
+
a table without the new prop draws as before.
|
|
396
|
+
|
|
397
|
+
- **`DataTable` `pager`** (`TablePagerMode`: `"bar"` | `"footer"` | `"none"`, default `"bar"`), where the paging lives:
|
|
398
|
+
- `"bar"`: rows per page inside the search box (or the bar without a search), the pager in the footer - as before.
|
|
399
|
+
- `"footer"`: rows per page beside the pager, in the footer; a table with nothing else on top draws no bar.
|
|
400
|
+
- `"none"`: every row on one page, for a short list read whole: no pager, no rows per page, no remembered or
|
|
401
|
+
preferred page size, and no footer unless a search or the filters leave rows out ("Showing 1–2 of 2 · filtered from
|
|
402
|
+
6"). The count line stays as a visually hidden live line, so a screen reader still hears it. With
|
|
403
|
+
`searchable={false}`, `columnsMenu={false}`, `exportable={false}` and `printable={false}` the table is its rows
|
|
404
|
+
alone in its frame (the bar already hid itself when it held nothing; the empty footer band was what was left). A
|
|
405
|
+
server's pages cannot all be shown, so `paging="server"` takes `"none"` as `"footer"`.
|
|
406
|
+
- **`Crud`** passes it through `table: { pager }` (`CrudTableOptions`).
|
|
407
|
+
- Proved by nexus.template's `/demo/data-table` "A short list read whole" (`data-table-pager.cjs`, 52 in each motion
|
|
408
|
+
mode: the whole table with 3, 14 and no notes, the footer's own rows per page and pager, a whole table's search, the
|
|
409
|
+
default unchanged, every theme light and dark at 1280x720 and 390x844 with no overflow), with `data-table.cjs` and
|
|
410
|
+
`crud.cjs`.
|
|
411
|
+
|
|
412
|
+
### A record's workspace draws its panel under the details
|
|
413
|
+
|
|
414
|
+
Card C-workspace-below (2026-11-05), the owner on a subscriber's window: *"make width full 100% for right side
|
|
415
|
+
content. the details page make full width 100% then bellow show others informations"*. No export changed; one option
|
|
416
|
+
was added to an exported type.
|
|
417
|
+
|
|
418
|
+
- **`CrudDetailsOptions.asidePlace`**: `"beside"` | `"below"`. `"below"` draws `details.aside` under the fields at
|
|
419
|
+
every width: the fields across the whole width of the tab, the panel across the whole width under them, as tall as
|
|
420
|
+
it needs, scrolled with them by the workspace's panel (or the popup's body) - never a column of its own, so no box of
|
|
421
|
+
its own clips a raised label or a focus ring. `"beside"` is the layout as it was: side by side from 1000px, stacked
|
|
422
|
+
under that.
|
|
423
|
+
- **The default changed for a record's workspace** (`tabs` or `profile`): `"below"`, so its Details tab reads as the
|
|
424
|
+
record first, then what belongs to it. Only the Subscribers page passes a workspace aside today; a plain details
|
|
425
|
+
popup keeps `"beside"`.
|
|
426
|
+
- `nx-crud-aside--below` in `Nexus.Crud.css`; the side-by-side rules now name `:not(.nx-crud-aside--below)`.
|
|
427
|
+
- Proved by nexus.template's `/demo/crud` "A guest's workspace" (a switch: notes under the details / beside them) and
|
|
428
|
+
`crud-aside-below.cjs` (82 per motion mode: widths, order, no scroll of its own, the note field's label and ring, at
|
|
429
|
+
1280x720, 1040x720 and 390x844, light and dark); `crud-field-room.cjs` now picks "beside" for the columns it
|
|
430
|
+
measures; `crud-workspace.cjs` and `crud.cjs` unchanged.
|
|
431
|
+
|
|
432
|
+
### A choice drawn as tabs
|
|
433
|
+
|
|
434
|
+
Card C-choice-tabs (2026-11-05), the owner on the counter's "What is it?" (Payment, Activate, Disconnect, each with a
|
|
435
|
+
sentence under it): *"if we make it tab types of design maybe it batter. (nav style) the helper label should be tool tips
|
|
436
|
+
or somethings"*. Additions only.
|
|
437
|
+
|
|
438
|
+
- **`RadioGroup` `design: "tabs"`** (`RadioGroupDesign` = `ChoiceDesign | "tabs"`, in `nexus-shared/client`; a
|
|
439
|
+
RadioGroup's `InputParams` take it too): the options as nav-style tabs on one `TabButtons` track, still native
|
|
440
|
+
radios in a fieldset with `role="radiogroup"` - the arrow keys move the choice, `required` and `validate` stop a
|
|
441
|
+
form's save with the group's message (the track edged in the danger colour), `readOnly` keeps the value with its
|
|
442
|
+
lock and note, `disabled` dims every tab, `size` sm/md/lg is the track's. The tabs wrap onto more rows where they do
|
|
443
|
+
not fit (a phone). `orientation` and `columns` do not apply.
|
|
444
|
+
- **Each option's `description`** is its tooltip (`title`, when it is text) and its input's accessible description
|
|
445
|
+
(`aria-describedby` to its note, which a screen reader reads even while hidden). The chosen option's note shows
|
|
446
|
+
under the track, since a phone has no tooltip; it follows every change and a form's reset (`showChosenNote`, the
|
|
447
|
+
group's `sync` rule), with no React state.
|
|
448
|
+
- **`Nexus.Tab.Buttons.css`**: the rules that raise each chosen option on its own (`multiple`) are shared by
|
|
449
|
+
`nx-tabs--wrap` (`:is(.nx-tabs--multiple, .nx-tabs--wrap)`, the same specificity), since a radio group's choice lives
|
|
450
|
+
in its inputs, not in a value a sliding indicator could follow; only `multiple` keeps the pointer on a chosen option.
|
|
451
|
+
`Nexus.Choice.css`: `nx-choices--tabs` (the danger edge) and `nx-choices__note`.
|
|
452
|
+
- The read-only note finds a tab as it finds a choice (`optionAnchor`: `.nx-choice, .nx-tabs__option`).
|
|
453
|
+
- Proved by nexus.template's `/demo/form` "A choice drawn as tabs" (`choice-tabs.cjs`, both motion modes: the track,
|
|
454
|
+
the tooltips and descriptions, required, a click and the arrow keys, read-only, disabled, the values sent, a reset,
|
|
455
|
+
every theme light and dark at 1280x720 and 390x844 with the labels and the note at AA), with `forms.cjs` and
|
|
456
|
+
`read-only.cjs`.
|
|
457
|
+
|
|
458
|
+
### An input that remembers its last value, or opens on the company's default
|
|
459
|
+
|
|
460
|
+
Card C-input-memory (2026-11-05), the owner on the counter's Paid by and Bill: *"we should be remember the input value.
|
|
461
|
+
we also need company level preference to select default, if default not config then we need remember"*. Additions only.
|
|
462
|
+
|
|
463
|
+
- **`InputLayoutParams.remember`** (`InputRemember`: `key`, `viewer`, `preferred`; Form only, any input type). When
|
|
464
|
+
the values the form opens with leave the input without one (they do not name it, or hold it empty - a new record), it
|
|
465
|
+
opens on `preferred` (the company's default, from a setting the page reads), else on the person's last saved choice,
|
|
466
|
+
else on its own `defaultValue`; `preferred` and the kept choice each only while the input still offers them (a
|
|
467
|
+
choice group's or a dropdown's own `options`: not disabled, not inactive, still listed; a list keeps what is
|
|
468
|
+
offered; a server's list is asked since [card C-input-memory-offered](#a-remembered-choice-from-a-servers-list-kept-per-group-on-add-only-and-in-rows)).
|
|
469
|
+
A value no longer offered is dropped without a word, never drawn as a broken one. It is the value the form opened with, not a change: no "Unsaved changes", no "Discard?".
|
|
470
|
+
- **Kept when a form saves** (`SubmitForm`, so `ApiForm` and a CRUD's forms too), in local storage under
|
|
471
|
+
`nx-input:{key}:{viewer}` as `{ value }`; an empty value forgets. A form reset after the save (`resetOnSuccess`)
|
|
472
|
+
opens on the new choice. No `viewer`: nothing read or kept (`preferred` still applies). A hidden input keeps nothing.
|
|
473
|
+
- **No hydration difference:** the storage is read once the form is in the browser (a layout effect, before the first
|
|
474
|
+
paint), so a form drawn by a server opens the same there and while it hydrates; `preferred` needs no storage and
|
|
475
|
+
applies from the first draw. A later load (a CRUD's record) opens remembered inputs again from what it was given.
|
|
476
|
+
- `FormStore` gains `attachMemory(storage)` and `rememberSaved(values)` (both `@internal`); the rules are pure in
|
|
477
|
+
`Helpers/InputMemory.ts` (`scripts/test-input-memory.mts`, 46 checks).
|
|
478
|
+
- **Fixed (card C-input-memory-fix):** an input with `showWhen` whose other input gets its value in the same load (its
|
|
479
|
+
`defaultValue`, the record's) opened empty - without its own `defaultValue` and without its remembered value -
|
|
480
|
+
because the form judged it hidden on the store's values from before the load. A load, a reset and a draft's restore
|
|
481
|
+
now judge `showWhen` on the values they open with (the store's `get` answers from them while they are worked out); a
|
|
482
|
+
hidden input still keeps only what the data gave it.
|
|
483
|
+
- Proved by nexus.template's `/demo/form` "Remembered choices" (`input-memory.cjs`, both motion modes: empty with
|
|
484
|
+
nothing kept, a save keeps and the reset opens on it with nothing unsaved, a new form, the company's default winning,
|
|
485
|
+
a disabled and a gone value dropped with no broken tag, a server-drawn page with no hydration error, Forget, light
|
|
486
|
+
and dark at 1280x720 and 390x844), with `forms.cjs`.
|
|
487
|
+
|
|
488
|
+
### A label that follows another input
|
|
489
|
+
|
|
490
|
+
Card C-label-follows (2026-11-05), the owner on a company's registration number: *"label should dynamic if possible like
|
|
491
|
+
if user choosed pan then pan number if vat then vat number if it possible our component allow minimal code logic"*.
|
|
492
|
+
Additions only.
|
|
493
|
+
|
|
494
|
+
- **`InputLayoutParams.labelWhen`** (`InputLabelWhen`: `name`, `labels`, `otherwise`, `placeholders`; Form only):
|
|
495
|
+
the input's label is `labels[value]` for the other input's value as it is now (compared as text, so `1` matches
|
|
496
|
+
`"1"`), else `otherwise`, else its own `label`; its placeholder is `placeholders[value]` where listed, else its own.
|
|
497
|
+
Its messages name it by the label shown ("VAT number is required.") unless it has a `fieldName`.
|
|
498
|
+
- Built on the `showWhen` machinery: `withInputConditions` gives such an input a live `label` (and `placeholder`)
|
|
499
|
+
getter and joins the other input's name into `dependsOn`, so the field draws again when it changes - no remount,
|
|
500
|
+
what was typed and the focus stay. `textFor` and `labelFor` are the pure rules (`test-input-conditions.mts`, 52 to 59).
|
|
501
|
+
- Proved by nexus.template's `/demo/form` "A label that follows another input" (`label-when.cjs`, both motion modes).
|
|
502
|
+
|
|
503
|
+
### A preview the server works out as the person types
|
|
504
|
+
|
|
505
|
+
Card C-server-preview (2026-11-05): nexus.web's counter form and the Print layouts editor each wrote by hand "ask the
|
|
506
|
+
server a moment after the last change, drop the older question, keep the last answer while a new one runs". Additions
|
|
507
|
+
only; nexus.web is not changed here.
|
|
508
|
+
|
|
509
|
+
- **`useServerPreview(ask, { delay, failure })`** (`nexus-shared/client`): `request(question)` asks `ask(question,
|
|
510
|
+
signal)` `delay` ms (default 300) after the last request; an older question is aborted (its signal) and a late
|
|
511
|
+
answer of it is never shown; `result` and the `question` it answers stay while a newer one waits or runs and after
|
|
512
|
+
one fails; `waiting` during the pause, `loading` while it is with the server; `error` is the thrown error's message (an abort is no failure; `failure`
|
|
513
|
+
is the words when it has none); `cancel()` drops what waits or runs, `clear()` the answer too; leaving the page
|
|
514
|
+
stops what runs; `request`, `cancel` and `clear` are the same functions on every draw.
|
|
515
|
+
- **`ServerPreviewStore`**: the same outside React (`getState`, `subscribe`), and the types `ServerPreview`,
|
|
516
|
+
`ServerPreviewState`, `ServerPreviewAsk`, `ServerPreviewOptions`.
|
|
517
|
+
- Proved by `scripts/test-server-preview.mts` (17: a burst asks once, waiting then loading, an older answer dropped, the last answer kept
|
|
518
|
+
through a newer question and a failure, cancel, clear, the latest ask) and nexus.template's `/demo/document-frame`
|
|
519
|
+
layout editor, which now reads its preview through the hook (`document-frame.cjs`, both motion modes, unchanged).
|
|
520
|
+
|
|
521
|
+
### A printable document's logo
|
|
522
|
+
|
|
523
|
+
Card C-print-logo (2026-11-05), for the receipt: the company's bill logo at its head. Additions only.
|
|
524
|
+
|
|
525
|
+
- **`PrintDocument` `logo`** (`PrintLogo` `{ src, alt }`, or any node) and **`logoOnThermal`** (default true). On A5 the
|
|
526
|
+
logo sits beside the title lines in a 24 mm column (at most 16 mm tall) with a column as wide after the lines, so they
|
|
527
|
+
stay centred on the paper and the head grows no taller than they are: a one-page receipt stays one page. On an 80 mm
|
|
528
|
+
roll, and on an A5 document shown narrower than 100 mm (a phone; `.nx-print` is now the `nx-print` container), it
|
|
529
|
+
sits small and centred above them (at most 36 x 10 mm on the roll); `logoOnThermal={false}` leaves it off a roll.
|
|
530
|
+
Printed in black and white (`filter: grayscale(1)`). An `{ src }` image is decoded at once (not lazy); it must
|
|
531
|
+
have loaded before `window.print()`.
|
|
532
|
+
- `Nexus.Print.css`: `nx-print__head--logo`, `nx-print__logo`, `nx-print__heading`. Without a logo the head's markup
|
|
533
|
+
is as it was.
|
|
534
|
+
- Proved by nexus.template's `/demo/print-document` (a colour logo, switches for it and for the roll) and
|
|
535
|
+
`print-document.cjs`: beside the title on A5 within 24 x 16 mm, the title centred, the head no taller than its lines;
|
|
536
|
+
centred above on the roll within 36 x 10 mm; off with `logoOnThermal`; grey in print; the longest receipt, voided,
|
|
537
|
+
printed alone to PDF: one A5 page (420 pt) and one roll page (227 pt).
|
|
538
|
+
|
|
539
|
+
### A page's own switch in the table's bar keeps its label on one line
|
|
540
|
+
|
|
541
|
+
Card C-bar-noshrink (2026-11-06), from nexus.web's Branch users: a `Switch` beside Add ("All branches") broke its label
|
|
542
|
+
between its words at every width, and a non-breaking space made it run into the lists instead. The cause was the Add
|
|
543
|
+
itself: it is a `block` button, `width: 100%` of the Add section, so on a shared line it claimed the section and the
|
|
544
|
+
page's own items shrank (a switch's field may shrink to nothing; a button stopped at its label).
|
|
545
|
+
|
|
546
|
+
- `Nexus.Table.css`: every item of `.nx-table__bar-add` keeps its width (`flex: none`, `min-width: fit-content`), and
|
|
547
|
+
the Add beside other parts is as wide as its label (`width: auto`), so a bar that does not fit takes its next step
|
|
548
|
+
(search, Reload into ⋮, the Add section's own line). On that line the items still share it (`flex: 1 1 0`) and one
|
|
549
|
+
that would not fit its share wraps whole onto a line of its own. No markup or prop changed.
|
|
550
|
+
- Proved by nexus.template's `/demo/data-table` section "The page's own controls in the bar" (a Switch and a Button in
|
|
551
|
+
`actions`, lists in `toolbar`) and `/demo/crud`'s amenities ("Reset demo") in `data-table-bar.cjs`: at table widths
|
|
552
|
+
1000, 899, 680, 660, 380, 370, 330, 320 and 300, with the switch and the button, each alone, and the CRUD's button,
|
|
553
|
+
in seven themes light and dark, compact and cozy: no label broken or cut, no two controls overlapping, nothing
|
|
554
|
+
outside the bar, one line from 670px (compact), the search alone under 670, Reload in ⋮ under 375, the Add section
|
|
555
|
+
last under 325.
|
|
556
|
+
|
|
557
|
+
### A list that can be widened to other endpoints
|
|
558
|
+
|
|
559
|
+
Card C-crud-scope (2026-11-06), from nexus.web's Branch users: the branch signed in at (`BranchUser`) and, for a person
|
|
560
|
+
who may, every branch (`AllBranchUser`), picked by a switch in the toolbar and remembered per person - written by hand
|
|
561
|
+
with a remounted `Crud`, a page-level hook for the storage and two copies of its columns and inputs. Additions only.
|
|
562
|
+
|
|
563
|
+
- **`Crud` `scopes`** (`CrudScopes`: `options`, `label`, `description`, `remember: { key, viewer }`,
|
|
564
|
+
`onChange`): other lists in place of the CRUD's own, each a **`CrudScope`** (`id`, `label`, `description`, `api`,
|
|
565
|
+
`when`, and where they differ `columns`, `inputs`, `form`, `table`, `aria-label`; `form` and `table` over the
|
|
566
|
+
CRUD's, field by field). One scope is a `Switch` after Add (`description` its tooltip); two or more are tabs, the
|
|
567
|
+
CRUD's own list first. A scope is offered to a person who may call one of its lists and passes `when`, again on every
|
|
568
|
+
change of their rights; a kept scope not offered opens the CRUD's own list and stays kept.
|
|
569
|
+
- **Switching keeps the CRUD**: the new list is read on its endpoints (`loadKey` names the scope), the switch keeps
|
|
570
|
+
the focus, popups stay open. Each scope keeps its own cache of lists while the page is open, so switching back costs
|
|
571
|
+
no request; a change made on one scope drops the other scopes' lists (the server alone knows which hold the record).
|
|
572
|
+
An answer asked on one scope and landing after a switch (a row action, a mark, a mass action, a form's save, a
|
|
573
|
+
details refresh) writes nothing into the list on screen: every kept list is dropped and the one on screen read again,
|
|
574
|
+
quietly, and `onChange` still hears of it.
|
|
575
|
+
- **Each scope keeps its own table settings**: its `table.stateKey`, else the CRUD's and `:{id}`. **`DataTable`
|
|
576
|
+
reads a changed `stateKey`** as the first draw does (page size, sort, hidden columns with each column's own
|
|
577
|
+
default, order, widths, pins, open sections) and never writes the settings on screen under the new key;
|
|
578
|
+
`TableStore.restore(saved, { notify: true })` restores over a table already shown.
|
|
579
|
+
- **`remember`** keeps the choice in this browser (`nx-crud-scope:{key}:{viewer}`, the CRUD's own list as nothing,
|
|
580
|
+
storage in try/catch), read after the first draw and before any list is asked for: a reload asks for one list.
|
|
581
|
+
- **`CrudHandle.scope`** and **`setScope(id | null)`**.
|
|
582
|
+
- **A `CheckBox`'s or `Switch`'s `title`** is now its whole row's tooltip (its label too), still on the input.
|
|
583
|
+
- New exports: `CrudScope`, `CrudScopes`, `offeredCrudScopes`, `shownCrudScope`, `crudScopeProps`,
|
|
584
|
+
`crudScopeStateKey`, `crudScopeStorageKey`, `readCrudScope`, `writeCrudScope` (`nexus-shared`);
|
|
585
|
+
`useCrudScopes`, `CrudScopeControl`, `CrudScopeState` (`nexus-shared/client`).
|
|
586
|
+
- Proved by `scripts/test-crud-scopes.mts` (32) and nexus.template's `/demo/crud` section "One desk, or every desk"
|
|
587
|
+
(posts through `DeskStaffPost` and, for the Admin role, every desk's through `StaffPost`; the demo backend's
|
|
588
|
+
narrower controllers are views of the same records) in `crud-scopes.cjs`.
|
|
589
|
+
- **Review fixes** (card C-scope-fix, review round 4):
|
|
590
|
+
- **Opening on a remembered scope no longer wipes the CRUD's own list's table settings.** `DataTable` saves what its
|
|
591
|
+
store holds, never the draw's settings: the first draw, which still held the opening defaults, wrote them under the
|
|
592
|
+
own list's key when the kept scope was read, and its sort and hidden columns were gone on the way back.
|
|
593
|
+
- **A form stays in the scope it was opened on**: an Edit pressed in a details popup after a switch is the details'
|
|
594
|
+
scope's form - its `inputs`, `form` and endpoints - and its save is that scope's answer (it went to the scope shown,
|
|
595
|
+
with the other scope's fields).
|
|
596
|
+
- **Each scope keeps its own Add**, and so its draft (the popup's name is `{name}@{scope}`): a minimized Add of one
|
|
597
|
+
list never comes back on another. An Edit keeps one popup per record wherever it is opened.
|
|
598
|
+
- **An answer that lands after a switch** still marks the lookups of the scope it was asked on and of the one shown as
|
|
599
|
+
old (`expireLookups`: a picker's or a join's next read asks again) and drops every tree's kept pages, as an answer on
|
|
600
|
+
screen writes them.
|
|
601
|
+
- Proved by `crud-scopes.cjs` section K, each line failing on the code before: the own list's sort and hidden
|
|
602
|
+
Department kept through a client navigation that opens on the kept scope; a Spa post's Edit from details restored
|
|
603
|
+
after a switch is every desk's form and saves to `StaffPost/Update`; the own Add's draft stays the own list's; the
|
|
604
|
+
demo's "Find a post" box (every desk's `StaffPost/ActiveListing`, its pages dropped on every change of the CRUD)
|
|
605
|
+
asks its list again after an answer for every desk lands on the front desk's list.
|
|
606
|
+
|
|
607
|
+
### Pinned columns give way on a narrow table
|
|
608
|
+
|
|
609
|
+
Card C-pin-narrow (2026-11-06, with C-scope-fix), from nexus.web's Branches at 390 px: Name and Picture pinned left took
|
|
610
|
+
284 of the table's 354 px, leaving 70 for every other column. Additions only, and no page passes anything.
|
|
611
|
+
|
|
612
|
+
- **The columns that stay - the numbers and checkbox column and the actions, which always stay, and the pinned ones -
|
|
613
|
+
take at most 60% of the table's width on screen** (`PINNED_SHARE`). Pins are kept from the outer edge in; a pinned
|
|
614
|
+
column that would pass that share, and every one inside it on the same side, scrolls with the others until the table
|
|
615
|
+
is wide enough again. Measured after every draw and whenever the table's width changes; staying or scrolling changes
|
|
616
|
+
no width, so it settles at once.
|
|
617
|
+
- A column that gives way keeps its side: it is drawn there, moves among its side's columns, and its menu still offers
|
|
618
|
+
Unpin. **`TableViewColumn.side`** (new) is the side chosen; **`pin`** is the side it stays on now (null while it
|
|
619
|
+
scrolls, so its cells carry no `data-pin`). Headings carry `data-pin-side`, which the drag's drop targets follow.
|
|
620
|
+
- New exports (`nexus-shared`): `PINNED_SHARE`, `releasedPins(view, frame, left, right, share?)`.
|
|
621
|
+
- Proved by `scripts/test-table.mts` (the rule, and `viewColumns` with released columns) and nexus.template's
|
|
622
|
+
`data-table.cjs` at 390 px with Booking and Guest pinned: the columns that stay keep to 60% of the table, the outer
|
|
623
|
+
ones stay with their cells and the others scroll, both keep their side, and at 1280 both stay.
|
|
624
|
+
|
|
625
|
+
### A row action that opens a form of its own for the record
|
|
626
|
+
|
|
627
|
+
Card C-row-form (2026-11-06), from nexus.web's Permissions row action (`usePermissionSetActions`, written by hand: a
|
|
628
|
+
popup with an `ApiForm` reading the set, saving it elsewhere, then `apply` of the row's count). Additions only.
|
|
629
|
+
|
|
630
|
+
- **`Crud` `rowForms`**, each a **`CrudRowForm`**: `id`, `label`, `description`, `icon`, `inline`, `modes`
|
|
631
|
+
(default the lists that offer Edit), `when(row, mode)`, `lock(row)` (listed switched off with its sentence), `target(row)`
|
|
632
|
+
(the `{id}` the endpoints are called with), `inputs` (params, or a function of the record and of what `load` read;
|
|
633
|
+
it may answer a promise), `load` (an endpoint or a function), `submit`, `transform(values, row, loaded)`, `writes`
|
|
634
|
+
(the answer's fields by name, or a function), `title`, `size`, `submitLabel`, `onSaved(result, row, values)`.
|
|
635
|
+
Offered after the built-in actions and before `rowActions`, to a person who may call `submit` and an endpoint
|
|
636
|
+
`load`; one popup per record and form (`reopen: "restore"`), a discard question before changes are lost.
|
|
637
|
+
- **A read that fails never leaves a savable form**: the API client answers `ok: false` rather than throwing, so a
|
|
638
|
+
failed answer (and one with no record, and a function's `null`) is turned into `ApiForm`'s failed state - "Could not
|
|
639
|
+
load the values" with Retry - and the footer's Save is gone until a read succeeds. (A permission set read that failed
|
|
640
|
+
opened empty, and its Save wiped the set.)
|
|
641
|
+
- **The write-back is only what the answer names**: `writes` fields merged over the record as the CRUD holds it when the
|
|
642
|
+
answer lands (the table, else a kept list, else a pinned section), never the record as the action was clicked, so an
|
|
643
|
+
Edit saved while the popup was minimized stays. Written through the cache sync (`applyRows` with the held record),
|
|
644
|
+
so every list kept and the pinned sections follow with no list read; an answer after a scope switch writes nothing.
|
|
645
|
+
- **`CrudHandle.openRowForm(id, row)`**. New exports: `CrudRowForm`, `crudRowFormOffered`, `crudRowFormWrites`
|
|
646
|
+
(`nexus-shared`); `openCrudRowForm`, `CrudRowFormRequest` (`nexus-shared/client`).
|
|
647
|
+
- Proved by `scripts/test-crud-row-forms.mts` (24) and nexus.template's `/demo/crud` posts, whose Duties (PostDuty's
|
|
648
|
+
Detail and Update in the demo backend, a count joined into each post) are a row form, in `crud-row-form.cjs`.
|
|
649
|
+
|
|
650
|
+
### A held menu item says why when it is pressed
|
|
651
|
+
|
|
652
|
+
Card C-held-action-note (2026-11-06), from a review of nexus.accounts' dashboard: a row action switched off with its
|
|
653
|
+
reason (`rowLock`, `rowActionLock`, a row form's `lock`) showed that reason only as its menu line, and a press did
|
|
654
|
+
nothing, where the table's held Add says why under it. Behaviour of `Menu`, `ContextMenu` and `SplitButton`'s list; no
|
|
655
|
+
export is new.
|
|
656
|
+
|
|
657
|
+
- **`MenuPanel`**: an item `disabled` with a `description` is held (`data-held`). A press - a click, Enter, Space -
|
|
658
|
+
shows the description in the note a read-only field and a held Add show (`useReadOnlyNotice`, `.nx-field-notice`,
|
|
659
|
+
`role="status"`), under the item; the menu stays open, nothing is chosen, and the note goes when the menu closes. The
|
|
660
|
+
arrow keys and the pointer reach a held item (`ENABLED_ITEM` takes `[data-held]`), so the keyboard can press it;
|
|
661
|
+
a disabled item without a description is skipped as before.
|
|
662
|
+
- Proved by nexus.template's `held-row-actions.cjs` on `/demo/crud`: Wi-Fi's held Move to trash (`rowActionLock`) and
|
|
663
|
+
a not-yet-joined person's held Duties (a row form's `lock`) say their menu line's words when clicked and on Enter
|
|
664
|
+
after the arrow keys reach them, with the menu open and no request sent; Esc closes both; `/demo/menus`' Print
|
|
665
|
+
(switched off with no reason) is still skipped; the note inside the window and its words at 4.5:1 or more in seven
|
|
666
|
+
themes, light and dark, at 1280 and 390.
|
|
667
|
+
|
|
668
|
+
### A form with changes asks before a navigation inside the app
|
|
669
|
+
|
|
670
|
+
Card C-leave-guard (2026-11-06), from a review of nexus.accounts' dashboard: its profile form lost its edits through the
|
|
671
|
+
side menu, since only the page's close (`beforeunload`) asked. Additions only.
|
|
672
|
+
|
|
673
|
+
- **The page's own forms**: a `SubmitForm` (and so an `ApiForm`) that asks before the page closes (`confirmLeave`, on
|
|
674
|
+
without a draft) also joins the tab's page guard (`PageLeaveGuard` in `FormLeaveGuard.ts`, beside the workspace's
|
|
675
|
+
guard), unless it is drawn in a `PopupHost` popup, which stays open through a navigation (`PopupHost` gives its
|
|
676
|
+
popups no page guard).
|
|
677
|
+
- **Links**: while one of them has changes, a press on a same-origin link - next/link or a plain `<a>`, the side menu,
|
|
678
|
+
the brand, the trail, a menu's link row - is held in the capture phase on `window` (before any other handler) and
|
|
679
|
+
asks "Discard your changes?" (`confirmDiscard`'s words, as a workspace tab and a form popup ask); Keep editing stays,
|
|
680
|
+
Discard follows the link as it was pressed (the app's router for its links). A link to another tab, a download,
|
|
681
|
+
another site (the browser's close question asks there) or this page is left alone. Set up once, by the first such
|
|
682
|
+
form.
|
|
683
|
+
- **`confirmPageLeave()`** (`nexus-shared/client`): the same question for a navigation the app makes itself
|
|
684
|
+
(`if (await confirmPageLeave()) router.push(href)`). A confirmed navigation is let go for a moment
|
|
685
|
+
(`PageLeaveGuard.allow`), so the form's `beforeunload` does not ask a second time after a plain link's Discard.
|
|
686
|
+
- Proved by nexus.template's `leave-guard.cjs` on `/admin/settings`: the side menu, the brand, the trail, Back to the
|
|
687
|
+
site, the account menu's link and the module menu each ask and Keep editing keeps what was typed; Discard goes (by the
|
|
688
|
+
router, and by the address for a plain link with no browser question); a form without changes, and one just saved,
|
|
689
|
+
goes with no question; a minimized Add with changes (a `PopupHost` popup) holds nothing and is still on the dock; a
|
|
690
|
+
reload with changes still gets the browser's question.
|
|
691
|
+
- **Review fixes** (card C-leave-fix, review round 4):
|
|
692
|
+
- **A popup's form keeps the browser's close question**: a confirmed Discard of the page's form let the next moment's
|
|
693
|
+
full-page load go with no question for every form, so a minimized Add with changes was lost without a word. Only the
|
|
694
|
+
page's own forms are let go now.
|
|
695
|
+
- **The page already in view asks nothing**: a module or account menu row for it lost nothing and still asked.
|
|
696
|
+
**`keepsPage(href, target?, download?)`** (new, `nexus-shared/client`) is the one test, for links and for the
|
|
697
|
+
rows nexus-admin moves by `router.push`.
|
|
698
|
+
- **A link inside text being edited** (the rich-text editor's) is not a navigation: a click there places the caret,
|
|
699
|
+
and nothing is asked.
|
|
700
|
+
- **`confirmPageLeave({ unload: true })`**, for a move that loads a whole page (a sign-out, `location.assign`): asks
|
|
701
|
+
about every form with changes, the popups' too, since the load takes them all; once confirmed, neither it nor the
|
|
702
|
+
browser's close question asks again until the page goes (`pagehide`), however long the call before the move takes.
|
|
703
|
+
**`endPageLeave()`** (new) gives the questions back when the move did not happen.
|
|
704
|
+
- Proved by `leave-guard.cjs` section F: the module row for the page in view and a link in the House rules editor ask
|
|
705
|
+
nothing and keep the typing; a page Discard followed by a full load still gets the browser's question for a
|
|
706
|
+
minimized Add with changes; Sign out (slowed by 2.5 s) asks about a minimized Add first, Keep editing sends nothing,
|
|
707
|
+
Discard signs out with no second question.
|
|
708
|
+
|
|
709
|
+
### Every picture through the editor, and within Drive's limits
|
|
710
|
+
|
|
711
|
+
Card IMG-comp (2026-11-06), the owner: *"any image editor should be open the editor then it upload ... first compress
|
|
712
|
+
then save ... ~300KB max but put it into config ... max resolutions, max quality (%) in config for drive."* A picture
|
|
713
|
+
of about 3 MB was stored: the gallery's Upload never opened the editor, a PNG stayed a PNG at up to 2048 px, a
|
|
714
|
+
remembered quality could be 100, and nothing kept a byte budget. Drive's side is card IMG-api
|
|
715
|
+
(`drive-picture-limits-contract.md`): its host settings `Nexus:Drive:Pictures` (MaxSide 1920, Quality 80, MaxKilobytes
|
|
716
|
+
300), answered by `File/List` and enforced on every upload.
|
|
717
|
+
|
|
718
|
+
- **`FileCategoryInfo`** gains `pictureMaxSide` (px), `pictureQuality` (%) and `pictureMaxBytes`, optional, read by
|
|
719
|
+
`NexusDriveAdapter` from `File/List` with the defaults when an older Drive answers none (so every picture is still
|
|
720
|
+
made small). **`PICTURE_LIMITS`** (`{ maxSide: 1920, quality: 80, maxBytes: 307200 }`) and **`pictureLimits(category)`**
|
|
721
|
+
(the category's, else the defaults; the bytes never above its `maxBytes`) are new. `MAX_PICTURE_SIDE` is now the
|
|
722
|
+
default 1920 (it was 2048) and nothing in the package reads it.
|
|
723
|
+
- **Every picture through the editor** (`useOwnerFiles`): the gallery's Upload - every picture column, the top bar's own
|
|
724
|
+
picture, a record's workspace, the accounts dashboard - opens the editor for each picture in turn ("Picture 2 of 5 ·
|
|
725
|
+
{title}"); Cancel skips that one and, with more to come, asks "Go on with the next file?" (Next file, Stop here). It
|
|
726
|
+
keeps the chosen file's name with the kind written. Documents (a PDF) are sent as they are. `renderImage` is no longer
|
|
727
|
+
called by the package.
|
|
728
|
+
- **Within the limits**: the editor gets the long edge at most `pictureMaxSide` (less when the params say), `maxQuality`
|
|
729
|
+
`pictureQuality` / 100 and `maxBytes` `pictureMaxBytes` (`ImageEditorOptions` and `renderImage`'s options take both;
|
|
730
|
+
`ImageEditorResult.fitted` says what the budget took, see nexus-image-editor's CHANGELOG). The toast after a save
|
|
731
|
+
says when a picture was made smaller.
|
|
732
|
+
- **`fileRefusal`** checks a picture against `pictureLimits(category).maxBytes` and a document against `maxBytes`; the
|
|
733
|
+
gallery's line says "up to 300 KB each", or "pictures up to 300 KB, documents up to 10 MB" where both are taken.
|
|
734
|
+
- Proved by `scripts/test-files.mts` (84) and nexus.template, whose pretend Drive answers and enforces the three
|
|
735
|
+
(`Demo/PictureLimits` raises them for the checks): `picture-limits.cjs` (a 4.2 MB photo stored at about 175 KB with the
|
|
736
|
+
Save panel's real size and its note, the slider's top 80 and a remembered 95 starting at 80, a see-through PNG stored
|
|
737
|
+
as a WebP that keeps its see-through parts, an opaque PNG as a JPEG, three pictures each through the editor with a
|
|
738
|
+
Cancel and Next file, Stop here, DRV_X059 for a client that skips the editor, raised settings letting a 1.8 MB
|
|
739
|
+
picture through) and `pictures.cjs` (the gallery's three uploads each through the editor, the page's square shape).
|
|
740
|
+
- **Review fixes** (card IMG-comp-fix, review round 4):
|
|
741
|
+
- **Only into a kind the category takes**: the editor gets the category's picture kinds (`ImageEditorOptions.types`,
|
|
742
|
+
from `pictureOptions`). Its Save panel offers only those, a picture opens on one of them, and a PNG over the size is
|
|
743
|
+
made a JPEG or a WebP only where that kind is taken - where neither is, it stays a PNG and is made smaller. A
|
|
744
|
+
see-through PNG for a category of JPEG and PNG was written as a WebP and then refused.
|
|
745
|
+
- **A picture the browser could not type** (no type, or `application/octet-stream`: a phone's share, a name with no
|
|
746
|
+
extension) is typed from its first bytes and goes through the editor, where it was sent as it was:
|
|
747
|
+
**`sniffPictureType(blob)`** (JPEG, PNG or WebP, else null) and **`typedFile(file)`** (new, `nexus-shared`). Any other
|
|
748
|
+
untyped file is still the backend's to judge.
|
|
749
|
+
- **An editor that cannot be downloaded says so where it was asked for**: the picture input's line under its buttons
|
|
750
|
+
and the gallery's alert say the loader's words ("The image editor is not installed on this server yet." for Drive's
|
|
751
|
+
DRV_X053) until a try opens it, besides the toast; the button stays ready, and pressing it again tries again.
|
|
752
|
+
`useOwnerFiles` answers `editorProblem`.
|
|
753
|
+
- Proved by `test-files.mts` (88) and `picture-limits.cjs` section G: the ID photo's category (JPEG and PNG) offers
|
|
754
|
+
those two in Save and stores a 3 MB see-through PNG as a PNG under 300 KB that is still see-through; a photo handed
|
|
755
|
+
over with no type opens the editor and is stored as "IMG_2041.jpg" under 300 KB; with Drive's editor missing the
|
|
756
|
+
input and the gallery say why, and once it is there the same button opens it and the line goes.
|
|
757
|
+
|
|
758
|
+
### A record's page of cards, every change in a popup
|
|
759
|
+
|
|
760
|
+
Card C-record-cards (2026-11-06), from the owner's request for nexus.accounts' profile: *"make card types block
|
|
761
|
+
buetifull design ... when click editor then show popup and apply editor or add or something do not show direct save or
|
|
762
|
+
update"*, and *"Emails / Phones / Addresses - those all put inside profile tab as card. put right side icons to verify if
|
|
763
|
+
not. switch primary like this. single page, CURD from popup show add button, edit, remove like this. primary do not
|
|
764
|
+
allow"*. Additions only.
|
|
765
|
+
|
|
766
|
+
- **`RecordCards`** (`nexus-shared/client`): one record that is not a list - a person's profile, my company, a
|
|
767
|
+
subscriber's details - as a hero and cards, declared by params (`RecordCardsProps`): `sources` (each read once by GET
|
|
768
|
+
through the app's `api`, or a function; `{ read, held }` draws what is known before, such as the session's name),
|
|
769
|
+
`hero` (`picture` by Drive params - a click opens the pictures window - `title`, `lines`, `marks`, `actions`), `cards`,
|
|
770
|
+
`readOnly`, `signedOut` (the page's words, `onSignIn`, `onEnded`), `isSessionEnded`, `name`, `headingLevel`. **The page
|
|
771
|
+
itself never saves.**
|
|
772
|
+
- Three cards, each with an `id` (its element id, so `#emails` leads to it), a `source`, `title`, `icon`, `description`,
|
|
773
|
+
heading `actions`, `hidden`, `wide`, `readOnly`:
|
|
774
|
+
- **fields** (`RecordFieldsCardParams`): label over value, each value drawn by the `InputParams` its Edit uses - a
|
|
775
|
+
choice by its label, a date in the person's format, a number with its format, Yes or No, a multiple choice or
|
|
776
|
+
`look: "chips"` as chips, long text on its own lines, Markdown and rich text as such, a password never, "Not said"
|
|
777
|
+
(`empty`) when empty; `show` draws one by the page. **`edit`**: a popup of the card's inputs on the record, its values
|
|
778
|
+
merged over the record as held now when sent, so an endpoint that takes the whole record works.
|
|
779
|
+
- **items** (`RecordItemsCardParams`): rows between hairlines (a picture or icon, the title, marks, lines) with **icon
|
|
780
|
+
actions on the right** (`RecordItemActionParams`), each named as a tooltip and to a screen reader with its item
|
|
781
|
+
("Verify: ram@example.com"), hidden, or held with its reason - aria-disabled, the reason in its tooltip and said in
|
|
782
|
+
the note a held Add shows when pressed; **`add`** in the heading, held at the `limit` ("5 of 5" beside the title,
|
|
783
|
+
`limitReason`).
|
|
784
|
+
- **content** (`RecordContentCardParams`): `render(context)`, the page's own; `context.open(flow)` opens a popup.
|
|
785
|
+
- **Every change is a popup** (`RecordFlowParams`): one step, or `steps` with `start` and `next` (a code sent, then
|
|
786
|
+
typed), each with its `lead`, `inputs`, `submit` (a URL, a request, an endpoint or a function), `transform`,
|
|
787
|
+
`submitLabel`/`submitTone`, `writes`, `also` (a button that calls and stays, held with a countdown while the server says
|
|
788
|
+
to wait: "Send again in 20s"), `done` (run another action: the code step after an Add, Make primary after a Verify),
|
|
789
|
+
`onError`, `render`, `confirmCancel`; one popup per card, action and item, brought back as it was; closing with changes
|
|
790
|
+
asks first. A form opens only on the plain values its inputs name, so a code box never opens on an item's `code`
|
|
791
|
+
record. **`confirm`** (`RecordConfirmParams`) asks, then calls.
|
|
792
|
+
- **The card shows the answer at once**: `writes` (`RecordWrites`) - the answer merged over the data as held now (the
|
|
793
|
+
default), the values, one item replaced or added, the item removed, a read again, named fields, or a function - into
|
|
794
|
+
the source, so every card on it (the hero too) follows and nothing is read again.
|
|
795
|
+
- **States**: loading draws every card as it will be, held (labels and bones as tall as their values, a held mark row,
|
|
796
|
+
Edit held with "Still being read."); a source that fails says why in each of its cards with Try again (the first says
|
|
797
|
+
it aloud); read-only draws no button; an answer that ended the session (default a 401 naming no input - a wrong
|
|
798
|
+
password names its box) closes the page's popups and leaves the page's words with Sign in again, which takes the
|
|
799
|
+
focus. The focus comes home when a popup's or a question's opener went with the change (the row's first button, else
|
|
800
|
+
the card's). A link to a card (`#emails`) is followed again once every source has answered, since the cards above it
|
|
801
|
+
grow as their records come.
|
|
802
|
+
- **Layout**: the hero across, then as many cards to a row as have 26rem (one on a phone), each as tall as what it
|
|
803
|
+
holds; fields as many to a row as have 11rem. It sits in the admin page's scroller, so the window never scrolls.
|
|
804
|
+
`Nexus.Record.css`; the table's tag gets `nx-table__tag--plain` (no dot) for a chip.
|
|
805
|
+
- New exports: `RecordCards` (`nexus-shared/client`); in `nexus-shared` the interfaces `RecordCardsProps`, `RecordRead`,
|
|
806
|
+
`RecordSourceParams`, `RecordWrites`, `RecordMark`, `RecordSubmit`, `RecordContext`, `RecordStepContext`,
|
|
807
|
+
`RecordStepAnswer`, `RecordCallParams`, `RecordStepParams`, `RecordFlowParams`, `RecordConfirmParams`,
|
|
808
|
+
`RecordCardActionParams`, `RecordItemActionParams`, `RecordCardBase`, `RecordFieldParams`, `RecordFieldsCardParams`,
|
|
809
|
+
`RecordItemView`, `RecordItemsCardParams`, `RecordContentCardParams`, `RecordCardParams`, `RecordHeroParams`,
|
|
810
|
+
`RecordSignedOutParams`; the typed helpers `fieldsCard`, `itemsCard`, `contentCard`; and the pure half
|
|
811
|
+
(Helpers/RecordCards.ts) `recordCardKind`, `recordField`, `recordFieldInputs`, `recordOpenedValues`, `recordFieldWide`,
|
|
812
|
+
`recordFieldHeld`, `recordValueView`, `RecordValueView`, `recordItems`, `recordItemKey`, `recordWithItems`,
|
|
813
|
+
`recordLimit`, `recordCount`, `recordLimitReason`, `recordWrite`, `RecordWriteRequest`, `RecordWriteOutcome`,
|
|
814
|
+
`recordActionState`, `recordActionEndpoint`, `RecordActionState`, `recordSteps`, `recordStartStep`, `recordStatusLine`,
|
|
815
|
+
`recordSessionEnded`, `secondsUntil`, `RecordSources`, `RecordSourceState`, `RecordReader`.
|
|
816
|
+
- Proved by `scripts/test-record-cards.mts` (130) and nexus.template's `/admin/profile` (a pretend person's profile on
|
|
817
|
+
`src/app/api/demo-profile`, with a menu of demo states) in `record-cards.cjs`.
|
|
818
|
+
|
|
819
|
+
### The pictures window in two panes, as 2026-06-20's
|
|
820
|
+
|
|
821
|
+
Card C-gallery (2026-11-06), from the owner's look at the pictures window on nexus.accounts' profile: *"the over all
|
|
822
|
+
design i do not like. when hover then show inside the image the absolute position. look like the second image. this
|
|
823
|
+
image capture from: 06-20 version. when click profile icon then show popup like this."* No new exports, nothing removed;
|
|
824
|
+
the look and where the buttons are changed, every behaviour stayed.
|
|
825
|
+
|
|
826
|
+
- **`PictureGallery` / `openPictureGallery`**, for pictures: two panes with a hairline between them. On the left the
|
|
827
|
+
main picture large (about 15rem on its long side, a rounded square for every shape; the picture icon when there is no
|
|
828
|
+
main one), **Upload** (a solid primary button; Replace in a category that keeps one file) and **Remove** (no main
|
|
829
|
+
picture, the pictures kept; Delete in a one-file category) under it, then the count and kinds ("2 of 20 · PNG, JPEG or
|
|
830
|
+
WebP, up to 300 KB each"), a read-only reason and the editor-cannot-load note. On the right the pictures as a grid of
|
|
831
|
+
rounded squares (about 11rem, at least two a row), a round category's too - only an avatar stays round. The main one
|
|
832
|
+
has the accent ring and a **Main** mark in its top left corner.
|
|
833
|
+
- **Each picture's buttons are inside it**, centred on a popover's panel: View, Make main and Delete (neither on the
|
|
834
|
+
main one: it is never deleted from its own picture, as in 2026-06-20's gallery - Remove it first, or make another the
|
|
835
|
+
main one) and Edit. They show on hover and on a keyboard's focus in the picture (`:has(:focus-visible)`, so a click
|
|
836
|
+
does not leave them shown), and always where nothing can hover (`@media (hover: none)`: a touch screen); hidden, they
|
|
837
|
+
stay in the Tab order and to a screen reader, each named as a tooltip and to a screen reader ("Make Sita.png the main
|
|
838
|
+
picture"). They fade in briefly, and not at all with animations off.
|
|
839
|
+
- **Under 560px of its own width** (a phone, a narrow tab) the main picture is 7rem beside its buttons, over the grid.
|
|
840
|
+
- **The window** (`openPictureGallery`) is `xl` and edge to edge for pictures, with a height of its own
|
|
841
|
+
(`min(36rem, 100dvh - 10rem)`, the whole window when maximized): only the grid scrolls. Documents stay a list in an
|
|
842
|
+
`lg` window; one opened without `view` changes to it once the category says it takes PDF.
|
|
843
|
+
- **States**: loading draws the panes at their size, the main picture's place held and three held pictures where the
|
|
844
|
+
grid will be ("Loading the pictures…"); a gallery with none says "No pictures yet." with Upload on the left; read-only
|
|
845
|
+
owners (`readOnly`, `lockReason`, a refused write ticket) get no Upload, Remove, Make main, Edit or Delete. Upload
|
|
846
|
+
and Edit when the category is full are `aria-disabled` with the reason as their tooltip, and say it when pressed.
|
|
847
|
+
- **`PictureInput`**: a click on its picture, or Enter, opens the same window, as All pictures does (read only where the
|
|
848
|
+
input is); its buttons stay. The picture is named by what it opens ("Picture: open the pictures"), never by the
|
|
849
|
+
file's name. Its buttons are a `role="group"`, and `useFocusThroughChange` keeps the focus in a tile, row or group, so
|
|
850
|
+
after a Delete the focus lands on Upload, not on the picture.
|
|
851
|
+
- The data hook is `data-state="loading|ready|failed"` on `.nx-files`; the tile's `.nx-files__tile[data-primary]`,
|
|
852
|
+
`.nx-files__thumb` and the buttons' names are as before. `.nx-files__tile-bar` and `.nx-files__thumb--round` are gone.
|
|
853
|
+
- Proved by nexus.template's `pictures.cjs` (its section K: the panes, the mark, hover, keyboard and touch, Remove,
|
|
854
|
+
loading, empty, read only, a 390 px phone, the picture input's picture) and `picture-limits.cjs`, both motion modes.
|
|
855
|
+
|
|
856
|
+
### A popup has no minimize button unless it asks
|
|
857
|
+
|
|
858
|
+
Card C-popup-minimize (2026-11-06), from the owner: *"every place do not put minimize popup buttons - for example the
|
|
859
|
+
profile picture viewer, or something just full admin page + CURD this place put minimized option for only the edit and
|
|
860
|
+
add"*. No new exports, nothing removed; **one default changed**, which an app sees without changing a line.
|
|
861
|
+
|
|
862
|
+
- **`Popup` / `openPopup`'s `minimizable` defaults to false** (it was `closable`, so every closable popup had the
|
|
863
|
+
button). Maximize and close are as they were. A page may still ask: `minimizable` on a `Popup` or in `openPopup`'s
|
|
864
|
+
options, or a rule by key (`definePopupRules({ "booking-edit-*": { minimizable: true } })`).
|
|
865
|
+
- **Only a CRUD page's forms ask**: a `Crud`'s Add and Edit (`openCrudForm`: from the list's Add, a row's Edit, a
|
|
866
|
+
tree's Add inside a row, and the details popup's Edit) and its row forms (`openCrudRowForm`, such as a record's
|
|
867
|
+
Permissions). They ask with `popupRuleFor(key)?.minimizable ?? true`, so a page's rule for their key can still take it
|
|
868
|
+
away. The dock, `reopen`, and the leave guard work for them as before: a minimized form with changes is still asked
|
|
869
|
+
about by a full-page move (`confirmPageLeave({ unload: true })`) and by the browser's `beforeunload`.
|
|
870
|
+
- **Everything else has none**: the details popup and a record's workspace (its Edit opens the form that has one), the
|
|
871
|
+
mass Description, the audit history, the spreadsheet import and bulk insert, the pictures window and gallery
|
|
872
|
+
(`openPictureGallery`), a record's cards' popups (`RecordCards`, `openRecordFlow`), a `Chat`'s popup, message boxes
|
|
873
|
+
and confirms (which already said `minimizable: false`), and any popup a page opens without asking. The image
|
|
874
|
+
editor's window and the picture viewer are the editor module's own and never had one.
|
|
875
|
+
- `handle.minimize()` from code still minimizes any popup opened with `openPopup`; the dock shows it as before.
|
|
876
|
+
- Proved by nexus.template's `popup-windows.cjs` (a popup that asks has Minimize, Maximize, Close; one that does not -
|
|
877
|
+
from a page, from code, opening maximized - has Maximize and Close; the dock, the stack, the rules by key, a scrolled
|
|
878
|
+
body kept), `crud.cjs`, `crud-row-form.cjs`, `staff-posts.cjs`, `leave-guard.cjs` (the forms that minimize),
|
|
879
|
+
`crud-workspace.cjs` (the workspace has none; a record off the page shown is now opened from the pinned guests),
|
|
880
|
+
`crud-scopes.cjs` (the details have none; the Edit form they open is minimized across a switch), `excel.cjs`,
|
|
881
|
+
`pictures.cjs` and `record-cards.cjs` (none), both motion modes, and `test-popup-rules.mts` (the forms' rule).
|
|
882
|
+
|
|
883
|
+
### A CRUD's narrowing box asks to be set
|
|
884
|
+
|
|
885
|
+
Card C-narrow-required (2026-11-06), from the owner on nexus.web's subscribers: *"when open it focus on the group and
|
|
886
|
+
high light the required tab which help user to it need to set"*. Every `Crud` `narrowPicker` changes; a page changes
|
|
887
|
+
nothing. New: `Field` / `Dropdown` `invalid`, `DataTable` `add.onDisabledPress`, and in `CrudNarrowPick.ts`
|
|
888
|
+
`findLoneNarrowPick`, `startNarrowPick` (`NarrowPickStart`) and `narrowBoxTakesCursor` (`NarrowCursorMoment`).
|
|
889
|
+
`useCrudNarrowPicker` takes the list's words too (optional) and returns more parts (`CrudNarrowPickerParts`); `Crud`
|
|
890
|
+
is its only caller in the packages.
|
|
891
|
+
|
|
892
|
+
- **It settles before it asks.** As the page opens, the box shows a spinner and the list waits (no "Pick a group" that
|
|
893
|
+
flashes before a remembered pick opens) while the kept pick is looked up; a kept pick no longer offered is forgotten.
|
|
894
|
+
- **A box that offers one record picks it by itself**, and keeps it as a person's pick is (`remember`), `onPick` told.
|
|
895
|
+
Offered as the box offers it: a record it never offers (a person still invited, an inactive one) does not count, and
|
|
896
|
+
its pages are read at the box's own size, only until a second record shows.
|
|
897
|
+
- **With nothing picked it asks:** the box is required (`aria-required`) and drawn with a field's invalid edge and ring
|
|
898
|
+
(`invalid`: the look an error gives, with no message line in the toolbar), and a screen reader hears the list's
|
|
899
|
+
words with it (`aria-describedby`; the page's `emptyMessage` when given). It takes the cursor as the page opens,
|
|
900
|
+
without scrolling, unless the person has already pressed a key or a pointer there, focus is elsewhere, a popup or a
|
|
901
|
+
panel is open, the box is not wholly on screen (a CRUD further down a demo page), or the screen is touch-only (a
|
|
902
|
+
cursor would raise the on-screen keyboard over the list). A pick removes the mark; clearing it brings it back.
|
|
903
|
+
- **The list points at it:** its empty message has a button with the box's placeholder ("↑ Pick a group") that sends
|
|
904
|
+
the cursor to the box and opens its list. A read-only list (no Add for this person) asks the same way.
|
|
905
|
+
- **Add pressed with nothing picked** sends the cursor to the box and nudges it (the field's nudge, as a read-only lock's;
|
|
906
|
+
none with reduced motion), and the box's description becomes Add's reason (`addMessage`), so a screen reader hears
|
|
907
|
+
why it landed there; the note under Add no longer shows for it. Add still looks unavailable and keeps its tooltip.
|
|
908
|
+
- **A failed read** of the box's records leaves it asking, the cursor in it; its own list says "Could not load the
|
|
909
|
+
options." with Retry when opened.
|
|
910
|
+
- Proved by nexus.template's new **Admin › Posts** page (`/admin/posts`: one person's posts, the Person box in the CRUD's
|
|
911
|
+
toolbar; `?people=one` offers one person) and `crud-narrow-required.cjs` (settling, the mark, the cursor, Add from the
|
|
912
|
+
mouse and the keyboard, the list's button, a pick, remembered, forgotten, at work, a popup open, a failed read and
|
|
913
|
+
Retry, one person offered among invited ones, below the fold and the Viewer role on /demo/crud, 390 px with a mouse
|
|
914
|
+
and on a touch screen, the edge at 3:1 and the button at AA in all seven themes light and dark), both motion modes,
|
|
915
|
+
and `test-input-narrowing.mts` (the opening and the cursor rule).
|
|
916
|
+
|
|
917
|
+
### A remembered choice from a server's list, kept per group, on Add only, and in rows
|
|
918
|
+
|
|
919
|
+
Cards C-input-memory-offered (2026-11-06, committed unfinished) and C-input-memory-offered-finish (2026-11-07), from what
|
|
920
|
+
nexus.web's Subscribers Add (card SUB-defaults) found in [`remember`](#an-input-that-remembers-its-last-value-or-opens-on-the-companys-default):
|
|
921
|
+
a kept type, area or package since trashed opened tagged "Inactive", one moved to another group opened untagged and the
|
|
922
|
+
save was refused, a contact's or a box's kind could not be remembered, and the page wrote "on Add only" and "per group"
|
|
923
|
+
by hand. Additions only; no new export.
|
|
924
|
+
|
|
925
|
+
- **A picker confirms a kept choice before it opens.** A dropdown whose options come from `loadOptions` (as `crudPicker`
|
|
926
|
+
gives) is asked by the value's key: at once from what the page holds (`peekOption`), else by `resolveOptions`, and for a
|
|
927
|
+
key that answers nothing (a picker that names only its kept list) or a record without its narrowing's field, through
|
|
928
|
+
its own pages narrowed as the form narrows them. Offered = found, not disabled, not inactive (`isOptionInactive`, else
|
|
929
|
+
`inactive`/`isActive: false`), and inside its `narrowedBy` narrowing; for a list of values, the part offered. A kept
|
|
930
|
+
choice not offered never opens and is forgotten; `preferred` gives way to the kept choice the same way. Before it was
|
|
931
|
+
"a server's list takes any value".
|
|
932
|
+
- **While it is asked** the box shows its spinner (`Dropdown` `loading`, through the new `InputOverrides.loading`) and
|
|
933
|
+
opens nothing; the answer opens the value as the one the form opened with. It opens nothing over the person: a pick
|
|
934
|
+
(even one cleared again) or a value set from code ends the ask at once. A save, a reset and a later load stop it. An
|
|
935
|
+
answer given for a group (the `narrowedBy`, `per` or `when` inputs) the form no longer holds opens nothing. A picker
|
|
936
|
+
that cannot be read opens nothing and forgets nothing; it is asked again by the next form. A reset that finds the
|
|
937
|
+
kept choice gone, or still to be asked, takes back the value memory opened before instead of leaving it in.
|
|
938
|
+
- **`remember.per`:** one value kept per value of another input, under `nx-input:{key}-{that value}:{viewer}`: a package
|
|
939
|
+
per group, since a package is one of its group's. While the other input holds no single value nothing is read or kept.
|
|
940
|
+
- **`remember.when`:** remember only while these `InputCondition`s hold on the values the form opens with and saves.
|
|
941
|
+
A CRUD's Edit form holds the record's key and its Add form does not, so `{ name: "id", filled: false }` remembers on
|
|
942
|
+
Add only: Edit opens on the record's values, asks no picker, and its save keeps nothing.
|
|
943
|
+
- **Rows remember** (a tabular or group form's row inputs): a new row's input opens on its kept value unless `newRow` or
|
|
944
|
+
the values it is added with name it (so a remembered row input takes its first choice as its own `defaultValue`, not
|
|
945
|
+
in `newRow`); a save of the form around them (`SubmitForm`) keeps the value of the row added last that is still there
|
|
946
|
+
(one added then removed does not count; none added, nothing kept). `per` and `when` read the row's own values. A
|
|
947
|
+
picker in a row is asked the same way (no spinner there), judged on the row as it was added.
|
|
948
|
+
- Inside: `Helpers/InputMemory.ts` (`keptStandingNow`, `findKeptStanding`, `rememberedOpeningNow`,
|
|
949
|
+
`findRememberedOpening`, `rememberDependencies`, `lastAddedRowKey`; pure), `FormStore` (`isRemembering`, internal),
|
|
950
|
+
`RowsStore` (`attachMemory`, `rememberAdded`), `RowsInput` and `SubmitForm` (the save tells the rows inside the
|
|
951
|
+
`<form>`). `test-input-memory.mts` 115 checks, `test-rows-store.mts` 123.
|
|
952
|
+
- Proved by nexus.template's `/demo/form` "Remembered picks from a server's list, and in rows" (an order's amenity, a
|
|
953
|
+
`crudPicker` narrowed by the order's group and kept per group on a new order only; a contact's kind in rows) and
|
|
954
|
+
`input-memory-picker.cjs`: nothing kept, a save and the reset, another group, a page drawn by the server with the
|
|
955
|
+
lookup held (the spinner, then the value, no hydration error), a trashed amenity and another group's (asked by key on
|
|
956
|
+
a cold page, and at once from the list the page holds; no tag), a pick before the answer, the lookup down and the
|
|
957
|
+
whole picker down, Edit, a row added then removed, a pick by the keyboard while asked and the kept value heard with
|
|
958
|
+
the box, light and dark at 1280x720 and 390x844, both motion modes.
|
|
959
|
+
|
|
960
|
+
### A record tab's footer that stays in view
|
|
961
|
+
|
|
962
|
+
Card C-tab-footer (2026-11-07), the owner's first ask of the day for the subscriber window's counter tabs: their line
|
|
963
|
+
("To pay …") and Save sat under the form, so at 1280x720 Save was at y=807 of a 644px panel and had to be scrolled to.
|
|
964
|
+
Additions only: 2 exports.
|
|
965
|
+
|
|
966
|
+
- **`CrudTabFooter`** (`nexus-shared/client`), with **`CrudTabFooterProps`** (`children`, `className`): put last in a
|
|
967
|
+
tab's content, it holds the tab's words (a `<p>`) at the start and its buttons (`FormButtons`, or `Button`s) at the
|
|
968
|
+
end. In a record's workspace it sticks to the bottom of the tab's scrolling panel while the tab scrolls and sits in its
|
|
969
|
+
place at the end of the content, so Save is always in view; it reaches across the panel's whole inner width on the
|
|
970
|
+
panel's own surface under a `--nx-border` hairline, with the popup footer's room (`--nx-space-3` above and below, the
|
|
971
|
+
panel's side padding at the sides) and gap, no shadow and no frame of its own. On a phone the words and the buttons
|
|
972
|
+
wrap as whole groups. Outside a workspace it is a plain bar where it stands.
|
|
973
|
+
```tsx
|
|
974
|
+
<ApiForm form={form} buttons={false} api={COUNTER_CREATE} inputs={inputs} />
|
|
975
|
+
<CrudTabFooter>
|
|
976
|
+
<p role="status" aria-live="polite">{summary}</p>
|
|
977
|
+
<FormButtons form={form} submitLabel="Save" />
|
|
978
|
+
</CrudTabFooter>
|
|
979
|
+
```
|
|
980
|
+
- **The keyboard and the footer.** A field (or a text box's caret) the focus moves to is scrolled clear of the footer:
|
|
981
|
+
the panel keeps the footer's height as room (`scroll-padding`), from `--nx-crud-tab-footer-size`, which the footer
|
|
982
|
+
measures and keeps on its panel while it is drawn. While the focus is in the footer that room goes, so Save taking
|
|
983
|
+
the focus never scrolls the tab. A Save that finds a problem scrolls the field to above the footer.
|
|
984
|
+
- Styles: `Nexus.Crud.css` (`nx-crud-tab-footer`; the panel's side padding is now `--nx-crud-panel-pad-x`, the same
|
|
985
|
+
`--nx-space-5`, and `--nx-space-4` under 720px, and it keeps no padding under a footer).
|
|
986
|
+
- Proved by nexus.template's `/demo/crud` "A guest's workspace", a new tab, **Book a stay** (a form longer than the
|
|
987
|
+
panel at 1280x720, its line and Save in the footer, saved into the guest's notes), and `crud-workspace.cjs` section K
|
|
988
|
+
and section F: the footer at the panel's foot scrolled to the top and to the end (Save hit at its centre both times),
|
|
989
|
+
across the panel's inner width, covering what scrolls under it, a field reached by Tab and Shift+Tab clear of it, Save
|
|
990
|
+
focused by code and by Shift+Tab without the tab moving, the first problem scrolled to, one Update and one Detail for
|
|
991
|
+
a save, the panel's own padding back on another tab, and a plain bar outside a workspace; then in 7 themes, light and
|
|
992
|
+
dark, at 1280x720 and 390x844, a `--nx-border` hairline, the panel's background, no shadow, the line at AA, nothing
|
|
993
|
+
past the bar or the window, whole groups wrapped.
|
|
994
|
+
|
|
995
|
+
### Form figures: an input worked out from others, the server's money rule, and a money column
|
|
996
|
+
|
|
997
|
+
Card C-form-figures (2026-11-07), the owner's first ask of the day: *"after the calculate what is actual value here not
|
|
998
|
+
showing the old version 06-20 showing as calculate value"*, on the price list, the miscellaneous fees and the counters.
|
|
999
|
+
Additions only: 13 exports, all from `nexus-shared`. One look moved by a pixel: a column dropdown's cells now meet their
|
|
1000
|
+
headings' edges (below).
|
|
1001
|
+
|
|
1002
|
+
- **`InputLayoutParams.compute`** (`InputCompute`: `from`, `value(values, withValue)`; Form only): an input worked out
|
|
1003
|
+
from others, shown and never typed. It is written when the form opens (new values, a loaded record) and again when an
|
|
1004
|
+
input in `from` changes - typed, set from code, loaded, reset - or when the form's `computeWith` changes. It is **no
|
|
1005
|
+
form value**: the store keeps none under its name (a loaded record's own field of that name is dropped), so it is
|
|
1006
|
+
never in `getValue()`, `getChanges()` or what Save sends, never checked (`problems()`), never a change (`isChanged()`,
|
|
1007
|
+
the leave guard, drafts), never remembered, and a value set from code or an `onFieldChange` patch naming it is not
|
|
1008
|
+
taken. A server's message or the form's rule naming it is the form's message. In rows it is ignored, as `showWhen` is.
|
|
1009
|
+
```ts
|
|
1010
|
+
{ type: EInputTypes.Number, name: "netAmount", label: "Calculated amount", decimals: 2, compute: { from: ["price", "vatPercentage"], value: values => priceAmounts(values).netAmount } }
|
|
1011
|
+
```
|
|
1012
|
+
- **The box**: the field's frame (label, hint, prefix, suffix, size, design) around an `<output>` named by its label,
|
|
1013
|
+
`aria-live="polite"`, on the read-only tint, its value in `--nx-text`: no lock, no read-only note, no − / +, no Tab
|
|
1014
|
+
stop. A Number is written with its `decimals` always (1,073.50), grouped, in the person's number format; null,
|
|
1015
|
+
undefined, "" and NaN leave it blank, never "NaN" or "0.00". While the values load (`loading`, an ApiForm's
|
|
1016
|
+
`loadValue`) it is blank and disabled with the inputs beside it; in a disabled or read-only form it takes that look.
|
|
1017
|
+
It is as tall as a NumberBox beside it, with the same edge, in every input design. A rule that throws shows nothing and
|
|
1018
|
+
says so in the console. Styles: `nx-field__control--output` and `nx-field__output-text` in `Nexus.Input.css`.
|
|
1019
|
+
- **`FormProps.computeWith`** (so `SubmitForm`'s and `ApiForm`'s too): what the page knows beside the values, such as a
|
|
1020
|
+
server's preview answer, handed to each `compute` as `withValue`. Only the worked-out inputs draw again when it
|
|
1021
|
+
changes, compared by identity, so pass state or a `useMemo` value. `compute: { from: [], value: (_values, line) =>
|
|
1022
|
+
line?.netAmount }` shows a figure the server worked out, and nothing until it answers.
|
|
1023
|
+
- `withInputConditions` joins `compute.from` into the input's `dependsOn`. **`isComputedInput`**, **`computedValue`**
|
|
1024
|
+
and **`computedText`** are the pure rules (`test-input-conditions.mts`).
|
|
1025
|
+
- **`priceAmounts(input, places = 2)`** (`PriceAmountsInput`: `price`, `quantity`, `discountPercentage`,
|
|
1026
|
+
`vatPercentage`; `PriceAmounts`: `amount`, `discountAmount`, `vatAmount`, `netAmount`): the Nexus server's money rule
|
|
1027
|
+
(`SubscriberRenewals.Price`, 06-20's `FN_CalculateAmount`), each step rounded half away from zero, on exact decimals
|
|
1028
|
+
(a scaled BigInt, each figure read from its shortest text), never on floats. A missing, null or NaN figure is 0, and a
|
|
1029
|
+
missing quantity 1. 1,000 at 5 % off and 13 % VAT is 1,073.50; 99.99 at 12.5 % and 13 % is 98.86.
|
|
1030
|
+
- **`priceLadder(row, { keys, locale, totalLabel })`** (`PriceLadderKeys`, `PriceLadderLine`, `PriceLadderOptions`): a
|
|
1031
|
+
receipt's money lines from a row's own figures, on the server's `CounterAmounts` names by default: Rate (left out
|
|
1032
|
+
when the row has none), Amount, "Discount (5%)" and "VAT (13%)" (each left out at 0), then "Calculated amount". A
|
|
1033
|
+
percentage has up to 4 places and no trailing zeros, in `locale` ("Discount (2,5%)" with a decimal comma).
|
|
1034
|
+
- **The presets** (`CrudColumns.ts`, one wording for every page; **`PriceKeys`**: `price`, `quantity`, `discount`,
|
|
1035
|
+
`vat`, by default `price`, `discountPercentage`, `vatPercentage` and no quantity):
|
|
1036
|
+
- `crudInputs.netAmount(keys?, changes?)`: a Number `netAmount` labelled "Calculated amount", 2 places, span 3, hint
|
|
1037
|
+
"Price less the discount, plus VAT on the rest.", worked out from the keys, and blank while the price is empty.
|
|
1038
|
+
`changes` replaces any key, `compute` included (a counter's box shows the server's preview instead).
|
|
1039
|
+
- `crudColumns.amounts(keys?, { group })`: Price (money), Discount and VAT (percents) and Calculated amount
|
|
1040
|
+
(`netAmount`, money, worked out from the row's own figures, so a row a save just answered shows it), under one band,
|
|
1041
|
+
"Amount details". A page spreads them: `...crudColumns.amounts()`.
|
|
1042
|
+
- `crudColumns.priceLadder(row, { keys, locale, totalLabel, group = "Money" })`: the ladder's lines as money columns
|
|
1043
|
+
for a record's details.
|
|
1044
|
+
- **A money column in a dropdown**: `DropdownColumnType` gains `"money"`: the item's figure with 2 places in the
|
|
1045
|
+
person's number format (1,00,000.00 under lakh and crore), at the end of its column (its heading too), in even digits,
|
|
1046
|
+
and empty for a missing one. Its track is `auto` unless it has a `width`. **`dropdownColumnAlign`** says where a
|
|
1047
|
+
column's cells sit. A column dropdown's rows now take the list's gutters (`column-gap: normal` on the row's subgrid),
|
|
1048
|
+
so every cell meets its heading's edges. Before, a row's own 8px gap set each cell a pixel past both sides of its
|
|
1049
|
+
heading.
|
|
1050
|
+
- Proved by `test-number-helpers.mts` (SubscriberRenewalCheck's money cases and more, and the ladder),
|
|
1051
|
+
`test-form-store.mts` (a computed input never in getValue, getChanges or problems, never a change; the presets),
|
|
1052
|
+
`test-input-conditions.mts` and `test:types`. Also by nexus.template's `/demo/form` "Worked out from other inputs"
|
|
1053
|
+
and `/demo/dropdown`'s Fee, with `form-figures.cjs` in both motion modes: 1,073.50 on open, every keystroke followed
|
|
1054
|
+
(900 / 0 / 13 gives 1,017.00), Reset, a held Load blank and disabled then 904.00, a cleared price blank, a 12-digit
|
|
1055
|
+
price's figure whole inside its box, the disabled look, and no netAmount sent; an `<output>` with no lock, stepper or Tab stop, as tall as Price with its edge in the
|
|
1056
|
+
three designs; the "From the page" pair blank, then 800.00 | 904.00; the money column at 1,00,000.00 and
|
|
1057
|
+
100,000.00, ending on its heading's edge; and in 7 themes, light and dark, at 1280x720 and 390x844, the figure at
|
|
1058
|
+
7.54:1 or more with nothing past its box or the window.
|
|
1059
|
+
|
|
1060
|
+
### A form asks before it saves, the full details in sections
|
|
1061
|
+
|
|
1062
|
+
Card C-form-confirm (2026-11-07), the owner's second ask of the day: *"the misc counter and payment counter when click
|
|
1063
|
+
save show popup all full details then user do contirm then save"*, as 2026-06-20's "Confirm Counter Payment" did. 7
|
|
1064
|
+
exports, all from `nexus-shared`, and three additions to existing options. Two looks change without a line of the
|
|
1065
|
+
app's: a record's details whose columns name a `group` now show a heading over each run (below), and a press outside a
|
|
1066
|
+
popup that stays open through it no longer takes the focus.
|
|
1067
|
+
|
|
1068
|
+
- **`SubmitFormProps.confirm`** (**`FormConfirmParams`**: `when`, `title`, `message`, `details`, `confirmLabel`,
|
|
1069
|
+
`cancelLabel`, `size`), so `ApiForm`'s too, and **`CrudFormOptions.confirm`** for a CRUD's Add and Edit: a question
|
|
1070
|
+
after every value has passed its checks and before the save starts. It is always a `question`, so its confirm button
|
|
1071
|
+
has the focus and Enter saves. While it is open nothing else submits the form (Save, Enter, Ctrl+S, `form.submit()`),
|
|
1072
|
+
Ctrl+S does not open the browser's Save page, and the form says no "Saving…". **Back to the form** (the default
|
|
1073
|
+
`cancelLabel`), Esc and × send nothing and leave every value as it was, the focus back on what opened it; a click
|
|
1074
|
+
outside leaves it open. **Confirm and save** (the default `confirmLabel`) goes on to the save as before. A rule here
|
|
1075
|
+
that throws shows its message under the form and asks nothing. `when` limits it to some saves (the kinds that take
|
|
1076
|
+
money). The question goes if the form leaves while it asks.
|
|
1077
|
+
```tsx
|
|
1078
|
+
<ApiForm api={COUNTER_CREATE} transform={saveBody} confirm={{
|
|
1079
|
+
when: values => ASKS_BEFORE_SAVING.includes(values.counterType),
|
|
1080
|
+
title: "Save this payment?",
|
|
1081
|
+
message: summary,
|
|
1082
|
+
details: () => ({ row: line, columns: counterDetailColumns(line, "confirm"), currency }),
|
|
1083
|
+
confirmLabel: "Confirm payment",
|
|
1084
|
+
size: "xl",
|
|
1085
|
+
}} />
|
|
1086
|
+
```
|
|
1087
|
+
- **What it shows is what is sent.** Each text and the details are handed the values and **`body`**, what will be sent:
|
|
1088
|
+
an `ApiForm`'s `transform` runs once, before the question (a throw there shows under the form and asks nothing), and
|
|
1089
|
+
that very body is sent after Confirm, never worked out again; a plain `SubmitForm`'s body is its values. Without
|
|
1090
|
+
`confirm`, `ApiForm` sends as before.
|
|
1091
|
+
- **`details`** answers **`FormConfirmDetails`** (`row`, `columns`, `currency`; the row and columns may be the page's own
|
|
1092
|
+
types) and is drawn by `CrudDetails`, in the normal text colour, under the muted line. The box is `lg` with details
|
|
1093
|
+
(two fields to a line) and `sm` without, unless `size` says otherwise (`xl`: three to a line). Its body scrolls while
|
|
1094
|
+
the buttons stay in view.
|
|
1095
|
+
- **`CrudDetails` draws `column.group`**: each run of neighbouring columns that name one group is a section under an
|
|
1096
|
+
`h3` heading (`nx-crud-details__section`, `nx-crud-details__heading`, the small muted heading style of a dropdown's
|
|
1097
|
+
groups), the heading nearer its fields than the section above it. Columns with no group draw as before, without a
|
|
1098
|
+
heading; with no group anywhere the markup is exactly as before. So every details view gets it: the View popup, a
|
|
1099
|
+
workspace's Details tab, a counter's preview, the question. **`crudDetailsRuns(columns)`** (**`CrudDetailsRun`**) is
|
|
1100
|
+
the rule; a group named again further on starts a new run, as a table's band does.
|
|
1101
|
+
- **`formConfirmAsks(confirm, values)`** and **`formConfirmQuestion(confirm, values, body)`**
|
|
1102
|
+
(**`FormConfirmQuestion`**) are the question's pure rules: whether to ask, each text settled, the defaults.
|
|
1103
|
+
- **The message box** (`messageBox.confirm`, `show`, `alert`, `prompt`) takes **`details`**: drawn after the message in
|
|
1104
|
+
its own `.nx-message__details`, in the normal text colour (the message stays muted), scrolling with the body. Also
|
|
1105
|
+
**`closeOnBackdrop`** (default: as `closable`; `false` keeps a box open through a click outside while Esc and × still
|
|
1106
|
+
dismiss it) and **`signal`** (an abort dismisses the box). A box without them draws exactly as before.
|
|
1107
|
+
- **`Popup`**: a press on the backdrop of a popup that stays open through it (`closeOnBackdrop: false`: the question, a
|
|
1108
|
+
CRUD's Add and Edit, a row's form, a spreadsheet import) leaves the focus where it was. It used to move to the
|
|
1109
|
+
dialog, so after a stray click Enter no longer confirmed and a field lost its caret.
|
|
1110
|
+
- Styles: `Nexus.Crud.css` (`nx-crud-details__section`, `nx-crud-details__heading`), `Nexus.Popup.css`
|
|
1111
|
+
(`nx-message__details`).
|
|
1112
|
+
- Proved by `test-form-confirm.mts` (the runs, `when`, the texts and the defaults) and `test:types` (`form-confirm.tsx`:
|
|
1113
|
+
a page's own row type in the details, the mistakes refused). Also by nexus.template's `/demo/form` "Confirm before
|
|
1114
|
+
saving" (an order: "Place this order?", the body in Order and Money sections, "Place order", `xl`, a transform that
|
|
1115
|
+
can throw, a customer the demo server refuses), with `form-confirm.cjs` in both motion modes: Place order, Enter in a box
|
|
1116
|
+
and Ctrl+S in a box each open one question with no call and no "Saving…"; a click where the form's button is, the
|
|
1117
|
+
form's own submit, Ctrl+S (its default held) and a backdrop click while it asks change nothing and leave the focus on
|
|
1118
|
+
Place order; Enter sends one call whose body is the figures shown; Esc, × and Back send nothing, keep every value and
|
|
1119
|
+
give the focus back to the button or the box that opened it; a throw and a refusal show under the form and the next
|
|
1120
|
+
save asks again; and in 7 themes, light and dark, at 1280x720 and 390x844, the details at 8.3:1 or more in the text
|
|
1121
|
+
colour, the headings at 4.85:1 or more, three fields to a line at `xl` and one on a phone, nothing past the window,
|
|
1122
|
+
and in windows too short for them the body scrolling with Place order still in view. `/demo/crud`'s guest workspace
|
|
1123
|
+
shows its Details in Contact and Account sections (`crud-workspace.cjs`, in every theme and both sizes).
|
|
1124
|
+
|
|
1125
|
+
### A few fields in a record's profile, left out of its Details tab
|
|
1126
|
+
|
|
1127
|
+
Card C-profile-fields (2026-11-07), the owner's third ask of the day: *"in subscriber profile when open then show the
|
|
1128
|
+
expirations status like the how much will run when disconnect the status which displaying in the datatable"* - a
|
|
1129
|
+
subscriber's Expiry and Inactive under the status tags of its window. Additions only: 2 exports, both from
|
|
1130
|
+
`nexus-shared`, and one addition to an existing type. A profile without `fields` draws exactly as before.
|
|
1131
|
+
|
|
1132
|
+
- **`CrudDetailsProfile.fields`** (the type takes the row now, `CrudDetailsProfile<TRow>`, defaulting as before): a
|
|
1133
|
+
few of the record's fields under the profile's tags, each a label and a value, the value drawn as the table and the
|
|
1134
|
+
details draw it (`TableCellValue`): its format in the person's settings (a date in their date format, read again when
|
|
1135
|
+
they change it), an option's tone, the column's `tag` after it. A field is a column given whole or a key of the
|
|
1136
|
+
details' `columns` (the table's, unless the details name their own); a key no column has is left out, and so is a
|
|
1137
|
+
field with no value for the record (null, undefined, blank text, an empty list, NaN; `false` and `0` are values).
|
|
1138
|
+
```ts
|
|
1139
|
+
profile: row => ({ title: row.fullName, tags: [...], fields: [EXPIRY_COLUMN, INACTIVE_COLUMN] })
|
|
1140
|
+
```
|
|
1141
|
+
- **The built-in Details tab leaves out every key `fields` names**, an empty one too, so a record's window never shows
|
|
1142
|
+
a field twice and a page filters nothing by hand. Sections (`group`) stay as they were for the columns left.
|
|
1143
|
+
- **The look**: a `<dl class="nx-crud-profile__fields">`, a `div.nx-crud-profile__field` a pair (`dt.nx-crud-profile__label`,
|
|
1144
|
+
`dd.nx-crud-profile__value`). Beside a wide popup's tabs it is a small grid in the middle of the 20rem column, a pair a
|
|
1145
|
+
line on the grid's two tracks, labels in `--nx-text-muted` and values in `--nx-text` at `--nx-text-sm`; a long value
|
|
1146
|
+
wraps inside the column and a tag that no longer fits goes under its value, at its start. Under 720px of popup width
|
|
1147
|
+
(the one-row profile) the pairs follow the tags as one wrapped line - beside them when they all fit, else on the next
|
|
1148
|
+
line - each pair wrapping as words do with its label kept with the start of its value.
|
|
1149
|
+
- **`crudProfileFields(fields, columns, row)`** (the fields a profile shows for a record) and
|
|
1150
|
+
**`crudDetailsWithoutProfile(columns, fields)`** (the details' columns less the keys named; the same list when none
|
|
1151
|
+
are) are the rules. `isCrudProfile` takes the row type too.
|
|
1152
|
+
- Styles: `Nexus.Crud.css`, beside the profile's.
|
|
1153
|
+
- Proved by `test-crud-profile-fields.mts` (37 checks: a column or a key, an unknown key, a key named twice, every kind of
|
|
1154
|
+
empty value, a column's own `value` and a path, the keys taken out of the Details and the groups kept) and
|
|
1155
|
+
`test:types` (`crud-profile.tsx`: a page's own row type, a column written in place with its tag typed from the row,
|
|
1156
|
+
a column of another row's type refused). Also by nexus.template's `/demo/crud` "A guest's workspace" (the guest's next
|
|
1157
|
+
stay, Arrives with how soon as a tag and Leaves, in the profile and not in the Details; Book a stay moves them), with
|
|
1158
|
+
`crud-workspace.cjs` section L: the `<dl>` after the tags, the dates in the default format and again as DD/MM/YYYY
|
|
1159
|
+
after a live change of the preference, no Arrives or Leaves in the Details, a long date (`dddd, MMMM DD, YYYY`) inside
|
|
1160
|
+
the 20rem column with its tag under it, one wrapped line at 700px and 390px with each label beside its value, a stay
|
|
1161
|
+
booked followed with one Update and one Detail, and a guest with no stay with no fields; then in 7 themes, light and
|
|
1162
|
+
dark, at 1280x720 and 390x844, the labels muted, the values and the tag at AA, the tag's dot in `--nx-info`, the
|
|
1163
|
+
date in two formats, nothing past the profile, its column or the window, and no Arrives or Leaves in the Details.
|
|
1164
|
+
|
|
1165
|
+
### An input's width that follows a value
|
|
1166
|
+
|
|
1167
|
+
Card C-span-when (2026-11-07), the owner's fourth ask of the day: *"if possible and easy: in the cash counter make:
|
|
1168
|
+
Bill date as col-12 when it put value and during the visible input time make it col-6."* - read as: Bill date takes
|
|
1169
|
+
the whole row while it is empty and half once a date is put, with the switch that counts from it beside it, so Bill
|
|
1170
|
+
number and what follows never move. Additions only: 2 exports, both from `nexus-shared`. An input without `spanWhen`
|
|
1171
|
+
draws exactly as before.
|
|
1172
|
+
|
|
1173
|
+
- **`InputLayoutParams.spanWhen`** (`InputSpanWhen`: `when`, `span`; Form only): the input spans `span` columns while
|
|
1174
|
+
every condition of `when` holds - `showWhen`'s conditions, one or a list, each on an input's value as it is now; the
|
|
1175
|
+
input's own name follows its own typing - else its own `span` (a value or a getter).
|
|
1176
|
+
```ts
|
|
1177
|
+
{ type: EInputTypes.Date, name: "billDate", label: "Bill date", span: 6, spanWhen: { when: { name: "billDate", filled: false }, span: 12 } }
|
|
1178
|
+
```
|
|
1179
|
+
- Built on the `showWhen` machinery: `withInputConditions` gives such an input a live `span` getter and joins the names
|
|
1180
|
+
its conditions read into `dependsOn`, so its cell takes the new width on the key that makes or unmakes the value,
|
|
1181
|
+
without mounting the input again: the text, the caret, the focus and an open calendar stay. Form's grid and
|
|
1182
|
+
`Nexus.Form.css` are unchanged (a cell reads `span` each time it draws), so a narrow form (under 34rem) still gives
|
|
1183
|
+
every input the whole row. **`spanFor(spanWhen, read, own)`** is the pure rule.
|
|
1184
|
+
- The `dependsOn` docs now name `span` among the getters a followed change reads afresh: a `get span()` with
|
|
1185
|
+
`dependsOn` already worked (nexus.builder.web's class shape uses one); `spanWhen` says it without a getter.
|
|
1186
|
+
- Like `showWhen`, it is Form's only: a tabular or group form's rows ignore it.
|
|
1187
|
+
- Proved by `test-input-conditions.mts` (21 new checks: the rule both ways, a list, an empty list, the live span both
|
|
1188
|
+
ways, a condition on the input's own name, its own `span` getter kept and giving way, with `showWhen` on the same
|
|
1189
|
+
input, `dependsOn` joined once each, the page's params unchanged; in a form store, the cell drawn again on its own
|
|
1190
|
+
typing with the same stamp, and a load) and `test:types` (`input-types.ts`: one condition or a list; refused without
|
|
1191
|
+
`span`, without `when`, a condition with no name, a span as text). Also by nexus.template's `/demo/form` "Inputs
|
|
1192
|
+
shown by another input", whose form now ends with a Bill section - Bill date, the switch "Count the dates from the bill
|
|
1193
|
+
date" (shown while the date is filled) and Bill number - with `label-when.cjs` section S in both motion modes: on every
|
|
1194
|
+
key the width follows the value and the focus and caret stay at the end of the same box, the calendar the click opened
|
|
1195
|
+
stays open at the box's start, the switch's top is the date's with its knob level with the date's box, Bill number
|
|
1196
|
+
never moves (typing, a key that unmakes the date, ×, a calendar pick), Tab goes from the date to the switch and then
|
|
1197
|
+
Bill number; every density and input design; then 7 themes, light and dark, at 1280x720 and, where every cell is the
|
|
1198
|
+
whole row, 390x844. Seen red with the rule broken (42 lines) and with a remount on a width change (33 lines).
|
|
1199
|
+
|
|
1200
|
+
### Record cards that fit what was built on them
|
|
1201
|
+
|
|
1202
|
+
Card C-record-cards-fit (2026-11-07): the nine gaps the two Accounts agents met while building nexus.accounts'
|
|
1203
|
+
dashboard on `RecordCards`, and the seven findings of the round-2 review in this package. Additions only: 6 exports, all
|
|
1204
|
+
from `nexus-shared` (`keepsTyping`, `recordActionName`, `recordBusyKeys`, `recordHeldLines`, `RECORD_STILL_READ`, the
|
|
1205
|
+
type `RecordGoToOptions`), and new optional params. **Three looks change with no app change**: a card's heading buttons
|
|
1206
|
+
go under its title where the card is narrow, a window popup's description wraps under its title on a phone, and a
|
|
1207
|
+
picture's hidden buttons take no press. **One behaviour changes**: a password or a one-time code typed is no longer
|
|
1208
|
+
asked about before it is dropped (below).
|
|
1209
|
+
|
|
1210
|
+
- **A step's form** (`RecordStepContext.form`): every step is handed its `FormStore`, and `inputs` as a function gets
|
|
1211
|
+
it, so an input's label, `hidden` or options can follow another input as typed (getters with `dependsOn`) - the
|
|
1212
|
+
region levels of an address, named after the place picked above. nexus.accounts' address card can now be a flow.
|
|
1213
|
+
```tsx
|
|
1214
|
+
add: { title: "Add an address", inputs: ({ form }) => addressInputs(form), submit: ACCOUNT_API.addAddress }
|
|
1215
|
+
```
|
|
1216
|
+
- **A step's own rule**, `RecordStepParams.validate(values, context)`, the form's `validate` (messages by input, or one
|
|
1217
|
+
for the step), asked before anything is sent: a new password and its confirmation that differ.
|
|
1218
|
+
- **A step shown fresh**: `goTo(step, { fresh: true })`, and `RecordStepParams.fresh` for a step shown fresh each time
|
|
1219
|
+
it is reached again (by `next` or `goTo`): its form is dropped and it opens on its values, so an old code or
|
|
1220
|
+
password is gone.
|
|
1221
|
+
- **Nothing worth keeping** (`FormStore.isWorthKeeping()`, `keepsTyping(params)`): a value typed in a `Password` or
|
|
1222
|
+
`Code` input is a change (Save and the status line say so) but never asked about - not by Cancel or a popup's close
|
|
1223
|
+
(`confirmDiscard`), not by a link inside the app, not by `beforeunload` or `confirmPageLeave`. Any other change is
|
|
1224
|
+
asked about as before. A record step with `confirmCancel: false` also leaves the page's guards.
|
|
1225
|
+
- **A heading's buttons while the data is read**: a card's (and the hero's) action `hidden` and `held`, typed on the
|
|
1226
|
+
data, are asked only once it is read; until then the button is held, saying "Still being read." (the hero's were
|
|
1227
|
+
hidden), or not drawn yet with the new `loading: "hidden"`. `recordActionState(action, null, ...)` is that rule.
|
|
1228
|
+
- **Heading buttons named with their card** to a screen reader ("Edit: Personal details"), as an item's icons are with
|
|
1229
|
+
their item (`recordActionName`); the visible label is unchanged and starts the name.
|
|
1230
|
+
- **Nothing moves when the records come**: a held list row is a read row's height (its name's line and lines under it
|
|
1231
|
+
as line boxes; a row's title is at least a mark's height, with marks or without), a content card's two held lines
|
|
1232
|
+
are two lines of text, a count in a title no longer makes its line a pixel taller, and the new
|
|
1233
|
+
`RecordFieldParams.heldLines` and `RecordItemsCardParams.heldRowLines` hold as many lines as a value or a row
|
|
1234
|
+
usually takes (`heldRows` already held as many rows). Text that wraps by the width can still grow its card.
|
|
1235
|
+
- **A narrow card's buttons under its title**: the heading's text keeps 12rem (8rem for a title alone) beside its
|
|
1236
|
+
buttons, else the buttons go on a line under it, at its start - no title squeezed to a word a line on a phone.
|
|
1237
|
+
- **A window popup's description on a phone** (600px and under) takes the lines under the title it needs, in full; the
|
|
1238
|
+
icon and the window buttons keep the bar's first line. Wider, it is the one low bar as before.
|
|
1239
|
+
- Review fixes: a page's popups close with it and are keyed by the page that opened them, so a page drawn afresh never
|
|
1240
|
+
brings back a popup bound to the old data (whose Save sent the stale record); an Edit with a Tabular or Group input
|
|
1241
|
+
opens on its rows (`recordOpenedValues`), so its Save no longer wipes them; a card keeps a list of its calls on their
|
|
1242
|
+
way (`recordBusyKeys`), so one that ends first never frees a row whose own call still runs; the once-only scroll to
|
|
1243
|
+
a linked card (`#emails`) is skipped once the person has scrolled, touched, pressed a key or 5 seconds have gone, or
|
|
1244
|
+
when the card is already in view; a picture's hidden buttons take no press (`pointer-events: none` until shown) and
|
|
1245
|
+
are always shown where any pointer is coarse (a laptop's touch screen); a list that could not be read in the pictures
|
|
1246
|
+
window has the window's padding; the document list's Upload in a full one-file category is held with its reason
|
|
1247
|
+
("This keeps one file: delete it first.").
|
|
1248
|
+
- Proved by `test-record-cards.mts` (151 checks: rows opened, the rules not asked while read, `loading`, busy keys,
|
|
1249
|
+
held lines), `test-form-store.mts` (worth keeping), `test:types` (`record-cards.tsx`), and nexus.template's
|
|
1250
|
+
`/admin/profile`, whose demo now has an address's lines named after what it is, Change password with its
|
|
1251
|
+
confirmation, Send a reset link, Back on the code step, and held rows as many as the lists: `record-cards.cjs` section
|
|
1252
|
+
K and new lines in A (every card's place and height through the load at 1280, every heading button's name), and
|
|
1253
|
+
`pictures.cjs` K7. Seen red with the busy list, the popups' closing, and the row's height undone.
|
|
1254
|
+
|
|
1255
|
+
### Also in 4.0.0: the changes prepared as 3.3.0
|
|
1256
|
+
|
|
1257
|
+
Prepared on 2026-10-22 with asks 1-6 of the template's auth follow-ups and everything unreleased since `3.2.0`, with the
|
|
1258
|
+
solid fills' hover added on 2026-10-26, and never published. On their own they are new features with no code break;
|
|
1259
|
+
the sections below say what each added. What can move for an app:
|
|
1260
|
+
|
|
1261
|
+
- **A list's Add button reads "Add New"** on every `Crud`, whatever its subject; the form it opens keeps its subject.
|
|
1262
|
+
- **Menus** fade a switched-off item's icon and label, not its reason, and wrap a long second line.
|
|
1263
|
+
- **A table's spreadsheet menu** offers only what the app's permissions allow; the template and the export stop at a
|
|
1264
|
+
failed settings call, with that call's one toast.
|
|
1265
|
+
- **A Nexus save is not read back**, except a new record with a `crudColumns.dbJoin` column given no `from`.
|
|
1266
|
+
- **An Edit saved while the CRUD shows another list** goes where its band says, no longer into that list.
|
|
1267
|
+
- **A `crudPicker` with a `valueKey` other than `id` and no `find`** no longer posts its values to `findListing`.
|
|
1268
|
+
- **A solid fill under the pointer is darker than at rest in every theme**, light and dark (it turned lighter in a dark
|
|
1269
|
+
theme): a primary or danger `Button`, and so `SplitButton`, `FormButtons` and every solid button built on them, the
|
|
1270
|
+
chat's unread button and Send, and the popup's close button while it is pressed. **An app's own theme that puts a
|
|
1271
|
+
dark label on a light solid fill** loses contrast under the pointer now: give its fills a light label at 4.6:1 or
|
|
1272
|
+
more, as the README's example does.
|
|
1273
|
+
- **Nord's dark solid fills are a little lighter and carry white**: `--nx-primary-solid` `#557b86` (was `#4b717c`),
|
|
1274
|
+
`--nx-danger-solid` `#99686d` (was `#8e5e63`), and `--nx-primary-text` and `--nx-danger-text` `#ffffff` (were snow,
|
|
1275
|
+
`#eceff4`), on every button, check, selected day and tab indicator in Nord's dark mode.
|
|
1276
|
+
|
|
1277
|
+
### A solid fill under the pointer keeps its label readable
|
|
1278
|
+
|
|
1279
|
+
2026-10-26, the owner's decision of 2026-10-22: "fix it everywhere". `--nx-primary-hover` and `--nx-danger-hover` mixed
|
|
1280
|
+
the fill 16% toward `--nx-text`. That darkened it in light mode, but lightened it in dark mode, where the fill is already
|
|
1281
|
+
as light as its white label allows, so the label under the pointer fell from 4.6:1 to 3.48-3.69:1 in all seven dark
|
|
1282
|
+
themes. Both are now the fill with **12% black** mixed in (OKLab), in every theme and in both modes:
|
|
1283
|
+
|
|
1284
|
+
- the label under the pointer, and pressed, measures **6.06:1 or more** in a dark theme and **7.09:1 or more** in a light
|
|
1285
|
+
one (it was 5.94:1 or more there); at rest and with the keyboard's focus nothing changed (4.60:1 or more);
|
|
1286
|
+
- the step from rest is the same size in every theme, an OKLab difference of **5.8 to 7.4** (x100). Light mode's was
|
|
1287
|
+
1.9 to 6.0, so a light theme's hover is a little darker than it was (Rosé Pine's, Nord's and Solarized's the most),
|
|
1288
|
+
and now seen in every theme.
|
|
1289
|
+
|
|
1290
|
+
Measured in a browser under a real mouse, the colours composited through a canvas, on every solid fill the packages
|
|
1291
|
+
hover: `Button` primary and danger, both halves of a `SplitButton`, `FormButtons`' danger submit, the chat's unread
|
|
1292
|
+
button and Send, and the popup's close button. The close button still turns `--nx-danger-solid` under the pointer;
|
|
1293
|
+
pressed, it takes `--nx-danger-hover`, so it now turns darker instead of lighter. Its minimise, maximise and restore
|
|
1294
|
+
buttons did not change. `npm run check:themes` reads the two lines from `Nexus.Tokens.css` and checks both: the label at
|
|
1295
|
+
4.5:1 or more, and a step of 5 or more. No export or constant changed. An app that used either token for something
|
|
1296
|
+
other than a solid fill's hover gets the darker colour in dark mode too.
|
|
1297
|
+
|
|
1298
|
+
**The fill under the pointer keeps 2:1 on the panel**, as it does at rest. In a dark theme the darker step moves the
|
|
1299
|
+
fill toward the panel. In Nord's dark mode it reached 1.79:1 on `#2e3440`, under the 2:1 every solid fill keeps at rest:
|
|
1300
|
+
Nord's panel is lighter than other themes' and its label was snow (`#eceff4`), which left the fill no room to darken.
|
|
1301
|
+
Nord's dark fills now carry white, as five of the other six themes' do, and were set the way every dark fill was: the
|
|
1302
|
+
same hue and chroma, at the lightness where white reaches 4.6:1. They stand 2.71:1 off the panel at rest (were 2.35)
|
|
1303
|
+
and 2.04:1 under the pointer, and the label reads 4.60:1 at rest and 6.11:1 under the pointer. The other dark themes'
|
|
1304
|
+
fills keep 2.30:1 or more under the pointer. `check:themes` now holds every hovered fill to 2:1 on `--nx-surface`. A
|
|
1305
|
+
solid fill's label is light in every theme and both modes, because hover and pressed darken the fill: a dark label on
|
|
1306
|
+
a light fill reads at rest and loses contrast under the pointer.
|
|
1307
|
+
|
|
1308
|
+
### A QR code, `QrCodeImage`
|
|
1309
|
+
|
|
1310
|
+
2026-10-22, ask 4 of the template's two-step pages (`nexus.accounts`, whose authenticator setup drew its own). Added
|
|
1311
|
+
only.
|
|
1312
|
+
|
|
1313
|
+
- **`QrCodeImage`** (`nexus-shared`; a server component can render it: nothing in it needs the browser): `value`,
|
|
1314
|
+
`label` for screen readers, `level` (L, M, Q, H; M by default), `className` and `style` for where the page puts it.
|
|
1315
|
+
One SVG path of black modules on a white square that holds the quiet zone, black on white in every theme and in
|
|
1316
|
+
forced colours. New stylesheet `Nexus.QrCode.css`, class `nx-qr`: 9rem wide, a control's rounded corners - the look
|
|
1317
|
+
of nexus.accounts' `.tp-auth__qr`, whose grid area stays the page's.
|
|
1318
|
+
- **`encodeQr(text, level)`** and **`qrPath(code, border)`** (`QrCode`, `QrLevel`): QR Code Model 2 written by hand,
|
|
1319
|
+
byte mode (UTF-8), versions 1 to 40, levels L, M, Q and H, Reed-Solomon, the eight masks scored by the four penalty
|
|
1320
|
+
rules. A text past version 40 throws, with how much the level holds. Plain erasable TypeScript, so Node runs it as it
|
|
1321
|
+
is: `Project-Skills/Checks/qr-code-check.mjs` reads every code back with jsQR 1.4.0 (410 of 410 against this file),
|
|
1322
|
+
and `npm run test:qr` checks its shape without a decoder.
|
|
1323
|
+
|
|
1324
|
+
### A list beside the record picked in another, and a picker's own find
|
|
1325
|
+
|
|
1326
|
+
2026-10-22, asks 5 and 6 of nexus.web's company users page. The owner picked "the places of the person picked beside
|
|
1327
|
+
the people", and decided that a person is given a place only once they have joined. Added only: no export removed and no
|
|
1328
|
+
exported constant changed. A table or CRUD that passes none of the new props is unchanged, but for the two fixes at the
|
|
1329
|
+
end, which any app can see.
|
|
1330
|
+
|
|
1331
|
+
- **A picked row, `DataTable` `onPick`** (and `Crud` `onPick`): rows the user picks one of, for a page that shows the
|
|
1332
|
+
picked one's records beside the table.
|
|
1333
|
+
- A click on a row picks it; a double-click still opens it, and `openOnClick` gives way. The keyboard reaches the rows
|
|
1334
|
+
through a radio button each, out of sight in the first column and named with the row: Tab to the picked one, and the
|
|
1335
|
+
arrow keys move the pick.
|
|
1336
|
+
- The picked row is tinted as a selected one is, a ring round it shows the radio's focus, and the rows are numbered
|
|
1337
|
+
unless `numbered` is false.
|
|
1338
|
+
- The pick lives in the store (**`TableState.picked`**, **`TableStore.pick`**, **`TableStoreOptions.onPickChange`**)
|
|
1339
|
+
and follows the record: a search, a sort or another page keep it, an upsert hands the page its new copy, and it is
|
|
1340
|
+
let go when the record is removed, is gone after a load, or another list starts over.
|
|
1341
|
+
- New attributes `data-pick` and `data-picked` on a row, and the class `nx-table__pick`.
|
|
1342
|
+
- **A list narrowed to it, `Crud` `narrow`** (**`CrudNarrow`**: `picked`, `key`, `valueKey`, `labelKey`, `addLock`,
|
|
1343
|
+
`emptyMessage`), fed from the other list's `onPick`.
|
|
1344
|
+
- Only the picked record's records show, under a caption naming it ("Places of: Ram Thapa"). The table's name for
|
|
1345
|
+
screen readers says it too.
|
|
1346
|
+
- A list paged in the browser keeps the whole list and shows the picked record's rows from it, so another pick costs
|
|
1347
|
+
no request. A server's pages carry the value as a filter named `key`, and are kept per picked record.
|
|
1348
|
+
- Add fixes the value in the new record, read-only with the lock and a note, in a form of its own per picked record
|
|
1349
|
+
("For Ram Thapa" in its title bar). Its box is named from the picked record, with no lookup.
|
|
1350
|
+
- Add waits, saying why on a press, while nothing is picked or the picked record may take none: `addLock`, by default
|
|
1351
|
+
the backend's rule (`CrudBackend.isPickable`), so a Nexus record pending (30-39), trashed or deleted takes none.
|
|
1352
|
+
- A save lands in the list of the record it names and in no other, whichever record is picked when it is saved.
|
|
1353
|
+
Joined names come from the picked record with no call (a `crudColumns.dbJoin` given `from`).
|
|
1354
|
+
- New helpers **`crudNarrowing`** and **`crudInNarrowing`** (**`CrudNarrowing`**).
|
|
1355
|
+
- **`DataTable` `caption`**: a line inside the table's frame, over its bar, saying what its rows are. Beside another
|
|
1356
|
+
table, the two frames still start level. New class `nx-table__caption`.
|
|
1357
|
+
- **`DataTable` `add`** takes `disabled` and `description`, as `TreeView`'s own button does. The button stays in the Tab
|
|
1358
|
+
order, `aria-disabled`, and a press shows the description in a note under it; the description is its tooltip too. While
|
|
1359
|
+
it waits, the spreadsheet's items stay in the options menu, without Bulk insert.
|
|
1360
|
+
- **`crudPicker` `find`**: where a value the kept list does not offer is looked up when `valueKey` is not `id` - an
|
|
1361
|
+
endpoint POSTed such values, as `findListing` is the keys, whose answers are read by `valueKey` (a person's `userId`,
|
|
1362
|
+
which their company row carries beside its own key); or a source of the page's own. Its answers are kept apart from
|
|
1363
|
+
`findListing`'s, and `expireLookups` marks them old with the controller's own. **`joinSource(source, backend, key)`**
|
|
1364
|
+
takes the field the answers are filed under.
|
|
1365
|
+
- **`lockInputs(inputs, locked, values, known)`** and **`CrudFormRequest.known`**: records the caller holds for the values
|
|
1366
|
+
it fixes. A fixed dropdown whose value one of them holds gets it as a selected option.
|
|
1367
|
+
- A save names its joined columns from a dropdown's `selectedOptions` too, after what the user picked.
|
|
1368
|
+
|
|
1369
|
+
What can move for an app on 3.2.0:
|
|
1370
|
+
|
|
1371
|
+
- **An Edit saved while the CRUD shows another list** - its form minimized, the trash shown meanwhile - no longer goes into
|
|
1372
|
+
that list's table, which then filed it in the cache under that list's name. It goes where its band says, and out of
|
|
1373
|
+
the table when an edit took it out of the list on screen.
|
|
1374
|
+
- **A `crudPicker` whose `valueKey` is not `id`, with no `find`**, no longer posts its values to `findListing`, which
|
|
1375
|
+
takes the records' keys and could answer none of them. The form names such a value from the record it holds
|
|
1376
|
+
(`selectedOptions`), or shows it as not found, without the call.
|
|
1377
|
+
|
|
1378
|
+
### A step that removes something
|
|
1379
|
+
|
|
1380
|
+
2026-10-22, asks 1-3 of the template's two-step pages (`nexus.accounts`, `/account/security`): a step that removes an
|
|
1381
|
+
authenticator app, turns two-step off or makes new codes confirms with a password, and carried two stand-ins of its own.
|
|
1382
|
+
Added only: no export removed, no exported constant changed, and a form that passes none of the new props is unchanged.
|
|
1383
|
+
|
|
1384
|
+
- **`FormButtons` `submitTone="danger"`** (and so `SubmitForm`, `ApiForm`): the submit button filled with
|
|
1385
|
+
`--nx-danger-solid` and `--nx-danger-text`, the `Button` `danger` variant every solid danger fill uses, for a save
|
|
1386
|
+
that removes something. Default `primary`, as before. New type **`FormSubmitTone`**.
|
|
1387
|
+
- **`downloadText(name, text, type)`** (`nexus-shared/client`): the file a table's CSV export hands over, for any text
|
|
1388
|
+
made in the browser - a Blob behind a link that is clicked and taken out, its address let go ten seconds later, the
|
|
1389
|
+
text after a byte order mark so Excel and Windows editors read UTF-8.
|
|
1390
|
+
- **`SubmitForm` `aria-describedby`** (and so `ApiForm`): passed to the `<form>` beside `aria-label` and
|
|
1391
|
+
`aria-labelledby`, so a step that puts the focus straight into its password box can point at the line saying what
|
|
1392
|
+
goes.
|
|
1393
|
+
|
|
1394
|
+
### A record nobody may change here, and a switched-off item's reason readable
|
|
1395
|
+
|
|
1396
|
+
2026-10-12, for the Builder's locked system models (the auth tables a person may only read). Added only: no export
|
|
1397
|
+
removed, no exported constant changed, and a `Crud` without the new prop behaves as before.
|
|
1398
|
+
|
|
1399
|
+
- **`Crud` `rowLock`** (`(row) => string | null | undefined`): why a record may not be changed here. Its Edit, Flag,
|
|
1400
|
+
Move to trash, Recover, Delete permanently and Move back to trash are listed **switched off** with that sentence under
|
|
1401
|
+
each (none on the row as an icon), its details offer no Edit, the handle's `edit(row)` opens its details instead, and
|
|
1402
|
+
a selection holding it has the mass actions that change records switched off. View, History, Pin and Favourite stay.
|
|
1403
|
+
It fills the gap pages worked around with row actions of their own ("`Crud` cannot switch a built-in action off for
|
|
1404
|
+
one row"); a page's own `rowActions` are still the page's to switch off.
|
|
1405
|
+
- **Menus** (`Nexus.Menu.css`): a switched-off item fades its icon, check, label and shortcut to half, no longer the
|
|
1406
|
+
whole item, so its second line (why it is off) is read at full strength: at half it measured 2.0 to 3.1:1 in every
|
|
1407
|
+
theme. `.nx-menu` sets `white-space: normal`, so a row's menu inside a table's actions cell (which does not wrap)
|
|
1408
|
+
wraps its items' second lines instead of cutting them. An app that undid either rule for its own menus can drop that.
|
|
1409
|
+
|
|
1410
|
+
### A table's spreadsheet menu asks the permissions
|
|
1411
|
+
|
|
1412
|
+
A review of 2026-10-10 found an Admin offered a model's whole spreadsheet menu on nexus.web, every item of it refused
|
|
1413
|
+
by the backend (the sheets are SuperAdmin's). No export or constant changed. What can move for an app on 3.2.0:
|
|
1414
|
+
|
|
1415
|
+
- **`DataTable` `excel`** (and so `Crud` `table={{ excel }}`): each item is offered only when the app's permissions
|
|
1416
|
+
(`canCall`) allow every endpoint it calls - Download template the settings and the template, Import from a
|
|
1417
|
+
spreadsheet and Bulk insert the settings, Verify and Import, Export to Excel the settings and the export - and the
|
|
1418
|
+
menu follows a change of rights. An app with no permissions registered allows everything, so it sees no change; one
|
|
1419
|
+
with `NexusPermissions` now needs its users' access actions to hold the `{Model}/Excel/...` actions for the menu to show.
|
|
1420
|
+
- **`downloadExcelTemplate` and `exportExcel`** stop at a failed settings call and answer it (`ok: false`, its status
|
|
1421
|
+
and message): the person sees that call's one toast. Before, the template went on to a second refused call and a
|
|
1422
|
+
second toast, and the export went on without knowing its row limit.
|
|
1423
|
+
|
|
1424
|
+
### The list's Add button says "Add New"
|
|
1425
|
+
|
|
1426
|
+
The owner, 2026-10-10: *"the CRUD has Add Region, Add Country and ... I want just fixed name Add New ... no dynamic"*.
|
|
1427
|
+
A `Crud`'s Add button above its list reads **Add New** on every page, whatever its `subject`; it was `Add <subject>`.
|
|
1428
|
+
The form it opens keeps its subject in the title and on its submit button ("Add region"), and Edit still says Save.
|
|
1429
|
+
No export or constant changed. An app's test that finds the list's button by its old name ("Add region") finds
|
|
1430
|
+
nothing now; one that finds the form's submit button is unaffected.
|
|
1431
|
+
|
|
1432
|
+
### A tree that fills
|
|
1433
|
+
|
|
1434
|
+
The owner, 2026-10-10, on nexus.web's administrative region tree: *"make full height here similar datatable both side
|
|
1435
|
+
panel, make sure responsive"*. Added only; a tree without the new prop is unchanged.
|
|
1436
|
+
|
|
1437
|
+
- **`TreeView` `fill`**: the tree takes the height it is given, as `DataTable` `fill` does - the toolbar on top, the
|
|
1438
|
+
rows scrolling inside under it - and `height` and `maxHeight` are ignored while it is on. New class
|
|
1439
|
+
`nx-tree-field--fill` on the field. Give it a cell with a height: a side of nexus-admin's `AdminSplit` on an
|
|
1440
|
+
`AdminPage` with `fill` (both new in nexus-admin), or a parent of your own.
|
|
1441
|
+
|
|
1442
|
+
### CRUD inside a tree, and changes written into a tree's kept pages
|
|
1443
|
+
|
|
1444
|
+
The owner, 2026-10-10, on the same page: *"allow CRUD ... inside the treeview"*. Added only: no export removed, no
|
|
1445
|
+
exported constant changed, and a page that passes none of the new params behaves as before - with one call fewer where
|
|
1446
|
+
a picker's saved value is in the list it keeps (see `crudPicker` below).
|
|
1447
|
+
|
|
1448
|
+
- **`CrudTree`** (`nexus-shared/client`): a controller's records as a lazy tree with Add New (under a row, or from the
|
|
1449
|
+
toolbar), Edit, Move to trash, Recover, Delete permanently, Move back to trash and History - through the **same**
|
|
1450
|
+
Add/Edit popup and calls `Crud` uses. Row icons for Add New, Edit and Move to trash (Insert, F2 and Delete on a
|
|
1451
|
+
focused row, all in its context menu); the picked record's fields and buttons go to a panel the page draws
|
|
1452
|
+
(`details`). Every change is written into the tree and the pages it keeps: a record added is placed where its
|
|
1453
|
+
siblings' order puts it, an edit is written in place or moved to its new parent, one in the trash leaves, and the
|
|
1454
|
+
parents' counts and chevrons follow. A Create still reads its record back once (as `Crud` does), and an edit that
|
|
1455
|
+
moved a record reads it once for the names joined from its new parent. New class `nx-crud-tree__actions`.
|
|
1456
|
+
- **`CrudTreePlacement`** (`cacheKey`, `rules`, `itemOf`, `parentOf`, `recordOf`, `childValues`): where a controller's
|
|
1457
|
+
records sit in a tree, given to `CrudTree` and to **`Crud` `trees`** - a data table on another page then writes its
|
|
1458
|
+
changes into that tree's kept pages too (an import drops them instead). `crudTreeChanges` turns a `CrudChange` into
|
|
1459
|
+
tree changes; **`CrudChange.before`** carries the records as they were.
|
|
1460
|
+
- **Changes into a tree's kept pages**: `writeTreeChanges(cacheKey, changes, rules)` patches `treePages`
|
|
1461
|
+
(`patchTreePages`) and every tree on screen with that `cacheKey` (`subscribeTreeChanges`); `TreeItemChange`
|
|
1462
|
+
(`put` / `remove`) and `TreeChangeRules` (`valueKey`, `hasChildrenKey`, `compare`). `TreeStore.placeItem` and
|
|
1463
|
+
`removeItem` keep a lazy parent's `loaded`/`total` and its own count right, so the next page skips the right number.
|
|
1464
|
+
- **`TreeView`**: `rowActions` (`TreeRowAction`: `TableRowAction` and a `shortcut` of Insert, F2 or Delete), `add` (a
|
|
1465
|
+
toolbar button of the page's own), and on the handle `apply(changes, rules)`, `getItem(value)`, and a `reveal` that
|
|
1466
|
+
waits for an item still loading. Focus follows a row that leaves or moves, released before the row leaves the page.
|
|
1467
|
+
- `RequestCache.entries(prefix)` and `set(key, value, at)` (a value written over a kept one keeps its age);
|
|
1468
|
+
`TableJoins.remember(source, records)` and `peekList(name)`.
|
|
1469
|
+
- **`crudPicker`'s `resolveOptions` names a saved value from the list it keeps** before asking `findListing`, so a form
|
|
1470
|
+
opened with a value the list offers (an Edit, an Add under a tree's country) makes no lookup. A value the list does not
|
|
1471
|
+
offer - a parent since trashed - is still looked up and shown tagged.
|
|
1472
|
+
- `Crud`'s form popup, its call for an action on one record and its action words now come from one internal module
|
|
1473
|
+
shared with `CrudTree` (`Components/Forms/CrudActions.tsx`): no change to what `Crud` shows or sends.
|
|
1474
|
+
|
|
1475
|
+
### A tree that fixes where a record sits
|
|
1476
|
+
|
|
1477
|
+
The owner, 2026-10-10, on the same tree: *"I have select the row to add, auto add inside this. Not allow to switch
|
|
1478
|
+
parent ... both should be disabled, not allow to change those values."* Added only: no export removed, no exported
|
|
1479
|
+
constant changed, and a tree whose placement names no `placeKeys` behaves as before - except that a `CrudTree` now
|
|
1480
|
+
selects the row it shows (below).
|
|
1481
|
+
|
|
1482
|
+
- **`CrudTreePlacement.placeKeys`**: the fields that say where a record sits (its parent, its country). Given, `CrudTree`
|
|
1483
|
+
fixes them in every form it opens - an Add inside a row takes them from `childValues` (a field it leaves out is fixed
|
|
1484
|
+
empty), an Edit keeps the record's own - and every Add goes inside a row: the row's +, Insert, the context menu, the
|
|
1485
|
+
panel's Add New, `handle.create(parent)`, and the toolbar's Add New, which now adds inside the selected row - the one
|
|
1486
|
+
the panel shows - and, while there is none (or the selected record is in the trash), says why on a press (a click, a
|
|
1487
|
+
tap, Enter); it used to open with nothing filled. A record then moves only where those fields are offered, such as a
|
|
1488
|
+
data table's form. Fixed is read-only, the design's "cannot change" look: the lock, and a note on any try ("Added
|
|
1489
|
+
inside Bagmati: pick another row in the tree to add elsewhere.", "Its place cannot be changed in the tree."); the
|
|
1490
|
+
values stay and are sent as before, and a form's own `onFieldChange` never rewrites them (**`lockedFieldChange`**).
|
|
1491
|
+
- **The selected row**: a click or Enter on a `CrudTree` row selects it - tinted, and staying so while focus, the arrow
|
|
1492
|
+
keys and the chevrons move on - and a record's row shows that record in the panel, while a row that is no record (a
|
|
1493
|
+
country above its regions) empties the panel and opens. So the toolbar's Add New and the panel's never disagree.
|
|
1494
|
+
- **`lockInputs(inputs, locked, values)`**: inputs fixed for one opening of a form (input name -> why), copied with their
|
|
1495
|
+
own property descriptors so a getter param keeps working; a required input with no value stays free. The Add and
|
|
1496
|
+
Edit popup `Crud` and `CrudTree` share (internal) takes them as `locked`. An Add with fixed inputs is a popup of its
|
|
1497
|
+
own per set of fixed values (**`crudFormPlace`**, added to the popup's and the draft's keys) and says where in its
|
|
1498
|
+
title bar ("Inside Bagmati"): an Add started inside one row never comes back inside another, and asking for an Add
|
|
1499
|
+
replaces every untouched Add of the controller, so only forms with work in them wait on the dock. A record keeps **one**
|
|
1500
|
+
Edit popup wherever it is asked for: with the same inputs fixed it comes back; with others (a tree's fixed form, a
|
|
1501
|
+
data table's free one) an untouched one is replaced, and one with changes comes back as it is, with a note saying
|
|
1502
|
+
why - an older form's save would undo what a newer one saved. Drafts stay one per set of fixed values, so a draft
|
|
1503
|
+
written before a record moved is not brought back over its new place.
|
|
1504
|
+
- **`TreeView`**: `selectedValue` - the row the page picked, marked `aria-selected` and tinted (new class
|
|
1505
|
+
`nx-tree__row--selected`); moving focus, a chevron, opening or closing a row leave it where it is. `add.disabled` with
|
|
1506
|
+
`add.description`: the toolbar button stays in the Tab order (`aria-disabled`) and a press shows the description in a
|
|
1507
|
+
note under it; the description is also its tooltip and what a screen reader hears after its name.
|
|
1508
|
+
- **`Button`** with `aria-disabled="true"` looks disabled, stays focusable, and draws a stronger ring through the dimming.
|
|
1509
|
+
- **The dock** shows a popup's `description` under its title (`DockedWindow.description`, new classes `nx-dock__text`
|
|
1510
|
+
and `nx-dock__description`), in the bar's tooltip and in its buttons' names: two Edit forms, or two Adds, read apart.
|
|
1511
|
+
- A **read-only** field's placeholder is drawn in muted text, at least 4.5:1 on the read-only box (it was under 3:1 in
|
|
1512
|
+
light themes), since it is all the field shows ("None: directly under the country").
|
|
1513
|
+
- The details panel's Add New says "Add New: inside {name}" in its tooltip (it said "under").
|
|
1514
|
+
|
|
1515
|
+
### A save is never read back
|
|
1516
|
+
|
|
1517
|
+
The owner, 2026-10-10: *"When I created a new item it calls a new API and gets details; this is not needed. We return the
|
|
1518
|
+
details during the creating time, so use this; the creationTime, recordStatus we will provide fake or just right now."*
|
|
1519
|
+
With the Nexus backend an Add is now one Create and an Edit one Update, on a data table and in a tree: the row shown is
|
|
1520
|
+
built from the answer, the form and what the page holds. This replaces the read backs described under "CRUD inside a
|
|
1521
|
+
tree" above (a Create read once, a move read once). Added only: no export removed, no exported constant changed. What
|
|
1522
|
+
can move for an app on 3.2.0:
|
|
1523
|
+
|
|
1524
|
+
- **A `crudColumns.dbJoin` column without `from`** - the form 3.2.0 documented - still has its new record read back once
|
|
1525
|
+
after an Add (`details`), as 3.2.0 read every Create with a payload answer back; it is now the only reason a Nexus save
|
|
1526
|
+
reads anything. Give the column `from` (the field of the joined record's key) and the read goes.
|
|
1527
|
+
- `TableColumn.dbJoin` is set by every `crudColumns.dbJoin` column, its `from` optional.
|
|
1528
|
+
|
|
1529
|
+
The changes:
|
|
1530
|
+
|
|
1531
|
+
- **`CrudBackend.savedRow(action, record, before)`**: the fields only the server keeps, written as the backend writes
|
|
1532
|
+
them. The default leaves the record as it is, and a CRUD still reads a record back once when its list cannot be told
|
|
1533
|
+
from it (another app's backend). **`NexusCrudBackend`**: a new record lacking them is Active (`NEXUS_ACTIVE_STATUS`),
|
|
1534
|
+
has no marks, was added now (`creationTime`, a UTC ISO string) and never changed; an edited one takes the status, the
|
|
1535
|
+
marks and the time added from the record the page holds now (`before`) - over its form's copies, which a flag or a
|
|
1536
|
+
mark made while the form was open has left behind - and was changed now.
|
|
1537
|
+
- **Joined names with no call**: `crudColumns.dbJoin` takes `from`, the field holding the joined record's key (kept as
|
|
1538
|
+
**`TableColumn.dbJoin`**), and after a save **`crudJoinedNames`** writes each such column again. A key kept keeps its
|
|
1539
|
+
name and status, an emptied one empties both, and a new or changed one takes the name (the dropdown's `labelKey`) and
|
|
1540
|
+
the `recordStatus` of the record behind it. That record is the one the user picked (the form now keeps a dropdown's
|
|
1541
|
+
item: **`FormStore.pickedItem(name)`**, `input(name, value, stamp, item)`), else one the page holds (**`Dropdown`
|
|
1542
|
+
`peekOption(key)`**: `crudPicker` gives one from its kept list and the FindListing cache, and **`peekFound(findListing,
|
|
1543
|
+
key)`** serves a picker of a page's own), else, in a tree, the parent the record sits under, else one the page held
|
|
1544
|
+
and has marked old (`peekOption(key, true)`). A key nothing names is left unnamed - an edit drops the old key's name -
|
|
1545
|
+
and **`crudJoinsMissing`** then has the record read back once. A data table's Edit that moves a record now shows the
|
|
1546
|
+
new parent's name; it showed the old one until the page was read again.
|
|
1547
|
+
- **Counts**: with `trees`, **`crudSavedCount`** gives a new record its placement's count (0) and an edited one the count
|
|
1548
|
+
the page holds now (a child added since its form opened counts), and **`crudParentCounts`** moves the counts on the
|
|
1549
|
+
parents' own rows that a data table holds - one more under the new parent, one fewer under the old - on a create, a
|
|
1550
|
+
move, the trash and back, as the tree's parents already did; each parent row once, however many placements the page
|
|
1551
|
+
gives. Both write what `CrudTreePlacement.recordOf` gives over the row, so `recordOf` carries the item's count back.
|
|
1552
|
+
- **In a tree**, a record added under a parent never opened is counted there and shown in the panel; the parent stays
|
|
1553
|
+
closed, since opening it would read its page.
|
|
1554
|
+
- `Dropdown` `peekOption` on its own: a value the page already holds is named from the first draw, with no
|
|
1555
|
+
`resolveOptions` call - while it is fresh: a record marked old since (a save on its controller, a Reload) is asked for
|
|
1556
|
+
again, so the box shows it as it is now. **`TableJoins.peekFresh(name, key)`**, and `peekFound`'s `aged`.
|
|
1557
|
+
|
|
1558
|
+
### New exports no section names
|
|
1559
|
+
|
|
1560
|
+
The types and small helpers that come with the parts the sections describe, so that every new name is listed once:
|
|
1561
|
+
|
|
1562
|
+
- `nexus-shared`: the file types `FileOwner`, `FileCopy`, `StoredFile`, `FileUpload`, `FileCallOptions`,
|
|
1563
|
+
`PictureType`, `PICTURE_TYPES`, `isPictureType`, `ImageViewerItem`, `PictureParams`; the file helpers `fileKindName`,
|
|
1564
|
+
`fileOwnerKey`, `acceptOf`, `readStoredFile`, `readOwnerFiles`, `FileApiClient`, `NexusDriveAdapterOptions`,
|
|
1565
|
+
`PrimariesReader`, `PrimaryPicturesOptions`; `QrCodeImageProps`, `MoneyFormatOptions`, `CrudJoinedRecord`,
|
|
1566
|
+
`NarrowPickWords`, `CrudNarrowPickerSource`, `CrudDetailsProfile`, `CrudDetailsTab`, `TableDbJoin`.
|
|
1567
|
+
- `nexus-shared/client`: `getFileAdapter`, `toastFileFailure`, `usePrimaryPicture`, the props types
|
|
1568
|
+
`PictureInputProps`, `OwnerPictureProps`, `PictureGalleryProps`, `OpenPictureGalleryOptions`, `PictureColumnParams`,
|
|
1569
|
+
and the tree CRUD's `CrudTreeProps`, `CrudTreeDetails`, `CrudTreeHandle`.
|
|
1570
|
+
|
|
1571
|
+
## 3.2.0
|
|
1572
|
+
|
|
1573
|
+
**New features, no code break.** Every export of `3.1.0` is still exported (99 are new: 72 from `nexus-shared`, 16
|
|
1574
|
+
from `nexus-shared/client`, 11 from `nexus-shared/server`), and no exported constant changed a value: six only gained
|
|
1575
|
+
keys or entries (`API_ACTIONS`, `CRUD_ROW_ACTIONS`, `CRUD_MASS_ACTIONS`, `MESSAGE_ACTION_NAMES`,
|
|
1576
|
+
`DEFAULT_USER_PREFERENCES`, `PREFERENCE_STORAGE_KEYS`). Both were checked against the published `3.1.0`: its exports
|
|
1577
|
+
by the TypeScript checker, its constants by importing both builds. The owner chose the number on 2026-10-10. It needs
|
|
1578
|
+
`nexus-icons` `^1.2.0`, the first with `iconStarOff`. What can move for an app:
|
|
1579
|
+
|
|
1580
|
+
- **A CRUD built from `apiController` reads `BookmarkListing` and writes `SwitchBookmarks`**, the Nexus backend's
|
|
1581
|
+
names since 2026-10-07, in place of `PinListing` / `Pin`. See "Pins and favourites from one call" below.
|
|
1582
|
+
- **Stylesheet overrides** of the pinned section's classes and of the outlined field's raised label (both below).
|
|
1583
|
+
- **`TablePinnedHead` is deprecated**: `TableSectionHead` takes the section's icon. The old name still renders the
|
|
1584
|
+
pinned section's head, with the pin icon and the new classes, so an import of it keeps building.
|
|
1585
|
+
|
|
1586
|
+
### A tree whose children load when they are opened
|
|
1587
|
+
|
|
1588
|
+
The owner, 2026-10-09: administrative regions in two versions, a data table and a tree, both paged on the server; the
|
|
1589
|
+
tree "when a node is expanded, calls the API, gets the info and caches it". The data table already paged on the
|
|
1590
|
+
server, so the change is the tree's. Added only; a tree without `loadChildren` works as before, with one fix it shares.
|
|
1591
|
+
|
|
1592
|
+
- **`TreeView` `loadChildren`** (`nexus-shared/client`): asks for an item's children when it is first expanded, a page
|
|
1593
|
+
at a time - `{ parent, parentValue, skip, take, signal }` in, `{ items, total?, hasMore? }` out, at once or in a
|
|
1594
|
+
promise - and for the top level too when `items` is left out. The children go under the item asked for, whatever
|
|
1595
|
+
their own parent field says. New params `childPageSize` (default 50), `hasChildrenKey` (a boolean or a count such as
|
|
1596
|
+
`childCount`; without it an item shows a chevron until its first page answers, and loses it when that is empty),
|
|
1597
|
+
`cacheKey`, `cacheLifetime` and `partialMessage`; `items` is optional now.
|
|
1598
|
+
- **Status rows**, rows of the windowed list like the others, reached with the arrows and pressed with Enter or Space:
|
|
1599
|
+
a loading row while a page comes (the parent `aria-busy`, a polite announcement), "Could not load the items." /
|
|
1600
|
+
"Could not load more items." with Retry (the Dropdown's words), and **Load more** with how many are left when the
|
|
1601
|
+
total is known. Focus on one of them follows the page to its first new item. New classes `.nx-tree__row--status`,
|
|
1602
|
+
`--loading`, `--error`, `--more`, `.nx-tree__status-icon`, `.nx-tree__status-text`, `.nx-tree__retry`.
|
|
1603
|
+
- **What is loaded stays**: collapse and expand again asks nothing. With `cacheKey`, pages are kept in `treePages`, a
|
|
1604
|
+
`RequestCache` shared by every tree on the page, for 5 minutes: a remount or a second tree asks nothing, and a request
|
|
1605
|
+
already on its way is shared. A failure is not kept, so Retry asks again.
|
|
1606
|
+
- **`refresh(value?)`** on the handle drops the kept pages (all, or an item's and everything under it) and loads again
|
|
1607
|
+
only what is on screen - the top level and the open items with rows in view; the rest load fresh when next opened or
|
|
1608
|
+
scrolled to. What is drawn stays until the fresh pages replace it, and an item still there keeps what is open under it.
|
|
1609
|
+
- **Checking, with `cascade`**: an item whose items are not all loaded (`TreeNode.partial`) cannot be checked as a
|
|
1610
|
+
whole - a check stands for everything under it, and the value can only hold items that were loaded, so it would
|
|
1611
|
+
silently leave the rest out. A click says so (`partialMessage`); it shows mixed when something loaded under it is
|
|
1612
|
+
checked, never checked; Select all checks the loaded parts. A saved value whose item is not loaded yet stays in the
|
|
1613
|
+
value and checks the item when it comes. Without `cascade` nothing changes.
|
|
1614
|
+
- **Nothing asks in bulk**: Expand all, `*` and `defaultExpanded` open loaded items only.
|
|
1615
|
+
- **`TreeStore`**: options `lazy`, `lazyRoots`, `hasChildrenKey`; `canExpand`, `find`, `pagesOf`, `beginLoad`,
|
|
1616
|
+
`receivePage`, `failLoad`, `replacePages`, `markStale`, `rootPages`, and `insertMany`, a batch insert that updates
|
|
1617
|
+
counts along one path (a page of 50 into 100,000 items: 0.3 ms); `TreeNode.pages`, `.partial`, `.status`. Helpers
|
|
1618
|
+
`treePages`, `treePagesKey`, `treePageKey`, `expireTreePages`, `treeHasChildren`, `treeStatusKey`, and the types
|
|
1619
|
+
`TreeChildrenRequest`, `TreeChildrenPage`, `TreeChildPages`, `TreeStatusKind`.
|
|
1620
|
+
- **`RequestCache`** (`Helpers/RequestCache.ts`, `nexus-shared`): answers kept by key for a lifetime, a request on its
|
|
1621
|
+
way shared, a failure not kept, `drop` by key prefix or test; `REQUEST_CACHE_LIFETIME`, `isPromiseLike`.
|
|
1622
|
+
- **Fix, every tree**: a node's `uid` counted from one counter for the whole page, so a page rendered on a server that
|
|
1623
|
+
had rendered trees before numbered its rows differently from the browser, and React reported a hydration mismatch
|
|
1624
|
+
in development (production does not check attributes). It is counted per store now.
|
|
1625
|
+
- **Fix, every tree: a scroll that takes the focused row out of view keeps its position** (nexus.web, the same
|
|
1626
|
+
evening: after a client-side navigation a jump to the end came back up to 1,120px, and a wheel step left Load more
|
|
1627
|
+
out of view). Chrome lays the page out inside the removal of a focused element, in the middle of React's commit,
|
|
1628
|
+
when the leaving rows are gone and the new ones not yet in, and clamped or anchored the scroll position to that short
|
|
1629
|
+
list. Focus now moves from a row to the tree's viewport in the event that moves the window, before the row leaves
|
|
1630
|
+
(a name box being edited lets go without saving, as when its row unmounted), and `.nx-tree__list` gets a `min-height`
|
|
1631
|
+
of all its rows, so a layout at any moment of a commit sees its full height.
|
|
1632
|
+
|
|
1633
|
+
### Focus rings shown whole
|
|
1634
|
+
|
|
1635
|
+
The owner, 2026-10-08: the theme panel cut the focus ring of its last example input (bottom) and of the date-format
|
|
1636
|
+
dropdown (left), and a focused outlined input's raised label sat across its own ring. No export changed; what a
|
|
1637
|
+
consumer may notice:
|
|
1638
|
+
|
|
1639
|
+
- **`.nx-popover__scroll` keeps 4px of room** on its top, bottom and left for focus rings, with a matching negative
|
|
1640
|
+
margin so nothing moves, and `scroll-padding` of 4px. Every panel that scrolls through it gets it: the theme panel,
|
|
1641
|
+
menus, and the table's Columns and Filters panels. A panel's `--nx-popover-pad` must be 4px or more. Where the
|
|
1642
|
+
scroller is its panel's first or last child, as a menu's list is, its left corner follows the panel's curve (the
|
|
1643
|
+
inner radius less that room: 9px in a menu), so a scrolled item's highlight never paints over the rounded border.
|
|
1644
|
+
- **A focused outlined field** (`design="outlined"`) draws a 2px accent outline over its border instead of the soft
|
|
1645
|
+
ring outside it, so the raised label sits in a clean gap. The raised label is placed with `top: -0.6em` instead of a
|
|
1646
|
+
transform, and its backing is now the label's `::before`: it starts exactly on the box's edge at every screen scale
|
|
1647
|
+
and paints nothing above it. The gradient it replaces painted a strip of the field's colour above read-only and
|
|
1648
|
+
disabled boxes and in dark popovers. An app that styles the outlined label's `::before` overrides that backing. The
|
|
1649
|
+
standard and inbox designs are unchanged.
|
|
1650
|
+
|
|
1651
|
+
### The API proxy hides the backend
|
|
1652
|
+
|
|
1653
|
+
The owner, 2026-10-08: nexus.web's network tab showed `POST /api/app/configuration/Region/MassRecover` answer
|
|
1654
|
+
`307 Temporary Redirect` with the backend's own https address in it, which the browser then called directly, and
|
|
1655
|
+
asked for a proxy behind which the app looks like the backend itself. `createApiProxy({ token })` is called as before
|
|
1656
|
+
and no export is removed; what a route may notice:
|
|
1657
|
+
|
|
1658
|
+
- **No redirect reaches the browser** (was: passed on, its Location rewritten only on the backend's own host). One that
|
|
1659
|
+
stays inside the module - the root's origin, or its host moved to https, under the root's path - is followed on the
|
|
1660
|
+
server as `fetch` follows it, up to 3; any other answers 502 `BACKEND_REDIRECT_REFUSED` or `BACKEND_REDIRECT_LIMIT`.
|
|
1661
|
+
- **Response headers by an allowlist** (was: all but the hop headers): `DEFAULT_PROXY_RESPONSE_HEADERS` and the Nexus
|
|
1662
|
+
backend's `X-Nexus-*`. A header an app read through the proxy (`X-Request-Id`, `X-Total-Count`) must now be named in
|
|
1663
|
+
`responseHeaders`; Server, X-Powered-By, X-AspNet-*, HSTS, CORS, Location, Link and Set-Cookie never pass (Set-Cookie
|
|
1664
|
+
with `passCookies`). Every answer gets `X-Content-Type-Options: nosniff`, and `Cache-Control: private, no-store`
|
|
1665
|
+
when the backend sent none.
|
|
1666
|
+
- **Error pages are replaced** with the failure envelope and the backend's status (`BACKEND_ERROR_PAGE`): HTML, text,
|
|
1667
|
+
stack traces, and JSON that repeats the user's token. JSON refusals pass as they came.
|
|
1668
|
+
- **Refusals carry `errorCode` and `errorMessages`** beside `message` and `messageCode`, and say nothing about the
|
|
1669
|
+
server: an unset module answers "The School API is not set up on this server." (was: "... is not set (API_SCHOOL).",
|
|
1670
|
+
which named a setting; the log has it now), and a module the browser calls directly "... is not available at this
|
|
1671
|
+
address." New: 403 `CROSS_SITE_REFUSED`, 413 `REQUEST_TOO_LARGE`, 400 `REQUEST_UNREADABLE`.
|
|
1672
|
+
- **The request**: the browser's X-Forwarded-*, Forwarded, X-Real-IP, method and path overrides, `Expect` and Next.js's
|
|
1673
|
+
own headers never go on; the proxy writes X-Forwarded-For (the last address the app received, and only ever an IPv4
|
|
1674
|
+
or IPv6 address) and X-Forwarded-Host and -Proto (the app's configured origin, else the request's). A module whose
|
|
1675
|
+
root is a path on the app is called at this server's own host and port, over http (was: the request URL's protocol,
|
|
1676
|
+
which Next.js takes from X-Forwarded-Proto), and never at the configured origin or anywhere a request header names.
|
|
1677
|
+
- **`configureServerApi({ origin })` must not trust the Host header.** `serverApi` calls a relative root at that origin,
|
|
1678
|
+
so one built from a request's Host lets a client pick an address on the server's machine. The template's
|
|
1679
|
+
`api-setup.ts` now takes the Host only as `localhost:<PORT>` or `127.0.0.1:<PORT>` (Next.js sets `PORT` to the port
|
|
1680
|
+
it listens on); an app with a copy of the older one, which took any localhost port, should change it the same way.
|
|
1681
|
+
- **A server call can say who the client is.** `lastForwardedAddress` (which now takes anything with a `get`, Next.js's
|
|
1682
|
+
`headers()` included) and `readIpAddress` are exported from `nexus-shared/server`: the client's address as the proxy
|
|
1683
|
+
reads it, an IP address or nothing. The template's `api-setup.ts` uses the first in `configureServerApi({ headers })`
|
|
1684
|
+
to give Identity calls, and only them, `X-Forwarded-For` and the browser's `User-Agent`: sign-in leaves from the
|
|
1685
|
+
server, and Identity's rate limit counts per address. An app that signs in through `serverApi` should do the same,
|
|
1686
|
+
for Identity's calls only, since reading the request makes a page render per request.
|
|
1687
|
+
- **New options** `responseHeaders`, `passCookies`, `maxBodySize` (100 MB), `maxRedirects`, `clientAddress`,
|
|
1688
|
+
`allowCrossSite`, `failure`, `onProblem`; **new exports** `DEFAULT_PROXY_RESPONSE_HEADERS`, `DEFAULT_PROXY_BODY_LIMIT`,
|
|
1689
|
+
`DEFAULT_PROXY_REDIRECTS`, `PROXY_REFUSALS`, `nexusFailure`, `lastForwardedAddress`, `readIpAddress` and the types
|
|
1690
|
+
`ApiProxyFailure`, `ApiProxyProblem`, `ApiProxyCode` (`nexus-shared/server`), and `serverAppOrigin`.
|
|
1691
|
+
- **A backend on a development certificate**: the proxy now calls the backend's https address itself, and Node.js
|
|
1692
|
+
trusts ASP.NET Core's development certificate only when started with `NODE_USE_SYSTEM_CA=1` (or
|
|
1693
|
+
`NODE_EXTRA_CA_CERTS`). Without it such a module answers 502 `BACKEND_UNREACHABLE`, and the server's log says why.
|
|
1694
|
+
|
|
1695
|
+
### The code box
|
|
1696
|
+
|
|
1697
|
+
The owner, 2026-10-08: one shared input for codes typed a box to a character - one-time codes, SSNs, and more - as
|
|
1698
|
+
dynamic as possible (numbers, strings, hex). New exports, nothing removed:
|
|
1699
|
+
|
|
1700
|
+
- **`CodeBox`** (`nexus-shared/client`): one real input lying unseen over a box per character, so autofill
|
|
1701
|
+
(`autoComplete="one-time-code"`), a paste and a screen reader see one field. `length` or `groups`, a `separator`
|
|
1702
|
+
outside the value, `characters` (`digits`, `letters`, `alphanumeric`, `hex`) or an `allowed` one-character pattern,
|
|
1703
|
+
`letterCase`, `masked`, `slotPlaceholder`, `block`, `onComplete`, and the field shell's label, hint, error, sizes and
|
|
1704
|
+
read-only note.
|
|
1705
|
+
- **`type: "code"`** in `InputParams` (`CodeInputParams`): `renderInput`, `validateInput`, `normalizeInputValue` and
|
|
1706
|
+
`formatInputValue` know it.
|
|
1707
|
+
- **One typing model** for every way text arrives - a key, a phone's keyboard or its suggestion, an input method, a
|
|
1708
|
+
paste, autofill: a whole code replaces everything; a character replaces a selected box, or goes into the box with the
|
|
1709
|
+
ring - inserted while the code has an empty box, so a box Backspace removed takes the right digit back, and over that
|
|
1710
|
+
box once the code is full. ← → Home End select box by box, and a full code's caret selects its last box. An input
|
|
1711
|
+
method's word goes in once, when it commits. A pasted message gives its one code ("Your 2-step code is 482913" is
|
|
1712
|
+
482913; "482 913" and "123-45-6789" are read whole), or nothing when it holds more characters than boxes and no one
|
|
1713
|
+
code. `onComplete` runs once per code, and again once the boxes were short - by typing, a form's reset, or the page
|
|
1714
|
+
emptying a controlled `value`.
|
|
1715
|
+
- **A saved code is never cut**: `value`, `defaultValue` and a form's loaded record (`normalizeInputValue`) lose only
|
|
1716
|
+
separators, case and a hex "#". A code the boxes cannot hold is kept, shown invalid at once, and a form refuses to
|
|
1717
|
+
save it. The over-long message names the count: "The code is 6 digits long, not 7."
|
|
1718
|
+
- **What a screen reader hears** is what the boxes show: "9 digits, in groups of 3, 2 and 4", "8 digits: DD/MM/YYYY".
|
|
1719
|
+
A read-only code shows the accent border on every box while it has focus.
|
|
1720
|
+
- **Styles**: a box without `block` keeps its own width (`.nx-code:not([data-block]) .nx-code__slot`), about as wide
|
|
1721
|
+
as it is tall, and the boxes shrink alike on a narrow screen.
|
|
1722
|
+
- **Helpers and types** (`nexus-shared`): `validateCode`, `formatCode`, `codeGroups`, `codeLength`, `loadCode`,
|
|
1723
|
+
`codeFits`, `describeCode`, `readCodeText`, `typeCode`, `settleCode`, `moveCode`, `codeAfterInput`,
|
|
1724
|
+
`CODE_CHARACTER_PATTERNS`, `DEFAULT_CODE_LENGTH`, `CodeCharacters`, `CodeRules`, `CodeShape`, `CodeEdit`, `CodeText`,
|
|
1725
|
+
`CodeMove`; `normalizeCode` stays exported, though the box itself now loads a code with `loadCode`.
|
|
1726
|
+
|
|
1727
|
+
### Another record's name, joined by the database or by the table
|
|
1728
|
+
|
|
1729
|
+
Added only; nothing an app uses changes (2026-10-08, the owner: "some columns we joined into DB, some frontend").
|
|
1730
|
+
|
|
1731
|
+
- **A column whose value is another record's** (`TableColumn.join`): the row holds the key (`from`), and the table asks
|
|
1732
|
+
the source once for every key its rows hold - a Nexus controller's `findListing`, or a `TableJoinSource` of the
|
|
1733
|
+
page's own - then shows the record's field, searched, sorted, exported and shown in the details like any column. A key
|
|
1734
|
+
with no record shows "Not found". `crudColumns.clientJoin(SUB_REGIONS, { key: "subRegionName", label: "Sub-region",
|
|
1735
|
+
from: "subRegionId" })` is the Nexus form.
|
|
1736
|
+
- **One cache for the page** (`tableJoins`, `Helpers/TableJoins.ts`): keys asked for in the same moment by every table and
|
|
1737
|
+
picker go in one call per source; a call already out is waited for; answers are kept 5 minutes (`JOIN_LIFETIME`) and an
|
|
1738
|
+
aged one is still drawn while it is asked again; a failed call is not repeated for 30 seconds. A Reload of a table, and
|
|
1739
|
+
a `Crud` write on the controller looked up, mark its answers old (`expireLookups`).
|
|
1740
|
+
- **A short tag after a value** (`TableColumn.tag`), and the one every reference to a record gets when that record is no
|
|
1741
|
+
longer available: `CrudBackend.unavailableTag(status)` - Nexus: "Trashed" (warning) from 40, "Deleted" (danger) from 60,
|
|
1742
|
+
none for a live record. `crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus" })` reads
|
|
1743
|
+
it from a status the database joined into the row; `clientJoin` from the record it looked up. New class
|
|
1744
|
+
`.nx-table__tag--after`.
|
|
1745
|
+
- **A picker of a controller's usable records** (`crudPicker(REGIONS, { labelKey: "regionName" })`, `nexus-shared/client`):
|
|
1746
|
+
its `ActiveListing` read once for the page and searched in the browser, a saved value it no longer offers found through
|
|
1747
|
+
the same cached `findListing`, tagged and never offered again. `CrudBackend.isPickable(record)` - Nexus: under 30.
|
|
1748
|
+
- `TableStore.refreshValues()` searches and sorts again when values changed without the rows; `CrudEndpoints` takes the
|
|
1749
|
+
`findListing` and `activeListing` an `apiController` already has.
|
|
1750
|
+
|
|
1751
|
+
### Pins and favourites from one call
|
|
1752
|
+
|
|
1753
|
+
Two things move for an app:
|
|
1754
|
+
|
|
1755
|
+
- **A CRUD built from `apiController` now reads `BookmarkListing` and writes `SwitchBookmarks` / `MassSwitchBookmarks`**,
|
|
1756
|
+
the Nexus backend's names since 2026-10-07. An app whose backend still answers only `PinListing` / `Pin` / `MassPin`
|
|
1757
|
+
passes its own endpoints without `bookmarks`, `bookmark` and `mass.bookmark`, and keeps the pin-only behaviour.
|
|
1758
|
+
- **The section classes are renamed** (an app's override of an old name stops matching):
|
|
1759
|
+
|
|
1760
|
+
| Was | Now |
|
|
1761
|
+
|---|---|
|
|
1762
|
+
| `tbody.nx-table__pins` | `tbody.nx-table__section[data-section="pins"]` (and `"favorites"`) |
|
|
1763
|
+
| `.nx-table__pin-row`, `__pin-cell`, `__pin-toggle`, `__pin-caret`, `__pin-icon`, `__pin-title`, `__pin-count`, `__pin-loading` | `.nx-table__section-row`, `__section-cell`, `__section-toggle`, `__section-caret`, `__section-icon`, `__section-title`, `__section-count`, `__section-loading` |
|
|
1764
|
+
| `.nx-table__pin-mark` | `.nx-table__mark`, inside `.nx-table__marks` |
|
|
1765
|
+
| `data-pinned` on a section's row, `data-table-pins` on its toggle | gone: `data-section` on the section |
|
|
1766
|
+
| saved `pinsOpen` | `openSections` (an old `pinsOpen: true` still opens the pinned section) |
|
|
1767
|
+
|
|
1768
|
+
What is new (the owner's rules, `Project-Skills/README.RULES.md`, "Rules for Favorite or Pin"):
|
|
1769
|
+
|
|
1770
|
+
- **Favourites beside pins, from one call.** `CrudEndpoints.bookmarks` (`Room/BookmarkListing`) answers every record
|
|
1771
|
+
this user marked, each with its bits (`bookmarkTypes`: 1 pinned, 2 favourite); `Crud` splits them into a **Pinned**
|
|
1772
|
+
and a **Favourites** section above the active list, each folding on its own and remembered. The lists' own rows need
|
|
1773
|
+
not carry marks: every record's marks come from that call.
|
|
1774
|
+
- **The marks show where a row's number is**, one under the other: a pinned favourite wears the pin above the star, in
|
|
1775
|
+
the list and in both sections, with no column of its own (the owner, 2026-10-08). `DataTable` takes
|
|
1776
|
+
`rowMarks(row)`; a section's row with none wears the section's icon.
|
|
1777
|
+
- **Directed switches, never a toggle.** Pin and **Add to favourites** / **Remove from favourites** on a row send the
|
|
1778
|
+
record's whole set (`bookmark`, `?bookmarkTypes=3`, built by `CrudBackend.marksRequest`); on a selection they send
|
|
1779
|
+
one bit and the way (`mass.bookmark`, `?bookmarkTypes=2&isAdding=true`, `massMarksRequest`), only the records it
|
|
1780
|
+
is for, and a selection is offered only the ways it can still go - both, when it is mixed. Bits the package does not
|
|
1781
|
+
know are kept. While a whole-set backend's marks are not read yet, the actions wait and say why.
|
|
1782
|
+
- **A limit each:** `configureCrud({ maxFavorites })`, the same as `maxPins` unless set (`markLimit`). Nothing is ever
|
|
1783
|
+
taken off to meet a lowered limit.
|
|
1784
|
+
- **No reload:** a switch's answer (`answeredMarks`, or the ids a mass switch answers) is written into the sections,
|
|
1785
|
+
the table and every list kept. A record trashed leaves the sections and keeps its marks; recovered, it comes back.
|
|
1786
|
+
- `DataTable` takes `sections` (`TableRowSection`: key, label, icon, rows, loading, max); `pinnedRows` and the other
|
|
1787
|
+
`pinned*` props stay, as one section with the key `pins`. Select by **Favourites** / **Not favourites**. The message
|
|
1788
|
+
builder knows `favorite` and `unfavorite` ("3 guests added to the favourites."). New helpers: `CRUD_MARKS`,
|
|
1789
|
+
`CRUD_MARK_BITS`, `hasMark`, `withMark`, `markLimit`, `markDirections`, `markedRecords`, `patchMarkedRecords`,
|
|
1790
|
+
`dropMarkedRows`, `crudMassEndpoint`.
|
|
1791
|
+
- **Needs `nexus-icons` with `iconStarOff`**, which is new in its source (not on npm): release `nexus-icons` with a
|
|
1792
|
+
raised version first, and raise this package's `nexus-icons` range to it.
|
|
1793
|
+
|
|
1794
|
+
### A CRUD page passes params and the CRUD does the rest (2026-10-08)
|
|
1795
|
+
|
|
1796
|
+
The owner: *"we just pass params no logic, due to we have 1000+ similar page"*. What the Regions page did itself is
|
|
1797
|
+
now `Crud`'s, so a page is its controller, columns and inputs:
|
|
1798
|
+
|
|
1799
|
+
- **`apiController(module, controller, subject, options)`**: `commands: "switch"` (the default, the Nexus backend since
|
|
1800
|
+
2026-10-07: `SwitchFlag`, `MassSwitchFlag`, `BookmarkListing`, `SwitchBookmarks`, `MassSwitchBookmarks`) or
|
|
1801
|
+
`"toggle"` (`Flag`, `MassFlag`, `PinListing`, `Pin`, `MassPin`), and `without` for what a backend lacks (`"pages"`,
|
|
1802
|
+
`"audit"`, `"flag"`, `"marks"`, `"mass.description"`, ...). **The return type now marks those members optional**
|
|
1803
|
+
(`pages`, `pagination`, `flag`, `audit`, the marks, the mass actions): a caller that reads one adds `!` or `?.`.
|
|
1804
|
+
`MODULES.controller` takes the same options.
|
|
1805
|
+
- **A directed flag.** Flag and Unflag send the way (`CrudBackend.flagRequest`, Nexus `?isFlagged=`); a selection is
|
|
1806
|
+
offered Flag and Unflag only as it can still go, sends only the records each is for, and says "the other N already
|
|
1807
|
+
were" when the answer lists fewer. `NexusCrudBackend.isFlagged` is the band 25-29, and `canFlag` keeps Flag off a
|
|
1808
|
+
pending record (30-39). The old "Flag / unflag" toggle of a mixed selection is gone.
|
|
1809
|
+
- **History built in.** With an `audit` endpoint the person may call, every row offers **History** and the details
|
|
1810
|
+
**View history**: `AuditHistory` and `openAuditHistory` (moved from nexus.web), its words from
|
|
1811
|
+
`CrudBackend.auditWords`.
|
|
1812
|
+
- **Presets:** `crudColumns.status()`, `.description()` (a `markdown` column: text in the table, formatted in the
|
|
1813
|
+
details), `.added()`, `.changed()`, and `crudInputs.description()` (Markdown, 1000 characters, one hint for every
|
|
1814
|
+
model). `TableColumn.markdown` is new.
|
|
1815
|
+
- **A Create answered with the payload** (no status, so no list can place it) is read back once through `details`.
|
|
1816
|
+
- **The export names the list on screen** (`CrudBackend.exportFilters`, Nexus `{ flag: 50 | 52 | 54 | 55 }`).
|
|
1817
|
+
### The theme panel: grouped, twice as wide, and the input design (2026-10-08)
|
|
1818
|
+
|
|
1819
|
+
- **Input design is a quick preference** (`UserPreferences.inputDesign`, key `nx-input-design`): Standard, Outlined or
|
|
1820
|
+
Inbox for every labelled input whose page and form name none (`useInputDesign`). An input with a design of its own
|
|
1821
|
+
(a grid's cells) keeps it, and one with no label (a table's search box) stays standard: it has no label to place.
|
|
1822
|
+
- **`ThemeSwitcher`'s panel is 680px wide** (was 340), its settings in named sections two to a row - Theme across the
|
|
1823
|
+
top, then Appearance, Language and region, Dates and times, Numbers, Tables, and Forms (the input design, with a box
|
|
1824
|
+
to try it) - and one under the other on a narrow window. `ThemePicker`'s card group is named Colors now, under Theme.
|
|
1825
|
+
|
|
1826
|
+
- **`Crud` takes `backend`**, for an app whose pages sit on two backends. `CrudBackend` gains `canFlag`, `flagRequest`,
|
|
1827
|
+
`exportFilters`, `statusText` and `auditWords`, each with a default, so an app's own backend class still compiles.
|
|
1828
|
+
|
|
1829
|
+
## 3.1.0
|
|
1830
|
+
|
|
1831
|
+
**New features, no code break.** Every export of `3.0.0` is still exported (49 are new: 33 from `nexus-shared`, 16 from
|
|
1832
|
+
`nexus-shared/client`), and no exported constant changed its value, so `^3.0.0` takes it. The owner chose the number
|
|
1833
|
+
on 2026-10-08. What can move for an app is its **stylesheet overrides**: these class names and custom properties were
|
|
1834
|
+
renamed or dropped, and an override of the old name no longer applies.
|
|
1835
|
+
|
|
1836
|
+
| Was | Now |
|
|
1837
|
+
|---|---|
|
|
1838
|
+
| `.nx-columns__head`, `__title`, `__count`, `__foot` | `.nx-table-panel__head`, `__title`, `__count`, `__foot` |
|
|
1839
|
+
| `.nx-table-panel__body` | `.nx-popover__scroll` |
|
|
1840
|
+
| `.nx-chat-avatar`, `--nx-chat-avatar` | `.nx-avatar`, `--nx-avatar-size` |
|
|
1841
|
+
| `.nx-table__frame`'s border | on `.nx-table` |
|
|
1842
|
+
| `--nx-field-scrollbar`, `--nx-field-scrollbar-hover`, `--nx-dropdown-scrollbar`, `--nx-time-scrollbar` | gone: one thin bar everywhere |
|
|
1843
|
+
| `.nx-form__skeleton*` | gone: a loading form shows its real inputs, disabled |
|
|
1844
|
+
| `.nx-excel__refused` | gone: the import's report lists are `.nx-excel__problems` |
|
|
1845
|
+
|
|
1846
|
+
- **Bulk insert, and rows added to an import (the owner, 2026-10-07).** `ExcelImport` and `openExcelImport` take
|
|
1847
|
+
`mode: "bulk"`: the import with no file, its new rows' editor opening with one empty row for the person to type
|
|
1848
|
+
into, sent to the same `Excel/Import` with the same checks, a problem then naming its row ("Row 3 · Region name").
|
|
1849
|
+
`DataTable` offers it as **Bulk insert** beside **Import from a spreadsheet**, and only beside an Add, as it is an Add
|
|
1850
|
+
of many rows (`TableExcel.bulk: false` leaves it out). A sheet with a required list cannot be typed in - a list's key
|
|
1851
|
+
is picked in the spreadsheet - and the panel says so; its file review takes no added rows either. Any other file's
|
|
1852
|
+
new rows take Add, Insert and Duplicate: an added row is numbered after every row the file had
|
|
1853
|
+
(`excelSheetRows(rows, columns, after)`), and one left empty is dropped, not refused. The new rows no longer fold
|
|
1854
|
+
away (folding unmounted the editor and lost every edit), the footer counts what will be sent, and the review is inert
|
|
1855
|
+
while an import is out.
|
|
1856
|
+
- **Nothing typed is lost unasked.** Closing the window, or Start over, over rows typed or changed in it asks first
|
|
1857
|
+
(`ExcelImportHandle.unsavedRows`); picking the entry again while its window is minimized brings that window back
|
|
1858
|
+
rather than a fresh one. An untouched date or time is no edit: `toInputValue` reads them as their inputs do.
|
|
1859
|
+
- **An import that wrote nothing keeps its rows** and lists under them each refusal, by row, in the backend's words:
|
|
1860
|
+
`readExcelResponse` keeps a refusal's report (`result`, as XLS_X044 sends it) instead of dropping it, so the panel no
|
|
1861
|
+
longer says only "No Region was added". Refusals read in row order.
|
|
1862
|
+
- `ExcelEditorRow`'s three own fields are optional, as an added row has none; new helpers `excelRowNumber`,
|
|
1863
|
+
`hasExcelValues` and `isExcelRowTyped`, and `excelProblemPlace(problem, byRow)`. `RowsInputHandle.remove(index,
|
|
1864
|
+
{ undo: false })` drops a row with no Undo offered, and keeps the person's own last Undo where it was.
|
|
1865
|
+
- **A list shows what an import wrote.** After an import, `DataTable` runs `excel.onImported` first and reads its rows
|
|
1866
|
+
after it: a `Crud` drops its cached lists there, and the reload used to be asked before, so on a page just opened it
|
|
1867
|
+
was answered from the cache and the imported rows stayed off the screen until a reload.
|
|
1868
|
+
- **What imported records start as.** `ExcelSettings` gains `recordStatuses` (`ExcelRecordStatus`: `value`, `label`)
|
|
1869
|
+
and `defaultRecordStatus`; when a model offers any, the import shows "New records start as", the default picked,
|
|
1870
|
+
and sends the choice (`importExcelRows(..., { recordStatus })`; `ExcelBackend.importRequest(rows, recordStatus?)`,
|
|
1871
|
+
the Nexus one sending `{ rows, recordStatus }`). The report says "3 cities added as In review".
|
|
1872
|
+
- **`Crud`, from the owner's review on 2026-10-07.** The active list is labelled **Active**, not "All" - it leaves out
|
|
1873
|
+
the trash and the deleted. **Recover asks first** ("Recover Asia?", "It goes back to the active list."), one record
|
|
1874
|
+
or many, like trash and delete. Recover and Move back to trash wear the back arrow (`iconArrowBackUp`): the
|
|
1875
|
+
clock-shaped restore icon read as History beside a page's history action.
|
|
1876
|
+
- **`details.actions`**: buttons of the page's own in the details popup's footer, before Edit and Close
|
|
1877
|
+
(`CrudDetailsAction`: `id`, `label`, `icon`, `endpoint`, `hidden`, `onSelect(row, popup)`). One that names an
|
|
1878
|
+
`endpoint` shows only to a user who may call it, by the rule every built-in action follows.
|
|
1879
|
+
- **The spreadsheet import, as the owner asked on 2026-10-07.** Once a file is picked, the steps and the template and
|
|
1880
|
+
file buttons give way to the file's name; another file is a Start over (now a bordered button) or a fresh popup. The
|
|
1881
|
+
rows already there are read-only and never sent, and the footer says they are skipped. The new rows have Remove and
|
|
1882
|
+
Lock at their end. After an import the review is gone: the panel shows what was added, what was not written and why,
|
|
1883
|
+
and what was skipped and why. The `nx-excel__refused` list is gone with it; the report's lists are
|
|
1884
|
+
`nx-excel__problems` blocks.
|
|
1885
|
+
- **`TabularForm` `actionsAt="end"` and `lockable`.** The row's actions in a column fixed at the right edge, as a data
|
|
1886
|
+
table has them: Remove and Lock as buttons of their own, anything else in a menu after them. A locked row's inputs
|
|
1887
|
+
are read-only ("Locked: unlock the row to change it.") and it cannot be removed; the locks are the screen's, never
|
|
1888
|
+
the value's.
|
|
1889
|
+
- **A tabular form no longer jumps when a cell is clicked.** The line under the table, which holds the hint or the
|
|
1890
|
+
message of the input with focus, keeps its height whether or not it says anything.
|
|
1891
|
+
- **A slow answer lands in the list the user moved to.** `Crud` writes an action's answer into the table only when
|
|
1892
|
+
the table still holds the list the action was asked from, fully loaded. Otherwise (the user switched lists while
|
|
1893
|
+
the request was out, or a list is still loading) it patches every cached list and reads the one on screen again from
|
|
1894
|
+
them, stopping a load that may have been asked before the change. Before, a record trashed from the active list and
|
|
1895
|
+
answered after a switch to the trash was written over by the stale trash list and showed in neither list until the
|
|
1896
|
+
page was opened again. The same rule covers `handle.apply`, Add, Edit and the read-back after an Add; a Create that
|
|
1897
|
+
answers no key now loads the list instead of showing the cached one.
|
|
1898
|
+
- **The theme panel is a quick preference panel.** Under the theme, `ThemeSwitcher` now shows `PreferencesPicker`
|
|
1899
|
+
(`preferences={false}` leaves it out): Language and region (interface language, calendar), Dates and times (date
|
|
1900
|
+
picker language, clock, display date format: the existing `DatePreferences`), Numbers (number scale, thousands
|
|
1901
|
+
separator) and Tables (default page size, a row click opens the record). New: `UserPreferences`,
|
|
1902
|
+
`readUserPreferences`, `setUserPreferences`, `useUserPreferences`, `subscribeUserPreferences`, `useNumberLocale`,
|
|
1903
|
+
`DEFAULT_USER_PREFERENCES`, `PREFERENCE_STORAGE_KEYS` (`nx-language`, `nx-number-scale`, `nx-thousands`,
|
|
1904
|
+
`nx-page-size`, `nx-row-click`), `InterfaceLanguage`, `ThousandsSeparator`, and `ENumberScales` (the core's enum, by
|
|
1905
|
+
member value).
|
|
1906
|
+
- **Numbers follow the person's settings.** `numberLocale(scale, separator)` gives `en-US`/`en-IN` or a key such as
|
|
1907
|
+
`"en-IN|.|,"` that `formatNumber`, `parseNumber` and `getNumberSymbols` read. `NumberBox` and `Slider` use it when
|
|
1908
|
+
they have no `locale` (they defaulted to `en-US`, which is still the setting's default), and so do a `DataTable`'s and
|
|
1909
|
+
`CrudDetails`' number cells (`CellTextOptions.locale`, new). A number box rewrites its text when the setting changes.
|
|
1910
|
+
- **`DataTable` `pageSize` and `openOnClick` give way to the person's choice once it is made**: the default page size
|
|
1911
|
+
(when the table offers it) and the row click. A page size picked in a table with a `stateKey` still wins, and that
|
|
1912
|
+
table now saves a size only when it differs from the one it opens with, so a later default reaches it.
|
|
1913
|
+
- **Animations: On, Off or System, chosen in the theme panel.** `ThemeScript` now writes `data-nx-motion`
|
|
1914
|
+
(`reduce` or `full`) on `<html>` before the first paint, from the visitor's choice (`data-nx-animations`, saved as
|
|
1915
|
+
`nx-animations`) and the system's `prefers-reduced-motion`, and keeps it current when the system setting changes.
|
|
1916
|
+
**Every stylesheet reads that attribute instead of the media query**, and so does script motion (the popup's
|
|
1917
|
+
minimize and maximize, smooth scrolls). On animates on a machine whose operating system turned animations off; Off
|
|
1918
|
+
never animates. An app that does not render `ThemeScript` animates whatever the system says. New: `AnimationMode`,
|
|
1919
|
+
`setAnimations`, `motionFor`, `REDUCED_MOTION_QUERY`, the `ANIMATIONS_*` and `MOTION_ATTRIBUTE` constants, and
|
|
1920
|
+
`useTheme()`'s `animations`, `motion` and `setAnimations`.
|
|
1921
|
+
- **Density: Compact or Cozy.** `data-nx-density` on `<html>` (`nx-density`; `setDensity`, `useTheme().density`).
|
|
1922
|
+
Cozy makes `--nx-control-sm/md/lg` 32/38/44 and the new `--nx-row` 38; compact is exactly the old sizes. Table rows
|
|
1923
|
+
and headers, menu items, dropdown options and the Columns panel's lines are now sized from `--nx-row`, so they
|
|
1924
|
+
follow it. `ThemeScript` takes `defaultDensity`.
|
|
1925
|
+
- **Scale grid, development builds only**: Off, px (a line every 4px, solid every 48px) or mm (millimetres, solid every
|
|
1926
|
+
centimetre), drawn over the window on `[data-nx-grid] body::after`. `ThemeScript`'s `grid` and `ThemePicker`'s
|
|
1927
|
+
`showGrid` default to `isDevelopmentBuild()`, so a production build neither shows the section nor applies a saved
|
|
1928
|
+
grid. New: `ScaleGrid`, `setScaleGrid`, `isDevelopmentBuild`.
|
|
1929
|
+
- **`ThemePicker` / `ThemeSwitcher`** add Density, Animations and Scale grid under Mode and Theme (`showDensity`,
|
|
1930
|
+
`showAnimations`, `showGrid`).
|
|
1931
|
+
- **Popups fade in and out** (160ms in, 120ms out), with nothing else moving: the slide and scale are gone. **Every
|
|
1932
|
+
popover panel fades** in and out in 120ms through one rule on `.nx-popover`; the menu's, the table panels' and the
|
|
1933
|
+
dropdown's own opening animations are gone. A closing panel takes no clicks, and nothing waits for a fade.
|
|
1934
|
+
- **Popover panels fit the window.** `placeUnderAnchor` and `placeAtPoint` pick the side that fits, or the one with
|
|
1935
|
+
more room, and write that room as `--nx-popover-room`; a panel that marks its scrolling part `.nx-popover__scroll`
|
|
1936
|
+
is never taller than it. That part reaches the panel's right wall, so its scroll bar sits on the wall, and carries
|
|
1937
|
+
the right padding inside. Menus, the theme panel and the table's Columns and Filters panels use it; `usePopoverPanel`
|
|
1938
|
+
lost its own height cap. New `showPanel` opens a panel scrolled to the top, and a hovered menu item no longer scrolls
|
|
1939
|
+
its list. Placing an open panel again (on every scroll) no longer throws its list back to the top.
|
|
1940
|
+
- **A panel's padding is `--nx-popover-pad`.** `.nx-popover`'s own padding loaded after the menu's and the table
|
|
1941
|
+
panels', so a menu was 16px inside its edge, not the 4px it asked for, and the table panels 16px, not 8px; both now
|
|
1942
|
+
get theirs. `.nx-table-panel__body` is gone (`.nx-popover__scroll`).
|
|
1943
|
+
- **One thin, quiet scroll bar everywhere, the design lab's.** `Nexus.Base.css` gives every element
|
|
1944
|
+
`scrollbar-width: thin` and a thumb of the text colour at 22% on no track. The theme panel, menus and context menus,
|
|
1945
|
+
the table's Columns and Filters panels, popup bodies, the date-time panel, the chat, code blocks and tables in
|
|
1946
|
+
Markdown and rich text, and the admin page and menu drew the browser's 15px bar with a painted grey track; on
|
|
1947
|
+
Windows, Chrome and Edge now draw every bar 10px wide, as the lab does (Firefox's thin bar is 8px; Safari draws its
|
|
1948
|
+
own thin bar). The components' own bars are gone: the 8px `::-webkit-scrollbar` pill of the text area, the Markdown
|
|
1949
|
+
preview and the rich text (Chromium and WebKit ignore it once `scrollbar-width` is set), and the thin bar with a
|
|
1950
|
+
stronger thumb of the tree, the tabular form, the dropdown list and the time picker. The data table's rows keep
|
|
1951
|
+
theirs (`Nexus.Table.css`) until a later change, and the time picker's columns still hide the thumb until the
|
|
1952
|
+
pointer or the keys are in them. `--nx-field-scrollbar`, `--nx-field-scrollbar-hover`, `--nx-dropdown-scrollbar` and
|
|
1953
|
+
`--nx-time-scrollbar` are gone; the text area's resize grip reads `--nx-field-grip`. Room kept for a bar narrows with
|
|
1954
|
+
it: every `scrollbar-gutter: stable` (the admin page and its bar, whose right edge moves from 27 to 22px off the
|
|
1955
|
+
window, the chat thread, the dropdown list) and the padding a modal popup keeps for the page's bar
|
|
1956
|
+
(`--nx-scrollbar-gap`, measured).
|
|
1957
|
+
- **A form whose values load shows its real inputs, disabled, instead of placeholders.** A `Crud`'s Edit popup, an
|
|
1958
|
+
`ApiForm` with `loadValue` and a `Form` with `loading` have their final size from the first frame, and the values
|
|
1959
|
+
land in the same inputs, as a load (no "Unsaved changes", no "Discard your changes?"). While loading, `FormStore`
|
|
1960
|
+
takes nothing typed, even from an input whose `disabled` was taken off by hand, and draws such an input again from
|
|
1961
|
+
the store when loading ends; a submit already waited. The `.nx-form__skeleton*` classes and their animation are gone.
|
|
1962
|
+
- **The form's status line keeps its one line when empty, and a form's bar fills a popup's foot**, so the status sits
|
|
1963
|
+
beside the buttons: "Loading…" or "Unsaved changes" coming and going no longer moves the bar, and a `Crud` popup
|
|
1964
|
+
lost the empty 12px row it had above its buttons.
|
|
1965
|
+
- **`Avatar`** (`nexus-shared/client`): a person's round picture, or their initials on a soft tone without one or
|
|
1966
|
+
when it fails to load. `size` `sm`/`md`/`lg` or `--nx-avatar-size`; `onChangePicture` makes it a button with a
|
|
1967
|
+
camera over it on hover. The chat's and the dropdown's avatars are now this one: `ChatAvatar` and
|
|
1968
|
+
`DropdownAvatar` still exist and draw it, `.nx-chat-avatar` is gone (`.nx-avatar`), `.nx-dropdown-avatar` stays
|
|
1969
|
+
as the dropdown's size, and `--nx-chat-avatar` is `--nx-avatar-size`.
|
|
1970
|
+
- **`Button` `dot`**: a small dot on the corner, for something new behind it such as unread notifications; `"primary"`
|
|
1971
|
+
marks a state that is on, such as filters.
|
|
1972
|
+
- **`Menu`: a control inside a heading is chosen like a row.** An entry `{ heading: <node> }` may hold a button with
|
|
1973
|
+
`role="menuitem"`: the arrow keys walk it with the rows, and a click on it closes the menu. A menu opened from the
|
|
1974
|
+
keyboard still starts on its first row.
|
|
1975
|
+
- **The data table is one frame**: the bar, the rows and the footer sit in one border (`.nx-table` carries it now,
|
|
1976
|
+
not `.nx-table__frame`), with a line under the bar and the footer as the bottom band (`--nx-footbar`, a new
|
|
1977
|
+
token). An empty bar takes no room. Column headings are 12px, weight 650.
|
|
1978
|
+
- **`DataTable` `add`**: the table's own Add, first in the bar. With `excel`, it is a split button whose arrow holds
|
|
1979
|
+
Download template, Import from a spreadsheet and Export to Excel, which then leave the options menu. `Crud`
|
|
1980
|
+
passes its Add this way.
|
|
1981
|
+
- **The data table's bar is one line on a wide table, in a new order**: the Add section (`add`, then the page's
|
|
1982
|
+
`actions`) and the lists (`toolbar`) on the left; Reload, the options menu (⋮) and the search, rows per page inside
|
|
1983
|
+
it, on the right. It was the lists, the search, Reload, ⋮, `actions` and Add, wrapping where they fell: at 1126px
|
|
1984
|
+
on the admin shell the bar broke into three ragged rows. A narrower table now moves whole parts, by its own width
|
|
1985
|
+
(container queries on `.nx-table`): under 670px the search takes the top line (with nothing on the left, it leads
|
|
1986
|
+
the buttons' line), under 375px Reload moves to the end of ⋮ (`DataTable` asks the hidden button, so the width is
|
|
1987
|
+
written once, in the stylesheet), and under 325px, beside lists, the Add section takes the last line with its buttons
|
|
1988
|
+
sharing it. On the admin shell those are windows of 695, 400 and 350px. A bar wider than those widths expect (a
|
|
1989
|
+
long Add label, the page's own buttons, cozy density) is squeezed into the next step as soon as a line would break
|
|
1990
|
+
(`data-squeeze` on `.nx-table__bar`, set a frame after a size change). A `toolbar` or `actions` that renders
|
|
1991
|
+
nothing takes no room and decides no step, and a bar left with nothing else is hidden. New classes
|
|
1992
|
+
`.nx-table__bar-add` (Add and `actions`) and `.nx-table__reload`; the search is now a child of `.nx-table__bar` after
|
|
1993
|
+
`.nx-table__bar-end`, not inside it, and grows to 19rem from a 12rem basis.
|
|
1994
|
+
- **Columns opens from the options menu (⋮)**, as its first item; the bar has no Columns button. Reload, Columns
|
|
1995
|
+
and the options button are ghost buttons. `TableColumnsPanel` takes an `anchor` and an `open()` handle for this.
|
|
1996
|
+
- **`TabButtons` `labels="auto"`**: every label while its container (a data table) is at least 900px wide, only the
|
|
1997
|
+
chosen one's below. `Crud`'s lists use it.
|
|
1998
|
+
- **`DataTable` `selectBy`**: while rows are selected, their count opens a menu that selects again by what the rows
|
|
1999
|
+
are: All on this page, then each `{ label, icon, test }`, which leaves exactly the matching rows on the page
|
|
2000
|
+
selected. `Crud`'s main list offers Pinned, Not pinned, Flagged and Not flagged.
|
|
2001
|
+
- **`SplitButton` `fixed`**: the main button never changes; the menu holds other ways to do it, run when picked.
|
|
2002
|
+
- **`DataTable` `filterInputs`: an advanced filter.** Input params, each named after the column it filters, become
|
|
2003
|
+
**Filters** in the options menu (⋮): a small form under the button, applied as it changes. The values are
|
|
2004
|
+
`TableQuery.filters` (`TableStore.setFilters` sets them, `{}` clears); a client table matches them in `queryRows`
|
|
2005
|
+
(a fifth argument), and a server gets them flat by name through `CrudBackend.pageRequest`'s third argument, as
|
|
2006
|
+
`filterRequest` writes them (a date range as `{name}From`/`{name}Before`). A `CrudBackend` or loader of your own
|
|
2007
|
+
ignores them until it reads that argument or `query.filters`. Also new: `activeFilters`, `asFilterInput`,
|
|
2008
|
+
`filterMatcher`, `filterNames`; `usePopoverPanel` and `PopoverPanelHandle` (`nexus-shared/client`), the panel
|
|
2009
|
+
Filters and Columns share; `followAnchor`'s second argument, a panel whose size it follows too.
|
|
2010
|
+
- **Renamed: `.nx-columns__head`, `__title`, `__count` and `__foot` are `.nx-table-panel__*`**, shared by both panels.
|
|
2011
|
+
An override of the old names no longer applies.
|
|
2012
|
+
|
|
2013
|
+
## 3.0.0
|
|
2014
|
+
|
|
2015
|
+
**Breaking. This changes the value of exported constants, and `2.0.0` is on npm with the old ones.** Publishing it
|
|
2016
|
+
as a patch or a minor would hand `^2.0.0` consumers different numbers under a compatible-looking range, which is the
|
|
2017
|
+
worst way to ship this, so it takes a major. The owner chose the number on 2026-10-02, together with `nexus-public`
|
|
2018
|
+
and `nexus-admin`, which both depend on this package and so move to `3.0.0` with it; their `nexus-shared` range is
|
|
2019
|
+
now `^3.0.0`, because a bump of this one alone would leave them pulling the old, wrong constants.
|
|
2020
|
+
|
|
2021
|
+
**`nexus-icons` must be installed at `1.1.0` or newer**, and the dependency range here says so. The chat components
|
|
2022
|
+
added in this version use `iconChecks`, `iconCornerUpLeft`, `iconMessageCircle`, `iconPaperclip`, `iconSend` and
|
|
2023
|
+
`iconUserPlus`, none of which exist in `nexus-icons@1.0.0`: with the old range a fresh install resolved to `1.0.0`
|
|
2024
|
+
and the build failed on six missing exports.
|
|
2025
|
+
|
|
2026
|
+
### `NexusModule` — `Chat` added, and every module after it moved up one bit
|
|
2027
|
+
|
|
2028
|
+
The backend's `EModules` gained `Chat` at bit 6, and everything after it shifted. `NexusModule` follows it exactly.
|
|
2029
|
+
A permission is a bitmask of these flags, so a package that kept the old numbers would grant and deny the wrong
|
|
2030
|
+
modules without any error to show for it.
|
|
2031
|
+
|
|
2032
|
+
| Module | Was | Now |
|
|
2033
|
+
|---|---|---|
|
|
2034
|
+
| `None` | 0 | 0 |
|
|
2035
|
+
| `Migrations` | 1 | 1 |
|
|
2036
|
+
| `Configuration` | 2 | 2 |
|
|
2037
|
+
| `Identity` | 4 | 4 |
|
|
2038
|
+
| `Enterprise` | 8 | 8 |
|
|
2039
|
+
| `Drive` | 16 | 16 |
|
|
2040
|
+
| `Message` | 32 | 32 |
|
|
2041
|
+
| **`Chat`** | *(did not exist)* | **64** |
|
|
2042
|
+
| `Accounting` | 64 | **128** |
|
|
2043
|
+
| `Report` | 128 | **256** |
|
|
2044
|
+
| `Requisition` | 256 | **512** |
|
|
2045
|
+
| `Tenant` | 512 | **1024** |
|
|
2046
|
+
| `Subscription` | 1024 | **2048** |
|
|
2047
|
+
| `Reservation` | 2048 | **4096** |
|
|
2048
|
+
| `School` | 4096 | **8192** |
|
|
2049
|
+
| `Pharmacy` | 8192 | **16384** |
|
|
2050
|
+
| `Staffing` | 16384 | **32768** |
|
|
2051
|
+
| `Portfolio` | 32768 | **65536** |
|
|
2052
|
+
|
|
2053
|
+
`NEXUS_MODULES` gained the matching entry (`key: "chat"`, `label: "Chat"`, `env: "API_CHAT"`), so `Chat` can be
|
|
2054
|
+
looked up, proxied and named in messages like every other module.
|
|
2055
|
+
|
|
2056
|
+
**What to check when you upgrade.** Code that uses the names (`NexusModule.Reservation`) needs nothing. Anything
|
|
2057
|
+
that stored, transmitted or hard-coded a **number** has to be revisited:
|
|
2058
|
+
|
|
2059
|
+
- permission masks saved in a database, a cache, a token claim or a settings file;
|
|
2060
|
+
- proxy URLs written with a number in place of a key (`/api/app/2048/…` is now `/api/app/4096/…`, and `reservation`
|
|
2061
|
+
by key has always worked and still does);
|
|
2062
|
+
- a backend that has not taken the `Chat` shift yet — both sides move together or neither does.
|
|
2063
|
+
|
|
2064
|
+
### `NEXUS_FLAGGED_STATUS` — 15 → 25
|
|
2065
|
+
|
|
2066
|
+
The owner settled the record-status bands, and `ERecordStatus.Flagged` is seeded at **25**. `NexusCrudBackend`
|
|
2067
|
+
decides whether a row is flagged by comparing `recordStatus` to this constant, so a package on 15 while the backend
|
|
2068
|
+
writes 25 shows the wrong flag on every row, and flagging one writes a status the backend does not recognise.
|
|
2069
|
+
|
|
2070
|
+
The bands, for anything that reasons about them:
|
|
2071
|
+
|
|
2072
|
+
| Band | Range |
|
|
2073
|
+
|---|---|
|
|
2074
|
+
| Active | 0–39 |
|
|
2075
|
+
| — Active account | 0–29 |
|
|
2076
|
+
| — Flag | **25–29** |
|
|
2077
|
+
| — Pending, unverified, in review, approval required | 30–39 |
|
|
2078
|
+
| Trash | 40–59 |
|
|
2079
|
+
| Delete | 60 and above |
|
|
2080
|
+
|
|
2081
|
+
`NEXUS_ACTIVE_STATUS` stays 0, and the `recordStatus < 40` reading of "a live record" that the rest of the code
|
|
2082
|
+
already assumes stays right.
|
|
2083
|
+
|
|
2084
|
+
Note that **the flag is a range, 25–29, and this release still checks a single value.** Only the seeded member at 25
|
|
2085
|
+
has a meaning today, and the bands are about to become configurable per project, so a range-aware check belongs with
|
|
2086
|
+
that work rather than here. Do not read the single value as the final answer.
|
|
2087
|
+
|
|
2088
|
+
### Removed: the archive
|
|
2089
|
+
|
|
2090
|
+
The owner removed the archive on 2026-09-30: a record is live, in the trash, or deleted, and the way back from
|
|
2091
|
+
deleted lands it in the trash. The second put-away list and its second recovery path were the only reason the CRUD
|
|
2092
|
+
had both Restore and Recover, and the bands close up behind it — **Trash is 40–59 and Delete 60 and above**, so
|
|
2093
|
+
nothing stored keeps a meaning it no longer has. `recordStatus < 40` still reads "a live record".
|
|
2094
|
+
|
|
2095
|
+
What goes, all of it breaking for anything that named it:
|
|
2096
|
+
|
|
2097
|
+
- `CrudMode` loses `"archive"`, so a CRUD shows All, Flagged, Trash and Deleted.
|
|
2098
|
+
- `CrudRowActionName` and `CrudMassActionName` lose `"archive"` and `"restore"`. Recover, the trash's own way back,
|
|
2099
|
+
is untouched: a CRUD that offered both now offers one.
|
|
2100
|
+
- `CrudEndpoints` loses `archive` and `restore`; `apiController` stops naming `Archive`, `Restore`, `MassArchive`,
|
|
2101
|
+
`MassRestore`, `ArchiveListing` and `ArchivePagination`, and `lists`/`pages` lose their `archive` key.
|
|
2102
|
+
- `MessageActionName` loses `"archive"`, and the message builder its words and its "It can be restored from the
|
|
2103
|
+
archive." confirmation detail. `"restore"` stays in the vocabulary: it is a plain verb, not the archive's.
|
|
2104
|
+
|
|
2105
|
+
**What to check when you upgrade.** A CRUD that listed `"archive"` in `modes` or `actions` no longer compiles,
|
|
2106
|
+
which is the point — the endpoint behind it is gone from the backend too. A backend that still answers
|
|
2107
|
+
`ArchiveListing` is not asked for it any more.
|
|
2108
|
+
|
|
2109
|
+
### Added: the chat
|
|
2110
|
+
|
|
2111
|
+
A discussion on any record — a leave request the manager and a co-worker talk through, a purchase order, an
|
|
2112
|
+
applicant, a tender — behind one icon that says whether anything is waiting. Nothing that does not use it changes.
|
|
2113
|
+
|
|
2114
|
+
- A conversation is named by three values, `ChatScope`: the **domain** (which kind of record), the **parent id** (the
|
|
2115
|
+
record) and the **user id** (who is reading). The domain is the app's own enum, given either as a template
|
|
2116
|
+
(`<Chat<EDomainTypes> …>`) or once in `ChatRegister`, the same way `ApiRegister` names the module enum. Without one
|
|
2117
|
+
it is a plain number.
|
|
2118
|
+
- **No domain numbers are defined here.** They belong to the backend's `EDomainTypes`, and the warning at the top of
|
|
2119
|
+
this file applies to them in full. `defineChatDomains` only gives an app's domains their names on screen.
|
|
2120
|
+
- `ChatButton` is an icon in three looks: quiet with no messages, the accent on its tint once there are messages, and
|
|
2121
|
+
the accent filled with an unread badge once some are unread. One size in all three, with the badge over the corner,
|
|
2122
|
+
so a column of them in a table does not move as the counts arrive.
|
|
2123
|
+
- `ChatSummaryCache` fills every icon that mounted in the same moment with **one call per domain**. A list whose rows
|
|
2124
|
+
already carry their counts passes `summary` and makes no call at all.
|
|
2125
|
+
- `Chat` is the icon with the conversation in a popup; `ChatPanel` is the conversation on its own, for a record's own
|
|
2126
|
+
page. Messages are **Markdown**: written in `MarkdownEditor` and drawn by `MarkdownView`. Enter sends and
|
|
2127
|
+
Shift+Enter starts a line, except in a list or a code fence, where the editor keeps Enter; `sendKey="modifier"`
|
|
2128
|
+
moves sending to Ctrl+Enter.
|
|
2129
|
+
- One mark where the time sits says how far a message of your own has got: a spinner while sending, a tick once the
|
|
2130
|
+
server has it, two once `deliveredTo` holds somebody, two in the accent once `readBy` does.
|
|
2131
|
+
- Where the data comes from is one interface, `ChatSource`. `apiChatSource` goes through the app's `ChatBackend`
|
|
2132
|
+
(`NexusChatBackend` by default); `localChatSource` keeps a conversation in memory for a demo or a test.
|
|
2133
|
+
- **The Nexus endpoints are the shape the Chat module is expected to take, not an API that answers today.**
|
|
2134
|
+
`NexusModule.Chat` is a number and a proxy key; the controller is not built. Each difference, when it lands, is one
|
|
2135
|
+
override on `NexusChatBackend`.
|
|
2136
|
+
- `check:themes` gained a rule: body text on the primary tint, which is a message bubble of your own. Every theme
|
|
2137
|
+
clears AAA except catppuccin light at 6.84, so it is held to the AA 4.5 that applies to body text.
|
|
2138
|
+
- **A record holds many conversations.** `ChatScope` names the record; each conversation in it is a
|
|
2139
|
+
`ChatConversation` with its own id and a type, `one-to-one` or `group`. `ChatPlaceSource` lists them and starts
|
|
2140
|
+
them, `useChatPlace` is the hook, and `apiChatPlace` is the API one. A panel given a `source` still shows that one
|
|
2141
|
+
conversation and no switch, so nothing that used the first round changes.
|
|
2142
|
+
- The panel's body has three views — the thread, the list of conversations, and New chat — and the bar moves between
|
|
2143
|
+
them. With several conversations the title is the switch; with one it is just the title.
|
|
2144
|
+
- Starting a chat never asks for the type: one person makes a one-to-one, more make a group, and a name makes a
|
|
2145
|
+
group either way. A one-to-one with somebody who already has one is expected to come back rather than be made
|
|
2146
|
+
twice; `localChatPlace` keeps that rule.
|
|
2147
|
+
- `@` at the start of a word offers the people in the conversation, narrows as it is typed, and owns Enter while it
|
|
2148
|
+
is open. A mention is plain `@Name`, or a Markdown link with `mentionHref`; either way the ids travel in the
|
|
2149
|
+
draft's `mentions`.
|
|
2150
|
+
- `ChatSnapshot` gained `loaded`: a store that has read nothing yet holds empty counts, and a panel must not write
|
|
2151
|
+
those over what a conversation list already knows.
|
|
2152
|
+
|
|
2153
|
+
### Added: several at once, `TabButtons multiple`
|
|
2154
|
+
|
|
2155
|
+
`TabButtons` picks one option; with `multiple` it picks several, so a list of choices that must be seen all at once
|
|
2156
|
+
(the weekdays a rule runs on, the letters or symbols a pattern allows) needs no second control. Nothing that does
|
|
2157
|
+
not pass it changes.
|
|
2158
|
+
|
|
2159
|
+
- `value` is an array and `onChange` hands back the chosen values **in option order**, whatever order they were
|
|
2160
|
+
clicked in. Values that match no option are ignored and never handed back.
|
|
2161
|
+
- The options are native checkboxes in a `role="group"`: Tab moves between them and Space toggles one. The group
|
|
2162
|
+
still needs `aria-label` or `aria-labelledby`.
|
|
2163
|
+
- There is no sliding indicator: each chosen option is raised with the indicator's own colors, border and shadow,
|
|
2164
|
+
and keeps the pointer, since a click takes it away again. An option that is not chosen sits flat on the track.
|
|
2165
|
+
- The options **wrap** onto more rows when they do not fit, each row as tall as a one-row track's options, and an
|
|
2166
|
+
option is never narrower than it is tall, so a row of single letters lines up. On a touch screen
|
|
2167
|
+
(`pointer: coarse`) every option is a medium control's height: 26 letters on a phone are 34px targets, not 22px.
|
|
2168
|
+
- Made and checked for the `nav` design (`/demo/tab-buttons`, "Several at once"); `design="list"` with `multiple`
|
|
2169
|
+
renders checkboxes too, but has not been checked yet.
|
|
2170
|
+
|
|
2171
|
+
### Added: inputs that follow others, `dependsOn`, and a row's own params, `cellParams`
|
|
2172
|
+
|
|
2173
|
+
Two optional keys, added 2026-09-26 for the builder's class form, whose company model shows only when the class has a
|
|
2174
|
+
company or a branch and whose type arguments offer a key type in one row and a model in the next. Nothing that does
|
|
2175
|
+
not pass them changes.
|
|
2176
|
+
|
|
2177
|
+
- **`dependsOn: ["hasCompany"]`** on an input's params (Form only). When a named input's value changes — typed, or
|
|
2178
|
+
set from code — this input draws again **without mounting again** (nothing typed in it is lost) and its params are
|
|
2179
|
+
read afresh, so getters for `hidden`, `readOnly`, `readOnlyMessage`, `label`, `hint` or `options` answer for the
|
|
2180
|
+
values as they are now. Its rule is asked again too, so a `required` a getter switched on counts at once. An input
|
|
2181
|
+
that names any is asked `hidden` each time it draws, so it can come and go; one that names none is asked once, when
|
|
2182
|
+
the form draws, as before. Name the input itself to follow its own typing.
|
|
2183
|
+
- **`cellParams(row, name, index)`** on `TabularForm` and `GroupForm`: the params of one cell that differ from its
|
|
2184
|
+
column's, such as a picker's `options` and `selectedOptions`. Asked as the row draws; return nothing for the
|
|
2185
|
+
column's own params, and the **same object** for the same answer (the merged params are kept per answer, so the cell
|
|
2186
|
+
does not draw again for nothing). Getters on either side stay getters. Rules still come from the column.
|
|
2187
|
+
|
|
2188
|
+
### Added: a generated column in a tabular or group form
|
|
2189
|
+
|
|
2190
|
+
Two optional props on `TabularForm` and `GroupForm`, for a column whose values a row's **position** decides rather
|
|
2191
|
+
than the user — a bitwise enum's value, a line number, a running total. Nothing that does not pass them changes.
|
|
2192
|
+
|
|
2193
|
+
- `deriveRows(rows)` returns one patch per row and runs after every change, including a row being added, removed,
|
|
2194
|
+
duplicated or **moved**. The patches are written as code writes, so only the cells whose value really changed
|
|
2195
|
+
mount again and focus stays on the row's own menu: a keyboard can move a row twice in a row.
|
|
2196
|
+
- `cellReadOnly(row, name, index)` is asked for every cell as it renders and returns the reason a cell is
|
|
2197
|
+
generated. The cell is locked and shows that reason as its read-only note. Because it is asked per render, a
|
|
2198
|
+
column can lock and unlock while the form around it changes, which params cannot do — they are fixed once a form
|
|
2199
|
+
opens.
|
|
2200
|
+
|
|
2201
|
+
### Added: rows nobody may change, `rowReadOnly`
|
|
2202
|
+
|
|
2203
|
+
One optional prop on `TabularForm` and `GroupForm`, for rows the user did not write and may not change — the fields
|
|
2204
|
+
a class inherits from its parent, shown first in the same grid as its own. Nothing that does not pass it changes.
|
|
2205
|
+
|
|
2206
|
+
- `rowReadOnly(row, index)` returns the reason (or `true`). Every cell of such a row is read-only with that reason
|
|
2207
|
+
as its note, the row's menu in a tabular form offers nothing (no insert, duplicate, move or remove), and the row
|
|
2208
|
+
below it cannot move up past it. Nothing reorders the rows: keep the locked ones first.
|
|
2209
|
+
- A locked row says nothing about its columns: the column-header lock is worked out from the other rows in view, so
|
|
2210
|
+
a grid of inherited rows and one own row does not put a lock on every header.
|
|
2211
|
+
|
|
2212
|
+
### Added: a panel beside a CRUD form, `form.aside`
|
|
2213
|
+
|
|
2214
|
+
An Add or Edit popup can show a panel next to its form: a preview of what a save would produce, a summary, a help
|
|
2215
|
+
panel. It takes a node, or a function handed `{ form, action, row }` — `form` being that popup's own `FormStore`, so
|
|
2216
|
+
a panel follows what is typed through `subscribeValue()` and the form around it never re-renders for it.
|
|
2217
|
+
|
|
2218
|
+
The two are columns above 1000px, each scrolling on its own so a long form cannot push the panel off the screen, and
|
|
2219
|
+
they stack below that with the **form first**, in the DOM as well as on the screen. `--nx-crud-aside-width` sets the
|
|
2220
|
+
panel's share (default `clamp(20rem, 38%, 44rem)`). Pair it with a wide `form.size` — `xl` or `full`; a panel in a
|
|
2221
|
+
560px popup leaves the form nothing. A CRUD that passes no `aside` renders exactly what it did before, wrapper and
|
|
2222
|
+
all.
|
|
2223
|
+
|
|
2224
|
+
### `TabularForm` reworked: one row control, rows that do not move, one lock per column
|
|
2225
|
+
|
|
2226
|
+
The owner called the tabular design bad, from a screenshot of an 18-row table with one generated column. Every fault
|
|
2227
|
+
was in this component rather than in the page that used it, so every fix is here. Nothing needs a new prop, and a
|
|
2228
|
+
table that passed nothing new looks different — deliberately.
|
|
2229
|
+
|
|
2230
|
+
**Breaking, visually and for tests.** Two things a consumer may be reaching for have moved:
|
|
2231
|
+
|
|
2232
|
+
- **The remove column is gone.** Remove was a column of unlabelled `×` buttons at the end, beside a row menu that
|
|
2233
|
+
already held Remove: two ways to act on one row, and a whole column spent on one of them. Now a row has one
|
|
2234
|
+
control, the menu beside its number, holding Insert above, Insert below, Duplicate, Move up, Move down and Remove.
|
|
2235
|
+
`removable` still decides whether Remove is offered. A test that clicked a button named `Remove <row>` opens the
|
|
2236
|
+
row's menu instead: `Row 2 actions`, then the `Remove` item. `.nx-tabular__end` and `.nx-tabular__remove` are gone
|
|
2237
|
+
from the DOM and the stylesheet; so is `--nx-tabular-end`, replaced by `--nx-tabular-menu` (the menu's slot width).
|
|
2238
|
+
- **The row number no longer opens the menu.** It used to *become* the menu on hover: the number faded out and dots
|
|
2239
|
+
faded in over it, so the numbering looked broken and the grid felt unstable as the pointer crossed it. The number
|
|
2240
|
+
now stays where it is, right-aligned, at every moment, and the menu button sits beside it in its own space,
|
|
2241
|
+
always present. The number column is that much wider.
|
|
2242
|
+
|
|
2243
|
+
**Fixed, with no change to how a table is written:**
|
|
2244
|
+
|
|
2245
|
+
- **Every row is one line high, whatever it holds.** The row's grid track was sized by its tallest cell, so a single
|
|
2246
|
+
`textarea` column of `rows: 2` made every cell of its row 20px taller than the row's own box — drawing the cells,
|
|
2247
|
+
their edges and their focus rings over the rows below. One row of content therefore looked taller than its
|
|
2248
|
+
neighbours, and a focus ring bled outside its cell. The row now has one track exactly its own height, cells keep
|
|
2249
|
+
what they hold inside their bounds, and a `textarea` fills its cell and scrolls inside it.
|
|
2250
|
+
- **No resize grips in cells.** A `textarea` in a cell took `resize: vertical` from the multi-line input rule, so
|
|
2251
|
+
every cell of such a column offered a grip that would break the alignment of its row. They are `resize: none` now.
|
|
2252
|
+
- **A filled cell and an empty one are drawn the same.** Same consequence as the above: only the cell with text was
|
|
2253
|
+
tall enough to show what the overflow was doing.
|
|
2254
|
+
- **`maxHeight` comes down to whole rows.** The scroller used to end wherever the number fell, cutting the last row
|
|
2255
|
+
in half, which reads as broken rather than as scrollable. The component measures its header, its frame and its
|
|
2256
|
+
real row height and caps itself just under what was asked for. A number too small for one row is left alone.
|
|
2257
|
+
- **A generated column says so once.** `cellReadOnly` put a lock in every cell of the column it locked — eighteen
|
|
2258
|
+
locks for one fact. The lock is now on the **column header**, with the reason as its title, and the cells keep the
|
|
2259
|
+
read-only tint and their own reason, which still shows under the table when one is used. A lock that really is per
|
|
2260
|
+
row (some rows locked, not all) leaves the header alone and is drawn per cell as before.
|
|
2261
|
+
- **Less chrome.** The frame is `--nx-border` rather than `--nx-border-strong`: the sheet draws every line it needs
|
|
2262
|
+
inside itself, and in a popup or a panel the strong edge was a box around a box around boxes.
|
|
2263
|
+
|
|
2264
|
+
## 2.0.0
|
|
2265
|
+
|
|
2266
|
+
The rewrite: compiled `dist` with the entry points `.`, `/client`, `/server` and `/styles.css`, in place of 1.x's
|
|
2267
|
+
raw TypeScript from `src` with `.`, `/client` and `/interface`. A major version, so nobody on `^1.1.21` is moved
|
|
2268
|
+
into a different API.
|