nexus-shared 3.2.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2359 -0
- package/README.md +1911 -1123
- package/dist/Client.Index.d.ts +27 -0
- package/dist/Client.Index.js +27 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +24 -3
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +628 -247
- package/dist/Components/Forms/CrudActions.d.ts +137 -0
- package/dist/Components/Forms/CrudActions.js +214 -0
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +37 -2
- package/dist/Components/Forms/CrudParts.js +123 -7
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +174 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
- package/dist/Components/Forms/CrudWorkspace.js +177 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +6 -5
- package/dist/Components/Forms/Form.js +134 -21
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +182 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -6
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +12 -12
- package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +75 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +15 -2
- package/dist/Components/Inputs/RowsInput.js +96 -8
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +45 -32
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +11 -2
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TreeView.d.ts +76 -2
- package/dist/Components/Inputs/TreeView.js +227 -16
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +34 -7
- package/dist/Components/Layouts/Popup.js +143 -13
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +153 -49
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
- package/dist/Components/Viewers/OwnerFiles.js +700 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- package/dist/Helpers/ApiModules.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +91 -6
- package/dist/Helpers/CrudColumns.js +148 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +116 -2
- package/dist/Helpers/CrudHelpers.js +256 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +32 -0
- package/dist/Helpers/CrudRowForms.js +76 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +158 -0
- package/dist/Helpers/FileAdapter.js +173 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +100 -5
- package/dist/Helpers/FormStore.js +368 -25
- package/dist/Helpers/InputConditions.d.ts +56 -0
- package/dist/Helpers/InputConditions.js +192 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +395 -0
- package/dist/Helpers/InputNarrowing.d.ts +50 -0
- package/dist/Helpers/InputNarrowing.js +148 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
- package/dist/Helpers/NexusDriveAdapter.js +236 -0
- package/dist/Helpers/NumberHelpers.d.ts +156 -0
- package/dist/Helpers/NumberHelpers.js +242 -0
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +14 -2
- package/dist/Helpers/RequestCache.js +22 -3
- package/dist/Helpers/RowsStore.d.ts +61 -2
- package/dist/Helpers/RowsStore.js +203 -16
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +9 -3
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +38 -4
- package/dist/Helpers/TableHelpers.js +78 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +28 -4
- package/dist/Helpers/TableStore.js +73 -11
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +156 -6
- package/dist/Helpers/TreeStore.js +547 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +183 -0
- package/dist/Interfaces/FileInterfaces.js +6 -0
- package/dist/Interfaces/FormInterfaces.d.ts +460 -32
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +22 -3
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +102 -3
- package/dist/Services/ExcelApi.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +79 -6
- package/dist/Services/JoinSources.js +132 -21
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +28 -0
- package/dist/Shared.Index.js +28 -0
- package/package.json +13 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +39 -4
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +486 -21
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +437 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +7 -0
- package/src/Styles/Nexus.Input.css +73 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +135 -1
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Tab.Buttons.css +87 -22
- package/src/Styles/Nexus.Table.css +1458 -1387
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +41 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,2365 @@ 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.1.0
|
|
8
|
+
|
|
9
|
+
**Additions only.** Everything written after `4.0.0` was published (2026-11-07 16:14 UTC; the first change after it is
|
|
10
|
+
card SUB4-component-review-fix-finish on 2026-11-09). Checked against the published `4.0.0` (`npm pack nexus-shared@4.0.0`)
|
|
11
|
+
with the four scripts in `Frontend/scripts/release-compare`, on 2026-11-11:
|
|
12
|
+
|
|
13
|
+
- **Exports** (`compare-exports.cjs`): every export of `4.0.0` is still exported; **279 are new**, 212 from
|
|
14
|
+
`nexus-shared` and 67 from `nexus-shared/client`, none from `nexus-shared/server`. Per card, as each counted its own:
|
|
15
|
+
2 are [card SUB4-component-review-fix's](#the-round-4-reviews-fixes-a-hasty-enter-a-question-read-out-a-remembered-value-kept), 4 are [card C-crud-hidden-value-after-save's](#the-row-a-save-leaves-is-what-was-sent), 58 are [card C-mail-thread's](#a-mail-thread-its-source-and-backend-and-three-params-any-page-can-use), 49 are [card C-domain-card's](#a-records-card-in-a-message-read-live-from-its-module), 18 are [card C-message-bell's](#a-message-bell-the-unread-count-on-a-top-bar-one-read-however-many-bells), 3 are [card C-mail-thread-fit's](#a-mail-threads-window-opens-on-its-subject-and-its-reply-box-keeps-one-row-on-a-phone), 1 is [card C-crud-row-confirm's](#a-row-form-asks-before-it-saves-a-picker-reads-another-listing-a-history-names-who), 8 are [card C-documents-add-only's](#an-owners-files-that-a-records-state-lets-only-be-added-to), 9 are [card C-row-picker-narrow's](#a-rows-picker-narrowed-by-the-forms-value-and-the-rows-own), 36 are [card C-mail-client's](#a-mail-client-three-panes-docked-chat-windows-and-a-compose-window), 58 are [card C-dashboard-board's](#a-customisable-dashboard-widgets-in-size-presets-one-call-per-module-only-near-the-screen), 1 is [card C-rows-solve's](#a-rows-worked-out-input-typed-in-a-lines-total-sets-its-rate), 5 are [card C-mail-client-fix's](#the-mail-clients-fixes-and-a-crud-forms-computewith), 5 are [card component-details-0620-look's](#details-as-the-2026-06-20-window-drew-them-a-list-of-rows-as-a-table-history-names-from-the-app), 7 are [card C-gallery-pending-removal's](#a-gallery-whose-removals-wait-for-a-review); the rest are named in their sections
|
|
16
|
+
([tree filters](#a-trees-filter-tabs), [the picture column first](#a-tables-picture-column-first-and-unpin-on-a-column-its-page-pins), ...).
|
|
17
|
+
- **Constants** (`compare-values.mjs`): 94 compared (90 from `nexus-shared`, 4 from `nexus-shared/server`), 2 differ,
|
|
18
|
+
both additions: `NexusModule.Inventory` (131072) and its
|
|
19
|
+
`NEXUS_MODULES` entry ([card INV-0-comp](#inventory-added-at-bit-17-no-value-moves)). No value changed or moved;
|
|
20
|
+
`NexusModule.Report` keeps its value and is [deprecated](#nexusmodulereport-is-deprecated-the-backend-retired-it).
|
|
21
|
+
- **Types** (`compare-types.cjs`, new with this release: each exported type's members and optionality, each function's
|
|
22
|
+
required parameters): 1,410 exports compared, **0 breaks**. 12 members are new and required, all on types this package
|
|
23
|
+
builds and an app only reads: `ChatRun.notification` (made by `groupChatMessages`), `TreeNode`'s six `shown...` counts
|
|
24
|
+
(made by `TreeStore`), and `FormStore.setFromPerson` (a method of the class, which no app can implement: it has
|
|
25
|
+
private members). One member's type widened: `TableState.pinned` may hold `null` for a column its page pins and the
|
|
26
|
+
user unpinned, which [could not be done before](#a-tables-picture-column-first-and-unpin-on-a-column-its-page-pins);
|
|
27
|
+
`columnPin(column, pinned)` reads the side. No app in this repository reads it.
|
|
28
|
+
- **Stylesheet**: no class name or custom property of `4.0.0` removed; 241 added.
|
|
29
|
+
- **`nexus-icons ^1.2.0`** (`compare-floor.cjs`): the 130 names this package imports are all in the published `1.2.0`.
|
|
30
|
+
|
|
31
|
+
The packages built on this one take it as they are: `nexus-admin` `4.0.0` asks `^4.0.0` and is unchanged since it was
|
|
32
|
+
published; `nexus-public` `3.0.2` asks `^3.0.0 || ^4.0.0`.
|
|
33
|
+
|
|
34
|
+
### `NexusModule.Report` is deprecated: the backend retired it
|
|
35
|
+
|
|
36
|
+
The backend retired its Report module (`EModules.Report`, 256) on 2026-11-11: print layouts, the print door and their
|
|
37
|
+
history are the Configuration module's now. `NexusModule.Report` is `@deprecated` and **keeps its value**, so bit 8 is
|
|
38
|
+
never given to another module and an old permission mask that holds it still reads; its `NEXUS_MODULES` entry stays as
|
|
39
|
+
well (removing an entry changes a published constant, which only a major may do), so an old `/api/app/report/...` proxy
|
|
40
|
+
URL is still recognised. An app sets no `API_REPORT` any more, and a list a person picks a module from leaves it out
|
|
41
|
+
(`info.module !== NexusModule.Report`). This package builds no such list; `nexus-admin`'s `fallbackModules()`, used only
|
|
42
|
+
when the backend's module list cannot be read, still names it until `nexus-admin` is next released.
|
|
43
|
+
|
|
44
|
+
### A tree's filter tabs
|
|
45
|
+
|
|
46
|
+
Card C-tree-filters (2026-11-10), for a permission set editor: small tabs above a checkable `TreeView` (All actions ·
|
|
47
|
+
View only · Add and edit · Trash and delete) that narrow its rows. 2 exports added (`TreeFilter`, `TreeFilterMatch`).
|
|
48
|
+
|
|
49
|
+
- **`TreeView` `filters`**: each a `TreeFilter` with an `id`, a `label` and a `match` asked of every leaf (none shows
|
|
50
|
+
all). The tree shows the leaves a filter takes and the parents that hold one. While one is on, checking a parent or
|
|
51
|
+
Select all checks only the leaves shown, a parent's box and count read only them, and a hidden leaf keeps its check.
|
|
52
|
+
The value is the same whatever tab is on: switching never changes it and never makes a form dirty.
|
|
53
|
+
`defaultFilter`, `rememberFilter` (`{ key, viewer }`, kept per signed-in person), `onFilterChange` and
|
|
54
|
+
`filtersLabel` (default "Show").
|
|
55
|
+
- **`TreeStore`**: `setFilter` and the option `filter`; each node gains its `shown...` counts beside the whole ones,
|
|
56
|
+
which the value and `counts()` still read.
|
|
57
|
+
- **`TabButtons` `wrap`**: the options wrap onto more rows when they do not fit (`nav` design), as the tree's tabs do on
|
|
58
|
+
a phone.
|
|
59
|
+
- Proved by `test-tree-store.mts` and nexus.template's `/demo/tree-view` "Filter tabs: a permission set".
|
|
60
|
+
|
|
61
|
+
### A table's picture column first, and Unpin on a column its page pins
|
|
62
|
+
|
|
63
|
+
Card C-picture-first (2026-11-10): every data table draws its picture or logo column first, after the row number.
|
|
64
|
+
|
|
65
|
+
- **`TableColumn.lead`**: drawn first of the page's columns until the user moves it; when the page pins other columns on
|
|
66
|
+
the left, a lead column is pinned there too, at their head. `pictureColumn` sets it. The user hides, moves, pins and
|
|
67
|
+
unpins it like any column, and Reset columns brings it back. `leadColumns(columns)` arranges them;
|
|
68
|
+
`namingColumn(columns)` is the column that names a row in a question ("Delete Kathmandu?"), never a picture.
|
|
69
|
+
- **Unpin works on a column its page pins.** The user's sides are a `TablePins` (`null` for unpinned), read by
|
|
70
|
+
`columnPin(column, pinned)`; before, a page's own `pinned` came back.
|
|
71
|
+
- Proved by `test-table.mts`.
|
|
72
|
+
|
|
73
|
+
### A full-size popup fits a phone screen while the address bar shows
|
|
74
|
+
|
|
75
|
+
A full-size popup is `100dvh` tall where the browser knows it (`100vh` stays for older ones), so on a phone or tablet
|
|
76
|
+
its footer (Save, a tab's footer) is no longer under the screen's edge while the address bar shows; the theme panel's
|
|
77
|
+
height follows the same rule. CSS only.
|
|
78
|
+
|
|
79
|
+
### The round-4 review's fixes: a hasty Enter, a question read out, a remembered value kept
|
|
80
|
+
|
|
81
|
+
Cards SUB4-component-review-fix and SUB4-component-review-fix-finish (2026-11-07/08): six findings of the review of
|
|
82
|
+
the subscription round's work in this package. Additions only: 2 exports, both from `nexus-shared`
|
|
83
|
+
(`isAwaitedCrudField` and the type `CrudRecordRead`, the read-state rule the profile and the Details tab share), and
|
|
84
|
+
`Popup`'s `aria-describedby` prop. **One behaviour changes for every message box**: a held Enter, or one within 300 ms of the Enter
|
|
85
|
+
before it, no longer answers it.
|
|
86
|
+
|
|
87
|
+
- **A hasty Enter never answers a box.** The Enter that opened a question (in a form's box, with `confirm`) could press
|
|
88
|
+
the Confirm it gave the focus to - by the key's repeat or the second tap of a double press - and save before the
|
|
89
|
+
details were read. Any Enter inside a message box with `repeat`, or within 300 ms of the Enter before it, is held;
|
|
90
|
+
the next deliberate Enter answers as before. A check that opens a box with Enter and answers with Enter waits 300 ms.
|
|
91
|
+
- **The question is read out.** A message box is described (`aria-describedby`) by its message and its `details`, so
|
|
92
|
+
a screen reader reads them with the title as the focus enters, before Confirm.
|
|
93
|
+
- **A remembered choice on a long list is kept.** A picker that cannot name a record by its key is looked through page
|
|
94
|
+
by page; when the 20 pages ran out before the list ended, a kept value not met was counted gone and forgotten. It is
|
|
95
|
+
now unknown: kept, not opened. Only the list's end (as the box reads one) makes it gone, and the pages are asked in
|
|
96
|
+
the box's own `pageSize`, so a loader paging by its own size with no `hasMore` is no longer cut after one page.
|
|
97
|
+
- **A value opened from memory redraws what follows it.** An input worked out from it (`compute`), sized by it
|
|
98
|
+
(`spanWhen`), shown or labelled by it, drew its old state until something else changed; memory's opening and a
|
|
99
|
+
picker's late answer now redraw the inputs that follow, as a change does.
|
|
100
|
+
- **A profile field the row lacks** (`details.profile.fields`) holds its line while the record is read and says "Not
|
|
101
|
+
loaded" when the read fails, as the Details tab's fields do; before, it was left out of both the profile and the
|
|
102
|
+
Details tab, and appeared late, moving the profile.
|
|
103
|
+
- **A picker asked for a remembered value says it is busy**: its box is `aria-busy` and described "Loading" while the
|
|
104
|
+
value may still come (a search's pages stay the list's busy state), and a person in the box keeps the focus when the
|
|
105
|
+
value opens - any input mounted again under the focus (a late value, a load, a reset) gets it back.
|
|
106
|
+
- Proved by `test-input-memory.mts` (1,200 records, a loader paging by 20, followers drawn), `test-crud-profile-fields.mts`
|
|
107
|
+
(the read state) and nexus.template's `form-confirm.cjs` E, `crud-workspace.cjs` M and `input-memory-picker.cjs` K;
|
|
108
|
+
each new line seen red against the package as it was before.
|
|
109
|
+
|
|
110
|
+
### The row a save leaves is what was sent
|
|
111
|
+
|
|
112
|
+
Card C-crud-hidden-value-after-save (2026-11-09), found by the HR leave setup (card H2): a value hidden by `showWhen`
|
|
113
|
+
stayed on screen after a save until the list was read again, though the server held none. A CRUD built the saved row
|
|
114
|
+
from the form's own values, over the row, under the answer; a page's `transform` that sends null for a hidden value
|
|
115
|
+
(a leave type's "Carry forward at most" while it does not carry forward) changed only the body, and a Nexus answer
|
|
116
|
+
leaves its nulls out, so nothing took the form's old value off the row. Additions only: 4 exports, all from
|
|
117
|
+
`nexus-shared` (`crudSavedRecord`, `crudHiddenOnSave` and the types `CrudSent`, `ApiFormSent`). **What can move for an
|
|
118
|
+
app:** the row after an Add or an Edit, on a data table and in a tree, wherever it differed from what was sent.
|
|
119
|
+
|
|
120
|
+
- **The row is what was sent** (`crudSavedRecord`): the row the form was opened on, then the body `transform` made -
|
|
121
|
+
when it is a record - over the form's values, then the answer. A field the body sends as null is empty on the row;
|
|
122
|
+
one it reshapes (a range sent as its two days) shows as sent, also when the answer is only a token. A shown field the
|
|
123
|
+
body leaves out keeps the form's value, as before; a body that is not a record (a list, a `FormData`) changes nothing.
|
|
124
|
+
- **A value `showWhen` hid when the form was saved** is on the row only as the server holds it: as the answer gives it
|
|
125
|
+
when the answer is a record - a field it leaves out has none - else as the body gives it. So a hidden value the server
|
|
126
|
+
clears by its own rule leaves the row too, with no `transform` (the demo's amenities: a free one has no charge). The
|
|
127
|
+
hidden box still keeps what was typed in it, and showing it again brings it back. A picture and an input worked out
|
|
128
|
+
from others (`compute`) hold no value of the form's and are left as they were.
|
|
129
|
+
- The values of a row are the ones the save sent, never ones typed while it ran.
|
|
130
|
+
- **`ApiForm` `onSuccess`** gets a fourth argument, `sent` (`ApiFormSent`): the values as they were sent and the body
|
|
131
|
+
`transform` made of them.
|
|
132
|
+
- Not changed: what a form sends. A value `showWhen` hides is still sent unless the page's `transform` clears it.
|
|
133
|
+
- Proved by `test-table.mts` (the leave type's case under the old merge and the new one, a dropped field, a token
|
|
134
|
+
answer, a reshaped body) and nexus.template's new `crud-saved-row.cjs`, whose answers a route makes Nexus-shaped (nulls
|
|
135
|
+
left out): its three row lines failed in both motion modes against the package as it was before.
|
|
136
|
+
|
|
137
|
+
### A mail thread, its source and backend, and three params any page can use
|
|
138
|
+
|
|
139
|
+
Card C-mail-thread (2026-11-09), card 2 of the domain messages spec: mail inside an app - a thread with a subject, a
|
|
140
|
+
sender and its people (To, Cc, Bcc), whose replies are the chat's own lines, which a module can write into when
|
|
141
|
+
something happens. Built to the spec's wire before the Message module's `Mail` doors exist, and proved against a
|
|
142
|
+
pretend `Mail` controller in nexus.template that answers that wire. Additions only: 58 exports (45 from `nexus-shared`,
|
|
143
|
+
13 from `nexus-shared/client`), compared with the build before the card by `Frontend/scripts/release-compare`; no
|
|
144
|
+
exported constant changed. **What can move for an app:** nothing it already draws - a chat line that names no kind is
|
|
145
|
+
drawn as before, and `.nx-chat-list__badge` is `inline-grid` (the same in the conversation list, where it is a flex
|
|
146
|
+
item; now its own width in a table cell too).
|
|
147
|
+
|
|
148
|
+
- **`MailThread`** (`nexus-shared/client`): the head - subject, time, From, To, Cc and Bcc **only as the source gives
|
|
149
|
+
it**, each list folded to three names and a "+2" button that opens the rest in place (`MailPeople`), the reader's own
|
|
150
|
+
name saying "you", the record it is about (`MailDomainLine`, or `renderDomain`) - over the chat's `ChatThread` and
|
|
151
|
+
`ChatComposer` on `source.threadSource(id)` through `ChatStore`, unchanged. A person copied unseen reads
|
|
152
|
+
`MAIL_BCC_NOTE` over the reply box; when they reply, or when anyone else's line arrives, the head is read again
|
|
153
|
+
(a reply makes a blind copy a Cc). `onChanged` tells the page what arrived (`reason: "notification"` when the record
|
|
154
|
+
may have changed, which also bumps the record slot's `revision`), `onSummary` its counts (a list's row clears its
|
|
155
|
+
unread), `onHead` each head read. States: bones while the head loads, a failure with Try again in the head and in
|
|
156
|
+
the thread, "This message is not available to you." for a 403 or 404, a closed thread read but not answered. A band
|
|
157
|
+
across the panel, never a box (`Nexus.Mail.css`, new); the chat panel's frame and body.
|
|
158
|
+
- **`ChatMessage.kind`** (`ChatMessageKind`: `chat`, `mail`, `notification`): `NexusChatBackend` reads it from a row's
|
|
159
|
+
`messageType` (the backend's `EChatMessageTypes` 0, 1, 2, or the word). A notification is its own run
|
|
160
|
+
(`ChatRun.notification`) and `ChatThread` draws it as an event line (`ChatEventNote`): the bell, who acted, when, and
|
|
161
|
+
the words, quiet, with no side and no bubble.
|
|
162
|
+
- **`ChatComposer` `notice`**: a line over the writing area that the text area is described by.
|
|
163
|
+
- **`MessageSource`** (`MailInterfaces.ts`, beside `ChatInterfaces.ts`; the builder's `MessageInterfaces.ts` keeps its
|
|
164
|
+
name): `inbox`, `thread`, `threadSource`, `compose`, `archive`, `unarchive`, `unread`, `people`, `defaults`, in this
|
|
165
|
+
package's words (`MailRole` `from | to | cc | bcc`, `MailList` `inbox | unread | sent | archived`).
|
|
166
|
+
**`MessageBackend`** / **`NexusMessageBackend`** / **`defineMessageBackend`** map them to an API (the Message
|
|
167
|
+
module's `Mail` controller by default; `module` and `controller` overridable, as `NexusChatBackend`), reading the
|
|
168
|
+
backend's numbers as words; **`apiMessageSource`** (`/client`) is the only file that calls.
|
|
169
|
+
**`localMessageSource`** keeps a mailbox in memory with a backend's rules (who sees a blind copy, a blind copy's reply
|
|
170
|
+
made a Cc, archive and unread per person, a retried send written once, every wrong input of a new message at once),
|
|
171
|
+
`as(userId)` to read it as somebody else and `post` for a module's notification.
|
|
172
|
+
- **Words and helpers**: `mailPeopleLine` (three names then "+2", never "+1"), `mailBccNote`, `mailPreviewText`,
|
|
173
|
+
`newMailClientId`, `mailInboxLoader` (a server-paged table's loader), `MAIL_ROLE_LABELS`, `MAIL_LIST_LABELS` and the
|
|
174
|
+
limits `MAIL_PEOPLE_MAX` (50), `MAIL_SUBJECT_MAX` (100), `MAIL_BODY_MAX` (4000).
|
|
175
|
+
- **An inbox's columns** (`mailInboxColumns` `from`, `subject` with the record as a tag before it, `lastMessage`,
|
|
176
|
+
`when` as "14 min ago", `unread` in the chat's badge; `mailInboxColumnList` all five, fitting a 1280px window beside a
|
|
177
|
+
menu) read a `MailInboxRow`'s fields, which are the answer's own names, so a CRUD of raw rows uses them too.
|
|
178
|
+
- **Three params any page can use**:
|
|
179
|
+
- **`pagedPicker(load, options)`** and **`holdPickerItems`**: a dropdown over any server search, paged, that names
|
|
180
|
+
every value it holds - from the pages shown, items handed over, a lookup by key (`find`), or the server's first
|
|
181
|
+
pages (`findPages`, 4) - so a form opened on values, or a draft brought back, never shows "Not found" for what a
|
|
182
|
+
page showed. `mailPeoplePicker(source)` is one for the people a message goes to.
|
|
183
|
+
- **`Crud` `form.createDefaults(signal)`**: an Add form's values read as it opens, over `newValues` and under the
|
|
184
|
+
values `create(values)` fixes; the inputs show disabled until they land and a draft comes back over them; a failed
|
|
185
|
+
read opens the form on `newValues` with a toast, since defaults are a help, not the record.
|
|
186
|
+
`mailComposeDefaults(source, picker)` is one: To, Cc and Bcc from the reader's defaults, named at once, and a new
|
|
187
|
+
`clientId` (give `newValues: () => ({ clientId: newMailClientId() })` too, so a form opened without its defaults
|
|
188
|
+
still sends one).
|
|
189
|
+
- **`DataTable` `rowStrong(row)`** (and a `Crud`'s `table.rowStrong`): the row's cells heavier in the text colour
|
|
190
|
+
(`data-strong`), nothing else, so it keeps its height and no column moves when it reads plain.
|
|
191
|
+
- Proved by `test-mail.mts` (146: roles, Bcc hidden also in read marks, a Bcc reply made Cc, archive, unread, a
|
|
192
|
+
notification, a closed thread, a new message refused with every wrong input at once and written once, paging, the
|
|
193
|
+
paged picker, the Nexus backend's requests and answers, a notification's run, a thread through `ChatStore`; red with
|
|
194
|
+
the Bcc rule taken out) and nexus.template's new `mail-thread.cjs` (82 in both motion modes, red with the strong
|
|
195
|
+
look or the blind copy's head refresh taken out).
|
|
196
|
+
|
|
197
|
+
### A record's card in a message, read live from its module
|
|
198
|
+
|
|
199
|
+
Card C-domain-card (2026-11-09), card 5 of the domain messages spec: the owner's "if type is chat and it has domain type
|
|
200
|
+
and id then we fetch those domain info and render those info into its own components ... we fetch that person's
|
|
201
|
+
remaining and taken leave". A message about a record shows the record as a card, read from the module that owns it each
|
|
202
|
+
time it is drawn, never a copy kept in the message. Additions only: 49 exports (39 from `nexus-shared`, 10 from
|
|
203
|
+
`nexus-shared/client`), compared with the build before the card by `Frontend/scripts/release-compare`; no exported
|
|
204
|
+
constant changed. **What can move for an app:** a `MailThread` whose record's kind has a card draws the card where
|
|
205
|
+
`MailDomainLine`'s line was (folded to its title line on a thread narrower than 480px); a kind with no card draws the
|
|
206
|
+
same line as before, now `DomainLine` (`.nx-domain-line`; `.nx-mail__domain` is gone).
|
|
207
|
+
|
|
208
|
+
- **`ChatDomainInfo.card`** (`DomainCardDefinition`): `load(id, signal, context)` reads the record through its
|
|
209
|
+
module's card door, which checks the reader; `view(data, context)` maps the answer to `DomainCardParams`; optional
|
|
210
|
+
`extra(data)` (one more piece of this package's parts), `held` (what the placeholder holds the first time) and
|
|
211
|
+
`lifetime`. **`domainCard({ load, view })`** types `view` by `load`'s answer; `defineChatDomains` refuses a card
|
|
212
|
+
without both. **`domainCardOf(kind)`** finds it. This package holds no kind and no number: the app's rows do.
|
|
213
|
+
- **`DomainCard`** (`nexus-shared/client`) `domain`, `id`, `label`, `revision`, `viewer`, `card` (instead of the
|
|
214
|
+
registered one, or `null`), `kind`, `headingLevel`, `collapsible`, `defaultCollapsed`, `ref` (`reload()`): a
|
|
215
|
+
placeholder of the card's own parts in the shape of the kind's last card (so the record lands without moving what is
|
|
216
|
+
under it, at any width - the head is laid out by the card's width, never its words), the card, "You cannot open this
|
|
217
|
+
leave request." for 403 and 404 alike, "Could not load this leave request." with Try again (which reads once) for any
|
|
218
|
+
other failure or a `view` that throws, and the kind and the record's words alone for a kind with no card or a message
|
|
219
|
+
with no id. Read again when `revision` grows, `reload()` or **`refreshDomainCards(kind, id?)`**: the card stays,
|
|
220
|
+
marked "Updated 2:45 PM" when the record turned out different or "Not up to date" with Try again when the read
|
|
221
|
+
failed; a refusal drops it. A screen reader is told each. `MailThread` passes its notifications' `revision` and its
|
|
222
|
+
reader.
|
|
223
|
+
- **`DomainCardFrame`**: a card from `DomainCardParams` alone (title, kind, code and subtitle on one quiet line with the
|
|
224
|
+
link at its end, state in its tone, fields, figures with a tone mark and a hint, lines with "and 3 more", currency),
|
|
225
|
+
with an optional fold button. Values by kind with the reader's settings (`domainCardValueText`): `date`, `datetime`,
|
|
226
|
+
`days`, `money`, `number`, `person`, `text`. **`DomainLine`**: a kind's tag and a record's words.
|
|
227
|
+
`Nexus.Domain.css` (new): a band, never a box; the mail head draws a line above it, edge to edge.
|
|
228
|
+
- **Reads**: **`DomainCardReads`** / **`domainCardReads`** - one read of a record at a time however many cards draw it,
|
|
229
|
+
an answer kept 30 seconds per reader (`DOMAIN_CARD_LIFETIME`), a failure never kept, a read nobody waits for aborted,
|
|
230
|
+
a read again taking only a read started after the change (and forgetting what other readers kept of the record).
|
|
231
|
+
**`DomainCardRun`** (one card's read outside React: `start`, `stop`, `reload`, `retry`, a snapshot to subscribe to)
|
|
232
|
+
and **`useDomainCard`** for a page's own look. Words: `domainCardNoun`, `domainCardRefusedText`,
|
|
233
|
+
`domainCardFailedText`; shapes: `domainCardShape`, `heldDomainCardShape`, `rememberDomainCardShape`,
|
|
234
|
+
`DEFAULT_DOMAIN_CARD_SHAPE`.
|
|
235
|
+
- **`formatDays(value, { decimals, locale, empty })`** and **`dayWord`** in NumberHelpers: "1 day", "0.5 days", "2.5
|
|
236
|
+
days", grouped as `formatNumber` does.
|
|
237
|
+
- Proved by `test-domain-card.mts` (100: the registry, keys, shared and kept reads, a read again after a change, abort
|
|
238
|
+
and remount, every state of a run, `refreshDomainCards`, words, values by kind, shapes; red on each of nine key lines
|
|
239
|
+
taken out on a scratch copy), `type-checks/domain-cards.tsx` (`view` typed by `load`), and nexus.template's new
|
|
240
|
+
`domain-card.cjs` (88 in both motion modes: a thread's leave card from a pretend `RequestCard` controller, one read,
|
|
241
|
+
kept per reader, Accounts' notification read once and marked updated, a reader refused with the thread whole, a
|
|
242
|
+
requisition's lines, the fold and the keyboard; every state; the placeholder's height kept at seven widths; a phone;
|
|
243
|
+
seven themes light and dark at AA - red on a scratch copy with the band rule, the revision, the narrow fold, the
|
|
244
|
+
placeholder's shape or its width-led head taken out).
|
|
245
|
+
|
|
246
|
+
### Inventory added at bit 17; no value moves
|
|
247
|
+
|
|
248
|
+
Card INV-0-comp (2026-11-09), card 1 of the Inventory spec (`owner-requests-2026-11-09/inventory-spec.md`): the owner's
|
|
249
|
+
Demand, Dakhila and Clearance get a backend module of their own, `EModules.Inventory = 1 << 17`, after Portfolio.
|
|
250
|
+
|
|
251
|
+
- **`NexusModule.Inventory`** = `1 << 17` (131072), the last bit. Every other value is what it was (`Portfolio` stays
|
|
252
|
+
65536), so no permission bitmask an app holds changes meaning - unlike `Chat`'s insertion of 2026-09-25 below. A new
|
|
253
|
+
module takes the next bit at the end, never one in between; the enum's comment says so.
|
|
254
|
+
- **`NEXUS_MODULES`** gains `{ module: NexusModule.Inventory, key: "inventory", label: "Inventory", env: "API_INVENTORY" }`
|
|
255
|
+
before the app's own row: its root is read from `API_INVENTORY` on the server and its browser calls go through the
|
|
256
|
+
proxy at `/api/app/inventory/...`.
|
|
257
|
+
- No export is added or removed (`NexusModule` gains a key); compared with the build before the card, every value of
|
|
258
|
+
`NexusModule` and every row of `NEXUS_MODULES` is kept and only these two are new.
|
|
259
|
+
- Proved by `test-api.mts` (210: the value, Portfolio's unchanged, the key, the label, the environment name, a lookup by
|
|
260
|
+
key and by number, the browser root, the proxy, and a new check that the list names every module of the enum with an
|
|
261
|
+
API exactly once; red on a scratch copy with Inventory on bit 16, its environment misnamed, its key misspelt or its row
|
|
262
|
+
left out) and nexus.template's `api-calls.cjs` (its modules table's Inventory row, both motion modes).
|
|
263
|
+
- **For an app on the published package**: nothing to do until it calls the Inventory API; then set `API_INVENTORY`
|
|
264
|
+
(the module's `.../App/Api` root) where the app's other `API_*` roots are, and allow the module in its proxy route.
|
|
265
|
+
|
|
266
|
+
### A message bell: the unread count on a top bar, one read however many bells
|
|
267
|
+
|
|
268
|
+
Card C-message-bell (2026-11-09), card 8 of the domain messages spec: how much internal mail waits, on every page.
|
|
269
|
+
Additions only: 18 exports (13 from `nexus-shared`, 5 from `nexus-shared/client`), compared with the build before the
|
|
270
|
+
card by `Frontend/scripts/release-compare`, none removed; no exported constant changed. **What can move for an app:** the
|
|
271
|
+
chat icon's unread badge is drawn by the rule `Button`'s new badge shares (`Nexus.Button.css`; the same look, moved),
|
|
272
|
+
and `localMessageSource`'s `unread()` now counts to 100 as the Message module's does.
|
|
273
|
+
|
|
274
|
+
- **`MessageBell`** (`nexus-shared/client`) `source`, `href`, `threadHref`, `label` ("Messages"), `interval` (a
|
|
275
|
+
minute), `size`, `icon`, `maxBadge` (99), `time`: one of a top bar's ghost buttons with the count over its corner in
|
|
276
|
+
the chat's badge - one size whatever the count, "99+" from 100 on - and named for a screen reader ("Messages: 3
|
|
277
|
+
unread messages", "not checked yet", "could not be checked"). A click opens a `Menu`: the newest unread threads
|
|
278
|
+
(subject; who wrote the newest line, the record, "14 min ago"), each a link to `href?thread=<id>`, Try again after a
|
|
279
|
+
failure, and "Open inbox" (with how many in all when more wait); the arrow keys walk it, Escape and a click outside
|
|
280
|
+
close it, and opening it reads the count again. When the count goes up, one bell of the page says it once in a polite
|
|
281
|
+
live region - not on every read, not as the page loads. Drawn in nexus-admin's bar through `renderUtility` with no
|
|
282
|
+
change to nexus-admin.
|
|
283
|
+
- **`useMessageUnread(source, { interval, enabled })`** and **`MessageUnreadPoller`** / **`messageUnreadPoller(source)`**:
|
|
284
|
+
one poller per source object, shared by every bell on the page. It reads when the first bell appears, then every
|
|
285
|
+
`MESSAGE_UNREAD_INTERVAL` (60 s) while the page is in front, at once when the window takes the focus or the tab is
|
|
286
|
+
shown again (`MESSAGE_UNREAD_WAKE_GAP` keeps the two from reading twice), never while the tab is hidden, never two
|
|
287
|
+
at once; a failure keeps the count read before; the last bell gone stops everything and the next one starts from what
|
|
288
|
+
was read. **`refreshMessageUnread(source)`** after a page reads or archives threads. The environment
|
|
289
|
+
(`MessageUnreadEnvironment`, `browserUnreadEnvironment`) is injected, so the rules are tested with a fake clock.
|
|
290
|
+
- **Words**: `mailUnreadWords`, `mailUnreadBadge`, `mailThreadHref`; **`MAIL_UNREAD_MAX`** (100, the backend's last
|
|
291
|
+
count). `LocalMailFailure` gains `"unread"`.
|
|
292
|
+
- **`Button` `badge`**: a count over a button's corner, the chat's badge, the button's size unchanged. Generic: any bar
|
|
293
|
+
or list button with a number on it.
|
|
294
|
+
- Proved by `test-message-bell.mts` (48: one read for two bells, a minute, hidden reads nothing, shown and focused,
|
|
295
|
+
never two on their way, the shortest interval, failures, what is said and by which bell, the last bell gone, a hidden
|
|
296
|
+
first draw, the words, the badge, a thread's address, the local count to 100) and nexus.template's new
|
|
297
|
+
`message-bell.cjs` (77 in both motion modes: a page's clock moved by a minute, a hidden tab, the bar unmoved against
|
|
298
|
+
the server's drawing, the menu by keyboard and pointer, the arrow, a line said once, a row that opens its thread and
|
|
299
|
+
lowers the count, every state, nexus-admin's bar, a phone, seven themes light and dark).
|
|
300
|
+
|
|
301
|
+
### A mail thread's head scrolls with its lines, and its record's card folds by the thread's real width
|
|
302
|
+
|
|
303
|
+
Card C-mail-thread-head (2026-11-09, a bug card): nexus.web's request and leave cards in `MailThread` (REQ-thread-pages)
|
|
304
|
+
showed two faults. No export added or removed; no constant changed. **What can move for an app:** a mail thread's head is
|
|
305
|
+
no longer a box of its own - it scrolls with the lines; a record's card shows its figures first and folds its details
|
|
306
|
+
(fields, lines, what a kind adds) under them, keeping the figures; a figure's hint keeps two lines.
|
|
307
|
+
|
|
308
|
+
- **The fold follows the thread's real width.** `MailThread` measured its width once, before a window had laid out
|
|
309
|
+
(a closed dialog reads 0), so an inbox window opened the card folded at 1280 px. Now a ResizeObserver follows the
|
|
310
|
+
width: the card is drawn once a real width is known - the first decision before the browser paints, so it never
|
|
311
|
+
shows one way and then the other - and folds under 480 px, unfolds over it, until the reader folds or unfolds it
|
|
312
|
+
themselves. **`DomainCard` `collapsed` / `onCollapsedChange`**: the fold a page holds (`defaultCollapsed` stays for
|
|
313
|
+
one it does not).
|
|
314
|
+
- **The head scrolls with the lines.** The head was capped at 45% of the thread with a scroll of its own, so a leave's
|
|
315
|
+
or a requisition's card showed in part at 720 px tall. **`ChatThread` `lead`**: what a conversation opens with, above
|
|
316
|
+
its first message and in the same scroll - the lines alone stay the screen reader's log. With a lead the thread opens
|
|
317
|
+
at its top (its first unread line at the top when that would be out of view), older pages load from their button so
|
|
318
|
+
the lead stays put, and whether the reader is at the bottom is worked out again when the lead grows (a card landing).
|
|
319
|
+
`MailThread` passes its head as the lead: at 1280×720 a leave's card shows whole as the window opens, every part of
|
|
320
|
+
either card is read by scrolling the thread, and the reply box keeps its 206 px.
|
|
321
|
+
- **The card's fold keeps the figures.** Folded, a card is its title, its code and its figures - a leave's balance -
|
|
322
|
+
with "Show the details" for the rest; a card with nothing to fold has no fold button. The figures come first, the
|
|
323
|
+
details under them, so opening them moves nothing above. A figure's hint keeps two lines (`min-height`), so a long
|
|
324
|
+
one ("1.5 paid, 0.5 unpaid (1 day's pay)") does not change the card's height against its placeholder.
|
|
325
|
+
- Proved by nexus.template's `domain-card.cjs` (105 in both motion modes; a new section reads every part of the leave
|
|
326
|
+
and requisition cards in the thread's one scroll at 1280×720 and 390×844, light and dark, the reply box's room
|
|
327
|
+
beside it) and `mail-thread.cjs` (122; a new section opens Puja's leave thread in the inbox's window: whole at 1280
|
|
328
|
+
and folded on a phone, never the other way first; the head in the thread's scroll, the thread at its top with the
|
|
329
|
+
figures in view; the fold following a narrowed and widened window until the reader folds it), red on a scratch copy
|
|
330
|
+
of the parent commit's sources. The demo's leave card carries the real one's hints: the year under Available, and the
|
|
331
|
+
paid/unpaid split under This request.
|
|
332
|
+
|
|
333
|
+
### A bell's refresh asked during a read is read after it, not lost
|
|
334
|
+
|
|
335
|
+
Card C-unread-requeue (2026-11-09, a bug card): nexus.web's bell (MAIL-bell) found that `MessageUnreadPoller.refresh()`
|
|
336
|
+
did nothing while a read was on its way; when that read had started before a read mark, an archive or an unarchive, the
|
|
337
|
+
bell kept the old count for up to a minute. No export added or removed; no constant changed.
|
|
338
|
+
|
|
339
|
+
- **`refresh()` (and `refreshMessageUnread(source)`, unchanged in its contract)**: with no read on its way it reads now,
|
|
340
|
+
as before; with one on its way it asks for exactly one more read after it - however many refreshes are asked
|
|
341
|
+
meanwhile, and whether that read answers or fails - so never two reads at once. While the page is hidden nothing is
|
|
342
|
+
read: the read asked for waits and runs at once when the page is shown, even within the two seconds a shown tab's
|
|
343
|
+
focus otherwise skips. The last bell gone drops it. The minute's timer counts from the read that followed.
|
|
344
|
+
- nexus.template's bell source now counts when asked and answers 150 ms later, as a server does, so the case can be
|
|
345
|
+
seen: `message-bell.cjs` presses Check now, lets Kiran write while that read is on its way, presses it again, and
|
|
346
|
+
the bell shows his line at once (red on the parent commit's poller, with the read mark's case beside it).
|
|
347
|
+
- Proved by `test-message-bell.mts` (66: three refreshes during a read make one read after it, the stale answer drawn
|
|
348
|
+
meanwhile, never two on their way, the minute from the read that followed, hidden then shown, a failed read followed,
|
|
349
|
+
the last bell gone, a bell drawn again inheriting nothing; red on the parent commit's poller and on each of five lines
|
|
350
|
+
of the fix broken) and `message-bell.cjs` (79 in both motion modes).
|
|
351
|
+
- **For an app that worked around it** (nexus.web's `recountUnread()` in `admin/mailbox.ts`, which refreshed once more
|
|
352
|
+
when the read on its way answered): a plain `refreshMessageUnread(source)` now does that by itself.
|
|
353
|
+
|
|
354
|
+
### A mail thread's window opens on its subject, and its reply box keeps one row on a phone
|
|
355
|
+
|
|
356
|
+
Card C-mail-thread-fit (2026-11-09, a bug card): nexus.web's Inbox (MAIL-pages) found two faults in `MailThread` that a
|
|
357
|
+
page cannot mend without CSS of its own. Additions only: 3 exports (`fitToolbar`, `ToolbarFit`, `ToolbarSizes`, from
|
|
358
|
+
`nexus-shared`), compared with the build before the card; no exported constant changed. **What can move for an app:** a
|
|
359
|
+
popup given no `initialFocus` now starts on an element of its body or footer marked `data-nx-focus` before the first
|
|
360
|
+
control (only `MessageBox`'s buttons carried the mark, and it names it itself); every `ChatComposer` - a chat panel's
|
|
361
|
+
too - keeps its formatting buttons on one row.
|
|
362
|
+
|
|
363
|
+
- **Where the focus lands.** A window opened on a thread put the focus on the first control in its body, which for a
|
|
364
|
+
thread with no "+2" and no record is the lines' scroller (`tabIndex 0`), and opened with its ring showing. The
|
|
365
|
+
window now starts on the thread's subject (`tabIndex -1`, marked `data-nx-focus`, no ring: a place to start reading,
|
|
366
|
+
the WAI dialog pattern for a long read), a screen reader reads it first, and Tab goes on through the people, the
|
|
367
|
+
record's card and the scroller - which shows its ring when the keyboard reaches it. `Popup`: with no `initialFocus`,
|
|
368
|
+
`[data-nx-focus]` in the body or footer first, then the first control.
|
|
369
|
+
- **The reply box on a phone.** Under 560 px the editor's buttons took a row of their own and wrapped, three rows over a
|
|
370
|
+
two-line text area, leaving about 190 px for the lines at 390 px. **`MarkdownEditor` `toolbarOverflow`**: `"wrap"`
|
|
371
|
+
(the default, as before) or `"menu"`, one row at any width - the buttons take the room the views and the full-screen
|
|
372
|
+
button leave, and those that do not fit go into a "More formatting" menu at their end, with their shortcuts (which
|
|
373
|
+
keep working); the arrow keys reach it with the other buttons. `ChatComposer` uses `"menu"`: at 390 px the bar is
|
|
374
|
+
Write, Preview, B, I, S, "…", full screen; the reply box is as tall as on a wide window (206 px) and the lines get
|
|
375
|
+
258 px. **`fitToolbar(items, width, sizes)`** is the arithmetic, pure.
|
|
376
|
+
- Proved by `test-toolbar-fit.mts` (10; red with the More button's room, the trailing divider, the all-fit case or the
|
|
377
|
+
gaps taken out) and nexus.template's `mail-thread.cjs` (107 in both motion modes: opened from the keyboard and by a
|
|
378
|
+
click, the subject focused with no ring and the scroller unringed, Tab reaching the scroller with its ring, the bar's
|
|
379
|
+
one row at 1280 and 390 light and dark, More formatting's tools at work, End reaching it, the composer's height the
|
|
380
|
+
same on a phone; red on a scratch copy of the parent commit's sources).
|
|
381
|
+
|
|
382
|
+
### A row form asks before it saves, a picker reads another listing, a history names who
|
|
383
|
+
|
|
384
|
+
Card C-crud-row-confirm (2026-11-09): three gaps nexus.web's pay runs (P1a) worked around. One export added
|
|
385
|
+
(`PickerListController`, a type); no constant changed. **What can move for an app:** a row form's save now marks what
|
|
386
|
+
pickers and joins looked up from the controller as old, as every other CRUD change does, so they read it again where it
|
|
387
|
+
is next shown; and `expireLookups` marks a CRUD's own `lists` old too.
|
|
388
|
+
|
|
389
|
+
- **A row form's question: `CrudRowForm.confirm`.** The same question a form's `confirm` asks (C-form-confirm): after
|
|
390
|
+
every value passes its checks, what will be saved drawn in full as a record's details, Confirm holding the focus, read
|
|
391
|
+
out with its line and details, a hasty second Enter never answering it. Params, or a function of the record and of
|
|
392
|
+
what `load` read, so it names the record: "Lock Kartik 2083?" over the run's totals. `when` asks only for some saves
|
|
393
|
+
(the demo's Duties asks only when duties are taken away).
|
|
394
|
+
- **A row action that is its question alone: a `CrudRowForm` with no `inputs`.** No popup form: choosing it asks
|
|
395
|
+
(`confirm`, else "{label} {the record's name}?"), then sends what `load` read - else the record as the list has it -
|
|
396
|
+
through `transform`, and writes what `writes` names. Its buttons are the action's own word (`submitLabel`, else
|
|
397
|
+
`label`) and "Cancel". The API's toasts say it was done or why not; a refusal writes nothing; Cancel, Esc and the
|
|
398
|
+
close button send nothing; choosing it again while it asks or sends does nothing. A pay run's Lock no longer needs
|
|
399
|
+
`api.confirm` by hand. `inputs` is optional for this; an empty list is still a form.
|
|
400
|
+
- **A picker over another listing: `crudPicker` `list`, `usable` and `option`.** `list` reads a listing in place of
|
|
401
|
+
the controller's `activeListing` - a controller with none, such as a pay run's CRUD endpoints (`PickerListController`,
|
|
402
|
+
`findListing` optional: without it a value the list does not offer is not looked up). `usable` says which records
|
|
403
|
+
count as usable - offered, and untagged when a form holds one - in place of the backend's rule (under 30), so a draft
|
|
404
|
+
run (31) or an invited person (30) can be picked. `option` makes each record's option, its words from two fields or a
|
|
405
|
+
state's word, made once per record read; the search reads them too. `expireLookups` also marks a CRUD's own `lists`
|
|
406
|
+
old, so a picker reading one is read again after a change there. Without them, a picker is as before.
|
|
407
|
+
- **A history names who made a change**: `AuditEntry.creatorName`, when the backend's answer carries it, is said as
|
|
408
|
+
"by Sita Rai" in place of the start of the token. The Nexus backend's `AuditResponseContract` carries only
|
|
409
|
+
`CreatorUserId` (a token), so HR's histories - every module's - show "User 3fA9b2…" until it fills a `CreatorName`.
|
|
410
|
+
- Proved by `test-crud-row-forms.mts` (36: a question alone, the question made for the record, its words),
|
|
411
|
+
`test-crud-picker-plan.mts` (11: the listing read, the lookup, the usable rule) and `test-audit-entries.mts` (8), each
|
|
412
|
+
key line red when broken on a scratch copy (8 of 8); `type-checks/crud-row-confirm.tsx`; nexus.template's
|
|
413
|
+
`crud-row-form.cjs` (56 with one theme, in both motion modes: the Duties question, Back, a double Enter; Promote as a
|
|
414
|
+
question alone, its refusal when the role changed since; a post's history naming Sita Sharma and showing another's
|
|
415
|
+
token; the question at 1280×720 and 390×844, light and dark) and the new `crud-picker-list.cjs` (17: `/admin/posts?people=all`
|
|
416
|
+
offers the invited people, says so under their names, and picks Maya Tamang), red on a scratch copy of the parent
|
|
417
|
+
commit's package sources.
|
|
418
|
+
|
|
419
|
+
### An owner's files that a record's state lets only be added to
|
|
420
|
+
|
|
421
|
+
Card C-documents-add-only (2026-11-09): since INV-2b a posted goods receipt's documents may only be added to - Drive
|
|
422
|
+
refuses a rename, a mark or a delete with the core's `API_X084` ("These files can only be added to now") - but
|
|
423
|
+
`DocumentList` still offered Rename and Delete on it. Eight exports added (`FileChange`, `FILE_CHANGES`,
|
|
424
|
+
`allowsFileChange`, `narrowFileChanges`, `fileChangesNote`, `ownerFileActions`, `OwnerFileActions`, `FILES_ADD_ONLY`);
|
|
425
|
+
no constant changed. Without the new params nothing moves.
|
|
426
|
+
|
|
427
|
+
- **`changes` and `changesReason`** on every owner's files (`OwnerFilesParams`: `DocumentList`, `PictureGallery`,
|
|
428
|
+
`openPictureGallery`, the picture input, `pictureColumn`): the changes a person may make here from the record's state,
|
|
429
|
+
within what the backend allows - `["add"]` for a posted receipt keeps Upload and offers no Rename, Delete, Make main or
|
|
430
|
+
Remove, nor a Replace or an Edit that would delete or mark - with the page's sentence under the files. The changes are
|
|
431
|
+
words (`FileChange`: `add`, `rename`, `primary`, `remove`), the core's `EFileChanges` one for one, as a category's kinds
|
|
432
|
+
are MIME types. `readOnly` and `lockReason` are as they were: the files shown, nothing offered.
|
|
433
|
+
- **A stale page learns from the refusal.** `FileAdapter.changesLeft(answer)` says which changes a refusal leaves
|
|
434
|
+
(`NexusDriveAdapter`: `API_X084` -> `["add"]`); after one, the list offers only those for that owner, under the server's
|
|
435
|
+
words, so a page read while the receipt was a draft stops offering what the server now refuses. The toast says what
|
|
436
|
+
was refused and why ("The file was not renamed" - "These files can only be added to now: ...").
|
|
437
|
+
- The buttons offered are one pure rule (`ownerFileActions`), and an action called while not allowed does nothing.
|
|
438
|
+
- Proved by `test-files.mts` (113: the rule for documents, pictures and a one-file category, the note, the refusal read
|
|
439
|
+
through the Nexus adapter), each of 8 key lines red when broken on a scratch copy; `type-checks/files-add-only.tsx`;
|
|
440
|
+
nexus.template's `/demo/pictures` "Documents only added to" (a receipt on the demo's Drive, Draft, Posted or Reversed,
|
|
441
|
+
and "Post it in another window") and its new `documents-add-only.cjs` (16 in both motion modes, light and dark at
|
|
442
|
+
1280×720 and 390×844), red on a scratch copy of the parent commit's sources (7 lines in each mode); `pictures.cjs` 123.
|
|
443
|
+
|
|
444
|
+
### A gallery whose removals wait for a review
|
|
445
|
+
|
|
446
|
+
Card C-gallery-pending-removal (2026-11-11): Drive holds the removals of a reviewed gallery (`company-public-gallery`,
|
|
447
|
+
whose changes Nexus support approves): a Delete of a picture the public sees only marks it `pendingRemoval`, still
|
|
448
|
+
served and listed, until the reviewer decides, and the person may Keep it until then. Seven exports added
|
|
449
|
+
(`removalHeld`, `removalHeldSentence`, `removalHeldQuestion`, `PENDING_REMOVAL_TAG`, `REMOVAL_REVIEWER`,
|
|
450
|
+
`OwnerFilesChange`, `OwnerFilesChangeKind`); no constant changed. Without a holding category nothing moves.
|
|
451
|
+
|
|
452
|
+
- **Records.** `StoredFile.pendingRemoval?` and `removalAt?` (Drive's `removalTime`, while one waits), and
|
|
453
|
+
`FileCategoryInfo.holdsRemovals?` (Drive's List); each read only when true, so every other record is as before.
|
|
454
|
+
- **The adapter.** `FileAdapter.remove` in a holding category answers the file marked (`removalHeld(result)` reads it);
|
|
455
|
+
the new optional `FileAdapter.keep(owner, fileId)` cancels the removal (`NexusDriveAdapter`: `PUT File/Keep/{id}` with
|
|
456
|
+
`{ ticket, id }`, a run-out ticket asked again once). An adapter without `keep` offers no Keep.
|
|
457
|
+
- **The views.** A waiting picture is faded with "Removal waiting for review" as a warning band across its foot, and its
|
|
458
|
+
buttons are View and Keep (no Delete, Make main or Edit); a waiting document's row says it with the time it was asked
|
|
459
|
+
and offers Keep in place of Rename and Delete; a waiting main picture's tag sits beside it. In a holding category the
|
|
460
|
+
Delete asks "If the public sees this picture, it stays until Nexus support approves its removal." and the answer
|
|
461
|
+
decides: held, the file stays marked and the toast says "Removal sent for review"; else it is gone, as before. A
|
|
462
|
+
one-file holding category offers no Replace or in-place Edit (its Delete frees no room until the review).
|
|
463
|
+
`ownerFileActions` gains `holdsRemovals`, `keeps` and the `keep` button.
|
|
464
|
+
- **Params.** `holdsRemovals` (told before the list is read; the backend's says it too) and `removalReviewer` (default
|
|
465
|
+
"Nexus support"); **`onFilesChange(files, { kind, file })`** after every change the backend took - `upload`, `edit`,
|
|
466
|
+
`primary`, `rename`, `delete`, `hold`, `keep` - with the files read again, for a page whose record follows its files.
|
|
467
|
+
`onChange(primary)` is as it was.
|
|
468
|
+
- Proved by `test-files.mts` (142: the records, the Delete and Keep calls, the buttons), nexus.template's `/demo/pictures`
|
|
469
|
+
"A reviewed gallery" (the demo's Drive holds `guest-public-gallery`, `Demo/Review` publishes, approves, declines) and the
|
|
470
|
+
new `gallery-held-removals.cjs` (56 in both motion modes: by keyboard, the list, seven themes light and dark at
|
|
471
|
+
1280×720 and 390×844).
|
|
472
|
+
|
|
473
|
+
### A row's picker narrowed by the form's value and the row's own
|
|
474
|
+
|
|
475
|
+
Card C-row-picker-narrow (2026-11-09): nexus.web's Nikasha (INV-3) had to type an asset line's tags, because a picker
|
|
476
|
+
in a line table could not be narrowed by two values at once - the form's Store and the line's Item. Nine exports added
|
|
477
|
+
(`narrowingsOf`, `inputNarrowingsOf`, `narrowInputParamsBy`, `NarrowingRead`, `isEmptyNarrowed`, `formNarrowingNames`,
|
|
478
|
+
`narrowedFollowers`, `DropdownNarrow`, `RowsFormValue`); no constant changed. **What can move for an app:** a row
|
|
479
|
+
input's `narrowedBy` now narrows in rows (it was ignored there), and a row redraws when a value its inputs `dependsOn`
|
|
480
|
+
changes; `narrowedBy` and `narrowQuery` are wider types, so code that reads `params.narrowedBy.name` reads
|
|
481
|
+
`narrowingsOf(params)` instead.
|
|
482
|
+
|
|
483
|
+
- **Several narrowings at once: `narrowedBy` as a list.** The options must fit every one; the box asks for the first
|
|
484
|
+
one still empty ("Pick a store first", then "Pick an item first"); the list loads afresh when any changes (one load key
|
|
485
|
+
for all). A loader is asked with `request.narrows` (every one) beside `request.narrow` (the first), and one narrowing
|
|
486
|
+
is asked exactly as before. `narrowInputParamsBy(params, read)` narrows by any values; `narrowInputParams` stays.
|
|
487
|
+
- **In rows, by the row's value and the form's: `from: "form"`.** A row input's narrowing names an input of its row
|
|
488
|
+
(`{ name: "itemId" }`) or of the form around the rows (`{ name: "storeId", from: "form" }`). The Form hands a rows
|
|
489
|
+
input the form values its rows follow (`formValues`, a prop a rows input used alone takes from the page) and redraws
|
|
490
|
+
its rows when one changes. A person's change clears a value picked among the old options: a row's Item clears that
|
|
491
|
+
row's Tags (and down the row's chain), the form's Store clears every row's (to the input's own empty value); a value
|
|
492
|
+
set from code - a saved document loading - keeps them. The rule is one pure function, `narrowedFollowers`, for a
|
|
493
|
+
form, a row and every row of a form.
|
|
494
|
+
- **A cell that follows its row: `dependsOn` in rows.** A change of a row value a row input names in `dependsOn` draws
|
|
495
|
+
the row again, so `cellParams` answers for the row as it is now: a line's Tags at most its Quantity (`{ max }`), the
|
|
496
|
+
picker's own rule then stopping a save when the quantity drops under the picks.
|
|
497
|
+
- **`crudPicker` narrowed by several values, or by its door's path.** Each value goes into the listing's path when the
|
|
498
|
+
path names it (`list: StockBalance/Units/{storeId}/{itemId}`), as a query when `narrowQuery` (now one name or a list)
|
|
499
|
+
names it, else narrows in the browser; each answer is kept per values, shared by every row that names them, and a
|
|
500
|
+
saved value is named from them. A path listing is never read unnarrowed.
|
|
501
|
+
- Proved by `test-input-narrowing.mts` (104), `test-rows-store.mts` (132) and `test-crud-picker-plan.mts` (20), each of
|
|
502
|
+
11 key lines red when broken on a scratch copy; nexus.template's `/demo/tabular-form` "An issue" (a Store, lines of an
|
|
503
|
+
Item, a Quantity and Tags from the demo backend's `StoreUnit/Units/{storeId}/{itemId}`, and "Open a saved issue") and
|
|
504
|
+
its new `row-picker-narrow.cjs` (25 in both motion modes, the Item and the Tags from the keyboard, light and dark at
|
|
505
|
+
1280×720 and 390×844), red on a scratch copy of the parent commit's sources (21 lines in each mode); the tabular,
|
|
506
|
+
group, forms, input-memory, narrowing, dropdown, form-figures and CRUD checks in both modes.
|
|
507
|
+
|
|
508
|
+
### A mail client: three panes, docked chat windows and a compose window
|
|
509
|
+
|
|
510
|
+
Card C-mail-client (2026-11-10), card 1 of the mail client spec (the owner: "the mail project UI make UI as mail,
|
|
511
|
+
compose, send, inbox ... chat exist inside mail ... we separate by this types"). 36 exports added: `MailClient`,
|
|
512
|
+
`MailReader`, `MailComposeWindow`, `ChatPopup` with their props, the pure `MailClientHelpers.ts` (the types a rail lists,
|
|
513
|
+
a row's time, a thread's discussion as a chat source, the split and the drafts kept per viewer), and the wire's
|
|
514
|
+
`MailThreadKind`, `MailFolders`, `MailFolderCount`, `MailKindCount`. No constant changed. **What can move for an app:**
|
|
515
|
+
`NexusMessageBackend.readInboxRow` no longer reads a row's `kind` as what the thread opened with (that is `messageType`
|
|
516
|
+
or `headMessageType`): on the MAIL-hub wire `kind` is the conversation's, Mail 1, Chat 2, Domain 3, and a chat's `kind`
|
|
517
|
+
2 used to read as a notification.
|
|
518
|
+
|
|
519
|
+
- **`MailClient`**: a bar (title, search, the split button, the app's `settings` slot - the ⚙ to its admin pages), the
|
|
520
|
+
rail (Compose; Inbox, Unread, Sent, Archived with their unread counts; Types - Mail, Chat, one per record kind the app
|
|
521
|
+
named in `defineChatDomains`, each with its unread count, a type with nothing in it left out), the list, and the
|
|
522
|
+
reading pane. Side by side, or one over the other by the reader's choice, remembered for them in this browser
|
|
523
|
+
(`nx-mail-split:<viewer>`); one over the other under 1000px of its own width; one pane at a time under 700px, with a
|
|
524
|
+
back arrow and a Folders button. It fills what holds it and never scrolls: each pane does. Params only: `source`,
|
|
525
|
+
`userId`, `view`/`onViewChange` and `threadId`/`onThreadChange` for a page's URL, `settings`, `chatSourceFor`,
|
|
526
|
+
`renderDomain`, `onChanged` (a bell's `refreshMessageUnread`), `pageSize`, `pollInterval`.
|
|
527
|
+
- **The list reads as mail**: avatar, sender (strong while unread, with the thread's line count), subject — last line (a
|
|
528
|
+
chat: its last line after its author), the type as a tag in its kind's tone, the time (today's as a time, yesterday's
|
|
529
|
+
as "Yesterday", older as a date, in the reader's zone and calendar), a select box and a star (only when the source
|
|
530
|
+
stars). Ticked rows take Archive or Move to inbox, Mark read and Mark unread in one `SplitButton`. The arrow keys,
|
|
531
|
+
Home and End move between rows, Enter opens (and puts the focus at the subject); more rows load as the end shows.
|
|
532
|
+
- **`MailReader`**: the subject, its type, From → To · Cc (· Bcc as the source gives it), when; the record's
|
|
533
|
+
`DomainCard`; the mails, the newest open and the earlier folded to a line each, a module's notifications as notes
|
|
534
|
+
among them; **Discuss (n)** (the thread's chat lines) and **Reply**. A chat is drawn by its head - who, since when, its
|
|
535
|
+
last line - and Open chat. Opening a conversation reads it.
|
|
536
|
+
- **`ChatPopup`**: a conversation's lines and composer in a window docked at the bottom right, minimisable to the dock,
|
|
537
|
+
several at once side by side (each next one to the left; on a screen too narrow, over each other, the last used on
|
|
538
|
+
top), Esc inside one closes it. A mail's Discuss opens its discussion (`mailDiscussionSource`: the chat lines alone,
|
|
539
|
+
whatever is written there a chat line, a chat line quoting a mail quoting nothing); a chat opens in one at once.
|
|
540
|
+
- **`MailComposeWindow`**: docked, minimise, maximise, close; To, Cc and Bcc (the source's people, To from its
|
|
541
|
+
`defaults`), subject, a Markdown body that fills the window, Send (Ctrl+Enter) and Discard. What is typed is kept per
|
|
542
|
+
viewer (`nx-mail-draft:<viewer>:new` or `:reply:<thread>`, every access in try/catch) until it is sent or discarded;
|
|
543
|
+
closing with words asks "Keep draft" or "Discard". A reply is a new mail in the thread (`ChatDraft.kind: "mail"`).
|
|
544
|
+
- **`Popup` `docked` and `height`** (general): a docked popup is non-modal (no backdrop, no page lock, Tab leaves it),
|
|
545
|
+
drawn on the body through a portal, placed along the bottom edge above the dock's bars. `height` sets any popup's
|
|
546
|
+
height (`--nx-popup-height`); without it a popup is `fit-content` tall as before.
|
|
547
|
+
- **The wire MAIL-hub answers** (the backend card may still change it, and says so in its card): an inbox row's `kind`
|
|
548
|
+
(1-3), `starred`, `fromAvatar`; the Inbox body's `kind` and `domainType` alone (a type); `GET Mail/Folders` answering
|
|
549
|
+
`{ folders: [{ list, unread }], kinds: [{ kind, domainType, unread, total }] }`; `PUT Mail/MarkRead`, `MarkUnread`,
|
|
550
|
+
`Star`, `Unstar` with `{ ids }`; Reply's `messageType: 1` for a mail (Chat stays the default). `MessageSource`'s
|
|
551
|
+
`folders`, `markRead`, `markUnread` and `star` are optional, and `MessageBackend.supports(...)` turns them off for an
|
|
552
|
+
older API. `localMessageSource` answers all of it (`chat` threads, `starredBy`, the failures `folders` and `marks`).
|
|
553
|
+
- Proved by `test-mail-client.mts` (103) and the CSS contract; nexus.template's `/demo/mail-client` (a mailbox with a
|
|
554
|
+
discussion, chats, two record kinds and a month of reports; Kiran writes, Keep a draft, Answer slowly, Server down,
|
|
555
|
+
Full window; empty, loading, failed and a 390px phone) measured by a 73-check browser pass (the client never scrolls,
|
|
556
|
+
one row height, the keyboard, Discuss and chat windows non-modal at the bottom right and side by side, Esc, minimise,
|
|
557
|
+
compose with its draft, a reply as mail, types, the record's card, stars, Archive, the remembered split, no layout
|
|
558
|
+
shift, no box in a box, a phone's one pane and no sideways scroll, the muted words at 4.5:1 or more on the open and
|
|
559
|
+
the hovered row in all seven themes light and dark, cozy density, animations on), and `popup-windows.cjs`,
|
|
560
|
+
`mail-thread.cjs` and the CRUD popup checks unchanged.
|
|
561
|
+
|
|
562
|
+
### A worked-out input that can be typed in: the total sets the price
|
|
563
|
+
|
|
564
|
+
Card C-editable-calculated-amount (2026-11-10), the owner: *"if i put final 2500 inside the calculate amount then it
|
|
565
|
+
calculate or set the price which is very easy. this logic we have inside 06-20 ... as possible generic dynamic."*
|
|
566
|
+
Additions only: 6 exports, all from `nexus-shared` (`priceFromNet`, `PriceFromNetOptions`, `PriceFromNetFound`,
|
|
567
|
+
`PriceFromNetRefused`, `InputSolved`, `NetAmountOptions`). **One behaviour changes:** `crudInputs.netAmount()` can be
|
|
568
|
+
typed in by default (below); `{ editable: false }` keeps it as it was.
|
|
569
|
+
|
|
570
|
+
- **`InputCompute.solve(typed, values, withValue)`**: a worked-out input worked backwards. With it a Number input's box
|
|
571
|
+
is a real text box (a NumberBox: its own Tab stop, the person's number format, its prefix and suffix) instead of the
|
|
572
|
+
`<output>`. Typing does nothing until the person leaves the box (Tab, a click away) or presses Enter; then `solve`
|
|
573
|
+
answers the values of the inputs it is worked out from (`InputSolved`: `{ values, note? }`), a sentence to refuse, or
|
|
574
|
+
`null` to change nothing. The answer is written as the person's own change (**`FormStore.setFromPerson(values)`**:
|
|
575
|
+
the inputs mount again with it, their rules are asked, the form's `onFieldChange` runs, inputs narrowed by them lose
|
|
576
|
+
their picks, it is a change for Save, drafts and the leave guard; nothing while the values load) and the input still
|
|
577
|
+
holds no value of its own. The box then shows the figure worked forward again, and a line under it says what was set
|
|
578
|
+
("Price set to 2,458.21."), and when the typed figure cannot be reached exactly, the nearest it now shows ("2,500.04
|
|
579
|
+
cannot be reached exactly; nearest is 2,500.05. Price set to 2,458.26."). A refusal is said in `--nx-danger` with the
|
|
580
|
+
alert icon and the box goes back. An input that is read-only or disabled is never written: that is refused by name.
|
|
581
|
+
Escape puts back the figure (and closes nothing); Enter with nothing new submits the form as from any box; an emptied
|
|
582
|
+
box or the same figure changes nothing. Read-only, disabled and loading forms keep the box read-only, disabled, blank.
|
|
583
|
+
- **The line under the box** shares one place with the hint (`nx-field__notes`, a one-cell grid, at least a line tall),
|
|
584
|
+
so nothing below moves when it comes and goes; it is `aria-live="polite"` and part of the box's description. It shows
|
|
585
|
+
until the figure moves on (another input changed), then the hint is back.
|
|
586
|
+
- **`priceFromNet(target, { quantity, discountPercentage, vatPercentage, decimals = 2, places = 2 })`**: the price that
|
|
587
|
+
comes to a net amount by the server's rule (`priceAmounts`). Plain division (06-20's `FindOriginalPrice`) often lands
|
|
588
|
+
a paisa off, because each step is rounded; this gallops from that estimate and halves among the prices at the
|
|
589
|
+
price's own places, each worked forward on exact decimals. The net amount never falls as the price rises, so **when
|
|
590
|
+
any price reaches the figure exactly, the smallest such price is answered** (`exact: true`); otherwise the price whose
|
|
591
|
+
net amount is nearest (a tie goes above, so the total is never short), with the net amount it reaches. A quantity of 0
|
|
592
|
+
or none counts as 1; a negative figure or quantity, a 100 % discount and VAT of -100 % are refused in words
|
|
593
|
+
(`{ ok: false, message }`). 2,500 at 10 % and 13 % is 2,458.21 exactly.
|
|
594
|
+
- **`crudInputs.netAmount(keys?, changes?, { editable = true, priceDecimals = 2 })`** wires `solve` to `priceFromNet`
|
|
595
|
+
on the `PriceKeys` (the quantity key too) and writes the price at `priceDecimals`. Its hint is "Price less the
|
|
596
|
+
discount, plus VAT. Type a total to set the price." (the old hint with `editable: false`). A `changes.compute` of the
|
|
597
|
+
page's own (a counter's server preview) has no `solve` and stays read-only, as before.
|
|
598
|
+
- **Not in rows** in this card: rows came with [card C-rows-solve](#a-rows-worked-out-input-typed-in-a-lines-total-sets-its-rate).
|
|
599
|
+
- Proved by `test-number-helpers.mts`: the owner's case and 20 more, and **560,000 targets** (every 0.01 to 100.00,
|
|
600
|
+
against 8 rate pairs from -5 % to 60 % off and 0 % to 13 % VAT, quantities 1, 2, 3, 7 and 1.5, and a price in 0, 2
|
|
601
|
+
and 3 places) against a sweep of every price: the net amount never fell, every one of the 287,075 reachable targets
|
|
602
|
+
was answered with its smallest exact price, every one of the 272,925 others with the nearest net amount any price
|
|
603
|
+
reaches, and every answer's net amount is what its price comes to (about 3.5 s). `test-form-store.mts` (the preset's
|
|
604
|
+
`solve`, `setFromPerson`: a change, its rule, `onFieldChange`, a new stamp, nothing while loading), the CSS contract,
|
|
605
|
+
and nexus.template's `/demo/form` "Worked out from other inputs" with `form-figures.cjs` (156 in each motion mode): 2,500
|
|
606
|
+
typed sets 2,458.21; 2,500.04 the nearest; Enter, Escape, an emptied box; a refusal; nothing below moving in compact
|
|
607
|
+
and cozy at 1280 and 390 wide; a percentage of marks with no money in it; the box as tall as Price with its edge and
|
|
608
|
+
background in three designs; the line under it at 4.85:1 or more and a refusal at 5.5:1 or more in 7 themes light and
|
|
609
|
+
dark. `form-confirm.cjs` unchanged (80).
|
|
610
|
+
|
|
611
|
+
### A customisable dashboard: widgets in size presets, one call per module, only near the screen
|
|
612
|
+
|
|
613
|
+
Card C-dashboard-board (2026-11-10), card 1 of the dashboard widgets spec (the owner: "customizable dashboard ...
|
|
614
|
+
similar lighweight best optimize ... some can specific module base components only allow to add on same module, some
|
|
615
|
+
can allow to add any place ... what's new, version info and more. comming soon"). Additions only: 58 exports, 35 from
|
|
616
|
+
`nexus-shared` (`DashboardInterfaces.ts`, `DashboardHelpers.ts`) and 23 from `nexus-shared/client` (`DashboardBoard`,
|
|
617
|
+
`WidgetFrame`, `WidgetLink`, `WidgetNavigateContext`, and the looks). No constant changed. Two shared components gained a
|
|
618
|
+
prop: `Popup` `drawer` and `ChatFailure` `flat`; without them both draw as before.
|
|
619
|
+
|
|
620
|
+
- **`DashboardBoard`**: a bar (the place's `title`, the page's `actions`, Refresh, Customise) over a 12-column grid whose
|
|
621
|
+
rows are three controls tall (102px compact, 114 cozy), `grid-auto-flow: dense`. A widget takes a size preset - S 3×1,
|
|
622
|
+
M 6×2, L 6×4, W 12×2, T 12×4 (`WIDGET_SPANS`) - from the sizes it declares, the first its default. The board is a size
|
|
623
|
+
container: under 48rem of its own width a small widget takes half and a medium or large one the whole; under 30rem
|
|
624
|
+
every widget is the full width, in the layout's order. A layout is an ordered list of `{ code, size }`; no coordinates.
|
|
625
|
+
Params only: `place`, `placeModule` (default `place`), `widgets` (the app's registry), `canSee(widget)` (the module
|
|
626
|
+
held, the key held), `layout` / `version` (the person's, or `null`), `defaultLayout` (the company's, else the code's),
|
|
627
|
+
`onSave(layout, version)`, `onReset(version)`, `canCustomise`, `canSaveCompanyDefault` + `onSaveCompanyDefault(layout)`,
|
|
628
|
+
`loadModule(module, codes)`, `initialData` (read with the page: drawn at once), `moduleLabel`, `onNavigate(href)` (the
|
|
629
|
+
app's router for every link inside), `loadMargin`.
|
|
630
|
+
- **Edit mode** (Customise): a dashed edge on every widget, its heading the drag handle, a size menu holding only its sizes,
|
|
631
|
+
and Remove. **Drag** by pointer events, no library: the widget follows the pointer and the others make room as it
|
|
632
|
+
passes over them; Esc puts the order back; listened for on the window, since the grid moves the widget in the document
|
|
633
|
+
and a moved element loses a pointer capture. **Keyboard**: a focused widget moves with the arrow keys (up and down to
|
|
634
|
+
the nearest widget in the row above or below, `neighbourInColumn`), Home and End; Enter or Space opens its size menu;
|
|
635
|
+
Delete or Backspace removes it, the focus going to the widget in its place. Every move is read out. **Add widget**
|
|
636
|
+
opens a drawer (`Popup drawer`) listing the widgets this place may hold, by module (the place's own first), each with
|
|
637
|
+
its shape on a small grid, its sizes, and Add or "On the board"; closing it shows the widget added last with the focus
|
|
638
|
+
on it. **Save** (with "Save as the company's default" in its menu when allowed), **Cancel**, **Reset to default** (Save
|
|
639
|
+
then calls `onReset`). A save answered `stale` (the server's 409 with the saved layout) draws that layout and says
|
|
640
|
+
"Changed in another window: the layout saved there is shown."
|
|
641
|
+
- **Hidden widgets**: a widget the person cannot see now (`canSee`), one not allowed in the place (`placement: "own"` on
|
|
642
|
+
another module's board), or one the app no longer declares, is drawn nowhere and offered nowhere, but **keeps its place
|
|
643
|
+
in the saved layout** (`mergeHidden`: each after the drawn widget it followed), so it comes back where it was.
|
|
644
|
+
- **Data**: `WidgetDefinition.batch: { module, codes?, pick? }` puts a widget in its module's one call per draw
|
|
645
|
+
(`createWidgetBatcher`: everything asked in one task goes together, a call per module with every code its widgets
|
|
646
|
+
need); `load(context)` is a widget's own call; a widget with neither needs no data. **Only widgets near the screen
|
|
647
|
+
load** (one IntersectionObserver for the board, `rootMargin` and `scrollMargin` of `loadMargin`, so a scroller between,
|
|
648
|
+
as an admin shell's page area, is widened too). Each answer is kept for the page's life, or `refresh` seconds while on
|
|
649
|
+
screen (one 5-second timer for the board, nothing while the page is hidden); Refresh reads everything near the screen
|
|
650
|
+
again and the rest when it comes near. A module that fails fails its own widgets alone.
|
|
651
|
+
- **`WidgetFrame`**: the title, an optional link ("View all"), its own refresh, and its body in one of four states - bones
|
|
652
|
+
by its size while it loads (one line for S, three for M and W, seven for L and T), the `empty` sentence, the failure
|
|
653
|
+
with Try again (`ChatFailure flat`: the tint without an edge, no box in the frame), or its content. Its size is the
|
|
654
|
+
grid's, so nothing moves when the data lands. Usable alone.
|
|
655
|
+
- **The looks**: `StatWidget` (figures side by side as they fit, each a link if it has `href`, a tone dot), `ListWidget`
|
|
656
|
+
(picture or icon, words and a muted second line, a time and a tag; **only the rows that fit whole**, the rest counted
|
|
657
|
+
on its more line: `total`, `more: { href, label? }`, `fit`), `CalendarWidget` (the package's `Calendar` with typed
|
|
658
|
+
marks under each day - `holiday`, `leave`, `event` or the app's own `kinds` - a holiday's number in the danger tone,
|
|
659
|
+
a legend counting the month's marks, the picked day's marks beside the month or under it; AD or BS; `marksByDay`
|
|
660
|
+
spreads a mark over its days), `NewsWidget` (release notes: title, version and Released / Coming soon tags, the
|
|
661
|
+
application and day, the first lines of the Markdown notes, Read more), `VersionWidget` (the version large, the
|
|
662
|
+
application and build day, What's new).
|
|
663
|
+
- **`Popup drawer`** (general): the window's whole height at its end edge, its width from `size` (all of it on a phone),
|
|
664
|
+
modal, neither moving nor maximising, fading in as every popup does.
|
|
665
|
+
- Proved by `test-dashboard.mts` (63: the sizes, own and anywhere, canSee, a layout kept whole with hidden widgets, moving
|
|
666
|
+
by keyboard and pointer, the Add list, the batcher - one call per module, the cache, refresh seconds, a shared call,
|
|
667
|
+
failures per module, a pick that throws), the CSS contract, and nexus.template's `/demo/dashboard` measured by
|
|
668
|
+
`dashboard-board.cjs` (98 in each motion mode): the first draw asks each module once and nothing below the fold until
|
|
669
|
+
it is scrolled near (then one call of its module alone), no layout shift, the spans, rows three controls tall in compact
|
|
670
|
+
and cozy, no list with a half row, the keyboard (move, size menu with only its sizes, Delete, focus kept), the drawer at
|
|
671
|
+
the right edge the window's height (the whole width at 360px), Add, Save, Cancel, Reset, drag and Esc, a stale save, the
|
|
672
|
+
company's default, Kiran's hidden Request widgets back in place for Puja after Kiran's save, a failed module with Try
|
|
673
|
+
again, 360px with no sideways scroll and every widget the full width in order, no box inside a widget, and every word
|
|
674
|
+
at 4.5:1 or more (the lowest 4.85) in all seven themes light and dark, the drawer's too. `popup-windows.cjs` unchanged.
|
|
675
|
+
|
|
676
|
+
### A row's worked-out input, typed in: a line's total sets its rate
|
|
677
|
+
|
|
678
|
+
Card C-rows-solve (2026-11-10), the follow-up C-editable-calculated-amount named: the owner's "if i put final 2500 inside
|
|
679
|
+
the calculate amount then it ... set the price", for a table of lines (Inventory's Dakhila). Additions only: 1 export
|
|
680
|
+
(`withComputeSources`, `nexus-shared`), and new members of exports already listed (`RowsStore.setFromPerson`,
|
|
681
|
+
`RowsInputProps.computeWith`, `crudInputs.lineAmount`, `PriceKeys.other`, `NetAmountOptions.taxed`). **One behaviour
|
|
682
|
+
changes:** a row input with `compute` was ignored in rows (drawn as a plain input holding a value); it is now worked out
|
|
683
|
+
on each row and holds none.
|
|
684
|
+
|
|
685
|
+
- **`compute` works in a tabular or group form's rows**, as in a form: `from` names the row's inputs, `values` is the
|
|
686
|
+
row, `withValue` is the rows' new **`computeWith`** (a `Form` hands the rows its own unless their params name one;
|
|
687
|
+
a new one draws every row's worked-out cells again). Read-only by default (an `<output>` on the read-only tint, no Tab
|
|
688
|
+
stop, named "Amount, line 2" in a table); with `solve` a Number is a box that can be typed in. A table has no line under
|
|
689
|
+
a cell, so the note ("Line 3: Rate set to 2,458.21.", or the nearest, or a refusal in red) goes in the line under the
|
|
690
|
+
rows until the figure moves on, and a hidden live region in the cell says it. Enter with a new figure sets it and
|
|
691
|
+
stays; with nothing new it moves down the column (on the last line it adds one), as from any cell. Escape puts the
|
|
692
|
+
figure back.
|
|
693
|
+
- **The rows hold no value under a worked-out name** (as `FormStore`'s `isComputedInput`): a loaded row's field of that
|
|
694
|
+
name is dropped, a value from code, an `onRowChange` or `deriveRows` patch, `newRow` or a new row's values naming it is
|
|
695
|
+
not taken, it has no rule and is never remembered, so the rows' value, `isChanged`, Save and the leave guard never see it.
|
|
696
|
+
- **`compute.from` joins the row input's `dependsOn`** (`withComputeSources`), so the row draws again when a source changes.
|
|
697
|
+
- **`RowsStore.setFromPerson(key, values)`**: a person's change of a row made through another cell, written as code
|
|
698
|
+
writes (the cells mount again with it) and followed as typing is (`onRowChange` runs for each value, narrowed followers
|
|
699
|
+
clear, value listeners hear it, so a parent form has unsaved changes and its leave guard asks).
|
|
700
|
+
- **`ComputedInput`** (inside the package, `Components/Inputs/ComputedInput.tsx`) is the one worked-out box, lifted out
|
|
701
|
+
of `Form.tsx` with `read`/`write` callbacks; `Form` and the rows' cells both draw it, and the Form behaves as before
|
|
702
|
+
(`form-figures.cjs` unchanged, 156 in each motion mode). `formInputLabel` moved with it and is still exported from the
|
|
703
|
+
same place.
|
|
704
|
+
- **Found and fixed on the way, in Form too:** a click straight into the Price after typing a total dropped the focus to
|
|
705
|
+
the page, because the Price box mounts again with its new value; it now lands in the box that took its place.
|
|
706
|
+
- **`crudInputs.lineAmount(keys?, changes?, options?)`**: `netAmount` for a table of lines - the rate (`unitPrice`)
|
|
707
|
+
times the `quantity`, less the discount, plus VAT - as a 9rem column, typed in per row with `priceFromNet`.
|
|
708
|
+
**`PriceKeys.other`** adds an amount whole after VAT (a line's other costs, rounded to 2 places: the Inventory
|
|
709
|
+
service's `StockMaths.Line`), taken off a typed total before the rate is worked out ("The amount cannot be less than
|
|
710
|
+
the other costs."). **`NetAmountOptions.taxed(withValue, values)`** says when the discount and VAT count (a purchase's
|
|
711
|
+
lines, not a donation's), read from `computeWith`.
|
|
712
|
+
- **An `<output>`'s text fills its box** (`.nx-field__output-text { flex: 1 }`), so the box's `text-align` places it: at
|
|
713
|
+
the end of a table's number column, as a typed figure is.
|
|
714
|
+
- Proved by `test-rows-store.mts` (section Z: no value held, `dependsOn` joined, `setFromPerson` with `onRowChange`, the
|
|
715
|
+
value listeners and a remount, other costs, `taxed`), the CSS contract, and nexus.template's `/demo/tabular-form` "A
|
|
716
|
+
goods receipt" with `rows-solve.cjs` (50 in each motion mode): figures on open and per keystroke, exact (Enter) and
|
|
717
|
+
nearest (Tab), Escape, a refusal, Tab order, a click into the Rate, Donation through the form's `computeWith`, Save
|
|
718
|
+
sending neither worked-out figure; 7 themes light and dark at 1280 and 390 wide, the Amount and the note at 4.5:1 or
|
|
719
|
+
more. `tabular-form.cjs` (85), `group-form.cjs` (61), `form-confirm.cjs` (80) and `input-memory.cjs` (22) unchanged.
|
|
720
|
+
|
|
721
|
+
### The mail client's fixes, and a Crud form's computeWith
|
|
722
|
+
|
|
723
|
+
Card C-mail-client-fix (2026-11-10), the four gaps nexus.web's Message › Inbox (card MAIL-client-pages) found in the mail
|
|
724
|
+
client, and the gap C-rows-solve left for a Crud page (Inventory's Dakhila). Additions only: 5 exports (`nexus-shared`:
|
|
725
|
+
`followMailReadMarks`, `followMailboxReadMarks`, `MailReadMarks`, `crudComputeWith`, `sameShallow`) and new members of
|
|
726
|
+
exports already listed (`MailClientProps.title` may be `false`, `.description`, `.headingLevel`;
|
|
727
|
+
`CrudFormOptions.computeWith`). **Two behaviours change:** a `threadId` that names a chat opens its chat window, and the
|
|
728
|
+
client's folder counts and its `onChanged` wait for the read marks on their way.
|
|
729
|
+
|
|
730
|
+
- **A long list no longer makes the page scroll.** `.nx-mailbox__rows` scrolled but was not positioned, so each unread
|
|
731
|
+
row's visually hidden ", unread" (`nx-visually-hidden` is `position: absolute`) was placed against the page at its
|
|
732
|
+
row's place down the list: 126 unread rows made the demo page 8,253px tall instead of 3,286px, and nexus.web's Inbox
|
|
733
|
+
taller than the window. Every scroller of the client and the chat is now `position: relative` (the list, the rail, the
|
|
734
|
+
reader's body and record card; the chat's thread, list, New chat, mentions and people), and the CSS contract holds
|
|
735
|
+
every rule of `Nexus.Mail.css` and `Nexus.Chat.css` that scrolls to setting a position (9 today).
|
|
736
|
+
- **A link to a chat opens its window.** A `threadId` the page sets (the bell's `?thread=`, an e-mail copy's) that names
|
|
737
|
+
a chat opens its chat window as a click on its row does - once its kind is known, from its row in the list or from its
|
|
738
|
+
head when the list shown does not hold it - once per opening, so a window closed stays closed until the row is clicked.
|
|
739
|
+
- **The counts wait for the read mark.** A conversation shows read as soon as it opens, before its read mark reaches the
|
|
740
|
+
server; the client read its counts at once, so the Unread folder stayed one too high (nexus.web's `mailbox.ts` waited
|
|
741
|
+
for its marks itself). The reader's and the chat windows' read marks now go through `followMailboxReadMarks`: the
|
|
742
|
+
folder counts are read once no mark is on its way, and read again (with `onChanged`, the page's bell) when each is
|
|
743
|
+
answered. A page needs no wrapper of its own for its counts.
|
|
744
|
+
- **One heading on a page.** `title={false}` draws no title in the bar (the search starts it), for a page whose own
|
|
745
|
+
heading names the client (nexus-admin's `AdminPage` with the menu row's words); `description` draws a sentence beside
|
|
746
|
+
the title, clipped to the rail's width with the whole of it on hover; `headingLevel` (default 2) makes it the page's h1.
|
|
747
|
+
- **`CrudFormOptions.computeWith(values, { action, row })`**: what a Crud's Add and Edit forms hand their worked-out
|
|
748
|
+
inputs (`compute`, and the rows' `crudInputs.lineAmount` with `taxed`), worked out from the form's own values after
|
|
749
|
+
every change and before the browser paints. The answer is compared field by field with the last (`sameShallow`), so a
|
|
750
|
+
fresh object of the same figures redraws nothing; a new one redraws the worked-out cells alone, with nothing remounted
|
|
751
|
+
and the focus kept. A rule that throws keeps its last answer and says why in the console (`crudComputeWith`).
|
|
752
|
+
- Proved by `test-mail-client.mts` (112: a mark held while on its way, let go and counted again once answered or refused,
|
|
753
|
+
every other door kept), `test-table.mts` (490: `crudComputeWith`, `sameShallow`), the CSS contract (3,035), and
|
|
754
|
+
nexus.template's browser checks on a production build of their own: `mail-client.cjs` 92 (new M: 126 unread rows keep
|
|
755
|
+
the page's height, every hidden word inside the client, the client as the window leaves the document no taller than it;
|
|
756
|
+
N: a link opens the chat's window, from its head when not listed, a closed one stays closed, a row click brings it back;
|
|
757
|
+
O: with read marks 1.5s slower than a count, the Unread folder still drops by one; P: one heading, the bar one height),
|
|
758
|
+
`rows-solve.cjs` 56 in each motion mode (new L: the receipt on a Crud page, `/demo/tabular-form` "The same receipt on a
|
|
759
|
+
Crud page", Source switched from the keyboard redraws the lines with the same inputs and the focus kept, a donation's
|
|
760
|
+
line typed in, the save's total), and `form-figures.cjs` 156 in each motion mode, unchanged.
|
|
761
|
+
|
|
762
|
+
### Details as the 2026-06-20 window drew them, a list of rows as a table, history names from the app
|
|
763
|
+
|
|
764
|
+
Card component-details-0620-look (2026-11-10), the owner's asks 1, 2 and 6 for the subscriber window, built for every
|
|
765
|
+
page with a details view.
|
|
766
|
+
|
|
767
|
+
- **The built-in details look** (`CrudDetails`: a Crud's details popup and workspace tab, a tree's panel, a form's
|
|
768
|
+
question): each field a label and its value in a box - a `--nx-border` hairline on `--nx-surface-2` - two pairs to a
|
|
769
|
+
line where the details are 46rem or wider and one where they are not, the label over its value under 20rem. The details
|
|
770
|
+
are their own size container (`nx-crud-details`), so a popup, a tab, a column beside a panel and a question's box each
|
|
771
|
+
get the look their width allows; every label has one width, so the values of both columns and of a field across the
|
|
772
|
+
line start on shared edges, and a label sits on its value's first line. Sections from the columns' `group` get a
|
|
773
|
+
numbered heading, "01. Contact", "02. Account", in the order they come. A `wrap` or Markdown column takes the whole
|
|
774
|
+
line. A form's question (`confirm`) now shows two fields to a line on a wide box, not three.
|
|
775
|
+
- **`TableColumn.table?: readonly TableColumn[]`**: the value is a list of rows (a subscriber's contacts, devices). The
|
|
776
|
+
details draw it as a small read-only table across the line - a header row, each row's number first, an option's tone
|
|
777
|
+
as its tag, a boolean as Yes or No in words - and say "None" for an empty list. A data table, an export and a search
|
|
778
|
+
show how many rows it holds (`formatCellText`).
|
|
779
|
+
- **`setAuditActorNames(resolver | null)`** and its type **`AuditActorNamesResolver`** = `(tokens: string[], signal:
|
|
780
|
+
AbortSignal) => Promise<Record<string, string>>` (`nexus-shared`): how `AuditHistory` names the person behind an entry
|
|
781
|
+
its answer names nobody for. It asks once for each page it shows, only for tokens it has had no answer for, and says
|
|
782
|
+
"by {the answer}" ("by Sita Rai (sita.rai)"); answers are kept for the page's life, a failed call keeps
|
|
783
|
+
"User 3fA9b2…" and is asked again by the next history, a token left out of the answer keeps it. The answer's own
|
|
784
|
+
`creatorName` still comes first.
|
|
785
|
+
- **A workspace's popup footer is the Details tab's alone.** View history, Edit, Close and the load's line show only
|
|
786
|
+
while the built-in Details tab is chosen; every other tab runs to the popup's foot and has its own `CrudTabFooter`. The
|
|
787
|
+
title bar's X and Esc still ask about unsaved changes, as do the footer's Edit and Close on Details. A details popup
|
|
788
|
+
without a workspace is unchanged.
|
|
789
|
+
- **A section that changes on its own** (`details.sections: [{ group, edit, label?, hidden? }]`, type
|
|
790
|
+
**`CrudDetailsSection`**): in a workspace's Details tab, a small ghost Edit at the end of the section's heading opens
|
|
791
|
+
the Crud's own Edit form (`edit: "edit"`) or one of its `rowForms` (`edit: "<id>"`) over the workspace, offered by the
|
|
792
|
+
row menu's rules (a list that offers Edit, the endpoint allowed, no lock); once saved the workspace reads the record
|
|
793
|
+
again (`refresh()`), the Details are drawn again in place, and the focus is back on the button. `CrudDetails` takes
|
|
794
|
+
`headingEnd(group)` for anything at the end of a heading, and follows a new `row` in place.
|
|
795
|
+
- **`openCrudForm`** and its **`CrudFormRequest`** are exported from `nexus-shared/client`: a controller's Add or Edit
|
|
796
|
+
form in a popup, opened from anywhere.
|
|
797
|
+
- **The workspace's profile picture is a circle** (`ProfilePicture`'s new `round`; the pictures window keeps the
|
|
798
|
+
picture's own shape and aspect).
|
|
799
|
+
- Proved by `test-audit-entries.mts` (25), `test-table.mts` (491), the CSS contract, and nexus.template's
|
|
800
|
+
`crud-workspace.cjs`, `form-confirm.cjs` and `crud-row-form.cjs` on a production build.
|
|
801
|
+
|
|
802
|
+
## 4.0.0
|
|
803
|
+
|
|
804
|
+
**Breaking: an input's `type` is a number, a member of `EInputTypes`, and a string is refused.** Everything else in
|
|
805
|
+
this version was prepared as `3.3.0`, which was never published; it follows under
|
|
806
|
+
[the changes prepared as 3.3.0](#also-in-400-the-changes-prepared-as-330).
|
|
807
|
+
|
|
808
|
+
Every export of the published `3.2.0` is still exported (287 are new: 226 from `nexus-shared`, 61 from
|
|
809
|
+
`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
|
|
810
|
+
[in the list at the end of these notes](#new-exports-no-section-names)), and no exported constant changed a value (86
|
|
811
|
+
compared), both checked against the published `3.2.0` with `Frontend/scripts/release-compare` (again on 2026-11-06,
|
|
812
|
+
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
|
|
813
|
+
every params type in it take `type` as an `EInputTypes` member, so every list of inputs an app writes changes.
|
|
814
|
+
`nexus-icons` stays `^1.2.0`; its published copy exports every name this package imports (125). The packages built on
|
|
815
|
+
this one move with it: `nexus-admin` `4.0.0` asks `^4.0.0` (a major, because its range crosses this one), and
|
|
816
|
+
`nexus-public` `3.0.1` asks `^3.0.0 || ^4.0.0` (it imports only `cx`).
|
|
817
|
+
|
|
818
|
+
### Breaking: an input's type is the numbered `EInputTypes`
|
|
819
|
+
|
|
820
|
+
The owner, 2026-10-30, pointing at `type: "switch"` and `type: "dropdown"` in an app's inputs: the old project held an
|
|
821
|
+
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#
|
|
822
|
+
enum can hold, and a member name that does not exist fails to compile where a mistyped string compiled.
|
|
823
|
+
|
|
824
|
+
```tsx
|
|
825
|
+
// 3.x
|
|
826
|
+
import type { InputParams } from "nexus-shared";
|
|
827
|
+
const INPUTS: InputParams[] = [
|
|
828
|
+
{ type: "text", name: "fullName", label: "Full name", required: true },
|
|
829
|
+
{ type: "dropdown", name: "tags", label: "Tags", multiple: true, options: TAGS },
|
|
830
|
+
{ type: "switch", name: "isActive", label: "Active" },
|
|
831
|
+
];
|
|
832
|
+
|
|
833
|
+
// 4.0.0
|
|
834
|
+
import { EInputTypes, type InputParams } from "nexus-shared";
|
|
835
|
+
const INPUTS: InputParams[] = [
|
|
836
|
+
{ type: EInputTypes.Text, name: "fullName", label: "Full name", required: true },
|
|
837
|
+
{ type: EInputTypes.Dropdown, name: "tags", label: "Tags", multiple: true, options: TAGS },
|
|
838
|
+
{ type: EInputTypes.Switch, name: "isActive", label: "Active" },
|
|
839
|
+
];
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
`EInputTypes` is written as `ENumberScales` is: an `as const` object and a type of the same name, so it is a value and
|
|
843
|
+
a type, works in a server component, and `InputParams` still narrows on it (a dropdown's `multiple: true` still makes
|
|
844
|
+
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
|
|
845
|
+
value left out is never a valid one, and a released number is never reused or given another meaning.
|
|
846
|
+
|
|
847
|
+
| Block | Kind | Member | Number | Was |
|
|
848
|
+
|---|---|---|---|---|
|
|
849
|
+
| 1–19 | Text boxes | `Text` | 1 | `"text"` |
|
|
850
|
+
| | | `Email` | 2 | `"email"` |
|
|
851
|
+
| | | `Password` | 3 | `"password"` |
|
|
852
|
+
| | | `Search` | 4 | `"search"` |
|
|
853
|
+
| | | `Tel` | 5 | `"tel"` |
|
|
854
|
+
| | | `Url` | 6 | `"url"` |
|
|
855
|
+
| | | `TextArea` | 7 | `"textarea"` |
|
|
856
|
+
| | | `Code` | 8 | `"code"` |
|
|
857
|
+
| 20–39 | Choices | `CheckBox` | 20 | `"checkbox"` |
|
|
858
|
+
| | | `Switch` | 21 | `"switch"` |
|
|
859
|
+
| | | `Radio` | 22 | `"radio"` |
|
|
860
|
+
| | | `CheckBoxGroup` | 23 | `"checkbox-group"` |
|
|
861
|
+
| | | `RadioGroup` | 24 | `"radio-group"` |
|
|
862
|
+
| | | `Dropdown` | 25 | `"dropdown"`, one item or many by `multiple` |
|
|
863
|
+
| 40–59 | Dates and times | `Date` | 40 | `"date"` |
|
|
864
|
+
| | | `DateRange` | 41 | `"date-range"` |
|
|
865
|
+
| | | `Time` | 42 | `"time"` |
|
|
866
|
+
| | | `DateTime` | 43 | `"date-time"` |
|
|
867
|
+
| 60–79 | Numbers and sliders | `Number` | 60 | `"number"` |
|
|
868
|
+
| | | `Slider` | 61 | `"slider"` |
|
|
869
|
+
| | | `RangeSlider` | 62 | `"range-slider"` |
|
|
870
|
+
| 80–99 | Editors | `Markdown` | 80 | `"markdown"` |
|
|
871
|
+
| | | `RichText` | 81 | `"rich-text"` |
|
|
872
|
+
| 100–119 | Structures | `Tree` | 100 | `"tree"` |
|
|
873
|
+
| | | `Tabular` | 101 | `"tabular"` |
|
|
874
|
+
| | | `Group` | 102 | `"group"` |
|
|
875
|
+
| | | `Picture` | 103 | new: [a record's picture](#pictures-and-documents-declared-by-params) |
|
|
876
|
+
|
|
877
|
+
Every number fits a byte. To move an app: import `EInputTypes` from `nexus-shared` and write the member in place of
|
|
878
|
+
each string in the table; the type checker lists every place it is still a string. A page that compared an input's type
|
|
879
|
+
(`params.type === "radio"`) compares the member. Unchanged: a `TextBox`'s own `type` prop is still its HTML type
|
|
880
|
+
(`<TextBox type="email" />`), which is not an input's params; a dropdown column's `type` (`"code"`, `"muted"`), a table
|
|
881
|
+
column's `format` and the chat's conversation `type` are not inputs either and keep their strings.
|
|
882
|
+
|
|
883
|
+
New beside it, and counted in the 30 above: `TextInputType` (the six text box members, whose `TextBox` gets its HTML
|
|
884
|
+
type from one map in the renderer) and `inputTypeName(type)`, the member's name for a message or a log
|
|
885
|
+
(`inputTypeName(EInputTypes.RichText)` is `"RichText"`; a number that is no member comes back as its digits). A table's
|
|
886
|
+
filter that cannot filter now warns with the name: *a RichText input cannot filter rows*.
|
|
887
|
+
|
|
888
|
+
### Pictures and documents, declared by params
|
|
889
|
+
|
|
890
|
+
Card D2 of the subscription demo round (2026-11-02). A record's pictures and documents - a company's logo, a branch's
|
|
891
|
+
pictures, a person's own picture, a subscriber's documents - are the files of an **owner** (a row of a module) in a
|
|
892
|
+
**category** (what they are for, with its limits), and a page adds them by declaration:
|
|
893
|
+
|
|
894
|
+
```tsx
|
|
895
|
+
import { EInputTypes, NexusModule } from "nexus-shared";
|
|
896
|
+
import { DocumentList, openPictureGallery, pictureColumn } from "nexus-shared/client";
|
|
897
|
+
|
|
898
|
+
const BRANCH_PICTURES = { module: NexusModule.Enterprise, category: "branch-picture", aspect: 4 / 3 };
|
|
899
|
+
|
|
900
|
+
// A form: the owner's main picture, changed at once (Upload, Edit, Remove, All pictures).
|
|
901
|
+
inputs: (action, row) => [{ type: EInputTypes.Picture, name: "picture", label: "Picture", ...BRANCH_PICTURES, ownerId: row?.id }];
|
|
902
|
+
// A table: each row's picture, the page's rows read in one call; a click opens the row's gallery.
|
|
903
|
+
columns: [pictureColumn({ ...BRANCH_PICTURES, rowTitle: row => row.branchName }), ...];
|
|
904
|
+
// A window, and a list for a category that takes PDF.
|
|
905
|
+
openPictureGallery({ ...BRANCH_PICTURES, ownerId: branch.id, title: branch.branchName });
|
|
906
|
+
<DocumentList module={NexusModule.Subscription} category="subscriber-document" ownerId={subscriber.id} />;
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
- **`EInputTypes.Picture` (103)**, the next number of the structures block: `PictureInput`, the owner's main picture
|
|
910
|
+
with Upload (choose, edit with the shape locked, store, make it the main one), Edit (a new picture from the main one),
|
|
911
|
+
Remove (no main picture; the pictures stay) and All pictures. In a category that keeps one file, Upload is Replace
|
|
912
|
+
(the new picture takes the kept one's place once the editor has saved) and Remove is Delete, so the input is never
|
|
913
|
+
left with a file it cannot delete. It holds no value of the form: the form store keeps no field for it
|
|
914
|
+
(**`holdsValue`**), so a save sends nothing under its name, and it takes no `required`, since the form could not
|
|
915
|
+
refuse a save without a picture. A record not saved yet (`ownerId` null) says "Save the {subject} first".
|
|
916
|
+
- **`PictureGallery` / `openPictureGallery`**: upload several at once up to the category's count (each clamped to
|
|
917
|
+
2048 px and given its thumbnail by the editor's `renderImage`, without opening it; a picture's size is checked once
|
|
918
|
+
it is made, so a phone photo over the category's size is stored), make one the main picture, view
|
|
919
|
+
them in the editor module's viewer, edit one (saved as a new picture), delete. **`DocumentList`**: the same for a
|
|
920
|
+
category that takes PDF, as a list (kind, size, date, open, rename, delete); the gallery picks it by itself, and
|
|
921
|
+
`openPictureGallery` without `view` names its window after it ("Documents") once the category is read.
|
|
922
|
+
- **`pictureColumn`**: each row's picture; every row a table draws asks `primaryPictures` (`PrimaryPictures`), which
|
|
923
|
+
gathers the asks of one moment into **one call per module and category**, at most the adapter's `maxOwners` (200),
|
|
924
|
+
and keeps the answers until a change on the page writes one in, so a row redraws without a reload. A refused or
|
|
925
|
+
failed read is kept as no picture for `retryAfter` (30 s), so rows that draw again do not each ask again; `clear()`
|
|
926
|
+
also drops the reads under way, so an ask after a sign-out never gets the answer meant for the person before.
|
|
927
|
+
- **`loadImageEditor()`** (about 1 KB): the image editor (`nexus-image-editor`) is **never in an app's bundle**. The
|
|
928
|
+
adapter names its versioned address (with Nexus, Drive's `Assets/File/nexus-image-editor.txt`), and the loader
|
|
929
|
+
imports it with the bundler told to leave the import alone (`webpackIgnore` / `turbopackIgnore`; proved in a
|
|
930
|
+
Next.js 16 Turbopack build: the chunk holds a plain `import(url)`), once per page; a failed load is not kept.
|
|
931
|
+
`preloadImageEditor()` starts it on the hover of an Edit or Upload button. Its types are in `ImageEditorModule`.
|
|
932
|
+
- **`FileAdapter`** is the abstract class an app implements for its own file backend (`list`, `primaries`, `upload`,
|
|
933
|
+
`setPrimary`, `rename`, `remove`, `fileUrl`, `editorModuleUrl`), registered once with `defineFileAdapter`;
|
|
934
|
+
**`NexusDriveAdapter`** is the default: a write ticket from the owner's module (`Files/Ticket/{category}/{ownerId}`,
|
|
935
|
+
kept until a minute before it runs out, asked again once when Drive answers `DRV_X011`), a read ticket
|
|
936
|
+
(`Files/Owners/{category}`) for a page's rows and for a person who may see but not change an owner's files, then
|
|
937
|
+
Drive's `File/List`, `Primaries`, `Upload`, `SetPrimary`, `Rename`, `Delete` and `View/{id}/{Original|Thumbnail}`.
|
|
938
|
+
Nothing generic names a backend's numbers: a category's kinds are MIME types.
|
|
939
|
+
- New shared helpers: `fileRefusal` (a kind or size the category refuses, said before the upload), `fileKindsSentence`,
|
|
940
|
+
`fileSizeText`, `roomLeft`, `takesDocuments`, `pictureName`; one stylesheet, `Nexus.Files.css` (`nx-picture`,
|
|
941
|
+
`nx-picture-input`, `nx-files`), whose hovered pictures get a ring and never a new size.
|
|
942
|
+
|
|
943
|
+
Proved by `npm run test:files` (the adapter against a pretend backend: the tickets, the fresh one after `DRV_X011`, the
|
|
944
|
+
read-only fallback, the page's order, the multipart fields, the pointer file) and nexus.template's `/demo/pictures`
|
|
945
|
+
with its browser check `pictures.cjs`, on a pretend Drive that answers as the C# one.
|
|
946
|
+
|
|
947
|
+
### The editor loads in every bundler; a reason, initials, one action locked, options narrowed
|
|
948
|
+
|
|
949
|
+
Card C1 of the subscription demo's second round (2026-11-02): fixes to the pictures above, and two features the
|
|
950
|
+
subscription pages needed and kept as code of their own.
|
|
951
|
+
|
|
952
|
+
- **The image editor loads in an app's production build.** The package build rewrites "./x.ts" imports to "./x.js"
|
|
953
|
+
(`rewriteRelativeImportExtensions`), and for an `import()` of a variable it wrapped the address in a run-time helper,
|
|
954
|
+
`import(__rewriteRelativeImportExtension(url))`. The bundler-ignore comments were then inside a call, and Next 16's
|
|
955
|
+
Turbopack compiled the import to "Cannot find module as expression is too dynamic": in every app built on `dist`,
|
|
956
|
+
Upload and Edit on a picture never opened the editor (an app reading the source, like nexus.template, never saw it).
|
|
957
|
+
The import now lives alone in **`importModule(url)`** (`Services/ModuleImport.ts`, which imports nothing), built a
|
|
958
|
+
second time without the rewrite (`tsconfig.build.module-import.json`, the second step of `npm run build` and of
|
|
959
|
+
`copy-packages`' watch), so `dist` keeps `import(/* webpackIgnore: true */ /* turbopackIgnore: true */ /* @vite-ignore */ url)`.
|
|
960
|
+
The setting stays for every other file. `scripts/test-dist-loader.mts` (in `npm test`) runs the build's steps into a
|
|
961
|
+
scratch folder and fails if any built file holds the helper.
|
|
962
|
+
- **An editor that cannot load says so.** `loadImageEditor()` rejects in plain words ("The image editor could not be
|
|
963
|
+
downloaded. Check the connection and try again; ...") and logs the cause (a network error, a 404, a bundler that
|
|
964
|
+
rewrote the import) with `console.error`; the picture input, the gallery and the viewer show it as a toast.
|
|
965
|
+
- **A picture input says why it is read only.** `OwnerFilesParams.lockReason` (on `PictureInput`, `PictureGallery`,
|
|
966
|
+
`openPictureGallery`, and `pictureColumn` as a sentence or `row => sentence`) makes the files read only with that
|
|
967
|
+
sentence under them: a record that is closed, a year that is locked. Without it, a person the backend lets only look
|
|
968
|
+
is told the backend's own words: `NexusDriveAdapter` keeps the refused write ticket's message as
|
|
969
|
+
`OwnerFiles.readOnlyReason`, and **`READ_ONLY_REASON`** stands in when there is none.
|
|
970
|
+
- **Initials for an owner with no picture.** `OwnerPicture` `name` and `pictureColumn` `nameKey` (the row's field with
|
|
971
|
+
a name): without a picture, the name's initials, on the tone the shared `Avatar` gives that name
|
|
972
|
+
(`.nx-picture--initials`; the avatar tones now also apply to `.nx-picture[data-tone]`).
|
|
973
|
+
- **One action locked on a row: `Crud` `rowActionLock`**: `row => ({ trash: "The head office stays open..." })` switches
|
|
974
|
+
off single actions of a record, each with its sentence in the row's menu, in a selection that holds it, and for
|
|
975
|
+
`edit` in its details too; the rest still run. `rowLock` still locks every change at once. New types
|
|
976
|
+
`CrudLockableAction`, `CrudActionLocks`.
|
|
977
|
+
- **Options narrowed by another input: `narrowedBy`** on any input (Form only), `{ name, key?, placeholder? }`: a
|
|
978
|
+
Package list only of the Group picked. The form hands the input its params narrowed by the other value as it draws
|
|
979
|
+
(`narrowInputParams`, `narrowingValue`, `narrowingPlaceholder`, pure): static `options` filtered, `loadOptions` asked
|
|
980
|
+
with the new **`DropdownPageRequest.narrow`**, a load key per value, nothing offered and "Pick a group first" while the
|
|
981
|
+
other is empty; the form store follows it as `dependsOn` would and clears the narrowed value when a person changes the
|
|
982
|
+
other (never when code sets it). `crudPicker` answers `narrow` from its kept list, or, with the new **`narrowQuery`**,
|
|
983
|
+
asks the server (`activeListing?{narrowQuery}={value}`, one kept list per value, dropped with the others on a save).
|
|
984
|
+
`FormStore.inputParams(name)` is new.
|
|
985
|
+
- **A list narrowed by a box of its own, remembered per viewer: `Crud` `narrowPicker`** (`CrudNarrowPicker`): the
|
|
986
|
+
picker's params, the box's label, `remember: { key, viewer }` and `onPick`. The CRUD draws the box in its toolbar, keeps
|
|
987
|
+
the pick as `narrow` would, and opens the same pick on the next visit in this browser for the same viewer while the
|
|
988
|
+
box still offers it; only the record's key is stored. `useCrudNarrowPicker` is exported (`nexus-shared/client`).
|
|
989
|
+
|
|
990
|
+
Proved by `npm test` (`test-dist-loader`, `test-input-narrowing`, `test-files`' reason), nexus.template's `/demo/pictures`
|
|
991
|
+
(`pictures.cjs`: initials, the lock reason, an editor whose module is refused) and `/demo/crud`'s "One group at a time"
|
|
992
|
+
(`crud-narrowing.cjs`), and nexus.web's production build against a Drive API.
|
|
993
|
+
|
|
994
|
+
Card C1-fix (the review of C1, 2026-11-02):
|
|
995
|
+
|
|
996
|
+
- **`narrowedBy` clears down a chain.** A person changing the Country clears the Region, and now the District picked
|
|
997
|
+
among that Region's too, whatever order the form lists them in (before, the District kept a value of the old Region,
|
|
998
|
+
and could be saved).
|
|
999
|
+
- **A narrowed input's getters answer on every draw.** `Form` copied a `narrowedBy` input's params once and kept the
|
|
1000
|
+
copy, so a `readOnly`, `hint`, `label` or `options` getter that `dependsOn` follows stayed as it first read. The
|
|
1001
|
+
copy is now made afresh each time the input draws.
|
|
1002
|
+
- **A `narrowPicker`'s words name its box**: "Pick a group in the Group box to see its amenities.", "... to add a new
|
|
1003
|
+
amenity for it." on Add, and "Added for Bathroom: pick another group in the Group box to add elsewhere." on the Add
|
|
1004
|
+
form's fixed field (before, all three said "the other list"). New on `CrudNarrow` (and so on `narrowPicker`):
|
|
1005
|
+
**`addMessage`** and **`fixedMessage: name => sentence`**, beside `emptyMessage`; the words are
|
|
1006
|
+
**`narrowPickWords(label, subject, plural)`**.
|
|
1007
|
+
- **The remembered pick keeps the key alone, and only for a viewer.** No name is written to local storage (a pick kept
|
|
1008
|
+
with one by C1's build still opens, and is written again without it), and without `remember.viewer` nothing is
|
|
1009
|
+
remembered: **`narrowPickerStorageKey`** (now from `nexus-shared`) answers `null`. A kept pick is found again by its
|
|
1010
|
+
key alone, **`findNarrowPick`**: `resolveOptions` when the picker has it, else the box's pages at 50 a page (never one
|
|
1011
|
+
page of everything, which a server-paged source refuses). `readNarrowPick`, `writeNarrowPick`,
|
|
1012
|
+
`narrowPickerPlaceholder` are the rest of `Helpers/CrudNarrowPick.ts`, pure.
|
|
1013
|
+
- **An owner not in use is not "not found".** When a module refuses the write ticket with a 404 (the owner pending,
|
|
1014
|
+
ended, in the trash or deleted) and a read ticket still finds it, the files say **`OWNER_NOT_IN_USE_REASON`** ("This
|
|
1015
|
+
record is not in use now, so its files can be seen but not changed.") rather than "The record was not found.".
|
|
1016
|
+
- **`test-dist-loader` checks this folder's whole `dist`** with the scratch build's rules (ModuleImport.js there, its
|
|
1017
|
+
import plain, no helper in any file), so a dist built with only the first step fails.
|
|
1018
|
+
|
|
1019
|
+
Proved by `npm test` (23 suites; `test-input-narrowing` 49, `test-files` 73, `test-dist-loader` 17) and nexus.template's
|
|
1020
|
+
`crud-narrowing.cjs` (55, both motions: the three sentences, a narrowed Icon whose hint follows "Free for guests"),
|
|
1021
|
+
`staff-posts.cjs` (86, the other-list words unchanged), `crud.cjs`, `forms.cjs` (154) and `pictures.cjs` (83).
|
|
1022
|
+
|
|
1023
|
+
### A record's workspace: a profile and tabs in the details popup
|
|
1024
|
+
|
|
1025
|
+
Card TABS-comp (2026-11-03), from the owner's request for the tab buttons of the 2026-06-20 project's record popups:
|
|
1026
|
+
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
|
|
1027
|
+
left and the chosen tab on the right. Additions only.
|
|
1028
|
+
|
|
1029
|
+
- **`CrudDetailsOptions.tabs`**: `{ id, label, icon?, endpoint?, hidden?(row), badge?, render(context) }[]`. A tab that
|
|
1030
|
+
names an `endpoint` shows only to a person who may call it (the rule of `CrudDetailsAction`, again whenever the rights
|
|
1031
|
+
change). `render` is handed a **`CrudDetailsTabContext`**: `row` (as the list has it now), `form` (the record in the
|
|
1032
|
+
`FormStore` the `aside` is handed: the row, then the `details` endpoint's record, then each refresh's), `popup`, `tab`,
|
|
1033
|
+
`select(tabId)` and **`refresh()`**, which reads the record again from the `details` endpoint and writes it into the
|
|
1034
|
+
table, the pinned band, every list kept and every tree (`trees`), as an edit's answer is - no list is read again - and
|
|
1035
|
+
answers the record (or `null`). The built-in tab `details` (the fields, with `aside` beside them) comes first; list
|
|
1036
|
+
`{ id: "details" }` to move or rename it, give it `hidden: () => true` to leave it out, or a `render` to replace it.
|
|
1037
|
+
Only the chosen tab is drawn. With tabs the popup's default size is `full`.
|
|
1038
|
+
- **`CrudDetailsOptions.profile`**: `row => ({ picture?, title, subtitle?, tags? })` or anything to draw. `picture` is
|
|
1039
|
+
the owner's main picture by the params the picture column takes (`{ ...GUEST_PICTURE, ownerId }`, a
|
|
1040
|
+
**`CrudDetailsPicture`**: a click opens the pictures, initials stand in without one) or a node; `tags` are
|
|
1041
|
+
`{ label, tone? }` in the look of a table's tags. It follows the record as it is loaded and refreshed.
|
|
1042
|
+
- **`CrudDetailsOptions.remember`**: `{ key, viewer }` keeps the last tab chosen in this browser
|
|
1043
|
+
(`nx-crud-tab:{key}:{viewer}`) and opens it next time while it is still offered; nothing is kept without a viewer.
|
|
1044
|
+
- The layout: a 20rem column (the picture 10rem, the name, the line, the tags, then the tabs as `design="list"` rows)
|
|
1045
|
+
and the tab beside it, which scrolls inside itself; the popup's body never scrolls and a switch never changes its
|
|
1046
|
+
size. The workspace is a size container: below 720px of popup width (a phone) the profile is one row (a 3rem
|
|
1047
|
+
picture) and the tabs one strip that scrolls sideways, above the tab. The focus opens on the chosen tab, a switch
|
|
1048
|
+
that takes away the panel holding the focus puts it on the chosen tab, and it never drops to the page.
|
|
1049
|
+
- **`Tabs`** and **`TabPanel`** (`nexus-shared/client`): tabs that switch panels, as a WAI-ARIA `tablist` - the one
|
|
1050
|
+
piece `TabButtons`' own doc pointed to and the package did not have. The look is `TabButtons`', from the same
|
|
1051
|
+
stylesheet (`design` `list`, `nav`, or `auto`: the list, and a strip in a container under 720px); the chosen tab is
|
|
1052
|
+
`aria-selected` and the only one in the Tab order, the arrows (either way), Home and End move and open, the panel is
|
|
1053
|
+
joined by `tabIds(id, value)` and takes the focus itself when it holds nothing that does. `TabOption`, `TabsDesign`,
|
|
1054
|
+
`TabsProps`, `TabPanelProps`.
|
|
1055
|
+
- The tabs' rules without a browser, in `nexus-shared`: `offeredCrudTabs`, `openingCrudTab`, `crudTabStorageKey`,
|
|
1056
|
+
`readCrudTab`, `writeCrudTab`, `isCrudProfile`, `isCrudPicture`, `CRUD_DETAILS_TAB`, `OfferedCrudTab`.
|
|
1057
|
+
- `OwnerPicture` `size` also takes a CSS length (`"var(--nx-crud-profile-picture)"`), so a picture can follow a
|
|
1058
|
+
container query.
|
|
1059
|
+
- Styles: `Nexus.Crud.css` (`nx-crud-workspace`, `__frame`, `__frame--alone`, `__side`, `__profile`, `__nav`,
|
|
1060
|
+
`__panel`; `nx-crud-profile`, `__picture`, `__text`, `__title`, `__subtitle`, `__tags`) and `Nexus.Tab.Buttons.css`
|
|
1061
|
+
(the chosen option is `:checked` or `[aria-selected="true"]`, a button's own look taken away, `nx-tabs--auto`,
|
|
1062
|
+
`nx-tab-panel`).
|
|
1063
|
+
- Proved by `scripts/test-crud-details-tabs.mts` (33) and nexus.template's `/demo/crud` "A guest's workspace" with
|
|
1064
|
+
`scripts/browser-checks/crud-workspace.cjs` (60 in both motion modes: the click on a row, the keyboard, one Update
|
|
1065
|
+
and one Detail for a check-in with the row's visits written in, the remembered tab, a role without the endpoint, every
|
|
1066
|
+
theme light and dark at 1280x720 and 390x844).
|
|
1067
|
+
- Card TABS-fix1-comp (2026-11-03), from the review of the first page built on it:
|
|
1068
|
+
- **A form in a tab is never thrown away without asking.** A `SubmitForm` (and so an `ApiForm`) joins the place it
|
|
1069
|
+
is drawn in by itself (`FormLeaveGuard`, by the rule of its "leave the page?" question: not while locked, not when
|
|
1070
|
+
a draft keeps it); while one has changes, choosing another tab, Close, Edit, the popup's X, Esc and its bar on the
|
|
1071
|
+
dock ask "Discard your changes?". Keep editing stays where it was. No new export.
|
|
1072
|
+
- **`refresh()` lands after the popup closed.** Only the first read of the record stops when the popup closes; a
|
|
1073
|
+
save in a tab followed at once by Close still writes the row's new status and dates into the lists.
|
|
1074
|
+
- **A record off the page shown is patched in place.** `refresh()` hands the record as the popup held it, so one
|
|
1075
|
+
opened from the pinned band, or from a page turned since, is written into every page kept that has it: no page is
|
|
1076
|
+
dropped and the page shown is not read again.
|
|
1077
|
+
- **A popup closed over another never leaves the focus on the page.** When its opener cannot take the focus back (an
|
|
1078
|
+
item of a menu that closed, a button disabled while its action runs), the popup now on top takes it where it puts
|
|
1079
|
+
it on opening: in a workspace, the chosen tab.
|
|
1080
|
+
- The examples here, in the README and in `CrudDetailsOptions.tabs`' doc are a guest's, as the template's demo is.
|
|
1081
|
+
|
|
1082
|
+
### A printable document, money in a currency, inputs shown by another, focus kept through a picture change
|
|
1083
|
+
|
|
1084
|
+
Card RF-comp (2026-11-03), from the whole-round review of the subscription demo's second round: three pieces the
|
|
1085
|
+
subscription pages had written as code of their own (findings rules-3, rules-4, rules-5), and the picture input and
|
|
1086
|
+
gallery keeping a keyboard's focus (packages-3).
|
|
1087
|
+
|
|
1088
|
+
- **`PrintDocument`** (`nexus-shared`, no state, so server components too): a receipt, a bill or a statement by params:
|
|
1089
|
+
`paper` (`"a5"`, the default, or `"thermal"`, an 80 mm roll), `title`, `subtitles`, `kind`, `stamp` ("Voided: ..."),
|
|
1090
|
+
`blocks` in order - `{ facts: [{ label, value, hidden? }] }` and `{ columns: [{ label, align? }], lines: [{ cells,
|
|
1091
|
+
note?, hidden? }], totals: [{ label, value }] }`, falsy entries left out - and `footer` (line breaks kept). On screen it
|
|
1092
|
+
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
|
|
1093
|
+
on the paper, black on white, at the paper's left edge, and the component writes the page's `@page` rule (A5 portrait
|
|
1094
|
+
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
|
|
1095
|
+
on the page (`body:has(.nx-print)`), so any other page prints as it is. **`PRINT_PAPERS`** are the papers as
|
|
1096
|
+
`TabButtons` options. Styles: `Styles/Nexus.Print.css` (`nx-print`, `--thermal`, `__head`, `__title`, `__kind`,
|
|
1097
|
+
`__stamp`, `__facts`, `__lines`, `__end`, `__note`, `__foot`). Types `PrintPaper`, `PrintFact`, `PrintColumn`,
|
|
1098
|
+
`PrintLine`, `PrintFactsBlock`, `PrintLinesBlock`, `PrintBlock`, `PrintDocumentProps`.
|
|
1099
|
+
- **Money in the company's currency, set once.** `DataTableProps.currency` (and `Crud` `table.currency`) writes every
|
|
1100
|
+
`money` column as "Rs. 1,250.00" in the cells, the details, the CSV and the print; **`TableColumn.currency`** gives a
|
|
1101
|
+
column its own, a text or `row => row.currencyText`. `formatCellText(column, value, options, row?)` takes the row for
|
|
1102
|
+
it and `CellTextOptions.currency` the table's; **`columnCurrency`** answers which. **`formatMoney(value, { currency,
|
|
1103
|
+
decimals = 2, locale, empty })`** writes the same anywhere else (a dashboard's figures, a receipt): "Rs. 1,250.00".
|
|
1104
|
+
`prefix` still works as before.
|
|
1105
|
+
- **An input shown by another input's value: `showWhen`** on any input (Form only): `{ name, in?, notIn?, filled? }`
|
|
1106
|
+
or a list of them, every one holding (`InputCondition`). Values compare as text (1 matches "1"), a list value matches
|
|
1107
|
+
`in` by any item. The form gives such an input a live `hidden` that reads its store and follows the inputs named as
|
|
1108
|
+
`dependsOn` would; a hidden input keeps its value and is not checked; its own `hidden` still hides it. Pure helpers
|
|
1109
|
+
`withInputConditions`, `inputShown`, `inputReadOnly`, `conditionsHold`, `conditionHolds`, `conditionsOf`, `isEmptyValue`.
|
|
1110
|
+
- **An input read-only by another input's value: `readOnlyWhen`** (Form only), the same `InputCondition`s: a live
|
|
1111
|
+
`readOnly` that follows the inputs named, the input's own `readOnlyMessage` as the reason, its value kept in the data
|
|
1112
|
+
and not checked while read-only; its own `readOnly` still locks it. A CRUD's Edit form holds the record's key and its
|
|
1113
|
+
Add form does not, so `readOnlyWhen: { name: "id", filled: true }` is a field picked on Add and fixed on Edit (a
|
|
1114
|
+
price's group).
|
|
1115
|
+
- **Options narrowed by a fixed value: `narrowedBy: { key, value, orUnset?, placeholder? }`** beside the
|
|
1116
|
+
`{ name }` form (`InputNarrowing` is now `InputNarrowingByInput | InputNarrowingByValue`): the subscriber's own group,
|
|
1117
|
+
known when the form opens; nothing followed, nothing cleared, a `null` value offers nothing. **`orUnset`** (both forms)
|
|
1118
|
+
also offers the options whose field is empty, a fee of every group: `DropdownPageRequest.narrow` carries it (and a
|
|
1119
|
+
`null` value with it), and `crudPicker` honours it from its kept list. `isFixedNarrowing` tells the forms apart.
|
|
1120
|
+
- **The picture input and the gallery keep a keyboard's focus.** Their buttons no longer switch themselves off while a
|
|
1121
|
+
change runs (`disabled` sent the focus to the page in every browser): they stay focusable, `aria-disabled`, their
|
|
1122
|
+
press ignored (**`busyProps(busy, run)`**). When a change ends with the focused button gone - Remove and Edit with
|
|
1123
|
+
the main picture, a star once its picture is the main one, a deleted file's buttons - the focus goes to the first
|
|
1124
|
+
control left in its tile, else in the input (**`useFocusThroughChange(root, busy)`**, `nexus-shared/client`).
|
|
1125
|
+
|
|
1126
|
+
Proved by `npm test` (24 suites; `test-input-conditions` new, 37; `test-input-narrowing` 61, `test-table` 439,
|
|
1127
|
+
`test-number-helpers` 125; the CSS contract), and nexus.template's `/demo/print-document` and `/demo/form`'s "Inputs
|
|
1128
|
+
shown by another input" (`print-document.cjs`, 52: the money cells, the receipt, its widths and `@page`, printed alone
|
|
1129
|
+
and black on white, a page without one printed whole, `showWhen` and the fixed narrowing, every theme light and dark at
|
|
1130
|
+
1280x720 and 390x844 at AA) and `pictures.cjs` (87: Remove, the star and Delete from the keyboard keep the focus).
|
|
1131
|
+
|
|
1132
|
+
### A server's page in a sandboxed frame; code text in a text area
|
|
1133
|
+
|
|
1134
|
+
Card R2-comp (2026-11-05), for the Report module's Print layouts page: its preview and print door answer a whole HTML
|
|
1135
|
+
page (`{ html }`, with its own `@page` sheet, screen style and a CSP that runs no script), and its editor holds a
|
|
1136
|
+
layout's HTML. Additions only.
|
|
1137
|
+
|
|
1138
|
+
- **`DocumentFrame`** (`nexus-shared/client`): the page in an `<iframe srcdoc>`, sandboxed `""` (an opaque origin) or,
|
|
1139
|
+
with `printable`, `"allow-same-origin allow-modals"`, so the handle's **`print()`** (`DocumentFrameHandle`) opens the
|
|
1140
|
+
print dialog for the page alone; never `allow-scripts`. Props (`DocumentFrameProps`): `html`, `title` (required; the
|
|
1141
|
+
frame's name to a screen reader), `loading` (the page area `aria-busy` at once; a read that lasts 0.4s dims the page
|
|
1142
|
+
and says `loadingMessage`, default "Loading…"), `error` (a `role="alert"` line over the last page, which stays),
|
|
1143
|
+
`emptyMessage` (default "Nothing to show yet."), `actions` (buttons at the end of the frame's bar - the form's bar and
|
|
1144
|
+
status line, shown only with actions or a status - which wrap under the line on a phone), `printable`, `className`,
|
|
1145
|
+
`style`, `ref`. It fills the height its container gives (a flex column, a grid row) and is never under 22rem; the page
|
|
1146
|
+
scrolls inside it, an A4 sheet sideways too. While loading or failed, an `html` of null keeps the last page; a
|
|
1147
|
+
printable page read again keeps its scroll while the title stays. A frame that becomes printable is a new frame (a
|
|
1148
|
+
sandbox counts from the next page). Styles in `Nexus.Print.css` (`nx-document-frame`, `__bar`, `__view`,
|
|
1149
|
+
`__view--busy`, `__page`, `__empty`).
|
|
1150
|
+
- **`TextArea` `code`** (and `code: true` in a TextArea's `InputParams`): the monospace token, lines kept as typed
|
|
1151
|
+
(`wrap="off"`, a long line scrolled sideways in the box), `tab-size: 2`, and `spellCheck`, `autoCapitalize`,
|
|
1152
|
+
`autoCorrect`, `autoComplete` off and `translate="no"`; Tab still leaves the box. `nx-field__control--code` in
|
|
1153
|
+
`Nexus.Input.css`; read-only and disabled code keep the look.
|
|
1154
|
+
- `.nx-form-status` takes its room beside a bar's buttons from `--nx-form-status-basis` (default 14rem, as before), so
|
|
1155
|
+
the frame's bar, whose words are few, keeps two buttons beside its line in a half-width frame (10rem).
|
|
1156
|
+
- A CRUD's details footer and the frame share one rule for a slow read (said after 0.4s, held 0.3s): `useSlowBusy`,
|
|
1157
|
+
internal, moved out of `Crud.tsx`.
|
|
1158
|
+
|
|
1159
|
+
Proved by `npm test` (25 suites; the CSS contract) and nexus.template's `/demo/document-frame`
|
|
1160
|
+
(`document-frame.cjs`, 68 and 69 with reduced motion: the sandboxes, srcdoc, the empty, slow and failed states, the
|
|
1161
|
+
bar's buttons by keyboard, Print firing the frame's `beforeprint` and `false` without a page or when not printable, the
|
|
1162
|
+
scroll kept, a script that never runs printable or sealed, the code text area, every theme light and dark at 1280x720
|
|
1163
|
+
and 375x812 with no overflow and the status lines at AA), with `crud.cjs` and `crud-workspace.cjs` for the footer.
|
|
1164
|
+
|
|
1165
|
+
### A field at the top of a CRUD popup's column keeps its label and focus ring whole
|
|
1166
|
+
|
|
1167
|
+
Card R2-fix-label (2026-11-05), the owner's "overflow hidden bug": in a subscriber's window, the outlined "New note" box
|
|
1168
|
+
first in the notes beside the details showed only the lower half of its raised label, and its focus outline was cut at
|
|
1169
|
+
the top and at the panel's right edge. No export changed.
|
|
1170
|
+
|
|
1171
|
+
- **`.nx-crud-aside__main` and `.nx-crud-aside__side`** (side by side, from 1000px) each scroll on their own, and a
|
|
1172
|
+
scroll box clips at its padding edge, so a field first in either lost what it draws past its box. Both now keep room
|
|
1173
|
+
inside their edges, paid back by a negative margin so nothing moves: above, an outlined field's raised label (0.6em
|
|
1174
|
+
of the xs text) and its 2px outline; on every side, the 3px focus ring. The form's column had only 2px (rings), now
|
|
1175
|
+
the same room; the panel's column had none.
|
|
1176
|
+
- **`scroll-padding`** on those columns, `.nx-crud-workspace__panel` and **`.nx-popup__body`**: focus scrolls a field's
|
|
1177
|
+
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
|
|
1178
|
+
to 7.2px. The room past the control is now kept the same way in every popup.
|
|
1179
|
+
- Proved by nexus.template's `/demo/crud` "A guest's workspace", which now has the guest's notes beside the details (a
|
|
1180
|
+
form first) and the guest's card beside the Add and Edit forms, and `scripts/browser-checks/crud-field-room.cjs`.
|
|
1181
|
+
It measures the label and the outline's and shadows' reach against every box that clips them, focused with text,
|
|
1182
|
+
in the standard, outlined and inbox designs and the sm, md and lg sizes, light and dark, at 1280x720, 1040x720 and
|
|
1183
|
+
390x844, and at each scroll edge. 306 pass. On the old CSS 108 failed: every case in a column side by side. With
|
|
1184
|
+
`scroll-padding` off, 20 failed: the edge cases.
|
|
1185
|
+
|
|
1186
|
+
### A short list read whole; paging in the footer only
|
|
1187
|
+
|
|
1188
|
+
Card C-table-whole (2026-11-05), the owner on a subscriber's notes: *"footer have gap top side have gap ... used only
|
|
1189
|
+
footer for this types of datatable"*, *"or do not put limit here"* (a subscriber keeps at most 50 notes). Additions only:
|
|
1190
|
+
a table without the new prop draws as before.
|
|
1191
|
+
|
|
1192
|
+
- **`DataTable` `pager`** (`TablePagerMode`: `"bar"` | `"footer"` | `"none"`, default `"bar"`), where the paging lives:
|
|
1193
|
+
- `"bar"`: rows per page inside the search box (or the bar without a search), the pager in the footer - as before.
|
|
1194
|
+
- `"footer"`: rows per page beside the pager, in the footer; a table with nothing else on top draws no bar.
|
|
1195
|
+
- `"none"`: every row on one page, for a short list read whole: no pager, no rows per page, no remembered or
|
|
1196
|
+
preferred page size, and no footer unless a search or the filters leave rows out ("Showing 1–2 of 2 · filtered from
|
|
1197
|
+
6"). The count line stays as a visually hidden live line, so a screen reader still hears it. With
|
|
1198
|
+
`searchable={false}`, `columnsMenu={false}`, `exportable={false}` and `printable={false}` the table is its rows
|
|
1199
|
+
alone in its frame (the bar already hid itself when it held nothing; the empty footer band was what was left). A
|
|
1200
|
+
server's pages cannot all be shown, so `paging="server"` takes `"none"` as `"footer"`.
|
|
1201
|
+
- **`Crud`** passes it through `table: { pager }` (`CrudTableOptions`).
|
|
1202
|
+
- Proved by nexus.template's `/demo/data-table` "A short list read whole" (`data-table-pager.cjs`, 52 in each motion
|
|
1203
|
+
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
|
|
1204
|
+
default unchanged, every theme light and dark at 1280x720 and 390x844 with no overflow), with `data-table.cjs` and
|
|
1205
|
+
`crud.cjs`.
|
|
1206
|
+
|
|
1207
|
+
### A record's workspace draws its panel under the details
|
|
1208
|
+
|
|
1209
|
+
Card C-workspace-below (2026-11-05), the owner on a subscriber's window: *"make width full 100% for right side
|
|
1210
|
+
content. the details page make full width 100% then bellow show others informations"*. No export changed; one option
|
|
1211
|
+
was added to an exported type.
|
|
1212
|
+
|
|
1213
|
+
- **`CrudDetailsOptions.asidePlace`**: `"beside"` | `"below"`. `"below"` draws `details.aside` under the fields at
|
|
1214
|
+
every width: the fields across the whole width of the tab, the panel across the whole width under them, as tall as
|
|
1215
|
+
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
|
|
1216
|
+
its own clips a raised label or a focus ring. `"beside"` is the layout as it was: side by side from 1000px, stacked
|
|
1217
|
+
under that.
|
|
1218
|
+
- **The default changed for a record's workspace** (`tabs` or `profile`): `"below"`, so its Details tab reads as the
|
|
1219
|
+
record first, then what belongs to it. Only the Subscribers page passes a workspace aside today; a plain details
|
|
1220
|
+
popup keeps `"beside"`.
|
|
1221
|
+
- `nx-crud-aside--below` in `Nexus.Crud.css`; the side-by-side rules now name `:not(.nx-crud-aside--below)`.
|
|
1222
|
+
- Proved by nexus.template's `/demo/crud` "A guest's workspace" (a switch: notes under the details / beside them) and
|
|
1223
|
+
`crud-aside-below.cjs` (82 per motion mode: widths, order, no scroll of its own, the note field's label and ring, at
|
|
1224
|
+
1280x720, 1040x720 and 390x844, light and dark); `crud-field-room.cjs` now picks "beside" for the columns it
|
|
1225
|
+
measures; `crud-workspace.cjs` and `crud.cjs` unchanged.
|
|
1226
|
+
|
|
1227
|
+
### A choice drawn as tabs
|
|
1228
|
+
|
|
1229
|
+
Card C-choice-tabs (2026-11-05), the owner on the counter's "What is it?" (Payment, Activate, Disconnect, each with a
|
|
1230
|
+
sentence under it): *"if we make it tab types of design maybe it batter. (nav style) the helper label should be tool tips
|
|
1231
|
+
or somethings"*. Additions only.
|
|
1232
|
+
|
|
1233
|
+
- **`RadioGroup` `design: "tabs"`** (`RadioGroupDesign` = `ChoiceDesign | "tabs"`, in `nexus-shared/client`; a
|
|
1234
|
+
RadioGroup's `InputParams` take it too): the options as nav-style tabs on one `TabButtons` track, still native
|
|
1235
|
+
radios in a fieldset with `role="radiogroup"` - the arrow keys move the choice, `required` and `validate` stop a
|
|
1236
|
+
form's save with the group's message (the track edged in the danger colour), `readOnly` keeps the value with its
|
|
1237
|
+
lock and note, `disabled` dims every tab, `size` sm/md/lg is the track's. The tabs wrap onto more rows where they do
|
|
1238
|
+
not fit (a phone). `orientation` and `columns` do not apply.
|
|
1239
|
+
- **Each option's `description`** is its tooltip (`title`, when it is text) and its input's accessible description
|
|
1240
|
+
(`aria-describedby` to its note, which a screen reader reads even while hidden). The chosen option's note shows
|
|
1241
|
+
under the track, since a phone has no tooltip; it follows every change and a form's reset (`showChosenNote`, the
|
|
1242
|
+
group's `sync` rule), with no React state.
|
|
1243
|
+
- **`Nexus.Tab.Buttons.css`**: the rules that raise each chosen option on its own (`multiple`) are shared by
|
|
1244
|
+
`nx-tabs--wrap` (`:is(.nx-tabs--multiple, .nx-tabs--wrap)`, the same specificity), since a radio group's choice lives
|
|
1245
|
+
in its inputs, not in a value a sliding indicator could follow; only `multiple` keeps the pointer on a chosen option.
|
|
1246
|
+
`Nexus.Choice.css`: `nx-choices--tabs` (the danger edge) and `nx-choices__note`.
|
|
1247
|
+
- The read-only note finds a tab as it finds a choice (`optionAnchor`: `.nx-choice, .nx-tabs__option`).
|
|
1248
|
+
- Proved by nexus.template's `/demo/form` "A choice drawn as tabs" (`choice-tabs.cjs`, both motion modes: the track,
|
|
1249
|
+
the tooltips and descriptions, required, a click and the arrow keys, read-only, disabled, the values sent, a reset,
|
|
1250
|
+
every theme light and dark at 1280x720 and 390x844 with the labels and the note at AA), with `forms.cjs` and
|
|
1251
|
+
`read-only.cjs`.
|
|
1252
|
+
|
|
1253
|
+
### An input that remembers its last value, or opens on the company's default
|
|
1254
|
+
|
|
1255
|
+
Card C-input-memory (2026-11-05), the owner on the counter's Paid by and Bill: *"we should be remember the input value.
|
|
1256
|
+
we also need company level preference to select default, if default not config then we need remember"*. Additions only.
|
|
1257
|
+
|
|
1258
|
+
- **`InputLayoutParams.remember`** (`InputRemember`: `key`, `viewer`, `preferred`; Form only, any input type). When
|
|
1259
|
+
the values the form opens with leave the input without one (they do not name it, or hold it empty - a new record), it
|
|
1260
|
+
opens on `preferred` (the company's default, from a setting the page reads), else on the person's last saved choice,
|
|
1261
|
+
else on its own `defaultValue`; `preferred` and the kept choice each only while the input still offers them (a
|
|
1262
|
+
choice group's or a dropdown's own `options`: not disabled, not inactive, still listed; a list keeps what is
|
|
1263
|
+
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)).
|
|
1264
|
+
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?".
|
|
1265
|
+
- **Kept when a form saves** (`SubmitForm`, so `ApiForm` and a CRUD's forms too), in local storage under
|
|
1266
|
+
`nx-input:{key}:{viewer}` as `{ value }`; an empty value forgets. A form reset after the save (`resetOnSuccess`)
|
|
1267
|
+
opens on the new choice. No `viewer`: nothing read or kept (`preferred` still applies). A hidden input keeps nothing.
|
|
1268
|
+
- **No hydration difference:** the storage is read once the form is in the browser (a layout effect, before the first
|
|
1269
|
+
paint), so a form drawn by a server opens the same there and while it hydrates; `preferred` needs no storage and
|
|
1270
|
+
applies from the first draw. A later load (a CRUD's record) opens remembered inputs again from what it was given.
|
|
1271
|
+
- `FormStore` gains `attachMemory(storage)` and `rememberSaved(values)` (both `@internal`); the rules are pure in
|
|
1272
|
+
`Helpers/InputMemory.ts` (`scripts/test-input-memory.mts`, 46 checks).
|
|
1273
|
+
- **Fixed (card C-input-memory-fix):** an input with `showWhen` whose other input gets its value in the same load (its
|
|
1274
|
+
`defaultValue`, the record's) opened empty - without its own `defaultValue` and without its remembered value -
|
|
1275
|
+
because the form judged it hidden on the store's values from before the load. A load, a reset and a draft's restore
|
|
1276
|
+
now judge `showWhen` on the values they open with (the store's `get` answers from them while they are worked out); a
|
|
1277
|
+
hidden input still keeps only what the data gave it.
|
|
1278
|
+
- Proved by nexus.template's `/demo/form` "Remembered choices" (`input-memory.cjs`, both motion modes: empty with
|
|
1279
|
+
nothing kept, a save keeps and the reset opens on it with nothing unsaved, a new form, the company's default winning,
|
|
1280
|
+
a disabled and a gone value dropped with no broken tag, a server-drawn page with no hydration error, Forget, light
|
|
1281
|
+
and dark at 1280x720 and 390x844), with `forms.cjs`.
|
|
1282
|
+
|
|
1283
|
+
### A label that follows another input
|
|
1284
|
+
|
|
1285
|
+
Card C-label-follows (2026-11-05), the owner on a company's registration number: *"label should dynamic if possible like
|
|
1286
|
+
if user choosed pan then pan number if vat then vat number if it possible our component allow minimal code logic"*.
|
|
1287
|
+
Additions only.
|
|
1288
|
+
|
|
1289
|
+
- **`InputLayoutParams.labelWhen`** (`InputLabelWhen`: `name`, `labels`, `otherwise`, `placeholders`; Form only):
|
|
1290
|
+
the input's label is `labels[value]` for the other input's value as it is now (compared as text, so `1` matches
|
|
1291
|
+
`"1"`), else `otherwise`, else its own `label`; its placeholder is `placeholders[value]` where listed, else its own.
|
|
1292
|
+
Its messages name it by the label shown ("VAT number is required.") unless it has a `fieldName`.
|
|
1293
|
+
- Built on the `showWhen` machinery: `withInputConditions` gives such an input a live `label` (and `placeholder`)
|
|
1294
|
+
getter and joins the other input's name into `dependsOn`, so the field draws again when it changes - no remount,
|
|
1295
|
+
what was typed and the focus stay. `textFor` and `labelFor` are the pure rules (`test-input-conditions.mts`, 52 to 59).
|
|
1296
|
+
- Proved by nexus.template's `/demo/form` "A label that follows another input" (`label-when.cjs`, both motion modes).
|
|
1297
|
+
|
|
1298
|
+
### A preview the server works out as the person types
|
|
1299
|
+
|
|
1300
|
+
Card C-server-preview (2026-11-05): nexus.web's counter form and the Print layouts editor each wrote by hand "ask the
|
|
1301
|
+
server a moment after the last change, drop the older question, keep the last answer while a new one runs". Additions
|
|
1302
|
+
only; nexus.web is not changed here.
|
|
1303
|
+
|
|
1304
|
+
- **`useServerPreview(ask, { delay, failure })`** (`nexus-shared/client`): `request(question)` asks `ask(question,
|
|
1305
|
+
signal)` `delay` ms (default 300) after the last request; an older question is aborted (its signal) and a late
|
|
1306
|
+
answer of it is never shown; `result` and the `question` it answers stay while a newer one waits or runs and after
|
|
1307
|
+
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`
|
|
1308
|
+
is the words when it has none); `cancel()` drops what waits or runs, `clear()` the answer too; leaving the page
|
|
1309
|
+
stops what runs; `request`, `cancel` and `clear` are the same functions on every draw.
|
|
1310
|
+
- **`ServerPreviewStore`**: the same outside React (`getState`, `subscribe`), and the types `ServerPreview`,
|
|
1311
|
+
`ServerPreviewState`, `ServerPreviewAsk`, `ServerPreviewOptions`.
|
|
1312
|
+
- Proved by `scripts/test-server-preview.mts` (17: a burst asks once, waiting then loading, an older answer dropped, the last answer kept
|
|
1313
|
+
through a newer question and a failure, cancel, clear, the latest ask) and nexus.template's `/demo/document-frame`
|
|
1314
|
+
layout editor, which now reads its preview through the hook (`document-frame.cjs`, both motion modes, unchanged).
|
|
1315
|
+
|
|
1316
|
+
### A printable document's logo
|
|
1317
|
+
|
|
1318
|
+
Card C-print-logo (2026-11-05), for the receipt: the company's bill logo at its head. Additions only.
|
|
1319
|
+
|
|
1320
|
+
- **`PrintDocument` `logo`** (`PrintLogo` `{ src, alt }`, or any node) and **`logoOnThermal`** (default true). On A5 the
|
|
1321
|
+
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
|
|
1322
|
+
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
|
|
1323
|
+
roll, and on an A5 document shown narrower than 100 mm (a phone; `.nx-print` is now the `nx-print` container), it
|
|
1324
|
+
sits small and centred above them (at most 36 x 10 mm on the roll); `logoOnThermal={false}` leaves it off a roll.
|
|
1325
|
+
Printed in black and white (`filter: grayscale(1)`). An `{ src }` image is decoded at once (not lazy); it must
|
|
1326
|
+
have loaded before `window.print()`.
|
|
1327
|
+
- `Nexus.Print.css`: `nx-print__head--logo`, `nx-print__logo`, `nx-print__heading`. Without a logo the head's markup
|
|
1328
|
+
is as it was.
|
|
1329
|
+
- Proved by nexus.template's `/demo/print-document` (a colour logo, switches for it and for the roll) and
|
|
1330
|
+
`print-document.cjs`: beside the title on A5 within 24 x 16 mm, the title centred, the head no taller than its lines;
|
|
1331
|
+
centred above on the roll within 36 x 10 mm; off with `logoOnThermal`; grey in print; the longest receipt, voided,
|
|
1332
|
+
printed alone to PDF: one A5 page (420 pt) and one roll page (227 pt).
|
|
1333
|
+
|
|
1334
|
+
### A page's own switch in the table's bar keeps its label on one line
|
|
1335
|
+
|
|
1336
|
+
Card C-bar-noshrink (2026-11-06), from nexus.web's Branch users: a `Switch` beside Add ("All branches") broke its label
|
|
1337
|
+
between its words at every width, and a non-breaking space made it run into the lists instead. The cause was the Add
|
|
1338
|
+
itself: it is a `block` button, `width: 100%` of the Add section, so on a shared line it claimed the section and the
|
|
1339
|
+
page's own items shrank (a switch's field may shrink to nothing; a button stopped at its label).
|
|
1340
|
+
|
|
1341
|
+
- `Nexus.Table.css`: every item of `.nx-table__bar-add` keeps its width (`flex: none`, `min-width: fit-content`), and
|
|
1342
|
+
the Add beside other parts is as wide as its label (`width: auto`), so a bar that does not fit takes its next step
|
|
1343
|
+
(search, Reload into ⋮, the Add section's own line). On that line the items still share it (`flex: 1 1 0`) and one
|
|
1344
|
+
that would not fit its share wraps whole onto a line of its own. No markup or prop changed.
|
|
1345
|
+
- Proved by nexus.template's `/demo/data-table` section "The page's own controls in the bar" (a Switch and a Button in
|
|
1346
|
+
`actions`, lists in `toolbar`) and `/demo/crud`'s amenities ("Reset demo") in `data-table-bar.cjs`: at table widths
|
|
1347
|
+
1000, 899, 680, 660, 380, 370, 330, 320 and 300, with the switch and the button, each alone, and the CRUD's button,
|
|
1348
|
+
in seven themes light and dark, compact and cozy: no label broken or cut, no two controls overlapping, nothing
|
|
1349
|
+
outside the bar, one line from 670px (compact), the search alone under 670, Reload in ⋮ under 375, the Add section
|
|
1350
|
+
last under 325.
|
|
1351
|
+
|
|
1352
|
+
### A list that can be widened to other endpoints
|
|
1353
|
+
|
|
1354
|
+
Card C-crud-scope (2026-11-06), from nexus.web's Branch users: the branch signed in at (`BranchUser`) and, for a person
|
|
1355
|
+
who may, every branch (`AllBranchUser`), picked by a switch in the toolbar and remembered per person - written by hand
|
|
1356
|
+
with a remounted `Crud`, a page-level hook for the storage and two copies of its columns and inputs. Additions only.
|
|
1357
|
+
|
|
1358
|
+
- **`Crud` `scopes`** (`CrudScopes`: `options`, `label`, `description`, `remember: { key, viewer }`,
|
|
1359
|
+
`onChange`): other lists in place of the CRUD's own, each a **`CrudScope`** (`id`, `label`, `description`, `api`,
|
|
1360
|
+
`when`, and where they differ `columns`, `inputs`, `form`, `table`, `aria-label`; `form` and `table` over the
|
|
1361
|
+
CRUD's, field by field). One scope is a `Switch` after Add (`description` its tooltip); two or more are tabs, the
|
|
1362
|
+
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
|
|
1363
|
+
change of their rights; a kept scope not offered opens the CRUD's own list and stays kept.
|
|
1364
|
+
- **Switching keeps the CRUD**: the new list is read on its endpoints (`loadKey` names the scope), the switch keeps
|
|
1365
|
+
the focus, popups stay open. Each scope keeps its own cache of lists while the page is open, so switching back costs
|
|
1366
|
+
no request; a change made on one scope drops the other scopes' lists (the server alone knows which hold the record).
|
|
1367
|
+
An answer asked on one scope and landing after a switch (a row action, a mark, a mass action, a form's save, a
|
|
1368
|
+
details refresh) writes nothing into the list on screen: every kept list is dropped and the one on screen read again,
|
|
1369
|
+
quietly, and `onChange` still hears of it.
|
|
1370
|
+
- **Each scope keeps its own table settings**: its `table.stateKey`, else the CRUD's and `:{id}`. **`DataTable`
|
|
1371
|
+
reads a changed `stateKey`** as the first draw does (page size, sort, hidden columns with each column's own
|
|
1372
|
+
default, order, widths, pins, open sections) and never writes the settings on screen under the new key;
|
|
1373
|
+
`TableStore.restore(saved, { notify: true })` restores over a table already shown.
|
|
1374
|
+
- **`remember`** keeps the choice in this browser (`nx-crud-scope:{key}:{viewer}`, the CRUD's own list as nothing,
|
|
1375
|
+
storage in try/catch), read after the first draw and before any list is asked for: a reload asks for one list.
|
|
1376
|
+
- **`CrudHandle.scope`** and **`setScope(id | null)`**.
|
|
1377
|
+
- **A `CheckBox`'s or `Switch`'s `title`** is now its whole row's tooltip (its label too), still on the input.
|
|
1378
|
+
- New exports: `CrudScope`, `CrudScopes`, `offeredCrudScopes`, `shownCrudScope`, `crudScopeProps`,
|
|
1379
|
+
`crudScopeStateKey`, `crudScopeStorageKey`, `readCrudScope`, `writeCrudScope` (`nexus-shared`);
|
|
1380
|
+
`useCrudScopes`, `CrudScopeControl`, `CrudScopeState` (`nexus-shared/client`).
|
|
1381
|
+
- Proved by `scripts/test-crud-scopes.mts` (32) and nexus.template's `/demo/crud` section "One desk, or every desk"
|
|
1382
|
+
(posts through `DeskStaffPost` and, for the Admin role, every desk's through `StaffPost`; the demo backend's
|
|
1383
|
+
narrower controllers are views of the same records) in `crud-scopes.cjs`.
|
|
1384
|
+
- **Review fixes** (card C-scope-fix, review round 4):
|
|
1385
|
+
- **Opening on a remembered scope no longer wipes the CRUD's own list's table settings.** `DataTable` saves what its
|
|
1386
|
+
store holds, never the draw's settings: the first draw, which still held the opening defaults, wrote them under the
|
|
1387
|
+
own list's key when the kept scope was read, and its sort and hidden columns were gone on the way back.
|
|
1388
|
+
- **A form stays in the scope it was opened on**: an Edit pressed in a details popup after a switch is the details'
|
|
1389
|
+
scope's form - its `inputs`, `form` and endpoints - and its save is that scope's answer (it went to the scope shown,
|
|
1390
|
+
with the other scope's fields).
|
|
1391
|
+
- **Each scope keeps its own Add**, and so its draft (the popup's name is `{name}@{scope}`): a minimized Add of one
|
|
1392
|
+
list never comes back on another. An Edit keeps one popup per record wherever it is opened.
|
|
1393
|
+
- **An answer that lands after a switch** still marks the lookups of the scope it was asked on and of the one shown as
|
|
1394
|
+
old (`expireLookups`: a picker's or a join's next read asks again) and drops every tree's kept pages, as an answer on
|
|
1395
|
+
screen writes them.
|
|
1396
|
+
- Proved by `crud-scopes.cjs` section K, each line failing on the code before: the own list's sort and hidden
|
|
1397
|
+
Department kept through a client navigation that opens on the kept scope; a Spa post's Edit from details restored
|
|
1398
|
+
after a switch is every desk's form and saves to `StaffPost/Update`; the own Add's draft stays the own list's; the
|
|
1399
|
+
demo's "Find a post" box (every desk's `StaffPost/ActiveListing`, its pages dropped on every change of the CRUD)
|
|
1400
|
+
asks its list again after an answer for every desk lands on the front desk's list.
|
|
1401
|
+
|
|
1402
|
+
### Pinned columns give way on a narrow table
|
|
1403
|
+
|
|
1404
|
+
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
|
|
1405
|
+
284 of the table's 354 px, leaving 70 for every other column. Additions only, and no page passes anything.
|
|
1406
|
+
|
|
1407
|
+
- **The columns that stay - the numbers and checkbox column and the actions, which always stay, and the pinned ones -
|
|
1408
|
+
take at most 60% of the table's width on screen** (`PINNED_SHARE`). Pins are kept from the outer edge in; a pinned
|
|
1409
|
+
column that would pass that share, and every one inside it on the same side, scrolls with the others until the table
|
|
1410
|
+
is wide enough again. Measured after every draw and whenever the table's width changes; staying or scrolling changes
|
|
1411
|
+
no width, so it settles at once.
|
|
1412
|
+
- A column that gives way keeps its side: it is drawn there, moves among its side's columns, and its menu still offers
|
|
1413
|
+
Unpin. **`TableViewColumn.side`** (new) is the side chosen; **`pin`** is the side it stays on now (null while it
|
|
1414
|
+
scrolls, so its cells carry no `data-pin`). Headings carry `data-pin-side`, which the drag's drop targets follow.
|
|
1415
|
+
- New exports (`nexus-shared`): `PINNED_SHARE`, `releasedPins(view, frame, left, right, share?)`.
|
|
1416
|
+
- Proved by `scripts/test-table.mts` (the rule, and `viewColumns` with released columns) and nexus.template's
|
|
1417
|
+
`data-table.cjs` at 390 px with Booking and Guest pinned: the columns that stay keep to 60% of the table, the outer
|
|
1418
|
+
ones stay with their cells and the others scroll, both keep their side, and at 1280 both stay.
|
|
1419
|
+
|
|
1420
|
+
### A row action that opens a form of its own for the record
|
|
1421
|
+
|
|
1422
|
+
Card C-row-form (2026-11-06), from nexus.web's Permissions row action (`usePermissionSetActions`, written by hand: a
|
|
1423
|
+
popup with an `ApiForm` reading the set, saving it elsewhere, then `apply` of the row's count). Additions only.
|
|
1424
|
+
|
|
1425
|
+
- **`Crud` `rowForms`**, each a **`CrudRowForm`**: `id`, `label`, `description`, `icon`, `inline`, `modes`
|
|
1426
|
+
(default the lists that offer Edit), `when(row, mode)`, `lock(row)` (listed switched off with its sentence), `target(row)`
|
|
1427
|
+
(the `{id}` the endpoints are called with), `inputs` (params, or a function of the record and of what `load` read;
|
|
1428
|
+
it may answer a promise), `load` (an endpoint or a function), `submit`, `transform(values, row, loaded)`, `writes`
|
|
1429
|
+
(the answer's fields by name, or a function), `title`, `size`, `submitLabel`, `onSaved(result, row, values)`.
|
|
1430
|
+
Offered after the built-in actions and before `rowActions`, to a person who may call `submit` and an endpoint
|
|
1431
|
+
`load`; one popup per record and form (`reopen: "restore"`), a discard question before changes are lost.
|
|
1432
|
+
- **A read that fails never leaves a savable form**: the API client answers `ok: false` rather than throwing, so a
|
|
1433
|
+
failed answer (and one with no record, and a function's `null`) is turned into `ApiForm`'s failed state - "Could not
|
|
1434
|
+
load the values" with Retry - and the footer's Save is gone until a read succeeds. (A permission set read that failed
|
|
1435
|
+
opened empty, and its Save wiped the set.)
|
|
1436
|
+
- **The write-back is only what the answer names**: `writes` fields merged over the record as the CRUD holds it when the
|
|
1437
|
+
answer lands (the table, else a kept list, else a pinned section), never the record as the action was clicked, so an
|
|
1438
|
+
Edit saved while the popup was minimized stays. Written through the cache sync (`applyRows` with the held record),
|
|
1439
|
+
so every list kept and the pinned sections follow with no list read; an answer after a scope switch writes nothing.
|
|
1440
|
+
- **`CrudHandle.openRowForm(id, row)`**. New exports: `CrudRowForm`, `crudRowFormOffered`, `crudRowFormWrites`
|
|
1441
|
+
(`nexus-shared`); `openCrudRowForm`, `CrudRowFormRequest` (`nexus-shared/client`).
|
|
1442
|
+
- Proved by `scripts/test-crud-row-forms.mts` (24) and nexus.template's `/demo/crud` posts, whose Duties (PostDuty's
|
|
1443
|
+
Detail and Update in the demo backend, a count joined into each post) are a row form, in `crud-row-form.cjs`.
|
|
1444
|
+
|
|
1445
|
+
### A held menu item says why when it is pressed
|
|
1446
|
+
|
|
1447
|
+
Card C-held-action-note (2026-11-06), from a review of nexus.accounts' dashboard: a row action switched off with its
|
|
1448
|
+
reason (`rowLock`, `rowActionLock`, a row form's `lock`) showed that reason only as its menu line, and a press did
|
|
1449
|
+
nothing, where the table's held Add says why under it. Behaviour of `Menu`, `ContextMenu` and `SplitButton`'s list; no
|
|
1450
|
+
export is new.
|
|
1451
|
+
|
|
1452
|
+
- **`MenuPanel`**: an item `disabled` with a `description` is held (`data-held`). A press - a click, Enter, Space -
|
|
1453
|
+
shows the description in the note a read-only field and a held Add show (`useReadOnlyNotice`, `.nx-field-notice`,
|
|
1454
|
+
`role="status"`), under the item; the menu stays open, nothing is chosen, and the note goes when the menu closes. The
|
|
1455
|
+
arrow keys and the pointer reach a held item (`ENABLED_ITEM` takes `[data-held]`), so the keyboard can press it;
|
|
1456
|
+
a disabled item without a description is skipped as before.
|
|
1457
|
+
- Proved by nexus.template's `held-row-actions.cjs` on `/demo/crud`: Wi-Fi's held Move to trash (`rowActionLock`) and
|
|
1458
|
+
a not-yet-joined person's held Duties (a row form's `lock`) say their menu line's words when clicked and on Enter
|
|
1459
|
+
after the arrow keys reach them, with the menu open and no request sent; Esc closes both; `/demo/menus`' Print
|
|
1460
|
+
(switched off with no reason) is still skipped; the note inside the window and its words at 4.5:1 or more in seven
|
|
1461
|
+
themes, light and dark, at 1280 and 390.
|
|
1462
|
+
|
|
1463
|
+
### A form with changes asks before a navigation inside the app
|
|
1464
|
+
|
|
1465
|
+
Card C-leave-guard (2026-11-06), from a review of nexus.accounts' dashboard: its profile form lost its edits through the
|
|
1466
|
+
side menu, since only the page's close (`beforeunload`) asked. Additions only.
|
|
1467
|
+
|
|
1468
|
+
- **The page's own forms**: a `SubmitForm` (and so an `ApiForm`) that asks before the page closes (`confirmLeave`, on
|
|
1469
|
+
without a draft) also joins the tab's page guard (`PageLeaveGuard` in `FormLeaveGuard.ts`, beside the workspace's
|
|
1470
|
+
guard), unless it is drawn in a `PopupHost` popup, which stays open through a navigation (`PopupHost` gives its
|
|
1471
|
+
popups no page guard).
|
|
1472
|
+
- **Links**: while one of them has changes, a press on a same-origin link - next/link or a plain `<a>`, the side menu,
|
|
1473
|
+
the brand, the trail, a menu's link row - is held in the capture phase on `window` (before any other handler) and
|
|
1474
|
+
asks "Discard your changes?" (`confirmDiscard`'s words, as a workspace tab and a form popup ask); Keep editing stays,
|
|
1475
|
+
Discard follows the link as it was pressed (the app's router for its links). A link to another tab, a download,
|
|
1476
|
+
another site (the browser's close question asks there) or this page is left alone. Set up once, by the first such
|
|
1477
|
+
form.
|
|
1478
|
+
- **`confirmPageLeave()`** (`nexus-shared/client`): the same question for a navigation the app makes itself
|
|
1479
|
+
(`if (await confirmPageLeave()) router.push(href)`). A confirmed navigation is let go for a moment
|
|
1480
|
+
(`PageLeaveGuard.allow`), so the form's `beforeunload` does not ask a second time after a plain link's Discard.
|
|
1481
|
+
- Proved by nexus.template's `leave-guard.cjs` on `/admin/settings`: the side menu, the brand, the trail, Back to the
|
|
1482
|
+
site, the account menu's link and the module menu each ask and Keep editing keeps what was typed; Discard goes (by the
|
|
1483
|
+
router, and by the address for a plain link with no browser question); a form without changes, and one just saved,
|
|
1484
|
+
goes with no question; a minimized Add with changes (a `PopupHost` popup) holds nothing and is still on the dock; a
|
|
1485
|
+
reload with changes still gets the browser's question.
|
|
1486
|
+
- **Review fixes** (card C-leave-fix, review round 4):
|
|
1487
|
+
- **A popup's form keeps the browser's close question**: a confirmed Discard of the page's form let the next moment's
|
|
1488
|
+
full-page load go with no question for every form, so a minimized Add with changes was lost without a word. Only the
|
|
1489
|
+
page's own forms are let go now.
|
|
1490
|
+
- **The page already in view asks nothing**: a module or account menu row for it lost nothing and still asked.
|
|
1491
|
+
**`keepsPage(href, target?, download?)`** (new, `nexus-shared/client`) is the one test, for links and for the
|
|
1492
|
+
rows nexus-admin moves by `router.push`.
|
|
1493
|
+
- **A link inside text being edited** (the rich-text editor's) is not a navigation: a click there places the caret,
|
|
1494
|
+
and nothing is asked.
|
|
1495
|
+
- **`confirmPageLeave({ unload: true })`**, for a move that loads a whole page (a sign-out, `location.assign`): asks
|
|
1496
|
+
about every form with changes, the popups' too, since the load takes them all; once confirmed, neither it nor the
|
|
1497
|
+
browser's close question asks again until the page goes (`pagehide`), however long the call before the move takes.
|
|
1498
|
+
**`endPageLeave()`** (new) gives the questions back when the move did not happen.
|
|
1499
|
+
- Proved by `leave-guard.cjs` section F: the module row for the page in view and a link in the House rules editor ask
|
|
1500
|
+
nothing and keep the typing; a page Discard followed by a full load still gets the browser's question for a
|
|
1501
|
+
minimized Add with changes; Sign out (slowed by 2.5 s) asks about a minimized Add first, Keep editing sends nothing,
|
|
1502
|
+
Discard signs out with no second question.
|
|
1503
|
+
|
|
1504
|
+
### Every picture through the editor, and within Drive's limits
|
|
1505
|
+
|
|
1506
|
+
Card IMG-comp (2026-11-06), the owner: *"any image editor should be open the editor then it upload ... first compress
|
|
1507
|
+
then save ... ~300KB max but put it into config ... max resolutions, max quality (%) in config for drive."* A picture
|
|
1508
|
+
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
|
|
1509
|
+
remembered quality could be 100, and nothing kept a byte budget. Drive's side is card IMG-api
|
|
1510
|
+
(`drive-picture-limits-contract.md`): its host settings `Nexus:Drive:Pictures` (MaxSide 1920, Quality 80, MaxKilobytes
|
|
1511
|
+
300), answered by `File/List` and enforced on every upload.
|
|
1512
|
+
|
|
1513
|
+
- **`FileCategoryInfo`** gains `pictureMaxSide` (px), `pictureQuality` (%) and `pictureMaxBytes`, optional, read by
|
|
1514
|
+
`NexusDriveAdapter` from `File/List` with the defaults when an older Drive answers none (so every picture is still
|
|
1515
|
+
made small). **`PICTURE_LIMITS`** (`{ maxSide: 1920, quality: 80, maxBytes: 307200 }`) and **`pictureLimits(category)`**
|
|
1516
|
+
(the category's, else the defaults; the bytes never above its `maxBytes`) are new. `MAX_PICTURE_SIDE` is now the
|
|
1517
|
+
default 1920 (it was 2048) and nothing in the package reads it.
|
|
1518
|
+
- **Every picture through the editor** (`useOwnerFiles`): the gallery's Upload - every picture column, the top bar's own
|
|
1519
|
+
picture, a record's workspace, the accounts dashboard - opens the editor for each picture in turn ("Picture 2 of 5 ·
|
|
1520
|
+
{title}"); Cancel skips that one and, with more to come, asks "Go on with the next file?" (Next file, Stop here). It
|
|
1521
|
+
keeps the chosen file's name with the kind written. Documents (a PDF) are sent as they are. `renderImage` is no longer
|
|
1522
|
+
called by the package.
|
|
1523
|
+
- **Within the limits**: the editor gets the long edge at most `pictureMaxSide` (less when the params say), `maxQuality`
|
|
1524
|
+
`pictureQuality` / 100 and `maxBytes` `pictureMaxBytes` (`ImageEditorOptions` and `renderImage`'s options take both;
|
|
1525
|
+
`ImageEditorResult.fitted` says what the budget took, see nexus-image-editor's CHANGELOG). The toast after a save
|
|
1526
|
+
says when a picture was made smaller.
|
|
1527
|
+
- **`fileRefusal`** checks a picture against `pictureLimits(category).maxBytes` and a document against `maxBytes`; the
|
|
1528
|
+
gallery's line says "up to 300 KB each", or "pictures up to 300 KB, documents up to 10 MB" where both are taken.
|
|
1529
|
+
- Proved by `scripts/test-files.mts` (84) and nexus.template, whose pretend Drive answers and enforces the three
|
|
1530
|
+
(`Demo/PictureLimits` raises them for the checks): `picture-limits.cjs` (a 4.2 MB photo stored at about 175 KB with the
|
|
1531
|
+
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
|
|
1532
|
+
as a WebP that keeps its see-through parts, an opaque PNG as a JPEG, three pictures each through the editor with a
|
|
1533
|
+
Cancel and Next file, Stop here, DRV_X059 for a client that skips the editor, raised settings letting a 1.8 MB
|
|
1534
|
+
picture through) and `pictures.cjs` (the gallery's three uploads each through the editor, the page's square shape).
|
|
1535
|
+
- **Review fixes** (card IMG-comp-fix, review round 4):
|
|
1536
|
+
- **Only into a kind the category takes**: the editor gets the category's picture kinds (`ImageEditorOptions.types`,
|
|
1537
|
+
from `pictureOptions`). Its Save panel offers only those, a picture opens on one of them, and a PNG over the size is
|
|
1538
|
+
made a JPEG or a WebP only where that kind is taken - where neither is, it stays a PNG and is made smaller. A
|
|
1539
|
+
see-through PNG for a category of JPEG and PNG was written as a WebP and then refused.
|
|
1540
|
+
- **A picture the browser could not type** (no type, or `application/octet-stream`: a phone's share, a name with no
|
|
1541
|
+
extension) is typed from its first bytes and goes through the editor, where it was sent as it was:
|
|
1542
|
+
**`sniffPictureType(blob)`** (JPEG, PNG or WebP, else null) and **`typedFile(file)`** (new, `nexus-shared`). Any other
|
|
1543
|
+
untyped file is still the backend's to judge.
|
|
1544
|
+
- **An editor that cannot be downloaded says so where it was asked for**: the picture input's line under its buttons
|
|
1545
|
+
and the gallery's alert say the loader's words ("The image editor is not installed on this server yet." for Drive's
|
|
1546
|
+
DRV_X053) until a try opens it, besides the toast; the button stays ready, and pressing it again tries again.
|
|
1547
|
+
`useOwnerFiles` answers `editorProblem`.
|
|
1548
|
+
- Proved by `test-files.mts` (88) and `picture-limits.cjs` section G: the ID photo's category (JPEG and PNG) offers
|
|
1549
|
+
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
|
|
1550
|
+
over with no type opens the editor and is stored as "IMG_2041.jpg" under 300 KB; with Drive's editor missing the
|
|
1551
|
+
input and the gallery say why, and once it is there the same button opens it and the line goes.
|
|
1552
|
+
|
|
1553
|
+
### A record's page of cards, every change in a popup
|
|
1554
|
+
|
|
1555
|
+
Card C-record-cards (2026-11-06), from the owner's request for nexus.accounts' profile: *"make card types block
|
|
1556
|
+
buetifull design ... when click editor then show popup and apply editor or add or something do not show direct save or
|
|
1557
|
+
update"*, and *"Emails / Phones / Addresses - those all put inside profile tab as card. put right side icons to verify if
|
|
1558
|
+
not. switch primary like this. single page, CURD from popup show add button, edit, remove like this. primary do not
|
|
1559
|
+
allow"*. Additions only.
|
|
1560
|
+
|
|
1561
|
+
- **`RecordCards`** (`nexus-shared/client`): one record that is not a list - a person's profile, my company, a
|
|
1562
|
+
subscriber's details - as a hero and cards, declared by params (`RecordCardsProps`): `sources` (each read once by GET
|
|
1563
|
+
through the app's `api`, or a function; `{ read, held }` draws what is known before, such as the session's name),
|
|
1564
|
+
`hero` (`picture` by Drive params - a click opens the pictures window - `title`, `lines`, `marks`, `actions`), `cards`,
|
|
1565
|
+
`readOnly`, `signedOut` (the page's words, `onSignIn`, `onEnded`), `isSessionEnded`, `name`, `headingLevel`. **The page
|
|
1566
|
+
itself never saves.**
|
|
1567
|
+
- Three cards, each with an `id` (its element id, so `#emails` leads to it), a `source`, `title`, `icon`, `description`,
|
|
1568
|
+
heading `actions`, `hidden`, `wide`, `readOnly`:
|
|
1569
|
+
- **fields** (`RecordFieldsCardParams`): label over value, each value drawn by the `InputParams` its Edit uses - a
|
|
1570
|
+
choice by its label, a date in the person's format, a number with its format, Yes or No, a multiple choice or
|
|
1571
|
+
`look: "chips"` as chips, long text on its own lines, Markdown and rich text as such, a password never, "Not said"
|
|
1572
|
+
(`empty`) when empty; `show` draws one by the page. **`edit`**: a popup of the card's inputs on the record, its values
|
|
1573
|
+
merged over the record as held now when sent, so an endpoint that takes the whole record works.
|
|
1574
|
+
- **items** (`RecordItemsCardParams`): rows between hairlines (a picture or icon, the title, marks, lines) with **icon
|
|
1575
|
+
actions on the right** (`RecordItemActionParams`), each named as a tooltip and to a screen reader with its item
|
|
1576
|
+
("Verify: ram@example.com"), hidden, or held with its reason - aria-disabled, the reason in its tooltip and said in
|
|
1577
|
+
the note a held Add shows when pressed; **`add`** in the heading, held at the `limit` ("5 of 5" beside the title,
|
|
1578
|
+
`limitReason`).
|
|
1579
|
+
- **content** (`RecordContentCardParams`): `render(context)`, the page's own; `context.open(flow)` opens a popup.
|
|
1580
|
+
- **Every change is a popup** (`RecordFlowParams`): one step, or `steps` with `start` and `next` (a code sent, then
|
|
1581
|
+
typed), each with its `lead`, `inputs`, `submit` (a URL, a request, an endpoint or a function), `transform`,
|
|
1582
|
+
`submitLabel`/`submitTone`, `writes`, `also` (a button that calls and stays, held with a countdown while the server says
|
|
1583
|
+
to wait: "Send again in 20s"), `done` (run another action: the code step after an Add, Make primary after a Verify),
|
|
1584
|
+
`onError`, `render`, `confirmCancel`; one popup per card, action and item, brought back as it was; closing with changes
|
|
1585
|
+
asks first. A form opens only on the plain values its inputs name, so a code box never opens on an item's `code`
|
|
1586
|
+
record. **`confirm`** (`RecordConfirmParams`) asks, then calls.
|
|
1587
|
+
- **The card shows the answer at once**: `writes` (`RecordWrites`) - the answer merged over the data as held now (the
|
|
1588
|
+
default), the values, one item replaced or added, the item removed, a read again, named fields, or a function - into
|
|
1589
|
+
the source, so every card on it (the hero too) follows and nothing is read again.
|
|
1590
|
+
- **States**: loading draws every card as it will be, held (labels and bones as tall as their values, a held mark row,
|
|
1591
|
+
Edit held with "Still being read."); a source that fails says why in each of its cards with Try again (the first says
|
|
1592
|
+
it aloud); read-only draws no button; an answer that ended the session (default a 401 naming no input - a wrong
|
|
1593
|
+
password names its box) closes the page's popups and leaves the page's words with Sign in again, which takes the
|
|
1594
|
+
focus. The focus comes home when a popup's or a question's opener went with the change (the row's first button, else
|
|
1595
|
+
the card's). A link to a card (`#emails`) is followed again once every source has answered, since the cards above it
|
|
1596
|
+
grow as their records come.
|
|
1597
|
+
- **Layout**: the hero across, then as many cards to a row as have 26rem (one on a phone), each as tall as what it
|
|
1598
|
+
holds; fields as many to a row as have 11rem. It sits in the admin page's scroller, so the window never scrolls.
|
|
1599
|
+
`Nexus.Record.css`; the table's tag gets `nx-table__tag--plain` (no dot) for a chip.
|
|
1600
|
+
- New exports: `RecordCards` (`nexus-shared/client`); in `nexus-shared` the interfaces `RecordCardsProps`, `RecordRead`,
|
|
1601
|
+
`RecordSourceParams`, `RecordWrites`, `RecordMark`, `RecordSubmit`, `RecordContext`, `RecordStepContext`,
|
|
1602
|
+
`RecordStepAnswer`, `RecordCallParams`, `RecordStepParams`, `RecordFlowParams`, `RecordConfirmParams`,
|
|
1603
|
+
`RecordCardActionParams`, `RecordItemActionParams`, `RecordCardBase`, `RecordFieldParams`, `RecordFieldsCardParams`,
|
|
1604
|
+
`RecordItemView`, `RecordItemsCardParams`, `RecordContentCardParams`, `RecordCardParams`, `RecordHeroParams`,
|
|
1605
|
+
`RecordSignedOutParams`; the typed helpers `fieldsCard`, `itemsCard`, `contentCard`; and the pure half
|
|
1606
|
+
(Helpers/RecordCards.ts) `recordCardKind`, `recordField`, `recordFieldInputs`, `recordOpenedValues`, `recordFieldWide`,
|
|
1607
|
+
`recordFieldHeld`, `recordValueView`, `RecordValueView`, `recordItems`, `recordItemKey`, `recordWithItems`,
|
|
1608
|
+
`recordLimit`, `recordCount`, `recordLimitReason`, `recordWrite`, `RecordWriteRequest`, `RecordWriteOutcome`,
|
|
1609
|
+
`recordActionState`, `recordActionEndpoint`, `RecordActionState`, `recordSteps`, `recordStartStep`, `recordStatusLine`,
|
|
1610
|
+
`recordSessionEnded`, `secondsUntil`, `RecordSources`, `RecordSourceState`, `RecordReader`.
|
|
1611
|
+
- Proved by `scripts/test-record-cards.mts` (130) and nexus.template's `/admin/profile` (a pretend person's profile on
|
|
1612
|
+
`src/app/api/demo-profile`, with a menu of demo states) in `record-cards.cjs`.
|
|
1613
|
+
|
|
1614
|
+
### The pictures window in two panes, as 2026-06-20's
|
|
1615
|
+
|
|
1616
|
+
Card C-gallery (2026-11-06), from the owner's look at the pictures window on nexus.accounts' profile: *"the over all
|
|
1617
|
+
design i do not like. when hover then show inside the image the absolute position. look like the second image. this
|
|
1618
|
+
image capture from: 06-20 version. when click profile icon then show popup like this."* No new exports, nothing removed;
|
|
1619
|
+
the look and where the buttons are changed, every behaviour stayed.
|
|
1620
|
+
|
|
1621
|
+
- **`PictureGallery` / `openPictureGallery`**, for pictures: two panes with a hairline between them. On the left the
|
|
1622
|
+
main picture large (about 15rem on its long side, a rounded square for every shape; the picture icon when there is no
|
|
1623
|
+
main one), **Upload** (a solid primary button; Replace in a category that keeps one file) and **Remove** (no main
|
|
1624
|
+
picture, the pictures kept; Delete in a one-file category) under it, then the count and kinds ("2 of 20 · PNG, JPEG or
|
|
1625
|
+
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
|
|
1626
|
+
rounded squares (about 11rem, at least two a row), a round category's too - only an avatar stays round. The main one
|
|
1627
|
+
has the accent ring and a **Main** mark in its top left corner.
|
|
1628
|
+
- **Each picture's buttons are inside it**, centred on a popover's panel: View, Make main and Delete (neither on the
|
|
1629
|
+
main one: it is never deleted from its own picture, as in 2026-06-20's gallery - Remove it first, or make another the
|
|
1630
|
+
main one) and Edit. They show on hover and on a keyboard's focus in the picture (`:has(:focus-visible)`, so a click
|
|
1631
|
+
does not leave them shown), and always where nothing can hover (`@media (hover: none)`: a touch screen); hidden, they
|
|
1632
|
+
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
|
|
1633
|
+
picture"). They fade in briefly, and not at all with animations off.
|
|
1634
|
+
- **Under 560px of its own width** (a phone, a narrow tab) the main picture is 7rem beside its buttons, over the grid.
|
|
1635
|
+
- **The window** (`openPictureGallery`) is `xl` and edge to edge for pictures, with a height of its own
|
|
1636
|
+
(`min(36rem, 100dvh - 10rem)`, the whole window when maximized): only the grid scrolls. Documents stay a list in an
|
|
1637
|
+
`lg` window; one opened without `view` changes to it once the category says it takes PDF.
|
|
1638
|
+
- **States**: loading draws the panes at their size, the main picture's place held and three held pictures where the
|
|
1639
|
+
grid will be ("Loading the pictures…"); a gallery with none says "No pictures yet." with Upload on the left; read-only
|
|
1640
|
+
owners (`readOnly`, `lockReason`, a refused write ticket) get no Upload, Remove, Make main, Edit or Delete. Upload
|
|
1641
|
+
and Edit when the category is full are `aria-disabled` with the reason as their tooltip, and say it when pressed.
|
|
1642
|
+
- **`PictureInput`**: a click on its picture, or Enter, opens the same window, as All pictures does (read only where the
|
|
1643
|
+
input is); its buttons stay. The picture is named by what it opens ("Picture: open the pictures"), never by the
|
|
1644
|
+
file's name. Its buttons are a `role="group"`, and `useFocusThroughChange` keeps the focus in a tile, row or group, so
|
|
1645
|
+
after a Delete the focus lands on Upload, not on the picture.
|
|
1646
|
+
- The data hook is `data-state="loading|ready|failed"` on `.nx-files`; the tile's `.nx-files__tile[data-primary]`,
|
|
1647
|
+
`.nx-files__thumb` and the buttons' names are as before. `.nx-files__tile-bar` and `.nx-files__thumb--round` are gone.
|
|
1648
|
+
- Proved by nexus.template's `pictures.cjs` (its section K: the panes, the mark, hover, keyboard and touch, Remove,
|
|
1649
|
+
loading, empty, read only, a 390 px phone, the picture input's picture) and `picture-limits.cjs`, both motion modes.
|
|
1650
|
+
|
|
1651
|
+
### A popup has no minimize button unless it asks
|
|
1652
|
+
|
|
1653
|
+
Card C-popup-minimize (2026-11-06), from the owner: *"every place do not put minimize popup buttons - for example the
|
|
1654
|
+
profile picture viewer, or something just full admin page + CURD this place put minimized option for only the edit and
|
|
1655
|
+
add"*. No new exports, nothing removed; **one default changed**, which an app sees without changing a line.
|
|
1656
|
+
|
|
1657
|
+
- **`Popup` / `openPopup`'s `minimizable` defaults to false** (it was `closable`, so every closable popup had the
|
|
1658
|
+
button). Maximize and close are as they were. A page may still ask: `minimizable` on a `Popup` or in `openPopup`'s
|
|
1659
|
+
options, or a rule by key (`definePopupRules({ "booking-edit-*": { minimizable: true } })`).
|
|
1660
|
+
- **Only a CRUD page's forms ask**: a `Crud`'s Add and Edit (`openCrudForm`: from the list's Add, a row's Edit, a
|
|
1661
|
+
tree's Add inside a row, and the details popup's Edit) and its row forms (`openCrudRowForm`, such as a record's
|
|
1662
|
+
Permissions). They ask with `popupRuleFor(key)?.minimizable ?? true`, so a page's rule for their key can still take it
|
|
1663
|
+
away. The dock, `reopen`, and the leave guard work for them as before: a minimized form with changes is still asked
|
|
1664
|
+
about by a full-page move (`confirmPageLeave({ unload: true })`) and by the browser's `beforeunload`.
|
|
1665
|
+
- **Everything else has none**: the details popup and a record's workspace (its Edit opens the form that has one), the
|
|
1666
|
+
mass Description, the audit history, the spreadsheet import and bulk insert, the pictures window and gallery
|
|
1667
|
+
(`openPictureGallery`), a record's cards' popups (`RecordCards`, `openRecordFlow`), a `Chat`'s popup, message boxes
|
|
1668
|
+
and confirms (which already said `minimizable: false`), and any popup a page opens without asking. The image
|
|
1669
|
+
editor's window and the picture viewer are the editor module's own and never had one.
|
|
1670
|
+
- `handle.minimize()` from code still minimizes any popup opened with `openPopup`; the dock shows it as before.
|
|
1671
|
+
- Proved by nexus.template's `popup-windows.cjs` (a popup that asks has Minimize, Maximize, Close; one that does not -
|
|
1672
|
+
from a page, from code, opening maximized - has Maximize and Close; the dock, the stack, the rules by key, a scrolled
|
|
1673
|
+
body kept), `crud.cjs`, `crud-row-form.cjs`, `staff-posts.cjs`, `leave-guard.cjs` (the forms that minimize),
|
|
1674
|
+
`crud-workspace.cjs` (the workspace has none; a record off the page shown is now opened from the pinned guests),
|
|
1675
|
+
`crud-scopes.cjs` (the details have none; the Edit form they open is minimized across a switch), `excel.cjs`,
|
|
1676
|
+
`pictures.cjs` and `record-cards.cjs` (none), both motion modes, and `test-popup-rules.mts` (the forms' rule).
|
|
1677
|
+
|
|
1678
|
+
### A CRUD's narrowing box asks to be set
|
|
1679
|
+
|
|
1680
|
+
Card C-narrow-required (2026-11-06), from the owner on nexus.web's subscribers: *"when open it focus on the group and
|
|
1681
|
+
high light the required tab which help user to it need to set"*. Every `Crud` `narrowPicker` changes; a page changes
|
|
1682
|
+
nothing. New: `Field` / `Dropdown` `invalid`, `DataTable` `add.onDisabledPress`, and in `CrudNarrowPick.ts`
|
|
1683
|
+
`findLoneNarrowPick`, `startNarrowPick` (`NarrowPickStart`) and `narrowBoxTakesCursor` (`NarrowCursorMoment`).
|
|
1684
|
+
`useCrudNarrowPicker` takes the list's words too (optional) and returns more parts (`CrudNarrowPickerParts`); `Crud`
|
|
1685
|
+
is its only caller in the packages.
|
|
1686
|
+
|
|
1687
|
+
- **It settles before it asks.** As the page opens, the box shows a spinner and the list waits (no "Pick a group" that
|
|
1688
|
+
flashes before a remembered pick opens) while the kept pick is looked up; a kept pick no longer offered is forgotten.
|
|
1689
|
+
- **A box that offers one record picks it by itself**, and keeps it as a person's pick is (`remember`), `onPick` told.
|
|
1690
|
+
Offered as the box offers it: a record it never offers (a person still invited, an inactive one) does not count, and
|
|
1691
|
+
its pages are read at the box's own size, only until a second record shows.
|
|
1692
|
+
- **With nothing picked it asks:** the box is required (`aria-required`) and drawn with a field's invalid edge and ring
|
|
1693
|
+
(`invalid`: the look an error gives, with no message line in the toolbar), and a screen reader hears the list's
|
|
1694
|
+
words with it (`aria-describedby`; the page's `emptyMessage` when given). It takes the cursor as the page opens,
|
|
1695
|
+
without scrolling, unless the person has already pressed a key or a pointer there, focus is elsewhere, a popup or a
|
|
1696
|
+
panel is open, the box is not wholly on screen (a CRUD further down a demo page), or the screen is touch-only (a
|
|
1697
|
+
cursor would raise the on-screen keyboard over the list). A pick removes the mark; clearing it brings it back.
|
|
1698
|
+
- **The list points at it:** its empty message has a button with the box's placeholder ("↑ Pick a group") that sends
|
|
1699
|
+
the cursor to the box and opens its list. A read-only list (no Add for this person) asks the same way.
|
|
1700
|
+
- **Add pressed with nothing picked** sends the cursor to the box and nudges it (the field's nudge, as a read-only lock's;
|
|
1701
|
+
none with reduced motion), and the box's description becomes Add's reason (`addMessage`), so a screen reader hears
|
|
1702
|
+
why it landed there; the note under Add no longer shows for it. Add still looks unavailable and keeps its tooltip.
|
|
1703
|
+
- **A failed read** of the box's records leaves it asking, the cursor in it; its own list says "Could not load the
|
|
1704
|
+
options." with Retry when opened.
|
|
1705
|
+
- Proved by nexus.template's new **Admin › Posts** page (`/admin/posts`: one person's posts, the Person box in the CRUD's
|
|
1706
|
+
toolbar; `?people=one` offers one person) and `crud-narrow-required.cjs` (settling, the mark, the cursor, Add from the
|
|
1707
|
+
mouse and the keyboard, the list's button, a pick, remembered, forgotten, at work, a popup open, a failed read and
|
|
1708
|
+
Retry, one person offered among invited ones, below the fold and the Viewer role on /demo/crud, 390 px with a mouse
|
|
1709
|
+
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,
|
|
1710
|
+
and `test-input-narrowing.mts` (the opening and the cursor rule).
|
|
1711
|
+
|
|
1712
|
+
### A remembered choice from a server's list, kept per group, on Add only, and in rows
|
|
1713
|
+
|
|
1714
|
+
Cards C-input-memory-offered (2026-11-06, committed unfinished) and C-input-memory-offered-finish (2026-11-07), from what
|
|
1715
|
+
nexus.web's Subscribers Add (card SUB-defaults) found in [`remember`](#an-input-that-remembers-its-last-value-or-opens-on-the-companys-default):
|
|
1716
|
+
a kept type, area or package since trashed opened tagged "Inactive", one moved to another group opened untagged and the
|
|
1717
|
+
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"
|
|
1718
|
+
by hand. Additions only; no new export.
|
|
1719
|
+
|
|
1720
|
+
- **A picker confirms a kept choice before it opens.** A dropdown whose options come from `loadOptions` (as `crudPicker`
|
|
1721
|
+
gives) is asked by the value's key: at once from what the page holds (`peekOption`), else by `resolveOptions`, and for a
|
|
1722
|
+
key that answers nothing (a picker that names only its kept list) or a record without its narrowing's field, through
|
|
1723
|
+
its own pages narrowed as the form narrows them. Offered = found, not disabled, not inactive (`isOptionInactive`, else
|
|
1724
|
+
`inactive`/`isActive: false`), and inside its `narrowedBy` narrowing; for a list of values, the part offered. A kept
|
|
1725
|
+
choice not offered never opens and is forgotten; `preferred` gives way to the kept choice the same way. Before it was
|
|
1726
|
+
"a server's list takes any value".
|
|
1727
|
+
- **While it is asked** the box shows its spinner (`Dropdown` `loading`, through the new `InputOverrides.loading`) and
|
|
1728
|
+
opens nothing; the answer opens the value as the one the form opened with. It opens nothing over the person: a pick
|
|
1729
|
+
(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
|
|
1730
|
+
answer given for a group (the `narrowedBy`, `per` or `when` inputs) the form no longer holds opens nothing. A picker
|
|
1731
|
+
that cannot be read opens nothing and forgets nothing; it is asked again by the next form. A reset that finds the
|
|
1732
|
+
kept choice gone, or still to be asked, takes back the value memory opened before instead of leaving it in.
|
|
1733
|
+
- **`remember.per`:** one value kept per value of another input, under `nx-input:{key}-{that value}:{viewer}`: a package
|
|
1734
|
+
per group, since a package is one of its group's. While the other input holds no single value nothing is read or kept.
|
|
1735
|
+
- **`remember.when`:** remember only while these `InputCondition`s hold on the values the form opens with and saves.
|
|
1736
|
+
A CRUD's Edit form holds the record's key and its Add form does not, so `{ name: "id", filled: false }` remembers on
|
|
1737
|
+
Add only: Edit opens on the record's values, asks no picker, and its save keeps nothing.
|
|
1738
|
+
- **Rows remember** (a tabular or group form's row inputs): a new row's input opens on its kept value unless `newRow` or
|
|
1739
|
+
the values it is added with name it (so a remembered row input takes its first choice as its own `defaultValue`, not
|
|
1740
|
+
in `newRow`); a save of the form around them (`SubmitForm`) keeps the value of the row added last that is still there
|
|
1741
|
+
(one added then removed does not count; none added, nothing kept). `per` and `when` read the row's own values. A
|
|
1742
|
+
picker in a row is asked the same way (no spinner there), judged on the row as it was added.
|
|
1743
|
+
- Inside: `Helpers/InputMemory.ts` (`keptStandingNow`, `findKeptStanding`, `rememberedOpeningNow`,
|
|
1744
|
+
`findRememberedOpening`, `rememberDependencies`, `lastAddedRowKey`; pure), `FormStore` (`isRemembering`, internal),
|
|
1745
|
+
`RowsStore` (`attachMemory`, `rememberAdded`), `RowsInput` and `SubmitForm` (the save tells the rows inside the
|
|
1746
|
+
`<form>`). `test-input-memory.mts` 115 checks, `test-rows-store.mts` 123.
|
|
1747
|
+
- Proved by nexus.template's `/demo/form` "Remembered picks from a server's list, and in rows" (an order's amenity, a
|
|
1748
|
+
`crudPicker` narrowed by the order's group and kept per group on a new order only; a contact's kind in rows) and
|
|
1749
|
+
`input-memory-picker.cjs`: nothing kept, a save and the reset, another group, a page drawn by the server with the
|
|
1750
|
+
lookup held (the spinner, then the value, no hydration error), a trashed amenity and another group's (asked by key on
|
|
1751
|
+
a cold page, and at once from the list the page holds; no tag), a pick before the answer, the lookup down and the
|
|
1752
|
+
whole picker down, Edit, a row added then removed, a pick by the keyboard while asked and the kept value heard with
|
|
1753
|
+
the box, light and dark at 1280x720 and 390x844, both motion modes.
|
|
1754
|
+
|
|
1755
|
+
### A record tab's footer that stays in view
|
|
1756
|
+
|
|
1757
|
+
Card C-tab-footer (2026-11-07), the owner's first ask of the day for the subscriber window's counter tabs: their line
|
|
1758
|
+
("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.
|
|
1759
|
+
Additions only: 2 exports.
|
|
1760
|
+
|
|
1761
|
+
- **`CrudTabFooter`** (`nexus-shared/client`), with **`CrudTabFooterProps`** (`children`, `className`): put last in a
|
|
1762
|
+
tab's content, it holds the tab's words (a `<p>`) at the start and its buttons (`FormButtons`, or `Button`s) at the
|
|
1763
|
+
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
|
|
1764
|
+
place at the end of the content, so Save is always in view; it reaches across the panel's whole inner width on the
|
|
1765
|
+
panel's own surface under a `--nx-border` hairline, with the popup footer's room (`--nx-space-3` above and below, the
|
|
1766
|
+
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
|
|
1767
|
+
wrap as whole groups. Outside a workspace it is a plain bar where it stands.
|
|
1768
|
+
```tsx
|
|
1769
|
+
<ApiForm form={form} buttons={false} api={COUNTER_CREATE} inputs={inputs} />
|
|
1770
|
+
<CrudTabFooter>
|
|
1771
|
+
<p role="status" aria-live="polite">{summary}</p>
|
|
1772
|
+
<FormButtons form={form} submitLabel="Save" />
|
|
1773
|
+
</CrudTabFooter>
|
|
1774
|
+
```
|
|
1775
|
+
- **The keyboard and the footer.** A field (or a text box's caret) the focus moves to is scrolled clear of the footer:
|
|
1776
|
+
the panel keeps the footer's height as room (`scroll-padding`), from `--nx-crud-tab-footer-size`, which the footer
|
|
1777
|
+
measures and keeps on its panel while it is drawn. While the focus is in the footer that room goes, so Save taking
|
|
1778
|
+
the focus never scrolls the tab. A Save that finds a problem scrolls the field to above the footer.
|
|
1779
|
+
- Styles: `Nexus.Crud.css` (`nx-crud-tab-footer`; the panel's side padding is now `--nx-crud-panel-pad-x`, the same
|
|
1780
|
+
`--nx-space-5`, and `--nx-space-4` under 720px, and it keeps no padding under a footer).
|
|
1781
|
+
- Proved by nexus.template's `/demo/crud` "A guest's workspace", a new tab, **Book a stay** (a form longer than the
|
|
1782
|
+
panel at 1280x720, its line and Save in the footer, saved into the guest's notes), and `crud-workspace.cjs` section K
|
|
1783
|
+
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),
|
|
1784
|
+
across the panel's inner width, covering what scrolls under it, a field reached by Tab and Shift+Tab clear of it, Save
|
|
1785
|
+
focused by code and by Shift+Tab without the tab moving, the first problem scrolled to, one Update and one Detail for
|
|
1786
|
+
a save, the panel's own padding back on another tab, and a plain bar outside a workspace; then in 7 themes, light and
|
|
1787
|
+
dark, at 1280x720 and 390x844, a `--nx-border` hairline, the panel's background, no shadow, the line at AA, nothing
|
|
1788
|
+
past the bar or the window, whole groups wrapped.
|
|
1789
|
+
|
|
1790
|
+
### Form figures: an input worked out from others, the server's money rule, and a money column
|
|
1791
|
+
|
|
1792
|
+
Card C-form-figures (2026-11-07), the owner's first ask of the day: *"after the calculate what is actual value here not
|
|
1793
|
+
showing the old version 06-20 showing as calculate value"*, on the price list, the miscellaneous fees and the counters.
|
|
1794
|
+
Additions only: 13 exports, all from `nexus-shared`. One look moved by a pixel: a column dropdown's cells now meet their
|
|
1795
|
+
headings' edges (below).
|
|
1796
|
+
|
|
1797
|
+
- **`InputLayoutParams.compute`** (`InputCompute`: `from`, `value(values, withValue)`; Form only): an input worked out
|
|
1798
|
+
from others, shown and never typed. It is written when the form opens (new values, a loaded record) and again when an
|
|
1799
|
+
input in `from` changes - typed, set from code, loaded, reset - or when the form's `computeWith` changes. It is **no
|
|
1800
|
+
form value**: the store keeps none under its name (a loaded record's own field of that name is dropped), so it is
|
|
1801
|
+
never in `getValue()`, `getChanges()` or what Save sends, never checked (`problems()`), never a change (`isChanged()`,
|
|
1802
|
+
the leave guard, drafts), never remembered, and a value set from code or an `onFieldChange` patch naming it is not
|
|
1803
|
+
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.
|
|
1804
|
+
```ts
|
|
1805
|
+
{ type: EInputTypes.Number, name: "netAmount", label: "Calculated amount", decimals: 2, compute: { from: ["price", "vatPercentage"], value: values => priceAmounts(values).netAmount } }
|
|
1806
|
+
```
|
|
1807
|
+
- **The box**: the field's frame (label, hint, prefix, suffix, size, design) around an `<output>` named by its label,
|
|
1808
|
+
`aria-live="polite"`, on the read-only tint, its value in `--nx-text`: no lock, no read-only note, no − / +, no Tab
|
|
1809
|
+
stop. A Number is written with its `decimals` always (1,073.50), grouped, in the person's number format; null,
|
|
1810
|
+
undefined, "" and NaN leave it blank, never "NaN" or "0.00". While the values load (`loading`, an ApiForm's
|
|
1811
|
+
`loadValue`) it is blank and disabled with the inputs beside it; in a disabled or read-only form it takes that look.
|
|
1812
|
+
It is as tall as a NumberBox beside it, with the same edge, in every input design. A rule that throws shows nothing and
|
|
1813
|
+
says so in the console. Styles: `nx-field__control--output` and `nx-field__output-text` in `Nexus.Input.css`.
|
|
1814
|
+
- **`FormProps.computeWith`** (so `SubmitForm`'s and `ApiForm`'s too): what the page knows beside the values, such as a
|
|
1815
|
+
server's preview answer, handed to each `compute` as `withValue`. Only the worked-out inputs draw again when it
|
|
1816
|
+
changes, compared by identity, so pass state or a `useMemo` value. `compute: { from: [], value: (_values, line) =>
|
|
1817
|
+
line?.netAmount }` shows a figure the server worked out, and nothing until it answers.
|
|
1818
|
+
- `withInputConditions` joins `compute.from` into the input's `dependsOn`. **`isComputedInput`**, **`computedValue`**
|
|
1819
|
+
and **`computedText`** are the pure rules (`test-input-conditions.mts`).
|
|
1820
|
+
- **`priceAmounts(input, places = 2)`** (`PriceAmountsInput`: `price`, `quantity`, `discountPercentage`,
|
|
1821
|
+
`vatPercentage`; `PriceAmounts`: `amount`, `discountAmount`, `vatAmount`, `netAmount`): the Nexus server's money rule
|
|
1822
|
+
(`SubscriberRenewals.Price`, 06-20's `FN_CalculateAmount`), each step rounded half away from zero, on exact decimals
|
|
1823
|
+
(a scaled BigInt, each figure read from its shortest text), never on floats. A missing, null or NaN figure is 0, and a
|
|
1824
|
+
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.
|
|
1825
|
+
- **`priceLadder(row, { keys, locale, totalLabel })`** (`PriceLadderKeys`, `PriceLadderLine`, `PriceLadderOptions`): a
|
|
1826
|
+
receipt's money lines from a row's own figures, on the server's `CounterAmounts` names by default: Rate (left out
|
|
1827
|
+
when the row has none), Amount, "Discount (5%)" and "VAT (13%)" (each left out at 0), then "Calculated amount". A
|
|
1828
|
+
percentage has up to 4 places and no trailing zeros, in `locale` ("Discount (2,5%)" with a decimal comma).
|
|
1829
|
+
- **The presets** (`CrudColumns.ts`, one wording for every page; **`PriceKeys`**: `price`, `quantity`, `discount`,
|
|
1830
|
+
`vat`, by default `price`, `discountPercentage`, `vatPercentage` and no quantity):
|
|
1831
|
+
- `crudInputs.netAmount(keys?, changes?)`: a Number `netAmount` labelled "Calculated amount", 2 places, span 3, hint
|
|
1832
|
+
"Price less the discount, plus VAT on the rest.", worked out from the keys, and blank while the price is empty.
|
|
1833
|
+
`changes` replaces any key, `compute` included (a counter's box shows the server's preview instead).
|
|
1834
|
+
- `crudColumns.amounts(keys?, { group })`: Price (money), Discount and VAT (percents) and Calculated amount
|
|
1835
|
+
(`netAmount`, money, worked out from the row's own figures, so a row a save just answered shows it), under one band,
|
|
1836
|
+
"Amount details". A page spreads them: `...crudColumns.amounts()`.
|
|
1837
|
+
- `crudColumns.priceLadder(row, { keys, locale, totalLabel, group = "Money" })`: the ladder's lines as money columns
|
|
1838
|
+
for a record's details.
|
|
1839
|
+
- **A money column in a dropdown**: `DropdownColumnType` gains `"money"`: the item's figure with 2 places in the
|
|
1840
|
+
person's number format (1,00,000.00 under lakh and crore), at the end of its column (its heading too), in even digits,
|
|
1841
|
+
and empty for a missing one. Its track is `auto` unless it has a `width`. **`dropdownColumnAlign`** says where a
|
|
1842
|
+
column's cells sit. A column dropdown's rows now take the list's gutters (`column-gap: normal` on the row's subgrid),
|
|
1843
|
+
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
|
|
1844
|
+
heading.
|
|
1845
|
+
- Proved by `test-number-helpers.mts` (SubscriberRenewalCheck's money cases and more, and the ladder),
|
|
1846
|
+
`test-form-store.mts` (a computed input never in getValue, getChanges or problems, never a change; the presets),
|
|
1847
|
+
`test-input-conditions.mts` and `test:types`. Also by nexus.template's `/demo/form` "Worked out from other inputs"
|
|
1848
|
+
and `/demo/dropdown`'s Fee, with `form-figures.cjs` in both motion modes: 1,073.50 on open, every keystroke followed
|
|
1849
|
+
(900 / 0 / 13 gives 1,017.00), Reset, a held Load blank and disabled then 904.00, a cleared price blank, a 12-digit
|
|
1850
|
+
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
|
|
1851
|
+
three designs; the "From the page" pair blank, then 800.00 | 904.00; the money column at 1,00,000.00 and
|
|
1852
|
+
100,000.00, ending on its heading's edge; and in 7 themes, light and dark, at 1280x720 and 390x844, the figure at
|
|
1853
|
+
7.54:1 or more with nothing past its box or the window.
|
|
1854
|
+
|
|
1855
|
+
### A form asks before it saves, the full details in sections
|
|
1856
|
+
|
|
1857
|
+
Card C-form-confirm (2026-11-07), the owner's second ask of the day: *"the misc counter and payment counter when click
|
|
1858
|
+
save show popup all full details then user do contirm then save"*, as 2026-06-20's "Confirm Counter Payment" did. 7
|
|
1859
|
+
exports, all from `nexus-shared`, and three additions to existing options. Two looks change without a line of the
|
|
1860
|
+
app's: a record's details whose columns name a `group` now show a heading over each run (below), and a press outside a
|
|
1861
|
+
popup that stays open through it no longer takes the focus.
|
|
1862
|
+
|
|
1863
|
+
- **`SubmitFormProps.confirm`** (**`FormConfirmParams`**: `when`, `title`, `message`, `details`, `confirmLabel`,
|
|
1864
|
+
`cancelLabel`, `size`), so `ApiForm`'s too, and **`CrudFormOptions.confirm`** for a CRUD's Add and Edit: a question
|
|
1865
|
+
after every value has passed its checks and before the save starts. It is always a `question`, so its confirm button
|
|
1866
|
+
has the focus and Enter saves. While it is open nothing else submits the form (Save, Enter, Ctrl+S, `form.submit()`),
|
|
1867
|
+
Ctrl+S does not open the browser's Save page, and the form says no "Saving…". **Back to the form** (the default
|
|
1868
|
+
`cancelLabel`), Esc and × send nothing and leave every value as it was, the focus back on what opened it; a click
|
|
1869
|
+
outside leaves it open. **Confirm and save** (the default `confirmLabel`) goes on to the save as before. A rule here
|
|
1870
|
+
that throws shows its message under the form and asks nothing. `when` limits it to some saves (the kinds that take
|
|
1871
|
+
money). The question goes if the form leaves while it asks.
|
|
1872
|
+
```tsx
|
|
1873
|
+
<ApiForm api={COUNTER_CREATE} transform={saveBody} confirm={{
|
|
1874
|
+
when: values => ASKS_BEFORE_SAVING.includes(values.counterType),
|
|
1875
|
+
title: "Save this payment?",
|
|
1876
|
+
message: summary,
|
|
1877
|
+
details: () => ({ row: line, columns: counterDetailColumns(line, "confirm"), currency }),
|
|
1878
|
+
confirmLabel: "Confirm payment",
|
|
1879
|
+
size: "xl",
|
|
1880
|
+
}} />
|
|
1881
|
+
```
|
|
1882
|
+
- **What it shows is what is sent.** Each text and the details are handed the values and **`body`**, what will be sent:
|
|
1883
|
+
an `ApiForm`'s `transform` runs once, before the question (a throw there shows under the form and asks nothing), and
|
|
1884
|
+
that very body is sent after Confirm, never worked out again; a plain `SubmitForm`'s body is its values. Without
|
|
1885
|
+
`confirm`, `ApiForm` sends as before.
|
|
1886
|
+
- **`details`** answers **`FormConfirmDetails`** (`row`, `columns`, `currency`; the row and columns may be the page's own
|
|
1887
|
+
types) and is drawn by `CrudDetails`, in the normal text colour, under the muted line. The box is `lg` with details
|
|
1888
|
+
(two fields to a line) and `sm` without, unless `size` says otherwise (`xl`: three to a line). Its body scrolls while
|
|
1889
|
+
the buttons stay in view.
|
|
1890
|
+
- **`CrudDetails` draws `column.group`**: each run of neighbouring columns that name one group is a section under an
|
|
1891
|
+
`h3` heading (`nx-crud-details__section`, `nx-crud-details__heading`, the small muted heading style of a dropdown's
|
|
1892
|
+
groups), the heading nearer its fields than the section above it. Columns with no group draw as before, without a
|
|
1893
|
+
heading; with no group anywhere the markup is exactly as before. So every details view gets it: the View popup, a
|
|
1894
|
+
workspace's Details tab, a counter's preview, the question. **`crudDetailsRuns(columns)`** (**`CrudDetailsRun`**) is
|
|
1895
|
+
the rule; a group named again further on starts a new run, as a table's band does.
|
|
1896
|
+
- **`formConfirmAsks(confirm, values)`** and **`formConfirmQuestion(confirm, values, body)`**
|
|
1897
|
+
(**`FormConfirmQuestion`**) are the question's pure rules: whether to ask, each text settled, the defaults.
|
|
1898
|
+
- **The message box** (`messageBox.confirm`, `show`, `alert`, `prompt`) takes **`details`**: drawn after the message in
|
|
1899
|
+
its own `.nx-message__details`, in the normal text colour (the message stays muted), scrolling with the body. Also
|
|
1900
|
+
**`closeOnBackdrop`** (default: as `closable`; `false` keeps a box open through a click outside while Esc and × still
|
|
1901
|
+
dismiss it) and **`signal`** (an abort dismisses the box). A box without them draws exactly as before.
|
|
1902
|
+
- **`Popup`**: a press on the backdrop of a popup that stays open through it (`closeOnBackdrop: false`: the question, a
|
|
1903
|
+
CRUD's Add and Edit, a row's form, a spreadsheet import) leaves the focus where it was. It used to move to the
|
|
1904
|
+
dialog, so after a stray click Enter no longer confirmed and a field lost its caret.
|
|
1905
|
+
- Styles: `Nexus.Crud.css` (`nx-crud-details__section`, `nx-crud-details__heading`), `Nexus.Popup.css`
|
|
1906
|
+
(`nx-message__details`).
|
|
1907
|
+
- Proved by `test-form-confirm.mts` (the runs, `when`, the texts and the defaults) and `test:types` (`form-confirm.tsx`:
|
|
1908
|
+
a page's own row type in the details, the mistakes refused). Also by nexus.template's `/demo/form` "Confirm before
|
|
1909
|
+
saving" (an order: "Place this order?", the body in Order and Money sections, "Place order", `xl`, a transform that
|
|
1910
|
+
can throw, a customer the demo server refuses), with `form-confirm.cjs` in both motion modes: Place order, Enter in a box
|
|
1911
|
+
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
|
|
1912
|
+
form's own submit, Ctrl+S (its default held) and a backdrop click while it asks change nothing and leave the focus on
|
|
1913
|
+
Place order; Enter sends one call whose body is the figures shown; Esc, × and Back send nothing, keep every value and
|
|
1914
|
+
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
|
|
1915
|
+
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
|
|
1916
|
+
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,
|
|
1917
|
+
and in windows too short for them the body scrolling with Place order still in view. `/demo/crud`'s guest workspace
|
|
1918
|
+
shows its Details in Contact and Account sections (`crud-workspace.cjs`, in every theme and both sizes).
|
|
1919
|
+
|
|
1920
|
+
### A few fields in a record's profile, left out of its Details tab
|
|
1921
|
+
|
|
1922
|
+
Card C-profile-fields (2026-11-07), the owner's third ask of the day: *"in subscriber profile when open then show the
|
|
1923
|
+
expirations status like the how much will run when disconnect the status which displaying in the datatable"* - a
|
|
1924
|
+
subscriber's Expiry and Inactive under the status tags of its window. Additions only: 2 exports, both from
|
|
1925
|
+
`nexus-shared`, and one addition to an existing type. A profile without `fields` draws exactly as before.
|
|
1926
|
+
|
|
1927
|
+
- **`CrudDetailsProfile.fields`** (the type takes the row now, `CrudDetailsProfile<TRow>`, defaulting as before): a
|
|
1928
|
+
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
|
|
1929
|
+
details draw it (`TableCellValue`): its format in the person's settings (a date in their date format, read again when
|
|
1930
|
+
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
|
|
1931
|
+
details' `columns` (the table's, unless the details name their own); a key no column has is left out, and so is a
|
|
1932
|
+
field with no value for the record (null, undefined, blank text, an empty list, NaN; `false` and `0` are values).
|
|
1933
|
+
```ts
|
|
1934
|
+
profile: row => ({ title: row.fullName, tags: [...], fields: [EXPIRY_COLUMN, INACTIVE_COLUMN] })
|
|
1935
|
+
```
|
|
1936
|
+
- **The built-in Details tab leaves out every key `fields` names**, an empty one too, so a record's window never shows
|
|
1937
|
+
a field twice and a page filters nothing by hand. Sections (`group`) stay as they were for the columns left.
|
|
1938
|
+
- **The look**: a `<dl class="nx-crud-profile__fields">`, a `div.nx-crud-profile__field` a pair (`dt.nx-crud-profile__label`,
|
|
1939
|
+
`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
|
|
1940
|
+
line on the grid's two tracks, labels in `--nx-text-muted` and values in `--nx-text` at `--nx-text-sm`; a long value
|
|
1941
|
+
wraps inside the column and a tag that no longer fits goes under its value, at its start. Under 720px of popup width
|
|
1942
|
+
(the one-row profile) the pairs follow the tags as one wrapped line - beside them when they all fit, else on the next
|
|
1943
|
+
line - each pair wrapping as words do with its label kept with the start of its value.
|
|
1944
|
+
- **`crudProfileFields(fields, columns, row)`** (the fields a profile shows for a record) and
|
|
1945
|
+
**`crudDetailsWithoutProfile(columns, fields)`** (the details' columns less the keys named; the same list when none
|
|
1946
|
+
are) are the rules. `isCrudProfile` takes the row type too.
|
|
1947
|
+
- Styles: `Nexus.Crud.css`, beside the profile's.
|
|
1948
|
+
- Proved by `test-crud-profile-fields.mts` (37 checks: a column or a key, an unknown key, a key named twice, every kind of
|
|
1949
|
+
empty value, a column's own `value` and a path, the keys taken out of the Details and the groups kept) and
|
|
1950
|
+
`test:types` (`crud-profile.tsx`: a page's own row type, a column written in place with its tag typed from the row,
|
|
1951
|
+
a column of another row's type refused). Also by nexus.template's `/demo/crud` "A guest's workspace" (the guest's next
|
|
1952
|
+
stay, Arrives with how soon as a tag and Leaves, in the profile and not in the Details; Book a stay moves them), with
|
|
1953
|
+
`crud-workspace.cjs` section L: the `<dl>` after the tags, the dates in the default format and again as DD/MM/YYYY
|
|
1954
|
+
after a live change of the preference, no Arrives or Leaves in the Details, a long date (`dddd, MMMM DD, YYYY`) inside
|
|
1955
|
+
the 20rem column with its tag under it, one wrapped line at 700px and 390px with each label beside its value, a stay
|
|
1956
|
+
booked followed with one Update and one Detail, and a guest with no stay with no fields; then in 7 themes, light and
|
|
1957
|
+
dark, at 1280x720 and 390x844, the labels muted, the values and the tag at AA, the tag's dot in `--nx-info`, the
|
|
1958
|
+
date in two formats, nothing past the profile, its column or the window, and no Arrives or Leaves in the Details.
|
|
1959
|
+
|
|
1960
|
+
### An input's width that follows a value
|
|
1961
|
+
|
|
1962
|
+
Card C-span-when (2026-11-07), the owner's fourth ask of the day: *"if possible and easy: in the cash counter make:
|
|
1963
|
+
Bill date as col-12 when it put value and during the visible input time make it col-6."* - read as: Bill date takes
|
|
1964
|
+
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
|
|
1965
|
+
number and what follows never move. Additions only: 2 exports, both from `nexus-shared`. An input without `spanWhen`
|
|
1966
|
+
draws exactly as before.
|
|
1967
|
+
|
|
1968
|
+
- **`InputLayoutParams.spanWhen`** (`InputSpanWhen`: `when`, `span`; Form only): the input spans `span` columns while
|
|
1969
|
+
every condition of `when` holds - `showWhen`'s conditions, one or a list, each on an input's value as it is now; the
|
|
1970
|
+
input's own name follows its own typing - else its own `span` (a value or a getter).
|
|
1971
|
+
```ts
|
|
1972
|
+
{ type: EInputTypes.Date, name: "billDate", label: "Bill date", span: 6, spanWhen: { when: { name: "billDate", filled: false }, span: 12 } }
|
|
1973
|
+
```
|
|
1974
|
+
- Built on the `showWhen` machinery: `withInputConditions` gives such an input a live `span` getter and joins the names
|
|
1975
|
+
its conditions read into `dependsOn`, so its cell takes the new width on the key that makes or unmakes the value,
|
|
1976
|
+
without mounting the input again: the text, the caret, the focus and an open calendar stay. Form's grid and
|
|
1977
|
+
`Nexus.Form.css` are unchanged (a cell reads `span` each time it draws), so a narrow form (under 34rem) still gives
|
|
1978
|
+
every input the whole row. **`spanFor(spanWhen, read, own)`** is the pure rule.
|
|
1979
|
+
- The `dependsOn` docs now name `span` among the getters a followed change reads afresh: a `get span()` with
|
|
1980
|
+
`dependsOn` already worked (nexus.builder.web's class shape uses one); `spanWhen` says it without a getter.
|
|
1981
|
+
- Like `showWhen`, it is Form's only: a tabular or group form's rows ignore it.
|
|
1982
|
+
- Proved by `test-input-conditions.mts` (21 new checks: the rule both ways, a list, an empty list, the live span both
|
|
1983
|
+
ways, a condition on the input's own name, its own `span` getter kept and giving way, with `showWhen` on the same
|
|
1984
|
+
input, `dependsOn` joined once each, the page's params unchanged; in a form store, the cell drawn again on its own
|
|
1985
|
+
typing with the same stamp, and a load) and `test:types` (`input-types.ts`: one condition or a list; refused without
|
|
1986
|
+
`span`, without `when`, a condition with no name, a span as text). Also by nexus.template's `/demo/form` "Inputs
|
|
1987
|
+
shown by another input", whose form now ends with a Bill section - Bill date, the switch "Count the dates from the bill
|
|
1988
|
+
date" (shown while the date is filled) and Bill number - with `label-when.cjs` section S in both motion modes: on every
|
|
1989
|
+
key the width follows the value and the focus and caret stay at the end of the same box, the calendar the click opened
|
|
1990
|
+
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
|
|
1991
|
+
never moves (typing, a key that unmakes the date, ×, a calendar pick), Tab goes from the date to the switch and then
|
|
1992
|
+
Bill number; every density and input design; then 7 themes, light and dark, at 1280x720 and, where every cell is the
|
|
1993
|
+
whole row, 390x844. Seen red with the rule broken (42 lines) and with a remount on a width change (33 lines).
|
|
1994
|
+
|
|
1995
|
+
### Record cards that fit what was built on them
|
|
1996
|
+
|
|
1997
|
+
Card C-record-cards-fit (2026-11-07): the nine gaps the two Accounts agents met while building nexus.accounts'
|
|
1998
|
+
dashboard on `RecordCards`, and the seven findings of the round-2 review in this package. Additions only: 6 exports, all
|
|
1999
|
+
from `nexus-shared` (`keepsTyping`, `recordActionName`, `recordBusyKeys`, `recordHeldLines`, `RECORD_STILL_READ`, the
|
|
2000
|
+
type `RecordGoToOptions`), and new optional params. **Three looks change with no app change**: a card's heading buttons
|
|
2001
|
+
go under its title where the card is narrow, a window popup's description wraps under its title on a phone, and a
|
|
2002
|
+
picture's hidden buttons take no press. **One behaviour changes**: a password or a one-time code typed is no longer
|
|
2003
|
+
asked about before it is dropped (below).
|
|
2004
|
+
|
|
2005
|
+
- **A step's form** (`RecordStepContext.form`): every step is handed its `FormStore`, and `inputs` as a function gets
|
|
2006
|
+
it, so an input's label, `hidden` or options can follow another input as typed (getters with `dependsOn`) - the
|
|
2007
|
+
region levels of an address, named after the place picked above. nexus.accounts' address card can now be a flow.
|
|
2008
|
+
```tsx
|
|
2009
|
+
add: { title: "Add an address", inputs: ({ form }) => addressInputs(form), submit: ACCOUNT_API.addAddress }
|
|
2010
|
+
```
|
|
2011
|
+
- **A step's own rule**, `RecordStepParams.validate(values, context)`, the form's `validate` (messages by input, or one
|
|
2012
|
+
for the step), asked before anything is sent: a new password and its confirmation that differ.
|
|
2013
|
+
- **A step shown fresh**: `goTo(step, { fresh: true })`, and `RecordStepParams.fresh` for a step shown fresh each time
|
|
2014
|
+
it is reached again (by `next` or `goTo`): its form is dropped and it opens on its values, so an old code or
|
|
2015
|
+
password is gone.
|
|
2016
|
+
- **Nothing worth keeping** (`FormStore.isWorthKeeping()`, `keepsTyping(params)`): a value typed in a `Password` or
|
|
2017
|
+
`Code` input is a change (Save and the status line say so) but never asked about - not by Cancel or a popup's close
|
|
2018
|
+
(`confirmDiscard`), not by a link inside the app, not by `beforeunload` or `confirmPageLeave`. Any other change is
|
|
2019
|
+
asked about as before. A record step with `confirmCancel: false` also leaves the page's guards.
|
|
2020
|
+
- **A heading's buttons while the data is read**: a card's (and the hero's) action `hidden` and `held`, typed on the
|
|
2021
|
+
data, are asked only once it is read; until then the button is held, saying "Still being read." (the hero's were
|
|
2022
|
+
hidden), or not drawn yet with the new `loading: "hidden"`. `recordActionState(action, null, ...)` is that rule.
|
|
2023
|
+
- **Heading buttons named with their card** to a screen reader ("Edit: Personal details"), as an item's icons are with
|
|
2024
|
+
their item (`recordActionName`); the visible label is unchanged and starts the name.
|
|
2025
|
+
- **Nothing moves when the records come**: a held list row is a read row's height (its name's line and lines under it
|
|
2026
|
+
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
|
|
2027
|
+
are two lines of text, a count in a title no longer makes its line a pixel taller, and the new
|
|
2028
|
+
`RecordFieldParams.heldLines` and `RecordItemsCardParams.heldRowLines` hold as many lines as a value or a row
|
|
2029
|
+
usually takes (`heldRows` already held as many rows). Text that wraps by the width can still grow its card.
|
|
2030
|
+
- **A narrow card's buttons under its title**: the heading's text keeps 12rem (8rem for a title alone) beside its
|
|
2031
|
+
buttons, else the buttons go on a line under it, at its start - no title squeezed to a word a line on a phone.
|
|
2032
|
+
- **A window popup's description on a phone** (600px and under) takes the lines under the title it needs, in full; the
|
|
2033
|
+
icon and the window buttons keep the bar's first line. Wider, it is the one low bar as before.
|
|
2034
|
+
- Review fixes: a page's popups close with it and are keyed by the page that opened them, so a page drawn afresh never
|
|
2035
|
+
brings back a popup bound to the old data (whose Save sent the stale record); an Edit with a Tabular or Group input
|
|
2036
|
+
opens on its rows (`recordOpenedValues`), so its Save no longer wipes them; a card keeps a list of its calls on their
|
|
2037
|
+
way (`recordBusyKeys`), so one that ends first never frees a row whose own call still runs; the once-only scroll to
|
|
2038
|
+
a linked card (`#emails`) is skipped once the person has scrolled, touched, pressed a key or 5 seconds have gone, or
|
|
2039
|
+
when the card is already in view; a picture's hidden buttons take no press (`pointer-events: none` until shown) and
|
|
2040
|
+
are always shown where any pointer is coarse (a laptop's touch screen); a list that could not be read in the pictures
|
|
2041
|
+
window has the window's padding; the document list's Upload in a full one-file category is held with its reason
|
|
2042
|
+
("This keeps one file: delete it first.").
|
|
2043
|
+
- Proved by `test-record-cards.mts` (151 checks: rows opened, the rules not asked while read, `loading`, busy keys,
|
|
2044
|
+
held lines), `test-form-store.mts` (worth keeping), `test:types` (`record-cards.tsx`), and nexus.template's
|
|
2045
|
+
`/admin/profile`, whose demo now has an address's lines named after what it is, Change password with its
|
|
2046
|
+
confirmation, Send a reset link, Back on the code step, and held rows as many as the lists: `record-cards.cjs` section
|
|
2047
|
+
K and new lines in A (every card's place and height through the load at 1280, every heading button's name), and
|
|
2048
|
+
`pictures.cjs` K7. Seen red with the busy list, the popups' closing, and the row's height undone.
|
|
2049
|
+
|
|
2050
|
+
### Also in 4.0.0: the changes prepared as 3.3.0
|
|
2051
|
+
|
|
2052
|
+
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
|
|
2053
|
+
solid fills' hover added on 2026-10-26, and never published. On their own they are new features with no code break;
|
|
2054
|
+
the sections below say what each added. What can move for an app:
|
|
2055
|
+
|
|
2056
|
+
- **A list's Add button reads "Add New"** on every `Crud`, whatever its subject; the form it opens keeps its subject.
|
|
2057
|
+
- **Menus** fade a switched-off item's icon and label, not its reason, and wrap a long second line.
|
|
2058
|
+
- **A table's spreadsheet menu** offers only what the app's permissions allow; the template and the export stop at a
|
|
2059
|
+
failed settings call, with that call's one toast.
|
|
2060
|
+
- **A Nexus save is not read back**, except a new record with a `crudColumns.dbJoin` column given no `from`.
|
|
2061
|
+
- **An Edit saved while the CRUD shows another list** goes where its band says, no longer into that list.
|
|
2062
|
+
- **A `crudPicker` with a `valueKey` other than `id` and no `find`** no longer posts its values to `findListing`.
|
|
2063
|
+
- **A solid fill under the pointer is darker than at rest in every theme**, light and dark (it turned lighter in a dark
|
|
2064
|
+
theme): a primary or danger `Button`, and so `SplitButton`, `FormButtons` and every solid button built on them, the
|
|
2065
|
+
chat's unread button and Send, and the popup's close button while it is pressed. **An app's own theme that puts a
|
|
2066
|
+
dark label on a light solid fill** loses contrast under the pointer now: give its fills a light label at 4.6:1 or
|
|
2067
|
+
more, as the README's example does.
|
|
2068
|
+
- **Nord's dark solid fills are a little lighter and carry white**: `--nx-primary-solid` `#557b86` (was `#4b717c`),
|
|
2069
|
+
`--nx-danger-solid` `#99686d` (was `#8e5e63`), and `--nx-primary-text` and `--nx-danger-text` `#ffffff` (were snow,
|
|
2070
|
+
`#eceff4`), on every button, check, selected day and tab indicator in Nord's dark mode.
|
|
2071
|
+
|
|
2072
|
+
### A solid fill under the pointer keeps its label readable
|
|
2073
|
+
|
|
2074
|
+
2026-10-26, the owner's decision of 2026-10-22: "fix it everywhere". `--nx-primary-hover` and `--nx-danger-hover` mixed
|
|
2075
|
+
the fill 16% toward `--nx-text`. That darkened it in light mode, but lightened it in dark mode, where the fill is already
|
|
2076
|
+
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
|
|
2077
|
+
themes. Both are now the fill with **12% black** mixed in (OKLab), in every theme and in both modes:
|
|
2078
|
+
|
|
2079
|
+
- 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
|
|
2080
|
+
one (it was 5.94:1 or more there); at rest and with the keyboard's focus nothing changed (4.60:1 or more);
|
|
2081
|
+
- 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
|
|
2082
|
+
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),
|
|
2083
|
+
and now seen in every theme.
|
|
2084
|
+
|
|
2085
|
+
Measured in a browser under a real mouse, the colours composited through a canvas, on every solid fill the packages
|
|
2086
|
+
hover: `Button` primary and danger, both halves of a `SplitButton`, `FormButtons`' danger submit, the chat's unread
|
|
2087
|
+
button and Send, and the popup's close button. The close button still turns `--nx-danger-solid` under the pointer;
|
|
2088
|
+
pressed, it takes `--nx-danger-hover`, so it now turns darker instead of lighter. Its minimise, maximise and restore
|
|
2089
|
+
buttons did not change. `npm run check:themes` reads the two lines from `Nexus.Tokens.css` and checks both: the label at
|
|
2090
|
+
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
|
|
2091
|
+
other than a solid fill's hover gets the darker colour in dark mode too.
|
|
2092
|
+
|
|
2093
|
+
**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
|
|
2094
|
+
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:
|
|
2095
|
+
Nord's panel is lighter than other themes' and its label was snow (`#eceff4`), which left the fill no room to darken.
|
|
2096
|
+
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
|
|
2097
|
+
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)
|
|
2098
|
+
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'
|
|
2099
|
+
fills keep 2.30:1 or more under the pointer. `check:themes` now holds every hovered fill to 2:1 on `--nx-surface`. A
|
|
2100
|
+
solid fill's label is light in every theme and both modes, because hover and pressed darken the fill: a dark label on
|
|
2101
|
+
a light fill reads at rest and loses contrast under the pointer.
|
|
2102
|
+
|
|
2103
|
+
### A QR code, `QrCodeImage`
|
|
2104
|
+
|
|
2105
|
+
2026-10-22, ask 4 of the template's two-step pages (`nexus.accounts`, whose authenticator setup drew its own). Added
|
|
2106
|
+
only.
|
|
2107
|
+
|
|
2108
|
+
- **`QrCodeImage`** (`nexus-shared`; a server component can render it: nothing in it needs the browser): `value`,
|
|
2109
|
+
`label` for screen readers, `level` (L, M, Q, H; M by default), `className` and `style` for where the page puts it.
|
|
2110
|
+
One SVG path of black modules on a white square that holds the quiet zone, black on white in every theme and in
|
|
2111
|
+
forced colours. New stylesheet `Nexus.QrCode.css`, class `nx-qr`: 9rem wide, a control's rounded corners - the look
|
|
2112
|
+
of nexus.accounts' `.tp-auth__qr`, whose grid area stays the page's.
|
|
2113
|
+
- **`encodeQr(text, level)`** and **`qrPath(code, border)`** (`QrCode`, `QrLevel`): QR Code Model 2 written by hand,
|
|
2114
|
+
byte mode (UTF-8), versions 1 to 40, levels L, M, Q and H, Reed-Solomon, the eight masks scored by the four penalty
|
|
2115
|
+
rules. A text past version 40 throws, with how much the level holds. Plain erasable TypeScript, so Node runs it as it
|
|
2116
|
+
is: `Project-Skills/Checks/qr-code-check.mjs` reads every code back with jsQR 1.4.0 (410 of 410 against this file),
|
|
2117
|
+
and `npm run test:qr` checks its shape without a decoder.
|
|
2118
|
+
|
|
2119
|
+
### A list beside the record picked in another, and a picker's own find
|
|
2120
|
+
|
|
2121
|
+
2026-10-22, asks 5 and 6 of nexus.web's company users page. The owner picked "the places of the person picked beside
|
|
2122
|
+
the people", and decided that a person is given a place only once they have joined. Added only: no export removed and no
|
|
2123
|
+
exported constant changed. A table or CRUD that passes none of the new props is unchanged, but for the two fixes at the
|
|
2124
|
+
end, which any app can see.
|
|
2125
|
+
|
|
2126
|
+
- **A picked row, `DataTable` `onPick`** (and `Crud` `onPick`): rows the user picks one of, for a page that shows the
|
|
2127
|
+
picked one's records beside the table.
|
|
2128
|
+
- A click on a row picks it; a double-click still opens it, and `openOnClick` gives way. The keyboard reaches the rows
|
|
2129
|
+
through a radio button each, out of sight in the first column and named with the row: Tab to the picked one, and the
|
|
2130
|
+
arrow keys move the pick.
|
|
2131
|
+
- The picked row is tinted as a selected one is, a ring round it shows the radio's focus, and the rows are numbered
|
|
2132
|
+
unless `numbered` is false.
|
|
2133
|
+
- The pick lives in the store (**`TableState.picked`**, **`TableStore.pick`**, **`TableStoreOptions.onPickChange`**)
|
|
2134
|
+
and follows the record: a search, a sort or another page keep it, an upsert hands the page its new copy, and it is
|
|
2135
|
+
let go when the record is removed, is gone after a load, or another list starts over.
|
|
2136
|
+
- New attributes `data-pick` and `data-picked` on a row, and the class `nx-table__pick`.
|
|
2137
|
+
- **A list narrowed to it, `Crud` `narrow`** (**`CrudNarrow`**: `picked`, `key`, `valueKey`, `labelKey`, `addLock`,
|
|
2138
|
+
`emptyMessage`), fed from the other list's `onPick`.
|
|
2139
|
+
- Only the picked record's records show, under a caption naming it ("Places of: Ram Thapa"). The table's name for
|
|
2140
|
+
screen readers says it too.
|
|
2141
|
+
- A list paged in the browser keeps the whole list and shows the picked record's rows from it, so another pick costs
|
|
2142
|
+
no request. A server's pages carry the value as a filter named `key`, and are kept per picked record.
|
|
2143
|
+
- 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
|
|
2144
|
+
("For Ram Thapa" in its title bar). Its box is named from the picked record, with no lookup.
|
|
2145
|
+
- Add waits, saying why on a press, while nothing is picked or the picked record may take none: `addLock`, by default
|
|
2146
|
+
the backend's rule (`CrudBackend.isPickable`), so a Nexus record pending (30-39), trashed or deleted takes none.
|
|
2147
|
+
- A save lands in the list of the record it names and in no other, whichever record is picked when it is saved.
|
|
2148
|
+
Joined names come from the picked record with no call (a `crudColumns.dbJoin` given `from`).
|
|
2149
|
+
- New helpers **`crudNarrowing`** and **`crudInNarrowing`** (**`CrudNarrowing`**).
|
|
2150
|
+
- **`DataTable` `caption`**: a line inside the table's frame, over its bar, saying what its rows are. Beside another
|
|
2151
|
+
table, the two frames still start level. New class `nx-table__caption`.
|
|
2152
|
+
- **`DataTable` `add`** takes `disabled` and `description`, as `TreeView`'s own button does. The button stays in the Tab
|
|
2153
|
+
order, `aria-disabled`, and a press shows the description in a note under it; the description is its tooltip too. While
|
|
2154
|
+
it waits, the spreadsheet's items stay in the options menu, without Bulk insert.
|
|
2155
|
+
- **`crudPicker` `find`**: where a value the kept list does not offer is looked up when `valueKey` is not `id` - an
|
|
2156
|
+
endpoint POSTed such values, as `findListing` is the keys, whose answers are read by `valueKey` (a person's `userId`,
|
|
2157
|
+
which their company row carries beside its own key); or a source of the page's own. Its answers are kept apart from
|
|
2158
|
+
`findListing`'s, and `expireLookups` marks them old with the controller's own. **`joinSource(source, backend, key)`**
|
|
2159
|
+
takes the field the answers are filed under.
|
|
2160
|
+
- **`lockInputs(inputs, locked, values, known)`** and **`CrudFormRequest.known`**: records the caller holds for the values
|
|
2161
|
+
it fixes. A fixed dropdown whose value one of them holds gets it as a selected option.
|
|
2162
|
+
- A save names its joined columns from a dropdown's `selectedOptions` too, after what the user picked.
|
|
2163
|
+
|
|
2164
|
+
What can move for an app on 3.2.0:
|
|
2165
|
+
|
|
2166
|
+
- **An Edit saved while the CRUD shows another list** - its form minimized, the trash shown meanwhile - no longer goes into
|
|
2167
|
+
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
|
|
2168
|
+
the table when an edit took it out of the list on screen.
|
|
2169
|
+
- **A `crudPicker` whose `valueKey` is not `id`, with no `find`**, no longer posts its values to `findListing`, which
|
|
2170
|
+
takes the records' keys and could answer none of them. The form names such a value from the record it holds
|
|
2171
|
+
(`selectedOptions`), or shows it as not found, without the call.
|
|
2172
|
+
|
|
2173
|
+
### A step that removes something
|
|
2174
|
+
|
|
2175
|
+
2026-10-22, asks 1-3 of the template's two-step pages (`nexus.accounts`, `/account/security`): a step that removes an
|
|
2176
|
+
authenticator app, turns two-step off or makes new codes confirms with a password, and carried two stand-ins of its own.
|
|
2177
|
+
Added only: no export removed, no exported constant changed, and a form that passes none of the new props is unchanged.
|
|
2178
|
+
|
|
2179
|
+
- **`FormButtons` `submitTone="danger"`** (and so `SubmitForm`, `ApiForm`): the submit button filled with
|
|
2180
|
+
`--nx-danger-solid` and `--nx-danger-text`, the `Button` `danger` variant every solid danger fill uses, for a save
|
|
2181
|
+
that removes something. Default `primary`, as before. New type **`FormSubmitTone`**.
|
|
2182
|
+
- **`downloadText(name, text, type)`** (`nexus-shared/client`): the file a table's CSV export hands over, for any text
|
|
2183
|
+
made in the browser - a Blob behind a link that is clicked and taken out, its address let go ten seconds later, the
|
|
2184
|
+
text after a byte order mark so Excel and Windows editors read UTF-8.
|
|
2185
|
+
- **`SubmitForm` `aria-describedby`** (and so `ApiForm`): passed to the `<form>` beside `aria-label` and
|
|
2186
|
+
`aria-labelledby`, so a step that puts the focus straight into its password box can point at the line saying what
|
|
2187
|
+
goes.
|
|
2188
|
+
|
|
2189
|
+
### A record nobody may change here, and a switched-off item's reason readable
|
|
2190
|
+
|
|
2191
|
+
2026-10-12, for the Builder's locked system models (the auth tables a person may only read). Added only: no export
|
|
2192
|
+
removed, no exported constant changed, and a `Crud` without the new prop behaves as before.
|
|
2193
|
+
|
|
2194
|
+
- **`Crud` `rowLock`** (`(row) => string | null | undefined`): why a record may not be changed here. Its Edit, Flag,
|
|
2195
|
+
Move to trash, Recover, Delete permanently and Move back to trash are listed **switched off** with that sentence under
|
|
2196
|
+
each (none on the row as an icon), its details offer no Edit, the handle's `edit(row)` opens its details instead, and
|
|
2197
|
+
a selection holding it has the mass actions that change records switched off. View, History, Pin and Favourite stay.
|
|
2198
|
+
It fills the gap pages worked around with row actions of their own ("`Crud` cannot switch a built-in action off for
|
|
2199
|
+
one row"); a page's own `rowActions` are still the page's to switch off.
|
|
2200
|
+
- **Menus** (`Nexus.Menu.css`): a switched-off item fades its icon, check, label and shortcut to half, no longer the
|
|
2201
|
+
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
|
|
2202
|
+
theme. `.nx-menu` sets `white-space: normal`, so a row's menu inside a table's actions cell (which does not wrap)
|
|
2203
|
+
wraps its items' second lines instead of cutting them. An app that undid either rule for its own menus can drop that.
|
|
2204
|
+
|
|
2205
|
+
### A table's spreadsheet menu asks the permissions
|
|
2206
|
+
|
|
2207
|
+
A review of 2026-10-10 found an Admin offered a model's whole spreadsheet menu on nexus.web, every item of it refused
|
|
2208
|
+
by the backend (the sheets are SuperAdmin's). No export or constant changed. What can move for an app on 3.2.0:
|
|
2209
|
+
|
|
2210
|
+
- **`DataTable` `excel`** (and so `Crud` `table={{ excel }}`): each item is offered only when the app's permissions
|
|
2211
|
+
(`canCall`) allow every endpoint it calls - Download template the settings and the template, Import from a
|
|
2212
|
+
spreadsheet and Bulk insert the settings, Verify and Import, Export to Excel the settings and the export - and the
|
|
2213
|
+
menu follows a change of rights. An app with no permissions registered allows everything, so it sees no change; one
|
|
2214
|
+
with `NexusPermissions` now needs its users' access actions to hold the `{Model}/Excel/...` actions for the menu to show.
|
|
2215
|
+
- **`downloadExcelTemplate` and `exportExcel`** stop at a failed settings call and answer it (`ok: false`, its status
|
|
2216
|
+
and message): the person sees that call's one toast. Before, the template went on to a second refused call and a
|
|
2217
|
+
second toast, and the export went on without knowing its row limit.
|
|
2218
|
+
|
|
2219
|
+
### The list's Add button says "Add New"
|
|
2220
|
+
|
|
2221
|
+
The owner, 2026-10-10: *"the CRUD has Add Region, Add Country and ... I want just fixed name Add New ... no dynamic"*.
|
|
2222
|
+
A `Crud`'s Add button above its list reads **Add New** on every page, whatever its `subject`; it was `Add <subject>`.
|
|
2223
|
+
The form it opens keeps its subject in the title and on its submit button ("Add region"), and Edit still says Save.
|
|
2224
|
+
No export or constant changed. An app's test that finds the list's button by its old name ("Add region") finds
|
|
2225
|
+
nothing now; one that finds the form's submit button is unaffected.
|
|
2226
|
+
|
|
2227
|
+
### A tree that fills
|
|
2228
|
+
|
|
2229
|
+
The owner, 2026-10-10, on nexus.web's administrative region tree: *"make full height here similar datatable both side
|
|
2230
|
+
panel, make sure responsive"*. Added only; a tree without the new prop is unchanged.
|
|
2231
|
+
|
|
2232
|
+
- **`TreeView` `fill`**: the tree takes the height it is given, as `DataTable` `fill` does - the toolbar on top, the
|
|
2233
|
+
rows scrolling inside under it - and `height` and `maxHeight` are ignored while it is on. New class
|
|
2234
|
+
`nx-tree-field--fill` on the field. Give it a cell with a height: a side of nexus-admin's `AdminSplit` on an
|
|
2235
|
+
`AdminPage` with `fill` (both new in nexus-admin), or a parent of your own.
|
|
2236
|
+
|
|
2237
|
+
### CRUD inside a tree, and changes written into a tree's kept pages
|
|
2238
|
+
|
|
2239
|
+
The owner, 2026-10-10, on the same page: *"allow CRUD ... inside the treeview"*. Added only: no export removed, no
|
|
2240
|
+
exported constant changed, and a page that passes none of the new params behaves as before - with one call fewer where
|
|
2241
|
+
a picker's saved value is in the list it keeps (see `crudPicker` below).
|
|
2242
|
+
|
|
2243
|
+
- **`CrudTree`** (`nexus-shared/client`): a controller's records as a lazy tree with Add New (under a row, or from the
|
|
2244
|
+
toolbar), Edit, Move to trash, Recover, Delete permanently, Move back to trash and History - through the **same**
|
|
2245
|
+
Add/Edit popup and calls `Crud` uses. Row icons for Add New, Edit and Move to trash (Insert, F2 and Delete on a
|
|
2246
|
+
focused row, all in its context menu); the picked record's fields and buttons go to a panel the page draws
|
|
2247
|
+
(`details`). Every change is written into the tree and the pages it keeps: a record added is placed where its
|
|
2248
|
+
siblings' order puts it, an edit is written in place or moved to its new parent, one in the trash leaves, and the
|
|
2249
|
+
parents' counts and chevrons follow. A Create still reads its record back once (as `Crud` does), and an edit that
|
|
2250
|
+
moved a record reads it once for the names joined from its new parent. New class `nx-crud-tree__actions`.
|
|
2251
|
+
- **`CrudTreePlacement`** (`cacheKey`, `rules`, `itemOf`, `parentOf`, `recordOf`, `childValues`): where a controller's
|
|
2252
|
+
records sit in a tree, given to `CrudTree` and to **`Crud` `trees`** - a data table on another page then writes its
|
|
2253
|
+
changes into that tree's kept pages too (an import drops them instead). `crudTreeChanges` turns a `CrudChange` into
|
|
2254
|
+
tree changes; **`CrudChange.before`** carries the records as they were.
|
|
2255
|
+
- **Changes into a tree's kept pages**: `writeTreeChanges(cacheKey, changes, rules)` patches `treePages`
|
|
2256
|
+
(`patchTreePages`) and every tree on screen with that `cacheKey` (`subscribeTreeChanges`); `TreeItemChange`
|
|
2257
|
+
(`put` / `remove`) and `TreeChangeRules` (`valueKey`, `hasChildrenKey`, `compare`). `TreeStore.placeItem` and
|
|
2258
|
+
`removeItem` keep a lazy parent's `loaded`/`total` and its own count right, so the next page skips the right number.
|
|
2259
|
+
- **`TreeView`**: `rowActions` (`TreeRowAction`: `TableRowAction` and a `shortcut` of Insert, F2 or Delete), `add` (a
|
|
2260
|
+
toolbar button of the page's own), and on the handle `apply(changes, rules)`, `getItem(value)`, and a `reveal` that
|
|
2261
|
+
waits for an item still loading. Focus follows a row that leaves or moves, released before the row leaves the page.
|
|
2262
|
+
- `RequestCache.entries(prefix)` and `set(key, value, at)` (a value written over a kept one keeps its age);
|
|
2263
|
+
`TableJoins.remember(source, records)` and `peekList(name)`.
|
|
2264
|
+
- **`crudPicker`'s `resolveOptions` names a saved value from the list it keeps** before asking `findListing`, so a form
|
|
2265
|
+
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
|
|
2266
|
+
offer - a parent since trashed - is still looked up and shown tagged.
|
|
2267
|
+
- `Crud`'s form popup, its call for an action on one record and its action words now come from one internal module
|
|
2268
|
+
shared with `CrudTree` (`Components/Forms/CrudActions.tsx`): no change to what `Crud` shows or sends.
|
|
2269
|
+
|
|
2270
|
+
### A tree that fixes where a record sits
|
|
2271
|
+
|
|
2272
|
+
The owner, 2026-10-10, on the same tree: *"I have select the row to add, auto add inside this. Not allow to switch
|
|
2273
|
+
parent ... both should be disabled, not allow to change those values."* Added only: no export removed, no exported
|
|
2274
|
+
constant changed, and a tree whose placement names no `placeKeys` behaves as before - except that a `CrudTree` now
|
|
2275
|
+
selects the row it shows (below).
|
|
2276
|
+
|
|
2277
|
+
- **`CrudTreePlacement.placeKeys`**: the fields that say where a record sits (its parent, its country). Given, `CrudTree`
|
|
2278
|
+
fixes them in every form it opens - an Add inside a row takes them from `childValues` (a field it leaves out is fixed
|
|
2279
|
+
empty), an Edit keeps the record's own - and every Add goes inside a row: the row's +, Insert, the context menu, the
|
|
2280
|
+
panel's Add New, `handle.create(parent)`, and the toolbar's Add New, which now adds inside the selected row - the one
|
|
2281
|
+
the panel shows - and, while there is none (or the selected record is in the trash), says why on a press (a click, a
|
|
2282
|
+
tap, Enter); it used to open with nothing filled. A record then moves only where those fields are offered, such as a
|
|
2283
|
+
data table's form. Fixed is read-only, the design's "cannot change" look: the lock, and a note on any try ("Added
|
|
2284
|
+
inside Bagmati: pick another row in the tree to add elsewhere.", "Its place cannot be changed in the tree."); the
|
|
2285
|
+
values stay and are sent as before, and a form's own `onFieldChange` never rewrites them (**`lockedFieldChange`**).
|
|
2286
|
+
- **The selected row**: a click or Enter on a `CrudTree` row selects it - tinted, and staying so while focus, the arrow
|
|
2287
|
+
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
|
|
2288
|
+
country above its regions) empties the panel and opens. So the toolbar's Add New and the panel's never disagree.
|
|
2289
|
+
- **`lockInputs(inputs, locked, values)`**: inputs fixed for one opening of a form (input name -> why), copied with their
|
|
2290
|
+
own property descriptors so a getter param keeps working; a required input with no value stays free. The Add and
|
|
2291
|
+
Edit popup `Crud` and `CrudTree` share (internal) takes them as `locked`. An Add with fixed inputs is a popup of its
|
|
2292
|
+
own per set of fixed values (**`crudFormPlace`**, added to the popup's and the draft's keys) and says where in its
|
|
2293
|
+
title bar ("Inside Bagmati"): an Add started inside one row never comes back inside another, and asking for an Add
|
|
2294
|
+
replaces every untouched Add of the controller, so only forms with work in them wait on the dock. A record keeps **one**
|
|
2295
|
+
Edit popup wherever it is asked for: with the same inputs fixed it comes back; with others (a tree's fixed form, a
|
|
2296
|
+
data table's free one) an untouched one is replaced, and one with changes comes back as it is, with a note saying
|
|
2297
|
+
why - an older form's save would undo what a newer one saved. Drafts stay one per set of fixed values, so a draft
|
|
2298
|
+
written before a record moved is not brought back over its new place.
|
|
2299
|
+
- **`TreeView`**: `selectedValue` - the row the page picked, marked `aria-selected` and tinted (new class
|
|
2300
|
+
`nx-tree__row--selected`); moving focus, a chevron, opening or closing a row leave it where it is. `add.disabled` with
|
|
2301
|
+
`add.description`: the toolbar button stays in the Tab order (`aria-disabled`) and a press shows the description in a
|
|
2302
|
+
note under it; the description is also its tooltip and what a screen reader hears after its name.
|
|
2303
|
+
- **`Button`** with `aria-disabled="true"` looks disabled, stays focusable, and draws a stronger ring through the dimming.
|
|
2304
|
+
- **The dock** shows a popup's `description` under its title (`DockedWindow.description`, new classes `nx-dock__text`
|
|
2305
|
+
and `nx-dock__description`), in the bar's tooltip and in its buttons' names: two Edit forms, or two Adds, read apart.
|
|
2306
|
+
- 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
|
|
2307
|
+
light themes), since it is all the field shows ("None: directly under the country").
|
|
2308
|
+
- The details panel's Add New says "Add New: inside {name}" in its tooltip (it said "under").
|
|
2309
|
+
|
|
2310
|
+
### A save is never read back
|
|
2311
|
+
|
|
2312
|
+
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
|
|
2313
|
+
details during the creating time, so use this; the creationTime, recordStatus we will provide fake or just right now."*
|
|
2314
|
+
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
|
|
2315
|
+
built from the answer, the form and what the page holds. This replaces the read backs described under "CRUD inside a
|
|
2316
|
+
tree" above (a Create read once, a move read once). Added only: no export removed, no exported constant changed. What
|
|
2317
|
+
can move for an app on 3.2.0:
|
|
2318
|
+
|
|
2319
|
+
- **A `crudColumns.dbJoin` column without `from`** - the form 3.2.0 documented - still has its new record read back once
|
|
2320
|
+
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
|
|
2321
|
+
reads anything. Give the column `from` (the field of the joined record's key) and the read goes.
|
|
2322
|
+
- `TableColumn.dbJoin` is set by every `crudColumns.dbJoin` column, its `from` optional.
|
|
2323
|
+
|
|
2324
|
+
The changes:
|
|
2325
|
+
|
|
2326
|
+
- **`CrudBackend.savedRow(action, record, before)`**: the fields only the server keeps, written as the backend writes
|
|
2327
|
+
them. The default leaves the record as it is, and a CRUD still reads a record back once when its list cannot be told
|
|
2328
|
+
from it (another app's backend). **`NexusCrudBackend`**: a new record lacking them is Active (`NEXUS_ACTIVE_STATUS`),
|
|
2329
|
+
has no marks, was added now (`creationTime`, a UTC ISO string) and never changed; an edited one takes the status, the
|
|
2330
|
+
marks and the time added from the record the page holds now (`before`) - over its form's copies, which a flag or a
|
|
2331
|
+
mark made while the form was open has left behind - and was changed now.
|
|
2332
|
+
- **Joined names with no call**: `crudColumns.dbJoin` takes `from`, the field holding the joined record's key (kept as
|
|
2333
|
+
**`TableColumn.dbJoin`**), and after a save **`crudJoinedNames`** writes each such column again. A key kept keeps its
|
|
2334
|
+
name and status, an emptied one empties both, and a new or changed one takes the name (the dropdown's `labelKey`) and
|
|
2335
|
+
the `recordStatus` of the record behind it. That record is the one the user picked (the form now keeps a dropdown's
|
|
2336
|
+
item: **`FormStore.pickedItem(name)`**, `input(name, value, stamp, item)`), else one the page holds (**`Dropdown`
|
|
2337
|
+
`peekOption(key)`**: `crudPicker` gives one from its kept list and the FindListing cache, and **`peekFound(findListing,
|
|
2338
|
+
key)`** serves a picker of a page's own), else, in a tree, the parent the record sits under, else one the page held
|
|
2339
|
+
and has marked old (`peekOption(key, true)`). A key nothing names is left unnamed - an edit drops the old key's name -
|
|
2340
|
+
and **`crudJoinsMissing`** then has the record read back once. A data table's Edit that moves a record now shows the
|
|
2341
|
+
new parent's name; it showed the old one until the page was read again.
|
|
2342
|
+
- **Counts**: with `trees`, **`crudSavedCount`** gives a new record its placement's count (0) and an edited one the count
|
|
2343
|
+
the page holds now (a child added since its form opened counts), and **`crudParentCounts`** moves the counts on the
|
|
2344
|
+
parents' own rows that a data table holds - one more under the new parent, one fewer under the old - on a create, a
|
|
2345
|
+
move, the trash and back, as the tree's parents already did; each parent row once, however many placements the page
|
|
2346
|
+
gives. Both write what `CrudTreePlacement.recordOf` gives over the row, so `recordOf` carries the item's count back.
|
|
2347
|
+
- **In a tree**, a record added under a parent never opened is counted there and shown in the panel; the parent stays
|
|
2348
|
+
closed, since opening it would read its page.
|
|
2349
|
+
- `Dropdown` `peekOption` on its own: a value the page already holds is named from the first draw, with no
|
|
2350
|
+
`resolveOptions` call - while it is fresh: a record marked old since (a save on its controller, a Reload) is asked for
|
|
2351
|
+
again, so the box shows it as it is now. **`TableJoins.peekFresh(name, key)`**, and `peekFound`'s `aged`.
|
|
2352
|
+
|
|
2353
|
+
### New exports no section names
|
|
2354
|
+
|
|
2355
|
+
The types and small helpers that come with the parts the sections describe, so that every new name is listed once:
|
|
2356
|
+
|
|
2357
|
+
- `nexus-shared`: the file types `FileOwner`, `FileCopy`, `StoredFile`, `FileUpload`, `FileCallOptions`,
|
|
2358
|
+
`PictureType`, `PICTURE_TYPES`, `isPictureType`, `ImageViewerItem`, `PictureParams`; the file helpers `fileKindName`,
|
|
2359
|
+
`fileOwnerKey`, `acceptOf`, `readStoredFile`, `readOwnerFiles`, `FileApiClient`, `NexusDriveAdapterOptions`,
|
|
2360
|
+
`PrimariesReader`, `PrimaryPicturesOptions`; `QrCodeImageProps`, `MoneyFormatOptions`, `CrudJoinedRecord`,
|
|
2361
|
+
`NarrowPickWords`, `CrudNarrowPickerSource`, `CrudDetailsProfile`, `CrudDetailsTab`, `TableDbJoin`.
|
|
2362
|
+
- `nexus-shared/client`: `getFileAdapter`, `toastFileFailure`, `usePrimaryPicture`, the props types
|
|
2363
|
+
`PictureInputProps`, `OwnerPictureProps`, `PictureGalleryProps`, `OpenPictureGalleryOptions`, `PictureColumnParams`,
|
|
2364
|
+
and the tree CRUD's `CrudTreeProps`, `CrudTreeDetails`, `CrudTreeHandle`.
|
|
2365
|
+
|
|
7
2366
|
## 3.2.0
|
|
8
2367
|
|
|
9
2368
|
**New features, no code break.** Every export of `3.1.0` is still exported (99 are new: 72 from `nexus-shared`, 16
|