tempest-react-sdk 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/bin/lib/css/findings.mjs +1 -0
- package/bin/lib/css/semantic.mjs +70 -6
- package/bin/lib/css/semantic.test.mjs +52 -1
- package/bin/lib/icons/generate.test.mjs +4 -4
- package/dist/br/geocode.cjs +1 -1
- package/dist/br/geocode.cjs.map +1 -1
- package/dist/br/geocode.js +1 -1
- package/dist/br/geocode.js.map +1 -1
- package/dist/capture/use-barcode-scanner.cjs +1 -1
- package/dist/capture/use-barcode-scanner.cjs.map +1 -1
- package/dist/capture/use-barcode-scanner.js +1 -1
- package/dist/capture/use-barcode-scanner.js.map +1 -1
- package/dist/components/AIChat/AIChat.cjs +1 -1
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js +1 -1
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
- package/dist/components/AIChat/AIChatTurn.js +1 -1
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/Alert/Alert.module.cjs.map +1 -1
- package/dist/components/Alert/Alert.module.js.map +1 -1
- package/dist/components/AppBar/use-sticky-body-warning.cjs +1 -1
- package/dist/components/AppBar/use-sticky-body-warning.cjs.map +1 -1
- package/dist/components/AppBar/use-sticky-body-warning.js +1 -1
- package/dist/components/AppBar/use-sticky-body-warning.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.js +2 -2
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
- package/dist/components/Badge/Badge.module.cjs.map +1 -1
- package/dist/components/Badge/Badge.module.js.map +1 -1
- package/dist/components/BarList/BarList.module.cjs.map +1 -1
- package/dist/components/BarList/BarList.module.js.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.module.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.module.js.map +1 -1
- package/dist/components/Chat/Chat.cjs +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js +1 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/Dropzone/Dropzone.module.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.module.js.map +1 -1
- package/dist/components/ErrorText/ErrorText.module.cjs.map +1 -1
- package/dist/components/ErrorText/ErrorText.module.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.cjs +1 -1
- package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.js +1 -1
- package/dist/components/FilterBar/FilterBar.js.map +1 -1
- package/dist/components/FilterBar/filter-apply.cjs +1 -1
- package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
- package/dist/components/FilterBar/filter-apply.js +1 -1
- package/dist/components/FilterBar/filter-apply.js.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.js +1 -1
- package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
- package/dist/components/Markdown/Markdown.module.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.module.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.module.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.module.js.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.module.cjs.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.module.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.js.map +1 -1
- package/dist/components/StepperInput/StepperInput.module.cjs.map +1 -1
- package/dist/components/StepperInput/StepperInput.module.js.map +1 -1
- package/dist/components/Tag/Tag.module.cjs.map +1 -1
- package/dist/components/Tag/Tag.module.js.map +1 -1
- package/dist/components/Timeline/Timeline.module.cjs.map +1 -1
- package/dist/components/Timeline/Timeline.module.js.map +1 -1
- package/dist/components/Toggle/Toggle.module.cjs.map +1 -1
- package/dist/components/Toggle/Toggle.module.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.js.map +1 -1
- package/dist/components/VideoPlayer/VideoPlayer.cjs +1 -1
- package/dist/components/VideoPlayer/VideoPlayer.cjs.map +1 -1
- package/dist/components/VideoPlayer/VideoPlayer.js +2 -2
- package/dist/components/VideoPlayer/VideoPlayer.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js +1 -1
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/feature-flags/growthbook-adapter.cjs +1 -1
- package/dist/feature-flags/growthbook-adapter.cjs.map +1 -1
- package/dist/feature-flags/growthbook-adapter.js +1 -1
- package/dist/feature-flags/growthbook-adapter.js.map +1 -1
- package/dist/hooks/use-focus-trap.cjs +1 -1
- package/dist/hooks/use-focus-trap.cjs.map +1 -1
- package/dist/hooks/use-focus-trap.js +1 -1
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-push-to-talk.cjs +1 -1
- package/dist/hooks/use-push-to-talk.cjs.map +1 -1
- package/dist/hooks/use-push-to-talk.js +1 -1
- package/dist/hooks/use-push-to-talk.js.map +1 -1
- package/dist/http/describe-api-error.cjs +1 -1
- package/dist/http/describe-api-error.cjs.map +1 -1
- package/dist/http/describe-api-error.js +12 -8
- package/dist/http/describe-api-error.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +21 -16
- package/dist/http/errors.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +1 -1
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/icons/generated/aliases.cjs +1 -1
- package/dist/icons/generated/aliases.cjs.map +1 -1
- package/dist/icons/generated/aliases.js +1 -0
- package/dist/icons/generated/aliases.js.map +1 -1
- package/dist/icons/generated/icon-names.cjs +1 -1
- package/dist/icons/generated/icon-names.cjs.map +1 -1
- package/dist/icons/generated/icon-names.js +1 -1
- package/dist/icons/generated/icon-names.js.map +1 -1
- package/dist/icons/generated/loaders.cjs +1 -1
- package/dist/icons/generated/loaders.cjs.map +1 -1
- package/dist/icons/generated/loaders.js +48 -42
- package/dist/icons/generated/loaders.js.map +1 -1
- package/dist/icons/generated/shard-02.cjs +1 -1
- package/dist/icons/generated/shard-02.cjs.map +1 -1
- package/dist/icons/generated/shard-02.js +20 -20
- package/dist/icons/generated/shard-02.js.map +1 -1
- package/dist/icons/generated/shard-03.cjs +1 -1
- package/dist/icons/generated/shard-03.cjs.map +1 -1
- package/dist/icons/generated/shard-03.js +41 -41
- package/dist/icons/generated/shard-03.js.map +1 -1
- package/dist/icons/generated/shard-04.cjs +1 -1
- package/dist/icons/generated/shard-04.cjs.map +1 -1
- package/dist/icons/generated/shard-04.js +41 -41
- package/dist/icons/generated/shard-04.js.map +1 -1
- package/dist/icons/generated/shard-05.cjs +1 -1
- package/dist/icons/generated/shard-05.cjs.map +1 -1
- package/dist/icons/generated/shard-05.js +41 -41
- package/dist/icons/generated/shard-05.js.map +1 -1
- package/dist/icons/generated/shard-06.cjs +1 -1
- package/dist/icons/generated/shard-06.cjs.map +1 -1
- package/dist/icons/generated/shard-06.js +41 -41
- package/dist/icons/generated/shard-06.js.map +1 -1
- package/dist/icons/generated/shard-07.cjs +1 -1
- package/dist/icons/generated/shard-07.cjs.map +1 -1
- package/dist/icons/generated/shard-07.js +41 -41
- package/dist/icons/generated/shard-07.js.map +1 -1
- package/dist/icons/generated/shard-08.cjs +1 -1
- package/dist/icons/generated/shard-08.cjs.map +1 -1
- package/dist/icons/generated/shard-08.js +41 -41
- package/dist/icons/generated/shard-08.js.map +1 -1
- package/dist/icons/generated/shard-09.cjs +1 -1
- package/dist/icons/generated/shard-09.cjs.map +1 -1
- package/dist/icons/generated/shard-09.js +41 -41
- package/dist/icons/generated/shard-09.js.map +1 -1
- package/dist/icons/generated/shard-10.cjs +1 -1
- package/dist/icons/generated/shard-10.cjs.map +1 -1
- package/dist/icons/generated/shard-10.js +41 -41
- package/dist/icons/generated/shard-10.js.map +1 -1
- package/dist/icons/generated/shard-11.cjs +1 -1
- package/dist/icons/generated/shard-11.cjs.map +1 -1
- package/dist/icons/generated/shard-11.js +41 -41
- package/dist/icons/generated/shard-11.js.map +1 -1
- package/dist/icons/generated/shard-12.cjs +1 -1
- package/dist/icons/generated/shard-12.cjs.map +1 -1
- package/dist/icons/generated/shard-12.js +41 -41
- package/dist/icons/generated/shard-12.js.map +1 -1
- package/dist/icons/generated/shard-13.cjs +1 -1
- package/dist/icons/generated/shard-13.cjs.map +1 -1
- package/dist/icons/generated/shard-13.js +41 -41
- package/dist/icons/generated/shard-13.js.map +1 -1
- package/dist/icons/generated/shard-14.cjs +1 -1
- package/dist/icons/generated/shard-14.cjs.map +1 -1
- package/dist/icons/generated/shard-14.js +41 -41
- package/dist/icons/generated/shard-14.js.map +1 -1
- package/dist/icons/generated/shard-15.cjs +1 -1
- package/dist/icons/generated/shard-15.cjs.map +1 -1
- package/dist/icons/generated/shard-15.js +41 -41
- package/dist/icons/generated/shard-15.js.map +1 -1
- package/dist/icons/generated/shard-16.cjs +1 -1
- package/dist/icons/generated/shard-16.cjs.map +1 -1
- package/dist/icons/generated/shard-16.js +41 -41
- package/dist/icons/generated/shard-16.js.map +1 -1
- package/dist/icons/generated/shard-17.cjs +1 -1
- package/dist/icons/generated/shard-17.cjs.map +1 -1
- package/dist/icons/generated/shard-17.js +41 -41
- package/dist/icons/generated/shard-17.js.map +1 -1
- package/dist/icons/generated/shard-18.cjs +1 -1
- package/dist/icons/generated/shard-18.cjs.map +1 -1
- package/dist/icons/generated/shard-18.js +41 -41
- package/dist/icons/generated/shard-18.js.map +1 -1
- package/dist/icons/generated/shard-19.cjs +1 -1
- package/dist/icons/generated/shard-19.cjs.map +1 -1
- package/dist/icons/generated/shard-19.js +41 -41
- package/dist/icons/generated/shard-19.js.map +1 -1
- package/dist/icons/generated/shard-20.cjs +1 -1
- package/dist/icons/generated/shard-20.cjs.map +1 -1
- package/dist/icons/generated/shard-20.js +41 -41
- package/dist/icons/generated/shard-20.js.map +1 -1
- package/dist/icons/generated/shard-21.cjs +1 -1
- package/dist/icons/generated/shard-21.cjs.map +1 -1
- package/dist/icons/generated/shard-21.js +41 -41
- package/dist/icons/generated/shard-21.js.map +1 -1
- package/dist/icons/generated/shard-22.cjs +1 -1
- package/dist/icons/generated/shard-22.cjs.map +1 -1
- package/dist/icons/generated/shard-22.js +41 -41
- package/dist/icons/generated/shard-22.js.map +1 -1
- package/dist/icons/generated/shard-23.cjs +1 -1
- package/dist/icons/generated/shard-23.cjs.map +1 -1
- package/dist/icons/generated/shard-23.js +41 -41
- package/dist/icons/generated/shard-23.js.map +1 -1
- package/dist/icons/generated/shard-24.cjs +1 -1
- package/dist/icons/generated/shard-24.cjs.map +1 -1
- package/dist/icons/generated/shard-24.js +41 -41
- package/dist/icons/generated/shard-24.js.map +1 -1
- package/dist/icons/generated/shard-25.cjs +1 -1
- package/dist/icons/generated/shard-25.cjs.map +1 -1
- package/dist/icons/generated/shard-25.js +41 -41
- package/dist/icons/generated/shard-25.js.map +1 -1
- package/dist/icons/generated/shard-26.cjs +1 -1
- package/dist/icons/generated/shard-26.cjs.map +1 -1
- package/dist/icons/generated/shard-26.js +41 -41
- package/dist/icons/generated/shard-26.js.map +1 -1
- package/dist/icons/generated/shard-27.cjs +1 -1
- package/dist/icons/generated/shard-27.cjs.map +1 -1
- package/dist/icons/generated/shard-27.js +41 -41
- package/dist/icons/generated/shard-27.js.map +1 -1
- package/dist/icons/generated/shard-28.cjs +1 -1
- package/dist/icons/generated/shard-28.cjs.map +1 -1
- package/dist/icons/generated/shard-28.js +41 -41
- package/dist/icons/generated/shard-28.js.map +1 -1
- package/dist/icons/generated/shard-29.cjs +1 -1
- package/dist/icons/generated/shard-29.cjs.map +1 -1
- package/dist/icons/generated/shard-29.js +41 -41
- package/dist/icons/generated/shard-29.js.map +1 -1
- package/dist/icons/generated/shard-30.cjs +1 -1
- package/dist/icons/generated/shard-30.cjs.map +1 -1
- package/dist/icons/generated/shard-30.js +41 -41
- package/dist/icons/generated/shard-30.js.map +1 -1
- package/dist/icons/generated/shard-31.cjs +1 -1
- package/dist/icons/generated/shard-31.cjs.map +1 -1
- package/dist/icons/generated/shard-31.js +41 -41
- package/dist/icons/generated/shard-31.js.map +1 -1
- package/dist/icons/generated/shard-32.cjs +1 -1
- package/dist/icons/generated/shard-32.cjs.map +1 -1
- package/dist/icons/generated/shard-32.js +41 -41
- package/dist/icons/generated/shard-32.js.map +1 -1
- package/dist/icons/generated/shard-33.cjs +1 -1
- package/dist/icons/generated/shard-33.cjs.map +1 -1
- package/dist/icons/generated/shard-33.js +41 -41
- package/dist/icons/generated/shard-33.js.map +1 -1
- package/dist/icons/generated/shard-34.cjs +1 -1
- package/dist/icons/generated/shard-34.cjs.map +1 -1
- package/dist/icons/generated/shard-34.js +41 -41
- package/dist/icons/generated/shard-34.js.map +1 -1
- package/dist/icons/generated/shard-35.cjs +1 -1
- package/dist/icons/generated/shard-35.cjs.map +1 -1
- package/dist/icons/generated/shard-35.js +41 -41
- package/dist/icons/generated/shard-35.js.map +1 -1
- package/dist/icons/generated/shard-36.cjs +1 -1
- package/dist/icons/generated/shard-36.cjs.map +1 -1
- package/dist/icons/generated/shard-36.js +41 -41
- package/dist/icons/generated/shard-36.js.map +1 -1
- package/dist/icons/generated/shard-37.cjs +1 -1
- package/dist/icons/generated/shard-37.cjs.map +1 -1
- package/dist/icons/generated/shard-37.js +41 -41
- package/dist/icons/generated/shard-37.js.map +1 -1
- package/dist/icons/generated/shard-38.cjs +1 -1
- package/dist/icons/generated/shard-38.cjs.map +1 -1
- package/dist/icons/generated/shard-38.js +41 -41
- package/dist/icons/generated/shard-38.js.map +1 -1
- package/dist/icons/generated/shard-39.cjs +1 -1
- package/dist/icons/generated/shard-39.cjs.map +1 -1
- package/dist/icons/generated/shard-39.js +41 -41
- package/dist/icons/generated/shard-39.js.map +1 -1
- package/dist/icons/generated/shard-40.cjs +1 -1
- package/dist/icons/generated/shard-40.cjs.map +1 -1
- package/dist/icons/generated/shard-40.js +41 -41
- package/dist/icons/generated/shard-40.js.map +1 -1
- package/dist/icons/generated/shard-41.cjs +1 -1
- package/dist/icons/generated/shard-41.cjs.map +1 -1
- package/dist/icons/generated/shard-41.js +41 -41
- package/dist/icons/generated/shard-41.js.map +1 -1
- package/dist/icons/generated/shard-42.cjs +1 -1
- package/dist/icons/generated/shard-42.cjs.map +1 -1
- package/dist/icons/generated/shard-42.js +41 -41
- package/dist/icons/generated/shard-42.js.map +1 -1
- package/dist/icons/generated/shard-43.cjs +1 -1
- package/dist/icons/generated/shard-43.cjs.map +1 -1
- package/dist/icons/generated/shard-43.js +41 -41
- package/dist/icons/generated/shard-43.js.map +1 -1
- package/dist/icons/generated/shard-44.cjs +1 -1
- package/dist/icons/generated/shard-44.cjs.map +1 -1
- package/dist/icons/generated/shard-44.js +43 -10
- package/dist/icons/generated/shard-44.js.map +1 -1
- package/dist/icons/generated/shard-45.cjs +2 -0
- package/dist/icons/generated/shard-45.cjs.map +1 -0
- package/dist/icons/generated/shard-45.js +15 -0
- package/dist/icons/generated/shard-45.js.map +1 -0
- package/dist/icons/material-symbols.cjs +1 -1
- package/dist/icons/material-symbols.cjs.map +1 -1
- package/dist/icons/material-symbols.js +3 -3
- package/dist/icons/material-symbols.js.map +1 -1
- package/dist/icons.d.ts +4 -4
- package/dist/query/foreign-client-warning.cjs +1 -1
- package/dist/query/foreign-client-warning.cjs.map +1 -1
- package/dist/query/foreign-client-warning.js +1 -1
- package/dist/query/foreign-client-warning.js.map +1 -1
- package/dist/styles/Alert.css +1 -1
- package/dist/styles/Badge.css +1 -1
- package/dist/styles/BarList.css +3 -3
- package/dist/styles/BottomSheet.css +1 -1
- package/dist/styles/DataTable.css +1 -1
- package/dist/styles/Dropzone.css +1 -1
- package/dist/styles/ErrorText.css +1 -1
- package/dist/styles/Markdown.css +1 -1
- package/dist/styles/PasswordInput.css +1 -1
- package/dist/styles/SegmentedControl.css +1 -1
- package/dist/styles/Sidebar.css +2 -2
- package/dist/styles/StepperInput.css +1 -1
- package/dist/styles/Tag.css +1 -1
- package/dist/styles/Timeline.css +1 -1
- package/dist/styles/Toggle.css +1 -1
- package/dist/styles/ToggleGroup.css +1 -1
- package/dist/styles/advanced.css +2 -2
- package/dist/styles/core.css +4 -2
- package/dist/styles/data.css +6 -6
- package/dist/styles/feedback.css +3 -3
- package/dist/styles/forms.css +3 -3
- package/dist/styles/navigation.css +3 -3
- package/dist/styles/overlay.css +1 -1
- package/dist/styles/utility.css +1 -1
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +81 -7
- package/dist/tempest-react-sdk.js +347 -346
- package/dist/theme/create-theme.cjs +3 -3
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +15 -14
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/utils/dev-mode.cjs +1 -1
- package/dist/utils/dev-mode.cjs.map +1 -1
- package/dist/utils/dev-mode.js +7 -2
- package/dist/utils/dev-mode.js.map +1 -1
- package/dist/utils/json-frame.cjs +1 -1
- package/dist/utils/json-frame.cjs.map +1 -1
- package/dist/utils/json-frame.js +1 -1
- package/dist/utils/json-frame.js.map +1 -1
- package/dist/vision/core/providers.cjs +1 -1
- package/dist/vision/core/providers.cjs.map +1 -1
- package/dist/vision/core/providers.js +22 -3
- package/dist/vision/core/providers.js.map +1 -1
- package/dist/vision/core/session.cjs +1 -1
- package/dist/vision/core/session.cjs.map +1 -1
- package/dist/vision/core/session.js +22 -15
- package/dist/vision/core/session.js.map +1 -1
- package/dist/vision/fusion.cjs.map +1 -1
- package/dist/vision/fusion.js.map +1 -1
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +17 -16
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/normalization.cjs +2 -0
- package/dist/vision/normalization.cjs.map +1 -0
- package/dist/vision/normalization.js +49 -0
- package/dist/vision/normalization.js.map +1 -0
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +8 -2
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js +36 -32
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision.cjs +1 -1
- package/dist/vision.d.ts +218 -13
- package/dist/vision.js +19 -18
- package/dist/vite/tempest-pwa-manifest.cjs +1 -1
- package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
- package/dist/vite/tempest-pwa-manifest.js +1 -1
- package/dist/vite/tempest-pwa-manifest.js.map +1 -1
- package/dist/webrtc/peer-link.cjs +1 -1
- package/dist/webrtc/peer-link.cjs.map +1 -1
- package/dist/webrtc/peer-link.js +1 -1
- package/dist/webrtc/peer-link.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +1 -1
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +1 -1
- package/dist/ws/create-web-socket.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioPlayer.cjs","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — src accepts a URL or a Blob, and\n * everything else is a facet of playing it: durationMs (because MediaRecorder WebM\n * carries none), sinkId (output device), autoPlay/loop, onEnded/onError, plus the\n * actions slot and locale for the labels. The body is the transport state machine —\n * object-URL lifecycle, seek, and the rAF-free time updates — which shares one audio\n * element ref throughout.\n */\nimport { Pause, Play } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\n\nimport { setAudioOutput } from \"@/audio/audio-output\";\nimport { formatDuration } from \"@/audio/duration\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./AudioPlayer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface AudioPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a page that records twenty notes does not leak twenty\n * URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useAudioRecorder` always does.\n * See the note on the seek bar for why the element's own `duration` is not\n * trustworthy for a fresh recording.\n */\n durationMs?: number;\n /** Output device, from `useMediaDevices().audioOutputs`. Chromium only. */\n sinkId?: string;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the right of the times — a download button, a delete button. */\n actions?: React.ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": { play: \"Tocar\", pause: \"Pausar\", seek: \"Posição\" },\n en: { play: \"Play\", pause: \"Pause\", seek: \"Seek\" },\n} as const;\n\n/**\n * Playback transport for one clip: play/pause, a seek bar, elapsed and total time.\n *\n * Built around a real `<audio>` element rather than the SDK's `createAudioPlayer`,\n * which is a fire-and-forget handle for notification chimes and has no transport to\n * expose. Accepts a `Blob` directly, because the thing an app most often plays is the\n * recording it just made.\n *\n * The seek bar is a bare `<input type=\"range\">` rather than the SDK's `Slider`: that\n * component is a form field, with a label row and a value badge, and a transport wants\n * neither. The native input keeps the keyboard and screen-reader behaviour that\n * matters here for free.\n *\n * @example\n * const rec = useAudioRecorder(mic.stream);\n * {rec.recording && (\n * <AudioPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function AudioPlayer({\n src,\n durationMs,\n sinkId,\n autoPlay = false,\n loop = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioPlayerProps) {\n const strings = STRINGS[locale];\n const audio = useRef<HTMLAudioElement | null>(null);\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const probed = useRef(false);\n\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n /**\n * Total length, preferring what the caller knows.\n *\n * `MediaRecorder` writes WebM with no duration in the header, so a fresh recording\n * reports `Infinity` — which is why the recorder keeps its own clock and passes it\n * here. `durationMs` wins whenever it is finite; the element's value is the\n * fallback for a plain URL.\n */\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n useEffect(() => {\n if (sinkId === undefined) return;\n void setAudioOutput(audio.current, sinkId);\n }, [sinkId, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after which\n * it knows the real length. It is a hack with no alternative — the header simply\n * does not carry the value — and it runs at most once per source, only when the\n * caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = audio.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onSeeked = (): void => {\n node.removeEventListener(\"timeupdate\", onSeeked);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onSeeked);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = audio.current;\n if (!node || !url) return;\n if (node.paused) {\n void node\n .play()\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = audio.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n return (\n <div className={cn(styles.player, className)} {...rest}>\n <audio\n ref={audio}\n src={url ?? undefined}\n loop={loop}\n autoPlay={autoPlay}\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = audio.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onEnded={() => {\n setPlaying(false);\n setCurrentMs(0);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n />\n\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AudioPlayer.cjs","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — src accepts a URL or a Blob, and\n * everything else is a facet of playing it: durationMs (because MediaRecorder WebM\n * carries none), sinkId (output device), autoPlay/loop, onEnded/onError, plus the\n * actions slot and locale for the labels. The body is the transport state machine —\n * object-URL lifecycle, seek, and the rAF-free time updates — which shares one audio\n * element ref throughout.\n */\nimport { Pause, Play } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\n\nimport { setAudioOutput } from \"@/audio/audio-output\";\nimport { formatDuration } from \"@/audio/duration\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./AudioPlayer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface AudioPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a page that records twenty notes does not leak twenty\n * URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useAudioRecorder` always does.\n * See the note on the seek bar for why the element's own `duration` is not\n * trustworthy for a fresh recording.\n */\n durationMs?: number;\n /** Output device, from `useMediaDevices().audioOutputs`. Chromium only. */\n sinkId?: string;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the right of the times — a download button, a delete button. */\n actions?: React.ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": { play: \"Tocar\", pause: \"Pausar\", seek: \"Posição\" },\n en: { play: \"Play\", pause: \"Pause\", seek: \"Seek\" },\n} as const;\n\n/**\n * Playback transport for one clip: play/pause, a seek bar, elapsed and total time.\n *\n * Built around a real `<audio>` element rather than the SDK's `createAudioPlayer`,\n * which is a fire-and-forget handle for notification chimes and has no transport to\n * expose. Accepts a `Blob` directly, because the thing an app most often plays is the\n * recording it just made.\n *\n * The seek bar is a bare `<input type=\"range\">` rather than the SDK's `Slider`: that\n * component is a form field, with a label row and a value badge, and a transport wants\n * neither. The native input keeps the keyboard and screen-reader behaviour that\n * matters here for free.\n *\n * @example\n * const rec = useAudioRecorder(mic.stream);\n * {rec.recording && (\n * <AudioPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function AudioPlayer({\n src,\n durationMs,\n sinkId,\n autoPlay = false,\n loop = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioPlayerProps) {\n const strings = STRINGS[locale];\n const audio = useRef<HTMLAudioElement | null>(null);\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const probed = useRef(false);\n\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n /**\n * Total length, preferring what the caller knows.\n *\n * `MediaRecorder` writes WebM with no duration in the header, so a fresh recording\n * reports `Infinity` — which is why the recorder keeps its own clock and passes it\n * here. `durationMs` wins whenever it is finite; the element's value is the\n * fallback for a plain URL.\n */\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n useEffect(() => {\n if (sinkId === undefined) return;\n void setAudioOutput(audio.current, sinkId);\n }, [sinkId, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after which\n * it knows the real length. It is a hack with no alternative — the header simply\n * does not carry the value — and it runs at most once per source, only when the\n * caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = audio.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onSeeked = (): void => {\n node.removeEventListener(\"timeupdate\", onSeeked);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onSeeked);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = audio.current;\n if (!node || !url) return;\n if (node.paused) {\n void node\n .play()\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = audio.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n return (\n <div className={cn(styles.player, className)} {...rest}>\n <audio\n ref={audio}\n src={url ?? undefined}\n loop={loop}\n autoPlay={autoPlay}\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = audio.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onEnded={() => {\n setPlaying(false);\n setCurrentMs(0);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n />\n\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n );\n}\n"],"mappings":"wRAwDA,IAAM,EAAU,CACZ,QAAS,CAAE,KAAM,QAAS,MAAO,SAAU,KAAM,SAAU,EAC3D,GAAI,CAAE,KAAM,OAAQ,MAAO,QAAS,KAAM,MAAO,CACrD,EAqBA,SAAgB,EAAY,CACxB,MACA,aACA,SACA,WAAW,GACX,OAAO,GACP,SAAS,QACT,UACA,UACA,UACA,WAAW,GACX,YACA,GAAG,GACc,CACjB,IAAM,EAAU,EAAQ,GAClB,GAAA,EAAQ,EAAA,OAAA,CAAgC,IAAI,EAC5C,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAS,EAAK,EACtC,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,CAAS,CAAC,EACtC,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,CAAS,GAAwB,EAC7D,GAAA,EAAS,EAAA,OAAA,CAAO,EAAK,EAErB,EAAU,EAAA,aAAa,OAAO,GAAQ,SAAW,KAAO,CAAG,EAC3D,EAAM,OAAO,GAAQ,SAAW,EAAM,EAUtC,EAAU,OAAO,SAAS,CAAU,EAAK,EAAwB,EACjE,EAAW,OAAO,SAAS,CAAO,GAAK,EAAU,GAEvD,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAa,CAAC,EACd,EAAW,EAAK,EAChB,EAAa,GAAwB,EACrC,EAAO,QAAU,EACrB,EAAG,CAAC,CAAG,CAAC,GAER,EAAA,EAAA,UAAA,KAAgB,CACR,IAAW,IAAA,IACf,EAAK,eAAe,EAAM,QAAS,CAAM,CAC7C,EAAG,CAAC,EAAQ,CAAG,CAAC,EAUhB,IAAM,MAA6B,CAC/B,IAAM,EAAO,EAAM,QACnB,GAAI,CAAC,EAAM,OACX,GAAI,OAAO,SAAS,EAAK,QAAQ,EAAG,CAChC,EAAa,EAAK,SAAW,GAAI,EACjC,MACJ,CACA,GAAI,EAAO,SAAW,OAAO,SAAS,CAAU,EAAG,OACnD,EAAO,QAAU,GACjB,IAAM,MAAuB,CACzB,EAAK,oBAAoB,aAAc,CAAQ,EAC3C,OAAO,SAAS,EAAK,QAAQ,GAAG,EAAa,EAAK,SAAW,GAAI,EACrE,EAAK,YAAc,CACvB,EACA,EAAK,iBAAiB,aAAc,CAAQ,EAC5C,EAAK,YAAc,KACvB,EAEM,MAAqB,CACvB,IAAM,EAAO,EAAM,QACf,GAAC,GAAS,EACd,IAAI,EAAK,OAAQ,CACb,EACK,KAAK,CAAC,CACN,SAAW,EAAW,EAAI,CAAC,CAAC,CAC5B,MAAO,GAAmB,IAAU,CAAK,CAAC,EAC/C,MACJ,CACA,EAAK,MAAM,EACX,EAAW,EAAK,CAFhB,CAGJ,EAEM,EAAQ,GAAqB,CAC/B,IAAM,EAAO,EAAM,QACd,GAAS,IACd,EAAK,YAAc,EAAK,IACxB,EAAa,CAAE,EACnB,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,CAAS,EAAG,GAAI,EAAlD,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,IAAK,EACL,IAAK,GAAO,IAAA,GACN,OACI,WACV,QAAQ,WACR,iBAAkB,EAClB,iBAAkB,EAClB,iBAAoB,CAChB,IAAM,EAAO,EAAM,QACf,GAAM,EAAa,EAAK,YAAc,GAAI,CAClD,EACA,WAAc,EAAW,EAAI,EAC7B,YAAe,EAAW,EAAK,EAC/B,YAAe,CACX,EAAW,EAAK,EAChB,EAAa,CAAC,EACd,IAAU,CACd,EACA,QAAU,GAAU,IAAU,CAAK,CACtC,CAAA,GAED,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,UAClB,QAAS,EACT,SAAU,GAAY,CAAC,EACvB,aAAY,EAAU,EAAQ,MAAQ,EAAQ,KAC9C,MAAO,EAAU,EAAQ,MAAQ,EAAQ,KAExC,SAAA,GAAU,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAAO,KAAM,GAAI,cAAA,EAAa,CAAA,GAAI,EAAA,EAAA,IAAA,CAAC,EAAA,KAAD,CAAM,KAAM,GAAI,cAAA,EAAa,CAAA,CACtE,CAAA,GAER,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,KAAK,QACL,UAAW,EAAA,QAAO,KAClB,IAAK,EACL,IAAK,EAAW,KAAK,MAAM,CAAO,EAAI,EACtC,KAAM,IACN,MAAO,KAAK,IAAI,KAAK,MAAM,CAAS,EAAG,EAAW,KAAK,MAAM,CAAO,EAAI,CAAC,EACzE,SAAW,GAAU,EAAK,OAAO,EAAM,OAAO,KAAK,CAAC,EACpD,SAAU,GAAY,CAAC,EACvB,aAAY,EAAQ,KACpB,iBAAgB,GAAG,EAAA,eAAe,CAAS,EAAE,KAAK,EAAA,eAAe,CAAO,GAC3E,CAAA,GAED,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAxB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAA,eAAe,CAAS,CAAQ,CAAA,GACvC,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,cAAY,OAAO,SAAA,GAAO,CAAA,GAChC,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAA,eAAe,CAAO,CAAQ,CAAA,CACnC,IAEL,IAAW,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,QAAU,SAAA,CAAc,CAAA,CAC3D,GAEb"}
|
|
@@ -41,7 +41,7 @@ function p({ src: p, durationMs: m, sinkId: h, autoPlay: g = !1, loop: _ = !1, l
|
|
|
41
41
|
e.addEventListener("timeupdate", t), e.currentTime = 1e101;
|
|
42
42
|
}, z = () => {
|
|
43
43
|
let e = E.current;
|
|
44
|
-
if (
|
|
44
|
+
if (e && F) {
|
|
45
45
|
if (e.paused) {
|
|
46
46
|
e.play().then(() => O(!0)).catch((e) => x?.(e));
|
|
47
47
|
return;
|
|
@@ -50,7 +50,7 @@ function p({ src: p, durationMs: m, sinkId: h, autoPlay: g = !1, loop: _ = !1, l
|
|
|
50
50
|
}
|
|
51
51
|
}, B = (e) => {
|
|
52
52
|
let t = E.current;
|
|
53
|
-
|
|
53
|
+
t && L && (t.currentTime = e / 1e3, A(e));
|
|
54
54
|
};
|
|
55
55
|
return /* @__PURE__ */ l("div", {
|
|
56
56
|
className: e(i.player, C),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioPlayer.js","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — src accepts a URL or a Blob, and\n * everything else is a facet of playing it: durationMs (because MediaRecorder WebM\n * carries none), sinkId (output device), autoPlay/loop, onEnded/onError, plus the\n * actions slot and locale for the labels. The body is the transport state machine —\n * object-URL lifecycle, seek, and the rAF-free time updates — which shares one audio\n * element ref throughout.\n */\nimport { Pause, Play } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\n\nimport { setAudioOutput } from \"@/audio/audio-output\";\nimport { formatDuration } from \"@/audio/duration\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./AudioPlayer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface AudioPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a page that records twenty notes does not leak twenty\n * URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useAudioRecorder` always does.\n * See the note on the seek bar for why the element's own `duration` is not\n * trustworthy for a fresh recording.\n */\n durationMs?: number;\n /** Output device, from `useMediaDevices().audioOutputs`. Chromium only. */\n sinkId?: string;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the right of the times — a download button, a delete button. */\n actions?: React.ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": { play: \"Tocar\", pause: \"Pausar\", seek: \"Posição\" },\n en: { play: \"Play\", pause: \"Pause\", seek: \"Seek\" },\n} as const;\n\n/**\n * Playback transport for one clip: play/pause, a seek bar, elapsed and total time.\n *\n * Built around a real `<audio>` element rather than the SDK's `createAudioPlayer`,\n * which is a fire-and-forget handle for notification chimes and has no transport to\n * expose. Accepts a `Blob` directly, because the thing an app most often plays is the\n * recording it just made.\n *\n * The seek bar is a bare `<input type=\"range\">` rather than the SDK's `Slider`: that\n * component is a form field, with a label row and a value badge, and a transport wants\n * neither. The native input keeps the keyboard and screen-reader behaviour that\n * matters here for free.\n *\n * @example\n * const rec = useAudioRecorder(mic.stream);\n * {rec.recording && (\n * <AudioPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function AudioPlayer({\n src,\n durationMs,\n sinkId,\n autoPlay = false,\n loop = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioPlayerProps) {\n const strings = STRINGS[locale];\n const audio = useRef<HTMLAudioElement | null>(null);\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const probed = useRef(false);\n\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n /**\n * Total length, preferring what the caller knows.\n *\n * `MediaRecorder` writes WebM with no duration in the header, so a fresh recording\n * reports `Infinity` — which is why the recorder keeps its own clock and passes it\n * here. `durationMs` wins whenever it is finite; the element's value is the\n * fallback for a plain URL.\n */\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n useEffect(() => {\n if (sinkId === undefined) return;\n void setAudioOutput(audio.current, sinkId);\n }, [sinkId, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after which\n * it knows the real length. It is a hack with no alternative — the header simply\n * does not carry the value — and it runs at most once per source, only when the\n * caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = audio.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onSeeked = (): void => {\n node.removeEventListener(\"timeupdate\", onSeeked);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onSeeked);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = audio.current;\n if (!node || !url) return;\n if (node.paused) {\n void node\n .play()\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = audio.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n return (\n <div className={cn(styles.player, className)} {...rest}>\n <audio\n ref={audio}\n src={url ?? undefined}\n loop={loop}\n autoPlay={autoPlay}\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = audio.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onEnded={() => {\n setPlaying(false);\n setCurrentMs(0);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n />\n\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AAwDA,IAAM,IAAU;CACZ,SAAS;EAAE,MAAM;EAAS,OAAO;EAAU,MAAM;CAAU;CAC3D,IAAI;EAAE,MAAM;EAAQ,OAAO;EAAS,MAAM;CAAO;AACrD;AAqBA,SAAgB,EAAY,EACxB,QACA,eACA,WACA,cAAW,IACX,UAAO,IACP,YAAS,SACT,YACA,YACA,YACA,cAAW,IACX,cACA,GAAG,KACc;CACjB,IAAM,IAAU,EAAQ,IAClB,IAAQ,EAAgC,IAAI,GAC5C,CAAC,GAAS,KAAc,EAAS,EAAK,GACtC,CAAC,GAAW,KAAgB,EAAS,CAAC,GACtC,CAAC,GAAW,KAAgB,EAAS,QAAwB,GAC7D,IAAS,EAAO,EAAK,GAErB,IAAU,EAAa,OAAO,KAAQ,WAAW,OAAO,CAAG,GAC3D,IAAM,OAAO,KAAQ,WAAW,IAAM,GAUtC,IAAU,OAAO,SAAS,CAAU,IAAK,IAAwB,GACjE,IAAW,OAAO,SAAS,CAAO,KAAK,IAAU;CASvD,AAPA,QAAgB;EAIZ,AAHA,EAAa,CAAC,GACd,EAAW,EAAK,GAChB,EAAa,QAAwB,GACrC,EAAO,UAAU;CACrB,GAAG,CAAC,CAAG,CAAC,GAER,QAAgB;EACR,MAAW,KAAA,KACf,EAAoB,EAAM,SAAS,CAAM;CAC7C,GAAG,CAAC,GAAQ,CAAG,CAAC;CAUhB,IAAM,UAA6B;EAC/B,IAAM,IAAO,EAAM;EACnB,IAAI,CAAC,GAAM;EACX,IAAI,OAAO,SAAS,EAAK,QAAQ,GAAG;GAChC,EAAa,EAAK,WAAW,GAAI;GACjC;EACJ;EACA,IAAI,EAAO,WAAW,OAAO,SAAS,CAAU,GAAG;EACnD,EAAO,UAAU;EACjB,IAAM,UAAuB;GAGzB,AAFA,EAAK,oBAAoB,cAAc,CAAQ,GAC3C,OAAO,SAAS,EAAK,QAAQ,KAAG,EAAa,EAAK,WAAW,GAAI,GACrE,EAAK,cAAc;EACvB;EAEA,AADA,EAAK,iBAAiB,cAAc,CAAQ,GAC5C,EAAK,cAAc;CACvB,GAEM,UAAqB;EACvB,IAAM,IAAO,EAAM;EACf,OAAC,KAAQ,CAAC,IACd;OAAI,EAAK,QAAQ;IACb,EACK,KAAK,CAAC,CACN,WAAW,EAAW,EAAI,CAAC,CAAC,CAC5B,OAAO,MAAmB,IAAU,CAAK,CAAC;IAC/C;GACJ;GAEA,AADA,EAAK,MAAM,GACX,EAAW,EAAK;EAFhB;CAGJ,GAEM,KAAQ,MAAqB;EAC/B,IAAM,IAAO,EAAM;EACf,CAAC,KAAQ,CAAC,MACd,EAAK,cAAc,IAAK,KACxB,EAAa,CAAE;CACnB;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,QAAQ,CAAS;EAAG,GAAI;EAAlD,UAAA;GACI,kBAAC,SAAD;IACI,KAAK;IACL,KAAK,KAAO,KAAA;IACN;IACI;IACV,SAAQ;IACR,kBAAkB;IAClB,kBAAkB;IAClB,oBAAoB;KAChB,IAAM,IAAO,EAAM;KACnB,AAAI,KAAM,EAAa,EAAK,cAAc,GAAI;IAClD;IACA,cAAc,EAAW,EAAI;IAC7B,eAAe,EAAW,EAAK;IAC/B,eAAe;KAGX,AAFA,EAAW,EAAK,GAChB,EAAa,CAAC,GACd,IAAU;IACd;IACA,UAAU,MAAU,IAAU,CAAK;GACtC,CAAA;GAED,kBAAC,UAAD;IACI,MAAK;IACL,WAAW,EAAO;IAClB,SAAS;IACT,UAAU,KAAY,CAAC;IACvB,cAAY,IAAU,EAAQ,QAAQ,EAAQ;IAC9C,OAAO,IAAU,EAAQ,QAAQ,EAAQ;IAExC,UAAU,EAAV,IAAW,IAAkC,GAAnC;KAAO,MAAM;KAAI,eAAA;IAAa,CAAiC;GACtE,CAAA;GAER,kBAAC,SAAD;IACI,MAAK;IACL,WAAW,EAAO;IAClB,KAAK;IACL,KAAK,IAAW,KAAK,MAAM,CAAO,IAAI;IACtC,MAAM;IACN,OAAO,KAAK,IAAI,KAAK,MAAM,CAAS,GAAG,IAAW,KAAK,MAAM,CAAO,IAAI,CAAC;IACzE,WAAW,MAAU,EAAK,OAAO,EAAM,OAAO,KAAK,CAAC;IACpD,UAAU,KAAY,CAAC;IACvB,cAAY,EAAQ;IACpB,kBAAgB,GAAG,EAAe,CAAS,EAAE,KAAK,EAAe,CAAO;GAC3E,CAAA;GAED,kBAAC,QAAD;IAAM,WAAW,EAAO;IAAxB,UAAA;KACI,kBAAC,QAAD,EAAA,UAAO,EAAe,CAAS,EAAQ,CAAA;KACvC,kBAAC,QAAD;MAAM,eAAY;MAAO,UAAA;KAAO,CAAA;KAChC,kBAAC,QAAD,EAAA,UAAO,EAAe,CAAO,EAAQ,CAAA;IACnC;;GAEL,KAAW,kBAAC,QAAD;IAAM,WAAW,EAAO;IAAU,UAAA;GAAc,CAAA;EAC3D;;AAEb"}
|
|
1
|
+
{"version":3,"file":"AudioPlayer.js","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — src accepts a URL or a Blob, and\n * everything else is a facet of playing it: durationMs (because MediaRecorder WebM\n * carries none), sinkId (output device), autoPlay/loop, onEnded/onError, plus the\n * actions slot and locale for the labels. The body is the transport state machine —\n * object-URL lifecycle, seek, and the rAF-free time updates — which shares one audio\n * element ref throughout.\n */\nimport { Pause, Play } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\n\nimport { setAudioOutput } from \"@/audio/audio-output\";\nimport { formatDuration } from \"@/audio/duration\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./AudioPlayer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface AudioPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a page that records twenty notes does not leak twenty\n * URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useAudioRecorder` always does.\n * See the note on the seek bar for why the element's own `duration` is not\n * trustworthy for a fresh recording.\n */\n durationMs?: number;\n /** Output device, from `useMediaDevices().audioOutputs`. Chromium only. */\n sinkId?: string;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the right of the times — a download button, a delete button. */\n actions?: React.ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": { play: \"Tocar\", pause: \"Pausar\", seek: \"Posição\" },\n en: { play: \"Play\", pause: \"Pause\", seek: \"Seek\" },\n} as const;\n\n/**\n * Playback transport for one clip: play/pause, a seek bar, elapsed and total time.\n *\n * Built around a real `<audio>` element rather than the SDK's `createAudioPlayer`,\n * which is a fire-and-forget handle for notification chimes and has no transport to\n * expose. Accepts a `Blob` directly, because the thing an app most often plays is the\n * recording it just made.\n *\n * The seek bar is a bare `<input type=\"range\">` rather than the SDK's `Slider`: that\n * component is a form field, with a label row and a value badge, and a transport wants\n * neither. The native input keeps the keyboard and screen-reader behaviour that\n * matters here for free.\n *\n * @example\n * const rec = useAudioRecorder(mic.stream);\n * {rec.recording && (\n * <AudioPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function AudioPlayer({\n src,\n durationMs,\n sinkId,\n autoPlay = false,\n loop = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioPlayerProps) {\n const strings = STRINGS[locale];\n const audio = useRef<HTMLAudioElement | null>(null);\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const probed = useRef(false);\n\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n /**\n * Total length, preferring what the caller knows.\n *\n * `MediaRecorder` writes WebM with no duration in the header, so a fresh recording\n * reports `Infinity` — which is why the recorder keeps its own clock and passes it\n * here. `durationMs` wins whenever it is finite; the element's value is the\n * fallback for a plain URL.\n */\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n useEffect(() => {\n if (sinkId === undefined) return;\n void setAudioOutput(audio.current, sinkId);\n }, [sinkId, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after which\n * it knows the real length. It is a hack with no alternative — the header simply\n * does not carry the value — and it runs at most once per source, only when the\n * caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = audio.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onSeeked = (): void => {\n node.removeEventListener(\"timeupdate\", onSeeked);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onSeeked);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = audio.current;\n if (!node || !url) return;\n if (node.paused) {\n void node\n .play()\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = audio.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n return (\n <div className={cn(styles.player, className)} {...rest}>\n <audio\n ref={audio}\n src={url ?? undefined}\n loop={loop}\n autoPlay={autoPlay}\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = audio.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onEnded={() => {\n setPlaying(false);\n setCurrentMs(0);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n />\n\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-media-permission.cjs"),n=require("../../audio/duration.cjs"),r=require("../AudioPlayer/AudioPlayer.cjs"),i=require("../../audio/audio-recorder.cjs"),a=require("../../audio/use-audio-recorder.cjs"),o=require("../../audio/use-microphone.cjs"),s=require("../../audio/wav.cjs"),c=require("./AudioRecorder.module.cjs");let l=require("react"),u=require("react/jsx-runtime"),d=require("lucide-react");var f={"pt-BR":{record:`Gravar`,stop:`Parar`,pause:`Pausar`,resume:`Continuar`,retake:`Gravar de novo`,level:`Nível do microfone`,asking:`Pedindo acesso ao microfone…`,denied:`Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.`,unsupported:`Este navegador não grava áudio.`,idleHint:`Toque em Gravar para começar.`,recording:`Gravando`,paused:`Pausado`,converting:`Convertendo…`},en:{record:`Record`,stop:`Stop`,pause:`Pause`,resume:`Resume`,retake:`Record again`,level:`Microphone level`,asking:`Requesting microphone access…`,denied:`Microphone access blocked. Allow it in site settings and reload.`,unsupported:`This browser cannot record audio.`,idleHint:`Press Record to start.`,recording:`Recording`,paused:`Paused`,converting:`Converting…`}};function p({onRecorded:p,maxDurationMs:m,deviceId:h,format:g=`native`,wavOptions:_,audioBitsPerSecond:v,review:y=!0,locale:b=`pt-BR`,footer:x,onError:S,disabled:C=!1,className:w,...T}){let E=f[b],D=t.useMediaPermission(`microphone`),O=o.useMicrophone({deviceId:h}),[k,A]=(0,l.useState)(!1),[j,M]=(0,l.useState)(null),[N,P]=(0,l.useState)(!0);(0,l.useEffect)(()=>P(i.isAudioRecordingSupported()),[]);let F=async e=>{if(g!==`wav`){M(e),p?.(e);return}A(!0);try{let t=await s.blobToWav(e.blob,_),n={...e,blob:t,mimeType:`audio/wav`};M(n),p?.(n)}catch(t){S?.(t),M(e),p?.(e)}finally{A(!1)}},I=a.useAudioRecorder(O.stream,{maxDurationMs:m,audioBitsPerSecond:v,onRecorded:e=>void F(e),onError:S}),[L,R]=(0,l.useState)(!1);(0,l.useEffect)(()=>{
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-media-permission.cjs"),n=require("../../audio/duration.cjs"),r=require("../AudioPlayer/AudioPlayer.cjs"),i=require("../../audio/audio-recorder.cjs"),a=require("../../audio/use-audio-recorder.cjs"),o=require("../../audio/use-microphone.cjs"),s=require("../../audio/wav.cjs"),c=require("./AudioRecorder.module.cjs");let l=require("react"),u=require("react/jsx-runtime"),d=require("lucide-react");var f={"pt-BR":{record:`Gravar`,stop:`Parar`,pause:`Pausar`,resume:`Continuar`,retake:`Gravar de novo`,level:`Nível do microfone`,asking:`Pedindo acesso ao microfone…`,denied:`Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.`,unsupported:`Este navegador não grava áudio.`,idleHint:`Toque em Gravar para começar.`,recording:`Gravando`,paused:`Pausado`,converting:`Convertendo…`},en:{record:`Record`,stop:`Stop`,pause:`Pause`,resume:`Resume`,retake:`Record again`,level:`Microphone level`,asking:`Requesting microphone access…`,denied:`Microphone access blocked. Allow it in site settings and reload.`,unsupported:`This browser cannot record audio.`,idleHint:`Press Record to start.`,recording:`Recording`,paused:`Paused`,converting:`Converting…`}};function p({onRecorded:p,maxDurationMs:m,deviceId:h,format:g=`native`,wavOptions:_,audioBitsPerSecond:v,review:y=!0,locale:b=`pt-BR`,footer:x,onError:S,disabled:C=!1,className:w,...T}){let E=f[b],D=t.useMediaPermission(`microphone`),O=o.useMicrophone({deviceId:h}),[k,A]=(0,l.useState)(!1),[j,M]=(0,l.useState)(null),[N,P]=(0,l.useState)(!0);(0,l.useEffect)(()=>P(i.isAudioRecordingSupported()),[]);let F=async e=>{if(g!==`wav`){M(e),p?.(e);return}A(!0);try{let t=await s.blobToWav(e.blob,_),n={...e,blob:t,mimeType:`audio/wav`};M(n),p?.(n)}catch(t){S?.(t),M(e),p?.(e)}finally{A(!1)}},I=a.useAudioRecorder(O.stream,{maxDurationMs:m,audioBitsPerSecond:v,onRecorded:e=>void F(e),onError:S}),[L,R]=(0,l.useState)(!1);(0,l.useEffect)(()=>{L&&I.ready&&I.status===`idle`&&(R(!1),I.start())},[L,I.ready,I.status,I]);let z=()=>{if(M(null),O.stream&&I.ready){I.start();return}R(!0),O.start()},B=()=>{M(null),I.cancel(),z()},V=I.status===`recording`||I.status===`paused`,H=D.state===`denied`;return N?(0,u.jsxs)(`div`,{className:e.cn(c.default.recorder,w),...T,children:[(0,u.jsxs)(`div`,{className:c.default.row,children:[!V&&(0,u.jsxs)(`button`,{type:`button`,className:c.default.record,onClick:j?B:z,disabled:C||H||k,"aria-label":j?E.retake:E.record,children:[j?(0,u.jsx)(d.RotateCcw,{size:16,"aria-hidden":!0}):(0,u.jsx)(d.Mic,{size:16,"aria-hidden":!0}),(0,u.jsx)(`span`,{children:j?E.retake:E.record})]}),V&&(0,u.jsxs)(u.Fragment,{children:[(0,u.jsxs)(`button`,{type:`button`,className:c.default.stop,onClick:()=>void I.stop(),"aria-label":E.stop,children:[(0,u.jsx)(d.Square,{size:14,"aria-hidden":!0}),(0,u.jsx)(`span`,{children:E.stop})]}),(0,u.jsx)(`button`,{type:`button`,className:c.default.secondary,onClick:I.status===`paused`?I.resume:I.pause,"aria-label":I.status===`paused`?E.resume:E.pause,children:I.status===`paused`?(0,u.jsx)(d.Play,{size:14,"aria-hidden":!0}):(0,u.jsx)(d.Pause,{size:14,"aria-hidden":!0})})]}),V&&(0,u.jsx)(`div`,{className:c.default.meter,role:`meter`,"aria-label":E.level,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":Math.round(I.level*100),children:(0,u.jsx)(`span`,{className:c.default.meterFill,style:{transform:`scaleX(${I.level})`}})}),(0,u.jsxs)(`span`,{className:c.default.clock,children:[n.formatDuration(V?I.durationMs:j?.durationMs??0),m!==void 0&&(0,u.jsxs)(`span`,{className:c.default.max,children:[` / `,n.formatDuration(m)]})]})]}),(0,u.jsx)(`p`,{className:c.default.status,role:`status`,children:k?E.converting:O.status===`requesting`?E.asking:H?E.denied:O.error?O.error.message:I.status===`recording`?E.recording:I.status===`paused`?E.paused:j?``:E.idleHint}),y&&j&&(0,u.jsx)(r.AudioPlayer,{src:j.blob,durationMs:j.durationMs,locale:b,onError:S}),x&&(0,u.jsx)(`div`,{className:c.default.footer,children:x})]}):(0,u.jsx)(`div`,{className:e.cn(c.default.recorder,w),...T,children:(0,u.jsx)(`p`,{className:c.default.notice,children:E.unsupported})})}exports.AudioRecorder=p;
|
|
2
2
|
//# sourceMappingURL=AudioRecorder.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioRecorder.cjs","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — recording has three\n * separate configurations that a caller genuinely picks between: the capture\n * (deviceId, audioBitsPerSecond, maxDurationMs), the output (format, wavOptions) and\n * the review step (review, footer, locale). The body holds permission, recording,\n * review and error as one state machine; splitting it would thread the same recorder\n * handle through props.\n */\nimport { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AudioRecorder.cjs","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — recording has three\n * separate configurations that a caller genuinely picks between: the capture\n * (deviceId, audioBitsPerSecond, maxDurationMs), the output (format, wavOptions) and\n * the review step (review, footer, locale). The body holds permission, recording,\n * review and error as one state machine; splitting it would thread the same recorder\n * handle through props.\n */\nimport { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -71,7 +71,7 @@ function b({ onRecorded: b, maxDurationMs: x, deviceId: S, format: C = "native",
|
|
|
71
71
|
onError: k
|
|
72
72
|
}), [W, G] = u(!1);
|
|
73
73
|
l(() => {
|
|
74
|
-
|
|
74
|
+
W && U.ready && U.status === "idle" && (G(!1), U.start());
|
|
75
75
|
}, [
|
|
76
76
|
W,
|
|
77
77
|
U.ready,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioRecorder.js","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — recording has three\n * separate configurations that a caller genuinely picks between: the capture\n * (deviceId, audioBitsPerSecond, maxDurationMs), the output (format, wavOptions) and\n * the review step (review, footer, locale). The body holds permission, recording,\n * review and error as one state machine; splitting it would thread the same recorder\n * handle through props.\n */\nimport { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AudioRecorder.js","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — recording has three\n * separate configurations that a caller genuinely picks between: the capture\n * (deviceId, audioBitsPerSecond, maxDurationMs), the output (format, wavOptions) and\n * the review step (review, footer, locale). The body holds permission, recording,\n * review and error as one state machine; splitting it would thread the same recorder\n * handle through props.\n */\nimport { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAgEA,IAAM,IAAU;CACZ,SAAS;EACL,QAAQ;EACR,MAAM;EACN,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,aAAa;EACb,UAAU;EACV,WAAW;EACX,QAAQ;EACR,YAAY;CAChB;CACA,IAAI;EACA,QAAQ;EACR,MAAM;EACN,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,aAAa;EACb,UAAU;EACV,WAAW;EACX,QAAQ;EACR,YAAY;CAChB;AACJ;AAyBA,SAAgB,EAAc,EAC1B,eACA,kBACA,aACA,YAAS,UACT,eACA,uBACA,YAAS,IACT,YAAS,SACT,WACA,YACA,cAAW,IACX,cACA,GAAG,KACgB;CACnB,IAAM,IAAU,EAAQ,IAClB,IAAa,EAAmB,YAAY,GAC5C,IAAM,EAAc,EAAE,YAAS,CAAC,GAChC,CAAC,GAAY,KAAiB,EAAS,EAAK,GAC5C,CAAC,GAAQ,KAAa,EAAgC,IAAI,GAC1D,CAAC,GAAW,KAAgB,EAAS,EAAI;CAE/C,QAAgB,EAAa,EAA0B,CAAC,GAAG,CAAC,CAAC;CAU7D,IAAM,IAAS,OAAO,MAA6C;EAC/D,IAAI,MAAW,OAAO;GAElB,AADA,EAAU,CAAS,GACnB,IAAa,CAAS;GACtB;EACJ;EACA,EAAc,EAAI;EAClB,IAAI;GACA,IAAM,IAAO,MAAM,EAAU,EAAU,MAAM,CAAU,GACjD,IAA4B;IAAE,GAAG;IAAW;IAAM,UAAU;GAAY;GAE9E,AADA,EAAU,CAAS,GACnB,IAAa,CAAS;EAC1B,SAAS,GAAO;GAGZ,AAFA,IAAU,CAAK,GACf,EAAU,CAAS,GACnB,IAAa,CAAS;EAC1B,UAAU;GACN,EAAc,EAAK;EACvB;CACJ,GAEM,IAAW,EAAiB,EAAI,QAAQ;EAC1C;EACA;EACA,aAAa,MAAc,KAAK,EAAO,CAAS;EAChD;CACJ,CAAC,GAUK,CAAC,GAAO,KAAY,EAAS,EAAK;CACxC,QAAgB;EACR,AAAC,KAAU,EAAS,SAAS,EAAS,WAAW,WACrD,EAAS,EAAK,GACd,EAAS,MAAM;CACnB,GAAG;EAAC;EAAO,EAAS;EAAO,EAAS;EAAQ;CAAQ,CAAC;CAErD,IAAM,UAAoB;EAEtB,IADA,EAAU,IAAI,GACV,EAAI,UAAU,EAAS,OAAO;GAC9B,EAAS,MAAM;GACf;EACJ;EAEA,AADA,EAAS,EAAI,GACb,EAAI,MAAM;CACd,GAEM,UAAqB;EAGvB,AAFA,EAAU,IAAI,GACd,EAAS,OAAO,GAChB,EAAM;CACV,GAEM,IAAO,EAAS,WAAW,eAAe,EAAS,WAAW,UAC9D,IAAU,EAAW,UAAU;CAUrC,OARK,IASD,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,UAAU,CAAS;EAAG,GAAI;EAApD,UAAA;GACI,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA;KACK,CAAC,KACE,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,SAAS,IAAS,IAAS;MAC3B,UAAU,KAAY,KAAW;MACjC,cAAY,IAAS,EAAQ,SAAS,EAAQ;MALlD,UAAA,CAQQ,EADH,IACI,IAEA,GAFD;OAAW,MAAM;OAAI,eAAA;MAAa,CAEN,GAEhC,kBAAC,QAAD,EAAA,UAAO,IAAS,EAAQ,SAAS,EAAQ,OAAa,CAAA,CAClD;;KAGX,KACG,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,eAAe,KAAK,EAAS,KAAK;MAClC,cAAY,EAAQ;MAJxB,UAAA,CAMI,kBAAC,GAAD;OAAQ,MAAM;OAAI,eAAA;MAAa,CAAA,GAC/B,kBAAC,QAAD,EAAA,UAAO,EAAQ,KAAW,CAAA,CACtB;KACR,CAAA,GAAA,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,SACI,EAAS,WAAW,WAAW,EAAS,SAAS,EAAS;MAE9D,cACI,EAAS,WAAW,WAAW,EAAQ,SAAS,EAAQ;MAG3D,UAAA,EAAS,WAAW,WACjB,kBAAC,GAAD;OAAM,MAAM;OAAI,eAAA;MAAa,CAAA,IAE7B,kBAAC,GAAD;OAAO,MAAM;OAAI,eAAA;MAAa,CAAA;KAE9B,CAAA,CACV,EAAA,CAAA;KAGL,KACG,kBAAC,OAAD;MACI,WAAW,EAAO;MAClB,MAAK;MACL,cAAY,EAAQ;MACpB,iBAAe;MACf,iBAAe;MACf,iBAAe,KAAK,MAAM,EAAS,QAAQ,GAAG;MAE9C,UAAA,kBAAC,QAAD;OACI,WAAW,EAAO;OAClB,OAAO,EAAE,WAAW,UAAU,EAAS,MAAM,GAAG;MACnD,CAAA;KACA,CAAA;KAGT,kBAAC,QAAD;MAAM,WAAW,EAAO;MAAxB,UAAA,CACK,EAAe,IAAO,EAAS,aAAc,GAAQ,cAAc,CAAE,GACrE,MAAkB,KAAA,KACf,kBAAC,QAAD;OAAM,WAAW,EAAO;OAAxB,UAAA,CAA6B,OAAI,EAAe,CAAa,CAAQ;MAEvE,CAAA,CAAA;;IACL;;GAEL,kBAAC,KAAD;IAAG,WAAW,EAAO;IAAQ,MAAK;IAC7B,UAAA,IACK,EAAQ,aACR,EAAI,WAAW,eACb,EAAQ,SACR,IACE,EAAQ,SACR,EAAI,QACF,EAAI,MAAM,UACV,EAAS,WAAW,cAClB,EAAQ,YACR,EAAS,WAAW,WAClB,EAAQ,SACR,IACE,KACA,EAAQ;GAC3B,CAAA;GAEF,KAAU,KACP,kBAAC,GAAD;IACI,KAAK,EAAO;IACZ,YAAY,EAAO;IACX;IACC;GACZ,CAAA;GAGJ,KAAU,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAS,UAAA;GAAY,CAAA;EACtD;MA5GD,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,UAAU,CAAS;EAAG,GAAI;EAChD,UAAA,kBAAC,KAAD;GAAG,WAAW,EAAO;GAAS,UAAA,EAAQ;EAAe,CAAA;CACpD,CAAA;AA4GjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Badge.module.cjs","names":[],"sources":["../../../src/components/Badge/Badge.module.css"],"sourcesContent":[".badge {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-1);\n padding: 2px var(--tempest-space-2);\n border: 1px solid transparent;\n border-radius: var(--tempest-radius-full);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n line-height: var(--tempest-leading-snug);\n letter-spacing: var(--tempest-tracking-normal);\n white-space: nowrap;\n}\n\n/* ============================================================\n * Sizes\n * ============================================================ */\n.sm {\n padding: 1px var(--tempest-space-2);\n font-size: var(--tempest-text-2xs);\n}\n.md {\n padding: 2px var(--tempest-space-2);\n font-size: var(--tempest-text-xs);\n}\n.lg {\n padding: 3px var(--tempest-space-3);\n font-size: var(--tempest-text-sm);\n}\n\n/* ============================================================\n * Shapes — pill is the default (border-radius-full).\n * `square` flips to a small radius for chip-style badges.\n * ============================================================ */\n.square {\n border-radius: var(--tempest-radius-sm);\n}\n\n/* ============================================================\n * Tone × Style matrix.\n * Soft (default) = tinted bg + colored fg.\n * Solid = filled colored bg + white fg.\n * Outline = transparent bg + colored fg + colored border.\n * ============================================================ */\n\n/* Neutral */\n.neutral {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n}\n.neutral.solid {\n background-color: var(--tempest-gray-700);\n color:
|
|
1
|
+
{"version":3,"file":"Badge.module.cjs","names":[],"sources":["../../../src/components/Badge/Badge.module.css"],"sourcesContent":[".badge {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-1);\n padding: 2px var(--tempest-space-2);\n border: 1px solid transparent;\n border-radius: var(--tempest-radius-full);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n line-height: var(--tempest-leading-snug);\n letter-spacing: var(--tempest-tracking-normal);\n white-space: nowrap;\n}\n\n/* ============================================================\n * Sizes\n * ============================================================ */\n.sm {\n padding: 1px var(--tempest-space-2);\n font-size: var(--tempest-text-2xs);\n}\n.md {\n padding: 2px var(--tempest-space-2);\n font-size: var(--tempest-text-xs);\n}\n.lg {\n padding: 3px var(--tempest-space-3);\n font-size: var(--tempest-text-sm);\n}\n\n/* ============================================================\n * Shapes — pill is the default (border-radius-full).\n * `square` flips to a small radius for chip-style badges.\n * ============================================================ */\n.square {\n border-radius: var(--tempest-radius-sm);\n}\n\n/* ============================================================\n * Tone × Style matrix.\n * Soft (default) = tinted bg + colored fg.\n * Solid = filled colored bg + white fg.\n * Outline = transparent bg + colored fg + colored border.\n * ============================================================ */\n\n/* Neutral */\n.neutral {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n}\n.neutral.solid {\n background-color: var(--tempest-gray-700);\n color: var(--tempest-neutral-on-solid);\n}\n.neutral.outline {\n background-color: transparent;\n color: var(--tempest-text);\n border-color: var(--tempest-border-strong);\n}\n\n/* Success */\n.success {\n background-color: var(--tempest-success-bg);\n color: var(--tempest-success-fg);\n}\n.success.solid {\n background-color: var(--tempest-success-solid);\n color: var(--tempest-success-on-solid);\n}\n.success.outline {\n background-color: transparent;\n color: var(--tempest-success);\n border-color: var(--tempest-success-border);\n}\n\n/* Warning */\n.warning {\n background-color: var(--tempest-warning-bg);\n color: var(--tempest-warning-fg);\n}\n.warning.solid {\n background-color: var(--tempest-warning-solid);\n color: var(--tempest-warning-on-solid);\n}\n.warning.outline {\n background-color: transparent;\n color: var(--tempest-warning);\n border-color: var(--tempest-warning-border);\n}\n\n/* Danger */\n.danger {\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-danger-fg);\n}\n.danger.solid {\n background-color: var(--tempest-danger-solid);\n color: var(--tempest-danger-on-solid);\n}\n.danger.outline {\n background-color: transparent;\n color: var(--tempest-danger);\n border-color: var(--tempest-danger-border);\n}\n\n/* Info */\n.info {\n background-color: var(--tempest-info-bg);\n color: var(--tempest-info-fg);\n}\n.info.solid {\n background-color: var(--tempest-info-solid);\n color: var(--tempest-info-on-solid);\n}\n.info.outline {\n background-color: transparent;\n color: var(--tempest-info);\n border-color: var(--tempest-info-border);\n}\n\n/* Primary */\n.primary {\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n.primary.solid {\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n}\n.primary.outline {\n background-color: transparent;\n color: var(--tempest-primary);\n border-color: var(--tempest-primary);\n}\n\n/* Status dot — small leading dot, color inherits from text. */\n.dot {\n width: 6px;\n height: 6px;\n border-radius: var(--tempest-radius-full);\n background-color: currentColor;\n display: inline-block;\n flex-shrink: 0;\n}\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Badge.module.js","names":[],"sources":["../../../src/components/Badge/Badge.module.css"],"sourcesContent":[".badge {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-1);\n padding: 2px var(--tempest-space-2);\n border: 1px solid transparent;\n border-radius: var(--tempest-radius-full);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n line-height: var(--tempest-leading-snug);\n letter-spacing: var(--tempest-tracking-normal);\n white-space: nowrap;\n}\n\n/* ============================================================\n * Sizes\n * ============================================================ */\n.sm {\n padding: 1px var(--tempest-space-2);\n font-size: var(--tempest-text-2xs);\n}\n.md {\n padding: 2px var(--tempest-space-2);\n font-size: var(--tempest-text-xs);\n}\n.lg {\n padding: 3px var(--tempest-space-3);\n font-size: var(--tempest-text-sm);\n}\n\n/* ============================================================\n * Shapes — pill is the default (border-radius-full).\n * `square` flips to a small radius for chip-style badges.\n * ============================================================ */\n.square {\n border-radius: var(--tempest-radius-sm);\n}\n\n/* ============================================================\n * Tone × Style matrix.\n * Soft (default) = tinted bg + colored fg.\n * Solid = filled colored bg + white fg.\n * Outline = transparent bg + colored fg + colored border.\n * ============================================================ */\n\n/* Neutral */\n.neutral {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n}\n.neutral.solid {\n background-color: var(--tempest-gray-700);\n color:
|
|
1
|
+
{"version":3,"file":"Badge.module.js","names":[],"sources":["../../../src/components/Badge/Badge.module.css"],"sourcesContent":[".badge {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-1);\n padding: 2px var(--tempest-space-2);\n border: 1px solid transparent;\n border-radius: var(--tempest-radius-full);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n line-height: var(--tempest-leading-snug);\n letter-spacing: var(--tempest-tracking-normal);\n white-space: nowrap;\n}\n\n/* ============================================================\n * Sizes\n * ============================================================ */\n.sm {\n padding: 1px var(--tempest-space-2);\n font-size: var(--tempest-text-2xs);\n}\n.md {\n padding: 2px var(--tempest-space-2);\n font-size: var(--tempest-text-xs);\n}\n.lg {\n padding: 3px var(--tempest-space-3);\n font-size: var(--tempest-text-sm);\n}\n\n/* ============================================================\n * Shapes — pill is the default (border-radius-full).\n * `square` flips to a small radius for chip-style badges.\n * ============================================================ */\n.square {\n border-radius: var(--tempest-radius-sm);\n}\n\n/* ============================================================\n * Tone × Style matrix.\n * Soft (default) = tinted bg + colored fg.\n * Solid = filled colored bg + white fg.\n * Outline = transparent bg + colored fg + colored border.\n * ============================================================ */\n\n/* Neutral */\n.neutral {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n}\n.neutral.solid {\n background-color: var(--tempest-gray-700);\n color: var(--tempest-neutral-on-solid);\n}\n.neutral.outline {\n background-color: transparent;\n color: var(--tempest-text);\n border-color: var(--tempest-border-strong);\n}\n\n/* Success */\n.success {\n background-color: var(--tempest-success-bg);\n color: var(--tempest-success-fg);\n}\n.success.solid {\n background-color: var(--tempest-success-solid);\n color: var(--tempest-success-on-solid);\n}\n.success.outline {\n background-color: transparent;\n color: var(--tempest-success);\n border-color: var(--tempest-success-border);\n}\n\n/* Warning */\n.warning {\n background-color: var(--tempest-warning-bg);\n color: var(--tempest-warning-fg);\n}\n.warning.solid {\n background-color: var(--tempest-warning-solid);\n color: var(--tempest-warning-on-solid);\n}\n.warning.outline {\n background-color: transparent;\n color: var(--tempest-warning);\n border-color: var(--tempest-warning-border);\n}\n\n/* Danger */\n.danger {\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-danger-fg);\n}\n.danger.solid {\n background-color: var(--tempest-danger-solid);\n color: var(--tempest-danger-on-solid);\n}\n.danger.outline {\n background-color: transparent;\n color: var(--tempest-danger);\n border-color: var(--tempest-danger-border);\n}\n\n/* Info */\n.info {\n background-color: var(--tempest-info-bg);\n color: var(--tempest-info-fg);\n}\n.info.solid {\n background-color: var(--tempest-info-solid);\n color: var(--tempest-info-on-solid);\n}\n.info.outline {\n background-color: transparent;\n color: var(--tempest-info);\n border-color: var(--tempest-info-border);\n}\n\n/* Primary */\n.primary {\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n.primary.solid {\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n}\n.primary.outline {\n background-color: transparent;\n color: var(--tempest-primary);\n border-color: var(--tempest-primary);\n}\n\n/* Status dot — small leading dot, color inherits from text. */\n.dot {\n width: 6px;\n height: 6px;\n border-radius: var(--tempest-radius-full);\n background-color: currentColor;\n display: inline-block;\n flex-shrink: 0;\n}\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarList.module.cjs","names":[],"sources":["../../../src/components/BarList/BarList.module.css"],"sourcesContent":[".list {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.item {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1, 0.25rem);\n}\n\n.line {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--tempest-space-3, 0.75rem);\n font-size: var(--tempest-
|
|
1
|
+
{"version":3,"file":"BarList.module.cjs","names":[],"sources":["../../../src/components/BarList/BarList.module.css"],"sourcesContent":[".list {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.item {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1, 0.25rem);\n}\n\n.line {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--tempest-space-3, 0.75rem);\n font-size: var(--tempest-text-sm);\n}\n\n/*\n * The label truncates instead of wrapping: a plan name long enough to wrap would\n * push its own bar down and break the alignment the list exists to show.\n */\n.label {\n min-width: 0;\n overflow: hidden;\n color: var(--tempest-text);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.value {\n display: inline-flex;\n align-items: baseline;\n gap: var(--tempest-space-2, 0.5rem);\n color: var(--tempest-text);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n/*\n * The share is de-emphasised by size, and only then by colour — and the colour it\n * uses is resolved against the page background, which is where this text actually\n * sits. Putting the label on top of the bar would need the fill's own foreground\n * re-verified per series, which is the trap this layout avoids entirely.\n */\n.percentage {\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n}\n\n.track {\n overflow: hidden;\n width: 100%;\n height: 0.5rem;\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-surface-2);\n}\n\n.fill {\n height: 100%;\n border-radius: inherit;\n transition: width var(--tempest-duration-base) ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fill {\n transition: none;\n }\n}\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarList.module.js","names":[],"sources":["../../../src/components/BarList/BarList.module.css"],"sourcesContent":[".list {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.item {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1, 0.25rem);\n}\n\n.line {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--tempest-space-3, 0.75rem);\n font-size: var(--tempest-
|
|
1
|
+
{"version":3,"file":"BarList.module.js","names":[],"sources":["../../../src/components/BarList/BarList.module.css"],"sourcesContent":[".list {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.item {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1, 0.25rem);\n}\n\n.line {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--tempest-space-3, 0.75rem);\n font-size: var(--tempest-text-sm);\n}\n\n/*\n * The label truncates instead of wrapping: a plan name long enough to wrap would\n * push its own bar down and break the alignment the list exists to show.\n */\n.label {\n min-width: 0;\n overflow: hidden;\n color: var(--tempest-text);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.value {\n display: inline-flex;\n align-items: baseline;\n gap: var(--tempest-space-2, 0.5rem);\n color: var(--tempest-text);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n/*\n * The share is de-emphasised by size, and only then by colour — and the colour it\n * uses is resolved against the page background, which is where this text actually\n * sits. Putting the label on top of the bar would need the fill's own foreground\n * re-verified per series, which is the trap this layout avoids entirely.\n */\n.percentage {\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n}\n\n.track {\n overflow: hidden;\n width: 100%;\n height: 0.5rem;\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-surface-2);\n}\n\n.fill {\n height: 100%;\n border-radius: inherit;\n transition: width var(--tempest-duration-base) ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fill {\n transition: none;\n }\n}\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomSheet.module.cjs","names":[],"sources":["../../../src/components/BottomSheet/BottomSheet.module.css"],"sourcesContent":[".root {\n position: fixed;\n inset: 0;\n z-index: 60;\n display: flex;\n align-items: flex-end;\n justify-content: center;\n}\n\n.backdrop {\n position: absolute;\n inset: 0;\n background: rgb(0 0 0 / 0.45);\n animation: fadeIn var(--tempest-duration-base, 200ms) var(--tempest-ease-out, ease-out);\n}\n\n.sheet {\n position: relative;\n width: 100%;\n max-width: 720px;\n max-height: 90dvh;\n background: var(--tempest-surface);\n border-top-left-radius: var(--tempest-radius-2xl, var(--tempest-radius-lg));\n border-top-right-radius: var(--tempest-radius-2xl, var(--tempest-radius-lg));\n box-shadow: var(--tempest-shadow-xl, 0 -10px 30px rgb(0 0 0 / 0.25));\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding-bottom: env(safe-area-inset-bottom, 0);\n animation: slideUp var(--tempest-duration-base, 240ms) var(--tempest-ease-out, ease-out);\n font-family: var(--tempest-font-sans);\n}\n\n.handle {\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: 0;\n padding: var(--tempest-space-2) 0;\n cursor: grab;\n flex: 0 0 auto;\n}\n\n.handleBar {\n display: block;\n width: 36px;\n height: 4px;\n background: var(--tempest-
|
|
1
|
+
{"version":3,"file":"BottomSheet.module.cjs","names":[],"sources":["../../../src/components/BottomSheet/BottomSheet.module.css"],"sourcesContent":[".root {\n position: fixed;\n inset: 0;\n z-index: 60;\n display: flex;\n align-items: flex-end;\n justify-content: center;\n}\n\n.backdrop {\n position: absolute;\n inset: 0;\n background: rgb(0 0 0 / 0.45);\n animation: fadeIn var(--tempest-duration-base, 200ms) var(--tempest-ease-out, ease-out);\n}\n\n.sheet {\n position: relative;\n width: 100%;\n max-width: 720px;\n max-height: 90dvh;\n background: var(--tempest-surface);\n border-top-left-radius: var(--tempest-radius-2xl, var(--tempest-radius-lg));\n border-top-right-radius: var(--tempest-radius-2xl, var(--tempest-radius-lg));\n box-shadow: var(--tempest-shadow-xl, 0 -10px 30px rgb(0 0 0 / 0.25));\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding-bottom: env(safe-area-inset-bottom, 0);\n animation: slideUp var(--tempest-duration-base, 240ms) var(--tempest-ease-out, ease-out);\n font-family: var(--tempest-font-sans);\n}\n\n.handle {\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: 0;\n padding: var(--tempest-space-2) 0;\n cursor: grab;\n flex: 0 0 auto;\n}\n\n.handleBar {\n display: block;\n width: 36px;\n height: 4px;\n background: var(--tempest-border-strong);\n border-radius: var(--tempest-radius-full);\n}\n\n.title {\n flex: 0 0 auto;\n padding: var(--tempest-space-2) var(--tempest-space-4) var(--tempest-space-3);\n font-size: var(--tempest-text-lg);\n font-weight: var(--tempest-weight-semibold);\n color: var(--tempest-text);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.body {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: var(--tempest-space-4);\n color: var(--tempest-text);\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes slideUp {\n from {\n transform: translateY(100%);\n }\n to {\n transform: translateY(0);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .backdrop,\n .sheet {\n animation: none;\n }\n}\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomSheet.module.js","names":[],"sources":["../../../src/components/BottomSheet/BottomSheet.module.css"],"sourcesContent":[".root {\n position: fixed;\n inset: 0;\n z-index: 60;\n display: flex;\n align-items: flex-end;\n justify-content: center;\n}\n\n.backdrop {\n position: absolute;\n inset: 0;\n background: rgb(0 0 0 / 0.45);\n animation: fadeIn var(--tempest-duration-base, 200ms) var(--tempest-ease-out, ease-out);\n}\n\n.sheet {\n position: relative;\n width: 100%;\n max-width: 720px;\n max-height: 90dvh;\n background: var(--tempest-surface);\n border-top-left-radius: var(--tempest-radius-2xl, var(--tempest-radius-lg));\n border-top-right-radius: var(--tempest-radius-2xl, var(--tempest-radius-lg));\n box-shadow: var(--tempest-shadow-xl, 0 -10px 30px rgb(0 0 0 / 0.25));\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding-bottom: env(safe-area-inset-bottom, 0);\n animation: slideUp var(--tempest-duration-base, 240ms) var(--tempest-ease-out, ease-out);\n font-family: var(--tempest-font-sans);\n}\n\n.handle {\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: 0;\n padding: var(--tempest-space-2) 0;\n cursor: grab;\n flex: 0 0 auto;\n}\n\n.handleBar {\n display: block;\n width: 36px;\n height: 4px;\n background: var(--tempest-
|
|
1
|
+
{"version":3,"file":"BottomSheet.module.js","names":[],"sources":["../../../src/components/BottomSheet/BottomSheet.module.css"],"sourcesContent":[".root {\n position: fixed;\n inset: 0;\n z-index: 60;\n display: flex;\n align-items: flex-end;\n justify-content: center;\n}\n\n.backdrop {\n position: absolute;\n inset: 0;\n background: rgb(0 0 0 / 0.45);\n animation: fadeIn var(--tempest-duration-base, 200ms) var(--tempest-ease-out, ease-out);\n}\n\n.sheet {\n position: relative;\n width: 100%;\n max-width: 720px;\n max-height: 90dvh;\n background: var(--tempest-surface);\n border-top-left-radius: var(--tempest-radius-2xl, var(--tempest-radius-lg));\n border-top-right-radius: var(--tempest-radius-2xl, var(--tempest-radius-lg));\n box-shadow: var(--tempest-shadow-xl, 0 -10px 30px rgb(0 0 0 / 0.25));\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding-bottom: env(safe-area-inset-bottom, 0);\n animation: slideUp var(--tempest-duration-base, 240ms) var(--tempest-ease-out, ease-out);\n font-family: var(--tempest-font-sans);\n}\n\n.handle {\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: 0;\n padding: var(--tempest-space-2) 0;\n cursor: grab;\n flex: 0 0 auto;\n}\n\n.handleBar {\n display: block;\n width: 36px;\n height: 4px;\n background: var(--tempest-border-strong);\n border-radius: var(--tempest-radius-full);\n}\n\n.title {\n flex: 0 0 auto;\n padding: var(--tempest-space-2) var(--tempest-space-4) var(--tempest-space-3);\n font-size: var(--tempest-text-lg);\n font-weight: var(--tempest-weight-semibold);\n color: var(--tempest-text);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.body {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: var(--tempest-space-4);\n color: var(--tempest-text);\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes slideUp {\n from {\n transform: translateY(100%);\n }\n to {\n transform: translateY(0);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .backdrop,\n .sheet {\n animation: none;\n }\n}\n"],"mappings":""}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("../EmptyState/EmptyState.cjs"),n=require("../VisuallyHidden/VisuallyHidden.cjs"),r=require("./chat-groups.cjs"),i=require("./Chat.module.cjs"),a=require("./ChatComposer.cjs");let o=require("react"),s=require("react/jsx-runtime");var c={sending:`◌`,sent:`✓`,read:`✓✓`,failed:`!`};function l({messages:l,currentUserId:u,onSend:d,onRetry:f,typing:p=[],renderAvatar:m,header:h,emptyState:g,groupWindowMs:_,locale:v=`pt-BR`,now:y,placeholder:b,composerActions:x,composerDisabled:S,onSendError:C,className:w,...T}){let E=r.chatStrings(v),D=r.groupMessages({messages:l,currentUserId:u,windowMs:_}),O=(0,o.useRef)(null),k=(0,o.useRef)(null),A=(0,o.useRef)(!0),j=()=>{let e=k.current;if(!e)return;let t=e.scrollHeight-e.scrollTop-e.clientHeight;A.current=t<48};(0,o.useLayoutEffect)(()=>{let e=k.current
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../EmptyState/EmptyState.cjs"),n=require("../VisuallyHidden/VisuallyHidden.cjs"),r=require("./chat-groups.cjs"),i=require("./Chat.module.cjs"),a=require("./ChatComposer.cjs");let o=require("react"),s=require("react/jsx-runtime");var c={sending:`◌`,sent:`✓`,read:`✓✓`,failed:`!`};function l({messages:l,currentUserId:u,onSend:d,onRetry:f,typing:p=[],renderAvatar:m,header:h,emptyState:g,groupWindowMs:_,locale:v=`pt-BR`,now:y,placeholder:b,composerActions:x,composerDisabled:S,onSendError:C,className:w,...T}){let E=r.chatStrings(v),D=r.groupMessages({messages:l,currentUserId:u,windowMs:_}),O=(0,o.useRef)(null),k=(0,o.useRef)(null),A=(0,o.useRef)(!0),j=()=>{let e=k.current;if(!e)return;let t=e.scrollHeight-e.scrollTop-e.clientHeight;A.current=t<48};(0,o.useLayoutEffect)(()=>{let e=k.current;e&&A.current&&(e.scrollTop=e.scrollHeight)},[l,p]),(0,o.useEffect)(()=>{let e=k.current;e&&(e.scrollTop=e.scrollHeight)},[]);let M=r.typingLabel(p,v);return(0,s.jsxs)(`div`,{className:e.cn(i.default.panel,w),...T,children:[h&&(0,s.jsx)(`header`,{className:i.default.header,children:h}),(0,s.jsx)(`div`,{ref:k,className:i.default.thread,onScroll:j,role:`log`,"aria-live":`polite`,"aria-relevant":`additions text`,tabIndex:0,"aria-label":E.thread,children:l.length===0?g??(0,s.jsx)(t.EmptyState,{title:E.empty}):D.map(t=>t.kind===`day`?(0,s.jsx)(`div`,{className:i.default.day,children:(0,s.jsx)(`span`,{className:i.default.dayLabel,children:r.dayLabel(t.date,{locale:v,now:y})})},t.key):(0,s.jsxs)(`div`,{className:e.cn(i.default.run,t.own&&i.default.ownRun),children:[m&&(0,s.jsx)(`div`,{className:i.default.avatar,"aria-hidden":`true`,children:m(t.messages[0])}),(0,s.jsxs)(`div`,{className:i.default.runBody,children:[(0,s.jsx)(`span`,{className:i.default.author,children:t.own?E.you:t.authorName??t.authorId}),(0,s.jsx)(`ul`,{className:i.default.bubbles,children:t.messages.map(a=>(0,s.jsxs)(`li`,{className:i.default.bubbleRow,children:[(0,s.jsxs)(`div`,{className:e.cn(i.default.bubble,t.own&&i.default.ownBubble,a.status===`failed`&&i.default.failedBubble),children:[(0,s.jsx)(`div`,{className:i.default.body,children:a.body}),(0,s.jsxs)(`div`,{className:i.default.meta,children:[(0,s.jsx)(`time`,{dateTime:new Date(a.sentAt).toISOString(),children:r.timeLabel(a.sentAt,v)}),t.own&&a.status&&(0,s.jsxs)(`span`,{className:e.cn(i.default.status,a.status===`failed`&&i.default.statusFailed),title:E[a.status],children:[(0,s.jsx)(`span`,{"aria-hidden":`true`,children:c[a.status]}),(0,s.jsx)(n.VisuallyHidden,{children:E[a.status]})]})]})]}),a.status===`failed`&&f&&(0,s.jsx)(`button`,{type:`button`,className:i.default.retry,onClick:()=>f(a),children:E.retry})]},a.id))})]})]},t.key))}),M&&(0,s.jsx)(`p`,{className:i.default.typing,"aria-live":`polite`,children:M}),d&&(0,s.jsx)(a.ChatComposer,{ref:O,onSend:d,locale:v,placeholder:b,actions:x,disabled:S,onError:C})]})}exports.Chat=l;
|
|
2
2
|
//# sourceMappingURL=Chat.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.cjs","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — message grouping needs\n * more context than a list does: currentUserId to decide sides, groupWindowMs and\n * now to decide where a bubble group breaks, typing for the indicator. The remaining\n * props are the composer's (placeholder, composerActions, composerDisabled, onSend,\n * onSendError) and the slots (renderAvatar, header, emptyState).\n */\nimport { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport {\n chatStrings,\n dayLabel,\n groupMessages,\n timeLabel,\n typingLabel,\n type ChatMessage,\n} from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The thread, **oldest first**. Never reordered by the component. */\n messages: readonly ChatMessage[];\n /** Author id treated as \"own\" — decides side, colour and status ticks. */\n currentUserId?: string;\n /** Renders the composer when given. Receives the trimmed text. */\n onSend?: (text: string) => void | Promise<void>;\n /** Enables the retry control on a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Names currently typing. One, two or a count is phrased for you. */\n typing?: readonly string[];\n /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n renderAvatar?: (message: ChatMessage) => ReactNode;\n /** Rendered above the thread, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no messages. */\n emptyState?: ReactNode;\n /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n groupWindowMs?: number;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n now?: number;\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Disable the composer — no permission, thread archived, offline. */\n composerDisabled?: boolean;\n /** Called when `onSend` rejects. The draft stays in the field either way. */\n onSendError?: (error: unknown) => void;\n}\n\n/** Status glyph and label for an outgoing message. */\nconst STATUS_GLYPH: Record<NonNullable<ChatMessage[\"status\"]>, string> = {\n sending: \"◌\",\n sent: \"✓\",\n read: \"✓✓\",\n failed: \"!\",\n};\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n * messages={messages}\n * currentUserId={me.id}\n * typing={typingNames}\n * onSend={(text) => send({ text })}\n * onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n messages,\n currentUserId,\n onSend,\n onRetry,\n typing = [],\n renderAvatar,\n header,\n emptyState,\n groupWindowMs,\n locale = \"pt-BR\",\n now,\n placeholder,\n composerActions,\n composerDisabled,\n onSendError,\n className,\n ...rest\n}: ChatProps) {\n const strings = chatStrings(locale);\n const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n const composer = useRef<ChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n\n /**\n * Remember whether the reader is at the bottom, before the next batch lands.\n *\n * A thread that always scrolls to the newest message yanks somebody out of the\n * history they were reading, every time anyone types. So the jump only happens\n * when they were already at the bottom — the same rule every chat app converges\n * on. The 48px slack covers a partially visible last row.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n stuckToBottom.current = distance < 48;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [messages, typing]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest message; from then on `trackPosition` decides.\n }, []);\n\n const typingText = typingLabel(typing, locale);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {messages.length === 0\n ? (emptyState ?? <EmptyState title={strings.empty} />)\n : sections.map((section) =>\n section.kind === \"day\" ? (\n <div key={section.key} className={styles.day}>\n <span className={styles.dayLabel}>\n {dayLabel(section.date, { locale, now })}\n </span>\n </div>\n ) : (\n <div\n key={section.key}\n className={cn(styles.run, section.own && styles.ownRun)}\n >\n {renderAvatar && (\n <div className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(section.messages[0])}\n </div>\n )}\n <div className={styles.runBody}>\n <span className={styles.author}>\n {section.own\n ? strings.you\n : (section.authorName ?? section.authorId)}\n </span>\n <ul className={styles.bubbles}>\n {section.messages.map((message) => (\n <li key={message.id} className={styles.bubbleRow}>\n <div\n className={cn(\n styles.bubble,\n section.own && styles.ownBubble,\n message.status === \"failed\" &&\n styles.failedBubble,\n )}\n >\n <div className={styles.body}>\n {message.body}\n </div>\n <div className={styles.meta}>\n <time\n dateTime={new Date(\n message.sentAt,\n ).toISOString()}\n >\n {timeLabel(message.sentAt, locale)}\n </time>\n {section.own && message.status && (\n <span\n className={cn(\n styles.status,\n message.status === \"failed\" &&\n styles.statusFailed,\n )}\n title={strings[message.status]}\n >\n <span aria-hidden=\"true\">\n {STATUS_GLYPH[message.status]}\n </span>\n <VisuallyHidden>\n {strings[message.status]}\n </VisuallyHidden>\n </span>\n )}\n </div>\n </div>\n {message.status === \"failed\" && onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </li>\n ))}\n </ul>\n </div>\n </div>\n ),\n )}\n </div>\n\n {typingText && (\n <p className={styles.typing} aria-live=\"polite\">\n {typingText}\n </p>\n )}\n\n {onSend && (\n <ChatComposer\n ref={composer}\n onSend={onSend}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n disabled={composerDisabled}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Chat.cjs","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — message grouping needs\n * more context than a list does: currentUserId to decide sides, groupWindowMs and\n * now to decide where a bubble group breaks, typing for the indicator. The remaining\n * props are the composer's (placeholder, composerActions, composerDisabled, onSend,\n * onSendError) and the slots (renderAvatar, header, emptyState).\n */\nimport { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport {\n chatStrings,\n dayLabel,\n groupMessages,\n timeLabel,\n typingLabel,\n type ChatMessage,\n} from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The thread, **oldest first**. Never reordered by the component. */\n messages: readonly ChatMessage[];\n /** Author id treated as \"own\" — decides side, colour and status ticks. */\n currentUserId?: string;\n /** Renders the composer when given. Receives the trimmed text. */\n onSend?: (text: string) => void | Promise<void>;\n /** Enables the retry control on a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Names currently typing. One, two or a count is phrased for you. */\n typing?: readonly string[];\n /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n renderAvatar?: (message: ChatMessage) => ReactNode;\n /** Rendered above the thread, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no messages. */\n emptyState?: ReactNode;\n /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n groupWindowMs?: number;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n now?: number;\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Disable the composer — no permission, thread archived, offline. */\n composerDisabled?: boolean;\n /** Called when `onSend` rejects. The draft stays in the field either way. */\n onSendError?: (error: unknown) => void;\n}\n\n/** Status glyph and label for an outgoing message. */\nconst STATUS_GLYPH: Record<NonNullable<ChatMessage[\"status\"]>, string> = {\n sending: \"◌\",\n sent: \"✓\",\n read: \"✓✓\",\n failed: \"!\",\n};\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n * messages={messages}\n * currentUserId={me.id}\n * typing={typingNames}\n * onSend={(text) => send({ text })}\n * onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n messages,\n currentUserId,\n onSend,\n onRetry,\n typing = [],\n renderAvatar,\n header,\n emptyState,\n groupWindowMs,\n locale = \"pt-BR\",\n now,\n placeholder,\n composerActions,\n composerDisabled,\n onSendError,\n className,\n ...rest\n}: ChatProps) {\n const strings = chatStrings(locale);\n const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n const composer = useRef<ChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n\n /**\n * Remember whether the reader is at the bottom, before the next batch lands.\n *\n * A thread that always scrolls to the newest message yanks somebody out of the\n * history they were reading, every time anyone types. So the jump only happens\n * when they were already at the bottom — the same rule every chat app converges\n * on. The 48px slack covers a partially visible last row.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n stuckToBottom.current = distance < 48;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [messages, typing]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest message; from then on `trackPosition` decides.\n }, []);\n\n const typingText = typingLabel(typing, locale);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {messages.length === 0\n ? (emptyState ?? <EmptyState title={strings.empty} />)\n : sections.map((section) =>\n section.kind === \"day\" ? (\n <div key={section.key} className={styles.day}>\n <span className={styles.dayLabel}>\n {dayLabel(section.date, { locale, now })}\n </span>\n </div>\n ) : (\n <div\n key={section.key}\n className={cn(styles.run, section.own && styles.ownRun)}\n >\n {renderAvatar && (\n <div className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(section.messages[0])}\n </div>\n )}\n <div className={styles.runBody}>\n <span className={styles.author}>\n {section.own\n ? strings.you\n : (section.authorName ?? section.authorId)}\n </span>\n <ul className={styles.bubbles}>\n {section.messages.map((message) => (\n <li key={message.id} className={styles.bubbleRow}>\n <div\n className={cn(\n styles.bubble,\n section.own && styles.ownBubble,\n message.status === \"failed\" &&\n styles.failedBubble,\n )}\n >\n <div className={styles.body}>\n {message.body}\n </div>\n <div className={styles.meta}>\n <time\n dateTime={new Date(\n message.sentAt,\n ).toISOString()}\n >\n {timeLabel(message.sentAt, locale)}\n </time>\n {section.own && message.status && (\n <span\n className={cn(\n styles.status,\n message.status === \"failed\" &&\n styles.statusFailed,\n )}\n title={strings[message.status]}\n >\n <span aria-hidden=\"true\">\n {STATUS_GLYPH[message.status]}\n </span>\n <VisuallyHidden>\n {strings[message.status]}\n </VisuallyHidden>\n </span>\n )}\n </div>\n </div>\n {message.status === \"failed\" && onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </li>\n ))}\n </ul>\n </div>\n </div>\n ),\n )}\n </div>\n\n {typingText && (\n <p className={styles.typing} aria-live=\"polite\">\n {typingText}\n </p>\n )}\n\n {onSend && (\n <ChatComposer\n ref={composer}\n onSend={onSend}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n disabled={composerDisabled}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -26,7 +26,7 @@ function g({ messages: g, currentUserId: _, onSend: v, onRetry: y, typing: b = [
|
|
|
26
26
|
};
|
|
27
27
|
d(() => {
|
|
28
28
|
let e = I.current;
|
|
29
|
-
|
|
29
|
+
e && L.current && (e.scrollTop = e.scrollHeight);
|
|
30
30
|
}, [g, b]), u(() => {
|
|
31
31
|
let e = I.current;
|
|
32
32
|
e && (e.scrollTop = e.scrollHeight);
|