@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,236 @@
1
+ /**
2
+ * The submission key and its slot (#3291): one key per filled-in form,
3
+ * rotated at most once, never rotated by a late result, and never lost to a
4
+ * storage error.
5
+ */
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { beginSubmit, clearSubmissionKey, isSubmissionKeyPersistent, mintSubmissionKey, readSubmissionKey, resetFormRetryMemory, rotateSubmissionKey, settleSubmit, submitDisposition, } from '../submission-key.js';
8
+ import { fullStorage, memoryStorage, refusingStorage, stickyStorage, } from './storage.js';
9
+ const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
10
+ beforeEach(() => {
11
+ resetFormRetryMemory();
12
+ });
13
+ afterEach(() => {
14
+ vi.unstubAllGlobals();
15
+ });
16
+ describe('submitDisposition', () => {
17
+ const LIVE = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
18
+ const STALE = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
19
+ it('rotates on a confirmed write', () => {
20
+ expect(submitDisposition(LIVE, LIVE, 'success')).toBe('rotate-and-apply');
21
+ });
22
+ it('keeps the key on a validation failure, which wrote nothing', () => {
23
+ expect(submitDisposition(LIVE, LIVE, 'failure')).toBe('apply');
24
+ });
25
+ it('keeps the key AND the form on a transport error', () => {
26
+ expect(submitDisposition(LIVE, LIVE, 'error')).toBe('transport-error');
27
+ });
28
+ it('ignores every result whose key is no longer the live one', () => {
29
+ for (const outcome of [
30
+ 'success',
31
+ 'failure',
32
+ 'redirect',
33
+ 'error',
34
+ ]) {
35
+ expect(submitDisposition(STALE, LIVE, outcome)).toBe('ignore');
36
+ expect(submitDisposition(LIVE, null, outcome)).toBe('ignore');
37
+ }
38
+ });
39
+ it('treats a redirect as unconfirmed unless the caller says otherwise', () => {
40
+ expect(submitDisposition(LIVE, LIVE, 'redirect')).toBe('apply');
41
+ expect(submitDisposition(LIVE, LIVE, 'redirect', {
42
+ redirectConfirmsWrite: true,
43
+ })).toBe('rotate-and-apply');
44
+ });
45
+ });
46
+ describe('the submission slot', () => {
47
+ const A = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
48
+ const B = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
49
+ const C = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc';
50
+ const idle = (token = A) => ({ token, inFlight: null });
51
+ it('refuses a SECOND submit while one is unresolved', () => {
52
+ const first = beginSubmit(idle());
53
+ expect(first).toMatchObject({ start: true, sent: A });
54
+ const second = beginSubmit(first.state);
55
+ expect(second.start).toBe(false);
56
+ expect(second.state).toEqual(first.state);
57
+ });
58
+ it('accepts the next submit once the first has settled', () => {
59
+ const first = beginSubmit(idle());
60
+ const settled = settleSubmit(first.state, first.sent, 'success', {
61
+ formCleared: true,
62
+ mint: () => B,
63
+ });
64
+ expect(settled).toEqual({
65
+ state: { token: B, inFlight: null },
66
+ disposition: 'rotate-and-apply',
67
+ });
68
+ expect(beginSubmit(settled.state)).toMatchObject({ start: true, sent: B });
69
+ });
70
+ it('keeps the key when the submit wrote nothing', () => {
71
+ for (const outcome of ['failure', 'error']) {
72
+ const first = beginSubmit(idle());
73
+ const settled = settleSubmit(first.state, first.sent, outcome, {
74
+ formCleared: true,
75
+ mint: () => B,
76
+ });
77
+ expect(settled.state, outcome).toEqual({ token: A, inFlight: null });
78
+ }
79
+ });
80
+ it('ignores a duplicate answer to a submit already settled', () => {
81
+ const first = beginSubmit(idle());
82
+ const settled = settleSubmit(first.state, first.sent, 'success', {
83
+ formCleared: true,
84
+ mint: () => B,
85
+ });
86
+ const late = settleSubmit(settled.state, first.sent, 'success', {
87
+ formCleared: true,
88
+ mint: () => C,
89
+ });
90
+ expect(late).toEqual({ state: settled.state, disposition: 'ignore' });
91
+ });
92
+ it('rotates at most once however many answers arrive', () => {
93
+ const begun = beginSubmit(idle());
94
+ let state = begun.state;
95
+ const mint = vi.fn(() => B);
96
+ for (let i = 0; i < 3; i += 1) {
97
+ state = settleSubmit(state, begun.sent, 'success', {
98
+ formCleared: true,
99
+ mint,
100
+ }).state;
101
+ }
102
+ expect(mint).toHaveBeenCalledTimes(1);
103
+ });
104
+ it('KEEPS the key when a confirmed write left content on the form', () => {
105
+ const first = beginSubmit(idle());
106
+ const settled = settleSubmit(first.state, first.sent, 'success', {
107
+ formCleared: false,
108
+ mint: () => B,
109
+ });
110
+ expect(settled).toEqual({
111
+ state: { token: A, inFlight: null },
112
+ disposition: 'rotate-and-apply',
113
+ });
114
+ });
115
+ });
116
+ describe('the key store', () => {
117
+ it('returns one stable UUID per form until it is rotated', () => {
118
+ const storage = memoryStorage();
119
+ const first = readSubmissionKey({ form: 'po-line', storage });
120
+ expect(first).toMatch(UUID_V4);
121
+ expect(readSubmissionKey({ form: 'po-line', storage })).toBe(first);
122
+ const rotated = rotateSubmissionKey({ form: 'po-line', storage });
123
+ expect(rotated).not.toBe(first);
124
+ expect(readSubmissionKey({ form: 'po-line', storage })).toBe(rotated);
125
+ });
126
+ it('keeps forms and scopes apart, under derived names', () => {
127
+ const storage = memoryStorage();
128
+ const a = readSubmissionKey({ form: 'receipt', scope: 'po/a', storage });
129
+ const b = readSubmissionKey({ form: 'receipt', scope: 'po-b', storage });
130
+ const other = readSubmissionKey({ form: 'request', storage });
131
+ expect(new Set([a, b, other]).size).toBe(3);
132
+ expect([...storage.store.keys()].sort()).toEqual([
133
+ 'smrt:form-retry:receipt:po%2Fa:key',
134
+ 'smrt:form-retry:receipt:po-b:key',
135
+ 'smrt:form-retry:request:key',
136
+ ]);
137
+ clearSubmissionKey({ form: 'receipt', scope: 'po-b', storage });
138
+ expect(readSubmissionKey({ form: 'receipt', scope: 'po/a', storage })).toBe(a);
139
+ });
140
+ it('honours an explicit storage name, for migrating existing keys', () => {
141
+ const storage = memoryStorage();
142
+ storage.store.set('legacy:flha:submission-key', 'carried');
143
+ expect(readSubmissionKey({
144
+ form: 'flha',
145
+ storage,
146
+ storageKey: 'legacy:flha:submission-key',
147
+ })).toBe('carried');
148
+ });
149
+ it('uses this tab sessionStorage by default, never localStorage', () => {
150
+ const session = memoryStorage();
151
+ const local = memoryStorage();
152
+ vi.stubGlobal('sessionStorage', session);
153
+ vi.stubGlobal('localStorage', local);
154
+ const key = readSubmissionKey({ form: 'po' });
155
+ expect(session.store.get('smrt:form-retry:po:key')).toBe(key);
156
+ expect(local.store.size).toBe(0);
157
+ expect(isSubmissionKeyPersistent({ form: 'po' })).toBe(true);
158
+ });
159
+ it('private window: falls back to memory, and the key still survives a remount', () => {
160
+ const storage = refusingStorage();
161
+ const first = readSubmissionKey({ form: 'po', storage });
162
+ // One page, two mounts: the second read must find the same key or a retry
163
+ // after `enhance`'s error path would carry a key the server never saw.
164
+ expect(readSubmissionKey({ form: 'po', storage })).toBe(first);
165
+ expect(isSubmissionKeyPersistent({ form: 'po', storage })).toBe(false);
166
+ expect(() => clearSubmissionKey({ form: 'po', storage })).not.toThrow();
167
+ expect(readSubmissionKey({ form: 'po', storage })).not.toBe(first);
168
+ });
169
+ it('private window: survives the sessionStorage property itself throwing', () => {
170
+ const original = Object.getOwnPropertyDescriptor(globalThis, 'sessionStorage');
171
+ Object.defineProperty(globalThis, 'sessionStorage', {
172
+ configurable: true,
173
+ get() {
174
+ throw new DOMException('The operation is insecure.', 'SecurityError');
175
+ },
176
+ });
177
+ try {
178
+ const first = readSubmissionKey({ form: 'po' });
179
+ expect(first).toMatch(UUID_V4);
180
+ expect(readSubmissionKey({ form: 'po' })).toBe(first);
181
+ }
182
+ finally {
183
+ if (original)
184
+ Object.defineProperty(globalThis, 'sessionStorage', original);
185
+ }
186
+ });
187
+ it('a refused write (full quota) still reads back the same key', () => {
188
+ const storage = fullStorage();
189
+ const first = readSubmissionKey({ form: 'po', storage });
190
+ expect(readSubmissionKey({ form: 'po', storage })).toBe(first);
191
+ });
192
+ it('rotates even when the store refuses to remove the old key', () => {
193
+ const storage = stickyStorage();
194
+ const first = readSubmissionKey({ form: 'po', storage });
195
+ const rotated = rotateSubmissionKey({ form: 'po', storage });
196
+ expect(rotated).not.toBe(first);
197
+ expect(readSubmissionKey({ form: 'po', storage })).toBe(rotated);
198
+ expect(storage.store.get('smrt:form-retry:po:key')).toBe(rotated);
199
+ expect(isSubmissionKeyPersistent({ form: 'po', storage })).toBe(true);
200
+ });
201
+ it('rotates when removal AND the replacement write are refused over an existing key', () => {
202
+ const storage = stickyStorage(true);
203
+ storage.store.set('smrt:form-retry:po:key', 'stale-key');
204
+ expect(readSubmissionKey({ form: 'po', storage })).toBe('stale-key');
205
+ const rotated = rotateSubmissionKey({ form: 'po', storage });
206
+ expect(rotated).not.toBe('stale-key');
207
+ // The fresh key lives in memory and takes precedence over stale storage.
208
+ expect(readSubmissionKey({ form: 'po', storage })).toBe(rotated);
209
+ expect(isSubmissionKeyPersistent({ form: 'po', storage })).toBe(false);
210
+ });
211
+ it.each([
212
+ ['writes still work', false],
213
+ ['writes are refused too', true],
214
+ ])('clearing a key the store will not remove mints a fresh one (%s)', (_label, refuseWrites) => {
215
+ const storage = stickyStorage();
216
+ const first = readSubmissionKey({ form: 'po', storage });
217
+ storage.refuseWrites = refuseWrites;
218
+ clearSubmissionKey({ form: 'po', storage });
219
+ const next = readSubmissionKey({ form: 'po', storage });
220
+ expect(next).not.toBe(first);
221
+ expect(next).toMatch(UUID_V4);
222
+ expect(readSubmissionKey({ form: 'po', storage })).toBe(next);
223
+ expect(isSubmissionKeyPersistent({ form: 'po', storage })).toBe(!refuseWrites);
224
+ });
225
+ it('`storage: null` keeps everything in memory', () => {
226
+ const first = readSubmissionKey({ form: 'po', storage: null });
227
+ expect(readSubmissionKey({ form: 'po', storage: null })).toBe(first);
228
+ expect(isSubmissionKeyPersistent({ form: 'po', storage: null })).toBe(false);
229
+ });
230
+ it('mints distinct v4 UUIDs', () => {
231
+ const keys = new Set(Array.from({ length: 50 }, mintSubmissionKey));
232
+ expect(keys.size).toBe(50);
233
+ for (const key of keys)
234
+ expect(key).toMatch(UUID_V4);
235
+ });
236
+ });
@@ -0,0 +1,334 @@
1
+ /**
2
+ * The restore-after-reload draft (#3291): restored only under the key it was
3
+ * submitted with, only for the owner who submitted it, only while fresh, only
4
+ * when well-formed — and never at the cost of a thrown storage error.
5
+ */
6
+ import { describe, expect, it } from 'vitest';
7
+ import { applyDraftToForm, clearSubmittedDraft, describeFormData, readSubmittedDraft, saveSubmittedDraft, } from '../submitted-draft.js';
8
+ import { memoryStorage, refusingStorage, stickyStorage } from './storage.js';
9
+ const TOKEN = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
10
+ const OWNER = 'tenant-1:person-1';
11
+ const NAME = 'smrt:form-retry:report:draft';
12
+ const ALWAYS = () => true;
13
+ const SAVED_AT = new Date('2026-10-01T12:00:00Z');
14
+ function save(storage, owner = OWNER) {
15
+ saveSubmittedDraft(storage, NAME, {
16
+ token: TOKEN,
17
+ owner,
18
+ savedAt: SAVED_AT,
19
+ fields: [
20
+ ['title', 'Trip over a hose'],
21
+ ['qty', '1'],
22
+ ['qty', '2'],
23
+ ],
24
+ files: [{ field: 'photo', name: 'a.jpg', type: 'image/jpeg', size: 3 }],
25
+ values: { captureKey: 'k-1' },
26
+ });
27
+ }
28
+ describe('saveSubmittedDraft / readSubmittedDraft', () => {
29
+ it('restores exactly what was submitted, under the same key and owner', () => {
30
+ const storage = memoryStorage();
31
+ save(storage);
32
+ expect(readSubmittedDraft(storage, NAME, {
33
+ token: TOKEN,
34
+ owner: OWNER,
35
+ fresh: ALWAYS,
36
+ })).toEqual({
37
+ token: TOKEN,
38
+ savedAt: SAVED_AT,
39
+ fields: [
40
+ ['title', 'Trip over a hose'],
41
+ ['qty', '1'],
42
+ ['qty', '2'],
43
+ ],
44
+ files: [{ field: 'photo', name: 'a.jpg', type: 'image/jpeg', size: 3 }],
45
+ values: { captureKey: 'k-1' },
46
+ });
47
+ });
48
+ it('does not restore under a new key, and drops the stale draft', () => {
49
+ const storage = memoryStorage();
50
+ save(storage);
51
+ expect(readSubmittedDraft(storage, NAME, {
52
+ token: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
53
+ owner: OWNER,
54
+ fresh: ALWAYS,
55
+ })).toBeNull();
56
+ expect(storage.store.size).toBe(0);
57
+ });
58
+ it('never restores one person’s draft for another on shared hardware', () => {
59
+ const storage = memoryStorage();
60
+ save(storage);
61
+ expect(readSubmittedDraft(storage, NAME, {
62
+ token: TOKEN,
63
+ owner: 'tenant-1:person-2',
64
+ fresh: ALWAYS,
65
+ })).toBeNull();
66
+ expect(storage.store.size).toBe(0);
67
+ });
68
+ it('never restores for an empty owner', () => {
69
+ const storage = memoryStorage();
70
+ save(storage, '');
71
+ expect(readSubmittedDraft(storage, NAME, {
72
+ token: TOKEN,
73
+ owner: '',
74
+ fresh: ALWAYS,
75
+ })).toBeNull();
76
+ });
77
+ it('drops an expired draft', () => {
78
+ const storage = memoryStorage();
79
+ save(storage);
80
+ const seen = [];
81
+ expect(readSubmittedDraft(storage, NAME, {
82
+ token: TOKEN,
83
+ owner: OWNER,
84
+ fresh: (savedAt) => {
85
+ seen.push(savedAt);
86
+ return false;
87
+ },
88
+ })).toBeNull();
89
+ expect(seen).toEqual([SAVED_AT]);
90
+ expect(storage.store.size).toBe(0);
91
+ });
92
+ it('drops a draft that is malformed or not JSON', () => {
93
+ const storage = memoryStorage();
94
+ const bad = [
95
+ '{not json',
96
+ JSON.stringify({
97
+ v: 2,
98
+ token: TOKEN,
99
+ owner: OWNER,
100
+ savedAt: 1,
101
+ fields: [],
102
+ files: [],
103
+ }),
104
+ JSON.stringify({
105
+ v: 1,
106
+ token: TOKEN,
107
+ owner: OWNER,
108
+ fields: [],
109
+ files: [],
110
+ }),
111
+ JSON.stringify({
112
+ v: 1,
113
+ token: TOKEN,
114
+ owner: OWNER,
115
+ savedAt: 1,
116
+ fields: [['a', 1]],
117
+ files: [],
118
+ }),
119
+ JSON.stringify({
120
+ v: 1,
121
+ token: TOKEN,
122
+ owner: OWNER,
123
+ savedAt: 1,
124
+ fields: [],
125
+ files: [{ field: 'p' }],
126
+ }),
127
+ ];
128
+ for (const raw of bad) {
129
+ storage.store.set(NAME, raw);
130
+ expect(readSubmittedDraft(storage, NAME, {
131
+ token: TOKEN,
132
+ owner: OWNER,
133
+ fresh: ALWAYS,
134
+ }), raw).toBeNull();
135
+ expect(storage.store.size, raw).toBe(0);
136
+ }
137
+ });
138
+ it('clears', () => {
139
+ const storage = memoryStorage();
140
+ save(storage);
141
+ clearSubmittedDraft(storage, NAME);
142
+ expect(storage.store.size).toBe(0);
143
+ });
144
+ it('survives a browser that refuses storage (private window)', () => {
145
+ const storage = refusingStorage();
146
+ expect(() => saveSubmittedDraft(storage, NAME, {
147
+ token: TOKEN,
148
+ owner: OWNER,
149
+ savedAt: SAVED_AT,
150
+ fields: [],
151
+ files: [],
152
+ })).not.toThrow();
153
+ expect(() => clearSubmittedDraft(storage, NAME)).not.toThrow();
154
+ });
155
+ it('skips a values capture that cannot be serialized', () => {
156
+ const storage = memoryStorage();
157
+ const cyclic = {};
158
+ cyclic.self = cyclic;
159
+ saveSubmittedDraft(storage, NAME, {
160
+ token: TOKEN,
161
+ owner: OWNER,
162
+ savedAt: SAVED_AT,
163
+ fields: [],
164
+ files: [],
165
+ values: cyclic,
166
+ });
167
+ expect(storage.store.size).toBe(0);
168
+ });
169
+ });
170
+ describe('a later attempt whose values cannot be serialized', () => {
171
+ it('clears the earlier attempt’s draft instead of leaving it to be restored', () => {
172
+ const storage = memoryStorage();
173
+ save(storage);
174
+ expect(storage.store.has(NAME)).toBe(true);
175
+ const cyclic = {};
176
+ cyclic.self = cyclic;
177
+ saveSubmittedDraft(storage, NAME, {
178
+ token: TOKEN,
179
+ owner: OWNER,
180
+ savedAt: SAVED_AT,
181
+ fields: [['title', 'A different submission']],
182
+ files: [],
183
+ values: cyclic,
184
+ });
185
+ expect(readSubmittedDraft(storage, NAME, {
186
+ token: TOKEN,
187
+ owner: OWNER,
188
+ fresh: ALWAYS,
189
+ })).toBeNull();
190
+ expect(storage.store.has(NAME)).toBe(false);
191
+ });
192
+ });
193
+ describe('a store that refuses to remove the draft', () => {
194
+ const read = (storage) => readSubmittedDraft(storage, NAME, {
195
+ token: TOKEN,
196
+ owner: OWNER,
197
+ fresh: ALWAYS,
198
+ });
199
+ it.each([
200
+ ['writes still work', false],
201
+ ['writes are refused too', true],
202
+ ])('clearing leaves nothing to restore (%s)', (_label, refuseWrites) => {
203
+ const storage = stickyStorage();
204
+ save(storage);
205
+ expect(read(storage)).not.toBeNull();
206
+ storage.refuseWrites = refuseWrites;
207
+ clearSubmittedDraft(storage, NAME);
208
+ expect(read(storage)).toBeNull();
209
+ });
210
+ it.each([
211
+ ['writes still work', false],
212
+ ['writes are refused too', true],
213
+ ])('an unserializable later attempt leaves nothing to restore (%s)', (_label, refuseWrites) => {
214
+ const storage = stickyStorage();
215
+ save(storage);
216
+ storage.refuseWrites = refuseWrites;
217
+ const cyclic = {};
218
+ cyclic.self = cyclic;
219
+ saveSubmittedDraft(storage, NAME, {
220
+ token: TOKEN,
221
+ owner: OWNER,
222
+ savedAt: SAVED_AT,
223
+ fields: [['title', 'A different submission']],
224
+ files: [],
225
+ values: cyclic,
226
+ });
227
+ expect(read(storage)).toBeNull();
228
+ });
229
+ it('a draft rejected at read time (another owner) is not restored later', () => {
230
+ const storage = stickyStorage();
231
+ save(storage);
232
+ expect(readSubmittedDraft(storage, NAME, {
233
+ token: TOKEN,
234
+ owner: 'tenant-1:person-2',
235
+ fresh: ALWAYS,
236
+ })).toBeNull();
237
+ expect(read(storage)).toBeNull();
238
+ });
239
+ it('a later save replaces the invalidation', () => {
240
+ const storage = stickyStorage();
241
+ save(storage);
242
+ clearSubmittedDraft(storage, NAME);
243
+ save(storage);
244
+ expect(read(storage)?.fields[0]).toEqual(['title', 'Trip over a hose']);
245
+ });
246
+ });
247
+ describe('describeFormData', () => {
248
+ it('keeps text entries in order, describes chosen files, skips excluded names', () => {
249
+ const data = new FormData();
250
+ data.append('submissionKey', TOKEN);
251
+ data.append('title', 'x');
252
+ data.append('photo', new File(['abc'], 'a.jpg', { type: 'image/jpeg' }));
253
+ data.append('photo', new File([], '', { type: 'application/octet-stream' }));
254
+ data.append('title', 'y');
255
+ expect(describeFormData(data, new Set(['submissionKey']))).toEqual({
256
+ fields: [
257
+ ['title', 'x'],
258
+ ['title', 'y'],
259
+ ],
260
+ files: [{ field: 'photo', name: 'a.jpg', type: 'image/jpeg', size: 3 }],
261
+ });
262
+ });
263
+ });
264
+ describe('applyDraftToForm', () => {
265
+ function formOf(html) {
266
+ document.body.innerHTML = `<form>${html}</form>`;
267
+ const form = document.body.querySelector('form');
268
+ if (!form)
269
+ throw new Error('no form');
270
+ return form;
271
+ }
272
+ it('puts values back by name and order, and notifies bindings', () => {
273
+ const form = formOf(`
274
+ <input type="hidden" name="jobId" value="server" />
275
+ <input name="title" />
276
+ <input name="qty" /><input name="qty" />
277
+ <textarea name="notes"></textarea>
278
+ <select name="unit"><option value="pcs">pcs</option><option value="ft">ft</option></select>
279
+ <select name="tags" multiple><option value="a" selected>a</option><option value="b">b</option></select>
280
+ <input type="checkbox" name="urgent" value="yes" />
281
+ <input type="checkbox" name="checked" value="on" checked />
282
+ <input type="radio" name="sev" value="minor" checked /><input type="radio" name="sev" value="major" />
283
+ <input type="password" name="pin" />
284
+ <input type="file" name="photo" />
285
+ <input name="secret" />
286
+ `);
287
+ const events = [];
288
+ form.addEventListener('input', (event) => {
289
+ events.push(event.target.name);
290
+ });
291
+ applyDraftToForm(form, {
292
+ fields: [
293
+ ['jobId', 'restored'],
294
+ ['title', 'Trip'],
295
+ ['qty', '1'],
296
+ ['qty', '2'],
297
+ ['notes', 'by the hose'],
298
+ ['unit', 'ft'],
299
+ ['tags', 'b'],
300
+ ['urgent', 'yes'],
301
+ ['sev', 'major'],
302
+ ['pin', '1234'],
303
+ ['secret', 'leaked'],
304
+ ],
305
+ }, new Set(['secret']));
306
+ const value = (selector) => form.querySelector(selector).value;
307
+ const checked = (selector) => form.querySelector(selector).checked;
308
+ expect(value('[name=jobId]')).toBe('server');
309
+ expect(value('[name=title]')).toBe('Trip');
310
+ expect(Array.from(form.querySelectorAll('[name=qty]'), (q) => q.value)).toEqual(['1', '2']);
311
+ expect(value('[name=notes]')).toBe('by the hose');
312
+ expect(value('[name=unit]')).toBe('ft');
313
+ expect(Array.from(form.querySelector('[name=tags]')
314
+ .selectedOptions, (o) => o.value)).toEqual(['b']);
315
+ expect(checked('[name=urgent]')).toBe(true);
316
+ // Submitted unchecked (absent from the draft): unchecked again.
317
+ expect(checked('[name=checked]')).toBe(false);
318
+ expect(checked('[value=major]')).toBe(true);
319
+ expect(value('[name=pin]')).toBe('');
320
+ expect(value('[name=secret]')).toBe('');
321
+ expect(events).toEqual(expect.arrayContaining([
322
+ 'title',
323
+ 'qty',
324
+ 'notes',
325
+ 'unit',
326
+ 'tags',
327
+ 'urgent',
328
+ 'checked',
329
+ 'sev',
330
+ ]));
331
+ expect(events).not.toContain('jobId');
332
+ expect(events).not.toContain('pin');
333
+ });
334
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The `runOnce()` content digest, computed in the browser (#3291).
3
+ *
4
+ * `runOnce()` (`@happyvertical/smrt-core`, `src/run-once.ts`) digests the
5
+ * submitted content itself on the server, so pairing a form with it does NOT
6
+ * need this function: send the submission key, pass the action's `FormData`
7
+ * as `content`, and the server derives the claim. This exists for the cases
8
+ * where the browser needs the SAME digest the server will compute — to tell a
9
+ * person whether the retry they are about to send is byte-identical to the
10
+ * submit that may already be recorded, to key a client-side record of an
11
+ * attempt, or to log a correlatable identity without logging the content.
12
+ *
13
+ * It is a byte-for-byte port of core's `digestRunOnceContent()`, using Web
14
+ * Crypto (`crypto.subtle`) instead of `node:crypto`; shared test vectors in
15
+ * both packages hold the two together. The algorithm:
16
+ *
17
+ * - Content is normalized once, following JSON serialization exactly:
18
+ * `toJSON(key)` is called with the property name or array index (a `Date`
19
+ * digests as its ISO string), and `undefined`, function and symbol values
20
+ * are omitted from objects and written as `null` in arrays.
21
+ * - A `FormData` digests as its ORDERED entry list — repeated fields and their
22
+ * order are significant — because JSON would reduce every form to `{}` and
23
+ * collapse distinct submissions (#3136). A `File`/`Blob` entry digests by
24
+ * `{ name, size, type }`, never its bytes: re-choosing the same file after a
25
+ * reload reproduces the digest, and a different file of the same name, size
26
+ * and type does too.
27
+ * - Content containing a `FormData` is hashed in a separate domain (the input
28
+ * is prefixed with a byte sorted-key JSON never starts with, and user keys
29
+ * starting with NUL are escaped), so no plain object can forge it.
30
+ * - `Map`, `Set`, `RegExp`, any other non-plain object without `toJSON()`, and
31
+ * `bigint` are rejected with a `TypeError` rather than hashed as `{}`.
32
+ * - The digest is the lowercase hex sha256 of the sorted-key JSON encoding.
33
+ *
34
+ * `crypto.subtle` exists only in secure contexts (HTTPS, `localhost`); the
35
+ * function rejects with a clear error elsewhere.
36
+ *
37
+ * @module
38
+ */
39
+ /**
40
+ * The exact string `runOnce()` hashes for `content`.
41
+ * @throws {TypeError} when `content` contains a value with no faithful JSON form.
42
+ * @internal
43
+ */
44
+ export declare function submissionDigestInput(content: unknown): string;
45
+ /**
46
+ * The content digest `runOnce()` will compute on the server for `content`:
47
+ * lowercase hex sha256. See the module doc for what is and is not digested
48
+ * (a `File` by name, size and type — never its bytes).
49
+ *
50
+ * @throws {TypeError} (as a rejection) when `content` has no faithful JSON
51
+ * form, or when Web Crypto is unavailable (not a secure context).
52
+ */
53
+ export declare function digestSubmissionContent(content: unknown): Promise<string>;
54
+ //# sourceMappingURL=content-digest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-digest.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/form-retry/content-digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAoHH;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAO9D;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,MAAM,CAAC,CAajB"}