nexus-shared 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +2268 -446
  2. package/LICENSE +21 -21
  3. package/README.md +1705 -1084
  4. package/dist/Client.Index.d.ts +17 -0
  5. package/dist/Client.Index.js +17 -0
  6. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  7. package/dist/Components/Documents/DocumentFrame.js +83 -0
  8. package/dist/Components/Documents/Menu.d.ts +4 -0
  9. package/dist/Components/Documents/Menu.js +40 -24
  10. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  11. package/dist/Components/Documents/PrintDocument.js +62 -0
  12. package/dist/Components/Documents/Tabs.d.ts +59 -0
  13. package/dist/Components/Documents/Tabs.js +70 -0
  14. package/dist/Components/Forms/ApiForm.js +19 -2
  15. package/dist/Components/Forms/Crud.d.ts +2 -2
  16. package/dist/Components/Forms/Crud.js +871 -377
  17. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  18. package/dist/Components/Forms/CrudActions.js +184 -0
  19. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  20. package/dist/Components/Forms/CrudHistory.js +140 -0
  21. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  22. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  23. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  24. package/dist/Components/Forms/CrudParts.js +56 -5
  25. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  26. package/dist/Components/Forms/CrudRowForms.js +114 -0
  27. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  28. package/dist/Components/Forms/CrudScopes.js +76 -0
  29. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  30. package/dist/Components/Forms/CrudTree.js +363 -0
  31. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  32. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  33. package/dist/Components/Forms/ExcelImport.js +3 -3
  34. package/dist/Components/Forms/Form.d.ts +4 -4
  35. package/dist/Components/Forms/Form.js +125 -16
  36. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  37. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  38. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  39. package/dist/Components/Forms/RecordFlows.js +282 -0
  40. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  41. package/dist/Components/Forms/SubmitForm.js +195 -10
  42. package/dist/Components/Inputs/CheckBox.js +1 -1
  43. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  44. package/dist/Components/Inputs/ChoiceField.js +28 -2
  45. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  46. package/dist/Components/Inputs/CodeBox.js +316 -0
  47. package/dist/Components/Inputs/DatePicker.js +4 -1
  48. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  49. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  50. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  51. package/dist/Components/Inputs/Dropdown.js +8 -5
  52. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  53. package/dist/Components/Inputs/DropdownParts.js +16 -4
  54. package/dist/Components/Inputs/Field.d.ts +7 -1
  55. package/dist/Components/Inputs/Field.js +2 -2
  56. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  57. package/dist/Components/Inputs/GroupForm.js +10 -11
  58. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  59. package/dist/Components/Inputs/InputRenderer.js +39 -31
  60. package/dist/Components/Inputs/NumberBox.js +4 -2
  61. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  62. package/dist/Components/Inputs/PictureInput.js +70 -0
  63. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  64. package/dist/Components/Inputs/RadioGroup.js +8 -3
  65. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  66. package/dist/Components/Inputs/RowsInput.js +18 -3
  67. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  68. package/dist/Components/Inputs/TabularForm.js +29 -29
  69. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  70. package/dist/Components/Inputs/TextArea.js +14 -2
  71. package/dist/Components/Inputs/TextBox.js +4 -1
  72. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  73. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  74. package/dist/Components/Inputs/TimePicker.js +4 -1
  75. package/dist/Components/Inputs/TreeView.d.ts +107 -6
  76. package/dist/Components/Inputs/TreeView.js +513 -30
  77. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  78. package/dist/Components/Layouts/MessageBox.js +56 -7
  79. package/dist/Components/Layouts/Popup.d.ts +13 -6
  80. package/dist/Components/Layouts/Popup.js +37 -9
  81. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  82. package/dist/Components/Layouts/PopupDock.js +2 -1
  83. package/dist/Components/Layouts/PopupHost.js +5 -4
  84. package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
  85. package/dist/Components/Layouts/ThemePicker.js +16 -4
  86. package/dist/Components/Viewers/DataTable.js +205 -61
  87. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  88. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  89. package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
  90. package/dist/Components/Viewers/DataTableParts.js +62 -21
  91. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  92. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  93. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  94. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  95. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  96. package/dist/Components/Viewers/RecordCards.js +509 -0
  97. package/dist/Helpers/ApiModules.d.ts +3 -34
  98. package/dist/Helpers/ApiModules.js +3 -3
  99. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  100. package/dist/Helpers/ApiRoutes.js +31 -27
  101. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  102. package/dist/Helpers/ArrayHelpers.js +2 -0
  103. package/dist/Helpers/BusyHooks.d.ts +6 -0
  104. package/dist/Helpers/BusyHooks.js +39 -0
  105. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  106. package/dist/Helpers/CodeHelpers.js +332 -0
  107. package/dist/Helpers/CrudBackend.d.ts +114 -6
  108. package/dist/Helpers/CrudBackend.js +214 -11
  109. package/dist/Helpers/CrudColumns.d.ts +98 -0
  110. package/dist/Helpers/CrudColumns.js +160 -0
  111. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  112. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  113. package/dist/Helpers/CrudHelpers.d.ts +130 -6
  114. package/dist/Helpers/CrudHelpers.js +304 -12
  115. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  116. package/dist/Helpers/CrudNarrowPick.js +157 -0
  117. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  118. package/dist/Helpers/CrudRowForms.js +43 -0
  119. package/dist/Helpers/CrudScopes.d.ts +32 -0
  120. package/dist/Helpers/CrudScopes.js +79 -0
  121. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  122. package/dist/Helpers/DropdownHelpers.js +6 -2
  123. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  124. package/dist/Helpers/ExcelHelpers.js +12 -11
  125. package/dist/Helpers/FileAdapter.d.ts +78 -0
  126. package/dist/Helpers/FileAdapter.js +102 -0
  127. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  128. package/dist/Helpers/FilePrimaries.js +138 -0
  129. package/dist/Helpers/FormConfirm.d.ts +32 -0
  130. package/dist/Helpers/FormConfirm.js +40 -0
  131. package/dist/Helpers/FormStore.d.ts +90 -5
  132. package/dist/Helpers/FormStore.js +318 -22
  133. package/dist/Helpers/InputConditions.d.ts +50 -0
  134. package/dist/Helpers/InputConditions.js +168 -0
  135. package/dist/Helpers/InputMemory.d.ts +106 -0
  136. package/dist/Helpers/InputMemory.js +385 -0
  137. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  138. package/dist/Helpers/InputNarrowing.js +67 -0
  139. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  140. package/dist/Helpers/InputParamsHelpers.js +162 -71
  141. package/dist/Helpers/MessageBuilder.js +2 -0
  142. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  143. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  144. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  145. package/dist/Helpers/NexusHelpers.js +1 -0
  146. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  147. package/dist/Helpers/NumberHelpers.js +126 -0
  148. package/dist/Helpers/Permissions.js +2 -1
  149. package/dist/Helpers/QrCode.d.ts +26 -0
  150. package/dist/Helpers/QrCode.js +463 -0
  151. package/dist/Helpers/RecordCards.d.ts +205 -0
  152. package/dist/Helpers/RecordCards.js +378 -0
  153. package/dist/Helpers/RequestCache.d.ts +47 -0
  154. package/dist/Helpers/RequestCache.js +132 -0
  155. package/dist/Helpers/RowsStore.d.ts +37 -2
  156. package/dist/Helpers/RowsStore.js +128 -10
  157. package/dist/Helpers/ServerPreview.d.ts +69 -0
  158. package/dist/Helpers/ServerPreview.js +99 -0
  159. package/dist/Helpers/TableColumns.js +4 -3
  160. package/dist/Helpers/TableExport.d.ts +7 -1
  161. package/dist/Helpers/TableExport.js +21 -15
  162. package/dist/Helpers/TableFilters.d.ts +1 -1
  163. package/dist/Helpers/TableFilters.js +24 -23
  164. package/dist/Helpers/TableHelpers.d.ts +24 -3
  165. package/dist/Helpers/TableHelpers.js +58 -12
  166. package/dist/Helpers/TableJoins.d.ts +83 -0
  167. package/dist/Helpers/TableJoins.js +284 -0
  168. package/dist/Helpers/TableStore.d.ts +26 -1
  169. package/dist/Helpers/TableStore.js +73 -7
  170. package/dist/Helpers/TreeStore.d.ts +306 -11
  171. package/dist/Helpers/TreeStore.js +1048 -90
  172. package/dist/Helpers/UserPreferences.d.ts +8 -0
  173. package/dist/Helpers/UserPreferences.js +17 -0
  174. package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
  175. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  176. package/dist/Interfaces/FileInterfaces.js +4 -0
  177. package/dist/Interfaces/FormInterfaces.d.ts +393 -29
  178. package/dist/Interfaces/FormInterfaces.js +79 -1
  179. package/dist/Interfaces/InputInterfaces.d.ts +39 -3
  180. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  181. package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
  182. package/dist/Interfaces/PreferenceInterfaces.js +2 -5
  183. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  184. package/dist/Interfaces/RecordInterfaces.js +1 -0
  185. package/dist/Interfaces/TableInterfaces.d.ts +160 -5
  186. package/dist/Services/ApiProxy.d.ts +36 -1
  187. package/dist/Services/ApiProxy.js +182 -52
  188. package/dist/Services/ApiProxyRules.d.ts +203 -0
  189. package/dist/Services/ApiProxyRules.js +426 -0
  190. package/dist/Services/ExcelApi.js +5 -0
  191. package/dist/Services/FileApi.d.ts +13 -0
  192. package/dist/Services/FileApi.js +29 -0
  193. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  194. package/dist/Services/ImageEditorLoader.js +51 -0
  195. package/dist/Services/JoinSources.d.ts +80 -0
  196. package/dist/Services/JoinSources.js +150 -0
  197. package/dist/Services/ModuleImport.d.ts +7 -0
  198. package/dist/Services/ModuleImport.js +22 -0
  199. package/dist/Services/ServerApi.d.ts +13 -2
  200. package/dist/Services/ServerApi.js +8 -3
  201. package/dist/Shared.Index.d.ts +20 -0
  202. package/dist/Shared.Index.js +20 -0
  203. package/package.json +92 -85
  204. package/src/Styles/Nexus.Avatar.css +80 -79
  205. package/src/Styles/Nexus.Base.css +117 -117
  206. package/src/Styles/Nexus.Button.css +186 -180
  207. package/src/Styles/Nexus.Calendar.css +273 -273
  208. package/src/Styles/Nexus.Chat.css +1443 -1443
  209. package/src/Styles/Nexus.Choice.css +359 -343
  210. package/src/Styles/Nexus.Crud.css +542 -168
  211. package/src/Styles/Nexus.DateTime.css +81 -81
  212. package/src/Styles/Nexus.Dropdown.css +733 -725
  213. package/src/Styles/Nexus.Editor.css +295 -295
  214. package/src/Styles/Nexus.Excel.css +389 -389
  215. package/src/Styles/Nexus.Files.css +410 -0
  216. package/src/Styles/Nexus.Form.css +282 -279
  217. package/src/Styles/Nexus.Index.css +41 -37
  218. package/src/Styles/Nexus.Input.css +238 -4
  219. package/src/Styles/Nexus.Markdown.css +237 -237
  220. package/src/Styles/Nexus.Menu.css +246 -237
  221. package/src/Styles/Nexus.Popup.css +800 -724
  222. package/src/Styles/Nexus.Print.css +288 -0
  223. package/src/Styles/Nexus.QrCode.css +12 -0
  224. package/src/Styles/Nexus.Record.css +430 -0
  225. package/src/Styles/Nexus.Rows.css +637 -637
  226. package/src/Styles/Nexus.Slider.css +276 -276
  227. package/src/Styles/Nexus.Tab.Buttons.css +497 -432
  228. package/src/Styles/Nexus.Table.css +98 -35
  229. package/src/Styles/Nexus.Theme.Picker.css +59 -3
  230. package/src/Styles/Nexus.Themes.css +200 -194
  231. package/src/Styles/Nexus.Time.css +144 -144
  232. package/src/Styles/Nexus.Toast.css +263 -263
  233. package/src/Styles/Nexus.Tokens.css +94 -89
  234. package/src/Styles/Nexus.Tree.css +530 -431
@@ -0,0 +1,16 @@
1
+ import type { RecordCardsProps } from "../../Interfaces/RecordInterfaces.ts";
2
+ /**
3
+ * A record's page of cards, declared by params: the `sources` it reads, a `hero`, and its `cards` (fields, items, or its
4
+ * own). See `RecordCardsProps` and the interfaces it names.
5
+ *
6
+ * @example
7
+ * <RecordCards
8
+ * sources={{ profile: ACCOUNT_API.profile, emails: ACCOUNT_API.emails }}
9
+ * hero={{ title: p => p.fullName, lines: p => [`@${p.username}`] }}
10
+ * cards={[
11
+ * { id: "personal", title: "Personal details", fields: [FULL_NAME, DATE_OF_BIRTH], edit: { submit: { url: ACCOUNT_API.saveProfile } } },
12
+ * { id: "emails", source: "emails", title: "Emails", items: "items", itemKey: "id", item: e => ({ title: e.address }), add: { ... } },
13
+ * ]}
14
+ * />
15
+ */
16
+ export declare function RecordCards(props: RecordCardsProps): import("react").JSX.Element;
@@ -0,0 +1,509 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { iconAlertCircle, iconPencil, iconPlus, iconRefresh, InlineIcon } from "nexus-icons";
4
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
5
+ import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { useDatePreferences } from "../../Helpers/DatePreferences.js";
7
+ import { getPermissions, permissionsVersion, subscribePermissions } from "../../Helpers/Permissions.js";
8
+ import { recordActionName, recordActionState, recordBusyKeys, recordCardKind, recordCount, recordField, recordFieldHeld, recordHeldLines, recordFieldInputs, recordFieldWide, recordItemKey, recordItems, recordLimit, recordLimitReason, RecordSources, recordSessionEnded, recordValueView, recordWrite, } from "../../Helpers/RecordCards.js";
9
+ import { api as browserApi } from "../../Services/BrowserApi.js";
10
+ import { Button } from "../Documents/Button.js";
11
+ import { ProfilePicture } from "../Forms/CrudWorkspace.js";
12
+ import { openRecordFlow, runRecordConfirm } from "../Forms/RecordFlows.js";
13
+ import { useReadOnlyNotice } from "../Inputs/ReadOnlyNotice.js";
14
+ import { toast } from "../Layouts/Toaster.js";
15
+ import { MarkdownView } from "./MarkdownView.js";
16
+ import { RichTextView } from "./RichTextView.js";
17
+ /*
18
+ * A record's page of cards: a hero across the top, then cards of fields, cards of items and the page's own cards, each
19
+ * drawn from one of the sources the page reads. Nothing on the page saves: a card's Edit, a list's Add and an item's
20
+ * icon actions open popups (RecordFlows.tsx), and their answers are written into the source at once, so every card on
21
+ * it shows the change. Loading draws the cards as they will be, held; a source that fails says so in each of its cards,
22
+ * with Try again; read-only draws no buttons; an ended session leaves the page's own words and its way back in.
23
+ */
24
+ /* eslint-disable @typescript-eslint/no-explicit-any */
25
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
26
+ const shown = (node) => node !== undefined && node !== null && node !== false && node !== "";
27
+ /** Reads a source: its own function, else a GET of its endpoint or URL, without the client's toasts. */
28
+ const readSource = (read, signal) => (typeof read === "function" ? read(signal) : browserApi.get(read, { signal, toast: false }));
29
+ /** One placeholder line while a source loads: the table's bone. */
30
+ /** How long after the page opens a link to a card is still followed again once the cards have their records. */
31
+ const FOLLOW_WITHIN_MS = 5000;
32
+ /** Tells one mounted page from another: a popup belongs to the page that opened it, never to the next one drawn. */
33
+ let pages = 0;
34
+ const Bone = ({ width = "60%" }) => _jsx("span", { className: "nx-table__bone nx-record__bone", style: { width }, "aria-hidden": "true" });
35
+ /**
36
+ * A record's page of cards, declared by params: the `sources` it reads, a `hero`, and its `cards` (fields, items, or its
37
+ * own). See `RecordCardsProps` and the interfaces it names.
38
+ *
39
+ * @example
40
+ * <RecordCards
41
+ * sources={{ profile: ACCOUNT_API.profile, emails: ACCOUNT_API.emails }}
42
+ * hero={{ title: p => p.fullName, lines: p => [`@${p.username}`] }}
43
+ * cards={[
44
+ * { id: "personal", title: "Personal details", fields: [FULL_NAME, DATE_OF_BIRTH], edit: { submit: { url: ACCOUNT_API.saveProfile } } },
45
+ * { id: "emails", source: "emails", title: "Emails", items: "items", itemKey: "id", item: e => ({ title: e.address }), add: { ... } },
46
+ * ]}
47
+ * />
48
+ */
49
+ export function RecordCards(props) {
50
+ const { hero, cards, readOnly = false, headingLevel = 2, className } = props;
51
+ const latest = useRef(props);
52
+ latest.current = props;
53
+ const popups = useRef(new Set());
54
+ const endRef = useRef(() => false);
55
+ const [store] = useState(() => new RecordSources(props.sources, async (read, signal) => {
56
+ const answer = await readSource(read, signal);
57
+ if (!answer.ok)
58
+ endRef.current(answer);
59
+ return answer;
60
+ }));
61
+ const states = useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot);
62
+ useSyncExternalStore(subscribePermissions, permissionsVersion, permissionsVersion);
63
+ const permissions = getPermissions();
64
+ const can = permissions.ready ? permissions.can.bind(permissions) : () => false;
65
+ const preferences = useDatePreferences();
66
+ const [busy, setBusy] = useState({});
67
+ // Each mounted page keys its popups apart: a page drawn afresh (another visit, a new `key`) never brings back a popup
68
+ // bound to the page before it, whose data is gone.
69
+ const [page] = useState(() => ++pages);
70
+ useEffect(() => {
71
+ store.start();
72
+ return () => store.stop();
73
+ }, [store]);
74
+ // The page's popups go with it: their answers would be written into data no card shows any more.
75
+ useEffect(() => {
76
+ const open = popups.current;
77
+ return () => {
78
+ const closing = [...open];
79
+ open.clear();
80
+ for (const popup of closing)
81
+ popup.close();
82
+ };
83
+ }, []);
84
+ // A link to a card (`/dashboard/profile#emails`) is followed while the cards are held; the cards above it may grow as
85
+ // their records come (a list longer than its held rows) and push it away. Once every source has answered, the card
86
+ // the address names is brought back into view - once, and only while the person has not moved on: a wheel, a touch,
87
+ // a key or a press since the page opened (or more than a few seconds gone) leaves the page where they are.
88
+ const followed = useRef(false);
89
+ const movedOn = useRef(false);
90
+ useEffect(() => {
91
+ const moved = () => {
92
+ movedOn.current = true;
93
+ };
94
+ const events = ["wheel", "touchstart", "keydown", "pointerdown"];
95
+ for (const name of events)
96
+ window.addEventListener(name, moved, { capture: true, passive: true });
97
+ const late = window.setTimeout(moved, FOLLOW_WITHIN_MS);
98
+ return () => {
99
+ window.clearTimeout(late);
100
+ for (const name of events)
101
+ window.removeEventListener(name, moved, { capture: true });
102
+ };
103
+ }, []);
104
+ const settled = Object.values(states).every(state => state.status !== "loading");
105
+ useEffect(() => {
106
+ if (!settled || followed.current)
107
+ return;
108
+ followed.current = true;
109
+ if (movedOn.current)
110
+ return;
111
+ let id = "";
112
+ try {
113
+ id = decodeURIComponent(window.location.hash.slice(1));
114
+ }
115
+ catch {
116
+ return;
117
+ }
118
+ if (!id)
119
+ return;
120
+ const target = document.getElementById(id);
121
+ if (!target?.closest(".nx-record"))
122
+ return;
123
+ // Already where the link put it (its top in view): left as it is.
124
+ const top = target.getBoundingClientRect().top;
125
+ if (top >= 0 && top < window.innerHeight / 2)
126
+ return;
127
+ target.scrollIntoView({ block: "start" });
128
+ }, [settled]);
129
+ /** An answer that ended the session, when the page says what to do then: the page's words, its popups closed. */
130
+ const ended = useCallback((answer) => {
131
+ const { signedOut, isSessionEnded = recordSessionEnded } = latest.current;
132
+ if (!signedOut || answer.ok || answer.aborted || !isSessionEnded(answer))
133
+ return false;
134
+ if (store.ended)
135
+ return true;
136
+ store.end(answer);
137
+ const closing = [...popups.current];
138
+ popups.current.clear();
139
+ for (const popup of closing)
140
+ popup.close();
141
+ // A closing popup hands the focus back to what opened it, which went with the cards: Sign in again takes it.
142
+ void Promise.all(closing.map(popup => popup.closed)).then(() => {
143
+ const active = document.activeElement;
144
+ if (!active || active === document.body)
145
+ document.getElementById(`${latest.current.name ?? "record"}-sign-in`)?.focus();
146
+ });
147
+ signedOut.onEnded?.(answer);
148
+ return true;
149
+ }, [store]);
150
+ endRef.current = ended;
151
+ const sourceOf = useCallback((card) => card.source ?? store.names[0] ?? "", [store]);
152
+ /** Reads a source again for a step: its data, or null - said in a toast while the cards still show the data. */
153
+ const reload = useCallback(async (source) => {
154
+ const answer = await store.read(source);
155
+ if (answer.ok)
156
+ return store.get(source).data;
157
+ if (!answer.aborted && !store.ended && store.get(source).status === "ready")
158
+ toast.error(`Not read again: ${answer.message ?? "the server did not answer."}`, "The page shows what was read before.");
159
+ return null;
160
+ }, [store]);
161
+ /** Puts the focus back in a card when what held it went away: the item's first button, the card's, or its title. */
162
+ const focusHome = useCallback((cardId, itemKey) => {
163
+ const root = document.getElementById(cardId);
164
+ if (!root)
165
+ return;
166
+ const row = itemKey !== undefined ? root.querySelector(`[data-key="${CSS.escape(itemKey)}"]`) : null;
167
+ const target = row?.querySelector("button") ?? root.querySelector(".nx-record-card__actions button, .nx-record-card__body button") ?? root.querySelector(".nx-record-card__title");
168
+ target?.focus({ preventScroll: true });
169
+ }, []);
170
+ const contextOf = useCallback((card, item) => {
171
+ const source = sourceOf(card);
172
+ const state = store.get(source);
173
+ const now = item && card.itemKey ? recordItems(state.data, card.items).find(one => recordItemKey(one, card.itemKey) === item.key) : undefined;
174
+ return {
175
+ data: state.data,
176
+ item: item ? (now ?? item.value) : undefined,
177
+ readOnly: latest.current.readOnly === true || card.readOnly === true,
178
+ reload: () => reload(source),
179
+ write: data => store.write(source, data),
180
+ run: (actionId, target) => {
181
+ const action = card.itemActions?.find(one => one.id === actionId);
182
+ if (action && card.itemKey)
183
+ runItemRef.current(card, action, target);
184
+ },
185
+ // A popup of the page's own: its values sent as they are (or merged over the item), its answer written as said.
186
+ open: flow => {
187
+ const named = flow.label ?? (typeof flow.title === "string" ? flow.title : "open");
188
+ const label = item ? (card.itemLabel?.(item.value) ?? item.key) : undefined;
189
+ openFlowRef.current(card, { id: `open-${named}`, label: named }, flow, item ? "item" : "add", item && label !== undefined ? { ...item, label } : undefined);
190
+ },
191
+ };
192
+ }, [store, sourceOf, reload]);
193
+ const hostOf = useCallback((card, item) => ({
194
+ context: () => contextOf(card, item),
195
+ subscribe: store.subscribe,
196
+ snapshot: store.snapshot,
197
+ write: async ({ writes, result, values, fallback }) => {
198
+ const source = sourceOf(card);
199
+ const context = contextOf(card, item);
200
+ const outcome = recordWrite({ writes, result, data: store.get(source).data, values, fallback, items: card.items, itemKey: card.itemKey, item: context.item });
201
+ if (outcome.kind === "write")
202
+ store.write(source, outcome.data);
203
+ else if (outcome.kind === "reload")
204
+ await reload(source);
205
+ },
206
+ ended,
207
+ track: popup => {
208
+ popups.current.add(popup);
209
+ void popup.closed.then(() => {
210
+ popups.current.delete(popup);
211
+ const active = document.activeElement;
212
+ if (!active || active === document.body)
213
+ focusHome(card.id, item?.key);
214
+ });
215
+ },
216
+ }), [store, contextOf, sourceOf, reload, ended, focusHome]);
217
+ const name = props.name ?? "record";
218
+ const openFlow = useCallback((card, action, flow, kind, item) => {
219
+ openRecordFlow({
220
+ flow,
221
+ host: hostOf(card, item),
222
+ key: `${name}:${page}:${card.id}:${action.id}${item ? `:${item.key}` : ""}`,
223
+ title: kind === "item" ? action.label : (card.title ?? action.label),
224
+ description: item?.label ?? (kind === "add" ? action.label : undefined),
225
+ kind,
226
+ inputs: kind === "edit" && card.fields ? recordFieldInputs({ fields: card.fields }) : undefined,
227
+ });
228
+ }, [hostOf, name, page]);
229
+ const openFlowRef = useRef(openFlow);
230
+ openFlowRef.current = openFlow;
231
+ /**
232
+ * A question, then the call: the item (or the card) busy meanwhile. The question hands the focus back to the button,
233
+ * which the answer may take away (a Remove takes its row): the focus then comes home, once the change is drawn.
234
+ */
235
+ const confirmThen = useCallback((card, confirm, busyKey, item) => {
236
+ void runRecordConfirm(confirm, hostOf(card, item), on => setBusy(now => ({ ...now, [card.id]: recordBusyKeys(now[card.id], busyKey, on) }))).then(() => window.requestAnimationFrame(() => {
237
+ const active = document.activeElement;
238
+ if (!active || active === document.body)
239
+ focusHome(card.id, item?.key);
240
+ }));
241
+ }, [hostOf, focusHome]);
242
+ const runCard = useCallback((card, action, kind) => {
243
+ if (action.flow)
244
+ openFlow(card, action, action.flow, kind);
245
+ else if (action.confirm)
246
+ confirmThen(card, action.confirm, "card");
247
+ else
248
+ action.onSelect?.(contextOf(card));
249
+ }, [openFlow, confirmThen, contextOf]);
250
+ const runItem = useCallback((card, action, value) => {
251
+ const key = recordItemKey(value, card.itemKey);
252
+ const label = card.itemLabel?.(value) ?? itemTitle(card, value, store.get(sourceOf(card)).data, key);
253
+ const item = { key, value, label };
254
+ const actionLabel = typeof action.label === "function" ? action.label(value, store.get(sourceOf(card)).data) : action.label;
255
+ if (action.flow)
256
+ openFlow(card, { id: action.id, label: actionLabel }, action.flow, "item", item);
257
+ else if (action.confirm)
258
+ confirmThen(card, action.confirm, key, item);
259
+ else
260
+ action.onSelect?.(contextOf(card, item));
261
+ }, [store, sourceOf, openFlow, confirmThen, contextOf]);
262
+ const runItemRef = useRef(runItem);
263
+ runItemRef.current = runItem;
264
+ const retry = useCallback((source, cardId) => {
265
+ const had = document.getElementById(cardId)?.contains(document.activeElement) ?? false;
266
+ void store.read(source).then(() => {
267
+ if (had && (!document.activeElement || document.activeElement === document.body))
268
+ focusHome(cardId);
269
+ });
270
+ }, [store, focusHome]);
271
+ const endedAnswer = store.ended;
272
+ const { signedOut } = props;
273
+ if (endedAnswer && signedOut) {
274
+ const Heading = headingLevel === 3 ? "h3" : "h2";
275
+ const message = typeof signedOut.message === "function" ? signedOut.message(endedAnswer) : (signedOut.message ?? endedAnswer.message ?? "Your session has ended. Sign in again.");
276
+ return (_jsx("div", { className: cx("nx-record", className), children: _jsxs("section", { className: "nx-record-card nx-record-card--wide", "aria-labelledby": `${name}-ended`, children: [_jsx("header", { className: "nx-record-card__head", children: _jsx("div", { className: "nx-record-card__text", children: _jsx(Heading, { id: `${name}-ended`, className: "nx-record-card__title", children: signedOut.title ?? "Your session has ended" }) }) }), _jsxs("div", { className: "nx-record-card__body", children: [_jsx("p", { role: "alert", className: "nx-record-card__message", children: message }), _jsx(Button, { id: `${name}-sign-in`, variant: "primary", autoFocus: true, onClick: signedOut.onSignIn, children: signedOut.label ?? "Sign in again" })] })] }) }));
277
+ }
278
+ const runtime = {
279
+ store,
280
+ readOnly,
281
+ level: headingLevel,
282
+ can,
283
+ preferences,
284
+ busy,
285
+ sourceOf,
286
+ loudFor: loudParts(hero ? [{ id: `${name}-hero`, source: sourceOf(hero) }, ...cards.map(card => ({ id: card.id, source: sourceOf(card) }))] : cards.map(card => ({ id: card.id, source: sourceOf(card) })), states),
287
+ retry,
288
+ runCard,
289
+ runItem,
290
+ contextOf,
291
+ };
292
+ return (_jsxs("div", { className: cx("nx-record", className), role: props["aria-label"] ? "region" : undefined, "aria-label": props["aria-label"], children: [hero && _jsx(RecordHero, { hero: hero, id: `${name}-hero`, state: states[sourceOf(hero)] ?? store.get(sourceOf(hero)), runtime: runtime }), _jsx("div", { className: "nx-record__cards", children: cards.map(card => (_jsx(RecordCardView, { card: card, state: states[sourceOf(card)] ?? store.get(sourceOf(card)), runtime: runtime }, card.id))) })] }));
293
+ }
294
+ /** The first part (in the page's order) that shows each failed source: it alone says the failure aloud. */
295
+ function loudParts(parts, states) {
296
+ const loud = new Map();
297
+ for (const part of parts)
298
+ if (states[part.source]?.status === "failed" && !loud.has(part.source))
299
+ loud.set(part.source, part.id);
300
+ return loud;
301
+ }
302
+ /** An item's name for a screen reader and a popup's line: its title when that is text, else its key. */
303
+ function itemTitle(card, item, data, key) {
304
+ const title = card.item(item, data).title;
305
+ return typeof title === "string" || typeof title === "number" ? String(title) : key;
306
+ }
307
+ /** The failure of a source, in a card: the server's words and Try again. The first part to show it says it aloud. */
308
+ function Failed({ state, source, cardId, runtime }) {
309
+ const loud = runtime.loudFor.get(source) === cardId;
310
+ return (_jsxs("div", { className: "nx-form-load-failed nx-record__failed", role: loud ? "alert" : undefined, children: [_jsx(InlineIcon, { icon: iconAlertCircle }), _jsxs("p", { children: [_jsx("strong", { children: "This could not be read." }), " ", state.answer.message ?? "The server did not answer."] }), _jsx("div", { className: "nx-form-load-failed__actions", children: _jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconRefresh }), onClick: () => runtime.retry(source, cardId), children: "Try again" }) })] }));
311
+ }
312
+ /** The marks after a name: the table's tags, each with its tone. */
313
+ function Marks({ marks, className }) {
314
+ if (!marks?.length)
315
+ return null;
316
+ return (_jsx("span", { className: className, children: marks.map((mark, index) => (_jsx("span", { className: "nx-table__tag", "data-tone": mark.tone, children: mark.label }, index))) }));
317
+ }
318
+ /**
319
+ * The buttons of a card's heading, the hero's, or an item's right side, with the note a held one says when pressed (the
320
+ * read-only fields' and the table's held Add's): one note for the whole part, saying the pressed button's reason.
321
+ */
322
+ function useHeldNote(rootRef) {
323
+ const [reason, setReason] = useState("");
324
+ const notice = useReadOnlyNotice(true, rootRef, reason, { anchor: target => target.closest(".nx-btn"), typing: false, lock: "label" });
325
+ return {
326
+ note: notice.notice,
327
+ onKeyDown: (event) => notice.onKeyDown(event),
328
+ say: (target, why) => {
329
+ setReason(why);
330
+ notice.show(target);
331
+ },
332
+ };
333
+ }
334
+ /** The text a card's title says, for a button named after it: its text, or the heading's own words once drawn. */
335
+ const titleText = (title) => (typeof title === "string" || typeof title === "number" ? String(title).trim() || null : null);
336
+ /**
337
+ * A heading's buttons: Edit or Add first, then the card's own; held ones say why when pressed. Each is named with its
338
+ * card to a screen reader ("Edit: Personal details"), as an item's icons are with their item: three Edits on a page are
339
+ * three different buttons.
340
+ */
341
+ function HeaderActions({ actions, card, runtime, say, busy }) {
342
+ const visible = actions.filter(entry => entry.state.kind !== "hidden");
343
+ if (!visible.length)
344
+ return null;
345
+ const of = titleText(card.title);
346
+ return (_jsx("div", { className: "nx-record-card__actions", children: visible.map(({ action, kind, state }) => {
347
+ const held = state.kind === "held" ? state.reason : null;
348
+ return (_jsx(Button, { size: "sm", icon: action.icon, hoverTone: action.tone === "danger" ? "danger" : undefined, "data-action": action.id, "data-held": held !== null || undefined, "aria-label": of ? recordActionName(action.label, of) : undefined, "aria-disabled": held !== null || busy || undefined, title: held ?? undefined, onClick: event => {
349
+ if (held !== null)
350
+ return say(event.currentTarget, held);
351
+ if (!busy)
352
+ runtime.runCard(card, action, kind);
353
+ }, children: action.label }, action.id));
354
+ }) }));
355
+ }
356
+ /** The card across the top: the picture that opens the pictures window, the name, its lines and marks. */
357
+ function RecordHero({ hero, id, state, runtime }) {
358
+ const rootRef = useRef(null);
359
+ const held = useHeldNote(rootRef);
360
+ const data = state.data ?? {};
361
+ const loading = state.status === "loading";
362
+ const source = runtime.sourceOf(hero);
363
+ const Heading = runtime.level === 3 ? "h3" : "h2";
364
+ const title = hero.title(data);
365
+ const picture = hero.picture?.(data);
366
+ const lines = hero.lines?.(data) ?? [];
367
+ const card = { id, source: hero.source, title };
368
+ const actions = (hero.actions ?? []).map(action => ({
369
+ action,
370
+ kind: "edit",
371
+ // While the record is read its rules are not asked: each is held (or not drawn yet, `loading: "hidden"`).
372
+ state: state.status === "failed" ? { kind: "hidden" } : recordActionState(action, state.status === "ready" ? [data] : null, { readOnly: runtime.readOnly, can: runtime.can }),
373
+ }));
374
+ return (_jsxs("section", { ref: rootRef, id: id, className: "nx-record-hero", "aria-labelledby": `${id}-title`, "aria-busy": loading || undefined, onKeyDown: held.onKeyDown, children: [shown(picture) && (_jsx("div", { className: "nx-record-hero__picture", children: _jsx(ProfilePicture, { picture: picture, title: title, size: "var(--nx-record-hero-picture)", readOnly: runtime.readOnly }) })), _jsxs("div", { className: "nx-record-hero__text", children: [_jsx(Heading, { id: `${id}-title`, className: "nx-record-hero__title", children: shown(title) ? title : _jsx(Bone, { width: "12rem" }) }), lines.map((line, index) => shown(line) ? (_jsx("p", { className: "nx-record-hero__line", children: line }, index)) : loading ? (_jsx("p", { className: "nx-record-hero__line", "aria-hidden": "true", children: "\u00A0" }, index)) : null), state.status === "failed" ? (_jsx(Failed, { state: state, source: source, cardId: id, runtime: runtime })) : loading && hero.marks && !hero.marks(data).length ? (_jsx("span", { className: "nx-record-hero__marks", "aria-hidden": "true", children: _jsx("span", { className: "nx-table__tag nx-table__tag--plain nx-record__bone-tag", children: "\u00A0" }) })) : (_jsx(Marks, { marks: hero.marks?.(data), className: "nx-record-hero__marks" }))] }), _jsx(HeaderActions, { actions: actions, card: card, runtime: runtime, say: held.say, busy: false }), held.note] }));
375
+ }
376
+ /** One card: its heading (icon, title, count, sentence, buttons) and its body (fields, items, or its own). */
377
+ function RecordCardView({ card, state, runtime }) {
378
+ const rootRef = useRef(null);
379
+ const held = useHeldNote(rootRef);
380
+ const kind = recordCardKind(card);
381
+ const source = runtime.sourceOf(card);
382
+ const data = state.data;
383
+ const ready = state.status === "ready";
384
+ if (ready && card.hidden?.(data))
385
+ return null;
386
+ const readOnly = runtime.readOnly || card.readOnly === true;
387
+ const busy = runtime.busy[card.id] ?? [];
388
+ const Heading = runtime.level === 3 ? "h3" : "h2";
389
+ const titleId = `${card.id}-title`;
390
+ const description = typeof card.description === "function" ? (ready ? card.description(data) : null) : card.description;
391
+ // The heading's buttons: Edit or Add first, then the card's own. Held while the source loads; none once it failed.
392
+ const items = kind === "items" ? card : null;
393
+ const list = items && ready ? recordItems(data, items.items) : [];
394
+ const limit = items && ready ? recordLimit(data, items.limit) : null;
395
+ // The rules typed on the data are asked once it is read; until then each button is held, saying so.
396
+ const stateOf = (action, extra) => {
397
+ if (state.status === "failed")
398
+ return { kind: "hidden" };
399
+ const own = recordActionState(action, ready ? [data] : null, { readOnly, can: runtime.can });
400
+ if (own.kind !== "ready")
401
+ return own;
402
+ return extra ? { kind: "held", reason: extra } : own;
403
+ };
404
+ const header = [];
405
+ if (kind === "fields") {
406
+ const edit = card.edit;
407
+ if (edit) {
408
+ const action = { id: "edit", label: edit.label ?? "Edit", icon: edit.icon ?? _jsx(InlineIcon, { icon: iconPencil }), flow: edit };
409
+ header.push({ action, kind: "edit", state: stateOf(action) });
410
+ }
411
+ }
412
+ if (items?.add) {
413
+ const add = items.add;
414
+ const action = { id: "add", label: add.label ?? "Add", icon: add.icon ?? _jsx(InlineIcon, { icon: iconPlus }), flow: add };
415
+ const full = limit !== null && list.length >= limit ? recordLimitReason(limit, items.limitReason) : null;
416
+ header.push({ action, kind: "add", state: stateOf(action, full) });
417
+ }
418
+ for (const action of card.actions ?? [])
419
+ header.push({ action, kind: "edit", state: stateOf(action) });
420
+ let body;
421
+ if (state.status === "failed")
422
+ body = _jsx(Failed, { state: state, source: source, cardId: card.id, runtime: runtime });
423
+ else if (kind === "fields")
424
+ body = _jsx(FieldsBody, { card: card, data: data, loading: !ready, preferences: runtime.preferences });
425
+ else if (kind === "items")
426
+ body = _jsx(ItemsBody, { card: items, data: data, loading: !ready, runtime: runtime, readOnly: readOnly, busy: busy, say: held.say });
427
+ else if (!ready)
428
+ body = _jsx(HeldLines, {});
429
+ else
430
+ body = card.render(runtime.contextOf(card));
431
+ return (_jsxs("section", { ref: rootRef, id: card.id, className: cx("nx-record-card", `nx-record-card--${kind}`, card.wide && "nx-record-card--wide"), "aria-labelledby": titleId, "aria-busy": state.status === "loading" || busy.length > 0 || undefined, "data-state": state.status, onKeyDown: held.onKeyDown, children: [_jsxs("header", { className: "nx-record-card__head", children: [card.icon && (_jsx("span", { className: "nx-record-card__icon", "aria-hidden": "true", children: card.icon })), _jsxs("div", { className: "nx-record-card__main", children: [_jsxs("div", { className: "nx-record-card__text", children: [_jsxs(Heading, { id: titleId, className: "nx-record-card__title", tabIndex: -1, children: [card.title, items && limit !== null && _jsx("span", { className: "nx-record-card__count", children: recordCount(list.length, limit) })] }), shown(description) && _jsx("p", { className: "nx-record-card__lead", children: description })] }), _jsx(HeaderActions, { actions: header, card: card, runtime: runtime, say: held.say, busy: busy.length > 0 })] })] }), _jsx("div", { className: "nx-record-card__body", children: body }), held.note] }));
432
+ }
433
+ /** Two held lines of text, for a card of the page's own while its source loads: each a line box, as a paragraph's are. */
434
+ const HeldLines = () => (_jsxs("div", { className: "nx-record__held", "aria-hidden": "true", children: [_jsx("span", { className: "nx-record__held-line", children: _jsx(Bone, { width: "80%" }) }), _jsx("span", { className: "nx-record__held-line", children: _jsx(Bone, { width: "55%" }) })] }));
435
+ /** A fields card's body: label over value, from the inputs its Edit uses. */
436
+ function FieldsBody({ card, data, loading, preferences }) {
437
+ const fields = card.fields.map(recordField);
438
+ return (_jsx("dl", { className: "nx-record-fields", children: fields.map(field => {
439
+ if (!loading && field.hidden?.(data))
440
+ return null;
441
+ const label = field.label ?? field.input.label ?? field.input.name;
442
+ return (_jsxs("div", { className: cx("nx-record-field", recordFieldWide(field) && "nx-record-field--wide"), children: [_jsx("dt", { className: "nx-record-field__label", children: label }), _jsx("dd", { className: "nx-record-field__value", children: loading ? _jsx(HeldValue, { field: field }) : _jsx(FieldValue, { field: field, data: data, empty: card.empty ?? "Not said", preferences: preferences }) })] }, field.input.name));
443
+ }) }));
444
+ }
445
+ /** A value held while its source loads, as tall as it will be: two lines for long text, a chip's height for chips. */
446
+ function HeldValue({ field }) {
447
+ const held = recordFieldHeld(field);
448
+ // Each bone on a line of its own, as the text will be (`heldLines`; long text 2); a chip as the chips will be.
449
+ if (held === "lines" || field.heldLines !== undefined) {
450
+ const lines = recordHeldLines(field);
451
+ return (_jsx(_Fragment, { children: Array.from({ length: lines }, (_, index) => (_jsx("span", { className: "nx-record__held-line", children: _jsx(Bone, { width: index === lines - 1 && lines > 1 ? "55%" : "85%" }) }, index))) }));
452
+ }
453
+ if (held === "chips")
454
+ return (_jsx("ul", { className: "nx-record-chips", "aria-hidden": "true", children: _jsx("li", { className: "nx-table__tag nx-table__tag--plain nx-record__bone-tag", children: "\u00A0" }) }));
455
+ return _jsx(Bone, { width: "60%" });
456
+ }
457
+ /** One value as its input draws it: a choice by its label, a date in the person's format, chips, lines, Markdown. */
458
+ function FieldValue({ field, data, empty, preferences }) {
459
+ const value = isRecord(data) ? data[field.input.name] : undefined;
460
+ const none = _jsx("span", { className: "nx-record-field__empty", children: empty });
461
+ if (field.show) {
462
+ const own = field.show(value, data);
463
+ return shown(own) ? _jsx(_Fragment, { children: own }) : none;
464
+ }
465
+ const view = recordValueView(field.input, value, { look: field.look, preferences });
466
+ switch (view.kind) {
467
+ case "empty":
468
+ return none;
469
+ case "text":
470
+ return _jsx(_Fragment, { children: view.text });
471
+ case "lines":
472
+ return _jsx("span", { className: "nx-record-field__lines", children: view.text });
473
+ case "chips":
474
+ return (_jsx("ul", { className: "nx-record-chips", children: view.items.map((item, index) => (_jsx("li", { className: "nx-table__tag nx-table__tag--plain", children: item }, `${index}:${item}`))) }));
475
+ case "markdown":
476
+ return _jsx(MarkdownView, { value: view.text });
477
+ case "rich":
478
+ return _jsx(RichTextView, { value: view.html });
479
+ }
480
+ }
481
+ /** An items card's body: the rows, each with its picture or icon, its name and marks, lines, and icon actions. */
482
+ function ItemsBody({ card, data, loading, runtime, readOnly, busy, say, }) {
483
+ if (loading) {
484
+ return (_jsx("ul", { className: "nx-record-items", "aria-hidden": "true", children: Array.from({ length: card.heldRows ?? 2 }, (_, index) => (_jsxs("li", { className: "nx-record-item", children: [_jsx("span", { className: "nx-record-item__media nx-record-item__media--held" }), _jsxs("span", { className: "nx-record-item__text", children: [_jsx("span", { className: "nx-record-item__title nx-record__held-line", children: _jsx(Bone, { width: "45%" }) }), Array.from({ length: recordHeldLines({ heldLines: card.heldRowLines ?? 1 }) }, (_, line) => (_jsx("span", { className: "nx-record-item__line nx-record__held-line", children: _jsx(Bone, { width: line ? "40%" : "30%" }) }, line)))] })] }, index))) }));
485
+ }
486
+ const items = recordItems(data, card.items);
487
+ if (!items.length)
488
+ return _jsx("p", { className: "nx-record-card__empty", children: card.empty ?? "Nothing here yet." });
489
+ return (_jsx("ul", { className: "nx-record-items", children: items.map(item => {
490
+ const key = recordItemKey(item, card.itemKey);
491
+ const view = card.item(item, data);
492
+ const label = card.itemLabel?.(item) ?? itemTitle(card, item, data, key);
493
+ const working = busy.includes(key) || busy.includes("card");
494
+ const actions = (card.itemActions ?? [])
495
+ .map(action => ({ action, state: recordActionState(action, [item, data], { readOnly, can: runtime.can }) }))
496
+ .filter(entry => entry.state.kind !== "hidden");
497
+ const media = view.picture ?? view.icon;
498
+ return (_jsxs("li", { className: "nx-record-item", "data-key": key, "aria-busy": working || undefined, children: [shown(media) && _jsx("span", { className: cx("nx-record-item__media", !view.picture && "nx-record-item__media--icon"), children: media }), _jsxs("div", { className: "nx-record-item__text", children: [_jsxs("p", { className: "nx-record-item__title", children: [_jsx("span", { className: "nx-record-item__name", children: view.title }), _jsx(Marks, { marks: view.marks, className: "nx-record-item__marks" })] }), (view.lines ?? []).filter(shown).map((line, index) => (_jsx("p", { className: "nx-record-item__line", children: line }, index)))] }), actions.length > 0 && (_jsx("div", { className: "nx-record-item__actions", children: actions.map(({ action, state }) => {
499
+ const name = typeof action.label === "function" ? action.label(item, data) : action.label;
500
+ const held = state.kind === "held" ? state.reason : null;
501
+ return (_jsx(Button, { variant: "ghost", size: "sm", icon: action.icon, hoverTone: action.tone === "danger" ? "danger" : undefined, "aria-label": recordActionName(name, label), title: held ? `${name}. ${held}` : name, "data-action": action.id, "data-held": held !== null || undefined, "aria-disabled": held !== null || working || undefined, onClick: event => {
502
+ if (held !== null)
503
+ return say(event.currentTarget, held);
504
+ if (!working)
505
+ runtime.runItem(card, action, item);
506
+ } }, action.id));
507
+ }) }))] }, key));
508
+ }) }));
509
+ }
@@ -1,5 +1,5 @@
1
1
  import { NexusModule, type ApiEndpoint, type ApiModuleInfo, type ApiModuleValue } from "../Interfaces/ApiInterfaces.ts";
2
- import { apiEndpoint, type ApiEndpointExtras, type ApiUrlParts } from "./ApiRoutes.ts";
2
+ import { apiEndpoint, type ApiControllerOptions, type ApiEndpointExtras, type ApiUrlParts } from "./ApiRoutes.ts";
3
3
  /** Where the proxy route sits in an app: `app/api/app/[module]/[...path]/route.ts`. */
4
4
  export declare const DEFAULT_PROXY_PATH = "/api/app";
5
5
  /** An environment variable, read when it is needed (never inlined at build time), so one build serves every environment. */
@@ -67,39 +67,8 @@ export declare abstract class ApiModules<TModule extends ApiModuleValue = ApiMod
67
67
  protected readEnv(name: string | undefined): string | undefined;
68
68
  /** Endpoints typed to this app's modules: `MODULES.endpoint.get(HrModule.Staff, "Employee/Birthdays")`. */
69
69
  readonly endpoint: Record<keyof typeof apiEndpoint, (module: TModule, path: string, extras?: ApiEndpointExtras) => ApiEndpoint<TModule>>;
70
- /** A controller's standard endpoints, typed to this app's modules. See `apiController`. */
71
- controller(module: TModule, controller: string, subject?: string): {
72
- module: TModule;
73
- controller: string;
74
- listing: ApiEndpoint<TModule>;
75
- activeListing: ApiEndpoint<TModule>;
76
- findListing: ApiEndpoint<TModule>;
77
- pagination: ApiEndpoint<TModule>;
78
- lists: Record<import("../Shared.Index.ts").CrudMode, ApiEndpoint<TModule>>;
79
- pages: Record<import("../Shared.Index.ts").CrudMode, ApiEndpoint<TModule>>;
80
- pins: ApiEndpoint<TModule>;
81
- details: ApiEndpoint<TModule>;
82
- create: ApiEndpoint<TModule>;
83
- update: ApiEndpoint<TModule>;
84
- delete: ApiEndpoint<TModule>;
85
- trash: ApiEndpoint<TModule>;
86
- trashFromDelete: ApiEndpoint<TModule>;
87
- recover: ApiEndpoint<TModule>;
88
- flag: ApiEndpoint<TModule>;
89
- pin: ApiEndpoint<TModule>;
90
- audit: ApiEndpoint<TModule>;
91
- accessActions: ApiEndpoint<TModule>;
92
- mass: {
93
- description: ApiEndpoint<TModule>;
94
- trash: ApiEndpoint<TModule>;
95
- trashFromDelete: ApiEndpoint<TModule>;
96
- flag: ApiEndpoint<TModule>;
97
- pin: ApiEndpoint<TModule>;
98
- delete: ApiEndpoint<TModule>;
99
- recover: ApiEndpoint<TModule>;
100
- };
101
- action: (method: import("../Shared.Index.ts").HttpMethod, action: string, extras?: Partial<Omit<ApiEndpoint<number>, "path" | "method" | "module">> | undefined) => ApiEndpoint<TModule>;
102
- };
70
+ /** A controller's standard endpoints, typed to this app's modules: its generation and what it lacks in `options`. See `apiController`. */
71
+ controller(module: TModule, controller: string, subject?: string, options?: ApiControllerOptions): import("./ApiRoutes.ts").ApiControllerEndpoints<TModule>;
103
72
  }
104
73
  /** The Nexus modules, their names in proxy URLs, and the environment variables with their roots (as in the previous project). */
105
74
  export declare const NEXUS_MODULES: readonly ApiModuleInfo<NexusModule>[];
@@ -103,9 +103,9 @@ export class ApiModules {
103
103
  }
104
104
  /** Endpoints typed to this app's modules: `MODULES.endpoint.get(HrModule.Staff, "Employee/Birthdays")`. */
105
105
  endpoint = apiEndpoint;
106
- /** A controller's standard endpoints, typed to this app's modules. See `apiController`. */
107
- controller(module, controller, subject) {
108
- return apiController(module, controller, subject);
106
+ /** A controller's standard endpoints, typed to this app's modules: its generation and what it lacks in `options`. See `apiController`. */
107
+ controller(module, controller, subject, options) {
108
+ return apiController(module, controller, subject, options);
109
109
  }
110
110
  }
111
111
  /** The Nexus modules, their names in proxy URLs, and the environment variables with their roots (as in the previous project). */