tempest-react-sdk 0.38.2 → 0.39.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 (332) hide show
  1. package/README.md +1 -1
  2. package/bin/lib/design/collect.mjs +19 -0
  3. package/bin/lib/design/functions.mjs +5 -0
  4. package/bin/lib/design/index.mjs +8 -3
  5. package/bin/lib/design/index.test.mjs +31 -0
  6. package/bin/lib/design/scan.mjs +34 -2
  7. package/bin/lib/design/scan.test.mjs +78 -0
  8. package/bin/lib/doctor/doctor.e2e.test.mjs +85 -0
  9. package/bin/tempest.mjs +90 -6
  10. package/dist/audio/level-meter.cjs.map +1 -1
  11. package/dist/audio/level-meter.js.map +1 -1
  12. package/dist/audio/use-audio-recorder.cjs.map +1 -1
  13. package/dist/audio/use-audio-recorder.js.map +1 -1
  14. package/dist/auth/passkey.cjs.map +1 -1
  15. package/dist/auth/passkey.js.map +1 -1
  16. package/dist/auth/use-passkey.cjs.map +1 -1
  17. package/dist/auth/use-passkey.js.map +1 -1
  18. package/dist/br/BrazilMap.cjs.map +1 -1
  19. package/dist/br/BrazilMap.js.map +1 -1
  20. package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
  21. package/dist/br/BrazilStateCitySelect.js.map +1 -1
  22. package/dist/br/BrazilStateMap.cjs.map +1 -1
  23. package/dist/br/BrazilStateMap.js.map +1 -1
  24. package/dist/br/PixQRCode.cjs.map +1 -1
  25. package/dist/br/PixQRCode.js.map +1 -1
  26. package/dist/br/boleto.cjs.map +1 -1
  27. package/dist/br/boleto.js.map +1 -1
  28. package/dist/br/pix.cjs.map +1 -1
  29. package/dist/br/pix.js.map +1 -1
  30. package/dist/br.d.ts +8 -0
  31. package/dist/capture/media-recorder.cjs.map +1 -1
  32. package/dist/capture/media-recorder.js.map +1 -1
  33. package/dist/capture/use-barcode-scanner.cjs.map +1 -1
  34. package/dist/capture/use-barcode-scanner.js.map +1 -1
  35. package/dist/capture/use-screen-capture.cjs.map +1 -1
  36. package/dist/capture/use-screen-capture.js.map +1 -1
  37. package/dist/capture/use-speech-recognition.cjs.map +1 -1
  38. package/dist/capture/use-speech-recognition.js.map +1 -1
  39. package/dist/capture/use-video-recorder.cjs.map +1 -1
  40. package/dist/capture/use-video-recorder.js.map +1 -1
  41. package/dist/charts/types.cjs.map +1 -1
  42. package/dist/charts/types.js.map +1 -1
  43. package/dist/charts.d.ts +8 -0
  44. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  45. package/dist/components/AIChat/AIChat.js.map +1 -1
  46. package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
  47. package/dist/components/AIChat/AIChatComposer.js.map +1 -1
  48. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  49. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  50. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  51. package/dist/components/AppBar/AppBar.js.map +1 -1
  52. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  53. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
  54. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
  55. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
  56. package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
  57. package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
  58. package/dist/components/Button/Button.cjs.map +1 -1
  59. package/dist/components/Button/Button.js.map +1 -1
  60. package/dist/components/Calendar/Calendar.cjs.map +1 -1
  61. package/dist/components/Calendar/Calendar.js.map +1 -1
  62. package/dist/components/Carousel/Carousel.cjs.map +1 -1
  63. package/dist/components/Carousel/Carousel.js.map +1 -1
  64. package/dist/components/Chat/Chat.cjs.map +1 -1
  65. package/dist/components/Chat/Chat.js.map +1 -1
  66. package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
  67. package/dist/components/ChipInput/ChipInput.js.map +1 -1
  68. package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
  69. package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
  70. package/dist/components/Combobox/Combobox.cjs.map +1 -1
  71. package/dist/components/Combobox/Combobox.js.map +1 -1
  72. package/dist/components/Command/Command.cjs.map +1 -1
  73. package/dist/components/Command/Command.js.map +1 -1
  74. package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
  75. package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
  76. package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
  77. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  78. package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
  79. package/dist/components/CopyButton/CopyButton.js.map +1 -1
  80. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  81. package/dist/components/DataTable/DataTable.js.map +1 -1
  82. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  83. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  84. package/dist/components/DataTable/EditableCell.cjs.map +1 -1
  85. package/dist/components/DataTable/EditableCell.js.map +1 -1
  86. package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
  87. package/dist/components/DatePicker/DatePicker.js.map +1 -1
  88. package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
  89. package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
  90. package/dist/components/Drawer/Drawer.cjs.map +1 -1
  91. package/dist/components/Drawer/Drawer.js.map +1 -1
  92. package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
  93. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  94. package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
  95. package/dist/components/Dropzone/Dropzone.js.map +1 -1
  96. package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
  97. package/dist/components/FileUpload/FileUpload.js.map +1 -1
  98. package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
  99. package/dist/components/FilterBar/FilterBar.js.map +1 -1
  100. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
  101. package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
  102. package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
  103. package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
  104. package/dist/components/Kanban/Kanban.cjs.map +1 -1
  105. package/dist/components/Kanban/Kanban.js.map +1 -1
  106. package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
  107. package/dist/components/Lightbox/Lightbox.js.map +1 -1
  108. package/dist/components/ListTile/ListTile.cjs.map +1 -1
  109. package/dist/components/ListTile/ListTile.js.map +1 -1
  110. package/dist/components/Markdown/Markdown.cjs.map +1 -1
  111. package/dist/components/Markdown/Markdown.js.map +1 -1
  112. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  113. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  114. package/dist/components/Menubar/Menubar.cjs.map +1 -1
  115. package/dist/components/Menubar/Menubar.js.map +1 -1
  116. package/dist/components/Modal/Modal.cjs.map +1 -1
  117. package/dist/components/Modal/Modal.js.map +1 -1
  118. package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
  119. package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
  120. package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
  121. package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
  122. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  123. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  124. package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
  125. package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
  126. package/dist/components/Page/Page.cjs.map +1 -1
  127. package/dist/components/Page/Page.js.map +1 -1
  128. package/dist/components/Pagination/Pagination.cjs.map +1 -1
  129. package/dist/components/Pagination/Pagination.js.map +1 -1
  130. package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
  131. package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
  132. package/dist/components/PinInput/PinInput.cjs.map +1 -1
  133. package/dist/components/PinInput/PinInput.js.map +1 -1
  134. package/dist/components/Popover/Popover.cjs.map +1 -1
  135. package/dist/components/Popover/Popover.js.map +1 -1
  136. package/dist/components/Progress/Progress.cjs.map +1 -1
  137. package/dist/components/Progress/Progress.js.map +1 -1
  138. package/dist/components/QRCode/qr-encode.cjs.map +1 -1
  139. package/dist/components/QRCode/qr-encode.js.map +1 -1
  140. package/dist/components/Radio/Radio.cjs.map +1 -1
  141. package/dist/components/Radio/Radio.js.map +1 -1
  142. package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
  143. package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
  144. package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
  145. package/dist/components/RatingStars/RatingStars.js.map +1 -1
  146. package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
  147. package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
  148. package/dist/components/Resizable/Resizable.cjs.map +1 -1
  149. package/dist/components/Resizable/Resizable.js.map +1 -1
  150. package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
  151. package/dist/components/Scheduler/Scheduler.js.map +1 -1
  152. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  153. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  154. package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
  155. package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
  156. package/dist/components/Slider/Slider.cjs.map +1 -1
  157. package/dist/components/Slider/Slider.js.map +1 -1
  158. package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
  159. package/dist/components/Sparkline/Sparkline.js.map +1 -1
  160. package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
  161. package/dist/components/StepperInput/StepperInput.js.map +1 -1
  162. package/dist/components/Table/Table.cjs.map +1 -1
  163. package/dist/components/Table/Table.js.map +1 -1
  164. package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
  165. package/dist/components/TimePicker/TimePicker.js.map +1 -1
  166. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  167. package/dist/components/Toast/ToastProvider.js.map +1 -1
  168. package/dist/components/Tour/Tour.cjs.map +1 -1
  169. package/dist/components/Tour/Tour.js.map +1 -1
  170. package/dist/components/Transfer/Transfer.cjs.map +1 -1
  171. package/dist/components/Transfer/Transfer.js.map +1 -1
  172. package/dist/components/TreeView/TreeView.cjs.map +1 -1
  173. package/dist/components/TreeView/TreeView.js.map +1 -1
  174. package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
  175. package/dist/components/VirtualList/VirtualList.js.map +1 -1
  176. package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
  177. package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
  178. package/dist/components/Wizard/Wizard.cjs.map +1 -1
  179. package/dist/components/Wizard/Wizard.js.map +1 -1
  180. package/dist/editor/RichTextEditor.cjs.map +1 -1
  181. package/dist/editor/RichTextEditor.js.map +1 -1
  182. package/dist/forms/FormField.cjs.map +1 -1
  183. package/dist/forms/FormField.js.map +1 -1
  184. package/dist/geo/TrajectoryMap.cjs.map +1 -1
  185. package/dist/geo/TrajectoryMap.js.map +1 -1
  186. package/dist/geo/estimate.cjs.map +1 -1
  187. package/dist/geo/estimate.js.map +1 -1
  188. package/dist/geo/projection.cjs.map +1 -1
  189. package/dist/geo/projection.js.map +1 -1
  190. package/dist/hooks/use-event-listener.cjs.map +1 -1
  191. package/dist/hooks/use-event-listener.js.map +1 -1
  192. package/dist/hooks/use-local-storage.cjs.map +1 -1
  193. package/dist/hooks/use-local-storage.js.map +1 -1
  194. package/dist/hooks/use-sortable.cjs.map +1 -1
  195. package/dist/hooks/use-sortable.js.map +1 -1
  196. package/dist/http/api-client.cjs.map +1 -1
  197. package/dist/http/api-client.js.map +1 -1
  198. package/dist/http/errors.cjs.map +1 -1
  199. package/dist/http/errors.js.map +1 -1
  200. package/dist/http/resumable-upload.cjs.map +1 -1
  201. package/dist/http/resumable-upload.js.map +1 -1
  202. package/dist/http/upload-with-progress.cjs.map +1 -1
  203. package/dist/http/upload-with-progress.js.map +1 -1
  204. package/dist/i18n/I18nProvider.cjs.map +1 -1
  205. package/dist/i18n/I18nProvider.js.map +1 -1
  206. package/dist/imaging/canvas.cjs.map +1 -1
  207. package/dist/imaging/canvas.js.map +1 -1
  208. package/dist/imaging.d.ts +5 -0
  209. package/dist/logger/logger.cjs.map +1 -1
  210. package/dist/logger/logger.js.map +1 -1
  211. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  212. package/dist/oauth/GoogleSignIn.js.map +1 -1
  213. package/dist/offline/create-offline-sync.cjs.map +1 -1
  214. package/dist/offline/create-offline-sync.js.map +1 -1
  215. package/dist/perf/cache-size.cjs +1 -1
  216. package/dist/perf/cache-size.cjs.map +1 -1
  217. package/dist/perf/cache-size.js +27 -6
  218. package/dist/perf/cache-size.js.map +1 -1
  219. package/dist/sse/create-event-stream.cjs.map +1 -1
  220. package/dist/sse/create-event-stream.js.map +1 -1
  221. package/dist/styles.css +1 -1
  222. package/dist/sw/background-sync.cjs.map +1 -1
  223. package/dist/sw/background-sync.js.map +1 -1
  224. package/dist/sw/cache.cjs.map +1 -1
  225. package/dist/sw/cache.js.map +1 -1
  226. package/dist/sw.d.ts +12 -0
  227. package/dist/tabular/compact.cjs.map +1 -1
  228. package/dist/tabular/compact.js.map +1 -1
  229. package/dist/tabular/predictor.cjs.map +1 -1
  230. package/dist/tabular/predictor.js.map +1 -1
  231. package/dist/tabular.d.ts +7 -2
  232. package/dist/tempest-react-sdk.d.ts +133 -6
  233. package/dist/theme/ThemeProvider.cjs.map +1 -1
  234. package/dist/theme/ThemeProvider.js.map +1 -1
  235. package/dist/theme/color.cjs.map +1 -1
  236. package/dist/theme/color.js.map +1 -1
  237. package/dist/theme/create-theme.cjs.map +1 -1
  238. package/dist/theme/create-theme.js.map +1 -1
  239. package/dist/theme/data-viz-ramps.cjs +1 -1
  240. package/dist/theme/data-viz-ramps.cjs.map +1 -1
  241. package/dist/theme/data-viz-ramps.js +1 -1
  242. package/dist/theme/data-viz-ramps.js.map +1 -1
  243. package/dist/utils/storage.cjs.map +1 -1
  244. package/dist/utils/storage.js.map +1 -1
  245. package/dist/vision/core/canvas.cjs +1 -1
  246. package/dist/vision/core/canvas.cjs.map +1 -1
  247. package/dist/vision/core/canvas.js +4 -4
  248. package/dist/vision/core/canvas.js.map +1 -1
  249. package/dist/vision/core/exceptions.cjs +1 -1
  250. package/dist/vision/core/exceptions.cjs.map +1 -1
  251. package/dist/vision/core/exceptions.js +2 -2
  252. package/dist/vision/core/exceptions.js.map +1 -1
  253. package/dist/vision/core/graph.cjs.map +1 -1
  254. package/dist/vision/core/graph.js.map +1 -1
  255. package/dist/vision/core/metadata.cjs +2 -2
  256. package/dist/vision/core/metadata.cjs.map +1 -1
  257. package/dist/vision/core/metadata.js +10 -7
  258. package/dist/vision/core/metadata.js.map +1 -1
  259. package/dist/vision/core/providers.cjs.map +1 -1
  260. package/dist/vision/core/providers.js.map +1 -1
  261. package/dist/vision/core/session.cjs +1 -1
  262. package/dist/vision/core/session.cjs.map +1 -1
  263. package/dist/vision/core/session.js +8 -5
  264. package/dist/vision/core/session.js.map +1 -1
  265. package/dist/vision/core/timing.cjs.map +1 -1
  266. package/dist/vision/core/timing.js.map +1 -1
  267. package/dist/vision/fusion.cjs +2 -0
  268. package/dist/vision/fusion.cjs.map +1 -0
  269. package/dist/vision/fusion.js +42 -0
  270. package/dist/vision/fusion.js.map +1 -0
  271. package/dist/vision/index.cjs +1 -1
  272. package/dist/vision/index.cjs.map +1 -1
  273. package/dist/vision/index.js +23 -20
  274. package/dist/vision/index.js.map +1 -1
  275. package/dist/vision/io/image.cjs.map +1 -1
  276. package/dist/vision/io/image.js.map +1 -1
  277. package/dist/vision/labels.cjs +1 -1
  278. package/dist/vision/labels.cjs.map +1 -1
  279. package/dist/vision/labels.js +7 -4
  280. package/dist/vision/labels.js.map +1 -1
  281. package/dist/vision/luminance.cjs.map +1 -1
  282. package/dist/vision/luminance.js.map +1 -1
  283. package/dist/vision/postprocess/classification.cjs.map +1 -1
  284. package/dist/vision/postprocess/classification.js.map +1 -1
  285. package/dist/vision/postprocess/detection.cjs +1 -1
  286. package/dist/vision/postprocess/detection.cjs.map +1 -1
  287. package/dist/vision/postprocess/detection.js +3 -10
  288. package/dist/vision/postprocess/detection.js.map +1 -1
  289. package/dist/vision/postprocess/segmentation.cjs +1 -1
  290. package/dist/vision/postprocess/segmentation.cjs.map +1 -1
  291. package/dist/vision/postprocess/segmentation.js +11 -15
  292. package/dist/vision/postprocess/segmentation.js.map +1 -1
  293. package/dist/vision/preprocess/image.cjs.map +1 -1
  294. package/dist/vision/preprocess/image.js.map +1 -1
  295. package/dist/vision/preprocess/pipeline.cjs +2 -0
  296. package/dist/vision/preprocess/pipeline.cjs.map +1 -0
  297. package/dist/vision/preprocess/pipeline.js +61 -0
  298. package/dist/vision/preprocess/pipeline.js.map +1 -0
  299. package/dist/vision/results.cjs +1 -1
  300. package/dist/vision/results.cjs.map +1 -1
  301. package/dist/vision/results.js +23 -2
  302. package/dist/vision/results.js.map +1 -1
  303. package/dist/vision/tasks/base.cjs +1 -1
  304. package/dist/vision/tasks/base.cjs.map +1 -1
  305. package/dist/vision/tasks/base.js +9 -2
  306. package/dist/vision/tasks/base.js.map +1 -1
  307. package/dist/vision/tasks/classifier.cjs.map +1 -1
  308. package/dist/vision/tasks/classifier.js.map +1 -1
  309. package/dist/vision/tasks/detectClassify.cjs +2 -0
  310. package/dist/vision/tasks/detectClassify.cjs.map +1 -0
  311. package/dist/vision/tasks/detectClassify.js +221 -0
  312. package/dist/vision/tasks/detectClassify.js.map +1 -0
  313. package/dist/vision/tasks/detector.cjs +1 -1
  314. package/dist/vision/tasks/detector.cjs.map +1 -1
  315. package/dist/vision/tasks/detector.js +54 -29
  316. package/dist/vision/tasks/detector.js.map +1 -1
  317. package/dist/vision/tasks/segmenter.cjs +1 -1
  318. package/dist/vision/tasks/segmenter.cjs.map +1 -1
  319. package/dist/vision/tasks/segmenter.js +53 -28
  320. package/dist/vision/tasks/segmenter.js.map +1 -1
  321. package/dist/vision/types.cjs.map +1 -1
  322. package/dist/vision/types.js.map +1 -1
  323. package/dist/vision.cjs +1 -1
  324. package/dist/vision.d.ts +1517 -956
  325. package/dist/vision.js +25 -22
  326. package/dist/vite/tempest-icons.cjs.map +1 -1
  327. package/dist/vite/tempest-icons.js.map +1 -1
  328. package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
  329. package/dist/vite/tempest-pwa-icons.js.map +1 -1
  330. package/dist/ws/create-web-socket.cjs.map +1 -1
  331. package/dist/ws/create-web-socket.js.map +1 -1
  332. package/package.json +1 -1
package/README.md CHANGED
@@ -279,7 +279,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
279
279
  | `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
280
280
  | `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker` (opt-in `autoUpdate` poll + reload, no `vite-plugin-pwa`), `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener`, `installPrecache` (app-shell offline + Navigation Preload), `installRuntimeCache` (per-route caching, incl. `rangeRequests`), `createPartialResponse` (206 range slicing), `installBackgroundSync` (offline mutation queue + `periodicsync`), `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
281
281
  | `editor` _(subpath `tempest-react-sdk/editor`, opt. peers `@tiptap/react`+`@tiptap/starter-kit`)_ | `RichTextEditor` — themed tiptap v3 rich-text editor with toolbar (tiptap externalized; install it only if you use the editor) |
282
- | `vision` _(subpath `tempest-react-sdk/vision`, opt. peer `onnxruntime-web`)_ | `Classifier`, `Detector`, `Segmenter` + results/labels/preprocess — on-device ONNX vision inference, vendored from `@mauriciobenjamin700/ort-vision-sdk-web` (onnxruntime-web externalized; install it only if you use vision). Plus browser capture primitives: `useCameraStream` (rear-camera `getUserMedia` + classified errors + `retry()`), `computeImageLuminance`/`isLuminanceAcceptable`/`LowLuminanceError`/`LUMINANCE_SAMPLE_MAX_EDGE` (BT.709 brightness), `useLiveLuminance` (live rAF sampling) |
282
+ | `vision` _(subpath `tempest-react-sdk/vision`, opt. peer `onnxruntime-web`)_ | `Classifier`, `Detector`, `Segmenter`, `DetectClassify` (fused detect→classify pipeline) + `warmup()`, `raiseOnEmpty`/`NoDetectionsError`, `LetterboxPipeline`, `readFusionSpec` and results/labels/preprocess — on-device ONNX vision inference, vendored from `@mauriciobenjamin700/ort-vision-sdk-web` (onnxruntime-web externalized; install it only if you use vision). Plus browser capture primitives: `useCameraStream` (rear-camera `getUserMedia` + classified errors + `retry()`), `computeImageLuminance`/`isLuminanceAcceptable`/`LowLuminanceError`/`LUMINANCE_SAMPLE_MAX_EDGE` (BT.709 brightness), `useLiveLuminance` (live rAF sampling) |
283
283
  | `tabular` _(subpath `tempest-react-sdk/tabular`, opt. peer `onnxruntime-web`)_ | `TabularPredictor`, `useTabularPredictor` — scikit-learn models exported by `tempest-fastapi-sdk`'s `export_sklearn_to_onnx`, running in the browser with no inference server. Offline by design: `fetchModelBytes`/`isModelCached`/`cacheModelBytes`/`clearModelCache` (Cache Storage, cache-first) plus `configureOrtAssets`/`ortAssetUrls`/`ORT_WASM_ASSETS` for the runtime's WebAssembly binaries, which ONNX Runtime Web fetches at session creation. Import the default `onnxruntime-web` entry — the WebGPU build has no `ai.onnx.ml` kernels |
284
284
  | `imaging` _(subpath `tempest-react-sdk/imaging`, no deps)_ | `resizeImage`, `cropImage`, `rotateImage`, `flipImage`, `compressToTarget` (binary-searches quality into a byte budget), `createThumbnails` (several sizes from one decode), `decodeImage`/`readImageInfo`, `encodeImage`, `supportsImageType`/`bestSupportedType`, plus `useImagePreview`/`useImageProcessing`. Canvas only — decodes with EXIF orientation applied (phone photos arrive upright), reports the format actually produced (asking for AVIF where there is no encoder silently returns PNG), and re-encoding drops EXIF, which is the privacy step as much as the bandwidth one |
285
285
  | `audio` | `createAudioPlayer`, `playAudio`, `stopAudio`, `useAudio` |
@@ -37,6 +37,25 @@ export function isExcluded(path) {
37
37
  );
38
38
  }
39
39
 
40
+ /** How far into a file the generated banner is looked for. */
41
+ const BANNER_BYTES = 800;
42
+
43
+ /**
44
+ * True when the file says, in its own head, that it is not hand-written.
45
+ *
46
+ * A generated or vendored file is upstream's shape, not this project's: the fix
47
+ * for a 300-line function in it is a change in the generator or in the package it
48
+ * was copied from, and any edit made here is undone by the next regeneration.
49
+ * Reporting it teaches the reader to skim the report, which is how a linter dies.
50
+ *
51
+ * @param {string} source - File contents.
52
+ * @returns {boolean}
53
+ */
54
+ export function declaresGenerated(source) {
55
+ const head = source.slice(0, BANNER_BYTES);
56
+ return /@generated\b/.test(head) || /\bDo not hand-edit\b/i.test(head);
57
+ }
58
+
40
59
  /**
41
60
  * Collect the project's own source files.
42
61
  *
@@ -174,6 +174,11 @@ export function findPropsTypes(masked) {
174
174
  const re = /\b(?:interface|type)\s+([A-Za-z_$][\w$]*Props)\b([^{;]*)/g;
175
175
  let match;
176
176
  while ((match = re.exec(masked))) {
177
+ // The body has to be this declaration's own. `type OverriddenDomProps =
178
+ // "children" | "onSubmit";` ends at the semicolon, and searching forward for a
179
+ // brace found the *next* interface — so a two-member string union was reported
180
+ // with the member count of whatever came after it.
181
+ if (!/^\s*\{/.test(masked.slice(match.index + match[0].length))) continue;
177
182
  const open = masked.indexOf("{", match.index + match[0].length - 1);
178
183
  if (open === -1) continue;
179
184
  const close = matchPair(masked, open, "{", "}");
@@ -5,7 +5,7 @@
5
5
  import { readFileSync } from "node:fs";
6
6
  import { relative } from "node:path";
7
7
 
8
- import { collectSources } from "./collect.mjs";
8
+ import { collectSources, declaresGenerated } from "./collect.mjs";
9
9
  import { countBySeverity, LIMITS, sortFindings } from "./findings.mjs";
10
10
  import { scanFile } from "./scan.mjs";
11
11
 
@@ -42,6 +42,7 @@ export function analyzeDesign({ root, targets, limits = {}, maxFiles } = {}) {
42
42
  const findings = [];
43
43
  const waivers = [];
44
44
  const lineCounts = [];
45
+ const skipped = [...collected.skipped];
45
46
  let largest = null;
46
47
  let codeLines = 0;
47
48
 
@@ -53,6 +54,10 @@ export function analyzeDesign({ root, targets, limits = {}, maxFiles } = {}) {
53
54
  } catch {
54
55
  continue;
55
56
  }
57
+ if (declaresGenerated(source)) {
58
+ skipped.push({ path, reason: "declares itself generated or vendored" });
59
+ continue;
60
+ }
56
61
  const result = scanFile({ file, source, limits });
57
62
  findings.push(...result.findings);
58
63
  waivers.push(...result.waivers.map((w) => ({ file, ...w })));
@@ -69,12 +74,12 @@ export function analyzeDesign({ root, targets, limits = {}, maxFiles } = {}) {
69
74
  waivers,
70
75
  counts: countBySeverity(sorted),
71
76
  stats: {
72
- files: collected.files.length,
77
+ files: collected.files.length - (skipped.length - collected.skipped.length),
73
78
  codeLines,
74
79
  medianLines: median(lineCounts),
75
80
  largest,
76
81
  },
77
- skipped: collected.skipped.map((s) => ({ file: relative(root, s.path), reason: s.reason })),
82
+ skipped: skipped.map((s) => ({ file: relative(root, s.path), reason: s.reason })),
78
83
  truncated: collected.truncated,
79
84
  };
80
85
  }
@@ -21,6 +21,37 @@ afterEach(() => {
21
21
  rmSync(root, { recursive: true, force: true });
22
22
  });
23
23
 
24
+ describe("analyzeDesign — generated and vendored code", () => {
25
+ it("skips a file that declares itself generated", () => {
26
+ write(
27
+ "src/vendored/detector.ts",
28
+ [
29
+ "/** @generated Vendored from upstream. Do not hand-edit. */",
30
+ "export function detect() {",
31
+ ...Array.from({ length: 300 }, (_, i) => ` const v${i} = ${i};`),
32
+ " return 1;",
33
+ "}",
34
+ ].join("\n"),
35
+ );
36
+ const result = analyzeDesign({ root });
37
+ expect(result.findings).toHaveLength(0);
38
+ expect(result.skipped.map((s) => s.file)).toContain("src/vendored/detector.ts");
39
+ });
40
+
41
+ it("still judges the same file once the banner is gone", () => {
42
+ write(
43
+ "src/owned/detector.ts",
44
+ [
45
+ "export function detect() {",
46
+ ...Array.from({ length: 300 }, (_, i) => ` const v${i} = ${i};`),
47
+ " return 1;",
48
+ "}",
49
+ ].join("\n"),
50
+ );
51
+ expect(analyzeDesign({ root }).findings.length).toBeGreaterThan(0);
52
+ });
53
+ });
54
+
24
55
  describe("analyzeDesign", () => {
25
56
  it("reports nothing for a well-shaped project", () => {
26
57
  write(
@@ -36,6 +36,30 @@ function isTypePosition(masked, index) {
36
36
  return /[:<|&(,[]\s*$|\bas\s+$/.test(before);
37
37
  }
38
38
 
39
+ /**
40
+ * True when the file only re-exports: a barrel.
41
+ *
42
+ * The file-lines limit measures how much a reader has to hold in their head before
43
+ * changing a line, and a barrel asks for none of that — a 350-line index of
44
+ * `export { X } from "./x"` is a directory listing, and the advice to "extract a
45
+ * sub-component" is meaningless there. Judging it also punishes exactly the layout
46
+ * this SDK asks projects to use: one re-export per public symbol.
47
+ *
48
+ * @param {string} masked - Source with comments and string bodies blanked.
49
+ * @returns {boolean}
50
+ */
51
+ function isBarrel(masked) {
52
+ const lines = masked
53
+ .split("\n")
54
+ .map((line) => line.trim())
55
+ .filter(Boolean);
56
+ if (lines.length === 0) return false;
57
+ const reexport = lines.filter((line) =>
58
+ /^(export|import)\b|^[\w$,{}*\s]+$|^} from |^};?$|^from /.test(line),
59
+ ).length;
60
+ return reexport / lines.length >= 0.95;
61
+ }
62
+
39
63
  /**
40
64
  * Analyze one already-read source file.
41
65
  *
@@ -70,7 +94,7 @@ export function scanFile({ file, source, limits = {} }) {
70
94
  }
71
95
 
72
96
  const codeLines = countCodeLines(masked);
73
- if (!isTest) {
97
+ if (!isTest && !isBarrel(masked)) {
74
98
  const fileMax = isTsx ? max.componentFileLines : max.moduleFileLines;
75
99
  if (codeLines > fileMax) {
76
100
  push(
@@ -114,7 +138,15 @@ export function scanFile({ file, source, limits = {} }) {
114
138
  `${fn.name} takes ${fn.params.length} parameters (limit ${max.params}) — pass one named object`,
115
139
  );
116
140
  }
117
- if (fn.kind === "component" && fn.destructuredProps > max.props) {
141
+ // The destructuring is only news when it disagrees with the type: a
142
+ // component that spreads its own `<Name>Props` reports the same excess
143
+ // twice, and two lines for one fact reads as two problems.
144
+ const declared = propsTypes.get(`${fn.name}Props`)?.count;
145
+ if (
146
+ fn.kind === "component" &&
147
+ fn.destructuredProps > max.props &&
148
+ fn.destructuredProps !== declared
149
+ ) {
118
150
  push(
119
151
  "props-count",
120
152
  fn.line,
@@ -12,6 +12,84 @@ const bigComponent = (lines) =>
12
12
  "}",
13
13
  ].join("\n");
14
14
 
15
+ describe("scanFile — what is not a props type", () => {
16
+ it("does not read a string union named *Props as a props type", () => {
17
+ const source = [
18
+ 'type OverriddenDomProps = "children" | "onSubmit";',
19
+ "",
20
+ "export interface CardProps {",
21
+ ...Array.from({ length: 12 }, (_, i) => ` prop${i}?: string;`),
22
+ "}",
23
+ "",
24
+ "export function Card(props: CardProps) {",
25
+ " return props;",
26
+ "}",
27
+ ].join("\n");
28
+ const result = scanFile({ file: "src/Card.tsx", source });
29
+ const propsFindings = result.findings.filter((f) => f.code === "props-count");
30
+ expect(propsFindings).toHaveLength(1);
31
+ expect(propsFindings[0].message).toContain("CardProps");
32
+ expect(propsFindings[0].message).not.toContain("OverriddenDomProps");
33
+ });
34
+
35
+ it("reports the type once, not again for the destructuring that mirrors it", () => {
36
+ const props = Array.from({ length: 9 }, (_, i) => `prop${i}`);
37
+ const source = [
38
+ "export interface PanelProps {",
39
+ ...props.map((p) => ` ${p}?: string;`),
40
+ "}",
41
+ "",
42
+ `export function Panel({ ${props.join(", ")} }: PanelProps) {`,
43
+ ` return ${props[0]};`,
44
+ "}",
45
+ ].join("\n");
46
+ const propsFindings = scanFile({ file: "src/Panel.tsx", source }).findings.filter(
47
+ (f) => f.code === "props-count",
48
+ );
49
+ expect(propsFindings).toHaveLength(1);
50
+ expect(propsFindings[0].message).toContain("PanelProps has 9 props");
51
+ });
52
+
53
+ it("still reports a destructuring that goes past its own type", () => {
54
+ const source = [
55
+ "export interface WideProps {",
56
+ " a?: string;",
57
+ "}",
58
+ "",
59
+ "export function Wide({ a, b, c, d, e, f, g, h, i }: WideProps & Extra) {",
60
+ " return a;",
61
+ "}",
62
+ ].join("\n");
63
+ const messages = scanFile({ file: "src/Wide.tsx", source })
64
+ .findings.filter((f) => f.code === "props-count")
65
+ .map((f) => f.message);
66
+ expect(messages.some((m) => m.includes("destructures 9 props"))).toBe(true);
67
+ });
68
+ });
69
+
70
+ describe("scanFile — barrels", () => {
71
+ it("does not size-check a file that only re-exports", () => {
72
+ const source = Array.from(
73
+ { length: 260 },
74
+ (_, i) => `export { Thing${i}, type Thing${i}Props } from "./thing-${i}";`,
75
+ ).join("\n");
76
+ expect(codes(scanFile({ file: "src/components/index.ts", source }))).not.toContain(
77
+ "file-lines",
78
+ );
79
+ });
80
+
81
+ it("still size-checks a file that re-exports and also holds logic", () => {
82
+ const source = [
83
+ ...Array.from({ length: 200 }, (_, i) => `export { T${i} } from "./t-${i}";`),
84
+ "export function helper() {",
85
+ ...Array.from({ length: 40 }, (_, i) => ` const v${i} = ${i};`),
86
+ " return 1;",
87
+ "}",
88
+ ].join("\n");
89
+ expect(codes(scanFile({ file: "src/mixed.ts", source }))).toContain("file-lines");
90
+ });
91
+ });
92
+
15
93
  describe("scanFile — size", () => {
16
94
  it("reports a .tsx over the component limit", () => {
17
95
  const result = scanFile({ file: "src/Big.tsx", source: bigComponent(200) });
@@ -277,6 +277,65 @@ describe("tempest doctor — a project that does use the SDK", () => {
277
277
  expect(doctor().out).not.toContain("Adopting the SDK (optional)");
278
278
  });
279
279
 
280
+ it("does not count a styles.css mentioned in a docstring as a second import", () => {
281
+ write("src/main.tsx", 'import "tempest-react-sdk/styles.css";\nexport const main = 1;');
282
+ write(
283
+ "src/theme.ts",
284
+ '/**\n * Theming notes.\n *\n * The app imports "tempest-react-sdk/styles.css" once, in the entry.\n */\nexport const theme = 1;',
285
+ );
286
+ const { out } = doctor();
287
+ expect(out).not.toContain("styles.css imported");
288
+ });
289
+
290
+ it("still catches a styles.css imported twice for real", () => {
291
+ write("src/main.tsx", 'import "tempest-react-sdk/styles.css";\nexport const main = 1;');
292
+ write("src/late.ts", 'import "tempest-react-sdk/styles.css";\nexport const late = 1;');
293
+ expect(doctor().out).toContain("styles.css imported 2×");
294
+ });
295
+
296
+ it("keeps quiet about an optional peer the project never uses", () => {
297
+ write("package.json", {
298
+ name: "sdk-lib",
299
+ type: "module",
300
+ dependencies: { "tempest-react-sdk": "^0.26.1" },
301
+ peerDependencies: { react: "^19.0.0", leaflet: ">=1.9.0" },
302
+ peerDependenciesMeta: { leaflet: { optional: true } },
303
+ });
304
+ const { out } = doctor();
305
+ expect(out).not.toContain("peerDependency(ies) unmet");
306
+ });
307
+
308
+ it("still reports a required peer that is not installed", () => {
309
+ write("package.json", {
310
+ name: "sdk-lib",
311
+ type: "module",
312
+ dependencies: { "tempest-react-sdk": "^0.26.1" },
313
+ peerDependencies: { react: "^19.0.0", leaflet: ">=1.9.0" },
314
+ });
315
+ const { out } = doctor();
316
+ expect(out).toContain("peerDependency(ies) unmet");
317
+ expect(out).toContain("leaflet");
318
+ });
319
+
320
+ it("does not demand an optional peer a test only renders to prove it degrades", () => {
321
+ write(
322
+ "src/TileMap.test.tsx",
323
+ 'import { TrajectoryMap } from "tempest-react-sdk";\n' +
324
+ 'export const t = <TrajectoryMap points={[]} tileUrl="https://t/{z}/{x}/{y}.png" />;',
325
+ );
326
+ const { out } = doctor();
327
+ expect(out).not.toContain("leaflet missing");
328
+ });
329
+
330
+ it("still demands the optional peer when production code uses it", () => {
331
+ write(
332
+ "src/TileMap.tsx",
333
+ 'import { TrajectoryMap } from "tempest-react-sdk";\n' +
334
+ 'export const T = () => <TrajectoryMap points={[]} tileUrl="https://t/{z}/{x}/{y}.png" />;',
335
+ );
336
+ expect(doctor().out).toContain("leaflet missing");
337
+ });
338
+
280
339
  it("warns when the SDK is installed but undeclared", () => {
281
340
  write("package.json", {
282
341
  name: "sdk-app",
@@ -289,6 +348,32 @@ describe("tempest doctor — a project that does use the SDK", () => {
289
348
  });
290
349
  });
291
350
 
351
+ describe("tempest doctor — the summary count", () => {
352
+ beforeEach(thirdPartyApp);
353
+
354
+ it("counts design findings the list had to cap", () => {
355
+ for (let i = 0; i < 12; i += 1) {
356
+ write(
357
+ `src/Comp${i}.tsx`,
358
+ [
359
+ `export function Comp${i}() {`,
360
+ ...Array.from({ length: 210 }, (_, j) => ` const v${j} = ${j};`),
361
+ " return null;",
362
+ "}",
363
+ ].join("\n"),
364
+ );
365
+ }
366
+ const { out } = doctor();
367
+ const hidden = /(\d+) more design finding\(s\) not shown/.exec(out);
368
+ const summary = /! (\d+) warning\(s\)/.exec(out);
369
+ expect(hidden).not.toBeNull();
370
+ expect(summary).not.toBeNull();
371
+ // The summary has to be at least the findings that were hidden plus the ones
372
+ // printed; before this it counted only the printed rows.
373
+ expect(Number(summary[1])).toBeGreaterThan(Number(hidden[1]));
374
+ });
375
+ });
376
+
292
377
  describe("tempest doctor — design analysis", () => {
293
378
  beforeEach(thirdPartyApp);
294
379
 
package/bin/tempest.mjs CHANGED
@@ -111,9 +111,65 @@ function nestedDupe(name) {
111
111
  return existsSync(join(ROOT, "node_modules", "tempest-react-sdk", "node_modules", name));
112
112
  }
113
113
 
114
+ /**
115
+ * Drop comment bodies, keeping string contents and line count intact.
116
+ *
117
+ * Every usage check below asks "does this project do X?" of raw source text, and
118
+ * a JSDoc `@example` showing X is not the project doing X — a docstring telling
119
+ * you to `import "tempest-react-sdk/styles.css"`, or one rendering a
120
+ * `<TrajectoryMap tileUrl=…>`, used to be read as a second import and as a
121
+ * leaflet dependency. Strings survive because the checks match import paths,
122
+ * which live inside them; the design rules use `maskSource` instead, which blanks
123
+ * both.
124
+ *
125
+ * @param {string} source - Raw file contents.
126
+ * @returns {string} The same text with `//` and block comments blanked.
127
+ */
128
+ function stripComments(source) {
129
+ let out = "";
130
+ let i = 0;
131
+ while (i < source.length) {
132
+ const ch = source[i];
133
+ const next = source[i + 1];
134
+ if (ch === "/" && next === "/") {
135
+ const end = source.indexOf("\n", i);
136
+ i = end === -1 ? source.length : end;
137
+ continue;
138
+ }
139
+ if (ch === "/" && next === "*") {
140
+ const end = source.indexOf("*/", i + 2);
141
+ const stop = end === -1 ? source.length : end + 2;
142
+ out += source.slice(i, stop).replace(/[^\n]/g, " ");
143
+ i = stop;
144
+ continue;
145
+ }
146
+ if (ch === '"' || ch === "'" || ch === "`") {
147
+ const quote = ch;
148
+ let j = i + 1;
149
+ while (j < source.length && source[j] !== quote) {
150
+ if (source[j] === "\\") j += 1;
151
+ if (source[j] === "\n" && quote !== "`") break;
152
+ j += 1;
153
+ }
154
+ out += source.slice(i, Math.min(j + 1, source.length));
155
+ i = j + 1;
156
+ continue;
157
+ }
158
+ out += ch;
159
+ i += 1;
160
+ }
161
+ return out;
162
+ }
163
+
114
164
  /**
115
165
  * Recursively collect source text (bounded) to scan for import usage, and note
116
166
  * whether any test file exists. Returns `{ text, hasTests }`.
167
+ *
168
+ * The text comes back with comments stripped (see {@link stripComments}), since
169
+ * every caller is asking what the code does, not what its docs say. Test files
170
+ * are counted (`hasTests`) but kept out of it for the same reason: a test that
171
+ * renders a component precisely to assert how it degrades **without** an optional
172
+ * peer would otherwise be read as a project needing that peer installed.
117
173
  */
118
174
  function scanSources() {
119
175
  const roots = ["src", "app"].map((d) => join(ROOT, d)).filter(existsSync);
@@ -136,9 +192,12 @@ function scanSources() {
136
192
  if (e.isDirectory()) walk(full);
137
193
  else if (exts.has(e.name.slice(e.name.lastIndexOf(".")))) {
138
194
  budget -= 1;
139
- if (/\.(test|spec)\./.test(e.name)) hasTests = true;
195
+ if (/\.(test|spec)\./.test(e.name)) {
196
+ hasTests = true;
197
+ continue;
198
+ }
140
199
  try {
141
- text += readFileSync(full, "utf8");
200
+ text += stripComments(readFileSync(full, "utf8"));
142
201
  } catch {
143
202
  /* ignore */
144
203
  }
@@ -372,6 +431,13 @@ function designChecks(limit = 6) {
372
431
  "the limits and the escape hatch are documented at /design/limits/",
373
432
  ]);
374
433
  }
434
+ // Carried to the summary so a capped list still reports its real size. Never
435
+ // printed: the reporter skips any row whose severity it does not know.
436
+ const hiddenWarns = Math.max(
437
+ 0,
438
+ analysis.findings.filter((f) => f.severity === "warn").length - limit,
439
+ );
440
+ if (hiddenWarns > 0) rows.push(["hidden-warns", hiddenWarns]);
375
441
  if (analysis.findings.length === 0) {
376
442
  rows.push(["ok", "no design problems found"]);
377
443
  }
@@ -552,8 +618,14 @@ function doctor(args = []) {
552
618
  checks.push(["ok", "all declared dependencies installed"]);
553
619
  }
554
620
 
555
- // App's own peerDependencies unmet.
556
- const peerDeps = Object.keys(pkg.peerDependencies ?? {});
621
+ // App's own peerDependencies unmet. A peer flagged `optional` in
622
+ // peerDependenciesMeta is not missing when absent — that is the whole point of
623
+ // declaring it optional, and reporting it turns every published package into a
624
+ // permanent warning about peers its consumers were never meant to install.
625
+ const peerMeta = pkg.peerDependenciesMeta ?? {};
626
+ const peerDeps = Object.keys(pkg.peerDependencies ?? {}).filter(
627
+ (name) => peerMeta[name]?.optional !== true,
628
+ );
557
629
  const unmetPeers = peerDeps.filter((name) => !installedVersion(name));
558
630
  if (unmetPeers.length > 0) {
559
631
  checks.push([
@@ -728,7 +800,12 @@ function doctor(args = []) {
728
800
  }
729
801
  }
730
802
  // styles.css should be imported once — duplicates re-inject the whole sheet.
731
- const stylesImports = (src.match(/tempest-react-sdk\/styles\.css/g) ?? []).length;
803
+ // Only an actual import statement counts: the path also shows up in prose (a
804
+ // JSDoc telling you to import it, a comment explaining what the sheet covers),
805
+ // and counting those reports a duplicate that does not exist.
806
+ const stylesImports = (
807
+ src.match(/(?:^|[\s;])import\s+["']tempest-react-sdk\/styles\.css["']/g) ?? []
808
+ ).length;
732
809
  if (stylesImports > 1) {
733
810
  checks.push([
734
811
  "warn",
@@ -846,6 +923,7 @@ function report(checks) {
846
923
  console.log(`\n${c.bold}${entry[1]}${c.reset}`);
847
924
  continue;
848
925
  }
926
+ if (entry[0] === "hidden-warns") continue;
849
927
  if (entry[0] === "info") {
850
928
  console.log(
851
929
  ` [${c.cyan}i${c.reset}] ${entry[1]}${entry[2] ? ` ${c.dim}— ${entry[2]}${c.reset}` : ""}`,
@@ -855,7 +933,13 @@ function report(checks) {
855
933
  console.log(fmt(entry[0], entry[1], entry[2]));
856
934
  }
857
935
  const fails = checks.filter((x) => x[0] === "fail").length;
858
- const warns = checks.filter((x) => x[0] === "warn").length;
936
+ // The design section prints at most `limit` findings per severity, so counting the
937
+ // printed rows under-reports it — a project with 245 design warnings used to close
938
+ // with "6 warning(s)", and a number that small is a number nobody acts on.
939
+ const hiddenWarns = checks
940
+ .filter((x) => x[0] === "hidden-warns")
941
+ .reduce((sum, x) => sum + x[1], 0);
942
+ const warns = checks.filter((x) => x[0] === "warn").length + hiddenWarns;
859
943
  console.log("");
860
944
  if (fails)
861
945
  console.log(
@@ -1 +1 @@
1
- {"version":3,"file":"level-meter.cjs","names":[],"sources":["../../src/audio/level-meter.ts"],"sourcesContent":["/** Options for {@link createLevelMeter}. */\nexport interface LevelMeterOptions {\n /**\n * FFT window. 1024 is a good default: ~21 ms at 48 kHz, long enough to be stable\n * and short enough to react to a syllable.\n */\n fftSize?: number;\n /**\n * Extra smoothing on the way down, 0–1. Default `0.7`.\n *\n * A raw RMS reading is jittery enough to make a meter look broken. Attack is left\n * instant — a meter that lags the voice going *up* reads as \"not recording\" —\n * while the decay is eased, which is how every hardware meter behaves.\n */\n decay?: number;\n}\n\n/** A running level meter. */\nexport interface LevelMeter {\n /** Latest level, 0–1. Read it on your own frame loop. */\n level: () => number;\n /** Stop sampling and release the Web Audio graph. */\n stop: () => void;\n}\n\n/**\n * Sample the loudness of a live stream, 0–1.\n *\n * This exists because a recorder with no visible level is indistinguishable from a\n * broken one. A muted OS input, a headset whose mic arm is switched off, the wrong\n * device selected — all three produce a perfectly successful recording of silence,\n * and the user only finds out after they finish talking.\n *\n * The value is RMS, not peak: peak reacts to a single sample and flickers, RMS tracks\n * perceived loudness. It is deliberately **not** React state — updating state per\n * frame would re-render the tree 60 times a second. Poll `level()` from a\n * `requestAnimationFrame` loop and write to the DOM, or read it inside an existing\n * animation.\n *\n * The `AudioContext` is created here and closed by `stop()`. Browsers cap the number\n * of live contexts (Chrome allows around six), so a meter left running on unmount\n * eventually breaks every later one on the page.\n *\n * @param stream - A live audio stream.\n * @param options - See {@link LevelMeterOptions}.\n * @returns A `level()` reader and a `stop()`.\n *\n * @example\n * const meter = createLevelMeter(stream);\n * const tick = () => { bar.style.transform = `scaleX(${meter.level()})`; raf = requestAnimationFrame(tick); };\n */\nexport function createLevelMeter(\n stream: MediaStream,\n { fftSize = 1024, decay = 0.7 }: LevelMeterOptions = {},\n): LevelMeter {\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!Ctor) {\n return { level: () => 0, stop: () => undefined };\n }\n\n const context = new Ctor();\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = fftSize;\n source.connect(analyser);\n\n const samples = new Float32Array(analyser.fftSize);\n let smoothed = 0;\n let closed = false;\n\n const read = (): number => {\n if (closed) return 0;\n analyser.getFloatTimeDomainData(samples);\n let sum = 0;\n for (let index = 0; index < samples.length; index += 1) {\n sum += samples[index] * samples[index];\n }\n const rms = Math.sqrt(sum / samples.length);\n // Instant attack, eased release.\n smoothed = rms > smoothed ? rms : smoothed * decay + rms * (1 - decay);\n return Math.min(1, smoothed);\n };\n\n return {\n level: read,\n stop: (): void => {\n if (closed) return;\n closed = true;\n try {\n source.disconnect();\n analyser.disconnect();\n } catch {\n // Already torn down with the context — nothing to release.\n }\n void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":"AAmDA,SAAgB,EACZ,EACA,CAAE,UAAU,KAAM,QAAQ,IAA2B,CAAC,EAC5C,CACV,IAAM,EACF,OAAO,aAAiB,IAClB,aACC,WAA4D,mBAEvE,GAAI,CAAC,EACD,MAAO,CAAE,UAAa,EAAG,SAAY,IAAA,EAAU,EAGnD,IAAM,EAAU,IAAI,EACd,EAAS,EAAQ,wBAAwB,CAAM,EAC/C,EAAW,EAAQ,eAAe,EACxC,EAAS,QAAU,EACnB,EAAO,QAAQ,CAAQ,EAEvB,IAAM,EAAU,IAAI,aAAa,EAAS,OAAO,EAC7C,EAAW,EACX,EAAS,GAeb,MAAO,CACH,UAduB,CACvB,GAAI,EAAQ,MAAO,GACnB,EAAS,uBAAuB,CAAO,EACvC,IAAI,EAAM,EACV,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,OAAQ,GAAS,EACjD,GAAO,EAAQ,GAAS,EAAQ,GAEpC,IAAM,EAAM,KAAK,KAAK,EAAM,EAAQ,MAAM,EAG1C,MADA,GAAW,EAAM,EAAW,EAAM,EAAW,EAAQ,GAAO,EAAI,GACzD,KAAK,IAAI,EAAG,CAAQ,CAC/B,EAII,SAAkB,CACV,MACJ,GAAS,GACT,GAAI,CACA,EAAO,WAAW,EAClB,EAAS,WAAW,CACxB,MAAQ,CAER,CACA,EAAa,MAAM,CAAC,CAAC,UAAY,IAAA,EAAS,CAPjC,CAQb,CACJ,CACJ"}
1
+ {"version":3,"file":"level-meter.cjs","names":[],"sources":["../../src/audio/level-meter.ts"],"sourcesContent":["/** Options for {@link createLevelMeter}. */\nexport interface LevelMeterOptions {\n /**\n * FFT window. 1024 is a good default: ~21 ms at 48 kHz, long enough to be stable\n * and short enough to react to a syllable.\n */\n fftSize?: number;\n /**\n * Extra smoothing on the way down, 0–1. Default `0.7`.\n *\n * A raw RMS reading is jittery enough to make a meter look broken. Attack is left\n * instant — a meter that lags the voice going *up* reads as \"not recording\" —\n * while the decay is eased, which is how every hardware meter behaves.\n */\n decay?: number;\n}\n\n/** A running level meter. */\nexport interface LevelMeter {\n /** Latest level, 0–1. Read it on your own frame loop. */\n level: () => number;\n /** Stop sampling and release the Web Audio graph. */\n stop: () => void;\n}\n\n/**\n * Sample the loudness of a live stream, 0–1.\n *\n * This exists because a recorder with no visible level is indistinguishable from a\n * broken one. A muted OS input, a headset whose mic arm is switched off, the wrong\n * device selected — all three produce a perfectly successful recording of silence,\n * and the user only finds out after they finish talking.\n *\n * The value is RMS, not peak: peak reacts to a single sample and flickers, RMS tracks\n * perceived loudness. It is deliberately **not** React state — updating state per\n * frame would re-render the tree 60 times a second. Poll `level()` from a\n * `requestAnimationFrame` loop and write to the DOM, or read it inside an existing\n * animation.\n *\n * The `AudioContext` is created here and closed by `stop()`. Browsers cap the number\n * of live contexts (Chrome allows around six), so a meter left running on unmount\n * eventually breaks every later one on the page.\n *\n * `level()` applies instant attack and eased release: a value above the current one\n * is taken as is, a lower one decays by `decay`. A meter that fell as fast as it rose\n * reads as noise instead of as loudness.\n *\n * @tempest-limits empty-catch — `stop()` disconnects nodes whose context may already\n * be closed (the tab was backgrounded, the stream's track ended, `stop()` raced an\n * unmount), and a disconnect on a dead graph throws. There is nothing to report and\n * nothing to retry: the resource this call would have released is already gone, and\n * the `context.close()` right after it is what actually matters.\n *\n * @param stream - A live audio stream.\n * @param options - See {@link LevelMeterOptions}.\n * @returns A `level()` reader and a `stop()`.\n *\n * @example\n * const meter = createLevelMeter(stream);\n * const tick = () => { bar.style.transform = `scaleX(${meter.level()})`; raf = requestAnimationFrame(tick); };\n */\nexport function createLevelMeter(\n stream: MediaStream,\n { fftSize = 1024, decay = 0.7 }: LevelMeterOptions = {},\n): LevelMeter {\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!Ctor) {\n return { level: () => 0, stop: () => undefined };\n }\n\n const context = new Ctor();\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = fftSize;\n source.connect(analyser);\n\n const samples = new Float32Array(analyser.fftSize);\n let smoothed = 0;\n let closed = false;\n\n const read = (): number => {\n if (closed) return 0;\n analyser.getFloatTimeDomainData(samples);\n let sum = 0;\n for (let index = 0; index < samples.length; index += 1) {\n sum += samples[index] * samples[index];\n }\n const rms = Math.sqrt(sum / samples.length);\n smoothed = rms > smoothed ? rms : smoothed * decay + rms * (1 - decay);\n return Math.min(1, smoothed);\n };\n\n return {\n level: read,\n stop: (): void => {\n if (closed) return;\n closed = true;\n try {\n source.disconnect();\n analyser.disconnect();\n } catch {\n /* empty */\n }\n void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":"AA6DA,SAAgB,EACZ,EACA,CAAE,UAAU,KAAM,QAAQ,IAA2B,CAAC,EAC5C,CACV,IAAM,EACF,OAAO,aAAiB,IAClB,aACC,WAA4D,mBAEvE,GAAI,CAAC,EACD,MAAO,CAAE,UAAa,EAAG,SAAY,IAAA,EAAU,EAGnD,IAAM,EAAU,IAAI,EACd,EAAS,EAAQ,wBAAwB,CAAM,EAC/C,EAAW,EAAQ,eAAe,EACxC,EAAS,QAAU,EACnB,EAAO,QAAQ,CAAQ,EAEvB,IAAM,EAAU,IAAI,aAAa,EAAS,OAAO,EAC7C,EAAW,EACX,EAAS,GAcb,MAAO,CACH,UAbuB,CACvB,GAAI,EAAQ,MAAO,GACnB,EAAS,uBAAuB,CAAO,EACvC,IAAI,EAAM,EACV,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,OAAQ,GAAS,EACjD,GAAO,EAAQ,GAAS,EAAQ,GAEpC,IAAM,EAAM,KAAK,KAAK,EAAM,EAAQ,MAAM,EAE1C,MADA,GAAW,EAAM,EAAW,EAAM,EAAW,EAAQ,GAAO,EAAI,GACzD,KAAK,IAAI,EAAG,CAAQ,CAC/B,EAII,SAAkB,CACV,MACJ,GAAS,GACT,GAAI,CACA,EAAO,WAAW,EAClB,EAAS,WAAW,CACxB,MAAQ,CAER,CACA,EAAa,MAAM,CAAC,CAAC,UAAY,IAAA,EAAS,CAPjC,CAQb,CACJ,CACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"level-meter.js","names":[],"sources":["../../src/audio/level-meter.ts"],"sourcesContent":["/** Options for {@link createLevelMeter}. */\nexport interface LevelMeterOptions {\n /**\n * FFT window. 1024 is a good default: ~21 ms at 48 kHz, long enough to be stable\n * and short enough to react to a syllable.\n */\n fftSize?: number;\n /**\n * Extra smoothing on the way down, 0–1. Default `0.7`.\n *\n * A raw RMS reading is jittery enough to make a meter look broken. Attack is left\n * instant — a meter that lags the voice going *up* reads as \"not recording\" —\n * while the decay is eased, which is how every hardware meter behaves.\n */\n decay?: number;\n}\n\n/** A running level meter. */\nexport interface LevelMeter {\n /** Latest level, 0–1. Read it on your own frame loop. */\n level: () => number;\n /** Stop sampling and release the Web Audio graph. */\n stop: () => void;\n}\n\n/**\n * Sample the loudness of a live stream, 0–1.\n *\n * This exists because a recorder with no visible level is indistinguishable from a\n * broken one. A muted OS input, a headset whose mic arm is switched off, the wrong\n * device selected — all three produce a perfectly successful recording of silence,\n * and the user only finds out after they finish talking.\n *\n * The value is RMS, not peak: peak reacts to a single sample and flickers, RMS tracks\n * perceived loudness. It is deliberately **not** React state — updating state per\n * frame would re-render the tree 60 times a second. Poll `level()` from a\n * `requestAnimationFrame` loop and write to the DOM, or read it inside an existing\n * animation.\n *\n * The `AudioContext` is created here and closed by `stop()`. Browsers cap the number\n * of live contexts (Chrome allows around six), so a meter left running on unmount\n * eventually breaks every later one on the page.\n *\n * @param stream - A live audio stream.\n * @param options - See {@link LevelMeterOptions}.\n * @returns A `level()` reader and a `stop()`.\n *\n * @example\n * const meter = createLevelMeter(stream);\n * const tick = () => { bar.style.transform = `scaleX(${meter.level()})`; raf = requestAnimationFrame(tick); };\n */\nexport function createLevelMeter(\n stream: MediaStream,\n { fftSize = 1024, decay = 0.7 }: LevelMeterOptions = {},\n): LevelMeter {\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!Ctor) {\n return { level: () => 0, stop: () => undefined };\n }\n\n const context = new Ctor();\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = fftSize;\n source.connect(analyser);\n\n const samples = new Float32Array(analyser.fftSize);\n let smoothed = 0;\n let closed = false;\n\n const read = (): number => {\n if (closed) return 0;\n analyser.getFloatTimeDomainData(samples);\n let sum = 0;\n for (let index = 0; index < samples.length; index += 1) {\n sum += samples[index] * samples[index];\n }\n const rms = Math.sqrt(sum / samples.length);\n // Instant attack, eased release.\n smoothed = rms > smoothed ? rms : smoothed * decay + rms * (1 - decay);\n return Math.min(1, smoothed);\n };\n\n return {\n level: read,\n stop: (): void => {\n if (closed) return;\n closed = true;\n try {\n source.disconnect();\n analyser.disconnect();\n } catch {\n // Already torn down with the context — nothing to release.\n }\n void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":";AAmDA,SAAgB,EACZ,GACA,EAAE,aAAU,MAAM,WAAQ,OAA2B,CAAC,GAC5C;CACV,IAAM,IACF,OAAO,eAAiB,MAClB,eACC,WAA4D;CAEvE,IAAI,CAAC,GACD,OAAO;EAAE,aAAa;EAAG,YAAY,KAAA;CAAU;CAGnD,IAAM,IAAU,IAAI,EAAK,GACnB,IAAS,EAAQ,wBAAwB,CAAM,GAC/C,IAAW,EAAQ,eAAe;CAExC,AADA,EAAS,UAAU,GACnB,EAAO,QAAQ,CAAQ;CAEvB,IAAM,IAAU,IAAI,aAAa,EAAS,OAAO,GAC7C,IAAW,GACX,IAAS;CAeb,OAAO;EACH,aAduB;GACvB,IAAI,GAAQ,OAAO;GACnB,EAAS,uBAAuB,CAAO;GACvC,IAAI,IAAM;GACV,KAAK,IAAI,IAAQ,GAAG,IAAQ,EAAQ,QAAQ,KAAS,GACjD,KAAO,EAAQ,KAAS,EAAQ;GAEpC,IAAM,IAAM,KAAK,KAAK,IAAM,EAAQ,MAAM;GAG1C,OADA,IAAW,IAAM,IAAW,IAAM,IAAW,IAAQ,KAAO,IAAI,IACzD,KAAK,IAAI,GAAG,CAAQ;EAC/B;EAII,YAAkB;GACV,QACJ;QAAS;IACT,IAAI;KAEA,AADA,EAAO,WAAW,GAClB,EAAS,WAAW;IACxB,QAAQ,CAER;IACA,EAAa,MAAM,CAAC,CAAC,YAAY,KAAA,CAAS;GAPjC;EAQb;CACJ;AACJ"}
1
+ {"version":3,"file":"level-meter.js","names":[],"sources":["../../src/audio/level-meter.ts"],"sourcesContent":["/** Options for {@link createLevelMeter}. */\nexport interface LevelMeterOptions {\n /**\n * FFT window. 1024 is a good default: ~21 ms at 48 kHz, long enough to be stable\n * and short enough to react to a syllable.\n */\n fftSize?: number;\n /**\n * Extra smoothing on the way down, 0–1. Default `0.7`.\n *\n * A raw RMS reading is jittery enough to make a meter look broken. Attack is left\n * instant — a meter that lags the voice going *up* reads as \"not recording\" —\n * while the decay is eased, which is how every hardware meter behaves.\n */\n decay?: number;\n}\n\n/** A running level meter. */\nexport interface LevelMeter {\n /** Latest level, 0–1. Read it on your own frame loop. */\n level: () => number;\n /** Stop sampling and release the Web Audio graph. */\n stop: () => void;\n}\n\n/**\n * Sample the loudness of a live stream, 0–1.\n *\n * This exists because a recorder with no visible level is indistinguishable from a\n * broken one. A muted OS input, a headset whose mic arm is switched off, the wrong\n * device selected — all three produce a perfectly successful recording of silence,\n * and the user only finds out after they finish talking.\n *\n * The value is RMS, not peak: peak reacts to a single sample and flickers, RMS tracks\n * perceived loudness. It is deliberately **not** React state — updating state per\n * frame would re-render the tree 60 times a second. Poll `level()` from a\n * `requestAnimationFrame` loop and write to the DOM, or read it inside an existing\n * animation.\n *\n * The `AudioContext` is created here and closed by `stop()`. Browsers cap the number\n * of live contexts (Chrome allows around six), so a meter left running on unmount\n * eventually breaks every later one on the page.\n *\n * `level()` applies instant attack and eased release: a value above the current one\n * is taken as is, a lower one decays by `decay`. A meter that fell as fast as it rose\n * reads as noise instead of as loudness.\n *\n * @tempest-limits empty-catch — `stop()` disconnects nodes whose context may already\n * be closed (the tab was backgrounded, the stream's track ended, `stop()` raced an\n * unmount), and a disconnect on a dead graph throws. There is nothing to report and\n * nothing to retry: the resource this call would have released is already gone, and\n * the `context.close()` right after it is what actually matters.\n *\n * @param stream - A live audio stream.\n * @param options - See {@link LevelMeterOptions}.\n * @returns A `level()` reader and a `stop()`.\n *\n * @example\n * const meter = createLevelMeter(stream);\n * const tick = () => { bar.style.transform = `scaleX(${meter.level()})`; raf = requestAnimationFrame(tick); };\n */\nexport function createLevelMeter(\n stream: MediaStream,\n { fftSize = 1024, decay = 0.7 }: LevelMeterOptions = {},\n): LevelMeter {\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!Ctor) {\n return { level: () => 0, stop: () => undefined };\n }\n\n const context = new Ctor();\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = fftSize;\n source.connect(analyser);\n\n const samples = new Float32Array(analyser.fftSize);\n let smoothed = 0;\n let closed = false;\n\n const read = (): number => {\n if (closed) return 0;\n analyser.getFloatTimeDomainData(samples);\n let sum = 0;\n for (let index = 0; index < samples.length; index += 1) {\n sum += samples[index] * samples[index];\n }\n const rms = Math.sqrt(sum / samples.length);\n smoothed = rms > smoothed ? rms : smoothed * decay + rms * (1 - decay);\n return Math.min(1, smoothed);\n };\n\n return {\n level: read,\n stop: (): void => {\n if (closed) return;\n closed = true;\n try {\n source.disconnect();\n analyser.disconnect();\n } catch {\n /* empty */\n }\n void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":";AA6DA,SAAgB,EACZ,GACA,EAAE,aAAU,MAAM,WAAQ,OAA2B,CAAC,GAC5C;CACV,IAAM,IACF,OAAO,eAAiB,MAClB,eACC,WAA4D;CAEvE,IAAI,CAAC,GACD,OAAO;EAAE,aAAa;EAAG,YAAY,KAAA;CAAU;CAGnD,IAAM,IAAU,IAAI,EAAK,GACnB,IAAS,EAAQ,wBAAwB,CAAM,GAC/C,IAAW,EAAQ,eAAe;CAExC,AADA,EAAS,UAAU,GACnB,EAAO,QAAQ,CAAQ;CAEvB,IAAM,IAAU,IAAI,aAAa,EAAS,OAAO,GAC7C,IAAW,GACX,IAAS;CAcb,OAAO;EACH,aAbuB;GACvB,IAAI,GAAQ,OAAO;GACnB,EAAS,uBAAuB,CAAO;GACvC,IAAI,IAAM;GACV,KAAK,IAAI,IAAQ,GAAG,IAAQ,EAAQ,QAAQ,KAAS,GACjD,KAAO,EAAQ,KAAS,EAAQ;GAEpC,IAAM,IAAM,KAAK,KAAK,IAAM,EAAQ,MAAM;GAE1C,OADA,IAAW,IAAM,IAAW,IAAM,IAAW,IAAQ,KAAO,IAAI,IACzD,KAAK,IAAI,GAAG,CAAQ;EAC/B;EAII,YAAkB;GACV,QACJ;QAAS;IACT,IAAI;KAEA,AADA,EAAO,WAAW,GAClB,EAAS,WAAW;IACxB,QAAQ,CAER;IACA,EAAa,MAAM,CAAC,CAAC,YAAY,KAAA,CAAS;GAPjC;EAQb;CACJ;AACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-audio-recorder.cjs","names":[],"sources":["../../src/audio/use-audio-recorder.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport {\n createAudioRecorder,\n type AudioRecorderHandle,\n type AudioRecorderOptions,\n type AudioRecorderStatus,\n type AudioRecording,\n} from \"./audio-recorder\";\nimport { createLevelMeter, type LevelMeter } from \"./level-meter\";\n\n/** Options for {@link useAudioRecorder}. */\nexport interface UseAudioRecorderOptions extends AudioRecorderOptions {\n /**\n * Stop automatically after this many milliseconds.\n *\n * Worth setting on anything user-facing: a recording left running by accident is\n * a memory buffer that grows until the tab dies.\n */\n maxDurationMs?: number;\n /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n onRecorded?: (recording: AudioRecording) => void;\n /** How often the clock and the level are published, in ms. Default 100. */\n tickMs?: number;\n /** Skip the level meter — it costs an `AudioContext`. Default `false`. */\n disableLevelMeter?: boolean;\n}\n\n/** Value returned by {@link useAudioRecorder}. */\nexport interface UseAudioRecorderResult {\n status: AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: number;\n /** Loudness 0–1, or `0` when the meter is off or nothing is recording. */\n level: number;\n /** The finished recording, or `null` before the first `stop()`. */\n recording: AudioRecording | null;\n /** Recorder-level error (encoder failure, device pulled mid-recording). */\n error: unknown;\n /** `false` when there is no stream yet, or the browser cannot record. */\n ready: boolean;\n start: () => void;\n pause: () => void;\n resume: () => void;\n /** Stop and resolve with the recording. Also published on `recording`. */\n stop: () => Promise<AudioRecording | null>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n}\n\n/**\n * Record the given stream, with a clock, a level meter and a state machine.\n *\n * Pass the stream from {@link useMicrophone}; the hook stays `ready: false` until\n * there is one, so a page can render the whole recorder UI before permission is\n * granted and simply have it disabled.\n *\n * The clock and the level are published on an interval (`tickMs`, default 100 ms)\n * rather than per animation frame. A hook that set state 60 times a second would\n * re-render everything the recorder is inside; 10 Hz is smooth enough for a timer and\n * a meter, and the underlying `createLevelMeter` is still available for anyone who\n * wants a frame-accurate bar written straight to the DOM.\n *\n * @param stream - The stream to record, or `null` while permission is pending.\n * @param options - See {@link UseAudioRecorderOptions}.\n *\n * @example\n * const mic = useMicrophone();\n * const rec = useAudioRecorder(mic.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar</button>\n */\nexport function useAudioRecorder(\n stream: MediaStream | null,\n options: UseAudioRecorderOptions = {},\n): UseAudioRecorderResult {\n const {\n maxDurationMs,\n onRecorded,\n onChunk,\n onError,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n tickMs = 100,\n disableLevelMeter = false,\n } = options;\n\n const [status, setStatus] = useState<AudioRecorderStatus>(\"idle\");\n const [durationMs, setDurationMs] = useState(0);\n const [level, setLevel] = useState(0);\n const [recording, setRecording] = useState<AudioRecording | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [ready, setReady] = useState(false);\n\n const recorderRef = useRef<AudioRecorderHandle | null>(null);\n const meterRef = useRef<LevelMeter | null>(null);\n\n const emitRecorded = useStableCallback((result: AudioRecording) => onRecorded?.(result));\n const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n const emitError = useStableCallback((err: unknown) => {\n setError(err);\n onError?.(err);\n });\n\n /**\n * Build the recorder and the meter for a stream, and tear both down when it\n * changes.\n *\n * The recorder is rebuilt per stream because `MediaRecorder` is bound to the\n * stream it was constructed with — swapping microphones mid-session hands us a\n * new `MediaStream`, and reusing the old recorder would keep recording the device\n * the user just switched away from. It is rebuilt on a container or bitrate change\n * for the same reason: both are constructor arguments.\n */\n useEffect(() => {\n if (!stream) {\n setReady(false);\n return;\n }\n let recorder: AudioRecorderHandle;\n try {\n recorder = createAudioRecorder(stream, {\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n onChunk: emitChunk,\n onError: emitError,\n });\n } catch (err) {\n setError(err);\n setReady(false);\n return;\n }\n recorderRef.current = recorder;\n meterRef.current = disableLevelMeter ? null : createLevelMeter(stream);\n setReady(true);\n setStatus(\"idle\");\n\n return () => {\n if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n recorder.cancel();\n }\n meterRef.current?.stop();\n meterRef.current = null;\n recorderRef.current = null;\n setReady(false);\n };\n }, [\n stream,\n disableLevelMeter,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n emitChunk,\n emitError,\n ]);\n\n const stop = useCallback(async (): Promise<AudioRecording | null> => {\n const recorder = recorderRef.current;\n if (!recorder) return null;\n const current = recorder.status();\n if (current !== \"recording\" && current !== \"paused\") return null;\n const result = await recorder.stop();\n setStatus(\"stopped\");\n setDurationMs(result.durationMs);\n setLevel(0);\n setRecording(result);\n emitRecorded(result);\n return result;\n }, [emitRecorded]);\n\n /**\n * Publish the clock and the level while something is running, and enforce\n * `maxDurationMs`.\n *\n * The cap is checked on the same tick rather than with a `setTimeout` armed at\n * `start()`, because a paused recording must not keep counting toward it — a\n * timeout would fire while the user is paused mid-sentence.\n */\n useEffect(() => {\n if (status !== \"recording\" && status !== \"paused\") {\n setLevel(0);\n return;\n }\n const id = setInterval(() => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n const elapsed = recorder.durationMs();\n setDurationMs(elapsed);\n setLevel(status === \"recording\" ? (meterRef.current?.level() ?? 0) : 0);\n if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n }, tickMs);\n return () => clearInterval(id);\n }, [status, tickMs, maxDurationMs, stop]);\n\n const start = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n setRecording(null);\n setError(null);\n setDurationMs(0);\n recorder.start();\n setStatus(recorder.status());\n }, []);\n\n const pause = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.pause();\n setDurationMs(recorder.durationMs());\n setStatus(recorder.status());\n }, []);\n\n const resume = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.resume();\n setStatus(recorder.status());\n }, []);\n\n const cancel = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.cancel();\n setStatus(\"idle\");\n setDurationMs(0);\n setLevel(0);\n setRecording(null);\n }, []);\n\n return {\n status,\n durationMs,\n level,\n recording,\n error,\n ready,\n start,\n pause,\n resume,\n stop,\n cancel,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-audio-recorder.cjs","names":[],"sources":["../../src/audio/use-audio-recorder.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the hook is the recorder's lifecycle seen from React:\n * permission, the recorder handle, the clock, the level meter and the auto-stop\n * timer, all of which have to be torn down together on unmount. A second hook would\n * own half the teardown.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport {\n createAudioRecorder,\n type AudioRecorderHandle,\n type AudioRecorderOptions,\n type AudioRecorderStatus,\n type AudioRecording,\n} from \"./audio-recorder\";\nimport { createLevelMeter, type LevelMeter } from \"./level-meter\";\n\n/** Options for {@link useAudioRecorder}. */\nexport interface UseAudioRecorderOptions extends AudioRecorderOptions {\n /**\n * Stop automatically after this many milliseconds.\n *\n * Worth setting on anything user-facing: a recording left running by accident is\n * a memory buffer that grows until the tab dies.\n */\n maxDurationMs?: number;\n /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n onRecorded?: (recording: AudioRecording) => void;\n /** How often the clock and the level are published, in ms. Default 100. */\n tickMs?: number;\n /** Skip the level meter — it costs an `AudioContext`. Default `false`. */\n disableLevelMeter?: boolean;\n}\n\n/** Value returned by {@link useAudioRecorder}. */\nexport interface UseAudioRecorderResult {\n status: AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: number;\n /** Loudness 0–1, or `0` when the meter is off or nothing is recording. */\n level: number;\n /** The finished recording, or `null` before the first `stop()`. */\n recording: AudioRecording | null;\n /** Recorder-level error (encoder failure, device pulled mid-recording). */\n error: unknown;\n /** `false` when there is no stream yet, or the browser cannot record. */\n ready: boolean;\n start: () => void;\n pause: () => void;\n resume: () => void;\n /** Stop and resolve with the recording. Also published on `recording`. */\n stop: () => Promise<AudioRecording | null>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n}\n\n/**\n * Record the given stream, with a clock, a level meter and a state machine.\n *\n * Pass the stream from {@link useMicrophone}; the hook stays `ready: false` until\n * there is one, so a page can render the whole recorder UI before permission is\n * granted and simply have it disabled.\n *\n * The clock and the level are published on an interval (`tickMs`, default 100 ms)\n * rather than per animation frame. A hook that set state 60 times a second would\n * re-render everything the recorder is inside; 10 Hz is smooth enough for a timer and\n * a meter, and the underlying `createLevelMeter` is still available for anyone who\n * wants a frame-accurate bar written straight to the DOM.\n *\n * @param stream - The stream to record, or `null` while permission is pending.\n * @param options - See {@link UseAudioRecorderOptions}.\n *\n * @example\n * const mic = useMicrophone();\n * const rec = useAudioRecorder(mic.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar</button>\n */\nexport function useAudioRecorder(\n stream: MediaStream | null,\n options: UseAudioRecorderOptions = {},\n): UseAudioRecorderResult {\n const {\n maxDurationMs,\n onRecorded,\n onChunk,\n onError,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n tickMs = 100,\n disableLevelMeter = false,\n } = options;\n\n const [status, setStatus] = useState<AudioRecorderStatus>(\"idle\");\n const [durationMs, setDurationMs] = useState(0);\n const [level, setLevel] = useState(0);\n const [recording, setRecording] = useState<AudioRecording | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [ready, setReady] = useState(false);\n\n const recorderRef = useRef<AudioRecorderHandle | null>(null);\n const meterRef = useRef<LevelMeter | null>(null);\n\n const emitRecorded = useStableCallback((result: AudioRecording) => onRecorded?.(result));\n const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n const emitError = useStableCallback((err: unknown) => {\n setError(err);\n onError?.(err);\n });\n\n /**\n * Build the recorder and the meter for a stream, and tear both down when it\n * changes.\n *\n * The recorder is rebuilt per stream because `MediaRecorder` is bound to the\n * stream it was constructed with — swapping microphones mid-session hands us a\n * new `MediaStream`, and reusing the old recorder would keep recording the device\n * the user just switched away from. It is rebuilt on a container or bitrate change\n * for the same reason: both are constructor arguments.\n */\n useEffect(() => {\n if (!stream) {\n setReady(false);\n return;\n }\n let recorder: AudioRecorderHandle;\n try {\n recorder = createAudioRecorder(stream, {\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n onChunk: emitChunk,\n onError: emitError,\n });\n } catch (err) {\n setError(err);\n setReady(false);\n return;\n }\n recorderRef.current = recorder;\n meterRef.current = disableLevelMeter ? null : createLevelMeter(stream);\n setReady(true);\n setStatus(\"idle\");\n\n return () => {\n if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n recorder.cancel();\n }\n meterRef.current?.stop();\n meterRef.current = null;\n recorderRef.current = null;\n setReady(false);\n };\n }, [\n stream,\n disableLevelMeter,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n emitChunk,\n emitError,\n ]);\n\n const stop = useCallback(async (): Promise<AudioRecording | null> => {\n const recorder = recorderRef.current;\n if (!recorder) return null;\n const current = recorder.status();\n if (current !== \"recording\" && current !== \"paused\") return null;\n const result = await recorder.stop();\n setStatus(\"stopped\");\n setDurationMs(result.durationMs);\n setLevel(0);\n setRecording(result);\n emitRecorded(result);\n return result;\n }, [emitRecorded]);\n\n /**\n * Publish the clock and the level while something is running, and enforce\n * `maxDurationMs`.\n *\n * The cap is checked on the same tick rather than with a `setTimeout` armed at\n * `start()`, because a paused recording must not keep counting toward it — a\n * timeout would fire while the user is paused mid-sentence.\n */\n useEffect(() => {\n if (status !== \"recording\" && status !== \"paused\") {\n setLevel(0);\n return;\n }\n const id = setInterval(() => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n const elapsed = recorder.durationMs();\n setDurationMs(elapsed);\n setLevel(status === \"recording\" ? (meterRef.current?.level() ?? 0) : 0);\n if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n }, tickMs);\n return () => clearInterval(id);\n }, [status, tickMs, maxDurationMs, stop]);\n\n const start = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n setRecording(null);\n setError(null);\n setDurationMs(0);\n recorder.start();\n setStatus(recorder.status());\n }, []);\n\n const pause = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.pause();\n setDurationMs(recorder.durationMs());\n setStatus(recorder.status());\n }, []);\n\n const resume = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.resume();\n setStatus(recorder.status());\n }, []);\n\n const cancel = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.cancel();\n setStatus(\"idle\");\n setDurationMs(0);\n setLevel(0);\n setRecording(null);\n }, []);\n\n return {\n status,\n durationMs,\n level,\n recording,\n error,\n ready,\n start,\n pause,\n resume,\n stop,\n cancel,\n };\n}\n"],"mappings":"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"}