nexus-shared 3.2.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 (187) hide show
  1. package/CHANGELOG.md +1564 -0
  2. package/README.md +1705 -1123
  3. package/dist/Client.Index.d.ts +14 -0
  4. package/dist/Client.Index.js +14 -0
  5. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  6. package/dist/Components/Documents/DocumentFrame.js +83 -0
  7. package/dist/Components/Documents/Menu.d.ts +4 -0
  8. package/dist/Components/Documents/Menu.js +40 -24
  9. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  10. package/dist/Components/Documents/PrintDocument.js +62 -0
  11. package/dist/Components/Documents/Tabs.d.ts +59 -0
  12. package/dist/Components/Documents/Tabs.js +70 -0
  13. package/dist/Components/Forms/ApiForm.js +19 -2
  14. package/dist/Components/Forms/Crud.d.ts +2 -2
  15. package/dist/Components/Forms/Crud.js +576 -243
  16. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  17. package/dist/Components/Forms/CrudActions.js +184 -0
  18. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  19. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  20. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  21. package/dist/Components/Forms/CrudParts.js +55 -5
  22. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  23. package/dist/Components/Forms/CrudRowForms.js +114 -0
  24. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  25. package/dist/Components/Forms/CrudScopes.js +76 -0
  26. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  27. package/dist/Components/Forms/CrudTree.js +363 -0
  28. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  29. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  30. package/dist/Components/Forms/ExcelImport.js +3 -3
  31. package/dist/Components/Forms/Form.d.ts +4 -4
  32. package/dist/Components/Forms/Form.js +123 -17
  33. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  34. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  35. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  36. package/dist/Components/Forms/RecordFlows.js +282 -0
  37. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  38. package/dist/Components/Forms/SubmitForm.js +195 -10
  39. package/dist/Components/Inputs/CheckBox.js +1 -1
  40. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  41. package/dist/Components/Inputs/ChoiceField.js +28 -2
  42. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  43. package/dist/Components/Inputs/Dropdown.js +5 -5
  44. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  45. package/dist/Components/Inputs/DropdownParts.js +16 -4
  46. package/dist/Components/Inputs/Field.d.ts +7 -1
  47. package/dist/Components/Inputs/Field.js +2 -2
  48. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  49. package/dist/Components/Inputs/GroupForm.js +10 -11
  50. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  51. package/dist/Components/Inputs/InputRenderer.js +37 -32
  52. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  53. package/dist/Components/Inputs/PictureInput.js +70 -0
  54. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  55. package/dist/Components/Inputs/RadioGroup.js +8 -3
  56. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  57. package/dist/Components/Inputs/RowsInput.js +18 -3
  58. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  59. package/dist/Components/Inputs/TabularForm.js +29 -29
  60. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  61. package/dist/Components/Inputs/TextArea.js +11 -2
  62. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  63. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  64. package/dist/Components/Inputs/TreeView.d.ts +53 -2
  65. package/dist/Components/Inputs/TreeView.js +147 -13
  66. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  67. package/dist/Components/Layouts/MessageBox.js +56 -7
  68. package/dist/Components/Layouts/Popup.d.ts +13 -6
  69. package/dist/Components/Layouts/Popup.js +37 -9
  70. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  71. package/dist/Components/Layouts/PopupDock.js +2 -1
  72. package/dist/Components/Layouts/PopupHost.js +5 -4
  73. package/dist/Components/Viewers/DataTable.js +150 -48
  74. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  75. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  76. package/dist/Components/Viewers/DataTableParts.d.ts +21 -5
  77. package/dist/Components/Viewers/DataTableParts.js +26 -14
  78. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  79. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  80. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  81. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  82. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  83. package/dist/Components/Viewers/RecordCards.js +509 -0
  84. package/dist/Helpers/BusyHooks.d.ts +6 -0
  85. package/dist/Helpers/BusyHooks.js +39 -0
  86. package/dist/Helpers/CrudBackend.d.ts +20 -2
  87. package/dist/Helpers/CrudBackend.js +46 -2
  88. package/dist/Helpers/CrudColumns.d.ts +53 -6
  89. package/dist/Helpers/CrudColumns.js +98 -7
  90. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  91. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  92. package/dist/Helpers/CrudHelpers.d.ts +81 -2
  93. package/dist/Helpers/CrudHelpers.js +195 -0
  94. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  95. package/dist/Helpers/CrudNarrowPick.js +157 -0
  96. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  97. package/dist/Helpers/CrudRowForms.js +43 -0
  98. package/dist/Helpers/CrudScopes.d.ts +32 -0
  99. package/dist/Helpers/CrudScopes.js +79 -0
  100. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  101. package/dist/Helpers/DropdownHelpers.js +6 -2
  102. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  103. package/dist/Helpers/ExcelHelpers.js +12 -11
  104. package/dist/Helpers/FileAdapter.d.ts +78 -0
  105. package/dist/Helpers/FileAdapter.js +102 -0
  106. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  107. package/dist/Helpers/FilePrimaries.js +138 -0
  108. package/dist/Helpers/FormConfirm.d.ts +32 -0
  109. package/dist/Helpers/FormConfirm.js +40 -0
  110. package/dist/Helpers/FormStore.d.ts +90 -5
  111. package/dist/Helpers/FormStore.js +318 -22
  112. package/dist/Helpers/InputConditions.d.ts +50 -0
  113. package/dist/Helpers/InputConditions.js +168 -0
  114. package/dist/Helpers/InputMemory.d.ts +106 -0
  115. package/dist/Helpers/InputMemory.js +385 -0
  116. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  117. package/dist/Helpers/InputNarrowing.js +67 -0
  118. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  119. package/dist/Helpers/InputParamsHelpers.js +157 -74
  120. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  121. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  122. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  123. package/dist/Helpers/NumberHelpers.js +126 -0
  124. package/dist/Helpers/Permissions.js +2 -1
  125. package/dist/Helpers/QrCode.d.ts +26 -0
  126. package/dist/Helpers/QrCode.js +463 -0
  127. package/dist/Helpers/RecordCards.d.ts +205 -0
  128. package/dist/Helpers/RecordCards.js +378 -0
  129. package/dist/Helpers/RequestCache.d.ts +14 -2
  130. package/dist/Helpers/RequestCache.js +22 -3
  131. package/dist/Helpers/RowsStore.d.ts +37 -2
  132. package/dist/Helpers/RowsStore.js +128 -10
  133. package/dist/Helpers/ServerPreview.d.ts +69 -0
  134. package/dist/Helpers/ServerPreview.js +99 -0
  135. package/dist/Helpers/TableColumns.js +4 -3
  136. package/dist/Helpers/TableExport.d.ts +7 -1
  137. package/dist/Helpers/TableExport.js +9 -3
  138. package/dist/Helpers/TableFilters.d.ts +1 -1
  139. package/dist/Helpers/TableFilters.js +24 -23
  140. package/dist/Helpers/TableHelpers.d.ts +23 -2
  141. package/dist/Helpers/TableHelpers.js +51 -10
  142. package/dist/Helpers/TableJoins.d.ts +13 -0
  143. package/dist/Helpers/TableJoins.js +34 -0
  144. package/dist/Helpers/TableStore.d.ts +21 -1
  145. package/dist/Helpers/TableStore.js +64 -7
  146. package/dist/Helpers/TreeStore.d.ts +81 -0
  147. package/dist/Helpers/TreeStore.js +365 -0
  148. package/dist/Interfaces/CrudInterfaces.d.ts +465 -5
  149. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  150. package/dist/Interfaces/FileInterfaces.js +4 -0
  151. package/dist/Interfaces/FormInterfaces.d.ts +391 -30
  152. package/dist/Interfaces/FormInterfaces.js +79 -1
  153. package/dist/Interfaces/InputInterfaces.d.ts +15 -3
  154. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  155. package/dist/Interfaces/RecordInterfaces.js +1 -0
  156. package/dist/Interfaces/TableInterfaces.d.ts +74 -1
  157. package/dist/Services/ExcelApi.js +5 -0
  158. package/dist/Services/FileApi.d.ts +13 -0
  159. package/dist/Services/FileApi.js +29 -0
  160. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  161. package/dist/Services/ImageEditorLoader.js +51 -0
  162. package/dist/Services/JoinSources.d.ts +37 -5
  163. package/dist/Services/JoinSources.js +90 -14
  164. package/dist/Services/ModuleImport.d.ts +7 -0
  165. package/dist/Services/ModuleImport.js +22 -0
  166. package/dist/Shared.Index.d.ts +16 -0
  167. package/dist/Shared.Index.js +16 -0
  168. package/package.json +7 -2
  169. package/src/Styles/Nexus.Avatar.css +6 -5
  170. package/src/Styles/Nexus.Button.css +10 -4
  171. package/src/Styles/Nexus.Choice.css +16 -0
  172. package/src/Styles/Nexus.Crud.css +383 -9
  173. package/src/Styles/Nexus.Dropdown.css +8 -0
  174. package/src/Styles/Nexus.Files.css +410 -0
  175. package/src/Styles/Nexus.Form.css +5 -2
  176. package/src/Styles/Nexus.Index.css +4 -0
  177. package/src/Styles/Nexus.Input.css +42 -0
  178. package/src/Styles/Nexus.Menu.css +10 -1
  179. package/src/Styles/Nexus.Popup.css +77 -1
  180. package/src/Styles/Nexus.Print.css +288 -0
  181. package/src/Styles/Nexus.QrCode.css +12 -0
  182. package/src/Styles/Nexus.Record.css +430 -0
  183. package/src/Styles/Nexus.Tab.Buttons.css +87 -22
  184. package/src/Styles/Nexus.Table.css +67 -5
  185. package/src/Styles/Nexus.Themes.css +12 -6
  186. package/src/Styles/Nexus.Tokens.css +8 -3
  187. package/src/Styles/Nexus.Tree.css +31 -1
@@ -0,0 +1,367 @@
1
+ import type { ReactNode } from "react";
2
+ import type { PopupSize } from "../Components/Layouts/Popup.tsx";
3
+ import type { ApiEndpoint, ApiResult, ApiTarget } from "./ApiInterfaces.ts";
4
+ import type { CrudDetailsPicture } from "./CrudInterfaces.ts";
5
+ import type { FormStore } from "../Helpers/FormStore.ts";
6
+ import type { ApiRequest, ApiSend, FormRuleResult, FormSubmitTone, FormValues, InputParams } from "./FormInterfaces.ts";
7
+ import type { Tone } from "./TypeInterfaces.ts";
8
+ /**
9
+ * How a part of the page is read: an endpoint or a URL, read with GET through the app's `api`, or a function of the
10
+ * page's own answering an `ApiResult` (the API client's shape: it never throws).
11
+ */
12
+ export type RecordRead<T = any> = ApiTarget | ((signal: AbortSignal) => Promise<ApiResult<T>>);
13
+ /** A source given with what is known before it is read: the name the session already has, drawn while it loads. */
14
+ export interface RecordSourceParams<T = any> {
15
+ read: RecordRead<T>;
16
+ /** Drawn while the read runs (the hero's name from the session), so nothing moves when it lands. */
17
+ held?: Partial<T>;
18
+ }
19
+ /**
20
+ * What an answer writes into the card's data, which every card on the same source then shows:
21
+ * - `"answer"`: a record answer, merged over the data as held now (only the fields it carries); a list answer replaces a
22
+ * list. Default. An answer that is neither falls back to the values (a card's Edit) or a read again (the rest).
23
+ * - `"values"`: the values sent, merged over the data as held now.
24
+ * - `"item"`: the answer is one item: it replaces the item of the same key, or is added at the end.
25
+ * - `"remove"`: the item acted on leaves the list.
26
+ * - `"reload"`: the source is read again. `"none"`: nothing.
27
+ * - field names: those the answer carries, over the data as held now.
28
+ * - a function: what to merge over the data as held now (a record), or the data's new value; `null` writes nothing.
29
+ */
30
+ export type RecordWrites<TData = any, TValues = any> = "answer" | "values" | "item" | "remove" | "reload" | "none" | readonly string[] | ((result: unknown, data: TData, values: TValues) => TData | Partial<TData> | null | undefined);
31
+ /** A short mark beside a name: "Primary", "Not verified", "This device". */
32
+ export interface RecordMark {
33
+ label: ReactNode;
34
+ tone?: Tone;
35
+ }
36
+ /**
37
+ * Where a step's values go: an endpoint, a URL (sent as a POST of JSON), a request to a URL outside the app's modules
38
+ * (`{ url, method }`), or a function of the page's own.
39
+ */
40
+ export type RecordSubmit<TValues extends FormValues = any> = string | ApiRequest | ApiEndpoint | ApiSend<TValues>;
41
+ /** What every step, confirm and action is handed: the card's data and the item as held now, and the ways on. */
42
+ export interface RecordContext<TData = any, TItem = any> {
43
+ /** The card's data as the page holds it now. */
44
+ data: TData;
45
+ /** The item acted on, as held now (found again by its key after each write); `undefined` for a card's own action. */
46
+ item: TItem | undefined;
47
+ /** The page or the card is read-only. */
48
+ readOnly: boolean;
49
+ /** Reads the card's source again: its data, or `null` when it could not be read (said in a toast, the data kept). */
50
+ reload: () => Promise<TData | null>;
51
+ /** Writes the card's source: every card on it shows the new data. */
52
+ write: (next: TData) => void;
53
+ /** Runs one of the card's item actions on an item: Make primary after a Verify left no primary, the code step after an Add. */
54
+ run: (action: string, item: [TItem] extends [undefined] ? any : TItem) => void;
55
+ /** Opens a popup for this card (and this item): a content card's own button, a two-step setup. */
56
+ open: (flow: RecordFlowParams<TData, TItem>) => void;
57
+ }
58
+ /** How a step is shown by `goTo`. */
59
+ export interface RecordGoToOptions {
60
+ /** Shown afresh: what was typed in it before (a password, an old code) is dropped, and it opens on its values again. */
61
+ fresh?: boolean;
62
+ }
63
+ /** What a popup's step is handed: the context, which step it is, its form, and the answer of the step that led to it. */
64
+ export interface RecordStepContext<TData = any, TItem = any> extends RecordContext<TData, TItem> {
65
+ step: string;
66
+ /** The answer of the step before, the one that led here. */
67
+ result: unknown;
68
+ /**
69
+ * The step's form: its values as typed now (`form.get("countryId")`), for inputs whose label, `hidden` or options follow
70
+ * another input through getters (give them `dependsOn`). Each step has its own; one shown fresh gets a new one.
71
+ */
72
+ form: FormStore;
73
+ /** Shows another step of the popup; `{ fresh: true }` shows it as if for the first time. */
74
+ goTo: (step: string, options?: RecordGoToOptions) => void;
75
+ /** Closes the popup. */
76
+ close: () => void;
77
+ }
78
+ /** What a step's `next` and `done` are handed after its save: the context, the answer, and the values sent. */
79
+ export interface RecordStepAnswer<TData = any, TItem = any, TValues = any> extends RecordStepContext<TData, TItem> {
80
+ result: unknown;
81
+ values: TValues;
82
+ }
83
+ /** A button in a step that calls an endpoint and stays on the step: "Send a new code", held while the server says. */
84
+ export interface RecordCallParams<TData = any, TItem = any> {
85
+ label: ReactNode | ((context: RecordStepContext<TData, TItem>) => ReactNode);
86
+ icon?: ReactNode;
87
+ submit: ApiTarget | ApiRequest | ((body: unknown) => Promise<unknown>);
88
+ /** What is sent. Default: nothing. */
89
+ transform?: (context: RecordStepContext<TData, TItem>) => unknown;
90
+ /** What its answer writes (default `"answer"`, else nothing). */
91
+ writes?: RecordWrites<TData>;
92
+ /** Held until this moment (an ISO text or a time in ms), counted down in its label: the server's resend time. */
93
+ waitUntil?: (context: RecordStepContext<TData, TItem>) => string | number | null | undefined;
94
+ /** The label while it waits. Default "Again in 23s". */
95
+ waitLabel?: (seconds: number) => ReactNode;
96
+ /** The toast after it answered. Default the server's message; `false` for none. */
97
+ successMessage?: ReactNode | false;
98
+ /** The input given the cursor after it answered: the code's boxes. */
99
+ focus?: string;
100
+ }
101
+ /** One step of a popup: a sentence, its inputs, where they go, what the answer writes, and what follows. */
102
+ export interface RecordStepParams<TData = any, TItem = any, TValues extends FormValues = any> {
103
+ /** Names the step for `next` and `start`. Default its place: "1", "2". */
104
+ id?: string;
105
+ /** The popup's title on this step. Default the flow's. */
106
+ title?: ReactNode | ((context: RecordStepContext<TData, TItem>) => ReactNode);
107
+ /** The line under the title. Default the flow's. */
108
+ description?: ReactNode | ((context: RecordStepContext<TData, TItem>) => ReactNode);
109
+ /** A sentence over the inputs: what will happen, where a code went. It follows the data as held now. */
110
+ lead?: ReactNode | ((context: RecordStepContext<TData, TItem>) => ReactNode);
111
+ /**
112
+ * The inputs. None: the step is its sentence and its button ("Send a code"). A function is handed the step's `form`,
113
+ * so an input's label or `hidden` can follow another input as typed (a region level named after the place above it).
114
+ */
115
+ inputs?: readonly InputParams[] | ((context: RecordStepContext<TData, TItem>) => readonly InputParams[]);
116
+ /** What the form opens on. Default: the item for an item's step, the card's data for a card's Edit, nothing for an Add. */
117
+ values?: (context: RecordStepContext<TData, TItem>) => Partial<TValues>;
118
+ /** Where the values go. */
119
+ submit?: RecordSubmit<TValues>;
120
+ /**
121
+ * The step's own rule over all its values, asked before anything is sent, as a form's `validate`: messages by input
122
+ * name, or one text for the whole step (a new password and its confirmation that differ).
123
+ */
124
+ validate?: (values: TValues, context: RecordStepContext<TData, TItem>) => FormRuleResult;
125
+ /**
126
+ * Shown afresh each time it is reached again (by `next` or `goTo`): a step of a password or a code, whose old values
127
+ * are no use. Default false: a step shown again keeps what was typed in it.
128
+ */
129
+ fresh?: boolean;
130
+ /**
131
+ * What is sent. Default: the values merged over what the step opened on, as held now - the whole record for a card's
132
+ * Edit (so an endpoint that takes the whole record works), the item for an item's step; the values alone for an Add.
133
+ */
134
+ transform?: (values: TValues, context: RecordStepContext<TData, TItem>) => unknown;
135
+ /** Default "Save". */
136
+ submitLabel?: ReactNode;
137
+ /** Default a floppy disk; `null` for none. */
138
+ submitIcon?: ReactNode | null;
139
+ /** `danger` for a step whose save removes something. */
140
+ submitTone?: FormSubmitTone;
141
+ /** What the answer writes into the card's data. Default `"answer"`. */
142
+ writes?: RecordWrites<TData, TValues>;
143
+ /** The step after this one, by its id; nothing closes the popup. */
144
+ next?: string | ((answer: RecordStepAnswer<TData, TItem, TValues>) => string | null | undefined);
145
+ /** After the last step's save, once the popup has closed: open what follows (`run`). */
146
+ done?: (answer: RecordStepAnswer<TData, TItem, TValues>) => void;
147
+ /** A button under the form that calls an endpoint and stays on the step. */
148
+ also?: RecordCallParams<TData, TItem>;
149
+ /** The toast after the save. Default the server's message; `false` for none. */
150
+ successMessage?: ReactNode | false | ((answer: RecordStepAnswer<TData, TItem, TValues>) => ReactNode | false);
151
+ /** Told of a refusal, which the form already shows on its inputs: a code that offers another way. */
152
+ onError?: (answer: ApiResult, context: RecordStepContext<TData, TItem>) => void;
153
+ /**
154
+ * Cancel, the close button and leaving the page ask before dropping changes. Default true. A password or a code typed
155
+ * is never asked about (it is no change worth keeping); false for a step whose other values are not worth it either.
156
+ */
157
+ confirmCancel?: boolean;
158
+ /** The page's own part under the form: a "Set a password" link after a refusal. */
159
+ render?: (context: RecordStepContext<TData, TItem>) => ReactNode;
160
+ }
161
+ /**
162
+ * A popup: one step (the flow is that step) or several (`steps`), opened by a card's Edit or Add, or by an item's
163
+ * action. One popup per card, action and item: opened again, it comes back as it was.
164
+ */
165
+ export interface RecordFlowParams<TData = any, TItem = any, TValues extends FormValues = any> extends RecordStepParams<TData, TItem, TValues> {
166
+ /** The button that opens it: "Edit", "Add". */
167
+ label?: string;
168
+ /** The button's icon and the popup's. */
169
+ icon?: ReactNode;
170
+ /** Default `md` for a card's Edit with more than two inputs, else `sm`. */
171
+ size?: PopupSize;
172
+ /** Several steps, in order: the first unless `start` says. */
173
+ steps?: readonly RecordStepParams<TData, TItem>[];
174
+ /** The step it opens on: "code" when a code already waits. */
175
+ start?: string | ((context: RecordContext<TData, TItem>) => string);
176
+ }
177
+ /** A question before a change that needs no form: "Remove ram@example.com?", then the call. */
178
+ export interface RecordConfirmParams<TData = any, TItem = any> {
179
+ title: ReactNode | ((context: RecordContext<TData, TItem>) => ReactNode);
180
+ message?: ReactNode | ((context: RecordContext<TData, TItem>) => ReactNode);
181
+ /** Default "Remove" for a danger tone, else "OK". */
182
+ confirmLabel?: ReactNode;
183
+ /** Default `danger`. */
184
+ tone?: "danger" | "warning" | "question" | "info";
185
+ submit: ApiTarget | ApiRequest | ((body: unknown) => Promise<unknown>);
186
+ /** What is sent. Default: the item, or the card's data. */
187
+ transform?: (context: RecordContext<TData, TItem>) => unknown;
188
+ /** What the answer writes. Default `"answer"`, else a read again. */
189
+ writes?: RecordWrites<TData>;
190
+ /** The toast after it. Default the server's message, else "Done". `false` for none. */
191
+ successMessage?: ReactNode | false | ((context: RecordContext<TData, TItem>) => ReactNode | false);
192
+ /** After the call answered and the card was written. */
193
+ done?: (context: RecordContext<TData, TItem> & {
194
+ result: unknown;
195
+ }) => void;
196
+ }
197
+ /** A button in a card's heading: a popup (`flow`), a question then a call (`confirm`), or the page's own (`onSelect`). */
198
+ export interface RecordCardActionParams<TData = any> {
199
+ id: string;
200
+ label: string;
201
+ icon?: ReactNode;
202
+ tone?: "danger";
203
+ /** Not shown for this data. Asked only once the data is read (see `loading`). */
204
+ hidden?: (data: TData) => boolean;
205
+ /**
206
+ * Shown switched off with this reason, said when pressed: "You have 5 emails, the most there may be." Asked only once
207
+ * the data is read.
208
+ */
209
+ held?: (data: TData) => string | false | null | undefined;
210
+ /**
211
+ * How it shows while the data is read, before `hidden` and `held` can be asked: `held` (default) switched off, saying
212
+ * "Still being read."; `hidden` not yet drawn - for an action seldom shown (Turn off on a setting mostly off).
213
+ */
214
+ loading?: "held" | "hidden";
215
+ flow?: RecordFlowParams<TData, undefined>;
216
+ confirm?: RecordConfirmParams<TData, undefined>;
217
+ onSelect?: (context: RecordContext<TData, undefined>) => void;
218
+ }
219
+ /** An icon button on the right of an item: named as a tooltip and to a screen reader, hidden or held with its reason. */
220
+ export interface RecordItemActionParams<TData = any, TItem = any> {
221
+ id: string;
222
+ /** Its name, the tooltip and what a screen reader says (with the item's name): "Verify", "Make primary". */
223
+ label: string | ((item: TItem, data: TData) => string);
224
+ icon: ReactNode;
225
+ tone?: "danger";
226
+ /** Not shown on this item. */
227
+ hidden?: (item: TItem, data: TData) => boolean;
228
+ /** Shown switched off with this reason, said when pressed: "The primary email cannot be removed." */
229
+ held?: (item: TItem, data: TData) => string | false | null | undefined;
230
+ flow?: RecordFlowParams<TData, TItem>;
231
+ confirm?: RecordConfirmParams<TData, TItem>;
232
+ onSelect?: (context: RecordContext<TData, TItem>) => void;
233
+ }
234
+ /** What one card has whatever it holds. */
235
+ export interface RecordCardBase<TData = any> {
236
+ /** The card's element id, so a link can lead to it (`/dashboard/profile#emails`); unique on the page. */
237
+ id: string;
238
+ /** The source it is drawn from (a key of `sources`). Default the first. */
239
+ source?: string;
240
+ title: ReactNode;
241
+ icon?: ReactNode;
242
+ /** A sentence under the title. */
243
+ description?: ReactNode | ((data: TData) => ReactNode);
244
+ /** Buttons in the heading, after the card's own Edit or Add. */
245
+ actions?: readonly RecordCardActionParams<TData>[];
246
+ /** Not shown for this data (judged once it is read). */
247
+ hidden?: (data: TData) => boolean;
248
+ /** Takes the page's whole width. */
249
+ wide?: boolean;
250
+ /** No buttons and no actions in this card. */
251
+ readOnly?: boolean;
252
+ }
253
+ /** One value of a fields card, when its input alone does not say how to show it. */
254
+ export interface RecordFieldParams<TData = any> {
255
+ /** The input the value is drawn by (a choice by its label, a date in the person's format) and edited with. */
256
+ input: InputParams;
257
+ /** Default the input's label. */
258
+ label?: ReactNode;
259
+ /** `chips` for a list written with commas (keywords), `lines` for text on its own lines. Default by the input's type. */
260
+ look?: "text" | "lines" | "chips";
261
+ /** The value drawn by the page. */
262
+ show?: (value: unknown, data: TData) => ReactNode;
263
+ /** In the Edit popup. Default true; false for a value only shown here (the primary email). */
264
+ edit?: boolean;
265
+ hidden?: (data: TData) => boolean;
266
+ /** Takes the card's whole width. Default for long text and chips. */
267
+ wide?: boolean;
268
+ /**
269
+ * Lines the value holds while it is read, so the card is as tall as it will be: about what the value usually takes.
270
+ * Default 2 for long text, else 1.
271
+ */
272
+ heldLines?: number;
273
+ }
274
+ /** A card of labels and values, drawn from the inputs its Edit popup uses. */
275
+ export interface RecordFieldsCardParams<TData = any> extends RecordCardBase<TData> {
276
+ fields: readonly (InputParams | RecordFieldParams<TData>)[];
277
+ /**
278
+ * The heading's Edit: a popup of the card's inputs (those with `edit` not false) on the data as loaded, whose values are
279
+ * merged over the record as held now, so an endpoint that takes the whole record works. Needs `submit`.
280
+ */
281
+ edit?: RecordFlowParams<TData, undefined>;
282
+ /** What an empty value says. Default "Not said". */
283
+ empty?: ReactNode;
284
+ }
285
+ /** How one item reads: a picture or an icon, its name, lines under it, and marks. */
286
+ export interface RecordItemView {
287
+ title: ReactNode;
288
+ lines?: readonly ReactNode[];
289
+ marks?: readonly RecordMark[];
290
+ /** A picture at the start (an `Avatar`, an `OwnerPicture`). */
291
+ picture?: ReactNode;
292
+ /** An icon at the start, where there is no picture. */
293
+ icon?: ReactNode;
294
+ }
295
+ /** A card of items: a list with Add in its heading and icon actions on each item's right. */
296
+ export interface RecordItemsCardParams<TData = any, TItem = any> extends RecordCardBase<TData> {
297
+ /** Where the items are: a field of the data (`"items"`), or a function. Default: the data is the list. */
298
+ items?: string | ((data: TData) => readonly TItem[]);
299
+ /** What tells one item from another: a field, or a function. */
300
+ itemKey: string | ((item: TItem) => string | number);
301
+ item: (item: TItem, data: TData) => RecordItemView;
302
+ /** The item's name for a screen reader ("Verify: ram@example.com"). Default its title when it is text. */
303
+ itemLabel?: (item: TItem) => string;
304
+ itemActions?: readonly RecordItemActionParams<TData, TItem>[];
305
+ /** The heading's Add: a popup form; held at the `limit`, saying why. Needs `submit`. */
306
+ add?: RecordFlowParams<TData, undefined>;
307
+ /** The most there may be: shown as "3 of 5", and Add held there. A field of the data, a number or a function. */
308
+ limit?: number | string | ((data: TData) => number | null | undefined);
309
+ /** Why Add is held at the limit. Default "The most there may be is 5. Remove one first." */
310
+ limitReason?: string | ((limit: number) => string);
311
+ /** What an empty list says. Default "Nothing here yet." */
312
+ empty?: ReactNode;
313
+ /** Rows drawn while the list loads: as many as it usually has, so nothing under the card moves. Default 2. */
314
+ heldRows?: number;
315
+ /** Lines under each held row's name: as many as a row usually has (`item(...).lines`). Default 1. */
316
+ heldRowLines?: number;
317
+ }
318
+ /** A card of the page's own. */
319
+ export interface RecordContentCardParams<TData = any> extends RecordCardBase<TData> {
320
+ render: (context: RecordContext<TData, undefined>) => ReactNode;
321
+ }
322
+ export type RecordCardParams<TData = any, TItem = any> = RecordFieldsCardParams<TData> | RecordItemsCardParams<TData, TItem> | RecordContentCardParams<TData>;
323
+ /** The card across the top: the picture (a click opens the pictures window), the name, a line or two, and marks. */
324
+ export interface RecordHeroParams<TData = any> {
325
+ /** Default the first source. */
326
+ source?: string;
327
+ /** The owner's main picture by its Drive params (`CrudDetailsPicture`), or anything to draw in its place. */
328
+ picture?: (data: TData) => CrudDetailsPicture | ReactNode;
329
+ title: (data: TData) => ReactNode;
330
+ /** Lines under the name. While the source loads they are given what `held` says; an empty one keeps its line. */
331
+ lines?: (data: TData) => readonly ReactNode[];
332
+ marks?: (data: TData) => readonly RecordMark[];
333
+ actions?: readonly RecordCardActionParams<TData>[];
334
+ }
335
+ /** What the page shows when a read or a change finds the session ended: its own words, and the way back in. */
336
+ export interface RecordSignedOutParams {
337
+ /** Default "Your session has ended". */
338
+ title?: ReactNode;
339
+ /** Default the answer's message. */
340
+ message?: ReactNode | ((answer: ApiResult) => ReactNode);
341
+ /** Default "Sign in again". */
342
+ label?: ReactNode;
343
+ onSignIn: () => void;
344
+ /** Told once, when the session is found ended. */
345
+ onEnded?: (answer: ApiResult) => void;
346
+ }
347
+ export interface RecordCardsProps {
348
+ /**
349
+ * What the page reads, by name, each once when it is first drawn (a new `key` reads them afresh):
350
+ * `{ profile: { read: ACCOUNT_API.profile, held: { fullName } }, emails: ACCOUNT_API.emails }`.
351
+ */
352
+ sources: Readonly<Record<string, RecordRead | RecordSourceParams>>;
353
+ hero?: RecordHeroParams;
354
+ cards: readonly RecordCardParams[];
355
+ /** No buttons and no actions anywhere: a person who may only look. */
356
+ readOnly?: boolean;
357
+ /** Without it, an ended session is a failed read like any other. */
358
+ signedOut?: RecordSignedOutParams;
359
+ /** Which answers mean the session ended. Default: a 401 that names no input (a wrong password names its box). */
360
+ isSessionEnded?: (answer: ApiResult) => boolean;
361
+ /** Tells this page's popups from another's. Default "record". */
362
+ name?: string;
363
+ /** The level of the cards' headings. Default 2, under the page's own. */
364
+ headingLevel?: 2 | 3;
365
+ "aria-label"?: string;
366
+ className?: string;
367
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -44,6 +44,11 @@ export interface TableColumn<TRow extends TableRow = TableRow> {
44
44
  decimals?: number;
45
45
  /** Text before a number, such as "Rs. ". */
46
46
  prefix?: string;
47
+ /**
48
+ * A `money` column's currency, written before the figures with a space: "Rs." gives "Rs. 1,250.00". A text, or the
49
+ * row's own (`row => row.currencyText`). Default the table's `currency`, so a page sets its company's once.
50
+ */
51
+ currency?: string | ((row: TRow) => string | null | undefined);
47
52
  /** Text after a number, such as " kg". */
48
53
  suffix?: string;
49
54
  /** Labels for the values the column holds, such as the names of an enum's numbers. */
@@ -95,6 +100,23 @@ export interface TableColumn<TRow extends TableRow = TableRow> {
95
100
  * the record it looked up; nothing when it answers nothing. `crudColumns.dbJoin` and `crudColumns.clientJoin` set it.
96
101
  */
97
102
  tag?: (row: TRow, joined?: TableJoinedRecord) => TableTag | null | undefined;
103
+ /**
104
+ * The value is another record's name the database joined into the row, by the key this row holds in `from`
105
+ * (`crudColumns.dbJoin`). The table shows the row as it is; a CRUD that saved the row writes the name - and the status
106
+ * in `status` - again from the record picked for that key, with no call (`crudJoinedNames`). Without `from`, or where
107
+ * no record is known for the key, it reads the saved record back once (`details`) instead (`crudJoinsMissing`).
108
+ */
109
+ dbJoin?: TableDbJoin;
110
+ }
111
+ /** Where a column the database joined takes its name from (`TableColumn.dbJoin`). */
112
+ export interface TableDbJoin {
113
+ /**
114
+ * The field of this row that holds the other record's key, and the form's input for it: `"parentId"`. Without it a
115
+ * save cannot tell whose name the column shows, and a new record is read back once for it.
116
+ */
117
+ from?: string;
118
+ /** The field of this row that holds the other record's status: `"parentRecordStatus"`. */
119
+ status?: string;
98
120
  }
99
121
  /** A record a `join` looked up: whatever its source answers for one key. */
100
122
  export type TableJoinedRecord = Readonly<Record<string, unknown>>;
@@ -164,6 +186,8 @@ export interface TablePage<TRow extends TableRow = TableRow> {
164
186
  export type TableLoader<TRow extends TableRow = TableRow> = (query: TableQuery, signal: AbortSignal) => TablePage<TRow> | readonly TRow[] | Promise<TablePage<TRow> | readonly TRow[]>;
165
187
  /** `client`: the table has every row and pages them itself. `server`: each page comes from the loader. */
166
188
  export type TablePaging = "client" | "server";
189
+ /** Where a table keeps its paging (`DataTableProps.pager`): the rows per page in the bar or in the footer, or every row on one page. */
190
+ export type TablePagerMode = "bar" | "footer" | "none";
167
191
  /** `loading`: the first rows are on their way. `ready`: rows (or none) are shown. `error`: the rows could not load. */
168
192
  export type TableStatus = "loading" | "ready" | "error";
169
193
  /** What the table shows now. A new object after every change. */
@@ -184,6 +208,8 @@ export interface TableState<TRow extends TableRow = TableRow> {
184
208
  query: TableQuery;
185
209
  /** The selected rows by key (keys as text), in the order they were picked. */
186
210
  selected: ReadonlyMap<string, TRow>;
211
+ /** The one row the user picked (`DataTable` `onPick`), as the table has it now: its newest copy. `null` for none. */
212
+ picked: TRow | null;
187
213
  /** Rows with an action running. */
188
214
  busy: ReadonlySet<string>;
189
215
  /** Rows just added or changed, highlighted for a moment. */
@@ -202,6 +228,12 @@ export interface TableViewColumn<TRow extends TableRow = TableRow> {
202
228
  column: TableColumn<TRow>;
203
229
  /** The side it stays on, or null when it scrolls with the rest. */
204
230
  pin: TablePinSide | null;
231
+ /**
232
+ * The side it is pinned to (the person's choice or the column's): where it is drawn, among which columns it moves,
233
+ * and what its menu offers. `pin` is the same unless the table is too narrow for it to stay (`PINNED_SHARE`): then
234
+ * `pin` is null and it scrolls with the rest until there is room again.
235
+ */
236
+ side: TablePinSide | null;
205
237
  /** Pixels from that edge, once the table has measured the columns before it. */
206
238
  offset: number;
207
239
  /** The last pinned column of its side: the one that shows the shadow while columns scroll under it. */
@@ -341,6 +373,11 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
341
373
  searchable?: boolean;
342
374
  /** Default "Search…". */
343
375
  searchPlaceholder?: string;
376
+ /**
377
+ * A line at the top of the table, inside its frame and over its bar, saying what its rows are: whose records a narrowed
378
+ * `Crud` shows ("Places of: Ram Thapa"). Beside another table, the two frames still start level.
379
+ */
380
+ caption?: ReactNode;
344
381
  /** On the left of the bar, after the Add section: tabs, filters. On a narrow table the line under the search. */
345
382
  toolbar?: ReactNode;
346
383
  /** The page's own buttons, in the Add section after the table's Add: first in the bar, last on a phone. */
@@ -348,10 +385,19 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
348
385
  /**
349
386
  * The table's Add, first in the bar. With `excel`, its arrow holds the template, the import and the export, which
350
387
  * then leave the options menu.
388
+ *
389
+ * Its `description` is its tooltip and what a screen reader hears after its name. `disabled` with a `description`
390
+ * says why nothing can be added yet: the button stays in the Tab order, looks unavailable, and a press - a click, a
391
+ * tap, Enter - shows the description in a note under it, as a `TreeView`'s own button does; the spreadsheet items then
392
+ * wait in the options menu, without Bulk insert. `onDisabledPress` answers such a press first: when it returns true the
393
+ * page has answered it itself (a CRUD narrowed by a box of its own moves the cursor to that box), and no note shows.
351
394
  */
352
395
  add?: {
353
396
  label: ReactNode;
354
397
  onClick: () => void;
398
+ disabled?: boolean;
399
+ description?: string;
400
+ onDisabledPress?: () => boolean;
355
401
  };
356
402
  /** The Columns panel, where columns are shown, hidden, and dragged into another order. Default true. */
357
403
  columnsMenu?: boolean;
@@ -410,6 +456,19 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
410
456
  * does, and so does a click that ends a text selection, so a value can still be copied out of a cell.
411
457
  */
412
458
  openOnClick?: boolean;
459
+ /**
460
+ * Rows the user picks one of, for a page that shows what belongs to the picked one beside the table: a person's
461
+ * places beside the people. A click on a row picks it (a double-click still opens it, and `openOnClick` gives way); the
462
+ * keyboard reaches the rows through a radio button each, read with the row's name: Tab to the picked one, the arrow
463
+ * keys to move the pick. The picked row is tinted as a selected one is, and the rows are numbered unless `numbered` is
464
+ * false.
465
+ *
466
+ * The pick lives in the table's store (`TableState.picked`, `TableStore.pick`) and follows the record: a search, a
467
+ * sort or another page keep it; an edit hands the page its new copy; it is let go when the record leaves the rows
468
+ * (trashed, deleted, gone after a reload) and when another list starts over (`loadKey`). Called with every change,
469
+ * `null` included.
470
+ */
471
+ onPick?: (row: TRow | null) => void;
413
472
  /**
414
473
  * Sections above the page, in this order, each folding open and shut on its own: the records the user pinned, then
415
474
  * their favourites. A section with no rows and none on their way does not show.
@@ -432,6 +491,14 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
432
491
  pinnedLabel?: string;
433
492
  /** Most rows that can be pinned, shown beside the count so the user knows the limit. */
434
493
  pinnedMax?: number;
494
+ /**
495
+ * How the rows are paged on screen (`TablePagerMode`). `"bar"` (default): rows per page inside the search box, or in the
496
+ * bar without a search, and the pager in the footer. `"footer"`: rows per page beside the pager, so a table with nothing
497
+ * else above its rows draws no bar. `"none"`: every row on one page, for a short list read whole (a record's notes, at
498
+ * most 50): no pager, no rows per page, and no footer unless a search or the filters leave rows out (a screen reader
499
+ * still hears the count). A server's pages cannot all be shown: with `paging="server"`, `"none"` is `"footer"`.
500
+ */
501
+ pager?: TablePagerMode;
435
502
  /** Rows per page. Default 10, or the user's last choice with `stateKey`. */
436
503
  pageSize?: number;
437
504
  /** Choices of rows per page. Default 10, 25, 50, 100. */
@@ -440,12 +507,18 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
440
507
  defaultSort?: TableSort | null;
441
508
  /**
442
509
  * Row numbers in the first column. Default: on when `selectable`, where the number becomes the row's checkbox as soon
443
- * as the pointer is over the row or anything is selected.
510
+ * as the pointer is over the row or anything is selected, and when rows are picked (`onPick`), where the first column
511
+ * also holds each row's radio button.
444
512
  */
445
513
  numbered?: boolean;
446
514
  /** Remembers the page size, the sort, hidden columns, the order, widths, and pins of the columns, and which sections
447
515
  * are open, on this device. */
448
516
  stateKey?: string;
517
+ /**
518
+ * The currency of every `money` column without its own (`TableColumn.currency`): "Rs." writes "Rs. 1,250.00" in the
519
+ * cells, the details, the CSV and the print. Set once per table from the company's settings.
520
+ */
521
+ currency?: string | null;
449
522
  /** Milliseconds after the last key before a search goes to a server. Default 300; a table with every row searches at once. */
450
523
  searchDelay?: number;
451
524
  /**
@@ -39,6 +39,9 @@ export function clearExcelSettings() {
39
39
  /** The template to fill in, as the backend built it, under the name the answer gives it. */
40
40
  export async function downloadExcelTemplate(endpoints, options = {}) {
41
41
  const limits = await excelSettings(endpoints, { signal: options.signal, params: options.params, toast: options.toast });
42
+ // A refused or failed settings call has said why, once: the file would only fail the same way, in a second toast.
43
+ if (!limits.ok)
44
+ return { ...limits, result: undefined };
42
45
  const name = limits.result?.templateFileName ?? "template.xlsx";
43
46
  const answer = await download(endpoints.template, options, name);
44
47
  if (answer.ok && answer.result)
@@ -57,6 +60,8 @@ export async function exportExcel(endpoints, options = {}) {
57
60
  if (!endpoint)
58
61
  throw new Error("This model has no export endpoint: `excelEndpoints` was given `exportable: false`, or the app's own endpoints left `export` out.");
59
62
  const limits = await excelSettings(endpoints, { signal: options.signal, params: options.params, toast: options.toast });
63
+ if (!limits.ok)
64
+ return { ...limits, result: undefined };
60
65
  const subject = options.subject ?? limits.result?.subject ?? "row";
61
66
  const limit = limits.result?.maxExportRows;
62
67
  if (options.confirm !== false && limit !== undefined && options.rows !== undefined && options.rows > limit) {
@@ -0,0 +1,13 @@
1
+ import { PrimaryPictures } from "../Helpers/FilePrimaries.ts";
2
+ import type { FileAdapter } from "../Helpers/FileAdapter.ts";
3
+ /**
4
+ * Registers the app's file backend, once, beside its API modules. Without it, the Nexus Drive.
5
+ *
6
+ * @example
7
+ * defineFileAdapter(new NexusDriveAdapter({ client: api, drive: HrModule.Files }));
8
+ */
9
+ export declare function defineFileAdapter<T extends FileAdapter>(adapter: T): T;
10
+ /** The registered file backend, or the Nexus Drive. */
11
+ export declare function getFileAdapter(): FileAdapter;
12
+ /** Each owner's picture for the page's rows, read in one call per page and kept until a change on the page. */
13
+ export declare const primaryPictures: PrimaryPictures;
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { PrimaryPictures } from "../Helpers/FilePrimaries.js";
3
+ import { NexusDriveAdapter } from "../Helpers/NexusDriveAdapter.js";
4
+ import { api } from "./BrowserApi.js";
5
+ /*
6
+ * The app's file backend and the page's kept pictures. Without a `defineFileAdapter`, the Nexus Drive through the app's
7
+ * `api` (its token, headers and proxy).
8
+ */
9
+ /* The app's adapter */
10
+ let current = null;
11
+ /**
12
+ * Registers the app's file backend, once, beside its API modules. Without it, the Nexus Drive.
13
+ *
14
+ * @example
15
+ * defineFileAdapter(new NexusDriveAdapter({ client: api, drive: HrModule.Files }));
16
+ */
17
+ export function defineFileAdapter(adapter) {
18
+ current = adapter;
19
+ primaryPictures.clear();
20
+ return adapter;
21
+ }
22
+ /** The registered file backend, or the Nexus Drive. */
23
+ export function getFileAdapter() {
24
+ return (current ??= new NexusDriveAdapter({ client: api }));
25
+ }
26
+ /** Each owner's picture for the page's rows, read in one call per page and kept until a change on the page. */
27
+ export const primaryPictures = new PrimaryPictures((module, category, ownerIds) => getFileAdapter().primaries(module, category, ownerIds, { toast: false }), {
28
+ batchSize: () => getFileAdapter().maxOwners,
29
+ });
@@ -0,0 +1,14 @@
1
+ import type { ImageEditorModule } from "../Interfaces/FileInterfaces.ts";
2
+ /**
3
+ * The image editor module, downloaded once per page. A failed download is not kept, so the next Edit tries again.
4
+ * Rejects with an `Error` in plain words ("The image editor is not installed on this server yet.", "The image editor
5
+ * could not be downloaded..."), and logs the cause (a network error, a 404, a bundler that rewrote the import) to the
6
+ * console, so a page that shows the message never hides why.
7
+ *
8
+ * @example
9
+ * const { openImageEditor } = await loadImageEditor();
10
+ * const picture = await openImageEditor({ src: file, aspect: 1, thumbnail: 256 });
11
+ */
12
+ export declare function loadImageEditor(): Promise<ImageEditorModule>;
13
+ /** Starts the download early, on the hover or focus of an Edit button, so the editor opens at once. Never rejects. */
14
+ export declare function preloadImageEditor(): void;
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { getFileAdapter } from "./FileApi.js";
3
+ import { importModule } from "./ModuleImport.js";
4
+ /*
5
+ * The image editor (`nexus-image-editor`, about 45 KB gzipped) is never in an app's bundle: the file backend serves it
6
+ * at a versioned address, and this loader downloads it the first time someone edits a picture. The address comes from
7
+ * the adapter (with Nexus, Drive's `Assets/File/nexus-image-editor.txt` names the current file), and the import is left
8
+ * alone by the bundler (`importModule`, ModuleImport.ts), so the module is fetched by the browser itself and cached by
9
+ * its immutable name.
10
+ */
11
+ let loading = null;
12
+ /** What a person reads when the module itself could not be downloaded or run: the cause goes to the console. */
13
+ const NOT_DOWNLOADED = "The image editor could not be downloaded. Check the connection and try again; if it keeps failing, tell the site's support.";
14
+ /**
15
+ * The image editor module, downloaded once per page. A failed download is not kept, so the next Edit tries again.
16
+ * Rejects with an `Error` in plain words ("The image editor is not installed on this server yet.", "The image editor
17
+ * could not be downloaded..."), and logs the cause (a network error, a 404, a bundler that rewrote the import) to the
18
+ * console, so a page that shows the message never hides why.
19
+ *
20
+ * @example
21
+ * const { openImageEditor } = await loadImageEditor();
22
+ * const picture = await openImageEditor({ src: file, aspect: 1, thumbnail: 256 });
23
+ */
24
+ export function loadImageEditor() {
25
+ loading ??= getFileAdapter()
26
+ .editorModuleUrl()
27
+ .then(url => importModule(url).catch((cause) => {
28
+ console.error(`loadImageEditor(): ${url} could not be imported.`, cause);
29
+ throw new Error(NOT_DOWNLOADED);
30
+ }), (cause) => {
31
+ // The address could not be read: the adapter says why in plain words ("not installed on this server yet").
32
+ console.error("loadImageEditor(): the image editor's address could not be read.", cause);
33
+ throw cause instanceof Error && cause.message ? cause : new Error(NOT_DOWNLOADED);
34
+ })
35
+ .then(module => {
36
+ if (typeof module?.openImageEditor !== "function") {
37
+ console.error("loadImageEditor(): the module downloaded has no openImageEditor.", module);
38
+ throw new Error("The image editor on the server is not one this page can open.");
39
+ }
40
+ return module;
41
+ })
42
+ .catch((error) => {
43
+ loading = null;
44
+ throw error instanceof Error ? error : new Error(NOT_DOWNLOADED);
45
+ });
46
+ return loading;
47
+ }
48
+ /** Starts the download early, on the hover or focus of an Edit button, so the editor opens at once. Never rejects. */
49
+ export function preloadImageEditor() {
50
+ loadImageEditor().catch(() => undefined);
51
+ }