@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,354 @@
1
+ import { fireEvent, render, screen } from '@testing-library/svelte';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { tick } from 'svelte';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import Combobox from '../Combobox.svelte';
6
+ import { createControlInteractionRegistry, executeLocalControlCommand, } from '../control-interaction.js';
7
+ import FormFixture from './combobox-form.fixture.svelte';
8
+ import LabelsFixture from './combobox-labels.fixture.svelte';
9
+ function data() {
10
+ return new FormData(screen.getByRole('form', { name: 'Order' }));
11
+ }
12
+ async function selectPlate() {
13
+ await userEvent.click(screen.getByRole('combobox', { name: 'SKU' }));
14
+ await userEvent.click(screen.getByRole('option', { name: 'Plate 1/4 A36' }));
15
+ }
16
+ describe('Combobox native form value', () => {
17
+ it('posts the selected value once, retaining it while searching', async () => {
18
+ render(FormFixture);
19
+ await selectPlate();
20
+ expect(data().getAll('sku')).toEqual(['42']);
21
+ const input = screen.getByRole('combobox', { name: 'SKU' });
22
+ expect(input).not.toHaveAttribute('name');
23
+ await userEvent.clear(input);
24
+ await userEvent.type(input, 'unselected search');
25
+ expect(data().getAll('sku')).toEqual(['42']);
26
+ });
27
+ it('posts keyboard-selected and custom values', async () => {
28
+ render(FormFixture, { props: { allowCustom: true } });
29
+ const input = screen.getByRole('combobox', { name: 'SKU' });
30
+ await userEvent.type(input, 'Plate');
31
+ await userEvent.keyboard('{Enter}');
32
+ expect(data().getAll('sku')).toEqual(['42']);
33
+ await userEvent.clear(input);
34
+ await userEvent.type(input, 'custom');
35
+ expect(data().getAll('sku')).toEqual(['custom']);
36
+ });
37
+ it('posts an empty named value and omits missing or empty names', async () => {
38
+ const { rerender } = render(FormFixture);
39
+ expect(data().getAll('sku')).toEqual(['']);
40
+ await rerender({ name: '' });
41
+ expect([...data()]).toEqual([]);
42
+ await rerender({ unnamed: true });
43
+ expect([...data()]).toEqual([]);
44
+ });
45
+ it.each([
46
+ { disabled: true },
47
+ { fieldsetDisabled: true },
48
+ ])('omits disabled values: %j', (props) => {
49
+ render(FormFixture, { props: { ...props, value: '42' } });
50
+ expect([...data()]).toEqual([]);
51
+ });
52
+ it('does not select disabled options', async () => {
53
+ render(FormFixture);
54
+ await userEvent.click(screen.getByRole('combobox', { name: 'SKU' }));
55
+ await userEvent.click(screen.getByRole('option', { name: 'Disabled plate' }));
56
+ expect(data().getAll('sku')).toEqual(['']);
57
+ });
58
+ it.each([
59
+ '',
60
+ '42',
61
+ ])('restores initial selection %j on native reset', async (value) => {
62
+ render(FormFixture, { props: { value, allowCustom: true } });
63
+ const input = screen.getByRole('combobox', { name: 'SKU' });
64
+ await userEvent.clear(input);
65
+ await userEvent.type(input, 'changed');
66
+ screen.getByRole('form', { name: 'Order' }).reset();
67
+ await settleReset();
68
+ expect(data().getAll('sku')).toEqual([value]);
69
+ expect(input).toHaveValue(value ? 'Plate 1/4 A36' : '');
70
+ });
71
+ it('preserves selection when reset is canceled', async () => {
72
+ render(FormFixture);
73
+ await selectPlate();
74
+ const form = screen.getByRole('form', { name: 'Order' });
75
+ form.addEventListener('reset', (event) => event.preventDefault());
76
+ await fireEvent.reset(form);
77
+ await settleReset();
78
+ expect(data().getAll('sku')).toEqual(['42']);
79
+ });
80
+ it('posts registry-applied values while preserving identity and staged isolation', async () => {
81
+ const registry = createControlInteractionRegistry({
82
+ isLocalGesture: () => true,
83
+ });
84
+ render(FormFixture, { props: { registry } });
85
+ await registry.execute({
86
+ action: 'stage',
87
+ identity: { formId: 'order', controlId: 'sku' },
88
+ value: '42',
89
+ }, { source: 'agent' });
90
+ expect(data().getAll('sku')).toEqual(['']);
91
+ await userEvent.click(screen.getByRole('button', { name: 'Apply valid changes' }));
92
+ expect(data().getAll('sku')).toEqual(['42']);
93
+ expect(registry.get({ formId: 'order', controlId: 'sku' })?.state.value).toBe('42');
94
+ });
95
+ });
96
+ const towns = [
97
+ { value: '0b9c-uuid-lacombe', label: 'Lacombe' },
98
+ { value: '77aa-uuid-blackfalds', label: 'Blackfalds' },
99
+ { value: '12cd-uuid-ponoka', label: 'Ponoka' },
100
+ ];
101
+ describe('Combobox labels and reopening', () => {
102
+ it('shows the option label on first render, never the raw id', () => {
103
+ render(LabelsFixture, {
104
+ props: { options: towns, value: '77aa-uuid-blackfalds' },
105
+ });
106
+ expect(screen.getByRole('combobox', { name: 'Town' })).toHaveValue('Blackfalds');
107
+ });
108
+ it('shows valueLabel (or nothing) until async options arrive, then the label', async () => {
109
+ const view = render(LabelsFixture, {
110
+ props: { options: [], value: '12cd-uuid-ponoka', valueLabel: 'Ponoka' },
111
+ });
112
+ const input = screen.getByRole('combobox', { name: 'Town' });
113
+ expect(input).toHaveValue('Ponoka');
114
+ await view.rerender({
115
+ options: [],
116
+ value: '12cd-uuid-ponoka',
117
+ valueLabel: undefined,
118
+ });
119
+ await settleReset();
120
+ expect(input).toHaveValue('');
121
+ expect(input).not.toHaveValue('12cd-uuid-ponoka');
122
+ await view.rerender({ options: towns, value: '12cd-uuid-ponoka' });
123
+ await settleReset();
124
+ expect(input).toHaveValue('Ponoka');
125
+ });
126
+ it('reopening after a choice lists every option, not only the current one', async () => {
127
+ render(LabelsFixture, { props: { options: towns, value: '' } });
128
+ const input = screen.getByRole('combobox', { name: 'Town' });
129
+ await userEvent.type(input, 'lacom');
130
+ expect(screen.getAllByRole('option')).toHaveLength(1);
131
+ await userEvent.click(screen.getByRole('option', { name: 'Lacombe' }));
132
+ expect(screen.getByTestId('value')).toHaveTextContent('0b9c-uuid-lacombe');
133
+ expect(input).toHaveValue('Lacombe');
134
+ await userEvent.click(document.body);
135
+ await userEvent.click(input);
136
+ expect(screen.getAllByRole('option')).toHaveLength(3);
137
+ expect(screen.getByRole('option', { name: 'Lacombe' })).toHaveAttribute('aria-selected', 'true');
138
+ });
139
+ it('puts the chosen label back when the person leaves without choosing', async () => {
140
+ render(LabelsFixture, {
141
+ props: { options: towns, value: '77aa-uuid-blackfalds' },
142
+ });
143
+ const input = screen.getByRole('combobox', { name: 'Town' });
144
+ await userEvent.clear(input);
145
+ await userEvent.type(input, 'pon');
146
+ await userEvent.keyboard('{Escape}');
147
+ expect(input).toHaveValue('Blackfalds');
148
+ expect(screen.getByTestId('value')).toHaveTextContent('77aa-uuid-blackfalds');
149
+ });
150
+ it('posts the committed id under its name, not the visible label', async () => {
151
+ const { container } = render(LabelsFixture, {
152
+ props: { options: towns, value: '77aa-uuid-blackfalds' },
153
+ });
154
+ const input = screen.getByRole('combobox', { name: 'Town' });
155
+ expect(input).not.toHaveAttribute('name');
156
+ const posted = container.querySelectorAll('input[name="town"]');
157
+ expect(posted).toHaveLength(1);
158
+ expect(posted[0]).toHaveValue('77aa-uuid-blackfalds');
159
+ await userEvent.click(input);
160
+ await userEvent.click(screen.getByRole('option', { name: 'Ponoka' }));
161
+ expect(container.querySelector('input[name="town"]')).toHaveValue('12cd-uuid-ponoka');
162
+ });
163
+ it('a required combobox is valid while its value is committed, even before its option loads', async () => {
164
+ const view = render(LabelsFixture, {
165
+ props: { options: [], value: '12cd-uuid-ponoka', required: true },
166
+ });
167
+ const input = screen.getByRole('combobox', {
168
+ name: 'Town',
169
+ });
170
+ expect(input).toHaveValue('');
171
+ expect(input.checkValidity()).toBe(true);
172
+ expect(input).toHaveAttribute('aria-required', 'true');
173
+ await view.rerender({ options: towns, value: '', required: true });
174
+ await settleReset();
175
+ expect(input.checkValidity()).toBe(false);
176
+ });
177
+ });
178
+ async function settleReset() {
179
+ await new Promise((resolve) => setTimeout(resolve));
180
+ await tick();
181
+ }
182
+ describe('Combobox deferred native reset', () => {
183
+ it('preserves a newer programmatic value before reset completion', async () => {
184
+ const target = document.createElement('form');
185
+ document.body.append(target);
186
+ const view = render(Combobox, {
187
+ target,
188
+ props: {
189
+ options: [
190
+ { value: 42, label: 'Initial' },
191
+ { value: 'other', label: 'Other' },
192
+ { value: 'changed', label: 'Changed' },
193
+ ],
194
+ label: 'Choice',
195
+ name: 'choice',
196
+ value: '42',
197
+ },
198
+ });
199
+ await view.rerender({ value: 'changed' });
200
+ target.reset();
201
+ await view.rerender({ value: 'other' });
202
+ await settleReset();
203
+ expect(new FormData(target).getAll('choice')).toEqual(['other']);
204
+ await view.unmount();
205
+ target.remove();
206
+ });
207
+ it('cancels pending completion when unmounted', async () => {
208
+ const target = document.createElement('form');
209
+ document.body.append(target);
210
+ const onChange = vi.fn();
211
+ const view = render(Combobox, {
212
+ target,
213
+ props: {
214
+ options: [
215
+ { value: 42, label: 'Initial' },
216
+ { value: 'other', label: 'Other' },
217
+ { value: 'changed', label: 'Changed' },
218
+ ],
219
+ label: 'Choice',
220
+ name: 'choice',
221
+ value: '42',
222
+ onvaluechange: onChange,
223
+ },
224
+ });
225
+ target.reset();
226
+ await view.unmount();
227
+ await settleReset();
228
+ expect(onChange).not.toHaveBeenCalled();
229
+ target.remove();
230
+ });
231
+ });
232
+ describe('Combobox reset listener ordering', () => {
233
+ it('honors cancellation from a later listener microtask', async () => {
234
+ const target = document.createElement('form');
235
+ document.body.append(target);
236
+ const onChange = vi.fn();
237
+ const view = render(Combobox, {
238
+ target,
239
+ props: {
240
+ options: [
241
+ { value: 42, label: 'Initial' },
242
+ { value: 'changed', label: 'Changed' },
243
+ ],
244
+ label: 'Choice',
245
+ name: 'choice',
246
+ value: '42',
247
+ onvaluechange: onChange,
248
+ },
249
+ });
250
+ await view.rerender({ value: 'changed' });
251
+ target.addEventListener('reset', (event) => {
252
+ queueMicrotask(() => event.preventDefault());
253
+ });
254
+ target.reset();
255
+ await settleReset();
256
+ expect(new FormData(target).getAll('choice')).toEqual(['changed']);
257
+ expect(onChange).not.toHaveBeenCalled();
258
+ await view.unmount();
259
+ target.remove();
260
+ });
261
+ });
262
+ function dispatchLocalCommand(registry, command) {
263
+ const target = new EventTarget();
264
+ let pending;
265
+ target.addEventListener('click', (event) => {
266
+ pending = executeLocalControlCommand(registry, command, event);
267
+ }, { once: true });
268
+ target.dispatchEvent(new Event('click'));
269
+ if (!pending)
270
+ throw new Error('local command handler did not run');
271
+ return pending;
272
+ }
273
+ describe('Combobox reset registry currency', () => {
274
+ it.each([
275
+ false,
276
+ true,
277
+ ])('keeps staged proposal currency correct when cancellation is %s', async (cancel) => {
278
+ const registry = createControlInteractionRegistry({
279
+ isLocalGesture: () => true,
280
+ });
281
+ render(FormFixture, {
282
+ props: { ...{ value: '42', allowCustom: true }, registry },
283
+ });
284
+ const identity = { formId: 'order', controlId: 'sku' };
285
+ const target = screen.getByRole('form', {
286
+ name: 'Order',
287
+ });
288
+ await userEvent.clear(screen.getByRole('combobox', { name: 'SKU' }));
289
+ await userEvent.type(screen.getByRole('combobox', { name: 'SKU' }), 'custom');
290
+ expect((await registry.execute({ action: 'stage', identity, value: '42' }, { source: 'agent' })).ok).toBe(true);
291
+ if (cancel)
292
+ target.addEventListener('reset', (event) => event.preventDefault());
293
+ target.reset();
294
+ await settleReset();
295
+ expect(registry.get(identity)?.state.staged?.stale).toBe(!cancel);
296
+ const applied = await dispatchLocalCommand(registry, {
297
+ action: 'apply',
298
+ identity,
299
+ });
300
+ expect(applied.ok).toBe(cancel);
301
+ if (!cancel) {
302
+ expect(applied.reason).toBe('staged_value_stale');
303
+ expect(new FormData(target).getAll('sku')).toEqual(['42']);
304
+ }
305
+ });
306
+ it.each([
307
+ false,
308
+ true,
309
+ ])('preserves reset over an async local clear when cancellation is %s', async (cancel) => {
310
+ let releasePolicy;
311
+ let policyStarted;
312
+ const blocked = new Promise((resolve) => {
313
+ releasePolicy = resolve;
314
+ });
315
+ const started = new Promise((resolve) => {
316
+ policyStarted = resolve;
317
+ });
318
+ const registry = createControlInteractionRegistry({
319
+ isLocalGesture: () => true,
320
+ policy: async (command) => {
321
+ if (command.action === 'clear') {
322
+ policyStarted();
323
+ await blocked;
324
+ }
325
+ return { allowed: true };
326
+ },
327
+ });
328
+ render(FormFixture, {
329
+ props: { ...{ value: '42', allowCustom: true }, registry },
330
+ });
331
+ const identity = { formId: 'order', controlId: 'sku' };
332
+ const target = screen.getByRole('form', {
333
+ name: 'Order',
334
+ });
335
+ await userEvent.clear(screen.getByRole('combobox', { name: 'SKU' }));
336
+ await userEvent.type(screen.getByRole('combobox', { name: 'SKU' }), 'custom');
337
+ const clearing = dispatchLocalCommand(registry, {
338
+ action: 'clear',
339
+ identity,
340
+ });
341
+ await started;
342
+ if (cancel)
343
+ target.addEventListener('reset', (event) => event.preventDefault());
344
+ target.reset();
345
+ await settleReset();
346
+ releasePolicy();
347
+ const result = await clearing;
348
+ await tick();
349
+ expect(result.ok).toBe(cancel);
350
+ if (!cancel)
351
+ expect(result.reason).toBe('staged_value_stale');
352
+ expect(new FormData(target).getAll('sku')).toEqual(cancel ? [''] : ['42']);
353
+ });
354
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Checkbox, Radio and Switch carry an invisible 44x44 hit area around the
3
+ * visible control. jsdom does no layout, so the geometry is asserted against
4
+ * the component source (the contract) and the click behavior against the DOM;
5
+ * the measured result is verified in a real browser.
6
+ */
7
+ import { readFileSync } from 'node:fs';
8
+ import { dirname, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { render, screen } from '@testing-library/svelte';
11
+ import userEvent from '@testing-library/user-event';
12
+ import { describe, expect, it } from 'vitest';
13
+ import Checkbox from '../Checkbox.svelte';
14
+ import Switch from '../Switch.svelte';
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ const source = (name) => readFileSync(resolve(here, '..', `${name}.svelte`), 'utf8');
17
+ describe.each(['Checkbox', 'Radio', 'Switch'])('%s hit area', (name) => {
18
+ const css = source(name).slice(source(name).indexOf('<style>'));
19
+ it('draws a 44px (2.75rem) transparent ::before target that adds no layout', () => {
20
+ expect(css).toContain('--smrt-control-hit-size, 2.75rem');
21
+ expect(css).toMatch(/::before \{[^}]*position: absolute/);
22
+ expect(css).toMatch(/::before \{[^}]*content: ''/);
23
+ });
24
+ it('keeps the visible control and its label above every hit area', () => {
25
+ expect(css).toMatch(/__(box|mark|track)[^{]*\{[^}]*z-index: 1/);
26
+ expect(css).toMatch(/__label[^{]*\{[^}]*z-index: 1/);
27
+ });
28
+ });
29
+ describe.each(['Checkbox', 'Radio'])('%s in table cells', (name) => {
30
+ it('never grows horizontally out of the cell', () => {
31
+ expect(source(name)).toMatch(/:global\(:is\(td, th\)\) \.(checkbox|radio)::before \{ inline-size: var\(--_box\)/);
32
+ });
33
+ });
34
+ describe.each(['Checkbox', 'Radio'])('%s beside other content', (name) => {
35
+ it('keeps to its own column unless it stands alone, so it never covers a neighbouring link or text', () => {
36
+ expect(source(name)).toMatch(/\.(checkbox|radio):not\(:only-child\)::before \{ inline-size: var\(--_box\); margin-inline-start: calc\(var\(--_box\) \/ -2\)/);
37
+ });
38
+ });
39
+ describe('Checkbox table cell fill', () => {
40
+ it('uses the whole cell, clipped to it, when the checkbox is its only content', () => {
41
+ const css = source('Checkbox');
42
+ expect(css).toContain('> [data-smrt-hit-target]:only-child');
43
+ expect(css).toMatch(/::before \{ inset: 0;/);
44
+ });
45
+ it('positions the cell at zero specificity, so sticky headers and pinned cells keep theirs', () => {
46
+ const css = source('Checkbox');
47
+ expect(css).toContain(':global(:where(td:has(> [data-smrt-hit-target]:only-child), th:has(> [data-smrt-hit-target]:only-child))) { position: relative; }');
48
+ expect(css).not.toMatch(/:global\(:is\(td, th\):has\([^)]*\)\)\) \{ position: relative/);
49
+ });
50
+ });
51
+ describe('hit area behavior', () => {
52
+ it('Checkbox toggles from a click on its label element', async () => {
53
+ const user = userEvent.setup();
54
+ const { container } = render(Checkbox, { props: { label: 'Accept' } });
55
+ expect(container.querySelector('label')).toHaveAttribute('data-smrt-hit-target');
56
+ await user.click(container.querySelector('label'));
57
+ expect(screen.getByRole('checkbox', { name: 'Accept' })).toBeChecked();
58
+ });
59
+ it('Checkbox without text keeps the label as the click target', async () => {
60
+ const user = userEvent.setup();
61
+ const { container } = render(Checkbox, {
62
+ props: { 'aria-label': 'Select row' },
63
+ });
64
+ await user.click(container.querySelector('label'));
65
+ expect(screen.getByRole('checkbox', { name: 'Select row' })).toBeChecked();
66
+ });
67
+ it('Switch toggles from a click on its label element', async () => {
68
+ const user = userEvent.setup();
69
+ const { container } = render(Switch, { props: { label: 'Alerts' } });
70
+ await user.click(container.querySelector('label'));
71
+ expect(screen.getByRole('switch', { name: 'Alerts' })).toBeChecked();
72
+ });
73
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * DictationButton + DictationStatus: a named 44px mic toggle with
3
+ * aria-pressed, and plain-words status for listening and each error.
4
+ */
5
+ import { render, screen } from '@testing-library/svelte';
6
+ import userEvent from '@testing-library/user-event';
7
+ import { flushSync } from 'svelte';
8
+ import { describe, expect, it, vi } from 'vitest';
9
+ import { expectNoA11yViolations } from '../../../test-support/a11y';
10
+ import { DictationError } from '../audio-capture.js';
11
+ import DictationButton from '../DictationButton.svelte';
12
+ import DictationStatus from '../DictationStatus.svelte';
13
+ import { Dictation } from '../dictation.svelte.js';
14
+ function quietSource() {
15
+ const ends = new Set();
16
+ return {
17
+ start: async () => { },
18
+ stop: async () => {
19
+ for (const cb of ends)
20
+ cb();
21
+ },
22
+ onResult: () => () => { },
23
+ onError: () => () => { },
24
+ onEnd: (cb) => (ends.add(cb), () => ends.delete(cb)),
25
+ };
26
+ }
27
+ describe('DictationButton', () => {
28
+ it('toggles listening with a name that follows the state', async () => {
29
+ const dictation = new Dictation({
30
+ source: quietSource,
31
+ onText: () => { },
32
+ beep: false,
33
+ });
34
+ const { container } = render(DictationButton, { props: { dictation } });
35
+ const button = screen.getByRole('button', {
36
+ name: 'Speak instead of typing',
37
+ });
38
+ expect(button).toHaveAttribute('aria-pressed', 'false');
39
+ await expectNoA11yViolations(container);
40
+ await userEvent.click(button);
41
+ await vi.waitFor(() => expect(dictation.state).toBe('listening'));
42
+ flushSync();
43
+ const stop = screen.getByRole('button', { name: 'Stop listening' });
44
+ expect(stop).toHaveAttribute('aria-pressed', 'true');
45
+ await userEvent.click(stop);
46
+ await vi.waitFor(() => expect(dictation.state).toBe('idle'));
47
+ });
48
+ });
49
+ describe('DictationButton — one click, one toggle', () => {
50
+ it('a single click starts once and does not stop again', async () => {
51
+ const start = vi.fn(async () => { });
52
+ const stop = vi.fn(async () => { });
53
+ const source = {
54
+ start,
55
+ stop,
56
+ onResult: () => () => { },
57
+ onError: () => () => { },
58
+ onEnd: () => () => { },
59
+ };
60
+ const dictation = new Dictation({
61
+ source: () => source,
62
+ onText: () => { },
63
+ beep: false,
64
+ });
65
+ render(DictationButton, { props: { dictation } });
66
+ // userEvent.click sends pointerdown, pointerup and click.
67
+ await userEvent.click(screen.getByRole('button', { name: 'Speak instead of typing' }));
68
+ await vi.waitFor(() => expect(dictation.state).toBe('listening'));
69
+ expect(start).toHaveBeenCalledTimes(1);
70
+ expect(stop).not.toHaveBeenCalled();
71
+ });
72
+ });
73
+ describe('DictationStatus', () => {
74
+ function erroringSource(code) {
75
+ const errors = new Set();
76
+ return {
77
+ start: async () => {
78
+ queueMicrotask(() => {
79
+ for (const cb of errors)
80
+ cb(Object.assign(new Error(code), { speechError: code }));
81
+ });
82
+ },
83
+ stop: async () => { },
84
+ onResult: () => () => { },
85
+ onError: (cb) => (errors.add(cb), () => errors.delete(cb)),
86
+ onEnd: () => () => { },
87
+ };
88
+ }
89
+ it.each([
90
+ [
91
+ 'network',
92
+ "Speech recognition isn't available in this browser. Brave blocks it",
93
+ ],
94
+ [
95
+ 'service-not-allowed',
96
+ "Speech recognition isn't available in this browser",
97
+ ],
98
+ ['not-allowed', 'The microphone is blocked'],
99
+ ['no-speech', "Didn't hear anything. Tap the mic and try again."],
100
+ ['audio-capture', "Couldn't use the microphone"],
101
+ ['aborted', 'Listening stopped straight away'],
102
+ ])('shows a plain message for %s instead of "Listening"', async (code, text) => {
103
+ const source = erroringSource(code);
104
+ const dictation = new Dictation({
105
+ source: () => source,
106
+ onText: () => { },
107
+ beep: false,
108
+ log: () => { },
109
+ });
110
+ const { container } = render(DictationStatus, { props: { dictation } });
111
+ await dictation.start();
112
+ await vi.waitFor(() => expect(dictation.state).toBe('error'));
113
+ flushSync();
114
+ expect(screen.getByRole('alert')).toHaveTextContent(text);
115
+ expect(screen.getByRole('alert')).toHaveAttribute('data-dictation-error', code);
116
+ expect(screen.getByRole('status')).not.toHaveTextContent('Listening');
117
+ expect(container.querySelector('[data-dictation-error]')).not.toBeNull();
118
+ });
119
+ it('says it is listening, then explains a blocked microphone', async () => {
120
+ const dictation = new Dictation({
121
+ source: quietSource,
122
+ onText: () => { },
123
+ beep: false,
124
+ });
125
+ render(DictationStatus, { props: { dictation } });
126
+ await dictation.start();
127
+ flushSync();
128
+ expect(screen.getByRole('status')).toHaveTextContent('Listening. Tap the microphone when you are done.');
129
+ const blocked = new Dictation({ source: null, onText: () => { } });
130
+ render(DictationStatus, { props: { dictation: blocked } });
131
+ await blocked.start();
132
+ flushSync();
133
+ expect(screen.getAllByRole('alert').at(-1)).toHaveTextContent("Speech recognition isn't available in this browser");
134
+ });
135
+ });
136
+ describe('Dictation recording fallback in words', () => {
137
+ it('says "Writing it down…" while a recording is written down, then explains a failure', async () => {
138
+ let finish;
139
+ const dictation = new Dictation({
140
+ onText: () => { },
141
+ beep: false,
142
+ log: () => { },
143
+ capture: () => ({
144
+ start: async () => { },
145
+ stop: async () => ({
146
+ audio: new Blob(['x'], { type: 'audio/webm' }),
147
+ mimeType: 'audio/webm',
148
+ durationMs: 1000,
149
+ reachedTimeLimit: false,
150
+ }),
151
+ cancel: () => { },
152
+ onLimit: () => () => { },
153
+ }),
154
+ transcribe: () => new Promise((_resolve, reject) => (finish = reject)),
155
+ });
156
+ const { container } = render(DictationStatus, { props: { dictation } });
157
+ render(DictationButton, { props: { dictation } });
158
+ await dictation.start();
159
+ flushSync();
160
+ expect(screen.getByRole('status')).toHaveTextContent('Listening');
161
+ void dictation.stop();
162
+ await vi.waitFor(() => expect(finish).toBeTypeOf('function'));
163
+ flushSync();
164
+ expect(screen.getByRole('status')).toHaveTextContent('Writing it down…');
165
+ const button = screen.getByRole('button', {
166
+ name: 'Speak instead of typing',
167
+ });
168
+ expect(button).toBeDisabled();
169
+ expect(button).toHaveAttribute('aria-busy', 'true');
170
+ await expectNoA11yViolations(container);
171
+ finish(new DictationError('unavailable', 'not set up'));
172
+ await vi.waitFor(() => expect(dictation.state).toBe('error'));
173
+ flushSync();
174
+ expect(screen.getByRole('alert')).toHaveTextContent("Speaking isn't set up here yet. Please type instead.");
175
+ });
176
+ });
@@ -0,0 +1,37 @@
1
+ import { render, screen } from '@testing-library/svelte';
2
+ import { createRawSnippet } from 'svelte';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { expectNoA11yViolations } from '../../../test-support/a11y';
5
+ import Fieldset from '../Fieldset.svelte';
6
+ const fields = createRawSnippet(() => ({
7
+ render: () => '<label>Headline <input name="headline" /></label>',
8
+ }));
9
+ describe('Fieldset', () => {
10
+ it('is a group named by its legend', () => {
11
+ render(Fieldset, { props: { legend: 'The ad', children: fields } });
12
+ expect(screen.getByRole('group', { name: 'The ad' })).toBeInTheDocument();
13
+ });
14
+ it('flows its content unless asked to stack it', () => {
15
+ const { container } = render(Fieldset, {
16
+ props: { legend: 'The ad', children: fields },
17
+ });
18
+ expect(container.querySelector('.content--stack')).toBeNull();
19
+ });
20
+ it('stacks its fields in one column with a gap when asked', () => {
21
+ const { container } = render(Fieldset, {
22
+ props: { legend: 'The ad', stack: true, children: fields },
23
+ });
24
+ expect(container.querySelector('.content.content--stack')).not.toBeNull();
25
+ });
26
+ it('is axe-clean', async () => {
27
+ const { container } = render(Fieldset, {
28
+ props: {
29
+ legend: 'The ad',
30
+ description: 'Shown to readers',
31
+ stack: true,
32
+ children: fields,
33
+ },
34
+ });
35
+ await expectNoA11yViolations(container);
36
+ });
37
+ });