nexus-shared 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +704 -446
  2. package/LICENSE +21 -21
  3. package/README.md +44 -5
  4. package/dist/Client.Index.d.ts +3 -0
  5. package/dist/Client.Index.js +3 -0
  6. package/dist/Components/Forms/Crud.js +322 -161
  7. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  8. package/dist/Components/Forms/CrudHistory.js +140 -0
  9. package/dist/Components/Forms/CrudParts.js +2 -1
  10. package/dist/Components/Forms/Form.js +4 -1
  11. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  12. package/dist/Components/Inputs/CodeBox.js +316 -0
  13. package/dist/Components/Inputs/DatePicker.js +4 -1
  14. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  15. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  16. package/dist/Components/Inputs/Dropdown.js +4 -1
  17. package/dist/Components/Inputs/InputRenderer.js +3 -0
  18. package/dist/Components/Inputs/NumberBox.js +4 -2
  19. package/dist/Components/Inputs/TextArea.js +4 -1
  20. package/dist/Components/Inputs/TextBox.js +4 -1
  21. package/dist/Components/Inputs/TimePicker.js +4 -1
  22. package/dist/Components/Inputs/TreeView.d.ts +55 -5
  23. package/dist/Components/Inputs/TreeView.js +372 -23
  24. package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
  25. package/dist/Components/Layouts/ThemePicker.js +16 -4
  26. package/dist/Components/Viewers/DataTable.js +63 -21
  27. package/dist/Components/Viewers/DataTableParts.d.ts +26 -8
  28. package/dist/Components/Viewers/DataTableParts.js +39 -10
  29. package/dist/Helpers/ApiModules.d.ts +3 -34
  30. package/dist/Helpers/ApiModules.js +3 -3
  31. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  32. package/dist/Helpers/ApiRoutes.js +31 -27
  33. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  34. package/dist/Helpers/ArrayHelpers.js +2 -0
  35. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  36. package/dist/Helpers/CodeHelpers.js +332 -0
  37. package/dist/Helpers/CrudBackend.d.ts +94 -4
  38. package/dist/Helpers/CrudBackend.js +168 -9
  39. package/dist/Helpers/CrudColumns.d.ts +51 -0
  40. package/dist/Helpers/CrudColumns.js +69 -0
  41. package/dist/Helpers/CrudHelpers.d.ts +50 -5
  42. package/dist/Helpers/CrudHelpers.js +109 -12
  43. package/dist/Helpers/InputParamsHelpers.js +8 -0
  44. package/dist/Helpers/MessageBuilder.js +2 -0
  45. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  46. package/dist/Helpers/NexusHelpers.js +1 -0
  47. package/dist/Helpers/RequestCache.d.ts +35 -0
  48. package/dist/Helpers/RequestCache.js +113 -0
  49. package/dist/Helpers/TableExport.js +12 -12
  50. package/dist/Helpers/TableHelpers.d.ts +1 -1
  51. package/dist/Helpers/TableHelpers.js +7 -2
  52. package/dist/Helpers/TableJoins.d.ts +70 -0
  53. package/dist/Helpers/TableJoins.js +250 -0
  54. package/dist/Helpers/TableStore.d.ts +5 -0
  55. package/dist/Helpers/TableStore.js +9 -0
  56. package/dist/Helpers/TreeStore.d.ts +225 -11
  57. package/dist/Helpers/TreeStore.js +683 -90
  58. package/dist/Helpers/UserPreferences.d.ts +8 -0
  59. package/dist/Helpers/UserPreferences.js +17 -0
  60. package/dist/Interfaces/CrudInterfaces.d.ts +47 -9
  61. package/dist/Interfaces/FormInterfaces.d.ts +4 -1
  62. package/dist/Interfaces/InputInterfaces.d.ts +24 -0
  63. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  64. package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
  65. package/dist/Interfaces/PreferenceInterfaces.js +2 -5
  66. package/dist/Interfaces/TableInterfaces.d.ts +86 -4
  67. package/dist/Services/ApiProxy.d.ts +36 -1
  68. package/dist/Services/ApiProxy.js +182 -52
  69. package/dist/Services/ApiProxyRules.d.ts +203 -0
  70. package/dist/Services/ApiProxyRules.js +426 -0
  71. package/dist/Services/JoinSources.d.ts +48 -0
  72. package/dist/Services/JoinSources.js +74 -0
  73. package/dist/Services/ServerApi.d.ts +13 -2
  74. package/dist/Services/ServerApi.js +8 -3
  75. package/dist/Shared.Index.d.ts +4 -0
  76. package/dist/Shared.Index.js +4 -0
  77. package/package.json +87 -85
  78. package/src/Styles/Nexus.Avatar.css +79 -79
  79. package/src/Styles/Nexus.Base.css +117 -117
  80. package/src/Styles/Nexus.Button.css +180 -180
  81. package/src/Styles/Nexus.Calendar.css +273 -273
  82. package/src/Styles/Nexus.Chat.css +1443 -1443
  83. package/src/Styles/Nexus.Choice.css +343 -343
  84. package/src/Styles/Nexus.Crud.css +168 -168
  85. package/src/Styles/Nexus.DateTime.css +81 -81
  86. package/src/Styles/Nexus.Dropdown.css +725 -725
  87. package/src/Styles/Nexus.Editor.css +295 -295
  88. package/src/Styles/Nexus.Excel.css +389 -389
  89. package/src/Styles/Nexus.Form.css +279 -279
  90. package/src/Styles/Nexus.Index.css +37 -37
  91. package/src/Styles/Nexus.Input.css +196 -4
  92. package/src/Styles/Nexus.Markdown.css +237 -237
  93. package/src/Styles/Nexus.Menu.css +237 -237
  94. package/src/Styles/Nexus.Popup.css +724 -724
  95. package/src/Styles/Nexus.Rows.css +637 -637
  96. package/src/Styles/Nexus.Slider.css +276 -276
  97. package/src/Styles/Nexus.Tab.Buttons.css +432 -432
  98. package/src/Styles/Nexus.Table.css +31 -30
  99. package/src/Styles/Nexus.Theme.Picker.css +59 -3
  100. package/src/Styles/Nexus.Themes.css +194 -194
  101. package/src/Styles/Nexus.Time.css +144 -144
  102. package/src/Styles/Nexus.Toast.css +263 -263
  103. package/src/Styles/Nexus.Tokens.css +89 -89
  104. package/src/Styles/Nexus.Tree.css +500 -431
@@ -1,3 +1,4 @@
1
+ import type { InputDesign } from "../Interfaces/InputInterfaces.ts";
1
2
  import { type UserPreferences } from "../Interfaces/PreferenceInterfaces.ts";
2
3
  /** The saved settings, with the defaults for anything missing or invalid. Safe to call on the server. */
3
4
  export declare function readUserPreferences(): UserPreferences;
@@ -12,3 +13,10 @@ export declare function useUserPreferences(): UserPreferences;
12
13
  * use when they are given no `locale` of their own.
13
14
  */
14
15
  export declare function useNumberLocale(): string;
16
+ /**
17
+ * The design an input takes: its own (its page's or its form's), else the person's choice in the quick preferences,
18
+ * else `standard`. A grid's cells and a demo of the designs name theirs, so they keep it. An input with no label of its
19
+ * own - a table's search box, a picker named by a heading above it - has no label to place, so the choice passes it by:
20
+ * a design for one would only leave an empty strip where the label would be.
21
+ */
22
+ export declare function useInputDesign(own: InputDesign | undefined, labelled?: boolean): InputDesign;
@@ -11,6 +11,7 @@ import { numberLocale } from "./NumberHelpers.js";
11
11
  const listeners = new Set();
12
12
  let snapshot = DEFAULT_USER_PREFERENCES;
13
13
  const SEPARATORS = ["comma", "period", "space", "none"];
14
+ const INPUT_DESIGNS = ["standard", "outlined", "inbox"];
14
15
  function read(key) {
15
16
  try {
16
17
  return localStorage.getItem(key);
@@ -28,12 +29,14 @@ export function readUserPreferences() {
28
29
  const thousands = read(PREFERENCE_STORAGE_KEYS.thousands);
29
30
  const pageSize = Number(read(PREFERENCE_STORAGE_KEYS.pageSize));
30
31
  const rowClick = read(PREFERENCE_STORAGE_KEYS.rowClick);
32
+ const inputDesign = read(PREFERENCE_STORAGE_KEYS.inputDesign);
31
33
  const next = {
32
34
  language: language === "ne" ? "ne" : DEFAULT_USER_PREFERENCES.language,
33
35
  numberScale: scale === ENumberScales.SouthAsian ? ENumberScales.SouthAsian : DEFAULT_USER_PREFERENCES.numberScale,
34
36
  thousands: thousands && SEPARATORS.includes(thousands) ? thousands : DEFAULT_USER_PREFERENCES.thousands,
35
37
  pageSize: Number.isInteger(pageSize) && pageSize > 0 ? pageSize : DEFAULT_USER_PREFERENCES.pageSize,
36
38
  rowClick: rowClick === "1" ? true : rowClick === "0" ? false : DEFAULT_USER_PREFERENCES.rowClick,
39
+ inputDesign: inputDesign && INPUT_DESIGNS.includes(inputDesign) ? inputDesign : DEFAULT_USER_PREFERENCES.inputDesign,
37
40
  };
38
41
  // The same object while nothing changed, so React sees a stable snapshot.
39
42
  const keys = Object.keys(next);
@@ -59,6 +62,10 @@ export function setUserPreferences(changes) {
59
62
  localStorage.removeItem(PREFERENCE_STORAGE_KEYS.rowClick);
60
63
  else if (changes.rowClick !== undefined)
61
64
  localStorage.setItem(PREFERENCE_STORAGE_KEYS.rowClick, changes.rowClick ? "1" : "0");
65
+ if (changes.inputDesign === null)
66
+ localStorage.removeItem(PREFERENCE_STORAGE_KEYS.inputDesign);
67
+ else if (changes.inputDesign !== undefined)
68
+ localStorage.setItem(PREFERENCE_STORAGE_KEYS.inputDesign, changes.inputDesign);
62
69
  }
63
70
  catch {
64
71
  // Storage blocked (private mode, for example): the components keep what they have.
@@ -97,3 +104,13 @@ export function useNumberLocale() {
97
104
  const { numberScale, thousands } = useUserPreferences();
98
105
  return numberLocale(numberScale, thousands);
99
106
  }
107
+ /**
108
+ * The design an input takes: its own (its page's or its form's), else the person's choice in the quick preferences,
109
+ * else `standard`. A grid's cells and a demo of the designs name theirs, so they keep it. An input with no label of its
110
+ * own - a table's search box, a picker named by a heading above it - has no label to place, so the choice passes it by:
111
+ * a design for one would only leave an empty strip where the label would be.
112
+ */
113
+ export function useInputDesign(own, labelled = true) {
114
+ const chosen = useSyncExternalStore(subscribeUserPreferences, () => readUserPreferences().inputDesign, () => DEFAULT_USER_PREFERENCES.inputDesign);
115
+ return own ?? (labelled ? chosen : null) ?? "standard";
116
+ }
@@ -2,6 +2,7 @@ import type { CSSProperties, ReactNode, Ref } from "react";
2
2
  import type { PopupSize } from "../Components/Layouts/Popup.tsx";
3
3
  import type { PopupHandle } from "../Components/Layouts/PopupHost.tsx";
4
4
  import type { FormStore } from "../Helpers/FormStore.ts";
5
+ import type { CrudBackend } from "../Helpers/CrudBackend.ts";
5
6
  import type { TableStore } from "../Helpers/TableStore.ts";
6
7
  import type { ApiEndpoint } from "./ApiInterfaces.ts";
7
8
  import type { FormProps, FormValues, InputParams } from "./FormInterfaces.ts";
@@ -9,14 +10,19 @@ import type { InputDesign } from "./InputInterfaces.ts";
9
10
  import type { DataTableProps, TableColumn, TableMassAction, TablePaging, TableRowAction } from "./TableInterfaces.ts";
10
11
  /**
11
12
  * The lists of a controller: every active record (`normal`), the flagged ones, the trash, and the deleted ones
12
- * (which can go back to the trash). The records the user pinned are not a list of their own: they sit above the
13
- * active list, and in it.
13
+ * (which can go back to the trash). The records the user pinned or made favourites are not lists of their own: they
14
+ * sit in sections above the active list, and in it.
14
15
  */
15
16
  export type CrudMode = "normal" | "flag" | "trash" | "deleted";
16
17
  /** The built-in actions on one record. */
17
- export type CrudRowActionName = "view" | "edit" | "flag" | "pin" | "trash" | "recover" | "delete" | "trashFromDelete";
18
+ export type CrudRowActionName = "view" | "edit" | "flag" | "pin" | "favorite" | "history" | "trash" | "recover" | "delete" | "trashFromDelete";
18
19
  /** The built-in actions on many records at once. */
19
- export type CrudMassActionName = "flag" | "pin" | "trash" | "recover" | "delete" | "trashFromDelete" | "description";
20
+ export type CrudMassActionName = "flag" | "pin" | "favorite" | "trash" | "recover" | "delete" | "trashFromDelete" | "description";
21
+ /**
22
+ * A mark a user puts on a record for themselves: pinned, or one of their favourites. Each has a section above the
23
+ * active list and its own limit, and a record may carry both.
24
+ */
25
+ export type CrudMark = "pin" | "favorite";
20
26
  /** Every built-in action: Add, the row actions, and a new description for many records. */
21
27
  export type CrudActionName = "create" | CrudRowActionName | "description";
22
28
  /**
@@ -29,8 +35,15 @@ export interface CrudEndpoints {
29
35
  /** One page of a list, for tables that page on the server: `Room/NormalPagination`, … */
30
36
  pages?: Partial<Record<CrudMode, ApiEndpoint>>;
31
37
  /**
32
- * The records the user pinned: `Room/PinListing`. They show in a section above the active list, which the user folds
33
- * open, and in the list itself. Without it there is no pinned section, and nothing counts the pins against the limit.
38
+ * This user's marked records, pins and favourites in one list: `Room/BookmarkListing`. Each record says which marks
39
+ * it has (`CrudBackend.marksOf`), and the CRUD splits them into the Pinned and Favourites sections above the active
40
+ * list. It is also where the CRUD learns every record's marks, so a list's own rows need not carry them. Without it
41
+ * (or `pins`) there are no sections, and nothing counts the marks against their limits.
42
+ */
43
+ bookmarks?: ApiEndpoint;
44
+ /**
45
+ * The records the user pinned, from a backend that keeps no favourites: `Room/PinListing`. A Pinned section and no
46
+ * Favourites. `bookmarks` wins when both are given.
34
47
  */
35
48
  pins?: ApiEndpoint;
36
49
  /** One record by its key: `{id}` in the path. */
@@ -38,14 +51,34 @@ export interface CrudEndpoints {
38
51
  create?: ApiEndpoint;
39
52
  update?: ApiEndpoint;
40
53
  flag?: ApiEndpoint;
41
- /** Pins a record, or unpins it: `Room/Pin/{id}`. */
54
+ /**
55
+ * Sets this user's marks on a record: `Room/SwitchBookmarks/{id}`, sent the whole set the record is to have, never a
56
+ * toggle (`CrudBackend.marksRequest`). Pin and Favourite both use it.
57
+ */
58
+ bookmark?: ApiEndpoint;
59
+ /** Pins a record or unpins it, on a backend without `bookmark`: `Room/Pin/{id}`, a toggle. `bookmark` wins when both are given. */
42
60
  pin?: ApiEndpoint;
61
+ /**
62
+ * A record's history, 50 entries a call: `Room/AuditListing/{ownerId}/{skipCount}`. With it every row offers History
63
+ * and the details View history, to a person who may call it.
64
+ */
65
+ audit?: ApiEndpoint;
43
66
  trash?: ApiEndpoint;
44
67
  recover?: ApiEndpoint;
45
68
  delete?: ApiEndpoint;
46
69
  trashFromDelete?: ApiEndpoint;
47
- /** Actions on many records, sent the selected keys. */
48
- mass?: Partial<Record<CrudMassActionName, ApiEndpoint>>;
70
+ /**
71
+ * Actions on many records, sent the selected keys. `bookmark` puts one mark on every record sent or takes it off, and
72
+ * leaves their other marks alone (`Room/MassSwitchBookmarks`): Pin and Favourite both use it, and it wins over `pin`.
73
+ */
74
+ mass?: Partial<Record<Exclude<CrudMassActionName, "favorite"> | "bookmark", ApiEndpoint>>;
75
+ /**
76
+ * Where other pages look this controller's records up: by key (`Room/FindListing`, a table's client join and a
77
+ * picker's saved values) and as a picker's list (`Room/ActiveListing`). The CRUD calls neither; a change it makes marks
78
+ * what was looked up through them as old, so a name or a status shown elsewhere is read again.
79
+ */
80
+ findListing?: ApiEndpoint;
81
+ activeListing?: ApiEndpoint;
49
82
  }
50
83
  /** Inputs of the Add and Edit forms: one list for both, a list for each, or a function of the action and the row. */
51
84
  export type CrudInputs<TRow extends FormValues = FormValues> = readonly InputParams[] | {
@@ -206,6 +239,11 @@ export interface CrudProps<TRow extends FormValues = FormValues> {
206
239
  table?: CrudTableOptions<TRow>;
207
240
  /** After every change the CRUD made: a record added, saved, trashed, deleted. */
208
241
  onChange?: (change: CrudChange<TRow>) => void;
242
+ /**
243
+ * The conventions of this controller's backend, when they differ from the app's (`defineCrudBackend`): an app whose
244
+ * pages sit on two backends gives the other one here. Default the app's, else the Nexus backend's.
245
+ */
246
+ backend?: CrudBackend;
209
247
  /** The table's name for screen readers. Default: the plural of the subject ("Guests"). */
210
248
  "aria-label"?: string;
211
249
  "aria-labelledby"?: string;
@@ -1,6 +1,7 @@
1
1
  import type { CSSProperties, ReactNode, Ref } from "react";
2
2
  import type { CheckBoxOnlyProps, CheckBoxProps } from "../Components/Inputs/CheckBox.tsx";
3
3
  import type { CheckBoxGroupProps } from "../Components/Inputs/CheckBoxGroup.tsx";
4
+ import type { CodeBoxProps } from "../Components/Inputs/CodeBox.tsx";
4
5
  import type { DatePickerProps } from "../Components/Inputs/DatePicker.tsx";
5
6
  import type { DateRangePickerProps } from "../Components/Inputs/DateRangePicker.tsx";
6
7
  import type { DateTimePickerProps } from "../Components/Inputs/DateTimePicker.tsx";
@@ -63,6 +64,8 @@ export type InputParamsOf<TType extends string, TProps> = DistributiveOmit<TProp
63
64
  export type TextInputParams = InputParamsOf<TextBoxType, TextBoxProps>;
64
65
  export type TextAreaParams = InputParamsOf<"textarea", TextAreaProps>;
65
66
  export type NumberInputParams = InputParamsOf<"number", NumberBoxProps>;
67
+ /** `CodeBox`: a box per character. The value is the code's characters, without separators. */
68
+ export type CodeInputParams = InputParamsOf<"code", CodeBoxProps>;
66
69
  /** A checkbox: `true` or `false`. `defaultChecked` is the value of a new row. */
67
70
  export type CheckBoxParams = InputParamsOf<"checkbox", CheckBoxProps & CheckBoxOnlyProps>;
68
71
  export type SwitchParams = InputParamsOf<"switch", CheckBoxProps>;
@@ -90,7 +93,7 @@ export type TabularParams = InputParamsOf<"tabular", TabularFormProps>;
90
93
  /** `GroupForm`: the same rows, each as a section of inputs. */
91
94
  export type GroupParams = InputParamsOf<"group", GroupFormProps>;
92
95
  /** The params of any input: the root type every form takes as `inputs`. */
93
- export type InputParams = TextInputParams | TextAreaParams | NumberInputParams | CheckBoxParams | SwitchParams | RadioParams | CheckBoxGroupParams | RadioGroupParams | DropdownParams | DateParams | DateRangeParams | TimeParams | DateTimeParams | SliderParams | RangeSliderParams | MarkdownParams | RichTextParams | TreeParams | TabularParams | GroupParams;
96
+ export type InputParams = TextInputParams | TextAreaParams | NumberInputParams | CodeInputParams | CheckBoxParams | SwitchParams | RadioParams | CheckBoxGroupParams | RadioGroupParams | DropdownParams | DateParams | DateRangeParams | TimeParams | DateTimeParams | SliderParams | RangeSliderParams | MarkdownParams | RichTextParams | TreeParams | TabularParams | GroupParams;
94
97
  export type InputType = InputParams["type"];
95
98
  /** Settings a check needs to write its message the way the field shows values: the user's date and time settings. */
96
99
  export interface InputValidationContext {
@@ -31,6 +31,30 @@ export interface TextRules {
31
31
  /** Runs after the built-in rules pass. */
32
32
  validate?: TextValidator;
33
33
  }
34
+ /**
35
+ * What a code box takes, one character to a box:
36
+ * - `digits`: 0–9, with the phone's number pad. A one-time code, a PIN, an SSN.
37
+ * - `letters`: A–Z.
38
+ * - `alphanumeric`: letters and digits, such as a licence key or a booking reference.
39
+ * - `hex`: 0–9 and A–F, such as a colour or a device id.
40
+ */
41
+ export type CodeCharacters = "digits" | "letters" | "alphanumeric" | "hex";
42
+ /** The shape and rules of a code: how many boxes, in which groups, and what each box takes. Plain values, so params stay serializable. */
43
+ export interface CodeRules {
44
+ /** How many boxes. Default 6. Left out when `groups` is given. */
45
+ length?: number;
46
+ /** Boxes in groups, with a gap or a `separator` between them: `[3, 2, 4]` for an SSN, `[3, 3]` for a one-time code. */
47
+ groups?: readonly number[];
48
+ /** Default `digits`. */
49
+ characters?: CodeCharacters;
50
+ /** A regular expression for one character, in place of `characters`: `"[A-HJ-NP-Z2-9]"` leaves out I, O, 0 and 1. */
51
+ allowed?: string;
52
+ /** Letters as typed are turned to this case. Default `upper`; `any` keeps them as typed. */
53
+ letterCase?: "upper" | "lower" | "any";
54
+ required?: boolean;
55
+ /** Runs after the built-in rules pass, with the code. */
56
+ validate?: TextValidator;
57
+ }
34
58
  /** Custom check for a number. Receives the value, or `null` when the field is empty; returns a message to fail. */
35
59
  export type NumberValidator = (value: number | null) => string | null | undefined;
36
60
  /** Validation rules for a number, named like the HTML attributes. */
@@ -1,5 +1,5 @@
1
1
  /** The actions the builder knows by name (`MESSAGE_ACTION_NAMES`); `actionWords(name)` gives their words. */
2
- export type MessageActionName = "load" | "save" | "create" | "update" | "delete" | "trash" | "restore" | "recover" | "flag" | "unflag" | "pin" | "unpin" | "send" | "submit" | "upload" | "download" | "import" | "export" | "copy" | "approve" | "reject" | "cancel";
2
+ export type MessageActionName = "load" | "save" | "create" | "update" | "delete" | "trash" | "restore" | "recover" | "flag" | "unflag" | "pin" | "unpin" | "favorite" | "unfavorite" | "send" | "submit" | "upload" | "download" | "import" | "export" | "copy" | "approve" | "reject" | "cancel";
3
3
  /** The words of an action. */
4
4
  export interface ActionWords {
5
5
  /** "save", or with the object inside: "move {object} to the trash". */
@@ -1,3 +1,4 @@
1
+ import type { InputDesign } from "./InputInterfaces.ts";
1
2
  /** The words of the app itself. Data keeps the language it was entered in. */
2
3
  export type InterfaceLanguage = "en" | "ne";
3
4
  /**
@@ -31,6 +32,12 @@ export interface UserPreferences {
31
32
  * the table says itself. `null` (the default): not chosen, each table's own `openOnClick`.
32
33
  */
33
34
  rowClick: boolean | null;
35
+ /**
36
+ * The design every input takes when its page or form names none: the label above the box (`standard`), on its edge
37
+ * (`outlined`) or inside its top (`inbox`). `null` (the default): not chosen, `standard`. An input whose page fixes a
38
+ * design - a grid's cells, a demo of the designs - keeps it.
39
+ */
40
+ inputDesign: InputDesign | null;
34
41
  }
35
42
  export declare const DEFAULT_USER_PREFERENCES: UserPreferences;
36
43
  /** Local storage keys of the settings. A number scale is kept as its member value ("0", "1"), a row click as "1" or "0". */
@@ -40,4 +47,5 @@ export declare const PREFERENCE_STORAGE_KEYS: {
40
47
  readonly thousands: "nx-thousands";
41
48
  readonly pageSize: "nx-page-size";
42
49
  readonly rowClick: "nx-row-click";
50
+ readonly inputDesign: "nx-input-design";
43
51
  };
@@ -1,8 +1,3 @@
1
- /*
2
- * The person's own settings beyond the theme and the dates: the interface language, how numbers are written, and what a
3
- * table does before anyone touches its own controls. Saved on this device and read by the components that show them,
4
- * so a change applies to everything on the page at once. The date settings are `DatePreferences` (DateInterfaces.ts).
5
- */
6
1
  /**
7
2
  * The core's `ENumberScales`, by member value: where the groups of a number fall. `International` writes 1,234,567
8
3
  * (thousand, million, billion); `SouthAsian` writes 12,34,567 (thousand, lakh, crore).
@@ -17,6 +12,7 @@ export const DEFAULT_USER_PREFERENCES = {
17
12
  thousands: "comma",
18
13
  pageSize: null,
19
14
  rowClick: null,
15
+ inputDesign: null,
20
16
  };
21
17
  /** Local storage keys of the settings. A number scale is kept as its member value ("0", "1"), a row click as "1" or "0". */
22
18
  export const PREFERENCE_STORAGE_KEYS = {
@@ -25,4 +21,5 @@ export const PREFERENCE_STORAGE_KEYS = {
25
21
  thousands: "nx-thousands",
26
22
  pageSize: "nx-page-size",
27
23
  rowClick: "nx-row-click",
24
+ inputDesign: "nx-input-design",
28
25
  };
@@ -1,5 +1,7 @@
1
+ import type { IconNode } from "nexus-icons";
1
2
  import type { CSSProperties, ReactNode, Ref } from "react";
2
3
  import type { TableStore } from "../Helpers/TableStore.ts";
4
+ import type { ApiEndpoint } from "./ApiInterfaces.ts";
3
5
  import type { ExcelEndpoints } from "./ExcelInterfaces.ts";
4
6
  import type { InputParams } from "./FormInterfaces.ts";
5
7
  import type { Size, Tone } from "./TypeInterfaces.ts";
@@ -16,6 +18,11 @@ export interface TableColumnOption {
16
18
  /** Draws the value as a tag with a dot of this color. */
17
19
  tone?: Tone;
18
20
  }
21
+ /** A short tag after a cell's value (`TableColumn.tag`): its words and the color of its dot. */
22
+ export interface TableTag {
23
+ label: string;
24
+ tone?: Tone;
25
+ }
19
26
  /** One column: which field it shows, how, and what the user can do with it. */
20
27
  export interface TableColumn<TRow extends TableRow = TableRow> {
21
28
  /** The field: `"name"`, or a path into the row, `"guest.name"`. It is also the column's id and its sort name. */
@@ -65,6 +72,8 @@ export interface TableColumn<TRow extends TableRow = TableRow> {
65
72
  hideable?: boolean;
66
73
  /** Long text wraps onto more lines instead of ending in "…". */
67
74
  wrap?: boolean;
75
+ /** Its text is Markdown: the table shows the text, and a record's details show it formatted. */
76
+ markdown?: boolean;
68
77
  /** The user can drag the heading to another place, or move it from its menu. Default true. */
69
78
  movable?: boolean;
70
79
  /** The user can drag the edge of the heading to change the width. Default true; `false` for a column that must keep its width. */
@@ -73,6 +82,44 @@ export interface TableColumn<TRow extends TableRow = TableRow> {
73
82
  pinnable?: boolean;
74
83
  /** Pinned before the user pins anything: `left` keeps it at the start, `right` at the end. */
75
84
  pinned?: TablePinSide;
85
+ /**
86
+ * The value is another record's, looked up by the key this row holds: a country's sub-region named from the
87
+ * sub-regions' `FindListing`. The table collects every key its rows hold, asks once for the ones it has not got, keeps
88
+ * the answers for every table on the page, and shows the field once they are in - searched and sorted like any other
89
+ * column. A key the source does not answer shows as "Not found". (A join the database already made needs none of
90
+ * this: the row carries the name, and `tag` reads its status.)
91
+ */
92
+ join?: TableJoin;
93
+ /**
94
+ * A short tag after the value: "Trashed" beside a parent that is in the trash. Given the row and, on a `join` column,
95
+ * the record it looked up; nothing when it answers nothing. `crudColumns.dbJoin` and `crudColumns.clientJoin` set it.
96
+ */
97
+ tag?: (row: TRow, joined?: TableJoinedRecord) => TableTag | null | undefined;
98
+ }
99
+ /** A record a `join` looked up: whatever its source answers for one key. */
100
+ export type TableJoinedRecord = Readonly<Record<string, unknown>>;
101
+ /** Where a `join` looks records up: the records behind a set of keys, in any status. */
102
+ export interface TableJoinSource {
103
+ /** One name per source. Every column and table that names it shares its answers and its calls. */
104
+ readonly name: string;
105
+ /** The records behind these keys; a key with no record is left out. One call for every key the page needs at once. */
106
+ load(keys: readonly string[], signal: AbortSignal): Promise<readonly TableJoinedRecord[]>;
107
+ /** The field of a loaded record that holds its key. Default `id`. */
108
+ readonly key?: string;
109
+ /** How long an answer is kept, in milliseconds, before it is asked again. Default 5 minutes. */
110
+ readonly lifetime?: number;
111
+ }
112
+ /** A column whose value is another record's (`TableColumn.join`). */
113
+ export interface TableJoin {
114
+ /** The field of this row that holds the other record's key: `"subRegionId"`. */
115
+ from: string;
116
+ /** The field of the other record the column shows. Default: the column's own `key`. */
117
+ field?: string;
118
+ /**
119
+ * Where the records come from: an endpoint the keys are POSTed to as an array (a Nexus controller's `findListing`),
120
+ * or a source of the page's own.
121
+ */
122
+ source: ApiEndpoint | TableJoinSource;
76
123
  }
77
124
  /** Which side a pinned column stays on while the others scroll. */
78
125
  export type TablePinSide = "left" | "right";
@@ -212,6 +259,30 @@ export interface TableMenuAction {
212
259
  separator?: boolean;
213
260
  onSelect: () => void;
214
261
  }
262
+ /**
263
+ * Rows kept in a section above the page, which the user folds open and shut: the records they pinned, their
264
+ * favourites. They are the rows as they came from their own endpoint, the same records also show in the list below,
265
+ * and each section's row wears the section's icon where the rows of the page show their number.
266
+ */
267
+ export interface TableRowSection<TRow extends TableRow = TableRow> {
268
+ /** Tells the sections apart; the table remembers each one's fold under it. */
269
+ key: string;
270
+ /** What the section is called: "Pinned rooms". */
271
+ label: string;
272
+ /** The section's icon, in its head and on each of its rows. */
273
+ icon: IconNode;
274
+ rows: readonly TRow[];
275
+ /** The rows are on their way: the section shows that it is loading. */
276
+ loading?: boolean;
277
+ /** Most rows the section can hold, shown beside the count so the user knows the limit. */
278
+ max?: number;
279
+ }
280
+ /** A mark a row carries for this user - pinned, a favourite - drawn in its first column where its number would be. */
281
+ export interface TableRowMark {
282
+ icon: IconNode;
283
+ /** What the mark says: "Pinned". */
284
+ label: string;
285
+ }
215
286
  /** A way to select rows by what they are, such as "Pinned" or "Not flagged". */
216
287
  export interface TableSelectBy<TRow extends TableRow = TableRow> {
217
288
  label: string;
@@ -340,8 +411,19 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
340
411
  */
341
412
  openOnClick?: boolean;
342
413
  /**
343
- * Rows kept in a section above the page, which the user folds open: the records they pinned. They are the rows as
344
- * they came from their own endpoint, and the same records also show in the list below. Leave it out for no section.
414
+ * Sections above the page, in this order, each folding open and shut on its own: the records the user pinned, then
415
+ * their favourites. A section with no rows and none on their way does not show.
416
+ */
417
+ sections?: readonly TableRowSection<TRow>[];
418
+ /**
419
+ * The marks each row carries (pinned, a favourite), in its first column in place of its number, one under the
420
+ * other - in the list and in the sections alike, so no column of their own is needed. A row with none shows its
421
+ * number; a section's row with none shows the section's icon.
422
+ */
423
+ rowMarks?: (row: TRow) => readonly TableRowMark[];
424
+ /**
425
+ * One section of pinned rows, before any `sections`: the same as a section with the key `pins` and the pin icon.
426
+ * Leave it out for no such section.
345
427
  */
346
428
  pinnedRows?: readonly TRow[];
347
429
  /** The pinned rows are on their way: the section shows that it is loading. */
@@ -361,8 +443,8 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
361
443
  * as the pointer is over the row or anything is selected.
362
444
  */
363
445
  numbered?: boolean;
364
- /** Remembers the page size, the sort, hidden columns, the order, widths, and pins of the columns, and whether the
365
- * pinned section is open, on this device. */
446
+ /** Remembers the page size, the sort, hidden columns, the order, widths, and pins of the columns, and which sections
447
+ * are open, on this device. */
366
448
  stateKey?: string;
367
449
  /** Milliseconds after the last key before a search goes to a server. Default 300; a table with every row searches at once. */
368
450
  searchDelay?: number;
@@ -1,4 +1,6 @@
1
1
  import type { ApiModuleValue } from "../Interfaces/ApiInterfaces.ts";
2
+ import { type ApiProxyCode, type ApiProxyFailure } from "./ApiProxyRules.ts";
3
+ export { DEFAULT_PROXY_BODY_LIMIT, DEFAULT_PROXY_REDIRECTS, DEFAULT_PROXY_RESPONSE_HEADERS, lastForwardedAddress, nexusFailure, PROXY_REFUSALS, readIpAddress, type ApiProxyCode, type ApiProxyFailure } from "./ApiProxyRules.ts";
2
4
  export interface ApiProxyOptions {
3
5
  /** The user's token for the backend, such as from the session cookie. Without one, the request's own Authorization header goes on. */
4
6
  token?: (request: Request) => string | null | undefined | Promise<string | null | undefined>;
@@ -6,10 +8,34 @@ export interface ApiProxyOptions {
6
8
  headers?: (request: Request) => Record<string, string> | undefined | Promise<Record<string, string> | undefined>;
7
9
  /** Serves only these modules, of those the app's `ApiModules` sends through the proxy. Default: all of those. */
8
10
  allow?: readonly ApiModuleValue[];
9
- /** Milliseconds to wait for the backend to start answering. Default 60,000. */
11
+ /** Milliseconds to wait for the backend to start answering, its redirects included. Default 60,000. */
10
12
  timeout?: number;
11
13
  /** The `fetch` to use. Default: the global one. */
12
14
  fetch?: typeof fetch;
15
+ /**
16
+ * More of the backend's response headers to pass on, besides `DEFAULT_PROXY_RESPONSE_HEADERS` and the Nexus backend's
17
+ * own `X-Nexus-*`: `["X-Total-Count"]`. A header that names the backend or carries its own policies (Server,
18
+ * X-Powered-By, X-AspNet-*, Via, Alt-Svc, Strict-Transport-Security, CORS, Location, Link) is dropped even when named.
19
+ */
20
+ responseHeaders?: readonly string[];
21
+ /** Passes the backend's Set-Cookie headers to the browser. Default false: the app keeps its own session. */
22
+ passCookies?: boolean;
23
+ /** The most bytes a request body may have; a bigger one is refused with 413 before it reaches the backend. Default 100 MB. */
24
+ maxBodySize?: number;
25
+ /** How many redirects inside a module to follow for one call; 0 refuses every redirect. Default 3. */
26
+ maxRedirects?: number;
27
+ /**
28
+ * The client's IP address, sent to the backend as X-Forwarded-For for its sign-in lockout and rate limits. Default:
29
+ * the last address of the X-Forwarded-For the app received, the one Next.js or the app's own reverse proxy wrote. Pass
30
+ * it behind more than one proxy of the app's own, or behind a CDN that names the client in a header of its own.
31
+ */
32
+ clientAddress?: (request: Request) => string | null | undefined | Promise<string | null | undefined>;
33
+ /** Lets pages of other sites call through the proxy. Default false: the app's own pages, links followed, and non-browser callers. */
34
+ allowCrossSite?: boolean;
35
+ /** Builds the answer to a refusal of the proxy's own, for a backend whose envelope is not Nexus's. Default `nexusFailure`. */
36
+ failure?: (failure: ApiProxyFailure) => Response;
37
+ /** Hears what the proxy hid or refused, on the server. Default: one line in `console.error`. */
38
+ onProblem?: (problem: ApiProxyProblem) => void;
13
39
  }
14
40
  /** What a Next.js route handler for `[module]/[...path]` receives. */
15
41
  export interface ApiProxyContext {
@@ -18,6 +44,15 @@ export interface ApiProxyContext {
18
44
  path?: string[];
19
45
  }>;
20
46
  }
47
+ /** What the proxy hid or refused, for the server's log. It never holds the user's token or a query string. */
48
+ export interface ApiProxyProblem {
49
+ /** The refusal's code, the one the browser got: "BACKEND_REDIRECT_REFUSED". */
50
+ messageCode: ApiProxyCode;
51
+ /** The call, as the browser made it: "POST configuration/Region/MassRecover". */
52
+ call: string;
53
+ /** What happened, for a developer: the backend's address and status, a piece of its error page, why it failed. */
54
+ detail: string;
55
+ }
21
56
  /**
22
57
  * Makes the route handlers of the proxy. Only the modules' configured roots can be reached: a path cannot climb out of
23
58
  * its module (`..` is refused), and the query goes on unchanged.