nexus-shared 4.0.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 +795 -0
- package/README.md +220 -14
- package/dist/Client.Index.d.ts +13 -0
- package/dist/Client.Index.js +13 -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/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +5 -1
- package/dist/Components/Forms/Crud.js +72 -24
- package/dist/Components/Forms/CrudActions.d.ts +5 -4
- package/dist/Components/Forms/CrudActions.js +42 -12
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudParts.d.ts +11 -4
- package/dist/Components/Forms/CrudParts.js +76 -10
- package/dist/Components/Forms/CrudRowForms.js +65 -5
- package/dist/Components/Forms/CrudTree.js +7 -7
- package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
- package/dist/Components/Forms/CrudWorkspace.js +19 -11
- package/dist/Components/Forms/Form.d.ts +3 -2
- package/dist/Components/Forms/Form.js +33 -26
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/SubmitForm.js +2 -15
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.js +4 -2
- package/dist/Components/Inputs/GroupForm.js +3 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +14 -1
- package/dist/Components/Inputs/RowsInput.js +78 -5
- package/dist/Components/Inputs/TabularForm.js +16 -3
- package/dist/Components/Inputs/TreeView.d.ts +24 -1
- package/dist/Components/Inputs/TreeView.js +84 -7
- package/dist/Components/Layouts/Popup.d.ts +22 -2
- package/dist/Components/Layouts/Popup.js +108 -6
- 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 +7 -5
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
- package/dist/Components/Viewers/DataTableParts.js +2 -2
- package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
- package/dist/Components/Viewers/OwnerFiles.js +142 -31
- 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/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/CrudColumns.d.ts +44 -6
- package/dist/Helpers/CrudColumns.js +63 -13
- package/dist/Helpers/CrudHelpers.d.ts +36 -1
- package/dist/Helpers/CrudHelpers.js +61 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +18 -1
- package/dist/Helpers/CrudRowForms.js +35 -2
- 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/FileAdapter.d.ts +82 -2
- package/dist/Helpers/FileAdapter.js +72 -1
- package/dist/Helpers/FormStore.d.ts +11 -1
- package/dist/Helpers/FormStore.js +66 -19
- package/dist/Helpers/InputConditions.d.ts +6 -0
- package/dist/Helpers/InputConditions.js +24 -0
- package/dist/Helpers/InputMemory.js +40 -30
- package/dist/Helpers/InputNarrowing.d.ts +38 -6
- package/dist/Helpers/InputNarrowing.js +102 -21
- 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 +17 -1
- package/dist/Helpers/NexusDriveAdapter.js +32 -3
- package/dist/Helpers/NumberHelpers.d.ts +61 -0
- package/dist/Helpers/NumberHelpers.js +124 -8
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/RowsStore.d.ts +24 -0
- package/dist/Helpers/RowsStore.js +79 -10
- package/dist/Helpers/TableHelpers.d.ts +15 -2
- package/dist/Helpers/TableHelpers.js +28 -1
- package/dist/Helpers/TableStore.d.ts +7 -3
- package/dist/Helpers/TableStore.js +9 -4
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +75 -6
- package/dist/Helpers/TreeStore.js +182 -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 +78 -5
- 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 +21 -0
- package/dist/Interfaces/FileInterfaces.js +2 -0
- package/dist/Interfaces/FormInterfaces.d.ts +78 -11
- package/dist/Interfaces/InputInterfaces.d.ts +12 -5
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +28 -2
- package/dist/Services/JoinSources.d.ts +47 -6
- package/dist/Services/JoinSources.js +68 -33
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Shared.Index.d.ts +12 -0
- package/dist/Shared.Index.js +12 -0
- package/package.json +7 -1
- package/src/Styles/Nexus.Button.css +29 -0
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Crud.css +119 -28
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +27 -0
- package/src/Styles/Nexus.Index.css +3 -0
- package/src/Styles/Nexus.Input.css +31 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Popup.css +58 -0
- package/src/Styles/Nexus.Table.css +1458 -1449
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Tree.css +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,801 @@ 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
|
+
|
|
7
802
|
## 4.0.0
|
|
8
803
|
|
|
9
804
|
**Breaking: an input's `type` is a number, a member of `EInputTypes`, and a string is refused.** Everything else in
|