tempest-react-sdk 0.57.0 → 0.58.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 (316) hide show
  1. package/bin/lib/icons/generate.test.mjs +4 -4
  2. package/dist/br/geocode.cjs +1 -1
  3. package/dist/br/geocode.cjs.map +1 -1
  4. package/dist/br/geocode.js +1 -1
  5. package/dist/br/geocode.js.map +1 -1
  6. package/dist/capture/use-barcode-scanner.cjs +1 -1
  7. package/dist/capture/use-barcode-scanner.cjs.map +1 -1
  8. package/dist/capture/use-barcode-scanner.js +1 -1
  9. package/dist/capture/use-barcode-scanner.js.map +1 -1
  10. package/dist/components/AIChat/AIChat.cjs +1 -1
  11. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  12. package/dist/components/AIChat/AIChat.js +1 -1
  13. package/dist/components/AIChat/AIChat.js.map +1 -1
  14. package/dist/components/AIChat/AIChatTurn.cjs +1 -1
  15. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  16. package/dist/components/AIChat/AIChatTurn.js +1 -1
  17. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  18. package/dist/components/AppBar/use-sticky-body-warning.cjs +1 -1
  19. package/dist/components/AppBar/use-sticky-body-warning.cjs.map +1 -1
  20. package/dist/components/AppBar/use-sticky-body-warning.js +1 -1
  21. package/dist/components/AppBar/use-sticky-body-warning.js.map +1 -1
  22. package/dist/components/AudioPlayer/AudioPlayer.cjs +1 -1
  23. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  24. package/dist/components/AudioPlayer/AudioPlayer.js +2 -2
  25. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
  26. package/dist/components/AudioRecorder/AudioRecorder.cjs +1 -1
  27. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
  28. package/dist/components/AudioRecorder/AudioRecorder.js +1 -1
  29. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
  30. package/dist/components/Chat/Chat.cjs +1 -1
  31. package/dist/components/Chat/Chat.cjs.map +1 -1
  32. package/dist/components/Chat/Chat.js +1 -1
  33. package/dist/components/Chat/Chat.js.map +1 -1
  34. package/dist/components/FilterBar/FilterBar.cjs +1 -1
  35. package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
  36. package/dist/components/FilterBar/FilterBar.js +1 -1
  37. package/dist/components/FilterBar/FilterBar.js.map +1 -1
  38. package/dist/components/FilterBar/filter-apply.cjs +1 -1
  39. package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
  40. package/dist/components/FilterBar/filter-apply.js +1 -1
  41. package/dist/components/FilterBar/filter-apply.js.map +1 -1
  42. package/dist/components/ImageCropper/ImageCropper.cjs +1 -1
  43. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
  44. package/dist/components/ImageCropper/ImageCropper.js +1 -1
  45. package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
  46. package/dist/components/VideoPlayer/VideoPlayer.cjs +1 -1
  47. package/dist/components/VideoPlayer/VideoPlayer.cjs.map +1 -1
  48. package/dist/components/VideoPlayer/VideoPlayer.js +2 -2
  49. package/dist/components/VideoPlayer/VideoPlayer.js.map +1 -1
  50. package/dist/components/VirtualTable/VirtualTable.cjs +1 -1
  51. package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
  52. package/dist/components/VirtualTable/VirtualTable.js +1 -1
  53. package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
  54. package/dist/feature-flags/growthbook-adapter.cjs +1 -1
  55. package/dist/feature-flags/growthbook-adapter.cjs.map +1 -1
  56. package/dist/feature-flags/growthbook-adapter.js +1 -1
  57. package/dist/feature-flags/growthbook-adapter.js.map +1 -1
  58. package/dist/hooks/use-focus-trap.cjs +1 -1
  59. package/dist/hooks/use-focus-trap.cjs.map +1 -1
  60. package/dist/hooks/use-focus-trap.js +1 -1
  61. package/dist/hooks/use-focus-trap.js.map +1 -1
  62. package/dist/hooks/use-push-to-talk.cjs +1 -1
  63. package/dist/hooks/use-push-to-talk.cjs.map +1 -1
  64. package/dist/hooks/use-push-to-talk.js +1 -1
  65. package/dist/hooks/use-push-to-talk.js.map +1 -1
  66. package/dist/http/resumable-upload.cjs +1 -1
  67. package/dist/http/resumable-upload.cjs.map +1 -1
  68. package/dist/http/resumable-upload.js +1 -1
  69. package/dist/http/resumable-upload.js.map +1 -1
  70. package/dist/icons/generated/aliases.cjs +1 -1
  71. package/dist/icons/generated/aliases.cjs.map +1 -1
  72. package/dist/icons/generated/aliases.js +1 -0
  73. package/dist/icons/generated/aliases.js.map +1 -1
  74. package/dist/icons/generated/icon-names.cjs +1 -1
  75. package/dist/icons/generated/icon-names.cjs.map +1 -1
  76. package/dist/icons/generated/icon-names.js +1 -1
  77. package/dist/icons/generated/icon-names.js.map +1 -1
  78. package/dist/icons/generated/loaders.cjs +1 -1
  79. package/dist/icons/generated/loaders.cjs.map +1 -1
  80. package/dist/icons/generated/loaders.js +48 -42
  81. package/dist/icons/generated/loaders.js.map +1 -1
  82. package/dist/icons/generated/shard-02.cjs +1 -1
  83. package/dist/icons/generated/shard-02.cjs.map +1 -1
  84. package/dist/icons/generated/shard-02.js +20 -20
  85. package/dist/icons/generated/shard-02.js.map +1 -1
  86. package/dist/icons/generated/shard-03.cjs +1 -1
  87. package/dist/icons/generated/shard-03.cjs.map +1 -1
  88. package/dist/icons/generated/shard-03.js +41 -41
  89. package/dist/icons/generated/shard-03.js.map +1 -1
  90. package/dist/icons/generated/shard-04.cjs +1 -1
  91. package/dist/icons/generated/shard-04.cjs.map +1 -1
  92. package/dist/icons/generated/shard-04.js +41 -41
  93. package/dist/icons/generated/shard-04.js.map +1 -1
  94. package/dist/icons/generated/shard-05.cjs +1 -1
  95. package/dist/icons/generated/shard-05.cjs.map +1 -1
  96. package/dist/icons/generated/shard-05.js +41 -41
  97. package/dist/icons/generated/shard-05.js.map +1 -1
  98. package/dist/icons/generated/shard-06.cjs +1 -1
  99. package/dist/icons/generated/shard-06.cjs.map +1 -1
  100. package/dist/icons/generated/shard-06.js +41 -41
  101. package/dist/icons/generated/shard-06.js.map +1 -1
  102. package/dist/icons/generated/shard-07.cjs +1 -1
  103. package/dist/icons/generated/shard-07.cjs.map +1 -1
  104. package/dist/icons/generated/shard-07.js +41 -41
  105. package/dist/icons/generated/shard-07.js.map +1 -1
  106. package/dist/icons/generated/shard-08.cjs +1 -1
  107. package/dist/icons/generated/shard-08.cjs.map +1 -1
  108. package/dist/icons/generated/shard-08.js +41 -41
  109. package/dist/icons/generated/shard-08.js.map +1 -1
  110. package/dist/icons/generated/shard-09.cjs +1 -1
  111. package/dist/icons/generated/shard-09.cjs.map +1 -1
  112. package/dist/icons/generated/shard-09.js +41 -41
  113. package/dist/icons/generated/shard-09.js.map +1 -1
  114. package/dist/icons/generated/shard-10.cjs +1 -1
  115. package/dist/icons/generated/shard-10.cjs.map +1 -1
  116. package/dist/icons/generated/shard-10.js +41 -41
  117. package/dist/icons/generated/shard-10.js.map +1 -1
  118. package/dist/icons/generated/shard-11.cjs +1 -1
  119. package/dist/icons/generated/shard-11.cjs.map +1 -1
  120. package/dist/icons/generated/shard-11.js +41 -41
  121. package/dist/icons/generated/shard-11.js.map +1 -1
  122. package/dist/icons/generated/shard-12.cjs +1 -1
  123. package/dist/icons/generated/shard-12.cjs.map +1 -1
  124. package/dist/icons/generated/shard-12.js +41 -41
  125. package/dist/icons/generated/shard-12.js.map +1 -1
  126. package/dist/icons/generated/shard-13.cjs +1 -1
  127. package/dist/icons/generated/shard-13.cjs.map +1 -1
  128. package/dist/icons/generated/shard-13.js +41 -41
  129. package/dist/icons/generated/shard-13.js.map +1 -1
  130. package/dist/icons/generated/shard-14.cjs +1 -1
  131. package/dist/icons/generated/shard-14.cjs.map +1 -1
  132. package/dist/icons/generated/shard-14.js +41 -41
  133. package/dist/icons/generated/shard-14.js.map +1 -1
  134. package/dist/icons/generated/shard-15.cjs +1 -1
  135. package/dist/icons/generated/shard-15.cjs.map +1 -1
  136. package/dist/icons/generated/shard-15.js +41 -41
  137. package/dist/icons/generated/shard-15.js.map +1 -1
  138. package/dist/icons/generated/shard-16.cjs +1 -1
  139. package/dist/icons/generated/shard-16.cjs.map +1 -1
  140. package/dist/icons/generated/shard-16.js +41 -41
  141. package/dist/icons/generated/shard-16.js.map +1 -1
  142. package/dist/icons/generated/shard-17.cjs +1 -1
  143. package/dist/icons/generated/shard-17.cjs.map +1 -1
  144. package/dist/icons/generated/shard-17.js +41 -41
  145. package/dist/icons/generated/shard-17.js.map +1 -1
  146. package/dist/icons/generated/shard-18.cjs +1 -1
  147. package/dist/icons/generated/shard-18.cjs.map +1 -1
  148. package/dist/icons/generated/shard-18.js +41 -41
  149. package/dist/icons/generated/shard-18.js.map +1 -1
  150. package/dist/icons/generated/shard-19.cjs +1 -1
  151. package/dist/icons/generated/shard-19.cjs.map +1 -1
  152. package/dist/icons/generated/shard-19.js +41 -41
  153. package/dist/icons/generated/shard-19.js.map +1 -1
  154. package/dist/icons/generated/shard-20.cjs +1 -1
  155. package/dist/icons/generated/shard-20.cjs.map +1 -1
  156. package/dist/icons/generated/shard-20.js +41 -41
  157. package/dist/icons/generated/shard-20.js.map +1 -1
  158. package/dist/icons/generated/shard-21.cjs +1 -1
  159. package/dist/icons/generated/shard-21.cjs.map +1 -1
  160. package/dist/icons/generated/shard-21.js +41 -41
  161. package/dist/icons/generated/shard-21.js.map +1 -1
  162. package/dist/icons/generated/shard-22.cjs +1 -1
  163. package/dist/icons/generated/shard-22.cjs.map +1 -1
  164. package/dist/icons/generated/shard-22.js +41 -41
  165. package/dist/icons/generated/shard-22.js.map +1 -1
  166. package/dist/icons/generated/shard-23.cjs +1 -1
  167. package/dist/icons/generated/shard-23.cjs.map +1 -1
  168. package/dist/icons/generated/shard-23.js +41 -41
  169. package/dist/icons/generated/shard-23.js.map +1 -1
  170. package/dist/icons/generated/shard-24.cjs +1 -1
  171. package/dist/icons/generated/shard-24.cjs.map +1 -1
  172. package/dist/icons/generated/shard-24.js +41 -41
  173. package/dist/icons/generated/shard-24.js.map +1 -1
  174. package/dist/icons/generated/shard-25.cjs +1 -1
  175. package/dist/icons/generated/shard-25.cjs.map +1 -1
  176. package/dist/icons/generated/shard-25.js +41 -41
  177. package/dist/icons/generated/shard-25.js.map +1 -1
  178. package/dist/icons/generated/shard-26.cjs +1 -1
  179. package/dist/icons/generated/shard-26.cjs.map +1 -1
  180. package/dist/icons/generated/shard-26.js +41 -41
  181. package/dist/icons/generated/shard-26.js.map +1 -1
  182. package/dist/icons/generated/shard-27.cjs +1 -1
  183. package/dist/icons/generated/shard-27.cjs.map +1 -1
  184. package/dist/icons/generated/shard-27.js +41 -41
  185. package/dist/icons/generated/shard-27.js.map +1 -1
  186. package/dist/icons/generated/shard-28.cjs +1 -1
  187. package/dist/icons/generated/shard-28.cjs.map +1 -1
  188. package/dist/icons/generated/shard-28.js +41 -41
  189. package/dist/icons/generated/shard-28.js.map +1 -1
  190. package/dist/icons/generated/shard-29.cjs +1 -1
  191. package/dist/icons/generated/shard-29.cjs.map +1 -1
  192. package/dist/icons/generated/shard-29.js +41 -41
  193. package/dist/icons/generated/shard-29.js.map +1 -1
  194. package/dist/icons/generated/shard-30.cjs +1 -1
  195. package/dist/icons/generated/shard-30.cjs.map +1 -1
  196. package/dist/icons/generated/shard-30.js +41 -41
  197. package/dist/icons/generated/shard-30.js.map +1 -1
  198. package/dist/icons/generated/shard-31.cjs +1 -1
  199. package/dist/icons/generated/shard-31.cjs.map +1 -1
  200. package/dist/icons/generated/shard-31.js +41 -41
  201. package/dist/icons/generated/shard-31.js.map +1 -1
  202. package/dist/icons/generated/shard-32.cjs +1 -1
  203. package/dist/icons/generated/shard-32.cjs.map +1 -1
  204. package/dist/icons/generated/shard-32.js +41 -41
  205. package/dist/icons/generated/shard-32.js.map +1 -1
  206. package/dist/icons/generated/shard-33.cjs +1 -1
  207. package/dist/icons/generated/shard-33.cjs.map +1 -1
  208. package/dist/icons/generated/shard-33.js +41 -41
  209. package/dist/icons/generated/shard-33.js.map +1 -1
  210. package/dist/icons/generated/shard-34.cjs +1 -1
  211. package/dist/icons/generated/shard-34.cjs.map +1 -1
  212. package/dist/icons/generated/shard-34.js +41 -41
  213. package/dist/icons/generated/shard-34.js.map +1 -1
  214. package/dist/icons/generated/shard-35.cjs +1 -1
  215. package/dist/icons/generated/shard-35.cjs.map +1 -1
  216. package/dist/icons/generated/shard-35.js +41 -41
  217. package/dist/icons/generated/shard-35.js.map +1 -1
  218. package/dist/icons/generated/shard-36.cjs +1 -1
  219. package/dist/icons/generated/shard-36.cjs.map +1 -1
  220. package/dist/icons/generated/shard-36.js +41 -41
  221. package/dist/icons/generated/shard-36.js.map +1 -1
  222. package/dist/icons/generated/shard-37.cjs +1 -1
  223. package/dist/icons/generated/shard-37.cjs.map +1 -1
  224. package/dist/icons/generated/shard-37.js +41 -41
  225. package/dist/icons/generated/shard-37.js.map +1 -1
  226. package/dist/icons/generated/shard-38.cjs +1 -1
  227. package/dist/icons/generated/shard-38.cjs.map +1 -1
  228. package/dist/icons/generated/shard-38.js +41 -41
  229. package/dist/icons/generated/shard-38.js.map +1 -1
  230. package/dist/icons/generated/shard-39.cjs +1 -1
  231. package/dist/icons/generated/shard-39.cjs.map +1 -1
  232. package/dist/icons/generated/shard-39.js +41 -41
  233. package/dist/icons/generated/shard-39.js.map +1 -1
  234. package/dist/icons/generated/shard-40.cjs +1 -1
  235. package/dist/icons/generated/shard-40.cjs.map +1 -1
  236. package/dist/icons/generated/shard-40.js +41 -41
  237. package/dist/icons/generated/shard-40.js.map +1 -1
  238. package/dist/icons/generated/shard-41.cjs +1 -1
  239. package/dist/icons/generated/shard-41.cjs.map +1 -1
  240. package/dist/icons/generated/shard-41.js +41 -41
  241. package/dist/icons/generated/shard-41.js.map +1 -1
  242. package/dist/icons/generated/shard-42.cjs +1 -1
  243. package/dist/icons/generated/shard-42.cjs.map +1 -1
  244. package/dist/icons/generated/shard-42.js +41 -41
  245. package/dist/icons/generated/shard-42.js.map +1 -1
  246. package/dist/icons/generated/shard-43.cjs +1 -1
  247. package/dist/icons/generated/shard-43.cjs.map +1 -1
  248. package/dist/icons/generated/shard-43.js +41 -41
  249. package/dist/icons/generated/shard-43.js.map +1 -1
  250. package/dist/icons/generated/shard-44.cjs +1 -1
  251. package/dist/icons/generated/shard-44.cjs.map +1 -1
  252. package/dist/icons/generated/shard-44.js +43 -10
  253. package/dist/icons/generated/shard-44.js.map +1 -1
  254. package/dist/icons/generated/shard-45.cjs +2 -0
  255. package/dist/icons/generated/shard-45.cjs.map +1 -0
  256. package/dist/icons/generated/shard-45.js +15 -0
  257. package/dist/icons/generated/shard-45.js.map +1 -0
  258. package/dist/icons/material-symbols.cjs +1 -1
  259. package/dist/icons/material-symbols.cjs.map +1 -1
  260. package/dist/icons/material-symbols.js +3 -3
  261. package/dist/icons/material-symbols.js.map +1 -1
  262. package/dist/icons.d.ts +4 -4
  263. package/dist/query/foreign-client-warning.cjs +1 -1
  264. package/dist/query/foreign-client-warning.cjs.map +1 -1
  265. package/dist/query/foreign-client-warning.js +1 -1
  266. package/dist/query/foreign-client-warning.js.map +1 -1
  267. package/dist/utils/json-frame.cjs +1 -1
  268. package/dist/utils/json-frame.cjs.map +1 -1
  269. package/dist/utils/json-frame.js +1 -1
  270. package/dist/utils/json-frame.js.map +1 -1
  271. package/dist/vision/core/providers.cjs +1 -1
  272. package/dist/vision/core/providers.cjs.map +1 -1
  273. package/dist/vision/core/providers.js +22 -3
  274. package/dist/vision/core/providers.js.map +1 -1
  275. package/dist/vision/core/session.cjs +1 -1
  276. package/dist/vision/core/session.cjs.map +1 -1
  277. package/dist/vision/core/session.js +22 -15
  278. package/dist/vision/core/session.js.map +1 -1
  279. package/dist/vision/fusion.cjs.map +1 -1
  280. package/dist/vision/fusion.js.map +1 -1
  281. package/dist/vision/index.cjs +1 -1
  282. package/dist/vision/index.cjs.map +1 -1
  283. package/dist/vision/index.js +17 -16
  284. package/dist/vision/index.js.map +1 -1
  285. package/dist/vision/normalization.cjs +2 -0
  286. package/dist/vision/normalization.cjs.map +1 -0
  287. package/dist/vision/normalization.js +49 -0
  288. package/dist/vision/normalization.js.map +1 -0
  289. package/dist/vision/results.cjs +1 -1
  290. package/dist/vision/results.cjs.map +1 -1
  291. package/dist/vision/results.js +8 -2
  292. package/dist/vision/results.js.map +1 -1
  293. package/dist/vision/tasks/classifier.cjs +1 -1
  294. package/dist/vision/tasks/classifier.cjs.map +1 -1
  295. package/dist/vision/tasks/classifier.js +36 -32
  296. package/dist/vision/tasks/classifier.js.map +1 -1
  297. package/dist/vision.cjs +1 -1
  298. package/dist/vision.d.ts +218 -13
  299. package/dist/vision.js +19 -18
  300. package/dist/vite/tempest-pwa-manifest.cjs +1 -1
  301. package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
  302. package/dist/vite/tempest-pwa-manifest.js +1 -1
  303. package/dist/vite/tempest-pwa-manifest.js.map +1 -1
  304. package/dist/webrtc/peer-link.cjs +1 -1
  305. package/dist/webrtc/peer-link.cjs.map +1 -1
  306. package/dist/webrtc/peer-link.js +1 -1
  307. package/dist/webrtc/peer-link.js.map +1 -1
  308. package/dist/webrtc/peer-mesh.cjs +1 -1
  309. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  310. package/dist/webrtc/peer-mesh.js +1 -1
  311. package/dist/webrtc/peer-mesh.js.map +1 -1
  312. package/dist/ws/create-web-socket.cjs +1 -1
  313. package/dist/ws/create-web-socket.cjs.map +1 -1
  314. package/dist/ws/create-web-socket.js +1 -1
  315. package/dist/ws/create-web-socket.js.map +1 -1
  316. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("../../utils/compare-values.cjs"),n=require("./VirtualTable.module.cjs");let r=require("react"),i=require("react/jsx-runtime");function a(e){if(e===`right`)return n.default.alignRight;if(e===`center`)return n.default.alignCenter}function o({row:t,index:r,rowHeight:o,columns:s,onRowClick:c}){return(0,i.jsx)(`tr`,{"aria-rowindex":r+1,className:e.cn(n.default.tr,c&&n.default.clickable),style:{height:o},tabIndex:c?0:void 0,onClick:c?()=>c(t,r):void 0,onKeyDown:c?e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),c(t,r))}:void 0,children:s.map(o=>(0,i.jsx)(`td`,{className:e.cn(n.default.td,a(o.align)),style:{width:o.width},children:o.render?o.render(t,r):t[o.key]??null},String(o.key)))})}var s=(0,r.memo)(o);function c({data:o,columns:c,rowHeight:l,height:u,overscan:d=4,rowKey:f,initialSort:p,onRowClick:m,emptyMessage:h=`Nenhum registro encontrado.`,caption:g,scrollToIndex:_,className:v,...y}){let b=(0,r.useRef)(null),[x,S]=(0,r.useState)(0),[C,w]=(0,r.useState)(0),[T,E]=(0,r.useState)(p??null);(0,r.useEffect)(()=>{let e=b.current;if(!e||(w(e.clientHeight),typeof ResizeObserver>`u`))return;let t=new ResizeObserver(()=>w(e.clientHeight));return t.observe(e),()=>t.disconnect()},[]),(0,r.useEffect)(()=>{let e=b.current;!e||_==null||(e.scrollTop=Math.max(0,Math.min(_,o.length-1))*l)},[_,l,o.length]);let D=(0,r.useMemo)(()=>{if(!T)return o;let e=T.direction===`asc`?1:-1;return[...o].sort((n,r)=>t.compareValues(n[T.key],r[T.key])*e)},[o,T]),O=Math.max(0,Math.floor(x/l)-d),k=Math.ceil(C/l)+d*2,A=Math.min(D.length,O+k),j=D.slice(O,A),M=(0,r.useCallback)(e=>{E(t=>!t||t.key!==e?{key:e,direction:`asc`}:t.direction===`asc`?{key:e,direction:`desc`}:null);let t=b.current;t&&(t.scrollTop=0),S(0)},[]);return(0,i.jsx)(`div`,{ref:b,className:e.cn(n.default.scroll,v),style:{height:u},onScroll:e=>S(e.currentTarget.scrollTop),...y,children:(0,i.jsxs)(`table`,{className:n.default.table,"aria-rowcount":D.length,children:[g?(0,i.jsx)(`caption`,{className:n.default.caption,children:g}):null,(0,i.jsx)(`thead`,{className:n.default.head,children:(0,i.jsx)(`tr`,{children:c.map(t=>{let r=T?.key===t.key;return(0,i.jsx)(`th`,{scope:`col`,className:e.cn(n.default.th,a(t.align)),style:{width:t.width},"aria-sort":r?T?.direction===`asc`?`ascending`:`descending`:t.sortable?`none`:void 0,children:t.sortable?(0,i.jsxs)(`button`,{type:`button`,className:n.default.sortButton,onClick:()=>M(t.key),children:[t.header,(0,i.jsx)(`span`,{className:n.default.sortIndicator,"aria-hidden":!0,children:r?T?.direction===`asc`?`▲`:`▼`:`↕`})]}):t.header},String(t.key))})})}),(0,i.jsx)(`tbody`,{children:D.length===0?(0,i.jsx)(`tr`,{children:(0,i.jsx)(`td`,{className:n.default.emptyRow,colSpan:c.length,children:h})}):(0,i.jsxs)(i.Fragment,{children:[O>0&&(0,i.jsx)(`tr`,{"aria-hidden":!0,style:{height:O*l}}),j.map((e,t)=>{let n=O+t;return(0,i.jsx)(s,{row:e,index:n,rowHeight:l,columns:c,onRowClick:m},f?f(e,n):n)}),A<D.length&&(0,i.jsx)(`tr`,{"aria-hidden":!0,style:{height:(D.length-A)*l}})]})})]})})}exports.VirtualTable=c;
1
+ const e=require("../../utils/cn.cjs"),t=require("../../utils/compare-values.cjs"),n=require("./VirtualTable.module.cjs");let r=require("react"),i=require("react/jsx-runtime");function a(e){if(e===`right`)return n.default.alignRight;if(e===`center`)return n.default.alignCenter}function o({row:t,index:r,rowHeight:o,columns:s,onRowClick:c}){return(0,i.jsx)(`tr`,{"aria-rowindex":r+1,className:e.cn(n.default.tr,c&&n.default.clickable),style:{height:o},tabIndex:c?0:void 0,onClick:c?()=>c(t,r):void 0,onKeyDown:c?e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),c(t,r))}:void 0,children:s.map(o=>(0,i.jsx)(`td`,{className:e.cn(n.default.td,a(o.align)),style:{width:o.width},children:o.render?o.render(t,r):t[o.key]??null},String(o.key)))})}var s=(0,r.memo)(o);function c({data:o,columns:c,rowHeight:l,height:u,overscan:d=4,rowKey:f,initialSort:p,onRowClick:m,emptyMessage:h=`Nenhum registro encontrado.`,caption:g,scrollToIndex:_,className:v,...y}){let b=(0,r.useRef)(null),[x,S]=(0,r.useState)(0),[C,w]=(0,r.useState)(0),[T,E]=(0,r.useState)(p??null);(0,r.useEffect)(()=>{let e=b.current;if(!e||(w(e.clientHeight),typeof ResizeObserver>`u`))return;let t=new ResizeObserver(()=>w(e.clientHeight));return t.observe(e),()=>t.disconnect()},[]),(0,r.useEffect)(()=>{let e=b.current;e&&_!=null&&(e.scrollTop=Math.max(0,Math.min(_,o.length-1))*l)},[_,l,o.length]);let D=(0,r.useMemo)(()=>{if(!T)return o;let e=T.direction===`asc`?1:-1;return[...o].sort((n,r)=>t.compareValues(n[T.key],r[T.key])*e)},[o,T]),O=Math.max(0,Math.floor(x/l)-d),k=Math.ceil(C/l)+d*2,A=Math.min(D.length,O+k),j=D.slice(O,A),M=(0,r.useCallback)(e=>{E(t=>!t||t.key!==e?{key:e,direction:`asc`}:t.direction===`asc`?{key:e,direction:`desc`}:null);let t=b.current;t&&(t.scrollTop=0),S(0)},[]);return(0,i.jsx)(`div`,{ref:b,className:e.cn(n.default.scroll,v),style:{height:u},onScroll:e=>S(e.currentTarget.scrollTop),...y,children:(0,i.jsxs)(`table`,{className:n.default.table,"aria-rowcount":D.length,children:[g?(0,i.jsx)(`caption`,{className:n.default.caption,children:g}):null,(0,i.jsx)(`thead`,{className:n.default.head,children:(0,i.jsx)(`tr`,{children:c.map(t=>{let r=T?.key===t.key;return(0,i.jsx)(`th`,{scope:`col`,className:e.cn(n.default.th,a(t.align)),style:{width:t.width},"aria-sort":r?T?.direction===`asc`?`ascending`:`descending`:t.sortable?`none`:void 0,children:t.sortable?(0,i.jsxs)(`button`,{type:`button`,className:n.default.sortButton,onClick:()=>M(t.key),children:[t.header,(0,i.jsx)(`span`,{className:n.default.sortIndicator,"aria-hidden":!0,children:r?T?.direction===`asc`?`▲`:`▼`:`↕`})]}):t.header},String(t.key))})})}),(0,i.jsx)(`tbody`,{children:D.length===0?(0,i.jsx)(`tr`,{children:(0,i.jsx)(`td`,{className:n.default.emptyRow,colSpan:c.length,children:h})}):(0,i.jsxs)(i.Fragment,{children:[O>0&&(0,i.jsx)(`tr`,{"aria-hidden":!0,style:{height:O*l}}),j.map((e,t)=>{let n=O+t;return(0,i.jsx)(s,{row:e,index:n,rowHeight:l,columns:c,onRowClick:m},f?f(e,n):n)}),A<D.length&&(0,i.jsx)(`tr`,{"aria-hidden":!0,style:{height:(D.length-A)*l}})]})})]})})}exports.VirtualTable=c;
2
2
  //# sourceMappingURL=VirtualTable.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"VirtualTable.cjs","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count — windowing needs its own measurements\n * (rowHeight, height, overscan, scrollToIndex) on top of everything a table already\n * takes (data, columns, rowKey, initialSort, onRowClick, emptyMessage, caption). The\n * caption is not optional chrome — a virtualised grid without one is unreadable to a\n * screen reader.\n */\nimport {\n type HTMLAttributes,\n type ReactNode,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\ninterface VirtualTableRowProps<T> {\n row: T;\n index: number;\n rowHeight: number;\n columns: VirtualTableColumn<T>[];\n onRowClick: ((row: T, index: number) => void) | undefined;\n}\n\n/**\n * One windowed table row, rendered on its own so React can skip it.\n *\n * Scrolling changes state that lives inside `VirtualTable`, so without this\n * boundary every scroll event re-rendered every visible row and every cell in\n * them — including whatever `column.render` builds — rather than the one row\n * that entered the window.\n *\n * `columns` and `onRowClick` take part in the comparison because a cell renderer\n * closes over the app's state: leaving them out would freeze a row showing a\n * stale cell. An app that keeps its `columns` array stable (a module constant, or\n * `useMemo`) therefore gets the skip, and one that rebuilds it every render gets\n * exactly today's behaviour.\n *\n * @param props - The row, its position and the column contract.\n * @returns The table row element.\n */\nfunction VirtualTableRowImpl<T>({\n row,\n index,\n rowHeight,\n columns,\n onRowClick,\n}: VirtualTableRowProps<T>) {\n return (\n <tr\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={onRowClick ? () => onRowClick(row, index) : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n}\n\n/**\n * `memo` erases the generic, so the memoised component is re-typed as the\n * function it wraps. The runtime value is unchanged; only the signature is\n * restored.\n */\nconst VirtualTableRow = memo(VirtualTableRowImpl) as typeof VirtualTableRowImpl;\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <VirtualTableRow\n key={rowKey ? rowKey(row, index) : index}\n row={row}\n index={index}\n rowHeight={rowHeight}\n columns={columns}\n onRowClick={onRowClick}\n />\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"VirtualTable.cjs","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count — windowing needs its own measurements\n * (rowHeight, height, overscan, scrollToIndex) on top of everything a table already\n * takes (data, columns, rowKey, initialSort, onRowClick, emptyMessage, caption). The\n * caption is not optional chrome — a virtualised grid without one is unreadable to a\n * screen reader.\n */\nimport {\n type HTMLAttributes,\n type ReactNode,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\ninterface VirtualTableRowProps<T> {\n row: T;\n index: number;\n rowHeight: number;\n columns: VirtualTableColumn<T>[];\n onRowClick: ((row: T, index: number) => void) | undefined;\n}\n\n/**\n * One windowed table row, rendered on its own so React can skip it.\n *\n * Scrolling changes state that lives inside `VirtualTable`, so without this\n * boundary every scroll event re-rendered every visible row and every cell in\n * them — including whatever `column.render` builds — rather than the one row\n * that entered the window.\n *\n * `columns` and `onRowClick` take part in the comparison because a cell renderer\n * closes over the app's state: leaving them out would freeze a row showing a\n * stale cell. An app that keeps its `columns` array stable (a module constant, or\n * `useMemo`) therefore gets the skip, and one that rebuilds it every render gets\n * exactly today's behaviour.\n *\n * @param props - The row, its position and the column contract.\n * @returns The table row element.\n */\nfunction VirtualTableRowImpl<T>({\n row,\n index,\n rowHeight,\n columns,\n onRowClick,\n}: VirtualTableRowProps<T>) {\n return (\n <tr\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={onRowClick ? () => onRowClick(row, index) : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n}\n\n/**\n * `memo` erases the generic, so the memoised component is re-typed as the\n * function it wraps. The runtime value is unchanged; only the signature is\n * restored.\n */\nconst VirtualTableRow = memo(VirtualTableRowImpl) as typeof VirtualTableRowImpl;\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <VirtualTableRow\n key={rowKey ? rowKey(row, index) : index}\n row={row}\n index={index}\n rowHeight={rowHeight}\n columns={columns}\n onRowClick={onRowClick}\n />\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -35,7 +35,7 @@ function h({ data: r, columns: p, rowHeight: h, height: g, overscan: _ = 4, rowK
35
35
  return t.observe(e), () => t.disconnect();
36
36
  }, []), a(() => {
37
37
  let e = E.current;
38
- !e || C == null || (e.scrollTop = Math.max(0, Math.min(C, r.length - 1)) * h);
38
+ e && C != null && (e.scrollTop = Math.max(0, Math.min(C, r.length - 1)) * h);
39
39
  }, [
40
40
  C,
41
41
  h,
@@ -1 +1 @@
1
- {"version":3,"file":"VirtualTable.js","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count — windowing needs its own measurements\n * (rowHeight, height, overscan, scrollToIndex) on top of everything a table already\n * takes (data, columns, rowKey, initialSort, onRowClick, emptyMessage, caption). The\n * caption is not optional chrome — a virtualised grid without one is unreadable to a\n * screen reader.\n */\nimport {\n type HTMLAttributes,\n type ReactNode,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\ninterface VirtualTableRowProps<T> {\n row: T;\n index: number;\n rowHeight: number;\n columns: VirtualTableColumn<T>[];\n onRowClick: ((row: T, index: number) => void) | undefined;\n}\n\n/**\n * One windowed table row, rendered on its own so React can skip it.\n *\n * Scrolling changes state that lives inside `VirtualTable`, so without this\n * boundary every scroll event re-rendered every visible row and every cell in\n * them — including whatever `column.render` builds — rather than the one row\n * that entered the window.\n *\n * `columns` and `onRowClick` take part in the comparison because a cell renderer\n * closes over the app's state: leaving them out would freeze a row showing a\n * stale cell. An app that keeps its `columns` array stable (a module constant, or\n * `useMemo`) therefore gets the skip, and one that rebuilds it every render gets\n * exactly today's behaviour.\n *\n * @param props - The row, its position and the column contract.\n * @returns The table row element.\n */\nfunction VirtualTableRowImpl<T>({\n row,\n index,\n rowHeight,\n columns,\n onRowClick,\n}: VirtualTableRowProps<T>) {\n return (\n <tr\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={onRowClick ? () => onRowClick(row, index) : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n}\n\n/**\n * `memo` erases the generic, so the memoised component is re-typed as the\n * function it wraps. The runtime value is unchanged; only the signature is\n * restored.\n */\nconst VirtualTableRow = memo(VirtualTableRowImpl) as typeof VirtualTableRowImpl;\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <VirtualTableRow\n key={rowKey ? rowKey(row, index) : index}\n row={row}\n index={index}\n rowHeight={rowHeight}\n columns={columns}\n onRowClick={onRowClick}\n />\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"VirtualTable.js","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count — windowing needs its own measurements\n * (rowHeight, height, overscan, scrollToIndex) on top of everything a table already\n * takes (data, columns, rowKey, initialSort, onRowClick, emptyMessage, caption). The\n * caption is not optional chrome — a virtualised grid without one is unreadable to a\n * screen reader.\n */\nimport {\n type HTMLAttributes,\n type ReactNode,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\ninterface VirtualTableRowProps<T> {\n row: T;\n index: number;\n rowHeight: number;\n columns: VirtualTableColumn<T>[];\n onRowClick: ((row: T, index: number) => void) | undefined;\n}\n\n/**\n * One windowed table row, rendered on its own so React can skip it.\n *\n * Scrolling changes state that lives inside `VirtualTable`, so without this\n * boundary every scroll event re-rendered every visible row and every cell in\n * them — including whatever `column.render` builds — rather than the one row\n * that entered the window.\n *\n * `columns` and `onRowClick` take part in the comparison because a cell renderer\n * closes over the app's state: leaving them out would freeze a row showing a\n * stale cell. An app that keeps its `columns` array stable (a module constant, or\n * `useMemo`) therefore gets the skip, and one that rebuilds it every render gets\n * exactly today's behaviour.\n *\n * @param props - The row, its position and the column contract.\n * @returns The table row element.\n */\nfunction VirtualTableRowImpl<T>({\n row,\n index,\n rowHeight,\n columns,\n onRowClick,\n}: VirtualTableRowProps<T>) {\n return (\n <tr\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={onRowClick ? () => onRowClick(row, index) : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n}\n\n/**\n * `memo` erases the generic, so the memoised component is re-typed as the\n * function it wraps. The runtime value is unchanged; only the signature is\n * restored.\n */\nconst VirtualTableRow = memo(VirtualTableRowImpl) as typeof VirtualTableRowImpl;\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <VirtualTableRow\n key={rowKey ? rowKey(row, index) : index}\n row={row}\n index={index}\n rowHeight={rowHeight}\n columns={columns}\n onRowClick={onRowClick}\n />\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- function e(e){let{growthbook:t}=e,n=new Set,r=!1;function i(){r||!t.setRenderer||(r=!0,t.setRenderer(()=>{for(let e of n)e()}))}return{isEnabled(e,n=!1){let r=t.isOn(e);return typeof r==`boolean`?r:n},get(e,n){return t.getFeatureValue(e,n)},onChange(e){return i(),n.add(e),()=>{n.delete(e)}}}}exports.createGrowthBookFeatureFlagsAdapter=e;
1
+ function e(e){let{growthbook:t}=e,n=new Set,r=!1;function i(){!r&&t.setRenderer&&(r=!0,t.setRenderer(()=>{for(let e of n)e()}))}return{isEnabled(e,n=!1){let r=t.isOn(e);return typeof r==`boolean`?r:n},get(e,n){return t.getFeatureValue(e,n)},onChange(e){return i(),n.add(e),()=>{n.delete(e)}}}}exports.createGrowthBookFeatureFlagsAdapter=e;
2
2
  //# sourceMappingURL=growthbook-adapter.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"growthbook-adapter.cjs","names":[],"sources":["../../src/feature-flags/growthbook-adapter.ts"],"sourcesContent":["import type { FeatureFlagsAdapter, FlagValue } from \"./types\";\n\n/**\n * Minimal subset of [`@growthbook/growthbook`](https://docs.growthbook.io/lib/js)\n * used by the adapter. Pass a `GrowthBook` instance directly.\n */\nexport interface GrowthBookLike {\n isOn: (key: string) => boolean;\n getFeatureValue: <T>(key: string, defaultValue: T) => T;\n /** Registers a renderer fired whenever GrowthBook re-evaluates features. */\n setRenderer?: (renderer: () => void) => void;\n}\n\nexport interface CreateGrowthBookFeatureFlagsAdapterOptions {\n /** The GrowthBook instance. Required. */\n growthbook: GrowthBookLike;\n}\n\n/**\n * Build a [[FeatureFlagsAdapter]] backed by a GrowthBook instance. Apps\n * initialise GrowthBook themselves (so they can load features over the\n * network, set attributes, etc.) — the adapter just routes lookups.\n *\n * Mapping:\n * - `isEnabled(key, default)` → `growthbook.isOn(key)` (falls back to `default` if no value)\n * - `get(key, default)` → `growthbook.getFeatureValue(key, default)`\n * - `onChange(listener)` → `growthbook.setRenderer(listener)` (single-renderer SDK constraint)\n *\n * @example\n * import { GrowthBook } from \"@growthbook/growthbook\";\n * import { FeatureFlagsProvider, createGrowthBookFeatureFlagsAdapter } from \"tempest-react-sdk\";\n *\n * const gb = new GrowthBook({ apiHost: \"...\", clientKey: \"...\" });\n * await gb.loadFeatures();\n * const adapter = createGrowthBookFeatureFlagsAdapter({ growthbook: gb });\n *\n * <FeatureFlagsProvider adapter={adapter}><App /></FeatureFlagsProvider>;\n */\nexport function createGrowthBookFeatureFlagsAdapter(\n options: CreateGrowthBookFeatureFlagsAdapterOptions,\n): FeatureFlagsAdapter {\n const { growthbook } = options;\n const listeners = new Set<() => void>();\n let rendererInstalled = false;\n\n function ensureRenderer(): void {\n if (rendererInstalled || !growthbook.setRenderer) return;\n rendererInstalled = true;\n growthbook.setRenderer(() => {\n for (const listener of listeners) listener();\n });\n }\n\n return {\n isEnabled(key: string, defaultValue = false) {\n const value = growthbook.isOn(key);\n if (typeof value === \"boolean\") return value;\n return defaultValue;\n },\n get<T extends FlagValue = FlagValue>(key: string, defaultValue?: T): T {\n return growthbook.getFeatureValue(key, defaultValue as T);\n },\n onChange(listener) {\n ensureRenderer();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"],"mappings":"AAsCA,SAAgB,EACZ,EACmB,CACnB,GAAM,CAAE,cAAe,EACjB,EAAY,IAAI,IAClB,EAAoB,GAExB,SAAS,GAAuB,CACxB,GAAqB,CAAC,EAAW,cACrC,EAAoB,GACpB,EAAW,gBAAkB,CACzB,IAAK,IAAM,KAAY,EAAW,EAAS,CAC/C,CAAC,EACL,CAEA,MAAO,CACH,UAAU,EAAa,EAAe,GAAO,CACzC,IAAM,EAAQ,EAAW,KAAK,CAAG,EAEjC,OADI,OAAO,GAAU,UAAkB,EAChC,CACX,EACA,IAAqC,EAAa,EAAqB,CACnE,OAAO,EAAW,gBAAgB,EAAK,CAAiB,CAC5D,EACA,SAAS,EAAU,CAGf,OAFA,EAAe,EACf,EAAU,IAAI,CAAQ,MACT,CACT,EAAU,OAAO,CAAQ,CAC7B,CACJ,CACJ,CACJ"}
1
+ {"version":3,"file":"growthbook-adapter.cjs","names":[],"sources":["../../src/feature-flags/growthbook-adapter.ts"],"sourcesContent":["import type { FeatureFlagsAdapter, FlagValue } from \"./types\";\n\n/**\n * Minimal subset of [`@growthbook/growthbook`](https://docs.growthbook.io/lib/js)\n * used by the adapter. Pass a `GrowthBook` instance directly.\n */\nexport interface GrowthBookLike {\n isOn: (key: string) => boolean;\n getFeatureValue: <T>(key: string, defaultValue: T) => T;\n /** Registers a renderer fired whenever GrowthBook re-evaluates features. */\n setRenderer?: (renderer: () => void) => void;\n}\n\nexport interface CreateGrowthBookFeatureFlagsAdapterOptions {\n /** The GrowthBook instance. Required. */\n growthbook: GrowthBookLike;\n}\n\n/**\n * Build a [[FeatureFlagsAdapter]] backed by a GrowthBook instance. Apps\n * initialise GrowthBook themselves (so they can load features over the\n * network, set attributes, etc.) — the adapter just routes lookups.\n *\n * Mapping:\n * - `isEnabled(key, default)` → `growthbook.isOn(key)` (falls back to `default` if no value)\n * - `get(key, default)` → `growthbook.getFeatureValue(key, default)`\n * - `onChange(listener)` → `growthbook.setRenderer(listener)` (single-renderer SDK constraint)\n *\n * @example\n * import { GrowthBook } from \"@growthbook/growthbook\";\n * import { FeatureFlagsProvider, createGrowthBookFeatureFlagsAdapter } from \"tempest-react-sdk\";\n *\n * const gb = new GrowthBook({ apiHost: \"...\", clientKey: \"...\" });\n * await gb.loadFeatures();\n * const adapter = createGrowthBookFeatureFlagsAdapter({ growthbook: gb });\n *\n * <FeatureFlagsProvider adapter={adapter}><App /></FeatureFlagsProvider>;\n */\nexport function createGrowthBookFeatureFlagsAdapter(\n options: CreateGrowthBookFeatureFlagsAdapterOptions,\n): FeatureFlagsAdapter {\n const { growthbook } = options;\n const listeners = new Set<() => void>();\n let rendererInstalled = false;\n\n function ensureRenderer(): void {\n if (rendererInstalled || !growthbook.setRenderer) return;\n rendererInstalled = true;\n growthbook.setRenderer(() => {\n for (const listener of listeners) listener();\n });\n }\n\n return {\n isEnabled(key: string, defaultValue = false) {\n const value = growthbook.isOn(key);\n if (typeof value === \"boolean\") return value;\n return defaultValue;\n },\n get<T extends FlagValue = FlagValue>(key: string, defaultValue?: T): T {\n return growthbook.getFeatureValue(key, defaultValue as T);\n },\n onChange(listener) {\n ensureRenderer();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"],"mappings":"AAsCA,SAAgB,EACZ,EACmB,CACnB,GAAM,CAAE,cAAe,EACjB,EAAY,IAAI,IAClB,EAAoB,GAExB,SAAS,GAAuB,CACxB,IAAsB,EAAW,cACrC,EAAoB,GACpB,EAAW,gBAAkB,CACzB,IAAK,IAAM,KAAY,EAAW,EAAS,CAC/C,CAAC,EACL,CAEA,MAAO,CACH,UAAU,EAAa,EAAe,GAAO,CACzC,IAAM,EAAQ,EAAW,KAAK,CAAG,EAEjC,OADI,OAAO,GAAU,UAAkB,EAChC,CACX,EACA,IAAqC,EAAa,EAAqB,CACnE,OAAO,EAAW,gBAAgB,EAAK,CAAiB,CAC5D,EACA,SAAS,EAAU,CAGf,OAFA,EAAe,EACf,EAAU,IAAI,CAAQ,MACT,CACT,EAAU,OAAO,CAAQ,CAC7B,CACJ,CACJ,CACJ"}
@@ -2,7 +2,7 @@
2
2
  function e(e) {
3
3
  let { growthbook: t } = e, n = /* @__PURE__ */ new Set(), r = !1;
4
4
  function i() {
5
- r || !t.setRenderer || (r = !0, t.setRenderer(() => {
5
+ !r && t.setRenderer && (r = !0, t.setRenderer(() => {
6
6
  for (let e of n) e();
7
7
  }));
8
8
  }
@@ -1 +1 @@
1
- {"version":3,"file":"growthbook-adapter.js","names":[],"sources":["../../src/feature-flags/growthbook-adapter.ts"],"sourcesContent":["import type { FeatureFlagsAdapter, FlagValue } from \"./types\";\n\n/**\n * Minimal subset of [`@growthbook/growthbook`](https://docs.growthbook.io/lib/js)\n * used by the adapter. Pass a `GrowthBook` instance directly.\n */\nexport interface GrowthBookLike {\n isOn: (key: string) => boolean;\n getFeatureValue: <T>(key: string, defaultValue: T) => T;\n /** Registers a renderer fired whenever GrowthBook re-evaluates features. */\n setRenderer?: (renderer: () => void) => void;\n}\n\nexport interface CreateGrowthBookFeatureFlagsAdapterOptions {\n /** The GrowthBook instance. Required. */\n growthbook: GrowthBookLike;\n}\n\n/**\n * Build a [[FeatureFlagsAdapter]] backed by a GrowthBook instance. Apps\n * initialise GrowthBook themselves (so they can load features over the\n * network, set attributes, etc.) — the adapter just routes lookups.\n *\n * Mapping:\n * - `isEnabled(key, default)` → `growthbook.isOn(key)` (falls back to `default` if no value)\n * - `get(key, default)` → `growthbook.getFeatureValue(key, default)`\n * - `onChange(listener)` → `growthbook.setRenderer(listener)` (single-renderer SDK constraint)\n *\n * @example\n * import { GrowthBook } from \"@growthbook/growthbook\";\n * import { FeatureFlagsProvider, createGrowthBookFeatureFlagsAdapter } from \"tempest-react-sdk\";\n *\n * const gb = new GrowthBook({ apiHost: \"...\", clientKey: \"...\" });\n * await gb.loadFeatures();\n * const adapter = createGrowthBookFeatureFlagsAdapter({ growthbook: gb });\n *\n * <FeatureFlagsProvider adapter={adapter}><App /></FeatureFlagsProvider>;\n */\nexport function createGrowthBookFeatureFlagsAdapter(\n options: CreateGrowthBookFeatureFlagsAdapterOptions,\n): FeatureFlagsAdapter {\n const { growthbook } = options;\n const listeners = new Set<() => void>();\n let rendererInstalled = false;\n\n function ensureRenderer(): void {\n if (rendererInstalled || !growthbook.setRenderer) return;\n rendererInstalled = true;\n growthbook.setRenderer(() => {\n for (const listener of listeners) listener();\n });\n }\n\n return {\n isEnabled(key: string, defaultValue = false) {\n const value = growthbook.isOn(key);\n if (typeof value === \"boolean\") return value;\n return defaultValue;\n },\n get<T extends FlagValue = FlagValue>(key: string, defaultValue?: T): T {\n return growthbook.getFeatureValue(key, defaultValue as T);\n },\n onChange(listener) {\n ensureRenderer();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"],"mappings":";AAsCA,SAAgB,EACZ,GACmB;CACnB,IAAM,EAAE,kBAAe,GACjB,oBAAY,IAAI,IAAgB,GAClC,IAAoB;CAExB,SAAS,IAAuB;EACxB,KAAqB,CAAC,EAAW,gBACrC,IAAoB,IACpB,EAAW,kBAAkB;GACzB,KAAK,IAAM,KAAY,GAAW,EAAS;EAC/C,CAAC;CACL;CAEA,OAAO;EACH,UAAU,GAAa,IAAe,IAAO;GACzC,IAAM,IAAQ,EAAW,KAAK,CAAG;GAEjC,OADI,OAAO,KAAU,YAAkB,IAChC;EACX;EACA,IAAqC,GAAa,GAAqB;GACnE,OAAO,EAAW,gBAAgB,GAAK,CAAiB;EAC5D;EACA,SAAS,GAAU;GAGf,OAFA,EAAe,GACf,EAAU,IAAI,CAAQ,SACT;IACT,EAAU,OAAO,CAAQ;GAC7B;EACJ;CACJ;AACJ"}
1
+ {"version":3,"file":"growthbook-adapter.js","names":[],"sources":["../../src/feature-flags/growthbook-adapter.ts"],"sourcesContent":["import type { FeatureFlagsAdapter, FlagValue } from \"./types\";\n\n/**\n * Minimal subset of [`@growthbook/growthbook`](https://docs.growthbook.io/lib/js)\n * used by the adapter. Pass a `GrowthBook` instance directly.\n */\nexport interface GrowthBookLike {\n isOn: (key: string) => boolean;\n getFeatureValue: <T>(key: string, defaultValue: T) => T;\n /** Registers a renderer fired whenever GrowthBook re-evaluates features. */\n setRenderer?: (renderer: () => void) => void;\n}\n\nexport interface CreateGrowthBookFeatureFlagsAdapterOptions {\n /** The GrowthBook instance. Required. */\n growthbook: GrowthBookLike;\n}\n\n/**\n * Build a [[FeatureFlagsAdapter]] backed by a GrowthBook instance. Apps\n * initialise GrowthBook themselves (so they can load features over the\n * network, set attributes, etc.) — the adapter just routes lookups.\n *\n * Mapping:\n * - `isEnabled(key, default)` → `growthbook.isOn(key)` (falls back to `default` if no value)\n * - `get(key, default)` → `growthbook.getFeatureValue(key, default)`\n * - `onChange(listener)` → `growthbook.setRenderer(listener)` (single-renderer SDK constraint)\n *\n * @example\n * import { GrowthBook } from \"@growthbook/growthbook\";\n * import { FeatureFlagsProvider, createGrowthBookFeatureFlagsAdapter } from \"tempest-react-sdk\";\n *\n * const gb = new GrowthBook({ apiHost: \"...\", clientKey: \"...\" });\n * await gb.loadFeatures();\n * const adapter = createGrowthBookFeatureFlagsAdapter({ growthbook: gb });\n *\n * <FeatureFlagsProvider adapter={adapter}><App /></FeatureFlagsProvider>;\n */\nexport function createGrowthBookFeatureFlagsAdapter(\n options: CreateGrowthBookFeatureFlagsAdapterOptions,\n): FeatureFlagsAdapter {\n const { growthbook } = options;\n const listeners = new Set<() => void>();\n let rendererInstalled = false;\n\n function ensureRenderer(): void {\n if (rendererInstalled || !growthbook.setRenderer) return;\n rendererInstalled = true;\n growthbook.setRenderer(() => {\n for (const listener of listeners) listener();\n });\n }\n\n return {\n isEnabled(key: string, defaultValue = false) {\n const value = growthbook.isOn(key);\n if (typeof value === \"boolean\") return value;\n return defaultValue;\n },\n get<T extends FlagValue = FlagValue>(key: string, defaultValue?: T): T {\n return growthbook.getFeatureValue(key, defaultValue as T);\n },\n onChange(listener) {\n ensureRenderer();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"],"mappings":";AAsCA,SAAgB,EACZ,GACmB;CACnB,IAAM,EAAE,kBAAe,GACjB,oBAAY,IAAI,IAAgB,GAClC,IAAoB;CAExB,SAAS,IAAuB;EACxB,MAAsB,EAAW,gBACrC,IAAoB,IACpB,EAAW,kBAAkB;GACzB,KAAK,IAAM,KAAY,GAAW,EAAS;EAC/C,CAAC;CACL;CAEA,OAAO;EACH,UAAU,GAAa,IAAe,IAAO;GACzC,IAAM,IAAQ,EAAW,KAAK,CAAG;GAEjC,OADI,OAAO,KAAU,YAAkB,IAChC;EACX;EACA,IAAqC,GAAa,GAAqB;GACnE,OAAO,EAAW,gBAAgB,GAAK,CAAiB;EAC5D;EACA,SAAS,GAAU;GAGf,OAFA,EAAe,GACf,EAAU,IAAI,CAAQ,SACT;IACT,EAAU,OAAO,CAAQ;GAC7B;EACJ;CACJ;AACJ"}
@@ -1,2 +1,2 @@
1
- let e=require("react");var t=[`a[href]`,`button:not([disabled])`,`textarea:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`[tabindex]:not([tabindex='-1'])`].join(`,`);function n(n,r){(0,e.useEffect)(()=>{if(!r)return;let e=n.current;if(!e)return;let i=document.activeElement;function a(){return Array.from(e?.querySelectorAll(t)??[]).filter(e=>{if(e.hasAttribute(`aria-hidden`))return!1;let t=typeof window<`u`?window.getComputedStyle(e):null;return!(t&&(t.display===`none`||t.visibility===`hidden`))})}function o(t){if(t.key!==`Tab`)return;let n=a();if(n.length===0){t.preventDefault();return}let r=n[0],i=n[n.length-1],o=document.activeElement;t.shiftKey?(o===r||!o||!e?.contains(o))&&(t.preventDefault(),i.focus()):o===i&&(t.preventDefault(),r.focus())}return a()[0]?.focus(),document.addEventListener(`keydown`,o),()=>{document.removeEventListener(`keydown`,o),i?.focus?.()}},[n,r])}exports.useFocusTrap=n;
1
+ let e=require("react");var t=[`a[href]`,`button:not([disabled])`,`textarea:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`[tabindex]:not([tabindex='-1'])`].join(`,`);function n(n,r){(0,e.useEffect)(()=>{if(!r)return;let e=n.current;if(!e)return;let i=document.activeElement;function a(){return Array.from(e?.querySelectorAll(t)??[]).filter(e=>{if(e.hasAttribute(`aria-hidden`))return!1;let t=typeof window<`u`?window.getComputedStyle(e):null;return!t||t.display!==`none`&&t.visibility!==`hidden`})}function o(t){if(t.key!==`Tab`)return;let n=a();if(n.length===0){t.preventDefault();return}let r=n[0],i=n[n.length-1],o=document.activeElement;t.shiftKey?(o===r||!o||!e?.contains(o))&&(t.preventDefault(),i.focus()):o===i&&(t.preventDefault(),r.focus())}return a()[0]?.focus(),document.addEventListener(`keydown`,o),()=>{document.removeEventListener(`keydown`,o),i?.focus?.()}},[n,r])}exports.useFocusTrap=n;
2
2
  //# sourceMappingURL=use-focus-trap.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-focus-trap.cjs","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\n\nconst FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true. Cycles\n * Tab and Shift+Tab between the first and last focusable descendants. Pair\n * with Modal/Drawer for fully-accessible overlays.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n function getFocusable(): HTMLElement[] {\n return Array.from(\n container?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [],\n ).filter((el) => {\n if (el.hasAttribute(\"aria-hidden\")) return false;\n const style = typeof window !== \"undefined\" ? window.getComputedStyle(el) : null;\n if (style && (style.display === \"none\" || style.visibility === \"hidden\")) {\n return false;\n }\n return true;\n });\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\") return;\n const elements = getFocusable();\n if (elements.length === 0) {\n event.preventDefault();\n return;\n }\n const first = elements[0]!;\n const last = elements[elements.length - 1]!;\n const current = document.activeElement as HTMLElement | null;\n if (event.shiftKey) {\n if (current === first || !current || !container?.contains(current)) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (current === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n const focusable = getFocusable();\n focusable[0]?.focus();\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n previouslyFocused?.focus?.();\n };\n }, [containerRef, active]);\n}\n"],"mappings":"uBAGA,IAAM,EAAqB,CACvB,UACA,yBACA,2BACA,wBACA,yBACA,iCACJ,CAAC,CAAC,KAAK,GAAG,EAOV,SAAgB,EAAa,EAA6C,EAAuB,EAC7F,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAQ,OACb,IAAM,EAAY,EAAa,QAC/B,GAAI,CAAC,EAAW,OAEhB,IAAM,EAAoB,SAAS,cAEnC,SAAS,GAA8B,CACnC,OAAO,MAAM,KACT,GAAW,iBAA8B,CAAkB,GAAK,CAAC,CACrE,CAAC,CAAC,OAAQ,GAAO,CACb,GAAI,EAAG,aAAa,aAAa,EAAG,MAAO,GAC3C,IAAM,EAAQ,OAAO,OAAW,IAAc,OAAO,iBAAiB,CAAE,EAAI,KAI5E,MAHA,EAAI,IAAU,EAAM,UAAY,QAAU,EAAM,aAAe,UAInE,CAAC,CACL,CAEA,SAAS,EAAc,EAA4B,CAC/C,GAAI,EAAM,MAAQ,MAAO,OACzB,IAAM,EAAW,EAAa,EAC9B,GAAI,EAAS,SAAW,EAAG,CACvB,EAAM,eAAe,EACrB,MACJ,CACA,IAAM,EAAQ,EAAS,GACjB,EAAO,EAAS,EAAS,OAAS,GAClC,EAAU,SAAS,cACrB,EAAM,UACF,IAAY,GAAS,CAAC,GAAW,CAAC,GAAW,SAAS,CAAO,KAC7D,EAAM,eAAe,EACrB,EAAK,MAAM,GAGX,IAAY,IACZ,EAAM,eAAe,EACrB,EAAM,MAAM,EAGxB,CAKA,OAFA,EAAA,CAAA,CAAU,EAAE,EAAE,MAAM,EACpB,SAAS,iBAAiB,UAAW,CAAa,MACrC,CACT,SAAS,oBAAoB,UAAW,CAAa,EACrD,GAAmB,QAAQ,CAC/B,CACJ,EAAG,CAAC,EAAc,CAAM,CAAC,CAC7B"}
1
+ {"version":3,"file":"use-focus-trap.cjs","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\n\nconst FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true. Cycles\n * Tab and Shift+Tab between the first and last focusable descendants. Pair\n * with Modal/Drawer for fully-accessible overlays.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n function getFocusable(): HTMLElement[] {\n return Array.from(\n container?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [],\n ).filter((el) => {\n if (el.hasAttribute(\"aria-hidden\")) return false;\n const style = typeof window !== \"undefined\" ? window.getComputedStyle(el) : null;\n if (style && (style.display === \"none\" || style.visibility === \"hidden\")) {\n return false;\n }\n return true;\n });\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\") return;\n const elements = getFocusable();\n if (elements.length === 0) {\n event.preventDefault();\n return;\n }\n const first = elements[0]!;\n const last = elements[elements.length - 1]!;\n const current = document.activeElement as HTMLElement | null;\n if (event.shiftKey) {\n if (current === first || !current || !container?.contains(current)) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (current === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n const focusable = getFocusable();\n focusable[0]?.focus();\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n previouslyFocused?.focus?.();\n };\n }, [containerRef, active]);\n}\n"],"mappings":"uBAGA,IAAM,EAAqB,CACvB,UACA,yBACA,2BACA,wBACA,yBACA,iCACJ,CAAC,CAAC,KAAK,GAAG,EAOV,SAAgB,EAAa,EAA6C,EAAuB,EAC7F,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAQ,OACb,IAAM,EAAY,EAAa,QAC/B,GAAI,CAAC,EAAW,OAEhB,IAAM,EAAoB,SAAS,cAEnC,SAAS,GAA8B,CACnC,OAAO,MAAM,KACT,GAAW,iBAA8B,CAAkB,GAAK,CAAC,CACrE,CAAC,CAAC,OAAQ,GAAO,CACb,GAAI,EAAG,aAAa,aAAa,EAAG,MAAO,GAC3C,IAAM,EAAQ,OAAO,OAAW,IAAc,OAAO,iBAAiB,CAAE,EAAI,KAI5E,MAHI,IAAU,EAAM,UAAY,QAAU,EAAM,aAAe,QAInE,CAAC,CACL,CAEA,SAAS,EAAc,EAA4B,CAC/C,GAAI,EAAM,MAAQ,MAAO,OACzB,IAAM,EAAW,EAAa,EAC9B,GAAI,EAAS,SAAW,EAAG,CACvB,EAAM,eAAe,EACrB,MACJ,CACA,IAAM,EAAQ,EAAS,GACjB,EAAO,EAAS,EAAS,OAAS,GAClC,EAAU,SAAS,cACrB,EAAM,UACF,IAAY,GAAS,CAAC,GAAW,CAAC,GAAW,SAAS,CAAO,KAC7D,EAAM,eAAe,EACrB,EAAK,MAAM,GAGX,IAAY,IACZ,EAAM,eAAe,EACrB,EAAM,MAAM,EAGxB,CAKA,OAFA,EAAA,CAAA,CAAU,EAAE,EAAE,MAAM,EACpB,SAAS,iBAAiB,UAAW,CAAa,MACrC,CACT,SAAS,oBAAoB,UAAW,CAAa,EACrD,GAAmB,QAAQ,CAC/B,CACJ,EAAG,CAAC,EAAc,CAAM,CAAC,CAC7B"}
@@ -18,7 +18,7 @@ function n(n, r) {
18
18
  return Array.from(e?.querySelectorAll(t) ?? []).filter((e) => {
19
19
  if (e.hasAttribute("aria-hidden")) return !1;
20
20
  let t = typeof window < "u" ? window.getComputedStyle(e) : null;
21
- return !(t && (t.display === "none" || t.visibility === "hidden"));
21
+ return !t || t.display !== "none" && t.visibility !== "hidden";
22
22
  });
23
23
  }
24
24
  function o(t) {
@@ -1 +1 @@
1
- {"version":3,"file":"use-focus-trap.js","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\n\nconst FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true. Cycles\n * Tab and Shift+Tab between the first and last focusable descendants. Pair\n * with Modal/Drawer for fully-accessible overlays.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n function getFocusable(): HTMLElement[] {\n return Array.from(\n container?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [],\n ).filter((el) => {\n if (el.hasAttribute(\"aria-hidden\")) return false;\n const style = typeof window !== \"undefined\" ? window.getComputedStyle(el) : null;\n if (style && (style.display === \"none\" || style.visibility === \"hidden\")) {\n return false;\n }\n return true;\n });\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\") return;\n const elements = getFocusable();\n if (elements.length === 0) {\n event.preventDefault();\n return;\n }\n const first = elements[0]!;\n const last = elements[elements.length - 1]!;\n const current = document.activeElement as HTMLElement | null;\n if (event.shiftKey) {\n if (current === first || !current || !container?.contains(current)) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (current === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n const focusable = getFocusable();\n focusable[0]?.focus();\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n previouslyFocused?.focus?.();\n };\n }, [containerRef, active]);\n}\n"],"mappings":";;AAGA,IAAM,IAAqB;CACvB;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC,CAAC,KAAK,GAAG;AAOV,SAAgB,EAAa,GAA6C,GAAuB;CAC7F,QAAgB;EACZ,IAAI,CAAC,GAAQ;EACb,IAAM,IAAY,EAAa;EAC/B,IAAI,CAAC,GAAW;EAEhB,IAAM,IAAoB,SAAS;EAEnC,SAAS,IAA8B;GACnC,OAAO,MAAM,KACT,GAAW,iBAA8B,CAAkB,KAAK,CAAC,CACrE,CAAC,CAAC,QAAQ,MAAO;IACb,IAAI,EAAG,aAAa,aAAa,GAAG,OAAO;IAC3C,IAAM,IAAQ,OAAO,SAAW,MAAc,OAAO,iBAAiB,CAAE,IAAI;IAI5E,OAHA,EAAI,MAAU,EAAM,YAAY,UAAU,EAAM,eAAe;GAInE,CAAC;EACL;EAEA,SAAS,EAAc,GAA4B;GAC/C,IAAI,EAAM,QAAQ,OAAO;GACzB,IAAM,IAAW,EAAa;GAC9B,IAAI,EAAS,WAAW,GAAG;IACvB,EAAM,eAAe;IACrB;GACJ;GACA,IAAM,IAAQ,EAAS,IACjB,IAAO,EAAS,EAAS,SAAS,IAClC,IAAU,SAAS;GACzB,AAAI,EAAM,YACF,MAAY,KAAS,CAAC,KAAW,CAAC,GAAW,SAAS,CAAO,OAC7D,EAAM,eAAe,GACrB,EAAK,MAAM,KAGX,MAAY,MACZ,EAAM,eAAe,GACrB,EAAM,MAAM;EAGxB;EAKA,OAFA,EAAA,CAAA,CAAU,EAAE,EAAE,MAAM,GACpB,SAAS,iBAAiB,WAAW,CAAa,SACrC;GAET,AADA,SAAS,oBAAoB,WAAW,CAAa,GACrD,GAAmB,QAAQ;EAC/B;CACJ,GAAG,CAAC,GAAc,CAAM,CAAC;AAC7B"}
1
+ {"version":3,"file":"use-focus-trap.js","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\n\nconst FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true. Cycles\n * Tab and Shift+Tab between the first and last focusable descendants. Pair\n * with Modal/Drawer for fully-accessible overlays.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n function getFocusable(): HTMLElement[] {\n return Array.from(\n container?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [],\n ).filter((el) => {\n if (el.hasAttribute(\"aria-hidden\")) return false;\n const style = typeof window !== \"undefined\" ? window.getComputedStyle(el) : null;\n if (style && (style.display === \"none\" || style.visibility === \"hidden\")) {\n return false;\n }\n return true;\n });\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\") return;\n const elements = getFocusable();\n if (elements.length === 0) {\n event.preventDefault();\n return;\n }\n const first = elements[0]!;\n const last = elements[elements.length - 1]!;\n const current = document.activeElement as HTMLElement | null;\n if (event.shiftKey) {\n if (current === first || !current || !container?.contains(current)) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (current === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n const focusable = getFocusable();\n focusable[0]?.focus();\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n previouslyFocused?.focus?.();\n };\n }, [containerRef, active]);\n}\n"],"mappings":";;AAGA,IAAM,IAAqB;CACvB;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC,CAAC,KAAK,GAAG;AAOV,SAAgB,EAAa,GAA6C,GAAuB;CAC7F,QAAgB;EACZ,IAAI,CAAC,GAAQ;EACb,IAAM,IAAY,EAAa;EAC/B,IAAI,CAAC,GAAW;EAEhB,IAAM,IAAoB,SAAS;EAEnC,SAAS,IAA8B;GACnC,OAAO,MAAM,KACT,GAAW,iBAA8B,CAAkB,KAAK,CAAC,CACrE,CAAC,CAAC,QAAQ,MAAO;IACb,IAAI,EAAG,aAAa,aAAa,GAAG,OAAO;IAC3C,IAAM,IAAQ,OAAO,SAAW,MAAc,OAAO,iBAAiB,CAAE,IAAI;IAI5E,OAHI,MAAU,EAAM,YAAY,UAAU,EAAM,eAAe;GAInE,CAAC;EACL;EAEA,SAAS,EAAc,GAA4B;GAC/C,IAAI,EAAM,QAAQ,OAAO;GACzB,IAAM,IAAW,EAAa;GAC9B,IAAI,EAAS,WAAW,GAAG;IACvB,EAAM,eAAe;IACrB;GACJ;GACA,IAAM,IAAQ,EAAS,IACjB,IAAO,EAAS,EAAS,SAAS,IAClC,IAAU,SAAS;GACzB,AAAI,EAAM,YACF,MAAY,KAAS,CAAC,KAAW,CAAC,GAAW,SAAS,CAAO,OAC7D,EAAM,eAAe,GACrB,EAAK,MAAM,KAGX,MAAY,MACZ,EAAM,eAAe,GACrB,EAAM,MAAM;EAGxB;EAKA,OAFA,EAAA,CAAA,CAAU,EAAE,EAAE,MAAM,GACpB,SAAS,iBAAiB,WAAW,CAAa,SACrC;GAET,AADA,SAAS,oBAAoB,WAAW,CAAa,GACrD,GAAmB,QAAQ;EAC/B;CACJ,GAAG,CAAC,GAAc,CAAM,CAAC;AAC7B"}
@@ -1,2 +1,2 @@
1
- let e=require("react");var t=[{code:`Space`,label:`Espaço`},{code:`ControlLeft`,label:`Ctrl esquerdo`},{code:`AltLeft`,label:`Alt esquerdo`},{code:`ShiftLeft`,label:`Shift esquerdo`},{code:`Backquote`,label:"Crase (`)"}],n=`Space`;function r(e){return t.find(t=>t.code===e)?.label??e}function i(e){if(!(e instanceof HTMLElement))return!1;if(e.isContentEditable)return!0;let t=e.tagName;return t===`INPUT`||t===`TEXTAREA`||t===`SELECT`}function a({code:t=n,onDown:r,onUp:a,enabled:o=!0}){let s=(0,e.useRef)({onDown:r,onUp:a});(0,e.useEffect)(()=>{s.current={onDown:r,onUp:a}}),(0,e.useEffect)(()=>{if(!o||typeof window>`u`)return;let e=!1,n=()=>{e&&(e=!1,s.current.onUp())},r=n=>{n.code!==t||n.repeat||i(n.target)||(n.preventDefault(),!e&&(e=!0,s.current.onDown()))},a=r=>{r.code!==t||!e||(r.preventDefault(),n())};return window.addEventListener(`keydown`,r),window.addEventListener(`keyup`,a),window.addEventListener(`blur`,n),()=>{window.removeEventListener(`keydown`,r),window.removeEventListener(`keyup`,a),window.removeEventListener(`blur`,n),n()}},[t,o])}exports.DEFAULT_PUSH_TO_TALK_KEY=n,exports.PUSH_TO_TALK_KEYS=t,exports.pushToTalkKeyLabel=r,exports.usePushToTalk=a;
1
+ let e=require("react");var t=[{code:`Space`,label:`Espaço`},{code:`ControlLeft`,label:`Ctrl esquerdo`},{code:`AltLeft`,label:`Alt esquerdo`},{code:`ShiftLeft`,label:`Shift esquerdo`},{code:`Backquote`,label:"Crase (`)"}],n=`Space`;function r(e){return t.find(t=>t.code===e)?.label??e}function i(e){if(!(e instanceof HTMLElement))return!1;if(e.isContentEditable)return!0;let t=e.tagName;return t===`INPUT`||t===`TEXTAREA`||t===`SELECT`}function a({code:t=n,onDown:r,onUp:a,enabled:o=!0}){let s=(0,e.useRef)({onDown:r,onUp:a});(0,e.useEffect)(()=>{s.current={onDown:r,onUp:a}}),(0,e.useEffect)(()=>{if(!o||typeof window>`u`)return;let e=!1,n=()=>{e&&(e=!1,s.current.onUp())},r=n=>{n.code!==t||n.repeat||i(n.target)||(n.preventDefault(),!e&&(e=!0,s.current.onDown()))},a=r=>{r.code===t&&e&&(r.preventDefault(),n())};return window.addEventListener(`keydown`,r),window.addEventListener(`keyup`,a),window.addEventListener(`blur`,n),()=>{window.removeEventListener(`keydown`,r),window.removeEventListener(`keyup`,a),window.removeEventListener(`blur`,n),n()}},[t,o])}exports.DEFAULT_PUSH_TO_TALK_KEY=n,exports.PUSH_TO_TALK_KEYS=t,exports.pushToTalkKeyLabel=r,exports.usePushToTalk=a;
2
2
  //# sourceMappingURL=use-push-to-talk.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-push-to-talk.cjs","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n /** `KeyboardEvent.code`, which is layout-independent. */\n code: string;\n /** What to show a person. `code` itself is not a label. */\n label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n { code: \"Space\", label: \"Espaço\" },\n { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n { code: \"AltLeft\", label: \"Alt esquerdo\" },\n { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n * showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n code?: string;\n /** Called once when the key goes down. */\n onDown: () => void;\n /** Called when the key comes up, the window blurs, or the hook unmounts. */\n onUp: () => void;\n /**\n * Whether the binding is live. Default `true`.\n *\n * Turning it off releases first, so flipping a call from push-to-talk to an\n * open microphone while the key is held does not leave `onUp` unfired.\n */\n enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n * browser never sees the `keyup`, and the microphone stays open for as long as\n * the person is looking at another window — exactly what push-to-talk exists\n * to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n * and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n * `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n * code: \"Space\",\n * onDown: () => setMicEnabled(true),\n * onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n code = DEFAULT_PUSH_TO_TALK_KEY,\n onDown,\n onUp,\n enabled = true,\n}: UsePushToTalkOptions): void {\n const callbacks = useRef({ onDown, onUp });\n useEffect(() => {\n callbacks.current = { onDown, onUp };\n });\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n let held = false;\n\n const release = (): void => {\n if (!held) return;\n held = false;\n callbacks.current.onUp();\n };\n\n const handleKeyDown = (event: KeyboardEvent): void => {\n if (event.code !== code || event.repeat) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n if (held) return;\n held = true;\n callbacks.current.onDown();\n };\n\n /**\n * Release on the way up, and never mind where the keystroke landed.\n *\n * The field guard belongs on the way **down** — it is what stops a\n * push-to-talk bound to Space from opening the microphone every time\n * somebody writes a message. On the way up it did the opposite of its\n * job: `keyup` is delivered to whatever is focused when the key rises,\n * not to what was focused when it fell, so clicking into an input while\n * still holding sent the release into the guard and left `held` true.\n * The microphone stayed open until the window blurred.\n *\n * `held` is the guard this needs. If we never opened the microphone\n * there is nothing to close, and `release()` already returns on that —\n * so the only thing left to decide is `preventDefault`, which is\n * honest only for a keystroke we actually acted on.\n */\n const handleKeyUp = (event: KeyboardEvent): void => {\n if (event.code !== code || !held) return;\n event.preventDefault();\n release();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n window.addEventListener(\"blur\", release);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n window.removeEventListener(\"blur\", release);\n release();\n };\n }, [code, enabled]);\n}\n"],"mappings":"uBAsBA,IAAa,EAA8C,CACvD,CAAE,KAAM,QAAS,MAAO,QAAS,EACjC,CAAE,KAAM,cAAe,MAAO,eAAgB,EAC9C,CAAE,KAAM,UAAW,MAAO,cAAe,EACzC,CAAE,KAAM,YAAa,MAAO,gBAAiB,EAC7C,CAAE,KAAM,YAAa,MAAO,WAAY,CAC5C,EAGa,EAA2B,QASxC,SAAgB,EAAmB,EAAsB,CACrD,OAAO,EAAkB,KAAM,GAAQ,EAAI,OAAS,CAAI,CAAC,EAAE,OAAS,CACxE,CAYA,SAAS,EAAe,EAAqC,CACzD,GAAI,EAAE,aAAkB,aAAc,MAAO,GAC7C,GAAI,EAAO,kBAAmB,MAAO,GACrC,IAAM,EAAM,EAAO,QACnB,OAAO,IAAQ,SAAW,IAAQ,YAAc,IAAQ,QAC5D,CAmDA,SAAgB,EAAc,CAC1B,OAAO,EACP,SACA,OACA,UAAU,IACiB,CAC3B,IAAM,GAAA,EAAY,EAAA,OAAA,CAAO,CAAE,SAAQ,MAAK,CAAC,GACzC,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAU,QAAU,CAAE,SAAQ,MAAK,CACvC,CAAC,GAED,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAW,OAAO,OAAW,IAAa,OAC/C,IAAI,EAAO,GAEL,MAAsB,CACnB,IACL,EAAO,GACP,EAAU,QAAQ,KAAK,EAC3B,EAEM,EAAiB,GAA+B,CAC9C,EAAM,OAAS,GAAQ,EAAM,QAC7B,EAAe,EAAM,MAAM,IAC/B,EAAM,eAAe,EACjB,KACJ,EAAO,GACP,EAAU,QAAQ,OAAO,GAC7B,EAkBM,EAAe,GAA+B,CAC5C,EAAM,OAAS,GAAQ,CAAC,IAC5B,EAAM,eAAe,EACrB,EAAQ,EACZ,EAMA,OAJA,OAAO,iBAAiB,UAAW,CAAa,EAChD,OAAO,iBAAiB,QAAS,CAAW,EAC5C,OAAO,iBAAiB,OAAQ,CAAO,MAE1B,CACT,OAAO,oBAAoB,UAAW,CAAa,EACnD,OAAO,oBAAoB,QAAS,CAAW,EAC/C,OAAO,oBAAoB,OAAQ,CAAO,EAC1C,EAAQ,CACZ,CACJ,EAAG,CAAC,EAAM,CAAO,CAAC,CACtB"}
1
+ {"version":3,"file":"use-push-to-talk.cjs","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n /** `KeyboardEvent.code`, which is layout-independent. */\n code: string;\n /** What to show a person. `code` itself is not a label. */\n label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n { code: \"Space\", label: \"Espaço\" },\n { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n { code: \"AltLeft\", label: \"Alt esquerdo\" },\n { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n * showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n code?: string;\n /** Called once when the key goes down. */\n onDown: () => void;\n /** Called when the key comes up, the window blurs, or the hook unmounts. */\n onUp: () => void;\n /**\n * Whether the binding is live. Default `true`.\n *\n * Turning it off releases first, so flipping a call from push-to-talk to an\n * open microphone while the key is held does not leave `onUp` unfired.\n */\n enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n * browser never sees the `keyup`, and the microphone stays open for as long as\n * the person is looking at another window — exactly what push-to-talk exists\n * to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n * and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n * `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n * code: \"Space\",\n * onDown: () => setMicEnabled(true),\n * onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n code = DEFAULT_PUSH_TO_TALK_KEY,\n onDown,\n onUp,\n enabled = true,\n}: UsePushToTalkOptions): void {\n const callbacks = useRef({ onDown, onUp });\n useEffect(() => {\n callbacks.current = { onDown, onUp };\n });\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n let held = false;\n\n const release = (): void => {\n if (!held) return;\n held = false;\n callbacks.current.onUp();\n };\n\n const handleKeyDown = (event: KeyboardEvent): void => {\n if (event.code !== code || event.repeat) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n if (held) return;\n held = true;\n callbacks.current.onDown();\n };\n\n /**\n * Release on the way up, and never mind where the keystroke landed.\n *\n * The field guard belongs on the way **down** — it is what stops a\n * push-to-talk bound to Space from opening the microphone every time\n * somebody writes a message. On the way up it did the opposite of its\n * job: `keyup` is delivered to whatever is focused when the key rises,\n * not to what was focused when it fell, so clicking into an input while\n * still holding sent the release into the guard and left `held` true.\n * The microphone stayed open until the window blurred.\n *\n * `held` is the guard this needs. If we never opened the microphone\n * there is nothing to close, and `release()` already returns on that —\n * so the only thing left to decide is `preventDefault`, which is\n * honest only for a keystroke we actually acted on.\n */\n const handleKeyUp = (event: KeyboardEvent): void => {\n if (event.code !== code || !held) return;\n event.preventDefault();\n release();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n window.addEventListener(\"blur\", release);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n window.removeEventListener(\"blur\", release);\n release();\n };\n }, [code, enabled]);\n}\n"],"mappings":"uBAsBA,IAAa,EAA8C,CACvD,CAAE,KAAM,QAAS,MAAO,QAAS,EACjC,CAAE,KAAM,cAAe,MAAO,eAAgB,EAC9C,CAAE,KAAM,UAAW,MAAO,cAAe,EACzC,CAAE,KAAM,YAAa,MAAO,gBAAiB,EAC7C,CAAE,KAAM,YAAa,MAAO,WAAY,CAC5C,EAGa,EAA2B,QASxC,SAAgB,EAAmB,EAAsB,CACrD,OAAO,EAAkB,KAAM,GAAQ,EAAI,OAAS,CAAI,CAAC,EAAE,OAAS,CACxE,CAYA,SAAS,EAAe,EAAqC,CACzD,GAAI,EAAE,aAAkB,aAAc,MAAO,GAC7C,GAAI,EAAO,kBAAmB,MAAO,GACrC,IAAM,EAAM,EAAO,QACnB,OAAO,IAAQ,SAAW,IAAQ,YAAc,IAAQ,QAC5D,CAmDA,SAAgB,EAAc,CAC1B,OAAO,EACP,SACA,OACA,UAAU,IACiB,CAC3B,IAAM,GAAA,EAAY,EAAA,OAAA,CAAO,CAAE,SAAQ,MAAK,CAAC,GACzC,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAU,QAAU,CAAE,SAAQ,MAAK,CACvC,CAAC,GAED,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAW,OAAO,OAAW,IAAa,OAC/C,IAAI,EAAO,GAEL,MAAsB,CACnB,IACL,EAAO,GACP,EAAU,QAAQ,KAAK,EAC3B,EAEM,EAAiB,GAA+B,CAC9C,EAAM,OAAS,GAAQ,EAAM,QAC7B,EAAe,EAAM,MAAM,IAC/B,EAAM,eAAe,EACjB,KACJ,EAAO,GACP,EAAU,QAAQ,OAAO,GAC7B,EAkBM,EAAe,GAA+B,CAC5C,EAAM,OAAS,GAAS,IAC5B,EAAM,eAAe,EACrB,EAAQ,EACZ,EAMA,OAJA,OAAO,iBAAiB,UAAW,CAAa,EAChD,OAAO,iBAAiB,QAAS,CAAW,EAC5C,OAAO,iBAAiB,OAAQ,CAAO,MAE1B,CACT,OAAO,oBAAoB,UAAW,CAAa,EACnD,OAAO,oBAAoB,QAAS,CAAW,EAC/C,OAAO,oBAAoB,OAAQ,CAAO,EAC1C,EAAQ,CACZ,CACJ,EAAG,CAAC,EAAM,CAAO,CAAC,CACtB"}
@@ -48,7 +48,7 @@ function o({ code: n = r, onDown: i, onUp: o, enabled: s = !0 }) {
48
48
  }, r = (t) => {
49
49
  t.code !== n || t.repeat || a(t.target) || (t.preventDefault(), !e && (e = !0, c.current.onDown()));
50
50
  }, i = (r) => {
51
- r.code !== n || !e || (r.preventDefault(), t());
51
+ r.code === n && e && (r.preventDefault(), t());
52
52
  };
53
53
  return window.addEventListener("keydown", r), window.addEventListener("keyup", i), window.addEventListener("blur", t), () => {
54
54
  window.removeEventListener("keydown", r), window.removeEventListener("keyup", i), window.removeEventListener("blur", t), t();
@@ -1 +1 @@
1
- {"version":3,"file":"use-push-to-talk.js","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n /** `KeyboardEvent.code`, which is layout-independent. */\n code: string;\n /** What to show a person. `code` itself is not a label. */\n label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n { code: \"Space\", label: \"Espaço\" },\n { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n { code: \"AltLeft\", label: \"Alt esquerdo\" },\n { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n * showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n code?: string;\n /** Called once when the key goes down. */\n onDown: () => void;\n /** Called when the key comes up, the window blurs, or the hook unmounts. */\n onUp: () => void;\n /**\n * Whether the binding is live. Default `true`.\n *\n * Turning it off releases first, so flipping a call from push-to-talk to an\n * open microphone while the key is held does not leave `onUp` unfired.\n */\n enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n * browser never sees the `keyup`, and the microphone stays open for as long as\n * the person is looking at another window — exactly what push-to-talk exists\n * to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n * and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n * `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n * code: \"Space\",\n * onDown: () => setMicEnabled(true),\n * onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n code = DEFAULT_PUSH_TO_TALK_KEY,\n onDown,\n onUp,\n enabled = true,\n}: UsePushToTalkOptions): void {\n const callbacks = useRef({ onDown, onUp });\n useEffect(() => {\n callbacks.current = { onDown, onUp };\n });\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n let held = false;\n\n const release = (): void => {\n if (!held) return;\n held = false;\n callbacks.current.onUp();\n };\n\n const handleKeyDown = (event: KeyboardEvent): void => {\n if (event.code !== code || event.repeat) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n if (held) return;\n held = true;\n callbacks.current.onDown();\n };\n\n /**\n * Release on the way up, and never mind where the keystroke landed.\n *\n * The field guard belongs on the way **down** — it is what stops a\n * push-to-talk bound to Space from opening the microphone every time\n * somebody writes a message. On the way up it did the opposite of its\n * job: `keyup` is delivered to whatever is focused when the key rises,\n * not to what was focused when it fell, so clicking into an input while\n * still holding sent the release into the guard and left `held` true.\n * The microphone stayed open until the window blurred.\n *\n * `held` is the guard this needs. If we never opened the microphone\n * there is nothing to close, and `release()` already returns on that —\n * so the only thing left to decide is `preventDefault`, which is\n * honest only for a keystroke we actually acted on.\n */\n const handleKeyUp = (event: KeyboardEvent): void => {\n if (event.code !== code || !held) return;\n event.preventDefault();\n release();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n window.addEventListener(\"blur\", release);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n window.removeEventListener(\"blur\", release);\n release();\n };\n }, [code, enabled]);\n}\n"],"mappings":";;AAsBA,IAAa,IAA8C;CACvD;EAAE,MAAM;EAAS,OAAO;CAAS;CACjC;EAAE,MAAM;EAAe,OAAO;CAAgB;CAC9C;EAAE,MAAM;EAAW,OAAO;CAAe;CACzC;EAAE,MAAM;EAAa,OAAO;CAAiB;CAC7C;EAAE,MAAM;EAAa,OAAO;CAAY;AAC5C,GAGa,IAA2B;AASxC,SAAgB,EAAmB,GAAsB;CACrD,OAAO,EAAkB,MAAM,MAAQ,EAAI,SAAS,CAAI,CAAC,EAAE,SAAS;AACxE;AAYA,SAAS,EAAe,GAAqC;CACzD,IAAI,EAAE,aAAkB,cAAc,OAAO;CAC7C,IAAI,EAAO,mBAAmB,OAAO;CACrC,IAAM,IAAM,EAAO;CACnB,OAAO,MAAQ,WAAW,MAAQ,cAAc,MAAQ;AAC5D;AAmDA,SAAgB,EAAc,EAC1B,UAAO,GACP,WACA,SACA,aAAU,MACiB;CAC3B,IAAM,IAAY,EAAO;EAAE;EAAQ;CAAK,CAAC;CAKzC,AAJA,QAAgB;EACZ,EAAU,UAAU;GAAE;GAAQ;EAAK;CACvC,CAAC,GAED,QAAgB;EACZ,IAAI,CAAC,KAAW,OAAO,SAAW,KAAa;EAC/C,IAAI,IAAO,IAEL,UAAsB;GACnB,MACL,IAAO,IACP,EAAU,QAAQ,KAAK;EAC3B,GAEM,KAAiB,MAA+B;GAC9C,EAAM,SAAS,KAAQ,EAAM,UAC7B,EAAe,EAAM,MAAM,MAC/B,EAAM,eAAe,GACjB,OACJ,IAAO,IACP,EAAU,QAAQ,OAAO;EAC7B,GAkBM,KAAe,MAA+B;GAC5C,EAAM,SAAS,KAAQ,CAAC,MAC5B,EAAM,eAAe,GACrB,EAAQ;EACZ;EAMA,OAJA,OAAO,iBAAiB,WAAW,CAAa,GAChD,OAAO,iBAAiB,SAAS,CAAW,GAC5C,OAAO,iBAAiB,QAAQ,CAAO,SAE1B;GAIT,AAHA,OAAO,oBAAoB,WAAW,CAAa,GACnD,OAAO,oBAAoB,SAAS,CAAW,GAC/C,OAAO,oBAAoB,QAAQ,CAAO,GAC1C,EAAQ;EACZ;CACJ,GAAG,CAAC,GAAM,CAAO,CAAC;AACtB"}
1
+ {"version":3,"file":"use-push-to-talk.js","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n /** `KeyboardEvent.code`, which is layout-independent. */\n code: string;\n /** What to show a person. `code` itself is not a label. */\n label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n { code: \"Space\", label: \"Espaço\" },\n { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n { code: \"AltLeft\", label: \"Alt esquerdo\" },\n { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n * showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n code?: string;\n /** Called once when the key goes down. */\n onDown: () => void;\n /** Called when the key comes up, the window blurs, or the hook unmounts. */\n onUp: () => void;\n /**\n * Whether the binding is live. Default `true`.\n *\n * Turning it off releases first, so flipping a call from push-to-talk to an\n * open microphone while the key is held does not leave `onUp` unfired.\n */\n enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n * browser never sees the `keyup`, and the microphone stays open for as long as\n * the person is looking at another window — exactly what push-to-talk exists\n * to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n * and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n * `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n * code: \"Space\",\n * onDown: () => setMicEnabled(true),\n * onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n code = DEFAULT_PUSH_TO_TALK_KEY,\n onDown,\n onUp,\n enabled = true,\n}: UsePushToTalkOptions): void {\n const callbacks = useRef({ onDown, onUp });\n useEffect(() => {\n callbacks.current = { onDown, onUp };\n });\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n let held = false;\n\n const release = (): void => {\n if (!held) return;\n held = false;\n callbacks.current.onUp();\n };\n\n const handleKeyDown = (event: KeyboardEvent): void => {\n if (event.code !== code || event.repeat) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n if (held) return;\n held = true;\n callbacks.current.onDown();\n };\n\n /**\n * Release on the way up, and never mind where the keystroke landed.\n *\n * The field guard belongs on the way **down** — it is what stops a\n * push-to-talk bound to Space from opening the microphone every time\n * somebody writes a message. On the way up it did the opposite of its\n * job: `keyup` is delivered to whatever is focused when the key rises,\n * not to what was focused when it fell, so clicking into an input while\n * still holding sent the release into the guard and left `held` true.\n * The microphone stayed open until the window blurred.\n *\n * `held` is the guard this needs. If we never opened the microphone\n * there is nothing to close, and `release()` already returns on that —\n * so the only thing left to decide is `preventDefault`, which is\n * honest only for a keystroke we actually acted on.\n */\n const handleKeyUp = (event: KeyboardEvent): void => {\n if (event.code !== code || !held) return;\n event.preventDefault();\n release();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n window.addEventListener(\"blur\", release);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n window.removeEventListener(\"blur\", release);\n release();\n };\n }, [code, enabled]);\n}\n"],"mappings":";;AAsBA,IAAa,IAA8C;CACvD;EAAE,MAAM;EAAS,OAAO;CAAS;CACjC;EAAE,MAAM;EAAe,OAAO;CAAgB;CAC9C;EAAE,MAAM;EAAW,OAAO;CAAe;CACzC;EAAE,MAAM;EAAa,OAAO;CAAiB;CAC7C;EAAE,MAAM;EAAa,OAAO;CAAY;AAC5C,GAGa,IAA2B;AASxC,SAAgB,EAAmB,GAAsB;CACrD,OAAO,EAAkB,MAAM,MAAQ,EAAI,SAAS,CAAI,CAAC,EAAE,SAAS;AACxE;AAYA,SAAS,EAAe,GAAqC;CACzD,IAAI,EAAE,aAAkB,cAAc,OAAO;CAC7C,IAAI,EAAO,mBAAmB,OAAO;CACrC,IAAM,IAAM,EAAO;CACnB,OAAO,MAAQ,WAAW,MAAQ,cAAc,MAAQ;AAC5D;AAmDA,SAAgB,EAAc,EAC1B,UAAO,GACP,WACA,SACA,aAAU,MACiB;CAC3B,IAAM,IAAY,EAAO;EAAE;EAAQ;CAAK,CAAC;CAKzC,AAJA,QAAgB;EACZ,EAAU,UAAU;GAAE;GAAQ;EAAK;CACvC,CAAC,GAED,QAAgB;EACZ,IAAI,CAAC,KAAW,OAAO,SAAW,KAAa;EAC/C,IAAI,IAAO,IAEL,UAAsB;GACnB,MACL,IAAO,IACP,EAAU,QAAQ,KAAK;EAC3B,GAEM,KAAiB,MAA+B;GAC9C,EAAM,SAAS,KAAQ,EAAM,UAC7B,EAAe,EAAM,MAAM,MAC/B,EAAM,eAAe,GACjB,OACJ,IAAO,IACP,EAAU,QAAQ,OAAO;EAC7B,GAkBM,KAAe,MAA+B;GAC5C,EAAM,SAAS,KAAS,MAC5B,EAAM,eAAe,GACrB,EAAQ;EACZ;EAMA,OAJA,OAAO,iBAAiB,WAAW,CAAa,GAChD,OAAO,iBAAiB,SAAS,CAAW,GAC5C,OAAO,iBAAiB,QAAQ,CAAO,SAE1B;GAIT,AAHA,OAAO,oBAAoB,WAAW,CAAa,GACnD,OAAO,oBAAoB,SAAS,CAAW,GAC/C,OAAO,oBAAoB,QAAQ,CAAO,GAC1C,EAAQ;EACZ;CACJ,GAAG,CAAC,GAAM,CAAO,CAAC;AACtB"}
@@ -1,2 +1,2 @@
1
- const e=require("../utils/base64.cjs"),t=require("./errors.cjs"),n=require("./retry.cjs"),r=require("./idempotency.cjs");var i=`1.0.0`,a=5242880;function o(t){return e.bytesToBase64(new TextEncoder().encode(t))}function s(e){if(!e)return null;let t=Object.entries(e).map(([e,t])=>`${e} ${o(t)}`);return t.length>0?t.join(`,`):null}function c(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 l(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 u(e){return new Promise((n,r)=>{let i=new XMLHttpRequest;i.open(e.method,e.url),i.withCredentials=e.withCredentials;for(let[t,n]of Object.entries(e.headers))i.setRequestHeader(t,n);if(e.onProgress){let t=e.onProgress;i.upload.onprogress=e=>t(e.loaded)}i.onload=()=>n({status:i.status,text:i.responseText,header:e=>i.getResponseHeader(e)}),i.onerror=()=>r(new t.TempestApiError({status:0,detail:`Falha de rede no upload resumível.`})),i.onabort=()=>r(new DOMException(`Aborted`,`AbortError`)),e.register(i),i.send(e.body)})}function d(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 f(e){if(!e)return null;try{return JSON.parse(e)}catch{return e}}var p=new Set([409,412]);function m(e){return!t.isApiError(e)||p.has(e.status)?!0:t.isRetriableStatus(e.status)}function h(e,n){let r=f(e.text),i=t.buildApiError(e.status,r,{get:e.header}),a=typeof r==`object`&&!!r&&(`detail`in r||`message`in r);return new t.TempestApiError({...i,detail:a?i.detail:n})}function g(e){let t=typeof window>`u`?void 0:window.location.href;try{return new URL(e,t).href}catch{return e}}function _(e){let{endpoint:o,file:f,chunkSize:p=a,metadata:_,headers:v={},getToken:y,withCredentials:b=!1,key:x=c(o,f),storage:S=l(),retry:C,onProgress:w,onStateChange:T}=e,E=`idle`,D=0,O=null,k=null,A=null,j=null,M=0;function N(e){E!==e&&(E=e,T?.(e))}function P(e){w?.({loaded:e,total:f.size,fraction:f.size===0?1:e/f.size,resumedFrom:M})}function F(){let e={...v,"Tus-Resumable":i},t=y?.();return t&&!(`Authorization`in e)&&(e.Authorization=`Bearer ${t}`),e}function I(e){j=e}async function L(){!S||!O||!k||await S.set(x,{url:O,offset:D,size:f.size,idempotencyKey:k,updatedAt:Date.now()})}async function R(e){let n=await u({method:`HEAD`,url:e,headers:F(),withCredentials:b,register:I});if(n.status===404||n.status===410)throw new t.TempestApiError({status:n.status,detail:`O upload expirou no servidor. Comece de novo.`});let r=d(n);if(r===null)throw h(n,`HEAD sem Upload-Offset.`);return r}async function z(){let e=S?await S.get(x):null;if(e&&e.size===f.size&&(k=e.idempotencyKey,e.url))try{return D=await R(e.url),O=e.url,e.url}catch{D=0}N(`creating`),k??=r.generateIdempotencyKey(),O=null,D=0,S&&await S.set(x,{url:``,offset:0,size:f.size,idempotencyKey:k,updatedAt:Date.now()});let t={...F(),"Upload-Length":String(f.size),"Idempotency-Key":k},n=s(_);n&&(t[`Upload-Metadata`]=n);let i=await u({method:`POST`,url:o,headers:t,withCredentials:b,register:I});if(i.status!==201)throw h(i,`Criação do upload recusada.`);let a=i.header(`Location`);if(!a)throw h(i,`Criação do upload sem cabeçalho Location.`);return O=g(a),await L(),O}async function B(e,t){if(t.needed&&(D=await R(e),t.needed=!1,P(D),await L(),D>=f.size))return;let n=Math.min(D+p,f.size),r=D,i=await u({method:`PATCH`,url:e,headers:{...F(),"Content-Type":`application/offset+octet-stream`,"Upload-Offset":String(r)},body:f.slice(r,n),withCredentials:b,onProgress:e=>P(Math.min(r+e,f.size)),register:I});if(i.status===409||i.status===412)throw t.needed=!0,h(i,`Offset divergente — o servidor já tinha esses bytes.`);if(i.status!==204&&i.status!==200)throw h(i,`Chunk recusado pelo servidor.`);D=d(i)??n,P(D),await L()}async function V(){A=null;let e=await z();M=D,N(`uploading`),P(D);let t={needed:!1};for(;D<f.size&&!A;)await n.retry(()=>B(e,t),{retries:5,...C,shouldRetry:(e,n)=>{if(A||e instanceof DOMException&&e.name===`AbortError`)return!1;let r=C?.shouldRetry?.(e,n)??m(e);return r&&(t.needed=!0),r}});return A===`pause`?(N(`paused`),null):A===`abort`?(N(`aborted`),null):(N(`done`),S&&await S.delete(x),{url:e,size:f.size})}async function H(){try{return await V()}catch(e){if(A!==null||e instanceof DOMException&&e.name===`AbortError`)return N(A===`abort`?`aborted`:`paused`),null;throw N(`error`),e}finally{j=null}}function U(e){A=e,j?.abort(),j=null}return{start:H,resume:H,pause:()=>U(`pause`),abort:async({discard:e=!1}={})=>{U(`abort`),N(`aborted`),e&&(O&&await u({method:`DELETE`,url:O,headers:F(),withCredentials:b,register:()=>void 0}).catch(()=>void 0),S&&await S.delete(x))},get state(){return E},get offset(){return D},get url(){return O},key:x}}exports.DEFAULT_CHUNK_SIZE=a,exports.TUS_VERSION=i,exports.createLocalUploadStorage=l,exports.createResumableUpload=_,exports.uploadFingerprint=c;
1
+ const e=require("../utils/base64.cjs"),t=require("./errors.cjs"),n=require("./retry.cjs"),r=require("./idempotency.cjs");var i=`1.0.0`,a=5242880;function o(t){return e.bytesToBase64(new TextEncoder().encode(t))}function s(e){if(!e)return null;let t=Object.entries(e).map(([e,t])=>`${e} ${o(t)}`);return t.length>0?t.join(`,`):null}function c(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 l(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 u(e){return new Promise((n,r)=>{let i=new XMLHttpRequest;i.open(e.method,e.url),i.withCredentials=e.withCredentials;for(let[t,n]of Object.entries(e.headers))i.setRequestHeader(t,n);if(e.onProgress){let t=e.onProgress;i.upload.onprogress=e=>t(e.loaded)}i.onload=()=>n({status:i.status,text:i.responseText,header:e=>i.getResponseHeader(e)}),i.onerror=()=>r(new t.TempestApiError({status:0,detail:`Falha de rede no upload resumível.`})),i.onabort=()=>r(new DOMException(`Aborted`,`AbortError`)),e.register(i),i.send(e.body)})}function d(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 f(e){if(!e)return null;try{return JSON.parse(e)}catch{return e}}var p=new Set([409,412]);function m(e){return!t.isApiError(e)||p.has(e.status)?!0:t.isRetriableStatus(e.status)}function h(e,n){let r=f(e.text),i=t.buildApiError(e.status,r,{get:e.header}),a=typeof r==`object`&&!!r&&(`detail`in r||`message`in r);return new t.TempestApiError({...i,detail:a?i.detail:n})}function g(e){let t=typeof window>`u`?void 0:window.location.href;try{return new URL(e,t).href}catch{return e}}function _(e){let{endpoint:o,file:f,chunkSize:p=a,metadata:_,headers:v={},getToken:y,withCredentials:b=!1,key:x=c(o,f),storage:S=l(),retry:C,onProgress:w,onStateChange:T}=e,E=`idle`,D=0,O=null,k=null,A=null,j=null,M=0;function N(e){E!==e&&(E=e,T?.(e))}function P(e){w?.({loaded:e,total:f.size,fraction:f.size===0?1:e/f.size,resumedFrom:M})}function F(){let e={...v,"Tus-Resumable":i},t=y?.();return t&&!(`Authorization`in e)&&(e.Authorization=`Bearer ${t}`),e}function I(e){j=e}async function L(){S&&O&&k&&await S.set(x,{url:O,offset:D,size:f.size,idempotencyKey:k,updatedAt:Date.now()})}async function R(e){let n=await u({method:`HEAD`,url:e,headers:F(),withCredentials:b,register:I});if(n.status===404||n.status===410)throw new t.TempestApiError({status:n.status,detail:`O upload expirou no servidor. Comece de novo.`});let r=d(n);if(r===null)throw h(n,`HEAD sem Upload-Offset.`);return r}async function z(){let e=S?await S.get(x):null;if(e&&e.size===f.size&&(k=e.idempotencyKey,e.url))try{return D=await R(e.url),O=e.url,e.url}catch{D=0}N(`creating`),k??=r.generateIdempotencyKey(),O=null,D=0,S&&await S.set(x,{url:``,offset:0,size:f.size,idempotencyKey:k,updatedAt:Date.now()});let t={...F(),"Upload-Length":String(f.size),"Idempotency-Key":k},n=s(_);n&&(t[`Upload-Metadata`]=n);let i=await u({method:`POST`,url:o,headers:t,withCredentials:b,register:I});if(i.status!==201)throw h(i,`Criação do upload recusada.`);let a=i.header(`Location`);if(!a)throw h(i,`Criação do upload sem cabeçalho Location.`);return O=g(a),await L(),O}async function B(e,t){if(t.needed&&(D=await R(e),t.needed=!1,P(D),await L(),D>=f.size))return;let n=Math.min(D+p,f.size),r=D,i=await u({method:`PATCH`,url:e,headers:{...F(),"Content-Type":`application/offset+octet-stream`,"Upload-Offset":String(r)},body:f.slice(r,n),withCredentials:b,onProgress:e=>P(Math.min(r+e,f.size)),register:I});if(i.status===409||i.status===412)throw t.needed=!0,h(i,`Offset divergente — o servidor já tinha esses bytes.`);if(i.status!==204&&i.status!==200)throw h(i,`Chunk recusado pelo servidor.`);D=d(i)??n,P(D),await L()}async function V(){A=null;let e=await z();M=D,N(`uploading`),P(D);let t={needed:!1};for(;D<f.size&&!A;)await n.retry(()=>B(e,t),{retries:5,...C,shouldRetry:(e,n)=>{if(A||e instanceof DOMException&&e.name===`AbortError`)return!1;let r=C?.shouldRetry?.(e,n)??m(e);return r&&(t.needed=!0),r}});return A===`pause`?(N(`paused`),null):A===`abort`?(N(`aborted`),null):(N(`done`),S&&await S.delete(x),{url:e,size:f.size})}async function H(){try{return await V()}catch(e){if(A!==null||e instanceof DOMException&&e.name===`AbortError`)return N(A===`abort`?`aborted`:`paused`),null;throw N(`error`),e}finally{j=null}}function U(e){A=e,j?.abort(),j=null}return{start:H,resume:H,pause:()=>U(`pause`),abort:async({discard:e=!1}={})=>{U(`abort`),N(`aborted`),e&&(O&&await u({method:`DELETE`,url:O,headers:F(),withCredentials:b,register:()=>void 0}).catch(()=>void 0),S&&await S.delete(x))},get state(){return E},get offset(){return D},get url(){return O},key:x}}exports.DEFAULT_CHUNK_SIZE=a,exports.TUS_VERSION=i,exports.createLocalUploadStorage=l,exports.createResumableUpload=_,exports.uploadFingerprint=c;
2
2
  //# sourceMappingURL=resumable-upload.cjs.map