tempest-react-sdk 0.38.3 → 0.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/_virtual/_rolldown/runtime.cjs +1 -1
- package/dist/access/access-control-context.cjs.map +1 -1
- package/dist/access/use-can.cjs.map +1 -1
- package/dist/app/AppProviders.cjs.map +1 -1
- package/dist/app/AppProviders.js.map +1 -1
- package/dist/audio/use-audio-recorder.cjs.map +1 -1
- package/dist/audio/use-audio.cjs.map +1 -1
- package/dist/audio/use-microphone.cjs.map +1 -1
- package/dist/auth/create-auth-store.cjs.map +1 -1
- package/dist/auth/lazy-with-retry.cjs.map +1 -1
- package/dist/auth/use-passkey.cjs.map +1 -1
- package/dist/br/BrazilMap.cjs +1 -1
- package/dist/br/BrazilMap.cjs.map +1 -1
- package/dist/br/BrazilMap.js +2 -2
- package/dist/br/BrazilMap.js.map +1 -1
- package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
- package/dist/br/BrazilStateCitySelect.js.map +1 -1
- package/dist/br/BrazilStateMap.cjs +1 -1
- package/dist/br/BrazilStateMap.cjs.map +1 -1
- package/dist/br/BrazilStateMap.js +2 -2
- package/dist/br/BrazilStateMap.js.map +1 -1
- package/dist/br/MapLegend.cjs.map +1 -1
- package/dist/br/MapLegend.js.map +1 -1
- package/dist/br/MapMarkers.cjs.map +1 -1
- package/dist/br/MapMarkers.js.map +1 -1
- package/dist/br/MunicipalitySearch.cjs.map +1 -1
- package/dist/br/MunicipalitySearch.js.map +1 -1
- package/dist/br/PixQRCode.cjs.map +1 -1
- package/dist/br/PixQRCode.js.map +1 -1
- package/dist/br/use-map-hover.cjs.map +1 -1
- package/dist/br/use-map-zoom.cjs.map +1 -1
- package/dist/capture/media-recorder.cjs +1 -1
- package/dist/capture/media-recorder.js +2 -2
- package/dist/capture/use-barcode-scanner.cjs +1 -1
- package/dist/capture/use-barcode-scanner.cjs.map +1 -1
- package/dist/capture/use-barcode-scanner.js +2 -2
- package/dist/capture/use-barcode-scanner.js.map +1 -1
- package/dist/capture/use-screen-capture.cjs.map +1 -1
- package/dist/capture/use-speech-recognition.cjs.map +1 -1
- package/dist/capture/use-torch.cjs.map +1 -1
- package/dist/capture/use-video-recorder.cjs.map +1 -1
- package/dist/charts/AreaChart.cjs.map +1 -1
- package/dist/charts/AreaChart.js.map +1 -1
- package/dist/charts/BarChart.cjs.map +1 -1
- package/dist/charts/BarChart.js.map +1 -1
- package/dist/charts/LineChart.cjs.map +1 -1
- package/dist/charts/LineChart.js.map +1 -1
- package/dist/charts/PieChart.cjs.map +1 -1
- package/dist/charts/PieChart.js.map +1 -1
- package/dist/charts/RadarChart.cjs.map +1 -1
- package/dist/charts/RadarChart.js.map +1 -1
- package/dist/charts/use-chart-colors.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/Accordion/Accordion.cjs.map +1 -1
- package/dist/components/Accordion/Accordion.js.map +1 -1
- package/dist/components/Alert/Alert.cjs.map +1 -1
- package/dist/components/Alert/Alert.js.map +1 -1
- package/dist/components/AppBar/AppBar.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.js.map +1 -1
- package/dist/components/AppShell/AppShell.cjs.map +1 -1
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
- package/dist/components/Avatar/Avatar.cjs.map +1 -1
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/AvatarGroup/AvatarGroup.cjs.map +1 -1
- package/dist/components/AvatarGroup/AvatarGroup.js.map +1 -1
- package/dist/components/Badge/Badge.cjs.map +1 -1
- package/dist/components/Badge/Badge.js.map +1 -1
- package/dist/components/Banner/Banner.cjs.map +1 -1
- package/dist/components/Banner/Banner.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.cjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.js.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.js.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.cjs +1 -1
- package/dist/components/Calendar/Calendar.cjs.map +1 -1
- package/dist/components/Calendar/Calendar.js +38 -41
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Card/Card.cjs.map +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/Carousel/Carousel.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.js.map +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/Chat/ChatComposer.cjs.map +1 -1
- package/dist/components/Chat/ChatComposer.js.map +1 -1
- package/dist/components/Chat/chat-groups.cjs +1 -1
- package/dist/components/Chat/chat-groups.cjs.map +1 -1
- package/dist/components/Chat/chat-groups.js +2 -2
- package/dist/components/Chat/chat-groups.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.cjs.map +1 -1
- package/dist/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
- package/dist/components/ChipInput/ChipInput.js.map +1 -1
- package/dist/components/ClickOutside/ClickOutside.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/Collapsible/Collapsible.cjs.map +1 -1
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/Combobox/Combobox.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.cjs.map +1 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
- package/dist/components/CopyButton/CopyButton.js.map +1 -1
- package/dist/components/DataList/DataList.cjs.map +1 -1
- package/dist/components/DataList/DataList.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +2 -2
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs.map +1 -1
- package/dist/components/DataTable/EditableCell.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.cjs.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.js.map +1 -1
- package/dist/components/Divider/Divider.cjs.map +1 -1
- package/dist/components/Divider/Divider.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.js.map +1 -1
- package/dist/components/EmptyState/EmptyState.cjs.map +1 -1
- package/dist/components/EmptyState/EmptyState.js.map +1 -1
- package/dist/components/ErrorState/ErrorState.cjs.map +1 -1
- package/dist/components/ErrorState/ErrorState.js.map +1 -1
- package/dist/components/FileUpload/FileUpload.cjs +1 -1
- package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
- package/dist/components/FileUpload/FileUpload.js +1 -1
- package/dist/components/FileUpload/FileUpload.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.js.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.cjs.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.js.map +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/HoverCard/HoverCard.cjs.map +1 -1
- package/dist/components/HoverCard/HoverCard.js.map +1 -1
- package/dist/components/Image/Image.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.js +1 -1
- package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
- package/dist/components/Input/Input.cjs.map +1 -1
- package/dist/components/Input/Input.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
- package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
- package/dist/components/InstallButton/InstallButton.js.map +1 -1
- package/dist/components/Kanban/Kanban.cjs.map +1 -1
- package/dist/components/Kanban/Kanban.js.map +1 -1
- package/dist/components/Label/Label.cjs.map +1 -1
- package/dist/components/Label/Label.js.map +1 -1
- package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
- package/dist/components/Lightbox/Lightbox.js.map +1 -1
- package/dist/components/ListTile/ListTile.cjs.map +1 -1
- package/dist/components/ListTile/ListTile.js.map +1 -1
- package/dist/components/Markdown/Markdown.cjs +1 -1
- package/dist/components/Markdown/Markdown.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.js +2 -2
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/Masonry/Masonry.cjs.map +1 -1
- package/dist/components/Masonry/Masonry.js.map +1 -1
- package/dist/components/Menubar/Menubar.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.js.map +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
- package/dist/components/Money/Money.cjs.map +1 -1
- package/dist/components/Money/Money.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/dist/components/NProgress/NProgress.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.cjs.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
- package/dist/components/NotificationCenter/use-notification-inbox.cjs.map +1 -1
- package/dist/components/OfflineIndicator/OfflineIndicator.cjs.map +1 -1
- package/dist/components/OfflineIndicator/OfflineIndicator.js.map +1 -1
- package/dist/components/Page/Page.cjs.map +1 -1
- package/dist/components/Page/Page.js.map +1 -1
- package/dist/components/Pagination/Pagination.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
- package/dist/components/PinInput/PinInput.cjs.map +1 -1
- package/dist/components/PinInput/PinInput.js.map +1 -1
- package/dist/components/Popover/Popover.cjs.map +1 -1
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Portal/Portal.cjs.map +1 -1
- package/dist/components/Progress/Progress.cjs.map +1 -1
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/QRCode/QRCode.cjs.map +1 -1
- package/dist/components/QRCode/QRCode.js.map +1 -1
- package/dist/components/Radio/Radio.cjs.map +1 -1
- package/dist/components/Radio/Radio.js.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
- package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
- package/dist/components/RatingStars/RatingStars.js.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
- package/dist/components/RelativeTime/RelativeTime.cjs.map +1 -1
- package/dist/components/RelativeTime/RelativeTime.js.map +1 -1
- package/dist/components/Resizable/Resizable.cjs.map +1 -1
- package/dist/components/Resizable/Resizable.js.map +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.cjs.map +1 -1
- package/dist/components/SearchBar/SearchBar.cjs.map +1 -1
- package/dist/components/SearchBar/SearchBar.js.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.cjs.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/components/Select/Select.cjs.map +1 -1
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
- package/dist/components/Slider/Slider.cjs.map +1 -1
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
- package/dist/components/Sparkline/Sparkline.js.map +1 -1
- package/dist/components/Spinner/Spinner.cjs.map +1 -1
- package/dist/components/Spinner/Spinner.js.map +1 -1
- package/dist/components/Stat/Stat.cjs.map +1 -1
- package/dist/components/Stat/Stat.js.map +1 -1
- package/dist/components/Stepper/Stepper.cjs.map +1 -1
- package/dist/components/Stepper/Stepper.js.map +1 -1
- package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
- package/dist/components/StepperInput/StepperInput.js.map +1 -1
- package/dist/components/Switch/Switch.cjs.map +1 -1
- package/dist/components/Switch/Switch.js.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js.map +1 -1
- package/dist/components/Table/Table.cjs.map +1 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tabs/Tabs.cjs.map +1 -1
- package/dist/components/Tabs/Tabs.js.map +1 -1
- package/dist/components/Tag/Tag.cjs.map +1 -1
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Textarea/Textarea.cjs.map +1 -1
- package/dist/components/Textarea/Textarea.js.map +1 -1
- package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
- package/dist/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/components/Timeline/Timeline.cjs.map +1 -1
- package/dist/components/Timeline/Timeline.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/components/Toggle/Toggle.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.cjs.map +1 -1
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/Tour/Tour.cjs.map +1 -1
- package/dist/components/Tour/Tour.js.map +1 -1
- package/dist/components/Transfer/Transfer.cjs.map +1 -1
- package/dist/components/Transfer/Transfer.js.map +1 -1
- package/dist/components/TreeView/TreeView.cjs +1 -1
- package/dist/components/TreeView/TreeView.cjs.map +1 -1
- package/dist/components/TreeView/TreeView.js +1 -4
- package/dist/components/TreeView/TreeView.js.map +1 -1
- package/dist/components/UpdatePrompt/UpdatePrompt.cjs.map +1 -1
- package/dist/components/UpdatePrompt/UpdatePrompt.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/components/VisuallyHidden/VisuallyHidden.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.js.map +1 -1
- package/dist/data/data-provider-context.cjs.map +1 -1
- package/dist/data/use-resource.cjs.map +1 -1
- package/dist/editor/RichTextEditor.cjs.map +1 -1
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/error-boundary/use-error-handler.cjs.map +1 -1
- package/dist/feature-flags/FeatureFlagsProvider.cjs.map +1 -1
- package/dist/forms/FormField.cjs.map +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/use-viacep.cjs.map +1 -1
- package/dist/forms/use-zod-form.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.js.map +1 -1
- package/dist/geo/leaflet-map.cjs.map +1 -1
- package/dist/geo/leaflet-map.js.map +1 -1
- package/dist/geo/use-position-tracker.cjs.map +1 -1
- package/dist/hooks/use-announce.cjs.map +1 -1
- package/dist/hooks/use-async.cjs.map +1 -1
- package/dist/hooks/use-before-install-prompt.cjs.map +1 -1
- package/dist/hooks/use-breakpoint.cjs.map +1 -1
- package/dist/hooks/use-click-outside.cjs.map +1 -1
- package/dist/hooks/use-client-filter.cjs.map +1 -1
- package/dist/hooks/use-clipboard.cjs.map +1 -1
- package/dist/hooks/use-counter.cjs.map +1 -1
- package/dist/hooks/use-debounce.cjs.map +1 -1
- package/dist/hooks/use-deep-memo.cjs.map +1 -1
- package/dist/hooks/use-disclosure.cjs.map +1 -1
- package/dist/hooks/use-document-visibility.cjs.map +1 -1
- package/dist/hooks/use-event-listener.cjs.map +1 -1
- package/dist/hooks/use-geolocation.cjs.map +1 -1
- package/dist/hooks/use-hover.cjs.map +1 -1
- package/dist/hooks/use-idle.cjs.map +1 -1
- package/dist/hooks/use-install-prompt.cjs +1 -1
- package/dist/hooks/use-install-prompt.cjs.map +1 -1
- package/dist/hooks/use-install-prompt.js +1 -1
- package/dist/hooks/use-install-prompt.js.map +1 -1
- package/dist/hooks/use-intersection-observer.cjs.map +1 -1
- package/dist/hooks/use-interval.cjs.map +1 -1
- package/dist/hooks/use-is-first-render.cjs.map +1 -1
- package/dist/hooks/use-keyboard-shortcut.cjs +1 -1
- package/dist/hooks/use-keyboard-shortcut.cjs.map +1 -1
- package/dist/hooks/use-keyboard-shortcut.js +1 -1
- package/dist/hooks/use-keyboard-shortcut.js.map +1 -1
- package/dist/hooks/use-list-state.cjs.map +1 -1
- package/dist/hooks/use-local-storage.cjs.map +1 -1
- package/dist/hooks/use-long-press-handlers.cjs.map +1 -1
- package/dist/hooks/use-long-press.cjs.map +1 -1
- package/dist/hooks/use-map.cjs.map +1 -1
- package/dist/hooks/use-media-devices.cjs.map +1 -1
- package/dist/hooks/use-media-permission.cjs.map +1 -1
- package/dist/hooks/use-media-query.cjs.map +1 -1
- package/dist/hooks/use-object-url.cjs.map +1 -1
- package/dist/hooks/use-online.cjs.map +1 -1
- package/dist/hooks/use-pagination.cjs.map +1 -1
- package/dist/hooks/use-previous.cjs.map +1 -1
- package/dist/hooks/use-queue.cjs.map +1 -1
- package/dist/hooks/use-resize-observer.cjs.map +1 -1
- package/dist/hooks/use-scroll-overflow.cjs.map +1 -1
- package/dist/hooks/use-service-worker-update.cjs.map +1 -1
- package/dist/hooks/use-set.cjs.map +1 -1
- package/dist/hooks/use-sortable.cjs +1 -1
- package/dist/hooks/use-sortable.cjs.map +1 -1
- package/dist/hooks/use-sortable.js +0 -2
- package/dist/hooks/use-sortable.js.map +1 -1
- package/dist/hooks/use-stable-callback.cjs.map +1 -1
- package/dist/hooks/use-storage-estimate.cjs.map +1 -1
- package/dist/hooks/use-throttle.cjs.map +1 -1
- package/dist/hooks/use-timeout.cjs.map +1 -1
- package/dist/hooks/use-toggle.cjs.map +1 -1
- package/dist/hooks/use-window-size.cjs.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +7 -5
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +1 -1
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/use-poll.cjs.map +1 -1
- package/dist/i18n/I18nProvider.cjs.map +1 -1
- package/dist/icons/Icon.cjs.map +1 -1
- package/dist/icons/IconProvider.cjs.map +1 -1
- package/dist/icons/icon-context.cjs.map +1 -1
- package/dist/icons/use-icon.cjs.map +1 -1
- package/dist/imaging/use-image-processing.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/offline/use-offline-sync.cjs.map +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/query/QueryProvider.cjs.map +1 -1
- package/dist/query/constants.cjs +1 -1
- package/dist/query/constants.cjs.map +1 -1
- package/dist/query/constants.js +10 -10
- package/dist/query/constants.js.map +1 -1
- package/dist/query/create-query-keys.cjs +1 -1
- package/dist/query/create-query-keys.cjs.map +1 -1
- package/dist/query/create-query-keys.js +1 -1
- package/dist/query/create-query-keys.js.map +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/use-cursor-query.cjs.map +1 -1
- package/dist/query/use-offline-mutation.cjs.map +1 -1
- package/dist/query/use-paginated-query.cjs.map +1 -1
- package/dist/router/AppRouter.cjs +1 -1
- package/dist/router/AppRouter.cjs.map +1 -1
- package/dist/router/AppRouter.js +7 -2
- package/dist/router/AppRouter.js.map +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/store/create-store.cjs.map +1 -1
- package/dist/tabular/use-tabular-predictor.cjs.map +1 -1
- package/dist/telemetry/TelemetryProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/color.cjs +1 -1
- package/dist/theme/color.cjs.map +1 -1
- package/dist/theme/color.js +1 -1
- package/dist/theme/color.js.map +1 -1
- package/dist/utils/cn.cjs +1 -1
- package/dist/utils/cn.cjs.map +1 -1
- package/dist/utils/cn.js +1 -1
- package/dist/utils/cn.js.map +1 -1
- package/dist/utils/objects.cjs +1 -1
- package/dist/utils/objects.cjs.map +1 -1
- package/dist/utils/objects.js +1 -1
- package/dist/utils/objects.js.map +1 -1
- package/dist/utils/xlsx.cjs +1 -1
- package/dist/utils/xlsx.cjs.map +1 -1
- package/dist/utils/xlsx.js +3 -3
- package/dist/utils/xlsx.js.map +1 -1
- package/dist/vision/core/canvas.cjs +1 -1
- package/dist/vision/core/canvas.cjs.map +1 -1
- package/dist/vision/core/canvas.js +4 -4
- package/dist/vision/core/canvas.js.map +1 -1
- package/dist/vision/core/exceptions.cjs +1 -1
- package/dist/vision/core/exceptions.cjs.map +1 -1
- package/dist/vision/core/exceptions.js +2 -2
- package/dist/vision/core/exceptions.js.map +1 -1
- package/dist/vision/core/metadata.cjs +2 -2
- package/dist/vision/core/metadata.cjs.map +1 -1
- package/dist/vision/core/metadata.js +11 -8
- package/dist/vision/core/metadata.js.map +1 -1
- package/dist/vision/core/session.cjs +1 -1
- package/dist/vision/core/session.cjs.map +1 -1
- package/dist/vision/core/session.js +8 -5
- package/dist/vision/core/session.js.map +1 -1
- package/dist/vision/fusion.cjs +2 -0
- package/dist/vision/fusion.cjs.map +1 -0
- package/dist/vision/fusion.js +42 -0
- package/dist/vision/fusion.js.map +1 -0
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +23 -20
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/io/image.cjs +1 -1
- package/dist/vision/io/image.cjs.map +1 -1
- package/dist/vision/io/image.js +4 -2
- package/dist/vision/io/image.js.map +1 -1
- package/dist/vision/labels.cjs +1 -1
- package/dist/vision/labels.cjs.map +1 -1
- package/dist/vision/labels.js +7 -4
- package/dist/vision/labels.js.map +1 -1
- package/dist/vision/postprocess/detection.cjs +1 -1
- package/dist/vision/postprocess/detection.cjs.map +1 -1
- package/dist/vision/postprocess/detection.js +3 -10
- package/dist/vision/postprocess/detection.js.map +1 -1
- package/dist/vision/postprocess/segmentation.cjs +1 -1
- package/dist/vision/postprocess/segmentation.cjs.map +1 -1
- package/dist/vision/postprocess/segmentation.js +11 -15
- package/dist/vision/postprocess/segmentation.js.map +1 -1
- package/dist/vision/preprocess/image.cjs +1 -1
- package/dist/vision/preprocess/image.cjs.map +1 -1
- package/dist/vision/preprocess/image.js +4 -2
- package/dist/vision/preprocess/image.js.map +1 -1
- package/dist/vision/preprocess/pipeline.cjs +2 -0
- package/dist/vision/preprocess/pipeline.cjs.map +1 -0
- package/dist/vision/preprocess/pipeline.js +61 -0
- package/dist/vision/preprocess/pipeline.js.map +1 -0
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +23 -2
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/base.cjs +1 -1
- package/dist/vision/tasks/base.cjs.map +1 -1
- package/dist/vision/tasks/base.js +11 -2
- package/dist/vision/tasks/base.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js +2 -2
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detectClassify.cjs +2 -0
- package/dist/vision/tasks/detectClassify.cjs.map +1 -0
- package/dist/vision/tasks/detectClassify.js +222 -0
- package/dist/vision/tasks/detectClassify.js.map +1 -0
- package/dist/vision/tasks/detector.cjs +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js +54 -29
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js +53 -28
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision/types.cjs.map +1 -1
- package/dist/vision/types.js.map +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-live-luminance.cjs.map +1 -1
- package/dist/vision.cjs +1 -1
- package/dist/vision.d.ts +1517 -962
- package/dist/vision.js +25 -22
- package/dist/vite/create-vite-config.cjs +1 -1
- package/dist/vite/create-vite-config.cjs.map +1 -1
- package/dist/vite/create-vite-config.js +3 -3
- package/dist/vite/create-vite-config.js.map +1 -1
- package/dist/vite/tempest-icons.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +5 -3
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +2 -2
- package/template/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Accordion.js","names":[],"sources":["../../../src/components/Accordion/Accordion.tsx"],"sourcesContent":["import { useCallback, useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Accordion.module.css\";\n\nexport interface AccordionItem {\n /** Stable identifier. */\n id: string;\n title: ReactNode;\n children: ReactNode;\n disabled?: boolean;\n}\n\nexport interface AccordionProps {\n items: AccordionItem[];\n /** When `true` multiple items can be open simultaneously. Default `false`. */\n multiple?: boolean;\n /** Controlled open ids. */\n value?: string[];\n /** Uncontrolled default open ids. */\n defaultValue?: string[];\n onChange?: (openIds: string[]) => void;\n className?: string;\n}\n\n/**\n * Accessible accordion. Each item collapses/expands its content. Single-mode by\n * default — pass `multiple` to allow more than one item open at a time. Can be\n * controlled via `value` + `onChange`, or uncontrolled via `defaultValue`.\n */\nexport function Accordion({\n items,\n multiple = false,\n value,\n defaultValue = [],\n onChange,\n className,\n}: AccordionProps) {\n const reactId = useId();\n const isControlled = value !== undefined;\n const [internalOpen, setInternalOpen] = useState<string[]>(defaultValue);\n const openIds = isControlled ? value : internalOpen;\n\n const toggle = useCallback(\n (id: string): void => {\n const isOpen = openIds.includes(id);\n const next = multiple\n ? isOpen\n ? openIds.filter((x) => x !== id)\n : [...openIds, id]\n : isOpen\n ? []\n : [id];\n if (!isControlled) setInternalOpen(next);\n onChange?.(next);\n },\n [openIds, multiple, isControlled, onChange],\n );\n\n return (\n <div className={cn(styles.accordion, className)}>\n {items.map((item) => {\n const open = openIds.includes(item.id);\n const panelId = `${reactId}-${item.id}-panel`;\n const triggerId = `${reactId}-${item.id}-trigger`;\n return (\n <div key={item.id} className={cn(styles.item, open && styles.open)}>\n <h3 className={styles.header}>\n <button\n type=\"button\"\n id={triggerId}\n className={styles.trigger}\n aria-expanded={open}\n aria-controls={panelId}\n disabled={item.disabled}\n onClick={() => toggle(item.id)}\n >\n <span>{item.title}</span>\n <ChevronIcon className={styles.chevron} />\n </button>\n </h3>\n {open && (\n <div\n id={panelId}\n role=\"region\"\n aria-labelledby={triggerId}\n className={styles.panel}\n >\n {item.children}\n </div>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nfunction ChevronIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden\n >\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;;AA8BA,SAAgB,EAAU,EACtB,UACA,cAAW,IACX,UACA,kBAAe,CAAC,GAChB,aACA,gBACe;CACf,IAAM,IAAU,EAAM,GAChB,IAAe,MAAU,KAAA,GACzB,CAAC,GAAc,KAAmB,EAAmB,CAAY,GACjE,IAAU,IAAe,IAAQ,GAEjC,IAAS,GACV,MAAqB;EAClB,IAAM,IAAS,EAAQ,SAAS,CAAE,GAC5B,IAAO,IACP,IACI,EAAQ,QAAQ,MAAM,MAAM,CAAE,IAC9B,CAAC,GAAG,GAAS,CAAE,IACnB,IACE,CAAC,IACD,CAAC,CAAE;EAEX,AADK,KAAc,EAAgB,CAAI,GACvC,IAAW,CAAI;CACnB,GACA;EAAC;EAAS;EAAU;EAAc;CAAQ,CAC9C;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,WAAW,CAAS;
|
|
1
|
+
{"version":3,"file":"Accordion.js","names":[],"sources":["../../../src/components/Accordion/Accordion.tsx"],"sourcesContent":["import { useCallback, useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Accordion.module.css\";\n\nexport interface AccordionItem {\n /** Stable identifier. */\n id: string;\n title: ReactNode;\n children: ReactNode;\n disabled?: boolean;\n}\n\nexport interface AccordionProps {\n items: AccordionItem[];\n /** When `true` multiple items can be open simultaneously. Default `false`. */\n multiple?: boolean;\n /** Controlled open ids. */\n value?: string[];\n /** Uncontrolled default open ids. */\n defaultValue?: string[];\n onChange?: (openIds: string[]) => void;\n className?: string;\n}\n\n/**\n * Accessible accordion. Each item collapses/expands its content. Single-mode by\n * default — pass `multiple` to allow more than one item open at a time. Can be\n * controlled via `value` + `onChange`, or uncontrolled via `defaultValue`.\n */\nexport function Accordion({\n items,\n multiple = false,\n value,\n defaultValue = [],\n onChange,\n className,\n}: AccordionProps) {\n const reactId = useId();\n const isControlled = value !== undefined;\n const [internalOpen, setInternalOpen] = useState<string[]>(defaultValue);\n const openIds = isControlled ? value : internalOpen;\n\n const toggle = useCallback(\n (id: string): void => {\n const isOpen = openIds.includes(id);\n const next = multiple\n ? isOpen\n ? openIds.filter((x) => x !== id)\n : [...openIds, id]\n : isOpen\n ? []\n : [id];\n if (!isControlled) setInternalOpen(next);\n onChange?.(next);\n },\n [openIds, multiple, isControlled, onChange],\n );\n\n return (\n <div className={cn(styles.accordion, className)}>\n {items.map((item) => {\n const open = openIds.includes(item.id);\n const panelId = `${reactId}-${item.id}-panel`;\n const triggerId = `${reactId}-${item.id}-trigger`;\n return (\n <div key={item.id} className={cn(styles.item, open && styles.open)}>\n <h3 className={styles.header}>\n <button\n type=\"button\"\n id={triggerId}\n className={styles.trigger}\n aria-expanded={open}\n aria-controls={panelId}\n disabled={item.disabled}\n onClick={() => toggle(item.id)}\n >\n <span>{item.title}</span>\n <ChevronIcon className={styles.chevron} />\n </button>\n </h3>\n {open && (\n <div\n id={panelId}\n role=\"region\"\n aria-labelledby={triggerId}\n className={styles.panel}\n >\n {item.children}\n </div>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nfunction ChevronIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden\n >\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;;AA8BA,SAAgB,EAAU,EACtB,UACA,cAAW,IACX,UACA,kBAAe,CAAC,GAChB,aACA,gBACe;CACf,IAAM,IAAU,EAAM,GAChB,IAAe,MAAU,KAAA,GACzB,CAAC,GAAc,KAAmB,EAAmB,CAAY,GACjE,IAAU,IAAe,IAAQ,GAEjC,IAAS,GACV,MAAqB;EAClB,IAAM,IAAS,EAAQ,SAAS,CAAE,GAC5B,IAAO,IACP,IACI,EAAQ,QAAQ,MAAM,MAAM,CAAE,IAC9B,CAAC,GAAG,GAAS,CAAE,IACnB,IACE,CAAC,IACD,CAAC,CAAE;EAEX,AADK,KAAc,EAAgB,CAAI,GACvC,IAAW,CAAI;CACnB,GACA;EAAC;EAAS;EAAU;EAAc;CAAQ,CAC9C;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,WAAW,CAAS;EACzC,UAAA,EAAM,KAAK,MAAS;GACjB,IAAM,IAAO,EAAQ,SAAS,EAAK,EAAE,GAC/B,IAAU,GAAG,EAAQ,GAAG,EAAK,GAAG,SAChC,IAAY,GAAG,EAAQ,GAAG,EAAK,GAAG;GACxC,OACI,kBAAC,OAAD;IAAmB,WAAW,EAAG,EAAO,MAAM,KAAQ,EAAO,IAAI;IAAjE,UAAA,CACI,kBAAC,MAAD;KAAI,WAAW,EAAO;KAClB,UAAA,kBAAC,UAAD;MACI,MAAK;MACL,IAAI;MACJ,WAAW,EAAO;MAClB,iBAAe;MACf,iBAAe;MACf,UAAU,EAAK;MACf,eAAe,EAAO,EAAK,EAAE;MAPjC,UAAA,CASI,kBAAC,QAAD,EAAA,UAAO,EAAK,MAAY,CAAA,GACxB,kBAAC,GAAD,EAAa,WAAW,EAAO,QAAU,CAAA,CACrC;;IACR,CAAA,GACH,KACG,kBAAC,OAAD;KACI,IAAI;KACJ,MAAK;KACL,mBAAiB;KACjB,WAAW,EAAO;KAEjB,UAAA,EAAK;IACL,CAAA,CAER;GAzBK,GAAA,EAAK,EAyBV;EAEb,CAAC;CACA,CAAA;AAEb;AAEA,SAAS,EAAY,EAAE,gBAAqC;CACxD,OACI,kBAAC,OAAD;EACe;EACX,OAAM;EACN,QAAO;EACP,SAAQ;EACR,MAAK;EACL,eAAA;EAEA,UAAA,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;GACd,gBAAe;EAClB,CAAA;CACA,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Alert.cjs","names":[],"sources":["../../../src/components/Alert/Alert.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Alert.module.css\";\n\nexport type AlertVariant = \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\";\nexport type AlertAppearance = \"soft\" | \"solid\" | \"outline\";\n\nexport interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n variant?: AlertVariant;\n /** Visual style: soft (default tinted bg), solid (filled), outline (bordered). */\n appearance?: AlertAppearance;\n title?: ReactNode;\n description?: ReactNode;\n icon?: ReactNode;\n /** Show a close button and invoke this when clicked. */\n onClose?: () => void;\n /** Custom close button label for screen readers. */\n closeLabel?: string;\n}\n\n/**\n * Inline alert / notice with tone (info/success/warning/danger) and appearance\n * (soft/solid/outline). Accepts optional `icon`, `title`, `description` and\n * a dismiss button via `onClose`.\n */\nexport function Alert({\n variant = \"info\",\n appearance = \"soft\",\n title,\n description,\n icon,\n onClose,\n closeLabel = \"Dismiss\",\n className,\n children,\n ...props\n}: AlertProps) {\n return (\n <div\n role=\"alert\"\n className={cn(\n styles.alert,\n styles[variant],\n appearance === \"solid\" && styles.solid,\n appearance === \"outline\" && styles.outline,\n className,\n )}\n {...props}\n >\n {icon && <span className={styles.icon}>{icon}</span>}\n <div className={styles.content}>\n {title && <p className={styles.title}>{title}</p>}\n {description && <p className={styles.description}>{description}</p>}\n {children}\n </div>\n {onClose && (\n <button\n type=\"button\"\n className={styles.close}\n onClick={onClose}\n aria-label={closeLabel}\n >\n <CloseIcon />\n </button>\n )}\n </div>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n <path\n d=\"M6 6l12 12M18 6L6 18\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"yGAyBA,SAAgB,EAAM,CAClB,UAAU,OACV,aAAa,OACb,QACA,cACA,OACA,UACA,aAAa,UACb,YACA,WACA,GAAG,GACQ,CACX,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,KAAK,QACL,UAAW,EAAA,GACP,EAAA,QAAO,MACP,EAAA,QAAO,GACP,IAAe,SAAW,EAAA,QAAO,MACjC,IAAe,WAAa,EAAA,QAAO,QACnC,CACJ,EACA,GAAI,
|
|
1
|
+
{"version":3,"file":"Alert.cjs","names":[],"sources":["../../../src/components/Alert/Alert.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Alert.module.css\";\n\nexport type AlertVariant = \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\";\nexport type AlertAppearance = \"soft\" | \"solid\" | \"outline\";\n\nexport interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n variant?: AlertVariant;\n /** Visual style: soft (default tinted bg), solid (filled), outline (bordered). */\n appearance?: AlertAppearance;\n title?: ReactNode;\n description?: ReactNode;\n icon?: ReactNode;\n /** Show a close button and invoke this when clicked. */\n onClose?: () => void;\n /** Custom close button label for screen readers. */\n closeLabel?: string;\n}\n\n/**\n * Inline alert / notice with tone (info/success/warning/danger) and appearance\n * (soft/solid/outline). Accepts optional `icon`, `title`, `description` and\n * a dismiss button via `onClose`.\n */\nexport function Alert({\n variant = \"info\",\n appearance = \"soft\",\n title,\n description,\n icon,\n onClose,\n closeLabel = \"Dismiss\",\n className,\n children,\n ...props\n}: AlertProps) {\n return (\n <div\n role=\"alert\"\n className={cn(\n styles.alert,\n styles[variant],\n appearance === \"solid\" && styles.solid,\n appearance === \"outline\" && styles.outline,\n className,\n )}\n {...props}\n >\n {icon && <span className={styles.icon}>{icon}</span>}\n <div className={styles.content}>\n {title && <p className={styles.title}>{title}</p>}\n {description && <p className={styles.description}>{description}</p>}\n {children}\n </div>\n {onClose && (\n <button\n type=\"button\"\n className={styles.close}\n onClick={onClose}\n aria-label={closeLabel}\n >\n <CloseIcon />\n </button>\n )}\n </div>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n <path\n d=\"M6 6l12 12M18 6L6 18\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"yGAyBA,SAAgB,EAAM,CAClB,UAAU,OACV,aAAa,OACb,QACA,cACA,OACA,UACA,aAAa,UACb,YACA,WACA,GAAG,GACQ,CACX,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,KAAK,QACL,UAAW,EAAA,GACP,EAAA,QAAO,MACP,EAAA,QAAO,GACP,IAAe,SAAW,EAAA,QAAO,MACjC,IAAe,WAAa,EAAA,QAAO,QACnC,CACJ,EACA,GAAI,EATR,SAAA,CAWK,IAAQ,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAO,SAAA,CAAW,CAAA,GACnD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAvB,SAAA,CACK,IAAS,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAS,CAAA,EAC/C,IAAe,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,YAAc,SAAA,CAAe,CAAA,EACjE,CACA,IACJ,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,MAClB,QAAS,EACT,aAAY,EAEZ,UAAA,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,CAAA,CACR,CAAA,CAEX,GAEb,CAEA,SAAS,GAAY,CACjB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,OAAO,cAAA,GACxD,UAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,uBACF,OAAO,eACP,YAAY,IACZ,cAAc,OACjB,CAAA,CACA,CAAA,CAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Alert.js","names":[],"sources":["../../../src/components/Alert/Alert.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Alert.module.css\";\n\nexport type AlertVariant = \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\";\nexport type AlertAppearance = \"soft\" | \"solid\" | \"outline\";\n\nexport interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n variant?: AlertVariant;\n /** Visual style: soft (default tinted bg), solid (filled), outline (bordered). */\n appearance?: AlertAppearance;\n title?: ReactNode;\n description?: ReactNode;\n icon?: ReactNode;\n /** Show a close button and invoke this when clicked. */\n onClose?: () => void;\n /** Custom close button label for screen readers. */\n closeLabel?: string;\n}\n\n/**\n * Inline alert / notice with tone (info/success/warning/danger) and appearance\n * (soft/solid/outline). Accepts optional `icon`, `title`, `description` and\n * a dismiss button via `onClose`.\n */\nexport function Alert({\n variant = \"info\",\n appearance = \"soft\",\n title,\n description,\n icon,\n onClose,\n closeLabel = \"Dismiss\",\n className,\n children,\n ...props\n}: AlertProps) {\n return (\n <div\n role=\"alert\"\n className={cn(\n styles.alert,\n styles[variant],\n appearance === \"solid\" && styles.solid,\n appearance === \"outline\" && styles.outline,\n className,\n )}\n {...props}\n >\n {icon && <span className={styles.icon}>{icon}</span>}\n <div className={styles.content}>\n {title && <p className={styles.title}>{title}</p>}\n {description && <p className={styles.description}>{description}</p>}\n {children}\n </div>\n {onClose && (\n <button\n type=\"button\"\n className={styles.close}\n onClick={onClose}\n aria-label={closeLabel}\n >\n <CloseIcon />\n </button>\n )}\n </div>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n <path\n d=\"M6 6l12 12M18 6L6 18\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;AAyBA,SAAgB,EAAM,EAClB,aAAU,QACV,gBAAa,QACb,UACA,gBACA,SACA,YACA,gBAAa,WACb,cACA,aACA,GAAG,KACQ;CACX,OACI,kBAAC,OAAD;EACI,MAAK;EACL,WAAW,EACP,EAAO,OACP,EAAO,IACP,MAAe,WAAW,EAAO,OACjC,MAAe,aAAa,EAAO,SACnC,CACJ;EACA,GAAI;
|
|
1
|
+
{"version":3,"file":"Alert.js","names":[],"sources":["../../../src/components/Alert/Alert.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Alert.module.css\";\n\nexport type AlertVariant = \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\";\nexport type AlertAppearance = \"soft\" | \"solid\" | \"outline\";\n\nexport interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n variant?: AlertVariant;\n /** Visual style: soft (default tinted bg), solid (filled), outline (bordered). */\n appearance?: AlertAppearance;\n title?: ReactNode;\n description?: ReactNode;\n icon?: ReactNode;\n /** Show a close button and invoke this when clicked. */\n onClose?: () => void;\n /** Custom close button label for screen readers. */\n closeLabel?: string;\n}\n\n/**\n * Inline alert / notice with tone (info/success/warning/danger) and appearance\n * (soft/solid/outline). Accepts optional `icon`, `title`, `description` and\n * a dismiss button via `onClose`.\n */\nexport function Alert({\n variant = \"info\",\n appearance = \"soft\",\n title,\n description,\n icon,\n onClose,\n closeLabel = \"Dismiss\",\n className,\n children,\n ...props\n}: AlertProps) {\n return (\n <div\n role=\"alert\"\n className={cn(\n styles.alert,\n styles[variant],\n appearance === \"solid\" && styles.solid,\n appearance === \"outline\" && styles.outline,\n className,\n )}\n {...props}\n >\n {icon && <span className={styles.icon}>{icon}</span>}\n <div className={styles.content}>\n {title && <p className={styles.title}>{title}</p>}\n {description && <p className={styles.description}>{description}</p>}\n {children}\n </div>\n {onClose && (\n <button\n type=\"button\"\n className={styles.close}\n onClick={onClose}\n aria-label={closeLabel}\n >\n <CloseIcon />\n </button>\n )}\n </div>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n <path\n d=\"M6 6l12 12M18 6L6 18\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;AAyBA,SAAgB,EAAM,EAClB,aAAU,QACV,gBAAa,QACb,UACA,gBACA,SACA,YACA,gBAAa,WACb,cACA,aACA,GAAG,KACQ;CACX,OACI,kBAAC,OAAD;EACI,MAAK;EACL,WAAW,EACP,EAAO,OACP,EAAO,IACP,MAAe,WAAW,EAAO,OACjC,MAAe,aAAa,EAAO,SACnC,CACJ;EACA,GAAI;EATR,UAAA;GAWK,KAAQ,kBAAC,QAAD;IAAM,WAAW,EAAO;IAAO,UAAA;GAAW,CAAA;GACnD,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA;KACK,KAAS,kBAAC,KAAD;MAAG,WAAW,EAAO;MAAQ,UAAA;KAAS,CAAA;KAC/C,KAAe,kBAAC,KAAD;MAAG,WAAW,EAAO;MAAc,UAAA;KAAe,CAAA;KACjE;IACA;;GACJ,KACG,kBAAC,UAAD;IACI,MAAK;IACL,WAAW,EAAO;IAClB,SAAS;IACT,cAAY;IAEZ,UAAA,kBAAC,GAAD,CAAY,CAAA;GACR,CAAA;EAEX;;AAEb;AAEA,SAAS,IAAY;CACjB,OACI,kBAAC,OAAD;EAAK,OAAM;EAAK,QAAO;EAAK,SAAQ;EAAY,MAAK;EAAO,eAAA;EACxD,UAAA,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACjB,CAAA;CACA,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppBar.cjs","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./AppBar.module.css\";\n\nexport type AppBarTone = \"surface\" | \"primary\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n /** Stick to the top of the scroll container. Default `true`. */\n sticky?: boolean;\n /** Visual tone. Default `\"surface\"`. */\n tone?: AppBarTone;\n /** Thin bottom border. Default `true`. */\n bordered?: boolean;\n /** Add `env(safe-area-inset-top)` padding (iOS notch / PWA). Default `true`. */\n safeArea?: boolean;\n}\n\n/**\n * Mobile-first top app bar for PWAs — leading (back / brand) + title +\n * trailing actions, sticky with safe-area padding out of the box.\n *\n * Consumers customise via tokens (`--tempest-*`) and slots; the SDK ships the\n * layout, sticky/safe-area behaviour, and accessible back button so apps don't\n * hand-roll one per screen. For a desktop three-slot nav use [[Navbar]].\n *\n * @example\n * // Detail screen with back + a trailing action\n * <AppBar\n * title=\"Profile\"\n * showBack\n * onBack={() => navigate(-1)}\n * actions={<IconButton icon={<Settings />} onClick={openSettings} />}\n * />\n *\n * @example\n * // Home screen — brand left, centered title disabled\n * <AppBar brand={<Logo />} actions={<UserMenu />} />\n */\nexport function AppBar({\n title,\n leading,\n showBack = false,\n onBack,\n backLabel = \"Go back\",\n backIcon,\n brand,\n actions,\n centered = false,\n sticky = true,\n tone = \"surface\",\n bordered = true,\n safeArea = true,\n className,\n ...props\n}: AppBarProps) {\n const handleBack = onBack ?? (() => window.history.back());\n\n const leadingContent = leading ?? (\n <>\n {showBack && (\n <button\n type=\"button\"\n className={styles.iconBtn}\n onClick={handleBack}\n aria-label={backLabel}\n >\n {backIcon ?? <ArrowLeft size={22} aria-hidden=\"true\" />}\n </button>\n )}\n {brand && <span className={styles.brand}>{brand}</span>}\n </>\n );\n\n return (\n <header\n className={cn(\n styles.bar,\n styles[tone],\n centered && styles.centered,\n sticky && styles.sticky,\n bordered && styles.bordered,\n safeArea && styles.safeArea,\n className,\n )}\n {...props}\n >\n <div className={styles.leading}>{leadingContent}</div>\n {title != null && title !== \"\" && (\n <h1 className={styles.title} aria-live=\"polite\">\n {title}\n </h1>\n )}\n <div className={styles.actions}>{actions}</div>\n </header>\n );\n}\n"],"mappings":"oIAuEA,SAAgB,EAAO,CACnB,QACA,UACA,WAAW,GACX,SACA,YAAY,UACZ,WACA,QACA,UACA,WAAW,GACX,SAAS,GACT,OAAO,UACP,WAAW,GACX,WAAW,GACX,YACA,GAAG,GACS,CACZ,IAAM,EAAa,QAAiB,OAAO,QAAQ,KAAK,GAElD,EAAiB,IACnB,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,CACK,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,QAAS,EACT,aAAY,
|
|
1
|
+
{"version":3,"file":"AppBar.cjs","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./AppBar.module.css\";\n\nexport type AppBarTone = \"surface\" | \"primary\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n /** Stick to the top of the scroll container. Default `true`. */\n sticky?: boolean;\n /** Visual tone. Default `\"surface\"`. */\n tone?: AppBarTone;\n /** Thin bottom border. Default `true`. */\n bordered?: boolean;\n /** Add `env(safe-area-inset-top)` padding (iOS notch / PWA). Default `true`. */\n safeArea?: boolean;\n}\n\n/**\n * Mobile-first top app bar for PWAs — leading (back / brand) + title +\n * trailing actions, sticky with safe-area padding out of the box.\n *\n * Consumers customise via tokens (`--tempest-*`) and slots; the SDK ships the\n * layout, sticky/safe-area behaviour, and accessible back button so apps don't\n * hand-roll one per screen. For a desktop three-slot nav use [[Navbar]].\n *\n * @example\n * // Detail screen with back + a trailing action\n * <AppBar\n * title=\"Profile\"\n * showBack\n * onBack={() => navigate(-1)}\n * actions={<IconButton icon={<Settings />} onClick={openSettings} />}\n * />\n *\n * @example\n * // Home screen — brand left, centered title disabled\n * <AppBar brand={<Logo />} actions={<UserMenu />} />\n */\nexport function AppBar({\n title,\n leading,\n showBack = false,\n onBack,\n backLabel = \"Go back\",\n backIcon,\n brand,\n actions,\n centered = false,\n sticky = true,\n tone = \"surface\",\n bordered = true,\n safeArea = true,\n className,\n ...props\n}: AppBarProps) {\n const handleBack = onBack ?? (() => window.history.back());\n\n const leadingContent = leading ?? (\n <>\n {showBack && (\n <button\n type=\"button\"\n className={styles.iconBtn}\n onClick={handleBack}\n aria-label={backLabel}\n >\n {backIcon ?? <ArrowLeft size={22} aria-hidden=\"true\" />}\n </button>\n )}\n {brand && <span className={styles.brand}>{brand}</span>}\n </>\n );\n\n return (\n <header\n className={cn(\n styles.bar,\n styles[tone],\n centered && styles.centered,\n sticky && styles.sticky,\n bordered && styles.bordered,\n safeArea && styles.safeArea,\n className,\n )}\n {...props}\n >\n <div className={styles.leading}>{leadingContent}</div>\n {title != null && title !== \"\" && (\n <h1 className={styles.title} aria-live=\"polite\">\n {title}\n </h1>\n )}\n <div className={styles.actions}>{actions}</div>\n </header>\n );\n}\n"],"mappings":"oIAuEA,SAAgB,EAAO,CACnB,QACA,UACA,WAAW,GACX,SACA,YAAY,UACZ,WACA,QACA,UACA,WAAW,GACX,SAAS,GACT,OAAO,UACP,WAAW,GACX,WAAW,GACX,YACA,GAAG,GACS,CACZ,IAAM,EAAa,QAAiB,OAAO,QAAQ,KAAK,GAElD,EAAiB,IACnB,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,CACK,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,QAAS,EACT,aAAY,EAEX,SAAA,IAAY,EAAA,EAAA,IAAA,CAAC,EAAA,UAAD,CAAW,KAAM,GAAI,cAAY,MAAQ,CAAA,CAClD,CAAA,EAEX,IAAS,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAY,CAAA,CACxD,CAAA,CAAA,EAGN,OACI,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,IACP,EAAA,QAAO,GACP,GAAY,EAAA,QAAO,SACnB,GAAU,EAAA,QAAO,OACjB,GAAY,EAAA,QAAO,SACnB,GAAY,EAAA,QAAO,SACnB,CACJ,EACA,GAAI,EAVR,SAAA,EAYI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,SAAA,CAAoB,CAAA,EACpD,GAAS,MAAQ,IAAU,KACxB,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MAAO,YAAU,SAClC,SAAA,CACD,CAAA,GAER,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,SAAA,CAAa,CAAA,CAC1C,GAEhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppBar.js","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./AppBar.module.css\";\n\nexport type AppBarTone = \"surface\" | \"primary\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n /** Stick to the top of the scroll container. Default `true`. */\n sticky?: boolean;\n /** Visual tone. Default `\"surface\"`. */\n tone?: AppBarTone;\n /** Thin bottom border. Default `true`. */\n bordered?: boolean;\n /** Add `env(safe-area-inset-top)` padding (iOS notch / PWA). Default `true`. */\n safeArea?: boolean;\n}\n\n/**\n * Mobile-first top app bar for PWAs — leading (back / brand) + title +\n * trailing actions, sticky with safe-area padding out of the box.\n *\n * Consumers customise via tokens (`--tempest-*`) and slots; the SDK ships the\n * layout, sticky/safe-area behaviour, and accessible back button so apps don't\n * hand-roll one per screen. For a desktop three-slot nav use [[Navbar]].\n *\n * @example\n * // Detail screen with back + a trailing action\n * <AppBar\n * title=\"Profile\"\n * showBack\n * onBack={() => navigate(-1)}\n * actions={<IconButton icon={<Settings />} onClick={openSettings} />}\n * />\n *\n * @example\n * // Home screen — brand left, centered title disabled\n * <AppBar brand={<Logo />} actions={<UserMenu />} />\n */\nexport function AppBar({\n title,\n leading,\n showBack = false,\n onBack,\n backLabel = \"Go back\",\n backIcon,\n brand,\n actions,\n centered = false,\n sticky = true,\n tone = \"surface\",\n bordered = true,\n safeArea = true,\n className,\n ...props\n}: AppBarProps) {\n const handleBack = onBack ?? (() => window.history.back());\n\n const leadingContent = leading ?? (\n <>\n {showBack && (\n <button\n type=\"button\"\n className={styles.iconBtn}\n onClick={handleBack}\n aria-label={backLabel}\n >\n {backIcon ?? <ArrowLeft size={22} aria-hidden=\"true\" />}\n </button>\n )}\n {brand && <span className={styles.brand}>{brand}</span>}\n </>\n );\n\n return (\n <header\n className={cn(\n styles.bar,\n styles[tone],\n centered && styles.centered,\n sticky && styles.sticky,\n bordered && styles.bordered,\n safeArea && styles.safeArea,\n className,\n )}\n {...props}\n >\n <div className={styles.leading}>{leadingContent}</div>\n {title != null && title !== \"\" && (\n <h1 className={styles.title} aria-live=\"polite\">\n {title}\n </h1>\n )}\n <div className={styles.actions}>{actions}</div>\n </header>\n );\n}\n"],"mappings":";;;;;AAuEA,SAAgB,EAAO,EACnB,UACA,YACA,cAAW,IACX,WACA,eAAY,WACZ,aACA,UACA,YACA,cAAW,IACX,YAAS,IACT,UAAO,WACP,cAAW,IACX,cAAW,IACX,cACA,GAAG,KACS;CACZ,IAAM,IAAa,YAAiB,OAAO,QAAQ,KAAK,IAElD,IAAiB,KACnB,kBAAA,GAAA,EAAA,UAAA,CACK,KACG,kBAAC,UAAD;EACI,MAAK;EACL,WAAW,EAAO;EAClB,SAAS;EACT,cAAY;
|
|
1
|
+
{"version":3,"file":"AppBar.js","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./AppBar.module.css\";\n\nexport type AppBarTone = \"surface\" | \"primary\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n /** Stick to the top of the scroll container. Default `true`. */\n sticky?: boolean;\n /** Visual tone. Default `\"surface\"`. */\n tone?: AppBarTone;\n /** Thin bottom border. Default `true`. */\n bordered?: boolean;\n /** Add `env(safe-area-inset-top)` padding (iOS notch / PWA). Default `true`. */\n safeArea?: boolean;\n}\n\n/**\n * Mobile-first top app bar for PWAs — leading (back / brand) + title +\n * trailing actions, sticky with safe-area padding out of the box.\n *\n * Consumers customise via tokens (`--tempest-*`) and slots; the SDK ships the\n * layout, sticky/safe-area behaviour, and accessible back button so apps don't\n * hand-roll one per screen. For a desktop three-slot nav use [[Navbar]].\n *\n * @example\n * // Detail screen with back + a trailing action\n * <AppBar\n * title=\"Profile\"\n * showBack\n * onBack={() => navigate(-1)}\n * actions={<IconButton icon={<Settings />} onClick={openSettings} />}\n * />\n *\n * @example\n * // Home screen — brand left, centered title disabled\n * <AppBar brand={<Logo />} actions={<UserMenu />} />\n */\nexport function AppBar({\n title,\n leading,\n showBack = false,\n onBack,\n backLabel = \"Go back\",\n backIcon,\n brand,\n actions,\n centered = false,\n sticky = true,\n tone = \"surface\",\n bordered = true,\n safeArea = true,\n className,\n ...props\n}: AppBarProps) {\n const handleBack = onBack ?? (() => window.history.back());\n\n const leadingContent = leading ?? (\n <>\n {showBack && (\n <button\n type=\"button\"\n className={styles.iconBtn}\n onClick={handleBack}\n aria-label={backLabel}\n >\n {backIcon ?? <ArrowLeft size={22} aria-hidden=\"true\" />}\n </button>\n )}\n {brand && <span className={styles.brand}>{brand}</span>}\n </>\n );\n\n return (\n <header\n className={cn(\n styles.bar,\n styles[tone],\n centered && styles.centered,\n sticky && styles.sticky,\n bordered && styles.bordered,\n safeArea && styles.safeArea,\n className,\n )}\n {...props}\n >\n <div className={styles.leading}>{leadingContent}</div>\n {title != null && title !== \"\" && (\n <h1 className={styles.title} aria-live=\"polite\">\n {title}\n </h1>\n )}\n <div className={styles.actions}>{actions}</div>\n </header>\n );\n}\n"],"mappings":";;;;;AAuEA,SAAgB,EAAO,EACnB,UACA,YACA,cAAW,IACX,WACA,eAAY,WACZ,aACA,UACA,YACA,cAAW,IACX,YAAS,IACT,UAAO,WACP,cAAW,IACX,cAAW,IACX,cACA,GAAG,KACS;CACZ,IAAM,IAAa,YAAiB,OAAO,QAAQ,KAAK,IAElD,IAAiB,KACnB,kBAAA,GAAA,EAAA,UAAA,CACK,KACG,kBAAC,UAAD;EACI,MAAK;EACL,WAAW,EAAO;EAClB,SAAS;EACT,cAAY;EAEX,UAAA,KAAY,kBAAC,GAAD;GAAW,MAAM;GAAI,eAAY;EAAQ,CAAA;CAClD,CAAA,GAEX,KAAS,kBAAC,QAAD;EAAM,WAAW,EAAO;EAAQ,UAAA;CAAY,CAAA,CACxD,EAAA,CAAA;CAGN,OACI,kBAAC,UAAD;EACI,WAAW,EACP,EAAO,KACP,EAAO,IACP,KAAY,EAAO,UACnB,KAAU,EAAO,QACjB,KAAY,EAAO,UACnB,KAAY,EAAO,UACnB,CACJ;EACA,GAAI;EAVR,UAAA;GAYI,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAU,UAAA;GAAoB,CAAA;GACpD,KAAS,QAAQ,MAAU,MACxB,kBAAC,MAAD;IAAI,WAAW,EAAO;IAAO,aAAU;IAClC,UAAA;GACD,CAAA;GAER,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAU,UAAA;GAAa,CAAA;EAC1C;;AAEhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppShell.cjs","names":[],"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { useBreakpoint } from \"@/hooks/use-breakpoint\";\nimport styles from \"./AppShell.module.css\";\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Top navigation bar (Navbar / custom). Renders on every breakpoint. */\n navbar?: ReactNode;\n /** Side navigation (Sidebar). Hidden below `md` — pair with `<Drawer>` for mobile. */\n sidebar?: ReactNode;\n /** Mobile-only bottom navigation (BottomNavigation). Hidden at `md+`. */\n bottomNav?: ReactNode;\n /** Footer rendered after the main content. */\n footer?: ReactNode;\n /** Breakpoint at which the sidebar appears. Default `\"md\"`. */\n sidebarBreakpoint?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n children?: ReactNode;\n}\n\n/**\n * Full app layout — composes [[Navbar]] + [[Sidebar]] + content +\n * [[BottomNavigation]] + footer with responsive behaviour:\n *\n * - **Desktop (`>= sidebarBreakpoint`)**: navbar + sidebar + main + footer.\n * - **Mobile (`< sidebarBreakpoint`)**: navbar + main + bottom nav + footer\n * (sidebar hidden — app should expose it via `<Drawer>` from a hamburger).\n *\n * @example\n * <AppShell\n * navbar={<Navbar logo={<Brand />} actions={<UserMenu />} />}\n * sidebar={<Sidebar items={...} value={tab} onChange={setTab} />}\n * bottomNav={<BottomNavigation items={...} value={tab} onChange={setTab} />}\n * footer={<Footer />}\n * >\n * <Page title=\"Dashboard\">{content}</Page>\n * </AppShell>\n */\nexport function AppShell({\n navbar,\n sidebar,\n bottomNav,\n footer,\n sidebarBreakpoint = \"md\",\n className,\n children,\n ...props\n}: AppShellProps) {\n const bp = useBreakpoint();\n const showSidebar = bp.above(sidebarBreakpoint);\n return (\n <div className={cn(styles.shell, className)} {...props}>\n {navbar && <div className={styles.navbar}>{navbar}</div>}\n <div className={styles.row}>\n {sidebar && showSidebar && <div className={styles.sidebar}>{sidebar}</div>}\n <main className={styles.main}>{children}</main>\n </div>\n {footer && <div className={styles.footer}>{footer}</div>}\n {bottomNav && !showSidebar && <div className={styles.bottomNav}>{bottomNav}</div>}\n </div>\n );\n}\n"],"mappings":"wJAqCA,SAAgB,EAAS,CACrB,SACA,UACA,YACA,SACA,oBAAoB,KACpB,YACA,WACA,GAAG,GACW,CAEd,IAAM,EADK,EAAA,cACS,CAAA,CAAG,MAAM,CAAiB,EAC9C,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,CAAS,EAAG,GAAI,
|
|
1
|
+
{"version":3,"file":"AppShell.cjs","names":[],"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { useBreakpoint } from \"@/hooks/use-breakpoint\";\nimport styles from \"./AppShell.module.css\";\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Top navigation bar (Navbar / custom). Renders on every breakpoint. */\n navbar?: ReactNode;\n /** Side navigation (Sidebar). Hidden below `md` — pair with `<Drawer>` for mobile. */\n sidebar?: ReactNode;\n /** Mobile-only bottom navigation (BottomNavigation). Hidden at `md+`. */\n bottomNav?: ReactNode;\n /** Footer rendered after the main content. */\n footer?: ReactNode;\n /** Breakpoint at which the sidebar appears. Default `\"md\"`. */\n sidebarBreakpoint?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n children?: ReactNode;\n}\n\n/**\n * Full app layout — composes [[Navbar]] + [[Sidebar]] + content +\n * [[BottomNavigation]] + footer with responsive behaviour:\n *\n * - **Desktop (`>= sidebarBreakpoint`)**: navbar + sidebar + main + footer.\n * - **Mobile (`< sidebarBreakpoint`)**: navbar + main + bottom nav + footer\n * (sidebar hidden — app should expose it via `<Drawer>` from a hamburger).\n *\n * @example\n * <AppShell\n * navbar={<Navbar logo={<Brand />} actions={<UserMenu />} />}\n * sidebar={<Sidebar items={...} value={tab} onChange={setTab} />}\n * bottomNav={<BottomNavigation items={...} value={tab} onChange={setTab} />}\n * footer={<Footer />}\n * >\n * <Page title=\"Dashboard\">{content}</Page>\n * </AppShell>\n */\nexport function AppShell({\n navbar,\n sidebar,\n bottomNav,\n footer,\n sidebarBreakpoint = \"md\",\n className,\n children,\n ...props\n}: AppShellProps) {\n const bp = useBreakpoint();\n const showSidebar = bp.above(sidebarBreakpoint);\n return (\n <div className={cn(styles.shell, className)} {...props}>\n {navbar && <div className={styles.navbar}>{navbar}</div>}\n <div className={styles.row}>\n {sidebar && showSidebar && <div className={styles.sidebar}>{sidebar}</div>}\n <main className={styles.main}>{children}</main>\n </div>\n {footer && <div className={styles.footer}>{footer}</div>}\n {bottomNav && !showSidebar && <div className={styles.bottomNav}>{bottomNav}</div>}\n </div>\n );\n}\n"],"mappings":"wJAqCA,SAAgB,EAAS,CACrB,SACA,UACA,YACA,SACA,oBAAoB,KACpB,YACA,WACA,GAAG,GACW,CAEd,IAAM,EADK,EAAA,cACS,CAAA,CAAG,MAAM,CAAiB,EAC9C,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,CAAS,EAAG,GAAI,EAAjD,SAAA,CACK,IAAU,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAS,SAAA,CAAY,CAAA,GACvD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,IAAvB,SAAA,CACK,GAAW,IAAe,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,SAAA,CAAa,CAAA,GACzE,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAO,UAAe,CAAA,CAC7C,IACJ,IAAU,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAS,SAAA,CAAY,CAAA,EACtD,GAAa,CAAC,IAAe,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,UAAY,SAAA,CAAe,CAAA,CAC/E,GAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppShell.js","names":[],"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { useBreakpoint } from \"@/hooks/use-breakpoint\";\nimport styles from \"./AppShell.module.css\";\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Top navigation bar (Navbar / custom). Renders on every breakpoint. */\n navbar?: ReactNode;\n /** Side navigation (Sidebar). Hidden below `md` — pair with `<Drawer>` for mobile. */\n sidebar?: ReactNode;\n /** Mobile-only bottom navigation (BottomNavigation). Hidden at `md+`. */\n bottomNav?: ReactNode;\n /** Footer rendered after the main content. */\n footer?: ReactNode;\n /** Breakpoint at which the sidebar appears. Default `\"md\"`. */\n sidebarBreakpoint?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n children?: ReactNode;\n}\n\n/**\n * Full app layout — composes [[Navbar]] + [[Sidebar]] + content +\n * [[BottomNavigation]] + footer with responsive behaviour:\n *\n * - **Desktop (`>= sidebarBreakpoint`)**: navbar + sidebar + main + footer.\n * - **Mobile (`< sidebarBreakpoint`)**: navbar + main + bottom nav + footer\n * (sidebar hidden — app should expose it via `<Drawer>` from a hamburger).\n *\n * @example\n * <AppShell\n * navbar={<Navbar logo={<Brand />} actions={<UserMenu />} />}\n * sidebar={<Sidebar items={...} value={tab} onChange={setTab} />}\n * bottomNav={<BottomNavigation items={...} value={tab} onChange={setTab} />}\n * footer={<Footer />}\n * >\n * <Page title=\"Dashboard\">{content}</Page>\n * </AppShell>\n */\nexport function AppShell({\n navbar,\n sidebar,\n bottomNav,\n footer,\n sidebarBreakpoint = \"md\",\n className,\n children,\n ...props\n}: AppShellProps) {\n const bp = useBreakpoint();\n const showSidebar = bp.above(sidebarBreakpoint);\n return (\n <div className={cn(styles.shell, className)} {...props}>\n {navbar && <div className={styles.navbar}>{navbar}</div>}\n <div className={styles.row}>\n {sidebar && showSidebar && <div className={styles.sidebar}>{sidebar}</div>}\n <main className={styles.main}>{children}</main>\n </div>\n {footer && <div className={styles.footer}>{footer}</div>}\n {bottomNav && !showSidebar && <div className={styles.bottomNav}>{bottomNav}</div>}\n </div>\n );\n}\n"],"mappings":";;;;;AAqCA,SAAgB,EAAS,EACrB,WACA,YACA,cACA,WACA,uBAAoB,MACpB,cACA,aACA,GAAG,KACW;CAEd,IAAM,IADK,EACS,CAAA,CAAG,MAAM,CAAiB;CAC9C,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,OAAO,CAAS;EAAG,GAAI;
|
|
1
|
+
{"version":3,"file":"AppShell.js","names":[],"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { useBreakpoint } from \"@/hooks/use-breakpoint\";\nimport styles from \"./AppShell.module.css\";\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Top navigation bar (Navbar / custom). Renders on every breakpoint. */\n navbar?: ReactNode;\n /** Side navigation (Sidebar). Hidden below `md` — pair with `<Drawer>` for mobile. */\n sidebar?: ReactNode;\n /** Mobile-only bottom navigation (BottomNavigation). Hidden at `md+`. */\n bottomNav?: ReactNode;\n /** Footer rendered after the main content. */\n footer?: ReactNode;\n /** Breakpoint at which the sidebar appears. Default `\"md\"`. */\n sidebarBreakpoint?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n children?: ReactNode;\n}\n\n/**\n * Full app layout — composes [[Navbar]] + [[Sidebar]] + content +\n * [[BottomNavigation]] + footer with responsive behaviour:\n *\n * - **Desktop (`>= sidebarBreakpoint`)**: navbar + sidebar + main + footer.\n * - **Mobile (`< sidebarBreakpoint`)**: navbar + main + bottom nav + footer\n * (sidebar hidden — app should expose it via `<Drawer>` from a hamburger).\n *\n * @example\n * <AppShell\n * navbar={<Navbar logo={<Brand />} actions={<UserMenu />} />}\n * sidebar={<Sidebar items={...} value={tab} onChange={setTab} />}\n * bottomNav={<BottomNavigation items={...} value={tab} onChange={setTab} />}\n * footer={<Footer />}\n * >\n * <Page title=\"Dashboard\">{content}</Page>\n * </AppShell>\n */\nexport function AppShell({\n navbar,\n sidebar,\n bottomNav,\n footer,\n sidebarBreakpoint = \"md\",\n className,\n children,\n ...props\n}: AppShellProps) {\n const bp = useBreakpoint();\n const showSidebar = bp.above(sidebarBreakpoint);\n return (\n <div className={cn(styles.shell, className)} {...props}>\n {navbar && <div className={styles.navbar}>{navbar}</div>}\n <div className={styles.row}>\n {sidebar && showSidebar && <div className={styles.sidebar}>{sidebar}</div>}\n <main className={styles.main}>{children}</main>\n </div>\n {footer && <div className={styles.footer}>{footer}</div>}\n {bottomNav && !showSidebar && <div className={styles.bottomNav}>{bottomNav}</div>}\n </div>\n );\n}\n"],"mappings":";;;;;AAqCA,SAAgB,EAAS,EACrB,WACA,YACA,cACA,WACA,uBAAoB,MACpB,cACA,aACA,GAAG,KACW;CAEd,IAAM,IADK,EACS,CAAA,CAAG,MAAM,CAAiB;CAC9C,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,OAAO,CAAS;EAAG,GAAI;EAAjD,UAAA;GACK,KAAU,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAS,UAAA;GAAY,CAAA;GACvD,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA,CACK,KAAW,KAAe,kBAAC,OAAD;KAAK,WAAW,EAAO;KAAU,UAAA;IAAa,CAAA,GACzE,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAO;IAAe,CAAA,CAC7C;;GACJ,KAAU,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAS,UAAA;GAAY,CAAA;GACtD,KAAa,CAAC,KAAe,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAY,UAAA;GAAe,CAAA;EAC/E;;AAEb"}
|
|
@@ -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":";;;;;;;;;;;;;AAgEA,IAAM,IAAU;CACZ,SAAS;EACL,QAAQ;EACR,MAAM;EACN,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,aAAa;EACb,UAAU;EACV,WAAW;EACX,QAAQ;EACR,YAAY;CAChB;CACA,IAAI;EACA,QAAQ;EACR,MAAM;EACN,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,aAAa;EACb,UAAU;EACV,WAAW;EACX,QAAQ;EACR,YAAY;CAChB;AACJ;AAyBA,SAAgB,EAAc,EAC1B,eACA,kBACA,aACA,YAAS,UACT,eACA,uBACA,YAAS,IACT,YAAS,SACT,WACA,YACA,cAAW,IACX,cACA,GAAG,KACgB;CACnB,IAAM,IAAU,EAAQ,IAClB,IAAa,EAAmB,YAAY,GAC5C,IAAM,EAAc,EAAE,YAAS,CAAC,GAChC,CAAC,GAAY,KAAiB,EAAS,EAAK,GAC5C,CAAC,GAAQ,KAAa,EAAgC,IAAI,GAC1D,CAAC,GAAW,KAAgB,EAAS,EAAI;CAE/C,QAAgB,EAAa,EAA0B,CAAC,GAAG,CAAC,CAAC;CAU7D,IAAM,IAAS,OAAO,MAA6C;EAC/D,IAAI,MAAW,OAAO;GAElB,AADA,EAAU,CAAS,GACnB,IAAa,CAAS;GACtB;EACJ;EACA,EAAc,EAAI;EAClB,IAAI;GACA,IAAM,IAAO,MAAM,EAAU,EAAU,MAAM,CAAU,GACjD,IAA4B;IAAE,GAAG;IAAW;IAAM,UAAU;GAAY;GAE9E,AADA,EAAU,CAAS,GACnB,IAAa,CAAS;EAC1B,SAAS,GAAO;GAGZ,AAFA,IAAU,CAAK,GACf,EAAU,CAAS,GACnB,IAAa,CAAS;EAC1B,UAAU;GACN,EAAc,EAAK;EACvB;CACJ,GAEM,IAAW,EAAiB,EAAI,QAAQ;EAC1C;EACA;EACA,aAAa,MAAc,KAAK,EAAO,CAAS;EAChD;CACJ,CAAC,GAUK,CAAC,GAAO,KAAY,EAAS,EAAK;CACxC,QAAgB;EACR,CAAC,KAAS,CAAC,EAAS,SAAS,EAAS,WAAW,WACrD,EAAS,EAAK,GACd,EAAS,MAAM;CACnB,GAAG;EAAC;EAAO,EAAS;EAAO,EAAS;EAAQ;CAAQ,CAAC;CAErD,IAAM,UAAoB;EAEtB,IADA,EAAU,IAAI,GACV,EAAI,UAAU,EAAS,OAAO;GAC9B,EAAS,MAAM;GACf;EACJ;EAEA,AADA,EAAS,EAAI,GACb,EAAI,MAAM;CACd,GAEM,UAAqB;EAGvB,AAFA,EAAU,IAAI,GACd,EAAS,OAAO,GAChB,EAAM;CACV,GAEM,IAAO,EAAS,WAAW,eAAe,EAAS,WAAW,UAC9D,IAAU,EAAW,UAAU;CAUrC,OARK,IASD,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,UAAU,CAAS;EAAG,GAAI;EAApD,UAAA;GACI,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA;KACK,CAAC,KACE,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,SAAS,IAAS,IAAS;MAC3B,UAAU,KAAY,KAAW;MACjC,cAAY,IAAS,EAAQ,SAAS,EAAQ;MALlD,UAAA,CAQQ,EADH,IACI,IAEA,GAFD;OAAW,MAAM;OAAI,eAAA;MAAa,CAEN,GAEhC,kBAAC,QAAD,EAAA,UAAO,IAAS,EAAQ,SAAS,EAAQ,OAAa,CAAA,CAClD;;KAGX,KACG,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,eAAe,KAAK,EAAS,KAAK;MAClC,cAAY,EAAQ;MAJxB,UAAA,CAMI,kBAAC,GAAD;OAAQ,MAAM;OAAI,eAAA;MAAa,CAAA,GAC/B,kBAAC,QAAD,EAAA,UAAO,EAAQ,KAAW,CAAA,CACtB;KACR,CAAA,GAAA,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,SACI,EAAS,WAAW,WAAW,EAAS,SAAS,EAAS;MAE9D,cACI,EAAS,WAAW,WAAW,EAAQ,SAAS,EAAQ;MAG3D,UAAA,EAAS,WAAW,WACjB,kBAAC,GAAD;OAAM,MAAM;OAAI,eAAA;MAAa,CAAA,IAE7B,kBAAC,GAAD;OAAO,MAAM;OAAI,eAAA;MAAa,CAAA;KAE9B,CAAA,CACV,EAAA,CAAA;KAGL,KACG,kBAAC,OAAD;MACI,WAAW,EAAO;MAClB,MAAK;MACL,cAAY,EAAQ;MACpB,iBAAe;MACf,iBAAe;MACf,iBAAe,KAAK,MAAM,EAAS,QAAQ,GAAG;MAE9C,UAAA,kBAAC,QAAD;OACI,WAAW,EAAO;OAClB,OAAO,EAAE,WAAW,UAAU,EAAS,MAAM,GAAG;MACnD,CAAA;KACA,CAAA;KAGT,kBAAC,QAAD;MAAM,WAAW,EAAO;MAAxB,UAAA,CACK,EAAe,IAAO,EAAS,aAAc,GAAQ,cAAc,CAAE,GACrE,MAAkB,KAAA,KACf,kBAAC,QAAD;OAAM,WAAW,EAAO;OAAxB,UAAA,CAA6B,OAAI,EAAe,CAAa,CAAQ;MAEvE,CAAA,CAAA;;IACL;;GAEL,kBAAC,KAAD;IAAG,WAAW,EAAO;IAAQ,MAAK;IAC7B,UAAA,IACK,EAAQ,aACR,EAAI,WAAW,eACb,EAAQ,SACR,IACE,EAAQ,SACR,EAAI,QACF,EAAI,MAAM,UACV,EAAS,WAAW,cAClB,EAAQ,YACR,EAAS,WAAW,WAClB,EAAQ,SACR,IACE,KACA,EAAQ;GAC3B,CAAA;GAEF,KAAU,KACP,kBAAC,GAAD;IACI,KAAK,EAAO;IACZ,YAAY,EAAO;IACX;IACC;GACZ,CAAA;GAGJ,KAAU,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAS,UAAA;GAAY,CAAA;EACtD;MA5GD,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,UAAU,CAAS;EAAG,GAAI;EAChD,UAAA,kBAAC,KAAD;GAAG,WAAW,EAAO;GAAS,UAAA,EAAQ;EAAe,CAAA;CACpD,CAAA;AA4GjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"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"}
|