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,724 +1,800 @@
1
- @layer nexus.components {
2
- /*
3
- * Popup: a native <dialog> shown modally. The panel is the dialog itself, so a click on the backdrop reaches the
4
- * dialog element and nothing else. The header and footer stay while the body scrolls.
5
- */
6
- .nx-popup {
7
- --nx-popup-width: 560px;
8
-
9
- /* Percentages, not vw: the viewport width here leaves out the scrollbar. */
10
- width: min(var(--nx-popup-width), calc(100% - 32px));
11
- max-width: none;
12
- max-height: min(calc(100vh - 48px), calc(100dvh - 48px));
13
- margin: auto;
14
- padding: 0;
15
- overflow: hidden;
16
- border: 1px solid var(--nx-border);
17
- border-radius: var(--nx-radius-lg);
18
- background: var(--nx-surface);
19
- color: var(--nx-text);
20
- box-shadow: var(--nx-shadow);
21
- outline: none;
22
- /* Where the title bar moved it, from the center (set by the Popup while dragging). */
23
- translate: var(--nx-popup-x, 0px) var(--nx-popup-y, 0px);
24
- }
25
-
26
- /*
27
- * Opening and closing are a plain fade, the popup and its backdrop together: it appears where it stays, at the size
28
- * it keeps, so nothing in it moves while it fades. Closing waits for its fade (`afterAnimation`), and takes no clicks
29
- * meanwhile, so a double-click on Save does not submit twice; with animations off there is no fade to wait for.
30
- */
31
- .nx-popup[open] {
32
- display: flex;
33
- flex-direction: column;
34
- animation: nx-popup-in 160ms ease-out;
35
- }
36
-
37
- .nx-popup::backdrop {
38
- background: rgb(0 0 0 / 0.5);
39
- animation: nx-popup-in 160ms ease-out;
40
- }
41
-
42
- .nx-popup--closing[open] {
43
- animation: nx-popup-out 120ms ease-in forwards;
44
- pointer-events: none;
45
- }
46
-
47
- .nx-popup--closing::backdrop {
48
- animation: nx-popup-out 120ms ease-in forwards;
49
- }
50
-
51
- @keyframes nx-popup-in {
52
- from {
53
- opacity: 0;
54
- }
55
- }
56
-
57
- @keyframes nx-popup-out {
58
- to {
59
- opacity: 0;
60
- }
61
- }
62
-
63
- /* The page behind a modal popup stops scrolling; the Popup sets --nx-scrollbar-gap to the scrollbar's width, so nothing shifts. */
64
- :root:has(.nx-popup:modal) {
65
- overflow: hidden;
66
- padding-right: var(--nx-scrollbar-gap, 0px);
67
- }
68
-
69
- /* Sizes */
70
- .nx-popup--xs {
71
- --nx-popup-width: 360px;
72
- }
73
-
74
- .nx-popup--sm {
75
- --nx-popup-width: 440px;
76
- }
77
-
78
- .nx-popup--lg {
79
- --nx-popup-width: 720px;
80
- }
81
-
82
- .nx-popup--xl {
83
- --nx-popup-width: 960px;
84
- }
85
-
86
- .nx-popup--full {
87
- --nx-popup-width: calc(100% - 32px);
88
-
89
- height: calc(100vh - 32px);
90
- max-height: none;
91
- }
92
-
93
- /* Maximized: the whole window, edge to edge, wherever it was moved. Two classes, to win over the sizes and phones. */
94
- .nx-popup.nx-popup--maximized {
95
- width: 100%;
96
- height: 100%;
97
- max-height: none;
98
- border-width: 0;
99
- border-radius: 0;
100
- translate: none;
101
- }
102
-
103
- /* Minimizing: the page comes back while the popup flies to the dock. */
104
- .nx-popup[data-nx-minimizing] {
105
- pointer-events: none;
106
- }
107
-
108
- .nx-popup[data-nx-minimizing]::backdrop {
109
- animation: nx-popup-out 200ms ease-in forwards;
110
- }
111
-
112
- /* Header, either design: the title bar moves the popup, so it selects no text and a touch drags instead of scrolling. */
113
- .nx-popup__head {
114
- display: flex;
115
- flex-shrink: 0;
116
- }
117
-
118
- .nx-popup--movable .nx-popup__head {
119
- cursor: move;
120
- touch-action: none;
121
- user-select: none;
122
- }
123
-
124
- .nx-popup__title,
125
- .nx-popup__description {
126
- margin: 0;
127
- }
128
-
129
- .nx-popup__controls {
130
- display: flex;
131
- flex-shrink: 0;
132
- margin-left: auto;
133
- }
134
-
135
- .nx-popup__control {
136
- display: grid;
137
- place-items: center;
138
- padding: 0;
139
- border: 0;
140
- background: none;
141
- color: var(--nx-text-muted);
142
- cursor: pointer;
143
- transition:
144
- background-color var(--nx-duration),
145
- color var(--nx-duration);
146
- }
147
-
148
- .nx-popup__control .nx-icon {
149
- vertical-align: 0;
150
- }
151
-
152
- /* Close turns red on hover, the one button that throws away what the popup holds. */
153
- .nx-popup__close:hover {
154
- background: var(--nx-danger-solid);
155
- color: var(--nx-danger-text);
156
- }
157
-
158
- /*
159
- * Window (the default): one low title bar, as on a desktop window. The icon and the title on the left, the
160
- * description after the title in muted text, and the window buttons filling the top right corner.
161
- */
162
- .nx-popup--window {
163
- --nx-popup-bar: 32px;
164
- }
165
-
166
- .nx-popup--window .nx-popup__head {
167
- box-sizing: content-box;
168
- align-items: center;
169
- gap: var(--nx-space-2);
170
- height: var(--nx-popup-bar);
171
- padding-left: var(--nx-space-3);
172
- border-bottom: 1px solid var(--nx-border);
173
- background: color-mix(in oklab, var(--nx-surface), var(--nx-text) 4%);
174
- }
175
-
176
- .nx-popup--window .nx-popup__icon {
177
- display: grid;
178
- flex-shrink: 0;
179
- place-items: center;
180
- color: var(--nx-primary);
181
- }
182
-
183
- .nx-popup--window .nx-popup__icon .nx-icon {
184
- width: 16px;
185
- height: 16px;
186
- vertical-align: 0;
187
- }
188
-
189
- /* One line: the title keeps its room, the description takes what is left, and both end in … when cut. */
190
- .nx-popup--window .nx-popup__heading {
191
- display: flex;
192
- flex: 1 1 auto;
193
- align-items: baseline;
194
- gap: var(--nx-space-2);
195
- min-width: 0;
196
- white-space: nowrap;
197
- }
198
-
199
- .nx-popup--window .nx-popup__title {
200
- flex: 0 1 auto;
201
- min-width: 0;
202
- overflow: hidden;
203
- font-size: var(--nx-text-sm);
204
- font-weight: 600;
205
- line-height: var(--nx-popup-bar);
206
- text-overflow: ellipsis;
207
- }
208
-
209
- .nx-popup--window .nx-popup__description {
210
- flex: 1 1 0;
211
- min-width: 0;
212
- overflow: hidden;
213
- color: var(--nx-text-muted);
214
- font-size: var(--nx-text-xs);
215
- text-overflow: ellipsis;
216
- }
217
-
218
- .nx-popup--window .nx-popup__controls {
219
- align-self: stretch;
220
- }
221
-
222
- .nx-popup--window .nx-popup__control {
223
- width: 46px;
224
- height: 100%;
225
- }
226
-
227
- .nx-popup--window .nx-popup__control:hover {
228
- background: color-mix(in oklab, var(--nx-text) 9%, transparent);
229
- color: var(--nx-text);
230
- }
231
-
232
- .nx-popup--window .nx-popup__control:active {
233
- background: color-mix(in oklab, var(--nx-text) 15%, transparent);
234
- }
235
-
236
- .nx-popup--window .nx-popup__close:hover {
237
- background: var(--nx-danger-solid);
238
- color: var(--nx-danger-text);
239
- }
240
-
241
- .nx-popup--window .nx-popup__close:active {
242
- background: var(--nx-danger-hover);
243
- }
244
-
245
- /* The buttons touch the popup's edges, where an outer ring would be cut off, so the ring is drawn inside. */
246
- .nx-popup--window .nx-popup__control:focus-visible {
247
- outline: none;
248
- box-shadow: inset 0 0 0 2px var(--nx-primary);
249
- }
250
-
251
- /* Thin lines, as desktop window buttons have; the square glyphs fill more of their box, so they are drawn smaller. */
252
- .nx-popup--window .nx-popup__control .nx-icon {
253
- width: 16px;
254
- height: 16px;
255
- stroke-width: 1.5;
256
- }
257
-
258
- .nx-popup--window .nx-popup__maximize .nx-icon {
259
- width: 13px;
260
- height: 13px;
261
- stroke-width: 1.75;
262
- }
263
-
264
- /* Touch screens get taller buttons. */
265
- @media (pointer: coarse) {
266
- .nx-popup--window {
267
- --nx-popup-bar: 40px;
268
- }
269
-
270
- .nx-popup--window .nx-popup__control {
271
- width: 48px;
272
- }
273
- }
274
-
275
- /*
276
- * Dialog (message boxes): a larger heading, the icon in a circle that carries the tone, the description under the
277
- * title, and a small close button.
278
- */
279
- .nx-popup--dialog .nx-popup__head {
280
- align-items: flex-start;
281
- gap: var(--nx-space-3);
282
- padding: var(--nx-space-5) var(--nx-space-5) 0;
283
- }
284
-
285
- .nx-popup--dialog .nx-popup__icon {
286
- display: grid;
287
- flex-shrink: 0;
288
- place-items: center;
289
- width: 40px;
290
- height: 40px;
291
- border-radius: 50%;
292
- background: var(--nx-primary-soft);
293
- color: var(--nx-primary);
294
- }
295
-
296
- .nx-popup--dialog .nx-popup__icon .nx-icon {
297
- width: 22px;
298
- height: 22px;
299
- vertical-align: 0;
300
- }
301
-
302
- .nx-popup--dialog .nx-popup__heading {
303
- flex: 1 1 auto;
304
- min-width: 0;
305
- padding-top: 2px;
306
- }
307
-
308
- .nx-popup--dialog .nx-popup__title {
309
- font-size: var(--nx-text-lg);
310
- font-weight: 600;
311
- line-height: 1.35;
312
- overflow-wrap: anywhere;
313
- }
314
-
315
- .nx-popup--dialog .nx-popup__description {
316
- margin-top: 4px;
317
- color: var(--nx-text-muted);
318
- font-size: var(--nx-text-sm);
319
- line-height: 1.45;
320
- }
321
-
322
- .nx-popup--dialog .nx-popup__controls {
323
- gap: 2px;
324
- margin: -6px -8px 0 auto;
325
- }
326
-
327
- .nx-popup--dialog .nx-popup__control {
328
- width: 30px;
329
- height: 30px;
330
- border-radius: var(--nx-radius);
331
- }
332
-
333
- .nx-popup--dialog .nx-popup__control:not(.nx-popup__close):hover {
334
- background: var(--nx-surface-hover);
335
- color: var(--nx-text);
336
- }
337
-
338
- .nx-popup--dialog .nx-popup__control:focus-visible {
339
- outline: none;
340
- box-shadow: var(--nx-focus-ring);
341
- }
342
-
343
- .nx-popup--dialog .nx-popup__control .nx-icon {
344
- width: 18px;
345
- height: 18px;
346
- }
347
-
348
- .nx-popup--dialog .nx-popup__maximize .nx-icon {
349
- width: 15px;
350
- height: 15px;
351
- }
352
-
353
- /* Body: scrolls when the content is taller than the window allows. */
354
- .nx-popup__body {
355
- flex: 1 1 auto;
356
- min-height: 0;
357
- padding: var(--nx-space-4) var(--nx-space-5) var(--nx-space-5);
358
- overflow: auto;
359
- overscroll-behavior: contain;
360
- }
361
-
362
- .nx-popup__head + .nx-popup__body:empty {
363
- padding-top: var(--nx-space-2);
364
- }
365
-
366
- .nx-popup__body:only-child,
367
- .nx-popup--window .nx-popup__body {
368
- padding-top: var(--nx-space-5);
369
- }
370
-
371
- .nx-popup--flush .nx-popup__body {
372
- padding: 0;
373
- }
374
-
375
- /* Footer: buttons at the end. */
376
- .nx-popup__foot {
377
- display: flex;
378
- flex-shrink: 0;
379
- flex-wrap: wrap;
380
- align-items: center;
381
- justify-content: flex-end;
382
- gap: var(--nx-space-2);
383
- padding: var(--nx-space-3) var(--nx-space-5);
384
- border-top: 1px solid var(--nx-border);
385
- }
386
-
387
- /* A form's bar fills the foot, so its status line sits beside the buttons instead of on a row of its own. */
388
- .nx-popup__foot > .nx-form-buttons {
389
- flex: 1 1 auto;
390
- }
391
-
392
- /* Message boxes: a small popup whose icon carries the tone. */
393
- .nx-popup--message .nx-popup__body {
394
- padding-top: var(--nx-space-2);
395
- }
396
-
397
- .nx-popup--message .nx-popup__body:empty {
398
- padding: var(--nx-space-3) 0 0;
399
- }
400
-
401
- .nx-message__text {
402
- color: var(--nx-text-muted);
403
- line-height: 1.5;
404
- }
405
-
406
- .nx-message__form {
407
- display: grid;
408
- gap: var(--nx-space-4);
409
- }
410
-
411
- .nx-popup--tone-info .nx-popup__icon {
412
- background: var(--nx-info-soft);
413
- color: var(--nx-info);
414
- }
415
-
416
- .nx-popup--tone-success .nx-popup__icon {
417
- background: var(--nx-success-soft);
418
- color: var(--nx-success);
419
- }
420
-
421
- .nx-popup--tone-warning .nx-popup__icon {
422
- background: var(--nx-warning-soft);
423
- color: var(--nx-warning);
424
- }
425
-
426
- .nx-popup--tone-danger .nx-popup__icon {
427
- background: var(--nx-danger-soft);
428
- color: var(--nx-danger);
429
- }
430
-
431
- /*
432
- * The dock: minimized popups as bars at the bottom right. Several stack behind the newest, with cards peeking out
433
- * above it and a count that opens the whole list. It sits on the page, so an open popup's backdrop covers it.
434
- */
435
- .nx-dock {
436
- --nx-dock-edge: 16px;
437
-
438
- position: fixed;
439
- right: calc(var(--nx-dock-edge) + env(safe-area-inset-right, 0px));
440
- bottom: calc(var(--nx-dock-edge) + env(safe-area-inset-bottom, 0px));
441
- z-index: 1000;
442
- display: flex;
443
- flex-direction: column;
444
- align-items: flex-end;
445
- gap: var(--nx-space-2);
446
- width: min(300px, calc(100% - 2 * var(--nx-dock-edge)));
447
- max-height: calc(100dvh - 2 * var(--nx-dock-edge));
448
- /* Only the bars and the count take clicks, not the space between them. */
449
- pointer-events: none;
450
- }
451
-
452
- .nx-dock > * {
453
- pointer-events: auto;
454
- }
455
-
456
- /* While a popup locks the page, its scrollbar's space is padding; the dock keeps its place beside it. */
457
- :root:has(.nx-popup:modal) .nx-dock {
458
- right: calc(var(--nx-dock-edge) + var(--nx-scrollbar-gap, 0px) + env(safe-area-inset-right, 0px));
459
- }
460
-
461
- .nx-dock__toggle {
462
- display: inline-flex;
463
- flex-shrink: 0;
464
- align-items: center;
465
- gap: 6px;
466
- height: 28px;
467
- padding: 0 10px 0 4px;
468
- border: 1px solid var(--nx-border);
469
- border-radius: var(--nx-radius-full);
470
- background: light-dark(var(--nx-surface), var(--nx-surface-2));
471
- color: var(--nx-text);
472
- box-shadow: var(--nx-shadow-sm);
473
- font: inherit;
474
- font-size: var(--nx-text-sm);
475
- font-weight: 600;
476
- cursor: pointer;
477
- transition: background-color var(--nx-duration);
478
- }
479
-
480
- .nx-dock__toggle:hover {
481
- background: var(--nx-surface-hover);
482
- }
483
-
484
- .nx-dock__toggle:focus-visible {
485
- outline: none;
486
- box-shadow: var(--nx-focus-ring);
487
- }
488
-
489
- .nx-dock__toggle .nx-icon {
490
- width: 16px;
491
- height: 16px;
492
- color: var(--nx-text-muted);
493
- }
494
-
495
- .nx-dock__count {
496
- display: grid;
497
- place-items: center;
498
- min-width: 20px;
499
- height: 20px;
500
- padding: 0 6px;
501
- border-radius: var(--nx-radius-full);
502
- background: var(--nx-primary-solid);
503
- color: var(--nx-primary-text);
504
- font-size: var(--nx-text-xs);
505
- font-variant-numeric: tabular-nums;
506
- line-height: 1;
507
- }
508
-
509
- .nx-dock__list {
510
- position: relative;
511
- isolation: isolate;
512
- display: flex;
513
- flex-direction: column;
514
- gap: 6px;
515
- width: 100%;
516
- min-height: 0;
517
- margin: 0;
518
- padding: 0;
519
- list-style: none;
520
- }
521
-
522
- /* Open: the whole list, oldest at the top; it scrolls when taller than the window, with room for the shadows. */
523
- .nx-dock--open .nx-dock__list {
524
- margin: -8px;
525
- padding: 8px;
526
- width: calc(100% + 16px);
527
- overflow-y: auto;
528
- overscroll-behavior: contain;
529
- }
530
-
531
- /* Stacked: one or two cards peek out above the newest bar; a click on them opens the list. */
532
- .nx-dock__list[data-depth] {
533
- margin-top: calc(var(--nx-dock-depth) * 5px);
534
- cursor: pointer;
535
- }
536
-
537
- .nx-dock__list[data-depth="1"] {
538
- --nx-dock-depth: 1;
539
- }
540
-
541
- .nx-dock__list[data-depth="2"] {
542
- --nx-dock-depth: 2;
543
- }
544
-
545
- .nx-dock__list[data-depth]::before,
546
- .nx-dock__list[data-depth="2"]::after {
547
- content: "";
548
- position: absolute;
549
- top: -5px;
550
- right: 8px;
551
- left: 8px;
552
- z-index: -1;
553
- height: 24px;
554
- border: 1px solid var(--nx-border);
555
- border-radius: var(--nx-radius-lg);
556
- background: light-dark(var(--nx-surface), var(--nx-surface-2));
557
- box-shadow: var(--nx-shadow-sm);
558
- }
559
-
560
- .nx-dock__list[data-depth="2"]::after {
561
- top: -10px;
562
- right: 16px;
563
- left: 16px;
564
- z-index: -2;
565
- }
566
-
567
- .nx-dock__item {
568
- display: flex;
569
- flex-shrink: 0;
570
- align-items: center;
571
- gap: 2px;
572
- height: 44px;
573
- padding-right: 6px;
574
- border: 1px solid var(--nx-border);
575
- border-radius: var(--nx-radius-lg);
576
- background: light-dark(var(--nx-surface), var(--nx-surface-2));
577
- color: var(--nx-text);
578
- box-shadow: var(--nx-shadow);
579
- cursor: default;
580
- transition: background-color var(--nx-duration);
581
- animation: nx-dock-in 180ms cubic-bezier(0.2, 0, 0, 1);
582
- }
583
-
584
- .nx-dock__item:has(.nx-dock__window:hover) {
585
- background: var(--nx-surface-hover);
586
- }
587
-
588
- .nx-dock__item:has(.nx-dock__window:focus-visible) {
589
- box-shadow: var(--nx-focus-ring), var(--nx-shadow);
590
- }
591
-
592
- /* The bar's title part brings the popup back. */
593
- .nx-dock__window {
594
- display: flex;
595
- flex: 1 1 auto;
596
- align-items: center;
597
- gap: 10px;
598
- min-width: 0;
599
- height: 100%;
600
- padding: 0 6px 0 7px;
601
- border: 0;
602
- border-radius: inherit;
603
- background: none;
604
- color: inherit;
605
- font: inherit;
606
- text-align: start;
607
- cursor: pointer;
608
- }
609
-
610
- .nx-dock__window:focus-visible {
611
- outline: none;
612
- }
613
-
614
- .nx-dock__icon {
615
- display: grid;
616
- flex-shrink: 0;
617
- place-items: center;
618
- width: 28px;
619
- height: 28px;
620
- border-radius: var(--nx-radius);
621
- background: var(--nx-primary-soft);
622
- color: var(--nx-primary);
623
- }
624
-
625
- .nx-dock__icon .nx-icon {
626
- width: 16px;
627
- height: 16px;
628
- vertical-align: 0;
629
- }
630
-
631
- .nx-dock__title {
632
- flex: 1 1 auto;
633
- min-width: 0;
634
- overflow: hidden;
635
- font-size: var(--nx-text-md);
636
- font-weight: 600;
637
- line-height: 1.3;
638
- text-overflow: ellipsis;
639
- white-space: nowrap;
640
- }
641
-
642
- .nx-dock__restore {
643
- flex-shrink: 0;
644
- width: 18px;
645
- height: 18px;
646
- color: var(--nx-text-muted);
647
- }
648
-
649
- .nx-dock__window:hover .nx-dock__restore {
650
- color: var(--nx-text);
651
- }
652
-
653
- .nx-dock__close {
654
- display: grid;
655
- flex-shrink: 0;
656
- place-items: center;
657
- width: 30px;
658
- height: 30px;
659
- padding: 0;
660
- border: 0;
661
- border-radius: var(--nx-radius);
662
- background: none;
663
- color: var(--nx-text-muted);
664
- cursor: pointer;
665
- transition:
666
- background-color var(--nx-duration),
667
- color var(--nx-duration);
668
- }
669
-
670
- .nx-dock__close:hover {
671
- background: var(--nx-danger-solid);
672
- color: var(--nx-danger-text);
673
- }
674
-
675
- .nx-dock__close:focus-visible {
676
- outline: none;
677
- box-shadow: var(--nx-focus-ring);
678
- }
679
-
680
- .nx-dock__close .nx-icon {
681
- width: 16px;
682
- height: 16px;
683
- vertical-align: 0;
684
- }
685
-
686
- @keyframes nx-dock-in {
687
- from {
688
- opacity: 0;
689
- transform: translateY(8px);
690
- }
691
- }
692
-
693
- @media (max-width: 600px) {
694
- .nx-popup {
695
- width: calc(100% - 16px);
696
- max-height: min(calc(100vh - 16px), calc(100dvh - 16px));
697
- }
698
-
699
- .nx-popup--dialog .nx-popup__head {
700
- padding: var(--nx-space-4) var(--nx-space-4) 0;
701
- }
702
-
703
- .nx-popup__body {
704
- padding: var(--nx-space-3) var(--nx-space-4) var(--nx-space-4);
705
- }
706
-
707
- .nx-popup--window:not(.nx-popup--flush) .nx-popup__body {
708
- padding-top: var(--nx-space-4);
709
- }
710
-
711
- .nx-popup__foot {
712
- padding: var(--nx-space-3) var(--nx-space-4);
713
- }
714
- }
715
-
716
- [data-nx-motion="reduce"] .nx-popup[open],
717
- [data-nx-motion="reduce"] .nx-popup--closing[open],
718
- [data-nx-motion="reduce"] .nx-popup::backdrop,
719
- [data-nx-motion="reduce"] .nx-popup--closing::backdrop,
720
- [data-nx-motion="reduce"] .nx-popup[data-nx-minimizing]::backdrop,
721
- [data-nx-motion="reduce"] .nx-dock__item {
722
- animation: none;
723
- }
724
- }
1
+ @layer nexus.components {
2
+ /*
3
+ * Popup: a native <dialog> shown modally. The panel is the dialog itself, so a click on the backdrop reaches the
4
+ * dialog element and nothing else. The header and footer stay while the body scrolls.
5
+ */
6
+ .nx-popup {
7
+ --nx-popup-width: 560px;
8
+
9
+ /* Percentages, not vw: the viewport width here leaves out the scrollbar. */
10
+ width: min(var(--nx-popup-width), calc(100% - 32px));
11
+ max-width: none;
12
+ max-height: min(calc(100vh - 48px), calc(100dvh - 48px));
13
+ margin: auto;
14
+ padding: 0;
15
+ overflow: hidden;
16
+ border: 1px solid var(--nx-border);
17
+ border-radius: var(--nx-radius-lg);
18
+ background: var(--nx-surface);
19
+ color: var(--nx-text);
20
+ box-shadow: var(--nx-shadow);
21
+ outline: none;
22
+ /* Where the title bar moved it, from the center (set by the Popup while dragging). */
23
+ translate: var(--nx-popup-x, 0px) var(--nx-popup-y, 0px);
24
+ }
25
+
26
+ /*
27
+ * Opening and closing are a plain fade, the popup and its backdrop together: it appears where it stays, at the size
28
+ * it keeps, so nothing in it moves while it fades. Closing waits for its fade (`afterAnimation`), and takes no clicks
29
+ * meanwhile, so a double-click on Save does not submit twice; with animations off there is no fade to wait for.
30
+ */
31
+ .nx-popup[open] {
32
+ display: flex;
33
+ flex-direction: column;
34
+ animation: nx-popup-in 160ms ease-out;
35
+ }
36
+
37
+ .nx-popup::backdrop {
38
+ background: rgb(0 0 0 / 0.5);
39
+ animation: nx-popup-in 160ms ease-out;
40
+ }
41
+
42
+ .nx-popup--closing[open] {
43
+ animation: nx-popup-out 120ms ease-in forwards;
44
+ pointer-events: none;
45
+ }
46
+
47
+ .nx-popup--closing::backdrop {
48
+ animation: nx-popup-out 120ms ease-in forwards;
49
+ }
50
+
51
+ @keyframes nx-popup-in {
52
+ from {
53
+ opacity: 0;
54
+ }
55
+ }
56
+
57
+ @keyframes nx-popup-out {
58
+ to {
59
+ opacity: 0;
60
+ }
61
+ }
62
+
63
+ /* The page behind a modal popup stops scrolling; the Popup sets --nx-scrollbar-gap to the scrollbar's width, so nothing shifts. */
64
+ :root:has(.nx-popup:modal) {
65
+ overflow: hidden;
66
+ padding-right: var(--nx-scrollbar-gap, 0px);
67
+ }
68
+
69
+ /* Sizes */
70
+ .nx-popup--xs {
71
+ --nx-popup-width: 360px;
72
+ }
73
+
74
+ .nx-popup--sm {
75
+ --nx-popup-width: 440px;
76
+ }
77
+
78
+ .nx-popup--lg {
79
+ --nx-popup-width: 720px;
80
+ }
81
+
82
+ .nx-popup--xl {
83
+ --nx-popup-width: 960px;
84
+ }
85
+
86
+ .nx-popup--full {
87
+ --nx-popup-width: calc(100% - 32px);
88
+
89
+ height: calc(100vh - 32px);
90
+ max-height: none;
91
+ }
92
+
93
+ /* Maximized: the whole window, edge to edge, wherever it was moved. Two classes, to win over the sizes and phones. */
94
+ .nx-popup.nx-popup--maximized {
95
+ width: 100%;
96
+ height: 100%;
97
+ max-height: none;
98
+ border-width: 0;
99
+ border-radius: 0;
100
+ translate: none;
101
+ }
102
+
103
+ /* Minimizing: the page comes back while the popup flies to the dock. */
104
+ .nx-popup[data-nx-minimizing] {
105
+ pointer-events: none;
106
+ }
107
+
108
+ .nx-popup[data-nx-minimizing]::backdrop {
109
+ animation: nx-popup-out 200ms ease-in forwards;
110
+ }
111
+
112
+ /* Header, either design: the title bar moves the popup, so it selects no text and a touch drags instead of scrolling. */
113
+ .nx-popup__head {
114
+ display: flex;
115
+ flex-shrink: 0;
116
+ }
117
+
118
+ .nx-popup--movable .nx-popup__head {
119
+ cursor: move;
120
+ touch-action: none;
121
+ user-select: none;
122
+ }
123
+
124
+ .nx-popup__title,
125
+ .nx-popup__description {
126
+ margin: 0;
127
+ }
128
+
129
+ .nx-popup__controls {
130
+ display: flex;
131
+ flex-shrink: 0;
132
+ margin-left: auto;
133
+ }
134
+
135
+ .nx-popup__control {
136
+ display: grid;
137
+ place-items: center;
138
+ padding: 0;
139
+ border: 0;
140
+ background: none;
141
+ color: var(--nx-text-muted);
142
+ cursor: pointer;
143
+ transition:
144
+ background-color var(--nx-duration),
145
+ color var(--nx-duration);
146
+ }
147
+
148
+ .nx-popup__control .nx-icon {
149
+ vertical-align: 0;
150
+ }
151
+
152
+ /* Close turns red on hover, the one button that throws away what the popup holds. */
153
+ .nx-popup__close:hover {
154
+ background: var(--nx-danger-solid);
155
+ color: var(--nx-danger-text);
156
+ }
157
+
158
+ /*
159
+ * Window (the default): one low title bar, as on a desktop window. The icon and the title on the left, the
160
+ * description after the title in muted text, and the window buttons filling the top right corner.
161
+ */
162
+ .nx-popup--window {
163
+ --nx-popup-bar: 32px;
164
+ }
165
+
166
+ .nx-popup--window .nx-popup__head {
167
+ box-sizing: content-box;
168
+ align-items: center;
169
+ gap: var(--nx-space-2);
170
+ height: var(--nx-popup-bar);
171
+ padding-left: var(--nx-space-3);
172
+ border-bottom: 1px solid var(--nx-border);
173
+ background: color-mix(in oklab, var(--nx-surface), var(--nx-text) 4%);
174
+ }
175
+
176
+ .nx-popup--window .nx-popup__icon {
177
+ display: grid;
178
+ flex-shrink: 0;
179
+ place-items: center;
180
+ color: var(--nx-primary);
181
+ }
182
+
183
+ .nx-popup--window .nx-popup__icon .nx-icon {
184
+ width: 16px;
185
+ height: 16px;
186
+ vertical-align: 0;
187
+ }
188
+
189
+ /* One line: the title keeps its room, the description takes what is left, and both end in … when cut. */
190
+ .nx-popup--window .nx-popup__heading {
191
+ display: flex;
192
+ flex: 1 1 auto;
193
+ align-items: baseline;
194
+ gap: var(--nx-space-2);
195
+ min-width: 0;
196
+ white-space: nowrap;
197
+ }
198
+
199
+ .nx-popup--window .nx-popup__title {
200
+ flex: 0 1 auto;
201
+ min-width: 0;
202
+ overflow: hidden;
203
+ font-size: var(--nx-text-sm);
204
+ font-weight: 600;
205
+ line-height: var(--nx-popup-bar);
206
+ text-overflow: ellipsis;
207
+ }
208
+
209
+ .nx-popup--window .nx-popup__description {
210
+ flex: 1 1 0;
211
+ min-width: 0;
212
+ overflow: hidden;
213
+ color: var(--nx-text-muted);
214
+ font-size: var(--nx-text-xs);
215
+ text-overflow: ellipsis;
216
+ }
217
+
218
+ .nx-popup--window .nx-popup__controls {
219
+ align-self: stretch;
220
+ }
221
+
222
+ .nx-popup--window .nx-popup__control {
223
+ width: 46px;
224
+ height: 100%;
225
+ }
226
+
227
+ .nx-popup--window .nx-popup__control:hover {
228
+ background: color-mix(in oklab, var(--nx-text) 9%, transparent);
229
+ color: var(--nx-text);
230
+ }
231
+
232
+ .nx-popup--window .nx-popup__control:active {
233
+ background: color-mix(in oklab, var(--nx-text) 15%, transparent);
234
+ }
235
+
236
+ .nx-popup--window .nx-popup__close:hover {
237
+ background: var(--nx-danger-solid);
238
+ color: var(--nx-danger-text);
239
+ }
240
+
241
+ .nx-popup--window .nx-popup__close:active {
242
+ background: var(--nx-danger-hover);
243
+ }
244
+
245
+ /* The buttons touch the popup's edges, where an outer ring would be cut off, so the ring is drawn inside. */
246
+ .nx-popup--window .nx-popup__control:focus-visible {
247
+ outline: none;
248
+ box-shadow: inset 0 0 0 2px var(--nx-primary);
249
+ }
250
+
251
+ /* Thin lines, as desktop window buttons have; the square glyphs fill more of their box, so they are drawn smaller. */
252
+ .nx-popup--window .nx-popup__control .nx-icon {
253
+ width: 16px;
254
+ height: 16px;
255
+ stroke-width: 1.5;
256
+ }
257
+
258
+ .nx-popup--window .nx-popup__maximize .nx-icon {
259
+ width: 13px;
260
+ height: 13px;
261
+ stroke-width: 1.75;
262
+ }
263
+
264
+ /* Touch screens get taller buttons. */
265
+ @media (pointer: coarse) {
266
+ .nx-popup--window {
267
+ --nx-popup-bar: 40px;
268
+ }
269
+
270
+ .nx-popup--window .nx-popup__control {
271
+ width: 48px;
272
+ }
273
+ }
274
+
275
+ /*
276
+ * Dialog (message boxes): a larger heading, the icon in a circle that carries the tone, the description under the
277
+ * title, and a small close button.
278
+ */
279
+ .nx-popup--dialog .nx-popup__head {
280
+ align-items: flex-start;
281
+ gap: var(--nx-space-3);
282
+ padding: var(--nx-space-5) var(--nx-space-5) 0;
283
+ }
284
+
285
+ .nx-popup--dialog .nx-popup__icon {
286
+ display: grid;
287
+ flex-shrink: 0;
288
+ place-items: center;
289
+ width: 40px;
290
+ height: 40px;
291
+ border-radius: 50%;
292
+ background: var(--nx-primary-soft);
293
+ color: var(--nx-primary);
294
+ }
295
+
296
+ .nx-popup--dialog .nx-popup__icon .nx-icon {
297
+ width: 22px;
298
+ height: 22px;
299
+ vertical-align: 0;
300
+ }
301
+
302
+ .nx-popup--dialog .nx-popup__heading {
303
+ flex: 1 1 auto;
304
+ min-width: 0;
305
+ padding-top: 2px;
306
+ }
307
+
308
+ .nx-popup--dialog .nx-popup__title {
309
+ font-size: var(--nx-text-lg);
310
+ font-weight: 600;
311
+ line-height: 1.35;
312
+ overflow-wrap: anywhere;
313
+ }
314
+
315
+ .nx-popup--dialog .nx-popup__description {
316
+ margin-top: 4px;
317
+ color: var(--nx-text-muted);
318
+ font-size: var(--nx-text-sm);
319
+ line-height: 1.45;
320
+ }
321
+
322
+ .nx-popup--dialog .nx-popup__controls {
323
+ gap: 2px;
324
+ margin: -6px -8px 0 auto;
325
+ }
326
+
327
+ .nx-popup--dialog .nx-popup__control {
328
+ width: 30px;
329
+ height: 30px;
330
+ border-radius: var(--nx-radius);
331
+ }
332
+
333
+ .nx-popup--dialog .nx-popup__control:not(.nx-popup__close):hover {
334
+ background: var(--nx-surface-hover);
335
+ color: var(--nx-text);
336
+ }
337
+
338
+ .nx-popup--dialog .nx-popup__control:focus-visible {
339
+ outline: none;
340
+ box-shadow: var(--nx-focus-ring);
341
+ }
342
+
343
+ .nx-popup--dialog .nx-popup__control .nx-icon {
344
+ width: 18px;
345
+ height: 18px;
346
+ }
347
+
348
+ .nx-popup--dialog .nx-popup__maximize .nx-icon {
349
+ width: 15px;
350
+ height: 15px;
351
+ }
352
+
353
+ /* Body: scrolls when the content is taller than the window allows. */
354
+ /*
355
+ * Focus scrolls a field's control to the body's edge: `scroll-padding` keeps room past it for what the field draws
356
+ * outside, an outlined field's raised label above (0.6em of the xs text, and the 2px outline) and the box's border and
357
+ * 3px focus ring below. A field first in the body has its padding for that.
358
+ */
359
+ .nx-popup__body {
360
+ flex: 1 1 auto;
361
+ min-height: 0;
362
+ padding: var(--nx-space-4) var(--nx-space-5) var(--nx-space-5);
363
+ overflow: auto;
364
+ overscroll-behavior: contain;
365
+ scroll-padding: calc(var(--nx-text-xs) * 0.6 + 2px) 4px 4px;
366
+ }
367
+
368
+ .nx-popup__head + .nx-popup__body:empty {
369
+ padding-top: var(--nx-space-2);
370
+ }
371
+
372
+ .nx-popup__body:only-child,
373
+ .nx-popup--window .nx-popup__body {
374
+ padding-top: var(--nx-space-5);
375
+ }
376
+
377
+ .nx-popup--flush .nx-popup__body {
378
+ padding: 0;
379
+ }
380
+
381
+ /* Footer: buttons at the end. */
382
+ .nx-popup__foot {
383
+ display: flex;
384
+ flex-shrink: 0;
385
+ flex-wrap: wrap;
386
+ align-items: center;
387
+ justify-content: flex-end;
388
+ gap: var(--nx-space-2);
389
+ padding: var(--nx-space-3) var(--nx-space-5);
390
+ border-top: 1px solid var(--nx-border);
391
+ }
392
+
393
+ /* A form's bar fills the foot, so its status line sits beside the buttons instead of on a row of its own. */
394
+ .nx-popup__foot > .nx-form-buttons {
395
+ flex: 1 1 auto;
396
+ }
397
+
398
+ /* Message boxes: a small popup whose icon carries the tone. */
399
+ .nx-popup--message .nx-popup__body {
400
+ padding-top: var(--nx-space-2);
401
+ }
402
+
403
+ .nx-popup--message .nx-popup__body:empty {
404
+ padding: var(--nx-space-3) 0 0;
405
+ }
406
+
407
+ .nx-message__text {
408
+ color: var(--nx-text-muted);
409
+ line-height: 1.5;
410
+ }
411
+
412
+ /* What a question is about, in full (`details`): read as content, so in the normal text colour, under the message. */
413
+ .nx-message__details {
414
+ color: var(--nx-text);
415
+ }
416
+
417
+ .nx-message__text + .nx-message__details {
418
+ margin-top: var(--nx-space-4);
419
+ }
420
+
421
+ .nx-message__form {
422
+ display: grid;
423
+ gap: var(--nx-space-4);
424
+ }
425
+
426
+ .nx-popup--tone-info .nx-popup__icon {
427
+ background: var(--nx-info-soft);
428
+ color: var(--nx-info);
429
+ }
430
+
431
+ .nx-popup--tone-success .nx-popup__icon {
432
+ background: var(--nx-success-soft);
433
+ color: var(--nx-success);
434
+ }
435
+
436
+ .nx-popup--tone-warning .nx-popup__icon {
437
+ background: var(--nx-warning-soft);
438
+ color: var(--nx-warning);
439
+ }
440
+
441
+ .nx-popup--tone-danger .nx-popup__icon {
442
+ background: var(--nx-danger-soft);
443
+ color: var(--nx-danger);
444
+ }
445
+
446
+ /*
447
+ * The dock: minimized popups as bars at the bottom right. Several stack behind the newest, with cards peeking out
448
+ * above it and a count that opens the whole list. It sits on the page, so an open popup's backdrop covers it.
449
+ */
450
+ .nx-dock {
451
+ --nx-dock-edge: 16px;
452
+
453
+ position: fixed;
454
+ right: calc(var(--nx-dock-edge) + env(safe-area-inset-right, 0px));
455
+ bottom: calc(var(--nx-dock-edge) + env(safe-area-inset-bottom, 0px));
456
+ z-index: 1000;
457
+ display: flex;
458
+ flex-direction: column;
459
+ align-items: flex-end;
460
+ gap: var(--nx-space-2);
461
+ width: min(300px, calc(100% - 2 * var(--nx-dock-edge)));
462
+ max-height: calc(100dvh - 2 * var(--nx-dock-edge));
463
+ /* Only the bars and the count take clicks, not the space between them. */
464
+ pointer-events: none;
465
+ }
466
+
467
+ .nx-dock > * {
468
+ pointer-events: auto;
469
+ }
470
+
471
+ /* While a popup locks the page, its scrollbar's space is padding; the dock keeps its place beside it. */
472
+ :root:has(.nx-popup:modal) .nx-dock {
473
+ right: calc(var(--nx-dock-edge) + var(--nx-scrollbar-gap, 0px) + env(safe-area-inset-right, 0px));
474
+ }
475
+
476
+ .nx-dock__toggle {
477
+ display: inline-flex;
478
+ flex-shrink: 0;
479
+ align-items: center;
480
+ gap: 6px;
481
+ height: 28px;
482
+ padding: 0 10px 0 4px;
483
+ border: 1px solid var(--nx-border);
484
+ border-radius: var(--nx-radius-full);
485
+ background: light-dark(var(--nx-surface), var(--nx-surface-2));
486
+ color: var(--nx-text);
487
+ box-shadow: var(--nx-shadow-sm);
488
+ font: inherit;
489
+ font-size: var(--nx-text-sm);
490
+ font-weight: 600;
491
+ cursor: pointer;
492
+ transition: background-color var(--nx-duration);
493
+ }
494
+
495
+ .nx-dock__toggle:hover {
496
+ background: var(--nx-surface-hover);
497
+ }
498
+
499
+ .nx-dock__toggle:focus-visible {
500
+ outline: none;
501
+ box-shadow: var(--nx-focus-ring);
502
+ }
503
+
504
+ .nx-dock__toggle .nx-icon {
505
+ width: 16px;
506
+ height: 16px;
507
+ color: var(--nx-text-muted);
508
+ }
509
+
510
+ .nx-dock__count {
511
+ display: grid;
512
+ place-items: center;
513
+ min-width: 20px;
514
+ height: 20px;
515
+ padding: 0 6px;
516
+ border-radius: var(--nx-radius-full);
517
+ background: var(--nx-primary-solid);
518
+ color: var(--nx-primary-text);
519
+ font-size: var(--nx-text-xs);
520
+ font-variant-numeric: tabular-nums;
521
+ line-height: 1;
522
+ }
523
+
524
+ .nx-dock__list {
525
+ position: relative;
526
+ isolation: isolate;
527
+ display: flex;
528
+ flex-direction: column;
529
+ gap: 6px;
530
+ width: 100%;
531
+ min-height: 0;
532
+ margin: 0;
533
+ padding: 0;
534
+ list-style: none;
535
+ }
536
+
537
+ /* Open: the whole list, oldest at the top; it scrolls when taller than the window, with room for the shadows. */
538
+ .nx-dock--open .nx-dock__list {
539
+ margin: -8px;
540
+ padding: 8px;
541
+ width: calc(100% + 16px);
542
+ overflow-y: auto;
543
+ overscroll-behavior: contain;
544
+ }
545
+
546
+ /* Stacked: one or two cards peek out above the newest bar; a click on them opens the list. */
547
+ .nx-dock__list[data-depth] {
548
+ margin-top: calc(var(--nx-dock-depth) * 5px);
549
+ cursor: pointer;
550
+ }
551
+
552
+ .nx-dock__list[data-depth="1"] {
553
+ --nx-dock-depth: 1;
554
+ }
555
+
556
+ .nx-dock__list[data-depth="2"] {
557
+ --nx-dock-depth: 2;
558
+ }
559
+
560
+ .nx-dock__list[data-depth]::before,
561
+ .nx-dock__list[data-depth="2"]::after {
562
+ content: "";
563
+ position: absolute;
564
+ top: -5px;
565
+ right: 8px;
566
+ left: 8px;
567
+ z-index: -1;
568
+ height: 24px;
569
+ border: 1px solid var(--nx-border);
570
+ border-radius: var(--nx-radius-lg);
571
+ background: light-dark(var(--nx-surface), var(--nx-surface-2));
572
+ box-shadow: var(--nx-shadow-sm);
573
+ }
574
+
575
+ .nx-dock__list[data-depth="2"]::after {
576
+ top: -10px;
577
+ right: 16px;
578
+ left: 16px;
579
+ z-index: -2;
580
+ }
581
+
582
+ .nx-dock__item {
583
+ display: flex;
584
+ flex-shrink: 0;
585
+ align-items: center;
586
+ gap: 2px;
587
+ height: 44px;
588
+ padding-right: 6px;
589
+ border: 1px solid var(--nx-border);
590
+ border-radius: var(--nx-radius-lg);
591
+ background: light-dark(var(--nx-surface), var(--nx-surface-2));
592
+ color: var(--nx-text);
593
+ box-shadow: var(--nx-shadow);
594
+ cursor: default;
595
+ transition: background-color var(--nx-duration);
596
+ animation: nx-dock-in 180ms cubic-bezier(0.2, 0, 0, 1);
597
+ }
598
+
599
+ .nx-dock__item:has(.nx-dock__window:hover) {
600
+ background: var(--nx-surface-hover);
601
+ }
602
+
603
+ .nx-dock__item:has(.nx-dock__window:focus-visible) {
604
+ box-shadow: var(--nx-focus-ring), var(--nx-shadow);
605
+ }
606
+
607
+ /* The bar's title part brings the popup back. */
608
+ .nx-dock__window {
609
+ display: flex;
610
+ flex: 1 1 auto;
611
+ align-items: center;
612
+ gap: 10px;
613
+ min-width: 0;
614
+ height: 100%;
615
+ padding: 0 6px 0 7px;
616
+ border: 0;
617
+ border-radius: inherit;
618
+ background: none;
619
+ color: inherit;
620
+ font: inherit;
621
+ text-align: start;
622
+ cursor: pointer;
623
+ }
624
+
625
+ .nx-dock__window:focus-visible {
626
+ outline: none;
627
+ }
628
+
629
+ .nx-dock__icon {
630
+ display: grid;
631
+ flex-shrink: 0;
632
+ place-items: center;
633
+ width: 28px;
634
+ height: 28px;
635
+ border-radius: var(--nx-radius);
636
+ background: var(--nx-primary-soft);
637
+ color: var(--nx-primary);
638
+ }
639
+
640
+ .nx-dock__icon .nx-icon {
641
+ width: 16px;
642
+ height: 16px;
643
+ vertical-align: 0;
644
+ }
645
+
646
+ /* The title, and under it what the window is about when it says (its description): the record, the place. */
647
+ .nx-dock__text {
648
+ display: flex;
649
+ flex: 1 1 auto;
650
+ flex-direction: column;
651
+ min-width: 0;
652
+ }
653
+
654
+ .nx-dock__title {
655
+ min-width: 0;
656
+ overflow: hidden;
657
+ font-size: var(--nx-text-md);
658
+ font-weight: 600;
659
+ line-height: 1.3;
660
+ text-overflow: ellipsis;
661
+ white-space: nowrap;
662
+ }
663
+
664
+ .nx-dock__description {
665
+ min-width: 0;
666
+ overflow: hidden;
667
+ color: var(--nx-text-muted);
668
+ font-size: var(--nx-text-xs);
669
+ line-height: 1.3;
670
+ text-overflow: ellipsis;
671
+ white-space: nowrap;
672
+ }
673
+
674
+ /* Muted text on the hover tint moves toward the text color, so it keeps 4.5:1 in every theme. */
675
+ .nx-dock__window:hover .nx-dock__description {
676
+ color: color-mix(in oklab, var(--nx-text-muted), var(--nx-text) 35%);
677
+ }
678
+
679
+ .nx-dock__restore {
680
+ flex-shrink: 0;
681
+ width: 18px;
682
+ height: 18px;
683
+ color: var(--nx-text-muted);
684
+ }
685
+
686
+ .nx-dock__window:hover .nx-dock__restore {
687
+ color: var(--nx-text);
688
+ }
689
+
690
+ .nx-dock__close {
691
+ display: grid;
692
+ flex-shrink: 0;
693
+ place-items: center;
694
+ width: 30px;
695
+ height: 30px;
696
+ padding: 0;
697
+ border: 0;
698
+ border-radius: var(--nx-radius);
699
+ background: none;
700
+ color: var(--nx-text-muted);
701
+ cursor: pointer;
702
+ transition:
703
+ background-color var(--nx-duration),
704
+ color var(--nx-duration);
705
+ }
706
+
707
+ .nx-dock__close:hover {
708
+ background: var(--nx-danger-solid);
709
+ color: var(--nx-danger-text);
710
+ }
711
+
712
+ .nx-dock__close:focus-visible {
713
+ outline: none;
714
+ box-shadow: var(--nx-focus-ring);
715
+ }
716
+
717
+ .nx-dock__close .nx-icon {
718
+ width: 16px;
719
+ height: 16px;
720
+ vertical-align: 0;
721
+ }
722
+
723
+ @keyframes nx-dock-in {
724
+ from {
725
+ opacity: 0;
726
+ transform: translateY(8px);
727
+ }
728
+ }
729
+
730
+ @media (max-width: 600px) {
731
+ .nx-popup {
732
+ width: calc(100% - 16px);
733
+ max-height: min(calc(100vh - 16px), calc(100dvh - 16px));
734
+ }
735
+
736
+ .nx-popup--dialog .nx-popup__head {
737
+ padding: var(--nx-space-4) var(--nx-space-4) 0;
738
+ }
739
+
740
+ .nx-popup__body {
741
+ padding: var(--nx-space-3) var(--nx-space-4) var(--nx-space-4);
742
+ }
743
+
744
+ .nx-popup--window:not(.nx-popup--flush) .nx-popup__body {
745
+ padding-top: var(--nx-space-4);
746
+ }
747
+
748
+ .nx-popup__foot {
749
+ padding: var(--nx-space-3) var(--nx-space-4);
750
+ }
751
+
752
+ /*
753
+ * A phone has no room for the description after the title: it takes the lines under the title it needs, in full,
754
+ * and the bar grows with it. The icon and the window buttons keep the bar's first line; the title stays one line.
755
+ */
756
+ .nx-popup--window .nx-popup__head {
757
+ align-items: flex-start;
758
+ height: auto;
759
+ min-height: var(--nx-popup-bar);
760
+ }
761
+
762
+ .nx-popup--window .nx-popup__icon {
763
+ align-self: flex-start;
764
+ height: var(--nx-popup-bar);
765
+ }
766
+
767
+ .nx-popup--window .nx-popup__heading {
768
+ flex-wrap: wrap;
769
+ row-gap: 0;
770
+ }
771
+
772
+ .nx-popup--window .nx-popup__title {
773
+ flex-basis: 100%;
774
+ }
775
+
776
+ .nx-popup--window .nx-popup__description:not(:empty) {
777
+ flex-basis: 100%;
778
+ margin-top: calc(var(--nx-space-1) * -1);
779
+ padding-bottom: var(--nx-space-2);
780
+ overflow: visible;
781
+ line-height: 1.45;
782
+ white-space: normal;
783
+ overflow-wrap: anywhere;
784
+ }
785
+
786
+ .nx-popup--window .nx-popup__controls {
787
+ align-self: flex-start;
788
+ height: var(--nx-popup-bar);
789
+ }
790
+ }
791
+
792
+ [data-nx-motion="reduce"] .nx-popup[open],
793
+ [data-nx-motion="reduce"] .nx-popup--closing[open],
794
+ [data-nx-motion="reduce"] .nx-popup::backdrop,
795
+ [data-nx-motion="reduce"] .nx-popup--closing::backdrop,
796
+ [data-nx-motion="reduce"] .nx-popup[data-nx-minimizing]::backdrop,
797
+ [data-nx-motion="reduce"] .nx-dock__item {
798
+ animation: none;
799
+ }
800
+ }