@happyvertical/smrt-ui 0.51.39 → 0.53.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 (392) hide show
  1. package/AGENTS.md +25 -3
  2. package/README.md +558 -8
  3. package/dist/actions/__tests__/swipe-dismiss.test.js +69 -0
  4. package/dist/actions/swipe-dismiss.d.ts +42 -0
  5. package/dist/actions/swipe-dismiss.d.ts.map +1 -0
  6. package/dist/actions/swipe-dismiss.js +69 -0
  7. package/dist/components/calendar/Calendar.svelte +5 -0
  8. package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -1
  9. package/dist/components/calendar/CalendarView.svelte +1250 -0
  10. package/dist/components/calendar/CalendarView.svelte.d.ts +66 -0
  11. package/dist/components/calendar/CalendarView.svelte.d.ts.map +1 -0
  12. package/dist/components/calendar/DayView.svelte +5 -0
  13. package/dist/components/calendar/DayView.svelte.d.ts.map +1 -1
  14. package/dist/components/calendar/__tests__/CalendarView.test.js +226 -0
  15. package/dist/components/calendar/__tests__/calendar-model.test.js +138 -0
  16. package/dist/components/calendar/calendar-model.d.ts +178 -0
  17. package/dist/components/calendar/calendar-model.d.ts.map +1 -0
  18. package/dist/components/calendar/calendar-model.js +362 -0
  19. package/dist/components/calendar/index.d.ts +12 -1
  20. package/dist/components/calendar/index.d.ts.map +1 -1
  21. package/dist/components/calendar/index.js +11 -1
  22. package/dist/components/data/CollectionList.svelte +23 -5
  23. package/dist/components/data/CollectionList.svelte.d.ts +8 -2
  24. package/dist/components/data/CollectionList.svelte.d.ts.map +1 -1
  25. package/dist/components/data/DataTable.svelte +178 -55
  26. package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
  27. package/dist/components/data/DataTableController.d.ts +4 -0
  28. package/dist/components/data/DataTableController.d.ts.map +1 -1
  29. package/dist/components/data/DataTableController.js +11 -4
  30. package/dist/components/data/DataTableResponsive.d.ts +4 -0
  31. package/dist/components/data/DataTableResponsive.d.ts.map +1 -0
  32. package/dist/components/data/DataTableResponsive.js +22 -0
  33. package/dist/components/data/ListSortSelect.svelte +121 -0
  34. package/dist/components/data/ListSortSelect.svelte.d.ts +22 -0
  35. package/dist/components/data/ListSortSelect.svelte.d.ts.map +1 -0
  36. package/dist/components/data/SortableHeader.svelte +169 -0
  37. package/dist/components/data/SortableHeader.svelte.d.ts +30 -0
  38. package/dist/components/data/SortableHeader.svelte.d.ts.map +1 -0
  39. package/dist/components/data/__tests__/CollectionList.test.js +37 -0
  40. package/dist/components/data/__tests__/DataTable.test.js +76 -0
  41. package/dist/components/data/__tests__/DataTableController.test.js +35 -0
  42. package/dist/components/data/__tests__/DataTableResponsive.test.js +80 -0
  43. package/dist/components/data/__tests__/SortableHeader.test.js +101 -0
  44. package/dist/components/data/__tests__/link-step-surface.test.js +172 -0
  45. package/dist/components/data/__tests__/list-sort-state.svelte.test.js +71 -0
  46. package/dist/components/data/__tests__/list-sort.test.js +91 -0
  47. package/dist/components/data/__tests__/responsive-table.fixture.svelte +29 -0
  48. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +7 -0
  49. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +1 -0
  50. package/dist/components/data/__tests__/surface-navigation.test.js +121 -0
  51. package/dist/components/data/data-surface.d.ts +1 -0
  52. package/dist/components/data/data-surface.d.ts.map +1 -1
  53. package/dist/components/data/data-surface.js +1 -0
  54. package/dist/components/data/index.d.ts +6 -0
  55. package/dist/components/data/index.d.ts.map +1 -1
  56. package/dist/components/data/index.js +6 -0
  57. package/dist/components/data/link-surface.d.ts +79 -0
  58. package/dist/components/data/link-surface.d.ts.map +1 -0
  59. package/dist/components/data/link-surface.js +155 -0
  60. package/dist/components/data/list-sort-state.svelte.d.ts +39 -0
  61. package/dist/components/data/list-sort-state.svelte.d.ts.map +1 -0
  62. package/dist/components/data/list-sort-state.svelte.js +47 -0
  63. package/dist/components/data/list-sort.d.ts +94 -0
  64. package/dist/components/data/list-sort.d.ts.map +1 -0
  65. package/dist/components/data/list-sort.js +182 -0
  66. package/dist/components/data/step-surface.d.ts +73 -0
  67. package/dist/components/data/step-surface.d.ts.map +1 -0
  68. package/dist/components/data/step-surface.js +177 -0
  69. package/dist/components/data/surface-navigation.d.ts +49 -0
  70. package/dist/components/data/surface-navigation.d.ts.map +1 -0
  71. package/dist/components/data/surface-navigation.js +100 -0
  72. package/dist/components/data/types.d.ts +27 -0
  73. package/dist/components/data/types.d.ts.map +1 -1
  74. package/dist/components/display/DateDisplay.svelte +76 -14
  75. package/dist/components/display/DateDisplay.svelte.d.ts +2 -0
  76. package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -1
  77. package/dist/components/display/Icon.svelte +3 -16
  78. package/dist/components/display/Icon.svelte.d.ts +0 -9
  79. package/dist/components/display/Icon.svelte.d.ts.map +1 -1
  80. package/dist/components/display/StatusBadge.svelte +29 -2
  81. package/dist/components/display/StatusBadge.svelte.d.ts +4 -2
  82. package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -1
  83. package/dist/components/display/__tests__/DateDisplay.test.js +82 -1
  84. package/dist/components/display/__tests__/Icon.test.js +132 -2
  85. package/dist/components/display/__tests__/StatusBadge.test.js +78 -0
  86. package/dist/components/display/icons.svelte.d.ts +11 -0
  87. package/dist/components/display/icons.svelte.d.ts.map +1 -0
  88. package/dist/components/display/icons.svelte.js +60 -0
  89. package/dist/components/display/index.d.ts +3 -1
  90. package/dist/components/display/index.d.ts.map +1 -1
  91. package/dist/components/display/index.js +1 -0
  92. package/dist/components/display/types.d.ts +2 -0
  93. package/dist/components/display/types.d.ts.map +1 -1
  94. package/dist/components/feedback/ConfirmDialog.svelte +59 -48
  95. package/dist/components/feedback/ConfirmDialog.svelte.d.ts +14 -2
  96. package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -1
  97. package/dist/components/feedback/PhoneSheet.svelte +224 -0
  98. package/dist/components/feedback/PhoneSheet.svelte.d.ts +35 -0
  99. package/dist/components/feedback/PhoneSheet.svelte.d.ts.map +1 -0
  100. package/dist/components/feedback/ToastViewport.svelte +156 -8
  101. package/dist/components/feedback/ToastViewport.svelte.d.ts +5 -1
  102. package/dist/components/feedback/ToastViewport.svelte.d.ts.map +1 -1
  103. package/dist/components/feedback/WorkingStrip.svelte +426 -0
  104. package/dist/components/feedback/WorkingStrip.svelte.d.ts +37 -0
  105. package/dist/components/feedback/WorkingStrip.svelte.d.ts.map +1 -0
  106. package/dist/components/feedback/__tests__/ConfirmDialog.test.js +38 -20
  107. package/dist/components/feedback/__tests__/PhoneSheet.test.js +101 -0
  108. package/dist/components/feedback/__tests__/WorkingStrip.test.js +172 -0
  109. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +9 -0
  110. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +19 -0
  111. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +1 -0
  112. package/dist/components/feedback/__tests__/toast.test.js +142 -1
  113. package/dist/components/feedback/index.d.ts +5 -1
  114. package/dist/components/feedback/index.d.ts.map +1 -1
  115. package/dist/components/feedback/index.js +5 -0
  116. package/dist/components/feedback/toast.d.ts +13 -1
  117. package/dist/components/feedback/toast.d.ts.map +1 -1
  118. package/dist/components/feedback/toast.js +51 -10
  119. package/dist/components/feedback/working-status.d.ts +34 -0
  120. package/dist/components/feedback/working-status.d.ts.map +1 -0
  121. package/dist/components/feedback/working-status.js +5 -0
  122. package/dist/components/forms/CameraCapture.svelte +619 -0
  123. package/dist/components/forms/CameraCapture.svelte.d.ts +42 -0
  124. package/dist/components/forms/CameraCapture.svelte.d.ts.map +1 -0
  125. package/dist/components/forms/Checkbox.svelte +34 -3
  126. package/dist/components/forms/Checkbox.svelte.d.ts +2 -0
  127. package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
  128. package/dist/components/forms/Combobox.svelte +108 -12
  129. package/dist/components/forms/Combobox.svelte.d.ts +6 -0
  130. package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
  131. package/dist/components/forms/DictationButton.svelte +128 -0
  132. package/dist/components/forms/DictationButton.svelte.d.ts +19 -0
  133. package/dist/components/forms/DictationButton.svelte.d.ts.map +1 -0
  134. package/dist/components/forms/DictationStatus.svelte +164 -0
  135. package/dist/components/forms/DictationStatus.svelte.d.ts +11 -0
  136. package/dist/components/forms/DictationStatus.svelte.d.ts.map +1 -0
  137. package/dist/components/forms/Fieldset.svelte +5 -1
  138. package/dist/components/forms/Fieldset.svelte.d.ts +2 -0
  139. package/dist/components/forms/Fieldset.svelte.d.ts.map +1 -1
  140. package/dist/components/forms/Form.svelte +5 -0
  141. package/dist/components/forms/Form.svelte.d.ts +1 -0
  142. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  143. package/dist/components/forms/FormActionBar.svelte +84 -0
  144. package/dist/components/forms/FormActionBar.svelte.d.ts +23 -0
  145. package/dist/components/forms/FormActionBar.svelte.d.ts.map +1 -0
  146. package/dist/components/forms/Input.svelte +17 -1
  147. package/dist/components/forms/Input.svelte.d.ts +2 -0
  148. package/dist/components/forms/Input.svelte.d.ts.map +1 -1
  149. package/dist/components/forms/Listbox.svelte +39 -1
  150. package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
  151. package/dist/components/forms/MultiSelect.svelte +41 -1
  152. package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
  153. package/dist/components/forms/Radio.svelte +16 -4
  154. package/dist/components/forms/RadioGroup.svelte +7 -1
  155. package/dist/components/forms/RadioGroup.svelte.d.ts +2 -0
  156. package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
  157. package/dist/components/forms/SearchInput.svelte +207 -0
  158. package/dist/components/forms/SearchInput.svelte.d.ts +22 -0
  159. package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
  160. package/dist/components/forms/SegmentedControl.svelte +152 -11
  161. package/dist/components/forms/SegmentedControl.svelte.d.ts +4 -0
  162. package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
  163. package/dist/components/forms/Select.svelte +17 -1
  164. package/dist/components/forms/Select.svelte.d.ts +2 -0
  165. package/dist/components/forms/Select.svelte.d.ts.map +1 -1
  166. package/dist/components/forms/SignaturePad.svelte +436 -0
  167. package/dist/components/forms/SignaturePad.svelte.d.ts +32 -0
  168. package/dist/components/forms/SignaturePad.svelte.d.ts.map +1 -0
  169. package/dist/components/forms/StagedControlReview.svelte +21 -12
  170. package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -1
  171. package/dist/components/forms/Switch.svelte +18 -5
  172. package/dist/components/forms/Switch.svelte.d.ts +2 -0
  173. package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
  174. package/dist/components/forms/TagsInput.svelte +8 -1
  175. package/dist/components/forms/Textarea.svelte +16 -1
  176. package/dist/components/forms/Textarea.svelte.d.ts +2 -0
  177. package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
  178. package/dist/components/forms/__tests__/CameraCapture.test.js +504 -0
  179. package/dist/components/forms/__tests__/Combobox.test.js +354 -0
  180. package/dist/components/forms/__tests__/ControlHitArea.test.js +73 -0
  181. package/dist/components/forms/__tests__/DictationButton.test.js +176 -0
  182. package/dist/components/forms/__tests__/Fieldset.test.js +37 -0
  183. package/dist/components/forms/__tests__/Form.test.js +46 -0
  184. package/dist/components/forms/__tests__/FormActionBar.test.js +22 -0
  185. package/dist/components/forms/__tests__/Listbox.test.js +303 -0
  186. package/dist/components/forms/__tests__/MultiSelect.test.js +281 -0
  187. package/dist/components/forms/__tests__/SearchInput.test.js +92 -0
  188. package/dist/components/forms/__tests__/SignaturePad.test.js +338 -0
  189. package/dist/components/forms/__tests__/StagedControlReview.test.js +37 -3
  190. package/dist/components/forms/__tests__/camera-capture-session.test.js +207 -0
  191. package/dist/components/forms/__tests__/capture-form-retry.test.js +166 -0
  192. package/dist/components/forms/__tests__/capture-form.fixture.svelte +38 -0
  193. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +10 -0
  194. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +1 -0
  195. package/dist/components/forms/__tests__/capture-test-env.d.ts +70 -0
  196. package/dist/components/forms/__tests__/capture-test-env.d.ts.map +1 -0
  197. package/dist/components/forms/__tests__/capture-test-env.js +122 -0
  198. package/dist/components/forms/__tests__/capture-touch.fixture.svelte +15 -0
  199. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +8 -0
  200. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +1 -0
  201. package/dist/components/forms/__tests__/combobox-form.fixture.svelte +29 -0
  202. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +14 -0
  203. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +1 -0
  204. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +19 -0
  205. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +11 -0
  206. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +1 -0
  207. package/dist/components/forms/__tests__/composite-registration.fixture.svelte +29 -0
  208. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +8 -0
  209. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +1 -0
  210. package/dist/components/forms/__tests__/control-interaction.test.js +19 -1
  211. package/dist/components/forms/__tests__/control-proposals.test.js +63 -0
  212. package/dist/components/forms/__tests__/dictation-recording.test.js +582 -0
  213. package/dist/components/forms/__tests__/dictation.test.js +486 -0
  214. package/dist/components/forms/__tests__/form-attachment.fixture.svelte +28 -0
  215. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +11 -0
  216. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +1 -0
  217. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +31 -0
  218. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +13 -0
  219. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +1 -0
  220. package/dist/components/forms/__tests__/form-retry-form.test.js +107 -0
  221. package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +69 -0
  222. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +29 -0
  223. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +9 -0
  224. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +1 -0
  225. package/dist/components/forms/__tests__/listbox-form.fixture.svelte +27 -0
  226. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +13 -0
  227. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +1 -0
  228. package/dist/components/forms/__tests__/long-press.test.js +175 -0
  229. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +27 -0
  230. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +13 -0
  231. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +1 -0
  232. package/dist/components/forms/__tests__/native-file-field.test.js +177 -0
  233. package/dist/components/forms/__tests__/place-picker.fixture.svelte +33 -0
  234. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +12 -0
  235. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +1 -0
  236. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +28 -0
  237. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +12 -0
  238. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +1 -0
  239. package/dist/components/forms/__tests__/segmented-posting.test.js +92 -0
  240. package/dist/components/forms/__tests__/signature-pad-logic.test.js +54 -0
  241. package/dist/components/forms/__tests__/touch-density.fixture.svelte +34 -0
  242. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +8 -0
  243. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +1 -0
  244. package/dist/components/forms/__tests__/touch-density.test.js +66 -0
  245. package/dist/components/forms/audio-capture.d.ts +69 -0
  246. package/dist/components/forms/audio-capture.d.ts.map +1 -0
  247. package/dist/components/forms/audio-capture.js +193 -0
  248. package/dist/components/forms/camera-capture-session.d.ts +81 -0
  249. package/dist/components/forms/camera-capture-session.d.ts.map +1 -0
  250. package/dist/components/forms/camera-capture-session.js +130 -0
  251. package/dist/components/forms/control-interaction.d.ts +6 -0
  252. package/dist/components/forms/control-interaction.d.ts.map +1 -1
  253. package/dist/components/forms/control-interaction.js +12 -1
  254. package/dist/components/forms/control-proposals.d.ts +55 -0
  255. package/dist/components/forms/control-proposals.d.ts.map +1 -0
  256. package/dist/components/forms/control-proposals.js +188 -0
  257. package/dist/components/forms/dictation-transcribe.d.ts +36 -0
  258. package/dist/components/forms/dictation-transcribe.d.ts.map +1 -0
  259. package/dist/components/forms/dictation-transcribe.js +65 -0
  260. package/dist/components/forms/dictation.svelte.d.ts +200 -0
  261. package/dist/components/forms/dictation.svelte.d.ts.map +1 -0
  262. package/dist/components/forms/dictation.svelte.js +686 -0
  263. package/dist/components/forms/form-retry/__tests__/boundaries.test.js +23 -0
  264. package/dist/components/forms/form-retry/__tests__/content-digest.test.js +70 -0
  265. package/dist/components/forms/form-retry/__tests__/controller.test.js +802 -0
  266. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +82 -0
  267. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +1 -0
  268. package/dist/components/forms/form-retry/__tests__/fake-kit.js +180 -0
  269. package/dist/components/forms/form-retry/__tests__/form-draft.test.js +169 -0
  270. package/dist/components/forms/form-retry/__tests__/ssr.test.js +37 -0
  271. package/dist/components/forms/form-retry/__tests__/storage.d.ts +26 -0
  272. package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +1 -0
  273. package/dist/components/forms/form-retry/__tests__/storage.js +63 -0
  274. package/dist/components/forms/form-retry/__tests__/submission-key.test.js +236 -0
  275. package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +334 -0
  276. package/dist/components/forms/form-retry/content-digest.d.ts +54 -0
  277. package/dist/components/forms/form-retry/content-digest.d.ts.map +1 -0
  278. package/dist/components/forms/form-retry/content-digest.js +169 -0
  279. package/dist/components/forms/form-retry/controller.d.ts +214 -0
  280. package/dist/components/forms/form-retry/controller.d.ts.map +1 -0
  281. package/dist/components/forms/form-retry/controller.js +404 -0
  282. package/dist/components/forms/form-retry/form-draft.d.ts +68 -0
  283. package/dist/components/forms/form-retry/form-draft.d.ts.map +1 -0
  284. package/dist/components/forms/form-retry/form-draft.js +161 -0
  285. package/dist/components/forms/form-retry/index.d.ts +19 -0
  286. package/dist/components/forms/form-retry/index.d.ts.map +1 -0
  287. package/dist/components/forms/form-retry/index.js +16 -0
  288. package/dist/components/forms/form-retry/submission-key.d.ts +244 -0
  289. package/dist/components/forms/form-retry/submission-key.d.ts.map +1 -0
  290. package/dist/components/forms/form-retry/submission-key.js +342 -0
  291. package/dist/components/forms/form-retry/submitted-draft.d.ts +112 -0
  292. package/dist/components/forms/form-retry/submitted-draft.d.ts.map +1 -0
  293. package/dist/components/forms/form-retry/submitted-draft.js +270 -0
  294. package/dist/components/forms/form-retry/types.d.ts +84 -0
  295. package/dist/components/forms/form-retry/types.d.ts.map +1 -0
  296. package/dist/components/forms/form-retry/types.js +13 -0
  297. package/dist/components/forms/index.d.ts +21 -2
  298. package/dist/components/forms/index.d.ts.map +1 -1
  299. package/dist/components/forms/index.js +22 -1
  300. package/dist/components/forms/insert-text.d.ts +12 -0
  301. package/dist/components/forms/insert-text.d.ts.map +1 -0
  302. package/dist/components/forms/insert-text.js +38 -0
  303. package/dist/components/forms/long-press.d.ts +88 -0
  304. package/dist/components/forms/long-press.d.ts.map +1 -0
  305. package/dist/components/forms/long-press.js +213 -0
  306. package/dist/components/forms/native-file-field.d.ts +94 -0
  307. package/dist/components/forms/native-file-field.d.ts.map +1 -0
  308. package/dist/components/forms/native-file-field.js +147 -0
  309. package/dist/components/forms/ready-beep.d.ts +24 -0
  310. package/dist/components/forms/ready-beep.d.ts.map +1 -0
  311. package/dist/components/forms/ready-beep.js +82 -0
  312. package/dist/components/forms/signature-pad-logic.d.ts +50 -0
  313. package/dist/components/forms/signature-pad-logic.d.ts.map +1 -0
  314. package/dist/components/forms/signature-pad-logic.js +48 -0
  315. package/dist/components/forms/types.d.ts +41 -0
  316. package/dist/components/forms/types.d.ts.map +1 -1
  317. package/dist/components/layout/PageHeader.svelte +151 -17
  318. package/dist/components/layout/PageHeader.svelte.d.ts +47 -7
  319. package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -1
  320. package/dist/components/layout/__tests__/PageHeader.test.js +87 -1
  321. package/dist/components/layout/index.d.ts +1 -0
  322. package/dist/components/layout/index.d.ts.map +1 -1
  323. package/dist/components/layout/index.js +1 -0
  324. package/dist/components/layout/page-header-context.d.ts +25 -0
  325. package/dist/components/layout/page-header-context.d.ts.map +1 -0
  326. package/dist/components/layout/page-header-context.js +16 -0
  327. package/dist/components/nav/FilterChips.svelte +8 -1
  328. package/dist/components/nav/FilterChips.svelte.d.ts +2 -0
  329. package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -1
  330. package/dist/components/nav/Tabs.svelte +241 -0
  331. package/dist/components/nav/Tabs.svelte.d.ts +15 -0
  332. package/dist/components/nav/Tabs.svelte.d.ts.map +1 -1
  333. package/dist/components/nav/__tests__/LinkTabs.test.js +117 -0
  334. package/dist/components/nav/index.d.ts +1 -0
  335. package/dist/components/nav/index.d.ts.map +1 -1
  336. package/dist/components/nav/index.js +1 -0
  337. package/dist/components/nav/tabs-overflow.d.ts +12 -0
  338. package/dist/components/nav/tabs-overflow.d.ts.map +1 -0
  339. package/dist/components/nav/tabs-overflow.js +17 -0
  340. package/dist/components/nav/types.d.ts +8 -0
  341. package/dist/components/nav/types.d.ts.map +1 -1
  342. package/dist/components/ui/Button.svelte +16 -2
  343. package/dist/components/ui/Button.svelte.d.ts +2 -0
  344. package/dist/components/ui/Button.svelte.d.ts.map +1 -1
  345. package/dist/components/ui/Card.svelte +14 -5
  346. package/dist/components/ui/Dropdown.svelte +55 -1
  347. package/dist/components/ui/Dropdown.svelte.d.ts +8 -0
  348. package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -1
  349. package/dist/components/ui/IconToggle.svelte +139 -0
  350. package/dist/components/ui/IconToggle.svelte.d.ts +32 -0
  351. package/dist/components/ui/IconToggle.svelte.d.ts.map +1 -0
  352. package/dist/components/ui/IconToggleGroup.svelte +56 -0
  353. package/dist/components/ui/IconToggleGroup.svelte.d.ts +21 -0
  354. package/dist/components/ui/IconToggleGroup.svelte.d.ts.map +1 -0
  355. package/dist/components/ui/Pagination.svelte +1 -1
  356. package/dist/components/ui/Popover.svelte +17 -1
  357. package/dist/components/ui/Popover.svelte.d.ts +8 -0
  358. package/dist/components/ui/Popover.svelte.d.ts.map +1 -1
  359. package/dist/components/ui/__tests__/IconToggle.test.js +59 -0
  360. package/dist/components/ui/__tests__/IconToggleGroup.test.js +30 -0
  361. package/dist/components/ui/__tests__/Popover.test.js +44 -0
  362. package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +125 -0
  363. package/dist/components/ui/__tests__/phone-touch-targets.test.js +39 -0
  364. package/dist/components/ui/index.d.ts +2 -0
  365. package/dist/components/ui/index.d.ts.map +1 -1
  366. package/dist/components/ui/index.js +2 -0
  367. package/dist/i18n/strings.ui.d.ts +65 -0
  368. package/dist/i18n/strings.ui.d.ts.map +1 -1
  369. package/dist/i18n/strings.ui.js +72 -0
  370. package/dist/styles/tokens.css +2 -0
  371. package/dist/svelte/__tests__/playground.test.js +3 -1
  372. package/dist/svelte/playground/CalendarPreview.svelte +140 -0
  373. package/dist/svelte/playground/CalendarPreview.svelte.d.ts +4 -0
  374. package/dist/svelte/playground/CalendarPreview.svelte.d.ts.map +1 -0
  375. package/dist/svelte/playground/CapturePreview.svelte +56 -0
  376. package/dist/svelte/playground/CapturePreview.svelte.d.ts +4 -0
  377. package/dist/svelte/playground/CapturePreview.svelte.d.ts.map +1 -0
  378. package/dist/svelte/playground.d.ts.map +1 -1
  379. package/dist/svelte/playground.js +20 -0
  380. package/dist/themes/ThemeProvider.svelte +9 -0
  381. package/dist/themes/ThemeProvider.svelte.d.ts +2 -0
  382. package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
  383. package/dist/themes/css-generator.d.ts.map +1 -1
  384. package/dist/themes/css-generator.js +3 -0
  385. package/dist/themes/styles/glass.css +4 -0
  386. package/dist/themes/styles/happyvertical.css +4 -0
  387. package/dist/themes/styles/material.css +4 -0
  388. package/dist/themes/styles/smrt.css +4 -0
  389. package/dist/themes/styles/studio.css +4 -0
  390. package/dist/types-generic.d.ts +2 -0
  391. package/dist/types-generic.d.ts.map +1 -1
  392. package/package.json +9 -3
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The per-form submission key: the browser half of `runOnce()` (#3291).
3
+ *
4
+ * `runOnce()` (`@happyvertical/smrt-core`, #3080) makes a write exactly-once
5
+ * when a retry carries the same claim. The claim is derived from the session's
6
+ * tenant and actor, this key, and a digest of the submitted content, so two
7
+ * submits carrying the same key and the same content record one row, and a
8
+ * submit carrying the same key with DIFFERENT content is a new submission
9
+ * (#3136).
10
+ *
11
+ * ## Why the key outlives the component
12
+ *
13
+ * The failure the key exists for is a lost response, and `enhance`'s error
14
+ * path can unmount the form. A key held only in component state would be
15
+ * re-minted on the way back and the retry would record a duplicate — the whole
16
+ * bug. So the key lives in storage: per tab (`sessionStorage`), per form (one
17
+ * slot per form, optionally scoped per record), cleared as soon as a submit is
18
+ * confirmed recorded, and gone when the tab closes.
19
+ *
20
+ * It is never `localStorage`: a tablet or kiosk is shared hardware, and one
21
+ * person's key must not still be sitting there for the next. (A key that does
22
+ * outlive one person's visit is harmless — the claim also covers actor and
23
+ * content — but there is no reason to keep it.)
24
+ *
25
+ * ## Every storage access is wrapped
26
+ *
27
+ * `sessionStorage` throws in private windows, with site data blocked, and in
28
+ * some embedded webviews; even reading the `sessionStorage` property can throw
29
+ * a `SecurityError`. Every access is guarded, and a refused write falls back
30
+ * to an in-memory store, which is read before the backing store so a value
31
+ * the store could not replace or remove is never handed back (a refused
32
+ * removal overwrites the value with a cleared marker; see `guardedRemove`).
33
+ * In the browser that store is module-level, so the key still survives a
34
+ * remount for the life of the page — only a full reload loses it, which is
35
+ * less idempotent, never wrong. During SSR there is no shared
36
+ * store at all — neither the module-level memory nor the default storage (Node
37
+ * 25+ has a process-wide `sessionStorage` global): either would hand one
38
+ * request's key to another, so each read off the browser mints a fresh,
39
+ * unshared key.
40
+ *
41
+ * ## One unresolved submit per key
42
+ *
43
+ * One key per form is only meaningful while at most one submission is
44
+ * unresolved under it. Without that, a person can submit A, change the form,
45
+ * and submit B before A answers: both carry key K, A's answer rotates the slot
46
+ * to K2, and B — a genuinely different write — is left holding a key nothing
47
+ * will reconcile; if B commits and loses its response, its retry carries K2,
48
+ * takes a different claim, and records a second row. So {@link beginSubmit}
49
+ * refuses a second submit while one is unresolved. That cancels the
50
+ * submission itself rather than hoping a disabled attribute was flushed in
51
+ * time, and it holds for a second DIFFERENT submission, not just a double tap.
52
+ *
53
+ * @module
54
+ */
55
+ /** The storage operations the helper uses: a subset of the DOM `Storage`. */
56
+ export interface FormRetryStorage {
57
+ /** Read a value, or `null` when nothing is stored. May throw. */
58
+ getItem(key: string): string | null;
59
+ /** Store a value. May throw (private window, quota). */
60
+ setItem(key: string, value: string): void;
61
+ /** Remove a value. May throw. */
62
+ removeItem(key: string): void;
63
+ }
64
+ /**
65
+ * Where a form's key and submitted draft live.
66
+ *
67
+ * `storage` is the backing store (default: this tab's `sessionStorage`,
68
+ * resolved lazily on every access because even reading the property can
69
+ * throw); pass `null` to keep everything in memory.
70
+ */
71
+ export interface SubmissionKeyLocation {
72
+ /** One slot per FORM, not per page: two forms on one page hold two keys. */
73
+ form: string;
74
+ /**
75
+ * One key per record within a form kind (a form rendered once per row).
76
+ * One record's confirmed write must never rotate the key another record's
77
+ * unresolved retry still carries.
78
+ */
79
+ scope?: string;
80
+ /** Backing store. Defaults to `sessionStorage`; `null` means memory only. */
81
+ storage?: FormRetryStorage | null;
82
+ /**
83
+ * Exact storage name for the key, overriding the derived
84
+ * `smrt:form-retry:<form>[:<scope>]:key`. For migrating an existing app
85
+ * whose tablets hold keys under an older name: renaming silently loses
86
+ * every unresolved key, and the next retry records a duplicate.
87
+ */
88
+ storageKey?: string;
89
+ /** Mints a new key. Defaults to {@link mintSubmissionKey} (UUID v4). */
90
+ mint?: () => string;
91
+ }
92
+ /** Test seam: forget every in-memory key and draft. */
93
+ export declare function resetFormRetryMemory(): void;
94
+ /**
95
+ * Read `key` from memory, then from `storage`. Memory holds a value only when
96
+ * its storage write was refused (a successful write and every removal drop the
97
+ * memory copy), so it is the newer of the two: a store that refused to replace
98
+ * or remove an old value must not hand that stale value back. A cleared value
99
+ * ({@link guardedRemove}) reads as `null` from either place, and a cleared
100
+ * memory entry hides whatever the store still holds.
101
+ * @internal
102
+ */
103
+ export declare function guardedGet(storage: FormRetryStorage | null | undefined, key: string): string | null;
104
+ /**
105
+ * Write `key`, falling back to memory when storage refuses. Returns whether the
106
+ * value reached the backing store (it survives a reload).
107
+ * @internal
108
+ */
109
+ export declare function guardedSet(storage: FormRetryStorage | null | undefined, key: string, value: string): boolean;
110
+ /**
111
+ * Remove `key` from storage and memory alike, so no later read returns it.
112
+ *
113
+ * A store may refuse `removeItem()` while reads still succeed. The old value
114
+ * is then overwritten with a cleared marker, which survives a reload of the
115
+ * tab like any stored value. If that write is refused too, the marker is kept
116
+ * in memory, which reads consult first: the old value is never read back for
117
+ * the life of the page. A store that refuses both cannot be changed at all,
118
+ * so after a full reload it still holds the old value; nothing in the page
119
+ * can prevent that.
120
+ * @internal
121
+ */
122
+ export declare function guardedRemove(storage: FormRetryStorage | null | undefined, key: string): void;
123
+ /** The derived storage base for a form (and scope). @internal */
124
+ export declare function storageBase(form: string, scope?: string): string;
125
+ /**
126
+ * A random v4 UUID from `crypto.getRandomValues` — unguessable, and accepted
127
+ * by a server-side UUID parser.
128
+ */
129
+ export declare function mintSubmissionKey(): string;
130
+ /**
131
+ * This tab's key for a form, minting and storing one on first use. The same
132
+ * value comes back across a remount or a reload until it is cleared, so every
133
+ * retry of one filled-in form carries one key.
134
+ */
135
+ export declare function readSubmissionKey(location: SubmissionKeyLocation): string;
136
+ /**
137
+ * Whether the form's key currently lives in the backing store (and so
138
+ * survives a reload), rather than only in memory.
139
+ */
140
+ export declare function isSubmissionKeyPersistent(location: SubmissionKeyLocation): boolean;
141
+ /**
142
+ * Drop the key once its submit is confirmed recorded, so the next fill of the
143
+ * form starts a claim of its own. This is the boundary between a resubmission
144
+ * and a deliberate repeat: adding the same line twice on purpose is a new fill
145
+ * and takes a new key; a double tap, a retry after a lost response, and two
146
+ * concurrent requests carry the key the form still holds.
147
+ *
148
+ * Call it only on a result the server confirmed recorded — never on a
149
+ * validation failure, whose submit wrote nothing and whose key is still free.
150
+ */
151
+ export declare function clearSubmissionKey(location: SubmissionKeyLocation): void;
152
+ /**
153
+ * Clear the key and mint the next one, returning it — for an inline form that
154
+ * stays on screen after a confirmed write.
155
+ *
156
+ * The fresh key is written over the old one directly rather than read back:
157
+ * a store may refuse `removeItem()` while reads still succeed, and reading
158
+ * after a failed clear would return the retired key. If the replacement write
159
+ * is refused too, the fresh key lives in memory, which {@link guardedGet}
160
+ * consults before the store.
161
+ */
162
+ export declare function rotateSubmissionKey(location: SubmissionKeyLocation): string;
163
+ /**
164
+ * One form's submission state: the key the next submit carries, and the key of
165
+ * the submit that has not answered yet (`null` when idle).
166
+ */
167
+ export interface SubmissionSlot {
168
+ /** The key the next submit carries. */
169
+ token: string;
170
+ /** The key of the unresolved submit, or `null` when the form is idle. */
171
+ inFlight: string | null;
172
+ }
173
+ /**
174
+ * Start a submit, or refuse it because one is still unresolved.
175
+ *
176
+ * `start: false` means the caller must `cancel()` the submission: nothing has
177
+ * been sent and the form keeps its content. A double tap lands here too — the
178
+ * second activation is cancelled in the browser, which costs nothing, because
179
+ * the server-side claim still covers every request the browser or the network
180
+ * retries on its own.
181
+ */
182
+ export declare function beginSubmit(state: SubmissionSlot): {
183
+ state: SubmissionSlot;
184
+ start: boolean;
185
+ sent: string;
186
+ };
187
+ /** What a result means for the form. */
188
+ export type SubmitDisposition = 'ignore' | 'transport-error' | 'rotate-and-apply' | 'apply';
189
+ /** The outcome kinds a result can have (SvelteKit's `ActionResult['type']`). */
190
+ export type SubmitOutcome = 'success' | 'failure' | 'redirect' | 'error';
191
+ /**
192
+ * The rotation policy as one pure function.
193
+ *
194
+ * `sent` is the key the submit carried; `live` is the key of the slot's
195
+ * unresolved submit, or `null` when it has none left to settle.
196
+ *
197
+ * - **A stale result is ignored outright.** A double tap can send two requests
198
+ * under one key, so two results come back, and by the time the second lands
199
+ * the first has already settled the slot. Acting on it would reset the form
200
+ * and rotate AGAIN, out from under a submission started since.
201
+ * - **A transport error keeps the key and the form.** It wrote nothing that
202
+ * the browser can know about, and the claim covers the content, so the
203
+ * unchanged re-submit must be able to reproduce it.
204
+ * - **A confirmed write applies**, and retires the key only if the form is
205
+ * cleared by that apply — see {@link settleSubmit}.
206
+ * - **A validation failure applies without rotating**: nothing was written, so
207
+ * the key is still free and the corrected re-submit keeps the same key.
208
+ * - **A redirect** is a confirmed write only when the caller says so
209
+ * (`redirectConfirmsWrite`); otherwise it is treated like a validation
210
+ * result, because it cannot vouch for a write (a redirect to a sign-in page
211
+ * wrote nothing).
212
+ */
213
+ export declare function submitDisposition(sent: string, live: string | null, outcome: SubmitOutcome, options?: {
214
+ redirectConfirmsWrite?: boolean;
215
+ }): SubmitDisposition;
216
+ /**
217
+ * Settle one result against the slot, returning the new slot and what to do.
218
+ *
219
+ * ## A key is retired when its FORM IS CLEARED, not merely on success
220
+ *
221
+ * Rotating on success alone leaves a window where the form still SHOWS
222
+ * content that is already recorded while holding a fresh key — after an edit
223
+ * the conditional reset preserved, or after a remount put the submitted values
224
+ * back. A further submit from that form would then take a new claim and record
225
+ * a genuine second row from content nobody re-entered. Keeping the key instead
226
+ * is right, and safe because the claim covers the CONTENT too:
227
+ *
228
+ * - the form still shows what was recorded, so re-submitting it derives the
229
+ * SAME claim and collapses onto the row that already exists;
230
+ * - the form shows something genuinely different, so it derives a DIFFERENT
231
+ * claim and is recorded, which is what it should be.
232
+ *
233
+ * Only an emptied form has nothing left to reconcile, so only then does the
234
+ * next entry deserve a key of its own.
235
+ */
236
+ export declare function settleSubmit(state: SubmissionSlot, sent: string, outcome: SubmitOutcome, options: {
237
+ formCleared: boolean;
238
+ mint: () => string;
239
+ redirectConfirmsWrite?: boolean;
240
+ }): {
241
+ state: SubmissionSlot;
242
+ disposition: SubmitDisposition;
243
+ };
244
+ //# sourceMappingURL=submission-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission-key.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/form-retry/submission-key.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,342 @@
1
+ /**
2
+ * The per-form submission key: the browser half of `runOnce()` (#3291).
3
+ *
4
+ * `runOnce()` (`@happyvertical/smrt-core`, #3080) makes a write exactly-once
5
+ * when a retry carries the same claim. The claim is derived from the session's
6
+ * tenant and actor, this key, and a digest of the submitted content, so two
7
+ * submits carrying the same key and the same content record one row, and a
8
+ * submit carrying the same key with DIFFERENT content is a new submission
9
+ * (#3136).
10
+ *
11
+ * ## Why the key outlives the component
12
+ *
13
+ * The failure the key exists for is a lost response, and `enhance`'s error
14
+ * path can unmount the form. A key held only in component state would be
15
+ * re-minted on the way back and the retry would record a duplicate — the whole
16
+ * bug. So the key lives in storage: per tab (`sessionStorage`), per form (one
17
+ * slot per form, optionally scoped per record), cleared as soon as a submit is
18
+ * confirmed recorded, and gone when the tab closes.
19
+ *
20
+ * It is never `localStorage`: a tablet or kiosk is shared hardware, and one
21
+ * person's key must not still be sitting there for the next. (A key that does
22
+ * outlive one person's visit is harmless — the claim also covers actor and
23
+ * content — but there is no reason to keep it.)
24
+ *
25
+ * ## Every storage access is wrapped
26
+ *
27
+ * `sessionStorage` throws in private windows, with site data blocked, and in
28
+ * some embedded webviews; even reading the `sessionStorage` property can throw
29
+ * a `SecurityError`. Every access is guarded, and a refused write falls back
30
+ * to an in-memory store, which is read before the backing store so a value
31
+ * the store could not replace or remove is never handed back (a refused
32
+ * removal overwrites the value with a cleared marker; see `guardedRemove`).
33
+ * In the browser that store is module-level, so the key still survives a
34
+ * remount for the life of the page — only a full reload loses it, which is
35
+ * less idempotent, never wrong. During SSR there is no shared
36
+ * store at all — neither the module-level memory nor the default storage (Node
37
+ * 25+ has a process-wide `sessionStorage` global): either would hand one
38
+ * request's key to another, so each read off the browser mints a fresh,
39
+ * unshared key.
40
+ *
41
+ * ## One unresolved submit per key
42
+ *
43
+ * One key per form is only meaningful while at most one submission is
44
+ * unresolved under it. Without that, a person can submit A, change the form,
45
+ * and submit B before A answers: both carry key K, A's answer rotates the slot
46
+ * to K2, and B — a genuinely different write — is left holding a key nothing
47
+ * will reconcile; if B commits and loses its response, its retry carries K2,
48
+ * takes a different claim, and records a second row. So {@link beginSubmit}
49
+ * refuses a second submit while one is unresolved. That cancels the
50
+ * submission itself rather than hoping a disabled attribute was flushed in
51
+ * time, and it holds for a second DIFFERENT submission, not just a double tap.
52
+ *
53
+ * @module
54
+ */
55
+ const PREFIX = 'smrt:form-retry';
56
+ /** True in a browser, where a module-level memory store is per-person. */
57
+ function inBrowser() {
58
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
59
+ }
60
+ /**
61
+ * The in-memory fallback. Module-level in the browser so a key survives a
62
+ * remount; never shared on a server (see the module doc).
63
+ */
64
+ const browserMemory = new Map();
65
+ function memoryStore() {
66
+ return inBrowser() ? browserMemory : null;
67
+ }
68
+ /** Test seam: forget every in-memory key and draft. */
69
+ export function resetFormRetryMemory() {
70
+ browserMemory.clear();
71
+ }
72
+ /**
73
+ * This tab's `sessionStorage`, or `null` where reading it throws — and `null`
74
+ * off the browser: Node 25+ exposes a process-wide in-memory `sessionStorage`
75
+ * global, which during SSR would hand one request's key to another.
76
+ */
77
+ function defaultStorage() {
78
+ if (!inBrowser())
79
+ return null;
80
+ try {
81
+ return (globalThis.sessionStorage ??
82
+ null);
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ function resolveStorage(storage) {
89
+ return storage === undefined ? defaultStorage() : storage;
90
+ }
91
+ /**
92
+ * What a clear writes when the store refuses `removeItem()`: a value no key or
93
+ * draft can take, which every read treats as absent. It is written over the
94
+ * old value so a store that still reads cannot hand that value back.
95
+ */
96
+ const CLEARED = '\u0000smrt:form-retry:cleared';
97
+ /**
98
+ * Read `key` from memory, then from `storage`. Memory holds a value only when
99
+ * its storage write was refused (a successful write and every removal drop the
100
+ * memory copy), so it is the newer of the two: a store that refused to replace
101
+ * or remove an old value must not hand that stale value back. A cleared value
102
+ * ({@link guardedRemove}) reads as `null` from either place, and a cleared
103
+ * memory entry hides whatever the store still holds.
104
+ * @internal
105
+ */
106
+ export function guardedGet(storage, key) {
107
+ const remembered = memoryStore()?.get(key);
108
+ if (remembered !== undefined) {
109
+ return remembered === CLEARED ? null : remembered;
110
+ }
111
+ const store = resolveStorage(storage);
112
+ if (!store)
113
+ return null;
114
+ try {
115
+ const value = store.getItem(key) ?? null;
116
+ return value === CLEARED ? null : value;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ /**
123
+ * Write `key`, falling back to memory when storage refuses. Returns whether the
124
+ * value reached the backing store (it survives a reload).
125
+ * @internal
126
+ */
127
+ export function guardedSet(storage, key, value) {
128
+ const store = resolveStorage(storage);
129
+ if (store) {
130
+ try {
131
+ store.setItem(key, value);
132
+ memoryStore()?.delete(key);
133
+ return true;
134
+ }
135
+ catch {
136
+ // Refused: fall through to memory.
137
+ }
138
+ }
139
+ memoryStore()?.set(key, value);
140
+ return false;
141
+ }
142
+ /**
143
+ * Remove `key` from storage and memory alike, so no later read returns it.
144
+ *
145
+ * A store may refuse `removeItem()` while reads still succeed. The old value
146
+ * is then overwritten with a cleared marker, which survives a reload of the
147
+ * tab like any stored value. If that write is refused too, the marker is kept
148
+ * in memory, which reads consult first: the old value is never read back for
149
+ * the life of the page. A store that refuses both cannot be changed at all,
150
+ * so after a full reload it still holds the old value; nothing in the page
151
+ * can prevent that.
152
+ * @internal
153
+ */
154
+ export function guardedRemove(storage, key) {
155
+ const memory = memoryStore();
156
+ memory?.delete(key);
157
+ const store = resolveStorage(storage);
158
+ if (!store)
159
+ return;
160
+ try {
161
+ store.removeItem(key);
162
+ return;
163
+ }
164
+ catch {
165
+ // Refused: overwrite the old value instead.
166
+ }
167
+ try {
168
+ store.setItem(key, CLEARED);
169
+ return;
170
+ }
171
+ catch {
172
+ // Refused too: hide the old value for the life of the page.
173
+ }
174
+ memory?.set(key, CLEARED);
175
+ }
176
+ /** The derived storage base for a form (and scope). @internal */
177
+ export function storageBase(form, scope) {
178
+ const base = `${PREFIX}:${encodeURIComponent(form)}`;
179
+ return scope === undefined ? base : `${base}:${encodeURIComponent(scope)}`;
180
+ }
181
+ function keyName(location) {
182
+ return (location.storageKey ?? `${storageBase(location.form, location.scope)}:key`);
183
+ }
184
+ /**
185
+ * A random v4 UUID from `crypto.getRandomValues` — unguessable, and accepted
186
+ * by a server-side UUID parser.
187
+ */
188
+ export function mintSubmissionKey() {
189
+ const bytes = new Uint8Array(16);
190
+ globalThis.crypto.getRandomValues(bytes);
191
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
192
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
193
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
194
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
195
+ }
196
+ /**
197
+ * This tab's key for a form, minting and storing one on first use. The same
198
+ * value comes back across a remount or a reload until it is cleared, so every
199
+ * retry of one filled-in form carries one key.
200
+ */
201
+ export function readSubmissionKey(location) {
202
+ const name = keyName(location);
203
+ const existing = guardedGet(location.storage, name);
204
+ if (existing)
205
+ return existing;
206
+ const minted = (location.mint ?? mintSubmissionKey)();
207
+ guardedSet(location.storage, name, minted);
208
+ return minted;
209
+ }
210
+ /**
211
+ * Whether the form's key currently lives in the backing store (and so
212
+ * survives a reload), rather than only in memory.
213
+ */
214
+ export function isSubmissionKeyPersistent(location) {
215
+ const store = resolveStorage(location.storage);
216
+ if (!store)
217
+ return false;
218
+ // A key held in memory is one the store refused to write: whatever the
219
+ // store still holds under that name is stale.
220
+ if (memoryStore()?.has(keyName(location)))
221
+ return false;
222
+ try {
223
+ const value = store.getItem(keyName(location));
224
+ return value !== null && value !== CLEARED;
225
+ }
226
+ catch {
227
+ return false;
228
+ }
229
+ }
230
+ /**
231
+ * Drop the key once its submit is confirmed recorded, so the next fill of the
232
+ * form starts a claim of its own. This is the boundary between a resubmission
233
+ * and a deliberate repeat: adding the same line twice on purpose is a new fill
234
+ * and takes a new key; a double tap, a retry after a lost response, and two
235
+ * concurrent requests carry the key the form still holds.
236
+ *
237
+ * Call it only on a result the server confirmed recorded — never on a
238
+ * validation failure, whose submit wrote nothing and whose key is still free.
239
+ */
240
+ export function clearSubmissionKey(location) {
241
+ guardedRemove(location.storage, keyName(location));
242
+ }
243
+ /**
244
+ * Clear the key and mint the next one, returning it — for an inline form that
245
+ * stays on screen after a confirmed write.
246
+ *
247
+ * The fresh key is written over the old one directly rather than read back:
248
+ * a store may refuse `removeItem()` while reads still succeed, and reading
249
+ * after a failed clear would return the retired key. If the replacement write
250
+ * is refused too, the fresh key lives in memory, which {@link guardedGet}
251
+ * consults before the store.
252
+ */
253
+ export function rotateSubmissionKey(location) {
254
+ clearSubmissionKey(location);
255
+ const minted = (location.mint ?? mintSubmissionKey)();
256
+ guardedSet(location.storage, keyName(location), minted);
257
+ return minted;
258
+ }
259
+ /**
260
+ * Start a submit, or refuse it because one is still unresolved.
261
+ *
262
+ * `start: false` means the caller must `cancel()` the submission: nothing has
263
+ * been sent and the form keeps its content. A double tap lands here too — the
264
+ * second activation is cancelled in the browser, which costs nothing, because
265
+ * the server-side claim still covers every request the browser or the network
266
+ * retries on its own.
267
+ */
268
+ export function beginSubmit(state) {
269
+ if (state.inFlight !== null) {
270
+ return { state, start: false, sent: state.inFlight };
271
+ }
272
+ return {
273
+ state: { ...state, inFlight: state.token },
274
+ start: true,
275
+ sent: state.token,
276
+ };
277
+ }
278
+ /**
279
+ * The rotation policy as one pure function.
280
+ *
281
+ * `sent` is the key the submit carried; `live` is the key of the slot's
282
+ * unresolved submit, or `null` when it has none left to settle.
283
+ *
284
+ * - **A stale result is ignored outright.** A double tap can send two requests
285
+ * under one key, so two results come back, and by the time the second lands
286
+ * the first has already settled the slot. Acting on it would reset the form
287
+ * and rotate AGAIN, out from under a submission started since.
288
+ * - **A transport error keeps the key and the form.** It wrote nothing that
289
+ * the browser can know about, and the claim covers the content, so the
290
+ * unchanged re-submit must be able to reproduce it.
291
+ * - **A confirmed write applies**, and retires the key only if the form is
292
+ * cleared by that apply — see {@link settleSubmit}.
293
+ * - **A validation failure applies without rotating**: nothing was written, so
294
+ * the key is still free and the corrected re-submit keeps the same key.
295
+ * - **A redirect** is a confirmed write only when the caller says so
296
+ * (`redirectConfirmsWrite`); otherwise it is treated like a validation
297
+ * result, because it cannot vouch for a write (a redirect to a sign-in page
298
+ * wrote nothing).
299
+ */
300
+ export function submitDisposition(sent, live, outcome, options = {}) {
301
+ if (live === null || sent !== live)
302
+ return 'ignore';
303
+ if (outcome === 'error')
304
+ return 'transport-error';
305
+ if (outcome === 'success')
306
+ return 'rotate-and-apply';
307
+ if (outcome === 'redirect' && options.redirectConfirmsWrite) {
308
+ return 'rotate-and-apply';
309
+ }
310
+ return 'apply';
311
+ }
312
+ /**
313
+ * Settle one result against the slot, returning the new slot and what to do.
314
+ *
315
+ * ## A key is retired when its FORM IS CLEARED, not merely on success
316
+ *
317
+ * Rotating on success alone leaves a window where the form still SHOWS
318
+ * content that is already recorded while holding a fresh key — after an edit
319
+ * the conditional reset preserved, or after a remount put the submitted values
320
+ * back. A further submit from that form would then take a new claim and record
321
+ * a genuine second row from content nobody re-entered. Keeping the key instead
322
+ * is right, and safe because the claim covers the CONTENT too:
323
+ *
324
+ * - the form still shows what was recorded, so re-submitting it derives the
325
+ * SAME claim and collapses onto the row that already exists;
326
+ * - the form shows something genuinely different, so it derives a DIFFERENT
327
+ * claim and is recorded, which is what it should be.
328
+ *
329
+ * Only an emptied form has nothing left to reconcile, so only then does the
330
+ * next entry deserve a key of its own.
331
+ */
332
+ export function settleSubmit(state, sent, outcome, options) {
333
+ const disposition = submitDisposition(sent, state.inFlight, outcome, {
334
+ redirectConfirmsWrite: options.redirectConfirmsWrite,
335
+ });
336
+ if (disposition === 'ignore')
337
+ return { state, disposition };
338
+ if (disposition === 'rotate-and-apply' && options.formCleared) {
339
+ return { state: { token: options.mint(), inFlight: null }, disposition };
340
+ }
341
+ return { state: { ...state, inFlight: null }, disposition };
342
+ }