nexus-shared 1.1.20 → 2.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 (332) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +903 -0
  3. package/dist/Client.Index.d.ts +44 -0
  4. package/dist/Client.Index.js +49 -0
  5. package/dist/Components/Documents/Button.d.ts +29 -0
  6. package/dist/Components/Documents/Button.js +11 -0
  7. package/dist/Components/Documents/Menu.d.ts +102 -0
  8. package/dist/Components/Documents/Menu.js +238 -0
  9. package/dist/Components/Documents/SplitButton.d.ts +64 -0
  10. package/dist/Components/Documents/SplitButton.js +127 -0
  11. package/dist/Components/Documents/TabButtons.d.ts +62 -0
  12. package/dist/Components/Documents/TabButtons.js +19 -0
  13. package/dist/Components/Forms/ApiForm.d.ts +21 -0
  14. package/dist/Components/Forms/ApiForm.js +99 -0
  15. package/dist/Components/Forms/Crud.d.ts +17 -0
  16. package/dist/Components/Forms/Crud.js +556 -0
  17. package/dist/Components/Forms/CrudParts.d.ts +15 -0
  18. package/dist/Components/Forms/CrudParts.js +36 -0
  19. package/dist/Components/Forms/Form.d.ts +52 -0
  20. package/dist/Components/Forms/Form.js +319 -0
  21. package/dist/Components/Forms/SubmitForm.d.ts +65 -0
  22. package/dist/Components/Forms/SubmitForm.js +198 -0
  23. package/dist/Components/Inputs/Calendar.d.ts +69 -0
  24. package/dist/Components/Inputs/Calendar.js +258 -0
  25. package/dist/Components/Inputs/CheckBox.d.ts +53 -0
  26. package/dist/Components/Inputs/CheckBox.js +87 -0
  27. package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
  28. package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
  29. package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
  30. package/dist/Components/Inputs/ChoiceField.js +169 -0
  31. package/dist/Components/Inputs/DatePicker.d.ts +54 -0
  32. package/dist/Components/Inputs/DatePicker.js +246 -0
  33. package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
  34. package/dist/Components/Inputs/DateRangePicker.js +283 -0
  35. package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
  36. package/dist/Components/Inputs/DateTimePicker.js +399 -0
  37. package/dist/Components/Inputs/Dropdown.d.ts +117 -0
  38. package/dist/Components/Inputs/Dropdown.js +839 -0
  39. package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
  40. package/dist/Components/Inputs/DropdownParts.js +102 -0
  41. package/dist/Components/Inputs/EditorShell.d.ts +45 -0
  42. package/dist/Components/Inputs/EditorShell.js +214 -0
  43. package/dist/Components/Inputs/Field.d.ts +53 -0
  44. package/dist/Components/Inputs/Field.js +16 -0
  45. package/dist/Components/Inputs/GroupForm.d.ts +20 -0
  46. package/dist/Components/Inputs/GroupForm.js +170 -0
  47. package/dist/Components/Inputs/InputField.d.ts +59 -0
  48. package/dist/Components/Inputs/InputField.js +169 -0
  49. package/dist/Components/Inputs/InputRenderer.d.ts +35 -0
  50. package/dist/Components/Inputs/InputRenderer.js +99 -0
  51. package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
  52. package/dist/Components/Inputs/MarkdownEditor.js +289 -0
  53. package/dist/Components/Inputs/NumberBox.d.ts +53 -0
  54. package/dist/Components/Inputs/NumberBox.js +228 -0
  55. package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
  56. package/dist/Components/Inputs/RadioGroup.js +43 -0
  57. package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
  58. package/dist/Components/Inputs/ReadOnlyNotice.js +116 -0
  59. package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
  60. package/dist/Components/Inputs/RichTextEditor.js +1170 -0
  61. package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
  62. package/dist/Components/Inputs/RichTextParts.js +157 -0
  63. package/dist/Components/Inputs/RowsInput.d.ts +104 -0
  64. package/dist/Components/Inputs/RowsInput.js +259 -0
  65. package/dist/Components/Inputs/Slider.d.ts +87 -0
  66. package/dist/Components/Inputs/Slider.js +216 -0
  67. package/dist/Components/Inputs/TabularForm.d.ts +23 -0
  68. package/dist/Components/Inputs/TabularForm.js +283 -0
  69. package/dist/Components/Inputs/TextArea.d.ts +43 -0
  70. package/dist/Components/Inputs/TextArea.js +31 -0
  71. package/dist/Components/Inputs/TextBox.d.ts +52 -0
  72. package/dist/Components/Inputs/TextBox.js +58 -0
  73. package/dist/Components/Inputs/TextField.d.ts +31 -0
  74. package/dist/Components/Inputs/TextField.js +35 -0
  75. package/dist/Components/Inputs/TimePanel.d.ts +34 -0
  76. package/dist/Components/Inputs/TimePanel.js +288 -0
  77. package/dist/Components/Inputs/TimePicker.d.ts +54 -0
  78. package/dist/Components/Inputs/TimePicker.js +239 -0
  79. package/dist/Components/Inputs/TreeView.d.ts +160 -0
  80. package/dist/Components/Inputs/TreeView.js +947 -0
  81. package/dist/Components/Layouts/MessageBox.d.ts +71 -0
  82. package/dist/Components/Layouts/MessageBox.js +104 -0
  83. package/dist/Components/Layouts/Popup.d.ts +92 -0
  84. package/dist/Components/Layouts/Popup.js +376 -0
  85. package/dist/Components/Layouts/PopupContext.d.ts +2 -0
  86. package/dist/Components/Layouts/PopupContext.js +4 -0
  87. package/dist/Components/Layouts/PopupDock.d.ts +22 -0
  88. package/dist/Components/Layouts/PopupDock.js +121 -0
  89. package/dist/Components/Layouts/PopupHost.d.ts +56 -0
  90. package/dist/Components/Layouts/PopupHost.js +163 -0
  91. package/dist/Components/Layouts/PopupRules.d.ts +30 -0
  92. package/dist/Components/Layouts/PopupRules.js +63 -0
  93. package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
  94. package/dist/Components/Layouts/ThemePicker.js +22 -0
  95. package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
  96. package/dist/Components/Layouts/ThemeScript.js +13 -0
  97. package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
  98. package/dist/Components/Layouts/ThemeSwitcher.js +29 -0
  99. package/dist/Components/Layouts/Toaster.d.ts +76 -0
  100. package/dist/Components/Layouts/Toaster.js +172 -0
  101. package/dist/Components/Viewers/DataTable.d.ts +25 -0
  102. package/dist/Components/Viewers/DataTable.js +542 -0
  103. package/dist/Components/Viewers/DataTableParts.d.ts +134 -0
  104. package/dist/Components/Viewers/DataTableParts.js +145 -0
  105. package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
  106. package/dist/Components/Viewers/MarkdownView.js +79 -0
  107. package/dist/Components/Viewers/RichTextView.d.ts +18 -0
  108. package/dist/Components/Viewers/RichTextView.js +17 -0
  109. package/dist/Helpers/AnimationHelpers.d.ts +6 -0
  110. package/dist/Helpers/AnimationHelpers.js +35 -0
  111. package/dist/Helpers/ApiClient.d.ts +74 -0
  112. package/dist/Helpers/ApiClient.js +197 -0
  113. package/dist/Helpers/ApiFormHelpers.d.ts +26 -0
  114. package/dist/Helpers/ApiFormHelpers.js +56 -0
  115. package/dist/Helpers/ApiModules.d.ts +135 -0
  116. package/dist/Helpers/ApiModules.js +194 -0
  117. package/dist/Helpers/ApiResponses.d.ts +41 -0
  118. package/dist/Helpers/ApiResponses.js +159 -0
  119. package/dist/Helpers/ApiRoutes.d.ts +150 -0
  120. package/dist/Helpers/ApiRoutes.js +191 -0
  121. package/dist/Helpers/ApiToasts.d.ts +21 -0
  122. package/dist/Helpers/ApiToasts.js +22 -0
  123. package/dist/Helpers/ClassHelpers.d.ts +3 -0
  124. package/dist/Helpers/ClassHelpers.js +4 -0
  125. package/dist/Helpers/CrudBackend.d.ts +56 -0
  126. package/dist/Helpers/CrudBackend.js +119 -0
  127. package/dist/Helpers/CrudHelpers.d.ts +76 -0
  128. package/dist/Helpers/CrudHelpers.js +202 -0
  129. package/dist/Helpers/DateHelpers.d.ts +88 -0
  130. package/dist/Helpers/DateHelpers.js +363 -0
  131. package/dist/Helpers/DatePreferences.d.ts +14 -0
  132. package/dist/Helpers/DatePreferences.js +94 -0
  133. package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
  134. package/dist/Helpers/DateTimeHelpers.js +286 -0
  135. package/dist/Helpers/DragHelpers.d.ts +129 -0
  136. package/dist/Helpers/DragHelpers.js +281 -0
  137. package/dist/Helpers/DropdownHelpers.d.ts +56 -0
  138. package/dist/Helpers/DropdownHelpers.js +180 -0
  139. package/dist/Helpers/FormDrafts.d.ts +18 -0
  140. package/dist/Helpers/FormDrafts.js +76 -0
  141. package/dist/Helpers/FormStore.d.ts +231 -0
  142. package/dist/Helpers/FormStore.js +734 -0
  143. package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
  144. package/dist/Helpers/InputParamsHelpers.js +388 -0
  145. package/dist/Helpers/MarkdownEditing.d.ts +27 -0
  146. package/dist/Helpers/MarkdownEditing.js +250 -0
  147. package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
  148. package/dist/Helpers/MarkdownHelpers.js +775 -0
  149. package/dist/Helpers/MessageBuilder.d.ts +74 -0
  150. package/dist/Helpers/MessageBuilder.js +254 -0
  151. package/dist/Helpers/NepaliCalendar.d.ts +25 -0
  152. package/dist/Helpers/NepaliCalendar.js +107 -0
  153. package/dist/Helpers/NumberHelpers.d.ts +71 -0
  154. package/dist/Helpers/NumberHelpers.js +202 -0
  155. package/dist/Helpers/Permissions.d.ts +62 -0
  156. package/dist/Helpers/Permissions.js +133 -0
  157. package/dist/Helpers/PopoverHelpers.d.ts +43 -0
  158. package/dist/Helpers/PopoverHelpers.js +91 -0
  159. package/dist/Helpers/RichTextHelpers.d.ts +77 -0
  160. package/dist/Helpers/RichTextHelpers.js +1072 -0
  161. package/dist/Helpers/RowsStore.d.ts +160 -0
  162. package/dist/Helpers/RowsStore.js +584 -0
  163. package/dist/Helpers/TableColumns.d.ts +40 -0
  164. package/dist/Helpers/TableColumns.js +254 -0
  165. package/dist/Helpers/TableExport.d.ts +30 -0
  166. package/dist/Helpers/TableExport.js +97 -0
  167. package/dist/Helpers/TableHelpers.d.ts +102 -0
  168. package/dist/Helpers/TableHelpers.js +281 -0
  169. package/dist/Helpers/TableStore.d.ts +142 -0
  170. package/dist/Helpers/TableStore.js +530 -0
  171. package/dist/Helpers/ThemeHelpers.d.ts +15 -0
  172. package/dist/Helpers/ThemeHelpers.js +16 -0
  173. package/dist/Helpers/TimeHelpers.d.ts +73 -0
  174. package/dist/Helpers/TimeHelpers.js +253 -0
  175. package/dist/Helpers/TreeStore.d.ts +188 -0
  176. package/dist/Helpers/TreeStore.js +577 -0
  177. package/dist/Helpers/ValidationHelpers.d.ts +124 -0
  178. package/dist/Helpers/ValidationHelpers.js +301 -0
  179. package/dist/Interfaces/ApiInterfaces.d.ts +149 -0
  180. package/dist/Interfaces/ApiInterfaces.js +24 -0
  181. package/dist/Interfaces/CrudInterfaces.d.ts +171 -0
  182. package/{src/sso-client.tsx → dist/Interfaces/CrudInterfaces.js} +1 -1
  183. package/dist/Interfaces/DateInterfaces.d.ts +154 -0
  184. package/dist/Interfaces/DateInterfaces.js +29 -0
  185. package/dist/Interfaces/FormInterfaces.d.ts +352 -0
  186. package/dist/Interfaces/FormInterfaces.js +1 -0
  187. package/dist/Interfaces/InputInterfaces.d.ts +141 -0
  188. package/dist/Interfaces/InputInterfaces.js +1 -0
  189. package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
  190. package/dist/Interfaces/MessageInterfaces.js +5 -0
  191. package/dist/Interfaces/TableInterfaces.d.ts +291 -0
  192. package/dist/Interfaces/TableInterfaces.js +1 -0
  193. package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
  194. package/dist/Interfaces/ThemeInterfaces.js +17 -0
  195. package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
  196. package/dist/Interfaces/TypeInterfaces.js +1 -0
  197. package/dist/Server.Index.d.ts +2 -0
  198. package/dist/Server.Index.js +5 -0
  199. package/dist/Services/ApiProxy.d.ts +32 -0
  200. package/dist/Services/ApiProxy.js +111 -0
  201. package/dist/Services/BrowserApi.d.ts +78 -0
  202. package/dist/Services/BrowserApi.js +111 -0
  203. package/dist/Services/ServerApi.d.ts +49 -0
  204. package/dist/Services/ServerApi.js +75 -0
  205. package/dist/Services/ThemeService.d.ts +17 -0
  206. package/dist/Services/ThemeService.js +83 -0
  207. package/dist/Shared.Index.d.ts +43 -0
  208. package/dist/Shared.Index.js +47 -0
  209. package/package.json +82 -33
  210. package/src/Styles/Nexus.Base.css +70 -0
  211. package/src/Styles/Nexus.Button.css +151 -0
  212. package/src/Styles/Nexus.Calendar.css +275 -0
  213. package/src/Styles/Nexus.Choice.css +345 -0
  214. package/src/Styles/Nexus.Crud.css +84 -0
  215. package/src/Styles/Nexus.DateTime.css +81 -0
  216. package/src/Styles/Nexus.Dropdown.css +774 -0
  217. package/src/Styles/Nexus.Editor.css +297 -0
  218. package/src/Styles/Nexus.Form.css +298 -0
  219. package/src/Styles/Nexus.Index.css +34 -0
  220. package/src/Styles/Nexus.Input.css +637 -0
  221. package/src/Styles/Nexus.Markdown.css +265 -0
  222. package/src/Styles/Nexus.Menu.css +249 -0
  223. package/src/Styles/Nexus.Popup.css +731 -0
  224. package/src/Styles/Nexus.RichText.css +463 -0
  225. package/src/Styles/Nexus.Rows.css +566 -0
  226. package/src/Styles/Nexus.Slider.css +278 -0
  227. package/src/Styles/Nexus.Tab.Buttons.css +364 -0
  228. package/src/Styles/Nexus.Table.css +1014 -0
  229. package/src/Styles/Nexus.Theme.Picker.css +159 -0
  230. package/src/Styles/Nexus.Themes.css +194 -0
  231. package/src/Styles/Nexus.Time.css +147 -0
  232. package/src/Styles/Nexus.Toast.css +265 -0
  233. package/src/Styles/Nexus.Tokens.css +72 -0
  234. package/src/Styles/Nexus.Tree.css +435 -0
  235. package/src/api-services/authentication-service.tsx +0 -23
  236. package/src/api-services/preference-service.tsx +0 -5
  237. package/src/api-services/system-service.tsx +0 -34
  238. package/src/client.ts +0 -31
  239. package/src/components/documents/button.tsx +0 -142
  240. package/src/components/documents/icon-box.tsx +0 -93
  241. package/src/components/documents/page-title.tsx +0 -7
  242. package/src/components/documents/tab-button.tsx +0 -172
  243. package/src/components/documents/tag.tsx +0 -30
  244. package/src/components/index.js +0 -0
  245. package/src/components/inputs/checkbox-input.tsx +0 -66
  246. package/src/components/inputs/input-box.tsx +0 -48
  247. package/src/components/inputs/input-element.tsx +0 -65
  248. package/src/components/inputs/input-form.tsx +0 -164
  249. package/src/components/inputs/input.tsx +0 -181
  250. package/src/components/inputs/number-input.tsx +0 -108
  251. package/src/components/inputs/radiobox-input.tsx +0 -53
  252. package/src/components/inputs/textarea-input.tsx +0 -47
  253. package/src/components/inputs/textbox-input.tsx +0 -45
  254. package/src/components/layouts/global-dialogbox.tsx +0 -431
  255. package/src/components/layouts/global-layout.tsx +0 -59
  256. package/src/components/layouts/layout-helpers.tsx +0 -20
  257. package/src/components/layouts/panels/user-panel.tsx +0 -112
  258. package/src/components/layouts/utility-menu.tsx +0 -49
  259. package/src/components/panels/theme-panel.tsx +0 -46
  260. package/src/helpers/bitwise-helpers.tsx +0 -11
  261. package/src/helpers/browser-helpers.tsx +0 -156
  262. package/src/helpers/datasource-helpers.tsx +0 -99
  263. package/src/helpers/element-helpers.tsx +0 -57
  264. package/src/helpers/input-helpers.tsx +0 -74
  265. package/src/helpers/string-helpers.tsx +0 -28
  266. package/src/helpers/utility-helpers.tsx +0 -45
  267. package/src/helpers/validation-helpers.tsx +0 -302
  268. package/src/index.ts +0 -26
  269. package/src/interface.ts +0 -19
  270. package/src/interfaces/auth-token-interfaces.tsx +0 -6
  271. package/src/interfaces/browser-interfaces.tsx +0 -36
  272. package/src/interfaces/button-interfaces.tsx +0 -63
  273. package/src/interfaces/datasource-interfaces.tsx +0 -22
  274. package/src/interfaces/datatable-interfaces.tsx +0 -25
  275. package/src/interfaces/dialogbox-interfaces.tsx +0 -5
  276. package/src/interfaces/exception-interfaces.tsx +0 -99
  277. package/src/interfaces/http-interfaces.tsx +0 -129
  278. package/src/interfaces/icon-interfaces.tsx +0 -129
  279. package/src/interfaces/input-interfaces.tsx +0 -410
  280. package/src/interfaces/layout-interfaces.tsx +0 -191
  281. package/src/interfaces/menu-interfaces.tsx +0 -49
  282. package/src/interfaces/message-interfaces.tsx +0 -32
  283. package/src/interfaces/permission-interfaces.tsx +0 -9
  284. package/src/interfaces/storage-interfaces.tsx +0 -5
  285. package/src/interfaces/system-interfaces.tsx +0 -22
  286. package/src/interfaces/theme-interfaces.tsx +0 -123
  287. package/src/interfaces/type-interfaces.tsx +0 -28
  288. package/src/interfaces/user-interfaces.tsx +0 -47
  289. package/src/nexus-client.tsx +0 -21
  290. package/src/nexus.environments.tsx +0 -66
  291. package/src/proxy-api/proxy-backend.tsx +0 -60
  292. package/src/proxy-api/proxy-constants.tsx +0 -28
  293. package/src/proxy-api/proxy-helpers.tsx +0 -91
  294. package/src/proxy-api-client.tsx +0 -1
  295. package/src/proxy-api-server.tsx +0 -2
  296. package/src/services/http-client-services.tsx +0 -75
  297. package/src/services/http-services.tsx +0 -158
  298. package/src/services/loader-service.tsx +0 -185
  299. package/src/services/localstorage-service.tsx +0 -119
  300. package/src/services/message-services.tsx +0 -383
  301. package/src/services/theme-service.tsx +0 -161
  302. package/src/services/user-services.tsx +0 -10
  303. package/src/sso-config/auth-token-validation.ts +0 -16
  304. package/src/sso-config/callback-route.tsx +0 -35
  305. package/src/sso-config/cookie-encryption.ts +0 -51
  306. package/src/sso-config/cookie-helpers.tsx +0 -102
  307. package/src/sso-config/forgot-password-route.tsx +0 -36
  308. package/src/sso-config/oauth-callback-state.ts +0 -61
  309. package/src/sso-config/pkce-helpers.ts +0 -9
  310. package/src/sso-config/provider-complete-route.tsx +0 -18
  311. package/src/sso-config/redirect-context-actions.tsx +0 -45
  312. package/src/sso-config/redirect-context-helpers.tsx +0 -159
  313. package/src/sso-config/redirect-context-persist.tsx +0 -18
  314. package/src/sso-config/redirect-context-route.tsx +0 -45
  315. package/src/sso-config/refresh-route.tsx +0 -23
  316. package/src/sso-config/sign-in-route.tsx +0 -74
  317. package/src/sso-config/sign-out-route.tsx +0 -57
  318. package/src/sso-config/sign-up-route.tsx +0 -57
  319. package/src/sso-config/sso-backend-flow.ts +0 -38
  320. package/src/sso-config/sso-complete-route.tsx +0 -50
  321. package/src/sso-config/sso-config.ts +0 -59
  322. package/src/sso-config/sso-interfaces.tsx +0 -99
  323. package/src/sso-config/sso-response-helpers.tsx +0 -20
  324. package/src/sso-server.tsx +0 -21
  325. package/src/styles/nexus.animation.css +0 -269
  326. package/src/styles/nexus.core.css +0 -119
  327. package/src/styles/nexus.dialog.css +0 -144
  328. package/src/styles/nexus.icon.css +0 -51
  329. package/src/styles/nexus.input.css +0 -207
  330. package/src/styles/nexus.loader.css +0 -11
  331. package/src/styles/nexus.logic.css +0 -47
  332. package/src/styles/nexus.utility.css +0 -347
@@ -0,0 +1,52 @@
1
+ import { FormStore, type AnyFormStore, type FormStatus } from "../../Helpers/FormStore.ts";
2
+ import type { DatePreferences } from "../../Interfaces/DateInterfaces.ts";
3
+ import type { FormProps, FormValues, InputParams } from "../../Interfaces/FormInterfaces.ts";
4
+ /**
5
+ * A form store for the page to hold: pass it to a form as `form`, then read, watch, set, and submit through it.
6
+ *
7
+ * @example
8
+ * const form = useForm<Employee>();
9
+ * const country = useFormValue<string>(form, "country");
10
+ * <SubmitForm form={form} inputs={inputs} onSubmit={save} />
11
+ */
12
+ export declare function useForm<TValues extends FormValues = FormValues>(): FormStore<TValues>;
13
+ /** One value of a form; the component renders again only when it changes. */
14
+ export declare function useFormValue<T = unknown>(form: AnyFormStore, name: string): T;
15
+ /** Every value of a form; the component renders again on every change, so keep it to small forms or summaries. */
16
+ export declare function useFormValues<TValues extends FormValues>(form: FormStore<TValues>): TValues;
17
+ /** What the form's frame shows: changed, submitting, problems, the form's message, drafts. */
18
+ export declare function useFormStatus(form: AnyFormStore): FormStatus;
19
+ /** The form's own store, or the one given. */
20
+ export declare function useFormStore<TValues extends FormValues>(given: FormStore<TValues> | undefined): FormStore<TValues>;
21
+ /** An input's name in the form's messages: its `fieldName`, its label when it is text, or its name. */
22
+ export declare function formInputLabel(params: InputParams | undefined, name?: string): string;
23
+ /** The names a list shows: all of them up to one more than `most` ("and 1 more" says less than the name), else the first `most`. */
24
+ export declare function namesToShow<T>(names: readonly T[], most?: number): {
25
+ shown: readonly T[];
26
+ more: number;
27
+ };
28
+ /** Names in a sentence: "A", "A and B", "A, B, and C", "A, B, C, and 3 more". */
29
+ export declare function listInSentence(names: readonly string[], most?: number): string;
30
+ /** A moment in the user's time format, with the date in their calendar when it was not today. */
31
+ export declare function formatFormTime(ms: number, preferences: DatePreferences): string;
32
+ /**
33
+ * Renders a list of input params on a 12-column grid (`span` in each input's params), with the values in a store outside
34
+ * React, so a form of any size types as fast as one input. The same params describe every input, so a CRUD page or a
35
+ * server can build a form from plain objects; a tabular or group input is one entry that holds rows.
36
+ *
37
+ * On its own it suits filters and settings that save as they change (`onValueChange`). `SubmitForm` adds checks on
38
+ * submit and the buttons; `ApiForm` sends the values to a server. Inside a plain `<form>`, each input sends its value
39
+ * under its name, and the form's own rule blocks the submit while it fails.
40
+ *
41
+ * @example
42
+ * <Form
43
+ * inputs={[
44
+ * { type: "text", name: "fullName", label: "Full name", required: true, span: 6 },
45
+ * { type: "email", name: "email", label: "Email", span: 6 },
46
+ * { type: "dropdown", name: "role", label: "Role", options: roles, span: 4 },
47
+ * ]}
48
+ * defaultValue={employee}
49
+ * onValueChange={values => saveFilters(values)}
50
+ * />
51
+ */
52
+ export declare function Form<TValues extends FormValues = FormValues>(props: FormProps<TValues>): import("react").JSX.Element;
@@ -0,0 +1,319 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { iconAlertCircle, iconHistory, iconX, InlineIcon } from "nexus-icons";
4
+ import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
5
+ import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { formatDate, toIsoDate, usesDevanagari } from "../../Helpers/DateHelpers.js";
7
+ import { useDatePreferences } from "../../Helpers/DatePreferences.js";
8
+ import { FormStore } from "../../Helpers/FormStore.js";
9
+ import { inputFieldName } from "../../Helpers/InputParamsHelpers.js";
10
+ import { formatTime } from "../../Helpers/TimeHelpers.js";
11
+ import { Button } from "../Documents/Button.js";
12
+ import { hasContent } from "../Inputs/Field.js";
13
+ import { isFirstInvalid } from "../Inputs/InputField.js";
14
+ import { renderInput } from "../Inputs/InputRenderer.js";
15
+ import { firstFocusable } from "../Inputs/RowsInput.js";
16
+ /*
17
+ * Form: a list of input params on a 12-column grid, with the values in a FormStore. Typing renders only the input
18
+ * typed in; an input renders again when a message it shows changes or code sets its value. The form checks nothing on
19
+ * its own: SubmitForm checks on submit, and a plain <form> around a Form blocks its submit while any input, or the form's
20
+ * own rule, has a problem.
21
+ */
22
+ /** Inputs without a box: they line up with the boxes beside them. */
23
+ const CHOICE_TYPES = new Set(["checkbox", "switch", "radio"]);
24
+ /** Inputs taller than one line, for the placeholders shown while the values load. */
25
+ const TALL_TYPES = new Set(["textarea", "markdown", "rich-text", "tree", "tabular", "group", "checkbox-group", "radio-group"]);
26
+ /** What takes focus in an input's cell: a control to type in or pick, before any button of its toolbar. */
27
+ const TYPING_TARGETS = 'input:not([type="hidden"]):not(.nx-visually-hidden):not(:disabled), textarea:not(.nx-visually-hidden):not(:disabled), select:not(:disabled), [contenteditable="true"]';
28
+ const INVALID_CONTROLS = ":is(input, textarea, select):invalid";
29
+ /* Hooks */
30
+ /**
31
+ * A form store for the page to hold: pass it to a form as `form`, then read, watch, set, and submit through it.
32
+ *
33
+ * @example
34
+ * const form = useForm<Employee>();
35
+ * const country = useFormValue<string>(form, "country");
36
+ * <SubmitForm form={form} inputs={inputs} onSubmit={save} />
37
+ */
38
+ export function useForm() {
39
+ const [store] = useState(() => new FormStore());
40
+ return store;
41
+ }
42
+ /** One value of a form; the component renders again only when it changes. */
43
+ export function useFormValue(form, name) {
44
+ const store = form;
45
+ const read = () => store.get(name);
46
+ return useSyncExternalStore(store.subscribeValue, read, read);
47
+ }
48
+ /** Every value of a form; the component renders again on every change, so keep it to small forms or summaries. */
49
+ export function useFormValues(form) {
50
+ const read = () => form.getValue();
51
+ return useSyncExternalStore(form.subscribeValue, read, read);
52
+ }
53
+ /** What the form's frame shows: changed, submitting, problems, the form's message, drafts. */
54
+ export function useFormStatus(form) {
55
+ const store = form;
56
+ return useSyncExternalStore(store.subscribeStatus, store.getStatus, store.getStatus);
57
+ }
58
+ /** The form's own store, or the one given. */
59
+ export function useFormStore(given) {
60
+ const [own] = useState(() => (given ? null : new FormStore()));
61
+ return given ?? own;
62
+ }
63
+ /* Text */
64
+ /** An input's name in the form's messages: its `fieldName`, its label when it is text, or its name. */
65
+ export function formInputLabel(params, name = "") {
66
+ if (!params)
67
+ return name;
68
+ return inputFieldName(params) ?? (typeof params.label === "string" ? params.label : params.name);
69
+ }
70
+ /** The names a list shows: all of them up to one more than `most` ("and 1 more" says less than the name), else the first `most`. */
71
+ export function namesToShow(names, most = 3) {
72
+ return names.length <= most + 1 ? { shown: names, more: 0 } : { shown: names.slice(0, most), more: names.length - most };
73
+ }
74
+ /** Names in a sentence: "A", "A and B", "A, B, and C", "A, B, C, and 3 more". */
75
+ export function listInSentence(names, most = 3) {
76
+ const { shown, more } = namesToShow(names, most);
77
+ if (more)
78
+ return `${shown.join(", ")}, and ${more} more`;
79
+ if (shown.length <= 1)
80
+ return shown[0] ?? "";
81
+ if (shown.length === 2)
82
+ return `${shown[0]} and ${shown[1]}`;
83
+ return `${shown.slice(0, -1).join(", ")}, and ${shown[shown.length - 1]}`;
84
+ }
85
+ /** A moment in the user's time format, with the date in their calendar when it was not today. */
86
+ export function formatFormTime(ms, preferences) {
87
+ const date = new Date(ms);
88
+ const script = usesDevanagari(preferences.calendar, preferences.script) ? "devanagari" : "latin";
89
+ const time = formatTime(date, { format: preferences.timeFormat, script });
90
+ if (toIsoDate(date) === toIsoDate(new Date()))
91
+ return time;
92
+ return `${formatDate(date, preferences)}, ${time}`;
93
+ }
94
+ const spanOf = (params) => Math.max(1, Math.min(12, Math.round(params.span ?? 12)));
95
+ /** A checkbox, switch, or radio that shares a row with labeled boxes: it lines up with their boxes, below their labels. */
96
+ const besideBoxes = (params, span, design) => CHOICE_TYPES.has(params.type) && span < 12 && (design ?? "standard") === "standard";
97
+ /** The input. It starts from the store's value and reports each change; a new stamp mounts it again with a new value. */
98
+ function FieldInput({ store, params, stamp, error, context }) {
99
+ const [initial] = useState(() => store.get(params.name));
100
+ const onValueChange = useCallback((value) => store.input(params.name, value, stamp), [store, params.name, stamp]);
101
+ const own = params;
102
+ const overrides = {
103
+ id: `${context.idPrefix}-${params.name}`,
104
+ defaultValue: initial,
105
+ onValueChange,
106
+ size: own.size ?? context.size,
107
+ readOnly: context.readOnly,
108
+ disabled: context.disabled,
109
+ };
110
+ if (context.design && !own.design)
111
+ overrides.design = context.design;
112
+ if (context.scopeNames && params.type === "radio-group")
113
+ overrides.name = `${context.idPrefix}:${params.name}`;
114
+ // Only a message from the form replaces the input's own.
115
+ if (error !== undefined)
116
+ overrides.error = error;
117
+ return renderInput(params, overrides);
118
+ }
119
+ /** One input's place on the grid. It renders again only when its input must: a message from the form, or a value from code. */
120
+ const FormCell = memo(function FormCell({ store, params, context }) {
121
+ const { name } = params;
122
+ const subscribe = useCallback((listener) => store.subscribeField(name, listener), [store, name]);
123
+ const version = () => store.fieldVersion(name);
124
+ useSyncExternalStore(subscribe, version, version);
125
+ const stamp = store.stamp(name);
126
+ const span = spanOf(params);
127
+ // Leaving the input shows the form's own rule message on it, such as "Passwords do not match".
128
+ function onBlur(event) {
129
+ if (event.relatedTarget instanceof Node && event.currentTarget.contains(event.relatedTarget))
130
+ return;
131
+ store.touch(name);
132
+ }
133
+ return (_jsx("div", { className: cx("nx-form__cell", CHOICE_TYPES.has(params.type) && "nx-form__cell--choice"), style: { "--nx-span": span }, "data-span": span, "data-beside": besideBoxes(params, span, context.design) || undefined, "data-nx-field": name, onBlur: onBlur, children: _jsx(FieldInput, { store: store, params: params, stamp: stamp, error: store.shownError(name), context: context }, stamp) }));
134
+ });
135
+ /** A placeholder in an input's shape while the values load. */
136
+ function SkeletonCell({ params, design }) {
137
+ const span = spanOf(params);
138
+ const choice = CHOICE_TYPES.has(params.type);
139
+ const ownDesign = params.design ?? design ?? "standard";
140
+ return (_jsxs("div", { className: cx("nx-form__cell", "nx-form__skeleton", choice && "nx-form__cell--choice"), style: { "--nx-span": span }, "data-span": span, "data-beside": besideBoxes(params, span, design) || undefined, children: [!choice && ownDesign === "standard" && hasContent(params.label) && _jsx("span", { className: "nx-form__skeleton-label" }), _jsx("span", { className: cx("nx-form__skeleton-box", TALL_TYPES.has(params.type) && "nx-form__skeleton-box--tall", choice && "nx-form__skeleton-box--choice") })] }));
141
+ }
142
+ /** The note over a form whose unsaved changes came back from a draft. */
143
+ function DraftNotice({ store, restored, inputs, preferences }) {
144
+ const label = (name) => formInputLabel(inputs.find(params => params.name === name), name);
145
+ const conflicts = restored.conflicts.map(label);
146
+ return (_jsxs("div", { className: "nx-form__notice", role: "status", children: [_jsx(InlineIcon, { icon: iconHistory }), _jsxs("p", { className: "nx-form__notice-text", children: ["Restored your unsaved changes from ", formatFormTime(restored.savedAt, preferences), ": ", listInSentence(restored.names.map(label)), ".", conflicts.length > 0 && (_jsxs("strong", { children: [" ", listInSentence(conflicts), " also changed on the server since; check ", conflicts.length === 1 ? "it" : "them", " before you save."] }))] }), _jsx(Button, { size: "sm", variant: "ghost", hoverTone: "danger", onClick: () => store.discardDraft(), children: "Discard" }), _jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconX }), "aria-label": "Keep the changes and close this note", title: "Close", onClick: () => store.dismissDraft() })] }));
147
+ }
148
+ /* Form */
149
+ /**
150
+ * Renders a list of input params on a 12-column grid (`span` in each input's params), with the values in a store outside
151
+ * React, so a form of any size types as fast as one input. The same params describe every input, so a CRUD page or a
152
+ * server can build a form from plain objects; a tabular or group input is one entry that holds rows.
153
+ *
154
+ * On its own it suits filters and settings that save as they change (`onValueChange`). `SubmitForm` adds checks on
155
+ * submit and the buttons; `ApiForm` sends the values to a server. Inside a plain `<form>`, each input sends its value
156
+ * under its name, and the form's own rule blocks the submit while it fails.
157
+ *
158
+ * @example
159
+ * <Form
160
+ * inputs={[
161
+ * { type: "text", name: "fullName", label: "Full name", required: true, span: 6 },
162
+ * { type: "email", name: "email", label: "Email", span: 6 },
163
+ * { type: "dropdown", name: "role", label: "Role", options: roles, span: 4 },
164
+ * ]}
165
+ * defaultValue={employee}
166
+ * onValueChange={values => saveFilters(values)}
167
+ * />
168
+ */
169
+ export function Form(props) {
170
+ const { inputs, design, size = "md", readOnly = false, disabled = false, loading = false, draftKey, id, className, style, ref } = props;
171
+ const store = useFormStore(props.form);
172
+ const anyStore = store;
173
+ const preferences = useDatePreferences();
174
+ const idPrefix = useId();
175
+ const rootRef = useRef(null);
176
+ const proxyRef = useRef(null);
177
+ const latest = useRef(props);
178
+ latest.current = props;
179
+ const locked = readOnly || disabled;
180
+ const options = { inputs, validate: props.validate, onFieldChange: props.onFieldChange, context: { preferences }, locked, draftKey };
181
+ // The first render loads the values quietly, so the inputs mount with them; the page hears of it once mounted. A store
182
+ // that already has values (from `useForm`, a form shown again) keeps them.
183
+ if (!store.started) {
184
+ store.configure(options, false);
185
+ store.load(props.defaultValue ?? {}, { draft: !loading, quiet: true });
186
+ }
187
+ useLayoutEffect(() => store.announce(), [store]);
188
+ // Inputs or settings that change what is valid check every value again; callbacks only swap.
189
+ const rulesKey = [inputs, preferences, locked, draftKey];
190
+ const rulesRef = useRef(null);
191
+ useLayoutEffect(() => {
192
+ const previous = rulesRef.current;
193
+ rulesRef.current = rulesKey;
194
+ store.configure(options, !previous || rulesKey.some((item, index) => !Object.is(item, previous[index])));
195
+ });
196
+ useLayoutEffect(() => store.setLoading(loading), [store, loading]);
197
+ useImperativeHandle(ref, () => store, [store]);
198
+ // Radio groups need names of their own when no <form> keeps them apart.
199
+ const [inForm, setInForm] = useState(true);
200
+ useLayoutEffect(() => setInForm(Boolean(rootRef.current?.closest("form"))), []);
201
+ const status = useFormStatus(store);
202
+ /** The inputs on the page that report a problem, and those the values fail, in form order. Fires no events. */
203
+ function findProblems() {
204
+ const root = rootRef.current;
205
+ const invalid = new Map();
206
+ if (root) {
207
+ for (const cell of root.querySelectorAll(":scope > .nx-form__grid > [data-nx-field]")) {
208
+ const controls = cell.querySelectorAll(INVALID_CONTROLS);
209
+ if (controls.length)
210
+ invalid.set(cell.dataset.nxField ?? "", [...controls]);
211
+ }
212
+ }
213
+ const fromValues = new Set(store.problems());
214
+ const names = latest.current.inputs.filter(params => !params.hidden && (invalid.has(params.name) || fromValues.has(params.name))).map(params => params.name);
215
+ return { names, invalid, fromValues };
216
+ }
217
+ function focusInput(name) {
218
+ const root = rootRef.current;
219
+ if (!root)
220
+ return;
221
+ const cell = root.querySelector(`:scope > .nx-form__grid > [data-nx-field${name === undefined ? "" : `="${CSS.escape(name)}"`}]`);
222
+ if (!cell)
223
+ return;
224
+ const target = cell.querySelector(TYPING_TARGETS) ?? firstFocusable(cell);
225
+ target?.focus();
226
+ }
227
+ const view = {
228
+ validate() {
229
+ const { names, invalid, fromValues } = findProblems();
230
+ // A problem the values have and the input on the page does not show: the form shows its message on the input.
231
+ const forced = [...fromValues].filter(name => !invalid.has(name) && store.ruleError(name));
232
+ store.reveal(names, forced);
233
+ // Every input shows its own message, and the first in the form takes focus.
234
+ for (const controls of invalid.values())
235
+ for (const control of controls)
236
+ control.checkValidity();
237
+ const first = names[0];
238
+ // After the inputs' own focus, which a list of rows moves in the next frame.
239
+ if (first && !invalid.has(first))
240
+ requestAnimationFrame(() => focusInput(first));
241
+ return names;
242
+ },
243
+ focus: focusInput,
244
+ };
245
+ useLayoutEffect(() => {
246
+ store.view = view;
247
+ return () => {
248
+ if (store.view === view)
249
+ store.view = null;
250
+ };
251
+ });
252
+ // Page-level listeners: the page's `onValueChange`, the problem list after a submit, and the proxy's validity.
253
+ useEffect(() => {
254
+ let frame = 0;
255
+ const syncProxy = () => {
256
+ const proxy = proxyRef.current;
257
+ if (!proxy)
258
+ return;
259
+ const first = latest.current.inputs.find(params => !params.hidden && store.formRuleError(params.name));
260
+ proxy.setCustomValidity((first ? store.formRuleError(first.name) : store.formRuleMessage) ?? "");
261
+ };
262
+ const recount = () => {
263
+ frame = 0;
264
+ if (store.getStatus().showAll)
265
+ store.setAttention(findProblems().names);
266
+ };
267
+ syncProxy();
268
+ const unsubscribe = store.subscribeValue((values, name) => {
269
+ syncProxy();
270
+ if (store.getStatus().showAll && !frame)
271
+ frame = requestAnimationFrame(recount);
272
+ latest.current.onValueChange?.(values, name);
273
+ });
274
+ return () => {
275
+ unsubscribe();
276
+ cancelAnimationFrame(frame);
277
+ };
278
+ // eslint-disable-next-line react-hooks/exhaustive-deps
279
+ }, [store]);
280
+ // Inside a <form>: a reset brings back the values the form opened with.
281
+ useEffect(() => {
282
+ const form = rootRef.current?.closest("form");
283
+ if (!form)
284
+ return;
285
+ const onReset = () => store.reset();
286
+ form.addEventListener("reset", onReset);
287
+ return () => form.removeEventListener("reset", onReset);
288
+ }, [store]);
289
+ // A draft is written when the page hides or the form leaves, so a change typed just before is kept too.
290
+ useEffect(() => {
291
+ if (!draftKey)
292
+ return;
293
+ const flush = () => store.flushDraft();
294
+ const onVisibility = () => {
295
+ if (document.visibilityState === "hidden")
296
+ flush();
297
+ };
298
+ window.addEventListener("pagehide", flush);
299
+ document.addEventListener("visibilitychange", onVisibility);
300
+ return () => {
301
+ window.removeEventListener("pagehide", flush);
302
+ document.removeEventListener("visibilitychange", onVisibility);
303
+ flush();
304
+ };
305
+ }, [store, draftKey]);
306
+ /** A native submit of the <form> around found the form's own rule failing. */
307
+ function onProxyInvalid(event) {
308
+ event.preventDefault();
309
+ const { names } = findProblems();
310
+ store.reveal(names);
311
+ const first = names.find(name => store.formRuleError(name));
312
+ if (first && isFirstInvalid(event.currentTarget))
313
+ focusInput(first);
314
+ }
315
+ const context = useMemo(() => ({ idPrefix, size, design, readOnly, disabled, scopeNames: !inForm }), [idPrefix, size, design, readOnly, disabled, inForm]);
316
+ // With FormButtons on the page, the form's message is in their status line.
317
+ const ownMessage = status.buttons === 0 ? status.message : null;
318
+ return (_jsxs("div", { ref: rootRef, id: id, className: cx("nx-form", `nx-form--${size}`, className), style: style, "data-nx-form": "", "aria-busy": loading || undefined, children: [status.restored && !loading && _jsx(DraftNotice, { store: anyStore, restored: status.restored, inputs: inputs, preferences: preferences }), _jsx("div", { className: "nx-form__grid", children: inputs.map(params => params.hidden ? null : loading ? (_jsx(SkeletonCell, { params: params, design: design }, params.name)) : (_jsx(FormCell, { store: anyStore, params: params, context: context }, params.name))) }), loading && (_jsx("p", { className: "nx-visually-hidden", role: "status", children: "Loading\u2026" })), _jsx("input", { ref: proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", disabled: locked, onInvalid: onProxyInvalid }), ownMessage && (_jsxs("p", { className: "nx-form__message nx-field__message nx-field__message--error", role: "alert", children: [_jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: ownMessage })] }))] }));
319
+ }
@@ -0,0 +1,65 @@
1
+ import { type ReactNode } from "react";
2
+ import type { AnyFormStore, FormStore } from "../../Helpers/FormStore.ts";
3
+ import type { FormValues, SubmitFormProps } from "../../Interfaces/FormInterfaces.ts";
4
+ import type { Size } from "../../Interfaces/TypeInterfaces.ts";
5
+ export interface FormButtonsProps<TValues extends FormValues = FormValues> {
6
+ /** The form's store: the one given to the form as `form`. */
7
+ form: FormStore<TValues>;
8
+ /** Default "Save". */
9
+ submitLabel?: ReactNode;
10
+ /** Default: a floppy disk. `null` shows none. */
11
+ submitIcon?: ReactNode | null;
12
+ /** Shows a Cancel button. */
13
+ onCancel?: () => void;
14
+ /** Cancel asks before it drops changed values. Default true. */
15
+ confirmCancel?: boolean;
16
+ /** Default "Cancel". */
17
+ cancelLabel?: ReactNode;
18
+ /** Shows a Reset button. */
19
+ resettable?: boolean;
20
+ /** Default "Reset". */
21
+ resetLabel?: ReactNode;
22
+ /** `end` (default): the status line, then the buttons at the end. `start`: the buttons first. */
23
+ align?: "start" | "end";
24
+ /** Size of the buttons. Default `md`. */
25
+ size?: Size;
26
+ /** Nothing to save: only Cancel shows. */
27
+ readOnly?: boolean;
28
+ className?: string;
29
+ }
30
+ /**
31
+ * Asks "Discard your changes?" when the form has changes. Resolves `true` to go on (nothing changed, or the user chose
32
+ * Discard), `false` to keep editing. Cancel buttons and popups that close a form ask it.
33
+ */
34
+ export declare function confirmDiscard(form: AnyFormStore): Promise<boolean>;
35
+ /**
36
+ * A form's bar: its status line (loading, saving, what needs attention, unsaved changes, saved), Reset, Cancel, and Save.
37
+ * `SubmitForm` renders one; render your own with `buttons={false}` to put the bar elsewhere, such as a popup's footer.
38
+ * Its Save button submits the form from anywhere on the page.
39
+ *
40
+ * @example
41
+ * const form = useForm();
42
+ * <Popup footer={<FormButtons form={form} onCancel={close} />}>
43
+ * <ApiForm form={form} buttons={false} inputs={inputs} api={api} />
44
+ * </Popup>
45
+ */
46
+ export declare function FormButtons<TValues extends FormValues = FormValues>({ form, submitLabel, submitIcon, onCancel, confirmCancel, cancelLabel, resettable, resetLabel, align, size, readOnly, className, }: FormButtonsProps<TValues>): import("react").JSX.Element;
47
+ /**
48
+ * A `Form` that submits: every value is checked on Save (the inputs' own rules, the form's `validate`, rows not on the
49
+ * page), each message shows, and the first problem takes focus; then `onSubmit` gets the values, and while it runs Save
50
+ * waits with a spinner and nothing submits twice. A save makes the values the form's own, so "Unsaved changes" clears;
51
+ * `{ ok: false, errors }` shows messages on their inputs. Cancel asks before it drops changes, the page asks before it
52
+ * closes with unsaved ones, and Ctrl+S or Ctrl+Enter saves from any input.
53
+ *
54
+ * @example
55
+ * <SubmitForm
56
+ * inputs={EMPLOYEE_INPUTS}
57
+ * defaultValue={employee}
58
+ * onSubmit={async values => {
59
+ * const answer = await api.save(values);
60
+ * if (!answer.ok) return { ok: false, errors: answer.errors };
61
+ * }}
62
+ * onCancel={close}
63
+ * />
64
+ */
65
+ export declare function SubmitForm<TValues extends FormValues = FormValues>(props: SubmitFormProps<TValues>): import("react").JSX.Element;
@@ -0,0 +1,198 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { iconAlertCircle, iconCircleCheck, iconDeviceFloppy, iconLoader2, iconRefresh, InlineIcon } from "nexus-icons";
4
+ import { useEffect, useId, useLayoutEffect, useRef } from "react";
5
+ import { errorMessage, isAbortError } from "../../Helpers/ApiResponses.js";
6
+ import { cx } from "../../Helpers/ClassHelpers.js";
7
+ import { useDatePreferences } from "../../Helpers/DatePreferences.js";
8
+ import { Button } from "../Documents/Button.js";
9
+ import { messageBox } from "../Layouts/MessageBox.js";
10
+ import { Form, formatFormTime, formInputLabel, namesToShow, useFormStatus, useFormStore } from "./Form.js";
11
+ /** The problems after a submit, each a button that goes to its input: "2 fields need attention: Email, Phone." */
12
+ function ProblemList({ form, names }) {
13
+ const { shown, more } = namesToShow(names);
14
+ return (_jsxs(_Fragment, { children: [names.length === 1 ? "1 field needs attention: " : `${names.length} fields need attention: `, shown.map((name, index) => (_jsxs("span", { children: [index > 0 && (index === shown.length - 1 && !more ? (shown.length > 2 ? ", and " : " and ") : ", "), _jsx("button", { type: "button", className: "nx-form-status__jump", onClick: () => form.focus(name), children: formInputLabel(form.inputs.find(params => params.name === name), name) })] }, name))), more > 0 && `, and ${more} more`, "."] }));
15
+ }
16
+ /** One line of what the form is doing: loading, saving, what needs attention, unsaved changes, or when it saved. */
17
+ function FormStatusLine({ form, status }) {
18
+ const preferences = useDatePreferences();
19
+ let tone = "muted";
20
+ let content = null;
21
+ if (status.loading) {
22
+ content = (_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin" }), _jsx("span", { children: "Loading\u2026" })] }));
23
+ }
24
+ else if (status.submitting) {
25
+ content = _jsx("span", { children: "Saving\u2026" });
26
+ }
27
+ else if (status.message || status.problems.length) {
28
+ tone = "error";
29
+ content = (_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconAlertCircle }), _jsxs("span", { children: [status.message, status.message && status.problems.length > 0 && " ", status.problems.length > 0 && _jsx(ProblemList, { form: form, names: status.problems })] })] }));
30
+ }
31
+ else if (status.changed) {
32
+ content = _jsx("span", { children: status.draftSavedAt ? "Unsaved changes, kept on this device." : "Unsaved changes." });
33
+ }
34
+ else if (status.savedAt) {
35
+ tone = "success";
36
+ content = (_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconCircleCheck }), _jsxs("span", { children: ["Saved at ", formatFormTime(status.savedAt, preferences), "."] })] }));
37
+ }
38
+ return (_jsx("p", { className: cx("nx-form-status", `nx-form-status--${tone}`), "aria-live": "polite", children: content }));
39
+ }
40
+ /**
41
+ * Asks "Discard your changes?" when the form has changes. Resolves `true` to go on (nothing changed, or the user chose
42
+ * Discard), `false` to keep editing. Cancel buttons and popups that close a form ask it.
43
+ */
44
+ export async function confirmDiscard(form) {
45
+ if (!form.isChanged())
46
+ return true;
47
+ return messageBox.confirm({
48
+ title: "Discard your changes?",
49
+ message: "What you changed in this form will be lost.",
50
+ tone: "warning",
51
+ confirmLabel: "Discard",
52
+ cancelLabel: "Keep editing",
53
+ });
54
+ }
55
+ /**
56
+ * A form's bar: its status line (loading, saving, what needs attention, unsaved changes, saved), Reset, Cancel, and Save.
57
+ * `SubmitForm` renders one; render your own with `buttons={false}` to put the bar elsewhere, such as a popup's footer.
58
+ * Its Save button submits the form from anywhere on the page.
59
+ *
60
+ * @example
61
+ * const form = useForm();
62
+ * <Popup footer={<FormButtons form={form} onCancel={close} />}>
63
+ * <ApiForm form={form} buttons={false} inputs={inputs} api={api} />
64
+ * </Popup>
65
+ */
66
+ export function FormButtons({ form, submitLabel = "Save", submitIcon, onCancel, confirmCancel = true, cancelLabel = "Cancel", resettable = false, resetLabel = "Reset", align = "end", size = "md", readOnly = false, className, }) {
67
+ const store = form;
68
+ const status = useFormStatus(store);
69
+ useEffect(() => store.addButtons(), [store]);
70
+ async function cancel() {
71
+ if (confirmCancel && !(await confirmDiscard(store)))
72
+ return;
73
+ store.dropDraft();
74
+ onCancel?.();
75
+ }
76
+ const buttonSize = size === "lg" ? "lg" : size === "sm" ? "sm" : "md";
77
+ const actions = [
78
+ resettable && !readOnly && (_jsx(Button, { size: buttonSize, variant: "ghost", icon: _jsx(InlineIcon, { icon: iconRefresh }), disabled: !status.changed || status.submitting || status.loading, onClick: () => store.reset(), children: resetLabel }, "reset")),
79
+ onCancel && (_jsx(Button, { size: buttonSize, onClick: () => void cancel(), children: cancelLabel }, "cancel")),
80
+ !readOnly && (_jsx(Button, { type: "submit", form: status.formId ?? undefined, size: buttonSize, variant: "primary", loading: status.submitting, disabled: status.loading, icon: submitIcon === undefined ? _jsx(InlineIcon, { icon: iconDeviceFloppy }) : (submitIcon ?? undefined), children: submitLabel }, "submit")),
81
+ ].filter(Boolean);
82
+ if (align === "start")
83
+ actions.reverse();
84
+ const line = _jsx(FormStatusLine, { form: store, status: status }, "status");
85
+ const bar = actions.length ? (_jsx("div", { className: "nx-form-buttons__actions", children: actions }, "actions")) : null;
86
+ return _jsx("div", { className: cx("nx-form-buttons", `nx-form-buttons--${align}`, className), children: align === "start" ? [bar, line] : [line, bar] });
87
+ }
88
+ /**
89
+ * A `Form` that submits: every value is checked on Save (the inputs' own rules, the form's `validate`, rows not on the
90
+ * page), each message shows, and the first problem takes focus; then `onSubmit` gets the values, and while it runs Save
91
+ * waits with a spinner and nothing submits twice. A save makes the values the form's own, so "Unsaved changes" clears;
92
+ * `{ ok: false, errors }` shows messages on their inputs. Cancel asks before it drops changes, the page asks before it
93
+ * closes with unsaved ones, and Ctrl+S or Ctrl+Enter saves from any input.
94
+ *
95
+ * @example
96
+ * <SubmitForm
97
+ * inputs={EMPLOYEE_INPUTS}
98
+ * defaultValue={employee}
99
+ * onSubmit={async values => {
100
+ * const answer = await api.save(values);
101
+ * if (!answer.ok) return { ok: false, errors: answer.errors };
102
+ * }}
103
+ * onCancel={close}
104
+ * />
105
+ */
106
+ export function SubmitForm(props) {
107
+ const { onSubmit: _onSubmit, onSaved: _onSaved, onCancel, confirmCancel, submitLabel, submitIcon, cancelLabel, resettable, resetLabel, resetOnSuccess: _resetOnSuccess, buttons = "end", confirmLeave, id, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...formProps } = props;
108
+ const store = useFormStore(props.form);
109
+ const formRef = useRef(null);
110
+ const generatedId = useId();
111
+ const formId = id ?? `${generatedId}form`;
112
+ const controllerRef = useRef(null);
113
+ const latest = useRef(props);
114
+ latest.current = props;
115
+ const locked = Boolean(props.readOnly || props.disabled);
116
+ useLayoutEffect(() => {
117
+ const submitter = () => formRef.current?.requestSubmit();
118
+ store.submitter = submitter;
119
+ store.setFormId(formId);
120
+ return () => {
121
+ if (store.submitter === submitter)
122
+ store.submitter = null;
123
+ store.setFormId(null);
124
+ };
125
+ }, [store, formId]);
126
+ // A submit still running when the form leaves stops.
127
+ useEffect(() => () => controllerRef.current?.abort(), []);
128
+ // Closing or reloading the page with unsaved changes asks first, unless a draft keeps them.
129
+ const guardLeave = confirmLeave ?? !props.draftKey;
130
+ useEffect(() => {
131
+ if (!guardLeave)
132
+ return;
133
+ const onBeforeUnload = (event) => {
134
+ if (!store.isChanged() || locked)
135
+ return;
136
+ event.preventDefault();
137
+ // Older browsers show the prompt only when this is set.
138
+ event.returnValue = "";
139
+ };
140
+ window.addEventListener("beforeunload", onBeforeUnload);
141
+ return () => window.removeEventListener("beforeunload", onBeforeUnload);
142
+ }, [store, guardLeave, locked]);
143
+ async function submit() {
144
+ const status = store.getStatus();
145
+ if (status.submitting || status.loading || locked)
146
+ return;
147
+ if (!store.validate())
148
+ return;
149
+ const controller = new AbortController();
150
+ controllerRef.current = controller;
151
+ const values = store.getValue();
152
+ const context = { changes: store.getChanges(), initial: store.getInitial(), form: store, signal: controller.signal };
153
+ store.beginSubmit();
154
+ let result;
155
+ try {
156
+ result = await latest.current.onSubmit(values, context);
157
+ }
158
+ catch (error) {
159
+ result = isAbortError(error) ? false : { ok: false, message: errorMessage(error) };
160
+ }
161
+ if (controllerRef.current === controller)
162
+ controllerRef.current = null;
163
+ if (controller.signal.aborted || result === false) {
164
+ store.endSubmit();
165
+ return;
166
+ }
167
+ if (result && typeof result === "object" && result.ok === false) {
168
+ store.endSubmit();
169
+ store.setErrors(result.errors ?? {}, result.message ?? null);
170
+ const first = store.getStatus().problems[0];
171
+ if (first)
172
+ store.focus(first);
173
+ return;
174
+ }
175
+ // Saved.
176
+ if (latest.current.resetOnSuccess) {
177
+ store.reset();
178
+ store.markSaved();
179
+ requestAnimationFrame(() => store.focus());
180
+ }
181
+ else {
182
+ store.commit(values, result && typeof result === "object" && result.ok ? result.values : undefined);
183
+ }
184
+ latest.current.onSaved?.(store.getValue(), store);
185
+ }
186
+ function onKeyDown(event) {
187
+ if (event.defaultPrevented || !(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey)
188
+ return;
189
+ if (event.key.toLowerCase() === "s" || event.key === "Enter") {
190
+ event.preventDefault();
191
+ formRef.current?.requestSubmit();
192
+ }
193
+ }
194
+ return (_jsxs("form", { ref: formRef, id: formId, className: cx("nx-submit-form", className), style: style, noValidate: true, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, onSubmit: event => {
195
+ event.preventDefault();
196
+ void submit();
197
+ }, onKeyDown: onKeyDown, children: [_jsx(Form, { ...formProps, form: store }), buttons !== false && (_jsx(FormButtons, { form: store, align: buttons, submitLabel: submitLabel, submitIcon: submitIcon, onCancel: onCancel, confirmCancel: confirmCancel, cancelLabel: cancelLabel, resettable: resettable, resetLabel: resetLabel, size: formProps.size, readOnly: locked }))] }));
198
+ }