nexus-shared 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +262 -0
  2. package/README.md +122 -25
  3. package/dist/Client.Index.d.ts +11 -0
  4. package/dist/Client.Index.js +11 -0
  5. package/dist/Components/Chats/Chat.d.ts +28 -0
  6. package/dist/Components/Chats/Chat.js +19 -0
  7. package/dist/Components/Chats/ChatButton.d.ts +26 -0
  8. package/dist/Components/Chats/ChatButton.js +41 -0
  9. package/dist/Components/Chats/ChatComposer.d.ts +41 -0
  10. package/dist/Components/Chats/ChatComposer.js +177 -0
  11. package/dist/Components/Chats/ChatConversations.d.ts +35 -0
  12. package/dist/Components/Chats/ChatConversations.js +38 -0
  13. package/dist/Components/Chats/ChatPanel.d.ts +66 -0
  14. package/dist/Components/Chats/ChatPanel.js +94 -0
  15. package/dist/Components/Chats/ChatParts.d.ts +87 -0
  16. package/dist/Components/Chats/ChatParts.js +100 -0
  17. package/dist/Components/Chats/ChatThread.d.ts +22 -0
  18. package/dist/Components/Chats/ChatThread.js +96 -0
  19. package/dist/Components/Documents/Menu.js +24 -20
  20. package/dist/Components/Documents/SplitButton.js +5 -3
  21. package/dist/Components/Documents/TabButtons.d.ts +24 -6
  22. package/dist/Components/Documents/TabButtons.js +23 -4
  23. package/dist/Components/Forms/ApiForm.d.ts +6 -4
  24. package/dist/Components/Forms/ApiForm.js +15 -14
  25. package/dist/Components/Forms/Crud.js +202 -50
  26. package/dist/Components/Forms/ExcelImport.d.ts +42 -0
  27. package/dist/Components/Forms/ExcelImport.js +190 -0
  28. package/dist/Components/Forms/Form.js +5 -1
  29. package/dist/Components/Inputs/DateTimePicker.js +2 -1
  30. package/dist/Components/Inputs/GroupForm.js +4 -2
  31. package/dist/Components/Inputs/InputRenderer.d.ts +2 -0
  32. package/dist/Components/Inputs/InputRenderer.js +1 -1
  33. package/dist/Components/Inputs/ReadOnlyNotice.js +2 -1
  34. package/dist/Components/Inputs/RowsInput.d.ts +12 -1
  35. package/dist/Components/Inputs/RowsInput.js +53 -4
  36. package/dist/Components/Inputs/TabularForm.d.ts +7 -3
  37. package/dist/Components/Inputs/TabularForm.js +85 -20
  38. package/dist/Components/Inputs/TimePicker.js +2 -1
  39. package/dist/Components/Layouts/ThemeSwitcher.d.ts +2 -2
  40. package/dist/Components/Layouts/ThemeSwitcher.js +35 -15
  41. package/dist/Components/Viewers/DataTable.js +88 -34
  42. package/dist/Components/Viewers/DataTableColumns.d.ts +28 -0
  43. package/dist/Components/Viewers/DataTableColumns.js +243 -0
  44. package/dist/Components/Viewers/DataTableParts.d.ts +6 -2
  45. package/dist/Components/Viewers/DataTableParts.js +21 -9
  46. package/dist/Helpers/ApiClient.d.ts +22 -0
  47. package/dist/Helpers/ApiClient.js +4 -1
  48. package/dist/Helpers/ApiFormHelpers.d.ts +27 -4
  49. package/dist/Helpers/ApiFormHelpers.js +73 -9
  50. package/dist/Helpers/ApiModules.d.ts +0 -4
  51. package/dist/Helpers/ApiModules.js +1 -0
  52. package/dist/Helpers/ApiResponses.d.ts +14 -4
  53. package/dist/Helpers/ApiResponses.js +116 -25
  54. package/dist/Helpers/ApiRoutes.d.ts +0 -10
  55. package/dist/Helpers/ApiRoutes.js +2 -10
  56. package/dist/Helpers/ChatBackend.d.ts +134 -0
  57. package/dist/Helpers/ChatBackend.js +332 -0
  58. package/dist/Helpers/ChatHelpers.d.ts +189 -0
  59. package/dist/Helpers/ChatHelpers.js +486 -0
  60. package/dist/Helpers/ChatHooks.d.ts +81 -0
  61. package/dist/Helpers/ChatHooks.js +175 -0
  62. package/dist/Helpers/ChatStore.d.ts +132 -0
  63. package/dist/Helpers/ChatStore.js +394 -0
  64. package/dist/Helpers/ChatSummaries.d.ts +39 -0
  65. package/dist/Helpers/ChatSummaries.js +118 -0
  66. package/dist/Helpers/CrudBackend.d.ts +47 -5
  67. package/dist/Helpers/CrudBackend.js +99 -4
  68. package/dist/Helpers/CrudHelpers.d.ts +39 -4
  69. package/dist/Helpers/CrudHelpers.js +76 -11
  70. package/dist/Helpers/ExcelBackend.d.ts +106 -0
  71. package/dist/Helpers/ExcelBackend.js +290 -0
  72. package/dist/Helpers/ExcelHelpers.d.ts +98 -0
  73. package/dist/Helpers/ExcelHelpers.js +302 -0
  74. package/dist/Helpers/FormStore.d.ts +2 -0
  75. package/dist/Helpers/FormStore.js +19 -1
  76. package/dist/Helpers/MessageBuilder.js +0 -2
  77. package/dist/Helpers/PopoverHelpers.d.ts +31 -6
  78. package/dist/Helpers/PopoverHelpers.js +116 -12
  79. package/dist/Helpers/RowsStore.d.ts +7 -1
  80. package/dist/Helpers/RowsStore.js +22 -0
  81. package/dist/Helpers/TableColumns.d.ts +7 -0
  82. package/dist/Helpers/TableColumns.js +41 -13
  83. package/dist/Helpers/TableHelpers.d.ts +9 -1
  84. package/dist/Helpers/TableHelpers.js +20 -0
  85. package/dist/Interfaces/ApiInterfaces.d.ts +29 -4
  86. package/dist/Interfaces/ApiInterfaces.js +16 -10
  87. package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
  88. package/dist/Interfaces/ChatInterfaces.js +1 -0
  89. package/dist/Interfaces/CrudInterfaces.d.ts +73 -12
  90. package/dist/Interfaces/ExcelInterfaces.d.ts +215 -0
  91. package/dist/Interfaces/ExcelInterfaces.js +45 -0
  92. package/dist/Interfaces/FormInterfaces.d.ts +52 -2
  93. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  94. package/dist/Interfaces/TableInterfaces.d.ts +62 -2
  95. package/dist/Services/BrowserApi.d.ts +18 -1
  96. package/dist/Services/BrowserApi.js +18 -0
  97. package/dist/Services/ChatApi.d.ts +37 -0
  98. package/dist/Services/ChatApi.js +98 -0
  99. package/dist/Services/ExcelApi.d.ts +46 -0
  100. package/dist/Services/ExcelApi.js +196 -0
  101. package/dist/Services/ServerApi.d.ts +6 -1
  102. package/dist/Services/ServerApi.js +3 -0
  103. package/dist/Shared.Index.d.ts +8 -0
  104. package/dist/Shared.Index.js +8 -0
  105. package/package.json +6 -3
  106. package/src/Styles/Nexus.Button.css +2 -0
  107. package/src/Styles/Nexus.Chat.css +1489 -0
  108. package/src/Styles/Nexus.Crud.css +77 -0
  109. package/src/Styles/Nexus.Excel.css +370 -0
  110. package/src/Styles/Nexus.Form.css +23 -0
  111. package/src/Styles/Nexus.Index.css +2 -0
  112. package/src/Styles/Nexus.Menu.css +12 -2
  113. package/src/Styles/Nexus.Rows.css +78 -48
  114. package/src/Styles/Nexus.Tab.Buttons.css +55 -0
  115. package/src/Styles/Nexus.Table.css +286 -4
  116. package/src/Styles/Nexus.Theme.Picker.css +75 -2
@@ -57,8 +57,13 @@
57
57
  */
58
58
  .nx-tabular {
59
59
  --nx-tabular-row: calc(var(--nx-control-md) + 1px);
60
- --nx-tabular-end: 40px;
60
+ --nx-tabular-menu: 22px;
61
61
  --nx-tabular-line: var(--nx-border);
62
+ /*
63
+ * The sheet keeps the strong edge. Making it the plain border to answer "boxes inside boxes" put the frame below
64
+ * the edge of every input box beside it, in all seven themes light and dark, which the library asserts against
65
+ * (`tabular-form.cjs`, "frame edge"): the outermost edge of a control cannot be fainter than the ones inside it.
66
+ */
62
67
  --nx-tabular-frame: var(--nx-border-strong);
63
68
  --nx-tabular-bg: var(--nx-surface);
64
69
  --nx-tabular-head: color-mix(in oklab, var(--nx-text) 4%, var(--nx-surface));
@@ -68,12 +73,12 @@
68
73
 
69
74
  .nx-tabular--sm {
70
75
  --nx-tabular-row: calc(var(--nx-control-sm) + 1px);
71
- --nx-tabular-end: 34px;
76
+ --nx-tabular-menu: 20px;
72
77
  }
73
78
 
74
79
  .nx-tabular--lg {
75
80
  --nx-tabular-row: calc(var(--nx-control-lg) + 1px);
76
- --nx-tabular-end: 44px;
81
+ --nx-tabular-menu: 24px;
77
82
  }
78
83
 
79
84
  .nx-tabular[data-problem] {
@@ -89,7 +94,7 @@
89
94
  box-shadow: var(--nx-shadow-sm);
90
95
  outline: none;
91
96
  /* A focused cell scrolls clear of the header and the fixed columns. */
92
- scroll-padding: var(--nx-tabular-row) var(--nx-tabular-end) 0 48px;
97
+ scroll-padding: var(--nx-tabular-row) 0 0 var(--nx-tabular-num, 48px);
93
98
  scrollbar-width: thin;
94
99
  scrollbar-color: color-mix(in oklab, var(--nx-text-subtle) 75%, transparent) transparent;
95
100
  transition: border-color var(--nx-duration);
@@ -106,6 +111,12 @@
106
111
  .nx-tabular__row {
107
112
  display: grid;
108
113
  grid-template-columns: var(--nx-tabular-columns);
114
+ /*
115
+ * One track, exactly the row's height. Without it the track is sized by the tallest cell, so a single
116
+ * textarea of two lines made every cell of its row 20px too tall and drew them over the rows below: the row
117
+ * looked taller than its neighbours, and its focus ring and its edges bled out of it.
118
+ */
119
+ grid-template-rows: minmax(0, 1fr);
109
120
  height: var(--nx-tabular-row);
110
121
  border-bottom: 1px solid var(--nx-tabular-line);
111
122
  background: var(--nx-tabular-bg);
@@ -163,9 +174,24 @@
163
174
  display: flex;
164
175
  align-items: stretch;
165
176
  min-width: 0;
177
+ /* Nothing a cell holds is drawn past its own edges: the ring, the invalid edge and the tint all stop here. */
178
+ min-height: 0;
179
+ overflow: hidden;
166
180
  border-left: 1px solid var(--nx-tabular-line);
167
181
  }
168
182
 
183
+ /*
184
+ * A textarea scrolls inside its cell instead of making the cell taller. Its own `rows` would size the row, and
185
+ * its resize grip would let one cell break the alignment of its row: in a grid where every other cell keeps the
186
+ * line, that grip is a handle for making the table wrong, eighteen of them down one column.
187
+ */
188
+ .nx-tabular__cell textarea.nx-field__control {
189
+ height: 100%;
190
+ min-height: 0;
191
+ /* Two classes: the multi-line rule in Nexus.Input.css is two, and one would lose to it in the same layer. */
192
+ resize: none;
193
+ }
194
+
169
195
  .nx-tabular__cell > * {
170
196
  flex: 1 1 auto;
171
197
  min-width: 0;
@@ -227,10 +253,33 @@
227
253
  margin: 0;
228
254
  }
229
255
 
230
- .nx-tabular__cell--check:has(.nx-field__lock) {
256
+ /*
257
+ * A generated cell wears the read-only tint and no lock: the column header carries one lock for the whole
258
+ * column, and the cell still says why under the table when it is used. One fact, said once, not once a row.
259
+ */
260
+ .nx-tabular__cell .nx-field__lock {
261
+ display: none;
262
+ }
263
+
264
+ .nx-tabular__cell--check:has([data-readonly]) {
231
265
  background: var(--nx-tabular-readonly);
232
266
  }
233
267
 
268
+ .nx-tabular__head-cell[data-nx-locked] {
269
+ color: var(--nx-text-muted);
270
+ }
271
+
272
+ .nx-tabular__head-lock {
273
+ width: 13px;
274
+ height: 13px;
275
+ margin-left: auto;
276
+ color: var(--nx-text-muted);
277
+ }
278
+
279
+ .nx-tabular__head-cell.nx-tabular__cell--number .nx-tabular__head-lock {
280
+ margin-left: 4px;
281
+ }
282
+
234
283
  .nx-tabular__cell .nx-choices {
235
284
  display: flex;
236
285
  align-items: center;
@@ -244,14 +293,19 @@
244
293
  overflow: hidden;
245
294
  }
246
295
 
247
- /* Row numbers, fixed at the start: the button of each row's menu. Dots take the number's place on hover. */
296
+ /*
297
+ * Row numbers, fixed at the start, with the row's menu beside them. Both have their own place and neither
298
+ * moves: a menu that takes the number's place on hover makes the numbering look broken and the grid unstable.
299
+ */
248
300
  .nx-tabular__num {
249
301
  position: sticky;
250
302
  left: 0;
251
303
  z-index: 1;
252
- display: flex;
304
+ display: grid;
305
+ grid-template-columns: minmax(0, 1fr) var(--nx-tabular-menu);
253
306
  align-items: center;
254
- justify-content: center;
307
+ gap: 2px;
308
+ padding-left: 6px;
255
309
  border-right: 1px solid var(--nx-tabular-line);
256
310
  background: inherit;
257
311
  color: var(--nx-text-muted);
@@ -264,6 +318,11 @@
264
318
  font-weight: 600;
265
319
  }
266
320
 
321
+ /* Right-aligned, so 9 and 10 line up under one another and the menu is always the same distance away. */
322
+ .nx-tabular__num-text {
323
+ text-align: end;
324
+ }
325
+
267
326
  .nx-tabular__num + .nx-tabular__cell,
268
327
  .nx-tabular__num + .nx-tabular__head-cell {
269
328
  border-left: 0;
@@ -272,26 +331,24 @@
272
331
  .nx-tabular__num-button {
273
332
  display: grid;
274
333
  place-items: center;
275
- width: calc(100% - 6px);
276
- height: calc(100% - 6px);
334
+ width: var(--nx-tabular-menu);
335
+ height: calc(var(--nx-tabular-row) - 8px);
277
336
  padding: 0;
278
337
  border: 0;
279
338
  border-radius: var(--nx-radius-sm);
280
339
  background: none;
281
- color: inherit;
340
+ /* Muted, like the numbers beside it, so the column reads as one thing; it is the row's only control now. */
341
+ color: var(--nx-text-muted);
282
342
  font: inherit;
283
343
  cursor: pointer;
344
+ transition:
345
+ background-color var(--nx-duration),
346
+ color var(--nx-duration);
284
347
  }
285
348
 
286
- .nx-tabular__num-button > * {
287
- grid-area: 1 / 1;
288
- transition: opacity var(--nx-duration);
289
- }
290
-
291
- .nx-tabular__num-icon {
292
- width: 16px;
293
- height: 16px;
294
- opacity: 0;
349
+ .nx-tabular__num-button .nx-icon {
350
+ width: 15px;
351
+ height: 15px;
295
352
  }
296
353
 
297
354
  .nx-tabular__num-button:is(:hover, :focus-visible, [aria-expanded="true"]) {
@@ -299,16 +356,6 @@
299
356
  color: var(--nx-text);
300
357
  }
301
358
 
302
- .nx-tabular__row:hover .nx-tabular__num-icon,
303
- .nx-tabular__num-button:is(:focus-visible, [aria-expanded="true"]) .nx-tabular__num-icon {
304
- opacity: 1;
305
- }
306
-
307
- .nx-tabular__row:hover .nx-tabular__num-button .nx-tabular__num-text,
308
- .nx-tabular__num-button:is(:focus-visible, [aria-expanded="true"]) .nx-tabular__num-text {
309
- opacity: 0;
310
- }
311
-
312
359
  .nx-tabular__num-button:focus-visible {
313
360
  outline: 2px solid var(--nx-primary);
314
361
  outline-offset: -2px;
@@ -321,23 +368,6 @@
321
368
  font-weight: 600;
322
369
  }
323
370
 
324
- /* Remove buttons, fixed at the end. */
325
- .nx-tabular__end {
326
- position: sticky;
327
- right: 0;
328
- z-index: 1;
329
- display: flex;
330
- align-items: center;
331
- justify-content: center;
332
- border-left: 1px solid var(--nx-tabular-line);
333
- background: inherit;
334
- }
335
-
336
- .nx-tabular__remove.nx-btn {
337
- width: calc(var(--nx-tabular-row) - 9px);
338
- height: calc(var(--nx-tabular-row) - 9px);
339
- color: var(--nx-text-muted);
340
- }
341
371
 
342
372
  .nx-tabular__row--empty {
343
373
  display: block;
@@ -557,7 +587,7 @@
557
587
 
558
588
  @media (prefers-reduced-motion: reduce) {
559
589
  .nx-tabular__viewport,
560
- .nx-tabular__num-button > *,
590
+ .nx-tabular__num-button,
561
591
  .nx-group-form__toggle,
562
592
  .nx-group-form__chevron {
563
593
  transition: none;
@@ -219,6 +219,55 @@
219
219
  text-overflow: ellipsis;
220
220
  }
221
221
 
222
+ /*
223
+ * multiple: several at once. There is no single place for a sliding indicator, so every chosen option is raised on
224
+ * its own, exactly as the one choice's indicator is, and an option that is not chosen sits flat on the track. The
225
+ * options wrap onto more rows when they do not fit, each row as tall as a one-row track's options, so a picker of
226
+ * 26 letters is one track of several rows rather than a line that runs off a phone. A one-character option is
227
+ * square, so a row of letters lines up.
228
+ */
229
+ .nx-tabs--multiple {
230
+ --nx-tabs-option-height: calc(var(--nx-tabs-height) - 2 * var(--nx-tabs-pad) - 2px);
231
+
232
+ display: inline-flex;
233
+ flex-wrap: wrap;
234
+ gap: 2px;
235
+ max-width: 100%;
236
+ height: auto;
237
+ }
238
+
239
+ .nx-tabs--multiple.nx-tabs--block {
240
+ display: flex;
241
+ }
242
+
243
+ .nx-tabs--multiple .nx-tabs__option {
244
+ flex: 0 0 auto;
245
+ min-width: var(--nx-tabs-option-height);
246
+ height: var(--nx-tabs-option-height);
247
+ }
248
+
249
+ /* A chosen option can be unchosen, so it keeps the pointer, and it keeps its raised look under the pointer too. */
250
+ .nx-tabs--multiple .nx-tabs__option:has(:checked) {
251
+ background: var(--nx-tabs-indicator);
252
+ box-shadow:
253
+ inset 0 0 0 1px var(--nx-tabs-indicator-border),
254
+ var(--nx-tabs-indicator-shadow);
255
+ cursor: pointer;
256
+ }
257
+
258
+ .nx-tabs--multiple .nx-tabs__option:has(:checked):has(:focus-visible) {
259
+ box-shadow:
260
+ inset 0 0 0 1px var(--nx-tabs-indicator-border),
261
+ var(--nx-focus-ring);
262
+ }
263
+
264
+ /* A finger needs more than a small button's 22px: on a touch screen every option is a medium control's height, square at the least. */
265
+ @media (pointer: coarse) {
266
+ .nx-tabs--multiple {
267
+ --nx-tabs-option-height: var(--nx-control-md);
268
+ }
269
+ }
270
+
222
271
  /*
223
272
  * design="list": a vertical list of rows, for settings pages and side panels. There is no track: rows sit on the
224
273
  * panel, the selected row is tinted (darker in light mode, lighter in dark mode) and marked by a bar at its start,
@@ -280,6 +329,11 @@
280
329
  background: var(--nx-tabs-list-selected);
281
330
  }
282
331
 
332
+ /* A list of several choices: a chosen row can be unchosen, so it keeps the pointer. */
333
+ .nx-tabs--list:not([role="radiogroup"]) .nx-tabs__option:has(:checked) {
334
+ cursor: pointer;
335
+ }
336
+
283
337
  .nx-tabs--list .nx-tabs__option:has(:checked)::before {
284
338
  opacity: 1;
285
339
  transform: none;
@@ -356,6 +410,7 @@
356
410
 
357
411
  @media (prefers-reduced-motion: reduce) {
358
412
  .nx-tabs__indicator,
413
+ .nx-tabs--multiple .nx-tabs__option,
359
414
  .nx-tabs--labels-selected :is(.nx-tabs__option, .nx-tabs__label),
360
415
  .nx-tabs--list .nx-tabs__option::before {
361
416
  transition: none;
@@ -13,6 +13,12 @@
13
13
  --nx-table-flash: color-mix(in oklab, var(--nx-success) 18%, var(--nx-surface));
14
14
  --nx-table-line: var(--nx-border);
15
15
  --nx-table-head-h: 40px;
16
+ /* The band of group headings above the columns' own, when any column names a group. */
17
+ --nx-table-group-h: 30px;
18
+ /* How many rows the header has: 1, or 2 while a band stands above the headings (`data-head-rows`). */
19
+ --nx-table-head-rows: 1;
20
+ /* The whole header, which the loading bar and the selection bar are measured from. */
21
+ --nx-table-head-all: calc(var(--nx-table-head-h) + (var(--nx-table-head-rows) - 1) * var(--nx-table-group-h));
16
22
  --nx-table-row-h: 44px;
17
23
  --nx-table-pad-x: 12px;
18
24
  --nx-table-select-w: 44px;
@@ -27,8 +33,13 @@
27
33
  color: var(--nx-text);
28
34
  }
29
35
 
36
+ .nx-table[data-head-rows="2"] {
37
+ --nx-table-head-rows: 2;
38
+ }
39
+
30
40
  .nx-table--sm {
31
41
  --nx-table-head-h: 34px;
42
+ --nx-table-group-h: 26px;
32
43
  --nx-table-row-h: 36px;
33
44
  --nx-table-pad-x: 10px;
34
45
  --nx-table-select-w: 38px;
@@ -103,6 +114,30 @@
103
114
  }
104
115
  }
105
116
 
117
+ /*
118
+ * `fill`: the table takes the height it is given instead of growing with its rows. The bar and the footer keep their
119
+ * own height, the frame takes the rest, and the rows scroll inside it. Nothing below the table moves when a page
120
+ * holds fewer rows, and the page behind it never gains a scrollbar.
121
+ */
122
+ .nx-table--fill {
123
+ height: 100%;
124
+ min-height: 0;
125
+ }
126
+
127
+ .nx-table--fill > .nx-table__frame {
128
+ display: flex;
129
+ flex: 1 1 auto;
130
+ flex-direction: column;
131
+ min-height: 0;
132
+ }
133
+
134
+ .nx-table--fill .nx-table__scroll {
135
+ flex: 1 1 auto;
136
+ min-height: 0;
137
+ /* The room for the scrollbar is kept, so the headings do not shift the moment the rows need one. */
138
+ scrollbar-gutter: stable;
139
+ }
140
+
106
141
  /* The frame. */
107
142
  .nx-table__frame {
108
143
  position: relative;
@@ -157,6 +192,11 @@
157
192
  --nx-row-bg: var(--nx-table-hover);
158
193
  }
159
194
 
195
+ /* A row a click opens says so under the pointer. Only the pointer changes: nothing in the row moves on hover. */
196
+ .nx-table__row[data-open] {
197
+ cursor: pointer;
198
+ }
199
+
160
200
  .nx-table__row[data-selected] {
161
201
  --nx-row-bg: var(--nx-table-selected);
162
202
  }
@@ -303,14 +343,15 @@
303
343
  }
304
344
 
305
345
  .nx-table__row:hover > .nx-table__cell--index[data-swap] > .nx-table__check,
306
- .nx-table__head:hover > .nx-table__cell--index[data-swap] > .nx-table__check,
346
+ /* The heading's own cell may stand in the band row, so the whole header counts as hovering it. */
347
+ .nx-table__thead:hover .nx-table__cell--index[data-swap] > .nx-table__check,
307
348
  .nx-table__cell--index[data-swap]:focus-within > .nx-table__check,
308
349
  .nx-table[data-selecting] .nx-table__cell--index[data-swap] > .nx-table__check {
309
350
  opacity: 1;
310
351
  }
311
352
 
312
353
  .nx-table__row:hover > .nx-table__cell--index[data-swap] > .nx-table__number,
313
- .nx-table__head:hover > .nx-table__cell--index[data-swap] > .nx-table__number,
354
+ .nx-table__thead:hover .nx-table__cell--index[data-swap] > .nx-table__number,
314
355
  .nx-table__cell--index[data-swap]:focus-within > .nx-table__number,
315
356
  .nx-table[data-selecting] .nx-table__cell--index[data-swap] > .nx-table__number {
316
357
  opacity: 0;
@@ -462,6 +503,42 @@
462
503
  padding: 0;
463
504
  }
464
505
 
506
+ /*
507
+ * Columns in bands: a group's heading spans the columns next to each other that name it, and the columns with no
508
+ * group keep one heading as tall as both rows. Both rows stay while the rows scroll, the band on top.
509
+ */
510
+ .nx-table__head--leaves > .nx-table__heading {
511
+ top: var(--nx-table-group-h);
512
+ }
513
+
514
+ .nx-table__heading--group {
515
+ height: var(--nx-table-group-h);
516
+ border-bottom: 0;
517
+ text-align: center;
518
+ }
519
+
520
+ /* The line under a band stops short of its edges, so two bands beside each other read as two, not as one rule. */
521
+ .nx-table__heading--group::after {
522
+ content: "";
523
+ position: absolute;
524
+ right: var(--nx-table-pad-x);
525
+ bottom: 0;
526
+ left: var(--nx-table-pad-x);
527
+ height: 1px;
528
+ background: var(--nx-table-line);
529
+ }
530
+
531
+ .nx-table__group {
532
+ display: block;
533
+ overflow: hidden;
534
+ color: var(--nx-text);
535
+ font-size: var(--nx-text-xs);
536
+ font-weight: 700;
537
+ letter-spacing: 0.04em;
538
+ text-overflow: ellipsis;
539
+ text-transform: uppercase;
540
+ }
541
+
465
542
  .nx-table__heading-inner {
466
543
  display: flex;
467
544
  align-items: center;
@@ -775,7 +852,7 @@
775
852
  /* A thin bar under the header while a load runs with rows on screen. The rows dim only if it takes a moment. */
776
853
  .nx-table__progress {
777
854
  position: absolute;
778
- top: calc(var(--nx-table-head-h) - 1px);
855
+ top: calc(var(--nx-table-head-all) - 1px);
779
856
  right: 0;
780
857
  left: 0;
781
858
  z-index: 4;
@@ -827,7 +904,7 @@
827
904
  display: flex;
828
905
  align-items: center;
829
906
  gap: var(--nx-space-2);
830
- height: calc(var(--nx-table-head-h) - 1px);
907
+ height: calc(var(--nx-table-head-all) - 1px);
831
908
  padding: 0 var(--nx-space-2) 0 2px;
832
909
  background: var(--nx-table-selected);
833
910
  }
@@ -1011,4 +1088,209 @@
1011
1088
  opacity: 0.6;
1012
1089
  }
1013
1090
  }
1091
+
1092
+ /*
1093
+ * The Columns panel: a checkbox per column and a handle to drag it into another place. It shares the .nx-popover
1094
+ * panel with the menu, and holds a list rather than actions, so a click inside it never closes it.
1095
+ */
1096
+ .nx-columns {
1097
+ --nx-columns-row-h: 32px;
1098
+ position: fixed;
1099
+ inset: auto;
1100
+ min-width: 232px;
1101
+ max-width: min(320px, calc(100% - 16px));
1102
+ max-height: calc(100vh - 16px);
1103
+ padding: var(--nx-space-2);
1104
+ outline: none;
1105
+ }
1106
+
1107
+ .nx-columns--sm {
1108
+ --nx-columns-row-h: 28px;
1109
+ font-size: var(--nx-text-sm);
1110
+ }
1111
+
1112
+ /* A shut popover is hidden by the browser, so the panel takes its layout only while it is open. */
1113
+ .nx-columns:popover-open {
1114
+ display: flex;
1115
+ flex-direction: column;
1116
+ animation: nx-menu-in 120ms ease-out;
1117
+ }
1118
+
1119
+ .nx-columns__head {
1120
+ display: flex;
1121
+ align-items: baseline;
1122
+ justify-content: space-between;
1123
+ gap: var(--nx-space-3);
1124
+ padding: 2px var(--nx-space-2) 6px;
1125
+ }
1126
+
1127
+ .nx-columns__title {
1128
+ font-weight: 600;
1129
+ }
1130
+
1131
+ .nx-columns__count {
1132
+ color: var(--nx-text-muted);
1133
+ font-size: var(--nx-text-xs);
1134
+ font-variant-numeric: tabular-nums;
1135
+ }
1136
+
1137
+ .nx-columns__list {
1138
+ display: flex;
1139
+ flex: 1 1 auto;
1140
+ flex-direction: column;
1141
+ min-height: 0;
1142
+ margin: 0;
1143
+ padding: 0;
1144
+ overflow: hidden auto;
1145
+ overscroll-behavior: contain;
1146
+ list-style: none;
1147
+ }
1148
+
1149
+ .nx-columns__row {
1150
+ display: flex;
1151
+ align-items: center;
1152
+ gap: 6px;
1153
+ min-height: var(--nx-columns-row-h);
1154
+ padding: 0 var(--nx-space-2) 0 2px;
1155
+ border: 1px solid transparent;
1156
+ border-radius: var(--nx-radius);
1157
+ }
1158
+
1159
+ .nx-columns__row:hover {
1160
+ background: var(--nx-surface-hover);
1161
+ }
1162
+
1163
+ /* The row being dragged stays in place and goes quiet; a line shows where it would land. */
1164
+ .nx-columns__row[data-held] {
1165
+ background: var(--nx-surface-hover);
1166
+ opacity: 0.6;
1167
+ }
1168
+
1169
+ .nx-columns__row[data-drop] {
1170
+ border-color: var(--nx-primary);
1171
+ }
1172
+
1173
+ /* A band: its own line, with the columns under it indented along a line that says where the band ends. */
1174
+ .nx-columns__row--band {
1175
+ display: block;
1176
+ padding: 0;
1177
+ }
1178
+
1179
+ .nx-columns__row--band:hover {
1180
+ background: none;
1181
+ }
1182
+
1183
+ .nx-columns__band {
1184
+ display: flex;
1185
+ align-items: center;
1186
+ gap: 6px;
1187
+ min-height: var(--nx-columns-row-h);
1188
+ padding: 0 var(--nx-space-2) 0 2px;
1189
+ border-radius: var(--nx-radius);
1190
+ }
1191
+
1192
+ .nx-columns__band:hover {
1193
+ background: var(--nx-surface-hover);
1194
+ }
1195
+
1196
+ .nx-columns__band .nx-columns__label {
1197
+ font-weight: 600;
1198
+ }
1199
+
1200
+ .nx-columns__band-count {
1201
+ flex: none;
1202
+ color: var(--nx-text-subtle);
1203
+ font-size: var(--nx-text-xs);
1204
+ font-variant-numeric: tabular-nums;
1205
+ }
1206
+
1207
+ .nx-columns__children {
1208
+ margin: 0 0 2px 13px;
1209
+ padding: 0 0 0 9px;
1210
+ border-left: 1px solid var(--nx-border);
1211
+ list-style: none;
1212
+ }
1213
+
1214
+ /* A column whose band is hidden stays readable but plainly off. */
1215
+ .nx-columns__row--child:has(input:disabled) .nx-columns__label {
1216
+ color: var(--nx-text-subtle);
1217
+ }
1218
+
1219
+ .nx-columns__handle {
1220
+ display: inline-flex;
1221
+ flex: none;
1222
+ align-items: center;
1223
+ justify-content: center;
1224
+ width: 20px;
1225
+ height: var(--nx-columns-row-h);
1226
+ padding: 0;
1227
+ border: 0;
1228
+ border-radius: var(--nx-radius-sm);
1229
+ background: none;
1230
+ color: var(--nx-text-subtle);
1231
+ cursor: grab;
1232
+ touch-action: none;
1233
+ }
1234
+
1235
+ .nx-columns__handle:hover {
1236
+ color: var(--nx-text);
1237
+ }
1238
+
1239
+ .nx-columns__handle:focus-visible {
1240
+ outline: none;
1241
+ box-shadow: var(--nx-focus-ring);
1242
+ }
1243
+
1244
+ .nx-columns[data-dragging] .nx-columns__handle {
1245
+ cursor: grabbing;
1246
+ }
1247
+
1248
+ .nx-columns__handle--fixed {
1249
+ cursor: default;
1250
+ }
1251
+
1252
+ .nx-columns__choice {
1253
+ flex: 1 1 auto;
1254
+ min-width: 0;
1255
+ padding: 2px 0;
1256
+ }
1257
+
1258
+ .nx-columns__label {
1259
+ overflow: hidden;
1260
+ text-overflow: ellipsis;
1261
+ white-space: nowrap;
1262
+ }
1263
+
1264
+ .nx-columns__pin {
1265
+ display: inline-flex;
1266
+ flex: none;
1267
+ color: var(--nx-text-subtle);
1268
+ }
1269
+
1270
+ .nx-columns__pin .nx-icon {
1271
+ width: 14px;
1272
+ height: 14px;
1273
+ }
1274
+
1275
+ .nx-columns__note {
1276
+ margin: 6px 0 0;
1277
+ padding: 0 var(--nx-space-2);
1278
+ color: var(--nx-text-muted);
1279
+ font-size: var(--nx-text-xs);
1280
+ line-height: 1.4;
1281
+ }
1282
+
1283
+ .nx-columns__foot {
1284
+ display: flex;
1285
+ justify-content: flex-end;
1286
+ margin-top: var(--nx-space-2);
1287
+ padding-top: var(--nx-space-2);
1288
+ border-top: 1px solid var(--nx-border);
1289
+ }
1290
+
1291
+ @media (prefers-reduced-motion: reduce) {
1292
+ .nx-columns:popover-open {
1293
+ animation: none;
1294
+ }
1295
+ }
1014
1296
  }