tempest-react-sdk 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/bin/lib/css/findings.mjs +1 -0
- package/bin/lib/css/semantic.mjs +70 -6
- package/bin/lib/css/semantic.test.mjs +52 -1
- package/bin/lib/icons/generate.test.mjs +4 -4
- package/dist/br/geocode.cjs +1 -1
- package/dist/br/geocode.cjs.map +1 -1
- package/dist/br/geocode.js +1 -1
- package/dist/br/geocode.js.map +1 -1
- package/dist/capture/use-barcode-scanner.cjs +1 -1
- package/dist/capture/use-barcode-scanner.cjs.map +1 -1
- package/dist/capture/use-barcode-scanner.js +1 -1
- package/dist/capture/use-barcode-scanner.js.map +1 -1
- package/dist/components/AIChat/AIChat.cjs +1 -1
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js +1 -1
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
- package/dist/components/AIChat/AIChatTurn.js +1 -1
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/Alert/Alert.module.cjs.map +1 -1
- package/dist/components/Alert/Alert.module.js.map +1 -1
- package/dist/components/AppBar/use-sticky-body-warning.cjs +1 -1
- package/dist/components/AppBar/use-sticky-body-warning.cjs.map +1 -1
- package/dist/components/AppBar/use-sticky-body-warning.js +1 -1
- package/dist/components/AppBar/use-sticky-body-warning.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.js +2 -2
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
- package/dist/components/Badge/Badge.module.cjs.map +1 -1
- package/dist/components/Badge/Badge.module.js.map +1 -1
- package/dist/components/BarList/BarList.module.cjs.map +1 -1
- package/dist/components/BarList/BarList.module.js.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.module.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.module.js.map +1 -1
- package/dist/components/Chat/Chat.cjs +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js +1 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/Dropzone/Dropzone.module.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.module.js.map +1 -1
- package/dist/components/ErrorText/ErrorText.module.cjs.map +1 -1
- package/dist/components/ErrorText/ErrorText.module.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.cjs +1 -1
- package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.js +1 -1
- package/dist/components/FilterBar/FilterBar.js.map +1 -1
- package/dist/components/FilterBar/filter-apply.cjs +1 -1
- package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
- package/dist/components/FilterBar/filter-apply.js +1 -1
- package/dist/components/FilterBar/filter-apply.js.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.js +1 -1
- package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
- package/dist/components/Markdown/Markdown.module.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.module.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.module.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.module.js.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.module.cjs.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.module.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.js.map +1 -1
- package/dist/components/StepperInput/StepperInput.module.cjs.map +1 -1
- package/dist/components/StepperInput/StepperInput.module.js.map +1 -1
- package/dist/components/Tag/Tag.module.cjs.map +1 -1
- package/dist/components/Tag/Tag.module.js.map +1 -1
- package/dist/components/Timeline/Timeline.module.cjs.map +1 -1
- package/dist/components/Timeline/Timeline.module.js.map +1 -1
- package/dist/components/Toggle/Toggle.module.cjs.map +1 -1
- package/dist/components/Toggle/Toggle.module.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.js.map +1 -1
- package/dist/components/VideoPlayer/VideoPlayer.cjs +1 -1
- package/dist/components/VideoPlayer/VideoPlayer.cjs.map +1 -1
- package/dist/components/VideoPlayer/VideoPlayer.js +2 -2
- package/dist/components/VideoPlayer/VideoPlayer.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js +1 -1
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/feature-flags/growthbook-adapter.cjs +1 -1
- package/dist/feature-flags/growthbook-adapter.cjs.map +1 -1
- package/dist/feature-flags/growthbook-adapter.js +1 -1
- package/dist/feature-flags/growthbook-adapter.js.map +1 -1
- package/dist/hooks/use-focus-trap.cjs +1 -1
- package/dist/hooks/use-focus-trap.cjs.map +1 -1
- package/dist/hooks/use-focus-trap.js +1 -1
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-push-to-talk.cjs +1 -1
- package/dist/hooks/use-push-to-talk.cjs.map +1 -1
- package/dist/hooks/use-push-to-talk.js +1 -1
- package/dist/hooks/use-push-to-talk.js.map +1 -1
- package/dist/http/describe-api-error.cjs +1 -1
- package/dist/http/describe-api-error.cjs.map +1 -1
- package/dist/http/describe-api-error.js +12 -8
- package/dist/http/describe-api-error.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +21 -16
- package/dist/http/errors.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +1 -1
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/icons/generated/aliases.cjs +1 -1
- package/dist/icons/generated/aliases.cjs.map +1 -1
- package/dist/icons/generated/aliases.js +1 -0
- package/dist/icons/generated/aliases.js.map +1 -1
- package/dist/icons/generated/icon-names.cjs +1 -1
- package/dist/icons/generated/icon-names.cjs.map +1 -1
- package/dist/icons/generated/icon-names.js +1 -1
- package/dist/icons/generated/icon-names.js.map +1 -1
- package/dist/icons/generated/loaders.cjs +1 -1
- package/dist/icons/generated/loaders.cjs.map +1 -1
- package/dist/icons/generated/loaders.js +48 -42
- package/dist/icons/generated/loaders.js.map +1 -1
- package/dist/icons/generated/shard-02.cjs +1 -1
- package/dist/icons/generated/shard-02.cjs.map +1 -1
- package/dist/icons/generated/shard-02.js +20 -20
- package/dist/icons/generated/shard-02.js.map +1 -1
- package/dist/icons/generated/shard-03.cjs +1 -1
- package/dist/icons/generated/shard-03.cjs.map +1 -1
- package/dist/icons/generated/shard-03.js +41 -41
- package/dist/icons/generated/shard-03.js.map +1 -1
- package/dist/icons/generated/shard-04.cjs +1 -1
- package/dist/icons/generated/shard-04.cjs.map +1 -1
- package/dist/icons/generated/shard-04.js +41 -41
- package/dist/icons/generated/shard-04.js.map +1 -1
- package/dist/icons/generated/shard-05.cjs +1 -1
- package/dist/icons/generated/shard-05.cjs.map +1 -1
- package/dist/icons/generated/shard-05.js +41 -41
- package/dist/icons/generated/shard-05.js.map +1 -1
- package/dist/icons/generated/shard-06.cjs +1 -1
- package/dist/icons/generated/shard-06.cjs.map +1 -1
- package/dist/icons/generated/shard-06.js +41 -41
- package/dist/icons/generated/shard-06.js.map +1 -1
- package/dist/icons/generated/shard-07.cjs +1 -1
- package/dist/icons/generated/shard-07.cjs.map +1 -1
- package/dist/icons/generated/shard-07.js +41 -41
- package/dist/icons/generated/shard-07.js.map +1 -1
- package/dist/icons/generated/shard-08.cjs +1 -1
- package/dist/icons/generated/shard-08.cjs.map +1 -1
- package/dist/icons/generated/shard-08.js +41 -41
- package/dist/icons/generated/shard-08.js.map +1 -1
- package/dist/icons/generated/shard-09.cjs +1 -1
- package/dist/icons/generated/shard-09.cjs.map +1 -1
- package/dist/icons/generated/shard-09.js +41 -41
- package/dist/icons/generated/shard-09.js.map +1 -1
- package/dist/icons/generated/shard-10.cjs +1 -1
- package/dist/icons/generated/shard-10.cjs.map +1 -1
- package/dist/icons/generated/shard-10.js +41 -41
- package/dist/icons/generated/shard-10.js.map +1 -1
- package/dist/icons/generated/shard-11.cjs +1 -1
- package/dist/icons/generated/shard-11.cjs.map +1 -1
- package/dist/icons/generated/shard-11.js +41 -41
- package/dist/icons/generated/shard-11.js.map +1 -1
- package/dist/icons/generated/shard-12.cjs +1 -1
- package/dist/icons/generated/shard-12.cjs.map +1 -1
- package/dist/icons/generated/shard-12.js +41 -41
- package/dist/icons/generated/shard-12.js.map +1 -1
- package/dist/icons/generated/shard-13.cjs +1 -1
- package/dist/icons/generated/shard-13.cjs.map +1 -1
- package/dist/icons/generated/shard-13.js +41 -41
- package/dist/icons/generated/shard-13.js.map +1 -1
- package/dist/icons/generated/shard-14.cjs +1 -1
- package/dist/icons/generated/shard-14.cjs.map +1 -1
- package/dist/icons/generated/shard-14.js +41 -41
- package/dist/icons/generated/shard-14.js.map +1 -1
- package/dist/icons/generated/shard-15.cjs +1 -1
- package/dist/icons/generated/shard-15.cjs.map +1 -1
- package/dist/icons/generated/shard-15.js +41 -41
- package/dist/icons/generated/shard-15.js.map +1 -1
- package/dist/icons/generated/shard-16.cjs +1 -1
- package/dist/icons/generated/shard-16.cjs.map +1 -1
- package/dist/icons/generated/shard-16.js +41 -41
- package/dist/icons/generated/shard-16.js.map +1 -1
- package/dist/icons/generated/shard-17.cjs +1 -1
- package/dist/icons/generated/shard-17.cjs.map +1 -1
- package/dist/icons/generated/shard-17.js +41 -41
- package/dist/icons/generated/shard-17.js.map +1 -1
- package/dist/icons/generated/shard-18.cjs +1 -1
- package/dist/icons/generated/shard-18.cjs.map +1 -1
- package/dist/icons/generated/shard-18.js +41 -41
- package/dist/icons/generated/shard-18.js.map +1 -1
- package/dist/icons/generated/shard-19.cjs +1 -1
- package/dist/icons/generated/shard-19.cjs.map +1 -1
- package/dist/icons/generated/shard-19.js +41 -41
- package/dist/icons/generated/shard-19.js.map +1 -1
- package/dist/icons/generated/shard-20.cjs +1 -1
- package/dist/icons/generated/shard-20.cjs.map +1 -1
- package/dist/icons/generated/shard-20.js +41 -41
- package/dist/icons/generated/shard-20.js.map +1 -1
- package/dist/icons/generated/shard-21.cjs +1 -1
- package/dist/icons/generated/shard-21.cjs.map +1 -1
- package/dist/icons/generated/shard-21.js +41 -41
- package/dist/icons/generated/shard-21.js.map +1 -1
- package/dist/icons/generated/shard-22.cjs +1 -1
- package/dist/icons/generated/shard-22.cjs.map +1 -1
- package/dist/icons/generated/shard-22.js +41 -41
- package/dist/icons/generated/shard-22.js.map +1 -1
- package/dist/icons/generated/shard-23.cjs +1 -1
- package/dist/icons/generated/shard-23.cjs.map +1 -1
- package/dist/icons/generated/shard-23.js +41 -41
- package/dist/icons/generated/shard-23.js.map +1 -1
- package/dist/icons/generated/shard-24.cjs +1 -1
- package/dist/icons/generated/shard-24.cjs.map +1 -1
- package/dist/icons/generated/shard-24.js +41 -41
- package/dist/icons/generated/shard-24.js.map +1 -1
- package/dist/icons/generated/shard-25.cjs +1 -1
- package/dist/icons/generated/shard-25.cjs.map +1 -1
- package/dist/icons/generated/shard-25.js +41 -41
- package/dist/icons/generated/shard-25.js.map +1 -1
- package/dist/icons/generated/shard-26.cjs +1 -1
- package/dist/icons/generated/shard-26.cjs.map +1 -1
- package/dist/icons/generated/shard-26.js +41 -41
- package/dist/icons/generated/shard-26.js.map +1 -1
- package/dist/icons/generated/shard-27.cjs +1 -1
- package/dist/icons/generated/shard-27.cjs.map +1 -1
- package/dist/icons/generated/shard-27.js +41 -41
- package/dist/icons/generated/shard-27.js.map +1 -1
- package/dist/icons/generated/shard-28.cjs +1 -1
- package/dist/icons/generated/shard-28.cjs.map +1 -1
- package/dist/icons/generated/shard-28.js +41 -41
- package/dist/icons/generated/shard-28.js.map +1 -1
- package/dist/icons/generated/shard-29.cjs +1 -1
- package/dist/icons/generated/shard-29.cjs.map +1 -1
- package/dist/icons/generated/shard-29.js +41 -41
- package/dist/icons/generated/shard-29.js.map +1 -1
- package/dist/icons/generated/shard-30.cjs +1 -1
- package/dist/icons/generated/shard-30.cjs.map +1 -1
- package/dist/icons/generated/shard-30.js +41 -41
- package/dist/icons/generated/shard-30.js.map +1 -1
- package/dist/icons/generated/shard-31.cjs +1 -1
- package/dist/icons/generated/shard-31.cjs.map +1 -1
- package/dist/icons/generated/shard-31.js +41 -41
- package/dist/icons/generated/shard-31.js.map +1 -1
- package/dist/icons/generated/shard-32.cjs +1 -1
- package/dist/icons/generated/shard-32.cjs.map +1 -1
- package/dist/icons/generated/shard-32.js +41 -41
- package/dist/icons/generated/shard-32.js.map +1 -1
- package/dist/icons/generated/shard-33.cjs +1 -1
- package/dist/icons/generated/shard-33.cjs.map +1 -1
- package/dist/icons/generated/shard-33.js +41 -41
- package/dist/icons/generated/shard-33.js.map +1 -1
- package/dist/icons/generated/shard-34.cjs +1 -1
- package/dist/icons/generated/shard-34.cjs.map +1 -1
- package/dist/icons/generated/shard-34.js +41 -41
- package/dist/icons/generated/shard-34.js.map +1 -1
- package/dist/icons/generated/shard-35.cjs +1 -1
- package/dist/icons/generated/shard-35.cjs.map +1 -1
- package/dist/icons/generated/shard-35.js +41 -41
- package/dist/icons/generated/shard-35.js.map +1 -1
- package/dist/icons/generated/shard-36.cjs +1 -1
- package/dist/icons/generated/shard-36.cjs.map +1 -1
- package/dist/icons/generated/shard-36.js +41 -41
- package/dist/icons/generated/shard-36.js.map +1 -1
- package/dist/icons/generated/shard-37.cjs +1 -1
- package/dist/icons/generated/shard-37.cjs.map +1 -1
- package/dist/icons/generated/shard-37.js +41 -41
- package/dist/icons/generated/shard-37.js.map +1 -1
- package/dist/icons/generated/shard-38.cjs +1 -1
- package/dist/icons/generated/shard-38.cjs.map +1 -1
- package/dist/icons/generated/shard-38.js +41 -41
- package/dist/icons/generated/shard-38.js.map +1 -1
- package/dist/icons/generated/shard-39.cjs +1 -1
- package/dist/icons/generated/shard-39.cjs.map +1 -1
- package/dist/icons/generated/shard-39.js +41 -41
- package/dist/icons/generated/shard-39.js.map +1 -1
- package/dist/icons/generated/shard-40.cjs +1 -1
- package/dist/icons/generated/shard-40.cjs.map +1 -1
- package/dist/icons/generated/shard-40.js +41 -41
- package/dist/icons/generated/shard-40.js.map +1 -1
- package/dist/icons/generated/shard-41.cjs +1 -1
- package/dist/icons/generated/shard-41.cjs.map +1 -1
- package/dist/icons/generated/shard-41.js +41 -41
- package/dist/icons/generated/shard-41.js.map +1 -1
- package/dist/icons/generated/shard-42.cjs +1 -1
- package/dist/icons/generated/shard-42.cjs.map +1 -1
- package/dist/icons/generated/shard-42.js +41 -41
- package/dist/icons/generated/shard-42.js.map +1 -1
- package/dist/icons/generated/shard-43.cjs +1 -1
- package/dist/icons/generated/shard-43.cjs.map +1 -1
- package/dist/icons/generated/shard-43.js +41 -41
- package/dist/icons/generated/shard-43.js.map +1 -1
- package/dist/icons/generated/shard-44.cjs +1 -1
- package/dist/icons/generated/shard-44.cjs.map +1 -1
- package/dist/icons/generated/shard-44.js +43 -10
- package/dist/icons/generated/shard-44.js.map +1 -1
- package/dist/icons/generated/shard-45.cjs +2 -0
- package/dist/icons/generated/shard-45.cjs.map +1 -0
- package/dist/icons/generated/shard-45.js +15 -0
- package/dist/icons/generated/shard-45.js.map +1 -0
- package/dist/icons/material-symbols.cjs +1 -1
- package/dist/icons/material-symbols.cjs.map +1 -1
- package/dist/icons/material-symbols.js +3 -3
- package/dist/icons/material-symbols.js.map +1 -1
- package/dist/icons.d.ts +4 -4
- package/dist/query/foreign-client-warning.cjs +1 -1
- package/dist/query/foreign-client-warning.cjs.map +1 -1
- package/dist/query/foreign-client-warning.js +1 -1
- package/dist/query/foreign-client-warning.js.map +1 -1
- package/dist/styles/Alert.css +1 -1
- package/dist/styles/Badge.css +1 -1
- package/dist/styles/BarList.css +3 -3
- package/dist/styles/BottomSheet.css +1 -1
- package/dist/styles/DataTable.css +1 -1
- package/dist/styles/Dropzone.css +1 -1
- package/dist/styles/ErrorText.css +1 -1
- package/dist/styles/Markdown.css +1 -1
- package/dist/styles/PasswordInput.css +1 -1
- package/dist/styles/SegmentedControl.css +1 -1
- package/dist/styles/Sidebar.css +2 -2
- package/dist/styles/StepperInput.css +1 -1
- package/dist/styles/Tag.css +1 -1
- package/dist/styles/Timeline.css +1 -1
- package/dist/styles/Toggle.css +1 -1
- package/dist/styles/ToggleGroup.css +1 -1
- package/dist/styles/advanced.css +2 -2
- package/dist/styles/core.css +4 -2
- package/dist/styles/data.css +6 -6
- package/dist/styles/feedback.css +3 -3
- package/dist/styles/forms.css +3 -3
- package/dist/styles/navigation.css +3 -3
- package/dist/styles/overlay.css +1 -1
- package/dist/styles/utility.css +1 -1
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +81 -7
- package/dist/tempest-react-sdk.js +347 -346
- package/dist/theme/create-theme.cjs +3 -3
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +15 -14
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/utils/dev-mode.cjs +1 -1
- package/dist/utils/dev-mode.cjs.map +1 -1
- package/dist/utils/dev-mode.js +7 -2
- package/dist/utils/dev-mode.js.map +1 -1
- package/dist/utils/json-frame.cjs +1 -1
- package/dist/utils/json-frame.cjs.map +1 -1
- package/dist/utils/json-frame.js +1 -1
- package/dist/utils/json-frame.js.map +1 -1
- package/dist/vision/core/providers.cjs +1 -1
- package/dist/vision/core/providers.cjs.map +1 -1
- package/dist/vision/core/providers.js +22 -3
- package/dist/vision/core/providers.js.map +1 -1
- package/dist/vision/core/session.cjs +1 -1
- package/dist/vision/core/session.cjs.map +1 -1
- package/dist/vision/core/session.js +22 -15
- package/dist/vision/core/session.js.map +1 -1
- package/dist/vision/fusion.cjs.map +1 -1
- package/dist/vision/fusion.js.map +1 -1
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +17 -16
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/normalization.cjs +2 -0
- package/dist/vision/normalization.cjs.map +1 -0
- package/dist/vision/normalization.js +49 -0
- package/dist/vision/normalization.js.map +1 -0
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +8 -2
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js +36 -32
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision.cjs +1 -1
- package/dist/vision.d.ts +218 -13
- package/dist/vision.js +19 -18
- package/dist/vite/tempest-pwa-manifest.cjs +1 -1
- package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
- package/dist/vite/tempest-pwa-manifest.js +1 -1
- package/dist/vite/tempest-pwa-manifest.js.map +1 -1
- package/dist/webrtc/peer-link.cjs +1 -1
- package/dist/webrtc/peer-link.cjs.map +1 -1
- package/dist/webrtc/peer-link.js +1 -1
- package/dist/webrtc/peer-link.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +1 -1
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +1 -1
- package/dist/ws/create-web-socket.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.cjs","names":[],"sources":["../../../src/components/VideoPlayer/VideoPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — `src` takes a URL or a Blob, and\n * every other prop is a facet of playing it: durationMs (a MediaRecorder WebM may\n * carry none), poster, captions, aspect ratio, the rate control and its presets,\n * pitch, fullscreen, autoPlay/loop/muted, onEnded/onError, plus the actions slot and\n * locale. The body is the transport state machine — object-URL lifecycle, seek, rate,\n * volume — around one video element ref.\n */\nimport { Maximize, Minimize, Pause, Play, Volume2, VolumeX } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { useFullscreen } from \"@/hooks/use-fullscreen\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport { DEFAULT_PLAYBACK_RATES } from \"./playback-rates\";\nimport styles from \"./VideoPlayer.module.css\";\n\n/**\n * DOM attributes this component redefines.\n *\n * `onRateChange` is a real collision, not tidiness: React types it on every\n * element as the media `ratechange` handler, `ReactEventHandler`. Ours carries\n * the rate the viewer picked, and a `(rate: number) => void` is not assignable\n * to a handler that receives an event — same shape of clash as `Input`'s `size`\n * against `HTMLInputElement.size`. The DOM one is useless on a `<div>` anyway:\n * the event fires on the media element inside, which this component owns.\n */\ntype OverriddenDomProps = \"children\" | \"onRateChange\";\n\n/** One caption or subtitle file, rendered as a `<track>`. */\nexport interface VideoTextTrack {\n /** URL of the WebVTT file. */\n src: string;\n /** BCP 47 language tag, e.g. `\"pt-BR\"`. */\n srcLang: string;\n /** Name shown in the browser's own track menu. */\n label: string;\n /** Track kind. Default `\"captions\"`. */\n kind?: \"subtitles\" | \"captions\" | \"descriptions\" | \"chapters\" | \"metadata\";\n /** Whether this track is on unless the viewer picks another. */\n default?: boolean;\n}\n\nexport interface VideoPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a session that records twenty clips does not leak\n * twenty URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useVideoRecorder` always\n * does. Without it the element's own `duration` is used, and a fresh\n * `MediaRecorder` blob may report `Infinity` until it is probed for one.\n */\n durationMs?: number;\n /** Still shown before playback. Pair it with `captureFrame` from `/imaging`. */\n poster?: string;\n /** Caption and subtitle files. */\n tracks?: readonly VideoTextTrack[];\n /** Frame shape while the video has no intrinsic size yet. Default `16 / 9`. */\n aspectRatio?: number;\n /**\n * Playback rate. Controlled when passed with `onRateChange`.\n *\n * Written to both `playbackRate` and `defaultPlaybackRate`, because the\n * element resets the first to the second when a source loads — a player that\n * only writes `playbackRate` silently drops back to 1× on every clip change.\n */\n rate?: number;\n /** Presets the control offers. `[]` hides it. Default {@link DEFAULT_PLAYBACK_RATES}. */\n rates?: readonly number[];\n /** Called with the rate the viewer picked. */\n onRateChange?: (rate: number) => void;\n /** Let the pitch rise with the rate (`preservesPitch = false`). Default `false`. */\n shiftPitch?: boolean;\n /** Offer a fullscreen button where the browser supports it. Default `true`. */\n fullscreen?: boolean;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Start muted. Autoplay without a gesture needs this. Default `false`. */\n muted?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the control row — a download button, a delete button. */\n actions?: ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n play: \"Tocar\",\n pause: \"Pausar\",\n seek: \"Posição\",\n mute: \"Silenciar\",\n unmute: \"Ativar som\",\n volume: \"Volume\",\n rate: \"Velocidade\",\n enterFullscreen: \"Tela cheia\",\n exitFullscreen: \"Sair da tela cheia\",\n },\n en: {\n play: \"Play\",\n pause: \"Pause\",\n seek: \"Seek\",\n mute: \"Mute\",\n unmute: \"Unmute\",\n volume: \"Volume\",\n rate: \"Speed\",\n enterFullscreen: \"Full screen\",\n exitFullscreen: \"Exit full screen\",\n },\n} as const;\n\n/**\n * Label a rate the way a viewer reads it.\n *\n * @param rate The multiplier.\n * @param locale Which decimal separator to use.\n * @returns e.g. `\"1,5×\"` in pt-BR, `\"1.5×\"` in English.\n */\nfunction rateLabel(rate: number, locale: \"pt-BR\" | \"en\"): string {\n return `${rate.toLocaleString(locale === \"en\" ? \"en-US\" : \"pt-BR\")}×`;\n}\n\n/**\n * Playback transport for one video: play/pause, seek, volume, speed, fullscreen.\n *\n * Built on a real `<video>` element, and deliberately **not** an overlay: the\n * controls sit in a bar under the frame rather than on top of it. Controls drawn\n * over video have to earn their contrast against arbitrary pixels, which no\n * `--tempest-*` token can promise — every text token in this SDK is resolved\n * against a known surface, and the two contrast defects this repo shipped were\n * both a text token used over a surface it was never checked against. A bar with\n * its own surface inherits the theme and is legible by construction.\n *\n * The seek bar, the volume slider and the speed picker are bare `<input>` and\n * `<select>` elements rather than the SDK's `Slider` and `Select`: those are form\n * fields, with a label row and a value badge, and a transport wants neither. The\n * native elements keep the keyboard and screen-reader behaviour that matters here\n * for free.\n *\n * @example\n * const rec = useVideoRecorder(screen.stream);\n * {rec.recording && (\n * <VideoPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function VideoPlayer({\n src,\n durationMs,\n poster,\n tracks,\n aspectRatio = 16 / 9,\n rate,\n rates = DEFAULT_PLAYBACK_RATES,\n onRateChange,\n shiftPitch = false,\n fullscreen = true,\n autoPlay = false,\n loop = false,\n muted = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: VideoPlayerProps) {\n const strings = STRINGS[locale];\n const shell = useRef<HTMLDivElement | null>(null);\n const video = useRef<HTMLVideoElement | null>(null);\n const probed = useRef(false);\n\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const [ownRate, setOwnRate] = useState(rate ?? 1);\n const [volume, setVolume] = useState(1);\n const [silent, setSilent] = useState(muted);\n\n const immersive = useFullscreen(shell);\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n const effectiveRate = rate ?? ownRate;\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n /**\n * Keep the rate on the element, and keep it across sources.\n *\n * `defaultPlaybackRate` is written alongside `playbackRate` because the load\n * algorithm copies the former onto the latter: writing only `playbackRate`\n * means the rate the viewer chose is silently lost on the next clip.\n */\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.defaultPlaybackRate = effectiveRate;\n node.playbackRate = effectiveRate;\n node.preservesPitch = !shiftPitch;\n }, [effectiveRate, shiftPitch, url]);\n\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.volume = volume;\n node.muted = silent;\n }, [volume, silent, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after\n * which it knows the real length. It runs at most once per source, and only\n * when the caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = video.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onProbed = (): void => {\n node.removeEventListener(\"timeupdate\", onProbed);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onProbed);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = video.current;\n if (!node || !url) return;\n if (node.paused) {\n void Promise.resolve(node.play())\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = video.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n const changeRate = (next: number): void => {\n if (rate === undefined) setOwnRate(next);\n onRateChange?.(next);\n };\n\n return (\n <div\n ref={shell}\n className={cn(styles.player, immersive.isFullscreen && styles.immersive, className)}\n {...rest}\n >\n <div className={styles.frame} style={{ aspectRatio }}>\n <video\n ref={video}\n className={styles.video}\n src={url ?? undefined}\n poster={poster}\n loop={loop}\n autoPlay={autoPlay}\n playsInline\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = video.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onVolumeChange={() => {\n const node = video.current;\n if (!node) return;\n setVolume(node.volume);\n setSilent(node.muted);\n }}\n onEnded={() => {\n setPlaying(false);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n >\n {tracks?.map((track) => (\n <track\n key={`${track.kind ?? \"captions\"}-${track.srcLang}-${track.src}`}\n kind={track.kind ?? \"captions\"}\n src={track.src}\n srcLang={track.srcLang}\n label={track.label}\n default={track.default}\n />\n ))}\n </video>\n </div>\n\n <div className={styles.chrome}>\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => setSilent((was) => !was)}\n disabled={disabled}\n aria-label={silent ? strings.unmute : strings.mute}\n title={silent ? strings.unmute : strings.mute}\n >\n {silent ? <VolumeX size={16} aria-hidden /> : <Volume2 size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.volume}\n min={0}\n max={1}\n step={0.05}\n value={silent ? 0 : volume}\n onChange={(event) => {\n const next = Number(event.target.value);\n setVolume(next);\n setSilent(next === 0);\n }}\n disabled={disabled}\n aria-label={strings.volume}\n />\n\n {rates.length > 0 && (\n <select\n className={styles.rate}\n value={effectiveRate}\n onChange={(event) => changeRate(Number(event.target.value))}\n disabled={disabled}\n aria-label={strings.rate}\n title={strings.rate}\n >\n {rates.map((option) => (\n <option key={option} value={option}>\n {rateLabel(option, locale)}\n </option>\n ))}\n </select>\n )}\n\n {fullscreen && immersive.supported && (\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => {\n void immersive.toggle().catch((error: unknown) => onError?.(error));\n }}\n disabled={disabled}\n aria-label={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n title={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n >\n {immersive.isFullscreen ? (\n <Minimize size={16} aria-hidden />\n ) : (\n <Maximize size={16} aria-hidden />\n )}\n </button>\n )}\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VideoPlayer.cjs","names":[],"sources":["../../../src/components/VideoPlayer/VideoPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — `src` takes a URL or a Blob, and\n * every other prop is a facet of playing it: durationMs (a MediaRecorder WebM may\n * carry none), poster, captions, aspect ratio, the rate control and its presets,\n * pitch, fullscreen, autoPlay/loop/muted, onEnded/onError, plus the actions slot and\n * locale. The body is the transport state machine — object-URL lifecycle, seek, rate,\n * volume — around one video element ref.\n */\nimport { Maximize, Minimize, Pause, Play, Volume2, VolumeX } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { useFullscreen } from \"@/hooks/use-fullscreen\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport { DEFAULT_PLAYBACK_RATES } from \"./playback-rates\";\nimport styles from \"./VideoPlayer.module.css\";\n\n/**\n * DOM attributes this component redefines.\n *\n * `onRateChange` is a real collision, not tidiness: React types it on every\n * element as the media `ratechange` handler, `ReactEventHandler`. Ours carries\n * the rate the viewer picked, and a `(rate: number) => void` is not assignable\n * to a handler that receives an event — same shape of clash as `Input`'s `size`\n * against `HTMLInputElement.size`. The DOM one is useless on a `<div>` anyway:\n * the event fires on the media element inside, which this component owns.\n */\ntype OverriddenDomProps = \"children\" | \"onRateChange\";\n\n/** One caption or subtitle file, rendered as a `<track>`. */\nexport interface VideoTextTrack {\n /** URL of the WebVTT file. */\n src: string;\n /** BCP 47 language tag, e.g. `\"pt-BR\"`. */\n srcLang: string;\n /** Name shown in the browser's own track menu. */\n label: string;\n /** Track kind. Default `\"captions\"`. */\n kind?: \"subtitles\" | \"captions\" | \"descriptions\" | \"chapters\" | \"metadata\";\n /** Whether this track is on unless the viewer picks another. */\n default?: boolean;\n}\n\nexport interface VideoPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a session that records twenty clips does not leak\n * twenty URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useVideoRecorder` always\n * does. Without it the element's own `duration` is used, and a fresh\n * `MediaRecorder` blob may report `Infinity` until it is probed for one.\n */\n durationMs?: number;\n /** Still shown before playback. Pair it with `captureFrame` from `/imaging`. */\n poster?: string;\n /** Caption and subtitle files. */\n tracks?: readonly VideoTextTrack[];\n /** Frame shape while the video has no intrinsic size yet. Default `16 / 9`. */\n aspectRatio?: number;\n /**\n * Playback rate. Controlled when passed with `onRateChange`.\n *\n * Written to both `playbackRate` and `defaultPlaybackRate`, because the\n * element resets the first to the second when a source loads — a player that\n * only writes `playbackRate` silently drops back to 1× on every clip change.\n */\n rate?: number;\n /** Presets the control offers. `[]` hides it. Default {@link DEFAULT_PLAYBACK_RATES}. */\n rates?: readonly number[];\n /** Called with the rate the viewer picked. */\n onRateChange?: (rate: number) => void;\n /** Let the pitch rise with the rate (`preservesPitch = false`). Default `false`. */\n shiftPitch?: boolean;\n /** Offer a fullscreen button where the browser supports it. Default `true`. */\n fullscreen?: boolean;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Start muted. Autoplay without a gesture needs this. Default `false`. */\n muted?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the control row — a download button, a delete button. */\n actions?: ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n play: \"Tocar\",\n pause: \"Pausar\",\n seek: \"Posição\",\n mute: \"Silenciar\",\n unmute: \"Ativar som\",\n volume: \"Volume\",\n rate: \"Velocidade\",\n enterFullscreen: \"Tela cheia\",\n exitFullscreen: \"Sair da tela cheia\",\n },\n en: {\n play: \"Play\",\n pause: \"Pause\",\n seek: \"Seek\",\n mute: \"Mute\",\n unmute: \"Unmute\",\n volume: \"Volume\",\n rate: \"Speed\",\n enterFullscreen: \"Full screen\",\n exitFullscreen: \"Exit full screen\",\n },\n} as const;\n\n/**\n * Label a rate the way a viewer reads it.\n *\n * @param rate The multiplier.\n * @param locale Which decimal separator to use.\n * @returns e.g. `\"1,5×\"` in pt-BR, `\"1.5×\"` in English.\n */\nfunction rateLabel(rate: number, locale: \"pt-BR\" | \"en\"): string {\n return `${rate.toLocaleString(locale === \"en\" ? \"en-US\" : \"pt-BR\")}×`;\n}\n\n/**\n * Playback transport for one video: play/pause, seek, volume, speed, fullscreen.\n *\n * Built on a real `<video>` element, and deliberately **not** an overlay: the\n * controls sit in a bar under the frame rather than on top of it. Controls drawn\n * over video have to earn their contrast against arbitrary pixels, which no\n * `--tempest-*` token can promise — every text token in this SDK is resolved\n * against a known surface, and the two contrast defects this repo shipped were\n * both a text token used over a surface it was never checked against. A bar with\n * its own surface inherits the theme and is legible by construction.\n *\n * The seek bar, the volume slider and the speed picker are bare `<input>` and\n * `<select>` elements rather than the SDK's `Slider` and `Select`: those are form\n * fields, with a label row and a value badge, and a transport wants neither. The\n * native elements keep the keyboard and screen-reader behaviour that matters here\n * for free.\n *\n * @example\n * const rec = useVideoRecorder(screen.stream);\n * {rec.recording && (\n * <VideoPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function VideoPlayer({\n src,\n durationMs,\n poster,\n tracks,\n aspectRatio = 16 / 9,\n rate,\n rates = DEFAULT_PLAYBACK_RATES,\n onRateChange,\n shiftPitch = false,\n fullscreen = true,\n autoPlay = false,\n loop = false,\n muted = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: VideoPlayerProps) {\n const strings = STRINGS[locale];\n const shell = useRef<HTMLDivElement | null>(null);\n const video = useRef<HTMLVideoElement | null>(null);\n const probed = useRef(false);\n\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const [ownRate, setOwnRate] = useState(rate ?? 1);\n const [volume, setVolume] = useState(1);\n const [silent, setSilent] = useState(muted);\n\n const immersive = useFullscreen(shell);\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n const effectiveRate = rate ?? ownRate;\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n /**\n * Keep the rate on the element, and keep it across sources.\n *\n * `defaultPlaybackRate` is written alongside `playbackRate` because the load\n * algorithm copies the former onto the latter: writing only `playbackRate`\n * means the rate the viewer chose is silently lost on the next clip.\n */\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.defaultPlaybackRate = effectiveRate;\n node.playbackRate = effectiveRate;\n node.preservesPitch = !shiftPitch;\n }, [effectiveRate, shiftPitch, url]);\n\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.volume = volume;\n node.muted = silent;\n }, [volume, silent, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after\n * which it knows the real length. It runs at most once per source, and only\n * when the caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = video.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onProbed = (): void => {\n node.removeEventListener(\"timeupdate\", onProbed);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onProbed);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = video.current;\n if (!node || !url) return;\n if (node.paused) {\n void Promise.resolve(node.play())\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = video.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n const changeRate = (next: number): void => {\n if (rate === undefined) setOwnRate(next);\n onRateChange?.(next);\n };\n\n return (\n <div\n ref={shell}\n className={cn(styles.player, immersive.isFullscreen && styles.immersive, className)}\n {...rest}\n >\n <div className={styles.frame} style={{ aspectRatio }}>\n <video\n ref={video}\n className={styles.video}\n src={url ?? undefined}\n poster={poster}\n loop={loop}\n autoPlay={autoPlay}\n playsInline\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = video.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onVolumeChange={() => {\n const node = video.current;\n if (!node) return;\n setVolume(node.volume);\n setSilent(node.muted);\n }}\n onEnded={() => {\n setPlaying(false);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n >\n {tracks?.map((track) => (\n <track\n key={`${track.kind ?? \"captions\"}-${track.srcLang}-${track.src}`}\n kind={track.kind ?? \"captions\"}\n src={track.src}\n srcLang={track.srcLang}\n label={track.label}\n default={track.default}\n />\n ))}\n </video>\n </div>\n\n <div className={styles.chrome}>\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => setSilent((was) => !was)}\n disabled={disabled}\n aria-label={silent ? strings.unmute : strings.mute}\n title={silent ? strings.unmute : strings.mute}\n >\n {silent ? <VolumeX size={16} aria-hidden /> : <Volume2 size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.volume}\n min={0}\n max={1}\n step={0.05}\n value={silent ? 0 : volume}\n onChange={(event) => {\n const next = Number(event.target.value);\n setVolume(next);\n setSilent(next === 0);\n }}\n disabled={disabled}\n aria-label={strings.volume}\n />\n\n {rates.length > 0 && (\n <select\n className={styles.rate}\n value={effectiveRate}\n onChange={(event) => changeRate(Number(event.target.value))}\n disabled={disabled}\n aria-label={strings.rate}\n title={strings.rate}\n >\n {rates.map((option) => (\n <option key={option} value={option}>\n {rateLabel(option, locale)}\n </option>\n ))}\n </select>\n )}\n\n {fullscreen && immersive.supported && (\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => {\n void immersive.toggle().catch((error: unknown) => onError?.(error));\n }}\n disabled={disabled}\n aria-label={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n title={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n >\n {immersive.isFullscreen ? (\n <Minimize size={16} aria-hidden />\n ) : (\n <Maximize size={16} aria-hidden />\n )}\n </button>\n )}\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n </div>\n );\n}\n"],"mappings":"4TAsGA,IAAM,EAAU,CACZ,QAAS,CACL,KAAM,QACN,MAAO,SACP,KAAM,UACN,KAAM,YACN,OAAQ,aACR,OAAQ,SACR,KAAM,aACN,gBAAiB,aACjB,eAAgB,oBACpB,EACA,GAAI,CACA,KAAM,OACN,MAAO,QACP,KAAM,OACN,KAAM,OACN,OAAQ,SACR,OAAQ,SACR,KAAM,QACN,gBAAiB,cACjB,eAAgB,kBACpB,CACJ,EASA,SAAS,EAAU,EAAc,EAAgC,CAC7D,MAAO,GAAG,EAAK,eAAe,IAAW,KAAO,QAAU,OAAO,EAAE,EACvE,CAyBA,SAAgB,EAAY,CACxB,MACA,aACA,SACA,SACA,cAAc,GAAK,EACnB,OACA,QAAQ,EAAA,uBACR,eACA,aAAa,GACb,aAAa,GACb,WAAW,GACX,OAAO,GACP,QAAQ,GACR,SAAS,QACT,UACA,UACA,UACA,WAAW,GACX,YACA,GAAG,GACc,CACjB,IAAM,EAAU,EAAQ,GAClB,GAAA,EAAQ,EAAA,OAAA,CAA8B,IAAI,EAC1C,GAAA,EAAQ,EAAA,OAAA,CAAgC,IAAI,EAC5C,GAAA,EAAS,EAAA,OAAA,CAAO,EAAK,EAErB,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAS,EAAK,EACtC,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,CAAS,CAAC,EACtC,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,CAAS,GAAwB,EAC7D,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAS,GAAQ,CAAC,EAC1C,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAAS,CAAC,EAChC,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAAS,CAAK,EAEpC,EAAY,EAAA,cAAc,CAAK,EAC/B,EAAU,EAAA,aAAa,OAAO,GAAQ,SAAW,KAAO,CAAG,EAC3D,EAAM,OAAO,GAAQ,SAAW,EAAM,EAEtC,EAAgB,GAAQ,EACxB,EAAU,OAAO,SAAS,CAAU,EAAK,EAAwB,EACjE,EAAW,OAAO,SAAS,CAAO,GAAK,EAAU,GAEvD,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAa,CAAC,EACd,EAAW,EAAK,EAChB,EAAa,GAAwB,EACrC,EAAO,QAAU,EACrB,EAAG,CAAC,CAAG,CAAC,GASR,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAO,EAAM,QACd,IACL,EAAK,oBAAsB,EAC3B,EAAK,aAAe,EACpB,EAAK,eAAiB,CAAC,EAC3B,EAAG,CAAC,EAAe,EAAY,CAAG,CAAC,GAEnC,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAO,EAAM,QACd,IACL,EAAK,OAAS,EACd,EAAK,MAAQ,EACjB,EAAG,CAAC,EAAQ,EAAQ,CAAG,CAAC,EASxB,IAAM,MAA6B,CAC/B,IAAM,EAAO,EAAM,QACnB,GAAI,CAAC,EAAM,OACX,GAAI,OAAO,SAAS,EAAK,QAAQ,EAAG,CAChC,EAAa,EAAK,SAAW,GAAI,EACjC,MACJ,CACA,GAAI,EAAO,SAAW,OAAO,SAAS,CAAU,EAAG,OACnD,EAAO,QAAU,GACjB,IAAM,MAAuB,CACzB,EAAK,oBAAoB,aAAc,CAAQ,EAC3C,OAAO,SAAS,EAAK,QAAQ,GAAG,EAAa,EAAK,SAAW,GAAI,EACrE,EAAK,YAAc,CACvB,EACA,EAAK,iBAAiB,aAAc,CAAQ,EAC5C,EAAK,YAAc,KACvB,EAEM,OAAqB,CACvB,IAAM,EAAO,EAAM,QACf,GAAC,GAAS,EACd,IAAI,EAAK,OAAQ,CACb,QAAa,QAAQ,EAAK,KAAK,CAAC,CAAC,CAC5B,SAAW,EAAW,EAAI,CAAC,CAAC,CAC5B,MAAO,GAAmB,IAAU,CAAK,CAAC,EAC/C,MACJ,CACA,EAAK,MAAM,EACX,EAAW,EAAK,CAFhB,CAGJ,EAEM,GAAQ,GAAqB,CAC/B,IAAM,EAAO,EAAM,QACd,GAAS,IACd,EAAK,YAAc,EAAK,IACxB,EAAa,CAAE,EACnB,EAEM,GAAc,GAAuB,CACnC,IAAS,IAAA,IAAW,EAAW,CAAI,EACvC,IAAe,CAAI,CACvB,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,IAAK,EACL,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,EAAU,cAAgB,EAAA,QAAO,UAAW,CAAS,EAClF,GAAI,EAHR,SAAA,EAKI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,MAAO,MAAO,CAAE,aAAY,EAC/C,UAAA,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,IAAK,EACL,UAAW,EAAA,QAAO,MAClB,IAAK,GAAO,IAAA,GACJ,SACF,OACI,WACV,YAAA,GACA,QAAQ,WACR,iBAAkB,EAClB,iBAAkB,EAClB,iBAAoB,CAChB,IAAM,EAAO,EAAM,QACf,GAAM,EAAa,EAAK,YAAc,GAAI,CAClD,EACA,WAAc,EAAW,EAAI,EAC7B,YAAe,EAAW,EAAK,EAC/B,mBAAsB,CAClB,IAAM,EAAO,EAAM,QACd,IACL,EAAU,EAAK,MAAM,EACrB,EAAU,EAAK,KAAK,EACxB,EACA,YAAe,CACX,EAAW,EAAK,EAChB,IAAU,CACd,EACA,QAAU,GAAU,IAAU,CAAK,EAElC,SAAA,GAAQ,IAAK,IACV,EAAA,EAAA,IAAA,CAAC,QAAD,CAEI,KAAM,EAAM,MAAQ,WACpB,IAAK,EAAM,IACX,QAAS,EAAM,QACf,MAAO,EAAM,MACb,QAAS,EAAM,OAClB,EANQ,GAAG,EAAM,MAAQ,WAAW,GAAG,EAAM,QAAQ,GAAG,EAAM,KAM9D,CACJ,CACE,CAAA,CACN,CAAA,GAEL,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,UAClB,QAAS,GACT,SAAU,GAAY,CAAC,EACvB,aAAY,EAAU,EAAQ,MAAQ,EAAQ,KAC9C,MAAO,EAAU,EAAQ,MAAQ,EAAQ,KAExC,SAAA,GAAU,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAAO,KAAM,GAAI,cAAA,EAAa,CAAA,GAAI,EAAA,EAAA,IAAA,CAAC,EAAA,KAAD,CAAM,KAAM,GAAI,cAAA,EAAa,CAAA,CACtE,CAAA,GAER,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,KAAK,QACL,UAAW,EAAA,QAAO,KAClB,IAAK,EACL,IAAK,EAAW,KAAK,MAAM,CAAO,EAAI,EACtC,KAAM,IACN,MAAO,KAAK,IAAI,KAAK,MAAM,CAAS,EAAG,EAAW,KAAK,MAAM,CAAO,EAAI,CAAC,EACzE,SAAW,GAAU,GAAK,OAAO,EAAM,OAAO,KAAK,CAAC,EACpD,SAAU,GAAY,CAAC,EACvB,aAAY,EAAQ,KACpB,iBAAgB,GAAG,EAAA,eAAe,CAAS,EAAE,KAAK,EAAA,eAAe,CAAO,GAC3E,CAAA,GAED,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAxB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAA,eAAe,CAAS,CAAQ,CAAA,GACvC,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,cAAY,OAAO,SAAA,GAAO,CAAA,GAChC,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAA,eAAe,CAAO,CAAQ,CAAA,CACnC,KAEN,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,KAClB,YAAe,EAAW,GAAQ,CAAC,CAAG,EAC5B,WACV,aAAY,EAAS,EAAQ,OAAS,EAAQ,KAC9C,MAAO,EAAS,EAAQ,OAAS,EAAQ,KAExC,SAAA,GAAS,EAAA,EAAA,IAAA,CAAC,EAAA,QAAD,CAAS,KAAM,GAAI,cAAA,EAAa,CAAA,GAAI,EAAA,EAAA,IAAA,CAAC,EAAA,QAAD,CAAS,KAAM,GAAI,cAAA,EAAa,CAAA,CAC1E,CAAA,GAER,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,KAAK,QACL,UAAW,EAAA,QAAO,OAClB,IAAK,EACL,IAAK,EACL,KAAM,IACN,MAAO,EAAS,EAAI,EACpB,SAAW,GAAU,CACjB,IAAM,EAAO,OAAO,EAAM,OAAO,KAAK,EACtC,EAAU,CAAI,EACd,EAAU,IAAS,CAAC,CACxB,EACU,WACV,aAAY,EAAQ,MACvB,CAAA,EAEA,EAAM,OAAS,IACZ,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,UAAW,EAAA,QAAO,KAClB,MAAO,EACP,SAAW,GAAU,GAAW,OAAO,EAAM,OAAO,KAAK,CAAC,EAChD,WACV,aAAY,EAAQ,KACpB,MAAO,EAAQ,KAEd,SAAA,EAAM,IAAK,IACR,EAAA,EAAA,IAAA,CAAC,SAAD,CAAqB,MAAO,EACvB,SAAA,EAAU,EAAQ,CAAM,CACrB,EAFK,CAEL,CACX,CACG,CAAA,EAGX,GAAc,EAAU,YACrB,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,KAClB,YAAe,CACX,EAAe,OAAO,CAAC,CAAC,MAAO,GAAmB,IAAU,CAAK,CAAC,CACtE,EACU,WACV,aACI,EAAU,aACJ,EAAQ,eACR,EAAQ,gBAElB,MACI,EAAU,aACJ,EAAQ,eACR,EAAQ,gBAGjB,SAAA,EAAU,cACP,EAAA,EAAA,IAAA,CAAC,EAAA,SAAD,CAAU,KAAM,GAAI,cAAA,EAAa,CAAA,GAEjC,EAAA,EAAA,IAAA,CAAC,EAAA,SAAD,CAAU,KAAM,GAAI,cAAA,EAAa,CAAA,CAEjC,CAAA,EAGX,IAAW,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,QAAU,SAAA,CAAc,CAAA,CAC3D,CACJ,CAAA,CAAA,GAEb"}
|
|
@@ -69,7 +69,7 @@ function m({ src: m, durationMs: h, poster: g, tracks: _, aspectRatio: v = 16 /
|
|
|
69
69
|
e.addEventListener("timeupdate", t), e.currentTime = 1e101;
|
|
70
70
|
}, ce = () => {
|
|
71
71
|
let e = P.current;
|
|
72
|
-
if (
|
|
72
|
+
if (e && Y) {
|
|
73
73
|
if (e.paused) {
|
|
74
74
|
Promise.resolve(e.play()).then(() => L(!0)).catch((e) => A?.(e));
|
|
75
75
|
return;
|
|
@@ -78,7 +78,7 @@ function m({ src: m, durationMs: h, poster: g, tracks: _, aspectRatio: v = 16 /
|
|
|
78
78
|
}
|
|
79
79
|
}, le = (e) => {
|
|
80
80
|
let t = P.current;
|
|
81
|
-
|
|
81
|
+
t && Q && (t.currentTime = e / 1e3, z(e));
|
|
82
82
|
}, ue = (e) => {
|
|
83
83
|
y === void 0 && U(e), x?.(e);
|
|
84
84
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.js","names":[],"sources":["../../../src/components/VideoPlayer/VideoPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — `src` takes a URL or a Blob, and\n * every other prop is a facet of playing it: durationMs (a MediaRecorder WebM may\n * carry none), poster, captions, aspect ratio, the rate control and its presets,\n * pitch, fullscreen, autoPlay/loop/muted, onEnded/onError, plus the actions slot and\n * locale. The body is the transport state machine — object-URL lifecycle, seek, rate,\n * volume — around one video element ref.\n */\nimport { Maximize, Minimize, Pause, Play, Volume2, VolumeX } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { useFullscreen } from \"@/hooks/use-fullscreen\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport { DEFAULT_PLAYBACK_RATES } from \"./playback-rates\";\nimport styles from \"./VideoPlayer.module.css\";\n\n/**\n * DOM attributes this component redefines.\n *\n * `onRateChange` is a real collision, not tidiness: React types it on every\n * element as the media `ratechange` handler, `ReactEventHandler`. Ours carries\n * the rate the viewer picked, and a `(rate: number) => void` is not assignable\n * to a handler that receives an event — same shape of clash as `Input`'s `size`\n * against `HTMLInputElement.size`. The DOM one is useless on a `<div>` anyway:\n * the event fires on the media element inside, which this component owns.\n */\ntype OverriddenDomProps = \"children\" | \"onRateChange\";\n\n/** One caption or subtitle file, rendered as a `<track>`. */\nexport interface VideoTextTrack {\n /** URL of the WebVTT file. */\n src: string;\n /** BCP 47 language tag, e.g. `\"pt-BR\"`. */\n srcLang: string;\n /** Name shown in the browser's own track menu. */\n label: string;\n /** Track kind. Default `\"captions\"`. */\n kind?: \"subtitles\" | \"captions\" | \"descriptions\" | \"chapters\" | \"metadata\";\n /** Whether this track is on unless the viewer picks another. */\n default?: boolean;\n}\n\nexport interface VideoPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a session that records twenty clips does not leak\n * twenty URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useVideoRecorder` always\n * does. Without it the element's own `duration` is used, and a fresh\n * `MediaRecorder` blob may report `Infinity` until it is probed for one.\n */\n durationMs?: number;\n /** Still shown before playback. Pair it with `captureFrame` from `/imaging`. */\n poster?: string;\n /** Caption and subtitle files. */\n tracks?: readonly VideoTextTrack[];\n /** Frame shape while the video has no intrinsic size yet. Default `16 / 9`. */\n aspectRatio?: number;\n /**\n * Playback rate. Controlled when passed with `onRateChange`.\n *\n * Written to both `playbackRate` and `defaultPlaybackRate`, because the\n * element resets the first to the second when a source loads — a player that\n * only writes `playbackRate` silently drops back to 1× on every clip change.\n */\n rate?: number;\n /** Presets the control offers. `[]` hides it. Default {@link DEFAULT_PLAYBACK_RATES}. */\n rates?: readonly number[];\n /** Called with the rate the viewer picked. */\n onRateChange?: (rate: number) => void;\n /** Let the pitch rise with the rate (`preservesPitch = false`). Default `false`. */\n shiftPitch?: boolean;\n /** Offer a fullscreen button where the browser supports it. Default `true`. */\n fullscreen?: boolean;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Start muted. Autoplay without a gesture needs this. Default `false`. */\n muted?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the control row — a download button, a delete button. */\n actions?: ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n play: \"Tocar\",\n pause: \"Pausar\",\n seek: \"Posição\",\n mute: \"Silenciar\",\n unmute: \"Ativar som\",\n volume: \"Volume\",\n rate: \"Velocidade\",\n enterFullscreen: \"Tela cheia\",\n exitFullscreen: \"Sair da tela cheia\",\n },\n en: {\n play: \"Play\",\n pause: \"Pause\",\n seek: \"Seek\",\n mute: \"Mute\",\n unmute: \"Unmute\",\n volume: \"Volume\",\n rate: \"Speed\",\n enterFullscreen: \"Full screen\",\n exitFullscreen: \"Exit full screen\",\n },\n} as const;\n\n/**\n * Label a rate the way a viewer reads it.\n *\n * @param rate The multiplier.\n * @param locale Which decimal separator to use.\n * @returns e.g. `\"1,5×\"` in pt-BR, `\"1.5×\"` in English.\n */\nfunction rateLabel(rate: number, locale: \"pt-BR\" | \"en\"): string {\n return `${rate.toLocaleString(locale === \"en\" ? \"en-US\" : \"pt-BR\")}×`;\n}\n\n/**\n * Playback transport for one video: play/pause, seek, volume, speed, fullscreen.\n *\n * Built on a real `<video>` element, and deliberately **not** an overlay: the\n * controls sit in a bar under the frame rather than on top of it. Controls drawn\n * over video have to earn their contrast against arbitrary pixels, which no\n * `--tempest-*` token can promise — every text token in this SDK is resolved\n * against a known surface, and the two contrast defects this repo shipped were\n * both a text token used over a surface it was never checked against. A bar with\n * its own surface inherits the theme and is legible by construction.\n *\n * The seek bar, the volume slider and the speed picker are bare `<input>` and\n * `<select>` elements rather than the SDK's `Slider` and `Select`: those are form\n * fields, with a label row and a value badge, and a transport wants neither. The\n * native elements keep the keyboard and screen-reader behaviour that matters here\n * for free.\n *\n * @example\n * const rec = useVideoRecorder(screen.stream);\n * {rec.recording && (\n * <VideoPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function VideoPlayer({\n src,\n durationMs,\n poster,\n tracks,\n aspectRatio = 16 / 9,\n rate,\n rates = DEFAULT_PLAYBACK_RATES,\n onRateChange,\n shiftPitch = false,\n fullscreen = true,\n autoPlay = false,\n loop = false,\n muted = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: VideoPlayerProps) {\n const strings = STRINGS[locale];\n const shell = useRef<HTMLDivElement | null>(null);\n const video = useRef<HTMLVideoElement | null>(null);\n const probed = useRef(false);\n\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const [ownRate, setOwnRate] = useState(rate ?? 1);\n const [volume, setVolume] = useState(1);\n const [silent, setSilent] = useState(muted);\n\n const immersive = useFullscreen(shell);\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n const effectiveRate = rate ?? ownRate;\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n /**\n * Keep the rate on the element, and keep it across sources.\n *\n * `defaultPlaybackRate` is written alongside `playbackRate` because the load\n * algorithm copies the former onto the latter: writing only `playbackRate`\n * means the rate the viewer chose is silently lost on the next clip.\n */\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.defaultPlaybackRate = effectiveRate;\n node.playbackRate = effectiveRate;\n node.preservesPitch = !shiftPitch;\n }, [effectiveRate, shiftPitch, url]);\n\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.volume = volume;\n node.muted = silent;\n }, [volume, silent, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after\n * which it knows the real length. It runs at most once per source, and only\n * when the caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = video.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onProbed = (): void => {\n node.removeEventListener(\"timeupdate\", onProbed);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onProbed);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = video.current;\n if (!node || !url) return;\n if (node.paused) {\n void Promise.resolve(node.play())\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = video.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n const changeRate = (next: number): void => {\n if (rate === undefined) setOwnRate(next);\n onRateChange?.(next);\n };\n\n return (\n <div\n ref={shell}\n className={cn(styles.player, immersive.isFullscreen && styles.immersive, className)}\n {...rest}\n >\n <div className={styles.frame} style={{ aspectRatio }}>\n <video\n ref={video}\n className={styles.video}\n src={url ?? undefined}\n poster={poster}\n loop={loop}\n autoPlay={autoPlay}\n playsInline\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = video.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onVolumeChange={() => {\n const node = video.current;\n if (!node) return;\n setVolume(node.volume);\n setSilent(node.muted);\n }}\n onEnded={() => {\n setPlaying(false);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n >\n {tracks?.map((track) => (\n <track\n key={`${track.kind ?? \"captions\"}-${track.srcLang}-${track.src}`}\n kind={track.kind ?? \"captions\"}\n src={track.src}\n srcLang={track.srcLang}\n label={track.label}\n default={track.default}\n />\n ))}\n </video>\n </div>\n\n <div className={styles.chrome}>\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => setSilent((was) => !was)}\n disabled={disabled}\n aria-label={silent ? strings.unmute : strings.mute}\n title={silent ? strings.unmute : strings.mute}\n >\n {silent ? <VolumeX size={16} aria-hidden /> : <Volume2 size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.volume}\n min={0}\n max={1}\n step={0.05}\n value={silent ? 0 : volume}\n onChange={(event) => {\n const next = Number(event.target.value);\n setVolume(next);\n setSilent(next === 0);\n }}\n disabled={disabled}\n aria-label={strings.volume}\n />\n\n {rates.length > 0 && (\n <select\n className={styles.rate}\n value={effectiveRate}\n onChange={(event) => changeRate(Number(event.target.value))}\n disabled={disabled}\n aria-label={strings.rate}\n title={strings.rate}\n >\n {rates.map((option) => (\n <option key={option} value={option}>\n {rateLabel(option, locale)}\n </option>\n ))}\n </select>\n )}\n\n {fullscreen && immersive.supported && (\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => {\n void immersive.toggle().catch((error: unknown) => onError?.(error));\n }}\n disabled={disabled}\n aria-label={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n title={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n >\n {immersive.isFullscreen ? (\n <Minimize size={16} aria-hidden />\n ) : (\n <Maximize size={16} aria-hidden />\n )}\n </button>\n )}\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VideoPlayer.js","names":[],"sources":["../../../src/components/VideoPlayer/VideoPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — `src` takes a URL or a Blob, and\n * every other prop is a facet of playing it: durationMs (a MediaRecorder WebM may\n * carry none), poster, captions, aspect ratio, the rate control and its presets,\n * pitch, fullscreen, autoPlay/loop/muted, onEnded/onError, plus the actions slot and\n * locale. The body is the transport state machine — object-URL lifecycle, seek, rate,\n * volume — around one video element ref.\n */\nimport { Maximize, Minimize, Pause, Play, Volume2, VolumeX } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { useFullscreen } from \"@/hooks/use-fullscreen\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport { DEFAULT_PLAYBACK_RATES } from \"./playback-rates\";\nimport styles from \"./VideoPlayer.module.css\";\n\n/**\n * DOM attributes this component redefines.\n *\n * `onRateChange` is a real collision, not tidiness: React types it on every\n * element as the media `ratechange` handler, `ReactEventHandler`. Ours carries\n * the rate the viewer picked, and a `(rate: number) => void` is not assignable\n * to a handler that receives an event — same shape of clash as `Input`'s `size`\n * against `HTMLInputElement.size`. The DOM one is useless on a `<div>` anyway:\n * the event fires on the media element inside, which this component owns.\n */\ntype OverriddenDomProps = \"children\" | \"onRateChange\";\n\n/** One caption or subtitle file, rendered as a `<track>`. */\nexport interface VideoTextTrack {\n /** URL of the WebVTT file. */\n src: string;\n /** BCP 47 language tag, e.g. `\"pt-BR\"`. */\n srcLang: string;\n /** Name shown in the browser's own track menu. */\n label: string;\n /** Track kind. Default `\"captions\"`. */\n kind?: \"subtitles\" | \"captions\" | \"descriptions\" | \"chapters\" | \"metadata\";\n /** Whether this track is on unless the viewer picks another. */\n default?: boolean;\n}\n\nexport interface VideoPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a session that records twenty clips does not leak\n * twenty URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useVideoRecorder` always\n * does. Without it the element's own `duration` is used, and a fresh\n * `MediaRecorder` blob may report `Infinity` until it is probed for one.\n */\n durationMs?: number;\n /** Still shown before playback. Pair it with `captureFrame` from `/imaging`. */\n poster?: string;\n /** Caption and subtitle files. */\n tracks?: readonly VideoTextTrack[];\n /** Frame shape while the video has no intrinsic size yet. Default `16 / 9`. */\n aspectRatio?: number;\n /**\n * Playback rate. Controlled when passed with `onRateChange`.\n *\n * Written to both `playbackRate` and `defaultPlaybackRate`, because the\n * element resets the first to the second when a source loads — a player that\n * only writes `playbackRate` silently drops back to 1× on every clip change.\n */\n rate?: number;\n /** Presets the control offers. `[]` hides it. Default {@link DEFAULT_PLAYBACK_RATES}. */\n rates?: readonly number[];\n /** Called with the rate the viewer picked. */\n onRateChange?: (rate: number) => void;\n /** Let the pitch rise with the rate (`preservesPitch = false`). Default `false`. */\n shiftPitch?: boolean;\n /** Offer a fullscreen button where the browser supports it. Default `true`. */\n fullscreen?: boolean;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Start muted. Autoplay without a gesture needs this. Default `false`. */\n muted?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the control row — a download button, a delete button. */\n actions?: ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n play: \"Tocar\",\n pause: \"Pausar\",\n seek: \"Posição\",\n mute: \"Silenciar\",\n unmute: \"Ativar som\",\n volume: \"Volume\",\n rate: \"Velocidade\",\n enterFullscreen: \"Tela cheia\",\n exitFullscreen: \"Sair da tela cheia\",\n },\n en: {\n play: \"Play\",\n pause: \"Pause\",\n seek: \"Seek\",\n mute: \"Mute\",\n unmute: \"Unmute\",\n volume: \"Volume\",\n rate: \"Speed\",\n enterFullscreen: \"Full screen\",\n exitFullscreen: \"Exit full screen\",\n },\n} as const;\n\n/**\n * Label a rate the way a viewer reads it.\n *\n * @param rate The multiplier.\n * @param locale Which decimal separator to use.\n * @returns e.g. `\"1,5×\"` in pt-BR, `\"1.5×\"` in English.\n */\nfunction rateLabel(rate: number, locale: \"pt-BR\" | \"en\"): string {\n return `${rate.toLocaleString(locale === \"en\" ? \"en-US\" : \"pt-BR\")}×`;\n}\n\n/**\n * Playback transport for one video: play/pause, seek, volume, speed, fullscreen.\n *\n * Built on a real `<video>` element, and deliberately **not** an overlay: the\n * controls sit in a bar under the frame rather than on top of it. Controls drawn\n * over video have to earn their contrast against arbitrary pixels, which no\n * `--tempest-*` token can promise — every text token in this SDK is resolved\n * against a known surface, and the two contrast defects this repo shipped were\n * both a text token used over a surface it was never checked against. A bar with\n * its own surface inherits the theme and is legible by construction.\n *\n * The seek bar, the volume slider and the speed picker are bare `<input>` and\n * `<select>` elements rather than the SDK's `Slider` and `Select`: those are form\n * fields, with a label row and a value badge, and a transport wants neither. The\n * native elements keep the keyboard and screen-reader behaviour that matters here\n * for free.\n *\n * @example\n * const rec = useVideoRecorder(screen.stream);\n * {rec.recording && (\n * <VideoPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function VideoPlayer({\n src,\n durationMs,\n poster,\n tracks,\n aspectRatio = 16 / 9,\n rate,\n rates = DEFAULT_PLAYBACK_RATES,\n onRateChange,\n shiftPitch = false,\n fullscreen = true,\n autoPlay = false,\n loop = false,\n muted = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: VideoPlayerProps) {\n const strings = STRINGS[locale];\n const shell = useRef<HTMLDivElement | null>(null);\n const video = useRef<HTMLVideoElement | null>(null);\n const probed = useRef(false);\n\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const [ownRate, setOwnRate] = useState(rate ?? 1);\n const [volume, setVolume] = useState(1);\n const [silent, setSilent] = useState(muted);\n\n const immersive = useFullscreen(shell);\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n const effectiveRate = rate ?? ownRate;\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n /**\n * Keep the rate on the element, and keep it across sources.\n *\n * `defaultPlaybackRate` is written alongside `playbackRate` because the load\n * algorithm copies the former onto the latter: writing only `playbackRate`\n * means the rate the viewer chose is silently lost on the next clip.\n */\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.defaultPlaybackRate = effectiveRate;\n node.playbackRate = effectiveRate;\n node.preservesPitch = !shiftPitch;\n }, [effectiveRate, shiftPitch, url]);\n\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.volume = volume;\n node.muted = silent;\n }, [volume, silent, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after\n * which it knows the real length. It runs at most once per source, and only\n * when the caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = video.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onProbed = (): void => {\n node.removeEventListener(\"timeupdate\", onProbed);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onProbed);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = video.current;\n if (!node || !url) return;\n if (node.paused) {\n void Promise.resolve(node.play())\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = video.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n const changeRate = (next: number): void => {\n if (rate === undefined) setOwnRate(next);\n onRateChange?.(next);\n };\n\n return (\n <div\n ref={shell}\n className={cn(styles.player, immersive.isFullscreen && styles.immersive, className)}\n {...rest}\n >\n <div className={styles.frame} style={{ aspectRatio }}>\n <video\n ref={video}\n className={styles.video}\n src={url ?? undefined}\n poster={poster}\n loop={loop}\n autoPlay={autoPlay}\n playsInline\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = video.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onVolumeChange={() => {\n const node = video.current;\n if (!node) return;\n setVolume(node.volume);\n setSilent(node.muted);\n }}\n onEnded={() => {\n setPlaying(false);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n >\n {tracks?.map((track) => (\n <track\n key={`${track.kind ?? \"captions\"}-${track.srcLang}-${track.src}`}\n kind={track.kind ?? \"captions\"}\n src={track.src}\n srcLang={track.srcLang}\n label={track.label}\n default={track.default}\n />\n ))}\n </video>\n </div>\n\n <div className={styles.chrome}>\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => setSilent((was) => !was)}\n disabled={disabled}\n aria-label={silent ? strings.unmute : strings.mute}\n title={silent ? strings.unmute : strings.mute}\n >\n {silent ? <VolumeX size={16} aria-hidden /> : <Volume2 size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.volume}\n min={0}\n max={1}\n step={0.05}\n value={silent ? 0 : volume}\n onChange={(event) => {\n const next = Number(event.target.value);\n setVolume(next);\n setSilent(next === 0);\n }}\n disabled={disabled}\n aria-label={strings.volume}\n />\n\n {rates.length > 0 && (\n <select\n className={styles.rate}\n value={effectiveRate}\n onChange={(event) => changeRate(Number(event.target.value))}\n disabled={disabled}\n aria-label={strings.rate}\n title={strings.rate}\n >\n {rates.map((option) => (\n <option key={option} value={option}>\n {rateLabel(option, locale)}\n </option>\n ))}\n </select>\n )}\n\n {fullscreen && immersive.supported && (\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => {\n void immersive.toggle().catch((error: unknown) => onError?.(error));\n }}\n disabled={disabled}\n aria-label={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n title={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n >\n {immersive.isFullscreen ? (\n <Minimize size={16} aria-hidden />\n ) : (\n <Maximize size={16} aria-hidden />\n )}\n </button>\n )}\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAsGA,IAAM,KAAU;CACZ,SAAS;EACL,MAAM;EACN,OAAO;EACP,MAAM;EACN,MAAM;EACN,QAAQ;EACR,QAAQ;EACR,MAAM;EACN,iBAAiB;EACjB,gBAAgB;CACpB;CACA,IAAI;EACA,MAAM;EACN,OAAO;EACP,MAAM;EACN,MAAM;EACN,QAAQ;EACR,QAAQ;EACR,MAAM;EACN,iBAAiB;EACjB,gBAAgB;CACpB;AACJ;AASA,SAAS,GAAU,GAAc,GAAgC;CAC7D,OAAO,GAAG,EAAK,eAAe,MAAW,OAAO,UAAU,OAAO,EAAE;AACvE;AAyBA,SAAgB,EAAY,EACxB,QACA,eACA,WACA,WACA,iBAAc,KAAK,GACnB,SACA,WAAQ,GACR,iBACA,gBAAa,IACb,gBAAa,IACb,cAAW,IACX,UAAO,IACP,WAAQ,IACR,YAAS,SACT,YACA,YACA,YACA,cAAW,IACX,eACA,GAAG,MACc;CACjB,IAAM,IAAU,GAAQ,IAClB,IAAQ,EAA8B,IAAI,GAC1C,IAAQ,EAAgC,IAAI,GAC5C,IAAS,EAAO,EAAK,GAErB,CAAC,GAAS,KAAc,EAAS,EAAK,GACtC,CAAC,GAAW,KAAgB,EAAS,CAAC,GACtC,CAAC,GAAW,KAAgB,EAAS,QAAwB,GAC7D,CAAC,GAAS,KAAc,EAAS,KAAQ,CAAC,GAC1C,CAAC,GAAQ,KAAa,EAAS,CAAC,GAChC,CAAC,GAAQ,KAAa,EAAS,CAAK,GAEpC,IAAY,EAAc,CAAK,GAC/B,KAAU,EAAa,OAAO,KAAQ,WAAW,OAAO,CAAG,GAC3D,IAAM,OAAO,KAAQ,WAAW,IAAM,IAEtC,IAAgB,KAAQ,GACxB,IAAU,OAAO,SAAS,CAAU,IAAK,IAAwB,GACjE,IAAW,OAAO,SAAS,CAAO,KAAK,IAAU;CAwBvD,AAtBA,QAAgB;EAIZ,AAHA,EAAa,CAAC,GACd,EAAW,EAAK,GAChB,EAAa,QAAwB,GACrC,EAAO,UAAU;CACrB,GAAG,CAAC,CAAG,CAAC,GASR,QAAgB;EACZ,IAAM,IAAO,EAAM;EACd,MACL,EAAK,sBAAsB,GAC3B,EAAK,eAAe,GACpB,EAAK,iBAAiB,CAAC;CAC3B,GAAG;EAAC;EAAe;EAAY;CAAG,CAAC,GAEnC,QAAgB;EACZ,IAAM,IAAO,EAAM;EACd,MACL,EAAK,SAAS,GACd,EAAK,QAAQ;CACjB,GAAG;EAAC;EAAQ;EAAQ;CAAG,CAAC;CASxB,IAAM,UAA6B;EAC/B,IAAM,IAAO,EAAM;EACnB,IAAI,CAAC,GAAM;EACX,IAAI,OAAO,SAAS,EAAK,QAAQ,GAAG;GAChC,EAAa,EAAK,WAAW,GAAI;GACjC;EACJ;EACA,IAAI,EAAO,WAAW,OAAO,SAAS,CAAU,GAAG;EACnD,EAAO,UAAU;EACjB,IAAM,UAAuB;GAGzB,AAFA,EAAK,oBAAoB,cAAc,CAAQ,GAC3C,OAAO,SAAS,EAAK,QAAQ,KAAG,EAAa,EAAK,WAAW,GAAI,GACrE,EAAK,cAAc;EACvB;EAEA,AADA,EAAK,iBAAiB,cAAc,CAAQ,GAC5C,EAAK,cAAc;CACvB,GAEM,WAAqB;EACvB,IAAM,IAAO,EAAM;EACf,IAAC,KAAS,GACd;OAAI,EAAK,QAAQ;IACb,QAAa,QAAQ,EAAK,KAAK,CAAC,CAAC,CAC5B,WAAW,EAAW,EAAI,CAAC,CAAC,CAC5B,OAAO,MAAmB,IAAU,CAAK,CAAC;IAC/C;GACJ;GAEA,AADA,EAAK,MAAM,GACX,EAAW,EAAK;EAFhB;CAGJ,GAEM,MAAQ,MAAqB;EAC/B,IAAM,IAAO,EAAM;EACf,AAAC,KAAS,MACd,EAAK,cAAc,IAAK,KACxB,EAAa,CAAE;CACnB,GAEM,MAAc,MAAuB;EAEvC,AADI,MAAS,KAAA,KAAW,EAAW,CAAI,GACvC,IAAe,CAAI;CACvB;CAEA,OACI,kBAAC,OAAD;EACI,KAAK;EACL,WAAW,EAAG,EAAO,QAAQ,EAAU,gBAAgB,EAAO,WAAW,EAAS;EAClF,GAAI;EAHR,UAAA,CAKI,kBAAC,OAAD;GAAK,WAAW,EAAO;GAAO,OAAO,EAAE,eAAY;GAC/C,UAAA,kBAAC,SAAD;IACI,KAAK;IACL,WAAW,EAAO;IAClB,KAAK,KAAO,KAAA;IACJ;IACF;IACI;IACV,aAAA;IACA,SAAQ;IACR,kBAAkB;IAClB,kBAAkB;IAClB,oBAAoB;KAChB,IAAM,IAAO,EAAM;KACnB,AAAI,KAAM,EAAa,EAAK,cAAc,GAAI;IAClD;IACA,cAAc,EAAW,EAAI;IAC7B,eAAe,EAAW,EAAK;IAC/B,sBAAsB;KAClB,IAAM,IAAO,EAAM;KACd,MACL,EAAU,EAAK,MAAM,GACrB,EAAU,EAAK,KAAK;IACxB;IACA,eAAe;KAEX,AADA,EAAW,EAAK,GAChB,IAAU;IACd;IACA,UAAU,MAAU,IAAU,CAAK;IAElC,UAAA,GAAQ,KAAK,MACV,kBAAC,SAAD;KAEI,MAAM,EAAM,QAAQ;KACpB,KAAK,EAAM;KACX,SAAS,EAAM;KACf,OAAO,EAAM;KACb,SAAS,EAAM;IAClB,GANQ,GAAG,EAAM,QAAQ,WAAW,GAAG,EAAM,QAAQ,GAAG,EAAM,KAM9D,CACJ;GACE,CAAA;EACN,CAAA,GAEL,kBAAC,OAAD;GAAK,WAAW,EAAO;GAAvB,UAAA;IACI,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,SAAS;KACT,UAAU,KAAY,CAAC;KACvB,cAAY,IAAU,EAAQ,QAAQ,EAAQ;KAC9C,OAAO,IAAU,EAAQ,QAAQ,EAAQ;KAExC,UAAU,EAAV,IAAW,KAAkC,IAAnC;MAAO,MAAM;MAAI,eAAA;KAAa,CAAiC;IACtE,CAAA;IAER,kBAAC,SAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,KAAK;KACL,KAAK,IAAW,KAAK,MAAM,CAAO,IAAI;KACtC,MAAM;KACN,OAAO,KAAK,IAAI,KAAK,MAAM,CAAS,GAAG,IAAW,KAAK,MAAM,CAAO,IAAI,CAAC;KACzE,WAAW,MAAU,GAAK,OAAO,EAAM,OAAO,KAAK,CAAC;KACpD,UAAU,KAAY,CAAC;KACvB,cAAY,EAAQ;KACpB,kBAAgB,GAAG,EAAe,CAAS,EAAE,KAAK,EAAe,CAAO;IAC3E,CAAA;IAED,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAxB,UAAA;MACI,kBAAC,QAAD,EAAA,UAAO,EAAe,CAAS,EAAQ,CAAA;MACvC,kBAAC,QAAD;OAAM,eAAY;OAAO,UAAA;MAAO,CAAA;MAChC,kBAAC,QAAD,EAAA,UAAO,EAAe,CAAO,EAAQ,CAAA;KACnC;;IAEN,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,eAAe,GAAW,MAAQ,CAAC,CAAG;KAC5B;KACV,cAAY,IAAS,EAAQ,SAAS,EAAQ;KAC9C,OAAO,IAAS,EAAQ,SAAS,EAAQ;KAExC,UAAS,EAAT,IAAU,IAAoC,GAArC;MAAS,MAAM;MAAI,eAAA;KAAa,CAAoC;IAC1E,CAAA;IAER,kBAAC,SAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,KAAK;KACL,KAAK;KACL,MAAM;KACN,OAAO,IAAS,IAAI;KACpB,WAAW,MAAU;MACjB,IAAM,IAAO,OAAO,EAAM,OAAO,KAAK;MAEtC,AADA,EAAU,CAAI,GACd,EAAU,MAAS,CAAC;KACxB;KACU;KACV,cAAY,EAAQ;IACvB,CAAA;IAEA,EAAM,SAAS,KACZ,kBAAC,UAAD;KACI,WAAW,EAAO;KAClB,OAAO;KACP,WAAW,MAAU,GAAW,OAAO,EAAM,OAAO,KAAK,CAAC;KAChD;KACV,cAAY,EAAQ;KACpB,OAAO,EAAQ;KAEd,UAAA,EAAM,KAAK,MACR,kBAAC,UAAD;MAAqB,OAAO;MACvB,UAAA,GAAU,GAAQ,CAAM;KACrB,GAFK,CAEL,CACX;IACG,CAAA;IAGX,KAAc,EAAU,aACrB,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,eAAe;MACX,EAAe,OAAO,CAAC,CAAC,OAAO,MAAmB,IAAU,CAAK,CAAC;KACtE;KACU;KACV,cACI,EAAU,eACJ,EAAQ,iBACR,EAAQ;KAElB,OACI,EAAU,eACJ,EAAQ,iBACR,EAAQ;KAGjB,UAAA,EAAU,eACP,kBAAC,IAAD;MAAU,MAAM;MAAI,eAAA;KAAa,CAAA,IAEjC,kBAAC,GAAD;MAAU,MAAM;MAAI,eAAA;KAAa,CAAA;IAEjC,CAAA;IAGX,KAAW,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAU,UAAA;IAAc,CAAA;GAC3D;EACJ,CAAA,CAAA;;AAEb"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("../../utils/compare-values.cjs"),n=require("./VirtualTable.module.cjs");let r=require("react"),i=require("react/jsx-runtime");function a(e){if(e===`right`)return n.default.alignRight;if(e===`center`)return n.default.alignCenter}function o({row:t,index:r,rowHeight:o,columns:s,onRowClick:c}){return(0,i.jsx)(`tr`,{"aria-rowindex":r+1,className:e.cn(n.default.tr,c&&n.default.clickable),style:{height:o},tabIndex:c?0:void 0,onClick:c?()=>c(t,r):void 0,onKeyDown:c?e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),c(t,r))}:void 0,children:s.map(o=>(0,i.jsx)(`td`,{className:e.cn(n.default.td,a(o.align)),style:{width:o.width},children:o.render?o.render(t,r):t[o.key]??null},String(o.key)))})}var s=(0,r.memo)(o);function c({data:o,columns:c,rowHeight:l,height:u,overscan:d=4,rowKey:f,initialSort:p,onRowClick:m,emptyMessage:h=`Nenhum registro encontrado.`,caption:g,scrollToIndex:_,className:v,...y}){let b=(0,r.useRef)(null),[x,S]=(0,r.useState)(0),[C,w]=(0,r.useState)(0),[T,E]=(0,r.useState)(p??null);(0,r.useEffect)(()=>{let e=b.current;if(!e||(w(e.clientHeight),typeof ResizeObserver>`u`))return;let t=new ResizeObserver(()=>w(e.clientHeight));return t.observe(e),()=>t.disconnect()},[]),(0,r.useEffect)(()=>{let e=b.current
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../../utils/compare-values.cjs"),n=require("./VirtualTable.module.cjs");let r=require("react"),i=require("react/jsx-runtime");function a(e){if(e===`right`)return n.default.alignRight;if(e===`center`)return n.default.alignCenter}function o({row:t,index:r,rowHeight:o,columns:s,onRowClick:c}){return(0,i.jsx)(`tr`,{"aria-rowindex":r+1,className:e.cn(n.default.tr,c&&n.default.clickable),style:{height:o},tabIndex:c?0:void 0,onClick:c?()=>c(t,r):void 0,onKeyDown:c?e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),c(t,r))}:void 0,children:s.map(o=>(0,i.jsx)(`td`,{className:e.cn(n.default.td,a(o.align)),style:{width:o.width},children:o.render?o.render(t,r):t[o.key]??null},String(o.key)))})}var s=(0,r.memo)(o);function c({data:o,columns:c,rowHeight:l,height:u,overscan:d=4,rowKey:f,initialSort:p,onRowClick:m,emptyMessage:h=`Nenhum registro encontrado.`,caption:g,scrollToIndex:_,className:v,...y}){let b=(0,r.useRef)(null),[x,S]=(0,r.useState)(0),[C,w]=(0,r.useState)(0),[T,E]=(0,r.useState)(p??null);(0,r.useEffect)(()=>{let e=b.current;if(!e||(w(e.clientHeight),typeof ResizeObserver>`u`))return;let t=new ResizeObserver(()=>w(e.clientHeight));return t.observe(e),()=>t.disconnect()},[]),(0,r.useEffect)(()=>{let e=b.current;e&&_!=null&&(e.scrollTop=Math.max(0,Math.min(_,o.length-1))*l)},[_,l,o.length]);let D=(0,r.useMemo)(()=>{if(!T)return o;let e=T.direction===`asc`?1:-1;return[...o].sort((n,r)=>t.compareValues(n[T.key],r[T.key])*e)},[o,T]),O=Math.max(0,Math.floor(x/l)-d),k=Math.ceil(C/l)+d*2,A=Math.min(D.length,O+k),j=D.slice(O,A),M=(0,r.useCallback)(e=>{E(t=>!t||t.key!==e?{key:e,direction:`asc`}:t.direction===`asc`?{key:e,direction:`desc`}:null);let t=b.current;t&&(t.scrollTop=0),S(0)},[]);return(0,i.jsx)(`div`,{ref:b,className:e.cn(n.default.scroll,v),style:{height:u},onScroll:e=>S(e.currentTarget.scrollTop),...y,children:(0,i.jsxs)(`table`,{className:n.default.table,"aria-rowcount":D.length,children:[g?(0,i.jsx)(`caption`,{className:n.default.caption,children:g}):null,(0,i.jsx)(`thead`,{className:n.default.head,children:(0,i.jsx)(`tr`,{children:c.map(t=>{let r=T?.key===t.key;return(0,i.jsx)(`th`,{scope:`col`,className:e.cn(n.default.th,a(t.align)),style:{width:t.width},"aria-sort":r?T?.direction===`asc`?`ascending`:`descending`:t.sortable?`none`:void 0,children:t.sortable?(0,i.jsxs)(`button`,{type:`button`,className:n.default.sortButton,onClick:()=>M(t.key),children:[t.header,(0,i.jsx)(`span`,{className:n.default.sortIndicator,"aria-hidden":!0,children:r?T?.direction===`asc`?`▲`:`▼`:`↕`})]}):t.header},String(t.key))})})}),(0,i.jsx)(`tbody`,{children:D.length===0?(0,i.jsx)(`tr`,{children:(0,i.jsx)(`td`,{className:n.default.emptyRow,colSpan:c.length,children:h})}):(0,i.jsxs)(i.Fragment,{children:[O>0&&(0,i.jsx)(`tr`,{"aria-hidden":!0,style:{height:O*l}}),j.map((e,t)=>{let n=O+t;return(0,i.jsx)(s,{row:e,index:n,rowHeight:l,columns:c,onRowClick:m},f?f(e,n):n)}),A<D.length&&(0,i.jsx)(`tr`,{"aria-hidden":!0,style:{height:(D.length-A)*l}})]})})]})})}exports.VirtualTable=c;
|
|
2
2
|
//# sourceMappingURL=VirtualTable.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VirtualTable.cjs","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count — windowing needs its own measurements\n * (rowHeight, height, overscan, scrollToIndex) on top of everything a table already\n * takes (data, columns, rowKey, initialSort, onRowClick, emptyMessage, caption). The\n * caption is not optional chrome — a virtualised grid without one is unreadable to a\n * screen reader.\n */\nimport {\n type HTMLAttributes,\n type ReactNode,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\ninterface VirtualTableRowProps<T> {\n row: T;\n index: number;\n rowHeight: number;\n columns: VirtualTableColumn<T>[];\n onRowClick: ((row: T, index: number) => void) | undefined;\n}\n\n/**\n * One windowed table row, rendered on its own so React can skip it.\n *\n * Scrolling changes state that lives inside `VirtualTable`, so without this\n * boundary every scroll event re-rendered every visible row and every cell in\n * them — including whatever `column.render` builds — rather than the one row\n * that entered the window.\n *\n * `columns` and `onRowClick` take part in the comparison because a cell renderer\n * closes over the app's state: leaving them out would freeze a row showing a\n * stale cell. An app that keeps its `columns` array stable (a module constant, or\n * `useMemo`) therefore gets the skip, and one that rebuilds it every render gets\n * exactly today's behaviour.\n *\n * @param props - The row, its position and the column contract.\n * @returns The table row element.\n */\nfunction VirtualTableRowImpl<T>({\n row,\n index,\n rowHeight,\n columns,\n onRowClick,\n}: VirtualTableRowProps<T>) {\n return (\n <tr\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={onRowClick ? () => onRowClick(row, index) : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n}\n\n/**\n * `memo` erases the generic, so the memoised component is re-typed as the\n * function it wraps. The runtime value is unchanged; only the signature is\n * restored.\n */\nconst VirtualTableRow = memo(VirtualTableRowImpl) as typeof VirtualTableRowImpl;\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <VirtualTableRow\n key={rowKey ? rowKey(row, index) : index}\n row={row}\n index={index}\n rowHeight={rowHeight}\n columns={columns}\n onRowClick={onRowClick}\n />\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VirtualTable.cjs","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count — windowing needs its own measurements\n * (rowHeight, height, overscan, scrollToIndex) on top of everything a table already\n * takes (data, columns, rowKey, initialSort, onRowClick, emptyMessage, caption). The\n * caption is not optional chrome — a virtualised grid without one is unreadable to a\n * screen reader.\n */\nimport {\n type HTMLAttributes,\n type ReactNode,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\ninterface VirtualTableRowProps<T> {\n row: T;\n index: number;\n rowHeight: number;\n columns: VirtualTableColumn<T>[];\n onRowClick: ((row: T, index: number) => void) | undefined;\n}\n\n/**\n * One windowed table row, rendered on its own so React can skip it.\n *\n * Scrolling changes state that lives inside `VirtualTable`, so without this\n * boundary every scroll event re-rendered every visible row and every cell in\n * them — including whatever `column.render` builds — rather than the one row\n * that entered the window.\n *\n * `columns` and `onRowClick` take part in the comparison because a cell renderer\n * closes over the app's state: leaving them out would freeze a row showing a\n * stale cell. An app that keeps its `columns` array stable (a module constant, or\n * `useMemo`) therefore gets the skip, and one that rebuilds it every render gets\n * exactly today's behaviour.\n *\n * @param props - The row, its position and the column contract.\n * @returns The table row element.\n */\nfunction VirtualTableRowImpl<T>({\n row,\n index,\n rowHeight,\n columns,\n onRowClick,\n}: VirtualTableRowProps<T>) {\n return (\n <tr\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={onRowClick ? () => onRowClick(row, index) : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n}\n\n/**\n * `memo` erases the generic, so the memoised component is re-typed as the\n * function it wraps. The runtime value is unchanged; only the signature is\n * restored.\n */\nconst VirtualTableRow = memo(VirtualTableRowImpl) as typeof VirtualTableRowImpl;\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <VirtualTableRow\n key={rowKey ? rowKey(row, index) : index}\n row={row}\n index={index}\n rowHeight={rowHeight}\n columns={columns}\n onRowClick={onRowClick}\n />\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -35,7 +35,7 @@ function h({ data: r, columns: p, rowHeight: h, height: g, overscan: _ = 4, rowK
|
|
|
35
35
|
return t.observe(e), () => t.disconnect();
|
|
36
36
|
}, []), a(() => {
|
|
37
37
|
let e = E.current;
|
|
38
|
-
|
|
38
|
+
e && C != null && (e.scrollTop = Math.max(0, Math.min(C, r.length - 1)) * h);
|
|
39
39
|
}, [
|
|
40
40
|
C,
|
|
41
41
|
h,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VirtualTable.js","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count — windowing needs its own measurements\n * (rowHeight, height, overscan, scrollToIndex) on top of everything a table already\n * takes (data, columns, rowKey, initialSort, onRowClick, emptyMessage, caption). The\n * caption is not optional chrome — a virtualised grid without one is unreadable to a\n * screen reader.\n */\nimport {\n type HTMLAttributes,\n type ReactNode,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\ninterface VirtualTableRowProps<T> {\n row: T;\n index: number;\n rowHeight: number;\n columns: VirtualTableColumn<T>[];\n onRowClick: ((row: T, index: number) => void) | undefined;\n}\n\n/**\n * One windowed table row, rendered on its own so React can skip it.\n *\n * Scrolling changes state that lives inside `VirtualTable`, so without this\n * boundary every scroll event re-rendered every visible row and every cell in\n * them — including whatever `column.render` builds — rather than the one row\n * that entered the window.\n *\n * `columns` and `onRowClick` take part in the comparison because a cell renderer\n * closes over the app's state: leaving them out would freeze a row showing a\n * stale cell. An app that keeps its `columns` array stable (a module constant, or\n * `useMemo`) therefore gets the skip, and one that rebuilds it every render gets\n * exactly today's behaviour.\n *\n * @param props - The row, its position and the column contract.\n * @returns The table row element.\n */\nfunction VirtualTableRowImpl<T>({\n row,\n index,\n rowHeight,\n columns,\n onRowClick,\n}: VirtualTableRowProps<T>) {\n return (\n <tr\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={onRowClick ? () => onRowClick(row, index) : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n}\n\n/**\n * `memo` erases the generic, so the memoised component is re-typed as the\n * function it wraps. The runtime value is unchanged; only the signature is\n * restored.\n */\nconst VirtualTableRow = memo(VirtualTableRowImpl) as typeof VirtualTableRowImpl;\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <VirtualTableRow\n key={rowKey ? rowKey(row, index) : index}\n row={row}\n index={index}\n rowHeight={rowHeight}\n columns={columns}\n onRowClick={onRowClick}\n />\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VirtualTable.js","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count — windowing needs its own measurements\n * (rowHeight, height, overscan, scrollToIndex) on top of everything a table already\n * takes (data, columns, rowKey, initialSort, onRowClick, emptyMessage, caption). The\n * caption is not optional chrome — a virtualised grid without one is unreadable to a\n * screen reader.\n */\nimport {\n type HTMLAttributes,\n type ReactNode,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\ninterface VirtualTableRowProps<T> {\n row: T;\n index: number;\n rowHeight: number;\n columns: VirtualTableColumn<T>[];\n onRowClick: ((row: T, index: number) => void) | undefined;\n}\n\n/**\n * One windowed table row, rendered on its own so React can skip it.\n *\n * Scrolling changes state that lives inside `VirtualTable`, so without this\n * boundary every scroll event re-rendered every visible row and every cell in\n * them — including whatever `column.render` builds — rather than the one row\n * that entered the window.\n *\n * `columns` and `onRowClick` take part in the comparison because a cell renderer\n * closes over the app's state: leaving them out would freeze a row showing a\n * stale cell. An app that keeps its `columns` array stable (a module constant, or\n * `useMemo`) therefore gets the skip, and one that rebuilds it every render gets\n * exactly today's behaviour.\n *\n * @param props - The row, its position and the column contract.\n * @returns The table row element.\n */\nfunction VirtualTableRowImpl<T>({\n row,\n index,\n rowHeight,\n columns,\n onRowClick,\n}: VirtualTableRowProps<T>) {\n return (\n <tr\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={onRowClick ? () => onRowClick(row, index) : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n}\n\n/**\n * `memo` erases the generic, so the memoised component is re-typed as the\n * function it wraps. The runtime value is unchanged; only the signature is\n * restored.\n */\nconst VirtualTableRow = memo(VirtualTableRowImpl) as typeof VirtualTableRowImpl;\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <VirtualTableRow\n key={rowKey ? rowKey(row, index) : index}\n row={row}\n index={index}\n rowHeight={rowHeight}\n columns={columns}\n onRowClick={onRowClick}\n />\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function e(e){let{growthbook:t}=e,n=new Set,r=!1;function i(){r
|
|
1
|
+
function e(e){let{growthbook:t}=e,n=new Set,r=!1;function i(){!r&&t.setRenderer&&(r=!0,t.setRenderer(()=>{for(let e of n)e()}))}return{isEnabled(e,n=!1){let r=t.isOn(e);return typeof r==`boolean`?r:n},get(e,n){return t.getFeatureValue(e,n)},onChange(e){return i(),n.add(e),()=>{n.delete(e)}}}}exports.createGrowthBookFeatureFlagsAdapter=e;
|
|
2
2
|
//# sourceMappingURL=growthbook-adapter.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"growthbook-adapter.cjs","names":[],"sources":["../../src/feature-flags/growthbook-adapter.ts"],"sourcesContent":["import type { FeatureFlagsAdapter, FlagValue } from \"./types\";\n\n/**\n * Minimal subset of [`@growthbook/growthbook`](https://docs.growthbook.io/lib/js)\n * used by the adapter. Pass a `GrowthBook` instance directly.\n */\nexport interface GrowthBookLike {\n isOn: (key: string) => boolean;\n getFeatureValue: <T>(key: string, defaultValue: T) => T;\n /** Registers a renderer fired whenever GrowthBook re-evaluates features. */\n setRenderer?: (renderer: () => void) => void;\n}\n\nexport interface CreateGrowthBookFeatureFlagsAdapterOptions {\n /** The GrowthBook instance. Required. */\n growthbook: GrowthBookLike;\n}\n\n/**\n * Build a [[FeatureFlagsAdapter]] backed by a GrowthBook instance. Apps\n * initialise GrowthBook themselves (so they can load features over the\n * network, set attributes, etc.) — the adapter just routes lookups.\n *\n * Mapping:\n * - `isEnabled(key, default)` → `growthbook.isOn(key)` (falls back to `default` if no value)\n * - `get(key, default)` → `growthbook.getFeatureValue(key, default)`\n * - `onChange(listener)` → `growthbook.setRenderer(listener)` (single-renderer SDK constraint)\n *\n * @example\n * import { GrowthBook } from \"@growthbook/growthbook\";\n * import { FeatureFlagsProvider, createGrowthBookFeatureFlagsAdapter } from \"tempest-react-sdk\";\n *\n * const gb = new GrowthBook({ apiHost: \"...\", clientKey: \"...\" });\n * await gb.loadFeatures();\n * const adapter = createGrowthBookFeatureFlagsAdapter({ growthbook: gb });\n *\n * <FeatureFlagsProvider adapter={adapter}><App /></FeatureFlagsProvider>;\n */\nexport function createGrowthBookFeatureFlagsAdapter(\n options: CreateGrowthBookFeatureFlagsAdapterOptions,\n): FeatureFlagsAdapter {\n const { growthbook } = options;\n const listeners = new Set<() => void>();\n let rendererInstalled = false;\n\n function ensureRenderer(): void {\n if (rendererInstalled || !growthbook.setRenderer) return;\n rendererInstalled = true;\n growthbook.setRenderer(() => {\n for (const listener of listeners) listener();\n });\n }\n\n return {\n isEnabled(key: string, defaultValue = false) {\n const value = growthbook.isOn(key);\n if (typeof value === \"boolean\") return value;\n return defaultValue;\n },\n get<T extends FlagValue = FlagValue>(key: string, defaultValue?: T): T {\n return growthbook.getFeatureValue(key, defaultValue as T);\n },\n onChange(listener) {\n ensureRenderer();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"],"mappings":"AAsCA,SAAgB,EACZ,EACmB,CACnB,GAAM,CAAE,cAAe,EACjB,EAAY,IAAI,IAClB,EAAoB,GAExB,SAAS,GAAuB,CACxB,
|
|
1
|
+
{"version":3,"file":"growthbook-adapter.cjs","names":[],"sources":["../../src/feature-flags/growthbook-adapter.ts"],"sourcesContent":["import type { FeatureFlagsAdapter, FlagValue } from \"./types\";\n\n/**\n * Minimal subset of [`@growthbook/growthbook`](https://docs.growthbook.io/lib/js)\n * used by the adapter. Pass a `GrowthBook` instance directly.\n */\nexport interface GrowthBookLike {\n isOn: (key: string) => boolean;\n getFeatureValue: <T>(key: string, defaultValue: T) => T;\n /** Registers a renderer fired whenever GrowthBook re-evaluates features. */\n setRenderer?: (renderer: () => void) => void;\n}\n\nexport interface CreateGrowthBookFeatureFlagsAdapterOptions {\n /** The GrowthBook instance. Required. */\n growthbook: GrowthBookLike;\n}\n\n/**\n * Build a [[FeatureFlagsAdapter]] backed by a GrowthBook instance. Apps\n * initialise GrowthBook themselves (so they can load features over the\n * network, set attributes, etc.) — the adapter just routes lookups.\n *\n * Mapping:\n * - `isEnabled(key, default)` → `growthbook.isOn(key)` (falls back to `default` if no value)\n * - `get(key, default)` → `growthbook.getFeatureValue(key, default)`\n * - `onChange(listener)` → `growthbook.setRenderer(listener)` (single-renderer SDK constraint)\n *\n * @example\n * import { GrowthBook } from \"@growthbook/growthbook\";\n * import { FeatureFlagsProvider, createGrowthBookFeatureFlagsAdapter } from \"tempest-react-sdk\";\n *\n * const gb = new GrowthBook({ apiHost: \"...\", clientKey: \"...\" });\n * await gb.loadFeatures();\n * const adapter = createGrowthBookFeatureFlagsAdapter({ growthbook: gb });\n *\n * <FeatureFlagsProvider adapter={adapter}><App /></FeatureFlagsProvider>;\n */\nexport function createGrowthBookFeatureFlagsAdapter(\n options: CreateGrowthBookFeatureFlagsAdapterOptions,\n): FeatureFlagsAdapter {\n const { growthbook } = options;\n const listeners = new Set<() => void>();\n let rendererInstalled = false;\n\n function ensureRenderer(): void {\n if (rendererInstalled || !growthbook.setRenderer) return;\n rendererInstalled = true;\n growthbook.setRenderer(() => {\n for (const listener of listeners) listener();\n });\n }\n\n return {\n isEnabled(key: string, defaultValue = false) {\n const value = growthbook.isOn(key);\n if (typeof value === \"boolean\") return value;\n return defaultValue;\n },\n get<T extends FlagValue = FlagValue>(key: string, defaultValue?: T): T {\n return growthbook.getFeatureValue(key, defaultValue as T);\n },\n onChange(listener) {\n ensureRenderer();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"],"mappings":"AAsCA,SAAgB,EACZ,EACmB,CACnB,GAAM,CAAE,cAAe,EACjB,EAAY,IAAI,IAClB,EAAoB,GAExB,SAAS,GAAuB,CACxB,IAAsB,EAAW,cACrC,EAAoB,GACpB,EAAW,gBAAkB,CACzB,IAAK,IAAM,KAAY,EAAW,EAAS,CAC/C,CAAC,EACL,CAEA,MAAO,CACH,UAAU,EAAa,EAAe,GAAO,CACzC,IAAM,EAAQ,EAAW,KAAK,CAAG,EAEjC,OADI,OAAO,GAAU,UAAkB,EAChC,CACX,EACA,IAAqC,EAAa,EAAqB,CACnE,OAAO,EAAW,gBAAgB,EAAK,CAAiB,CAC5D,EACA,SAAS,EAAU,CAGf,OAFA,EAAe,EACf,EAAU,IAAI,CAAQ,MACT,CACT,EAAU,OAAO,CAAQ,CAC7B,CACJ,CACJ,CACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"growthbook-adapter.js","names":[],"sources":["../../src/feature-flags/growthbook-adapter.ts"],"sourcesContent":["import type { FeatureFlagsAdapter, FlagValue } from \"./types\";\n\n/**\n * Minimal subset of [`@growthbook/growthbook`](https://docs.growthbook.io/lib/js)\n * used by the adapter. Pass a `GrowthBook` instance directly.\n */\nexport interface GrowthBookLike {\n isOn: (key: string) => boolean;\n getFeatureValue: <T>(key: string, defaultValue: T) => T;\n /** Registers a renderer fired whenever GrowthBook re-evaluates features. */\n setRenderer?: (renderer: () => void) => void;\n}\n\nexport interface CreateGrowthBookFeatureFlagsAdapterOptions {\n /** The GrowthBook instance. Required. */\n growthbook: GrowthBookLike;\n}\n\n/**\n * Build a [[FeatureFlagsAdapter]] backed by a GrowthBook instance. Apps\n * initialise GrowthBook themselves (so they can load features over the\n * network, set attributes, etc.) — the adapter just routes lookups.\n *\n * Mapping:\n * - `isEnabled(key, default)` → `growthbook.isOn(key)` (falls back to `default` if no value)\n * - `get(key, default)` → `growthbook.getFeatureValue(key, default)`\n * - `onChange(listener)` → `growthbook.setRenderer(listener)` (single-renderer SDK constraint)\n *\n * @example\n * import { GrowthBook } from \"@growthbook/growthbook\";\n * import { FeatureFlagsProvider, createGrowthBookFeatureFlagsAdapter } from \"tempest-react-sdk\";\n *\n * const gb = new GrowthBook({ apiHost: \"...\", clientKey: \"...\" });\n * await gb.loadFeatures();\n * const adapter = createGrowthBookFeatureFlagsAdapter({ growthbook: gb });\n *\n * <FeatureFlagsProvider adapter={adapter}><App /></FeatureFlagsProvider>;\n */\nexport function createGrowthBookFeatureFlagsAdapter(\n options: CreateGrowthBookFeatureFlagsAdapterOptions,\n): FeatureFlagsAdapter {\n const { growthbook } = options;\n const listeners = new Set<() => void>();\n let rendererInstalled = false;\n\n function ensureRenderer(): void {\n if (rendererInstalled || !growthbook.setRenderer) return;\n rendererInstalled = true;\n growthbook.setRenderer(() => {\n for (const listener of listeners) listener();\n });\n }\n\n return {\n isEnabled(key: string, defaultValue = false) {\n const value = growthbook.isOn(key);\n if (typeof value === \"boolean\") return value;\n return defaultValue;\n },\n get<T extends FlagValue = FlagValue>(key: string, defaultValue?: T): T {\n return growthbook.getFeatureValue(key, defaultValue as T);\n },\n onChange(listener) {\n ensureRenderer();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"],"mappings":";AAsCA,SAAgB,EACZ,GACmB;CACnB,IAAM,EAAE,kBAAe,GACjB,oBAAY,IAAI,IAAgB,GAClC,IAAoB;CAExB,SAAS,IAAuB;EACxB,
|
|
1
|
+
{"version":3,"file":"growthbook-adapter.js","names":[],"sources":["../../src/feature-flags/growthbook-adapter.ts"],"sourcesContent":["import type { FeatureFlagsAdapter, FlagValue } from \"./types\";\n\n/**\n * Minimal subset of [`@growthbook/growthbook`](https://docs.growthbook.io/lib/js)\n * used by the adapter. Pass a `GrowthBook` instance directly.\n */\nexport interface GrowthBookLike {\n isOn: (key: string) => boolean;\n getFeatureValue: <T>(key: string, defaultValue: T) => T;\n /** Registers a renderer fired whenever GrowthBook re-evaluates features. */\n setRenderer?: (renderer: () => void) => void;\n}\n\nexport interface CreateGrowthBookFeatureFlagsAdapterOptions {\n /** The GrowthBook instance. Required. */\n growthbook: GrowthBookLike;\n}\n\n/**\n * Build a [[FeatureFlagsAdapter]] backed by a GrowthBook instance. Apps\n * initialise GrowthBook themselves (so they can load features over the\n * network, set attributes, etc.) — the adapter just routes lookups.\n *\n * Mapping:\n * - `isEnabled(key, default)` → `growthbook.isOn(key)` (falls back to `default` if no value)\n * - `get(key, default)` → `growthbook.getFeatureValue(key, default)`\n * - `onChange(listener)` → `growthbook.setRenderer(listener)` (single-renderer SDK constraint)\n *\n * @example\n * import { GrowthBook } from \"@growthbook/growthbook\";\n * import { FeatureFlagsProvider, createGrowthBookFeatureFlagsAdapter } from \"tempest-react-sdk\";\n *\n * const gb = new GrowthBook({ apiHost: \"...\", clientKey: \"...\" });\n * await gb.loadFeatures();\n * const adapter = createGrowthBookFeatureFlagsAdapter({ growthbook: gb });\n *\n * <FeatureFlagsProvider adapter={adapter}><App /></FeatureFlagsProvider>;\n */\nexport function createGrowthBookFeatureFlagsAdapter(\n options: CreateGrowthBookFeatureFlagsAdapterOptions,\n): FeatureFlagsAdapter {\n const { growthbook } = options;\n const listeners = new Set<() => void>();\n let rendererInstalled = false;\n\n function ensureRenderer(): void {\n if (rendererInstalled || !growthbook.setRenderer) return;\n rendererInstalled = true;\n growthbook.setRenderer(() => {\n for (const listener of listeners) listener();\n });\n }\n\n return {\n isEnabled(key: string, defaultValue = false) {\n const value = growthbook.isOn(key);\n if (typeof value === \"boolean\") return value;\n return defaultValue;\n },\n get<T extends FlagValue = FlagValue>(key: string, defaultValue?: T): T {\n return growthbook.getFeatureValue(key, defaultValue as T);\n },\n onChange(listener) {\n ensureRenderer();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"],"mappings":";AAsCA,SAAgB,EACZ,GACmB;CACnB,IAAM,EAAE,kBAAe,GACjB,oBAAY,IAAI,IAAgB,GAClC,IAAoB;CAExB,SAAS,IAAuB;EACxB,MAAsB,EAAW,gBACrC,IAAoB,IACpB,EAAW,kBAAkB;GACzB,KAAK,IAAM,KAAY,GAAW,EAAS;EAC/C,CAAC;CACL;CAEA,OAAO;EACH,UAAU,GAAa,IAAe,IAAO;GACzC,IAAM,IAAQ,EAAW,KAAK,CAAG;GAEjC,OADI,OAAO,KAAU,YAAkB,IAChC;EACX;EACA,IAAqC,GAAa,GAAqB;GACnE,OAAO,EAAW,gBAAgB,GAAK,CAAiB;EAC5D;EACA,SAAS,GAAU;GAGf,OAFA,EAAe,GACf,EAAU,IAAI,CAAQ,SACT;IACT,EAAU,OAAO,CAAQ;GAC7B;EACJ;CACJ;AACJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let e=require("react");var t=[`a[href]`,`button:not([disabled])`,`textarea:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`[tabindex]:not([tabindex='-1'])`].join(`,`);function n(n,r){(0,e.useEffect)(()=>{if(!r)return;let e=n.current;if(!e)return;let i=document.activeElement;function a(){return Array.from(e?.querySelectorAll(t)??[]).filter(e=>{if(e.hasAttribute(`aria-hidden`))return!1;let t=typeof window<`u`?window.getComputedStyle(e):null;return!
|
|
1
|
+
let e=require("react");var t=[`a[href]`,`button:not([disabled])`,`textarea:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`[tabindex]:not([tabindex='-1'])`].join(`,`);function n(n,r){(0,e.useEffect)(()=>{if(!r)return;let e=n.current;if(!e)return;let i=document.activeElement;function a(){return Array.from(e?.querySelectorAll(t)??[]).filter(e=>{if(e.hasAttribute(`aria-hidden`))return!1;let t=typeof window<`u`?window.getComputedStyle(e):null;return!t||t.display!==`none`&&t.visibility!==`hidden`})}function o(t){if(t.key!==`Tab`)return;let n=a();if(n.length===0){t.preventDefault();return}let r=n[0],i=n[n.length-1],o=document.activeElement;t.shiftKey?(o===r||!o||!e?.contains(o))&&(t.preventDefault(),i.focus()):o===i&&(t.preventDefault(),r.focus())}return a()[0]?.focus(),document.addEventListener(`keydown`,o),()=>{document.removeEventListener(`keydown`,o),i?.focus?.()}},[n,r])}exports.useFocusTrap=n;
|
|
2
2
|
//# sourceMappingURL=use-focus-trap.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-focus-trap.cjs","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\n\nconst FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true. Cycles\n * Tab and Shift+Tab between the first and last focusable descendants. Pair\n * with Modal/Drawer for fully-accessible overlays.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n function getFocusable(): HTMLElement[] {\n return Array.from(\n container?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [],\n ).filter((el) => {\n if (el.hasAttribute(\"aria-hidden\")) return false;\n const style = typeof window !== \"undefined\" ? window.getComputedStyle(el) : null;\n if (style && (style.display === \"none\" || style.visibility === \"hidden\")) {\n return false;\n }\n return true;\n });\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\") return;\n const elements = getFocusable();\n if (elements.length === 0) {\n event.preventDefault();\n return;\n }\n const first = elements[0]!;\n const last = elements[elements.length - 1]!;\n const current = document.activeElement as HTMLElement | null;\n if (event.shiftKey) {\n if (current === first || !current || !container?.contains(current)) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (current === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n const focusable = getFocusable();\n focusable[0]?.focus();\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n previouslyFocused?.focus?.();\n };\n }, [containerRef, active]);\n}\n"],"mappings":"uBAGA,IAAM,EAAqB,CACvB,UACA,yBACA,2BACA,wBACA,yBACA,iCACJ,CAAC,CAAC,KAAK,GAAG,EAOV,SAAgB,EAAa,EAA6C,EAAuB,EAC7F,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAQ,OACb,IAAM,EAAY,EAAa,QAC/B,GAAI,CAAC,EAAW,OAEhB,IAAM,EAAoB,SAAS,cAEnC,SAAS,GAA8B,CACnC,OAAO,MAAM,KACT,GAAW,iBAA8B,CAAkB,GAAK,CAAC,CACrE,CAAC,CAAC,OAAQ,GAAO,CACb,GAAI,EAAG,aAAa,aAAa,EAAG,MAAO,GAC3C,IAAM,EAAQ,OAAO,OAAW,IAAc,OAAO,iBAAiB,CAAE,EAAI,KAI5E,
|
|
1
|
+
{"version":3,"file":"use-focus-trap.cjs","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\n\nconst FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true. Cycles\n * Tab and Shift+Tab between the first and last focusable descendants. Pair\n * with Modal/Drawer for fully-accessible overlays.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n function getFocusable(): HTMLElement[] {\n return Array.from(\n container?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [],\n ).filter((el) => {\n if (el.hasAttribute(\"aria-hidden\")) return false;\n const style = typeof window !== \"undefined\" ? window.getComputedStyle(el) : null;\n if (style && (style.display === \"none\" || style.visibility === \"hidden\")) {\n return false;\n }\n return true;\n });\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\") return;\n const elements = getFocusable();\n if (elements.length === 0) {\n event.preventDefault();\n return;\n }\n const first = elements[0]!;\n const last = elements[elements.length - 1]!;\n const current = document.activeElement as HTMLElement | null;\n if (event.shiftKey) {\n if (current === first || !current || !container?.contains(current)) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (current === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n const focusable = getFocusable();\n focusable[0]?.focus();\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n previouslyFocused?.focus?.();\n };\n }, [containerRef, active]);\n}\n"],"mappings":"uBAGA,IAAM,EAAqB,CACvB,UACA,yBACA,2BACA,wBACA,yBACA,iCACJ,CAAC,CAAC,KAAK,GAAG,EAOV,SAAgB,EAAa,EAA6C,EAAuB,EAC7F,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAQ,OACb,IAAM,EAAY,EAAa,QAC/B,GAAI,CAAC,EAAW,OAEhB,IAAM,EAAoB,SAAS,cAEnC,SAAS,GAA8B,CACnC,OAAO,MAAM,KACT,GAAW,iBAA8B,CAAkB,GAAK,CAAC,CACrE,CAAC,CAAC,OAAQ,GAAO,CACb,GAAI,EAAG,aAAa,aAAa,EAAG,MAAO,GAC3C,IAAM,EAAQ,OAAO,OAAW,IAAc,OAAO,iBAAiB,CAAE,EAAI,KAI5E,MAHI,IAAU,EAAM,UAAY,QAAU,EAAM,aAAe,QAInE,CAAC,CACL,CAEA,SAAS,EAAc,EAA4B,CAC/C,GAAI,EAAM,MAAQ,MAAO,OACzB,IAAM,EAAW,EAAa,EAC9B,GAAI,EAAS,SAAW,EAAG,CACvB,EAAM,eAAe,EACrB,MACJ,CACA,IAAM,EAAQ,EAAS,GACjB,EAAO,EAAS,EAAS,OAAS,GAClC,EAAU,SAAS,cACrB,EAAM,UACF,IAAY,GAAS,CAAC,GAAW,CAAC,GAAW,SAAS,CAAO,KAC7D,EAAM,eAAe,EACrB,EAAK,MAAM,GAGX,IAAY,IACZ,EAAM,eAAe,EACrB,EAAM,MAAM,EAGxB,CAKA,OAFA,EAAA,CAAA,CAAU,EAAE,EAAE,MAAM,EACpB,SAAS,iBAAiB,UAAW,CAAa,MACrC,CACT,SAAS,oBAAoB,UAAW,CAAa,EACrD,GAAmB,QAAQ,CAC/B,CACJ,EAAG,CAAC,EAAc,CAAM,CAAC,CAC7B"}
|
|
@@ -18,7 +18,7 @@ function n(n, r) {
|
|
|
18
18
|
return Array.from(e?.querySelectorAll(t) ?? []).filter((e) => {
|
|
19
19
|
if (e.hasAttribute("aria-hidden")) return !1;
|
|
20
20
|
let t = typeof window < "u" ? window.getComputedStyle(e) : null;
|
|
21
|
-
return !
|
|
21
|
+
return !t || t.display !== "none" && t.visibility !== "hidden";
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
function o(t) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-focus-trap.js","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\n\nconst FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true. Cycles\n * Tab and Shift+Tab between the first and last focusable descendants. Pair\n * with Modal/Drawer for fully-accessible overlays.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n function getFocusable(): HTMLElement[] {\n return Array.from(\n container?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [],\n ).filter((el) => {\n if (el.hasAttribute(\"aria-hidden\")) return false;\n const style = typeof window !== \"undefined\" ? window.getComputedStyle(el) : null;\n if (style && (style.display === \"none\" || style.visibility === \"hidden\")) {\n return false;\n }\n return true;\n });\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\") return;\n const elements = getFocusable();\n if (elements.length === 0) {\n event.preventDefault();\n return;\n }\n const first = elements[0]!;\n const last = elements[elements.length - 1]!;\n const current = document.activeElement as HTMLElement | null;\n if (event.shiftKey) {\n if (current === first || !current || !container?.contains(current)) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (current === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n const focusable = getFocusable();\n focusable[0]?.focus();\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n previouslyFocused?.focus?.();\n };\n }, [containerRef, active]);\n}\n"],"mappings":";;AAGA,IAAM,IAAqB;CACvB;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC,CAAC,KAAK,GAAG;AAOV,SAAgB,EAAa,GAA6C,GAAuB;CAC7F,QAAgB;EACZ,IAAI,CAAC,GAAQ;EACb,IAAM,IAAY,EAAa;EAC/B,IAAI,CAAC,GAAW;EAEhB,IAAM,IAAoB,SAAS;EAEnC,SAAS,IAA8B;GACnC,OAAO,MAAM,KACT,GAAW,iBAA8B,CAAkB,KAAK,CAAC,CACrE,CAAC,CAAC,QAAQ,MAAO;IACb,IAAI,EAAG,aAAa,aAAa,GAAG,OAAO;IAC3C,IAAM,IAAQ,OAAO,SAAW,MAAc,OAAO,iBAAiB,CAAE,IAAI;IAI5E,
|
|
1
|
+
{"version":3,"file":"use-focus-trap.js","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\n\nconst FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true. Cycles\n * Tab and Shift+Tab between the first and last focusable descendants. Pair\n * with Modal/Drawer for fully-accessible overlays.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n function getFocusable(): HTMLElement[] {\n return Array.from(\n container?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [],\n ).filter((el) => {\n if (el.hasAttribute(\"aria-hidden\")) return false;\n const style = typeof window !== \"undefined\" ? window.getComputedStyle(el) : null;\n if (style && (style.display === \"none\" || style.visibility === \"hidden\")) {\n return false;\n }\n return true;\n });\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\") return;\n const elements = getFocusable();\n if (elements.length === 0) {\n event.preventDefault();\n return;\n }\n const first = elements[0]!;\n const last = elements[elements.length - 1]!;\n const current = document.activeElement as HTMLElement | null;\n if (event.shiftKey) {\n if (current === first || !current || !container?.contains(current)) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (current === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n const focusable = getFocusable();\n focusable[0]?.focus();\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n previouslyFocused?.focus?.();\n };\n }, [containerRef, active]);\n}\n"],"mappings":";;AAGA,IAAM,IAAqB;CACvB;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC,CAAC,KAAK,GAAG;AAOV,SAAgB,EAAa,GAA6C,GAAuB;CAC7F,QAAgB;EACZ,IAAI,CAAC,GAAQ;EACb,IAAM,IAAY,EAAa;EAC/B,IAAI,CAAC,GAAW;EAEhB,IAAM,IAAoB,SAAS;EAEnC,SAAS,IAA8B;GACnC,OAAO,MAAM,KACT,GAAW,iBAA8B,CAAkB,KAAK,CAAC,CACrE,CAAC,CAAC,QAAQ,MAAO;IACb,IAAI,EAAG,aAAa,aAAa,GAAG,OAAO;IAC3C,IAAM,IAAQ,OAAO,SAAW,MAAc,OAAO,iBAAiB,CAAE,IAAI;IAI5E,OAHI,MAAU,EAAM,YAAY,UAAU,EAAM,eAAe;GAInE,CAAC;EACL;EAEA,SAAS,EAAc,GAA4B;GAC/C,IAAI,EAAM,QAAQ,OAAO;GACzB,IAAM,IAAW,EAAa;GAC9B,IAAI,EAAS,WAAW,GAAG;IACvB,EAAM,eAAe;IACrB;GACJ;GACA,IAAM,IAAQ,EAAS,IACjB,IAAO,EAAS,EAAS,SAAS,IAClC,IAAU,SAAS;GACzB,AAAI,EAAM,YACF,MAAY,KAAS,CAAC,KAAW,CAAC,GAAW,SAAS,CAAO,OAC7D,EAAM,eAAe,GACrB,EAAK,MAAM,KAGX,MAAY,MACZ,EAAM,eAAe,GACrB,EAAM,MAAM;EAGxB;EAKA,OAFA,EAAA,CAAA,CAAU,EAAE,EAAE,MAAM,GACpB,SAAS,iBAAiB,WAAW,CAAa,SACrC;GAET,AADA,SAAS,oBAAoB,WAAW,CAAa,GACrD,GAAmB,QAAQ;EAC/B;CACJ,GAAG,CAAC,GAAc,CAAM,CAAC;AAC7B"}
|