nexus-shared 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +1564 -0
  2. package/README.md +1705 -1123
  3. package/dist/Client.Index.d.ts +14 -0
  4. package/dist/Client.Index.js +14 -0
  5. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  6. package/dist/Components/Documents/DocumentFrame.js +83 -0
  7. package/dist/Components/Documents/Menu.d.ts +4 -0
  8. package/dist/Components/Documents/Menu.js +40 -24
  9. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  10. package/dist/Components/Documents/PrintDocument.js +62 -0
  11. package/dist/Components/Documents/Tabs.d.ts +59 -0
  12. package/dist/Components/Documents/Tabs.js +70 -0
  13. package/dist/Components/Forms/ApiForm.js +19 -2
  14. package/dist/Components/Forms/Crud.d.ts +2 -2
  15. package/dist/Components/Forms/Crud.js +576 -243
  16. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  17. package/dist/Components/Forms/CrudActions.js +184 -0
  18. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  19. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  20. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  21. package/dist/Components/Forms/CrudParts.js +55 -5
  22. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  23. package/dist/Components/Forms/CrudRowForms.js +114 -0
  24. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  25. package/dist/Components/Forms/CrudScopes.js +76 -0
  26. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  27. package/dist/Components/Forms/CrudTree.js +363 -0
  28. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  29. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  30. package/dist/Components/Forms/ExcelImport.js +3 -3
  31. package/dist/Components/Forms/Form.d.ts +4 -4
  32. package/dist/Components/Forms/Form.js +123 -17
  33. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  34. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  35. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  36. package/dist/Components/Forms/RecordFlows.js +282 -0
  37. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  38. package/dist/Components/Forms/SubmitForm.js +195 -10
  39. package/dist/Components/Inputs/CheckBox.js +1 -1
  40. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  41. package/dist/Components/Inputs/ChoiceField.js +28 -2
  42. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  43. package/dist/Components/Inputs/Dropdown.js +5 -5
  44. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  45. package/dist/Components/Inputs/DropdownParts.js +16 -4
  46. package/dist/Components/Inputs/Field.d.ts +7 -1
  47. package/dist/Components/Inputs/Field.js +2 -2
  48. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  49. package/dist/Components/Inputs/GroupForm.js +10 -11
  50. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  51. package/dist/Components/Inputs/InputRenderer.js +37 -32
  52. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  53. package/dist/Components/Inputs/PictureInput.js +70 -0
  54. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  55. package/dist/Components/Inputs/RadioGroup.js +8 -3
  56. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  57. package/dist/Components/Inputs/RowsInput.js +18 -3
  58. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  59. package/dist/Components/Inputs/TabularForm.js +29 -29
  60. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  61. package/dist/Components/Inputs/TextArea.js +11 -2
  62. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  63. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  64. package/dist/Components/Inputs/TreeView.d.ts +53 -2
  65. package/dist/Components/Inputs/TreeView.js +147 -13
  66. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  67. package/dist/Components/Layouts/MessageBox.js +56 -7
  68. package/dist/Components/Layouts/Popup.d.ts +13 -6
  69. package/dist/Components/Layouts/Popup.js +37 -9
  70. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  71. package/dist/Components/Layouts/PopupDock.js +2 -1
  72. package/dist/Components/Layouts/PopupHost.js +5 -4
  73. package/dist/Components/Viewers/DataTable.js +150 -48
  74. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  75. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  76. package/dist/Components/Viewers/DataTableParts.d.ts +21 -5
  77. package/dist/Components/Viewers/DataTableParts.js +26 -14
  78. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  79. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  80. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  81. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  82. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  83. package/dist/Components/Viewers/RecordCards.js +509 -0
  84. package/dist/Helpers/BusyHooks.d.ts +6 -0
  85. package/dist/Helpers/BusyHooks.js +39 -0
  86. package/dist/Helpers/CrudBackend.d.ts +20 -2
  87. package/dist/Helpers/CrudBackend.js +46 -2
  88. package/dist/Helpers/CrudColumns.d.ts +53 -6
  89. package/dist/Helpers/CrudColumns.js +98 -7
  90. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  91. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  92. package/dist/Helpers/CrudHelpers.d.ts +81 -2
  93. package/dist/Helpers/CrudHelpers.js +195 -0
  94. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  95. package/dist/Helpers/CrudNarrowPick.js +157 -0
  96. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  97. package/dist/Helpers/CrudRowForms.js +43 -0
  98. package/dist/Helpers/CrudScopes.d.ts +32 -0
  99. package/dist/Helpers/CrudScopes.js +79 -0
  100. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  101. package/dist/Helpers/DropdownHelpers.js +6 -2
  102. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  103. package/dist/Helpers/ExcelHelpers.js +12 -11
  104. package/dist/Helpers/FileAdapter.d.ts +78 -0
  105. package/dist/Helpers/FileAdapter.js +102 -0
  106. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  107. package/dist/Helpers/FilePrimaries.js +138 -0
  108. package/dist/Helpers/FormConfirm.d.ts +32 -0
  109. package/dist/Helpers/FormConfirm.js +40 -0
  110. package/dist/Helpers/FormStore.d.ts +90 -5
  111. package/dist/Helpers/FormStore.js +318 -22
  112. package/dist/Helpers/InputConditions.d.ts +50 -0
  113. package/dist/Helpers/InputConditions.js +168 -0
  114. package/dist/Helpers/InputMemory.d.ts +106 -0
  115. package/dist/Helpers/InputMemory.js +385 -0
  116. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  117. package/dist/Helpers/InputNarrowing.js +67 -0
  118. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  119. package/dist/Helpers/InputParamsHelpers.js +157 -74
  120. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  121. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  122. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  123. package/dist/Helpers/NumberHelpers.js +126 -0
  124. package/dist/Helpers/Permissions.js +2 -1
  125. package/dist/Helpers/QrCode.d.ts +26 -0
  126. package/dist/Helpers/QrCode.js +463 -0
  127. package/dist/Helpers/RecordCards.d.ts +205 -0
  128. package/dist/Helpers/RecordCards.js +378 -0
  129. package/dist/Helpers/RequestCache.d.ts +14 -2
  130. package/dist/Helpers/RequestCache.js +22 -3
  131. package/dist/Helpers/RowsStore.d.ts +37 -2
  132. package/dist/Helpers/RowsStore.js +128 -10
  133. package/dist/Helpers/ServerPreview.d.ts +69 -0
  134. package/dist/Helpers/ServerPreview.js +99 -0
  135. package/dist/Helpers/TableColumns.js +4 -3
  136. package/dist/Helpers/TableExport.d.ts +7 -1
  137. package/dist/Helpers/TableExport.js +9 -3
  138. package/dist/Helpers/TableFilters.d.ts +1 -1
  139. package/dist/Helpers/TableFilters.js +24 -23
  140. package/dist/Helpers/TableHelpers.d.ts +23 -2
  141. package/dist/Helpers/TableHelpers.js +51 -10
  142. package/dist/Helpers/TableJoins.d.ts +13 -0
  143. package/dist/Helpers/TableJoins.js +34 -0
  144. package/dist/Helpers/TableStore.d.ts +21 -1
  145. package/dist/Helpers/TableStore.js +64 -7
  146. package/dist/Helpers/TreeStore.d.ts +81 -0
  147. package/dist/Helpers/TreeStore.js +365 -0
  148. package/dist/Interfaces/CrudInterfaces.d.ts +465 -5
  149. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  150. package/dist/Interfaces/FileInterfaces.js +4 -0
  151. package/dist/Interfaces/FormInterfaces.d.ts +391 -30
  152. package/dist/Interfaces/FormInterfaces.js +79 -1
  153. package/dist/Interfaces/InputInterfaces.d.ts +15 -3
  154. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  155. package/dist/Interfaces/RecordInterfaces.js +1 -0
  156. package/dist/Interfaces/TableInterfaces.d.ts +74 -1
  157. package/dist/Services/ExcelApi.js +5 -0
  158. package/dist/Services/FileApi.d.ts +13 -0
  159. package/dist/Services/FileApi.js +29 -0
  160. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  161. package/dist/Services/ImageEditorLoader.js +51 -0
  162. package/dist/Services/JoinSources.d.ts +37 -5
  163. package/dist/Services/JoinSources.js +90 -14
  164. package/dist/Services/ModuleImport.d.ts +7 -0
  165. package/dist/Services/ModuleImport.js +22 -0
  166. package/dist/Shared.Index.d.ts +16 -0
  167. package/dist/Shared.Index.js +16 -0
  168. package/package.json +7 -2
  169. package/src/Styles/Nexus.Avatar.css +6 -5
  170. package/src/Styles/Nexus.Button.css +10 -4
  171. package/src/Styles/Nexus.Choice.css +16 -0
  172. package/src/Styles/Nexus.Crud.css +383 -9
  173. package/src/Styles/Nexus.Dropdown.css +8 -0
  174. package/src/Styles/Nexus.Files.css +410 -0
  175. package/src/Styles/Nexus.Form.css +5 -2
  176. package/src/Styles/Nexus.Index.css +4 -0
  177. package/src/Styles/Nexus.Input.css +42 -0
  178. package/src/Styles/Nexus.Menu.css +10 -1
  179. package/src/Styles/Nexus.Popup.css +77 -1
  180. package/src/Styles/Nexus.Print.css +288 -0
  181. package/src/Styles/Nexus.QrCode.css +12 -0
  182. package/src/Styles/Nexus.Record.css +430 -0
  183. package/src/Styles/Nexus.Tab.Buttons.css +87 -22
  184. package/src/Styles/Nexus.Table.css +67 -5
  185. package/src/Styles/Nexus.Themes.css +12 -6
  186. package/src/Styles/Nexus.Tokens.css +8 -3
  187. package/src/Styles/Nexus.Tree.css +31 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,1570 @@ Notable changes to `nexus-shared`. The **values** of exported constants count as
4
4
  Nexus backend's own numbers, and a value that differs from the backend by one does not fail loudly — it silently
5
5
  means the wrong thing on every record and every permission check.
6
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
+
7
1571
  ## 3.2.0
8
1572
 
9
1573
  **New features, no code break.** Every export of `3.1.0` is still exported (99 are new: 72 from `nexus-shared`, 16