nexus-shared 3.0.0 → 3.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +446 -262
  2. package/LICENSE +21 -21
  3. package/README.md +1084 -1000
  4. package/dist/Client.Index.d.ts +3 -0
  5. package/dist/Client.Index.js +3 -0
  6. package/dist/Components/Chats/ChatParts.d.ts +2 -2
  7. package/dist/Components/Chats/ChatParts.js +3 -7
  8. package/dist/Components/Chats/ChatThread.js +3 -3
  9. package/dist/Components/Documents/Button.d.ts +6 -1
  10. package/dist/Components/Documents/Button.js +2 -2
  11. package/dist/Components/Documents/Menu.js +21 -9
  12. package/dist/Components/Documents/SplitButton.d.ts +3 -1
  13. package/dist/Components/Documents/SplitButton.js +7 -5
  14. package/dist/Components/Documents/TabButtons.d.ts +5 -2
  15. package/dist/Components/Documents/TabButtons.js +2 -2
  16. package/dist/Components/Forms/ApiForm.d.ts +1 -1
  17. package/dist/Components/Forms/ApiForm.js +1 -1
  18. package/dist/Components/Forms/Crud.js +177 -58
  19. package/dist/Components/Forms/CrudParts.d.ts +5 -1
  20. package/dist/Components/Forms/CrudParts.js +37 -5
  21. package/dist/Components/Forms/ExcelImport.d.ts +23 -7
  22. package/dist/Components/Forms/ExcelImport.js +189 -60
  23. package/dist/Components/Forms/Form.js +5 -13
  24. package/dist/Components/Forms/SubmitForm.d.ts +11 -2
  25. package/dist/Components/Forms/SubmitForm.js +23 -12
  26. package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
  27. package/dist/Components/Inputs/DropdownParts.js +4 -6
  28. package/dist/Components/Inputs/NumberBox.js +24 -2
  29. package/dist/Components/Inputs/RowsInput.js +2 -2
  30. package/dist/Components/Inputs/Slider.js +4 -1
  31. package/dist/Components/Inputs/TabularForm.js +42 -10
  32. package/dist/Components/Inputs/TimePanel.js +4 -3
  33. package/dist/Components/Layouts/Popup.js +1 -4
  34. package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
  35. package/dist/Components/Layouts/ThemePicker.js +77 -6
  36. package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
  37. package/dist/Components/Layouts/ThemeScript.js +4 -2
  38. package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
  39. package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
  40. package/dist/Components/Viewers/Avatar.d.ts +21 -0
  41. package/dist/Components/Viewers/Avatar.js +21 -0
  42. package/dist/Components/Viewers/DataTable.js +192 -44
  43. package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
  44. package/dist/Components/Viewers/DataTableColumns.js +25 -68
  45. package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
  46. package/dist/Components/Viewers/DataTableFilters.js +68 -0
  47. package/dist/Components/Viewers/DataTableParts.d.ts +2 -2
  48. package/dist/Components/Viewers/DataTableParts.js +3 -3
  49. package/dist/Helpers/AnimationHelpers.d.ts +11 -0
  50. package/dist/Helpers/AnimationHelpers.js +20 -2
  51. package/dist/Helpers/CrudBackend.d.ts +7 -4
  52. package/dist/Helpers/CrudBackend.js +4 -2
  53. package/dist/Helpers/ExcelBackend.d.ts +3 -3
  54. package/dist/Helpers/ExcelBackend.js +8 -2
  55. package/dist/Helpers/ExcelHelpers.d.ts +36 -8
  56. package/dist/Helpers/ExcelHelpers.js +55 -13
  57. package/dist/Helpers/FormStore.d.ts +9 -5
  58. package/dist/Helpers/FormStore.js +25 -5
  59. package/dist/Helpers/MessageBuilder.js +1 -0
  60. package/dist/Helpers/NumberHelpers.d.ts +7 -1
  61. package/dist/Helpers/NumberHelpers.js +41 -3
  62. package/dist/Helpers/PopoverHelpers.d.ts +17 -5
  63. package/dist/Helpers/PopoverHelpers.js +52 -17
  64. package/dist/Helpers/PopoverHooks.d.ts +24 -0
  65. package/dist/Helpers/PopoverHooks.js +62 -0
  66. package/dist/Helpers/RowsStore.d.ts +7 -2
  67. package/dist/Helpers/RowsStore.js +15 -3
  68. package/dist/Helpers/TableExport.js +12 -12
  69. package/dist/Helpers/TableFilters.d.ts +25 -0
  70. package/dist/Helpers/TableFilters.js +162 -0
  71. package/dist/Helpers/TableHelpers.d.ts +6 -3
  72. package/dist/Helpers/TableHelpers.js +8 -4
  73. package/dist/Helpers/TableStore.d.ts +11 -3
  74. package/dist/Helpers/TableStore.js +16 -5
  75. package/dist/Helpers/ThemeHelpers.d.ts +21 -4
  76. package/dist/Helpers/ThemeHelpers.js +46 -9
  77. package/dist/Helpers/UserPreferences.d.ts +14 -0
  78. package/dist/Helpers/UserPreferences.js +99 -0
  79. package/dist/Interfaces/CrudInterfaces.d.ts +20 -1
  80. package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
  81. package/dist/Interfaces/FormInterfaces.d.ts +33 -5
  82. package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
  83. package/dist/Interfaces/PreferenceInterfaces.js +28 -0
  84. package/dist/Interfaces/TableInterfaces.d.ts +59 -12
  85. package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
  86. package/dist/Interfaces/ThemeInterfaces.js +15 -0
  87. package/dist/Services/ExcelApi.d.ts +3 -1
  88. package/dist/Services/ExcelApi.js +1 -1
  89. package/dist/Services/ThemeService.d.ts +18 -2
  90. package/dist/Services/ThemeService.js +80 -16
  91. package/dist/Shared.Index.d.ts +2 -0
  92. package/dist/Shared.Index.js +2 -0
  93. package/package.json +85 -85
  94. package/src/Styles/Nexus.Avatar.css +79 -0
  95. package/src/Styles/Nexus.Base.css +117 -70
  96. package/src/Styles/Nexus.Button.css +180 -153
  97. package/src/Styles/Nexus.Calendar.css +273 -275
  98. package/src/Styles/Nexus.Chat.css +1443 -1489
  99. package/src/Styles/Nexus.Choice.css +343 -345
  100. package/src/Styles/Nexus.Crud.css +168 -161
  101. package/src/Styles/Nexus.DateTime.css +81 -81
  102. package/src/Styles/Nexus.Dropdown.css +725 -774
  103. package/src/Styles/Nexus.Editor.css +295 -297
  104. package/src/Styles/Nexus.Excel.css +389 -370
  105. package/src/Styles/Nexus.Form.css +279 -321
  106. package/src/Styles/Nexus.Index.css +37 -36
  107. package/src/Styles/Nexus.Input.css +602 -637
  108. package/src/Styles/Nexus.Markdown.css +237 -265
  109. package/src/Styles/Nexus.Menu.css +237 -259
  110. package/src/Styles/Nexus.Popup.css +724 -731
  111. package/src/Styles/Nexus.RichText.css +2 -32
  112. package/src/Styles/Nexus.Rows.css +637 -596
  113. package/src/Styles/Nexus.Slider.css +276 -278
  114. package/src/Styles/Nexus.Tab.Buttons.css +432 -419
  115. package/src/Styles/Nexus.Table.css +1386 -1296
  116. package/src/Styles/Nexus.Theme.Picker.css +286 -232
  117. package/src/Styles/Nexus.Themes.css +194 -194
  118. package/src/Styles/Nexus.Time.css +144 -147
  119. package/src/Styles/Nexus.Toast.css +263 -265
  120. package/src/Styles/Nexus.Tokens.css +89 -72
  121. package/src/Styles/Nexus.Tree.css +431 -435
package/CHANGELOG.md CHANGED
@@ -1,262 +1,446 @@
1
- # Changelog
2
-
3
- Notable changes to `nexus-shared`. The **values** of exported constants count as API here: several of them are the
4
- Nexus backend's own numbers, and a value that differs from the backend by one does not fail loudly — it silently
5
- means the wrong thing on every record and every permission check.
6
-
7
- ## 3.0.0
8
-
9
- **Breaking. This changes the value of exported constants, and `2.0.0` is on npm with the old ones.** Publishing it
10
- as a patch or a minor would hand `^2.0.0` consumers different numbers under a compatible-looking range, which is the
11
- worst way to ship this, so it takes a major. The owner chose the number on 2026-10-02, together with `nexus-public`
12
- and `nexus-admin`, which both depend on this package and so move to `3.0.0` with it; their `nexus-shared` range is
13
- now `^3.0.0`, because a bump of this one alone would leave them pulling the old, wrong constants.
14
-
15
- **`nexus-icons` must be installed at `1.1.0` or newer**, and the dependency range here says so. The chat components
16
- added in this version use `iconChecks`, `iconCornerUpLeft`, `iconMessageCircle`, `iconPaperclip`, `iconSend` and
17
- `iconUserPlus`, none of which exist in `nexus-icons@1.0.0`: with the old range a fresh install resolved to `1.0.0`
18
- and the build failed on six missing exports.
19
-
20
- ### `NexusModule` — `Chat` added, and every module after it moved up one bit
21
-
22
- The backend's `EModules` gained `Chat` at bit 6, and everything after it shifted. `NexusModule` follows it exactly.
23
- A permission is a bitmask of these flags, so a package that kept the old numbers would grant and deny the wrong
24
- modules without any error to show for it.
25
-
26
- | Module | Was | Now |
27
- |---|---|---|
28
- | `None` | 0 | 0 |
29
- | `Migrations` | 1 | 1 |
30
- | `Configuration` | 2 | 2 |
31
- | `Identity` | 4 | 4 |
32
- | `Enterprise` | 8 | 8 |
33
- | `Drive` | 16 | 16 |
34
- | `Message` | 32 | 32 |
35
- | **`Chat`** | *(did not exist)* | **64** |
36
- | `Accounting` | 64 | **128** |
37
- | `Report` | 128 | **256** |
38
- | `Requisition` | 256 | **512** |
39
- | `Tenant` | 512 | **1024** |
40
- | `Subscription` | 1024 | **2048** |
41
- | `Reservation` | 2048 | **4096** |
42
- | `School` | 4096 | **8192** |
43
- | `Pharmacy` | 8192 | **16384** |
44
- | `Staffing` | 16384 | **32768** |
45
- | `Portfolio` | 32768 | **65536** |
46
-
47
- `NEXUS_MODULES` gained the matching entry (`key: "chat"`, `label: "Chat"`, `env: "API_CHAT"`), so `Chat` can be
48
- looked up, proxied and named in messages like every other module.
49
-
50
- **What to check when you upgrade.** Code that uses the names (`NexusModule.Reservation`) needs nothing. Anything
51
- that stored, transmitted or hard-coded a **number** has to be revisited:
52
-
53
- - permission masks saved in a database, a cache, a token claim or a settings file;
54
- - proxy URLs written with a number in place of a key (`/api/app/2048/…` is now `/api/app/4096/…`, and `reservation`
55
- by key has always worked and still does);
56
- - a backend that has not taken the `Chat` shift yet — both sides move together or neither does.
57
-
58
- ### `NEXUS_FLAGGED_STATUS` — 15 → 25
59
-
60
- The owner settled the record-status bands, and `ERecordStatus.Flagged` is seeded at **25**. `NexusCrudBackend`
61
- decides whether a row is flagged by comparing `recordStatus` to this constant, so a package on 15 while the backend
62
- writes 25 shows the wrong flag on every row, and flagging one writes a status the backend does not recognise.
63
-
64
- The bands, for anything that reasons about them:
65
-
66
- | Band | Range |
67
- |---|---|
68
- | Active | 0–39 |
69
- | — Active account | 0–29 |
70
- | — Flag | **25–29** |
71
- | — Pending, unverified, in review, approval required | 30–39 |
72
- | Trash | 40–59 |
73
- | Delete | 60 and above |
74
-
75
- `NEXUS_ACTIVE_STATUS` stays 0, and the `recordStatus < 40` reading of "a live record" that the rest of the code
76
- already assumes stays right.
77
-
78
- Note that **the flag is a range, 25–29, and this release still checks a single value.** Only the seeded member at 25
79
- has a meaning today, and the bands are about to become configurable per project, so a range-aware check belongs with
80
- that work rather than here. Do not read the single value as the final answer.
81
-
82
- ### Removed: the archive
83
-
84
- The owner removed the archive on 2026-09-30: a record is live, in the trash, or deleted, and the way back from
85
- deleted lands it in the trash. The second put-away list and its second recovery path were the only reason the CRUD
86
- had both Restore and Recover, and the bands close up behind it — **Trash is 40–59 and Delete 60 and above**, so
87
- nothing stored keeps a meaning it no longer has. `recordStatus < 40` still reads "a live record".
88
-
89
- What goes, all of it breaking for anything that named it:
90
-
91
- - `CrudMode` loses `"archive"`, so a CRUD shows All, Flagged, Trash and Deleted.
92
- - `CrudRowActionName` and `CrudMassActionName` lose `"archive"` and `"restore"`. Recover, the trash's own way back,
93
- is untouched: a CRUD that offered both now offers one.
94
- - `CrudEndpoints` loses `archive` and `restore`; `apiController` stops naming `Archive`, `Restore`, `MassArchive`,
95
- `MassRestore`, `ArchiveListing` and `ArchivePagination`, and `lists`/`pages` lose their `archive` key.
96
- - `MessageActionName` loses `"archive"`, and the message builder its words and its "It can be restored from the
97
- archive." confirmation detail. `"restore"` stays in the vocabulary: it is a plain verb, not the archive's.
98
-
99
- **What to check when you upgrade.** A CRUD that listed `"archive"` in `modes` or `actions` no longer compiles,
100
- which is the point — the endpoint behind it is gone from the backend too. A backend that still answers
101
- `ArchiveListing` is not asked for it any more.
102
-
103
- ### Added: the chat
104
-
105
- A discussion on any record — a leave request the manager and a co-worker talk through, a purchase order, an
106
- applicant, a tender — behind one icon that says whether anything is waiting. Nothing that does not use it changes.
107
-
108
- - A conversation is named by three values, `ChatScope`: the **domain** (which kind of record), the **parent id** (the
109
- record) and the **user id** (who is reading). The domain is the app's own enum, given either as a template
110
- (`<Chat<EDomainTypes> …>`) or once in `ChatRegister`, the same way `ApiRegister` names the module enum. Without one
111
- it is a plain number.
112
- - **No domain numbers are defined here.** They belong to the backend's `EDomainTypes`, and the warning at the top of
113
- this file applies to them in full. `defineChatDomains` only gives an app's domains their names on screen.
114
- - `ChatButton` is an icon in three looks: quiet with no messages, the accent on its tint once there are messages, and
115
- the accent filled with an unread badge once some are unread. One size in all three, with the badge over the corner,
116
- so a column of them in a table does not move as the counts arrive.
117
- - `ChatSummaryCache` fills every icon that mounted in the same moment with **one call per domain**. A list whose rows
118
- already carry their counts passes `summary` and makes no call at all.
119
- - `Chat` is the icon with the conversation in a popup; `ChatPanel` is the conversation on its own, for a record's own
120
- page. Messages are **Markdown**: written in `MarkdownEditor` and drawn by `MarkdownView`. Enter sends and
121
- Shift+Enter starts a line, except in a list or a code fence, where the editor keeps Enter; `sendKey="modifier"`
122
- moves sending to Ctrl+Enter.
123
- - One mark where the time sits says how far a message of your own has got: a spinner while sending, a tick once the
124
- server has it, two once `deliveredTo` holds somebody, two in the accent once `readBy` does.
125
- - Where the data comes from is one interface, `ChatSource`. `apiChatSource` goes through the app's `ChatBackend`
126
- (`NexusChatBackend` by default); `localChatSource` keeps a conversation in memory for a demo or a test.
127
- - **The Nexus endpoints are the shape the Chat module is expected to take, not an API that answers today.**
128
- `NexusModule.Chat` is a number and a proxy key; the controller is not built. Each difference, when it lands, is one
129
- override on `NexusChatBackend`.
130
- - `check:themes` gained a rule: body text on the primary tint, which is a message bubble of your own. Every theme
131
- clears AAA except catppuccin light at 6.84, so it is held to the AA 4.5 that applies to body text.
132
- - **A record holds many conversations.** `ChatScope` names the record; each conversation in it is a
133
- `ChatConversation` with its own id and a type, `one-to-one` or `group`. `ChatPlaceSource` lists them and starts
134
- them, `useChatPlace` is the hook, and `apiChatPlace` is the API one. A panel given a `source` still shows that one
135
- conversation and no switch, so nothing that used the first round changes.
136
- - The panel's body has three views — the thread, the list of conversations, and New chat — and the bar moves between
137
- them. With several conversations the title is the switch; with one it is just the title.
138
- - Starting a chat never asks for the type: one person makes a one-to-one, more make a group, and a name makes a
139
- group either way. A one-to-one with somebody who already has one is expected to come back rather than be made
140
- twice; `localChatPlace` keeps that rule.
141
- - `@` at the start of a word offers the people in the conversation, narrows as it is typed, and owns Enter while it
142
- is open. A mention is plain `@Name`, or a Markdown link with `mentionHref`; either way the ids travel in the
143
- draft's `mentions`.
144
- - `ChatSnapshot` gained `loaded`: a store that has read nothing yet holds empty counts, and a panel must not write
145
- those over what a conversation list already knows.
146
-
147
- ### Added: several at once, `TabButtons multiple`
148
-
149
- `TabButtons` picks one option; with `multiple` it picks several, so a list of choices that must be seen all at once
150
- (the weekdays a rule runs on, the letters or symbols a pattern allows) needs no second control. Nothing that does
151
- not pass it changes.
152
-
153
- - `value` is an array and `onChange` hands back the chosen values **in option order**, whatever order they were
154
- clicked in. Values that match no option are ignored and never handed back.
155
- - The options are native checkboxes in a `role="group"`: Tab moves between them and Space toggles one. The group
156
- still needs `aria-label` or `aria-labelledby`.
157
- - There is no sliding indicator: each chosen option is raised with the indicator's own colors, border and shadow,
158
- and keeps the pointer, since a click takes it away again. An option that is not chosen sits flat on the track.
159
- - The options **wrap** onto more rows when they do not fit, each row as tall as a one-row track's options, and an
160
- option is never narrower than it is tall, so a row of single letters lines up. On a touch screen
161
- (`pointer: coarse`) every option is a medium control's height: 26 letters on a phone are 34px targets, not 22px.
162
- - Made and checked for the `nav` design (`/demo/tab-buttons`, "Several at once"); `design="list"` with `multiple`
163
- renders checkboxes too, but has not been checked yet.
164
-
165
- ### Added: inputs that follow others, `dependsOn`, and a row's own params, `cellParams`
166
-
167
- Two optional keys, added 2026-09-26 for the builder's class form, whose company model shows only when the class has a
168
- company or a branch and whose type arguments offer a key type in one row and a model in the next. Nothing that does
169
- not pass them changes.
170
-
171
- - **`dependsOn: ["hasCompany"]`** on an input's params (Form only). When a named input's value changes — typed, or
172
- set from code — this input draws again **without mounting again** (nothing typed in it is lost) and its params are
173
- read afresh, so getters for `hidden`, `readOnly`, `readOnlyMessage`, `label`, `hint` or `options` answer for the
174
- values as they are now. Its rule is asked again too, so a `required` a getter switched on counts at once. An input
175
- that names any is asked `hidden` each time it draws, so it can come and go; one that names none is asked once, when
176
- the form draws, as before. Name the input itself to follow its own typing.
177
- - **`cellParams(row, name, index)`** on `TabularForm` and `GroupForm`: the params of one cell that differ from its
178
- column's, such as a picker's `options` and `selectedOptions`. Asked as the row draws; return nothing for the
179
- column's own params, and the **same object** for the same answer (the merged params are kept per answer, so the cell
180
- does not draw again for nothing). Getters on either side stay getters. Rules still come from the column.
181
-
182
- ### Added: a generated column in a tabular or group form
183
-
184
- Two optional props on `TabularForm` and `GroupForm`, for a column whose values a row's **position** decides rather
185
- than the user — a bitwise enum's value, a line number, a running total. Nothing that does not pass them changes.
186
-
187
- - `deriveRows(rows)` returns one patch per row and runs after every change, including a row being added, removed,
188
- duplicated or **moved**. The patches are written as code writes, so only the cells whose value really changed
189
- mount again and focus stays on the row's own menu: a keyboard can move a row twice in a row.
190
- - `cellReadOnly(row, name, index)` is asked for every cell as it renders and returns the reason a cell is
191
- generated. The cell is locked and shows that reason as its read-only note. Because it is asked per render, a
192
- column can lock and unlock while the form around it changes, which params cannot do — they are fixed once a form
193
- opens.
194
-
195
- ### Added: rows nobody may change, `rowReadOnly`
196
-
197
- One optional prop on `TabularForm` and `GroupForm`, for rows the user did not write and may not change — the fields
198
- a class inherits from its parent, shown first in the same grid as its own. Nothing that does not pass it changes.
199
-
200
- - `rowReadOnly(row, index)` returns the reason (or `true`). Every cell of such a row is read-only with that reason
201
- as its note, the row's menu in a tabular form offers nothing (no insert, duplicate, move or remove), and the row
202
- below it cannot move up past it. Nothing reorders the rows: keep the locked ones first.
203
- - A locked row says nothing about its columns: the column-header lock is worked out from the other rows in view, so
204
- a grid of inherited rows and one own row does not put a lock on every header.
205
-
206
- ### Added: a panel beside a CRUD form, `form.aside`
207
-
208
- An Add or Edit popup can show a panel next to its form: a preview of what a save would produce, a summary, a help
209
- panel. It takes a node, or a function handed `{ form, action, row }` — `form` being that popup's own `FormStore`, so
210
- a panel follows what is typed through `subscribeValue()` and the form around it never re-renders for it.
211
-
212
- The two are columns above 1000px, each scrolling on its own so a long form cannot push the panel off the screen, and
213
- they stack below that with the **form first**, in the DOM as well as on the screen. `--nx-crud-aside-width` sets the
214
- panel's share (default `clamp(20rem, 38%, 44rem)`). Pair it with a wide `form.size` — `xl` or `full`; a panel in a
215
- 560px popup leaves the form nothing. A CRUD that passes no `aside` renders exactly what it did before, wrapper and
216
- all.
217
-
218
- ### `TabularForm` reworked: one row control, rows that do not move, one lock per column
219
-
220
- The owner called the tabular design bad, from a screenshot of an 18-row table with one generated column. Every fault
221
- was in this component rather than in the page that used it, so every fix is here. Nothing needs a new prop, and a
222
- table that passed nothing new looks different — deliberately.
223
-
224
- **Breaking, visually and for tests.** Two things a consumer may be reaching for have moved:
225
-
226
- - **The remove column is gone.** Remove was a column of unlabelled `×` buttons at the end, beside a row menu that
227
- already held Remove: two ways to act on one row, and a whole column spent on one of them. Now a row has one
228
- control, the menu beside its number, holding Insert above, Insert below, Duplicate, Move up, Move down and Remove.
229
- `removable` still decides whether Remove is offered. A test that clicked a button named `Remove <row>` opens the
230
- row's menu instead: `Row 2 actions`, then the `Remove` item. `.nx-tabular__end` and `.nx-tabular__remove` are gone
231
- from the DOM and the stylesheet; so is `--nx-tabular-end`, replaced by `--nx-tabular-menu` (the menu's slot width).
232
- - **The row number no longer opens the menu.** It used to *become* the menu on hover: the number faded out and dots
233
- faded in over it, so the numbering looked broken and the grid felt unstable as the pointer crossed it. The number
234
- now stays where it is, right-aligned, at every moment, and the menu button sits beside it in its own space,
235
- always present. The number column is that much wider.
236
-
237
- **Fixed, with no change to how a table is written:**
238
-
239
- - **Every row is one line high, whatever it holds.** The row's grid track was sized by its tallest cell, so a single
240
- `textarea` column of `rows: 2` made every cell of its row 20px taller than the row's own box — drawing the cells,
241
- their edges and their focus rings over the rows below. One row of content therefore looked taller than its
242
- neighbours, and a focus ring bled outside its cell. The row now has one track exactly its own height, cells keep
243
- what they hold inside their bounds, and a `textarea` fills its cell and scrolls inside it.
244
- - **No resize grips in cells.** A `textarea` in a cell took `resize: vertical` from the multi-line input rule, so
245
- every cell of such a column offered a grip that would break the alignment of its row. They are `resize: none` now.
246
- - **A filled cell and an empty one are drawn the same.** Same consequence as the above: only the cell with text was
247
- tall enough to show what the overflow was doing.
248
- - **`maxHeight` comes down to whole rows.** The scroller used to end wherever the number fell, cutting the last row
249
- in half, which reads as broken rather than as scrollable. The component measures its header, its frame and its
250
- real row height and caps itself just under what was asked for. A number too small for one row is left alone.
251
- - **A generated column says so once.** `cellReadOnly` put a lock in every cell of the column it locked — eighteen
252
- locks for one fact. The lock is now on the **column header**, with the reason as its title, and the cells keep the
253
- read-only tint and their own reason, which still shows under the table when one is used. A lock that really is per
254
- row (some rows locked, not all) leaves the header alone and is drawn per cell as before.
255
- - **Less chrome.** The frame is `--nx-border` rather than `--nx-border-strong`: the sheet draws every line it needs
256
- inside itself, and in a popup or a panel the strong edge was a box around a box around boxes.
257
-
258
- ## 2.0.0
259
-
260
- The rewrite: compiled `dist` with the entry points `.`, `/client`, `/server` and `/styles.css`, in place of 1.x's
261
- raw TypeScript from `src` with `.`, `/client` and `/interface`. A major version, so nobody on `^1.1.21` is moved
262
- into a different API.
1
+ # Changelog
2
+
3
+ Notable changes to `nexus-shared`. The **values** of exported constants count as API here: several of them are the
4
+ Nexus backend's own numbers, and a value that differs from the backend by one does not fail loudly — it silently
5
+ means the wrong thing on every record and every permission check.
6
+
7
+ ## 3.1.0
8
+
9
+ **New features, no code break.** Every export of `3.0.0` is still exported (49 are new: 33 from `nexus-shared`, 16 from
10
+ `nexus-shared/client`), and no exported constant changed its value, so `^3.0.0` takes it. The owner chose the number
11
+ on 2026-10-08. What can move for an app is its **stylesheet overrides**: these class names and custom properties were
12
+ renamed or dropped, and an override of the old name no longer applies.
13
+
14
+ | Was | Now |
15
+ |---|---|
16
+ | `.nx-columns__head`, `__title`, `__count`, `__foot` | `.nx-table-panel__head`, `__title`, `__count`, `__foot` |
17
+ | `.nx-table-panel__body` | `.nx-popover__scroll` |
18
+ | `.nx-chat-avatar`, `--nx-chat-avatar` | `.nx-avatar`, `--nx-avatar-size` |
19
+ | `.nx-table__frame`'s border | on `.nx-table` |
20
+ | `--nx-field-scrollbar`, `--nx-field-scrollbar-hover`, `--nx-dropdown-scrollbar`, `--nx-time-scrollbar` | gone: one thin bar everywhere |
21
+ | `.nx-form__skeleton*` | gone: a loading form shows its real inputs, disabled |
22
+ | `.nx-excel__refused` | gone: the import's report lists are `.nx-excel__problems` |
23
+
24
+ - **Bulk insert, and rows added to an import (the owner, 2026-10-07).** `ExcelImport` and `openExcelImport` take
25
+ `mode: "bulk"`: the import with no file, its new rows' editor opening with one empty row for the person to type
26
+ into, sent to the same `Excel/Import` with the same checks, a problem then naming its row ("Row 3 · Region name").
27
+ `DataTable` offers it as **Bulk insert** beside **Import from a spreadsheet**, and only beside an Add, as it is an Add
28
+ of many rows (`TableExcel.bulk: false` leaves it out). A sheet with a required list cannot be typed in - a list's key
29
+ is picked in the spreadsheet - and the panel says so; its file review takes no added rows either. Any other file's
30
+ new rows take Add, Insert and Duplicate: an added row is numbered after every row the file had
31
+ (`excelSheetRows(rows, columns, after)`), and one left empty is dropped, not refused. The new rows no longer fold
32
+ away (folding unmounted the editor and lost every edit), the footer counts what will be sent, and the review is inert
33
+ while an import is out.
34
+ - **Nothing typed is lost unasked.** Closing the window, or Start over, over rows typed or changed in it asks first
35
+ (`ExcelImportHandle.unsavedRows`); picking the entry again while its window is minimized brings that window back
36
+ rather than a fresh one. An untouched date or time is no edit: `toInputValue` reads them as their inputs do.
37
+ - **An import that wrote nothing keeps its rows** and lists under them each refusal, by row, in the backend's words:
38
+ `readExcelResponse` keeps a refusal's report (`result`, as XLS_X044 sends it) instead of dropping it, so the panel no
39
+ longer says only "No Region was added". Refusals read in row order.
40
+ - `ExcelEditorRow`'s three own fields are optional, as an added row has none; new helpers `excelRowNumber`,
41
+ `hasExcelValues` and `isExcelRowTyped`, and `excelProblemPlace(problem, byRow)`. `RowsInputHandle.remove(index,
42
+ { undo: false })` drops a row with no Undo offered, and keeps the person's own last Undo where it was.
43
+ - **A list shows what an import wrote.** After an import, `DataTable` runs `excel.onImported` first and reads its rows
44
+ after it: a `Crud` drops its cached lists there, and the reload used to be asked before, so on a page just opened it
45
+ was answered from the cache and the imported rows stayed off the screen until a reload.
46
+ - **What imported records start as.** `ExcelSettings` gains `recordStatuses` (`ExcelRecordStatus`: `value`, `label`)
47
+ and `defaultRecordStatus`; when a model offers any, the import shows "New records start as", the default picked,
48
+ and sends the choice (`importExcelRows(..., { recordStatus })`; `ExcelBackend.importRequest(rows, recordStatus?)`,
49
+ the Nexus one sending `{ rows, recordStatus }`). The report says "3 cities added as In review".
50
+ - **`Crud`, from the owner's review on 2026-10-07.** The active list is labelled **Active**, not "All" - it leaves out
51
+ the trash and the deleted. **Recover asks first** ("Recover Asia?", "It goes back to the active list."), one record
52
+ or many, like trash and delete. Recover and Move back to trash wear the back arrow (`iconArrowBackUp`): the
53
+ clock-shaped restore icon read as History beside a page's history action.
54
+ - **`details.actions`**: buttons of the page's own in the details popup's footer, before Edit and Close
55
+ (`CrudDetailsAction`: `id`, `label`, `icon`, `endpoint`, `hidden`, `onSelect(row, popup)`). One that names an
56
+ `endpoint` shows only to a user who may call it, by the rule every built-in action follows.
57
+ - **The spreadsheet import, as the owner asked on 2026-10-07.** Once a file is picked, the steps and the template and
58
+ file buttons give way to the file's name; another file is a Start over (now a bordered button) or a fresh popup. The
59
+ rows already there are read-only and never sent, and the footer says they are skipped. The new rows have Remove and
60
+ Lock at their end. After an import the review is gone: the panel shows what was added, what was not written and why,
61
+ and what was skipped and why. The `nx-excel__refused` list is gone with it; the report's lists are
62
+ `nx-excel__problems` blocks.
63
+ - **`TabularForm` `actionsAt="end"` and `lockable`.** The row's actions in a column fixed at the right edge, as a data
64
+ table has them: Remove and Lock as buttons of their own, anything else in a menu after them. A locked row's inputs
65
+ are read-only ("Locked: unlock the row to change it.") and it cannot be removed; the locks are the screen's, never
66
+ the value's.
67
+ - **A tabular form no longer jumps when a cell is clicked.** The line under the table, which holds the hint or the
68
+ message of the input with focus, keeps its height whether or not it says anything.
69
+ - **A slow answer lands in the list the user moved to.** `Crud` writes an action's answer into the table only when
70
+ the table still holds the list the action was asked from, fully loaded. Otherwise (the user switched lists while
71
+ the request was out, or a list is still loading) it patches every cached list and reads the one on screen again from
72
+ them, stopping a load that may have been asked before the change. Before, a record trashed from the active list and
73
+ answered after a switch to the trash was written over by the stale trash list and showed in neither list until the
74
+ page was opened again. The same rule covers `handle.apply`, Add, Edit and the read-back after an Add; a Create that
75
+ answers no key now loads the list instead of showing the cached one.
76
+ - **The theme panel is a quick preference panel.** Under the theme, `ThemeSwitcher` now shows `PreferencesPicker`
77
+ (`preferences={false}` leaves it out): Language and region (interface language, calendar), Dates and times (date
78
+ picker language, clock, display date format: the existing `DatePreferences`), Numbers (number scale, thousands
79
+ separator) and Tables (default page size, a row click opens the record). New: `UserPreferences`,
80
+ `readUserPreferences`, `setUserPreferences`, `useUserPreferences`, `subscribeUserPreferences`, `useNumberLocale`,
81
+ `DEFAULT_USER_PREFERENCES`, `PREFERENCE_STORAGE_KEYS` (`nx-language`, `nx-number-scale`, `nx-thousands`,
82
+ `nx-page-size`, `nx-row-click`), `InterfaceLanguage`, `ThousandsSeparator`, and `ENumberScales` (the core's enum, by
83
+ member value).
84
+ - **Numbers follow the person's settings.** `numberLocale(scale, separator)` gives `en-US`/`en-IN` or a key such as
85
+ `"en-IN|.|,"` that `formatNumber`, `parseNumber` and `getNumberSymbols` read. `NumberBox` and `Slider` use it when
86
+ they have no `locale` (they defaulted to `en-US`, which is still the setting's default), and so do a `DataTable`'s and
87
+ `CrudDetails`' number cells (`CellTextOptions.locale`, new). A number box rewrites its text when the setting changes.
88
+ - **`DataTable` `pageSize` and `openOnClick` give way to the person's choice once it is made**: the default page size
89
+ (when the table offers it) and the row click. A page size picked in a table with a `stateKey` still wins, and that
90
+ table now saves a size only when it differs from the one it opens with, so a later default reaches it.
91
+ - **Animations: On, Off or System, chosen in the theme panel.** `ThemeScript` now writes `data-nx-motion`
92
+ (`reduce` or `full`) on `<html>` before the first paint, from the visitor's choice (`data-nx-animations`, saved as
93
+ `nx-animations`) and the system's `prefers-reduced-motion`, and keeps it current when the system setting changes.
94
+ **Every stylesheet reads that attribute instead of the media query**, and so does script motion (the popup's
95
+ minimize and maximize, smooth scrolls). On animates on a machine whose operating system turned animations off; Off
96
+ never animates. An app that does not render `ThemeScript` animates whatever the system says. New: `AnimationMode`,
97
+ `setAnimations`, `motionFor`, `REDUCED_MOTION_QUERY`, the `ANIMATIONS_*` and `MOTION_ATTRIBUTE` constants, and
98
+ `useTheme()`'s `animations`, `motion` and `setAnimations`.
99
+ - **Density: Compact or Cozy.** `data-nx-density` on `<html>` (`nx-density`; `setDensity`, `useTheme().density`).
100
+ Cozy makes `--nx-control-sm/md/lg` 32/38/44 and the new `--nx-row` 38; compact is exactly the old sizes. Table rows
101
+ and headers, menu items, dropdown options and the Columns panel's lines are now sized from `--nx-row`, so they
102
+ follow it. `ThemeScript` takes `defaultDensity`.
103
+ - **Scale grid, development builds only**: Off, px (a line every 4px, solid every 48px) or mm (millimetres, solid every
104
+ centimetre), drawn over the window on `[data-nx-grid] body::after`. `ThemeScript`'s `grid` and `ThemePicker`'s
105
+ `showGrid` default to `isDevelopmentBuild()`, so a production build neither shows the section nor applies a saved
106
+ grid. New: `ScaleGrid`, `setScaleGrid`, `isDevelopmentBuild`.
107
+ - **`ThemePicker` / `ThemeSwitcher`** add Density, Animations and Scale grid under Mode and Theme (`showDensity`,
108
+ `showAnimations`, `showGrid`).
109
+ - **Popups fade in and out** (160ms in, 120ms out), with nothing else moving: the slide and scale are gone. **Every
110
+ popover panel fades** in and out in 120ms through one rule on `.nx-popover`; the menu's, the table panels' and the
111
+ dropdown's own opening animations are gone. A closing panel takes no clicks, and nothing waits for a fade.
112
+ - **Popover panels fit the window.** `placeUnderAnchor` and `placeAtPoint` pick the side that fits, or the one with
113
+ more room, and write that room as `--nx-popover-room`; a panel that marks its scrolling part `.nx-popover__scroll`
114
+ is never taller than it. That part reaches the panel's right wall, so its scroll bar sits on the wall, and carries
115
+ the right padding inside. Menus, the theme panel and the table's Columns and Filters panels use it; `usePopoverPanel`
116
+ lost its own height cap. New `showPanel` opens a panel scrolled to the top, and a hovered menu item no longer scrolls
117
+ its list. Placing an open panel again (on every scroll) no longer throws its list back to the top.
118
+ - **A panel's padding is `--nx-popover-pad`.** `.nx-popover`'s own padding loaded after the menu's and the table
119
+ panels', so a menu was 16px inside its edge, not the 4px it asked for, and the table panels 16px, not 8px; both now
120
+ get theirs. `.nx-table-panel__body` is gone (`.nx-popover__scroll`).
121
+ - **One thin, quiet scroll bar everywhere, the design lab's.** `Nexus.Base.css` gives every element
122
+ `scrollbar-width: thin` and a thumb of the text colour at 22% on no track. The theme panel, menus and context menus,
123
+ the table's Columns and Filters panels, popup bodies, the date-time panel, the chat, code blocks and tables in
124
+ Markdown and rich text, and the admin page and menu drew the browser's 15px bar with a painted grey track; on
125
+ Windows, Chrome and Edge now draw every bar 10px wide, as the lab does (Firefox's thin bar is 8px; Safari draws its
126
+ own thin bar). The components' own bars are gone: the 8px `::-webkit-scrollbar` pill of the text area, the Markdown
127
+ preview and the rich text (Chromium and WebKit ignore it once `scrollbar-width` is set), and the thin bar with a
128
+ stronger thumb of the tree, the tabular form, the dropdown list and the time picker. The data table's rows keep
129
+ theirs (`Nexus.Table.css`) until a later change, and the time picker's columns still hide the thumb until the
130
+ pointer or the keys are in them. `--nx-field-scrollbar`, `--nx-field-scrollbar-hover`, `--nx-dropdown-scrollbar` and
131
+ `--nx-time-scrollbar` are gone; the text area's resize grip reads `--nx-field-grip`. Room kept for a bar narrows with
132
+ it: every `scrollbar-gutter: stable` (the admin page and its bar, whose right edge moves from 27 to 22px off the
133
+ window, the chat thread, the dropdown list) and the padding a modal popup keeps for the page's bar
134
+ (`--nx-scrollbar-gap`, measured).
135
+ - **A form whose values load shows its real inputs, disabled, instead of placeholders.** A `Crud`'s Edit popup, an
136
+ `ApiForm` with `loadValue` and a `Form` with `loading` have their final size from the first frame, and the values
137
+ land in the same inputs, as a load (no "Unsaved changes", no "Discard your changes?"). While loading, `FormStore`
138
+ takes nothing typed, even from an input whose `disabled` was taken off by hand, and draws such an input again from
139
+ the store when loading ends; a submit already waited. The `.nx-form__skeleton*` classes and their animation are gone.
140
+ - **The form's status line keeps its one line when empty, and a form's bar fills a popup's foot**, so the status sits
141
+ beside the buttons: "Loading…" or "Unsaved changes" coming and going no longer moves the bar, and a `Crud` popup
142
+ lost the empty 12px row it had above its buttons.
143
+ - **`Avatar`** (`nexus-shared/client`): a person's round picture, or their initials on a soft tone without one or
144
+ when it fails to load. `size` `sm`/`md`/`lg` or `--nx-avatar-size`; `onChangePicture` makes it a button with a
145
+ camera over it on hover. The chat's and the dropdown's avatars are now this one: `ChatAvatar` and
146
+ `DropdownAvatar` still exist and draw it, `.nx-chat-avatar` is gone (`.nx-avatar`), `.nx-dropdown-avatar` stays
147
+ as the dropdown's size, and `--nx-chat-avatar` is `--nx-avatar-size`.
148
+ - **`Button` `dot`**: a small dot on the corner, for something new behind it such as unread notifications; `"primary"`
149
+ marks a state that is on, such as filters.
150
+ - **`Menu`: a control inside a heading is chosen like a row.** An entry `{ heading: <node> }` may hold a button with
151
+ `role="menuitem"`: the arrow keys walk it with the rows, and a click on it closes the menu. A menu opened from the
152
+ keyboard still starts on its first row.
153
+ - **The data table is one frame**: the bar, the rows and the footer sit in one border (`.nx-table` carries it now,
154
+ not `.nx-table__frame`), with a line under the bar and the footer as the bottom band (`--nx-footbar`, a new
155
+ token). An empty bar takes no room. Column headings are 12px, weight 650.
156
+ - **`DataTable` `add`**: the table's own Add, first in the bar. With `excel`, it is a split button whose arrow holds
157
+ Download template, Import from a spreadsheet and Export to Excel, which then leave the options menu. `Crud`
158
+ passes its Add this way.
159
+ - **The data table's bar is one line on a wide table, in a new order**: the Add section (`add`, then the page's
160
+ `actions`) and the lists (`toolbar`) on the left; Reload, the options menu (⋮) and the search, rows per page inside
161
+ it, on the right. It was the lists, the search, Reload, ⋮, `actions` and Add, wrapping where they fell: at 1126px
162
+ on the admin shell the bar broke into three ragged rows. A narrower table now moves whole parts, by its own width
163
+ (container queries on `.nx-table`): under 670px the search takes the top line (with nothing on the left, it leads
164
+ the buttons' line), under 375px Reload moves to the end of ⋮ (`DataTable` asks the hidden button, so the width is
165
+ written once, in the stylesheet), and under 325px, beside lists, the Add section takes the last line with its buttons
166
+ sharing it. On the admin shell those are windows of 695, 400 and 350px. A bar wider than those widths expect (a
167
+ long Add label, the page's own buttons, cozy density) is squeezed into the next step as soon as a line would break
168
+ (`data-squeeze` on `.nx-table__bar`, set a frame after a size change). A `toolbar` or `actions` that renders
169
+ nothing takes no room and decides no step, and a bar left with nothing else is hidden. New classes
170
+ `.nx-table__bar-add` (Add and `actions`) and `.nx-table__reload`; the search is now a child of `.nx-table__bar` after
171
+ `.nx-table__bar-end`, not inside it, and grows to 19rem from a 12rem basis.
172
+ - **Columns opens from the options menu (⋮)**, as its first item; the bar has no Columns button. Reload, Columns
173
+ and the options button are ghost buttons. `TableColumnsPanel` takes an `anchor` and an `open()` handle for this.
174
+ - **`TabButtons` `labels="auto"`**: every label while its container (a data table) is at least 900px wide, only the
175
+ chosen one's below. `Crud`'s lists use it.
176
+ - **`DataTable` `selectBy`**: while rows are selected, their count opens a menu that selects again by what the rows
177
+ are: All on this page, then each `{ label, icon, test }`, which leaves exactly the matching rows on the page
178
+ selected. `Crud`'s main list offers Pinned, Not pinned, Flagged and Not flagged.
179
+ - **`SplitButton` `fixed`**: the main button never changes; the menu holds other ways to do it, run when picked.
180
+ - **`DataTable` `filterInputs`: an advanced filter.** Input params, each named after the column it filters, become
181
+ **Filters** in the options menu (⋮): a small form under the button, applied as it changes. The values are
182
+ `TableQuery.filters` (`TableStore.setFilters` sets them, `{}` clears); a client table matches them in `queryRows`
183
+ (a fifth argument), and a server gets them flat by name through `CrudBackend.pageRequest`'s third argument, as
184
+ `filterRequest` writes them (a date range as `{name}From`/`{name}Before`). A `CrudBackend` or loader of your own
185
+ ignores them until it reads that argument or `query.filters`. Also new: `activeFilters`, `asFilterInput`,
186
+ `filterMatcher`, `filterNames`; `usePopoverPanel` and `PopoverPanelHandle` (`nexus-shared/client`), the panel
187
+ Filters and Columns share; `followAnchor`'s second argument, a panel whose size it follows too.
188
+ - **Renamed: `.nx-columns__head`, `__title`, `__count` and `__foot` are `.nx-table-panel__*`**, shared by both panels.
189
+ An override of the old names no longer applies.
190
+
191
+ ## 3.0.0
192
+
193
+ **Breaking. This changes the value of exported constants, and `2.0.0` is on npm with the old ones.** Publishing it
194
+ as a patch or a minor would hand `^2.0.0` consumers different numbers under a compatible-looking range, which is the
195
+ worst way to ship this, so it takes a major. The owner chose the number on 2026-10-02, together with `nexus-public`
196
+ and `nexus-admin`, which both depend on this package and so move to `3.0.0` with it; their `nexus-shared` range is
197
+ now `^3.0.0`, because a bump of this one alone would leave them pulling the old, wrong constants.
198
+
199
+ **`nexus-icons` must be installed at `1.1.0` or newer**, and the dependency range here says so. The chat components
200
+ added in this version use `iconChecks`, `iconCornerUpLeft`, `iconMessageCircle`, `iconPaperclip`, `iconSend` and
201
+ `iconUserPlus`, none of which exist in `nexus-icons@1.0.0`: with the old range a fresh install resolved to `1.0.0`
202
+ and the build failed on six missing exports.
203
+
204
+ ### `NexusModule` — `Chat` added, and every module after it moved up one bit
205
+
206
+ The backend's `EModules` gained `Chat` at bit 6, and everything after it shifted. `NexusModule` follows it exactly.
207
+ A permission is a bitmask of these flags, so a package that kept the old numbers would grant and deny the wrong
208
+ modules without any error to show for it.
209
+
210
+ | Module | Was | Now |
211
+ |---|---|---|
212
+ | `None` | 0 | 0 |
213
+ | `Migrations` | 1 | 1 |
214
+ | `Configuration` | 2 | 2 |
215
+ | `Identity` | 4 | 4 |
216
+ | `Enterprise` | 8 | 8 |
217
+ | `Drive` | 16 | 16 |
218
+ | `Message` | 32 | 32 |
219
+ | **`Chat`** | *(did not exist)* | **64** |
220
+ | `Accounting` | 64 | **128** |
221
+ | `Report` | 128 | **256** |
222
+ | `Requisition` | 256 | **512** |
223
+ | `Tenant` | 512 | **1024** |
224
+ | `Subscription` | 1024 | **2048** |
225
+ | `Reservation` | 2048 | **4096** |
226
+ | `School` | 4096 | **8192** |
227
+ | `Pharmacy` | 8192 | **16384** |
228
+ | `Staffing` | 16384 | **32768** |
229
+ | `Portfolio` | 32768 | **65536** |
230
+
231
+ `NEXUS_MODULES` gained the matching entry (`key: "chat"`, `label: "Chat"`, `env: "API_CHAT"`), so `Chat` can be
232
+ looked up, proxied and named in messages like every other module.
233
+
234
+ **What to check when you upgrade.** Code that uses the names (`NexusModule.Reservation`) needs nothing. Anything
235
+ that stored, transmitted or hard-coded a **number** has to be revisited:
236
+
237
+ - permission masks saved in a database, a cache, a token claim or a settings file;
238
+ - proxy URLs written with a number in place of a key (`/api/app/2048/…` is now `/api/app/4096/…`, and `reservation`
239
+ by key has always worked and still does);
240
+ - a backend that has not taken the `Chat` shift yet — both sides move together or neither does.
241
+
242
+ ### `NEXUS_FLAGGED_STATUS` — 15 → 25
243
+
244
+ The owner settled the record-status bands, and `ERecordStatus.Flagged` is seeded at **25**. `NexusCrudBackend`
245
+ decides whether a row is flagged by comparing `recordStatus` to this constant, so a package on 15 while the backend
246
+ writes 25 shows the wrong flag on every row, and flagging one writes a status the backend does not recognise.
247
+
248
+ The bands, for anything that reasons about them:
249
+
250
+ | Band | Range |
251
+ |---|---|
252
+ | Active | 0–39 |
253
+ | — Active account | 0–29 |
254
+ | — Flag | **25–29** |
255
+ | — Pending, unverified, in review, approval required | 30–39 |
256
+ | Trash | 40–59 |
257
+ | Delete | 60 and above |
258
+
259
+ `NEXUS_ACTIVE_STATUS` stays 0, and the `recordStatus < 40` reading of "a live record" that the rest of the code
260
+ already assumes stays right.
261
+
262
+ Note that **the flag is a range, 25–29, and this release still checks a single value.** Only the seeded member at 25
263
+ has a meaning today, and the bands are about to become configurable per project, so a range-aware check belongs with
264
+ that work rather than here. Do not read the single value as the final answer.
265
+
266
+ ### Removed: the archive
267
+
268
+ The owner removed the archive on 2026-09-30: a record is live, in the trash, or deleted, and the way back from
269
+ deleted lands it in the trash. The second put-away list and its second recovery path were the only reason the CRUD
270
+ had both Restore and Recover, and the bands close up behind it — **Trash is 40–59 and Delete 60 and above**, so
271
+ nothing stored keeps a meaning it no longer has. `recordStatus < 40` still reads "a live record".
272
+
273
+ What goes, all of it breaking for anything that named it:
274
+
275
+ - `CrudMode` loses `"archive"`, so a CRUD shows All, Flagged, Trash and Deleted.
276
+ - `CrudRowActionName` and `CrudMassActionName` lose `"archive"` and `"restore"`. Recover, the trash's own way back,
277
+ is untouched: a CRUD that offered both now offers one.
278
+ - `CrudEndpoints` loses `archive` and `restore`; `apiController` stops naming `Archive`, `Restore`, `MassArchive`,
279
+ `MassRestore`, `ArchiveListing` and `ArchivePagination`, and `lists`/`pages` lose their `archive` key.
280
+ - `MessageActionName` loses `"archive"`, and the message builder its words and its "It can be restored from the
281
+ archive." confirmation detail. `"restore"` stays in the vocabulary: it is a plain verb, not the archive's.
282
+
283
+ **What to check when you upgrade.** A CRUD that listed `"archive"` in `modes` or `actions` no longer compiles,
284
+ which is the point — the endpoint behind it is gone from the backend too. A backend that still answers
285
+ `ArchiveListing` is not asked for it any more.
286
+
287
+ ### Added: the chat
288
+
289
+ A discussion on any record — a leave request the manager and a co-worker talk through, a purchase order, an
290
+ applicant, a tender — behind one icon that says whether anything is waiting. Nothing that does not use it changes.
291
+
292
+ - A conversation is named by three values, `ChatScope`: the **domain** (which kind of record), the **parent id** (the
293
+ record) and the **user id** (who is reading). The domain is the app's own enum, given either as a template
294
+ (`<Chat<EDomainTypes> …>`) or once in `ChatRegister`, the same way `ApiRegister` names the module enum. Without one
295
+ it is a plain number.
296
+ - **No domain numbers are defined here.** They belong to the backend's `EDomainTypes`, and the warning at the top of
297
+ this file applies to them in full. `defineChatDomains` only gives an app's domains their names on screen.
298
+ - `ChatButton` is an icon in three looks: quiet with no messages, the accent on its tint once there are messages, and
299
+ the accent filled with an unread badge once some are unread. One size in all three, with the badge over the corner,
300
+ so a column of them in a table does not move as the counts arrive.
301
+ - `ChatSummaryCache` fills every icon that mounted in the same moment with **one call per domain**. A list whose rows
302
+ already carry their counts passes `summary` and makes no call at all.
303
+ - `Chat` is the icon with the conversation in a popup; `ChatPanel` is the conversation on its own, for a record's own
304
+ page. Messages are **Markdown**: written in `MarkdownEditor` and drawn by `MarkdownView`. Enter sends and
305
+ Shift+Enter starts a line, except in a list or a code fence, where the editor keeps Enter; `sendKey="modifier"`
306
+ moves sending to Ctrl+Enter.
307
+ - One mark where the time sits says how far a message of your own has got: a spinner while sending, a tick once the
308
+ server has it, two once `deliveredTo` holds somebody, two in the accent once `readBy` does.
309
+ - Where the data comes from is one interface, `ChatSource`. `apiChatSource` goes through the app's `ChatBackend`
310
+ (`NexusChatBackend` by default); `localChatSource` keeps a conversation in memory for a demo or a test.
311
+ - **The Nexus endpoints are the shape the Chat module is expected to take, not an API that answers today.**
312
+ `NexusModule.Chat` is a number and a proxy key; the controller is not built. Each difference, when it lands, is one
313
+ override on `NexusChatBackend`.
314
+ - `check:themes` gained a rule: body text on the primary tint, which is a message bubble of your own. Every theme
315
+ clears AAA except catppuccin light at 6.84, so it is held to the AA 4.5 that applies to body text.
316
+ - **A record holds many conversations.** `ChatScope` names the record; each conversation in it is a
317
+ `ChatConversation` with its own id and a type, `one-to-one` or `group`. `ChatPlaceSource` lists them and starts
318
+ them, `useChatPlace` is the hook, and `apiChatPlace` is the API one. A panel given a `source` still shows that one
319
+ conversation and no switch, so nothing that used the first round changes.
320
+ - The panel's body has three views — the thread, the list of conversations, and New chat — and the bar moves between
321
+ them. With several conversations the title is the switch; with one it is just the title.
322
+ - Starting a chat never asks for the type: one person makes a one-to-one, more make a group, and a name makes a
323
+ group either way. A one-to-one with somebody who already has one is expected to come back rather than be made
324
+ twice; `localChatPlace` keeps that rule.
325
+ - `@` at the start of a word offers the people in the conversation, narrows as it is typed, and owns Enter while it
326
+ is open. A mention is plain `@Name`, or a Markdown link with `mentionHref`; either way the ids travel in the
327
+ draft's `mentions`.
328
+ - `ChatSnapshot` gained `loaded`: a store that has read nothing yet holds empty counts, and a panel must not write
329
+ those over what a conversation list already knows.
330
+
331
+ ### Added: several at once, `TabButtons multiple`
332
+
333
+ `TabButtons` picks one option; with `multiple` it picks several, so a list of choices that must be seen all at once
334
+ (the weekdays a rule runs on, the letters or symbols a pattern allows) needs no second control. Nothing that does
335
+ not pass it changes.
336
+
337
+ - `value` is an array and `onChange` hands back the chosen values **in option order**, whatever order they were
338
+ clicked in. Values that match no option are ignored and never handed back.
339
+ - The options are native checkboxes in a `role="group"`: Tab moves between them and Space toggles one. The group
340
+ still needs `aria-label` or `aria-labelledby`.
341
+ - There is no sliding indicator: each chosen option is raised with the indicator's own colors, border and shadow,
342
+ and keeps the pointer, since a click takes it away again. An option that is not chosen sits flat on the track.
343
+ - The options **wrap** onto more rows when they do not fit, each row as tall as a one-row track's options, and an
344
+ option is never narrower than it is tall, so a row of single letters lines up. On a touch screen
345
+ (`pointer: coarse`) every option is a medium control's height: 26 letters on a phone are 34px targets, not 22px.
346
+ - Made and checked for the `nav` design (`/demo/tab-buttons`, "Several at once"); `design="list"` with `multiple`
347
+ renders checkboxes too, but has not been checked yet.
348
+
349
+ ### Added: inputs that follow others, `dependsOn`, and a row's own params, `cellParams`
350
+
351
+ Two optional keys, added 2026-09-26 for the builder's class form, whose company model shows only when the class has a
352
+ company or a branch and whose type arguments offer a key type in one row and a model in the next. Nothing that does
353
+ not pass them changes.
354
+
355
+ - **`dependsOn: ["hasCompany"]`** on an input's params (Form only). When a named input's value changes — typed, or
356
+ set from code — this input draws again **without mounting again** (nothing typed in it is lost) and its params are
357
+ read afresh, so getters for `hidden`, `readOnly`, `readOnlyMessage`, `label`, `hint` or `options` answer for the
358
+ values as they are now. Its rule is asked again too, so a `required` a getter switched on counts at once. An input
359
+ that names any is asked `hidden` each time it draws, so it can come and go; one that names none is asked once, when
360
+ the form draws, as before. Name the input itself to follow its own typing.
361
+ - **`cellParams(row, name, index)`** on `TabularForm` and `GroupForm`: the params of one cell that differ from its
362
+ column's, such as a picker's `options` and `selectedOptions`. Asked as the row draws; return nothing for the
363
+ column's own params, and the **same object** for the same answer (the merged params are kept per answer, so the cell
364
+ does not draw again for nothing). Getters on either side stay getters. Rules still come from the column.
365
+
366
+ ### Added: a generated column in a tabular or group form
367
+
368
+ Two optional props on `TabularForm` and `GroupForm`, for a column whose values a row's **position** decides rather
369
+ than the user — a bitwise enum's value, a line number, a running total. Nothing that does not pass them changes.
370
+
371
+ - `deriveRows(rows)` returns one patch per row and runs after every change, including a row being added, removed,
372
+ duplicated or **moved**. The patches are written as code writes, so only the cells whose value really changed
373
+ mount again and focus stays on the row's own menu: a keyboard can move a row twice in a row.
374
+ - `cellReadOnly(row, name, index)` is asked for every cell as it renders and returns the reason a cell is
375
+ generated. The cell is locked and shows that reason as its read-only note. Because it is asked per render, a
376
+ column can lock and unlock while the form around it changes, which params cannot do — they are fixed once a form
377
+ opens.
378
+
379
+ ### Added: rows nobody may change, `rowReadOnly`
380
+
381
+ One optional prop on `TabularForm` and `GroupForm`, for rows the user did not write and may not change — the fields
382
+ a class inherits from its parent, shown first in the same grid as its own. Nothing that does not pass it changes.
383
+
384
+ - `rowReadOnly(row, index)` returns the reason (or `true`). Every cell of such a row is read-only with that reason
385
+ as its note, the row's menu in a tabular form offers nothing (no insert, duplicate, move or remove), and the row
386
+ below it cannot move up past it. Nothing reorders the rows: keep the locked ones first.
387
+ - A locked row says nothing about its columns: the column-header lock is worked out from the other rows in view, so
388
+ a grid of inherited rows and one own row does not put a lock on every header.
389
+
390
+ ### Added: a panel beside a CRUD form, `form.aside`
391
+
392
+ An Add or Edit popup can show a panel next to its form: a preview of what a save would produce, a summary, a help
393
+ panel. It takes a node, or a function handed `{ form, action, row }` — `form` being that popup's own `FormStore`, so
394
+ a panel follows what is typed through `subscribeValue()` and the form around it never re-renders for it.
395
+
396
+ The two are columns above 1000px, each scrolling on its own so a long form cannot push the panel off the screen, and
397
+ they stack below that with the **form first**, in the DOM as well as on the screen. `--nx-crud-aside-width` sets the
398
+ panel's share (default `clamp(20rem, 38%, 44rem)`). Pair it with a wide `form.size` — `xl` or `full`; a panel in a
399
+ 560px popup leaves the form nothing. A CRUD that passes no `aside` renders exactly what it did before, wrapper and
400
+ all.
401
+
402
+ ### `TabularForm` reworked: one row control, rows that do not move, one lock per column
403
+
404
+ The owner called the tabular design bad, from a screenshot of an 18-row table with one generated column. Every fault
405
+ was in this component rather than in the page that used it, so every fix is here. Nothing needs a new prop, and a
406
+ table that passed nothing new looks different — deliberately.
407
+
408
+ **Breaking, visually and for tests.** Two things a consumer may be reaching for have moved:
409
+
410
+ - **The remove column is gone.** Remove was a column of unlabelled `×` buttons at the end, beside a row menu that
411
+ already held Remove: two ways to act on one row, and a whole column spent on one of them. Now a row has one
412
+ control, the menu beside its number, holding Insert above, Insert below, Duplicate, Move up, Move down and Remove.
413
+ `removable` still decides whether Remove is offered. A test that clicked a button named `Remove <row>` opens the
414
+ row's menu instead: `Row 2 actions`, then the `Remove` item. `.nx-tabular__end` and `.nx-tabular__remove` are gone
415
+ from the DOM and the stylesheet; so is `--nx-tabular-end`, replaced by `--nx-tabular-menu` (the menu's slot width).
416
+ - **The row number no longer opens the menu.** It used to *become* the menu on hover: the number faded out and dots
417
+ faded in over it, so the numbering looked broken and the grid felt unstable as the pointer crossed it. The number
418
+ now stays where it is, right-aligned, at every moment, and the menu button sits beside it in its own space,
419
+ always present. The number column is that much wider.
420
+
421
+ **Fixed, with no change to how a table is written:**
422
+
423
+ - **Every row is one line high, whatever it holds.** The row's grid track was sized by its tallest cell, so a single
424
+ `textarea` column of `rows: 2` made every cell of its row 20px taller than the row's own box — drawing the cells,
425
+ their edges and their focus rings over the rows below. One row of content therefore looked taller than its
426
+ neighbours, and a focus ring bled outside its cell. The row now has one track exactly its own height, cells keep
427
+ what they hold inside their bounds, and a `textarea` fills its cell and scrolls inside it.
428
+ - **No resize grips in cells.** A `textarea` in a cell took `resize: vertical` from the multi-line input rule, so
429
+ every cell of such a column offered a grip that would break the alignment of its row. They are `resize: none` now.
430
+ - **A filled cell and an empty one are drawn the same.** Same consequence as the above: only the cell with text was
431
+ tall enough to show what the overflow was doing.
432
+ - **`maxHeight` comes down to whole rows.** The scroller used to end wherever the number fell, cutting the last row
433
+ in half, which reads as broken rather than as scrollable. The component measures its header, its frame and its
434
+ real row height and caps itself just under what was asked for. A number too small for one row is left alone.
435
+ - **A generated column says so once.** `cellReadOnly` put a lock in every cell of the column it locked — eighteen
436
+ locks for one fact. The lock is now on the **column header**, with the reason as its title, and the cells keep the
437
+ read-only tint and their own reason, which still shows under the table when one is used. A lock that really is per
438
+ row (some rows locked, not all) leaves the header alone and is drawn per cell as before.
439
+ - **Less chrome.** The frame is `--nx-border` rather than `--nx-border-strong`: the sheet draws every line it needs
440
+ inside itself, and in a popup or a panel the strong edge was a box around a box around boxes.
441
+
442
+ ## 2.0.0
443
+
444
+ The rewrite: compiled `dist` with the entry points `.`, `/client`, `/server` and `/styles.css`, in place of 1.x's
445
+ raw TypeScript from `src` with `.`, `/client` and `/interface`. A major version, so nobody on `^1.1.21` is moved
446
+ into a different API.