nexus-shared 3.1.0 → 4.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 (234) hide show
  1. package/CHANGELOG.md +2268 -446
  2. package/LICENSE +21 -21
  3. package/README.md +1705 -1084
  4. package/dist/Client.Index.d.ts +17 -0
  5. package/dist/Client.Index.js +17 -0
  6. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  7. package/dist/Components/Documents/DocumentFrame.js +83 -0
  8. package/dist/Components/Documents/Menu.d.ts +4 -0
  9. package/dist/Components/Documents/Menu.js +40 -24
  10. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  11. package/dist/Components/Documents/PrintDocument.js +62 -0
  12. package/dist/Components/Documents/Tabs.d.ts +59 -0
  13. package/dist/Components/Documents/Tabs.js +70 -0
  14. package/dist/Components/Forms/ApiForm.js +19 -2
  15. package/dist/Components/Forms/Crud.d.ts +2 -2
  16. package/dist/Components/Forms/Crud.js +871 -377
  17. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  18. package/dist/Components/Forms/CrudActions.js +184 -0
  19. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  20. package/dist/Components/Forms/CrudHistory.js +140 -0
  21. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  22. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  23. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  24. package/dist/Components/Forms/CrudParts.js +56 -5
  25. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  26. package/dist/Components/Forms/CrudRowForms.js +114 -0
  27. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  28. package/dist/Components/Forms/CrudScopes.js +76 -0
  29. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  30. package/dist/Components/Forms/CrudTree.js +363 -0
  31. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  32. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  33. package/dist/Components/Forms/ExcelImport.js +3 -3
  34. package/dist/Components/Forms/Form.d.ts +4 -4
  35. package/dist/Components/Forms/Form.js +125 -16
  36. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  37. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  38. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  39. package/dist/Components/Forms/RecordFlows.js +282 -0
  40. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  41. package/dist/Components/Forms/SubmitForm.js +195 -10
  42. package/dist/Components/Inputs/CheckBox.js +1 -1
  43. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  44. package/dist/Components/Inputs/ChoiceField.js +28 -2
  45. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  46. package/dist/Components/Inputs/CodeBox.js +316 -0
  47. package/dist/Components/Inputs/DatePicker.js +4 -1
  48. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  49. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  50. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  51. package/dist/Components/Inputs/Dropdown.js +8 -5
  52. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  53. package/dist/Components/Inputs/DropdownParts.js +16 -4
  54. package/dist/Components/Inputs/Field.d.ts +7 -1
  55. package/dist/Components/Inputs/Field.js +2 -2
  56. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  57. package/dist/Components/Inputs/GroupForm.js +10 -11
  58. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  59. package/dist/Components/Inputs/InputRenderer.js +39 -31
  60. package/dist/Components/Inputs/NumberBox.js +4 -2
  61. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  62. package/dist/Components/Inputs/PictureInput.js +70 -0
  63. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  64. package/dist/Components/Inputs/RadioGroup.js +8 -3
  65. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  66. package/dist/Components/Inputs/RowsInput.js +18 -3
  67. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  68. package/dist/Components/Inputs/TabularForm.js +29 -29
  69. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  70. package/dist/Components/Inputs/TextArea.js +14 -2
  71. package/dist/Components/Inputs/TextBox.js +4 -1
  72. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  73. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  74. package/dist/Components/Inputs/TimePicker.js +4 -1
  75. package/dist/Components/Inputs/TreeView.d.ts +107 -6
  76. package/dist/Components/Inputs/TreeView.js +513 -30
  77. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  78. package/dist/Components/Layouts/MessageBox.js +56 -7
  79. package/dist/Components/Layouts/Popup.d.ts +13 -6
  80. package/dist/Components/Layouts/Popup.js +37 -9
  81. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  82. package/dist/Components/Layouts/PopupDock.js +2 -1
  83. package/dist/Components/Layouts/PopupHost.js +5 -4
  84. package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
  85. package/dist/Components/Layouts/ThemePicker.js +16 -4
  86. package/dist/Components/Viewers/DataTable.js +205 -61
  87. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  88. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  89. package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
  90. package/dist/Components/Viewers/DataTableParts.js +62 -21
  91. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  92. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  93. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  94. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  95. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  96. package/dist/Components/Viewers/RecordCards.js +509 -0
  97. package/dist/Helpers/ApiModules.d.ts +3 -34
  98. package/dist/Helpers/ApiModules.js +3 -3
  99. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  100. package/dist/Helpers/ApiRoutes.js +31 -27
  101. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  102. package/dist/Helpers/ArrayHelpers.js +2 -0
  103. package/dist/Helpers/BusyHooks.d.ts +6 -0
  104. package/dist/Helpers/BusyHooks.js +39 -0
  105. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  106. package/dist/Helpers/CodeHelpers.js +332 -0
  107. package/dist/Helpers/CrudBackend.d.ts +114 -6
  108. package/dist/Helpers/CrudBackend.js +214 -11
  109. package/dist/Helpers/CrudColumns.d.ts +98 -0
  110. package/dist/Helpers/CrudColumns.js +160 -0
  111. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  112. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  113. package/dist/Helpers/CrudHelpers.d.ts +130 -6
  114. package/dist/Helpers/CrudHelpers.js +304 -12
  115. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  116. package/dist/Helpers/CrudNarrowPick.js +157 -0
  117. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  118. package/dist/Helpers/CrudRowForms.js +43 -0
  119. package/dist/Helpers/CrudScopes.d.ts +32 -0
  120. package/dist/Helpers/CrudScopes.js +79 -0
  121. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  122. package/dist/Helpers/DropdownHelpers.js +6 -2
  123. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  124. package/dist/Helpers/ExcelHelpers.js +12 -11
  125. package/dist/Helpers/FileAdapter.d.ts +78 -0
  126. package/dist/Helpers/FileAdapter.js +102 -0
  127. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  128. package/dist/Helpers/FilePrimaries.js +138 -0
  129. package/dist/Helpers/FormConfirm.d.ts +32 -0
  130. package/dist/Helpers/FormConfirm.js +40 -0
  131. package/dist/Helpers/FormStore.d.ts +90 -5
  132. package/dist/Helpers/FormStore.js +318 -22
  133. package/dist/Helpers/InputConditions.d.ts +50 -0
  134. package/dist/Helpers/InputConditions.js +168 -0
  135. package/dist/Helpers/InputMemory.d.ts +106 -0
  136. package/dist/Helpers/InputMemory.js +385 -0
  137. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  138. package/dist/Helpers/InputNarrowing.js +67 -0
  139. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  140. package/dist/Helpers/InputParamsHelpers.js +162 -71
  141. package/dist/Helpers/MessageBuilder.js +2 -0
  142. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  143. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  144. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  145. package/dist/Helpers/NexusHelpers.js +1 -0
  146. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  147. package/dist/Helpers/NumberHelpers.js +126 -0
  148. package/dist/Helpers/Permissions.js +2 -1
  149. package/dist/Helpers/QrCode.d.ts +26 -0
  150. package/dist/Helpers/QrCode.js +463 -0
  151. package/dist/Helpers/RecordCards.d.ts +205 -0
  152. package/dist/Helpers/RecordCards.js +378 -0
  153. package/dist/Helpers/RequestCache.d.ts +47 -0
  154. package/dist/Helpers/RequestCache.js +132 -0
  155. package/dist/Helpers/RowsStore.d.ts +37 -2
  156. package/dist/Helpers/RowsStore.js +128 -10
  157. package/dist/Helpers/ServerPreview.d.ts +69 -0
  158. package/dist/Helpers/ServerPreview.js +99 -0
  159. package/dist/Helpers/TableColumns.js +4 -3
  160. package/dist/Helpers/TableExport.d.ts +7 -1
  161. package/dist/Helpers/TableExport.js +21 -15
  162. package/dist/Helpers/TableFilters.d.ts +1 -1
  163. package/dist/Helpers/TableFilters.js +24 -23
  164. package/dist/Helpers/TableHelpers.d.ts +24 -3
  165. package/dist/Helpers/TableHelpers.js +58 -12
  166. package/dist/Helpers/TableJoins.d.ts +83 -0
  167. package/dist/Helpers/TableJoins.js +284 -0
  168. package/dist/Helpers/TableStore.d.ts +26 -1
  169. package/dist/Helpers/TableStore.js +73 -7
  170. package/dist/Helpers/TreeStore.d.ts +306 -11
  171. package/dist/Helpers/TreeStore.js +1048 -90
  172. package/dist/Helpers/UserPreferences.d.ts +8 -0
  173. package/dist/Helpers/UserPreferences.js +17 -0
  174. package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
  175. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  176. package/dist/Interfaces/FileInterfaces.js +4 -0
  177. package/dist/Interfaces/FormInterfaces.d.ts +393 -29
  178. package/dist/Interfaces/FormInterfaces.js +79 -1
  179. package/dist/Interfaces/InputInterfaces.d.ts +39 -3
  180. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  181. package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
  182. package/dist/Interfaces/PreferenceInterfaces.js +2 -5
  183. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  184. package/dist/Interfaces/RecordInterfaces.js +1 -0
  185. package/dist/Interfaces/TableInterfaces.d.ts +160 -5
  186. package/dist/Services/ApiProxy.d.ts +36 -1
  187. package/dist/Services/ApiProxy.js +182 -52
  188. package/dist/Services/ApiProxyRules.d.ts +203 -0
  189. package/dist/Services/ApiProxyRules.js +426 -0
  190. package/dist/Services/ExcelApi.js +5 -0
  191. package/dist/Services/FileApi.d.ts +13 -0
  192. package/dist/Services/FileApi.js +29 -0
  193. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  194. package/dist/Services/ImageEditorLoader.js +51 -0
  195. package/dist/Services/JoinSources.d.ts +80 -0
  196. package/dist/Services/JoinSources.js +150 -0
  197. package/dist/Services/ModuleImport.d.ts +7 -0
  198. package/dist/Services/ModuleImport.js +22 -0
  199. package/dist/Services/ServerApi.d.ts +13 -2
  200. package/dist/Services/ServerApi.js +8 -3
  201. package/dist/Shared.Index.d.ts +20 -0
  202. package/dist/Shared.Index.js +20 -0
  203. package/package.json +92 -85
  204. package/src/Styles/Nexus.Avatar.css +80 -79
  205. package/src/Styles/Nexus.Base.css +117 -117
  206. package/src/Styles/Nexus.Button.css +186 -180
  207. package/src/Styles/Nexus.Calendar.css +273 -273
  208. package/src/Styles/Nexus.Chat.css +1443 -1443
  209. package/src/Styles/Nexus.Choice.css +359 -343
  210. package/src/Styles/Nexus.Crud.css +542 -168
  211. package/src/Styles/Nexus.DateTime.css +81 -81
  212. package/src/Styles/Nexus.Dropdown.css +733 -725
  213. package/src/Styles/Nexus.Editor.css +295 -295
  214. package/src/Styles/Nexus.Excel.css +389 -389
  215. package/src/Styles/Nexus.Files.css +410 -0
  216. package/src/Styles/Nexus.Form.css +282 -279
  217. package/src/Styles/Nexus.Index.css +41 -37
  218. package/src/Styles/Nexus.Input.css +238 -4
  219. package/src/Styles/Nexus.Markdown.css +237 -237
  220. package/src/Styles/Nexus.Menu.css +246 -237
  221. package/src/Styles/Nexus.Popup.css +800 -724
  222. package/src/Styles/Nexus.Print.css +288 -0
  223. package/src/Styles/Nexus.QrCode.css +12 -0
  224. package/src/Styles/Nexus.Record.css +430 -0
  225. package/src/Styles/Nexus.Rows.css +637 -637
  226. package/src/Styles/Nexus.Slider.css +276 -276
  227. package/src/Styles/Nexus.Tab.Buttons.css +497 -432
  228. package/src/Styles/Nexus.Table.css +98 -35
  229. package/src/Styles/Nexus.Theme.Picker.css +59 -3
  230. package/src/Styles/Nexus.Themes.css +200 -194
  231. package/src/Styles/Nexus.Time.css +144 -144
  232. package/src/Styles/Nexus.Toast.css +263 -263
  233. package/src/Styles/Nexus.Tokens.css +94 -89
  234. package/src/Styles/Nexus.Tree.css +530 -431
@@ -1,168 +1,542 @@
1
- @layer nexus.components {
2
- /*
3
- * CRUD: the table comes from Nexus.Table.css. Here: the details popup and the message for a user who may not see any
4
- * list.
5
- */
6
- .nx-crud {
7
- min-width: 0;
8
- }
9
-
10
- /* A CRUD whose table fills takes the height it is given and hands all of it to the table. */
11
- .nx-crud:has(.nx-table--fill) {
12
- display: flex;
13
- flex-direction: column;
14
- height: 100%;
15
- min-height: 0;
16
- }
17
-
18
- .nx-crud:has(.nx-table--fill) > .nx-table--fill {
19
- flex: 1 1 auto;
20
- }
21
-
22
- /* Details: label over value, two to a line where there is room. */
23
- .nx-crud-details {
24
- display: flex;
25
- flex-direction: column;
26
- gap: var(--nx-space-3);
27
- }
28
-
29
- .nx-crud-details__list {
30
- display: grid;
31
- grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr));
32
- gap: var(--nx-space-4) var(--nx-space-5);
33
- margin: 0;
34
- }
35
-
36
- .nx-crud-details__item {
37
- display: flex;
38
- flex-direction: column;
39
- gap: 2px;
40
- min-width: 0;
41
- }
42
-
43
- .nx-crud-details__item--wide {
44
- grid-column: 1 / -1;
45
- }
46
-
47
- .nx-crud-details__label {
48
- color: var(--nx-text-muted);
49
- font-size: var(--nx-text-sm);
50
- }
51
-
52
- .nx-crud-details__value {
53
- /* A value still to come keeps its line, so the full record lands without moving anything. */
54
- min-height: 1lh;
55
- margin: 0;
56
- overflow-wrap: anywhere;
57
- font-size: var(--nx-text-md);
58
- }
59
-
60
- /* A field only the full record has, when that record could not be loaded. */
61
- .nx-crud-details__missing {
62
- color: var(--nx-text-muted);
63
- }
64
-
65
- /*
66
- * The details popup's footer is a form's bar (Nexus.Form.css): its status line on the left - "Loading…" for a slow
67
- * record, "Not all fields loaded." for one that failed - and Edit and Close on the right. The line takes only the room
68
- * the buttons leave, not a form's 14rem, so the bar stays one row on a phone and what it says never moves anything.
69
- */
70
- .nx-crud-details__foot > .nx-form-status {
71
- flex-basis: 0;
72
- }
73
-
74
- /* No list the user may see. */
75
- .nx-crud--denied {
76
- display: flex;
77
- flex-direction: column;
78
- align-items: center;
79
- gap: 4px;
80
- padding: 48px var(--nx-space-4);
81
- border: 1px dashed var(--nx-border-strong);
82
- border-radius: var(--nx-radius-lg);
83
- text-align: center;
84
- }
85
-
86
- .nx-crud__denied-icon {
87
- width: 28px;
88
- height: 28px;
89
- margin-bottom: 6px;
90
- color: var(--nx-text-muted);
91
- }
92
-
93
- .nx-crud__denied-title {
94
- margin: 0;
95
- font-weight: 600;
96
- }
97
-
98
- .nx-crud__denied-detail {
99
- margin: 0;
100
- color: var(--nx-text-muted);
101
- font-size: var(--nx-text-sm);
102
- }
103
-
104
- /*
105
- * A form popup with a panel beside it (`form.aside`): a preview of what will be written, a summary, help. The form
106
- * is first in the DOM, so a narrow window stacks the panel under it and the keyboard still reaches the form first.
107
- */
108
- .nx-crud-aside {
109
- display: grid;
110
- grid-template-columns: minmax(0, 1fr);
111
- gap: var(--nx-space-5);
112
- }
113
-
114
- .nx-crud-aside__main,
115
- .nx-crud-aside__side {
116
- min-width: 0;
117
- }
118
-
119
- /* Stacked, the panel does not take the whole window: the popup's body scrolls past it back to the form above. */
120
- .nx-crud-aside__side {
121
- display: flex;
122
- min-height: 0;
123
- max-height: 70vh;
124
- flex-direction: column;
125
- }
126
-
127
- /*
128
- * Side by side, where there is room for both. The popup's body stops scrolling and each column takes its own, so a
129
- * long form never pushes the panel off the screen. The body is a flex row here, which is what gives its one child a
130
- * height to work from: no percentage height anywhere, so it holds at every popup size.
131
- */
132
- @media (min-width: 1000px) {
133
- .nx-popup__body:has(> .nx-crud-aside) {
134
- display: flex;
135
- overflow: hidden;
136
- }
137
-
138
- .nx-crud-aside {
139
- flex: 1 1 auto;
140
- min-width: 0;
141
- /* The panel takes a share of the width, not half of it: a form has columns to fit, and code lines are short. */
142
- grid-template-columns: minmax(0, 1fr) minmax(0, var(--nx-crud-aside-width, clamp(20rem, 38%, 44rem)));
143
- }
144
-
145
- /*
146
- * A details popup's panel (`details.aside`) is what the popup is for, and the details beside it are a short list
147
- * of fields, so the shares turn round: the fields take a column of their own width and the panel the rest.
148
- */
149
- .nx-crud-aside.nx-crud-aside--details {
150
- grid-template-columns: minmax(0, var(--nx-crud-details-width, clamp(16rem, 30%, 26rem))) minmax(0, 1fr);
151
- }
152
-
153
- .nx-crud-aside__main {
154
- overflow: auto;
155
- overscroll-behavior: contain;
156
- /* Focus rings reach past an input's edge; without this the scroll box cuts them off. */
157
- padding: 2px;
158
- margin: -2px;
159
- }
160
-
161
- /* A panel taller than the popup scrolls here, whatever it holds; one that fills the height scrolls inside itself. */
162
- .nx-crud-aside__side {
163
- max-height: none;
164
- overflow: auto;
165
- overscroll-behavior: contain;
166
- }
167
- }
168
- }
1
+ @layer nexus.components {
2
+ /*
3
+ * CRUD: the table comes from Nexus.Table.css. Here: the details popup and the message for a user who may not see any
4
+ * list.
5
+ */
6
+ .nx-crud {
7
+ min-width: 0;
8
+ }
9
+
10
+ /* A CRUD whose table fills takes the height it is given and hands all of it to the table. */
11
+ .nx-crud:has(.nx-table--fill) {
12
+ display: flex;
13
+ flex-direction: column;
14
+ height: 100%;
15
+ min-height: 0;
16
+ }
17
+
18
+ .nx-crud:has(.nx-table--fill) > .nx-table--fill {
19
+ flex: 1 1 auto;
20
+ }
21
+
22
+ /*
23
+ * A list narrowed to one record picked in another (`narrow`): whose records these are, in the table's caption, kept
24
+ * while nothing is picked so the rows never move. Positioned, so the words kept for screen readers stay in it.
25
+ */
26
+ .nx-crud__scope {
27
+ position: relative;
28
+ margin: 0;
29
+ }
30
+
31
+ .nx-crud__scope-name {
32
+ color: var(--nx-text);
33
+ font-weight: 600;
34
+ }
35
+
36
+ /* A tree of records (`CrudTree`): the picked record's buttons, beside its name in the page's panel, wrapping as one group. */
37
+ .nx-crud-tree__actions {
38
+ display: flex;
39
+ flex-wrap: wrap;
40
+ align-items: center;
41
+ gap: var(--nx-space-2);
42
+ }
43
+
44
+ /* Details: label over value, two to a line where there is room. */
45
+ .nx-crud-details {
46
+ display: flex;
47
+ flex-direction: column;
48
+ gap: var(--nx-space-3);
49
+ }
50
+
51
+ .nx-crud-details__list {
52
+ display: grid;
53
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr));
54
+ gap: var(--nx-space-4) var(--nx-space-5);
55
+ margin: 0;
56
+ }
57
+
58
+ .nx-crud-details__item {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 2px;
62
+ min-width: 0;
63
+ }
64
+
65
+ .nx-crud-details__item--wide {
66
+ grid-column: 1 / -1;
67
+ }
68
+
69
+ /*
70
+ * Details in sections (`column.group`): each run of fields under its heading in the small muted heading style, the
71
+ * heading nearer its fields than the section before it.
72
+ */
73
+ .nx-crud-details__section {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: var(--nx-space-2);
77
+ min-width: 0;
78
+ }
79
+
80
+ .nx-crud-details__section + .nx-crud-details__section {
81
+ margin-top: var(--nx-space-3);
82
+ }
83
+
84
+ .nx-crud-details__heading {
85
+ margin: 0;
86
+ color: var(--nx-text-muted);
87
+ font-size: var(--nx-text-xs);
88
+ font-weight: 600;
89
+ line-height: 1.3;
90
+ letter-spacing: 0.04em;
91
+ text-transform: uppercase;
92
+ }
93
+
94
+ .nx-crud-details__label {
95
+ color: var(--nx-text-muted);
96
+ font-size: var(--nx-text-sm);
97
+ }
98
+
99
+ .nx-crud-details__value {
100
+ /* A value still to come keeps its line, so the full record lands without moving anything. */
101
+ min-height: 1lh;
102
+ margin: 0;
103
+ overflow-wrap: anywhere;
104
+ font-size: var(--nx-text-md);
105
+ }
106
+
107
+ /* A field only the full record has, when that record could not be loaded. */
108
+ .nx-crud-details__missing {
109
+ color: var(--nx-text-muted);
110
+ }
111
+
112
+ /*
113
+ * The details popup's footer is a form's bar (Nexus.Form.css): its status line on the left - "Loading…" for a slow
114
+ * record, "Not all fields loaded." for one that failed - and Edit and Close on the right. The line takes only the room
115
+ * the buttons leave, not a form's 14rem, so the bar stays one row on a phone and what it says never moves anything.
116
+ */
117
+ .nx-crud-details__foot > .nx-form-status {
118
+ flex-basis: 0;
119
+ }
120
+
121
+ /* No list the user may see. */
122
+ .nx-crud--denied {
123
+ display: flex;
124
+ flex-direction: column;
125
+ align-items: center;
126
+ gap: 4px;
127
+ padding: 48px var(--nx-space-4);
128
+ border: 1px dashed var(--nx-border-strong);
129
+ border-radius: var(--nx-radius-lg);
130
+ text-align: center;
131
+ }
132
+
133
+ .nx-crud__denied-icon {
134
+ width: 28px;
135
+ height: 28px;
136
+ margin-bottom: 6px;
137
+ color: var(--nx-text-muted);
138
+ }
139
+
140
+ .nx-crud__denied-title {
141
+ margin: 0;
142
+ font-weight: 600;
143
+ }
144
+
145
+ .nx-crud__denied-detail {
146
+ margin: 0;
147
+ color: var(--nx-text-muted);
148
+ font-size: var(--nx-text-sm);
149
+ }
150
+
151
+ /*
152
+ * A form popup with a panel beside it (`form.aside`): a preview of what will be written, a summary, help. The form
153
+ * is first in the DOM, so a narrow window stacks the panel under it and the keyboard still reaches the form first.
154
+ */
155
+ .nx-crud-aside {
156
+ display: grid;
157
+ grid-template-columns: minmax(0, 1fr);
158
+ gap: var(--nx-space-5);
159
+ }
160
+
161
+ .nx-crud-aside__main,
162
+ .nx-crud-aside__side {
163
+ min-width: 0;
164
+ }
165
+
166
+ /* Stacked, the panel does not take the whole window: the popup's body scrolls past it back to the form above. */
167
+ .nx-crud-aside__side {
168
+ display: flex;
169
+ min-height: 0;
170
+ max-height: 70vh;
171
+ flex-direction: column;
172
+ }
173
+
174
+ /*
175
+ * Under the fields, at every width (`details.asidePlace: "below"`, a record's workspace by default): the fields take
176
+ * the whole width, the panel the whole width under them, as tall as what it holds; what scrolls is the box around
177
+ * both (the workspace's panel, the popup's body), never a column of its own.
178
+ */
179
+ .nx-crud-aside--below > .nx-crud-aside__side {
180
+ max-height: none;
181
+ }
182
+
183
+ /*
184
+ * Side by side, where there is room for both. The popup's body stops scrolling and each column takes its own, so a
185
+ * long form never pushes the panel off the screen. The body is a flex row here, which is what gives its one child a
186
+ * height to work from: no percentage height anywhere, so it holds at every popup size.
187
+ */
188
+ @media (min-width: 1000px) {
189
+ .nx-popup__body:has(> .nx-crud-aside:not(.nx-crud-aside--below)) {
190
+ display: flex;
191
+ overflow: hidden;
192
+ }
193
+
194
+ .nx-crud-aside:not(.nx-crud-aside--below) {
195
+ flex: 1 1 auto;
196
+ min-width: 0;
197
+ /* The panel takes a share of the width, not half of it: a form has columns to fit, and code lines are short. */
198
+ grid-template-columns: minmax(0, 1fr) minmax(0, var(--nx-crud-aside-width, clamp(20rem, 38%, 44rem)));
199
+ }
200
+
201
+ /*
202
+ * A details popup's panel (`details.aside`) is what the popup is for, and the details beside it are a short list
203
+ * of fields, so the shares turn round: the fields take a column of their own width and the panel the rest.
204
+ */
205
+ .nx-crud-aside--details:not(.nx-crud-aside--below) {
206
+ grid-template-columns: minmax(0, var(--nx-crud-details-width, clamp(16rem, 30%, 26rem))) minmax(0, 1fr);
207
+ }
208
+
209
+ /*
210
+ * Each column scrolls on its own, and a scroll box clips at its padding edge: a field first in it would lose what it
211
+ * draws past its box. Room inside the edges, paid back by a negative margin so nothing moves: above, an outlined
212
+ * field's raised label (lifted 0.6em of the xs text over the top border) and its 2px focus outline; on every side,
213
+ * the 3px focus ring (`--nx-focus-ring`). Focus scrolls a field's control to an edge, so `scroll-padding` keeps the
214
+ * same room past the control: the label above, the box's 1px border and the ring below (the popup's body too).
215
+ */
216
+ .nx-crud-aside:not(.nx-crud-aside--below) > :is(.nx-crud-aside__main, .nx-crud-aside__side) {
217
+ overflow: auto;
218
+ overscroll-behavior: contain;
219
+ padding: calc(var(--nx-text-xs) * 0.6 + 2px) 3px 3px;
220
+ margin: calc(var(--nx-text-xs) * -0.6 - 2px) -3px -3px;
221
+ scroll-padding: calc(var(--nx-text-xs) * 0.6 + 2px) 4px 4px;
222
+ }
223
+
224
+ /* A panel taller than the popup scrolls here, whatever it holds; one that fills the height scrolls inside itself. */
225
+ .nx-crud-aside__side {
226
+ max-height: none;
227
+ }
228
+ }
229
+
230
+ /*
231
+ * A record's workspace in its details popup (`details.tabs`, `details.profile`): who the record is and its tabs in a
232
+ * column on the left, the chosen tab on the right. The popup's body stops scrolling and only the tab scrolls, inside
233
+ * itself, so the profile, the tabs and the footer stay where they are and a switch never changes the popup's size. The
234
+ * workspace is the container its column and the tabs' `auto` design follow: below 720px (a phone) the profile is one
235
+ * row and the tabs one strip, above the tab.
236
+ */
237
+ .nx-popup__body:has(> .nx-crud-workspace) {
238
+ display: flex;
239
+ overflow: hidden;
240
+ }
241
+
242
+ .nx-crud-workspace {
243
+ container-type: inline-size;
244
+ flex: 1 1 auto;
245
+ min-width: 0;
246
+ /* A popup of a set width (`lg`, `xl`) gets a height of its own, so no tab changes it; `full` gives its own. */
247
+ height: min(40rem, calc(100dvh - 10rem));
248
+ }
249
+
250
+ .nx-popup:is(.nx-popup--full, .nx-popup--maximized) .nx-crud-workspace {
251
+ height: auto;
252
+ }
253
+
254
+ .nx-crud-workspace__frame {
255
+ --nx-crud-profile-picture: 10rem;
256
+
257
+ display: grid;
258
+ grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
259
+ height: 100%;
260
+ }
261
+
262
+ /* Neither a profile nor more than one tab for this person: the tab alone, the width of the popup. */
263
+ .nx-crud-workspace__frame--alone {
264
+ grid-template-columns: minmax(0, 1fr);
265
+ }
266
+
267
+ .nx-crud-workspace__side {
268
+ display: flex;
269
+ flex-direction: column;
270
+ min-height: 0;
271
+ border-inline-end: 1px solid var(--nx-border);
272
+ background: color-mix(in oklab, var(--nx-text) 3%, transparent);
273
+ }
274
+
275
+ .nx-crud-workspace__profile {
276
+ flex-shrink: 0;
277
+ padding: var(--nx-space-5) var(--nx-space-4);
278
+ }
279
+
280
+ .nx-crud-workspace__profile + .nx-crud-workspace__nav {
281
+ border-top: 1px solid var(--nx-border);
282
+ }
283
+
284
+ .nx-crud-workspace__nav {
285
+ flex: 1 1 auto;
286
+ min-height: 0;
287
+ padding: var(--nx-space-3);
288
+ overflow-y: auto;
289
+ overscroll-behavior: contain;
290
+ }
291
+
292
+ /*
293
+ * Its padding is the room for a field first in it; `scroll-padding` the room round one the focus scrolls to (as the
294
+ * aside's). The side padding is named, so a tab's footer can reach across it.
295
+ */
296
+ .nx-crud-workspace__panel {
297
+ --nx-crud-panel-pad-x: var(--nx-space-5);
298
+
299
+ min-width: 0;
300
+ min-height: 0;
301
+ padding: var(--nx-space-4) var(--nx-crud-panel-pad-x) var(--nx-space-5);
302
+ overflow: auto;
303
+ overscroll-behavior: contain;
304
+ scroll-padding: calc(var(--nx-text-xs) * 0.6 + 2px) 4px 4px;
305
+ }
306
+
307
+ /*
308
+ * A tab's footer (`CrudTabFooter`): one bar, its words at the start and its buttons at the end, with the popup
309
+ * footer's room and hairline. On a phone the words and the buttons wrap as whole groups. Where it is not in a
310
+ * workspace's panel, this is all it is: a plain bar where it stands.
311
+ */
312
+ .nx-crud-tab-footer {
313
+ display: flex;
314
+ flex-wrap: wrap;
315
+ align-items: center;
316
+ justify-content: flex-end;
317
+ gap: var(--nx-space-2);
318
+ padding-block: var(--nx-space-3);
319
+ border-top: 1px solid var(--nx-border);
320
+ }
321
+
322
+ .nx-crud-tab-footer > p {
323
+ flex: 1 1 auto;
324
+ min-width: 0;
325
+ margin: 0;
326
+ }
327
+
328
+ /* A form's bar fills the rest, so its status line sits beside its buttons (as in the popup's footer). */
329
+ .nx-crud-tab-footer > .nx-form-buttons {
330
+ flex: 1 1 auto;
331
+ }
332
+
333
+ /*
334
+ * In a workspace's panel it stays in view: it sticks to the bottom of the panel while the tab scrolls and sits in its
335
+ * place at the end, across the panel's whole width (past its side padding) on the panel's own surface, never a box of
336
+ * its own (no shadow, no frame). The panel keeps no padding under it, so it meets the panel's foot at the end as
337
+ * while it sticks. What the focus scrolls into view (a field, a caret) clears it: the panel keeps the footer's height
338
+ * as `CrudTabFooter` measures it (`scroll-padding`). Above what a tab's content raises (a field's label, a table's
339
+ * sticky header: 5 at most).
340
+ */
341
+ .nx-crud-workspace__panel:has(.nx-crud-tab-footer) {
342
+ padding-bottom: 0;
343
+ scroll-padding-bottom: calc(var(--nx-crud-tab-footer-size, 0px) + 4px);
344
+ }
345
+
346
+ /* While the focus is in the footer, which is always in view, that room goes: its Save never scrolls the tab. */
347
+ .nx-crud-workspace__panel:has(.nx-crud-tab-footer:focus-within) {
348
+ scroll-padding-bottom: 4px;
349
+ }
350
+
351
+ .nx-crud-workspace__panel .nx-crud-tab-footer {
352
+ position: sticky;
353
+ bottom: 0;
354
+ z-index: 6;
355
+ margin-inline: calc(-1 * var(--nx-crud-panel-pad-x));
356
+ padding-inline: var(--nx-crud-panel-pad-x);
357
+ background: var(--nx-surface);
358
+ }
359
+
360
+ /* The profile: the picture over the name, the line under it and the tags, in the middle of the column. */
361
+ .nx-crud-profile {
362
+ display: flex;
363
+ flex-direction: column;
364
+ align-items: center;
365
+ gap: var(--nx-space-3);
366
+ min-width: 0;
367
+ text-align: center;
368
+ }
369
+
370
+ .nx-crud-profile__picture {
371
+ display: flex;
372
+ flex-shrink: 0;
373
+ }
374
+
375
+ .nx-crud-profile__text {
376
+ display: grid;
377
+ justify-items: center;
378
+ gap: 2px;
379
+ min-width: 0;
380
+ max-width: 100%;
381
+ }
382
+
383
+ .nx-crud-profile__title {
384
+ margin: 0;
385
+ font-size: var(--nx-text-lg);
386
+ font-weight: 600;
387
+ line-height: 1.35;
388
+ overflow-wrap: anywhere;
389
+ }
390
+
391
+ .nx-crud-profile__subtitle {
392
+ margin: 0;
393
+ color: var(--nx-text-muted);
394
+ font-size: var(--nx-text-sm);
395
+ overflow-wrap: anywhere;
396
+ }
397
+
398
+ .nx-crud-profile__tags {
399
+ display: flex;
400
+ flex-wrap: wrap;
401
+ justify-content: center;
402
+ gap: 4px;
403
+ margin: 4px 0 0;
404
+ }
405
+
406
+ /*
407
+ * A few of the record's fields under the tags (`profile.fields`): a small grid in the middle of the column, a label
408
+ * (muted) and its value (the text colour) a line, each pair on the grid's two tracks so the values line up. A long
409
+ * value wraps inside the column, never past it, and a column's tag follows its value, under it when the line is full.
410
+ */
411
+ .nx-crud-profile__fields {
412
+ display: grid;
413
+ grid-template-columns: auto minmax(0, 1fr);
414
+ gap: var(--nx-space-1) var(--nx-space-3);
415
+ max-width: 100%;
416
+ margin: var(--nx-space-2) 0 0;
417
+ font-size: var(--nx-text-sm);
418
+ line-height: 1.5;
419
+ text-align: start;
420
+ }
421
+
422
+ .nx-crud-profile__field {
423
+ display: grid;
424
+ grid-column: 1 / -1;
425
+ grid-template-columns: subgrid;
426
+ align-items: baseline;
427
+ min-width: 0;
428
+ }
429
+
430
+ .nx-crud-profile__label {
431
+ color: var(--nx-text-muted);
432
+ overflow-wrap: break-word;
433
+ }
434
+
435
+ .nx-crud-profile__value {
436
+ display: flex;
437
+ flex-wrap: wrap;
438
+ align-items: baseline;
439
+ gap: 2px var(--nx-space-2);
440
+ min-width: 0;
441
+ margin: 0;
442
+ color: var(--nx-text);
443
+ overflow-wrap: anywhere;
444
+ }
445
+
446
+ /* The gap places the tag: a word's space after the value, or the start of the next line. */
447
+ .nx-crud-profile__value > .nx-table__tag--after {
448
+ margin-inline-start: 0;
449
+ }
450
+
451
+ /* A field only the full record has, held while it is read: a short value's width on the value's baseline. */
452
+ .nx-table__bone.nx-crud-profile__bone {
453
+ width: 6rem;
454
+ }
455
+
456
+ @container (max-width: 719.98px) {
457
+ .nx-crud-workspace__frame {
458
+ --nx-crud-profile-picture: 3rem;
459
+
460
+ display: flex;
461
+ flex-direction: column;
462
+ }
463
+
464
+ .nx-crud-workspace__side {
465
+ flex-shrink: 0;
466
+ border-inline-end: 0;
467
+ border-bottom: 1px solid var(--nx-border);
468
+ }
469
+
470
+ .nx-crud-workspace__profile {
471
+ padding: var(--nx-space-3) var(--nx-space-4);
472
+ }
473
+
474
+ .nx-crud-workspace__profile + .nx-crud-workspace__nav {
475
+ border-top: 0;
476
+ }
477
+
478
+ .nx-crud-profile {
479
+ flex-direction: row;
480
+ text-align: start;
481
+ }
482
+
483
+ .nx-crud-profile__text {
484
+ justify-items: start;
485
+ }
486
+
487
+ .nx-crud-profile__tags {
488
+ justify-content: flex-start;
489
+ }
490
+
491
+ /*
492
+ * The fields follow the tags as one wrapped line of "label value" pairs: beside the tags when all of them fit there,
493
+ * else from the next line, so they take the lines their words need, not a line a field.
494
+ */
495
+ .nx-crud-profile__text:has(> .nx-crud-profile__fields) {
496
+ display: flex;
497
+ flex-wrap: wrap;
498
+ align-items: baseline;
499
+ gap: 2px var(--nx-space-3);
500
+ }
501
+
502
+ .nx-crud-profile__text:has(> .nx-crud-profile__fields) > :is(.nx-crud-profile__title, .nx-crud-profile__subtitle) {
503
+ flex-basis: 100%;
504
+ }
505
+
506
+ .nx-crud-profile__fields {
507
+ display: flex;
508
+ flex-wrap: wrap;
509
+ gap: 2px var(--nx-space-3);
510
+ min-width: 0;
511
+ margin-top: 4px;
512
+ }
513
+
514
+ /* A pair longer than the line wraps as words do, its label kept with the start of its value. */
515
+ .nx-crud-profile__field {
516
+ display: block;
517
+ min-width: 0;
518
+ }
519
+
520
+ .nx-crud-profile__label,
521
+ .nx-crud-profile__value {
522
+ display: inline;
523
+ }
524
+
525
+ .nx-crud-profile__value > .nx-table__tag--after {
526
+ margin-inline-start: var(--nx-space-2);
527
+ }
528
+
529
+ .nx-crud-workspace__nav {
530
+ flex: 0 0 auto;
531
+ padding: 0 var(--nx-space-2) var(--nx-space-1);
532
+ overflow: visible;
533
+ }
534
+
535
+ .nx-crud-workspace__panel {
536
+ --nx-crud-panel-pad-x: var(--nx-space-4);
537
+
538
+ flex: 1 1 auto;
539
+ padding: var(--nx-space-3) var(--nx-crud-panel-pad-x) var(--nx-space-4);
540
+ }
541
+ }
542
+ }