tempest-react-sdk 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -6
- package/dist/access/Can.cjs +1 -1
- package/dist/access/Can.cjs.map +1 -1
- package/dist/access/Can.js +3 -3
- package/dist/access/Can.js.map +1 -1
- package/dist/audio/level-meter.cjs +1 -1
- package/dist/audio/level-meter.cjs.map +1 -1
- package/dist/audio/level-meter.js +15 -15
- package/dist/audio/level-meter.js.map +1 -1
- package/dist/audio/noise-floor.cjs +2 -0
- package/dist/audio/noise-floor.cjs.map +1 -0
- package/dist/audio/noise-floor.js +27 -0
- package/dist/audio/noise-floor.js.map +1 -0
- package/dist/audio/voice-activity.cjs +2 -0
- package/dist/audio/voice-activity.cjs.map +1 -0
- package/dist/audio/voice-activity.js +26 -0
- package/dist/audio/voice-activity.js.map +1 -0
- package/dist/audio/voice-chain-stages.cjs +2 -0
- package/dist/audio/voice-chain-stages.cjs.map +1 -0
- package/dist/audio/voice-chain-stages.js +82 -0
- package/dist/audio/voice-chain-stages.js.map +1 -0
- package/dist/audio/voice-chain.cjs +2 -0
- package/dist/audio/voice-chain.cjs.map +1 -0
- package/dist/audio/voice-chain.js +74 -0
- package/dist/audio/voice-chain.js.map +1 -0
- package/dist/auth/lazy-with-retry.cjs.map +1 -1
- package/dist/auth/lazy-with-retry.js.map +1 -1
- package/dist/auth/passkey.cjs.map +1 -1
- package/dist/auth/passkey.js.map +1 -1
- package/dist/br/BrazilStateCitySelect.cjs +1 -1
- package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
- package/dist/br/BrazilStateCitySelect.js +36 -28
- package/dist/br/BrazilStateCitySelect.js.map +1 -1
- package/dist/br/data/br-centroids.cjs +1 -1
- package/dist/br/data/br-centroids.cjs.map +1 -1
- package/dist/br/data/br-centroids.js +11105 -11045
- package/dist/br/data/br-centroids.js.map +1 -1
- package/dist/br/data/br-locations.cjs +1 -1
- package/dist/br/data/br-locations.cjs.map +1 -1
- package/dist/br/data/br-locations.js +5803 -137
- package/dist/br/data/br-locations.js.map +1 -1
- package/dist/br/data/br-uf-geo.cjs +1 -1
- package/dist/br/data/br-uf-geo.cjs.map +1 -1
- package/dist/br/data/br-uf-geo.js +5721 -6777
- package/dist/br/data/br-uf-geo.js.map +1 -1
- package/dist/br/data/mun/AC.cjs +1 -1
- package/dist/br/data/mun/AC.cjs.map +1 -1
- package/dist/br/data/mun/AC.js +681 -689
- package/dist/br/data/mun/AC.js.map +1 -1
- package/dist/br/data/mun/AL.cjs +1 -1
- package/dist/br/data/mun/AL.cjs.map +1 -1
- package/dist/br/data/mun/AL.js +949 -844
- package/dist/br/data/mun/AL.js.map +1 -1
- package/dist/br/data/mun/AM.cjs +1 -1
- package/dist/br/data/mun/AM.cjs.map +1 -1
- package/dist/br/data/mun/AM.js +4337 -4218
- package/dist/br/data/mun/AM.js.map +1 -1
- package/dist/br/data/mun/AP.cjs +1 -1
- package/dist/br/data/mun/AP.cjs.map +1 -1
- package/dist/br/data/mun/AP.js +786 -780
- package/dist/br/data/mun/AP.js.map +1 -1
- package/dist/br/data/mun/BA.cjs +1 -1
- package/dist/br/data/mun/BA.cjs.map +1 -1
- package/dist/br/data/mun/BA.js +6329 -5633
- package/dist/br/data/mun/BA.js.map +1 -1
- package/dist/br/data/mun/CE.cjs +1 -1
- package/dist/br/data/mun/CE.cjs.map +1 -1
- package/dist/br/data/mun/CE.js +2544 -2487
- package/dist/br/data/mun/CE.js.map +1 -1
- package/dist/br/data/mun/DF.cjs +1 -1
- package/dist/br/data/mun/DF.cjs.map +1 -1
- package/dist/br/data/mun/DF.js +17 -16
- package/dist/br/data/mun/DF.js.map +1 -1
- package/dist/br/data/mun/ES.cjs +1 -1
- package/dist/br/data/mun/ES.cjs.map +1 -1
- package/dist/br/data/mun/ES.js +1101 -1070
- package/dist/br/data/mun/ES.js.map +1 -1
- package/dist/br/data/mun/GO.cjs +1 -1
- package/dist/br/data/mun/GO.cjs.map +1 -1
- package/dist/br/data/mun/GO.js +4632 -4636
- package/dist/br/data/mun/GO.js.map +1 -1
- package/dist/br/data/mun/MA.cjs +1 -1
- package/dist/br/data/mun/MA.cjs.map +1 -1
- package/dist/br/data/mun/MA.js +3603 -3457
- package/dist/br/data/mun/MA.js.map +1 -1
- package/dist/br/data/mun/MG.cjs +1 -1
- package/dist/br/data/mun/MG.cjs.map +1 -1
- package/dist/br/data/mun/MG.js +12716 -11739
- package/dist/br/data/mun/MG.js.map +1 -1
- package/dist/br/data/mun/MS.cjs +1 -1
- package/dist/br/data/mun/MS.cjs.map +1 -1
- package/dist/br/data/mun/MS.js +2182 -2335
- package/dist/br/data/mun/MS.js.map +1 -1
- package/dist/br/data/mun/MT.cjs +1 -1
- package/dist/br/data/mun/MT.cjs.map +1 -1
- package/dist/br/data/mun/MT.js +4808 -4896
- package/dist/br/data/mun/MT.js.map +1 -1
- package/dist/br/data/mun/PA.cjs +1 -1
- package/dist/br/data/mun/PA.cjs.map +1 -1
- package/dist/br/data/mun/PA.js +4981 -4901
- package/dist/br/data/mun/PA.js.map +1 -1
- package/dist/br/data/mun/PB.cjs +1 -1
- package/dist/br/data/mun/PB.cjs.map +1 -1
- package/dist/br/data/mun/PB.js +2418 -1956
- package/dist/br/data/mun/PB.js.map +1 -1
- package/dist/br/data/mun/PE.cjs +1 -1
- package/dist/br/data/mun/PE.cjs.map +1 -1
- package/dist/br/data/mun/PE.js +2164 -1987
- package/dist/br/data/mun/PE.js.map +1 -1
- package/dist/br/data/mun/PI.cjs +1 -1
- package/dist/br/data/mun/PI.cjs.map +1 -1
- package/dist/br/data/mun/PI.js +2982 -2844
- package/dist/br/data/mun/PI.js.map +1 -1
- package/dist/br/data/mun/PR.cjs +1 -1
- package/dist/br/data/mun/PR.cjs.map +1 -1
- package/dist/br/data/mun/PR.js +5425 -5240
- package/dist/br/data/mun/PR.js.map +1 -1
- package/dist/br/data/mun/RJ.cjs +1 -1
- package/dist/br/data/mun/RJ.cjs.map +1 -1
- package/dist/br/data/mun/RJ.js +1395 -1178
- package/dist/br/data/mun/RJ.js.map +1 -1
- package/dist/br/data/mun/RN.cjs +1 -1
- package/dist/br/data/mun/RN.cjs.map +1 -1
- package/dist/br/data/mun/RN.js +1608 -1382
- package/dist/br/data/mun/RN.js.map +1 -1
- package/dist/br/data/mun/RO.cjs +1 -1
- package/dist/br/data/mun/RO.cjs.map +1 -1
- package/dist/br/data/mun/RO.js +1392 -1470
- package/dist/br/data/mun/RO.js.map +1 -1
- package/dist/br/data/mun/RR.cjs +1 -1
- package/dist/br/data/mun/RR.cjs.map +1 -1
- package/dist/br/data/mun/RR.js +795 -778
- package/dist/br/data/mun/RR.js.map +1 -1
- package/dist/br/data/mun/RS.cjs +1 -1
- package/dist/br/data/mun/RS.cjs.map +1 -1
- package/dist/br/data/mun/RS.js +7579 -6424
- package/dist/br/data/mun/RS.js.map +1 -1
- package/dist/br/data/mun/SC.cjs +1 -1
- package/dist/br/data/mun/SC.cjs.map +1 -1
- package/dist/br/data/mun/SC.js +3979 -3357
- package/dist/br/data/mun/SC.js.map +1 -1
- package/dist/br/data/mun/SE.cjs +1 -1
- package/dist/br/data/mun/SE.cjs.map +1 -1
- package/dist/br/data/mun/SE.js +812 -625
- package/dist/br/data/mun/SE.js.map +1 -1
- package/dist/br/data/mun/SP.cjs +1 -1
- package/dist/br/data/mun/SP.cjs.map +1 -1
- package/dist/br/data/mun/SP.js +7866 -6803
- package/dist/br/data/mun/SP.js.map +1 -1
- package/dist/br/data/mun/TO.cjs +1 -1
- package/dist/br/data/mun/TO.cjs.map +1 -1
- package/dist/br/data/mun/TO.js +2763 -2817
- package/dist/br/data/mun/TO.js.map +1 -1
- package/dist/br/locations.cjs +1 -1
- package/dist/br/locations.cjs.map +1 -1
- package/dist/br/locations.js +97 -24
- package/dist/br/locations.js.map +1 -1
- package/dist/br/scales.cjs +1 -1
- package/dist/br/scales.cjs.map +1 -1
- package/dist/br/scales.js +8 -4
- package/dist/br/scales.js.map +1 -1
- package/dist/br/whatsapp.cjs +2 -0
- package/dist/br/whatsapp.cjs.map +1 -0
- package/dist/br/whatsapp.js +21 -0
- package/dist/br/whatsapp.js.map +1 -0
- package/dist/br.cjs +1 -1
- package/dist/br.cjs.map +1 -1
- package/dist/br.d.ts +188 -10
- package/dist/br.js +18 -17
- package/dist/br.js.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.js +30 -27
- package/dist/components/BottomSheet/BottomSheet.js.map +1 -1
- package/dist/components/Command/Command.cjs +1 -1
- package/dist/components/Command/Command.cjs.map +1 -1
- package/dist/components/Command/Command.js +78 -75
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js +42 -39
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js +111 -67
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/Modal/Modal.cjs +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js +45 -42
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/Money/Money.cjs +1 -1
- package/dist/components/Money/Money.cjs.map +1 -1
- package/dist/components/Money/Money.js +10 -9
- package/dist/components/Money/Money.js.map +1 -1
- package/dist/components/Pagination/Pagination.cjs +1 -1
- package/dist/components/Pagination/Pagination.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.js +38 -31
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/Pagination/Pagination.module.cjs +1 -1
- package/dist/components/Pagination/Pagination.module.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.module.js +4 -4
- package/dist/components/Pagination/Pagination.module.js.map +1 -1
- package/dist/components/Portal/Portal.cjs +1 -1
- package/dist/components/Portal/Portal.cjs.map +1 -1
- package/dist/components/Portal/Portal.js +9 -6
- package/dist/components/Portal/Portal.js.map +1 -1
- package/dist/components/Portal/portal-host.cjs +2 -0
- package/dist/components/Portal/portal-host.cjs.map +1 -0
- package/dist/components/Portal/portal-host.js +11 -0
- package/dist/components/Portal/portal-host.js.map +1 -0
- package/dist/components/Scheduler/Scheduler.cjs +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js +83 -82
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +55 -53
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js +41 -31
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js +78 -68
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/forms/masked-inputs.cjs +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/masked-inputs.js +20 -19
- package/dist/forms/masked-inputs.js.map +1 -1
- package/dist/hooks/use-fullscreen-element.cjs +2 -0
- package/dist/hooks/use-fullscreen-element.cjs.map +1 -0
- package/dist/hooks/use-fullscreen-element.js +20 -0
- package/dist/hooks/use-fullscreen-element.js.map +1 -0
- package/dist/hooks/use-fullscreen.cjs +2 -0
- package/dist/hooks/use-fullscreen.cjs.map +1 -0
- package/dist/hooks/use-fullscreen.js +66 -0
- package/dist/hooks/use-fullscreen.js.map +1 -0
- package/dist/hooks/use-push-to-talk.cjs +2 -0
- package/dist/hooks/use-push-to-talk.cjs.map +1 -0
- package/dist/hooks/use-push-to-talk.js +61 -0
- package/dist/hooks/use-push-to-talk.js.map +1 -0
- package/dist/http/describe-api-error.cjs.map +1 -1
- package/dist/http/describe-api-error.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +34 -16
- package/dist/http/errors.js.map +1 -1
- package/dist/http/parse-response.cjs +2 -2
- package/dist/http/parse-response.cjs.map +1 -1
- package/dist/http/parse-response.js +9 -8
- package/dist/http/parse-response.js.map +1 -1
- package/dist/i18n/create-i18n.cjs +1 -1
- package/dist/i18n/create-i18n.cjs.map +1 -1
- package/dist/i18n/create-i18n.js +28 -27
- package/dist/i18n/create-i18n.js.map +1 -1
- package/dist/router/AppRouter.cjs +1 -1
- package/dist/router/AppRouter.cjs.map +1 -1
- package/dist/router/AppRouter.js +28 -21
- package/dist/router/AppRouter.js.map +1 -1
- package/dist/router/RouteGuard.cjs +1 -1
- package/dist/router/RouteGuard.cjs.map +1 -1
- package/dist/router/RouteGuard.js +4 -4
- package/dist/router/RouteGuard.js.map +1 -1
- package/dist/sse/create-event-stream.cjs +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js +34 -38
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/styles/AIChat.css +79 -0
- package/dist/styles/Accordion.css +12 -0
- package/dist/styles/Alert.css +32 -0
- package/dist/styles/AppBar.css +17 -0
- package/dist/styles/AppShell.css +8 -0
- package/dist/styles/AspectRatio.css +3 -0
- package/dist/styles/AudioPlayer.css +13 -0
- package/dist/styles/AudioRecorder.css +20 -0
- package/dist/styles/Avatar.css +13 -0
- package/dist/styles/AvatarGroup.css +18 -0
- package/dist/styles/Badge.css +25 -0
- package/dist/styles/Banner.css +13 -0
- package/dist/styles/BarList.css +10 -0
- package/dist/styles/BarcodeScanner.css +21 -0
- package/dist/styles/BottomNavigation.css +10 -0
- package/dist/styles/BottomSheet.css +11 -0
- package/dist/styles/BrazilMap.css +20 -0
- package/dist/styles/Breadcrumbs.css +8 -0
- package/dist/styles/Button.css +42 -0
- package/dist/styles/Calendar.css +16 -0
- package/dist/styles/Card.css +15 -0
- package/dist/styles/Carousel.css +17 -0
- package/dist/styles/Center.css +5 -0
- package/dist/styles/Chat.css +35 -0
- package/dist/styles/Checkbox.css +11 -0
- package/dist/styles/ChipInput.css +13 -0
- package/dist/styles/CodeBlock.css +25 -0
- package/dist/styles/Collapsible.css +6 -0
- package/dist/styles/Combobox.css +18 -0
- package/dist/styles/Command.css +20 -0
- package/dist/styles/ContextMenu.css +11 -0
- package/dist/styles/CopyButton.css +4 -0
- package/dist/styles/DataList.css +3 -0
- package/dist/styles/DataTable.css +19 -0
- package/dist/styles/DateRangePicker.css +22 -0
- package/dist/styles/DescriptionList.css +4 -0
- package/dist/styles/Divider.css +11 -0
- package/dist/styles/Drawer.css +22 -0
- package/dist/styles/DropdownMenu.css +16 -0
- package/dist/styles/Dropzone.css +7 -0
- package/dist/styles/EmptyState.css +6 -0
- package/dist/styles/ErrorState.css +6 -0
- package/dist/styles/ErrorText.css +2 -0
- package/dist/styles/FileUpload.css +17 -0
- package/dist/styles/FilterBar.css +19 -0
- package/dist/styles/FloatingActionButton.css +23 -0
- package/dist/styles/Form.css +21 -0
- package/dist/styles/HoverCard.css +11 -0
- package/dist/styles/IconPicker.css +6 -0
- package/dist/styles/Image.css +2 -0
- package/dist/styles/ImageCropper.css +16 -0
- package/dist/styles/Input.css +24 -0
- package/dist/styles/InstallBanner.css +8 -0
- package/dist/styles/Kanban.css +15 -0
- package/dist/styles/Kbd.css +5 -0
- package/dist/styles/Label.css +3 -0
- package/dist/styles/Layout.css +22 -0
- package/dist/styles/Lightbox.css +21 -0
- package/dist/styles/ListTile.css +14 -0
- package/dist/styles/MapLegend.css +9 -0
- package/dist/styles/Markdown.css +24 -0
- package/dist/styles/Masonry.css +4 -0
- package/dist/styles/Menubar.css +17 -0
- package/dist/styles/Modal.css +21 -0
- package/dist/styles/ModalsManager.css +2 -0
- package/dist/styles/MultiSelect.css +21 -0
- package/dist/styles/MunicipalitySearch.css +6 -0
- package/dist/styles/NProgress.css +2 -0
- package/dist/styles/Navbar.css +10 -0
- package/dist/styles/NavigationMenu.css +12 -0
- package/dist/styles/NavigationRail.css +14 -0
- package/dist/styles/NotificationCenter.css +21 -0
- package/dist/styles/OfflineIndicator.css +7 -0
- package/dist/styles/Page.css +13 -0
- package/dist/styles/Pagination.css +12 -0
- package/dist/styles/PasswordInput.css +21 -0
- package/dist/styles/PinInput.css +13 -0
- package/dist/styles/PixQRCode.css +9 -0
- package/dist/styles/Popover.css +10 -0
- package/dist/styles/Progress.css +11 -0
- package/dist/styles/QRCode.css +3 -0
- package/dist/styles/Radio.css +14 -0
- package/dist/styles/RangeSlider.css +15 -0
- package/dist/styles/RatingStars.css +11 -0
- package/dist/styles/RefreshIndicator.css +5 -0
- package/dist/styles/Resizable.css +13 -0
- package/dist/styles/RichTextEditor.css +19 -0
- package/dist/styles/SafeArea.css +7 -0
- package/dist/styles/Scheduler.css +24 -0
- package/dist/styles/ScrollArea.css +7 -0
- package/dist/styles/SearchBar.css +11 -0
- package/dist/styles/SegmentedControl.css +12 -0
- package/dist/styles/Select.css +14 -0
- package/dist/styles/Sidebar.css +19 -0
- package/dist/styles/SignaturePad.css +6 -0
- package/dist/styles/Skeleton.css +7 -0
- package/dist/styles/Slider.css +15 -0
- package/dist/styles/Spacer.css +4 -0
- package/dist/styles/Sparkline.css +4 -0
- package/dist/styles/Spinner.css +12 -0
- package/dist/styles/Stat.css +12 -0
- package/dist/styles/Stepper.css +17 -0
- package/dist/styles/StepperInput.css +15 -0
- package/dist/styles/Switch.css +11 -0
- package/dist/styles/SyncStatusBadge.css +13 -0
- package/dist/styles/Table.css +16 -0
- package/dist/styles/Tabs.css +14 -0
- package/dist/styles/Tag.css +14 -0
- package/dist/styles/Textarea.css +13 -0
- package/dist/styles/TimePicker.css +14 -0
- package/dist/styles/Timeline.css +17 -0
- package/dist/styles/Toast.css +21 -0
- package/dist/styles/Toggle.css +12 -0
- package/dist/styles/ToggleGroup.css +9 -0
- package/dist/styles/Tooltip.css +10 -0
- package/dist/styles/Tour.css +20 -0
- package/dist/styles/TrajectoryMap.css +16 -0
- package/dist/styles/Transfer.css +21 -0
- package/dist/styles/TreeView.css +17 -0
- package/dist/styles/TruncateText.css +2 -0
- package/dist/styles/UpdatePrompt.css +10 -0
- package/dist/styles/VirtualList.css +5 -0
- package/dist/styles/VirtualTable.css +17 -0
- package/dist/styles/VisuallyHidden.css +2 -0
- package/dist/styles/Wizard.css +4 -0
- package/dist/styles/actions.css +104 -0
- package/dist/styles/advanced.css +66 -0
- package/dist/styles/br.css +41 -0
- package/dist/styles/chat.css +113 -0
- package/dist/styles/core.css +40 -0
- package/dist/styles/data.css +222 -0
- package/dist/styles/editor.css +19 -0
- package/dist/styles/feedback.css +201 -0
- package/dist/styles/forms.css +321 -0
- package/dist/styles/geo.css +16 -0
- package/dist/styles/icons.css +6 -0
- package/dist/styles/identity.css +48 -0
- package/dist/styles/layout.css +122 -0
- package/dist/styles/media.css +52 -0
- package/dist/styles/navigation.css +167 -0
- package/dist/styles/overlay.css +73 -0
- package/dist/styles/utility.css +39 -0
- package/dist/styles.css +1 -1
- package/dist/sw/register-service-worker.cjs +1 -1
- package/dist/sw/register-service-worker.cjs.map +1 -1
- package/dist/sw/register-service-worker.js +21 -17
- package/dist/sw/register-service-worker.js.map +1 -1
- package/dist/sw.d.ts +17 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +1151 -41
- package/dist/tempest-react-sdk.js +295 -286
- package/dist/utils/dev-mode.cjs.map +1 -1
- package/dist/utils/dev-mode.js.map +1 -1
- package/dist/utils/format.cjs +1 -1
- package/dist/utils/format.cjs.map +1 -1
- package/dist/utils/format.js +22 -14
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/intl-cache.cjs +2 -0
- package/dist/utils/intl-cache.cjs.map +1 -0
- package/dist/utils/intl-cache.js +23 -0
- package/dist/utils/intl-cache.js.map +1 -0
- package/dist/utils/json-frame.cjs +2 -0
- package/dist/utils/json-frame.cjs.map +1 -0
- package/dist/utils/json-frame.js +30 -0
- package/dist/utils/json-frame.js.map +1 -0
- package/dist/utils/numbers.cjs +1 -1
- package/dist/utils/numbers.cjs.map +1 -1
- package/dist/utils/numbers.js +8 -7
- package/dist/utils/numbers.js.map +1 -1
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +26 -17
- package/dist/vision/results.js.map +1 -1
- package/dist/vision.d.ts +29 -0
- package/dist/vite/tempest-icons.cjs +1 -1
- package/dist/vite/tempest-icons.cjs.map +1 -1
- package/dist/vite/tempest-icons.js +1 -1
- package/dist/vite/tempest-icons.js.map +1 -1
- package/dist/webrtc/link-stats.cjs +2 -0
- package/dist/webrtc/link-stats.cjs.map +1 -0
- package/dist/webrtc/link-stats.js +74 -0
- package/dist/webrtc/link-stats.js.map +1 -0
- package/dist/webrtc/mesh-quality.cjs +2 -0
- package/dist/webrtc/mesh-quality.cjs.map +1 -0
- package/dist/webrtc/mesh-quality.js +51 -0
- package/dist/webrtc/mesh-quality.js.map +1 -0
- package/dist/webrtc/peer-link.cjs +2 -0
- package/dist/webrtc/peer-link.cjs.map +1 -0
- package/dist/webrtc/peer-link.js +45 -0
- package/dist/webrtc/peer-link.js.map +1 -0
- package/dist/webrtc/peer-mesh.cjs +2 -0
- package/dist/webrtc/peer-mesh.cjs.map +1 -0
- package/dist/webrtc/peer-mesh.js +155 -0
- package/dist/webrtc/peer-mesh.js.map +1 -0
- package/dist/webrtc/use-link-stats.cjs +2 -0
- package/dist/webrtc/use-link-stats.cjs.map +1 -0
- package/dist/webrtc/use-link-stats.js +22 -0
- package/dist/webrtc/use-link-stats.js.map +1 -0
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +81 -85
- package/dist/ws/create-web-socket.js.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 +42 -48
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +5 -2
|
@@ -1,42 +1,43 @@
|
|
|
1
1
|
import { Input as e } from "../components/Input/Input.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { numberFormat as t } from "../utils/intl-cache.js";
|
|
3
|
+
import { formatCPF as n, formatPhone as r } from "../utils/format.js";
|
|
4
|
+
import { formatCEP as i, formatCNPJ as a } from "./br-validators.js";
|
|
5
|
+
import { forwardRef as o } from "react";
|
|
6
|
+
import { jsx as s } from "react/jsx-runtime";
|
|
6
7
|
//#region src/forms/masked-inputs.tsx
|
|
7
|
-
function
|
|
8
|
-
return
|
|
9
|
-
return /* @__PURE__ */
|
|
8
|
+
function c(t, n = "numeric") {
|
|
9
|
+
return o(function({ value: r, onChange: i, ...a }, o) {
|
|
10
|
+
return /* @__PURE__ */ s(e, {
|
|
10
11
|
...a,
|
|
11
|
-
ref:
|
|
12
|
+
ref: o,
|
|
12
13
|
value: t(r ?? ""),
|
|
13
14
|
inputMode: n,
|
|
14
15
|
onChange: (e) => i?.(t(e.target.value))
|
|
15
16
|
});
|
|
16
17
|
});
|
|
17
18
|
}
|
|
18
|
-
var
|
|
19
|
-
function
|
|
20
|
-
return
|
|
19
|
+
var l = c(n), u = c(a), d = c(r, "tel"), f = c(i);
|
|
20
|
+
function p(e, n, r) {
|
|
21
|
+
return t(n, {
|
|
21
22
|
style: "currency",
|
|
22
|
-
currency:
|
|
23
|
+
currency: r
|
|
23
24
|
}).format(e / 100);
|
|
24
25
|
}
|
|
25
|
-
function
|
|
26
|
+
function m(e) {
|
|
26
27
|
let t = e.replace(/\D/g, "");
|
|
27
28
|
return t ? Number.parseInt(t, 10) : 0;
|
|
28
29
|
}
|
|
29
|
-
var
|
|
30
|
-
return /* @__PURE__ */
|
|
30
|
+
var h = o(function({ value: t, onChange: n, currency: r = "BRL", locale: i = "pt-BR", ...a }, o) {
|
|
31
|
+
return /* @__PURE__ */ s(e, {
|
|
31
32
|
...a,
|
|
32
|
-
ref:
|
|
33
|
+
ref: o,
|
|
33
34
|
type: "text",
|
|
34
35
|
inputMode: "numeric",
|
|
35
|
-
value:
|
|
36
|
-
onChange: (e) => n(
|
|
36
|
+
value: p(t || 0, i, r),
|
|
37
|
+
onChange: (e) => n(m(e.target.value))
|
|
37
38
|
});
|
|
38
39
|
});
|
|
39
40
|
//#endregion
|
|
40
|
-
export {
|
|
41
|
+
export { f as CEPInput, u as CNPJInput, l as CPFInput, h as MoneyInput, d as PhoneInput };
|
|
41
42
|
|
|
42
43
|
//# sourceMappingURL=masked-inputs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"masked-inputs.js","names":[],"sources":["../../src/forms/masked-inputs.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Input, type InputProps } from \"@/components/Input\";\nimport { formatCEP, formatCNPJ } from \"./br-validators\";\nimport { formatCPF, formatPhone } from \"@/utils/format\";\n\n/**\n * Props of every masked input.\n *\n * `value` and `onChange` are optional because the primary documented usage is\n * inside `<FormField>`, which supplies both through `cloneElement` — declaring\n * them required made `<FormField name=\"cpf\"><CPFInput /></FormField>` fail to\n * type-check while working perfectly at runtime. Standalone usage still passes\n * both; an uncontrolled input without `onChange` simply reports nothing.\n */\ntype MaskedFieldProps = Omit<InputProps, \"value\" | \"onChange\"> & {\n value?: string;\n onChange?: (value: string) => void;\n};\n\nfunction maskedInput(\n mask: (input: string) => string,\n inputMode: InputHTMLAttributes<HTMLInputElement>[\"inputMode\"] = \"numeric\",\n) {\n return forwardRef<HTMLInputElement, MaskedFieldProps>(function MaskedInput(\n { value, onChange, ...props },\n ref,\n ) {\n return (\n <Input\n {...props}\n ref={ref}\n value={mask(value ?? \"\")}\n inputMode={inputMode}\n onChange={(event) => onChange?.(mask(event.target.value))}\n />\n );\n });\n}\n\nexport const CPFInput = maskedInput(formatCPF);\nexport const CNPJInput = maskedInput(formatCNPJ);\nexport const PhoneInput = maskedInput(formatPhone, \"tel\");\nexport const CEPInput = maskedInput(formatCEP);\n\nexport interface MoneyInputProps extends Omit<InputProps, \"value\" | \"onChange\" | \"type\"> {\n /** Cents (integer). Internally treated as 1/100 of the currency unit. */\n value: number;\n onChange: (cents: number) => void;\n /** Currency code for `Intl.NumberFormat`. Default: `\"BRL\"`. */\n currency?: string;\n /** Locale for `Intl.NumberFormat`. Default: `\"pt-BR\"`. */\n locale?: string;\n}\n\nfunction formatCents(cents: number, locale: string, currency: string): string {\n return
|
|
1
|
+
{"version":3,"file":"masked-inputs.js","names":[],"sources":["../../src/forms/masked-inputs.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Input, type InputProps } from \"@/components/Input\";\nimport { formatCEP, formatCNPJ } from \"./br-validators\";\nimport { formatCPF, formatPhone } from \"@/utils/format\";\nimport { numberFormat } from \"@/utils/intl-cache\";\n\n/**\n * Props of every masked input.\n *\n * `value` and `onChange` are optional because the primary documented usage is\n * inside `<FormField>`, which supplies both through `cloneElement` — declaring\n * them required made `<FormField name=\"cpf\"><CPFInput /></FormField>` fail to\n * type-check while working perfectly at runtime. Standalone usage still passes\n * both; an uncontrolled input without `onChange` simply reports nothing.\n */\ntype MaskedFieldProps = Omit<InputProps, \"value\" | \"onChange\"> & {\n value?: string;\n onChange?: (value: string) => void;\n};\n\nfunction maskedInput(\n mask: (input: string) => string,\n inputMode: InputHTMLAttributes<HTMLInputElement>[\"inputMode\"] = \"numeric\",\n) {\n return forwardRef<HTMLInputElement, MaskedFieldProps>(function MaskedInput(\n { value, onChange, ...props },\n ref,\n ) {\n return (\n <Input\n {...props}\n ref={ref}\n value={mask(value ?? \"\")}\n inputMode={inputMode}\n onChange={(event) => onChange?.(mask(event.target.value))}\n />\n );\n });\n}\n\nexport const CPFInput = maskedInput(formatCPF);\nexport const CNPJInput = maskedInput(formatCNPJ);\nexport const PhoneInput = maskedInput(formatPhone, \"tel\");\nexport const CEPInput = maskedInput(formatCEP);\n\nexport interface MoneyInputProps extends Omit<InputProps, \"value\" | \"onChange\" | \"type\"> {\n /** Cents (integer). Internally treated as 1/100 of the currency unit. */\n value: number;\n onChange: (cents: number) => void;\n /** Currency code for `Intl.NumberFormat`. Default: `\"BRL\"`. */\n currency?: string;\n /** Locale for `Intl.NumberFormat`. Default: `\"pt-BR\"`. */\n locale?: string;\n}\n\nfunction formatCents(cents: number, locale: string, currency: string): string {\n return numberFormat(locale, { style: \"currency\", currency }).format(cents / 100);\n}\n\nfunction parseCents(text: string): number {\n const digits = text.replace(/\\D/g, \"\");\n if (!digits) return 0;\n return Number.parseInt(digits, 10);\n}\n\n/**\n * Currency-masked input. Stores the value as an integer number of cents to\n * avoid floating-point error. Suitable for `react-hook-form` once you adapt\n * the field to expose cents.\n */\nexport const MoneyInput = forwardRef<HTMLInputElement, MoneyInputProps>(function MoneyInput(\n { value, onChange, currency = \"BRL\", locale = \"pt-BR\", ...props },\n ref,\n) {\n return (\n <Input\n {...props}\n ref={ref}\n type=\"text\"\n inputMode=\"numeric\"\n value={formatCents(value || 0, locale, currency)}\n onChange={(event) => onChange(parseCents(event.target.value))}\n />\n );\n});\n"],"mappings":";;;;;;;AAqBA,SAAS,EACL,GACA,IAAgE,WAClE;CACE,OAAO,EAA+C,SAClD,EAAE,UAAO,aAAU,GAAG,KACtB,GACF;EACE,OACI,kBAAC,GAAD;GACI,GAAI;GACC;GACL,OAAO,EAAK,KAAS,EAAE;GACZ;GACX,WAAW,MAAU,IAAW,EAAK,EAAM,OAAO,KAAK,CAAC;EAC3D,CAAA;CAET,CAAC;AACL;AAEA,IAAa,IAAW,EAAY,CAAS,GAChC,IAAY,EAAY,CAAU,GAClC,IAAa,EAAY,GAAa,KAAK,GAC3C,IAAW,EAAY,CAAS;AAY7C,SAAS,EAAY,GAAe,GAAgB,GAA0B;CAC1E,OAAO,EAAa,GAAQ;EAAE,OAAO;EAAY;CAAS,CAAC,CAAC,CAAC,OAAO,IAAQ,GAAG;AACnF;AAEA,SAAS,EAAW,GAAsB;CACtC,IAAM,IAAS,EAAK,QAAQ,OAAO,EAAE;CAErC,OADK,IACE,OAAO,SAAS,GAAQ,EAAE,IADb;AAExB;AAOA,IAAa,IAAa,EAA8C,SACpE,EAAE,UAAO,aAAU,cAAW,OAAO,YAAS,SAAS,GAAG,KAC1D,GACF;CACE,OACI,kBAAC,GAAD;EACI,GAAI;EACC;EACL,MAAK;EACL,WAAU;EACV,OAAO,EAAY,KAAS,GAAG,GAAQ,CAAQ;EAC/C,WAAW,MAAU,EAAS,EAAW,EAAM,OAAO,KAAK,CAAC;CAC/D,CAAA;AAET,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("react");function t(){if(typeof document>`u`)return null;let e=document;return document.fullscreenElement??e.webkitFullscreenElement??null}function n(){let[n,r]=(0,e.useState)(t);return(0,e.useEffect)(()=>{let e=()=>r(t());return e(),document.addEventListener(`fullscreenchange`,e),document.addEventListener(`webkitfullscreenchange`,e),()=>{document.removeEventListener(`fullscreenchange`,e),document.removeEventListener(`webkitfullscreenchange`,e)}},[]),n}exports.getFullscreenElement=t,exports.useFullscreenElement=n;
|
|
2
|
+
//# sourceMappingURL=use-fullscreen-element.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-fullscreen-element.cjs","names":[],"sources":["../../src/hooks/use-fullscreen-element.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\n\n/**\n * Vendor-prefixed fullscreen member of `document`, still shipped by WebKit.\n *\n * Declared optional on purpose: the standard property is what every other engine\n * answers to, and Safari is the only reason the second read exists.\n */\ninterface WebkitFullscreenDocument {\n webkitFullscreenElement?: Element | null;\n}\n\n/**\n * Read the element the browser is presenting fullscreen, standard property first.\n *\n * A plain function rather than a hook, because two very different callers need\n * the answer: `useFullscreenElement` below, which turns it into state, and the\n * action callbacks of `useFullscreen`, which need the value *at call time* and\n * cannot wait for a render to have happened since the last change.\n *\n * @returns The element being presented fullscreen, or `null` — including in any\n * environment without a `document` (a service worker, a build plugin, a Node\n * test), where the SDK must not throw.\n */\nexport function getFullscreenElement(): Element | null {\n if (typeof document === \"undefined\") return null;\n const prefixed = document as Document & WebkitFullscreenDocument;\n return document.fullscreenElement ?? prefixed.webkitFullscreenElement ?? null;\n}\n\n/**\n * Track the element the browser is presenting fullscreen.\n *\n * The SDK's single subscription to `fullscreenchange` (and WebKit's\n * `webkitfullscreenchange`). Two features need exactly this fact and would\n * otherwise each carry their own copy of the prefix dance: `usePortalHost` asks\n * *where do I mount* — the fullscreen element paints its own subtree and nothing\n * else, so a dialog portalled to `document.body` is invisible while fullscreen is\n * on — and `useFullscreen` asks *is my element the one presented*. One\n * subscription, two questions.\n *\n * The event is the source of truth rather than the return of `requestFullscreen`\n * / `exitFullscreen`, because `Esc`, the browser's own exit affordance and F11\n * leave fullscreen without ever passing through those calls.\n *\n * The first value is resolved during render rather than in the effect, so a\n * consumer that mounts while fullscreen is already active is correct in its very\n * first commit instead of correcting itself one frame later.\n *\n * @returns The fullscreen element, or `null` when nothing is presented.\n */\nexport function useFullscreenElement(): Element | null {\n const [element, setElement] = useState<Element | null>(getFullscreenElement);\n\n useEffect(() => {\n const sync = (): void => setElement(getFullscreenElement());\n sync();\n document.addEventListener(\"fullscreenchange\", sync);\n document.addEventListener(\"webkitfullscreenchange\", sync);\n return () => {\n document.removeEventListener(\"fullscreenchange\", sync);\n document.removeEventListener(\"webkitfullscreenchange\", sync);\n };\n }, []);\n\n return element;\n}\n"],"mappings":"uBAwBA,SAAgB,GAAuC,CACnD,GAAI,OAAO,SAAa,IAAa,OAAO,KAC5C,IAAM,EAAW,SACjB,OAAO,SAAS,mBAAqB,EAAS,yBAA2B,IAC7E,CAuBA,SAAgB,GAAuC,CACnD,GAAM,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAyB,CAAoB,EAa3E,OAXA,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,MAAmB,EAAW,EAAqB,CAAC,EAI1D,OAHA,EAAK,EACL,SAAS,iBAAiB,mBAAoB,CAAI,EAClD,SAAS,iBAAiB,yBAA0B,CAAI,MAC3C,CACT,SAAS,oBAAoB,mBAAoB,CAAI,EACrD,SAAS,oBAAoB,yBAA0B,CAAI,CAC/D,CACJ,EAAG,CAAC,CAAC,EAEE,CACX"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect as e, useState as t } from "react";
|
|
2
|
+
//#region src/hooks/use-fullscreen-element.ts
|
|
3
|
+
function n() {
|
|
4
|
+
if (typeof document > "u") return null;
|
|
5
|
+
let e = document;
|
|
6
|
+
return document.fullscreenElement ?? e.webkitFullscreenElement ?? null;
|
|
7
|
+
}
|
|
8
|
+
function r() {
|
|
9
|
+
let [r, i] = t(n);
|
|
10
|
+
return e(() => {
|
|
11
|
+
let e = () => i(n());
|
|
12
|
+
return e(), document.addEventListener("fullscreenchange", e), document.addEventListener("webkitfullscreenchange", e), () => {
|
|
13
|
+
document.removeEventListener("fullscreenchange", e), document.removeEventListener("webkitfullscreenchange", e);
|
|
14
|
+
};
|
|
15
|
+
}, []), r;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { n as getFullscreenElement, r as useFullscreenElement };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=use-fullscreen-element.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-fullscreen-element.js","names":[],"sources":["../../src/hooks/use-fullscreen-element.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\n\n/**\n * Vendor-prefixed fullscreen member of `document`, still shipped by WebKit.\n *\n * Declared optional on purpose: the standard property is what every other engine\n * answers to, and Safari is the only reason the second read exists.\n */\ninterface WebkitFullscreenDocument {\n webkitFullscreenElement?: Element | null;\n}\n\n/**\n * Read the element the browser is presenting fullscreen, standard property first.\n *\n * A plain function rather than a hook, because two very different callers need\n * the answer: `useFullscreenElement` below, which turns it into state, and the\n * action callbacks of `useFullscreen`, which need the value *at call time* and\n * cannot wait for a render to have happened since the last change.\n *\n * @returns The element being presented fullscreen, or `null` — including in any\n * environment without a `document` (a service worker, a build plugin, a Node\n * test), where the SDK must not throw.\n */\nexport function getFullscreenElement(): Element | null {\n if (typeof document === \"undefined\") return null;\n const prefixed = document as Document & WebkitFullscreenDocument;\n return document.fullscreenElement ?? prefixed.webkitFullscreenElement ?? null;\n}\n\n/**\n * Track the element the browser is presenting fullscreen.\n *\n * The SDK's single subscription to `fullscreenchange` (and WebKit's\n * `webkitfullscreenchange`). Two features need exactly this fact and would\n * otherwise each carry their own copy of the prefix dance: `usePortalHost` asks\n * *where do I mount* — the fullscreen element paints its own subtree and nothing\n * else, so a dialog portalled to `document.body` is invisible while fullscreen is\n * on — and `useFullscreen` asks *is my element the one presented*. One\n * subscription, two questions.\n *\n * The event is the source of truth rather than the return of `requestFullscreen`\n * / `exitFullscreen`, because `Esc`, the browser's own exit affordance and F11\n * leave fullscreen without ever passing through those calls.\n *\n * The first value is resolved during render rather than in the effect, so a\n * consumer that mounts while fullscreen is already active is correct in its very\n * first commit instead of correcting itself one frame later.\n *\n * @returns The fullscreen element, or `null` when nothing is presented.\n */\nexport function useFullscreenElement(): Element | null {\n const [element, setElement] = useState<Element | null>(getFullscreenElement);\n\n useEffect(() => {\n const sync = (): void => setElement(getFullscreenElement());\n sync();\n document.addEventListener(\"fullscreenchange\", sync);\n document.addEventListener(\"webkitfullscreenchange\", sync);\n return () => {\n document.removeEventListener(\"fullscreenchange\", sync);\n document.removeEventListener(\"webkitfullscreenchange\", sync);\n };\n }, []);\n\n return element;\n}\n"],"mappings":";;AAwBA,SAAgB,IAAuC;CACnD,IAAI,OAAO,WAAa,KAAa,OAAO;CAC5C,IAAM,IAAW;CACjB,OAAO,SAAS,qBAAqB,EAAS,2BAA2B;AAC7E;AAuBA,SAAgB,IAAuC;CACnD,IAAM,CAAC,GAAS,KAAc,EAAyB,CAAoB;CAa3E,OAXA,QAAgB;EACZ,IAAM,UAAmB,EAAW,EAAqB,CAAC;EAI1D,OAHA,EAAK,GACL,SAAS,iBAAiB,oBAAoB,CAAI,GAClD,SAAS,iBAAiB,0BAA0B,CAAI,SAC3C;GAET,AADA,SAAS,oBAAoB,oBAAoB,CAAI,GACrD,SAAS,oBAAoB,0BAA0B,CAAI;EAC/D;CACJ,GAAG,CAAC,CAAC,GAEE;AACX"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./use-fullscreen-element.cjs");let t=require("react");function n(e){return typeof document>`u`?null:e?e.current:document.documentElement}function r(){if(typeof document>`u`)return!1;let e=document.documentElement,t=document;return(typeof e.requestFullscreen==`function`||typeof e.webkitRequestFullscreen==`function`)&&(t.fullscreenEnabled??t.webkitFullscreenEnabled??!0)}function i(i){let a=e.useFullscreenElement(),[o]=(0,t.useState)(r),[s,c]=(0,t.useState)(!1);(0,t.useEffect)(()=>{let e=n(i);c(e!==null&&a===e)},[a,i]);let l=(0,t.useCallback)(async()=>{let e=n(i);if(e===null)throw Error(`useFullscreen: nothing to present. Either this environment has no document, or the ref was never attached to a mounted element.`);let t=e;if(typeof t.requestFullscreen==`function`){await t.requestFullscreen();return}if(typeof t.webkitRequestFullscreen==`function`){await t.webkitRequestFullscreen();return}throw Error(`useFullscreen: this browser exposes no way to enter fullscreen.`)},[i]),u=(0,t.useCallback)(async()=>{if(e.getFullscreenElement()===null)return;let t=document;if(typeof t.exitFullscreen==`function`){await t.exitFullscreen();return}if(typeof t.webkitExitFullscreen==`function`){await t.webkitExitFullscreen();return}throw Error(`useFullscreen: this browser exposes no way to leave fullscreen.`)},[]);return{isFullscreen:s,supported:o,enter:l,exit:u,toggle:(0,t.useCallback)(async()=>{let t=n(i);if(t!==null&&e.getFullscreenElement()===t){await u();return}await l()},[l,u,i])}}exports.isFullscreenSupported=r,exports.useFullscreen=i;
|
|
2
|
+
//# sourceMappingURL=use-fullscreen.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-fullscreen.cjs","names":[],"sources":["../../src/hooks/use-fullscreen.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport type { RefObject } from \"react\";\nimport { getFullscreenElement, useFullscreenElement } from \"./use-fullscreen-element\";\n\n/**\n * The fullscreen members of `document` this hook calls, standard and\n * vendor-prefixed, every one of them optional.\n *\n * `lib.dom` declares the standard members as always present, which is a promise\n * the runtime does not keep: an older WebKit ships only the prefixed pair, and a\n * document inside an `<iframe>` without `allowfullscreen` reports\n * `fullscreenEnabled: false`. Reading through an all-optional shape is what makes\n * `typeof x === \"function\"` a check the compiler agrees is worth making.\n */\ninterface FullscreenDocument {\n fullscreenEnabled?: boolean;\n exitFullscreen?: () => Promise<void>;\n webkitFullscreenEnabled?: boolean;\n webkitExitFullscreen?: () => Promise<void> | void;\n}\n\n/** The fullscreen members of an element, standard and vendor-prefixed. */\ninterface FullscreenTarget {\n requestFullscreen?: (options?: FullscreenOptions) => Promise<void>;\n webkitRequestFullscreen?: () => Promise<void> | void;\n}\n\n/**\n * Resolve which element the hook acts on.\n *\n * `ref.current` when a ref was given, `document.documentElement` when it was not\n * — the whole page, which is what \"go fullscreen\" means with no element named.\n *\n * A missing `ref.current` deliberately resolves to `null` rather than falling\n * back to the page: a caller who named an element and whose element is not\n * mounted yet wants an error, not the entire document blown up to fill the\n * screen behind their video.\n *\n * @param ref - The element to present, or `undefined` for the whole page.\n * @returns The element, or `null` when there is nothing to act on.\n */\nfunction resolveTarget(ref?: RefObject<HTMLElement | null>): HTMLElement | null {\n if (typeof document === \"undefined\") return null;\n return ref ? ref.current : document.documentElement;\n}\n\n/**\n * Whether this environment can present anything fullscreen at all.\n *\n * Two independent reasons for `false`, and skipping either one ships a button\n * that does nothing when pressed: the API can be missing (older WebKit, and every\n * browser on iOS for non-`<video>` elements), or present and **disabled** — a\n * document inside an `<iframe>` without the `allowfullscreen` attribute exposes\n * `requestFullscreen` and rejects every call to it, which `fullscreenEnabled`\n * reports up front.\n *\n * An engine that exposes the methods and no flag at all is treated as capable:\n * the flag is the newer half of the API, so its absence says nothing.\n *\n * @returns `true` when a fullscreen request stands a chance of being honoured.\n */\nexport function isFullscreenSupported(): boolean {\n if (typeof document === \"undefined\") return false;\n const root: FullscreenTarget = document.documentElement;\n const doc: FullscreenDocument = document;\n const reachable =\n typeof root.requestFullscreen === \"function\" ||\n typeof root.webkitRequestFullscreen === \"function\";\n return reachable && (doc.fullscreenEnabled ?? doc.webkitFullscreenEnabled ?? true);\n}\n\n/** Value returned by {@link useFullscreen}. */\nexport interface UseFullscreenResult {\n /** Whether the target element is the one the browser is presenting right now. */\n isFullscreen: boolean;\n /** Whether a request stands a chance at all — hide the control when `false`. */\n supported: boolean;\n /** Present the target. Must be called from a user gesture. Rejects if refused. */\n enter: () => Promise<void>;\n /** Leave fullscreen. Resolves immediately when nothing is presented. */\n exit: () => Promise<void>;\n /** Leave if the target is presented, otherwise enter. Same gesture rule as `enter`. */\n toggle: () => Promise<void>;\n}\n\n/**\n * Drive an immersive mode whose state stays true to the browser.\n *\n * The mistake this exists to remove is storing `isFullscreen` in state and\n * flipping it inside your own `enter()` / `exit()`. Fullscreen ends in ways your\n * code never sees — `Esc`, the browser's own exit affordance, F11 pressed while an\n * API fullscreen is active — and each of those leaves the flag saying \"sair\" over\n * a page that is already windowed. The only honest source is the\n * `fullscreenchange` event, which fires for every one of them, so that is what\n * this hook reads and what the returned `isFullscreen` reflects. The action\n * callbacks never touch it.\n *\n * The subscription is not this hook's own: it lives in `useFullscreenElement`,\n * shared with `usePortalHost`, which needs the same fact to decide where an\n * overlay mounts while fullscreen is on. Two independent listeners for one event\n * would mean two copies of the WebKit prefix dance and two places to fix the next\n * quirk, so the primitive answers \"which element is presented\" once and each\n * consumer asks its own question of the answer.\n *\n * WebKit's prefixed members are handled throughout — `webkitfullscreenchange`,\n * `webkitFullscreenElement`, `webkitRequestFullscreen`, `webkitExitFullscreen` —\n * because Safari on iPad still ships them and nothing else.\n *\n * `isFullscreen` is identity against the target, not containment: a `<video>`\n * inside your element going fullscreen on its own is not your element being\n * presented, and reporting `true` there would put your exit control inside a\n * subtree the browser is not painting.\n *\n * @param ref - The element to present. Omit it to present the whole page\n * (`document.documentElement`).\n * @returns `{ isFullscreen, supported, enter, exit, toggle }`.\n * @throws Nothing during render. `enter()` and `toggle()` reject with the\n * browser's own error when the request is refused — most often a `TypeError`\n * because the call did not come from a user gesture — and with an `Error` when\n * there is no element to present or the environment ships no Fullscreen API.\n *\n * @example\n * const stage = useRef<HTMLDivElement>(null);\n * const { isFullscreen, supported, toggle } = useFullscreen(stage);\n *\n * <div ref={stage}>\n * <video src=\"/aula.mp4\" controls />\n * {supported ? (\n * <button onClick={() => void toggle().catch(() => setRefused(true))}>\n * {isFullscreen ? \"Sair da tela cheia\" : \"Tela cheia\"}\n * </button>\n * ) : null}\n * </div>\n */\nexport function useFullscreen(ref?: RefObject<HTMLElement | null>): UseFullscreenResult {\n const element = useFullscreenElement();\n const [supported] = useState(isFullscreenSupported);\n const [isFullscreen, setIsFullscreen] = useState(false);\n\n useEffect(() => {\n const target = resolveTarget(ref);\n setIsFullscreen(target !== null && element === target);\n }, [element, ref]);\n\n const enter = useCallback(async (): Promise<void> => {\n const target = resolveTarget(ref);\n if (target === null) {\n throw new Error(\n \"useFullscreen: nothing to present. Either this environment has no document, \" +\n \"or the ref was never attached to a mounted element.\",\n );\n }\n const candidate: FullscreenTarget = target;\n if (typeof candidate.requestFullscreen === \"function\") {\n await candidate.requestFullscreen();\n return;\n }\n if (typeof candidate.webkitRequestFullscreen === \"function\") {\n await candidate.webkitRequestFullscreen();\n return;\n }\n throw new Error(\"useFullscreen: this browser exposes no way to enter fullscreen.\");\n }, [ref]);\n\n const exit = useCallback(async (): Promise<void> => {\n if (getFullscreenElement() === null) return;\n const doc: FullscreenDocument = document;\n if (typeof doc.exitFullscreen === \"function\") {\n await doc.exitFullscreen();\n return;\n }\n if (typeof doc.webkitExitFullscreen === \"function\") {\n await doc.webkitExitFullscreen();\n return;\n }\n throw new Error(\"useFullscreen: this browser exposes no way to leave fullscreen.\");\n }, []);\n\n const toggle = useCallback(async (): Promise<void> => {\n const target = resolveTarget(ref);\n if (target !== null && getFullscreenElement() === target) {\n await exit();\n return;\n }\n await enter();\n }, [enter, exit, ref]);\n\n return { isFullscreen, supported, enter, exit, toggle };\n}\n"],"mappings":"uEAyCA,SAAS,EAAc,EAAyD,CAE5E,OADI,OAAO,SAAa,IAAoB,KACrC,EAAM,EAAI,QAAU,SAAS,eACxC,CAiBA,SAAgB,GAAiC,CAC7C,GAAI,OAAO,SAAa,IAAa,MAAO,GAC5C,IAAM,EAAyB,SAAS,gBAClC,EAA0B,SAIhC,OAFI,OAAO,EAAK,mBAAsB,YAClC,OAAO,EAAK,yBAA4B,cACvB,EAAI,mBAAqB,EAAI,yBAA2B,GACjF,CAiEA,SAAgB,EAAc,EAA0D,CACpF,IAAM,EAAU,EAAA,qBAAqB,EAC/B,CAAC,IAAA,EAAa,EAAA,SAAA,CAAS,CAAqB,EAC5C,CAAC,EAAc,IAAA,EAAmB,EAAA,SAAA,CAAS,EAAK,GAEtD,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAS,EAAc,CAAG,EAChC,EAAgB,IAAW,MAAQ,IAAY,CAAM,CACzD,EAAG,CAAC,EAAS,CAAG,CAAC,EAEjB,IAAM,GAAA,EAAQ,EAAA,YAAA,CAAY,SAA2B,CACjD,IAAM,EAAS,EAAc,CAAG,EAChC,GAAI,IAAW,KACX,MAAU,MACN,iIAEJ,EAEJ,IAAM,EAA8B,EACpC,GAAI,OAAO,EAAU,mBAAsB,WAAY,CACnD,MAAM,EAAU,kBAAkB,EAClC,MACJ,CACA,GAAI,OAAO,EAAU,yBAA4B,WAAY,CACzD,MAAM,EAAU,wBAAwB,EACxC,MACJ,CACA,MAAU,MAAM,iEAAiE,CACrF,EAAG,CAAC,CAAG,CAAC,EAEF,GAAA,EAAO,EAAA,YAAA,CAAY,SAA2B,CAChD,GAAI,EAAA,qBAAqB,IAAM,KAAM,OACrC,IAAM,EAA0B,SAChC,GAAI,OAAO,EAAI,gBAAmB,WAAY,CAC1C,MAAM,EAAI,eAAe,EACzB,MACJ,CACA,GAAI,OAAO,EAAI,sBAAyB,WAAY,CAChD,MAAM,EAAI,qBAAqB,EAC/B,MACJ,CACA,MAAU,MAAM,iEAAiE,CACrF,EAAG,CAAC,CAAC,EAWL,MAAO,CAAE,eAAc,YAAW,QAAO,OAAM,QAAA,EAThC,EAAA,YAAA,CAAY,SAA2B,CAClD,IAAM,EAAS,EAAc,CAAG,EAChC,GAAI,IAAW,MAAQ,EAAA,qBAAqB,IAAM,EAAQ,CACtD,MAAM,EAAK,EACX,MACJ,CACA,MAAM,EAAM,CAChB,EAAG,CAAC,EAAO,EAAM,CAAG,CAE2B,CAAO,CAC1D"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getFullscreenElement as e, useFullscreenElement as t } from "./use-fullscreen-element.js";
|
|
2
|
+
import { useCallback as n, useEffect as r, useState as i } from "react";
|
|
3
|
+
//#region src/hooks/use-fullscreen.ts
|
|
4
|
+
function a(e) {
|
|
5
|
+
return typeof document > "u" ? null : e ? e.current : document.documentElement;
|
|
6
|
+
}
|
|
7
|
+
function o() {
|
|
8
|
+
if (typeof document > "u") return !1;
|
|
9
|
+
let e = document.documentElement, t = document;
|
|
10
|
+
return (typeof e.requestFullscreen == "function" || typeof e.webkitRequestFullscreen == "function") && (t.fullscreenEnabled ?? t.webkitFullscreenEnabled ?? !0);
|
|
11
|
+
}
|
|
12
|
+
function s(s) {
|
|
13
|
+
let c = t(), [l] = i(o), [u, d] = i(!1);
|
|
14
|
+
r(() => {
|
|
15
|
+
let e = a(s);
|
|
16
|
+
d(e !== null && c === e);
|
|
17
|
+
}, [c, s]);
|
|
18
|
+
let f = n(async () => {
|
|
19
|
+
let e = a(s);
|
|
20
|
+
if (e === null) throw Error("useFullscreen: nothing to present. Either this environment has no document, or the ref was never attached to a mounted element.");
|
|
21
|
+
let t = e;
|
|
22
|
+
if (typeof t.requestFullscreen == "function") {
|
|
23
|
+
await t.requestFullscreen();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (typeof t.webkitRequestFullscreen == "function") {
|
|
27
|
+
await t.webkitRequestFullscreen();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
throw Error("useFullscreen: this browser exposes no way to enter fullscreen.");
|
|
31
|
+
}, [s]), p = n(async () => {
|
|
32
|
+
if (e() === null) return;
|
|
33
|
+
let t = document;
|
|
34
|
+
if (typeof t.exitFullscreen == "function") {
|
|
35
|
+
await t.exitFullscreen();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (typeof t.webkitExitFullscreen == "function") {
|
|
39
|
+
await t.webkitExitFullscreen();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
throw Error("useFullscreen: this browser exposes no way to leave fullscreen.");
|
|
43
|
+
}, []);
|
|
44
|
+
return {
|
|
45
|
+
isFullscreen: u,
|
|
46
|
+
supported: l,
|
|
47
|
+
enter: f,
|
|
48
|
+
exit: p,
|
|
49
|
+
toggle: n(async () => {
|
|
50
|
+
let t = a(s);
|
|
51
|
+
if (t !== null && e() === t) {
|
|
52
|
+
await p();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await f();
|
|
56
|
+
}, [
|
|
57
|
+
f,
|
|
58
|
+
p,
|
|
59
|
+
s
|
|
60
|
+
])
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { o as isFullscreenSupported, s as useFullscreen };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=use-fullscreen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-fullscreen.js","names":[],"sources":["../../src/hooks/use-fullscreen.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport type { RefObject } from \"react\";\nimport { getFullscreenElement, useFullscreenElement } from \"./use-fullscreen-element\";\n\n/**\n * The fullscreen members of `document` this hook calls, standard and\n * vendor-prefixed, every one of them optional.\n *\n * `lib.dom` declares the standard members as always present, which is a promise\n * the runtime does not keep: an older WebKit ships only the prefixed pair, and a\n * document inside an `<iframe>` without `allowfullscreen` reports\n * `fullscreenEnabled: false`. Reading through an all-optional shape is what makes\n * `typeof x === \"function\"` a check the compiler agrees is worth making.\n */\ninterface FullscreenDocument {\n fullscreenEnabled?: boolean;\n exitFullscreen?: () => Promise<void>;\n webkitFullscreenEnabled?: boolean;\n webkitExitFullscreen?: () => Promise<void> | void;\n}\n\n/** The fullscreen members of an element, standard and vendor-prefixed. */\ninterface FullscreenTarget {\n requestFullscreen?: (options?: FullscreenOptions) => Promise<void>;\n webkitRequestFullscreen?: () => Promise<void> | void;\n}\n\n/**\n * Resolve which element the hook acts on.\n *\n * `ref.current` when a ref was given, `document.documentElement` when it was not\n * — the whole page, which is what \"go fullscreen\" means with no element named.\n *\n * A missing `ref.current` deliberately resolves to `null` rather than falling\n * back to the page: a caller who named an element and whose element is not\n * mounted yet wants an error, not the entire document blown up to fill the\n * screen behind their video.\n *\n * @param ref - The element to present, or `undefined` for the whole page.\n * @returns The element, or `null` when there is nothing to act on.\n */\nfunction resolveTarget(ref?: RefObject<HTMLElement | null>): HTMLElement | null {\n if (typeof document === \"undefined\") return null;\n return ref ? ref.current : document.documentElement;\n}\n\n/**\n * Whether this environment can present anything fullscreen at all.\n *\n * Two independent reasons for `false`, and skipping either one ships a button\n * that does nothing when pressed: the API can be missing (older WebKit, and every\n * browser on iOS for non-`<video>` elements), or present and **disabled** — a\n * document inside an `<iframe>` without the `allowfullscreen` attribute exposes\n * `requestFullscreen` and rejects every call to it, which `fullscreenEnabled`\n * reports up front.\n *\n * An engine that exposes the methods and no flag at all is treated as capable:\n * the flag is the newer half of the API, so its absence says nothing.\n *\n * @returns `true` when a fullscreen request stands a chance of being honoured.\n */\nexport function isFullscreenSupported(): boolean {\n if (typeof document === \"undefined\") return false;\n const root: FullscreenTarget = document.documentElement;\n const doc: FullscreenDocument = document;\n const reachable =\n typeof root.requestFullscreen === \"function\" ||\n typeof root.webkitRequestFullscreen === \"function\";\n return reachable && (doc.fullscreenEnabled ?? doc.webkitFullscreenEnabled ?? true);\n}\n\n/** Value returned by {@link useFullscreen}. */\nexport interface UseFullscreenResult {\n /** Whether the target element is the one the browser is presenting right now. */\n isFullscreen: boolean;\n /** Whether a request stands a chance at all — hide the control when `false`. */\n supported: boolean;\n /** Present the target. Must be called from a user gesture. Rejects if refused. */\n enter: () => Promise<void>;\n /** Leave fullscreen. Resolves immediately when nothing is presented. */\n exit: () => Promise<void>;\n /** Leave if the target is presented, otherwise enter. Same gesture rule as `enter`. */\n toggle: () => Promise<void>;\n}\n\n/**\n * Drive an immersive mode whose state stays true to the browser.\n *\n * The mistake this exists to remove is storing `isFullscreen` in state and\n * flipping it inside your own `enter()` / `exit()`. Fullscreen ends in ways your\n * code never sees — `Esc`, the browser's own exit affordance, F11 pressed while an\n * API fullscreen is active — and each of those leaves the flag saying \"sair\" over\n * a page that is already windowed. The only honest source is the\n * `fullscreenchange` event, which fires for every one of them, so that is what\n * this hook reads and what the returned `isFullscreen` reflects. The action\n * callbacks never touch it.\n *\n * The subscription is not this hook's own: it lives in `useFullscreenElement`,\n * shared with `usePortalHost`, which needs the same fact to decide where an\n * overlay mounts while fullscreen is on. Two independent listeners for one event\n * would mean two copies of the WebKit prefix dance and two places to fix the next\n * quirk, so the primitive answers \"which element is presented\" once and each\n * consumer asks its own question of the answer.\n *\n * WebKit's prefixed members are handled throughout — `webkitfullscreenchange`,\n * `webkitFullscreenElement`, `webkitRequestFullscreen`, `webkitExitFullscreen` —\n * because Safari on iPad still ships them and nothing else.\n *\n * `isFullscreen` is identity against the target, not containment: a `<video>`\n * inside your element going fullscreen on its own is not your element being\n * presented, and reporting `true` there would put your exit control inside a\n * subtree the browser is not painting.\n *\n * @param ref - The element to present. Omit it to present the whole page\n * (`document.documentElement`).\n * @returns `{ isFullscreen, supported, enter, exit, toggle }`.\n * @throws Nothing during render. `enter()` and `toggle()` reject with the\n * browser's own error when the request is refused — most often a `TypeError`\n * because the call did not come from a user gesture — and with an `Error` when\n * there is no element to present or the environment ships no Fullscreen API.\n *\n * @example\n * const stage = useRef<HTMLDivElement>(null);\n * const { isFullscreen, supported, toggle } = useFullscreen(stage);\n *\n * <div ref={stage}>\n * <video src=\"/aula.mp4\" controls />\n * {supported ? (\n * <button onClick={() => void toggle().catch(() => setRefused(true))}>\n * {isFullscreen ? \"Sair da tela cheia\" : \"Tela cheia\"}\n * </button>\n * ) : null}\n * </div>\n */\nexport function useFullscreen(ref?: RefObject<HTMLElement | null>): UseFullscreenResult {\n const element = useFullscreenElement();\n const [supported] = useState(isFullscreenSupported);\n const [isFullscreen, setIsFullscreen] = useState(false);\n\n useEffect(() => {\n const target = resolveTarget(ref);\n setIsFullscreen(target !== null && element === target);\n }, [element, ref]);\n\n const enter = useCallback(async (): Promise<void> => {\n const target = resolveTarget(ref);\n if (target === null) {\n throw new Error(\n \"useFullscreen: nothing to present. Either this environment has no document, \" +\n \"or the ref was never attached to a mounted element.\",\n );\n }\n const candidate: FullscreenTarget = target;\n if (typeof candidate.requestFullscreen === \"function\") {\n await candidate.requestFullscreen();\n return;\n }\n if (typeof candidate.webkitRequestFullscreen === \"function\") {\n await candidate.webkitRequestFullscreen();\n return;\n }\n throw new Error(\"useFullscreen: this browser exposes no way to enter fullscreen.\");\n }, [ref]);\n\n const exit = useCallback(async (): Promise<void> => {\n if (getFullscreenElement() === null) return;\n const doc: FullscreenDocument = document;\n if (typeof doc.exitFullscreen === \"function\") {\n await doc.exitFullscreen();\n return;\n }\n if (typeof doc.webkitExitFullscreen === \"function\") {\n await doc.webkitExitFullscreen();\n return;\n }\n throw new Error(\"useFullscreen: this browser exposes no way to leave fullscreen.\");\n }, []);\n\n const toggle = useCallback(async (): Promise<void> => {\n const target = resolveTarget(ref);\n if (target !== null && getFullscreenElement() === target) {\n await exit();\n return;\n }\n await enter();\n }, [enter, exit, ref]);\n\n return { isFullscreen, supported, enter, exit, toggle };\n}\n"],"mappings":";;;AAyCA,SAAS,EAAc,GAAyD;CAE5E,OADI,OAAO,WAAa,MAAoB,OACrC,IAAM,EAAI,UAAU,SAAS;AACxC;AAiBA,SAAgB,IAAiC;CAC7C,IAAI,OAAO,WAAa,KAAa,OAAO;CAC5C,IAAM,IAAyB,SAAS,iBAClC,IAA0B;CAIhC,QAFI,OAAO,EAAK,qBAAsB,cAClC,OAAO,EAAK,2BAA4B,gBACvB,EAAI,qBAAqB,EAAI,2BAA2B;AACjF;AAiEA,SAAgB,EAAc,GAA0D;CACpF,IAAM,IAAU,EAAqB,GAC/B,CAAC,KAAa,EAAS,CAAqB,GAC5C,CAAC,GAAc,KAAmB,EAAS,EAAK;CAEtD,QAAgB;EACZ,IAAM,IAAS,EAAc,CAAG;EAChC,EAAgB,MAAW,QAAQ,MAAY,CAAM;CACzD,GAAG,CAAC,GAAS,CAAG,CAAC;CAEjB,IAAM,IAAQ,EAAY,YAA2B;EACjD,IAAM,IAAS,EAAc,CAAG;EAChC,IAAI,MAAW,MACX,MAAU,MACN,iIAEJ;EAEJ,IAAM,IAA8B;EACpC,IAAI,OAAO,EAAU,qBAAsB,YAAY;GACnD,MAAM,EAAU,kBAAkB;GAClC;EACJ;EACA,IAAI,OAAO,EAAU,2BAA4B,YAAY;GACzD,MAAM,EAAU,wBAAwB;GACxC;EACJ;EACA,MAAU,MAAM,iEAAiE;CACrF,GAAG,CAAC,CAAG,CAAC,GAEF,IAAO,EAAY,YAA2B;EAChD,IAAI,EAAqB,MAAM,MAAM;EACrC,IAAM,IAA0B;EAChC,IAAI,OAAO,EAAI,kBAAmB,YAAY;GAC1C,MAAM,EAAI,eAAe;GACzB;EACJ;EACA,IAAI,OAAO,EAAI,wBAAyB,YAAY;GAChD,MAAM,EAAI,qBAAqB;GAC/B;EACJ;EACA,MAAU,MAAM,iEAAiE;CACrF,GAAG,CAAC,CAAC;CAWL,OAAO;EAAE;EAAc;EAAW;EAAO;EAAM,QAThC,EAAY,YAA2B;GAClD,IAAM,IAAS,EAAc,CAAG;GAChC,IAAI,MAAW,QAAQ,EAAqB,MAAM,GAAQ;IACtD,MAAM,EAAK;IACX;GACJ;GACA,MAAM,EAAM;EAChB,GAAG;GAAC;GAAO;GAAM;EAAG,CAE2B;CAAO;AAC1D"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("react");var t=[{code:`Space`,label:`Espaço`},{code:`ControlLeft`,label:`Ctrl esquerdo`},{code:`AltLeft`,label:`Alt esquerdo`},{code:`ShiftLeft`,label:`Shift esquerdo`},{code:`Backquote`,label:"Crase (`)"}],n=`Space`;function r(e){return t.find(t=>t.code===e)?.label??e}function i(e){if(!(e instanceof HTMLElement))return!1;if(e.isContentEditable)return!0;let t=e.tagName;return t===`INPUT`||t===`TEXTAREA`||t===`SELECT`}function a({code:t=n,onDown:r,onUp:a,enabled:o=!0}){let s=(0,e.useRef)({onDown:r,onUp:a});(0,e.useEffect)(()=>{s.current={onDown:r,onUp:a}}),(0,e.useEffect)(()=>{if(!o||typeof window>`u`)return;let e=!1,n=()=>{e&&(e=!1,s.current.onUp())},r=n=>{n.code!==t||n.repeat||i(n.target)||(n.preventDefault(),!e&&(e=!0,s.current.onDown()))},a=e=>{e.code===t&&(i(e.target)||(e.preventDefault(),n()))};return window.addEventListener(`keydown`,r),window.addEventListener(`keyup`,a),window.addEventListener(`blur`,n),()=>{window.removeEventListener(`keydown`,r),window.removeEventListener(`keyup`,a),window.removeEventListener(`blur`,n),n()}},[t,o])}exports.DEFAULT_PUSH_TO_TALK_KEY=n,exports.PUSH_TO_TALK_KEYS=t,exports.pushToTalkKeyLabel=r,exports.usePushToTalk=a;
|
|
2
|
+
//# sourceMappingURL=use-push-to-talk.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-push-to-talk.cjs","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n /** `KeyboardEvent.code`, which is layout-independent. */\n code: string;\n /** What to show a person. `code` itself is not a label. */\n label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n { code: \"Space\", label: \"Espaço\" },\n { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n { code: \"AltLeft\", label: \"Alt esquerdo\" },\n { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n * showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n code?: string;\n /** Called once when the key goes down. */\n onDown: () => void;\n /** Called when the key comes up, the window blurs, or the hook unmounts. */\n onUp: () => void;\n /**\n * Whether the binding is live. Default `true`.\n *\n * Turning it off releases first, so flipping a call from push-to-talk to an\n * open microphone while the key is held does not leave `onUp` unfired.\n */\n enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n * browser never sees the `keyup`, and the microphone stays open for as long as\n * the person is looking at another window — exactly what push-to-talk exists\n * to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n * and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n * `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n * code: \"Space\",\n * onDown: () => setMicEnabled(true),\n * onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n code = DEFAULT_PUSH_TO_TALK_KEY,\n onDown,\n onUp,\n enabled = true,\n}: UsePushToTalkOptions): void {\n const callbacks = useRef({ onDown, onUp });\n useEffect(() => {\n callbacks.current = { onDown, onUp };\n });\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n let held = false;\n\n const release = (): void => {\n if (!held) return;\n held = false;\n callbacks.current.onUp();\n };\n\n const handleKeyDown = (event: KeyboardEvent): void => {\n if (event.code !== code || event.repeat) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n if (held) return;\n held = true;\n callbacks.current.onDown();\n };\n\n const handleKeyUp = (event: KeyboardEvent): void => {\n if (event.code !== code) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n release();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n window.addEventListener(\"blur\", release);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n window.removeEventListener(\"blur\", release);\n release();\n };\n }, [code, enabled]);\n}\n"],"mappings":"uBAsBA,IAAa,EAA8C,CACvD,CAAE,KAAM,QAAS,MAAO,QAAS,EACjC,CAAE,KAAM,cAAe,MAAO,eAAgB,EAC9C,CAAE,KAAM,UAAW,MAAO,cAAe,EACzC,CAAE,KAAM,YAAa,MAAO,gBAAiB,EAC7C,CAAE,KAAM,YAAa,MAAO,WAAY,CAC5C,EAGa,EAA2B,QASxC,SAAgB,EAAmB,EAAsB,CACrD,OAAO,EAAkB,KAAM,GAAQ,EAAI,OAAS,CAAI,CAAC,EAAE,OAAS,CACxE,CAYA,SAAS,EAAe,EAAqC,CACzD,GAAI,EAAE,aAAkB,aAAc,MAAO,GAC7C,GAAI,EAAO,kBAAmB,MAAO,GACrC,IAAM,EAAM,EAAO,QACnB,OAAO,IAAQ,SAAW,IAAQ,YAAc,IAAQ,QAC5D,CAmDA,SAAgB,EAAc,CAC1B,OAAO,EACP,SACA,OACA,UAAU,IACiB,CAC3B,IAAM,GAAA,EAAY,EAAA,OAAA,CAAO,CAAE,SAAQ,MAAK,CAAC,GACzC,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAU,QAAU,CAAE,SAAQ,MAAK,CACvC,CAAC,GAED,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAW,OAAO,OAAW,IAAa,OAC/C,IAAI,EAAO,GAEL,MAAsB,CACnB,IACL,EAAO,GACP,EAAU,QAAQ,KAAK,EAC3B,EAEM,EAAiB,GAA+B,CAC9C,EAAM,OAAS,GAAQ,EAAM,QAC7B,EAAe,EAAM,MAAM,IAC/B,EAAM,eAAe,EACjB,KACJ,EAAO,GACP,EAAU,QAAQ,OAAO,GAC7B,EAEM,EAAe,GAA+B,CAC5C,EAAM,OAAS,IACf,EAAe,EAAM,MAAM,IAC/B,EAAM,eAAe,EACrB,EAAQ,GACZ,EAMA,OAJA,OAAO,iBAAiB,UAAW,CAAa,EAChD,OAAO,iBAAiB,QAAS,CAAW,EAC5C,OAAO,iBAAiB,OAAQ,CAAO,MAE1B,CACT,OAAO,oBAAoB,UAAW,CAAa,EACnD,OAAO,oBAAoB,QAAS,CAAW,EAC/C,OAAO,oBAAoB,OAAQ,CAAO,EAC1C,EAAQ,CACZ,CACJ,EAAG,CAAC,EAAM,CAAO,CAAC,CACtB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect as e, useRef as t } from "react";
|
|
2
|
+
//#region src/hooks/use-push-to-talk.ts
|
|
3
|
+
var n = [
|
|
4
|
+
{
|
|
5
|
+
code: "Space",
|
|
6
|
+
label: "Espaço"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
code: "ControlLeft",
|
|
10
|
+
label: "Ctrl esquerdo"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
code: "AltLeft",
|
|
14
|
+
label: "Alt esquerdo"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
code: "ShiftLeft",
|
|
18
|
+
label: "Shift esquerdo"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
code: "Backquote",
|
|
22
|
+
label: "Crase (`)"
|
|
23
|
+
}
|
|
24
|
+
], r = "Space";
|
|
25
|
+
function i(e) {
|
|
26
|
+
return n.find((t) => t.code === e)?.label ?? e;
|
|
27
|
+
}
|
|
28
|
+
function a(e) {
|
|
29
|
+
if (!(e instanceof HTMLElement)) return !1;
|
|
30
|
+
if (e.isContentEditable) return !0;
|
|
31
|
+
let t = e.tagName;
|
|
32
|
+
return t === "INPUT" || t === "TEXTAREA" || t === "SELECT";
|
|
33
|
+
}
|
|
34
|
+
function o({ code: n = r, onDown: i, onUp: o, enabled: s = !0 }) {
|
|
35
|
+
let c = t({
|
|
36
|
+
onDown: i,
|
|
37
|
+
onUp: o
|
|
38
|
+
});
|
|
39
|
+
e(() => {
|
|
40
|
+
c.current = {
|
|
41
|
+
onDown: i,
|
|
42
|
+
onUp: o
|
|
43
|
+
};
|
|
44
|
+
}), e(() => {
|
|
45
|
+
if (!s || typeof window > "u") return;
|
|
46
|
+
let e = !1, t = () => {
|
|
47
|
+
e && (e = !1, c.current.onUp());
|
|
48
|
+
}, r = (t) => {
|
|
49
|
+
t.code !== n || t.repeat || a(t.target) || (t.preventDefault(), !e && (e = !0, c.current.onDown()));
|
|
50
|
+
}, i = (e) => {
|
|
51
|
+
e.code === n && (a(e.target) || (e.preventDefault(), t()));
|
|
52
|
+
};
|
|
53
|
+
return window.addEventListener("keydown", r), window.addEventListener("keyup", i), window.addEventListener("blur", t), () => {
|
|
54
|
+
window.removeEventListener("keydown", r), window.removeEventListener("keyup", i), window.removeEventListener("blur", t), t();
|
|
55
|
+
};
|
|
56
|
+
}, [n, s]);
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { r as DEFAULT_PUSH_TO_TALK_KEY, n as PUSH_TO_TALK_KEYS, i as pushToTalkKeyLabel, o as usePushToTalk };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=use-push-to-talk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-push-to-talk.js","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n /** `KeyboardEvent.code`, which is layout-independent. */\n code: string;\n /** What to show a person. `code` itself is not a label. */\n label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n { code: \"Space\", label: \"Espaço\" },\n { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n { code: \"AltLeft\", label: \"Alt esquerdo\" },\n { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n * showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n code?: string;\n /** Called once when the key goes down. */\n onDown: () => void;\n /** Called when the key comes up, the window blurs, or the hook unmounts. */\n onUp: () => void;\n /**\n * Whether the binding is live. Default `true`.\n *\n * Turning it off releases first, so flipping a call from push-to-talk to an\n * open microphone while the key is held does not leave `onUp` unfired.\n */\n enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n * browser never sees the `keyup`, and the microphone stays open for as long as\n * the person is looking at another window — exactly what push-to-talk exists\n * to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n * and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n * `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n * code: \"Space\",\n * onDown: () => setMicEnabled(true),\n * onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n code = DEFAULT_PUSH_TO_TALK_KEY,\n onDown,\n onUp,\n enabled = true,\n}: UsePushToTalkOptions): void {\n const callbacks = useRef({ onDown, onUp });\n useEffect(() => {\n callbacks.current = { onDown, onUp };\n });\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n let held = false;\n\n const release = (): void => {\n if (!held) return;\n held = false;\n callbacks.current.onUp();\n };\n\n const handleKeyDown = (event: KeyboardEvent): void => {\n if (event.code !== code || event.repeat) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n if (held) return;\n held = true;\n callbacks.current.onDown();\n };\n\n const handleKeyUp = (event: KeyboardEvent): void => {\n if (event.code !== code) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n release();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n window.addEventListener(\"blur\", release);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n window.removeEventListener(\"blur\", release);\n release();\n };\n }, [code, enabled]);\n}\n"],"mappings":";;AAsBA,IAAa,IAA8C;CACvD;EAAE,MAAM;EAAS,OAAO;CAAS;CACjC;EAAE,MAAM;EAAe,OAAO;CAAgB;CAC9C;EAAE,MAAM;EAAW,OAAO;CAAe;CACzC;EAAE,MAAM;EAAa,OAAO;CAAiB;CAC7C;EAAE,MAAM;EAAa,OAAO;CAAY;AAC5C,GAGa,IAA2B;AASxC,SAAgB,EAAmB,GAAsB;CACrD,OAAO,EAAkB,MAAM,MAAQ,EAAI,SAAS,CAAI,CAAC,EAAE,SAAS;AACxE;AAYA,SAAS,EAAe,GAAqC;CACzD,IAAI,EAAE,aAAkB,cAAc,OAAO;CAC7C,IAAI,EAAO,mBAAmB,OAAO;CACrC,IAAM,IAAM,EAAO;CACnB,OAAO,MAAQ,WAAW,MAAQ,cAAc,MAAQ;AAC5D;AAmDA,SAAgB,EAAc,EAC1B,UAAO,GACP,WACA,SACA,aAAU,MACiB;CAC3B,IAAM,IAAY,EAAO;EAAE;EAAQ;CAAK,CAAC;CAKzC,AAJA,QAAgB;EACZ,EAAU,UAAU;GAAE;GAAQ;EAAK;CACvC,CAAC,GAED,QAAgB;EACZ,IAAI,CAAC,KAAW,OAAO,SAAW,KAAa;EAC/C,IAAI,IAAO,IAEL,UAAsB;GACnB,MACL,IAAO,IACP,EAAU,QAAQ,KAAK;EAC3B,GAEM,KAAiB,MAA+B;GAC9C,EAAM,SAAS,KAAQ,EAAM,UAC7B,EAAe,EAAM,MAAM,MAC/B,EAAM,eAAe,GACjB,OACJ,IAAO,IACP,EAAU,QAAQ,OAAO;EAC7B,GAEM,KAAe,MAA+B;GAC5C,EAAM,SAAS,MACf,EAAe,EAAM,MAAM,MAC/B,EAAM,eAAe,GACrB,EAAQ;EACZ;EAMA,OAJA,OAAO,iBAAiB,WAAW,CAAa,GAChD,OAAO,iBAAiB,SAAS,CAAW,GAC5C,OAAO,iBAAiB,QAAQ,CAAO,SAE1B;GAIT,AAHA,OAAO,oBAAoB,WAAW,CAAa,GACnD,OAAO,oBAAoB,SAAS,CAAW,GAC/C,OAAO,oBAAoB,QAAQ,CAAO,GAC1C,EAAQ;EACZ;CACJ,GAAG,CAAC,GAAM,CAAO,CAAC;AACtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-api-error.cjs","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Everything {@link describeApiError} accepts beyond the error and the fallback.\n *\n * Extends the fixed sentences rather than sitting beside them, so a caller that\n * already passed `{ offline, validation }` keeps compiling untouched.\n */\nexport interface DescribeApiErrorOptions extends Partial<ApiErrorStrings> {\n /**\n * Maps the backend's programmatic `code` to a sentence in your language.\n *\n * The client already surfaces `code` on `ApiError`, but without this every\n * app writes the same `switch` over it. A hit here wins over every other\n * step: it is the only sentence written for that exact case, by someone who\n * knew both the backend contract and the screen it lands on.\n */\n codes?: Readonly<Record<string, string>>;\n /**\n * Whether the backend's `detail` may be shown when no `code` matched.\n * Default `true`.\n *\n * Set it to `false` when `detail` is written for developers rather than\n * users, or when it could echo internals — the result is then always either\n * a sentence you wrote or the fallback.\n */\n useDetail?: boolean;\n}\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 0. `codes[error.code]` — the sentence you wrote for that exact backend case.\n * Checked first because nothing the funnel derives can beat it, and because a\n * request that never landed carries no `code` for it to shadow.\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a `422` the `detail` line is assembled
|
|
1
|
+
{"version":3,"file":"describe-api-error.cjs","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Everything {@link describeApiError} accepts beyond the error and the fallback.\n *\n * Extends the fixed sentences rather than sitting beside them, so a caller that\n * already passed `{ offline, validation }` keeps compiling untouched.\n */\nexport interface DescribeApiErrorOptions extends Partial<ApiErrorStrings> {\n /**\n * Maps the backend's programmatic `code` to a sentence in your language.\n *\n * The client already surfaces `code` on `ApiError`, but without this every\n * app writes the same `switch` over it. A hit here wins over every other\n * step: it is the only sentence written for that exact case, by someone who\n * knew both the backend contract and the screen it lands on.\n */\n codes?: Readonly<Record<string, string>>;\n /**\n * Whether the backend's `detail` may be shown when no `code` matched.\n * Default `true`.\n *\n * Set it to `false` when `detail` is written for developers rather than\n * users, or when it could echo internals — the result is then always either\n * a sentence you wrote or the fallback.\n */\n useDetail?: boolean;\n}\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 0. `codes[error.code]` — the sentence you wrote for that exact backend case.\n * Checked first because nothing the funnel derives can beat it, and because a\n * request that never landed carries no `code` for it to shadow.\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a FastAPI `422` the `detail` line is assembled\n * from the backend's field paths and the validator's own wording\n * (`\"items.0.price: Input should be greater than 0\"`), which is right for a\n * log and wrong for a person: it is half English in a pt-BR screen and it\n * names internals. The per-field messages stay on `fields`, where a form can\n * attach them to the inputs that failed.\n *\n * This step also catches a business error that named a single field, whose\n * `detail` **was** a finished sentence (\"Cidade não encontrada para o estado\n * informado.\"). The trade is deliberate: that sentence is now on `fields`,\n * against the input it is about, which is where it does the most good. Three\n * ways back, in order of preference: `codes: { VALIDATION_ERROR: \"…\" }` (step 0,\n * which still wins), `validation: error.detail` at the call site, or reading\n * `error.detail` yourself — it is untouched.\n * 3. The backend's own `detail`, which is the most specific thing available and\n * is already written for a person — unless `useDetail: false` says that text\n * is for developers.\n * 4. `fallback`, with `(HTTP <status>)` appended when a status is known, so the\n * screenshot in the support ticket carries the one fact a developer needs.\n *\n * Pure on purpose: it works in an interceptor, in a logger and anywhere outside\n * the React tree. {@link useDescribeApiError} is the same funnel with the\n * sentences resolved through `I18nProvider`.\n *\n * @example\n * catch (error) {\n * toast(describeApiError(error, \"Não foi possível salvar o pedido\"));\n * }\n *\n * @example\n * catch (error) {\n * toast(\n * describeApiError(error, \"Não foi possível se candidatar\", {\n * codes: {\n * SERVICE_FULL: \"Este serviço atingiu o limite de vagas.\",\n * CANDIDATE_ALREADY_EXISTS: \"Você já se candidatou a este serviço.\",\n * },\n * useDetail: false,\n * }),\n * );\n * }\n *\n * @param error - The caught value, of any shape.\n * @param fallback - What to say when the error carries nothing better.\n * @param options - A `codes` catalog, `useDetail`, and overrides for the fixed\n * sentences.\n * @returns A sentence to show the user.\n */\nexport function describeApiError(\n error: unknown,\n fallback: string,\n options?: DescribeApiErrorOptions,\n): string {\n const offline = options?.offline ?? DEFAULT_API_ERROR_STRINGS.offline;\n\n if (isApiError(error)) {\n const mapped = error.code === undefined ? undefined : options?.codes?.[error.code];\n if (mapped !== undefined) return mapped;\n if (error.status === 0) return offline;\n if (error.fields && Object.keys(error.fields).length > 0) {\n return options?.validation ?? DEFAULT_API_ERROR_STRINGS.validation;\n }\n const detail = error.detail.trim();\n if (\n options?.useDetail !== false &&\n detail !== \"\" &&\n detail !== syntheticDetail(error.status)\n ) {\n return detail;\n }\n return `${fallback} (HTTP ${error.status})`;\n }\n\n if (browserIsOffline()) return offline;\n\n return fallback;\n}\n"],"mappings":"gCA2BA,IAAa,EAA6C,CACtD,QAAS,sEACT,WAAY,+CAChB,EAUa,EAAwB,wBAMxB,EAA2B,2BAuCxC,SAAS,GAA4B,CACjC,OAAO,OAAO,UAAc,KAAe,UAAU,SAAW,EACpE,CA+DA,SAAgB,EACZ,EACA,EACA,EACM,CACN,IAAM,EAAU,GAAS,SAAW,EAA0B,QAE9D,GAAI,EAAA,WAAW,CAAK,EAAG,CACnB,IAAM,EAAS,EAAM,OAAS,IAAA,GAAY,IAAA,GAAY,GAAS,QAAQ,EAAM,MAC7E,GAAI,IAAW,IAAA,GAAW,OAAO,EACjC,GAAI,EAAM,SAAW,EAAG,OAAO,EAC/B,GAAI,EAAM,QAAU,OAAO,KAAK,EAAM,MAAM,CAAC,CAAC,OAAS,EACnD,OAAO,GAAS,YAAc,EAA0B,WAE5D,IAAM,EAAS,EAAM,OAAO,KAAK,EAQjC,OANI,GAAS,YAAc,IACvB,IAAW,IACX,IAAW,EAAA,gBAAgB,EAAM,MAAM,EAEhC,EAEJ,GAAG,EAAS,SAAS,EAAM,OAAO,EAC7C,CAIA,OAFI,EAAiB,EAAU,EAExB,CACX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-api-error.js","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Everything {@link describeApiError} accepts beyond the error and the fallback.\n *\n * Extends the fixed sentences rather than sitting beside them, so a caller that\n * already passed `{ offline, validation }` keeps compiling untouched.\n */\nexport interface DescribeApiErrorOptions extends Partial<ApiErrorStrings> {\n /**\n * Maps the backend's programmatic `code` to a sentence in your language.\n *\n * The client already surfaces `code` on `ApiError`, but without this every\n * app writes the same `switch` over it. A hit here wins over every other\n * step: it is the only sentence written for that exact case, by someone who\n * knew both the backend contract and the screen it lands on.\n */\n codes?: Readonly<Record<string, string>>;\n /**\n * Whether the backend's `detail` may be shown when no `code` matched.\n * Default `true`.\n *\n * Set it to `false` when `detail` is written for developers rather than\n * users, or when it could echo internals — the result is then always either\n * a sentence you wrote or the fallback.\n */\n useDetail?: boolean;\n}\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 0. `codes[error.code]` — the sentence you wrote for that exact backend case.\n * Checked first because nothing the funnel derives can beat it, and because a\n * request that never landed carries no `code` for it to shadow.\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a `422` the `detail` line is assembled
|
|
1
|
+
{"version":3,"file":"describe-api-error.js","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Everything {@link describeApiError} accepts beyond the error and the fallback.\n *\n * Extends the fixed sentences rather than sitting beside them, so a caller that\n * already passed `{ offline, validation }` keeps compiling untouched.\n */\nexport interface DescribeApiErrorOptions extends Partial<ApiErrorStrings> {\n /**\n * Maps the backend's programmatic `code` to a sentence in your language.\n *\n * The client already surfaces `code` on `ApiError`, but without this every\n * app writes the same `switch` over it. A hit here wins over every other\n * step: it is the only sentence written for that exact case, by someone who\n * knew both the backend contract and the screen it lands on.\n */\n codes?: Readonly<Record<string, string>>;\n /**\n * Whether the backend's `detail` may be shown when no `code` matched.\n * Default `true`.\n *\n * Set it to `false` when `detail` is written for developers rather than\n * users, or when it could echo internals — the result is then always either\n * a sentence you wrote or the fallback.\n */\n useDetail?: boolean;\n}\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 0. `codes[error.code]` — the sentence you wrote for that exact backend case.\n * Checked first because nothing the funnel derives can beat it, and because a\n * request that never landed carries no `code` for it to shadow.\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a FastAPI `422` the `detail` line is assembled\n * from the backend's field paths and the validator's own wording\n * (`\"items.0.price: Input should be greater than 0\"`), which is right for a\n * log and wrong for a person: it is half English in a pt-BR screen and it\n * names internals. The per-field messages stay on `fields`, where a form can\n * attach them to the inputs that failed.\n *\n * This step also catches a business error that named a single field, whose\n * `detail` **was** a finished sentence (\"Cidade não encontrada para o estado\n * informado.\"). The trade is deliberate: that sentence is now on `fields`,\n * against the input it is about, which is where it does the most good. Three\n * ways back, in order of preference: `codes: { VALIDATION_ERROR: \"…\" }` (step 0,\n * which still wins), `validation: error.detail` at the call site, or reading\n * `error.detail` yourself — it is untouched.\n * 3. The backend's own `detail`, which is the most specific thing available and\n * is already written for a person — unless `useDetail: false` says that text\n * is for developers.\n * 4. `fallback`, with `(HTTP <status>)` appended when a status is known, so the\n * screenshot in the support ticket carries the one fact a developer needs.\n *\n * Pure on purpose: it works in an interceptor, in a logger and anywhere outside\n * the React tree. {@link useDescribeApiError} is the same funnel with the\n * sentences resolved through `I18nProvider`.\n *\n * @example\n * catch (error) {\n * toast(describeApiError(error, \"Não foi possível salvar o pedido\"));\n * }\n *\n * @example\n * catch (error) {\n * toast(\n * describeApiError(error, \"Não foi possível se candidatar\", {\n * codes: {\n * SERVICE_FULL: \"Este serviço atingiu o limite de vagas.\",\n * CANDIDATE_ALREADY_EXISTS: \"Você já se candidatou a este serviço.\",\n * },\n * useDetail: false,\n * }),\n * );\n * }\n *\n * @param error - The caught value, of any shape.\n * @param fallback - What to say when the error carries nothing better.\n * @param options - A `codes` catalog, `useDetail`, and overrides for the fixed\n * sentences.\n * @returns A sentence to show the user.\n */\nexport function describeApiError(\n error: unknown,\n fallback: string,\n options?: DescribeApiErrorOptions,\n): string {\n const offline = options?.offline ?? DEFAULT_API_ERROR_STRINGS.offline;\n\n if (isApiError(error)) {\n const mapped = error.code === undefined ? undefined : options?.codes?.[error.code];\n if (mapped !== undefined) return mapped;\n if (error.status === 0) return offline;\n if (error.fields && Object.keys(error.fields).length > 0) {\n return options?.validation ?? DEFAULT_API_ERROR_STRINGS.validation;\n }\n const detail = error.detail.trim();\n if (\n options?.useDetail !== false &&\n detail !== \"\" &&\n detail !== syntheticDetail(error.status)\n ) {\n return detail;\n }\n return `${fallback} (HTTP ${error.status})`;\n }\n\n if (browserIsOffline()) return offline;\n\n return fallback;\n}\n"],"mappings":";;AA2BA,IAAa,IAA6C;CACtD,SAAS;CACT,YAAY;AAChB,GAUa,IAAwB,yBAMxB,IAA2B;AAuCxC,SAAS,IAA4B;CACjC,OAAO,OAAO,YAAc,OAAe,UAAU,WAAW;AACpE;AA+DA,SAAgB,EACZ,GACA,GACA,GACM;CACN,IAAM,IAAU,GAAS,WAAW,EAA0B;CAE9D,IAAI,EAAW,CAAK,GAAG;EACnB,IAAM,IAAS,EAAM,SAAS,KAAA,IAAY,KAAA,IAAY,GAAS,QAAQ,EAAM;EAC7E,IAAI,MAAW,KAAA,GAAW,OAAO;EACjC,IAAI,EAAM,WAAW,GAAG,OAAO;EAC/B,IAAI,EAAM,UAAU,OAAO,KAAK,EAAM,MAAM,CAAC,CAAC,SAAS,GACnD,OAAO,GAAS,cAAc,EAA0B;EAE5D,IAAM,IAAS,EAAM,OAAO,KAAK;EAQjC,OANI,GAAS,cAAc,MACvB,MAAW,MACX,MAAW,EAAgB,EAAM,MAAM,IAEhC,IAEJ,GAAG,EAAS,SAAS,EAAM,OAAO;CAC7C;CAIA,OAFI,EAAiB,IAAU,IAExB;AACX"}
|
package/dist/http/errors.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var e=class extends Error{status;detail;code;requestId;fields;body;constructor(e){super(e.detail),this.name=`TempestApiError`,this.status=e.status,this.detail=e.detail,this.code=e.code,this.requestId=e.requestId,this.fields=e.fields,this.body=e.body}};function t(e){return typeof e==`object`&&!!e&&typeof e.status==`number`&&typeof e.detail==`string`}var n=new Set([`body`,`query`,`path`,`header`,`cookie`]);function r(e){if(!Array.isArray(e))return;let t=e.filter(e=>typeof e==`string`||typeof e==`number`).filter((e,t)=>!(t===0&&n.has(String(e))));return t.length>0?t.join(`.`):void 0}var i=4;function a(e){if(!Array.isArray(e))return;let t={};for(let n of e){if(typeof n!=`object`||!n)continue;let e=n,i=r(e.loc);if(i===void 0||i in t)continue;let a=
|
|
1
|
+
var e=class extends Error{status;detail;code;requestId;fields;body;constructor(e){super(e.detail),this.name=`TempestApiError`,this.status=e.status,this.detail=e.detail,this.code=e.code,this.requestId=e.requestId,this.fields=e.fields,this.body=e.body}};function t(e){return typeof e==`object`&&!!e&&typeof e.status==`number`&&typeof e.detail==`string`}var n=new Set([`body`,`query`,`path`,`header`,`cookie`]);function r(e){if(!Array.isArray(e))return;let t=e.filter(e=>typeof e==`string`||typeof e==`number`).filter((e,t)=>!(t===0&&n.has(String(e))));return t.length>0?t.join(`.`):void 0}var i=4;function a(e){if(!Array.isArray(e))return;let t={};for(let n of e){if(typeof n!=`object`||!n)continue;let e=n,i=r(e.loc);if(i===void 0||i in t)continue;let a=l(e.msg)??l(e.message);a!==void 0&&(t[i]=a)}return Object.keys(t).length>0?t:void 0}function o(e){return typeof e==`object`&&e&&!Array.isArray(e)?e:void 0}function s(e){if(e===null)return;let t=[o(e.detail)?.field,e.field,o(e.details)?.field];for(let e of t)if(typeof e==`string`&&e!==``)return e}function c(e,t){let n=a(e?.detail);if(n!==void 0)return n;let r=s(e);if(r!==void 0&&t!==void 0)return{[r]:t}}function l(e,t=0){if(e!=null){if(typeof e==`string`)return e===``?void 0:e;if(typeof e==`number`||typeof e==`boolean`)return String(e);if(!(t>=i)){if(Array.isArray(e)){let n=e.map(e=>{let n=l(e,t+1);if(n===void 0)return;let i=typeof e==`object`&&e?r(e.loc):void 0;return i===void 0?n:`${i}: ${n}`}).filter(e=>e!==void 0);return n.length>0?n.join(`; `):void 0}if(typeof e==`object`){let n=e;return l(n.msg,t+1)??l(n.message,t+1)??l(n.detail,t+1)}}}}var u=new Set([0,408,425,429]);function d(e){return u.has(e)||e>=500}function f(e){return`Erro ${e}`}function p(e,t,n,r){let i=typeof t==`object`&&t?t:null,a=l(i?.detail)??l(i?.message),o=a??f(e),s=typeof i?.code==`string`?i.code:void 0,u=typeof i?.details==`object`&&i.details!==null?i.details:null;return{status:e,detail:o,code:s,requestId:(typeof u?.request_id==`string`?u.request_id:void 0)??n?.get(`X-Request-ID`)??r??void 0??void 0,retryAfter:m(n?.get(`Retry-After`)),fields:c(i,a),body:t}}function m(e){if(!e)return;let t=e.trim();if(/^\d+$/.test(t))return Number(t);let n=Date.parse(t);if(!Number.isNaN(n))return Math.max(0,Math.round((n-Date.now())/1e3))}exports.TempestApiError=e,exports.buildApiError=p,exports.isApiError=t,exports.isRetriableStatus=d,exports.parseRetryAfter=m,exports.syntheticDetail=f;
|
|
2
2
|
//# sourceMappingURL=errors.cjs.map
|
package/dist/http/errors.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.cjs","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import type { ApiError } from \"./types\";\n\n/**\n * Error thrown by {@link createApiClient} / {@link uploadWithProgress} on a\n * non-2xx response. Mirrors the Tempest FastAPI SDK error envelope\n * (`{ detail, code, details.request_id }`) so callers get a typed `code` and a\n * `requestId` for log correlation, while still being a real `Error` (stack\n * trace, `instanceof Error`).\n *\n * @example\n * try {\n * await api.post(\"/users\", { body });\n * } catch (err) {\n * if (isApiError(err) && err.code === \"EMAIL_TAKEN\") {\n * showFieldError(\"email\", err.detail);\n * }\n * }\n */\nexport class TempestApiError extends Error implements ApiError {\n readonly status: number;\n readonly detail: string;\n readonly code?: string;\n readonly requestId?: string;\n readonly fields?: Record<string, string>;\n readonly body?: unknown;\n\n constructor(init: ApiError) {\n super(init.detail);\n this.name = \"TempestApiError\";\n this.status = init.status;\n this.detail = init.detail;\n this.code = init.code;\n this.requestId = init.requestId;\n this.fields = init.fields;\n this.body = init.body;\n }\n}\n\n/**\n * Type guard for the {@link ApiError} shape. Matches both {@link TempestApiError}\n * instances and plain objects carrying `status` + `detail`.\n *\n * @param error - The unknown value (typically a caught error).\n * @returns Whether `error` conforms to the `ApiError` contract.\n */\nexport function isApiError(error: unknown): error is ApiError {\n return (\n typeof error === \"object\" &&\n error !== null &&\n typeof (error as ApiError).status === \"number\" &&\n typeof (error as ApiError).detail === \"string\"\n );\n}\n\n/**\n * Location prefixes FastAPI puts at the head of a validation error's `loc`,\n * naming the part of the request rather than the field. Dropped from the\n * rendered path, so `[\"body\", \"email\"]` reads as `email`.\n */\nconst LOC_ROOTS: ReadonlySet<string> = new Set([\"body\", \"query\", \"path\", \"header\", \"cookie\"]);\n\n/**\n * Render a FastAPI validation error's `loc` tuple as a dotted field path.\n *\n * @param loc - The raw `loc` value from one validation error entry.\n * @returns The dotted path (`\"items.0.price\"`), or undefined when `loc` carries\n * nothing addressable.\n */\nfunction formatLoc(loc: unknown): string | undefined {\n if (!Array.isArray(loc)) return undefined;\n const parts = loc\n .filter(\n (part): part is string | number => typeof part === \"string\" || typeof part === \"number\",\n )\n .filter((part, index) => !(index === 0 && LOC_ROOTS.has(String(part))));\n return parts.length > 0 ? parts.join(\".\") : undefined;\n}\n\n/**\n * How deep {@link normalizeDetail} follows a nested `detail` before giving up.\n *\n * A real envelope needs two or three levels: the list, an entry, the entry's\n * own `detail`. The cap exists because the body is untrusted input arriving on\n * the error path — a response nesting `{\"detail\":{\"detail\":…}}` twenty thousand\n * deep (a 220 KB body) overflowed the stack, and a `RangeError` thrown while\n * *building* the error is worse than the error: the caller's `catch` stops\n * receiving a `TempestApiError`, so `isApiError` is false, `describeApiError`\n * has nothing to read and the `401` handling never runs.\n */\nconst MAX_DETAIL_DEPTH = 4;\n\n/**\n * Pull field-level messages out of a validation `detail` list.\n *\n * FastAPI's `422` body is `detail: [{ loc, msg, type }]`, which is exactly what\n * a form needs and exactly what the flattened `detail` string destroys. Only the\n * top level is read: a validation error names one field per entry, and following\n * nesting here would invent paths the backend never sent.\n *\n * @param raw - The `detail` value from the error body.\n * @returns Field path to message, or undefined when the body is not a\n * validation list (or carries no entry naming a field).\n */\nfunction collectFields(raw: unknown): Record<string, string> | undefined {\n if (!Array.isArray(raw)) return undefined;\n\n const fields: Record<string, string> = {};\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const record = entry as Record<string, unknown>;\n const field = formatLoc(record.loc);\n if (field === undefined || field in fields) continue;\n const message = normalizeDetail(record.msg) ?? normalizeDetail(record.message);\n if (message === undefined) continue;\n fields[field] = message;\n }\n\n return Object.keys(fields).length > 0 ? fields : undefined;\n}\n\n/**\n * Collapse a backend `detail` of any shape into a single readable line.\n *\n * FastAPI answers a `422` with `detail` as a **list** of\n * `{ loc, msg, type }` entries, not a string. Passing that through `String()`\n * yields `\"[object Object]\"` — an error message that tells the user nothing and\n * hides which field failed. Each entry becomes `\"<field>: <msg>\"` and the\n * entries are joined with `\"; \"`; a nested object is read through its\n * `msg`/`message`/`detail` string.\n *\n * @param raw - The `detail` (or `message`) value from the error body.\n * @param depth - Current nesting level. Past {@link MAX_DETAIL_DEPTH} the value\n * is treated as unreadable instead of followed further.\n * @returns The rendered message, or undefined when nothing readable is there —\n * letting the caller fall back to the synthetic `Erro <status>`.\n */\nfunction normalizeDetail(raw: unknown, depth: number = 0): string | undefined {\n if (raw === null || raw === undefined) return undefined;\n if (typeof raw === \"string\") return raw === \"\" ? undefined : raw;\n if (typeof raw === \"number\" || typeof raw === \"boolean\") return String(raw);\n if (depth >= MAX_DETAIL_DEPTH) return undefined;\n\n if (Array.isArray(raw)) {\n const lines = raw\n .map((entry) => {\n const message = normalizeDetail(entry, depth + 1);\n if (message === undefined) return undefined;\n const field =\n typeof entry === \"object\" && entry !== null\n ? formatLoc((entry as Record<string, unknown>).loc)\n : undefined;\n return field === undefined ? message : `${field}: ${message}`;\n })\n .filter((line): line is string => line !== undefined);\n return lines.length > 0 ? lines.join(\"; \") : undefined;\n }\n\n if (typeof raw === \"object\") {\n const entry = raw as Record<string, unknown>;\n return (\n normalizeDetail(entry.msg, depth + 1) ??\n normalizeDetail(entry.message, depth + 1) ??\n normalizeDetail(entry.detail, depth + 1)\n );\n }\n\n return undefined;\n}\n\n/**\n * Statuses worth a second attempt, as a set for the sub-500 cases.\n *\n * A network failure (status `0`), a request timeout, a too-early replay, and a\n * rate limit — which usually carries the `Retry-After` the backoff honours.\n * Everything else below 500 is the server refusing on purpose.\n */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\n\n/**\n * Whether an HTTP status describes a condition a replay can plausibly fix.\n *\n * The single owner of that decision. It used to be spelled out in three places —\n * the client's own policy, the react-query default and the bare `retry()` helper\n * — and they had already drifted: the query default was missing `425`, so the\n * same `425 Too Early` was replayed through `createApiClient({ retry: true })`\n * and not replayed through `useQuery`. Same app, same error, two behaviours, and\n * no test caught it because each file asserted against its own copy.\n *\n * Deliberately about the status and nothing else. Whether a *non*-API error is\n * worth replaying, and whether the request's method may be replayed at all, are\n * the caller's calls: {@link createApiClient} refuses a non-idempotent method,\n * while a bare `retry()` has no method to inspect.\n *\n * @example\n * await api.get(\"/report\", {\n * retry: { shouldRetry: (error) => isApiError(error) && isRetriableStatus(error.status) },\n * });\n *\n * @param status - The HTTP status, where `0` means the request never landed.\n * @returns Whether a retry is worth attempting.\n */\nexport function isRetriableStatus(status: number): boolean {\n return RETRIABLE_STATUSES.has(status) || status >= 500;\n}\n\n/**\n * Detail text synthesised when a response body carries none.\n *\n * Exported because {@link describeApiError} has to recognise it: a detail the\n * server never sent says strictly less than the caller's own fallback, so the\n * funnel drops it. Comparing against a copied literal would silently stop\n * matching the day this sentence is reworded — no type error, no failing test.\n *\n * @param status - The HTTP status of the error.\n * @returns The synthetic detail for that status.\n */\nexport function syntheticDetail(status: number): string {\n return `Erro ${status}`;\n}\n\n/**\n * Parse an error body + response into the Tempest {@link ApiError} envelope.\n *\n * Reads `detail`/`message`, the programmatic `code`, and the correlation id\n * from `details.request_id` (falling back to the `X-Request-ID` header, then\n * the id the client sent).\n *\n * A `422` from FastAPI carries `detail` as a list of `{ loc, msg, type }`\n * entries, so it is flattened to `\"<field>: <msg>; <field>: <msg>\"` instead of\n * being stringified into `\"[object Object]\"`, and the same entries are indexed\n * on `fields` (`{ email: \"Field required\" }`) for a form to consume without\n * parsing that line back apart. The untouched body stays on `body`.\n *\n * That flattened `detail` is developer-facing: it carries the backend's field\n * paths and the validator's own wording. `describeApiError` knows not to show it\n * to a person when `fields` is set.\n *\n * @param status - HTTP status code.\n * @param body - The parsed error body (object, string, or null).\n * @param headers - The response headers (for the `X-Request-ID` fallback).\n * @param sentRequestId - The id the client sent on the request, if any.\n * @returns A fully-populated `ApiError`.\n *\n * @tempest-limits param-count — the arguments are the response as it arrives\n * (`status`, `body`, `headers`) plus the id the request was sent with, and they are\n * passed at exactly one place: the client's response path. Exported from the package\n * root, so the rewrite would be breaking for callers that build their own errors.\n */\nexport function buildApiError(\n status: number,\n body: unknown,\n headers?: Headers | { get(name: string): string | null },\n sentRequestId?: string,\n): ApiError {\n const obj =\n typeof body === \"object\" && body !== null ? (body as Record<string, unknown>) : null;\n const detail =\n normalizeDetail(obj?.detail) ?? normalizeDetail(obj?.message) ?? syntheticDetail(status);\n const code = typeof obj?.code === \"string\" ? obj.code : undefined;\n const details =\n typeof obj?.details === \"object\" && obj.details !== null\n ? (obj.details as Record<string, unknown>)\n : null;\n const requestId =\n (typeof details?.request_id === \"string\" ? details.request_id : undefined) ??\n headers?.get(\"X-Request-ID\") ??\n sentRequestId ??\n undefined;\n\n return {\n status,\n detail,\n code,\n requestId: requestId ?? undefined,\n retryAfter: parseRetryAfter(headers?.get(\"Retry-After\")),\n fields: collectFields(obj?.detail),\n body,\n };\n}\n\n/**\n * Parse a `Retry-After` header into seconds. Accepts a delta-seconds integer\n * (`\"120\"`) or an HTTP-date (`\"Wed, 21 Oct 2015 07:28:00 GMT\"`).\n *\n * @param value - The raw header value, or null.\n * @returns The delay in seconds (>= 0), or undefined when absent/unparseable.\n */\nexport function parseRetryAfter(value: string | null | undefined): number | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) return Number(trimmed);\n const when = Date.parse(trimmed);\n if (Number.isNaN(when)) return undefined;\n return Math.max(0, Math.round((when - Date.now()) / 1000));\n}\n"],"mappings":"AAkBA,IAAa,EAAb,cAAqC,KAA0B,CAC3D,OACA,OACA,KACA,UACA,OACA,KAEA,YAAY,EAAgB,CACxB,MAAM,EAAK,MAAM,EACjB,KAAK,KAAO,kBACZ,KAAK,OAAS,EAAK,OACnB,KAAK,OAAS,EAAK,OACnB,KAAK,KAAO,EAAK,KACjB,KAAK,UAAY,EAAK,UACtB,KAAK,OAAS,EAAK,OACnB,KAAK,KAAO,EAAK,IACrB,CACJ,EASA,SAAgB,EAAW,EAAmC,CAC1D,OACI,OAAO,GAAU,YACjB,GACA,OAAQ,EAAmB,QAAW,UACtC,OAAQ,EAAmB,QAAW,QAE9C,CAOA,IAAM,EAAiC,IAAI,IAAI,CAAC,OAAQ,QAAS,OAAQ,SAAU,QAAQ,CAAC,EAS5F,SAAS,EAAU,EAAkC,CACjD,GAAI,CAAC,MAAM,QAAQ,CAAG,EAAG,OACzB,IAAM,EAAQ,EACT,OACI,GAAkC,OAAO,GAAS,UAAY,OAAO,GAAS,QACnF,CAAC,CACA,QAAQ,EAAM,IAAU,EAAE,IAAU,GAAK,EAAU,IAAI,OAAO,CAAI,CAAC,EAAE,EAC1E,OAAO,EAAM,OAAS,EAAI,EAAM,KAAK,GAAG,EAAI,IAAA,EAChD,CAaA,IAAM,EAAmB,EAczB,SAAS,EAAc,EAAkD,CACrE,GAAI,CAAC,MAAM,QAAQ,CAAG,EAAG,OAEzB,IAAM,EAAiC,CAAC,EACxC,IAAK,IAAM,KAAS,EAAK,CACrB,GAAI,OAAO,GAAU,WAAY,EAAgB,SACjD,IAAM,EAAS,EACT,EAAQ,EAAU,EAAO,GAAG,EAClC,GAAI,IAAU,IAAA,IAAa,KAAS,EAAQ,SAC5C,IAAM,EAAU,EAAgB,EAAO,GAAG,GAAK,EAAgB,EAAO,OAAO,EACzE,IAAY,IAAA,KAChB,EAAO,GAAS,EACpB,CAEA,OAAO,OAAO,KAAK,CAAM,CAAC,CAAC,OAAS,EAAI,EAAS,IAAA,EACrD,CAkBA,SAAS,EAAgB,EAAc,EAAgB,EAAuB,CACtE,MAAQ,KACZ,IAAI,OAAO,GAAQ,SAAU,OAAO,IAAQ,GAAK,IAAA,GAAY,EAC7D,GAAI,OAAO,GAAQ,UAAY,OAAO,GAAQ,UAAW,OAAO,OAAO,CAAG,EACtE,QAAS,GAEb,IAAI,MAAM,QAAQ,CAAG,EAAG,CACpB,IAAM,EAAQ,EACT,IAAK,GAAU,CACZ,IAAM,EAAU,EAAgB,EAAO,EAAQ,CAAC,EAChD,GAAI,IAAY,IAAA,GAAW,OAC3B,IAAM,EACF,OAAO,GAAU,UAAY,EACvB,EAAW,EAAkC,GAAG,EAChD,IAAA,GACV,OAAO,IAAU,IAAA,GAAY,EAAU,GAAG,EAAM,IAAI,GACxD,CAAC,CAAC,CACD,OAAQ,GAAyB,IAAS,IAAA,EAAS,EACxD,OAAO,EAAM,OAAS,EAAI,EAAM,KAAK,IAAI,EAAI,IAAA,EACjD,CAEA,GAAI,OAAO,GAAQ,SAAU,CACzB,IAAM,EAAQ,EACd,OACI,EAAgB,EAAM,IAAK,EAAQ,CAAC,GACpC,EAAgB,EAAM,QAAS,EAAQ,CAAC,GACxC,EAAgB,EAAM,OAAQ,EAAQ,CAAC,CAE/C,CATA,CAjB6D,CA6BjE,CASA,IAAM,EAA0C,IAAI,IAAI,CAAC,EAAG,IAAK,IAAK,GAAG,CAAC,EAyB1E,SAAgB,EAAkB,EAAyB,CACvD,OAAO,EAAmB,IAAI,CAAM,GAAK,GAAU,GACvD,CAaA,SAAgB,EAAgB,EAAwB,CACpD,MAAO,QAAQ,GACnB,CA8BA,SAAgB,EACZ,EACA,EACA,EACA,EACQ,CACR,IAAM,EACF,OAAO,GAAS,UAAY,EAAiB,EAAmC,KAC9E,EACF,EAAgB,GAAK,MAAM,GAAK,EAAgB,GAAK,OAAO,GAAK,EAAgB,CAAM,EACrF,EAAO,OAAO,GAAK,MAAS,SAAW,EAAI,KAAO,IAAA,GAClD,EACF,OAAO,GAAK,SAAY,UAAY,EAAI,UAAY,KAC7C,EAAI,QACL,KAOV,MAAO,CACH,SACA,SACA,OACA,WATC,OAAO,GAAS,YAAe,SAAW,EAAQ,WAAa,IAAA,KAChE,GAAS,IAAI,cAAc,GAC3B,GACA,IAAA,IAMwB,IAAA,GACxB,WAAY,EAAgB,GAAS,IAAI,aAAa,CAAC,EACvD,OAAQ,EAAc,GAAK,MAAM,EACjC,MACJ,CACJ,CASA,SAAgB,EAAgB,EAAsD,CAClF,GAAI,CAAC,EAAO,OACZ,IAAM,EAAU,EAAM,KAAK,EAC3B,GAAI,QAAQ,KAAK,CAAO,EAAG,OAAO,OAAO,CAAO,EAChD,IAAM,EAAO,KAAK,MAAM,CAAO,EAC3B,WAAO,MAAM,CAAI,EACrB,OAAO,KAAK,IAAI,EAAG,KAAK,OAAO,EAAO,KAAK,IAAI,GAAK,GAAI,CAAC,CAC7D"}
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import type { ApiError } from \"./types\";\n\n/**\n * Error thrown by {@link createApiClient} / {@link uploadWithProgress} on a\n * non-2xx response. Mirrors the Tempest FastAPI SDK error envelope\n * (`{ detail, code, details.request_id }`) so callers get a typed `code` and a\n * `requestId` for log correlation, while still being a real `Error` (stack\n * trace, `instanceof Error`).\n *\n * @example\n * try {\n * await api.post(\"/users\", { body });\n * } catch (err) {\n * if (isApiError(err) && err.code === \"EMAIL_TAKEN\") {\n * showFieldError(\"email\", err.detail);\n * }\n * }\n */\nexport class TempestApiError extends Error implements ApiError {\n readonly status: number;\n readonly detail: string;\n readonly code?: string;\n readonly requestId?: string;\n readonly fields?: Record<string, string>;\n readonly body?: unknown;\n\n constructor(init: ApiError) {\n super(init.detail);\n this.name = \"TempestApiError\";\n this.status = init.status;\n this.detail = init.detail;\n this.code = init.code;\n this.requestId = init.requestId;\n this.fields = init.fields;\n this.body = init.body;\n }\n}\n\n/**\n * Type guard for the {@link ApiError} shape. Matches both {@link TempestApiError}\n * instances and plain objects carrying `status` + `detail`.\n *\n * @param error - The unknown value (typically a caught error).\n * @returns Whether `error` conforms to the `ApiError` contract.\n */\nexport function isApiError(error: unknown): error is ApiError {\n return (\n typeof error === \"object\" &&\n error !== null &&\n typeof (error as ApiError).status === \"number\" &&\n typeof (error as ApiError).detail === \"string\"\n );\n}\n\n/**\n * Location prefixes FastAPI puts at the head of a validation error's `loc`,\n * naming the part of the request rather than the field. Dropped from the\n * rendered path, so `[\"body\", \"email\"]` reads as `email`.\n */\nconst LOC_ROOTS: ReadonlySet<string> = new Set([\"body\", \"query\", \"path\", \"header\", \"cookie\"]);\n\n/**\n * Render a FastAPI validation error's `loc` tuple as a dotted field path.\n *\n * @param loc - The raw `loc` value from one validation error entry.\n * @returns The dotted path (`\"items.0.price\"`), or undefined when `loc` carries\n * nothing addressable.\n */\nfunction formatLoc(loc: unknown): string | undefined {\n if (!Array.isArray(loc)) return undefined;\n const parts = loc\n .filter(\n (part): part is string | number => typeof part === \"string\" || typeof part === \"number\",\n )\n .filter((part, index) => !(index === 0 && LOC_ROOTS.has(String(part))));\n return parts.length > 0 ? parts.join(\".\") : undefined;\n}\n\n/**\n * How deep {@link normalizeDetail} follows a nested `detail` before giving up.\n *\n * A real envelope needs two or three levels: the list, an entry, the entry's\n * own `detail`. The cap exists because the body is untrusted input arriving on\n * the error path — a response nesting `{\"detail\":{\"detail\":…}}` twenty thousand\n * deep (a 220 KB body) overflowed the stack, and a `RangeError` thrown while\n * *building* the error is worse than the error: the caller's `catch` stops\n * receiving a `TempestApiError`, so `isApiError` is false, `describeApiError`\n * has nothing to read and the `401` handling never runs.\n */\nconst MAX_DETAIL_DEPTH = 4;\n\n/**\n * Pull field-level messages out of a validation `detail` **list**.\n *\n * FastAPI's `422` body is `detail: [{ loc, msg, type }]`, which is exactly what\n * a form needs and exactly what the flattened `detail` string destroys. Only the\n * top level is read: a validation error names one field per entry, and following\n * nesting here would invent paths the backend never sent.\n *\n * This is one of two ways a body names a field — see {@link collectFields}, which\n * is the entry point and falls back to the singular keys when the list names\n * nothing addressable.\n *\n * @param raw - The `detail` value from the error body.\n * @returns Field path to message, or undefined when the body is not a\n * validation list (or carries no entry naming a field).\n */\nfunction collectListFields(raw: unknown): Record<string, string> | undefined {\n if (!Array.isArray(raw)) return undefined;\n\n const fields: Record<string, string> = {};\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const record = entry as Record<string, unknown>;\n const field = formatLoc(record.loc);\n if (field === undefined || field in fields) continue;\n const message = normalizeDetail(record.msg) ?? normalizeDetail(record.message);\n if (message === undefined) continue;\n fields[field] = message;\n }\n\n return Object.keys(fields).length > 0 ? fields : undefined;\n}\n\n/**\n * Narrow a value to a plain object — a record, and not an array.\n *\n * Arrays are excluded because `typeof [] === \"object\"` while a list means\n * something else entirely here: FastAPI's `detail` list is read by\n * {@link collectListFields}, and reading `.field` off it would only ever be\n * undefined.\n *\n * @param value - The candidate value from the error body.\n * @returns The value as a record, or undefined when it is not a plain object.\n */\nfunction plainRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\n/**\n * Read the field a **singular** error envelope names.\n *\n * A backend built on `tempest-fastapi-sdk` never answers with FastAPI's `detail`\n * list once it owns the handler: it names the guilty field in a key beside the\n * message. Three shapes are seen in the wild, and they are read inner-out:\n *\n * 1. `detail.field` — the field sits in the same object as the message it\n * describes (`{ detail: { detail: \"Cidade não encontrada…\", field: \"city\" } }`),\n * so it is the least ambiguous claim about which message belongs to which input.\n * 2. `field` at the top level — a flattened `RequestValidationError`\n * (`{ detail: \"Value error, … for field 'phone' in 'body'\", field: \"phone\" }`)\n * makes the same claim one level out.\n * 3. `details.field` — `details` is the envelope's free-form context bag, not a\n * validation channel, and its `field` may be about something no input on screen\n * carries (an unknown sort column, say). It answers last for exactly that reason.\n *\n * `location` (`\"body -> phone\"`) is deliberately not parsed: it renders the same\n * path `field` already names, no observed envelope sends it without `field`, and\n * splitting an arrow-separated string would invent a path the backend never sent.\n *\n * @param body - The parsed error body, or null when it was not an object.\n * @returns The field name, or undefined when none of the three keys carried a\n * non-empty string — anything else is not usable as a key on `fields`.\n */\nfunction namedField(body: Record<string, unknown> | null): string | undefined {\n if (body === null) return undefined;\n const candidates: readonly unknown[] = [\n plainRecord(body.detail)?.field,\n body.field,\n plainRecord(body.details)?.field,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === \"string\" && candidate !== \"\") return candidate;\n }\n return undefined;\n}\n\n/**\n * Index the body's field-level messages, whichever envelope carried them.\n *\n * The single entry point behind `ApiError.fields`, and it applies two rules the\n * tests pin:\n *\n * - FastAPI's `detail` list wins whenever it names at least one addressable\n * field. A plain FastAPI app still answers a schema-level `422` with that list\n * even on a `tempest-fastapi-sdk` backend, so it stays authoritative.\n * - A named field with no readable message produces nothing. The only string\n * left at that point is the synthetic `Erro <status>`, and `{ phone: \"Erro 422\" }`\n * on an input is noise rather than an error message.\n *\n * @param body - The parsed error body, or null when it was not an object.\n * @param message - The readable message the same body produced, before the\n * synthetic fallback — the string that also becomes `ApiError.detail`.\n * @returns Field name to message, or undefined when nothing named a field.\n */\nfunction collectFields(\n body: Record<string, unknown> | null,\n message: string | undefined,\n): Record<string, string> | undefined {\n const listed = collectListFields(body?.detail);\n if (listed !== undefined) return listed;\n const field = namedField(body);\n if (field === undefined || message === undefined) return undefined;\n return { [field]: message };\n}\n\n/**\n * Collapse a backend `detail` of any shape into a single readable line.\n *\n * FastAPI answers a `422` with `detail` as a **list** of\n * `{ loc, msg, type }` entries, not a string. Passing that through `String()`\n * yields `\"[object Object]\"` — an error message that tells the user nothing and\n * hides which field failed. Each entry becomes `\"<field>: <msg>\"` and the\n * entries are joined with `\"; \"`; a nested object is read through its\n * `msg`/`message`/`detail` string.\n *\n * @param raw - The `detail` (or `message`) value from the error body.\n * @param depth - Current nesting level. Past {@link MAX_DETAIL_DEPTH} the value\n * is treated as unreadable instead of followed further.\n * @returns The rendered message, or undefined when nothing readable is there —\n * letting the caller fall back to the synthetic `Erro <status>`.\n */\nfunction normalizeDetail(raw: unknown, depth: number = 0): string | undefined {\n if (raw === null || raw === undefined) return undefined;\n if (typeof raw === \"string\") return raw === \"\" ? undefined : raw;\n if (typeof raw === \"number\" || typeof raw === \"boolean\") return String(raw);\n if (depth >= MAX_DETAIL_DEPTH) return undefined;\n\n if (Array.isArray(raw)) {\n const lines = raw\n .map((entry) => {\n const message = normalizeDetail(entry, depth + 1);\n if (message === undefined) return undefined;\n const field =\n typeof entry === \"object\" && entry !== null\n ? formatLoc((entry as Record<string, unknown>).loc)\n : undefined;\n return field === undefined ? message : `${field}: ${message}`;\n })\n .filter((line): line is string => line !== undefined);\n return lines.length > 0 ? lines.join(\"; \") : undefined;\n }\n\n if (typeof raw === \"object\") {\n const entry = raw as Record<string, unknown>;\n return (\n normalizeDetail(entry.msg, depth + 1) ??\n normalizeDetail(entry.message, depth + 1) ??\n normalizeDetail(entry.detail, depth + 1)\n );\n }\n\n return undefined;\n}\n\n/**\n * Statuses worth a second attempt, as a set for the sub-500 cases.\n *\n * A network failure (status `0`), a request timeout, a too-early replay, and a\n * rate limit — which usually carries the `Retry-After` the backoff honours.\n * Everything else below 500 is the server refusing on purpose.\n */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\n\n/**\n * Whether an HTTP status describes a condition a replay can plausibly fix.\n *\n * The single owner of that decision. It used to be spelled out in three places —\n * the client's own policy, the react-query default and the bare `retry()` helper\n * — and they had already drifted: the query default was missing `425`, so the\n * same `425 Too Early` was replayed through `createApiClient({ retry: true })`\n * and not replayed through `useQuery`. Same app, same error, two behaviours, and\n * no test caught it because each file asserted against its own copy.\n *\n * Deliberately about the status and nothing else. Whether a *non*-API error is\n * worth replaying, and whether the request's method may be replayed at all, are\n * the caller's calls: {@link createApiClient} refuses a non-idempotent method,\n * while a bare `retry()` has no method to inspect.\n *\n * @example\n * await api.get(\"/report\", {\n * retry: { shouldRetry: (error) => isApiError(error) && isRetriableStatus(error.status) },\n * });\n *\n * @param status - The HTTP status, where `0` means the request never landed.\n * @returns Whether a retry is worth attempting.\n */\nexport function isRetriableStatus(status: number): boolean {\n return RETRIABLE_STATUSES.has(status) || status >= 500;\n}\n\n/**\n * Detail text synthesised when a response body carries none.\n *\n * Exported because {@link describeApiError} has to recognise it: a detail the\n * server never sent says strictly less than the caller's own fallback, so the\n * funnel drops it. Comparing against a copied literal would silently stop\n * matching the day this sentence is reworded — no type error, no failing test.\n *\n * @param status - The HTTP status of the error.\n * @returns The synthetic detail for that status.\n */\nexport function syntheticDetail(status: number): string {\n return `Erro ${status}`;\n}\n\n/**\n * Parse an error body + response into the Tempest {@link ApiError} envelope.\n *\n * Reads `detail`/`message`, the programmatic `code`, and the correlation id\n * from `details.request_id` (falling back to the `X-Request-ID` header, then\n * the id the client sent).\n *\n * A `422` from FastAPI carries `detail` as a list of `{ loc, msg, type }`\n * entries, so it is flattened to `\"<field>: <msg>; <field>: <msg>\"` instead of\n * being stringified into `\"[object Object]\"`, and the same entries are indexed\n * on `fields` (`{ email: \"Field required\" }`) for a form to consume without\n * parsing that line back apart. The untouched body stays on `body`.\n *\n * A backend that owns its handlers — every `tempest-fastapi-sdk` app — sends no\n * such list, and names the field in a key instead:\n *\n * ```json\n * { \"detail\": \"Value error, … for field 'phone' in 'body'\", \"field\": \"phone\" }\n * { \"detail\": { \"detail\": \"Cidade não encontrada…\", \"field\": \"city\" },\n * \"code\": \"VALIDATION_ERROR\", \"details\": { \"field\": \"city\" } }\n * ```\n *\n * Those are indexed too, keyed by the field the backend named and valued with the\n * same sentence that becomes `detail`. Precedence is the list first, then\n * `detail.field`, `field`, `details.field` — see {@link collectFields}.\n *\n * A flattened `detail` from the list is developer-facing: it carries the\n * backend's field paths and the validator's own wording. `describeApiError` knows\n * not to show it to a person when `fields` is set — which now also covers a\n * business error that named a field, whose `detail` was a finished sentence. That\n * sentence is not lost: it is on `fields`, attached to the input that failed.\n *\n * @param status - HTTP status code.\n * @param body - The parsed error body (object, string, or null).\n * @param headers - The response headers (for the `X-Request-ID` fallback).\n * @param sentRequestId - The id the client sent on the request, if any.\n * @returns A fully-populated `ApiError`.\n *\n * @tempest-limits param-count — the arguments are the response as it arrives\n * (`status`, `body`, `headers`) plus the id the request was sent with, and they are\n * passed at exactly three places, all of them a client's response path\n * (`createApiClient`, `uploadWithProgress`, `createResumableUpload`). Wrapping them\n * in an options object would name each argument twice at every call site to say\n * nothing new.\n */\nexport function buildApiError(\n status: number,\n body: unknown,\n headers?: Headers | { get(name: string): string | null },\n sentRequestId?: string,\n): ApiError {\n const obj =\n typeof body === \"object\" && body !== null ? (body as Record<string, unknown>) : null;\n const message = normalizeDetail(obj?.detail) ?? normalizeDetail(obj?.message);\n const detail = message ?? syntheticDetail(status);\n const code = typeof obj?.code === \"string\" ? obj.code : undefined;\n const details =\n typeof obj?.details === \"object\" && obj.details !== null\n ? (obj.details as Record<string, unknown>)\n : null;\n const requestId =\n (typeof details?.request_id === \"string\" ? details.request_id : undefined) ??\n headers?.get(\"X-Request-ID\") ??\n sentRequestId ??\n undefined;\n\n return {\n status,\n detail,\n code,\n requestId: requestId ?? undefined,\n retryAfter: parseRetryAfter(headers?.get(\"Retry-After\")),\n fields: collectFields(obj, message),\n body,\n };\n}\n\n/**\n * Parse a `Retry-After` header into seconds. Accepts a delta-seconds integer\n * (`\"120\"`) or an HTTP-date (`\"Wed, 21 Oct 2015 07:28:00 GMT\"`).\n *\n * @param value - The raw header value, or null.\n * @returns The delay in seconds (>= 0), or undefined when absent/unparseable.\n */\nexport function parseRetryAfter(value: string | null | undefined): number | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) return Number(trimmed);\n const when = Date.parse(trimmed);\n if (Number.isNaN(when)) return undefined;\n return Math.max(0, Math.round((when - Date.now()) / 1000));\n}\n"],"mappings":"AAkBA,IAAa,EAAb,cAAqC,KAA0B,CAC3D,OACA,OACA,KACA,UACA,OACA,KAEA,YAAY,EAAgB,CACxB,MAAM,EAAK,MAAM,EACjB,KAAK,KAAO,kBACZ,KAAK,OAAS,EAAK,OACnB,KAAK,OAAS,EAAK,OACnB,KAAK,KAAO,EAAK,KACjB,KAAK,UAAY,EAAK,UACtB,KAAK,OAAS,EAAK,OACnB,KAAK,KAAO,EAAK,IACrB,CACJ,EASA,SAAgB,EAAW,EAAmC,CAC1D,OACI,OAAO,GAAU,YACjB,GACA,OAAQ,EAAmB,QAAW,UACtC,OAAQ,EAAmB,QAAW,QAE9C,CAOA,IAAM,EAAiC,IAAI,IAAI,CAAC,OAAQ,QAAS,OAAQ,SAAU,QAAQ,CAAC,EAS5F,SAAS,EAAU,EAAkC,CACjD,GAAI,CAAC,MAAM,QAAQ,CAAG,EAAG,OACzB,IAAM,EAAQ,EACT,OACI,GAAkC,OAAO,GAAS,UAAY,OAAO,GAAS,QACnF,CAAC,CACA,QAAQ,EAAM,IAAU,EAAE,IAAU,GAAK,EAAU,IAAI,OAAO,CAAI,CAAC,EAAE,EAC1E,OAAO,EAAM,OAAS,EAAI,EAAM,KAAK,GAAG,EAAI,IAAA,EAChD,CAaA,IAAM,EAAmB,EAkBzB,SAAS,EAAkB,EAAkD,CACzE,GAAI,CAAC,MAAM,QAAQ,CAAG,EAAG,OAEzB,IAAM,EAAiC,CAAC,EACxC,IAAK,IAAM,KAAS,EAAK,CACrB,GAAI,OAAO,GAAU,WAAY,EAAgB,SACjD,IAAM,EAAS,EACT,EAAQ,EAAU,EAAO,GAAG,EAClC,GAAI,IAAU,IAAA,IAAa,KAAS,EAAQ,SAC5C,IAAM,EAAU,EAAgB,EAAO,GAAG,GAAK,EAAgB,EAAO,OAAO,EACzE,IAAY,IAAA,KAChB,EAAO,GAAS,EACpB,CAEA,OAAO,OAAO,KAAK,CAAM,CAAC,CAAC,OAAS,EAAI,EAAS,IAAA,EACrD,CAaA,SAAS,EAAY,EAAqD,CACtE,OAAO,OAAO,GAAU,UAAY,GAAkB,CAAC,MAAM,QAAQ,CAAK,EACnE,EACD,IAAA,EACV,CA2BA,SAAS,EAAW,EAA0D,CAC1E,GAAI,IAAS,KAAM,OACnB,IAAM,EAAiC,CACnC,EAAY,EAAK,MAAM,CAAC,EAAE,MAC1B,EAAK,MACL,EAAY,EAAK,OAAO,CAAC,EAAE,KAC/B,EACA,IAAK,IAAM,KAAa,EACpB,GAAI,OAAO,GAAc,UAAY,IAAc,GAAI,OAAO,CAGtE,CAoBA,SAAS,EACL,EACA,EACkC,CAClC,IAAM,EAAS,EAAkB,GAAM,MAAM,EAC7C,GAAI,IAAW,IAAA,GAAW,OAAO,EACjC,IAAM,EAAQ,EAAW,CAAI,EACzB,OAAU,IAAA,IAAa,IAAY,IAAA,GACvC,MAAO,EAAG,GAAQ,CAAQ,CAC9B,CAkBA,SAAS,EAAgB,EAAc,EAAgB,EAAuB,CACtE,MAAQ,KACZ,IAAI,OAAO,GAAQ,SAAU,OAAO,IAAQ,GAAK,IAAA,GAAY,EAC7D,GAAI,OAAO,GAAQ,UAAY,OAAO,GAAQ,UAAW,OAAO,OAAO,CAAG,EACtE,QAAS,GAEb,IAAI,MAAM,QAAQ,CAAG,EAAG,CACpB,IAAM,EAAQ,EACT,IAAK,GAAU,CACZ,IAAM,EAAU,EAAgB,EAAO,EAAQ,CAAC,EAChD,GAAI,IAAY,IAAA,GAAW,OAC3B,IAAM,EACF,OAAO,GAAU,UAAY,EACvB,EAAW,EAAkC,GAAG,EAChD,IAAA,GACV,OAAO,IAAU,IAAA,GAAY,EAAU,GAAG,EAAM,IAAI,GACxD,CAAC,CAAC,CACD,OAAQ,GAAyB,IAAS,IAAA,EAAS,EACxD,OAAO,EAAM,OAAS,EAAI,EAAM,KAAK,IAAI,EAAI,IAAA,EACjD,CAEA,GAAI,OAAO,GAAQ,SAAU,CACzB,IAAM,EAAQ,EACd,OACI,EAAgB,EAAM,IAAK,EAAQ,CAAC,GACpC,EAAgB,EAAM,QAAS,EAAQ,CAAC,GACxC,EAAgB,EAAM,OAAQ,EAAQ,CAAC,CAE/C,CATA,CAjB6D,CA6BjE,CASA,IAAM,EAA0C,IAAI,IAAI,CAAC,EAAG,IAAK,IAAK,GAAG,CAAC,EAyB1E,SAAgB,EAAkB,EAAyB,CACvD,OAAO,EAAmB,IAAI,CAAM,GAAK,GAAU,GACvD,CAaA,SAAgB,EAAgB,EAAwB,CACpD,MAAO,QAAQ,GACnB,CA+CA,SAAgB,EACZ,EACA,EACA,EACA,EACQ,CACR,IAAM,EACF,OAAO,GAAS,UAAY,EAAiB,EAAmC,KAC9E,EAAU,EAAgB,GAAK,MAAM,GAAK,EAAgB,GAAK,OAAO,EACtE,EAAS,GAAW,EAAgB,CAAM,EAC1C,EAAO,OAAO,GAAK,MAAS,SAAW,EAAI,KAAO,IAAA,GAClD,EACF,OAAO,GAAK,SAAY,UAAY,EAAI,UAAY,KAC7C,EAAI,QACL,KAOV,MAAO,CACH,SACA,SACA,OACA,WATC,OAAO,GAAS,YAAe,SAAW,EAAQ,WAAa,IAAA,KAChE,GAAS,IAAI,cAAc,GAC3B,GACA,IAAA,IAMwB,IAAA,GACxB,WAAY,EAAgB,GAAS,IAAI,aAAa,CAAC,EACvD,OAAQ,EAAc,EAAK,CAAO,EAClC,MACJ,CACJ,CASA,SAAgB,EAAgB,EAAsD,CAClF,GAAI,CAAC,EAAO,OACZ,IAAM,EAAU,EAAM,KAAK,EAC3B,GAAI,QAAQ,KAAK,CAAO,EAAG,OAAO,OAAO,CAAO,EAChD,IAAM,EAAO,KAAK,MAAM,CAAO,EAC3B,WAAO,MAAM,CAAI,EACrB,OAAO,KAAK,IAAI,EAAG,KAAK,OAAO,EAAO,KAAK,IAAI,GAAK,GAAI,CAAC,CAC7D"}
|