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,289 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { iconBlockquote, iconBold, iconCode, iconEye, iconHeading, iconItalic, iconLayoutColumns, iconLink, iconList, iconListCheck, iconListNumbers, iconMarkdown, iconPencil, iconPhoto, iconSeparatorHorizontal, iconSourceCode, iconStrikethrough, iconTable, InlineIcon, } from "nexus-icons";
4
+ import { startTransition, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react";
5
+ import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { applyMarkdownTool, continueMarkdownList, linkPastedUrl, toggleMarkdownTask } from "../../Helpers/MarkdownEditing.js";
7
+ import { TabButtons } from "../Documents/TabButtons.js";
8
+ import { MarkdownView } from "../Viewers/MarkdownView.js";
9
+ import { EditorCount, useEditorShell, writeEditorCount } from "./EditorShell.js";
10
+ import { Field } from "./Field.js";
11
+ import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
12
+ import { useTextField } from "./TextField.js";
13
+ /** The default toolbar: text marks, then inserts, then lists, then blocks. */
14
+ export const MARKDOWN_TOOLBAR = [
15
+ "heading",
16
+ "bold",
17
+ "italic",
18
+ "strikethrough",
19
+ "|",
20
+ "quote",
21
+ "code",
22
+ "codeBlock",
23
+ "link",
24
+ "image",
25
+ "|",
26
+ "bulletList",
27
+ "numberedList",
28
+ "taskList",
29
+ "|",
30
+ "table",
31
+ "rule",
32
+ ];
33
+ const MODES = {
34
+ write: { label: "Write", icon: iconPencil },
35
+ split: { label: "Split", icon: iconLayoutColumns },
36
+ preview: { label: "Preview", icon: iconEye },
37
+ };
38
+ const TOOLS = {
39
+ heading: { label: "Heading", icon: iconHeading },
40
+ bold: { label: "Bold", icon: iconBold, keys: "Control+B" },
41
+ italic: { label: "Italic", icon: iconItalic, keys: "Control+I" },
42
+ strikethrough: { label: "Strikethrough", icon: iconStrikethrough, keys: "Control+Shift+X" },
43
+ quote: { label: "Quote", icon: iconBlockquote, keys: "Control+Shift+." },
44
+ code: { label: "Code", icon: iconCode, keys: "Control+E" },
45
+ codeBlock: { label: "Code block", icon: iconSourceCode },
46
+ link: { label: "Link", icon: iconLink, keys: "Control+K" },
47
+ image: { label: "Image", icon: iconPhoto },
48
+ bulletList: { label: "Bulleted list", icon: iconList, keys: "Control+Shift+8" },
49
+ numberedList: { label: "Numbered list", icon: iconListNumbers, keys: "Control+Shift+7" },
50
+ taskList: { label: "Task list", icon: iconListCheck, keys: "Control+Shift+L" },
51
+ table: { label: "Table", icon: iconTable },
52
+ rule: { label: "Horizontal line", icon: iconSeparatorHorizontal },
53
+ };
54
+ const ALL_MODES = ["write", "split", "preview"];
55
+ /**
56
+ * A Markdown editor: write, split, and preview views, a toolbar with shortcuts, lists that continue on Enter, a pasted
57
+ * URL that links the selected text, a height handle that drags, steps with the arrow keys, and can remember its height,
58
+ * and a full-screen view. The text area is uncontrolled by default, so typing re-renders nothing but the preview, and
59
+ * the preview renders at low priority, so typing stays smooth in long documents. Toolbar edits keep the browser's undo.
60
+ *
61
+ * @example <MarkdownEditor name="description" label="Description" defaultMode="split" heightKey="room-description" maxLength={5000} />
62
+ */
63
+ export function MarkdownEditor(props) {
64
+ const { label, hint, error, defaultMode = "write", mode: modeProp, onModeChange, modes = ALL_MODES, toolbar = MARKDOWN_TOOLBAR, height = 280, minHeight = 120, maxHeight = 960, resizable = true, heightKey, fullscreen: canFullscreen = true, breaks = true, monospace = false, showCount = true, emptyPreview = "Nothing to preview", readOnlyMessage = "Read-only: this text cannot be changed.", fieldName, validateOn, onValueChange, className, textAreaClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, onPaste, onScroll, ...textAreaProps } = props;
65
+ const [modeState, setModeState] = useState(() => (modes.includes(defaultMode) ? defaultMode : (modes[0] ?? "write")));
66
+ const mode = modeProp ?? modeState;
67
+ const previewRef = useRef(null);
68
+ const countRef = useRef(null);
69
+ const frameRef = useRef(0);
70
+ const focusTextRef = useRef(false);
71
+ // The side whose scroll was just set to follow the other, so its own scroll event is not echoed back.
72
+ const followRef = useRef({ element: null, at: 0 });
73
+ const field = useTextField({
74
+ ...props,
75
+ showCount: false,
76
+ onChange: event => {
77
+ onChange?.(event);
78
+ textChanged();
79
+ },
80
+ });
81
+ // A lock at the end of the bar, and a note under the bar when someone tries to change the text.
82
+ const readOnlyNotice = useReadOnlyNotice(Boolean(readOnly) && !disabled, field.elementRef, readOnlyMessage, {
83
+ keys: ["Enter"],
84
+ anchor: target => target.closest(".nx-editor__frame")?.querySelector(".nx-editor__bar") ?? null,
85
+ align: "end",
86
+ });
87
+ const tools = new Set(toolbar.filter((item) => item !== "|"));
88
+ const firstTool = toolbar.findIndex(item => item !== "|");
89
+ const editable = !readOnly && !disabled;
90
+ const shell = useEditorShell({
91
+ height,
92
+ minHeight,
93
+ maxHeight,
94
+ resizable,
95
+ storageKey: heightKey && `nx-md-height:${heightKey}`,
96
+ fullscreen: canFullscreen,
97
+ disabled,
98
+ focusTarget: box => box.querySelector(mode === "preview" ? ".nx-md__preview" : "textarea"),
99
+ });
100
+ const initialText = () => field.elementRef.current?.value ?? String(props.value ?? props.defaultValue ?? "");
101
+ /** Counts and the preview follow the text once per frame, however fast it changes. */
102
+ function textChanged() {
103
+ cancelAnimationFrame(frameRef.current);
104
+ frameRef.current = requestAnimationFrame(() => {
105
+ const text = field.elementRef.current?.value ?? "";
106
+ writeEditorCount(countRef.current, text, maxLength);
107
+ previewRef.current?.update(text);
108
+ });
109
+ }
110
+ useEffect(() => () => cancelAnimationFrame(frameRef.current), []);
111
+ // A controlled value set by the page, or a form reset, changes the text without typing.
112
+ useEffect(() => {
113
+ if (props.value !== undefined)
114
+ textChanged();
115
+ }, [props.value]);
116
+ useEffect(() => {
117
+ const form = field.elementRef.current?.form;
118
+ if (!form)
119
+ return;
120
+ const onReset = () => setTimeout(textChanged);
121
+ form.addEventListener("reset", onReset);
122
+ return () => form.removeEventListener("reset", onReset);
123
+ }, []);
124
+ function changeMode(next) {
125
+ if (next === mode)
126
+ return;
127
+ if (modeProp === undefined)
128
+ setModeState(next);
129
+ onModeChange?.(next);
130
+ }
131
+ // A submit that finds the text invalid while only the preview shows brings the text back and focuses it.
132
+ useLayoutEffect(() => {
133
+ if (!focusTextRef.current || mode === "preview")
134
+ return;
135
+ focusTextRef.current = false;
136
+ field.elementRef.current?.focus();
137
+ }, [mode]);
138
+ // ---- Editing ----
139
+ function runTool(tool) {
140
+ const element = field.elementRef.current;
141
+ if (element && editable)
142
+ applyEdit(element, applyMarkdownTool(element.value, element.selectionStart, element.selectionEnd, tool));
143
+ }
144
+ function toggleTask(line, checked) {
145
+ const element = field.elementRef.current;
146
+ const edit = element && editable ? toggleMarkdownTask(element.value, line, checked) : null;
147
+ if (element && edit)
148
+ applyEdit(element, edit, false);
149
+ }
150
+ function handleKeyDown(event) {
151
+ readOnlyNotice.onKeyDown(event);
152
+ onKeyDown?.(event);
153
+ if (event.defaultPrevented || event.isPropagationStopped() || !editable || event.nativeEvent.isComposing)
154
+ return;
155
+ const element = event.currentTarget;
156
+ const plainEnter = event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey;
157
+ const tool = shortcutTool(event);
158
+ const edit = plainEnter
159
+ ? continueMarkdownList(element.value, element.selectionStart, element.selectionEnd)
160
+ : tool && tools.has(tool)
161
+ ? applyMarkdownTool(element.value, element.selectionStart, element.selectionEnd, tool)
162
+ : null;
163
+ if (!edit)
164
+ return;
165
+ event.preventDefault();
166
+ applyEdit(element, edit);
167
+ }
168
+ function handlePaste(event) {
169
+ onPaste?.(event);
170
+ if (event.defaultPrevented || !editable)
171
+ return;
172
+ const element = event.currentTarget;
173
+ const edit = linkPastedUrl(element.value, element.selectionStart, element.selectionEnd, event.clipboardData.getData("text/plain"));
174
+ if (!edit)
175
+ return;
176
+ event.preventDefault();
177
+ applyEdit(element, edit);
178
+ }
179
+ function handleClick(event) {
180
+ readOnlyNotice.onClick(event);
181
+ onClick?.(event);
182
+ }
183
+ function handleBlur(event) {
184
+ readOnlyNotice.hide();
185
+ field.controlProps.onBlur(event);
186
+ }
187
+ function handleInvalid(event) {
188
+ if (mode === "preview") {
189
+ const view = modes.find(option => option !== "preview");
190
+ if (view) {
191
+ focusTextRef.current = true;
192
+ changeMode(view);
193
+ }
194
+ }
195
+ field.controlProps.onInvalid(event);
196
+ }
197
+ // ---- Scrolling in split view ----
198
+ /** Scrolls `to` to the same share of its length as `from`. */
199
+ function follow(from, to) {
200
+ if (!to || mode !== "split")
201
+ return;
202
+ const last = followRef.current;
203
+ if (last.element === from && performance.now() - last.at < 100)
204
+ return;
205
+ const range = from.scrollHeight - from.clientHeight;
206
+ followRef.current = { element: to, at: performance.now() };
207
+ to.scrollTop = range > 0 ? (from.scrollTop / range) * (to.scrollHeight - to.clientHeight) : 0;
208
+ }
209
+ function handleScroll(event) {
210
+ onScroll?.(event);
211
+ follow(event.currentTarget, previewRef.current?.element);
212
+ }
213
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, hint: hint, error: field.error, disabled: disabled, readOnly: readOnly, multiline: true, className: cx("nx-editor", "nx-md", monospace && "nx-md--mono", className), style: style, children: _jsxs("div", { ref: shell.frameRef, className: "nx-editor__frame", "data-fullscreen": shell.fullscreen || undefined, onKeyDown: shell.onFrameKeyDown, children: [_jsxs("div", { className: "nx-editor__bar", children: [modes.length > 1 && (_jsx(TabButtons, { "aria-label": "View", size: "sm", labels: "selected", options: modes.map(option => ({ value: option, label: MODES[option].label, icon: _jsx(InlineIcon, { icon: MODES[option].icon }), disabled })), value: mode, onChange: changeMode,
214
+ // An id that matches no form: the view is never sent with the form around the editor.
215
+ form: `${field.controlId}-view`, className: "nx-editor__modes" })), editable && mode !== "preview" && firstTool >= 0 && (_jsx("div", { role: "toolbar", "aria-label": "Formatting", "aria-controls": field.controlId, className: "nx-editor__tools", onKeyDown: shell.moveTool, children: toolbar.map((item, index) => item === "|" ? (_jsx("span", { className: "nx-editor__divider", "aria-hidden": "true" }, index)) : (_jsx("button", { type: "button", className: "nx-editor__tool", tabIndex: index === firstTool ? 0 : -1, "aria-label": TOOLS[item].label, "aria-keyshortcuts": TOOLS[item].keys, title: TOOLS[item].keys ? `${TOOLS[item].label} (${TOOLS[item].keys.replace("Control", "Ctrl")})` : TOOLS[item].label,
216
+ // The text area keeps focus and its selection.
217
+ onMouseDown: event => event.preventDefault(), onClick: () => runTool(item), children: _jsx(InlineIcon, { icon: TOOLS[item].icon }) }, index))) })), _jsxs("span", { className: "nx-editor__end", children: [readOnlyNotice.lock, shell.fullscreenButton] })] }), _jsxs("div", { ref: shell.bodyRef, className: "nx-editor__body", "data-mode": mode, style: shell.bodyStyle, children: [_jsx("textarea", { ...textAreaProps, ...field.controlProps, className: cx("nx-field__control", "nx-md__input", textAreaClassName), disabled: disabled, readOnly: readOnly, onClick: handleClick, onBlur: handleBlur, onKeyDown: handleKeyDown, onPaste: handlePaste, onInvalid: handleInvalid, onScroll: handleScroll }), mode !== "write" && (_jsx(MarkdownPreview, { ref: previewRef, initialText: initialText(), breaks: breaks, empty: emptyPreview, onTaskToggle: editable ? toggleTask : undefined, onScroll: event => follow(event.currentTarget, field.elementRef.current), onRender: () => {
218
+ if (field.elementRef.current)
219
+ follow(field.elementRef.current, previewRef.current?.element);
220
+ } }))] }), shell.footer(_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconMarkdown }), _jsx("span", { children: "Markdown" })] }), showCount && _jsx(EditorCount, { ref: countRef, text: initialText(), maxLength: maxLength }))] }) }), readOnlyNotice.notice] }));
221
+ }
222
+ /**
223
+ * The preview, with its own state, so a change in the text re-renders only the preview. Updates render as a
224
+ * transition: typing never waits for a long document to render, and a newer text replaces one still rendering.
225
+ */
226
+ function MarkdownPreview({ ref, initialText, breaks, empty, onTaskToggle, onScroll, onRender }) {
227
+ const [text, setText] = useState(initialText);
228
+ const elementRef = useRef(null);
229
+ useImperativeHandle(ref, () => ({
230
+ update: next => startTransition(() => setText(next)),
231
+ get element() {
232
+ return elementRef.current;
233
+ },
234
+ }), []);
235
+ // New content changes the preview's length; keep it lined up with the text.
236
+ useLayoutEffect(() => {
237
+ onRender();
238
+ }, [text]);
239
+ return (_jsx("div", { ref: elementRef, className: "nx-md__preview", role: "region", "aria-label": "Preview", tabIndex: 0, onScroll: onScroll, children: _jsx(MarkdownView, { value: text, breaks: breaks, newTab: "all", placeholder: empty, onTaskToggle: onTaskToggle }) }));
240
+ }
241
+ /**
242
+ * Applies an edit as typing would: through the browser's own editing, so Ctrl+Z undoes it, and with an input event,
243
+ * so React's onChange and the field's validation run. Without focus (a task ticked in the preview), or where the
244
+ * browser cannot, the text is set directly.
245
+ */
246
+ function applyEdit(element, edit, focus = true) {
247
+ const { selectionStart, selectionEnd, scrollTop } = element;
248
+ if (edit.start === edit.end && !edit.text)
249
+ return;
250
+ let done = false;
251
+ if (focus) {
252
+ element.focus({ preventScroll: true });
253
+ // Only when the text area has focus: the command edits whatever is focused.
254
+ if (document.activeElement === element) {
255
+ element.setSelectionRange(edit.start, edit.end);
256
+ done = document.execCommand(edit.text ? "insertText" : "delete", false, edit.text);
257
+ }
258
+ }
259
+ if (!done) {
260
+ element.setRangeText(edit.text, edit.start, edit.end, "end");
261
+ element.dispatchEvent(new Event("input", { bubbles: true }));
262
+ }
263
+ element.setSelectionRange(edit.selectionStart ?? selectionStart, edit.selectionEnd ?? selectionEnd);
264
+ if (!focus)
265
+ element.scrollTop = scrollTop;
266
+ }
267
+ /** The toolbar tool for a shortcut, by key position for the shifted digits so it works on any keyboard layout. */
268
+ function shortcutTool(event) {
269
+ if (!(event.ctrlKey || event.metaKey) || event.altKey)
270
+ return null;
271
+ if (!event.shiftKey) {
272
+ const key = event.key.toLowerCase();
273
+ return key === "b" ? "bold" : key === "i" ? "italic" : key === "k" ? "link" : key === "e" ? "code" : null;
274
+ }
275
+ switch (event.code) {
276
+ case "Digit7":
277
+ return "numberedList";
278
+ case "Digit8":
279
+ return "bulletList";
280
+ case "Period":
281
+ return "quote";
282
+ case "KeyL":
283
+ return "taskList";
284
+ case "KeyX":
285
+ return "strikethrough";
286
+ default:
287
+ return null;
288
+ }
289
+ }
@@ -0,0 +1,53 @@
1
+ import { type ChangeEvent, type ComponentPropsWithRef, type ReactNode } from "react";
2
+ import type { InputDesign, NumberFormatOptions, NumberRules, ValidateOn } from "../../Interfaces/InputInterfaces.ts";
3
+ import type { Size } from "../../Interfaces/TypeInterfaces.ts";
4
+ type NativeInputProps = Omit<ComponentPropsWithRef<"input">, "type" | "size" | "value" | "defaultValue" | "min" | "max" | "step" | "prefix" | "pattern" | "minLength" | "maxLength" | "children" | keyof NumberRules>;
5
+ export interface NumberBoxProps extends NativeInputProps, NumberRules, NumberFormatOptions {
6
+ label?: ReactNode;
7
+ /** Help text under the box. */
8
+ hint?: ReactNode;
9
+ /** An error from outside, such as a server message. Shown before the field's own message. */
10
+ error?: string | null;
11
+ /** Default `standard`. */
12
+ design?: InputDesign;
13
+ /** Default `md`. */
14
+ size?: Size;
15
+ /** Decorative icon at the start. */
16
+ icon?: ReactNode;
17
+ /** Decorative icon at the end. */
18
+ iconEnd?: ReactNode;
19
+ /** Text before the value inside the box, such as a currency: "Rs". */
20
+ prefix?: ReactNode;
21
+ /** Text after the value inside the box, such as a unit: "kg", "%", "nights". */
22
+ suffix?: ReactNode;
23
+ /** Amount the arrow keys and the − and + buttons add. Default 1. Shift, Page Up, and Page Down move 10 steps. */
24
+ step?: number;
25
+ /** Shows − and + buttons: `true` puts both at the end, `"split"` puts − before and + after a centered value. Hold to repeat. */
26
+ stepper?: boolean | "split";
27
+ /** On blur, moves a value outside `min` or `max` to the nearest limit instead of showing a message. */
28
+ clamp?: boolean;
29
+ /** What a read-only field says when someone tries to change it. Default "Read-only: this number cannot be changed." */
30
+ readOnlyMessage?: string;
31
+ /** Name used in messages, e.g. "Nights must be at least 1." Default: the label, when it is text. */
32
+ fieldName?: string;
33
+ /** When the field shows its own message. Default `blur`; submitting a form always shows it. */
34
+ validateOn?: ValidateOn;
35
+ /** The number, or `null` for empty. With `value`, the box follows the prop and keeps partial text such as "12." while typing. */
36
+ value?: number | null;
37
+ defaultValue?: number | null;
38
+ /** The new number after each change, or `null` when the box is empty. */
39
+ onValueChange?: (value: number | null, event: ChangeEvent<HTMLInputElement>) => void;
40
+ /** Class of the outer field. `inputClassName` goes on the input. */
41
+ className?: string;
42
+ inputClassName?: string;
43
+ }
44
+ /**
45
+ * A number input. Only digits, one decimal separator, and a minus sign (when `min` allows it) can be typed, so the value
46
+ * is always a number or empty. Pasted text such as "Rs 1,250.50" keeps just the number. Values round to `decimals` on
47
+ * blur, and `grouping` shows 1,250,000 while the box is not being edited. Arrow keys and the optional − and + buttons step.
48
+ * Uncontrolled by default, so typing re-renders nothing. Inside a `<form>`, the value is sent without group separators.
49
+ *
50
+ * @example <NumberBox label="Amount" prefix="Rs" min={0} decimals={2} fixedDecimals grouping required />
51
+ */
52
+ export declare function NumberBox(props: NumberBoxProps): import("react").JSX.Element;
53
+ export {};
@@ -0,0 +1,228 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { InlineIcon, iconMinus, iconPlus } from "nexus-icons";
4
+ import { useEffect, useLayoutEffect, useRef, } from "react";
5
+ import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { clampNumber, cleanNumberText, DEFAULT_NUMBER_LOCALE, formatNumber, getNumberSymbols, isPartialNumber, parseNumber, readPastedNumber, roundDecimal, stepNumber, } from "../../Helpers/NumberHelpers.js";
7
+ import { validateNumber } from "../../Helpers/ValidationHelpers.js";
8
+ import { Field, hasContent } from "./Field.js";
9
+ import { setInputValue, useInputField } from "./InputField.js";
10
+ import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
11
+ // Holding a − or + button: one step, a pause, then repeats that come sooner each time, down to the fastest pace.
12
+ const REPEAT_WAIT = 400;
13
+ const REPEAT_START = 150;
14
+ const REPEAT_FASTEST = 40;
15
+ const REPEAT_ACCELERATION = 0.85;
16
+ const isNumber = (value) => value !== null && !Number.isNaN(value);
17
+ /**
18
+ * A number input. Only digits, one decimal separator, and a minus sign (when `min` allows it) can be typed, so the value
19
+ * is always a number or empty. Pasted text such as "Rs 1,250.50" keeps just the number. Values round to `decimals` on
20
+ * blur, and `grouping` shows 1,250,000 while the box is not being edited. Arrow keys and the optional − and + buttons step.
21
+ * Uncontrolled by default, so typing re-renders nothing. Inside a `<form>`, the value is sent without group separators.
22
+ *
23
+ * @example <NumberBox label="Amount" prefix="Rs" min={0} decimals={2} fixedDecimals grouping required />
24
+ */
25
+ export function NumberBox(props) {
26
+ const { label, hint, error, design = "standard", size = "md", icon, iconEnd, prefix, suffix, step = 1, stepper = false, clamp = false, readOnlyMessage = "Read-only: this number cannot be changed.", fieldName, validateOn, value, defaultValue, onValueChange, decimals, fixedDecimals = false, grouping = false, locale = DEFAULT_NUMBER_LOCALE, className, inputClassName, style, required, min, max, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onPaste, onClick, ...inputProps } = props;
27
+ const format = { decimals, fixedDecimals, grouping, locale };
28
+ const { decimal, group } = getNumberSymbols(locale);
29
+ const textRules = { decimals, negative: min === undefined || min < 0 };
30
+ const editable = !disabled && !readOnly;
31
+ const initial = value !== undefined ? value : (defaultValue ?? null);
32
+ // Grouped text ("1,250") is for reading; with a name, a hidden input sends the plain number ("1250") with the form.
33
+ const sendsHidden = grouping && Boolean(name);
34
+ const hiddenRef = useRef(null);
35
+ const minusRef = useRef(null);
36
+ const plusRef = useRef(null);
37
+ const repeatRef = useRef(undefined);
38
+ const displayText = (number) => formatNumber(number, format);
39
+ const editText = (number) => formatNumber(number, { ...format, grouping: false });
40
+ const atLimit = (number, direction) => isNumber(number) && (direction < 0 ? min !== undefined && number <= min : max !== undefined && number >= max);
41
+ const field = useInputField({
42
+ ...props,
43
+ onChange: event => {
44
+ const number = parseNumber(event.currentTarget.value, locale);
45
+ sync(number);
46
+ onChange?.(event);
47
+ onValueChange?.(isNumber(number) ? number : null, event);
48
+ },
49
+ }, {
50
+ check: (element, messageName) => validateNumber(parseNumber(element.value, locale), { required, min, max, validate }, messageName, limit => formatNumber(limit, { locale, grouping: true })),
51
+ checkKeys: [value, required, min, max, validate, locale],
52
+ onReset: element => sync(parseNumber(element.value, locale)),
53
+ });
54
+ // Read-only has no − and + buttons: a lock takes their place, and a note under the box says why nothing changes.
55
+ const readOnlyNotice = useReadOnlyNotice(Boolean(readOnly) && !disabled, field.elementRef, readOnlyMessage, {
56
+ keys: ["ArrowUp", "ArrowDown", "PageUp", "PageDown"],
57
+ });
58
+ /** Writes what depends on the value straight to the DOM, so typing re-renders nothing: the hidden input and the button limits. */
59
+ function sync(number) {
60
+ if (hiddenRef.current)
61
+ hiddenRef.current.value = isNumber(number) ? String(number) : "";
62
+ for (const [button, direction] of [
63
+ [minusRef.current, -1],
64
+ [plusRef.current, 1],
65
+ ]) {
66
+ if (atLimit(number, direction))
67
+ button?.setAttribute("aria-disabled", "true");
68
+ else
69
+ button?.removeAttribute("aria-disabled");
70
+ }
71
+ }
72
+ // A controlled value that differs from the box's text replaces it. Text that already means this number ("12.") stays.
73
+ useLayoutEffect(() => {
74
+ const input = field.elementRef.current;
75
+ if (!input || value === undefined || parseNumber(input.value, locale) === value)
76
+ return;
77
+ input.value = document.activeElement === input ? editText(value) : displayText(value);
78
+ sync(value);
79
+ }, [value]);
80
+ // React sets a hidden input's default value again on every render, and for a hidden input that is its value.
81
+ // After each render, before paint, the hidden input takes the box's number back.
82
+ useLayoutEffect(() => {
83
+ const input = field.elementRef.current;
84
+ if (sendsHidden && input)
85
+ sync(parseNumber(input.value, locale));
86
+ });
87
+ useEffect(() => () => clearTimeout(repeatRef.current), []);
88
+ /** Inserts text at the selection the way typing does, so onChange runs. */
89
+ function insertText(input, text, start, end) {
90
+ input.setRangeText(text, start, end, "end");
91
+ input.dispatchEvent(new Event("input", { bubbles: true }));
92
+ }
93
+ /** Moves the value one step (times `multiplier`). Returns false when it cannot move, at a limit. */
94
+ function stepBy(direction, multiplier = 1) {
95
+ const input = field.elementRef.current;
96
+ if (!input || !editable)
97
+ return false;
98
+ const current = parseNumber(input.value, locale);
99
+ const next = stepNumber(isNumber(current) ? current : null, direction, { step: step * multiplier, min, max, decimals });
100
+ if (next === current)
101
+ return false;
102
+ setInputValue(input, document.activeElement === input ? editText(next) : displayText(next));
103
+ return true;
104
+ }
105
+ function handleKeyDown(event) {
106
+ const input = event.currentTarget;
107
+ const { key } = event;
108
+ readOnlyNotice.onKeyDown(event);
109
+ if (editable && (key === "ArrowUp" || key === "ArrowDown" || key === "PageUp" || key === "PageDown")) {
110
+ event.preventDefault();
111
+ const direction = key === "ArrowUp" || key === "PageUp" ? 1 : -1;
112
+ if (stepBy(direction, key.startsWith("Page") || event.shiftKey ? 10 : 1))
113
+ field.touch();
114
+ }
115
+ else if (editable && key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
116
+ // Blocks a character that cannot be part of the number. "." and "," both type the decimal separator,
117
+ // except the one that is the locale's group separator, which would change the number's meaning.
118
+ const char = (key === "." || key === ",") && key !== group ? decimal : key;
119
+ const start = input.selectionStart ?? input.value.length;
120
+ const end = input.selectionEnd ?? start;
121
+ const next = input.value.slice(0, start) + char + input.value.slice(end);
122
+ if (!isPartialNumber(next, decimal, textRules)) {
123
+ event.preventDefault();
124
+ }
125
+ else if (char !== key) {
126
+ event.preventDefault();
127
+ insertText(input, char, start, end);
128
+ }
129
+ }
130
+ onKeyDown?.(event);
131
+ }
132
+ function handlePaste(event) {
133
+ onPaste?.(event);
134
+ if (!editable || event.defaultPrevented)
135
+ return;
136
+ event.preventDefault();
137
+ const input = event.currentTarget;
138
+ const pasted = readPastedNumber(event.clipboardData.getData("text/plain"), locale);
139
+ if (pasted === null)
140
+ return;
141
+ const start = input.selectionStart ?? input.value.length;
142
+ const end = input.selectionEnd ?? start;
143
+ const next = input.value.slice(0, start) + pasted + input.value.slice(end);
144
+ // A paste that would not make one number ("1.5" into "2.5") is dropped rather than guessed at.
145
+ if (!isPartialNumber(next, decimal, { ...textRules, decimals: undefined }))
146
+ return;
147
+ const number = parseNumber(next, locale);
148
+ if (decimals !== undefined && isNumber(number) && !isPartialNumber(next, decimal, textRules)) {
149
+ setInputValue(input, editText(roundDecimal(number, decimals)));
150
+ }
151
+ else {
152
+ insertText(input, pasted, start, end);
153
+ }
154
+ }
155
+ function handleChange(event) {
156
+ // Text that skipped the key and paste checks (an input method, autofill, a drop) loses what is not a number.
157
+ const input = event.currentTarget;
158
+ if (Number.isNaN(parseNumber(input.value, locale))) {
159
+ const cleaned = cleanNumberText(input.value, input.selectionStart ?? input.value.length, decimal, textRules);
160
+ input.value = cleaned.text;
161
+ input.setSelectionRange(cleaned.caret, cleaned.caret);
162
+ }
163
+ field.controlProps.onChange(event);
164
+ }
165
+ function handleFocus(event) {
166
+ // Group separators come off while editing, so the caret moves over digits only.
167
+ const input = event.currentTarget;
168
+ const number = parseNumber(input.value, locale);
169
+ if (grouping && editable && isNumber(number)) {
170
+ const selectedAll = input.value !== "" && input.selectionStart === 0 && input.selectionEnd === input.value.length;
171
+ input.value = editText(number);
172
+ if (selectedAll)
173
+ input.select();
174
+ }
175
+ onFocus?.(event);
176
+ }
177
+ function handleClick(event) {
178
+ readOnlyNotice.onClick(event);
179
+ onClick?.(event);
180
+ }
181
+ function handleBlur(event) {
182
+ readOnlyNotice.hide();
183
+ const input = event.currentTarget;
184
+ const number = parseNumber(input.value, locale);
185
+ if (editable && number === null && input.value) {
186
+ // A lone "-" or "." means nothing.
187
+ setInputValue(input, "");
188
+ }
189
+ else if (editable && isNumber(number)) {
190
+ let next = decimals === undefined ? number : roundDecimal(number, decimals);
191
+ if (clamp)
192
+ next = clampNumber(next, min, max);
193
+ if (next !== number)
194
+ setInputValue(input, editText(next));
195
+ input.value = displayText(next);
196
+ }
197
+ field.controlProps.onBlur(event);
198
+ }
199
+ function startRepeat(event, direction) {
200
+ if (event.button !== 0 || !editable)
201
+ return;
202
+ event.currentTarget.setPointerCapture(event.pointerId);
203
+ clearTimeout(repeatRef.current);
204
+ const tick = (delay, next) => {
205
+ if (!stepBy(direction))
206
+ return;
207
+ field.touch();
208
+ repeatRef.current = setTimeout(() => tick(next, Math.max(REPEAT_FASTEST, next * REPEAT_ACCELERATION)), delay);
209
+ };
210
+ tick(REPEAT_WAIT, REPEAT_START);
211
+ }
212
+ function stopRepeat() {
213
+ clearTimeout(repeatRef.current);
214
+ }
215
+ // Pointer presses step in startRepeat; this handles a click with no pointer, from a keyboard or a screen reader.
216
+ function clickStep(event, direction) {
217
+ if (event.detail === 0 && stepBy(direction))
218
+ field.touch();
219
+ }
220
+ const stepButton = (direction) => (_jsx("button", { ref: direction < 0 ? minusRef : plusRef, type: "button", className: "nx-field__action nx-field__action--step",
221
+ // Out of the tab order, like the clear button: the arrow keys do the same from the box.
222
+ tabIndex: -1, "aria-label": direction < 0 ? "Decrease" : "Increase", "aria-controls": field.controlId, "aria-disabled": atLimit(initial, direction) || undefined, title: direction < 0 ? "Decrease" : "Increase", disabled: disabled, onPointerDown: event => startRepeat(event, direction), onPointerUp: stopRepeat, onPointerCancel: stopRepeat, onLostPointerCapture: stopRepeat, onMouseDown: event => event.preventDefault(), onContextMenu: event => event.preventDefault(), onClick: event => clickStep(event, direction), children: _jsx(InlineIcon, { icon: direction < 0 ? iconMinus : iconPlus }) }));
223
+ const showStepper = Boolean(stepper) && !readOnly;
224
+ const split = stepper === "split";
225
+ return (_jsxs(_Fragment, { children: [_jsxs(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, design: design, size: size, hint: hint, error: field.error, icon: icon, iconEnd: iconEnd, prefix: prefix, suffix: suffix, actionsStart: showStepper && split ? stepButton(-1) : null, actions: showStepper ? (_jsxs(_Fragment, { children: [!split && stepButton(-1), stepButton(1)] })) : (readOnlyNotice.lock), disabled: disabled, readOnly: readOnly,
226
+ // A prefix or a − button sits where the resting label would, so the label stays raised.
227
+ raiseLabel: Boolean(placeholder) || hasContent(prefix) || (showStepper && split), className: cx(showStepper && split && "nx-field--split", className), style: style, children: [_jsx("input", { inputMode: decimals === 0 ? "numeric" : "decimal", autoComplete: "off", ...inputProps, ...field.controlProps, type: "text", name: sendsHidden ? undefined : name, form: form, defaultValue: displayText(initial), className: cx("nx-field__control", "nx-field__control--number", inputClassName), placeholder: placeholder || " ", disabled: disabled, readOnly: readOnly, onChange: handleChange, onClick: handleClick, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, onPaste: handlePaste }), sendsHidden && _jsx("input", { ref: hiddenRef, type: "hidden", name: name, form: form, disabled: disabled, defaultValue: isNumber(initial) ? String(initial) : "" })] }), readOnlyNotice.notice] }));
228
+ }
@@ -0,0 +1,20 @@
1
+ import type { ChangeEvent } from "react";
2
+ import type { OptionValue } from "../../Interfaces/InputInterfaces.ts";
3
+ import { type ChoiceGroupProps } from "./ChoiceField.tsx";
4
+ export interface RadioGroupProps<T extends OptionValue = string> extends ChoiceGroupProps<T> {
5
+ /** The chosen value, or `null` for none. With `value`, the group is controlled; pair it with `onValueChange`. */
6
+ value?: T | null;
7
+ defaultValue?: T | null;
8
+ /** Runs after `required` passes, with the chosen value or `null`. */
9
+ validate?: (value: T | null) => string | null | undefined;
10
+ /** The chosen option's value, with its original type, after each change. */
11
+ onValueChange?: (value: T, event: ChangeEvent<HTMLElement>) => void;
12
+ }
13
+ /**
14
+ * One choice from a list of options, such as a room type or a payment method. Native radio buttons, so the arrow keys
15
+ * move the choice. `design="cards"` shows each option as a card with its description. Read-only (default note
16
+ * "Read-only: this choice cannot be changed.") shows a lock after the label and a note under a clicked option.
17
+ *
18
+ * @example <RadioGroup name="roomType" label="Room type" required design="cards" options={[{ value: "deluxe", label: "Deluxe" }]} />
19
+ */
20
+ export declare function RadioGroup<T extends OptionValue = string>(props: RadioGroupProps<T>): import("react").JSX.Element;
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { validateSelection } from "../../Helpers/ValidationHelpers.js";
4
+ import { ChoiceItem, ChoiceLegend, GroupFooter, groupLayout, leavesField, OPTION_ATTRIBUTE, optionAnchor, useChoiceField } from "./ChoiceField.js";
5
+ import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
6
+ /**
7
+ * One choice from a list of options, such as a room type or a payment method. Native radio buttons, so the arrow keys
8
+ * move the choice. `design="cards"` shows each option as a card with its description. Read-only (default note
9
+ * "Read-only: this choice cannot be changed.") shows a lock after the label and a note under a clicked option.
10
+ *
11
+ * @example <RadioGroup name="roomType" label="Room type" required design="cards" options={[{ value: "deluxe", label: "Deluxe" }]} />
12
+ */
13
+ export function RadioGroup(props) {
14
+ const { options, name, label, hint, required, validate, value, defaultValue, readOnly = false, disabled, onValueChange } = props;
15
+ const { readOnlyMessage = "Read-only: this choice cannot be changed." } = props;
16
+ const { size = "md" } = props;
17
+ const chosen = (inputs) => {
18
+ const input = inputs.find(item => item.checked);
19
+ return input ? [options[Number(input.getAttribute(OPTION_ATTRIBUTE))].value] : [];
20
+ };
21
+ const field = useChoiceField(props, {
22
+ check: (inputs, fieldName) => validateSelection(chosen(inputs), { required, validate: validate && (values => validate(values[0] ?? null)) }, fieldName),
23
+ checkKeys: [value, required, validate, options],
24
+ });
25
+ function handleChange(event) {
26
+ if (readOnly)
27
+ return;
28
+ field.onChange();
29
+ const [chosenValue] = chosen(field.inputs());
30
+ if (chosenValue !== undefined)
31
+ onValueChange?.(chosenValue, event);
32
+ }
33
+ // Clicks and focus bubble to the fieldset. The arrow keys reach the click handler too: browsers move the choice with a click.
34
+ const readOnlyNotice = useReadOnlyNotice(readOnly && !disabled, field.containerRef, readOnlyMessage, { anchor: optionAnchor, typing: false, lock: "label" });
35
+ function handleBlur(event) {
36
+ if (leavesField(event))
37
+ readOnlyNotice.hide();
38
+ field.onBlur(event);
39
+ }
40
+ const invalid = Boolean(field.error);
41
+ const layout = groupLayout(props);
42
+ return (_jsxs("fieldset", { ref: field.containerRef, id: field.fieldId, role: "radiogroup", className: layout.className, style: props.style, disabled: disabled, "data-invalid": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"], "aria-describedby": field.error || hint ? field.messageId : undefined, "aria-required": required || undefined, "aria-invalid": invalid || undefined, "aria-readonly": readOnly || undefined, onChange: handleChange, onClick: readOnlyNotice.onClick, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [_jsx(ChoiceLegend, { label: label, required: required, lock: readOnlyNotice.lock }), _jsx("div", { className: "nx-choices__options", style: layout.optionsStyle, children: options.map((option, index) => (_jsx(ChoiceItem, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, size: size, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) }), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
43
+ }