@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,582 @@
1
+ /**
2
+ * Dictation's recording fallback: when the browser has no speech recognition
3
+ * (Firefox) or it fails with no speech service behind it (Brave), record with
4
+ * MediaRecorder and hand the audio to `transcribe`. Fake speech source, fake
5
+ * recorder, fake MediaRecorder: nothing here touches a real microphone.
6
+ */
7
+ import { afterEach, describe, expect, it, vi } from 'vitest';
8
+ import { createMediaRecorderCapture, DictationError, pickDictationMimeType, } from '../audio-capture.js';
9
+ import { Dictation, } from '../dictation.svelte.js';
10
+ import { createHttpTranscriber } from '../dictation-transcribe.js';
11
+ function fakeSource(options = {}) {
12
+ const results = new Set();
13
+ const errors = new Set();
14
+ const ends = new Set();
15
+ return {
16
+ start: vi.fn(async () => {
17
+ if (options.startError)
18
+ throw options.startError;
19
+ }),
20
+ stop: vi.fn(async () => {
21
+ for (const cb of ends)
22
+ cb();
23
+ }),
24
+ onResult: (cb) => (results.add(cb), () => results.delete(cb)),
25
+ onError: (cb) => (errors.add(cb), () => errors.delete(cb)),
26
+ onEnd: (cb) => (ends.add(cb), () => ends.delete(cb)),
27
+ emitResult(text, isFinal) {
28
+ for (const cb of results)
29
+ cb({ text, isFinal });
30
+ },
31
+ emitError(error) {
32
+ for (const cb of errors)
33
+ cb(error);
34
+ },
35
+ emitEnd() {
36
+ for (const cb of ends)
37
+ cb();
38
+ },
39
+ };
40
+ }
41
+ function speechError(code) {
42
+ return Object.assign(new Error(`Speech recognition error: ${code}`), {
43
+ speechError: code,
44
+ });
45
+ }
46
+ function fakeCaptureFactory(options = {}) {
47
+ const made = [];
48
+ const factory = vi.fn(() => {
49
+ const limits = new Set();
50
+ const capture = {
51
+ start: vi.fn(async () => {
52
+ await options.startGate;
53
+ if (options.startError)
54
+ throw options.startError;
55
+ }),
56
+ stop: vi.fn(async () => {
57
+ if (options.stopError)
58
+ throw options.stopError;
59
+ return {
60
+ audio: new Blob(['opus-bytes'], { type: 'audio/webm;codecs=opus' }),
61
+ mimeType: 'audio/webm;codecs=opus',
62
+ durationMs: 3200,
63
+ reachedTimeLimit: false,
64
+ ...options.recording,
65
+ };
66
+ }),
67
+ cancel: vi.fn(),
68
+ onLimit: (cb) => (limits.add(cb), () => limits.delete(cb)),
69
+ hitLimit(reason) {
70
+ for (const cb of limits)
71
+ cb(reason);
72
+ },
73
+ };
74
+ made.push(capture);
75
+ return capture;
76
+ });
77
+ return { factory, made };
78
+ }
79
+ afterEach(() => {
80
+ vi.useRealTimers();
81
+ vi.unstubAllGlobals();
82
+ });
83
+ describe('Dictation recording fallback', () => {
84
+ it('records when there is no speech source, then writes it down', async () => {
85
+ const { factory, made } = fakeCaptureFactory();
86
+ const transcribe = vi.fn(async () => 'make it square');
87
+ const onText = vi.fn();
88
+ const beep = vi.fn(async () => true);
89
+ const dictation = new Dictation({
90
+ onText,
91
+ transcribe,
92
+ capture: factory,
93
+ beep,
94
+ language: 'en-CA',
95
+ maxDurationMs: 90_000,
96
+ maxBytes: 5_000_000,
97
+ });
98
+ expect(dictation.available).toBe(true);
99
+ await dictation.start();
100
+ expect(dictation.state).toBe('listening');
101
+ expect(dictation.recording).toBe(true);
102
+ expect(factory).toHaveBeenCalledWith({
103
+ maxDurationMs: 90_000,
104
+ maxBytes: 5_000_000,
105
+ });
106
+ await vi.waitFor(() => expect(beep).toHaveBeenCalledTimes(1));
107
+ let release;
108
+ transcribe.mockImplementationOnce(() => new Promise((resolve) => (release = resolve)));
109
+ const stopped = dictation.stop();
110
+ await vi.waitFor(() => expect(transcribe).toHaveBeenCalled());
111
+ expect(dictation.state).toBe('transcribing');
112
+ expect(dictation.active).toBe(false);
113
+ release(' make it square ');
114
+ await stopped;
115
+ expect(made[0].stop).toHaveBeenCalled();
116
+ expect(transcribe).toHaveBeenCalledWith(expect.any(Blob), {
117
+ mimeType: 'audio/webm;codecs=opus',
118
+ language: 'en-CA',
119
+ durationMs: 3200,
120
+ signal: expect.any(AbortSignal),
121
+ });
122
+ expect(onText).toHaveBeenCalledWith('make it square');
123
+ expect(dictation.state).toBe('idle');
124
+ expect(dictation.recording).toBe(false);
125
+ });
126
+ it('switches to recording when Brave fails with `network`, and records straight away next time', async () => {
127
+ const source = fakeSource();
128
+ const { factory, made } = fakeCaptureFactory();
129
+ const log = vi.fn();
130
+ const onText = vi.fn();
131
+ const dictation = new Dictation({
132
+ source: () => source,
133
+ onText,
134
+ transcribe: async () => 'the arena at night',
135
+ capture: factory,
136
+ beep: false,
137
+ requestMicrophone: false,
138
+ log,
139
+ });
140
+ await dictation.start();
141
+ expect(dictation.state).toBe('listening');
142
+ expect(dictation.recording).toBe(false);
143
+ source.emitError(speechError('network'));
144
+ await vi.waitFor(() => expect(made).toHaveLength(1));
145
+ await vi.waitFor(() => expect(made[0].start).toHaveBeenCalled());
146
+ await made[0].start.mock.results[0].value;
147
+ await Promise.resolve();
148
+ expect(dictation.state).toBe('listening');
149
+ expect(dictation.recording).toBe(true);
150
+ expect(source.stop).toHaveBeenCalled();
151
+ expect(log).toHaveBeenCalledWith(expect.objectContaining({
152
+ kind: 'unsupported',
153
+ code: 'network',
154
+ fallback: 'recording',
155
+ }));
156
+ await dictation.stop();
157
+ expect(onText).toHaveBeenCalledWith('the arena at night');
158
+ expect(dictation.state).toBe('idle');
159
+ source.start.mockClear();
160
+ await dictation.start();
161
+ expect(source.start).not.toHaveBeenCalled();
162
+ expect(made).toHaveLength(2);
163
+ expect(dictation.recording).toBe(true);
164
+ });
165
+ it('records when the recogniser refuses to start (`service-not-allowed`)', async () => {
166
+ const source = fakeSource({
167
+ startError: speechError('service-not-allowed'),
168
+ });
169
+ const { factory, made } = fakeCaptureFactory();
170
+ const dictation = new Dictation({
171
+ source: () => source,
172
+ onText: () => { },
173
+ transcribe: async () => 'hello',
174
+ capture: factory,
175
+ beep: false,
176
+ requestMicrophone: false,
177
+ log: () => { },
178
+ });
179
+ await dictation.start();
180
+ await vi.waitFor(() => expect(dictation.state).toBe('listening'));
181
+ expect(dictation.recording).toBe(true);
182
+ expect(made).toHaveLength(1);
183
+ });
184
+ it('records when the browser has no speech recognition at all (Firefox)', async () => {
185
+ const { factory } = fakeCaptureFactory();
186
+ const dictation = new Dictation({
187
+ source: () => {
188
+ throw Object.assign(new Error('Speech recognition not available'), {
189
+ code: 'CAPABILITY_NOT_AVAILABLE',
190
+ });
191
+ },
192
+ onText: () => { },
193
+ transcribe: async () => 'hello',
194
+ capture: factory,
195
+ beep: false,
196
+ requestMicrophone: false,
197
+ log: () => { },
198
+ });
199
+ await dictation.start();
200
+ await vi.waitFor(() => expect(dictation.state).toBe('listening'));
201
+ expect(dictation.recording).toBe(true);
202
+ });
203
+ it('records when the recogniser ends straight away without an error', async () => {
204
+ const source = fakeSource();
205
+ const { factory } = fakeCaptureFactory();
206
+ const dictation = new Dictation({
207
+ source: () => source,
208
+ onText: () => { },
209
+ transcribe: async () => 'hello',
210
+ capture: factory,
211
+ beep: false,
212
+ requestMicrophone: false,
213
+ log: () => { },
214
+ });
215
+ await dictation.start();
216
+ source.emitEnd();
217
+ await vi.waitFor(() => expect(dictation.recording).toBe(true));
218
+ expect(dictation.state).toBe('listening');
219
+ });
220
+ it('keeps the plain error without a transcribe function', async () => {
221
+ const source = fakeSource();
222
+ const dictation = new Dictation({
223
+ source: () => source,
224
+ onText: () => { },
225
+ beep: false,
226
+ requestMicrophone: false,
227
+ log: () => { },
228
+ });
229
+ await dictation.start();
230
+ source.emitError(speechError('network'));
231
+ expect(dictation.state).toBe('error');
232
+ expect(dictation.errorKind).toBe('unsupported');
233
+ });
234
+ it('does not switch after words were already heard', async () => {
235
+ const source = fakeSource();
236
+ const { factory } = fakeCaptureFactory();
237
+ const dictation = new Dictation({
238
+ source: () => source,
239
+ onText: () => { },
240
+ transcribe: async () => 'x',
241
+ capture: factory,
242
+ beep: false,
243
+ requestMicrophone: false,
244
+ log: () => { },
245
+ });
246
+ await dictation.start();
247
+ source.emitResult('hello', true);
248
+ source.emitError(speechError('network'));
249
+ expect(dictation.state).toBe('error');
250
+ expect(factory).not.toHaveBeenCalled();
251
+ });
252
+ it('does not record over a blocked microphone', async () => {
253
+ const { factory } = fakeCaptureFactory({
254
+ startError: Object.assign(new Error('Permission denied'), {
255
+ name: 'NotAllowedError',
256
+ }),
257
+ });
258
+ const dictation = new Dictation({
259
+ onText: () => { },
260
+ transcribe: async () => 'x',
261
+ capture: factory,
262
+ beep: false,
263
+ log: () => { },
264
+ });
265
+ await dictation.start();
266
+ expect(dictation.state).toBe('error');
267
+ expect(dictation.errorKind).toBe('denied');
268
+ });
269
+ it.each([
270
+ [new DictationError('too-long', 'too big'), 'too-long'],
271
+ [new DictationError('unavailable', 'not set up'), 'unavailable'],
272
+ [new DictationError('forbidden', 'no'), 'forbidden'],
273
+ [new Error('Failed to fetch: network down'), 'not-transcribed'],
274
+ ])('says why writing it down failed (%s)', async (error, kind) => {
275
+ const { factory } = fakeCaptureFactory();
276
+ const onText = vi.fn();
277
+ const dictation = new Dictation({
278
+ onText,
279
+ transcribe: async () => {
280
+ throw error;
281
+ },
282
+ capture: factory,
283
+ beep: false,
284
+ log: () => { },
285
+ });
286
+ await dictation.start();
287
+ await dictation.stop();
288
+ expect(dictation.state).toBe('error');
289
+ expect(dictation.errorKind).toBe(kind);
290
+ expect(onText).not.toHaveBeenCalled();
291
+ });
292
+ it('says nothing was heard when the text comes back empty', async () => {
293
+ const { factory } = fakeCaptureFactory();
294
+ const dictation = new Dictation({
295
+ onText: () => { },
296
+ transcribe: async () => ({ text: ' ' }),
297
+ capture: factory,
298
+ beep: false,
299
+ log: () => { },
300
+ });
301
+ await dictation.start();
302
+ await dictation.stop();
303
+ expect(dictation.errorKind).toBe('no-speech');
304
+ });
305
+ it('writes the recording down by itself at the time limit', async () => {
306
+ const { factory, made } = fakeCaptureFactory({
307
+ recording: { reachedTimeLimit: true, durationMs: 120_000 },
308
+ });
309
+ const onText = vi.fn();
310
+ const dictation = new Dictation({
311
+ onText,
312
+ transcribe: async () => 'a long story',
313
+ capture: factory,
314
+ beep: false,
315
+ });
316
+ await dictation.start();
317
+ made[0].hitLimit('time');
318
+ await vi.waitFor(() => expect(onText).toHaveBeenCalledWith('a long story'));
319
+ expect(dictation.state).toBe('idle');
320
+ });
321
+ it('fails as too long when the recording outgrows the size limit', async () => {
322
+ const { factory, made } = fakeCaptureFactory({
323
+ stopError: new DictationError('too-long', 'too large'),
324
+ });
325
+ const transcribe = vi.fn(async () => 'x');
326
+ const dictation = new Dictation({
327
+ onText: () => { },
328
+ transcribe,
329
+ capture: factory,
330
+ beep: false,
331
+ log: () => { },
332
+ });
333
+ await dictation.start();
334
+ made[0].hitLimit('size');
335
+ await vi.waitFor(() => expect(dictation.state).toBe('error'));
336
+ expect(dictation.errorKind).toBe('too-long');
337
+ expect(transcribe).not.toHaveBeenCalled();
338
+ });
339
+ it('stops quietly when the person stops before the microphone opened', async () => {
340
+ let open;
341
+ const { factory, made } = fakeCaptureFactory({
342
+ startGate: new Promise((resolve) => (open = resolve)),
343
+ });
344
+ const transcribe = vi.fn(async () => 'x');
345
+ const dictation = new Dictation({
346
+ onText: () => { },
347
+ transcribe,
348
+ capture: factory,
349
+ beep: false,
350
+ });
351
+ const started = dictation.start();
352
+ await vi.waitFor(() => expect(made).toHaveLength(1));
353
+ expect(dictation.state).toBe('starting');
354
+ await dictation.stop();
355
+ expect(dictation.state).toBe('idle');
356
+ expect(made[0].cancel).toHaveBeenCalled();
357
+ open();
358
+ await started;
359
+ expect(dictation.state).toBe('idle');
360
+ expect(transcribe).not.toHaveBeenCalled();
361
+ });
362
+ it('cancels writing it down when disposed', async () => {
363
+ const { factory } = fakeCaptureFactory();
364
+ let signal;
365
+ const onText = vi.fn();
366
+ const dictation = new Dictation({
367
+ onText,
368
+ transcribe: (_audio, options) => {
369
+ signal = options.signal;
370
+ return new Promise(() => { });
371
+ },
372
+ capture: factory,
373
+ beep: false,
374
+ });
375
+ await dictation.start();
376
+ void dictation.stop();
377
+ await vi.waitFor(() => expect(signal).toBeDefined());
378
+ expect(dictation.state).toBe('transcribing');
379
+ dictation.dispose();
380
+ expect(signal.aborted).toBe(true);
381
+ expect(dictation.state).toBe('idle');
382
+ expect(onText).not.toHaveBeenCalled();
383
+ });
384
+ it('gives up writing it down after the timeout', async () => {
385
+ const { factory } = fakeCaptureFactory();
386
+ const dictation = new Dictation({
387
+ onText: () => { },
388
+ transcribe: (_audio, { signal }) => new Promise((_resolve, reject) => signal.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')))),
389
+ capture: factory,
390
+ beep: false,
391
+ transcribeTimeoutMs: 20,
392
+ log: () => { },
393
+ });
394
+ await dictation.start();
395
+ await dictation.stop();
396
+ expect(dictation.errorKind).toBe('not-transcribed');
397
+ expect(dictation.errorCode).toBe('timeout');
398
+ });
399
+ it('ignores taps while writing it down', async () => {
400
+ const { factory, made } = fakeCaptureFactory();
401
+ const dictation = new Dictation({
402
+ onText: () => { },
403
+ transcribe: () => new Promise(() => { }),
404
+ capture: factory,
405
+ beep: false,
406
+ });
407
+ await dictation.start();
408
+ void dictation.stop();
409
+ await vi.waitFor(() => expect(dictation.state).toBe('transcribing'));
410
+ await dictation.toggle();
411
+ expect(dictation.state).toBe('transcribing');
412
+ expect(made).toHaveLength(1);
413
+ dictation.dispose();
414
+ });
415
+ });
416
+ describe('createHttpTranscriber', () => {
417
+ it('posts the raw audio with its type, language and length, and reads the text', async () => {
418
+ const fetch = vi.fn(async () => new Response(JSON.stringify({ text: 'hello there' }), {
419
+ status: 200,
420
+ headers: { 'content-type': 'application/json' },
421
+ }));
422
+ const transcribe = createHttpTranscriber('/sites/x/api/transcribe', {
423
+ fetch: fetch,
424
+ });
425
+ const audio = new Blob(['bytes'], { type: 'audio/mp4' });
426
+ const controller = new AbortController();
427
+ const text = await transcribe(audio, {
428
+ mimeType: 'audio/mp4',
429
+ language: 'en-US',
430
+ durationMs: 4321.4,
431
+ signal: controller.signal,
432
+ });
433
+ expect(text).toBe('hello there');
434
+ const [url, init] = fetch.mock.calls[0];
435
+ const parsed = new URL(url);
436
+ expect(parsed.pathname).toBe('/sites/x/api/transcribe');
437
+ expect(parsed.searchParams.get('language')).toBe('en-US');
438
+ expect(parsed.searchParams.get('durationMs')).toBe('4321');
439
+ expect(init.method).toBe('POST');
440
+ expect(init.body).toBe(audio);
441
+ expect(init.headers['Content-Type']).toBe('audio/mp4');
442
+ expect(init.signal).toBe(controller.signal);
443
+ });
444
+ it.each([
445
+ [413, 'too-long'],
446
+ [503, 'unavailable'],
447
+ [403, 'forbidden'],
448
+ [401, 'forbidden'],
449
+ [502, 'not-transcribed'],
450
+ ])('turns HTTP %i into %s', async (status, kind) => {
451
+ const transcribe = createHttpTranscriber('/t', {
452
+ fetch: (async () => new Response(JSON.stringify({ error: 'nope' }), {
453
+ status,
454
+ })),
455
+ });
456
+ const error = await transcribe(new Blob(['x']), {
457
+ mimeType: 'audio/webm',
458
+ language: 'en',
459
+ durationMs: 1,
460
+ signal: new AbortController().signal,
461
+ }).catch((e) => e);
462
+ expect(error).toBeInstanceOf(DictationError);
463
+ expect(error.dictationKind).toBe(kind);
464
+ expect(error.message).toBe('nope');
465
+ });
466
+ });
467
+ class FakeMediaRecorder extends EventTarget {
468
+ stream;
469
+ static supported = new Set(['audio/webm;codecs=opus', 'audio/webm']);
470
+ static isTypeSupported(type) {
471
+ return FakeMediaRecorder.supported.has(type);
472
+ }
473
+ static last = null;
474
+ state = 'inactive';
475
+ mimeType;
476
+ timeslice;
477
+ constructor(stream, options) {
478
+ super();
479
+ this.stream = stream;
480
+ this.mimeType = options?.mimeType ?? '';
481
+ FakeMediaRecorder.last = this;
482
+ }
483
+ start(timeslice) {
484
+ this.timeslice = timeslice;
485
+ this.state = 'recording';
486
+ }
487
+ stop() {
488
+ this.state = 'inactive';
489
+ queueMicrotask(() => this.dispatchEvent(new Event('stop')));
490
+ }
491
+ emitData(bytes) {
492
+ const event = Object.assign(new Event('dataavailable'), {
493
+ data: new Blob([new Uint8Array(bytes)]),
494
+ });
495
+ this.dispatchEvent(event);
496
+ }
497
+ }
498
+ function stubMedia() {
499
+ const track = { stop: vi.fn() };
500
+ const stream = { getTracks: () => [track] };
501
+ const getUserMedia = vi.fn(async () => stream);
502
+ vi.stubGlobal('MediaRecorder', FakeMediaRecorder);
503
+ vi.stubGlobal('navigator', { mediaDevices: { getUserMedia } });
504
+ return { track, getUserMedia };
505
+ }
506
+ describe('createMediaRecorderCapture', () => {
507
+ it('prefers WebM/Opus, then MP4 (Safari)', () => {
508
+ stubMedia();
509
+ expect(pickDictationMimeType()).toBe('audio/webm;codecs=opus');
510
+ FakeMediaRecorder.supported = new Set(['audio/mp4']);
511
+ expect(pickDictationMimeType()).toBe('audio/mp4');
512
+ FakeMediaRecorder.supported = new Set([
513
+ 'audio/webm;codecs=opus',
514
+ 'audio/webm',
515
+ ]);
516
+ });
517
+ it('records, stops, and closes the microphone', async () => {
518
+ const { track, getUserMedia } = stubMedia();
519
+ const capture = createMediaRecorderCapture({});
520
+ await capture.start();
521
+ expect(getUserMedia).toHaveBeenCalledWith({ audio: true });
522
+ const recorder = FakeMediaRecorder.last;
523
+ expect(recorder.mimeType).toBe('audio/webm;codecs=opus');
524
+ expect(recorder.timeslice).toBe(1000);
525
+ recorder.emitData(100);
526
+ recorder.emitData(50);
527
+ const recording = await capture.stop();
528
+ expect(recording.audio.size).toBe(150);
529
+ expect(recording.mimeType).toBe('audio/webm;codecs=opus');
530
+ expect(recording.reachedTimeLimit).toBe(false);
531
+ expect(track.stop).toHaveBeenCalled();
532
+ });
533
+ it('stops by itself at the time limit and keeps the audio', async () => {
534
+ vi.useFakeTimers();
535
+ const { track } = stubMedia();
536
+ const capture = createMediaRecorderCapture({ maxDurationMs: 5000 });
537
+ const onLimit = vi.fn();
538
+ capture.onLimit(onLimit);
539
+ await capture.start();
540
+ FakeMediaRecorder.last.emitData(10);
541
+ vi.advanceTimersByTime(5000);
542
+ expect(onLimit).toHaveBeenCalledWith('time');
543
+ expect(track.stop).toHaveBeenCalled();
544
+ const recording = await capture.stop();
545
+ expect(recording.reachedTimeLimit).toBe(true);
546
+ expect(recording.audio.size).toBe(10);
547
+ });
548
+ it('fails as too long past the size limit', async () => {
549
+ stubMedia();
550
+ const capture = createMediaRecorderCapture({ maxBytes: 100 });
551
+ const onLimit = vi.fn();
552
+ capture.onLimit(onLimit);
553
+ await capture.start();
554
+ FakeMediaRecorder.last.emitData(80);
555
+ FakeMediaRecorder.last.emitData(80);
556
+ expect(onLimit).toHaveBeenCalledWith('size');
557
+ const error = await capture.stop().catch((e) => e);
558
+ expect(error.dictationKind).toBe('too-long');
559
+ });
560
+ it('stops waiting when the browser never fires the recorder stop event', async () => {
561
+ vi.useFakeTimers();
562
+ stubMedia();
563
+ const capture = createMediaRecorderCapture({});
564
+ await capture.start();
565
+ const recorder = FakeMediaRecorder.last;
566
+ recorder.stop = () => {
567
+ recorder.state = 'inactive';
568
+ };
569
+ recorder.emitData(42);
570
+ const pending = capture.stop();
571
+ await vi.advanceTimersByTimeAsync(3000);
572
+ const recording = await pending;
573
+ expect(recording.audio.size).toBe(42);
574
+ vi.useRealTimers();
575
+ });
576
+ it('reports a browser that cannot record', async () => {
577
+ vi.stubGlobal('MediaRecorder', undefined);
578
+ const capture = createMediaRecorderCapture({});
579
+ const error = await capture.start().catch((e) => e);
580
+ expect(error.dictationKind).toBe('unsupported');
581
+ });
582
+ });