tempest-react-sdk 0.35.0 → 0.37.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 (235) hide show
  1. package/README.md +72 -70
  2. package/dist/audio/audio-output.cjs +2 -0
  3. package/dist/audio/audio-output.cjs.map +1 -0
  4. package/dist/audio/audio-output.js +17 -0
  5. package/dist/audio/audio-output.js.map +1 -0
  6. package/dist/audio/audio-player.cjs +1 -1
  7. package/dist/audio/audio-player.cjs.map +1 -1
  8. package/dist/audio/audio-player.js +21 -20
  9. package/dist/audio/audio-player.js.map +1 -1
  10. package/dist/audio/audio-recorder.cjs +2 -0
  11. package/dist/audio/audio-recorder.cjs.map +1 -0
  12. package/dist/audio/audio-recorder.js +30 -0
  13. package/dist/audio/audio-recorder.js.map +1 -0
  14. package/dist/audio/duration.cjs +2 -0
  15. package/dist/audio/duration.cjs.map +1 -0
  16. package/dist/audio/duration.js +10 -0
  17. package/dist/audio/duration.js.map +1 -0
  18. package/dist/audio/level-meter.cjs +2 -0
  19. package/dist/audio/level-meter.cjs.map +1 -0
  20. package/dist/audio/level-meter.js +34 -0
  21. package/dist/audio/level-meter.js.map +1 -0
  22. package/dist/audio/media-access.cjs +2 -0
  23. package/dist/audio/media-access.cjs.map +1 -0
  24. package/dist/audio/media-access.js +51 -0
  25. package/dist/audio/media-access.js.map +1 -0
  26. package/dist/audio/use-audio-recorder.cjs +2 -0
  27. package/dist/audio/use-audio-recorder.cjs.map +1 -0
  28. package/dist/audio/use-audio-recorder.js +94 -0
  29. package/dist/audio/use-audio-recorder.js.map +1 -0
  30. package/dist/audio/use-audio.cjs.map +1 -1
  31. package/dist/audio/use-audio.js.map +1 -1
  32. package/dist/audio/use-microphone.cjs +2 -0
  33. package/dist/audio/use-microphone.cjs.map +1 -0
  34. package/dist/audio/use-microphone.js +54 -0
  35. package/dist/audio/use-microphone.js.map +1 -0
  36. package/dist/audio/wav.cjs +2 -0
  37. package/dist/audio/wav.cjs.map +1 -0
  38. package/dist/audio/wav.js +49 -0
  39. package/dist/audio/wav.js.map +1 -0
  40. package/dist/auth/passkey.cjs +2 -0
  41. package/dist/auth/passkey.cjs.map +1 -0
  42. package/dist/auth/passkey.js +171 -0
  43. package/dist/auth/passkey.js.map +1 -0
  44. package/dist/auth/use-passkey.cjs +2 -0
  45. package/dist/auth/use-passkey.cjs.map +1 -0
  46. package/dist/auth/use-passkey.js +120 -0
  47. package/dist/auth/use-passkey.js.map +1 -0
  48. package/dist/br/PixQRCode.cjs +2 -0
  49. package/dist/br/PixQRCode.cjs.map +1 -0
  50. package/dist/br/PixQRCode.js +70 -0
  51. package/dist/br/PixQRCode.js.map +1 -0
  52. package/dist/br/PixQRCode.module.cjs +2 -0
  53. package/dist/br/PixQRCode.module.cjs.map +1 -0
  54. package/dist/br/PixQRCode.module.js +15 -0
  55. package/dist/br/PixQRCode.module.js.map +1 -0
  56. package/dist/br/boleto.cjs +2 -0
  57. package/dist/br/boleto.cjs.map +1 -0
  58. package/dist/br/boleto.js +225 -0
  59. package/dist/br/boleto.js.map +1 -0
  60. package/dist/br/holidays.cjs +2 -0
  61. package/dist/br/holidays.cjs.map +1 -0
  62. package/dist/br/holidays.js +148 -0
  63. package/dist/br/holidays.js.map +1 -0
  64. package/dist/br/nfe.cjs +2 -0
  65. package/dist/br/nfe.cjs.map +1 -0
  66. package/dist/br/nfe.js +104 -0
  67. package/dist/br/nfe.js.map +1 -0
  68. package/dist/br/pix.cjs +2 -0
  69. package/dist/br/pix.cjs.map +1 -0
  70. package/dist/br/pix.js +148 -0
  71. package/dist/br/pix.js.map +1 -0
  72. package/dist/br.cjs +1 -1
  73. package/dist/br.cjs.map +1 -1
  74. package/dist/br.d.ts +878 -0
  75. package/dist/br.js +7 -2
  76. package/dist/br.js.map +1 -1
  77. package/dist/capture/barcode.cjs +2 -0
  78. package/dist/capture/barcode.cjs.map +1 -0
  79. package/dist/capture/barcode.js +59 -0
  80. package/dist/capture/barcode.js.map +1 -0
  81. package/dist/capture/media-recorder.cjs +2 -0
  82. package/dist/capture/media-recorder.cjs.map +1 -0
  83. package/dist/capture/media-recorder.js +64 -0
  84. package/dist/capture/media-recorder.js.map +1 -0
  85. package/dist/capture/use-barcode-scanner.cjs +2 -0
  86. package/dist/capture/use-barcode-scanner.cjs.map +1 -0
  87. package/dist/capture/use-barcode-scanner.js +86 -0
  88. package/dist/capture/use-barcode-scanner.js.map +1 -0
  89. package/dist/capture/use-screen-capture.cjs +2 -0
  90. package/dist/capture/use-screen-capture.cjs.map +1 -0
  91. package/dist/capture/use-screen-capture.js +79 -0
  92. package/dist/capture/use-screen-capture.js.map +1 -0
  93. package/dist/capture/use-speech-recognition.cjs +2 -0
  94. package/dist/capture/use-speech-recognition.cjs.map +1 -0
  95. package/dist/capture/use-speech-recognition.js +118 -0
  96. package/dist/capture/use-speech-recognition.js.map +1 -0
  97. package/dist/capture/use-torch.cjs +2 -0
  98. package/dist/capture/use-torch.cjs.map +1 -0
  99. package/dist/capture/use-torch.js +33 -0
  100. package/dist/capture/use-torch.js.map +1 -0
  101. package/dist/capture/use-video-recorder.cjs +2 -0
  102. package/dist/capture/use-video-recorder.cjs.map +1 -0
  103. package/dist/capture/use-video-recorder.js +90 -0
  104. package/dist/capture/use-video-recorder.js.map +1 -0
  105. package/dist/capture/video-recorder.cjs +2 -0
  106. package/dist/capture/video-recorder.cjs.map +1 -0
  107. package/dist/capture/video-recorder.js +31 -0
  108. package/dist/capture/video-recorder.js.map +1 -0
  109. package/dist/components/AIChat/AIChat.cjs +1 -1
  110. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  111. package/dist/components/AIChat/AIChat.js +93 -97
  112. package/dist/components/AIChat/AIChat.js.map +1 -1
  113. package/dist/components/AIChat/AIChatComposer.cjs +1 -1
  114. package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
  115. package/dist/components/AIChat/AIChatComposer.js +2 -1
  116. package/dist/components/AIChat/AIChatComposer.js.map +1 -1
  117. package/dist/components/AIChat/AIChatTurn.cjs +2 -2
  118. package/dist/components/AIChat/AIChatTurn.js +6 -6
  119. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  120. package/dist/components/Alert/Alert.module.cjs.map +1 -1
  121. package/dist/components/Alert/Alert.module.js.map +1 -1
  122. package/dist/components/AudioPlayer/AudioPlayer.cjs +2 -0
  123. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -0
  124. package/dist/components/AudioPlayer/AudioPlayer.js +123 -0
  125. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -0
  126. package/dist/components/AudioPlayer/AudioPlayer.module.cjs +2 -0
  127. package/dist/components/AudioPlayer/AudioPlayer.module.cjs.map +1 -0
  128. package/dist/components/AudioPlayer/AudioPlayer.module.js +12 -0
  129. package/dist/components/AudioPlayer/AudioPlayer.module.js.map +1 -0
  130. package/dist/components/AudioRecorder/AudioRecorder.cjs +2 -0
  131. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -0
  132. package/dist/components/AudioRecorder/AudioRecorder.js +179 -0
  133. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -0
  134. package/dist/components/AudioRecorder/AudioRecorder.module.cjs +2 -0
  135. package/dist/components/AudioRecorder/AudioRecorder.module.cjs.map +1 -0
  136. package/dist/components/AudioRecorder/AudioRecorder.module.js +19 -0
  137. package/dist/components/AudioRecorder/AudioRecorder.module.js.map +1 -0
  138. package/dist/components/Badge/Badge.module.cjs.map +1 -1
  139. package/dist/components/Badge/Badge.module.js.map +1 -1
  140. package/dist/components/BarcodeScanner/BarcodeScanner.cjs +2 -0
  141. package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -0
  142. package/dist/components/BarcodeScanner/BarcodeScanner.js +123 -0
  143. package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -0
  144. package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs +2 -0
  145. package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs.map +1 -0
  146. package/dist/components/BarcodeScanner/BarcodeScanner.module.js +22 -0
  147. package/dist/components/BarcodeScanner/BarcodeScanner.module.js.map +1 -0
  148. package/dist/components/BottomNavigation/BottomNavigation.module.cjs.map +1 -1
  149. package/dist/components/BottomNavigation/BottomNavigation.module.js.map +1 -1
  150. package/dist/components/Button/Button.module.cjs.map +1 -1
  151. package/dist/components/Button/Button.module.js.map +1 -1
  152. package/dist/components/Combobox/Combobox.module.cjs.map +1 -1
  153. package/dist/components/Combobox/Combobox.module.js.map +1 -1
  154. package/dist/components/DataTable/DataTable.cjs +1 -1
  155. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  156. package/dist/components/DataTable/DataTable.js +203 -66
  157. package/dist/components/DataTable/DataTable.js.map +1 -1
  158. package/dist/components/DataTable/DataTable.module.cjs +1 -1
  159. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  160. package/dist/components/DataTable/DataTable.module.js +9 -3
  161. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  162. package/dist/components/DataTable/EditableCell.cjs +2 -0
  163. package/dist/components/DataTable/EditableCell.cjs.map +1 -0
  164. package/dist/components/DataTable/EditableCell.js +77 -0
  165. package/dist/components/DataTable/EditableCell.js.map +1 -0
  166. package/dist/components/DataTable/edit-labels.cjs +2 -0
  167. package/dist/components/DataTable/edit-labels.cjs.map +1 -0
  168. package/dist/components/DataTable/edit-labels.js +11 -0
  169. package/dist/components/DataTable/edit-labels.js.map +1 -0
  170. package/dist/components/DateRangePicker/DateRangePicker.module.cjs.map +1 -1
  171. package/dist/components/DateRangePicker/DateRangePicker.module.js.map +1 -1
  172. package/dist/components/MultiSelect/MultiSelect.module.cjs.map +1 -1
  173. package/dist/components/MultiSelect/MultiSelect.module.js.map +1 -1
  174. package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
  175. package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
  176. package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs +1 -1
  177. package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs.map +1 -1
  178. package/dist/components/SyncStatusBadge/SyncStatusBadge.js +33 -32
  179. package/dist/components/SyncStatusBadge/SyncStatusBadge.js.map +1 -1
  180. package/dist/components/Toast/ToastProvider.cjs +1 -1
  181. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  182. package/dist/components/Toast/ToastProvider.js +1 -2
  183. package/dist/components/Toast/ToastProvider.js.map +1 -1
  184. package/dist/hooks/use-announce.cjs +2 -0
  185. package/dist/hooks/use-announce.cjs.map +1 -0
  186. package/dist/hooks/use-announce.js +50 -0
  187. package/dist/hooks/use-announce.js.map +1 -0
  188. package/dist/hooks/use-media-devices.cjs +2 -0
  189. package/dist/hooks/use-media-devices.cjs.map +1 -0
  190. package/dist/hooks/use-media-devices.js +46 -0
  191. package/dist/hooks/use-media-devices.js.map +1 -0
  192. package/dist/hooks/use-media-permission.cjs +2 -0
  193. package/dist/hooks/use-media-permission.cjs.map +1 -0
  194. package/dist/hooks/use-media-permission.js +33 -0
  195. package/dist/hooks/use-media-permission.js.map +1 -0
  196. package/dist/http/resumable-upload.cjs +2 -0
  197. package/dist/http/resumable-upload.cjs.map +1 -0
  198. package/dist/http/resumable-upload.js +249 -0
  199. package/dist/http/resumable-upload.js.map +1 -0
  200. package/dist/styles.css +1 -1
  201. package/dist/tempest-react-sdk.cjs +1 -1
  202. package/dist/tempest-react-sdk.d.ts +2485 -29
  203. package/dist/tempest-react-sdk.js +268 -244
  204. package/dist/vision/core/graph.cjs +2 -0
  205. package/dist/vision/core/graph.cjs.map +1 -0
  206. package/dist/vision/core/graph.js +17 -0
  207. package/dist/vision/core/graph.js.map +1 -0
  208. package/dist/vision/core/session.cjs +1 -1
  209. package/dist/vision/core/session.cjs.map +1 -1
  210. package/dist/vision/core/session.js +19 -9
  211. package/dist/vision/core/session.js.map +1 -1
  212. package/dist/vision/index.cjs +1 -1
  213. package/dist/vision/index.cjs.map +1 -1
  214. package/dist/vision/index.js +14 -13
  215. package/dist/vision/index.js.map +1 -1
  216. package/dist/vision/tasks/classifier.cjs +1 -1
  217. package/dist/vision/tasks/classifier.cjs.map +1 -1
  218. package/dist/vision/tasks/classifier.js +33 -25
  219. package/dist/vision/tasks/classifier.js.map +1 -1
  220. package/dist/vision/tasks/detector.cjs +1 -1
  221. package/dist/vision/tasks/detector.cjs.map +1 -1
  222. package/dist/vision/tasks/detector.js +28 -20
  223. package/dist/vision/tasks/detector.js.map +1 -1
  224. package/dist/vision/tasks/segmenter.cjs +1 -1
  225. package/dist/vision/tasks/segmenter.cjs.map +1 -1
  226. package/dist/vision/tasks/segmenter.js +23 -15
  227. package/dist/vision/tasks/segmenter.js.map +1 -1
  228. package/dist/vision/use-camera-stream.cjs +1 -1
  229. package/dist/vision/use-camera-stream.cjs.map +1 -1
  230. package/dist/vision/use-camera-stream.js +40 -57
  231. package/dist/vision/use-camera-stream.js.map +1 -1
  232. package/dist/vision.cjs +1 -1
  233. package/dist/vision.d.ts +149 -8
  234. package/dist/vision.js +22 -21
  235. package/package.json +1 -1
@@ -0,0 +1,50 @@
1
+ import { useCallback as e, useEffect as t } from "react";
2
+ //#region src/hooks/use-announce.ts
3
+ var n = 7e3, r = {
4
+ position: "absolute",
5
+ width: "1px",
6
+ height: "1px",
7
+ margin: "-1px",
8
+ padding: "0",
9
+ border: "0",
10
+ overflow: "hidden",
11
+ clip: "rect(0 0 0 0)",
12
+ clipPath: "inset(50%)",
13
+ whiteSpace: "nowrap"
14
+ }, i = null, a = /* @__PURE__ */ new Set();
15
+ function o(e) {
16
+ let t = document.createElement("div");
17
+ return e === "polite" && t.setAttribute("role", "status"), t.setAttribute("aria-live", e), t.setAttribute("aria-atomic", "true"), t.setAttribute("data-tempest-announcer", e), Object.assign(t.style, r), document.body.appendChild(t), t;
18
+ }
19
+ function s() {
20
+ return typeof document > "u" ? null : i && i.polite.isConnected && i.assertive.isConnected ? i : (i?.polite.remove(), i?.assertive.remove(), i = {
21
+ polite: o("polite"),
22
+ assertive: o("assertive")
23
+ }, i);
24
+ }
25
+ function c(e, t = "polite") {
26
+ if (!e) return;
27
+ let r = s();
28
+ if (!r) return;
29
+ let i = t === "assertive" ? r.assertive : r.polite;
30
+ i.replaceChildren();
31
+ let o = document.createElement("div");
32
+ o.textContent = e, i.appendChild(o);
33
+ let c = setTimeout(() => {
34
+ a.delete(c), o.isConnected && o.remove();
35
+ }, n);
36
+ a.add(c);
37
+ }
38
+ function l() {
39
+ for (let e of a) clearTimeout(e);
40
+ a.clear(), i?.polite.remove(), i?.assertive.remove(), i = null;
41
+ }
42
+ function u() {
43
+ return t(() => {
44
+ s();
45
+ }, []), e((e, t = "polite") => c(e, t), []);
46
+ }
47
+ //#endregion
48
+ export { c as announce, l as clearAnnouncer, u as useAnnounce };
49
+
50
+ //# sourceMappingURL=use-announce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-announce.js","names":[],"sources":["../../src/hooks/use-announce.ts"],"sourcesContent":["import { useCallback, useEffect } from \"react\";\n\n/**\n * How urgently a screen reader should interrupt.\n *\n * `\"polite\"` waits for a pause in what is being read. `\"assertive\"` cuts in\n * immediately, which is right for an error the user must act on and wrong for\n * everything else — an assertive announcement can truncate the sentence the user\n * was in the middle of.\n */\nexport type AnnouncePoliteness = \"polite\" | \"assertive\";\n\n/** How long an announcement stays in the DOM before it is cleaned up. */\nconst CLEAR_AFTER_MS = 7_000;\n\n/**\n * Inline styles that hide an element visually while leaving it readable.\n *\n * Inline and not a CSS module on purpose: these nodes are created imperatively by\n * a hook, and a hook that only works when the app remembered to import\n * `tempest-react-sdk/styles.css` would drop two visible, unstyled paragraphs into\n * the page — a visual bug caused by an accessibility feature.\n */\nconst HIDDEN_STYLE: Partial<CSSStyleDeclaration> = {\n position: \"absolute\",\n width: \"1px\",\n height: \"1px\",\n margin: \"-1px\",\n padding: \"0\",\n border: \"0\",\n overflow: \"hidden\",\n clip: \"rect(0 0 0 0)\",\n clipPath: \"inset(50%)\",\n whiteSpace: \"nowrap\",\n};\n\ninterface Regions {\n polite: HTMLElement;\n assertive: HTMLElement;\n}\n\nlet regions: Regions | null = null;\nconst timers = new Set<ReturnType<typeof setTimeout>>();\n\n/**\n * Build one live region.\n *\n * The polite region carries `role=\"status\"`; the assertive one carries only\n * `aria-live=\"assertive\"`. `role=\"alert\"` is deliberately **not** used: these\n * regions live in the document for the page's whole life, and an empty element\n * claiming to be an alert is both a lie about its content and a trap for every\n * `getByRole(\"alert\")` in a consuming app's test suite, which would suddenly match\n * two nodes. A bare `aria-live` region is announced by every screen reader that\n * supports live regions at all.\n *\n * @param politeness - Which region to build.\n * @returns The region, already appended to `<body>`.\n */\nfunction createRegion(politeness: AnnouncePoliteness): HTMLElement {\n const node = document.createElement(\"div\");\n if (politeness === \"polite\") node.setAttribute(\"role\", \"status\");\n node.setAttribute(\"aria-live\", politeness);\n node.setAttribute(\"aria-atomic\", \"true\");\n node.setAttribute(\"data-tempest-announcer\", politeness);\n Object.assign(node.style, HIDDEN_STYLE);\n document.body.appendChild(node);\n return node;\n}\n\n/**\n * Get the two shared live regions, creating them on first use.\n *\n * **Two regions, not one with a switchable `aria-live`.** Politeness is a property\n * of the region, read when the assistive technology first registers it — flipping\n * the attribute later is honoured by some screen readers, ignored by others, and\n * in the worst case drops the announcement entirely. Two regions that never change\n * are the only version that behaves the same everywhere.\n *\n * **Shared, not one per component.** Every live region on the page is polled;\n * several of them mutating at once is how announcements get dropped or doubled.\n * One pair per document keeps the order deterministic.\n *\n * A pair is replaced as a **pair**, discarding the old one: if only one region was\n * torn out of the DOM (a router that replaced `<body>`, a micro-frontend unmount),\n * keeping its still-attached sibling would leave two regions of that politeness on\n * the page — the exact duplication the shared pair exists to prevent.\n *\n * @returns The polite and assertive regions, or `null` outside a document.\n */\nfunction ensureRegions(): Regions | null {\n if (typeof document === \"undefined\") return null;\n if (regions && regions.polite.isConnected && regions.assertive.isConnected) return regions;\n regions?.polite.remove();\n regions?.assertive.remove();\n regions = { polite: createRegion(\"polite\"), assertive: createRegion(\"assertive\") };\n return regions;\n}\n\n/**\n * Announce a message to screen readers, from anywhere — a hook, an event handler,\n * a plain function outside React.\n *\n * ## Why the same string announces twice\n *\n * Screen readers announce a live region when its **content changes**. Writing the\n * same text again is not a change, so \"Item removido\" twice in a row is read once —\n * the classic reason these announcers are quietly broken. Instead of mutating text,\n * every call replaces the region's child with a **new element**. The DOM mutation is\n * real even when the string is identical, so the second announcement happens, and\n * the reader hears the exact message with no padding characters bolted on.\n *\n * @param message - Text to read out. Empty strings are ignored.\n * @param politeness - `\"polite\"` (default) or `\"assertive\"`.\n *\n * @example\n * announce(`${count} pedidos encontrados`);\n * announce(\"Falha ao salvar\", \"assertive\");\n */\nexport function announce(message: string, politeness: AnnouncePoliteness = \"polite\"): void {\n if (!message) return;\n const pair = ensureRegions();\n if (!pair) return;\n\n const region = politeness === \"assertive\" ? pair.assertive : pair.polite;\n region.replaceChildren();\n const item = document.createElement(\"div\");\n item.textContent = message;\n region.appendChild(item);\n\n const timer = setTimeout(() => {\n timers.delete(timer);\n if (item.isConnected) item.remove();\n }, CLEAR_AFTER_MS);\n timers.add(timer);\n}\n\n/**\n * Remove the shared regions and cancel pending cleanups.\n *\n * For test teardown and for a micro-frontend being unmounted from a page it does\n * not own. Regular apps never need it — two empty hidden `div`s cost nothing, and\n * tearing them down while another component still announces would lose messages.\n */\nexport function clearAnnouncer(): void {\n for (const timer of timers) clearTimeout(timer);\n timers.clear();\n regions?.polite.remove();\n regions?.assertive.remove();\n regions = null;\n}\n\n/**\n * Announce transient messages to screen readers through one shared live region\n * pair.\n *\n * Reach for this when something happened that a sighted user can see and a screen\n * reader user cannot: a filter narrowed a list, a row saved, a copy succeeded, an\n * upload failed. It is **not** for content that is already on screen inside a\n * region with a role — a status pill, a toast that renders as `role=\"status\"`,\n * a form error tied to its input — announcing those again reads them twice.\n *\n * !!! warning \"Never wrap streaming text in a live region\"\n * A live region over text that grows token by token makes the reader start the\n * whole answer again on every token. Announce the **edges** instead — \"gerando\n * resposta\" and \"resposta concluída\" — and leave the transcript in a plain\n * `role=\"log\"` the user reads at their own pace. `AIChat` does exactly that.\n *\n * Mounting the hook creates the regions even before the first message. That is not\n * tidiness: a live region inserted into the DOM in the same frame as its first\n * content routinely loses that announcement, because the assistive technology has\n * to have registered the region before it can notice a change inside it.\n *\n * @returns A stable `announce(message, politeness?)` function.\n *\n * @example\n * const announce = useAnnounce();\n *\n * function onFilter(rows: Row[]) {\n * announce(`${rows.length} resultados`);\n * }\n */\nexport function useAnnounce(): (message: string, politeness?: AnnouncePoliteness) => void {\n useEffect(() => {\n ensureRegions();\n }, []);\n\n return useCallback(\n (message: string, politeness: AnnouncePoliteness = \"polite\") =>\n announce(message, politeness),\n [],\n );\n}\n"],"mappings":";;AAaA,IAAM,IAAiB,KAUjB,IAA6C;CAC/C,UAAU;CACV,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,UAAU;CACV,MAAM;CACN,UAAU;CACV,YAAY;AAChB,GAOI,IAA0B,MACxB,oBAAS,IAAI,IAAmC;AAgBtD,SAAS,EAAa,GAA6C;CAC/D,IAAM,IAAO,SAAS,cAAc,KAAK;CAOzC,OANI,MAAe,YAAU,EAAK,aAAa,QAAQ,QAAQ,GAC/D,EAAK,aAAa,aAAa,CAAU,GACzC,EAAK,aAAa,eAAe,MAAM,GACvC,EAAK,aAAa,0BAA0B,CAAU,GACtD,OAAO,OAAO,EAAK,OAAO,CAAY,GACtC,SAAS,KAAK,YAAY,CAAI,GACvB;AACX;AAsBA,SAAS,IAAgC;CAMrC,OALI,OAAO,WAAa,MAAoB,OACxC,KAAW,EAAQ,OAAO,eAAe,EAAQ,UAAU,cAAoB,KACnF,GAAS,OAAO,OAAO,GACvB,GAAS,UAAU,OAAO,GAC1B,IAAU;EAAE,QAAQ,EAAa,QAAQ;EAAG,WAAW,EAAa,WAAW;CAAE,GAC1E;AACX;AAsBA,SAAgB,EAAS,GAAiB,IAAiC,UAAgB;CACvF,IAAI,CAAC,GAAS;CACd,IAAM,IAAO,EAAc;CAC3B,IAAI,CAAC,GAAM;CAEX,IAAM,IAAS,MAAe,cAAc,EAAK,YAAY,EAAK;CAClE,EAAO,gBAAgB;CACvB,IAAM,IAAO,SAAS,cAAc,KAAK;CAEzC,AADA,EAAK,cAAc,GACnB,EAAO,YAAY,CAAI;CAEvB,IAAM,IAAQ,iBAAiB;EAE3B,AADA,EAAO,OAAO,CAAK,GACf,EAAK,eAAa,EAAK,OAAO;CACtC,GAAG,CAAc;CACjB,EAAO,IAAI,CAAK;AACpB;AASA,SAAgB,IAAuB;CACnC,KAAK,IAAM,KAAS,GAAQ,aAAa,CAAK;CAI9C,AAHA,EAAO,MAAM,GACb,GAAS,OAAO,OAAO,GACvB,GAAS,UAAU,OAAO,GAC1B,IAAU;AACd;AAgCA,SAAgB,IAA0E;CAKtF,OAJA,QAAgB;EACZ,EAAc;CAClB,GAAG,CAAC,CAAC,GAEE,GACF,GAAiB,IAAiC,aAC/C,EAAS,GAAS,CAAU,GAChC,CAAC,CACL;AACJ"}
@@ -0,0 +1,2 @@
1
+ let e=require("react");var t=[];function n(){let[n,r]=(0,e.useState)(t),[i,a]=(0,e.useState)(!1),[o,s]=(0,e.useState)(0),c=(0,e.useCallback)(()=>s(e=>e+1),[]);return(0,e.useEffect)(()=>{let e=!1;if(typeof navigator>`u`||!navigator.mediaDevices||typeof navigator.mediaDevices.enumerateDevices!=`function`){a(!1),r(t);return}a(!0);let n=navigator.mediaDevices;async function i(){try{let t=await n.enumerateDevices();if(e)return;r(t.map(e=>({deviceId:e.deviceId,label:e.label,kind:e.kind,groupId:e.groupId})))}catch{e||r(t)}}i();let o=()=>void i();return n.addEventListener?.(`devicechange`,o),()=>{e=!0,n.removeEventListener?.(`devicechange`,o)}},[o]),{devices:n,audioInputs:n.filter(e=>e.kind===`audioinput`),audioOutputs:n.filter(e=>e.kind===`audiooutput`),videoInputs:n.filter(e=>e.kind===`videoinput`),labelsAvailable:n.length>0&&n.some(e=>e.label!==``),supported:i,refresh:c}}exports.useMediaDevices=n;
2
+ //# sourceMappingURL=use-media-devices.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-media-devices.cjs","names":[],"sources":["../../src/hooks/use-media-devices.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/** One device the browser is willing to tell us about. */\nexport interface MediaDeviceOption {\n deviceId: string;\n /**\n * Human label, or `\"\"` until permission has been granted.\n *\n * See {@link UseMediaDevicesResult.labelsAvailable} — an empty label is not a\n * bug, it is the browser refusing to fingerprint the machine.\n */\n label: string;\n kind: MediaDeviceKind;\n groupId: string;\n}\n\n/** Value returned by {@link useMediaDevices}. */\nexport interface UseMediaDevicesResult {\n /** Every device, in the order the browser listed them. */\n devices: MediaDeviceOption[];\n /** Microphones and other audio sources. */\n audioInputs: MediaDeviceOption[];\n /** Speakers and headsets. Empty on browsers with no output routing (Safari, Firefox). */\n audioOutputs: MediaDeviceOption[];\n /** Cameras. */\n videoInputs: MediaDeviceOption[];\n /**\n * Whether labels are filled in.\n *\n * `false` means the list is real but anonymous — render a picker only after a\n * capture permission has been granted, or the user sees \"\" repeated N times.\n */\n labelsAvailable: boolean;\n /** `enumerateDevices` is unavailable in this browser. */\n supported: boolean;\n /** Re-enumerate. The hook already does this on `devicechange`. */\n refresh: () => void;\n}\n\nconst EMPTY: MediaDeviceOption[] = [];\n\n/**\n * List capture and playback devices, and keep the list fresh.\n *\n * Two things about `enumerateDevices` that decide how a picker must be built:\n *\n * 1. **Labels are gated behind permission.** Before the user grants a capture\n * permission, every `label` is `\"\"` — the list length and ids are real, the names\n * are not. A device picker rendered at that point is a column of blanks, so\n * `labelsAvailable` is exposed to gate it. Ask for the microphone first, then\n * show the picker.\n * 2. **The list changes while the page is open.** Plugging in a headset mid-recording\n * is the normal case, not an edge case, so the hook subscribes to `devicechange`\n * instead of enumerating once on mount.\n *\n * `audioOutputs` is empty on browsers with no output routing at all (Safari and\n * Firefox do not implement `setSinkId`); an empty array there means \"you cannot\n * offer this choice\", not \"no speakers\".\n *\n * @returns The device lists, whether labels are filled in, and a `refresh()`.\n *\n * @example\n * const { audioInputs, labelsAvailable } = useMediaDevices();\n * // ...after the mic permission is granted:\n * {labelsAvailable && <Select options={audioInputs.map((d) => ({ value: d.deviceId, label: d.label }))} />}\n */\nexport function useMediaDevices(): UseMediaDevicesResult {\n const [devices, setDevices] = useState<MediaDeviceOption[]>(EMPTY);\n const [supported, setSupported] = useState(false);\n const [token, setToken] = useState(0);\n\n const refresh = useCallback(() => setToken((value) => value + 1), []);\n\n useEffect(() => {\n let cancelled = false;\n\n if (\n typeof navigator === \"undefined\" ||\n !navigator.mediaDevices ||\n typeof navigator.mediaDevices.enumerateDevices !== \"function\"\n ) {\n setSupported(false);\n setDevices(EMPTY);\n return;\n }\n setSupported(true);\n const media = navigator.mediaDevices;\n\n async function read(): Promise<void> {\n try {\n const list = await media.enumerateDevices();\n if (cancelled) return;\n setDevices(\n list.map((device) => ({\n deviceId: device.deviceId,\n label: device.label,\n kind: device.kind,\n groupId: device.groupId,\n })),\n );\n } catch {\n if (!cancelled) setDevices(EMPTY);\n }\n }\n\n void read();\n const onChange = (): void => void read();\n media.addEventListener?.(\"devicechange\", onChange);\n\n return () => {\n cancelled = true;\n media.removeEventListener?.(\"devicechange\", onChange);\n };\n }, [token]);\n\n return {\n devices,\n audioInputs: devices.filter((device) => device.kind === \"audioinput\"),\n audioOutputs: devices.filter((device) => device.kind === \"audiooutput\"),\n videoInputs: devices.filter((device) => device.kind === \"videoinput\"),\n labelsAvailable: devices.length > 0 && devices.some((device) => device.label !== \"\"),\n supported,\n refresh,\n };\n}\n"],"mappings":"uBAuCA,IAAM,EAA6B,CAAC,EA2BpC,SAAgB,GAAyC,CACrD,GAAM,CAAC,EAAS,IAAA,EAAA,EAAA,SAAA,CAA4C,CAAK,EAC3D,CAAC,EAAW,IAAA,EAAA,EAAA,SAAA,CAAyB,EAAK,EAC1C,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAAqB,CAAC,EAE9B,GAAA,EAAA,EAAA,YAAA,KAA4B,EAAU,GAAU,EAAQ,CAAC,EAAG,CAAC,CAAC,EA4CpE,OA1CA,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAI,EAAY,GAEhB,GACI,OAAO,UAAc,KACrB,CAAC,UAAU,cACX,OAAO,UAAU,aAAa,kBAAqB,WACrD,CACE,EAAa,EAAK,EAClB,EAAW,CAAK,EAChB,MACJ,CACA,EAAa,EAAI,EACjB,IAAM,EAAQ,UAAU,aAExB,eAAe,GAAsB,CACjC,GAAI,CACA,IAAM,EAAO,MAAM,EAAM,iBAAiB,EAC1C,GAAI,EAAW,OACf,EACI,EAAK,IAAK,IAAY,CAClB,SAAU,EAAO,SACjB,MAAO,EAAO,MACd,KAAM,EAAO,KACb,QAAS,EAAO,OACpB,EAAE,CACN,CACJ,MAAQ,CACC,GAAW,EAAW,CAAK,CACpC,CACJ,CAEA,EAAU,EACV,IAAM,MAAuB,KAAK,EAAK,EAGvC,OAFA,EAAM,mBAAmB,eAAgB,CAAQ,MAEpC,CACT,EAAY,GACZ,EAAM,sBAAsB,eAAgB,CAAQ,CACxD,CACJ,EAAG,CAAC,CAAK,CAAC,EAEH,CACH,UACA,YAAa,EAAQ,OAAQ,GAAW,EAAO,OAAS,YAAY,EACpE,aAAc,EAAQ,OAAQ,GAAW,EAAO,OAAS,aAAa,EACtE,YAAa,EAAQ,OAAQ,GAAW,EAAO,OAAS,YAAY,EACpE,gBAAiB,EAAQ,OAAS,GAAK,EAAQ,KAAM,GAAW,EAAO,QAAU,EAAE,EACnF,YACA,SACJ,CACJ"}
@@ -0,0 +1,46 @@
1
+ import { useCallback as e, useEffect as t, useState as n } from "react";
2
+ //#region src/hooks/use-media-devices.ts
3
+ var r = [];
4
+ function i() {
5
+ let [i, a] = n(r), [o, s] = n(!1), [c, l] = n(0), u = e(() => l((e) => e + 1), []);
6
+ return t(() => {
7
+ let e = !1;
8
+ if (typeof navigator > "u" || !navigator.mediaDevices || typeof navigator.mediaDevices.enumerateDevices != "function") {
9
+ s(!1), a(r);
10
+ return;
11
+ }
12
+ s(!0);
13
+ let t = navigator.mediaDevices;
14
+ async function n() {
15
+ try {
16
+ let n = await t.enumerateDevices();
17
+ if (e) return;
18
+ a(n.map((e) => ({
19
+ deviceId: e.deviceId,
20
+ label: e.label,
21
+ kind: e.kind,
22
+ groupId: e.groupId
23
+ })));
24
+ } catch {
25
+ e || a(r);
26
+ }
27
+ }
28
+ n();
29
+ let i = () => void n();
30
+ return t.addEventListener?.("devicechange", i), () => {
31
+ e = !0, t.removeEventListener?.("devicechange", i);
32
+ };
33
+ }, [c]), {
34
+ devices: i,
35
+ audioInputs: i.filter((e) => e.kind === "audioinput"),
36
+ audioOutputs: i.filter((e) => e.kind === "audiooutput"),
37
+ videoInputs: i.filter((e) => e.kind === "videoinput"),
38
+ labelsAvailable: i.length > 0 && i.some((e) => e.label !== ""),
39
+ supported: o,
40
+ refresh: u
41
+ };
42
+ }
43
+ //#endregion
44
+ export { i as useMediaDevices };
45
+
46
+ //# sourceMappingURL=use-media-devices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-media-devices.js","names":[],"sources":["../../src/hooks/use-media-devices.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/** One device the browser is willing to tell us about. */\nexport interface MediaDeviceOption {\n deviceId: string;\n /**\n * Human label, or `\"\"` until permission has been granted.\n *\n * See {@link UseMediaDevicesResult.labelsAvailable} — an empty label is not a\n * bug, it is the browser refusing to fingerprint the machine.\n */\n label: string;\n kind: MediaDeviceKind;\n groupId: string;\n}\n\n/** Value returned by {@link useMediaDevices}. */\nexport interface UseMediaDevicesResult {\n /** Every device, in the order the browser listed them. */\n devices: MediaDeviceOption[];\n /** Microphones and other audio sources. */\n audioInputs: MediaDeviceOption[];\n /** Speakers and headsets. Empty on browsers with no output routing (Safari, Firefox). */\n audioOutputs: MediaDeviceOption[];\n /** Cameras. */\n videoInputs: MediaDeviceOption[];\n /**\n * Whether labels are filled in.\n *\n * `false` means the list is real but anonymous — render a picker only after a\n * capture permission has been granted, or the user sees \"\" repeated N times.\n */\n labelsAvailable: boolean;\n /** `enumerateDevices` is unavailable in this browser. */\n supported: boolean;\n /** Re-enumerate. The hook already does this on `devicechange`. */\n refresh: () => void;\n}\n\nconst EMPTY: MediaDeviceOption[] = [];\n\n/**\n * List capture and playback devices, and keep the list fresh.\n *\n * Two things about `enumerateDevices` that decide how a picker must be built:\n *\n * 1. **Labels are gated behind permission.** Before the user grants a capture\n * permission, every `label` is `\"\"` — the list length and ids are real, the names\n * are not. A device picker rendered at that point is a column of blanks, so\n * `labelsAvailable` is exposed to gate it. Ask for the microphone first, then\n * show the picker.\n * 2. **The list changes while the page is open.** Plugging in a headset mid-recording\n * is the normal case, not an edge case, so the hook subscribes to `devicechange`\n * instead of enumerating once on mount.\n *\n * `audioOutputs` is empty on browsers with no output routing at all (Safari and\n * Firefox do not implement `setSinkId`); an empty array there means \"you cannot\n * offer this choice\", not \"no speakers\".\n *\n * @returns The device lists, whether labels are filled in, and a `refresh()`.\n *\n * @example\n * const { audioInputs, labelsAvailable } = useMediaDevices();\n * // ...after the mic permission is granted:\n * {labelsAvailable && <Select options={audioInputs.map((d) => ({ value: d.deviceId, label: d.label }))} />}\n */\nexport function useMediaDevices(): UseMediaDevicesResult {\n const [devices, setDevices] = useState<MediaDeviceOption[]>(EMPTY);\n const [supported, setSupported] = useState(false);\n const [token, setToken] = useState(0);\n\n const refresh = useCallback(() => setToken((value) => value + 1), []);\n\n useEffect(() => {\n let cancelled = false;\n\n if (\n typeof navigator === \"undefined\" ||\n !navigator.mediaDevices ||\n typeof navigator.mediaDevices.enumerateDevices !== \"function\"\n ) {\n setSupported(false);\n setDevices(EMPTY);\n return;\n }\n setSupported(true);\n const media = navigator.mediaDevices;\n\n async function read(): Promise<void> {\n try {\n const list = await media.enumerateDevices();\n if (cancelled) return;\n setDevices(\n list.map((device) => ({\n deviceId: device.deviceId,\n label: device.label,\n kind: device.kind,\n groupId: device.groupId,\n })),\n );\n } catch {\n if (!cancelled) setDevices(EMPTY);\n }\n }\n\n void read();\n const onChange = (): void => void read();\n media.addEventListener?.(\"devicechange\", onChange);\n\n return () => {\n cancelled = true;\n media.removeEventListener?.(\"devicechange\", onChange);\n };\n }, [token]);\n\n return {\n devices,\n audioInputs: devices.filter((device) => device.kind === \"audioinput\"),\n audioOutputs: devices.filter((device) => device.kind === \"audiooutput\"),\n videoInputs: devices.filter((device) => device.kind === \"videoinput\"),\n labelsAvailable: devices.length > 0 && devices.some((device) => device.label !== \"\"),\n supported,\n refresh,\n };\n}\n"],"mappings":";;AAuCA,IAAM,IAA6B,CAAC;AA2BpC,SAAgB,IAAyC;CACrD,IAAM,CAAC,GAAS,KAAc,EAA8B,CAAK,GAC3D,CAAC,GAAW,KAAgB,EAAS,EAAK,GAC1C,CAAC,GAAO,KAAY,EAAS,CAAC,GAE9B,IAAU,QAAkB,GAAU,MAAU,IAAQ,CAAC,GAAG,CAAC,CAAC;CA4CpE,OA1CA,QAAgB;EACZ,IAAI,IAAY;EAEhB,IACI,OAAO,YAAc,OACrB,CAAC,UAAU,gBACX,OAAO,UAAU,aAAa,oBAAqB,YACrD;GAEE,AADA,EAAa,EAAK,GAClB,EAAW,CAAK;GAChB;EACJ;EACA,EAAa,EAAI;EACjB,IAAM,IAAQ,UAAU;EAExB,eAAe,IAAsB;GACjC,IAAI;IACA,IAAM,IAAO,MAAM,EAAM,iBAAiB;IAC1C,IAAI,GAAW;IACf,EACI,EAAK,KAAK,OAAY;KAClB,UAAU,EAAO;KACjB,OAAO,EAAO;KACd,MAAM,EAAO;KACb,SAAS,EAAO;IACpB,EAAE,CACN;GACJ,QAAQ;IACJ,AAAK,KAAW,EAAW,CAAK;GACpC;EACJ;EAEA,EAAU;EACV,IAAM,UAAuB,KAAK,EAAK;EAGvC,OAFA,EAAM,mBAAmB,gBAAgB,CAAQ,SAEpC;GAET,AADA,IAAY,IACZ,EAAM,sBAAsB,gBAAgB,CAAQ;EACxD;CACJ,GAAG,CAAC,CAAK,CAAC,GAEH;EACH;EACA,aAAa,EAAQ,QAAQ,MAAW,EAAO,SAAS,YAAY;EACpE,cAAc,EAAQ,QAAQ,MAAW,EAAO,SAAS,aAAa;EACtE,aAAa,EAAQ,QAAQ,MAAW,EAAO,SAAS,YAAY;EACpE,iBAAiB,EAAQ,SAAS,KAAK,EAAQ,MAAM,MAAW,EAAO,UAAU,EAAE;EACnF;EACA;CACJ;AACJ"}
@@ -0,0 +1,2 @@
1
+ let e=require("react");function t(t){let[n,r]=(0,e.useState)(`unknown`),[i,a]=(0,e.useState)(!1),[o,s]=(0,e.useState)(0),c=(0,e.useCallback)(()=>s(e=>e+1),[]);return(0,e.useEffect)(()=>{let e=!1,n=null,i=()=>{!e&&n&&r(n.state)};async function o(){if(typeof navigator>`u`||!navigator.permissions?.query){e||(a(!1),r(`unknown`));return}try{if(n=await navigator.permissions.query({name:t}),e)return;a(!0),r(n.state),n.addEventListener(`change`,i)}catch{e||(a(!1),r(`unknown`))}}return o(),()=>{e=!0,n?.removeEventListener(`change`,i)}},[t,o]),{state:n,supported:i,refresh:c}}exports.useMediaPermission=t;
2
+ //# sourceMappingURL=use-media-permission.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-media-permission.cjs","names":[],"sources":["../../src/hooks/use-media-permission.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/** Which capture permission to observe. */\nexport type MediaPermissionName = \"microphone\" | \"camera\";\n\n/**\n * Permission state, including the two cases the browser prompt cannot tell you\n * apart on its own.\n *\n * - `\"unknown\"` — the Permissions API is unavailable (Safari does not report\n * `microphone`), so the only way to learn the answer is to ask for the device.\n * - `\"prompt\"` — never asked. Asking now shows the browser prompt.\n * - `\"granted\"` / `\"denied\"` — settled. `\"denied\"` is **sticky**: calling\n * `getUserMedia` again does not re-prompt, it rejects immediately.\n */\nexport type MediaPermissionState = \"unknown\" | \"prompt\" | \"granted\" | \"denied\";\n\n/** Value returned by {@link useMediaPermission}. */\nexport interface UseMediaPermissionResult {\n /** Current state. Updates live when the user changes it in browser settings. */\n state: MediaPermissionState;\n /** Whether the Permissions API could answer at all. */\n supported: boolean;\n /** Re-read the permission. Rarely needed — the hook subscribes to changes. */\n refresh: () => void;\n}\n\n/**\n * Read a capture permission **without triggering the prompt**.\n *\n * This is the piece that makes a good permission flow possible. `getUserMedia` is\n * the only way to *get* a device, but calling it is also the only way to *ask*, and\n * a prompt fired on page load — before the user has pressed anything — is the\n * single most reliable way to earn a permanent \"Block\". Reading the state first\n * lets a page explain why it needs the microphone, and only then ask.\n *\n * It also separates \"never asked\" from \"denied\", which matters because they need\n * opposite UI: `\"prompt\"` gets a button, `\"denied\"` gets instructions for the\n * browser's site settings, since no amount of clicking will re-prompt.\n *\n * Safari does not expose `microphone` to the Permissions API and throws on the\n * query, which surfaces as `state: \"unknown\"` and `supported: false` — treat that\n * as \"you will have to ask to find out\", not as an error.\n *\n * @param name - Which permission to observe.\n * @returns The live state, whether it could be read, and a manual `refresh()`.\n *\n * @example\n * const { state } = useMediaPermission(\"microphone\");\n * if (state === \"denied\") return <p>Libere o microfone nas configurações do site.</p>;\n * return <button onClick={start}>Gravar</button>;\n */\nexport function useMediaPermission(name: MediaPermissionName): UseMediaPermissionResult {\n const [state, setState] = useState<MediaPermissionState>(\"unknown\");\n const [supported, setSupported] = useState(false);\n const [token, setToken] = useState(0);\n\n const refresh = useCallback(() => setToken((value) => value + 1), []);\n\n useEffect(() => {\n let cancelled = false;\n let status: PermissionStatus | null = null;\n const onChange = (): void => {\n if (!cancelled && status) setState(status.state as MediaPermissionState);\n };\n\n async function read(): Promise<void> {\n if (typeof navigator === \"undefined\" || !navigator.permissions?.query) {\n if (!cancelled) {\n setSupported(false);\n setState(\"unknown\");\n }\n return;\n }\n try {\n status = await navigator.permissions.query({\n name: name as PermissionName,\n });\n if (cancelled) return;\n setSupported(true);\n setState(status.state as MediaPermissionState);\n status.addEventListener(\"change\", onChange);\n } catch {\n if (!cancelled) {\n setSupported(false);\n setState(\"unknown\");\n }\n }\n }\n\n void read();\n\n return () => {\n cancelled = true;\n status?.removeEventListener(\"change\", onChange);\n };\n }, [name, token]);\n\n return { state, supported, refresh };\n}\n"],"mappings":"uBAoDA,SAAgB,EAAmB,EAAqD,CACpF,GAAM,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAA2C,SAAS,EAC5D,CAAC,EAAW,IAAA,EAAA,EAAA,SAAA,CAAyB,EAAK,EAC1C,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAAqB,CAAC,EAE9B,GAAA,EAAA,EAAA,YAAA,KAA4B,EAAU,GAAU,EAAQ,CAAC,EAAG,CAAC,CAAC,EAyCpE,OAvCA,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAI,EAAY,GACZ,EAAkC,KAChC,MAAuB,CACrB,CAAC,GAAa,GAAQ,EAAS,EAAO,KAA6B,CAC3E,EAEA,eAAe,GAAsB,CACjC,GAAI,OAAO,UAAc,KAAe,CAAC,UAAU,aAAa,MAAO,CAC9D,IACD,EAAa,EAAK,EAClB,EAAS,SAAS,GAEtB,MACJ,CACA,GAAI,CAIA,GAHA,EAAS,MAAM,UAAU,YAAY,MAAM,CACjC,MACV,CAAC,EACG,EAAW,OACf,EAAa,EAAI,EACjB,EAAS,EAAO,KAA6B,EAC7C,EAAO,iBAAiB,SAAU,CAAQ,CAC9C,MAAQ,CACC,IACD,EAAa,EAAK,EAClB,EAAS,SAAS,EAE1B,CACJ,CAIA,OAFA,EAAU,MAEG,CACT,EAAY,GACZ,GAAQ,oBAAoB,SAAU,CAAQ,CAClD,CACJ,EAAG,CAAC,EAAM,CAAK,CAAC,EAET,CAAE,QAAO,YAAW,SAAQ,CACvC"}
@@ -0,0 +1,33 @@
1
+ import { useCallback as e, useEffect as t, useState as n } from "react";
2
+ //#region src/hooks/use-media-permission.ts
3
+ function r(r) {
4
+ let [i, a] = n("unknown"), [o, s] = n(!1), [c, l] = n(0), u = e(() => l((e) => e + 1), []);
5
+ return t(() => {
6
+ let e = !1, t = null, n = () => {
7
+ !e && t && a(t.state);
8
+ };
9
+ async function i() {
10
+ if (typeof navigator > "u" || !navigator.permissions?.query) {
11
+ e || (s(!1), a("unknown"));
12
+ return;
13
+ }
14
+ try {
15
+ if (t = await navigator.permissions.query({ name: r }), e) return;
16
+ s(!0), a(t.state), t.addEventListener("change", n);
17
+ } catch {
18
+ e || (s(!1), a("unknown"));
19
+ }
20
+ }
21
+ return i(), () => {
22
+ e = !0, t?.removeEventListener("change", n);
23
+ };
24
+ }, [r, c]), {
25
+ state: i,
26
+ supported: o,
27
+ refresh: u
28
+ };
29
+ }
30
+ //#endregion
31
+ export { r as useMediaPermission };
32
+
33
+ //# sourceMappingURL=use-media-permission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-media-permission.js","names":[],"sources":["../../src/hooks/use-media-permission.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/** Which capture permission to observe. */\nexport type MediaPermissionName = \"microphone\" | \"camera\";\n\n/**\n * Permission state, including the two cases the browser prompt cannot tell you\n * apart on its own.\n *\n * - `\"unknown\"` — the Permissions API is unavailable (Safari does not report\n * `microphone`), so the only way to learn the answer is to ask for the device.\n * - `\"prompt\"` — never asked. Asking now shows the browser prompt.\n * - `\"granted\"` / `\"denied\"` — settled. `\"denied\"` is **sticky**: calling\n * `getUserMedia` again does not re-prompt, it rejects immediately.\n */\nexport type MediaPermissionState = \"unknown\" | \"prompt\" | \"granted\" | \"denied\";\n\n/** Value returned by {@link useMediaPermission}. */\nexport interface UseMediaPermissionResult {\n /** Current state. Updates live when the user changes it in browser settings. */\n state: MediaPermissionState;\n /** Whether the Permissions API could answer at all. */\n supported: boolean;\n /** Re-read the permission. Rarely needed — the hook subscribes to changes. */\n refresh: () => void;\n}\n\n/**\n * Read a capture permission **without triggering the prompt**.\n *\n * This is the piece that makes a good permission flow possible. `getUserMedia` is\n * the only way to *get* a device, but calling it is also the only way to *ask*, and\n * a prompt fired on page load — before the user has pressed anything — is the\n * single most reliable way to earn a permanent \"Block\". Reading the state first\n * lets a page explain why it needs the microphone, and only then ask.\n *\n * It also separates \"never asked\" from \"denied\", which matters because they need\n * opposite UI: `\"prompt\"` gets a button, `\"denied\"` gets instructions for the\n * browser's site settings, since no amount of clicking will re-prompt.\n *\n * Safari does not expose `microphone` to the Permissions API and throws on the\n * query, which surfaces as `state: \"unknown\"` and `supported: false` — treat that\n * as \"you will have to ask to find out\", not as an error.\n *\n * @param name - Which permission to observe.\n * @returns The live state, whether it could be read, and a manual `refresh()`.\n *\n * @example\n * const { state } = useMediaPermission(\"microphone\");\n * if (state === \"denied\") return <p>Libere o microfone nas configurações do site.</p>;\n * return <button onClick={start}>Gravar</button>;\n */\nexport function useMediaPermission(name: MediaPermissionName): UseMediaPermissionResult {\n const [state, setState] = useState<MediaPermissionState>(\"unknown\");\n const [supported, setSupported] = useState(false);\n const [token, setToken] = useState(0);\n\n const refresh = useCallback(() => setToken((value) => value + 1), []);\n\n useEffect(() => {\n let cancelled = false;\n let status: PermissionStatus | null = null;\n const onChange = (): void => {\n if (!cancelled && status) setState(status.state as MediaPermissionState);\n };\n\n async function read(): Promise<void> {\n if (typeof navigator === \"undefined\" || !navigator.permissions?.query) {\n if (!cancelled) {\n setSupported(false);\n setState(\"unknown\");\n }\n return;\n }\n try {\n status = await navigator.permissions.query({\n name: name as PermissionName,\n });\n if (cancelled) return;\n setSupported(true);\n setState(status.state as MediaPermissionState);\n status.addEventListener(\"change\", onChange);\n } catch {\n if (!cancelled) {\n setSupported(false);\n setState(\"unknown\");\n }\n }\n }\n\n void read();\n\n return () => {\n cancelled = true;\n status?.removeEventListener(\"change\", onChange);\n };\n }, [name, token]);\n\n return { state, supported, refresh };\n}\n"],"mappings":";;AAoDA,SAAgB,EAAmB,GAAqD;CACpF,IAAM,CAAC,GAAO,KAAY,EAA+B,SAAS,GAC5D,CAAC,GAAW,KAAgB,EAAS,EAAK,GAC1C,CAAC,GAAO,KAAY,EAAS,CAAC,GAE9B,IAAU,QAAkB,GAAU,MAAU,IAAQ,CAAC,GAAG,CAAC,CAAC;CAyCpE,OAvCA,QAAgB;EACZ,IAAI,IAAY,IACZ,IAAkC,MAChC,UAAuB;GACzB,AAAI,CAAC,KAAa,KAAQ,EAAS,EAAO,KAA6B;EAC3E;EAEA,eAAe,IAAsB;GACjC,IAAI,OAAO,YAAc,OAAe,CAAC,UAAU,aAAa,OAAO;IACnE,AAAK,MACD,EAAa,EAAK,GAClB,EAAS,SAAS;IAEtB;GACJ;GACA,IAAI;IAIA,IAHA,IAAS,MAAM,UAAU,YAAY,MAAM,EACjC,QACV,CAAC,GACG,GAAW;IAGf,AAFA,EAAa,EAAI,GACjB,EAAS,EAAO,KAA6B,GAC7C,EAAO,iBAAiB,UAAU,CAAQ;GAC9C,QAAQ;IACJ,AAAK,MACD,EAAa,EAAK,GAClB,EAAS,SAAS;GAE1B;EACJ;EAIA,OAFA,EAAU,SAEG;GAET,AADA,IAAY,IACZ,GAAQ,oBAAoB,UAAU,CAAQ;EAClD;CACJ,GAAG,CAAC,GAAM,CAAK,CAAC,GAET;EAAE;EAAO;EAAW;CAAQ;AACvC"}
@@ -0,0 +1,2 @@
1
+ const e=require("./errors.cjs"),t=require("./idempotency.cjs"),n=require("./retry.cjs");var r=`1.0.0`,i=5*1024*1024;function a(e){let t=new TextEncoder().encode(e),n=``;for(let e of t)n+=String.fromCharCode(e);return btoa(n)}function o(e){if(!e)return null;let t=Object.entries(e).map(([e,t])=>`${e} ${a(t)}`);return t.length>0?t.join(`,`):null}function s(e,t){let n=t,r=typeof n.name==`string`?n.name:`blob`,i=typeof n.lastModified==`number`?n.lastModified:0;return`${e}|${r}|${t.size}|${t.type}|${i}`}function c(e=`tempest-upload:`){function t(){try{return typeof localStorage>`u`?null:localStorage}catch{return null}}return{get(n){let r=t()?.getItem(e+n);if(!r)return null;try{return JSON.parse(r)}catch{return null}},set(n,r){t()?.setItem(e+n,JSON.stringify(r))},delete(n){t()?.removeItem(e+n)}}}function l(t){return new Promise((n,r)=>{let i=new XMLHttpRequest;i.open(t.method,t.url),i.withCredentials=t.withCredentials;for(let[e,n]of Object.entries(t.headers))i.setRequestHeader(e,n);if(t.onProgress){let e=t.onProgress;i.upload.onprogress=t=>e(t.loaded)}i.onload=()=>n({status:i.status,text:i.responseText,header:e=>i.getResponseHeader(e)}),i.onerror=()=>r(new e.TempestApiError({status:0,detail:`Falha de rede no upload resumível.`})),i.onabort=()=>r(new DOMException(`Aborted`,`AbortError`)),t.register(i),i.send(t.body)})}function u(e){let t=e.header(`Upload-Offset`);if(t===null)return null;let n=Number(t);return Number.isFinite(n)&&n>=0?n:null}function d(e){if(!e)return null;try{return JSON.parse(e)}catch{return e}}function f(t,n){let r=d(t.text),i=e.buildApiError(t.status,r,{get:t.header}),a=typeof r==`object`&&!!r&&(`detail`in r||`message`in r);return new e.TempestApiError({...i,detail:a?i.detail:n})}function p(e){let t=typeof window>`u`?void 0:window.location.href;try{return new URL(e,t).href}catch{return e}}function m(a){let{endpoint:d,file:m,chunkSize:h=i,metadata:g,headers:_={},getToken:v,withCredentials:y=!1,key:b=s(d,m),storage:x=c(),retry:S,onProgress:C,onStateChange:w}=a,T=`idle`,E=0,D=null,O=null,k=null,A=null,j=0;function M(e){T!==e&&(T=e,w?.(e))}function N(e){C?.({loaded:e,total:m.size,fraction:m.size===0?1:e/m.size,resumedFrom:j})}function P(){let e={..._,"Tus-Resumable":r},t=v?.();return t&&!(`Authorization`in e)&&(e.Authorization=`Bearer ${t}`),e}function F(e){A=e}async function I(){!x||!D||!O||await x.set(b,{url:D,offset:E,size:m.size,idempotencyKey:O,updatedAt:Date.now()})}async function L(t){let n=await l({method:`HEAD`,url:t,headers:P(),withCredentials:y,register:F});if(n.status===404||n.status===410)throw new e.TempestApiError({status:n.status,detail:`O upload expirou no servidor. Comece de novo.`});let r=u(n);if(r===null)throw f(n,`HEAD sem Upload-Offset.`);return r}async function R(){let e=x?await x.get(b):null;if(e&&e.size===m.size&&(O=e.idempotencyKey,e.url))try{return E=await L(e.url),D=e.url,e.url}catch{E=0}M(`creating`),O??=t.generateIdempotencyKey(),D=null,E=0,x&&await x.set(b,{url:``,offset:0,size:m.size,idempotencyKey:O,updatedAt:Date.now()});let n={...P(),"Upload-Length":String(m.size),"Idempotency-Key":O},r=o(g);r&&(n[`Upload-Metadata`]=r);let i=await l({method:`POST`,url:d,headers:n,withCredentials:y,register:F});if(i.status!==201)throw f(i,`Criação do upload recusada.`);let a=i.header(`Location`);if(!a)throw f(i,`Criação do upload sem cabeçalho Location.`);return D=p(a),await I(),D}async function z(e,t){if(t.needed&&(E=await L(e),t.needed=!1,N(E),await I(),E>=m.size))return;let n=Math.min(E+h,m.size),r=E,i=await l({method:`PATCH`,url:e,headers:{...P(),"Content-Type":`application/offset+octet-stream`,"Upload-Offset":String(r)},body:m.slice(r,n),withCredentials:y,onProgress:e=>N(Math.min(r+e,m.size)),register:F});if(i.status===409||i.status===412)throw t.needed=!0,f(i,`Offset divergente — o servidor já tinha esses bytes.`);if(i.status!==204&&i.status!==200)throw f(i,`Chunk recusado pelo servidor.`);E=u(i)??n,N(E),await I()}async function B(){k=null;let e=await R();j=E,M(`uploading`),N(E);let t={needed:!1};for(;E<m.size&&!k;)await n.retry(()=>z(e,t),{retries:5,...S,shouldRetry:(e,n)=>k||e instanceof DOMException&&e.name===`AbortError`?!1:(t.needed=!0,S?.shouldRetry?.(e,n)??!0)});return k===`pause`?(M(`paused`),null):k===`abort`?(M(`aborted`),null):(M(`done`),x&&await x.delete(b),{url:e,size:m.size})}async function V(){try{return await B()}catch(e){if(k!==null||e instanceof DOMException&&e.name===`AbortError`)return M(k===`abort`?`aborted`:`paused`),null;throw M(`error`),e}finally{A=null}}function H(e){k=e,A?.abort(),A=null}return{start:V,resume:V,pause:()=>H(`pause`),abort:async({discard:e=!1}={})=>{H(`abort`),M(`aborted`),e&&(D&&await l({method:`DELETE`,url:D,headers:P(),withCredentials:y,register:()=>void 0}).catch(()=>void 0),x&&await x.delete(b))},get state(){return T},get offset(){return E},get url(){return D},key:b}}exports.DEFAULT_CHUNK_SIZE=i,exports.TUS_VERSION=r,exports.createLocalUploadStorage=c,exports.createResumableUpload=m,exports.uploadFingerprint=s;
2
+ //# sourceMappingURL=resumable-upload.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resumable-upload.cjs","names":[],"sources":["../../src/http/resumable-upload.ts"],"sourcesContent":["import { buildApiError, TempestApiError } from \"./errors\";\nimport { generateIdempotencyKey } from \"./idempotency\";\nimport { retry, type RetryOptions } from \"./retry\";\n\n/** The tus protocol version this client speaks. */\nexport const TUS_VERSION = \"1.0.0\";\n\n/** Default chunk size: 5 MiB, the size most tus servers are tuned for. */\nexport const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;\n\n/**\n * Where a resumable upload is.\n *\n * `\"paused\"` and `\"aborted\"` are both \"not running\", but only `\"paused\"` keeps the\n * persisted offset — `abort({ discard: true })` throws it away.\n */\nexport type ResumableUploadState =\n \"idle\" | \"creating\" | \"uploading\" | \"paused\" | \"done\" | \"error\" | \"aborted\";\n\n/** Byte-level progress for a resumable upload. */\nexport interface ResumableUploadProgress {\n /** Bytes the server holds, including anything a resume skipped. */\n loaded: number;\n /** Total size of the file. */\n total: number;\n /** `loaded / total`, between 0 and 1. */\n fraction: number;\n /** Bytes already on the server when this run started. `0` on a fresh upload. */\n resumedFrom: number;\n}\n\n/** What has to survive a page reload for a resume to be possible. */\nexport interface ResumableUploadRecord {\n /** Upload URL the creation POST returned, absolute. */\n url: string;\n /** Last offset the server confirmed. */\n offset: number;\n /** File size, so a different file under the same key is not resumed into. */\n size: number;\n /** Idempotency key of the creation request, reused if creation is retried. */\n idempotencyKey: string;\n /** Epoch ms of the last write, so an app can sweep stale records. */\n updatedAt: number;\n}\n\n/**\n * Persistence for resume state. Sync or async — both are awaited.\n *\n * Implement it over anything: the default is `localStorage`, and\n * `createOfflineStore` from `@/offline` slots in when you already have a Dexie\n * database open.\n */\nexport interface ResumableUploadStorage {\n /** Read the record for `key`, or `null`. */\n get(key: string): Promise<ResumableUploadRecord | null> | ResumableUploadRecord | null;\n /** Write the record for `key`. */\n set(key: string, record: ResumableUploadRecord): Promise<void> | void;\n /** Forget the record for `key`. */\n delete(key: string): Promise<void> | void;\n}\n\n/** Options for {@link createResumableUpload}. */\nexport interface ResumableUploadOptions {\n /** tus creation endpoint, e.g. `\"/api/uploads\"`. */\n endpoint: string;\n /** The bytes to upload. A `File` also supplies the default resume key. */\n file: Blob | File;\n /** Bytes per `PATCH`. Default {@link DEFAULT_CHUNK_SIZE}. */\n chunkSize?: number;\n /** Sent as `Upload-Metadata` (base64-encoded values), e.g. `{ filename }`. */\n metadata?: Record<string, string>;\n /** Extra headers on every request. */\n headers?: Record<string, string>;\n /** Returns the current bearer token, read before each request. */\n getToken?: () => string | null | undefined;\n /** Send cookies. Default `false`. */\n withCredentials?: boolean;\n /**\n * Resume key. Defaults to a fingerprint of endpoint + file name/size/mtime, so\n * picking the same file after a reload resumes instead of restarting.\n */\n key?: string;\n /**\n * Where to persist resume state. Defaults to `localStorage`. Pass `null` to\n * disable persistence — resume then only survives a network blip, not a reload.\n */\n storage?: ResumableUploadStorage | null;\n /** Backoff for a failed chunk. Forwarded to `retry`. Default 5 attempts. */\n retry?: RetryOptions;\n /** Called on every upload-progress tick and after every confirmed chunk. */\n onProgress?: (progress: ResumableUploadProgress) => void;\n /** Called whenever {@link ResumableUpload.state} changes. */\n onStateChange?: (state: ResumableUploadState) => void;\n}\n\n/** What a finished upload resolves with. */\nexport interface ResumableUploadResult {\n /** The tus upload URL — hand this to your API to link the stored file. */\n url: string;\n /** Total bytes uploaded. */\n size: number;\n}\n\n/** A resumable upload in progress. Build one with {@link createResumableUpload}. */\nexport interface ResumableUpload {\n /**\n * Create (or re-attach to) the upload and push chunks until it is complete.\n *\n * Resolves `null` when the run stopped because of `pause()` or `abort()` —\n * neither is a failure. Rejects with a `TempestApiError` when the server\n * refused and the retries ran out.\n */\n start(): Promise<ResumableUploadResult | null>;\n /** Stop after the in-flight chunk is dropped, keeping the resume point. */\n pause(): void;\n /** Continue from the server's offset. Same resolution contract as `start`. */\n resume(): Promise<ResumableUploadResult | null>;\n /**\n * Stop for good.\n *\n * @param options - `discard: true` also sends `DELETE` (tus termination) and\n * forgets the persisted record, so the next `start()` uploads from zero.\n */\n abort(options?: { discard?: boolean }): Promise<void>;\n /** Current state. */\n readonly state: ResumableUploadState;\n /** Bytes the server has confirmed. */\n readonly offset: number;\n /** The upload URL, once creation succeeded. */\n readonly url: string | null;\n /** The resume key in use. */\n readonly key: string;\n}\n\ninterface RawResponse {\n status: number;\n text: string;\n header(name: string): string | null;\n}\n\n/**\n * Encode a string as standard base64 (padded), UTF-8 first.\n *\n * `Upload-Metadata` carries base64 values precisely so a filename with accents\n * survives an HTTP header, so the UTF-8 step is not optional: `btoa` alone throws\n * on any code point above U+00FF.\n *\n * @param value - Text to encode.\n * @returns Padded base64.\n */\nfunction base64Utf8(value: string): string {\n const bytes = new TextEncoder().encode(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/**\n * Build the `Upload-Metadata` header value: comma-separated `key base64(value)`.\n *\n * @param metadata - Plain string map.\n * @returns The header value, or `null` when there is nothing to send.\n */\nfunction encodeMetadata(metadata: Record<string, string> | undefined): string | null {\n if (!metadata) return null;\n const parts = Object.entries(metadata).map(([name, value]) => `${name} ${base64Utf8(value)}`);\n return parts.length > 0 ? parts.join(\",\") : null;\n}\n\n/**\n * A stable-enough identity for a file, used as the default resume key.\n *\n * Name + size + last-modified is what the tus reference clients fingerprint on:\n * it is cheap (hashing the bytes of a 400 MB recording is not) and it changes\n * whenever the file does, which is the property that matters — resuming into the\n * wrong file would corrupt it silently.\n *\n * @param endpoint - Creation endpoint, so the same file to two servers is two uploads.\n * @param file - The blob or file being uploaded.\n * @returns A key safe to use in `localStorage`.\n */\nexport function uploadFingerprint(endpoint: string, file: Blob | File): string {\n const named = file as File;\n const name = typeof named.name === \"string\" ? named.name : \"blob\";\n const modified = typeof named.lastModified === \"number\" ? named.lastModified : 0;\n return `${endpoint}|${name}|${file.size}|${file.type}|${modified}`;\n}\n\n/**\n * `localStorage`-backed resume storage — the default.\n *\n * `localStorage` and not IndexedDB on purpose. The record is four fields and a\n * URL; the requirement is only that it survives a reload, and pulling Dexie in for\n * that would put an IndexedDB dependency in the bundle of every app that uploads a\n * file. Apps that already have `createOfflineStore` open can pass their own\n * {@link ResumableUploadStorage} instead.\n *\n * @param prefix - Key prefix. Default `\"tempest-upload:\"`.\n * @returns A storage that no-ops when `localStorage` is unavailable.\n */\nexport function createLocalUploadStorage(prefix = \"tempest-upload:\"): ResumableUploadStorage {\n function backend(): Storage | null {\n try {\n return typeof localStorage === \"undefined\" ? null : localStorage;\n } catch {\n return null;\n }\n }\n\n return {\n get(key) {\n const raw = backend()?.getItem(prefix + key);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as ResumableUploadRecord;\n } catch {\n return null;\n }\n },\n set(key, record) {\n backend()?.setItem(prefix + key, JSON.stringify(record));\n },\n delete(key) {\n backend()?.removeItem(prefix + key);\n },\n };\n}\n\n/**\n * Send one request over `XMLHttpRequest`.\n *\n * `XMLHttpRequest` rather than `fetch` for the same reason `uploadWithProgress`\n * uses it — `fetch` still cannot report upload progress in any browser — plus one\n * more: tus answers every write with the new `Upload-Offset` in a **response\n * header**, and `uploadWithProgress` only hands back a parsed body, so it could\n * not be reused here.\n *\n * @param init - Method, URL, headers, optional body and progress callback.\n * @returns Status, raw text and a header reader.\n */\nfunction sendRequest(init: {\n method: \"POST\" | \"HEAD\" | \"PATCH\" | \"DELETE\";\n url: string;\n headers: Record<string, string>;\n body?: Blob;\n withCredentials: boolean;\n onProgress?: (loaded: number) => void;\n register: (xhr: XMLHttpRequest) => void;\n}): Promise<RawResponse> {\n return new Promise<RawResponse>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(init.method, init.url);\n xhr.withCredentials = init.withCredentials;\n for (const [name, value] of Object.entries(init.headers)) {\n xhr.setRequestHeader(name, value);\n }\n if (init.onProgress) {\n const report = init.onProgress;\n xhr.upload.onprogress = (event: ProgressEvent) => report(event.loaded);\n }\n xhr.onload = () =>\n resolve({\n status: xhr.status,\n text: xhr.responseText,\n header: (name) => xhr.getResponseHeader(name),\n });\n xhr.onerror = () =>\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload resumível.\",\n }),\n );\n xhr.onabort = () => reject(new DOMException(\"Aborted\", \"AbortError\"));\n init.register(xhr);\n xhr.send(init.body);\n });\n}\n\nfunction parseOffset(response: RawResponse): number | null {\n const raw = response.header(\"Upload-Offset\");\n if (raw === null) return null;\n const value = Number(raw);\n return Number.isFinite(value) && value >= 0 ? value : null;\n}\n\n/**\n * Read an error body without assuming it is JSON.\n *\n * A tus proxy that rejects a chunk often answers with plain text or an HTML error\n * page, and `JSON.parse` throwing there would replace a useful status with a parse\n * error.\n *\n * @param text - Raw response text.\n * @returns The parsed object, the raw text, or `null` when the body was empty.\n */\nfunction parseErrorBody(text: string): unknown {\n if (!text) return null;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\n/**\n * Turn a refused tus response into a `TempestApiError`.\n *\n * The fallback `detail` is used unless the server sent a real error envelope,\n * because `buildApiError`'s own fallback (`\"Erro 409\"`) says nothing about which\n * step of the protocol broke — and that is the whole diagnostic value here.\n *\n * @param response - The raw response that was not acceptable.\n * @param detail - Message to use when the body carries none.\n * @returns The error to throw.\n */\nfunction failed(response: RawResponse, detail: string): TempestApiError {\n const body = parseErrorBody(response.text);\n const envelope = buildApiError(response.status, body, { get: response.header });\n const hasDetail =\n typeof body === \"object\" && body !== null && (\"detail\" in body || \"message\" in body);\n return new TempestApiError({ ...envelope, detail: hasDetail ? envelope.detail : detail });\n}\n\n/**\n * Resolve a `Location` header against the page, so a relative upload URL works.\n *\n * tus servers are free to answer creation with either an absolute URL or a\n * path, and the spec does not prefer one — a client that only handles absolute\n * URLs breaks against half the implementations.\n *\n * @param value - The raw `Location` header.\n * @returns An absolute URL, or the input when there is no base to resolve against.\n */\nfunction resolveUploadUrl(value: string): string {\n const base = typeof window === \"undefined\" ? undefined : window.location.href;\n try {\n return new URL(value, base).href;\n } catch {\n return value;\n }\n}\n\n/**\n * Chunked, resumable upload speaking the **tus 1.0.0** protocol (core plus the\n * *creation* and *termination* extensions).\n *\n * ## Why tus and not a bespoke scheme\n *\n * A resumable client whose wire format is undocumented cannot be integrated, and\n * inventing one means the backend is ours forever. tus is a published spec with\n * off-the-shelf servers (`tusd`, `tuspy`, `tus-node-server`), so a caller can point\n * this at something they did not write.\n *\n * ## What the backend must implement\n *\n * Every request carries `Tus-Resumable: 1.0.0`.\n *\n * | Step | Request | Expected response |\n * | --- | --- | --- |\n * | Create | `POST {endpoint}` + `Upload-Length`, `Upload-Metadata`, `Idempotency-Key` | `201` + `Location` (the upload URL, absolute or endpoint-relative) |\n * | Probe | `HEAD {uploadUrl}` | `200`/`204` + `Upload-Offset` |\n * | Write | `PATCH {uploadUrl}` + `Upload-Offset`, `Content-Type: application/offset+octet-stream`, chunk body | `204` + the new `Upload-Offset`; `409` when the offset does not match |\n * | Discard | `DELETE {uploadUrl}` | `204` |\n *\n * ## The failure that actually happens\n *\n * A chunk that the server stored but whose response never arrived. The client\n * cannot tell that from a chunk that was lost, and re-sending it blindly would\n * duplicate bytes. Two things prevent that:\n *\n * - **Writes are addressed, not appended.** Every `PATCH` states the offset it\n * writes at, so a retry after a lost response is asked to write bytes the server\n * already has and answers `409`. On any retry the client re-reads the truth with\n * `HEAD` first and continues from there.\n * - **Creation carries an `Idempotency-Key`** (from `generateIdempotencyKey`),\n * persisted before the first attempt and reused on retry. tus has no idempotent\n * creation of its own, so without this a lost `201` leaves an orphan upload on\n * the server. A backend that honours the header returns the same `Location`; one\n * that ignores it still works, it just keeps the orphan.\n *\n * @param options - Endpoint, file, and the knobs above.\n * @returns A handle with `start`/`pause`/`resume`/`abort` and live `state`/`offset`.\n *\n * @example\n * const upload = createResumableUpload({\n * endpoint: \"/api/uploads\",\n * file: recording,\n * metadata: { filename: \"nota.webm\", ticket: ticketId },\n * getToken: () => auth.getToken(),\n * onProgress: ({ fraction }) => setPercent(Math.round(fraction * 100)),\n * });\n *\n * const done = await upload.start();\n * if (done) await api.post(\"/api/tickets/1/audio\", { body: { url: done.url } });\n */\nexport function createResumableUpload(options: ResumableUploadOptions): ResumableUpload {\n const {\n endpoint,\n file,\n chunkSize = DEFAULT_CHUNK_SIZE,\n metadata,\n headers = {},\n getToken,\n withCredentials = false,\n key = uploadFingerprint(endpoint, file),\n storage = createLocalUploadStorage(),\n retry: retryOptions,\n onProgress,\n onStateChange,\n } = options;\n\n let state: ResumableUploadState = \"idle\";\n let offset = 0;\n let url: string | null = null;\n let idempotencyKey: string | null = null;\n let stopping: \"pause\" | \"abort\" | null = null;\n let inFlight: XMLHttpRequest | null = null;\n let resumedFrom = 0;\n\n function setState(next: ResumableUploadState): void {\n if (state === next) return;\n state = next;\n onStateChange?.(next);\n }\n\n function report(loaded: number): void {\n onProgress?.({\n loaded,\n total: file.size,\n fraction: file.size === 0 ? 1 : loaded / file.size,\n resumedFrom,\n });\n }\n\n function baseHeaders(): Record<string, string> {\n const result: Record<string, string> = { ...headers, \"Tus-Resumable\": TUS_VERSION };\n const token = getToken?.();\n if (token && !(\"Authorization\" in result)) result.Authorization = `Bearer ${token}`;\n return result;\n }\n\n function register(xhr: XMLHttpRequest): void {\n inFlight = xhr;\n }\n\n async function persist(): Promise<void> {\n if (!storage || !url || !idempotencyKey) return;\n await storage.set(key, {\n url,\n offset,\n size: file.size,\n idempotencyKey,\n updatedAt: Date.now(),\n });\n }\n\n /**\n * Ask the server how much it holds. The only source of truth after any failure.\n */\n async function probe(target: string): Promise<number> {\n const response = await sendRequest({\n method: \"HEAD\",\n url: target,\n headers: baseHeaders(),\n withCredentials,\n register,\n });\n if (response.status === 404 || response.status === 410) {\n throw new TempestApiError({\n status: response.status,\n detail: \"O upload expirou no servidor. Comece de novo.\",\n });\n }\n const confirmed = parseOffset(response);\n if (confirmed === null) throw failed(response, \"HEAD sem Upload-Offset.\");\n return confirmed;\n }\n\n /**\n * Re-attach to a persisted upload, or create a new one.\n *\n * The persisted record is only trusted when the file size still matches, and the\n * offset it holds is re-checked with `HEAD` — the client's copy can be ahead of\n * the server's whenever the last response was lost.\n */\n async function ensureUpload(): Promise<string> {\n const stored = storage ? await storage.get(key) : null;\n if (stored && stored.size === file.size) {\n idempotencyKey = stored.idempotencyKey;\n if (stored.url) {\n try {\n offset = await probe(stored.url);\n url = stored.url;\n return stored.url;\n } catch {\n offset = 0;\n }\n }\n }\n\n setState(\"creating\");\n idempotencyKey ??= generateIdempotencyKey();\n url = null;\n offset = 0;\n if (storage) {\n await storage.set(key, {\n url: \"\",\n offset: 0,\n size: file.size,\n idempotencyKey,\n updatedAt: Date.now(),\n });\n }\n\n const creationHeaders: Record<string, string> = {\n ...baseHeaders(),\n \"Upload-Length\": String(file.size),\n \"Idempotency-Key\": idempotencyKey,\n };\n const encoded = encodeMetadata(metadata);\n if (encoded) creationHeaders[\"Upload-Metadata\"] = encoded;\n\n const response = await sendRequest({\n method: \"POST\",\n url: endpoint,\n headers: creationHeaders,\n withCredentials,\n register,\n });\n if (response.status !== 201) throw failed(response, \"Criação do upload recusada.\");\n const locationHeader = response.header(\"Location\");\n if (!locationHeader) throw failed(response, \"Criação do upload sem cabeçalho Location.\");\n\n url = resolveUploadUrl(locationHeader);\n await persist();\n return url;\n }\n\n /** Push one chunk, resyncing the offset first when a previous attempt failed. */\n async function writeChunk(target: string, resync: { needed: boolean }): Promise<void> {\n if (resync.needed) {\n offset = await probe(target);\n resync.needed = false;\n report(offset);\n await persist();\n if (offset >= file.size) return;\n }\n\n const end = Math.min(offset + chunkSize, file.size);\n const from = offset;\n const response = await sendRequest({\n method: \"PATCH\",\n url: target,\n headers: {\n ...baseHeaders(),\n \"Content-Type\": \"application/offset+octet-stream\",\n \"Upload-Offset\": String(from),\n },\n body: file.slice(from, end),\n withCredentials,\n onProgress: (loaded) => report(Math.min(from + loaded, file.size)),\n register,\n });\n\n if (response.status === 409 || response.status === 412) {\n resync.needed = true;\n throw failed(response, \"Offset divergente — o servidor já tinha esses bytes.\");\n }\n if (response.status !== 204 && response.status !== 200) {\n throw failed(response, \"Chunk recusado pelo servidor.\");\n }\n\n offset = parseOffset(response) ?? end;\n report(offset);\n await persist();\n }\n\n /**\n * Drive the whole upload: attach or create, then chunk until complete.\n *\n * The `shouldRetry` predicate does double duty — besides deciding, it arms\n * `resync` so the next attempt re-reads the server's offset with `HEAD` before\n * writing. That is deliberate: it is the one place that sees *every* chunk\n * failure, whatever the cause, and after any failure the client's idea of the\n * offset is exactly what cannot be trusted.\n *\n * @returns The result, or `null` when `pause`/`abort` stopped the run.\n */\n async function run(): Promise<ResumableUploadResult | null> {\n stopping = null;\n const target = await ensureUpload();\n resumedFrom = offset;\n setState(\"uploading\");\n report(offset);\n\n const resync = { needed: false };\n while (offset < file.size) {\n if (stopping) break;\n await retry(() => writeChunk(target, resync), {\n retries: 5,\n ...retryOptions,\n shouldRetry: (error, attempt) => {\n if (stopping) return false;\n if (error instanceof DOMException && error.name === \"AbortError\") return false;\n resync.needed = true;\n return retryOptions?.shouldRetry?.(error, attempt) ?? true;\n },\n });\n }\n\n if (stopping === \"pause\") {\n setState(\"paused\");\n return null;\n }\n if (stopping === \"abort\") {\n setState(\"aborted\");\n return null;\n }\n\n setState(\"done\");\n if (storage) await storage.delete(key);\n return { url: target, size: file.size };\n }\n\n async function guarded(): Promise<ResumableUploadResult | null> {\n try {\n return await run();\n } catch (error) {\n if (\n stopping !== null ||\n (error instanceof DOMException && error.name === \"AbortError\")\n ) {\n setState(stopping === \"abort\" ? \"aborted\" : \"paused\");\n return null;\n }\n setState(\"error\");\n throw error;\n } finally {\n inFlight = null;\n }\n }\n\n function stop(reason: \"pause\" | \"abort\"): void {\n stopping = reason;\n inFlight?.abort();\n inFlight = null;\n }\n\n return {\n start: guarded,\n resume: guarded,\n pause: () => stop(\"pause\"),\n abort: async ({ discard = false } = {}) => {\n stop(\"abort\");\n setState(\"aborted\");\n if (!discard) return;\n if (url) {\n await sendRequest({\n method: \"DELETE\",\n url,\n headers: baseHeaders(),\n withCredentials,\n register: () => undefined,\n }).catch(() => undefined);\n }\n if (storage) await storage.delete(key);\n },\n get state() {\n return state;\n },\n get offset() {\n return offset;\n },\n get url() {\n return url;\n },\n key,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,249 @@
1
+ import { TempestApiError as e, buildApiError as t } from "./errors.js";
2
+ import { generateIdempotencyKey as n } from "./idempotency.js";
3
+ import { retry as r } from "./retry.js";
4
+ //#region src/http/resumable-upload.ts
5
+ var i = "1.0.0", a = 5 * 1024 * 1024;
6
+ function o(e) {
7
+ let t = new TextEncoder().encode(e), n = "";
8
+ for (let e of t) n += String.fromCharCode(e);
9
+ return btoa(n);
10
+ }
11
+ function s(e) {
12
+ if (!e) return null;
13
+ let t = Object.entries(e).map(([e, t]) => `${e} ${o(t)}`);
14
+ return t.length > 0 ? t.join(",") : null;
15
+ }
16
+ function c(e, t) {
17
+ let n = t, r = typeof n.name == "string" ? n.name : "blob", i = typeof n.lastModified == "number" ? n.lastModified : 0;
18
+ return `${e}|${r}|${t.size}|${t.type}|${i}`;
19
+ }
20
+ function l(e = "tempest-upload:") {
21
+ function t() {
22
+ try {
23
+ return typeof localStorage > "u" ? null : localStorage;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ return {
29
+ get(n) {
30
+ let r = t()?.getItem(e + n);
31
+ if (!r) return null;
32
+ try {
33
+ return JSON.parse(r);
34
+ } catch {
35
+ return null;
36
+ }
37
+ },
38
+ set(n, r) {
39
+ t()?.setItem(e + n, JSON.stringify(r));
40
+ },
41
+ delete(n) {
42
+ t()?.removeItem(e + n);
43
+ }
44
+ };
45
+ }
46
+ function u(t) {
47
+ return new Promise((n, r) => {
48
+ let i = new XMLHttpRequest();
49
+ i.open(t.method, t.url), i.withCredentials = t.withCredentials;
50
+ for (let [e, n] of Object.entries(t.headers)) i.setRequestHeader(e, n);
51
+ if (t.onProgress) {
52
+ let e = t.onProgress;
53
+ i.upload.onprogress = (t) => e(t.loaded);
54
+ }
55
+ i.onload = () => n({
56
+ status: i.status,
57
+ text: i.responseText,
58
+ header: (e) => i.getResponseHeader(e)
59
+ }), i.onerror = () => r(new e({
60
+ status: 0,
61
+ detail: "Falha de rede no upload resumível."
62
+ })), i.onabort = () => r(new DOMException("Aborted", "AbortError")), t.register(i), i.send(t.body);
63
+ });
64
+ }
65
+ function d(e) {
66
+ let t = e.header("Upload-Offset");
67
+ if (t === null) return null;
68
+ let n = Number(t);
69
+ return Number.isFinite(n) && n >= 0 ? n : null;
70
+ }
71
+ function f(e) {
72
+ if (!e) return null;
73
+ try {
74
+ return JSON.parse(e);
75
+ } catch {
76
+ return e;
77
+ }
78
+ }
79
+ function p(n, r) {
80
+ let i = f(n.text), a = t(n.status, i, { get: n.header }), o = typeof i == "object" && !!i && ("detail" in i || "message" in i);
81
+ return new e({
82
+ ...a,
83
+ detail: o ? a.detail : r
84
+ });
85
+ }
86
+ function m(e) {
87
+ let t = typeof window > "u" ? void 0 : window.location.href;
88
+ try {
89
+ return new URL(e, t).href;
90
+ } catch {
91
+ return e;
92
+ }
93
+ }
94
+ function h(t) {
95
+ let { endpoint: o, file: f, chunkSize: h = a, metadata: g, headers: _ = {}, getToken: v, withCredentials: y = !1, key: b = c(o, f), storage: x = l(), retry: S, onProgress: C, onStateChange: w } = t, T = "idle", E = 0, D = null, O = null, k = null, A = null, j = 0;
96
+ function M(e) {
97
+ T !== e && (T = e, w?.(e));
98
+ }
99
+ function N(e) {
100
+ C?.({
101
+ loaded: e,
102
+ total: f.size,
103
+ fraction: f.size === 0 ? 1 : e / f.size,
104
+ resumedFrom: j
105
+ });
106
+ }
107
+ function P() {
108
+ let e = {
109
+ ..._,
110
+ "Tus-Resumable": i
111
+ }, t = v?.();
112
+ return t && !("Authorization" in e) && (e.Authorization = `Bearer ${t}`), e;
113
+ }
114
+ function F(e) {
115
+ A = e;
116
+ }
117
+ async function I() {
118
+ !x || !D || !O || await x.set(b, {
119
+ url: D,
120
+ offset: E,
121
+ size: f.size,
122
+ idempotencyKey: O,
123
+ updatedAt: Date.now()
124
+ });
125
+ }
126
+ async function L(t) {
127
+ let n = await u({
128
+ method: "HEAD",
129
+ url: t,
130
+ headers: P(),
131
+ withCredentials: y,
132
+ register: F
133
+ });
134
+ if (n.status === 404 || n.status === 410) throw new e({
135
+ status: n.status,
136
+ detail: "O upload expirou no servidor. Comece de novo."
137
+ });
138
+ let r = d(n);
139
+ if (r === null) throw p(n, "HEAD sem Upload-Offset.");
140
+ return r;
141
+ }
142
+ async function R() {
143
+ let e = x ? await x.get(b) : null;
144
+ if (e && e.size === f.size && (O = e.idempotencyKey, e.url)) try {
145
+ return E = await L(e.url), D = e.url, e.url;
146
+ } catch {
147
+ E = 0;
148
+ }
149
+ M("creating"), O ??= n(), D = null, E = 0, x && await x.set(b, {
150
+ url: "",
151
+ offset: 0,
152
+ size: f.size,
153
+ idempotencyKey: O,
154
+ updatedAt: Date.now()
155
+ });
156
+ let t = {
157
+ ...P(),
158
+ "Upload-Length": String(f.size),
159
+ "Idempotency-Key": O
160
+ }, r = s(g);
161
+ r && (t["Upload-Metadata"] = r);
162
+ let i = await u({
163
+ method: "POST",
164
+ url: o,
165
+ headers: t,
166
+ withCredentials: y,
167
+ register: F
168
+ });
169
+ if (i.status !== 201) throw p(i, "Criação do upload recusada.");
170
+ let a = i.header("Location");
171
+ if (!a) throw p(i, "Criação do upload sem cabeçalho Location.");
172
+ return D = m(a), await I(), D;
173
+ }
174
+ async function z(e, t) {
175
+ if (t.needed && (E = await L(e), t.needed = !1, N(E), await I(), E >= f.size)) return;
176
+ let n = Math.min(E + h, f.size), r = E, i = await u({
177
+ method: "PATCH",
178
+ url: e,
179
+ headers: {
180
+ ...P(),
181
+ "Content-Type": "application/offset+octet-stream",
182
+ "Upload-Offset": String(r)
183
+ },
184
+ body: f.slice(r, n),
185
+ withCredentials: y,
186
+ onProgress: (e) => N(Math.min(r + e, f.size)),
187
+ register: F
188
+ });
189
+ if (i.status === 409 || i.status === 412) throw t.needed = !0, p(i, "Offset divergente — o servidor já tinha esses bytes.");
190
+ if (i.status !== 204 && i.status !== 200) throw p(i, "Chunk recusado pelo servidor.");
191
+ E = d(i) ?? n, N(E), await I();
192
+ }
193
+ async function B() {
194
+ k = null;
195
+ let e = await R();
196
+ j = E, M("uploading"), N(E);
197
+ let t = { needed: !1 };
198
+ for (; E < f.size && !k;) await r(() => z(e, t), {
199
+ retries: 5,
200
+ ...S,
201
+ shouldRetry: (e, n) => k || e instanceof DOMException && e.name === "AbortError" ? !1 : (t.needed = !0, S?.shouldRetry?.(e, n) ?? !0)
202
+ });
203
+ return k === "pause" ? (M("paused"), null) : k === "abort" ? (M("aborted"), null) : (M("done"), x && await x.delete(b), {
204
+ url: e,
205
+ size: f.size
206
+ });
207
+ }
208
+ async function V() {
209
+ try {
210
+ return await B();
211
+ } catch (e) {
212
+ if (k !== null || e instanceof DOMException && e.name === "AbortError") return M(k === "abort" ? "aborted" : "paused"), null;
213
+ throw M("error"), e;
214
+ } finally {
215
+ A = null;
216
+ }
217
+ }
218
+ function H(e) {
219
+ k = e, A?.abort(), A = null;
220
+ }
221
+ return {
222
+ start: V,
223
+ resume: V,
224
+ pause: () => H("pause"),
225
+ abort: async ({ discard: e = !1 } = {}) => {
226
+ H("abort"), M("aborted"), e && (D && await u({
227
+ method: "DELETE",
228
+ url: D,
229
+ headers: P(),
230
+ withCredentials: y,
231
+ register: () => void 0
232
+ }).catch(() => void 0), x && await x.delete(b));
233
+ },
234
+ get state() {
235
+ return T;
236
+ },
237
+ get offset() {
238
+ return E;
239
+ },
240
+ get url() {
241
+ return D;
242
+ },
243
+ key: b
244
+ };
245
+ }
246
+ //#endregion
247
+ export { a as DEFAULT_CHUNK_SIZE, i as TUS_VERSION, l as createLocalUploadStorage, h as createResumableUpload, c as uploadFingerprint };
248
+
249
+ //# sourceMappingURL=resumable-upload.js.map