tempest-react-sdk 0.38.3 → 0.39.1
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 +1 -1
- package/dist/_virtual/_rolldown/runtime.cjs +1 -1
- package/dist/access/access-control-context.cjs.map +1 -1
- package/dist/access/use-can.cjs.map +1 -1
- package/dist/app/AppProviders.cjs.map +1 -1
- package/dist/app/AppProviders.js.map +1 -1
- package/dist/audio/use-audio-recorder.cjs.map +1 -1
- package/dist/audio/use-audio.cjs.map +1 -1
- package/dist/audio/use-microphone.cjs.map +1 -1
- package/dist/auth/create-auth-store.cjs.map +1 -1
- package/dist/auth/lazy-with-retry.cjs.map +1 -1
- package/dist/auth/use-passkey.cjs.map +1 -1
- package/dist/br/BrazilMap.cjs +1 -1
- package/dist/br/BrazilMap.cjs.map +1 -1
- package/dist/br/BrazilMap.js +2 -2
- package/dist/br/BrazilMap.js.map +1 -1
- package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
- package/dist/br/BrazilStateCitySelect.js.map +1 -1
- package/dist/br/BrazilStateMap.cjs +1 -1
- package/dist/br/BrazilStateMap.cjs.map +1 -1
- package/dist/br/BrazilStateMap.js +2 -2
- package/dist/br/BrazilStateMap.js.map +1 -1
- package/dist/br/MapLegend.cjs.map +1 -1
- package/dist/br/MapLegend.js.map +1 -1
- package/dist/br/MapMarkers.cjs.map +1 -1
- package/dist/br/MapMarkers.js.map +1 -1
- package/dist/br/MunicipalitySearch.cjs.map +1 -1
- package/dist/br/MunicipalitySearch.js.map +1 -1
- package/dist/br/PixQRCode.cjs.map +1 -1
- package/dist/br/PixQRCode.js.map +1 -1
- package/dist/br/use-map-hover.cjs.map +1 -1
- package/dist/br/use-map-zoom.cjs.map +1 -1
- package/dist/capture/media-recorder.cjs +1 -1
- package/dist/capture/media-recorder.js +2 -2
- 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 +2 -2
- package/dist/capture/use-barcode-scanner.js.map +1 -1
- package/dist/capture/use-screen-capture.cjs.map +1 -1
- package/dist/capture/use-speech-recognition.cjs.map +1 -1
- package/dist/capture/use-torch.cjs.map +1 -1
- package/dist/capture/use-video-recorder.cjs.map +1 -1
- package/dist/charts/AreaChart.cjs.map +1 -1
- package/dist/charts/AreaChart.js.map +1 -1
- package/dist/charts/BarChart.cjs.map +1 -1
- package/dist/charts/BarChart.js.map +1 -1
- package/dist/charts/LineChart.cjs.map +1 -1
- package/dist/charts/LineChart.js.map +1 -1
- package/dist/charts/PieChart.cjs.map +1 -1
- package/dist/charts/PieChart.js.map +1 -1
- package/dist/charts/RadarChart.cjs.map +1 -1
- package/dist/charts/RadarChart.js.map +1 -1
- package/dist/charts/use-chart-colors.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/Accordion/Accordion.cjs.map +1 -1
- package/dist/components/Accordion/Accordion.js.map +1 -1
- package/dist/components/Alert/Alert.cjs.map +1 -1
- package/dist/components/Alert/Alert.js.map +1 -1
- package/dist/components/AppBar/AppBar.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.js.map +1 -1
- package/dist/components/AppShell/AppShell.cjs.map +1 -1
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
- package/dist/components/Avatar/Avatar.cjs.map +1 -1
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/AvatarGroup/AvatarGroup.cjs.map +1 -1
- package/dist/components/AvatarGroup/AvatarGroup.js.map +1 -1
- package/dist/components/Badge/Badge.cjs.map +1 -1
- package/dist/components/Badge/Badge.js.map +1 -1
- package/dist/components/Banner/Banner.cjs.map +1 -1
- package/dist/components/Banner/Banner.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.cjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.js.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.js.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.cjs +1 -1
- package/dist/components/Calendar/Calendar.cjs.map +1 -1
- package/dist/components/Calendar/Calendar.js +38 -41
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Card/Card.cjs.map +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/Carousel/Carousel.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.js.map +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/Chat/ChatComposer.cjs.map +1 -1
- package/dist/components/Chat/ChatComposer.js.map +1 -1
- package/dist/components/Chat/chat-groups.cjs +1 -1
- package/dist/components/Chat/chat-groups.cjs.map +1 -1
- package/dist/components/Chat/chat-groups.js +2 -2
- package/dist/components/Chat/chat-groups.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.cjs.map +1 -1
- package/dist/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
- package/dist/components/ChipInput/ChipInput.js.map +1 -1
- package/dist/components/ClickOutside/ClickOutside.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/Collapsible/Collapsible.cjs.map +1 -1
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/Combobox/Combobox.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.cjs.map +1 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
- package/dist/components/CopyButton/CopyButton.js.map +1 -1
- package/dist/components/DataList/DataList.cjs.map +1 -1
- package/dist/components/DataList/DataList.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +2 -2
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs.map +1 -1
- package/dist/components/DataTable/EditableCell.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.cjs.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.js.map +1 -1
- package/dist/components/Divider/Divider.cjs.map +1 -1
- package/dist/components/Divider/Divider.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.js.map +1 -1
- package/dist/components/EmptyState/EmptyState.cjs.map +1 -1
- package/dist/components/EmptyState/EmptyState.js.map +1 -1
- package/dist/components/ErrorState/ErrorState.cjs.map +1 -1
- package/dist/components/ErrorState/ErrorState.js.map +1 -1
- package/dist/components/FileUpload/FileUpload.cjs +1 -1
- package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
- package/dist/components/FileUpload/FileUpload.js +1 -1
- package/dist/components/FileUpload/FileUpload.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.js.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.cjs.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.js.map +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/HoverCard/HoverCard.cjs.map +1 -1
- package/dist/components/HoverCard/HoverCard.js.map +1 -1
- package/dist/components/Image/Image.cjs.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/Input/Input.cjs.map +1 -1
- package/dist/components/Input/Input.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
- package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
- package/dist/components/InstallButton/InstallButton.js.map +1 -1
- package/dist/components/Kanban/Kanban.cjs.map +1 -1
- package/dist/components/Kanban/Kanban.js.map +1 -1
- package/dist/components/Label/Label.cjs.map +1 -1
- package/dist/components/Label/Label.js.map +1 -1
- package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
- package/dist/components/Lightbox/Lightbox.js.map +1 -1
- package/dist/components/ListTile/ListTile.cjs.map +1 -1
- package/dist/components/ListTile/ListTile.js.map +1 -1
- package/dist/components/Markdown/Markdown.cjs +1 -1
- package/dist/components/Markdown/Markdown.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.js +2 -2
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/Masonry/Masonry.cjs.map +1 -1
- package/dist/components/Masonry/Masonry.js.map +1 -1
- package/dist/components/Menubar/Menubar.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.js.map +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
- package/dist/components/Money/Money.cjs.map +1 -1
- package/dist/components/Money/Money.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/dist/components/NProgress/NProgress.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.cjs.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
- package/dist/components/NotificationCenter/use-notification-inbox.cjs.map +1 -1
- package/dist/components/OfflineIndicator/OfflineIndicator.cjs.map +1 -1
- package/dist/components/OfflineIndicator/OfflineIndicator.js.map +1 -1
- package/dist/components/Page/Page.cjs.map +1 -1
- package/dist/components/Page/Page.js.map +1 -1
- package/dist/components/Pagination/Pagination.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
- package/dist/components/PinInput/PinInput.cjs.map +1 -1
- package/dist/components/PinInput/PinInput.js.map +1 -1
- package/dist/components/Popover/Popover.cjs.map +1 -1
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Portal/Portal.cjs.map +1 -1
- package/dist/components/Progress/Progress.cjs.map +1 -1
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/QRCode/QRCode.cjs.map +1 -1
- package/dist/components/QRCode/QRCode.js.map +1 -1
- package/dist/components/Radio/Radio.cjs.map +1 -1
- package/dist/components/Radio/Radio.js.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
- package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
- package/dist/components/RatingStars/RatingStars.js.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
- package/dist/components/RelativeTime/RelativeTime.cjs.map +1 -1
- package/dist/components/RelativeTime/RelativeTime.js.map +1 -1
- package/dist/components/Resizable/Resizable.cjs.map +1 -1
- package/dist/components/Resizable/Resizable.js.map +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.cjs.map +1 -1
- package/dist/components/SearchBar/SearchBar.cjs.map +1 -1
- package/dist/components/SearchBar/SearchBar.js.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.cjs.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/components/Select/Select.cjs.map +1 -1
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
- package/dist/components/Slider/Slider.cjs.map +1 -1
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
- package/dist/components/Sparkline/Sparkline.js.map +1 -1
- package/dist/components/Spinner/Spinner.cjs.map +1 -1
- package/dist/components/Spinner/Spinner.js.map +1 -1
- package/dist/components/Stat/Stat.cjs.map +1 -1
- package/dist/components/Stat/Stat.js.map +1 -1
- package/dist/components/Stepper/Stepper.cjs.map +1 -1
- package/dist/components/Stepper/Stepper.js.map +1 -1
- package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
- package/dist/components/StepperInput/StepperInput.js.map +1 -1
- package/dist/components/Switch/Switch.cjs.map +1 -1
- package/dist/components/Switch/Switch.js.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js.map +1 -1
- package/dist/components/Table/Table.cjs.map +1 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tabs/Tabs.cjs.map +1 -1
- package/dist/components/Tabs/Tabs.js.map +1 -1
- package/dist/components/Tag/Tag.cjs.map +1 -1
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Textarea/Textarea.cjs.map +1 -1
- package/dist/components/Textarea/Textarea.js.map +1 -1
- package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
- package/dist/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/components/Timeline/Timeline.cjs.map +1 -1
- package/dist/components/Timeline/Timeline.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/components/Toggle/Toggle.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.cjs.map +1 -1
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/Tour/Tour.cjs.map +1 -1
- package/dist/components/Tour/Tour.js.map +1 -1
- package/dist/components/Transfer/Transfer.cjs.map +1 -1
- package/dist/components/Transfer/Transfer.js.map +1 -1
- package/dist/components/TreeView/TreeView.cjs +1 -1
- package/dist/components/TreeView/TreeView.cjs.map +1 -1
- package/dist/components/TreeView/TreeView.js +1 -4
- package/dist/components/TreeView/TreeView.js.map +1 -1
- package/dist/components/UpdatePrompt/UpdatePrompt.cjs.map +1 -1
- package/dist/components/UpdatePrompt/UpdatePrompt.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/components/VisuallyHidden/VisuallyHidden.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.js.map +1 -1
- package/dist/data/data-provider-context.cjs.map +1 -1
- package/dist/data/use-resource.cjs.map +1 -1
- package/dist/editor/RichTextEditor.cjs.map +1 -1
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/error-boundary/use-error-handler.cjs.map +1 -1
- package/dist/feature-flags/FeatureFlagsProvider.cjs.map +1 -1
- package/dist/forms/FormField.cjs.map +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/use-viacep.cjs.map +1 -1
- package/dist/forms/use-zod-form.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.js.map +1 -1
- package/dist/geo/leaflet-map.cjs.map +1 -1
- package/dist/geo/leaflet-map.js.map +1 -1
- package/dist/geo/use-position-tracker.cjs.map +1 -1
- package/dist/hooks/use-announce.cjs.map +1 -1
- package/dist/hooks/use-async.cjs.map +1 -1
- package/dist/hooks/use-before-install-prompt.cjs.map +1 -1
- package/dist/hooks/use-breakpoint.cjs.map +1 -1
- package/dist/hooks/use-click-outside.cjs.map +1 -1
- package/dist/hooks/use-client-filter.cjs.map +1 -1
- package/dist/hooks/use-clipboard.cjs.map +1 -1
- package/dist/hooks/use-counter.cjs.map +1 -1
- package/dist/hooks/use-debounce.cjs.map +1 -1
- package/dist/hooks/use-deep-memo.cjs.map +1 -1
- package/dist/hooks/use-disclosure.cjs.map +1 -1
- package/dist/hooks/use-document-visibility.cjs.map +1 -1
- package/dist/hooks/use-event-listener.cjs.map +1 -1
- package/dist/hooks/use-geolocation.cjs.map +1 -1
- package/dist/hooks/use-hover.cjs.map +1 -1
- package/dist/hooks/use-idle.cjs.map +1 -1
- package/dist/hooks/use-install-prompt.cjs +1 -1
- package/dist/hooks/use-install-prompt.cjs.map +1 -1
- package/dist/hooks/use-install-prompt.js +1 -1
- package/dist/hooks/use-install-prompt.js.map +1 -1
- package/dist/hooks/use-intersection-observer.cjs.map +1 -1
- package/dist/hooks/use-interval.cjs.map +1 -1
- package/dist/hooks/use-is-first-render.cjs.map +1 -1
- package/dist/hooks/use-keyboard-shortcut.cjs +1 -1
- package/dist/hooks/use-keyboard-shortcut.cjs.map +1 -1
- package/dist/hooks/use-keyboard-shortcut.js +1 -1
- package/dist/hooks/use-keyboard-shortcut.js.map +1 -1
- package/dist/hooks/use-list-state.cjs.map +1 -1
- package/dist/hooks/use-local-storage.cjs.map +1 -1
- package/dist/hooks/use-long-press-handlers.cjs.map +1 -1
- package/dist/hooks/use-long-press.cjs.map +1 -1
- package/dist/hooks/use-map.cjs.map +1 -1
- package/dist/hooks/use-media-devices.cjs.map +1 -1
- package/dist/hooks/use-media-permission.cjs.map +1 -1
- package/dist/hooks/use-media-query.cjs.map +1 -1
- package/dist/hooks/use-object-url.cjs.map +1 -1
- package/dist/hooks/use-online.cjs.map +1 -1
- package/dist/hooks/use-pagination.cjs.map +1 -1
- package/dist/hooks/use-previous.cjs.map +1 -1
- package/dist/hooks/use-queue.cjs.map +1 -1
- package/dist/hooks/use-resize-observer.cjs.map +1 -1
- package/dist/hooks/use-scroll-overflow.cjs.map +1 -1
- package/dist/hooks/use-service-worker-update.cjs.map +1 -1
- package/dist/hooks/use-set.cjs.map +1 -1
- package/dist/hooks/use-sortable.cjs +1 -1
- package/dist/hooks/use-sortable.cjs.map +1 -1
- package/dist/hooks/use-sortable.js +0 -2
- package/dist/hooks/use-sortable.js.map +1 -1
- package/dist/hooks/use-stable-callback.cjs.map +1 -1
- package/dist/hooks/use-storage-estimate.cjs.map +1 -1
- package/dist/hooks/use-throttle.cjs.map +1 -1
- package/dist/hooks/use-timeout.cjs.map +1 -1
- package/dist/hooks/use-toggle.cjs.map +1 -1
- package/dist/hooks/use-window-size.cjs.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +7 -5
- package/dist/http/api-client.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/http/use-poll.cjs.map +1 -1
- package/dist/i18n/I18nProvider.cjs.map +1 -1
- package/dist/icons/Icon.cjs.map +1 -1
- package/dist/icons/IconProvider.cjs.map +1 -1
- package/dist/icons/icon-context.cjs.map +1 -1
- package/dist/icons/use-icon.cjs.map +1 -1
- package/dist/imaging/use-image-processing.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/offline/use-offline-sync.cjs.map +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/query/QueryProvider.cjs.map +1 -1
- package/dist/query/constants.cjs +1 -1
- package/dist/query/constants.cjs.map +1 -1
- package/dist/query/constants.js +10 -10
- package/dist/query/constants.js.map +1 -1
- package/dist/query/create-query-keys.cjs +1 -1
- package/dist/query/create-query-keys.cjs.map +1 -1
- package/dist/query/create-query-keys.js +1 -1
- package/dist/query/create-query-keys.js.map +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/use-cursor-query.cjs.map +1 -1
- package/dist/query/use-offline-mutation.cjs.map +1 -1
- package/dist/query/use-paginated-query.cjs.map +1 -1
- package/dist/router/AppRouter.cjs +1 -1
- package/dist/router/AppRouter.cjs.map +1 -1
- package/dist/router/AppRouter.js +7 -2
- package/dist/router/AppRouter.js.map +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/store/create-store.cjs.map +1 -1
- package/dist/tabular/use-tabular-predictor.cjs.map +1 -1
- package/dist/telemetry/TelemetryProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/color.cjs +1 -1
- package/dist/theme/color.cjs.map +1 -1
- package/dist/theme/color.js +1 -1
- package/dist/theme/color.js.map +1 -1
- package/dist/utils/cn.cjs +1 -1
- package/dist/utils/cn.cjs.map +1 -1
- package/dist/utils/cn.js +1 -1
- package/dist/utils/cn.js.map +1 -1
- package/dist/utils/objects.cjs +1 -1
- package/dist/utils/objects.cjs.map +1 -1
- package/dist/utils/objects.js +1 -1
- package/dist/utils/objects.js.map +1 -1
- package/dist/utils/xlsx.cjs +1 -1
- package/dist/utils/xlsx.cjs.map +1 -1
- package/dist/utils/xlsx.js +3 -3
- package/dist/utils/xlsx.js.map +1 -1
- package/dist/vision/core/canvas.cjs +1 -1
- package/dist/vision/core/canvas.cjs.map +1 -1
- package/dist/vision/core/canvas.js +4 -4
- package/dist/vision/core/canvas.js.map +1 -1
- package/dist/vision/core/exceptions.cjs +1 -1
- package/dist/vision/core/exceptions.cjs.map +1 -1
- package/dist/vision/core/exceptions.js +2 -2
- package/dist/vision/core/exceptions.js.map +1 -1
- package/dist/vision/core/metadata.cjs +2 -2
- package/dist/vision/core/metadata.cjs.map +1 -1
- package/dist/vision/core/metadata.js +11 -8
- package/dist/vision/core/metadata.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 +8 -5
- package/dist/vision/core/session.js.map +1 -1
- package/dist/vision/fusion.cjs +2 -0
- package/dist/vision/fusion.cjs.map +1 -0
- package/dist/vision/fusion.js +42 -0
- package/dist/vision/fusion.js.map +1 -0
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +23 -20
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/io/image.cjs +1 -1
- package/dist/vision/io/image.cjs.map +1 -1
- package/dist/vision/io/image.js +4 -2
- package/dist/vision/io/image.js.map +1 -1
- package/dist/vision/labels.cjs +1 -1
- package/dist/vision/labels.cjs.map +1 -1
- package/dist/vision/labels.js +7 -4
- package/dist/vision/labels.js.map +1 -1
- package/dist/vision/postprocess/detection.cjs +1 -1
- package/dist/vision/postprocess/detection.cjs.map +1 -1
- package/dist/vision/postprocess/detection.js +3 -10
- package/dist/vision/postprocess/detection.js.map +1 -1
- package/dist/vision/postprocess/segmentation.cjs +1 -1
- package/dist/vision/postprocess/segmentation.cjs.map +1 -1
- package/dist/vision/postprocess/segmentation.js +11 -15
- package/dist/vision/postprocess/segmentation.js.map +1 -1
- package/dist/vision/preprocess/image.cjs +1 -1
- package/dist/vision/preprocess/image.cjs.map +1 -1
- package/dist/vision/preprocess/image.js +4 -2
- package/dist/vision/preprocess/image.js.map +1 -1
- package/dist/vision/preprocess/pipeline.cjs +2 -0
- package/dist/vision/preprocess/pipeline.cjs.map +1 -0
- package/dist/vision/preprocess/pipeline.js +61 -0
- package/dist/vision/preprocess/pipeline.js.map +1 -0
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +23 -2
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/base.cjs +1 -1
- package/dist/vision/tasks/base.cjs.map +1 -1
- package/dist/vision/tasks/base.js +11 -2
- package/dist/vision/tasks/base.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 +2 -2
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detectClassify.cjs +2 -0
- package/dist/vision/tasks/detectClassify.cjs.map +1 -0
- package/dist/vision/tasks/detectClassify.js +222 -0
- package/dist/vision/tasks/detectClassify.js.map +1 -0
- package/dist/vision/tasks/detector.cjs +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js +54 -29
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js +53 -28
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision/types.cjs.map +1 -1
- package/dist/vision/types.js.map +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-live-luminance.cjs.map +1 -1
- package/dist/vision.cjs +1 -1
- package/dist/vision.d.ts +1517 -962
- package/dist/vision.js +25 -22
- package/dist/vite/create-vite-config.cjs +1 -1
- package/dist/vite/create-vite-config.cjs.map +1 -1
- package/dist/vite/create-vite-config.js +3 -3
- package/dist/vite/create-vite-config.js.map +1 -1
- package/dist/vite/tempest-icons.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +5 -3
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +2 -2
- package/template/package.json +1 -1
|
@@ -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));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,
|
|
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":"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"}
|
|
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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"}
|
|
@@ -11,7 +11,7 @@ function f(f = {}) {
|
|
|
11
11
|
let { formats: p = n, onScan: m, intervalMs: h = 200, repeatDelayMs: g = 2500, paused: _ = !1, detector: v, constraints: y, onError: b } = f, [x, S] = u(() => v !== void 0 || a()), C = t({
|
|
12
12
|
constraints: y,
|
|
13
13
|
enabled: x
|
|
14
|
-
}), w = s(C.stream), [T, E] = u(v === void 0 ? [] : p), [D, O] = u(!1), [k, A] = u(null), j = l(null), M = l(null), N = l(0), P = e((e) => m?.(e)), F = e((e) => b?.(e));
|
|
14
|
+
}), w = s(C.stream), [T, E] = u(v === void 0 ? [] : p), [D, O] = u(!1), [k, A] = u(null), j = l(null), M = l(null), N = l(0), P = e((e) => m?.(e)), F = e((e) => b?.(e)), I = p.join(",");
|
|
15
15
|
return c(() => {
|
|
16
16
|
if (v) {
|
|
17
17
|
j.current = v, S(!0), E(p);
|
|
@@ -29,7 +29,7 @@ function f(f = {}) {
|
|
|
29
29
|
}), () => {
|
|
30
30
|
e = !0;
|
|
31
31
|
};
|
|
32
|
-
}, [v,
|
|
32
|
+
}, [v, I]), c(() => {
|
|
33
33
|
if (!x || _ || C.status !== "ready") {
|
|
34
34
|
O(!1);
|
|
35
35
|
return;
|
|
@@ -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":";;;;;;AAiGA,SAAS,EAAa,GAAkC;CACpD,OAAO,EAAM,cAAc,KAAK,EAAM,aAAa,KAAK,EAAM,cAAc;AAChF;AA6BA,SAAgB,EAAkB,IAAoC,CAAC,GAA4B;CAC/F,IAAM,EACF,SAAS,IAAY,GACrB,WACA,gBAAa,KACb,mBAAgB,MAChB,YAAS,IACT,UAAU,GACV,gBACA,eACA,GAEE,CAAC,GAAW,KAAgB,QACxB,MAAa,KAAA,KAAa,EAA4B,CAChE,GASM,IAAS,EAAgB;EAAE;EAAa,SAAS;CAAU,CAAC,GAC5D,IAAQ,EAAS,EAAO,MAAM,GAE9B,CAAC,GAAS,KAAc,EAC1B,MAAa,KAAA,IAAwB,CAAC,IAAb,CAC7B,GACM,CAAC,GAAU,KAAe,EAAS,EAAK,GACxC,CAAC,GAAQ,KAAa,EAAmC,IAAI,GAE7D,IAAc,EAAmC,IAAI,GACrD,IAAY,EAAsB,IAAI,GACtC,IAAS,EAAO,CAAC,GAEjB,IAAW,GAAmB,MAA4B,IAAS,CAAI,CAAC,GACxE,IAAY,GAAmB,MAAmB,IAAU,CAAK,CAAC;CAgHxE,OApGA,QAAgB;EACZ,IAAI,GAAU;GAGV,AAFA,EAAY,UAAU,GACtB,EAAa,EAAI,GACjB,EAAW,CAAS;GACpB;EACJ;EACA,IAAI,CAAC,EAA4B,GAAG;GAGhC,AAFA,EAAY,UAAU,MACtB,EAAa,EAAK,GAClB,EAAW,CAAC,CAAC;GACb;EACJ;EACA,IAAI,IAAY;EAYhB,OAXA,EAAgC,CAAC,CAAC,MAAM,MAAc;GAClD,IAAI,GAAW;GACf,IAAM,IACF,EAAU,WAAW,IACf,IACA,EAAU,QAAQ,MAAW,EAAU,SAAS,CAAM,CAAC,GAC3D,IAAW,EAAO,SAAS,IAAI,EAAsB,CAAM,IAAI;GAGrE,AAFA,EAAY,UAAU,GACtB,EAAW,IAAW,IAAS,CAAC,CAAC,GACjC,EAAa,MAAa,IAAI;EAClC,CAAC,SACY;GACT,IAAY;EAChB;CAIJ,GAAG,CAAC,GAzCiB,EAAU,KAAK,GAyCtB,CAAY,CAAC,GAU3B,QAAgB;EACZ,IAAI,CAAC,KAAa,KAAU,EAAO,WAAW,SAAS;GACnD,EAAY,EAAK;GACjB;EACJ;EACA,IAAI,IAAU,IACV;EACJ,EAAY,EAAI;EAEhB,IAAM,KAAU,MAAkC;GAC9C,IAAM,IAAM,KAAK,IAAI;GAEjB,EAAK,aAAa,EAAU,WAAW,IAAM,EAAO,UAAU,MAElE,EAAU,UAAU,EAAK,UACzB,EAAO,UAAU,GACjB,EAAU,CAAI,GACd,EAAS,CAAI;EACjB,GAEM,IAAO,YAA2B;GACpC,IAAM,IAAQ,EAAO,SAAS,SACxB,IAAW,EAAY;GACzB,OAAC,KAAS,CAAC,KAAY,CAAC,EAAa,CAAK,IAC9C,IAAI;IACA,IAAM,IAAQ,MAAM,EAAS,OAAO,CAAK;IACzC,IAAI,GAAS;IACb,KAAK,IAAM,KAAO,GAAO;KACrB,IAAM,IAAO,EAAiB,CAAG;KACjC,AAAI,EAAK,aAAa,MAAI,EAAO,CAAI;IACzC;GACJ,SAAS,GAAO;IACZ,AAAK,KAAS,EAAU,CAAK;GACjC;EACJ,GAEM,UAAmB;GACrB,EAAU,CAAC,CAAC,cAAc;IACtB,AAAK,MAAS,IAAQ,WAAW,GAAM,CAAU;GACrD,CAAC;EACL;EAGA,OAFA,EAAK,SAEQ;GAGT,AAFA,IAAU,IACN,MAAU,KAAA,KAAW,aAAa,CAAK,GAC3C,EAAY,EAAK;EACrB;CACJ,GAAG;EACC;EACA;EACA,EAAO;EACP,EAAO;EACP;EACA;EACA;EACA;CACJ,CAAC,GAEM;EACH,UAAU,EAAO;EACjB,QAAQ,EAAO;EACf,OAAO,EAAO;EACd;EACA;EACA;EACA;EACA;EACA,OAAO,EAAO;CAClB;AACJ"}
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-screen-capture.cjs","names":[],"sources":["../../src/capture/use-screen-capture.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — getDisplayMedia's cancellation is indistinguishable\n * from a policy block, so the hook holds the classification, the track's own `ended`\n * event (the user can stop sharing from the browser's bar) and the stop path in one\n * place — all three end the same session.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"@/audio/media-access\";\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\n/** Lifecycle of a screen share. */\nexport type ScreenCaptureStatus = \"idle\" | \"requesting\" | \"sharing\" | \"error\";\n\n/** Which surface to put first in the picker. */\nexport type DisplaySurfaceHint = \"monitor\" | \"window\" | \"browser\";\n\n/**\n * Hints `getDisplayMedia` takes that TypeScript's `DisplayMediaStreamOptions` does not\n * list yet.\n */\ntype DisplayMediaOptionsWithHints = DisplayMediaStreamOptions & {\n preferCurrentTab?: boolean;\n selfBrowserSurface?: \"include\" | \"exclude\";\n surfaceSwitching?: \"include\" | \"exclude\";\n systemAudio?: \"include\" | \"exclude\";\n};\n\n/** `displaySurface` is a constrainable property of a display track, but not in the DOM lib. */\ntype DisplayVideoConstraints = MediaTrackConstraints & { displaySurface?: DisplaySurfaceHint };\n\n/** Options for {@link useScreenCapture}. */\nexport interface UseScreenCaptureOptions {\n /**\n * Capture the tab's audio too. Default `false`.\n *\n * Chromium only offers this for a **tab** — sharing a window or a whole screen\n * yields no audio track no matter what you ask for, and Safari has no display\n * audio at all. Ask for it and check what you got.\n */\n audio?: boolean;\n /**\n * Which surface the picker should offer first — `\"browser\"` is a tab.\n *\n * A hint, never a guarantee: the user can always pick something else, and Firefox\n * ignores it. Read `surface` afterwards to learn what actually happened.\n */\n displaySurface?: DisplaySurfaceHint;\n /**\n * Put *this* tab at the top of the picker. Default `false`.\n *\n * The right setting for \"record what you are seeing right now\" in a support flow.\n * Chromium only.\n */\n preferCurrentTab?: boolean;\n /** Offer this tab in the list at all. `\"exclude\"` prevents the hall-of-mirrors capture. */\n selfBrowserSurface?: \"include\" | \"exclude\";\n /** Let the user switch to a different surface mid-share, without a new prompt. */\n surfaceSwitching?: \"include\" | \"exclude\";\n /** Include the system audio when a whole screen is shared. Chromium, Windows only. */\n systemAudio?: \"include\" | \"exclude\";\n /** Escape hatch: full options, replacing everything above. */\n options?: DisplayMediaStreamOptions;\n /**\n * The user stopped the share from the browser's own bar.\n *\n * The single most important callback here — see the note on\n * {@link useScreenCapture}.\n */\n onEnded?: () => void;\n /**\n * The user dismissed the picker. **Not an error.**\n *\n * Receives the rejection so an app that needs to tell a dismissal from an OS-level\n * block (macOS screen-recording permission) can look at the message. Most should\n * simply return the UI to its previous state.\n */\n onCancelled?: (reason: unknown) => void;\n}\n\n/** Value returned by {@link useScreenCapture}. */\nexport interface UseScreenCaptureResult {\n status: ScreenCaptureStatus;\n /** The live stream, or `null`. Feed it to {@link useVideoRecorder} or a `<video>`. */\n stream: MediaStream | null;\n /** Classified error, or `null`. A cancelled picker leaves this `null`. */\n error: MediaAccessError | null;\n /** What the user actually picked, when the browser reports it. */\n surface: string | null;\n /** Whether the stream carries an audio track — ask, do not assume. */\n hasAudio: boolean;\n /** `false` when `getDisplayMedia` is missing (every browser on iOS, insecure pages). */\n supported: boolean;\n /** Open the picker. Must be called from a user gesture. */\n start: () => void;\n /** Stop sharing from the app side. Fires nothing — you asked for it. */\n stop: () => void;\n}\n\n/** Whether `getDisplayMedia` is reachable at all. */\nexport function isScreenCaptureSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n navigator.mediaDevices !== undefined &&\n typeof navigator.mediaDevices.getDisplayMedia === \"function\"\n );\n}\n\n/**\n * A rejection from `getDisplayMedia` that means \"the user said no thanks\".\n *\n * There is no distinct exception for a dismissed picker: closing it produces the same\n * `NotAllowedError` as a policy block, and some builds report `AbortError` instead. The\n * useful default is therefore to treat both as a cancellation, because a\n * display-capture prompt is **always** user-initiated — nothing can open it behind\n * their back — so the overwhelmingly likely cause is that they changed their mind, and\n * a red error toast for that punishes them for it. The rejection is handed to\n * `onCancelled` so the rarer causes stay diagnosable.\n */\nfunction isCancellation(err: unknown): boolean {\n return (\n err instanceof DOMException && (err.name === \"NotAllowedError\" || err.name === \"AbortError\")\n );\n}\n\n/**\n * Capture a screen, a window or a tab with `getDisplayMedia`.\n *\n * Three states decide whether this feels right, and two of them are easy to miss:\n *\n * - **The user dismissed the picker.** A rejection, but not a failure. It leaves\n * `error` at `null` and the status back at `\"idle\"`, and calls `onCancelled`.\n * - **The user stopped the share from the browser's own bar.** Nothing in your UI was\n * clicked and no promise rejects — the *only* signal is the video track's `ended`\n * event, so the hook listens for it and clears the stream. Without that listener, an\n * app shows \"gravando\" over a stream that is already dead.\n * - **The share is live.** `surface` says what was picked and `hasAudio` says whether\n * audio actually came along, which is not what you asked for but what you got.\n *\n * The stream is owned here: `stop()` and unmount both release every track. A recorder\n * built on it (`useVideoRecorder`) deliberately does not, so stopping a recording\n * leaves the share running for the next take.\n *\n * @param options - See {@link UseScreenCaptureOptions}.\n * @returns The stream, its status, a classified error and `start`/`stop`.\n *\n * @example\n * const screen = useScreenCapture({ preferCurrentTab: true, onEnded: () => save() });\n * const rec = useVideoRecorder(screen.stream);\n * <button onClick={screen.start}>Compartilhar tela</button>\n */\nexport function useScreenCapture(options: UseScreenCaptureOptions = {}): UseScreenCaptureResult {\n const {\n audio = false,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n options: raw,\n onEnded,\n onCancelled,\n } = options;\n\n const [status, setStatus] = useState<ScreenCaptureStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const [surface, setSurface] = useState<string | null>(null);\n const [hasAudio, setHasAudio] = useState(false);\n const [supported] = useState(isScreenCaptureSupported);\n\n const streamRef = useRef<MediaStream | null>(null);\n const detach = useRef<(() => void) | null>(null);\n const generation = useRef(0);\n\n const emitEnded = useStableCallback(() => onEnded?.());\n const emitCancelled = useStableCallback((reason: unknown) => onCancelled?.(reason));\n\n const release = useCallback((): void => {\n detach.current?.();\n detach.current = null;\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const reset = useCallback((): void => {\n setStream(null);\n setSurface(null);\n setHasAudio(false);\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n reset();\n setStatus(\"idle\");\n setError(null);\n }, [release, reset]);\n\n const start = useCallback((): void => {\n if (streamRef.current) return;\n const run = (generation.current += 1);\n setStatus(\"requesting\");\n setError(null);\n\n if (!isScreenCaptureSupported()) {\n setError(missingCaptureApiError(\"screen\"));\n setStatus(\"error\");\n return;\n }\n\n const video: DisplayVideoConstraints = displaySurface ? { displaySurface } : {};\n const request: DisplayMediaOptionsWithHints = raw ?? {\n video,\n audio,\n ...(preferCurrentTab !== undefined ? { preferCurrentTab } : {}),\n ...(selfBrowserSurface !== undefined ? { selfBrowserSurface } : {}),\n ...(surfaceSwitching !== undefined ? { surfaceSwitching } : {}),\n ...(systemAudio !== undefined ? { systemAudio } : {}),\n };\n\n void navigator.mediaDevices\n .getDisplayMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n const track = next.getVideoTracks()[0];\n const ended = (): void => {\n if (run !== generation.current) return;\n release();\n reset();\n setStatus(\"idle\");\n emitEnded();\n };\n track?.addEventListener(\"ended\", ended);\n detach.current = () => track?.removeEventListener(\"ended\", ended);\n\n setStream(next);\n setSurface(track?.getSettings().displaySurface ?? null);\n setHasAudio(next.getAudioTracks().length > 0);\n setStatus(\"sharing\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n if (isCancellation(err)) {\n setStatus(\"idle\");\n emitCancelled(err);\n return;\n }\n setError(classifyMediaError(err, \"screen\"));\n setStatus(\"error\");\n });\n }, [\n audio,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n raw,\n release,\n reset,\n emitEnded,\n emitCancelled,\n ]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, surface, hasAudio, supported, start, stop };\n}\n"],"mappings":"kHAwGA,SAAgB,GAAoC,CAChD,OACI,OAAO,UAAc,KACrB,UAAU,eAAiB,IAAA,IAC3B,OAAO,UAAU,aAAa,iBAAoB,UAE1D,CAaA,SAAS,EAAe,EAAuB,CAC3C,OACI,aAAe,eAAiB,EAAI,OAAS,mBAAqB,EAAI,OAAS,aAEvF,CA4BA,SAAgB,EAAiB,EAAmC,CAAC,EAA2B,CAC5F,GAAM,CACF,QAAQ,GACR,iBACA,mBACA,qBACA,mBACA,cACA,QAAS,EACT,UACA,eACA,EAEE,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAA2C,MAAM,EAC1D,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAA0C,IAAI,EACvD,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAA8C,IAAI,EAC1D,CAAC,EAAS,IAAA,EAAA,EAAA,SAAA,CAAsC,IAAI,EACpD,CAAC,EAAU,IAAA,EAAA,EAAA,SAAA,CAAwB,EAAK,EACxC,CAAC,IAAA,EAAA,EAAA,SAAA,CAAsB,CAAwB,EAE/C,GAAA,EAAA,EAAA,OAAA,CAAuC,IAAI,EAC3C,GAAA,EAAA,EAAA,OAAA,CAAqC,IAAI,EACzC,GAAA,EAAA,EAAA,OAAA,CAAoB,CAAC,EAErB,EAAY,EAAA,sBAAwB,IAAU,CAAC,EAC/C,EAAgB,EAAA,kBAAmB,GAAoB,IAAc,CAAM,CAAC,EAE5E,GAAA,EAAA,EAAA,YAAA,KAAkC,CACpC,EAAO,UAAU,EACjB,EAAO,QAAU,KACjB,EAAU,SAAS,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAC9D,EAAU,QAAU,IACxB,EAAG,CAAC,CAAC,EAEC,GAAA,EAAA,EAAA,YAAA,KAAgC,CAClC,EAAU,IAAI,EACd,EAAW,IAAI,EACf,EAAY,EAAK,CACrB,EAAG,CAAC,CAAC,EAEC,GAAA,EAAA,EAAA,YAAA,KAA+B,CACjC,EAAW,SAAW,EACtB,EAAQ,EACR,EAAM,EACN,EAAU,MAAM,EAChB,EAAS,IAAI,CACjB,EAAG,CAAC,EAAS,CAAK,CAAC,EAEb,GAAA,EAAA,EAAA,YAAA,KAAgC,CAClC,GAAI,EAAU,QAAS,OACvB,IAAM,EAAO,EAAW,SAAW,EAInC,GAHA,EAAU,YAAY,EACtB,EAAS,IAAI,EAET,CAAC,EAAyB,EAAG,CAC7B,EAAS,EAAA,uBAAuB,QAAQ,CAAC,EACzC,EAAU,OAAO,EACjB,MACJ,CAGA,IAAM,EAAwC,GAAO,CACjD,MAFmC,EAAiB,CAAE,gBAAe,EAAI,CAAC,EAG1E,QACA,GAAI,IAAqB,IAAA,GAAmC,CAAC,EAAxB,CAAE,kBAAiB,EACxD,GAAI,IAAuB,IAAA,GAAqC,CAAC,EAA1B,CAAE,oBAAmB,EAC5D,GAAI,IAAqB,IAAA,GAAmC,CAAC,EAAxB,CAAE,kBAAiB,EACxD,GAAI,IAAgB,IAAA,GAA8B,CAAC,EAAnB,CAAE,aAAY,CAClD,EAEA,UAAe,aACV,gBAAgB,CAAO,CAAC,CACxB,KAAM,GAAS,CACZ,GAAI,IAAQ,EAAW,QAAS,CAC5B,EAAK,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAChD,MACJ,CACA,EAAU,QAAU,EACpB,IAAM,EAAQ,EAAK,eAAe,CAAC,CAAC,GAC9B,MAAoB,CAClB,IAAQ,EAAW,UACvB,EAAQ,EACR,EAAM,EACN,EAAU,MAAM,EAChB,EAAU,EACd,EACA,GAAO,iBAAiB,QAAS,CAAK,EACtC,EAAO,YAAgB,GAAO,oBAAoB,QAAS,CAAK,EAEhE,EAAU,CAAI,EACd,EAAW,GAAO,YAAY,CAAC,CAAC,gBAAkB,IAAI,EACtD,EAAY,EAAK,eAAe,CAAC,CAAC,OAAS,CAAC,EAC5C,EAAU,SAAS,CACvB,CAAC,CAAC,CACD,MAAO,GAAiB,CACjB,OAAQ,EAAW,QACvB,IAAI,EAAe,CAAG,EAAG,CACrB,EAAU,MAAM,EAChB,EAAc,CAAG,EACjB,MACJ,CACA,EAAS,EAAA,mBAAmB,EAAK,QAAQ,CAAC,EAC1C,EAAU,OAAO,CAFjB,CAGJ,CAAC,CACT,EAAG,CACC,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,CACJ,CAAC,EAID,OAFA,EAAA,EAAA,UAAA,KAAgB,EAAS,CAAC,CAAO,CAAC,EAE3B,CAAE,SAAQ,SAAQ,QAAO,UAAS,WAAU,YAAW,QAAO,MAAK,CAC9E"}
|
|
1
|
+
{"version":3,"file":"use-screen-capture.cjs","names":[],"sources":["../../src/capture/use-screen-capture.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — getDisplayMedia's cancellation is indistinguishable\n * from a policy block, so the hook holds the classification, the track's own `ended`\n * event (the user can stop sharing from the browser's bar) and the stop path in one\n * place — all three end the same session.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"@/audio/media-access\";\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\n/** Lifecycle of a screen share. */\nexport type ScreenCaptureStatus = \"idle\" | \"requesting\" | \"sharing\" | \"error\";\n\n/** Which surface to put first in the picker. */\nexport type DisplaySurfaceHint = \"monitor\" | \"window\" | \"browser\";\n\n/**\n * Hints `getDisplayMedia` takes that TypeScript's `DisplayMediaStreamOptions` does not\n * list yet.\n */\ntype DisplayMediaOptionsWithHints = DisplayMediaStreamOptions & {\n preferCurrentTab?: boolean;\n selfBrowserSurface?: \"include\" | \"exclude\";\n surfaceSwitching?: \"include\" | \"exclude\";\n systemAudio?: \"include\" | \"exclude\";\n};\n\n/** `displaySurface` is a constrainable property of a display track, but not in the DOM lib. */\ntype DisplayVideoConstraints = MediaTrackConstraints & { displaySurface?: DisplaySurfaceHint };\n\n/** Options for {@link useScreenCapture}. */\nexport interface UseScreenCaptureOptions {\n /**\n * Capture the tab's audio too. Default `false`.\n *\n * Chromium only offers this for a **tab** — sharing a window or a whole screen\n * yields no audio track no matter what you ask for, and Safari has no display\n * audio at all. Ask for it and check what you got.\n */\n audio?: boolean;\n /**\n * Which surface the picker should offer first — `\"browser\"` is a tab.\n *\n * A hint, never a guarantee: the user can always pick something else, and Firefox\n * ignores it. Read `surface` afterwards to learn what actually happened.\n */\n displaySurface?: DisplaySurfaceHint;\n /**\n * Put *this* tab at the top of the picker. Default `false`.\n *\n * The right setting for \"record what you are seeing right now\" in a support flow.\n * Chromium only.\n */\n preferCurrentTab?: boolean;\n /** Offer this tab in the list at all. `\"exclude\"` prevents the hall-of-mirrors capture. */\n selfBrowserSurface?: \"include\" | \"exclude\";\n /** Let the user switch to a different surface mid-share, without a new prompt. */\n surfaceSwitching?: \"include\" | \"exclude\";\n /** Include the system audio when a whole screen is shared. Chromium, Windows only. */\n systemAudio?: \"include\" | \"exclude\";\n /** Escape hatch: full options, replacing everything above. */\n options?: DisplayMediaStreamOptions;\n /**\n * The user stopped the share from the browser's own bar.\n *\n * The single most important callback here — see the note on\n * {@link useScreenCapture}.\n */\n onEnded?: () => void;\n /**\n * The user dismissed the picker. **Not an error.**\n *\n * Receives the rejection so an app that needs to tell a dismissal from an OS-level\n * block (macOS screen-recording permission) can look at the message. Most should\n * simply return the UI to its previous state.\n */\n onCancelled?: (reason: unknown) => void;\n}\n\n/** Value returned by {@link useScreenCapture}. */\nexport interface UseScreenCaptureResult {\n status: ScreenCaptureStatus;\n /** The live stream, or `null`. Feed it to {@link useVideoRecorder} or a `<video>`. */\n stream: MediaStream | null;\n /** Classified error, or `null`. A cancelled picker leaves this `null`. */\n error: MediaAccessError | null;\n /** What the user actually picked, when the browser reports it. */\n surface: string | null;\n /** Whether the stream carries an audio track — ask, do not assume. */\n hasAudio: boolean;\n /** `false` when `getDisplayMedia` is missing (every browser on iOS, insecure pages). */\n supported: boolean;\n /** Open the picker. Must be called from a user gesture. */\n start: () => void;\n /** Stop sharing from the app side. Fires nothing — you asked for it. */\n stop: () => void;\n}\n\n/** Whether `getDisplayMedia` is reachable at all. */\nexport function isScreenCaptureSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n navigator.mediaDevices !== undefined &&\n typeof navigator.mediaDevices.getDisplayMedia === \"function\"\n );\n}\n\n/**\n * A rejection from `getDisplayMedia` that means \"the user said no thanks\".\n *\n * There is no distinct exception for a dismissed picker: closing it produces the same\n * `NotAllowedError` as a policy block, and some builds report `AbortError` instead. The\n * useful default is therefore to treat both as a cancellation, because a\n * display-capture prompt is **always** user-initiated — nothing can open it behind\n * their back — so the overwhelmingly likely cause is that they changed their mind, and\n * a red error toast for that punishes them for it. The rejection is handed to\n * `onCancelled` so the rarer causes stay diagnosable.\n */\nfunction isCancellation(err: unknown): boolean {\n return (\n err instanceof DOMException && (err.name === \"NotAllowedError\" || err.name === \"AbortError\")\n );\n}\n\n/**\n * Capture a screen, a window or a tab with `getDisplayMedia`.\n *\n * Three states decide whether this feels right, and two of them are easy to miss:\n *\n * - **The user dismissed the picker.** A rejection, but not a failure. It leaves\n * `error` at `null` and the status back at `\"idle\"`, and calls `onCancelled`.\n * - **The user stopped the share from the browser's own bar.** Nothing in your UI was\n * clicked and no promise rejects — the *only* signal is the video track's `ended`\n * event, so the hook listens for it and clears the stream. Without that listener, an\n * app shows \"gravando\" over a stream that is already dead.\n * - **The share is live.** `surface` says what was picked and `hasAudio` says whether\n * audio actually came along, which is not what you asked for but what you got.\n *\n * The stream is owned here: `stop()` and unmount both release every track. A recorder\n * built on it (`useVideoRecorder`) deliberately does not, so stopping a recording\n * leaves the share running for the next take.\n *\n * @param options - See {@link UseScreenCaptureOptions}.\n * @returns The stream, its status, a classified error and `start`/`stop`.\n *\n * @example\n * const screen = useScreenCapture({ preferCurrentTab: true, onEnded: () => save() });\n * const rec = useVideoRecorder(screen.stream);\n * <button onClick={screen.start}>Compartilhar tela</button>\n */\nexport function useScreenCapture(options: UseScreenCaptureOptions = {}): UseScreenCaptureResult {\n const {\n audio = false,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n options: raw,\n onEnded,\n onCancelled,\n } = options;\n\n const [status, setStatus] = useState<ScreenCaptureStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const [surface, setSurface] = useState<string | null>(null);\n const [hasAudio, setHasAudio] = useState(false);\n const [supported] = useState(isScreenCaptureSupported);\n\n const streamRef = useRef<MediaStream | null>(null);\n const detach = useRef<(() => void) | null>(null);\n const generation = useRef(0);\n\n const emitEnded = useStableCallback(() => onEnded?.());\n const emitCancelled = useStableCallback((reason: unknown) => onCancelled?.(reason));\n\n const release = useCallback((): void => {\n detach.current?.();\n detach.current = null;\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const reset = useCallback((): void => {\n setStream(null);\n setSurface(null);\n setHasAudio(false);\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n reset();\n setStatus(\"idle\");\n setError(null);\n }, [release, reset]);\n\n const start = useCallback((): void => {\n if (streamRef.current) return;\n const run = (generation.current += 1);\n setStatus(\"requesting\");\n setError(null);\n\n if (!isScreenCaptureSupported()) {\n setError(missingCaptureApiError(\"screen\"));\n setStatus(\"error\");\n return;\n }\n\n const video: DisplayVideoConstraints = displaySurface ? { displaySurface } : {};\n const request: DisplayMediaOptionsWithHints = raw ?? {\n video,\n audio,\n ...(preferCurrentTab !== undefined ? { preferCurrentTab } : {}),\n ...(selfBrowserSurface !== undefined ? { selfBrowserSurface } : {}),\n ...(surfaceSwitching !== undefined ? { surfaceSwitching } : {}),\n ...(systemAudio !== undefined ? { systemAudio } : {}),\n };\n\n void navigator.mediaDevices\n .getDisplayMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n const track = next.getVideoTracks()[0];\n const ended = (): void => {\n if (run !== generation.current) return;\n release();\n reset();\n setStatus(\"idle\");\n emitEnded();\n };\n track?.addEventListener(\"ended\", ended);\n detach.current = () => track?.removeEventListener(\"ended\", ended);\n\n setStream(next);\n setSurface(track?.getSettings().displaySurface ?? null);\n setHasAudio(next.getAudioTracks().length > 0);\n setStatus(\"sharing\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n if (isCancellation(err)) {\n setStatus(\"idle\");\n emitCancelled(err);\n return;\n }\n setError(classifyMediaError(err, \"screen\"));\n setStatus(\"error\");\n });\n }, [\n audio,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n raw,\n release,\n reset,\n emitEnded,\n emitCancelled,\n ]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, surface, hasAudio, supported, start, stop };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-speech-recognition.cjs","names":[],"sources":["../../src/capture/use-speech-recognition.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, hook-lines — the Web Speech API restarts itself on\n * silence, reports interim and final results on the same event, and reports `no-\n * speech` as an error that is not one. The hook is that reconciliation plus the\n * continuous-mode restart loop, and its length is mostly the vendor quirks the\n * docstring warns about.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\n/** One reading of what was heard. */\nexport interface SpeechAlternativeLike {\n transcript: string;\n confidence?: number;\n}\n\n/** One recognised phrase, settled (`isFinal`) or still being revised. */\nexport interface SpeechResultLike {\n isFinal: boolean;\n length: number;\n [index: number]: SpeechAlternativeLike | undefined;\n}\n\n/** The growing list of phrases in a session. */\nexport interface SpeechResultListLike {\n length: number;\n [index: number]: SpeechResultLike | undefined;\n}\n\n/** The `result` event, reduced to what this hook reads. */\nexport interface SpeechRecognitionEventLike {\n /** Index of the first result that changed — everything before it is settled. */\n resultIndex: number;\n results: SpeechResultListLike;\n}\n\n/** The `error` event, reduced to what this hook reads. */\nexport interface SpeechRecognitionErrorEventLike {\n error: string;\n message?: string;\n}\n\n/**\n * The slice of the Web Speech `SpeechRecognition` object this SDK uses.\n *\n * Declared here rather than imported: TypeScript's DOM lib ships the *event* types but\n * not the constructor, because the API is still prefixed in Chromium and absent in\n * Firefox. Exported so a test — or a consumer wrapping a different engine — can hand in\n * something else.\n */\nexport interface SpeechRecognitionLike {\n lang: string;\n continuous: boolean;\n interimResults: boolean;\n maxAlternatives: number;\n start: () => void;\n stop: () => void;\n abort: () => void;\n onresult: ((event: SpeechRecognitionEventLike) => void) | null;\n onerror: ((event: SpeechRecognitionErrorEventLike) => void) | null;\n onend: (() => void) | null;\n onstart: (() => void) | null;\n}\n\n/** Classified reason recognition stopped or never started. */\nexport type SpeechErrorKind =\n | \"unsupported\"\n | \"not-allowed\"\n | \"no-speech\"\n | \"audio-capture\"\n | \"network\"\n | \"aborted\"\n | \"language-not-supported\"\n | \"unknown\";\n\n/** A classified speech error with a human-readable, English message. */\nexport interface SpeechError {\n kind: SpeechErrorKind;\n message: string;\n}\n\n/** Options for {@link useSpeechRecognition}. */\nexport interface UseSpeechRecognitionOptions {\n /** BCP-47 tag. Default `\"pt-BR\"`. */\n lang?: string;\n /**\n * Keep listening after the first phrase settles. Default `false`.\n *\n * Even with this on, the engine ends the session by itself after a stretch of\n * silence — that is a server-side timeout, not a bug — so watch `listening` rather\n * than assuming the microphone stays open.\n */\n continuous?: boolean;\n /** Publish the running guess as it changes. Default `true`. */\n interimResults?: boolean;\n /** How many readings per phrase to ask for. Default 1. */\n maxAlternatives?: number;\n /** Every update, settled or not. */\n onResult?: (result: { transcript: string; isFinal: boolean }) => void;\n /** Only the settled text of a phrase. The one to wire dictation to. */\n onFinal?: (transcript: string) => void;\n /** Classified failure. `no-speech` and `aborted` arrive here too — they are routine. */\n onError?: (error: SpeechError) => void;\n /** The session ended, for any reason. */\n onEnd?: () => void;\n /** Build the recogniser yourself — another engine, or a stub in a test. */\n factory?: () => SpeechRecognitionLike;\n}\n\n/** Value returned by {@link useSpeechRecognition}. */\nexport interface UseSpeechRecognitionResult {\n /** `false` in Firefox and in every browser that is not Chromium-based. */\n supported: boolean;\n /** Whether a session is open right now. */\n listening: boolean;\n /** Everything settled so far in this session. Cleared by `reset()`. */\n transcript: string;\n /** The running guess. Replaced on every event, empty once the phrase settles. */\n interim: string;\n /** Classified error, or `null`. */\n error: SpeechError | null;\n /** Open a session. No-op while already listening. */\n start: () => void;\n /** Close the session, keeping what was recognised. */\n stop: () => void;\n /** Close the session and throw the pending phrase away. */\n abort: () => void;\n /** Clear `transcript`, `interim` and `error`. Does not stop a session. */\n reset: () => void;\n}\n\n/** English messages for the `error` codes the spec defines. */\nconst MESSAGES: Record<SpeechErrorKind, string> = {\n unsupported: \"Speech recognition is not supported in this browser.\",\n \"not-allowed\": \"Microphone permission denied. Enable access in your browser settings.\",\n \"no-speech\": \"No speech was detected.\",\n \"audio-capture\": \"No microphone available on this device.\",\n network: \"The recognition service could not be reached.\",\n aborted: \"Recognition was cancelled.\",\n \"language-not-supported\": \"The recognition service does not support this language.\",\n unknown: \"Unexpected error during speech recognition.\",\n};\n\n/**\n * Map a spec `error` code to a kind an app can branch on.\n *\n * `service-not-allowed` collapses into `not-allowed` because the fix is the same from\n * the user's side, and `bad-grammar` into `unknown` because this hook never sets a\n * grammar, so seeing it means something outside our control went wrong.\n *\n * @param code - The `error` property of the event.\n * @returns The classified kind.\n */\nfunction classifySpeechError(code: string): SpeechErrorKind {\n switch (code) {\n case \"not-allowed\":\n case \"service-not-allowed\":\n return \"not-allowed\";\n case \"no-speech\":\n return \"no-speech\";\n case \"audio-capture\":\n return \"audio-capture\";\n case \"network\":\n return \"network\";\n case \"aborted\":\n return \"aborted\";\n case \"language-not-supported\":\n return \"language-not-supported\";\n default:\n return \"unknown\";\n }\n}\n\n/** The constructor, prefixed or not, or `null` where the API does not exist. */\nfunction speechRecognitionConstructor(): (new () => SpeechRecognitionLike) | null {\n const scope = globalThis as {\n SpeechRecognition?: unknown;\n webkitSpeechRecognition?: unknown;\n };\n const candidate = scope.SpeechRecognition ?? scope.webkitSpeechRecognition;\n return typeof candidate === \"function\" ? (candidate as new () => SpeechRecognitionLike) : null;\n}\n\n/** Whether this browser exposes the Web Speech recognition API at all. */\nexport function isSpeechRecognitionSupported(): boolean {\n return speechRecognitionConstructor() !== null;\n}\n\n/**\n * Dictate into your app with the Web Speech API — no dependency, no API key.\n *\n * ## Recognition is not local\n *\n * **Chromium streams the captured audio to a Google server to transcribe it.** Nothing\n * about the API says so, there is no setting that changes it, and it happens on every\n * `start()`. Anything the user says while a session is open leaves the device. Do not\n * put this on a field that takes clinical notes, credentials, or a client's financial\n * detail without telling them first — and if the data cannot leave your infrastructure,\n * this API is the wrong tool and a self-hosted model is the right one.\n *\n * ## What the states mean\n *\n * `transcript` accumulates the phrases the engine has **settled** on; `interim` is the\n * guess it is still revising and is replaced wholesale on every event, so rendering\n * `transcript + interim` gives the live caption effect and rendering `transcript` alone\n * gives the committed text. `no-speech` and `aborted` come through `onError` but are\n * routine — a user who pressed the button and said nothing is not a failure to report.\n *\n * There is deliberately **no auto-restart** when the engine ends a session on silence.\n * A restart loop is how an app ends up holding the microphone indefinitely — and, in\n * Chromium, streaming audio to a third party indefinitely. Show that listening stopped\n * and let the user press again.\n *\n * @param options - See {@link UseSpeechRecognitionOptions}.\n * @returns Session state, the transcript and the controls.\n *\n * @example\n * const speech = useSpeechRecognition({ onFinal: (text) => setPrompt(text) });\n * <button onClick={speech.listening ? speech.stop : speech.start}>\n * {speech.listening ? \"Parar\" : \"Ditar\"}\n * </button>\n * <p>{speech.transcript}{speech.interim}</p>\n */\nexport function useSpeechRecognition(\n options: UseSpeechRecognitionOptions = {},\n): UseSpeechRecognitionResult {\n const {\n lang = \"pt-BR\",\n continuous = false,\n interimResults = true,\n maxAlternatives = 1,\n onResult,\n onFinal,\n onError,\n onEnd,\n factory,\n } = options;\n\n const [supported] = useState(() => factory !== undefined || isSpeechRecognitionSupported());\n const [listening, setListening] = useState(false);\n const [transcript, setTranscript] = useState(\"\");\n const [interim, setInterim] = useState(\"\");\n const [error, setError] = useState<SpeechError | null>(null);\n\n const sessionRef = useRef<SpeechRecognitionLike | null>(null);\n\n const emitResult = useStableCallback((result: { transcript: string; isFinal: boolean }) =>\n onResult?.(result),\n );\n const emitFinal = useStableCallback((text: string) => onFinal?.(text));\n const emitEnd = useStableCallback(() => onEnd?.());\n const emitError = useStableCallback((failure: SpeechError) => {\n setError(failure);\n onError?.(failure);\n });\n\n const teardown = useCallback((): void => {\n const session = sessionRef.current;\n if (!session) return;\n session.onresult = null;\n session.onerror = null;\n session.onend = null;\n session.onstart = null;\n sessionRef.current = null;\n }, []);\n\n const start = useCallback((): void => {\n if (sessionRef.current) return;\n const build =\n factory ??\n (() => {\n const constructor = speechRecognitionConstructor();\n return constructor ? new constructor() : null;\n });\n const session = build();\n if (!session) {\n emitError({ kind: \"unsupported\", message: MESSAGES.unsupported });\n return;\n }\n\n session.lang = lang;\n session.continuous = continuous;\n session.interimResults = interimResults;\n session.maxAlternatives = maxAlternatives;\n\n /**\n * Fold one event into the two published strings.\n *\n * Only the results from `resultIndex` on are new; re-reading the whole list\n * would append phrases that are already in `transcript`. Settled text is\n * accumulated, unsettled text replaces the previous guess entirely — the engine\n * revises a phrase in place, so appending it would stutter the caption.\n */\n session.onresult = (event: SpeechRecognitionEventLike): void => {\n let settled = \"\";\n let pending = \"\";\n for (let index = event.resultIndex; index < event.results.length; index += 1) {\n const result = event.results[index];\n if (!result) continue;\n const text = result[0]?.transcript ?? \"\";\n if (result.isFinal) settled += text;\n else pending += text;\n }\n setInterim(pending);\n if (settled !== \"\") {\n setTranscript((previous) => previous + settled);\n emitFinal(settled);\n }\n emitResult({ transcript: settled !== \"\" ? settled : pending, isFinal: settled !== \"\" });\n };\n\n session.onerror = (event: SpeechRecognitionErrorEventLike): void => {\n const kind = classifySpeechError(event.error);\n emitError({ kind, message: event.message || MESSAGES[kind] });\n };\n\n session.onend = (): void => {\n setListening(false);\n setInterim(\"\");\n teardown();\n emitEnd();\n };\n\n session.onstart = (): void => setListening(true);\n\n setError(null);\n try {\n session.start();\n } catch (failure) {\n // Chromium throws `InvalidStateError` when a session is already running in\n // another component. Reporting it beats leaving a dead handle behind.\n emitError({\n kind: \"unknown\",\n message: failure instanceof Error ? failure.message : MESSAGES.unknown,\n });\n return;\n }\n sessionRef.current = session;\n setListening(true);\n }, [\n lang,\n continuous,\n interimResults,\n maxAlternatives,\n factory,\n emitResult,\n emitFinal,\n emitEnd,\n emitError,\n teardown,\n ]);\n\n const stop = useCallback((): void => {\n sessionRef.current?.stop();\n }, []);\n\n const abort = useCallback((): void => {\n sessionRef.current?.abort();\n }, []);\n\n const reset = useCallback((): void => {\n setTranscript(\"\");\n setInterim(\"\");\n setError(null);\n }, []);\n\n /**\n * Never leave a session open past unmount.\n *\n * `abort()` and not `stop()`: a component that is gone has nowhere to put a final\n * result, and `stop()` would keep the microphone — and the upstream connection —\n * alive while the engine finishes deciding what the last word was.\n */\n useEffect(\n () => () => {\n sessionRef.current?.abort();\n teardown();\n },\n [teardown],\n );\n\n return {\n supported,\n listening,\n transcript,\n interim,\n error,\n start,\n stop,\n abort,\n reset,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-speech-recognition.cjs","names":[],"sources":["../../src/capture/use-speech-recognition.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, hook-lines — the Web Speech API restarts itself on\n * silence, reports interim and final results on the same event, and reports `no-\n * speech` as an error that is not one. The hook is that reconciliation plus the\n * continuous-mode restart loop, and its length is mostly the vendor quirks the\n * docstring warns about.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\n/** One reading of what was heard. */\nexport interface SpeechAlternativeLike {\n transcript: string;\n confidence?: number;\n}\n\n/** One recognised phrase, settled (`isFinal`) or still being revised. */\nexport interface SpeechResultLike {\n isFinal: boolean;\n length: number;\n [index: number]: SpeechAlternativeLike | undefined;\n}\n\n/** The growing list of phrases in a session. */\nexport interface SpeechResultListLike {\n length: number;\n [index: number]: SpeechResultLike | undefined;\n}\n\n/** The `result` event, reduced to what this hook reads. */\nexport interface SpeechRecognitionEventLike {\n /** Index of the first result that changed — everything before it is settled. */\n resultIndex: number;\n results: SpeechResultListLike;\n}\n\n/** The `error` event, reduced to what this hook reads. */\nexport interface SpeechRecognitionErrorEventLike {\n error: string;\n message?: string;\n}\n\n/**\n * The slice of the Web Speech `SpeechRecognition` object this SDK uses.\n *\n * Declared here rather than imported: TypeScript's DOM lib ships the *event* types but\n * not the constructor, because the API is still prefixed in Chromium and absent in\n * Firefox. Exported so a test — or a consumer wrapping a different engine — can hand in\n * something else.\n */\nexport interface SpeechRecognitionLike {\n lang: string;\n continuous: boolean;\n interimResults: boolean;\n maxAlternatives: number;\n start: () => void;\n stop: () => void;\n abort: () => void;\n onresult: ((event: SpeechRecognitionEventLike) => void) | null;\n onerror: ((event: SpeechRecognitionErrorEventLike) => void) | null;\n onend: (() => void) | null;\n onstart: (() => void) | null;\n}\n\n/** Classified reason recognition stopped or never started. */\nexport type SpeechErrorKind =\n | \"unsupported\"\n | \"not-allowed\"\n | \"no-speech\"\n | \"audio-capture\"\n | \"network\"\n | \"aborted\"\n | \"language-not-supported\"\n | \"unknown\";\n\n/** A classified speech error with a human-readable, English message. */\nexport interface SpeechError {\n kind: SpeechErrorKind;\n message: string;\n}\n\n/** Options for {@link useSpeechRecognition}. */\nexport interface UseSpeechRecognitionOptions {\n /** BCP-47 tag. Default `\"pt-BR\"`. */\n lang?: string;\n /**\n * Keep listening after the first phrase settles. Default `false`.\n *\n * Even with this on, the engine ends the session by itself after a stretch of\n * silence — that is a server-side timeout, not a bug — so watch `listening` rather\n * than assuming the microphone stays open.\n */\n continuous?: boolean;\n /** Publish the running guess as it changes. Default `true`. */\n interimResults?: boolean;\n /** How many readings per phrase to ask for. Default 1. */\n maxAlternatives?: number;\n /** Every update, settled or not. */\n onResult?: (result: { transcript: string; isFinal: boolean }) => void;\n /** Only the settled text of a phrase. The one to wire dictation to. */\n onFinal?: (transcript: string) => void;\n /** Classified failure. `no-speech` and `aborted` arrive here too — they are routine. */\n onError?: (error: SpeechError) => void;\n /** The session ended, for any reason. */\n onEnd?: () => void;\n /** Build the recogniser yourself — another engine, or a stub in a test. */\n factory?: () => SpeechRecognitionLike;\n}\n\n/** Value returned by {@link useSpeechRecognition}. */\nexport interface UseSpeechRecognitionResult {\n /** `false` in Firefox and in every browser that is not Chromium-based. */\n supported: boolean;\n /** Whether a session is open right now. */\n listening: boolean;\n /** Everything settled so far in this session. Cleared by `reset()`. */\n transcript: string;\n /** The running guess. Replaced on every event, empty once the phrase settles. */\n interim: string;\n /** Classified error, or `null`. */\n error: SpeechError | null;\n /** Open a session. No-op while already listening. */\n start: () => void;\n /** Close the session, keeping what was recognised. */\n stop: () => void;\n /** Close the session and throw the pending phrase away. */\n abort: () => void;\n /** Clear `transcript`, `interim` and `error`. Does not stop a session. */\n reset: () => void;\n}\n\n/** English messages for the `error` codes the spec defines. */\nconst MESSAGES: Record<SpeechErrorKind, string> = {\n unsupported: \"Speech recognition is not supported in this browser.\",\n \"not-allowed\": \"Microphone permission denied. Enable access in your browser settings.\",\n \"no-speech\": \"No speech was detected.\",\n \"audio-capture\": \"No microphone available on this device.\",\n network: \"The recognition service could not be reached.\",\n aborted: \"Recognition was cancelled.\",\n \"language-not-supported\": \"The recognition service does not support this language.\",\n unknown: \"Unexpected error during speech recognition.\",\n};\n\n/**\n * Map a spec `error` code to a kind an app can branch on.\n *\n * `service-not-allowed` collapses into `not-allowed` because the fix is the same from\n * the user's side, and `bad-grammar` into `unknown` because this hook never sets a\n * grammar, so seeing it means something outside our control went wrong.\n *\n * @param code - The `error` property of the event.\n * @returns The classified kind.\n */\nfunction classifySpeechError(code: string): SpeechErrorKind {\n switch (code) {\n case \"not-allowed\":\n case \"service-not-allowed\":\n return \"not-allowed\";\n case \"no-speech\":\n return \"no-speech\";\n case \"audio-capture\":\n return \"audio-capture\";\n case \"network\":\n return \"network\";\n case \"aborted\":\n return \"aborted\";\n case \"language-not-supported\":\n return \"language-not-supported\";\n default:\n return \"unknown\";\n }\n}\n\n/** The constructor, prefixed or not, or `null` where the API does not exist. */\nfunction speechRecognitionConstructor(): (new () => SpeechRecognitionLike) | null {\n const scope = globalThis as {\n SpeechRecognition?: unknown;\n webkitSpeechRecognition?: unknown;\n };\n const candidate = scope.SpeechRecognition ?? scope.webkitSpeechRecognition;\n return typeof candidate === \"function\" ? (candidate as new () => SpeechRecognitionLike) : null;\n}\n\n/** Whether this browser exposes the Web Speech recognition API at all. */\nexport function isSpeechRecognitionSupported(): boolean {\n return speechRecognitionConstructor() !== null;\n}\n\n/**\n * Dictate into your app with the Web Speech API — no dependency, no API key.\n *\n * ## Recognition is not local\n *\n * **Chromium streams the captured audio to a Google server to transcribe it.** Nothing\n * about the API says so, there is no setting that changes it, and it happens on every\n * `start()`. Anything the user says while a session is open leaves the device. Do not\n * put this on a field that takes clinical notes, credentials, or a client's financial\n * detail without telling them first — and if the data cannot leave your infrastructure,\n * this API is the wrong tool and a self-hosted model is the right one.\n *\n * ## What the states mean\n *\n * `transcript` accumulates the phrases the engine has **settled** on; `interim` is the\n * guess it is still revising and is replaced wholesale on every event, so rendering\n * `transcript + interim` gives the live caption effect and rendering `transcript` alone\n * gives the committed text. `no-speech` and `aborted` come through `onError` but are\n * routine — a user who pressed the button and said nothing is not a failure to report.\n *\n * There is deliberately **no auto-restart** when the engine ends a session on silence.\n * A restart loop is how an app ends up holding the microphone indefinitely — and, in\n * Chromium, streaming audio to a third party indefinitely. Show that listening stopped\n * and let the user press again.\n *\n * @param options - See {@link UseSpeechRecognitionOptions}.\n * @returns Session state, the transcript and the controls.\n *\n * @example\n * const speech = useSpeechRecognition({ onFinal: (text) => setPrompt(text) });\n * <button onClick={speech.listening ? speech.stop : speech.start}>\n * {speech.listening ? \"Parar\" : \"Ditar\"}\n * </button>\n * <p>{speech.transcript}{speech.interim}</p>\n */\nexport function useSpeechRecognition(\n options: UseSpeechRecognitionOptions = {},\n): UseSpeechRecognitionResult {\n const {\n lang = \"pt-BR\",\n continuous = false,\n interimResults = true,\n maxAlternatives = 1,\n onResult,\n onFinal,\n onError,\n onEnd,\n factory,\n } = options;\n\n const [supported] = useState(() => factory !== undefined || isSpeechRecognitionSupported());\n const [listening, setListening] = useState(false);\n const [transcript, setTranscript] = useState(\"\");\n const [interim, setInterim] = useState(\"\");\n const [error, setError] = useState<SpeechError | null>(null);\n\n const sessionRef = useRef<SpeechRecognitionLike | null>(null);\n\n const emitResult = useStableCallback((result: { transcript: string; isFinal: boolean }) =>\n onResult?.(result),\n );\n const emitFinal = useStableCallback((text: string) => onFinal?.(text));\n const emitEnd = useStableCallback(() => onEnd?.());\n const emitError = useStableCallback((failure: SpeechError) => {\n setError(failure);\n onError?.(failure);\n });\n\n const teardown = useCallback((): void => {\n const session = sessionRef.current;\n if (!session) return;\n session.onresult = null;\n session.onerror = null;\n session.onend = null;\n session.onstart = null;\n sessionRef.current = null;\n }, []);\n\n const start = useCallback((): void => {\n if (sessionRef.current) return;\n const build =\n factory ??\n (() => {\n const constructor = speechRecognitionConstructor();\n return constructor ? new constructor() : null;\n });\n const session = build();\n if (!session) {\n emitError({ kind: \"unsupported\", message: MESSAGES.unsupported });\n return;\n }\n\n session.lang = lang;\n session.continuous = continuous;\n session.interimResults = interimResults;\n session.maxAlternatives = maxAlternatives;\n\n /**\n * Fold one event into the two published strings.\n *\n * Only the results from `resultIndex` on are new; re-reading the whole list\n * would append phrases that are already in `transcript`. Settled text is\n * accumulated, unsettled text replaces the previous guess entirely — the engine\n * revises a phrase in place, so appending it would stutter the caption.\n */\n session.onresult = (event: SpeechRecognitionEventLike): void => {\n let settled = \"\";\n let pending = \"\";\n for (let index = event.resultIndex; index < event.results.length; index += 1) {\n const result = event.results[index];\n if (!result) continue;\n const text = result[0]?.transcript ?? \"\";\n if (result.isFinal) settled += text;\n else pending += text;\n }\n setInterim(pending);\n if (settled !== \"\") {\n setTranscript((previous) => previous + settled);\n emitFinal(settled);\n }\n emitResult({ transcript: settled !== \"\" ? settled : pending, isFinal: settled !== \"\" });\n };\n\n session.onerror = (event: SpeechRecognitionErrorEventLike): void => {\n const kind = classifySpeechError(event.error);\n emitError({ kind, message: event.message || MESSAGES[kind] });\n };\n\n session.onend = (): void => {\n setListening(false);\n setInterim(\"\");\n teardown();\n emitEnd();\n };\n\n session.onstart = (): void => setListening(true);\n\n setError(null);\n try {\n session.start();\n } catch (failure) {\n // Chromium throws `InvalidStateError` when a session is already running in\n // another component. Reporting it beats leaving a dead handle behind.\n emitError({\n kind: \"unknown\",\n message: failure instanceof Error ? failure.message : MESSAGES.unknown,\n });\n return;\n }\n sessionRef.current = session;\n setListening(true);\n }, [\n lang,\n continuous,\n interimResults,\n maxAlternatives,\n factory,\n emitResult,\n emitFinal,\n emitEnd,\n emitError,\n teardown,\n ]);\n\n const stop = useCallback((): void => {\n sessionRef.current?.stop();\n }, []);\n\n const abort = useCallback((): void => {\n sessionRef.current?.abort();\n }, []);\n\n const reset = useCallback((): void => {\n setTranscript(\"\");\n setInterim(\"\");\n setError(null);\n }, []);\n\n /**\n * Never leave a session open past unmount.\n *\n * `abort()` and not `stop()`: a component that is gone has nowhere to put a final\n * result, and `stop()` would keep the microphone — and the upstream connection —\n * alive while the engine finishes deciding what the last word was.\n */\n useEffect(\n () => () => {\n sessionRef.current?.abort();\n teardown();\n },\n [teardown],\n );\n\n return {\n supported,\n listening,\n transcript,\n interim,\n error,\n start,\n stop,\n abort,\n reset,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-torch.cjs","names":[],"sources":["../../src/capture/use-torch.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/**\n * `torch` is a real constrainable property in the Media Capture spec, but\n * TypeScript's `MediaTrackConstraintSet` does not list it, so the literal has to be\n * widened before `applyConstraints` will take it.\n */\ntype TorchConstraints = MediaTrackConstraints & {\n advanced?: Array<MediaTrackConstraintSet & { torch?: boolean }>;\n};\n\n/** `getCapabilities()` reports `torch` as a list of the values the lamp accepts. */\ntype TorchCapabilities = MediaTrackCapabilities & { torch?: readonly boolean[] };\n\n/** Value returned by {@link useTorch}. */\nexport interface UseTorchResult {\n /** Whether this track has a controllable lamp. `false` on every desktop webcam. */\n supported: boolean;\n /** Whether the lamp is on, as far as this hook knows. */\n on: boolean;\n /**\n * Turn the lamp on or off.\n *\n * @param next - `true` to light it up.\n * @returns `false` when the track refused — treat that as \"no torch\".\n */\n set: (next: boolean) => Promise<boolean>;\n /** Flip it. Same return contract as {@link UseTorchResult.set}. */\n toggle: () => Promise<boolean>;\n}\n\n/**\n * Drive the camera's LED torch on the video track of a stream.\n *\n * The lamp is not a device you can open — it is a **constraint on a live video\n * track**, so there is nothing to control until a camera stream exists, and it\n * disappears when that stream is released. That is also why `supported` can only be\n * answered after the track is live: the same code is `true` on an Android rear camera\n * and `false` on the front one of the same phone.\n *\n * Capability detection prefers `getCapabilities()` and falls back to\n * `getSettings()`, because Firefox implements neither the torch nor\n * `getCapabilities()` and Safari reports the setting without the capability. When\n * neither mentions `torch`, the hook reports `supported: false` instead of offering a\n * button that silently does nothing.\n *\n * @param stream - A live camera stream, or `null` before permission lands.\n * @returns Whether a lamp exists, its state, and setters.\n *\n * @example\n * const camera = useBarcodeScanner();\n * {camera.torch.supported && (\n * <button onClick={() => void camera.torch.toggle()}>Lanterna</button>\n * )}\n */\nexport function useTorch(stream: MediaStream | null): UseTorchResult {\n const [supported, setSupported] = useState(false);\n const [on, setOn] = useState(false);\n\n useEffect(() => {\n const track = stream?.getVideoTracks()[0] ?? null;\n if (!track) {\n setSupported(false);\n setOn(false);\n return;\n }\n const capabilities = (track.getCapabilities?.() ?? {}) as TorchCapabilities;\n const settings = track.getSettings();\n setSupported(capabilities.torch !== undefined || settings.torch !== undefined);\n setOn(settings.torch === true);\n }, [stream]);\n\n const set = useCallback(\n async (next: boolean): Promise<boolean> => {\n const track = stream?.getVideoTracks()[0] ?? null;\n if (!track) return false;\n try {\n await track.applyConstraints({\n advanced: [{ torch: next }],\n } as TorchConstraints);\n setOn(next);\n return true;\n } catch {\n // A track without a lamp rejects with `OverconstrainedError`. There is\n // nothing for the user to fix, and the honest report is \"no torch\".\n setSupported(false);\n setOn(false);\n return false;\n }\n },\n [stream],\n );\n\n const toggle = useCallback((): Promise<boolean> => set(!on), [set, on]);\n\n return { supported, on, set, toggle };\n}\n"],"mappings":"uBAuDA,SAAgB,EAAS,EAA4C,CACjE,GAAM,CAAC,EAAW,IAAA,
|
|
1
|
+
{"version":3,"file":"use-torch.cjs","names":[],"sources":["../../src/capture/use-torch.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/**\n * `torch` is a real constrainable property in the Media Capture spec, but\n * TypeScript's `MediaTrackConstraintSet` does not list it, so the literal has to be\n * widened before `applyConstraints` will take it.\n */\ntype TorchConstraints = MediaTrackConstraints & {\n advanced?: Array<MediaTrackConstraintSet & { torch?: boolean }>;\n};\n\n/** `getCapabilities()` reports `torch` as a list of the values the lamp accepts. */\ntype TorchCapabilities = MediaTrackCapabilities & { torch?: readonly boolean[] };\n\n/** Value returned by {@link useTorch}. */\nexport interface UseTorchResult {\n /** Whether this track has a controllable lamp. `false` on every desktop webcam. */\n supported: boolean;\n /** Whether the lamp is on, as far as this hook knows. */\n on: boolean;\n /**\n * Turn the lamp on or off.\n *\n * @param next - `true` to light it up.\n * @returns `false` when the track refused — treat that as \"no torch\".\n */\n set: (next: boolean) => Promise<boolean>;\n /** Flip it. Same return contract as {@link UseTorchResult.set}. */\n toggle: () => Promise<boolean>;\n}\n\n/**\n * Drive the camera's LED torch on the video track of a stream.\n *\n * The lamp is not a device you can open — it is a **constraint on a live video\n * track**, so there is nothing to control until a camera stream exists, and it\n * disappears when that stream is released. That is also why `supported` can only be\n * answered after the track is live: the same code is `true` on an Android rear camera\n * and `false` on the front one of the same phone.\n *\n * Capability detection prefers `getCapabilities()` and falls back to\n * `getSettings()`, because Firefox implements neither the torch nor\n * `getCapabilities()` and Safari reports the setting without the capability. When\n * neither mentions `torch`, the hook reports `supported: false` instead of offering a\n * button that silently does nothing.\n *\n * @param stream - A live camera stream, or `null` before permission lands.\n * @returns Whether a lamp exists, its state, and setters.\n *\n * @example\n * const camera = useBarcodeScanner();\n * {camera.torch.supported && (\n * <button onClick={() => void camera.torch.toggle()}>Lanterna</button>\n * )}\n */\nexport function useTorch(stream: MediaStream | null): UseTorchResult {\n const [supported, setSupported] = useState(false);\n const [on, setOn] = useState(false);\n\n useEffect(() => {\n const track = stream?.getVideoTracks()[0] ?? null;\n if (!track) {\n setSupported(false);\n setOn(false);\n return;\n }\n const capabilities = (track.getCapabilities?.() ?? {}) as TorchCapabilities;\n const settings = track.getSettings();\n setSupported(capabilities.torch !== undefined || settings.torch !== undefined);\n setOn(settings.torch === true);\n }, [stream]);\n\n const set = useCallback(\n async (next: boolean): Promise<boolean> => {\n const track = stream?.getVideoTracks()[0] ?? null;\n if (!track) return false;\n try {\n await track.applyConstraints({\n advanced: [{ torch: next }],\n } as TorchConstraints);\n setOn(next);\n return true;\n } catch {\n // A track without a lamp rejects with `OverconstrainedError`. There is\n // nothing for the user to fix, and the honest report is \"no torch\".\n setSupported(false);\n setOn(false);\n return false;\n }\n },\n [stream],\n );\n\n const toggle = useCallback((): Promise<boolean> => set(!on), [set, on]);\n\n return { supported, on, set, toggle };\n}\n"],"mappings":"uBAuDA,SAAgB,EAAS,EAA4C,CACjE,GAAM,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,CAAS,EAAK,EAC1C,CAAC,EAAI,IAAA,EAAS,EAAA,SAAA,CAAS,EAAK,GAElC,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAQ,GAAQ,eAAe,CAAC,CAAC,IAAM,KAC7C,GAAI,CAAC,EAAO,CACR,EAAa,EAAK,EAClB,EAAM,EAAK,EACX,MACJ,CACA,IAAM,EAAgB,EAAM,kBAAkB,GAAK,CAAC,EAC9C,EAAW,EAAM,YAAY,EACnC,EAAa,EAAa,QAAU,IAAA,IAAa,EAAS,QAAU,IAAA,EAAS,EAC7E,EAAM,EAAS,QAAU,EAAI,CACjC,EAAG,CAAC,CAAM,CAAC,EAEX,IAAM,GAAA,EAAM,EAAA,YAAA,CACR,KAAO,IAAoC,CACvC,IAAM,EAAQ,GAAQ,eAAe,CAAC,CAAC,IAAM,KAC7C,GAAI,CAAC,EAAO,MAAO,GACnB,GAAI,CAKA,OAJA,MAAM,EAAM,iBAAiB,CACzB,SAAU,CAAC,CAAE,MAAO,CAAK,CAAC,CAC9B,CAAqB,EACrB,EAAM,CAAI,EACH,EACX,MAAQ,CAKJ,OAFA,EAAa,EAAK,EAClB,EAAM,EAAK,EACJ,EACX,CACJ,EACA,CAAC,CAAM,CACX,EAIA,MAAO,CAAE,YAAW,KAAI,MAAK,QAAA,EAFd,EAAA,YAAA,KAAoC,EAAI,CAAC,CAAE,EAAG,CAAC,EAAK,CAAE,CAExC,CAAO,CACxC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-video-recorder.cjs","names":[],"sources":["../../src/capture/use-video-recorder.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the same lifecycle as the audio recorder —\n * permission, recorder, clock, auto-stop — over a stream that also carries video,\n * with the preview element to attach and release.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport type { MediaRecorderHandle, MediaRecorderStatus } from \"./media-recorder\";\nimport {\n createVideoRecorder,\n type VideoRecorderOptions,\n type VideoRecording,\n} from \"./video-recorder\";\n\n/** Options for {@link useVideoRecorder}. */\nexport interface UseVideoRecorderOptions extends VideoRecorderOptions {\n /**\n * Stop automatically after this many milliseconds.\n *\n * Worth setting on anything user-facing, and more so than for audio: a minute of\n * 1080p at 2.5 Mbps is roughly 19 MB, so a recording left running by accident\n * fills memory an order of magnitude faster than a voice note does.\n */\n maxDurationMs?: number;\n /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n onRecorded?: (recording: VideoRecording) => void;\n /** How often the clock is published, in ms. Default 250. */\n tickMs?: number;\n}\n\n/** Value returned by {@link useVideoRecorder}. */\nexport interface UseVideoRecorderResult {\n status: MediaRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: number;\n /** The finished recording, or `null` before the first `stop()`. */\n recording: VideoRecording | null;\n /** Recorder-level error (encoder failure, share revoked mid-recording). */\n error: unknown;\n /** `false` when there is no stream yet, or the browser cannot record video. */\n ready: boolean;\n start: () => void;\n pause: () => void;\n resume: () => void;\n /** Stop and resolve with the recording. Also published on `recording`. */\n stop: () => Promise<VideoRecording | null>;\n /** Stop and throw the video away. */\n cancel: () => void;\n}\n\n/**\n * Record a video stream — from {@link useScreenCapture}, from a camera, or from a\n * canvas.\n *\n * Pass the stream in; the hook stays `ready: false` until there is one, so a page can\n * render the whole recorder UI before the user has picked a screen and simply have it\n * disabled.\n *\n * There is no level meter here, which is the one real difference from\n * `useAudioRecorder`: metering a screen share means opening an `AudioContext` on a\n * stream that usually has no audio track at all, and browsers cap the number of live\n * contexts. If you are recording a camera **and** want a level, run\n * `createLevelMeter` on the same stream yourself.\n *\n * The clock is published every `tickMs` (default 250 ms) rather than per frame: a\n * video recording UI shows `0:07`, and a hook that re-rendered its parent 60 times a\n * second to move a one-second counter would be the most expensive thing on the page.\n *\n * @param stream - The stream to record, or `null` while the picker is open.\n * @param options - See {@link UseVideoRecorderOptions}.\n *\n * @example\n * const screen = useScreenCapture();\n * const rec = useVideoRecorder(screen.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar tela</button>\n */\nexport function useVideoRecorder(\n stream: MediaStream | null,\n options: UseVideoRecorderOptions = {},\n): UseVideoRecorderResult {\n const {\n maxDurationMs,\n onRecorded,\n onChunk,\n onError,\n mimeType,\n videoBitsPerSecond,\n audioBitsPerSecond,\n timesliceMs,\n tickMs = 250,\n } = options;\n\n const [status, setStatus] = useState<MediaRecorderStatus>(\"idle\");\n const [durationMs, setDurationMs] = useState(0);\n const [recording, setRecording] = useState<VideoRecording | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [ready, setReady] = useState(false);\n\n const recorderRef = useRef<MediaRecorderHandle | null>(null);\n\n const emitRecorded = useStableCallback((result: VideoRecording) => onRecorded?.(result));\n const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n const emitError = useStableCallback((err: unknown) => {\n setError(err);\n onError?.(err);\n });\n\n /**\n * Build the recorder for a stream, and tear it down when the stream changes.\n *\n * Rebuilt per stream because `MediaRecorder` is bound to the stream it was\n * constructed with: a user who stops sharing and picks a different window hands us\n * a new `MediaStream`, and reusing the old recorder would keep recording a surface\n * that no longer exists. Same reason for a container or bitrate change — both are\n * constructor arguments.\n */\n useEffect(() => {\n if (!stream) {\n setReady(false);\n return;\n }\n let recorder: MediaRecorderHandle;\n try {\n recorder = createVideoRecorder(stream, {\n mimeType,\n videoBitsPerSecond,\n audioBitsPerSecond,\n timesliceMs,\n onChunk: emitChunk,\n onError: emitError,\n });\n } catch (err) {\n setError(err);\n setReady(false);\n return;\n }\n recorderRef.current = recorder;\n setReady(true);\n setStatus(\"idle\");\n\n return () => {\n if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n recorder.cancel();\n }\n recorderRef.current = null;\n setReady(false);\n };\n }, [\n stream,\n mimeType,\n videoBitsPerSecond,\n audioBitsPerSecond,\n timesliceMs,\n emitChunk,\n emitError,\n ]);\n\n const stop = useCallback(async (): Promise<VideoRecording | null> => {\n const recorder = recorderRef.current;\n if (!recorder) return null;\n const current = recorder.status();\n if (current !== \"recording\" && current !== \"paused\") return null;\n const result = await recorder.stop();\n setStatus(\"stopped\");\n setDurationMs(result.durationMs);\n setRecording(result);\n emitRecorded(result);\n return result;\n }, [emitRecorded]);\n\n /**\n * Publish the clock while something is running, and enforce `maxDurationMs`.\n *\n * The cap is checked on the same tick rather than with a `setTimeout` armed at\n * `start()`, because a paused recording must not keep counting toward it — a\n * timeout would fire while the user is paused on a slide.\n */\n useEffect(() => {\n if (status !== \"recording\" && status !== \"paused\") return;\n const id = setInterval(() => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n const elapsed = recorder.durationMs();\n setDurationMs(elapsed);\n if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n }, tickMs);\n return () => clearInterval(id);\n }, [status, tickMs, maxDurationMs, stop]);\n\n const start = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n setRecording(null);\n setError(null);\n setDurationMs(0);\n recorder.start();\n setStatus(recorder.status());\n }, []);\n\n const pause = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.pause();\n setDurationMs(recorder.durationMs());\n setStatus(recorder.status());\n }, []);\n\n const resume = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.resume();\n setStatus(recorder.status());\n }, []);\n\n const cancel = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.cancel();\n setStatus(\"idle\");\n setDurationMs(0);\n setRecording(null);\n }, []);\n\n return { status, durationMs, recording, error, ready, start, pause, resume, stop, cancel };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-video-recorder.cjs","names":[],"sources":["../../src/capture/use-video-recorder.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the same lifecycle as the audio recorder —\n * permission, recorder, clock, auto-stop — over a stream that also carries video,\n * with the preview element to attach and release.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport type { MediaRecorderHandle, MediaRecorderStatus } from \"./media-recorder\";\nimport {\n createVideoRecorder,\n type VideoRecorderOptions,\n type VideoRecording,\n} from \"./video-recorder\";\n\n/** Options for {@link useVideoRecorder}. */\nexport interface UseVideoRecorderOptions extends VideoRecorderOptions {\n /**\n * Stop automatically after this many milliseconds.\n *\n * Worth setting on anything user-facing, and more so than for audio: a minute of\n * 1080p at 2.5 Mbps is roughly 19 MB, so a recording left running by accident\n * fills memory an order of magnitude faster than a voice note does.\n */\n maxDurationMs?: number;\n /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n onRecorded?: (recording: VideoRecording) => void;\n /** How often the clock is published, in ms. Default 250. */\n tickMs?: number;\n}\n\n/** Value returned by {@link useVideoRecorder}. */\nexport interface UseVideoRecorderResult {\n status: MediaRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: number;\n /** The finished recording, or `null` before the first `stop()`. */\n recording: VideoRecording | null;\n /** Recorder-level error (encoder failure, share revoked mid-recording). */\n error: unknown;\n /** `false` when there is no stream yet, or the browser cannot record video. */\n ready: boolean;\n start: () => void;\n pause: () => void;\n resume: () => void;\n /** Stop and resolve with the recording. Also published on `recording`. */\n stop: () => Promise<VideoRecording | null>;\n /** Stop and throw the video away. */\n cancel: () => void;\n}\n\n/**\n * Record a video stream — from {@link useScreenCapture}, from a camera, or from a\n * canvas.\n *\n * Pass the stream in; the hook stays `ready: false` until there is one, so a page can\n * render the whole recorder UI before the user has picked a screen and simply have it\n * disabled.\n *\n * There is no level meter here, which is the one real difference from\n * `useAudioRecorder`: metering a screen share means opening an `AudioContext` on a\n * stream that usually has no audio track at all, and browsers cap the number of live\n * contexts. If you are recording a camera **and** want a level, run\n * `createLevelMeter` on the same stream yourself.\n *\n * The clock is published every `tickMs` (default 250 ms) rather than per frame: a\n * video recording UI shows `0:07`, and a hook that re-rendered its parent 60 times a\n * second to move a one-second counter would be the most expensive thing on the page.\n *\n * @param stream - The stream to record, or `null` while the picker is open.\n * @param options - See {@link UseVideoRecorderOptions}.\n *\n * @example\n * const screen = useScreenCapture();\n * const rec = useVideoRecorder(screen.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar tela</button>\n */\nexport function useVideoRecorder(\n stream: MediaStream | null,\n options: UseVideoRecorderOptions = {},\n): UseVideoRecorderResult {\n const {\n maxDurationMs,\n onRecorded,\n onChunk,\n onError,\n mimeType,\n videoBitsPerSecond,\n audioBitsPerSecond,\n timesliceMs,\n tickMs = 250,\n } = options;\n\n const [status, setStatus] = useState<MediaRecorderStatus>(\"idle\");\n const [durationMs, setDurationMs] = useState(0);\n const [recording, setRecording] = useState<VideoRecording | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [ready, setReady] = useState(false);\n\n const recorderRef = useRef<MediaRecorderHandle | null>(null);\n\n const emitRecorded = useStableCallback((result: VideoRecording) => onRecorded?.(result));\n const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n const emitError = useStableCallback((err: unknown) => {\n setError(err);\n onError?.(err);\n });\n\n /**\n * Build the recorder for a stream, and tear it down when the stream changes.\n *\n * Rebuilt per stream because `MediaRecorder` is bound to the stream it was\n * constructed with: a user who stops sharing and picks a different window hands us\n * a new `MediaStream`, and reusing the old recorder would keep recording a surface\n * that no longer exists. Same reason for a container or bitrate change — both are\n * constructor arguments.\n */\n useEffect(() => {\n if (!stream) {\n setReady(false);\n return;\n }\n let recorder: MediaRecorderHandle;\n try {\n recorder = createVideoRecorder(stream, {\n mimeType,\n videoBitsPerSecond,\n audioBitsPerSecond,\n timesliceMs,\n onChunk: emitChunk,\n onError: emitError,\n });\n } catch (err) {\n setError(err);\n setReady(false);\n return;\n }\n recorderRef.current = recorder;\n setReady(true);\n setStatus(\"idle\");\n\n return () => {\n if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n recorder.cancel();\n }\n recorderRef.current = null;\n setReady(false);\n };\n }, [\n stream,\n mimeType,\n videoBitsPerSecond,\n audioBitsPerSecond,\n timesliceMs,\n emitChunk,\n emitError,\n ]);\n\n const stop = useCallback(async (): Promise<VideoRecording | null> => {\n const recorder = recorderRef.current;\n if (!recorder) return null;\n const current = recorder.status();\n if (current !== \"recording\" && current !== \"paused\") return null;\n const result = await recorder.stop();\n setStatus(\"stopped\");\n setDurationMs(result.durationMs);\n setRecording(result);\n emitRecorded(result);\n return result;\n }, [emitRecorded]);\n\n /**\n * Publish the clock while something is running, and enforce `maxDurationMs`.\n *\n * The cap is checked on the same tick rather than with a `setTimeout` armed at\n * `start()`, because a paused recording must not keep counting toward it — a\n * timeout would fire while the user is paused on a slide.\n */\n useEffect(() => {\n if (status !== \"recording\" && status !== \"paused\") return;\n const id = setInterval(() => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n const elapsed = recorder.durationMs();\n setDurationMs(elapsed);\n if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n }, tickMs);\n return () => clearInterval(id);\n }, [status, tickMs, maxDurationMs, stop]);\n\n const start = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n setRecording(null);\n setError(null);\n setDurationMs(0);\n recorder.start();\n setStatus(recorder.status());\n }, []);\n\n const pause = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.pause();\n setDurationMs(recorder.durationMs());\n setStatus(recorder.status());\n }, []);\n\n const resume = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.resume();\n setStatus(recorder.status());\n }, []);\n\n const cancel = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.cancel();\n setStatus(\"idle\");\n setDurationMs(0);\n setRecording(null);\n }, []);\n\n return { status, durationMs, recording, error, ready, start, pause, resume, stop, cancel };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AreaChart.cjs","names":[],"sources":["../../src/charts/AreaChart.tsx"],"sourcesContent":["import {\n Area,\n AreaChart as RAreaChart,\n CartesianGrid,\n Legend,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * AreaChart — themed wrapper over recharts `AreaChart`.\n *\n * Plots one filled area per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). When `stack` is set, all\n * areas share a stackId. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function AreaChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n stack = false,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const stackId = stack ? \"stack\" : undefined;\n\n const chart = (\n <RAreaChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => {\n const color = palette[i % palette.length];\n return (\n <Area\n key={category}\n type=\"monotone\"\n dataKey={category}\n stackId={stackId}\n stroke={color}\n fill={color}\n fillOpacity={0.3}\n />\n );\n })}\n </RAreaChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":"yJAuBA,SAAgB,EAAU,CACtB,OACA,QACA,aACA,SACA,SAAS,IACT,QACA,QAAQ,GACR,aAAa,GACb,WAAW,GACX,cAAc,GACd,iBACA,aACoB,CACpB,IAAM,EAAU,EAAA,eAAe,CAAM,EAC/B,EAAU,EAAQ,QAAU,IAAA,GAE5B,GACF,EAAA,EAAA,KAAA,CAAC,EAAA,UAAD,CAAkB,OAAa,QAAe,
|
|
1
|
+
{"version":3,"file":"AreaChart.cjs","names":[],"sources":["../../src/charts/AreaChart.tsx"],"sourcesContent":["import {\n Area,\n AreaChart as RAreaChart,\n CartesianGrid,\n Legend,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * AreaChart — themed wrapper over recharts `AreaChart`.\n *\n * Plots one filled area per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). When `stack` is set, all\n * areas share a stackId. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function AreaChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n stack = false,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const stackId = stack ? \"stack\" : undefined;\n\n const chart = (\n <RAreaChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => {\n const color = palette[i % palette.length];\n return (\n <Area\n key={category}\n type=\"monotone\"\n dataKey={category}\n stackId={stackId}\n stroke={color}\n fill={color}\n fillOpacity={0.3}\n />\n );\n })}\n </RAreaChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":"yJAuBA,SAAgB,EAAU,CACtB,OACA,QACA,aACA,SACA,SAAS,IACT,QACA,QAAQ,GACR,aAAa,GACb,WAAW,GACX,cAAc,GACd,iBACA,aACoB,CACpB,IAAM,EAAU,EAAA,eAAe,CAAM,EAC/B,EAAU,EAAQ,QAAU,IAAA,GAE5B,GACF,EAAA,EAAA,KAAA,CAAC,EAAA,UAAD,CAAkB,OAAa,QAAe,SAA9C,SAAA,CACK,GAAW,EAAA,EAAA,IAAA,CAAC,EAAA,cAAD,CAAe,gBAAgB,KAAO,CAAA,EAAI,MACtD,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAAO,QAAS,CAAQ,CAAA,GACxB,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAAO,cAAe,CAAiB,CAAA,EACtC,GAAc,EAAA,EAAA,IAAA,CAAC,EAAA,QAAD,CAAS,UAAW,EAAA,mBAAmB,CAAc,CAAI,CAAA,EAAI,KAC3E,GAAa,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CAAS,CAAA,EAAI,KAC1B,EAAW,KAAK,EAAU,IAAM,CAC7B,IAAM,EAAQ,EAAQ,EAAI,EAAQ,QAClC,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,KAAD,CAEI,KAAK,WACL,QAAS,EACA,UACT,OAAQ,EACR,KAAM,EACN,YAAa,EAChB,EAPQ,CAOR,CAET,CAAC,CACO,IAGhB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,CAAS,EACvB,SAAA,IAAU,IAAA,IAGP,EAAA,EAAA,IAAA,CAAC,EAAA,oBAAD,CAAqB,MAAM,OAAe,SACrC,SAAA,CACgB,CAAA,EAJrB,CAMH,CAAA,CAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AreaChart.js","names":[],"sources":["../../src/charts/AreaChart.tsx"],"sourcesContent":["import {\n Area,\n AreaChart as RAreaChart,\n CartesianGrid,\n Legend,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * AreaChart — themed wrapper over recharts `AreaChart`.\n *\n * Plots one filled area per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). When `stack` is set, all\n * areas share a stackId. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function AreaChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n stack = false,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const stackId = stack ? \"stack\" : undefined;\n\n const chart = (\n <RAreaChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => {\n const color = palette[i % palette.length];\n return (\n <Area\n key={category}\n type=\"monotone\"\n dataKey={category}\n stackId={stackId}\n stroke={color}\n fill={color}\n fillOpacity={0.3}\n />\n );\n })}\n </RAreaChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAuBA,SAAgB,EAAU,EACtB,SACA,UACA,eACA,WACA,YAAS,KACT,UACA,WAAQ,IACR,gBAAa,IACb,cAAW,IACX,iBAAc,IACd,mBACA,gBACoB;CACpB,IAAM,IAAU,EAAe,CAAM,GAC/B,IAAU,IAAQ,UAAU,KAAA,GAE5B,IACF,kBAAC,GAAD;EAAkB;EAAa;EAAe;
|
|
1
|
+
{"version":3,"file":"AreaChart.js","names":[],"sources":["../../src/charts/AreaChart.tsx"],"sourcesContent":["import {\n Area,\n AreaChart as RAreaChart,\n CartesianGrid,\n Legend,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * AreaChart — themed wrapper over recharts `AreaChart`.\n *\n * Plots one filled area per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). When `stack` is set, all\n * areas share a stackId. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function AreaChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n stack = false,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const stackId = stack ? \"stack\" : undefined;\n\n const chart = (\n <RAreaChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => {\n const color = palette[i % palette.length];\n return (\n <Area\n key={category}\n type=\"monotone\"\n dataKey={category}\n stackId={stackId}\n stroke={color}\n fill={color}\n fillOpacity={0.3}\n />\n );\n })}\n </RAreaChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAuBA,SAAgB,EAAU,EACtB,SACA,UACA,eACA,WACA,YAAS,KACT,UACA,WAAQ,IACR,gBAAa,IACb,cAAW,IACX,iBAAc,IACd,mBACA,gBACoB;CACpB,IAAM,IAAU,EAAe,CAAM,GAC/B,IAAU,IAAQ,UAAU,KAAA,GAE5B,IACF,kBAAC,GAAD;EAAkB;EAAa;EAAe;EAA9C,UAAA;GACK,IAAW,kBAAC,GAAD,EAAe,iBAAgB,MAAO,CAAA,IAAI;GACtD,kBAAC,GAAD,EAAO,SAAS,EAAQ,CAAA;GACxB,kBAAC,GAAD,EAAO,eAAe,EAAiB,CAAA;GACtC,IAAc,kBAAC,GAAD,EAAS,WAAW,EAAmB,CAAc,EAAI,CAAA,IAAI;GAC3E,IAAa,kBAAC,GAAD,CAAS,CAAA,IAAI;GAC1B,EAAW,KAAK,GAAU,MAAM;IAC7B,IAAM,IAAQ,EAAQ,IAAI,EAAQ;IAClC,OACI,kBAAC,GAAD;KAEI,MAAK;KACL,SAAS;KACA;KACT,QAAQ;KACR,MAAM;KACN,aAAa;IAChB,GAPQ,CAOR;GAET,CAAC;EACO;;CAGhB,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,CAAS;EACvB,UAAA,MAAU,KAAA,IAGP,kBAAC,GAAD;GAAqB,OAAM;GAAe;GACrC,UAAA;EACgB,CAAA,IAJrB;CAMH,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarChart.cjs","names":[],"sources":["../../src/charts/BarChart.tsx"],"sourcesContent":["import {\n Bar,\n BarChart as RBarChart,\n CartesianGrid,\n Legend,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * BarChart — themed wrapper over recharts `BarChart`.\n *\n * Plots one bar series per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). When `stack` is set, all\n * bars share a stackId. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function BarChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n stack = false,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const stackId = stack ? \"stack\" : undefined;\n\n const chart = (\n <RBarChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => (\n <Bar\n key={category}\n dataKey={category}\n stackId={stackId}\n fill={palette[i % palette.length]}\n />\n ))}\n </RBarChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":"yJAuBA,SAAgB,EAAS,CACrB,OACA,QACA,aACA,SACA,SAAS,IACT,QACA,QAAQ,GACR,aAAa,GACb,WAAW,GACX,cAAc,GACd,iBACA,aACoB,CACpB,IAAM,EAAU,EAAA,eAAe,CAAM,EAC/B,EAAU,EAAQ,QAAU,IAAA,GAE5B,GACF,EAAA,EAAA,KAAA,CAAC,EAAA,SAAD,CAAiB,OAAa,QAAe,
|
|
1
|
+
{"version":3,"file":"BarChart.cjs","names":[],"sources":["../../src/charts/BarChart.tsx"],"sourcesContent":["import {\n Bar,\n BarChart as RBarChart,\n CartesianGrid,\n Legend,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * BarChart — themed wrapper over recharts `BarChart`.\n *\n * Plots one bar series per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). When `stack` is set, all\n * bars share a stackId. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function BarChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n stack = false,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const stackId = stack ? \"stack\" : undefined;\n\n const chart = (\n <RBarChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => (\n <Bar\n key={category}\n dataKey={category}\n stackId={stackId}\n fill={palette[i % palette.length]}\n />\n ))}\n </RBarChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":"yJAuBA,SAAgB,EAAS,CACrB,OACA,QACA,aACA,SACA,SAAS,IACT,QACA,QAAQ,GACR,aAAa,GACb,WAAW,GACX,cAAc,GACd,iBACA,aACoB,CACpB,IAAM,EAAU,EAAA,eAAe,CAAM,EAC/B,EAAU,EAAQ,QAAU,IAAA,GAE5B,GACF,EAAA,EAAA,KAAA,CAAC,EAAA,SAAD,CAAiB,OAAa,QAAe,SAA7C,SAAA,CACK,GAAW,EAAA,EAAA,IAAA,CAAC,EAAA,cAAD,CAAe,gBAAgB,KAAO,CAAA,EAAI,MACtD,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAAO,QAAS,CAAQ,CAAA,GACxB,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAAO,cAAe,CAAiB,CAAA,EACtC,GAAc,EAAA,EAAA,IAAA,CAAC,EAAA,QAAD,CAAS,UAAW,EAAA,mBAAmB,CAAc,CAAI,CAAA,EAAI,KAC3E,GAAa,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CAAS,CAAA,EAAI,KAC1B,EAAW,KAAK,EAAU,KACvB,EAAA,EAAA,IAAA,CAAC,EAAA,IAAD,CAEI,QAAS,EACA,UACT,KAAM,EAAQ,EAAI,EAAQ,OAC7B,EAJQ,CAIR,CACJ,CACM,IAGf,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,CAAS,EACvB,SAAA,IAAU,IAAA,IAGP,EAAA,EAAA,IAAA,CAAC,EAAA,oBAAD,CAAqB,MAAM,OAAe,SACrC,SAAA,CACgB,CAAA,EAJrB,CAMH,CAAA,CAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarChart.js","names":[],"sources":["../../src/charts/BarChart.tsx"],"sourcesContent":["import {\n Bar,\n BarChart as RBarChart,\n CartesianGrid,\n Legend,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * BarChart — themed wrapper over recharts `BarChart`.\n *\n * Plots one bar series per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). When `stack` is set, all\n * bars share a stackId. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function BarChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n stack = false,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const stackId = stack ? \"stack\" : undefined;\n\n const chart = (\n <RBarChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => (\n <Bar\n key={category}\n dataKey={category}\n stackId={stackId}\n fill={palette[i % palette.length]}\n />\n ))}\n </RBarChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAuBA,SAAgB,EAAS,EACrB,SACA,UACA,eACA,WACA,YAAS,KACT,UACA,WAAQ,IACR,gBAAa,IACb,cAAW,IACX,iBAAc,IACd,mBACA,gBACoB;CACpB,IAAM,IAAU,EAAe,CAAM,GAC/B,IAAU,IAAQ,UAAU,KAAA,GAE5B,IACF,kBAAC,GAAD;EAAiB;EAAa;EAAe;
|
|
1
|
+
{"version":3,"file":"BarChart.js","names":[],"sources":["../../src/charts/BarChart.tsx"],"sourcesContent":["import {\n Bar,\n BarChart as RBarChart,\n CartesianGrid,\n Legend,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * BarChart — themed wrapper over recharts `BarChart`.\n *\n * Plots one bar series per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). When `stack` is set, all\n * bars share a stackId. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function BarChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n stack = false,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const stackId = stack ? \"stack\" : undefined;\n\n const chart = (\n <RBarChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => (\n <Bar\n key={category}\n dataKey={category}\n stackId={stackId}\n fill={palette[i % palette.length]}\n />\n ))}\n </RBarChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAuBA,SAAgB,EAAS,EACrB,SACA,UACA,eACA,WACA,YAAS,KACT,UACA,WAAQ,IACR,gBAAa,IACb,cAAW,IACX,iBAAc,IACd,mBACA,gBACoB;CACpB,IAAM,IAAU,EAAe,CAAM,GAC/B,IAAU,IAAQ,UAAU,KAAA,GAE5B,IACF,kBAAC,GAAD;EAAiB;EAAa;EAAe;EAA7C,UAAA;GACK,IAAW,kBAAC,GAAD,EAAe,iBAAgB,MAAO,CAAA,IAAI;GACtD,kBAAC,GAAD,EAAO,SAAS,EAAQ,CAAA;GACxB,kBAAC,GAAD,EAAO,eAAe,EAAiB,CAAA;GACtC,IAAc,kBAAC,GAAD,EAAS,WAAW,EAAmB,CAAc,EAAI,CAAA,IAAI;GAC3E,IAAa,kBAAC,GAAD,CAAS,CAAA,IAAI;GAC1B,EAAW,KAAK,GAAU,MACvB,kBAAC,GAAD;IAEI,SAAS;IACA;IACT,MAAM,EAAQ,IAAI,EAAQ;GAC7B,GAJQ,CAIR,CACJ;EACM;;CAGf,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,CAAS;EACvB,UAAA,MAAU,KAAA,IAGP,kBAAC,GAAD;GAAqB,OAAM;GAAe;GACrC,UAAA;EACgB,CAAA,IAJrB;CAMH,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LineChart.cjs","names":[],"sources":["../../src/charts/LineChart.tsx"],"sourcesContent":["import {\n CartesianGrid,\n Legend,\n Line,\n LineChart as RLineChart,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * LineChart — themed wrapper over recharts `LineChart`.\n *\n * Plots one line per entry in `categories`, colored from `colors` (cycling\n * the `--tempest-chart-*` theme tokens by default). `stack` is accepted for API parity\n * but does not visually stack lines. When `width` is provided the chart renders\n * at that fixed size without a ResponsiveContainer; otherwise it fills its\n * parent.\n */\nexport function LineChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RLineChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => (\n <Line\n key={category}\n type=\"monotone\"\n dataKey={category}\n stroke={palette[i % palette.length]}\n dot={false}\n />\n ))}\n </RLineChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":"yJAwBA,SAAgB,EAAU,CACtB,OACA,QACA,aACA,SACA,SAAS,IACT,QACA,aAAa,GACb,WAAW,GACX,cAAc,GACd,iBACA,aACoB,CACpB,IAAM,EAAU,EAAA,eAAe,CAAM,EAC/B,GACF,EAAA,EAAA,KAAA,CAAC,EAAA,UAAD,CAAkB,OAAa,QAAe,
|
|
1
|
+
{"version":3,"file":"LineChart.cjs","names":[],"sources":["../../src/charts/LineChart.tsx"],"sourcesContent":["import {\n CartesianGrid,\n Legend,\n Line,\n LineChart as RLineChart,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * LineChart — themed wrapper over recharts `LineChart`.\n *\n * Plots one line per entry in `categories`, colored from `colors` (cycling\n * the `--tempest-chart-*` theme tokens by default). `stack` is accepted for API parity\n * but does not visually stack lines. When `width` is provided the chart renders\n * at that fixed size without a ResponsiveContainer; otherwise it fills its\n * parent.\n */\nexport function LineChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RLineChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => (\n <Line\n key={category}\n type=\"monotone\"\n dataKey={category}\n stroke={palette[i % palette.length]}\n dot={false}\n />\n ))}\n </RLineChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":"yJAwBA,SAAgB,EAAU,CACtB,OACA,QACA,aACA,SACA,SAAS,IACT,QACA,aAAa,GACb,WAAW,GACX,cAAc,GACd,iBACA,aACoB,CACpB,IAAM,EAAU,EAAA,eAAe,CAAM,EAC/B,GACF,EAAA,EAAA,KAAA,CAAC,EAAA,UAAD,CAAkB,OAAa,QAAe,SAA9C,SAAA,CACK,GAAW,EAAA,EAAA,IAAA,CAAC,EAAA,cAAD,CAAe,gBAAgB,KAAO,CAAA,EAAI,MACtD,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAAO,QAAS,CAAQ,CAAA,GACxB,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAAO,cAAe,CAAiB,CAAA,EACtC,GAAc,EAAA,EAAA,IAAA,CAAC,EAAA,QAAD,CAAS,UAAW,EAAA,mBAAmB,CAAc,CAAI,CAAA,EAAI,KAC3E,GAAa,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CAAS,CAAA,EAAI,KAC1B,EAAW,KAAK,EAAU,KACvB,EAAA,EAAA,IAAA,CAAC,EAAA,KAAD,CAEI,KAAK,WACL,QAAS,EACT,OAAQ,EAAQ,EAAI,EAAQ,QAC5B,IAAK,EACR,EALQ,CAKR,CACJ,CACO,IAGhB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,CAAS,EACvB,SAAA,IAAU,IAAA,IAGP,EAAA,EAAA,IAAA,CAAC,EAAA,oBAAD,CAAqB,MAAM,OAAe,SACrC,SAAA,CACgB,CAAA,EAJrB,CAMH,CAAA,CAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LineChart.js","names":[],"sources":["../../src/charts/LineChart.tsx"],"sourcesContent":["import {\n CartesianGrid,\n Legend,\n Line,\n LineChart as RLineChart,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * LineChart — themed wrapper over recharts `LineChart`.\n *\n * Plots one line per entry in `categories`, colored from `colors` (cycling\n * the `--tempest-chart-*` theme tokens by default). `stack` is accepted for API parity\n * but does not visually stack lines. When `width` is provided the chart renders\n * at that fixed size without a ResponsiveContainer; otherwise it fills its\n * parent.\n */\nexport function LineChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RLineChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => (\n <Line\n key={category}\n type=\"monotone\"\n dataKey={category}\n stroke={palette[i % palette.length]}\n dot={false}\n />\n ))}\n </RLineChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAwBA,SAAgB,EAAU,EACtB,SACA,UACA,eACA,WACA,YAAS,KACT,UACA,gBAAa,IACb,cAAW,IACX,iBAAc,IACd,mBACA,gBACoB;CACpB,IAAM,IAAU,EAAe,CAAM,GAC/B,IACF,kBAAC,GAAD;EAAkB;EAAa;EAAe;
|
|
1
|
+
{"version":3,"file":"LineChart.js","names":[],"sources":["../../src/charts/LineChart.tsx"],"sourcesContent":["import {\n CartesianGrid,\n Legend,\n Line,\n LineChart as RLineChart,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * LineChart — themed wrapper over recharts `LineChart`.\n *\n * Plots one line per entry in `categories`, colored from `colors` (cycling\n * the `--tempest-chart-*` theme tokens by default). `stack` is accepted for API parity\n * but does not visually stack lines. When `width` is provided the chart renders\n * at that fixed size without a ResponsiveContainer; otherwise it fills its\n * parent.\n */\nexport function LineChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n showLegend = true,\n showGrid = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RLineChart data={data} width={width} height={height}>\n {showGrid ? <CartesianGrid strokeDasharray=\"3 3\" /> : null}\n <XAxis dataKey={index} />\n <YAxis tickFormatter={valueFormatter} />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => (\n <Line\n key={category}\n type=\"monotone\"\n dataKey={category}\n stroke={palette[i % palette.length]}\n dot={false}\n />\n ))}\n </RLineChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAwBA,SAAgB,EAAU,EACtB,SACA,UACA,eACA,WACA,YAAS,KACT,UACA,gBAAa,IACb,cAAW,IACX,iBAAc,IACd,mBACA,gBACoB;CACpB,IAAM,IAAU,EAAe,CAAM,GAC/B,IACF,kBAAC,GAAD;EAAkB;EAAa;EAAe;EAA9C,UAAA;GACK,IAAW,kBAAC,GAAD,EAAe,iBAAgB,MAAO,CAAA,IAAI;GACtD,kBAAC,GAAD,EAAO,SAAS,EAAQ,CAAA;GACxB,kBAAC,GAAD,EAAO,eAAe,EAAiB,CAAA;GACtC,IAAc,kBAAC,GAAD,EAAS,WAAW,EAAmB,CAAc,EAAI,CAAA,IAAI;GAC3E,IAAa,kBAAC,GAAD,CAAS,CAAA,IAAI;GAC1B,EAAW,KAAK,GAAU,MACvB,kBAAC,GAAD;IAEI,MAAK;IACL,SAAS;IACT,QAAQ,EAAQ,IAAI,EAAQ;IAC5B,KAAK;GACR,GALQ,CAKR,CACJ;EACO;;CAGhB,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,CAAS;EACvB,UAAA,MAAU,KAAA,IAGP,kBAAC,GAAD;GAAqB,OAAM;GAAe;GACrC,UAAA;EACgB,CAAA,IAJrB;CAMH,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PieChart.cjs","names":[],"sources":["../../src/charts/PieChart.tsx"],"sourcesContent":["import { Cell, Legend, Pie, PieChart as RPieChart, ResponsiveContainer, Tooltip } from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { PieChartProps } from \"./types\";\n\n/**\n * PieChart — themed wrapper over recharts `PieChart`.\n *\n * Renders one slice per row, reading the numeric value from `category` and the\n * label from `index`. Each slice is colored from `colors` (cycling\n * the `--tempest-chart-*` theme tokens by default). When `donut` is set the pie gets a\n * non-zero inner radius. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function PieChart({\n data,\n category,\n index,\n colors,\n height = 300,\n width,\n donut = false,\n showLegend = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: PieChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RPieChart width={width} height={height}>\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n <Pie\n data={data}\n dataKey={category}\n nameKey={index}\n innerRadius={donut ? \"60%\" : 0}\n outerRadius=\"80%\"\n >\n {data.map((row, i) => (\n <Cell key={`${String(row[index])}-${i}`} fill={palette[i % palette.length]} />\n ))}\n </Pie>\n </RPieChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":"yJAeA,SAAgB,EAAS,CACrB,OACA,WACA,QACA,SACA,SAAS,IACT,QACA,QAAQ,GACR,aAAa,GACb,cAAc,GACd,iBACA,aACc,CACd,IAAM,EAAU,EAAA,eAAe,CAAM,EAC/B,GACF,EAAA,EAAA,KAAA,CAAC,EAAA,SAAD,CAAkB,QAAe,
|
|
1
|
+
{"version":3,"file":"PieChart.cjs","names":[],"sources":["../../src/charts/PieChart.tsx"],"sourcesContent":["import { Cell, Legend, Pie, PieChart as RPieChart, ResponsiveContainer, Tooltip } from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { PieChartProps } from \"./types\";\n\n/**\n * PieChart — themed wrapper over recharts `PieChart`.\n *\n * Renders one slice per row, reading the numeric value from `category` and the\n * label from `index`. Each slice is colored from `colors` (cycling\n * the `--tempest-chart-*` theme tokens by default). When `donut` is set the pie gets a\n * non-zero inner radius. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function PieChart({\n data,\n category,\n index,\n colors,\n height = 300,\n width,\n donut = false,\n showLegend = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: PieChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RPieChart width={width} height={height}>\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n <Pie\n data={data}\n dataKey={category}\n nameKey={index}\n innerRadius={donut ? \"60%\" : 0}\n outerRadius=\"80%\"\n >\n {data.map((row, i) => (\n <Cell key={`${String(row[index])}-${i}`} fill={palette[i % palette.length]} />\n ))}\n </Pie>\n </RPieChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":"yJAeA,SAAgB,EAAS,CACrB,OACA,WACA,QACA,SACA,SAAS,IACT,QACA,QAAQ,GACR,aAAa,GACb,cAAc,GACd,iBACA,aACc,CACd,IAAM,EAAU,EAAA,eAAe,CAAM,EAC/B,GACF,EAAA,EAAA,KAAA,CAAC,EAAA,SAAD,CAAkB,QAAe,SAAjC,SAAA,CACK,GAAc,EAAA,EAAA,IAAA,CAAC,EAAA,QAAD,CAAS,UAAW,EAAA,mBAAmB,CAAc,CAAI,CAAA,EAAI,KAC3E,GAAa,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CAAS,CAAA,EAAI,MAC3B,EAAA,EAAA,IAAA,CAAC,EAAA,IAAD,CACU,OACN,QAAS,EACT,QAAS,EACT,YAAa,EAAQ,MAAQ,EAC7B,YAAY,MAEX,SAAA,EAAK,KAAK,EAAK,KACZ,EAAA,EAAA,IAAA,CAAC,EAAA,KAAD,CAAyC,KAAM,EAAQ,EAAI,EAAQ,OAAU,EAAlE,GAAG,OAAO,EAAI,EAAM,EAAE,GAAG,GAAyC,CAChF,CACA,CAAA,CACE,IAGf,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,CAAS,EACvB,SAAA,IAAU,IAAA,IAGP,EAAA,EAAA,IAAA,CAAC,EAAA,oBAAD,CAAqB,MAAM,OAAe,SACrC,SAAA,CACgB,CAAA,EAJrB,CAMH,CAAA,CAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PieChart.js","names":[],"sources":["../../src/charts/PieChart.tsx"],"sourcesContent":["import { Cell, Legend, Pie, PieChart as RPieChart, ResponsiveContainer, Tooltip } from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { PieChartProps } from \"./types\";\n\n/**\n * PieChart — themed wrapper over recharts `PieChart`.\n *\n * Renders one slice per row, reading the numeric value from `category` and the\n * label from `index`. Each slice is colored from `colors` (cycling\n * the `--tempest-chart-*` theme tokens by default). When `donut` is set the pie gets a\n * non-zero inner radius. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function PieChart({\n data,\n category,\n index,\n colors,\n height = 300,\n width,\n donut = false,\n showLegend = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: PieChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RPieChart width={width} height={height}>\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n <Pie\n data={data}\n dataKey={category}\n nameKey={index}\n innerRadius={donut ? \"60%\" : 0}\n outerRadius=\"80%\"\n >\n {data.map((row, i) => (\n <Cell key={`${String(row[index])}-${i}`} fill={palette[i % palette.length]} />\n ))}\n </Pie>\n </RPieChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAeA,SAAgB,EAAS,EACrB,SACA,aACA,UACA,WACA,YAAS,KACT,UACA,WAAQ,IACR,gBAAa,IACb,iBAAc,IACd,mBACA,gBACc;CACd,IAAM,IAAU,EAAe,CAAM,GAC/B,IACF,kBAAC,GAAD;EAAkB;EAAe;
|
|
1
|
+
{"version":3,"file":"PieChart.js","names":[],"sources":["../../src/charts/PieChart.tsx"],"sourcesContent":["import { Cell, Legend, Pie, PieChart as RPieChart, ResponsiveContainer, Tooltip } from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { PieChartProps } from \"./types\";\n\n/**\n * PieChart — themed wrapper over recharts `PieChart`.\n *\n * Renders one slice per row, reading the numeric value from `category` and the\n * label from `index`. Each slice is colored from `colors` (cycling\n * the `--tempest-chart-*` theme tokens by default). When `donut` is set the pie gets a\n * non-zero inner radius. When `width` is provided the chart renders at that\n * fixed size without a ResponsiveContainer; otherwise it fills its parent.\n */\nexport function PieChart({\n data,\n category,\n index,\n colors,\n height = 300,\n width,\n donut = false,\n showLegend = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: PieChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RPieChart width={width} height={height}>\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n <Pie\n data={data}\n dataKey={category}\n nameKey={index}\n innerRadius={donut ? \"60%\" : 0}\n outerRadius=\"80%\"\n >\n {data.map((row, i) => (\n <Cell key={`${String(row[index])}-${i}`} fill={palette[i % palette.length]} />\n ))}\n </Pie>\n </RPieChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAeA,SAAgB,EAAS,EACrB,SACA,aACA,UACA,WACA,YAAS,KACT,UACA,WAAQ,IACR,gBAAa,IACb,iBAAc,IACd,mBACA,gBACc;CACd,IAAM,IAAU,EAAe,CAAM,GAC/B,IACF,kBAAC,GAAD;EAAkB;EAAe;EAAjC,UAAA;GACK,IAAc,kBAAC,GAAD,EAAS,WAAW,EAAmB,CAAc,EAAI,CAAA,IAAI;GAC3E,IAAa,kBAAC,GAAD,CAAS,CAAA,IAAI;GAC3B,kBAAC,GAAD;IACU;IACN,SAAS;IACT,SAAS;IACT,aAAa,IAAQ,QAAQ;IAC7B,aAAY;IAEX,UAAA,EAAK,KAAK,GAAK,MACZ,kBAAC,GAAD,EAAyC,MAAM,EAAQ,IAAI,EAAQ,QAAU,GAAlE,GAAG,OAAO,EAAI,EAAM,EAAE,GAAG,GAAyC,CAChF;GACA,CAAA;EACE;;CAGf,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,CAAS;EACvB,UAAA,MAAU,KAAA,IAGP,kBAAC,GAAD;GAAqB,OAAM;GAAe;GACrC,UAAA;EACgB,CAAA,IAJrB;CAMH,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RadarChart.cjs","names":[],"sources":["../../src/charts/RadarChart.tsx"],"sourcesContent":["import {\n Legend,\n PolarAngleAxis,\n PolarGrid,\n PolarRadiusAxis,\n RadarChart as RRadarChart,\n Radar,\n ResponsiveContainer,\n Tooltip,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * RadarChart — themed wrapper over recharts `RadarChart`.\n *\n * Plots one radar polygon per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). `index` drives the angle\n * axis. When `width` is provided the chart renders at that fixed size without a\n * ResponsiveContainer; otherwise it fills its parent.\n */\nexport function RadarChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n showLegend = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RRadarChart data={data} width={width} height={height}>\n <PolarGrid />\n <PolarAngleAxis dataKey={index} />\n <PolarRadiusAxis />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => {\n const color = palette[i % palette.length];\n return (\n <Radar\n key={category}\n dataKey={category}\n stroke={color}\n fill={color}\n fillOpacity={0.3}\n />\n );\n })}\n </RRadarChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":"yJAuBA,SAAgB,EAAW,CACvB,OACA,QACA,aACA,SACA,SAAS,IACT,QACA,aAAa,GACb,cAAc,GACd,iBACA,aACoB,CACpB,IAAM,EAAU,EAAA,eAAe,CAAM,EAC/B,GACF,EAAA,EAAA,KAAA,CAAC,EAAA,WAAD,CAAmB,OAAa,QAAe,
|
|
1
|
+
{"version":3,"file":"RadarChart.cjs","names":[],"sources":["../../src/charts/RadarChart.tsx"],"sourcesContent":["import {\n Legend,\n PolarAngleAxis,\n PolarGrid,\n PolarRadiusAxis,\n RadarChart as RRadarChart,\n Radar,\n ResponsiveContainer,\n Tooltip,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * RadarChart — themed wrapper over recharts `RadarChart`.\n *\n * Plots one radar polygon per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). `index` drives the angle\n * axis. When `width` is provided the chart renders at that fixed size without a\n * ResponsiveContainer; otherwise it fills its parent.\n */\nexport function RadarChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n showLegend = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RRadarChart data={data} width={width} height={height}>\n <PolarGrid />\n <PolarAngleAxis dataKey={index} />\n <PolarRadiusAxis />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => {\n const color = palette[i % palette.length];\n return (\n <Radar\n key={category}\n dataKey={category}\n stroke={color}\n fill={color}\n fillOpacity={0.3}\n />\n );\n })}\n </RRadarChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":"yJAuBA,SAAgB,EAAW,CACvB,OACA,QACA,aACA,SACA,SAAS,IACT,QACA,aAAa,GACb,cAAc,GACd,iBACA,aACoB,CACpB,IAAM,EAAU,EAAA,eAAe,CAAM,EAC/B,GACF,EAAA,EAAA,KAAA,CAAC,EAAA,WAAD,CAAmB,OAAa,QAAe,SAA/C,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,UAAD,CAAY,CAAA,GACZ,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAgB,QAAS,CAAQ,CAAA,GACjC,EAAA,EAAA,IAAA,CAAC,EAAA,gBAAD,CAAkB,CAAA,EACjB,GAAc,EAAA,EAAA,IAAA,CAAC,EAAA,QAAD,CAAS,UAAW,EAAA,mBAAmB,CAAc,CAAI,CAAA,EAAI,KAC3E,GAAa,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CAAS,CAAA,EAAI,KAC1B,EAAW,KAAK,EAAU,IAAM,CAC7B,IAAM,EAAQ,EAAQ,EAAI,EAAQ,QAClC,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAEI,QAAS,EACT,OAAQ,EACR,KAAM,EACN,YAAa,EAChB,EALQ,CAKR,CAET,CAAC,CACQ,IAGjB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,CAAS,EACvB,SAAA,IAAU,IAAA,IAGP,EAAA,EAAA,IAAA,CAAC,EAAA,oBAAD,CAAqB,MAAM,OAAe,SACrC,SAAA,CACgB,CAAA,EAJrB,CAMH,CAAA,CAEb"}
|