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
package/README.md
CHANGED
|
@@ -201,6 +201,8 @@ import "tempest-react-sdk/styles.css";
|
|
|
201
201
|
|
|
202
202
|
This injects the design tokens (`--tempest-primary`, `--tempest-radius-md`, ...), a minimal CSS reset, and the per-component CSS Modules. Tokens live on `:root` and on `[data-tempest-theme="dark"]`, so the app can override them globally or per subtree (see [Theming](#theming-reference)).
|
|
203
203
|
|
|
204
|
+
The reset claims the document surface too — `body { margin: 0 }` plus the theme's background and text colour, and a zero-specificity `height: 100%` chain down to `#root`. Without it an `AppShell` app opens with a spurious scrollbar and an unpainted frame around the shell in dark mode. All four are one ordinary declaration away from being overridden; see [The document surface](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/styles/#the-document-surface).
|
|
205
|
+
|
|
204
206
|
The styles ship hashed under the `tempest_` namespace — they do **not** collide with Tailwind, Stitches, Linaria, or app-level CSS Modules.
|
|
205
207
|
|
|
206
208
|
**Importing less.** The JavaScript you import is tree-shaken; the CSS is not, so `styles.css` carries all ~150 components whether you mount thirteen or all of them. Pay for what you use by taking the foundation plus the sheets you need:
|
|
@@ -271,7 +273,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
271
273
|
| `perf` | `createInferenceProfiler`, `readDeviceProfile`, `cachedResponseBytes`, `formatDurationMs`, types: `InferenceProfiler`, `InferenceReport`, `InferenceReportOptions`, `DeviceProfile`, `ProfiledModel`, `ProfiledModelSize` — custo de inferência on-device: cronômetro por etapa, perfil do dispositivo (cores/RAM/heap) e tamanho dos modelos em Cache Storage |
|
|
272
274
|
| `feature-flags` | `FeatureFlagsProvider`, `useFeatureFlag`, `useFlagValue`, `createInMemoryFlags`, `createGrowthBookFeatureFlagsAdapter`, `createLaunchDarklyFeatureFlagsAdapter`, types: `FeatureFlagsAdapter`, `FlagValue`, `GrowthBookLike`, `LDClientLike` |
|
|
273
275
|
| `share` | `share`, `isShareSupported`, `shareOrDownloadBlob` (Web Share file → download fallback), types: `SharePayload`, `ShareResult`, `ShareOrDownloadOptions` |
|
|
274
|
-
| `utils` | `cn`, format BR (`formatCurrency`, `formatDate`, `formatDateForInput`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`), `storage`, strings (`slugify`, `truncate`, `capitalize`, `camelCase`, `kebabCase`, `pluralize`), numbers (`clamp`, `formatBytes`, `formatCompactNumber`, `percentOf`), arrays (`groupBy`, `uniqueBy`, `chunk`, `range`), objects (`pick`, `omit`, `deepMerge`, `isEmpty`), guards (`isDefined`, `isString`, `isNumber`, `isPlainObject`, `assertNever`), functions (`debounce`, `throttle`, `once`, `memoizeOne`), promises (`sleep`, `withTimeout`), `randomId`, `relativeTime`, `writeXlsx` (generic single-sheet OOXML `.xlsx` writer → `Uint8Array`), CSV (`toCsv`, `downloadCsv` — RFC 4180 escaping + BOM)
|
|
276
|
+
| `utils` | `cn`, format BR (`formatCurrency`, `formatDate`, `formatDateForInput`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`), `storage`, strings (`slugify`, `truncate`, `capitalize`, `camelCase`, `kebabCase`, `pluralize`), numbers (`clamp`, `formatBytes`, `formatCompactNumber`, `percentOf`), arrays (`groupBy`, `uniqueBy`, `chunk`, `range`), objects (`pick`, `omit`, `deepMerge`, `isEmpty`), guards (`isDefined`, `isString`, `isNumber`, `isPlainObject`, `assertNever`), functions (`debounce`, `throttle`, `once`, `memoizeOne`), promises (`sleep`, `withTimeout`), `randomId`, `relativeTime`, `writeXlsx` (generic single-sheet OOXML `.xlsx` writer → `Uint8Array`), CSV (`toCsv`, `downloadCsv` — RFC 4180 escaping + BOM), `setDevBuild` (tell the SDK your build is dev — required under Vite) |
|
|
275
277
|
| generic components | display (`CopyButton`, `RelativeTime`, `Money`, `TruncateText`, `VisuallyHidden`), headless (`Portal`, `ClickOutside`, `ConditionalWrapper`, `For`, `ErrorText`), media/content (`Image`, `DataList`, `DescriptionList`) |
|
|
276
278
|
| Module | Exports |
|
|
277
279
|
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -309,7 +311,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
309
311
|
| `perf` | `createInferenceProfiler`, `readDeviceProfile`, `cachedResponseBytes`, `formatDurationMs`, types: `InferenceProfiler`, `InferenceReport`, `InferenceReportOptions`, `DeviceProfile`, `ProfiledModel`, `ProfiledModelSize` — custo de inferência on-device: cronômetro por etapa, perfil do dispositivo (cores/RAM/heap) e tamanho dos modelos em Cache Storage |
|
|
310
312
|
| `feature-flags` | `FeatureFlagsProvider`, `useFeatureFlag`, `useFlagValue`, `createInMemoryFlags`, `createGrowthBookFeatureFlagsAdapter`, `createLaunchDarklyFeatureFlagsAdapter`, types: `FeatureFlagsAdapter`, `FlagValue`, `GrowthBookLike`, `LDClientLike` |
|
|
311
313
|
| `share` | `share`, `isShareSupported`, `shareOrDownloadBlob` (Web Share file → download fallback), types: `SharePayload`, `ShareResult`, `ShareOrDownloadOptions` |
|
|
312
|
-
| `utils` | `cn`, format BR (`formatCurrency`, `formatDate`, `formatDateForInput`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`), `storage`, strings (`slugify`, `truncate`, `capitalize`, `camelCase`, `kebabCase`, `pluralize`), numbers (`clamp`, `formatBytes`, `formatCompactNumber`, `percentOf`), arrays (`groupBy`, `uniqueBy`, `chunk`, `range`), objects (`pick`, `omit`, `deepMerge`, `isEmpty`), guards (`isDefined`, `isString`, `isNumber`, `isPlainObject`, `assertNever`), functions (`debounce`, `throttle`, `once`, `memoizeOne`), promises (`sleep`, `withTimeout`), `randomId`, `relativeTime`, `writeXlsx` (generic single-sheet OOXML `.xlsx` writer → `Uint8Array`), CSV (`toCsv`, `downloadCsv` — RFC 4180 escaping + BOM)
|
|
314
|
+
| `utils` | `cn`, format BR (`formatCurrency`, `formatDate`, `formatDateForInput`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`), `storage`, strings (`slugify`, `truncate`, `capitalize`, `camelCase`, `kebabCase`, `pluralize`), numbers (`clamp`, `formatBytes`, `formatCompactNumber`, `percentOf`), arrays (`groupBy`, `uniqueBy`, `chunk`, `range`), objects (`pick`, `omit`, `deepMerge`, `isEmpty`), guards (`isDefined`, `isString`, `isNumber`, `isPlainObject`, `assertNever`), functions (`debounce`, `throttle`, `once`, `memoizeOne`), promises (`sleep`, `withTimeout`), `randomId`, `relativeTime`, `writeXlsx` (generic single-sheet OOXML `.xlsx` writer → `Uint8Array`), CSV (`toCsv`, `downloadCsv` — RFC 4180 escaping + BOM), `setDevBuild` (tell the SDK your build is dev — required under Vite) |
|
|
313
315
|
| generic components | display (`CopyButton`, `RelativeTime`, `Money`, `TruncateText`, `VisuallyHidden`), headless (`Portal`, `ClickOutside`, `ConditionalWrapper`, `For`, `ErrorText`), media/content (`Image`, `DataList`, `DescriptionList`) |
|
|
314
316
|
|
|
315
317
|
Full per-module docs are published as a bilingual MkDocs site on GitHub Pages — **[Português (BR)](https://mauriciobenjamin700.github.io/tempest-react-sdk/)** / **[English (US)](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/)** (one page per module + draw.io diagrams in [`docs/diagrams/`](./docs/diagrams)). The source markdown lives in [`docs/`](./docs) (PT-BR base files + `.en.md` translations).
|
|
@@ -632,6 +634,16 @@ export async function getUser(id: string) {
|
|
|
632
634
|
|
|
633
635
|
On validation failure `parseResponse` throws an `Error` whose `message` includes the request label and the zod issues — exactly the diagnostic you want during a wire-protocol drift.
|
|
634
636
|
|
|
637
|
+
**Under Vite that report needs one line to turn on.** The SDK detects a dev build by reading `process.env.NODE_ENV`, which webpack, Rspack and Parcel substitute while building your app; Vite substitutes neither half, and a browser bundle has no `process` at all, so the read fails and the SDK stays on the safe side — the generic sentence, `vite dev` included. Say so once at bootstrap:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
import { setDevBuild } from "tempest-react-sdk";
|
|
641
|
+
|
|
642
|
+
setDevBuild(import.meta.env.DEV);
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
The SDK cannot read `import.meta.env.DEV` for you: Vite would replace it while building _this package_ and the published artifact would carry the constant. The same line un-mutes every other development-only diagnostic in the SDK.
|
|
646
|
+
|
|
635
647
|
### Upload with progress recipe
|
|
636
648
|
|
|
637
649
|
`fetch` cannot report upload progress in browsers. `uploadWithProgress` falls back to `XMLHttpRequest` internally while keeping the same `ApiError` contract:
|
package/bin/lib/css/findings.mjs
CHANGED
package/bin/lib/css/semantic.mjs
CHANGED
|
@@ -219,14 +219,64 @@ function reportDuplicateRule({ block, previous, at }) {
|
|
|
219
219
|
);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The family a token name belongs to: everything up to the last segment.
|
|
224
|
+
*
|
|
225
|
+
* `--tempest-primary-soft` → `--tempest-primary`. Returns `null` when nothing is
|
|
226
|
+
* left after the prefix, which is what keeps two-segment names like
|
|
227
|
+
* `--tempest-tx` out of the family check: their family is the bare prefix, and
|
|
228
|
+
* that matches every token there is.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} name - A custom property name.
|
|
231
|
+
* @returns {string | null} The family, or null when the name has no family.
|
|
232
|
+
*/
|
|
233
|
+
function familyOf(name) {
|
|
234
|
+
const family = name.slice(0, name.lastIndexOf("-"));
|
|
235
|
+
return family === "--tempest" || !family.startsWith("--tempest-") ? null : family;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Per-name-set cache, so the families are derived once per run and not per declaration. */
|
|
239
|
+
const FAMILY_CACHE = new WeakMap();
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The families the SDK's own tokens form, derived once per token set.
|
|
243
|
+
*
|
|
244
|
+
* @param {Set<string>} names - Declared SDK token names.
|
|
245
|
+
* @returns {Set<string>} Every family with at least one declared member.
|
|
246
|
+
*/
|
|
247
|
+
function familiesOf(names) {
|
|
248
|
+
const cached = FAMILY_CACHE.get(names);
|
|
249
|
+
if (cached) return cached;
|
|
250
|
+
const families = new Set();
|
|
251
|
+
for (const name of names) {
|
|
252
|
+
const family = familyOf(name);
|
|
253
|
+
if (family) families.add(family);
|
|
254
|
+
}
|
|
255
|
+
FAMILY_CACHE.set(names, families);
|
|
256
|
+
return families;
|
|
257
|
+
}
|
|
258
|
+
|
|
222
259
|
/**
|
|
223
260
|
* Property-level and value-level checks for a single declaration.
|
|
224
261
|
*
|
|
225
|
-
* A `var()` **with a fallback** is never reported. That is the SDK's own
|
|
226
|
-
* idiom — `var(--tempest-card-padding, var(--tempest-space-5))` is a
|
|
227
|
-
* offering an override nobody is required to set — so "this name is
|
|
228
|
-
* would fire on the pattern the docs teach. Without a fallback the
|
|
229
|
-
* declaration resolves to nothing, which is a real defect and is reported.
|
|
262
|
+
* A `var()` **with a fallback** is *almost* never reported. That is the SDK's own
|
|
263
|
+
* knob idiom — `var(--tempest-card-padding, var(--tempest-space-5))` is a
|
|
264
|
+
* component offering an override nobody is required to set — so "this name is
|
|
265
|
+
* not a token" would fire on the pattern the docs teach. Without a fallback the
|
|
266
|
+
* same declaration resolves to nothing, which is a real defect and is reported.
|
|
267
|
+
*
|
|
268
|
+
* The exception is the one case where a knob and a typo stop being
|
|
269
|
+
* indistinguishable: **the family already exists.** `--tempest-primary-contrast`
|
|
270
|
+
* is undeclared, but `--tempest-primary-*` has twelve declared siblings, so the
|
|
271
|
+
* name reads as a member of a real family that is missing — a misspelling.
|
|
272
|
+
* `--tempest-card-padding` has no declared `--tempest-card-*` sibling at all, so
|
|
273
|
+
* it reads as what it is, a knob this component invented.
|
|
274
|
+
*
|
|
275
|
+
* Measured over the SDK's own `src/` when the rule was written: five hits, five
|
|
276
|
+
* real defects, no false positive. Two weaker signals were measured and dropped
|
|
277
|
+
* — matching on the *last* segment instead (`--tempest-font-size-sm` looks like
|
|
278
|
+
* `--tempest-text-sm`) fired on sixteen names of which three were real, because
|
|
279
|
+
* every layout knob in `utilities.css` ends in `-gap` or `-width`.
|
|
230
280
|
*/
|
|
231
281
|
function checkDecl({ decl, prop, tokens, definedVars, at }) {
|
|
232
282
|
if (isCheckableProperty(prop) && !KNOWN_PROPERTIES.has(prop)) {
|
|
@@ -240,8 +290,22 @@ function checkDecl({ decl, prop, tokens, definedVars, at }) {
|
|
|
240
290
|
}
|
|
241
291
|
}
|
|
242
292
|
|
|
293
|
+
const families = familiesOf(tokens.names);
|
|
243
294
|
for (const ref of varReferences(decl.value)) {
|
|
244
|
-
if (ref.
|
|
295
|
+
if (definedVars.has(ref.name) || tokens.names.has(ref.name)) continue;
|
|
296
|
+
if (ref.hasFallback) {
|
|
297
|
+
if (!ref.name.startsWith("--tempest-") || tokens.names.size === 0) continue;
|
|
298
|
+
const family = familyOf(ref.name);
|
|
299
|
+
if (!family || !families.has(family)) continue;
|
|
300
|
+
const suggestion = nearest(ref.name, tokens.names, 3);
|
|
301
|
+
at(
|
|
302
|
+
"token-family-typo",
|
|
303
|
+
decl.line,
|
|
304
|
+
`\`${ref.name}\` is not a token, but \`${family}-*\` is a real family — the fallback ` +
|
|
305
|
+
`hides the misspelling instead of failing${suggestion ? `. Did you mean \`${suggestion}\`?` : ""}`,
|
|
306
|
+
);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
245
309
|
if (ref.name.startsWith("--tempest-")) {
|
|
246
310
|
if (tokens.names.size === 0) continue;
|
|
247
311
|
const suggestion = nearest(ref.name, tokens.names, 3);
|
|
@@ -4,7 +4,13 @@ import { parseCss } from "./parse.mjs";
|
|
|
4
4
|
import { analyzeParsed, declSignature, definedCustomProperties } from "./semantic.mjs";
|
|
5
5
|
|
|
6
6
|
const TOKENS = {
|
|
7
|
-
names: new Set([
|
|
7
|
+
names: new Set([
|
|
8
|
+
"--tempest-space-2",
|
|
9
|
+
"--tempest-primary",
|
|
10
|
+
"--tempest-text",
|
|
11
|
+
"--tempest-primary-soft",
|
|
12
|
+
"--tempest-primary-hover",
|
|
13
|
+
]),
|
|
8
14
|
byValue: new Map([
|
|
9
15
|
["8px", ["--tempest-space-2"]],
|
|
10
16
|
["#0d6efd", ["--tempest-primary"]],
|
|
@@ -152,6 +158,51 @@ describe("analyzeParsed — custom properties", () => {
|
|
|
152
158
|
expect(codes(".a { width: var(--row-height, 40px); }")).toEqual([]);
|
|
153
159
|
});
|
|
154
160
|
|
|
161
|
+
it("flags a fallback whose name belongs to a family the SDK actually declares", () => {
|
|
162
|
+
// The one case where a knob and a typo stop being indistinguishable.
|
|
163
|
+
// `--tempest-primary-*` has declared siblings, so an undeclared member of
|
|
164
|
+
// it reads as a misspelling rather than as a hook an app may set.
|
|
165
|
+
const found = first(
|
|
166
|
+
".a { color: var(--tempest-primary-contrast, #fff); }",
|
|
167
|
+
"token-family-typo",
|
|
168
|
+
);
|
|
169
|
+
expect(found.severity).toBe("warn");
|
|
170
|
+
expect(found.message).toContain("--tempest-primary-*");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("adds a suggestion only when a declared token is actually close", () => {
|
|
174
|
+
// The family match is the finding; the "did you mean" is best effort. A
|
|
175
|
+
// real misspelling is often far from every token — `-contrast` is nowhere
|
|
176
|
+
// near `-soft` — and inventing a suggestion there would send the reader to
|
|
177
|
+
// the wrong token with the tool's confidence behind it.
|
|
178
|
+
const far = first(
|
|
179
|
+
".a { color: var(--tempest-primary-contrast, #fff); }",
|
|
180
|
+
"token-family-typo",
|
|
181
|
+
);
|
|
182
|
+
expect(far.message).not.toContain("Did you mean");
|
|
183
|
+
|
|
184
|
+
const near = first(".a { color: var(--tempest-primary-sofr, #fff); }", "token-family-typo");
|
|
185
|
+
expect(near.message).toContain("Did you mean `--tempest-primary-soft`");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("stays quiet on a knob whose family the SDK never declares", () => {
|
|
189
|
+
// `--tempest-card-*` has no declared sibling at all — this is a component
|
|
190
|
+
// inventing a hook, which is the idiom the docs teach. Reporting it is what
|
|
191
|
+
// made the first version of this analysis fire 43 times on the SDK's own CSS.
|
|
192
|
+
expect(codes(".a { padding: var(--tempest-card-padding, 8px); }")).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("stays quiet on a two-segment name, whose family is the bare prefix", () => {
|
|
196
|
+
// `--tempest-tx` has no family: everything before the last segment is
|
|
197
|
+
// `--tempest`, which matches every token there is. Treating that as a match
|
|
198
|
+
// is how a naive version of this rule fires on `Tooltip`'s transform knobs.
|
|
199
|
+
expect(codes(".a { translate: var(--tempest-tx, 0); }")).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("stays quiet when the name is a token, fallback or not", () => {
|
|
203
|
+
expect(codes(".a { color: var(--tempest-primary-soft, #fff); }")).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
|
|
155
206
|
it("accepts a var() the same sheet defines", () => {
|
|
156
207
|
const text = ":root { --mine: 4px; }\n.a { gap: var(--mine); }\n";
|
|
157
208
|
const parsed = parseCss(text);
|
|
@@ -28,9 +28,9 @@ describe("scanIconSlugs", () => {
|
|
|
28
28
|
|
|
29
29
|
describe("renderRegistry", () => {
|
|
30
30
|
it("emits a createIconRegistry call with static imports", () => {
|
|
31
|
-
const out = renderRegistry(["save", "
|
|
31
|
+
const out = renderRegistry(["save", "shopping-cart"], iconAliases);
|
|
32
32
|
expect(out).toContain(`import { createIconRegistry } from "tempest-react-sdk/icons";`);
|
|
33
|
-
expect(out).toContain(`import { Save,
|
|
33
|
+
expect(out).toContain(`import { Save, ShoppingCart } from "lucide-react";`);
|
|
34
34
|
expect(out).toContain(` "save": Save,`);
|
|
35
35
|
});
|
|
36
36
|
it("imports the canonical component for an alias but keeps the alias key", () => {
|
|
@@ -42,8 +42,8 @@ describe("renderRegistry", () => {
|
|
|
42
42
|
expect(renderRegistry([], iconAliases)).toContain("createIconRegistry({})");
|
|
43
43
|
});
|
|
44
44
|
it("is deterministic regardless of input order", () => {
|
|
45
|
-
expect(renderRegistry(["
|
|
46
|
-
renderRegistry(["save", "
|
|
45
|
+
expect(renderRegistry(["shopping-cart", "save"], iconAliases)).toBe(
|
|
46
|
+
renderRegistry(["save", "shopping-cart"], iconAliases),
|
|
47
47
|
);
|
|
48
48
|
});
|
|
49
49
|
});
|
package/dist/br/geocode.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../geo/distance.cjs"),t=require("./state-geo.cjs");var n=null,r=null;function i(e){return e.normalize(`NFD`).replace(/\p{Diacritic}/gu,``).trim().toLowerCase()}async function a(){return n||(r||=Promise.resolve().then(()=>require("./data/br-centroids.cjs")).then(e=>{let t=e.default??e,r={};for(let[e,[n,i]]of Object.entries(t.states))r[e]={latitude:i,longitude:n};return n={states:r,municipalities:t.municipalities.map(([e,t,n,r,i])=>({id:e,name:t,uf:n,latitude:i,longitude:r}))},n}),r)}async function o(t){let{municipalities:n}=await a(),r=null,i=1/0;for(let a of n){let n=e.haversineKm(t,a);n<i&&(i=n,r=a)}return r?{...r,distanceKm:i}:null}async function s(e,t){let{municipalities:n}=await a(),r=i(e);return n.filter(e=>(!t||e.uf===t)&&i(e.name)===r)}async function c(e,t={}){let{uf:n,limit:r=20}=t,o=i(e);if(!o)return[];let{municipalities:s}=await a();return s.filter(e=>(!n||e.uf===n)&&i(e.name).includes(o)).sort((e,t)
|
|
1
|
+
const e=require("../geo/distance.cjs"),t=require("./state-geo.cjs");var n=null,r=null;function i(e){return e.normalize(`NFD`).replace(/\p{Diacritic}/gu,``).trim().toLowerCase()}async function a(){return n||(r||=Promise.resolve().then(()=>require("./data/br-centroids.cjs")).then(e=>{let t=e.default??e,r={};for(let[e,[n,i]]of Object.entries(t.states))r[e]={latitude:i,longitude:n};return n={states:r,municipalities:t.municipalities.map(([e,t,n,r,i])=>({id:e,name:t,uf:n,latitude:i,longitude:r}))},n}),r)}async function o(t){let{municipalities:n}=await a(),r=null,i=1/0;for(let a of n){let n=e.haversineKm(t,a);n<i&&(i=n,r=a)}return r?{...r,distanceKm:i}:null}async function s(e,t){let{municipalities:n}=await a(),r=i(e);return n.filter(e=>(!t||e.uf===t)&&i(e.name)===r)}async function c(e,t={}){let{uf:n,limit:r=20}=t,o=i(e);if(!o)return[];let{municipalities:s}=await a();return s.filter(e=>(!n||e.uf===n)&&i(e.name).includes(o)).sort((e,t)=>+!i(e.name).startsWith(o)-!i(t.name).startsWith(o)||e.name.localeCompare(t.name,`pt-BR`)).slice(0,r)}async function l(e){let{municipalities:t}=await a();return t.find(t=>t.id===e)??null}async function u(e){let{states:t}=await a();return t[e]??null}function d(e,t,n){let r=!1;for(let i=0,a=n.length-1;i<n.length;a=i,i+=1){let[o,s]=n[i],[c,l]=n[a];s>t!=l>t&&e<(c-o)*(t-s)/(l-s)+o&&(r=!r)}return r}function f(e,t,n){if(n.length===0||!d(e,t,n[0]))return!1;for(let r=1;r<n.length;r+=1)if(d(e,t,n[r]))return!1;return!0}function p(e,t,n){return n.type===`Polygon`?f(e,t,n.coordinates):n.coordinates.some(n=>f(e,t,n))}async function m(e,n={}){let r=n.uf?null:await o(e),i=n.uf??r?.uf;if(!i)return null;let a=(await t.loadStateMunicipalities(i))?.features.find(t=>p(e.longitude,e.latitude,t.geometry));if(a)return{id:a.properties.id,name:a.properties.name,uf:i};let s=r??await o(e);return s?{id:s.id,name:s.name,uf:s.uf}:null}exports.geocodeMunicipality=s,exports.municipalityCentroid=l,exports.nearestMunicipality=o,exports.reverseGeocode=m,exports.searchMunicipalities=c,exports.stateCentroid=u;
|
|
2
2
|
//# sourceMappingURL=geocode.cjs.map
|
package/dist/br/geocode.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"geocode.cjs","names":[],"sources":["../../src/br/geocode.ts"],"sourcesContent":["import { haversineKm } from \"@/geo/distance\";\nimport type { Coordinate } from \"@/geo/types\";\nimport type { BrUfGeometry, Ring } from \"./br-geo\";\nimport type { UF } from \"./locations\";\nimport { loadStateMunicipalities } from \"./state-geo\";\n\n/** A municipality with its representative coordinate (IBGE centroid). */\nexport interface MunicipalityCentroid {\n /** 7-digit IBGE code. */\n id: string;\n /** Municipality name. */\n name: string;\n /** Federative unit. */\n uf: UF;\n latitude: number;\n longitude: number;\n}\n\n/** {@link MunicipalityCentroid} plus the distance from the query point. */\nexport interface NearestMunicipality extends MunicipalityCentroid {\n /** Great-circle distance from the query coordinate, in kilometers. */\n distanceKm: number;\n}\n\n/** A municipality identified by point-in-polygon (see {@link reverseGeocode}). */\nexport interface ReverseGeocodeResult {\n id: string;\n name: string;\n uf: UF;\n}\n\ninterface RawIndex {\n states: Record<string, [number, number]>;\n municipalities: [string, string, string, number, number][];\n}\n\ninterface CentroidIndex {\n states: Partial<Record<UF, Coordinate>>;\n municipalities: MunicipalityCentroid[];\n}\n\nlet cache: CentroidIndex | null = null;\nlet pending: Promise<CentroidIndex> | null = null;\n\n/** Strip accents and lowercase, for accent-insensitive name matching. */\nfunction normalizeName(value: string): string {\n return value\n .normalize(\"NFD\")\n .replace(/\\p{Diacritic}/gu, \"\")\n .trim()\n .toLowerCase();\n}\n\n/**\n * Lazily load and cache the compact centroid index (~97 KB gzip). Kept out of\n * the synchronous barrel so importing `/br` for data/maps never pulls it — only\n * a geocode call fetches it, and only once.\n */\nasync function loadIndex(): Promise<CentroidIndex> {\n if (cache) return cache;\n if (!pending) {\n pending = import(\"./data/br-centroids.json\").then((mod) => {\n const raw = (mod.default ?? mod) as unknown as RawIndex;\n const states: Partial<Record<UF, Coordinate>> = {};\n for (const [uf, [lon, lat]] of Object.entries(raw.states)) {\n states[uf as UF] = { latitude: lat, longitude: lon };\n }\n cache = {\n states,\n municipalities: raw.municipalities.map(([id, name, uf, lon, lat]) => ({\n id,\n name,\n uf: uf as UF,\n latitude: lat,\n longitude: lon,\n })),\n };\n return cache;\n });\n }\n return pending;\n}\n\n/**\n * Nearest Brazilian municipality to a coordinate by **centroid distance** —\n * fast and geometry-free (only the small centroid index loads). Approximate:\n * near borders or inside large municipalities a neighbor's centroid can be\n * closer. For an exact containing municipality use {@link reverseGeocode}.\n *\n * @param coord - Query coordinate.\n * @returns The nearest municipality with `distanceKm`, or `null` if the index\n * is somehow empty.\n *\n * @example\n * const near = await nearestMunicipality({ latitude: -23.55, longitude: -46.63 });\n * // { id, name, uf: \"SP\", …, distanceKm }\n */\nexport async function nearestMunicipality(coord: Coordinate): Promise<NearestMunicipality | null> {\n const { municipalities } = await loadIndex();\n let best: MunicipalityCentroid | null = null;\n let bestDistance = Infinity;\n for (const m of municipalities) {\n const d = haversineKm(coord, m);\n if (d < bestDistance) {\n bestDistance = d;\n best = m;\n }\n }\n return best ? { ...best, distanceKm: bestDistance } : null;\n}\n\n/**\n * Forward-geocode a municipality by name (accent-insensitive), optionally scoped\n * to a UF. Returns every exact-name match — the same name can occur in several\n * states (e.g. \"Bonito\").\n *\n * @param name - Municipality name.\n * @param uf - Optional UF to restrict the search.\n * @returns Matching municipalities (empty array when none match).\n */\nexport async function geocodeMunicipality(name: string, uf?: UF): Promise<MunicipalityCentroid[]> {\n const { municipalities } = await loadIndex();\n const target = normalizeName(name);\n return municipalities.filter((m) => (!uf || m.uf === uf) && normalizeName(m.name) === target);\n}\n\n/**\n * Substring search over municipality names (accent-insensitive), for\n * autocomplete. Ranked prefix-first, then alphabetically.\n *\n * @param query - Partial name.\n * @param options - `uf` to scope, `limit` (default 20).\n * @returns Ranked matches.\n */\nexport async function searchMunicipalities(\n query: string,\n options: { uf?: UF; limit?: number } = {},\n): Promise<MunicipalityCentroid[]> {\n const { uf, limit = 20 } = options;\n const q = normalizeName(query);\n if (!q) return [];\n const { municipalities } = await loadIndex();\n const hits = municipalities\n .filter((m) => (!uf || m.uf === uf) && normalizeName(m.name).includes(q))\n .sort((a, b) => {\n const ap = normalizeName(a.name).startsWith(q) ? 0 : 1;\n const bp = normalizeName(b.name).startsWith(q) ? 0 : 1;\n return ap - bp || a.name.localeCompare(b.name, \"pt-BR\");\n });\n return hits.slice(0, limit);\n}\n\n/**\n * Centroid of a municipality by IBGE code.\n *\n * @param id - 7-digit IBGE code.\n * @returns The municipality centroid, or `null` if unknown.\n */\nexport async function municipalityCentroid(id: string): Promise<MunicipalityCentroid | null> {\n const { municipalities } = await loadIndex();\n return municipalities.find((m) => m.id === id) ?? null;\n}\n\n/**\n * Centroid of a federative unit.\n *\n * @param uf - Federative unit acronym.\n * @returns The state centroid, or `null` if unknown.\n */\nexport async function stateCentroid(uf: UF): Promise<Coordinate | null> {\n const { states } = await loadIndex();\n return states[uf] ?? null;\n}\n\n/** Ray-casting point-in-ring test (`ring` is `[lon, lat]` pairs). */\nfunction pointInRing(lon: number, lat: number, ring: Ring): boolean {\n let inside = false;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {\n const [xi, yi] = ring[i];\n const [xj, yj] = ring[j];\n const intersects = yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;\n if (intersects) inside = !inside;\n }\n return inside;\n}\n\n/** True when the point lies in a polygon's outer ring and not in a hole. */\nfunction pointInPolygon(lon: number, lat: number, rings: Ring[]): boolean {\n if (rings.length === 0 || !pointInRing(lon, lat, rings[0])) return false;\n for (let i = 1; i < rings.length; i += 1) {\n if (pointInRing(lon, lat, rings[i])) return false; // inside a hole\n }\n return true;\n}\n\n/** True when the point lies inside a (multi)polygon geometry. */\nfunction pointInGeometry(lon: number, lat: number, geom: BrUfGeometry): boolean {\n if (geom.type === \"Polygon\") {\n return pointInPolygon(lon, lat, geom.coordinates as Ring[]);\n }\n return (geom.coordinates as Ring[][]).some((poly) => pointInPolygon(lon, lat, poly));\n}\n\n/**\n * Exact reverse-geocode: the municipality whose (simplified) boundary\n * **contains** the coordinate, via point-in-polygon. Loads one state's geometry\n * chunk — pass `uf` when you know it to skip the centroid lookup that picks the\n * candidate state. Boundaries are simplified (~2 km), so points within ~1-2 km\n * of a border may resolve to the neighbor; offshore points return `null`.\n *\n * @param coord - Query coordinate.\n * @param options - `uf` to force the state searched.\n * @returns The containing municipality, or `null` (falls back to the nearest\n * centroid municipality when the point is inside no polygon of the state).\n *\n * @example\n * const here = await reverseGeocode({ latitude: -23.5505, longitude: -46.6333 });\n * // { id: \"3550308\", name: \"São Paulo\", uf: \"SP\" }\n */\nexport async function reverseGeocode(\n coord: Coordinate,\n options: { uf?: UF } = {},\n): Promise<ReverseGeocodeResult | null> {\n const nearest = options.uf ? null : await nearestMunicipality(coord);\n const uf = options.uf ?? nearest?.uf;\n if (!uf) return null;\n\n const state = await loadStateMunicipalities(uf);\n const hit = state?.features.find((f) =>\n pointInGeometry(coord.longitude, coord.latitude, f.geometry),\n );\n if (hit) return { id: hit.properties.id, name: hit.properties.name, uf };\n\n // Point inside no polygon (border/offshore) — fall back to nearest centroid.\n const fallback = nearest ?? (await nearestMunicipality(coord));\n return fallback ? { id: fallback.id, name: fallback.name, uf: fallback.uf } : null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"geocode.cjs","names":[],"sources":["../../src/br/geocode.ts"],"sourcesContent":["import { haversineKm } from \"@/geo/distance\";\nimport type { Coordinate } from \"@/geo/types\";\nimport type { BrUfGeometry, Ring } from \"./br-geo\";\nimport type { UF } from \"./locations\";\nimport { loadStateMunicipalities } from \"./state-geo\";\n\n/** A municipality with its representative coordinate (IBGE centroid). */\nexport interface MunicipalityCentroid {\n /** 7-digit IBGE code. */\n id: string;\n /** Municipality name. */\n name: string;\n /** Federative unit. */\n uf: UF;\n latitude: number;\n longitude: number;\n}\n\n/** {@link MunicipalityCentroid} plus the distance from the query point. */\nexport interface NearestMunicipality extends MunicipalityCentroid {\n /** Great-circle distance from the query coordinate, in kilometers. */\n distanceKm: number;\n}\n\n/** A municipality identified by point-in-polygon (see {@link reverseGeocode}). */\nexport interface ReverseGeocodeResult {\n id: string;\n name: string;\n uf: UF;\n}\n\ninterface RawIndex {\n states: Record<string, [number, number]>;\n municipalities: [string, string, string, number, number][];\n}\n\ninterface CentroidIndex {\n states: Partial<Record<UF, Coordinate>>;\n municipalities: MunicipalityCentroid[];\n}\n\nlet cache: CentroidIndex | null = null;\nlet pending: Promise<CentroidIndex> | null = null;\n\n/** Strip accents and lowercase, for accent-insensitive name matching. */\nfunction normalizeName(value: string): string {\n return value\n .normalize(\"NFD\")\n .replace(/\\p{Diacritic}/gu, \"\")\n .trim()\n .toLowerCase();\n}\n\n/**\n * Lazily load and cache the compact centroid index (~97 KB gzip). Kept out of\n * the synchronous barrel so importing `/br` for data/maps never pulls it — only\n * a geocode call fetches it, and only once.\n */\nasync function loadIndex(): Promise<CentroidIndex> {\n if (cache) return cache;\n if (!pending) {\n pending = import(\"./data/br-centroids.json\").then((mod) => {\n const raw = (mod.default ?? mod) as unknown as RawIndex;\n const states: Partial<Record<UF, Coordinate>> = {};\n for (const [uf, [lon, lat]] of Object.entries(raw.states)) {\n states[uf as UF] = { latitude: lat, longitude: lon };\n }\n cache = {\n states,\n municipalities: raw.municipalities.map(([id, name, uf, lon, lat]) => ({\n id,\n name,\n uf: uf as UF,\n latitude: lat,\n longitude: lon,\n })),\n };\n return cache;\n });\n }\n return pending;\n}\n\n/**\n * Nearest Brazilian municipality to a coordinate by **centroid distance** —\n * fast and geometry-free (only the small centroid index loads). Approximate:\n * near borders or inside large municipalities a neighbor's centroid can be\n * closer. For an exact containing municipality use {@link reverseGeocode}.\n *\n * @param coord - Query coordinate.\n * @returns The nearest municipality with `distanceKm`, or `null` if the index\n * is somehow empty.\n *\n * @example\n * const near = await nearestMunicipality({ latitude: -23.55, longitude: -46.63 });\n * // { id, name, uf: \"SP\", …, distanceKm }\n */\nexport async function nearestMunicipality(coord: Coordinate): Promise<NearestMunicipality | null> {\n const { municipalities } = await loadIndex();\n let best: MunicipalityCentroid | null = null;\n let bestDistance = Infinity;\n for (const m of municipalities) {\n const d = haversineKm(coord, m);\n if (d < bestDistance) {\n bestDistance = d;\n best = m;\n }\n }\n return best ? { ...best, distanceKm: bestDistance } : null;\n}\n\n/**\n * Forward-geocode a municipality by name (accent-insensitive), optionally scoped\n * to a UF. Returns every exact-name match — the same name can occur in several\n * states (e.g. \"Bonito\").\n *\n * @param name - Municipality name.\n * @param uf - Optional UF to restrict the search.\n * @returns Matching municipalities (empty array when none match).\n */\nexport async function geocodeMunicipality(name: string, uf?: UF): Promise<MunicipalityCentroid[]> {\n const { municipalities } = await loadIndex();\n const target = normalizeName(name);\n return municipalities.filter((m) => (!uf || m.uf === uf) && normalizeName(m.name) === target);\n}\n\n/**\n * Substring search over municipality names (accent-insensitive), for\n * autocomplete. Ranked prefix-first, then alphabetically.\n *\n * @param query - Partial name.\n * @param options - `uf` to scope, `limit` (default 20).\n * @returns Ranked matches.\n */\nexport async function searchMunicipalities(\n query: string,\n options: { uf?: UF; limit?: number } = {},\n): Promise<MunicipalityCentroid[]> {\n const { uf, limit = 20 } = options;\n const q = normalizeName(query);\n if (!q) return [];\n const { municipalities } = await loadIndex();\n const hits = municipalities\n .filter((m) => (!uf || m.uf === uf) && normalizeName(m.name).includes(q))\n .sort((a, b) => {\n const ap = normalizeName(a.name).startsWith(q) ? 0 : 1;\n const bp = normalizeName(b.name).startsWith(q) ? 0 : 1;\n return ap - bp || a.name.localeCompare(b.name, \"pt-BR\");\n });\n return hits.slice(0, limit);\n}\n\n/**\n * Centroid of a municipality by IBGE code.\n *\n * @param id - 7-digit IBGE code.\n * @returns The municipality centroid, or `null` if unknown.\n */\nexport async function municipalityCentroid(id: string): Promise<MunicipalityCentroid | null> {\n const { municipalities } = await loadIndex();\n return municipalities.find((m) => m.id === id) ?? null;\n}\n\n/**\n * Centroid of a federative unit.\n *\n * @param uf - Federative unit acronym.\n * @returns The state centroid, or `null` if unknown.\n */\nexport async function stateCentroid(uf: UF): Promise<Coordinate | null> {\n const { states } = await loadIndex();\n return states[uf] ?? null;\n}\n\n/** Ray-casting point-in-ring test (`ring` is `[lon, lat]` pairs). */\nfunction pointInRing(lon: number, lat: number, ring: Ring): boolean {\n let inside = false;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {\n const [xi, yi] = ring[i];\n const [xj, yj] = ring[j];\n const intersects = yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;\n if (intersects) inside = !inside;\n }\n return inside;\n}\n\n/** True when the point lies in a polygon's outer ring and not in a hole. */\nfunction pointInPolygon(lon: number, lat: number, rings: Ring[]): boolean {\n if (rings.length === 0 || !pointInRing(lon, lat, rings[0])) return false;\n for (let i = 1; i < rings.length; i += 1) {\n if (pointInRing(lon, lat, rings[i])) return false; // inside a hole\n }\n return true;\n}\n\n/** True when the point lies inside a (multi)polygon geometry. */\nfunction pointInGeometry(lon: number, lat: number, geom: BrUfGeometry): boolean {\n if (geom.type === \"Polygon\") {\n return pointInPolygon(lon, lat, geom.coordinates as Ring[]);\n }\n return (geom.coordinates as Ring[][]).some((poly) => pointInPolygon(lon, lat, poly));\n}\n\n/**\n * Exact reverse-geocode: the municipality whose (simplified) boundary\n * **contains** the coordinate, via point-in-polygon. Loads one state's geometry\n * chunk — pass `uf` when you know it to skip the centroid lookup that picks the\n * candidate state. Boundaries are simplified (~2 km), so points within ~1-2 km\n * of a border may resolve to the neighbor; offshore points return `null`.\n *\n * @param coord - Query coordinate.\n * @param options - `uf` to force the state searched.\n * @returns The containing municipality, or `null` (falls back to the nearest\n * centroid municipality when the point is inside no polygon of the state).\n *\n * @example\n * const here = await reverseGeocode({ latitude: -23.5505, longitude: -46.6333 });\n * // { id: \"3550308\", name: \"São Paulo\", uf: \"SP\" }\n */\nexport async function reverseGeocode(\n coord: Coordinate,\n options: { uf?: UF } = {},\n): Promise<ReverseGeocodeResult | null> {\n const nearest = options.uf ? null : await nearestMunicipality(coord);\n const uf = options.uf ?? nearest?.uf;\n if (!uf) return null;\n\n const state = await loadStateMunicipalities(uf);\n const hit = state?.features.find((f) =>\n pointInGeometry(coord.longitude, coord.latitude, f.geometry),\n );\n if (hit) return { id: hit.properties.id, name: hit.properties.name, uf };\n\n // Point inside no polygon (border/offshore) — fall back to nearest centroid.\n const fallback = nearest ?? (await nearestMunicipality(coord));\n return fallback ? { id: fallback.id, name: fallback.name, uf: fallback.uf } : null;\n}\n"],"mappings":"oEAyCA,IAAI,EAA8B,KAC9B,EAAyC,KAG7C,SAAS,EAAc,EAAuB,CAC1C,OAAO,EACF,UAAU,KAAK,CAAC,CAChB,QAAQ,kBAAmB,EAAE,CAAC,CAC9B,KAAK,CAAC,CACN,YAAY,CACrB,CAOA,eAAe,GAAoC,CAsB/C,OArBI,IACJ,AACI,IAAA,QAAA,QAAA,CAAA,CAAA,SAAA,QAAU,yBAAA,CAAA,CAAA,CAAmC,KAAM,GAAQ,CACvD,IAAM,EAAO,EAAI,SAAW,EACtB,EAA0C,CAAC,EACjD,IAAK,GAAM,CAAC,EAAI,CAAC,EAAK,MAAS,OAAO,QAAQ,EAAI,MAAM,EACpD,EAAO,GAAY,CAAE,SAAU,EAAK,UAAW,CAAI,EAYvD,MAVA,GAAQ,CACJ,SACA,eAAgB,EAAI,eAAe,KAAK,CAAC,EAAI,EAAM,EAAI,EAAK,MAAU,CAClE,KACA,OACI,KACJ,SAAU,EACV,UAAW,CACf,EAAE,CACN,EACO,CACX,CAAC,EAEE,EACX,CAgBA,eAAsB,EAAoB,EAAwD,CAC9F,GAAM,CAAE,kBAAmB,MAAM,EAAU,EACvC,EAAoC,KACpC,EAAe,IACnB,IAAK,IAAM,KAAK,EAAgB,CAC5B,IAAM,EAAI,EAAA,YAAY,EAAO,CAAC,EAC1B,EAAI,IACJ,EAAe,EACf,EAAO,EAEf,CACA,OAAO,EAAO,CAAE,GAAG,EAAM,WAAY,CAAa,EAAI,IAC1D,CAWA,eAAsB,EAAoB,EAAc,EAA0C,CAC9F,GAAM,CAAE,kBAAmB,MAAM,EAAU,EACrC,EAAS,EAAc,CAAI,EACjC,OAAO,EAAe,OAAQ,IAAO,CAAC,GAAM,EAAE,KAAO,IAAO,EAAc,EAAE,IAAI,IAAM,CAAM,CAChG,CAUA,eAAsB,EAClB,EACA,EAAuC,CAAC,EACT,CAC/B,GAAM,CAAE,KAAI,QAAQ,IAAO,EACrB,EAAI,EAAc,CAAK,EAC7B,GAAI,CAAC,EAAG,MAAO,CAAC,EAChB,GAAM,CAAE,kBAAmB,MAAM,EAAU,EAQ3C,OAPa,EACR,OAAQ,IAAO,CAAC,GAAM,EAAE,KAAO,IAAO,EAAc,EAAE,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CACxE,MAAM,EAAG,IACK,IAAc,EAAE,IAAI,CAAC,CAAC,WAAW,CAAC,EAClC,GAAc,EAAE,IAAI,CAAC,CAAC,WAAW,CAAC,GAC3B,EAAE,KAAK,cAAc,EAAE,KAAM,OAAO,CAEvD,CAAA,CAAK,MAAM,EAAG,CAAK,CAC9B,CAQA,eAAsB,EAAqB,EAAkD,CACzF,GAAM,CAAE,kBAAmB,MAAM,EAAU,EAC3C,OAAO,EAAe,KAAM,GAAM,EAAE,KAAO,CAAE,GAAK,IACtD,CAQA,eAAsB,EAAc,EAAoC,CACpE,GAAM,CAAE,UAAW,MAAM,EAAU,EACnC,OAAO,EAAO,IAAO,IACzB,CAGA,SAAS,EAAY,EAAa,EAAa,EAAqB,CAChE,IAAI,EAAS,GACb,IAAK,IAAI,EAAI,EAAG,EAAI,EAAK,OAAS,EAAG,EAAI,EAAK,OAAQ,EAAI,EAAG,GAAK,EAAG,CACjE,GAAM,CAAC,EAAI,GAAM,EAAK,GAChB,CAAC,EAAI,GAAM,EAAK,GACH,EAAK,GAAQ,EAAK,GAAO,GAAQ,EAAK,IAAO,EAAM,IAAQ,EAAK,GAAM,IACzE,EAAS,CAAC,EAC9B,CACA,OAAO,CACX,CAGA,SAAS,EAAe,EAAa,EAAa,EAAwB,CACtE,GAAI,EAAM,SAAW,GAAK,CAAC,EAAY,EAAK,EAAK,EAAM,EAAE,EAAG,MAAO,GACnE,IAAK,IAAI,EAAI,EAAG,EAAI,EAAM,OAAQ,GAAK,EACnC,GAAI,EAAY,EAAK,EAAK,EAAM,EAAE,EAAG,MAAO,GAEhD,MAAO,EACX,CAGA,SAAS,EAAgB,EAAa,EAAa,EAA6B,CAI5E,OAHI,EAAK,OAAS,UACP,EAAe,EAAK,EAAK,EAAK,WAAqB,EAEtD,EAAK,YAAyB,KAAM,GAAS,EAAe,EAAK,EAAK,CAAI,CAAC,CACvF,CAkBA,eAAsB,EAClB,EACA,EAAuB,CAAC,EACY,CACpC,IAAM,EAAU,EAAQ,GAAK,KAAO,MAAM,EAAoB,CAAK,EAC7D,EAAK,EAAQ,IAAM,GAAS,GAClC,GAAI,CAAC,EAAI,OAAO,KAGhB,IAAM,GAAM,MADQ,EAAA,wBAAwB,CAAE,EAAA,EAC3B,SAAS,KAAM,GAC9B,EAAgB,EAAM,UAAW,EAAM,SAAU,EAAE,QAAQ,CAC/D,EACA,GAAI,EAAK,MAAO,CAAE,GAAI,EAAI,WAAW,GAAI,KAAM,EAAI,WAAW,KAAM,IAAG,EAGvE,IAAM,EAAW,GAAY,MAAM,EAAoB,CAAK,EAC5D,OAAO,EAAW,CAAE,GAAI,EAAS,GAAI,KAAM,EAAS,KAAM,GAAI,EAAS,EAAG,EAAI,IAClF"}
|
package/dist/br/geocode.js
CHANGED
|
@@ -43,7 +43,7 @@ async function c(e, t = {}) {
|
|
|
43
43
|
let { uf: n, limit: r = 20 } = t, o = i(e);
|
|
44
44
|
if (!o) return [];
|
|
45
45
|
let { municipalities: s } = await a();
|
|
46
|
-
return s.filter((e) => (!n || e.uf === n) && i(e.name).includes(o)).sort((e, t) =>
|
|
46
|
+
return s.filter((e) => (!n || e.uf === n) && i(e.name).includes(o)).sort((e, t) => +!i(e.name).startsWith(o) - !i(t.name).startsWith(o) || e.name.localeCompare(t.name, "pt-BR")).slice(0, r);
|
|
47
47
|
}
|
|
48
48
|
async function l(e) {
|
|
49
49
|
let { municipalities: t } = await a();
|
package/dist/br/geocode.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"geocode.js","names":[],"sources":["../../src/br/geocode.ts"],"sourcesContent":["import { haversineKm } from \"@/geo/distance\";\nimport type { Coordinate } from \"@/geo/types\";\nimport type { BrUfGeometry, Ring } from \"./br-geo\";\nimport type { UF } from \"./locations\";\nimport { loadStateMunicipalities } from \"./state-geo\";\n\n/** A municipality with its representative coordinate (IBGE centroid). */\nexport interface MunicipalityCentroid {\n /** 7-digit IBGE code. */\n id: string;\n /** Municipality name. */\n name: string;\n /** Federative unit. */\n uf: UF;\n latitude: number;\n longitude: number;\n}\n\n/** {@link MunicipalityCentroid} plus the distance from the query point. */\nexport interface NearestMunicipality extends MunicipalityCentroid {\n /** Great-circle distance from the query coordinate, in kilometers. */\n distanceKm: number;\n}\n\n/** A municipality identified by point-in-polygon (see {@link reverseGeocode}). */\nexport interface ReverseGeocodeResult {\n id: string;\n name: string;\n uf: UF;\n}\n\ninterface RawIndex {\n states: Record<string, [number, number]>;\n municipalities: [string, string, string, number, number][];\n}\n\ninterface CentroidIndex {\n states: Partial<Record<UF, Coordinate>>;\n municipalities: MunicipalityCentroid[];\n}\n\nlet cache: CentroidIndex | null = null;\nlet pending: Promise<CentroidIndex> | null = null;\n\n/** Strip accents and lowercase, for accent-insensitive name matching. */\nfunction normalizeName(value: string): string {\n return value\n .normalize(\"NFD\")\n .replace(/\\p{Diacritic}/gu, \"\")\n .trim()\n .toLowerCase();\n}\n\n/**\n * Lazily load and cache the compact centroid index (~97 KB gzip). Kept out of\n * the synchronous barrel so importing `/br` for data/maps never pulls it — only\n * a geocode call fetches it, and only once.\n */\nasync function loadIndex(): Promise<CentroidIndex> {\n if (cache) return cache;\n if (!pending) {\n pending = import(\"./data/br-centroids.json\").then((mod) => {\n const raw = (mod.default ?? mod) as unknown as RawIndex;\n const states: Partial<Record<UF, Coordinate>> = {};\n for (const [uf, [lon, lat]] of Object.entries(raw.states)) {\n states[uf as UF] = { latitude: lat, longitude: lon };\n }\n cache = {\n states,\n municipalities: raw.municipalities.map(([id, name, uf, lon, lat]) => ({\n id,\n name,\n uf: uf as UF,\n latitude: lat,\n longitude: lon,\n })),\n };\n return cache;\n });\n }\n return pending;\n}\n\n/**\n * Nearest Brazilian municipality to a coordinate by **centroid distance** —\n * fast and geometry-free (only the small centroid index loads). Approximate:\n * near borders or inside large municipalities a neighbor's centroid can be\n * closer. For an exact containing municipality use {@link reverseGeocode}.\n *\n * @param coord - Query coordinate.\n * @returns The nearest municipality with `distanceKm`, or `null` if the index\n * is somehow empty.\n *\n * @example\n * const near = await nearestMunicipality({ latitude: -23.55, longitude: -46.63 });\n * // { id, name, uf: \"SP\", …, distanceKm }\n */\nexport async function nearestMunicipality(coord: Coordinate): Promise<NearestMunicipality | null> {\n const { municipalities } = await loadIndex();\n let best: MunicipalityCentroid | null = null;\n let bestDistance = Infinity;\n for (const m of municipalities) {\n const d = haversineKm(coord, m);\n if (d < bestDistance) {\n bestDistance = d;\n best = m;\n }\n }\n return best ? { ...best, distanceKm: bestDistance } : null;\n}\n\n/**\n * Forward-geocode a municipality by name (accent-insensitive), optionally scoped\n * to a UF. Returns every exact-name match — the same name can occur in several\n * states (e.g. \"Bonito\").\n *\n * @param name - Municipality name.\n * @param uf - Optional UF to restrict the search.\n * @returns Matching municipalities (empty array when none match).\n */\nexport async function geocodeMunicipality(name: string, uf?: UF): Promise<MunicipalityCentroid[]> {\n const { municipalities } = await loadIndex();\n const target = normalizeName(name);\n return municipalities.filter((m) => (!uf || m.uf === uf) && normalizeName(m.name) === target);\n}\n\n/**\n * Substring search over municipality names (accent-insensitive), for\n * autocomplete. Ranked prefix-first, then alphabetically.\n *\n * @param query - Partial name.\n * @param options - `uf` to scope, `limit` (default 20).\n * @returns Ranked matches.\n */\nexport async function searchMunicipalities(\n query: string,\n options: { uf?: UF; limit?: number } = {},\n): Promise<MunicipalityCentroid[]> {\n const { uf, limit = 20 } = options;\n const q = normalizeName(query);\n if (!q) return [];\n const { municipalities } = await loadIndex();\n const hits = municipalities\n .filter((m) => (!uf || m.uf === uf) && normalizeName(m.name).includes(q))\n .sort((a, b) => {\n const ap = normalizeName(a.name).startsWith(q) ? 0 : 1;\n const bp = normalizeName(b.name).startsWith(q) ? 0 : 1;\n return ap - bp || a.name.localeCompare(b.name, \"pt-BR\");\n });\n return hits.slice(0, limit);\n}\n\n/**\n * Centroid of a municipality by IBGE code.\n *\n * @param id - 7-digit IBGE code.\n * @returns The municipality centroid, or `null` if unknown.\n */\nexport async function municipalityCentroid(id: string): Promise<MunicipalityCentroid | null> {\n const { municipalities } = await loadIndex();\n return municipalities.find((m) => m.id === id) ?? null;\n}\n\n/**\n * Centroid of a federative unit.\n *\n * @param uf - Federative unit acronym.\n * @returns The state centroid, or `null` if unknown.\n */\nexport async function stateCentroid(uf: UF): Promise<Coordinate | null> {\n const { states } = await loadIndex();\n return states[uf] ?? null;\n}\n\n/** Ray-casting point-in-ring test (`ring` is `[lon, lat]` pairs). */\nfunction pointInRing(lon: number, lat: number, ring: Ring): boolean {\n let inside = false;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {\n const [xi, yi] = ring[i];\n const [xj, yj] = ring[j];\n const intersects = yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;\n if (intersects) inside = !inside;\n }\n return inside;\n}\n\n/** True when the point lies in a polygon's outer ring and not in a hole. */\nfunction pointInPolygon(lon: number, lat: number, rings: Ring[]): boolean {\n if (rings.length === 0 || !pointInRing(lon, lat, rings[0])) return false;\n for (let i = 1; i < rings.length; i += 1) {\n if (pointInRing(lon, lat, rings[i])) return false; // inside a hole\n }\n return true;\n}\n\n/** True when the point lies inside a (multi)polygon geometry. */\nfunction pointInGeometry(lon: number, lat: number, geom: BrUfGeometry): boolean {\n if (geom.type === \"Polygon\") {\n return pointInPolygon(lon, lat, geom.coordinates as Ring[]);\n }\n return (geom.coordinates as Ring[][]).some((poly) => pointInPolygon(lon, lat, poly));\n}\n\n/**\n * Exact reverse-geocode: the municipality whose (simplified) boundary\n * **contains** the coordinate, via point-in-polygon. Loads one state's geometry\n * chunk — pass `uf` when you know it to skip the centroid lookup that picks the\n * candidate state. Boundaries are simplified (~2 km), so points within ~1-2 km\n * of a border may resolve to the neighbor; offshore points return `null`.\n *\n * @param coord - Query coordinate.\n * @param options - `uf` to force the state searched.\n * @returns The containing municipality, or `null` (falls back to the nearest\n * centroid municipality when the point is inside no polygon of the state).\n *\n * @example\n * const here = await reverseGeocode({ latitude: -23.5505, longitude: -46.6333 });\n * // { id: \"3550308\", name: \"São Paulo\", uf: \"SP\" }\n */\nexport async function reverseGeocode(\n coord: Coordinate,\n options: { uf?: UF } = {},\n): Promise<ReverseGeocodeResult | null> {\n const nearest = options.uf ? null : await nearestMunicipality(coord);\n const uf = options.uf ?? nearest?.uf;\n if (!uf) return null;\n\n const state = await loadStateMunicipalities(uf);\n const hit = state?.features.find((f) =>\n pointInGeometry(coord.longitude, coord.latitude, f.geometry),\n );\n if (hit) return { id: hit.properties.id, name: hit.properties.name, uf };\n\n // Point inside no polygon (border/offshore) — fall back to nearest centroid.\n const fallback = nearest ?? (await nearestMunicipality(coord));\n return fallback ? { id: fallback.id, name: fallback.name, uf: fallback.uf } : null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"geocode.js","names":[],"sources":["../../src/br/geocode.ts"],"sourcesContent":["import { haversineKm } from \"@/geo/distance\";\nimport type { Coordinate } from \"@/geo/types\";\nimport type { BrUfGeometry, Ring } from \"./br-geo\";\nimport type { UF } from \"./locations\";\nimport { loadStateMunicipalities } from \"./state-geo\";\n\n/** A municipality with its representative coordinate (IBGE centroid). */\nexport interface MunicipalityCentroid {\n /** 7-digit IBGE code. */\n id: string;\n /** Municipality name. */\n name: string;\n /** Federative unit. */\n uf: UF;\n latitude: number;\n longitude: number;\n}\n\n/** {@link MunicipalityCentroid} plus the distance from the query point. */\nexport interface NearestMunicipality extends MunicipalityCentroid {\n /** Great-circle distance from the query coordinate, in kilometers. */\n distanceKm: number;\n}\n\n/** A municipality identified by point-in-polygon (see {@link reverseGeocode}). */\nexport interface ReverseGeocodeResult {\n id: string;\n name: string;\n uf: UF;\n}\n\ninterface RawIndex {\n states: Record<string, [number, number]>;\n municipalities: [string, string, string, number, number][];\n}\n\ninterface CentroidIndex {\n states: Partial<Record<UF, Coordinate>>;\n municipalities: MunicipalityCentroid[];\n}\n\nlet cache: CentroidIndex | null = null;\nlet pending: Promise<CentroidIndex> | null = null;\n\n/** Strip accents and lowercase, for accent-insensitive name matching. */\nfunction normalizeName(value: string): string {\n return value\n .normalize(\"NFD\")\n .replace(/\\p{Diacritic}/gu, \"\")\n .trim()\n .toLowerCase();\n}\n\n/**\n * Lazily load and cache the compact centroid index (~97 KB gzip). Kept out of\n * the synchronous barrel so importing `/br` for data/maps never pulls it — only\n * a geocode call fetches it, and only once.\n */\nasync function loadIndex(): Promise<CentroidIndex> {\n if (cache) return cache;\n if (!pending) {\n pending = import(\"./data/br-centroids.json\").then((mod) => {\n const raw = (mod.default ?? mod) as unknown as RawIndex;\n const states: Partial<Record<UF, Coordinate>> = {};\n for (const [uf, [lon, lat]] of Object.entries(raw.states)) {\n states[uf as UF] = { latitude: lat, longitude: lon };\n }\n cache = {\n states,\n municipalities: raw.municipalities.map(([id, name, uf, lon, lat]) => ({\n id,\n name,\n uf: uf as UF,\n latitude: lat,\n longitude: lon,\n })),\n };\n return cache;\n });\n }\n return pending;\n}\n\n/**\n * Nearest Brazilian municipality to a coordinate by **centroid distance** —\n * fast and geometry-free (only the small centroid index loads). Approximate:\n * near borders or inside large municipalities a neighbor's centroid can be\n * closer. For an exact containing municipality use {@link reverseGeocode}.\n *\n * @param coord - Query coordinate.\n * @returns The nearest municipality with `distanceKm`, or `null` if the index\n * is somehow empty.\n *\n * @example\n * const near = await nearestMunicipality({ latitude: -23.55, longitude: -46.63 });\n * // { id, name, uf: \"SP\", …, distanceKm }\n */\nexport async function nearestMunicipality(coord: Coordinate): Promise<NearestMunicipality | null> {\n const { municipalities } = await loadIndex();\n let best: MunicipalityCentroid | null = null;\n let bestDistance = Infinity;\n for (const m of municipalities) {\n const d = haversineKm(coord, m);\n if (d < bestDistance) {\n bestDistance = d;\n best = m;\n }\n }\n return best ? { ...best, distanceKm: bestDistance } : null;\n}\n\n/**\n * Forward-geocode a municipality by name (accent-insensitive), optionally scoped\n * to a UF. Returns every exact-name match — the same name can occur in several\n * states (e.g. \"Bonito\").\n *\n * @param name - Municipality name.\n * @param uf - Optional UF to restrict the search.\n * @returns Matching municipalities (empty array when none match).\n */\nexport async function geocodeMunicipality(name: string, uf?: UF): Promise<MunicipalityCentroid[]> {\n const { municipalities } = await loadIndex();\n const target = normalizeName(name);\n return municipalities.filter((m) => (!uf || m.uf === uf) && normalizeName(m.name) === target);\n}\n\n/**\n * Substring search over municipality names (accent-insensitive), for\n * autocomplete. Ranked prefix-first, then alphabetically.\n *\n * @param query - Partial name.\n * @param options - `uf` to scope, `limit` (default 20).\n * @returns Ranked matches.\n */\nexport async function searchMunicipalities(\n query: string,\n options: { uf?: UF; limit?: number } = {},\n): Promise<MunicipalityCentroid[]> {\n const { uf, limit = 20 } = options;\n const q = normalizeName(query);\n if (!q) return [];\n const { municipalities } = await loadIndex();\n const hits = municipalities\n .filter((m) => (!uf || m.uf === uf) && normalizeName(m.name).includes(q))\n .sort((a, b) => {\n const ap = normalizeName(a.name).startsWith(q) ? 0 : 1;\n const bp = normalizeName(b.name).startsWith(q) ? 0 : 1;\n return ap - bp || a.name.localeCompare(b.name, \"pt-BR\");\n });\n return hits.slice(0, limit);\n}\n\n/**\n * Centroid of a municipality by IBGE code.\n *\n * @param id - 7-digit IBGE code.\n * @returns The municipality centroid, or `null` if unknown.\n */\nexport async function municipalityCentroid(id: string): Promise<MunicipalityCentroid | null> {\n const { municipalities } = await loadIndex();\n return municipalities.find((m) => m.id === id) ?? null;\n}\n\n/**\n * Centroid of a federative unit.\n *\n * @param uf - Federative unit acronym.\n * @returns The state centroid, or `null` if unknown.\n */\nexport async function stateCentroid(uf: UF): Promise<Coordinate | null> {\n const { states } = await loadIndex();\n return states[uf] ?? null;\n}\n\n/** Ray-casting point-in-ring test (`ring` is `[lon, lat]` pairs). */\nfunction pointInRing(lon: number, lat: number, ring: Ring): boolean {\n let inside = false;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {\n const [xi, yi] = ring[i];\n const [xj, yj] = ring[j];\n const intersects = yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;\n if (intersects) inside = !inside;\n }\n return inside;\n}\n\n/** True when the point lies in a polygon's outer ring and not in a hole. */\nfunction pointInPolygon(lon: number, lat: number, rings: Ring[]): boolean {\n if (rings.length === 0 || !pointInRing(lon, lat, rings[0])) return false;\n for (let i = 1; i < rings.length; i += 1) {\n if (pointInRing(lon, lat, rings[i])) return false; // inside a hole\n }\n return true;\n}\n\n/** True when the point lies inside a (multi)polygon geometry. */\nfunction pointInGeometry(lon: number, lat: number, geom: BrUfGeometry): boolean {\n if (geom.type === \"Polygon\") {\n return pointInPolygon(lon, lat, geom.coordinates as Ring[]);\n }\n return (geom.coordinates as Ring[][]).some((poly) => pointInPolygon(lon, lat, poly));\n}\n\n/**\n * Exact reverse-geocode: the municipality whose (simplified) boundary\n * **contains** the coordinate, via point-in-polygon. Loads one state's geometry\n * chunk — pass `uf` when you know it to skip the centroid lookup that picks the\n * candidate state. Boundaries are simplified (~2 km), so points within ~1-2 km\n * of a border may resolve to the neighbor; offshore points return `null`.\n *\n * @param coord - Query coordinate.\n * @param options - `uf` to force the state searched.\n * @returns The containing municipality, or `null` (falls back to the nearest\n * centroid municipality when the point is inside no polygon of the state).\n *\n * @example\n * const here = await reverseGeocode({ latitude: -23.5505, longitude: -46.6333 });\n * // { id: \"3550308\", name: \"São Paulo\", uf: \"SP\" }\n */\nexport async function reverseGeocode(\n coord: Coordinate,\n options: { uf?: UF } = {},\n): Promise<ReverseGeocodeResult | null> {\n const nearest = options.uf ? null : await nearestMunicipality(coord);\n const uf = options.uf ?? nearest?.uf;\n if (!uf) return null;\n\n const state = await loadStateMunicipalities(uf);\n const hit = state?.features.find((f) =>\n pointInGeometry(coord.longitude, coord.latitude, f.geometry),\n );\n if (hit) return { id: hit.properties.id, name: hit.properties.name, uf };\n\n // Point inside no polygon (border/offshore) — fall back to nearest centroid.\n const fallback = nearest ?? (await nearestMunicipality(coord));\n return fallback ? { id: fallback.id, name: fallback.name, uf: fallback.uf } : null;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../hooks/use-stable-callback.cjs"),t=require("../vision/use-camera-stream.cjs"),n=require("./barcode.cjs"),r=require("./use-torch.cjs");let i=require("react");function a(e){return e.readyState>=2&&e.videoWidth>0&&e.videoHeight>0}function o(o={}){let{formats:s=n.DEFAULT_BARCODE_FORMATS,onScan:c,intervalMs:l=200,repeatDelayMs:u=2500,paused:d=!1,detector:f,constraints:p,onError:m}=o,[h,g]=(0,i.useState)(()=>f!==void 0||n.isBarcodeDetectionSupported()),_=t.useCameraStream({constraints:p,enabled:h}),v=r.useTorch(_.stream),[y,b]=(0,i.useState)(f===void 0?[]:s),[x,S]=(0,i.useState)(!1),[C,w]=(0,i.useState)(null),T=(0,i.useRef)(null),E=(0,i.useRef)(null),D=(0,i.useRef)(0),O=e.useStableCallback(e=>c?.(e)),k=e.useStableCallback(e=>m?.(e)),A=s.join(`,`);return(0,i.useEffect)(()=>{if(f){T.current=f,g(!0),b(s);return}if(!n.isBarcodeDetectionSupported()){T.current=null,g(!1),b([]);return}let e=!1;return n.getSupportedBarcodeFormats().then(t=>{if(e)return;let r=t.length===0?s:s.filter(e=>t.includes(e)),i=r.length>0?n.createBarcodeDetector(r):null;T.current=i,b(i?r:[]),g(i!==null)}),()=>{e=!0}},[f,A]),(0,i.useEffect)(()=>{if(!h||d||_.status!==`ready`){S(!1);return}let e=!1,t;S(!0);let r=e=>{let t=Date.now();e.rawValue===E.current&&t-D.current<u||(E.current=e.rawValue,D.current=t,w(e),O(e))},i=async()=>{let t=_.videoRef.current,i=T.current;if(
|
|
1
|
+
const e=require("../hooks/use-stable-callback.cjs"),t=require("../vision/use-camera-stream.cjs"),n=require("./barcode.cjs"),r=require("./use-torch.cjs");let i=require("react");function a(e){return e.readyState>=2&&e.videoWidth>0&&e.videoHeight>0}function o(o={}){let{formats:s=n.DEFAULT_BARCODE_FORMATS,onScan:c,intervalMs:l=200,repeatDelayMs:u=2500,paused:d=!1,detector:f,constraints:p,onError:m}=o,[h,g]=(0,i.useState)(()=>f!==void 0||n.isBarcodeDetectionSupported()),_=t.useCameraStream({constraints:p,enabled:h}),v=r.useTorch(_.stream),[y,b]=(0,i.useState)(f===void 0?[]:s),[x,S]=(0,i.useState)(!1),[C,w]=(0,i.useState)(null),T=(0,i.useRef)(null),E=(0,i.useRef)(null),D=(0,i.useRef)(0),O=e.useStableCallback(e=>c?.(e)),k=e.useStableCallback(e=>m?.(e)),A=s.join(`,`);return(0,i.useEffect)(()=>{if(f){T.current=f,g(!0),b(s);return}if(!n.isBarcodeDetectionSupported()){T.current=null,g(!1),b([]);return}let e=!1;return n.getSupportedBarcodeFormats().then(t=>{if(e)return;let r=t.length===0?s:s.filter(e=>t.includes(e)),i=r.length>0?n.createBarcodeDetector(r):null;T.current=i,b(i?r:[]),g(i!==null)}),()=>{e=!0}},[f,A]),(0,i.useEffect)(()=>{if(!h||d||_.status!==`ready`){S(!1);return}let e=!1,t;S(!0);let r=e=>{let t=Date.now();e.rawValue===E.current&&t-D.current<u||(E.current=e.rawValue,D.current=t,w(e),O(e))},i=async()=>{let t=_.videoRef.current,i=T.current;if(t&&i&&a(t))try{let a=await i.detect(t);if(e)return;for(let e of a){let t=n.normalizeBarcode(e);t.rawValue!==``&&r(t)}}catch(t){e||k(t)}},o=()=>{i().finally(()=>{e||(t=setTimeout(o,l))})};return o(),()=>{e=!0,t!==void 0&&clearTimeout(t),S(!1)}},[h,d,_.status,_.videoRef,l,u,O,k]),{videoRef:_.videoRef,status:_.status,error:_.error,supported:h,formats:y,scanning:x,result:C,torch:v,retry:_.retry}}exports.useBarcodeScanner=o;
|
|
2
2
|
//# sourceMappingURL=use-barcode-scanner.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-barcode-scanner.cjs","names":[],"sources":["../../src/capture/use-barcode-scanner.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the scan loop, the repeat suppression window and the\n * torch control share the same track and the same interval handle; splitting them\n * would leave a timer running against a track another hook stopped.\n */\nimport { useEffect, useRef, useState, type RefObject } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\nimport {\n useCameraStream,\n type CameraStreamError,\n type CameraStreamStatus,\n} from \"@/vision/use-camera-stream\";\n\nimport {\n createBarcodeDetector,\n getSupportedBarcodeFormats,\n isBarcodeDetectionSupported,\n normalizeBarcode,\n DEFAULT_BARCODE_FORMATS,\n type BarcodeDetectorLike,\n type BarcodeFormat,\n type BarcodeScanResult,\n} from \"./barcode\";\nimport { useTorch, type UseTorchResult } from \"./use-torch\";\n\n/** Options for {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerOptions {\n /** Symbologies to look for. Defaults to {@link DEFAULT_BARCODE_FORMATS}. */\n formats?: readonly BarcodeFormat[];\n /** Called for every accepted read — that is, after repeat suppression. */\n onScan?: (result: BarcodeScanResult) => void;\n /**\n * How often a frame is examined, in ms. Default 200.\n *\n * Not `requestAnimationFrame`: decoding is 10–40 ms of main-thread work on a\n * phone, so running it per frame competes with the preview it is reading from and\n * makes the video stutter. Five looks per second is faster than a human can aim.\n */\n intervalMs?: number;\n /**\n * Ignore the **same** value again for this long, in ms. Default 2500.\n *\n * A symbol stays in frame for as long as the user holds the camera there, so a\n * scanner without this fires the same code five times a second — which, wired to\n * \"add item to cart\", is a bug the user pays for. A *different* value is never\n * suppressed.\n */\n repeatDelayMs?: number;\n /** Stop looking without releasing the camera — a confirmation sheet is open. */\n paused?: boolean;\n /**\n * A decoder to use instead of the native one.\n *\n * The way to support Safari and Firefox: hand in a polyfill and everything else\n * here works unchanged. See {@link isBarcodeDetectionSupported} for why the SDK\n * does not bundle one.\n */\n detector?: BarcodeDetectorLike;\n /** Camera constraints, forwarded to `useCameraStream`. Defaults to the rear camera. */\n constraints?: MediaStreamConstraints;\n /**\n * A frame the engine refused to decode.\n *\n * Not \"nothing found\" — that resolves to an empty list and is the normal case.\n * This is the engine itself failing, which the loop survives because it is\n * usually transient (a frame arriving between two resolutions).\n */\n onError?: (error: unknown) => void;\n}\n\n/** Value returned by {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerResult {\n /** Attach to a `<video ref={…} muted playsInline />`. */\n videoRef: RefObject<HTMLVideoElement | null>;\n /** Camera lifecycle. `\"ready\"` means the preview is running. */\n status: CameraStreamStatus;\n /** Classified camera error, or `null`. */\n error: CameraStreamError | null;\n /** `false` when there is no decoder — no native API and none injected. */\n supported: boolean;\n /**\n * Formats actually in use: the requested ones intersected with what the engine\n * reports. Empty while the probe is in flight, or when nothing matched.\n */\n formats: readonly BarcodeFormat[];\n /** Whether the detect loop is running right now. */\n scanning: boolean;\n /** The most recent accepted read, or `null`. */\n result: BarcodeScanResult | null;\n /** The LED torch of this camera, when it has one. */\n torch: UseTorchResult;\n /** Re-open the camera after an error (the user changed the permission). */\n retry: () => void;\n}\n\n/** A video is decodable once it has data for the current frame and a real size. */\nfunction frameIsReady(video: HTMLVideoElement): boolean {\n return video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0;\n}\n\n/**\n * Read barcodes and QR codes from the camera.\n *\n * The camera and its classified errors come from `useCameraStream`, so this hook is\n * only the decoding half: it drives a `BarcodeDetector` over the preview on an\n * interval, suppresses the same value repeating, and exposes the torch.\n *\n * **Mounting this opens the camera.** It inherits that from `useCameraStream`, which\n * acquires on mount — so mount it *after* the user asks to scan (a button that reveals\n * the scanner), never on a page that merely contains one. A permission prompt nobody\n * provoked is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever prompting again.\n *\n * `supported` deserves a branch in the UI, not an assertion: `BarcodeDetector` is\n * Chromium-only and missing on Windows/Linux desktop, Firefox and everything on iOS.\n * Inject a `detector` to cover those, or tell the user to type the code.\n *\n * @param options - See {@link UseBarcodeScannerOptions}.\n * @returns The camera plumbing plus the scan state.\n *\n * @example\n * const scanner = useBarcodeScanner({\n * formats: [\"ean_13\"],\n * onScan: ({ rawValue }) => addToCart(rawValue),\n * });\n * return <video ref={scanner.videoRef} muted playsInline />;\n */\nexport function useBarcodeScanner(options: UseBarcodeScannerOptions = {}): UseBarcodeScannerResult {\n const {\n formats: requested = DEFAULT_BARCODE_FORMATS,\n onScan,\n intervalMs = 200,\n repeatDelayMs = 2500,\n paused = false,\n detector: injected,\n constraints,\n onError,\n } = options;\n\n const [supported, setSupported] = useState(\n () => injected !== undefined || isBarcodeDetectionSupported(),\n );\n\n /**\n * No decoder, no camera.\n *\n * Opening the camera only to report \"this browser cannot decode barcodes\" spends a\n * permission prompt on nothing — and a refusal is permanent, so it also spends the\n * *next* feature that needs the camera.\n */\n const camera = useCameraStream({ constraints, enabled: supported });\n const torch = useTorch(camera.stream);\n\n const [formats, setFormats] = useState<readonly BarcodeFormat[]>(\n injected !== undefined ? requested : [],\n );\n const [scanning, setScanning] = useState(false);\n const [result, setResult] = useState<BarcodeScanResult | null>(null);\n\n const detectorRef = useRef<BarcodeDetectorLike | null>(null);\n const lastValue = useRef<string | null>(null);\n const lastAt = useRef(0);\n\n const emitScan = useStableCallback((scan: BarcodeScanResult) => onScan?.(scan));\n const emitError = useStableCallback((error: unknown) => onError?.(error));\n\n const requestedKey = requested.join(\",\");\n\n /**\n * Resolve which formats the engine will take, then build the detector.\n *\n * The intersection is not defensive coding: `new BarcodeDetector({ formats })`\n * throws `NotSupportedError` when any entry is unknown to the platform decoder, and\n * that list differs between two Chromium builds on two operating systems. Asking\n * for the intersection is the only way one call site works everywhere.\n */\n useEffect(() => {\n if (injected) {\n detectorRef.current = injected;\n setSupported(true);\n setFormats(requested);\n return;\n }\n if (!isBarcodeDetectionSupported()) {\n detectorRef.current = null;\n setSupported(false);\n setFormats([]);\n return;\n }\n let cancelled = false;\n void getSupportedBarcodeFormats().then((available) => {\n if (cancelled) return;\n const usable =\n available.length === 0\n ? requested\n : requested.filter((format) => available.includes(format));\n const detector = usable.length > 0 ? createBarcodeDetector(usable) : null;\n detectorRef.current = detector;\n setFormats(detector ? usable : []);\n setSupported(detector !== null);\n });\n return () => {\n cancelled = true;\n };\n // `requestedKey` stands in for the array identity, so a caller passing an\n // inline `formats={[\"ean_13\"]}` does not rebuild the detector every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [injected, requestedKey]);\n\n /**\n * Look at a frame every `intervalMs`, and never overlap two looks.\n *\n * The loop re-arms itself *after* each `detect()` settles rather than running on a\n * fixed `setInterval`: decoding sometimes takes longer than the interval, and an\n * interval would then queue calls faster than the engine drains them until the tab\n * is unusable.\n */\n useEffect(() => {\n if (!supported || paused || camera.status !== \"ready\") {\n setScanning(false);\n return;\n }\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n setScanning(true);\n\n const accept = (scan: BarcodeScanResult): void => {\n const now = Date.now();\n const isRepeat =\n scan.rawValue === lastValue.current && now - lastAt.current < repeatDelayMs;\n if (isRepeat) return;\n lastValue.current = scan.rawValue;\n lastAt.current = now;\n setResult(scan);\n emitScan(scan);\n };\n\n const look = async (): Promise<void> => {\n const video = camera.videoRef.current;\n const detector = detectorRef.current;\n if (!video || !detector || !frameIsReady(video)) return;\n try {\n const found = await detector.detect(video);\n if (stopped) return;\n for (const raw of found) {\n const scan = normalizeBarcode(raw);\n if (scan.rawValue !== \"\") accept(scan);\n }\n } catch (error) {\n if (!stopped) emitError(error);\n }\n };\n\n const tick = (): void => {\n void look().finally(() => {\n if (!stopped) timer = setTimeout(tick, intervalMs);\n });\n };\n tick();\n\n return () => {\n stopped = true;\n if (timer !== undefined) clearTimeout(timer);\n setScanning(false);\n };\n }, [\n supported,\n paused,\n camera.status,\n camera.videoRef,\n intervalMs,\n repeatDelayMs,\n emitScan,\n emitError,\n ]);\n\n return {\n videoRef: camera.videoRef,\n status: camera.status,\n error: camera.error,\n supported,\n formats,\n scanning,\n result,\n torch,\n retry: camera.retry,\n };\n}\n"],"mappings":"gLAiGA,SAAS,EAAa,EAAkC,CACpD,OAAO,EAAM,YAAc,GAAK,EAAM,WAAa,GAAK,EAAM,YAAc,CAChF,CA6BA,SAAgB,EAAkB,EAAoC,CAAC,EAA4B,CAC/F,GAAM,CACF,QAAS,EAAY,EAAA,wBACrB,SACA,aAAa,IACb,gBAAgB,KAChB,SAAS,GACT,SAAU,EACV,cACA,WACA,EAEE,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,KACxB,IAAa,IAAA,IAAa,EAAA,4BAA4B,CAChE,EASM,EAAS,EAAA,gBAAgB,CAAE,cAAa,QAAS,CAAU,CAAC,EAC5D,EAAQ,EAAA,SAAS,EAAO,MAAM,EAE9B,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAC1B,IAAa,IAAA,GAAwB,CAAC,EAAb,CAC7B,EACM,CAAC,EAAU,IAAA,EAAe,EAAA,SAAA,CAAS,EAAK,EACxC,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAAmC,IAAI,EAE7D,GAAA,EAAc,EAAA,OAAA,CAAmC,IAAI,EACrD,GAAA,EAAY,EAAA,OAAA,CAAsB,IAAI,EACtC,GAAA,EAAS,EAAA,OAAA,CAAO,CAAC,EAEjB,EAAW,EAAA,kBAAmB,GAA4B,IAAS,CAAI,CAAC,EACxE,EAAY,EAAA,kBAAmB,GAAmB,IAAU,CAAK,CAAC,EAElE,EAAe,EAAU,KAAK,GAAG,EA8GvC,OApGA,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,EAAU,CACV,EAAY,QAAU,EACtB,EAAa,EAAI,EACjB,EAAW,CAAS,EACpB,MACJ,CACA,GAAI,CAAC,EAAA,4BAA4B,EAAG,CAChC,EAAY,QAAU,KACtB,EAAa,EAAK,EAClB,EAAW,CAAC,CAAC,EACb,MACJ,CACA,IAAI,EAAY,GAYhB,OAXA,EAAK,2BAA2B,CAAC,CAAC,KAAM,GAAc,CAClD,GAAI,EAAW,OACf,IAAM,EACF,EAAU,SAAW,EACf,EACA,EAAU,OAAQ,GAAW,EAAU,SAAS,CAAM,CAAC,EAC3D,EAAW,EAAO,OAAS,EAAI,EAAA,sBAAsB,CAAM,EAAI,KACrE,EAAY,QAAU,EACtB,EAAW,EAAW,EAAS,CAAC,CAAC,EACjC,EAAa,IAAa,IAAI,CAClC,CAAC,MACY,CACT,EAAY,EAChB,CAIJ,EAAG,CAAC,EAAU,CAAY,CAAC,GAU3B,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAa,GAAU,EAAO,SAAW,QAAS,CACnD,EAAY,EAAK,EACjB,MACJ,CACA,IAAI,EAAU,GACV,EACJ,EAAY,EAAI,EAEhB,IAAM,EAAU,GAAkC,CAC9C,IAAM,EAAM,KAAK,IAAI,EAEjB,EAAK,WAAa,EAAU,SAAW,EAAM,EAAO,QAAU,IAElE,EAAU,QAAU,EAAK,SACzB,EAAO,QAAU,EACjB,EAAU,CAAI,EACd,EAAS,CAAI,EACjB,EAEM,EAAO,SAA2B,CACpC,IAAM,EAAQ,EAAO,SAAS,QACxB,EAAW,EAAY,QACzB,MAAC,GAAS,CAAC,GAAY,CAAC,EAAa,CAAK,GAC9C,GAAI,CACA,IAAM,EAAQ,MAAM,EAAS,OAAO,CAAK,EACzC,GAAI,EAAS,OACb,IAAK,IAAM,KAAO,EAAO,CACrB,IAAM,EAAO,EAAA,iBAAiB,CAAG,EAC7B,EAAK,WAAa,IAAI,EAAO,CAAI,CACzC,CACJ,OAAS,EAAO,CACP,GAAS,EAAU,CAAK,CACjC,CACJ,EAEM,MAAmB,CACrB,EAAU,CAAC,CAAC,YAAc,CACjB,IAAS,EAAQ,WAAW,EAAM,CAAU,EACrD,CAAC,CACL,EAGA,OAFA,EAAK,MAEQ,CACT,EAAU,GACN,IAAU,IAAA,IAAW,aAAa,CAAK,EAC3C,EAAY,EAAK,CACrB,CACJ,EAAG,CACC,EACA,EACA,EAAO,OACP,EAAO,SACP,EACA,EACA,EACA,CACJ,CAAC,EAEM,CACH,SAAU,EAAO,SACjB,OAAQ,EAAO,OACf,MAAO,EAAO,MACd,YACA,UACA,WACA,SACA,QACA,MAAO,EAAO,KAClB,CACJ"}
|
|
1
|
+
{"version":3,"file":"use-barcode-scanner.cjs","names":[],"sources":["../../src/capture/use-barcode-scanner.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the scan loop, the repeat suppression window and the\n * torch control share the same track and the same interval handle; splitting them\n * would leave a timer running against a track another hook stopped.\n */\nimport { useEffect, useRef, useState, type RefObject } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\nimport {\n useCameraStream,\n type CameraStreamError,\n type CameraStreamStatus,\n} from \"@/vision/use-camera-stream\";\n\nimport {\n createBarcodeDetector,\n getSupportedBarcodeFormats,\n isBarcodeDetectionSupported,\n normalizeBarcode,\n DEFAULT_BARCODE_FORMATS,\n type BarcodeDetectorLike,\n type BarcodeFormat,\n type BarcodeScanResult,\n} from \"./barcode\";\nimport { useTorch, type UseTorchResult } from \"./use-torch\";\n\n/** Options for {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerOptions {\n /** Symbologies to look for. Defaults to {@link DEFAULT_BARCODE_FORMATS}. */\n formats?: readonly BarcodeFormat[];\n /** Called for every accepted read — that is, after repeat suppression. */\n onScan?: (result: BarcodeScanResult) => void;\n /**\n * How often a frame is examined, in ms. Default 200.\n *\n * Not `requestAnimationFrame`: decoding is 10–40 ms of main-thread work on a\n * phone, so running it per frame competes with the preview it is reading from and\n * makes the video stutter. Five looks per second is faster than a human can aim.\n */\n intervalMs?: number;\n /**\n * Ignore the **same** value again for this long, in ms. Default 2500.\n *\n * A symbol stays in frame for as long as the user holds the camera there, so a\n * scanner without this fires the same code five times a second — which, wired to\n * \"add item to cart\", is a bug the user pays for. A *different* value is never\n * suppressed.\n */\n repeatDelayMs?: number;\n /** Stop looking without releasing the camera — a confirmation sheet is open. */\n paused?: boolean;\n /**\n * A decoder to use instead of the native one.\n *\n * The way to support Safari and Firefox: hand in a polyfill and everything else\n * here works unchanged. See {@link isBarcodeDetectionSupported} for why the SDK\n * does not bundle one.\n */\n detector?: BarcodeDetectorLike;\n /** Camera constraints, forwarded to `useCameraStream`. Defaults to the rear camera. */\n constraints?: MediaStreamConstraints;\n /**\n * A frame the engine refused to decode.\n *\n * Not \"nothing found\" — that resolves to an empty list and is the normal case.\n * This is the engine itself failing, which the loop survives because it is\n * usually transient (a frame arriving between two resolutions).\n */\n onError?: (error: unknown) => void;\n}\n\n/** Value returned by {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerResult {\n /** Attach to a `<video ref={…} muted playsInline />`. */\n videoRef: RefObject<HTMLVideoElement | null>;\n /** Camera lifecycle. `\"ready\"` means the preview is running. */\n status: CameraStreamStatus;\n /** Classified camera error, or `null`. */\n error: CameraStreamError | null;\n /** `false` when there is no decoder — no native API and none injected. */\n supported: boolean;\n /**\n * Formats actually in use: the requested ones intersected with what the engine\n * reports. Empty while the probe is in flight, or when nothing matched.\n */\n formats: readonly BarcodeFormat[];\n /** Whether the detect loop is running right now. */\n scanning: boolean;\n /** The most recent accepted read, or `null`. */\n result: BarcodeScanResult | null;\n /** The LED torch of this camera, when it has one. */\n torch: UseTorchResult;\n /** Re-open the camera after an error (the user changed the permission). */\n retry: () => void;\n}\n\n/** A video is decodable once it has data for the current frame and a real size. */\nfunction frameIsReady(video: HTMLVideoElement): boolean {\n return video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0;\n}\n\n/**\n * Read barcodes and QR codes from the camera.\n *\n * The camera and its classified errors come from `useCameraStream`, so this hook is\n * only the decoding half: it drives a `BarcodeDetector` over the preview on an\n * interval, suppresses the same value repeating, and exposes the torch.\n *\n * **Mounting this opens the camera.** It inherits that from `useCameraStream`, which\n * acquires on mount — so mount it *after* the user asks to scan (a button that reveals\n * the scanner), never on a page that merely contains one. A permission prompt nobody\n * provoked is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever prompting again.\n *\n * `supported` deserves a branch in the UI, not an assertion: `BarcodeDetector` is\n * Chromium-only and missing on Windows/Linux desktop, Firefox and everything on iOS.\n * Inject a `detector` to cover those, or tell the user to type the code.\n *\n * @param options - See {@link UseBarcodeScannerOptions}.\n * @returns The camera plumbing plus the scan state.\n *\n * @example\n * const scanner = useBarcodeScanner({\n * formats: [\"ean_13\"],\n * onScan: ({ rawValue }) => addToCart(rawValue),\n * });\n * return <video ref={scanner.videoRef} muted playsInline />;\n */\nexport function useBarcodeScanner(options: UseBarcodeScannerOptions = {}): UseBarcodeScannerResult {\n const {\n formats: requested = DEFAULT_BARCODE_FORMATS,\n onScan,\n intervalMs = 200,\n repeatDelayMs = 2500,\n paused = false,\n detector: injected,\n constraints,\n onError,\n } = options;\n\n const [supported, setSupported] = useState(\n () => injected !== undefined || isBarcodeDetectionSupported(),\n );\n\n /**\n * No decoder, no camera.\n *\n * Opening the camera only to report \"this browser cannot decode barcodes\" spends a\n * permission prompt on nothing — and a refusal is permanent, so it also spends the\n * *next* feature that needs the camera.\n */\n const camera = useCameraStream({ constraints, enabled: supported });\n const torch = useTorch(camera.stream);\n\n const [formats, setFormats] = useState<readonly BarcodeFormat[]>(\n injected !== undefined ? requested : [],\n );\n const [scanning, setScanning] = useState(false);\n const [result, setResult] = useState<BarcodeScanResult | null>(null);\n\n const detectorRef = useRef<BarcodeDetectorLike | null>(null);\n const lastValue = useRef<string | null>(null);\n const lastAt = useRef(0);\n\n const emitScan = useStableCallback((scan: BarcodeScanResult) => onScan?.(scan));\n const emitError = useStableCallback((error: unknown) => onError?.(error));\n\n const requestedKey = requested.join(\",\");\n\n /**\n * Resolve which formats the engine will take, then build the detector.\n *\n * The intersection is not defensive coding: `new BarcodeDetector({ formats })`\n * throws `NotSupportedError` when any entry is unknown to the platform decoder, and\n * that list differs between two Chromium builds on two operating systems. Asking\n * for the intersection is the only way one call site works everywhere.\n */\n useEffect(() => {\n if (injected) {\n detectorRef.current = injected;\n setSupported(true);\n setFormats(requested);\n return;\n }\n if (!isBarcodeDetectionSupported()) {\n detectorRef.current = null;\n setSupported(false);\n setFormats([]);\n return;\n }\n let cancelled = false;\n void getSupportedBarcodeFormats().then((available) => {\n if (cancelled) return;\n const usable =\n available.length === 0\n ? requested\n : requested.filter((format) => available.includes(format));\n const detector = usable.length > 0 ? createBarcodeDetector(usable) : null;\n detectorRef.current = detector;\n setFormats(detector ? usable : []);\n setSupported(detector !== null);\n });\n return () => {\n cancelled = true;\n };\n // `requestedKey` stands in for the array identity, so a caller passing an\n // inline `formats={[\"ean_13\"]}` does not rebuild the detector every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [injected, requestedKey]);\n\n /**\n * Look at a frame every `intervalMs`, and never overlap two looks.\n *\n * The loop re-arms itself *after* each `detect()` settles rather than running on a\n * fixed `setInterval`: decoding sometimes takes longer than the interval, and an\n * interval would then queue calls faster than the engine drains them until the tab\n * is unusable.\n */\n useEffect(() => {\n if (!supported || paused || camera.status !== \"ready\") {\n setScanning(false);\n return;\n }\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n setScanning(true);\n\n const accept = (scan: BarcodeScanResult): void => {\n const now = Date.now();\n const isRepeat =\n scan.rawValue === lastValue.current && now - lastAt.current < repeatDelayMs;\n if (isRepeat) return;\n lastValue.current = scan.rawValue;\n lastAt.current = now;\n setResult(scan);\n emitScan(scan);\n };\n\n const look = async (): Promise<void> => {\n const video = camera.videoRef.current;\n const detector = detectorRef.current;\n if (!video || !detector || !frameIsReady(video)) return;\n try {\n const found = await detector.detect(video);\n if (stopped) return;\n for (const raw of found) {\n const scan = normalizeBarcode(raw);\n if (scan.rawValue !== \"\") accept(scan);\n }\n } catch (error) {\n if (!stopped) emitError(error);\n }\n };\n\n const tick = (): void => {\n void look().finally(() => {\n if (!stopped) timer = setTimeout(tick, intervalMs);\n });\n };\n tick();\n\n return () => {\n stopped = true;\n if (timer !== undefined) clearTimeout(timer);\n setScanning(false);\n };\n }, [\n supported,\n paused,\n camera.status,\n camera.videoRef,\n intervalMs,\n repeatDelayMs,\n emitScan,\n emitError,\n ]);\n\n return {\n videoRef: camera.videoRef,\n status: camera.status,\n error: camera.error,\n supported,\n formats,\n scanning,\n result,\n torch,\n retry: camera.retry,\n };\n}\n"],"mappings":"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"}
|
|
@@ -41,7 +41,7 @@ function f(f = {}) {
|
|
|
41
41
|
e.rawValue === M.current && t - N.current < g || (M.current = e.rawValue, N.current = t, A(e), P(e));
|
|
42
42
|
}, r = async () => {
|
|
43
43
|
let t = C.videoRef.current, r = j.current;
|
|
44
|
-
if (
|
|
44
|
+
if (t && r && d(t)) try {
|
|
45
45
|
let i = await r.detect(t);
|
|
46
46
|
if (e) return;
|
|
47
47
|
for (let e of i) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-barcode-scanner.js","names":[],"sources":["../../src/capture/use-barcode-scanner.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the scan loop, the repeat suppression window and the\n * torch control share the same track and the same interval handle; splitting them\n * would leave a timer running against a track another hook stopped.\n */\nimport { useEffect, useRef, useState, type RefObject } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\nimport {\n useCameraStream,\n type CameraStreamError,\n type CameraStreamStatus,\n} from \"@/vision/use-camera-stream\";\n\nimport {\n createBarcodeDetector,\n getSupportedBarcodeFormats,\n isBarcodeDetectionSupported,\n normalizeBarcode,\n DEFAULT_BARCODE_FORMATS,\n type BarcodeDetectorLike,\n type BarcodeFormat,\n type BarcodeScanResult,\n} from \"./barcode\";\nimport { useTorch, type UseTorchResult } from \"./use-torch\";\n\n/** Options for {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerOptions {\n /** Symbologies to look for. Defaults to {@link DEFAULT_BARCODE_FORMATS}. */\n formats?: readonly BarcodeFormat[];\n /** Called for every accepted read — that is, after repeat suppression. */\n onScan?: (result: BarcodeScanResult) => void;\n /**\n * How often a frame is examined, in ms. Default 200.\n *\n * Not `requestAnimationFrame`: decoding is 10–40 ms of main-thread work on a\n * phone, so running it per frame competes with the preview it is reading from and\n * makes the video stutter. Five looks per second is faster than a human can aim.\n */\n intervalMs?: number;\n /**\n * Ignore the **same** value again for this long, in ms. Default 2500.\n *\n * A symbol stays in frame for as long as the user holds the camera there, so a\n * scanner without this fires the same code five times a second — which, wired to\n * \"add item to cart\", is a bug the user pays for. A *different* value is never\n * suppressed.\n */\n repeatDelayMs?: number;\n /** Stop looking without releasing the camera — a confirmation sheet is open. */\n paused?: boolean;\n /**\n * A decoder to use instead of the native one.\n *\n * The way to support Safari and Firefox: hand in a polyfill and everything else\n * here works unchanged. See {@link isBarcodeDetectionSupported} for why the SDK\n * does not bundle one.\n */\n detector?: BarcodeDetectorLike;\n /** Camera constraints, forwarded to `useCameraStream`. Defaults to the rear camera. */\n constraints?: MediaStreamConstraints;\n /**\n * A frame the engine refused to decode.\n *\n * Not \"nothing found\" — that resolves to an empty list and is the normal case.\n * This is the engine itself failing, which the loop survives because it is\n * usually transient (a frame arriving between two resolutions).\n */\n onError?: (error: unknown) => void;\n}\n\n/** Value returned by {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerResult {\n /** Attach to a `<video ref={…} muted playsInline />`. */\n videoRef: RefObject<HTMLVideoElement | null>;\n /** Camera lifecycle. `\"ready\"` means the preview is running. */\n status: CameraStreamStatus;\n /** Classified camera error, or `null`. */\n error: CameraStreamError | null;\n /** `false` when there is no decoder — no native API and none injected. */\n supported: boolean;\n /**\n * Formats actually in use: the requested ones intersected with what the engine\n * reports. Empty while the probe is in flight, or when nothing matched.\n */\n formats: readonly BarcodeFormat[];\n /** Whether the detect loop is running right now. */\n scanning: boolean;\n /** The most recent accepted read, or `null`. */\n result: BarcodeScanResult | null;\n /** The LED torch of this camera, when it has one. */\n torch: UseTorchResult;\n /** Re-open the camera after an error (the user changed the permission). */\n retry: () => void;\n}\n\n/** A video is decodable once it has data for the current frame and a real size. */\nfunction frameIsReady(video: HTMLVideoElement): boolean {\n return video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0;\n}\n\n/**\n * Read barcodes and QR codes from the camera.\n *\n * The camera and its classified errors come from `useCameraStream`, so this hook is\n * only the decoding half: it drives a `BarcodeDetector` over the preview on an\n * interval, suppresses the same value repeating, and exposes the torch.\n *\n * **Mounting this opens the camera.** It inherits that from `useCameraStream`, which\n * acquires on mount — so mount it *after* the user asks to scan (a button that reveals\n * the scanner), never on a page that merely contains one. A permission prompt nobody\n * provoked is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever prompting again.\n *\n * `supported` deserves a branch in the UI, not an assertion: `BarcodeDetector` is\n * Chromium-only and missing on Windows/Linux desktop, Firefox and everything on iOS.\n * Inject a `detector` to cover those, or tell the user to type the code.\n *\n * @param options - See {@link UseBarcodeScannerOptions}.\n * @returns The camera plumbing plus the scan state.\n *\n * @example\n * const scanner = useBarcodeScanner({\n * formats: [\"ean_13\"],\n * onScan: ({ rawValue }) => addToCart(rawValue),\n * });\n * return <video ref={scanner.videoRef} muted playsInline />;\n */\nexport function useBarcodeScanner(options: UseBarcodeScannerOptions = {}): UseBarcodeScannerResult {\n const {\n formats: requested = DEFAULT_BARCODE_FORMATS,\n onScan,\n intervalMs = 200,\n repeatDelayMs = 2500,\n paused = false,\n detector: injected,\n constraints,\n onError,\n } = options;\n\n const [supported, setSupported] = useState(\n () => injected !== undefined || isBarcodeDetectionSupported(),\n );\n\n /**\n * No decoder, no camera.\n *\n * Opening the camera only to report \"this browser cannot decode barcodes\" spends a\n * permission prompt on nothing — and a refusal is permanent, so it also spends the\n * *next* feature that needs the camera.\n */\n const camera = useCameraStream({ constraints, enabled: supported });\n const torch = useTorch(camera.stream);\n\n const [formats, setFormats] = useState<readonly BarcodeFormat[]>(\n injected !== undefined ? requested : [],\n );\n const [scanning, setScanning] = useState(false);\n const [result, setResult] = useState<BarcodeScanResult | null>(null);\n\n const detectorRef = useRef<BarcodeDetectorLike | null>(null);\n const lastValue = useRef<string | null>(null);\n const lastAt = useRef(0);\n\n const emitScan = useStableCallback((scan: BarcodeScanResult) => onScan?.(scan));\n const emitError = useStableCallback((error: unknown) => onError?.(error));\n\n const requestedKey = requested.join(\",\");\n\n /**\n * Resolve which formats the engine will take, then build the detector.\n *\n * The intersection is not defensive coding: `new BarcodeDetector({ formats })`\n * throws `NotSupportedError` when any entry is unknown to the platform decoder, and\n * that list differs between two Chromium builds on two operating systems. Asking\n * for the intersection is the only way one call site works everywhere.\n */\n useEffect(() => {\n if (injected) {\n detectorRef.current = injected;\n setSupported(true);\n setFormats(requested);\n return;\n }\n if (!isBarcodeDetectionSupported()) {\n detectorRef.current = null;\n setSupported(false);\n setFormats([]);\n return;\n }\n let cancelled = false;\n void getSupportedBarcodeFormats().then((available) => {\n if (cancelled) return;\n const usable =\n available.length === 0\n ? requested\n : requested.filter((format) => available.includes(format));\n const detector = usable.length > 0 ? createBarcodeDetector(usable) : null;\n detectorRef.current = detector;\n setFormats(detector ? usable : []);\n setSupported(detector !== null);\n });\n return () => {\n cancelled = true;\n };\n // `requestedKey` stands in for the array identity, so a caller passing an\n // inline `formats={[\"ean_13\"]}` does not rebuild the detector every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [injected, requestedKey]);\n\n /**\n * Look at a frame every `intervalMs`, and never overlap two looks.\n *\n * The loop re-arms itself *after* each `detect()` settles rather than running on a\n * fixed `setInterval`: decoding sometimes takes longer than the interval, and an\n * interval would then queue calls faster than the engine drains them until the tab\n * is unusable.\n */\n useEffect(() => {\n if (!supported || paused || camera.status !== \"ready\") {\n setScanning(false);\n return;\n }\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n setScanning(true);\n\n const accept = (scan: BarcodeScanResult): void => {\n const now = Date.now();\n const isRepeat =\n scan.rawValue === lastValue.current && now - lastAt.current < repeatDelayMs;\n if (isRepeat) return;\n lastValue.current = scan.rawValue;\n lastAt.current = now;\n setResult(scan);\n emitScan(scan);\n };\n\n const look = async (): Promise<void> => {\n const video = camera.videoRef.current;\n const detector = detectorRef.current;\n if (!video || !detector || !frameIsReady(video)) return;\n try {\n const found = await detector.detect(video);\n if (stopped) return;\n for (const raw of found) {\n const scan = normalizeBarcode(raw);\n if (scan.rawValue !== \"\") accept(scan);\n }\n } catch (error) {\n if (!stopped) emitError(error);\n }\n };\n\n const tick = (): void => {\n void look().finally(() => {\n if (!stopped) timer = setTimeout(tick, intervalMs);\n });\n };\n tick();\n\n return () => {\n stopped = true;\n if (timer !== undefined) clearTimeout(timer);\n setScanning(false);\n };\n }, [\n supported,\n paused,\n camera.status,\n camera.videoRef,\n intervalMs,\n repeatDelayMs,\n emitScan,\n emitError,\n ]);\n\n return {\n videoRef: camera.videoRef,\n status: camera.status,\n error: camera.error,\n supported,\n formats,\n scanning,\n result,\n torch,\n retry: camera.retry,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-barcode-scanner.js","names":[],"sources":["../../src/capture/use-barcode-scanner.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the scan loop, the repeat suppression window and the\n * torch control share the same track and the same interval handle; splitting them\n * would leave a timer running against a track another hook stopped.\n */\nimport { useEffect, useRef, useState, type RefObject } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\nimport {\n useCameraStream,\n type CameraStreamError,\n type CameraStreamStatus,\n} from \"@/vision/use-camera-stream\";\n\nimport {\n createBarcodeDetector,\n getSupportedBarcodeFormats,\n isBarcodeDetectionSupported,\n normalizeBarcode,\n DEFAULT_BARCODE_FORMATS,\n type BarcodeDetectorLike,\n type BarcodeFormat,\n type BarcodeScanResult,\n} from \"./barcode\";\nimport { useTorch, type UseTorchResult } from \"./use-torch\";\n\n/** Options for {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerOptions {\n /** Symbologies to look for. Defaults to {@link DEFAULT_BARCODE_FORMATS}. */\n formats?: readonly BarcodeFormat[];\n /** Called for every accepted read — that is, after repeat suppression. */\n onScan?: (result: BarcodeScanResult) => void;\n /**\n * How often a frame is examined, in ms. Default 200.\n *\n * Not `requestAnimationFrame`: decoding is 10–40 ms of main-thread work on a\n * phone, so running it per frame competes with the preview it is reading from and\n * makes the video stutter. Five looks per second is faster than a human can aim.\n */\n intervalMs?: number;\n /**\n * Ignore the **same** value again for this long, in ms. Default 2500.\n *\n * A symbol stays in frame for as long as the user holds the camera there, so a\n * scanner without this fires the same code five times a second — which, wired to\n * \"add item to cart\", is a bug the user pays for. A *different* value is never\n * suppressed.\n */\n repeatDelayMs?: number;\n /** Stop looking without releasing the camera — a confirmation sheet is open. */\n paused?: boolean;\n /**\n * A decoder to use instead of the native one.\n *\n * The way to support Safari and Firefox: hand in a polyfill and everything else\n * here works unchanged. See {@link isBarcodeDetectionSupported} for why the SDK\n * does not bundle one.\n */\n detector?: BarcodeDetectorLike;\n /** Camera constraints, forwarded to `useCameraStream`. Defaults to the rear camera. */\n constraints?: MediaStreamConstraints;\n /**\n * A frame the engine refused to decode.\n *\n * Not \"nothing found\" — that resolves to an empty list and is the normal case.\n * This is the engine itself failing, which the loop survives because it is\n * usually transient (a frame arriving between two resolutions).\n */\n onError?: (error: unknown) => void;\n}\n\n/** Value returned by {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerResult {\n /** Attach to a `<video ref={…} muted playsInline />`. */\n videoRef: RefObject<HTMLVideoElement | null>;\n /** Camera lifecycle. `\"ready\"` means the preview is running. */\n status: CameraStreamStatus;\n /** Classified camera error, or `null`. */\n error: CameraStreamError | null;\n /** `false` when there is no decoder — no native API and none injected. */\n supported: boolean;\n /**\n * Formats actually in use: the requested ones intersected with what the engine\n * reports. Empty while the probe is in flight, or when nothing matched.\n */\n formats: readonly BarcodeFormat[];\n /** Whether the detect loop is running right now. */\n scanning: boolean;\n /** The most recent accepted read, or `null`. */\n result: BarcodeScanResult | null;\n /** The LED torch of this camera, when it has one. */\n torch: UseTorchResult;\n /** Re-open the camera after an error (the user changed the permission). */\n retry: () => void;\n}\n\n/** A video is decodable once it has data for the current frame and a real size. */\nfunction frameIsReady(video: HTMLVideoElement): boolean {\n return video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0;\n}\n\n/**\n * Read barcodes and QR codes from the camera.\n *\n * The camera and its classified errors come from `useCameraStream`, so this hook is\n * only the decoding half: it drives a `BarcodeDetector` over the preview on an\n * interval, suppresses the same value repeating, and exposes the torch.\n *\n * **Mounting this opens the camera.** It inherits that from `useCameraStream`, which\n * acquires on mount — so mount it *after* the user asks to scan (a button that reveals\n * the scanner), never on a page that merely contains one. A permission prompt nobody\n * provoked is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever prompting again.\n *\n * `supported` deserves a branch in the UI, not an assertion: `BarcodeDetector` is\n * Chromium-only and missing on Windows/Linux desktop, Firefox and everything on iOS.\n * Inject a `detector` to cover those, or tell the user to type the code.\n *\n * @param options - See {@link UseBarcodeScannerOptions}.\n * @returns The camera plumbing plus the scan state.\n *\n * @example\n * const scanner = useBarcodeScanner({\n * formats: [\"ean_13\"],\n * onScan: ({ rawValue }) => addToCart(rawValue),\n * });\n * return <video ref={scanner.videoRef} muted playsInline />;\n */\nexport function useBarcodeScanner(options: UseBarcodeScannerOptions = {}): UseBarcodeScannerResult {\n const {\n formats: requested = DEFAULT_BARCODE_FORMATS,\n onScan,\n intervalMs = 200,\n repeatDelayMs = 2500,\n paused = false,\n detector: injected,\n constraints,\n onError,\n } = options;\n\n const [supported, setSupported] = useState(\n () => injected !== undefined || isBarcodeDetectionSupported(),\n );\n\n /**\n * No decoder, no camera.\n *\n * Opening the camera only to report \"this browser cannot decode barcodes\" spends a\n * permission prompt on nothing — and a refusal is permanent, so it also spends the\n * *next* feature that needs the camera.\n */\n const camera = useCameraStream({ constraints, enabled: supported });\n const torch = useTorch(camera.stream);\n\n const [formats, setFormats] = useState<readonly BarcodeFormat[]>(\n injected !== undefined ? requested : [],\n );\n const [scanning, setScanning] = useState(false);\n const [result, setResult] = useState<BarcodeScanResult | null>(null);\n\n const detectorRef = useRef<BarcodeDetectorLike | null>(null);\n const lastValue = useRef<string | null>(null);\n const lastAt = useRef(0);\n\n const emitScan = useStableCallback((scan: BarcodeScanResult) => onScan?.(scan));\n const emitError = useStableCallback((error: unknown) => onError?.(error));\n\n const requestedKey = requested.join(\",\");\n\n /**\n * Resolve which formats the engine will take, then build the detector.\n *\n * The intersection is not defensive coding: `new BarcodeDetector({ formats })`\n * throws `NotSupportedError` when any entry is unknown to the platform decoder, and\n * that list differs between two Chromium builds on two operating systems. Asking\n * for the intersection is the only way one call site works everywhere.\n */\n useEffect(() => {\n if (injected) {\n detectorRef.current = injected;\n setSupported(true);\n setFormats(requested);\n return;\n }\n if (!isBarcodeDetectionSupported()) {\n detectorRef.current = null;\n setSupported(false);\n setFormats([]);\n return;\n }\n let cancelled = false;\n void getSupportedBarcodeFormats().then((available) => {\n if (cancelled) return;\n const usable =\n available.length === 0\n ? requested\n : requested.filter((format) => available.includes(format));\n const detector = usable.length > 0 ? createBarcodeDetector(usable) : null;\n detectorRef.current = detector;\n setFormats(detector ? usable : []);\n setSupported(detector !== null);\n });\n return () => {\n cancelled = true;\n };\n // `requestedKey` stands in for the array identity, so a caller passing an\n // inline `formats={[\"ean_13\"]}` does not rebuild the detector every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [injected, requestedKey]);\n\n /**\n * Look at a frame every `intervalMs`, and never overlap two looks.\n *\n * The loop re-arms itself *after* each `detect()` settles rather than running on a\n * fixed `setInterval`: decoding sometimes takes longer than the interval, and an\n * interval would then queue calls faster than the engine drains them until the tab\n * is unusable.\n */\n useEffect(() => {\n if (!supported || paused || camera.status !== \"ready\") {\n setScanning(false);\n return;\n }\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n setScanning(true);\n\n const accept = (scan: BarcodeScanResult): void => {\n const now = Date.now();\n const isRepeat =\n scan.rawValue === lastValue.current && now - lastAt.current < repeatDelayMs;\n if (isRepeat) return;\n lastValue.current = scan.rawValue;\n lastAt.current = now;\n setResult(scan);\n emitScan(scan);\n };\n\n const look = async (): Promise<void> => {\n const video = camera.videoRef.current;\n const detector = detectorRef.current;\n if (!video || !detector || !frameIsReady(video)) return;\n try {\n const found = await detector.detect(video);\n if (stopped) return;\n for (const raw of found) {\n const scan = normalizeBarcode(raw);\n if (scan.rawValue !== \"\") accept(scan);\n }\n } catch (error) {\n if (!stopped) emitError(error);\n }\n };\n\n const tick = (): void => {\n void look().finally(() => {\n if (!stopped) timer = setTimeout(tick, intervalMs);\n });\n };\n tick();\n\n return () => {\n stopped = true;\n if (timer !== undefined) clearTimeout(timer);\n setScanning(false);\n };\n }, [\n supported,\n paused,\n camera.status,\n camera.videoRef,\n intervalMs,\n repeatDelayMs,\n emitScan,\n emitError,\n ]);\n\n return {\n videoRef: camera.videoRef,\n status: camera.status,\n error: camera.error,\n supported,\n formats,\n scanning,\n result,\n torch,\n retry: camera.retry,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("../EmptyState/EmptyState.cjs"),n=require("../../hooks/use-announce.cjs"),r=require("./ai-chat-turns.cjs"),i=require("./AIChat.module.cjs"),a=require("./AIChatComposer.cjs"),o=require("./AIChatTurn.cjs");let s=require("react"),c=require("react/jsx-runtime"),l=require("lucide-react");var u=48;function d({messages:d,onSend:f,onStop:p,onRegenerate:m,onEditSubmit:h,onFeedback:g,onRetry:_,pending:v=!1,suggestions:y=[],renderAvatar:b,renderContent:x,votes:S,header:C,emptyState:w,showSystem:T=!1,defaultReasoningOpen:E=!1,showLineNumbers:D=!1,locale:O=`pt-BR`,placeholder:k,composerActions:A,composerRef:j,composerFooter:M,composerDisabled:N,maxRows:P,onSendError:F,className:I,...L}){let R=r.aiChatStrings(O),z=r.visibleTurns({messages:d,showSystem:T}),B=v||r.isGenerating(d),V=r.lastAssistantId(d),H=r.tailSignature(d),U=(0,s.useRef)(null),W=(0,s.useRef)(null),G=(0,s.useRef)(!0),K=(0,s.useRef)(!1),[q,J]=(0,s.useState)(!0),Y=n.useAnnounce(),X=()=>{let e=W.current;if(!e)return;let t=e.scrollHeight-e.scrollTop-e.clientHeight<u;G.current=t,J(t)},Z=()=>{let e=W.current;e&&(G.current=!0,J(!0),e.scrollTop=e.scrollHeight)};(0,s.useLayoutEffect)(()=>{let e=W.current
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../EmptyState/EmptyState.cjs"),n=require("../../hooks/use-announce.cjs"),r=require("./ai-chat-turns.cjs"),i=require("./AIChat.module.cjs"),a=require("./AIChatComposer.cjs"),o=require("./AIChatTurn.cjs");let s=require("react"),c=require("react/jsx-runtime"),l=require("lucide-react");var u=48;function d({messages:d,onSend:f,onStop:p,onRegenerate:m,onEditSubmit:h,onFeedback:g,onRetry:_,pending:v=!1,suggestions:y=[],renderAvatar:b,renderContent:x,votes:S,header:C,emptyState:w,showSystem:T=!1,defaultReasoningOpen:E=!1,showLineNumbers:D=!1,locale:O=`pt-BR`,placeholder:k,composerActions:A,composerRef:j,composerFooter:M,composerDisabled:N,maxRows:P,onSendError:F,className:I,...L}){let R=r.aiChatStrings(O),z=r.visibleTurns({messages:d,showSystem:T}),B=v||r.isGenerating(d),V=r.lastAssistantId(d),H=r.tailSignature(d),U=(0,s.useRef)(null),W=(0,s.useRef)(null),G=(0,s.useRef)(!0),K=(0,s.useRef)(!1),[q,J]=(0,s.useState)(!0),Y=n.useAnnounce(),X=()=>{let e=W.current;if(!e)return;let t=e.scrollHeight-e.scrollTop-e.clientHeight<u;G.current=t,J(t)},Z=()=>{let e=W.current;e&&(G.current=!0,J(!0),e.scrollTop=e.scrollHeight)};(0,s.useLayoutEffect)(()=>{let e=W.current;e&&G.current&&(e.scrollTop=e.scrollHeight)},[H,v]),(0,s.useEffect)(()=>{let e=W.current;e&&(e.scrollTop=e.scrollHeight)},[]),(0,s.useEffect)(()=>{if(B){Y(R.generating),K.current=!0;return}K.current&&=(Y(R.done),!1)},[B,R.generating,R.done,Y]);let Q=z.length===0&&y.length>0&&f!==void 0,$=!q&&z.length>0;return(0,c.jsxs)(`div`,{className:e.cn(i.default.panel,I),...L,children:[C&&(0,c.jsx)(`header`,{className:i.default.header,children:C}),(0,c.jsxs)(`div`,{className:i.default.threadWrapper,children:[(0,c.jsxs)(`div`,{ref:W,className:i.default.thread,onScroll:X,role:`log`,tabIndex:0,"aria-label":R.thread,children:[z.length===0&&!Q?w??(0,c.jsx)(t.EmptyState,{title:R.empty,description:R.emptyHint}):z.map(e=>(0,c.jsx)(o.AIChatTurn,{message:e,locale:O,canRegenerate:e.id===V&&e.streaming!==!0,onRegenerate:m,onFeedback:g,onEditSubmit:h,onEditError:F,onRetry:_,renderAvatar:b,renderContent:x,vote:S?.[e.id],defaultReasoningOpen:E,showLineNumbers:D},e.id)),Q&&(0,c.jsxs)(`div`,{className:i.default.suggestions,children:[(0,c.jsx)(`p`,{className:i.default.suggestionsTitle,children:R.emptyHint}),y.map(e=>(0,c.jsx)(`button`,{type:`button`,className:i.default.suggestion,onClick:()=>void f?.(e),children:e},e))]}),v&&!r.isGenerating(d)&&(0,c.jsxs)(`p`,{className:i.default.thinking,children:[(0,c.jsxs)(`span`,{className:i.default.dots,"aria-hidden":`true`,children:[(0,c.jsx)(`span`,{}),(0,c.jsx)(`span`,{}),(0,c.jsx)(`span`,{})]}),R.thinking]})]}),$&&(0,c.jsx)(`button`,{type:`button`,className:i.default.jump,onClick:Z,"aria-label":R.jumpToLatest,title:R.jumpToLatest,children:(0,c.jsx)(l.ArrowDown,{size:16,"aria-hidden":!0})})]}),f&&(0,c.jsx)(a.AIChatComposer,{ref:j??U,onSend:f,onStop:p,generating:B,locale:O,placeholder:k,actions:A,footer:M,disabled:N,maxRows:P,onError:F})]})}exports.AIChat=d;
|
|
2
2
|
//# sourceMappingURL=AIChat.cjs.map
|