nexus-shared 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +446 -0
  2. package/LICENSE +21 -21
  3. package/README.md +1084 -903
  4. package/dist/Client.Index.d.ts +14 -0
  5. package/dist/Client.Index.js +14 -0
  6. package/dist/Components/Chats/Chat.d.ts +28 -0
  7. package/dist/Components/Chats/Chat.js +19 -0
  8. package/dist/Components/Chats/ChatButton.d.ts +26 -0
  9. package/dist/Components/Chats/ChatButton.js +41 -0
  10. package/dist/Components/Chats/ChatComposer.d.ts +41 -0
  11. package/dist/Components/Chats/ChatComposer.js +177 -0
  12. package/dist/Components/Chats/ChatConversations.d.ts +35 -0
  13. package/dist/Components/Chats/ChatConversations.js +38 -0
  14. package/dist/Components/Chats/ChatPanel.d.ts +66 -0
  15. package/dist/Components/Chats/ChatPanel.js +94 -0
  16. package/dist/Components/Chats/ChatParts.d.ts +87 -0
  17. package/dist/Components/Chats/ChatParts.js +96 -0
  18. package/dist/Components/Chats/ChatThread.d.ts +22 -0
  19. package/dist/Components/Chats/ChatThread.js +96 -0
  20. package/dist/Components/Documents/Button.d.ts +6 -1
  21. package/dist/Components/Documents/Button.js +2 -2
  22. package/dist/Components/Documents/Menu.js +44 -28
  23. package/dist/Components/Documents/SplitButton.d.ts +3 -1
  24. package/dist/Components/Documents/SplitButton.js +12 -8
  25. package/dist/Components/Documents/TabButtons.d.ts +29 -8
  26. package/dist/Components/Documents/TabButtons.js +24 -5
  27. package/dist/Components/Forms/ApiForm.d.ts +6 -4
  28. package/dist/Components/Forms/ApiForm.js +15 -14
  29. package/dist/Components/Forms/Crud.js +342 -71
  30. package/dist/Components/Forms/CrudParts.d.ts +5 -1
  31. package/dist/Components/Forms/CrudParts.js +37 -5
  32. package/dist/Components/Forms/ExcelImport.d.ts +58 -0
  33. package/dist/Components/Forms/ExcelImport.js +319 -0
  34. package/dist/Components/Forms/Form.js +9 -13
  35. package/dist/Components/Forms/SubmitForm.d.ts +11 -2
  36. package/dist/Components/Forms/SubmitForm.js +23 -12
  37. package/dist/Components/Inputs/DateTimePicker.js +2 -1
  38. package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
  39. package/dist/Components/Inputs/DropdownParts.js +4 -6
  40. package/dist/Components/Inputs/GroupForm.js +4 -2
  41. package/dist/Components/Inputs/InputRenderer.d.ts +2 -0
  42. package/dist/Components/Inputs/InputRenderer.js +1 -1
  43. package/dist/Components/Inputs/NumberBox.js +24 -2
  44. package/dist/Components/Inputs/ReadOnlyNotice.js +2 -1
  45. package/dist/Components/Inputs/RowsInput.d.ts +12 -1
  46. package/dist/Components/Inputs/RowsInput.js +55 -6
  47. package/dist/Components/Inputs/Slider.js +4 -1
  48. package/dist/Components/Inputs/TabularForm.d.ts +7 -3
  49. package/dist/Components/Inputs/TabularForm.js +117 -20
  50. package/dist/Components/Inputs/TimePanel.js +4 -3
  51. package/dist/Components/Inputs/TimePicker.js +2 -1
  52. package/dist/Components/Layouts/Popup.js +1 -4
  53. package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
  54. package/dist/Components/Layouts/ThemePicker.js +77 -6
  55. package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
  56. package/dist/Components/Layouts/ThemeScript.js +4 -2
  57. package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
  58. package/dist/Components/Layouts/ThemeSwitcher.js +39 -18
  59. package/dist/Components/Viewers/Avatar.d.ts +21 -0
  60. package/dist/Components/Viewers/Avatar.js +21 -0
  61. package/dist/Components/Viewers/DataTable.js +262 -60
  62. package/dist/Components/Viewers/DataTableColumns.d.ts +32 -0
  63. package/dist/Components/Viewers/DataTableColumns.js +200 -0
  64. package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
  65. package/dist/Components/Viewers/DataTableFilters.js +68 -0
  66. package/dist/Components/Viewers/DataTableParts.d.ts +8 -4
  67. package/dist/Components/Viewers/DataTableParts.js +24 -12
  68. package/dist/Helpers/AnimationHelpers.d.ts +11 -0
  69. package/dist/Helpers/AnimationHelpers.js +20 -2
  70. package/dist/Helpers/ApiClient.d.ts +22 -0
  71. package/dist/Helpers/ApiClient.js +4 -1
  72. package/dist/Helpers/ApiFormHelpers.d.ts +27 -4
  73. package/dist/Helpers/ApiFormHelpers.js +73 -9
  74. package/dist/Helpers/ApiModules.d.ts +0 -4
  75. package/dist/Helpers/ApiModules.js +1 -0
  76. package/dist/Helpers/ApiResponses.d.ts +14 -4
  77. package/dist/Helpers/ApiResponses.js +116 -25
  78. package/dist/Helpers/ApiRoutes.d.ts +0 -10
  79. package/dist/Helpers/ApiRoutes.js +2 -10
  80. package/dist/Helpers/ChatBackend.d.ts +134 -0
  81. package/dist/Helpers/ChatBackend.js +332 -0
  82. package/dist/Helpers/ChatHelpers.d.ts +189 -0
  83. package/dist/Helpers/ChatHelpers.js +486 -0
  84. package/dist/Helpers/ChatHooks.d.ts +81 -0
  85. package/dist/Helpers/ChatHooks.js +175 -0
  86. package/dist/Helpers/ChatStore.d.ts +132 -0
  87. package/dist/Helpers/ChatStore.js +394 -0
  88. package/dist/Helpers/ChatSummaries.d.ts +39 -0
  89. package/dist/Helpers/ChatSummaries.js +118 -0
  90. package/dist/Helpers/CrudBackend.d.ts +54 -9
  91. package/dist/Helpers/CrudBackend.js +103 -6
  92. package/dist/Helpers/CrudHelpers.d.ts +39 -4
  93. package/dist/Helpers/CrudHelpers.js +76 -11
  94. package/dist/Helpers/ExcelBackend.d.ts +106 -0
  95. package/dist/Helpers/ExcelBackend.js +296 -0
  96. package/dist/Helpers/ExcelHelpers.d.ts +126 -0
  97. package/dist/Helpers/ExcelHelpers.js +344 -0
  98. package/dist/Helpers/FormStore.d.ts +11 -5
  99. package/dist/Helpers/FormStore.js +44 -6
  100. package/dist/Helpers/MessageBuilder.js +1 -2
  101. package/dist/Helpers/NumberHelpers.d.ts +7 -1
  102. package/dist/Helpers/NumberHelpers.js +41 -3
  103. package/dist/Helpers/PopoverHelpers.d.ts +46 -9
  104. package/dist/Helpers/PopoverHelpers.js +163 -24
  105. package/dist/Helpers/PopoverHooks.d.ts +24 -0
  106. package/dist/Helpers/PopoverHooks.js +62 -0
  107. package/dist/Helpers/RowsStore.d.ts +14 -3
  108. package/dist/Helpers/RowsStore.js +37 -3
  109. package/dist/Helpers/TableColumns.d.ts +7 -0
  110. package/dist/Helpers/TableColumns.js +41 -13
  111. package/dist/Helpers/TableExport.js +12 -12
  112. package/dist/Helpers/TableFilters.d.ts +25 -0
  113. package/dist/Helpers/TableFilters.js +162 -0
  114. package/dist/Helpers/TableHelpers.d.ts +15 -4
  115. package/dist/Helpers/TableHelpers.js +28 -4
  116. package/dist/Helpers/TableStore.d.ts +11 -3
  117. package/dist/Helpers/TableStore.js +16 -5
  118. package/dist/Helpers/ThemeHelpers.d.ts +21 -4
  119. package/dist/Helpers/ThemeHelpers.js +46 -9
  120. package/dist/Helpers/UserPreferences.d.ts +14 -0
  121. package/dist/Helpers/UserPreferences.js +99 -0
  122. package/dist/Interfaces/ApiInterfaces.d.ts +29 -4
  123. package/dist/Interfaces/ApiInterfaces.js +16 -10
  124. package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
  125. package/dist/Interfaces/ChatInterfaces.js +1 -0
  126. package/dist/Interfaces/CrudInterfaces.d.ts +92 -12
  127. package/dist/Interfaces/ExcelInterfaces.d.ts +227 -0
  128. package/dist/Interfaces/ExcelInterfaces.js +45 -0
  129. package/dist/Interfaces/FormInterfaces.d.ts +85 -7
  130. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  131. package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
  132. package/dist/Interfaces/PreferenceInterfaces.js +28 -0
  133. package/dist/Interfaces/TableInterfaces.d.ts +116 -9
  134. package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
  135. package/dist/Interfaces/ThemeInterfaces.js +15 -0
  136. package/dist/Services/BrowserApi.d.ts +18 -1
  137. package/dist/Services/BrowserApi.js +18 -0
  138. package/dist/Services/ChatApi.d.ts +37 -0
  139. package/dist/Services/ChatApi.js +98 -0
  140. package/dist/Services/ExcelApi.d.ts +48 -0
  141. package/dist/Services/ExcelApi.js +196 -0
  142. package/dist/Services/ServerApi.d.ts +6 -1
  143. package/dist/Services/ServerApi.js +3 -0
  144. package/dist/Services/ThemeService.d.ts +18 -2
  145. package/dist/Services/ThemeService.js +80 -16
  146. package/dist/Shared.Index.d.ts +10 -0
  147. package/dist/Shared.Index.js +10 -0
  148. package/package.json +85 -82
  149. package/src/Styles/Nexus.Avatar.css +79 -0
  150. package/src/Styles/Nexus.Base.css +117 -70
  151. package/src/Styles/Nexus.Button.css +180 -151
  152. package/src/Styles/Nexus.Calendar.css +273 -275
  153. package/src/Styles/Nexus.Chat.css +1443 -0
  154. package/src/Styles/Nexus.Choice.css +343 -345
  155. package/src/Styles/Nexus.Crud.css +168 -84
  156. package/src/Styles/Nexus.DateTime.css +81 -81
  157. package/src/Styles/Nexus.Dropdown.css +725 -774
  158. package/src/Styles/Nexus.Editor.css +295 -297
  159. package/src/Styles/Nexus.Excel.css +389 -0
  160. package/src/Styles/Nexus.Form.css +279 -298
  161. package/src/Styles/Nexus.Index.css +37 -34
  162. package/src/Styles/Nexus.Input.css +602 -637
  163. package/src/Styles/Nexus.Markdown.css +237 -265
  164. package/src/Styles/Nexus.Menu.css +237 -249
  165. package/src/Styles/Nexus.Popup.css +724 -731
  166. package/src/Styles/Nexus.RichText.css +2 -32
  167. package/src/Styles/Nexus.Rows.css +637 -566
  168. package/src/Styles/Nexus.Slider.css +276 -278
  169. package/src/Styles/Nexus.Tab.Buttons.css +432 -364
  170. package/src/Styles/Nexus.Table.css +1386 -1014
  171. package/src/Styles/Nexus.Theme.Picker.css +286 -159
  172. package/src/Styles/Nexus.Themes.css +194 -194
  173. package/src/Styles/Nexus.Time.css +144 -147
  174. package/src/Styles/Nexus.Toast.css +263 -265
  175. package/src/Styles/Nexus.Tokens.css +89 -72
  176. package/src/Styles/Nexus.Tree.css +431 -435
@@ -1,5 +1,7 @@
1
1
  import type { CSSProperties, ReactNode, Ref } from "react";
2
2
  import type { TableStore } from "../Helpers/TableStore.ts";
3
+ import type { ExcelEndpoints } from "./ExcelInterfaces.ts";
4
+ import type { InputParams } from "./FormInterfaces.ts";
3
5
  import type { Size, Tone } from "./TypeInterfaces.ts";
4
6
  /** A row's id: its `rowKey` field, such as `id`. */
5
7
  export type TableKey = string | number;
@@ -20,6 +22,15 @@ export interface TableColumn<TRow extends TableRow = TableRow> {
20
22
  key: string;
21
23
  /** The heading, and the column's name in the Columns menu and the details view. */
22
24
  label: string;
25
+ /**
26
+ * The heading of the band above this column: "Amount details" over the quantity, the price, and the total. Columns
27
+ * next to each other that name the same group share one heading spanning them all, and the columns with no group keep
28
+ * one heading of their own, as tall as both rows. A table whose columns name no group has one header row, as before.
29
+ *
30
+ * The bands are read off the order on screen, so a column the user drags out of its run takes its group's heading
31
+ * with it and the run it left is drawn with the same heading twice.
32
+ */
33
+ group?: string;
23
34
  /** Default `text`. Numbers, money, and percents line up at the end; dates and times follow the user's settings. */
24
35
  format?: TableColumnFormat;
25
36
  /** Digits after the decimal point of a number (money: 2). */
@@ -71,7 +82,13 @@ export interface TableSort {
71
82
  key: string;
72
83
  direction: TableSortDirection;
73
84
  }
74
- /** What the table shows: the search, the sort, and the page. */
85
+ /**
86
+ * The advanced filter's values, by input name: only the inputs that hold a value, in the order of the inputs, so the
87
+ * same filters always serialize the same way (a cache key, a request). A date range is its pair ["2026-10-01", null],
88
+ * a range slider [2, 7], a multiple choice its list, a ticked checkbox `true`.
89
+ */
90
+ export type TableFilters = Readonly<Record<string, unknown>>;
91
+ /** What the table shows: the search, the filters, the sort, and the page. */
75
92
  export interface TableQuery {
76
93
  /** The search text, trimmed. */
77
94
  search: string;
@@ -79,13 +96,18 @@ export interface TableQuery {
79
96
  /** The page, from 1. */
80
97
  page: number;
81
98
  pageSize: number;
99
+ /**
100
+ * The advanced filter while any input holds a value; absent otherwise. A client table matches its rows; a server
101
+ * table's loader receives the raw values (`filterRequest` gives them in a server's names).
102
+ */
103
+ filters?: TableFilters;
82
104
  }
83
105
  /** One page of rows, as a server sends it. */
84
106
  export interface TablePage<TRow extends TableRow = TableRow> {
85
107
  rows: readonly TRow[];
86
108
  /** Every row there is. Default: the rows given. */
87
109
  total?: number;
88
- /** The rows that match the search. Default: `total`. */
110
+ /** The rows that match the search and the filters. Default: `total`. */
89
111
  matches?: number;
90
112
  }
91
113
  /**
@@ -110,7 +132,7 @@ export interface TableState<TRow extends TableRow = TableRow> {
110
132
  offset: number;
111
133
  /** Every row there is. */
112
134
  total: number;
113
- /** The rows that match the search. */
135
+ /** The rows that match the search and the filters. */
114
136
  matches: number;
115
137
  query: TableQuery;
116
138
  /** The selected rows by key (keys as text), in the order they were picked. */
@@ -144,6 +166,37 @@ export interface TableViewColumn<TRow extends TableRow = TableRow> {
144
166
  /** Its width in pixels, once the user has set one. */
145
167
  width?: number;
146
168
  }
169
+ /**
170
+ * One cell of the band above the headings: a group's heading over the columns next to each other that name it, or a
171
+ * single column with no group, whose own heading stands in the band row and is as tall as both rows.
172
+ */
173
+ export interface TableHeadBand<TRow extends TableRow = TableRow> {
174
+ /** The heading above the columns, or `null` for a column with no group. */
175
+ group: string | null;
176
+ /** The columns under it, in the order they are drawn. One, for a column with no group. */
177
+ columns: TableViewColumn<TRow>[];
178
+ }
179
+ /**
180
+ * What a table needs to work a spreadsheet: the model's Excel endpoints, and what the page knows that the table does
181
+ * not. The search, the sort, and how many rows the search leaves are the table's own and are sent for it.
182
+ */
183
+ export interface TableExcel {
184
+ /** The model's endpoints, from `excelEndpoints(controller)`. */
185
+ endpoints: ExcelEndpoints;
186
+ /** What one row is, for the questions and the messages: "city". Default: what the backend's settings say. */
187
+ subject?: string;
188
+ /** Values the endpoints' own params need, such as a parent's id. */
189
+ params?: Record<string, string | number>;
190
+ /** Filters the page holds outside the table, sent beside the table's own (`filterInputs`); the page's win a clash. */
191
+ filters?: () => Record<string, unknown>;
192
+ /** After an import wrote something, before the table reads its rows again: drop here whatever the page keeps of them. */
193
+ onImported?: () => void;
194
+ /**
195
+ * No **Bulk insert** entry. A sheet with a required list cannot be typed in - its key is picked in the spreadsheet -
196
+ * and its panel says so; this leaves the entry out instead.
197
+ */
198
+ bulk?: false;
199
+ }
147
200
  /** An item of the table's options menu (⋮): Import, Print, a report of the page's own. */
148
201
  export interface TableMenuAction {
149
202
  /** Tells items apart; a test or a style can find it by `data-action`. */
@@ -159,6 +212,12 @@ export interface TableMenuAction {
159
212
  separator?: boolean;
160
213
  onSelect: () => void;
161
214
  }
215
+ /** A way to select rows by what they are, such as "Pinned" or "Not flagged". */
216
+ export interface TableSelectBy<TRow extends TableRow = TableRow> {
217
+ label: string;
218
+ icon?: ReactNode;
219
+ test: (row: TRow) => boolean;
220
+ }
162
221
  /** An action on one row, in its row: an icon button (`inline`) or an item of the row's menu. */
163
222
  export interface TableRowAction<TRow extends TableRow = TableRow> {
164
223
  /** Tells actions apart; a test or a style can find it by `data-action`. */
@@ -196,7 +255,7 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
196
255
  load?: TableLoader<TRow>;
197
256
  /** Default `client`. */
198
257
  paging?: TablePaging;
199
- /** Loads again, from the first page, when it changes: another list behind the same table, such as a CRUD's archive. */
258
+ /** Loads again, from the first page, when it changes: another list behind the same table, such as a CRUD's trash. */
200
259
  loadKey?: string | number;
201
260
  /** The field that tells rows apart. Default `id`. */
202
261
  rowKey?: string | ((row: TRow) => TableKey);
@@ -211,13 +270,33 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
211
270
  searchable?: boolean;
212
271
  /** Default "Search…". */
213
272
  searchPlaceholder?: string;
214
- /** Before the search box, on the left of the bar: tabs, filters. */
273
+ /** On the left of the bar, after the Add section: tabs, filters. On a narrow table the line under the search. */
215
274
  toolbar?: ReactNode;
216
- /** At the end of the bar: an Add button. */
275
+ /** The page's own buttons, in the Add section after the table's Add: first in the bar, last on a phone. */
217
276
  actions?: ReactNode;
218
- /** The Columns menu, to show and hide columns. Default true. */
277
+ /**
278
+ * The table's Add, first in the bar. With `excel`, its arrow holds the template, the import and the export, which
279
+ * then leave the options menu.
280
+ */
281
+ add?: {
282
+ label: ReactNode;
283
+ onClick: () => void;
284
+ };
285
+ /** The Columns panel, where columns are shown, hidden, and dragged into another order. Default true. */
219
286
  columnsMenu?: boolean;
220
- /** A Reload button. Default true when rows are loaded. */
287
+ /**
288
+ * The spreadsheet side of this table, against the backend's Excel controller. The options menu (⋮) then offers
289
+ * **Download template**, **Import from a spreadsheet** (the file is verified, the person goes over the rows, and the
290
+ * table reloads once they are written), **Bulk insert** (the same import with no file: the rows are typed into the
291
+ * same editor; offered only with `add`, as it is an Add of many rows) and, where the endpoints have one, **Export to
292
+ * Excel** - which asks the server with the search, the filters and the sort on screen instead of writing a file of
293
+ * the rows this browser happens to hold.
294
+ *
295
+ * It replaces `onImport`, which is the hook for a page that imports its own way. With `add`, they are under the
296
+ * Add's arrow instead of the options menu.
297
+ */
298
+ excel?: TableExcel;
299
+ /** A Reload button (in the options menu on a table under 375px wide). Default true when rows are loaded. */
221
300
  reloadable?: boolean;
222
301
  /** When the user presses Reload, before the rows load again: drop a cache of your own. */
223
302
  onReload?: () => void;
@@ -243,12 +322,23 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
243
322
  maxSelection?: number;
244
323
  /** After the selection changes. */
245
324
  onSelectionChange?: (rows: TRow[]) => void;
325
+ /**
326
+ * While rows are selected, their count opens a menu that selects again by what the rows are: every row on the page,
327
+ * then these, each picking exactly the rows on the page that pass its test.
328
+ */
329
+ selectBy?: readonly TableSelectBy<TRow>[];
246
330
  /** Actions on the selected rows. A function gets the selected rows. */
247
331
  massActions?: readonly TableMassAction<TRow>[] | ((rows: TRow[]) => readonly TableMassAction<TRow>[]);
248
332
  /** Actions of each row. */
249
333
  rowActions?: (row: TRow) => readonly TableRowAction<TRow>[];
250
334
  /** A double-click on a row, such as to open its details. */
251
335
  onRowOpen?: (row: TRow) => void;
336
+ /**
337
+ * A single click opens a row, not only a double-click: for tables whose rows are opened to be read more than they
338
+ * are selected. A click on a control in the row (its menu, a link, a checkbox) still does only what that control
339
+ * does, and so does a click that ends a text selection, so a value can still be copied out of a cell.
340
+ */
341
+ openOnClick?: boolean;
252
342
  /**
253
343
  * Rows kept in a section above the page, which the user folds open: the records they pinned. They are the rows as
254
344
  * they came from their own endpoint, and the same records also show in the list below. Leave it out for no section.
@@ -276,12 +366,29 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
276
366
  stateKey?: string;
277
367
  /** Milliseconds after the last key before a search goes to a server. Default 300; a table with every row searches at once. */
278
368
  searchDelay?: number;
369
+ /**
370
+ * The advanced filter: the inputs the rows can be filtered by, as a form takes them. Keep the array stable.
371
+ * With any, the options menu (⋮) offers **Filters**: a panel of these inputs under its button, applied as they change.
372
+ * Name an input after the column `key` it filters (else a path into the row); that name is also what a server gets.
373
+ * A `number` input named `{key}From` / `{key}To` is a lower / upper bound of column `{key}`.
374
+ * `required`, `defaultValue` and `defaultChecked` are ignored, so the Add form's params can be reused.
375
+ */
376
+ filterInputs?: readonly InputParams[];
279
377
  /** Shown when there are no rows at all. Default "No records yet." */
280
378
  emptyMessage?: ReactNode;
281
- /** Beside the empty message, such as an Add button. */
379
+ /** Beside the empty message. Default: the table's Add (`add`); `null` for none. */
282
380
  emptyAction?: ReactNode;
283
381
  /** The tallest the table grows before its rows scroll under a header that stays: pixels or a CSS length. */
284
382
  maxHeight?: number | string;
383
+ /**
384
+ * The table takes the whole height it is given instead of growing with its rows: the bar stays on top, the rows
385
+ * scroll inside the frame under a header that stays, and the footer sits at the bottom of the page. The page around
386
+ * it never scrolls, so the footer never moves and no scrollbar comes and goes as rows load.
387
+ *
388
+ * It needs a parent with a height. An `AdminPage` gives it one: the admin shell is exactly the window, and a page
389
+ * holding a filling table stretches it to the room under the heading. `maxHeight` is ignored while it is on.
390
+ */
391
+ fill?: boolean;
285
392
  /** Row height and text size. Default `md`. */
286
393
  size?: Extract<Size, "sm" | "md">;
287
394
  id?: string;
@@ -9,9 +9,34 @@ export interface ThemeOption {
9
9
  /** The themes in `nexus-shared/styles.css`. Each has a light and a dark palette. */
10
10
  export declare const NEXUS_THEMES: readonly ThemeOption[];
11
11
  export declare const DEFAULT_THEME = "nexus";
12
+ /** `compact` (default): today's sizes. `cozy`: every control and row a step taller, for touch screens and large monitors. */
13
+ export type Density = "compact" | "cozy";
14
+ /**
15
+ * Whether things move: `on` even when the operating system asks for less motion, `off` never, `system` as the
16
+ * operating system says (`prefers-reduced-motion`). Some people turn animations off for the whole desktop and still
17
+ * want them here, which is why this is a choice of its own.
18
+ */
19
+ export type AnimationMode = "on" | "off" | "system";
20
+ /** The scale grid, a development tool: off, a 4px sheet with a solid line every 48px, or millimetres and centimetres. */
21
+ export type ScaleGrid = "off" | "px" | "cm";
12
22
  /** Browser storage keys for the saved choice. */
13
23
  export declare const THEME_STORAGE_KEY = "nx-theme";
14
24
  export declare const THEME_MODE_STORAGE_KEY = "nx-mode";
25
+ export declare const DENSITY_STORAGE_KEY = "nx-density";
26
+ export declare const ANIMATIONS_STORAGE_KEY = "nx-animations";
27
+ export declare const GRID_STORAGE_KEY = "nx-grid";
15
28
  /** Attributes on `<html>` that select the theme and the mode. Without `data-nx-mode`, the system setting applies. */
16
29
  export declare const THEME_ATTRIBUTE = "data-nx-theme";
17
30
  export declare const THEME_MODE_ATTRIBUTE = "data-nx-mode";
31
+ /** `compact` or `cozy`; the size tokens follow it. */
32
+ export declare const DENSITY_ATTRIBUTE = "data-nx-density";
33
+ /** The visitor's animations choice, `on` or `off`. Without it, the system setting applies. */
34
+ export declare const ANIMATIONS_ATTRIBUTE = "data-nx-animations";
35
+ /**
36
+ * What the stylesheets read: `reduce` while nothing may move, `full` otherwise. `ThemeScript` works it out from the
37
+ * choice and the system setting before the first paint, and again when the system setting changes, so no component
38
+ * asks `prefers-reduced-motion` itself.
39
+ */
40
+ export declare const MOTION_ATTRIBUTE = "data-nx-motion";
41
+ /** `px` or `cm` while the scale grid shows; development builds only. */
42
+ export declare const GRID_ATTRIBUTE = "data-nx-grid";
@@ -12,6 +12,21 @@ export const DEFAULT_THEME = "nexus";
12
12
  /** Browser storage keys for the saved choice. */
13
13
  export const THEME_STORAGE_KEY = "nx-theme";
14
14
  export const THEME_MODE_STORAGE_KEY = "nx-mode";
15
+ export const DENSITY_STORAGE_KEY = "nx-density";
16
+ export const ANIMATIONS_STORAGE_KEY = "nx-animations";
17
+ export const GRID_STORAGE_KEY = "nx-grid";
15
18
  /** Attributes on `<html>` that select the theme and the mode. Without `data-nx-mode`, the system setting applies. */
16
19
  export const THEME_ATTRIBUTE = "data-nx-theme";
17
20
  export const THEME_MODE_ATTRIBUTE = "data-nx-mode";
21
+ /** `compact` or `cozy`; the size tokens follow it. */
22
+ export const DENSITY_ATTRIBUTE = "data-nx-density";
23
+ /** The visitor's animations choice, `on` or `off`. Without it, the system setting applies. */
24
+ export const ANIMATIONS_ATTRIBUTE = "data-nx-animations";
25
+ /**
26
+ * What the stylesheets read: `reduce` while nothing may move, `full` otherwise. `ThemeScript` works it out from the
27
+ * choice and the system setting before the first paint, and again when the system setting changes, so no component
28
+ * asks `prefers-reduced-motion` itself.
29
+ */
30
+ export const MOTION_ATTRIBUTE = "data-nx-motion";
31
+ /** `px` or `cm` while the scale grid shows; development builds only. */
32
+ export const GRID_ATTRIBUTE = "data-nx-grid";
@@ -1,5 +1,5 @@
1
1
  import { type ConfirmOptions } from "../Components/Layouts/MessageBox.tsx";
2
- import { type ApiCall } from "../Helpers/ApiClient.ts";
2
+ import { type ApiCall, type ApiResponseReader } from "../Helpers/ApiClient.ts";
3
3
  import { type ApiToastDefaults, type ApiToastOption } from "../Helpers/ApiToasts.ts";
4
4
  import type { ApiCallOptions, ApiResult, ApiTarget } from "../Interfaces/ApiInterfaces.ts";
5
5
  import type { DropdownItem, DropdownPage, DropdownPageRequest } from "../Interfaces/InputInterfaces.ts";
@@ -18,6 +18,12 @@ export interface BrowserApiConfig {
18
18
  credentials?: RequestCredentials;
19
19
  /** Milliseconds to wait for an answer. Default 60,000. */
20
20
  timeout?: number;
21
+ /**
22
+ * Reads every answer, for a backend that does not answer as Nexus or ASP.NET Core do. Default `readApiResponse`.
23
+ * A call still reads its own with `read`. Registered once, it holds for calls the components make inside
24
+ * themselves too (`Crud`'s lists and actions, `ApiForm`'s save), which cannot be given one.
25
+ */
26
+ read?: ApiResponseReader<BrowserApiOptions>;
21
27
  /** Which toasts show when a call does not say. Default: errors always, successes for changes. */
22
28
  toast?: ApiToastDefaults;
23
29
  /** After a 401, or an expired token (Nexus error 2003), such as to send the user to sign in. The error toast shows too. */
@@ -30,6 +36,17 @@ export interface BrowserApiConfig {
30
36
  * settings they name. Nothing is needed when the proxy is at `/api/app` and the defaults suit.
31
37
  */
32
38
  export declare function configureApi(config: BrowserApiConfig): void;
39
+ /**
40
+ * The app's default response reader, when it registered one (`configureApi({ read })`). For answers that did not come
41
+ * from a call and so have no client to read them: what an `ApiForm`'s own send function returned.
42
+ */
43
+ export declare function getApiReader(): ApiResponseReader<BrowserApiOptions> | undefined;
44
+ /**
45
+ * The app's call settings, for a request `api` cannot make: a download, which has to read the file's own bytes and the
46
+ * headers that name it, rather than an answer read as JSON. The headers, the token, and the cookie rule an app
47
+ * registered once belong on such a request too (`Services/ExcelApi.ts` is the one caller).
48
+ */
49
+ export declare function getApiCallSettings(): BrowserApiConfig;
33
50
  /** Options of `api.confirm`: the call's, and changes to the question. */
34
51
  export type ConfirmApiOptions = ApiCallOptions & BrowserApiOptions & {
35
52
  question?: Partial<ConfirmOptions>;
@@ -15,6 +15,21 @@ let settings = {};
15
15
  export function configureApi(config) {
16
16
  settings = { ...settings, ...config };
17
17
  }
18
+ /**
19
+ * The app's default response reader, when it registered one (`configureApi({ read })`). For answers that did not come
20
+ * from a call and so have no client to read them: what an `ApiForm`'s own send function returned.
21
+ */
22
+ export function getApiReader() {
23
+ return settings.read;
24
+ }
25
+ /**
26
+ * The app's call settings, for a request `api` cannot make: a download, which has to read the file's own bytes and the
27
+ * headers that name it, rather than an answer read as JSON. The headers, the token, and the cookie rule an app
28
+ * registered once belong on such a request too (`Services/ExcelApi.ts` is the one caller).
29
+ */
30
+ export function getApiCallSettings() {
31
+ return settings;
32
+ }
18
33
  /** Nexus error code for an expired token. */
19
34
  const TOKEN_EXPIRED = 2003;
20
35
  const TOAST_DURATION = 5000;
@@ -32,6 +47,9 @@ const config = {
32
47
  get credentials() {
33
48
  return settings.credentials;
34
49
  },
50
+ get read() {
51
+ return settings.read;
52
+ },
35
53
  dedupe: true,
36
54
  onStart(call) {
37
55
  const loading = planLoadingToast(call.context, call.options.toast);
@@ -0,0 +1,37 @@
1
+ import { type ChatBackend } from "../Helpers/ChatBackend.ts";
2
+ import { ChatSummaryCache } from "../Helpers/ChatSummaries.ts";
3
+ import type { ApiCallOptions } from "../Interfaces/ApiInterfaces.ts";
4
+ import type { ChatDomainValue, ChatId, ChatPlaceSource, ChatScope, ChatSource, ChatSummary } from "../Interfaces/ChatInterfaces.ts";
5
+ /** Options of one chat's source. */
6
+ export interface ApiChatSourceOptions {
7
+ /** Which conversation at the place, when the record holds more than one. */
8
+ conversationId?: ChatId;
9
+ /** The backend to use, when it is not the app's registered one. */
10
+ backend?: ChatBackend<ChatDomainValue>;
11
+ /** Extras on every call this source makes, such as a header. */
12
+ call?: Omit<ApiCallOptions, "body" | "signal" | "params">;
13
+ }
14
+ /**
15
+ * A conversation's data through the app's API.
16
+ *
17
+ * @example
18
+ * const scope = { domain: EDomainTypes.Leave, parentId: leave.id, userId: me.id };
19
+ * const source = useMemo(() => apiChatSource(scope), [scope.domain, scope.parentId, scope.userId]);
20
+ */
21
+ export declare function apiChatSource<TDomain extends ChatDomainValue>(scope: ChatScope<TDomain>, options?: ApiChatSourceOptions): ChatSource;
22
+ /**
23
+ * The conversations a record holds, and how to start another.
24
+ *
25
+ * @example
26
+ * const place = useMemo(() => apiChatPlace(scope), [scope.domain, scope.parentId, scope.userId]);
27
+ */
28
+ export declare function apiChatPlace<TDomain extends ChatDomainValue>(scope: ChatScope<TDomain>, options?: ApiChatSourceOptions): ChatPlaceSource;
29
+ /** Asks the app's backend for many records' counts in one call. */
30
+ export declare function loadChatSummaries(domain: ChatDomainValue, parentIds: readonly ChatId[], userId: ChatId): Promise<Map<string, ChatSummary>>;
31
+ /**
32
+ * The counts every trigger icon on the page reads, filled in one call per domain however many icons there are. A page
33
+ * that already has the counts in its rows passes them to the icon instead, and never touches this.
34
+ */
35
+ export declare function chatSummaries(): ChatSummaryCache;
36
+ /** Replaces the page's count cache: a test's, or one with a different batching window. */
37
+ export declare function defineChatSummaries(replacement: ChatSummaryCache): ChatSummaryCache;
@@ -0,0 +1,98 @@
1
+ "use client";
2
+ import { getChatBackend } from "../Helpers/ChatBackend.js";
3
+ import { ChatSummaryCache } from "../Helpers/ChatSummaries.js";
4
+ import { unwrapApiResult } from "../Helpers/ApiResponses.js";
5
+ import { api } from "./BrowserApi.js";
6
+ const callOptions = (options, signal) => ({ ...options?.call, ...(signal && { signal }), toast: false });
7
+ /**
8
+ * A conversation's data through the app's API.
9
+ *
10
+ * @example
11
+ * const scope = { domain: EDomainTypes.Leave, parentId: leave.id, userId: me.id };
12
+ * const source = useMemo(() => apiChatSource(scope), [scope.domain, scope.parentId, scope.userId]);
13
+ */
14
+ export function apiChatSource(scope, options) {
15
+ const backend = (options?.backend ?? getChatBackend());
16
+ const chat = options?.conversationId;
17
+ const source = {
18
+ async history(request) {
19
+ const answer = await api.send(backend.endpoint("history"), { ...callOptions(options, request.signal), body: backend.historyRequest(scope, request, chat) });
20
+ return backend.readHistory(unwrapApiResult(answer));
21
+ },
22
+ async send(draft, signal) {
23
+ const answer = await api.send(backend.endpoint("send"), { ...callOptions(options, signal), body: backend.sendRequest(scope, draft, chat) });
24
+ return backend.readMessage(unwrapApiResult(answer), scope, draft);
25
+ },
26
+ };
27
+ if (backend.supports("participants")) {
28
+ source.participants = async (query) => {
29
+ const answer = await api.send(backend.endpoint("participants"), { ...callOptions(options, query.signal), body: backend.participantsRequest(scope, query, chat) });
30
+ return backend.readParticipants(unwrapApiResult(answer));
31
+ };
32
+ }
33
+ if (backend.supports("addParticipants")) {
34
+ source.addParticipants = async (ids, signal) => {
35
+ const answer = await api.send(backend.endpoint("addParticipants"), { ...callOptions(options, signal), body: backend.addParticipantsRequest(scope, ids, chat) });
36
+ return backend.readParticipants(unwrapApiResult(answer)).items;
37
+ };
38
+ }
39
+ if (backend.supports("read")) {
40
+ source.markRead = async (upTo, signal) => {
41
+ unwrapApiResult(await api.send(backend.endpoint("read"), { ...callOptions(options, signal), body: backend.readMarkRequest(scope, upTo, chat) }));
42
+ };
43
+ }
44
+ if (backend.supports("remove")) {
45
+ source.remove = async (messageId, signal) => {
46
+ unwrapApiResult(await api.send(backend.endpoint("remove"), { ...callOptions(options, signal), params: backend.removeParams(scope, messageId) }));
47
+ };
48
+ }
49
+ return source;
50
+ }
51
+ /**
52
+ * The conversations a record holds, and how to start another.
53
+ *
54
+ * @example
55
+ * const place = useMemo(() => apiChatPlace(scope), [scope.domain, scope.parentId, scope.userId]);
56
+ */
57
+ export function apiChatPlace(scope, options) {
58
+ const backend = (options?.backend ?? getChatBackend());
59
+ const place = {
60
+ async conversations(signal) {
61
+ const answer = await api.send(backend.endpoint("conversations"), { ...callOptions(options, signal), body: backend.conversationsRequest(scope) });
62
+ return backend.readConversations(unwrapApiResult(answer));
63
+ },
64
+ };
65
+ if (backend.supports("createConversation")) {
66
+ place.create = async (request, signal) => {
67
+ const answer = await api.send(backend.endpoint("createConversation"), { ...callOptions(options, signal), body: backend.createConversationRequest(scope, request) });
68
+ return backend.readConversation(unwrapApiResult(answer));
69
+ };
70
+ }
71
+ if (backend.supports("participants")) {
72
+ // Without a conversation, this asks who could be put in a new one here.
73
+ place.participants = async (query) => {
74
+ const answer = await api.send(backend.endpoint("participants"), { ...callOptions(options, query.signal), body: backend.participantsRequest(scope, query) });
75
+ return backend.readParticipants(unwrapApiResult(answer));
76
+ };
77
+ }
78
+ return place;
79
+ }
80
+ /** Asks the app's backend for many records' counts in one call. */
81
+ export async function loadChatSummaries(domain, parentIds, userId) {
82
+ const backend = getChatBackend();
83
+ const answer = await api.send(backend.endpoint("summaries"), { body: backend.summariesRequest(domain, parentIds, userId), toast: false });
84
+ return backend.readSummaries(unwrapApiResult(answer));
85
+ }
86
+ let cache;
87
+ /**
88
+ * The counts every trigger icon on the page reads, filled in one call per domain however many icons there are. A page
89
+ * that already has the counts in its rows passes them to the icon instead, and never touches this.
90
+ */
91
+ export function chatSummaries() {
92
+ return (cache ??= new ChatSummaryCache({ load: loadChatSummaries }));
93
+ }
94
+ /** Replaces the page's count cache: a test's, or one with a different batching window. */
95
+ export function defineChatSummaries(replacement) {
96
+ cache = replacement;
97
+ return replacement;
98
+ }
@@ -0,0 +1,48 @@
1
+ import type { ApiCallOptions, ApiResult } from "../Interfaces/ApiInterfaces.ts";
2
+ import type { ExcelDownload, ExcelEndpoints, ExcelExportQuery, ExcelImportAnswer, ExcelSettings, ExcelSheetRow, ExcelVerifyAnswer } from "../Interfaces/ExcelInterfaces.ts";
3
+ /** Options every spreadsheet call takes. */
4
+ export interface ExcelCallOptions extends Pick<ApiCallOptions, "params" | "query" | "headers" | "signal" | "timeout" | "subject" | "read"> {
5
+ /** Shows no toast for this call. Default: a failure is told, a success is not (the panel says what happened). */
6
+ toast?: false;
7
+ }
8
+ /** Options of an export. Its `query` is what the person has on screen, not query-string values. */
9
+ export interface ExcelExportOptions extends Omit<ExcelCallOptions, "query"> {
10
+ /** The search, the sort and the filters the person has on screen. */
11
+ query?: ExcelExportQuery;
12
+ /**
13
+ * Rows the search on screen leaves, when the page knows. Over the backend's limit, the person is asked first and
14
+ * told what will happen; without it, nothing is asked and the answer's own headers say what was left out.
15
+ */
16
+ rows?: number;
17
+ /** What one row is, for the question and the toast: "city". Default: the settings' subject. */
18
+ subject?: string;
19
+ /** Asks nothing, whatever the count. For a page that asked already. */
20
+ confirm?: false;
21
+ }
22
+ /**
23
+ * The limits and the shape of the sheet. It is the same for everybody and changes once a release, so the first answer
24
+ * is kept for as long as the page lives - the backend says the same with a `Cache-Control` of an hour.
25
+ */
26
+ export declare function excelSettings(endpoints: ExcelEndpoints, options?: ExcelCallOptions): Promise<ApiResult<ExcelSettings>>;
27
+ /** Forgets the kept settings, for a test or an app that changed what a sheet holds while it ran. */
28
+ export declare function clearExcelSettings(): void;
29
+ /** The template to fill in, as the backend built it, under the name the answer gives it. */
30
+ export declare function downloadExcelTemplate(endpoints: ExcelEndpoints, options?: ExcelCallOptions): Promise<ApiResult<ExcelDownload>>;
31
+ /**
32
+ * An export of what the person has on screen: the search, the sort and the filters, sent to the model's own action.
33
+ * Not a file written from the rows the browser happens to hold - the backend writes every row the search leaves, up to
34
+ * its own limit, and says in the answer's headers what it left out.
35
+ *
36
+ * Over that limit the person is asked first, because a file that is quietly short is worse than one they chose.
37
+ */
38
+ export declare function exportExcel(endpoints: ExcelEndpoints, options?: ExcelExportOptions): Promise<ApiResult<ExcelDownload> | null>;
39
+ /** The filled file read and judged, with nothing written. The rows it answers with are what an import sends back. */
40
+ export declare function verifyExcelFile(endpoints: ExcelEndpoints, file: File, options?: ExcelCallOptions): Promise<ApiResult<ExcelVerifyAnswer>>;
41
+ /**
42
+ * Writes the rows a verify answered with, as the person left them - the **rows**, never the file again, or every fix
43
+ * they made on screen would be lost. Every check runs again on the way in: what the browser sends is a row, not a
44
+ * decision.
45
+ */
46
+ export declare function importExcelRows(endpoints: ExcelEndpoints, rows: readonly ExcelSheetRow[], options?: ExcelCallOptions & {
47
+ recordStatus?: number;
48
+ }): Promise<ApiResult<ExcelImportAnswer>>;