tempest-react-sdk 0.38.2 → 0.38.3

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 (291) hide show
  1. package/bin/lib/design/collect.mjs +19 -0
  2. package/bin/lib/design/functions.mjs +5 -0
  3. package/bin/lib/design/index.mjs +8 -3
  4. package/bin/lib/design/index.test.mjs +31 -0
  5. package/bin/lib/design/scan.mjs +34 -2
  6. package/bin/lib/design/scan.test.mjs +78 -0
  7. package/bin/lib/doctor/doctor.e2e.test.mjs +85 -0
  8. package/bin/tempest.mjs +90 -6
  9. package/dist/audio/level-meter.cjs.map +1 -1
  10. package/dist/audio/level-meter.js.map +1 -1
  11. package/dist/audio/use-audio-recorder.cjs.map +1 -1
  12. package/dist/audio/use-audio-recorder.js.map +1 -1
  13. package/dist/auth/passkey.cjs.map +1 -1
  14. package/dist/auth/passkey.js.map +1 -1
  15. package/dist/auth/use-passkey.cjs.map +1 -1
  16. package/dist/auth/use-passkey.js.map +1 -1
  17. package/dist/br/BrazilMap.cjs.map +1 -1
  18. package/dist/br/BrazilMap.js.map +1 -1
  19. package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
  20. package/dist/br/BrazilStateCitySelect.js.map +1 -1
  21. package/dist/br/BrazilStateMap.cjs.map +1 -1
  22. package/dist/br/BrazilStateMap.js.map +1 -1
  23. package/dist/br/PixQRCode.cjs.map +1 -1
  24. package/dist/br/PixQRCode.js.map +1 -1
  25. package/dist/br/boleto.cjs.map +1 -1
  26. package/dist/br/boleto.js.map +1 -1
  27. package/dist/br/pix.cjs.map +1 -1
  28. package/dist/br/pix.js.map +1 -1
  29. package/dist/br.d.ts +8 -0
  30. package/dist/capture/media-recorder.cjs.map +1 -1
  31. package/dist/capture/media-recorder.js.map +1 -1
  32. package/dist/capture/use-barcode-scanner.cjs.map +1 -1
  33. package/dist/capture/use-barcode-scanner.js.map +1 -1
  34. package/dist/capture/use-screen-capture.cjs.map +1 -1
  35. package/dist/capture/use-screen-capture.js.map +1 -1
  36. package/dist/capture/use-speech-recognition.cjs.map +1 -1
  37. package/dist/capture/use-speech-recognition.js.map +1 -1
  38. package/dist/capture/use-video-recorder.cjs.map +1 -1
  39. package/dist/capture/use-video-recorder.js.map +1 -1
  40. package/dist/charts/types.cjs.map +1 -1
  41. package/dist/charts/types.js.map +1 -1
  42. package/dist/charts.d.ts +8 -0
  43. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  44. package/dist/components/AIChat/AIChat.js.map +1 -1
  45. package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
  46. package/dist/components/AIChat/AIChatComposer.js.map +1 -1
  47. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  48. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  49. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  50. package/dist/components/AppBar/AppBar.js.map +1 -1
  51. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  52. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
  53. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
  54. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
  55. package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
  56. package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
  57. package/dist/components/Button/Button.cjs.map +1 -1
  58. package/dist/components/Button/Button.js.map +1 -1
  59. package/dist/components/Calendar/Calendar.cjs.map +1 -1
  60. package/dist/components/Calendar/Calendar.js.map +1 -1
  61. package/dist/components/Carousel/Carousel.cjs.map +1 -1
  62. package/dist/components/Carousel/Carousel.js.map +1 -1
  63. package/dist/components/Chat/Chat.cjs.map +1 -1
  64. package/dist/components/Chat/Chat.js.map +1 -1
  65. package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
  66. package/dist/components/ChipInput/ChipInput.js.map +1 -1
  67. package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
  68. package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
  69. package/dist/components/Combobox/Combobox.cjs.map +1 -1
  70. package/dist/components/Combobox/Combobox.js.map +1 -1
  71. package/dist/components/Command/Command.cjs.map +1 -1
  72. package/dist/components/Command/Command.js.map +1 -1
  73. package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
  74. package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
  75. package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
  76. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  77. package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
  78. package/dist/components/CopyButton/CopyButton.js.map +1 -1
  79. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  80. package/dist/components/DataTable/DataTable.js.map +1 -1
  81. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  82. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  83. package/dist/components/DataTable/EditableCell.cjs.map +1 -1
  84. package/dist/components/DataTable/EditableCell.js.map +1 -1
  85. package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
  86. package/dist/components/DatePicker/DatePicker.js.map +1 -1
  87. package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
  88. package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
  89. package/dist/components/Drawer/Drawer.cjs.map +1 -1
  90. package/dist/components/Drawer/Drawer.js.map +1 -1
  91. package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
  92. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  93. package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
  94. package/dist/components/Dropzone/Dropzone.js.map +1 -1
  95. package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
  96. package/dist/components/FileUpload/FileUpload.js.map +1 -1
  97. package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
  98. package/dist/components/FilterBar/FilterBar.js.map +1 -1
  99. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
  100. package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
  101. package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
  102. package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
  103. package/dist/components/Kanban/Kanban.cjs.map +1 -1
  104. package/dist/components/Kanban/Kanban.js.map +1 -1
  105. package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
  106. package/dist/components/Lightbox/Lightbox.js.map +1 -1
  107. package/dist/components/ListTile/ListTile.cjs.map +1 -1
  108. package/dist/components/ListTile/ListTile.js.map +1 -1
  109. package/dist/components/Markdown/Markdown.cjs.map +1 -1
  110. package/dist/components/Markdown/Markdown.js.map +1 -1
  111. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  112. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  113. package/dist/components/Menubar/Menubar.cjs.map +1 -1
  114. package/dist/components/Menubar/Menubar.js.map +1 -1
  115. package/dist/components/Modal/Modal.cjs.map +1 -1
  116. package/dist/components/Modal/Modal.js.map +1 -1
  117. package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
  118. package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
  119. package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
  120. package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
  121. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  122. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  123. package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
  124. package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
  125. package/dist/components/Page/Page.cjs.map +1 -1
  126. package/dist/components/Page/Page.js.map +1 -1
  127. package/dist/components/Pagination/Pagination.cjs.map +1 -1
  128. package/dist/components/Pagination/Pagination.js.map +1 -1
  129. package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
  130. package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
  131. package/dist/components/PinInput/PinInput.cjs.map +1 -1
  132. package/dist/components/PinInput/PinInput.js.map +1 -1
  133. package/dist/components/Popover/Popover.cjs.map +1 -1
  134. package/dist/components/Popover/Popover.js.map +1 -1
  135. package/dist/components/Progress/Progress.cjs.map +1 -1
  136. package/dist/components/Progress/Progress.js.map +1 -1
  137. package/dist/components/QRCode/qr-encode.cjs.map +1 -1
  138. package/dist/components/QRCode/qr-encode.js.map +1 -1
  139. package/dist/components/Radio/Radio.cjs.map +1 -1
  140. package/dist/components/Radio/Radio.js.map +1 -1
  141. package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
  142. package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
  143. package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
  144. package/dist/components/RatingStars/RatingStars.js.map +1 -1
  145. package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
  146. package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
  147. package/dist/components/Resizable/Resizable.cjs.map +1 -1
  148. package/dist/components/Resizable/Resizable.js.map +1 -1
  149. package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
  150. package/dist/components/Scheduler/Scheduler.js.map +1 -1
  151. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  152. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  153. package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
  154. package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
  155. package/dist/components/Slider/Slider.cjs.map +1 -1
  156. package/dist/components/Slider/Slider.js.map +1 -1
  157. package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
  158. package/dist/components/Sparkline/Sparkline.js.map +1 -1
  159. package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
  160. package/dist/components/StepperInput/StepperInput.js.map +1 -1
  161. package/dist/components/Table/Table.cjs.map +1 -1
  162. package/dist/components/Table/Table.js.map +1 -1
  163. package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
  164. package/dist/components/TimePicker/TimePicker.js.map +1 -1
  165. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  166. package/dist/components/Toast/ToastProvider.js.map +1 -1
  167. package/dist/components/Tour/Tour.cjs.map +1 -1
  168. package/dist/components/Tour/Tour.js.map +1 -1
  169. package/dist/components/Transfer/Transfer.cjs.map +1 -1
  170. package/dist/components/Transfer/Transfer.js.map +1 -1
  171. package/dist/components/TreeView/TreeView.cjs.map +1 -1
  172. package/dist/components/TreeView/TreeView.js.map +1 -1
  173. package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
  174. package/dist/components/VirtualList/VirtualList.js.map +1 -1
  175. package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
  176. package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
  177. package/dist/components/Wizard/Wizard.cjs.map +1 -1
  178. package/dist/components/Wizard/Wizard.js.map +1 -1
  179. package/dist/editor/RichTextEditor.cjs.map +1 -1
  180. package/dist/editor/RichTextEditor.js.map +1 -1
  181. package/dist/forms/FormField.cjs.map +1 -1
  182. package/dist/forms/FormField.js.map +1 -1
  183. package/dist/geo/TrajectoryMap.cjs.map +1 -1
  184. package/dist/geo/TrajectoryMap.js.map +1 -1
  185. package/dist/geo/estimate.cjs.map +1 -1
  186. package/dist/geo/estimate.js.map +1 -1
  187. package/dist/geo/projection.cjs.map +1 -1
  188. package/dist/geo/projection.js.map +1 -1
  189. package/dist/hooks/use-event-listener.cjs.map +1 -1
  190. package/dist/hooks/use-event-listener.js.map +1 -1
  191. package/dist/hooks/use-local-storage.cjs.map +1 -1
  192. package/dist/hooks/use-local-storage.js.map +1 -1
  193. package/dist/hooks/use-sortable.cjs.map +1 -1
  194. package/dist/hooks/use-sortable.js.map +1 -1
  195. package/dist/http/api-client.cjs.map +1 -1
  196. package/dist/http/api-client.js.map +1 -1
  197. package/dist/http/errors.cjs.map +1 -1
  198. package/dist/http/errors.js.map +1 -1
  199. package/dist/http/resumable-upload.cjs.map +1 -1
  200. package/dist/http/resumable-upload.js.map +1 -1
  201. package/dist/http/upload-with-progress.cjs.map +1 -1
  202. package/dist/http/upload-with-progress.js.map +1 -1
  203. package/dist/i18n/I18nProvider.cjs.map +1 -1
  204. package/dist/i18n/I18nProvider.js.map +1 -1
  205. package/dist/imaging/canvas.cjs.map +1 -1
  206. package/dist/imaging/canvas.js.map +1 -1
  207. package/dist/imaging.d.ts +5 -0
  208. package/dist/logger/logger.cjs.map +1 -1
  209. package/dist/logger/logger.js.map +1 -1
  210. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  211. package/dist/oauth/GoogleSignIn.js.map +1 -1
  212. package/dist/offline/create-offline-sync.cjs.map +1 -1
  213. package/dist/offline/create-offline-sync.js.map +1 -1
  214. package/dist/perf/cache-size.cjs +1 -1
  215. package/dist/perf/cache-size.cjs.map +1 -1
  216. package/dist/perf/cache-size.js +27 -6
  217. package/dist/perf/cache-size.js.map +1 -1
  218. package/dist/sse/create-event-stream.cjs.map +1 -1
  219. package/dist/sse/create-event-stream.js.map +1 -1
  220. package/dist/styles.css +1 -1
  221. package/dist/sw/background-sync.cjs.map +1 -1
  222. package/dist/sw/background-sync.js.map +1 -1
  223. package/dist/sw/cache.cjs.map +1 -1
  224. package/dist/sw/cache.js.map +1 -1
  225. package/dist/sw.d.ts +12 -0
  226. package/dist/tabular/compact.cjs.map +1 -1
  227. package/dist/tabular/compact.js.map +1 -1
  228. package/dist/tabular/predictor.cjs.map +1 -1
  229. package/dist/tabular/predictor.js.map +1 -1
  230. package/dist/tabular.d.ts +7 -2
  231. package/dist/tempest-react-sdk.d.ts +133 -6
  232. package/dist/theme/ThemeProvider.cjs.map +1 -1
  233. package/dist/theme/ThemeProvider.js.map +1 -1
  234. package/dist/theme/color.cjs.map +1 -1
  235. package/dist/theme/color.js.map +1 -1
  236. package/dist/theme/create-theme.cjs.map +1 -1
  237. package/dist/theme/create-theme.js.map +1 -1
  238. package/dist/theme/data-viz-ramps.cjs +1 -1
  239. package/dist/theme/data-viz-ramps.cjs.map +1 -1
  240. package/dist/theme/data-viz-ramps.js +1 -1
  241. package/dist/theme/data-viz-ramps.js.map +1 -1
  242. package/dist/utils/storage.cjs.map +1 -1
  243. package/dist/utils/storage.js.map +1 -1
  244. package/dist/vision/core/canvas.cjs.map +1 -1
  245. package/dist/vision/core/canvas.js.map +1 -1
  246. package/dist/vision/core/exceptions.cjs.map +1 -1
  247. package/dist/vision/core/exceptions.js.map +1 -1
  248. package/dist/vision/core/graph.cjs.map +1 -1
  249. package/dist/vision/core/graph.js.map +1 -1
  250. package/dist/vision/core/metadata.cjs.map +1 -1
  251. package/dist/vision/core/metadata.js.map +1 -1
  252. package/dist/vision/core/providers.cjs.map +1 -1
  253. package/dist/vision/core/providers.js.map +1 -1
  254. package/dist/vision/core/session.cjs.map +1 -1
  255. package/dist/vision/core/session.js.map +1 -1
  256. package/dist/vision/core/timing.cjs.map +1 -1
  257. package/dist/vision/core/timing.js.map +1 -1
  258. package/dist/vision/io/image.cjs.map +1 -1
  259. package/dist/vision/io/image.js.map +1 -1
  260. package/dist/vision/labels.cjs.map +1 -1
  261. package/dist/vision/labels.js.map +1 -1
  262. package/dist/vision/luminance.cjs.map +1 -1
  263. package/dist/vision/luminance.js.map +1 -1
  264. package/dist/vision/postprocess/classification.cjs.map +1 -1
  265. package/dist/vision/postprocess/classification.js.map +1 -1
  266. package/dist/vision/postprocess/detection.cjs.map +1 -1
  267. package/dist/vision/postprocess/detection.js.map +1 -1
  268. package/dist/vision/postprocess/segmentation.cjs.map +1 -1
  269. package/dist/vision/postprocess/segmentation.js.map +1 -1
  270. package/dist/vision/preprocess/image.cjs.map +1 -1
  271. package/dist/vision/preprocess/image.js.map +1 -1
  272. package/dist/vision/results.cjs.map +1 -1
  273. package/dist/vision/results.js.map +1 -1
  274. package/dist/vision/tasks/base.cjs.map +1 -1
  275. package/dist/vision/tasks/base.js.map +1 -1
  276. package/dist/vision/tasks/classifier.cjs.map +1 -1
  277. package/dist/vision/tasks/classifier.js.map +1 -1
  278. package/dist/vision/tasks/detector.cjs.map +1 -1
  279. package/dist/vision/tasks/detector.js.map +1 -1
  280. package/dist/vision/tasks/segmenter.cjs.map +1 -1
  281. package/dist/vision/tasks/segmenter.js.map +1 -1
  282. package/dist/vision/types.cjs.map +1 -1
  283. package/dist/vision/types.js.map +1 -1
  284. package/dist/vision.d.ts +6 -0
  285. package/dist/vite/tempest-icons.cjs.map +1 -1
  286. package/dist/vite/tempest-icons.js.map +1 -1
  287. package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
  288. package/dist/vite/tempest-pwa-icons.js.map +1 -1
  289. package/dist/ws/create-web-socket.cjs.map +1 -1
  290. package/dist/ws/create-web-socket.js.map +1 -1
  291. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TimePicker.cjs","names":[],"sources":["../../../src/components/TimePicker/TimePicker.tsx"],"sourcesContent":["import { useId, useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./TimePicker.module.css\";\n\nexport interface TimePickerProps {\n /** Selected time as a 24h `\"HH:MM\"` string (e.g. `\"14:30\"`). May be `\"\"` for none. */\n value: string;\n /** Fired with the new 24h `\"HH:MM\"` string whenever a cell is picked. */\n onChange: (value: string) => void;\n /** Granularity of the minute column. Defaults to `5`. */\n minuteStep?: number;\n /** When true, show a 1–12 hour column plus an AM/PM column (still emits 24h). */\n use12Hours?: boolean;\n label?: string;\n helperText?: string;\n disabled?: boolean;\n className?: string;\n}\n\ntype Period = \"AM\" | \"PM\";\n\ninterface ParsedTime {\n hour24: number | null;\n minute: number | null;\n}\n\nfunction pad2(value: number): string {\n return value.toString().padStart(2, \"0\");\n}\n\n/** Parse a 24h `\"HH:MM\"` string into hour/minute numbers, or nulls when empty/invalid. */\nfunction parseValue(value: string): ParsedTime {\n const match = /^(\\d{1,2}):(\\d{1,2})$/.exec(value.trim());\n if (!match) return { hour24: null, minute: null };\n const hour24 = Number(match[1]);\n const minute = Number(match[2]);\n if (hour24 < 0 || hour24 > 23 || minute < 0 || minute > 59) {\n return { hour24: null, minute: null };\n }\n return { hour24, minute };\n}\n\n/** Convert a 24h hour into its 12h display hour (1–12). */\nfunction to12Hour(hour24: number): number {\n const mod = hour24 % 12;\n return mod === 0 ? 12 : mod;\n}\n\n/** Convert a 12h display hour + period into a 24h hour (0–23). */\nfunction to24Hour(hour12: number, period: Period): number {\n const base = hour12 % 12;\n return period === \"PM\" ? base + 12 : base;\n}\n\n/**\n * TimePicker — inline, dependency-free time picker with Material \"spinner column\" styling.\n *\n * Renders scrollable columns of selectable cells (hours, minutes, and AM/PM when\n * `use12Hours`). Always emits a 24h `\"HH:MM\"` string via `onChange`, regardless of\n * whether the 12h display is enabled.\n */\nexport function TimePicker({\n value,\n onChange,\n minuteStep = 5,\n use12Hours = false,\n label,\n helperText,\n disabled,\n className,\n}: TimePickerProps) {\n const id = useId();\n const { hour24, minute } = useMemo(() => parseValue(value), [value]);\n\n const period: Period = hour24 !== null && hour24 >= 12 ? \"PM\" : \"AM\";\n\n const hourCells = useMemo<number[]>(() => {\n if (use12Hours) {\n return Array.from({ length: 12 }, (_, index) => index + 1);\n }\n return Array.from({ length: 24 }, (_, index) => index);\n }, [use12Hours]);\n\n const minuteCells = useMemo<number[]>(() => {\n const step = minuteStep > 0 ? minuteStep : 1;\n const cells: number[] = [];\n for (let current = 0; current < 60; current += step) {\n cells.push(current);\n }\n return cells;\n }, [minuteStep]);\n\n const selectedHourDisplay = hour24 === null ? null : use12Hours ? to12Hour(hour24) : hour24;\n\n const emit = (nextHour24: number, nextMinute: number): void => {\n onChange(`${pad2(nextHour24)}:${pad2(nextMinute)}`);\n };\n\n const handleHour = (displayHour: number): void => {\n if (disabled) return;\n const nextMinute = minute ?? 0;\n const nextHour24 = use12Hours ? to24Hour(displayHour, period) : displayHour;\n emit(nextHour24, nextMinute);\n };\n\n const handleMinute = (nextMinute: number): void => {\n if (disabled) return;\n const baseHour24 = hour24 ?? (use12Hours ? to24Hour(12, \"AM\") : 0);\n emit(baseHour24, nextMinute);\n };\n\n const handlePeriod = (nextPeriod: Period): void => {\n if (disabled) return;\n const displayHour = selectedHourDisplay ?? 12;\n const nextMinute = minute ?? 0;\n emit(to24Hour(displayHour, nextPeriod), nextMinute);\n };\n\n return (\n <div className={cn(styles.wrapper, disabled && styles.disabled, className)}>\n {label && (\n <span id={`${id}-label`} className={styles.label}>\n {label}\n </span>\n )}\n <div className={styles.columns}>\n <ul role=\"listbox\" aria-label=\"Horas\" className={styles.column}>\n {hourCells.map((cell) => {\n const active = cell === selectedHourDisplay;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handleHour(cell)}\n >\n {use12Hours ? cell : pad2(cell)}\n </button>\n </li>\n );\n })}\n </ul>\n <ul role=\"listbox\" aria-label=\"Minutos\" className={styles.column}>\n {minuteCells.map((cell) => {\n const active = cell === minute;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handleMinute(cell)}\n >\n {pad2(cell)}\n </button>\n </li>\n );\n })}\n </ul>\n {use12Hours && (\n <ul role=\"listbox\" aria-label=\"Período\" className={styles.column}>\n {([\"AM\", \"PM\"] as Period[]).map((cell) => {\n const active = selectedHourDisplay !== null && cell === period;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handlePeriod(cell)}\n >\n {cell}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n {helperText && <span className={styles.helper}>{helperText}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimePicker.cjs","names":[],"sources":["../../../src/components/TimePicker/TimePicker.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — minuteStep and\n * use12Hours change both the list of options and how a typed value parses, so the\n * body holds parsing, formatting and the option list together; the remaining props\n * are the shared field contract.\n */\nimport { useId, useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./TimePicker.module.css\";\n\nexport interface TimePickerProps {\n /** Selected time as a 24h `\"HH:MM\"` string (e.g. `\"14:30\"`). May be `\"\"` for none. */\n value: string;\n /** Fired with the new 24h `\"HH:MM\"` string whenever a cell is picked. */\n onChange: (value: string) => void;\n /** Granularity of the minute column. Defaults to `5`. */\n minuteStep?: number;\n /** When true, show a 1–12 hour column plus an AM/PM column (still emits 24h). */\n use12Hours?: boolean;\n label?: string;\n helperText?: string;\n disabled?: boolean;\n className?: string;\n}\n\ntype Period = \"AM\" | \"PM\";\n\ninterface ParsedTime {\n hour24: number | null;\n minute: number | null;\n}\n\nfunction pad2(value: number): string {\n return value.toString().padStart(2, \"0\");\n}\n\n/** Parse a 24h `\"HH:MM\"` string into hour/minute numbers, or nulls when empty/invalid. */\nfunction parseValue(value: string): ParsedTime {\n const match = /^(\\d{1,2}):(\\d{1,2})$/.exec(value.trim());\n if (!match) return { hour24: null, minute: null };\n const hour24 = Number(match[1]);\n const minute = Number(match[2]);\n if (hour24 < 0 || hour24 > 23 || minute < 0 || minute > 59) {\n return { hour24: null, minute: null };\n }\n return { hour24, minute };\n}\n\n/** Convert a 24h hour into its 12h display hour (1–12). */\nfunction to12Hour(hour24: number): number {\n const mod = hour24 % 12;\n return mod === 0 ? 12 : mod;\n}\n\n/** Convert a 12h display hour + period into a 24h hour (0–23). */\nfunction to24Hour(hour12: number, period: Period): number {\n const base = hour12 % 12;\n return period === \"PM\" ? base + 12 : base;\n}\n\n/**\n * TimePicker — inline, dependency-free time picker with Material \"spinner column\" styling.\n *\n * Renders scrollable columns of selectable cells (hours, minutes, and AM/PM when\n * `use12Hours`). Always emits a 24h `\"HH:MM\"` string via `onChange`, regardless of\n * whether the 12h display is enabled.\n */\nexport function TimePicker({\n value,\n onChange,\n minuteStep = 5,\n use12Hours = false,\n label,\n helperText,\n disabled,\n className,\n}: TimePickerProps) {\n const id = useId();\n const { hour24, minute } = useMemo(() => parseValue(value), [value]);\n\n const period: Period = hour24 !== null && hour24 >= 12 ? \"PM\" : \"AM\";\n\n const hourCells = useMemo<number[]>(() => {\n if (use12Hours) {\n return Array.from({ length: 12 }, (_, index) => index + 1);\n }\n return Array.from({ length: 24 }, (_, index) => index);\n }, [use12Hours]);\n\n const minuteCells = useMemo<number[]>(() => {\n const step = minuteStep > 0 ? minuteStep : 1;\n const cells: number[] = [];\n for (let current = 0; current < 60; current += step) {\n cells.push(current);\n }\n return cells;\n }, [minuteStep]);\n\n const selectedHourDisplay = hour24 === null ? null : use12Hours ? to12Hour(hour24) : hour24;\n\n const emit = (nextHour24: number, nextMinute: number): void => {\n onChange(`${pad2(nextHour24)}:${pad2(nextMinute)}`);\n };\n\n const handleHour = (displayHour: number): void => {\n if (disabled) return;\n const nextMinute = minute ?? 0;\n const nextHour24 = use12Hours ? to24Hour(displayHour, period) : displayHour;\n emit(nextHour24, nextMinute);\n };\n\n const handleMinute = (nextMinute: number): void => {\n if (disabled) return;\n const baseHour24 = hour24 ?? (use12Hours ? to24Hour(12, \"AM\") : 0);\n emit(baseHour24, nextMinute);\n };\n\n const handlePeriod = (nextPeriod: Period): void => {\n if (disabled) return;\n const displayHour = selectedHourDisplay ?? 12;\n const nextMinute = minute ?? 0;\n emit(to24Hour(displayHour, nextPeriod), nextMinute);\n };\n\n return (\n <div className={cn(styles.wrapper, disabled && styles.disabled, className)}>\n {label && (\n <span id={`${id}-label`} className={styles.label}>\n {label}\n </span>\n )}\n <div className={styles.columns}>\n <ul role=\"listbox\" aria-label=\"Horas\" className={styles.column}>\n {hourCells.map((cell) => {\n const active = cell === selectedHourDisplay;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handleHour(cell)}\n >\n {use12Hours ? cell : pad2(cell)}\n </button>\n </li>\n );\n })}\n </ul>\n <ul role=\"listbox\" aria-label=\"Minutos\" className={styles.column}>\n {minuteCells.map((cell) => {\n const active = cell === minute;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handleMinute(cell)}\n >\n {pad2(cell)}\n </button>\n </li>\n );\n })}\n </ul>\n {use12Hours && (\n <ul role=\"listbox\" aria-label=\"Período\" className={styles.column}>\n {([\"AM\", \"PM\"] as Period[]).map((cell) => {\n const active = selectedHourDisplay !== null && cell === period;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handlePeriod(cell)}\n >\n {cell}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n {helperText && <span className={styles.helper}>{helperText}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TimePicker.js","names":[],"sources":["../../../src/components/TimePicker/TimePicker.tsx"],"sourcesContent":["import { useId, useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./TimePicker.module.css\";\n\nexport interface TimePickerProps {\n /** Selected time as a 24h `\"HH:MM\"` string (e.g. `\"14:30\"`). May be `\"\"` for none. */\n value: string;\n /** Fired with the new 24h `\"HH:MM\"` string whenever a cell is picked. */\n onChange: (value: string) => void;\n /** Granularity of the minute column. Defaults to `5`. */\n minuteStep?: number;\n /** When true, show a 1–12 hour column plus an AM/PM column (still emits 24h). */\n use12Hours?: boolean;\n label?: string;\n helperText?: string;\n disabled?: boolean;\n className?: string;\n}\n\ntype Period = \"AM\" | \"PM\";\n\ninterface ParsedTime {\n hour24: number | null;\n minute: number | null;\n}\n\nfunction pad2(value: number): string {\n return value.toString().padStart(2, \"0\");\n}\n\n/** Parse a 24h `\"HH:MM\"` string into hour/minute numbers, or nulls when empty/invalid. */\nfunction parseValue(value: string): ParsedTime {\n const match = /^(\\d{1,2}):(\\d{1,2})$/.exec(value.trim());\n if (!match) return { hour24: null, minute: null };\n const hour24 = Number(match[1]);\n const minute = Number(match[2]);\n if (hour24 < 0 || hour24 > 23 || minute < 0 || minute > 59) {\n return { hour24: null, minute: null };\n }\n return { hour24, minute };\n}\n\n/** Convert a 24h hour into its 12h display hour (1–12). */\nfunction to12Hour(hour24: number): number {\n const mod = hour24 % 12;\n return mod === 0 ? 12 : mod;\n}\n\n/** Convert a 12h display hour + period into a 24h hour (0–23). */\nfunction to24Hour(hour12: number, period: Period): number {\n const base = hour12 % 12;\n return period === \"PM\" ? base + 12 : base;\n}\n\n/**\n * TimePicker — inline, dependency-free time picker with Material \"spinner column\" styling.\n *\n * Renders scrollable columns of selectable cells (hours, minutes, and AM/PM when\n * `use12Hours`). Always emits a 24h `\"HH:MM\"` string via `onChange`, regardless of\n * whether the 12h display is enabled.\n */\nexport function TimePicker({\n value,\n onChange,\n minuteStep = 5,\n use12Hours = false,\n label,\n helperText,\n disabled,\n className,\n}: TimePickerProps) {\n const id = useId();\n const { hour24, minute } = useMemo(() => parseValue(value), [value]);\n\n const period: Period = hour24 !== null && hour24 >= 12 ? \"PM\" : \"AM\";\n\n const hourCells = useMemo<number[]>(() => {\n if (use12Hours) {\n return Array.from({ length: 12 }, (_, index) => index + 1);\n }\n return Array.from({ length: 24 }, (_, index) => index);\n }, [use12Hours]);\n\n const minuteCells = useMemo<number[]>(() => {\n const step = minuteStep > 0 ? minuteStep : 1;\n const cells: number[] = [];\n for (let current = 0; current < 60; current += step) {\n cells.push(current);\n }\n return cells;\n }, [minuteStep]);\n\n const selectedHourDisplay = hour24 === null ? null : use12Hours ? to12Hour(hour24) : hour24;\n\n const emit = (nextHour24: number, nextMinute: number): void => {\n onChange(`${pad2(nextHour24)}:${pad2(nextMinute)}`);\n };\n\n const handleHour = (displayHour: number): void => {\n if (disabled) return;\n const nextMinute = minute ?? 0;\n const nextHour24 = use12Hours ? to24Hour(displayHour, period) : displayHour;\n emit(nextHour24, nextMinute);\n };\n\n const handleMinute = (nextMinute: number): void => {\n if (disabled) return;\n const baseHour24 = hour24 ?? (use12Hours ? to24Hour(12, \"AM\") : 0);\n emit(baseHour24, nextMinute);\n };\n\n const handlePeriod = (nextPeriod: Period): void => {\n if (disabled) return;\n const displayHour = selectedHourDisplay ?? 12;\n const nextMinute = minute ?? 0;\n emit(to24Hour(displayHour, nextPeriod), nextMinute);\n };\n\n return (\n <div className={cn(styles.wrapper, disabled && styles.disabled, className)}>\n {label && (\n <span id={`${id}-label`} className={styles.label}>\n {label}\n </span>\n )}\n <div className={styles.columns}>\n <ul role=\"listbox\" aria-label=\"Horas\" className={styles.column}>\n {hourCells.map((cell) => {\n const active = cell === selectedHourDisplay;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handleHour(cell)}\n >\n {use12Hours ? cell : pad2(cell)}\n </button>\n </li>\n );\n })}\n </ul>\n <ul role=\"listbox\" aria-label=\"Minutos\" className={styles.column}>\n {minuteCells.map((cell) => {\n const active = cell === minute;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handleMinute(cell)}\n >\n {pad2(cell)}\n </button>\n </li>\n );\n })}\n </ul>\n {use12Hours && (\n <ul role=\"listbox\" aria-label=\"Período\" className={styles.column}>\n {([\"AM\", \"PM\"] as Period[]).map((cell) => {\n const active = selectedHourDisplay !== null && cell === period;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handlePeriod(cell)}\n >\n {cell}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n {helperText && <span className={styles.helper}>{helperText}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimePicker.js","names":[],"sources":["../../../src/components/TimePicker/TimePicker.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — minuteStep and\n * use12Hours change both the list of options and how a typed value parses, so the\n * body holds parsing, formatting and the option list together; the remaining props\n * are the shared field contract.\n */\nimport { useId, useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./TimePicker.module.css\";\n\nexport interface TimePickerProps {\n /** Selected time as a 24h `\"HH:MM\"` string (e.g. `\"14:30\"`). May be `\"\"` for none. */\n value: string;\n /** Fired with the new 24h `\"HH:MM\"` string whenever a cell is picked. */\n onChange: (value: string) => void;\n /** Granularity of the minute column. Defaults to `5`. */\n minuteStep?: number;\n /** When true, show a 1–12 hour column plus an AM/PM column (still emits 24h). */\n use12Hours?: boolean;\n label?: string;\n helperText?: string;\n disabled?: boolean;\n className?: string;\n}\n\ntype Period = \"AM\" | \"PM\";\n\ninterface ParsedTime {\n hour24: number | null;\n minute: number | null;\n}\n\nfunction pad2(value: number): string {\n return value.toString().padStart(2, \"0\");\n}\n\n/** Parse a 24h `\"HH:MM\"` string into hour/minute numbers, or nulls when empty/invalid. */\nfunction parseValue(value: string): ParsedTime {\n const match = /^(\\d{1,2}):(\\d{1,2})$/.exec(value.trim());\n if (!match) return { hour24: null, minute: null };\n const hour24 = Number(match[1]);\n const minute = Number(match[2]);\n if (hour24 < 0 || hour24 > 23 || minute < 0 || minute > 59) {\n return { hour24: null, minute: null };\n }\n return { hour24, minute };\n}\n\n/** Convert a 24h hour into its 12h display hour (1–12). */\nfunction to12Hour(hour24: number): number {\n const mod = hour24 % 12;\n return mod === 0 ? 12 : mod;\n}\n\n/** Convert a 12h display hour + period into a 24h hour (0–23). */\nfunction to24Hour(hour12: number, period: Period): number {\n const base = hour12 % 12;\n return period === \"PM\" ? base + 12 : base;\n}\n\n/**\n * TimePicker — inline, dependency-free time picker with Material \"spinner column\" styling.\n *\n * Renders scrollable columns of selectable cells (hours, minutes, and AM/PM when\n * `use12Hours`). Always emits a 24h `\"HH:MM\"` string via `onChange`, regardless of\n * whether the 12h display is enabled.\n */\nexport function TimePicker({\n value,\n onChange,\n minuteStep = 5,\n use12Hours = false,\n label,\n helperText,\n disabled,\n className,\n}: TimePickerProps) {\n const id = useId();\n const { hour24, minute } = useMemo(() => parseValue(value), [value]);\n\n const period: Period = hour24 !== null && hour24 >= 12 ? \"PM\" : \"AM\";\n\n const hourCells = useMemo<number[]>(() => {\n if (use12Hours) {\n return Array.from({ length: 12 }, (_, index) => index + 1);\n }\n return Array.from({ length: 24 }, (_, index) => index);\n }, [use12Hours]);\n\n const minuteCells = useMemo<number[]>(() => {\n const step = minuteStep > 0 ? minuteStep : 1;\n const cells: number[] = [];\n for (let current = 0; current < 60; current += step) {\n cells.push(current);\n }\n return cells;\n }, [minuteStep]);\n\n const selectedHourDisplay = hour24 === null ? null : use12Hours ? to12Hour(hour24) : hour24;\n\n const emit = (nextHour24: number, nextMinute: number): void => {\n onChange(`${pad2(nextHour24)}:${pad2(nextMinute)}`);\n };\n\n const handleHour = (displayHour: number): void => {\n if (disabled) return;\n const nextMinute = minute ?? 0;\n const nextHour24 = use12Hours ? to24Hour(displayHour, period) : displayHour;\n emit(nextHour24, nextMinute);\n };\n\n const handleMinute = (nextMinute: number): void => {\n if (disabled) return;\n const baseHour24 = hour24 ?? (use12Hours ? to24Hour(12, \"AM\") : 0);\n emit(baseHour24, nextMinute);\n };\n\n const handlePeriod = (nextPeriod: Period): void => {\n if (disabled) return;\n const displayHour = selectedHourDisplay ?? 12;\n const nextMinute = minute ?? 0;\n emit(to24Hour(displayHour, nextPeriod), nextMinute);\n };\n\n return (\n <div className={cn(styles.wrapper, disabled && styles.disabled, className)}>\n {label && (\n <span id={`${id}-label`} className={styles.label}>\n {label}\n </span>\n )}\n <div className={styles.columns}>\n <ul role=\"listbox\" aria-label=\"Horas\" className={styles.column}>\n {hourCells.map((cell) => {\n const active = cell === selectedHourDisplay;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handleHour(cell)}\n >\n {use12Hours ? cell : pad2(cell)}\n </button>\n </li>\n );\n })}\n </ul>\n <ul role=\"listbox\" aria-label=\"Minutos\" className={styles.column}>\n {minuteCells.map((cell) => {\n const active = cell === minute;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handleMinute(cell)}\n >\n {pad2(cell)}\n </button>\n </li>\n );\n })}\n </ul>\n {use12Hours && (\n <ul role=\"listbox\" aria-label=\"Período\" className={styles.column}>\n {([\"AM\", \"PM\"] as Period[]).map((cell) => {\n const active = selectedHourDisplay !== null && cell === period;\n return (\n <li key={cell} className={styles.cellWrapper}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n disabled={disabled}\n className={cn(styles.cell, active && styles.active)}\n onClick={() => handlePeriod(cell)}\n >\n {cell}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n {helperText && <span className={styles.helper}>{helperText}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ToastProvider.cjs","names":[],"sources":["../../../src/components/Toast/ToastProvider.tsx"],"sourcesContent":["import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Toast.module.css\";\n\nexport type ToastVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\nexport interface ToastOptions {\n id?: string;\n title?: string;\n description?: string;\n variant?: ToastVariant;\n /** Auto-dismiss timeout in ms. Pass `0` to keep the toast until dismissed manually. */\n duration?: number;\n}\n\ninterface ToastEntry extends Required<Omit<ToastOptions, \"id\" | \"title\" | \"description\">> {\n id: string;\n title?: string;\n description?: string;\n}\n\nexport interface ToastApi {\n show: (options: ToastOptions) => string;\n dismiss: (id: string) => void;\n success: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n error: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n warning: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n info: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n}\n\nconst ToastContext = createContext<ToastApi | null>(null);\n\n/**\n * Access the toast API. Must be used inside a {@link ToastProvider}.\n *\n * @returns Methods to show and dismiss toasts.\n */\nexport function useToast(): ToastApi {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used inside a <ToastProvider>\");\n return ctx;\n}\n\nexport type ToastPosition =\n \"top-right\" | \"top-left\" | \"top-center\" | \"bottom-right\" | \"bottom-left\" | \"bottom-center\";\n\nexport interface ToastProviderProps {\n children: ReactNode;\n /** Default auto-dismiss duration (ms). Default 4000. */\n defaultDuration?: number;\n /** Stack position on screen. Default `\"top-right\"`. */\n position?: ToastPosition;\n}\n\n/**\n * Renders a portalled toast container and exposes the imperative {@link useToast} API.\n */\nexport function ToastProvider({\n children,\n defaultDuration = 4000,\n position = \"top-right\",\n}: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const counter = useRef<number>(0);\n\n const dismiss = useCallback((id: string): void => {\n setToasts((current) => current.filter((toast) => toast.id !== id));\n }, []);\n\n const show = useCallback(\n (options: ToastOptions): string => {\n const id = options.id ?? `toast-${++counter.current}`;\n const entry: ToastEntry = {\n id,\n title: options.title,\n description: options.description,\n variant: options.variant ?? \"info\",\n duration: options.duration ?? defaultDuration,\n };\n setToasts((current) => [...current, entry]);\n return id;\n },\n [defaultDuration],\n );\n\n const api = useMemo<ToastApi>(\n () => ({\n show,\n dismiss,\n success: (description, options) =>\n show({ ...options, description, variant: \"success\" }),\n error: (description, options) => show({ ...options, description, variant: \"error\" }),\n warning: (description, options) =>\n show({ ...options, description, variant: \"warning\" }),\n info: (description, options) => show({ ...options, description, variant: \"info\" }),\n }),\n [show, dismiss],\n );\n\n return (\n <ToastContext.Provider value={api}>\n {children}\n <ToastContainer toasts={toasts} onDismiss={dismiss} position={position} />\n </ToastContext.Provider>\n );\n}\n\ninterface ContainerProps {\n toasts: ToastEntry[];\n onDismiss: (id: string) => void;\n position: ToastPosition;\n}\n\nfunction positionClass(position: ToastPosition): string {\n switch (position) {\n case \"top-left\":\n return styles.positionTopLeft;\n case \"top-center\":\n return styles.positionTopCenter;\n case \"bottom-right\":\n return styles.positionBottomRight;\n case \"bottom-left\":\n return styles.positionBottomLeft;\n case \"bottom-center\":\n return styles.positionBottomCenter;\n case \"top-right\":\n default:\n return styles.positionTopRight;\n }\n}\n\n/**\n * The portalled stack, and the page's live region for toasts.\n *\n * `aria-atomic=\"false\"` matters: with `\"true\"` a screen reader re-reads the\n * **entire** stack on every change, so the third toast of a batch is announced as\n * all three, and dismissing one re-announces the survivors. `\"false\"` announces only\n * the node that was added, which is what the user needs.\n *\n * The stack stays the live region rather than delegating to `useAnnounce`, because\n * routing it through the shared announcer would put the same text in the document\n * twice — once visible, once hidden — and every `getByText(\"Salvo\")` in a consuming\n * app's test suite would start matching two nodes. The announcer is for messages\n * that have no on-screen home; a toast has one.\n */\nfunction ToastContainer({ toasts, onDismiss, position }: ContainerProps) {\n if (typeof document === \"undefined\") return null;\n return createPortal(\n <div\n className={cn(styles.container, positionClass(position))}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n >\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />\n ))}\n </div>,\n document.body,\n );\n}\n\ninterface ItemProps {\n toast: ToastEntry;\n onDismiss: (id: string) => void;\n}\n\n/**\n * One toast.\n *\n * No `role=\"status\"` of its own: it already lives inside the container's live\n * region, and a live region nested in a live region is announced twice by some\n * screen readers.\n */\nfunction ToastItem({ toast, onDismiss }: ItemProps) {\n useEffect(() => {\n if (!toast.duration) return;\n const timer = setTimeout(() => onDismiss(toast.id), toast.duration);\n return () => clearTimeout(timer);\n }, [toast.id, toast.duration, onDismiss]);\n\n return (\n <div className={cn(styles.toast, styles[toast.variant])}>\n <div>\n {toast.title && <p className={styles.title}>{toast.title}</p>}\n {toast.description && <p className={styles.description}>{toast.description}</p>}\n </div>\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar notificação\"\n onClick={() => onDismiss(toast.id)}\n >\n <CloseIcon />\n </button>\n </div>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ToastProvider.cjs","names":[],"sources":["../../../src/components/Toast/ToastProvider.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines — the provider is the queue: enqueue, dedupe, auto-\n * dismiss timers, pause on hover and the portal that renders them. Splitting the\n * queue from the renderer would put a timer in one file and the element it dismisses\n * in another.\n */\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Toast.module.css\";\n\nexport type ToastVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\nexport interface ToastOptions {\n id?: string;\n title?: string;\n description?: string;\n variant?: ToastVariant;\n /** Auto-dismiss timeout in ms. Pass `0` to keep the toast until dismissed manually. */\n duration?: number;\n}\n\ninterface ToastEntry extends Required<Omit<ToastOptions, \"id\" | \"title\" | \"description\">> {\n id: string;\n title?: string;\n description?: string;\n}\n\nexport interface ToastApi {\n show: (options: ToastOptions) => string;\n dismiss: (id: string) => void;\n success: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n error: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n warning: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n info: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n}\n\nconst ToastContext = createContext<ToastApi | null>(null);\n\n/**\n * Access the toast API. Must be used inside a {@link ToastProvider}.\n *\n * @returns Methods to show and dismiss toasts.\n */\nexport function useToast(): ToastApi {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used inside a <ToastProvider>\");\n return ctx;\n}\n\nexport type ToastPosition =\n \"top-right\" | \"top-left\" | \"top-center\" | \"bottom-right\" | \"bottom-left\" | \"bottom-center\";\n\nexport interface ToastProviderProps {\n children: ReactNode;\n /** Default auto-dismiss duration (ms). Default 4000. */\n defaultDuration?: number;\n /** Stack position on screen. Default `\"top-right\"`. */\n position?: ToastPosition;\n}\n\n/**\n * Renders a portalled toast container and exposes the imperative {@link useToast} API.\n */\nexport function ToastProvider({\n children,\n defaultDuration = 4000,\n position = \"top-right\",\n}: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const counter = useRef<number>(0);\n\n const dismiss = useCallback((id: string): void => {\n setToasts((current) => current.filter((toast) => toast.id !== id));\n }, []);\n\n const show = useCallback(\n (options: ToastOptions): string => {\n const id = options.id ?? `toast-${++counter.current}`;\n const entry: ToastEntry = {\n id,\n title: options.title,\n description: options.description,\n variant: options.variant ?? \"info\",\n duration: options.duration ?? defaultDuration,\n };\n setToasts((current) => [...current, entry]);\n return id;\n },\n [defaultDuration],\n );\n\n const api = useMemo<ToastApi>(\n () => ({\n show,\n dismiss,\n success: (description, options) =>\n show({ ...options, description, variant: \"success\" }),\n error: (description, options) => show({ ...options, description, variant: \"error\" }),\n warning: (description, options) =>\n show({ ...options, description, variant: \"warning\" }),\n info: (description, options) => show({ ...options, description, variant: \"info\" }),\n }),\n [show, dismiss],\n );\n\n return (\n <ToastContext.Provider value={api}>\n {children}\n <ToastContainer toasts={toasts} onDismiss={dismiss} position={position} />\n </ToastContext.Provider>\n );\n}\n\ninterface ContainerProps {\n toasts: ToastEntry[];\n onDismiss: (id: string) => void;\n position: ToastPosition;\n}\n\nfunction positionClass(position: ToastPosition): string {\n switch (position) {\n case \"top-left\":\n return styles.positionTopLeft;\n case \"top-center\":\n return styles.positionTopCenter;\n case \"bottom-right\":\n return styles.positionBottomRight;\n case \"bottom-left\":\n return styles.positionBottomLeft;\n case \"bottom-center\":\n return styles.positionBottomCenter;\n case \"top-right\":\n default:\n return styles.positionTopRight;\n }\n}\n\n/**\n * The portalled stack, and the page's live region for toasts.\n *\n * `aria-atomic=\"false\"` matters: with `\"true\"` a screen reader re-reads the\n * **entire** stack on every change, so the third toast of a batch is announced as\n * all three, and dismissing one re-announces the survivors. `\"false\"` announces only\n * the node that was added, which is what the user needs.\n *\n * The stack stays the live region rather than delegating to `useAnnounce`, because\n * routing it through the shared announcer would put the same text in the document\n * twice — once visible, once hidden — and every `getByText(\"Salvo\")` in a consuming\n * app's test suite would start matching two nodes. The announcer is for messages\n * that have no on-screen home; a toast has one.\n */\nfunction ToastContainer({ toasts, onDismiss, position }: ContainerProps) {\n if (typeof document === \"undefined\") return null;\n return createPortal(\n <div\n className={cn(styles.container, positionClass(position))}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n >\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />\n ))}\n </div>,\n document.body,\n );\n}\n\ninterface ItemProps {\n toast: ToastEntry;\n onDismiss: (id: string) => void;\n}\n\n/**\n * One toast.\n *\n * No `role=\"status\"` of its own: it already lives inside the container's live\n * region, and a live region nested in a live region is announced twice by some\n * screen readers.\n */\nfunction ToastItem({ toast, onDismiss }: ItemProps) {\n useEffect(() => {\n if (!toast.duration) return;\n const timer = setTimeout(() => onDismiss(toast.id), toast.duration);\n return () => clearTimeout(timer);\n }, [toast.id, toast.duration, onDismiss]);\n\n return (\n <div className={cn(styles.toast, styles[toast.variant])}>\n <div>\n {toast.title && <p className={styles.title}>{toast.title}</p>}\n {toast.description && <p className={styles.description}>{toast.description}</p>}\n </div>\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar notificação\"\n onClick={() => onDismiss(toast.id)}\n >\n <CloseIcon />\n </button>\n </div>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"mJAoDA,IAAM,GAAA,EAAA,EAAA,cAAA,CAA8C,IAAI,EAOxD,SAAgB,GAAqB,CACjC,IAAM,GAAA,EAAA,EAAA,WAAA,CAAiB,CAAY,EACnC,GAAI,CAAC,EAAK,MAAU,MAAM,gDAAgD,EAC1E,OAAO,CACX,CAgBA,SAAgB,EAAc,CAC1B,WACA,kBAAkB,IAClB,WAAW,aACQ,CACnB,GAAM,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAAoC,CAAC,CAAC,EAC/C,GAAA,EAAA,EAAA,OAAA,CAAyB,CAAC,EAE1B,GAAA,EAAA,EAAA,YAAA,CAAuB,GAAqB,CAC9C,EAAW,GAAY,EAAQ,OAAQ,GAAU,EAAM,KAAO,CAAE,CAAC,CACrE,EAAG,CAAC,CAAC,EAEC,GAAA,EAAA,EAAA,YAAA,CACD,GAAkC,CAC/B,IAAM,EAAK,EAAQ,IAAM,SAAS,EAAE,EAAQ,UACtC,EAAoB,CACtB,KACA,MAAO,EAAQ,MACf,YAAa,EAAQ,YACrB,QAAS,EAAQ,SAAW,OAC5B,SAAU,EAAQ,UAAY,CAClC,EAEA,OADA,EAAW,GAAY,CAAC,GAAG,EAAS,CAAK,CAAC,EACnC,CACX,EACA,CAAC,CAAe,CACpB,EAEM,GAAA,EAAA,EAAA,QAAA,MACK,CACH,OACA,UACA,SAAU,EAAa,IACnB,EAAK,CAAE,GAAG,EAAS,cAAa,QAAS,SAAU,CAAC,EACxD,OAAQ,EAAa,IAAY,EAAK,CAAE,GAAG,EAAS,cAAa,QAAS,OAAQ,CAAC,EACnF,SAAU,EAAa,IACnB,EAAK,CAAE,GAAG,EAAS,cAAa,QAAS,SAAU,CAAC,EACxD,MAAO,EAAa,IAAY,EAAK,CAAE,GAAG,EAAS,cAAa,QAAS,MAAO,CAAC,CACrF,GACA,CAAC,EAAM,CAAO,CAClB,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,EAAa,SAAd,CAAuB,MAAO,WAA9B,CACK,GACD,EAAA,EAAA,IAAA,CAAC,EAAD,CAAwB,SAAQ,UAAW,EAAmB,UAAW,CAAA,CACtD,GAE/B,CAQA,SAAS,EAAc,EAAiC,CACpD,OAAQ,EAAR,CACI,IAAK,WACD,OAAO,EAAA,QAAO,gBAClB,IAAK,aACD,OAAO,EAAA,QAAO,kBAClB,IAAK,eACD,OAAO,EAAA,QAAO,oBAClB,IAAK,cACD,OAAO,EAAA,QAAO,mBAClB,IAAK,gBACD,OAAO,EAAA,QAAO,qBAElB,QACI,OAAO,EAAA,QAAO,gBACtB,CACJ,CAgBA,SAAS,EAAe,CAAE,SAAQ,YAAW,YAA4B,CAErE,OADI,OAAO,SAAa,IAAoB,MAC5C,EAAA,EAAA,aAAA,EACI,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,GAAG,EAAA,QAAO,UAAW,EAAc,CAAQ,CAAC,EACvD,YAAU,SACV,cAAY,iBAEX,EAAO,IAAK,IACT,EAAA,EAAA,IAAA,CAAC,EAAD,CAAiC,QAAkB,WAAY,EAA/C,EAAM,EAAyC,CAClE,CACA,CAAA,EACL,SAAS,IACb,CACJ,CAcA,SAAS,EAAU,CAAE,QAAO,aAAwB,CAOhD,OANA,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAM,SAAU,OACrB,IAAM,EAAQ,eAAiB,EAAU,EAAM,EAAE,EAAG,EAAM,QAAQ,EAClE,UAAa,aAAa,CAAK,CACnC,EAAG,CAAC,EAAM,GAAI,EAAM,SAAU,CAAS,CAAC,GAGpC,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,EAAA,QAAO,EAAM,QAAQ,WAAtD,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAA,SAAA,CACK,EAAM,QAAS,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,eAAQ,EAAM,KAAS,CAAA,EAC3D,EAAM,cAAe,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,qBAAc,EAAM,WAAe,CAAA,CAC7E,CAAA,CAAA,GACL,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,MAClB,aAAW,qBACX,YAAe,EAAU,EAAM,EAAE,YAEjC,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,CAAA,CACR,CAAA,CACP,GAEb,CAEA,SAAS,GAAY,CACjB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,iBACjD,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,uBACF,OAAO,eACP,YAAY,IACZ,cAAc,OACjB,CAAA,CACA,CAAA,CAEb"}
@@ -1 +1 @@
1
- {"version":3,"file":"ToastProvider.js","names":[],"sources":["../../../src/components/Toast/ToastProvider.tsx"],"sourcesContent":["import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Toast.module.css\";\n\nexport type ToastVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\nexport interface ToastOptions {\n id?: string;\n title?: string;\n description?: string;\n variant?: ToastVariant;\n /** Auto-dismiss timeout in ms. Pass `0` to keep the toast until dismissed manually. */\n duration?: number;\n}\n\ninterface ToastEntry extends Required<Omit<ToastOptions, \"id\" | \"title\" | \"description\">> {\n id: string;\n title?: string;\n description?: string;\n}\n\nexport interface ToastApi {\n show: (options: ToastOptions) => string;\n dismiss: (id: string) => void;\n success: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n error: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n warning: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n info: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n}\n\nconst ToastContext = createContext<ToastApi | null>(null);\n\n/**\n * Access the toast API. Must be used inside a {@link ToastProvider}.\n *\n * @returns Methods to show and dismiss toasts.\n */\nexport function useToast(): ToastApi {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used inside a <ToastProvider>\");\n return ctx;\n}\n\nexport type ToastPosition =\n \"top-right\" | \"top-left\" | \"top-center\" | \"bottom-right\" | \"bottom-left\" | \"bottom-center\";\n\nexport interface ToastProviderProps {\n children: ReactNode;\n /** Default auto-dismiss duration (ms). Default 4000. */\n defaultDuration?: number;\n /** Stack position on screen. Default `\"top-right\"`. */\n position?: ToastPosition;\n}\n\n/**\n * Renders a portalled toast container and exposes the imperative {@link useToast} API.\n */\nexport function ToastProvider({\n children,\n defaultDuration = 4000,\n position = \"top-right\",\n}: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const counter = useRef<number>(0);\n\n const dismiss = useCallback((id: string): void => {\n setToasts((current) => current.filter((toast) => toast.id !== id));\n }, []);\n\n const show = useCallback(\n (options: ToastOptions): string => {\n const id = options.id ?? `toast-${++counter.current}`;\n const entry: ToastEntry = {\n id,\n title: options.title,\n description: options.description,\n variant: options.variant ?? \"info\",\n duration: options.duration ?? defaultDuration,\n };\n setToasts((current) => [...current, entry]);\n return id;\n },\n [defaultDuration],\n );\n\n const api = useMemo<ToastApi>(\n () => ({\n show,\n dismiss,\n success: (description, options) =>\n show({ ...options, description, variant: \"success\" }),\n error: (description, options) => show({ ...options, description, variant: \"error\" }),\n warning: (description, options) =>\n show({ ...options, description, variant: \"warning\" }),\n info: (description, options) => show({ ...options, description, variant: \"info\" }),\n }),\n [show, dismiss],\n );\n\n return (\n <ToastContext.Provider value={api}>\n {children}\n <ToastContainer toasts={toasts} onDismiss={dismiss} position={position} />\n </ToastContext.Provider>\n );\n}\n\ninterface ContainerProps {\n toasts: ToastEntry[];\n onDismiss: (id: string) => void;\n position: ToastPosition;\n}\n\nfunction positionClass(position: ToastPosition): string {\n switch (position) {\n case \"top-left\":\n return styles.positionTopLeft;\n case \"top-center\":\n return styles.positionTopCenter;\n case \"bottom-right\":\n return styles.positionBottomRight;\n case \"bottom-left\":\n return styles.positionBottomLeft;\n case \"bottom-center\":\n return styles.positionBottomCenter;\n case \"top-right\":\n default:\n return styles.positionTopRight;\n }\n}\n\n/**\n * The portalled stack, and the page's live region for toasts.\n *\n * `aria-atomic=\"false\"` matters: with `\"true\"` a screen reader re-reads the\n * **entire** stack on every change, so the third toast of a batch is announced as\n * all three, and dismissing one re-announces the survivors. `\"false\"` announces only\n * the node that was added, which is what the user needs.\n *\n * The stack stays the live region rather than delegating to `useAnnounce`, because\n * routing it through the shared announcer would put the same text in the document\n * twice — once visible, once hidden — and every `getByText(\"Salvo\")` in a consuming\n * app's test suite would start matching two nodes. The announcer is for messages\n * that have no on-screen home; a toast has one.\n */\nfunction ToastContainer({ toasts, onDismiss, position }: ContainerProps) {\n if (typeof document === \"undefined\") return null;\n return createPortal(\n <div\n className={cn(styles.container, positionClass(position))}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n >\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />\n ))}\n </div>,\n document.body,\n );\n}\n\ninterface ItemProps {\n toast: ToastEntry;\n onDismiss: (id: string) => void;\n}\n\n/**\n * One toast.\n *\n * No `role=\"status\"` of its own: it already lives inside the container's live\n * region, and a live region nested in a live region is announced twice by some\n * screen readers.\n */\nfunction ToastItem({ toast, onDismiss }: ItemProps) {\n useEffect(() => {\n if (!toast.duration) return;\n const timer = setTimeout(() => onDismiss(toast.id), toast.duration);\n return () => clearTimeout(timer);\n }, [toast.id, toast.duration, onDismiss]);\n\n return (\n <div className={cn(styles.toast, styles[toast.variant])}>\n <div>\n {toast.title && <p className={styles.title}>{toast.title}</p>}\n {toast.description && <p className={styles.description}>{toast.description}</p>}\n </div>\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar notificação\"\n onClick={() => onDismiss(toast.id)}\n >\n <CloseIcon />\n </button>\n </div>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ToastProvider.js","names":[],"sources":["../../../src/components/Toast/ToastProvider.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines — the provider is the queue: enqueue, dedupe, auto-\n * dismiss timers, pause on hover and the portal that renders them. Splitting the\n * queue from the renderer would put a timer in one file and the element it dismisses\n * in another.\n */\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Toast.module.css\";\n\nexport type ToastVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\nexport interface ToastOptions {\n id?: string;\n title?: string;\n description?: string;\n variant?: ToastVariant;\n /** Auto-dismiss timeout in ms. Pass `0` to keep the toast until dismissed manually. */\n duration?: number;\n}\n\ninterface ToastEntry extends Required<Omit<ToastOptions, \"id\" | \"title\" | \"description\">> {\n id: string;\n title?: string;\n description?: string;\n}\n\nexport interface ToastApi {\n show: (options: ToastOptions) => string;\n dismiss: (id: string) => void;\n success: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n error: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n warning: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n info: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n}\n\nconst ToastContext = createContext<ToastApi | null>(null);\n\n/**\n * Access the toast API. Must be used inside a {@link ToastProvider}.\n *\n * @returns Methods to show and dismiss toasts.\n */\nexport function useToast(): ToastApi {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used inside a <ToastProvider>\");\n return ctx;\n}\n\nexport type ToastPosition =\n \"top-right\" | \"top-left\" | \"top-center\" | \"bottom-right\" | \"bottom-left\" | \"bottom-center\";\n\nexport interface ToastProviderProps {\n children: ReactNode;\n /** Default auto-dismiss duration (ms). Default 4000. */\n defaultDuration?: number;\n /** Stack position on screen. Default `\"top-right\"`. */\n position?: ToastPosition;\n}\n\n/**\n * Renders a portalled toast container and exposes the imperative {@link useToast} API.\n */\nexport function ToastProvider({\n children,\n defaultDuration = 4000,\n position = \"top-right\",\n}: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const counter = useRef<number>(0);\n\n const dismiss = useCallback((id: string): void => {\n setToasts((current) => current.filter((toast) => toast.id !== id));\n }, []);\n\n const show = useCallback(\n (options: ToastOptions): string => {\n const id = options.id ?? `toast-${++counter.current}`;\n const entry: ToastEntry = {\n id,\n title: options.title,\n description: options.description,\n variant: options.variant ?? \"info\",\n duration: options.duration ?? defaultDuration,\n };\n setToasts((current) => [...current, entry]);\n return id;\n },\n [defaultDuration],\n );\n\n const api = useMemo<ToastApi>(\n () => ({\n show,\n dismiss,\n success: (description, options) =>\n show({ ...options, description, variant: \"success\" }),\n error: (description, options) => show({ ...options, description, variant: \"error\" }),\n warning: (description, options) =>\n show({ ...options, description, variant: \"warning\" }),\n info: (description, options) => show({ ...options, description, variant: \"info\" }),\n }),\n [show, dismiss],\n );\n\n return (\n <ToastContext.Provider value={api}>\n {children}\n <ToastContainer toasts={toasts} onDismiss={dismiss} position={position} />\n </ToastContext.Provider>\n );\n}\n\ninterface ContainerProps {\n toasts: ToastEntry[];\n onDismiss: (id: string) => void;\n position: ToastPosition;\n}\n\nfunction positionClass(position: ToastPosition): string {\n switch (position) {\n case \"top-left\":\n return styles.positionTopLeft;\n case \"top-center\":\n return styles.positionTopCenter;\n case \"bottom-right\":\n return styles.positionBottomRight;\n case \"bottom-left\":\n return styles.positionBottomLeft;\n case \"bottom-center\":\n return styles.positionBottomCenter;\n case \"top-right\":\n default:\n return styles.positionTopRight;\n }\n}\n\n/**\n * The portalled stack, and the page's live region for toasts.\n *\n * `aria-atomic=\"false\"` matters: with `\"true\"` a screen reader re-reads the\n * **entire** stack on every change, so the third toast of a batch is announced as\n * all three, and dismissing one re-announces the survivors. `\"false\"` announces only\n * the node that was added, which is what the user needs.\n *\n * The stack stays the live region rather than delegating to `useAnnounce`, because\n * routing it through the shared announcer would put the same text in the document\n * twice — once visible, once hidden — and every `getByText(\"Salvo\")` in a consuming\n * app's test suite would start matching two nodes. The announcer is for messages\n * that have no on-screen home; a toast has one.\n */\nfunction ToastContainer({ toasts, onDismiss, position }: ContainerProps) {\n if (typeof document === \"undefined\") return null;\n return createPortal(\n <div\n className={cn(styles.container, positionClass(position))}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n >\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />\n ))}\n </div>,\n document.body,\n );\n}\n\ninterface ItemProps {\n toast: ToastEntry;\n onDismiss: (id: string) => void;\n}\n\n/**\n * One toast.\n *\n * No `role=\"status\"` of its own: it already lives inside the container's live\n * region, and a live region nested in a live region is announced twice by some\n * screen readers.\n */\nfunction ToastItem({ toast, onDismiss }: ItemProps) {\n useEffect(() => {\n if (!toast.duration) return;\n const timer = setTimeout(() => onDismiss(toast.id), toast.duration);\n return () => clearTimeout(timer);\n }, [toast.id, toast.duration, onDismiss]);\n\n return (\n <div className={cn(styles.toast, styles[toast.variant])}>\n <div>\n {toast.title && <p className={styles.title}>{toast.title}</p>}\n {toast.description && <p className={styles.description}>{toast.description}</p>}\n </div>\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar notificação\"\n onClick={() => onDismiss(toast.id)}\n >\n <CloseIcon />\n </button>\n </div>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Tour.cjs","names":[],"sources":["../../../src/components/Tour/Tour.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport { cn } from \"@/utils/cn\";\n\nimport { backdropRects, placeCard, type TourPlacement, type TourRect } from \"./tour-position\";\nimport styles from \"./Tour.module.css\";\n\n/** One stop of the tour. */\nexport interface TourStep {\n /**\n * CSS selector of the element to point at.\n *\n * A selector rather than a ref, so a tour can be declared as data — in a config\n * file, from the backend, next to the copy — without every screen having to\n * thread refs up to whoever renders the tour.\n */\n target?: string;\n /** Heading of the card. */\n title?: ReactNode;\n /** The explanation. */\n body: ReactNode;\n /** Preferred side. Default `\"bottom\"`, flipped when it does not fit. */\n placement?: TourPlacement;\n}\n\n/** Labels, per locale. */\ninterface TourStrings {\n next: string;\n back: string;\n finish: string;\n skip: string;\n close: string;\n progress: (current: number, total: number) => string;\n}\n\nconst PT_BR: TourStrings = {\n next: \"Próximo\",\n back: \"Voltar\",\n finish: \"Concluir\",\n skip: \"Pular\",\n close: \"Fechar tour\",\n progress: (current, total) => `Passo ${current} de ${total}`,\n};\n\nconst EN: TourStrings = {\n next: \"Next\",\n back: \"Back\",\n finish: \"Done\",\n skip: \"Skip\",\n close: \"Close tour\",\n progress: (current, total) => `Step ${current} of ${total}`,\n};\n\nexport interface TourProps {\n /** The stops, in order. */\n steps: readonly TourStep[];\n /** Whether the tour is showing. Controlled. */\n open: boolean;\n /** Closed by `Esc`, by the close button or by \"skip\". */\n onClose: () => void;\n /** Called after the last step is confirmed. `onClose` follows it. */\n onFinish?: () => void;\n /** Current step index, when the app wants to drive it. */\n index?: number;\n /** Step changed — required only when `index` is given. */\n onIndexChange?: (index: number) => void;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Space kept clear around the highlighted element, in px. Default 4. */\n spotlightPadding?: number;\n /** Extra class on the card. */\n className?: string;\n}\n\n/**\n * A guided tour: dim the page, highlight one element at a time, explain it.\n *\n * The highlighted element stays **clickable** while everything else is blocked,\n * because the useful kind of coachmark says \"press this\" and lets you press it. That\n * is why the backdrop is four rectangles around the target rather than one overlay\n * with a `box-shadow` hole — a shadow is not hit-testable, so a hole made that way\n * would not block anything.\n *\n * Whether a user has seen the tour is the app's business: this component takes\n * `open` and emits `onClose`/`onFinish`. Persisting a flag in `localStorage` is one\n * line in the app and a wrong default here.\n *\n * @example\n * const [open, setOpen] = useState(!storage.get(\"tour-v1\"));\n *\n * <Tour\n * open={open}\n * steps={[\n * { target: \"#novo-pedido\", title: \"Comece aqui\", body: \"Todo pedido nasce deste botão.\" },\n * { target: \"[data-tour='filtros']\", body: \"E filtre por período aqui.\" },\n * ]}\n * onClose={() => setOpen(false)}\n * onFinish={() => storage.set(\"tour-v1\", true)}\n * />\n */\nexport function Tour({\n steps,\n open,\n onClose,\n onFinish,\n index,\n onIndexChange,\n locale = \"pt-BR\",\n spotlightPadding = 4,\n className,\n}: TourProps) {\n const strings = locale === \"en\" ? EN : PT_BR;\n const [internal, setInternal] = useState(0);\n const current = Math.min(index ?? internal, Math.max(0, steps.length - 1));\n const step = steps[current];\n\n const card = useRef<HTMLDivElement | null>(null);\n const [target, setTarget] = useState<TourRect | null>(null);\n const [position, setPosition] = useState<{\n placement: TourPlacement;\n top: number;\n left: number;\n }>({ placement: \"center\", top: 0, left: 0 });\n const [viewport, setViewport] = useState({ width: 0, height: 0 });\n const titleId = useId();\n const bodyId = useId();\n\n useFocusTrap(card, open);\n\n const goTo = useCallback(\n (next: number) => {\n if (index === undefined) setInternal(next);\n onIndexChange?.(next);\n },\n [index, onIndexChange],\n );\n\n /** Reset to the first step whenever the tour opens again. */\n useEffect(() => {\n if (open && index === undefined) setInternal(0);\n }, [open, index]);\n\n /**\n * Find the step's element, bring it into view, and measure everything.\n *\n * The scroll happens before measuring, and the measurement is deferred to the\n * next frame: reading a rect in the same tick as `scrollIntoView` gives the\n * position the element had *before* the scroll, which lands the card over the\n * wrong part of the page.\n */\n const measure = useCallback(() => {\n if (!open || !step) return;\n const element = step.target ? document.querySelector<HTMLElement>(step.target) : null;\n const rect = element?.getBoundingClientRect() ?? null;\n setTarget(\n rect\n ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height }\n : null,\n );\n\n const size = {\n width: card.current?.offsetWidth ?? 320,\n height: card.current?.offsetHeight ?? 160,\n };\n const view = { width: window.innerWidth, height: window.innerHeight };\n setViewport(view);\n setPosition(\n placeCard({\n target: rect\n ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height }\n : null,\n card: size,\n viewport: view,\n preferred: step.placement,\n }),\n );\n }, [open, step]);\n\n useLayoutEffect(() => {\n if (!open || !step) return;\n const element = step.target ? document.querySelector<HTMLElement>(step.target) : null;\n element?.scrollIntoView({ block: \"center\", inline: \"nearest\", behavior: \"smooth\" });\n const frame = requestAnimationFrame(measure);\n return () => cancelAnimationFrame(frame);\n }, [open, step, measure]);\n\n useEffect(() => {\n if (!open) return;\n window.addEventListener(\"resize\", measure);\n window.addEventListener(\"scroll\", measure, true);\n return () => {\n window.removeEventListener(\"resize\", measure);\n window.removeEventListener(\"scroll\", measure, true);\n };\n }, [open, measure]);\n\n useEffect(() => {\n if (open) card.current?.focus();\n }, [open, current]);\n\n if (!open || !step) return null;\n\n const last = current === steps.length - 1;\n\n const finish = (): void => {\n onFinish?.();\n onClose();\n };\n\n /**\n * Arrow keys walk the tour and `Esc` leaves it.\n *\n * `Esc` is handled here rather than on `window` so a tour opened over a modal\n * does not close both: the innermost handler that sees the key wins, and the\n * card has focus.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n if (event.key === \"Escape\") {\n event.stopPropagation();\n onClose();\n return;\n }\n if (event.key === \"ArrowRight\" && !last) {\n event.preventDefault();\n goTo(current + 1);\n }\n if (event.key === \"ArrowLeft\" && current > 0) {\n event.preventDefault();\n goTo(current - 1);\n }\n };\n\n return (\n <div className={styles.root} data-placement={position.placement}>\n {backdropRects(target, viewport, spotlightPadding).map((rect, i) => (\n <div\n key={i}\n className={styles.backdrop}\n style={{\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n }}\n onClick={onClose}\n />\n ))}\n\n {target && (\n <div\n className={styles.spotlight}\n style={{\n top: target.top - spotlightPadding,\n left: target.left - spotlightPadding,\n width: target.width + spotlightPadding * 2,\n height: target.height + spotlightPadding * 2,\n }}\n />\n )}\n\n <div\n ref={card}\n className={cn(styles.card, className)}\n style={{ top: position.top, left: position.left }}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={step.title ? titleId : undefined}\n aria-describedby={bodyId}\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n >\n <div className={styles.header}>\n <span className={styles.progress}>\n {strings.progress(current + 1, steps.length)}\n </span>\n <button\n type=\"button\"\n className={styles.close}\n aria-label={strings.close}\n onClick={onClose}\n >\n ×\n </button>\n </div>\n\n {step.title && (\n <h2 className={styles.title} id={titleId}>\n {step.title}\n </h2>\n )}\n <div className={styles.body} id={bodyId}>\n {step.body}\n </div>\n\n <div className={styles.footer}>\n {!last && (\n <button type=\"button\" className={styles.skip} onClick={onClose}>\n {strings.skip}\n </button>\n )}\n <div className={styles.actions}>\n {current > 0 && (\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={() => goTo(current - 1)}\n >\n {strings.back}\n </button>\n )}\n <button\n type=\"button\"\n className={styles.primary}\n onClick={() => (last ? finish() : goTo(current + 1))}\n >\n {last ? strings.finish : strings.next}\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Tour.cjs","names":[],"sources":["../../../src/components/Tour/Tour.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — a product tour is a\n * controlled sequence (steps, index, onIndexChange, open, onClose, onFinish) drawn\n * over someone else's DOM (spotlightPadding, locale, className). The body measures\n * the target element, positions the spotlight and the card against it, and re-\n * measures on scroll and resize — one geometry, three triggers.\n */\nimport {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport { cn } from \"@/utils/cn\";\n\nimport { backdropRects, placeCard, type TourPlacement, type TourRect } from \"./tour-position\";\nimport styles from \"./Tour.module.css\";\n\n/** One stop of the tour. */\nexport interface TourStep {\n /**\n * CSS selector of the element to point at.\n *\n * A selector rather than a ref, so a tour can be declared as data — in a config\n * file, from the backend, next to the copy — without every screen having to\n * thread refs up to whoever renders the tour.\n */\n target?: string;\n /** Heading of the card. */\n title?: ReactNode;\n /** The explanation. */\n body: ReactNode;\n /** Preferred side. Default `\"bottom\"`, flipped when it does not fit. */\n placement?: TourPlacement;\n}\n\n/** Labels, per locale. */\ninterface TourStrings {\n next: string;\n back: string;\n finish: string;\n skip: string;\n close: string;\n progress: (current: number, total: number) => string;\n}\n\nconst PT_BR: TourStrings = {\n next: \"Próximo\",\n back: \"Voltar\",\n finish: \"Concluir\",\n skip: \"Pular\",\n close: \"Fechar tour\",\n progress: (current, total) => `Passo ${current} de ${total}`,\n};\n\nconst EN: TourStrings = {\n next: \"Next\",\n back: \"Back\",\n finish: \"Done\",\n skip: \"Skip\",\n close: \"Close tour\",\n progress: (current, total) => `Step ${current} of ${total}`,\n};\n\nexport interface TourProps {\n /** The stops, in order. */\n steps: readonly TourStep[];\n /** Whether the tour is showing. Controlled. */\n open: boolean;\n /** Closed by `Esc`, by the close button or by \"skip\". */\n onClose: () => void;\n /** Called after the last step is confirmed. `onClose` follows it. */\n onFinish?: () => void;\n /** Current step index, when the app wants to drive it. */\n index?: number;\n /** Step changed — required only when `index` is given. */\n onIndexChange?: (index: number) => void;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Space kept clear around the highlighted element, in px. Default 4. */\n spotlightPadding?: number;\n /** Extra class on the card. */\n className?: string;\n}\n\n/**\n * A guided tour: dim the page, highlight one element at a time, explain it.\n *\n * The highlighted element stays **clickable** while everything else is blocked,\n * because the useful kind of coachmark says \"press this\" and lets you press it. That\n * is why the backdrop is four rectangles around the target rather than one overlay\n * with a `box-shadow` hole — a shadow is not hit-testable, so a hole made that way\n * would not block anything.\n *\n * Whether a user has seen the tour is the app's business: this component takes\n * `open` and emits `onClose`/`onFinish`. Persisting a flag in `localStorage` is one\n * line in the app and a wrong default here.\n *\n * @example\n * const [open, setOpen] = useState(!storage.get(\"tour-v1\"));\n *\n * <Tour\n * open={open}\n * steps={[\n * { target: \"#novo-pedido\", title: \"Comece aqui\", body: \"Todo pedido nasce deste botão.\" },\n * { target: \"[data-tour='filtros']\", body: \"E filtre por período aqui.\" },\n * ]}\n * onClose={() => setOpen(false)}\n * onFinish={() => storage.set(\"tour-v1\", true)}\n * />\n */\nexport function Tour({\n steps,\n open,\n onClose,\n onFinish,\n index,\n onIndexChange,\n locale = \"pt-BR\",\n spotlightPadding = 4,\n className,\n}: TourProps) {\n const strings = locale === \"en\" ? EN : PT_BR;\n const [internal, setInternal] = useState(0);\n const current = Math.min(index ?? internal, Math.max(0, steps.length - 1));\n const step = steps[current];\n\n const card = useRef<HTMLDivElement | null>(null);\n const [target, setTarget] = useState<TourRect | null>(null);\n const [position, setPosition] = useState<{\n placement: TourPlacement;\n top: number;\n left: number;\n }>({ placement: \"center\", top: 0, left: 0 });\n const [viewport, setViewport] = useState({ width: 0, height: 0 });\n const titleId = useId();\n const bodyId = useId();\n\n useFocusTrap(card, open);\n\n const goTo = useCallback(\n (next: number) => {\n if (index === undefined) setInternal(next);\n onIndexChange?.(next);\n },\n [index, onIndexChange],\n );\n\n /** Reset to the first step whenever the tour opens again. */\n useEffect(() => {\n if (open && index === undefined) setInternal(0);\n }, [open, index]);\n\n /**\n * Find the step's element, bring it into view, and measure everything.\n *\n * The scroll happens before measuring, and the measurement is deferred to the\n * next frame: reading a rect in the same tick as `scrollIntoView` gives the\n * position the element had *before* the scroll, which lands the card over the\n * wrong part of the page.\n */\n const measure = useCallback(() => {\n if (!open || !step) return;\n const element = step.target ? document.querySelector<HTMLElement>(step.target) : null;\n const rect = element?.getBoundingClientRect() ?? null;\n setTarget(\n rect\n ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height }\n : null,\n );\n\n const size = {\n width: card.current?.offsetWidth ?? 320,\n height: card.current?.offsetHeight ?? 160,\n };\n const view = { width: window.innerWidth, height: window.innerHeight };\n setViewport(view);\n setPosition(\n placeCard({\n target: rect\n ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height }\n : null,\n card: size,\n viewport: view,\n preferred: step.placement,\n }),\n );\n }, [open, step]);\n\n useLayoutEffect(() => {\n if (!open || !step) return;\n const element = step.target ? document.querySelector<HTMLElement>(step.target) : null;\n element?.scrollIntoView({ block: \"center\", inline: \"nearest\", behavior: \"smooth\" });\n const frame = requestAnimationFrame(measure);\n return () => cancelAnimationFrame(frame);\n }, [open, step, measure]);\n\n useEffect(() => {\n if (!open) return;\n window.addEventListener(\"resize\", measure);\n window.addEventListener(\"scroll\", measure, true);\n return () => {\n window.removeEventListener(\"resize\", measure);\n window.removeEventListener(\"scroll\", measure, true);\n };\n }, [open, measure]);\n\n useEffect(() => {\n if (open) card.current?.focus();\n }, [open, current]);\n\n if (!open || !step) return null;\n\n const last = current === steps.length - 1;\n\n const finish = (): void => {\n onFinish?.();\n onClose();\n };\n\n /**\n * Arrow keys walk the tour and `Esc` leaves it.\n *\n * `Esc` is handled here rather than on `window` so a tour opened over a modal\n * does not close both: the innermost handler that sees the key wins, and the\n * card has focus.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n if (event.key === \"Escape\") {\n event.stopPropagation();\n onClose();\n return;\n }\n if (event.key === \"ArrowRight\" && !last) {\n event.preventDefault();\n goTo(current + 1);\n }\n if (event.key === \"ArrowLeft\" && current > 0) {\n event.preventDefault();\n goTo(current - 1);\n }\n };\n\n return (\n <div className={styles.root} data-placement={position.placement}>\n {backdropRects(target, viewport, spotlightPadding).map((rect, i) => (\n <div\n key={i}\n className={styles.backdrop}\n style={{\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n }}\n onClick={onClose}\n />\n ))}\n\n {target && (\n <div\n className={styles.spotlight}\n style={{\n top: target.top - spotlightPadding,\n left: target.left - spotlightPadding,\n width: target.width + spotlightPadding * 2,\n height: target.height + spotlightPadding * 2,\n }}\n />\n )}\n\n <div\n ref={card}\n className={cn(styles.card, className)}\n style={{ top: position.top, left: position.left }}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={step.title ? titleId : undefined}\n aria-describedby={bodyId}\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n >\n <div className={styles.header}>\n <span className={styles.progress}>\n {strings.progress(current + 1, steps.length)}\n </span>\n <button\n type=\"button\"\n className={styles.close}\n aria-label={strings.close}\n onClick={onClose}\n >\n ×\n </button>\n </div>\n\n {step.title && (\n <h2 className={styles.title} id={titleId}>\n {step.title}\n </h2>\n )}\n <div className={styles.body} id={bodyId}>\n {step.body}\n </div>\n\n <div className={styles.footer}>\n {!last && (\n <button type=\"button\" className={styles.skip} onClick={onClose}>\n {strings.skip}\n </button>\n )}\n <div className={styles.actions}>\n {current > 0 && (\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={() => goTo(current - 1)}\n >\n {strings.back}\n </button>\n )}\n <button\n type=\"button\"\n className={styles.primary}\n onClick={() => (last ? finish() : goTo(current + 1))}\n >\n {last ? strings.finish : strings.next}\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Tour.js","names":[],"sources":["../../../src/components/Tour/Tour.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport { cn } from \"@/utils/cn\";\n\nimport { backdropRects, placeCard, type TourPlacement, type TourRect } from \"./tour-position\";\nimport styles from \"./Tour.module.css\";\n\n/** One stop of the tour. */\nexport interface TourStep {\n /**\n * CSS selector of the element to point at.\n *\n * A selector rather than a ref, so a tour can be declared as data — in a config\n * file, from the backend, next to the copy — without every screen having to\n * thread refs up to whoever renders the tour.\n */\n target?: string;\n /** Heading of the card. */\n title?: ReactNode;\n /** The explanation. */\n body: ReactNode;\n /** Preferred side. Default `\"bottom\"`, flipped when it does not fit. */\n placement?: TourPlacement;\n}\n\n/** Labels, per locale. */\ninterface TourStrings {\n next: string;\n back: string;\n finish: string;\n skip: string;\n close: string;\n progress: (current: number, total: number) => string;\n}\n\nconst PT_BR: TourStrings = {\n next: \"Próximo\",\n back: \"Voltar\",\n finish: \"Concluir\",\n skip: \"Pular\",\n close: \"Fechar tour\",\n progress: (current, total) => `Passo ${current} de ${total}`,\n};\n\nconst EN: TourStrings = {\n next: \"Next\",\n back: \"Back\",\n finish: \"Done\",\n skip: \"Skip\",\n close: \"Close tour\",\n progress: (current, total) => `Step ${current} of ${total}`,\n};\n\nexport interface TourProps {\n /** The stops, in order. */\n steps: readonly TourStep[];\n /** Whether the tour is showing. Controlled. */\n open: boolean;\n /** Closed by `Esc`, by the close button or by \"skip\". */\n onClose: () => void;\n /** Called after the last step is confirmed. `onClose` follows it. */\n onFinish?: () => void;\n /** Current step index, when the app wants to drive it. */\n index?: number;\n /** Step changed — required only when `index` is given. */\n onIndexChange?: (index: number) => void;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Space kept clear around the highlighted element, in px. Default 4. */\n spotlightPadding?: number;\n /** Extra class on the card. */\n className?: string;\n}\n\n/**\n * A guided tour: dim the page, highlight one element at a time, explain it.\n *\n * The highlighted element stays **clickable** while everything else is blocked,\n * because the useful kind of coachmark says \"press this\" and lets you press it. That\n * is why the backdrop is four rectangles around the target rather than one overlay\n * with a `box-shadow` hole — a shadow is not hit-testable, so a hole made that way\n * would not block anything.\n *\n * Whether a user has seen the tour is the app's business: this component takes\n * `open` and emits `onClose`/`onFinish`. Persisting a flag in `localStorage` is one\n * line in the app and a wrong default here.\n *\n * @example\n * const [open, setOpen] = useState(!storage.get(\"tour-v1\"));\n *\n * <Tour\n * open={open}\n * steps={[\n * { target: \"#novo-pedido\", title: \"Comece aqui\", body: \"Todo pedido nasce deste botão.\" },\n * { target: \"[data-tour='filtros']\", body: \"E filtre por período aqui.\" },\n * ]}\n * onClose={() => setOpen(false)}\n * onFinish={() => storage.set(\"tour-v1\", true)}\n * />\n */\nexport function Tour({\n steps,\n open,\n onClose,\n onFinish,\n index,\n onIndexChange,\n locale = \"pt-BR\",\n spotlightPadding = 4,\n className,\n}: TourProps) {\n const strings = locale === \"en\" ? EN : PT_BR;\n const [internal, setInternal] = useState(0);\n const current = Math.min(index ?? internal, Math.max(0, steps.length - 1));\n const step = steps[current];\n\n const card = useRef<HTMLDivElement | null>(null);\n const [target, setTarget] = useState<TourRect | null>(null);\n const [position, setPosition] = useState<{\n placement: TourPlacement;\n top: number;\n left: number;\n }>({ placement: \"center\", top: 0, left: 0 });\n const [viewport, setViewport] = useState({ width: 0, height: 0 });\n const titleId = useId();\n const bodyId = useId();\n\n useFocusTrap(card, open);\n\n const goTo = useCallback(\n (next: number) => {\n if (index === undefined) setInternal(next);\n onIndexChange?.(next);\n },\n [index, onIndexChange],\n );\n\n /** Reset to the first step whenever the tour opens again. */\n useEffect(() => {\n if (open && index === undefined) setInternal(0);\n }, [open, index]);\n\n /**\n * Find the step's element, bring it into view, and measure everything.\n *\n * The scroll happens before measuring, and the measurement is deferred to the\n * next frame: reading a rect in the same tick as `scrollIntoView` gives the\n * position the element had *before* the scroll, which lands the card over the\n * wrong part of the page.\n */\n const measure = useCallback(() => {\n if (!open || !step) return;\n const element = step.target ? document.querySelector<HTMLElement>(step.target) : null;\n const rect = element?.getBoundingClientRect() ?? null;\n setTarget(\n rect\n ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height }\n : null,\n );\n\n const size = {\n width: card.current?.offsetWidth ?? 320,\n height: card.current?.offsetHeight ?? 160,\n };\n const view = { width: window.innerWidth, height: window.innerHeight };\n setViewport(view);\n setPosition(\n placeCard({\n target: rect\n ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height }\n : null,\n card: size,\n viewport: view,\n preferred: step.placement,\n }),\n );\n }, [open, step]);\n\n useLayoutEffect(() => {\n if (!open || !step) return;\n const element = step.target ? document.querySelector<HTMLElement>(step.target) : null;\n element?.scrollIntoView({ block: \"center\", inline: \"nearest\", behavior: \"smooth\" });\n const frame = requestAnimationFrame(measure);\n return () => cancelAnimationFrame(frame);\n }, [open, step, measure]);\n\n useEffect(() => {\n if (!open) return;\n window.addEventListener(\"resize\", measure);\n window.addEventListener(\"scroll\", measure, true);\n return () => {\n window.removeEventListener(\"resize\", measure);\n window.removeEventListener(\"scroll\", measure, true);\n };\n }, [open, measure]);\n\n useEffect(() => {\n if (open) card.current?.focus();\n }, [open, current]);\n\n if (!open || !step) return null;\n\n const last = current === steps.length - 1;\n\n const finish = (): void => {\n onFinish?.();\n onClose();\n };\n\n /**\n * Arrow keys walk the tour and `Esc` leaves it.\n *\n * `Esc` is handled here rather than on `window` so a tour opened over a modal\n * does not close both: the innermost handler that sees the key wins, and the\n * card has focus.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n if (event.key === \"Escape\") {\n event.stopPropagation();\n onClose();\n return;\n }\n if (event.key === \"ArrowRight\" && !last) {\n event.preventDefault();\n goTo(current + 1);\n }\n if (event.key === \"ArrowLeft\" && current > 0) {\n event.preventDefault();\n goTo(current - 1);\n }\n };\n\n return (\n <div className={styles.root} data-placement={position.placement}>\n {backdropRects(target, viewport, spotlightPadding).map((rect, i) => (\n <div\n key={i}\n className={styles.backdrop}\n style={{\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n }}\n onClick={onClose}\n />\n ))}\n\n {target && (\n <div\n className={styles.spotlight}\n style={{\n top: target.top - spotlightPadding,\n left: target.left - spotlightPadding,\n width: target.width + spotlightPadding * 2,\n height: target.height + spotlightPadding * 2,\n }}\n />\n )}\n\n <div\n ref={card}\n className={cn(styles.card, className)}\n style={{ top: position.top, left: position.left }}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={step.title ? titleId : undefined}\n aria-describedby={bodyId}\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n >\n <div className={styles.header}>\n <span className={styles.progress}>\n {strings.progress(current + 1, steps.length)}\n </span>\n <button\n type=\"button\"\n className={styles.close}\n aria-label={strings.close}\n onClick={onClose}\n >\n ×\n </button>\n </div>\n\n {step.title && (\n <h2 className={styles.title} id={titleId}>\n {step.title}\n </h2>\n )}\n <div className={styles.body} id={bodyId}>\n {step.body}\n </div>\n\n <div className={styles.footer}>\n {!last && (\n <button type=\"button\" className={styles.skip} onClick={onClose}>\n {strings.skip}\n </button>\n )}\n <div className={styles.actions}>\n {current > 0 && (\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={() => goTo(current - 1)}\n >\n {strings.back}\n </button>\n )}\n <button\n type=\"button\"\n className={styles.primary}\n onClick={() => (last ? finish() : goTo(current + 1))}\n >\n {last ? strings.finish : strings.next}\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Tour.js","names":[],"sources":["../../../src/components/Tour/Tour.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — a product tour is a\n * controlled sequence (steps, index, onIndexChange, open, onClose, onFinish) drawn\n * over someone else's DOM (spotlightPadding, locale, className). The body measures\n * the target element, positions the spotlight and the card against it, and re-\n * measures on scroll and resize — one geometry, three triggers.\n */\nimport {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport { cn } from \"@/utils/cn\";\n\nimport { backdropRects, placeCard, type TourPlacement, type TourRect } from \"./tour-position\";\nimport styles from \"./Tour.module.css\";\n\n/** One stop of the tour. */\nexport interface TourStep {\n /**\n * CSS selector of the element to point at.\n *\n * A selector rather than a ref, so a tour can be declared as data — in a config\n * file, from the backend, next to the copy — without every screen having to\n * thread refs up to whoever renders the tour.\n */\n target?: string;\n /** Heading of the card. */\n title?: ReactNode;\n /** The explanation. */\n body: ReactNode;\n /** Preferred side. Default `\"bottom\"`, flipped when it does not fit. */\n placement?: TourPlacement;\n}\n\n/** Labels, per locale. */\ninterface TourStrings {\n next: string;\n back: string;\n finish: string;\n skip: string;\n close: string;\n progress: (current: number, total: number) => string;\n}\n\nconst PT_BR: TourStrings = {\n next: \"Próximo\",\n back: \"Voltar\",\n finish: \"Concluir\",\n skip: \"Pular\",\n close: \"Fechar tour\",\n progress: (current, total) => `Passo ${current} de ${total}`,\n};\n\nconst EN: TourStrings = {\n next: \"Next\",\n back: \"Back\",\n finish: \"Done\",\n skip: \"Skip\",\n close: \"Close tour\",\n progress: (current, total) => `Step ${current} of ${total}`,\n};\n\nexport interface TourProps {\n /** The stops, in order. */\n steps: readonly TourStep[];\n /** Whether the tour is showing. Controlled. */\n open: boolean;\n /** Closed by `Esc`, by the close button or by \"skip\". */\n onClose: () => void;\n /** Called after the last step is confirmed. `onClose` follows it. */\n onFinish?: () => void;\n /** Current step index, when the app wants to drive it. */\n index?: number;\n /** Step changed — required only when `index` is given. */\n onIndexChange?: (index: number) => void;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Space kept clear around the highlighted element, in px. Default 4. */\n spotlightPadding?: number;\n /** Extra class on the card. */\n className?: string;\n}\n\n/**\n * A guided tour: dim the page, highlight one element at a time, explain it.\n *\n * The highlighted element stays **clickable** while everything else is blocked,\n * because the useful kind of coachmark says \"press this\" and lets you press it. That\n * is why the backdrop is four rectangles around the target rather than one overlay\n * with a `box-shadow` hole — a shadow is not hit-testable, so a hole made that way\n * would not block anything.\n *\n * Whether a user has seen the tour is the app's business: this component takes\n * `open` and emits `onClose`/`onFinish`. Persisting a flag in `localStorage` is one\n * line in the app and a wrong default here.\n *\n * @example\n * const [open, setOpen] = useState(!storage.get(\"tour-v1\"));\n *\n * <Tour\n * open={open}\n * steps={[\n * { target: \"#novo-pedido\", title: \"Comece aqui\", body: \"Todo pedido nasce deste botão.\" },\n * { target: \"[data-tour='filtros']\", body: \"E filtre por período aqui.\" },\n * ]}\n * onClose={() => setOpen(false)}\n * onFinish={() => storage.set(\"tour-v1\", true)}\n * />\n */\nexport function Tour({\n steps,\n open,\n onClose,\n onFinish,\n index,\n onIndexChange,\n locale = \"pt-BR\",\n spotlightPadding = 4,\n className,\n}: TourProps) {\n const strings = locale === \"en\" ? EN : PT_BR;\n const [internal, setInternal] = useState(0);\n const current = Math.min(index ?? internal, Math.max(0, steps.length - 1));\n const step = steps[current];\n\n const card = useRef<HTMLDivElement | null>(null);\n const [target, setTarget] = useState<TourRect | null>(null);\n const [position, setPosition] = useState<{\n placement: TourPlacement;\n top: number;\n left: number;\n }>({ placement: \"center\", top: 0, left: 0 });\n const [viewport, setViewport] = useState({ width: 0, height: 0 });\n const titleId = useId();\n const bodyId = useId();\n\n useFocusTrap(card, open);\n\n const goTo = useCallback(\n (next: number) => {\n if (index === undefined) setInternal(next);\n onIndexChange?.(next);\n },\n [index, onIndexChange],\n );\n\n /** Reset to the first step whenever the tour opens again. */\n useEffect(() => {\n if (open && index === undefined) setInternal(0);\n }, [open, index]);\n\n /**\n * Find the step's element, bring it into view, and measure everything.\n *\n * The scroll happens before measuring, and the measurement is deferred to the\n * next frame: reading a rect in the same tick as `scrollIntoView` gives the\n * position the element had *before* the scroll, which lands the card over the\n * wrong part of the page.\n */\n const measure = useCallback(() => {\n if (!open || !step) return;\n const element = step.target ? document.querySelector<HTMLElement>(step.target) : null;\n const rect = element?.getBoundingClientRect() ?? null;\n setTarget(\n rect\n ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height }\n : null,\n );\n\n const size = {\n width: card.current?.offsetWidth ?? 320,\n height: card.current?.offsetHeight ?? 160,\n };\n const view = { width: window.innerWidth, height: window.innerHeight };\n setViewport(view);\n setPosition(\n placeCard({\n target: rect\n ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height }\n : null,\n card: size,\n viewport: view,\n preferred: step.placement,\n }),\n );\n }, [open, step]);\n\n useLayoutEffect(() => {\n if (!open || !step) return;\n const element = step.target ? document.querySelector<HTMLElement>(step.target) : null;\n element?.scrollIntoView({ block: \"center\", inline: \"nearest\", behavior: \"smooth\" });\n const frame = requestAnimationFrame(measure);\n return () => cancelAnimationFrame(frame);\n }, [open, step, measure]);\n\n useEffect(() => {\n if (!open) return;\n window.addEventListener(\"resize\", measure);\n window.addEventListener(\"scroll\", measure, true);\n return () => {\n window.removeEventListener(\"resize\", measure);\n window.removeEventListener(\"scroll\", measure, true);\n };\n }, [open, measure]);\n\n useEffect(() => {\n if (open) card.current?.focus();\n }, [open, current]);\n\n if (!open || !step) return null;\n\n const last = current === steps.length - 1;\n\n const finish = (): void => {\n onFinish?.();\n onClose();\n };\n\n /**\n * Arrow keys walk the tour and `Esc` leaves it.\n *\n * `Esc` is handled here rather than on `window` so a tour opened over a modal\n * does not close both: the innermost handler that sees the key wins, and the\n * card has focus.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n if (event.key === \"Escape\") {\n event.stopPropagation();\n onClose();\n return;\n }\n if (event.key === \"ArrowRight\" && !last) {\n event.preventDefault();\n goTo(current + 1);\n }\n if (event.key === \"ArrowLeft\" && current > 0) {\n event.preventDefault();\n goTo(current - 1);\n }\n };\n\n return (\n <div className={styles.root} data-placement={position.placement}>\n {backdropRects(target, viewport, spotlightPadding).map((rect, i) => (\n <div\n key={i}\n className={styles.backdrop}\n style={{\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n }}\n onClick={onClose}\n />\n ))}\n\n {target && (\n <div\n className={styles.spotlight}\n style={{\n top: target.top - spotlightPadding,\n left: target.left - spotlightPadding,\n width: target.width + spotlightPadding * 2,\n height: target.height + spotlightPadding * 2,\n }}\n />\n )}\n\n <div\n ref={card}\n className={cn(styles.card, className)}\n style={{ top: position.top, left: position.left }}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={step.title ? titleId : undefined}\n aria-describedby={bodyId}\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n >\n <div className={styles.header}>\n <span className={styles.progress}>\n {strings.progress(current + 1, steps.length)}\n </span>\n <button\n type=\"button\"\n className={styles.close}\n aria-label={strings.close}\n onClick={onClose}\n >\n ×\n </button>\n </div>\n\n {step.title && (\n <h2 className={styles.title} id={titleId}>\n {step.title}\n </h2>\n )}\n <div className={styles.body} id={bodyId}>\n {step.body}\n </div>\n\n <div className={styles.footer}>\n {!last && (\n <button type=\"button\" className={styles.skip} onClick={onClose}>\n {strings.skip}\n </button>\n )}\n <div className={styles.actions}>\n {current > 0 && (\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={() => goTo(current - 1)}\n >\n {strings.back}\n </button>\n )}\n <button\n type=\"button\"\n className={styles.primary}\n onClick={() => (last ? finish() : goTo(current + 1))}\n >\n {last ? strings.finish : strings.next}\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAoDA,IAAM,IAAqB;CACvB,MAAM;CACN,MAAM;CACN,QAAQ;CACR,MAAM;CACN,OAAO;CACP,WAAW,GAAS,MAAU,SAAS,EAAQ,MAAM;AACzD,GAEM,IAAkB;CACpB,MAAM;CACN,MAAM;CACN,QAAQ;CACR,MAAM;CACN,OAAO;CACP,WAAW,GAAS,MAAU,QAAQ,EAAQ,MAAM;AACxD;AAiDA,SAAgB,EAAK,EACjB,UACA,SACA,YACA,aACA,UACA,kBACA,YAAS,SACT,sBAAmB,GACnB,gBACU;CACV,IAAM,IAAU,MAAW,OAAO,IAAK,GACjC,CAAC,GAAU,KAAe,EAAS,CAAC,GACpC,IAAU,KAAK,IAAI,KAAS,GAAU,KAAK,IAAI,GAAG,EAAM,SAAS,CAAC,CAAC,GACnE,IAAO,EAAM,IAEb,IAAO,EAA8B,IAAI,GACzC,CAAC,GAAQ,KAAa,EAA0B,IAAI,GACpD,CAAC,GAAU,KAAe,EAI7B;EAAE,WAAW;EAAU,KAAK;EAAG,MAAM;CAAE,CAAC,GACrC,CAAC,GAAU,KAAe,EAAS;EAAE,OAAO;EAAG,QAAQ;CAAE,CAAC,GAC1D,IAAU,EAAM,GAChB,IAAS,EAAM;CAErB,EAAa,GAAM,CAAI;CAEvB,IAAM,IAAO,GACR,MAAiB;EAEd,AADI,MAAU,KAAA,KAAW,EAAY,CAAI,GACzC,IAAgB,CAAI;CACxB,GACA,CAAC,GAAO,CAAa,CACzB;CAGA,QAAgB;EACZ,AAAI,KAAQ,MAAU,KAAA,KAAW,EAAY,CAAC;CAClD,GAAG,CAAC,GAAM,CAAK,CAAC;CAUhB,IAAM,IAAU,QAAkB;EAC9B,IAAI,CAAC,KAAQ,CAAC,GAAM;EAEpB,IAAM,KADU,EAAK,SAAS,SAAS,cAA2B,EAAK,MAAM,IAAI,KAAA,EAC3D,sBAAsB,KAAK;EACjD,EACI,IACM;GAAE,KAAK,EAAK;GAAK,MAAM,EAAK;GAAM,OAAO,EAAK;GAAO,QAAQ,EAAK;EAAO,IACzE,IACV;EAEA,IAAM,IAAO;GACT,OAAO,EAAK,SAAS,eAAe;GACpC,QAAQ,EAAK,SAAS,gBAAgB;EAC1C,GACM,IAAO;GAAE,OAAO,OAAO;GAAY,QAAQ,OAAO;EAAY;EAEpE,AADA,EAAY,CAAI,GAChB,EACI,EAAU;GACN,QAAQ,IACF;IAAE,KAAK,EAAK;IAAK,MAAM,EAAK;IAAM,OAAO,EAAK;IAAO,QAAQ,EAAK;GAAO,IACzE;GACN,MAAM;GACN,UAAU;GACV,WAAW,EAAK;EACpB,CAAC,CACL;CACJ,GAAG,CAAC,GAAM,CAAI,CAAC;CAwBf,IAtBA,QAAsB;EAClB,IAAI,CAAC,KAAQ,CAAC,GAAM;EAEpB,CADgB,EAAK,SAAS,SAAS,cAA2B,EAAK,MAAM,IAAI,KAAA,EACxE,eAAe;GAAE,OAAO;GAAU,QAAQ;GAAW,UAAU;EAAS,CAAC;EAClF,IAAM,IAAQ,sBAAsB,CAAO;EAC3C,aAAa,qBAAqB,CAAK;CAC3C,GAAG;EAAC;EAAM;EAAM;CAAO,CAAC,GAExB,QAAgB;EACP,OAGL,OAFA,OAAO,iBAAiB,UAAU,CAAO,GACzC,OAAO,iBAAiB,UAAU,GAAS,EAAI,SAClC;GAET,AADA,OAAO,oBAAoB,UAAU,CAAO,GAC5C,OAAO,oBAAoB,UAAU,GAAS,EAAI;EACtD;CACJ,GAAG,CAAC,GAAM,CAAO,CAAC,GAElB,QAAgB;EACZ,AAAI,KAAM,EAAK,SAAS,MAAM;CAClC,GAAG,CAAC,GAAM,CAAO,CAAC,GAEd,CAAC,KAAQ,CAAC,GAAM,OAAO;CAE3B,IAAM,IAAO,MAAY,EAAM,SAAS,GAElC,UAAqB;EAEvB,AADA,IAAW,GACX,EAAQ;CACZ;CAyBA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAO;EAAM,kBAAgB,EAAS;YAAtD;GACK,EAAc,GAAQ,GAAU,CAAgB,CAAC,CAAC,KAAK,GAAM,MAC1D,kBAAC,OAAD;IAEI,WAAW,EAAO;IAClB,OAAO;KACH,KAAK,EAAK;KACV,MAAM,EAAK;KACX,OAAO,EAAK;KACZ,QAAQ,EAAK;IACjB;IACA,SAAS;GACZ,GATQ,CASR,CACJ;GAEA,KACG,kBAAC,OAAD;IACI,WAAW,EAAO;IAClB,OAAO;KACH,KAAK,EAAO,MAAM;KAClB,MAAM,EAAO,OAAO;KACpB,OAAO,EAAO,QAAQ,IAAmB;KACzC,QAAQ,EAAO,SAAS,IAAmB;IAC/C;GACH,CAAA;GAGL,kBAAC,OAAD;IACI,KAAK;IACL,WAAW,EAAG,EAAO,MAAM,CAAS;IACpC,OAAO;KAAE,KAAK,EAAS;KAAK,MAAM,EAAS;IAAK;IAChD,MAAK;IACL,cAAW;IACX,mBAAiB,EAAK,QAAQ,IAAU,KAAA;IACxC,oBAAkB;IAClB,UAAU;IACV,YArDW,MAA+C;KAClE,IAAI,EAAM,QAAQ,UAAU;MAExB,AADA,EAAM,gBAAgB,GACtB,EAAQ;MACR;KACJ;KAKA,AAJI,EAAM,QAAQ,gBAAgB,CAAC,MAC/B,EAAM,eAAe,GACrB,EAAK,IAAU,CAAC,IAEhB,EAAM,QAAQ,eAAe,IAAU,MACvC,EAAM,eAAe,GACrB,EAAK,IAAU,CAAC;IAExB;cA8BQ;KAWI,kBAAC,OAAD;MAAK,WAAW,EAAO;gBAAvB,CACI,kBAAC,QAAD;OAAM,WAAW,EAAO;iBACnB,EAAQ,SAAS,IAAU,GAAG,EAAM,MAAM;MACzC,CAAA,GACN,kBAAC,UAAD;OACI,MAAK;OACL,WAAW,EAAO;OAClB,cAAY,EAAQ;OACpB,SAAS;iBACZ;MAEO,CAAA,CACP;;KAEJ,EAAK,SACF,kBAAC,MAAD;MAAI,WAAW,EAAO;MAAO,IAAI;gBAC5B,EAAK;KACN,CAAA;KAER,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAM,IAAI;gBAC5B,EAAK;KACL,CAAA;KAEL,kBAAC,OAAD;MAAK,WAAW,EAAO;gBAAvB,CACK,CAAC,KACE,kBAAC,UAAD;OAAQ,MAAK;OAAS,WAAW,EAAO;OAAM,SAAS;iBAClD,EAAQ;MACL,CAAA,GAEZ,kBAAC,OAAD;OAAK,WAAW,EAAO;iBAAvB,CACK,IAAU,KACP,kBAAC,UAAD;QACI,MAAK;QACL,WAAW,EAAO;QAClB,eAAe,EAAK,IAAU,CAAC;kBAE9B,EAAQ;OACL,CAAA,GAEZ,kBAAC,UAAD;QACI,MAAK;QACL,WAAW,EAAO;QAClB,eAAgB,IAAO,EAAO,IAAI,EAAK,IAAU,CAAC;kBAEjD,IAAO,EAAQ,SAAS,EAAQ;OAC7B,CAAA,CACP;QACJ;;IACJ;;EACJ;;AAEb"}
@@ -1 +1 @@
1
- {"version":3,"file":"Transfer.cjs","names":[],"sources":["../../../src/components/Transfer/Transfer.tsx"],"sourcesContent":["import { useId, useMemo, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { Checkbox } from \"../Checkbox\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n applyMove,\n filterItems,\n movableIds,\n splitSides,\n transferStrings,\n type TransferItem,\n type TransferSide,\n} from \"./transfer-state\";\nimport styles from \"./Transfer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\" | \"defaultValue\";\n\nexport interface TransferProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The whole catalogue. Both panes are derived from it. */\n items: readonly TransferItem[];\n /** Ids on the target side. Controlled. */\n value: readonly string[];\n /** Next value, always in catalogue order. */\n onChange: (value: string[]) => void;\n /** Heading of the left pane. Default `\"Disponíveis\"`. */\n sourceTitle?: ReactNode;\n /** Heading of the right pane. Default `\"Selecionados\"`. */\n targetTitle?: ReactNode;\n /** Show a search box above each pane. Default `true` past 8 items. */\n searchable?: boolean;\n /** Custom row body. Gets the item and which side it is on. */\n renderItem?: (item: TransferItem, side: TransferSide) => ReactNode;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Height of each pane's scroll area, any CSS length. Default `\"16rem\"`. */\n height?: string;\n /** Block every move. */\n disabled?: boolean;\n}\n\n/**\n * Two panes and a set of move controls: pick a subset out of a catalogue.\n *\n * Controlled on **ids of the target side only**. The panes are derived on every\n * render, never stored, because two stored lists drift the moment the catalogue\n * changes under them — a permission removed upstream lingers in whichever pane\n * held it, and an id in both panes is a bug nobody can see.\n *\n * @example\n * const [roles, setRoles] = useState<string[]>([]);\n *\n * <Transfer\n * items={allRoles}\n * value={roles}\n * onChange={setRoles}\n * sourceTitle=\"Papéis disponíveis\"\n * targetTitle=\"Papéis do usuário\"\n * />\n */\nexport function Transfer({\n items,\n value,\n onChange,\n sourceTitle,\n targetTitle,\n searchable,\n renderItem,\n locale = \"pt-BR\",\n height = \"16rem\",\n disabled = false,\n className,\n ...rest\n}: TransferProps) {\n const strings = transferStrings(locale);\n const baseId = useId();\n const [checked, setChecked] = useState<Set<string>>(new Set());\n const [queries, setQueries] = useState<{ source: string; target: string }>({\n source: \"\",\n target: \"\",\n });\n const [announcement, setAnnouncement] = useState(\"\");\n\n const { source, target } = useMemo(() => splitSides(items, value), [items, value]);\n const withSearch = searchable ?? items.length > 8;\n\n /**\n * What each pane is showing right now — the filter included.\n *\n * Every control acts on this, not on the pane's full contents: filtering to\n * \"sao\" and pressing \"move all\" has to move what you are looking at. Moving\n * rows the filter hid is the kind of surprise that makes people stop trusting\n * the bulk button.\n */\n const visible: Record<TransferSide, TransferItem[]> = {\n source: withSearch ? filterItems(source, queries.source) : [...source],\n target: withSearch ? filterItems(target, queries.target) : [...target],\n };\n\n const panes: Array<{ side: TransferSide; title: ReactNode; items: TransferItem[] }> = [\n { side: \"source\", title: sourceTitle ?? strings.sourceTitle, items: source },\n { side: \"target\", title: targetTitle ?? strings.targetTitle, items: target },\n ];\n\n /**\n * Move ids and drop them from the checked set.\n *\n * Clearing the checks is the point: after a move the rows are on the other\n * side, and leaving them checked means the next click on the opposite button\n * sends them straight back — which reads as the component undoing itself.\n */\n const move = (moving: readonly string[], to: TransferSide): void => {\n if (disabled || moving.length === 0) return;\n const next = applyMove({ value, moving, to, items });\n const changed = to === \"target\" ? next.length - value.length : value.length - next.length;\n onChange(next);\n setChecked((current) => {\n const remaining = new Set(current);\n for (const id of moving) remaining.delete(id);\n return remaining;\n });\n if (changed > 0) {\n const sideName = to === \"target\" ? strings.targetTitle : strings.sourceTitle;\n setAnnouncement(strings.moved(changed, String(sideName).toLowerCase()));\n }\n };\n\n const toggle = (id: string): void => {\n setChecked((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n };\n\n /** Ids checked **and visible** on one side — what the move buttons act on. */\n const checkedOn = (side: TransferSide): string[] =>\n movableIds(visible[side]).filter((id) => checked.has(id));\n\n const checkedSource = checkedOn(\"source\");\n const checkedTarget = checkedOn(\"target\");\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {panes.map((pane) => {\n const shown = visible[pane.side];\n const movable = movableIds(shown);\n const checkedHere = movable.filter((id) => checked.has(id));\n const allChecked = movable.length > 0 && checkedHere.length === movable.length;\n const titleId = `${baseId}-${pane.side}-title`;\n\n return (\n <section key={pane.side} className={styles.pane} aria-labelledby={titleId}>\n <div className={styles.paneHeader}>\n <div className={styles.paneTitle}>\n <Checkbox\n checked={allChecked}\n indeterminate={checkedHere.length > 0 && !allChecked}\n disabled={disabled || movable.length === 0}\n aria-label={\n typeof pane.title === \"string\"\n ? pane.title\n : String(pane.side)\n }\n onChange={() =>\n setChecked((current) => {\n const next = new Set(current);\n for (const id of movable) {\n if (allChecked) next.delete(id);\n else next.add(id);\n }\n return next;\n })\n }\n />\n <h3 className={styles.title} id={titleId}>\n {pane.title}\n </h3>\n </div>\n <span className={styles.count}>\n {strings.selected(checkedHere.length, pane.items.length)}\n </span>\n </div>\n\n {withSearch && (\n <input\n type=\"search\"\n className={styles.search}\n value={queries[pane.side]}\n placeholder={strings.search}\n aria-label={`${strings.search}: ${\n typeof pane.title === \"string\" ? pane.title : pane.side\n }`}\n onChange={(event) =>\n setQueries((current) => ({\n ...current,\n [pane.side]: event.target.value,\n }))\n }\n />\n )}\n\n <ul className={styles.list} style={{ height }}>\n {shown.length === 0 ? (\n <li className={styles.empty}>\n {queries[pane.side] ? strings.noMatches : strings.empty}\n </li>\n ) : (\n shown.map((item) => (\n <li key={item.id} className={styles.row}>\n <Checkbox\n checked={checked.has(item.id)}\n disabled={disabled || item.disabled}\n label={\n renderItem\n ? renderItem(item, pane.side)\n : item.label\n }\n onChange={() => toggle(item.id)}\n onDoubleClick={() =>\n move(\n [item.id],\n pane.side === \"source\" ? \"target\" : \"source\",\n )\n }\n />\n </li>\n ))\n )}\n </ul>\n </section>\n );\n })}\n\n <div className={styles.controls}>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || movableIds(visible.source).length === 0}\n aria-label={strings.allToTarget}\n title={strings.allToTarget}\n onClick={() => move(movableIds(visible.source), \"target\")}\n >\n »\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || checkedSource.length === 0}\n aria-label={strings.toTarget}\n title={strings.toTarget}\n onClick={() => move(checkedSource, \"target\")}\n >\n ›\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || checkedTarget.length === 0}\n aria-label={strings.toSource}\n title={strings.toSource}\n onClick={() => move(checkedTarget, \"source\")}\n >\n ‹\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || movableIds(visible.target).length === 0}\n aria-label={strings.allToSource}\n title={strings.allToSource}\n onClick={() => move(movableIds(visible.target), \"source\")}\n >\n «\n </button>\n </div>\n\n <VisuallyHidden aria-live=\"polite\" role=\"status\">\n {announcement}\n </VisuallyHidden>\n </div>\n );\n}\n"],"mappings":"yPA8DA,SAAgB,EAAS,CACrB,QACA,QACA,WACA,cACA,cACA,aACA,aACA,SAAS,QACT,SAAS,QACT,WAAW,GACX,YACA,GAAG,GACW,CACd,IAAM,EAAU,EAAA,gBAAgB,CAAM,EAChC,GAAA,EAAA,EAAA,MAAA,CAAe,EACf,CAAC,EAAS,IAAA,EAAA,EAAA,SAAA,CAAoC,IAAI,GAAK,EACvD,CAAC,EAAS,IAAA,EAAA,EAAA,SAAA,CAA2D,CACvE,OAAQ,GACR,OAAQ,EACZ,CAAC,EACK,CAAC,EAAc,IAAA,EAAA,EAAA,SAAA,CAA4B,EAAE,EAE7C,CAAE,SAAQ,WAAA,EAAA,EAAA,QAAA,KAAyB,EAAA,WAAW,EAAO,CAAK,EAAG,CAAC,EAAO,CAAK,CAAC,EAC3E,EAAa,GAAc,EAAM,OAAS,EAU1C,EAAgD,CAClD,OAAQ,EAAa,EAAA,YAAY,EAAQ,EAAQ,MAAM,EAAI,CAAC,GAAG,CAAM,EACrE,OAAQ,EAAa,EAAA,YAAY,EAAQ,EAAQ,MAAM,EAAI,CAAC,GAAG,CAAM,CACzE,EAEM,EAAgF,CAClF,CAAE,KAAM,SAAU,MAAO,GAAe,EAAQ,YAAa,MAAO,CAAO,EAC3E,CAAE,KAAM,SAAU,MAAO,GAAe,EAAQ,YAAa,MAAO,CAAO,CAC/E,EASM,GAAQ,EAA2B,IAA2B,CAChE,GAAI,GAAY,EAAO,SAAW,EAAG,OACrC,IAAM,EAAO,EAAA,UAAU,CAAE,QAAO,SAAQ,KAAI,OAAM,CAAC,EAC7C,EAAU,IAAO,SAAW,EAAK,OAAS,EAAM,OAAS,EAAM,OAAS,EAAK,OAOnF,GANA,EAAS,CAAI,EACb,EAAY,GAAY,CACpB,IAAM,EAAY,IAAI,IAAI,CAAO,EACjC,IAAK,IAAM,KAAM,EAAQ,EAAU,OAAO,CAAE,EAC5C,OAAO,CACX,CAAC,EACG,EAAU,EAAG,CACb,IAAM,EAAW,IAAO,SAAW,EAAQ,YAAc,EAAQ,YACjE,EAAgB,EAAQ,MAAM,EAAS,OAAO,CAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAC1E,CACJ,EAEM,EAAU,GAAqB,CACjC,EAAY,GAAY,CACpB,IAAM,EAAO,IAAI,IAAI,CAAO,EAG5B,OAFI,EAAK,IAAI,CAAE,EAAG,EAAK,OAAO,CAAE,EAC3B,EAAK,IAAI,CAAE,EACT,CACX,CAAC,CACL,EAGM,EAAa,GACf,EAAA,WAAW,EAAQ,EAAK,CAAC,CAAC,OAAQ,GAAO,EAAQ,IAAI,CAAE,CAAC,EAEtD,EAAgB,EAAU,QAAQ,EAClC,EAAgB,EAAU,QAAQ,EAExC,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,CAAS,EAAG,GAAI,WAAnD,CACK,EAAM,IAAK,GAAS,CACjB,IAAM,EAAQ,EAAQ,EAAK,MACrB,EAAU,EAAA,WAAW,CAAK,EAC1B,EAAc,EAAQ,OAAQ,GAAO,EAAQ,IAAI,CAAE,CAAC,EACpD,EAAa,EAAQ,OAAS,GAAK,EAAY,SAAW,EAAQ,OAClE,EAAU,GAAG,EAAO,GAAG,EAAK,KAAK,QAEvC,OACI,EAAA,EAAA,KAAA,CAAC,UAAD,CAAyB,UAAW,EAAA,QAAO,KAAM,kBAAiB,WAAlE,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,oBAAvB,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,mBAAvB,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,SAAD,CACI,QAAS,EACT,cAAe,EAAY,OAAS,GAAK,CAAC,EAC1C,SAAU,GAAY,EAAQ,SAAW,EACzC,aACI,OAAO,EAAK,OAAU,SAChB,EAAK,MACL,OAAO,EAAK,IAAI,EAE1B,aACI,EAAY,GAAY,CACpB,IAAM,EAAO,IAAI,IAAI,CAAO,EAC5B,IAAK,IAAM,KAAM,EACT,EAAY,EAAK,OAAO,CAAE,EACzB,EAAK,IAAI,CAAE,EAEpB,OAAO,CACX,CAAC,CAER,CAAA,GACD,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MAAO,GAAI,WAC5B,EAAK,KACN,CAAA,CACH,KACL,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eACnB,EAAQ,SAAS,EAAY,OAAQ,EAAK,MAAM,MAAM,CACrD,CAAA,CACL,IAEJ,IACG,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,OAClB,MAAO,EAAQ,EAAK,MACpB,YAAa,EAAQ,OACrB,aAAY,GAAG,EAAQ,OAAO,IAC1B,OAAO,EAAK,OAAU,SAAW,EAAK,MAAQ,EAAK,OAEvD,SAAW,GACP,EAAY,IAAa,CACrB,GAAG,GACF,EAAK,MAAO,EAAM,OAAO,KAC9B,EAAE,CAET,CAAA,GAGL,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,KAAM,MAAO,CAAE,QAAO,WACvC,EAAM,SAAW,GACd,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,eACjB,EAAQ,EAAK,MAAQ,EAAQ,UAAY,EAAQ,KAClD,CAAA,EAEJ,EAAM,IAAK,IACP,EAAA,EAAA,IAAA,CAAC,KAAD,CAAkB,UAAW,EAAA,QAAO,cAChC,EAAA,EAAA,IAAA,CAAC,EAAA,SAAD,CACI,QAAS,EAAQ,IAAI,EAAK,EAAE,EAC5B,SAAU,GAAY,EAAK,SAC3B,MACI,EACM,EAAW,EAAM,EAAK,IAAI,EAC1B,EAAK,MAEf,aAAgB,EAAO,EAAK,EAAE,EAC9B,kBACI,EACI,CAAC,EAAK,EAAE,EACR,EAAK,OAAS,SAAW,SAAW,QACxC,CAEP,CAAA,CACD,EAjBK,EAAK,EAiBV,CACP,CAEL,CAAA,CACC,GA9EK,EAAK,IA8EV,CAEjB,CAAC,GAED,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,kBAAvB,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,GAAY,EAAA,WAAW,EAAQ,MAAM,CAAC,CAAC,SAAW,EAC5D,aAAY,EAAQ,YACpB,MAAO,EAAQ,YACf,YAAe,EAAK,EAAA,WAAW,EAAQ,MAAM,EAAG,QAAQ,WAC3D,GAEO,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,GAAY,EAAc,SAAW,EAC/C,aAAY,EAAQ,SACpB,MAAO,EAAQ,SACf,YAAe,EAAK,EAAe,QAAQ,WAC9C,GAEO,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,GAAY,EAAc,SAAW,EAC/C,aAAY,EAAQ,SACpB,MAAO,EAAQ,SACf,YAAe,EAAK,EAAe,QAAQ,WAC9C,GAEO,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,GAAY,EAAA,WAAW,EAAQ,MAAM,CAAC,CAAC,SAAW,EAC5D,aAAY,EAAQ,YACpB,MAAO,EAAQ,YACf,YAAe,EAAK,EAAA,WAAW,EAAQ,MAAM,EAAG,QAAQ,WAC3D,GAEO,CAAA,CACP,KAEL,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAgB,YAAU,SAAS,KAAK,kBACnC,CACW,CAAA,CACf,GAEb"}
1
+ {"version":3,"file":"Transfer.cjs","names":[],"sources":["../../../src/components/Transfer/Transfer.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — two lists that hand\n * items to each other: items and value are the model, onChange the move,\n * sourceTitle/targetTitle/renderItem/locale/height the presentation, and\n * searchable/disabled the behaviour. The body keeps both selections and the filter\n * query, and every move reads all three.\n */\nimport { useId, useMemo, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { Checkbox } from \"../Checkbox\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n applyMove,\n filterItems,\n movableIds,\n splitSides,\n transferStrings,\n type TransferItem,\n type TransferSide,\n} from \"./transfer-state\";\nimport styles from \"./Transfer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\" | \"defaultValue\";\n\nexport interface TransferProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The whole catalogue. Both panes are derived from it. */\n items: readonly TransferItem[];\n /** Ids on the target side. Controlled. */\n value: readonly string[];\n /** Next value, always in catalogue order. */\n onChange: (value: string[]) => void;\n /** Heading of the left pane. Default `\"Disponíveis\"`. */\n sourceTitle?: ReactNode;\n /** Heading of the right pane. Default `\"Selecionados\"`. */\n targetTitle?: ReactNode;\n /** Show a search box above each pane. Default `true` past 8 items. */\n searchable?: boolean;\n /** Custom row body. Gets the item and which side it is on. */\n renderItem?: (item: TransferItem, side: TransferSide) => ReactNode;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Height of each pane's scroll area, any CSS length. Default `\"16rem\"`. */\n height?: string;\n /** Block every move. */\n disabled?: boolean;\n}\n\n/**\n * Two panes and a set of move controls: pick a subset out of a catalogue.\n *\n * Controlled on **ids of the target side only**. The panes are derived on every\n * render, never stored, because two stored lists drift the moment the catalogue\n * changes under them — a permission removed upstream lingers in whichever pane\n * held it, and an id in both panes is a bug nobody can see.\n *\n * @example\n * const [roles, setRoles] = useState<string[]>([]);\n *\n * <Transfer\n * items={allRoles}\n * value={roles}\n * onChange={setRoles}\n * sourceTitle=\"Papéis disponíveis\"\n * targetTitle=\"Papéis do usuário\"\n * />\n */\nexport function Transfer({\n items,\n value,\n onChange,\n sourceTitle,\n targetTitle,\n searchable,\n renderItem,\n locale = \"pt-BR\",\n height = \"16rem\",\n disabled = false,\n className,\n ...rest\n}: TransferProps) {\n const strings = transferStrings(locale);\n const baseId = useId();\n const [checked, setChecked] = useState<Set<string>>(new Set());\n const [queries, setQueries] = useState<{ source: string; target: string }>({\n source: \"\",\n target: \"\",\n });\n const [announcement, setAnnouncement] = useState(\"\");\n\n const { source, target } = useMemo(() => splitSides(items, value), [items, value]);\n const withSearch = searchable ?? items.length > 8;\n\n /**\n * What each pane is showing right now — the filter included.\n *\n * Every control acts on this, not on the pane's full contents: filtering to\n * \"sao\" and pressing \"move all\" has to move what you are looking at. Moving\n * rows the filter hid is the kind of surprise that makes people stop trusting\n * the bulk button.\n */\n const visible: Record<TransferSide, TransferItem[]> = {\n source: withSearch ? filterItems(source, queries.source) : [...source],\n target: withSearch ? filterItems(target, queries.target) : [...target],\n };\n\n const panes: Array<{ side: TransferSide; title: ReactNode; items: TransferItem[] }> = [\n { side: \"source\", title: sourceTitle ?? strings.sourceTitle, items: source },\n { side: \"target\", title: targetTitle ?? strings.targetTitle, items: target },\n ];\n\n /**\n * Move ids and drop them from the checked set.\n *\n * Clearing the checks is the point: after a move the rows are on the other\n * side, and leaving them checked means the next click on the opposite button\n * sends them straight back — which reads as the component undoing itself.\n */\n const move = (moving: readonly string[], to: TransferSide): void => {\n if (disabled || moving.length === 0) return;\n const next = applyMove({ value, moving, to, items });\n const changed = to === \"target\" ? next.length - value.length : value.length - next.length;\n onChange(next);\n setChecked((current) => {\n const remaining = new Set(current);\n for (const id of moving) remaining.delete(id);\n return remaining;\n });\n if (changed > 0) {\n const sideName = to === \"target\" ? strings.targetTitle : strings.sourceTitle;\n setAnnouncement(strings.moved(changed, String(sideName).toLowerCase()));\n }\n };\n\n const toggle = (id: string): void => {\n setChecked((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n };\n\n /** Ids checked **and visible** on one side — what the move buttons act on. */\n const checkedOn = (side: TransferSide): string[] =>\n movableIds(visible[side]).filter((id) => checked.has(id));\n\n const checkedSource = checkedOn(\"source\");\n const checkedTarget = checkedOn(\"target\");\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {panes.map((pane) => {\n const shown = visible[pane.side];\n const movable = movableIds(shown);\n const checkedHere = movable.filter((id) => checked.has(id));\n const allChecked = movable.length > 0 && checkedHere.length === movable.length;\n const titleId = `${baseId}-${pane.side}-title`;\n\n return (\n <section key={pane.side} className={styles.pane} aria-labelledby={titleId}>\n <div className={styles.paneHeader}>\n <div className={styles.paneTitle}>\n <Checkbox\n checked={allChecked}\n indeterminate={checkedHere.length > 0 && !allChecked}\n disabled={disabled || movable.length === 0}\n aria-label={\n typeof pane.title === \"string\"\n ? pane.title\n : String(pane.side)\n }\n onChange={() =>\n setChecked((current) => {\n const next = new Set(current);\n for (const id of movable) {\n if (allChecked) next.delete(id);\n else next.add(id);\n }\n return next;\n })\n }\n />\n <h3 className={styles.title} id={titleId}>\n {pane.title}\n </h3>\n </div>\n <span className={styles.count}>\n {strings.selected(checkedHere.length, pane.items.length)}\n </span>\n </div>\n\n {withSearch && (\n <input\n type=\"search\"\n className={styles.search}\n value={queries[pane.side]}\n placeholder={strings.search}\n aria-label={`${strings.search}: ${\n typeof pane.title === \"string\" ? pane.title : pane.side\n }`}\n onChange={(event) =>\n setQueries((current) => ({\n ...current,\n [pane.side]: event.target.value,\n }))\n }\n />\n )}\n\n <ul className={styles.list} style={{ height }}>\n {shown.length === 0 ? (\n <li className={styles.empty}>\n {queries[pane.side] ? strings.noMatches : strings.empty}\n </li>\n ) : (\n shown.map((item) => (\n <li key={item.id} className={styles.row}>\n <Checkbox\n checked={checked.has(item.id)}\n disabled={disabled || item.disabled}\n label={\n renderItem\n ? renderItem(item, pane.side)\n : item.label\n }\n onChange={() => toggle(item.id)}\n onDoubleClick={() =>\n move(\n [item.id],\n pane.side === \"source\" ? \"target\" : \"source\",\n )\n }\n />\n </li>\n ))\n )}\n </ul>\n </section>\n );\n })}\n\n <div className={styles.controls}>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || movableIds(visible.source).length === 0}\n aria-label={strings.allToTarget}\n title={strings.allToTarget}\n onClick={() => move(movableIds(visible.source), \"target\")}\n >\n »\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || checkedSource.length === 0}\n aria-label={strings.toTarget}\n title={strings.toTarget}\n onClick={() => move(checkedSource, \"target\")}\n >\n ›\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || checkedTarget.length === 0}\n aria-label={strings.toSource}\n title={strings.toSource}\n onClick={() => move(checkedTarget, \"source\")}\n >\n ‹\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || movableIds(visible.target).length === 0}\n aria-label={strings.allToSource}\n title={strings.allToSource}\n onClick={() => move(movableIds(visible.target), \"source\")}\n >\n «\n </button>\n </div>\n\n <VisuallyHidden aria-live=\"polite\" role=\"status\">\n {announcement}\n </VisuallyHidden>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Transfer.js","names":[],"sources":["../../../src/components/Transfer/Transfer.tsx"],"sourcesContent":["import { useId, useMemo, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { Checkbox } from \"../Checkbox\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n applyMove,\n filterItems,\n movableIds,\n splitSides,\n transferStrings,\n type TransferItem,\n type TransferSide,\n} from \"./transfer-state\";\nimport styles from \"./Transfer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\" | \"defaultValue\";\n\nexport interface TransferProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The whole catalogue. Both panes are derived from it. */\n items: readonly TransferItem[];\n /** Ids on the target side. Controlled. */\n value: readonly string[];\n /** Next value, always in catalogue order. */\n onChange: (value: string[]) => void;\n /** Heading of the left pane. Default `\"Disponíveis\"`. */\n sourceTitle?: ReactNode;\n /** Heading of the right pane. Default `\"Selecionados\"`. */\n targetTitle?: ReactNode;\n /** Show a search box above each pane. Default `true` past 8 items. */\n searchable?: boolean;\n /** Custom row body. Gets the item and which side it is on. */\n renderItem?: (item: TransferItem, side: TransferSide) => ReactNode;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Height of each pane's scroll area, any CSS length. Default `\"16rem\"`. */\n height?: string;\n /** Block every move. */\n disabled?: boolean;\n}\n\n/**\n * Two panes and a set of move controls: pick a subset out of a catalogue.\n *\n * Controlled on **ids of the target side only**. The panes are derived on every\n * render, never stored, because two stored lists drift the moment the catalogue\n * changes under them — a permission removed upstream lingers in whichever pane\n * held it, and an id in both panes is a bug nobody can see.\n *\n * @example\n * const [roles, setRoles] = useState<string[]>([]);\n *\n * <Transfer\n * items={allRoles}\n * value={roles}\n * onChange={setRoles}\n * sourceTitle=\"Papéis disponíveis\"\n * targetTitle=\"Papéis do usuário\"\n * />\n */\nexport function Transfer({\n items,\n value,\n onChange,\n sourceTitle,\n targetTitle,\n searchable,\n renderItem,\n locale = \"pt-BR\",\n height = \"16rem\",\n disabled = false,\n className,\n ...rest\n}: TransferProps) {\n const strings = transferStrings(locale);\n const baseId = useId();\n const [checked, setChecked] = useState<Set<string>>(new Set());\n const [queries, setQueries] = useState<{ source: string; target: string }>({\n source: \"\",\n target: \"\",\n });\n const [announcement, setAnnouncement] = useState(\"\");\n\n const { source, target } = useMemo(() => splitSides(items, value), [items, value]);\n const withSearch = searchable ?? items.length > 8;\n\n /**\n * What each pane is showing right now — the filter included.\n *\n * Every control acts on this, not on the pane's full contents: filtering to\n * \"sao\" and pressing \"move all\" has to move what you are looking at. Moving\n * rows the filter hid is the kind of surprise that makes people stop trusting\n * the bulk button.\n */\n const visible: Record<TransferSide, TransferItem[]> = {\n source: withSearch ? filterItems(source, queries.source) : [...source],\n target: withSearch ? filterItems(target, queries.target) : [...target],\n };\n\n const panes: Array<{ side: TransferSide; title: ReactNode; items: TransferItem[] }> = [\n { side: \"source\", title: sourceTitle ?? strings.sourceTitle, items: source },\n { side: \"target\", title: targetTitle ?? strings.targetTitle, items: target },\n ];\n\n /**\n * Move ids and drop them from the checked set.\n *\n * Clearing the checks is the point: after a move the rows are on the other\n * side, and leaving them checked means the next click on the opposite button\n * sends them straight back — which reads as the component undoing itself.\n */\n const move = (moving: readonly string[], to: TransferSide): void => {\n if (disabled || moving.length === 0) return;\n const next = applyMove({ value, moving, to, items });\n const changed = to === \"target\" ? next.length - value.length : value.length - next.length;\n onChange(next);\n setChecked((current) => {\n const remaining = new Set(current);\n for (const id of moving) remaining.delete(id);\n return remaining;\n });\n if (changed > 0) {\n const sideName = to === \"target\" ? strings.targetTitle : strings.sourceTitle;\n setAnnouncement(strings.moved(changed, String(sideName).toLowerCase()));\n }\n };\n\n const toggle = (id: string): void => {\n setChecked((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n };\n\n /** Ids checked **and visible** on one side — what the move buttons act on. */\n const checkedOn = (side: TransferSide): string[] =>\n movableIds(visible[side]).filter((id) => checked.has(id));\n\n const checkedSource = checkedOn(\"source\");\n const checkedTarget = checkedOn(\"target\");\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {panes.map((pane) => {\n const shown = visible[pane.side];\n const movable = movableIds(shown);\n const checkedHere = movable.filter((id) => checked.has(id));\n const allChecked = movable.length > 0 && checkedHere.length === movable.length;\n const titleId = `${baseId}-${pane.side}-title`;\n\n return (\n <section key={pane.side} className={styles.pane} aria-labelledby={titleId}>\n <div className={styles.paneHeader}>\n <div className={styles.paneTitle}>\n <Checkbox\n checked={allChecked}\n indeterminate={checkedHere.length > 0 && !allChecked}\n disabled={disabled || movable.length === 0}\n aria-label={\n typeof pane.title === \"string\"\n ? pane.title\n : String(pane.side)\n }\n onChange={() =>\n setChecked((current) => {\n const next = new Set(current);\n for (const id of movable) {\n if (allChecked) next.delete(id);\n else next.add(id);\n }\n return next;\n })\n }\n />\n <h3 className={styles.title} id={titleId}>\n {pane.title}\n </h3>\n </div>\n <span className={styles.count}>\n {strings.selected(checkedHere.length, pane.items.length)}\n </span>\n </div>\n\n {withSearch && (\n <input\n type=\"search\"\n className={styles.search}\n value={queries[pane.side]}\n placeholder={strings.search}\n aria-label={`${strings.search}: ${\n typeof pane.title === \"string\" ? pane.title : pane.side\n }`}\n onChange={(event) =>\n setQueries((current) => ({\n ...current,\n [pane.side]: event.target.value,\n }))\n }\n />\n )}\n\n <ul className={styles.list} style={{ height }}>\n {shown.length === 0 ? (\n <li className={styles.empty}>\n {queries[pane.side] ? strings.noMatches : strings.empty}\n </li>\n ) : (\n shown.map((item) => (\n <li key={item.id} className={styles.row}>\n <Checkbox\n checked={checked.has(item.id)}\n disabled={disabled || item.disabled}\n label={\n renderItem\n ? renderItem(item, pane.side)\n : item.label\n }\n onChange={() => toggle(item.id)}\n onDoubleClick={() =>\n move(\n [item.id],\n pane.side === \"source\" ? \"target\" : \"source\",\n )\n }\n />\n </li>\n ))\n )}\n </ul>\n </section>\n );\n })}\n\n <div className={styles.controls}>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || movableIds(visible.source).length === 0}\n aria-label={strings.allToTarget}\n title={strings.allToTarget}\n onClick={() => move(movableIds(visible.source), \"target\")}\n >\n »\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || checkedSource.length === 0}\n aria-label={strings.toTarget}\n title={strings.toTarget}\n onClick={() => move(checkedSource, \"target\")}\n >\n ›\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || checkedTarget.length === 0}\n aria-label={strings.toSource}\n title={strings.toSource}\n onClick={() => move(checkedTarget, \"source\")}\n >\n ‹\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || movableIds(visible.target).length === 0}\n aria-label={strings.allToSource}\n title={strings.allToSource}\n onClick={() => move(movableIds(visible.target), \"source\")}\n >\n «\n </button>\n </div>\n\n <VisuallyHidden aria-live=\"polite\" role=\"status\">\n {announcement}\n </VisuallyHidden>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Transfer.js","names":[],"sources":["../../../src/components/Transfer/Transfer.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — two lists that hand\n * items to each other: items and value are the model, onChange the move,\n * sourceTitle/targetTitle/renderItem/locale/height the presentation, and\n * searchable/disabled the behaviour. The body keeps both selections and the filter\n * query, and every move reads all three.\n */\nimport { useId, useMemo, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { Checkbox } from \"../Checkbox\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n applyMove,\n filterItems,\n movableIds,\n splitSides,\n transferStrings,\n type TransferItem,\n type TransferSide,\n} from \"./transfer-state\";\nimport styles from \"./Transfer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\" | \"defaultValue\";\n\nexport interface TransferProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The whole catalogue. Both panes are derived from it. */\n items: readonly TransferItem[];\n /** Ids on the target side. Controlled. */\n value: readonly string[];\n /** Next value, always in catalogue order. */\n onChange: (value: string[]) => void;\n /** Heading of the left pane. Default `\"Disponíveis\"`. */\n sourceTitle?: ReactNode;\n /** Heading of the right pane. Default `\"Selecionados\"`. */\n targetTitle?: ReactNode;\n /** Show a search box above each pane. Default `true` past 8 items. */\n searchable?: boolean;\n /** Custom row body. Gets the item and which side it is on. */\n renderItem?: (item: TransferItem, side: TransferSide) => ReactNode;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Height of each pane's scroll area, any CSS length. Default `\"16rem\"`. */\n height?: string;\n /** Block every move. */\n disabled?: boolean;\n}\n\n/**\n * Two panes and a set of move controls: pick a subset out of a catalogue.\n *\n * Controlled on **ids of the target side only**. The panes are derived on every\n * render, never stored, because two stored lists drift the moment the catalogue\n * changes under them — a permission removed upstream lingers in whichever pane\n * held it, and an id in both panes is a bug nobody can see.\n *\n * @example\n * const [roles, setRoles] = useState<string[]>([]);\n *\n * <Transfer\n * items={allRoles}\n * value={roles}\n * onChange={setRoles}\n * sourceTitle=\"Papéis disponíveis\"\n * targetTitle=\"Papéis do usuário\"\n * />\n */\nexport function Transfer({\n items,\n value,\n onChange,\n sourceTitle,\n targetTitle,\n searchable,\n renderItem,\n locale = \"pt-BR\",\n height = \"16rem\",\n disabled = false,\n className,\n ...rest\n}: TransferProps) {\n const strings = transferStrings(locale);\n const baseId = useId();\n const [checked, setChecked] = useState<Set<string>>(new Set());\n const [queries, setQueries] = useState<{ source: string; target: string }>({\n source: \"\",\n target: \"\",\n });\n const [announcement, setAnnouncement] = useState(\"\");\n\n const { source, target } = useMemo(() => splitSides(items, value), [items, value]);\n const withSearch = searchable ?? items.length > 8;\n\n /**\n * What each pane is showing right now — the filter included.\n *\n * Every control acts on this, not on the pane's full contents: filtering to\n * \"sao\" and pressing \"move all\" has to move what you are looking at. Moving\n * rows the filter hid is the kind of surprise that makes people stop trusting\n * the bulk button.\n */\n const visible: Record<TransferSide, TransferItem[]> = {\n source: withSearch ? filterItems(source, queries.source) : [...source],\n target: withSearch ? filterItems(target, queries.target) : [...target],\n };\n\n const panes: Array<{ side: TransferSide; title: ReactNode; items: TransferItem[] }> = [\n { side: \"source\", title: sourceTitle ?? strings.sourceTitle, items: source },\n { side: \"target\", title: targetTitle ?? strings.targetTitle, items: target },\n ];\n\n /**\n * Move ids and drop them from the checked set.\n *\n * Clearing the checks is the point: after a move the rows are on the other\n * side, and leaving them checked means the next click on the opposite button\n * sends them straight back — which reads as the component undoing itself.\n */\n const move = (moving: readonly string[], to: TransferSide): void => {\n if (disabled || moving.length === 0) return;\n const next = applyMove({ value, moving, to, items });\n const changed = to === \"target\" ? next.length - value.length : value.length - next.length;\n onChange(next);\n setChecked((current) => {\n const remaining = new Set(current);\n for (const id of moving) remaining.delete(id);\n return remaining;\n });\n if (changed > 0) {\n const sideName = to === \"target\" ? strings.targetTitle : strings.sourceTitle;\n setAnnouncement(strings.moved(changed, String(sideName).toLowerCase()));\n }\n };\n\n const toggle = (id: string): void => {\n setChecked((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n };\n\n /** Ids checked **and visible** on one side — what the move buttons act on. */\n const checkedOn = (side: TransferSide): string[] =>\n movableIds(visible[side]).filter((id) => checked.has(id));\n\n const checkedSource = checkedOn(\"source\");\n const checkedTarget = checkedOn(\"target\");\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {panes.map((pane) => {\n const shown = visible[pane.side];\n const movable = movableIds(shown);\n const checkedHere = movable.filter((id) => checked.has(id));\n const allChecked = movable.length > 0 && checkedHere.length === movable.length;\n const titleId = `${baseId}-${pane.side}-title`;\n\n return (\n <section key={pane.side} className={styles.pane} aria-labelledby={titleId}>\n <div className={styles.paneHeader}>\n <div className={styles.paneTitle}>\n <Checkbox\n checked={allChecked}\n indeterminate={checkedHere.length > 0 && !allChecked}\n disabled={disabled || movable.length === 0}\n aria-label={\n typeof pane.title === \"string\"\n ? pane.title\n : String(pane.side)\n }\n onChange={() =>\n setChecked((current) => {\n const next = new Set(current);\n for (const id of movable) {\n if (allChecked) next.delete(id);\n else next.add(id);\n }\n return next;\n })\n }\n />\n <h3 className={styles.title} id={titleId}>\n {pane.title}\n </h3>\n </div>\n <span className={styles.count}>\n {strings.selected(checkedHere.length, pane.items.length)}\n </span>\n </div>\n\n {withSearch && (\n <input\n type=\"search\"\n className={styles.search}\n value={queries[pane.side]}\n placeholder={strings.search}\n aria-label={`${strings.search}: ${\n typeof pane.title === \"string\" ? pane.title : pane.side\n }`}\n onChange={(event) =>\n setQueries((current) => ({\n ...current,\n [pane.side]: event.target.value,\n }))\n }\n />\n )}\n\n <ul className={styles.list} style={{ height }}>\n {shown.length === 0 ? (\n <li className={styles.empty}>\n {queries[pane.side] ? strings.noMatches : strings.empty}\n </li>\n ) : (\n shown.map((item) => (\n <li key={item.id} className={styles.row}>\n <Checkbox\n checked={checked.has(item.id)}\n disabled={disabled || item.disabled}\n label={\n renderItem\n ? renderItem(item, pane.side)\n : item.label\n }\n onChange={() => toggle(item.id)}\n onDoubleClick={() =>\n move(\n [item.id],\n pane.side === \"source\" ? \"target\" : \"source\",\n )\n }\n />\n </li>\n ))\n )}\n </ul>\n </section>\n );\n })}\n\n <div className={styles.controls}>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || movableIds(visible.source).length === 0}\n aria-label={strings.allToTarget}\n title={strings.allToTarget}\n onClick={() => move(movableIds(visible.source), \"target\")}\n >\n »\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || checkedSource.length === 0}\n aria-label={strings.toTarget}\n title={strings.toTarget}\n onClick={() => move(checkedSource, \"target\")}\n >\n ›\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || checkedTarget.length === 0}\n aria-label={strings.toSource}\n title={strings.toSource}\n onClick={() => move(checkedTarget, \"source\")}\n >\n ‹\n </button>\n <button\n type=\"button\"\n className={styles.control}\n disabled={disabled || movableIds(visible.target).length === 0}\n aria-label={strings.allToSource}\n title={strings.allToSource}\n onClick={() => move(movableIds(visible.target), \"source\")}\n >\n «\n </button>\n </div>\n\n <VisuallyHidden aria-live=\"polite\" role=\"status\">\n {announcement}\n </VisuallyHidden>\n </div>\n );\n}\n"],"mappings":"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"}