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,87 @@
1
+ import { type ChangeEvent, type ComponentPropsWithRef, type CSSProperties, type ReactNode } from "react";
2
+ import type { NumberFormatOptions, SliderMark, ValidateOn } from "../../Interfaces/InputInterfaces.ts";
3
+ import type { Size } from "../../Interfaces/TypeInterfaces.ts";
4
+ interface SliderBaseProps extends NumberFormatOptions {
5
+ label?: ReactNode;
6
+ /** Help text under the slider. An error takes its place. */
7
+ hint?: ReactNode;
8
+ /** An error from outside, such as a server message. */
9
+ error?: string | null;
10
+ /** Default `md`. */
11
+ size?: Size;
12
+ /** Default 0. */
13
+ min?: number;
14
+ /** Default 100. */
15
+ max?: number;
16
+ /** Default 1. */
17
+ step?: number;
18
+ /** Text before the shown value, e.g. "Rs ". Include a space if one is wanted. */
19
+ prefix?: string;
20
+ /** Text after the shown value, e.g. "%" or " nights". */
21
+ suffix?: string;
22
+ /** Writes the shown value; replaces the number format, `prefix`, and `suffix`. For example, hours as "14:30". */
23
+ format?: (value: number) => string;
24
+ /** Shows the value beside the label. Default true. */
25
+ showValue?: boolean;
26
+ /** Shows the value above the thumb while it is hovered, focused, or dragged. Best with `showValue={false}`. */
27
+ tooltip?: boolean;
28
+ /** Labelled points under the track. */
29
+ marks?: readonly SliderMark[];
30
+ disabled?: boolean;
31
+ /** Can be focused but not moved; the value is still sent with a form. A lock follows the label, and a try to move it shows a note. */
32
+ readOnly?: boolean;
33
+ /** What a read-only slider says when someone tries to move it. Default "Read-only: this value cannot be changed." ("this range" for a range slider). */
34
+ readOnlyMessage?: string;
35
+ /** Form field name. A range slider sends two values under it: start, then end. */
36
+ name?: string;
37
+ id?: string;
38
+ /** Name used in messages. Default: the label, when it is text. */
39
+ fieldName?: string;
40
+ /** When a `validate` message shows. Default `blur`; submitting a form always shows it. */
41
+ validateOn?: ValidateOn;
42
+ className?: string;
43
+ style?: CSSProperties;
44
+ }
45
+ type NativeInputProps = Omit<ComponentPropsWithRef<"input">, keyof SliderBaseProps | "type" | "value" | "defaultValue" | "onChange" | "children">;
46
+ export interface SliderProps extends SliderBaseProps, NativeInputProps {
47
+ value?: number;
48
+ /** Default: `min`. */
49
+ defaultValue?: number;
50
+ /** Returns a message to fail, e.g. a deposit below a minimum. */
51
+ validate?: (value: number) => string | null | undefined;
52
+ /** Every change while dragging or stepping. */
53
+ onValueChange?: (value: number, event: ChangeEvent<HTMLElement>) => void;
54
+ /** Once the thumb is released or a key step ends: the moment to call an API. */
55
+ onValueCommit?: (value: number) => void;
56
+ inputClassName?: string;
57
+ }
58
+ export interface RangeSliderProps extends SliderBaseProps {
59
+ /** `[start, end]`. With `value`, the slider is controlled. */
60
+ value?: readonly [number, number];
61
+ /** Default: `[min, max]`. */
62
+ defaultValue?: readonly [number, number];
63
+ /** Smallest gap between start and end. Default 0. */
64
+ minDistance?: number;
65
+ /** Names of the two thumbs for screen readers, after the label. Default `["minimum", "maximum"]`. */
66
+ thumbLabels?: readonly [string, string];
67
+ validate?: (value: [number, number]) => string | null | undefined;
68
+ onValueChange?: (value: [number, number], event: ChangeEvent<HTMLElement>) => void;
69
+ onValueCommit?: (value: [number, number]) => void;
70
+ "aria-label"?: string;
71
+ }
72
+ /**
73
+ * Pick a number by dragging, with the arrow keys (Page Up and Page Down for bigger steps, Home and End for the
74
+ * limits), or by clicking a mark. A native range input, so touch and screen readers work as usual. Dragging writes
75
+ * the fill and the shown value straight to the DOM: nothing re-renders.
76
+ *
77
+ * @example <Slider name="discount" label="Discount" max={50} suffix="%" defaultValue={10} />
78
+ */
79
+ export declare function Slider({ value, defaultValue, validate, onValueChange, onValueCommit, inputClassName, ...props }: SliderProps): import("react").JSX.Element;
80
+ /**
81
+ * Pick a range with two thumbs, such as a price filter. Clicking the track moves the nearer thumb, and the thumbs
82
+ * keep `minDistance` apart.
83
+ *
84
+ * @example <RangeSlider name="price" label="Price per night" min={1000} max={30000} step={500} prefix="Rs " grouping />
85
+ */
86
+ export declare function RangeSlider({ value, defaultValue, minDistance, thumbLabels, validate, onValueChange, onValueCommit, ...props }: RangeSliderProps): import("react").JSX.Element;
87
+ export {};
@@ -0,0 +1,216 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Fragment, useEffect, useEffectEvent, useRef, } from "react";
4
+ import { cx } from "../../Helpers/ClassHelpers.js";
5
+ import { clampNumber, countDecimals, formatNumber, roundDecimal } from "../../Helpers/NumberHelpers.js";
6
+ import { GroupFooter, leavesField, OPTION_ATTRIBUTE, useChoiceField } from "./ChoiceField.js";
7
+ import { hasContent } from "./Field.js";
8
+ import { setInputValue } from "./InputField.js";
9
+ import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
10
+ /**
11
+ * Pick a number by dragging, with the arrow keys (Page Up and Page Down for bigger steps, Home and End for the
12
+ * limits), or by clicking a mark. A native range input, so touch and screen readers work as usual. Dragging writes
13
+ * the fill and the shown value straight to the DOM: nothing re-renders.
14
+ *
15
+ * @example <Slider name="discount" label="Discount" max={50} suffix="%" defaultValue={10} />
16
+ */
17
+ export function Slider({ value, defaultValue, validate, onValueChange, onValueCommit, inputClassName, ...props }) {
18
+ const base = pickBase(props);
19
+ const inputProps = omitBase(props);
20
+ return (_jsx(SliderField, { ...base, values: value === undefined ? undefined : [value], defaultValues: [defaultValue ?? base.min ?? 0], validate: validate && (values => validate(values[0])), onValues: onValueChange && ((values, event) => onValueChange(values[0], event)), onCommit: onValueCommit && (values => onValueCommit(values[0])), inputProps: inputProps, inputClassName: inputClassName }));
21
+ }
22
+ /**
23
+ * Pick a range with two thumbs, such as a price filter. Clicking the track moves the nearer thumb, and the thumbs
24
+ * keep `minDistance` apart.
25
+ *
26
+ * @example <RangeSlider name="price" label="Price per night" min={1000} max={30000} step={500} prefix="Rs " grouping />
27
+ */
28
+ export function RangeSlider({ value, defaultValue, minDistance = 0, thumbLabels = ["minimum", "maximum"], validate, onValueChange, onValueCommit, ...props }) {
29
+ const base = pickBase(props);
30
+ const min = base.min ?? 0;
31
+ const max = base.max ?? 100;
32
+ const start = defaultValue ?? [min, max];
33
+ return (_jsx(SliderField, { ...base, range: true, values: value, defaultValues: [Math.min(start[0], start[1]), Math.max(start[0], start[1])], minDistance: minDistance, thumbLabels: thumbLabels, validate: validate && (values => validate([values[0], values[1]])), onValues: onValueChange && ((values, event) => onValueChange([values[0], values[1]], event)), onCommit: onValueCommit && (values => onValueCommit([values[0], values[1]])), inputProps: { "aria-label": props["aria-label"] } }));
34
+ }
35
+ const BASE_KEYS = [
36
+ "label", "hint", "error", "size", "min", "max", "step", "prefix", "suffix", "format", "showValue", "tooltip", "marks", "disabled", "readOnly", "readOnlyMessage",
37
+ "name", "id", "fieldName", "validateOn", "className", "style", "decimals", "fixedDecimals", "grouping", "locale",
38
+ ];
39
+ function pickBase(props) {
40
+ const base = {};
41
+ for (const key of BASE_KEYS)
42
+ if (key in props)
43
+ base[key] = props[key];
44
+ return base;
45
+ }
46
+ function omitBase(props) {
47
+ const rest = { ...props };
48
+ for (const key of BASE_KEYS)
49
+ delete rest[key];
50
+ return rest;
51
+ }
52
+ function SliderField(props) {
53
+ const { label, hint, size = "md", min = 0, max = 100, step = 1, prefix, suffix, format, showValue = true, tooltip = false, marks, disabled = false, readOnly = false, name, fieldName, className, style, decimals, fixedDecimals, grouping, locale, range = false, values, defaultValues, minDistance = 0, thumbLabels = [], validate, onValues, onCommit, inputProps, inputClassName, } = props;
54
+ const { readOnlyMessage = range ? "Read-only: this range cannot be changed." : "Read-only: this value cannot be changed." } = props;
55
+ const span = max - min || 1;
56
+ const initial = (values ?? defaultValues).map(value => clampNumber(value, min, max));
57
+ const text = (value) => (format ? format(value) : `${prefix ?? ""}${formatNumber(value, { decimals, fixedDecimals, grouping, locale })}${suffix ?? ""}`);
58
+ const fraction = (value) => (clampNumber(value, min, max) - min) / span;
59
+ const controlRef = useRef(null);
60
+ const valueRef = useRef(null);
61
+ const tooltipRefs = useRef([]);
62
+ // The last values, so a read-only slider can put a moved thumb straight back.
63
+ const lastRef = useRef(initial);
64
+ const field = useChoiceField(props, {
65
+ check: inputs => validate?.(read(inputs)) || null,
66
+ checkKeys: [values?.join(), validate, min, max],
67
+ sync,
68
+ });
69
+ const read = (inputs) => inputs.map(input => Number(input.value));
70
+ // A read-only slider puts a moved thumb straight back: a lock after the label, and a note under the track, say why.
71
+ const readOnlyNotice = useReadOnlyNotice(readOnly && !disabled, field.containerRef, readOnlyMessage, {
72
+ anchor: target => target.closest(".nx-slider__control"),
73
+ typing: false,
74
+ keys: ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End"],
75
+ lock: "label",
76
+ });
77
+ /** Writes the fill, the shown value, the tooltips, and each thumb's spoken value straight to the DOM. */
78
+ function sync(inputs) {
79
+ const current = read(inputs);
80
+ const control = controlRef.current;
81
+ if (!control || current.length === 0)
82
+ return;
83
+ control.style.setProperty("--nx-slider-start", String(range ? fraction(current[0]) : 0));
84
+ control.style.setProperty("--nx-slider-end", String(fraction(current[current.length - 1])));
85
+ inputs.forEach((input, index) => {
86
+ const valueText = text(current[index]);
87
+ input.setAttribute("aria-valuetext", valueText);
88
+ const bubble = tooltipRefs.current[index];
89
+ if (bubble) {
90
+ bubble.textContent = valueText;
91
+ bubble.style.setProperty("--nx-slider-at", String(fraction(current[index])));
92
+ }
93
+ });
94
+ if (valueRef.current)
95
+ valueRef.current.textContent = current.map(text).join(" – ");
96
+ lastRef.current = current;
97
+ }
98
+ function handleChange(event) {
99
+ const input = event.target;
100
+ if (!(input instanceof HTMLInputElement) || !input.hasAttribute(OPTION_ATTRIBUTE))
101
+ return;
102
+ const index = Number(input.getAttribute(OPTION_ATTRIBUTE));
103
+ if (readOnly) {
104
+ input.value = String(lastRef.current[index]);
105
+ return;
106
+ }
107
+ const inputs = field.inputs();
108
+ if (range && inputs.length === 2) {
109
+ // The thumbs never cross, and keep minDistance apart.
110
+ const [start, end] = inputs;
111
+ if (index === 0 && Number(start.value) > Number(end.value) - minDistance)
112
+ start.value = String(Math.max(min, Number(end.value) - minDistance));
113
+ if (index === 1 && Number(end.value) < Number(start.value) + minDistance)
114
+ end.value = String(Math.min(max, Number(start.value) + minDistance));
115
+ controlRef.current?.setAttribute("data-active", String(index));
116
+ }
117
+ field.onChange();
118
+ onValues?.(read(inputs), event);
119
+ }
120
+ // A released thumb, or a finished key step, fires the native change event.
121
+ const commit = useEffectEvent(() => onCommit?.(read(field.inputs())));
122
+ useEffect(() => {
123
+ const control = controlRef.current;
124
+ if (!control)
125
+ return;
126
+ const onNativeChange = () => commit();
127
+ control.addEventListener("change", onNativeChange);
128
+ return () => control.removeEventListener("change", onNativeChange);
129
+ }, []);
130
+ /** The value under a pointer's x position, on the step grid. */
131
+ function valueAt(clientX) {
132
+ const control = controlRef.current;
133
+ const rect = control.getBoundingClientRect();
134
+ const thumb = parseFloat(getComputedStyle(control).getPropertyValue("--nx-slider-thumb")) || 16;
135
+ const position = clampNumber((clientX - rect.left - thumb / 2) / Math.max(1, rect.width - thumb), 0, 1);
136
+ const stepped = min + Math.round((position * span) / step) * step;
137
+ return clampNumber(roundDecimal(stepped, Math.max(countDecimals(step), countDecimals(min))), min, max);
138
+ }
139
+ /** Moves the thumb nearest to a value there, as a click on the track or on a mark does. */
140
+ function moveTo(value, focus = true) {
141
+ const input = nearestInput(value);
142
+ if (disabled || readOnly || !input)
143
+ return null;
144
+ if (focus)
145
+ input.focus();
146
+ setInputValue(input, String(value));
147
+ return input;
148
+ }
149
+ /** The thumb a press at a value would move: the only one, or the nearer of two. */
150
+ function nearestInput(value) {
151
+ const inputs = field.inputs();
152
+ if (inputs.length < 2)
153
+ return inputs[0];
154
+ const [start, end] = read(inputs);
155
+ return inputs[value <= start ? 0 : value >= end ? 1 : value - start <= end - value ? 0 : 1];
156
+ }
157
+ function commitInput(input) {
158
+ input.dispatchEvent(new Event("change", { bubbles: true }));
159
+ }
160
+ function handleControlPointerDown(event) {
161
+ if (!readOnly) {
162
+ if (range)
163
+ handleTrackPointerDown(event);
164
+ return;
165
+ }
166
+ // A press on a range slider's track focuses the nearer thumb, as when editable, so focus stays in the slider
167
+ // and the note stays up.
168
+ if (range && event.button === 0 && !(event.target instanceof HTMLInputElement)) {
169
+ event.preventDefault();
170
+ nearestInput(valueAt(event.clientX))?.focus();
171
+ }
172
+ readOnlyNotice.show(event.currentTarget);
173
+ }
174
+ function handleBlur(event) {
175
+ if (leavesField(event))
176
+ readOnlyNotice.hide();
177
+ field.onBlur(event);
178
+ }
179
+ // A range slider's inputs only take pointer events on their thumbs, so a press on the track lands here.
180
+ function handleTrackPointerDown(event) {
181
+ if (event.button !== 0 || event.target instanceof HTMLInputElement)
182
+ return;
183
+ const input = moveTo(valueAt(event.clientX));
184
+ if (!input)
185
+ return;
186
+ event.preventDefault();
187
+ const control = event.currentTarget;
188
+ control.setPointerCapture(event.pointerId);
189
+ const move = (moveEvent) => setInputValue(input, String(valueAt(moveEvent.clientX)));
190
+ const end = () => {
191
+ control.removeEventListener("pointermove", move);
192
+ control.removeEventListener("lostpointercapture", end);
193
+ commitInput(input);
194
+ };
195
+ control.addEventListener("pointermove", move);
196
+ control.addEventListener("lostpointercapture", end);
197
+ }
198
+ const labelId = `${field.fieldId}-label`;
199
+ const inputId = (index) => (index === 0 ? field.fieldId : `${field.fieldId}-${index}`);
200
+ const describedBy = field.error || hint ? field.messageId : undefined;
201
+ const nameText = fieldName ?? (typeof label === "string" ? label : undefined);
202
+ const controlStyle = { "--nx-slider-start": range ? fraction(initial[0]) : 0, "--nx-slider-end": fraction(initial[initial.length - 1]) };
203
+ return (_jsxs("div", { ref: field.containerRef, role: range ? "group" : undefined, "aria-labelledby": range && hasContent(label) ? labelId : undefined, className: cx("nx-slider", `nx-slider--${size}`, range && "nx-slider--range", className), style: style, "data-invalid": field.error ? true : undefined, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, onChange: handleChange, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [(hasContent(label) || showValue) && (_jsxs("div", { className: "nx-slider__head", children: [hasContent(label) &&
204
+ (range ? (_jsxs("span", { id: labelId, className: "nx-field__label", children: [label, readOnlyNotice.lock] })) : (_jsxs("label", { id: labelId, htmlFor: field.fieldId, className: "nx-field__label", children: [label, readOnlyNotice.lock] }))), showValue && (_jsx("output", { ref: valueRef, className: "nx-slider__value", htmlFor: initial.map((_, index) => inputId(index)).join(" "), "aria-hidden": "true", children: initial.map(text).join(" – ") }))] })), _jsxs("div", { ref: controlRef, className: "nx-slider__control", style: controlStyle, onPointerDown: range || readOnly ? handleControlPointerDown : undefined, children: [_jsx("span", { className: "nx-slider__track", "aria-hidden": "true", children: _jsx("span", { className: "nx-slider__fill" }) }), initial.map((value, index) => (_jsxs(Fragment, { children: [_jsx("input", { ...(index === 0 ? inputProps : undefined), [OPTION_ATTRIBUTE]: index, id: inputId(index), type: "range", className: cx("nx-slider__input", inputClassName), name: name, min: min, max: max, step: step, value: values ? value : undefined, defaultValue: values ? undefined : value, disabled: disabled, "aria-label": range ? [nameText, thumbLabels[index]].filter(Boolean).join(" ") || undefined : inputProps["aria-label"], "aria-readonly": readOnly || undefined, "aria-invalid": field.error ? true : undefined, "aria-describedby": describedBy, "aria-valuetext": text(value), onInvalid: field.onInvalid,
205
+ // A controlled value changes through onValueChange; this silences React's missing-onChange warning.
206
+ onChange: values ? noop : undefined }), tooltip && (_jsx("span", { ref: node => {
207
+ tooltipRefs.current[index] = node;
208
+ }, className: "nx-slider__tooltip", style: { "--nx-slider-at": fraction(value) }, "aria-hidden": "true", children: text(value) }))] }, index)))] }), marks && marks.length > 0 && (_jsx("div", { className: "nx-slider__marks", "aria-hidden": "true", children: marks.map(mark => (_jsx("button", { type: "button", tabIndex: -1, className: "nx-slider__mark", style: { "--nx-slider-at": fraction(mark.value) }, "data-edge": mark.value <= min ? "start" : mark.value >= max ? "end" : undefined, disabled: disabled, onClick: () => {
209
+ const input = moveTo(mark.value);
210
+ if (input)
211
+ commitInput(input);
212
+ else
213
+ readOnlyNotice.show(controlRef.current);
214
+ }, children: mark.label ?? text(mark.value) }, mark.value))) })), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
215
+ }
216
+ function noop() { }
@@ -0,0 +1,23 @@
1
+ import type { RowValue, TabularFormProps } from "../../Interfaces/FormInterfaces.ts";
2
+ /**
3
+ * Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
4
+ * value is an array of row objects, one value per input, with any other fields the rows came with (such as ids). Inputs
5
+ * come as params, the same objects a form takes, so the same list can show as sections in a `GroupForm`.
6
+ *
7
+ * Only the rows in view are on the page, so a sheet of thousands of rows opens and types as fast as a short one; the
8
+ * values live in a store outside React, and typing renders nothing. The header, the row numbers, and the remove buttons
9
+ * stay in place while the table scrolls. A row number opens the row's menu (insert, duplicate, move, remove). Enter in
10
+ * a text or number cell moves down the column, and on the last row adds a row.
11
+ *
12
+ * Inside a `<form>`, the rows are sent as JSON under `name`; a row with a problem blocks submit, and the first one
13
+ * scrolls into view with focus.
14
+ *
15
+ * @example
16
+ * <TabularForm name="emails" label="Emails" itemLabel="Email" max={5} unique={["email"]} inputs={[
17
+ * { type: "email", name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
18
+ * { type: "dropdown", name: "type", label: "Type", options: EMAIL_TYPES },
19
+ * { type: "checkbox", name: "isVerified", label: "Verified" },
20
+ * { type: "radio", name: "isPrimary", label: "Primary", required: true },
21
+ * ]} />
22
+ */
23
+ export declare function TabularForm<TRow extends RowValue = RowValue>(props: TabularFormProps<TRow>): import("react").JSX.Element;
@@ -0,0 +1,283 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { iconArrowDown, iconArrowUp, iconCopy, iconDotsVertical, iconRowInsertBottom, iconRowInsertTop, iconTrash, iconX, InlineIcon } from "nexus-icons";
4
+ import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState, } from "react";
5
+ import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { Button } from "../Documents/Button.js";
7
+ import { Menu } from "../Documents/Menu.js";
8
+ import { firstFocusable, labelText, RowCell, rowName, RowsFooter, RowsLegend, useRowsFrame, useRowsInput, useRowVersion } from "./RowsInput.js";
9
+ /** Height of a row: the input's box and the line under it. Measured from the first row once it renders. */
10
+ const ROW_HEIGHT = { sm: 29, md: 35, lg: 41 };
11
+ /** Rows rendered above and below the ones in view, so Tab and Enter always find the next row on the page. */
12
+ const OVERSCAN = 8;
13
+ const DEFAULT_MAX_HEIGHT = 440;
14
+ /** A column's width by input type, when its params give none. */
15
+ const COLUMN_WIDTH = {
16
+ text: "10rem",
17
+ email: "13rem",
18
+ password: "10rem",
19
+ search: "10rem",
20
+ tel: "9rem",
21
+ url: "13rem",
22
+ textarea: "14rem",
23
+ number: "7.5rem",
24
+ checkbox: "6rem",
25
+ switch: "6rem",
26
+ radio: "6rem",
27
+ "checkbox-group": "16rem",
28
+ "radio-group": "16rem",
29
+ dropdown: "10rem",
30
+ date: "10rem",
31
+ "date-range": "15rem",
32
+ time: "8rem",
33
+ "date-time": "14rem",
34
+ slider: "10rem",
35
+ "range-slider": "12rem",
36
+ };
37
+ /** Columns that keep their width instead of sharing the spare room. */
38
+ const FIXED_TYPES = new Set(["checkbox", "switch", "radio"]);
39
+ const ENTER_MOVES = new Set(["text", "email", "tel", "url", "search", "password", "number"]);
40
+ const cssLength = (value) => (typeof value === "number" ? `${value}px` : value);
41
+ /** How a cell lines up: numbers at the end, checkboxes and radios in the middle, the rest at the start. */
42
+ function cellKind(params) {
43
+ if (params.type === "number")
44
+ return "number";
45
+ if (FIXED_TYPES.has(params.type))
46
+ return "check";
47
+ return "text";
48
+ }
49
+ function columnWidth(params) {
50
+ return cssLength(params.width ?? COLUMN_WIDTH[params.type] ?? "12rem");
51
+ }
52
+ /** One row of inputs. It renders again only when its position, its messages, or a value set from code change. */
53
+ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, columns, context, onAction }) {
54
+ useRowVersion(store, rowKey);
55
+ const name = rowName(store, index);
56
+ const title = `${store.names.title} ${index + 1}`;
57
+ const invalid = context.showAll && store.rowProblems(rowKey) > 0;
58
+ const items = [];
59
+ if (context.canAdd) {
60
+ items.push({ value: "insert-above", label: "Insert above", icon: _jsx(InlineIcon, { icon: iconRowInsertTop }) }, { value: "insert-below", label: "Insert below", icon: _jsx(InlineIcon, { icon: iconRowInsertBottom }) }, { value: "duplicate", label: "Duplicate", icon: _jsx(InlineIcon, { icon: iconCopy }) });
61
+ }
62
+ if (context.sortable) {
63
+ if (items.length)
64
+ items.push({ separator: true });
65
+ items.push({ value: "move-up", label: "Move up", icon: _jsx(InlineIcon, { icon: iconArrowUp }), disabled: index === 0 }, { value: "move-down", label: "Move down", icon: _jsx(InlineIcon, { icon: iconArrowDown }), disabled: index === count - 1 });
66
+ }
67
+ if (context.removable) {
68
+ if (items.length)
69
+ items.push({ separator: true });
70
+ items.push({ value: "remove", label: "Remove", icon: _jsx(InlineIcon, { icon: iconTrash }), tone: "danger" });
71
+ }
72
+ // Leaving an input counts as using it: a repeated value shows its message from then on.
73
+ function onBlur(event) {
74
+ const cell = event.target.closest("[data-nx-cell]");
75
+ if (!cell || (event.relatedTarget instanceof Node && cell.contains(event.relatedTarget)))
76
+ return;
77
+ store.touch(rowKey, cell.dataset.nxCell ?? "");
78
+ }
79
+ return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsx("div", { role: "cell", className: "nx-tabular__num", children: items.length > (context.removable ? 1 : 0) ? (_jsx(Menu, { items: items, onSelect: value => onAction(value, rowKey), "aria-label": `${title} actions`, children: _jsxs("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${title} actions`, title: `${title} actions`, "data-nx-row-actions": "", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), _jsx(InlineIcon, { icon: iconDotsVertical, className: "nx-tabular__num-icon" })] }) })) : (_jsx("span", { className: "nx-tabular__num-text", children: index + 1 })) })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: params, index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled }, store.revision(rowKey, params.name)) }, params.name))), context.removable && (_jsx("div", { role: "cell", className: "nx-tabular__end", children: _jsx(Button, { variant: "ghost", size: "sm", hoverTone: "danger", className: "nx-tabular__remove", icon: _jsx(InlineIcon, { icon: iconX }), "aria-label": `Remove ${name}`, title: `Remove ${name}`, onClick: () => onAction("remove", rowKey) }) }))] }));
80
+ });
81
+ /**
82
+ * Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
83
+ * value is an array of row objects, one value per input, with any other fields the rows came with (such as ids). Inputs
84
+ * come as params, the same objects a form takes, so the same list can show as sections in a `GroupForm`.
85
+ *
86
+ * Only the rows in view are on the page, so a sheet of thousands of rows opens and types as fast as a short one; the
87
+ * values live in a store outside React, and typing renders nothing. The header, the row numbers, and the remove buttons
88
+ * stay in place while the table scrolls. A row number opens the row's menu (insert, duplicate, move, remove). Enter in
89
+ * a text or number cell moves down the column, and on the last row adds a row.
90
+ *
91
+ * Inside a `<form>`, the rows are sent as JSON under `name`; a row with a problem blocks submit, and the first one
92
+ * scrolls into view with focus.
93
+ *
94
+ * @example
95
+ * <TabularForm name="emails" label="Emails" itemLabel="Email" max={5} unique={["email"]} inputs={[
96
+ * { type: "email", name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
97
+ * { type: "dropdown", name: "type", label: "Type", options: EMAIL_TYPES },
98
+ * { type: "checkbox", name: "isVerified", label: "Verified" },
99
+ * { type: "radio", name: "isPrimary", label: "Primary", required: true },
100
+ * ]} />
101
+ */
102
+ export function TabularForm(props) {
103
+ const { inputs, label, hint, error, size = "md", maxHeight = DEFAULT_MAX_HEIGHT, numbered = true, readOnly = false, disabled = false, required, max, className, style } = props;
104
+ const addable = props.addable !== false && !readOnly;
105
+ const removable = props.removable !== false && !readOnly && !disabled;
106
+ const sortable = Boolean(props.sortable) && !readOnly && !disabled;
107
+ const layoutRef = useRef(null);
108
+ const rows = useRowsInput(props, "Row", layoutRef);
109
+ const store = rows.store;
110
+ const { snapshot } = rows;
111
+ const frame = useRowsFrame(props, store);
112
+ const columns = useMemo(() => inputs.filter(params => !params.hidden), [inputs]);
113
+ const count = snapshot.keys.length;
114
+ const viewportRef = useRef(null);
115
+ const headRef = useRef(null);
116
+ const bodyRef = useRef(null);
117
+ const [rowHeight, setRowHeight] = useState(ROW_HEIGHT[size]);
118
+ const pageRows = Math.ceil((typeof maxHeight === "number" ? maxHeight : 600) / rowHeight);
119
+ const [range, setRange] = useState({ start: 0, end: pageRows + OVERSCAN });
120
+ const [status, setStatus] = useState(null);
121
+ const pendingRef = useRef(null);
122
+ const start = Math.min(range.start, count);
123
+ const end = Math.min(count, Math.max(range.end, start + 1));
124
+ const shownKeys = snapshot.keys.slice(start, end);
125
+ /** The rows in view, from the scroll position, and a margin around them. */
126
+ const updateRange = useCallback(() => {
127
+ const viewport = viewportRef.current;
128
+ if (!viewport)
129
+ return;
130
+ const head = headRef.current?.offsetHeight ?? 0;
131
+ const top = Math.max(0, viewport.scrollTop - head);
132
+ const first = Math.max(0, Math.floor(top / rowHeight) - OVERSCAN);
133
+ const last = Math.ceil((top + Math.max(viewport.clientHeight - head, rowHeight)) / rowHeight) + OVERSCAN;
134
+ // Focus in a row that is about to leave the page moves to the table, so the keyboard keeps its place.
135
+ const active = document.activeElement;
136
+ const activeRow = active instanceof Element ? active.closest("[data-nx-row]") : null;
137
+ if (activeRow && bodyRef.current?.contains(activeRow)) {
138
+ const index = store.indexOf(activeRow.dataset.nxRow ?? "");
139
+ if (index >= 0 && (index < first || index >= last))
140
+ viewport.focus({ preventScroll: true });
141
+ }
142
+ setRange(current => (current.start === first && current.end === last ? current : { start: first, end: last }));
143
+ }, [rowHeight, store]);
144
+ // The real row height, once a row is on the page (custom styles may change it).
145
+ useLayoutEffect(() => {
146
+ const row = bodyRef.current?.querySelector(":scope > .nx-tabular__row");
147
+ const height = row?.offsetHeight;
148
+ if (height && height !== rowHeight)
149
+ setRowHeight(height);
150
+ });
151
+ // A resize, a new row height, or fewer rows change which rows are in view.
152
+ useLayoutEffect(() => {
153
+ const viewport = viewportRef.current;
154
+ if (!viewport)
155
+ return;
156
+ updateRange();
157
+ const observer = new ResizeObserver(updateRange);
158
+ observer.observe(viewport);
159
+ return () => observer.disconnect();
160
+ }, [updateRange, count]);
161
+ function tryFocus() {
162
+ const pending = pendingRef.current;
163
+ const body = bodyRef.current;
164
+ if (!pending || !body)
165
+ return;
166
+ const row = body.querySelector(`:scope > [data-nx-row="${CSS.escape(pending.key)}"]`);
167
+ if (!row)
168
+ return;
169
+ pendingRef.current = null;
170
+ const { target } = pending;
171
+ let element;
172
+ if (typeof target === "string")
173
+ element = firstFocusable(row.querySelector(`[data-nx-cell="${CSS.escape(target)}"]`));
174
+ else if ("actions" in target)
175
+ element = row.querySelector("[data-nx-row-actions]") ?? firstFocusable(row);
176
+ else
177
+ element = firstFocusable(row.querySelector("[data-nx-cell]")) ?? firstFocusable(row);
178
+ element?.focus();
179
+ }
180
+ layoutRef.current = {
181
+ focusRow(key, target) {
182
+ const index = store.indexOf(key);
183
+ const viewport = viewportRef.current;
184
+ if (index < 0 || !viewport)
185
+ return;
186
+ pendingRef.current = { key, target };
187
+ // Scroll the row into view below the header, and render the rows around it before the scroll event comes.
188
+ const head = headRef.current?.offsetHeight ?? 0;
189
+ const top = index * rowHeight;
190
+ if (top < viewport.scrollTop)
191
+ viewport.scrollTop = top;
192
+ else if (head + top + rowHeight > viewport.scrollTop + viewport.clientHeight)
193
+ viewport.scrollTop = head + top + rowHeight - viewport.clientHeight;
194
+ setRange(current => (index >= current.start && index < current.end ? current : { start: Math.max(0, index - OVERSCAN), end: index + pageRows + OVERSCAN }));
195
+ // After the render that brings the row (a layout effect), or in the next frame when nothing renders.
196
+ requestAnimationFrame(tryFocus);
197
+ },
198
+ };
199
+ useLayoutEffect(tryFocus);
200
+ const actionsRef = useRef(rows.actions);
201
+ actionsRef.current = rows.actions;
202
+ const onAction = useCallback((action, key) => {
203
+ const index = store.indexOf(key);
204
+ const run = actionsRef.current;
205
+ if (action === "insert-above")
206
+ run.add(undefined, index);
207
+ else if (action === "insert-below")
208
+ run.add(undefined, index + 1);
209
+ else if (action === "duplicate")
210
+ run.duplicate(key);
211
+ else if (action === "move-up")
212
+ run.move(key, -1);
213
+ else if (action === "move-down")
214
+ run.move(key, 1);
215
+ else
216
+ run.remove(key);
217
+ }, [store]);
218
+ /** The message or hint of the input with focus, shown under the table: cells have no room for their own. */
219
+ function readStatus() {
220
+ const active = document.activeElement;
221
+ const cell = active instanceof Element ? active.closest("[data-nx-cell]") : null;
222
+ const row = cell?.closest("[data-nx-row]");
223
+ let next = null;
224
+ if (cell && row && bodyRef.current?.contains(cell)) {
225
+ const message = cell.querySelector(".nx-field__message");
226
+ const text = message?.textContent?.trim() ?? "";
227
+ const isError = Boolean(message?.classList.contains("nx-field__message--error"));
228
+ const params = columns.find(item => item.name === cell.dataset.nxCell);
229
+ if (text)
230
+ next = { text: isError ? `${store.names.title} ${store.indexOf(row.dataset.nxRow ?? "") + 1}: ${text}` : `${params ? labelText(params) : ""}: ${text}`, error: isError };
231
+ }
232
+ setStatus(current => (current?.text === next?.text && current?.error === next?.error ? current : next));
233
+ }
234
+ const readStatusSoon = () => requestAnimationFrame(readStatus);
235
+ useLayoutEffect(() => {
236
+ if (status)
237
+ readStatus();
238
+ });
239
+ /** Enter moves down the column, Shift+Enter up; on the last row Enter adds a row. */
240
+ function onBodyKeyDown(event) {
241
+ if (event.key !== "Enter" || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
242
+ return;
243
+ const target = event.target;
244
+ if (!(target instanceof HTMLInputElement) || !ENTER_MOVES.has(target.type) || target.hasAttribute("aria-haspopup") || target.getAttribute("role") === "combobox")
245
+ return;
246
+ const cell = target.closest("[data-nx-cell]");
247
+ const row = target.closest("[data-nx-row]");
248
+ if (!cell || !row)
249
+ return;
250
+ event.preventDefault();
251
+ const name = cell.dataset.nxCell ?? "";
252
+ const index = store.indexOf(row.dataset.nxRow ?? "") + (event.shiftKey ? -1 : 1);
253
+ if (index < 0)
254
+ return;
255
+ const next = store.keyAt(index);
256
+ if (next)
257
+ layoutRef.current?.focusRow(next, name);
258
+ else if (!event.shiftKey && rows.canAdd) {
259
+ const key = store.add();
260
+ layoutRef.current?.focusRow(key, name);
261
+ }
262
+ }
263
+ const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll]);
264
+ const numberWidth = `calc(${Math.max(2, String(count).length)}ch + 24px)`;
265
+ const widths = columns.map(columnWidth);
266
+ const template = [
267
+ numbered ? numberWidth : null,
268
+ ...columns.map((params, index) => {
269
+ const grow = params.grow ?? (FIXED_TYPES.has(params.type) ? 0 : 1);
270
+ return grow > 0 ? `minmax(${widths[index]}, ${grow}fr)` : widths[index];
271
+ }),
272
+ removable ? "var(--nx-tabular-end)" : null,
273
+ ]
274
+ .filter(Boolean)
275
+ .join(" ");
276
+ const minWidth = `calc(${[numbered ? numberWidth : "0px", ...widths, removable ? "var(--nx-tabular-end)" : "0px"].join(" + ")})`;
277
+ const tableStyle = { "--nx-tabular-columns": template, minWidth };
278
+ const invalid = Boolean(error || snapshot.message || snapshot.invalidRows);
279
+ return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-tabular", `nx-tabular--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-describedby": frame.messageId, children: [_jsx(RowsLegend, { id: frame.legendId, label: label, required: required || (props.min ?? 0) > 0, lock: frame.lock }), _jsx("input", { ref: rows.proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", form: props.form, onInvalid: rows.onInvalid }), _jsx("div", { ref: viewportRef, className: "nx-tabular__viewport", tabIndex: -1, style: { maxHeight: cssLength(maxHeight) }, onScroll: updateRange, children: _jsxs("div", { role: "table", className: "nx-tabular__table", style: tableStyle, "aria-labelledby": label ? frame.legendId : undefined, "aria-label": label ? undefined : props["aria-label"], "aria-rowcount": count + 1, children: [_jsx("div", { ref: headRef, role: "rowgroup", className: "nx-tabular__head", children: _jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": 1, children: [numbered && (_jsxs("div", { role: "columnheader", className: "nx-tabular__num", children: [_jsx("span", { "aria-hidden": "true", children: "#" }), _jsx("span", { className: "nx-visually-hidden", children: store.names.title })] })), columns.map(params => {
280
+ const columnHint = params.hint;
281
+ return (_jsxs("div", { role: "columnheader", className: cx("nx-tabular__head-cell", `nx-tabular__cell--${cellKind(params)}`), title: typeof columnHint === "string" ? columnHint : undefined, children: [_jsx("span", { className: "nx-tabular__head-label", children: params.label ?? params.name }), params.required && (_jsx("span", { className: "nx-field__required", "aria-hidden": "true", children: "*" }))] }, params.name));
282
+ }), removable && (_jsx("div", { role: "columnheader", className: "nx-tabular__end", children: _jsx("span", { className: "nx-visually-hidden", children: "Remove" }) }))] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status: status, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
283
+ }