tempest-react-sdk 0.39.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/passkey.cjs.map +1 -1
- package/dist/auth/passkey.js.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 +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/masked-inputs.js +1 -1
- package/dist/forms/masked-inputs.js.map +1 -1
- package/dist/forms/use-viacep.cjs.map +1 -1
- package/dist/forms/use-zod-form.cjs.map +1 -1
- package/dist/forms/zod-resolver.cjs.map +1 -1
- package/dist/forms/zod-resolver.js.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/tempest-react-sdk.d.ts +25 -26
- 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/exceptions.cjs.map +1 -1
- package/dist/vision/core/exceptions.js.map +1 -1
- package/dist/vision/core/metadata.cjs +1 -1
- package/dist/vision/core/metadata.cjs.map +1 -1
- package/dist/vision/core/metadata.js +1 -1
- package/dist/vision/core/metadata.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/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/tasks/base.cjs +1 -1
- package/dist/vision/tasks/base.cjs.map +1 -1
- package/dist/vision/tasks/base.js +3 -1
- 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 +1 -1
- package/dist/vision/tasks/detectClassify.cjs.map +1 -1
- package/dist/vision/tasks/detectClassify.js +16 -15
- package/dist/vision/tasks/detectClassify.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/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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioPlayer.cjs","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — src accepts a URL or a Blob, and\n * everything else is a facet of playing it: durationMs (because MediaRecorder WebM\n * carries none), sinkId (output device), autoPlay/loop, onEnded/onError, plus the\n * actions slot and locale for the labels. The body is the transport state machine —\n * object-URL lifecycle, seek, and the rAF-free time updates — which shares one audio\n * element ref throughout.\n */\nimport { Pause, Play } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\n\nimport { setAudioOutput } from \"@/audio/audio-output\";\nimport { formatDuration } from \"@/audio/duration\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./AudioPlayer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface AudioPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a page that records twenty notes does not leak twenty\n * URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useAudioRecorder` always does.\n * See the note on the seek bar for why the element's own `duration` is not\n * trustworthy for a fresh recording.\n */\n durationMs?: number;\n /** Output device, from `useMediaDevices().audioOutputs`. Chromium only. */\n sinkId?: string;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the right of the times — a download button, a delete button. */\n actions?: React.ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": { play: \"Tocar\", pause: \"Pausar\", seek: \"Posição\" },\n en: { play: \"Play\", pause: \"Pause\", seek: \"Seek\" },\n} as const;\n\n/**\n * Playback transport for one clip: play/pause, a seek bar, elapsed and total time.\n *\n * Built around a real `<audio>` element rather than the SDK's `createAudioPlayer`,\n * which is a fire-and-forget handle for notification chimes and has no transport to\n * expose. Accepts a `Blob` directly, because the thing an app most often plays is the\n * recording it just made.\n *\n * The seek bar is a bare `<input type=\"range\">` rather than the SDK's `Slider`: that\n * component is a form field, with a label row and a value badge, and a transport wants\n * neither. The native input keeps the keyboard and screen-reader behaviour that\n * matters here for free.\n *\n * @example\n * const rec = useAudioRecorder(mic.stream);\n * {rec.recording && (\n * <AudioPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function AudioPlayer({\n src,\n durationMs,\n sinkId,\n autoPlay = false,\n loop = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioPlayerProps) {\n const strings = STRINGS[locale];\n const audio = useRef<HTMLAudioElement | null>(null);\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const probed = useRef(false);\n\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n /**\n * Total length, preferring what the caller knows.\n *\n * `MediaRecorder` writes WebM with no duration in the header, so a fresh recording\n * reports `Infinity` — which is why the recorder keeps its own clock and passes it\n * here. `durationMs` wins whenever it is finite; the element's value is the\n * fallback for a plain URL.\n */\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n useEffect(() => {\n if (sinkId === undefined) return;\n void setAudioOutput(audio.current, sinkId);\n }, [sinkId, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after which\n * it knows the real length. It is a hack with no alternative — the header simply\n * does not carry the value — and it runs at most once per source, only when the\n * caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = audio.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onSeeked = (): void => {\n node.removeEventListener(\"timeupdate\", onSeeked);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onSeeked);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = audio.current;\n if (!node || !url) return;\n if (node.paused) {\n void node\n .play()\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = audio.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n return (\n <div className={cn(styles.player, className)} {...rest}>\n <audio\n ref={audio}\n src={url ?? undefined}\n loop={loop}\n autoPlay={autoPlay}\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = audio.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onEnded={() => {\n setPlaying(false);\n setCurrentMs(0);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n />\n\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AudioPlayer.cjs","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — src accepts a URL or a Blob, and\n * everything else is a facet of playing it: durationMs (because MediaRecorder WebM\n * carries none), sinkId (output device), autoPlay/loop, onEnded/onError, plus the\n * actions slot and locale for the labels. The body is the transport state machine —\n * object-URL lifecycle, seek, and the rAF-free time updates — which shares one audio\n * element ref throughout.\n */\nimport { Pause, Play } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\n\nimport { setAudioOutput } from \"@/audio/audio-output\";\nimport { formatDuration } from \"@/audio/duration\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./AudioPlayer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface AudioPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a page that records twenty notes does not leak twenty\n * URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useAudioRecorder` always does.\n * See the note on the seek bar for why the element's own `duration` is not\n * trustworthy for a fresh recording.\n */\n durationMs?: number;\n /** Output device, from `useMediaDevices().audioOutputs`. Chromium only. */\n sinkId?: string;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the right of the times — a download button, a delete button. */\n actions?: React.ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": { play: \"Tocar\", pause: \"Pausar\", seek: \"Posição\" },\n en: { play: \"Play\", pause: \"Pause\", seek: \"Seek\" },\n} as const;\n\n/**\n * Playback transport for one clip: play/pause, a seek bar, elapsed and total time.\n *\n * Built around a real `<audio>` element rather than the SDK's `createAudioPlayer`,\n * which is a fire-and-forget handle for notification chimes and has no transport to\n * expose. Accepts a `Blob` directly, because the thing an app most often plays is the\n * recording it just made.\n *\n * The seek bar is a bare `<input type=\"range\">` rather than the SDK's `Slider`: that\n * component is a form field, with a label row and a value badge, and a transport wants\n * neither. The native input keeps the keyboard and screen-reader behaviour that\n * matters here for free.\n *\n * @example\n * const rec = useAudioRecorder(mic.stream);\n * {rec.recording && (\n * <AudioPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function AudioPlayer({\n src,\n durationMs,\n sinkId,\n autoPlay = false,\n loop = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioPlayerProps) {\n const strings = STRINGS[locale];\n const audio = useRef<HTMLAudioElement | null>(null);\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const probed = useRef(false);\n\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n /**\n * Total length, preferring what the caller knows.\n *\n * `MediaRecorder` writes WebM with no duration in the header, so a fresh recording\n * reports `Infinity` — which is why the recorder keeps its own clock and passes it\n * here. `durationMs` wins whenever it is finite; the element's value is the\n * fallback for a plain URL.\n */\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n useEffect(() => {\n if (sinkId === undefined) return;\n void setAudioOutput(audio.current, sinkId);\n }, [sinkId, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after which\n * it knows the real length. It is a hack with no alternative — the header simply\n * does not carry the value — and it runs at most once per source, only when the\n * caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = audio.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onSeeked = (): void => {\n node.removeEventListener(\"timeupdate\", onSeeked);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onSeeked);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = audio.current;\n if (!node || !url) return;\n if (node.paused) {\n void node\n .play()\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = audio.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n return (\n <div className={cn(styles.player, className)} {...rest}>\n <audio\n ref={audio}\n src={url ?? undefined}\n loop={loop}\n autoPlay={autoPlay}\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = audio.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onEnded={() => {\n setPlaying(false);\n setCurrentMs(0);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n />\n\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioPlayer.js","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — src accepts a URL or a Blob, and\n * everything else is a facet of playing it: durationMs (because MediaRecorder WebM\n * carries none), sinkId (output device), autoPlay/loop, onEnded/onError, plus the\n * actions slot and locale for the labels. The body is the transport state machine —\n * object-URL lifecycle, seek, and the rAF-free time updates — which shares one audio\n * element ref throughout.\n */\nimport { Pause, Play } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\n\nimport { setAudioOutput } from \"@/audio/audio-output\";\nimport { formatDuration } from \"@/audio/duration\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./AudioPlayer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface AudioPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a page that records twenty notes does not leak twenty\n * URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useAudioRecorder` always does.\n * See the note on the seek bar for why the element's own `duration` is not\n * trustworthy for a fresh recording.\n */\n durationMs?: number;\n /** Output device, from `useMediaDevices().audioOutputs`. Chromium only. */\n sinkId?: string;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the right of the times — a download button, a delete button. */\n actions?: React.ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": { play: \"Tocar\", pause: \"Pausar\", seek: \"Posição\" },\n en: { play: \"Play\", pause: \"Pause\", seek: \"Seek\" },\n} as const;\n\n/**\n * Playback transport for one clip: play/pause, a seek bar, elapsed and total time.\n *\n * Built around a real `<audio>` element rather than the SDK's `createAudioPlayer`,\n * which is a fire-and-forget handle for notification chimes and has no transport to\n * expose. Accepts a `Blob` directly, because the thing an app most often plays is the\n * recording it just made.\n *\n * The seek bar is a bare `<input type=\"range\">` rather than the SDK's `Slider`: that\n * component is a form field, with a label row and a value badge, and a transport wants\n * neither. The native input keeps the keyboard and screen-reader behaviour that\n * matters here for free.\n *\n * @example\n * const rec = useAudioRecorder(mic.stream);\n * {rec.recording && (\n * <AudioPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function AudioPlayer({\n src,\n durationMs,\n sinkId,\n autoPlay = false,\n loop = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioPlayerProps) {\n const strings = STRINGS[locale];\n const audio = useRef<HTMLAudioElement | null>(null);\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const probed = useRef(false);\n\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n /**\n * Total length, preferring what the caller knows.\n *\n * `MediaRecorder` writes WebM with no duration in the header, so a fresh recording\n * reports `Infinity` — which is why the recorder keeps its own clock and passes it\n * here. `durationMs` wins whenever it is finite; the element's value is the\n * fallback for a plain URL.\n */\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n useEffect(() => {\n if (sinkId === undefined) return;\n void setAudioOutput(audio.current, sinkId);\n }, [sinkId, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after which\n * it knows the real length. It is a hack with no alternative — the header simply\n * does not carry the value — and it runs at most once per source, only when the\n * caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = audio.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onSeeked = (): void => {\n node.removeEventListener(\"timeupdate\", onSeeked);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onSeeked);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = audio.current;\n if (!node || !url) return;\n if (node.paused) {\n void node\n .play()\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = audio.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n return (\n <div className={cn(styles.player, className)} {...rest}>\n <audio\n ref={audio}\n src={url ?? undefined}\n loop={loop}\n autoPlay={autoPlay}\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = audio.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onEnded={() => {\n setPlaying(false);\n setCurrentMs(0);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n />\n\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AudioPlayer.js","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — src accepts a URL or a Blob, and\n * everything else is a facet of playing it: durationMs (because MediaRecorder WebM\n * carries none), sinkId (output device), autoPlay/loop, onEnded/onError, plus the\n * actions slot and locale for the labels. The body is the transport state machine —\n * object-URL lifecycle, seek, and the rAF-free time updates — which shares one audio\n * element ref throughout.\n */\nimport { Pause, Play } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\n\nimport { setAudioOutput } from \"@/audio/audio-output\";\nimport { formatDuration } from \"@/audio/duration\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./AudioPlayer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface AudioPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a page that records twenty notes does not leak twenty\n * URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useAudioRecorder` always does.\n * See the note on the seek bar for why the element's own `duration` is not\n * trustworthy for a fresh recording.\n */\n durationMs?: number;\n /** Output device, from `useMediaDevices().audioOutputs`. Chromium only. */\n sinkId?: string;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the right of the times — a download button, a delete button. */\n actions?: React.ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": { play: \"Tocar\", pause: \"Pausar\", seek: \"Posição\" },\n en: { play: \"Play\", pause: \"Pause\", seek: \"Seek\" },\n} as const;\n\n/**\n * Playback transport for one clip: play/pause, a seek bar, elapsed and total time.\n *\n * Built around a real `<audio>` element rather than the SDK's `createAudioPlayer`,\n * which is a fire-and-forget handle for notification chimes and has no transport to\n * expose. Accepts a `Blob` directly, because the thing an app most often plays is the\n * recording it just made.\n *\n * The seek bar is a bare `<input type=\"range\">` rather than the SDK's `Slider`: that\n * component is a form field, with a label row and a value badge, and a transport wants\n * neither. The native input keeps the keyboard and screen-reader behaviour that\n * matters here for free.\n *\n * @example\n * const rec = useAudioRecorder(mic.stream);\n * {rec.recording && (\n * <AudioPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function AudioPlayer({\n src,\n durationMs,\n sinkId,\n autoPlay = false,\n loop = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioPlayerProps) {\n const strings = STRINGS[locale];\n const audio = useRef<HTMLAudioElement | null>(null);\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const probed = useRef(false);\n\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n /**\n * Total length, preferring what the caller knows.\n *\n * `MediaRecorder` writes WebM with no duration in the header, so a fresh recording\n * reports `Infinity` — which is why the recorder keeps its own clock and passes it\n * here. `durationMs` wins whenever it is finite; the element's value is the\n * fallback for a plain URL.\n */\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n useEffect(() => {\n if (sinkId === undefined) return;\n void setAudioOutput(audio.current, sinkId);\n }, [sinkId, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after which\n * it knows the real length. It is a hack with no alternative — the header simply\n * does not carry the value — and it runs at most once per source, only when the\n * caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = audio.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onSeeked = (): void => {\n node.removeEventListener(\"timeupdate\", onSeeked);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onSeeked);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = audio.current;\n if (!node || !url) return;\n if (node.paused) {\n void node\n .play()\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = audio.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n return (\n <div className={cn(styles.player, className)} {...rest}>\n <audio\n ref={audio}\n src={url ?? undefined}\n loop={loop}\n autoPlay={autoPlay}\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = audio.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onEnded={() => {\n setPlaying(false);\n setCurrentMs(0);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n />\n\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioRecorder.cjs","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — recording has three\n * separate configurations that a caller genuinely picks between: the capture\n * (deviceId, audioBitsPerSecond, maxDurationMs), the output (format, wavOptions) and\n * the review step (review, footer, locale). The body holds permission, recording,\n * review and error as one state machine; splitting it would thread the same recorder\n * handle through props.\n */\nimport { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AudioRecorder.cjs","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — recording has three\n * separate configurations that a caller genuinely picks between: the capture\n * (deviceId, audioBitsPerSecond, maxDurationMs), the output (format, wavOptions) and\n * the review step (review, footer, locale). The body holds permission, recording,\n * review and error as one state machine; splitting it would thread the same recorder\n * handle through props.\n */\nimport { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioRecorder.js","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — recording has three\n * separate configurations that a caller genuinely picks between: the capture\n * (deviceId, audioBitsPerSecond, maxDurationMs), the output (format, wavOptions) and\n * the review step (review, footer, locale). The body holds permission, recording,\n * review and error as one state machine; splitting it would thread the same recorder\n * handle through props.\n */\nimport { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AudioRecorder.js","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — recording has three\n * separate configurations that a caller genuinely picks between: the capture\n * (deviceId, audioBitsPerSecond, maxDurationMs), the output (format, wavOptions) and\n * the review step (review, footer, locale). The body holds permission, recording,\n * review and error as one state machine; splitting it would thread the same recorder\n * handle through props.\n */\nimport { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.cjs","names":[],"sources":["../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Avatar.module.css\";\n\nexport type AvatarSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\nexport type AvatarStatus = \"online\" | \"offline\" | \"busy\";\n\nexport interface AvatarProps {\n src?: string;\n alt?: string;\n name?: string;\n size?: AvatarSize;\n status?: AvatarStatus;\n className?: string;\n onClick?: () => void;\n}\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n if (parts.length === 1) return parts[0]!.slice(0, 2);\n return `${parts[0]!.charAt(0)}${parts[parts.length - 1]!.charAt(0)}`;\n}\n\n/**\n * Round avatar. Falls back to colored initials when the image fails to load\n * or no `src` is provided. Optional status dot in the bottom-right corner.\n */\nexport function Avatar({\n src,\n alt,\n name = \"\",\n size = \"md\",\n status,\n className,\n onClick,\n}: AvatarProps) {\n const [errored, setErrored] = useState<boolean>(false);\n const initials = useMemo(() => getInitials(name), [name]);\n\n const showImage = src && !errored;\n const role = onClick ? \"button\" : undefined;\n const tabIndex = onClick ? 0 : undefined;\n\n return (\n <span\n className={cn(styles.avatar, styles[size], status && styles.status, className)}\n role={role}\n tabIndex={tabIndex}\n onClick={onClick}\n onKeyDown={\n onClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick();\n }\n }\n : undefined\n }\n >\n {showImage ? (\n <img\n src={src}\n alt={alt ?? name}\n className={styles.image}\n onError={() => setErrored(true)}\n />\n ) : (\n <span aria-label={alt ?? name}>{initials}</span>\n )}\n {status && <span className={cn(styles.dot, styles[status])} aria-hidden />}\n </span>\n );\n}\n"],"mappings":"6HAiBA,SAAS,EAAY,EAAsB,CACvC,IAAM,EAAQ,EAAK,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO,EAGrD,OAFI,EAAM,SAAW,EAAU,IAC3B,EAAM,SAAW,EAAU,EAAM,EAAE,CAAE,MAAM,EAAG,CAAC,EAC5C,GAAG,EAAM,EAAE,CAAE,OAAO,CAAC,IAAI,EAAM,EAAM,OAAS,EAAE,CAAE,OAAO,CAAC,GACrE,CAMA,SAAgB,EAAO,CACnB,MACA,MACA,OAAO,GACP,OAAO,KACP,SACA,YACA,WACY,CACZ,GAAM,CAAC,EAAS,IAAA,
|
|
1
|
+
{"version":3,"file":"Avatar.cjs","names":[],"sources":["../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Avatar.module.css\";\n\nexport type AvatarSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\nexport type AvatarStatus = \"online\" | \"offline\" | \"busy\";\n\nexport interface AvatarProps {\n src?: string;\n alt?: string;\n name?: string;\n size?: AvatarSize;\n status?: AvatarStatus;\n className?: string;\n onClick?: () => void;\n}\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n if (parts.length === 1) return parts[0]!.slice(0, 2);\n return `${parts[0]!.charAt(0)}${parts[parts.length - 1]!.charAt(0)}`;\n}\n\n/**\n * Round avatar. Falls back to colored initials when the image fails to load\n * or no `src` is provided. Optional status dot in the bottom-right corner.\n */\nexport function Avatar({\n src,\n alt,\n name = \"\",\n size = \"md\",\n status,\n className,\n onClick,\n}: AvatarProps) {\n const [errored, setErrored] = useState<boolean>(false);\n const initials = useMemo(() => getInitials(name), [name]);\n\n const showImage = src && !errored;\n const role = onClick ? \"button\" : undefined;\n const tabIndex = onClick ? 0 : undefined;\n\n return (\n <span\n className={cn(styles.avatar, styles[size], status && styles.status, className)}\n role={role}\n tabIndex={tabIndex}\n onClick={onClick}\n onKeyDown={\n onClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick();\n }\n }\n : undefined\n }\n >\n {showImage ? (\n <img\n src={src}\n alt={alt ?? name}\n className={styles.image}\n onError={() => setErrored(true)}\n />\n ) : (\n <span aria-label={alt ?? name}>{initials}</span>\n )}\n {status && <span className={cn(styles.dot, styles[status])} aria-hidden />}\n </span>\n );\n}\n"],"mappings":"6HAiBA,SAAS,EAAY,EAAsB,CACvC,IAAM,EAAQ,EAAK,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO,EAGrD,OAFI,EAAM,SAAW,EAAU,IAC3B,EAAM,SAAW,EAAU,EAAM,EAAE,CAAE,MAAM,EAAG,CAAC,EAC5C,GAAG,EAAM,EAAE,CAAE,OAAO,CAAC,IAAI,EAAM,EAAM,OAAS,EAAE,CAAE,OAAO,CAAC,GACrE,CAMA,SAAgB,EAAO,CACnB,MACA,MACA,OAAO,GACP,OAAO,KACP,SACA,YACA,WACY,CACZ,GAAM,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAkB,EAAK,EAC/C,GAAA,EAAW,EAAA,QAAA,KAAc,EAAY,CAAI,EAAG,CAAC,CAAI,CAAC,EAElD,EAAY,GAAO,CAAC,EACpB,EAAO,EAAU,SAAW,IAAA,GAC5B,EAAW,EAAU,EAAI,IAAA,GAE/B,OACI,EAAA,EAAA,KAAA,CAAC,OAAD,CACI,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,EAAA,QAAO,GAAO,GAAU,EAAA,QAAO,OAAQ,CAAS,EACvE,OACI,WACD,UACT,UACI,EACO,GAAU,EACH,EAAM,MAAQ,SAAW,EAAM,MAAQ,OACvC,EAAM,eAAe,EACrB,EAAQ,EAEhB,EACA,IAAA,GAbd,SAAA,CAgBK,GACG,EAAA,EAAA,IAAA,CAAC,MAAD,CACS,MACL,IAAK,GAAO,EACZ,UAAW,EAAA,QAAO,MAClB,YAAe,EAAW,EAAI,CACjC,CAAA,GAED,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,aAAY,GAAO,EAAO,SAAA,CAAe,CAAA,EAElD,IAAU,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,GAAG,EAAA,QAAO,IAAK,EAAA,QAAO,EAAO,EAAG,cAAA,EAAa,CAAA,CACvE,GAEd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.js","names":[],"sources":["../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Avatar.module.css\";\n\nexport type AvatarSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\nexport type AvatarStatus = \"online\" | \"offline\" | \"busy\";\n\nexport interface AvatarProps {\n src?: string;\n alt?: string;\n name?: string;\n size?: AvatarSize;\n status?: AvatarStatus;\n className?: string;\n onClick?: () => void;\n}\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n if (parts.length === 1) return parts[0]!.slice(0, 2);\n return `${parts[0]!.charAt(0)}${parts[parts.length - 1]!.charAt(0)}`;\n}\n\n/**\n * Round avatar. Falls back to colored initials when the image fails to load\n * or no `src` is provided. Optional status dot in the bottom-right corner.\n */\nexport function Avatar({\n src,\n alt,\n name = \"\",\n size = \"md\",\n status,\n className,\n onClick,\n}: AvatarProps) {\n const [errored, setErrored] = useState<boolean>(false);\n const initials = useMemo(() => getInitials(name), [name]);\n\n const showImage = src && !errored;\n const role = onClick ? \"button\" : undefined;\n const tabIndex = onClick ? 0 : undefined;\n\n return (\n <span\n className={cn(styles.avatar, styles[size], status && styles.status, className)}\n role={role}\n tabIndex={tabIndex}\n onClick={onClick}\n onKeyDown={\n onClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick();\n }\n }\n : undefined\n }\n >\n {showImage ? (\n <img\n src={src}\n alt={alt ?? name}\n className={styles.image}\n onError={() => setErrored(true)}\n />\n ) : (\n <span aria-label={alt ?? name}>{initials}</span>\n )}\n {status && <span className={cn(styles.dot, styles[status])} aria-hidden />}\n </span>\n );\n}\n"],"mappings":";;;;;AAiBA,SAAS,EAAY,GAAsB;CACvC,IAAM,IAAQ,EAAK,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO;CAGrD,OAFI,EAAM,WAAW,IAAU,MAC3B,EAAM,WAAW,IAAU,EAAM,EAAE,CAAE,MAAM,GAAG,CAAC,IAC5C,GAAG,EAAM,EAAE,CAAE,OAAO,CAAC,IAAI,EAAM,EAAM,SAAS,EAAE,CAAE,OAAO,CAAC;AACrE;AAMA,SAAgB,EAAO,EACnB,QACA,QACA,UAAO,IACP,UAAO,MACP,WACA,cACA,cACY;CACZ,IAAM,CAAC,GAAS,KAAc,EAAkB,EAAK,GAC/C,IAAW,QAAc,EAAY,CAAI,GAAG,CAAC,CAAI,CAAC,GAElD,IAAY,KAAO,CAAC,GACpB,IAAO,IAAU,WAAW,KAAA,GAC5B,IAAW,IAAU,IAAI,KAAA;CAE/B,OACI,kBAAC,QAAD;EACI,WAAW,EAAG,EAAO,QAAQ,EAAO,IAAO,KAAU,EAAO,QAAQ,CAAS;EACvE;EACI;EACD;EACT,WACI,KACO,MAAU;GACP,CAAI,EAAM,QAAQ,WAAW,EAAM,QAAQ,SACvC,EAAM,eAAe,GACrB,EAAQ;EAEhB,IACA,KAAA;
|
|
1
|
+
{"version":3,"file":"Avatar.js","names":[],"sources":["../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Avatar.module.css\";\n\nexport type AvatarSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\nexport type AvatarStatus = \"online\" | \"offline\" | \"busy\";\n\nexport interface AvatarProps {\n src?: string;\n alt?: string;\n name?: string;\n size?: AvatarSize;\n status?: AvatarStatus;\n className?: string;\n onClick?: () => void;\n}\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n if (parts.length === 1) return parts[0]!.slice(0, 2);\n return `${parts[0]!.charAt(0)}${parts[parts.length - 1]!.charAt(0)}`;\n}\n\n/**\n * Round avatar. Falls back to colored initials when the image fails to load\n * or no `src` is provided. Optional status dot in the bottom-right corner.\n */\nexport function Avatar({\n src,\n alt,\n name = \"\",\n size = \"md\",\n status,\n className,\n onClick,\n}: AvatarProps) {\n const [errored, setErrored] = useState<boolean>(false);\n const initials = useMemo(() => getInitials(name), [name]);\n\n const showImage = src && !errored;\n const role = onClick ? \"button\" : undefined;\n const tabIndex = onClick ? 0 : undefined;\n\n return (\n <span\n className={cn(styles.avatar, styles[size], status && styles.status, className)}\n role={role}\n tabIndex={tabIndex}\n onClick={onClick}\n onKeyDown={\n onClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick();\n }\n }\n : undefined\n }\n >\n {showImage ? (\n <img\n src={src}\n alt={alt ?? name}\n className={styles.image}\n onError={() => setErrored(true)}\n />\n ) : (\n <span aria-label={alt ?? name}>{initials}</span>\n )}\n {status && <span className={cn(styles.dot, styles[status])} aria-hidden />}\n </span>\n );\n}\n"],"mappings":";;;;;AAiBA,SAAS,EAAY,GAAsB;CACvC,IAAM,IAAQ,EAAK,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO;CAGrD,OAFI,EAAM,WAAW,IAAU,MAC3B,EAAM,WAAW,IAAU,EAAM,EAAE,CAAE,MAAM,GAAG,CAAC,IAC5C,GAAG,EAAM,EAAE,CAAE,OAAO,CAAC,IAAI,EAAM,EAAM,SAAS,EAAE,CAAE,OAAO,CAAC;AACrE;AAMA,SAAgB,EAAO,EACnB,QACA,QACA,UAAO,IACP,UAAO,MACP,WACA,cACA,cACY;CACZ,IAAM,CAAC,GAAS,KAAc,EAAkB,EAAK,GAC/C,IAAW,QAAc,EAAY,CAAI,GAAG,CAAC,CAAI,CAAC,GAElD,IAAY,KAAO,CAAC,GACpB,IAAO,IAAU,WAAW,KAAA,GAC5B,IAAW,IAAU,IAAI,KAAA;CAE/B,OACI,kBAAC,QAAD;EACI,WAAW,EAAG,EAAO,QAAQ,EAAO,IAAO,KAAU,EAAO,QAAQ,CAAS;EACvE;EACI;EACD;EACT,WACI,KACO,MAAU;GACP,CAAI,EAAM,QAAQ,WAAW,EAAM,QAAQ,SACvC,EAAM,eAAe,GACrB,EAAQ;EAEhB,IACA,KAAA;EAbd,UAAA,CAgBK,IACG,kBAAC,OAAD;GACS;GACL,KAAK,KAAO;GACZ,WAAW,EAAO;GAClB,eAAe,EAAW,EAAI;EACjC,CAAA,IAED,kBAAC,QAAD;GAAM,cAAY,KAAO;GAAO,UAAA;EAAe,CAAA,GAElD,KAAU,kBAAC,QAAD;GAAM,WAAW,EAAG,EAAO,KAAK,EAAO,EAAO;GAAG,eAAA;EAAa,CAAA,CACvE;;AAEd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarGroup.cjs","names":[],"sources":["../../../src/components/AvatarGroup/AvatarGroup.tsx"],"sourcesContent":["import { cn } from \"@/utils/cn\";\nimport { Avatar } from \"../Avatar\";\nimport type { AvatarSize } from \"../Avatar\";\nimport styles from \"./AvatarGroup.module.css\";\n\n/** One participant of the group. */\nexport interface AvatarGroupItem {\n /** Image URL. Falls back to initials from `name`. */\n src?: string;\n /** Full name — drives the initials and the accessible label. */\n name: string;\n}\n\nexport interface AvatarGroupProps {\n items: AvatarGroupItem[];\n /** How many avatars to show before collapsing into `+N`. Default `4`. */\n max?: number;\n size?: AvatarSize;\n /** Accessible name of the group, e.g. `\"Participantes\"`. */\n label?: string;\n /**\n * Called when the `+N` chip is activated — a natural hook for \"see all\n * participants\". Without it the chip is plain text and not focusable.\n */\n onOverflowClick?: () => void;\n className?: string;\n}\n\n/**\n * Overlapping row of avatars with a `+N` overflow chip — participants of a\n * meeting, assignees of a task, members of a team.\n *\n * The whole row is one `role=\"group\"` with a single accessible name, and each\n * avatar's name is exposed as its label. That is deliberate: announcing seven\n * separate images with no relation between them is noise, and the overflow chip\n * carries the remaining count so the total is never hidden from a screen reader.\n *\n * @example\n * ```tsx\n * <AvatarGroup\n * label=\"Participantes\"\n * max={3}\n * items={[\n * { name: \"Ada Lovelace\", src: ada },\n * { name: \"Grace Hopper\" },\n * { name: \"Alan Turing\" },\n * { name: \"Edsger Dijkstra\" },\n * ]}\n * onOverflowClick={() => setDrawerOpen(true)}\n * />\n * ```\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"md\",\n label,\n onOverflowClick,\n className,\n}: AvatarGroupProps) {\n const limit = Math.max(0, max);\n const visible = items.slice(0, limit);\n const overflow = items.length - visible.length;\n\n return (\n <div role=\"group\" aria-label={label} className={cn(styles.group, styles[size], className)}>\n {visible.map((item, index) => (\n <span key={`${item.name}-${index}`} className={styles.slot}>\n <Avatar src={item.src} name={item.name} alt={item.name} size={size} />\n </span>\n ))}\n\n {overflow > 0 ? (\n <span className={styles.slot}>\n {onOverflowClick ? (\n <button\n type=\"button\"\n className={cn(styles.overflow, styles.overflowButton)}\n aria-label={`${overflow} more`}\n onClick={onOverflowClick}\n >\n +{overflow}\n </button>\n ) : (\n <span className={styles.overflow} aria-label={`${overflow} more`}>\n +{overflow}\n </span>\n )}\n </span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"iJAoDA,SAAgB,EAAY,CACxB,QACA,MAAM,EACN,OAAO,KACP,QACA,kBACA,aACiB,CACjB,IAAM,EAAQ,KAAK,IAAI,EAAG,CAAG,EACvB,EAAU,EAAM,MAAM,EAAG,CAAK,EAC9B,EAAW,EAAM,OAAS,EAAQ,OAExC,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,KAAK,QAAQ,aAAY,EAAO,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,EAAA,QAAO,GAAO,CAAS,
|
|
1
|
+
{"version":3,"file":"AvatarGroup.cjs","names":[],"sources":["../../../src/components/AvatarGroup/AvatarGroup.tsx"],"sourcesContent":["import { cn } from \"@/utils/cn\";\nimport { Avatar } from \"../Avatar\";\nimport type { AvatarSize } from \"../Avatar\";\nimport styles from \"./AvatarGroup.module.css\";\n\n/** One participant of the group. */\nexport interface AvatarGroupItem {\n /** Image URL. Falls back to initials from `name`. */\n src?: string;\n /** Full name — drives the initials and the accessible label. */\n name: string;\n}\n\nexport interface AvatarGroupProps {\n items: AvatarGroupItem[];\n /** How many avatars to show before collapsing into `+N`. Default `4`. */\n max?: number;\n size?: AvatarSize;\n /** Accessible name of the group, e.g. `\"Participantes\"`. */\n label?: string;\n /**\n * Called when the `+N` chip is activated — a natural hook for \"see all\n * participants\". Without it the chip is plain text and not focusable.\n */\n onOverflowClick?: () => void;\n className?: string;\n}\n\n/**\n * Overlapping row of avatars with a `+N` overflow chip — participants of a\n * meeting, assignees of a task, members of a team.\n *\n * The whole row is one `role=\"group\"` with a single accessible name, and each\n * avatar's name is exposed as its label. That is deliberate: announcing seven\n * separate images with no relation between them is noise, and the overflow chip\n * carries the remaining count so the total is never hidden from a screen reader.\n *\n * @example\n * ```tsx\n * <AvatarGroup\n * label=\"Participantes\"\n * max={3}\n * items={[\n * { name: \"Ada Lovelace\", src: ada },\n * { name: \"Grace Hopper\" },\n * { name: \"Alan Turing\" },\n * { name: \"Edsger Dijkstra\" },\n * ]}\n * onOverflowClick={() => setDrawerOpen(true)}\n * />\n * ```\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"md\",\n label,\n onOverflowClick,\n className,\n}: AvatarGroupProps) {\n const limit = Math.max(0, max);\n const visible = items.slice(0, limit);\n const overflow = items.length - visible.length;\n\n return (\n <div role=\"group\" aria-label={label} className={cn(styles.group, styles[size], className)}>\n {visible.map((item, index) => (\n <span key={`${item.name}-${index}`} className={styles.slot}>\n <Avatar src={item.src} name={item.name} alt={item.name} size={size} />\n </span>\n ))}\n\n {overflow > 0 ? (\n <span className={styles.slot}>\n {onOverflowClick ? (\n <button\n type=\"button\"\n className={cn(styles.overflow, styles.overflowButton)}\n aria-label={`${overflow} more`}\n onClick={onOverflowClick}\n >\n +{overflow}\n </button>\n ) : (\n <span className={styles.overflow} aria-label={`${overflow} more`}>\n +{overflow}\n </span>\n )}\n </span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"iJAoDA,SAAgB,EAAY,CACxB,QACA,MAAM,EACN,OAAO,KACP,QACA,kBACA,aACiB,CACjB,IAAM,EAAQ,KAAK,IAAI,EAAG,CAAG,EACvB,EAAU,EAAM,MAAM,EAAG,CAAK,EAC9B,EAAW,EAAM,OAAS,EAAQ,OAExC,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,KAAK,QAAQ,aAAY,EAAO,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,EAAA,QAAO,GAAO,CAAS,EAAxF,SAAA,CACK,EAAQ,KAAK,EAAM,KAChB,EAAA,EAAA,IAAA,CAAC,OAAD,CAAoC,UAAW,EAAA,QAAO,KAClD,UAAA,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CAAQ,IAAK,EAAK,IAAK,KAAM,EAAK,KAAM,IAAK,EAAK,KAAY,MAAO,CAAA,CACnE,EAFK,GAAG,EAAK,KAAK,GAAG,GAErB,CACT,EAEA,EAAW,GACR,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KACnB,SAAA,GACG,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,GAAG,EAAA,QAAO,SAAU,EAAA,QAAO,cAAc,EACpD,aAAY,GAAG,EAAS,OACxB,QAAS,EAJb,SAAA,CAKC,IACK,CACE,CAER,CAAA,GAAA,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,SAAU,aAAY,GAAG,EAAS,OAA1D,SAAA,CAAkE,IAC5D,CACA,GAER,CAAA,EACN,IACH,GAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarGroup.js","names":[],"sources":["../../../src/components/AvatarGroup/AvatarGroup.tsx"],"sourcesContent":["import { cn } from \"@/utils/cn\";\nimport { Avatar } from \"../Avatar\";\nimport type { AvatarSize } from \"../Avatar\";\nimport styles from \"./AvatarGroup.module.css\";\n\n/** One participant of the group. */\nexport interface AvatarGroupItem {\n /** Image URL. Falls back to initials from `name`. */\n src?: string;\n /** Full name — drives the initials and the accessible label. */\n name: string;\n}\n\nexport interface AvatarGroupProps {\n items: AvatarGroupItem[];\n /** How many avatars to show before collapsing into `+N`. Default `4`. */\n max?: number;\n size?: AvatarSize;\n /** Accessible name of the group, e.g. `\"Participantes\"`. */\n label?: string;\n /**\n * Called when the `+N` chip is activated — a natural hook for \"see all\n * participants\". Without it the chip is plain text and not focusable.\n */\n onOverflowClick?: () => void;\n className?: string;\n}\n\n/**\n * Overlapping row of avatars with a `+N` overflow chip — participants of a\n * meeting, assignees of a task, members of a team.\n *\n * The whole row is one `role=\"group\"` with a single accessible name, and each\n * avatar's name is exposed as its label. That is deliberate: announcing seven\n * separate images with no relation between them is noise, and the overflow chip\n * carries the remaining count so the total is never hidden from a screen reader.\n *\n * @example\n * ```tsx\n * <AvatarGroup\n * label=\"Participantes\"\n * max={3}\n * items={[\n * { name: \"Ada Lovelace\", src: ada },\n * { name: \"Grace Hopper\" },\n * { name: \"Alan Turing\" },\n * { name: \"Edsger Dijkstra\" },\n * ]}\n * onOverflowClick={() => setDrawerOpen(true)}\n * />\n * ```\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"md\",\n label,\n onOverflowClick,\n className,\n}: AvatarGroupProps) {\n const limit = Math.max(0, max);\n const visible = items.slice(0, limit);\n const overflow = items.length - visible.length;\n\n return (\n <div role=\"group\" aria-label={label} className={cn(styles.group, styles[size], className)}>\n {visible.map((item, index) => (\n <span key={`${item.name}-${index}`} className={styles.slot}>\n <Avatar src={item.src} name={item.name} alt={item.name} size={size} />\n </span>\n ))}\n\n {overflow > 0 ? (\n <span className={styles.slot}>\n {onOverflowClick ? (\n <button\n type=\"button\"\n className={cn(styles.overflow, styles.overflowButton)}\n aria-label={`${overflow} more`}\n onClick={onOverflowClick}\n >\n +{overflow}\n </button>\n ) : (\n <span className={styles.overflow} aria-label={`${overflow} more`}>\n +{overflow}\n </span>\n )}\n </span>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;AAoDA,SAAgB,EAAY,EACxB,UACA,SAAM,GACN,UAAO,MACP,UACA,oBACA,gBACiB;CACjB,IAAM,IAAQ,KAAK,IAAI,GAAG,CAAG,GACvB,IAAU,EAAM,MAAM,GAAG,CAAK,GAC9B,IAAW,EAAM,SAAS,EAAQ;CAExC,OACI,kBAAC,OAAD;EAAK,MAAK;EAAQ,cAAY;EAAO,WAAW,EAAG,EAAO,OAAO,EAAO,IAAO,CAAS;
|
|
1
|
+
{"version":3,"file":"AvatarGroup.js","names":[],"sources":["../../../src/components/AvatarGroup/AvatarGroup.tsx"],"sourcesContent":["import { cn } from \"@/utils/cn\";\nimport { Avatar } from \"../Avatar\";\nimport type { AvatarSize } from \"../Avatar\";\nimport styles from \"./AvatarGroup.module.css\";\n\n/** One participant of the group. */\nexport interface AvatarGroupItem {\n /** Image URL. Falls back to initials from `name`. */\n src?: string;\n /** Full name — drives the initials and the accessible label. */\n name: string;\n}\n\nexport interface AvatarGroupProps {\n items: AvatarGroupItem[];\n /** How many avatars to show before collapsing into `+N`. Default `4`. */\n max?: number;\n size?: AvatarSize;\n /** Accessible name of the group, e.g. `\"Participantes\"`. */\n label?: string;\n /**\n * Called when the `+N` chip is activated — a natural hook for \"see all\n * participants\". Without it the chip is plain text and not focusable.\n */\n onOverflowClick?: () => void;\n className?: string;\n}\n\n/**\n * Overlapping row of avatars with a `+N` overflow chip — participants of a\n * meeting, assignees of a task, members of a team.\n *\n * The whole row is one `role=\"group\"` with a single accessible name, and each\n * avatar's name is exposed as its label. That is deliberate: announcing seven\n * separate images with no relation between them is noise, and the overflow chip\n * carries the remaining count so the total is never hidden from a screen reader.\n *\n * @example\n * ```tsx\n * <AvatarGroup\n * label=\"Participantes\"\n * max={3}\n * items={[\n * { name: \"Ada Lovelace\", src: ada },\n * { name: \"Grace Hopper\" },\n * { name: \"Alan Turing\" },\n * { name: \"Edsger Dijkstra\" },\n * ]}\n * onOverflowClick={() => setDrawerOpen(true)}\n * />\n * ```\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"md\",\n label,\n onOverflowClick,\n className,\n}: AvatarGroupProps) {\n const limit = Math.max(0, max);\n const visible = items.slice(0, limit);\n const overflow = items.length - visible.length;\n\n return (\n <div role=\"group\" aria-label={label} className={cn(styles.group, styles[size], className)}>\n {visible.map((item, index) => (\n <span key={`${item.name}-${index}`} className={styles.slot}>\n <Avatar src={item.src} name={item.name} alt={item.name} size={size} />\n </span>\n ))}\n\n {overflow > 0 ? (\n <span className={styles.slot}>\n {onOverflowClick ? (\n <button\n type=\"button\"\n className={cn(styles.overflow, styles.overflowButton)}\n aria-label={`${overflow} more`}\n onClick={onOverflowClick}\n >\n +{overflow}\n </button>\n ) : (\n <span className={styles.overflow} aria-label={`${overflow} more`}>\n +{overflow}\n </span>\n )}\n </span>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;AAoDA,SAAgB,EAAY,EACxB,UACA,SAAM,GACN,UAAO,MACP,UACA,oBACA,gBACiB;CACjB,IAAM,IAAQ,KAAK,IAAI,GAAG,CAAG,GACvB,IAAU,EAAM,MAAM,GAAG,CAAK,GAC9B,IAAW,EAAM,SAAS,EAAQ;CAExC,OACI,kBAAC,OAAD;EAAK,MAAK;EAAQ,cAAY;EAAO,WAAW,EAAG,EAAO,OAAO,EAAO,IAAO,CAAS;EAAxF,UAAA,CACK,EAAQ,KAAK,GAAM,MAChB,kBAAC,QAAD;GAAoC,WAAW,EAAO;GAClD,UAAA,kBAAC,GAAD;IAAQ,KAAK,EAAK;IAAK,MAAM,EAAK;IAAM,KAAK,EAAK;IAAY;GAAO,CAAA;EACnE,GAFK,GAAG,EAAK,KAAK,GAAG,GAErB,CACT,GAEA,IAAW,IACR,kBAAC,QAAD;GAAM,WAAW,EAAO;GACnB,UAAA,IACG,kBAAC,UAAD;IACI,MAAK;IACL,WAAW,EAAG,EAAO,UAAU,EAAO,cAAc;IACpD,cAAY,GAAG,EAAS;IACxB,SAAS;IAJb,UAAA,CAKC,KACK,CACE;GAER,CAAA,IAAA,kBAAC,QAAD;IAAM,WAAW,EAAO;IAAU,cAAY,GAAG,EAAS;IAA1D,UAAA,CAAkE,KAC5D,CACA;;EAER,CAAA,IACN,IACH;;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Badge.cjs","names":[],"sources":["../../../src/components/Badge/Badge.tsx"],"sourcesContent":["import type { HTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Badge.module.css\";\n\nexport type BadgeVariant = \"neutral\" | \"primary\" | \"success\" | \"warning\" | \"danger\" | \"info\";\nexport type BadgeAppearance = \"soft\" | \"solid\" | \"outline\";\nexport type BadgeSize = \"sm\" | \"md\" | \"lg\";\nexport type BadgeShape = \"pill\" | \"square\";\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n variant?: BadgeVariant;\n /** Visual style: soft (default tinted bg), solid (filled), outline (bordered). */\n appearance?: BadgeAppearance;\n size?: BadgeSize;\n /** Pill (default rounded) or square (slightly rounded). */\n shape?: BadgeShape;\n /** Renders a leading status dot in the badge color. */\n dot?: boolean;\n}\n\n/**\n * Pill / square status badge.\n *\n * - `variant` picks the tone (neutral, primary, success, warning, danger, info).\n * - `appearance` picks the style (soft, solid, outline).\n * - `dot` prepends a status dot in the same tone.\n */\nexport function Badge({\n variant = \"neutral\",\n appearance = \"soft\",\n size = \"md\",\n shape = \"pill\",\n dot = false,\n className,\n children,\n ...props\n}: BadgeProps) {\n return (\n <span\n className={cn(\n styles.badge,\n styles[variant],\n appearance === \"solid\" && styles.solid,\n appearance === \"outline\" && styles.outline,\n styles[size],\n shape === \"square\" && styles.square,\n className,\n )}\n {...props}\n >\n {dot && <span className={styles.dot} aria-hidden />}\n {children}\n </span>\n );\n}\n"],"mappings":"yGA2BA,SAAgB,EAAM,CAClB,UAAU,UACV,aAAa,OACb,OAAO,KACP,QAAQ,OACR,MAAM,GACN,YACA,WACA,GAAG,GACQ,CACX,OACI,EAAA,EAAA,KAAA,CAAC,OAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,MACP,EAAA,QAAO,GACP,IAAe,SAAW,EAAA,QAAO,MACjC,IAAe,WAAa,EAAA,QAAO,QACnC,EAAA,QAAO,GACP,IAAU,UAAY,EAAA,QAAO,OAC7B,CACJ,EACA,GAAI,
|
|
1
|
+
{"version":3,"file":"Badge.cjs","names":[],"sources":["../../../src/components/Badge/Badge.tsx"],"sourcesContent":["import type { HTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Badge.module.css\";\n\nexport type BadgeVariant = \"neutral\" | \"primary\" | \"success\" | \"warning\" | \"danger\" | \"info\";\nexport type BadgeAppearance = \"soft\" | \"solid\" | \"outline\";\nexport type BadgeSize = \"sm\" | \"md\" | \"lg\";\nexport type BadgeShape = \"pill\" | \"square\";\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n variant?: BadgeVariant;\n /** Visual style: soft (default tinted bg), solid (filled), outline (bordered). */\n appearance?: BadgeAppearance;\n size?: BadgeSize;\n /** Pill (default rounded) or square (slightly rounded). */\n shape?: BadgeShape;\n /** Renders a leading status dot in the badge color. */\n dot?: boolean;\n}\n\n/**\n * Pill / square status badge.\n *\n * - `variant` picks the tone (neutral, primary, success, warning, danger, info).\n * - `appearance` picks the style (soft, solid, outline).\n * - `dot` prepends a status dot in the same tone.\n */\nexport function Badge({\n variant = \"neutral\",\n appearance = \"soft\",\n size = \"md\",\n shape = \"pill\",\n dot = false,\n className,\n children,\n ...props\n}: BadgeProps) {\n return (\n <span\n className={cn(\n styles.badge,\n styles[variant],\n appearance === \"solid\" && styles.solid,\n appearance === \"outline\" && styles.outline,\n styles[size],\n shape === \"square\" && styles.square,\n className,\n )}\n {...props}\n >\n {dot && <span className={styles.dot} aria-hidden />}\n {children}\n </span>\n );\n}\n"],"mappings":"yGA2BA,SAAgB,EAAM,CAClB,UAAU,UACV,aAAa,OACb,OAAO,KACP,QAAQ,OACR,MAAM,GACN,YACA,WACA,GAAG,GACQ,CACX,OACI,EAAA,EAAA,KAAA,CAAC,OAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,MACP,EAAA,QAAO,GACP,IAAe,SAAW,EAAA,QAAO,MACjC,IAAe,WAAa,EAAA,QAAO,QACnC,EAAA,QAAO,GACP,IAAU,UAAY,EAAA,QAAO,OAC7B,CACJ,EACA,GAAI,EAVR,SAAA,CAYK,IAAO,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,IAAK,cAAA,EAAa,CAAA,EACjD,CACC,GAEd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Badge.js","names":[],"sources":["../../../src/components/Badge/Badge.tsx"],"sourcesContent":["import type { HTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Badge.module.css\";\n\nexport type BadgeVariant = \"neutral\" | \"primary\" | \"success\" | \"warning\" | \"danger\" | \"info\";\nexport type BadgeAppearance = \"soft\" | \"solid\" | \"outline\";\nexport type BadgeSize = \"sm\" | \"md\" | \"lg\";\nexport type BadgeShape = \"pill\" | \"square\";\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n variant?: BadgeVariant;\n /** Visual style: soft (default tinted bg), solid (filled), outline (bordered). */\n appearance?: BadgeAppearance;\n size?: BadgeSize;\n /** Pill (default rounded) or square (slightly rounded). */\n shape?: BadgeShape;\n /** Renders a leading status dot in the badge color. */\n dot?: boolean;\n}\n\n/**\n * Pill / square status badge.\n *\n * - `variant` picks the tone (neutral, primary, success, warning, danger, info).\n * - `appearance` picks the style (soft, solid, outline).\n * - `dot` prepends a status dot in the same tone.\n */\nexport function Badge({\n variant = \"neutral\",\n appearance = \"soft\",\n size = \"md\",\n shape = \"pill\",\n dot = false,\n className,\n children,\n ...props\n}: BadgeProps) {\n return (\n <span\n className={cn(\n styles.badge,\n styles[variant],\n appearance === \"solid\" && styles.solid,\n appearance === \"outline\" && styles.outline,\n styles[size],\n shape === \"square\" && styles.square,\n className,\n )}\n {...props}\n >\n {dot && <span className={styles.dot} aria-hidden />}\n {children}\n </span>\n );\n}\n"],"mappings":";;;;AA2BA,SAAgB,EAAM,EAClB,aAAU,WACV,gBAAa,QACb,UAAO,MACP,WAAQ,QACR,SAAM,IACN,cACA,aACA,GAAG,KACQ;CACX,OACI,kBAAC,QAAD;EACI,WAAW,EACP,EAAO,OACP,EAAO,IACP,MAAe,WAAW,EAAO,OACjC,MAAe,aAAa,EAAO,SACnC,EAAO,IACP,MAAU,YAAY,EAAO,QAC7B,CACJ;EACA,GAAI;
|
|
1
|
+
{"version":3,"file":"Badge.js","names":[],"sources":["../../../src/components/Badge/Badge.tsx"],"sourcesContent":["import type { HTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Badge.module.css\";\n\nexport type BadgeVariant = \"neutral\" | \"primary\" | \"success\" | \"warning\" | \"danger\" | \"info\";\nexport type BadgeAppearance = \"soft\" | \"solid\" | \"outline\";\nexport type BadgeSize = \"sm\" | \"md\" | \"lg\";\nexport type BadgeShape = \"pill\" | \"square\";\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n variant?: BadgeVariant;\n /** Visual style: soft (default tinted bg), solid (filled), outline (bordered). */\n appearance?: BadgeAppearance;\n size?: BadgeSize;\n /** Pill (default rounded) or square (slightly rounded). */\n shape?: BadgeShape;\n /** Renders a leading status dot in the badge color. */\n dot?: boolean;\n}\n\n/**\n * Pill / square status badge.\n *\n * - `variant` picks the tone (neutral, primary, success, warning, danger, info).\n * - `appearance` picks the style (soft, solid, outline).\n * - `dot` prepends a status dot in the same tone.\n */\nexport function Badge({\n variant = \"neutral\",\n appearance = \"soft\",\n size = \"md\",\n shape = \"pill\",\n dot = false,\n className,\n children,\n ...props\n}: BadgeProps) {\n return (\n <span\n className={cn(\n styles.badge,\n styles[variant],\n appearance === \"solid\" && styles.solid,\n appearance === \"outline\" && styles.outline,\n styles[size],\n shape === \"square\" && styles.square,\n className,\n )}\n {...props}\n >\n {dot && <span className={styles.dot} aria-hidden />}\n {children}\n </span>\n );\n}\n"],"mappings":";;;;AA2BA,SAAgB,EAAM,EAClB,aAAU,WACV,gBAAa,QACb,UAAO,MACP,WAAQ,QACR,SAAM,IACN,cACA,aACA,GAAG,KACQ;CACX,OACI,kBAAC,QAAD;EACI,WAAW,EACP,EAAO,OACP,EAAO,IACP,MAAe,WAAW,EAAO,OACjC,MAAe,aAAa,EAAO,SACnC,EAAO,IACP,MAAU,YAAY,EAAO,QAC7B,CACJ;EACA,GAAI;EAVR,UAAA,CAYK,KAAO,kBAAC,QAAD;GAAM,WAAW,EAAO;GAAK,eAAA;EAAa,CAAA,GACjD,CACC;;AAEd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Banner.cjs","names":[],"sources":["../../../src/components/Banner/Banner.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Banner.module.css\";\n\nexport type BannerVariant = \"info\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface BannerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Visual variant. Default `\"info\"`. */\n variant?: BannerVariant;\n /** Optional leading icon. */\n icon?: ReactNode;\n /** Optional title displayed before description. */\n title?: ReactNode;\n /** Optional action button rendered on the right side. */\n action?: ReactNode;\n /** Show a dismiss button on the right. */\n dismissible?: boolean;\n /** Callback fired when the dismiss button is clicked. */\n onDismiss?: () => void;\n children?: ReactNode;\n}\n\n/**\n * Top-of-page persistent notice. Use for environment indicators\n * (\"Você está em sandbox\"), maintenance windows, account warnings.\n * Different from `Alert` (inline near a field) and `Toast` (transient).\n *\n * @example\n * <Banner variant=\"warning\" dismissible onDismiss={() => setOpen(false)}>\n * Sua assinatura expira em 3 dias.\n * </Banner>\n */\nexport function Banner({\n variant = \"info\",\n icon,\n title,\n action,\n dismissible = false,\n onDismiss,\n className,\n children,\n ...props\n}: BannerProps) {\n const [open, setOpen] = useState(true);\n if (!open) return null;\n return (\n <div className={cn(styles.banner, styles[variant], className)} role=\"status\" {...props}>\n {icon && <span className={styles.icon}>{icon}</span>}\n <div className={styles.body}>\n {title && <p className={styles.title}>{title}</p>}\n {children && <p className={styles.description}>{children}</p>}\n </div>\n {action && <div className={styles.action}>{action}</div>}\n {dismissible && (\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar\"\n onClick={() => {\n setOpen(false);\n onDismiss?.();\n }}\n >\n ×\n </button>\n )}\n </div>\n );\n}\n"],"mappings":"6HAiCA,SAAgB,EAAO,CACnB,UAAU,OACV,OACA,QACA,SACA,cAAc,GACd,YACA,YACA,WACA,GAAG,GACS,CACZ,GAAM,CAAC,EAAM,IAAA,
|
|
1
|
+
{"version":3,"file":"Banner.cjs","names":[],"sources":["../../../src/components/Banner/Banner.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Banner.module.css\";\n\nexport type BannerVariant = \"info\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface BannerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Visual variant. Default `\"info\"`. */\n variant?: BannerVariant;\n /** Optional leading icon. */\n icon?: ReactNode;\n /** Optional title displayed before description. */\n title?: ReactNode;\n /** Optional action button rendered on the right side. */\n action?: ReactNode;\n /** Show a dismiss button on the right. */\n dismissible?: boolean;\n /** Callback fired when the dismiss button is clicked. */\n onDismiss?: () => void;\n children?: ReactNode;\n}\n\n/**\n * Top-of-page persistent notice. Use for environment indicators\n * (\"Você está em sandbox\"), maintenance windows, account warnings.\n * Different from `Alert` (inline near a field) and `Toast` (transient).\n *\n * @example\n * <Banner variant=\"warning\" dismissible onDismiss={() => setOpen(false)}>\n * Sua assinatura expira em 3 dias.\n * </Banner>\n */\nexport function Banner({\n variant = \"info\",\n icon,\n title,\n action,\n dismissible = false,\n onDismiss,\n className,\n children,\n ...props\n}: BannerProps) {\n const [open, setOpen] = useState(true);\n if (!open) return null;\n return (\n <div className={cn(styles.banner, styles[variant], className)} role=\"status\" {...props}>\n {icon && <span className={styles.icon}>{icon}</span>}\n <div className={styles.body}>\n {title && <p className={styles.title}>{title}</p>}\n {children && <p className={styles.description}>{children}</p>}\n </div>\n {action && <div className={styles.action}>{action}</div>}\n {dismissible && (\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar\"\n onClick={() => {\n setOpen(false);\n onDismiss?.();\n }}\n >\n ×\n </button>\n )}\n </div>\n );\n}\n"],"mappings":"6HAiCA,SAAgB,EAAO,CACnB,UAAU,OACV,OACA,QACA,SACA,cAAc,GACd,YACA,YACA,WACA,GAAG,GACS,CACZ,GAAM,CAAC,EAAM,IAAA,EAAW,EAAA,SAAA,CAAS,EAAI,EAErC,OADK,GAED,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,EAAA,QAAO,GAAU,CAAS,EAAG,KAAK,SAAS,GAAI,EAAjF,SAAA,CACK,IAAQ,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAO,SAAA,CAAW,CAAA,GACnD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAvB,SAAA,CACK,IAAS,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAS,CAAA,EAC/C,IAAY,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,YAAc,UAAY,CAAA,CAC3D,IACJ,IAAU,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAS,SAAA,CAAY,CAAA,EACtD,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,MAClB,aAAW,SACX,YAAe,CACX,EAAQ,EAAK,EACb,IAAY,CAChB,EACH,SAAA,GAEO,CAAA,CAEX,IAtBS,IAwBtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Banner.js","names":[],"sources":["../../../src/components/Banner/Banner.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Banner.module.css\";\n\nexport type BannerVariant = \"info\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface BannerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Visual variant. Default `\"info\"`. */\n variant?: BannerVariant;\n /** Optional leading icon. */\n icon?: ReactNode;\n /** Optional title displayed before description. */\n title?: ReactNode;\n /** Optional action button rendered on the right side. */\n action?: ReactNode;\n /** Show a dismiss button on the right. */\n dismissible?: boolean;\n /** Callback fired when the dismiss button is clicked. */\n onDismiss?: () => void;\n children?: ReactNode;\n}\n\n/**\n * Top-of-page persistent notice. Use for environment indicators\n * (\"Você está em sandbox\"), maintenance windows, account warnings.\n * Different from `Alert` (inline near a field) and `Toast` (transient).\n *\n * @example\n * <Banner variant=\"warning\" dismissible onDismiss={() => setOpen(false)}>\n * Sua assinatura expira em 3 dias.\n * </Banner>\n */\nexport function Banner({\n variant = \"info\",\n icon,\n title,\n action,\n dismissible = false,\n onDismiss,\n className,\n children,\n ...props\n}: BannerProps) {\n const [open, setOpen] = useState(true);\n if (!open) return null;\n return (\n <div className={cn(styles.banner, styles[variant], className)} role=\"status\" {...props}>\n {icon && <span className={styles.icon}>{icon}</span>}\n <div className={styles.body}>\n {title && <p className={styles.title}>{title}</p>}\n {children && <p className={styles.description}>{children}</p>}\n </div>\n {action && <div className={styles.action}>{action}</div>}\n {dismissible && (\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar\"\n onClick={() => {\n setOpen(false);\n onDismiss?.();\n }}\n >\n ×\n </button>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;AAiCA,SAAgB,EAAO,EACnB,aAAU,QACV,SACA,UACA,WACA,iBAAc,IACd,cACA,cACA,aACA,GAAG,KACS;CACZ,IAAM,CAAC,GAAM,KAAW,EAAS,EAAI;CAErC,OADK,IAED,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,QAAQ,EAAO,IAAU,CAAS;EAAG,MAAK;EAAS,GAAI;
|
|
1
|
+
{"version":3,"file":"Banner.js","names":[],"sources":["../../../src/components/Banner/Banner.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Banner.module.css\";\n\nexport type BannerVariant = \"info\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface BannerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Visual variant. Default `\"info\"`. */\n variant?: BannerVariant;\n /** Optional leading icon. */\n icon?: ReactNode;\n /** Optional title displayed before description. */\n title?: ReactNode;\n /** Optional action button rendered on the right side. */\n action?: ReactNode;\n /** Show a dismiss button on the right. */\n dismissible?: boolean;\n /** Callback fired when the dismiss button is clicked. */\n onDismiss?: () => void;\n children?: ReactNode;\n}\n\n/**\n * Top-of-page persistent notice. Use for environment indicators\n * (\"Você está em sandbox\"), maintenance windows, account warnings.\n * Different from `Alert` (inline near a field) and `Toast` (transient).\n *\n * @example\n * <Banner variant=\"warning\" dismissible onDismiss={() => setOpen(false)}>\n * Sua assinatura expira em 3 dias.\n * </Banner>\n */\nexport function Banner({\n variant = \"info\",\n icon,\n title,\n action,\n dismissible = false,\n onDismiss,\n className,\n children,\n ...props\n}: BannerProps) {\n const [open, setOpen] = useState(true);\n if (!open) return null;\n return (\n <div className={cn(styles.banner, styles[variant], className)} role=\"status\" {...props}>\n {icon && <span className={styles.icon}>{icon}</span>}\n <div className={styles.body}>\n {title && <p className={styles.title}>{title}</p>}\n {children && <p className={styles.description}>{children}</p>}\n </div>\n {action && <div className={styles.action}>{action}</div>}\n {dismissible && (\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar\"\n onClick={() => {\n setOpen(false);\n onDismiss?.();\n }}\n >\n ×\n </button>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;AAiCA,SAAgB,EAAO,EACnB,aAAU,QACV,SACA,UACA,WACA,iBAAc,IACd,cACA,cACA,aACA,GAAG,KACS;CACZ,IAAM,CAAC,GAAM,KAAW,EAAS,EAAI;CAErC,OADK,IAED,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,QAAQ,EAAO,IAAU,CAAS;EAAG,MAAK;EAAS,GAAI;EAAjF,UAAA;GACK,KAAQ,kBAAC,QAAD;IAAM,WAAW,EAAO;IAAO,UAAA;GAAW,CAAA;GACnD,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA,CACK,KAAS,kBAAC,KAAD;KAAG,WAAW,EAAO;KAAQ,UAAA;IAAS,CAAA,GAC/C,KAAY,kBAAC,KAAD;KAAG,WAAW,EAAO;KAAc;IAAY,CAAA,CAC3D;;GACJ,KAAU,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAS,UAAA;GAAY,CAAA;GACtD,KACG,kBAAC,UAAD;IACI,MAAK;IACL,WAAW,EAAO;IAClB,cAAW;IACX,eAAe;KAEX,AADA,EAAQ,EAAK,GACb,IAAY;IAChB;IACH,UAAA;GAEO,CAAA;EAEX;MAtBS;AAwBtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarcodeScanner.cjs","names":[],"sources":["../../../src/components/BarcodeScanner/BarcodeScanner.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the scan loop is tuned\n * by the caller because the right values depend on the symbology and the device:\n * formats, intervalMs, repeatDelayMs, detector, paused, torch, aspectRatio. The rest\n * are the surfaces to fill when the API is missing (unsupported, footer, locale) and\n * the two outputs (onScan, onError).\n */\nimport { Flashlight, FlashlightOff, ScanLine } from \"lucide-react\";\nimport { type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useBarcodeScanner } from \"@/capture/use-barcode-scanner\";\nimport type { BarcodeDetectorLike, BarcodeFormat, BarcodeScanResult } from \"@/capture/barcode\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./BarcodeScanner.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface BarcodeScannerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Called for every accepted read — repeats of the same value are suppressed. */\n onScan: (result: BarcodeScanResult) => void;\n /** Symbologies to look for. Defaults to QR + EAN-13 + Code 128. */\n formats?: readonly BarcodeFormat[];\n /** Stop looking without releasing the camera — set it while a confirmation is open. */\n paused?: boolean;\n /** A decoder to use instead of the native one, for Safari and Firefox. */\n detector?: BarcodeDetectorLike;\n /** How often a frame is examined, in ms. Default 200. */\n intervalMs?: number;\n /** Ignore the same value again for this long, in ms. Default 2500. */\n repeatDelayMs?: number;\n /** Offer the torch toggle when the camera has a lamp. Default `true`. */\n torch?: boolean;\n /** Viewport aspect ratio, `width / height`. Default `4 / 3`. */\n aspectRatio?: number;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Under the viewport — an instruction, the code just read, a manual-entry link. */\n footer?: ReactNode;\n /**\n * Rendered instead of the camera when there is no decoder.\n *\n * Worth filling in: on iOS and Firefox this is the **only** thing the user sees, so\n * the fallback is usually a plain text field for typing the code.\n */\n unsupported?: ReactNode;\n /** A frame the engine refused to decode. Routine and usually transient. */\n onError?: (error: unknown) => void;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n viewport: \"Visor da câmera\",\n scanning: \"Procurando código…\",\n paused: \"Leitura pausada\",\n opening: \"Abrindo a câmera…\",\n hint: \"Aponte a câmera para o código.\",\n found: (value: string) => `Código lido: ${value}`,\n torchOn: \"Desligar lanterna\",\n torchOff: \"Ligar lanterna\",\n unsupported: \"Este navegador não decodifica códigos de barras.\",\n retry: \"Tentar de novo\",\n },\n en: {\n viewport: \"Camera viewport\",\n scanning: \"Looking for a code…\",\n paused: \"Scanning paused\",\n opening: \"Opening the camera…\",\n hint: \"Point the camera at the code.\",\n found: (value: string) => `Code read: ${value}`,\n torchOn: \"Turn torch off\",\n torchOff: \"Turn torch on\",\n unsupported: \"This browser cannot decode barcodes.\",\n retry: \"Try again\",\n },\n} as const;\n\n/**\n * Point the camera at a barcode and get its value.\n *\n * The counterpart to {@link QRCode}, which only encodes. Under it are\n * `useBarcodeScanner` (the detect loop and repeat suppression), `useCameraStream` (the\n * stream and its classified errors) and `useTorch`.\n *\n * **Mounting this opens the camera**, so mount it when the user asks to scan rather\n * than on a page that happens to contain a scanner: a permission prompt nobody\n * provoked is the surest way to earn a permanent block, and after that `getUserMedia`\n * rejects without ever prompting again. The usual shape is a button that reveals it.\n *\n * The `unsupported` slot is not a nicety. `BarcodeDetector` is Chromium-only — absent\n * on Firefox, on every browser on iOS, and on Chromium for Windows and Linux — so on a\n * large share of real devices the fallback *is* the feature. Give it a text field, or\n * inject a polyfill through `detector`.\n *\n * The preview itself is `aria-hidden`: a live camera frame has nothing to announce and\n * no audio to caption, so what a screen reader gets is the `role=\"status\"` line, which\n * says whether scanning is running and reads out each accepted code.\n *\n * @example\n * <BarcodeScanner\n * formats={[\"ean_13\"]}\n * onScan={({ rawValue }) => addToCart(rawValue)}\n * footer={<small>Aponte para o código de barras da embalagem.</small>}\n * unsupported={<ManualCodeInput onSubmit={addToCart} />}\n * />\n */\nexport function BarcodeScanner({\n onScan,\n formats,\n paused = false,\n detector,\n intervalMs,\n repeatDelayMs,\n torch = true,\n aspectRatio = 4 / 3,\n locale = \"pt-BR\",\n footer,\n unsupported,\n onError,\n className,\n ...rest\n}: BarcodeScannerProps) {\n const strings = STRINGS[locale];\n const {\n videoRef,\n status,\n error,\n supported,\n scanning,\n result,\n torch: lamp,\n retry,\n } = useBarcodeScanner({\n formats,\n paused,\n detector,\n intervalMs,\n repeatDelayMs,\n onScan,\n onError,\n });\n\n if (!supported) {\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n {unsupported}\n </div>\n );\n }\n\n const message = result\n ? strings.found(result.rawValue)\n : paused\n ? strings.paused\n : scanning\n ? strings.scanning\n : strings.opening;\n\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <div\n className={styles.viewport}\n style={{ aspectRatio: String(aspectRatio) }}\n aria-label={strings.viewport}\n role=\"group\"\n >\n <video\n ref={videoRef}\n className={styles.video}\n muted\n playsInline\n aria-hidden=\"true\"\n />\n\n {status === \"ready\" && (\n <div className={styles.frame} aria-hidden=\"true\">\n <span className={cn(styles.laser, scanning && styles.laserActive)} />\n </div>\n )}\n\n {torch && lamp.supported && (\n <button\n type=\"button\"\n className={styles.torch}\n onClick={() => void lamp.toggle()}\n aria-pressed={lamp.on}\n aria-label={lamp.on ? strings.torchOn : strings.torchOff}\n >\n {lamp.on ? (\n <FlashlightOff size={18} aria-hidden />\n ) : (\n <Flashlight size={18} aria-hidden />\n )}\n </button>\n )}\n\n {error && (\n <div className={styles.overlay} role=\"alert\">\n <p className={styles.overlayText}>{error.message}</p>\n <button type=\"button\" className={styles.retry} onClick={retry}>\n {strings.retry}\n </button>\n </div>\n )}\n </div>\n\n {!error && (\n <p className={styles.status} role=\"status\">\n <ScanLine size={14} aria-hidden className={styles.statusIcon} />\n {message}\n </p>\n )}\n\n {footer ? <div className={styles.footer}>{footer}</div> : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BarcodeScanner.cjs","names":[],"sources":["../../../src/components/BarcodeScanner/BarcodeScanner.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the scan loop is tuned\n * by the caller because the right values depend on the symbology and the device:\n * formats, intervalMs, repeatDelayMs, detector, paused, torch, aspectRatio. The rest\n * are the surfaces to fill when the API is missing (unsupported, footer, locale) and\n * the two outputs (onScan, onError).\n */\nimport { Flashlight, FlashlightOff, ScanLine } from \"lucide-react\";\nimport { type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useBarcodeScanner } from \"@/capture/use-barcode-scanner\";\nimport type { BarcodeDetectorLike, BarcodeFormat, BarcodeScanResult } from \"@/capture/barcode\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./BarcodeScanner.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface BarcodeScannerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Called for every accepted read — repeats of the same value are suppressed. */\n onScan: (result: BarcodeScanResult) => void;\n /** Symbologies to look for. Defaults to QR + EAN-13 + Code 128. */\n formats?: readonly BarcodeFormat[];\n /** Stop looking without releasing the camera — set it while a confirmation is open. */\n paused?: boolean;\n /** A decoder to use instead of the native one, for Safari and Firefox. */\n detector?: BarcodeDetectorLike;\n /** How often a frame is examined, in ms. Default 200. */\n intervalMs?: number;\n /** Ignore the same value again for this long, in ms. Default 2500. */\n repeatDelayMs?: number;\n /** Offer the torch toggle when the camera has a lamp. Default `true`. */\n torch?: boolean;\n /** Viewport aspect ratio, `width / height`. Default `4 / 3`. */\n aspectRatio?: number;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Under the viewport — an instruction, the code just read, a manual-entry link. */\n footer?: ReactNode;\n /**\n * Rendered instead of the camera when there is no decoder.\n *\n * Worth filling in: on iOS and Firefox this is the **only** thing the user sees, so\n * the fallback is usually a plain text field for typing the code.\n */\n unsupported?: ReactNode;\n /** A frame the engine refused to decode. Routine and usually transient. */\n onError?: (error: unknown) => void;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n viewport: \"Visor da câmera\",\n scanning: \"Procurando código…\",\n paused: \"Leitura pausada\",\n opening: \"Abrindo a câmera…\",\n hint: \"Aponte a câmera para o código.\",\n found: (value: string) => `Código lido: ${value}`,\n torchOn: \"Desligar lanterna\",\n torchOff: \"Ligar lanterna\",\n unsupported: \"Este navegador não decodifica códigos de barras.\",\n retry: \"Tentar de novo\",\n },\n en: {\n viewport: \"Camera viewport\",\n scanning: \"Looking for a code…\",\n paused: \"Scanning paused\",\n opening: \"Opening the camera…\",\n hint: \"Point the camera at the code.\",\n found: (value: string) => `Code read: ${value}`,\n torchOn: \"Turn torch off\",\n torchOff: \"Turn torch on\",\n unsupported: \"This browser cannot decode barcodes.\",\n retry: \"Try again\",\n },\n} as const;\n\n/**\n * Point the camera at a barcode and get its value.\n *\n * The counterpart to {@link QRCode}, which only encodes. Under it are\n * `useBarcodeScanner` (the detect loop and repeat suppression), `useCameraStream` (the\n * stream and its classified errors) and `useTorch`.\n *\n * **Mounting this opens the camera**, so mount it when the user asks to scan rather\n * than on a page that happens to contain a scanner: a permission prompt nobody\n * provoked is the surest way to earn a permanent block, and after that `getUserMedia`\n * rejects without ever prompting again. The usual shape is a button that reveals it.\n *\n * The `unsupported` slot is not a nicety. `BarcodeDetector` is Chromium-only — absent\n * on Firefox, on every browser on iOS, and on Chromium for Windows and Linux — so on a\n * large share of real devices the fallback *is* the feature. Give it a text field, or\n * inject a polyfill through `detector`.\n *\n * The preview itself is `aria-hidden`: a live camera frame has nothing to announce and\n * no audio to caption, so what a screen reader gets is the `role=\"status\"` line, which\n * says whether scanning is running and reads out each accepted code.\n *\n * @example\n * <BarcodeScanner\n * formats={[\"ean_13\"]}\n * onScan={({ rawValue }) => addToCart(rawValue)}\n * footer={<small>Aponte para o código de barras da embalagem.</small>}\n * unsupported={<ManualCodeInput onSubmit={addToCart} />}\n * />\n */\nexport function BarcodeScanner({\n onScan,\n formats,\n paused = false,\n detector,\n intervalMs,\n repeatDelayMs,\n torch = true,\n aspectRatio = 4 / 3,\n locale = \"pt-BR\",\n footer,\n unsupported,\n onError,\n className,\n ...rest\n}: BarcodeScannerProps) {\n const strings = STRINGS[locale];\n const {\n videoRef,\n status,\n error,\n supported,\n scanning,\n result,\n torch: lamp,\n retry,\n } = useBarcodeScanner({\n formats,\n paused,\n detector,\n intervalMs,\n repeatDelayMs,\n onScan,\n onError,\n });\n\n if (!supported) {\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n {unsupported}\n </div>\n );\n }\n\n const message = result\n ? strings.found(result.rawValue)\n : paused\n ? strings.paused\n : scanning\n ? strings.scanning\n : strings.opening;\n\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <div\n className={styles.viewport}\n style={{ aspectRatio: String(aspectRatio) }}\n aria-label={strings.viewport}\n role=\"group\"\n >\n <video\n ref={videoRef}\n className={styles.video}\n muted\n playsInline\n aria-hidden=\"true\"\n />\n\n {status === \"ready\" && (\n <div className={styles.frame} aria-hidden=\"true\">\n <span className={cn(styles.laser, scanning && styles.laserActive)} />\n </div>\n )}\n\n {torch && lamp.supported && (\n <button\n type=\"button\"\n className={styles.torch}\n onClick={() => void lamp.toggle()}\n aria-pressed={lamp.on}\n aria-label={lamp.on ? strings.torchOn : strings.torchOff}\n >\n {lamp.on ? (\n <FlashlightOff size={18} aria-hidden />\n ) : (\n <Flashlight size={18} aria-hidden />\n )}\n </button>\n )}\n\n {error && (\n <div className={styles.overlay} role=\"alert\">\n <p className={styles.overlayText}>{error.message}</p>\n <button type=\"button\" className={styles.retry} onClick={retry}>\n {strings.retry}\n </button>\n </div>\n )}\n </div>\n\n {!error && (\n <p className={styles.status} role=\"status\">\n <ScanLine size={14} aria-hidden className={styles.statusIcon} />\n {message}\n </p>\n )}\n\n {footer ? <div className={styles.footer}>{footer}</div> : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarcodeScanner.js","names":[],"sources":["../../../src/components/BarcodeScanner/BarcodeScanner.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the scan loop is tuned\n * by the caller because the right values depend on the symbology and the device:\n * formats, intervalMs, repeatDelayMs, detector, paused, torch, aspectRatio. The rest\n * are the surfaces to fill when the API is missing (unsupported, footer, locale) and\n * the two outputs (onScan, onError).\n */\nimport { Flashlight, FlashlightOff, ScanLine } from \"lucide-react\";\nimport { type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useBarcodeScanner } from \"@/capture/use-barcode-scanner\";\nimport type { BarcodeDetectorLike, BarcodeFormat, BarcodeScanResult } from \"@/capture/barcode\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./BarcodeScanner.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface BarcodeScannerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Called for every accepted read — repeats of the same value are suppressed. */\n onScan: (result: BarcodeScanResult) => void;\n /** Symbologies to look for. Defaults to QR + EAN-13 + Code 128. */\n formats?: readonly BarcodeFormat[];\n /** Stop looking without releasing the camera — set it while a confirmation is open. */\n paused?: boolean;\n /** A decoder to use instead of the native one, for Safari and Firefox. */\n detector?: BarcodeDetectorLike;\n /** How often a frame is examined, in ms. Default 200. */\n intervalMs?: number;\n /** Ignore the same value again for this long, in ms. Default 2500. */\n repeatDelayMs?: number;\n /** Offer the torch toggle when the camera has a lamp. Default `true`. */\n torch?: boolean;\n /** Viewport aspect ratio, `width / height`. Default `4 / 3`. */\n aspectRatio?: number;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Under the viewport — an instruction, the code just read, a manual-entry link. */\n footer?: ReactNode;\n /**\n * Rendered instead of the camera when there is no decoder.\n *\n * Worth filling in: on iOS and Firefox this is the **only** thing the user sees, so\n * the fallback is usually a plain text field for typing the code.\n */\n unsupported?: ReactNode;\n /** A frame the engine refused to decode. Routine and usually transient. */\n onError?: (error: unknown) => void;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n viewport: \"Visor da câmera\",\n scanning: \"Procurando código…\",\n paused: \"Leitura pausada\",\n opening: \"Abrindo a câmera…\",\n hint: \"Aponte a câmera para o código.\",\n found: (value: string) => `Código lido: ${value}`,\n torchOn: \"Desligar lanterna\",\n torchOff: \"Ligar lanterna\",\n unsupported: \"Este navegador não decodifica códigos de barras.\",\n retry: \"Tentar de novo\",\n },\n en: {\n viewport: \"Camera viewport\",\n scanning: \"Looking for a code…\",\n paused: \"Scanning paused\",\n opening: \"Opening the camera…\",\n hint: \"Point the camera at the code.\",\n found: (value: string) => `Code read: ${value}`,\n torchOn: \"Turn torch off\",\n torchOff: \"Turn torch on\",\n unsupported: \"This browser cannot decode barcodes.\",\n retry: \"Try again\",\n },\n} as const;\n\n/**\n * Point the camera at a barcode and get its value.\n *\n * The counterpart to {@link QRCode}, which only encodes. Under it are\n * `useBarcodeScanner` (the detect loop and repeat suppression), `useCameraStream` (the\n * stream and its classified errors) and `useTorch`.\n *\n * **Mounting this opens the camera**, so mount it when the user asks to scan rather\n * than on a page that happens to contain a scanner: a permission prompt nobody\n * provoked is the surest way to earn a permanent block, and after that `getUserMedia`\n * rejects without ever prompting again. The usual shape is a button that reveals it.\n *\n * The `unsupported` slot is not a nicety. `BarcodeDetector` is Chromium-only — absent\n * on Firefox, on every browser on iOS, and on Chromium for Windows and Linux — so on a\n * large share of real devices the fallback *is* the feature. Give it a text field, or\n * inject a polyfill through `detector`.\n *\n * The preview itself is `aria-hidden`: a live camera frame has nothing to announce and\n * no audio to caption, so what a screen reader gets is the `role=\"status\"` line, which\n * says whether scanning is running and reads out each accepted code.\n *\n * @example\n * <BarcodeScanner\n * formats={[\"ean_13\"]}\n * onScan={({ rawValue }) => addToCart(rawValue)}\n * footer={<small>Aponte para o código de barras da embalagem.</small>}\n * unsupported={<ManualCodeInput onSubmit={addToCart} />}\n * />\n */\nexport function BarcodeScanner({\n onScan,\n formats,\n paused = false,\n detector,\n intervalMs,\n repeatDelayMs,\n torch = true,\n aspectRatio = 4 / 3,\n locale = \"pt-BR\",\n footer,\n unsupported,\n onError,\n className,\n ...rest\n}: BarcodeScannerProps) {\n const strings = STRINGS[locale];\n const {\n videoRef,\n status,\n error,\n supported,\n scanning,\n result,\n torch: lamp,\n retry,\n } = useBarcodeScanner({\n formats,\n paused,\n detector,\n intervalMs,\n repeatDelayMs,\n onScan,\n onError,\n });\n\n if (!supported) {\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n {unsupported}\n </div>\n );\n }\n\n const message = result\n ? strings.found(result.rawValue)\n : paused\n ? strings.paused\n : scanning\n ? strings.scanning\n : strings.opening;\n\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <div\n className={styles.viewport}\n style={{ aspectRatio: String(aspectRatio) }}\n aria-label={strings.viewport}\n role=\"group\"\n >\n <video\n ref={videoRef}\n className={styles.video}\n muted\n playsInline\n aria-hidden=\"true\"\n />\n\n {status === \"ready\" && (\n <div className={styles.frame} aria-hidden=\"true\">\n <span className={cn(styles.laser, scanning && styles.laserActive)} />\n </div>\n )}\n\n {torch && lamp.supported && (\n <button\n type=\"button\"\n className={styles.torch}\n onClick={() => void lamp.toggle()}\n aria-pressed={lamp.on}\n aria-label={lamp.on ? strings.torchOn : strings.torchOff}\n >\n {lamp.on ? (\n <FlashlightOff size={18} aria-hidden />\n ) : (\n <Flashlight size={18} aria-hidden />\n )}\n </button>\n )}\n\n {error && (\n <div className={styles.overlay} role=\"alert\">\n <p className={styles.overlayText}>{error.message}</p>\n <button type=\"button\" className={styles.retry} onClick={retry}>\n {strings.retry}\n </button>\n </div>\n )}\n </div>\n\n {!error && (\n <p className={styles.status} role=\"status\">\n <ScanLine size={14} aria-hidden className={styles.statusIcon} />\n {message}\n </p>\n )}\n\n {footer ? <div className={styles.footer}>{footer}</div> : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAsDA,IAAM,IAAU;CACZ,SAAS;EACL,UAAU;EACV,UAAU;EACV,QAAQ;EACR,SAAS;EACT,MAAM;EACN,QAAQ,MAAkB,gBAAgB;EAC1C,SAAS;EACT,UAAU;EACV,aAAa;EACb,OAAO;CACX;CACA,IAAI;EACA,UAAU;EACV,UAAU;EACV,QAAQ;EACR,SAAS;EACT,MAAM;EACN,QAAQ,MAAkB,cAAc;EACxC,SAAS;EACT,UAAU;EACV,aAAa;EACb,OAAO;CACX;AACJ;AA+BA,SAAgB,EAAe,EAC3B,WACA,YACA,YAAS,IACT,aACA,eACA,kBACA,WAAQ,IACR,iBAAc,IAAI,GAClB,YAAS,SACT,WACA,gBACA,YACA,cACA,GAAG,KACiB;CACpB,IAAM,IAAU,EAAQ,IAClB,EACF,aACA,WACA,UACA,cACA,aACA,WACA,OAAO,GACP,aACA,EAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;CACJ,CAAC;CAED,IAAI,CAAC,GACD,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,GAAI;YAAnD,CACI,kBAAC,KAAD;GAAG,WAAW,EAAO;aAAS,EAAQ;EAAe,CAAA,GACpD,CACA;;CAIb,IAAM,IAAU,IACV,EAAQ,MAAM,EAAO,QAAQ,IAC7B,IACE,EAAQ,SACR,IACE,EAAQ,WACR,EAAQ;CAElB,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,GAAI;YAAnD;GACI,kBAAC,OAAD;IACI,WAAW,EAAO;IAClB,OAAO,EAAE,aAAa,OAAO,CAAW,EAAE;IAC1C,cAAY,EAAQ;IACpB,MAAK;cAJT;KAMI,kBAAC,SAAD;MACI,KAAK;MACL,WAAW,EAAO;MAClB,OAAA;MACA,aAAA;MACA,eAAY;KACf,CAAA;KAEA,MAAW,WACR,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAO,eAAY;gBACtC,kBAAC,QAAD,EAAM,WAAW,EAAG,EAAO,OAAO,KAAY,EAAO,WAAW,EAAI,CAAA;KACnE,CAAA;KAGR,KAAS,EAAK,aACX,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,eAAe,KAAK,EAAK,OAAO;MAChC,gBAAc,EAAK;MACnB,cAAY,EAAK,KAAK,EAAQ,UAAU,EAAQ;gBAE/C,EAAK,KACF,kBAAC,GAAD;OAAe,MAAM;OAAI,eAAA;MAAa,CAAA,IAEtC,kBAAC,GAAD;OAAY,MAAM;OAAI,eAAA;MAAa,CAAA;KAEnC,CAAA;KAGX,KACG,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAS,MAAK;gBAArC,CACI,kBAAC,KAAD;OAAG,WAAW,EAAO;iBAAc,EAAM;MAAW,CAAA,GACpD,kBAAC,UAAD;OAAQ,MAAK;OAAS,WAAW,EAAO;OAAO,SAAS;iBACnD,EAAQ;MACL,CAAA,CACP;;IAER;;GAEJ,CAAC,KACE,kBAAC,KAAD;IAAG,WAAW,EAAO;IAAQ,MAAK;cAAlC,CACI,kBAAC,GAAD;KAAU,MAAM;KAAI,eAAA;KAAY,WAAW,EAAO;IAAa,CAAA,GAC9D,CACF;;GAGN,IAAS,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAS;GAAY,CAAA,IAAI;EACzD;;AAEb"}
|
|
1
|
+
{"version":3,"file":"BarcodeScanner.js","names":[],"sources":["../../../src/components/BarcodeScanner/BarcodeScanner.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the scan loop is tuned\n * by the caller because the right values depend on the symbology and the device:\n * formats, intervalMs, repeatDelayMs, detector, paused, torch, aspectRatio. The rest\n * are the surfaces to fill when the API is missing (unsupported, footer, locale) and\n * the two outputs (onScan, onError).\n */\nimport { Flashlight, FlashlightOff, ScanLine } from \"lucide-react\";\nimport { type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useBarcodeScanner } from \"@/capture/use-barcode-scanner\";\nimport type { BarcodeDetectorLike, BarcodeFormat, BarcodeScanResult } from \"@/capture/barcode\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./BarcodeScanner.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface BarcodeScannerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Called for every accepted read — repeats of the same value are suppressed. */\n onScan: (result: BarcodeScanResult) => void;\n /** Symbologies to look for. Defaults to QR + EAN-13 + Code 128. */\n formats?: readonly BarcodeFormat[];\n /** Stop looking without releasing the camera — set it while a confirmation is open. */\n paused?: boolean;\n /** A decoder to use instead of the native one, for Safari and Firefox. */\n detector?: BarcodeDetectorLike;\n /** How often a frame is examined, in ms. Default 200. */\n intervalMs?: number;\n /** Ignore the same value again for this long, in ms. Default 2500. */\n repeatDelayMs?: number;\n /** Offer the torch toggle when the camera has a lamp. Default `true`. */\n torch?: boolean;\n /** Viewport aspect ratio, `width / height`. Default `4 / 3`. */\n aspectRatio?: number;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Under the viewport — an instruction, the code just read, a manual-entry link. */\n footer?: ReactNode;\n /**\n * Rendered instead of the camera when there is no decoder.\n *\n * Worth filling in: on iOS and Firefox this is the **only** thing the user sees, so\n * the fallback is usually a plain text field for typing the code.\n */\n unsupported?: ReactNode;\n /** A frame the engine refused to decode. Routine and usually transient. */\n onError?: (error: unknown) => void;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n viewport: \"Visor da câmera\",\n scanning: \"Procurando código…\",\n paused: \"Leitura pausada\",\n opening: \"Abrindo a câmera…\",\n hint: \"Aponte a câmera para o código.\",\n found: (value: string) => `Código lido: ${value}`,\n torchOn: \"Desligar lanterna\",\n torchOff: \"Ligar lanterna\",\n unsupported: \"Este navegador não decodifica códigos de barras.\",\n retry: \"Tentar de novo\",\n },\n en: {\n viewport: \"Camera viewport\",\n scanning: \"Looking for a code…\",\n paused: \"Scanning paused\",\n opening: \"Opening the camera…\",\n hint: \"Point the camera at the code.\",\n found: (value: string) => `Code read: ${value}`,\n torchOn: \"Turn torch off\",\n torchOff: \"Turn torch on\",\n unsupported: \"This browser cannot decode barcodes.\",\n retry: \"Try again\",\n },\n} as const;\n\n/**\n * Point the camera at a barcode and get its value.\n *\n * The counterpart to {@link QRCode}, which only encodes. Under it are\n * `useBarcodeScanner` (the detect loop and repeat suppression), `useCameraStream` (the\n * stream and its classified errors) and `useTorch`.\n *\n * **Mounting this opens the camera**, so mount it when the user asks to scan rather\n * than on a page that happens to contain a scanner: a permission prompt nobody\n * provoked is the surest way to earn a permanent block, and after that `getUserMedia`\n * rejects without ever prompting again. The usual shape is a button that reveals it.\n *\n * The `unsupported` slot is not a nicety. `BarcodeDetector` is Chromium-only — absent\n * on Firefox, on every browser on iOS, and on Chromium for Windows and Linux — so on a\n * large share of real devices the fallback *is* the feature. Give it a text field, or\n * inject a polyfill through `detector`.\n *\n * The preview itself is `aria-hidden`: a live camera frame has nothing to announce and\n * no audio to caption, so what a screen reader gets is the `role=\"status\"` line, which\n * says whether scanning is running and reads out each accepted code.\n *\n * @example\n * <BarcodeScanner\n * formats={[\"ean_13\"]}\n * onScan={({ rawValue }) => addToCart(rawValue)}\n * footer={<small>Aponte para o código de barras da embalagem.</small>}\n * unsupported={<ManualCodeInput onSubmit={addToCart} />}\n * />\n */\nexport function BarcodeScanner({\n onScan,\n formats,\n paused = false,\n detector,\n intervalMs,\n repeatDelayMs,\n torch = true,\n aspectRatio = 4 / 3,\n locale = \"pt-BR\",\n footer,\n unsupported,\n onError,\n className,\n ...rest\n}: BarcodeScannerProps) {\n const strings = STRINGS[locale];\n const {\n videoRef,\n status,\n error,\n supported,\n scanning,\n result,\n torch: lamp,\n retry,\n } = useBarcodeScanner({\n formats,\n paused,\n detector,\n intervalMs,\n repeatDelayMs,\n onScan,\n onError,\n });\n\n if (!supported) {\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n {unsupported}\n </div>\n );\n }\n\n const message = result\n ? strings.found(result.rawValue)\n : paused\n ? strings.paused\n : scanning\n ? strings.scanning\n : strings.opening;\n\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <div\n className={styles.viewport}\n style={{ aspectRatio: String(aspectRatio) }}\n aria-label={strings.viewport}\n role=\"group\"\n >\n <video\n ref={videoRef}\n className={styles.video}\n muted\n playsInline\n aria-hidden=\"true\"\n />\n\n {status === \"ready\" && (\n <div className={styles.frame} aria-hidden=\"true\">\n <span className={cn(styles.laser, scanning && styles.laserActive)} />\n </div>\n )}\n\n {torch && lamp.supported && (\n <button\n type=\"button\"\n className={styles.torch}\n onClick={() => void lamp.toggle()}\n aria-pressed={lamp.on}\n aria-label={lamp.on ? strings.torchOn : strings.torchOff}\n >\n {lamp.on ? (\n <FlashlightOff size={18} aria-hidden />\n ) : (\n <Flashlight size={18} aria-hidden />\n )}\n </button>\n )}\n\n {error && (\n <div className={styles.overlay} role=\"alert\">\n <p className={styles.overlayText}>{error.message}</p>\n <button type=\"button\" className={styles.retry} onClick={retry}>\n {strings.retry}\n </button>\n </div>\n )}\n </div>\n\n {!error && (\n <p className={styles.status} role=\"status\">\n <ScanLine size={14} aria-hidden className={styles.statusIcon} />\n {message}\n </p>\n )}\n\n {footer ? <div className={styles.footer}>{footer}</div> : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAsDA,IAAM,IAAU;CACZ,SAAS;EACL,UAAU;EACV,UAAU;EACV,QAAQ;EACR,SAAS;EACT,MAAM;EACN,QAAQ,MAAkB,gBAAgB;EAC1C,SAAS;EACT,UAAU;EACV,aAAa;EACb,OAAO;CACX;CACA,IAAI;EACA,UAAU;EACV,UAAU;EACV,QAAQ;EACR,SAAS;EACT,MAAM;EACN,QAAQ,MAAkB,cAAc;EACxC,SAAS;EACT,UAAU;EACV,aAAa;EACb,OAAO;CACX;AACJ;AA+BA,SAAgB,EAAe,EAC3B,WACA,YACA,YAAS,IACT,aACA,eACA,kBACA,WAAQ,IACR,iBAAc,IAAI,GAClB,YAAS,SACT,WACA,gBACA,YACA,cACA,GAAG,KACiB;CACpB,IAAM,IAAU,EAAQ,IAClB,EACF,aACA,WACA,UACA,cACA,aACA,WACA,OAAO,GACP,aACA,EAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;CACJ,CAAC;CAED,IAAI,CAAC,GACD,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,GAAI;EAAnD,UAAA,CACI,kBAAC,KAAD;GAAG,WAAW,EAAO;GAAS,UAAA,EAAQ;EAAe,CAAA,GACpD,CACA;;CAIb,IAAM,IAAU,IACV,EAAQ,MAAM,EAAO,QAAQ,IAC7B,IACE,EAAQ,SACR,IACE,EAAQ,WACR,EAAQ;CAElB,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,GAAI;EAAnD,UAAA;GACI,kBAAC,OAAD;IACI,WAAW,EAAO;IAClB,OAAO,EAAE,aAAa,OAAO,CAAW,EAAE;IAC1C,cAAY,EAAQ;IACpB,MAAK;IAJT,UAAA;KAMI,kBAAC,SAAD;MACI,KAAK;MACL,WAAW,EAAO;MAClB,OAAA;MACA,aAAA;MACA,eAAY;KACf,CAAA;KAEA,MAAW,WACR,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAO,eAAY;MACtC,UAAA,kBAAC,QAAD,EAAM,WAAW,EAAG,EAAO,OAAO,KAAY,EAAO,WAAW,EAAI,CAAA;KACnE,CAAA;KAGR,KAAS,EAAK,aACX,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,eAAe,KAAK,EAAK,OAAO;MAChC,gBAAc,EAAK;MACnB,cAAY,EAAK,KAAK,EAAQ,UAAU,EAAQ;MAE/C,UAAA,EAAK,KACF,kBAAC,GAAD;OAAe,MAAM;OAAI,eAAA;MAAa,CAAA,IAEtC,kBAAC,GAAD;OAAY,MAAM;OAAI,eAAA;MAAa,CAAA;KAEnC,CAAA;KAGX,KACG,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAS,MAAK;MAArC,UAAA,CACI,kBAAC,KAAD;OAAG,WAAW,EAAO;OAAc,UAAA,EAAM;MAAW,CAAA,GACpD,kBAAC,UAAD;OAAQ,MAAK;OAAS,WAAW,EAAO;OAAO,SAAS;OACnD,UAAA,EAAQ;MACL,CAAA,CACP;;IAER;;GAEJ,CAAC,KACE,kBAAC,KAAD;IAAG,WAAW,EAAO;IAAQ,MAAK;IAAlC,UAAA,CACI,kBAAC,GAAD;KAAU,MAAM;KAAI,eAAA;KAAY,WAAW,EAAO;IAAa,CAAA,GAC9D,CACF;;GAGN,IAAS,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAS,UAAA;GAAY,CAAA,IAAI;EACzD;;AAEb"}
|