tempest-react-sdk 0.57.0 → 0.59.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 (396) hide show
  1. package/README.md +14 -2
  2. package/bin/lib/css/findings.mjs +1 -0
  3. package/bin/lib/css/semantic.mjs +70 -6
  4. package/bin/lib/css/semantic.test.mjs +52 -1
  5. package/bin/lib/icons/generate.test.mjs +4 -4
  6. package/dist/br/geocode.cjs +1 -1
  7. package/dist/br/geocode.cjs.map +1 -1
  8. package/dist/br/geocode.js +1 -1
  9. package/dist/br/geocode.js.map +1 -1
  10. package/dist/capture/use-barcode-scanner.cjs +1 -1
  11. package/dist/capture/use-barcode-scanner.cjs.map +1 -1
  12. package/dist/capture/use-barcode-scanner.js +1 -1
  13. package/dist/capture/use-barcode-scanner.js.map +1 -1
  14. package/dist/components/AIChat/AIChat.cjs +1 -1
  15. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  16. package/dist/components/AIChat/AIChat.js +1 -1
  17. package/dist/components/AIChat/AIChat.js.map +1 -1
  18. package/dist/components/AIChat/AIChatTurn.cjs +1 -1
  19. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  20. package/dist/components/AIChat/AIChatTurn.js +1 -1
  21. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  22. package/dist/components/Alert/Alert.module.cjs.map +1 -1
  23. package/dist/components/Alert/Alert.module.js.map +1 -1
  24. package/dist/components/AppBar/use-sticky-body-warning.cjs +1 -1
  25. package/dist/components/AppBar/use-sticky-body-warning.cjs.map +1 -1
  26. package/dist/components/AppBar/use-sticky-body-warning.js +1 -1
  27. package/dist/components/AppBar/use-sticky-body-warning.js.map +1 -1
  28. package/dist/components/AudioPlayer/AudioPlayer.cjs +1 -1
  29. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  30. package/dist/components/AudioPlayer/AudioPlayer.js +2 -2
  31. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
  32. package/dist/components/AudioRecorder/AudioRecorder.cjs +1 -1
  33. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
  34. package/dist/components/AudioRecorder/AudioRecorder.js +1 -1
  35. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
  36. package/dist/components/Badge/Badge.module.cjs.map +1 -1
  37. package/dist/components/Badge/Badge.module.js.map +1 -1
  38. package/dist/components/BarList/BarList.module.cjs.map +1 -1
  39. package/dist/components/BarList/BarList.module.js.map +1 -1
  40. package/dist/components/BottomSheet/BottomSheet.module.cjs.map +1 -1
  41. package/dist/components/BottomSheet/BottomSheet.module.js.map +1 -1
  42. package/dist/components/Chat/Chat.cjs +1 -1
  43. package/dist/components/Chat/Chat.cjs.map +1 -1
  44. package/dist/components/Chat/Chat.js +1 -1
  45. package/dist/components/Chat/Chat.js.map +1 -1
  46. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  47. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  48. package/dist/components/Dropzone/Dropzone.module.cjs.map +1 -1
  49. package/dist/components/Dropzone/Dropzone.module.js.map +1 -1
  50. package/dist/components/ErrorText/ErrorText.module.cjs.map +1 -1
  51. package/dist/components/ErrorText/ErrorText.module.js.map +1 -1
  52. package/dist/components/FilterBar/FilterBar.cjs +1 -1
  53. package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
  54. package/dist/components/FilterBar/FilterBar.js +1 -1
  55. package/dist/components/FilterBar/FilterBar.js.map +1 -1
  56. package/dist/components/FilterBar/filter-apply.cjs +1 -1
  57. package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
  58. package/dist/components/FilterBar/filter-apply.js +1 -1
  59. package/dist/components/FilterBar/filter-apply.js.map +1 -1
  60. package/dist/components/ImageCropper/ImageCropper.cjs +1 -1
  61. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
  62. package/dist/components/ImageCropper/ImageCropper.js +1 -1
  63. package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
  64. package/dist/components/Markdown/Markdown.module.cjs.map +1 -1
  65. package/dist/components/Markdown/Markdown.module.js.map +1 -1
  66. package/dist/components/PasswordInput/PasswordInput.module.cjs.map +1 -1
  67. package/dist/components/PasswordInput/PasswordInput.module.js.map +1 -1
  68. package/dist/components/SegmentedControl/SegmentedControl.module.cjs.map +1 -1
  69. package/dist/components/SegmentedControl/SegmentedControl.module.js.map +1 -1
  70. package/dist/components/Sidebar/Sidebar.module.cjs.map +1 -1
  71. package/dist/components/Sidebar/Sidebar.module.js.map +1 -1
  72. package/dist/components/StepperInput/StepperInput.module.cjs.map +1 -1
  73. package/dist/components/StepperInput/StepperInput.module.js.map +1 -1
  74. package/dist/components/Tag/Tag.module.cjs.map +1 -1
  75. package/dist/components/Tag/Tag.module.js.map +1 -1
  76. package/dist/components/Timeline/Timeline.module.cjs.map +1 -1
  77. package/dist/components/Timeline/Timeline.module.js.map +1 -1
  78. package/dist/components/Toggle/Toggle.module.cjs.map +1 -1
  79. package/dist/components/Toggle/Toggle.module.js.map +1 -1
  80. package/dist/components/ToggleGroup/ToggleGroup.module.cjs.map +1 -1
  81. package/dist/components/ToggleGroup/ToggleGroup.module.js.map +1 -1
  82. package/dist/components/VideoPlayer/VideoPlayer.cjs +1 -1
  83. package/dist/components/VideoPlayer/VideoPlayer.cjs.map +1 -1
  84. package/dist/components/VideoPlayer/VideoPlayer.js +2 -2
  85. package/dist/components/VideoPlayer/VideoPlayer.js.map +1 -1
  86. package/dist/components/VirtualTable/VirtualTable.cjs +1 -1
  87. package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
  88. package/dist/components/VirtualTable/VirtualTable.js +1 -1
  89. package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
  90. package/dist/feature-flags/growthbook-adapter.cjs +1 -1
  91. package/dist/feature-flags/growthbook-adapter.cjs.map +1 -1
  92. package/dist/feature-flags/growthbook-adapter.js +1 -1
  93. package/dist/feature-flags/growthbook-adapter.js.map +1 -1
  94. package/dist/hooks/use-focus-trap.cjs +1 -1
  95. package/dist/hooks/use-focus-trap.cjs.map +1 -1
  96. package/dist/hooks/use-focus-trap.js +1 -1
  97. package/dist/hooks/use-focus-trap.js.map +1 -1
  98. package/dist/hooks/use-push-to-talk.cjs +1 -1
  99. package/dist/hooks/use-push-to-talk.cjs.map +1 -1
  100. package/dist/hooks/use-push-to-talk.js +1 -1
  101. package/dist/hooks/use-push-to-talk.js.map +1 -1
  102. package/dist/http/describe-api-error.cjs +1 -1
  103. package/dist/http/describe-api-error.cjs.map +1 -1
  104. package/dist/http/describe-api-error.js +12 -8
  105. package/dist/http/describe-api-error.js.map +1 -1
  106. package/dist/http/errors.cjs +1 -1
  107. package/dist/http/errors.cjs.map +1 -1
  108. package/dist/http/errors.js +21 -16
  109. package/dist/http/errors.js.map +1 -1
  110. package/dist/http/resumable-upload.cjs +1 -1
  111. package/dist/http/resumable-upload.cjs.map +1 -1
  112. package/dist/http/resumable-upload.js +1 -1
  113. package/dist/http/resumable-upload.js.map +1 -1
  114. package/dist/icons/generated/aliases.cjs +1 -1
  115. package/dist/icons/generated/aliases.cjs.map +1 -1
  116. package/dist/icons/generated/aliases.js +1 -0
  117. package/dist/icons/generated/aliases.js.map +1 -1
  118. package/dist/icons/generated/icon-names.cjs +1 -1
  119. package/dist/icons/generated/icon-names.cjs.map +1 -1
  120. package/dist/icons/generated/icon-names.js +1 -1
  121. package/dist/icons/generated/icon-names.js.map +1 -1
  122. package/dist/icons/generated/loaders.cjs +1 -1
  123. package/dist/icons/generated/loaders.cjs.map +1 -1
  124. package/dist/icons/generated/loaders.js +48 -42
  125. package/dist/icons/generated/loaders.js.map +1 -1
  126. package/dist/icons/generated/shard-02.cjs +1 -1
  127. package/dist/icons/generated/shard-02.cjs.map +1 -1
  128. package/dist/icons/generated/shard-02.js +20 -20
  129. package/dist/icons/generated/shard-02.js.map +1 -1
  130. package/dist/icons/generated/shard-03.cjs +1 -1
  131. package/dist/icons/generated/shard-03.cjs.map +1 -1
  132. package/dist/icons/generated/shard-03.js +41 -41
  133. package/dist/icons/generated/shard-03.js.map +1 -1
  134. package/dist/icons/generated/shard-04.cjs +1 -1
  135. package/dist/icons/generated/shard-04.cjs.map +1 -1
  136. package/dist/icons/generated/shard-04.js +41 -41
  137. package/dist/icons/generated/shard-04.js.map +1 -1
  138. package/dist/icons/generated/shard-05.cjs +1 -1
  139. package/dist/icons/generated/shard-05.cjs.map +1 -1
  140. package/dist/icons/generated/shard-05.js +41 -41
  141. package/dist/icons/generated/shard-05.js.map +1 -1
  142. package/dist/icons/generated/shard-06.cjs +1 -1
  143. package/dist/icons/generated/shard-06.cjs.map +1 -1
  144. package/dist/icons/generated/shard-06.js +41 -41
  145. package/dist/icons/generated/shard-06.js.map +1 -1
  146. package/dist/icons/generated/shard-07.cjs +1 -1
  147. package/dist/icons/generated/shard-07.cjs.map +1 -1
  148. package/dist/icons/generated/shard-07.js +41 -41
  149. package/dist/icons/generated/shard-07.js.map +1 -1
  150. package/dist/icons/generated/shard-08.cjs +1 -1
  151. package/dist/icons/generated/shard-08.cjs.map +1 -1
  152. package/dist/icons/generated/shard-08.js +41 -41
  153. package/dist/icons/generated/shard-08.js.map +1 -1
  154. package/dist/icons/generated/shard-09.cjs +1 -1
  155. package/dist/icons/generated/shard-09.cjs.map +1 -1
  156. package/dist/icons/generated/shard-09.js +41 -41
  157. package/dist/icons/generated/shard-09.js.map +1 -1
  158. package/dist/icons/generated/shard-10.cjs +1 -1
  159. package/dist/icons/generated/shard-10.cjs.map +1 -1
  160. package/dist/icons/generated/shard-10.js +41 -41
  161. package/dist/icons/generated/shard-10.js.map +1 -1
  162. package/dist/icons/generated/shard-11.cjs +1 -1
  163. package/dist/icons/generated/shard-11.cjs.map +1 -1
  164. package/dist/icons/generated/shard-11.js +41 -41
  165. package/dist/icons/generated/shard-11.js.map +1 -1
  166. package/dist/icons/generated/shard-12.cjs +1 -1
  167. package/dist/icons/generated/shard-12.cjs.map +1 -1
  168. package/dist/icons/generated/shard-12.js +41 -41
  169. package/dist/icons/generated/shard-12.js.map +1 -1
  170. package/dist/icons/generated/shard-13.cjs +1 -1
  171. package/dist/icons/generated/shard-13.cjs.map +1 -1
  172. package/dist/icons/generated/shard-13.js +41 -41
  173. package/dist/icons/generated/shard-13.js.map +1 -1
  174. package/dist/icons/generated/shard-14.cjs +1 -1
  175. package/dist/icons/generated/shard-14.cjs.map +1 -1
  176. package/dist/icons/generated/shard-14.js +41 -41
  177. package/dist/icons/generated/shard-14.js.map +1 -1
  178. package/dist/icons/generated/shard-15.cjs +1 -1
  179. package/dist/icons/generated/shard-15.cjs.map +1 -1
  180. package/dist/icons/generated/shard-15.js +41 -41
  181. package/dist/icons/generated/shard-15.js.map +1 -1
  182. package/dist/icons/generated/shard-16.cjs +1 -1
  183. package/dist/icons/generated/shard-16.cjs.map +1 -1
  184. package/dist/icons/generated/shard-16.js +41 -41
  185. package/dist/icons/generated/shard-16.js.map +1 -1
  186. package/dist/icons/generated/shard-17.cjs +1 -1
  187. package/dist/icons/generated/shard-17.cjs.map +1 -1
  188. package/dist/icons/generated/shard-17.js +41 -41
  189. package/dist/icons/generated/shard-17.js.map +1 -1
  190. package/dist/icons/generated/shard-18.cjs +1 -1
  191. package/dist/icons/generated/shard-18.cjs.map +1 -1
  192. package/dist/icons/generated/shard-18.js +41 -41
  193. package/dist/icons/generated/shard-18.js.map +1 -1
  194. package/dist/icons/generated/shard-19.cjs +1 -1
  195. package/dist/icons/generated/shard-19.cjs.map +1 -1
  196. package/dist/icons/generated/shard-19.js +41 -41
  197. package/dist/icons/generated/shard-19.js.map +1 -1
  198. package/dist/icons/generated/shard-20.cjs +1 -1
  199. package/dist/icons/generated/shard-20.cjs.map +1 -1
  200. package/dist/icons/generated/shard-20.js +41 -41
  201. package/dist/icons/generated/shard-20.js.map +1 -1
  202. package/dist/icons/generated/shard-21.cjs +1 -1
  203. package/dist/icons/generated/shard-21.cjs.map +1 -1
  204. package/dist/icons/generated/shard-21.js +41 -41
  205. package/dist/icons/generated/shard-21.js.map +1 -1
  206. package/dist/icons/generated/shard-22.cjs +1 -1
  207. package/dist/icons/generated/shard-22.cjs.map +1 -1
  208. package/dist/icons/generated/shard-22.js +41 -41
  209. package/dist/icons/generated/shard-22.js.map +1 -1
  210. package/dist/icons/generated/shard-23.cjs +1 -1
  211. package/dist/icons/generated/shard-23.cjs.map +1 -1
  212. package/dist/icons/generated/shard-23.js +41 -41
  213. package/dist/icons/generated/shard-23.js.map +1 -1
  214. package/dist/icons/generated/shard-24.cjs +1 -1
  215. package/dist/icons/generated/shard-24.cjs.map +1 -1
  216. package/dist/icons/generated/shard-24.js +41 -41
  217. package/dist/icons/generated/shard-24.js.map +1 -1
  218. package/dist/icons/generated/shard-25.cjs +1 -1
  219. package/dist/icons/generated/shard-25.cjs.map +1 -1
  220. package/dist/icons/generated/shard-25.js +41 -41
  221. package/dist/icons/generated/shard-25.js.map +1 -1
  222. package/dist/icons/generated/shard-26.cjs +1 -1
  223. package/dist/icons/generated/shard-26.cjs.map +1 -1
  224. package/dist/icons/generated/shard-26.js +41 -41
  225. package/dist/icons/generated/shard-26.js.map +1 -1
  226. package/dist/icons/generated/shard-27.cjs +1 -1
  227. package/dist/icons/generated/shard-27.cjs.map +1 -1
  228. package/dist/icons/generated/shard-27.js +41 -41
  229. package/dist/icons/generated/shard-27.js.map +1 -1
  230. package/dist/icons/generated/shard-28.cjs +1 -1
  231. package/dist/icons/generated/shard-28.cjs.map +1 -1
  232. package/dist/icons/generated/shard-28.js +41 -41
  233. package/dist/icons/generated/shard-28.js.map +1 -1
  234. package/dist/icons/generated/shard-29.cjs +1 -1
  235. package/dist/icons/generated/shard-29.cjs.map +1 -1
  236. package/dist/icons/generated/shard-29.js +41 -41
  237. package/dist/icons/generated/shard-29.js.map +1 -1
  238. package/dist/icons/generated/shard-30.cjs +1 -1
  239. package/dist/icons/generated/shard-30.cjs.map +1 -1
  240. package/dist/icons/generated/shard-30.js +41 -41
  241. package/dist/icons/generated/shard-30.js.map +1 -1
  242. package/dist/icons/generated/shard-31.cjs +1 -1
  243. package/dist/icons/generated/shard-31.cjs.map +1 -1
  244. package/dist/icons/generated/shard-31.js +41 -41
  245. package/dist/icons/generated/shard-31.js.map +1 -1
  246. package/dist/icons/generated/shard-32.cjs +1 -1
  247. package/dist/icons/generated/shard-32.cjs.map +1 -1
  248. package/dist/icons/generated/shard-32.js +41 -41
  249. package/dist/icons/generated/shard-32.js.map +1 -1
  250. package/dist/icons/generated/shard-33.cjs +1 -1
  251. package/dist/icons/generated/shard-33.cjs.map +1 -1
  252. package/dist/icons/generated/shard-33.js +41 -41
  253. package/dist/icons/generated/shard-33.js.map +1 -1
  254. package/dist/icons/generated/shard-34.cjs +1 -1
  255. package/dist/icons/generated/shard-34.cjs.map +1 -1
  256. package/dist/icons/generated/shard-34.js +41 -41
  257. package/dist/icons/generated/shard-34.js.map +1 -1
  258. package/dist/icons/generated/shard-35.cjs +1 -1
  259. package/dist/icons/generated/shard-35.cjs.map +1 -1
  260. package/dist/icons/generated/shard-35.js +41 -41
  261. package/dist/icons/generated/shard-35.js.map +1 -1
  262. package/dist/icons/generated/shard-36.cjs +1 -1
  263. package/dist/icons/generated/shard-36.cjs.map +1 -1
  264. package/dist/icons/generated/shard-36.js +41 -41
  265. package/dist/icons/generated/shard-36.js.map +1 -1
  266. package/dist/icons/generated/shard-37.cjs +1 -1
  267. package/dist/icons/generated/shard-37.cjs.map +1 -1
  268. package/dist/icons/generated/shard-37.js +41 -41
  269. package/dist/icons/generated/shard-37.js.map +1 -1
  270. package/dist/icons/generated/shard-38.cjs +1 -1
  271. package/dist/icons/generated/shard-38.cjs.map +1 -1
  272. package/dist/icons/generated/shard-38.js +41 -41
  273. package/dist/icons/generated/shard-38.js.map +1 -1
  274. package/dist/icons/generated/shard-39.cjs +1 -1
  275. package/dist/icons/generated/shard-39.cjs.map +1 -1
  276. package/dist/icons/generated/shard-39.js +41 -41
  277. package/dist/icons/generated/shard-39.js.map +1 -1
  278. package/dist/icons/generated/shard-40.cjs +1 -1
  279. package/dist/icons/generated/shard-40.cjs.map +1 -1
  280. package/dist/icons/generated/shard-40.js +41 -41
  281. package/dist/icons/generated/shard-40.js.map +1 -1
  282. package/dist/icons/generated/shard-41.cjs +1 -1
  283. package/dist/icons/generated/shard-41.cjs.map +1 -1
  284. package/dist/icons/generated/shard-41.js +41 -41
  285. package/dist/icons/generated/shard-41.js.map +1 -1
  286. package/dist/icons/generated/shard-42.cjs +1 -1
  287. package/dist/icons/generated/shard-42.cjs.map +1 -1
  288. package/dist/icons/generated/shard-42.js +41 -41
  289. package/dist/icons/generated/shard-42.js.map +1 -1
  290. package/dist/icons/generated/shard-43.cjs +1 -1
  291. package/dist/icons/generated/shard-43.cjs.map +1 -1
  292. package/dist/icons/generated/shard-43.js +41 -41
  293. package/dist/icons/generated/shard-43.js.map +1 -1
  294. package/dist/icons/generated/shard-44.cjs +1 -1
  295. package/dist/icons/generated/shard-44.cjs.map +1 -1
  296. package/dist/icons/generated/shard-44.js +43 -10
  297. package/dist/icons/generated/shard-44.js.map +1 -1
  298. package/dist/icons/generated/shard-45.cjs +2 -0
  299. package/dist/icons/generated/shard-45.cjs.map +1 -0
  300. package/dist/icons/generated/shard-45.js +15 -0
  301. package/dist/icons/generated/shard-45.js.map +1 -0
  302. package/dist/icons/material-symbols.cjs +1 -1
  303. package/dist/icons/material-symbols.cjs.map +1 -1
  304. package/dist/icons/material-symbols.js +3 -3
  305. package/dist/icons/material-symbols.js.map +1 -1
  306. package/dist/icons.d.ts +4 -4
  307. package/dist/query/foreign-client-warning.cjs +1 -1
  308. package/dist/query/foreign-client-warning.cjs.map +1 -1
  309. package/dist/query/foreign-client-warning.js +1 -1
  310. package/dist/query/foreign-client-warning.js.map +1 -1
  311. package/dist/styles/Alert.css +1 -1
  312. package/dist/styles/Badge.css +1 -1
  313. package/dist/styles/BarList.css +3 -3
  314. package/dist/styles/BottomSheet.css +1 -1
  315. package/dist/styles/DataTable.css +1 -1
  316. package/dist/styles/Dropzone.css +1 -1
  317. package/dist/styles/ErrorText.css +1 -1
  318. package/dist/styles/Markdown.css +1 -1
  319. package/dist/styles/PasswordInput.css +1 -1
  320. package/dist/styles/SegmentedControl.css +1 -1
  321. package/dist/styles/Sidebar.css +2 -2
  322. package/dist/styles/StepperInput.css +1 -1
  323. package/dist/styles/Tag.css +1 -1
  324. package/dist/styles/Timeline.css +1 -1
  325. package/dist/styles/Toggle.css +1 -1
  326. package/dist/styles/ToggleGroup.css +1 -1
  327. package/dist/styles/advanced.css +2 -2
  328. package/dist/styles/core.css +4 -2
  329. package/dist/styles/data.css +6 -6
  330. package/dist/styles/feedback.css +3 -3
  331. package/dist/styles/forms.css +3 -3
  332. package/dist/styles/navigation.css +3 -3
  333. package/dist/styles/overlay.css +1 -1
  334. package/dist/styles/utility.css +1 -1
  335. package/dist/styles.css +1 -1
  336. package/dist/tempest-react-sdk.cjs +1 -1
  337. package/dist/tempest-react-sdk.d.ts +81 -7
  338. package/dist/tempest-react-sdk.js +347 -346
  339. package/dist/theme/create-theme.cjs +3 -3
  340. package/dist/theme/create-theme.cjs.map +1 -1
  341. package/dist/theme/create-theme.js +15 -14
  342. package/dist/theme/create-theme.js.map +1 -1
  343. package/dist/utils/dev-mode.cjs +1 -1
  344. package/dist/utils/dev-mode.cjs.map +1 -1
  345. package/dist/utils/dev-mode.js +7 -2
  346. package/dist/utils/dev-mode.js.map +1 -1
  347. package/dist/utils/json-frame.cjs +1 -1
  348. package/dist/utils/json-frame.cjs.map +1 -1
  349. package/dist/utils/json-frame.js +1 -1
  350. package/dist/utils/json-frame.js.map +1 -1
  351. package/dist/vision/core/providers.cjs +1 -1
  352. package/dist/vision/core/providers.cjs.map +1 -1
  353. package/dist/vision/core/providers.js +22 -3
  354. package/dist/vision/core/providers.js.map +1 -1
  355. package/dist/vision/core/session.cjs +1 -1
  356. package/dist/vision/core/session.cjs.map +1 -1
  357. package/dist/vision/core/session.js +22 -15
  358. package/dist/vision/core/session.js.map +1 -1
  359. package/dist/vision/fusion.cjs.map +1 -1
  360. package/dist/vision/fusion.js.map +1 -1
  361. package/dist/vision/index.cjs +1 -1
  362. package/dist/vision/index.cjs.map +1 -1
  363. package/dist/vision/index.js +17 -16
  364. package/dist/vision/index.js.map +1 -1
  365. package/dist/vision/normalization.cjs +2 -0
  366. package/dist/vision/normalization.cjs.map +1 -0
  367. package/dist/vision/normalization.js +49 -0
  368. package/dist/vision/normalization.js.map +1 -0
  369. package/dist/vision/results.cjs +1 -1
  370. package/dist/vision/results.cjs.map +1 -1
  371. package/dist/vision/results.js +8 -2
  372. package/dist/vision/results.js.map +1 -1
  373. package/dist/vision/tasks/classifier.cjs +1 -1
  374. package/dist/vision/tasks/classifier.cjs.map +1 -1
  375. package/dist/vision/tasks/classifier.js +36 -32
  376. package/dist/vision/tasks/classifier.js.map +1 -1
  377. package/dist/vision.cjs +1 -1
  378. package/dist/vision.d.ts +218 -13
  379. package/dist/vision.js +19 -18
  380. package/dist/vite/tempest-pwa-manifest.cjs +1 -1
  381. package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
  382. package/dist/vite/tempest-pwa-manifest.js +1 -1
  383. package/dist/vite/tempest-pwa-manifest.js.map +1 -1
  384. package/dist/webrtc/peer-link.cjs +1 -1
  385. package/dist/webrtc/peer-link.cjs.map +1 -1
  386. package/dist/webrtc/peer-link.js +1 -1
  387. package/dist/webrtc/peer-link.js.map +1 -1
  388. package/dist/webrtc/peer-mesh.cjs +1 -1
  389. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  390. package/dist/webrtc/peer-mesh.js +1 -1
  391. package/dist/webrtc/peer-mesh.js.map +1 -1
  392. package/dist/ws/create-web-socket.cjs +1 -1
  393. package/dist/ws/create-web-socket.cjs.map +1 -1
  394. package/dist/ws/create-web-socket.js +1 -1
  395. package/dist/ws/create-web-socket.js.map +1 -1
  396. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"Chat.js","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — message grouping needs\n * more context than a list does: currentUserId to decide sides, groupWindowMs and\n * now to decide where a bubble group breaks, typing for the indicator. The remaining\n * props are the composer's (placeholder, composerActions, composerDisabled, onSend,\n * onSendError) and the slots (renderAvatar, header, emptyState).\n */\nimport { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport {\n chatStrings,\n dayLabel,\n groupMessages,\n timeLabel,\n typingLabel,\n type ChatMessage,\n} from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The thread, **oldest first**. Never reordered by the component. */\n messages: readonly ChatMessage[];\n /** Author id treated as \"own\" — decides side, colour and status ticks. */\n currentUserId?: string;\n /** Renders the composer when given. Receives the trimmed text. */\n onSend?: (text: string) => void | Promise<void>;\n /** Enables the retry control on a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Names currently typing. One, two or a count is phrased for you. */\n typing?: readonly string[];\n /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n renderAvatar?: (message: ChatMessage) => ReactNode;\n /** Rendered above the thread, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no messages. */\n emptyState?: ReactNode;\n /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n groupWindowMs?: number;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n now?: number;\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Disable the composer — no permission, thread archived, offline. */\n composerDisabled?: boolean;\n /** Called when `onSend` rejects. The draft stays in the field either way. */\n onSendError?: (error: unknown) => void;\n}\n\n/** Status glyph and label for an outgoing message. */\nconst STATUS_GLYPH: Record<NonNullable<ChatMessage[\"status\"]>, string> = {\n sending: \"◌\",\n sent: \"✓\",\n read: \"✓✓\",\n failed: \"!\",\n};\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n * messages={messages}\n * currentUserId={me.id}\n * typing={typingNames}\n * onSend={(text) => send({ text })}\n * onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n messages,\n currentUserId,\n onSend,\n onRetry,\n typing = [],\n renderAvatar,\n header,\n emptyState,\n groupWindowMs,\n locale = \"pt-BR\",\n now,\n placeholder,\n composerActions,\n composerDisabled,\n onSendError,\n className,\n ...rest\n}: ChatProps) {\n const strings = chatStrings(locale);\n const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n const composer = useRef<ChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n\n /**\n * Remember whether the reader is at the bottom, before the next batch lands.\n *\n * A thread that always scrolls to the newest message yanks somebody out of the\n * history they were reading, every time anyone types. So the jump only happens\n * when they were already at the bottom — the same rule every chat app converges\n * on. The 48px slack covers a partially visible last row.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n stuckToBottom.current = distance < 48;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [messages, typing]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest message; from then on `trackPosition` decides.\n }, []);\n\n const typingText = typingLabel(typing, locale);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {messages.length === 0\n ? (emptyState ?? <EmptyState title={strings.empty} />)\n : sections.map((section) =>\n section.kind === \"day\" ? (\n <div key={section.key} className={styles.day}>\n <span className={styles.dayLabel}>\n {dayLabel(section.date, { locale, now })}\n </span>\n </div>\n ) : (\n <div\n key={section.key}\n className={cn(styles.run, section.own && styles.ownRun)}\n >\n {renderAvatar && (\n <div className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(section.messages[0])}\n </div>\n )}\n <div className={styles.runBody}>\n <span className={styles.author}>\n {section.own\n ? strings.you\n : (section.authorName ?? section.authorId)}\n </span>\n <ul className={styles.bubbles}>\n {section.messages.map((message) => (\n <li key={message.id} className={styles.bubbleRow}>\n <div\n className={cn(\n styles.bubble,\n section.own && styles.ownBubble,\n message.status === \"failed\" &&\n styles.failedBubble,\n )}\n >\n <div className={styles.body}>\n {message.body}\n </div>\n <div className={styles.meta}>\n <time\n dateTime={new Date(\n message.sentAt,\n ).toISOString()}\n >\n {timeLabel(message.sentAt, locale)}\n </time>\n {section.own && message.status && (\n <span\n className={cn(\n styles.status,\n message.status === \"failed\" &&\n styles.statusFailed,\n )}\n title={strings[message.status]}\n >\n <span aria-hidden=\"true\">\n {STATUS_GLYPH[message.status]}\n </span>\n <VisuallyHidden>\n {strings[message.status]}\n </VisuallyHidden>\n </span>\n )}\n </div>\n </div>\n {message.status === \"failed\" && onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </li>\n ))}\n </ul>\n </div>\n </div>\n ),\n )}\n </div>\n\n {typingText && (\n <p className={styles.typing} aria-live=\"polite\">\n {typingText}\n </p>\n )}\n\n {onSend && (\n <ChatComposer\n ref={composer}\n onSend={onSend}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n disabled={composerDisabled}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Chat.js","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — message grouping needs\n * more context than a list does: currentUserId to decide sides, groupWindowMs and\n * now to decide where a bubble group breaks, typing for the indicator. The remaining\n * props are the composer's (placeholder, composerActions, composerDisabled, onSend,\n * onSendError) and the slots (renderAvatar, header, emptyState).\n */\nimport { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport {\n chatStrings,\n dayLabel,\n groupMessages,\n timeLabel,\n typingLabel,\n type ChatMessage,\n} from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The thread, **oldest first**. Never reordered by the component. */\n messages: readonly ChatMessage[];\n /** Author id treated as \"own\" — decides side, colour and status ticks. */\n currentUserId?: string;\n /** Renders the composer when given. Receives the trimmed text. */\n onSend?: (text: string) => void | Promise<void>;\n /** Enables the retry control on a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Names currently typing. One, two or a count is phrased for you. */\n typing?: readonly string[];\n /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n renderAvatar?: (message: ChatMessage) => ReactNode;\n /** Rendered above the thread, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no messages. */\n emptyState?: ReactNode;\n /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n groupWindowMs?: number;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n now?: number;\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Disable the composer — no permission, thread archived, offline. */\n composerDisabled?: boolean;\n /** Called when `onSend` rejects. The draft stays in the field either way. */\n onSendError?: (error: unknown) => void;\n}\n\n/** Status glyph and label for an outgoing message. */\nconst STATUS_GLYPH: Record<NonNullable<ChatMessage[\"status\"]>, string> = {\n sending: \"◌\",\n sent: \"✓\",\n read: \"✓✓\",\n failed: \"!\",\n};\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n * messages={messages}\n * currentUserId={me.id}\n * typing={typingNames}\n * onSend={(text) => send({ text })}\n * onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n messages,\n currentUserId,\n onSend,\n onRetry,\n typing = [],\n renderAvatar,\n header,\n emptyState,\n groupWindowMs,\n locale = \"pt-BR\",\n now,\n placeholder,\n composerActions,\n composerDisabled,\n onSendError,\n className,\n ...rest\n}: ChatProps) {\n const strings = chatStrings(locale);\n const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n const composer = useRef<ChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n\n /**\n * Remember whether the reader is at the bottom, before the next batch lands.\n *\n * A thread that always scrolls to the newest message yanks somebody out of the\n * history they were reading, every time anyone types. So the jump only happens\n * when they were already at the bottom — the same rule every chat app converges\n * on. The 48px slack covers a partially visible last row.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n stuckToBottom.current = distance < 48;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [messages, typing]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest message; from then on `trackPosition` decides.\n }, []);\n\n const typingText = typingLabel(typing, locale);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {messages.length === 0\n ? (emptyState ?? <EmptyState title={strings.empty} />)\n : sections.map((section) =>\n section.kind === \"day\" ? (\n <div key={section.key} className={styles.day}>\n <span className={styles.dayLabel}>\n {dayLabel(section.date, { locale, now })}\n </span>\n </div>\n ) : (\n <div\n key={section.key}\n className={cn(styles.run, section.own && styles.ownRun)}\n >\n {renderAvatar && (\n <div className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(section.messages[0])}\n </div>\n )}\n <div className={styles.runBody}>\n <span className={styles.author}>\n {section.own\n ? strings.you\n : (section.authorName ?? section.authorId)}\n </span>\n <ul className={styles.bubbles}>\n {section.messages.map((message) => (\n <li key={message.id} className={styles.bubbleRow}>\n <div\n className={cn(\n styles.bubble,\n section.own && styles.ownBubble,\n message.status === \"failed\" &&\n styles.failedBubble,\n )}\n >\n <div className={styles.body}>\n {message.body}\n </div>\n <div className={styles.meta}>\n <time\n dateTime={new Date(\n message.sentAt,\n ).toISOString()}\n >\n {timeLabel(message.sentAt, locale)}\n </time>\n {section.own && message.status && (\n <span\n className={cn(\n styles.status,\n message.status === \"failed\" &&\n styles.statusFailed,\n )}\n title={strings[message.status]}\n >\n <span aria-hidden=\"true\">\n {STATUS_GLYPH[message.status]}\n </span>\n <VisuallyHidden>\n {strings[message.status]}\n </VisuallyHidden>\n </span>\n )}\n </div>\n </div>\n {message.status === \"failed\" && onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </li>\n ))}\n </ul>\n </div>\n </div>\n ),\n )}\n </div>\n\n {typingText && (\n <p className={styles.typing} aria-live=\"polite\">\n {typingText}\n </p>\n )}\n\n {onSend && (\n <ChatComposer\n ref={composer}\n onSend={onSend}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n disabled={composerDisabled}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.module.cjs","names":[],"sources":["../../../src/components/DataTable/DataTable.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n}\n\n.search {\n max-width: 20rem;\n}\n\n/*\n * A page swap keeps the rows it already has, dimmed. Replacing them with a\n * spinner would collapse the table to nothing and bounce whatever sits below it\n * back up, which is the jump the user's cursor was resting on.\n */\n.pending {\n opacity: 0.6;\n transition: opacity var(--tempest-duration-fast, 120ms) ease;\n pointer-events: none;\n}\n\n.loading {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n padding: var(--tempest-space-3, 0.75rem) 0;\n}\n\n.loadingRow {\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: 1fr;\n gap: var(--tempest-space-4, 1rem);\n align-items: center;\n}\n\n.sortButton {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n padding: 0;\n border: none;\n background: none;\n font: inherit;\n color: inherit;\n cursor: pointer;\n user-select: none;\n}\n\n.sortIndicator {\n font-size: 0.75em;\n opacity: 0.7;\n}\n\n/* ── Inline editing ─────────────────────────────────────────────────────────── */\n\n.cellWrap {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1, 0.25rem);\n align-items: stretch;\n width: 100%;\n}\n\n/*\n * The dashed bottom edge is a resting affordance, not only a hover one: on a touch\n * screen there is no hover, so an editable cell that looks exactly like a read-only\n * one is undiscoverable. It is the weakest mark that still says \"this is editable\";\n * hover and focus then promote it to a full box.\n */\n.cellButton {\n position: relative;\n display: block;\n width: 100%;\n margin: calc(-1 * var(--tempest-space-1, 0.25rem)) calc(-1 * var(--tempest-space-2, 0.5rem));\n padding: var(--tempest-space-1, 0.25rem) var(--tempest-space-2, 0.5rem);\n border: 1px dashed transparent;\n border-bottom-color: var(--tempest-border-strong);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background: none;\n font: inherit;\n color: inherit;\n text-align: inherit;\n cursor: pointer;\n}\n\n.cellButton:hover {\n border-color: var(--tempest-border-strong);\n background-color: var(--tempest-surface-2);\n}\n\n.cellButton:focus-visible {\n outline: none;\n border-color: transparent;\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.cellButtonInvalid {\n border-color: var(--tempest-danger-border);\n}\n\n/*\n * Touch: grow the hit area to the cell's own padding — exactly `--tempest-space-3`\n * vertically and `--tempest-space-4` horizontally in `Table.module.css` — so the\n * whole `<td>` is tappable and the target clears 44px without spilling into the\n * neighbouring row, which a symmetric slop would do in a dense table.\n */\n@media (pointer: coarse) {\n .cellButton::after {\n content: \"\";\n position: absolute;\n inset-block: calc(-1 * var(--tempest-space-3, 0.75rem));\n inset-inline: calc(-1 * var(--tempest-space-4, 1rem));\n }\n}\n\n.cellButton[aria-busy=\"true\"] {\n opacity: 0.6;\n}\n\n.cellEditor {\n width: 100%;\n min-width: 0;\n margin: calc(-1 * var(--tempest-space-1, 0.25rem)) calc(-1 * var(--tempest-space-2, 0.5rem));\n padding: var(--tempest-space-1, 0.25rem) var(--tempest-space-2, 0.5rem);\n border: 1px solid var(--tempest-border-strong);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-bg);\n font: inherit;\n color: var(--tempest-text);\n}\n\n.cellEditor:focus {\n outline: none;\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.cellEditorInvalid {\n border-color: var(--tempest-danger-border);\n}\n\n/*\n * The message sits on a tinted fill because `--tempest-danger-fg` over\n * `--tempest-danger-bg` is a pair `contrast.test.ts` verifies in both themes;\n * danger text straight onto the table background is not.\n */\n.cellError {\n padding: 0.125rem var(--tempest-space-2, 0.5rem);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-danger-fg);\n font-size: var(--tempest-font-size-sm, 0.8125rem);\n line-height: 1.4;\n white-space: normal;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"DataTable.module.cjs","names":[],"sources":["../../../src/components/DataTable/DataTable.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n}\n\n.search {\n max-width: 20rem;\n}\n\n/*\n * A page swap keeps the rows it already has, dimmed. Replacing them with a\n * spinner would collapse the table to nothing and bounce whatever sits below it\n * back up, which is the jump the user's cursor was resting on.\n */\n.pending {\n opacity: 0.6;\n transition: opacity var(--tempest-duration-fast, 120ms) ease;\n pointer-events: none;\n}\n\n.loading {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n padding: var(--tempest-space-3, 0.75rem) 0;\n}\n\n.loadingRow {\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: 1fr;\n gap: var(--tempest-space-4, 1rem);\n align-items: center;\n}\n\n.sortButton {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n padding: 0;\n border: none;\n background: none;\n font: inherit;\n color: inherit;\n cursor: pointer;\n user-select: none;\n}\n\n.sortIndicator {\n font-size: 0.75em;\n opacity: 0.7;\n}\n\n/* ── Inline editing ─────────────────────────────────────────────────────────── */\n\n.cellWrap {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1, 0.25rem);\n align-items: stretch;\n width: 100%;\n}\n\n/*\n * The dashed bottom edge is a resting affordance, not only a hover one: on a touch\n * screen there is no hover, so an editable cell that looks exactly like a read-only\n * one is undiscoverable. It is the weakest mark that still says \"this is editable\";\n * hover and focus then promote it to a full box.\n */\n.cellButton {\n position: relative;\n display: block;\n width: 100%;\n margin: calc(-1 * var(--tempest-space-1, 0.25rem)) calc(-1 * var(--tempest-space-2, 0.5rem));\n padding: var(--tempest-space-1, 0.25rem) var(--tempest-space-2, 0.5rem);\n border: 1px dashed transparent;\n border-bottom-color: var(--tempest-border-strong);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background: none;\n font: inherit;\n color: inherit;\n text-align: inherit;\n cursor: pointer;\n}\n\n.cellButton:hover {\n border-color: var(--tempest-border-strong);\n background-color: var(--tempest-surface-2);\n}\n\n.cellButton:focus-visible {\n outline: none;\n border-color: transparent;\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.cellButtonInvalid {\n border-color: var(--tempest-danger-border);\n}\n\n/*\n * Touch: grow the hit area to the cell's own padding — exactly `--tempest-space-3`\n * vertically and `--tempest-space-4` horizontally in `Table.module.css` — so the\n * whole `<td>` is tappable and the target clears 44px without spilling into the\n * neighbouring row, which a symmetric slop would do in a dense table.\n */\n@media (pointer: coarse) {\n .cellButton::after {\n content: \"\";\n position: absolute;\n inset-block: calc(-1 * var(--tempest-space-3, 0.75rem));\n inset-inline: calc(-1 * var(--tempest-space-4, 1rem));\n }\n}\n\n.cellButton[aria-busy=\"true\"] {\n opacity: 0.6;\n}\n\n.cellEditor {\n width: 100%;\n min-width: 0;\n margin: calc(-1 * var(--tempest-space-1, 0.25rem)) calc(-1 * var(--tempest-space-2, 0.5rem));\n padding: var(--tempest-space-1, 0.25rem) var(--tempest-space-2, 0.5rem);\n border: 1px solid var(--tempest-border-strong);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-bg);\n font: inherit;\n color: var(--tempest-text);\n}\n\n.cellEditor:focus {\n outline: none;\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.cellEditorInvalid {\n border-color: var(--tempest-danger-border);\n}\n\n/*\n * The message sits on a tinted fill because `--tempest-danger-fg` over\n * `--tempest-danger-bg` is a pair `contrast.test.ts` verifies in both themes;\n * danger text straight onto the table background is not.\n */\n.cellError {\n padding: 0.125rem var(--tempest-space-2, 0.5rem);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-danger-fg);\n font-size: var(--tempest-text-sm);\n line-height: 1.4;\n white-space: normal;\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.module.js","names":[],"sources":["../../../src/components/DataTable/DataTable.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n}\n\n.search {\n max-width: 20rem;\n}\n\n/*\n * A page swap keeps the rows it already has, dimmed. Replacing them with a\n * spinner would collapse the table to nothing and bounce whatever sits below it\n * back up, which is the jump the user's cursor was resting on.\n */\n.pending {\n opacity: 0.6;\n transition: opacity var(--tempest-duration-fast, 120ms) ease;\n pointer-events: none;\n}\n\n.loading {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n padding: var(--tempest-space-3, 0.75rem) 0;\n}\n\n.loadingRow {\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: 1fr;\n gap: var(--tempest-space-4, 1rem);\n align-items: center;\n}\n\n.sortButton {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n padding: 0;\n border: none;\n background: none;\n font: inherit;\n color: inherit;\n cursor: pointer;\n user-select: none;\n}\n\n.sortIndicator {\n font-size: 0.75em;\n opacity: 0.7;\n}\n\n/* ── Inline editing ─────────────────────────────────────────────────────────── */\n\n.cellWrap {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1, 0.25rem);\n align-items: stretch;\n width: 100%;\n}\n\n/*\n * The dashed bottom edge is a resting affordance, not only a hover one: on a touch\n * screen there is no hover, so an editable cell that looks exactly like a read-only\n * one is undiscoverable. It is the weakest mark that still says \"this is editable\";\n * hover and focus then promote it to a full box.\n */\n.cellButton {\n position: relative;\n display: block;\n width: 100%;\n margin: calc(-1 * var(--tempest-space-1, 0.25rem)) calc(-1 * var(--tempest-space-2, 0.5rem));\n padding: var(--tempest-space-1, 0.25rem) var(--tempest-space-2, 0.5rem);\n border: 1px dashed transparent;\n border-bottom-color: var(--tempest-border-strong);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background: none;\n font: inherit;\n color: inherit;\n text-align: inherit;\n cursor: pointer;\n}\n\n.cellButton:hover {\n border-color: var(--tempest-border-strong);\n background-color: var(--tempest-surface-2);\n}\n\n.cellButton:focus-visible {\n outline: none;\n border-color: transparent;\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.cellButtonInvalid {\n border-color: var(--tempest-danger-border);\n}\n\n/*\n * Touch: grow the hit area to the cell's own padding — exactly `--tempest-space-3`\n * vertically and `--tempest-space-4` horizontally in `Table.module.css` — so the\n * whole `<td>` is tappable and the target clears 44px without spilling into the\n * neighbouring row, which a symmetric slop would do in a dense table.\n */\n@media (pointer: coarse) {\n .cellButton::after {\n content: \"\";\n position: absolute;\n inset-block: calc(-1 * var(--tempest-space-3, 0.75rem));\n inset-inline: calc(-1 * var(--tempest-space-4, 1rem));\n }\n}\n\n.cellButton[aria-busy=\"true\"] {\n opacity: 0.6;\n}\n\n.cellEditor {\n width: 100%;\n min-width: 0;\n margin: calc(-1 * var(--tempest-space-1, 0.25rem)) calc(-1 * var(--tempest-space-2, 0.5rem));\n padding: var(--tempest-space-1, 0.25rem) var(--tempest-space-2, 0.5rem);\n border: 1px solid var(--tempest-border-strong);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-bg);\n font: inherit;\n color: var(--tempest-text);\n}\n\n.cellEditor:focus {\n outline: none;\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.cellEditorInvalid {\n border-color: var(--tempest-danger-border);\n}\n\n/*\n * The message sits on a tinted fill because `--tempest-danger-fg` over\n * `--tempest-danger-bg` is a pair `contrast.test.ts` verifies in both themes;\n * danger text straight onto the table background is not.\n */\n.cellError {\n padding: 0.125rem var(--tempest-space-2, 0.5rem);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-danger-fg);\n font-size: var(--tempest-font-size-sm, 0.8125rem);\n line-height: 1.4;\n white-space: normal;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"DataTable.module.js","names":[],"sources":["../../../src/components/DataTable/DataTable.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n}\n\n.search {\n max-width: 20rem;\n}\n\n/*\n * A page swap keeps the rows it already has, dimmed. Replacing them with a\n * spinner would collapse the table to nothing and bounce whatever sits below it\n * back up, which is the jump the user's cursor was resting on.\n */\n.pending {\n opacity: 0.6;\n transition: opacity var(--tempest-duration-fast, 120ms) ease;\n pointer-events: none;\n}\n\n.loading {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n padding: var(--tempest-space-3, 0.75rem) 0;\n}\n\n.loadingRow {\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: 1fr;\n gap: var(--tempest-space-4, 1rem);\n align-items: center;\n}\n\n.sortButton {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n padding: 0;\n border: none;\n background: none;\n font: inherit;\n color: inherit;\n cursor: pointer;\n user-select: none;\n}\n\n.sortIndicator {\n font-size: 0.75em;\n opacity: 0.7;\n}\n\n/* ── Inline editing ─────────────────────────────────────────────────────────── */\n\n.cellWrap {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1, 0.25rem);\n align-items: stretch;\n width: 100%;\n}\n\n/*\n * The dashed bottom edge is a resting affordance, not only a hover one: on a touch\n * screen there is no hover, so an editable cell that looks exactly like a read-only\n * one is undiscoverable. It is the weakest mark that still says \"this is editable\";\n * hover and focus then promote it to a full box.\n */\n.cellButton {\n position: relative;\n display: block;\n width: 100%;\n margin: calc(-1 * var(--tempest-space-1, 0.25rem)) calc(-1 * var(--tempest-space-2, 0.5rem));\n padding: var(--tempest-space-1, 0.25rem) var(--tempest-space-2, 0.5rem);\n border: 1px dashed transparent;\n border-bottom-color: var(--tempest-border-strong);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background: none;\n font: inherit;\n color: inherit;\n text-align: inherit;\n cursor: pointer;\n}\n\n.cellButton:hover {\n border-color: var(--tempest-border-strong);\n background-color: var(--tempest-surface-2);\n}\n\n.cellButton:focus-visible {\n outline: none;\n border-color: transparent;\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.cellButtonInvalid {\n border-color: var(--tempest-danger-border);\n}\n\n/*\n * Touch: grow the hit area to the cell's own padding — exactly `--tempest-space-3`\n * vertically and `--tempest-space-4` horizontally in `Table.module.css` — so the\n * whole `<td>` is tappable and the target clears 44px without spilling into the\n * neighbouring row, which a symmetric slop would do in a dense table.\n */\n@media (pointer: coarse) {\n .cellButton::after {\n content: \"\";\n position: absolute;\n inset-block: calc(-1 * var(--tempest-space-3, 0.75rem));\n inset-inline: calc(-1 * var(--tempest-space-4, 1rem));\n }\n}\n\n.cellButton[aria-busy=\"true\"] {\n opacity: 0.6;\n}\n\n.cellEditor {\n width: 100%;\n min-width: 0;\n margin: calc(-1 * var(--tempest-space-1, 0.25rem)) calc(-1 * var(--tempest-space-2, 0.5rem));\n padding: var(--tempest-space-1, 0.25rem) var(--tempest-space-2, 0.5rem);\n border: 1px solid var(--tempest-border-strong);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-bg);\n font: inherit;\n color: var(--tempest-text);\n}\n\n.cellEditor:focus {\n outline: none;\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.cellEditorInvalid {\n border-color: var(--tempest-danger-border);\n}\n\n/*\n * The message sits on a tinted fill because `--tempest-danger-fg` over\n * `--tempest-danger-bg` is a pair `contrast.test.ts` verifies in both themes;\n * danger text straight onto the table background is not.\n */\n.cellError {\n padding: 0.125rem var(--tempest-space-2, 0.5rem);\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-danger-fg);\n font-size: var(--tempest-text-sm);\n line-height: 1.4;\n white-space: normal;\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"Dropzone.module.cjs","names":[],"sources":["../../../src/components/Dropzone/Dropzone.module.css"],"sourcesContent":[".zone {\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n min-height: 120px;\n padding: var(--tempest-space-6, 24px);\n border: 2px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-lg, 12px);\n background: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n transition:\n border-color var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out),\n background var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out);\n}\n\n.zone:hover,\n.zone:focus-visible {\n border-color: var(--tempest-primary);\n color: var(--tempest-text);\n outline: none;\n}\n\n.dragging {\n border-color: var(--tempest-primary);\n background: var(--tempest-primary-subtle, var(--tempest-surface));\n color: var(--tempest-text);\n}\n\n.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.input {\n display: none;\n}\n\n.prompt {\n line-height: 1.4;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"Dropzone.module.cjs","names":[],"sources":["../../../src/components/Dropzone/Dropzone.module.css"],"sourcesContent":[".zone {\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n min-height: 120px;\n padding: var(--tempest-space-6, 24px);\n border: 2px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-lg, 12px);\n background: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n transition:\n border-color var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out),\n background var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out);\n}\n\n.zone:hover,\n.zone:focus-visible {\n border-color: var(--tempest-primary);\n color: var(--tempest-text);\n outline: none;\n}\n\n.dragging {\n border-color: var(--tempest-primary);\n background: var(--tempest-primary-soft);\n color: var(--tempest-text);\n}\n\n.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.input {\n display: none;\n}\n\n.prompt {\n line-height: 1.4;\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"Dropzone.module.js","names":[],"sources":["../../../src/components/Dropzone/Dropzone.module.css"],"sourcesContent":[".zone {\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n min-height: 120px;\n padding: var(--tempest-space-6, 24px);\n border: 2px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-lg, 12px);\n background: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n transition:\n border-color var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out),\n background var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out);\n}\n\n.zone:hover,\n.zone:focus-visible {\n border-color: var(--tempest-primary);\n color: var(--tempest-text);\n outline: none;\n}\n\n.dragging {\n border-color: var(--tempest-primary);\n background: var(--tempest-primary-subtle, var(--tempest-surface));\n color: var(--tempest-text);\n}\n\n.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.input {\n display: none;\n}\n\n.prompt {\n line-height: 1.4;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"Dropzone.module.js","names":[],"sources":["../../../src/components/Dropzone/Dropzone.module.css"],"sourcesContent":[".zone {\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n min-height: 120px;\n padding: var(--tempest-space-6, 24px);\n border: 2px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-lg, 12px);\n background: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n transition:\n border-color var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out),\n background var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out);\n}\n\n.zone:hover,\n.zone:focus-visible {\n border-color: var(--tempest-primary);\n color: var(--tempest-text);\n outline: none;\n}\n\n.dragging {\n border-color: var(--tempest-primary);\n background: var(--tempest-primary-soft);\n color: var(--tempest-text);\n}\n\n.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.input {\n display: none;\n}\n\n.prompt {\n line-height: 1.4;\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"ErrorText.module.cjs","names":[],"sources":["../../../src/components/ErrorText/ErrorText.module.css"],"sourcesContent":[".errorText {\n margin: 0;\n font-size: var(--tempest-font-size-sm, 0.8125rem);\n line-height: 1.4;\n color: var(--tempest-danger, var(--tempest-color-danger, #dc2626));\n}\n"],"mappings":""}
1
+ {"version":3,"file":"ErrorText.module.cjs","names":[],"sources":["../../../src/components/ErrorText/ErrorText.module.css"],"sourcesContent":[".errorText {\n margin: 0;\n font-size: var(--tempest-text-sm);\n line-height: 1.4;\n color: var(--tempest-danger);\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"ErrorText.module.js","names":[],"sources":["../../../src/components/ErrorText/ErrorText.module.css"],"sourcesContent":[".errorText {\n margin: 0;\n font-size: var(--tempest-font-size-sm, 0.8125rem);\n line-height: 1.4;\n color: var(--tempest-danger, var(--tempest-color-danger, #dc2626));\n}\n"],"mappings":""}
1
+ {"version":3,"file":"ErrorText.module.js","names":[],"sources":["../../../src/components/ErrorText/ErrorText.module.css"],"sourcesContent":[".errorText {\n margin: 0;\n font-size: var(--tempest-text-sm);\n line-height: 1.4;\n color: var(--tempest-danger);\n}\n"],"mappings":""}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("../VisuallyHidden/VisuallyHidden.cjs"),n=require("./filter-model.cjs"),r=require("./FilterBar.module.cjs");let i=require("react"),a=require("react/jsx-runtime");function o(e){return{field:e.name,operator:n.defaultOperator(e),value:``,to:``}}function s(e){return n.isValueless(e.operator)?{field:e.field,operator:e.operator}:e.operator===`between`?{field:e.field,operator:e.operator,value:[e.value,e.to]}:n.isMulti(e.operator)?{field:e.field,operator:e.operator,value:e.value.split(`,`).map(e=>e.trim()).filter(Boolean)}:{field:e.field,operator:e.operator,value:e.value}}function c({fields:c,value:u,onChange:d,locale:f=`pt-BR`,actions:p,className:m,...h}){let g=n.filterStrings(f),_=(0,i.useId)(),[v,y]=(0,i.useState)(null),b=v?c.find(e=>e.name===v.field):void 0,x=v?s(v):null,S=x!==null&&n.isComplete(x),C=()=>{c.length>0&&y(o(c[0]))},w=()=>{!x||!S||(d([...u,x]),y(null))},T=e=>{d(u.filter((t,n)=>n!==e))};return(0,a.jsxs)(`div`,{className:e.cn(r.default.bar,m),...h,children:[(0,a.jsxs)(`ul`,{className:r.default.chips,children:[u.length===0&&!v&&(0,a.jsx)(`li`,{className:r.default.empty,children:g.empty}),u.map((e,t)=>{let i=n.describeFilter(e,c,f);return(0,a.jsxs)(`li`,{className:r.default.chip,children:[(0,a.jsx)(`span`,{children:i}),(0,a.jsx)(`button`,{type:`button`,className:r.default.remove,"aria-label":g.remove(i),onClick:()=>T(t),children:`×`})]},`${e.field}-${e.operator}-${t}`)})]}),v&&b?(0,a.jsxs)(`div`,{className:r.default.editor,role:`group`,"aria-label":g.add,children:[(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:g.field}),(0,a.jsx)(`select`,{className:r.default.select,value:v.field,onChange:e=>{let t=c.find(t=>t.name===e.target.value);t&&y(o(t))},children:c.map(e=>(0,a.jsx)(`option`,{value:e.name,children:e.label},e.name))})]}),(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:g.operator}),(0,a.jsx)(`select`,{className:r.default.select,value:v.operator,onChange:e=>y({...v,operator:e.target.value,value:``,to:``}),children:n.operatorsFor(b).map(e=>(0,a.jsx)(`option`,{value:e,children:n.operatorLabel(e,f)},e))})]}),!n.isValueless(v.operator)&&(0,a.jsx)(l,{id:`${_}-value`,field:b,draft:v,strings:g,onChange:y}),(0,a.jsx)(`button`,{type:`button`,className:r.default.primary,disabled:!S,onClick:w,children:g.apply}),(0,a.jsx)(`button`,{type:`button`,className:r.default.ghost,onClick:()=>y(null),children:g.cancel})]}):(0,a.jsxs)(`div`,{className:r.default.actions,children:[(0,a.jsxs)(`button`,{type:`button`,className:r.default.secondary,disabled:c.length===0,onClick:C,children:[`+ `,g.add]}),u.length>0&&(0,a.jsx)(`button`,{type:`button`,className:r.default.ghost,onClick:()=>d([]),children:g.clear}),p]}),(0,a.jsx)(t.VisuallyHidden,{"aria-live":`polite`,role:`status`,children:g.active(u.length)})]})}function l({id:e,field:i,draft:o,strings:s,onChange:c}){let l=i.type===`number`?`number`:i.type===`date`?`date`:`text`;return i.type===`select`&&!n.isMulti(o.operator)?(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.value}),(0,a.jsxs)(`select`,{id:e,className:r.default.select,value:o.value,onChange:e=>c({...o,value:e.target.value}),children:[(0,a.jsx)(`option`,{value:``,children:`—`}),i.options?.map(e=>(0,a.jsx)(`option`,{value:e.value,children:e.label},e.value))]})]}):i.type===`boolean`?(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.value}),(0,a.jsxs)(`select`,{id:e,className:r.default.select,value:o.value,onChange:e=>c({...o,value:e.target.value}),children:[(0,a.jsx)(`option`,{value:``,children:`—`}),(0,a.jsx)(`option`,{value:`true`,children:s.yes}),(0,a.jsx)(`option`,{value:`false`,children:s.no})]})]}):o.operator===`between`?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.from}),(0,a.jsx)(`input`,{id:e,className:r.default.input,type:l,value:o.value,placeholder:s.from,onChange:e=>c({...o,value:e.target.value})})]}),(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.to}),(0,a.jsx)(`input`,{className:r.default.input,type:l,value:o.to,placeholder:s.to,onChange:e=>c({...o,to:e.target.value})})]})]}):(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.value}),(0,a.jsx)(`input`,{id:e,className:r.default.input,type:l,value:o.value,placeholder:i.placeholder??s.value,onChange:e=>c({...o,value:e.target.value})})]})}exports.FilterBar=c;
1
+ const e=require("../../utils/cn.cjs"),t=require("../VisuallyHidden/VisuallyHidden.cjs"),n=require("./filter-model.cjs"),r=require("./FilterBar.module.cjs");let i=require("react"),a=require("react/jsx-runtime");function o(e){return{field:e.name,operator:n.defaultOperator(e),value:``,to:``}}function s(e){return n.isValueless(e.operator)?{field:e.field,operator:e.operator}:e.operator===`between`?{field:e.field,operator:e.operator,value:[e.value,e.to]}:n.isMulti(e.operator)?{field:e.field,operator:e.operator,value:e.value.split(`,`).map(e=>e.trim()).filter(Boolean)}:{field:e.field,operator:e.operator,value:e.value}}function c({fields:c,value:u,onChange:d,locale:f=`pt-BR`,actions:p,className:m,...h}){let g=n.filterStrings(f),_=(0,i.useId)(),[v,y]=(0,i.useState)(null),b=v?c.find(e=>e.name===v.field):void 0,x=v?s(v):null,S=x!==null&&n.isComplete(x),C=()=>{c.length>0&&y(o(c[0]))},w=()=>{x&&S&&(d([...u,x]),y(null))},T=e=>{d(u.filter((t,n)=>n!==e))};return(0,a.jsxs)(`div`,{className:e.cn(r.default.bar,m),...h,children:[(0,a.jsxs)(`ul`,{className:r.default.chips,children:[u.length===0&&!v&&(0,a.jsx)(`li`,{className:r.default.empty,children:g.empty}),u.map((e,t)=>{let i=n.describeFilter(e,c,f);return(0,a.jsxs)(`li`,{className:r.default.chip,children:[(0,a.jsx)(`span`,{children:i}),(0,a.jsx)(`button`,{type:`button`,className:r.default.remove,"aria-label":g.remove(i),onClick:()=>T(t),children:`×`})]},`${e.field}-${e.operator}-${t}`)})]}),v&&b?(0,a.jsxs)(`div`,{className:r.default.editor,role:`group`,"aria-label":g.add,children:[(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:g.field}),(0,a.jsx)(`select`,{className:r.default.select,value:v.field,onChange:e=>{let t=c.find(t=>t.name===e.target.value);t&&y(o(t))},children:c.map(e=>(0,a.jsx)(`option`,{value:e.name,children:e.label},e.name))})]}),(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:g.operator}),(0,a.jsx)(`select`,{className:r.default.select,value:v.operator,onChange:e=>y({...v,operator:e.target.value,value:``,to:``}),children:n.operatorsFor(b).map(e=>(0,a.jsx)(`option`,{value:e,children:n.operatorLabel(e,f)},e))})]}),!n.isValueless(v.operator)&&(0,a.jsx)(l,{id:`${_}-value`,field:b,draft:v,strings:g,onChange:y}),(0,a.jsx)(`button`,{type:`button`,className:r.default.primary,disabled:!S,onClick:w,children:g.apply}),(0,a.jsx)(`button`,{type:`button`,className:r.default.ghost,onClick:()=>y(null),children:g.cancel})]}):(0,a.jsxs)(`div`,{className:r.default.actions,children:[(0,a.jsxs)(`button`,{type:`button`,className:r.default.secondary,disabled:c.length===0,onClick:C,children:[`+ `,g.add]}),u.length>0&&(0,a.jsx)(`button`,{type:`button`,className:r.default.ghost,onClick:()=>d([]),children:g.clear}),p]}),(0,a.jsx)(t.VisuallyHidden,{"aria-live":`polite`,role:`status`,children:g.active(u.length)})]})}function l({id:e,field:i,draft:o,strings:s,onChange:c}){let l=i.type===`number`?`number`:i.type===`date`?`date`:`text`;return i.type===`select`&&!n.isMulti(o.operator)?(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.value}),(0,a.jsxs)(`select`,{id:e,className:r.default.select,value:o.value,onChange:e=>c({...o,value:e.target.value}),children:[(0,a.jsx)(`option`,{value:``,children:`—`}),i.options?.map(e=>(0,a.jsx)(`option`,{value:e.value,children:e.label},e.value))]})]}):i.type===`boolean`?(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.value}),(0,a.jsxs)(`select`,{id:e,className:r.default.select,value:o.value,onChange:e=>c({...o,value:e.target.value}),children:[(0,a.jsx)(`option`,{value:``,children:`—`}),(0,a.jsx)(`option`,{value:`true`,children:s.yes}),(0,a.jsx)(`option`,{value:`false`,children:s.no})]})]}):o.operator===`between`?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.from}),(0,a.jsx)(`input`,{id:e,className:r.default.input,type:l,value:o.value,placeholder:s.from,onChange:e=>c({...o,value:e.target.value})})]}),(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.to}),(0,a.jsx)(`input`,{className:r.default.input,type:l,value:o.to,placeholder:s.to,onChange:e=>c({...o,to:e.target.value})})]})]}):(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.value}),(0,a.jsx)(`input`,{id:e,className:r.default.input,type:l,value:o.value,placeholder:i.placeholder??s.value,onChange:e=>c({...o,value:e.target.value})})]})}exports.FilterBar=c;
2
2
  //# sourceMappingURL=FilterBar.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"FilterBar.cjs","names":[],"sources":["../../../src/components/FilterBar/FilterBar.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a filter bar renders a different\n * editor per filter kind — select, multi-select, range, date range, text — and the\n * body is that dispatch plus the chip summary, both reading the same value map.\n */\nimport { useId, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n defaultOperator,\n describeFilter,\n filterStrings,\n isComplete,\n isMulti,\n isValueless,\n operatorLabel,\n operatorsFor,\n type Filter,\n type FilterField,\n} from \"./filter-model\";\nimport styles from \"./FilterBar.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\";\n\nexport interface FilterBarProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** Fields the user may filter by. */\n fields: readonly FilterField[];\n /** Applied filters. Controlled. */\n value: readonly Filter[];\n /** Next filter set. Combined with AND. */\n onChange: (filters: Filter[]) => void;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the bar — a \"save this view\" button, a counter. */\n actions?: ReactNode;\n}\n\n/** The draft filter being built in the editor. */\ninterface Draft {\n field: string;\n operator: Filter[\"operator\"];\n value: string;\n to: string;\n}\n\n/** A fresh draft for a field. */\nfunction draftFor(field: FilterField): Draft {\n return { field: field.name, operator: defaultOperator(field), value: \"\", to: \"\" };\n}\n\n/** Turn a draft into a filter. */\nfunction toFilter(draft: Draft): Filter {\n if (isValueless(draft.operator)) return { field: draft.field, operator: draft.operator };\n if (draft.operator === \"between\") {\n return { field: draft.field, operator: draft.operator, value: [draft.value, draft.to] };\n }\n if (isMulti(draft.operator)) {\n return {\n field: draft.field,\n operator: draft.operator,\n value: draft.value\n .split(\",\")\n .map((part) => part.trim())\n .filter(Boolean),\n };\n }\n return { field: draft.field, operator: draft.operator, value: draft.value };\n}\n\n/**\n * A row of applied filters, and a small editor to add another.\n *\n * Filters are combined with **AND**, flat — no nested `(a OR b) AND c` groups. That\n * is the shape of nearly every admin list (\"status is paid, created after March,\n * text contains nota\"), and it is what serializes into a URL somebody can send to a\n * colleague. Nested groups need a tree UI with per-node operators and a different\n * serialization; a component trying to be both is clumsy at the common case.\n *\n * Controlled, like the rest of the SDK: it holds the half-typed draft in the editor\n * and nothing else. The applied set is the app's, which is what lets it come from —\n * and go back into — the URL.\n *\n * @example\n * const [filters, setFilters] = useState<Filter[]>(() =>\n * filtersFromSearchParams(new URLSearchParams(location.search), FIELDS),\n * );\n *\n * <FilterBar fields={FIELDS} value={filters} onChange={setFilters} />\n */\nexport function FilterBar({\n fields,\n value,\n onChange,\n locale = \"pt-BR\",\n actions,\n className,\n ...rest\n}: FilterBarProps) {\n const strings = filterStrings(locale);\n const baseId = useId();\n const [draft, setDraft] = useState<Draft | null>(null);\n\n const field = draft ? fields.find((candidate) => candidate.name === draft.field) : undefined;\n const pending = draft ? toFilter(draft) : null;\n const ready = pending !== null && isComplete(pending);\n\n const open = (): void => {\n if (fields.length > 0) setDraft(draftFor(fields[0]));\n };\n\n const apply = (): void => {\n if (!pending || !ready) return;\n onChange([...value, pending]);\n setDraft(null);\n };\n\n const removeAt = (index: number): void => {\n onChange(value.filter((_, position) => position !== index));\n };\n\n return (\n <div className={cn(styles.bar, className)} {...rest}>\n <ul className={styles.chips}>\n {value.length === 0 && !draft && <li className={styles.empty}>{strings.empty}</li>}\n {value.map((filter, index) => {\n const description = describeFilter(filter, fields, locale);\n return (\n <li\n key={`${filter.field}-${filter.operator}-${index}`}\n className={styles.chip}\n >\n <span>{description}</span>\n <button\n type=\"button\"\n className={styles.remove}\n aria-label={strings.remove(description)}\n onClick={() => removeAt(index)}\n >\n ×\n </button>\n </li>\n );\n })}\n </ul>\n\n {draft && field ? (\n <div className={styles.editor} role=\"group\" aria-label={strings.add}>\n <label className={styles.control}>\n <VisuallyHidden>{strings.field}</VisuallyHidden>\n <select\n className={styles.select}\n value={draft.field}\n onChange={(event) => {\n const next = fields.find(\n (candidate) => candidate.name === event.target.value,\n );\n if (next) setDraft(draftFor(next));\n }}\n >\n {fields.map((candidate) => (\n <option key={candidate.name} value={candidate.name}>\n {candidate.label}\n </option>\n ))}\n </select>\n </label>\n\n <label className={styles.control}>\n <VisuallyHidden>{strings.operator}</VisuallyHidden>\n <select\n className={styles.select}\n value={draft.operator}\n onChange={(event) =>\n setDraft({\n ...draft,\n operator: event.target.value as Filter[\"operator\"],\n value: \"\",\n to: \"\",\n })\n }\n >\n {operatorsFor(field).map((operator) => (\n <option key={operator} value={operator}>\n {operatorLabel(operator, locale)}\n </option>\n ))}\n </select>\n </label>\n\n {!isValueless(draft.operator) && (\n <ValueInput\n id={`${baseId}-value`}\n field={field}\n draft={draft}\n strings={strings}\n onChange={setDraft}\n />\n )}\n\n <button\n type=\"button\"\n className={styles.primary}\n disabled={!ready}\n onClick={apply}\n >\n {strings.apply}\n </button>\n <button type=\"button\" className={styles.ghost} onClick={() => setDraft(null)}>\n {strings.cancel}\n </button>\n </div>\n ) : (\n <div className={styles.actions}>\n <button\n type=\"button\"\n className={styles.secondary}\n disabled={fields.length === 0}\n onClick={open}\n >\n + {strings.add}\n </button>\n {value.length > 0 && (\n <button type=\"button\" className={styles.ghost} onClick={() => onChange([])}>\n {strings.clear}\n </button>\n )}\n {actions}\n </div>\n )}\n\n <VisuallyHidden aria-live=\"polite\" role=\"status\">\n {strings.active(value.length)}\n </VisuallyHidden>\n </div>\n );\n}\n\n/**\n * The value input for the draft.\n *\n * The input type follows the **field**, not the operator: a date field gets a date\n * picker even under `between`, where two of them are needed. Typing a date into a\n * text box is the fastest way to produce a filter the backend cannot parse.\n */\nfunction ValueInput({\n id,\n field,\n draft,\n strings,\n onChange,\n}: {\n id: string;\n field: FilterField;\n draft: Draft;\n strings: ReturnType<typeof filterStrings>;\n onChange: (draft: Draft) => void;\n}) {\n const type = field.type === \"number\" ? \"number\" : field.type === \"date\" ? \"date\" : \"text\";\n\n if (field.type === \"select\" && !isMulti(draft.operator)) {\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <select\n id={id}\n className={styles.select}\n value={draft.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n >\n <option value=\"\">—</option>\n {field.options?.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n );\n }\n\n if (field.type === \"boolean\") {\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <select\n id={id}\n className={styles.select}\n value={draft.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n >\n <option value=\"\">—</option>\n <option value=\"true\">{strings.yes}</option>\n <option value=\"false\">{strings.no}</option>\n </select>\n </label>\n );\n }\n\n if (draft.operator === \"between\") {\n return (\n <>\n <label className={styles.control}>\n <VisuallyHidden>{strings.from}</VisuallyHidden>\n <input\n id={id}\n className={styles.input}\n type={type}\n value={draft.value}\n placeholder={strings.from}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n />\n </label>\n <label className={styles.control}>\n <VisuallyHidden>{strings.to}</VisuallyHidden>\n <input\n className={styles.input}\n type={type}\n value={draft.to}\n placeholder={strings.to}\n onChange={(event) => onChange({ ...draft, to: event.target.value })}\n />\n </label>\n </>\n );\n }\n\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <input\n id={id}\n className={styles.input}\n type={type}\n value={draft.value}\n placeholder={field.placeholder ?? strings.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n />\n </label>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"FilterBar.cjs","names":[],"sources":["../../../src/components/FilterBar/FilterBar.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a filter bar renders a different\n * editor per filter kind — select, multi-select, range, date range, text — and the\n * body is that dispatch plus the chip summary, both reading the same value map.\n */\nimport { useId, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n defaultOperator,\n describeFilter,\n filterStrings,\n isComplete,\n isMulti,\n isValueless,\n operatorLabel,\n operatorsFor,\n type Filter,\n type FilterField,\n} from \"./filter-model\";\nimport styles from \"./FilterBar.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\";\n\nexport interface FilterBarProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** Fields the user may filter by. */\n fields: readonly FilterField[];\n /** Applied filters. Controlled. */\n value: readonly Filter[];\n /** Next filter set. Combined with AND. */\n onChange: (filters: Filter[]) => void;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the bar — a \"save this view\" button, a counter. */\n actions?: ReactNode;\n}\n\n/** The draft filter being built in the editor. */\ninterface Draft {\n field: string;\n operator: Filter[\"operator\"];\n value: string;\n to: string;\n}\n\n/** A fresh draft for a field. */\nfunction draftFor(field: FilterField): Draft {\n return { field: field.name, operator: defaultOperator(field), value: \"\", to: \"\" };\n}\n\n/** Turn a draft into a filter. */\nfunction toFilter(draft: Draft): Filter {\n if (isValueless(draft.operator)) return { field: draft.field, operator: draft.operator };\n if (draft.operator === \"between\") {\n return { field: draft.field, operator: draft.operator, value: [draft.value, draft.to] };\n }\n if (isMulti(draft.operator)) {\n return {\n field: draft.field,\n operator: draft.operator,\n value: draft.value\n .split(\",\")\n .map((part) => part.trim())\n .filter(Boolean),\n };\n }\n return { field: draft.field, operator: draft.operator, value: draft.value };\n}\n\n/**\n * A row of applied filters, and a small editor to add another.\n *\n * Filters are combined with **AND**, flat — no nested `(a OR b) AND c` groups. That\n * is the shape of nearly every admin list (\"status is paid, created after March,\n * text contains nota\"), and it is what serializes into a URL somebody can send to a\n * colleague. Nested groups need a tree UI with per-node operators and a different\n * serialization; a component trying to be both is clumsy at the common case.\n *\n * Controlled, like the rest of the SDK: it holds the half-typed draft in the editor\n * and nothing else. The applied set is the app's, which is what lets it come from —\n * and go back into — the URL.\n *\n * @example\n * const [filters, setFilters] = useState<Filter[]>(() =>\n * filtersFromSearchParams(new URLSearchParams(location.search), FIELDS),\n * );\n *\n * <FilterBar fields={FIELDS} value={filters} onChange={setFilters} />\n */\nexport function FilterBar({\n fields,\n value,\n onChange,\n locale = \"pt-BR\",\n actions,\n className,\n ...rest\n}: FilterBarProps) {\n const strings = filterStrings(locale);\n const baseId = useId();\n const [draft, setDraft] = useState<Draft | null>(null);\n\n const field = draft ? fields.find((candidate) => candidate.name === draft.field) : undefined;\n const pending = draft ? toFilter(draft) : null;\n const ready = pending !== null && isComplete(pending);\n\n const open = (): void => {\n if (fields.length > 0) setDraft(draftFor(fields[0]));\n };\n\n const apply = (): void => {\n if (!pending || !ready) return;\n onChange([...value, pending]);\n setDraft(null);\n };\n\n const removeAt = (index: number): void => {\n onChange(value.filter((_, position) => position !== index));\n };\n\n return (\n <div className={cn(styles.bar, className)} {...rest}>\n <ul className={styles.chips}>\n {value.length === 0 && !draft && <li className={styles.empty}>{strings.empty}</li>}\n {value.map((filter, index) => {\n const description = describeFilter(filter, fields, locale);\n return (\n <li\n key={`${filter.field}-${filter.operator}-${index}`}\n className={styles.chip}\n >\n <span>{description}</span>\n <button\n type=\"button\"\n className={styles.remove}\n aria-label={strings.remove(description)}\n onClick={() => removeAt(index)}\n >\n ×\n </button>\n </li>\n );\n })}\n </ul>\n\n {draft && field ? (\n <div className={styles.editor} role=\"group\" aria-label={strings.add}>\n <label className={styles.control}>\n <VisuallyHidden>{strings.field}</VisuallyHidden>\n <select\n className={styles.select}\n value={draft.field}\n onChange={(event) => {\n const next = fields.find(\n (candidate) => candidate.name === event.target.value,\n );\n if (next) setDraft(draftFor(next));\n }}\n >\n {fields.map((candidate) => (\n <option key={candidate.name} value={candidate.name}>\n {candidate.label}\n </option>\n ))}\n </select>\n </label>\n\n <label className={styles.control}>\n <VisuallyHidden>{strings.operator}</VisuallyHidden>\n <select\n className={styles.select}\n value={draft.operator}\n onChange={(event) =>\n setDraft({\n ...draft,\n operator: event.target.value as Filter[\"operator\"],\n value: \"\",\n to: \"\",\n })\n }\n >\n {operatorsFor(field).map((operator) => (\n <option key={operator} value={operator}>\n {operatorLabel(operator, locale)}\n </option>\n ))}\n </select>\n </label>\n\n {!isValueless(draft.operator) && (\n <ValueInput\n id={`${baseId}-value`}\n field={field}\n draft={draft}\n strings={strings}\n onChange={setDraft}\n />\n )}\n\n <button\n type=\"button\"\n className={styles.primary}\n disabled={!ready}\n onClick={apply}\n >\n {strings.apply}\n </button>\n <button type=\"button\" className={styles.ghost} onClick={() => setDraft(null)}>\n {strings.cancel}\n </button>\n </div>\n ) : (\n <div className={styles.actions}>\n <button\n type=\"button\"\n className={styles.secondary}\n disabled={fields.length === 0}\n onClick={open}\n >\n + {strings.add}\n </button>\n {value.length > 0 && (\n <button type=\"button\" className={styles.ghost} onClick={() => onChange([])}>\n {strings.clear}\n </button>\n )}\n {actions}\n </div>\n )}\n\n <VisuallyHidden aria-live=\"polite\" role=\"status\">\n {strings.active(value.length)}\n </VisuallyHidden>\n </div>\n );\n}\n\n/**\n * The value input for the draft.\n *\n * The input type follows the **field**, not the operator: a date field gets a date\n * picker even under `between`, where two of them are needed. Typing a date into a\n * text box is the fastest way to produce a filter the backend cannot parse.\n */\nfunction ValueInput({\n id,\n field,\n draft,\n strings,\n onChange,\n}: {\n id: string;\n field: FilterField;\n draft: Draft;\n strings: ReturnType<typeof filterStrings>;\n onChange: (draft: Draft) => void;\n}) {\n const type = field.type === \"number\" ? \"number\" : field.type === \"date\" ? \"date\" : \"text\";\n\n if (field.type === \"select\" && !isMulti(draft.operator)) {\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <select\n id={id}\n className={styles.select}\n value={draft.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n >\n <option value=\"\">—</option>\n {field.options?.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n );\n }\n\n if (field.type === \"boolean\") {\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <select\n id={id}\n className={styles.select}\n value={draft.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n >\n <option value=\"\">—</option>\n <option value=\"true\">{strings.yes}</option>\n <option value=\"false\">{strings.no}</option>\n </select>\n </label>\n );\n }\n\n if (draft.operator === \"between\") {\n return (\n <>\n <label className={styles.control}>\n <VisuallyHidden>{strings.from}</VisuallyHidden>\n <input\n id={id}\n className={styles.input}\n type={type}\n value={draft.value}\n placeholder={strings.from}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n />\n </label>\n <label className={styles.control}>\n <VisuallyHidden>{strings.to}</VisuallyHidden>\n <input\n className={styles.input}\n type={type}\n value={draft.to}\n placeholder={strings.to}\n onChange={(event) => onChange({ ...draft, to: event.target.value })}\n />\n </label>\n </>\n );\n }\n\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <input\n id={id}\n className={styles.input}\n type={type}\n value={draft.value}\n placeholder={field.placeholder ?? strings.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n />\n </label>\n );\n}\n"],"mappings":"kNAiDA,SAAS,EAAS,EAA2B,CACzC,MAAO,CAAE,MAAO,EAAM,KAAM,SAAU,EAAA,gBAAgB,CAAK,EAAG,MAAO,GAAI,GAAI,EAAG,CACpF,CAGA,SAAS,EAAS,EAAsB,CAepC,OAdI,EAAA,YAAY,EAAM,QAAQ,EAAU,CAAE,MAAO,EAAM,MAAO,SAAU,EAAM,QAAS,EACnF,EAAM,WAAa,UACZ,CAAE,MAAO,EAAM,MAAO,SAAU,EAAM,SAAU,MAAO,CAAC,EAAM,MAAO,EAAM,EAAE,CAAE,EAEtF,EAAA,QAAQ,EAAM,QAAQ,EACf,CACH,MAAO,EAAM,MACb,SAAU,EAAM,SAChB,MAAO,EAAM,MACR,MAAM,GAAG,CAAC,CACV,IAAK,GAAS,EAAK,KAAK,CAAC,CAAC,CAC1B,OAAO,OAAO,CACvB,EAEG,CAAE,MAAO,EAAM,MAAO,SAAU,EAAM,SAAU,MAAO,EAAM,KAAM,CAC9E,CAsBA,SAAgB,EAAU,CACtB,SACA,QACA,WACA,SAAS,QACT,UACA,YACA,GAAG,GACY,CACf,IAAM,EAAU,EAAA,cAAc,CAAM,EAC9B,GAAA,EAAS,EAAA,MAAA,CAAM,EACf,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAuB,IAAI,EAE/C,EAAQ,EAAQ,EAAO,KAAM,GAAc,EAAU,OAAS,EAAM,KAAK,EAAI,IAAA,GAC7E,EAAU,EAAQ,EAAS,CAAK,EAAI,KACpC,EAAQ,IAAY,MAAQ,EAAA,WAAW,CAAO,EAE9C,MAAmB,CACjB,EAAO,OAAS,GAAG,EAAS,EAAS,EAAO,EAAE,CAAC,CACvD,EAEM,MAAoB,CACjB,GAAY,IACjB,EAAS,CAAC,GAAG,EAAO,CAAO,CAAC,EAC5B,EAAS,IAAI,EACjB,EAEM,EAAY,GAAwB,CACtC,EAAS,EAAM,QAAQ,EAAG,IAAa,IAAa,CAAK,CAAC,CAC9D,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,IAAK,CAAS,EAAG,GAAI,EAA/C,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MAAtB,SAAA,CACK,EAAM,SAAW,GAAK,CAAC,IAAS,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MAAQ,SAAA,EAAQ,KAAU,CAAA,EAChF,EAAM,KAAK,EAAQ,IAAU,CAC1B,IAAM,EAAc,EAAA,eAAe,EAAQ,EAAQ,CAAM,EACzD,OACI,EAAA,EAAA,KAAA,CAAC,KAAD,CAEI,UAAW,EAAA,QAAO,KAFtB,SAAA,EAII,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,CAAkB,CAAA,GACzB,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,OAClB,aAAY,EAAQ,OAAO,CAAW,EACtC,YAAe,EAAS,CAAK,EAChC,SAAA,GAEO,CAAA,CACR,CAZK,EAAA,GAAG,EAAO,MAAM,GAAG,EAAO,SAAS,GAAG,GAY3C,CAEZ,CAAC,CACD,IAEH,GAAS,GACN,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAQ,KAAK,QAAQ,aAAY,EAAQ,IAAhE,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,UAAW,EAAA,QAAO,QAAzB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAA,SAAiB,EAAQ,KAAsB,CAAA,GAC/C,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,UAAW,EAAA,QAAO,OAClB,MAAO,EAAM,MACb,SAAW,GAAU,CACjB,IAAM,EAAO,EAAO,KACf,GAAc,EAAU,OAAS,EAAM,OAAO,KACnD,EACI,GAAM,EAAS,EAAS,CAAI,CAAC,CACrC,EAEC,SAAA,EAAO,IAAK,IACT,EAAA,EAAA,IAAA,CAAC,SAAD,CAA6B,MAAO,EAAU,KACzC,SAAA,EAAU,KACP,EAFK,EAAU,IAEf,CACX,CACG,CAAA,CACL,KAEP,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,UAAW,EAAA,QAAO,QAAzB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAA,SAAiB,EAAQ,QAAyB,CAAA,GAClD,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,UAAW,EAAA,QAAO,OAClB,MAAO,EAAM,SACb,SAAW,GACP,EAAS,CACL,GAAG,EACH,SAAU,EAAM,OAAO,MACvB,MAAO,GACP,GAAI,EACR,CAAC,EAGJ,SAAA,EAAA,aAAa,CAAK,CAAC,CAAC,IAAK,IACtB,EAAA,EAAA,IAAA,CAAC,SAAD,CAAuB,MAAO,EACzB,SAAA,EAAA,cAAc,EAAU,CAAM,CAC3B,EAFK,CAEL,CACX,CACG,CAAA,CACL,IAEN,CAAC,EAAA,YAAY,EAAM,QAAQ,IACxB,EAAA,EAAA,IAAA,CAAC,EAAD,CACI,GAAI,GAAG,EAAO,QACP,QACA,QACE,UACT,SAAU,CACb,CAAA,GAGL,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,CAAC,EACX,QAAS,EAER,SAAA,EAAQ,KACL,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAW,EAAA,QAAO,MAAO,YAAe,EAAS,IAAI,EACtE,SAAA,EAAQ,MACL,CAAA,CACP,CAEL,CAAA,GAAA,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAvB,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,UAClB,SAAU,EAAO,SAAW,EAC5B,QAAS,EAJb,SAAA,CAKC,KACM,EAAQ,GACP,IACP,EAAM,OAAS,IACZ,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAW,EAAA,QAAO,MAAO,YAAe,EAAS,CAAC,CAAC,EACpE,SAAA,EAAQ,KACL,CAAA,EAEX,CACA,KAGT,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAgB,YAAU,SAAS,KAAK,SACnC,SAAA,EAAQ,OAAO,EAAM,MAAM,CAChB,CAAA,CACf,GAEb,CASA,SAAS,EAAW,CAChB,KACA,QACA,QACA,UACA,YAOD,CACC,IAAM,EAAO,EAAM,OAAS,SAAW,SAAW,EAAM,OAAS,OAAS,OAAS,OAqEnF,OAnEI,EAAM,OAAS,UAAY,CAAC,EAAA,QAAQ,EAAM,QAAQ,GAE9C,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,UAAW,EAAA,QAAO,QAAzB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAA,SAAiB,EAAQ,KAAsB,CAAA,GAC/C,EAAA,EAAA,KAAA,CAAC,SAAD,CACQ,KACJ,UAAW,EAAA,QAAO,OAClB,MAAO,EAAM,MACb,SAAW,GAAU,EAAS,CAAE,GAAG,EAAO,MAAO,EAAM,OAAO,KAAM,CAAC,EAJzE,SAAA,EAMI,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,GAAG,SAAA,GAAS,CAAA,EACzB,EAAM,SAAS,IAAK,IACjB,EAAA,EAAA,IAAA,CAAC,SAAD,CAA2B,MAAO,EAAO,MACpC,SAAA,EAAO,KACJ,EAFK,EAAO,KAEZ,CACX,CACG,CACL,CAAA,CAAA,IAIX,EAAM,OAAS,WAEX,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,UAAW,EAAA,QAAO,QAAzB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAA,SAAiB,EAAQ,KAAsB,CAAA,GAC/C,EAAA,EAAA,KAAA,CAAC,SAAD,CACQ,KACJ,UAAW,EAAA,QAAO,OAClB,MAAO,EAAM,MACb,SAAW,GAAU,EAAS,CAAE,GAAG,EAAO,MAAO,EAAM,OAAO,KAAM,CAAC,EAJzE,SAAA,EAMI,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,GAAG,SAAA,GAAS,CAAA,GAC1B,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,OAAQ,SAAA,EAAQ,GAAY,CAAA,GAC1C,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,QAAS,SAAA,EAAQ,EAAW,CAAA,CACtC,CACL,CAAA,CAAA,IAIX,EAAM,WAAa,WAEf,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,UAAW,EAAA,QAAO,QAAzB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAA,SAAiB,EAAQ,IAAqB,CAAA,GAC9C,EAAA,EAAA,IAAA,CAAC,QAAD,CACQ,KACJ,UAAW,EAAA,QAAO,MACZ,OACN,MAAO,EAAM,MACb,YAAa,EAAQ,KACrB,SAAW,GAAU,EAAS,CAAE,GAAG,EAAO,MAAO,EAAM,OAAO,KAAM,CAAC,CACxE,CAAA,CACE,CACP,CAAA,GAAA,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,UAAW,EAAA,QAAO,QAAzB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAA,SAAiB,EAAQ,EAAmB,CAAA,GAC5C,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,UAAW,EAAA,QAAO,MACZ,OACN,MAAO,EAAM,GACb,YAAa,EAAQ,GACrB,SAAW,GAAU,EAAS,CAAE,GAAG,EAAO,GAAI,EAAM,OAAO,KAAM,CAAC,CACrE,CAAA,CACE,CACT,CAAA,CAAA,CAAA,CAAA,GAKN,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,UAAW,EAAA,QAAO,QAAzB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAA,SAAiB,EAAQ,KAAsB,CAAA,GAC/C,EAAA,EAAA,IAAA,CAAC,QAAD,CACQ,KACJ,UAAW,EAAA,QAAO,MACZ,OACN,MAAO,EAAM,MACb,YAAa,EAAM,aAAe,EAAQ,MAC1C,SAAW,GAAU,EAAS,CAAE,GAAG,EAAO,MAAO,EAAM,OAAO,KAAM,CAAC,CACxE,CAAA,CACE,GAEf"}
@@ -35,7 +35,7 @@ function v({ fields: n, value: o, onChange: p, locale: v = "pt-BR", actions: b,
35
35
  let C = i(v), w = d(), [T, E] = f(null), D = T ? n.find((e) => e.name === T.field) : void 0, O = T ? _(T) : null, k = O !== null && a(O), A = () => {
36
36
  n.length > 0 && E(g(n[0]));
37
37
  }, j = () => {
38
- !O || !k || (p([...o, O]), E(null));
38
+ O && k && (p([...o, O]), E(null));
39
39
  }, M = (e) => {
40
40
  p(o.filter((t, n) => n !== e));
41
41
  };
@@ -1 +1 @@
1
- {"version":3,"file":"FilterBar.js","names":[],"sources":["../../../src/components/FilterBar/FilterBar.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a filter bar renders a different\n * editor per filter kind — select, multi-select, range, date range, text — and the\n * body is that dispatch plus the chip summary, both reading the same value map.\n */\nimport { useId, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n defaultOperator,\n describeFilter,\n filterStrings,\n isComplete,\n isMulti,\n isValueless,\n operatorLabel,\n operatorsFor,\n type Filter,\n type FilterField,\n} from \"./filter-model\";\nimport styles from \"./FilterBar.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\";\n\nexport interface FilterBarProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** Fields the user may filter by. */\n fields: readonly FilterField[];\n /** Applied filters. Controlled. */\n value: readonly Filter[];\n /** Next filter set. Combined with AND. */\n onChange: (filters: Filter[]) => void;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the bar — a \"save this view\" button, a counter. */\n actions?: ReactNode;\n}\n\n/** The draft filter being built in the editor. */\ninterface Draft {\n field: string;\n operator: Filter[\"operator\"];\n value: string;\n to: string;\n}\n\n/** A fresh draft for a field. */\nfunction draftFor(field: FilterField): Draft {\n return { field: field.name, operator: defaultOperator(field), value: \"\", to: \"\" };\n}\n\n/** Turn a draft into a filter. */\nfunction toFilter(draft: Draft): Filter {\n if (isValueless(draft.operator)) return { field: draft.field, operator: draft.operator };\n if (draft.operator === \"between\") {\n return { field: draft.field, operator: draft.operator, value: [draft.value, draft.to] };\n }\n if (isMulti(draft.operator)) {\n return {\n field: draft.field,\n operator: draft.operator,\n value: draft.value\n .split(\",\")\n .map((part) => part.trim())\n .filter(Boolean),\n };\n }\n return { field: draft.field, operator: draft.operator, value: draft.value };\n}\n\n/**\n * A row of applied filters, and a small editor to add another.\n *\n * Filters are combined with **AND**, flat — no nested `(a OR b) AND c` groups. That\n * is the shape of nearly every admin list (\"status is paid, created after March,\n * text contains nota\"), and it is what serializes into a URL somebody can send to a\n * colleague. Nested groups need a tree UI with per-node operators and a different\n * serialization; a component trying to be both is clumsy at the common case.\n *\n * Controlled, like the rest of the SDK: it holds the half-typed draft in the editor\n * and nothing else. The applied set is the app's, which is what lets it come from —\n * and go back into — the URL.\n *\n * @example\n * const [filters, setFilters] = useState<Filter[]>(() =>\n * filtersFromSearchParams(new URLSearchParams(location.search), FIELDS),\n * );\n *\n * <FilterBar fields={FIELDS} value={filters} onChange={setFilters} />\n */\nexport function FilterBar({\n fields,\n value,\n onChange,\n locale = \"pt-BR\",\n actions,\n className,\n ...rest\n}: FilterBarProps) {\n const strings = filterStrings(locale);\n const baseId = useId();\n const [draft, setDraft] = useState<Draft | null>(null);\n\n const field = draft ? fields.find((candidate) => candidate.name === draft.field) : undefined;\n const pending = draft ? toFilter(draft) : null;\n const ready = pending !== null && isComplete(pending);\n\n const open = (): void => {\n if (fields.length > 0) setDraft(draftFor(fields[0]));\n };\n\n const apply = (): void => {\n if (!pending || !ready) return;\n onChange([...value, pending]);\n setDraft(null);\n };\n\n const removeAt = (index: number): void => {\n onChange(value.filter((_, position) => position !== index));\n };\n\n return (\n <div className={cn(styles.bar, className)} {...rest}>\n <ul className={styles.chips}>\n {value.length === 0 && !draft && <li className={styles.empty}>{strings.empty}</li>}\n {value.map((filter, index) => {\n const description = describeFilter(filter, fields, locale);\n return (\n <li\n key={`${filter.field}-${filter.operator}-${index}`}\n className={styles.chip}\n >\n <span>{description}</span>\n <button\n type=\"button\"\n className={styles.remove}\n aria-label={strings.remove(description)}\n onClick={() => removeAt(index)}\n >\n ×\n </button>\n </li>\n );\n })}\n </ul>\n\n {draft && field ? (\n <div className={styles.editor} role=\"group\" aria-label={strings.add}>\n <label className={styles.control}>\n <VisuallyHidden>{strings.field}</VisuallyHidden>\n <select\n className={styles.select}\n value={draft.field}\n onChange={(event) => {\n const next = fields.find(\n (candidate) => candidate.name === event.target.value,\n );\n if (next) setDraft(draftFor(next));\n }}\n >\n {fields.map((candidate) => (\n <option key={candidate.name} value={candidate.name}>\n {candidate.label}\n </option>\n ))}\n </select>\n </label>\n\n <label className={styles.control}>\n <VisuallyHidden>{strings.operator}</VisuallyHidden>\n <select\n className={styles.select}\n value={draft.operator}\n onChange={(event) =>\n setDraft({\n ...draft,\n operator: event.target.value as Filter[\"operator\"],\n value: \"\",\n to: \"\",\n })\n }\n >\n {operatorsFor(field).map((operator) => (\n <option key={operator} value={operator}>\n {operatorLabel(operator, locale)}\n </option>\n ))}\n </select>\n </label>\n\n {!isValueless(draft.operator) && (\n <ValueInput\n id={`${baseId}-value`}\n field={field}\n draft={draft}\n strings={strings}\n onChange={setDraft}\n />\n )}\n\n <button\n type=\"button\"\n className={styles.primary}\n disabled={!ready}\n onClick={apply}\n >\n {strings.apply}\n </button>\n <button type=\"button\" className={styles.ghost} onClick={() => setDraft(null)}>\n {strings.cancel}\n </button>\n </div>\n ) : (\n <div className={styles.actions}>\n <button\n type=\"button\"\n className={styles.secondary}\n disabled={fields.length === 0}\n onClick={open}\n >\n + {strings.add}\n </button>\n {value.length > 0 && (\n <button type=\"button\" className={styles.ghost} onClick={() => onChange([])}>\n {strings.clear}\n </button>\n )}\n {actions}\n </div>\n )}\n\n <VisuallyHidden aria-live=\"polite\" role=\"status\">\n {strings.active(value.length)}\n </VisuallyHidden>\n </div>\n );\n}\n\n/**\n * The value input for the draft.\n *\n * The input type follows the **field**, not the operator: a date field gets a date\n * picker even under `between`, where two of them are needed. Typing a date into a\n * text box is the fastest way to produce a filter the backend cannot parse.\n */\nfunction ValueInput({\n id,\n field,\n draft,\n strings,\n onChange,\n}: {\n id: string;\n field: FilterField;\n draft: Draft;\n strings: ReturnType<typeof filterStrings>;\n onChange: (draft: Draft) => void;\n}) {\n const type = field.type === \"number\" ? \"number\" : field.type === \"date\" ? \"date\" : \"text\";\n\n if (field.type === \"select\" && !isMulti(draft.operator)) {\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <select\n id={id}\n className={styles.select}\n value={draft.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n >\n <option value=\"\">—</option>\n {field.options?.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n );\n }\n\n if (field.type === \"boolean\") {\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <select\n id={id}\n className={styles.select}\n value={draft.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n >\n <option value=\"\">—</option>\n <option value=\"true\">{strings.yes}</option>\n <option value=\"false\">{strings.no}</option>\n </select>\n </label>\n );\n }\n\n if (draft.operator === \"between\") {\n return (\n <>\n <label className={styles.control}>\n <VisuallyHidden>{strings.from}</VisuallyHidden>\n <input\n id={id}\n className={styles.input}\n type={type}\n value={draft.value}\n placeholder={strings.from}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n />\n </label>\n <label className={styles.control}>\n <VisuallyHidden>{strings.to}</VisuallyHidden>\n <input\n className={styles.input}\n type={type}\n value={draft.to}\n placeholder={strings.to}\n onChange={(event) => onChange({ ...draft, to: event.target.value })}\n />\n </label>\n </>\n );\n }\n\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <input\n id={id}\n className={styles.input}\n type={type}\n value={draft.value}\n placeholder={field.placeholder ?? strings.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n />\n </label>\n );\n}\n"],"mappings":";;;;;;;AAiDA,SAAS,EAAS,GAA2B;CACzC,OAAO;EAAE,OAAO,EAAM;EAAM,UAAU,EAAgB,CAAK;EAAG,OAAO;EAAI,IAAI;CAAG;AACpF;AAGA,SAAS,EAAS,GAAsB;CAepC,OAdI,EAAY,EAAM,QAAQ,IAAU;EAAE,OAAO,EAAM;EAAO,UAAU,EAAM;CAAS,IACnF,EAAM,aAAa,YACZ;EAAE,OAAO,EAAM;EAAO,UAAU,EAAM;EAAU,OAAO,CAAC,EAAM,OAAO,EAAM,EAAE;CAAE,IAEtF,EAAQ,EAAM,QAAQ,IACf;EACH,OAAO,EAAM;EACb,UAAU,EAAM;EAChB,OAAO,EAAM,MACR,MAAM,GAAG,CAAC,CACV,KAAK,MAAS,EAAK,KAAK,CAAC,CAAC,CAC1B,OAAO,OAAO;CACvB,IAEG;EAAE,OAAO,EAAM;EAAO,UAAU,EAAM;EAAU,OAAO,EAAM;CAAM;AAC9E;AAsBA,SAAgB,EAAU,EACtB,WACA,UACA,aACA,YAAS,SACT,YACA,cACA,GAAG,KACY;CACf,IAAM,IAAU,EAAc,CAAM,GAC9B,IAAS,EAAM,GACf,CAAC,GAAO,KAAY,EAAuB,IAAI,GAE/C,IAAQ,IAAQ,EAAO,MAAM,MAAc,EAAU,SAAS,EAAM,KAAK,IAAI,KAAA,GAC7E,IAAU,IAAQ,EAAS,CAAK,IAAI,MACpC,IAAQ,MAAY,QAAQ,EAAW,CAAO,GAE9C,UAAmB;EACrB,AAAI,EAAO,SAAS,KAAG,EAAS,EAAS,EAAO,EAAE,CAAC;CACvD,GAEM,UAAoB;EAClB,CAAC,KAAW,CAAC,MACjB,EAAS,CAAC,GAAG,GAAO,CAAO,CAAC,GAC5B,EAAS,IAAI;CACjB,GAEM,KAAY,MAAwB;EACtC,EAAS,EAAM,QAAQ,GAAG,MAAa,MAAa,CAAK,CAAC;CAC9D;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,KAAK,CAAS;EAAG,GAAI;EAA/C,UAAA;GACI,kBAAC,MAAD;IAAI,WAAW,EAAO;IAAtB,UAAA,CACK,EAAM,WAAW,KAAK,CAAC,KAAS,kBAAC,MAAD;KAAI,WAAW,EAAO;KAAQ,UAAA,EAAQ;IAAU,CAAA,GAChF,EAAM,KAAK,GAAQ,MAAU;KAC1B,IAAM,IAAc,EAAe,GAAQ,GAAQ,CAAM;KACzD,OACI,kBAAC,MAAD;MAEI,WAAW,EAAO;MAFtB,UAAA,CAII,kBAAC,QAAD,EAAA,UAAO,EAAkB,CAAA,GACzB,kBAAC,UAAD;OACI,MAAK;OACL,WAAW,EAAO;OAClB,cAAY,EAAQ,OAAO,CAAW;OACtC,eAAe,EAAS,CAAK;OAChC,UAAA;MAEO,CAAA,CACR;KAZK,GAAA,GAAG,EAAO,MAAM,GAAG,EAAO,SAAS,GAAG,GAY3C;IAEZ,CAAC,CACD;;GAEH,KAAS,IACN,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAQ,MAAK;IAAQ,cAAY,EAAQ;IAAhE,UAAA;KACI,kBAAC,SAAD;MAAO,WAAW,EAAO;MAAzB,UAAA,CACI,kBAAC,GAAD,EAAA,UAAiB,EAAQ,MAAsB,CAAA,GAC/C,kBAAC,UAAD;OACI,WAAW,EAAO;OAClB,OAAO,EAAM;OACb,WAAW,MAAU;QACjB,IAAM,IAAO,EAAO,MACf,MAAc,EAAU,SAAS,EAAM,OAAO,KACnD;QACA,AAAI,KAAM,EAAS,EAAS,CAAI,CAAC;OACrC;OAEC,UAAA,EAAO,KAAK,MACT,kBAAC,UAAD;QAA6B,OAAO,EAAU;QACzC,UAAA,EAAU;OACP,GAFK,EAAU,IAEf,CACX;MACG,CAAA,CACL;;KAEP,kBAAC,SAAD;MAAO,WAAW,EAAO;MAAzB,UAAA,CACI,kBAAC,GAAD,EAAA,UAAiB,EAAQ,SAAyB,CAAA,GAClD,kBAAC,UAAD;OACI,WAAW,EAAO;OAClB,OAAO,EAAM;OACb,WAAW,MACP,EAAS;QACL,GAAG;QACH,UAAU,EAAM,OAAO;QACvB,OAAO;QACP,IAAI;OACR,CAAC;OAGJ,UAAA,EAAa,CAAK,CAAC,CAAC,KAAK,MACtB,kBAAC,UAAD;QAAuB,OAAO;QACzB,UAAA,EAAc,GAAU,CAAM;OAC3B,GAFK,CAEL,CACX;MACG,CAAA,CACL;;KAEN,CAAC,EAAY,EAAM,QAAQ,KACxB,kBAAC,GAAD;MACI,IAAI,GAAG,EAAO;MACP;MACA;MACE;MACT,UAAU;KACb,CAAA;KAGL,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,UAAU,CAAC;MACX,SAAS;MAER,UAAA,EAAQ;KACL,CAAA;KACR,kBAAC,UAAD;MAAQ,MAAK;MAAS,WAAW,EAAO;MAAO,eAAe,EAAS,IAAI;MACtE,UAAA,EAAQ;KACL,CAAA;IACP;GAEL,CAAA,IAAA,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA;KACI,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,UAAU,EAAO,WAAW;MAC5B,SAAS;MAJb,UAAA,CAKC,MACM,EAAQ,GACP;;KACP,EAAM,SAAS,KACZ,kBAAC,UAAD;MAAQ,MAAK;MAAS,WAAW,EAAO;MAAO,eAAe,EAAS,CAAC,CAAC;MACpE,UAAA,EAAQ;KACL,CAAA;KAEX;IACA;;GAGT,kBAAC,GAAD;IAAgB,aAAU;IAAS,MAAK;IACnC,UAAA,EAAQ,OAAO,EAAM,MAAM;GAChB,CAAA;EACf;;AAEb;AASA,SAAS,EAAW,EAChB,OACA,UACA,UACA,YACA,eAOD;CACC,IAAM,IAAO,EAAM,SAAS,WAAW,WAAW,EAAM,SAAS,SAAS,SAAS;CAqEnF,OAnEI,EAAM,SAAS,YAAY,CAAC,EAAQ,EAAM,QAAQ,IAE9C,kBAAC,SAAD;EAAO,WAAW,EAAO;EAAzB,UAAA,CACI,kBAAC,GAAD,EAAA,UAAiB,EAAQ,MAAsB,CAAA,GAC/C,kBAAC,UAAD;GACQ;GACJ,WAAW,EAAO;GAClB,OAAO,EAAM;GACb,WAAW,MAAU,EAAS;IAAE,GAAG;IAAO,OAAO,EAAM,OAAO;GAAM,CAAC;GAJzE,UAAA,CAMI,kBAAC,UAAD;IAAQ,OAAM;IAAG,UAAA;GAAS,CAAA,GACzB,EAAM,SAAS,KAAK,MACjB,kBAAC,UAAD;IAA2B,OAAO,EAAO;IACpC,UAAA,EAAO;GACJ,GAFK,EAAO,KAEZ,CACX,CACG;EACL,CAAA,CAAA;MAIX,EAAM,SAAS,YAEX,kBAAC,SAAD;EAAO,WAAW,EAAO;EAAzB,UAAA,CACI,kBAAC,GAAD,EAAA,UAAiB,EAAQ,MAAsB,CAAA,GAC/C,kBAAC,UAAD;GACQ;GACJ,WAAW,EAAO;GAClB,OAAO,EAAM;GACb,WAAW,MAAU,EAAS;IAAE,GAAG;IAAO,OAAO,EAAM,OAAO;GAAM,CAAC;GAJzE,UAAA;IAMI,kBAAC,UAAD;KAAQ,OAAM;KAAG,UAAA;IAAS,CAAA;IAC1B,kBAAC,UAAD;KAAQ,OAAM;KAAQ,UAAA,EAAQ;IAAY,CAAA;IAC1C,kBAAC,UAAD;KAAQ,OAAM;KAAS,UAAA,EAAQ;IAAW,CAAA;GACtC;EACL,CAAA,CAAA;MAIX,EAAM,aAAa,YAEf,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,SAAD;EAAO,WAAW,EAAO;EAAzB,UAAA,CACI,kBAAC,GAAD,EAAA,UAAiB,EAAQ,KAAqB,CAAA,GAC9C,kBAAC,SAAD;GACQ;GACJ,WAAW,EAAO;GACZ;GACN,OAAO,EAAM;GACb,aAAa,EAAQ;GACrB,WAAW,MAAU,EAAS;IAAE,GAAG;IAAO,OAAO,EAAM,OAAO;GAAM,CAAC;EACxE,CAAA,CACE;CACP,CAAA,GAAA,kBAAC,SAAD;EAAO,WAAW,EAAO;EAAzB,UAAA,CACI,kBAAC,GAAD,EAAA,UAAiB,EAAQ,GAAmB,CAAA,GAC5C,kBAAC,SAAD;GACI,WAAW,EAAO;GACZ;GACN,OAAO,EAAM;GACb,aAAa,EAAQ;GACrB,WAAW,MAAU,EAAS;IAAE,GAAG;IAAO,IAAI,EAAM,OAAO;GAAM,CAAC;EACrE,CAAA,CACE;CACT,CAAA,CAAA,EAAA,CAAA,IAKN,kBAAC,SAAD;EAAO,WAAW,EAAO;EAAzB,UAAA,CACI,kBAAC,GAAD,EAAA,UAAiB,EAAQ,MAAsB,CAAA,GAC/C,kBAAC,SAAD;GACQ;GACJ,WAAW,EAAO;GACZ;GACN,OAAO,EAAM;GACb,aAAa,EAAM,eAAe,EAAQ;GAC1C,WAAW,MAAU,EAAS;IAAE,GAAG;IAAO,OAAO,EAAM,OAAO;GAAM,CAAC;EACxE,CAAA,CACE;;AAEf"}
1
+ {"version":3,"file":"FilterBar.js","names":[],"sources":["../../../src/components/FilterBar/FilterBar.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a filter bar renders a different\n * editor per filter kind — select, multi-select, range, date range, text — and the\n * body is that dispatch plus the chip summary, both reading the same value map.\n */\nimport { useId, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n defaultOperator,\n describeFilter,\n filterStrings,\n isComplete,\n isMulti,\n isValueless,\n operatorLabel,\n operatorsFor,\n type Filter,\n type FilterField,\n} from \"./filter-model\";\nimport styles from \"./FilterBar.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\";\n\nexport interface FilterBarProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** Fields the user may filter by. */\n fields: readonly FilterField[];\n /** Applied filters. Controlled. */\n value: readonly Filter[];\n /** Next filter set. Combined with AND. */\n onChange: (filters: Filter[]) => void;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the bar — a \"save this view\" button, a counter. */\n actions?: ReactNode;\n}\n\n/** The draft filter being built in the editor. */\ninterface Draft {\n field: string;\n operator: Filter[\"operator\"];\n value: string;\n to: string;\n}\n\n/** A fresh draft for a field. */\nfunction draftFor(field: FilterField): Draft {\n return { field: field.name, operator: defaultOperator(field), value: \"\", to: \"\" };\n}\n\n/** Turn a draft into a filter. */\nfunction toFilter(draft: Draft): Filter {\n if (isValueless(draft.operator)) return { field: draft.field, operator: draft.operator };\n if (draft.operator === \"between\") {\n return { field: draft.field, operator: draft.operator, value: [draft.value, draft.to] };\n }\n if (isMulti(draft.operator)) {\n return {\n field: draft.field,\n operator: draft.operator,\n value: draft.value\n .split(\",\")\n .map((part) => part.trim())\n .filter(Boolean),\n };\n }\n return { field: draft.field, operator: draft.operator, value: draft.value };\n}\n\n/**\n * A row of applied filters, and a small editor to add another.\n *\n * Filters are combined with **AND**, flat — no nested `(a OR b) AND c` groups. That\n * is the shape of nearly every admin list (\"status is paid, created after March,\n * text contains nota\"), and it is what serializes into a URL somebody can send to a\n * colleague. Nested groups need a tree UI with per-node operators and a different\n * serialization; a component trying to be both is clumsy at the common case.\n *\n * Controlled, like the rest of the SDK: it holds the half-typed draft in the editor\n * and nothing else. The applied set is the app's, which is what lets it come from —\n * and go back into — the URL.\n *\n * @example\n * const [filters, setFilters] = useState<Filter[]>(() =>\n * filtersFromSearchParams(new URLSearchParams(location.search), FIELDS),\n * );\n *\n * <FilterBar fields={FIELDS} value={filters} onChange={setFilters} />\n */\nexport function FilterBar({\n fields,\n value,\n onChange,\n locale = \"pt-BR\",\n actions,\n className,\n ...rest\n}: FilterBarProps) {\n const strings = filterStrings(locale);\n const baseId = useId();\n const [draft, setDraft] = useState<Draft | null>(null);\n\n const field = draft ? fields.find((candidate) => candidate.name === draft.field) : undefined;\n const pending = draft ? toFilter(draft) : null;\n const ready = pending !== null && isComplete(pending);\n\n const open = (): void => {\n if (fields.length > 0) setDraft(draftFor(fields[0]));\n };\n\n const apply = (): void => {\n if (!pending || !ready) return;\n onChange([...value, pending]);\n setDraft(null);\n };\n\n const removeAt = (index: number): void => {\n onChange(value.filter((_, position) => position !== index));\n };\n\n return (\n <div className={cn(styles.bar, className)} {...rest}>\n <ul className={styles.chips}>\n {value.length === 0 && !draft && <li className={styles.empty}>{strings.empty}</li>}\n {value.map((filter, index) => {\n const description = describeFilter(filter, fields, locale);\n return (\n <li\n key={`${filter.field}-${filter.operator}-${index}`}\n className={styles.chip}\n >\n <span>{description}</span>\n <button\n type=\"button\"\n className={styles.remove}\n aria-label={strings.remove(description)}\n onClick={() => removeAt(index)}\n >\n ×\n </button>\n </li>\n );\n })}\n </ul>\n\n {draft && field ? (\n <div className={styles.editor} role=\"group\" aria-label={strings.add}>\n <label className={styles.control}>\n <VisuallyHidden>{strings.field}</VisuallyHidden>\n <select\n className={styles.select}\n value={draft.field}\n onChange={(event) => {\n const next = fields.find(\n (candidate) => candidate.name === event.target.value,\n );\n if (next) setDraft(draftFor(next));\n }}\n >\n {fields.map((candidate) => (\n <option key={candidate.name} value={candidate.name}>\n {candidate.label}\n </option>\n ))}\n </select>\n </label>\n\n <label className={styles.control}>\n <VisuallyHidden>{strings.operator}</VisuallyHidden>\n <select\n className={styles.select}\n value={draft.operator}\n onChange={(event) =>\n setDraft({\n ...draft,\n operator: event.target.value as Filter[\"operator\"],\n value: \"\",\n to: \"\",\n })\n }\n >\n {operatorsFor(field).map((operator) => (\n <option key={operator} value={operator}>\n {operatorLabel(operator, locale)}\n </option>\n ))}\n </select>\n </label>\n\n {!isValueless(draft.operator) && (\n <ValueInput\n id={`${baseId}-value`}\n field={field}\n draft={draft}\n strings={strings}\n onChange={setDraft}\n />\n )}\n\n <button\n type=\"button\"\n className={styles.primary}\n disabled={!ready}\n onClick={apply}\n >\n {strings.apply}\n </button>\n <button type=\"button\" className={styles.ghost} onClick={() => setDraft(null)}>\n {strings.cancel}\n </button>\n </div>\n ) : (\n <div className={styles.actions}>\n <button\n type=\"button\"\n className={styles.secondary}\n disabled={fields.length === 0}\n onClick={open}\n >\n + {strings.add}\n </button>\n {value.length > 0 && (\n <button type=\"button\" className={styles.ghost} onClick={() => onChange([])}>\n {strings.clear}\n </button>\n )}\n {actions}\n </div>\n )}\n\n <VisuallyHidden aria-live=\"polite\" role=\"status\">\n {strings.active(value.length)}\n </VisuallyHidden>\n </div>\n );\n}\n\n/**\n * The value input for the draft.\n *\n * The input type follows the **field**, not the operator: a date field gets a date\n * picker even under `between`, where two of them are needed. Typing a date into a\n * text box is the fastest way to produce a filter the backend cannot parse.\n */\nfunction ValueInput({\n id,\n field,\n draft,\n strings,\n onChange,\n}: {\n id: string;\n field: FilterField;\n draft: Draft;\n strings: ReturnType<typeof filterStrings>;\n onChange: (draft: Draft) => void;\n}) {\n const type = field.type === \"number\" ? \"number\" : field.type === \"date\" ? \"date\" : \"text\";\n\n if (field.type === \"select\" && !isMulti(draft.operator)) {\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <select\n id={id}\n className={styles.select}\n value={draft.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n >\n <option value=\"\">—</option>\n {field.options?.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n );\n }\n\n if (field.type === \"boolean\") {\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <select\n id={id}\n className={styles.select}\n value={draft.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n >\n <option value=\"\">—</option>\n <option value=\"true\">{strings.yes}</option>\n <option value=\"false\">{strings.no}</option>\n </select>\n </label>\n );\n }\n\n if (draft.operator === \"between\") {\n return (\n <>\n <label className={styles.control}>\n <VisuallyHidden>{strings.from}</VisuallyHidden>\n <input\n id={id}\n className={styles.input}\n type={type}\n value={draft.value}\n placeholder={strings.from}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n />\n </label>\n <label className={styles.control}>\n <VisuallyHidden>{strings.to}</VisuallyHidden>\n <input\n className={styles.input}\n type={type}\n value={draft.to}\n placeholder={strings.to}\n onChange={(event) => onChange({ ...draft, to: event.target.value })}\n />\n </label>\n </>\n );\n }\n\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <input\n id={id}\n className={styles.input}\n type={type}\n value={draft.value}\n placeholder={field.placeholder ?? strings.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n />\n </label>\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("./filter-model.cjs"),t=require("../../utils/format.cjs");var n=/^\d{4}-\d{2}-\d{2}$/,r=new Intl.Collator(void 0,{numeric:!0});function i(e,t){if(!(typeof e!=`object`||!e))return e[t]}function a(e){return e instanceof Date?t.formatDateForInput(e)||null:typeof e!=`string`||e.trim()===``?null:t.formatDateForInput(e)||null}function o(e,t){if(e==null)return null;if(typeof e==`number`){let n=Number(t);return Number.isNaN(n)?null:e-n}if(typeof e==`boolean`){let n=t===`true`||t!==`false`&&null;return n===null?null:Number(e)-Number(n)}if(n.test(t)){let n=a(e);return n===null?null:n.localeCompare(t)}return r.compare(String(e),t)}function s(e,t){if(e===t)return!0;let n=o(e,t);return n===null?String(e??``)===t:n===0}function c(e){return e==null?!0:typeof e==`string`?e.trim()===``:Array.isArray(e)?e.length===0:!1}function l(e){return e.value===void 0?[]:Array.isArray(e.value)?[...e.value]:[String(e.value)]}function u(e,t,n){switch(e){case`empty`:return c(t);case`notEmpty`:return!c(t);case`eq`:return s(t,n[0]??``);case`ne`:return!s(t,n[0]??``);case`contains`:return String(t??``).toLowerCase().includes((n[0]??``).toLowerCase());case`in`:return n.some(e=>s(t,e));case`between`:{let[e=``,r=``]=n,i=o(t,e),a=o(t,r);return i===null||a===null?!1:i>=0&&a<=0}default:{let r=o(t,n[0]??``);return r===null?!1:e===`gt`?r>0:e===`gte`?r>=0:e===`lt`?r<0:r<=0}}}function d(e){let[t=``,i=``]=e;if(n.test(t)&&n.test(i))return t<=i?[t,i]:[i,t];let a=Number(t),o=Number(i);return!Number.isNaN(a)&&!Number.isNaN(o)?a<=o?[t,i]:[i,t]:r.compare(t,i)<=0?[t,i]:[i,t]}function f(t,n){let r=n.filter(e.isComplete).map(e=>({field:e.field,operator:e.operator,values:e.operator===`between`?d(l(e)):l(e)}));return r.length===0?[...t]:t.filter(e=>r.every(({field:t,operator:n,values:r})=>u(n,i(e,t),r)))}exports.applyFilters=f,exports.orderRange=d;
1
+ const e=require("./filter-model.cjs"),t=require("../../utils/format.cjs");var n=/^\d{4}-\d{2}-\d{2}$/,r=new Intl.Collator(void 0,{numeric:!0});function i(e,t){if(typeof e==`object`&&e)return e[t]}function a(e){return e instanceof Date?t.formatDateForInput(e)||null:typeof e!=`string`||e.trim()===``?null:t.formatDateForInput(e)||null}function o(e,t){if(e==null)return null;if(typeof e==`number`){let n=Number(t);return Number.isNaN(n)?null:e-n}if(typeof e==`boolean`){let n=t===`true`||t!==`false`&&null;return n===null?null:Number(e)-Number(n)}if(n.test(t)){let n=a(e);return n===null?null:n.localeCompare(t)}return r.compare(String(e),t)}function s(e,t){if(e===t)return!0;let n=o(e,t);return n===null?String(e??``)===t:n===0}function c(e){return e==null?!0:typeof e==`string`?e.trim()===``:Array.isArray(e)?e.length===0:!1}function l(e){return e.value===void 0?[]:Array.isArray(e.value)?[...e.value]:[String(e.value)]}function u(e,t,n){switch(e){case`empty`:return c(t);case`notEmpty`:return!c(t);case`eq`:return s(t,n[0]??``);case`ne`:return!s(t,n[0]??``);case`contains`:return String(t??``).toLowerCase().includes((n[0]??``).toLowerCase());case`in`:return n.some(e=>s(t,e));case`between`:{let[e=``,r=``]=n,i=o(t,e),a=o(t,r);return i===null||a===null?!1:i>=0&&a<=0}default:{let r=o(t,n[0]??``);return r===null?!1:e===`gt`?r>0:e===`gte`?r>=0:e===`lt`?r<0:r<=0}}}function d(e){let[t=``,i=``]=e;if(n.test(t)&&n.test(i))return t<=i?[t,i]:[i,t];let a=Number(t),o=Number(i);return!Number.isNaN(a)&&!Number.isNaN(o)?a<=o?[t,i]:[i,t]:r.compare(t,i)<=0?[t,i]:[i,t]}function f(t,n){let r=n.filter(e.isComplete).map(e=>({field:e.field,operator:e.operator,values:e.operator===`between`?d(l(e)):l(e)}));return r.length===0?[...t]:t.filter(e=>r.every(({field:t,operator:n,values:r})=>u(n,i(e,t),r)))}exports.applyFilters=f,exports.orderRange=d;
2
2
  //# sourceMappingURL=filter-apply.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"filter-apply.cjs","names":[],"sources":["../../../src/components/FilterBar/filter-apply.ts"],"sourcesContent":["// The other half of the filter model: evaluating a `Filter[]` instead of only\n// describing it. `FilterBar` produces filters, `applyFilters` runs them over an\n// in-memory list, and `filtersToQueryParams` (filter-query.ts) hands the same set\n// to a paginated backend. Without these two the model stops one step short of\n// useful and every app writes the eleven operator branches again.\n\nimport { formatDateForInput } from \"@/utils/format\";\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete } from \"./filter-model\";\n\n/** Matches a plain `yyyy-mm-dd`, the shape `<input type=\"date\">` produces. */\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * One collator for every text comparison in this module.\n *\n * `localeCompare(value, undefined, { numeric: true })` builds a collator on each\n * call — passing an options bag opts out of the engine's cached-default fast\n * path — and this comparison runs once per row per filter. Measured at 200k\n * comparisons: 453 ms through `localeCompare` against 25 ms through a hoisted\n * collator, i.e. ~23 ms of main thread per keystroke on a 10k-row list.\n */\nconst TEXT_COLLATOR = new Intl.Collator(undefined, { numeric: true });\n\n/**\n * Read a property off a row without asserting the row's shape.\n *\n * `Filter.field` is a string chosen by the app's field list, so it cannot be a\n * `keyof T`; the lookup goes through an index signature and a missing property\n * yields `undefined`, which the operators treat as an empty value.\n *\n * @param item - The row being tested.\n * @param field - Property name from the filter.\n * @returns The property value, or `undefined` when the row has no such key.\n */\nfunction readField(item: unknown, field: string): unknown {\n if (item == null || typeof item !== \"object\") return undefined;\n return (item as Record<string, unknown>)[field];\n}\n\n/**\n * Local `yyyy-mm-dd` for a value that represents a date, or `null`.\n *\n * Delegates the formatting to {@link formatDateForInput}, which owns the rule\n * that the key is built from the local calendar parts rather than `toISOString`.\n * This adds only the narrowing from `unknown` and maps \"not a date\" to `null`\n * instead of to the empty string a date input wants.\n *\n * @param value - A `Date`, a date-ish string, or anything else.\n * @returns The day key, or `null` when the value does not denote a date.\n */\nfunction dayKey(value: unknown): string | null {\n if (value instanceof Date) return formatDateForInput(value) || null;\n if (typeof value !== \"string\" || value.trim() === \"\") return null;\n return formatDateForInput(value) || null;\n}\n\n/**\n * Compare a row value against a filter value, in the row value's own type.\n *\n * The filter always carries strings — it comes from form inputs and from the\n * URL — while the row carries whatever the API returned. Comparing them as text\n * gets the two common cases wrong: `10` would sort before `9`, and a date would\n * be compared by however it happens to be formatted. So the row value picks the\n * comparison, and the filter value is coerced into it.\n *\n * Dates compare by **day**, not by instant: a row stamped `2026-03-05T13:00:00Z`\n * is `>= 2026-03-05`, which is what the person picking a date in the UI means.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Negative, zero or positive, or `null` when the two cannot be compared.\n */\nfunction compare(left: unknown, right: string): number | null {\n if (left == null) return null;\n\n if (typeof left === \"number\") {\n const parsed = Number(right);\n return Number.isNaN(parsed) ? null : left - parsed;\n }\n\n if (typeof left === \"boolean\") {\n const parsed = right === \"true\" ? true : right === \"false\" ? false : null;\n return parsed === null ? null : Number(left) - Number(parsed);\n }\n\n if (DATE_ONLY.test(right)) {\n const key = dayKey(left);\n return key === null ? null : key.localeCompare(right);\n }\n\n return TEXT_COLLATOR.compare(String(left), right);\n}\n\n/**\n * Equality in the row value's type, falling back to exact text.\n *\n * Case-sensitive on purpose: `eq` maps to a plain `WHERE column = value` on the\n * server side, so a case-insensitive client would quietly disagree with the\n * backend for the same filter. `contains` is the case-insensitive operator, and\n * it is also the default one for text fields, so the friendly behaviour is what\n * people get without asking.\n *\n * Identical strings take a fast path out, since the row value is text in the\n * common case and the collator is the expensive part.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Whether the two are equal.\n */\nfunction equals(left: unknown, right: string): boolean {\n if (left === right) return true;\n const result = compare(left, right);\n if (result !== null) return result === 0;\n return String(left ?? \"\") === right;\n}\n\n/**\n * Is this row value empty?\n *\n * Empty means absent, blank text or an empty list — never falsy. `0` and `false`\n * are values somebody chose, and a filter that hid them would be reporting the\n * wrong count on every dashboard that tracks zeroes.\n *\n * @param value - The row value.\n * @returns Whether the value counts as empty.\n */\nfunction isEmptyValue(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * The filter's value as a list, whatever arity the operator uses.\n *\n * @param filter - The filter to read.\n * @returns Its values, trimmed of nothing — a leading space may be meaningful text.\n */\nfunction valuesOf(filter: Filter): string[] {\n if (filter.value === undefined) return [];\n return Array.isArray(filter.value) ? [...filter.value] : [String(filter.value)];\n}\n\n/**\n * Evaluate one operator against one row value.\n *\n * @param operator - The comparison to make.\n * @param left - The row value.\n * @param values - The filter values (one, two for `between`, many for `in`).\n * @returns Whether the row value satisfies the operator.\n */\nfunction matchesOperator(operator: FilterOperator, left: unknown, values: string[]): boolean {\n switch (operator) {\n case \"empty\":\n return isEmptyValue(left);\n case \"notEmpty\":\n return !isEmptyValue(left);\n case \"eq\":\n return equals(left, values[0] ?? \"\");\n case \"ne\":\n return !equals(left, values[0] ?? \"\");\n case \"contains\":\n return String(left ?? \"\")\n .toLowerCase()\n .includes((values[0] ?? \"\").toLowerCase());\n case \"in\":\n return values.some((value) => equals(left, value));\n case \"between\": {\n const [first = \"\", second = \"\"] = values;\n const low = compare(left, first);\n const high = compare(left, second);\n if (low === null || high === null) return false;\n return low >= 0 && high <= 0;\n }\n default: {\n const result = compare(left, values[0] ?? \"\");\n if (result === null) return false;\n if (operator === \"gt\") return result > 0;\n if (operator === \"gte\") return result >= 0;\n if (operator === \"lt\") return result < 0;\n return result <= 0;\n }\n }\n}\n\n/**\n * Order the two values of a `between` so the range is never empty by accident.\n *\n * A person who picks the later date first means the range between the two dates,\n * not \"no rows\". The server-side `BETWEEN` needs `(lo, hi)` in order too, which\n * is why the normalisation lives here and is shared by `filtersToQueryParams`.\n *\n * @param values - The raw pair.\n * @returns The pair, ascending.\n */\nexport function orderRange(values: readonly string[]): string[] {\n const [first = \"\", second = \"\"] = values;\n if (DATE_ONLY.test(first) && DATE_ONLY.test(second)) {\n return first <= second ? [first, second] : [second, first];\n }\n const low = Number(first);\n const high = Number(second);\n if (!Number.isNaN(low) && !Number.isNaN(high)) {\n return low <= high ? [first, second] : [second, first];\n }\n return TEXT_COLLATOR.compare(first, second) <= 0 ? [first, second] : [second, first];\n}\n\n/**\n * Run a filter set over an in-memory list.\n *\n * Closes the loop `FilterBar` opens: the bar produces `Filter[]`, this applies\n * them. Filters combine with `AND`, matching the flat model the bar builds, and\n * an incomplete filter is skipped rather than treated as a match of nothing — a\n * half-filled form should not empty the table underneath it.\n *\n * Comparison follows the row's type, not the filter's: numbers compare\n * numerically, dates compare by day, and everything else compares as text with\n * `numeric: true` so `\"item 2\"` lands before `\"item 10\"`.\n *\n * A few behaviours differ from the SQL the server-side twin produces, and the\n * difference is deliberate rather than accidental:\n *\n * - `ne` matches rows whose value is absent. In SQL, `column <> 'x'` is `NULL`\n * for a `NULL` column and the row drops out. Here \"is not paid\" shows the rows\n * with no status at all, which is what the chip claims.\n * - `empty` matches `NULL`, blank text and empty lists; `__isnull` on the server\n * only matches `NULL`. A column that stores `\"\"` instead of `NULL` is where\n * the two disagree.\n *\n * @example\n * const visible = applyFilters(orders, filters);\n *\n * Each filter's values are normalised once, before the scan, rather than inside\n * the per-row predicate: the arity, the `between` ordering and the value array\n * depend only on the filter, so deriving them per row multiplied that work by\n * the row count. `empty`/`notEmpty` ignore their values entirely, which is why\n * the prepared shape does not need to distinguish them.\n *\n * @param items - The full list.\n * @param filters - Applied filters; incomplete ones are ignored.\n * @returns A new array with the rows that satisfy every complete filter.\n */\nexport function applyFilters<T>(items: readonly T[], filters: readonly Filter[]): T[] {\n const active = filters.filter(isComplete).map((filter) => ({\n field: filter.field,\n operator: filter.operator,\n values: filter.operator === \"between\" ? orderRange(valuesOf(filter)) : valuesOf(filter),\n }));\n if (active.length === 0) return [...items];\n\n return items.filter((item) =>\n active.every(({ field, operator, values }) =>\n matchesOperator(operator, readField(item, field), values),\n ),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"filter-apply.cjs","names":[],"sources":["../../../src/components/FilterBar/filter-apply.ts"],"sourcesContent":["// The other half of the filter model: evaluating a `Filter[]` instead of only\n// describing it. `FilterBar` produces filters, `applyFilters` runs them over an\n// in-memory list, and `filtersToQueryParams` (filter-query.ts) hands the same set\n// to a paginated backend. Without these two the model stops one step short of\n// useful and every app writes the eleven operator branches again.\n\nimport { formatDateForInput } from \"@/utils/format\";\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete } from \"./filter-model\";\n\n/** Matches a plain `yyyy-mm-dd`, the shape `<input type=\"date\">` produces. */\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * One collator for every text comparison in this module.\n *\n * `localeCompare(value, undefined, { numeric: true })` builds a collator on each\n * call — passing an options bag opts out of the engine's cached-default fast\n * path — and this comparison runs once per row per filter. Measured at 200k\n * comparisons: 453 ms through `localeCompare` against 25 ms through a hoisted\n * collator, i.e. ~23 ms of main thread per keystroke on a 10k-row list.\n */\nconst TEXT_COLLATOR = new Intl.Collator(undefined, { numeric: true });\n\n/**\n * Read a property off a row without asserting the row's shape.\n *\n * `Filter.field` is a string chosen by the app's field list, so it cannot be a\n * `keyof T`; the lookup goes through an index signature and a missing property\n * yields `undefined`, which the operators treat as an empty value.\n *\n * @param item - The row being tested.\n * @param field - Property name from the filter.\n * @returns The property value, or `undefined` when the row has no such key.\n */\nfunction readField(item: unknown, field: string): unknown {\n if (item == null || typeof item !== \"object\") return undefined;\n return (item as Record<string, unknown>)[field];\n}\n\n/**\n * Local `yyyy-mm-dd` for a value that represents a date, or `null`.\n *\n * Delegates the formatting to {@link formatDateForInput}, which owns the rule\n * that the key is built from the local calendar parts rather than `toISOString`.\n * This adds only the narrowing from `unknown` and maps \"not a date\" to `null`\n * instead of to the empty string a date input wants.\n *\n * @param value - A `Date`, a date-ish string, or anything else.\n * @returns The day key, or `null` when the value does not denote a date.\n */\nfunction dayKey(value: unknown): string | null {\n if (value instanceof Date) return formatDateForInput(value) || null;\n if (typeof value !== \"string\" || value.trim() === \"\") return null;\n return formatDateForInput(value) || null;\n}\n\n/**\n * Compare a row value against a filter value, in the row value's own type.\n *\n * The filter always carries strings — it comes from form inputs and from the\n * URL — while the row carries whatever the API returned. Comparing them as text\n * gets the two common cases wrong: `10` would sort before `9`, and a date would\n * be compared by however it happens to be formatted. So the row value picks the\n * comparison, and the filter value is coerced into it.\n *\n * Dates compare by **day**, not by instant: a row stamped `2026-03-05T13:00:00Z`\n * is `>= 2026-03-05`, which is what the person picking a date in the UI means.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Negative, zero or positive, or `null` when the two cannot be compared.\n */\nfunction compare(left: unknown, right: string): number | null {\n if (left == null) return null;\n\n if (typeof left === \"number\") {\n const parsed = Number(right);\n return Number.isNaN(parsed) ? null : left - parsed;\n }\n\n if (typeof left === \"boolean\") {\n const parsed = right === \"true\" ? true : right === \"false\" ? false : null;\n return parsed === null ? null : Number(left) - Number(parsed);\n }\n\n if (DATE_ONLY.test(right)) {\n const key = dayKey(left);\n return key === null ? null : key.localeCompare(right);\n }\n\n return TEXT_COLLATOR.compare(String(left), right);\n}\n\n/**\n * Equality in the row value's type, falling back to exact text.\n *\n * Case-sensitive on purpose: `eq` maps to a plain `WHERE column = value` on the\n * server side, so a case-insensitive client would quietly disagree with the\n * backend for the same filter. `contains` is the case-insensitive operator, and\n * it is also the default one for text fields, so the friendly behaviour is what\n * people get without asking.\n *\n * Identical strings take a fast path out, since the row value is text in the\n * common case and the collator is the expensive part.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Whether the two are equal.\n */\nfunction equals(left: unknown, right: string): boolean {\n if (left === right) return true;\n const result = compare(left, right);\n if (result !== null) return result === 0;\n return String(left ?? \"\") === right;\n}\n\n/**\n * Is this row value empty?\n *\n * Empty means absent, blank text or an empty list — never falsy. `0` and `false`\n * are values somebody chose, and a filter that hid them would be reporting the\n * wrong count on every dashboard that tracks zeroes.\n *\n * @param value - The row value.\n * @returns Whether the value counts as empty.\n */\nfunction isEmptyValue(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * The filter's value as a list, whatever arity the operator uses.\n *\n * @param filter - The filter to read.\n * @returns Its values, trimmed of nothing — a leading space may be meaningful text.\n */\nfunction valuesOf(filter: Filter): string[] {\n if (filter.value === undefined) return [];\n return Array.isArray(filter.value) ? [...filter.value] : [String(filter.value)];\n}\n\n/**\n * Evaluate one operator against one row value.\n *\n * @param operator - The comparison to make.\n * @param left - The row value.\n * @param values - The filter values (one, two for `between`, many for `in`).\n * @returns Whether the row value satisfies the operator.\n */\nfunction matchesOperator(operator: FilterOperator, left: unknown, values: string[]): boolean {\n switch (operator) {\n case \"empty\":\n return isEmptyValue(left);\n case \"notEmpty\":\n return !isEmptyValue(left);\n case \"eq\":\n return equals(left, values[0] ?? \"\");\n case \"ne\":\n return !equals(left, values[0] ?? \"\");\n case \"contains\":\n return String(left ?? \"\")\n .toLowerCase()\n .includes((values[0] ?? \"\").toLowerCase());\n case \"in\":\n return values.some((value) => equals(left, value));\n case \"between\": {\n const [first = \"\", second = \"\"] = values;\n const low = compare(left, first);\n const high = compare(left, second);\n if (low === null || high === null) return false;\n return low >= 0 && high <= 0;\n }\n default: {\n const result = compare(left, values[0] ?? \"\");\n if (result === null) return false;\n if (operator === \"gt\") return result > 0;\n if (operator === \"gte\") return result >= 0;\n if (operator === \"lt\") return result < 0;\n return result <= 0;\n }\n }\n}\n\n/**\n * Order the two values of a `between` so the range is never empty by accident.\n *\n * A person who picks the later date first means the range between the two dates,\n * not \"no rows\". The server-side `BETWEEN` needs `(lo, hi)` in order too, which\n * is why the normalisation lives here and is shared by `filtersToQueryParams`.\n *\n * @param values - The raw pair.\n * @returns The pair, ascending.\n */\nexport function orderRange(values: readonly string[]): string[] {\n const [first = \"\", second = \"\"] = values;\n if (DATE_ONLY.test(first) && DATE_ONLY.test(second)) {\n return first <= second ? [first, second] : [second, first];\n }\n const low = Number(first);\n const high = Number(second);\n if (!Number.isNaN(low) && !Number.isNaN(high)) {\n return low <= high ? [first, second] : [second, first];\n }\n return TEXT_COLLATOR.compare(first, second) <= 0 ? [first, second] : [second, first];\n}\n\n/**\n * Run a filter set over an in-memory list.\n *\n * Closes the loop `FilterBar` opens: the bar produces `Filter[]`, this applies\n * them. Filters combine with `AND`, matching the flat model the bar builds, and\n * an incomplete filter is skipped rather than treated as a match of nothing — a\n * half-filled form should not empty the table underneath it.\n *\n * Comparison follows the row's type, not the filter's: numbers compare\n * numerically, dates compare by day, and everything else compares as text with\n * `numeric: true` so `\"item 2\"` lands before `\"item 10\"`.\n *\n * A few behaviours differ from the SQL the server-side twin produces, and the\n * difference is deliberate rather than accidental:\n *\n * - `ne` matches rows whose value is absent. In SQL, `column <> 'x'` is `NULL`\n * for a `NULL` column and the row drops out. Here \"is not paid\" shows the rows\n * with no status at all, which is what the chip claims.\n * - `empty` matches `NULL`, blank text and empty lists; `__isnull` on the server\n * only matches `NULL`. A column that stores `\"\"` instead of `NULL` is where\n * the two disagree.\n *\n * @example\n * const visible = applyFilters(orders, filters);\n *\n * Each filter's values are normalised once, before the scan, rather than inside\n * the per-row predicate: the arity, the `between` ordering and the value array\n * depend only on the filter, so deriving them per row multiplied that work by\n * the row count. `empty`/`notEmpty` ignore their values entirely, which is why\n * the prepared shape does not need to distinguish them.\n *\n * @param items - The full list.\n * @param filters - Applied filters; incomplete ones are ignored.\n * @returns A new array with the rows that satisfy every complete filter.\n */\nexport function applyFilters<T>(items: readonly T[], filters: readonly Filter[]): T[] {\n const active = filters.filter(isComplete).map((filter) => ({\n field: filter.field,\n operator: filter.operator,\n values: filter.operator === \"between\" ? orderRange(valuesOf(filter)) : valuesOf(filter),\n }));\n if (active.length === 0) return [...items];\n\n return items.filter((item) =>\n active.every(({ field, operator, values }) =>\n matchesOperator(operator, readField(item, field), values),\n ),\n );\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { formatDateForInput as t } from "../../utils/format.js";
3
3
  //#region src/components/FilterBar/filter-apply.ts
4
4
  var n = /^\d{4}-\d{2}-\d{2}$/, r = new Intl.Collator(void 0, { numeric: !0 });
5
5
  function i(e, t) {
6
- if (!(typeof e != "object" || !e)) return e[t];
6
+ if (typeof e == "object" && e) return e[t];
7
7
  }
8
8
  function a(e) {
9
9
  return e instanceof Date ? t(e) || null : typeof e != "string" || e.trim() === "" ? null : t(e) || null;
@@ -1 +1 @@
1
- {"version":3,"file":"filter-apply.js","names":[],"sources":["../../../src/components/FilterBar/filter-apply.ts"],"sourcesContent":["// The other half of the filter model: evaluating a `Filter[]` instead of only\n// describing it. `FilterBar` produces filters, `applyFilters` runs them over an\n// in-memory list, and `filtersToQueryParams` (filter-query.ts) hands the same set\n// to a paginated backend. Without these two the model stops one step short of\n// useful and every app writes the eleven operator branches again.\n\nimport { formatDateForInput } from \"@/utils/format\";\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete } from \"./filter-model\";\n\n/** Matches a plain `yyyy-mm-dd`, the shape `<input type=\"date\">` produces. */\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * One collator for every text comparison in this module.\n *\n * `localeCompare(value, undefined, { numeric: true })` builds a collator on each\n * call — passing an options bag opts out of the engine's cached-default fast\n * path — and this comparison runs once per row per filter. Measured at 200k\n * comparisons: 453 ms through `localeCompare` against 25 ms through a hoisted\n * collator, i.e. ~23 ms of main thread per keystroke on a 10k-row list.\n */\nconst TEXT_COLLATOR = new Intl.Collator(undefined, { numeric: true });\n\n/**\n * Read a property off a row without asserting the row's shape.\n *\n * `Filter.field` is a string chosen by the app's field list, so it cannot be a\n * `keyof T`; the lookup goes through an index signature and a missing property\n * yields `undefined`, which the operators treat as an empty value.\n *\n * @param item - The row being tested.\n * @param field - Property name from the filter.\n * @returns The property value, or `undefined` when the row has no such key.\n */\nfunction readField(item: unknown, field: string): unknown {\n if (item == null || typeof item !== \"object\") return undefined;\n return (item as Record<string, unknown>)[field];\n}\n\n/**\n * Local `yyyy-mm-dd` for a value that represents a date, or `null`.\n *\n * Delegates the formatting to {@link formatDateForInput}, which owns the rule\n * that the key is built from the local calendar parts rather than `toISOString`.\n * This adds only the narrowing from `unknown` and maps \"not a date\" to `null`\n * instead of to the empty string a date input wants.\n *\n * @param value - A `Date`, a date-ish string, or anything else.\n * @returns The day key, or `null` when the value does not denote a date.\n */\nfunction dayKey(value: unknown): string | null {\n if (value instanceof Date) return formatDateForInput(value) || null;\n if (typeof value !== \"string\" || value.trim() === \"\") return null;\n return formatDateForInput(value) || null;\n}\n\n/**\n * Compare a row value against a filter value, in the row value's own type.\n *\n * The filter always carries strings — it comes from form inputs and from the\n * URL — while the row carries whatever the API returned. Comparing them as text\n * gets the two common cases wrong: `10` would sort before `9`, and a date would\n * be compared by however it happens to be formatted. So the row value picks the\n * comparison, and the filter value is coerced into it.\n *\n * Dates compare by **day**, not by instant: a row stamped `2026-03-05T13:00:00Z`\n * is `>= 2026-03-05`, which is what the person picking a date in the UI means.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Negative, zero or positive, or `null` when the two cannot be compared.\n */\nfunction compare(left: unknown, right: string): number | null {\n if (left == null) return null;\n\n if (typeof left === \"number\") {\n const parsed = Number(right);\n return Number.isNaN(parsed) ? null : left - parsed;\n }\n\n if (typeof left === \"boolean\") {\n const parsed = right === \"true\" ? true : right === \"false\" ? false : null;\n return parsed === null ? null : Number(left) - Number(parsed);\n }\n\n if (DATE_ONLY.test(right)) {\n const key = dayKey(left);\n return key === null ? null : key.localeCompare(right);\n }\n\n return TEXT_COLLATOR.compare(String(left), right);\n}\n\n/**\n * Equality in the row value's type, falling back to exact text.\n *\n * Case-sensitive on purpose: `eq` maps to a plain `WHERE column = value` on the\n * server side, so a case-insensitive client would quietly disagree with the\n * backend for the same filter. `contains` is the case-insensitive operator, and\n * it is also the default one for text fields, so the friendly behaviour is what\n * people get without asking.\n *\n * Identical strings take a fast path out, since the row value is text in the\n * common case and the collator is the expensive part.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Whether the two are equal.\n */\nfunction equals(left: unknown, right: string): boolean {\n if (left === right) return true;\n const result = compare(left, right);\n if (result !== null) return result === 0;\n return String(left ?? \"\") === right;\n}\n\n/**\n * Is this row value empty?\n *\n * Empty means absent, blank text or an empty list — never falsy. `0` and `false`\n * are values somebody chose, and a filter that hid them would be reporting the\n * wrong count on every dashboard that tracks zeroes.\n *\n * @param value - The row value.\n * @returns Whether the value counts as empty.\n */\nfunction isEmptyValue(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * The filter's value as a list, whatever arity the operator uses.\n *\n * @param filter - The filter to read.\n * @returns Its values, trimmed of nothing — a leading space may be meaningful text.\n */\nfunction valuesOf(filter: Filter): string[] {\n if (filter.value === undefined) return [];\n return Array.isArray(filter.value) ? [...filter.value] : [String(filter.value)];\n}\n\n/**\n * Evaluate one operator against one row value.\n *\n * @param operator - The comparison to make.\n * @param left - The row value.\n * @param values - The filter values (one, two for `between`, many for `in`).\n * @returns Whether the row value satisfies the operator.\n */\nfunction matchesOperator(operator: FilterOperator, left: unknown, values: string[]): boolean {\n switch (operator) {\n case \"empty\":\n return isEmptyValue(left);\n case \"notEmpty\":\n return !isEmptyValue(left);\n case \"eq\":\n return equals(left, values[0] ?? \"\");\n case \"ne\":\n return !equals(left, values[0] ?? \"\");\n case \"contains\":\n return String(left ?? \"\")\n .toLowerCase()\n .includes((values[0] ?? \"\").toLowerCase());\n case \"in\":\n return values.some((value) => equals(left, value));\n case \"between\": {\n const [first = \"\", second = \"\"] = values;\n const low = compare(left, first);\n const high = compare(left, second);\n if (low === null || high === null) return false;\n return low >= 0 && high <= 0;\n }\n default: {\n const result = compare(left, values[0] ?? \"\");\n if (result === null) return false;\n if (operator === \"gt\") return result > 0;\n if (operator === \"gte\") return result >= 0;\n if (operator === \"lt\") return result < 0;\n return result <= 0;\n }\n }\n}\n\n/**\n * Order the two values of a `between` so the range is never empty by accident.\n *\n * A person who picks the later date first means the range between the two dates,\n * not \"no rows\". The server-side `BETWEEN` needs `(lo, hi)` in order too, which\n * is why the normalisation lives here and is shared by `filtersToQueryParams`.\n *\n * @param values - The raw pair.\n * @returns The pair, ascending.\n */\nexport function orderRange(values: readonly string[]): string[] {\n const [first = \"\", second = \"\"] = values;\n if (DATE_ONLY.test(first) && DATE_ONLY.test(second)) {\n return first <= second ? [first, second] : [second, first];\n }\n const low = Number(first);\n const high = Number(second);\n if (!Number.isNaN(low) && !Number.isNaN(high)) {\n return low <= high ? [first, second] : [second, first];\n }\n return TEXT_COLLATOR.compare(first, second) <= 0 ? [first, second] : [second, first];\n}\n\n/**\n * Run a filter set over an in-memory list.\n *\n * Closes the loop `FilterBar` opens: the bar produces `Filter[]`, this applies\n * them. Filters combine with `AND`, matching the flat model the bar builds, and\n * an incomplete filter is skipped rather than treated as a match of nothing — a\n * half-filled form should not empty the table underneath it.\n *\n * Comparison follows the row's type, not the filter's: numbers compare\n * numerically, dates compare by day, and everything else compares as text with\n * `numeric: true` so `\"item 2\"` lands before `\"item 10\"`.\n *\n * A few behaviours differ from the SQL the server-side twin produces, and the\n * difference is deliberate rather than accidental:\n *\n * - `ne` matches rows whose value is absent. In SQL, `column <> 'x'` is `NULL`\n * for a `NULL` column and the row drops out. Here \"is not paid\" shows the rows\n * with no status at all, which is what the chip claims.\n * - `empty` matches `NULL`, blank text and empty lists; `__isnull` on the server\n * only matches `NULL`. A column that stores `\"\"` instead of `NULL` is where\n * the two disagree.\n *\n * @example\n * const visible = applyFilters(orders, filters);\n *\n * Each filter's values are normalised once, before the scan, rather than inside\n * the per-row predicate: the arity, the `between` ordering and the value array\n * depend only on the filter, so deriving them per row multiplied that work by\n * the row count. `empty`/`notEmpty` ignore their values entirely, which is why\n * the prepared shape does not need to distinguish them.\n *\n * @param items - The full list.\n * @param filters - Applied filters; incomplete ones are ignored.\n * @returns A new array with the rows that satisfy every complete filter.\n */\nexport function applyFilters<T>(items: readonly T[], filters: readonly Filter[]): T[] {\n const active = filters.filter(isComplete).map((filter) => ({\n field: filter.field,\n operator: filter.operator,\n values: filter.operator === \"between\" ? orderRange(valuesOf(filter)) : valuesOf(filter),\n }));\n if (active.length === 0) return [...items];\n\n return items.filter((item) =>\n active.every(({ field, operator, values }) =>\n matchesOperator(operator, readField(item, field), values),\n ),\n );\n}\n"],"mappings":";;;AAWA,IAAM,IAAY,uBAWZ,IAAgB,IAAI,KAAK,SAAS,KAAA,GAAW,EAAE,SAAS,GAAK,CAAC;AAapE,SAAS,EAAU,GAAe,GAAwB;CAClD,MAAgB,OAAO,KAAS,aAAhC,IACJ,OAAQ,EAAiC;AAC7C;AAaA,SAAS,EAAO,GAA+B;CAG3C,OAFI,aAAiB,OAAa,EAAmB,CAAK,KAAK,OAC3D,OAAO,KAAU,YAAY,EAAM,KAAK,MAAM,KAAW,OACtD,EAAmB,CAAK,KAAK;AACxC;AAkBA,SAAS,EAAQ,GAAe,GAA8B;CAC1D,IAAI,KAAQ,MAAM,OAAO;CAEzB,IAAI,OAAO,KAAS,UAAU;EAC1B,IAAM,IAAS,OAAO,CAAK;EAC3B,OAAO,OAAO,MAAM,CAAM,IAAI,OAAO,IAAO;CAChD;CAEA,IAAI,OAAO,KAAS,WAAW;EAC3B,IAAM,IAAS,MAAU,UAAgB,MAAU,WAAkB;EACrE,OAAO,MAAW,OAAO,OAAO,OAAO,CAAI,IAAI,OAAO,CAAM;CAChE;CAEA,IAAI,EAAU,KAAK,CAAK,GAAG;EACvB,IAAM,IAAM,EAAO,CAAI;EACvB,OAAO,MAAQ,OAAO,OAAO,EAAI,cAAc,CAAK;CACxD;CAEA,OAAO,EAAc,QAAQ,OAAO,CAAI,GAAG,CAAK;AACpD;AAkBA,SAAS,EAAO,GAAe,GAAwB;CACnD,IAAI,MAAS,GAAO,OAAO;CAC3B,IAAM,IAAS,EAAQ,GAAM,CAAK;CAElC,OADI,MAAW,OACR,OAAO,KAAQ,EAAE,MAAM,IADF,MAAW;AAE3C;AAYA,SAAS,EAAa,GAAyB;CAI3C,OAHI,KAAS,OAAa,KACtB,OAAO,KAAU,WAAiB,EAAM,KAAK,MAAM,KACnD,MAAM,QAAQ,CAAK,IAAU,EAAM,WAAW,IAC3C;AACX;AAQA,SAAS,EAAS,GAA0B;CAExC,OADI,EAAO,UAAU,KAAA,IAAkB,CAAC,IACjC,MAAM,QAAQ,EAAO,KAAK,IAAI,CAAC,GAAG,EAAO,KAAK,IAAI,CAAC,OAAO,EAAO,KAAK,CAAC;AAClF;AAUA,SAAS,EAAgB,GAA0B,GAAe,GAA2B;CACzF,QAAQ,GAAR;EACI,KAAK,SACD,OAAO,EAAa,CAAI;EAC5B,KAAK,YACD,OAAO,CAAC,EAAa,CAAI;EAC7B,KAAK,MACD,OAAO,EAAO,GAAM,EAAO,MAAM,EAAE;EACvC,KAAK,MACD,OAAO,CAAC,EAAO,GAAM,EAAO,MAAM,EAAE;EACxC,KAAK,YACD,OAAO,OAAO,KAAQ,EAAE,CAAC,CACpB,YAAY,CAAC,CACb,UAAU,EAAO,MAAM,GAAA,CAAI,YAAY,CAAC;EACjD,KAAK,MACD,OAAO,EAAO,MAAM,MAAU,EAAO,GAAM,CAAK,CAAC;EACrD,KAAK,WAAW;GACZ,IAAM,CAAC,IAAQ,IAAI,IAAS,MAAM,GAC5B,IAAM,EAAQ,GAAM,CAAK,GACzB,IAAO,EAAQ,GAAM,CAAM;GAEjC,OADI,MAAQ,QAAQ,MAAS,OAAa,KACnC,KAAO,KAAK,KAAQ;EAC/B;EACA,SAAS;GACL,IAAM,IAAS,EAAQ,GAAM,EAAO,MAAM,EAAE;GAK5C,OAJI,MAAW,OAAa,KACxB,MAAa,OAAa,IAAS,IACnC,MAAa,QAAc,KAAU,IACrC,MAAa,OAAa,IAAS,IAChC,KAAU;EACrB;CACJ;AACJ;AAYA,SAAgB,EAAW,GAAqC;CAC5D,IAAM,CAAC,IAAQ,IAAI,IAAS,MAAM;CAClC,IAAI,EAAU,KAAK,CAAK,KAAK,EAAU,KAAK,CAAM,GAC9C,OAAO,KAAS,IAAS,CAAC,GAAO,CAAM,IAAI,CAAC,GAAQ,CAAK;CAE7D,IAAM,IAAM,OAAO,CAAK,GAClB,IAAO,OAAO,CAAM;CAI1B,OAHI,CAAC,OAAO,MAAM,CAAG,KAAK,CAAC,OAAO,MAAM,CAAI,IACjC,KAAO,IAAO,CAAC,GAAO,CAAM,IAAI,CAAC,GAAQ,CAAK,IAElD,EAAc,QAAQ,GAAO,CAAM,KAAK,IAAI,CAAC,GAAO,CAAM,IAAI,CAAC,GAAQ,CAAK;AACvF;AAqCA,SAAgB,EAAgB,GAAqB,GAAiC;CAClF,IAAM,IAAS,EAAQ,OAAO,CAAU,CAAC,CAAC,KAAK,OAAY;EACvD,OAAO,EAAO;EACd,UAAU,EAAO;EACjB,QAAQ,EAAO,aAAa,YAAY,EAAW,EAAS,CAAM,CAAC,IAAI,EAAS,CAAM;CAC1F,EAAE;CAGF,OAFI,EAAO,WAAW,IAAU,CAAC,GAAG,CAAK,IAElC,EAAM,QAAQ,MACjB,EAAO,OAAO,EAAE,UAAO,aAAU,gBAC7B,EAAgB,GAAU,EAAU,GAAM,CAAK,GAAG,CAAM,CAC5D,CACJ;AACJ"}
1
+ {"version":3,"file":"filter-apply.js","names":[],"sources":["../../../src/components/FilterBar/filter-apply.ts"],"sourcesContent":["// The other half of the filter model: evaluating a `Filter[]` instead of only\n// describing it. `FilterBar` produces filters, `applyFilters` runs them over an\n// in-memory list, and `filtersToQueryParams` (filter-query.ts) hands the same set\n// to a paginated backend. Without these two the model stops one step short of\n// useful and every app writes the eleven operator branches again.\n\nimport { formatDateForInput } from \"@/utils/format\";\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete } from \"./filter-model\";\n\n/** Matches a plain `yyyy-mm-dd`, the shape `<input type=\"date\">` produces. */\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * One collator for every text comparison in this module.\n *\n * `localeCompare(value, undefined, { numeric: true })` builds a collator on each\n * call — passing an options bag opts out of the engine's cached-default fast\n * path — and this comparison runs once per row per filter. Measured at 200k\n * comparisons: 453 ms through `localeCompare` against 25 ms through a hoisted\n * collator, i.e. ~23 ms of main thread per keystroke on a 10k-row list.\n */\nconst TEXT_COLLATOR = new Intl.Collator(undefined, { numeric: true });\n\n/**\n * Read a property off a row without asserting the row's shape.\n *\n * `Filter.field` is a string chosen by the app's field list, so it cannot be a\n * `keyof T`; the lookup goes through an index signature and a missing property\n * yields `undefined`, which the operators treat as an empty value.\n *\n * @param item - The row being tested.\n * @param field - Property name from the filter.\n * @returns The property value, or `undefined` when the row has no such key.\n */\nfunction readField(item: unknown, field: string): unknown {\n if (item == null || typeof item !== \"object\") return undefined;\n return (item as Record<string, unknown>)[field];\n}\n\n/**\n * Local `yyyy-mm-dd` for a value that represents a date, or `null`.\n *\n * Delegates the formatting to {@link formatDateForInput}, which owns the rule\n * that the key is built from the local calendar parts rather than `toISOString`.\n * This adds only the narrowing from `unknown` and maps \"not a date\" to `null`\n * instead of to the empty string a date input wants.\n *\n * @param value - A `Date`, a date-ish string, or anything else.\n * @returns The day key, or `null` when the value does not denote a date.\n */\nfunction dayKey(value: unknown): string | null {\n if (value instanceof Date) return formatDateForInput(value) || null;\n if (typeof value !== \"string\" || value.trim() === \"\") return null;\n return formatDateForInput(value) || null;\n}\n\n/**\n * Compare a row value against a filter value, in the row value's own type.\n *\n * The filter always carries strings — it comes from form inputs and from the\n * URL — while the row carries whatever the API returned. Comparing them as text\n * gets the two common cases wrong: `10` would sort before `9`, and a date would\n * be compared by however it happens to be formatted. So the row value picks the\n * comparison, and the filter value is coerced into it.\n *\n * Dates compare by **day**, not by instant: a row stamped `2026-03-05T13:00:00Z`\n * is `>= 2026-03-05`, which is what the person picking a date in the UI means.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Negative, zero or positive, or `null` when the two cannot be compared.\n */\nfunction compare(left: unknown, right: string): number | null {\n if (left == null) return null;\n\n if (typeof left === \"number\") {\n const parsed = Number(right);\n return Number.isNaN(parsed) ? null : left - parsed;\n }\n\n if (typeof left === \"boolean\") {\n const parsed = right === \"true\" ? true : right === \"false\" ? false : null;\n return parsed === null ? null : Number(left) - Number(parsed);\n }\n\n if (DATE_ONLY.test(right)) {\n const key = dayKey(left);\n return key === null ? null : key.localeCompare(right);\n }\n\n return TEXT_COLLATOR.compare(String(left), right);\n}\n\n/**\n * Equality in the row value's type, falling back to exact text.\n *\n * Case-sensitive on purpose: `eq` maps to a plain `WHERE column = value` on the\n * server side, so a case-insensitive client would quietly disagree with the\n * backend for the same filter. `contains` is the case-insensitive operator, and\n * it is also the default one for text fields, so the friendly behaviour is what\n * people get without asking.\n *\n * Identical strings take a fast path out, since the row value is text in the\n * common case and the collator is the expensive part.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Whether the two are equal.\n */\nfunction equals(left: unknown, right: string): boolean {\n if (left === right) return true;\n const result = compare(left, right);\n if (result !== null) return result === 0;\n return String(left ?? \"\") === right;\n}\n\n/**\n * Is this row value empty?\n *\n * Empty means absent, blank text or an empty list — never falsy. `0` and `false`\n * are values somebody chose, and a filter that hid them would be reporting the\n * wrong count on every dashboard that tracks zeroes.\n *\n * @param value - The row value.\n * @returns Whether the value counts as empty.\n */\nfunction isEmptyValue(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * The filter's value as a list, whatever arity the operator uses.\n *\n * @param filter - The filter to read.\n * @returns Its values, trimmed of nothing — a leading space may be meaningful text.\n */\nfunction valuesOf(filter: Filter): string[] {\n if (filter.value === undefined) return [];\n return Array.isArray(filter.value) ? [...filter.value] : [String(filter.value)];\n}\n\n/**\n * Evaluate one operator against one row value.\n *\n * @param operator - The comparison to make.\n * @param left - The row value.\n * @param values - The filter values (one, two for `between`, many for `in`).\n * @returns Whether the row value satisfies the operator.\n */\nfunction matchesOperator(operator: FilterOperator, left: unknown, values: string[]): boolean {\n switch (operator) {\n case \"empty\":\n return isEmptyValue(left);\n case \"notEmpty\":\n return !isEmptyValue(left);\n case \"eq\":\n return equals(left, values[0] ?? \"\");\n case \"ne\":\n return !equals(left, values[0] ?? \"\");\n case \"contains\":\n return String(left ?? \"\")\n .toLowerCase()\n .includes((values[0] ?? \"\").toLowerCase());\n case \"in\":\n return values.some((value) => equals(left, value));\n case \"between\": {\n const [first = \"\", second = \"\"] = values;\n const low = compare(left, first);\n const high = compare(left, second);\n if (low === null || high === null) return false;\n return low >= 0 && high <= 0;\n }\n default: {\n const result = compare(left, values[0] ?? \"\");\n if (result === null) return false;\n if (operator === \"gt\") return result > 0;\n if (operator === \"gte\") return result >= 0;\n if (operator === \"lt\") return result < 0;\n return result <= 0;\n }\n }\n}\n\n/**\n * Order the two values of a `between` so the range is never empty by accident.\n *\n * A person who picks the later date first means the range between the two dates,\n * not \"no rows\". The server-side `BETWEEN` needs `(lo, hi)` in order too, which\n * is why the normalisation lives here and is shared by `filtersToQueryParams`.\n *\n * @param values - The raw pair.\n * @returns The pair, ascending.\n */\nexport function orderRange(values: readonly string[]): string[] {\n const [first = \"\", second = \"\"] = values;\n if (DATE_ONLY.test(first) && DATE_ONLY.test(second)) {\n return first <= second ? [first, second] : [second, first];\n }\n const low = Number(first);\n const high = Number(second);\n if (!Number.isNaN(low) && !Number.isNaN(high)) {\n return low <= high ? [first, second] : [second, first];\n }\n return TEXT_COLLATOR.compare(first, second) <= 0 ? [first, second] : [second, first];\n}\n\n/**\n * Run a filter set over an in-memory list.\n *\n * Closes the loop `FilterBar` opens: the bar produces `Filter[]`, this applies\n * them. Filters combine with `AND`, matching the flat model the bar builds, and\n * an incomplete filter is skipped rather than treated as a match of nothing — a\n * half-filled form should not empty the table underneath it.\n *\n * Comparison follows the row's type, not the filter's: numbers compare\n * numerically, dates compare by day, and everything else compares as text with\n * `numeric: true` so `\"item 2\"` lands before `\"item 10\"`.\n *\n * A few behaviours differ from the SQL the server-side twin produces, and the\n * difference is deliberate rather than accidental:\n *\n * - `ne` matches rows whose value is absent. In SQL, `column <> 'x'` is `NULL`\n * for a `NULL` column and the row drops out. Here \"is not paid\" shows the rows\n * with no status at all, which is what the chip claims.\n * - `empty` matches `NULL`, blank text and empty lists; `__isnull` on the server\n * only matches `NULL`. A column that stores `\"\"` instead of `NULL` is where\n * the two disagree.\n *\n * @example\n * const visible = applyFilters(orders, filters);\n *\n * Each filter's values are normalised once, before the scan, rather than inside\n * the per-row predicate: the arity, the `between` ordering and the value array\n * depend only on the filter, so deriving them per row multiplied that work by\n * the row count. `empty`/`notEmpty` ignore their values entirely, which is why\n * the prepared shape does not need to distinguish them.\n *\n * @param items - The full list.\n * @param filters - Applied filters; incomplete ones are ignored.\n * @returns A new array with the rows that satisfy every complete filter.\n */\nexport function applyFilters<T>(items: readonly T[], filters: readonly Filter[]): T[] {\n const active = filters.filter(isComplete).map((filter) => ({\n field: filter.field,\n operator: filter.operator,\n values: filter.operator === \"between\" ? orderRange(valuesOf(filter)) : valuesOf(filter),\n }));\n if (active.length === 0) return [...items];\n\n return items.filter((item) =>\n active.every(({ field, operator, values }) =>\n matchesOperator(operator, readField(item, field), values),\n ),\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("./crop-geometry.cjs"),n=require("./ImageCropper.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=12,o=.15;function s({src:s,aspect:c=1,maxZoom:l=4,maxSize:u,outputType:d=`image/png`,outputQuality:f=.92,shape:p=`rect`,onCropChange:m,label:h=`Área de recorte`,className:g,ref:_,...v}){let y=`${(0,r.useId)()}-hint`,b=(0,r.useRef)(null),x=(0,r.useRef)(null),S=(0,r.useRef)(null),[C,w]=(0,r.useState)(null),[T,E]=(0,r.useState)(null),[D,O]=(0,r.useState)({width:0,height:0}),[k,A]=(0,r.useState)(1),[j,M]=(0,r.useState)({x:0,y:0});(0,r.useEffect)(()=>{if(typeof s==`string`){w(s);return}let e=URL.createObjectURL(s);return w(e),()=>URL.revokeObjectURL(e)},[s]),(0,r.useEffect)(()=>{E(null),A(1),M({x:0,y:0})},[C]),(0,r.useEffect)(()=>{let e=b.current;if(!e)return;let t=()=>O({width:e.clientWidth,height:e.clientHeight});if(t(),typeof ResizeObserver>`u`)return;let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[]);let N=T?t.coverScale(T,D)*k:0,P=(0,r.useMemo)(()=>T?{width:T.width*N,height:T.height*N}:{width:0,height:0},[T,N]),F=(0,r.useCallback)(e=>{M(n=>{let r=t.clampOffset(e,P,D);return r.x===n.x&&r.y===n.y?n:(m?.({zoom:k,offset:r}),r)})},[P,D,m,k]),I=(0,r.useCallback)(e=>{let n=Math.min(l,Math.max(1,e));if(A(n),!T)return;let r=t.coverScale(T,D)*n,i={width:T.width*r,height:T.height*r};M(e=>{let r=t.clampOffset(e,i,D);return m?.({zoom:n,offset:r}),r})},[D,l,T,m]),L=(0,r.useCallback)(()=>{A(1),M({x:0,y:0}),m?.({zoom:1,offset:{x:0,y:0}})},[m]),R=(0,r.useCallback)(async()=>{let e=x.current;if(!e||!T)return null;let n=t.computeCropRect({image:T,frame:D,zoom:k,offset:j});if(n.sWidth<=0||n.sHeight<=0)return null;let r=t.outputSize(n,u),i=document.createElement(`canvas`);i.width=r.width,i.height=r.height;let a=i.getContext(`2d`);return a?(a.drawImage(e,n.sx,n.sy,n.sWidth,n.sHeight,0,0,r.width,r.height),new Promise(e=>{i.toBlob(t=>e(t),d,f)})):null},[D,u,T,j,f,d,k]);(0,r.useImperativeHandle)(_,()=>({crop:R,reset:L}),[R,L]);let z=e=>{T&&(S.current={pointerId:e.pointerId,startX:e.clientX,startY:e.clientY,from:j},e.currentTarget.setPointerCapture?.(e.pointerId))},B=e=>{let t=S.current;!t||t.pointerId!==e.pointerId||F({x:t.from.x+(e.clientX-t.startX),y:t.from.y+(e.clientY-t.startY)})},V=e=>{S.current?.pointerId===e.pointerId&&(S.current=null)},H=e=>{let t=e.shiftKey?48:a,n={ArrowLeft:{x:-t,y:0},ArrowRight:{x:t,y:0},ArrowUp:{x:0,y:-t},ArrowDown:{x:0,y:t}}[e.key];if(n){e.preventDefault(),F({x:j.x+n.x,y:j.y+n.y});return}e.key===`+`||e.key===`=`?(e.preventDefault(),I(k+o)):e.key===`-`||e.key===`_`?(e.preventDefault(),I(k-o)):e.key===`0`&&(e.preventDefault(),L())};return(0,i.jsxs)(`div`,{className:e.cn(n.default.wrapper,g),...v,children:[(0,i.jsx)(`div`,{ref:b,className:e.cn(n.default.frame,p===`circle`&&n.default.circle),style:{aspectRatio:String(c)},role:`group`,"aria-label":h,"aria-describedby":y,tabIndex:0,onPointerDown:z,onPointerMove:B,onPointerUp:V,onPointerCancel:V,onKeyDown:H,onWheel:e=>{e.preventDefault(),I(k+(e.deltaY<0?o:-.15))},children:C&&(0,i.jsx)(`img`,{ref:x,src:C,alt:``,draggable:!1,className:n.default.image,style:{width:P.width||void 0,height:P.height||void 0,transform:`translate(${j.x}px, ${j.y}px)`},onLoad:e=>{let t=e.currentTarget;t.naturalWidth>0&&t.naturalHeight>0&&E({width:t.naturalWidth,height:t.naturalHeight})}})}),(0,i.jsxs)(`div`,{className:n.default.controls,children:[(0,i.jsx)(`input`,{type:`range`,className:n.default.zoom,min:1,max:l,step:.01,value:k,onChange:e=>I(Number(e.target.value)),"aria-label":`Zoom`,disabled:!T}),(0,i.jsx)(`button`,{type:`button`,className:n.default.reset,onClick:L,disabled:!T,children:`Centralizar`})]}),(0,i.jsxs)(`p`,{id:y,className:n.default.hint,children:[`Arraste para reposicionar. Setas movem, `,(0,i.jsx)(`kbd`,{children:`+`}),` e `,(0,i.jsx)(`kbd`,{children:`−`}),` dão zoom,`,` `,(0,i.jsx)(`kbd`,{children:`0`}),` centraliza.`]})]})}exports.ImageCropper=s;
1
+ const e=require("../../utils/cn.cjs"),t=require("./crop-geometry.cjs"),n=require("./ImageCropper.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=12,o=.15;function s({src:s,aspect:c=1,maxZoom:l=4,maxSize:u,outputType:d=`image/png`,outputQuality:f=.92,shape:p=`rect`,onCropChange:m,label:h=`Área de recorte`,className:g,ref:_,...v}){let y=`${(0,r.useId)()}-hint`,b=(0,r.useRef)(null),x=(0,r.useRef)(null),S=(0,r.useRef)(null),[C,w]=(0,r.useState)(null),[T,E]=(0,r.useState)(null),[D,O]=(0,r.useState)({width:0,height:0}),[k,A]=(0,r.useState)(1),[j,M]=(0,r.useState)({x:0,y:0});(0,r.useEffect)(()=>{if(typeof s==`string`){w(s);return}let e=URL.createObjectURL(s);return w(e),()=>URL.revokeObjectURL(e)},[s]),(0,r.useEffect)(()=>{E(null),A(1),M({x:0,y:0})},[C]),(0,r.useEffect)(()=>{let e=b.current;if(!e)return;let t=()=>O({width:e.clientWidth,height:e.clientHeight});if(t(),typeof ResizeObserver>`u`)return;let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[]);let N=T?t.coverScale(T,D)*k:0,P=(0,r.useMemo)(()=>T?{width:T.width*N,height:T.height*N}:{width:0,height:0},[T,N]),F=(0,r.useCallback)(e=>{M(n=>{let r=t.clampOffset(e,P,D);return r.x===n.x&&r.y===n.y?n:(m?.({zoom:k,offset:r}),r)})},[P,D,m,k]),I=(0,r.useCallback)(e=>{let n=Math.min(l,Math.max(1,e));if(A(n),!T)return;let r=t.coverScale(T,D)*n,i={width:T.width*r,height:T.height*r};M(e=>{let r=t.clampOffset(e,i,D);return m?.({zoom:n,offset:r}),r})},[D,l,T,m]),L=(0,r.useCallback)(()=>{A(1),M({x:0,y:0}),m?.({zoom:1,offset:{x:0,y:0}})},[m]),R=(0,r.useCallback)(async()=>{let e=x.current;if(!e||!T)return null;let n=t.computeCropRect({image:T,frame:D,zoom:k,offset:j});if(n.sWidth<=0||n.sHeight<=0)return null;let r=t.outputSize(n,u),i=document.createElement(`canvas`);i.width=r.width,i.height=r.height;let a=i.getContext(`2d`);return a?(a.drawImage(e,n.sx,n.sy,n.sWidth,n.sHeight,0,0,r.width,r.height),new Promise(e=>{i.toBlob(t=>e(t),d,f)})):null},[D,u,T,j,f,d,k]);(0,r.useImperativeHandle)(_,()=>({crop:R,reset:L}),[R,L]);let z=e=>{T&&(S.current={pointerId:e.pointerId,startX:e.clientX,startY:e.clientY,from:j},e.currentTarget.setPointerCapture?.(e.pointerId))},B=e=>{let t=S.current;t&&t.pointerId===e.pointerId&&F({x:t.from.x+(e.clientX-t.startX),y:t.from.y+(e.clientY-t.startY)})},V=e=>{S.current?.pointerId===e.pointerId&&(S.current=null)},H=e=>{let t=e.shiftKey?48:a,n={ArrowLeft:{x:-t,y:0},ArrowRight:{x:t,y:0},ArrowUp:{x:0,y:-t},ArrowDown:{x:0,y:t}}[e.key];if(n){e.preventDefault(),F({x:j.x+n.x,y:j.y+n.y});return}e.key===`+`||e.key===`=`?(e.preventDefault(),I(k+o)):e.key===`-`||e.key===`_`?(e.preventDefault(),I(k-o)):e.key===`0`&&(e.preventDefault(),L())};return(0,i.jsxs)(`div`,{className:e.cn(n.default.wrapper,g),...v,children:[(0,i.jsx)(`div`,{ref:b,className:e.cn(n.default.frame,p===`circle`&&n.default.circle),style:{aspectRatio:String(c)},role:`group`,"aria-label":h,"aria-describedby":y,tabIndex:0,onPointerDown:z,onPointerMove:B,onPointerUp:V,onPointerCancel:V,onKeyDown:H,onWheel:e=>{e.preventDefault(),I(k+(e.deltaY<0?o:-.15))},children:C&&(0,i.jsx)(`img`,{ref:x,src:C,alt:``,draggable:!1,className:n.default.image,style:{width:P.width||void 0,height:P.height||void 0,transform:`translate(${j.x}px, ${j.y}px)`},onLoad:e=>{let t=e.currentTarget;t.naturalWidth>0&&t.naturalHeight>0&&E({width:t.naturalWidth,height:t.naturalHeight})}})}),(0,i.jsxs)(`div`,{className:n.default.controls,children:[(0,i.jsx)(`input`,{type:`range`,className:n.default.zoom,min:1,max:l,step:.01,value:k,onChange:e=>I(Number(e.target.value)),"aria-label":`Zoom`,disabled:!T}),(0,i.jsx)(`button`,{type:`button`,className:n.default.reset,onClick:L,disabled:!T,children:`Centralizar`})]}),(0,i.jsxs)(`p`,{id:y,className:n.default.hint,children:[`Arraste para reposicionar. Setas movem, `,(0,i.jsx)(`kbd`,{children:`+`}),` e `,(0,i.jsx)(`kbd`,{children:`−`}),` dão zoom,`,` `,(0,i.jsx)(`kbd`,{children:`0`}),` centraliza.`]})]})}exports.ImageCropper=s;
2
2
  //# sourceMappingURL=ImageCropper.cjs.map