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,281 @@
1
+ "use client";
2
+ import { useEffect, useLayoutEffect, useRef } from "react";
3
+ /** Controls inside a handle keep their own clicks and text selection. */
4
+ export const DRAG_CANCEL_SELECTOR = 'button, a[href], input, select, textarea, label, summary, [contenteditable]:not([contenteditable="false"]), [data-nx-no-drag]';
5
+ /** The browser window, without its scrollbars. */
6
+ export function viewportBounds() {
7
+ const root = document.documentElement;
8
+ return { left: 0, top: 0, width: root.clientWidth || window.innerWidth, height: root.clientHeight || window.innerHeight };
9
+ }
10
+ /** The inside of an element's borders (its padding box), without a scrollbar, in viewport pixels. */
11
+ export function elementBounds(element) {
12
+ const rect = element.getBoundingClientRect();
13
+ const html = element;
14
+ // An inline element has no client box; its outer box stands in.
15
+ if (!html.clientWidth && !html.clientHeight)
16
+ return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
17
+ return { left: rect.left + html.clientLeft, top: rect.top + html.clientTop, width: html.clientWidth, height: html.clientHeight };
18
+ }
19
+ /** Turns a `DragBounds` into a box, for an element being moved. */
20
+ export function resolveBounds(bounds, element) {
21
+ if (bounds === undefined || bounds === "viewport")
22
+ return viewportBounds();
23
+ if (bounds === null)
24
+ return null;
25
+ if (bounds === "parent") {
26
+ const parent = element.offsetParent ?? element.parentElement;
27
+ return parent ? elementBounds(parent) : viewportBounds();
28
+ }
29
+ if (typeof bounds === "function")
30
+ return bounds();
31
+ if (bounds instanceof Element)
32
+ return elementBounds(bounds);
33
+ return bounds;
34
+ }
35
+ function clampAxis(start, size, min, room, keepVisible) {
36
+ if (keepVisible === undefined)
37
+ return Math.max(min, Math.min(start, min + room - size));
38
+ const keep = Math.min(Math.max(keepVisible, 0), size);
39
+ return Math.max(min - size + keep, Math.min(start, min + room - keep));
40
+ }
41
+ /**
42
+ * The top-left corner a box must move to, the least distance, to stay inside bounds.
43
+ *
44
+ * @example clampToBounds({ left: -20, top: 40, width: 300, height: 200 }, viewportBounds()) // { x: 0, y: 40 }
45
+ */
46
+ export function clampToBounds(box, bounds, options = {}) {
47
+ const margin = options.margin ?? 0;
48
+ return {
49
+ x: clampAxis(box.left, box.width, bounds.left + margin, bounds.width - 2 * margin, options.keepVisible),
50
+ y: clampAxis(box.top, box.height, bounds.top + margin, bounds.height - 2 * margin, options.keepVisible),
51
+ };
52
+ }
53
+ function readPixels(value) {
54
+ const [x = 0, y = 0] = value.split(/\s+/).map(part => Number.parseFloat(part));
55
+ return { x: Number.isFinite(x) ? x : 0, y: Number.isFinite(y) ? y : 0 };
56
+ }
57
+ /** The default position: the element's inline CSS `translate`, in pixels. */
58
+ export function translatePosition(element) {
59
+ return {
60
+ get: () => readPixels(element.style.translate),
61
+ set: ({ x, y }) => {
62
+ element.style.translate = `${x}px ${y}px`;
63
+ },
64
+ };
65
+ }
66
+ /** A position kept in two CSS variables on the element, for a stylesheet that places it with them (and can ignore them, e.g. when maximized). */
67
+ export function cssVariablePosition(element, xVariable, yVariable) {
68
+ return {
69
+ get: () => ({ x: Number.parseFloat(element.style.getPropertyValue(xVariable)) || 0, y: Number.parseFloat(element.style.getPropertyValue(yVariable)) || 0 }),
70
+ set: ({ x, y }) => {
71
+ element.style.setProperty(xVariable, `${x}px`);
72
+ element.style.setProperty(yVariable, `${y}px`);
73
+ },
74
+ };
75
+ }
76
+ /**
77
+ * Moves an element by an offset, kept inside bounds, and returns its new position. With no offset it pulls an element
78
+ * back inside, e.g. after the window got smaller or the element grew. Also the way to move an element from the keyboard.
79
+ */
80
+ export function moveWithin(element, by, options = {}) {
81
+ const position = options.position ?? translatePosition(element);
82
+ const from = position.get();
83
+ let dx = options.axis === "y" ? 0 : by.x;
84
+ let dy = options.axis === "x" ? 0 : by.y;
85
+ const bounds = resolveBounds(options.bounds, element);
86
+ if (bounds) {
87
+ const rect = element.getBoundingClientRect();
88
+ const corner = clampToBounds({ left: rect.left + dx, top: rect.top + dy, width: rect.width, height: rect.height }, bounds, options);
89
+ if (options.axis !== "y")
90
+ dx = corner.x - rect.left;
91
+ if (options.axis !== "x")
92
+ dy = corner.y - rect.top;
93
+ }
94
+ const to = { x: from.x + dx, y: from.y + dy };
95
+ if (to.x !== from.x || to.y !== from.y)
96
+ position.set(to);
97
+ return to;
98
+ }
99
+ /**
100
+ * Starts dragging `target` from a `pointerdown` event, e.g. in a React `onPointerDown` on a title bar. The target
101
+ * follows the pointer once it has moved past the threshold, stays inside the bounds, and goes back where it started on
102
+ * Esc. The handle needs `touch-action: none` in its CSS, or a touch pans the page instead. Returns a function that ends
103
+ * the drag early.
104
+ */
105
+ export function startDrag(event, options) {
106
+ const { target, threshold = 3, axis = "both" } = options;
107
+ const position = options.position ?? translatePosition(target);
108
+ const handle = options.handle ?? (event.currentTarget instanceof Element ? event.currentTarget : target);
109
+ const pointerId = event.pointerId;
110
+ const start = { x: event.clientX, y: event.clientY };
111
+ let pointer = start;
112
+ let moving = false;
113
+ let done = false;
114
+ let base = { x: 0, y: 0 };
115
+ let baseRect = { left: 0, top: 0, width: 0, height: 0 };
116
+ let bounds = null;
117
+ let current = { x: 0, y: 0 };
118
+ const state = () => ({ target, start, pointer, position: current, delta: { x: current.x - base.x, y: current.y - base.y } });
119
+ function place() {
120
+ let dx = axis === "y" ? 0 : pointer.x - start.x;
121
+ let dy = axis === "x" ? 0 : pointer.y - start.y;
122
+ if (bounds) {
123
+ const corner = clampToBounds({ left: baseRect.left + dx, top: baseRect.top + dy, width: baseRect.width, height: baseRect.height }, bounds, options);
124
+ if (axis !== "y")
125
+ dx = corner.x - baseRect.left;
126
+ if (axis !== "x")
127
+ dy = corner.y - baseRect.top;
128
+ }
129
+ current = { x: base.x + dx, y: base.y + dy };
130
+ position.set(current);
131
+ options.onMove?.(state());
132
+ }
133
+ function begin() {
134
+ current = position.get();
135
+ base = current;
136
+ if (options.onStart?.(state()) === false)
137
+ return false;
138
+ base = position.get();
139
+ current = base;
140
+ const rect = target.getBoundingClientRect();
141
+ baseRect = { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
142
+ bounds = resolveBounds(options.bounds, target);
143
+ moving = true;
144
+ target.setAttribute("data-nx-dragging", "");
145
+ window.getSelection()?.removeAllRanges();
146
+ try {
147
+ handle.setPointerCapture(pointerId);
148
+ }
149
+ catch {
150
+ // The pointer is already gone; the next pointerup ends the drag.
151
+ }
152
+ return true;
153
+ }
154
+ function onPointerMove(move) {
155
+ if (move.pointerId !== pointerId)
156
+ return;
157
+ pointer = { x: move.clientX, y: move.clientY };
158
+ if (!moving) {
159
+ if (Math.hypot(pointer.x - start.x, pointer.y - start.y) < threshold)
160
+ return;
161
+ if (!begin())
162
+ return finish(false);
163
+ }
164
+ move.preventDefault();
165
+ place();
166
+ }
167
+ function onPointerEnd(end) {
168
+ if (end.pointerId === pointerId)
169
+ finish(false);
170
+ }
171
+ function onKeyDown(key) {
172
+ if (key.key !== "Escape" || !moving)
173
+ return;
174
+ // Esc cancels the drag only: it must not also close the popup or menu around the target.
175
+ key.preventDefault();
176
+ key.stopPropagation();
177
+ current = base;
178
+ position.set(base);
179
+ finish(true);
180
+ }
181
+ // A drag selects no text on its way.
182
+ function onSelectStart(select) {
183
+ select.preventDefault();
184
+ }
185
+ // The click that ends a drag is not a click on what lies under the pointer.
186
+ function swallowClick(click) {
187
+ click.stopPropagation();
188
+ click.preventDefault();
189
+ }
190
+ function finish(cancelled) {
191
+ if (done)
192
+ return;
193
+ done = true;
194
+ window.removeEventListener("pointermove", onPointerMove, true);
195
+ window.removeEventListener("pointerup", onPointerEnd, true);
196
+ window.removeEventListener("pointercancel", onPointerEnd, true);
197
+ window.removeEventListener("keydown", onKeyDown, true);
198
+ window.removeEventListener("blur", onBlur);
199
+ document.removeEventListener("selectstart", onSelectStart, true);
200
+ if (!moving)
201
+ return;
202
+ target.removeAttribute("data-nx-dragging");
203
+ if (handle.hasPointerCapture?.(pointerId))
204
+ handle.releasePointerCapture(pointerId);
205
+ window.addEventListener("click", swallowClick, { capture: true, once: true });
206
+ window.setTimeout(() => window.removeEventListener("click", swallowClick, true), 0);
207
+ options.onEnd?.(state(), cancelled);
208
+ }
209
+ const onBlur = () => finish(false);
210
+ window.addEventListener("pointermove", onPointerMove, true);
211
+ window.addEventListener("pointerup", onPointerEnd, true);
212
+ window.addEventListener("pointercancel", onPointerEnd, true);
213
+ window.addEventListener("keydown", onKeyDown, true);
214
+ window.addEventListener("blur", onBlur);
215
+ document.addEventListener("selectstart", onSelectStart, true);
216
+ return () => finish(false);
217
+ }
218
+ /**
219
+ * Makes `handle` drag `options.target` (or itself), and returns a function that stops it. Options can be a function,
220
+ * read at every press, so a component can pass its latest props.
221
+ *
222
+ * @example
223
+ * const stop = makeDraggable(card.querySelector(".title"), { target: card, bounds: "parent" });
224
+ */
225
+ export function makeDraggable(handle, options = {}) {
226
+ const read = typeof options === "function" ? options : () => options;
227
+ let stopDrag = null;
228
+ const touchAction = handle.style.touchAction;
229
+ handle.style.touchAction = "none";
230
+ function onPointerDown(event) {
231
+ if (event.button !== 0 || !event.isPrimary)
232
+ return;
233
+ const current = read();
234
+ if (current.enabled && !current.enabled())
235
+ return;
236
+ const origin = event.target instanceof Element ? event.target.closest(current.cancel ?? DRAG_CANCEL_SELECTOR) : null;
237
+ if (origin && origin !== handle && handle.contains(origin))
238
+ return;
239
+ const target = current.target === undefined ? handle : typeof current.target === "function" ? current.target() : current.target;
240
+ if (!target)
241
+ return;
242
+ stopDrag?.();
243
+ stopDrag = startDrag(event, {
244
+ ...current,
245
+ target,
246
+ handle,
247
+ onEnd: (state, cancelled) => {
248
+ stopDrag = null;
249
+ current.onEnd?.(state, cancelled);
250
+ },
251
+ });
252
+ }
253
+ handle.addEventListener("pointerdown", onPointerDown);
254
+ return () => {
255
+ handle.removeEventListener("pointerdown", onPointerDown);
256
+ handle.style.touchAction = touchAction;
257
+ stopDrag?.();
258
+ };
259
+ }
260
+ /**
261
+ * React: the element in `handleRef` drags the target (or itself). The options may change on every render; the latest
262
+ * are used at each press.
263
+ *
264
+ * @example
265
+ * const card = useRef<HTMLDivElement>(null);
266
+ * const title = useRef<HTMLDivElement>(null);
267
+ * useDraggable(title, { target: () => card.current, bounds: "parent" });
268
+ */
269
+ export function useDraggable(handleRef, options = {}) {
270
+ const latest = useRef(options);
271
+ useLayoutEffect(() => {
272
+ latest.current = options;
273
+ });
274
+ const disabled = options.disabled ?? false;
275
+ useEffect(() => {
276
+ const handle = handleRef.current;
277
+ if (!handle || disabled)
278
+ return;
279
+ return makeDraggable(handle, () => latest.current);
280
+ }, [handleRef, disabled]);
281
+ }
@@ -0,0 +1,56 @@
1
+ import type { DropdownColumn, DropdownItem, OptionValue } from "../Interfaces/InputInterfaces.ts";
2
+ /** Tones an avatar without a photo takes, picked from its name. Each is a soft theme color under the text color. */
3
+ export declare const AVATAR_TONES: readonly ["primary", "info", "success", "warning", "neutral"];
4
+ export type AvatarTone = (typeof AVATAR_TONES)[number];
5
+ /** Reads an item's field; `undefined` for a missing item or field. */
6
+ export declare function readField(item: DropdownItem | null | undefined, key: string): unknown;
7
+ /**
8
+ * The key a value is compared by. A number and its text are the same value, so id 7 from an API matches "7" from a
9
+ * form or a URL. `null` and `undefined` give "".
10
+ */
11
+ export declare function optionKey(value: unknown): string;
12
+ /** An item's value under `valueKey`, when it is a string or a number. */
13
+ export declare function itemValue(item: DropdownItem, valueKey: string): OptionValue | undefined;
14
+ /** An item's label as text: the field under `labelKey`, or its value when the label is empty. */
15
+ export declare function itemLabel(item: DropdownItem, labelKey: string, valueKey: string): string;
16
+ /** Whether an item is inactive: `inactive: true`, or `isActive: false` as many APIs mark a deactivated record. */
17
+ export declare function isInactiveItem(item: DropdownItem): boolean;
18
+ /** Whether an item is disabled: `disabled: true`, as on a `ChoiceOption`. */
19
+ export declare function isDisabledItem(item: DropdownItem): boolean;
20
+ /**
21
+ * Values as a list: `null` or `undefined` → `[]`, one value → `[value]`, a list → a copy without repeated keys.
22
+ * A single choice keeps only the first value.
23
+ */
24
+ export declare function toValueList<T extends OptionValue>(value: T | readonly T[] | null | undefined, multiple: boolean): T[];
25
+ /** Whether two lists hold the same values, by key, in the same order. */
26
+ export declare function sameValues(a: readonly OptionValue[], b: readonly OptionValue[]): boolean;
27
+ /** A list with the value removed when it is there, or added at the end when it is not. */
28
+ export declare function toggleValue<T extends OptionValue>(values: readonly T[], value: T): T[];
29
+ /** A map from each item's value key to the item; the first item wins when two share a value. Cached per array. */
30
+ export declare function indexItems<TItem extends DropdownItem>(items: readonly TItem[], valueKey: string): Map<string, TItem>;
31
+ /**
32
+ * Text folded for matching what people type: lower case, and Latin, Greek, and Cyrillic letters without their accents
33
+ * ("José" → "jose"). Other scripts keep their marks, so Devanagari vowel signs still count.
34
+ */
35
+ export declare function foldSearchText(text: string): string;
36
+ /** The words of a search. An item matches when it contains every word, in any order. */
37
+ export declare function searchWords(search: string): string[];
38
+ /** An item's fields under `keys`, folded and joined, for matching a search. */
39
+ export declare function itemSearchText(item: DropdownItem, keys: readonly string[]): string;
40
+ /** Whether an item's fields under `keys` contain every word. */
41
+ export declare function matchesSearch(item: DropdownItem, words: readonly string[], keys: readonly string[]): boolean;
42
+ /** The items whose fields under `keys` contain every word of the search, in their order. All items for an empty search. */
43
+ export declare function filterItems<TItem extends DropdownItem>(items: readonly TItem[], search: string, keys: readonly string[]): TItem[];
44
+ /** The fields searched by default: the label, the description, the value, and every text, muted, or code column. */
45
+ export declare function defaultSearchKeys(labelKey: string, descriptionKey: string, valueKey: string, columns?: readonly DropdownColumn<never>[]): string[];
46
+ /** Whether a column draws a picture: an avatar, an image, or an icon. The first one also leads the value in the box. */
47
+ export declare function isMediaColumn(column: Pick<DropdownColumn<never>, "type">): boolean;
48
+ /** A column's grid track: its `width`, or `auto` for pictures and codes and `minmax(0, 1fr)` for text. */
49
+ export declare function columnTrack(column: Pick<DropdownColumn<never>, "type" | "width">): string;
50
+ /**
51
+ * Up to two initials of a name, from its first and last words: "Ram Bahadur Sharma" → "RS", "admin" → "A",
52
+ * "sita.rai" → "SR". Letters keep their script, so "राम शर्मा" → "रश".
53
+ */
54
+ export declare function initials(name: string): string;
55
+ /** The tone of an avatar without a photo: the same name always gets the same tone. */
56
+ export declare function avatarTone(name: string): AvatarTone;
@@ -0,0 +1,180 @@
1
+ /*
2
+ * Pure helpers behind the dropdown: reading items by key, comparing values, searching, and the look of avatars.
3
+ * No DOM and no React, so a form or a data table can use them on data alone.
4
+ */
5
+ /** Tones an avatar without a photo takes, picked from its name. Each is a soft theme color under the text color. */
6
+ export const AVATAR_TONES = ["primary", "info", "success", "warning", "neutral"];
7
+ /** Reads an item's field; `undefined` for a missing item or field. */
8
+ export function readField(item, key) {
9
+ return item ? item[key] : undefined;
10
+ }
11
+ /**
12
+ * The key a value is compared by. A number and its text are the same value, so id 7 from an API matches "7" from a
13
+ * form or a URL. `null` and `undefined` give "".
14
+ */
15
+ export function optionKey(value) {
16
+ return value === null || value === undefined ? "" : String(value);
17
+ }
18
+ /** An item's value under `valueKey`, when it is a string or a number. */
19
+ export function itemValue(item, valueKey) {
20
+ const value = readField(item, valueKey);
21
+ return typeof value === "string" || typeof value === "number" ? value : undefined;
22
+ }
23
+ /** An item's label as text: the field under `labelKey`, or its value when the label is empty. */
24
+ export function itemLabel(item, labelKey, valueKey) {
25
+ const label = readField(item, labelKey);
26
+ if (label !== null && label !== undefined && label !== "")
27
+ return String(label);
28
+ return optionKey(readField(item, valueKey));
29
+ }
30
+ /** Whether an item is inactive: `inactive: true`, or `isActive: false` as many APIs mark a deactivated record. */
31
+ export function isInactiveItem(item) {
32
+ const record = item;
33
+ return record.inactive === true || record.isActive === false;
34
+ }
35
+ /** Whether an item is disabled: `disabled: true`, as on a `ChoiceOption`. */
36
+ export function isDisabledItem(item) {
37
+ return item.disabled === true;
38
+ }
39
+ /**
40
+ * Values as a list: `null` or `undefined` → `[]`, one value → `[value]`, a list → a copy without repeated keys.
41
+ * A single choice keeps only the first value.
42
+ */
43
+ export function toValueList(value, multiple) {
44
+ if (value === null || value === undefined)
45
+ return [];
46
+ const list = Array.isArray(value) ? value : [value];
47
+ const seen = new Set();
48
+ const result = [];
49
+ for (const item of list) {
50
+ if (item === null || item === undefined)
51
+ continue;
52
+ const key = optionKey(item);
53
+ if (seen.has(key))
54
+ continue;
55
+ seen.add(key);
56
+ result.push(item);
57
+ if (!multiple)
58
+ break;
59
+ }
60
+ return result;
61
+ }
62
+ /** Whether two lists hold the same values, by key, in the same order. */
63
+ export function sameValues(a, b) {
64
+ return a.length === b.length && a.every((value, index) => optionKey(value) === optionKey(b[index]));
65
+ }
66
+ /** A list with the value removed when it is there, or added at the end when it is not. */
67
+ export function toggleValue(values, value) {
68
+ const key = optionKey(value);
69
+ return values.some(item => optionKey(item) === key) ? values.filter(item => optionKey(item) !== key) : [...values, value];
70
+ }
71
+ // One index per items array and value key, so a thousand dropdowns sharing one list of countries build it once.
72
+ const indexes = new WeakMap();
73
+ /** A map from each item's value key to the item; the first item wins when two share a value. Cached per array. */
74
+ export function indexItems(items, valueKey) {
75
+ let byKey = indexes.get(items);
76
+ if (!byKey) {
77
+ byKey = new Map();
78
+ indexes.set(items, byKey);
79
+ }
80
+ let index = byKey.get(valueKey);
81
+ if (!index) {
82
+ index = new Map();
83
+ for (const item of items) {
84
+ const key = optionKey(itemValue(item, valueKey));
85
+ if (key && !index.has(key))
86
+ index.set(key, item);
87
+ }
88
+ byKey.set(valueKey, index);
89
+ }
90
+ return index;
91
+ }
92
+ /**
93
+ * Text folded for matching what people type: lower case, and Latin, Greek, and Cyrillic letters without their accents
94
+ * ("José" → "jose"). Other scripts keep their marks, so Devanagari vowel signs still count.
95
+ */
96
+ export function foldSearchText(text) {
97
+ return text
98
+ .normalize("NFD")
99
+ .replace(/(?<=[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}])\p{M}+/gu, "")
100
+ .normalize("NFC")
101
+ .toLocaleLowerCase();
102
+ }
103
+ /** The words of a search. An item matches when it contains every word, in any order. */
104
+ export function searchWords(search) {
105
+ return foldSearchText(search).split(/\s+/).filter(Boolean);
106
+ }
107
+ // The folded text of an item, kept per item object and set of keys, so each keystroke only compares strings.
108
+ const searchTexts = new WeakMap();
109
+ /** An item's fields under `keys`, folded and joined, for matching a search. */
110
+ export function itemSearchText(item, keys) {
111
+ const joinedKeys = keys.join("\n");
112
+ const cached = searchTexts.get(item);
113
+ if (cached && cached.keys === joinedKeys)
114
+ return cached.text;
115
+ const parts = [];
116
+ for (const key of keys) {
117
+ const value = readField(item, key);
118
+ if (typeof value === "string" || typeof value === "number")
119
+ parts.push(String(value));
120
+ }
121
+ const text = foldSearchText(parts.join("\n"));
122
+ searchTexts.set(item, { keys: joinedKeys, text });
123
+ return text;
124
+ }
125
+ /** Whether an item's fields under `keys` contain every word. */
126
+ export function matchesSearch(item, words, keys) {
127
+ if (words.length === 0)
128
+ return true;
129
+ const text = itemSearchText(item, keys);
130
+ return words.every(word => text.includes(word));
131
+ }
132
+ /** The items whose fields under `keys` contain every word of the search, in their order. All items for an empty search. */
133
+ export function filterItems(items, search, keys) {
134
+ const words = searchWords(search);
135
+ if (words.length === 0)
136
+ return items.slice();
137
+ return items.filter(item => matchesSearch(item, words, keys));
138
+ }
139
+ /** Column types that draw text, and so are searched by default. */
140
+ const TEXT_TYPES = new Set(["text", "muted", "code"]);
141
+ /** The fields searched by default: the label, the description, the value, and every text, muted, or code column. */
142
+ export function defaultSearchKeys(labelKey, descriptionKey, valueKey, columns) {
143
+ const keys = [labelKey, descriptionKey, valueKey];
144
+ for (const column of columns ?? [])
145
+ if (TEXT_TYPES.has(column.type ?? "text") && !keys.includes(column.key))
146
+ keys.push(column.key);
147
+ return keys;
148
+ }
149
+ /** Whether a column draws a picture: an avatar, an image, or an icon. The first one also leads the value in the box. */
150
+ export function isMediaColumn(column) {
151
+ return column.type === "avatar" || column.type === "image" || column.type === "icon";
152
+ }
153
+ /** A column's grid track: its `width`, or `auto` for pictures and codes and `minmax(0, 1fr)` for text. */
154
+ export function columnTrack(column) {
155
+ if (column.width)
156
+ return column.width;
157
+ return isMediaColumn(column) || column.type === "code" ? "auto" : "minmax(0, 1fr)";
158
+ }
159
+ /**
160
+ * Up to two initials of a name, from its first and last words: "Ram Bahadur Sharma" → "RS", "admin" → "A",
161
+ * "sita.rai" → "SR". Letters keep their script, so "राम शर्मा" → "रश".
162
+ */
163
+ export function initials(name) {
164
+ const words = name
165
+ .trim()
166
+ .split(/[\s._\-@]+/)
167
+ .filter(word => /[\p{L}\p{N}]/u.test(word));
168
+ if (words.length === 0)
169
+ return "";
170
+ const first = (word) => Array.from(word.replace(/^[^\p{L}\p{N}]+/u, ""))[0] ?? "";
171
+ const letters = words.length === 1 ? first(words[0]) : first(words[0]) + first(words[words.length - 1]);
172
+ return letters.toLocaleUpperCase();
173
+ }
174
+ /** The tone of an avatar without a photo: the same name always gets the same tone. */
175
+ export function avatarTone(name) {
176
+ let hash = 0;
177
+ for (let index = 0; index < name.length; index++)
178
+ hash = (hash * 31 + name.charCodeAt(index)) | 0;
179
+ return AVATAR_TONES[Math.abs(hash) % AVATAR_TONES.length];
180
+ }
@@ -0,0 +1,18 @@
1
+ import type { FormValues } from "../Interfaces/FormInterfaces.ts";
2
+ /** A form's unsaved changes. */
3
+ export interface FormDraft {
4
+ /** The changed values, by input name. */
5
+ values: FormValues;
6
+ /** The same inputs' values when the changes were made, to tell whether someone else saved them since. */
7
+ base: FormValues;
8
+ /** When the draft was saved (milliseconds since 1970). */
9
+ savedAt: number;
10
+ }
11
+ /** Drafts older than this are dropped when read: 30 days. */
12
+ export declare const FORM_DRAFT_MAX_AGE: number;
13
+ /** The draft kept under `key`, or `null`. A draft older than `maxAge` is dropped. */
14
+ export declare function readFormDraft(key: string, maxAge?: number): Promise<FormDraft | null>;
15
+ /** Keeps a draft under `key`. Resolves `true` once it is stored. */
16
+ export declare function writeFormDraft(key: string, draft: FormDraft): Promise<boolean>;
17
+ /** Removes the draft kept under `key`, if any. */
18
+ export declare function deleteFormDraft(key: string): Promise<void>;
@@ -0,0 +1,76 @@
1
+ const DB_NAME = "nexus-forms";
2
+ const STORE_NAME = "drafts";
3
+ /** Drafts older than this are dropped when read: 30 days. */
4
+ export const FORM_DRAFT_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
5
+ let database = null;
6
+ function openDatabase() {
7
+ if (typeof indexedDB === "undefined")
8
+ return Promise.resolve(null);
9
+ database ??= new Promise(resolve => {
10
+ try {
11
+ const request = indexedDB.open(DB_NAME, 1);
12
+ request.onupgradeneeded = () => request.result.createObjectStore(STORE_NAME);
13
+ request.onsuccess = () => {
14
+ const db = request.result;
15
+ // Another tab upgrading the database closes this connection; the next call opens a new one.
16
+ db.onversionchange = () => {
17
+ db.close();
18
+ database = null;
19
+ };
20
+ resolve(db);
21
+ };
22
+ request.onerror = () => resolve(null);
23
+ request.onblocked = () => resolve(null);
24
+ }
25
+ catch {
26
+ resolve(null);
27
+ }
28
+ });
29
+ return database;
30
+ }
31
+ function run(mode, work) {
32
+ return openDatabase().then(db => new Promise(resolve => {
33
+ if (!db)
34
+ return resolve(null);
35
+ try {
36
+ const transaction = db.transaction(STORE_NAME, mode);
37
+ const request = work(transaction.objectStore(STORE_NAME));
38
+ transaction.oncomplete = () => resolve(request.result ?? null);
39
+ transaction.onerror = () => resolve(null);
40
+ transaction.onabort = () => resolve(null);
41
+ }
42
+ catch {
43
+ resolve(null);
44
+ }
45
+ }));
46
+ }
47
+ function isDraft(value) {
48
+ const draft = value;
49
+ return Boolean(draft && typeof draft === "object" && draft.values && typeof draft.values === "object" && typeof draft.savedAt === "number");
50
+ }
51
+ /** The draft kept under `key`, or `null`. A draft older than `maxAge` is dropped. */
52
+ export async function readFormDraft(key, maxAge = FORM_DRAFT_MAX_AGE) {
53
+ const found = await run("readonly", store => store.get(key));
54
+ if (!isDraft(found))
55
+ return null;
56
+ if (Date.now() - found.savedAt > maxAge) {
57
+ void deleteFormDraft(key);
58
+ return null;
59
+ }
60
+ return { values: found.values, base: found.base ?? {}, savedAt: found.savedAt };
61
+ }
62
+ /** Keeps a draft under `key`. Resolves `true` once it is stored. */
63
+ export async function writeFormDraft(key, draft) {
64
+ try {
65
+ const done = await run("readwrite", store => store.put(draft, key));
66
+ return done !== null;
67
+ }
68
+ catch {
69
+ // Values the browser cannot store (a function in a value) keep no draft.
70
+ return false;
71
+ }
72
+ }
73
+ /** Removes the draft kept under `key`, if any. */
74
+ export async function deleteFormDraft(key) {
75
+ await run("readwrite", store => store.delete(key));
76
+ }