nexus-shared 3.1.0 → 3.2.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 (104) hide show
  1. package/CHANGELOG.md +704 -446
  2. package/LICENSE +21 -21
  3. package/README.md +44 -5
  4. package/dist/Client.Index.d.ts +3 -0
  5. package/dist/Client.Index.js +3 -0
  6. package/dist/Components/Forms/Crud.js +322 -161
  7. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  8. package/dist/Components/Forms/CrudHistory.js +140 -0
  9. package/dist/Components/Forms/CrudParts.js +2 -1
  10. package/dist/Components/Forms/Form.js +4 -1
  11. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  12. package/dist/Components/Inputs/CodeBox.js +316 -0
  13. package/dist/Components/Inputs/DatePicker.js +4 -1
  14. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  15. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  16. package/dist/Components/Inputs/Dropdown.js +4 -1
  17. package/dist/Components/Inputs/InputRenderer.js +3 -0
  18. package/dist/Components/Inputs/NumberBox.js +4 -2
  19. package/dist/Components/Inputs/TextArea.js +4 -1
  20. package/dist/Components/Inputs/TextBox.js +4 -1
  21. package/dist/Components/Inputs/TimePicker.js +4 -1
  22. package/dist/Components/Inputs/TreeView.d.ts +55 -5
  23. package/dist/Components/Inputs/TreeView.js +372 -23
  24. package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
  25. package/dist/Components/Layouts/ThemePicker.js +16 -4
  26. package/dist/Components/Viewers/DataTable.js +63 -21
  27. package/dist/Components/Viewers/DataTableParts.d.ts +26 -8
  28. package/dist/Components/Viewers/DataTableParts.js +39 -10
  29. package/dist/Helpers/ApiModules.d.ts +3 -34
  30. package/dist/Helpers/ApiModules.js +3 -3
  31. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  32. package/dist/Helpers/ApiRoutes.js +31 -27
  33. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  34. package/dist/Helpers/ArrayHelpers.js +2 -0
  35. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  36. package/dist/Helpers/CodeHelpers.js +332 -0
  37. package/dist/Helpers/CrudBackend.d.ts +94 -4
  38. package/dist/Helpers/CrudBackend.js +168 -9
  39. package/dist/Helpers/CrudColumns.d.ts +51 -0
  40. package/dist/Helpers/CrudColumns.js +69 -0
  41. package/dist/Helpers/CrudHelpers.d.ts +50 -5
  42. package/dist/Helpers/CrudHelpers.js +109 -12
  43. package/dist/Helpers/InputParamsHelpers.js +8 -0
  44. package/dist/Helpers/MessageBuilder.js +2 -0
  45. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  46. package/dist/Helpers/NexusHelpers.js +1 -0
  47. package/dist/Helpers/RequestCache.d.ts +35 -0
  48. package/dist/Helpers/RequestCache.js +113 -0
  49. package/dist/Helpers/TableExport.js +12 -12
  50. package/dist/Helpers/TableHelpers.d.ts +1 -1
  51. package/dist/Helpers/TableHelpers.js +7 -2
  52. package/dist/Helpers/TableJoins.d.ts +70 -0
  53. package/dist/Helpers/TableJoins.js +250 -0
  54. package/dist/Helpers/TableStore.d.ts +5 -0
  55. package/dist/Helpers/TableStore.js +9 -0
  56. package/dist/Helpers/TreeStore.d.ts +225 -11
  57. package/dist/Helpers/TreeStore.js +683 -90
  58. package/dist/Helpers/UserPreferences.d.ts +8 -0
  59. package/dist/Helpers/UserPreferences.js +17 -0
  60. package/dist/Interfaces/CrudInterfaces.d.ts +47 -9
  61. package/dist/Interfaces/FormInterfaces.d.ts +4 -1
  62. package/dist/Interfaces/InputInterfaces.d.ts +24 -0
  63. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  64. package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
  65. package/dist/Interfaces/PreferenceInterfaces.js +2 -5
  66. package/dist/Interfaces/TableInterfaces.d.ts +86 -4
  67. package/dist/Services/ApiProxy.d.ts +36 -1
  68. package/dist/Services/ApiProxy.js +182 -52
  69. package/dist/Services/ApiProxyRules.d.ts +203 -0
  70. package/dist/Services/ApiProxyRules.js +426 -0
  71. package/dist/Services/JoinSources.d.ts +48 -0
  72. package/dist/Services/JoinSources.js +74 -0
  73. package/dist/Services/ServerApi.d.ts +13 -2
  74. package/dist/Services/ServerApi.js +8 -3
  75. package/dist/Shared.Index.d.ts +4 -0
  76. package/dist/Shared.Index.js +4 -0
  77. package/package.json +87 -85
  78. package/src/Styles/Nexus.Avatar.css +79 -79
  79. package/src/Styles/Nexus.Base.css +117 -117
  80. package/src/Styles/Nexus.Button.css +180 -180
  81. package/src/Styles/Nexus.Calendar.css +273 -273
  82. package/src/Styles/Nexus.Chat.css +1443 -1443
  83. package/src/Styles/Nexus.Choice.css +343 -343
  84. package/src/Styles/Nexus.Crud.css +168 -168
  85. package/src/Styles/Nexus.DateTime.css +81 -81
  86. package/src/Styles/Nexus.Dropdown.css +725 -725
  87. package/src/Styles/Nexus.Editor.css +295 -295
  88. package/src/Styles/Nexus.Excel.css +389 -389
  89. package/src/Styles/Nexus.Form.css +279 -279
  90. package/src/Styles/Nexus.Index.css +37 -37
  91. package/src/Styles/Nexus.Input.css +196 -4
  92. package/src/Styles/Nexus.Markdown.css +237 -237
  93. package/src/Styles/Nexus.Menu.css +237 -237
  94. package/src/Styles/Nexus.Popup.css +724 -724
  95. package/src/Styles/Nexus.Rows.css +637 -637
  96. package/src/Styles/Nexus.Slider.css +276 -276
  97. package/src/Styles/Nexus.Tab.Buttons.css +432 -432
  98. package/src/Styles/Nexus.Table.css +31 -30
  99. package/src/Styles/Nexus.Theme.Picker.css +59 -3
  100. package/src/Styles/Nexus.Themes.css +194 -194
  101. package/src/Styles/Nexus.Time.css +144 -144
  102. package/src/Styles/Nexus.Toast.css +263 -263
  103. package/src/Styles/Nexus.Tokens.css +89 -89
  104. package/src/Styles/Nexus.Tree.css +500 -431
@@ -1,432 +1,432 @@
1
- @layer nexus.components {
2
- /*
3
- * Tab buttons: a track, and an indicator that slides to the selected option.
4
- * Light mode: a soft tint of the text color with no frame, so the track suits any panel, and a white indicator
5
- * lifted by a soft shadow instead of a hard border.
6
- * Dark mode: the raised panel color with an edge, and an indicator lighter than the track (never the darker panel
7
- * color), so the selection reads at a glance in every theme.
8
- */
9
- .nx-tabs {
10
- --nx-tabs-height: var(--nx-control-md);
11
- --nx-tabs-pad: 3px;
12
- --nx-tabs-radius: calc(var(--nx-radius) + 2px);
13
- --nx-tabs-track: light-dark(color-mix(in oklab, var(--nx-text) 5%, transparent), var(--nx-surface-2));
14
- --nx-tabs-track-border: light-dark(transparent, var(--nx-border));
15
- --nx-tabs-track-shadow: inset 0 1px 2px light-dark(transparent, rgb(0 0 0 / 0.35));
16
- /* Lighter than the panel in light themes too, whose panels are not always white (Nord, Solarized, Dracula). */
17
- --nx-tabs-indicator: light-dark(color-mix(in oklab, var(--nx-surface), #fff 65%), color-mix(in oklab, var(--nx-surface-hover), var(--nx-text) 12%));
18
- --nx-tabs-indicator-border: light-dark(transparent, color-mix(in oklab, var(--nx-border-strong), var(--nx-text) 18%));
19
- --nx-tabs-indicator-shadow:
20
- 0 1px 2px light-dark(rgb(16 24 40 / 0.1), rgb(0 0 0 / 0.5)), 0 1px 3px light-dark(rgb(16 24 40 / 0.06), rgb(0 0 0 / 0.3)),
21
- 0 0 0 1px light-dark(rgb(16 24 40 / 0.04), transparent);
22
- --nx-tabs-hover: color-mix(in oklab, var(--nx-text) 5%, transparent);
23
- /* Unselected labels: a little darker than muted text in light mode, to keep AA contrast on the tinted track. */
24
- --nx-tabs-color: light-dark(color-mix(in oklab, var(--nx-text-muted), var(--nx-text) 20%), var(--nx-text-muted));
25
- --nx-tabs-active-color: var(--nx-text);
26
- --nx-tabs-active-icon: var(--nx-primary);
27
-
28
- position: relative;
29
- isolation: isolate;
30
- display: inline-grid;
31
- grid-auto-columns: 1fr;
32
- grid-auto-flow: column;
33
- height: var(--nx-tabs-height);
34
- padding: var(--nx-tabs-pad);
35
- border: 1px solid var(--nx-tabs-track-border);
36
- border-radius: var(--nx-tabs-radius);
37
- background: var(--nx-tabs-track);
38
- box-shadow: var(--nx-tabs-track-shadow);
39
- vertical-align: middle;
40
- }
41
-
42
- .nx-tabs--block {
43
- display: grid;
44
- width: 100%;
45
- }
46
-
47
- /* Primary: a solid primary indicator with its own text color, for a choice that matters, such as a plan. */
48
- .nx-tabs--primary {
49
- --nx-tabs-indicator: var(--nx-primary-solid);
50
- --nx-tabs-indicator-border: transparent;
51
- --nx-tabs-indicator-shadow: 0 1px 2px color-mix(in oklab, var(--nx-primary) 35%, transparent);
52
- --nx-tabs-active-color: var(--nx-primary-text);
53
- --nx-tabs-active-icon: var(--nx-primary-text);
54
- }
55
-
56
- /* One column wide; --nx-tabs-count and --nx-tabs-index come from the component. */
57
- .nx-tabs__indicator {
58
- position: absolute;
59
- z-index: -1;
60
- top: var(--nx-tabs-pad);
61
- bottom: var(--nx-tabs-pad);
62
- left: var(--nx-tabs-pad);
63
- width: calc((100% - 2 * var(--nx-tabs-pad)) / var(--nx-tabs-count));
64
- border: 1px solid var(--nx-tabs-indicator-border);
65
- border-radius: calc(var(--nx-tabs-radius) - 2px);
66
- background: var(--nx-tabs-indicator);
67
- box-shadow: var(--nx-tabs-indicator-shadow);
68
- transform: translateX(calc(100% * var(--nx-tabs-index)));
69
- transition:
70
- transform 240ms cubic-bezier(0.2, 0, 0, 1),
71
- background-color var(--nx-duration);
72
- }
73
-
74
- .nx-tabs__option {
75
- display: inline-flex;
76
- align-items: center;
77
- justify-content: center;
78
- gap: 6px;
79
- min-width: 0;
80
- padding: 0 var(--nx-space-3);
81
- border-radius: calc(var(--nx-tabs-radius) - 2px);
82
- color: var(--nx-tabs-color);
83
- font-size: var(--nx-text-sm);
84
- font-weight: 500;
85
- line-height: 1.3;
86
- white-space: nowrap;
87
- cursor: pointer;
88
- user-select: none;
89
- transition:
90
- color var(--nx-duration),
91
- background-color var(--nx-duration);
92
- }
93
-
94
- /* Room below the baseline, so the ellipsis clip never cuts letters such as y and g. */
95
- .nx-tabs__label {
96
- overflow: hidden;
97
- padding-block: 2px;
98
- text-overflow: ellipsis;
99
- }
100
-
101
- .nx-tabs__option .nx-icon {
102
- width: 1.15em;
103
- height: 1.15em;
104
- vertical-align: 0;
105
- transition: color var(--nx-duration);
106
- }
107
-
108
- .nx-tabs__option:hover:not(:has(:checked, :disabled)) {
109
- background: var(--nx-tabs-hover);
110
- color: var(--nx-text);
111
- }
112
-
113
- .nx-tabs__option:has(:checked) {
114
- color: var(--nx-tabs-active-color);
115
- cursor: default;
116
- }
117
-
118
- .nx-tabs__option:has(:checked) .nx-icon {
119
- color: var(--nx-tabs-active-icon);
120
- }
121
-
122
- .nx-tabs__option:has(:focus-visible) {
123
- box-shadow: var(--nx-focus-ring);
124
- }
125
-
126
- .nx-tabs__option:has(:disabled) {
127
- cursor: not-allowed;
128
- opacity: 0.45;
129
- }
130
-
131
- /* Sizes */
132
- .nx-tabs--sm {
133
- --nx-tabs-height: var(--nx-control-sm);
134
- --nx-tabs-pad: 2px;
135
- --nx-tabs-radius: var(--nx-radius);
136
- }
137
-
138
- .nx-tabs--sm .nx-tabs__option {
139
- gap: 4px;
140
- padding: 0 var(--nx-space-2);
141
- font-size: var(--nx-text-xs);
142
- }
143
-
144
- .nx-tabs--lg {
145
- --nx-tabs-height: var(--nx-control-lg);
146
- --nx-tabs-pad: 4px;
147
- --nx-tabs-radius: var(--nx-radius-lg);
148
- }
149
-
150
- .nx-tabs--lg .nx-tabs__option {
151
- gap: var(--nx-space-2);
152
- padding: 0 var(--nx-space-4);
153
- font-size: var(--nx-text-md);
154
- }
155
-
156
- /*
157
- * labels="selected": icons only, and the selected option grows to show its label. Options differ in width,
158
- * so the selected option draws the indicator itself, and its label column animates from 0 to full width.
159
- */
160
- .nx-tabs--labels-selected {
161
- display: inline-flex;
162
- gap: 2px;
163
- }
164
-
165
- .nx-tabs--labels-selected.nx-tabs--block {
166
- display: flex;
167
- }
168
-
169
- /* Icon-only options are square: side padding centers the 1.15em icon in the option's height. */
170
- .nx-tabs--labels-selected .nx-tabs__option {
171
- flex: 0 0 auto;
172
- gap: 0;
173
- padding: 0 calc((var(--nx-tabs-height) - 2 * var(--nx-tabs-pad) - 2px - 1.15em) / 2);
174
- transition:
175
- color var(--nx-duration),
176
- background-color var(--nx-duration),
177
- box-shadow var(--nx-duration),
178
- padding 240ms cubic-bezier(0.2, 0, 0, 1);
179
- }
180
-
181
- .nx-tabs--labels-selected .nx-tabs__option:has(:checked) {
182
- padding: 0 var(--nx-space-3);
183
- background: var(--nx-tabs-indicator);
184
- box-shadow:
185
- inset 0 0 0 1px var(--nx-tabs-indicator-border),
186
- var(--nx-tabs-indicator-shadow);
187
- }
188
-
189
- /* In a full-width group, the selected option takes the free space. */
190
- .nx-tabs--labels-selected.nx-tabs--block .nx-tabs__option:has(:checked) {
191
- flex: 1 1 auto;
192
- }
193
-
194
- /* The label column and the space before it both animate from 0, so a hidden label takes no width at all. */
195
- .nx-tabs--labels-selected .nx-tabs__label {
196
- display: grid;
197
- grid-template-columns: 0fr;
198
- margin-inline-start: 0;
199
- padding-block: 0;
200
- transition:
201
- grid-template-columns 240ms cubic-bezier(0.2, 0, 0, 1),
202
- margin 240ms cubic-bezier(0.2, 0, 0, 1);
203
- }
204
-
205
- .nx-tabs--labels-selected .nx-tabs__option:has(:checked) .nx-tabs__label {
206
- grid-template-columns: 1fr;
207
- margin-inline-start: 6px;
208
- }
209
-
210
- /* An option without an icon always shows its label. */
211
- .nx-tabs--labels-selected .nx-tabs__option:not(:has(.nx-icon)) .nx-tabs__label {
212
- grid-template-columns: 1fr;
213
- }
214
-
215
- /*
216
- * `auto`: in a container wide enough for them, every option says what it is, as the chosen one does. 900px is a data
217
- * table's bar with its Add, four lists, Reload, ⋮ and a full search on one line, and room to spare.
218
- */
219
- @container (min-width: 900px) {
220
- .nx-tabs--labels-auto .nx-tabs__option {
221
- padding: 0 var(--nx-space-3);
222
- }
223
-
224
- .nx-tabs--labels-auto .nx-tabs__label {
225
- grid-template-columns: 1fr;
226
- margin-inline-start: 6px;
227
- }
228
- }
229
-
230
- .nx-tabs--labels-selected .nx-tabs__text {
231
- min-width: 0;
232
- overflow: hidden;
233
- padding-block: 2px;
234
- text-overflow: ellipsis;
235
- }
236
-
237
- /*
238
- * multiple: several at once. There is no single place for a sliding indicator, so every chosen option is raised on
239
- * its own, exactly as the one choice's indicator is, and an option that is not chosen sits flat on the track. The
240
- * 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
241
- * 26 letters is one track of several rows rather than a line that runs off a phone. A one-character option is
242
- * square, so a row of letters lines up.
243
- */
244
- .nx-tabs--multiple {
245
- --nx-tabs-option-height: calc(var(--nx-tabs-height) - 2 * var(--nx-tabs-pad) - 2px);
246
-
247
- display: inline-flex;
248
- flex-wrap: wrap;
249
- gap: 2px;
250
- max-width: 100%;
251
- height: auto;
252
- }
253
-
254
- .nx-tabs--multiple.nx-tabs--block {
255
- display: flex;
256
- }
257
-
258
- .nx-tabs--multiple .nx-tabs__option {
259
- flex: 0 0 auto;
260
- min-width: var(--nx-tabs-option-height);
261
- height: var(--nx-tabs-option-height);
262
- }
263
-
264
- /* A chosen option can be unchosen, so it keeps the pointer, and it keeps its raised look under the pointer too. */
265
- .nx-tabs--multiple .nx-tabs__option:has(:checked) {
266
- background: var(--nx-tabs-indicator);
267
- box-shadow:
268
- inset 0 0 0 1px var(--nx-tabs-indicator-border),
269
- var(--nx-tabs-indicator-shadow);
270
- cursor: pointer;
271
- }
272
-
273
- .nx-tabs--multiple .nx-tabs__option:has(:checked):has(:focus-visible) {
274
- box-shadow:
275
- inset 0 0 0 1px var(--nx-tabs-indicator-border),
276
- var(--nx-focus-ring);
277
- }
278
-
279
- /* 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. */
280
- @media (pointer: coarse) {
281
- .nx-tabs--multiple {
282
- --nx-tabs-option-height: var(--nx-control-md);
283
- }
284
- }
285
-
286
- /*
287
- * design="list": a vertical list of rows, for settings pages and side panels. There is no track: rows sit on the
288
- * panel, the selected row is tinted (darker in light mode, lighter in dark mode) and marked by a bar at its start,
289
- * so the choice reads at a glance in every theme, and the label turns bold as a second cue.
290
- */
291
- .nx-tabs--list {
292
- --nx-tabs-list-row: var(--nx-control-md);
293
- --nx-tabs-list-font: var(--nx-text-md);
294
- --nx-tabs-list-pad: 6px var(--nx-space-3);
295
- --nx-tabs-list-gap: 10px;
296
- --nx-tabs-list-selected: light-dark(color-mix(in oklab, var(--nx-text) 6%, transparent), color-mix(in oklab, var(--nx-text) 10%, transparent));
297
- --nx-tabs-list-bar: var(--nx-primary);
298
- --nx-tabs-list-badge: color-mix(in oklab, var(--nx-text) 8%, transparent);
299
- --nx-tabs-list-badge-color: var(--nx-text-muted);
300
-
301
- display: grid;
302
- grid-auto-flow: row;
303
- grid-auto-columns: auto;
304
- gap: 2px;
305
- width: 100%;
306
- height: auto;
307
- padding: 0;
308
- border: 0;
309
- border-radius: 0;
310
- background: none;
311
- box-shadow: none;
312
- }
313
-
314
- .nx-tabs--list .nx-tabs__option {
315
- position: relative;
316
- justify-content: flex-start;
317
- gap: var(--nx-tabs-list-gap);
318
- min-height: var(--nx-tabs-list-row);
319
- padding: var(--nx-tabs-list-pad);
320
- border-radius: var(--nx-radius);
321
- font-size: var(--nx-tabs-list-font);
322
- text-align: left;
323
- white-space: normal;
324
- }
325
-
326
- /* The bar at the start of the selected row grows in from the middle. */
327
- .nx-tabs--list .nx-tabs__option::before {
328
- content: "";
329
- position: absolute;
330
- top: 8px;
331
- bottom: 8px;
332
- left: 0;
333
- width: 3px;
334
- border-radius: 3px;
335
- background: var(--nx-tabs-list-bar);
336
- opacity: 0;
337
- transform: scaleY(0.3);
338
- transition:
339
- opacity var(--nx-duration),
340
- transform 200ms cubic-bezier(0.2, 0, 0, 1);
341
- }
342
-
343
- .nx-tabs--list .nx-tabs__option:has(:checked) {
344
- background: var(--nx-tabs-list-selected);
345
- }
346
-
347
- /* A list of several choices: a chosen row can be unchosen, so it keeps the pointer. */
348
- .nx-tabs--list:not([role="radiogroup"]) .nx-tabs__option:has(:checked) {
349
- cursor: pointer;
350
- }
351
-
352
- .nx-tabs--list .nx-tabs__option:has(:checked)::before {
353
- opacity: 1;
354
- transform: none;
355
- }
356
-
357
- .nx-tabs--list .nx-tabs__option:has(:checked) .nx-tabs__text {
358
- font-weight: 600;
359
- }
360
-
361
- .nx-tabs--list .nx-tabs__option .nx-icon {
362
- flex-shrink: 0;
363
- }
364
-
365
- .nx-tabs--list .nx-tabs__label {
366
- display: grid;
367
- flex: 1 1 auto;
368
- min-width: 0;
369
- padding-block: 0;
370
- }
371
-
372
- .nx-tabs--list .nx-tabs__text {
373
- overflow: hidden;
374
- line-height: 1.35;
375
- text-overflow: ellipsis;
376
- white-space: nowrap;
377
- }
378
-
379
- .nx-tabs__description {
380
- color: var(--nx-text-muted);
381
- font-size: var(--nx-text-xs);
382
- font-weight: 400;
383
- line-height: 1.4;
384
- }
385
-
386
- .nx-tabs__badge {
387
- flex-shrink: 0;
388
- min-width: 20px;
389
- padding: 0 7px;
390
- border-radius: var(--nx-radius-full);
391
- background: var(--nx-tabs-list-badge);
392
- color: var(--nx-tabs-list-badge-color);
393
- font-size: var(--nx-text-xs);
394
- font-weight: 600;
395
- font-variant-numeric: tabular-nums;
396
- line-height: 20px;
397
- text-align: center;
398
- }
399
-
400
- /* Primary: the selected row takes the primary soft background and the primary text. */
401
- .nx-tabs--list.nx-tabs--primary {
402
- --nx-tabs-list-selected: var(--nx-primary-soft);
403
- --nx-tabs-active-color: var(--nx-primary);
404
- --nx-tabs-active-icon: var(--nx-primary);
405
- }
406
-
407
- .nx-tabs--list.nx-tabs--primary .nx-tabs__option:has(:checked) .nx-tabs__badge {
408
- background: var(--nx-primary-solid);
409
- color: var(--nx-primary-text);
410
- }
411
-
412
- .nx-tabs--list.nx-tabs--sm {
413
- --nx-tabs-list-row: var(--nx-control-sm);
414
- --nx-tabs-list-font: var(--nx-text-sm);
415
- --nx-tabs-list-pad: 4px var(--nx-space-2);
416
- --nx-tabs-list-gap: 8px;
417
- }
418
-
419
- .nx-tabs--list.nx-tabs--lg {
420
- --nx-tabs-list-row: var(--nx-control-lg);
421
- --nx-tabs-list-font: var(--nx-text-lg);
422
- --nx-tabs-list-pad: 8px var(--nx-space-4);
423
- --nx-tabs-list-gap: 12px;
424
- }
425
-
426
- [data-nx-motion="reduce"] .nx-tabs__indicator,
427
- [data-nx-motion="reduce"] .nx-tabs--multiple .nx-tabs__option,
428
- [data-nx-motion="reduce"] .nx-tabs--labels-selected :is(.nx-tabs__option, .nx-tabs__label),
429
- [data-nx-motion="reduce"] .nx-tabs--list .nx-tabs__option::before {
430
- transition: none;
431
- }
432
- }
1
+ @layer nexus.components {
2
+ /*
3
+ * Tab buttons: a track, and an indicator that slides to the selected option.
4
+ * Light mode: a soft tint of the text color with no frame, so the track suits any panel, and a white indicator
5
+ * lifted by a soft shadow instead of a hard border.
6
+ * Dark mode: the raised panel color with an edge, and an indicator lighter than the track (never the darker panel
7
+ * color), so the selection reads at a glance in every theme.
8
+ */
9
+ .nx-tabs {
10
+ --nx-tabs-height: var(--nx-control-md);
11
+ --nx-tabs-pad: 3px;
12
+ --nx-tabs-radius: calc(var(--nx-radius) + 2px);
13
+ --nx-tabs-track: light-dark(color-mix(in oklab, var(--nx-text) 5%, transparent), var(--nx-surface-2));
14
+ --nx-tabs-track-border: light-dark(transparent, var(--nx-border));
15
+ --nx-tabs-track-shadow: inset 0 1px 2px light-dark(transparent, rgb(0 0 0 / 0.35));
16
+ /* Lighter than the panel in light themes too, whose panels are not always white (Nord, Solarized, Dracula). */
17
+ --nx-tabs-indicator: light-dark(color-mix(in oklab, var(--nx-surface), #fff 65%), color-mix(in oklab, var(--nx-surface-hover), var(--nx-text) 12%));
18
+ --nx-tabs-indicator-border: light-dark(transparent, color-mix(in oklab, var(--nx-border-strong), var(--nx-text) 18%));
19
+ --nx-tabs-indicator-shadow:
20
+ 0 1px 2px light-dark(rgb(16 24 40 / 0.1), rgb(0 0 0 / 0.5)), 0 1px 3px light-dark(rgb(16 24 40 / 0.06), rgb(0 0 0 / 0.3)),
21
+ 0 0 0 1px light-dark(rgb(16 24 40 / 0.04), transparent);
22
+ --nx-tabs-hover: color-mix(in oklab, var(--nx-text) 5%, transparent);
23
+ /* Unselected labels: a little darker than muted text in light mode, to keep AA contrast on the tinted track. */
24
+ --nx-tabs-color: light-dark(color-mix(in oklab, var(--nx-text-muted), var(--nx-text) 20%), var(--nx-text-muted));
25
+ --nx-tabs-active-color: var(--nx-text);
26
+ --nx-tabs-active-icon: var(--nx-primary);
27
+
28
+ position: relative;
29
+ isolation: isolate;
30
+ display: inline-grid;
31
+ grid-auto-columns: 1fr;
32
+ grid-auto-flow: column;
33
+ height: var(--nx-tabs-height);
34
+ padding: var(--nx-tabs-pad);
35
+ border: 1px solid var(--nx-tabs-track-border);
36
+ border-radius: var(--nx-tabs-radius);
37
+ background: var(--nx-tabs-track);
38
+ box-shadow: var(--nx-tabs-track-shadow);
39
+ vertical-align: middle;
40
+ }
41
+
42
+ .nx-tabs--block {
43
+ display: grid;
44
+ width: 100%;
45
+ }
46
+
47
+ /* Primary: a solid primary indicator with its own text color, for a choice that matters, such as a plan. */
48
+ .nx-tabs--primary {
49
+ --nx-tabs-indicator: var(--nx-primary-solid);
50
+ --nx-tabs-indicator-border: transparent;
51
+ --nx-tabs-indicator-shadow: 0 1px 2px color-mix(in oklab, var(--nx-primary) 35%, transparent);
52
+ --nx-tabs-active-color: var(--nx-primary-text);
53
+ --nx-tabs-active-icon: var(--nx-primary-text);
54
+ }
55
+
56
+ /* One column wide; --nx-tabs-count and --nx-tabs-index come from the component. */
57
+ .nx-tabs__indicator {
58
+ position: absolute;
59
+ z-index: -1;
60
+ top: var(--nx-tabs-pad);
61
+ bottom: var(--nx-tabs-pad);
62
+ left: var(--nx-tabs-pad);
63
+ width: calc((100% - 2 * var(--nx-tabs-pad)) / var(--nx-tabs-count));
64
+ border: 1px solid var(--nx-tabs-indicator-border);
65
+ border-radius: calc(var(--nx-tabs-radius) - 2px);
66
+ background: var(--nx-tabs-indicator);
67
+ box-shadow: var(--nx-tabs-indicator-shadow);
68
+ transform: translateX(calc(100% * var(--nx-tabs-index)));
69
+ transition:
70
+ transform 240ms cubic-bezier(0.2, 0, 0, 1),
71
+ background-color var(--nx-duration);
72
+ }
73
+
74
+ .nx-tabs__option {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ gap: 6px;
79
+ min-width: 0;
80
+ padding: 0 var(--nx-space-3);
81
+ border-radius: calc(var(--nx-tabs-radius) - 2px);
82
+ color: var(--nx-tabs-color);
83
+ font-size: var(--nx-text-sm);
84
+ font-weight: 500;
85
+ line-height: 1.3;
86
+ white-space: nowrap;
87
+ cursor: pointer;
88
+ user-select: none;
89
+ transition:
90
+ color var(--nx-duration),
91
+ background-color var(--nx-duration);
92
+ }
93
+
94
+ /* Room below the baseline, so the ellipsis clip never cuts letters such as y and g. */
95
+ .nx-tabs__label {
96
+ overflow: hidden;
97
+ padding-block: 2px;
98
+ text-overflow: ellipsis;
99
+ }
100
+
101
+ .nx-tabs__option .nx-icon {
102
+ width: 1.15em;
103
+ height: 1.15em;
104
+ vertical-align: 0;
105
+ transition: color var(--nx-duration);
106
+ }
107
+
108
+ .nx-tabs__option:hover:not(:has(:checked, :disabled)) {
109
+ background: var(--nx-tabs-hover);
110
+ color: var(--nx-text);
111
+ }
112
+
113
+ .nx-tabs__option:has(:checked) {
114
+ color: var(--nx-tabs-active-color);
115
+ cursor: default;
116
+ }
117
+
118
+ .nx-tabs__option:has(:checked) .nx-icon {
119
+ color: var(--nx-tabs-active-icon);
120
+ }
121
+
122
+ .nx-tabs__option:has(:focus-visible) {
123
+ box-shadow: var(--nx-focus-ring);
124
+ }
125
+
126
+ .nx-tabs__option:has(:disabled) {
127
+ cursor: not-allowed;
128
+ opacity: 0.45;
129
+ }
130
+
131
+ /* Sizes */
132
+ .nx-tabs--sm {
133
+ --nx-tabs-height: var(--nx-control-sm);
134
+ --nx-tabs-pad: 2px;
135
+ --nx-tabs-radius: var(--nx-radius);
136
+ }
137
+
138
+ .nx-tabs--sm .nx-tabs__option {
139
+ gap: 4px;
140
+ padding: 0 var(--nx-space-2);
141
+ font-size: var(--nx-text-xs);
142
+ }
143
+
144
+ .nx-tabs--lg {
145
+ --nx-tabs-height: var(--nx-control-lg);
146
+ --nx-tabs-pad: 4px;
147
+ --nx-tabs-radius: var(--nx-radius-lg);
148
+ }
149
+
150
+ .nx-tabs--lg .nx-tabs__option {
151
+ gap: var(--nx-space-2);
152
+ padding: 0 var(--nx-space-4);
153
+ font-size: var(--nx-text-md);
154
+ }
155
+
156
+ /*
157
+ * labels="selected": icons only, and the selected option grows to show its label. Options differ in width,
158
+ * so the selected option draws the indicator itself, and its label column animates from 0 to full width.
159
+ */
160
+ .nx-tabs--labels-selected {
161
+ display: inline-flex;
162
+ gap: 2px;
163
+ }
164
+
165
+ .nx-tabs--labels-selected.nx-tabs--block {
166
+ display: flex;
167
+ }
168
+
169
+ /* Icon-only options are square: side padding centers the 1.15em icon in the option's height. */
170
+ .nx-tabs--labels-selected .nx-tabs__option {
171
+ flex: 0 0 auto;
172
+ gap: 0;
173
+ padding: 0 calc((var(--nx-tabs-height) - 2 * var(--nx-tabs-pad) - 2px - 1.15em) / 2);
174
+ transition:
175
+ color var(--nx-duration),
176
+ background-color var(--nx-duration),
177
+ box-shadow var(--nx-duration),
178
+ padding 240ms cubic-bezier(0.2, 0, 0, 1);
179
+ }
180
+
181
+ .nx-tabs--labels-selected .nx-tabs__option:has(:checked) {
182
+ padding: 0 var(--nx-space-3);
183
+ background: var(--nx-tabs-indicator);
184
+ box-shadow:
185
+ inset 0 0 0 1px var(--nx-tabs-indicator-border),
186
+ var(--nx-tabs-indicator-shadow);
187
+ }
188
+
189
+ /* In a full-width group, the selected option takes the free space. */
190
+ .nx-tabs--labels-selected.nx-tabs--block .nx-tabs__option:has(:checked) {
191
+ flex: 1 1 auto;
192
+ }
193
+
194
+ /* The label column and the space before it both animate from 0, so a hidden label takes no width at all. */
195
+ .nx-tabs--labels-selected .nx-tabs__label {
196
+ display: grid;
197
+ grid-template-columns: 0fr;
198
+ margin-inline-start: 0;
199
+ padding-block: 0;
200
+ transition:
201
+ grid-template-columns 240ms cubic-bezier(0.2, 0, 0, 1),
202
+ margin 240ms cubic-bezier(0.2, 0, 0, 1);
203
+ }
204
+
205
+ .nx-tabs--labels-selected .nx-tabs__option:has(:checked) .nx-tabs__label {
206
+ grid-template-columns: 1fr;
207
+ margin-inline-start: 6px;
208
+ }
209
+
210
+ /* An option without an icon always shows its label. */
211
+ .nx-tabs--labels-selected .nx-tabs__option:not(:has(.nx-icon)) .nx-tabs__label {
212
+ grid-template-columns: 1fr;
213
+ }
214
+
215
+ /*
216
+ * `auto`: in a container wide enough for them, every option says what it is, as the chosen one does. 900px is a data
217
+ * table's bar with its Add, four lists, Reload, ⋮ and a full search on one line, and room to spare.
218
+ */
219
+ @container (min-width: 900px) {
220
+ .nx-tabs--labels-auto .nx-tabs__option {
221
+ padding: 0 var(--nx-space-3);
222
+ }
223
+
224
+ .nx-tabs--labels-auto .nx-tabs__label {
225
+ grid-template-columns: 1fr;
226
+ margin-inline-start: 6px;
227
+ }
228
+ }
229
+
230
+ .nx-tabs--labels-selected .nx-tabs__text {
231
+ min-width: 0;
232
+ overflow: hidden;
233
+ padding-block: 2px;
234
+ text-overflow: ellipsis;
235
+ }
236
+
237
+ /*
238
+ * multiple: several at once. There is no single place for a sliding indicator, so every chosen option is raised on
239
+ * its own, exactly as the one choice's indicator is, and an option that is not chosen sits flat on the track. The
240
+ * 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
241
+ * 26 letters is one track of several rows rather than a line that runs off a phone. A one-character option is
242
+ * square, so a row of letters lines up.
243
+ */
244
+ .nx-tabs--multiple {
245
+ --nx-tabs-option-height: calc(var(--nx-tabs-height) - 2 * var(--nx-tabs-pad) - 2px);
246
+
247
+ display: inline-flex;
248
+ flex-wrap: wrap;
249
+ gap: 2px;
250
+ max-width: 100%;
251
+ height: auto;
252
+ }
253
+
254
+ .nx-tabs--multiple.nx-tabs--block {
255
+ display: flex;
256
+ }
257
+
258
+ .nx-tabs--multiple .nx-tabs__option {
259
+ flex: 0 0 auto;
260
+ min-width: var(--nx-tabs-option-height);
261
+ height: var(--nx-tabs-option-height);
262
+ }
263
+
264
+ /* A chosen option can be unchosen, so it keeps the pointer, and it keeps its raised look under the pointer too. */
265
+ .nx-tabs--multiple .nx-tabs__option:has(:checked) {
266
+ background: var(--nx-tabs-indicator);
267
+ box-shadow:
268
+ inset 0 0 0 1px var(--nx-tabs-indicator-border),
269
+ var(--nx-tabs-indicator-shadow);
270
+ cursor: pointer;
271
+ }
272
+
273
+ .nx-tabs--multiple .nx-tabs__option:has(:checked):has(:focus-visible) {
274
+ box-shadow:
275
+ inset 0 0 0 1px var(--nx-tabs-indicator-border),
276
+ var(--nx-focus-ring);
277
+ }
278
+
279
+ /* 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. */
280
+ @media (pointer: coarse) {
281
+ .nx-tabs--multiple {
282
+ --nx-tabs-option-height: var(--nx-control-md);
283
+ }
284
+ }
285
+
286
+ /*
287
+ * design="list": a vertical list of rows, for settings pages and side panels. There is no track: rows sit on the
288
+ * panel, the selected row is tinted (darker in light mode, lighter in dark mode) and marked by a bar at its start,
289
+ * so the choice reads at a glance in every theme, and the label turns bold as a second cue.
290
+ */
291
+ .nx-tabs--list {
292
+ --nx-tabs-list-row: var(--nx-control-md);
293
+ --nx-tabs-list-font: var(--nx-text-md);
294
+ --nx-tabs-list-pad: 6px var(--nx-space-3);
295
+ --nx-tabs-list-gap: 10px;
296
+ --nx-tabs-list-selected: light-dark(color-mix(in oklab, var(--nx-text) 6%, transparent), color-mix(in oklab, var(--nx-text) 10%, transparent));
297
+ --nx-tabs-list-bar: var(--nx-primary);
298
+ --nx-tabs-list-badge: color-mix(in oklab, var(--nx-text) 8%, transparent);
299
+ --nx-tabs-list-badge-color: var(--nx-text-muted);
300
+
301
+ display: grid;
302
+ grid-auto-flow: row;
303
+ grid-auto-columns: auto;
304
+ gap: 2px;
305
+ width: 100%;
306
+ height: auto;
307
+ padding: 0;
308
+ border: 0;
309
+ border-radius: 0;
310
+ background: none;
311
+ box-shadow: none;
312
+ }
313
+
314
+ .nx-tabs--list .nx-tabs__option {
315
+ position: relative;
316
+ justify-content: flex-start;
317
+ gap: var(--nx-tabs-list-gap);
318
+ min-height: var(--nx-tabs-list-row);
319
+ padding: var(--nx-tabs-list-pad);
320
+ border-radius: var(--nx-radius);
321
+ font-size: var(--nx-tabs-list-font);
322
+ text-align: left;
323
+ white-space: normal;
324
+ }
325
+
326
+ /* The bar at the start of the selected row grows in from the middle. */
327
+ .nx-tabs--list .nx-tabs__option::before {
328
+ content: "";
329
+ position: absolute;
330
+ top: 8px;
331
+ bottom: 8px;
332
+ left: 0;
333
+ width: 3px;
334
+ border-radius: 3px;
335
+ background: var(--nx-tabs-list-bar);
336
+ opacity: 0;
337
+ transform: scaleY(0.3);
338
+ transition:
339
+ opacity var(--nx-duration),
340
+ transform 200ms cubic-bezier(0.2, 0, 0, 1);
341
+ }
342
+
343
+ .nx-tabs--list .nx-tabs__option:has(:checked) {
344
+ background: var(--nx-tabs-list-selected);
345
+ }
346
+
347
+ /* A list of several choices: a chosen row can be unchosen, so it keeps the pointer. */
348
+ .nx-tabs--list:not([role="radiogroup"]) .nx-tabs__option:has(:checked) {
349
+ cursor: pointer;
350
+ }
351
+
352
+ .nx-tabs--list .nx-tabs__option:has(:checked)::before {
353
+ opacity: 1;
354
+ transform: none;
355
+ }
356
+
357
+ .nx-tabs--list .nx-tabs__option:has(:checked) .nx-tabs__text {
358
+ font-weight: 600;
359
+ }
360
+
361
+ .nx-tabs--list .nx-tabs__option .nx-icon {
362
+ flex-shrink: 0;
363
+ }
364
+
365
+ .nx-tabs--list .nx-tabs__label {
366
+ display: grid;
367
+ flex: 1 1 auto;
368
+ min-width: 0;
369
+ padding-block: 0;
370
+ }
371
+
372
+ .nx-tabs--list .nx-tabs__text {
373
+ overflow: hidden;
374
+ line-height: 1.35;
375
+ text-overflow: ellipsis;
376
+ white-space: nowrap;
377
+ }
378
+
379
+ .nx-tabs__description {
380
+ color: var(--nx-text-muted);
381
+ font-size: var(--nx-text-xs);
382
+ font-weight: 400;
383
+ line-height: 1.4;
384
+ }
385
+
386
+ .nx-tabs__badge {
387
+ flex-shrink: 0;
388
+ min-width: 20px;
389
+ padding: 0 7px;
390
+ border-radius: var(--nx-radius-full);
391
+ background: var(--nx-tabs-list-badge);
392
+ color: var(--nx-tabs-list-badge-color);
393
+ font-size: var(--nx-text-xs);
394
+ font-weight: 600;
395
+ font-variant-numeric: tabular-nums;
396
+ line-height: 20px;
397
+ text-align: center;
398
+ }
399
+
400
+ /* Primary: the selected row takes the primary soft background and the primary text. */
401
+ .nx-tabs--list.nx-tabs--primary {
402
+ --nx-tabs-list-selected: var(--nx-primary-soft);
403
+ --nx-tabs-active-color: var(--nx-primary);
404
+ --nx-tabs-active-icon: var(--nx-primary);
405
+ }
406
+
407
+ .nx-tabs--list.nx-tabs--primary .nx-tabs__option:has(:checked) .nx-tabs__badge {
408
+ background: var(--nx-primary-solid);
409
+ color: var(--nx-primary-text);
410
+ }
411
+
412
+ .nx-tabs--list.nx-tabs--sm {
413
+ --nx-tabs-list-row: var(--nx-control-sm);
414
+ --nx-tabs-list-font: var(--nx-text-sm);
415
+ --nx-tabs-list-pad: 4px var(--nx-space-2);
416
+ --nx-tabs-list-gap: 8px;
417
+ }
418
+
419
+ .nx-tabs--list.nx-tabs--lg {
420
+ --nx-tabs-list-row: var(--nx-control-lg);
421
+ --nx-tabs-list-font: var(--nx-text-lg);
422
+ --nx-tabs-list-pad: 8px var(--nx-space-4);
423
+ --nx-tabs-list-gap: 12px;
424
+ }
425
+
426
+ [data-nx-motion="reduce"] .nx-tabs__indicator,
427
+ [data-nx-motion="reduce"] .nx-tabs--multiple .nx-tabs__option,
428
+ [data-nx-motion="reduce"] .nx-tabs--labels-selected :is(.nx-tabs__option, .nx-tabs__label),
429
+ [data-nx-motion="reduce"] .nx-tabs--list .nx-tabs__option::before {
430
+ transition: none;
431
+ }
432
+ }