@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
@@ -15,6 +15,8 @@ import { tryGetFormGroupContext } from './form-group-context.js';
15
15
  import { useControlRegistration } from './use-control-registration.svelte.js';
16
16
 
17
17
  export interface Props extends Omit<HTMLTextareaAttributes, 'class' | 'value'> {
18
+ /** Target density; omit to inherit the ThemeProvider setting. */
19
+ density?: 'comfortable' | 'touch';
18
20
  /** The text content of the textarea. */
19
21
  value?: string;
20
22
  /** Number of visible text rows. */
@@ -39,6 +41,7 @@ let {
39
41
  'aria-label': ariaLabel,
40
42
  'aria-describedby': ariaDescribedby,
41
43
  'aria-invalid': ariaInvalid,
44
+ density,
42
45
  ...rest
43
46
  }: Props = $props();
44
47
 
@@ -133,7 +136,7 @@ export function getElement(): HTMLTextAreaElement | null {
133
136
  {rows}
134
137
  aria-describedby={resolvedDescribedBy}
135
138
  aria-invalid={resolvedInvalid}
136
- class="textarea {className}"
139
+ data-density={density} class="textarea {className}"
137
140
  data-smrt-control={resolvedControlId}
138
141
  data-smrt-form={controlInteraction?.formId}
139
142
  data-smrt-subject-type={resolvedInteraction === false ? undefined : resolvedInteraction.subject?.type}
@@ -181,9 +184,21 @@ export function getElement(): HTMLTextAreaElement | null {
181
184
  color: var(--smrt-color-on-surface-variant, #9ca3af);
182
185
  }
183
186
 
187
+ /* Phones: 16px text so iOS Safari doesn't zoom the page when the field
188
+ gets focus. */
189
+ @media (max-width: 48rem) {
190
+ .textarea {
191
+ font-size: max(var(--smrt-typography-body-medium-size, 0.875rem), 16px);
192
+ }
193
+ }
194
+
184
195
  @media (prefers-reduced-motion: reduce) {
185
196
  .textarea {
186
197
  transition: none;
187
198
  }
188
199
  }
200
+
201
+ .textarea[data-density='touch'] { --smrt-control-target-min: var(--smrt-touch-target-min, 48px); }
202
+ .textarea[data-density='comfortable'] { --smrt-control-target-min: 0px; }
203
+ .textarea { min-height: max(80px, var(--smrt-control-target-min, 0px)); }
189
204
  </style>
@@ -1,6 +1,8 @@
1
1
  import type { HTMLTextareaAttributes } from 'svelte/elements';
2
2
  import type { ControlInteractionOptions } from './control-interaction.js';
3
3
  export interface Props extends Omit<HTMLTextareaAttributes, 'class' | 'value'> {
4
+ /** Target density; omit to inherit the ThemeProvider setting. */
5
+ density?: 'comfortable' | 'touch';
4
6
  /** The text content of the textarea. */
5
7
  value?: string;
6
8
  /** Number of visible text rows. */
@@ -1 +1 @@
1
- {"version":3,"file":"Textarea.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Textarea.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAM9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAU1E,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,OAAO,CAAC;IAC5E,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,WAAW,CAAC,EAAE,yBAAyB,GAAG,KAAK,CAAC;CACjD;AAgHD,QAAA,MAAM,QAAQ;iBAfK,IAAI;sBAIC,mBAAmB,GAAG,IAAI;WAWI,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Textarea.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Textarea.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAM9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAU1E,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,OAAO,CAAC;IAC5E,iEAAiE;IACjE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC;IAClC,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,WAAW,CAAC,EAAE,yBAAyB,GAAG,KAAK,CAAC;CACjD;AAiHD,QAAA,MAAM,QAAQ;iBAfK,IAAI;sBAIC,mBAAmB,GAAG,IAAI;WAWI,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,504 @@
1
+ /**
2
+ * CameraCapture rendered states, stream release, and native form posting
3
+ * (smrt#3290). `navigator.mediaDevices` is a fake; canvas, playback,
4
+ * DataTransfer and the `formdata` event are jsdom stand-ins from
5
+ * `capture-test-env.ts`.
6
+ */
7
+ import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import { expectNoA11yViolations } from '../../../test-support/a11y.js';
10
+ import CameraCapture from '../CameraCapture.svelte';
11
+ import CaptureFormFixture from './capture-form.fixture.svelte';
12
+ import { createFileList, fakeStream, installMediaDevices, removeMediaDevices, stubCanvas, stubMediaPlayback, submittedEntries, useFileFieldStrategy, } from './capture-test-env.js';
13
+ function grantCamera(track = { stop: vi.fn() }) {
14
+ const getUserMedia = vi.fn().mockResolvedValue(fakeStream([track]));
15
+ installMediaDevices({ getUserMedia });
16
+ return { getUserMedia, track };
17
+ }
18
+ function rejectCamera(name) {
19
+ const getUserMedia = vi.fn().mockRejectedValue(new DOMException('x', name));
20
+ installMediaDevices({ getUserMedia });
21
+ return getUserMedia;
22
+ }
23
+ function root(container) {
24
+ return container.querySelector('.camera-capture');
25
+ }
26
+ async function expectState(container, state) {
27
+ await waitFor(() => expect(root(container)).toHaveAttribute('data-state', state));
28
+ }
29
+ beforeEach(() => {
30
+ stubCanvas();
31
+ stubMediaPlayback();
32
+ useFileFieldStrategy('data-transfer');
33
+ });
34
+ afterEach(() => {
35
+ removeMediaDevices();
36
+ vi.restoreAllMocks();
37
+ vi.unstubAllGlobals();
38
+ });
39
+ describe('CameraCapture states', () => {
40
+ it('shows the starting state while the permission prompt is pending', async () => {
41
+ installMediaDevices({ getUserMedia: () => new Promise(() => { }) });
42
+ const { container } = render(CameraCapture);
43
+ await expectState(container, 'starting');
44
+ expect(screen.getByRole('status')).toHaveTextContent('Starting camera…');
45
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
46
+ });
47
+ it('streams a live preview with a Take photo action', async () => {
48
+ const { getUserMedia } = grantCamera();
49
+ const { container } = render(CameraCapture, {
50
+ props: { facingMode: 'user' },
51
+ });
52
+ await expectState(container, 'streaming');
53
+ expect(getUserMedia).toHaveBeenCalledWith({
54
+ video: { facingMode: 'user' },
55
+ audio: false,
56
+ });
57
+ expect(container.querySelector('video')).not.toHaveClass('hidden');
58
+ expect(screen.getByRole('button', { name: 'Take photo' })).toBeEnabled();
59
+ await expectNoA11yViolations(container);
60
+ });
61
+ it.each([
62
+ ['NotAllowedError', 'permission-denied', /Camera access was denied/],
63
+ ['NotFoundError', 'no-camera', /No camera was found/],
64
+ ['AbortError', 'error', /Could not access the camera/],
65
+ ])('renders %s as the %s state with a retry', async (name, state, copy) => {
66
+ const getUserMedia = rejectCamera(name);
67
+ const { container } = render(CameraCapture);
68
+ await expectState(container, state);
69
+ expect(screen.getByRole('status')).toHaveTextContent(copy);
70
+ await expectNoA11yViolations(container);
71
+ await fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
72
+ await waitFor(() => expect(getUserMedia).toHaveBeenCalledTimes(2));
73
+ });
74
+ it('renders the unsupported state without getUserMedia, with no file-input fallback by default', async () => {
75
+ installMediaDevices(undefined);
76
+ const { container } = render(CameraCapture, { props: { name: 'photo' } });
77
+ await expectState(container, 'unsupported');
78
+ expect(screen.getByRole('status')).toHaveTextContent(/cannot use the camera/);
79
+ expect(container.querySelector('input[capture]')).toBeNull();
80
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
81
+ await expectNoA11yViolations(container);
82
+ });
83
+ it('renders the off state when disabled and never requests the camera', async () => {
84
+ const { getUserMedia } = grantCamera();
85
+ const { container } = render(CameraCapture, { props: { disabled: true } });
86
+ await expectState(container, 'off');
87
+ expect(screen.getByRole('status')).toHaveTextContent('The camera is off.');
88
+ expect(getUserMedia).not.toHaveBeenCalled();
89
+ });
90
+ it('accepts label overrides', async () => {
91
+ rejectCamera('NotAllowedError');
92
+ render(CameraCapture, {
93
+ props: {
94
+ labels: { permissionDenied: 'Kamera verweigert', retry: 'Nochmal' },
95
+ },
96
+ });
97
+ expect(await screen.findByText('Kamera verweigert')).toBeInTheDocument();
98
+ expect(screen.getByRole('button', { name: 'Nochmal' })).toBeInTheDocument();
99
+ });
100
+ });
101
+ describe('CameraCapture review flow', () => {
102
+ it('captures, reviews, retakes, and reports only the committed photo', async () => {
103
+ const { getUserMedia, track } = grantCamera();
104
+ const onCapture = vi.fn();
105
+ const { container } = render(CameraCapture, { props: { onCapture } });
106
+ await expectState(container, 'streaming');
107
+ await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
108
+ await expectState(container, 'reviewing');
109
+ expect(track.stop).toHaveBeenCalledTimes(1);
110
+ expect(screen.getByRole('img', { name: 'Captured photo, not yet used' })).toBeInTheDocument();
111
+ await expectNoA11yViolations(container);
112
+ await fireEvent.click(screen.getByRole('button', { name: 'Retake' }));
113
+ await expectState(container, 'streaming');
114
+ expect(getUserMedia).toHaveBeenCalledTimes(2);
115
+ expect(onCapture).not.toHaveBeenCalled();
116
+ await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
117
+ await expectState(container, 'reviewing');
118
+ await fireEvent.click(screen.getByRole('button', { name: 'Use photo' }));
119
+ await expectState(container, 'committed');
120
+ expect(onCapture).toHaveBeenCalledTimes(1);
121
+ const [{ blob, dataUrl }] = onCapture.mock.calls[0];
122
+ expect(blob).toBeInstanceOf(Blob);
123
+ expect(blob.type).toBe('image/jpeg');
124
+ expect(dataUrl).toMatch(/^data:image\/jpeg/);
125
+ expect(screen.getByRole('status')).toHaveTextContent('Photo attached.');
126
+ expect(screen.getByRole('img', { name: 'Attached photo' })).toBeInTheDocument();
127
+ });
128
+ it('stops the stream on unmount', async () => {
129
+ const { track } = grantCamera();
130
+ const { container, unmount } = render(CameraCapture);
131
+ await expectState(container, 'streaming');
132
+ unmount();
133
+ expect(track.stop).toHaveBeenCalledTimes(1);
134
+ });
135
+ it('stops the stream when disabled and restarts it when re-enabled', async () => {
136
+ const { getUserMedia, track } = grantCamera();
137
+ const { container, rerender } = render(CameraCapture);
138
+ await expectState(container, 'streaming');
139
+ await rerender({ disabled: true });
140
+ expect(track.stop).toHaveBeenCalledTimes(1);
141
+ await expectState(container, 'off');
142
+ await rerender({ disabled: false });
143
+ await expectState(container, 'streaming');
144
+ expect(getUserMedia).toHaveBeenCalledTimes(2);
145
+ });
146
+ it('re-requests the camera when facingMode changes while live', async () => {
147
+ const { getUserMedia, track } = grantCamera();
148
+ const { container, rerender } = render(CameraCapture);
149
+ await expectState(container, 'streaming');
150
+ await rerender({ facingMode: 'user' });
151
+ expect(track.stop).toHaveBeenCalledTimes(1);
152
+ await waitFor(() => expect(getUserMedia).toHaveBeenLastCalledWith({
153
+ video: { facingMode: 'user' },
154
+ audio: false,
155
+ }));
156
+ await expectState(container, 'streaming');
157
+ });
158
+ it('keeps a photo under review across disable without reopening the camera', async () => {
159
+ const { getUserMedia } = grantCamera();
160
+ const { container, rerender } = render(CameraCapture);
161
+ await expectState(container, 'streaming');
162
+ await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
163
+ await expectState(container, 'reviewing');
164
+ await rerender({ disabled: true });
165
+ expect(root(container)).toHaveAttribute('data-state', 'reviewing');
166
+ expect(screen.getByRole('button', { name: 'Use photo' })).toBeDisabled();
167
+ await rerender({ disabled: false });
168
+ expect(root(container)).toHaveAttribute('data-state', 'reviewing');
169
+ expect(getUserMedia).toHaveBeenCalledTimes(1);
170
+ });
171
+ it('releases a stream granted after unmount (late permission)', async () => {
172
+ let grant;
173
+ installMediaDevices({
174
+ getUserMedia: () => new Promise((resolve) => {
175
+ grant = resolve;
176
+ }),
177
+ });
178
+ const track = { stop: vi.fn() };
179
+ const { unmount } = render(CameraCapture);
180
+ await waitFor(() => expect(grant).toBeTypeOf('function'));
181
+ unmount();
182
+ grant(fakeStream([track]));
183
+ await waitFor(() => expect(track.stop).toHaveBeenCalledTimes(1));
184
+ });
185
+ });
186
+ describe('CameraCapture native form post', () => {
187
+ async function commitPhoto(container) {
188
+ await expectState(container, 'streaming');
189
+ await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
190
+ await expectState(container, 'reviewing');
191
+ await fireEvent.click(screen.getByRole('button', { name: 'Use photo' }));
192
+ await expectState(container, 'committed');
193
+ }
194
+ it('posts the committed photo as a file through DataTransfer', async () => {
195
+ grantCamera();
196
+ const onClear = vi.fn();
197
+ const { container } = render(CaptureFormFixture, {
198
+ props: { kind: 'camera', name: 'photo', onClear },
199
+ });
200
+ const form = container.querySelector('form');
201
+ expect(root(container)).toHaveAttribute('data-smrt-file-field', 'data-transfer');
202
+ expect(form.method).toBe('post');
203
+ expect(form.enctype).toBe('multipart/form-data');
204
+ // Before commit the field posts what an empty native file input posts.
205
+ await expectState(container, 'streaming');
206
+ expect(submittedEntries(form).get('photo').size).toBe(0);
207
+ await commitPhoto(container);
208
+ const posted = submittedEntries(form).getAll('photo');
209
+ expect(posted).toHaveLength(1);
210
+ expect(posted[0].name).toBe('photo.jpg');
211
+ expect(posted[0].type).toBe('image/jpeg');
212
+ expect(posted[0].size).toBeGreaterThan(0);
213
+ await fireEvent.click(screen.getByRole('button', { name: 'Retake' }));
214
+ expect(onClear).toHaveBeenCalledTimes(1);
215
+ expect(submittedEntries(form).get('photo').size).toBe(0);
216
+ });
217
+ it('honours fileName and imageType', async () => {
218
+ grantCamera();
219
+ const { container } = render(CaptureFormFixture, {
220
+ props: {
221
+ kind: 'camera',
222
+ name: 'receipt',
223
+ fileName: 'receipt.png',
224
+ imageType: 'image/png',
225
+ },
226
+ });
227
+ await commitPhoto(container);
228
+ const posted = submittedEntries(container.querySelector('form')).get('receipt');
229
+ expect(posted.name).toBe('receipt.png');
230
+ expect(posted.type).toBe('image/png');
231
+ });
232
+ it('falls back to the formdata event when DataTransfer is unavailable', async () => {
233
+ useFileFieldStrategy('formdata-event');
234
+ grantCamera();
235
+ const { container } = render(CaptureFormFixture, {
236
+ props: { kind: 'camera', name: 'photo' },
237
+ });
238
+ const form = container.querySelector('form');
239
+ await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', 'formdata-event'));
240
+ const field = container.querySelector('[data-smrt-capture-field]');
241
+ expect(field).not.toHaveAttribute('name');
242
+ await commitPhoto(container);
243
+ const posted = submittedEntries(form).getAll('photo');
244
+ expect(posted).toHaveLength(1);
245
+ expect(posted[0].name).toBe('photo.jpg');
246
+ });
247
+ it('posts nothing but still reports the photo when no strategy exists', async () => {
248
+ useFileFieldStrategy('none');
249
+ grantCamera();
250
+ const onCapture = vi.fn();
251
+ const { container } = render(CaptureFormFixture, {
252
+ props: { kind: 'camera', name: 'photo', onCapture },
253
+ });
254
+ await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', 'none'));
255
+ await commitPhoto(container);
256
+ expect(onCapture).toHaveBeenCalledTimes(1);
257
+ expect(submittedEntries(container.querySelector('form')).has('photo')).toBe(false);
258
+ });
259
+ it('opt-in file-input fallback posts the chosen photo natively', async () => {
260
+ installMediaDevices(undefined);
261
+ const onCapture = vi.fn();
262
+ const { container } = render(CaptureFormFixture, {
263
+ props: {
264
+ kind: 'camera',
265
+ name: 'photo',
266
+ fileInputFallback: true,
267
+ onCapture,
268
+ },
269
+ });
270
+ await expectState(container, 'fallback');
271
+ expect(root(container)).toHaveAttribute('data-smrt-file-field', 'file-input');
272
+ const picker = screen.getByLabelText('Take or choose a photo');
273
+ expect(picker).toHaveAttribute('capture', 'environment');
274
+ expect(picker).toHaveAttribute('accept', 'image/*');
275
+ expect(container.querySelector('[data-smrt-capture-field]')).toBeNull();
276
+ await expectNoA11yViolations(container);
277
+ const chosen = new File(['jpeg'], 'IMG_0001.jpg', { type: 'image/jpeg' });
278
+ picker.files = createFileList([chosen]);
279
+ await fireEvent.change(picker);
280
+ await expectState(container, 'committed');
281
+ expect(onCapture).toHaveBeenCalledWith({
282
+ blob: chosen,
283
+ dataUrl: expect.stringMatching(/^data:image\/jpeg/),
284
+ });
285
+ expect(submittedEntries(container.querySelector('form')).getAll('photo')).toEqual([chosen]);
286
+ });
287
+ });
288
+ describe('CameraCapture disabled with the opt-in file-input fallback', () => {
289
+ it('keeps a committed photo in the submission while blocking the picker', async () => {
290
+ installMediaDevices(undefined);
291
+ const props = { kind: 'camera', name: 'photo', fileInputFallback: true };
292
+ const { container, rerender } = render(CaptureFormFixture, { props });
293
+ await expectState(container, 'fallback');
294
+ const form = container.querySelector('form');
295
+ const picker = screen.getByLabelText('Take or choose a photo');
296
+ const chosen = new File(['jpeg'], 'IMG_0001.jpg', { type: 'image/jpeg' });
297
+ picker.files = createFileList([chosen]);
298
+ await fireEvent.change(picker);
299
+ await expectState(container, 'committed');
300
+ await rerender({ ...props, disabled: true });
301
+ await waitFor(() => expect(root(container)).toHaveClass('disabled'));
302
+ // The photo still shown is the photo still posted.
303
+ expect(submittedEntries(form).getAll('photo')).toEqual([chosen]);
304
+ // ...but the picker cannot be opened or tabbed to.
305
+ expect(picker).toHaveAttribute('aria-disabled', 'true');
306
+ expect(picker).toHaveAttribute('tabindex', '-1');
307
+ const click = new MouseEvent('click', { bubbles: true, cancelable: true });
308
+ picker.dispatchEvent(click);
309
+ expect(click.defaultPrevented).toBe(true);
310
+ await rerender({ ...props, disabled: false });
311
+ await waitFor(() => expect(picker).not.toHaveAttribute('aria-disabled'));
312
+ const reopen = new MouseEvent('click', { bubbles: true, cancelable: true });
313
+ picker.dispatchEvent(reopen);
314
+ expect(reopen.defaultPrevented).toBe(false);
315
+ expect(submittedEntries(form).getAll('photo')).toEqual([chosen]);
316
+ });
317
+ });
318
+ describe('CameraCapture in a disabled fieldset', () => {
319
+ it.each([
320
+ 'data-transfer',
321
+ 'formdata-event',
322
+ ])('posts nothing, committed or empty, like a disabled native control (%s)', async (strategy) => {
323
+ useFileFieldStrategy(strategy);
324
+ grantCamera();
325
+ const { container } = render(CaptureFormFixture, {
326
+ props: { kind: 'camera', name: 'photo', inFieldset: true },
327
+ });
328
+ await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', strategy));
329
+ const form = container.querySelector('form');
330
+ const fieldset = form.querySelector('fieldset');
331
+ await expectState(container, 'streaming');
332
+ fieldset.disabled = true;
333
+ expect(submittedEntries(form).has('photo')).toBe(false);
334
+ fieldset.disabled = false;
335
+ await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
336
+ await expectState(container, 'reviewing');
337
+ await fireEvent.click(screen.getByRole('button', { name: 'Use photo' }));
338
+ await expectState(container, 'committed');
339
+ expect(submittedEntries(form).getAll('photo')).toHaveLength(1);
340
+ fieldset.disabled = true;
341
+ expect(submittedEntries(form).has('photo')).toBe(false);
342
+ expect(submittedEntries(form).get('note')).toBe('hazard');
343
+ fieldset.disabled = false;
344
+ expect(submittedEntries(form).getAll('photo')).toHaveLength(1);
345
+ });
346
+ });
347
+ /**
348
+ * A reset of the owning form (smrt#3290): SvelteKit `enhance`'s `update()`
349
+ * after a success, `createFormRetry()`'s conditional reset, a reset button and
350
+ * `form.reset()` all empty a native file input, so the photo goes with it.
351
+ * jsdom's reset does not empty file inputs (browsers do), so these assertions
352
+ * prove the component empties its own field.
353
+ */
354
+ describe('CameraCapture form reset', () => {
355
+ function form(container) {
356
+ return container.querySelector('form');
357
+ }
358
+ async function commitPhoto(container) {
359
+ await expectState(container, 'streaming');
360
+ await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
361
+ await expectState(container, 'reviewing');
362
+ await fireEvent.click(screen.getByRole('button', { name: 'Use photo' }));
363
+ await expectState(container, 'committed');
364
+ }
365
+ function postedPhotos(container) {
366
+ return submittedEntries(form(container)).getAll('photo');
367
+ }
368
+ it.each([
369
+ 'data-transfer',
370
+ 'formdata-event',
371
+ ])('discards a committed photo and reopens the camera (%s)', async (strategy) => {
372
+ useFileFieldStrategy(strategy);
373
+ const { getUserMedia } = grantCamera();
374
+ const onClear = vi.fn();
375
+ const { container } = render(CaptureFormFixture, {
376
+ props: { kind: 'camera', name: 'photo', onClear },
377
+ });
378
+ await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', strategy));
379
+ await commitPhoto(container);
380
+ expect(postedPhotos(container)[0].name).toBe('photo.jpg');
381
+ form(container).reset();
382
+ await expectState(container, 'streaming');
383
+ expect(onClear).toHaveBeenCalledTimes(1);
384
+ expect(getUserMedia).toHaveBeenCalledTimes(2);
385
+ expect(screen.queryByRole('img')).not.toBeInTheDocument();
386
+ const posted = postedPhotos(container);
387
+ expect(posted).toHaveLength(1);
388
+ expect(posted[0].size).toBe(0);
389
+ expect(posted[0].name).toBe('');
390
+ // The next entry takes and posts its own photo.
391
+ await commitPhoto(container);
392
+ expect(postedPhotos(container)[0].name).toBe('photo.jpg');
393
+ });
394
+ it('a reset button discards a photo under review, without onClear', async () => {
395
+ const { getUserMedia } = grantCamera();
396
+ const onClear = vi.fn();
397
+ const onCapture = vi.fn();
398
+ const { container } = render(CaptureFormFixture, {
399
+ props: { kind: 'camera', name: 'photo', onClear, onCapture },
400
+ });
401
+ await expectState(container, 'streaming');
402
+ await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
403
+ await expectState(container, 'reviewing');
404
+ await fireEvent.click(screen.getByRole('button', { name: 'Reset form' }));
405
+ await expectState(container, 'streaming');
406
+ expect(screen.queryByRole('img')).not.toBeInTheDocument();
407
+ expect(getUserMedia).toHaveBeenCalledTimes(2);
408
+ expect(onClear).not.toHaveBeenCalled();
409
+ expect(onCapture).not.toHaveBeenCalled();
410
+ });
411
+ it('clears a committed photo while disabled and keeps the camera off', async () => {
412
+ const { getUserMedia } = grantCamera();
413
+ const onClear = vi.fn();
414
+ const props = { kind: 'camera', name: 'photo', onClear };
415
+ const { container, rerender } = render(CaptureFormFixture, { props });
416
+ await commitPhoto(container);
417
+ await rerender({ ...props, disabled: true });
418
+ form(container).reset();
419
+ await expectState(container, 'off');
420
+ expect(onClear).toHaveBeenCalledTimes(1);
421
+ expect(getUserMedia).toHaveBeenCalledTimes(1);
422
+ expect(postedPhotos(container)[0].size).toBe(0);
423
+ await rerender({ ...props, disabled: false });
424
+ await expectState(container, 'streaming');
425
+ expect(getUserMedia).toHaveBeenCalledTimes(2);
426
+ });
427
+ it('supersedes a frame still encoding', async () => {
428
+ let finish;
429
+ vi.mocked(HTMLCanvasElement.prototype.toBlob).mockImplementation((callback, type) => {
430
+ finish = () => callback(new Blob(['pixels'], { type }));
431
+ });
432
+ const { getUserMedia } = grantCamera();
433
+ const { container } = render(CaptureFormFixture, {
434
+ props: { kind: 'camera', name: 'photo' },
435
+ });
436
+ await expectState(container, 'streaming');
437
+ await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
438
+ form(container).reset();
439
+ finish();
440
+ await waitFor(() => expect(getUserMedia).toHaveBeenCalledTimes(2));
441
+ await expectState(container, 'streaming');
442
+ expect(screen.queryByRole('img')).not.toBeInTheDocument();
443
+ });
444
+ it('changes nothing when no photo is held, and never re-prompts', async () => {
445
+ const getUserMedia = rejectCamera('NotAllowedError');
446
+ const { container } = render(CaptureFormFixture, {
447
+ props: { kind: 'camera', name: 'photo' },
448
+ });
449
+ await expectState(container, 'permission-denied');
450
+ form(container).reset();
451
+ expect(root(container)).toHaveAttribute('data-state', 'permission-denied');
452
+ expect(getUserMedia).toHaveBeenCalledTimes(1);
453
+ });
454
+ it('opt-in file-input fallback returns to the picker', async () => {
455
+ installMediaDevices(undefined);
456
+ const onClear = vi.fn();
457
+ const { container } = render(CaptureFormFixture, {
458
+ props: {
459
+ kind: 'camera',
460
+ name: 'photo',
461
+ fileInputFallback: true,
462
+ onClear,
463
+ },
464
+ });
465
+ await expectState(container, 'fallback');
466
+ const picker = screen.getByLabelText('Take or choose a photo');
467
+ picker.files = createFileList([
468
+ new File(['jpeg'], 'IMG_0001.jpg', { type: 'image/jpeg' }),
469
+ ]);
470
+ await fireEvent.change(picker);
471
+ await expectState(container, 'committed');
472
+ form(container).reset();
473
+ await expectState(container, 'fallback');
474
+ expect(onClear).toHaveBeenCalledTimes(1);
475
+ expect(screen.queryByRole('img')).not.toBeInTheDocument();
476
+ expect(picker.files).toHaveLength(0);
477
+ const posted = postedPhotos(container);
478
+ expect(posted).toHaveLength(1);
479
+ expect(posted[0].size).toBe(0);
480
+ });
481
+ it('opt-in file-input fallback drops a pick still being read', async () => {
482
+ installMediaDevices(undefined);
483
+ const onCapture = vi.fn();
484
+ const { container } = render(CaptureFormFixture, {
485
+ props: {
486
+ kind: 'camera',
487
+ name: 'photo',
488
+ fileInputFallback: true,
489
+ onCapture,
490
+ },
491
+ });
492
+ await expectState(container, 'fallback');
493
+ const picker = screen.getByLabelText('Take or choose a photo');
494
+ picker.files = createFileList([
495
+ new File(['jpeg'], 'IMG_0002.jpg', { type: 'image/jpeg' }),
496
+ ]);
497
+ // Reset in the same task as the change, before the FileReader can land.
498
+ picker.dispatchEvent(new Event('change', { bubbles: true }));
499
+ form(container).reset();
500
+ await new Promise((resolve) => setTimeout(resolve, 20));
501
+ expect(root(container)).toHaveAttribute('data-state', 'fallback');
502
+ expect(onCapture).not.toHaveBeenCalled();
503
+ });
504
+ });